From 105e28af362cd019686d0a4364c04879bbba985a Mon Sep 17 00:00:00 2001 From: a Date: Wed, 13 Apr 2022 19:57:28 -0500 Subject: [PATCH] Start --- cmd/cli/main.go | 7 --- cmd/server/main.go | 145 ++++++++++++++++++++++++++++++++++++++++++++ common/game/game.go | 37 ++++++++++- go.mod | 4 ++ go.sum | 8 +++ 5 files changed, 192 insertions(+), 9 deletions(-) create mode 100644 cmd/server/main.go diff --git a/cmd/cli/main.go b/cmd/cli/main.go index 7da1ab6..576627a 100644 --- a/cmd/cli/main.go +++ b/cmd/cli/main.go @@ -62,15 +62,8 @@ lives: %0.4v | score: %d | combo: %d if err != nil { log.Println(err) } - - } else { - trial.SelectCard(0) } fmt.Printf("hand: %s\n", trial.ShowString()) - case "debug": - fmt.Println(trial) - case "exit", "quit", "q": - os.Exit(0) } } } diff --git a/cmd/server/main.go b/cmd/server/main.go new file mode 100644 index 0000000..0877c0b --- /dev/null +++ b/cmd/server/main.go @@ -0,0 +1,145 @@ +package main + +import ( + "encoding/json" + "log" + "os" + "strconv" + + "context" + "net/http" + + "git.tuxpa.in/a/card_id/common/game" + "git.tuxpa.in/a/card_id/common/game/card" + "lukechampine.com/frand" + + "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/v5/middleware" + "github.com/go-chi/render" +) + +type Engine struct { + dealer *card.Dealer + + games map[string]*game.Game +} + +func main() { + dealer, err := card.NewDealer().ReadFromRoot(os.Getenv("CARD_DATA_DIR")) + if err != nil { + log.Panicln(err) + } + + e := &Engine{ + dealer: dealer, + games: map[string]*game.Game{}, + } + + r := chi.NewRouter() + + r.Use(middleware.RequestID) + r.Use(middleware.Logger) + r.Use(middleware.Recoverer) + r.Use(middleware.URLFormat) + r.Use(render.SetContentType(render.ContentTypeJSON)) + + r.Get("/healthz", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("OK")) + }) + + r.Get("/ping", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("pong")) + }) + + // RESTy routes for "instances" resource + r.Route("/game", func(r chi.Router) { + // r.Use(oauth.Authorize(os.Getenv("OAUTH_TOKEN"), nil)) + r.Post("/new", e.CreateGame) + r.Route("/{instanceId}", func(r chi.Router) { + r.Use(e.GameCtx) + r.Route("/play", func(r chi.Router) { + r.Get("/click/{cardId}", nil) + }) + r.Route("/debug", func(r chi.Router) { + r.Get("/info", e.GetGame) + }) + }) + + }) + port := ":3333" + log.Println(port) + http.ListenAndServe(port, r) +} + +// GameCtx middleware is used to load an Game object from +// the URL parameters passed through as the request. In case +// the Game could not be found, we stop here and return a 404. +func (e *Engine) GameCtx(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var instance *game.Game + if instanceID := chi.URLParam(r, "instanceId"); instanceID != "" { + instance = e.games[instanceID] + } + if instance == nil { + render.Render(w, r, ErrNotFound) + return + } + ctx := context.WithValue(r.Context(), "instance", instance) + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} + +func (e *Engine) CreateGame(w http.ResponseWriter, r *http.Request) { + id := strconv.Itoa(frand.Intn(0xfff)) + e.games[id] = game.New("", e.dealer) + w.Write([]byte(id)) + w.WriteHeader(200) +} + +func (e *Engine) GetGame(w http.ResponseWriter, r *http.Request) { + instance, ok := r.Context().Value("instance").(*game.Game) + if !ok { + render.Render(w, r, ErrNotFound) + return + } + err := json.NewEncoder(w).Encode(instance) + if err != nil { + render.Render(w, r, ErrInvalidRequest(err)) + return + } + w.WriteHeader(200) +} + +var ErrNotFound = &ErrResponse{HTTPStatusCode: 404, StatusText: "game not found."} + +type ErrResponse struct { + Err error `json:"-"` // low-level runtime error + HTTPStatusCode int `json:"-"` // http response status code + + StatusText string `json:"status"` // user-level status message + AppCode int64 `json:"code,omitempty"` // application-specific error code + ErrorText string `json:"error,omitempty"` // application-level error message, for debugging +} + +func (e *ErrResponse) Render(w http.ResponseWriter, r *http.Request) error { + render.Status(r, e.HTTPStatusCode) + return nil +} + +func ErrInvalidRequest(err error) render.Renderer { + return &ErrResponse{ + Err: err, + HTTPStatusCode: 400, + StatusText: "Invalid request.", + ErrorText: err.Error(), + } +} + +func ErrRender(err error) render.Renderer { + return &ErrResponse{ + Err: err, + HTTPStatusCode: 422, + StatusText: "Error rendering response.", + ErrorText: err.Error(), + } +} diff --git a/common/game/game.go b/common/game/game.go index 84f350b..f4205c5 100644 --- a/common/game/game.go +++ b/common/game/game.go @@ -1,7 +1,11 @@ package game import ( + "strconv" + + "git.tuxpa.in/a/card_id/common/errs" "git.tuxpa.in/a/card_id/common/game/card" + "git.tuxpa.in/a/card_id/common/game/score" "lukechampine.com/frand" ) @@ -22,15 +26,44 @@ type Game struct { Dealer *card.Dealer `json:"-"` } -func (g *Game) New(user string, dealer *card.Dealer) *Game { +func New(user string, dealer *card.Dealer) *Game { return &Game{ Dealer: dealer, Player: user, } } -func (g *Game) Resolve(action string, args []string) { +func (g *Game) Resolve(action string, args []string) (resp any, err error) { + switch action { + case "click": + resp, err = g.ActionClick(args) + default: + err = errs.Invalid("cmd not found") + } + return +} +func (g *Game) ActionClick(args []string) (resp any, err error) { + if len(args) > 1 { + num, _ := strconv.Atoi(args[1]) + _, err := g.Trial.SelectCard(num) + if err != nil { + return nil, err + } + } + return nil, errs.Invalid("missing arg") +} + +func (g *Game) Tick() (err error) { + sc, lives := g.Trial.CheckSelection() + if sc != 0 { + g.Combo = g.Combo + 1 + g.Score = g.Score + sc*score.DefaultTable.Ratio(g.Combo)*g.CurrentTrial + } else { + g.Combo = 0 + } + g.Lives = g.Lives + lives + return nil } func (g *Game) CreateTrial(pairs int, extra int) *Trial { diff --git a/go.mod b/go.mod index 8039bb9..aa468a9 100644 --- a/go.mod +++ b/go.mod @@ -3,11 +3,15 @@ module git.tuxpa.in/a/card_id go 1.18 require ( + github.com/go-chi/chi/v5 v5.0.7 + github.com/go-chi/oauth v0.0.0-20210913085627-d937e221b3ef + github.com/go-chi/render v1.0.1 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b lukechampine.com/frand v1.4.2 ) require ( github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect + github.com/gofrs/uuid v4.0.0+incompatible // indirect golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb // indirect ) diff --git a/go.sum b/go.sum index a10dd4e..36f4a1a 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,13 @@ github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY= github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA= +github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8= +github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-chi/oauth v0.0.0-20210913085627-d937e221b3ef h1:lqU8HyH6bzhV+HHvgFaT2xBl19tcjs9F4UULmw3hTxc= +github.com/go-chi/oauth v0.0.0-20210913085627-d937e221b3ef/go.mod h1:eFAdB6Jo7GOKhl1PWiN2lKPxgFr7dBFkRrsz6S5IwOs= +github.com/go-chi/render v1.0.1 h1:4/5tis2cKaNdnv9zFLfXzcquC9HbeZgCnxGnKrltBS8= +github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns= +github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= +github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb h1:fgwFCsaw9buMuxNd6+DQfAuSFqbNiQZpcgJQAgJsK6k= golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=