package main import ( "context" "encoding/json" "fmt" "io" "net/http" "slices" "strconv" "strings" "github.com/coder/websocket" ) 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 { 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) } func (ws WS) Serve(w http.ResponseWriter, r *http.Request) error { ctx, can := context.WithCancel(r.Context()) defer can() r = r.WithContext(ctx) ugs, err := ws.newUserGameServer(ctx) if err != nil { return err } go ugs.Listen(ctx, can, func(ctx context.Context) ([]byte, error) { _, b, err := ws.c.Read(ctx) return b, err }) for ugs.More(ctx) == nil { if err := ws.Push(ctx, ugs); err != nil { 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.", }, "
") 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 string `json:"v"` } 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) { v := gameState.Players[uid] if uid == ugs.Session.ID { return nil, nil } name, err := ws.games.UserName(ctx, uid) if err != nil { return nil, err } tags := []inProgressMsgItemTag{} if self := gameState.Players[ugs.Session.ID]; self.KillWords.Assignee == uid { for _, private := range v.KillWords.Assignment.Private { tags = append(tags, inProgressMsgItemTag{ K: private.Word, V: strconv.Itoa(private.Points), }) } } for _, public := range v.KillWords.Assignment.Public { tags = append(tags, inProgressMsgItemTag{ K: public.Word, V: strconv.Itoa(public.Points), }) } if self := gameState.Players[ugs.Session.ID]; !slices.ContainsFunc(self.Kills, func(a Kill) bool { return a.Victim == uid }) { tags = append(tags, inProgressMsgItemTag{ K: self.KillWords.Global.Word, V: strconv.Itoa(self.KillWords.Global.Points), }) } return &inProgressMsgItem{ Name: name, Title: strconv.Itoa(v.Points()), Tags: tags, }, nil }