Compare commits

..

30 Commits

Author SHA1 Message Date
Bel LaPointe
617785ad51 f u wannabe cors 2024-12-18 19:45:45 -07:00
Bel LaPointe
d2fa707628 tests pass but LOTS of todos 2024-12-16 17:29:27 -07:00
bel
421331eb71 i have a plan but also realize event state computation CANNOT create events because itll evalulate many times 2024-12-15 22:28:26 -07:00
Bel LaPointe
c6ffa12354 grr how do trial 2024-12-15 19:03:35 -07:00
Bel LaPointe
58fae19522 stub codename trial, accuse events 2024-12-15 18:43:15 -07:00
Bel LaPointe
4abac89472 use codename.Consumed instead of kills to know whether codename is available for murdering 2024-12-15 18:36:19 -07:00
Bel LaPointe
15078a626d Global-1 to Codename+200 2024-12-15 18:25:47 -07:00
Bel LaPointe
02dc21c124 todo 2024-12-15 17:53:38 -07:00
Bel LaPointe
4d9abef04c todo 2024-12-15 16:40:59 -07:00
Bel LaPointe
557e1ec6d4 accept ?uuid=X instead of cookie 2024-12-15 14:39:14 -07:00
Bel LaPointe
3095d54f99 testapi accepts ?uuid instead of cookie.uuid 2024-12-15 14:34:59 -07:00
Bel LaPointe
d6db4a8a58 oops cross platform testing doesnt play nice with cookies 2024-12-15 14:33:18 -07:00
Bel LaPointe
d36ef7b629 todo.yaml 2024-12-15 14:29:41 -07:00
Bel LaPointe
ab3a549f78 cors pls 2024-12-15 14:24:20 -07:00
Bel LaPointe
bf3b341b69 GameReset event 2024-12-15 14:17:07 -07:00
Bel LaPointe
54116131b3 stub GameReset event 2024-12-15 14:09:54 -07:00
Bel LaPointe
57dd66e510 impl first game reset but realize it must be an event to broadcast to all players 2024-12-15 14:06:08 -07:00
Bel LaPointe
f1282f588d impl ws kill 2024-12-15 13:56:59 -07:00
Bel LaPointe
706d55631b games.UserByName 2024-12-15 13:50:27 -07:00
Bel LaPointe
0b22ba4bd2 random name generation done 2024-12-15 13:41:57 -07:00
Bel LaPointe
f7a303168a GameByName doesnt take uid 2024-12-15 13:26:28 -07:00
Bel LaPointe
37291e68aa ugs listen needs love 2024-12-15 13:17:36 -07:00
Bel LaPointe
5109dc9fdb test usergameserver.State after game completes WOO 2024-12-15 13:16:04 -07:00
Bel LaPointe
fc899056e0 test game state after some kills 2024-12-15 13:13:03 -07:00
Bel LaPointe
8d73f97c3a bugfix if victim was targeting killer then killer cant target itself 2024-12-15 13:03:37 -07:00
Bel LaPointe
598cb0684c test UserGameState of freshly started game 2024-12-15 12:52:10 -07:00
Bel LaPointe
aba5225ed2 unittest not impls 2024-12-15 12:42:53 -07:00
Bel LaPointe
f3f70e10f4 test UserGameServer unstarted state has all empty players 2024-12-15 12:39:36 -07:00
Bel LaPointe
1e4198b291 drop temp fail 2024-12-15 12:27:17 -07:00
Bel LaPointe
c760dac44b ws blindly pushes public, private and lets UserGameServer limit visibility of user 2024-12-15 12:26:30 -07:00
12 changed files with 1217 additions and 123 deletions

249
cmd/server/adjectives.txt Normal file
View File

@@ -0,0 +1,249 @@
amazing
astounding
adorable
astonishing
awesome
agreeable
admirable
affectionate
artistic
animated
beautiful
brave
brilliant
big
bouncy
bright
busy
bold
blissful
bubbly
caring
clever
courageous
creative
cute
cheerful
charming
confident
curious
content
daring
delightful
determined
diligent
dynamic
decisive
dazzling
dependable
dramatic
dreamy
eager
earnest
effervescent
elegant
energetic
enchanting
enthusiastic
excellent
exciting
expressive
fair
faithful
fantastic
fearless
festive
friendly
funny
feisty
forgiving
focused
gentle
giving
glad
gleeful
glorious
good
graceful
grateful
great
gregarious
happy
helpful
honest
hopeful
hungry
hilarious
honorable
huggable
humble
hardworking
important
impressive
incredible
independent
inquisitive
intelligent
interesting
imaginative
inventive
inspiring
joyful
jolly
jovial
judicious
just
jaunty
jubilant
jazzy
jumpy
joking
kind
knowledgeable
keen
kooky
kindhearted
karate-chopping
krazy
kicking
kissable
kidding
loving
laughing
likable
lucky
lovely
light
little
loud
lazy
lanky
merry
magical
marvellous
mysterious
mischievous
masterful
mindful
melodic
mighty
motivated
nice
nimble
nifty
noisy
nutty
nautical
noteworthy
nosey
neat
nourished
odd
old
obedient
outstanding
opinionated
optimistic
orderly
outgoing
overjoyed
organized
perfect
playful
pleasant
polite
powerful
peaceful
patient
proud
puzzled
perky
quirky
quick
queenly
quaint
qualified
quizzical
quaint
quiet
quirky
quacking
rainy
rambunctious
respectful
right
responsible
ripe
rustic
rotten
rhythmic
righteous
silly
sweet
smart
smiling
strong
super
skillful
sleepy
scented
spotless
thankful
tired
tasty
talented
thoughtful
tremendous
terrific
truthful
tough
trustworthy
unique
understanding
unusual
upbeat
useful
uplifting
unafraid
universal
unlimited
unselfish
victorious
vivacious
valuable
vibrant
versatile
virtuous
visionary
vocal
vivacious
valiant
wonderful
whimsical
welcoming
witty
wise
wild
warm
wacky
willing
watchful
xenial
xeric
yummy
yellow
yippee
yappy
young
yucky
yummy
youthful
yakky
zany
zesty
zen
zealous
zingy
zippy
zonal
zonked

100
cmd/server/animals.txt Normal file
View File

@@ -0,0 +1,100 @@
lion
tiger
goat
horse
donkey
dog
cat
pig
cow
elephant
giraffe
kangaroo
koala
deer
moose
sheep
zebra
bear
wolf
fox
otter
raccoon
squirrel
bat
chimpanzee
gorilla
orangutan
lemur
panda
red panda
hippopotamus
rhinoceros
camel
llama
alpaca
ferret
hedgehog
skunk
beaver
walrus
seal
dolphin
whale
bat
eagle
hawk
falcon
owl
parrot
crow
raven
pigeon
dove
swan
goose
duck
chicken
turkey
peacock
ostrich
emu
snake
lizard
turtle
crocodile
alligator
chameleon
gecko
shark
dolphin
whale
octopus
lobster
crab
shrimp
clam
oyster
ant
bee
butterfly
caterpillar
cricket
grasshopper
ladybug
mosquito
scorpion
spider
worm
snail
jellyfish
starfish
sponge
platypus
koala
tasmanian devil
kangaroo
wombat
wallaby
meerkat
lemming

