From 5e925f667d3249ce58b3aa2cacd46fa1e6713bee Mon Sep 17 00:00:00 2001 From: Mariano Gappa Date: Tue, 30 Jul 2024 19:42:35 +0100 Subject: [PATCH] Fix issues with flor implementation. --- truco/action_me_voy_al_mazo.go | 11 ++ truco/action_reveal_card.go | 2 +- truco/actions.go | 4 + truco/actions_any_flor.go | 57 ++++++---- truco/actions_any_flor_test.go | 185 +++++++++++++++++++++++++++++++++ truco/flor_sequence.go | 2 +- truco/truco.go | 37 ++++++- 7 files changed, 272 insertions(+), 26 deletions(-) create mode 100644 truco/actions_any_flor_test.go diff --git a/truco/action_me_voy_al_mazo.go b/truco/action_me_voy_al_mazo.go index 3bb86cb..275ee05 100644 --- a/truco/action_me_voy_al_mazo.go +++ b/truco/action_me_voy_al_mazo.go @@ -11,6 +11,9 @@ func (a ActionSayMeVoyAlMazo) IsPossible(g GameState) bool { if !g.EnvidoSequence.IsEmpty() && !g.IsEnvidoFinished && !g.EnvidoSequence.IsFinished() { return false } + if !g.FlorSequence.IsEmpty() && !g.FlorSequence.IsFinished() { + return false + } if g.IsEnvidoFinished && !g.TrucoSequence.IsEmpty() && !g.TrucoSequence.IsFinished() { return false } @@ -48,3 +51,11 @@ func (a ActionSayMeVoyAlMazo) Run(g *GameState) error { g.IsRoundFinished = true return nil } + +func (a ActionSayMeVoyAlMazo) GetPriority() int { + return 2 +} + +func (a ActionSayMeVoyAlMazo) AllowLowerPriority() bool { + return true +} diff --git a/truco/action_reveal_card.go b/truco/action_reveal_card.go index 51c840f..48ab379 100644 --- a/truco/action_reveal_card.go +++ b/truco/action_reveal_card.go @@ -68,7 +68,7 @@ func (a *ActionRevealCard) Run(g *GameState) error { a.Score = g.Players[a.PlayerID].Hand.EnvidoScore() // it must be the action's player } // Revealing a card may cause the flor score to be revealed - if g.tryAwardFlorPoints(a.PlayerID) { + if g.tryAwardFlorPoints() { a.EnMesa = true a.Score = g.Players[a.PlayerID].Hand.FlorScore() // it must be the action's player } diff --git a/truco/actions.go b/truco/actions.go index 793c9e9..a8b35b7 100644 --- a/truco/actions.go +++ b/truco/actions.go @@ -24,6 +24,10 @@ func (a act) GetPriority() int { return 0 } +func (a act) AllowLowerPriority() bool { + return false +} + // By default, actions don't need to be enriched. func (a act) Enrich(g GameState) {} diff --git a/truco/actions_any_flor.go b/truco/actions_any_flor.go index 9a7e822..d4d3c20 100644 --- a/truco/actions_any_flor.go +++ b/truco/actions_any_flor.go @@ -1,5 +1,7 @@ package truco +import "fmt" + const ( SAY_FLOR = "say_flor" SAY_CON_FLOR_ME_ACHICO = "say_con_flor_me_achico" @@ -91,18 +93,20 @@ func (a ActionSayFlorSonMejores) IsPossible(g GameState) bool { } func (a ActionRevealFlorScore) IsPossible(g GameState) bool { - if !g.anyFlorActionIsPossible(&a) { + if !g.RuleIsFlorEnabled { + return false + } + if !g.Players[a.GetPlayerID()].Hand.HasFlor() { return false } - if g.RoundsLog[g.RoundNumber].FlorWinnerPlayerID != a.PlayerID { + roundLog := g.RoundsLog[g.RoundNumber] + if roundLog.FlorWinnerPlayerID != a.PlayerID { return false } - if !g.IsRoundFinished { + if !g.IsRoundFinished && g.Players[a.PlayerID].Score+roundLog.FlorPoints < g.RuleMaxPoints { return false } - hand := g.Players[a.PlayerID].Hand - revealedHand := Hand{Revealed: g.Players[a.PlayerID].Hand.Revealed} - return revealedHand.FlorScore() == hand.FlorScore() + return len(g.Players[a.PlayerID].Hand.Revealed) != 3 } func (g GameState) anyFlorActionIsPossible(a Action) bool { @@ -112,10 +116,10 @@ func (g GameState) anyFlorActionIsPossible(a Action) bool { if !g.Players[a.GetPlayerID()].Hand.HasFlor() { return false } - if a.GetName() != REVEAL_FLOR_SCORE && g.IsRoundFinished { + if g.IsRoundFinished { return false } - // For any flor action except "say_flor", both players must have flor + // For any flor action except "say_flor" && "reveal_flor_score", both players must have flor if a.GetName() != SAY_FLOR && !g.Players[g.OpponentOf(a.GetPlayerID())].Hand.HasFlor() { return false } @@ -190,11 +194,11 @@ func (a ActionRevealFlorScore) Run(g *GameState) error { return errActionNotPossible } g.IsEnvidoFinished = true - if !g.tryAwardFlorPoints(a.PlayerID) { - return errActionNotPossible + for len(g.Players[a.PlayerID].Hand.Unrevealed) > 0 { + _ = g.Players[a.PlayerID].Hand.RevealCard(g.Players[a.PlayerID].Hand.Unrevealed[0]) } - for _, c := range g.Players[a.PlayerID].Hand.Unrevealed { - _ = g.Players[a.PlayerID].Hand.RevealCard(c) + if !g.tryAwardFlorPoints() { + return fmt.Errorf("cannot award flor points") } return nil } @@ -215,11 +219,11 @@ func finalizeFlorSequence(winnerPlayerID int, g *GameState) error { } g.RoundsLog[g.RoundNumber].FlorWinnerPlayerID = winnerPlayerID g.RoundsLog[g.RoundNumber].FlorPoints = cost - g.tryAwardFlorPoints(winnerPlayerID) + g.tryAwardFlorPoints() return nil } -func (g *GameState) canAwardFlorPoints(revealedHand Hand) bool { +func (g *GameState) canAwardFlorPoints() bool { if !g.RuleIsFlorEnabled { return false } @@ -227,20 +231,26 @@ func (g *GameState) canAwardFlorPoints(revealedHand Hand) bool { if wonBy == -1 { return false } - if !g.FlorSequence.WasAccepted() { + if g.FlorSequence.FlorPointsAwarded { return false } - if g.FlorSequence.FlorPointsAwarded { + // If the flor sequence was finished with "say_con_flor_me_achico", then + // the points can be awarded immediately even though the sequence wasn't accepted + // and the cards weren't revealed. + if !g.FlorSequence.IsEmpty() && g.FlorSequence.IsFinished() && g.FlorSequence.Sequence[len(g.FlorSequence.Sequence)-1] == SAY_CON_FLOR_ME_ACHICO { + return true + } + if !g.FlorSequence.WasAccepted() { return false } - if revealedHand.FlorScore() != g.Players[wonBy].Hand.FlorScore() { + if len(g.Players[wonBy].Hand.Revealed) != 3 { return false } return true } -func (g *GameState) tryAwardFlorPoints(playerID int) bool { - if !g.canAwardFlorPoints(Hand{Revealed: g.Players[playerID].Hand.Revealed}) { +func (g *GameState) tryAwardFlorPoints() bool { + if !g.canAwardFlorPoints() { return false } wonBy := g.RoundsLog[g.RoundNumber].FlorWinnerPlayerID @@ -305,6 +315,11 @@ func (a *ActionRevealFlorScore) Enrich(g GameState) { a.Score = g.Players[a.PlayerID].Hand.FlorScore() } +func (a ActionSayFlor) YieldsTurn(g GameState) bool { + // If the opponent doesn't have flor, then "flor" is just a declaration and the turn continues + return g.Players[g.OpponentOf(a.PlayerID)].Hand.HasFlor() +} + func (a ActionSayFlorSonBuenas) YieldsTurn(g GameState) bool { // In son_buenas/son_mejores/no_quiero, the turn should go to whoever started the sequence return a.PlayerID != g.FlorSequence.StartingPlayerID @@ -312,6 +327,10 @@ func (a ActionSayFlorSonBuenas) YieldsTurn(g GameState) bool { func (a ActionSayFlorSonMejores) YieldsTurn(g GameState) bool { // In son_buenas/son_mejores/no_quiero, the turn should go to whoever started the sequence + // Unless the game should end due to the points won by this action. + if g.Players[a.PlayerID].Score+g.RoundsLog[g.RoundNumber].FlorPoints >= g.RuleMaxPoints { + return false + } return a.PlayerID != g.FlorSequence.StartingPlayerID } diff --git a/truco/actions_any_flor_test.go b/truco/actions_any_flor_test.go new file mode 100644 index 0000000..5bc05be --- /dev/null +++ b/truco/actions_any_flor_test.go @@ -0,0 +1,185 @@ +package truco + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestFlor(t *testing.T) { + type testStep struct { + action Action + expectedPlayerTurnAfterRunning *int + expectedIsFinishedAfterRunning *bool + expectedPossibleActionNamesBefore []string + expectedPossibleActionNamesAfter []string + expectedCustomValidationBeforeAction func(*GameState) + } + + tests := []struct { + name string + hands []Hand + steps []testStep + }{ + { + name: "in a starting hand with flor, player can say flor or leave", + steps: []testStep{ + { + expectedPossibleActionNamesBefore: []string{SAY_FLOR, SAY_ME_VOY_AL_MAZO}, + action: NewActionSayFlor(0), + }, + }, + }, + { + name: "if both players have flor, opponent can accept, decline, raise or leave, but nothing else", + steps: []testStep{ + { + expectedPossibleActionNamesBefore: []string{SAY_FLOR, SAY_ME_VOY_AL_MAZO}, + action: NewActionSayFlor(0), + }, + { + expectedPossibleActionNamesBefore: []string{ + SAY_CONTRAFLOR, + SAY_CONTRAFLOR_AL_RESTO, + SAY_CON_FLOR_ME_ACHICO, + SAY_CON_FLOR_QUIERO, + }, + }, + }, + }, + { + name: "if first player says flor, envido is finished", + hands: []Hand{ + {Unrevealed: []Card{{Number: 1, Suit: ORO}, {Number: 2, Suit: ORO}, {Number: 3, Suit: ORO}}}, // 26 + {Unrevealed: []Card{{Number: 4, Suit: COPA}, {Number: 5, Suit: ORO}, {Number: 6, Suit: ORO}}}, // no flor + }, + steps: []testStep{ + { + expectedPossibleActionNamesBefore: []string{SAY_FLOR, SAY_ME_VOY_AL_MAZO}, + action: NewActionSayFlor(0), + }, + { + expectedCustomValidationBeforeAction: func(g *GameState) { + require.True(t, g.IsEnvidoFinished) + }, + }, + }, + }, + { + name: "if first player says flor and opponent does'nt have flor, actions are different, and 3 points are not won yet!", + hands: []Hand{ + {Unrevealed: []Card{{Number: 1, Suit: ORO}, {Number: 2, Suit: ORO}, {Number: 3, Suit: ORO}}}, // 26 + {Unrevealed: []Card{{Number: 4, Suit: COPA}, {Number: 5, Suit: ORO}, {Number: 6, Suit: ORO}}}, // no flor + }, + steps: []testStep{ + { + expectedPossibleActionNamesBefore: []string{SAY_FLOR, SAY_ME_VOY_AL_MAZO}, + action: NewActionSayFlor(0), + }, + { + expectedPossibleActionNamesBefore: []string{ + REVEAL_CARD, + REVEAL_CARD, + REVEAL_CARD, + SAY_TRUCO, + SAY_ME_VOY_AL_MAZO, + }, + expectedCustomValidationBeforeAction: func(g *GameState) { + require.Equal(t, g.Players[0].Score, 0) + }, + }, + }, + }, + { + name: "3 flor points are won when cards are revealed", + hands: []Hand{ + {Unrevealed: []Card{{Number: 1, Suit: ORO}, {Number: 2, Suit: ORO}, {Number: 3, Suit: ORO}}}, // 26 + {Unrevealed: []Card{{Number: 4, Suit: COPA}, {Number: 5, Suit: ORO}, {Number: 6, Suit: ORO}}}, // no flor + }, + steps: []testStep{ + { + action: NewActionSayFlor(0), + }, + { + action: NewActionRevealCard(Card{Number: 1, Suit: ORO}, 0), + }, + { + action: NewActionRevealCard(Card{Number: 4, Suit: COPA}, 1), + }, + { + action: NewActionSayMeVoyAlMazo(0), + }, + { + action: NewActionRevealFlorScore(0), + }, + { + expectedCustomValidationBeforeAction: func(g *GameState) { + require.Equal(t, 3, g.Players[0].Score) + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // if tt.name != "if first player says flor and opponent does'nt have flor, actions are different" { + // t.Skip() + // } + defaultHands := []Hand{ + {Unrevealed: []Card{{Number: 1, Suit: ORO}, {Number: 2, Suit: ORO}, {Number: 3, Suit: ORO}}}, // 26 + {Unrevealed: []Card{{Number: 4, Suit: ORO}, {Number: 5, Suit: ORO}, {Number: 6, Suit: ORO}}}, // 35 + } + if len(tt.hands) == 0 { + tt.hands = defaultHands + } + gameState := New(withDeck(newTestDeck(tt.hands)), WithFlorEnabled(true)) + + require.Equal(t, 0, gameState.TurnPlayerID) + + for i, step := range tt.steps { + + if step.expectedPossibleActionNamesBefore != nil { + actualAvailableActionNamesBefore := []string{} + for _, a := range gameState.CalculatePossibleActions() { + actualAvailableActionNamesBefore = append(actualAvailableActionNamesBefore, a.GetName()) + } + assert.ElementsMatch(t, step.expectedPossibleActionNamesBefore, actualAvailableActionNamesBefore, "at step %v", i) + } + + if step.expectedCustomValidationBeforeAction != nil { + step.expectedCustomValidationBeforeAction(gameState) + } + + if step.action == nil { + continue + } + + step.action.Enrich(*gameState) + err := gameState.RunAction(step.action) + require.NoError(t, err, "at step %v", i) + + if step.expectedPossibleActionNamesAfter != nil { + actualAvailableActionNamesAfter := []string{} + for _, a := range gameState.CalculatePossibleActions() { + actualAvailableActionNamesAfter = append(actualAvailableActionNamesAfter, a.GetName()) + } + assert.ElementsMatch(t, step.expectedPossibleActionNamesAfter, actualAvailableActionNamesAfter, "at step %v", i) + } + + if step.expectedPlayerTurnAfterRunning != nil { + assert.Equal(t, *step.expectedPlayerTurnAfterRunning, gameState.TurnPlayerID, "at step %v expected player turn %v but got %v", i, step.expectedPlayerTurnAfterRunning, gameState.TurnPlayerID) + } + + if step.expectedIsFinishedAfterRunning != nil { + assert.Equal(t, *step.expectedIsFinishedAfterRunning, gameState.EnvidoSequence.IsFinished(), "at step %v expected isFinished to be %v but wasn't", i, step.expectedIsFinishedAfterRunning) + } + } + }) + } +} + +func _p[T any](v T) *T { + return &v +} diff --git a/truco/flor_sequence.go b/truco/flor_sequence.go index 5532c46..2965511 100644 --- a/truco/flor_sequence.go +++ b/truco/flor_sequence.go @@ -20,8 +20,8 @@ var ( // All "me achico" _s(SAY_FLOR, SAY_CON_FLOR_ME_ACHICO): 3, _s(SAY_FLOR, SAY_CONTRAFLOR, SAY_CON_FLOR_ME_ACHICO): 4, - _s(SAY_FLOR, SAY_CONTRAFLOR, SAY_CONTRAFLOR_AL_RESTO, SAY_CON_FLOR_ME_ACHICO): 6, _s(SAY_FLOR, SAY_CONTRAFLOR_AL_RESTO, SAY_CON_FLOR_ME_ACHICO): 4, + _s(SAY_FLOR, SAY_CONTRAFLOR, SAY_CONTRAFLOR_AL_RESTO, SAY_CON_FLOR_ME_ACHICO): 6, // "quiero" to "flor" _s(SAY_FLOR, SAY_CON_FLOR_QUIERO): 4, diff --git a/truco/truco.go b/truco/truco.go index 988888a..b8bab7e 100644 --- a/truco/truco.go +++ b/truco/truco.go @@ -225,11 +225,11 @@ func (g *GameState) RunAction(action Action) error { } if !action.IsPossible(*g) { - return errActionNotPossible + return fmt.Errorf("%w trying to run [%v]", errActionNotPossible, action) } err := action.Run(g) if err != nil { - return err + return fmt.Errorf("%w trying to run [%v] after checking it was possible", err, action) } if action.GetName() != CONFIRM_ROUND_FINISHED { @@ -253,7 +253,7 @@ func (g *GameState) RunAction(action Action) error { if !g.IsGameEnded && g.IsRoundFinished && len(g.RoundFinishedConfirmedPlayerIDs) == 1 { if g.RoundFinishedConfirmedPlayerIDs[g.TurnPlayerID] { - g.TurnPlayerID, g.TurnOpponentPlayerID = g.TurnOpponentPlayerID, g.TurnPlayerID + g.changeTurn() } } @@ -267,6 +267,12 @@ func (g *GameState) RunAction(action Action) error { } possibleActions := g.CalculatePossibleActions() + if g.countActionsOfTurnPlayer() == 0 { + // If the current player has no actions left, it's the opponent's turn. + g.changeTurn() + possibleActions = g.CalculatePossibleActions() + } + g.PossibleActions = _serializeActions(possibleActions) log.Printf("Possible actions: %v\n", possibleActions) @@ -274,6 +280,20 @@ func (g *GameState) RunAction(action Action) error { return nil } +func (g *GameState) changeTurn() { + g.TurnPlayerID, g.TurnOpponentPlayerID = g.TurnOpponentPlayerID, g.TurnPlayerID +} + +func (g GameState) countActionsOfTurnPlayer() int { + count := 0 + for _, a := range g.CalculatePossibleActions() { + if a.GetPlayerID() == g.TurnPlayerID { + count++ + } + } + return count +} + func (g GameState) OpponentOf(playerID int) int { for id := range g.Players { if id != playerID { @@ -344,6 +364,8 @@ type Action interface { // For example, if Flor is possible, then it should be higher priority. GetPriority() int + AllowLowerPriority() bool + fmt.Stringer } @@ -371,10 +393,10 @@ func (g GameState) CalculatePossibleActions() []Action { NewActionSayQuieroValeCuatro(g.TurnPlayerID), NewActionSaySonBuenas(g.TurnPlayerID), NewActionSaySonMejores(g.TurnPlayerID), - NewActionSayMeVoyAlMazo(g.TurnPlayerID), NewActionConfirmRoundFinished(g.TurnPlayerID), NewActionConfirmRoundFinished(g.TurnOpponentPlayerID), NewActionRevealEnvidoScore(g.TurnPlayerID), + NewActionRevealEnvidoScore(g.TurnOpponentPlayerID), NewActionSayFlor(g.TurnPlayerID), NewActionSayContraflor(g.TurnPlayerID), NewActionSayContraflorAlResto(g.TurnPlayerID), @@ -384,6 +406,8 @@ func (g GameState) CalculatePossibleActions() []Action { NewActionSayFlorSonBuenas(g.TurnPlayerID), NewActionSayFlorSonMejores(g.TurnPlayerID), NewActionRevealFlorScore(g.TurnPlayerID), + NewActionRevealFlorScore(g.TurnOpponentPlayerID), + NewActionSayMeVoyAlMazo(g.TurnPlayerID), ) possibleActions := []Action{} @@ -393,7 +417,10 @@ func (g GameState) CalculatePossibleActions() []Action { if !action.IsPossible(g) { continue } - if action.GetPriority() > priority { + if action.GetPriority() < priority { + continue + } + if action.GetPriority() > priority && !action.AllowLowerPriority() { priority = action.GetPriority() possibleActions = []Action{} }