Compare commits

...

19 Commits

Author SHA1 Message Date
Bel LaPointe
617785ad51 f u wannabe cors 2024-12-18 19:45:45 -07:00
Bel LaPointe
d2fa707628 tests pass but LOTS of todos 2024-12-16 17:29:27 -07:00
bel
421331eb71 i have a plan but also realize event state computation CANNOT create events because itll evalulate many times 2024-12-15 22:28:26 -07:00
Bel LaPointe
c6ffa12354 grr how do trial 2024-12-15 19:03:35 -07:00
Bel LaPointe
58fae19522 stub codename trial, accuse events 2024-12-15 18:43:15 -07:00
Bel LaPointe
4abac89472 use codename.Consumed instead of kills to know whether codename is available for murdering 2024-12-15 18:36:19 -07:00
Bel LaPointe
15078a626d Global-1 to Codename+200 2024-12-15 18:25:47 -07:00
Bel LaPointe
02dc21c124 todo 2024-12-15 17:53:38 -07:00
Bel LaPointe
4d9abef04c todo 2024-12-15 16:40:59 -07:00
Bel LaPointe
557e1ec6d4 accept ?uuid=X instead of cookie 2024-12-15 14:39:14 -07:00
Bel LaPointe
3095d54f99 testapi accepts ?uuid instead of cookie.uuid 2024-12-15 14:34:59 -07:00
Bel LaPointe
d6db4a8a58 oops cross platform testing doesnt play nice with cookies 2024-12-15 14:33:18 -07:00
Bel LaPointe
d36ef7b629 todo.yaml 2024-12-15 14:29:41 -07:00
Bel LaPointe
ab3a549f78 cors pls 2024-12-15 14:24:20 -07:00
Bel LaPointe
bf3b341b69 GameReset event 2024-12-15 14:17:07 -07:00
Bel LaPointe
54116131b3 stub GameReset event 2024-12-15 14:09:54 -07:00
Bel LaPointe
57dd66e510 impl first game reset but realize it must be an event to broadcast to all players 2024-12-15 14:06:08 -07:00
Bel LaPointe
f1282f588d impl ws kill 2024-12-15 13:56:59 -07:00
Bel LaPointe
706d55631b games.UserByName 2024-12-15 13:50:27 -07:00
10 changed files with 386 additions and 96 deletions

View File

