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)
}
func (words KillWords) Privates() []KillWord {
a := slices.Clone(words.Assignment.Private)
slices.SortFunc(a, func(a, b KillWord) int {
return strings.Compare(a.Word, b.Word)
})
return a
}
func (words KillWords) Publics() []KillWord {
a := slices.Clone(words.Assignment.Public)
slices.SortFunc(a, func(a, b KillWord) int {
return strings.Compare(a.Word, b.Word)
})
return a
}
func (prev AllKillWords) ShuffleAssignees(killer, victim, word string) AllKillWords {
m := prev.withoutAssignees()

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"
"fmt"
"io"
"log"
"net/http"
"slices"
"strconv"
"strings"
"time"
"github.com/coder/websocket"
)
@@ -19,166 +17,106 @@ func isWS(r *http.Request) bool {
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 {
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)
if err != nil {
return err
}
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 {
ctx, can := context.WithCancel(r.Context())
defer can()
for {
_, b, err := c.Read(ctx)
r = r.WithContext(ctx)
ugs, err := ws.newUserGameServer(ctx)
if err != nil {
log.Println(err)
return
return err
}
log.Printf("READ %s", b)
var m map[string]string
if err := json.Unmarshal(b, &m); err != nil {
log.Println(err)
return
}
log.Printf("UNMARSHAL %+v", m)
go ugs.Listen(ctx, can, func(ctx context.Context) ([]byte, error) {
_, b, err := ws.c.Read(ctx)
return b, err
})
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 ugs.More(ctx) == nil {
if err := ws.Push(ctx, ugs); err != nil {
return err
}
}
}()
var last time.Time
for {
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()
func (ws WS) newUserGameServer(ctx context.Context) (*UserGameServer, error) {
return NewUserGameServer(ctx, ws.Session(ctx), ws.games)
}
gameState, err := s.games.GameState(ctx, game)
func (ws WS) Push(ctx context.Context, ugs *UserGameServer) error {
gameState, err := ugs.State(ctx)
if err != nil {
return err
}
msg := map[string]any{
"help": strings.Join([]string{
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 on the kill word a target said.",
"2. Click the word to collect points.",
"3. Review new kill words.",
"",
"The game ends when everyone has been assassinated.",
}, "<br>"),
}
}, "<br>")
if gameState.Started {
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"
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)
name, err := ws.games.UserName(ctx, k)
if err != nil {
return err
return nil, err
}
tags := []map[string]any{}
for _, kill := range v.Kills {
@@ -194,23 +132,92 @@ func (s *S) serveWS(w http.ResponseWriter, r *http.Request) error {
})
}
msg["items"] = items
}
} else {
msg["page"] = "A"
items := []map[string]any{}
for k := range gameState.Players {
name, err := s.games.UserName(ctx, k)
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 err
}
items = append(items, map[string]any{"name": name})
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
}
msgB, _ := json.Marshal(msg)
if err := c.Write(ctx, 1, msgB); err != nil {
return err
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
}