Compare commits

..

14 Commits

Author SHA1 Message Date
Bel LaPointe
c1933dc180 sort ws tags 2024-12-15 12:03:21 -07:00
Bel LaPointe
9a74575e6c refactor out ws.inProgressMsgItem 2024-12-15 11:45:42 -07:00
Bel LaPointe
8d0ded9ee9 refactors 2024-12-15 11:35:00 -07:00
Bel LaPointe
e8817f9e74 refactor out incomplete msg 2024-12-15 11:32:23 -07:00
Bel LaPointe
3a83fe7c17 refactor out ws.unstartedMsg 2024-12-15 11:28:42 -07:00
Bel LaPointe
c6da3d17a1 refactor pushing state over websocket into a func 2024-12-15 11:24:51 -07:00
Bel LaPointe
64165c5745 refactor pushing state over websocket into a func 2024-12-15 11:24:32 -07:00
Bel LaPointe
74a403fa6d drop logs 2024-12-15 11:10:37 -07:00
Bel LaPointe
e95b63d9ce renmae 2024-12-15 11:09:44 -07:00
Bel LaPointe
2c3e870750 ugs aliases game state, shorter help sentences 2024-12-15 11:09:25 -07:00
Bel LaPointe
c3e9c18e95 refactor 2024-12-15 11:06:13 -07:00
Bel LaPointe
39c9eae7ad ugs.more blocks until event arrives 2024-12-15 11:05:39 -07:00
Bel LaPointe
0d44fd56ed extract reading from websocket into UserGameServer 2024-12-15 11:01:48 -07:00
Bel LaPointe
51006c7946 total coverage of db and games naisu 2024-12-15 10:49:33 -07:00
3 changed files with 309 additions and 183 deletions

View File

@@ -378,6 +378,22 @@ func (games Games) CreateEventAssignmentRotation(ctx context.Context, id string,
return games.createEvent(ctx, id, event) return games.createEvent(ctx, id, event)
} }
func (words KillWords) Privates() []KillWord {
a := slices.Clone(words.Assignment.Private)
slices.SortFunc(a, func(a, b KillWord) int {
return strings.Compare(a.Word, b.Word)
})
return a
}
func (words KillWords) Publics() []KillWord {
a := slices.Clone(words.Assignment.Public)
slices.SortFunc(a, func(a, b KillWord) int {
return strings.Compare(a.Word, b.Word)
})
return a
}
func (prev AllKillWords) ShuffleAssignees(killer, victim, word string) AllKillWords { func (prev AllKillWords) ShuffleAssignees(killer, victim, word string) AllKillWords {
m := prev.withoutAssignees() m := prev.withoutAssignees()

View File

@@ -0,0 +1,103 @@
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"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 m["party"] == "start" {
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 m["k"] != "" {
return fmt.Errorf("not impl: a kill occurred: %+v", m)
} else if name := m["name"]; name != "" {
if err := ugs.games.UpdateUserName(ctx, ugs.Session.ID, name); err != nil {
return err
}
} else if m["again"] == "true" {
if gameState, err := ugs.games.GameState(ctx, ugs.ID); err != nil {
return err
} else if gameState.Completed.IsZero() {
} else {
return fmt.Errorf("not impl: new game: %+v", m)
}
} else {
return fmt.Errorf("UNKNOWN: %+v", m)
}
}
return ctx.Err()
}
func (ugs *UserGameServer) State(ctx context.Context) (GameState, error) {
return ugs.games.GameState(ctx, ugs.ID)
}

View File

