Compare commits

...

19 Commits

Author SHA1 Message Date
Bel LaPointe
808266d9c9 todo 2024-12-15 02:47:51 -07:00
Bel LaPointe
a99cb0e102 todo numbah one is TEST 2024-12-15 02:47:01 -07:00
Bel LaPointe
ba21848373 todo 2024-12-15 02:46:46 -07:00
Bel LaPointe
cb335639dd i think it is all todoed out 2024-12-15 02:43:59 -07:00
Bel LaPointe
264b165f1c o i think that looks like everything but is hard to tell 2024-12-15 02:36:18 -07:00
Bel LaPointe
5587467a2d PlayerState.Points() 2024-12-15 02:30:44 -07:00
Bel LaPointe
2a635bc8b5 compiles 2024-12-15 02:26:52 -07:00
Bel LaPointe
62df483665 err on err 2024-12-15 02:22:44 -07:00
Bel LaPointe
ceff02789a HELP msg always 2024-12-15 02:22:18 -07:00
Bel LaPointe
67ff6284bf W TAGS 2024-12-15 02:17:55 -07:00
Bel LaPointe
8115bdf3ff page=B events=B ezpz w tags 2024-12-15 02:17:43 -07:00
Bel LaPointe
b9392a38e2 page=B events=B ezpz 2024-12-15 02:16:44 -07:00
Bel LaPointe
0a944f3b0a tests pass again 2024-12-15 02:12:50 -07:00
Bel LaPointe
f17328a9ee games passes test 2024-12-15 01:58:40 -07:00
Bel LaPointe
130f8122b6 complete game pls 2024-12-15 01:45:25 -07:00
Bel LaPointe
9dd661ecd0 naisu test 2024-12-15 01:41:22 -07:00
Bel LaPointe
39b1a6a1e8 a test! 2024-12-15 01:15:36 -07:00
Bel LaPointe
659bf0f559 impl more 2024-12-15 00:48:47 -07:00
Bel LaPointe
58904c8619 if rw.Lock then no need for nested rw.RLock 2024-12-15 00:45:27 -07:00
9 changed files with 794 additions and 48 deletions

View File

@@ -3,7 +3,6 @@ package main
import (
"context"
"database/sql"
"io"
"sync"
"time"
@@ -14,16 +13,19 @@ type DB struct {
scheme string
conn string
rw *sync.RWMutex
locked *bool
}
func NewDB(ctx context.Context, scheme, conn string) (DB, error) {
ctx, can := context.WithTimeout(ctx, time.Second*10)
defer can()
locked := false
db := DB{
scheme: scheme,
conn: conn,
rw: &sync.RWMutex{},
locked: &locked,
}
sql, err := db.dial(ctx)
@@ -38,12 +40,18 @@ func NewDB(ctx context.Context, scheme, conn string) (DB, error) {
func (db DB) WithLock(cb func() error) error {
db.rw.Lock()
defer db.rw.Unlock()
*db.locked = true
defer func() {
*db.locked = false
}()
return cb()
}
func (db DB) Exec(ctx context.Context, q string, args ...any) error {
db.rw.RLock()
defer db.rw.RUnlock()
if !*db.locked {
db.rw.RLock()
defer db.rw.RUnlock()
}
return db.exec(ctx, q, args...)
}
@@ -59,8 +67,10 @@ 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 {
db.rw.RLock()
defer db.rw.RUnlock()
if !*db.locked {
db.rw.RLock()
defer db.rw.RUnlock()
}
return db.query(ctx, cb, q, args...)
}
@@ -96,7 +106,3 @@ func (db DB) dial(ctx context.Context) (*sql.DB, error) {
}
return c, nil
}
func (db DB) GetParty(id string) (string, error) {
return "", io.EOF
}

95
cmd/server/db_test.go Normal file
View File

@@ -0,0 +1,95 @@
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)
}
})
}

View File

