Skip to content

Commit

Permalink
Merge pull request #18 from daithihearn/buy-cards
Browse files Browse the repository at this point in the history
feat: buys cards
  • Loading branch information
daithihearn authored Jan 19, 2024
2 parents 715a3b1 + f668ef5 commit bae3139
Show file tree
Hide file tree
Showing 10 changed files with 550 additions and 36 deletions.
1 change: 1 addition & 0 deletions cmd/api/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ func main() {
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.PUT("/api/v1/game/:gameId/buy", auth.EnsureValidTokenGin([]string{auth.WriteGame}), gameHandler.Buy)
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)
Expand Down
23 changes: 21 additions & 2 deletions pkg/game/deck-utils.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package game

import (
"fmt"
"math/rand"
)

Expand All @@ -15,16 +16,34 @@ func ShuffleCards(cards []CardName) []CardName {
return shuffled
}

func DealCards(deck []CardName, numPlayers int) ([]CardName, [][]CardName) {
func DealCards(deck []CardName, numPlayers int) ([]CardName, [][]CardName, error) {
hands := make([][]CardName, numPlayers+1)
// Deal the cards
for i := 0; i < 5; i++ {
for j := 0; j < numPlayers+1; j++ {
if len(deck) == 0 {
return nil, nil, fmt.Errorf("deck is empty")
}
hands[j] = append(hands[j], deck[0])
deck = deck[1:]
}
}
return deck, hands
return deck, hands, nil
}

func BuyCards(deck []CardName, cards []CardName) ([]CardName, []CardName, error) {
for {
if len(cards) == 5 {
break
}
if len(deck) == 0 {
return nil, nil, fmt.Errorf("deck is empty")
}

cards = append(cards, deck[0])
deck = deck[1:]
}
return deck, cards, nil
}

func NewDeck() []CardName {
Expand Down
176 changes: 176 additions & 0 deletions pkg/game/deck-utils_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
package game

import "testing"

func TestDeck_ShuffleCards(t *testing.T) {
tests := []struct {
name string
cards []CardName
}{
{
name: "Empty deck",
cards: []CardName{},
},
{
name: "Single card",
cards: []CardName{ACE_HEARTS},
},
{
name: "Multiple cards",
cards: []CardName{ACE_HEARTS, TWO_HEARTS, THREE_HEARTS, FOUR_HEARTS, FIVE_HEARTS, SIX_HEARTS, SEVEN_HEARTS, EIGHT_HEARTS, NINE_HEARTS, TEN_HEARTS, JACK_HEARTS, QUEEN_HEARTS, KING_HEARTS, ACE_DIAMONDS, TWO_DIAMONDS, THREE_DIAMONDS, FOUR_DIAMONDS, FIVE_DIAMONDS, SIX_DIAMONDS, SEVEN_DIAMONDS, EIGHT_DIAMONDS, NINE_DIAMONDS, TEN_DIAMONDS, JACK_DIAMONDS, QUEEN_DIAMONDS, KING_DIAMONDS, ACE_CLUBS, TWO_CLUBS, THREE_CLUBS, FOUR_CLUBS, FIVE_CLUBS, SIX_CLUBS, SEVEN_CLUBS, EIGHT_CLUBS, NINE_CLUBS, TEN_CLUBS, JACK_CLUBS, QUEEN_CLUBS, KING_CLUBS, ACE_SPADES, TWO_SPADES, THREE_SPADES, FOUR_SPADES, FIVE_SPADES, SIX_SPADES, SEVEN_SPADES, EIGHT_SPADES, NINE_SPADES, TEN_SPADES, JACK_SPADES, QUEEN_SPADES, KING_SPADES, JOKER},
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
shuffled := ShuffleCards(test.cards)
if len(shuffled) != len(test.cards) {
t.Errorf("expected %d cards, got %d", len(test.cards), len(shuffled))
}

if !compare(shuffled, test.cards) {
t.Errorf("expected cards to be the same, got %v", shuffled)
}

// Cards should not be in the same order as they started (unless the deck is empty or has a small number of cards)
if len(test.cards) > 7 {
var sameOrder = true
for i, card := range test.cards {
if card != shuffled[i] {
sameOrder = false
break
}
}
if sameOrder {
t.Errorf("expected cards to be shuffled, got %v", shuffled)
}
}
})
}
}

func TestDeck_DealCards(t *testing.T) {
tests := []struct {
name string
deck []CardName
numPlayers int
expectError bool
}{
{
name: "Empty deck",
deck: []CardName{},
numPlayers: 2,
expectError: true,
},
{
name: "Not enough cards",
deck: []CardName{ACE_HEARTS},
numPlayers: 2,
expectError: true,
},
{
name: "Multiple cards",
deck: []CardName{ACE_HEARTS, TWO_HEARTS, THREE_HEARTS, FOUR_HEARTS, FIVE_HEARTS, SIX_HEARTS, SEVEN_HEARTS, EIGHT_HEARTS, NINE_HEARTS, TEN_HEARTS, JACK_HEARTS, QUEEN_HEARTS, KING_HEARTS, ACE_DIAMONDS, TWO_DIAMONDS, THREE_DIAMONDS, FOUR_DIAMONDS, FIVE_DIAMONDS, SIX_DIAMONDS, SEVEN_DIAMONDS, EIGHT_DIAMONDS, NINE_DIAMONDS, TEN_DIAMONDS, JACK_DIAMONDS, QUEEN_DIAMONDS, KING_DIAMONDS, ACE_CLUBS, TWO_CLUBS, THREE_CLUBS, FOUR_CLUBS, FIVE_CLUBS, SIX_CLUBS, SEVEN_CLUBS, EIGHT_CLUBS, NINE_CLUBS, TEN_CLUBS, JACK_CLUBS, QUEEN_CLUBS, KING_CLUBS, ACE_SPADES, TWO_SPADES, THREE_SPADES, FOUR_SPADES, FIVE_SPADES, SIX_SPADES, SEVEN_SPADES, EIGHT_SPADES, NINE_SPADES, TEN_SPADES, JACK_SPADES, QUEEN_SPADES, KING_SPADES, JOKER},
numPlayers: 2,
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
deck, hands, err := DealCards(test.deck, test.numPlayers)

if test.expectError {
if err == nil {
t.Errorf("expected an error")
}
} else {
if err != nil {
t.Errorf("expected no error, got %v", err)
}

// The output deck and hands should have all the same cards as the input deck
var outputCards = deck
for _, hand := range hands {
outputCards = append(outputCards, hand...)
}
if !compare(outputCards, test.deck) {
t.Errorf("expected cards to be the same, got %v", outputCards)
}
}
})
}
}

