From b771d9cd2d7bdf6800474517ef80f84ab720424a Mon Sep 17 00:00:00 2001 From: Daithi Hearn Date: Tue, 23 Jan 2024 00:17:31 +0100 Subject: [PATCH] feat: initial checkin of play card functionality --- cmd/api/main.go | 1 + pkg/game/deck-utils.go | 226 +++++++ pkg/game/deck-utils_test.go | 637 +++++++++++++++++++ pkg/game/deck.go | 120 ++-- pkg/game/game-handler.go | 53 ++ pkg/game/game-methods.go | 239 ++++++++ pkg/game/game-service.go | 30 +- pkg/game/game-utils.go | 304 +++++++++- pkg/game/game-utils_test.go | 1141 ++++++++++++++++++++++++++++++++++- 9 files changed, 2689 insertions(+), 62 deletions(-) diff --git a/cmd/api/main.go b/cmd/api/main.go index 8e6b45e..cd69bc1 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -132,6 +132,7 @@ func main() { 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.PUT("/api/v1/game/:gameId/buy", auth.EnsureValidTokenGin([]string{auth.WriteGame}), gameHandler.Buy) + router.PUT("/api/v1/game/:gameId/play", auth.EnsureValidTokenGin([]string{auth.WriteGame}), gameHandler.Play) 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 9be0416..bb5e969 100644 --- a/pkg/game/deck-utils.go +++ b/pkg/game/deck-utils.go @@ -103,3 +103,229 @@ func NewDeck() []CardName { JOKER, } } + +func ParseCardName(s string) (CardName, error) { + switch s { + case "TWO_HEARTS": + return TWO_HEARTS, nil + case "THREE_HEARTS": + return THREE_HEARTS, nil + case "FOUR_HEARTS": + return FOUR_HEARTS, nil + case "SIX_HEARTS": + return SIX_HEARTS, nil + case "SEVEN_HEARTS": + return SEVEN_HEARTS, nil + case "EIGHT_HEARTS": + return EIGHT_HEARTS, nil + case "NINE_HEARTS": + return NINE_HEARTS, nil + case "TEN_HEARTS": + return TEN_HEARTS, nil + case "QUEEN_HEARTS": + return QUEEN_HEARTS, nil + case "KING_HEARTS": + return KING_HEARTS, nil + case "ACE_HEARTS": + return ACE_HEARTS, nil + case "JACK_HEARTS": + return JACK_HEARTS, nil + case "FIVE_HEARTS": + return FIVE_HEARTS, nil + case "TWO_DIAMONDS": + return TWO_DIAMONDS, nil + case "THREE_DIAMONDS": + return THREE_DIAMONDS, nil + case "FOUR_DIAMONDS": + return FOUR_DIAMONDS, nil + case "SIX_DIAMONDS": + return SIX_DIAMONDS, nil + case "SEVEN_DIAMONDS": + return SEVEN_DIAMONDS, nil + case "EIGHT_DIAMONDS": + return EIGHT_DIAMONDS, nil + case "NINE_DIAMONDS": + return NINE_DIAMONDS, nil + case "TEN_DIAMONDS": + return TEN_DIAMONDS, nil + case "QUEEN_DIAMONDS": + return QUEEN_DIAMONDS, nil + case "KING_DIAMONDS": + return KING_DIAMONDS, nil + case "ACE_DIAMONDS": + return ACE_DIAMONDS, nil + case "JACK_DIAMONDS": + return JACK_DIAMONDS, nil + case "FIVE_DIAMONDS": + return FIVE_DIAMONDS, nil + case "TEN_CLUBS": + return TEN_CLUBS, nil + case "NINE_CLUBS": + return NINE_CLUBS, nil + case "EIGHT_CLUBS": + return EIGHT_CLUBS, nil + case "SEVEN_CLUBS": + return SEVEN_CLUBS, nil + case "SIX_CLUBS": + return SIX_CLUBS, nil + case "FOUR_CLUBS": + return FOUR_CLUBS, nil + case "THREE_CLUBS": + return THREE_CLUBS, nil + case "TWO_CLUBS": + return TWO_CLUBS, nil + case "QUEEN_CLUBS": + return QUEEN_CLUBS, nil + case "KING_CLUBS": + return KING_CLUBS, nil + case "ACE_CLUBS": + return ACE_CLUBS, nil + case "JACK_CLUBS": + return JACK_CLUBS, nil + case "FIVE_CLUBS": + return FIVE_CLUBS, nil + case "TEN_SPADES": + return TEN_SPADES, nil + case "NINE_SPADES": + return NINE_SPADES, nil + case "EIGHT_SPADES": + return EIGHT_SPADES, nil + case "SEVEN_SPADES": + return SEVEN_SPADES, nil + case "SIX_SPADES": + return SIX_SPADES, nil + case "FOUR_SPADES": + return FOUR_SPADES, nil + case "THREE_SPADES": + return THREE_SPADES, nil + case "TWO_SPADES": + return TWO_SPADES, nil + case "QUEEN_SPADES": + return QUEEN_SPADES, nil + case "KING_SPADES": + return KING_SPADES, nil + case "ACE_SPADES": + return ACE_SPADES, nil + case "JACK_SPADES": + return JACK_SPADES, nil + case "FIVE_SPADES": + return FIVE_SPADES, nil + case "JOKER": + return JOKER, nil + default: + return EMPTY_CARD, fmt.Errorf("invalid card name") + } +} + +func ParseCard(c CardName) Card { + switch c { + case TWO_HEARTS: + return TwoHearts + case THREE_HEARTS: + return ThreeHearts + case FOUR_HEARTS: + return FourHearts + case FIVE_HEARTS: + return FiveHearts + case SIX_HEARTS: + return SixHearts + case SEVEN_HEARTS: + return SevenHearts + case EIGHT_HEARTS: + return EightHearts + case NINE_HEARTS: + return NineHearts + case TEN_HEARTS: + return TenHearts + case JACK_HEARTS: + return JackHearts + case QUEEN_HEARTS: + return QueenHearts + case KING_HEARTS: + return KingHearts + case ACE_HEARTS: + return AceHearts + case TWO_DIAMONDS: + return TwoDiamonds + case THREE_DIAMONDS: + return ThreeDiamonds + case FOUR_DIAMONDS: + return FourDiamonds + case FIVE_DIAMONDS: + return FiveDiamonds + case SIX_DIAMONDS: + return SixDiamonds + case SEVEN_DIAMONDS: + return SevenDiamonds + case EIGHT_DIAMONDS: + return EightDiamonds + case NINE_DIAMONDS: + return NineDiamonds + case TEN_DIAMONDS: + return TenDiamonds + case JACK_DIAMONDS: + return JackDiamonds + case QUEEN_DIAMONDS: + return QueenDiamonds + case KING_DIAMONDS: + return KingDiamonds + case ACE_DIAMONDS: + return AceDiamonds + case TWO_CLUBS: + return TwoClubs + case THREE_CLUBS: + return ThreeClubs + case FOUR_CLUBS: + return FourClubs + case FIVE_CLUBS: + return FiveClubs + case SIX_CLUBS: + return SixClubs + case SEVEN_CLUBS: + return SevenClubs + case EIGHT_CLUBS: + return EightClubs + case NINE_CLUBS: + return NineClubs + case TEN_CLUBS: + return TenClubs + case JACK_CLUBS: + return JackClubs + case QUEEN_CLUBS: + return QueenClubs + case KING_CLUBS: + return KingClubs + case ACE_CLUBS: + return AceClubs + case TWO_SPADES: + return TwoSpades + case THREE_SPADES: + return ThreeSpades + case FOUR_SPADES: + return FourSpades + case FIVE_SPADES: + return FiveSpades + case SIX_SPADES: + return SixSpades + case SEVEN_SPADES: + return SevenSpades + case EIGHT_SPADES: + return EightSpades + case NINE_SPADES: + return NineSpades + case TEN_SPADES: + return TenSpades + case JACK_SPADES: + return JackSpades + case QUEEN_SPADES: + return QueenSpades + case KING_SPADES: + return KingSpades + case ACE_SPADES: + return AceSpades + case JOKER: + return Joker + default: + return EmptyCard + } +} diff --git a/pkg/game/deck-utils_test.go b/pkg/game/deck-utils_test.go index a48ce80..729423e 100644 --- a/pkg/game/deck-utils_test.go +++ b/pkg/game/deck-utils_test.go @@ -174,3 +174,640 @@ func TestDeck_NewDeck(t *testing.T) { t.Errorf("expected 53 cards, got %d", len(deck)) } } + +func TestDeck_ParseCardName(t *testing.T) { + tests := []struct { + name string + cardName string + expectError bool + expected CardName + }{ + { + name: "Empty string", + cardName: "", + expectError: true, + }, + { + name: "Invalid card name", + cardName: "INVALID", + expectError: true, + }, + { + name: "ACE_HEARTS", + cardName: "ACE_HEARTS", + expectError: false, + expected: ACE_HEARTS, + }, + { + name: "TWO_HEARTS", + cardName: "TWO_HEARTS", + expectError: false, + expected: TWO_HEARTS, + }, + { + name: "THREE_HEARTS", + cardName: "THREE_HEARTS", + expectError: false, + expected: THREE_HEARTS, + }, + { + name: "FOUR_HEARTS", + cardName: "FOUR_HEARTS", + expectError: false, + expected: FOUR_HEARTS, + }, + { + name: "FIVE_HEARTS", + cardName: "FIVE_HEARTS", + expectError: false, + expected: FIVE_HEARTS, + }, + { + name: "SIX_HEARTS", + cardName: "SIX_HEARTS", + expectError: false, + expected: SIX_HEARTS, + }, + { + name: "SEVEN_HEARTS", + cardName: "SEVEN_HEARTS", + expectError: false, + expected: SEVEN_HEARTS, + }, + { + name: "EIGHT_HEARTS", + cardName: "EIGHT_HEARTS", + expectError: false, + expected: EIGHT_HEARTS, + }, + { + name: "NINE_HEARTS", + cardName: "NINE_HEARTS", + expectError: false, + expected: NINE_HEARTS, + }, + { + name: "TEN_HEARTS", + cardName: "TEN_HEARTS", + expectError: false, + expected: TEN_HEARTS, + }, + { + name: "JACK_HEARTS", + cardName: "JACK_HEARTS", + expectError: false, + expected: JACK_HEARTS, + }, + { + name: "QUEEN_HEARTS", + cardName: "QUEEN_HEARTS", + expectError: false, + expected: QUEEN_HEARTS, + }, + { + name: "KING_HEARTS", + cardName: "KING_HEARTS", + expectError: false, + expected: KING_HEARTS, + }, + { + name: "ACE_DIAMONDS", + cardName: "ACE_DIAMONDS", + expectError: false, + expected: ACE_DIAMONDS, + }, + { + name: "TWO_DIAMONDS", + cardName: "TWO_DIAMONDS", + expectError: false, + expected: TWO_DIAMONDS, + }, + { + name: "THREE_DIAMONDS", + cardName: "THREE_DIAMONDS", + expectError: false, + expected: THREE_DIAMONDS, + }, + { + name: "FOUR_DIAMONDS", + cardName: "FOUR_DIAMONDS", + expectError: false, + expected: FOUR_DIAMONDS, + }, + { + name: "FIVE_DIAMONDS", + cardName: "FIVE_DIAMONDS", + expectError: false, + expected: FIVE_DIAMONDS, + }, + { + name: "SIX_DIAMONDS", + cardName: "SIX_DIAMONDS", + expectError: false, + expected: SIX_DIAMONDS, + }, + { + name: "SEVEN_DIAMONDS", + cardName: "SEVEN_DIAMONDS", + expectError: false, + expected: SEVEN_DIAMONDS, + }, + { + name: "EIGHT_DIAMONDS", + cardName: "EIGHT_DIAMONDS", + expectError: false, + expected: EIGHT_DIAMONDS, + }, + { + name: "NINE_DIAMONDS", + cardName: "NINE_DIAMONDS", + expectError: false, + expected: NINE_DIAMONDS, + }, + { + name: "TEN_DIAMONDS", + cardName: "TEN_DIAMONDS", + expectError: false, + expected: TEN_DIAMONDS, + }, + { + name: "JACK_DIAMONDS", + cardName: "JACK_DIAMONDS", + expectError: false, + expected: JACK_DIAMONDS, + }, + { + name: "QUEEN_DIAMONDS", + cardName: "QUEEN_DIAMONDS", + expectError: false, + expected: QUEEN_DIAMONDS, + }, + { + name: "KING_DIAMONDS", + cardName: "KING_DIAMONDS", + expectError: false, + expected: KING_DIAMONDS, + }, + { + name: "ACE_CLUBS", + cardName: "ACE_CLUBS", + expectError: false, + expected: ACE_CLUBS, + }, + { + name: "TWO_CLUBS", + cardName: "TWO_CLUBS", + expectError: false, + expected: TWO_CLUBS, + }, + { + name: "THREE_CLUBS", + cardName: "THREE_CLUBS", + expectError: false, + expected: THREE_CLUBS, + }, + { + name: "FOUR_CLUBS", + cardName: "FOUR_CLUBS", + expectError: false, + expected: FOUR_CLUBS, + }, + { + name: "FIVE_CLUBS", + cardName: "FIVE_CLUBS", + expectError: false, + expected: FIVE_CLUBS, + }, + { + name: "SIX_CLUBS", + cardName: "SIX_CLUBS", + expectError: false, + expected: SIX_CLUBS, + }, + { + name: "SEVEN_CLUBS", + cardName: "SEVEN_CLUBS", + expectError: false, + expected: SEVEN_CLUBS, + }, + { + name: "EIGHT_CLUBS", + cardName: "EIGHT_CLUBS", + expectError: false, + expected: EIGHT_CLUBS, + }, + { + name: "NINE_CLUBS", + cardName: "NINE_CLUBS", + expectError: false, + expected: NINE_CLUBS, + }, + { + name: "TEN_CLUBS", + cardName: "TEN_CLUBS", + expectError: false, + expected: TEN_CLUBS, + }, + { + name: "JACK_CLUBS", + cardName: "JACK_CLUBS", + expectError: false, + expected: JACK_CLUBS, + }, + { + name: "QUEEN_CLUBS", + cardName: "QUEEN_CLUBS", + expectError: false, + expected: QUEEN_CLUBS, + }, + { + name: "KING_CLUBS", + cardName: "KING_CLUBS", + expectError: false, + expected: KING_CLUBS, + }, + { + name: "ACE_SPADES", + cardName: "ACE_SPADES", + expectError: false, + expected: ACE_SPADES, + }, + { + name: "TWO_SPADES", + cardName: "TWO_SPADES", + expectError: false, + expected: TWO_SPADES, + }, + { + name: "THREE_SPADES", + cardName: "THREE_SPADES", + expectError: false, + expected: THREE_SPADES, + }, + { + name: "FOUR_SPADES", + cardName: "FOUR_SPADES", + expectError: false, + expected: FOUR_SPADES, + }, + { + name: "FIVE_SPADES", + cardName: "FIVE_SPADES", + expectError: false, + expected: FIVE_SPADES, + }, + { + name: "SIX_SPADES", + cardName: "SIX_SPADES", + expectError: false, + expected: SIX_SPADES, + }, + { + name: "SEVEN_SPADES", + cardName: "SEVEN_SPADES", + expectError: false, + expected: SEVEN_SPADES, + }, + { + name: "EIGHT_SPADES", + cardName: "EIGHT_SPADES", + expectError: false, + expected: EIGHT_SPADES, + }, + { + name: "NINE_SPADES", + cardName: "NINE_SPADES", + expectError: false, + expected: NINE_SPADES, + }, + { + name: "TEN_SPADES", + cardName: "TEN_SPADES", + expectError: false, + expected: TEN_SPADES, + }, + { + name: "JACK_SPADES", + cardName: "JACK_SPADES", + expectError: false, + expected: JACK_SPADES, + }, + { + name: "QUEEN_SPADES", + cardName: "QUEEN_SPADES", + expectError: false, + expected: QUEEN_SPADES, + }, + { + name: "KING_SPADES", + cardName: "KING_SPADES", + expectError: false, + expected: KING_SPADES, + }, + { + name: "JOKER", + cardName: "JOKER", + expectError: false, + expected: JOKER, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + cardName, err := ParseCardName(test.cardName) + + if test.expectError { + if err == nil { + t.Errorf("expected an error") + } + } else { + if err != nil { + t.Errorf("expected no error, got %v", err) + } + + if cardName != test.expected { + t.Errorf("expected card name to be %s, got %s", test.expected, cardName) + } + } + }) + } +} + +func TestDeckUtils_parseCard(t *testing.T) { + tests := []struct { + name string + cardName CardName + expected Card + }{ + { + name: "ACE_HEARTS", + cardName: ACE_HEARTS, + expected: AceHearts, + }, + { + name: "TWO_HEARTS", + cardName: TWO_HEARTS, + expected: TwoHearts, + }, + { + name: "THREE_HEARTS", + cardName: THREE_HEARTS, + expected: ThreeHearts, + }, + { + name: "FOUR_HEARTS", + cardName: FOUR_HEARTS, + expected: FourHearts, + }, + { + name: "FIVE_HEARTS", + cardName: FIVE_HEARTS, + expected: FiveHearts, + }, + { + name: "SIX_HEARTS", + cardName: SIX_HEARTS, + expected: SixHearts, + }, + { + name: "SEVEN_HEARTS", + cardName: SEVEN_HEARTS, + expected: SevenHearts, + }, + { + name: "EIGHT_HEARTS", + cardName: EIGHT_HEARTS, + expected: EightHearts, + }, + { + name: "NINE_HEARTS", + cardName: NINE_HEARTS, + expected: NineHearts, + }, + { + name: "TEN_HEARTS", + cardName: TEN_HEARTS, + expected: TenHearts, + }, + { + name: "JACK_HEARTS", + cardName: JACK_HEARTS, + expected: JackHearts, + }, + { + name: "QUEEN_HEARTS", + cardName: QUEEN_HEARTS, + expected: QueenHearts, + }, + { + name: "KING_HEARTS", + cardName: KING_HEARTS, + expected: KingHearts, + }, + { + name: "ACE_DIAMONDS", + cardName: ACE_DIAMONDS, + expected: AceDiamonds, + }, + { + name: "TWO_DIAMONDS", + cardName: TWO_DIAMONDS, + expected: TwoDiamonds, + }, + { + name: "THREE_DIAMONDS", + cardName: THREE_DIAMONDS, + expected: ThreeDiamonds, + }, + { + name: "FOUR_DIAMONDS", + cardName: FOUR_DIAMONDS, + expected: FourDiamonds, + }, + { + name: "FIVE_DIAMONDS", + cardName: FIVE_DIAMONDS, + expected: FiveDiamonds, + }, + { + name: "SIX_DIAMONDS", + cardName: SIX_DIAMONDS, + expected: SixDiamonds, + }, + { + name: "SEVEN_DIAMONDS", + cardName: SEVEN_DIAMONDS, + expected: SevenDiamonds, + }, + { + name: "EIGHT_DIAMONDS", + cardName: EIGHT_DIAMONDS, + expected: EightDiamonds, + }, + { + name: "NINE_DIAMONDS", + cardName: NINE_DIAMONDS, + expected: NineDiamonds, + }, + { + name: "TEN_DIAMONDS", + cardName: TEN_DIAMONDS, + expected: TenDiamonds, + }, + { + name: "JACK_DIAMONDS", + cardName: JACK_DIAMONDS, + expected: JackDiamonds, + }, + { + name: "QUEEN_DIAMONDS", + cardName: QUEEN_DIAMONDS, + expected: QueenDiamonds, + }, + { + name: "KING_DIAMONDS", + cardName: KING_DIAMONDS, + expected: KingDiamonds, + }, + { + name: "ACE_CLUBS", + cardName: ACE_CLUBS, + expected: AceClubs, + }, + { + name: "TWO_CLUBS", + cardName: TWO_CLUBS, + expected: TwoClubs, + }, + { + name: "THREE_CLUBS", + cardName: THREE_CLUBS, + expected: ThreeClubs, + }, + { + name: "FOUR_CLUBS", + cardName: FOUR_CLUBS, + expected: FourClubs, + }, + { + name: "FIVE_CLUBS", + cardName: FIVE_CLUBS, + expected: FiveClubs, + }, + { + name: "SIX_CLUBS", + cardName: SIX_CLUBS, + expected: SixClubs, + }, + { + name: "SEVEN_CLUBS", + cardName: SEVEN_CLUBS, + expected: SevenClubs, + }, + { + name: "EIGHT_CLUBS", + cardName: EIGHT_CLUBS, + expected: EightClubs, + }, + { + name: "NINE_CLUBS", + cardName: NINE_CLUBS, + expected: NineClubs, + }, + { + name: "TEN_CLUBS", + cardName: TEN_CLUBS, + expected: TenClubs, + }, + { + name: "JACK_CLUBS", + cardName: JACK_CLUBS, + expected: JackClubs, + }, + { + name: "QUEEN_CLUBS", + cardName: QUEEN_CLUBS, + expected: QueenClubs, + }, + { + name: "KING_CLUBS", + cardName: KING_CLUBS, + expected: KingClubs, + }, + { + name: "ACE_SPADES", + cardName: ACE_SPADES, + expected: AceSpades, + }, + { + name: "TWO_SPADES", + cardName: TWO_SPADES, + expected: TwoSpades, + }, + { + name: "THREE_SPADES", + cardName: THREE_SPADES, + expected: ThreeSpades, + }, + { + name: "FOUR_SPADES", + cardName: FOUR_SPADES, + expected: FourSpades, + }, + { + name: "FIVE_SPADES", + cardName: FIVE_SPADES, + expected: FiveSpades, + }, + { + name: "SIX_SPADES", + cardName: SIX_SPADES, + expected: SixSpades, + }, + { + name: "SEVEN_SPADES", + cardName: SEVEN_SPADES, + expected: SevenSpades, + }, + { + name: "EIGHT_SPADES", + cardName: EIGHT_SPADES, + expected: EightSpades, + }, + { + name: "NINE_SPADES", + cardName: NINE_SPADES, + expected: NineSpades, + }, + { + name: "TEN_SPADES", + cardName: TEN_SPADES, + expected: TenSpades, + }, + { + name: "JACK_SPADES", + cardName: JACK_SPADES, + expected: JackSpades, + }, + { + name: "QUEEN_SPADES", + cardName: QUEEN_SPADES, + expected: QueenSpades, + }, + { + name: "KING_SPADES", + cardName: KING_SPADES, + expected: KingSpades, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + card := test.cardName.Card() + if card != test.expected { + t.Errorf("expected card to be %v, got %v", test.expected, card) + } + }) + } + +} diff --git a/pkg/game/deck.go b/pkg/game/deck.go index b774858..509d474 100644 --- a/pkg/game/deck.go +++ b/pkg/game/deck.go @@ -5,10 +5,10 @@ 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" ) @@ -71,6 +71,10 @@ const ( JOKER = "JOKER" ) +func (c CardName) Card() Card { + return ParseCard(c) +} + // Card represents a card with a value, coldValue, suit, and renegable status. type Card struct { Name CardName @@ -82,60 +86,60 @@ 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{Name: EMPTY_CARD, Value: 0, ColdValue: 0, Suit: Empty, Renegable: false} + TwoHearts = Card{Name: TWO_HEARTS, Value: 101, ColdValue: 2, Suit: Hearts, Renegable: false} + ThreeHearts = Card{Name: THREE_HEARTS, Value: 102, ColdValue: 3, Suit: Hearts, Renegable: false} + FourHearts = Card{Name: FOUR_HEARTS, Value: 103, ColdValue: 4, Suit: Hearts, Renegable: false} + FiveHearts = Card{Name: FIVE_HEARTS, Value: 115, ColdValue: 5, Suit: Hearts, Renegable: true} + SixHearts = Card{Name: SIX_HEARTS, Value: 104, ColdValue: 6, Suit: Hearts, Renegable: false} + SevenHearts = Card{Name: SEVEN_HEARTS, Value: 105, ColdValue: 7, Suit: Hearts, Renegable: false} + EightHearts = Card{Name: EIGHT_HEARTS, Value: 106, ColdValue: 8, Suit: Hearts, Renegable: false} + NineHearts = Card{Name: NINE_HEARTS, Value: 107, ColdValue: 9, Suit: Hearts, Renegable: false} + TenHearts = Card{Name: TEN_HEARTS, Value: 108, ColdValue: 10, Suit: Hearts, Renegable: false} + JackHearts = Card{Name: JACK_HEARTS, Value: 114, ColdValue: 11, Suit: Hearts, Renegable: true} + QueenHearts = Card{Name: QUEEN_HEARTS, Value: 109, ColdValue: 12, Suit: Hearts, Renegable: false} + KingHearts = Card{Name: KING_HEARTS, Value: 110, ColdValue: 13, Suit: Hearts, Renegable: false} + AceHearts = Card{Name: ACE_HEARTS, Value: 112, ColdValue: 0, Suit: Wild, Renegable: true} + TwoDiamonds = Card{Name: TWO_DIAMONDS, Value: 101, ColdValue: 2, Suit: Diamonds, Renegable: false} + ThreeDiamonds = Card{Name: THREE_DIAMONDS, Value: 102, ColdValue: 3, Suit: Diamonds, Renegable: false} + FourDiamonds = Card{Name: FOUR_DIAMONDS, Value: 103, ColdValue: 4, Suit: Diamonds, Renegable: false} + FiveDiamonds = Card{Name: FIVE_DIAMONDS, Value: 115, ColdValue: 5, Suit: Diamonds, Renegable: true} + SixDiamonds = Card{Name: SIX_DIAMONDS, Value: 104, ColdValue: 6, Suit: Diamonds, Renegable: false} + SevenDiamonds = Card{Name: SEVEN_DIAMONDS, Value: 105, ColdValue: 7, Suit: Diamonds, Renegable: false} + EightDiamonds = Card{Name: EIGHT_DIAMONDS, Value: 106, ColdValue: 8, Suit: Diamonds, Renegable: false} + NineDiamonds = Card{Name: NINE_DIAMONDS, Value: 107, ColdValue: 9, Suit: Diamonds, Renegable: false} + TenDiamonds = Card{Name: TEN_DIAMONDS, Value: 108, ColdValue: 10, Suit: Diamonds, Renegable: false} + JackDiamonds = Card{Name: JACK_DIAMONDS, Value: 114, ColdValue: 11, Suit: Diamonds, Renegable: true} + QueenDiamonds = Card{Name: QUEEN_DIAMONDS, Value: 109, ColdValue: 12, Suit: Diamonds, Renegable: false} + KingDiamonds = Card{Name: KING_DIAMONDS, Value: 110, ColdValue: 13, Suit: Diamonds, Renegable: false} + AceDiamonds = Card{Name: ACE_DIAMONDS, Value: 111, ColdValue: 1, Suit: Diamonds, Renegable: false} + TwoClubs = Card{Name: TWO_CLUBS, Value: 108, ColdValue: 9, Suit: Clubs, Renegable: false} + ThreeClubs = Card{Name: THREE_CLUBS, Value: 107, ColdValue: 8, Suit: Clubs, Renegable: false} + FourClubs = Card{Name: FOUR_CLUBS, Value: 106, ColdValue: 7, Suit: Clubs, Renegable: false} + FiveClubs = Card{Name: FIVE_CLUBS, Value: 115, ColdValue: 6, Suit: Clubs, Renegable: true} + SixClubs = Card{Name: SIX_CLUBS, Value: 105, ColdValue: 5, Suit: Clubs, Renegable: false} + SevenClubs = Card{Name: SEVEN_CLUBS, Value: 104, ColdValue: 4, Suit: Clubs, Renegable: false} + EightClubs = Card{Name: EIGHT_CLUBS, Value: 103, ColdValue: 3, Suit: Clubs, Renegable: false} + NineClubs = Card{Name: NINE_CLUBS, Value: 102, ColdValue: 2, Suit: Clubs, Renegable: false} + TenClubs = Card{Name: TEN_CLUBS, Value: 101, ColdValue: 1, Suit: Clubs, Renegable: false} + JackClubs = Card{Name: JACK_CLUBS, Value: 114, ColdValue: 11, Suit: Clubs, Renegable: true} + QueenClubs = Card{Name: QUEEN_CLUBS, Value: 109, ColdValue: 12, Suit: Clubs, Renegable: false} + KingClubs = Card{Name: KING_CLUBS, Value: 110, ColdValue: 13, Suit: Clubs, Renegable: false} + AceClubs = Card{Name: ACE_CLUBS, Value: 111, ColdValue: 10, Suit: Clubs, Renegable: false} + TwoSpades = Card{Name: TWO_SPADES, Value: 108, ColdValue: 9, Suit: Spades, Renegable: false} + ThreeSpades = Card{Name: THREE_SPADES, Value: 107, ColdValue: 8, Suit: Spades, Renegable: false} + FourSpades = Card{Name: FOUR_SPADES, Value: 106, ColdValue: 7, Suit: Spades, Renegable: false} + FiveSpades = Card{Name: FIVE_SPADES, Value: 115, ColdValue: 6, Suit: Spades, Renegable: true} + SixSpades = Card{Name: SIX_SPADES, Value: 105, ColdValue: 5, Suit: Spades, Renegable: false} + SevenSpades = Card{Name: SEVEN_SPADES, Value: 104, ColdValue: 4, Suit: Spades, Renegable: false} + EightSpades = Card{Name: EIGHT_SPADES, Value: 103, ColdValue: 3, Suit: Spades, Renegable: false} + NineSpades = Card{Name: NINE_SPADES, Value: 102, ColdValue: 2, Suit: Spades, Renegable: false} + TenSpades = Card{Name: TEN_SPADES, Value: 101, ColdValue: 1, Suit: Spades, Renegable: false} + JackSpades = Card{Name: JACK_SPADES, Value: 114, ColdValue: 11, Suit: Spades, Renegable: true} + QueenSpades = Card{Name: QUEEN_SPADES, Value: 109, ColdValue: 12, Suit: Spades, Renegable: false} + KingSpades = Card{Name: KING_SPADES, Value: 110, ColdValue: 13, Suit: Spades, Renegable: false} + AceSpades = Card{Name: ACE_SPADES, Value: 111, ColdValue: 10, Suit: Spades, Renegable: false} + Joker = Card{Name: JOKER, Value: 113, ColdValue: 0, Suit: Wild, Renegable: true} ) func (c Card) String() string { diff --git a/pkg/game/game-handler.go b/pkg/game/game-handler.go index d85d1c1..ed786b9 100644 --- a/pkg/game/game-handler.go +++ b/pkg/game/game-handler.go @@ -371,3 +371,56 @@ func (h *Handler) Buy(c *gin.Context) { } c.IndentedJSON(http.StatusOK, state) } + +// Play @Summary Play a card +// @Description When in the Playing state, the current player can play a card +// @Tags Game +// @ID play +// @Produce json +// @Security Bearer +// @Param gameId path string true "Game ID" +// @Param card query string true "Card" +// @Success 200 {object} State +// @Failure 400 {object} api.ErrorResponse +// @Failure 500 {object} api.ErrorResponse +// @Router /game/{gameId}/play [put] +func (h *Handler) Play(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 card from the request + card, exists := c.GetQuery("card") + if !exists { + c.JSON(http.StatusBadRequest, api.ErrorResponse{Message: "Missing card"}) + return + } + // Check if is a valid card + cn, err := ParseCardName(card) + if err != nil { + c.JSON(http.StatusBadRequest, api.ErrorResponse{Message: err.Error()}) + return + } + + // Play the card + game, err := h.S.Play(ctx, gameId, id, cn) + + 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 a0467db..6f86247 100644 --- a/pkg/game/game-methods.go +++ b/pkg/game/game-methods.go @@ -393,3 +393,242 @@ func (g *Game) Buy(id string, cards []CardName) error { return nil } + +func (g *Game) Play(id string, card CardName) error { + // Verify the at the round is in the playing state + if g.CurrentRound.Status != Playing { + return fmt.Errorf("round must be in the playing state to play a card") + } + + // Verify that is the player's go + if g.CurrentRound.CurrentHand.CurrentPlayerID != id { + return fmt.Errorf("only the current player can play a card") + } + + // Verify the card is valid + state, err := g.GetState(id) + if err != nil { + return err + } + if !contains(state.Cards, card) { + return fmt.Errorf("invalid card selected") + } + + // Check that they are following suit + if g.CurrentRound.CurrentHand.LeadOut == "" { + // I must be leading out + g.CurrentRound.CurrentHand.LeadOut = card + } else { + if !isFollowing(card, state.Cards, g.CurrentRound.CurrentHand, g.CurrentRound.Suit) { + return fmt.Errorf("must follow suit") + } + } + + // Remove the card from the player's hand + var cards []CardName + for _, c := range state.Cards { + if c != card { + cards = append(cards, c) + } + } + for i, p := range g.Players { + if p.ID == id { + g.Players[i].Cards = cards + break + } + } + + // Add the card to the played cards + pc := PlayedCard{ + PlayerID: id, + Card: card, + } + g.CurrentRound.CurrentHand.PlayedCards = append(g.CurrentRound.CurrentHand.PlayedCards, pc) + + // Check if the hand is complete + if len(g.CurrentRound.CurrentHand.PlayedCards) < len(g.Players) { + // Set the next player + np, err := nextPlayer(g.Players, id) + if err != nil { + return err + } + g.CurrentRound.CurrentHand.CurrentPlayerID = np.ID + } else { + + g.completeHand() + + // Check if the round is complete + if len(g.CurrentRound.CompletedHands) == 5 { + err := g.applyScores() + if err != nil { + return err + } + + // Check if the game is complete + if g.isGameOver() { + err := g.completeGame() + if err != nil { + return err + } + } else { + err := g.completeRound() + if err != nil { + return err + } + } + } + } + + // Increment revision + g.Revision++ + + return nil + +} + +func (g *Game) completeHand() { + // 1. Find the winner + winningCard, err := determineWinner(g.CurrentRound.CurrentHand, g.CurrentRound.Suit) + if err != nil { + log.Printf("Error determining winner: %s", err.Error()) + return + } + + // 2. Add the hand to the completed hands + g.CurrentRound.CompletedHands = append(g.CurrentRound.CompletedHands, g.CurrentRound.CurrentHand) + + // 3. Set the next hand + g.CurrentRound.CurrentHand = Hand{ + Timestamp: time.Now(), + CurrentPlayerID: winningCard.PlayerID, + PlayedCards: make([]PlayedCard, 0), + } +} + +func (g *Game) applyScores() error { + // 1. Find winning card for each hand + winningCards, err := findWinningCardsForRound(g.CurrentRound) + if err != nil { + return err + } + + // 2. Check if there was a jink + jinkHappened, err := checkForJink(winningCards, g.Players, g.CurrentRound.GoerID) + if err != nil { + return err + } + if jinkHappened { + teamId, err := getTeamID(winningCards[0].PlayerID, g.Players) + if err != nil { + return err + } + // The successful team gets 60 points + for i, p := range g.Players { + if p.ID == teamId { + g.Players[i].Score += 60 + } + } + return nil + } + + // 3. Calculate scores + scores, err := calculateScores(winningCards, g.Players, g.CurrentRound.Suit) + + // 4. If the goer didn't make their contract, set score to minus the contract + goer, err := findPlayer(g.CurrentRound.GoerID, g.Players) + if err != nil { + return err + } + call := int(goer.Call) + if scores[goer.TeamID] < call { + scores[goer.TeamID] = -call + } + + // 5. Apply scores + for teamId, score := range scores { + for i, p := range g.Players { + if p.TeamID == teamId { + g.Players[i].Score += score + } + } + } + + return nil +} + +func (g *Game) isGameOver() bool { + for _, p := range g.Players { + if p.Score >= 110 { + return true + } + } + return false +} + +func (g *Game) completeRound() error { + // 1. Add round to completed + g.Completed = append(g.Completed, g.CurrentRound) + + // 2. Create next round + nextDealer, err := nextPlayer(g.Players, g.CurrentRound.DealerID) + if err != nil { + return err + } + nextPlayer, err := nextPlayer(g.Players, nextDealer.ID) + if err != nil { + return err + } + nextHand := Hand{ + Timestamp: time.Now(), + CurrentPlayerID: nextPlayer.ID, + } + g.CurrentRound = Round{ + Timestamp: time.Now(), + Number: g.CurrentRound.Number + 1, + DealerID: nextDealer.ID, + Status: Calling, + CurrentHand: nextHand, + } + + // 3. Clear cards + for i := range g.Players { + g.Players[i].Cards = make([]CardName, 0) + } + + // 4. Deal cards + deck, hands, err := DealCards(ShuffleCards(NewDeck()), len(g.Players)) + if err != nil { + return err + } + 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 + + // 5. Increment revision + g.Revision++ + + return nil +} + +func (g *Game) completeGame() error { + winningTeam, err := findWinningTeam(g.Players, g.CurrentRound) + if err != nil { + return err + } + + for i, p := range g.Players { + if p.TeamID == winningTeam { + g.Players[i].Winner = true + } + } + g.Status = Completed + + return nil +} diff --git a/pkg/game/game-service.go b/pkg/game/game-service.go index 39d885c..de76372 100644 --- a/pkg/game/game-service.go +++ b/pkg/game/game-service.go @@ -15,8 +15,9 @@ 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) - Buy(ctx context.Context, id string, id2 string, cards []CardName) (Game, error) + SelectSuit(ctx context.Context, gameId string, playerId string, suit Suit, cards []CardName) (Game, error) + Buy(ctx context.Context, gameId string, playerId string, cards []CardName) (Game, error) + Play(ctx context.Context, gameId string, playerId string, card CardName) (Game, error) } type Service struct { @@ -176,3 +177,28 @@ func (s *Service) Buy(ctx context.Context, gameId string, playerID string, cards } return game, nil } + +// Play a card +func (s *Service) Play(ctx context.Context, gameId string, playerID string, card 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") + } + + // Play the card. + err = game.Play(playerID, card) + 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 fdb72f1..b51d24c 100644 --- a/pkg/game/game-utils.go +++ b/pkg/game/game-utils.go @@ -170,7 +170,7 @@ func NewGame(playerIDs []string, name string, adminID string) (Game, error) { // Create the game game := Game{ - ID: "game-" + strconv.Itoa(rand.Intn(1000000)), + ID: strconv.Itoa(rand.Intn(10000000)), Timestamp: time.Now(), Name: name, Status: Active, @@ -208,6 +208,15 @@ func containsAllUnique(referenceSlice, targetSlice []CardName) bool { return true } +func contains(referenceSlice []CardName, target CardName) bool { + for _, item := range referenceSlice { + if item == target { + return true + } + } + return false +} + // compare checks if targetSlice and referenceSlice are equivalent func compare(referenceSlice, targetSlice []CardName) bool { if len(referenceSlice) != len(targetSlice) { @@ -215,3 +224,296 @@ func compare(referenceSlice, targetSlice []CardName) bool { } return containsAllUnique(referenceSlice, targetSlice) } + +func canRenage(leadOut Card, myTrumps []Card) bool { + for _, trump := range myTrumps { + if !trump.Renegable || trump.Value <= leadOut.Value { + return false + } + } + return true +} + +func isFollowing(myCard CardName, myCards []CardName, currentHand Hand, suit Suit) bool { + mySuit := myCard.Card().Suit + leadOut := currentHand.LeadOut.Card() + suitLead := leadOut.Suit == suit || leadOut.Suit == Wild + + if suitLead { + var myTrumps []Card + for _, card := range myCards { + if mySuit == suit || mySuit == Wild { + myTrumps = append(myTrumps, card.Card()) + } + } + + return len(myTrumps) == 0 || + mySuit == suit || + mySuit == Wild || + canRenage(leadOut, myTrumps) + } + + var mySuitedCards []Card + for _, card := range myCards { + if mySuit == leadOut.Suit { + mySuitedCards = append(mySuitedCards, card.Card()) + } + } + + return len(mySuitedCards) == 0 || + mySuit == suit || + mySuit == Wild || + mySuit == leadOut.Suit +} + +func determineWinner(hand Hand, suit Suit) (PlayedCard, error) { + // Get active suit + activeSuit := getActiveSuit(hand, suit) + + // Find the winning card + return findWinningCard(hand.PlayedCards, suit, activeSuit) +} + +// getActiveSuit Was a suit or wild card played? If not set the lead out card as the suit +func getActiveSuit(hand Hand, suit Suit) Suit { + // If a trump was played, the active suit is the trump suit + for _, playedCard := range hand.PlayedCards { + if playedCard.Card.Card().Suit == suit || playedCard.Card.Card().Suit == Wild { + return suit + } + } + + return hand.LeadOut.Card().Suit +} + +func findWinningCard(playedCards []PlayedCard, suit Suit, activeSuit Suit) (PlayedCard, error) { + if len(playedCards) == 0 { + return PlayedCard{}, errors.New("no cards played") + } + winningCard := playedCards[0] + for i, playedCard := range playedCards { + if i == 0 { + continue + } + card := playedCard.Card.Card() + if suit == activeSuit { + // A trump card was played + if card.Value > winningCard.Card.Card().Value { + winningCard = playedCard + } + } else { + // A cold card will win as no trump was played + if card.Suit == activeSuit { + if card.ColdValue > winningCard.Card.Card().ColdValue { + winningCard = playedCard + } + } + } + } + return winningCard, nil +} + +func findBestTrump(cards []PlayedCard, suit Suit) (PlayedCard, bool, error) { + if len(cards) == 0 { + return PlayedCard{}, false, errors.New("no cards played") + } + + // 1. Verify at least one trump was played + trumpPlayed := false + for _, playedCard := range cards { + if playedCard.Card.Card().Suit == suit || playedCard.Card.Card().Suit == Wild { + trumpPlayed = true + break + } + } + + if !trumpPlayed { + return PlayedCard{}, false, nil + } + + // 2. Get the best card + var winningCard PlayedCard + for _, c := range cards { + card := c.Card.Card() + if (card.Suit == suit || card.Suit == Wild) && (winningCard.Card == "" || card.Value > winningCard.Card.Card().Value) { + winningCard = c + } + } + return winningCard, true, nil +} + +func findWinningCardsForRound(round Round) ([]PlayedCard, error) { + if round.Suit == "" { + return nil, errors.New("suit not set") + } + winningCards := make([]PlayedCard, 5) + if len(round.CompletedHands) != 5 { + return nil, errors.New("round not complete") + } + + for i, hand := range round.CompletedHands { + winningCard, err := determineWinner(hand, round.Suit) + if err != nil { + return nil, err + } + winningCards[i] = winningCard + } + + return winningCards, nil +} + +func findPlayer(playerID string, players []Player) (Player, error) { + for _, player := range players { + if player.ID == playerID { + return player, nil + } + } + return Player{}, errors.New("player not found") +} + +func getTeamID(playerID string, players []Player) (string, error) { + for _, player := range players { + if player.ID == playerID { + return player.TeamID, nil + } + } + return "", errors.New("player not found") +} + +func checkForJink(winningCards []PlayedCard, players []Player, goerId string) (bool, error) { + goingTeam, err := getTeamID(goerId, players) + if err != nil { + return false, err + } + if len(winningCards) != 5 { + return false, errors.New("invalid number of winning cards") + } + // Jink is only valid if there are more than 2 players + if len(players) < 3 { + return false, nil + } + + for _, winningCard := range winningCards { + teamID, err := getTeamID(winningCard.PlayerID, players) + if err != nil { + return false, err + } + if teamID != goingTeam { + return false, nil + } + } + return true, nil +} + +func findWinningTeam(players []Player, round Round) (string, error) { + // 1. If only one team >= 110 -> they are the winner + winningTeams := getTeamsOver110(players) + if len(winningTeams) == 1 { + for teamID := range winningTeams { + return teamID, nil + } + } + + // 2. If more than one team >= 110 but one is the goer -> the goer is the winning team + goerTeamID := "" + for _, player := range players { + if player.ID == round.GoerID { + goerTeamID = player.TeamID + break + } + } + if winningTeams[goerTeamID] { + return goerTeamID, nil + } + + // 3. Else first team >= 110 is the winner + return findFirstTeamToPass110(players, round) +} + +func getTeamsOver110(players []Player) map[string]bool { + teamsOver110 := make(map[string]bool) + for _, player := range players { + if player.Score >= 110 { + teamsOver110[player.TeamID] = true + } + } + return teamsOver110 +} + +func findFirstTeamToPass110(players []Player, round Round) (string, error) { + winningCards, err := findWinningCardsForRound(round) + if err != nil { + return "", err + } + + bestTrump, trumpPlayed, err := findBestTrump(winningCards, round.Suit) + if err != nil { + return "", err + } + + // Go backwards through the hands and find the first team to pass 110 + for i := len(winningCards) - 1; i >= 0; i-- { + card := winningCards[i] + if err != nil { + return "", err + } + teamID, err := getTeamID(card.PlayerID, players) + if err != nil { + return "", err + } + + for j, p := range players { + if p.TeamID == teamID { + if trumpPlayed && bestTrump.Card == card.Card { + players[j].Score -= 10 + } else { + players[j].Score -= 5 + } + } + } + winningTeams := getTeamsOver110(players) + if len(winningTeams) == 1 { + for t := range winningTeams { + return t, nil + } + } + } + + return "", errors.New("no winning team found") +} + +func calculateScores(winningCards []PlayedCard, players []Player, suit Suit) (map[string]int, error) { + // 1. Get the best card + bestCard, trumpPlayed, err := findBestTrump(winningCards, suit) + if err != nil { + return nil, err + } + + // Calculate the scores per player + scoresPlayer := make(map[string]int) + for _, winningCard := range winningCards { + if err != nil { + return nil, err + } + for _, p := range players { + if p.ID == winningCard.PlayerID { + if trumpPlayed && winningCard.Card == bestCard.Card { + scoresPlayer[p.ID] += 10 + } else { + scoresPlayer[p.ID] += 5 + } + } + } + } + + // Aggregate the scores per team + scoresTeam := make(map[string]int) + for playerID, score := range scoresPlayer { + teamID, err := getTeamID(playerID, players) + if err != nil { + return nil, err + } + scoresTeam[teamID] += score + } + return scoresTeam, nil +} diff --git a/pkg/game/game-utils_test.go b/pkg/game/game-utils_test.go index 4556fb7..7a7ae34 100644 --- a/pkg/game/game-utils_test.go +++ b/pkg/game/game-utils_test.go @@ -2,7 +2,50 @@ package game import "testing" -func TestGame_ParseCall(t *testing.T) { +func TestGameUtils_validateNumberOfPlayers(t *testing.T) { + tests := []struct { + name string + playerIDs []string + expectingError bool + }{ + { + name: "Valid number of players - 2", + playerIDs: []string{"1", "2"}, + expectingError: false, + }, + { + name: "Valid number of players - 6", + playerIDs: []string{"1", "2", "3", "4", "5", "6"}, + expectingError: false, + }, + { + name: "Invalid number of players - 1", + playerIDs: []string{"1"}, + expectingError: true, + }, + { + name: "Invalid number of players - 7", + playerIDs: []string{"1", "2", "3", "4", "5", "6", "7"}, + expectingError: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := validateNumberOfPlayers(test.playerIDs) + 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) + } + } + }) + } +} +func TestGameUtils_ParseCall(t *testing.T) { tests := []struct { name string callStr string @@ -69,3 +112,1099 @@ func TestGame_ParseCall(t *testing.T) { }) } } + +func TestGameUtils_shuffle(t *testing.T) { + tests := []struct { + name string + input []string + }{ + { + name: "Shuffle 2", + input: []string{"1", "2", "3", "4", "5", "6", "7", "8", "9", "10"}, + }, + { + name: "Shuffle 6", + input: []string{"1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13"}, + }, + { + name: "Shuffle 0", + input: []string{}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + shuffled := shuffle(test.input) + + expectedSize := len(test.input) + if len(shuffled) != expectedSize { + t.Errorf("expected shuffled size to be %d, got %d", expectedSize, len(shuffled)) + } + + // Check that all the elements are in the shuffled slice + for _, v := range test.input { + found := false + for _, s := range shuffled { + if v == s { + found = true + break + } + } + if !found { + t.Errorf("expected %s to be in shuffled slice", v) + } + } + + // Expect the order to be different if length > 8 + if len(test.input) > 8 { + // Check that at least one element is in a different position + found := false + for i, v := range test.input { + if v != shuffled[i] { + found = true + break + } + } + if !found { + t.Errorf("expected at least one element to be in a different position") + } + } + + }) + } +} + +func TestGameUtils_createPlayers(t *testing.T) { + tests := []struct { + name string + playerIDs []string + expectingError bool + }{ + { + name: "Valid number of players - 2", + playerIDs: []string{"1", "2"}, + expectingError: false, + }, + { + name: "Valid number of players - 6", + playerIDs: []string{"1", "2", "3", "4", "5", "6"}, + expectingError: false, + }, + { + name: "Invalid number of players - 1", + playerIDs: []string{"1"}, + expectingError: true, + }, + { + name: "Invalid number of players - 7", + playerIDs: []string{"1", "2", "3", "4", "5", "6", "7"}, + expectingError: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + players, err := createPlayers(test.playerIDs) + 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 len(players) != len(test.playerIDs) { + t.Errorf("expected %d players, got %d", len(test.playerIDs), len(players)) + } + } + }) + } +} + +func TestGameUtils_canRenage(t *testing.T) { + tests := []struct { + name string + leadCard Card + myTrumps []Card + expectedResult bool + }{ + { + name: "Can renage - Jack", + leadCard: TwoHearts, + myTrumps: []Card{JackHearts}, + expectedResult: true, + }, + { + name: "Can renage - Five", + leadCard: TwoHearts, + myTrumps: []Card{FiveHearts}, + expectedResult: true, + }, + { + name: "Can renage - Ace Hearts", + leadCard: TwoHearts, + myTrumps: []Card{AceHearts}, + expectedResult: true, + }, + { + name: "Can renage - Ace Hearts with different suit", + leadCard: TwoDiamonds, + myTrumps: []Card{AceHearts}, + expectedResult: true, + }, + { + name: "Can renage - Joker", + leadCard: TenSpades, + myTrumps: []Card{Joker}, + expectedResult: true, + }, + { + name: "Can't renage - Two Hearts", + leadCard: FiveHearts, + myTrumps: []Card{TwoHearts}, + expectedResult: false, + }, + { + name: "Can't renage - ace of spades", + leadCard: FourSpades, + myTrumps: []Card{AceSpades}, + expectedResult: false, + }, + { + name: "Can't renage - when higher value trump is lead", + leadCard: JackHearts, + myTrumps: []Card{Joker}, + expectedResult: false, + }, + { + name: "Can't renage - when higher value trump is lead", + leadCard: FiveDiamonds, + myTrumps: []Card{JackDiamonds}, + expectedResult: false, + }, + { + name: "Can't renage - when higher value trump is lead", + leadCard: Joker, + myTrumps: []Card{AceHearts}, + expectedResult: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + result := canRenage(test.leadCard, test.myTrumps) + if result != test.expectedResult { + t.Errorf("expected %v, got %v", test.expectedResult, result) + } + }) + } +} + +func TestGameUtils_isFollowing(t *testing.T) { + tests := []struct { + name string + myCard CardName + myCards []CardName + currentHand Hand + suit Suit + expectedResult bool + }{ + { + name: "Can follow - Jack of Hearts", + myCard: JACK_HEARTS, + myCards: []CardName{JACK_HEARTS, FIVE_HEARTS, ACE_HEARTS}, + currentHand: Hand{LeadOut: TWO_HEARTS}, + suit: Hearts, + expectedResult: true, + }, + { + name: "Can follow - Ace of Hearts", + myCard: ACE_HEARTS, + myCards: []CardName{JACK_HEARTS, FIVE_HEARTS, ACE_HEARTS}, + currentHand: Hand{LeadOut: TWO_HEARTS}, + suit: Hearts, + expectedResult: true, + }, + { + name: "Can follow - Joker", + myCard: JOKER, + myCards: []CardName{JACK_HEARTS, FIVE_HEARTS, ACE_HEARTS, JOKER}, + currentHand: Hand{LeadOut: TWO_HEARTS}, + suit: Hearts, + expectedResult: true, + }, + { + name: "Can follow - Joker", + myCard: JOKER, + myCards: []CardName{JACK_HEARTS, FIVE_HEARTS, ACE_HEARTS, JOKER}, + currentHand: Hand{LeadOut: TWO_HEARTS}, + suit: Hearts, + expectedResult: true, + }, + { + name: "Can follow - I have no trumps", + myCard: JACK_HEARTS, + myCards: []CardName{JACK_HEARTS, FIVE_HEARTS, TWO_HEARTS}, + currentHand: Hand{LeadOut: FIVE_DIAMONDS}, + suit: Diamonds, + expectedResult: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + result := isFollowing(test.myCard, test.myCards, test.currentHand, test.suit) + if result != test.expectedResult { + t.Errorf("expected %v, got %v", test.expectedResult, result) + } + }) + } +} + +func TestGameUtils_getActiveSuit(t *testing.T) { + tests := []struct { + name string + hand Hand + suit Suit + expectedResult Suit + }{ + { + 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: Hearts, + }, + { + name: "Joker played", + hand: Hand{LeadOut: JOKER, PlayedCards: []PlayedCard{{Card: JACK_HEARTS, PlayerID: "1"}, {Card: FIVE_HEARTS, PlayerID: "2"}, {Card: JOKER, PlayerID: "3"}}}, + suit: Diamonds, + expectedResult: Diamonds, + }, + { + name: "Ace of hearts played", + hand: Hand{LeadOut: ACE_HEARTS, PlayedCards: []PlayedCard{{Card: JACK_HEARTS, PlayerID: "1"}, {Card: FIVE_HEARTS, PlayerID: "2"}, {Card: ACE_HEARTS, PlayerID: "3"}}}, + suit: Diamonds, + expectedResult: Diamonds, + }, + { + name: "No trump cards played", + hand: Hand{LeadOut: JACK_HEARTS, PlayedCards: []PlayedCard{{Card: JACK_HEARTS, PlayerID: "1"}, {Card: FIVE_HEARTS, PlayerID: "2"}, {Card: ACE_DIAMONDS, PlayerID: "3"}}}, + suit: Clubs, + expectedResult: Hearts, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + result := getActiveSuit(test.hand, test.suit) + if result != test.expectedResult { + t.Errorf("expected %v, got %v", test.expectedResult, result) + } + }) + } +} + +func TestGameUtils_findWinningCard(t *testing.T) { + tests := []struct { + name string + hand Hand + suit Suit + activeSuit Suit + expectedResult PlayedCard + expectingError bool + }{ + { + name: "No cards played", + hand: Hand{LeadOut: JACK_HEARTS, PlayedCards: []PlayedCard{}}, + suit: Hearts, + activeSuit: Hearts, + expectedResult: PlayedCard{}, + expectingError: true, + }, + { + name: "No trump cards played", + hand: Hand{LeadOut: JACK_HEARTS, PlayedCards: []PlayedCard{{Card: JACK_HEARTS, PlayerID: "1"}, {Card: FIVE_HEARTS, PlayerID: "2"}, {Card: ACE_DIAMONDS, PlayerID: "3"}}}, + suit: Diamonds, + activeSuit: Hearts, + 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, + activeSuit: Hearts, + expectedResult: PlayedCard{Card: FIVE_HEARTS, PlayerID: "2"}, + expectingError: false, + }, + { + name: "Trump cards played - Joker", + hand: Hand{LeadOut: TEN_HEARTS, PlayedCards: []PlayedCard{{Card: TEN_HEARTS, PlayerID: "1"}, {Card: SIX_HEARTS, PlayerID: "2"}, {Card: JOKER, PlayerID: "3"}}}, + suit: Hearts, + activeSuit: Hearts, + expectedResult: PlayedCard{Card: JOKER, PlayerID: "3"}, + expectingError: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + result, err := findWinningCard(test.hand.PlayedCards, test.suit, test.activeSuit) + 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 result != test.expectedResult { + t.Errorf("expected %v, got %v", test.expectedResult, result) + } + } + }) + } +} + +func TestGameUtils_determineWinner(t *testing.T) { + tests := []struct { + name string + hand Hand + suit Suit + expectedResult PlayedCard + expectingError bool + }{ + { + name: "No cards played", + hand: Hand{LeadOut: JACK_HEARTS, PlayedCards: []PlayedCard{}}, + suit: Hearts, + expectedResult: PlayedCard{}, + expectingError: true, + }, + { + name: "No trump cards played", + 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: TEN_HEARTS, PlayedCards: []PlayedCard{{Card: TEN_HEARTS, PlayerID: "1"}, {Card: SIX_HEARTS, PlayerID: "2"}, {Card: JOKER, PlayerID: "3"}}}, + suit: Hearts, + expectedResult: PlayedCard{Card: JOKER, PlayerID: "3"}, + expectingError: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + result, err := determineWinner(test.hand, test.suit) + 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 result != test.expectedResult { + t.Errorf("expected %v, got %v", test.expectedResult, result) + } + } + }) + } +} + +func TestGameUtils_findWinningCardsForRound(t *testing.T) { + tests := []struct { + name string + round Round + expectedResult []PlayedCard + expectingError bool + }{ + { + name: "Round complete", + round: Round{ + Suit: Hearts, + CompletedHands: []Hand{ + {LeadOut: JACK_HEARTS, PlayedCards: []PlayedCard{{Card: JACK_HEARTS, PlayerID: "1"}, {Card: FIVE_HEARTS, PlayerID: "2"}, {Card: ACE_HEARTS, PlayerID: "3"}}}, + {LeadOut: TWO_SPADES, PlayedCards: []PlayedCard{{Card: TWO_SPADES, PlayerID: "1"}, {Card: SIX_SPADES, PlayerID: "2"}, {Card: ACE_SPADES, PlayerID: "3"}}}, + {LeadOut: SIX_DIAMONDS, PlayedCards: []PlayedCard{{Card: TWO_DIAMONDS, PlayerID: "1"}, {Card: SIX_DIAMONDS, PlayerID: "2"}, {Card: ACE_DIAMONDS, PlayerID: "3"}}}, + {LeadOut: ACE_CLUBS, PlayedCards: []PlayedCard{{Card: TWO_CLUBS, PlayerID: "1"}, {Card: SIX_CLUBS, PlayerID: "2"}, {Card: ACE_CLUBS, PlayerID: "3"}}}, + {LeadOut: SEVEN_HEARTS, PlayedCards: []PlayedCard{{Card: TWO_HEARTS, PlayerID: "1"}, {Card: SIX_HEARTS, PlayerID: "2"}, {Card: SEVEN_HEARTS, PlayerID: "3"}}}, + }, + }, + expectedResult: []PlayedCard{{Card: FIVE_HEARTS, PlayerID: "2"}, {Card: ACE_SPADES, PlayerID: "3"}, {Card: SIX_DIAMONDS, PlayerID: "2"}, {Card: ACE_CLUBS, PlayerID: "3"}, {Card: SEVEN_HEARTS, PlayerID: "3"}}, + }, + { + name: "Round not complete", + round: Round{ + Suit: Hearts, + CompletedHands: []Hand{ + {LeadOut: JACK_HEARTS, PlayedCards: []PlayedCard{{Card: JACK_HEARTS, PlayerID: "1"}, {Card: FIVE_HEARTS, PlayerID: "2"}, {Card: ACE_HEARTS, PlayerID: "3"}}}, + {LeadOut: TWO_SPADES, PlayedCards: []PlayedCard{{Card: TWO_SPADES, PlayerID: "1"}, {Card: SIX_SPADES, PlayerID: "2"}, {Card: ACE_SPADES, PlayerID: "3"}}}, + {LeadOut: SIX_DIAMONDS, PlayedCards: []PlayedCard{{Card: TWO_DIAMONDS, PlayerID: "1"}, {Card: SIX_DIAMONDS, PlayerID: "2"}, {Card: ACE_DIAMONDS, PlayerID: "3"}}}, + {LeadOut: ACE_CLUBS, PlayedCards: []PlayedCard{{Card: TWO_CLUBS, PlayerID: "1"}, {Card: SIX_CLUBS, PlayerID: "2"}, {Card: ACE_CLUBS, PlayerID: "3"}}}, + }, + }, + expectingError: true, + }, + { + name: "No suit set", + round: Round{CompletedHands: []Hand{ + {LeadOut: JACK_HEARTS, PlayedCards: []PlayedCard{{Card: JACK_HEARTS, PlayerID: "1"}, {Card: FIVE_HEARTS, PlayerID: "2"}, {Card: ACE_HEARTS, PlayerID: "3"}}}, + {LeadOut: TWO_SPADES, PlayedCards: []PlayedCard{{Card: TWO_SPADES, PlayerID: "1"}, {Card: SIX_SPADES, PlayerID: "2"}, {Card: ACE_SPADES, PlayerID: "3"}}}, + {LeadOut: SIX_DIAMONDS, PlayedCards: []PlayedCard{{Card: TWO_DIAMONDS, PlayerID: "1"}, {Card: SIX_DIAMONDS, PlayerID: "2"}, {Card: ACE_DIAMONDS, PlayerID: "3"}}}, + {LeadOut: ACE_CLUBS, PlayedCards: []PlayedCard{{Card: TWO_CLUBS, PlayerID: "1"}, {Card: SIX_CLUBS, PlayerID: "2"}, {Card: ACE_CLUBS, PlayerID: "3"}}}, + {LeadOut: SEVEN_HEARTS, PlayedCards: []PlayedCard{{Card: TWO_HEARTS, PlayerID: "1"}, {Card: SIX_HEARTS, PlayerID: "2"}, {Card: SEVEN_HEARTS, PlayerID: "3"}}}, + }}, + expectingError: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + result, err := findWinningCardsForRound(test.round) + 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 len(result) != len(test.expectedResult) { + t.Errorf("expected %d cards, got %d", len(test.expectedResult), len(result)) + } + // Check the cards are the same + for i, v := range result { + if v != test.expectedResult[i] { + t.Errorf("expected %v, got %v", test.expectedResult[i], v) + } + } + } + }) + } +} + +func TestGameUtils_checkForJink(t *testing.T) { + tests := []struct { + name string + winningCards []PlayedCard + players []Player + goerID string + expectedResult bool + expectingError bool + }{ + { + name: "Jink - doubles", + winningCards: []PlayedCard{ + {Card: JACK_HEARTS, PlayerID: "1"}, + {Card: JACK_DIAMONDS, PlayerID: "1"}, + {Card: JACK_CLUBS, PlayerID: "4"}, + {Card: JACK_SPADES, PlayerID: "4"}, + {Card: JACK_HEARTS, PlayerID: "1"}, + }, + players: []Player{ + {ID: "1", TeamID: "1"}, + {ID: "2", TeamID: "2"}, + {ID: "3", TeamID: "3"}, + {ID: "4", TeamID: "1"}, + {ID: "5", TeamID: "2"}, + {ID: "6", TeamID: "3"}, + }, + goerID: "1", + expectedResult: true, + }, + { + name: "Jink - two player game", + winningCards: []PlayedCard{ + {Card: JACK_HEARTS, PlayerID: "1"}, + {Card: JACK_DIAMONDS, PlayerID: "1"}, + {Card: JACK_CLUBS, PlayerID: "1"}, + {Card: JACK_SPADES, PlayerID: "1"}, + {Card: JACK_HEARTS, PlayerID: "1"}, + }, + players: []Player{ + {ID: "1", TeamID: "1"}, + {ID: "2", TeamID: "2"}, + }, + goerID: "1", + expectedResult: false, + }, + { + name: "Jink - three player game", + winningCards: []PlayedCard{ + {Card: JACK_HEARTS, PlayerID: "1"}, + {Card: JACK_DIAMONDS, PlayerID: "1"}, + {Card: JACK_CLUBS, PlayerID: "1"}, + {Card: JACK_SPADES, PlayerID: "1"}, + {Card: JACK_HEARTS, PlayerID: "1"}, + }, + players: []Player{ + {ID: "1", TeamID: "1"}, + {ID: "2", TeamID: "2"}, + {ID: "3", TeamID: "3"}, + }, + goerID: "1", + expectedResult: true, + }, + { + name: "Jink - four player game", + winningCards: []PlayedCard{ + {Card: JACK_HEARTS, PlayerID: "4"}, + {Card: JACK_DIAMONDS, PlayerID: "4"}, + {Card: JACK_CLUBS, PlayerID: "4"}, + {Card: JACK_SPADES, PlayerID: "4"}, + {Card: JACK_HEARTS, PlayerID: "4"}, + }, + players: []Player{ + {ID: "1", TeamID: "1"}, + {ID: "2", TeamID: "2"}, + {ID: "3", TeamID: "3"}, + {ID: "4", TeamID: "4"}, + }, + goerID: "4", + expectedResult: true, + }, + { + name: "Jink - five player game", + winningCards: []PlayedCard{ + {Card: JACK_HEARTS, PlayerID: "3"}, + {Card: JACK_DIAMONDS, PlayerID: "3"}, + {Card: JACK_CLUBS, PlayerID: "3"}, + {Card: JACK_SPADES, PlayerID: "3"}, + {Card: JACK_HEARTS, PlayerID: "3"}, + }, + players: []Player{ + {ID: "1", TeamID: "1"}, + {ID: "2", TeamID: "2"}, + {ID: "3", TeamID: "3"}, + {ID: "4", TeamID: "4"}, + {ID: "5", TeamID: "5"}, + }, + goerID: "3", + expectedResult: true, + }, + { + name: "Jink - no jink", + winningCards: []PlayedCard{ + {Card: JACK_HEARTS, PlayerID: "1"}, + {Card: JACK_DIAMONDS, PlayerID: "1"}, + {Card: JACK_CLUBS, PlayerID: "3"}, + {Card: JACK_SPADES, PlayerID: "2"}, + {Card: JACK_HEARTS, PlayerID: "1"}, + }, + players: []Player{ + {ID: "1", TeamID: "1"}, + {ID: "2", TeamID: "2"}, + {ID: "3", TeamID: "3"}, + {ID: "4", TeamID: "1"}, + {ID: "5", TeamID: "2"}, + {ID: "6", TeamID: "3"}, + }, + goerID: "1", + expectedResult: false, + }, + { + name: "invalid number of cards", + winningCards: []PlayedCard{ + {Card: JACK_HEARTS, PlayerID: "1"}, + {Card: JACK_DIAMONDS, PlayerID: "1"}, + {Card: JACK_CLUBS, PlayerID: "2"}, + {Card: JACK_SPADES, PlayerID: "2"}, + }, + players: []Player{ + {ID: "1", TeamID: "1"}, + {ID: "2", TeamID: "2"}, + {ID: "3", TeamID: "3"}, + }, + goerID: "1", + expectingError: true, + }, + { + name: "invalid player ID", + winningCards: []PlayedCard{ + {Card: JACK_HEARTS, PlayerID: "invalid"}, + {Card: JACK_DIAMONDS, PlayerID: "1"}, + {Card: JACK_CLUBS, PlayerID: "2"}, + {Card: JACK_SPADES, PlayerID: "2"}, + {Card: JACK_HEARTS, PlayerID: "1"}, + }, + players: []Player{ + {ID: "1", TeamID: "1"}, + {ID: "2", TeamID: "2"}, + {ID: "3", TeamID: "3"}, + }, + goerID: "1", + expectingError: true, + }, + { + name: "invalid player ID", + winningCards: []PlayedCard{ + {Card: JACK_HEARTS, PlayerID: "1"}, + {Card: JACK_DIAMONDS, PlayerID: "invalid"}, + {Card: JACK_CLUBS, PlayerID: "2"}, + {Card: JACK_SPADES, PlayerID: "2"}, + {Card: JACK_HEARTS, PlayerID: "1"}, + }, + players: []Player{ + {ID: "1", TeamID: "1"}, + {ID: "2", TeamID: "2"}, + {ID: "3", TeamID: "3"}, + }, + goerID: "1", + expectingError: true, + }, + { + name: "jink but not for going team", + winningCards: []PlayedCard{ + {Card: JACK_HEARTS, PlayerID: "1"}, + {Card: JACK_DIAMONDS, PlayerID: "1"}, + {Card: JACK_CLUBS, PlayerID: "1"}, + {Card: JACK_SPADES, PlayerID: "1"}, + {Card: JACK_HEARTS, PlayerID: "1"}, + }, + players: []Player{ + {ID: "1", TeamID: "1"}, + {ID: "2", TeamID: "2"}, + {ID: "3", TeamID: "3"}, + }, + goerID: "2", + expectedResult: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + result, err := checkForJink(test.winningCards, test.players, test.goerID) + 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 result != test.expectedResult { + t.Errorf("expected %v, got %v", test.expectedResult, result) + } + } + }) + } +} + +func TestGameUtils_getTeamID(t *testing.T) { + tests := []struct { + name string + playerID string + players []Player + expectedResult string + expectingError bool + }{ + { + name: "Team 1", + playerID: "1", + players: []Player{{ID: "1", TeamID: "1"}}, + expectedResult: "1", + }, + { + name: "Team 2", + playerID: "2", + players: []Player{{ID: "1", TeamID: "1"}, {ID: "2", TeamID: "2"}}, + expectedResult: "2", + }, + { + name: "Team 3", + playerID: "3", + players: []Player{{ID: "1", TeamID: "1"}, {ID: "2", TeamID: "2"}, {ID: "3", TeamID: "3"}}, + expectedResult: "3", + }, + { + name: "Team 4", + playerID: "4", + players: []Player{{ID: "1", TeamID: "1"}, {ID: "2", TeamID: "2"}, {ID: "3", TeamID: "3"}, {ID: "4", TeamID: "4"}}, + expectedResult: "4", + }, + { + name: "Team 5", + playerID: "5", + players: []Player{{ID: "1", TeamID: "1"}, {ID: "2", TeamID: "2"}, {ID: "3", TeamID: "3"}, {ID: "4", TeamID: "4"}, {ID: "5", TeamID: "5"}}, + expectedResult: "5", + }, + { + name: "Team 6", + playerID: "6", + players: []Player{{ID: "1", TeamID: "1"}, {ID: "2", TeamID: "2"}, {ID: "3", TeamID: "3"}, {ID: "4", TeamID: "1"}, {ID: "5", TeamID: "2"}, {ID: "6", TeamID: "3"}}, + expectedResult: "3", + }, + { + name: "Invalid player ID", + playerID: "invalid", + players: []Player{{ID: "1", TeamID: "1"}, {ID: "2", TeamID: "2"}, {ID: "3", TeamID: "3"}, {ID: "4", TeamID: "1"}, {ID: "5", TeamID: "2"}, {ID: "6", TeamID: "3"}}, + expectingError: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + result, err := getTeamID(test.playerID, test.players) + 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 result != test.expectedResult { + t.Errorf("expected %v, got %v", test.expectedResult, result) + } + } + }) + } +} + +func TestGameUtils_findWinningTeam(t *testing.T) { + tests := []struct { + name string + players []Player + round Round + expectedResult string + expectingError bool + }{ + { + name: "Team 1 wins", + players: []Player{ + {ID: "1", TeamID: "1", Score: 110}, + {ID: "2", TeamID: "2", Score: 0}, + }, + round: Round{}, + expectedResult: "1", + }, + { + name: "Team 2 wins", + players: []Player{ + {ID: "1", TeamID: "1", Score: 0}, + {ID: "2", TeamID: "2", Score: 110}, + {ID: "3", TeamID: "3", Score: 100}, + {ID: "4", TeamID: "4", Score: 105}, + }, + expectedResult: "2", + }, + { + name: "Two teams over 110 - the goer is one of them", + players: []Player{ + {ID: "1", TeamID: "1", Score: 110}, + {ID: "2", TeamID: "2", Score: 0}, + {ID: "3", TeamID: "3", Score: 120}, + }, + round: Round{GoerID: "1"}, + expectedResult: "1", + }, + { + name: "Two teams over 110 - the goer is not one of them", + players: []Player{ + {ID: "1", TeamID: "1", Score: 110}, + {ID: "2", TeamID: "2", Score: 0}, + {ID: "3", TeamID: "3", Score: 120}, + }, + round: Round{GoerID: "2", Suit: Clubs, CompletedHands: []Hand{ + {LeadOut: JACK_HEARTS, PlayedCards: []PlayedCard{{Card: JACK_HEARTS, PlayerID: "1"}, {Card: TWO_SPADES, PlayerID: "2"}, {Card: ACE_HEARTS, PlayerID: "3"}}}, + {LeadOut: TWO_SPADES, PlayedCards: []PlayedCard{{Card: SEVEN_CLUBS, PlayerID: "1"}, {Card: THREE_SPADES, PlayerID: "2"}, {Card: SEVEN_CLUBS, PlayerID: "3"}}}, + {LeadOut: THREE_CLUBS, PlayedCards: []PlayedCard{{Card: JOKER, PlayerID: "1"}, {Card: FOUR_SPADES, PlayerID: "2"}, {Card: THREE_CLUBS, PlayerID: "3"}}}, + {LeadOut: TWO_CLUBS, PlayedCards: []PlayedCard{{Card: ACE_CLUBS, PlayerID: "1"}, {Card: TWO_CLUBS, PlayerID: "2"}, {Card: FIVE_HEARTS, PlayerID: "3"}}}, + {LeadOut: JACK_CLUBS, PlayedCards: []PlayedCard{{Card: JACK_CLUBS, PlayerID: "1"}, {Card: SIX_SPADES, PlayerID: "2"}, {Card: ACE_SPADES, PlayerID: "3"}}}, + }}, + expectedResult: "3", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + result, err := findWinningTeam(test.players, test.round) + 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 result != test.expectedResult { + t.Errorf("expected %v, got %v", test.expectedResult, result) + } + } + }) + } +} + +func TestGameUtils_getTeamsOver110(t *testing.T) { + tests := []struct { + name string + players []Player + expectedResult map[string]bool + }{ + { + name: "1 team over 110", + players: []Player{ + {ID: "1", TeamID: "1", Score: 110}, + {ID: "2", TeamID: "2", Score: 0}, + {ID: "3", TeamID: "3", Score: 100}, + }, + expectedResult: map[string]bool{"1": true}, + }, + { + name: "2 teams over 110", + players: []Player{ + {ID: "1", TeamID: "1", Score: 110}, + {ID: "2", TeamID: "2", Score: 0}, + {ID: "3", TeamID: "3", Score: 120}, + }, + expectedResult: map[string]bool{"1": true, "3": true}, + }, + { + name: "no teams over 110", + players: []Player{ + {ID: "1", TeamID: "1", Score: 100}, + {ID: "2", TeamID: "2", Score: 0}, + {ID: "3", TeamID: "3", Score: 105}, + }, + expectedResult: map[string]bool{}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + result := getTeamsOver110(test.players) + if len(result) != len(test.expectedResult) { + t.Errorf("expected %d teams, got %d", len(test.expectedResult), len(result)) + } + for k, v := range result { + if test.expectedResult[k] != v { + t.Errorf("expected %v, got %v", test.expectedResult[k], v) + } + } + }) + } +} + +func TestGameUtils_findBestTrump(t *testing.T) { + tests := []struct { + name string + cards []PlayedCard + suit Suit + expectedResult PlayedCard + expectTrump bool + expectingError bool + }{ + { + name: "Jack of Hearts", + cards: []PlayedCard{ + {Card: JACK_HEARTS, PlayerID: "1"}, + {Card: JACK_DIAMONDS, PlayerID: "2"}, + {Card: JACK_CLUBS, PlayerID: "3"}, + }, + suit: Hearts, + expectedResult: PlayedCard{Card: JACK_HEARTS, PlayerID: "1"}, + expectTrump: true, + }, + { + name: "Joker", + cards: []PlayedCard{ + {Card: JACK_HEARTS, PlayerID: "1"}, + {Card: ACE_HEARTS, PlayerID: "2"}, + {Card: JOKER, PlayerID: "3"}, + }, + suit: Diamonds, + expectedResult: PlayedCard{Card: JOKER, PlayerID: "3"}, + expectTrump: true, + }, + { + name: "Ace of Hearts", + cards: []PlayedCard{ + {Card: JACK_HEARTS, PlayerID: "1"}, + {Card: ACE_HEARTS, PlayerID: "2"}, + {Card: ACE_CLUBS, PlayerID: "3"}, + }, + suit: Clubs, + expectedResult: PlayedCard{Card: ACE_HEARTS, PlayerID: "2"}, + expectTrump: true, + }, + { + name: "No trump cards", + cards: []PlayedCard{ + {Card: JACK_HEARTS, PlayerID: "1"}, + {Card: TWO_SPADES, PlayerID: "2"}, + {Card: ACE_CLUBS, PlayerID: "3"}, + }, + suit: Diamonds, + expectTrump: false, + }, + { + name: "No cards played", + cards: []PlayedCard{}, + suit: Clubs, + expectingError: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + result, trump, err := findBestTrump(test.cards, test.suit) + 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 result != test.expectedResult { + t.Errorf("expected %v, got %v", test.expectedResult, result) + } + if trump != test.expectTrump { + t.Errorf("expected %v, got %v", test.expectTrump, trump) + } + } + }) + } +} + +func TestGameUtils_findPlayer(t *testing.T) { + tests := []struct { + name string + playerID string + players []Player + expectedResult Player + expectingError bool + }{ + { + name: "Player 1", + playerID: "1", + players: []Player{{ID: "1", TeamID: "1"}, {ID: "2", TeamID: "2"}}, + expectedResult: Player{ID: "1", TeamID: "1"}, + }, + { + name: "Player 2", + playerID: "2", + players: []Player{{ID: "1", TeamID: "1"}, {ID: "2", TeamID: "2"}}, + expectedResult: Player{ID: "2", TeamID: "2"}, + }, + { + name: "Player not found", + playerID: "3", + players: []Player{{ID: "1", TeamID: "1"}, {ID: "2", TeamID: "2"}}, + expectingError: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + result, err := findPlayer(test.playerID, test.players) + 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 result.ID != test.expectedResult.ID { + t.Errorf("expected %v, got %v", test.expectedResult, result) + } + } + }) + } +} + +func TestGameUtils_CalculateScores(t *testing.T) { + tests := []struct { + name string + winningCards []PlayedCard + players []Player + suit Suit + expectedResult map[string]int + expectingError bool + }{ + { + name: "Add up scores", + winningCards: []PlayedCard{{Card: JACK_HEARTS, PlayerID: "1"}, {Card: JACK_DIAMONDS, PlayerID: "2"}, {Card: JACK_CLUBS, PlayerID: "4"}, {Card: JACK_SPADES, PlayerID: "4"}, {Card: FIVE_HEARTS, PlayerID: "1"}}, + players: []Player{ + {ID: "1", TeamID: "1", Score: 0}, + {ID: "2", TeamID: "2", Score: 0}, + {ID: "3", TeamID: "3", Score: 0}, + {ID: "4", TeamID: "4", Score: 0}, + }, + suit: Hearts, + expectedResult: map[string]int{ + "1": 15, + "2": 5, + "4": 10, + }, + }, + { + name: "Team game", + winningCards: []PlayedCard{{Card: JACK_HEARTS, PlayerID: "1"}, {Card: JACK_DIAMONDS, PlayerID: "2"}, {Card: JACK_CLUBS, PlayerID: "4"}, {Card: JACK_SPADES, PlayerID: "4"}, {Card: FIVE_HEARTS, PlayerID: "1"}}, + players: []Player{ + {ID: "1", TeamID: "1", Score: 0}, + {ID: "2", TeamID: "2", Score: 0}, + {ID: "3", TeamID: "3", Score: 0}, + {ID: "4", TeamID: "1", Score: 0}, + {ID: "5", TeamID: "2", Score: 0}, + {ID: "6", TeamID: "3", Score: 0}, + }, + suit: Hearts, + expectedResult: map[string]int{ + "1": 25, + "2": 5, + }, + }, + { + name: "No trump cards", + winningCards: []PlayedCard{{Card: JACK_HEARTS, PlayerID: "1"}, {Card: JACK_DIAMONDS, PlayerID: "2"}, {Card: JACK_CLUBS, PlayerID: "4"}, {Card: TWO_CLUBS, PlayerID: "4"}, {Card: FIVE_HEARTS, PlayerID: "1"}}, + players: []Player{ + {ID: "1", TeamID: "1", Score: 0}, + {ID: "2", TeamID: "2", Score: 0}, + {ID: "3", TeamID: "3", Score: 0}, + {ID: "4", TeamID: "4", Score: 0}, + }, + suit: Spades, + expectedResult: map[string]int{ + "1": 10, + "2": 5, + "4": 10, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + result, err := calculateScores(test.winningCards, test.players, test.suit) + 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) + } + + for k, v := range result { + if test.expectedResult[k] != v { + t.Errorf("expected %v, got %v", test.expectedResult[k], v) + } + } + } + }) + } + +}