@@ -3,11 +3,16 @@ package main
import (
"context"
"database/sql"
_ "embed"
"encoding/json"
"fmt"
"io"
"maps"
"math/rand"
"slices"
"strings"
"time"
"github.com/google/uuid"
)
type Games struct {
@@ -56,12 +61,50 @@ func (games Games) GamesForUser(ctx context.Context, id string) ([]string, error
return result, err
}
func (games Games) GameByName(ctx context.Context, name string) (string, error) {
return "", io.EOF
func (games Games) UpdateUserName(ctx context.Context, id, name string) error {
return games.db.Exec(ctx, `UPDATE users SET name=? WHERE uuid=?`, name, id)
}
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)
return result, err
}
func (s PlayerState) Points() int {
points := 0
for _, kill := range s.Kills {
points += kill.KillWord.Points
}
return points
}
func (games Games) GameByName(ctx context.Context, uid, name string) (string, error) {
var result string
err := games.db.Query(ctx, func(rows *sql.Rows) error {
return rows.Scan(&result)
}, `
SELECT
players.game_uuid
FROM
players
JOIN games ON players.game_uuid=games.uuid
WHERE players.user_uuid=? AND games.name=?
ORDER BY games.updated DESC
LIMIT 1
`, uid, name)
return result, err
}
type (
GameState struct {
Started bool
Completed time.Time
Players map[string]PlayerState
}
@@ -74,11 +117,11 @@ type (
Kill struct {
Timestamp time.Time
Victim string
Public bool
KillWord KillWord
}
KillWords struct {
Global string
Global KillWord
Assigned time.Time
Assignee string
@@ -87,23 +130,33 @@ type (
}
Assignment struct {
Public []string
Private []string
Public []KillWord
Private []KillWord
}
KillWord struct {
Word string
Points int
}
EventType int
EventPlayerJoin struct {
ID string
Type EventType
ID string
}
EventPlayerLeave struct {
ID string
Type EventType
ID string
}
EventGameComplete struct {
Type EventType
}
EventGameComplete struct{}
EventAssignmentRotation struct {
Type EventType
Killer string
Killed string
KillWord string
Victim string
KillWord KillWord
KillWords map[string]KillWords
}
)
@@ -155,18 +208,19 @@ func (games Games) GameState(ctx context.Context, id string) (GameState, error)
result.Completed = timestamp
return nil
case AssignmentRotation:
result.Started = true
var assignmentRotation EventAssignmentRotation
if err := json.Unmarshal(payload, &assignmentRotation); err != nil {
return err
}
if killer, ok := result.Players[assignmentRotation.Killer]; !ok {
} else if victim, ok := result.Players[assignmentRotation.Killed]; !ok {
} else if _, ok := result.Players[assignmentRotation.Victim]; !ok {
} else {
killer.Kills = append(killer.Kills, Kill{
Timestamp: timestamp,
Victim: assignmentRotation.Killed,
Public: slices.Contains(victim.KillWords.Assignment.Public, assignmentRotation.KillWord),
Victim: assignmentRotation.Victim,
KillWord: assignmentRotation.KillWord,
})
result.Players[assignmentRotation.Killer] = killer
}
@@ -196,29 +250,206 @@ func (games Games) GameState(ctx context.Context, id string) (GameState, error)
return result, err
}
func (games Games) CreateEventPlayerJoin(ctx context.Context, id string, player string) error {
return games.createEvent(ctx, id, EventPlayerJoin{ID: player})
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, name string) error {
if err := games.db.Exec(ctx, `
INSERT INTO users (
uuid,
name
) VALUES (?, ?)
ON CONFLICT DO UPDATE SET name=? WHERE uuid=?;
`, player, name, name, player); err != nil {
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 {
return games.createEvent(ctx, id, EventPlayerLeave{ID: player})
return games.createEvent(ctx, id, EventPlayerLeave{Type: PlayerLeave, ID: player})
}
//go:embed holiday.txt
var wordsHoliday string
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
}
now := time.Now()
event := EventAssignmentRotation{
Type: AssignmentRotation,
Killer: killer,
Victim: victim,
KillWord: KillWord{
Word: word,
Points: points,
},
KillWords: map[string]KillWords{},
}
toAssign := []string{}
doNotAssign := map[string]string{}
for k, v := range state.Players {
v := v.KillWords
toAssign = append(toAssign, k)
doNotAssign[k] = v.Assignee
event.KillWords[k] = KillWords{
Global: v.Global,
Assigned: now,
Assignee: "",
Assignment: v.Assignment,
}
}
if killerState, ok := state.Players[killer]; !ok {
} else if victimState, ok := state.Players[victim]; !ok {
} else {
event.KillWords[killer] = KillWords{
Global: killerState.KillWords.Global,
Assigned: now,
Assignee: victimState.KillWords.Assignee,
Assignment: killerState.KillWords.Assignment,
}
toAssign = slices.DeleteFunc(toAssign, func(s string) bool { return s == event.KillWords[killer].Assignee })
if killerState.KillWords.Global.Word != word {
victimState.KillWords.Assignment = Assignment{}
state.Players[victim] = victimState
}
}
for !func() bool {
toAssign := slices.Clone(toAssign)
doNotAssign := maps.Clone(doNotAssign)
eventKillWords := maps.Clone(event.KillWords)
for i := range toAssign {
j := rand.Intn(i + 1)
toAssign[i], toAssign[j] = toAssign[j], toAssign[i]
}
for k, v := range eventKillWords {
if k == toAssign[0] || doNotAssign[k] == toAssign[0] {
return false
}
eventKillWords[k] = KillWords{
Global: v.Global,
Assigned: now,
Assignee: toAssign[0],
Assignment: v.Assignment,
}
toAssign = toAssign[1:]
}
event.KillWords = eventKillWords
return true
}() {
}
globalsInUse := map[string]any{}
publicsInUse := map[string]any{}
privatesInUse := map[string]any{}
for _, v := range event.KillWords {
globalsInUse[v.Global.Word] = nil
for _, public := range v.Assignment.Public {
publicsInUse[public.Word] = nil
}
for _, private := range v.Assignment.Private {
privatesInUse[private.Word] = nil
}
}
randWord := func(words string, taken map[string]any) string {
wordsList := strings.Fields(words)
for {
got := wordsList[rand.Intn(len(wordsList))]
if _, ok := taken[got]; !ok {
taken[got] = nil
return got
}
}
}
randGlobal := func() string {
return randWord(wordsHoliday, globalsInUse)
}
randPublic := func() string {
return randWord(wordsHoliday, publicsInUse)
}
randPrivate := func() string {
return randWord(wordsHoliday, privatesInUse)
}
// TODO generate .Global=...us major cities?, .Assignments.Public=...?, .Assignments.Private=holiday
for k, v := range event.KillWords {
if v.Global.Word == "" {
v.Global = KillWord{Word: randGlobal(), Points: 1}
}
if len(v.Assignment.Public) == 0 {
v.Assignment.Public = []KillWord{
KillWord{Word: randPublic(), Points: 50},
KillWord{Word: randPublic(), Points: 50},
}
}
if len(v.Assignment.Private) == 0 {
v.Assignment.Private = []KillWord{
KillWord{Word: randPrivate(), Points: 100},
}
}
event.KillWords[k] = v
}
return games.createEvent(ctx, id, event)
}
func (games Games) CreateEventGameComplete(ctx context.Context, id string) error {
return games.createEvent(ctx, id, EventGameComplete{})
}
func (games Games) CreateEventAssignmentRotation(ctx context.Context, id string, killer, killed, killWord string) error {
// TODO gather current assignees
// 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
return io.EOF
return games.createEvent(ctx, id, v)
if err := games.db.Exec(ctx, `
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) createEvent(ctx context.Context, id string, v any) error {
payload, err := json.Marshal(any)
payload, err := json.Marshal(v)
if err != nil {
panic(err)
}

121
cmd/server/games_test.go Normal file
View File

@@ -0,0 +1,121 @@
package main
import (
"context"
"fmt"
"testing"
)
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", "player zero"); 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, "player "+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 empty")
}
}
if err := games.CreateEventAssignmentRotation(ctx, id, "", "", "", 1); err != nil {
t.Fatal("err creating rotation:", err)
}
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, "p1", "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].KillWords.Global.Word == "" {
t.Error(p, "no killwords.global")
} 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")
}
})
}

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

@@ -0,0 +1,100 @@
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

View File

@@ -1,9 +1,16 @@
package main
import (
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"slices"
"strconv"
"strings"
"time"
"github.com/coder/websocket"
)
@@ -12,18 +19,190 @@ func isWS(r *http.Request) bool {
return r.URL.Path == "/ws" || strings.HasPrefix(r.URL.Path, "/ws/")
}
func (s *S) serveWS(httpw http.ResponseWriter, httpr *http.Request) error {
ctx := httpr.Context()
func (s *S) serveWS(w http.ResponseWriter, r *http.Request) error {
ctx, can := context.WithCancel(r.Context())
defer can()
r = r.WithContext(ctx)
c, err := websocket.Accept(httpw, httpr, nil)
session := s.Session(ctx)
games, err := s.games.GamesForUser(ctx, session.ID)
if err != nil {
return err
}
if len(games) == 0 {
return fmt.Errorf("user %s is in zero games", session.ID)
}
game := games[0]
c, err := websocket.Accept(w, r, nil)
if err != nil {
return err
}
defer c.CloseNow()
if err := c.Write(ctx, 1, []byte("hello world")); err != nil {
return err
}
go func() {
defer can()
for {
_, b, err := c.Read(ctx)
if err != nil {
log.Println(err)
return
}
log.Printf("READ %s", b)
return fmt.Errorf("not impl")
var m map[string]string
if err := json.Unmarshal(b, &m); err != nil {
log.Println(err)
return
}
log.Printf("UNMARSHAL %+v", m)
if m["party"] == "start" {
if gameState, err := s.games.GameState(ctx, game); err != nil {
log.Println(err)
return
} else if gameState.Started {
} else if err := s.games.CreateEventAssignmentRotation(ctx, game, "", "", "", 0); err != nil {
log.Println(err)
return
}
} else if m["k"] != "" {
log.Println("TODO a kill occurred")
return
} else if name := m["name"]; name != "" {
if err := s.games.UpdateUserName(ctx, s.Session(ctx).ID, name); err != nil {
log.Println(err)
return
}
} else if m["again"] == "true" {
if gameState, err := s.games.GameState(ctx, game); err != nil {
log.Println(err)
return
} else if gameState.Completed.IsZero() {
} else {
log.Println("TODO new game")
return
}
} else {
log.Printf("UNKNOWN: %+v", m)
return
}
}
}()
for {
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(time.Second * 1):
}
gameState, err := s.games.GameState(ctx, game)
if err != nil {
return err
}
msg := map[string]any{
"help": strings.Join([]string{
"CARD ASSASSINS (Mobile Ed.)",
"",
"1. Get any target to say any of his or her kill words.",
"2. Click on the kill word a target said.",
"",
"The game ends when everyone has been assassinated.",
}, "<br>"),
}
if gameState.Started {
msg["page"] = "B"
if gameState.Completed.IsZero() {
msg["event"] = "A"
items := []map[string]any{}
for k, v := range gameState.Players {
if k == s.Session(ctx).ID {
continue
}
name, err := s.games.UserName(ctx, k)
if err != nil {
return err
}
tags := []map[string]any{}
if self := gameState.Players[s.Session(ctx).ID]; self.KillWords.Assignee == k {
for _, private := range v.KillWords.Assignment.Private {
tags = append(tags, map[string]any{
"k": private.Word,
"v": private.Points,
})
}
}
for _, public := range v.KillWords.Assignment.Public {
tags = append(tags, map[string]any{
"k": public.Word,
"v": public.Points,
})
}
if self := gameState.Players[s.Session(ctx).ID]; !slices.ContainsFunc(self.Kills, func(a Kill) bool {
return a.Victim == k
}) {
tags = append(tags, map[string]any{
"k": self.KillWords.Global.Word,
"v": self.KillWords.Global.Points,
})
}
items = append(items, map[string]any{
"name": name,
"title": strconv.Itoa(v.Points()),
"tags": tags,
})
}
slices.SortFunc(items, func(a, b map[string]any) int {
an, _ := strconv.Atoi(fmt.Sprint(a["title"]))
bn, _ := strconv.Atoi(fmt.Sprint(b["title"]))
return an - bn
})
return io.EOF
} else {
msg["event"] = "B"
items := []map[string]any{}
for k, v := range gameState.Players {
name, err := s.games.UserName(ctx, k)
if err != nil {
return 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
}
} else {
msg["page"] = "A"
items := []map[string]any{}
for k := range gameState.Players {
name, err := s.games.UserName(ctx, k)
if err != nil {
return err
}
items = append(items, map[string]any{"name": name})
}
msg["items"] = items
}
msgB, _ := json.Marshal(msg)
if err := c.Write(ctx, 1, msgB); err != nil {
return err
}
}
}

2
go.mod
View File

@@ -9,7 +9,7 @@ require (
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/glebarez/go-sqlite v1.21.2 // indirect
github.com/glebarez/sqlite v1.11.0 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect

2
go.sum
View File

@@ -8,6 +8,8 @@ github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GM
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/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/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=

12
todo.yaml Executable file
View File

@@ -0,0 +1,12 @@
todo:
- refactor ws.go into like WebSocketSession struct and stuff
- test ws flow
- notifications system with dismissal server-side so users see X got a kill
- play mp3 on kill + shuffle
- end condition; everyone has died
- word lists; already got holidays
- event driven woulda been nice
- remake
- quit
scheduled: []
done: []