out/cmd/server/ws.go

221 lines
5.0 KiB
Go

package main
import (
"context"
"encoding/json"
"fmt"
"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, ugs, 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, ugs *UserGameServer, gameState UserGameState) (msg map[string]any, _ error) {
msg["page"] = "A"
items := []map[string]any{}
for k := range gameState.Players {
if k == ugs.Session.ID {
continue
}
name, err := ws.games.UserName(ctx, k)
if err != nil {
return nil, err
}
items = append(items, map[string]any{"id": k, "name": name})
}
msg["items"] = items
return msg, nil
}
func (ws WS) completeMsg(ctx context.Context, gameState UserGameState) (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 UserGameState) (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 msg, nil
}
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 UserGameState) ([]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 UserGameState, uid string) (*inProgressMsgItem, error) {
if isSelf := uid == ugs.Session.ID; isSelf {
return nil, nil
}
self := gameState.Players[ugs.Session.ID]
v := gameState.Players[uid]
tags := []inProgressMsgItemTag{}
if hasBeenKilledWithGlobal := slices.ContainsFunc(self.Kills, func(a Kill) bool {
return a.Victim == uid && a.KillWord.Word == self.KillWords.Global.Word
}); !hasBeenKilledWithGlobal {
tags = append(tags, newInProgressMsgItemTag(self.KillWords.Global))
}
for _, killWord := range append(
v.KillWords.Publics(),
v.KillWords.Privates()...,
) {
tags = append(tags, newInProgressMsgItemTag(killWord))
}
name, err := ws.games.UserName(ctx, uid)
return &inProgressMsgItem{
Name: name,
Title: strconv.Itoa(v.Points()),
Tags: tags,
}, err
}
func newInProgressMsgItemTag(kw KillWord) inProgressMsgItemTag {
return inProgressMsgItemTag{
K: kw.Word,
V: kw.Points,
}
}