@@ -11,6 +11,7 @@ import (
"slices" "slices"
"strings" "strings"
"time" "time"
"unicode"
"github.com/google/uuid" "github.com/google/uuid"
) )
@@ -102,6 +103,19 @@ func (games Games) UserName(ctx context.Context, id string) (string, error) {
return result, err return result, err
} }
func (games Games) UserByName(ctx context.Context, gid, name string) (string, error) {
result := ""
err := games.db.Query(ctx, func(rows *sql.Rows) error {
return rows.Scan(&result)
}, `
SELECT users.uuid
FROM players
JOIN users ON players.user_uuid=users.uuid
WHERE players.game_uuid=? AND users.name=?
`, gid, name)
return result, err
}
func (a KillWord) Empty() bool { func (a KillWord) Empty() bool {
return a == (KillWord{}) return a == (KillWord{})
} }
@@ -156,9 +170,13 @@ func (games Games) GameByName(ctx context.Context, name string) (string, error)
type ( type (
GameState struct { GameState struct {
ID string
Started bool Started bool
Completed time.Time Completed time.Time
Players map[string]PlayerState Players map[string]PlayerState
Trial Trial
} }
PlayerState struct { PlayerState struct {
@@ -173,7 +191,7 @@ type (
} }
KillWords struct { KillWords struct {
Global KillWord Codename Codename
Assigned time.Time Assigned time.Time
Assignee string Assignee string
@@ -181,6 +199,11 @@ type (
Assignment Assignment Assignment Assignment
} }
Codename struct {
KillWord KillWord
Consumed bool
}
Assignment struct { Assignment struct {
Public []KillWord Public []KillWord
Private []KillWord Private []KillWord
@@ -191,6 +214,12 @@ type (
Points int Points int
} }
Trial struct {
Prosecutor string
Defendant string
Word string
}
EventType int EventType int
EventPlayerJoin struct { EventPlayerJoin struct {
@@ -215,6 +244,29 @@ type (
KillWord KillWord KillWord KillWord
AllKillWords AllKillWords AllKillWords AllKillWords
} }
EventGameReset struct {
Type EventType
Timestamp time.Time
ID string
}
EventCodenameAccusal struct {
Type EventType
Timestamp time.Time
Prosecutor string
Defendant string
Word string
}
EventCodenameTrial struct {
Type EventType
Timestamp time.Time
Guilty bool
}
EventNotification struct {
Type EventType
Timestamp time.Time
Recipient string
Message string
}
AllKillWords map[string]KillWords AllKillWords map[string]KillWords
) )
@@ -223,6 +275,10 @@ const (
PlayerLeave PlayerLeave
GameComplete GameComplete
AssignmentRotation AssignmentRotation
GameReset
CodenameAccusal
CodenameTrial
Notification
) )
type Event interface{ event() } type Event interface{ event() }
@@ -231,6 +287,10 @@ func (EventPlayerJoin) event() {}
func (EventPlayerLeave) event() {} func (EventPlayerLeave) event() {}
func (EventGameComplete) event() {} func (EventGameComplete) event() {}
func (EventAssignmentRotation) event() {} func (EventAssignmentRotation) event() {}
func (EventGameReset) event() {}
func (EventCodenameAccusal) event() {}
func (EventCodenameTrial) event() {}
func (EventNotification) event() {}
func EventWithTime(event Event, t time.Time) Event { func EventWithTime(event Event, t time.Time) Event {
switch e := event.(type) { switch e := event.(type) {
@@ -246,6 +306,18 @@ func EventWithTime(event Event, t time.Time) Event {
case EventAssignmentRotation: case EventAssignmentRotation:
e.Timestamp = t e.Timestamp = t
event = e event = e
case EventGameReset:
e.Timestamp = t
event = e
case EventCodenameAccusal:
e.Timestamp = t
event = e
case EventCodenameTrial:
e.Timestamp = t
event = e
case EventNotification:
e.Timestamp = t
event = e
} }
return event return event
} }
@@ -296,12 +368,28 @@ func parseEvent(b []byte, timestamp time.Time) (Event, error) {
var v EventAssignmentRotation var v EventAssignmentRotation
err := json.Unmarshal(b, &v) err := json.Unmarshal(b, &v)
return EventWithTime(v, timestamp), err return EventWithTime(v, timestamp), err
case GameReset:
var v EventGameReset
err := json.Unmarshal(b, &v)
return EventWithTime(v, timestamp), err
case CodenameAccusal:
var v EventCodenameAccusal
err := json.Unmarshal(b, &v)
return EventWithTime(v, timestamp), err
case CodenameTrial:
var v EventCodenameTrial
err := json.Unmarshal(b, &v)
return EventWithTime(v, timestamp), err
case Notification:
var v EventNotification
err := json.Unmarshal(b, &v)
return EventWithTime(v, timestamp), err
} }
return nil, fmt.Errorf("unknown event type %d: %s", peek.Type, b) return nil, fmt.Errorf("unknown event type %d: %s", peek.Type, b)
} }
func (games Games) GameState(ctx context.Context, id string) (GameState, error) { func (games Games) GameState(ctx context.Context, id string) (GameState, error) {
result := GameState{Players: map[string]PlayerState{}} result := GameState{ID: id, Players: map[string]PlayerState{}}
events, err := games.GameEvents(ctx, id, time.Time{}) events, err := games.GameEvents(ctx, id, time.Time{})
if err != nil { if err != nil {
@@ -309,19 +397,16 @@ func (games Games) GameState(ctx context.Context, id string) (GameState, error)
} }
for _, event := range events { for _, event := range events {
switch event.(type) { switch e := event.(type) {
case EventPlayerJoin: case EventPlayerJoin:
playerJoin := event.(EventPlayerJoin) result.Players[e.ID] = PlayerState{}
result.Players[playerJoin.ID] = PlayerState{}
case EventPlayerLeave: case EventPlayerLeave:
playerLeave := event.(EventPlayerLeave) delete(result.Players, e.ID)
delete(result.Players, playerLeave.ID)
case EventGameComplete: case EventGameComplete:
gameComplete := event.(EventGameComplete) result.Completed = e.Timestamp
result.Completed = gameComplete.Timestamp
case EventAssignmentRotation: case EventAssignmentRotation:
result.Started = true result.Started = true
assignmentRotation := event.(EventAssignmentRotation) assignmentRotation := e
if killer, ok := result.Players[assignmentRotation.Killer]; !ok { if killer, ok := result.Players[assignmentRotation.Killer]; !ok {
} else if _, ok := result.Players[assignmentRotation.Victim]; !ok { } else if _, ok := result.Players[assignmentRotation.Victim]; !ok {
@@ -344,6 +429,48 @@ func (games Games) GameState(ctx context.Context, id string) (GameState, error)
player.KillWords = v player.KillWords = v
result.Players[k] = player result.Players[k] = player
} }
case EventCodenameAccusal:
if actual := result.Players[e.Defendant].KillWords.Codename; !actual.Consumed {
result.Trial.Prosecutor = e.Prosecutor
result.Trial.Defendant = e.Defendant
result.Trial.Word = e.Word
if !basicallyTheSame(actual.KillWord.Word, e.Word) {
} else if err := games.CreateEventCodenameTrial(ctx, id, true); err != nil { // TODO cannot be in State loop
return GameState{}, err
}
}
case EventCodenameTrial:
if result.Trial == (Trial{}) {
} else if e.Guilty {
if err := games.CreateEventNotification(ctx, id, fmt.Sprintf(`%s revealed %s is %s and collected %s's bounty.`, result.Trial.Prosecutor, result.Trial.Defendant, result.Trial.Word, result.Trial.Defendant)); err != nil { // TODO not in this loop
return GameState{}, err
}
return GameState{}, fmt.Errorf("not impl: trial: guilty: %+v", e)
} else {
v := result.Players[result.Trial.Prosecutor]
v.KillWords.Codename.Consumed = true
v.Kills = append(v.Kills, Kill{
Timestamp: e.Timestamp,
Victim: result.Trial.Defendant,
KillWord: KillWord{
Word: result.Trial.Word,
Points: -200,
},
})
result.Players[result.Trial.Prosecutor] = v
v = result.Players[result.Trial.Defendant]
v.KillWords.Codename.KillWord.Word = "" // TODO
return GameState{}, fmt.Errorf("creating state CANNOT create events because it will eval every loop")
if err := games.CreateEventNotification(ctx, id, fmt.Sprintf(`%s accused the innocent %s of being %s. %s will get a new codename.`, result.Trial.Prosecutor, result.Trial.Defendant, result.Trial.Word, result.Trial.Defendant)); err != nil {
return GameState{}, err
}
}
result.Trial = Trial{}
case EventGameReset:
return games.GameState(ctx, e.ID)
default: default:
return GameState{}, fmt.Errorf("unknown event type %T", event) return GameState{}, fmt.Errorf("unknown event type %T", event)
} }
@@ -352,6 +479,20 @@ func (games Games) GameState(ctx context.Context, id string) (GameState, error)
return result, err return result, err
} }
func basicallyTheSame(a, b string) bool {
simplify := func(s string) string {
s = strings.TrimSpace(strings.ToLower(s))
s2 := ""
for _, c := range s {
if unicode.IsLetter(c) {
s2 = fmt.Sprintf("%s%c", s2, c)
}
}
return s2
}
return simplify(a) == simplify(b)
}
func (games Games) CreateGame(ctx context.Context, name string) (string, error) { func (games Games) CreateGame(ctx context.Context, name string) (string, error) {
var exists string var exists string
if err := games.db.Query(ctx, if err := games.db.Query(ctx,
@@ -378,14 +519,8 @@ func (games Games) CreateGame(ctx context.Context, name string) (string, error)
`, id, time.Now(), name) `, id, time.Now(), name)
} }
func (games Games) CreateEventPlayerJoin(ctx context.Context, id string, player, name string) error { func (games Games) CreateEventPlayerJoin(ctx context.Context, id string, player string) error {
if err := games.db.Exec(ctx, ` if _, err := games.UserName(ctx, player); err != nil {
INSERT INTO users (
uuid,
name
) VALUES (?, ?)
ON CONFLICT DO UPDATE SET name=? WHERE uuid=?;
`, player, name, name, player); err != nil {
return err return err
} }
if err := games.db.Exec(ctx, ` if err := games.db.Exec(ctx, `
@@ -419,10 +554,7 @@ func (games Games) CreateEventAssignmentRotation(ctx context.Context, id string,
}, },
} }
prevAllKillWords := make(AllKillWords) prevAllKillWords := state.AllKillWords()
for k, v := range state.Players {
prevAllKillWords[k] = v.KillWords
}
event.AllKillWords = prevAllKillWords.ShuffleAssignees(killer, victim, word) event.AllKillWords = prevAllKillWords.ShuffleAssignees(killer, victim, word)
event.AllKillWords = event.AllKillWords.FillKillWords() event.AllKillWords = event.AllKillWords.FillKillWords()
@@ -430,8 +562,42 @@ func (games Games) CreateEventAssignmentRotation(ctx context.Context, id string,
return games.createEvent(ctx, id, event) return games.createEvent(ctx, id, event)
} }
func (state GameState) AllKillWords() AllKillWords {
m := make(AllKillWords)
for k, v := range state.Players {
m[k] = v.KillWords
}
return m
}
func (games Games) CreateEventGameReset(ctx context.Context, gid string) error {
state, err := games.GameState(ctx, gid)
if err != nil {
return err
}
name, err := games.GameName(ctx, gid)
if err != nil {
return err
}
gid2, err := games.CreateGame(ctx, name)
if err != nil {
return err
}
for p := range state.Players {
if err := games.CreateEventPlayerJoin(ctx, gid2, p); err != nil {
return err
}
}
event := EventGameReset{
Type: GameReset,
Timestamp: time.Now(),
ID: gid2,
}
return games.createEvent(ctx, gid, event)
}
func (words KillWords) Empty() bool { func (words KillWords) Empty() bool {
return words.Global == (KillWord{}) && words.Assigned.IsZero() && words.Assignee == "" && words.Assignment.Empty() return words.Codename == (Codename{}) && words.Assigned.IsZero() && words.Assignee == "" && words.Assignment.Empty()
} }
func (words KillWords) Privates() []KillWord { func (words KillWords) Privates() []KillWord {
@@ -502,7 +668,7 @@ func (m AllKillWords) withoutAssignees() AllKillWords {
result := make(AllKillWords) result := make(AllKillWords)
for k := range m { for k := range m {
result[k] = KillWords{ result[k] = KillWords{
Global: m[k].Global, Codename: m[k].Codename,
Assigned: now, Assigned: now,
Assignee: "", Assignee: "",
Assignment: m[k].Assignment, Assignment: m[k].Assignment,
@@ -551,7 +717,7 @@ func (m AllKillWords) FillKillWords() AllKillWords {
} }
func (m AllKillWords) fillKillWords( func (m AllKillWords) fillKillWords(
poolGlobal []string, poolCodename []string,
nPublic int, nPublic int,
poolPublic []string, poolPublic []string,
nPrivate int, nPrivate int,
@@ -560,8 +726,8 @@ func (m AllKillWords) fillKillWords(
result := maps.Clone(m) result := maps.Clone(m)
m = result m = result
for k, v := range m { for k, v := range m {
if v.Global.Word == "" { if v.Codename.KillWord.Word == "" {
v.Global = KillWord{Word: m.unusedGlobal(poolGlobal), Points: -1} v.Codename = Codename{KillWord: KillWord{Word: m.unusedCodename(poolCodename), Points: 200}}
m[k] = v m[k] = v
} }
if len(v.Assignment.Public) == 0 { if len(v.Assignment.Public) == 0 {
@@ -582,11 +748,11 @@ func (m AllKillWords) fillKillWords(
return m return m
} }
func (m AllKillWords) unusedGlobal(pool []string) string { func (m AllKillWords) unusedCodename(pool []string) string {
inUse := func() []string { inUse := func() []string {
result := []string{} result := []string{}
for _, killWords := range m { for _, killWords := range m {
result = append(result, killWords.Global.Word) result = append(result, killWords.Codename.KillWord.Word)
} }
return result return result
} }
@@ -645,6 +811,23 @@ func (games Games) CreateEventGameComplete(ctx context.Context, id string) error
return games.createEvent(ctx, id, EventGameComplete{Type: GameComplete}) return games.createEvent(ctx, id, EventGameComplete{Type: GameComplete})
} }
func (games Games) CreateEventCodenameAccusal(ctx context.Context, gid, prosecutor, defendant, codename string) error {
return fmt.Errorf("not impl: x accused y")
return fmt.Errorf("not impl: x caught by y")
}
func (games Games) CreateEventCodenameTrial(ctx context.Context, gid string, guilty bool) error {
return fmt.Errorf("not impl: x found guilty/notguilty")
}
func (games Games) CreateEventNotification(ctx context.Context, gid, msg string) error {
return games.CreateEventNotificationTo(ctx, gid, "", msg)
}
func (games Games) CreateEventNotificationTo(ctx context.Context, gid, uid, msg string) error {
return fmt.Errorf("not impl: simple")
}
func (games Games) createEvent(ctx context.Context, id string, v any) error { func (games Games) createEvent(ctx context.Context, id string, v any) error {
payload, err := json.Marshal(v) payload, err := json.Marshal(v)
if err != nil { if err != nil {

View File

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

View File

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

View File

@@ -5,6 +5,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"log" "log"
"slices"
"time" "time"
) )
@@ -79,7 +80,34 @@ func (ugs *UserGameServer) listen(ctx context.Context, reader func(context.Conte
return err return err
} }
} else if killOccurred := m["k"] != ""; killOccurred { } else if killOccurred := m["k"] != ""; killOccurred {
return fmt.Errorf("not impl: a kill occurred: %+v", m) 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 { } else if isRename := m["name"] != ""; isRename {
if err := ugs.games.UpdateUserName(ctx, ugs.Session.ID, m["name"]); err != nil { if err := ugs.games.UpdateUserName(ctx, ugs.Session.ID, m["name"]); err != nil {
return err return err
@@ -88,8 +116,8 @@ func (ugs *UserGameServer) listen(ctx context.Context, reader func(context.Conte
if gameState, err := ugs.games.GameState(ctx, ugs.ID); err != nil { if gameState, err := ugs.games.GameState(ctx, ugs.ID); err != nil {
return err return err
} else if gameState.Completed.IsZero() { } else if gameState.Completed.IsZero() {
} else { } else if err := ugs.games.CreateEventGameReset(ctx, ugs.ID); err != nil {
return fmt.Errorf("not impl: new game: %+v", m) return err
} }
} else { } else {
return fmt.Errorf("UNKNOWN: %+v", m) return fmt.Errorf("UNKNOWN: %+v", m)
@@ -105,6 +133,7 @@ func (ugs *UserGameServer) State(ctx context.Context) (UserGameState, error) {
if err != nil { if err != nil {
return UserGameState{}, err return UserGameState{}, err
} }
ugs.ID = gameState.ID
if complete := !gameState.Completed.IsZero(); complete { if complete := !gameState.Completed.IsZero(); complete {
return UserGameState(gameState), nil return UserGameState(gameState), nil
@@ -122,7 +151,7 @@ func (ugs *UserGameServer) State(ctx context.Context) (UserGameState, error) {
if isSelf := k == ugs.Session.ID; isSelf { if isSelf := k == ugs.Session.ID; isSelf {
v.KillWords.Assignment = Assignment{} v.KillWords.Assignment = Assignment{}
} else { } else {
v.KillWords.Global = KillWord{} v.KillWords.Codename = Codename{}
v.KillWords.Assignee = "" v.KillWords.Assignee = ""
for i := range v.Kills { for i := range v.Kills {
v.Kills[i].Victim = "" v.Kills[i].Victim = ""

View File

@@ -18,7 +18,7 @@ func TestUserGameServer(t *testing.T) {
pids := []string{} pids := []string{}
for i := 0; i < 4; i++ { for i := 0; i < 4; i++ {
pid := fmt.Sprintf("p%d", i+1) pid := fmt.Sprintf("p%d", i+1)
if err := games.CreateEventPlayerJoin(ctx, gid, pid, "player "+pid); err != nil { if err := games.CreateEventPlayerJoin(ctx, gid, pid); err != nil {
t.Fatal(err) t.Fatal(err)
} }
pids = append(pids, pid) pids = append(pids, pid)
@@ -83,8 +83,8 @@ func TestUserGameServer(t *testing.T) {
} }
if isSelf := pid == ugs.Session.ID; isSelf { if isSelf := pid == ugs.Session.ID; isSelf {
if p.KillWords.Global.Word == "" || p.KillWords.Global.Points == 0 { if p.KillWords.Codename.KillWord.Word == "" || p.KillWords.Codename.KillWord.Points == 0 {
t.Error("self global missing field") t.Error("self codename missing field")
} }
if p.KillWords.Assignee == "" { if p.KillWords.Assignee == "" {
t.Error("assignee is empty") t.Error("assignee is empty")
@@ -96,8 +96,8 @@ func TestUserGameServer(t *testing.T) {
t.Error("self knows its own private") t.Error("self knows its own private")
} }
} else { } else {
if !p.KillWords.Global.Empty() { if !p.KillWords.Codename.KillWord.Empty() {
t.Error("can see not self global") t.Error("can see not self Codename")
} }
if p.KillWords.Assignee != "" { if p.KillWords.Assignee != "" {
t.Error("can see other player's assignee") t.Error("can see other player's assignee")
@@ -167,8 +167,8 @@ func TestUserGameServer(t *testing.T) {
} else if kill.KillWord.Word == "" { } else if kill.KillWord.Word == "" {
t.Errorf("dont know own kill word") t.Errorf("dont know own kill word")
} }
if p.KillWords.Global.Word == "" || p.KillWords.Global.Points == 0 { if p.KillWords.Codename.KillWord.Word == "" || p.KillWords.Codename.KillWord.Points == 0 {
t.Error("self global missing field") t.Error("self Codename missing field")
} }
if p.KillWords.Assignee == "" { if p.KillWords.Assignee == "" {
t.Error("assignee is empty") t.Error("assignee is empty")
@@ -190,8 +190,8 @@ func TestUserGameServer(t *testing.T) {
} else if kill.KillWord.Word != "" { } else if kill.KillWord.Word != "" {
t.Errorf("know other's kill word") t.Errorf("know other's kill word")
} }
if !p.KillWords.Global.Empty() { if !p.KillWords.Codename.KillWord.Empty() {
t.Error("can see not self global") t.Error("can see not self Codename")
} }
if p.KillWords.Assignee != "" { if p.KillWords.Assignee != "" {
t.Error("can see other player's assignee") t.Error("can see other player's assignee")
@@ -248,8 +248,8 @@ func TestUserGameServer(t *testing.T) {
} else if kill.KillWord.Word == "" { } else if kill.KillWord.Word == "" {
t.Errorf("dont know own kill word") t.Errorf("dont know own kill word")
} }
if p.KillWords.Global.Word == "" || p.KillWords.Global.Points == 0 { if p.KillWords.Codename.KillWord.Word == "" || p.KillWords.Codename.KillWord.Points == 0 {
t.Error("self global missing field") t.Error("self Codename missing field")
} }
if p.KillWords.Assignee == "" { if p.KillWords.Assignee == "" {
t.Error("assignee is empty") t.Error("assignee is empty")
@@ -271,8 +271,8 @@ func TestUserGameServer(t *testing.T) {
} else if kill.KillWord.Word == "" { } else if kill.KillWord.Word == "" {
t.Errorf("dont know other's kill word") t.Errorf("dont know other's kill word")
} }
if p.KillWords.Global.Empty() { if p.KillWords.Codename.KillWord.Empty() {
t.Error("cannot see not self global") t.Error("cannot see not self Codename")
} }
if p.KillWords.Assignee == "" { if p.KillWords.Assignee == "" {
t.Error("cannot see other player's assignee") t.Error("cannot see other player's assignee")

View File

@@ -46,11 +46,6 @@ 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 {
userName, err := s.games.UserName(r.Context(), s.Session(r.Context()).ID)
if err != nil {
return err
}
party, err := io.ReadAll(r.Body) party, err := io.ReadAll(r.Body)
if err != nil { if err != nil {
return err return err
@@ -73,5 +68,5 @@ func (s *S) serveV1PutParty(w http.ResponseWriter, r *http.Request) error {
if slices.Contains(games, gid) { if slices.Contains(games, gid) {
return nil return nil
} }
return s.games.CreateEventPlayerJoin(r.Context(), gid, s.Session(r.Context()).ID, userName) return s.games.CreateEventPlayerJoin(r.Context(), gid, s.Session(r.Context()).ID)
} }

View File

@@ -191,10 +191,8 @@ func (ws WS) inProgressMsgItem(ctx context.Context, ugs *UserGameServer, gameSta
tags := []inProgressMsgItemTag{} tags := []inProgressMsgItemTag{}
if hasBeenKilledWithGlobal := slices.ContainsFunc(self.Kills, func(a Kill) bool { if canKillWithCodename := !self.KillWords.Codename.Consumed; canKillWithCodename {
return a.Victim == uid && a.KillWord.Word == self.KillWords.Global.Word tags = append(tags, newInProgressMsgItemTag(self.KillWords.Codename.KillWord))
}); !hasBeenKilledWithGlobal {
tags = append(tags, newInProgressMsgItemTag(self.KillWords.Global))
} }
for _, killWord := range append( for _, killWord := range append(

View File

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

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

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

View File

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