Compare commits
19 Commits
78511f25f3
...
808266d9c9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
808266d9c9 | ||
|
|
a99cb0e102 | ||
|
|
ba21848373 | ||
|
|
cb335639dd | ||
|
|
264b165f1c | ||
|
|
5587467a2d | ||
|
|
2a635bc8b5 | ||
|
|
62df483665 | ||
|
|
ceff02789a | ||
|
|
67ff6284bf | ||
|
|
8115bdf3ff | ||
|
|
b9392a38e2 | ||
|
|
0a944f3b0a | ||
|
|
f17328a9ee | ||
|
|
130f8122b6 | ||
|
|
9dd661ecd0 | ||
|
|
39b1a6a1e8 | ||
|
|
659bf0f559 | ||
|
|
58904c8619 |
@@ -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
95
cmd/server/db_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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
121
cmd/server/games_test.go
Normal 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
100
cmd/server/holiday.txt
Normal 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
|
||||
193
cmd/server/ws.go
193
cmd/server/ws.go
@@ -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
2
go.mod
@@ -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
2
go.sum
@@ -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
12
todo.yaml
Executable 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: []
|
||||
Reference in New Issue
Block a user