func TestDeck_BuyCards(t *testing.T) {
tests := []struct {
name string
deck []CardName
cards []CardName
expectError bool
expectedDeck []CardName
expectedCards []CardName
}{
{
name: "Empty deck",
deck: []CardName{},
cards: []CardName{},
expectError: true,
},
{
name: "Not enough cards",
deck: []CardName{ACE_HEARTS},
cards: []CardName{},
expectError: true,
},
{
name: "Not enough cards",
deck: []CardName{ACE_DIAMONDS},
cards: []CardName{ACE_HEARTS},
expectError: true,
},
{
name: "Enough cards",
deck: []CardName{ACE_DIAMONDS},
cards: []CardName{ACE_HEARTS, TWO_HEARTS, THREE_HEARTS, FOUR_HEARTS},
expectedDeck: []CardName{},
expectedCards: []CardName{ACE_HEARTS, TWO_HEARTS, THREE_HEARTS, FOUR_HEARTS, ACE_DIAMONDS},
},
{
name: "Loads of cards",
deck: []CardName{ACE_HEARTS, TWO_HEARTS, THREE_HEARTS, FOUR_HEARTS, FIVE_HEARTS, SIX_HEARTS, SEVEN_HEARTS, EIGHT_HEARTS, NINE_HEARTS, TEN_HEARTS, JACK_HEARTS, QUEEN_HEARTS, KING_HEARTS, ACE_DIAMONDS, TWO_DIAMONDS, THREE_DIAMONDS, FOUR_DIAMONDS, FIVE_DIAMONDS, SIX_DIAMONDS, SEVEN_DIAMONDS, EIGHT_DIAMONDS, NINE_DIAMONDS, TEN_DIAMONDS, JACK_DIAMONDS, QUEEN_DIAMONDS, KING_DIAMONDS, ACE_CLUBS, TWO_CLUBS, THREE_CLUBS, FOUR_CLUBS, FIVE_CLUBS, SIX_CLUBS, SEVEN_CLUBS, EIGHT_CLUBS, NINE_CLUBS, TEN_CLUBS, JACK_CLUBS, QUEEN_CLUBS, KING_CLUBS, ACE_SPADES, TWO_SPADES, THREE_SPADES, FOUR_SPADES, FIVE_SPADES, SIX_SPADES, SEVEN_SPADES, EIGHT_SPADES, NINE_SPADES, TEN_SPADES, JACK_SPADES, QUEEN_SPADES, KING_SPADES, JOKER},
cards: []CardName{},
expectedDeck: []CardName{SIX_HEARTS, SEVEN_HEARTS, EIGHT_HEARTS, NINE_HEARTS, TEN_HEARTS, JACK_HEARTS, QUEEN_HEARTS, KING_HEARTS, ACE_DIAMONDS, TWO_DIAMONDS, THREE_DIAMONDS, FOUR_DIAMONDS, FIVE_DIAMONDS, SIX_DIAMONDS, SEVEN_DIAMONDS, EIGHT_DIAMONDS, NINE_DIAMONDS, TEN_DIAMONDS, JACK_DIAMONDS, QUEEN_DIAMONDS, KING_DIAMONDS, ACE_CLUBS, TWO_CLUBS, THREE_CLUBS, FOUR_CLUBS, FIVE_CLUBS, SIX_CLUBS, SEVEN_CLUBS, EIGHT_CLUBS, NINE_CLUBS, TEN_CLUBS, JACK_CLUBS, QUEEN_CLUBS, KING_CLUBS, ACE_SPADES, TWO_SPADES, THREE_SPADES, FOUR_SPADES, FIVE_SPADES, SIX_SPADES, SEVEN_SPADES, EIGHT_SPADES, NINE_SPADES, TEN_SPADES, JACK_SPADES, QUEEN_SPADES, KING_SPADES, JOKER},
expectedCards: []CardName{ACE_HEARTS, TWO_HEARTS, THREE_HEARTS, FOUR_HEARTS, FIVE_HEARTS},
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
deck, cards, err := BuyCards(test.deck, test.cards)

if test.expectError {
if err == nil {
t.Errorf("expected an error")
}
} else {
if err != nil {
t.Errorf("expected no error, got %v", err)
}

if !compare(deck, test.expectedDeck) {
t.Errorf("expected deck to be %v, got %v", test.expectedDeck, deck)
}
if !compare(cards, test.expectedCards) {
t.Errorf("expected cards to be %v, got %v", test.expectedCards, cards)
}
}
})
}
}

