Compare commits
44 Commits
94d3d97645
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
617785ad51 | ||
|
|
d2fa707628 | ||
|
|
421331eb71 | ||
|
|
c6ffa12354 | ||
|
|
58fae19522 | ||
|
|
4abac89472 | ||
|
|
15078a626d | ||
|
|
02dc21c124 | ||
|
|
4d9abef04c | ||
|
|
557e1ec6d4 | ||
|
|
3095d54f99 | ||
|
|
d6db4a8a58 | ||
|
|
d36ef7b629 | ||
|
|
ab3a549f78 | ||
|
|
bf3b341b69 | ||
|
|
54116131b3 | ||
|
|
57dd66e510 | ||
|
|
f1282f588d | ||
|
|
706d55631b | ||
|
|
0b22ba4bd2 | ||
|
|
f7a303168a | ||
|
|
37291e68aa | ||
|
|
5109dc9fdb | ||
|
|
fc899056e0 | ||
|
|
8d73f97c3a | ||
|
|
598cb0684c | ||
|
|
aba5225ed2 | ||
|
|
f3f70e10f4 | ||
|
|
1e4198b291 | ||
|
|
c760dac44b | ||
|
|
c1933dc180 | ||
|
|
9a74575e6c | ||
|
|
8d0ded9ee9 | ||
|
|
e8817f9e74 | ||
|
|
3a83fe7c17 | ||
|
|
c6da3d17a1 | ||
|
|
64165c5745 | ||
|
|
74a403fa6d | ||
|
|
e95b63d9ce | ||
|
|
2c3e870750 | ||
|
|
c3e9c18e95 | ||
|
|
39c9eae7ad | ||
|
|
0d44fd56ed | ||
|
|
51006c7946 |
249
cmd/server/adjectives.txt
Normal file
249
cmd/server/adjectives.txt
Normal 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
100
cmd/server/animals.txt
Normal 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
|
||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
@@ -62,9 +63,24 @@ func (games Games) GamesForUser(ctx context.Context, id string) ([]string, error
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (games Games) UpdateUserName(ctx context.Context, id, name 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, `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) {
|
func (games Games) UserName(ctx context.Context, id string) (string, error) {
|
||||||
result := ""
|
result := ""
|
||||||
err := games.db.Query(ctx, func(rows *sql.Rows) error {
|
err := games.db.Query(ctx, func(rows *sql.Rows) error {
|
||||||
@@ -74,9 +90,44 @@ func (games Games) UserName(ctx context.Context, id string) (string, error) {
|
|||||||
FROM users
|
FROM users
|
||||||
WHERE users.uuid=?
|
WHERE users.uuid=?
|
||||||
`, id)
|
`, 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
|
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 {
|
func (s PlayerState) Points() int {
|
||||||
points := 0
|
points := 0
|
||||||
for _, kill := range s.Kills {
|
for _, kill := range s.Kills {
|
||||||
@@ -85,28 +136,47 @@ func (s PlayerState) Points() int {
|
|||||||
return points
|
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
|
var result string
|
||||||
err := games.db.Query(ctx, func(rows *sql.Rows) error {
|
err := games.db.Query(ctx, func(rows *sql.Rows) error {
|
||||||
return rows.Scan(&result)
|
return rows.Scan(&result)
|
||||||
}, `
|
}, `
|
||||||
SELECT
|
SELECT
|
||||||
players.game_uuid
|
games.name
|
||||||
FROM
|
FROM
|
||||||
players
|
games
|
||||||
JOIN games ON players.game_uuid=games.uuid
|
WHERE games.uuid=?
|
||||||
WHERE players.user_uuid=? AND games.name=?
|
|
||||||
ORDER BY games.updated DESC
|
ORDER BY games.updated DESC
|
||||||
LIMIT 1
|
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
|
return result, err
|
||||||
}
|
}
|
||||||
|
|
||||||
type (
|
type (
|
||||||
GameState struct {
|
GameState struct {
|
||||||
|
ID string
|
||||||
Started bool
|
Started bool
|
||||||
Completed time.Time
|
Completed time.Time
|
||||||
|
|
||||||
Players map[string]PlayerState
|
Players map[string]PlayerState
|
||||||
|
|
||||||
|
Trial Trial
|
||||||
}
|
}
|
||||||
|
|
||||||
PlayerState struct {
|
PlayerState struct {
|
||||||
@@ -121,7 +191,7 @@ type (
|
|||||||
}
|
}
|
||||||
|
|
||||||
KillWords struct {
|
KillWords struct {
|
||||||
Global KillWord
|
Codename Codename
|
||||||
|
|
||||||
Assigned time.Time
|
Assigned time.Time
|
||||||
Assignee string
|
Assignee string
|
||||||
@@ -129,6 +199,11 @@ type (
|
|||||||
Assignment Assignment
|
Assignment Assignment
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Codename struct {
|
||||||
|
KillWord KillWord
|
||||||
|
Consumed bool
|
||||||
|
}
|
||||||
|
|
||||||
Assignment struct {
|
Assignment struct {
|
||||||
Public []KillWord
|
Public []KillWord
|
||||||
Private []KillWord
|
Private []KillWord
|
||||||
@@ -139,6 +214,12 @@ type (
|
|||||||
Points int
|
Points int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Trial struct {
|
||||||
|
Prosecutor string
|
||||||
|
Defendant string
|
||||||
|
Word string
|
||||||
|
}
|
||||||
|
|
||||||
EventType int
|
EventType int
|
||||||
|
|
||||||
EventPlayerJoin struct {
|
EventPlayerJoin struct {
|
||||||
@@ -163,6 +244,29 @@ type (
|
|||||||
KillWord KillWord
|
KillWord KillWord
|
||||||
AllKillWords AllKillWords
|
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
|
AllKillWords map[string]KillWords
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -171,6 +275,10 @@ const (
|
|||||||
PlayerLeave
|
PlayerLeave
|
||||||
GameComplete
|
GameComplete
|
||||||
AssignmentRotation
|
AssignmentRotation
|
||||||
|
GameReset
|
||||||
|
CodenameAccusal
|
||||||
|
CodenameTrial
|
||||||
|
Notification
|
||||||
)
|
)
|
||||||
|
|
||||||
type Event interface{ event() }
|
type Event interface{ event() }
|
||||||
@@ -179,6 +287,10 @@ func (EventPlayerJoin) event() {}
|
|||||||
func (EventPlayerLeave) event() {}
|
func (EventPlayerLeave) event() {}
|
||||||
func (EventGameComplete) event() {}
|
func (EventGameComplete) event() {}
|
||||||
func (EventAssignmentRotation) event() {}
|
func (EventAssignmentRotation) event() {}
|
||||||
|
func (EventGameReset) event() {}
|
||||||
|
func (EventCodenameAccusal) event() {}
|
||||||
|
func (EventCodenameTrial) event() {}
|
||||||
|
func (EventNotification) event() {}
|
||||||
|
|
||||||
func EventWithTime(event Event, t time.Time) Event {
|
func EventWithTime(event Event, t time.Time) Event {
|
||||||
switch e := event.(type) {
|
switch e := event.(type) {
|
||||||
@@ -194,6 +306,18 @@ func EventWithTime(event Event, t time.Time) Event {
|
|||||||
case EventAssignmentRotation:
|
case EventAssignmentRotation:
|
||||||
e.Timestamp = t
|
e.Timestamp = t
|
||||||
event = e
|
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
|
return event
|
||||||
}
|
}
|
||||||
@@ -244,12 +368,28 @@ func parseEvent(b []byte, timestamp time.Time) (Event, error) {
|
|||||||
var v EventAssignmentRotation
|
var v EventAssignmentRotation
|
||||||
err := json.Unmarshal(b, &v)
|
err := json.Unmarshal(b, &v)
|
||||||
return EventWithTime(v, timestamp), err
|
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)
|
return nil, fmt.Errorf("unknown event type %d: %s", peek.Type, b)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (games Games) GameState(ctx context.Context, id string) (GameState, error) {
|
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{})
|
events, err := games.GameEvents(ctx, id, time.Time{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -257,19 +397,16 @@ func (games Games) GameState(ctx context.Context, id string) (GameState, error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, event := range events {
|
for _, event := range events {
|
||||||
switch event.(type) {
|
switch e := event.(type) {
|
||||||
case EventPlayerJoin:
|
case EventPlayerJoin:
|
||||||
playerJoin := event.(EventPlayerJoin)
|
result.Players[e.ID] = PlayerState{}
|
||||||
result.Players[playerJoin.ID] = PlayerState{}
|
|
||||||
case EventPlayerLeave:
|
case EventPlayerLeave:
|
||||||
playerLeave := event.(EventPlayerLeave)
|
delete(result.Players, e.ID)
|
||||||
delete(result.Players, playerLeave.ID)
|
|
||||||
case EventGameComplete:
|
case EventGameComplete:
|
||||||
gameComplete := event.(EventGameComplete)
|
result.Completed = e.Timestamp
|
||||||
result.Completed = gameComplete.Timestamp
|
|
||||||
case EventAssignmentRotation:
|
case EventAssignmentRotation:
|
||||||
result.Started = true
|
result.Started = true
|
||||||
assignmentRotation := event.(EventAssignmentRotation)
|
assignmentRotation := e
|
||||||
|
|
||||||
if killer, ok := result.Players[assignmentRotation.Killer]; !ok {
|
if killer, ok := result.Players[assignmentRotation.Killer]; !ok {
|
||||||
} else if _, ok := result.Players[assignmentRotation.Victim]; !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
|
player.KillWords = v
|
||||||
result.Players[k] = player
|
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:
|
default:
|
||||||
return GameState{}, fmt.Errorf("unknown event type %T", event)
|
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
|
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) {
|
func (games Games) CreateGame(ctx context.Context, name string) (string, error) {
|
||||||
var exists string
|
var exists string
|
||||||
if err := games.db.Query(ctx,
|
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)
|
`, id, time.Now(), name)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (games Games) CreateEventPlayerJoin(ctx context.Context, id string, player, name string) error {
|
func (games Games) CreateEventPlayerJoin(ctx context.Context, id string, player string) error {
|
||||||
if err := games.db.Exec(ctx, `
|
if _, err := games.UserName(ctx, player); err != nil {
|
||||||
INSERT INTO users (
|
|
||||||
uuid,
|
|
||||||
name
|
|
||||||
) VALUES (?, ?)
|
|
||||||
ON CONFLICT DO UPDATE SET name=? WHERE uuid=?;
|
|
||||||
`, player, name, name, player); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := games.db.Exec(ctx, `
|
if err := games.db.Exec(ctx, `
|
||||||
@@ -367,10 +554,7 @@ func (games Games) CreateEventAssignmentRotation(ctx context.Context, id string,
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
prevAllKillWords := make(AllKillWords)
|
prevAllKillWords := state.AllKillWords()
|
||||||
for k, v := range state.Players {
|
|
||||||
prevAllKillWords[k] = v.KillWords
|
|
||||||
}
|
|
||||||
|
|
||||||
event.AllKillWords = prevAllKillWords.ShuffleAssignees(killer, victim, word)
|
event.AllKillWords = prevAllKillWords.ShuffleAssignees(killer, victim, word)
|
||||||
event.AllKillWords = event.AllKillWords.FillKillWords()
|
event.AllKillWords = event.AllKillWords.FillKillWords()
|
||||||
@@ -378,12 +562,66 @@ func (games Games) CreateEventAssignmentRotation(ctx context.Context, id string,
|
|||||||
return games.createEvent(ctx, id, event)
|
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 {
|
||||||
|
return strings.Compare(a.Word, b.Word)
|
||||||
|
})
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
func (words KillWords) Publics() []KillWord {
|
||||||
|
a := slices.Clone(words.Assignment.Public)
|
||||||
|
slices.SortFunc(a, func(a, b KillWord) int {
|
||||||
|
return strings.Compare(a.Word, b.Word)
|
||||||
|
})
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
func (prev AllKillWords) ShuffleAssignees(killer, victim, word string) AllKillWords {
|
func (prev AllKillWords) ShuffleAssignees(killer, victim, word string) AllKillWords {
|
||||||
m := prev.withoutAssignees()
|
m := prev.withoutAssignees()
|
||||||
|
|
||||||
if _, ok := prev[killer]; !ok {
|
if _, ok := prev[killer]; !ok {
|
||||||
} else if victimState, ok := prev[victim]; !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)
|
m.assign(killer, victimState.Assignee)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -430,7 +668,7 @@ func (m AllKillWords) withoutAssignees() AllKillWords {
|
|||||||
result := make(AllKillWords)
|
result := make(AllKillWords)
|
||||||
for k := range m {
|
for k := range m {
|
||||||
result[k] = KillWords{
|
result[k] = KillWords{
|
||||||
Global: m[k].Global,
|
Codename: m[k].Codename,
|
||||||
Assigned: now,
|
Assigned: now,
|
||||||
Assignee: "",
|
Assignee: "",
|
||||||
Assignment: m[k].Assignment,
|
Assignment: m[k].Assignment,
|
||||||
@@ -479,7 +717,7 @@ func (m AllKillWords) FillKillWords() AllKillWords {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m AllKillWords) fillKillWords(
|
func (m AllKillWords) fillKillWords(
|
||||||
poolGlobal []string,
|
poolCodename []string,
|
||||||
nPublic int,
|
nPublic int,
|
||||||
poolPublic []string,
|
poolPublic []string,
|
||||||
nPrivate int,
|
nPrivate int,
|
||||||
@@ -488,8 +726,8 @@ func (m AllKillWords) fillKillWords(
|
|||||||
result := maps.Clone(m)
|
result := maps.Clone(m)
|
||||||
m = result
|
m = result
|
||||||
for k, v := range m {
|
for k, v := range m {
|
||||||
if v.Global.Word == "" {
|
if v.Codename.KillWord.Word == "" {
|
||||||
v.Global = KillWord{Word: m.unusedGlobal(poolGlobal), Points: -1}
|
v.Codename = Codename{KillWord: KillWord{Word: m.unusedCodename(poolCodename), Points: 200}}
|
||||||
m[k] = v
|
m[k] = v
|
||||||
}
|
}
|
||||||
if len(v.Assignment.Public) == 0 {
|
if len(v.Assignment.Public) == 0 {
|
||||||
@@ -510,11 +748,11 @@ func (m AllKillWords) fillKillWords(
|
|||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m AllKillWords) unusedGlobal(pool []string) string {
|
func (m AllKillWords) unusedCodename(pool []string) string {
|
||||||
inUse := func() []string {
|
inUse := func() []string {
|
||||||
result := []string{}
|
result := []string{}
|
||||||
for _, killWords := range m {
|
for _, killWords := range m {
|
||||||
result = append(result, killWords.Global.Word)
|
result = append(result, killWords.Codename.KillWord.Word)
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
@@ -573,6 +811,23 @@ func (games Games) CreateEventGameComplete(ctx context.Context, id string) error
|
|||||||
return games.createEvent(ctx, id, EventGameComplete{Type: GameComplete})
|
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 {
|
func (games Games) createEvent(ctx context.Context, id string, v any) error {
|
||||||
payload, err := json.Marshal(v)
|
payload, err := json.Marshal(v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ func TestGames(t *testing.T) {
|
|||||||
t.Error(v)
|
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)
|
t.Error("err getting game by empty name for empty user:", err)
|
||||||
} else if len(v) > 0 {
|
} else if len(v) > 0 {
|
||||||
t.Error(v)
|
t.Error(v)
|
||||||
@@ -56,7 +56,7 @@ func TestGames(t *testing.T) {
|
|||||||
t.Fatal("redundant create game didnt return same id:", id2)
|
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)
|
t.Fatal("err creating event player join:", err)
|
||||||
} else if err := games.CreateEventPlayerLeave(ctx, id, "p0"); err != nil {
|
} else if err := games.CreateEventPlayerLeave(ctx, id, "p0"); err != nil {
|
||||||
t.Fatal("err creating event player leave:", err)
|
t.Fatal("err creating event player leave:", err)
|
||||||
@@ -64,12 +64,12 @@ func TestGames(t *testing.T) {
|
|||||||
|
|
||||||
for i := 0; i < 4; i++ {
|
for i := 0; i < 4; i++ {
|
||||||
p := fmt.Sprintf("p%d", i+1)
|
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)
|
t.Fatal(p, "err creating event player join", err)
|
||||||
}
|
}
|
||||||
if name, err := games.UserName(ctx, p); err != nil {
|
if name, err := games.UserName(ctx, p); err != nil {
|
||||||
t.Fatal(p, "err getting user name", err)
|
t.Fatal(p, "err getting user name", err)
|
||||||
} else if name != "player "+p {
|
} else if name == "" {
|
||||||
t.Fatal("name wrong", name)
|
t.Fatal("name wrong", name)
|
||||||
}
|
}
|
||||||
if err := games.UpdateUserName(ctx, p, "player! "+p); err != nil {
|
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)
|
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)
|
t.Error("err getting game by name for user:", err)
|
||||||
} else if v != id {
|
} else if v != id {
|
||||||
t.Error("wrong game by name for user:", v)
|
t.Error("wrong game by name for user:", v)
|
||||||
@@ -128,8 +128,8 @@ func TestGames(t *testing.T) {
|
|||||||
if v.Players[p].Points() != 0 {
|
if v.Players[p].Points() != 0 {
|
||||||
t.Error("nonzero points after zero kills:", v.Players[p].Points())
|
t.Error("nonzero points after zero kills:", v.Players[p].Points())
|
||||||
}
|
}
|
||||||
if v.Players[p].KillWords.Global.Word == "" {
|
if v.Players[p].KillWords.Codename.KillWord.Word == "" {
|
||||||
t.Error(p, "no killwords.global")
|
t.Error(p, "no killwords.Codename")
|
||||||
} else if v.Players[p].KillWords.Assigned.IsZero() {
|
} else if v.Players[p].KillWords.Assigned.IsZero() {
|
||||||
t.Error(p, "no killwords.assigned")
|
t.Error(p, "no killwords.assigned")
|
||||||
} else if v.Players[p].KillWords.Assignee == "" {
|
} else if v.Players[p].KillWords.Assignee == "" {
|
||||||
@@ -149,6 +149,28 @@ func TestGames(t *testing.T) {
|
|||||||
} else if state.Completed.IsZero() {
|
} else if state.Completed.IsZero() {
|
||||||
t.Fatal("state.Completed is zero")
|
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{
|
AllKillWords: map[string]KillWords{
|
||||||
"x": KillWords{
|
"x": KillWords{
|
||||||
Global: KillWord{
|
Codename: Codename{KillWord: KillWord{
|
||||||
Word: "a",
|
Word: "a",
|
||||||
Points: -1,
|
Points: 200,
|
||||||
},
|
}},
|
||||||
Assignee: "z",
|
Assignee: "z",
|
||||||
Assigned: now,
|
Assigned: now,
|
||||||
Assignment: Assignment{
|
Assignment: Assignment{
|
||||||
@@ -238,48 +260,48 @@ func TestAllKillWordsFill(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
"full": {
|
"full": {
|
||||||
given: KillWords{
|
given: KillWords{
|
||||||
Global: kw(-1, "global"),
|
Codename: Codename{KillWord: kw(200, "global")},
|
||||||
Assignment: ass("pub", "pri"),
|
Assignment: ass("pub", "pri"),
|
||||||
},
|
},
|
||||||
expect: KillWords{
|
expect: KillWords{
|
||||||
Global: kw(-1, "global"),
|
Codename: Codename{KillWord: kw(200, "global")},
|
||||||
Assignment: ass("pub", "pri"),
|
Assignment: ass("pub", "pri"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"no ass": {
|
"no ass": {
|
||||||
given: KillWords{
|
given: KillWords{
|
||||||
Global: kw(-1, "global"),
|
Codename: Codename{KillWord: kw(200, "global")},
|
||||||
Assignment: Assignment{},
|
Assignment: Assignment{},
|
||||||
},
|
},
|
||||||
expect: KillWords{
|
expect: KillWords{
|
||||||
Global: kw(-1, "global"),
|
Codename: Codename{KillWord: kw(200, "global")},
|
||||||
Assignment: ass("filled-public", "filled-private"),
|
Assignment: ass("filled-public", "filled-private"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"no pub": {
|
"no pub": {
|
||||||
given: KillWords{
|
given: KillWords{
|
||||||
Global: kw(-1, "global"),
|
Codename: Codename{KillWord: kw(200, "global")},
|
||||||
Assignment: ass("", "pri"),
|
Assignment: ass("", "pri"),
|
||||||
},
|
},
|
||||||
expect: KillWords{
|
expect: KillWords{
|
||||||
Global: kw(-1, "global"),
|
Codename: Codename{KillWord: kw(200, "global")},
|
||||||
Assignment: ass("filled-public", "pri"),
|
Assignment: ass("filled-public", "pri"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"no pri": {
|
"no pri": {
|
||||||
given: KillWords{
|
given: KillWords{
|
||||||
Global: kw(-1, "global"),
|
Codename: Codename{KillWord: kw(200, "global")},
|
||||||
Assignment: ass("pub", ""),
|
Assignment: ass("pub", ""),
|
||||||
},
|
},
|
||||||
expect: KillWords{
|
expect: KillWords{
|
||||||
Global: kw(-1, "global"),
|
Codename: Codename{KillWord: kw(200, "global")},
|
||||||
Assignment: ass("pub", "filled-private"),
|
Assignment: ass("pub", "filled-private"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"empty": {
|
"empty": {
|
||||||
given: KillWords{},
|
given: KillWords{},
|
||||||
expect: KillWords{
|
expect: KillWords{
|
||||||
Global: kw(-1, "filled-global"),
|
Codename: Codename{KillWord: kw(200, "filled-global")},
|
||||||
Assignment: ass("filled-public", "filled-private"),
|
Assignment: ass("filled-public", "filled-private"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -288,7 +310,7 @@ func TestAllKillWordsFill(t *testing.T) {
|
|||||||
Assignment: ass("pub", "pri"),
|
Assignment: ass("pub", "pri"),
|
||||||
},
|
},
|
||||||
expect: KillWords{
|
expect: KillWords{
|
||||||
Global: kw(-1, "filled-global"),
|
Codename: Codename{KillWord: kw(200, "filled-global")},
|
||||||
Assignment: ass("pub", "pri"),
|
Assignment: ass("pub", "pri"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -332,7 +354,7 @@ func TestAllKillWordsUnused(t *testing.T) {
|
|||||||
t.Error("empty playerbase didnt think only option was unused")
|
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")
|
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) {
|
t.Run("private", func(t *testing.T) {
|
||||||
akw := make(AllKillWords)
|
akw := make(AllKillWords)
|
||||||
akw["k"] = KillWords{
|
akw["k"] = KillWords{
|
||||||
Global: KillWord{Word: "x"},
|
Codename: Codename{KillWord: KillWord{Word: "x"}},
|
||||||
Assignment: Assignment{
|
Assignment: Assignment{
|
||||||
Private: []KillWord{{}, {Word: "y"}},
|
Private: []KillWord{{}, {Word: "y"}},
|
||||||
Public: []KillWord{{}, {Word: "x"}},
|
Public: []KillWord{{}, {Word: "x"}},
|
||||||
@@ -355,13 +377,13 @@ func TestAllKillWordsUnused(t *testing.T) {
|
|||||||
t.Run("global", func(t *testing.T) {
|
t.Run("global", func(t *testing.T) {
|
||||||
akw := make(AllKillWords)
|
akw := make(AllKillWords)
|
||||||
akw["k"] = KillWords{
|
akw["k"] = KillWords{
|
||||||
Global: KillWord{Word: "y"},
|
Codename: Codename{KillWord: KillWord{Word: "y"}},
|
||||||
Assignment: Assignment{
|
Assignment: Assignment{
|
||||||
Private: []KillWord{{}, {Word: "x"}},
|
Private: []KillWord{{}, {Word: "x"}},
|
||||||
Public: []KillWord{{}, {Word: "x"}},
|
Public: []KillWord{{}, {Word: "x"}},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
got := akw.unusedGlobal([]string{"x", "y"})
|
got := akw.unusedCodename([]string{"x", "y"})
|
||||||
if got != "x" {
|
if got != "x" {
|
||||||
t.Error("didnt return only unused option")
|
t.Error("didnt return only unused option")
|
||||||
}
|
}
|
||||||
@@ -369,7 +391,7 @@ func TestAllKillWordsUnused(t *testing.T) {
|
|||||||
t.Run("public", func(t *testing.T) {
|
t.Run("public", func(t *testing.T) {
|
||||||
akw := make(AllKillWords)
|
akw := make(AllKillWords)
|
||||||
akw["k"] = KillWords{
|
akw["k"] = KillWords{
|
||||||
Global: KillWord{Word: "x"},
|
Codename: Codename{KillWord: KillWord{Word: "x"}},
|
||||||
Assignment: Assignment{
|
Assignment: Assignment{
|
||||||
Private: []KillWord{{}, {Word: "x"}},
|
Private: []KillWord{{}, {Word: "x"}},
|
||||||
Public: []KillWord{{}, {Word: "y"}},
|
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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -59,13 +59,19 @@ type Session struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *S) injectContext(w http.ResponseWriter, r *http.Request) error {
|
func (s *S) injectContext(w http.ResponseWriter, r *http.Request) error {
|
||||||
id, err := r.Cookie("uuid")
|
id := r.Header.Get("uuid")
|
||||||
if err != nil || id.Value == "" {
|
if id == "" {
|
||||||
|
c, _ := r.Cookie("uuid")
|
||||||
|
if c != nil {
|
||||||
|
id = c.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if id == "" {
|
||||||
return io.EOF
|
return io.EOF
|
||||||
}
|
}
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
ctx = context.WithValue(ctx, "session", Session{
|
ctx = context.WithValue(ctx, "session", Session{
|
||||||
ID: id.Value,
|
ID: id,
|
||||||
})
|
})
|
||||||
*r = *r.WithContext(ctx)
|
*r = *r.WithContext(ctx)
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
170
cmd/server/usergameserver.go
Normal file
170
cmd/server/usergameserver.go
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"slices"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserGameServer struct {
|
||||||
|
ID string
|
||||||
|
Session Session
|
||||||
|
games Games
|
||||||
|
lastPoll time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUserGameServer(ctx context.Context, session Session, games Games) (*UserGameServer, error) {
|
||||||
|
ids, err := games.GamesForUser(ctx, session.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(ids) == 0 {
|
||||||
|
return nil, fmt.Errorf("user %s is in zero games", session.ID)
|
||||||
|
}
|
||||||
|
return &UserGameServer{
|
||||||
|
ID: ids[0],
|
||||||
|
Session: session,
|
||||||
|
games: games,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ugs *UserGameServer) More(ctx context.Context) error {
|
||||||
|
defer func() {
|
||||||
|
ugs.lastPoll = time.Now()
|
||||||
|
}()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
case <-time.After(time.Second * 1):
|
||||||
|
}
|
||||||
|
|
||||||
|
if events, err := ugs.games.GameEvents(ctx, ugs.ID, ugs.lastPoll); err != nil {
|
||||||
|
return err
|
||||||
|
} else if len(events) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ugs *UserGameServer) Listen(ctx context.Context, can context.CancelFunc, reader func(context.Context) ([]byte, error)) {
|
||||||
|
defer can()
|
||||||
|
if err := ugs.listen(ctx, reader); err != nil && ctx.Err() == nil {
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ugs *UserGameServer) listen(ctx context.Context, reader func(context.Context) ([]byte, error)) error {
|
||||||
|
for ctx.Err() == nil {
|
||||||
|
b, err := reader(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var m map[string]string
|
||||||
|
if err := json.Unmarshal(b, &m); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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
|
||||||
|
}
|
||||||
|
|
||||||
|
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 if err := ugs.games.CreateEventGameReset(ctx, ugs.ID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("UNKNOWN: %+v", m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
291
cmd/server/usergameserver_test.go
Normal file
291
cmd/server/usergameserver_test.go
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,9 +1,12 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path"
|
"path"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -12,8 +15,29 @@ func isV1(r *http.Request) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *S) serveV1(w http.ResponseWriter, r *http.Request) error {
|
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) {
|
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)
|
return s.serveV1PutParty(w, r)
|
||||||
default:
|
default:
|
||||||
http.NotFound(w, r)
|
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 {
|
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)
|
||||||
}
|
}
|
||||||
|
|||||||
272
cmd/server/ws.go
272
cmd/server/ws.go
@@ -4,13 +4,10 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/coder/websocket"
|
"github.com/coder/websocket"
|
||||||
)
|
)
|
||||||
@@ -19,166 +16,110 @@ func isWS(r *http.Request) bool {
|
|||||||
return r.URL.Path == "/ws" || strings.HasPrefix(r.URL.Path, "/ws/")
|
return r.URL.Path == "/ws" || strings.HasPrefix(r.URL.Path, "/ws/")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type WS struct {
|
||||||
|
*S
|
||||||
|
c *websocket.Conn
|
||||||
|
}
|
||||||
|
|
||||||
func (s *S) serveWS(w http.ResponseWriter, r *http.Request) error {
|
func (s *S) serveWS(w http.ResponseWriter, r *http.Request) error {
|
||||||
ctx, can := context.WithCancel(r.Context())
|
|
||||||
defer can()
|
|
||||||
r = r.WithContext(ctx)
|
|
||||||
|
|
||||||
session := s.Session(ctx)
|
|
||||||
games, err := s.games.GamesForUser(ctx, session.ID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if len(games) == 0 {
|
|
||||||
return fmt.Errorf("user %s is in zero games", session.ID)
|
|
||||||
}
|
|
||||||
game := games[0]
|
|
||||||
|
|
||||||
c, err := websocket.Accept(w, r, nil)
|
c, err := websocket.Accept(w, r, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer c.CloseNow()
|
defer c.CloseNow()
|
||||||
|
ws := WS{S: s, c: c}
|
||||||
|
return ws.Serve(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
go func() {
|
func (ws WS) Serve(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
ctx, can := context.WithCancel(r.Context())
|
||||||
defer can()
|
defer can()
|
||||||
for {
|
r = r.WithContext(ctx)
|
||||||
_, b, err := c.Read(ctx)
|
|
||||||
|
ugs, err := ws.newUserGameServer(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println(err)
|
return err
|
||||||
return
|
|
||||||
}
|
}
|
||||||
log.Printf("READ %s", b)
|
|
||||||
|
|
||||||
var m map[string]string
|
go ugs.Listen(ctx, can, func(ctx context.Context) ([]byte, error) {
|
||||||
if err := json.Unmarshal(b, &m); err != nil {
|
_, b, err := ws.c.Read(ctx)
|
||||||
log.Println(err)
|
return b, err
|
||||||
return
|
})
|
||||||
}
|
|
||||||
log.Printf("UNMARSHAL %+v", m)
|
|
||||||
|
|
||||||
if m["party"] == "start" {
|
for ugs.More(ctx) == nil {
|
||||||
if gameState, err := s.games.GameState(ctx, game); err != nil {
|
if err := ws.Push(ctx, ugs); err != nil {
|
||||||
log.Println(err)
|
return err
|
||||||
return
|
|
||||||
} else if gameState.Started {
|
|
||||||
} else if err := s.games.CreateEventAssignmentRotation(ctx, game, "", "", "", 0); err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} else if m["k"] != "" {
|
|
||||||
log.Println("TODO a kill occurred")
|
|
||||||
return
|
|
||||||
} else if name := m["name"]; name != "" {
|
|
||||||
if err := s.games.UpdateUserName(ctx, s.Session(ctx).ID, name); err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} else if m["again"] == "true" {
|
|
||||||
if gameState, err := s.games.GameState(ctx, game); err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
return
|
|
||||||
} else if gameState.Completed.IsZero() {
|
|
||||||
} else {
|
|
||||||
log.Println("TODO new game")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log.Printf("UNKNOWN: %+v", m)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
|
||||||
|
|
||||||
var last time.Time
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return ctx.Err()
|
return ctx.Err()
|
||||||
case <-time.After(time.Second * 1):
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if events, err := s.games.GameEvents(ctx, game, last); err != nil {
|
func (ws WS) newUserGameServer(ctx context.Context) (*UserGameServer, error) {
|
||||||
return err
|
return NewUserGameServer(ctx, ws.Session(ctx), ws.games)
|
||||||
} else if len(events) == 0 {
|
}
|
||||||
continue
|
|
||||||
}
|
|
||||||
last = time.Now()
|
|
||||||
|
|
||||||
gameState, err := s.games.GameState(ctx, game)
|
func (ws WS) Push(ctx context.Context, ugs *UserGameServer) error {
|
||||||
|
gameState, err := ugs.State(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
msg := map[string]any{
|
var msg map[string]any
|
||||||
"help": strings.Join([]string{
|
if unstarted := !gameState.Started; unstarted {
|
||||||
|
msg, err = ws.unstartedMsg(ctx, ugs, gameState)
|
||||||
|
} else if complete := !gameState.Completed.IsZero(); complete {
|
||||||
|
msg, err = ws.completeMsg(ctx, gameState)
|
||||||
|
} else {
|
||||||
|
msg, err = ws.inProgressMsg(ctx, ugs, gameState)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
msg["help"] = strings.Join([]string{
|
||||||
"CARD ASSASSINS (Mobile Ed.)",
|
"CARD ASSASSINS (Mobile Ed.)",
|
||||||
"",
|
"",
|
||||||
"1. Get any target to say any of his or her kill words.",
|
"1. Get any target to say any of his or her kill words.",
|
||||||
"2. Click on the kill word a target said.",
|
"2. Click the word to collect points.",
|
||||||
|
"3. Review new kill words.",
|
||||||
"",
|
"",
|
||||||
"The game ends when everyone has been assassinated.",
|
"The game ends when everyone has been assassinated.",
|
||||||
}, "<br>"),
|
}, "<br>")
|
||||||
}
|
|
||||||
|
msgB, _ := json.Marshal(msg)
|
||||||
|
return ws.c.Write(ctx, 1, msgB)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ws WS) unstartedMsg(ctx context.Context, ugs *UserGameServer, gameState UserGameState) (msg map[string]any, _ error) {
|
||||||
|
msg["page"] = "A"
|
||||||
|
|
||||||
if gameState.Started {
|
|
||||||
msg["page"] = "B"
|
|
||||||
if gameState.Completed.IsZero() {
|
|
||||||
msg["event"] = "A"
|
|
||||||
items := []map[string]any{}
|
items := []map[string]any{}
|
||||||
for k, v := range gameState.Players {
|
for k := range gameState.Players {
|
||||||
if k == s.Session(ctx).ID {
|
if k == ugs.Session.ID {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
name, err := s.games.UserName(ctx, k)
|
name, err := ws.games.UserName(ctx, k)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
items = append(items, map[string]any{"id": k, "name": name})
|
||||||
|
}
|
||||||
|
msg["items"] = items
|
||||||
|
|
||||||
tags := []map[string]any{}
|
return msg, nil
|
||||||
if self := gameState.Players[s.Session(ctx).ID]; self.KillWords.Assignee == k {
|
}
|
||||||
for _, private := range v.KillWords.Assignment.Private {
|
|
||||||
tags = append(tags, map[string]any{
|
|
||||||
"k": private.Word,
|
|
||||||
"v": private.Points,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, public := range v.KillWords.Assignment.Public {
|
|
||||||
tags = append(tags, map[string]any{
|
|
||||||
"k": public.Word,
|
|
||||||
"v": public.Points,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if self := gameState.Players[s.Session(ctx).ID]; !slices.ContainsFunc(self.Kills, func(a Kill) bool {
|
|
||||||
return a.Victim == k
|
|
||||||
}) {
|
|
||||||
tags = append(tags, map[string]any{
|
|
||||||
"k": self.KillWords.Global.Word,
|
|
||||||
"v": self.KillWords.Global.Points,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
items = append(items, map[string]any{
|
func (ws WS) completeMsg(ctx context.Context, gameState UserGameState) (msg map[string]any, _ error) {
|
||||||
"name": name,
|
msg["page"] = "B"
|
||||||
"title": strconv.Itoa(v.Points()),
|
|
||||||
"tags": tags,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
slices.SortFunc(items, func(a, b map[string]any) int {
|
|
||||||
an, _ := strconv.Atoi(fmt.Sprint(a["title"]))
|
|
||||||
bn, _ := strconv.Atoi(fmt.Sprint(b["title"]))
|
|
||||||
return an - bn
|
|
||||||
})
|
|
||||||
return io.EOF
|
|
||||||
} else {
|
|
||||||
msg["event"] = "B"
|
msg["event"] = "B"
|
||||||
|
|
||||||
items := []map[string]any{}
|
items := []map[string]any{}
|
||||||
for k, v := range gameState.Players {
|
for k, v := range gameState.Players {
|
||||||
name, err := s.games.UserName(ctx, k)
|
name, err := ws.games.UserName(ctx, k)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
tags := []map[string]any{}
|
tags := []map[string]any{}
|
||||||
for _, kill := range v.Kills {
|
for _, kill := range v.Kills {
|
||||||
@@ -194,23 +135,84 @@ func (s *S) serveWS(w http.ResponseWriter, r *http.Request) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
msg["items"] = items
|
msg["items"] = items
|
||||||
}
|
return msg, nil
|
||||||
} else {
|
}
|
||||||
msg["page"] = "A"
|
|
||||||
items := []map[string]any{}
|
func (ws WS) inProgressMsg(ctx context.Context, ugs *UserGameServer, gameState UserGameState) (msg map[string]any, _ error) {
|
||||||
for k := range gameState.Players {
|
msg["page"] = "B"
|
||||||
name, err := s.games.UserName(ctx, k)
|
msg["event"] = "A"
|
||||||
|
|
||||||
|
items, err := ws.inProgressMsgItems(ctx, ugs, gameState)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
|
||||||
items = append(items, map[string]any{"name": name})
|
|
||||||
}
|
}
|
||||||
msg["items"] = items
|
msg["items"] = items
|
||||||
|
|
||||||
|
return msg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type inProgressMsgItem struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Tags []inProgressMsgItemTag `json:"tags"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type inProgressMsgItemTag struct {
|
||||||
|
K string `json:"k"`
|
||||||
|
V int `json:"v,string"`
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if item == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
items = append(items, *item)
|
||||||
|
}
|
||||||
|
slices.SortFunc(items, func(a, b inProgressMsgItem) int {
|
||||||
|
an, _ := strconv.Atoi(a.Title)
|
||||||
|
bn, _ := strconv.Atoi(b.Title)
|
||||||
|
return an - bn
|
||||||
|
})
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
self := gameState.Players[ugs.Session.ID]
|
||||||
|
v := gameState.Players[uid]
|
||||||
|
|
||||||
|
tags := []inProgressMsgItemTag{}
|
||||||
|
|
||||||
|
if canKillWithCodename := !self.KillWords.Codename.Consumed; canKillWithCodename {
|
||||||
|
tags = append(tags, newInProgressMsgItemTag(self.KillWords.Codename.KillWord))
|
||||||
}
|
}
|
||||||
|
|
||||||
msgB, _ := json.Marshal(msg)
|
for _, killWord := range append(
|
||||||
if err := c.Write(ctx, 1, msgB); err != nil {
|
v.KillWords.Publics(),
|
||||||
return err
|
v.KillWords.Privates()...,
|
||||||
|
) {
|
||||||
|
tags = append(tags, newInProgressMsgItemTag(killWord))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
name, err := ws.games.UserName(ctx, uid)
|
||||||
|
return &inProgressMsgItem{
|
||||||
|
Name: name,
|
||||||
|
Title: strconv.Itoa(v.Points()),
|
||||||
|
Tags: tags,
|
||||||
|
}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func newInProgressMsgItemTag(kw KillWord) inProgressMsgItemTag {
|
||||||
|
return inProgressMsgItemTag{
|
||||||
|
K: kw.Word,
|
||||||
|
V: kw.Points,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/rand"
|
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"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 {
|
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) {
|
if isV1(r) || isWS(r) {
|
||||||
return s.serveAPI(w, r)
|
return s.serveAPI(w, r)
|
||||||
}
|
}
|
||||||
@@ -113,13 +122,17 @@ type Session struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *S) injectContext(w http.ResponseWriter, r *http.Request) error {
|
func (s *S) injectContext(w http.ResponseWriter, r *http.Request) error {
|
||||||
id, err := r.Cookie("uuid")
|
id := r.URL.Query().Get("uuid")
|
||||||
if err != nil || id.Value == "" {
|
if id == "" {
|
||||||
|
c, err := r.Cookie("uuid")
|
||||||
|
if err != nil || c.Value == "" {
|
||||||
return io.EOF
|
return io.EOF
|
||||||
}
|
}
|
||||||
|
id = c.Value
|
||||||
|
}
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
ctx = context.WithValue(ctx, "session", Session{
|
ctx = context.WithValue(ctx, "session", Session{
|
||||||
ID: id.Value,
|
ID: id,
|
||||||
})
|
})
|
||||||
*r = *r.WithContext(ctx)
|
*r = *r.WithContext(ctx)
|
||||||
return nil
|
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 {
|
func (s *S) serveWS(w http.ResponseWriter, r *http.Request) error {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
|
|
||||||
c, err := websocket.Accept(w, r, nil)
|
c, err := websocket.Accept(w, r, &websocket.AcceptOptions{
|
||||||
|
InsecureSkipVerify: true,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -177,12 +192,12 @@ func (s *S) serveV1(w http.ResponseWriter, r *http.Request) error {
|
|||||||
switch r.Method + r.URL.Path {
|
switch r.Method + r.URL.Path {
|
||||||
case "GET/v1/state/" + s.Session(ctx).ID:
|
case "GET/v1/state/" + s.Session(ctx).ID:
|
||||||
if rand.Int()%2 == 0 {
|
if rand.Int()%2 == 0 {
|
||||||
w.Write(`{"name": "foo"}`)
|
w.Write([]byte(`{"name": "foo"}`))
|
||||||
} else {
|
} 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":
|
case "PUT/v1/state/" + s.Session(ctx).ID + "/party":
|
||||||
w.Write(`{}`)
|
w.Write([]byte(`{}`))
|
||||||
default:
|
default:
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
16
cmd/testws/index.html
Normal file
16
cmd/testws/index.html
Normal 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>
|
||||||
24
todo.yaml
24
todo.yaml
@@ -1,12 +1,28 @@
|
|||||||
todo:
|
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
|
- test ws flow
|
||||||
- notifications system with dismissal server-side so users see X got a kill
|
- notifications system with dismissal server-side so users see X got a kill
|
||||||
- play mp3 on kill + shuffle
|
- play mp3 on kill + shuffle
|
||||||
- end condition; everyone has died
|
- end condition; everyone has died
|
||||||
- word lists; already got holidays
|
- word lists; already got holidays
|
||||||
- event driven woulda been nice
|
- event driven woulda been nice
|
||||||
- remake
|
- leave game
|
||||||
- quit
|
|
||||||
scheduled: []
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user