Skip to content

Commit

Permalink
Fix issues with flor implementation.
Browse files Browse the repository at this point in the history
  • Loading branch information
marianogappa committed Jul 30, 2024
1 parent ac3332f commit 5e925f6
Show file tree
Hide file tree
Showing 7 changed files with 272 additions and 26 deletions.
11 changes: 11 additions & 0 deletions truco/action_me_voy_al_mazo.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
2 changes: 1 addition & 1 deletion truco/action_reveal_card.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
4 changes: 4 additions & 0 deletions truco/actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {}

Expand Down
57 changes: 38 additions & 19 deletions truco/actions_any_flor.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package truco

import "fmt"

const (
SAY_FLOR = "say_flor"
SAY_CON_FLOR_ME_ACHICO = "say_con_flor_me_achico"
Expand Down Expand Up @@ -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 {
Expand All @@ -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
}
Expand Down Expand Up @@ -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
}
Expand All @@ -215,32 +219,38 @@ 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
}
wonBy := g.RoundsLog[g.RoundNumber].FlorWinnerPlayerID
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
Expand Down Expand Up @@ -305,13 +315,22 @@ 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
}

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
}

Expand Down
185 changes: 185 additions & 0 deletions truco/actions_any_flor_test.go
Original file line number Diff line number Diff line change
@@ -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
}
2 changes: 1 addition & 1 deletion truco/flor_sequence.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading

0 comments on commit 5e925f6

Please sign in to comment.