func TestDeck_NewDeck(t *testing.T) {
deck := NewDeck()
if len(deck) != 53 {
t.Errorf("expected 53 cards, got %d", len(deck))
}
}
51 changes: 51 additions & 0 deletions pkg/game/game-handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -320,3 +320,54 @@ func (h *Handler) SelectSuit(c *gin.Context) {
}
c.IndentedJSON(http.StatusOK, state)
}

type BuyRequest struct {
Cards []CardName `json:"cards"`
}

// Buy @Summary Buy cards
// @Description When in the Buying state, the Goer can buy cards from the deck
// @Tags Game
// @ID buy
// @Produce json
// @Security Bearer
// @Param gameId path string true "Game ID"
// @Para body BuyRequest true "Buy Request"
// @Success 200 {object} State
// @Failure 400 {object} api.ErrorResponse
// @Failure 500 {object} api.ErrorResponse
// @Router /game/{gameId}/buy [put]
func (h *Handler) Buy(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 BuyRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, api.ErrorResponse{Message: err.Error()})
return
}

// Buy the cards
game, err := h.S.Buy(ctx, gameId, id, 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)
}
75 changes: 74 additions & 1 deletion pkg/game/game-methods.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,10 @@ func (g *Game) EndRound() error {
}

// Deal the cards
deck, hands := DealCards(ShuffleCards(NewDeck()), len(g.Players))
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) {
Expand Down Expand Up @@ -320,3 +323,73 @@ func (g *Game) SelectSuit(playerID string, suit Suit, cards []CardName) error {

return nil
}

func (g *Game) Buy(id string, cards []CardName) error {
// Verify the at the round is in the buying state
if g.CurrentRound.Status != Buying {
return fmt.Errorf("round must be in the buying state to buy cards")
}

// Verify that is the player's go
if g.CurrentRound.CurrentHand.CurrentPlayerID != id {
return fmt.Errorf("only the current player can buy cards")
}

// 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(id)
if err != nil {
return err
}
if !containsAllUnique(state.Cards, cards) {
return fmt.Errorf("invalid card selected")
}

// Get cards from the deck so the player has 5 cards
deck, cards, err := BuyCards(g.Deck, cards)
if err != nil {
return err
}

g.Deck = deck

// Set my cards
for i, p := range g.Players {
if p.ID == id {
g.Players[i].Cards = cards
break
}
}

// If the current player is the dealer update the round status to playing
if g.CurrentRound.DealerID == id {
g.CurrentRound.Status = Playing

// Set the next player when the dealer buys
np, err := nextPlayer(g.Players, g.CurrentRound.GoerID)
if err != nil {
return err
}
g.CurrentRound.CurrentHand.CurrentPlayerID = np.ID
} else {
// Set the next player
np, err := nextPlayer(g.Players, id)
if err != nil {
return err
}
g.CurrentRound.CurrentHand.CurrentPlayerID = np.ID
}

// Increment revision
g.Revision++

return nil
}
Loading

0 comments on commit bae3139

Please sign in to comment.