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: adding revision #12

Merged
merged 2 commits into from
Jan 17, 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
28 changes: 21 additions & 7 deletions pkg/game/game-handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"cards-110-api/pkg/auth"
"github.com/gin-gonic/gin"
"net/http"
"strconv"
)

type Handler struct {
Expand Down Expand Up @@ -99,8 +100,10 @@ func (h *Handler) Get(c *gin.Context) {
// @ID get-game-state
// @Produce json
// @Param gameId path string true "Game ID"
// @Param revision query int false "Revision"
// @Security Bearer
// @Success 200 {object} GameState
// @Success 200 {object} State
// @Success 204 "No Content"
// @Failure 400 {object} api.ErrorResponse
// @Failure 500 {object} api.ErrorResponse
// @Router /game/{gameId}/state [get]
Expand All @@ -117,8 +120,11 @@ func (h *Handler) GetState(c *gin.Context) {
// Get the game ID from the request
gameId := c.Param("gameId")

// Get the revision from the request
revision, exists := c.GetQuery("revision")

// Get the game from the database
game, has, err := h.S.Get(ctx, gameId)
state, has, err := h.S.GetState(ctx, gameId, id)
if err != nil {
c.JSON(http.StatusInternalServerError, api.ErrorResponse{Message: err.Error()})
return
Expand All @@ -128,11 +134,19 @@ func (h *Handler) GetState(c *gin.Context) {
return
}

// Get the state for the current user
state, err := game.GetState(id)
if err != nil {
c.JSON(http.StatusInternalServerError, api.ErrorResponse{Message: err.Error()})
return
// Check if the game has been updated
if exists {
// First convert the revision to an int
// If the revision is less than or equal to the current revision, return no content
rev, err := strconv.Atoi(revision)
if err != nil {
c.JSON(http.StatusBadRequest, api.ErrorResponse{Message: "Invalid revision"})
return
}
if state.Revision <= rev {
c.Status(http.StatusNoContent)
return
}
}

c.IndentedJSON(http.StatusOK, state)
Expand Down
256 changes: 256 additions & 0 deletions pkg/game/game-methods.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
package game

import (
"fmt"
"log"
"time"
)

func (g *Game) Me(playerID string) (Player, error) {
for _, p := range g.Players {
if p.ID == playerID {
return p, nil
}
}
return Player{}, fmt.Errorf("player not found in game")
}

func (g *Game) GetState(playerID string) (State, error) {
// Get player
me, err := g.Me(playerID)
if err != nil {
return State{}, err
}

// 1. Find dummy
var dummy Player
if g.CurrentRound.GoerID == playerID {
for _, p := range g.Players {
if p.ID == "dummy" {
dummy = p
break
}
}
}

// 2. Get max call
maxCall := Pass
for _, p := range g.Players {
if p.Call > maxCall {
maxCall = p.Call
}
}

// 3. Add dummy if applicable
iamGoer := g.CurrentRound.GoerID == playerID
if iamGoer && g.CurrentRound.Status == Called && dummy.ID != "" {
me.Cards = append(me.Cards, dummy.Cards...)
}

// 4. Return player's game state
gameState := State{
ID: g.ID,
Revision: g.Revision,
Me: me,
IamSpectator: false,
IsMyGo: g.CurrentRound.CurrentHand.CurrentPlayerID == me.ID,
IamGoer: iamGoer,
IamDealer: g.CurrentRound.DealerID == me.ID,
IamAdmin: g.AdminID == me.ID,
Cards: me.Cards,
Status: g.Status,
Round: g.CurrentRound,
MaxCall: maxCall,
Players: make([]Player, 0),
}

for _, p := range g.Players {
if p.ID != "dummy" {
gameState.Players = append(gameState.Players, p)
}
}

return gameState, nil
}

func (g *Game) EndRound() error {
// Add the current round to the completed rounds
g.Completed = append(g.Completed, g.CurrentRound)

// Get next dealer
nextDealer, err := nextPlayer(g.Players, g.CurrentRound.DealerID)
if err != nil {
return err
}

// Create next hand
nextPlayer, err := nextPlayer(g.Players, nextDealer.ID)
if err != nil {
return err
}
nextHand := Hand{
Timestamp: time.Now(),
CurrentPlayerID: nextPlayer.ID,
}

// Create a new round
g.CurrentRound = Round{
Timestamp: time.Now(),
Number: g.CurrentRound.Number + 1,
DealerID: nextDealer.ID,
Status: Calling,
CurrentHand: nextHand,
}

// Deal the cards
deck := ShuffleCards(NewDeck())
g.Deck, g.Players = DealCards(deck, g.Players)

// Increment revision
g.Revision++

return nil
}

func (g *Game) Call(playerID string, call Call) error {
// Check the game is active
if g.Status != Active {
return fmt.Errorf("game not active")
}

// Check current round is calling
if g.CurrentRound.Status != Calling {
return fmt.Errorf("round not calling")
}

// Check the player is the current player
if g.CurrentRound.CurrentHand.CurrentPlayerID != playerID {
return fmt.Errorf("not current player")
}

// If they are in the bunker (score < -30) they can only pass
state, err := g.GetState(playerID)
if err != nil {
return err
}
if state.Me.Score < -30 && call != Pass {
return fmt.Errorf("player in bunker")
}

// Check the call is valid i.e. > all previous calls or a pass
// The dealer can take a call of greater than 10
if call != Pass {
callForComparison := call
if state.IamDealer {
callForComparison++
}
for _, p := range g.Players {
if p.Call >= callForComparison {
return fmt.Errorf("invalid call")
}
}
}

// Validate 10 call
if call == Ten {
if len(g.Players) != 6 {
return fmt.Errorf("can only call 10 in doubles")
}
}

// Set the player's call
for i, p := range g.Players {
if p.ID == playerID {
g.Players[i].Call = call
break
}
}

// Set next player/round status
if call == Jink {
log.Printf("Jink called by %s", playerID)
if state.IamDealer {
// If the dealer calls Jink, calling is complete
g.CurrentRound.Status = Called
g.CurrentRound.GoerID = playerID
g.CurrentRound.CurrentHand.CurrentPlayerID = playerID
} else {
// If any other player calls Jink, jump to the dealer
g.CurrentRound.CurrentHand.CurrentPlayerID = g.CurrentRound.DealerID
}
} else if state.IamDealer {
// Get the highest calls
var topCall Call
for _, p := range g.Players {
if p.Call > topCall {
topCall = p.Call
}
}

if topCall <= Ten {
log.Printf("No one called. Starting new round...")
err = g.EndRound()
return err
}

// Get the players who made the top call (the dealer may have taken a call, this will result in more than one player)
var topCallPlayers []Player
for _, p := range g.Players {
if p.Call == topCall {
topCallPlayers = append(topCallPlayers, p)
}
}
if len(topCallPlayers) == 0 || len(topCallPlayers) > 2 {
return fmt.Errorf("invalid call state. There are %d top callers of %d", len(topCallPlayers), topCall)
}
var takenPlayer Player
var caller Player
if len(topCallPlayers) == 2 {
for _, p := range topCallPlayers {
if p.ID == g.CurrentRound.DealerID {
caller = p
} else {
takenPlayer = p
}
}
} else {
caller = topCallPlayers[0]
}

if takenPlayer.ID != "" {
log.Printf("Dealer seeing call by %s", takenPlayer.ID)
g.CurrentRound.DealerSeeing = true
g.CurrentRound.CurrentHand.CurrentPlayerID = takenPlayer.ID
} else {
log.Printf("Call successful. %s is goer", caller.ID)
g.CurrentRound.Status = Called
g.CurrentRound.GoerID = caller.ID
g.CurrentRound.CurrentHand.CurrentPlayerID = caller.ID
}

} else if g.CurrentRound.DealerSeeing {
log.Printf("%s was taken by the dealer.", playerID)
if call == Pass {
log.Printf("%s is letting the dealer go.", playerID)
g.CurrentRound.Status = Called
g.CurrentRound.GoerID = g.CurrentRound.DealerID
g.CurrentRound.CurrentHand.CurrentPlayerID = g.CurrentRound.DealerID
} else {
log.Printf("%s has raised the call.", playerID)
g.CurrentRound.CurrentHand.CurrentPlayerID = g.CurrentRound.DealerID
g.CurrentRound.DealerSeeing = false
}
} else {
log.Printf("Calling not complete. Next player...")
nextPlayer, err := nextPlayer(g.Players, playerID)
if err != nil {
return err
}
g.CurrentRound.CurrentHand.CurrentPlayerID = nextPlayer.ID
}

// Increment revision
g.Revision++

return nil
}
17 changes: 17 additions & 0 deletions pkg/game/game-service.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
type ServiceI interface {
Create(ctx context.Context, playerIDs []string, name string, adminID string) (Game, error)
Get(ctx context.Context, gameId string) (Game, bool, error)
GetState(ctx context.Context, gameId string, playerId string) (State, bool, error)
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)
Expand Down Expand Up @@ -53,6 +54,22 @@ func (s *Service) Get(ctx context.Context, gameId string) (Game, bool, error) {
return s.Col.FindOne(ctx, bson.M{"_id": gameId})
}

func (s *Service) GetState(ctx context.Context, gameId string, playerId string) (State, bool, error) {
// Get the game from the database.
game, has, err := s.Get(ctx, gameId)
if err != nil || !has {
return State{}, has, err
}

// Get the state for the player.
state, err := game.GetState(playerId)
if err != nil {
return State{}, true, err
}

return state, true, nil
}

// GetAll Get all games.
func (s *Service) GetAll(ctx context.Context) ([]Game, error) {
return s.Col.Find(ctx, bson.M{})
Expand Down
Loading
Loading