From 36dcd94043663773ea704ec4862cdf5dbd2b7477 Mon Sep 17 00:00:00 2001 From: Daithi Hearn Date: Thu, 18 Jan 2024 01:03:55 +0100 Subject: [PATCH] feat: select suit --- cmd/api/main.go | 1 + pkg/game/deck-utils.go | 12 +- pkg/game/deck.go | 124 ++++++++--------- pkg/game/game-handler.go | 60 +++++++- pkg/game/game-methods.go | 116 ++++++++++++---- pkg/game/game-service.go | 26 ++++ pkg/game/game-utils.go | 58 +++++++- pkg/game/game.go | 25 +--- pkg/game/game_test.go | 290 ++++++++++++++++++++++++++++++++++++++- pkg/game/testdata.go | 124 ++++++++++++++++- 10 files changed, 708 insertions(+), 128 deletions(-) diff --git a/cmd/api/main.go b/cmd/api/main.go index 0df2e61..c57154b 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -130,6 +130,7 @@ func main() { router.GET("/api/v1/game/:gameId", auth.EnsureValidTokenGin([]string{auth.ReadGame}), gameHandler.Get) router.GET("/api/v1/game/:gameId/state", auth.EnsureValidTokenGin([]string{auth.ReadGame}), gameHandler.GetState) router.PUT("/api/v1/game/:gameId/call", auth.EnsureValidTokenGin([]string{auth.WriteGame}), gameHandler.Call) + router.PUT("/api/v1/game/:gameId/suit", auth.EnsureValidTokenGin([]string{auth.WriteGame}), gameHandler.SelectSuit) router.GET("/api/v1/game/all", auth.EnsureValidTokenGin([]string{auth.ReadGame}), gameHandler.GetAll) router.PUT("/api/v1/game", auth.EnsureValidTokenGin([]string{auth.WriteAdmin}), gameHandler.Create) router.DELETE("/api/v1/game/:gameId", auth.EnsureValidTokenGin([]string{auth.WriteAdmin}), gameHandler.Delete) diff --git a/pkg/game/deck-utils.go b/pkg/game/deck-utils.go index edc013c..ec95856 100644 --- a/pkg/game/deck-utils.go +++ b/pkg/game/deck-utils.go @@ -15,16 +15,16 @@ func ShuffleCards(cards []CardName) []CardName { return shuffled } -func DealCards(cards []CardName, players []Player) ([]CardName, []Player) { +func DealCards(deck []CardName, numPlayers int) ([]CardName, [][]CardName) { + hands := make([][]CardName, numPlayers+1) // Deal the cards for i := 0; i < 5; i++ { - for j := 0; j < len(players); j++ { - players[j].Cards = append(players[j].Cards, cards[0]) - cards = cards[1:] + for j := 0; j < numPlayers+1; j++ { + hands[j] = append(hands[j], deck[0]) + deck = deck[1:] } } - - return cards, players + return deck, hands } func NewDeck() []CardName { diff --git a/pkg/game/deck.go b/pkg/game/deck.go index 444292f..b774858 100644 --- a/pkg/game/deck.go +++ b/pkg/game/deck.go @@ -4,12 +4,12 @@ package game type Suit string const ( - EMPTY Suit = "EMPTY" - CLUBS = "CLUBS" - DIAMONDS = "DIAMONDS" - HEARTS = "HEARTS" - SPADES = "SPADES" - WILD = "WILD" + Empty Suit = "EMPTY" + Clubs = "CLUBS" + Diamonds = "DIAMONDS" + Hearts = "HEARTS" + Spades = "SPADES" + Wild = "WILD" ) type CardName string @@ -73,7 +73,7 @@ const ( // Card represents a card with a value, coldValue, suit, and renegable status. type Card struct { - NAME CardName + Name CardName Value int ColdValue int Suit Suit @@ -82,62 +82,62 @@ type Card struct { // Define cards as constants. var ( - EmptyCard = Card{EMPTY_CARD, 0, 0, EMPTY, false} - TwoHearts = Card{NAME: TWO_HEARTS, Value: 2, ColdValue: 0, Suit: HEARTS, Renegable: false} - ThreeHearts = Card{THREE_HEARTS, 3, 0, HEARTS, false} - FourHearts = Card{FOUR_HEARTS, 4, 0, HEARTS, false} - SixHearts = Card{SIX_HEARTS, 6, 0, HEARTS, false} - SevenHearts = Card{SEVEN_HEARTS, 7, 0, HEARTS, false} - EightHearts = Card{EIGHT_HEARTS, 8, 0, HEARTS, false} - NineHearts = Card{NINE_HEARTS, 9, 0, HEARTS, false} - TenHearts = Card{TEN_HEARTS, 10, 0, HEARTS, false} - QueenHearts = Card{QUEEN_HEARTS, 12, 0, HEARTS, false} - KingHearts = Card{KING_HEARTS, 13, 0, HEARTS, false} - AceHearts = Card{ACE_HEARTS, 1, 0, HEARTS, false} - JackHearts = Card{JACK_HEARTS, 11, 0, HEARTS, true} - FiveHearts = Card{FIVE_HEARTS, 5, 0, HEARTS, true} - TwoDiamonds = Card{TWO_DIAMONDS, 2, 0, DIAMONDS, false} - ThreeDiamonds = Card{THREE_DIAMONDS, 3, 0, DIAMONDS, false} - FourDiamonds = Card{FOUR_DIAMONDS, 4, 0, DIAMONDS, false} - SixDiamonds = Card{SIX_DIAMONDS, 6, 0, DIAMONDS, false} - SevenDiamonds = Card{SEVEN_DIAMONDS, 7, 0, DIAMONDS, false} - EightDiamonds = Card{EIGHT_DIAMONDS, 8, 0, DIAMONDS, false} - NineDiamonds = Card{NINE_DIAMONDS, 9, 0, DIAMONDS, false} - TenDiamonds = Card{TEN_DIAMONDS, 10, 0, DIAMONDS, false} - QueenDiamonds = Card{QUEEN_DIAMONDS, 12, 0, DIAMONDS, false} - KingDiamonds = Card{KING_DIAMONDS, 13, 0, DIAMONDS, false} - AceDiamonds = Card{ACE_DIAMONDS, 1, 0, DIAMONDS, false} - JackDiamonds = Card{JACK_DIAMONDS, 11, 0, DIAMONDS, true} - FiveDiamonds = Card{FIVE_DIAMONDS, 5, 0, DIAMONDS, true} - TenClubs = Card{TEN_CLUBS, 10, 0, CLUBS, false} - NineClubs = Card{NINE_CLUBS, 9, 0, CLUBS, false} - EightClubs = Card{EIGHT_CLUBS, 8, 0, CLUBS, false} - SevenClubs = Card{SEVEN_CLUBS, 7, 0, CLUBS, false} - SixClubs = Card{SIX_CLUBS, 6, 0, CLUBS, false} - FourClubs = Card{FOUR_CLUBS, 4, 0, CLUBS, false} - ThreeClubs = Card{THREE_CLUBS, 3, 0, CLUBS, false} - TwoClubs = Card{TWO_CLUBS, 2, 0, CLUBS, false} - QueenClubs = Card{QUEEN_CLUBS, 12, 0, CLUBS, false} - KingClubs = Card{KING_CLUBS, 13, 0, CLUBS, false} - AceClubs = Card{ACE_CLUBS, 1, 0, CLUBS, false} - JackClubs = Card{JACK_CLUBS, 11, 0, CLUBS, true} - FiveClubs = Card{FIVE_CLUBS, 5, 0, CLUBS, true} - TenSpades = Card{TEN_SPADES, 10, 0, SPADES, false} - NineSpades = Card{NINE_SPADES, 9, 0, SPADES, false} - EightSpades = Card{EIGHT_SPADES, 8, 0, SPADES, false} - SevenSpades = Card{SEVEN_SPADES, 7, 0, SPADES, false} - SixSpades = Card{SIX_SPADES, 6, 0, SPADES, false} - FourSpades = Card{FOUR_SPADES, 4, 0, SPADES, false} - ThreeSpades = Card{THREE_SPADES, 3, 0, SPADES, false} - TwoSpades = Card{TWO_SPADES, 2, 0, SPADES, false} - QueenSpades = Card{QUEEN_SPADES, 12, 0, SPADES, false} - KingSpades = Card{KING_SPADES, 13, 0, SPADES, false} - AceSpades = Card{ACE_SPADES, 1, 0, SPADES, false} - JackSpades = Card{JACK_SPADES, 11, 0, SPADES, true} - FiveSpades = Card{FIVE_SPADES, 5, 0, SPADES, true} - Joker = Card{JOKER, 0, 0, WILD, true} + EmptyCard = Card{EMPTY_CARD, 0, 0, Empty, false} + TwoHearts = Card{Name: TWO_HEARTS, Value: 2, ColdValue: 0, Suit: Hearts, Renegable: false} + ThreeHearts = Card{THREE_HEARTS, 3, 0, Hearts, false} + FourHearts = Card{FOUR_HEARTS, 4, 0, Hearts, false} + SixHearts = Card{SIX_HEARTS, 6, 0, Hearts, false} + SevenHearts = Card{SEVEN_HEARTS, 7, 0, Hearts, false} + EightHearts = Card{EIGHT_HEARTS, 8, 0, Hearts, false} + NineHearts = Card{NINE_HEARTS, 9, 0, Hearts, false} + TenHearts = Card{TEN_HEARTS, 10, 0, Hearts, false} + QueenHearts = Card{QUEEN_HEARTS, 12, 0, Hearts, false} + KingHearts = Card{KING_HEARTS, 13, 0, Hearts, false} + AceHearts = Card{ACE_HEARTS, 1, 0, Hearts, false} + JackHearts = Card{JACK_HEARTS, 11, 0, Hearts, true} + FiveHearts = Card{FIVE_HEARTS, 5, 0, Hearts, true} + TwoDiamonds = Card{TWO_DIAMONDS, 2, 0, Diamonds, false} + ThreeDiamonds = Card{THREE_DIAMONDS, 3, 0, Diamonds, false} + FourDiamonds = Card{FOUR_DIAMONDS, 4, 0, Diamonds, false} + SixDiamonds = Card{SIX_DIAMONDS, 6, 0, Diamonds, false} + SevenDiamonds = Card{SEVEN_DIAMONDS, 7, 0, Diamonds, false} + EightDiamonds = Card{EIGHT_DIAMONDS, 8, 0, Diamonds, false} + NineDiamonds = Card{NINE_DIAMONDS, 9, 0, Diamonds, false} + TenDiamonds = Card{TEN_DIAMONDS, 10, 0, Diamonds, false} + QueenDiamonds = Card{QUEEN_DIAMONDS, 12, 0, Diamonds, false} + KingDiamonds = Card{KING_DIAMONDS, 13, 0, Diamonds, false} + AceDiamonds = Card{ACE_DIAMONDS, 1, 0, Diamonds, false} + JackDiamonds = Card{JACK_DIAMONDS, 11, 0, Diamonds, true} + FiveDiamonds = Card{FIVE_DIAMONDS, 5, 0, Diamonds, true} + TenClubs = Card{TEN_CLUBS, 10, 0, Clubs, false} + NineClubs = Card{NINE_CLUBS, 9, 0, Clubs, false} + EightClubs = Card{EIGHT_CLUBS, 8, 0, Clubs, false} + SevenClubs = Card{SEVEN_CLUBS, 7, 0, Clubs, false} + SixClubs = Card{SIX_CLUBS, 6, 0, Clubs, false} + FourClubs = Card{FOUR_CLUBS, 4, 0, Clubs, false} + ThreeClubs = Card{THREE_CLUBS, 3, 0, Clubs, false} + TwoClubs = Card{TWO_CLUBS, 2, 0, Clubs, false} + QueenClubs = Card{QUEEN_CLUBS, 12, 0, Clubs, false} + KingClubs = Card{KING_CLUBS, 13, 0, Clubs, false} + AceClubs = Card{ACE_CLUBS, 1, 0, Clubs, false} + JackClubs = Card{JACK_CLUBS, 11, 0, Clubs, true} + FiveClubs = Card{FIVE_CLUBS, 5, 0, Clubs, true} + TenSpades = Card{TEN_SPADES, 10, 0, Spades, false} + NineSpades = Card{NINE_SPADES, 9, 0, Spades, false} + EightSpades = Card{EIGHT_SPADES, 8, 0, Spades, false} + SevenSpades = Card{SEVEN_SPADES, 7, 0, Spades, false} + SixSpades = Card{SIX_SPADES, 6, 0, Spades, false} + FourSpades = Card{FOUR_SPADES, 4, 0, Spades, false} + ThreeSpades = Card{THREE_SPADES, 3, 0, Spades, false} + TwoSpades = Card{TWO_SPADES, 2, 0, Spades, false} + QueenSpades = Card{QUEEN_SPADES, 12, 0, Spades, false} + KingSpades = Card{KING_SPADES, 13, 0, Spades, false} + AceSpades = Card{ACE_SPADES, 1, 0, Spades, false} + JackSpades = Card{JACK_SPADES, 11, 0, Spades, true} + FiveSpades = Card{FIVE_SPADES, 5, 0, Spades, true} + Joker = Card{JOKER, 0, 0, Wild, true} ) func (c Card) String() string { - return string(c.NAME) + return string(c.Name) } diff --git a/pkg/game/game-handler.go b/pkg/game/game-handler.go index 98d6256..118bbc9 100644 --- a/pkg/game/game-handler.go +++ b/pkg/game/game-handler.go @@ -224,7 +224,7 @@ func (h *Handler) Delete(c *gin.Context) { // @Security Bearer // @Param gameId path string true "Game ID" // @Param call query int true "Call" -// @Success 200 {object} Game +// @Success 200 {object} State // @Failure 400 {object} api.ErrorResponse // @Failure 500 {object} api.ErrorResponse // @Router /game/{gameId}/call [put] @@ -261,6 +261,62 @@ func (h *Handler) Call(c *gin.Context) { c.JSON(http.StatusInternalServerError, api.ErrorResponse{Message: err.Error()}) return } + state, err := game.GetState(id) + if err != nil { + c.JSON(http.StatusInternalServerError, api.ErrorResponse{Message: err.Error()}) + return + } + c.IndentedJSON(http.StatusOK, state) +} - c.IndentedJSON(http.StatusOK, game) +type SelectSuitRequest struct { + Suit Suit `json:"suit"` + Cards []CardName `json:"cards"` +} + +// SelectSuit @Summary Select the suit +// @Description When in the Called state, the Goer can select the suit and what cards they want to keep from their hand and the dummy hand +// @Tags Game +// @ID select-suit +// @Produce json +// @Security Bearer +// @Param gameId path string true "Game ID" +// @Para body SelectSuitRequest true "Select Suit Request" +// @Success 200 {object} State +// @Failure 400 {object} api.ErrorResponse +// @Failure 500 {object} api.ErrorResponse +// @Router /game/{gameId}/suit [put] +func (h *Handler) SelectSuit(c *gin.Context) { + // Check the user is correctly authenticated + id, ok := auth.CheckValidated(c) + if !ok { + return + } + + // Get the context from the request + ctx := c.Request.Context() + + // Get the game ID from the request + gameId := c.Param("gameId") + + // Get the request body + var req SelectSuitRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, api.ErrorResponse{Message: err.Error()}) + return + } + + // Select the suit + game, err := h.S.SelectSuit(ctx, gameId, id, req.Suit, req.Cards) + + if err != nil { + c.JSON(http.StatusInternalServerError, api.ErrorResponse{Message: err.Error()}) + return + } + state, err := game.GetState(id) + if err != nil { + c.JSON(http.StatusInternalServerError, api.ErrorResponse{Message: err.Error()}) + return + } + c.IndentedJSON(http.StatusOK, state) } diff --git a/pkg/game/game-methods.go b/pkg/game/game-methods.go index cffd158..5c7e295 100644 --- a/pkg/game/game-methods.go +++ b/pkg/game/game-methods.go @@ -22,18 +22,7 @@ func (g *Game) GetState(playerID string) (State, error) { 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 + // 1. Get max call maxCall := Pass for _, p := range g.Players { if p.Call > maxCall { @@ -41,13 +30,13 @@ func (g *Game) GetState(playerID string) (State, error) { } } - // 3. Add dummy if applicable + // 2. Add dummy if applicable iamGoer := g.CurrentRound.GoerID == playerID - if iamGoer && g.CurrentRound.Status == Called && dummy.ID != "" { - me.Cards = append(me.Cards, dummy.Cards...) + if iamGoer && g.CurrentRound.Status == Called && g.Dummy != nil { + me.Cards = append(me.Cards, g.Dummy...) } - // 4. Return player's game state + // 3. Return player's game state gameState := State{ ID: g.ID, Revision: g.Revision, @@ -61,13 +50,7 @@ func (g *Game) GetState(playerID string) (State, error) { 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) - } + Players: g.Players, } return gameState, nil @@ -103,8 +86,17 @@ func (g *Game) EndRound() error { } // Deal the cards - deck := ShuffleCards(NewDeck()) - g.Deck, g.Players = DealCards(deck, g.Players) + deck, hands := DealCards(ShuffleCards(NewDeck()), len(g.Players)) + var dummy []CardName + for i, hand := range hands { + if i >= len(g.Players) { + dummy = hand + break + } + g.Players[i].Cards = hand + } + g.Dummy = dummy + g.Deck = deck // Increment revision g.Revision++ @@ -254,3 +246,77 @@ func (g *Game) Call(playerID string, call Call) error { return nil } + +// MinKeep returns the minimum number of cards that must be kept by a player +func (g *Game) MinKeep() (int, error) { + switch len(g.Players) { + case 2: + return 0, nil + case 3: + return 0, nil + case 4: + return 0, nil + case 5: + return 1, nil + case 6: + return 2, nil + } + return 0, fmt.Errorf("invalid number of players") +} + +func (g *Game) SelectSuit(playerID string, suit Suit, cards []CardName) error { + // Verify the at the round is in the called state + if g.CurrentRound.Status != Called { + return fmt.Errorf("round must be in the called state to select a suit") + } + + // Verify that the player is the goer + if g.CurrentRound.GoerID != playerID { + return fmt.Errorf("only the goer can select the suit") + } + + // Verify the number of cards selected is valid (<=5 and >= minKeep) + minKeep, err := g.MinKeep() + if err != nil { + return err + } + if len(cards) > 5 || len(cards) < minKeep { + return fmt.Errorf("invalid number of cards selected") + } + + // Verify the cards are valid (must be either in the player's hand or the dummy's hand and must be unique + state, err := g.GetState(playerID) + if err != nil { + return err + } + if !containsAllUnique(state.Cards, cards) { + return fmt.Errorf("invalid card selected") + } + + // Update the round + g.CurrentRound.Status = Buying + g.CurrentRound.Suit = suit + + // Set my cards + for i, p := range g.Players { + if p.ID == playerID { + g.Players[i].Cards = cards + break + } + } + + // Remove the dummy player + g.Dummy = nil + + // Set the next player + np, err := nextPlayer(g.Players, playerID) + if err != nil { + return err + } + g.CurrentRound.CurrentHand.CurrentPlayerID = np.ID + + // Increment revision + g.Revision++ + + return nil +} diff --git a/pkg/game/game-service.go b/pkg/game/game-service.go index 44ee5ad..7459c5b 100644 --- a/pkg/game/game-service.go +++ b/pkg/game/game-service.go @@ -15,6 +15,7 @@ type ServiceI interface { 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) + SelectSuit(ctx context.Context, id string, id2 string, suit Suit, cards []CardName) (Game, error) } type Service struct { @@ -124,3 +125,28 @@ func (s *Service) Call(ctx context.Context, gameId string, playerId string, call } return game, nil } + +// SelectSuit select a suit +func (s *Service) SelectSuit(ctx context.Context, gameId string, playerID string, suit Suit, cards []CardName) (Game, error) { + // Get the game from the database. + game, has, err := s.Get(ctx, gameId) + if err != nil { + return Game{}, err + } + if !has { + return Game{}, errors.New("game not found") + } + + // Select the suit. + err = game.SelectSuit(playerID, suit, cards) + if err != nil { + return Game{}, err + } + + // Save the game to the database. + err = s.Col.UpdateOne(ctx, game, game.ID) + if err != nil { + return Game{}, err + } + return game, nil +} diff --git a/pkg/game/game-utils.go b/pkg/game/game-utils.go index e50628c..56e6ce5 100644 --- a/pkg/game/game-utils.go +++ b/pkg/game/game-utils.go @@ -17,6 +17,25 @@ func validateNumberOfPlayers(playerIDs []string) error { return nil } +func ParseCall(c string) (Call, error) { + switch c { + case "0": + return Pass, nil + case "10": + return Ten, nil + case "15": + return Fifteen, nil + case "20": + return Twenty, nil + case "25": + return TwentyFive, nil + case "30": + return Jink, nil + default: + return 0, fmt.Errorf("invalid call") + } +} + // shuffle a slice of strings func shuffle(input []string) []string { shuffled := make([]string, len(input)) @@ -72,9 +91,6 @@ func nextPlayer(players []Player, currentPlayerId string) (Player, error) { } nextIndex := (currentIndex + 1) % len(players) - if players[nextIndex].ID == "dummy" { - return nextPlayer(players, players[nextIndex].ID) - } return players[nextIndex], nil } @@ -127,8 +143,15 @@ func NewGame(playerIDs []string, name string, adminID string) (Game, error) { round, err := createFirstRound(players, dealer) // Deal the cards - deck := ShuffleCards(NewDeck()) - deck, players = DealCards(deck, players) + deck, hands := DealCards(ShuffleCards(NewDeck()), len(players)) + var dummy []CardName + for i, hand := range hands { + if i >= len(players) { + dummy = hand + break + } + players[i].Cards = hand + } // Create the game game := Game{ @@ -138,9 +161,34 @@ func NewGame(playerIDs []string, name string, adminID string) (Game, error) { Status: Active, AdminID: adminID, Players: players, + Dummy: dummy, CurrentRound: round, Deck: deck, } return game, nil } + +// containsAllUnique checks if targetSlice contains all unique elements of referenceSlice. +func containsAllUnique(referenceSlice, targetSlice []CardName) bool { + if len(targetSlice) > len(referenceSlice) { + return false + } + + existsInReferenceSlice := make(map[CardName]bool) + for _, item := range referenceSlice { + existsInReferenceSlice[item] = true + } + + seenInTargetSlice := make(map[CardName]bool) + for _, item := range targetSlice { + if _, ok := existsInReferenceSlice[item]; !ok { + return false // Element in targetSlice is not in referenceSlice + } + if _, seen := seenInTargetSlice[item]; seen { + return false // Element is not unique in targetSlice + } + seenInTargetSlice[item] = true + } + return true +} diff --git a/pkg/game/game.go b/pkg/game/game.go index 640cf87..258c917 100644 --- a/pkg/game/game.go +++ b/pkg/game/game.go @@ -1,7 +1,6 @@ package game import ( - "fmt" "time" ) @@ -32,23 +31,6 @@ const ( Jink = 30 ) -func ParseCall(c string) (Call, error) { - switch c { - case "10": - return Ten, nil - case "15": - return Fifteen, nil - case "20": - return Twenty, nil - case "25": - return TwentyFive, nil - case "30": - return Jink, nil - default: - return 0, fmt.Errorf("invalid call") - } -} - type Player struct { ID string `bson:"_id,omitempty" json:"id"` Seat int `bson:"seatNumber" json:"seatNumber"` @@ -68,7 +50,7 @@ type PlayedCard struct { type Hand struct { Timestamp time.Time `bson:"timestamp" json:"timestamp"` - LeadOut CardName `bson:"leadOut" json:"leadOut"` + LeadOut CardName `bson:"leadOut" json:"leadOut,omitempty"` CurrentPlayerID string `bson:"currentPlayerId" json:"currentPlayerId"` PlayedCards []PlayedCard `bson:"playedCards" json:"playedCards"` } @@ -77,8 +59,8 @@ type Round struct { Timestamp time.Time `bson:"timestamp" json:"timestamp"` Number int `bson:"number" json:"number"` DealerID string `bson:"dealerId" json:"dealerId"` - GoerID string `bson:"goerId" json:"goerId"` - Suit Suit `bson:"suit" json:"suit"` + GoerID string `bson:"goerId" json:"goerId,omitempty"` + Suit Suit `bson:"suit" json:"suit,omitempty"` Status RoundStatus `bson:"status" json:"status"` CurrentHand Hand `bson:"currentHand" json:"currentHand"` DealerSeeing bool `bson:"dealerSeeingCall" json:"dealerSeeingCall"` @@ -93,6 +75,7 @@ type Game struct { Name string `bson:"name" json:"name"` Status Status `bson:"status" json:"status"` Players []Player `bson:"players" json:"players"` + Dummy []CardName `bson:"dummy" json:"dummy"` CurrentRound Round `bson:"currentRound" json:"currentRound"` Completed []Round `bson:"completedRounds" json:"completedRounds"` Deck []CardName `bson:"deck" json:"-"` diff --git a/pkg/game/game_test.go b/pkg/game/game_test.go index 5950758..e01c41f 100644 --- a/pkg/game/game_test.go +++ b/pkg/game/game_test.go @@ -10,7 +10,7 @@ type CallWrap struct { expectedRevision int } -func TestCallMethod(t *testing.T) { +func TestGame_Call(t *testing.T) { tests := []struct { name string game Game @@ -190,6 +190,36 @@ func TestCallMethod(t *testing.T) { }, expectedGoerID: "5", }, + { + name: "Dealer takes caller, caller increases bet and dealer passes", + game: TwoPlayerGame(), + calls: []CallWrap{ + { + playerID: "2", + call: Fifteen, + expectedNextPlayerID: "1", + expectedRevision: 1, + }, + { + playerID: "1", + call: Fifteen, + expectedNextPlayerID: "2", + expectedRevision: 2, + }, + { + playerID: "2", + call: Twenty, + expectedNextPlayerID: "1", + expectedRevision: 3, + }, + { + playerID: "1", + call: Pass, + expectedNextPlayerID: "2", + expectedRevision: 4, + }, + }, + }, } for _, test := range tests { @@ -230,3 +260,261 @@ func TestCallMethod(t *testing.T) { }) } } + +func TestGame_MinKeep(t *testing.T) { + tests := []struct { + name string + game Game + expectedMinKeep int + errorExpected bool + }{ + { + name: "2 player game", + game: TwoPlayerGame(), + expectedMinKeep: 0, + }, + { + name: "3 player game", + game: ThreePlayerGame(), + expectedMinKeep: 0, + }, + { + name: "4 player game", + game: FourPlayerGame(), + expectedMinKeep: 0, + }, + { + name: "5 player game", + game: FivePlayerGame(), + expectedMinKeep: 1, + }, + { + name: "6 player game", + game: SixPlayerGame(), + expectedMinKeep: 2, + }, + { + name: "Invalid number of players - 1", + game: OnePlayerGame(), + errorExpected: true, + }, + { + name: "Invalid number of players - 7", + game: SevenPlayerGame(), + errorExpected: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + minKeep, err := test.game.MinKeep() + if test.errorExpected { + if err == nil { + t.Errorf("expected an error, got nil") + } + } else { + if err != nil { + t.Errorf("expected no error, got %v", err) + } + if minKeep != test.expectedMinKeep { + t.Errorf("expected minKeep to be %d, got %d", test.expectedMinKeep, minKeep) + } + } + }) + } +} + +func TestGame_SelectSuit(t *testing.T) { + tests := []struct { + name string + game Game + playerID string + suit Suit + cards []CardName + expectedSuit Suit + expectedStatus RoundStatus + expectedRevision int + expectingError bool + }{ + { + name: "Valid selection - keep 1 from my hand and 1 from dummy", + game: CalledGame(), + playerID: "2", + suit: Diamonds, + cards: []CardName{ACE_DIAMONDS, KING_SPADES}, + expectedSuit: Diamonds, + expectedStatus: Buying, + expectedRevision: 1, + }, + { + name: "Valid selection - not keeping any cards", + game: CalledGame(), + playerID: "2", + suit: Diamonds, + cards: []CardName{}, + expectedSuit: Diamonds, + expectedStatus: Buying, + expectedRevision: 1, + }, + { + name: "Valid selection - keep 5 cards", + game: CalledGame(), + playerID: "2", + suit: Diamonds, + cards: []CardName{ACE_DIAMONDS, KING_SPADES, QUEEN_SPADES, JACK_SPADES, JOKER}, + expectedSuit: Diamonds, + expectedStatus: Buying, + expectedRevision: 1, + }, + { + name: "Invalid player - not the goer", + game: CalledGame(), + playerID: "1", + suit: Hearts, + cards: []CardName{ACE_HEARTS}, + expectedSuit: Hearts, + expectedStatus: Buying, + expectingError: true, + }, + { + name: "Invalid state", + game: TwoPlayerGame(), + playerID: "1", + suit: Hearts, + cards: []CardName{ACE_HEARTS}, + expectedSuit: Hearts, + expectedStatus: Buying, + expectingError: true, + }, + { + name: "Invalid number of cards", + game: CalledGame(), + playerID: "2", + suit: Hearts, + cards: []CardName{ACE_SPADES, KING_SPADES, QUEEN_SPADES, JACK_SPADES, JOKER, ACE_DIAMONDS}, + expectedSuit: Hearts, + expectedStatus: Buying, + expectingError: true, + }, + { + name: "Invalid card - not in hand", + game: CalledGame(), + playerID: "2", + suit: Hearts, + cards: []CardName{FIVE_CLUBS}, + expectedSuit: Hearts, + expectedStatus: Buying, + expectingError: true, + }, + { + name: "Duplicate card", + game: CalledGame(), + playerID: "2", + suit: Hearts, + cards: []CardName{ACE_DIAMONDS, ACE_DIAMONDS}, + expectedSuit: Hearts, + expectedStatus: Buying, + expectingError: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := test.game.SelectSuit(test.playerID, test.suit, test.cards) + if test.expectingError { + if err == nil { + t.Errorf("expected an error, got nil") + } + } else { + if err != nil { + t.Errorf("expected no error, got %v", err) + } + if test.game.CurrentRound.Status != test.expectedStatus { + t.Errorf("expected round status to be %s, got %s", test.expectedStatus, test.game.CurrentRound.Status) + } + if test.game.CurrentRound.Suit != test.expectedSuit { + t.Errorf("expected suit to be %s, got %s", test.expectedSuit, test.game.CurrentRound.Suit) + } + if test.game.Revision != test.expectedRevision { + t.Errorf("expected revision to be %d, got %d", test.expectedRevision, test.game.Revision) + } + // Check that he has all of retained the cards he selected + state, err := test.game.GetState(test.playerID) + if err != nil { + t.Errorf("expected no error, got %v", err) + } + if !containsAllUnique(state.Cards, test.cards) { + t.Errorf("expected player to have all of the selected cards %v, got %v", test.cards, state.Cards) + } + } + }) + } +} + +func TestGame_ParseCall(t *testing.T) { + tests := []struct { + name string + callStr string + expectedCall Call + expectingError bool + }{ + { + name: "Valid call - 0", + callStr: "0", + expectedCall: Pass, + }, + { + name: "Valid call - 10", + callStr: "10", + expectedCall: Ten, + }, + { + name: "Valid call - 15", + callStr: "15", + expectedCall: Fifteen, + }, + { + name: "Valid call - 20", + callStr: "20", + expectedCall: Twenty, + }, + { + name: "Valid call - 25", + callStr: "25", + expectedCall: TwentyFive, + }, + { + name: "Valid call - 30", + callStr: "30", + expectedCall: Jink, + }, + { + name: "Invalid call - 5", + callStr: "5", + expectingError: true, + }, + { + name: "Invalid call - 35", + callStr: "35", + expectingError: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + call, err := ParseCall(test.callStr) + if test.expectingError { + if err == nil { + t.Errorf("expected an error, got nil") + } + } else { + if err != nil { + t.Errorf("expected no error, got %v", err) + } + if call != test.expectedCall { + t.Errorf("expected call to be %d, got %d", test.expectedCall, call) + } + } + }) + } +} diff --git a/pkg/game/testdata.go b/pkg/game/testdata.go index d677854..435c88f 100644 --- a/pkg/game/testdata.go +++ b/pkg/game/testdata.go @@ -6,12 +6,16 @@ import ( "time" ) +func Dummy() []CardName { + return []CardName{ACE_SPADES, KING_SPADES, QUEEN_SPADES, JACK_SPADES, JOKER} +} + func Player1() Player { return Player{ ID: "1", Seat: 1, Call: 0, - Cards: []CardName{}, + Cards: []CardName{ACE_HEARTS, KING_HEARTS, QUEEN_HEARTS, JACK_HEARTS, TEN_HEARTS}, Bought: 0, } } @@ -21,7 +25,7 @@ func Player2() Player { ID: "2", Seat: 2, Call: 0, - Cards: []CardName{}, + Cards: []CardName{ACE_DIAMONDS, KING_DIAMONDS, QUEEN_DIAMONDS, JACK_DIAMONDS, TEN_DIAMONDS}, Bought: 0, } } @@ -31,7 +35,7 @@ func Player3() Player { ID: "3", Seat: 3, Call: 0, - Cards: []CardName{}, + Cards: []CardName{ACE_CLUBS, KING_CLUBS, QUEEN_CLUBS, JACK_CLUBS, TEN_CLUBS}, Bought: 0, } } @@ -41,7 +45,7 @@ func Player4() Player { ID: "4", Seat: 4, Call: 0, - Cards: []CardName{}, + Cards: []CardName{NINE_CLUBS, EIGHT_CLUBS, SEVEN_CLUBS, SIX_CLUBS, FIVE_CLUBS}, Bought: 0, } } @@ -51,7 +55,7 @@ func Player5() Player { ID: "5", Seat: 5, Call: 0, - Cards: []CardName{}, + Cards: []CardName{NINE_DIAMONDS, EIGHT_DIAMONDS, SEVEN_DIAMONDS, SIX_DIAMONDS, FIVE_DIAMONDS}, Bought: 0, } } @@ -61,11 +65,40 @@ func Player6() Player { ID: "6", Seat: 6, Call: 0, - Cards: []CardName{}, + Cards: []CardName{NINE_HEARTS, EIGHT_HEARTS, SEVEN_HEARTS, SIX_HEARTS, FIVE_HEARTS}, Bought: 0, } } +func Player7() Player { + return Player{ + ID: "7", + Seat: 7, + Call: 0, + Cards: []CardName{NINE_SPADES, EIGHT_SPADES, SEVEN_SPADES, SIX_SPADES, FIVE_SPADES}, + Bought: 0, + } +} + +func OnePlayerGame() Game { + return Game{ + ID: "1", + Name: "Test Game", + Status: Active, + Timestamp: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC), + Players: []Player{Player1()}, + Dummy: Dummy(), + CurrentRound: Round{ + DealerID: "1", + Status: Calling, + CurrentHand: Hand{ + CurrentPlayerID: "1", + }, + }, + AdminID: "1", + } +} + func TwoPlayerGame() Game { return Game{ ID: "1", @@ -73,6 +106,64 @@ func TwoPlayerGame() Game { Status: Active, Timestamp: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC), Players: []Player{Player1(), Player2()}, + Dummy: Dummy(), + CurrentRound: Round{ + DealerID: "1", + Status: Calling, + CurrentHand: Hand{ + CurrentPlayerID: "2", + }, + }, + AdminID: "1", + } +} + +func ThreePlayerGame() Game { + return Game{ + ID: "1", + Name: "Test Game", + Status: Active, + Timestamp: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC), + Players: []Player{Player1(), Player2(), Player3()}, + Dummy: Dummy(), + CurrentRound: Round{ + DealerID: "1", + Status: Calling, + CurrentHand: Hand{ + CurrentPlayerID: "2", + }, + }, + AdminID: "1", + } +} + +func FourPlayerGame() Game { + return Game{ + ID: "1", + Name: "Test Game", + Status: Active, + Timestamp: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC), + Players: []Player{Player1(), Player2(), Player3(), Player4()}, + Dummy: Dummy(), + CurrentRound: Round{ + DealerID: "1", + Status: Calling, + CurrentHand: Hand{ + CurrentPlayerID: "2", + }, + }, + AdminID: "1", + } +} + +func FivePlayerGame() Game { + return Game{ + ID: "1", + Name: "Test Game", + Status: Active, + Timestamp: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC), + Players: []Player{Player1(), Player2(), Player3(), Player4(), Player5()}, + Dummy: Dummy(), CurrentRound: Round{ DealerID: "1", Status: Calling, @@ -91,6 +182,26 @@ func SixPlayerGame() Game { Status: Active, Timestamp: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC), Players: []Player{Player1(), Player2(), Player3(), Player4(), Player5(), Player6()}, + Dummy: Dummy(), + CurrentRound: Round{ + DealerID: "1", + Status: Calling, + CurrentHand: Hand{ + CurrentPlayerID: "5", + }, + }, + AdminID: "1", + } +} + +func SevenPlayerGame() Game { + return Game{ + ID: "1", + Name: "Test Game", + Status: Active, + Timestamp: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC), + Players: []Player{Player1(), Player2(), Player3(), Player4(), Player5(), Player6(), Player7()}, + Dummy: Dummy(), CurrentRound: Round{ DealerID: "1", Status: Calling, @@ -109,6 +220,7 @@ func CalledGame() Game { Status: Active, Timestamp: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC), Players: []Player{Player1(), Player2()}, + Dummy: Dummy(), CurrentRound: Round{ DealerID: "1", GoerID: "2",