From 61b30756f13612cf666be0d28932c2296ec6fac6 Mon Sep 17 00:00:00 2001 From: Daithi Hearn Date: Wed, 17 Jan 2024 09:37:53 +0100 Subject: [PATCH] feat: adding revision chore: unit tests --- pkg/game/game-handler.go | 28 +++- pkg/game/game-methods.go | 256 ++++++++++++++++++++++++++++++++++ pkg/game/game-service.go | 17 +++ pkg/game/game-service_test.go | 113 +++++++++++++++ pkg/game/game.go | 246 +------------------------------- pkg/game/game_test.go | 50 +++++-- 6 files changed, 447 insertions(+), 263 deletions(-) create mode 100644 pkg/game/game-methods.go diff --git a/pkg/game/game-handler.go b/pkg/game/game-handler.go index e3a6b71..98d6256 100644 --- a/pkg/game/game-handler.go +++ b/pkg/game/game-handler.go @@ -5,6 +5,7 @@ import ( "cards-110-api/pkg/auth" "github.com/gin-gonic/gin" "net/http" + "strconv" ) type Handler struct { @@ -99,8 +100,10 @@ func (h *Handler) Get(c *gin.Context) { // @ID get-game-state // @Produce json // @Param gameId path string true "Game ID" +// @Param revision query int false "Revision" // @Security Bearer -// @Success 200 {object} GameState +// @Success 200 {object} State +// @Success 204 "No Content" // @Failure 400 {object} api.ErrorResponse // @Failure 500 {object} api.ErrorResponse // @Router /game/{gameId}/state [get] @@ -117,8 +120,11 @@ func (h *Handler) GetState(c *gin.Context) { // Get the game ID from the request gameId := c.Param("gameId") + // Get the revision from the request + revision, exists := c.GetQuery("revision") + // Get the game from the database - game, has, err := h.S.Get(ctx, gameId) + state, has, err := h.S.GetState(ctx, gameId, id) if err != nil { c.JSON(http.StatusInternalServerError, api.ErrorResponse{Message: err.Error()}) return @@ -128,11 +134,19 @@ func (h *Handler) GetState(c *gin.Context) { return } - // Get the state for the current user - state, err := game.GetState(id) - if err != nil { - c.JSON(http.StatusInternalServerError, api.ErrorResponse{Message: err.Error()}) - return + // Check if the game has been updated + if exists { + // First convert the revision to an int + // If the revision is less than or equal to the current revision, return no content + rev, err := strconv.Atoi(revision) + if err != nil { + c.JSON(http.StatusBadRequest, api.ErrorResponse{Message: "Invalid revision"}) + return + } + if state.Revision <= rev { + c.Status(http.StatusNoContent) + return + } } c.IndentedJSON(http.StatusOK, state) diff --git a/pkg/game/game-methods.go b/pkg/game/game-methods.go new file mode 100644 index 0000000..cffd158 --- /dev/null +++ b/pkg/game/game-methods.go @@ -0,0 +1,256 @@ +package game + +import ( + "fmt" + "log" + "time" +) + +func (g *Game) Me(playerID string) (Player, error) { + for _, p := range g.Players { + if p.ID == playerID { + return p, nil + } + } + return Player{}, fmt.Errorf("player not found in game") +} + +func (g *Game) GetState(playerID string) (State, error) { + // Get player + me, err := g.Me(playerID) + if err != nil { + return State{}, err + } + + // 1. Find dummy + var dummy Player + if g.CurrentRound.GoerID == playerID { + for _, p := range g.Players { + if p.ID == "dummy" { + dummy = p + break + } + } + } + + // 2. Get max call + maxCall := Pass + for _, p := range g.Players { + if p.Call > maxCall { + maxCall = p.Call + } + } + + // 3. Add dummy if applicable + iamGoer := g.CurrentRound.GoerID == playerID + if iamGoer && g.CurrentRound.Status == Called && dummy.ID != "" { + me.Cards = append(me.Cards, dummy.Cards...) + } + + // 4. Return player's game state + gameState := State{ + ID: g.ID, + Revision: g.Revision, + Me: me, + IamSpectator: false, + IsMyGo: g.CurrentRound.CurrentHand.CurrentPlayerID == me.ID, + IamGoer: iamGoer, + IamDealer: g.CurrentRound.DealerID == me.ID, + IamAdmin: g.AdminID == me.ID, + Cards: me.Cards, + Status: g.Status, + Round: g.CurrentRound, + MaxCall: maxCall, + Players: make([]Player, 0), + } + + for _, p := range g.Players { + if p.ID != "dummy" { + gameState.Players = append(gameState.Players, p) + } + } + + return gameState, nil +} + +func (g *Game) EndRound() error { + // Add the current round to the completed rounds + g.Completed = append(g.Completed, g.CurrentRound) + + // Get next dealer + nextDealer, err := nextPlayer(g.Players, g.CurrentRound.DealerID) + if err != nil { + return err + } + + // Create next hand + nextPlayer, err := nextPlayer(g.Players, nextDealer.ID) + if err != nil { + return err + } + nextHand := Hand{ + Timestamp: time.Now(), + CurrentPlayerID: nextPlayer.ID, + } + + // Create a new round + g.CurrentRound = Round{ + Timestamp: time.Now(), + Number: g.CurrentRound.Number + 1, + DealerID: nextDealer.ID, + Status: Calling, + CurrentHand: nextHand, + } + + // Deal the cards + deck := ShuffleCards(NewDeck()) + g.Deck, g.Players = DealCards(deck, g.Players) + + // Increment revision + g.Revision++ + + return nil +} + +func (g *Game) Call(playerID string, call Call) error { + // Check the game is active + if g.Status != Active { + return fmt.Errorf("game not active") + } + + // Check current round is calling + if g.CurrentRound.Status != Calling { + return fmt.Errorf("round not calling") + } + + // Check the player is the current player + if g.CurrentRound.CurrentHand.CurrentPlayerID != playerID { + return fmt.Errorf("not current player") + } + + // If they are in the bunker (score < -30) they can only pass + state, err := g.GetState(playerID) + if err != nil { + return err + } + if state.Me.Score < -30 && call != Pass { + return fmt.Errorf("player in bunker") + } + + // Check the call is valid i.e. > all previous calls or a pass + // The dealer can take a call of greater than 10 + if call != Pass { + callForComparison := call + if state.IamDealer { + callForComparison++ + } + for _, p := range g.Players { + if p.Call >= callForComparison { + return fmt.Errorf("invalid call") + } + } + } + + // Validate 10 call + if call == Ten { + if len(g.Players) != 6 { + return fmt.Errorf("can only call 10 in doubles") + } + } + + // Set the player's call + for i, p := range g.Players { + if p.ID == playerID { + g.Players[i].Call = call + break + } + } + + // Set next player/round status + if call == Jink { + log.Printf("Jink called by %s", playerID) + if state.IamDealer { + // If the dealer calls Jink, calling is complete + g.CurrentRound.Status = Called + g.CurrentRound.GoerID = playerID + g.CurrentRound.CurrentHand.CurrentPlayerID = playerID + } else { + // If any other player calls Jink, jump to the dealer + g.CurrentRound.CurrentHand.CurrentPlayerID = g.CurrentRound.DealerID + } + } else if state.IamDealer { + // Get the highest calls + var topCall Call + for _, p := range g.Players { + if p.Call > topCall { + topCall = p.Call + } + } + + if topCall <= Ten { + log.Printf("No one called. Starting new round...") + err = g.EndRound() + return err + } + + // Get the players who made the top call (the dealer may have taken a call, this will result in more than one player) + var topCallPlayers []Player + for _, p := range g.Players { + if p.Call == topCall { + topCallPlayers = append(topCallPlayers, p) + } + } + if len(topCallPlayers) == 0 || len(topCallPlayers) > 2 { + return fmt.Errorf("invalid call state. There are %d top callers of %d", len(topCallPlayers), topCall) + } + var takenPlayer Player + var caller Player + if len(topCallPlayers) == 2 { + for _, p := range topCallPlayers { + if p.ID == g.CurrentRound.DealerID { + caller = p + } else { + takenPlayer = p + } + } + } else { + caller = topCallPlayers[0] + } + + if takenPlayer.ID != "" { + log.Printf("Dealer seeing call by %s", takenPlayer.ID) + g.CurrentRound.DealerSeeing = true + g.CurrentRound.CurrentHand.CurrentPlayerID = takenPlayer.ID + } else { + log.Printf("Call successful. %s is goer", caller.ID) + g.CurrentRound.Status = Called + g.CurrentRound.GoerID = caller.ID + g.CurrentRound.CurrentHand.CurrentPlayerID = caller.ID + } + + } else if g.CurrentRound.DealerSeeing { + log.Printf("%s was taken by the dealer.", playerID) + if call == Pass { + log.Printf("%s is letting the dealer go.", playerID) + g.CurrentRound.Status = Called + g.CurrentRound.GoerID = g.CurrentRound.DealerID + g.CurrentRound.CurrentHand.CurrentPlayerID = g.CurrentRound.DealerID + } else { + log.Printf("%s has raised the call.", playerID) + g.CurrentRound.CurrentHand.CurrentPlayerID = g.CurrentRound.DealerID + g.CurrentRound.DealerSeeing = false + } + } else { + log.Printf("Calling not complete. Next player...") + nextPlayer, err := nextPlayer(g.Players, playerID) + if err != nil { + return err + } + g.CurrentRound.CurrentHand.CurrentPlayerID = nextPlayer.ID + } + + // Increment revision + g.Revision++ + + return nil +} diff --git a/pkg/game/game-service.go b/pkg/game/game-service.go index 16298db..44ee5ad 100644 --- a/pkg/game/game-service.go +++ b/pkg/game/game-service.go @@ -11,6 +11,7 @@ import ( type ServiceI interface { Create(ctx context.Context, playerIDs []string, name string, adminID string) (Game, error) Get(ctx context.Context, gameId string) (Game, bool, error) + GetState(ctx context.Context, gameId string, playerId string) (State, bool, error) GetAll(ctx context.Context) ([]Game, error) Delete(ctx context.Context, gameId string, adminId string) error Call(ctx context.Context, gameId string, playerId string, call Call) (Game, error) @@ -53,6 +54,22 @@ func (s *Service) Get(ctx context.Context, gameId string) (Game, bool, error) { return s.Col.FindOne(ctx, bson.M{"_id": gameId}) } +func (s *Service) GetState(ctx context.Context, gameId string, playerId string) (State, bool, error) { + // Get the game from the database. + game, has, err := s.Get(ctx, gameId) + if err != nil || !has { + return State{}, has, err + } + + // Get the state for the player. + state, err := game.GetState(playerId) + if err != nil { + return State{}, true, err + } + + return state, true, nil +} + // GetAll Get all games. func (s *Service) GetAll(ctx context.Context) ([]Game, error) { return s.Col.Find(ctx, bson.M{}) diff --git a/pkg/game/game-service_test.go b/pkg/game/game-service_test.go index 8fb6b7f..ed60c69 100644 --- a/pkg/game/game-service_test.go +++ b/pkg/game/game-service_test.go @@ -331,3 +331,116 @@ func TestDelete(t *testing.T) { }) } } + +func TestCall(t *testing.T) { + ctx := context.Background() + + tests := []struct { + name string + gameID string + playerID string + call Call + mockGetResult *[]Game + mockGetExists *[]bool + mockGetError *[]error + mockUpdateOneError *[]error + expectingError bool + expectedRevision int + }{ + { + name: "simple call", + gameID: TwoPlayerGame().ID, + playerID: "2", + call: Jink, + mockGetResult: &[]Game{TwoPlayerGame()}, + mockGetExists: &[]bool{true}, + mockGetError: &[]error{nil}, + mockUpdateOneError: &[]error{nil}, + expectedRevision: 1, + }, + { + name: "game not found", + gameID: "1", + playerID: "2", + call: Jink, + mockGetResult: &[]Game{{}}, + mockGetExists: &[]bool{false}, + mockGetError: &[]error{nil}, + expectingError: true, + }, + { + name: "error thrown getting game", + gameID: "1", + playerID: "2", + call: Jink, + mockGetResult: &[]Game{{}}, + mockGetExists: &[]bool{false}, + mockGetError: &[]error{errors.New("something went wrong")}, + expectingError: true, + }, + { + name: "error thrown getting game - true exists", + gameID: "1", + playerID: "2", + call: Jink, + mockGetResult: &[]Game{{}}, + mockGetExists: &[]bool{true}, + mockGetError: &[]error{errors.New("something went wrong")}, + expectingError: true, + }, + { + name: "error thrown updating game", + gameID: TwoPlayerGame().ID, + playerID: "2", + call: Jink, + mockGetResult: &[]Game{TwoPlayerGame()}, + mockGetExists: &[]bool{true}, + mockGetError: &[]error{nil}, + mockUpdateOneError: &[]error{errors.New("something went wrong")}, + expectingError: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + mockCol := &db.MockCollection[Game]{ + MockFindOneResult: test.mockGetResult, + MockFindOneExists: test.mockGetExists, + MockFindOneErr: test.mockGetError, + MockUpdateOneErr: test.mockUpdateOneError, + } + + ds := &Service{ + Col: mockCol, + } + + game, err := ds.Call(ctx, test.gameID, test.playerID, test.call) + + if test.expectingError { + if err == nil { + t.Errorf("expected an error, got nil") + } + } else { + var player Player + for _, p := range game.Players { + if p.ID == test.playerID { + player = p + break + } + } + // Check call has been made + if player.ID == "" { + t.Errorf("Player not found") + } + + if player.Call != test.call { + t.Errorf("expected call %v, got %v", test.call, player.Call) + } + // Check revision has been incremented + if game.Revision != test.expectedRevision { + t.Errorf("expected revision %d, got %d", test.expectedRevision, game.Revision) + } + } + }) + } +} diff --git a/pkg/game/game.go b/pkg/game/game.go index 466d88c..640cf87 100644 --- a/pkg/game/game.go +++ b/pkg/game/game.go @@ -2,7 +2,6 @@ package game import ( "fmt" - "log" "time" ) @@ -88,6 +87,7 @@ type Round struct { type Game struct { ID string `bson:"_id,omitempty" json:"id"` + Revision int `bson:"revision" json:"revision"` AdminID string `bson:"adminId" json:"adminId"` Timestamp time.Time `bson:"timestamp" json:"timestamp"` Name string `bson:"name" json:"name"` @@ -98,8 +98,9 @@ type Game struct { Deck []CardName `bson:"deck" json:"-"` } -type GameState struct { +type State struct { ID string `json:"id"` + Revision int `bson:"revision" json:"revision"` Status Status `json:"status"` Me Player `json:"me"` IamSpectator bool `json:"iamSpectator"` @@ -112,244 +113,3 @@ type GameState struct { Round Round `json:"round"` Cards []CardName `json:"cards"` } - -func (g *Game) Me(playerID string) (Player, error) { - for _, p := range g.Players { - if p.ID == playerID { - return p, nil - } - } - return Player{}, fmt.Errorf("player not found in game") -} - -func (g *Game) GetState(playerID string) (GameState, error) { - // Get player - me, err := g.Me(playerID) - if err != nil { - return GameState{}, err - } - - // 1. Find dummy - var dummy Player - if g.CurrentRound.GoerID == playerID { - for _, p := range g.Players { - if p.ID == "dummy" { - dummy = p - break - } - } - } - - // 2. Get max call - maxCall := Pass - for _, p := range g.Players { - if p.Call > maxCall { - maxCall = p.Call - } - } - - // 3. Add dummy if applicable - iamGoer := g.CurrentRound.GoerID == playerID - if iamGoer && g.CurrentRound.Status == Called && dummy.ID != "" { - me.Cards = append(me.Cards, dummy.Cards...) - } - - // 4. Return player's game state - gameState := GameState{ - ID: g.ID, - Me: me, - IamSpectator: false, - IsMyGo: g.CurrentRound.CurrentHand.CurrentPlayerID == me.ID, - IamGoer: iamGoer, - IamDealer: g.CurrentRound.DealerID == me.ID, - IamAdmin: g.AdminID == me.ID, - Cards: me.Cards, - Status: g.Status, - Round: g.CurrentRound, - MaxCall: maxCall, - Players: make([]Player, 0), - } - - for _, p := range g.Players { - if p.ID != "dummy" { - gameState.Players = append(gameState.Players, p) - } - } - - return gameState, nil -} - -func (g *Game) EndRound() error { - // Add the current round to the completed rounds - g.Completed = append(g.Completed, g.CurrentRound) - - // Get next dealer - nextDealer, err := nextPlayer(g.Players, g.CurrentRound.DealerID) - if err != nil { - return err - } - - // Create next hand - nextPlayer, err := nextPlayer(g.Players, nextDealer.ID) - if err != nil { - return err - } - nextHand := Hand{ - Timestamp: time.Now(), - CurrentPlayerID: nextPlayer.ID, - } - - // Create a new round - g.CurrentRound = Round{ - Timestamp: time.Now(), - Number: g.CurrentRound.Number + 1, - DealerID: nextDealer.ID, - Status: Calling, - CurrentHand: nextHand, - } - - // Deal the cards - deck := ShuffleCards(NewDeck()) - g.Deck, g.Players = DealCards(deck, g.Players) - - return nil -} - -func (g *Game) Call(playerID string, call Call) error { - // Check the game is active - if g.Status != Active { - return fmt.Errorf("game not active") - } - - // Check current round is calling - if g.CurrentRound.Status != Calling { - return fmt.Errorf("round not calling") - } - - // Check the player is the current player - if g.CurrentRound.CurrentHand.CurrentPlayerID != playerID { - return fmt.Errorf("not current player") - } - - // If they are in the bunker (score < -30) they can only pass - state, err := g.GetState(playerID) - if err != nil { - return err - } - if state.Me.Score < -30 && call != Pass { - return fmt.Errorf("player in bunker") - } - - // Check the call is valid i.e. > all previous calls or a pass - // The dealer can take a call of greater than 10 - if call != Pass { - callForComparison := call - if state.IamDealer { - callForComparison++ - } - for _, p := range g.Players { - if p.Call >= callForComparison { - return fmt.Errorf("invalid call") - } - } - } - - // Validate 10 call - if call == Ten { - if len(g.Players) != 6 { - return fmt.Errorf("can only call 10 in doubles") - } - } - - // Set the player's call - for i, p := range g.Players { - if p.ID == playerID { - g.Players[i].Call = call - break - } - } - - // Set next player/round status - if call == Jink { - log.Printf("Jink called by %s", playerID) - if state.IamDealer { - // If the dealer calls Jink, calling is complete - g.CurrentRound.Status = Called - g.CurrentRound.GoerID = playerID - g.CurrentRound.CurrentHand.CurrentPlayerID = playerID - } else { - // If any other player calls Jink, jump to the dealer - g.CurrentRound.CurrentHand.CurrentPlayerID = g.CurrentRound.DealerID - } - } else if state.IamDealer { - // Get the highest calls - var topCall Call - for _, p := range g.Players { - if p.Call > topCall { - topCall = p.Call - } - } - - if topCall <= Ten { - log.Printf("No one called. Starting new round...") - err = g.EndRound() - return err - } - - // Get the players who made the top call (the dealer may have taken a call, this will result in more than one player) - var topCallPlayers []Player - for _, p := range g.Players { - if p.Call == topCall { - topCallPlayers = append(topCallPlayers, p) - } - } - if len(topCallPlayers) == 0 || len(topCallPlayers) > 2 { - return fmt.Errorf("invalid call state. There are %d top callers of %d", len(topCallPlayers), topCall) - } - var takenPlayer Player - var caller Player - if len(topCallPlayers) == 2 { - for _, p := range topCallPlayers { - if p.ID == g.CurrentRound.DealerID { - caller = p - } else { - takenPlayer = p - } - } - } else { - caller = topCallPlayers[0] - } - - if takenPlayer.ID != "" { - log.Printf("Dealer seeing call by %s", takenPlayer.ID) - g.CurrentRound.DealerSeeing = true - g.CurrentRound.CurrentHand.CurrentPlayerID = takenPlayer.ID - } else { - log.Printf("Call successful. %s is goer", caller.ID) - g.CurrentRound.Status = Called - g.CurrentRound.GoerID = caller.ID - g.CurrentRound.CurrentHand.CurrentPlayerID = caller.ID - } - - } else if g.CurrentRound.DealerSeeing { - log.Printf("%s was taken by the dealer.", playerID) - if call == Pass { - log.Printf("%s is letting the dealer go.", playerID) - g.CurrentRound.Status = Called - g.CurrentRound.GoerID = g.CurrentRound.DealerID - g.CurrentRound.CurrentHand.CurrentPlayerID = g.CurrentRound.DealerID - } else { - log.Printf("%s has raised the call.", playerID) - g.CurrentRound.CurrentHand.CurrentPlayerID = g.CurrentRound.DealerID - g.CurrentRound.DealerSeeing = false - } - } else { - log.Printf("Calling not complete. Next player...") - nextPlayer, err := nextPlayer(g.Players, playerID) - if err != nil { - return err - } - g.CurrentRound.CurrentHand.CurrentPlayerID = nextPlayer.ID - } - return nil -} diff --git a/pkg/game/game_test.go b/pkg/game/game_test.go index 9f3d29f..5950758 100644 --- a/pkg/game/game_test.go +++ b/pkg/game/game_test.go @@ -7,9 +7,10 @@ type CallWrap struct { call Call expectedNextPlayerID string expectingError bool + expectedRevision int } -func TestCall(t *testing.T) { +func TestCallMethod(t *testing.T) { tests := []struct { name string game Game @@ -23,33 +24,37 @@ func TestCall(t *testing.T) { playerID: "2", call: Fifteen, expectedNextPlayerID: "1", + expectedRevision: 1, }}, }, { name: "Completed game", game: CompletedGame(), calls: []CallWrap{{ - playerID: "2", - call: Fifteen, - expectingError: true, + playerID: "2", + call: Fifteen, + expectingError: true, + expectedRevision: 0, }}, }, { name: "Game in called state", game: CalledGame(), calls: []CallWrap{{ - playerID: "2", - call: Fifteen, - expectingError: true, + playerID: "2", + call: Fifteen, + expectingError: true, + expectedRevision: 0, }}, }, { name: "Invalid call 10 in a 2 player game", game: TwoPlayerGame(), calls: []CallWrap{{ - playerID: "2", - call: Ten, - expectingError: true, + playerID: "2", + call: Ten, + expectingError: true, + expectedRevision: 0, }}, }, { @@ -59,6 +64,7 @@ func TestCall(t *testing.T) { playerID: "5", call: Ten, expectedNextPlayerID: "6", + expectedRevision: 1, }}, }, { @@ -69,11 +75,13 @@ func TestCall(t *testing.T) { playerID: "2", call: Twenty, expectedNextPlayerID: "1", + expectedRevision: 1, }, { - playerID: "1", - call: Fifteen, - expectingError: true, + playerID: "1", + call: Fifteen, + expectingError: true, + expectedRevision: 1, }, }, }, @@ -85,11 +93,13 @@ func TestCall(t *testing.T) { playerID: "2", call: Twenty, expectedNextPlayerID: "1", + expectedRevision: 1, }, { playerID: "1", call: Twenty, expectedNextPlayerID: "2", + expectedRevision: 2, }, }, }, @@ -101,11 +111,13 @@ func TestCall(t *testing.T) { playerID: "2", call: Fifteen, expectedNextPlayerID: "1", + expectedRevision: 1, }, { playerID: "1", call: Twenty, expectedNextPlayerID: "1", + expectedRevision: 2, }, }, expectedGoerID: "1", @@ -118,11 +130,13 @@ func TestCall(t *testing.T) { playerID: "2", call: Pass, expectedNextPlayerID: "1", + expectedRevision: 1, }, { playerID: "1", call: Pass, expectedNextPlayerID: "1", + expectedRevision: 2, }, }, }, @@ -134,6 +148,7 @@ func TestCall(t *testing.T) { playerID: "5", call: Jink, expectedNextPlayerID: "1", + expectedRevision: 1, }, }, }, @@ -145,11 +160,13 @@ func TestCall(t *testing.T) { playerID: "5", call: Jink, expectedNextPlayerID: "1", + expectedRevision: 1, }, { playerID: "1", call: Jink, expectedNextPlayerID: "1", + expectedRevision: 2, }, }, expectedGoerID: "1", @@ -162,11 +179,13 @@ func TestCall(t *testing.T) { playerID: "5", call: Jink, expectedNextPlayerID: "1", + expectedRevision: 1, }, { playerID: "1", call: Pass, expectedNextPlayerID: "5", + expectedRevision: 2, }, }, expectedGoerID: "5", @@ -195,7 +214,12 @@ func TestCall(t *testing.T) { if test.game.CurrentRound.CurrentHand.CurrentPlayerID != call.expectedNextPlayerID { t.Errorf("expected player %s to be current player, got %s", call.expectedNextPlayerID, test.game.CurrentRound.CurrentHand.CurrentPlayerID) } + // Check revision has been incremented + if test.game.Revision != call.expectedRevision { + t.Errorf("expected revision %d, got %d", call.expectedRevision, test.game.Revision) + } } + } if test.expectedGoerID != "" { if test.game.CurrentRound.GoerID != test.expectedGoerID {