diff --git a/examplebot/bot.go b/examplebot/bot.go index 87f2121..59b79bf 100644 --- a/examplebot/bot.go +++ b/examplebot/bot.go @@ -233,20 +233,38 @@ func shouldAnyEnvido(gs truco.ClientGameState, aggresiveness string, log func(st } func shouldAnyFlor(gs truco.ClientGameState, aggresiveness string, log func(string, ...any)) bool { + // If bot doesn't have flor, bot shouldn't say flor + if calculateFlorScore(gs) == 0 { + log("I don't have flor, so I'm not going to say flor.") + return false + } + + possible := possibleActionsMap(gs) + + // If human doesn't necessarily have flor, and bot has flor then bot should say flor + if quieroActions := filter(possible, truco.NewActionSayConFlorQuiero(gs.YouPlayerID)); len(quieroActions) == 0 { + log("Human doesn't necessarily have flor, so I'm going to say flor.") + return true + } + // if "no quiero" is possible and saying no quiero means losing, return true - // possible := possibleActionsMap(gs) - // noQuieroActions := filter(possible, truco.NewActionSayConFlorMeAchico(gs.YouPlayerID)) - // if len(noQuieroActions) > 0 { - // cost := noQuieroActions[0].(*truco.ActionSayConFlorMeAchico).Cost - // if gs.TheirScore+cost >= gs.RuleMaxPoints { - // return true - // } - // } - // //TODO - // return true + noQuieroActions := filter(possible, truco.NewActionSayConFlorMeAchico(gs.YouPlayerID)) + if len(noQuieroActions) > 0 { + log("Bot can say no quiero to flor, and saying no quiero might mean losing.") + cost := noQuieroActions[0].(*truco.ActionSayConFlorMeAchico).Cost + if gs.TheirScore+cost >= gs.RuleMaxPoints { + log("Bot should say quiero to flor, because bot loses otherwise.") + return true + } + } - // In principle let's always choose an action, since flor is unlikely to be matched once one has it - return true + // Both have flor and saying no quiero doesn't lose the game. At this point it depends on the score and the aggresiveness + log("Bot has flor, and human has flor. Bot's flor score is %v, and aggresiveness is: %v", calculateFlorScore(gs), aggresiveness) + return calculateFlorScore(gs) >= map[string]int{ + "low": 31, + "normal": 29, + "high": 26, + }[aggresiveness] } func chooseFlorAction(gs truco.ClientGameState, aggresiveness string) truco.Action { @@ -758,7 +776,7 @@ func (m Bot) ChooseAction(gs truco.ClientGameState) truco.Action { return truco.NewActionSayConFlorMeAchico(gs.YouPlayerID) } if shouldFlor { - // This is the case where the bot initiates the envido + // This is the case where the bot initiates the flor // Sometimes (<50%), a human player would hide their envido by not initiating, and hoping the other says it first // TODO: should this chance based on aggresiveness? if rand.Float64() < 0.67 { diff --git a/truco/action_any_quiero.go b/truco/action_any_quiero.go index d6716fb..02f16d5 100644 --- a/truco/action_any_quiero.go +++ b/truco/action_any_quiero.go @@ -93,7 +93,7 @@ func (a ActionSayEnvidoNoQuiero) Run(g *GameState) error { } g.EnvidoSequence.AddStep(a.GetName()) g.IsEnvidoFinished = true - cost, err := g.EnvidoSequence.Cost(g.RuleMaxPoints, g.Players[g.TurnPlayerID].Score, g.Players[g.TurnOpponentPlayerID].Score) + cost, err := g.EnvidoSequence.Cost(g.RuleMaxPoints, g.Players[g.TurnPlayerID].Score, g.Players[g.TurnOpponentPlayerID].Score, false) if err != nil { return err } @@ -170,11 +170,17 @@ func (a *ActionSayEnvidoQuiero) Enrich(g GameState) { return } var ( - youScore = g.Players[a.GetPlayerID()].Score - theirScore = g.Players[g.OpponentOf(a.GetPlayerID())].Score - quieroSeq, _ = g.EnvidoSequence.WithStep(SAY_ENVIDO_QUIERO) - quieroCost, _ = quieroSeq.Cost(g.RuleMaxPoints, youScore, theirScore) + youScore = g.Players[a.GetPlayerID()].Score + theirScore = g.Players[g.OpponentOf(a.GetPlayerID())].Score + quieroSeq, err = g.EnvidoSequence.WithStep(SAY_ENVIDO_QUIERO) + quieroCost, err2 = quieroSeq.Cost(g.RuleMaxPoints, youScore, theirScore, true) ) + if err != nil { + panic(err) + } + if err2 != nil { + panic(err2) + } a.Cost = quieroCost } @@ -183,11 +189,17 @@ func (a *ActionSayEnvidoNoQuiero) Enrich(g GameState) { return } var ( - youScore = g.Players[a.GetPlayerID()].Score - theirScore = g.Players[g.OpponentOf(a.GetPlayerID())].Score - noQuieroSeq, _ = g.EnvidoSequence.WithStep(SAY_ENVIDO_NO_QUIERO) - noQuieroCost, _ = noQuieroSeq.Cost(g.RuleMaxPoints, youScore, theirScore) + youScore = g.Players[a.GetPlayerID()].Score + theirScore = g.Players[g.OpponentOf(a.GetPlayerID())].Score + noQuieroSeq, err = g.EnvidoSequence.WithStep(SAY_ENVIDO_NO_QUIERO) + noQuieroCost, err2 = noQuieroSeq.Cost(g.RuleMaxPoints, youScore, theirScore, true) ) + if err != nil { + panic(err) + } + if err2 != nil { + panic(err2) + } a.Cost = noQuieroCost } diff --git a/truco/action_me_voy_al_mazo.go b/truco/action_me_voy_al_mazo.go index 275ee05..dad3b81 100644 --- a/truco/action_me_voy_al_mazo.go +++ b/truco/action_me_voy_al_mazo.go @@ -17,6 +17,9 @@ func (a ActionSayMeVoyAlMazo) IsPossible(g GameState) bool { if g.IsEnvidoFinished && !g.TrucoSequence.IsEmpty() && !g.TrucoSequence.IsFinished() { return false } + if NewActionRevealFlorScore(a.PlayerID).IsPossible(g) || NewActionRevealEnvidoScore(a.PlayerID).IsPossible(g) { + return false + } return true } diff --git a/truco/action_reveal_card.go b/truco/action_reveal_card.go index 48ab379..25c6f1a 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() { + if ok, _ := g.tryAwardFlorPoints(); ok { a.EnMesa = true a.Score = g.Players[a.PlayerID].Hand.FlorScore() // it must be the action's player } diff --git a/truco/action_son_buenas.go b/truco/action_son_buenas.go index 8f089de..daa40a5 100644 --- a/truco/action_son_buenas.go +++ b/truco/action_son_buenas.go @@ -38,7 +38,7 @@ func (a ActionSaySonBuenas) Run(g *GameState) error { return errActionNotPossible } g.EnvidoSequence.AddStep(a.GetName()) - cost, err := g.EnvidoSequence.Cost(g.RuleMaxPoints, g.Players[g.TurnPlayerID].Score, g.Players[g.TurnOpponentPlayerID].Score) + cost, err := g.EnvidoSequence.Cost(g.RuleMaxPoints, g.Players[g.TurnPlayerID].Score, g.Players[g.TurnOpponentPlayerID].Score, false) if err != nil { return err } diff --git a/truco/action_son_mejores.go b/truco/action_son_mejores.go index 1128a12..fad51a4 100644 --- a/truco/action_son_mejores.go +++ b/truco/action_son_mejores.go @@ -37,7 +37,7 @@ func (a ActionSaySonMejores) Run(g *GameState) error { return errActionNotPossible } g.EnvidoSequence.AddStep(a.GetName()) - cost, err := g.EnvidoSequence.Cost(g.RuleMaxPoints, g.Players[g.TurnPlayerID].Score, g.Players[g.TurnOpponentPlayerID].Score) + cost, err := g.EnvidoSequence.Cost(g.RuleMaxPoints, g.Players[g.TurnPlayerID].Score, g.Players[g.TurnOpponentPlayerID].Score, false) if err != nil { return err } diff --git a/truco/actions_any_envido.go b/truco/actions_any_envido.go index 67eaacd..5e2d4fc 100644 --- a/truco/actions_any_envido.go +++ b/truco/actions_any_envido.go @@ -195,13 +195,26 @@ func (g GameState) AnyEnvidoActionTypeEnrich(a Action) { return } var ( - youScore = g.Players[a.GetPlayerID()].Score - theirScore = g.Players[g.OpponentOf(a.GetPlayerID())].Score - quieroSeq, _ = g.EnvidoSequence.WithStep(SAY_ENVIDO_QUIERO) - quieroCost, _ = quieroSeq.Cost(g.RuleMaxPoints, youScore, theirScore) - noQuieroSeq, _ = g.EnvidoSequence.WithStep(SAY_ENVIDO_NO_QUIERO) - noQuieroCost, _ = noQuieroSeq.Cost(g.RuleMaxPoints, youScore, theirScore) + seq, _ = g.EnvidoSequence.WithStep(a.GetName()) + youScore = g.Players[a.GetPlayerID()].Score + theirScore = g.Players[g.OpponentOf(a.GetPlayerID())].Score + quieroSeq, err = seq.WithStep(SAY_ENVIDO_QUIERO) + quieroCost, err2 = quieroSeq.Cost(g.RuleMaxPoints, youScore, theirScore, true) + noQuieroSeq, err3 = seq.WithStep(SAY_ENVIDO_NO_QUIERO) + noQuieroCost, err4 = noQuieroSeq.Cost(g.RuleMaxPoints, youScore, theirScore, true) ) + if err != nil { + panic(err) + } + if err2 != nil { + panic(err2) + } + if err3 != nil { + panic(err3) + } + if err4 != nil { + panic(err4) + } switch a.GetName() { case SAY_ENVIDO: diff --git a/truco/actions_any_envido_test.go b/truco/actions_any_envido_test.go new file mode 100644 index 0000000..c661d0b --- /dev/null +++ b/truco/actions_any_envido_test.go @@ -0,0 +1,100 @@ +package truco + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestEnvido(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: "it is still possible to say envido after opponent's first card is revealed", + steps: []testStep{ + { + action: NewActionRevealCard(Card{Number: 1, Suit: COPA}, 0), + }, + { + expectedPossibleActionNamesBefore: []string{ + REVEAL_CARD, + REVEAL_CARD, + REVEAL_CARD, + SAY_ENVIDO, + SAY_REAL_ENVIDO, + SAY_FALTA_ENVIDO, + SAY_TRUCO, + SAY_ME_VOY_AL_MAZO, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + defaultHands := []Hand{ + {Unrevealed: []Card{{Number: 1, Suit: COPA}, {Number: 2, Suit: ORO}, {Number: 3, Suit: ORO}}}, + {Unrevealed: []Card{{Number: 4, Suit: COPA}, {Number: 5, Suit: ORO}, {Number: 6, Suit: ORO}}}, + } + 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) + } + } + }) + } +} diff --git a/truco/actions_any_flor.go b/truco/actions_any_flor.go index d4d3c20..a3d4968 100644 --- a/truco/actions_any_flor.go +++ b/truco/actions_any_flor.go @@ -16,23 +16,23 @@ const ( type ActionSayFlor struct { act - QuieroCost int + QuieroCost int `json:"quiero_cost"` } type ActionSayConFlorMeAchico struct { act - Cost int + Cost int `json:"cost"` } type ActionSayContraflor struct { act - QuieroCost int + QuieroCost int `json:"quiero_cost"` } type ActionSayContraflorAlResto struct { act - QuieroCost int + QuieroCost int `json:"quiero_cost"` } type ActionSayConFlorQuiero struct { act - Cost int + Cost int `json:"cost"` } type ActionSayFlorScore struct { act @@ -103,6 +103,9 @@ func (a ActionRevealFlorScore) IsPossible(g GameState) bool { if roundLog.FlorWinnerPlayerID != a.PlayerID { return false } + if g.FlorSequence.FlorPointsAwarded { + return false + } if !g.IsRoundFinished && g.Players[a.PlayerID].Score+roundLog.FlorPoints < g.RuleMaxPoints { return false } @@ -197,8 +200,8 @@ func (a ActionRevealFlorScore) Run(g *GameState) error { for len(g.Players[a.PlayerID].Hand.Unrevealed) > 0 { _ = g.Players[a.PlayerID].Hand.RevealCard(g.Players[a.PlayerID].Hand.Unrevealed[0]) } - if !g.tryAwardFlorPoints() { - return fmt.Errorf("cannot award flor points") + if ok, err := g.tryAwardFlorPoints(); !ok { + return fmt.Errorf("cannot award flor points: %w", err) } return nil } @@ -213,96 +216,138 @@ func (g *GameState) anyFlorActionRun(a Action) error { } func finalizeFlorSequence(winnerPlayerID int, g *GameState) error { - cost, err := g.FlorSequence.Cost(g.RuleMaxPoints, g.Players[winnerPlayerID].Score, g.Players[g.OpponentOf(winnerPlayerID)].Score) + cost, err := g.FlorSequence.Cost(g.RuleMaxPoints, g.Players[winnerPlayerID].Score, g.Players[g.OpponentOf(winnerPlayerID)].Score, false) if err != nil { return err } g.RoundsLog[g.RoundNumber].FlorWinnerPlayerID = winnerPlayerID g.RoundsLog[g.RoundNumber].FlorPoints = cost - g.tryAwardFlorPoints() + _, _ = g.tryAwardFlorPoints() return nil } -func (g *GameState) canAwardFlorPoints() bool { +func (g *GameState) canAwardFlorPoints() (bool, error) { if !g.RuleIsFlorEnabled { - return false + return false, fmt.Errorf("flor is not enabled") } wonBy := g.RoundsLog[g.RoundNumber].FlorWinnerPlayerID if wonBy == -1 { - return false + return false, fmt.Errorf("no player won the flor") } if g.FlorSequence.FlorPointsAwarded { - return false + return false, fmt.Errorf("flor points already awarded") } // 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 + return true, nil } if !g.FlorSequence.WasAccepted() { - return false + return false, fmt.Errorf("flor sequence was not accepted") } if len(g.Players[wonBy].Hand.Revealed) != 3 { - return false + return false, fmt.Errorf("player %v has not revealed all cards", wonBy) } - return true + return true, nil } -func (g *GameState) tryAwardFlorPoints() bool { - if !g.canAwardFlorPoints() { - return false +func (g *GameState) tryAwardFlorPoints() (bool, error) { + if ok, err := g.canAwardFlorPoints(); !ok { + return false, err } wonBy := g.RoundsLog[g.RoundNumber].FlorWinnerPlayerID score := g.RoundsLog[g.RoundNumber].FlorPoints g.Players[wonBy].Score += score g.FlorSequence.FlorPointsAwarded = true - return true + return true, nil } func (a *ActionSayFlor) Enrich(g GameState) { var ( - youScore = g.Players[a.GetPlayerID()].Score - theirScore = g.Players[g.OpponentOf(a.GetPlayerID())].Score - quieroSeq, _ = g.EnvidoSequence.WithStep(SAY_CON_FLOR_QUIERO) - quieroCost, _ = quieroSeq.Cost(g.RuleMaxPoints, youScore, theirScore) + seq, err0 = g.FlorSequence.WithStep(SAY_FLOR) + youScore = g.Players[a.GetPlayerID()].Score + theirScore = g.Players[g.OpponentOf(a.GetPlayerID())].Score + quieroSeq, err = seq.WithStep(SAY_CON_FLOR_QUIERO) + quieroCost, err2 = quieroSeq.Cost(g.RuleMaxPoints, youScore, theirScore, true) ) + if err0 != nil { + return + } + if err != nil { + panic(fmt.Errorf("when enriching %v: %w", a.GetName(), err)) + } + if err2 != nil { + panic(fmt.Errorf("when enriching %v: %w", a.GetName(), err2)) + } a.QuieroCost = quieroCost } func (a *ActionSayContraflor) Enrich(g GameState) { var ( - youScore = g.Players[a.GetPlayerID()].Score - theirScore = g.Players[g.OpponentOf(a.GetPlayerID())].Score - quieroSeq, _ = g.EnvidoSequence.WithStep(SAY_CON_FLOR_QUIERO) - quieroCost, _ = quieroSeq.Cost(g.RuleMaxPoints, youScore, theirScore) + seq, err0 = g.FlorSequence.WithStep(SAY_CONTRAFLOR) + youScore = g.Players[a.GetPlayerID()].Score + theirScore = g.Players[g.OpponentOf(a.GetPlayerID())].Score + quieroSeq, err = seq.WithStep(SAY_CON_FLOR_QUIERO) + quieroCost, err2 = quieroSeq.Cost(g.RuleMaxPoints, youScore, theirScore, true) ) + if err0 != nil { + return + } + if err != nil { + panic(fmt.Errorf("when enriching %v: %w", a.GetName(), err)) + } + if err2 != nil { + panic(fmt.Errorf("when enriching %v: %w", a.GetName(), err2)) + } a.QuieroCost = quieroCost } func (a *ActionSayContraflorAlResto) Enrich(g GameState) { var ( - youScore = g.Players[a.GetPlayerID()].Score - theirScore = g.Players[g.OpponentOf(a.GetPlayerID())].Score - quieroSeq, _ = g.EnvidoSequence.WithStep(SAY_CON_FLOR_QUIERO) - quieroCost, _ = quieroSeq.Cost(g.RuleMaxPoints, youScore, theirScore) + seq, err0 = g.FlorSequence.WithStep(SAY_CONTRAFLOR_AL_RESTO) + youScore = g.Players[a.GetPlayerID()].Score + theirScore = g.Players[g.OpponentOf(a.GetPlayerID())].Score + quieroSeq, err = seq.WithStep(SAY_CON_FLOR_QUIERO) + quieroCost, err2 = quieroSeq.Cost(g.RuleMaxPoints, youScore, theirScore, true) ) + if err0 != nil { + return + } + if err != nil { + panic(fmt.Errorf("when enriching %v: %w", a.GetName(), err)) + } + if err2 != nil { + panic(fmt.Errorf("when enriching %v: %w", a.GetName(), err2)) + } a.QuieroCost = quieroCost } func (a *ActionSayConFlorMeAchico) Enrich(g GameState) { var ( - youScore = g.Players[a.GetPlayerID()].Score - theirScore = g.Players[g.OpponentOf(a.GetPlayerID())].Score - noQuieroSeq, _ = g.EnvidoSequence.WithStep(SAY_CON_FLOR_ME_ACHICO) - noQuieroCost, _ = noQuieroSeq.Cost(g.RuleMaxPoints, youScore, theirScore) + seq, err0 = g.FlorSequence.WithStep(SAY_CON_FLOR_ME_ACHICO) + youScore = g.Players[a.GetPlayerID()].Score + theirScore = g.Players[g.OpponentOf(a.GetPlayerID())].Score + noQuieroCost, err = seq.Cost(g.RuleMaxPoints, youScore, theirScore, true) ) + if err0 != nil { + return + } + if err != nil { + panic(fmt.Errorf("when enriching %v: %w", a.GetName(), err)) + } a.Cost = noQuieroCost } func (a *ActionSayConFlorQuiero) Enrich(g GameState) { var ( - youScore = g.Players[a.GetPlayerID()].Score - theirScore = g.Players[g.OpponentOf(a.GetPlayerID())].Score - quieroSeq, _ = g.EnvidoSequence.WithStep(SAY_CON_FLOR_QUIERO) - quieroCost, _ = quieroSeq.Cost(g.RuleMaxPoints, youScore, theirScore) + seq, err0 = g.FlorSequence.WithStep(SAY_CON_FLOR_QUIERO) + youScore = g.Players[a.GetPlayerID()].Score + theirScore = g.Players[g.OpponentOf(a.GetPlayerID())].Score + quieroCost, err = seq.Cost(g.RuleMaxPoints, youScore, theirScore, true) ) + if err0 != nil { + return + } + if err != nil { + panic(fmt.Errorf("when enriching %v: %w", a.GetName(), err)) + } a.Cost = quieroCost } func (a *ActionSayFlorScore) Enrich(g GameState) { @@ -316,8 +361,16 @@ func (a *ActionRevealFlorScore) Enrich(g GameState) { } 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() + // If the opponent has flor too, it yields turn + if g.Players[g.OpponentOf(a.PlayerID)].Hand.HasFlor() { + return true + } + // If they don't, the turn should go to "mano", except if the opponent just said "truco" + actionsOpponent := _deserializeCurrentRoundActionsByPlayerID(g.OpponentOf(a.PlayerID), g) + if len(actionsOpponent) > 0 && actionsOpponent[len(actionsOpponent)-1].GetName() == SAY_TRUCO { + return false + } + return g.RoundTurnPlayerID != a.PlayerID } func (a ActionSayFlorSonBuenas) YieldsTurn(g GameState) bool { diff --git a/truco/actions_any_flor_test.go b/truco/actions_any_flor_test.go index 5bc05be..11d0278 100644 --- a/truco/actions_any_flor_test.go +++ b/truco/actions_any_flor_test.go @@ -67,7 +67,7 @@ func TestFlor(t *testing.T) { }, }, { - name: "if first player says flor and opponent does'nt have flor, actions are different, and 3 points are not won yet!", + name: "if first player says flor and opponent doesn't 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 @@ -120,6 +120,60 @@ func TestFlor(t *testing.T) { }, }, }, + { + name: "mano says envido, opponent says flor, turn should go to mano", + hands: []Hand{ + {Unrevealed: []Card{{Number: 1, Suit: COPA}, {Number: 2, Suit: ORO}, {Number: 3, Suit: ORO}}}, // no flor + {Unrevealed: []Card{{Number: 4, Suit: ORO}, {Number: 5, Suit: ORO}, {Number: 6, Suit: ORO}}}, // flor + }, + steps: []testStep{ + { + action: NewActionSayEnvido(0), + }, + { + action: NewActionSayFlor(1), + expectedPlayerTurnAfterRunning: _p(0), + }, + }, + }, + { + name: "mano says truco, opponent says flor, mano doesn't have flor, turn should stay with opponent to answer thr truco", + hands: []Hand{ + {Unrevealed: []Card{{Number: 1, Suit: COPA}, {Number: 2, Suit: ORO}, {Number: 3, Suit: ORO}}}, // no flor + {Unrevealed: []Card{{Number: 4, Suit: ORO}, {Number: 5, Suit: ORO}, {Number: 6, Suit: ORO}}}, // flor + }, + steps: []testStep{ + { + action: NewActionSayTruco(0), + }, + { + action: NewActionSayFlor(1), + expectedPlayerTurnAfterRunning: _p(1), + }, + { + action: NewActionSayTrucoQuiero(1), + }, + }, + }, + { + name: "mano says flor, opponent says me achico, mano says me voy, there shouldn't be a reveal score action", + hands: []Hand{ + {Unrevealed: []Card{{Number: 1, Suit: ORO}, {Number: 2, Suit: ORO}, {Number: 3, Suit: ORO}}}, // flor + {Unrevealed: []Card{{Number: 4, Suit: ORO}, {Number: 5, Suit: ORO}, {Number: 6, Suit: ORO}}}, // flor + }, + steps: []testStep{ + { + action: NewActionSayFlor(0), + }, + { + action: NewActionSayConFlorMeAchico(1), + }, + { + action: NewActionSayMeVoyAlMazo(0), + expectedPossibleActionNamesAfter: []string{CONFIRM_ROUND_FINISHED, CONFIRM_ROUND_FINISHED}, + }, + }, + }, } for _, tt := range tests { @@ -169,11 +223,11 @@ func TestFlor(t *testing.T) { } 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) + 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) + assert.Equal(t, *step.expectedIsFinishedAfterRunning, gameState.EnvidoSequence.IsFinished(), "at step %v expected isFinished to be %v but wasn't", i, *step.expectedIsFinishedAfterRunning) } } }) diff --git a/truco/deck.go b/truco/deck.go index f31676c..ef0e86c 100644 --- a/truco/deck.go +++ b/truco/deck.go @@ -200,6 +200,10 @@ func (d *deck) defaultDealHand() *Hand { hand.Unrevealed = append(hand.Unrevealed, d.cards[i]) } d.cards = d.cards[3:] + // if !hand.HasFlor() { + // d.shuffle() + // return d.defaultDealHand() + // } return hand } diff --git a/truco/envido_sequence.go b/truco/envido_sequence.go index 1b416e4..9f08904 100644 --- a/truco/envido_sequence.go +++ b/truco/envido_sequence.go @@ -139,7 +139,7 @@ func (es *EnvidoSequence) IsFinished() bool { return last == SAY_SON_BUENAS || last == SAY_SON_MEJORES || last == SAY_ENVIDO_NO_QUIERO } -func (es EnvidoSequence) Cost(maxPoints, winnerPlayerScore, loserPlayerScore int) (int, error) { +func (es EnvidoSequence) Cost(maxPoints, winnerPlayerScore, loserPlayerScore int, forHint bool) (int, error) { if !es.isValid() { return COST_NOT_READY, errInvalidEnvidoSequence } @@ -147,6 +147,10 @@ func (es EnvidoSequence) Cost(maxPoints, winnerPlayerScore, loserPlayerScore int if cost == COST_FALTA_ENVIDO { return calculateFaltaEnvidoCost(maxPoints, winnerPlayerScore, loserPlayerScore), nil } + // If this is a hint, return the cost as is, without checking if the sequence is finished. + if forHint { + return cost, nil + } if !es.IsFinished() { return cost, errUnfinishedEnvidoSequence } diff --git a/truco/flor_sequence.go b/truco/flor_sequence.go index 2965511..a243e36 100644 --- a/truco/flor_sequence.go +++ b/truco/flor_sequence.go @@ -2,6 +2,7 @@ package truco import ( "errors" + "fmt" "strings" ) @@ -108,16 +109,21 @@ func (es *FlorSequence) IsFinished() bool { return last == SAY_FLOR_SON_BUENAS || last == SAY_FLOR_SON_MEJORES || last == SAY_CON_FLOR_ME_ACHICO || (last == SAY_FLOR && es.IsSinglePlayerFlor) } -func (es FlorSequence) Cost(maxPoints, winnerPlayerScore, loserPlayerScore int) (int, error) { +func (es FlorSequence) Cost(maxPoints, winnerPlayerScore, loserPlayerScore int, forHint bool) (int, error) { if !es.isValid() { - return COST_NOT_READY, errInvalidFlorSequence + return COST_NOT_READY, fmt.Errorf("%w: [%v]", errInvalidFlorSequence, strings.Join(es.Sequence, ",")) } cost := validFlorSequenceCosts[es.String()] if cost == COST_CONTRAFLOR_AL_RESTO { return calculateFaltaEnvidoCost(maxPoints, winnerPlayerScore, loserPlayerScore), nil } + // If this calculation is for enriching an action, we don't care if it's finished. + // If it's for assigning cost, then it must be finished. + if forHint { + return cost, nil + } if !es.IsFinished() { - return cost, errUnfinishedFlorSequence + return cost, fmt.Errorf("%w: %v", errUnfinishedFlorSequence, strings.Join(es.Sequence, ",")) } return cost, nil } @@ -141,7 +147,7 @@ func (es FlorSequence) Clone() *FlorSequence { func (es FlorSequence) WithStep(step string) (FlorSequence, error) { if !es.CanAddStep(step) { - return es, errInvalidFlorSequence + return es, fmt.Errorf("%w: [%v]", errInvalidFlorSequence, _s(es.Sequence...)) } newEs := es.Clone() newEs.AddStep(step)