diff --git a/exampleclient/ui.go b/exampleclient/ui.go index b9d2ed9..7f68066 100644 --- a/exampleclient/ui.go +++ b/exampleclient/ui.go @@ -39,7 +39,7 @@ func (u *ui) play(playerID int, gameState truco.GameState) (truco.Action, error) return nil, err } - if gameState.IsEnded { + if gameState.IsGameEnded { return nil, nil } @@ -76,11 +76,13 @@ func (u *ui) render(playerID int, state truco.GameState, mode printMode) error { mx, my = termbox.Size() you = playerID them = state.OpponentOf(you) - hand = *state.Hands[them] + hand = *state.Players[them].Hand ) if mode == PRINT_MODE_SHOW_ROUND_RESULT { - hand = *state.HandsDealt[len(state.HandsDealt)-2][them] + // Note that RoundNumber has already been incremented + // so we need to get the previous round's hands. + hand = *state.RoundsLog[state.RoundNumber-1].HandsDealt[them] } var ( @@ -101,13 +103,15 @@ func (u *ui) render(playerID int, state truco.GameState, mode printMode) error { themMano = " (mano)" } - printUpToAt(mx-1, 1, fmt.Sprintf("Vos%v %v", youMano, spanishScore(state.Scores[you]))) - printUpToAt(mx-1, 2, fmt.Sprintf("Elle%v %v", themMano, spanishScore(state.Scores[them]))) + printUpToAt(mx-1, 1, fmt.Sprintf("Vos%v %v", youMano, spanishScore(state.Players[you].Score))) + printUpToAt(mx-1, 2, fmt.Sprintf("Elle%v %v", themMano, spanishScore(state.Players[them].Score))) - hand = *state.Hands[you] + hand = *state.Players[you].Hand if mode == PRINT_MODE_SHOW_ROUND_RESULT { - hand = *state.HandsDealt[len(state.HandsDealt)-2][you] + // Note that RoundNumber has already been incremented + // so we need to get the previous round's hands. + hand = *state.RoundsLog[state.RoundNumber-1].HandsDealt[you] } unrevealed = getCardsString(hand.Unrevealed, false, false) @@ -125,27 +129,27 @@ func (u *ui) render(playerID int, state truco.GameState, mode printMode) error { printAt(0, my/2, lastActionString) case PRINT_MODE_SHOW_ROUND_RESULT: - lastActionString, err := getActionString(state.Actions[len(state.Actions)-1], state.ActionOwnerPlayerIDs[len(state.ActionOwnerPlayerIDs)-1], you) + lastRoundLog := state.RoundsLog[state.RoundNumber-1] + lastActionString, err := getActionString(lastRoundLog.ActionsLog[len(lastRoundLog.ActionsLog)-1], you) if err != nil { return err } printAt(0, my/2, lastActionString) - lastRoundResult := state.RoundResults[len(state.RoundResults)-1] envidoPart := "el envido no se jugó" - if lastRoundResult.EnvidoWinnerPlayerID != -1 { + if lastRoundLog.EnvidoWinnerPlayerID != -1 { envidoWinner := "vos" won := "ganaste" - if lastRoundResult.EnvidoWinnerPlayerID == them { + if lastRoundLog.EnvidoWinnerPlayerID == them { envidoWinner = "elle" won = "ganó" } - envidoPart = fmt.Sprintf("%v %v %v puntos por el envido", envidoWinner, won, lastRoundResult.EnvidoPoints) + envidoPart = fmt.Sprintf("%v %v %v puntos por el envido", envidoWinner, won, lastRoundLog.EnvidoPoints) } trucoWinner := "vos" won := "ganaste" - if lastRoundResult.TrucoWinnerPlayerID == them { + if lastRoundLog.TrucoWinnerPlayerID == them { trucoWinner = "elle" won = "ganó" } @@ -155,11 +159,12 @@ func (u *ui) render(playerID int, state truco.GameState, mode printMode) error { envidoPart, trucoWinner, won, - lastRoundResult.TrucoPoints, + lastRoundLog.TrucoPoints, ) printAt(0, my/2+1, result) case PRINT_MODE_END: - lastActionString, err := getActionString(state.Actions[len(state.Actions)-1], state.ActionOwnerPlayerIDs[len(state.ActionOwnerPlayerIDs)-1], you) + lastActionLog := state.RoundsLog[state.RoundNumber].ActionsLog[len(state.RoundsLog[state.RoundNumber].ActionsLog)-1] + lastActionString, err := getActionString(lastActionLog, you) if err != nil { return err } @@ -182,7 +187,7 @@ func (u *ui) render(playerID int, state truco.GameState, mode printMode) error { if mode == PRINT_MODE_NORMAL { actionsString := "" for i, action := range _deserializeActions(state.PossibleActions) { - action := spanishAction(action, state) + action := spanishAction(action) actionsString += fmt.Sprintf("%d. %s ", i+1, action) } printAt(0, my-2, actionsString) @@ -246,20 +251,21 @@ func suitEmoji(suit string) string { } func getLastActionString(playerID int, state truco.GameState) (string, error) { - if len(state.Actions) == 0 { - return "¡Empezó el juego!", nil - } - if state.RoundJustStarted { + actionsLog := state.RoundsLog[state.RoundNumber].ActionsLog + + if len(actionsLog) == 0 { + if state.RoundNumber == 1 { + return "¡Empezó el juego!", nil + } return "¡Empezó la mano!", nil } - lastActionBs := state.Actions[len(state.Actions)-1] - lastActionOwnerPlayerID := state.ActionOwnerPlayerIDs[len(state.ActionOwnerPlayerIDs)-1] - return getActionString(lastActionBs, lastActionOwnerPlayerID, playerID) + lastActionLog := actionsLog[len(actionsLog)-1] + return getActionString(lastActionLog, playerID) } -func getActionString(lastActionBs json.RawMessage, lastActionOwnerPlayerID int, playerID int) (string, error) { - lastAction, err := truco.DeserializeAction(lastActionBs) +func getActionString(log truco.ActionLog, playerID int) (string, error) { + lastAction, err := truco.DeserializeAction(log.Action) if err != nil { return "", err } @@ -267,7 +273,7 @@ func getActionString(lastActionBs json.RawMessage, lastActionOwnerPlayerID int, said := "dijiste" revealed := "tiraste" who := "Vos" - if playerID != lastActionOwnerPlayerID { + if playerID != log.PlayerID { who = "Elle" said = "dijo" revealed = "tiró" @@ -370,7 +376,7 @@ func spanishScore(score int) string { return fmt.Sprintf("%d buenas", score-14) } -func spanishAction(action truco.Action, state truco.GameState) string { +func spanishAction(action truco.Action) string { switch action.GetName() { case truco.REVEAL_CARD: _action := action.(*truco.ActionRevealCard) diff --git a/exampleclient/websocket_client.go b/exampleclient/websocket_client.go index 91f714a..98c53c0 100644 --- a/exampleclient/websocket_client.go +++ b/exampleclient/websocket_client.go @@ -30,7 +30,7 @@ func Player(playerID int, address string) { log.Fatal(err) } - if gameState.IsEnded { + if gameState.IsGameEnded { _ = ui.render(playerID, *gameState, PRINT_MODE_END) return } diff --git a/truco/action_any_quiero.go b/truco/action_any_quiero.go index f0ca87a..e1ca999 100644 --- a/truco/action_any_quiero.go +++ b/truco/action_any_quiero.go @@ -9,14 +9,14 @@ type ActionSayTrucoQuiero struct{ act } type ActionSayTrucoNoQuiero struct{ act } func (a ActionSayEnvidoNoQuiero) IsPossible(g GameState) bool { - if g.EnvidoFinished { + if g.IsEnvidoFinished { return false } return g.EnvidoSequence.CanAddStep(a.GetName()) } func (a ActionSayEnvidoQuiero) IsPossible(g GameState) bool { - if g.EnvidoFinished { + if g.IsEnvidoFinished { return false } return g.EnvidoSequence.CanAddStep(a.GetName()) @@ -53,12 +53,14 @@ func (a ActionSayEnvidoNoQuiero) Run(g *GameState) error { return errActionNotPossible } g.EnvidoSequence.AddStep(a.GetName()) - g.EnvidoFinished = true - cost, err := g.EnvidoSequence.Cost(g.CurrentPlayerScore(), g.OpponentPlayerScore()) + g.IsEnvidoFinished = true + cost, err := g.EnvidoSequence.Cost(g.Players[g.TurnPlayerID].Score, g.Players[g.TurnOpponentPlayerID].Score) if err != nil { return err } - g.Scores[g.OpponentPlayerID()] += cost + g.RoundsLog[g.RoundNumber].EnvidoPoints = cost + g.RoundsLog[g.RoundNumber].EnvidoWinnerPlayerID = g.TurnOpponentPlayerID + g.Players[g.TurnOpponentPlayerID].Score += cost return nil } @@ -75,7 +77,7 @@ func (a ActionSayTrucoQuiero) Run(g *GameState) error { return errActionNotPossible } g.TrucoSequence.AddStep(a.GetName()) - g.TrucoQuieroOwnerPlayerId = g.TurnPlayerID + g.TrucoSequence.QuieroOwnerPlayerID = g.TurnPlayerID return nil } @@ -84,14 +86,14 @@ func (a ActionSayTrucoNoQuiero) Run(g *GameState) error { return errActionNotPossible } g.TrucoSequence.AddStep(a.GetName()) - g.RoundFinished = true + g.IsRoundFinished = true cost, err := g.TrucoSequence.Cost() if err != nil { return err } - g.CurrentRoundResult.TrucoPoints = cost - g.CurrentRoundResult.TrucoWinnerPlayerID = g.OpponentPlayerID() - g.Scores[g.OpponentPlayerID()] += cost + g.RoundsLog[g.RoundNumber].TrucoPoints = cost + g.RoundsLog[g.RoundNumber].TrucoWinnerPlayerID = g.TurnOpponentPlayerID + g.Players[g.TurnOpponentPlayerID].Score += cost return nil } diff --git a/truco/action_me_voy_al_mazo.go b/truco/action_me_voy_al_mazo.go index 4d830af..e89b657 100644 --- a/truco/action_me_voy_al_mazo.go +++ b/truco/action_me_voy_al_mazo.go @@ -5,10 +5,10 @@ type ActionSayMeVoyAlMazo struct { } func (a ActionSayMeVoyAlMazo) IsPossible(g GameState) bool { - if !g.EnvidoSequence.IsEmpty() && !g.EnvidoFinished && !g.EnvidoSequence.IsFinished() { + if !g.EnvidoSequence.IsEmpty() && !g.IsEnvidoFinished && !g.EnvidoSequence.IsFinished() { return false } - if g.EnvidoFinished && !g.TrucoSequence.IsEmpty() && !g.TrucoSequence.IsFinished() { + if g.IsEnvidoFinished && !g.TrucoSequence.IsEmpty() && !g.TrucoSequence.IsFinished() { return false } return true @@ -16,23 +16,23 @@ func (a ActionSayMeVoyAlMazo) IsPossible(g GameState) bool { func (a ActionSayMeVoyAlMazo) Run(g *GameState) error { var cost int - if g.TrucoSequence.IsEmpty() && g.EnvidoFinished { + if g.TrucoSequence.IsEmpty() && g.IsEnvidoFinished { // Envido is finished, so either the envido cost was updated already, or it's zero cost = 1 } - if g.EnvidoSequence.IsEmpty() && g.TrucoSequence.IsEmpty() && !g.EnvidoFinished { + if g.EnvidoSequence.IsEmpty() && g.TrucoSequence.IsEmpty() && !g.IsEnvidoFinished { cost = 2 } - if g.EnvidoFinished && !g.TrucoSequence.IsEmpty() { + if g.IsEnvidoFinished && !g.TrucoSequence.IsEmpty() { var err error cost, err = g.TrucoSequence.Cost() if err != nil { return err } } - g.CurrentRoundResult.TrucoPoints = cost - g.CurrentRoundResult.TrucoWinnerPlayerID = g.OpponentPlayerID() - g.Scores[g.OpponentPlayerID()] += cost - g.RoundFinished = true + g.RoundsLog[g.RoundNumber].TrucoPoints = cost + g.RoundsLog[g.RoundNumber].TrucoWinnerPlayerID = g.TurnOpponentPlayerID + g.Players[g.TurnOpponentPlayerID].Score += cost + g.IsRoundFinished = true return nil } diff --git a/truco/action_reveal_card.go b/truco/action_reveal_card.go index d16cd41..479354b 100644 --- a/truco/action_reveal_card.go +++ b/truco/action_reveal_card.go @@ -11,7 +11,7 @@ func NewActionRevealCard(card Card) ActionRevealCard { func (a ActionRevealCard) IsPossible(g GameState) bool { // If envido was said and it hasn't finished, then the card can't be revealed - if !g.EnvidoFinished && !g.EnvidoSequence.IsEmpty() && !g.EnvidoSequence.IsFinished() { + if !g.IsEnvidoFinished && !g.EnvidoSequence.IsEmpty() && !g.EnvidoSequence.IsFinished() { return false } @@ -22,7 +22,7 @@ func (a ActionRevealCard) IsPossible(g GameState) bool { step := CardRevealSequenceStep{ card: a.Card, - playerID: g.CurrentPlayerID(), + playerID: g.TurnPlayerID, } return g.CardRevealSequence.CanAddStep(step, g) @@ -34,15 +34,15 @@ func (a ActionRevealCard) Run(g *GameState) error { } step := CardRevealSequenceStep{ card: a.Card, - playerID: g.CurrentPlayerID(), + playerID: g.TurnPlayerID, } g.CardRevealSequence.AddStep(step, *g) - err := g.Hands[g.CurrentPlayerID()].RevealCard(a.Card) + err := g.Players[g.TurnPlayerID].Hand.RevealCard(a.Card) if err != nil { return err } if g.CardRevealSequence.IsFinished() { - g.RoundFinished = true + g.IsRoundFinished = true var score int @@ -57,13 +57,13 @@ func (a ActionRevealCard) Run(g *GameState) error { score = cost } - g.Scores[g.CardRevealSequence.WinnerPlayerID()] += score - g.CurrentRoundResult.TrucoPoints = score - g.CurrentRoundResult.TrucoWinnerPlayerID = g.CardRevealSequence.WinnerPlayerID() + g.Players[g.CardRevealSequence.WinnerPlayerID()].Score += score + g.RoundsLog[g.RoundNumber].TrucoPoints = score + g.RoundsLog[g.RoundNumber].TrucoWinnerPlayerID = g.CardRevealSequence.WinnerPlayerID() } // If both players have revealed a card, then envido cannot be played anymore - if !g.EnvidoFinished && len(g.Hands[g.TurnPlayerID].Revealed) >= 1 && len(g.Hands[g.OpponentOf(g.TurnPlayerID)].Revealed) >= 1 { - g.EnvidoFinished = true + if !g.IsEnvidoFinished && len(g.Players[g.TurnPlayerID].Hand.Revealed) >= 1 && len(g.Players[g.TurnOpponentPlayerID].Hand.Revealed) >= 1 { + g.IsEnvidoFinished = true } return nil } diff --git a/truco/action_son_buenas.go b/truco/action_son_buenas.go index 74cfa88..93b9447 100644 --- a/truco/action_son_buenas.go +++ b/truco/action_son_buenas.go @@ -6,16 +6,16 @@ type ActionSaySonBuenas struct { } func (a ActionSaySonBuenas) IsPossible(g GameState) bool { - if g.EnvidoFinished { + if g.IsEnvidoFinished { return false } var ( mano = g.RoundTurnPlayerID me = g.TurnPlayerID - other = g.OpponentOf(g.TurnPlayerID) - meScore = g.Hands[me].EnvidoScore() - otherScore = g.Hands[other].EnvidoScore() + other = g.TurnOpponentPlayerID + meScore = g.Players[me].Hand.EnvidoScore() + otherScore = g.Players[other].Hand.EnvidoScore() ) // TODO: should I allow people to lose voluntarily? @@ -35,15 +35,14 @@ func (a ActionSaySonBuenas) Run(g *GameState) error { return errActionNotPossible } g.EnvidoSequence.AddStep(a.GetName()) - cost, err := g.EnvidoSequence.Cost(g.CurrentPlayerScore(), g.OpponentPlayerScore()) + cost, err := g.EnvidoSequence.Cost(g.Players[g.TurnPlayerID].Score, g.Players[g.TurnOpponentPlayerID].Score) if err != nil { return err } - g.CurrentRoundResult.EnvidoPoints = cost - g.CurrentRoundResult.EnvidoWinnerPlayerID = g.OpponentOf(g.CurrentPlayerID()) - g.EnvidoWinnerPlayerID = g.OpponentOf(g.CurrentPlayerID()) - g.EnvidoFinished = true - g.Scores[g.OpponentPlayerID()] += cost + g.RoundsLog[g.RoundNumber].EnvidoPoints = cost + g.RoundsLog[g.RoundNumber].EnvidoWinnerPlayerID = g.TurnOpponentPlayerID + g.IsEnvidoFinished = true + g.Players[g.TurnOpponentPlayerID].Score += cost return nil } diff --git a/truco/action_son_mejores.go b/truco/action_son_mejores.go index d55f6a6..6ceb938 100644 --- a/truco/action_son_mejores.go +++ b/truco/action_son_mejores.go @@ -6,15 +6,15 @@ type ActionSaySonMejores struct { } func (a ActionSaySonMejores) IsPossible(g GameState) bool { - if g.EnvidoFinished { + if g.IsEnvidoFinished { return false } var ( mano = g.RoundTurnPlayerID me = g.TurnPlayerID - other = g.OpponentOf(g.TurnPlayerID) - meScore = g.Hands[me].EnvidoScore() - otherScore = g.Hands[other].EnvidoScore() + other = g.TurnOpponentPlayerID + meScore = g.Players[me].Hand.EnvidoScore() + otherScore = g.Players[other].Hand.EnvidoScore() ) // TODO: should I allow people to lose voluntarily? @@ -34,15 +34,14 @@ func (a ActionSaySonMejores) Run(g *GameState) error { return errActionNotPossible } g.EnvidoSequence.AddStep(a.GetName()) - cost, err := g.EnvidoSequence.Cost(g.CurrentPlayerScore(), g.OpponentPlayerScore()) + cost, err := g.EnvidoSequence.Cost(g.Players[g.TurnPlayerID].Score, g.Players[g.TurnOpponentPlayerID].Score) if err != nil { return err } - g.CurrentRoundResult.EnvidoPoints = cost - g.CurrentRoundResult.EnvidoWinnerPlayerID = g.OpponentOf(g.CurrentPlayerID()) - g.EnvidoWinnerPlayerID = g.CurrentPlayerID() - g.EnvidoFinished = true - g.Scores[g.CurrentPlayerID()] += cost + g.RoundsLog[g.RoundNumber].EnvidoPoints = cost + g.RoundsLog[g.RoundNumber].EnvidoWinnerPlayerID = g.TurnPlayerID + g.IsEnvidoFinished = true + g.Players[g.TurnPlayerID].Score += cost return nil } diff --git a/truco/actions_any_envido.go b/truco/actions_any_envido.go index 1a985c0..00eadbc 100644 --- a/truco/actions_any_envido.go +++ b/truco/actions_any_envido.go @@ -13,7 +13,7 @@ func (a ActionSayFaltaEnvido) Run(g *GameState) error { return g.AnyEnvidoAction func (a ActionSayRealEnvido) Run(g *GameState) error { return g.AnyEnvidoActionTypeRunAction(a) } func (g GameState) AnyEnvidoActionTypeIsPossible(a Action) bool { - if g.EnvidoFinished { + if g.IsEnvidoFinished { return false } // If there was a "truco" and an answer to it, regardless when, envido is not possible anymore. @@ -28,7 +28,7 @@ func (g GameState) AnyEnvidoActionTypeIsPossible(a Action) bool { } func (g *GameState) AnyEnvidoActionTypeRunAction(a Action) error { - if g.EnvidoFinished { + if g.IsEnvidoFinished { return errEnvidoFinished } if !g.AnyEnvidoActionTypeIsPossible(a) { diff --git a/truco/actions_any_truco.go b/truco/actions_any_truco.go index bbf6b07..8476168 100644 --- a/truco/actions_any_truco.go +++ b/truco/actions_any_truco.go @@ -13,22 +13,25 @@ func (a ActionSayQuieroRetruco) Run(g *GameState) error { return g.AnyTrucoAc func (a ActionSayQuieroValeCuatro) Run(g *GameState) error { return g.AnyTrucoActionRunAction(a) } func (g GameState) AnyTrucoActionIsPossible(a Action) bool { - if !g.EnvidoSequence.IsEmpty() && !g.EnvidoFinished { + if !g.EnvidoSequence.IsEmpty() && !g.IsEnvidoFinished { return false } // Only the player who said "quiero" last can raise the stakes, unless quiero hasn't been said yet, // which can only happen if the last action is "truco" - if !g.IsLastActionOfName(SAY_TRUCO) && (a.GetName() == SAY_QUIERO_RETRUCO || a.GetName() == SAY_QUIERO_VALE_CUATRO) && g.TrucoQuieroOwnerPlayerId != g.TurnPlayerID { + if !g.IsLastActionOfName(SAY_TRUCO) && + (a.GetName() == SAY_QUIERO_RETRUCO || a.GetName() == SAY_QUIERO_VALE_CUATRO) && + g.TrucoSequence.QuieroOwnerPlayerID != g.TurnPlayerID { return false } return g.TrucoSequence.CanAddStep(a.GetName()) } func (g GameState) IsLastActionOfName(name string) bool { - if len(g.Actions) == 0 { + actionsLog := g.RoundsLog[g.RoundNumber].ActionsLog + if len(actionsLog) == 0 { return false } - lastActionBs := g.Actions[len(g.Actions)-1] + lastActionBs := actionsLog[len(actionsLog)-1].Action lastAction, err := DeserializeAction(lastActionBs) if err != nil { return false diff --git a/truco/card_reveal_sequence.go b/truco/card_reveal_sequence.go index e383550..d657c33 100644 --- a/truco/card_reveal_sequence.go +++ b/truco/card_reveal_sequence.go @@ -12,11 +12,11 @@ type CardRevealSequence struct { func (crs CardRevealSequence) CanAddStep(step CardRevealSequenceStep, g GameState) bool { // Sanity check: the action's player must be the current player - if g.CurrentPlayerID() != step.playerID { + if g.TurnPlayerID != step.playerID { return false } // Sanity check: the card must be in the player's hand, and it must be unrevealed - if !g.Hands[step.playerID].HasUnrevealedCard(step.card) { + if !g.Players[step.playerID].Hand.HasUnrevealedCard(step.card) { return false } // Sanity check: the sequence must not be finished (i.e. neither player must have won) @@ -27,7 +27,7 @@ func (crs CardRevealSequence) CanAddStep(step CardRevealSequenceStep, g GameStat case 0: // Sanity check: if there are no steps, the first step must be from the rounds's first player return step.playerID == g.RoundTurnPlayerID case 1: // If there is one step, the second step must be from the round's second player - return step.playerID == g.RoundTurnOpponentPlayerID() + return step.playerID == g.OpponentOf((g.RoundTurnPlayerID)) case 2: // If there are two steps, the third step must be from the first faceoff winner, or round's first player if tied if crs.BistepWinners[0] == -1 { return step.playerID == g.RoundTurnPlayerID @@ -35,7 +35,7 @@ func (crs CardRevealSequence) CanAddStep(step CardRevealSequenceStep, g GameStat return step.playerID == crs.BistepWinners[0] case 3: // If there are 3 steps, the 4th step must be from the first faceoff winner's opponent, or round's second player if tied if crs.BistepWinners[0] == -1 { - return step.playerID == g.RoundTurnOpponentPlayerID() + return step.playerID == g.OpponentOf((g.RoundTurnPlayerID)) } return step.playerID == g.OpponentOf(crs.BistepWinners[0]) case 4: diff --git a/truco/deck_test.go b/truco/deck_test.go index 4ec36ae..be5913a 100644 --- a/truco/deck_test.go +++ b/truco/deck_test.go @@ -109,8 +109,8 @@ func TestEnvidoScore(t *testing.T) { t.Run(fmt.Sprintf("%v vs %v", tt.hands[0], tt.hands[1]), func(t *testing.T) { gameState := New(withDeck(newTestDeck(tt.hands))) - assert.Equal(t, tt.expected1, gameState.Hands[0].EnvidoScore()) - assert.Equal(t, tt.expected2, gameState.Hands[1].EnvidoScore()) + assert.Equal(t, tt.expected1, gameState.Players[0].Hand.EnvidoScore()) + assert.Equal(t, tt.expected2, gameState.Players[1].Hand.EnvidoScore()) }) } } diff --git a/truco/envido_sequence_test.go b/truco/envido_sequence_test.go index b1d8115..d1b09c7 100644 --- a/truco/envido_sequence_test.go +++ b/truco/envido_sequence_test.go @@ -160,7 +160,7 @@ func TestEnvidoSequence(t *testing.T) { continue } - cost, err := gameState.EnvidoSequence.Cost(gameState.Scores[gameState.TurnPlayerID], gameState.Scores[gameState.OpponentPlayerID()]) + cost, err := gameState.EnvidoSequence.Cost(gameState.Players[gameState.TurnPlayerID].Score, gameState.Players[gameState.TurnOpponentPlayerID].Score) require.NoError(t, err) assert.Equal(t, step.expectedCostAfterRunning, cost, "at step %v expected cost %v but got %v", i, step.expectedCostAfterRunning, cost) } diff --git a/truco/truco.go b/truco/truco.go index d4f04de..195e0ad 100644 --- a/truco/truco.go +++ b/truco/truco.go @@ -22,12 +22,13 @@ type GameState struct { // They are the same at the beginning of the round. TurnPlayerID int `json:"turnPlayerID"` - // Hands is a map of player IDs to their respective hands. - Hands map[int]*Hand `json:"hands"` + // TurnOpponentPlayerID is the player ID of the opponent of the player whose turn it is. + TurnOpponentPlayerID int `json:"turnOpponentPlayerID"` - // Scores is a map of player IDs to their respective scores. - // Scores go from 0 to 30. - Scores map[int]int `json:"scores"` + // Players is a map of player IDs to their respective hands and scores. + // There are 2 players in a game. Use TurnPlayerID and TurnOpponentPlayerID to index + // into this map, or iterate over it to discover player ids. + Players map[int]*Player `json:"players"` // PossibleActions is a list of possible actions that the current player can take. // Possible actions are calculated based on game state at the beginnin of the round and after @@ -51,85 +52,100 @@ type GameState struct { // A faceoff result will have the playerID of the winner, or -1 if it was a tie. CardRevealSequence *CardRevealSequence `json:"cardRevealSequence"` - // EnvidoFinished is true if the envido sequence is finished, or can no longer be continued. + // IsEnvidoFinished is true if the envido sequence is finished, or can no longer be continued. // TODO: can we remove this? Looks redundant to other state. But need tests first. - EnvidoFinished bool `json:"envidoFinished"` + IsEnvidoFinished bool `json:"isEnvidoFinished"` - // EnvidoWinnerPlayerID is the player ID of the player who won the envido sequence. - // TODO: looks like this is assigned to but never used. Can we remove? - EnvidoWinnerPlayerID int `json:"envidoWinnerPlayerID"` - - // RoundFinished is true if the current round is finished. Each action's `Run()` method is responsible + // IsRoundFinished is true if the current round is finished. Each action's `Run()` method is responsible // for setting this. During `GameState.RunAction()`, If the action's `Run()` method sets this to true, // then `GameState.startNewRound()` will be called. // // Clients are not really notified of a round change, so they should keep track of the "last round // number" to see if it changes. - RoundFinished bool `json:"roundFinished"` + IsRoundFinished bool `json:"isRoundFinished"` - // IsEnded is true if the whole game is ended, rather than an individual round. This happens when + // IsGameEnded is true if the whole game is ended, rather than an individual round. This happens when // a player reaches 30 points. - IsEnded bool `json:"isEnded"` + IsGameEnded bool `json:"isGameEnded"` // WinnerPlayerID is the player ID of the player who won the game. This is only set when `IsEnded` is // `true`. Otherwise, it's -1. WinnerPlayerID int `json:"winnerPlayerID"` - // CurrentRoundResult contains the live results of the ongoing round for envido/truco winners & points. - // It is set when actions are run, and is reset at the beginning of each round. Be careful when using - // this in the client, because if the last action caused the round to finish, it will be reset before - // you can use it to summarise what happened. You should use `RoundResults` in this case. - CurrentRoundResult RoundResult `json:"currentRoundResult"` - - // RoundJustStarted is true if a round has just started (i.e. no actions have been run on this round). - // This isn't used at all by the engine. It's strictly for clients to know, since there's no way to - // relate actions to rounds. TODO: that's probably a problem? - RoundJustStarted bool `json:"roundJustStarted"` - // TrucoQuieroOwnerPlayerId is the player ID of the player who said "quiero" last in the truco // sequence. This is used to determine who can raise the stakes in the truco sequence. // // TODO: this should probably be inside TrucoSequence? - TrucoQuieroOwnerPlayerId int `json:"trucoQuieroOwnerPlayerId"` + // TrucoQuieroOwnerPlayerId int `json:"trucoQuieroOwnerPlayerId"` - // Actions is the list of actions that have been run in the game. - // Each element is a JSON-serialized action. This is because `Action` is an interface, and we can't - // serialize it directly otherwise. Clients should use `DeserializeAction` to get the actual action. + // RoundsLog is the ordered list of logs of each round that was played in the game. + // + // Use GameState.RoundNumber to index into this list (note thus that it's 1-indexed). + // This means that there is an empty round at the beginning of the list. // - // TODO: rather than having "Actions", "HandsDealt", "RoundResults", "ActionOwnerPlayerIDs" as - // separate fields, we should have a single "ActionLog" field that contains all of these. - Actions []json.RawMessage `json:"actions"` - - // HandsDealt is the list of hands that were dealt in the game. Each element is a map of player IDs to - // their respective hands. - HandsDealt []map[int]*Hand `json:"handsDealt"` - - // RoundResults is the list of results of each round. Each element is a `RoundResult` struct. - // This is useful for clients to see the results of each round, since `CurrentRoundResult` is reset - // at the beginning of each round. - RoundResults []RoundResult `json:"roundResults"` - - // ActionOwnerPlayerIDs is the list of player IDs who ran each action. There is no PlayerID field in - // the `Action` struct, mostly because it would be annoying to have to distrust the client who sends - // it. + // Note that there is a "live entry" for the current round. This entry will always have + // the HandsDealt, but the other fields depend on the stage the round is in. You can + // use the ActionsLog's length to determine if a round has just started. // - // Note: this definitely should be inside an "ActionLog" slice, instead of here. - ActionOwnerPlayerIDs []int `json:"actionOwnerPlayerIDs"` + // Note that, if the last action ran caused a round to finish, if you want to render + // the screen with the last action, you have to check the last action of the previous round instead! + RoundsLog []*RoundLog `json:"actionLog"` deck *deck `json:"-"` } +type Player struct { + // Hands contains the revealed and unrevealed cards of the player. + Hand *Hand `json:"hand"` + + // Score is the player's scores (from 0 to 30). + Score int `json:"score"` +} + +// RoundLog is a log of a round that was played in the game +type RoundLog struct { + // HandsDealt is a map from PlayerID to its hand durinf this round. + HandsDealt map[int]*Hand `json:"handsDealt"` + + // For envido/truco winners and points, note that there is still a + // winner of 1 point if a player said "no quiero" to the envido/truco. + // + // If envido wasn't played at all, then EnvidoWinnerPlayerID is -1. + // + // At the end of a round, there will always be a TrucoWinnerPlayerID, + // even if truco wasn't played, implicitly by revealing the cards. + + EnvidoWinnerPlayerID int `json:"envidoWinnerPlayerID"` + EnvidoPoints int `json:"envidoPoints"` + TrucoWinnerPlayerID int `json:"trucoWinnerPlayerID"` + TrucoPoints int `json:"trucoPoints"` + + // ActionsLog is the ordered list of actions of this round. + ActionsLog []ActionLog `json:"actionsLog"` +} + +// ActionLog is a log of an action that was run in a round. +type ActionLog struct { + // PlayerID is the player ID of the player who ran the action. + PlayerID int `json:"playerID"` + + // Action is a JSON-serialized action. This is because `Action` is an interface, and we can't + // serialize it directly otherwise. Clients should use `truco.DeserializeAction`.` + Action json.RawMessage `json:"action"` +} + func New(opts ...func(*GameState)) *GameState { - // TODO: support taking player ids, ser/de, ... gs := &GameState{ RoundTurnPlayerID: 1, RoundNumber: 0, - Scores: map[int]int{0: 0, 1: 0}, - Hands: map[int]*Hand{0: nil, 1: nil}, - IsEnded: false, - WinnerPlayerID: -1, - Actions: []json.RawMessage{}, - deck: newDeck(), + Players: map[int]*Player{ + 0: {Hand: nil, Score: 0}, + 1: {Hand: nil, Score: 0}, + }, + IsGameEnded: false, + WinnerPlayerID: -1, + RoundsLog: []*RoundLog{{}}, // initialised with an empty round to be 1-indexed + deck: newDeck(), } for _, opt := range opts { @@ -142,40 +158,33 @@ func New(opts ...func(*GameState)) *GameState { } func (g *GameState) startNewRound() { - g.CurrentRoundResult = RoundResult{ - EnvidoWinnerPlayerID: -1, - EnvidoPoints: 0, - TrucoWinnerPlayerID: -1, - TrucoPoints: 0, - } - - g.RoundJustStarted = true g.RoundTurnPlayerID = g.OpponentOf(g.RoundTurnPlayerID) g.RoundNumber++ g.TurnPlayerID = g.RoundTurnPlayerID - - handPlayer0 := g.deck.dealHand() - handPlayer1 := g.deck.dealHand() - g.HandsDealt = append(g.HandsDealt, map[int]*Hand{ - g.RoundTurnPlayerID: handPlayer0, - g.OpponentOf(g.RoundTurnPlayerID): handPlayer1, - }) - g.Hands = map[int]*Hand{ - g.RoundTurnPlayerID: handPlayer0, - g.OpponentOf(g.RoundTurnPlayerID): handPlayer1, - } - g.EnvidoWinnerPlayerID = -1 + g.TurnOpponentPlayerID = g.OpponentOf(g.TurnPlayerID) + g.Players[g.TurnPlayerID].Hand = g.deck.dealHand() + g.Players[g.TurnOpponentPlayerID].Hand = g.deck.dealHand() g.EnvidoSequence = &EnvidoSequence{StartingPlayerID: -1} - g.TrucoSequence = &TrucoSequence{} + g.TrucoSequence = &TrucoSequence{StartingPlayerID: -1, QuieroOwnerPlayerID: -1} g.CardRevealSequence = &CardRevealSequence{} - g.EnvidoFinished = false - g.RoundFinished = false - g.TrucoQuieroOwnerPlayerId = -1 + g.IsEnvidoFinished = false + g.IsRoundFinished = false + g.RoundsLog = append(g.RoundsLog, &RoundLog{ + HandsDealt: map[int]*Hand{ + g.TurnPlayerID: g.Players[g.TurnPlayerID].Hand, + g.TurnOpponentPlayerID: g.Players[g.TurnOpponentPlayerID].Hand, + }, + EnvidoWinnerPlayerID: -1, + EnvidoPoints: 0, + TrucoWinnerPlayerID: -1, + TrucoPoints: 0, + ActionsLog: []ActionLog{}, + }) g.PossibleActions = _serializeActions(g.CalculatePossibleActions()) } func (g *GameState) RunAction(action Action) error { - if g.IsEnded { + if g.IsGameEnded { return errGameIsEnded } @@ -186,32 +195,29 @@ func (g *GameState) RunAction(action Action) error { if err != nil { return err } - g.RoundJustStarted = false bs := SerializeAction(action) - g.Actions = append(g.Actions, bs) - g.ActionOwnerPlayerIDs = append(g.ActionOwnerPlayerIDs, g.CurrentPlayerID()) + g.RoundsLog[g.RoundNumber].ActionsLog = append(g.RoundsLog[g.RoundNumber].ActionsLog, ActionLog{ + PlayerID: g.TurnPlayerID, + Action: bs, + }) // Start new round if current round is finished - if !g.IsEnded && g.RoundFinished { - g.RoundResults = append(g.RoundResults, g.CurrentRoundResult) + if !g.IsGameEnded && g.IsRoundFinished { g.startNewRound() return nil } // Switch player turn within current round (unless current action doesn't yield turn) - if !g.IsEnded && !g.RoundFinished && action.YieldsTurn(*g) { - g.TurnPlayerID = g.OpponentOf(g.TurnPlayerID) + if !g.IsGameEnded && !g.IsRoundFinished && action.YieldsTurn(*g) { + g.TurnPlayerID, g.TurnOpponentPlayerID = g.TurnOpponentPlayerID, g.TurnPlayerID } // Handle end of game due to score - // TODO: this changes if players are not 0 & 1 (or more than 2 players) - if g.Scores[0] >= 30 || g.Scores[1] >= 30 { - g.IsEnded = true - g.Scores[0] = min(30, g.Scores[0]) - g.Scores[1] = min(30, g.Scores[1]) - g.WinnerPlayerID = 0 - if g.Scores[1] > g.Scores[0] { - g.WinnerPlayerID = 1 + for playerID := range g.Players { + if g.Players[playerID].Score >= 30 { + g.Players[playerID].Score = 30 + g.IsGameEnded = true + g.WinnerPlayerID = playerID } } @@ -219,36 +225,10 @@ func (g *GameState) RunAction(action Action) error { return nil } -func (g GameState) CurrentPlayerID() int { - return g.TurnPlayerID -} - -func (g GameState) CurrentPlayerScore() int { - return g.Scores[g.TurnPlayerID] -} - -func (g GameState) OpponentPlayerID() int { - return g.OpponentOf(g.CurrentPlayerID()) -} - -func (g GameState) OpponentPlayerScore() int { - return g.Scores[g.OpponentPlayerID()] -} - -func (g GameState) RoundTurnOpponentPlayerID() int { - return g.OpponentOf(g.RoundTurnPlayerID) -} - func (g GameState) OpponentOf(playerID int) int { - // N.B. this function doesn't check if playerID is valid - players := []int{} - for playerID := range g.Hands { - players = append(players, playerID) - } - // TODO: This still assumes 2 players, but at least it doesn't hardcode the player IDs - for _, p := range players { - if p != playerID { - return p + for id := range g.Players { + if id != playerID { + return id } } return -1 // Unreachable @@ -281,11 +261,11 @@ var ( ) func (g GameState) CalculatePossibleActions() []Action { - envidoScore := g.Hands[g.TurnPlayerID].EnvidoScore() + envidoScore := g.Players[g.TurnPlayerID].Hand.EnvidoScore() allActions := []Action{} - for _, card := range g.Hands[g.TurnPlayerID].Unrevealed { + for _, card := range g.Players[g.TurnPlayerID].Hand.Unrevealed { allActions = append(allActions, newActionRevealCard(card)) } diff --git a/truco/truco_sequence.go b/truco/truco_sequence.go index 76820bb..7959e19 100644 --- a/truco/truco_sequence.go +++ b/truco/truco_sequence.go @@ -48,8 +48,13 @@ type TrucoSequence struct { // Sub-sequences are separated by "quiero" actions. // // StartingPlayerID holds the player ID that started the _current_ sub-sequence. - StartingPlayerID int `json:"starting_player_id"` - Sequence []string `json:"sequence"` + StartingPlayerID int `json:"starting_player_id"` + + // QuieroOwnerPlayerID is the player ID of the player who said "quiero" last in the truco + // sequence. This is used to determine who can raise the stakes in the truco sequence. + QuieroOwnerPlayerID int `json:"quiero_owner_player_id"` + + Sequence []string `json:"sequence"` } func (ts TrucoSequence) String() string { diff --git a/truco/truco_test.go b/truco/truco_test.go index cff4f65..352f753 100644 --- a/truco/truco_test.go +++ b/truco/truco_test.go @@ -6,50 +6,43 @@ import ( "github.com/stretchr/testify/require" ) -// func TestTruco(t *testing.T) { -// gameState := New() -// err := gameState.RunAction(ActionSayRealEnvido{}) -// if err != nil { -// t.Fatal(err) -// } -// pretty, err := gameState.PrettyPrint() -// if err != nil { -// t.Error(err) -// } - -// fmt.Printf("gameState: \n%v\n", pretty) -// t.Fail() -// } - func TestInitialOptions(t *testing.T) { gameState := New() + + expectedActions := []Action{ + newActionRevealCard(gameState.Players[gameState.TurnPlayerID].Hand.Unrevealed[0]), + newActionRevealCard(gameState.Players[gameState.TurnPlayerID].Hand.Unrevealed[1]), + newActionRevealCard(gameState.Players[gameState.TurnPlayerID].Hand.Unrevealed[2]), + newActionSayEnvido(), + newActionSayRealEnvido(), + newActionSayFaltaEnvido(), + newActionSayTruco(), + newActionSayMeVoyAlMazo(), + } + require.Equal( t, - []string{ - "reveal_card", - "say_envido", - "say_real_envido", - "say_falta_envido", - "say_truco", - "say_me_voy_al_mazo", - }, + _serializeActions(expectedActions), gameState.PossibleActions, ) } func TestAfterRealEnvidoOptions(t *testing.T) { gameState := New() - err := gameState.RunAction(ActionSayRealEnvido{act: act{Name: SAY_REAL_ENVIDO}}) + + expectedActions := []Action{ + newActionSayFaltaEnvido(), + newActionSayEnvidoQuiero(gameState.Players[gameState.TurnOpponentPlayerID].Hand.EnvidoScore()), + newActionSayEnvidoNoQuiero(), + } + + err := gameState.RunAction(newActionSayRealEnvido()) if err != nil { t.Fatal(err) } require.Equal( t, - []string{ - "say_falta_envido", - "say_envido_quiero", - "say_envido_no_quiero", - }, + _serializeActions(expectedActions), gameState.PossibleActions, ) }