Compare commits
9 Commits
main
...
fbf512c0d2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fbf512c0d2 | ||
|
|
2d87e78556 | ||
|
|
aadc23c45b | ||
|
|
d597a74a2b | ||
|
|
0bade0bd4f | ||
|
|
85fdfac888 | ||
|
|
d3689e36a7 | ||
|
|
91357afb1f | ||
|
|
73e79212a9 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,3 +1 @@
|
|||||||
/cmd/server/server
|
/cmd/server/server
|
||||||
**/exec-*
|
|
||||||
**/.*.sw*
|
|
||||||
|
|||||||
BIN
cmd/server/.games.go.swp
Normal file
BIN
cmd/server/.games.go.swp
Normal file
Binary file not shown.
@@ -1,249 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -3,6 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"io"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -13,19 +14,16 @@ type DB struct {
|
|||||||
scheme string
|
scheme string
|
||||||
conn string
|
conn string
|
||||||
rw *sync.RWMutex
|
rw *sync.RWMutex
|
||||||
locked *bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDB(ctx context.Context, scheme, conn string) (DB, error) {
|
func NewDB(ctx context.Context, scheme, conn string) (DB, error) {
|
||||||
ctx, can := context.WithTimeout(ctx, time.Second*10)
|
ctx, can := context.WithTimeout(ctx, time.Second*10)
|
||||||
defer can()
|
defer can()
|
||||||
|
|
||||||
locked := false
|
|
||||||
db := DB{
|
db := DB{
|
||||||
scheme: scheme,
|
scheme: scheme,
|
||||||
conn: conn,
|
conn: conn,
|
||||||
rw: &sync.RWMutex{},
|
rw: &sync.RWMutex{},
|
||||||
locked: &locked,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sql, err := db.dial(ctx)
|
sql, err := db.dial(ctx)
|
||||||
@@ -40,18 +38,12 @@ func NewDB(ctx context.Context, scheme, conn string) (DB, error) {
|
|||||||
func (db DB) WithLock(cb func() error) error {
|
func (db DB) WithLock(cb func() error) error {
|
||||||
db.rw.Lock()
|
db.rw.Lock()
|
||||||
defer db.rw.Unlock()
|
defer db.rw.Unlock()
|
||||||
*db.locked = true
|
|
||||||
defer func() {
|
|
||||||
*db.locked = false
|
|
||||||
}()
|
|
||||||
return cb()
|
return cb()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db DB) Exec(ctx context.Context, q string, args ...any) error {
|
func (db DB) Exec(ctx context.Context, q string, args ...any) error {
|
||||||
if !*db.locked {
|
db.rw.RLock()
|
||||||
db.rw.RLock()
|
defer db.rw.RUnlock()
|
||||||
defer db.rw.RUnlock()
|
|
||||||
}
|
|
||||||
return db.exec(ctx, q, args...)
|
return db.exec(ctx, q, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,10 +59,8 @@ func (db DB) exec(ctx context.Context, q string, args ...any) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (db DB) Query(ctx context.Context, cb func(*sql.Rows) error, q string, args ...any) error {
|
func (db DB) Query(ctx context.Context, cb func(*sql.Rows) error, q string, args ...any) error {
|
||||||
if !*db.locked {
|
db.rw.RLock()
|
||||||
db.rw.RLock()
|
defer db.rw.RUnlock()
|
||||||
defer db.rw.RUnlock()
|
|
||||||
}
|
|
||||||
return db.query(ctx, cb, q, args...)
|
return db.query(ctx, cb, q, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,3 +96,7 @@ func (db DB) dial(ctx context.Context) (*sql.DB, error) {
|
|||||||
}
|
}
|
||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (db DB) GetParty(id string) (string, error) {
|
||||||
|
return "", io.EOF
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,95 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"database/sql"
|
|
||||||
"path"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func newTestDB(t *testing.T) DB {
|
|
||||||
ctx, can := context.WithTimeout(context.Background(), time.Minute)
|
|
||||||
defer can()
|
|
||||||
conn := path.Join(t.TempDir(), "db")
|
|
||||||
|
|
||||||
db, err := NewDB(ctx, "sqlite", conn)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
return db
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDB(t *testing.T) {
|
|
||||||
ctx := context.Background()
|
|
||||||
db := newTestDB(t)
|
|
||||||
|
|
||||||
t.Run("with lock", func(t *testing.T) {
|
|
||||||
var called [2]bool
|
|
||||||
if err := db.WithLock(func() error {
|
|
||||||
for i := range called {
|
|
||||||
if err := db.Query(ctx, func(rows *sql.Rows) error {
|
|
||||||
return rows.Scan(&called[i])
|
|
||||||
}, `SELECT true`); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if !called[0] {
|
|
||||||
t.Error(0)
|
|
||||||
}
|
|
||||||
if !called[1] {
|
|
||||||
t.Error(1)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("exec, query", func(t *testing.T) {
|
|
||||||
if err := db.Exec(ctx, `
|
|
||||||
CREATE TABLE IF NOT EXISTS my_table (
|
|
||||||
text TEXT,
|
|
||||||
datetime DATETIME,
|
|
||||||
number NUMBER
|
|
||||||
)
|
|
||||||
`); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := db.Exec(ctx, `
|
|
||||||
INSERT INTO my_table (
|
|
||||||
text,
|
|
||||||
datetime,
|
|
||||||
number
|
|
||||||
) VALUES (?, ?, ?)
|
|
||||||
`, "text", time.Now(), 1); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var text string
|
|
||||||
var datetime time.Time
|
|
||||||
var number int
|
|
||||||
if err := db.Query(ctx, func(rows *sql.Rows) error {
|
|
||||||
return rows.Scan(&text, &datetime, &number)
|
|
||||||
}, `
|
|
||||||
SELECT
|
|
||||||
text,
|
|
||||||
datetime,
|
|
||||||
number
|
|
||||||
FROM my_table
|
|
||||||
`); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if text != "text" {
|
|
||||||
t.Error(text)
|
|
||||||
}
|
|
||||||
if datetime.IsZero() {
|
|
||||||
t.Error(datetime)
|
|
||||||
}
|
|
||||||
if number != 1 {
|
|
||||||
t.Error(number)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -3,17 +3,11 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
_ "embed"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"maps"
|
"io"
|
||||||
"math/rand"
|
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
"unicode"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Games struct {
|
type Games struct {
|
||||||
@@ -57,126 +51,19 @@ func (games Games) GamesForUser(ctx context.Context, id string) ([]string, error
|
|||||||
}, `
|
}, `
|
||||||
SELECT players.game_uuid
|
SELECT players.game_uuid
|
||||||
FROM players
|
FROM players
|
||||||
WHERE players.user_uuid=?
|
WHERE players.user_uuid=?
|
||||||
`, id)
|
|
||||||
return result, err
|
|
||||||
}
|
|
||||||
|
|
||||||
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 := ""
|
|
||||||
err := games.db.Query(ctx, func(rows *sql.Rows) error {
|
|
||||||
return rows.Scan(&result)
|
|
||||||
}, `
|
|
||||||
SELECT users.name
|
|
||||||
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 {
|
|
||||||
points += kill.KillWord.Points
|
|
||||||
}
|
|
||||||
return points
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
games.name
|
|
||||||
FROM
|
|
||||||
games
|
|
||||||
WHERE games.uuid=?
|
|
||||||
ORDER BY games.updated DESC
|
|
||||||
LIMIT 1
|
|
||||||
`, id)
|
`, id)
|
||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (games Games) GameByName(ctx context.Context, name string) (string, error) {
|
func (games Games) GameByName(ctx context.Context, name string) (string, error) {
|
||||||
var result string
|
return "", io.EOF
|
||||||
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 (
|
type (
|
||||||
GameState struct {
|
GameState struct {
|
||||||
ID string
|
|
||||||
Started bool
|
|
||||||
Completed time.Time
|
Completed time.Time
|
||||||
|
Players map[string]PlayerState
|
||||||
Players map[string]PlayerState
|
|
||||||
|
|
||||||
Trial Trial
|
|
||||||
}
|
}
|
||||||
|
|
||||||
PlayerState struct {
|
PlayerState struct {
|
||||||
@@ -187,11 +74,11 @@ type (
|
|||||||
Kill struct {
|
Kill struct {
|
||||||
Timestamp time.Time
|
Timestamp time.Time
|
||||||
Victim string
|
Victim string
|
||||||
KillWord KillWord
|
Public bool
|
||||||
}
|
}
|
||||||
|
|
||||||
KillWords struct {
|
KillWords struct {
|
||||||
Codename Codename
|
Global string
|
||||||
|
|
||||||
Assigned time.Time
|
Assigned time.Time
|
||||||
Assignee string
|
Assignee string
|
||||||
@@ -199,75 +86,26 @@ type (
|
|||||||
Assignment Assignment
|
Assignment Assignment
|
||||||
}
|
}
|
||||||
|
|
||||||
Codename struct {
|
|
||||||
KillWord KillWord
|
|
||||||
Consumed bool
|
|
||||||
}
|
|
||||||
|
|
||||||
Assignment struct {
|
Assignment struct {
|
||||||
Public []KillWord
|
Public []string
|
||||||
Private []KillWord
|
Private []string
|
||||||
}
|
|
||||||
|
|
||||||
KillWord struct {
|
|
||||||
Word string
|
|
||||||
Points int
|
|
||||||
}
|
|
||||||
|
|
||||||
Trial struct {
|
|
||||||
Prosecutor string
|
|
||||||
Defendant string
|
|
||||||
Word string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
EventType int
|
EventType int
|
||||||
|
|
||||||
EventPlayerJoin struct {
|
EventPlayerJoin struct {
|
||||||
Type EventType
|
ID string
|
||||||
Timestamp time.Time
|
|
||||||
ID string
|
|
||||||
}
|
}
|
||||||
EventPlayerLeave struct {
|
EventPlayerLeave struct {
|
||||||
Type EventType
|
ID string
|
||||||
Timestamp time.Time
|
|
||||||
ID string
|
|
||||||
}
|
|
||||||
EventGameComplete struct {
|
|
||||||
Type EventType
|
|
||||||
Timestamp time.Time
|
|
||||||
}
|
}
|
||||||
|
EventGameComplete struct{}
|
||||||
EventAssignmentRotation struct {
|
EventAssignmentRotation struct {
|
||||||
Type EventType
|
Killer string
|
||||||
Timestamp time.Time
|
Killed string
|
||||||
Killer string
|
KillWord string
|
||||||
Victim string
|
KillWords map[string]KillWords
|
||||||
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
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -275,146 +113,60 @@ const (
|
|||||||
PlayerLeave
|
PlayerLeave
|
||||||
GameComplete
|
GameComplete
|
||||||
AssignmentRotation
|
AssignmentRotation
|
||||||
GameReset
|
|
||||||
CodenameAccusal
|
|
||||||
CodenameTrial
|
|
||||||
Notification
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Event interface{ event() }
|
func (games Games) GameState(ctx context.Context, id string) (GameState, error) {
|
||||||
|
result := GameState{Players: map[string]PlayerState{}}
|
||||||
|
|
||||||
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) {
|
|
||||||
case EventPlayerJoin:
|
|
||||||
e.Timestamp = t
|
|
||||||
event = e
|
|
||||||
case EventPlayerLeave:
|
|
||||||
e.Timestamp = t
|
|
||||||
event = e
|
|
||||||
case EventGameComplete:
|
|
||||||
e.Timestamp = t
|
|
||||||
event = e
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
func (games Games) GameEvents(ctx context.Context, id string, since time.Time) ([]Event, error) {
|
|
||||||
var results []Event
|
|
||||||
err := games.db.Query(ctx, func(rows *sql.Rows) error {
|
err := games.db.Query(ctx, func(rows *sql.Rows) error {
|
||||||
var timestamp time.Time
|
var timestamp time.Time
|
||||||
var b []byte
|
var payload []byte
|
||||||
if err := rows.Scan(×tamp, &b); err != nil {
|
if err := rows.Scan(×tamp, &payload); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
event, err := parseEvent(b, timestamp)
|
|
||||||
results = append(results, event)
|
|
||||||
return err
|
|
||||||
}, `
|
|
||||||
SELECT
|
|
||||||
timestamp,
|
|
||||||
payload
|
|
||||||
FROM events
|
|
||||||
WHERE game_uuid=? and timestamp>?
|
|
||||||
ORDER BY timestamp ASC
|
|
||||||
`, id, since)
|
|
||||||
return results, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseEvent(b []byte, timestamp time.Time) (Event, error) {
|
var peek struct {
|
||||||
var peek struct {
|
Type EventType
|
||||||
Type EventType
|
}
|
||||||
}
|
if err := json.Unmarshal(payload, &peek); err != nil {
|
||||||
if err := json.Unmarshal(b, &peek); err != nil {
|
return err
|
||||||
return nil, err
|
}
|
||||||
}
|
|
||||||
switch peek.Type {
|
|
||||||
case PlayerJoin:
|
|
||||||
var v EventPlayerJoin
|
|
||||||
err := json.Unmarshal(b, &v)
|
|
||||||
return EventWithTime(v, timestamp), err
|
|
||||||
case PlayerLeave:
|
|
||||||
var v EventPlayerLeave
|
|
||||||
err := json.Unmarshal(b, &v)
|
|
||||||
return EventWithTime(v, timestamp), err
|
|
||||||
case GameComplete:
|
|
||||||
var v EventGameComplete
|
|
||||||
err := json.Unmarshal(b, &v)
|
|
||||||
return EventWithTime(v, timestamp), err
|
|
||||||
case AssignmentRotation:
|
|
||||||
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) {
|
switch peek.Type {
|
||||||
result := GameState{ID: id, Players: map[string]PlayerState{}}
|
case PlayerJoin:
|
||||||
|
var playerJoin EventPlayerJoin
|
||||||
events, err := games.GameEvents(ctx, id, time.Time{})
|
if err := json.Unmarshal(payload, &playerJoin); err != nil {
|
||||||
if err != nil {
|
return err
|
||||||
return result, err
|
}
|
||||||
}
|
result.Players[playerJoin.ID] = PlayerState{}
|
||||||
|
return nil
|
||||||
for _, event := range events {
|
case PlayerLeave:
|
||||||
switch e := event.(type) {
|
var playerLeave EventPlayerLeave
|
||||||
case EventPlayerJoin:
|
if err := json.Unmarshal(payload, &playerLeave); err != nil {
|
||||||
result.Players[e.ID] = PlayerState{}
|
return err
|
||||||
case EventPlayerLeave:
|
}
|
||||||
delete(result.Players, e.ID)
|
delete(result.Players, playerLeave.ID)
|
||||||
case EventGameComplete:
|
return nil
|
||||||
result.Completed = e.Timestamp
|
case GameComplete:
|
||||||
case EventAssignmentRotation:
|
var gameComplete EventGameComplete
|
||||||
result.Started = true
|
if err := json.Unmarshal(payload, &gameComplete); err != nil {
|
||||||
assignmentRotation := e
|
return err
|
||||||
|
}
|
||||||
|
result.Completed = timestamp
|
||||||
|
return nil
|
||||||
|
case AssignmentRotation:
|
||||||
|
var assignmentRotation EventAssignmentRotation
|
||||||
|
if err := json.Unmarshal(payload, &assignmentRotation); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
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 victim, ok := result.Players[assignmentRotation.Killed]; !ok {
|
||||||
} else {
|
} else {
|
||||||
killer.Kills = append(killer.Kills, Kill{
|
killer.Kills = append(killer.Kills, Kill{
|
||||||
Timestamp: assignmentRotation.Timestamp,
|
Timestamp: timestamp,
|
||||||
Victim: assignmentRotation.Victim,
|
Victim: assignmentRotation.Killed,
|
||||||
KillWord: assignmentRotation.KillWord,
|
Public: slices.Contains(victim.KillWords.Assignment.Public, assignmentRotation.KillWord),
|
||||||
})
|
})
|
||||||
result.Players[assignmentRotation.Killer] = killer
|
result.Players[assignmentRotation.Killer] = killer
|
||||||
}
|
}
|
||||||
@@ -424,419 +176,56 @@ func (games Games) GameState(ctx context.Context, id string) (GameState, error)
|
|||||||
result.Players[k] = v
|
result.Players[k] = v
|
||||||
}
|
}
|
||||||
|
|
||||||
for k, v := range assignmentRotation.AllKillWords {
|
for k, v := range assignmentRotation.KillWords {
|
||||||
player := result.Players[k]
|
player := result.Players[k]
|
||||||
player.KillWords = v
|
player.KillWords = v
|
||||||
result.Players[k] = player
|
result.Players[k] = player
|
||||||
}
|
}
|
||||||
case EventCodenameAccusal:
|
return nil
|
||||||
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 fmt.Errorf("unknown event type %d: %s", peek.Type, payload)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
return nil
|
||||||
|
}, `
|
||||||
|
SELECT timestamp, payload
|
||||||
|
FROM events
|
||||||
|
WHERE game_uuid=?
|
||||||
|
`, id)
|
||||||
|
|
||||||
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) {
|
|
||||||
var exists string
|
|
||||||
if err := games.db.Query(ctx,
|
|
||||||
func(rows *sql.Rows) error {
|
|
||||||
return rows.Scan(&exists)
|
|
||||||
},
|
|
||||||
`
|
|
||||||
SELECT uuid
|
|
||||||
FROM games
|
|
||||||
WHERE name=? AND completed IS NULL
|
|
||||||
`, name); err != nil {
|
|
||||||
return "", err
|
|
||||||
} else if exists != "" {
|
|
||||||
return exists, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
id := uuid.New().String()
|
|
||||||
return id, games.db.Exec(ctx, `
|
|
||||||
INSERT INTO games (
|
|
||||||
uuid,
|
|
||||||
updated,
|
|
||||||
name
|
|
||||||
) VALUES (?, ?, ?)
|
|
||||||
`, id, time.Now(), name)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (games Games) CreateEventPlayerJoin(ctx context.Context, id string, player string) error {
|
func (games Games) CreateEventPlayerJoin(ctx context.Context, id string, player string) error {
|
||||||
if _, err := games.UserName(ctx, player); err != nil {
|
return games.createEvent(ctx, id, EventPlayerJoin{ID: player})
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := games.db.Exec(ctx, `
|
|
||||||
INSERT INTO players (
|
|
||||||
game_uuid,
|
|
||||||
user_uuid
|
|
||||||
) VALUES (?, ?);
|
|
||||||
`, id, player); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return games.createEvent(ctx, id, EventPlayerJoin{Type: PlayerJoin, ID: player})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (games Games) CreateEventPlayerLeave(ctx context.Context, id string, player string) error {
|
func (games Games) CreateEventPlayerLeave(ctx context.Context, id string, player string) error {
|
||||||
return games.createEvent(ctx, id, EventPlayerLeave{Type: PlayerLeave, ID: player})
|
return games.createEvent(ctx, id, EventPlayerLeave{ID: player})
|
||||||
}
|
|
||||||
|
|
||||||
func (games Games) CreateEventAssignmentRotation(ctx context.Context, id string, killer, victim, word string, points int) error {
|
|
||||||
state, err := games.GameState(ctx, id)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
event := EventAssignmentRotation{
|
|
||||||
Type: AssignmentRotation,
|
|
||||||
Killer: killer,
|
|
||||||
Victim: victim,
|
|
||||||
KillWord: KillWord{
|
|
||||||
Word: word,
|
|
||||||
Points: points,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
prevAllKillWords := state.AllKillWords()
|
|
||||||
|
|
||||||
event.AllKillWords = prevAllKillWords.ShuffleAssignees(killer, victim, word)
|
|
||||||
event.AllKillWords = event.AllKillWords.FillKillWords()
|
|
||||||
|
|
||||||
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 {
|
|
||||||
m := prev.withoutAssignees()
|
|
||||||
|
|
||||||
if _, ok := prev[killer]; !ok {
|
|
||||||
} else if victimState, ok := prev[victim]; !ok {
|
|
||||||
} else if killer != victimState.Assignee { // if victim was targeting killer, just randomize
|
|
||||||
m.assign(killer, victimState.Assignee)
|
|
||||||
}
|
|
||||||
|
|
||||||
for !func() bool {
|
|
||||||
allKillWords := maps.Clone(m)
|
|
||||||
unassigned := allKillWords.unassigned()
|
|
||||||
pop := func() string {
|
|
||||||
result := unassigned[0]
|
|
||||||
unassigned = unassigned[1:]
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
for k, v := range allKillWords {
|
|
||||||
if v.Assignee != "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
v.Assignee = pop()
|
|
||||||
if k == v.Assignee || prev[k].Assignee == v.Assignee {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
allKillWords[k] = v
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(unassigned) > 0 {
|
|
||||||
panic(unassigned)
|
|
||||||
}
|
|
||||||
|
|
||||||
m = allKillWords
|
|
||||||
return true
|
|
||||||
}() {
|
|
||||||
}
|
|
||||||
|
|
||||||
return m
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m AllKillWords) assign(killer, victim string) {
|
|
||||||
v := m[killer]
|
|
||||||
v.Assignee = victim
|
|
||||||
m[killer] = v
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m AllKillWords) withoutAssignees() AllKillWords {
|
|
||||||
now := time.Now()
|
|
||||||
result := make(AllKillWords)
|
|
||||||
for k := range m {
|
|
||||||
result[k] = KillWords{
|
|
||||||
Codename: m[k].Codename,
|
|
||||||
Assigned: now,
|
|
||||||
Assignee: "",
|
|
||||||
Assignment: m[k].Assignment,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m AllKillWords) unassigned() []string {
|
|
||||||
var result []string
|
|
||||||
for k := range m {
|
|
||||||
if !slices.Contains(m.assigned(), k) {
|
|
||||||
result = append(result, k)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for i := range result {
|
|
||||||
j := rand.Intn(i + 1)
|
|
||||||
result[i], result[j] = result[j], result[i]
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m AllKillWords) assigned() []string {
|
|
||||||
var result []string
|
|
||||||
for k := range m {
|
|
||||||
v := m[k].Assignee
|
|
||||||
if v == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
result = append(result, v)
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
//go:embed holiday.txt
|
|
||||||
var wordsHoliday string
|
|
||||||
|
|
||||||
func (m AllKillWords) FillKillWords() AllKillWords {
|
|
||||||
return m.fillKillWords(
|
|
||||||
strings.Fields(wordsHoliday),
|
|
||||||
1,
|
|
||||||
strings.Fields(wordsHoliday), // TODO medium difficulty
|
|
||||||
2,
|
|
||||||
strings.Fields(wordsHoliday), // TODO hard difficulty
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m AllKillWords) fillKillWords(
|
|
||||||
poolCodename []string,
|
|
||||||
nPublic int,
|
|
||||||
poolPublic []string,
|
|
||||||
nPrivate int,
|
|
||||||
poolPrivate []string,
|
|
||||||
) AllKillWords {
|
|
||||||
result := maps.Clone(m)
|
|
||||||
m = result
|
|
||||||
for k, v := range m {
|
|
||||||
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 {
|
|
||||||
v.Assignment.Public = []KillWord{}
|
|
||||||
for i := 0; i < nPublic; i++ {
|
|
||||||
v.Assignment.Public = append(v.Assignment.Public, KillWord{Word: m.unusedPublic(poolPublic), Points: 50})
|
|
||||||
m[k] = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(v.Assignment.Private) == 0 {
|
|
||||||
v.Assignment.Private = []KillWord{}
|
|
||||||
for i := 0; i < nPrivate; i++ {
|
|
||||||
v.Assignment.Private = append(v.Assignment.Private, KillWord{Word: m.unusedPrivate(poolPrivate), Points: 100})
|
|
||||||
m[k] = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return m
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m AllKillWords) unusedCodename(pool []string) string {
|
|
||||||
inUse := func() []string {
|
|
||||||
result := []string{}
|
|
||||||
for _, killWords := range m {
|
|
||||||
result = append(result, killWords.Codename.KillWord.Word)
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
for {
|
|
||||||
picked := pool[rand.Intn(len(pool))]
|
|
||||||
if !slices.Contains(inUse(), picked) {
|
|
||||||
return picked
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m AllKillWords) unusedPrivate(pool []string) string {
|
|
||||||
inUse := func() []string {
|
|
||||||
result := []string{}
|
|
||||||
for _, killWords := range m {
|
|
||||||
for _, killWord := range killWords.Assignment.Private {
|
|
||||||
result = append(result, killWord.Word)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
for {
|
|
||||||
picked := pool[rand.Intn(len(pool))]
|
|
||||||
if !slices.Contains(inUse(), picked) {
|
|
||||||
return picked
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m AllKillWords) unusedPublic(pool []string) string {
|
|
||||||
inUse := func() []string {
|
|
||||||
result := []string{}
|
|
||||||
for _, killWords := range m {
|
|
||||||
for _, killWord := range killWords.Assignment.Public {
|
|
||||||
result = append(result, killWord.Word)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
for {
|
|
||||||
picked := pool[rand.Intn(len(pool))]
|
|
||||||
if !slices.Contains(inUse(), picked) {
|
|
||||||
return picked
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (games Games) CreateEventGameComplete(ctx context.Context, id string) error {
|
func (games Games) CreateEventGameComplete(ctx context.Context, id string) error {
|
||||||
if err := games.db.Exec(ctx, `
|
return games.createEvent(ctx, id, EventGameComplete{})
|
||||||
UPDATE games
|
|
||||||
SET completed=?, updated=?
|
|
||||||
WHERE uuid=?
|
|
||||||
`, time.Now(), time.Now(), id); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return games.createEvent(ctx, id, EventGameComplete{Type: GameComplete})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (games Games) CreateEventCodenameAccusal(ctx context.Context, gid, prosecutor, defendant, codename string) error {
|
func (games Games) CreateEventAssignmentRotation(ctx context.Context, id string, killer, killed, killWord string) error {
|
||||||
return fmt.Errorf("not impl: x accused y")
|
// TODO gather current assignees
|
||||||
return fmt.Errorf("not impl: x caught by y")
|
// TODO get victim's target
|
||||||
}
|
// TODO assign victim's target to killer
|
||||||
|
// TODO randomize everyone else so not the same as before AND not self
|
||||||
func (games Games) CreateEventCodenameTrial(ctx context.Context, gid string, guilty bool) error {
|
return io.EOF
|
||||||
return fmt.Errorf("not impl: x found guilty/notguilty")
|
return games.createEvent(ctx, id, v)
|
||||||
}
|
|
||||||
|
|
||||||
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(any)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return games.db.Exec(ctx, `
|
return games.db.Exec(ctx, `
|
||||||
INSERT INTO events (
|
INSERT INTO events (
|
||||||
game_uuid,
|
game_uuid,
|
||||||
timestamp,
|
timestamp,
|
||||||
payload
|
payload
|
||||||
) VALUES (?, ?, ?)
|
) VALUES (?, ?, ?)
|
||||||
|
|||||||
@@ -1,438 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func newTestGames(t *testing.T) Games {
|
|
||||||
db := newTestDB(t)
|
|
||||||
|
|
||||||
games, err := NewGames(context.Background(), db)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
return games
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGames(t *testing.T) {
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
t.Run("empty", func(t *testing.T) {
|
|
||||||
games := newTestGames(t)
|
|
||||||
|
|
||||||
if v, err := games.GamesForUser(ctx, ""); err != nil {
|
|
||||||
t.Error("err getting games for empty user:", err)
|
|
||||||
} else if len(v) > 0 {
|
|
||||||
t.Error(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
if v, err := games.GameState(ctx, ""); err != nil {
|
|
||||||
t.Error("err getting game state for empty:", err)
|
|
||||||
} else if len(v.Players) > 0 || !v.Completed.IsZero() {
|
|
||||||
t.Error(v)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("mvp", func(t *testing.T) {
|
|
||||||
games := newTestGames(t)
|
|
||||||
|
|
||||||
id, err := games.CreateGame(ctx, "g1")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("err creating game:", err)
|
|
||||||
} else if id2, err := games.CreateGame(ctx, "g1"); err != nil {
|
|
||||||
t.Fatal("err creating game redundantly:", err)
|
|
||||||
} else if id != id2 {
|
|
||||||
t.Fatal("redundant create game didnt return same id:", id2)
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < 4; i++ {
|
|
||||||
p := fmt.Sprintf("p%d", i+1)
|
|
||||||
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 == "" {
|
|
||||||
t.Fatal("name wrong", name)
|
|
||||||
}
|
|
||||||
if err := games.UpdateUserName(ctx, p, "player! "+p); err != nil {
|
|
||||||
t.Fatal(p, "failed to rename:", err)
|
|
||||||
} else if name, err := games.UserName(ctx, p); err != nil {
|
|
||||||
t.Fatal(p, "err getting user name", err)
|
|
||||||
} else if name != "player! "+p {
|
|
||||||
t.Fatal("updated name wrong", name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if events, err := games.GameEvents(ctx, id, time.Time{}); err != nil {
|
|
||||||
t.Fatal("failed to get player join, leave events:", err)
|
|
||||||
} else if len(events) != 6 {
|
|
||||||
t.Error("wrong number of events:", len(events))
|
|
||||||
}
|
|
||||||
|
|
||||||
now := time.Now()
|
|
||||||
if err := games.CreateEventAssignmentRotation(ctx, id, "", "", "", 1); err != nil {
|
|
||||||
t.Fatal("err creating rotation:", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if events, err := games.GameEvents(ctx, id, time.Time{}); err != nil {
|
|
||||||
t.Fatal("failed to get player join, leave events:", err)
|
|
||||||
} else if len(events) != 7 {
|
|
||||||
t.Error("wrong number of events:", len(events))
|
|
||||||
} else if events, err = games.GameEvents(ctx, id, now); err != nil {
|
|
||||||
t.Fatal("failed to get assignment rotation event:", err)
|
|
||||||
} else if len(events) != 1 {
|
|
||||||
t.Error("wrong number of events:", len(events))
|
|
||||||
} else if _, ok := events[0].(EventAssignmentRotation); !ok {
|
|
||||||
t.Errorf("not an assignment rotation event: %T", events[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
if v, err := games.GamesForUser(ctx, "p1"); err != nil {
|
|
||||||
t.Error("err getting games for user:", err)
|
|
||||||
} else if len(v) < 1 {
|
|
||||||
t.Error("no games found for user:", v)
|
|
||||||
} else if v[0] != id {
|
|
||||||
t.Error("wrong game found for user:", v)
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
if v, err := games.GameState(ctx, id); err != nil {
|
|
||||||
t.Error("err getting game state:", err)
|
|
||||||
} else if len(v.Players) != 4 || !v.Completed.IsZero() {
|
|
||||||
t.Error("wrong game state:", v)
|
|
||||||
} else {
|
|
||||||
for i := 0; i < 4; i++ {
|
|
||||||
p := fmt.Sprintf("p%d", i+1)
|
|
||||||
if v.Players[p].Points() != 0 {
|
|
||||||
t.Error("nonzero points after zero kills:", v.Players[p].Points())
|
|
||||||
}
|
|
||||||
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 == "" {
|
|
||||||
t.Error(p, "no killwords.assignee")
|
|
||||||
} else if len(v.Players[p].KillWords.Assignment.Public) == 0 {
|
|
||||||
t.Error(p, "no killwords.assigment.public")
|
|
||||||
} else if len(v.Players[p].KillWords.Assignment.Private) == 0 {
|
|
||||||
t.Error(p, "no killwords.assigment.private")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := games.CreateEventGameComplete(ctx, id); err != nil {
|
|
||||||
t.Fatal("err creating game complete:", err)
|
|
||||||
} else if state, err := games.GameState(ctx, id); err != nil {
|
|
||||||
t.Fatal("err fetching state after completing:", err)
|
|
||||||
} 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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParseEvent(t *testing.T) {
|
|
||||||
now := time.Now()
|
|
||||||
|
|
||||||
cases := map[string]Event{
|
|
||||||
"player join": EventPlayerJoin{
|
|
||||||
Type: PlayerJoin,
|
|
||||||
ID: "x",
|
|
||||||
},
|
|
||||||
"player leave": EventPlayerLeave{
|
|
||||||
Type: PlayerLeave,
|
|
||||||
ID: "x",
|
|
||||||
},
|
|
||||||
"game complete": EventGameComplete{
|
|
||||||
Type: GameComplete,
|
|
||||||
},
|
|
||||||
"assignment rotation": EventAssignmentRotation{
|
|
||||||
Type: AssignmentRotation,
|
|
||||||
Killer: "x",
|
|
||||||
Victim: "y",
|
|
||||||
KillWord: KillWord{
|
|
||||||
Word: "word",
|
|
||||||
Points: 1,
|
|
||||||
},
|
|
||||||
AllKillWords: map[string]KillWords{
|
|
||||||
"x": KillWords{
|
|
||||||
Codename: Codename{KillWord: KillWord{
|
|
||||||
Word: "a",
|
|
||||||
Points: 200,
|
|
||||||
}},
|
|
||||||
Assignee: "z",
|
|
||||||
Assigned: now,
|
|
||||||
Assignment: Assignment{
|
|
||||||
Public: []KillWord{{
|
|
||||||
Word: "word2",
|
|
||||||
Points: 2,
|
|
||||||
}},
|
|
||||||
Private: []KillWord{{
|
|
||||||
Word: "word3",
|
|
||||||
Points: 3,
|
|
||||||
}},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for name, d := range cases {
|
|
||||||
c := d
|
|
||||||
t.Run(name, func(t *testing.T) {
|
|
||||||
c := EventWithTime(c, now)
|
|
||||||
b, _ := json.Marshal(c)
|
|
||||||
got, err := parseEvent(b, now)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
gotb, _ := json.Marshal(got)
|
|
||||||
if string(b) != string(gotb) {
|
|
||||||
t.Errorf("expected (%T) %+v, but got (%T) %+v", c, c, got, got)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAllKillWordsFill(t *testing.T) {
|
|
||||||
kw := func(p int, w string) KillWord {
|
|
||||||
return KillWord{Word: w, Points: p}
|
|
||||||
}
|
|
||||||
kws := func(points int, w string) []KillWord {
|
|
||||||
if w == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return []KillWord{kw(points, w)}
|
|
||||||
}
|
|
||||||
ass := func(pub, pri string) Assignment {
|
|
||||||
return Assignment{
|
|
||||||
Public: kws(50, pub),
|
|
||||||
Private: kws(100, pri),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cases := map[string]struct {
|
|
||||||
given KillWords
|
|
||||||
expect KillWords
|
|
||||||
}{
|
|
||||||
"full": {
|
|
||||||
given: KillWords{
|
|
||||||
Codename: Codename{KillWord: kw(200, "global")},
|
|
||||||
Assignment: ass("pub", "pri"),
|
|
||||||
},
|
|
||||||
expect: KillWords{
|
|
||||||
Codename: Codename{KillWord: kw(200, "global")},
|
|
||||||
Assignment: ass("pub", "pri"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"no ass": {
|
|
||||||
given: KillWords{
|
|
||||||
Codename: Codename{KillWord: kw(200, "global")},
|
|
||||||
Assignment: Assignment{},
|
|
||||||
},
|
|
||||||
expect: KillWords{
|
|
||||||
Codename: Codename{KillWord: kw(200, "global")},
|
|
||||||
Assignment: ass("filled-public", "filled-private"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"no pub": {
|
|
||||||
given: KillWords{
|
|
||||||
Codename: Codename{KillWord: kw(200, "global")},
|
|
||||||
Assignment: ass("", "pri"),
|
|
||||||
},
|
|
||||||
expect: KillWords{
|
|
||||||
Codename: Codename{KillWord: kw(200, "global")},
|
|
||||||
Assignment: ass("filled-public", "pri"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"no pri": {
|
|
||||||
given: KillWords{
|
|
||||||
Codename: Codename{KillWord: kw(200, "global")},
|
|
||||||
Assignment: ass("pub", ""),
|
|
||||||
},
|
|
||||||
expect: KillWords{
|
|
||||||
Codename: Codename{KillWord: kw(200, "global")},
|
|
||||||
Assignment: ass("pub", "filled-private"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"empty": {
|
|
||||||
given: KillWords{},
|
|
||||||
expect: KillWords{
|
|
||||||
Codename: Codename{KillWord: kw(200, "filled-global")},
|
|
||||||
Assignment: ass("filled-public", "filled-private"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"no global": {
|
|
||||||
given: KillWords{
|
|
||||||
Assignment: ass("pub", "pri"),
|
|
||||||
},
|
|
||||||
expect: KillWords{
|
|
||||||
Codename: Codename{KillWord: kw(200, "filled-global")},
|
|
||||||
Assignment: ass("pub", "pri"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
equal := func(a, b KillWords) bool {
|
|
||||||
ba, _ := json.Marshal(a)
|
|
||||||
bb, _ := json.Marshal(b)
|
|
||||||
return bytes.Equal(ba, bb)
|
|
||||||
}
|
|
||||||
|
|
||||||
for name, d := range cases {
|
|
||||||
c := d
|
|
||||||
t.Run(name, func(t *testing.T) {
|
|
||||||
akw := make(AllKillWords)
|
|
||||||
akw[name] = c.given
|
|
||||||
akw = akw.fillKillWords(
|
|
||||||
[]string{"filled-global"},
|
|
||||||
1,
|
|
||||||
[]string{"filled-public"},
|
|
||||||
1,
|
|
||||||
[]string{"filled-private"},
|
|
||||||
)
|
|
||||||
got := akw[name]
|
|
||||||
|
|
||||||
if !equal(c.expect, got) {
|
|
||||||
t.Errorf("expected \n\t%+v but got \n\t%+v", c.expect, got)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAllKillWordsUnused(t *testing.T) {
|
|
||||||
t.Run("empty", func(t *testing.T) {
|
|
||||||
akw := make(AllKillWords)
|
|
||||||
if got := akw.unusedPublic([]string{"x"}); got != "x" {
|
|
||||||
t.Error("empty playerbase didnt think only option was unused")
|
|
||||||
}
|
|
||||||
|
|
||||||
if got := akw.unusedPrivate([]string{"x"}); got != "x" {
|
|
||||||
t.Error("empty playerbase didnt think only option was unused")
|
|
||||||
}
|
|
||||||
|
|
||||||
if got := akw.unusedCodename([]string{"x"}); got != "x" {
|
|
||||||
t.Error("empty playerbase didnt think only option was unused")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("dont return used", func(t *testing.T) {
|
|
||||||
t.Run("private", func(t *testing.T) {
|
|
||||||
akw := make(AllKillWords)
|
|
||||||
akw["k"] = KillWords{
|
|
||||||
Codename: Codename{KillWord: KillWord{Word: "x"}},
|
|
||||||
Assignment: Assignment{
|
|
||||||
Private: []KillWord{{}, {Word: "y"}},
|
|
||||||
Public: []KillWord{{}, {Word: "x"}},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
got := akw.unusedPrivate([]string{"x", "y"})
|
|
||||||
if got != "x" {
|
|
||||||
t.Error("didnt return only unused option")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
t.Run("global", func(t *testing.T) {
|
|
||||||
akw := make(AllKillWords)
|
|
||||||
akw["k"] = KillWords{
|
|
||||||
Codename: Codename{KillWord: KillWord{Word: "y"}},
|
|
||||||
Assignment: Assignment{
|
|
||||||
Private: []KillWord{{}, {Word: "x"}},
|
|
||||||
Public: []KillWord{{}, {Word: "x"}},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
got := akw.unusedCodename([]string{"x", "y"})
|
|
||||||
if got != "x" {
|
|
||||||
t.Error("didnt return only unused option")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
t.Run("public", func(t *testing.T) {
|
|
||||||
akw := make(AllKillWords)
|
|
||||||
akw["k"] = KillWords{
|
|
||||||
Codename: Codename{KillWord: KillWord{Word: "x"}},
|
|
||||||
Assignment: Assignment{
|
|
||||||
Private: []KillWord{{}, {Word: "x"}},
|
|
||||||
Public: []KillWord{{}, {Word: "y"}},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
got := akw.unusedPublic([]string{"x", "y"})
|
|
||||||
if got != "x" {
|
|
||||||
t.Error("didnt return only unused option")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
Advent
|
|
||||||
Angels
|
|
||||||
Announcement
|
|
||||||
Bells
|
|
||||||
Bethlehem
|
|
||||||
Blitzen
|
|
||||||
Candles
|
|
||||||
Candy
|
|
||||||
Candy canes
|
|
||||||
Cards
|
|
||||||
Cedar
|
|
||||||
Celebrate
|
|
||||||
Ceremonies
|
|
||||||
Chimney
|
|
||||||
Christmas cookies
|
|
||||||
Christmas tree
|
|
||||||
Cold
|
|
||||||
Comet
|
|
||||||
Cranberry sauce
|
|
||||||
Crowds
|
|
||||||
Cupid
|
|
||||||
Dancer
|
|
||||||
Dasher
|
|
||||||
December
|
|
||||||
Decorations
|
|
||||||
Dolls
|
|
||||||
Donner
|
|
||||||
Dressing
|
|
||||||
Eggnog
|
|
||||||
Elves
|
|
||||||
Family reunion
|
|
||||||
Festival
|
|
||||||
Fir
|
|
||||||
Frosty
|
|
||||||
Fruitcake
|
|
||||||
Gift boxes
|
|
||||||
Gifts
|
|
||||||
Goodwill
|
|
||||||
Greetings
|
|
||||||
Ham
|
|
||||||
Happy
|
|
||||||
Holiday
|
|
||||||
Holly
|
|
||||||
Holy
|
|
||||||
Icicles
|
|
||||||
Jolly
|
|
||||||
Lights
|
|
||||||
Lists
|
|
||||||
Merry
|
|
||||||
Miracle
|
|
||||||
Mistletoe
|
|
||||||
New Year
|
|
||||||
Noel
|
|
||||||
North Pole
|
|
||||||
Pageant
|
|
||||||
Parades
|
|
||||||
Party
|
|
||||||
Pie
|
|
||||||
Pine
|
|
||||||
Plum pudding
|
|
||||||
Poinsettia
|
|
||||||
Prancer
|
|
||||||
Presents
|
|
||||||
Pumpkin pie
|
|
||||||
Punch
|
|
||||||
Red/green
|
|
||||||
Reindeer
|
|
||||||
Ribbon
|
|
||||||
Rudolph
|
|
||||||
Sacred
|
|
||||||
Sales
|
|
||||||
Sauce
|
|
||||||
Scrooge
|
|
||||||
Season
|
|
||||||
Sled
|
|
||||||
Sleighbells
|
|
||||||
Snowflakes
|
|
||||||
Spirit
|
|
||||||
St. Nick
|
|
||||||
Stand
|
|
||||||
Star
|
|
||||||
Stickers
|
|
||||||
Stocking stuffers
|
|
||||||
Sweet potato
|
|
||||||
Tidings
|
|
||||||
Tinsel
|
|
||||||
Togetherness
|
|
||||||
Toys
|
|
||||||
Tradition
|
|
||||||
Traffic
|
|
||||||
Trips
|
|
||||||
Turkey
|
|
||||||
Vacation
|
|
||||||
Vixen
|
|
||||||
Winter
|
|
||||||
Worship
|
|
||||||
Wrapping paper
|
|
||||||
Wreath
|
|
||||||
Yule
|
|
||||||
Yuletide
|
|
||||||
@@ -59,19 +59,13 @@ 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 := r.Header.Get("uuid")
|
id, err := r.Cookie("uuid")
|
||||||
if id == "" {
|
if err != nil || id.Value == "" {
|
||||||
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,
|
ID: id.Value,
|
||||||
})
|
})
|
||||||
*r = *r.WithContext(ctx)
|
*r = *r.WithContext(ctx)
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -1,170 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,291 +0,0 @@
|
|||||||
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,12 +1,9 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"fmt"
|
||||||
"encoding/json"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"path"
|
"path"
|
||||||
"slices"
|
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -15,29 +12,8 @@ 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 "GET/v1/state/" + uid:
|
case "PUT/v1/state/" + s.Session(r.Context()).ID + "/party":
|
||||||
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)
|
||||||
@@ -46,27 +22,5 @@ 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 {
|
||||||
party, err := io.ReadAll(r.Body)
|
return fmt.Errorf("not impl")
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|||||||
199
cmd/server/ws.go
199
cmd/server/ws.go
@@ -1,12 +1,8 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"slices"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/coder/websocket"
|
"github.com/coder/websocket"
|
||||||
@@ -16,203 +12,18 @@ 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 {
|
func (s *S) serveWS(httpw http.ResponseWriter, httpr *http.Request) error {
|
||||||
*S
|
ctx := httpr.Context()
|
||||||
c *websocket.Conn
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *S) serveWS(w http.ResponseWriter, r *http.Request) error {
|
c, err := websocket.Accept(httpw, httpr, 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ws WS) Serve(w http.ResponseWriter, r *http.Request) error {
|
if err := c.Write(ctx, 1, []byte("hello world")); err != nil {
|
||||||
ctx, can := context.WithCancel(r.Context())
|
|
||||||
defer can()
|
|
||||||
r = r.WithContext(ctx)
|
|
||||||
|
|
||||||
ugs, err := ws.newUserGameServer(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
go ugs.Listen(ctx, can, func(ctx context.Context) ([]byte, error) {
|
return fmt.Errorf("not impl")
|
||||||
_, b, err := ws.c.Read(ctx)
|
|
||||||
return b, err
|
|
||||||
})
|
|
||||||
|
|
||||||
for ugs.More(ctx) == nil {
|
|
||||||
if err := ws.Push(ctx, ugs); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ctx.Err()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ws WS) newUserGameServer(ctx context.Context) (*UserGameServer, error) {
|
|
||||||
return NewUserGameServer(ctx, ws.Session(ctx), ws.games)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ws WS) Push(ctx context.Context, ugs *UserGameServer) error {
|
|
||||||
gameState, err := ugs.State(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var msg map[string]any
|
|
||||||
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.)",
|
|
||||||
"",
|
|
||||||
"1. Get any target to say any of his or her kill words.",
|
|
||||||
"2. Click the word to collect points.",
|
|
||||||
"3. Review new kill words.",
|
|
||||||
"",
|
|
||||||
"The game ends when everyone has been assassinated.",
|
|
||||||
}, "<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"
|
|
||||||
|
|
||||||
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{"id": k, "name": name})
|
|
||||||
}
|
|
||||||
msg["items"] = items
|
|
||||||
|
|
||||||
return msg, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ws WS) completeMsg(ctx context.Context, gameState UserGameState) (msg map[string]any, _ error) {
|
|
||||||
msg["page"] = "B"
|
|
||||||
msg["event"] = "B"
|
|
||||||
|
|
||||||
items := []map[string]any{}
|
|
||||||
for k, v := range gameState.Players {
|
|
||||||
name, err := ws.games.UserName(ctx, k)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
tags := []map[string]any{}
|
|
||||||
for _, kill := range v.Kills {
|
|
||||||
tags = append(tags, map[string]any{
|
|
||||||
"k": kill.KillWord.Word,
|
|
||||||
"v": kill.Victim,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
items = append(items, map[string]any{
|
|
||||||
"name": name,
|
|
||||||
"title": fmt.Sprint(v.Points()),
|
|
||||||
"tags": tags,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
msg["items"] = items
|
|
||||||
return msg, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ws WS) inProgressMsg(ctx context.Context, ugs *UserGameServer, gameState UserGameState) (msg map[string]any, _ error) {
|
|
||||||
msg["page"] = "B"
|
|
||||||
msg["event"] = "A"
|
|
||||||
|
|
||||||
items, err := ws.inProgressMsgItems(ctx, ugs, gameState)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, killWord := range append(
|
|
||||||
v.KillWords.Publics(),
|
|
||||||
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,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
cmd/testapi/exec-testapi
Executable file
BIN
cmd/testapi/exec-testapi
Executable file
Binary file not shown.
@@ -6,7 +6,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"math/rand"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
@@ -72,15 +71,6 @@ 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)
|
||||||
}
|
}
|
||||||
@@ -122,17 +112,13 @@ 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 := r.URL.Query().Get("uuid")
|
id, err := r.Cookie("uuid")
|
||||||
if id == "" {
|
if err != nil || id.Value == "" {
|
||||||
c, err := r.Cookie("uuid")
|
return io.EOF
|
||||||
if err != nil || c.Value == "" {
|
|
||||||
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,
|
ID: id.Value,
|
||||||
})
|
})
|
||||||
*r = *r.WithContext(ctx)
|
*r = *r.WithContext(ctx)
|
||||||
return nil
|
return nil
|
||||||
@@ -146,9 +132,7 @@ 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, &websocket.AcceptOptions{
|
c, err := websocket.Accept(w, r, nil)
|
||||||
InsecureSkipVerify: true,
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -190,17 +174,9 @@ func (s *S) serveWS(w http.ResponseWriter, r *http.Request) error {
|
|||||||
func (s *S) serveV1(w http.ResponseWriter, r *http.Request) error {
|
func (s *S) serveV1(w http.ResponseWriter, r *http.Request) error {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
switch r.Method + r.URL.Path {
|
switch r.Method + r.URL.Path {
|
||||||
case "GET/v1/state/" + s.Session(ctx).ID:
|
|
||||||
if rand.Int()%2 == 0 {
|
|
||||||
w.Write([]byte(`{"name": "foo"}`))
|
|
||||||
} else {
|
|
||||||
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([]byte(`{}`))
|
|
||||||
default:
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
http.NotFound(w, r)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
<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>
|
|
||||||
2
go.mod
2
go.mod
@@ -9,7 +9,7 @@ require (
|
|||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/glebarez/go-sqlite v1.21.2 // indirect
|
github.com/glebarez/go-sqlite v1.21.2 // indirect
|
||||||
github.com/glebarez/sqlite v1.11.0 // indirect
|
github.com/glebarez/sqlite v1.11.0 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.3.0 // indirect
|
||||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
github.com/jinzhu/now v1.1.5 // indirect
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.17 // indirect
|
github.com/mattn/go-isatty v0.0.17 // indirect
|
||||||
|
|||||||
2
go.sum
2
go.sum
@@ -8,8 +8,6 @@ github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GM
|
|||||||
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
|
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
|
||||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
|
||||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||||
|
|||||||
28
todo.yaml
28
todo.yaml
@@ -1,28 +0,0 @@
|
|||||||
todo:
|
|
||||||
- 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
|
|
||||||
- leave game
|
|
||||||
scheduled: []
|
|
||||||
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