diff --git a/pkg/game/deck.go b/pkg/game/deck.go index 509d474..c16c708 100644 --- a/pkg/game/deck.go +++ b/pkg/game/deck.go @@ -5,13 +5,17 @@ type Suit string const ( Empty Suit = "EMPTY" - Clubs = "Clubs" - Diamonds = "Diamonds" - Hearts = "Hearts" - Spades = "Spades" + Clubs = "CLUBS" + Diamonds = "DIAMONDS" + Hearts = "HEARTS" + Spades = "SPADES" Wild = "WILD" ) +func (s Suit) isValid() bool { + return s == Clubs || s == Diamonds || s == Hearts || s == Spades || s == Wild +} + type CardName string const ( diff --git a/pkg/game/game-methods.go b/pkg/game/game-methods.go index 071b827..32b13e8 100644 --- a/pkg/game/game-methods.go +++ b/pkg/game/game-methods.go @@ -80,6 +80,10 @@ func (g *Game) completeHand() error { return err } + log.Printf("Winning card: %s", winningCard.Card) + log.Printf("Current hand: %v", g.CurrentRound.CurrentHand) + log.Printf("Current suit: %s", g.CurrentRound.Suit) + // 2. Add the hand to the completed hands g.CurrentRound.CompletedHands = append(g.CurrentRound.CompletedHands, g.CurrentRound.CurrentHand) @@ -397,6 +401,11 @@ func (g *Game) Call(playerID string, call Call) error { } func (g *Game) SelectSuit(playerID string, suit Suit, cards []CardName) error { + // Validate suit + if !suit.isValid() { + return fmt.Errorf("invalid suit") + } + // Validate the caller err := g.validateCaller(playerID, Called) if err != nil { diff --git a/pkg/game/game-service.go b/pkg/game/game-service.go index 4b81fcb..1f74deb 100644 --- a/pkg/game/game-service.go +++ b/pkg/game/game-service.go @@ -31,6 +31,21 @@ func getCacheKey(gameId string, playerId string) string { return gameId + "-" + playerId } +func (s *Service) updateStateCache(game Game) error { + // Update the state cache for all players in the game. + for _, player := range game.Players { + state, err := game.GetState(player.ID) + if err != nil { + return err + } + errC := s.Cache.Set(getCacheKey(game.ID, player.ID), state, time.Minute) + if errC != nil { + return errC + } + } + return nil +} + // Create a new game. func (s *Service) Create(ctx context.Context, playerIDs []string, name string, adminID string) (Game, error) { log.Printf("Creating new game (%s)", name) @@ -77,16 +92,15 @@ func (s *Service) GetState(ctx context.Context, gameId string, playerID string) return State{}, has, errG } - // Get the state for the player. state, err = game.GetState(playerID) if err != nil { return State{}, true, err } - // Save the state to the cache. - err = s.Cache.Set(getCacheKey(gameId, playerID), state, 10*time.Minute) - if err != nil { - log.Printf("Failed to save state to cache: %s", err) + // Update the state cache for all players in the game. + errC := s.updateStateCache(game) + if errC != nil { + return State{}, true, errC } return state, true, nil @@ -145,15 +159,10 @@ func (s *Service) Call(ctx context.Context, gameId string, playerID string, call return Game{}, err } - // Save the state to the cache. - state, err := game.GetState(playerID) - if err != nil { - return Game{}, err - } - - err = s.Cache.Set(getCacheKey(gameId, playerID), state, 10*time.Minute) - if err != nil { - log.Printf("Failed to save state to cache: %s", err) + // Update the state cache for all players in the game. + errC := s.updateStateCache(game) + if errC != nil { + return Game{}, errC } return game, nil @@ -182,15 +191,10 @@ func (s *Service) SelectSuit(ctx context.Context, gameId string, playerID string return Game{}, err } - // Save the state to the cache. - state, err := game.GetState(playerID) - if err != nil { - return Game{}, err - } - - err = s.Cache.Set(getCacheKey(gameId, playerID), state, 10*time.Minute) - if err != nil { - log.Printf("Failed to save state to cache: %s", err) + // Update the state cache for all players in the game. + errC := s.updateStateCache(game) + if errC != nil { + return Game{}, errC } return game, nil @@ -219,15 +223,10 @@ func (s *Service) Buy(ctx context.Context, gameId string, playerID string, cards return Game{}, err } - // Save the state to the cache. - state, err := game.GetState(playerID) - if err != nil { - return Game{}, err - } - - err = s.Cache.Set(getCacheKey(gameId, playerID), state, 10*time.Minute) - if err != nil { - log.Printf("Failed to save state to cache: %s", err) + // Update the state cache for all players in the game. + errC := s.updateStateCache(game) + if errC != nil { + return Game{}, errC } return game, nil @@ -256,15 +255,10 @@ func (s *Service) Play(ctx context.Context, gameId string, playerID string, card return Game{}, err } - // Save the state to the cache. - state, err := game.GetState(playerID) - if err != nil { - return Game{}, err - } - - err = s.Cache.Set(getCacheKey(gameId, playerID), state, 10*time.Minute) - if err != nil { - log.Printf("Failed to save state to cache: %s", err) + // Update the state cache for all players in the game. + errC := s.updateStateCache(game) + if errC != nil { + return Game{}, errC } return game, nil diff --git a/pkg/game/game-service_test.go b/pkg/game/game-service_test.go index d398254..2c9876f 100644 --- a/pkg/game/game-service_test.go +++ b/pkg/game/game-service_test.go @@ -372,7 +372,7 @@ func TestGameService_GetState(t *testing.T) { expectedExists: true, }, { - name: "error writing to cache shouldn't stop the function from returning the result", + name: "error writing to cache should return an error", gameID: TwoPlayerGame().ID, playerID: "2", mockGetResult: &[]Game{TwoPlayerGame()}, @@ -398,7 +398,7 @@ func TestGameService_GetState(t *testing.T) { Players: TwoPlayerGame().Players, }, expectedExists: true, - expectingError: false, + expectingError: true, }, } @@ -609,7 +609,7 @@ func TestGameService_Call(t *testing.T) { mockGetError: &[]error{nil}, mockUpdateOneError: &[]error{nil}, mockSetCacheError: &[]error{nil}, - expectedRevision: 1, + expectedRevision: 3, }, { name: "game not found", @@ -663,7 +663,7 @@ func TestGameService_Call(t *testing.T) { expectingError: true, }, { - name: "error writing to cache shouldn't stop the function from returning the result", + name: "error writing to cache should return an error", gameID: TwoPlayerGame().ID, playerID: "2", call: Jink, @@ -672,7 +672,8 @@ func TestGameService_Call(t *testing.T) { mockGetError: &[]error{nil}, mockUpdateOneError: &[]error{nil}, mockSetCacheError: &[]error{errors.New("failed to write to cache")}, - expectedRevision: 1, + expectedRevision: 2, + expectingError: true, }, } @@ -807,7 +808,7 @@ func TestGameService_SelectSuit(t *testing.T) { expectingError: true, }, { - name: "error writing to cache shouldn't stop the function from returning the result", + name: "error writing to cache should return an error", gameID: CalledGameFivePlayers().ID, playerID: "PlayerCalled", suit: Hearts, @@ -818,6 +819,7 @@ func TestGameService_SelectSuit(t *testing.T) { mockUpdateOneError: &[]error{nil}, mockSetCacheError: &[]error{errors.New("failed to write to cache")}, expectedRevision: 1, + expectingError: true, }, } @@ -936,7 +938,7 @@ func TestGameService_Buy(t *testing.T) { expectingError: true, }, { - name: "error writing to cache shouldn't stop the function from returning the result", + name: "error writing to cache should return an error", gameID: "1", playerID: "2", cards: []CardName{SEVEN_HEARTS, EIGHT_HEARTS, NINE_HEARTS}, @@ -945,7 +947,7 @@ func TestGameService_Buy(t *testing.T) { mockGetError: &[]error{nil}, mockUpdateOneError: &[]error{nil}, mockSetCacheError: &[]error{errors.New("failed to write to cache")}, - expectedRevision: 1, + expectingError: true, }, } @@ -1068,7 +1070,7 @@ func TestGameService_Play(t *testing.T) { expectingError: true, }, { - name: "error writing to cache shouldn't stop the function from returning the result", + name: "error writing to cache should return an error", gameID: "1", playerID: "1", card: TWO_HEARTS, @@ -1077,7 +1079,7 @@ func TestGameService_Play(t *testing.T) { mockGetError: &[]error{nil}, mockUpdateOneError: &[]error{nil}, mockSetCacheError: &[]error{errors.New("failed to write to cache")}, - expectedRevision: 1, + expectingError: true, }, } diff --git a/pkg/game/game-utils.go b/pkg/game/game-utils.go index c3bd7d0..3eba370 100644 --- a/pkg/game/game-utils.go +++ b/pkg/game/game-utils.go @@ -282,6 +282,9 @@ func findWinningCard(hand Hand, suit Suit) (PlayedCard, error) { if len(hand.PlayedCards) == 0 { return PlayedCard{}, errors.New("no cards played") } + if !suit.isValid() { + return PlayedCard{}, errors.New("invalid suit") + } // Get active suit activeSuit, err := getActiveSuit(hand, suit) diff --git a/pkg/game/game-utils_test.go b/pkg/game/game-utils_test.go index dcb5927..74b9185 100644 --- a/pkg/game/game-utils_test.go +++ b/pkg/game/game-utils_test.go @@ -460,28 +460,36 @@ func TestGameUtils_findWinningCard(t *testing.T) { hand: Hand{LeadOut: JACK_HEARTS, PlayedCards: []PlayedCard{{Card: JACK_HEARTS, PlayerID: "1"}, {Card: FIVE_HEARTS, PlayerID: "2"}, {Card: ACE_SPADES, PlayerID: "3"}}}, suit: Diamonds, expectedResult: PlayedCard{Card: JACK_HEARTS, PlayerID: "1"}, - expectingError: false, }, { name: "Trump cards played", hand: Hand{LeadOut: JACK_HEARTS, PlayedCards: []PlayedCard{{Card: JACK_HEARTS, PlayerID: "1"}, {Card: FIVE_HEARTS, PlayerID: "2"}, {Card: ACE_HEARTS, PlayerID: "3"}}}, suit: Hearts, expectedResult: PlayedCard{Card: FIVE_HEARTS, PlayerID: "2"}, - expectingError: false, }, { name: "Trump cards played - Joker", hand: Hand{LeadOut: JACK_CLUBS, PlayedCards: []PlayedCard{{Card: JACK_CLUBS, PlayerID: "1"}, {Card: SIX_CLUBS, PlayerID: "2"}, {Card: JOKER, PlayerID: "3"}}}, suit: Hearts, expectedResult: PlayedCard{Card: JOKER, PlayerID: "3"}, - expectingError: false, }, { name: "Trump cards played - Ace of hearts", hand: Hand{LeadOut: JACK_CLUBS, PlayedCards: []PlayedCard{{Card: JACK_CLUBS, PlayerID: "1"}, {Card: SIX_CLUBS, PlayerID: "2"}, {Card: ACE_HEARTS, PlayerID: "3"}}}, suit: Hearts, expectedResult: PlayedCard{Card: ACE_HEARTS, PlayerID: "3"}, - expectingError: false, + }, + { + name: "Ten of trumps beats jack of cold suit", + hand: Hand{LeadOut: JACK_SPADES, PlayedCards: []PlayedCard{{Card: JACK_SPADES, PlayerID: "1"}, {Card: TEN_DIAMONDS, PlayerID: "2"}}}, + suit: Diamonds, + expectedResult: PlayedCard{Card: TEN_DIAMONDS, PlayerID: "2"}, + }, + { + name: "real world scenario", + hand: Hand{LeadOut: ACE_SPADES, PlayedCards: []PlayedCard{{Card: ACE_SPADES, PlayerID: "2"}, {Card: FIVE_SPADES, PlayerID: "1"}}}, + suit: "SPADES", + expectedResult: PlayedCard{Card: FIVE_SPADES, PlayerID: "1"}, }, } diff --git a/pkg/game/game_test.go b/pkg/game/game_test.go index 22ac241..8f89367 100644 --- a/pkg/game/game_test.go +++ b/pkg/game/game_test.go @@ -710,7 +710,7 @@ func TestGame_Call(t *testing.T) { playerID: "2", call: Fifteen, expectedNextPlayerID: "1", - expectedRevision: 1, + expectedRevision: 3, }}, }, { @@ -761,13 +761,13 @@ func TestGame_Call(t *testing.T) { playerID: "2", call: Twenty, expectedNextPlayerID: "1", - expectedRevision: 1, + expectedRevision: 3, }, { playerID: "1", call: Fifteen, expectingError: true, - expectedRevision: 1, + expectedRevision: 3, }, }, }, @@ -779,13 +779,13 @@ func TestGame_Call(t *testing.T) { playerID: "2", call: Twenty, expectedNextPlayerID: "1", - expectedRevision: 1, + expectedRevision: 3, }, { playerID: "1", call: Twenty, expectedNextPlayerID: "2", - expectedRevision: 2, + expectedRevision: 4, }, }, }, @@ -797,13 +797,13 @@ func TestGame_Call(t *testing.T) { playerID: "2", call: Fifteen, expectedNextPlayerID: "1", - expectedRevision: 1, + expectedRevision: 3, }, { playerID: "1", call: Twenty, expectedNextPlayerID: "1", - expectedRevision: 2, + expectedRevision: 4, }, }, expectedGoerID: "1", @@ -816,13 +816,13 @@ func TestGame_Call(t *testing.T) { playerID: "2", call: Pass, expectedNextPlayerID: "1", - expectedRevision: 1, + expectedRevision: 3, }, { playerID: "1", call: Pass, expectedNextPlayerID: "1", - expectedRevision: 2, + expectedRevision: 4, }, }, }, @@ -884,25 +884,25 @@ func TestGame_Call(t *testing.T) { playerID: "2", call: Fifteen, expectedNextPlayerID: "1", - expectedRevision: 1, + expectedRevision: 3, }, { playerID: "1", call: Fifteen, expectedNextPlayerID: "2", - expectedRevision: 2, + expectedRevision: 4, }, { playerID: "2", call: Twenty, expectedNextPlayerID: "1", - expectedRevision: 3, + expectedRevision: 5, }, { playerID: "1", call: Pass, expectedNextPlayerID: "2", - expectedRevision: 4, + expectedRevision: 6, }, }, }, @@ -1032,6 +1032,14 @@ func TestGame_SelectSuit(t *testing.T) { cards: []CardName{JOKER, JOKER}, expectingError: true, }, + { + name: "Invalid suit", + game: CalledGameFivePlayers(), + playerID: "PlayerCalled", + suit: "invalid", + cards: []CardName{JOKER}, + expectingError: true, + }, } for _, test := range tests { @@ -1133,7 +1141,7 @@ func TestGame_Buy(t *testing.T) { cards: []CardName{ACE_HEARTS}, expectedStatus: Buying, expectingError: true, - expectedRevision: 0, + expectedRevision: 2, }, { name: "Invalid number of cards", @@ -1229,13 +1237,29 @@ func TestGame_Play(t *testing.T) { expectingError: true, }, { - name: "Following suit", + name: "Player 1 - wins trick", game: PlayingGame_RoundStart_FirstCardPlayed(), playerID: "2", card: THREE_CLUBS, expectedStatus: Playing, expectedNextPlayer: "1", }, + { + name: "Player 1 - wins trick - different seating arrangement", + game: PlayingGame_WinHand(), + playerID: "player2", + card: THREE_SPADES, + expectedStatus: Playing, + expectedNextPlayer: "player1", + }, + { + name: "Player 2 - wins trick", + game: PlayingGame_RoundStart_FirstCardPlayed(), + playerID: "2", + card: FIVE_CLUBS, + expectedStatus: Playing, + expectedNextPlayer: "2", + }, { name: "Not following suit", game: PlayingGame_RoundStart_FirstCardPlayed(), @@ -1271,6 +1295,10 @@ func TestGame_Play(t *testing.T) { if contains(state.Cards, test.card) { t.Errorf("expected player to not have played card %s, got %v", test.card, state.Cards) } + + if state.Round.CurrentHand.CurrentPlayerID != test.expectedNextPlayer { + t.Errorf("expected next player to be %s, got %s", test.expectedNextPlayer, state.Round.CurrentHand.CurrentPlayerID) + } } }) } diff --git a/pkg/game/testdata.go b/pkg/game/testdata.go index 5512af6..2214e8f 100644 --- a/pkg/game/testdata.go +++ b/pkg/game/testdata.go @@ -120,6 +120,7 @@ func OnePlayerGame() Game { func TwoPlayerGame() Game { return Game{ ID: "1", + Revision: 2, Name: "Test Game", Status: Active, Timestamp: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC), @@ -316,7 +317,7 @@ func PlayingGame_RoundStart_FirstCardPlayed() Game { p1 := Player1() p1.Cards = []CardName{KING_CLUBS, QUEEN_HEARTS, JACK_SPADES, TEN_DIAMONDS} p2 := Player2() - p2.Cards = []CardName{TWO_HEARTS, THREE_CLUBS, FOUR_DIAMONDS, FIVE_SPADES, SIX_HEARTS} + p2.Cards = []CardName{TWO_HEARTS, THREE_CLUBS, FOUR_DIAMONDS, FIVE_CLUBS, ACE_HEARTS} return Game{ ID: "1", @@ -624,6 +625,80 @@ func PlayingGame_DoesntMakeContract_Doubles() Game { } } +func PlayingGame_WinHand() Game { + return Game{ + ID: "2809635", + Revision: 166, + Status: "ACTIVE", + Players: []Player{ + { + ID: "player1", + Seat: 1, + Call: Fifteen, + Bought: 0, + Score: 90, + Rings: 2, + TeamID: "1", + Winner: false, + Cards: []CardName{ + JACK_SPADES, + THREE_HEARTS, + QUEEN_CLUBS, + }, + }, + { + ID: "player2", + Seat: 2, + Call: Pass, + Bought: 0, + Score: 50, + Rings: 2, + TeamID: "2", + Winner: false, + Cards: []CardName{ + EIGHT_CLUBS, + JACK_DIAMONDS, + TEN_DIAMONDS, + THREE_SPADES, + }, + }, + }, + CurrentRound: Round{ + Number: 13, + DealerID: "player1", + GoerID: "player1", + Suit: Spades, + Status: Playing, + CurrentHand: Hand{ + LeadOut: FIVE_SPADES, + CurrentPlayerID: "player2", + PlayedCards: []PlayedCard{ + { + PlayerID: "player1", + Card: FIVE_SPADES, + }, + }, + }, + DealerSeeing: false, + CompletedHands: []Hand{ + { + LeadOut: TEN_SPADES, + CurrentPlayerID: "player1", + PlayedCards: []PlayedCard{ + { + PlayerID: "player2", + Card: TEN_SPADES, + }, + { + PlayerID: "player1", + Card: QUEEN_SPADES, + }, + }, + }}, + }, + } +} + func PlayingGame_Thirty() Game { game := PlayingGame_Jink() game.Players[2].Call = TwentyFive