diff options
Diffstat (limited to 'go/draftsim/server/main.go.gorilla')
| -rw-r--r-- | go/draftsim/server/main.go.gorilla | 253 |
1 files changed, 253 insertions, 0 deletions
diff --git a/go/draftsim/server/main.go.gorilla b/go/draftsim/server/main.go.gorilla new file mode 100644 index 00000000..7b0b9a7b --- /dev/null +++ b/go/draftsim/server/main.go.gorilla @@ -0,0 +1,253 @@ +// Draftsim server implementation +// TODO: implement timeout +// TODO: implement reconnect +// TODO: persist draft rates +package main + +import ( + "encoding/json" + "errors" + "flag" + "io" + "log" + "net/http" + "os" + "strings" + "sync" + + "github.com/adrg/xdg" + "github.com/gorilla/websocket" + + "muhq.space/muhqs-game/go/draftsim/common" + "muhq.space/muhqs-game/go/game" +) + +type table struct { + id string + draft *game.Draft + lock sync.Mutex + rates draftRates + rwlock sync.RWMutex +} + +type draftRates struct { + rates map[string]float32 + lock sync.RWMutex +} + +var tables map[string]*table +var rates draftRates + +func (rates *draftRates) loadFrom(path string) (err error) { + if path == "" { + path, err = xdg.DataFile("draftsim/rates.json") + if err != nil { + return + } + } + + f, err := os.Open(path) + if err != nil { + return + } + defer f.Close() + + d, err := io.ReadAll(f) + if err != nil { + return + } + + err = json.Unmarshal(d, &rates.rates) + return +} + +func (r *draftRates) updateRateUnsafe(card string, rate float32) { + if _, found := r.rates[card]; !found { + r.rates[card] = rate + } else { + r.rates[card] = (r.rates[card] + rate) / 2 + } +} + +func (r *draftRates) updateRate(card string, rate float32) { + r.lock.Lock() + defer r.lock.Unlock() + + r.updateRateUnsafe(card, rate) +} + +func (r *draftRates) mergeRates(r2 *draftRates) { + r.lock.Lock() + defer r.lock.Unlock() + + for c, v := range r2.rates { + r.updateRateUnsafe(c, v) + } +} + +type recordingDraftCtrl struct { + game.RWPlayerControl + table *table +} + +type gorillaConnRW struct { + c *websocket.Conn +} + +func (rw gorillaConnRW) Read(p []byte) (int, error) { + _, r, err := rw.c.NextReader() + if err != nil { + return 0, err + } + + return r.Read(p) +} + +func (rw gorillaConnRW) Write(p []byte) (int, error) { + w, err := rw.c.NextWriter(websocket.TextMessage) + if err != nil { + return 0, err + } + + return w.Write(p) +} + +func (rw gorillaConnRW) Close() (error) { + return rw.c.Close() +} + +func newRecordingDraftCrtl(p *game.Player, table *table, c *websocket.Conn) *recordingDraftCtrl { + + return &recordingDraftCtrl{ + *game.NewRWPlayerControl(p, gorillaConnRW{c}, nil), + table, + } +} + +// Record the received draft pick. +func (ctrl *recordingDraftCtrl) RecvAction() game.Action { + pick := ctrl.RWPlayerControl.RecvAction().(*game.DraftPick) + rate := float32(pick.Pack().Size() / ctrl.table.draft.PackSize()) + ctrl.table.rates.updateRate(pick.Pick().Name, rate) + return pick +} + +var upgrader = websocket.Upgrader { + // FIXME: find a way to send an Origin header using wasm + CheckOrigin: func(*http.Request) bool { return true }, +} + +func joinDraft(w http.ResponseWriter, r *http.Request) { + c, err := upgrader.Upgrade(w, r, nil) + if err != nil { + log.Println("upgrade failed:", err) + } + + var args common.JoinParams + err = c.ReadJSON(&args) + if err != nil { + c.WriteJSON("failed to parse request body") + c.Close() + return + } + + tableId := args.Table + + var found bool + var t *table + if t, found = tables[tableId]; !found { + t = &table{id: tableId, draft: game.NewDraftFromDesc([]*game.Player{}, args.Desc)} + tables[tableId] = t + } + + t.lock.Lock() + defer t.lock.Unlock() + + p := game.NewPlayer(-1, args.Name, game.NewDeck(), nil, nil) + p.Ctrl = newRecordingDraftCrtl(p, t, c) + t.draft.AddPlayer(p) +} + +func startDraft(w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(r.Body) + w.Header().Set("Access-Control-Allow-Origin", "*") + if err != nil { + http.Error(w, "failed to read request body", 400) + } + tableId := string(body) + if t, found := tables[tableId]; found { + go func() { + t.draft.Run() + for _, p := range t.draft.Players() { + p.Ctrl.(*recordingDraftCtrl).Close() + } + rates.mergeRates(&t.rates) + }() + } else { + http.Error(w, "table not found", 404) + } +} + +func allStats(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Content-Type", "application/json") + rates.lock.RLock() + resp, _ := json.Marshal(rates.rates) + rates.lock.RUnlock() + w.Write(resp) +} + +func specificStats(w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, "failed to read request body", 400) + } + cards := string(body) + + specific := make(map[string]float32) + + rates.lock.RLock() + for card := range strings.SplitSeq(cards, "\n") { + specific[card] = rates.rates[card] + } + rates.lock.RUnlock() + + resp, _ := json.Marshal(specific) + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Content-Type", "application/json") + w.Write(resp) +} + +func cardStat(w http.ResponseWriter, r *http.Request) { + card := r.PathValue("card") + rates.lock.RLock() + resp, _ := json.Marshal(rates.rates[card]) + rates.lock.RUnlock() + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Content-Type", "application/json") + w.Write(resp) +} + +var ratesFile = flag.String("ratesfile", "", "file to persist the rates") + +var addr = flag.String("addr", ":8080", "http service address") + +func main() { + tables = make(map[string]*table) + rates = draftRates{rates: make(map[string]float32)} + flag.Parse() + + if err := rates.loadFrom(*ratesFile); err != nil && !errors.Is(err, os.ErrNotExist) { + log.Fatal(err) + } + + http.HandleFunc("/join", joinDraft) + http.HandleFunc("POST /start", startDraft) + http.HandleFunc("GET /stats", allStats) + http.HandleFunc("POST /stats", specificStats) + http.HandleFunc("GET /stats/{card}", cardStat) + log.Println("listening on", *addr) + if err := http.ListenAndServe(*addr, nil); err != nil { + log.Fatal(err) + } +} |
