Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: buys cards #18

Merged
merged 1 commit into from
Jan 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading