aboutsummaryrefslogtreecommitdiff
path: root/go/draftsim/server/main.go.gorilla
diff options
context:
space:
mode:
Diffstat (limited to 'go/draftsim/server/main.go.gorilla')
-rw-r--r--go/draftsim/server/main.go.gorilla253
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)
+ }
+}