@@ -5,12 +5,10 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"log"
"net/http" "net/http"
"slices" "slices"
"strconv" "strconv"
"strings" "strings"
"time"
"github.com/coder/websocket" "github.com/coder/websocket"
) )
@@ -19,198 +17,207 @@ func isWS(r *http.Request) bool {
return r.URL.Path == "/ws" || strings.HasPrefix(r.URL.Path, "/ws/") return r.URL.Path == "/ws" || strings.HasPrefix(r.URL.Path, "/ws/")
} }
type WS struct {
*S
c *websocket.Conn
}
func (s *S) serveWS(w http.ResponseWriter, r *http.Request) error { func (s *S) serveWS(w http.ResponseWriter, r *http.Request) error {
ctx, can := context.WithCancel(r.Context())
defer can()
r = r.WithContext(ctx)
session := s.Session(ctx)
games, err := s.games.GamesForUser(ctx, session.ID)
if err != nil {
return err
}
if len(games) == 0 {
return fmt.Errorf("user %s is in zero games", session.ID)
}
game := games[0]
c, err := websocket.Accept(w, r, nil) c, err := websocket.Accept(w, r, nil)
if err != nil { if err != nil {
return err return err
} }
defer c.CloseNow() defer c.CloseNow()
ws := WS{S: s, c: c}
return ws.Serve(w, r)
}
go func() { func (ws WS) Serve(w http.ResponseWriter, r *http.Request) error {
defer can() ctx, can := context.WithCancel(r.Context())
for { defer can()
_, b, err := c.Read(ctx) r = r.WithContext(ctx)
if err != nil {
log.Println(err)
return
}
log.Printf("READ %s", b)
var m map[string]string ugs, err := ws.newUserGameServer(ctx)
if err := json.Unmarshal(b, &m); err != nil { if err != nil {
log.Println(err) return err
return }
}
log.Printf("UNMARSHAL %+v", m)
if m["party"] == "start" { go ugs.Listen(ctx, can, func(ctx context.Context) ([]byte, error) {
if gameState, err := s.games.GameState(ctx, game); err != nil { _, b, err := ws.c.Read(ctx)
log.Println(err) return b, err
return })
} else if gameState.Started {
} else if err := s.games.CreateEventAssignmentRotation(ctx, game, "", "", "", 0); err != nil {
log.Println(err)
return
}
} else if m["k"] != "" {
log.Println("TODO a kill occurred")
return
} else if name := m["name"]; name != "" {
if err := s.games.UpdateUserName(ctx, s.Session(ctx).ID, name); err != nil {
log.Println(err)
return
}
} else if m["again"] == "true" {
if gameState, err := s.games.GameState(ctx, game); err != nil {
log.Println(err)
return
} else if gameState.Completed.IsZero() {
} else {
log.Println("TODO new game")
return
}
} else {
log.Printf("UNKNOWN: %+v", m)
return
}
}
}()
var last time.Time for ugs.More(ctx) == nil {
for { if err := ws.Push(ctx, ugs); err != nil {
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(time.Second * 1):
}
if events, err := s.games.GameEvents(ctx, game, last); err != nil {
return err
} else if len(events) == 0 {
continue
}
last = time.Now()
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 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, 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, gameState GameState) (msg map[string]any, _ error) {
msg["page"] = "A"
items := []map[string]any{}
for k := range gameState.Players {
name, err := ws.games.UserName(ctx, k)
if err != nil {
return nil, err
}
items = append(items, map[string]any{"name": name})
}
msg["items"] = items
return msg, nil
}
func (ws WS) completeMsg(ctx context.Context, gameState GameState) (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 GameState) (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 nil, io.EOF
}
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 GameState) ([]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 GameState, uid string) (*inProgressMsgItem, error) {
if isSelf := uid == ugs.Session.ID; isSelf {
return nil, nil
}
v := gameState.Players[uid]
self := gameState.Players[ugs.Session.ID]
tags := []inProgressMsgItemTag{}
if hasBeenKilledWithGlobal := slices.ContainsFunc(self.Kills, func(a Kill) bool {
return a.KillWord.Word == self.KillWords.Global.Word && a.Victim == uid
}); !hasBeenKilledWithGlobal {
tags = append(tags, inProgressMsgItemTag{
K: self.KillWords.Global.Word,
V: self.KillWords.Global.Points,
})
}
for _, public := range v.KillWords.Publics() {
tags = append(tags, inProgressMsgItemTag{
K: public.Word,
V: public.Points,
})
}
if isAssigned := self.KillWords.Assignee == uid; isAssigned {
for _, private := range v.KillWords.Privates() {
tags = append(tags, inProgressMsgItemTag{
K: private.Word,
V: private.Points,
})
}
}
name, err := ws.games.UserName(ctx, uid)
return &inProgressMsgItem{
Name: name,
Title: strconv.Itoa(v.Points()),
Tags: tags,
}, err
} }