From ff83ef119f06f124140d1bb78389db746b185781 Mon Sep 17 00:00:00 2001 From: Mariano Gappa Date: Thu, 25 Jul 2024 21:15:24 +0100 Subject: [PATCH] Fix tests. Use Action.Enrich() pattern. --- examplebot/bot.go | 28 +++--------- examplebot/bot_test.go | 66 +++-------------------------- truco/action_any_quiero.go | 40 ++++++++--------- truco/action_confirm_round_ended.go | 2 +- truco/action_reveal_card.go | 7 +++ truco/action_son_mejores.go | 4 ++ truco/actions.go | 23 +++++++--- truco/envido_sequence_test.go | 21 ++++++--- truco/reveal_envido_score_test.go | 8 ++-- truco/truco.go | 40 ++++++----------- 10 files changed, 97 insertions(+), 142 deletions(-) diff --git a/examplebot/bot.go b/examplebot/bot.go index 33ff886..a33f109 100644 --- a/examplebot/bot.go +++ b/examplebot/bot.go @@ -62,13 +62,13 @@ func calculateEnvidoScore(gs truco.ClientGameState) int { func calculateCardStrength(gs truco.Card) int { specialValues := map[truco.Card]int{ - {Suit: truco.ESPADA, Number: 1}: 19, - {Suit: truco.BASTO, Number: 1}: 18, - {Suit: truco.ESPADA, Number: 7}: 17, - {Suit: truco.ORO, Number: 7}: 16, + {Suit: truco.ESPADA, Number: 1}: 15, + {Suit: truco.BASTO, Number: 1}: 14, + {Suit: truco.ESPADA, Number: 7}: 13, + {Suit: truco.ORO, Number: 7}: 12, } if _, ok := specialValues[gs]; ok { - return specialValues[gs] - 4 + return specialValues[gs] } if gs.Number <= 3 { return gs.Number + 12 - 4 @@ -84,14 +84,6 @@ func faceoffResults(gs truco.ClientGameState) []int { return results } -func calculateTrucoHandChance(cards []truco.Card) float64 { - sum := 0.0 - for _, card := range cards { - sum += float64(calculateCardStrength(card)) - } - return sum / (15 + 14 + 13) -} - func canAnyEnvido(actions map[string]truco.Action) bool { return len(filter(actions, truco.NewActionSayEnvido(1), @@ -299,13 +291,7 @@ func lowestCardThatBeats(card truco.Card, cards []truco.Card) truco.Card { } func cardsChance(cards []truco.Card) float64 { - divisor := float64(19.0) - if len(cards) == 2 { - divisor = 15.0 + 14.0 - } - if len(cards) == 3 { - divisor = 15.0 + 14.0 + 13.0 - } + divisor := float64([]float64{1, 15.0, 15.0 + 14.0, 15.0 + 14.0 + 13.0}[len(cards)]) sum := 0.0 for _, card := range cards { sum += float64(calculateCardStrength(card)) @@ -575,7 +561,7 @@ func sonBuenas(gs truco.ClientGameState) truco.Action { return truco.NewActionSaySonBuenas(gs.YouPlayerID) } func sonMejores(gs truco.ClientGameState) truco.Action { - return truco.NewActionSaySonMejores(0, gs.YouPlayerID) + return truco.NewActionSaySonMejores(gs.YouPlayerID) } func envidoNoQuiero(gs truco.ClientGameState) truco.Action { return truco.NewActionSayEnvidoNoQuiero(gs.YouPlayerID) diff --git a/examplebot/bot_test.go b/examplebot/bot_test.go index 091097d..0d0c2d1 100644 --- a/examplebot/bot_test.go +++ b/examplebot/bot_test.go @@ -12,10 +12,10 @@ func TestCalculateCardStrength(t *testing.T) { card truco.Card expected int }{ - {card: truco.Card{Suit: truco.ESPADA, Number: 1}, expected: 19}, - {card: truco.Card{Suit: truco.ESPADA, Number: 2}, expected: 14}, - {card: truco.Card{Suit: truco.ESPADA, Number: 3}, expected: 15}, - {card: truco.Card{Suit: truco.ESPADA, Number: 4}, expected: 4}, + {card: truco.Card{Suit: truco.ESPADA, Number: 1}, expected: 15}, + {card: truco.Card{Suit: truco.ESPADA, Number: 2}, expected: 10}, + {card: truco.Card{Suit: truco.ESPADA, Number: 3}, expected: 11}, + {card: truco.Card{Suit: truco.ESPADA, Number: 4}, expected: 0}, } for _, tc := range testCases { @@ -26,44 +26,6 @@ func TestCalculateCardStrength(t *testing.T) { } } -func TestCalculateTrucoHandChance(t *testing.T) { - testCases := []struct { - cards []truco.Card - expected float64 - }{ - { - cards: []truco.Card{ - {Suit: truco.ESPADA, Number: 1}, - {Suit: truco.ESPADA, Number: 7}, - {Suit: truco.BASTO, Number: 1}, - }, - expected: 1, - }, - { - cards: []truco.Card{ - {Suit: truco.BASTO, Number: 4}, - {Suit: truco.COPA, Number: 4}, - {Suit: truco.ORO, Number: 4}, - }, - expected: 0.0, - }, - // Add more test cases here... - } - - for _, tc := range testCases { - actual := calculateTrucoHandChance(tc.cards) - if !floatEquals(actual, tc.expected, 0.01) { - t.Errorf("calculateTrucoHandChance(%v) = %f, expected %f", tc.cards, actual, tc.expected) - } - - } -} - -// Function to compare floating-point numbers within a tolerance. -func floatEquals(a, b, tolerance float64) bool { - return math.Abs(a-b) < tolerance -} - func TestCanBeatCard(t *testing.T) { testCases := []struct { card truco.Card @@ -662,7 +624,7 @@ func TestChanceOfWinningTruco(t *testing.T) { expected: 0.733, }, { - name: "Can't beat the opponent's revealed card, highest remaining card 3: (10+7-8)/(19+18-8)*0.66 = 20.4%% chance of winning", + name: "Can't beat the opponent's revealed card, highest remaining card 3: ((10+7-8)/(15+14))^2 = 9.63%% chance of winning", gs: truco.ClientGameState{ YourRevealedCards: []truco.Card{}, YourUnrevealedCards: []truco.Card{ @@ -674,7 +636,7 @@ func TestChanceOfWinningTruco(t *testing.T) { {Suit: truco.ORO, Number: 7}, }, }, - expected: 0.204, + expected: 0.096, }, { name: "Tie at first revealed card, highest possible remaining card = 100.0%% chance of winning", @@ -876,22 +838,6 @@ func TestChanceOfWinningTruco(t *testing.T) { }, expected: 0.4, }, - { - name: "test", - gs: truco.ClientGameState{ - YourRevealedCards: []truco.Card{ - {Suit: truco.ESPADA, Number: 10}, - {Suit: truco.ORO, Number: 1}, - }, - YourUnrevealedCards: []truco.Card{ - {Suit: truco.BASTO, Number: 1}, - }, - TheirRevealedCards: []truco.Card{ - {Suit: truco.COPA, Number: 4}, - }, - }, - expected: 0.4, - }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { diff --git a/truco/action_any_quiero.go b/truco/action_any_quiero.go index 5fb7357..8ac71ea 100644 --- a/truco/action_any_quiero.go +++ b/truco/action_any_quiero.go @@ -87,8 +87,8 @@ func (a ActionSayTrucoQuiero) IsPossible(g GameState) bool { me = a.PlayerID isEnvidoQuieroPossible = NewActionSayEnvidoQuiero(me).IsPossible(g) isSonBuenasPossible = NewActionSaySonBuenas(me).IsPossible(g) - isSonMejoresPossible = NewActionSaySonMejores(0, me).IsPossible(g) - isSayEnvidoScorePossible = NewActionSayEnvidoScore(0, me).IsPossible(g) + isSonMejoresPossible = NewActionSaySonMejores(me).IsPossible(g) + isSayEnvidoScorePossible = NewActionSayEnvidoScore(me).IsPossible(g) ) if isEnvidoQuieroPossible || isSonBuenasPossible || isSonMejoresPossible || isSayEnvidoScorePossible { return false @@ -107,8 +107,8 @@ func (a ActionSayTrucoNoQuiero) IsPossible(g GameState) bool { me = a.PlayerID isEnvidoQuieroPossible = NewActionSayEnvidoQuiero(me).IsPossible(g) isSonBuenasPossible = NewActionSaySonBuenas(me).IsPossible(g) - isSonMejoresPossible = NewActionSaySonMejores(0, me).IsPossible(g) - isSayEnvidoScorePossible = NewActionSayEnvidoScore(0, me).IsPossible(g) + isSonMejoresPossible = NewActionSaySonMejores(me).IsPossible(g) + isSayEnvidoScorePossible = NewActionSayEnvidoScore(me).IsPossible(g) ) if isEnvidoQuieroPossible || isSonBuenasPossible || isSonMejoresPossible || isSayEnvidoScorePossible { return false @@ -251,26 +251,28 @@ func (a ActionRevealEnvidoScore) YieldsTurn(g GameState) bool { return false } -func (a *ActionSayTrucoQuiero) withRequiresReminder(g GameState) Action { - if len(g.RoundsLog[g.RoundNumber].ActionsLog) == 0 { - a.RequiresReminder = false - return a - } - lastAction := _deserializeCurrentRoundLastAction(g) - // If the last action wasn't a truco action, then an envido sequence - // got in the middle of the truco sequence. A reminder is needed. - a.RequiresReminder = !slices.Contains[[]string]([]string{SAY_TRUCO, SAY_QUIERO_RETRUCO, SAY_QUIERO_VALE_CUATRO}, lastAction.GetName()) - return a +func (a *ActionSayTrucoQuiero) Enrich(g GameState) { + a.RequiresReminder = _doesTrucoActionRequireReminder(g) +} + +func (a *ActionSayTrucoNoQuiero) Enrich(g GameState) { + a.RequiresReminder = _doesTrucoActionRequireReminder(g) } -func (a *ActionSayTrucoNoQuiero) withRequiresReminder(g GameState) Action { +func _doesTrucoActionRequireReminder(g GameState) bool { if len(g.RoundsLog[g.RoundNumber].ActionsLog) == 0 { - a.RequiresReminder = false - return a + return false } lastAction := _deserializeCurrentRoundLastAction(g) // If the last action wasn't a truco action, then an envido sequence // got in the middle of the truco sequence. A reminder is needed. - a.RequiresReminder = !slices.Contains[[]string]([]string{SAY_TRUCO, SAY_QUIERO_RETRUCO, SAY_QUIERO_VALE_CUATRO}, lastAction.GetName()) - return a + return !slices.Contains[[]string]([]string{SAY_TRUCO, SAY_QUIERO_RETRUCO, SAY_QUIERO_VALE_CUATRO}, lastAction.GetName()) +} + +func (a *ActionSayEnvidoScore) Enrich(g GameState) { + a.Score = g.Players[a.PlayerID].Hand.EnvidoScore() +} + +func (a *ActionRevealEnvidoScore) Enrich(g GameState) { + a.Score = g.Players[a.PlayerID].Hand.EnvidoScore() } diff --git a/truco/action_confirm_round_ended.go b/truco/action_confirm_round_ended.go index b65596b..164bb8f 100644 --- a/truco/action_confirm_round_ended.go +++ b/truco/action_confirm_round_ended.go @@ -6,7 +6,7 @@ type ActionConfirmRoundFinished struct { func (a ActionConfirmRoundFinished) IsPossible(g GameState) bool { return g.IsRoundFinished && - !NewActionRevealEnvidoScore(a.PlayerID, 0).IsPossible(g) && + !NewActionRevealEnvidoScore(a.PlayerID).IsPossible(g) && !g.RoundFinishedConfirmedPlayerIDs[a.PlayerID] } diff --git a/truco/action_reveal_card.go b/truco/action_reveal_card.go index 15e3f0b..4f6419b 100644 --- a/truco/action_reveal_card.go +++ b/truco/action_reveal_card.go @@ -73,3 +73,10 @@ func (a *ActionRevealCard) Run(g *GameState) error { func (a ActionRevealCard) YieldsTurn(g GameState) bool { return g.CardRevealSequence.YieldsTurn(g) } + +func (a *ActionRevealCard) Enrich(g GameState) { + if g.canAwardEnvidoPoints(Hand{Revealed: append(g.Players[g.TurnPlayerID].Hand.Revealed, a.Card)}) { + a.EnMesa = true + a.Score = g.Players[g.TurnPlayerID].Hand.EnvidoScore() + } +} diff --git a/truco/action_son_mejores.go b/truco/action_son_mejores.go index 7bd8ced..dd13f50 100644 --- a/truco/action_son_mejores.go +++ b/truco/action_son_mejores.go @@ -56,3 +56,7 @@ func (a ActionSaySonMejores) YieldsTurn(g GameState) bool { } return g.TurnPlayerID != g.EnvidoSequence.StartingPlayerID } + +func (a *ActionSaySonMejores) Enrich(g GameState) { + a.Score = g.Players[a.PlayerID].Hand.EnvidoScore() +} diff --git a/truco/actions.go b/truco/actions.go index a8e4943..3d40db7 100644 --- a/truco/actions.go +++ b/truco/actions.go @@ -20,6 +20,9 @@ func (a act) GetPlayerID() int { return a.PlayerID } +// By default, actions don't need to be enriched. +func (a act) Enrich(g GameState) {} + func (a act) String() string { name := strings.ReplaceAll(strings.TrimPrefix(a.Name, "say_"), "_", " ") return fmt.Sprintf("Player %v says %v", a.PlayerID, name) @@ -49,8 +52,8 @@ func NewActionSayEnvidoQuiero(playerID int) Action { return &ActionSayEnvidoQuiero{act: act{Name: SAY_ENVIDO_QUIERO, PlayerID: playerID}} } -func NewActionSayEnvidoScore(score int, playerID int) Action { - return &ActionSayEnvidoScore{act: act{Name: SAY_ENVIDO_SCORE, PlayerID: playerID}, Score: score} +func NewActionSayEnvidoScore(playerID int) Action { + return &ActionSayEnvidoScore{act: act{Name: SAY_ENVIDO_SCORE, PlayerID: playerID}} } func NewActionSayTrucoQuiero(playerID int) Action { @@ -77,14 +80,22 @@ func NewActionSaySonBuenas(playerID int) Action { return &ActionSaySonBuenas{act: act{Name: SAY_SON_BUENAS, PlayerID: playerID}} } -func NewActionSaySonMejores(score int, playerID int) Action { - return &ActionSaySonMejores{act: act{Name: SAY_SON_MEJORES, PlayerID: playerID}, Score: score} +func NewActionSaySonMejores(playerID int) Action { + return &ActionSaySonMejores{act: act{Name: SAY_SON_MEJORES, PlayerID: playerID}} } func NewActionRevealCard(card Card, playerID int) Action { return &ActionRevealCard{act: act{Name: REVEAL_CARD, PlayerID: playerID}, Card: card} } +func NewActionsRevealCards(playerID int, gameState GameState) []Action { + actions := []Action{} + for _, card := range gameState.Players[playerID].Hand.Unrevealed { + actions = append(actions, NewActionRevealCard(card, playerID)) + } + return actions +} + func NewActionSayMeVoyAlMazo(playerID int) Action { return &ActionSayMeVoyAlMazo{act: act{Name: SAY_ME_VOY_AL_MAZO, PlayerID: playerID}} } @@ -93,8 +104,8 @@ 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} +func NewActionRevealEnvidoScore(playerID int) Action { + return &ActionRevealEnvidoScore{act: act{Name: REVEAL_ENVIDO_SCORE, PlayerID: playerID}} } func (a ActionSaySonMejores) String() string { diff --git a/truco/envido_sequence_test.go b/truco/envido_sequence_test.go index 1b3009d..4c8ca4c 100644 --- a/truco/envido_sequence_test.go +++ b/truco/envido_sequence_test.go @@ -53,7 +53,7 @@ func TestEnvidoSequence(t *testing.T) { name: "cannot start with son_mejores", steps: []testStep{ { - action: NewActionSaySonMejores(10, 0), + action: NewActionSaySonMejores(0), expectedIsValid: false, }, }, @@ -74,7 +74,7 @@ func TestEnvidoSequence(t *testing.T) { }, }, { - name: "basic envido finished sequence with son buenas", + name: "basic envido finished sequence with son mejores", steps: []testStep{ { action: NewActionSayEnvido(0), @@ -87,7 +87,12 @@ func TestEnvidoSequence(t *testing.T) { expectedPlayerTurnAfterRunning: 0, }, { - action: NewActionSaySonBuenas(0), + action: NewActionSayEnvidoScore(0), + expectedIsValid: true, + expectedPlayerTurnAfterRunning: 1, + }, + { + action: NewActionSaySonMejores(1), expectedIsValid: true, expectedPlayerTurnAfterRunning: 0, // doesn't yield turn because envido is over, so player who started gets to play expectedIsFinishedAfterRunning: true, @@ -110,10 +115,15 @@ func TestEnvidoSequence(t *testing.T) { { action: NewActionSayEnvidoQuiero(0), expectedIsValid: true, + expectedPlayerTurnAfterRunning: 0, + }, + { + action: NewActionSayEnvidoScore(0), + expectedIsValid: true, expectedPlayerTurnAfterRunning: 1, }, { - action: NewActionSaySonMejores(31, 1), + action: NewActionSaySonMejores(1), expectedIsValid: true, expectedPlayerTurnAfterRunning: 1, // doesn't yield turn because envido is over, so player who started gets to play expectedIsFinishedAfterRunning: true, @@ -145,8 +155,9 @@ func TestEnvidoSequence(t *testing.T) { } } + step.action.Enrich(*gameState) err := gameState.RunAction(step.action) - require.NoError(t, err) + require.NoError(t, err, "at step %v", i) if step.ignoreAction { continue diff --git a/truco/reveal_envido_score_test.go b/truco/reveal_envido_score_test.go index dcfe7e5..27034b3 100644 --- a/truco/reveal_envido_score_test.go +++ b/truco/reveal_envido_score_test.go @@ -32,11 +32,11 @@ func TestRevealEnvidoScore(t *testing.T) { ignoreAction: true, }, { - action: NewActionSayEnvidoScore(25, 0), + action: NewActionSayEnvidoScore(0), ignoreAction: true, }, { - action: NewActionSaySonMejores(31, 1), + action: NewActionSaySonMejores(1), ignoreAction: true, }, { @@ -54,7 +54,7 @@ func TestRevealEnvidoScore(t *testing.T) { }, // Revealing the envido score is valid { - action: NewActionRevealEnvidoScore(1, 31), + action: NewActionRevealEnvidoScore(1), expectedIsValid: true, expectedPlayerTurnAfterRunning: 1, // doesn't yield turn }, @@ -66,7 +66,7 @@ func TestRevealEnvidoScore(t *testing.T) { }, // Revealing the envido score again is invalid { - action: NewActionRevealEnvidoScore(1, 31), + action: NewActionRevealEnvidoScore(1), expectedIsValid: false, }, }, diff --git a/truco/truco.go b/truco/truco.go index 957b255..b48744f 100644 --- a/truco/truco.go +++ b/truco/truco.go @@ -304,6 +304,10 @@ type Action interface { GetName() string GetPlayerID() int YieldsTurn(g GameState) bool + // Some actions need to be enriched with additional information. + // e.g. a say_truco_quiero action is enriched with "RequiresReminder". + // GameState.CalculatePossibleActions() must call this method on all actions. + Enrich(g GameState) fmt.Stringer } @@ -316,41 +320,26 @@ var ( ) func (g GameState) CalculatePossibleActions() []Action { - - envidoScore := g.Players[g.TurnPlayerID].Hand.EnvidoScore() - opponentEnvidoScore := g.Players[g.TurnOpponentPlayerID].Hand.EnvidoScore() - allActions := []Action{} - - for _, card := range g.Players[g.TurnPlayerID].Hand.Unrevealed { - revealCard := NewActionRevealCard(card, g.TurnPlayerID).(*ActionRevealCard) - // If revealing a card would award envido points, then set the score in the action - if g.canAwardEnvidoPoints(Hand{Revealed: append(g.Players[g.TurnPlayerID].Hand.Revealed, card)}) { - revealCard.EnMesa = true - revealCard.Score = g.Players[g.TurnPlayerID].Hand.EnvidoScore() - } - allActions = append(allActions, revealCard) - } - + allActions = append(allActions, NewActionsRevealCards(g.TurnPlayerID, g)...) allActions = append(allActions, NewActionSayEnvido(g.TurnPlayerID), NewActionSayRealEnvido(g.TurnPlayerID), NewActionSayFaltaEnvido(g.TurnPlayerID), NewActionSayEnvidoQuiero(g.TurnPlayerID), - NewActionSayEnvidoScore(envidoScore, g.TurnPlayerID), + NewActionSayEnvidoScore(g.TurnPlayerID), NewActionSayEnvidoNoQuiero(g.TurnPlayerID), NewActionSayTruco(g.TurnPlayerID), - NewActionSayTrucoQuiero(g.TurnPlayerID).(*ActionSayTrucoQuiero).withRequiresReminder(g), - NewActionSayTrucoNoQuiero(g.TurnPlayerID).(*ActionSayTrucoNoQuiero).withRequiresReminder(g), + NewActionSayTrucoQuiero(g.TurnPlayerID), + NewActionSayTrucoNoQuiero(g.TurnPlayerID), NewActionSayQuieroRetruco(g.TurnPlayerID), NewActionSayQuieroValeCuatro(g.TurnPlayerID), NewActionSaySonBuenas(g.TurnPlayerID), - NewActionSaySonMejores(envidoScore, g.TurnPlayerID), + NewActionSaySonMejores(g.TurnPlayerID), NewActionSayMeVoyAlMazo(g.TurnPlayerID), NewActionConfirmRoundFinished(g.TurnPlayerID), NewActionConfirmRoundFinished(g.TurnOpponentPlayerID), - NewActionRevealEnvidoScore(g.TurnPlayerID, envidoScore), - NewActionRevealEnvidoScore(g.TurnOpponentPlayerID, opponentEnvidoScore), + NewActionRevealEnvidoScore(g.TurnPlayerID), ) // The reveal_envido_score action happens in two cases: @@ -358,16 +347,15 @@ func (g GameState) CalculatePossibleActions() []Action { // 2. Round is going on, but player would win the game by revealing the score // // In both cases, this should be the only action available. So don't check others. - actionRevealEnvidoScore := NewActionRevealEnvidoScore(g.TurnPlayerID, envidoScore) + actionRevealEnvidoScore := NewActionRevealEnvidoScore(g.TurnPlayerID) + actionRevealEnvidoScore.Enrich(g) if actionRevealEnvidoScore.IsPossible(g) { - allActions = []Action{ - actionRevealEnvidoScore, - NewActionRevealEnvidoScore(g.TurnOpponentPlayerID, opponentEnvidoScore), - } + allActions = []Action{actionRevealEnvidoScore} } possibleActions := []Action{} for _, action := range allActions { + action.Enrich(g) if action.IsPossible(g) { possibleActions = append(possibleActions, action) }