From d40200b93058d79ec41c75d8bfe64a0762d81a75 Mon Sep 17 00:00:00 2001 From: William Kennedy Date: Sat, 27 Jan 2024 15:58:39 -0500 Subject: [PATCH] refactored code around state and events --- app/cli/liars/board/bet.go | 14 +-- app/cli/liars/board/board.go | 22 ++--- app/cli/liars/board/draw.go | 28 +++--- app/cli/liars/board/events.go | 22 ++--- app/cli/liars/board/keyboard.go | 38 ++++---- app/cli/liars/board/modal.go | 14 +-- app/cli/liars/engine/engine.go | 84 ++++++++-------- app/cli/liars/engine/models.go | 4 +- .../engine/v1/handlers/gamegrp/events.go | 33 ++++++- .../engine/v1/handlers/gamegrp/gamegrp.go | 72 ++++++-------- .../engine/v1/handlers/gamegrp/models.go | 52 ++++++++-- .../engine/v1/handlers/gamegrp/route.go | 2 +- business/core/game/game.go | 6 +- business/core/game/game_test.go | 96 ++++++++++--------- business/core/game/model.go | 4 +- business/core/game/stores/gamedb/gamedb.go | 21 ++++ business/core/game/stores/gamedb/model.go | 54 +++++++++++ business/data/migrate/sql/migrate.sql | 12 +-- 18 files changed, 351 insertions(+), 227 deletions(-) create mode 100644 business/core/game/stores/gamedb/gamedb.go create mode 100644 business/core/game/stores/gamedb/model.go diff --git a/app/cli/liars/board/bet.go b/app/cli/liars/board/bet.go index d01061fc..936245d7 100644 --- a/app/cli/liars/board/bet.go +++ b/app/cli/liars/board/bet.go @@ -9,7 +9,7 @@ import ( // addBet takes the value selected on the keyboard and adds it to the // bet slice and screen. func (b *Board) addBet(r rune) error { - if b.lastStatus.CurrentAcctID != b.accountID { + if b.lastState.CurrentAcctID != b.accountID { return errors.New("not your turn") } @@ -37,7 +37,7 @@ func (b *Board) addBet(r rune) error { // subBet removes a value from the bet slice and screen. func (b *Board) subBet() error { - if b.lastStatus.CurrentAcctID != b.accountID { + if b.lastState.CurrentAcctID != b.accountID { return errors.New("not your turn") } @@ -68,16 +68,16 @@ func (b *Board) subBet() error { // enterBet is called to submit a bet. func (b *Board) enterBet() error { - status, err := b.engine.QueryStatus(b.lastStatus.GameID) + state, err := b.engine.QueryState(b.lastState.GameID) if err != nil { return err } - if status.Status != "playing" { - return errors.New("invalid status state: " + status.Status) + if state.Status != "playing" { + return errors.New("invalid status state: " + state.Status) } - if status.CurrentAcctID != b.accountID { + if state.CurrentAcctID != b.accountID { return errors.New("not your turn") } @@ -85,7 +85,7 @@ func (b *Board) enterBet() error { return errors.New("missing bet information") } - if _, err = b.engine.Bet(b.lastStatus.GameID, len(b.bets), b.bets[0]); err != nil { + if _, err = b.engine.Bet(b.lastState.GameID, len(b.bets), b.bets[0]); err != nil { return err } diff --git a/app/cli/liars/board/board.go b/app/cli/liars/board/board.go index e1c27a3a..26737a8d 100644 --- a/app/cli/liars/board/board.go +++ b/app/cli/liars/board/board.go @@ -50,17 +50,17 @@ var words = []string{"", "one's", "two's", "three's", "four's", "five's", "six's // Board represents the game board and all its state. type Board struct { - accountID common.Address - engine *engine.Engine - config engine.Config - screen tcell.Screen - style tcell.Style - bets []rune - messages []string - lastStatus engine.Status - modalUp bool - modalMsg string - modalFn func(r rune) + accountID common.Address + engine *engine.Engine + config engine.Config + screen tcell.Screen + style tcell.Style + bets []rune + messages []string + lastState engine.State + modalUp bool + modalMsg string + modalFn func(r rune) } // New contructs a game board and renders the board. diff --git a/app/cli/liars/board/draw.go b/app/cli/liars/board/draw.go index 88f8c231..76eb688a 100644 --- a/app/cli/liars/board/draw.go +++ b/app/cli/liars/board/draw.go @@ -12,10 +12,10 @@ import ( // drawInit generates the initial game board and starts the event loop. func (b *Board) drawInit(active bool) error { - var status engine.Status - if b.lastStatus.GameID != "" { + var state engine.State + if b.lastState.GameID != "" { var err error - status, err = b.engine.QueryStatus(b.lastStatus.GameID) + state, err = b.engine.QueryState(b.lastState.GameID) if err != nil { return err } @@ -51,19 +51,19 @@ func (b *Board) drawInit(active bool) error { b.screen.SetCursorStyle(tcell.CursorStyleBlinkingBlock) b.print(betRowX, betRowY, " ") - b.drawBoard(status) + b.drawBoard(state) return nil } // drawBoard display the status information. -func (b *Board) drawBoard(status engine.Status) { +func (b *Board) drawBoard(status engine.State) { if status.GameID == "" { return } - // Save this status for modal support. - b.lastStatus = status + // Save this state for modal support. + b.lastState = status // Print the current game status and round. b.print(helpX+11, statusY-6, fmt.Sprintf("%-10s / %s", status.Status, status.GameID)) @@ -87,13 +87,6 @@ func (b *Board) drawBoard(status engine.Status) { var pot float64 - // Clear the player lines. - for i := 0; i < 5; i++ { - addrY := columnHeight + 1 + i - b.print(playersX, addrY, fmt.Sprintf("%-*s", boardWidth-4, " ")) - b.print(myDiceX, myDiceY, fmt.Sprintf("%-20s", " ")) - } - // Print the player lines. for i, cup := range status.Cups { pot += status.AnteUSD @@ -157,6 +150,13 @@ func (b *Board) drawBoard(status engine.Status) { } } + // Print any existing messages. + b.print(3, messageHeight+1, b.messages[0]) + b.print(3, messageHeight+2, b.messages[1]) + b.print(3, messageHeight+3, b.messages[2]) + b.print(3, messageHeight+4, b.messages[3]) + b.print(3, messageHeight+5, b.messages[4]) + // Hide the cursor to show the game is over. if status.Status == "gameover" { b.screen.HideCursor() diff --git a/app/cli/liars/board/events.go b/app/cli/liars/board/events.go index 40b40210..8801dda2 100644 --- a/app/cli/liars/board/events.go +++ b/app/cli/liars/board/events.go @@ -15,12 +15,12 @@ func (b *Board) webEvents(event string, address common.Address) { b.printMessage(message, true) } - var status engine.Status + var state engine.State var err error switch event { case "start": - status, err = b.engine.RollDice(b.lastStatus.GameID) + state, err = b.engine.RollDice(b.lastState.GameID) if err != nil { b.printMessage("error rolling dice", true) } @@ -34,12 +34,12 @@ func (b *Board) webEvents(event string, address common.Address) { } case "callliar": - status, err = b.modalWinnerLoser("*** WON ROUND ***", "*** LOST ROUND ***") + state, err = b.modalWinnerLoser("*** WON ROUND ***", "*** LOST ROUND ***") if err != nil { b.printMessage("winner/loser", true) } - status, err = b.reconcile(status) + state, err = b.reconcile(state) if err != nil { b.printMessage(err.Error(), true) } @@ -49,19 +49,19 @@ func (b *Board) webEvents(event string, address common.Address) { } // If we don't have a new status, retrieve the latest. - if status.Status == "" { - status, err = b.engine.QueryStatus(b.lastStatus.GameID) + if state.Status == "" { + state, err = b.engine.QueryState(b.lastState.GameID) if err != nil { return } } // Redraw the screen on any event to keep it up to date. - b.drawBoard(status) + b.drawBoard(state) } // reconcile the game the winner gets paid. -func (b *Board) reconcile(status engine.Status) (engine.Status, error) { +func (b *Board) reconcile(status engine.State) (engine.State, error) { if status.Status != "gameover" { return status, nil } @@ -70,10 +70,10 @@ func (b *Board) reconcile(status engine.Status) (engine.Status, error) { return status, nil } - newStatus, err := b.engine.Reconcile(status.GameID) + newState, err := b.engine.Reconcile(status.GameID) if err != nil { - return engine.Status{}, err + return engine.State{}, err } - return newStatus, nil + return newState, nil } diff --git a/app/cli/liars/board/keyboard.go b/app/cli/liars/board/keyboard.go index 79aea12a..f3409075 100644 --- a/app/cli/liars/board/keyboard.go +++ b/app/cli/liars/board/keyboard.go @@ -106,12 +106,12 @@ func (b *Board) value(r rune) error { // newGame starts a new game. func (b *Board) newGame() error { - status, err := b.engine.NewGame() + state, err := b.engine.NewGame() if err != nil { return err } - b.lastStatus = status + b.lastState = state b.drawInit(true) @@ -120,7 +120,7 @@ func (b *Board) newGame() error { // joinGame adds the account to the game. func (b *Board) joinGame() error { - tables, err := b.engine.Tables(b.lastStatus.GameID) + tables, err := b.engine.Tables(b.lastState.GameID) if err != nil { return err } @@ -139,36 +139,36 @@ func (b *Board) joinGame() error { gameID := tables.GameIDs[sel-1] - status, err := b.engine.QueryStatus(gameID) + state, err := b.engine.QueryState(gameID) if err != nil { b.closeModal() b.showModal(err.Error()) return } - for _, acct := range status.CupsOrder { + for _, acct := range state.CupsOrder { if acct.Cmp(b.accountID) == 0 { b.closeModal() - b.lastStatus = status + b.lastState = state b.drawInit(true) return } } - if status.Status != "newgame" { + if state.Status != "newgame" { b.closeModal() - b.showModal(fmt.Sprintf("invalid status state: " + status.Status)) + b.showModal(fmt.Sprintf("invalid status state: " + state.Status)) return } - status, err = b.engine.JoinGame(gameID) + state, err = b.engine.JoinGame(gameID) if err != nil { b.closeModal() b.showModal(err.Error()) return } - b.lastStatus = status + b.lastState = state } b.showModalList(tables.GameIDs, fn) @@ -178,16 +178,16 @@ func (b *Board) joinGame() error { // startGame start the game so it can be played. func (b *Board) startGame() error { - status, err := b.engine.QueryStatus(b.lastStatus.GameID) + state, err := b.engine.QueryState(b.lastState.GameID) if err != nil { return err } - if status.Status != "newgame" { - return errors.New("invalid status state: " + status.Status) + if state.Status != "newgame" { + return errors.New("invalid status state: " + state.Status) } - if _, err := b.engine.StartGame(b.lastStatus.GameID); err != nil { + if _, err := b.engine.StartGame(b.lastState.GameID); err != nil { return err } @@ -196,20 +196,20 @@ func (b *Board) startGame() error { // callLiar calls the last bet a lie. func (b *Board) callLiar() error { - status, err := b.engine.QueryStatus(b.lastStatus.GameID) + state, err := b.engine.QueryState(b.lastState.GameID) if err != nil { return err } - if status.Status != "playing" { - return errors.New("invalid status state: " + status.Status) + if state.Status != "playing" { + return errors.New("invalid status state: " + state.Status) } - if status.CurrentAcctID != b.accountID { + if state.CurrentAcctID != b.accountID { return errors.New("not your turn") } - if _, err := b.engine.Liar(b.lastStatus.GameID); err != nil { + if _, err := b.engine.Liar(b.lastState.GameID); err != nil { return err } diff --git a/app/cli/liars/board/modal.go b/app/cli/liars/board/modal.go index 6525ea50..0970f3f0 100644 --- a/app/cli/liars/board/modal.go +++ b/app/cli/liars/board/modal.go @@ -8,19 +8,19 @@ import ( ) // modalWinnerLoser shows the user if they won or lost. -func (b *Board) modalWinnerLoser(win string, los string) (engine.Status, error) { - status, err := b.engine.QueryStatus(b.lastStatus.GameID) +func (b *Board) modalWinnerLoser(win string, los string) (engine.State, error) { + state, err := b.engine.QueryState(b.lastState.GameID) if err != nil { - return engine.Status{}, err + return engine.State{}, err } - if status.LastWinAcctID == b.accountID { + if state.LastWinAcctID == b.accountID { b.showModal(win) - return status, nil + return state, nil } b.showModal(los) - return status, nil + return state, nil } // showModal displays a modal dialog box. @@ -71,7 +71,7 @@ func (b *Board) closeModal() { b.modalFn = nil active := false - if b.lastStatus.CurrentAcctID == b.accountID { + if b.lastState.CurrentAcctID == b.accountID { active = true } diff --git a/app/cli/liars/engine/engine.go b/app/cli/liars/engine/engine.go index 498938dd..970ad9a6 100644 --- a/app/cli/liars/engine/engine.go +++ b/app/cli/liars/engine/engine.go @@ -146,16 +146,16 @@ func (e *Engine) Configuration() (Config, error) { return config, nil } -// QueryStatus starts a new game on the game engine. -func (e *Engine) QueryStatus(gameID string) (Status, error) { - url := fmt.Sprintf("%s/v1/game/%s/status", e.url, gameID) +// QueryState returns the current state of the specified game. +func (e *Engine) QueryState(gameID string) (State, error) { + url := fmt.Sprintf("%s/v1/game/%s/state", e.url, gameID) - var status Status - if err := e.do(url, &status, nil); err != nil { - return Status{}, err + var state State + if err := e.do(url, &state, nil); err != nil { + return State{}, err } - return status, nil + return state, nil } // Tables returns the current set of tables. @@ -171,87 +171,87 @@ func (e *Engine) Tables(gameID string) (Tables, error) { } // NewGame starts a new game on the game engine. -func (e *Engine) NewGame() (Status, error) { +func (e *Engine) NewGame() (State, error) { url := fmt.Sprintf("%s/v1/game/new", e.url) - var status Status - if err := e.do(url, &status, nil); err != nil { - return Status{}, err + var state State + if err := e.do(url, &state, nil); err != nil { + return State{}, err } - return status, nil + return state, nil } // StartGame generates the five dice for the player. -func (e *Engine) StartGame(gameID string) (Status, error) { +func (e *Engine) StartGame(gameID string) (State, error) { url := fmt.Sprintf("%s/v1/game/%s/start", e.url, gameID) - var status Status - if err := e.do(url, &status, nil); err != nil { - return Status{}, err + var state State + if err := e.do(url, &state, nil); err != nil { + return State{}, err } - return status, nil + return state, nil } // RollDice generates the five dice for the player. -func (e *Engine) RollDice(gameID string) (Status, error) { +func (e *Engine) RollDice(gameID string) (State, error) { url := fmt.Sprintf("%s/v1/game/%s/rolldice", e.url, gameID) - var status Status - if err := e.do(url, &status, nil); err != nil { - return Status{}, err + var state State + if err := e.do(url, &state, nil); err != nil { + return State{}, err } - return status, nil + return state, nil } // JoinGame adds a player to the current game. -func (e *Engine) JoinGame(gameID string) (Status, error) { +func (e *Engine) JoinGame(gameID string) (State, error) { url := fmt.Sprintf("%s/v1/game/%s/join", e.url, gameID) - var status Status - if err := e.do(url, &status, nil); err != nil { - return Status{}, err + var state State + if err := e.do(url, &state, nil); err != nil { + return State{}, err } - return status, nil + return state, nil } // Bet submits a bet to the game engine. -func (e *Engine) Bet(gameID string, number int, suit rune) (Status, error) { +func (e *Engine) Bet(gameID string, number int, suit rune) (State, error) { url := fmt.Sprintf("%s/v1/game/%s/bet/%d/%c", e.url, gameID, number, suit) - var status Status - if err := e.do(url, &status, nil); err != nil { - return Status{}, err + var state State + if err := e.do(url, &state, nil); err != nil { + return State{}, err } - return status, nil + return state, nil } // Liar submits a liar call to the game engine. -func (e *Engine) Liar(gameID string) (Status, error) { +func (e *Engine) Liar(gameID string) (State, error) { url := fmt.Sprintf("%s/v1/game/%s/liar", e.url, gameID) - var status Status - if err := e.do(url, &status, nil); err != nil { - return Status{}, err + var state State + if err := e.do(url, &state, nil); err != nil { + return State{}, err } - return status, nil + return state, nil } // Reconcile submits a reconcile call when the game is over. -func (e *Engine) Reconcile(gameID string) (Status, error) { +func (e *Engine) Reconcile(gameID string) (State, error) { url := fmt.Sprintf("%s/v1/game/%s/reconcile", e.url, gameID) - var status Status - if err := e.do(url, &status, nil); err != nil { - return Status{}, err + var state State + if err := e.do(url, &state, nil); err != nil { + return State{}, err } - return status, nil + return state, nil } // do makes the actual http call to the engine. diff --git a/app/cli/liars/engine/models.go b/app/cli/liars/engine/models.go index 55a99c54..0081e181 100644 --- a/app/cli/liars/engine/models.go +++ b/app/cli/liars/engine/models.go @@ -21,8 +21,8 @@ type Token struct { Address common.Address `json:"address"` } -// Status represents the game status. -type Status struct { +// State represents the game state. +type State struct { GameID string `json:"gameID"` Status string `json:"status"` AnteUSD float64 `json:"anteUSD"` diff --git a/app/services/engine/v1/handlers/gamegrp/events.go b/app/services/engine/v1/handlers/gamegrp/events.go index c0b8c490..743dea87 100644 --- a/app/services/engine/v1/handlers/gamegrp/events.go +++ b/app/services/engine/v1/handlers/gamegrp/events.go @@ -1,8 +1,12 @@ package gamegrp import ( + "context" + "encoding/json" "fmt" "sync" + + "github.com/ardanlabs/liarsdice/business/web/v1/mid" ) // These types exist for documentation purposes. The API will @@ -125,7 +129,7 @@ func (evt *events) removePlayersFromGame(gID string) error { // send signals a message to every registered channel for the specified // game. Send will not block waiting for a receiver on any given channel. -func (evt *events) send(gID string, s string) { +func (evt *events) send(ctx context.Context, gID string, typ string, v ...any) { evt.mu.RLock() defer evt.mu.RUnlock() @@ -136,6 +140,31 @@ func (evt *events) send(gID string, s string) { return } + var msg string + switch { + case v == nil: + msg = fmt.Sprintf(`{"type":"%s","address":"%s"}`, typ, mid.GetSubject(ctx)) + + default: + m := map[string]any{ + "type": typ, + "address": mid.GetSubject(ctx), + } + + for i := 0; i < len(v); i = i + 2 { + if vs, ok := v[i].(string); ok { + m[vs] = v[i+1] + } + } + + data, err := json.Marshal(m) + if err != nil { + return + } + + msg = string(data) + } + for playID := range playerMap { ch, exists := evt.players[playID] if !exists { @@ -143,7 +172,7 @@ func (evt *events) send(gID string, s string) { } select { - case ch <- s: + case ch <- msg: default: } } diff --git a/app/services/engine/v1/handlers/gamegrp/gamegrp.go b/app/services/engine/v1/handlers/gamegrp/gamegrp.go index 7bcb0c36..6727b44c 100644 --- a/app/services/engine/v1/handlers/gamegrp/gamegrp.go +++ b/app/services/engine/v1/handlers/gamegrp/gamegrp.go @@ -199,8 +199,8 @@ func (h *handlers) tables(ctx context.Context, w http.ResponseWriter, r *http.Re return web.Respond(ctx, w, info, http.StatusOK) } -// status will return information about the game. -func (h *handlers) status(ctx context.Context, w http.ResponseWriter, r *http.Request) error { +// state will return information about the game. +func (h *handlers) state(ctx context.Context, w http.ResponseWriter, r *http.Request) error { claims := mid.GetClaims(ctx) address := common.HexToAddress(claims.Subject) @@ -208,47 +208,33 @@ func (h *handlers) status(ctx context.Context, w http.ResponseWriter, r *http.Re g, err := game.Tables.Retrieve(gameID) if err != nil { - resp := Status{ + resp := appState{ Status: "nogame", AnteUSD: h.anteUSD, } return web.Respond(ctx, w, resp, http.StatusOK) } - status := g.Info(ctx) + state := g.State(ctx) - var cups []Cup - for _, accountID := range status.ExistingPlayers { - cup := status.Cups[accountID] + var cups []appCup + for _, accountID := range state.ExistingPlayers { + cup := state.Cups[accountID] // Don't share the dice information for other players. dice := []int{0, 0, 0, 0, 0} if accountID == address { dice = cup.Dice } - cups = append(cups, Cup{Player: cup.Player, Dice: dice, Outs: cup.Outs}) + cups = append(cups, toAppCup(cup, dice)) } - var bets []Bet - for _, bet := range status.Bets { - bets = append(bets, Bet{Player: bet.Player, Number: bet.Number, Suit: bet.Suit}) + var bets []appBet + for _, bet := range state.Bets { + bets = append(bets, toAppBet(bet)) } - resp := Status{ - GameID: status.GameID, - Status: status.Status, - AnteUSD: h.anteUSD, - PlayerLastOut: status.PlayerLastOut, - PlayerLastWin: status.PlayerLastWin, - PlayerTurn: status.PlayerTurn, - Round: status.Round, - Cups: cups, - ExistingPlayers: status.ExistingPlayers, - Bets: bets, - Balances: status.Balances, - } - - return web.Respond(ctx, w, resp, http.StatusOK) + return web.Respond(ctx, w, toAppState(state, h.anteUSD, cups, bets), http.StatusOK) } // newGame creates a new game if there is no game or the status of the current game @@ -268,7 +254,7 @@ func (h *handlers) newGame(ctx context.Context, w http.ResponseWriter, r *http.R ctx = web.SetParam(ctx, "id", g.ID()) - return h.status(ctx, w, r) + return h.state(ctx, w, r) } // join adds the given player to the game. @@ -298,9 +284,9 @@ func (h *handlers) join(ctx context.Context, w http.ResponseWriter, r *http.Requ h.log.Info(ctx, "evts.addPlayerToGame", "ERROR", err, "account", subjectID) } - evts.send(g.ID(), fmt.Sprintf(`{"type":"join","address":%q}`, subjectID)) + evts.send(ctx, g.ID(), "join") - return h.status(ctx, w, r) + return h.state(ctx, w, r) } // startGame changes the status of the game so players can begin to play. @@ -314,9 +300,9 @@ func (h *handlers) startGame(ctx context.Context, w http.ResponseWriter, r *http return v1.NewTrustedError(err, http.StatusBadRequest) } - evts.send(g.ID(), fmt.Sprintf(`{"type":"start","address":%q}`, mid.GetSubject(ctx))) + evts.send(ctx, g.ID(), "start") - return h.status(ctx, w, r) + return h.state(ctx, w, r) } // rollDice will roll 5 dice for the given player and game. @@ -330,9 +316,9 @@ func (h *handlers) rollDice(ctx context.Context, w http.ResponseWriter, r *http. return v1.NewTrustedError(err, http.StatusBadRequest) } - evts.send(g.ID(), fmt.Sprintf(`{"type":"rolldice","address":%q}`, mid.GetSubject(ctx))) + evts.send(ctx, g.ID(), "rolldice") - return h.status(ctx, w, r) + return h.state(ctx, w, r) } // bet processes a bet made by a player in a game. @@ -358,9 +344,9 @@ func (h *handlers) bet(ctx context.Context, w http.ResponseWriter, r *http.Reque return v1.NewTrustedError(err, http.StatusBadRequest) } - evts.send(g.ID(), fmt.Sprintf(`{"type":"bet","address":%q,"index":%d}`, address, g.Info(ctx).Cups[address].OrderIdx)) + evts.send(ctx, g.ID(), "bet", "index", g.State(ctx).Cups[address].OrderIdx) - return h.status(ctx, w, r) + return h.state(ctx, w, r) } // callLiar processes the claims and defines a winner and a loser for the round. @@ -378,9 +364,9 @@ func (h *handlers) callLiar(ctx context.Context, w http.ResponseWriter, r *http. return v1.NewTrustedError(err, http.StatusBadRequest) } - evts.send(g.ID(), fmt.Sprintf(`{"type":"callliar","address":%q}`, mid.GetSubject(ctx))) + evts.send(ctx, g.ID(), "callliar") - return h.status(ctx, w, r) + return h.state(ctx, w, r) } // reconcile calls the smart contract reconcile method. @@ -397,11 +383,11 @@ func (h *handlers) reconcile(ctx context.Context, w http.ResponseWriter, r *http return v1.NewTrustedError(err, http.StatusInternalServerError) } - evts.send(g.ID(), fmt.Sprintf(`{"type":"reconcile","address":%q}`, mid.GetSubject(ctx))) + evts.send(ctx, g.ID(), "reconcile") evts.removePlayersFromGame(g.ID()) - return h.status(ctx, w, r) + return h.state(ctx, w, r) } // balance returns the player balance from the smart contract. @@ -434,9 +420,9 @@ func (h *handlers) nextTurn(ctx context.Context, w http.ResponseWriter, r *http. return v1.NewTrustedError(err, http.StatusBadRequest) } - evts.send(g.ID(), fmt.Sprintf(`{"type":"nextturn","address":%q}`, mid.GetSubject(ctx))) + evts.send(ctx, g.ID(), "nextturn") - return h.status(ctx, w, r) + return h.state(ctx, w, r) } // updateOut replaces the current out amount of the player. This call is not @@ -459,9 +445,9 @@ func (h *handlers) updateOut(ctx context.Context, w http.ResponseWriter, r *http return v1.NewTrustedError(err, http.StatusBadRequest) } - evts.send(g.ID(), fmt.Sprintf(`{"type":"outs","address":%q}`, address)) + evts.send(ctx, g.ID(), "outs") - return h.status(ctx, w, r) + return h.state(ctx, w, r) } func validateSignature(r *http.Request, timeout time.Duration) (string, error) { diff --git a/app/services/engine/v1/handlers/gamegrp/models.go b/app/services/engine/v1/handlers/gamegrp/models.go index a2d41abb..f5a09edf 100644 --- a/app/services/engine/v1/handlers/gamegrp/models.go +++ b/app/services/engine/v1/handlers/gamegrp/models.go @@ -1,9 +1,11 @@ package gamegrp -import "github.com/ethereum/go-ethereum/common" +import ( + "github.com/ardanlabs/liarsdice/business/core/game" + "github.com/ethereum/go-ethereum/common" +) -// Status represents the game status. -type Status struct { +type appState struct { GameID string `json:"gameID"` Status string `json:"status"` AnteUSD float64 `json:"anteUSD"` @@ -11,23 +13,53 @@ type Status struct { PlayerLastWin common.Address `json:"lastWin"` PlayerTurn common.Address `json:"currentID"` Round int `json:"round"` - Cups []Cup `json:"cups"` + Cups []appCup `json:"cups"` ExistingPlayers []common.Address `json:"playerOrder"` - Bets []Bet `json:"bets"` + Bets []appBet `json:"bets"` Balances []string `json:"balances"` } -// Bet represents the bet response. -type Bet struct { +func toAppState(state game.State, anteUSD float64, cups []appCup, bets []appBet) appState { + return appState{ + GameID: state.GameID, + Status: state.Status, + AnteUSD: anteUSD, + PlayerLastOut: state.PlayerLastOut, + PlayerLastWin: state.PlayerLastWin, + PlayerTurn: state.PlayerTurn, + Round: state.Round, + Cups: cups, + ExistingPlayers: state.ExistingPlayers, + Bets: bets, + Balances: state.Balances, + } +} + +type appBet struct { Player common.Address `json:"account"` Number int `json:"number"` Suit int `json:"suit"` } -// Cup represents the cup response. -type Cup struct { +func toAppBet(bet game.Bet) appBet { + return appBet{ + Player: bet.Player, + Number: bet.Number, + Suit: bet.Suit, + } +} + +type appCup struct { Player common.Address `json:"account"` Dice []int `json:"dice"` - LastBet Bet `json:"lastBet"` + LastBet appBet `json:"lastBet"` Outs int `json:"outs"` } + +func toAppCup(cup game.Cup, dice []int) appCup { + return appCup{ + Player: cup.Player, + Dice: dice, + Outs: cup.Outs, + } +} diff --git a/app/services/engine/v1/handlers/gamegrp/route.go b/app/services/engine/v1/handlers/gamegrp/route.go index e8ba12e2..449728aa 100644 --- a/app/services/engine/v1/handlers/gamegrp/route.go +++ b/app/services/engine/v1/handlers/gamegrp/route.go @@ -51,7 +51,7 @@ func Routes(app *web.App, cfg Config) { app.Handle(http.MethodGet, version, "/game/balance", hdl.balance, mid.Authenticate(cfg.Auth)) app.Handle(http.MethodGet, version, "/game/tables", hdl.tables, mid.Authenticate(cfg.Auth)) - app.Handle(http.MethodGet, version, "/game/:id/status", hdl.status, mid.Authenticate(cfg.Auth)) + app.Handle(http.MethodGet, version, "/game/:id/state", hdl.state, mid.Authenticate(cfg.Auth)) app.Handle(http.MethodGet, version, "/game/:id/join", hdl.join, mid.Authenticate(cfg.Auth)) app.Handle(http.MethodGet, version, "/game/:id/start", hdl.startGame, mid.Authenticate(cfg.Auth)) app.Handle(http.MethodGet, version, "/game/:id/rolldice", hdl.rollDice, mid.Authenticate(cfg.Auth)) diff --git a/business/core/game/game.go b/business/core/game/game.go index f880dd38..7099b0ca 100644 --- a/business/core/game/game.go +++ b/business/core/game/game.go @@ -539,8 +539,8 @@ func (g *Game) Reconcile(ctx context.Context) (*types.Transaction, *types.Receip return tx, receipt, nil } -// Info returns a copy of the game status. -func (g *Game) Info(ctx context.Context) Status { +// State returns a copy of the game state. +func (g *Game) State(ctx context.Context) State { g.mu.RLock() defer g.mu.RUnlock() @@ -560,7 +560,7 @@ func (g *Game) Info(ctx context.Context) Status { balances[i] = g.converter.GWei2USD(bal) } - return Status{ + return State{ GameID: g.id, Status: g.status, PlayerLastOut: g.playerLastOut, diff --git a/business/core/game/game_test.go b/business/core/game/game_test.go index ce476576..89ed073d 100644 --- a/business/core/game/game_test.go +++ b/business/core/game/game_test.go @@ -81,9 +81,9 @@ func Test_SuccessGamePlay(t *testing.T) { t.Fatalf("unexpected error starting the game: %s", err) } - status := engine.Info(ctx) - if status.Status != game.StatusPlaying { - t.Fatalf("expecting game status to be %s; got %s", game.StatusPlaying, status.Status) + state := engine.State(ctx) + if state.Status != game.StatusPlaying { + t.Fatalf("expecting game status to be %s; got %s", game.StatusPlaying, state.Status) } // ------------------------------------------------------------------------- @@ -98,17 +98,17 @@ func Test_SuccessGamePlay(t *testing.T) { // ------------------------------------------------------------------------- // Game Play: Each player makes a bet and player1 calls liar. - winnerAcct := engine.Info(ctx).PlayerTurn + winnerAcct := engine.State(ctx).PlayerTurn if err := engine.Bet(ctx, winnerAcct, 2, 3); err != nil { t.Fatalf("unexpected error making bet for player1: %s", err) } - loserAcct := engine.Info(ctx).PlayerTurn + loserAcct := engine.State(ctx).PlayerTurn if err := engine.Bet(ctx, loserAcct, 3, 4); err != nil { t.Fatalf("unexpected error making bet for player2: %s", err) } - winner, loser, err := engine.CallLiar(ctx, engine.Info(ctx).PlayerTurn) + winner, loser, err := engine.CallLiar(ctx, engine.State(ctx).PlayerTurn) if err != nil { t.Fatalf("unexpected error calling liar for player1: %s", err) } @@ -124,14 +124,14 @@ func Test_SuccessGamePlay(t *testing.T) { t.Fatalf("expecting 'player2' to be the loser; got '%s'", loser) } - status = engine.Info(ctx) + state = engine.State(ctx) - if status.Cups[loserAcct].Outs != 1 { - t.Fatalf("expecting 'player2' to have 1 out; got %d", status.Cups[player2Addr].Outs) + if state.Cups[loserAcct].Outs != 1 { + t.Fatalf("expecting 'player2' to have 1 out; got %d", state.Cups[player2Addr].Outs) } - if status.Status != game.StatusRoundOver { - t.Fatalf("expecting game status to be %s; got %s", game.StatusRoundOver, status.Status) + if state.Status != game.StatusRoundOver { + t.Fatalf("expecting game status to be %s; got %s", game.StatusRoundOver, state.Status) } // ------------------------------------------------------------------------- @@ -146,10 +146,10 @@ func Test_SuccessGamePlay(t *testing.T) { t.Fatalf("expecting 2 players; got %d", leftToPlay) } - status = engine.Info(ctx) + state = engine.State(ctx) - if status.Status != game.StatusPlaying { - t.Fatalf("expecting game status to be %s; got %s", game.StatusPlaying, status.Status) + if state.Status != game.StatusPlaying { + t.Fatalf("expecting game status to be %s; got %s", game.StatusPlaying, state.Status) } // ------------------------------------------------------------------------- @@ -185,14 +185,14 @@ func Test_SuccessGamePlay(t *testing.T) { t.Fatalf("expecting 'player2' to be the loser; got '%s'", loser) } - status = engine.Info(ctx) + state = engine.State(ctx) - if status.Cups[loserAcct].Outs != 2 { - t.Fatalf("expecting 'player2' to have 2 out; got %d", status.Cups[player2Addr].Outs) + if state.Cups[loserAcct].Outs != 2 { + t.Fatalf("expecting 'player2' to have 2 out; got %d", state.Cups[player2Addr].Outs) } - if status.Status != game.StatusRoundOver { - t.Fatalf("expecting game status to be %s; got %s", game.StatusRoundOver, status.Status) + if state.Status != game.StatusRoundOver { + t.Fatalf("expecting game status to be %s; got %s", game.StatusRoundOver, state.Status) } // ------------------------------------------------------------------------- @@ -207,10 +207,10 @@ func Test_SuccessGamePlay(t *testing.T) { t.Fatalf("expecting 2 players; got %d", leftToPlay) } - status = engine.Info(ctx) + state = engine.State(ctx) - if status.Status != game.StatusPlaying { - t.Fatalf("expecting game status to be %s; got %s", game.StatusPlaying, status.Status) + if state.Status != game.StatusPlaying { + t.Fatalf("expecting game status to be %s; got %s", game.StatusPlaying, state.Status) } // ------------------------------------------------------------------------- @@ -246,14 +246,14 @@ func Test_SuccessGamePlay(t *testing.T) { t.Fatalf("expecting 'player2' to be the loser; got '%s'", loser) } - status = engine.Info(ctx) + state = engine.State(ctx) - if status.Cups[loserAcct].Outs != 3 { - t.Fatalf("expecting 'player2' to have 3 out; got %d", status.Cups[player2Addr].Outs) + if state.Cups[loserAcct].Outs != 3 { + t.Fatalf("expecting 'player2' to have 3 out; got %d", state.Cups[player2Addr].Outs) } - if status.Status != game.StatusRoundOver { - t.Fatalf("expecting game status to be %s; got %s", game.StatusRoundOver, status.Status) + if state.Status != game.StatusRoundOver { + t.Fatalf("expecting game status to be %s; got %s", game.StatusRoundOver, state.Status) } // ------------------------------------------------------------------------- @@ -268,14 +268,14 @@ func Test_SuccessGamePlay(t *testing.T) { t.Fatalf("expecting 1 player; got %d", leftToPlay) } - status = engine.Info(ctx) + state = engine.State(ctx) - if status.Status != game.StatusGameOver { - t.Fatalf("expecting game status to be %s; got %s", game.StatusGameOver, status.Status) + if state.Status != game.StatusGameOver { + t.Fatalf("expecting game status to be %s; got %s", game.StatusGameOver, state.Status) } - if status.PlayerLastWin != winnerAcct { - t.Fatalf("expecting 'player1' to be the LastWinAcct; got '%s'", status.PlayerLastWin) + if state.PlayerLastWin != winnerAcct { + t.Fatalf("expecting 'player1' to be the LastWinAcct; got '%s'", state.PlayerLastWin) } // ------------------------------------------------------------------------- @@ -324,10 +324,10 @@ func Test_SuccessGamePlay(t *testing.T) { // ------------------------------------------------------------------------- // Validate final game state - status = engine.Info(ctx) + state = engine.State(ctx) - if status.Status != game.StatusReconciled { - t.Fatalf("expecting game status to be %s; got %s", game.StatusReconciled, status.Status) + if state.Status != game.StatusReconciled { + t.Fatalf("expecting game status to be %s; got %s", game.StatusReconciled, state.Status) } } @@ -348,9 +348,10 @@ func Test_InvalidBet(t *testing.T) { t.Fatalf("unexpected error starting the game: %s", err) } - status := engine.Info(ctx) - if status.Status != game.StatusPlaying { - t.Fatalf("expecting game status to be %s; got %s", game.StatusPlaying, status.Status) + state := engine.State(ctx) + + if state.Status != game.StatusPlaying { + t.Fatalf("expecting game status to be %s; got %s", game.StatusPlaying, state.Status) } // ------------------------------------------------------------------------- @@ -365,13 +366,13 @@ func Test_InvalidBet(t *testing.T) { // ------------------------------------------------------------------------- // Game Play : player 1 makes bet and player 2 makes invalid bet - if err := engine.Bet(ctx, engine.Info(ctx).PlayerTurn, 3, 3); err != nil { + if err := engine.Bet(ctx, engine.State(ctx).PlayerTurn, 3, 3); err != nil { t.Fatalf("unexpected error making bet for player1: %s", err) } engine.NextTurn(ctx) - if err := engine.Bet(ctx, engine.Info(ctx).PlayerTurn, 2, 6); err == nil { + if err := engine.Bet(ctx, engine.State(ctx).PlayerTurn, 2, 6); err == nil { t.Fatal("expecting error making an invalid bet") } } @@ -393,13 +394,14 @@ func Test_WrongPlayerTryingToPlay(t *testing.T) { t.Fatalf("unexpected error starting the game: %s", err) } - status := engine.Info(ctx) - if status.Status != game.StatusPlaying { - t.Fatalf("expecting game status to be %s; got %s", game.StatusPlaying, status.Status) + state := engine.State(ctx) + + if state.Status != game.StatusPlaying { + t.Fatalf("expecting game status to be %s; got %s", game.StatusPlaying, state.Status) } var wrongPlayer common.Address - switch engine.Info(ctx).PlayerTurn { + switch engine.State(ctx).PlayerTurn { case player1Addr: wrongPlayer = player2Addr case player2Addr: @@ -589,10 +591,10 @@ func gameSetup(t *testing.T) (*bank.Bank, *game.Game) { t.Fatalf("unexpected error adding player 2: %s", err) } - status := game.Info(ctx) + state := game.State(ctx) - if len(status.Cups) != 2 { - t.Fatalf("expecting 2 players; got %d", len(status.Cups)) + if len(state.Cups) != 2 { + t.Fatalf("expecting 2 players; got %d", len(state.Cups)) } return bank, game diff --git a/business/core/game/model.go b/business/core/game/model.go index 31839b2d..69112c06 100644 --- a/business/core/game/model.go +++ b/business/core/game/model.go @@ -17,8 +17,8 @@ const ( // to play a game. const minNumberPlayers = 2 -// Status represents a copy of the game status. -type Status struct { +// State represents a copy of the game state. +type State struct { GameID string Status string PlayerLastOut common.Address diff --git a/business/core/game/stores/gamedb/gamedb.go b/business/core/game/stores/gamedb/gamedb.go new file mode 100644 index 00000000..e79ecd76 --- /dev/null +++ b/business/core/game/stores/gamedb/gamedb.go @@ -0,0 +1,21 @@ +// Package gamedb contains game related CRUD functionality. +package gamedb + +import ( + "github.com/ardanlabs/liarsdice/foundation/logger" + "github.com/jmoiron/sqlx" +) + +// Store manages the set of APIs for user database access. +type Store struct { + log *logger.Logger + db sqlx.ExtContext +} + +// NewStore constructs the api for data access. +func NewStore(log *logger.Logger, db *sqlx.DB) *Store { + return &Store{ + log: log, + db: db, + } +} diff --git a/business/core/game/stores/gamedb/model.go b/business/core/game/stores/gamedb/model.go new file mode 100644 index 00000000..c74af659 --- /dev/null +++ b/business/core/game/stores/gamedb/model.go @@ -0,0 +1,54 @@ +package gamedb + +type dbGame struct { + ID string `db:"game_id"` + Name string `db:"name"` +} + +type dbGameStatus struct { + ID string `db:"game_id"` + Iteration int `db:"iteration"` + Status string `db:"status"` + PlayerLastOut string `db:"player_last_out"` + PlayerLastWin string `db:"player_last_win"` + PlayerTurn string `db:"player_turn"` + Round int `db:"round"` +} + +type dbGameCup struct { + ID string `db:"game_id"` + Iteration int `db:"iteration"` + Player string `db:"player"` + OrderIdx int `db:"order_idx"` + Outs int `db:"outs"` +} + +type dbGameDice struct { + ID string `db:"game_id"` + Iteration int `db:"iteration"` + Player string `db:"player"` + Dice int `db:"dice"` +} + +type dbGameExistingPlayers struct { + ID string `db:"game_id"` + Iteration int `db:"iteration"` + Player string `db:"player"` +} + +type dbGameBets struct { + ID string `db:"game_id"` + Iteration int `db:"iteration"` + Player string `db:"player"` + Number int `db:"number"` + Suit int `db:"suit"` +} + +type dbGameBalances struct { + ID string `db:"game_id"` + Iteration int `db:"iteration"` + Player string `db:"player"` + Balance string `db:"balance"` +} + +// func toDBGame(status game.State) diff --git a/business/data/migrate/sql/migrate.sql b/business/data/migrate/sql/migrate.sql index 669a0405..5b00528d 100644 --- a/business/data/migrate/sql/migrate.sql +++ b/business/data/migrate/sql/migrate.sql @@ -24,11 +24,11 @@ CREATE TABLE game_status CREATE TABLE game_cups ( - game_id VARCHAR NOT NULL, - iteration INT NOT NULL, - player VARCHAR NOT NULL, - OrderIdx INT NOT NULL, - Outs INT NOT NULL, + game_id VARCHAR NOT NULL, + iteration INT NOT NULL, + player VARCHAR NOT NULL, + order_idx INT NOT NULL, + outs INT NOT NULL, PRIMARY KEY (game_id, iteration, player) FOREIGN KEY (game_id) REFERENCES games(game_id) ON DELETE CASCADE @@ -39,7 +39,7 @@ CREATE TABLE game_dice game_id VARCHAR NOT NULL, iteration INT NOT NULL, player VARCHAR NOT NULL, - Dice INT NOT NULL, + dice INT NOT NULL, PRIMARY KEY (game_id, iteration, player) FOREIGN KEY (game_id) REFERENCES games(game_id) ON DELETE CASCADE