From 7fc169e2568004a91138a0b72e0dce6587f50d57 Mon Sep 17 00:00:00 2001 From: Mariano Gappa Date: Thu, 18 Jul 2024 19:52:56 +0100 Subject: [PATCH] Add REVEAL_ENVIDO_SCORE action & logic. Also clean up ClientGameState state. --- exampleclient/ui.go | 10 +-- exampleclient/ui_spanish.go | 8 +- truco/action_any_quiero.go | 74 +++++++++++++++++++ truco/action_confirm_round_ended.go | 4 +- truco/actions.go | 4 + truco/envido_sequence.go | 14 +++- truco/reveal_envido_score_test.go | 109 ++++++++++++++++++++++++++++ truco/truco.go | 34 +++++---- truco/truco_sequence.go | 9 +++ 9 files changed, 241 insertions(+), 25 deletions(-) create mode 100644 truco/reveal_envido_score_test.go diff --git a/exampleclient/ui.go b/exampleclient/ui.go index c825f37..23efe90 100644 --- a/exampleclient/ui.go +++ b/exampleclient/ui.go @@ -126,18 +126,18 @@ func renderEndSummary(rs renderState) { switch rs.mode { case PRINT_MODE_SHOW_ROUND_RESULT: envidoPart := "el envido no se jugó" - if rs.gs.LastRoundLog.EnvidoWinnerPlayerID != -1 { + if rs.gs.EnvidoWinnerPlayerID != -1 { envidoWinner := "vos" won := "ganaste" - if rs.gs.LastRoundLog.EnvidoWinnerPlayerID == rs.gs.ThemPlayerID { + if rs.gs.EnvidoWinnerPlayerID == rs.gs.ThemPlayerID { envidoWinner = "elle" won = "ganó" } - envidoPart = fmt.Sprintf("%v %v %v puntos por el envido", envidoWinner, won, rs.gs.LastRoundLog.EnvidoPoints) + envidoPart = fmt.Sprintf("%v %v %v puntos por el envido", envidoWinner, won, rs.gs.EnvidoPoints) } trucoWinner := "vos" won := "ganaste" - if rs.gs.LastRoundLog.TrucoWinnerPlayerID == rs.gs.ThemPlayerID { + if rs.gs.TrucoWinnerPlayerID == rs.gs.ThemPlayerID { trucoWinner = "elle" won = "ganó" } @@ -147,7 +147,7 @@ func renderEndSummary(rs renderState) { envidoPart, trucoWinner, won, - rs.gs.LastRoundLog.TrucoPoints, + rs.gs.TrucoPoints, ) case PRINT_MODE_END: var resultText string diff --git a/exampleclient/ui_spanish.go b/exampleclient/ui_spanish.go index 0d38bab..ff66f23 100644 --- a/exampleclient/ui_spanish.go +++ b/exampleclient/ui_spanish.go @@ -67,6 +67,9 @@ func getActionString(log truco.ActionLog, playerID int) string { what = fmt.Sprintf("%v %d son mejores", said, action.Score) case truco.SAY_ME_VOY_AL_MAZO: what = fmt.Sprintf("%v me voy al mazo", said) + case truco.REVEAL_ENVIDO_SCORE: + _action := lastAction.(*truco.ActionRevealEnvidoScore) + what = fmt.Sprintf("%v en mesa", _action.Score) case truco.CONFIRM_ROUND_FINISHED: what = "" default: @@ -86,7 +89,7 @@ func spanishScore(score int) string { if score == 15 { return "entraste" } - return fmt.Sprintf("%d buenas", score-14) + return fmt.Sprintf("%d buenas", score-15) } func spanishAction(action truco.Action) string { @@ -126,6 +129,9 @@ func spanishAction(action truco.Action) string { return "me voy al mazo" case truco.CONFIRM_ROUND_FINISHED: return "seguir" + case truco.REVEAL_ENVIDO_SCORE: + _action := action.(*truco.ActionRevealEnvidoScore) + return fmt.Sprintf("mostrar las %v", _action.Score) default: return "???" } diff --git a/truco/action_any_quiero.go b/truco/action_any_quiero.go index 009980e..f69225f 100644 --- a/truco/action_any_quiero.go +++ b/truco/action_any_quiero.go @@ -1,11 +1,17 @@ package truco +import "fmt" + type ActionSayEnvidoNoQuiero struct{ act } type ActionSayEnvidoQuiero struct{ act } type ActionSayEnvidoScore struct { act Score int `json:"score"` } +type ActionRevealEnvidoScore struct { + act + Score int `json:"score"` +} type ActionSayTrucoQuiero struct{ act } type ActionSayTrucoNoQuiero struct{ act } @@ -40,6 +46,21 @@ func (a ActionSayEnvidoScore) IsPossible(g GameState) bool { return g.EnvidoSequence.CanAddStep(a.GetName()) } +func (a ActionRevealEnvidoScore) IsPossible(g GameState) bool { + if !g.IsRoundFinished { + return false + } + if !g.EnvidoSequence.WasAccepted() { + return false + } + roundLog := g.RoundsLog[g.RoundNumber] + if roundLog.EnvidoWinnerPlayerID != a.PlayerID { + return false + } + revealedHand := Hand{Revealed: g.Players[a.PlayerID].Hand.Revealed} + return revealedHand.EnvidoScore() != g.Players[a.PlayerID].Hand.EnvidoScore() +} + func (a ActionSayTrucoQuiero) IsPossible(g GameState) bool { if g.IsRoundFinished { return false @@ -104,6 +125,53 @@ func (a ActionSayEnvidoScore) Run(g *GameState) error { return nil } +func (a ActionRevealEnvidoScore) Run(g *GameState) error { + if !a.IsPossible(*g) { + return errActionNotPossible + } + // We need to reveal the least amount of cards such that the envido score is revealed. + // Since we don't know which cards to reveal, let's try all possible reveal combinations. + // + // allPossibleReveals is a `map[unrevealed_len][]map[card_index]struct{}{}` + // + // Note: len(unrevealed) == 0 must be impossible if this line is reached + _s := struct{}{} + allPossibleReveals := map[int][]map[int]struct{}{ + 1: {{0: _s}}, // i.e. if there's only one unrevealed card, only option is to reveal that card + 2: {{0: _s}, {1: _s}, {0: _s, 1: _s}}, + 3: {{0: _s}, {1: _s}, {2: _s}, {0: _s, 1: _s}, {0: _s, 2: _s}, {1: _s, 2: _s}}, + } + curPlayersHand := g.Players[a.PlayerID].Hand + + // for each possible combination of card reveals + for _, is := range allPossibleReveals[len(curPlayersHand.Unrevealed)] { + // create a candidate hand but only with reveal cards + candidateHand := Hand{Revealed: append([]Card{}, curPlayersHand.Revealed...)} + // and reveal the additional cards of this combination + for i := range is { + candidateHand.Revealed = append(candidateHand.Revealed, curPlayersHand.Unrevealed[i]) + } + // if by revealing these cards we reach the expected envido score, this is the right reveal + // Note: this is only true if the reveal combinations are sorted by reveal count ascending! + // Note: we didn't add the unrevealed cards to the candidate hand yet, because we need to + // reach the expected envido score only with revealed cards! That's the whole point! + if candidateHand.EnvidoScore() == curPlayersHand.EnvidoScore() { + // don't forget to add the unrevealed cards to the candidate hand + for i := range curPlayersHand.Unrevealed { + // add all unrevealed cards from the players hand, except if we revealed them + if _, ok := is[i]; !ok { + candidateHand.Unrevealed = append(candidateHand.Unrevealed, curPlayersHand.Unrevealed[i]) + } + } + // replace hand with our satisfactory candidate hand + g.Players[a.PlayerID].Hand = &candidateHand + return nil + } + } + // we tried all possible reveal combinations, so it should be impossible that we didn't find the right combination! + return fmt.Errorf("couldn't reveal envido score due to a bug, this code should be unreachable") +} + func (a ActionSayTrucoQuiero) Run(g *GameState) error { if !a.IsPossible(*g) { return errActionNotPossible @@ -146,3 +214,9 @@ func (a ActionSayEnvidoQuiero) YieldsTurn(g GameState) bool { // This should always be the "mano" player. return g.TurnPlayerID != g.RoundTurnPlayerID } + +func (a ActionRevealEnvidoScore) YieldsTurn(g GameState) bool { + // this action doesn't change turn because the round is finished at this point + // and the current player must confirm round finished right after this action + return false +} diff --git a/truco/action_confirm_round_ended.go b/truco/action_confirm_round_ended.go index 5229959..b65596b 100644 --- a/truco/action_confirm_round_ended.go +++ b/truco/action_confirm_round_ended.go @@ -5,7 +5,9 @@ type ActionConfirmRoundFinished struct { } func (a ActionConfirmRoundFinished) IsPossible(g GameState) bool { - return g.IsRoundFinished && !g.RoundFinishedConfirmedPlayerIDs[a.PlayerID] + return g.IsRoundFinished && + !NewActionRevealEnvidoScore(a.PlayerID, 0).IsPossible(g) && + !g.RoundFinishedConfirmedPlayerIDs[a.PlayerID] } func (a ActionConfirmRoundFinished) Run(g *GameState) error { diff --git a/truco/actions.go b/truco/actions.go index 3f611f9..4756b7f 100644 --- a/truco/actions.go +++ b/truco/actions.go @@ -88,3 +88,7 @@ func NewActionSayMeVoyAlMazo(playerID int) Action { func NewActionConfirmRoundFinished(playerID int) Action { return ActionConfirmRoundFinished{act: act{Name: CONFIRM_ROUND_FINISHED, PlayerID: playerID}} } + +func NewActionRevealEnvidoScore(playerID int, score int) Action { + return ActionRevealEnvidoScore{act: act{Name: REVEAL_ENVIDO_SCORE, PlayerID: playerID}, Score: score} +} diff --git a/truco/envido_sequence.go b/truco/envido_sequence.go index ca22ede..88a3997 100644 --- a/truco/envido_sequence.go +++ b/truco/envido_sequence.go @@ -18,6 +18,7 @@ const ( REVEAL_CARD = "reveal_card" CONFIRM_ROUND_FINISHED = "confirm_round_finished" SAY_ENVIDO_SCORE = "say_envido_score" + REVEAL_ENVIDO_SCORE = "reveal_envido_score" COST_NOT_READY = -1 COST_FALTA_ENVIDO = -2 @@ -141,12 +142,21 @@ func (es EnvidoSequence) Cost(currentPlayerScore int, otherPlayerScore int) (int } cost := validEnvidoSequenceCosts[es.String()] if cost == COST_FALTA_ENVIDO { - return es.calculateFaltaEnvidoCost(currentPlayerScore, otherPlayerScore), nil + return calculateFaltaEnvidoCost(currentPlayerScore, otherPlayerScore), nil } return cost, nil } -func (es EnvidoSequence) calculateFaltaEnvidoCost(meScore int, youScore int) int { +func (es EnvidoSequence) WasAccepted() bool { + for _, step := range es.Sequence { + if step == SAY_ENVIDO_QUIERO { + return true + } + } + return false +} + +func calculateFaltaEnvidoCost(meScore int, youScore int) int { if meScore < 15 && youScore < 15 { return 15 - meScore } diff --git a/truco/reveal_envido_score_test.go b/truco/reveal_envido_score_test.go new file mode 100644 index 0000000..dcfe7e5 --- /dev/null +++ b/truco/reveal_envido_score_test.go @@ -0,0 +1,109 @@ +package truco + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRevealEnvidoScore(t *testing.T) { + type testStep struct { + action Action + expectedIsValid bool + expectedPlayerTurnAfterRunning int + ignoreAction bool + } + + tests := []struct { + name string + hands []Hand + steps []testStep + }{ + { + name: "test reveal envido score", + steps: []testStep{ + { + action: NewActionSayEnvido(0), + ignoreAction: true, + }, + { + action: NewActionSayEnvidoQuiero(1), + ignoreAction: true, + }, + { + action: NewActionSayEnvidoScore(25, 0), + ignoreAction: true, + }, + { + action: NewActionSaySonMejores(31, 1), + ignoreAction: true, + }, + { + action: NewActionSayTruco(0), + ignoreAction: true, + }, + { + action: NewActionSayTrucoNoQuiero(1), + ignoreAction: true, + }, + // At this point the round is finished, so it's valid for player1 to reveal the envido score, but not to confirm round end + { + action: NewActionConfirmRoundFinished(1), + expectedIsValid: false, + }, + // Revealing the envido score is valid + { + action: NewActionRevealEnvidoScore(1, 31), + expectedIsValid: true, + expectedPlayerTurnAfterRunning: 1, // doesn't yield turn + }, + // Now that the envido score is revealed, it's valid to confirm the round end + { + action: NewActionConfirmRoundFinished(1), + expectedIsValid: true, + expectedPlayerTurnAfterRunning: 0, // yields turn + }, + // Revealing the envido score again is invalid + { + action: NewActionRevealEnvidoScore(1, 31), + expectedIsValid: false, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + defaultHands := []Hand{ + {Unrevealed: []Card{{Number: 1, Suit: ORO}, {Number: 2, Suit: ORO}, {Number: 3, Suit: ORO}}}, // 25 + {Unrevealed: []Card{{Number: 4, Suit: ORO}, {Number: 5, Suit: ORO}, {Number: 6, Suit: ORO}}}, // 31 + } + if len(tt.hands) == 0 { + tt.hands = defaultHands + } + gameState := New(withDeck(newTestDeck(tt.hands))) + + require.Equal(t, 0, gameState.TurnPlayerID) + + for i, step := range tt.steps { + if !step.ignoreAction { + actualIsValid := step.action.IsPossible(*gameState) + require.Equal(t, step.expectedIsValid, actualIsValid, "at step %v expected isValid to be %v but wasn't", i, step.expectedIsValid) + if !step.expectedIsValid { + continue + } + } + + err := gameState.RunAction(step.action) + require.NoError(t, err) + + if step.ignoreAction { + continue + } + + assert.Equal(t, step.expectedPlayerTurnAfterRunning, gameState.TurnPlayerID, "at step %v expected player turn %v but got %v", i, step.expectedPlayerTurnAfterRunning, gameState.TurnPlayerID) + } + }) + } +} diff --git a/truco/truco.go b/truco/truco.go index aec4b85..a99aae0 100644 --- a/truco/truco.go +++ b/truco/truco.go @@ -202,10 +202,6 @@ func (g *GameState) RunAction(action Action) error { return err } - if g.IsRoundFinished { - log.Println("Round finished") - } - if action.GetName() != CONFIRM_ROUND_FINISHED { g.RoundsLog[g.RoundNumber].ActionsLog = append(g.RoundsLog[g.RoundNumber].ActionsLog, ActionLog{ PlayerID: g.TurnPlayerID, @@ -225,10 +221,6 @@ func (g *GameState) RunAction(action Action) error { g.TurnPlayerID, g.TurnOpponentPlayerID = g.TurnOpponentPlayerID, g.TurnPlayerID } - if g.IsRoundFinished { - fmt.Println("len(g.RoundFinishedConfirmedPlayerIDs):", len(g.RoundFinishedConfirmedPlayerIDs)) - } - if !g.IsGameEnded && g.IsRoundFinished && len(g.RoundFinishedConfirmedPlayerIDs) == 1 { if g.RoundFinishedConfirmedPlayerIDs[g.TurnPlayerID] { g.TurnPlayerID, g.TurnOpponentPlayerID = g.TurnOpponentPlayerID, g.TurnPlayerID @@ -293,6 +285,7 @@ var ( func (g GameState) CalculatePossibleActions() []Action { envidoScore := g.Players[g.TurnPlayerID].Hand.EnvidoScore() + opponentEnvidoScore := g.Players[g.TurnOpponentPlayerID].Hand.EnvidoScore() allActions := []Action{} @@ -317,6 +310,8 @@ func (g GameState) CalculatePossibleActions() []Action { NewActionSayMeVoyAlMazo(g.TurnPlayerID), NewActionConfirmRoundFinished(g.TurnPlayerID), NewActionConfirmRoundFinished(g.TurnOpponentPlayerID), + NewActionRevealEnvidoScore(g.TurnPlayerID, envidoScore), + NewActionRevealEnvidoScore(g.TurnOpponentPlayerID, opponentEnvidoScore), ) possibleActions := []Action{} @@ -377,6 +372,8 @@ func DeserializeAction(bs []byte) (Action, error) { action = &ActionSayMeVoyAlMazo{} case CONFIRM_ROUND_FINISHED: action = &ActionConfirmRoundFinished{} + case REVEAL_ENVIDO_SCORE: + action = &ActionRevealEnvidoScore{} default: return nil, fmt.Errorf("unknown action type %v", actionName.Name) } @@ -429,7 +426,12 @@ func (g *GameState) ToClientGameState(youPlayerID int) ClientGameState { IsGameEnded: g.IsGameEnded, IsRoundFinished: g.IsRoundFinished, WinnerPlayerID: g.WinnerPlayerID, - LastRoundLog: *g.RoundsLog[g.RoundNumber-1], // TODO elide their unrevealed cards + EnvidoWinnerPlayerID: g.RoundsLog[g.RoundNumber].EnvidoWinnerPlayerID, + WasEnvidoAccepted: g.EnvidoSequence.WasAccepted(), + EnvidoPoints: g.RoundsLog[g.RoundNumber].EnvidoPoints, + TrucoWinnerPlayerID: g.RoundsLog[g.RoundNumber].TrucoWinnerPlayerID, + TrucoPoints: g.RoundsLog[g.RoundNumber].TrucoPoints, + WasTrucoAccepted: g.TrucoSequence.WasAccepted(), } if len(g.RoundsLog[g.RoundNumber].ActionsLog) > 0 { @@ -480,13 +482,13 @@ type ClientGameState struct { // `true`. Otherwise, it's -1. WinnerPlayerID int `json:"winnerPlayerID"` - // LastRoundLog is the log of the last round that was played. Clients need this in order to render - // the cards of the last round, because upon an action that ends a round, the game state is updated - // to the next round, and the cards are no longer available. - // - // The rendering of the end of the round might also want to show how the points were won, so this - // is available as well. - LastRoundLog RoundLog `json:"lastRoundLog"` + // Some state information about the current round, in case it's useful to the client. + EnvidoWinnerPlayerID int `json:"envidoWinnerPlayerID"` + WasEnvidoAccepted bool `json:"wasEnvidoAccepted"` + EnvidoPoints int `json:"envidoPoints"` + TrucoWinnerPlayerID int `json:"trucoWinnerPlayerID"` + TrucoPoints int `json:"trucoPoints"` + WasTrucoAccepted bool `json:"wasTrucoAccepted"` // LastActionLog is the log of the last action that was run in the current round. If the round has // just started, this will be nil. Clients typically want to user this to show the current player diff --git a/truco/truco_sequence.go b/truco/truco_sequence.go index 7959e19..7a3d37f 100644 --- a/truco/truco_sequence.go +++ b/truco/truco_sequence.go @@ -119,6 +119,15 @@ func (ts TrucoSequence) IsSubsequenceStart() bool { return false } +func (ts TrucoSequence) WasAccepted() bool { + for _, step := range ts.Sequence { + if step == SAY_TRUCO_QUIERO { + return true + } + } + return false +} + var ( errInvalidTrucoSequence = errors.New("invalid truco sequence") errUnfinishedTrucoSequence = errors.New("unfinished truco sequence")