View File

@@ -11,6 +11,7 @@ import (
"slices"
"strings"
"time"
"unicode"
"github.com/google/uuid"
)
@@ -62,8 +63,23 @@ func (games Games) GamesForUser(ctx context.Context, id string) ([]string, error
}
func (games Games) UpdateUserName(ctx context.Context, id, name string) error {
var n int
if err := games.db.Query(ctx, func(rows *sql.Rows) error {
return rows.Scan(&n)
}, `SELECT COUNT(uuid) FROM users WHERE uuid=?`, id); err != nil {
return err
}
if n > 0 {
return games.db.Exec(ctx, `UPDATE users SET name=? WHERE uuid=?`, name, id)
}
return games.db.Exec(ctx, `INSERT INTO users (uuid, name) VALUES (?, ?)`, id, name)
}
//go:embed adjectives.txt
var namesAdjectives string
//go:embed animals.txt
var namesAnimals string
func (games Games) UserName(ctx context.Context, id string) (string, error) {
result := ""
@@ -74,9 +90,44 @@ func (games Games) UserName(ctx context.Context, id string) (string, error) {
FROM users
WHERE users.uuid=?
`, id)
if result == "" {
adjectives := strings.Fields(namesAdjectives)
animals := strings.Split(namesAnimals, "\n")
animals = slices.DeleteFunc(animals, func(s string) bool { return s == "" })
name := strings.Title(fmt.Sprintf("%s %s", adjectives[rand.Intn(len(adjectives))], animals[rand.Intn(len(animals))]))
if err := games.UpdateUserName(ctx, id, name); err != nil {
return "", err
}
return games.UserName(ctx, id)
}
return result, err
}
func (games Games) UserByName(ctx context.Context, gid, name string) (string, error) {
result := ""
err := games.db.Query(ctx, func(rows *sql.Rows) error {
return rows.Scan(&result)
}, `
SELECT users.uuid
FROM players
JOIN users ON players.user_uuid=users.uuid
WHERE players.game_uuid=? AND users.name=?
`, gid, name)
return result, err
}
func (a KillWord) Empty() bool {
return a == (KillWord{})
}
func (a Assignment) Empty() bool {
return len(a.Public) == 0 && len(a.Private) == 0
}
func (s PlayerState) Empty() bool {
return len(s.Kills) == 0 && s.KillWords.Empty()
}
func (s PlayerState) Points() int {
points := 0
for _, kill := range s.Kills {
@@ -85,28 +136,47 @@ func (s PlayerState) Points() int {
return points
}
func (games Games) GameByName(ctx context.Context, uid, name string) (string, error) {
func (games Games) GameName(ctx context.Context, id string) (string, error) {
var result string
err := games.db.Query(ctx, func(rows *sql.Rows) error {
return rows.Scan(&result)
}, `
SELECT
players.game_uuid
games.name
FROM
players
JOIN games ON players.game_uuid=games.uuid
WHERE players.user_uuid=? AND games.name=?
games
WHERE games.uuid=?
ORDER BY games.updated DESC
LIMIT 1
`, uid, name)
`, id)
return result, err
}
func (games Games) GameByName(ctx context.Context, name string) (string, error) {
var result string
err := games.db.Query(ctx, func(rows *sql.Rows) error {
return rows.Scan(&result)
}, `
SELECT
games.uuid
FROM
games
WHERE games.name=?
ORDER BY games.updated DESC
LIMIT 1
`, name)
return result, err
}
type (
GameState struct {
ID string
Started bool
Completed time.Time
Players map[string]PlayerState
Trial Trial
}
PlayerState struct {
@@ -121,7 +191,7 @@ type (
}
KillWords struct {
Global KillWord
Codename Codename
Assigned time.Time
Assignee string
@@ -129,6 +199,11 @@ type (
Assignment Assignment
}
Codename struct {
KillWord KillWord
Consumed bool
}
Assignment struct {
Public []KillWord
Private []KillWord
@@ -139,6 +214,12 @@ type (
Points int
}
Trial struct {
Prosecutor string
Defendant string
Word string
}
EventType int
EventPlayerJoin struct {
@@ -163,6 +244,29 @@ type (
KillWord KillWord
AllKillWords AllKillWords
}
EventGameReset struct {
Type EventType
Timestamp time.Time
ID string
}
EventCodenameAccusal struct {
Type EventType
Timestamp time.Time
Prosecutor string
Defendant string
Word string
}
EventCodenameTrial struct {
Type EventType
Timestamp time.Time
Guilty bool
}
EventNotification struct {
Type EventType
Timestamp time.Time
Recipient string
Message string
}
AllKillWords map[string]KillWords
)
@@ -171,6 +275,10 @@ const (
PlayerLeave
GameComplete
AssignmentRotation
GameReset
CodenameAccusal
CodenameTrial
Notification
)
type Event interface{ event() }
@@ -179,6 +287,10 @@ func (EventPlayerJoin) event() {}
func (EventPlayerLeave) event() {}
func (EventGameComplete) event() {}
func (EventAssignmentRotation) event() {}
func (EventGameReset) event() {}
func (EventCodenameAccusal) event() {}
func (EventCodenameTrial) event() {}
func (EventNotification) event() {}
func EventWithTime(event Event, t time.Time) Event {
switch e := event.(type) {
@@ -194,6 +306,18 @@ func EventWithTime(event Event, t time.Time) Event {
case EventAssignmentRotation:
e.Timestamp = t
event = e
case EventGameReset:
e.Timestamp = t
event = e
case EventCodenameAccusal:
e.Timestamp = t
event = e
case EventCodenameTrial:
e.Timestamp = t
event = e
case EventNotification:
e.Timestamp = t
event = e
}
return event
}
@@ -244,12 +368,28 @@ func parseEvent(b []byte, timestamp time.Time) (Event, error) {
var v EventAssignmentRotation
err := json.Unmarshal(b, &v)
return EventWithTime(v, timestamp), err
case GameReset:
var v EventGameReset
err := json.Unmarshal(b, &v)
return EventWithTime(v, timestamp), err
case CodenameAccusal:
var v EventCodenameAccusal
err := json.Unmarshal(b, &v)
return EventWithTime(v, timestamp), err
case CodenameTrial:
var v EventCodenameTrial
err := json.Unmarshal(b, &v)
return EventWithTime(v, timestamp), err
case Notification:
var v EventNotification
err := json.Unmarshal(b, &v)
return EventWithTime(v, timestamp), err
}
return nil, fmt.Errorf("unknown event type %d: %s", peek.Type, b)
}
func (games Games) GameState(ctx context.Context, id string) (GameState, error) {
result := GameState{Players: map[string]PlayerState{}}
result := GameState{ID: id, Players: map[string]PlayerState{}}
events, err := games.GameEvents(ctx, id, time.Time{})
if err != nil {
@@ -257,19 +397,16 @@ func (games Games) GameState(ctx context.Context, id string) (GameState, error)
}
for _, event := range events {
switch event.(type) {
switch e := event.(type) {
case EventPlayerJoin:
playerJoin := event.(EventPlayerJoin)
result.Players[playerJoin.ID] = PlayerState{}
result.Players[e.ID] = PlayerState{}
case EventPlayerLeave:
playerLeave := event.(EventPlayerLeave)
delete(result.Players, playerLeave.ID)
delete(result.Players, e.ID)
case EventGameComplete:
gameComplete := event.(EventGameComplete)
result.Completed = gameComplete.Timestamp
result.Completed = e.Timestamp
case EventAssignmentRotation:
result.Started = true
assignmentRotation := event.(EventAssignmentRotation)
assignmentRotation := e
if killer, ok := result.Players[assignmentRotation.Killer]; !ok {
} else if _, ok := result.Players[assignmentRotation.Victim]; !ok {
@@ -292,6 +429,48 @@ func (games Games) GameState(ctx context.Context, id string) (GameState, error)
player.KillWords = v
result.Players[k] = player
}
case EventCodenameAccusal:
if actual := result.Players[e.Defendant].KillWords.Codename; !actual.Consumed {
result.Trial.Prosecutor = e.Prosecutor
result.Trial.Defendant = e.Defendant
result.Trial.Word = e.Word
if !basicallyTheSame(actual.KillWord.Word, e.Word) {
} else if err := games.CreateEventCodenameTrial(ctx, id, true); err != nil { // TODO cannot be in State loop
return GameState{}, err
}
}
case EventCodenameTrial:
if result.Trial == (Trial{}) {
} else if e.Guilty {
if err := games.CreateEventNotification(ctx, id, fmt.Sprintf(`%s revealed %s is %s and collected %s's bounty.`, result.Trial.Prosecutor, result.Trial.Defendant, result.Trial.Word, result.Trial.Defendant)); err != nil { // TODO not in this loop
return GameState{}, err
}
return GameState{}, fmt.Errorf("not impl: trial: guilty: %+v", e)
} else {
v := result.Players[result.Trial.Prosecutor]
v.KillWords.Codename.Consumed = true
v.Kills = append(v.Kills, Kill{
Timestamp: e.Timestamp,
Victim: result.Trial.Defendant,
KillWord: KillWord{
Word: result.Trial.Word,
Points: -200,
},
})
result.Players[result.Trial.Prosecutor] = v
v = result.Players[result.Trial.Defendant]
v.KillWords.Codename.KillWord.Word = "" // TODO
return GameState{}, fmt.Errorf("creating state CANNOT create events because it will eval every loop")
if err := games.CreateEventNotification(ctx, id, fmt.Sprintf(`%s accused the innocent %s of being %s. %s will get a new codename.`, result.Trial.Prosecutor, result.Trial.Defendant, result.Trial.Word, result.Trial.Defendant)); err != nil {
return GameState{}, err
}
}
result.Trial = Trial{}
case EventGameReset:
return games.GameState(ctx, e.ID)
default:
return GameState{}, fmt.Errorf("unknown event type %T", event)
}
@@ -300,6 +479,20 @@ func (games Games) GameState(ctx context.Context, id string) (GameState, error)
return result, err
}
func basicallyTheSame(a, b string) bool {
simplify := func(s string) string {
s = strings.TrimSpace(strings.ToLower(s))
s2 := ""
for _, c := range s {
if unicode.IsLetter(c) {
s2 = fmt.Sprintf("%s%c", s2, c)
}
}
return s2
}
return simplify(a) == simplify(b)
}
func (games Games) CreateGame(ctx context.Context, name string) (string, error) {
var exists string
if err := games.db.Query(ctx,
@@ -326,14 +519,8 @@ func (games Games) CreateGame(ctx context.Context, name string) (string, error)
`, id, time.Now(), name)
}
func (games Games) CreateEventPlayerJoin(ctx context.Context, id string, player, name string) error {
if err := games.db.Exec(ctx, `
INSERT INTO users (
uuid,
name
) VALUES (?, ?)
ON CONFLICT DO UPDATE SET name=? WHERE uuid=?;
`, player, name, name, player); err != nil {
func (games Games) CreateEventPlayerJoin(ctx context.Context, id string, player string) error {
if _, err := games.UserName(ctx, player); err != nil {
return err
}
if err := games.db.Exec(ctx, `
@@ -367,10 +554,7 @@ func (games Games) CreateEventAssignmentRotation(ctx context.Context, id string,
},
}
prevAllKillWords := make(AllKillWords)
for k, v := range state.Players {
prevAllKillWords[k] = v.KillWords
}
prevAllKillWords := state.AllKillWords()
event.AllKillWords = prevAllKillWords.ShuffleAssignees(killer, victim, word)
event.AllKillWords = event.AllKillWords.FillKillWords()
@@ -378,6 +562,44 @@ func (games Games) CreateEventAssignmentRotation(ctx context.Context, id string,
return games.createEvent(ctx, id, event)
}
func (state GameState) AllKillWords() AllKillWords {
m := make(AllKillWords)
for k, v := range state.Players {
m[k] = v.KillWords
}
return m
}
func (games Games) CreateEventGameReset(ctx context.Context, gid string) error {
state, err := games.GameState(ctx, gid)
if err != nil {
return err
}
name, err := games.GameName(ctx, gid)
if err != nil {
return err
}
gid2, err := games.CreateGame(ctx, name)
if err != nil {
return err
}
for p := range state.Players {
if err := games.CreateEventPlayerJoin(ctx, gid2, p); err != nil {
return err
}
}
event := EventGameReset{
Type: GameReset,
Timestamp: time.Now(),
ID: gid2,
}
return games.createEvent(ctx, gid, event)
}
func (words KillWords) Empty() bool {
return words.Codename == (Codename{}) && words.Assigned.IsZero() && words.Assignee == "" && words.Assignment.Empty()
}
func (words KillWords) Privates() []KillWord {
a := slices.Clone(words.Assignment.Private)
slices.SortFunc(a, func(a, b KillWord) int {
@@ -399,7 +621,7 @@ func (prev AllKillWords) ShuffleAssignees(killer, victim, word string) AllKillWo
if _, ok := prev[killer]; !ok {
} else if victimState, ok := prev[victim]; !ok {
} else {
} else if killer != victimState.Assignee { // if victim was targeting killer, just randomize
m.assign(killer, victimState.Assignee)
}
@@ -446,7 +668,7 @@ func (m AllKillWords) withoutAssignees() AllKillWords {
result := make(AllKillWords)
for k := range m {
result[k] = KillWords{
Global: m[k].Global,
Codename: m[k].Codename,
Assigned: now,
Assignee: "",
Assignment: m[k].Assignment,
@@ -495,7 +717,7 @@ func (m AllKillWords) FillKillWords() AllKillWords {
}
func (m AllKillWords) fillKillWords(
poolGlobal []string,
poolCodename []string,
nPublic int,
poolPublic []string,
nPrivate int,
@@ -504,8 +726,8 @@ func (m AllKillWords) fillKillWords(
result := maps.Clone(m)
m = result
for k, v := range m {
if v.Global.Word == "" {
v.Global = KillWord{Word: m.unusedGlobal(poolGlobal), Points: -1}
if v.Codename.KillWord.Word == "" {
v.Codename = Codename{KillWord: KillWord{Word: m.unusedCodename(poolCodename), Points: 200}}
m[k] = v
}
if len(v.Assignment.Public) == 0 {
@@ -526,11 +748,11 @@ func (m AllKillWords) fillKillWords(
return m
}
func (m AllKillWords) unusedGlobal(pool []string) string {
func (m AllKillWords) unusedCodename(pool []string) string {
inUse := func() []string {
result := []string{}
for _, killWords := range m {
result = append(result, killWords.Global.Word)
result = append(result, killWords.Codename.KillWord.Word)
}
return result
}
@@ -589,6 +811,23 @@ func (games Games) CreateEventGameComplete(ctx context.Context, id string) error
return games.createEvent(ctx, id, EventGameComplete{Type: GameComplete})
}
func (games Games) CreateEventCodenameAccusal(ctx context.Context, gid, prosecutor, defendant, codename string) error {
return fmt.Errorf("not impl: x accused y")
return fmt.Errorf("not impl: x caught by y")
}
func (games Games) CreateEventCodenameTrial(ctx context.Context, gid string, guilty bool) error {
return fmt.Errorf("not impl: x found guilty/notguilty")
}
func (games Games) CreateEventNotification(ctx context.Context, gid, msg string) error {
return games.CreateEventNotificationTo(ctx, gid, "", msg)
}
func (games Games) CreateEventNotificationTo(ctx context.Context, gid, uid, msg string) error {
return fmt.Errorf("not impl: simple")
}
func (games Games) createEvent(ctx context.Context, id string, v any) error {
payload, err := json.Marshal(v)
if err != nil {

View File

@@ -31,7 +31,7 @@ func TestGames(t *testing.T) {
t.Error(v)
}
if v, err := games.GameByName(ctx, "", ""); err != nil {
if v, err := games.GameByName(ctx, ""); err != nil {
t.Error("err getting game by empty name for empty user:", err)
} else if len(v) > 0 {
t.Error(v)
@@ -56,7 +56,7 @@ func TestGames(t *testing.T) {
t.Fatal("redundant create game didnt return same id:", id2)
}
if err := games.CreateEventPlayerJoin(ctx, id, "p0", "player zero"); err != nil {
if err := games.CreateEventPlayerJoin(ctx, id, "p0"); err != nil {
t.Fatal("err creating event player join:", err)
} else if err := games.CreateEventPlayerLeave(ctx, id, "p0"); err != nil {
t.Fatal("err creating event player leave:", err)
@@ -64,12 +64,12 @@ func TestGames(t *testing.T) {
for i := 0; i < 4; i++ {
p := fmt.Sprintf("p%d", i+1)
if err := games.CreateEventPlayerJoin(ctx, id, p, "player "+p); err != nil {
if err := games.CreateEventPlayerJoin(ctx, id, p); err != nil {
t.Fatal(p, "err creating event player join", err)
}
if name, err := games.UserName(ctx, p); err != nil {
t.Fatal(p, "err getting user name", err)
} else if name != "player "+p {
} else if name == "" {
t.Fatal("name wrong", name)
}
if err := games.UpdateUserName(ctx, p, "player! "+p); err != nil {
@@ -112,7 +112,7 @@ func TestGames(t *testing.T) {
t.Error("wrong game found for user:", v)
}
if v, err := games.GameByName(ctx, "p1", "g1"); err != nil {
if v, err := games.GameByName(ctx, "g1"); err != nil {
t.Error("err getting game by name for user:", err)
} else if v != id {
t.Error("wrong game by name for user:", v)
@@ -128,8 +128,8 @@ func TestGames(t *testing.T) {
if v.Players[p].Points() != 0 {
t.Error("nonzero points after zero kills:", v.Players[p].Points())
}
if v.Players[p].KillWords.Global.Word == "" {
t.Error(p, "no killwords.global")
if v.Players[p].KillWords.Codename.KillWord.Word == "" {
t.Error(p, "no killwords.Codename")
} else if v.Players[p].KillWords.Assigned.IsZero() {
t.Error(p, "no killwords.assigned")
} else if v.Players[p].KillWords.Assignee == "" {
@@ -149,6 +149,28 @@ func TestGames(t *testing.T) {
} else if state.Completed.IsZero() {
t.Fatal("state.Completed is zero")
}
if err := games.CreateEventGameReset(ctx, id); err != nil {
t.Fatal(err)
} else if state, err := games.GameState(ctx, id); err != nil {
t.Fatal(err)
} else if state.ID == id {
t.Fatal("getting state for reset game didnt return state for new game")
} else if state.Started {
t.Fatal("reset game is started", state.Started)
} else if !state.Completed.IsZero() {
t.Fatal("reset game is complete", state.Completed)
} else if len(state.Players) != 4 {
t.Fatal("reset game doesnt have all players", len(state.Players))
} else if p := state.Players["p1"]; !p.Empty() {
t.Fatal("reset game missing p1", p)
} else if p := state.Players["p2"]; !p.Empty() {
t.Fatal("reset game missing p2", p)
} else if p := state.Players["p3"]; !p.Empty() {
t.Fatal("reset game missing p3", p)
} else if p := state.Players["p4"]; !p.Empty() {
t.Fatal("reset game missing p4", p)
}
})
}
@@ -177,10 +199,10 @@ func TestParseEvent(t *testing.T) {
},
AllKillWords: map[string]KillWords{
"x": KillWords{
Global: KillWord{
Codename: Codename{KillWord: KillWord{
Word: "a",
Points: -1,
},
Points: 200,
}},
Assignee: "z",
Assigned: now,
Assignment: Assignment{
@@ -238,48 +260,48 @@ func TestAllKillWordsFill(t *testing.T) {
}{
"full": {
given: KillWords{
Global: kw(-1, "global"),
Codename: Codename{KillWord: kw(200, "global")},
Assignment: ass("pub", "pri"),
},
expect: KillWords{
Global: kw(-1, "global"),
Codename: Codename{KillWord: kw(200, "global")},
Assignment: ass("pub", "pri"),
},
},
"no ass": {
given: KillWords{
Global: kw(-1, "global"),
Codename: Codename{KillWord: kw(200, "global")},
Assignment: Assignment{},
},
expect: KillWords{
Global: kw(-1, "global"),
Codename: Codename{KillWord: kw(200, "global")},
Assignment: ass("filled-public", "filled-private"),
},
},
"no pub": {
given: KillWords{
Global: kw(-1, "global"),
Codename: Codename{KillWord: kw(200, "global")},
Assignment: ass("", "pri"),
},
expect: KillWords{
Global: kw(-1, "global"),
Codename: Codename{KillWord: kw(200, "global")},
Assignment: ass("filled-public", "pri"),
},
},
"no pri": {
given: KillWords{
Global: kw(-1, "global"),
Codename: Codename{KillWord: kw(200, "global")},
Assignment: ass("pub", ""),
},
expect: KillWords{
Global: kw(-1, "global"),
Codename: Codename{KillWord: kw(200, "global")},
Assignment: ass("pub", "filled-private"),
},
},
"empty": {
given: KillWords{},
expect: KillWords{
Global: kw(-1, "filled-global"),
Codename: Codename{KillWord: kw(200, "filled-global")},
Assignment: ass("filled-public", "filled-private"),
},
},
@@ -288,7 +310,7 @@ func TestAllKillWordsFill(t *testing.T) {
Assignment: ass("pub", "pri"),
},
expect: KillWords{
Global: kw(-1, "filled-global"),
Codename: Codename{KillWord: kw(200, "filled-global")},
Assignment: ass("pub", "pri"),
},
},
@@ -332,7 +354,7 @@ func TestAllKillWordsUnused(t *testing.T) {
t.Error("empty playerbase didnt think only option was unused")
}
if got := akw.unusedGlobal([]string{"x"}); got != "x" {
if got := akw.unusedCodename([]string{"x"}); got != "x" {
t.Error("empty playerbase didnt think only option was unused")
}
})
@@ -341,7 +363,7 @@ func TestAllKillWordsUnused(t *testing.T) {
t.Run("private", func(t *testing.T) {
akw := make(AllKillWords)
akw["k"] = KillWords{
Global: KillWord{Word: "x"},
Codename: Codename{KillWord: KillWord{Word: "x"}},
Assignment: Assignment{
Private: []KillWord{{}, {Word: "y"}},
Public: []KillWord{{}, {Word: "x"}},
@@ -355,13 +377,13 @@ func TestAllKillWordsUnused(t *testing.T) {
t.Run("global", func(t *testing.T) {
akw := make(AllKillWords)
akw["k"] = KillWords{
Global: KillWord{Word: "y"},
Codename: Codename{KillWord: KillWord{Word: "y"}},
Assignment: Assignment{
Private: []KillWord{{}, {Word: "x"}},
Public: []KillWord{{}, {Word: "x"}},
},
}
got := akw.unusedGlobal([]string{"x", "y"})
got := akw.unusedCodename([]string{"x", "y"})
if got != "x" {
t.Error("didnt return only unused option")
}
@@ -369,7 +391,7 @@ func TestAllKillWordsUnused(t *testing.T) {
t.Run("public", func(t *testing.T) {
akw := make(AllKillWords)
akw["k"] = KillWords{
Global: KillWord{Word: "x"},
Codename: Codename{KillWord: KillWord{Word: "x"}},
Assignment: Assignment{
Private: []KillWord{{}, {Word: "x"}},
Public: []KillWord{{}, {Word: "y"}},
@@ -382,3 +404,35 @@ func TestAllKillWordsUnused(t *testing.T) {
})
})
}
func TestGenerateUserName(t *testing.T) {
games := newTestGames(t)
name, err := games.UserName(context.Background(), "id")
if err != nil {
t.Fatal(err)
}
if name == "" {
t.Fatal(name)
}
name2, err := games.UserName(context.Background(), "id")
if err != nil {
t.Fatal(err)
}
if name2 != name {
t.Fatal(name2)
}
if err := games.CreateEventPlayerJoin(context.Background(), "gid", "id"); err != nil {
t.Fatal("err creating event player join:", err)
}
if id, err := games.UserByName(context.Background(), "gid", name); err != nil {
t.Fatal("err getting user by name:", err)
} else if id != "id" {
t.Fatal("getting user by name yielded wrong id:", id)
}
t.Log(name)
}

View File

@@ -59,13 +59,19 @@ type Session struct {
}
func (s *S) injectContext(w http.ResponseWriter, r *http.Request) error {
id, err := r.Cookie("uuid")
if err != nil || id.Value == "" {
id := r.Header.Get("uuid")
if id == "" {
c, _ := r.Cookie("uuid")
if c != nil {
id = c.Value
}
}
if id == "" {
return io.EOF
}
ctx := r.Context()
ctx = context.WithValue(ctx, "session", Session{
ID: id.Value,
ID: id,
})
*r = *r.WithContext(ctx)
return nil

View File

@@ -5,6 +5,7 @@ import (
"encoding/json"
"fmt"
"log"
"slices"
"time"
)
@@ -71,25 +72,52 @@ func (ugs *UserGameServer) listen(ctx context.Context, reader func(context.Conte
return err
}
if m["party"] == "start" {
if startGame := m["party"] == "start"; startGame {
if gameState, err := ugs.games.GameState(ctx, ugs.ID); err != nil {
return err
} else if gameState.Started {
} else if err := ugs.games.CreateEventAssignmentRotation(ctx, ugs.ID, "", "", "", 0); err != nil {
return err
}
} else if m["k"] != "" {
return fmt.Errorf("not impl: a kill occurred: %+v", m)
} else if name := m["name"]; name != "" {
if err := ugs.games.UpdateUserName(ctx, ugs.Session.ID, name); err != nil {
} else if killOccurred := m["k"] != ""; killOccurred {
victimName := m["name"]
word := m["k"]
if word == "" {
return fmt.Errorf("expected .k")
}
killer := ugs.Session.ID
victim, err := ugs.games.UserByName(ctx, ugs.ID, victimName)
if err != nil {
return err
}
} else if m["again"] == "true" {
var points int
if gameState, err := ugs.games.GameState(ctx, ugs.ID); err != nil {
return err
} else if codename := gameState.Players[killer].KillWords.Codename.KillWord; codename.Word == word {
points = codename.Points
} else if matches := slices.DeleteFunc(gameState.Players[victim].KillWords.Publics(), func(kw KillWord) bool { return kw.Word != word }); len(matches) > 0 {
points = matches[0].Points
} else if matches := slices.DeleteFunc(gameState.Players[victim].KillWords.Privates(), func(kw KillWord) bool { return kw.Word != word }); len(matches) > 0 {
points = matches[0].Points
} else {
return fmt.Errorf("refusing unexpected .k")
}
if err := ugs.games.CreateEventAssignmentRotation(ctx, ugs.ID, killer, victim, word, points); err != nil {
return err
}
} else if isRename := m["name"] != ""; isRename {
if err := ugs.games.UpdateUserName(ctx, ugs.Session.ID, m["name"]); err != nil {
return err
}
} else if isRestart := m["again"] == "true"; isRestart {
if gameState, err := ugs.games.GameState(ctx, ugs.ID); err != nil {
return err
} else if gameState.Completed.IsZero() {
} else {
return fmt.Errorf("not impl: new game: %+v", m)
} else if err := ugs.games.CreateEventGameReset(ctx, ugs.ID); err != nil {
return err
}
} else {
return fmt.Errorf("UNKNOWN: %+v", m)
@@ -98,6 +126,45 @@ func (ugs *UserGameServer) listen(ctx context.Context, reader func(context.Conte
return ctx.Err()
}
func (ugs *UserGameServer) State(ctx context.Context) (GameState, error) {
return ugs.games.GameState(ctx, ugs.ID)
type UserGameState GameState
func (ugs *UserGameServer) State(ctx context.Context) (UserGameState, error) {
gameState, err := ugs.games.GameState(ctx, ugs.ID)
if err != nil {
return UserGameState{}, err
}
ugs.ID = gameState.ID
if complete := !gameState.Completed.IsZero(); complete {
return UserGameState(gameState), nil
}
self := gameState.Players[ugs.Session.ID]
for i := range self.Kills {
self.Kills[i].KillWord.Points = 0
}
self.KillWords.Assignment.Public = nil
self.KillWords.Assignment.Private = nil
gameState.Players[ugs.Session.ID] = self
for k, v := range gameState.Players {
if isSelf := k == ugs.Session.ID; isSelf {
v.KillWords.Assignment = Assignment{}
} else {
v.KillWords.Codename = Codename{}
v.KillWords.Assignee = ""
for i := range v.Kills {
v.Kills[i].Victim = ""
v.Kills[i].KillWord.Word = ""
}
}
if assignedToSomeoneElse := self.KillWords.Assignee != k; assignedToSomeoneElse {
v.KillWords.Assignment.Private = v.KillWords.Assignment.Private[:0]
}
gameState.Players[k] = v
}
return UserGameState(gameState), nil
}

View File

@@ -0,0 +1,291 @@
package main
import (
"context"
"fmt"
"testing"
)
func TestUserGameServer(t *testing.T) {
ctx := context.Background()
games := newTestGames(t)
gid, err := games.CreateGame(ctx, "g1")
if err != nil {
t.Fatal(err)
}
pids := []string{}
for i := 0; i < 4; i++ {
pid := fmt.Sprintf("p%d", i+1)
if err := games.CreateEventPlayerJoin(ctx, gid, pid); err != nil {
t.Fatal(err)
}
pids = append(pids, pid)
}
ugs, err := NewUserGameServer(ctx, Session{ID: pids[0]}, games)
if err != nil {
t.Fatal(err)
}
t.Run("unstarted", func(t *testing.T) {
state, err := ugs.State(ctx)
if err != nil {
t.Fatal(err)
}
if state.Started {
t.Error("started after player joins only")
}
if !state.Completed.IsZero() {
t.Error("completed after player joins only")
}
for _, pid := range pids {
if p, ok := state.Players[pid]; !ok {
t.Error(pid, "not in players")
} else if !p.Empty() {
t.Error(pid, p)
}
}
})
if err := games.CreateEventAssignmentRotation(ctx, gid, "", "", "", 0); err != nil {
t.Fatal(err)
}
t.Run("just started", func(t *testing.T) {
state, err := ugs.State(ctx)
if err != nil {
t.Fatal(err)
}
if !state.Started {
t.Error("not started after assignment rotation")
}
if !state.Completed.IsZero() {
t.Error("completed after assignment rotation")
}
for _, pid := range pids {
p, ok := state.Players[pid]
if !ok {
t.Error(pid, "not in players")
} else if p.Empty() {
t.Error(pid, p)
} else if len(p.Kills) > 0 {
t.Error(pid, "has a kill")
} else if p.KillWords.Assigned.IsZero() {
t.Error("assigned is zero")
}
if isSelf := pid == ugs.Session.ID; isSelf {
if p.KillWords.Codename.KillWord.Word == "" || p.KillWords.Codename.KillWord.Points == 0 {
t.Error("self codename missing field")
}
if p.KillWords.Assignee == "" {
t.Error("assignee is empty")
}
if len(p.KillWords.Assignment.Public) > 0 {
t.Error("self knows its own public")
}
if len(p.KillWords.Assignment.Private) > 0 {
t.Error("self knows its own private")
}
} else {
if !p.KillWords.Codename.KillWord.Empty() {
t.Error("can see not self Codename")
}
if p.KillWords.Assignee != "" {
t.Error("can see other player's assignee")
}
if len(p.KillWords.Assignment.Public) == 0 {
t.Error("cannot see other player's public")
}
if state.Players[ugs.Session.ID].KillWords.Assignee == pid && len(p.KillWords.Assignment.Private) == 0 {
t.Error("cannot see assignee's private")
} else if state.Players[ugs.Session.ID].KillWords.Assignee != pid && len(p.KillWords.Assignment.Private) > 0 {
t.Error("can see not assignee's private")
}
}
}
})
for i := 0; i < 3; i++ {
state, err := games.GameState(ctx, ugs.ID)
if err != nil {
t.Fatal(err)
}
killer := ugs.Session.ID
if i > 0 {
killer = state.Players[killer].KillWords.Assignee
}
victim := state.Players[killer].KillWords.Assignee
word := state.Players[victim].KillWords.Assignment.Public[0].Word
points := state.Players[victim].KillWords.Assignment.Public[0].Points
if err := games.CreateEventAssignmentRotation(ctx, gid, killer, victim, word, points); err != nil {
t.Fatal(err)
}
}
t.Run("after kills", func(t *testing.T) {
state, err := ugs.State(ctx)
if err != nil {
t.Fatal(err)
}
if !state.Started {
t.Error("not started after kills")
}
if !state.Completed.IsZero() {
t.Error("completed after kills")
}
for _, pid := range pids {
p, ok := state.Players[pid]
if !ok {
t.Error(pid, "not in players")
} else if p.Empty() {
t.Error(pid, p)
} else if p.KillWords.Assigned.IsZero() {
t.Error("assigned is zero")
}
if isSelf := pid == ugs.Session.ID; isSelf {
if len(p.Kills) == 0 {
t.Error("self never got a kill")
} else if kill := p.Kills[0]; kill.Timestamp.IsZero() {
t.Errorf("kill has no timestamp")
} else if kill.Victim == "" {
t.Errorf("kill has no victim")
} else if kill.KillWord.Points != 0 {
t.Errorf("know points of own kill")
} else if kill.KillWord.Word == "" {
t.Errorf("dont know own kill word")
}
if p.KillWords.Codename.KillWord.Word == "" || p.KillWords.Codename.KillWord.Points == 0 {
t.Error("self Codename missing field")
}
if p.KillWords.Assignee == "" {
t.Error("assignee is empty")
}
if len(p.KillWords.Assignment.Public) > 0 {
t.Error("self knows its own public")
}
if len(p.KillWords.Assignment.Private) > 0 {
t.Error("self knows its own private")
}
} else {
if len(p.Kills) == 0 {
} else if kill := p.Kills[0]; kill.Timestamp.IsZero() {
t.Errorf("kill has no timestamp")
} else if kill.Victim != "" {
t.Errorf("know other's victim")
} else if kill.KillWord.Points == 0 {
t.Errorf("other's kill has no points")
} else if kill.KillWord.Word != "" {
t.Errorf("know other's kill word")
}
if !p.KillWords.Codename.KillWord.Empty() {
t.Error("can see not self Codename")
}
if p.KillWords.Assignee != "" {
t.Error("can see other player's assignee")
}
if len(p.KillWords.Assignment.Public) == 0 {
t.Error("cannot see other player's public")
}
if state.Players[ugs.Session.ID].KillWords.Assignee == pid && len(p.KillWords.Assignment.Private) == 0 {
t.Error("cannot see assignee's private")
} else if state.Players[ugs.Session.ID].KillWords.Assignee != pid && len(p.KillWords.Assignment.Private) > 0 {
t.Error("can see not assignee's private")
}
}
}
})
if err := games.CreateEventGameComplete(ctx, gid); err != nil {
t.Fatal(err)
}
t.Run("completed", func(t *testing.T) {
state, err := ugs.State(ctx)
if err != nil {
t.Fatal(err)
}
if !state.Started {
t.Error("not started after complete")
}
if state.Completed.IsZero() {
t.Error("not complete after complete")
}
for _, pid := range pids {
p, ok := state.Players[pid]
if !ok {
t.Error(pid, "not in players")
} else if p.Empty() {
t.Error(pid, p)
} else if p.KillWords.Assigned.IsZero() {
t.Error("assigned is zero")
}
if isSelf := pid == ugs.Session.ID; isSelf {
if len(p.Kills) == 0 {
t.Error("self never got a kill")
} else if kill := p.Kills[0]; kill.Timestamp.IsZero() {
t.Errorf("kill has no timestamp")
} else if kill.Victim == "" {
t.Errorf("kill has no victim")
} else if kill.KillWord.Points == 0 {
t.Errorf("dont know points of own kill at game review")
} else if kill.KillWord.Word == "" {
t.Errorf("dont know own kill word")
}
if p.KillWords.Codename.KillWord.Word == "" || p.KillWords.Codename.KillWord.Points == 0 {
t.Error("self Codename missing field")
}
if p.KillWords.Assignee == "" {
t.Error("assignee is empty")
}
if len(p.KillWords.Assignment.Public) == 0 {
t.Error("self doesnt know its own public after game")
}
if len(p.KillWords.Assignment.Private) == 0 {
t.Error("self doesnt know its own private after game")
}
} else {
if len(p.Kills) == 0 {
} else if kill := p.Kills[0]; kill.Timestamp.IsZero() {
t.Errorf("kill has no timestamp")
} else if kill.Victim == "" {
t.Errorf("cannot know other's victim")
} else if kill.KillWord.Points == 0 {
t.Errorf("other's kill has no points")
} else if kill.KillWord.Word == "" {
t.Errorf("dont know other's kill word")
}
if p.KillWords.Codename.KillWord.Empty() {
t.Error("cannot see not self Codename")
}
if p.KillWords.Assignee == "" {
t.Error("cannot see other player's assignee")
}
if len(p.KillWords.Assignment.Public) == 0 {
t.Error("cannot see other player's public")
}
if state.Players[ugs.Session.ID].KillWords.Assignee == pid && len(p.KillWords.Assignment.Private) == 0 {
t.Error("cannot see assignee's private")
} else if state.Players[ugs.Session.ID].KillWords.Assignee != pid && len(p.KillWords.Assignment.Private) == 0 {
t.Error("cannot see not assignee's private after game")
}
}
}
})
}

View File

@@ -1,9 +1,12 @@
package main
import (
"fmt"
"bytes"
"encoding/json"
"io"
"net/http"
"path"
"slices"
"strings"
)
@@ -12,8 +15,29 @@ func isV1(r *http.Request) bool {
}
func (s *S) serveV1(w http.ResponseWriter, r *http.Request) error {
uid := s.Session(r.Context()).ID
switch path.Join(r.Method, r.URL.Path) {
case "PUT/v1/state/" + s.Session(r.Context()).ID + "/party":
case "GET/v1/state/" + uid:
name, err := s.games.UserName(r.Context(), uid)
if err != nil {
return err
}
gids, err := s.games.GamesForUser(r.Context(), uid)
if err != nil {
return err
}
msg := map[string]any{
"name": name,
}
if len(gids) > 0 {
party, err := s.games.GameName(r.Context(), gids[0])
if err != nil {
return err
}
msg["party"] = party
}
return json.NewEncoder(w).Encode(msg)
case "PUT/v1/state/" + uid + "/party":
return s.serveV1PutParty(w, r)
default:
http.NotFound(w, r)
@@ -22,5 +46,27 @@ func (s *S) serveV1(w http.ResponseWriter, r *http.Request) error {
}
func (s *S) serveV1PutParty(w http.ResponseWriter, r *http.Request) error {
return fmt.Errorf("not impl")
party, err := io.ReadAll(r.Body)
if err != nil {
return err
}
party = bytes.TrimSpace(party)
if len(party) == 0 {
return nil
}
gid, err := s.games.GameByName(r.Context(), string(party))
if err != nil {
return err
}
games, err := s.games.GamesForUser(r.Context(), gid)
if err != nil {
return err
}
if slices.Contains(games, gid) {
return nil
}
return s.games.CreateEventPlayerJoin(r.Context(), gid, s.Session(r.Context()).ID)
}

View File

@@ -4,7 +4,6 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"slices"
"strconv"
@@ -68,7 +67,7 @@ func (ws WS) Push(ctx context.Context, ugs *UserGameServer) error {
var msg map[string]any
if unstarted := !gameState.Started; unstarted {
msg, err = ws.unstartedMsg(ctx, gameState)
msg, err = ws.unstartedMsg(ctx, ugs, gameState)
} else if complete := !gameState.Completed.IsZero(); complete {
msg, err = ws.completeMsg(ctx, gameState)
} else {
@@ -92,23 +91,27 @@ func (ws WS) Push(ctx context.Context, ugs *UserGameServer) error {
return ws.c.Write(ctx, 1, msgB)
}
func (ws WS) unstartedMsg(ctx context.Context, gameState GameState) (msg map[string]any, _ error) {
func (ws WS) unstartedMsg(ctx context.Context, ugs *UserGameServer, gameState UserGameState) (msg map[string]any, _ error) {
msg["page"] = "A"
items := []map[string]any{}
for k := range gameState.Players {
if k == ugs.Session.ID {
continue
}
name, err := ws.games.UserName(ctx, k)
if err != nil {
return nil, err
}
items = append(items, map[string]any{"name": name})
items = append(items, map[string]any{"id": k, "name": name})
}
msg["items"] = items
return msg, nil
}
func (ws WS) completeMsg(ctx context.Context, gameState GameState) (msg map[string]any, _ error) {
func (ws WS) completeMsg(ctx context.Context, gameState UserGameState) (msg map[string]any, _ error) {
msg["page"] = "B"
msg["event"] = "B"
@@ -135,7 +138,7 @@ func (ws WS) completeMsg(ctx context.Context, gameState GameState) (msg map[stri
return msg, nil
}
func (ws WS) inProgressMsg(ctx context.Context, ugs *UserGameServer, gameState GameState) (msg map[string]any, _ error) {
func (ws WS) inProgressMsg(ctx context.Context, ugs *UserGameServer, gameState UserGameState) (msg map[string]any, _ error) {
msg["page"] = "B"
msg["event"] = "A"
@@ -145,7 +148,7 @@ func (ws WS) inProgressMsg(ctx context.Context, ugs *UserGameServer, gameState G
}
msg["items"] = items
return nil, io.EOF
return msg, nil
}
type inProgressMsgItem struct {
@@ -159,7 +162,7 @@ type inProgressMsgItemTag struct {
V int `json:"v,string"`
}
func (ws WS) inProgressMsgItems(ctx context.Context, ugs *UserGameServer, gameState GameState) ([]inProgressMsgItem, error) {
func (ws WS) inProgressMsgItems(ctx context.Context, ugs *UserGameServer, gameState UserGameState) ([]inProgressMsgItem, error) {
items := []inProgressMsgItem{}
for k := range gameState.Players {
item, err := ws.inProgressMsgItem(ctx, ugs, gameState, k)
@@ -179,39 +182,24 @@ func (ws WS) inProgressMsgItems(ctx context.Context, ugs *UserGameServer, gameSt
return items, nil
}
func (ws WS) inProgressMsgItem(ctx context.Context, ugs *UserGameServer, gameState GameState, uid string) (*inProgressMsgItem, error) {
func (ws WS) inProgressMsgItem(ctx context.Context, ugs *UserGameServer, gameState UserGameState, uid string) (*inProgressMsgItem, error) {
if isSelf := uid == ugs.Session.ID; isSelf {
return nil, nil
}
v := gameState.Players[uid]
self := gameState.Players[ugs.Session.ID]
v := gameState.Players[uid]
tags := []inProgressMsgItemTag{}
if hasBeenKilledWithGlobal := slices.ContainsFunc(self.Kills, func(a Kill) bool {
return a.KillWord.Word == self.KillWords.Global.Word && a.Victim == uid
}); !hasBeenKilledWithGlobal {
tags = append(tags, inProgressMsgItemTag{
K: self.KillWords.Global.Word,
V: self.KillWords.Global.Points,
})
if canKillWithCodename := !self.KillWords.Codename.Consumed; canKillWithCodename {
tags = append(tags, newInProgressMsgItemTag(self.KillWords.Codename.KillWord))
}
for _, public := range v.KillWords.Publics() {
tags = append(tags, inProgressMsgItemTag{
K: public.Word,
V: public.Points,
})
}
if isAssigned := self.KillWords.Assignee == uid; isAssigned {
for _, private := range v.KillWords.Privates() {
tags = append(tags, inProgressMsgItemTag{
K: private.Word,
V: private.Points,
})
}
for _, killWord := range append(
v.KillWords.Publics(),
v.KillWords.Privates()...,
) {
tags = append(tags, newInProgressMsgItemTag(killWord))
}
name, err := ws.games.UserName(ctx, uid)
@@ -221,3 +209,10 @@ func (ws WS) inProgressMsgItem(ctx context.Context, ugs *UserGameServer, gameSta
Tags: tags,
}, err
}
func newInProgressMsgItemTag(kw KillWord) inProgressMsgItemTag {
return inProgressMsgItemTag{
K: kw.Word,
V: kw.Points,
}
}

View File

@@ -2,11 +2,11 @@ package main
import (
"context"
"crypto/rand"
"flag"
"fmt"
"io"
"log"
"math/rand"
"net/http"
"os"
"os/signal"
@@ -72,6 +72,15 @@ func (s *S) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
func (s *S) serveHTTP(w http.ResponseWriter, r *http.Request) error {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Headers", "X-Auth-Token, content-type, Content-Type")
if r.Method == http.MethodOptions {
w.Header().Set("Content-Length", "0")
w.Header().Set("Content-Type", "text/plain")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, OPTIONS, TRACE, PATCH, HEAD, DELETE")
w.WriteHeader(http.StatusOK)
return nil
}
if isV1(r) || isWS(r) {
return s.serveAPI(w, r)
}
@@ -113,13 +122,17 @@ type Session struct {
}
func (s *S) injectContext(w http.ResponseWriter, r *http.Request) error {
id, err := r.Cookie("uuid")
if err != nil || id.Value == "" {
id := r.URL.Query().Get("uuid")
if id == "" {
c, err := r.Cookie("uuid")
if err != nil || c.Value == "" {
return io.EOF
}
id = c.Value
}
ctx := r.Context()
ctx = context.WithValue(ctx, "session", Session{
ID: id.Value,
ID: id,
})
*r = *r.WithContext(ctx)
return nil
@@ -133,7 +146,9 @@ func (s *S) Session(ctx context.Context) Session {
func (s *S) serveWS(w http.ResponseWriter, r *http.Request) error {
ctx := r.Context()
c, err := websocket.Accept(w, r, nil)
c, err := websocket.Accept(w, r, &websocket.AcceptOptions{
InsecureSkipVerify: true,
})
if err != nil {
return err
}
@@ -177,12 +192,12 @@ func (s *S) serveV1(w http.ResponseWriter, r *http.Request) error {
switch r.Method + r.URL.Path {
case "GET/v1/state/" + s.Session(ctx).ID:
if rand.Int()%2 == 0 {
w.Write(`{"name": "foo"}`)
w.Write([]byte(`{"name": "foo"}`))
} else {
w.Write(`{"name": "bar", "party": "party name"}`)
w.Write([]byte(`{"name": "bar", "party": "party name"}`))
}
case "PUT/v1/state/" + s.Session(ctx).ID + "/party":
w.Write(`{}`)
w.Write([]byte(`{}`))
default:
http.NotFound(w, r)
return nil

16
cmd/testws/index.html Normal file
View File

@@ -0,0 +1,16 @@
<html>
<header>
<script>
function log(msg) {
console.log(msg)
}
const ws = new Websocket("wss://out-test.breel.dev/ws")
ws.onmessage = () => { console.log("got a message") }
ws.onerror = () => { console.log("got an error") }
ws.onclose = () => { console.log("closed") }
</script>
</header>
<body>
<div id="msg"></div>
</body>
</html>

View File

@@ -1,12 +1,28 @@
todo:
- refactor ws.go into like WebSocketSession struct and stuff
- global+public+private to public+private+CODENAME
- accuse event which either ends in a successful codename call worth 100% points +
disables codename OR ends in a failed codename call costing 50% codename points
- report system
- how to generate word lists??
- '"handler" system; there are both assassinations AND tenet-friendship-codeword systems
in-flight'
- dont like other players points; just order by points so winner is at top of list
- comeback/rebound system
- kingkiller system, increased bounty for those who havent died recently + high scorers
- test ws flow
- notifications system with dismissal server-side so users see X got a kill
- play mp3 on kill + shuffle
- end condition; everyone has died
- word lists; already got holidays
- event driven woulda been nice
- remake
- quit
- leave game
scheduled: []
done: []
done:
- todo: refactor ws.go into like WebSocketSession struct and stuff
ts: Sun Dec 15 14:28:29 MST 2024
- todo: remake
ts: Sun Dec 15 14:28:29 MST 2024
- todo: quit
ts: Sun Dec 15 14:28:34 MST 2024
- todo: '"handler" system??'
ts: Sun Dec 15 16:43:34 MST 2024