From ac3332f5346540dcafb9eda1ca337b4a6550d176 Mon Sep 17 00:00:00 2001 From: Mariano Gappa Date: Sun, 28 Jul 2024 20:28:01 +0100 Subject: [PATCH] Irresponsibly half implement flor on a Sunday. --- examplebot/bot.go | 150 +++++++++-- truco/action_any_quiero.go | 116 --------- truco/action_confirm_round_ended.go | 4 + truco/action_reveal_card.go | 5 + truco/action_son_buenas.go | 4 + truco/action_son_mejores.go | 4 + truco/actions.go | 40 +++ truco/actions_any_envido.go | 125 ++++++++++ truco/actions_any_flor.go | 369 ++++++++++++++++++++++++++++ truco/deck.go | 26 ++ truco/envido_sequence.go | 135 +++++----- truco/flor_sequence.go | 154 ++++++++++++ truco/truco.go | 93 +++++-- truco/truco_test.go | 15 +- 14 files changed, 1016 insertions(+), 224 deletions(-) create mode 100644 truco/actions_any_flor.go create mode 100644 truco/flor_sequence.go diff --git a/examplebot/bot.go b/examplebot/bot.go index 9cd8a01..87f2121 100644 --- a/examplebot/bot.go +++ b/examplebot/bot.go @@ -61,6 +61,10 @@ func calculateEnvidoScore(gs truco.ClientGameState) int { return truco.Hand{Revealed: gs.YourRevealedCards, Unrevealed: gs.YourUnrevealedCards}.EnvidoScore() } +func calculateFlorScore(gs truco.ClientGameState) int { + return truco.Hand{Revealed: gs.YourRevealedCards, Unrevealed: gs.YourUnrevealedCards}.FlorScore() +} + func calculateCardStrength(gs truco.Card) int { specialValues := map[truco.Card]int{ {Suit: truco.ESPADA, Number: 1}: 15, @@ -95,24 +99,14 @@ func canAnyEnvido(actions map[string]truco.Action) bool { )) > 0 } -func possibleEnvidoActionsMap(gs truco.ClientGameState) map[string]truco.Action { - possible := possibleActionsMap(gs) - - filter := map[string]struct{}{ - truco.SAY_ENVIDO: {}, - truco.SAY_REAL_ENVIDO: {}, - truco.SAY_FALTA_ENVIDO: {}, - truco.SAY_ENVIDO_QUIERO: {}, - } - - possibleEnvidoActions := make(map[string]truco.Action) - for name, action := range possible { - if _, ok := filter[name]; ok { - possibleEnvidoActions[name] = action - } - } - - return possibleEnvidoActions +func canAnyFlor(actions map[string]truco.Action) bool { + return len(filter(actions, + truco.NewActionSayFlor(1), + truco.NewActionSayContraflor(1), + truco.NewActionSayContraflorAlResto(1), + truco.NewActionSayConFlorQuiero(1), + truco.NewActionSayConFlorMeAchico(1), + )) > 0 } func possibleTrucoActionsMap(gs truco.ClientGameState) map[string]truco.Action { @@ -136,7 +130,7 @@ func possibleTrucoActionsMap(gs truco.ClientGameState) map[string]truco.Action { } func sortPossibleEnvidoActions(gs truco.ClientGameState) []truco.Action { - possible := possibleEnvidoActionsMap(gs) + possible := possibleActionsMap(gs) filter := []string{ truco.SAY_ENVIDO_QUIERO, truco.SAY_ENVIDO, @@ -160,6 +154,46 @@ func sortPossibleEnvidoActions(gs truco.ClientGameState) []truco.Action { return actions } +func sortPossibleFlorActions(gs truco.ClientGameState) []truco.Action { + possible := possibleActionsMap(gs) + filter := []string{ + truco.SAY_FLOR, + truco.SAY_CON_FLOR_QUIERO, + truco.SAY_CONTRAFLOR, + truco.SAY_CONTRAFLOR_AL_RESTO, + } + + actions := []truco.Action{} + for _, name := range filter { + if action, ok := possible[name]; ok { + actions = append(actions, action) + } + } + + // Sort actions based on their cost + // TODO: this is broken at the moment because the cost doesn't work well + sort.Slice(actions, func(i, j int) bool { + return _getFlorActionQuieroCost(actions[i]) < _getFlorActionQuieroCost(actions[j]) + }) + + return actions +} + +func _getFlorActionQuieroCost(action truco.Action) int { + switch a := action.(type) { + case *truco.ActionSayFlor: + return a.QuieroCost + case *truco.ActionSayConFlorQuiero: + return a.Cost + case *truco.ActionSayContraflor: + return a.QuieroCost + case *truco.ActionSayContraflorAlResto: + return a.QuieroCost + default: + panic("this code should be unreachable! bug in _getFlorActionCost! please report this bug.") + } +} + func _getEnvidoActionQuieroCost(action truco.Action) int { switch a := action.(type) { case *truco.ActionSayEnvidoQuiero: @@ -198,6 +232,53 @@ func shouldAnyEnvido(gs truco.ClientGameState, aggresiveness string, log func(st return score >= shouldMap[aggresiveness] } +func shouldAnyFlor(gs truco.ClientGameState, aggresiveness string, log func(string, ...any)) bool { + // 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 + + // In principle let's always choose an action, since flor is unlikely to be matched once one has it + return true +} + +func chooseFlorAction(gs truco.ClientGameState, aggresiveness string) truco.Action { + possibleActions := sortPossibleFlorActions(gs) + score := calculateFlorScore(gs) + + minScore := map[string]int{ + "low": 31, + "normal": 29, + "high": 26, + }[aggresiveness] + maxScore := 38 + + span := maxScore - minScore + numActions := len(possibleActions) + + // Calculate bucket width + bucketWidth := float64(span) / float64(numActions) + + // Determine the bucket for the score + bucket := int(float64(score-minScore) / bucketWidth) + + // Handle edge cases + if bucket < 0 { + bucket = 0 + } else if bucket >= numActions { + bucket = numActions - 1 + } + + return possibleActions[bucket] +} + func chooseEnvidoAction(gs truco.ClientGameState, aggresiveness string) truco.Action { possibleActions := sortPossibleEnvidoActions(gs) score := calculateEnvidoScore(gs) @@ -612,6 +693,12 @@ func envidoNoQuiero(gs truco.ClientGameState) truco.Action { func envidoQuiero(gs truco.ClientGameState) truco.Action { return truco.NewActionSayEnvidoQuiero(gs.YouPlayerID) } +func florQuiero(gs truco.ClientGameState) truco.Action { + return truco.NewActionSayConFlorQuiero(gs.YouPlayerID) +} +func florNoQuiero(gs truco.ClientGameState) truco.Action { + return truco.NewActionSayConFlorMeAchico(gs.YouPlayerID) +} func trucoQuiero(gs truco.ClientGameState) truco.Action { return truco.NewActionSayTrucoQuiero(gs.YouPlayerID) } @@ -654,9 +741,32 @@ func (m Bot) ChooseAction(gs truco.ClientGameState) truco.Action { var ( aggresiveness = calculateAggresiveness(gs) shouldEnvido = shouldAnyEnvido(gs, aggresiveness, m.log) + shouldFlor = shouldAnyFlor(gs, aggresiveness, m.log) shouldTruco = shouldAcceptTruco(gs, aggresiveness, m.log) ) + // Handle flor responses or actions + if canAnyFlor(actions) { + m.log("Flor actions are on the table.") + + if shouldFlor && len(filter(actions, florQuiero(gs))) > 0 { + m.log("I chose an flor action due to considering I should based on my aggresiveness, which is %v and my flor score is %v", aggresiveness, calculateFlorScore(gs)) + return chooseFlorAction(gs, aggresiveness) + } + if !shouldFlor && len(filter(actions, florNoQuiero(gs))) > 0 { + m.log("I said no quiero to flor due to considering I shouldn't based on my aggresiveness, which is %v and my flor score is %v", aggresiveness, calculateFlorScore(gs)) + return truco.NewActionSayConFlorMeAchico(gs.YouPlayerID) + } + if shouldFlor { + // This is the case where the bot initiates the envido + // 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 { + return chooseFlorAction(gs, aggresiveness) + } + } + } + // Handle envido responses or actions if canAnyEnvido(actions) { m.log("Envido actions are on the table.") @@ -667,7 +777,7 @@ func (m Bot) ChooseAction(gs truco.ClientGameState) truco.Action { } if !shouldEnvido && len(filter(actions, envidoNoQuiero(gs))) > 0 { m.log("I said no quiero to envido due to considering I shouldn't based on my aggresiveness, which is %v and my envido score is %v", aggresiveness, calculateEnvidoScore(gs)) - return truco.NewActionSayEnvidoNoQuiero(1) + return truco.NewActionSayEnvidoNoQuiero(gs.YouPlayerID) } if shouldEnvido { // This is the case where the bot initiates the envido diff --git a/truco/action_any_quiero.go b/truco/action_any_quiero.go index c551ae8..d6716fb 100644 --- a/truco/action_any_quiero.go +++ b/truco/action_any_quiero.go @@ -1,7 +1,6 @@ package truco import ( - "fmt" "slices" ) @@ -13,14 +12,6 @@ type ActionSayEnvidoQuiero struct { act Cost int `json:"cost"` } -type ActionSayEnvidoScore struct { - act - Score int `json:"score"` -} -type ActionRevealEnvidoScore struct { - act - Score int `json:"score"` -} type ActionSayTrucoQuiero struct { act Cost int `json:"cost"` @@ -56,35 +47,6 @@ func (a ActionSayEnvidoQuiero) IsPossible(g GameState) bool { return g.EnvidoSequence.CanAddStep(a.GetName()) } -func (a ActionSayEnvidoScore) IsPossible(g GameState) bool { - if len(g.RoundsLog[g.RoundNumber].ActionsLog) == 0 { - return false - } - lastAction := _deserializeCurrentRoundLastAction(g) - if lastAction.GetName() != SAY_ENVIDO_QUIERO { - return false - } - return g.EnvidoSequence.CanAddStep(a.GetName()) -} - -func (a ActionRevealEnvidoScore) IsPossible(g GameState) bool { - if !g.EnvidoSequence.WasAccepted() { - return false - } - if g.EnvidoSequence.EnvidoPointsAwarded { - return false - } - roundLog := g.RoundsLog[g.RoundNumber] - if roundLog.EnvidoWinnerPlayerID != a.PlayerID { - return false - } - if !g.IsRoundFinished && g.Players[a.PlayerID].Score+roundLog.EnvidoPoints < g.RuleMaxPoints { - 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 @@ -149,70 +111,6 @@ func (a ActionSayEnvidoQuiero) Run(g *GameState) error { return nil } -func (a ActionSayEnvidoScore) Run(g *GameState) error { - if !a.IsPossible(*g) { - return errActionNotPossible - } - g.EnvidoSequence.AddStep(a.GetName()) - 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...)} - for i := range curPlayersHand.Unrevealed { - card := curPlayersHand.Unrevealed[i] - candidateHand.displayUnrevealedCards = append(candidateHand.displayUnrevealedCards, DisplayCard{Number: card.Number, Suit: card.Suit}) - } - - // and reveal the additional cards of this combination - for i := range is { - candidateHand.Revealed = append(candidateHand.Revealed, curPlayersHand.Unrevealed[i]) - candidateHand.displayUnrevealedCards[i].IsHole = true - } - // 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 - if !g.tryAwardEnvidoPoints(a.PlayerID) { - panic("couldn't award envido score after running reveal envido score action due to a bug, this code should be unreachable") - } - 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 @@ -253,12 +151,6 @@ func (a ActionSayEnvidoQuiero) YieldsTurn(g GameState) bool { 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 -} - func (a *ActionSayTrucoQuiero) Enrich(g GameState) { a.RequiresReminder = _doesTrucoActionRequireReminder(g) quieroSeq, _ := g.TrucoSequence.WithStep(SAY_TRUCO_QUIERO) @@ -308,11 +200,3 @@ func _doesTrucoActionRequireReminder(g GameState) bool { // got in the middle of the truco sequence. A reminder is needed. 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 164bb8f..89067d4 100644 --- a/truco/action_confirm_round_ended.go +++ b/truco/action_confirm_round_ended.go @@ -22,3 +22,7 @@ func (a ActionConfirmRoundFinished) YieldsTurn(g GameState) bool { // The turn should go to the player who is left to confirm the round finished return a.PlayerID == g.TurnPlayerID } + +func (a ActionConfirmRoundFinished) GetPriority() int { + return 1 +} diff --git a/truco/action_reveal_card.go b/truco/action_reveal_card.go index 4f6419b..51c840f 100644 --- a/truco/action_reveal_card.go +++ b/truco/action_reveal_card.go @@ -67,6 +67,11 @@ func (a *ActionRevealCard) Run(g *GameState) error { a.EnMesa = true 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) { + a.EnMesa = true + a.Score = g.Players[a.PlayerID].Hand.FlorScore() // it must be the action's player + } return nil } diff --git a/truco/action_son_buenas.go b/truco/action_son_buenas.go index f367bb4..8f089de 100644 --- a/truco/action_son_buenas.go +++ b/truco/action_son_buenas.go @@ -53,3 +53,7 @@ func (a ActionSaySonBuenas) YieldsTurn(g GameState) bool { // In son_buenas/son_mejores/no_quiero, the turn should go to whoever started the sequence return g.TurnPlayerID != g.EnvidoSequence.StartingPlayerID } + +func (a ActionSaySonBuenas) GetPriority() int { + return 1 +} diff --git a/truco/action_son_mejores.go b/truco/action_son_mejores.go index 27c37f9..1128a12 100644 --- a/truco/action_son_mejores.go +++ b/truco/action_son_mejores.go @@ -60,3 +60,7 @@ func (a ActionSaySonMejores) YieldsTurn(g GameState) bool { func (a *ActionSaySonMejores) Enrich(g GameState) { a.Score = g.Players[a.PlayerID].Hand.EnvidoScore() } + +func (a ActionSaySonMejores) GetPriority() int { + return 1 +} diff --git a/truco/actions.go b/truco/actions.go index 3d40db7..793c9e9 100644 --- a/truco/actions.go +++ b/truco/actions.go @@ -20,6 +20,10 @@ func (a act) GetPlayerID() int { return a.PlayerID } +func (a act) GetPriority() int { + return 0 +} + // By default, actions don't need to be enriched. func (a act) Enrich(g GameState) {} @@ -108,6 +112,42 @@ func NewActionRevealEnvidoScore(playerID int) Action { return &ActionRevealEnvidoScore{act: act{Name: REVEAL_ENVIDO_SCORE, PlayerID: playerID}} } +func NewActionSayFlor(playerID int) Action { + return &ActionSayFlor{act: act{Name: SAY_FLOR, PlayerID: playerID}} +} + +func NewActionSayConFlorMeAchico(playerID int) Action { + return &ActionSayConFlorMeAchico{act: act{Name: SAY_CON_FLOR_ME_ACHICO, PlayerID: playerID}} +} + +func NewActionSayContraflor(playerID int) Action { + return &ActionSayContraflor{act: act{Name: SAY_CONTRAFLOR, PlayerID: playerID}} +} + +func NewActionSayContraflorAlResto(playerID int) Action { + return &ActionSayContraflorAlResto{act: act{Name: SAY_CONTRAFLOR_AL_RESTO, PlayerID: playerID}} +} + +func NewActionSayConFlorQuiero(playerID int) Action { + return &ActionSayConFlorQuiero{act: act{Name: SAY_CON_FLOR_QUIERO, PlayerID: playerID}} +} + +func NewActionSayFlorScore(playerID int) Action { + return &ActionSayFlorScore{act: act{Name: SAY_FLOR_SCORE, PlayerID: playerID}} +} + +func NewActionSayFlorSonBuenas(playerID int) Action { + return &ActionSayFlorSonBuenas{act: act{Name: SAY_FLOR_SON_BUENAS, PlayerID: playerID}} +} + +func NewActionSayFlorSonMejores(playerID int) Action { + return &ActionSayFlorSonMejores{act: act{Name: SAY_FLOR_SON_MEJORES, PlayerID: playerID}} +} + +func NewActionRevealFlorScore(playerID int) Action { + return &ActionRevealFlorScore{act: act{Name: REVEAL_FLOR_SCORE, PlayerID: playerID}} +} + func (a ActionSaySonMejores) String() string { return fmt.Sprintf("Player %v says %v son mejores", a.PlayerID, a.Score) } diff --git a/truco/actions_any_envido.go b/truco/actions_any_envido.go index 025ed50..67eaacd 100644 --- a/truco/actions_any_envido.go +++ b/truco/actions_any_envido.go @@ -1,5 +1,7 @@ package truco +import "fmt" + type ActionSayEnvido struct { act NoQuieroCost int `json:"noQuieroCost"` @@ -15,6 +17,14 @@ type ActionSayRealEnvido struct { NoQuieroCost int `json:"noQuieroCost"` QuieroCost int `json:"quieroCost"` } +type ActionSayEnvidoScore struct { + act + Score int `json:"score"` +} +type ActionRevealEnvidoScore struct { + act + Score int `json:"score"` +} func (a ActionSayEnvido) IsPossible(g GameState) bool { return g.AnyEnvidoActionTypeIsPossible(&a) } func (a ActionSayFaltaEnvido) IsPossible(g GameState) bool { @@ -22,14 +32,129 @@ func (a ActionSayFaltaEnvido) IsPossible(g GameState) bool { } func (a ActionSayRealEnvido) IsPossible(g GameState) bool { return g.AnyEnvidoActionTypeIsPossible(&a) } +func (a ActionSayEnvidoScore) IsPossible(g GameState) bool { + if len(g.RoundsLog[g.RoundNumber].ActionsLog) == 0 { + return false + } + lastAction := _deserializeCurrentRoundLastAction(g) + if lastAction.GetName() != SAY_ENVIDO_QUIERO { + return false + } + return g.EnvidoSequence.CanAddStep(a.GetName()) +} + +func (a ActionRevealEnvidoScore) IsPossible(g GameState) bool { + if !g.EnvidoSequence.WasAccepted() { + return false + } + if g.EnvidoSequence.EnvidoPointsAwarded { + return false + } + roundLog := g.RoundsLog[g.RoundNumber] + if roundLog.EnvidoWinnerPlayerID != a.PlayerID { + return false + } + if !g.IsRoundFinished && g.Players[a.PlayerID].Score+roundLog.EnvidoPoints < g.RuleMaxPoints { + return false + } + revealedHand := Hand{Revealed: g.Players[a.PlayerID].Hand.Revealed} + return revealedHand.EnvidoScore() != g.Players[a.PlayerID].Hand.EnvidoScore() +} + func (a ActionSayEnvido) Run(g *GameState) error { return g.AnyEnvidoActionTypeRunAction(&a) } func (a ActionSayFaltaEnvido) Run(g *GameState) error { return g.AnyEnvidoActionTypeRunAction(&a) } func (a ActionSayRealEnvido) Run(g *GameState) error { return g.AnyEnvidoActionTypeRunAction(&a) } +func (a ActionSayEnvidoScore) Run(g *GameState) error { + if !a.IsPossible(*g) { + return errActionNotPossible + } + g.EnvidoSequence.AddStep(a.GetName()) + 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...)} + for i := range curPlayersHand.Unrevealed { + card := curPlayersHand.Unrevealed[i] + candidateHand.displayUnrevealedCards = append(candidateHand.displayUnrevealedCards, DisplayCard{Number: card.Number, Suit: card.Suit}) + } + + // and reveal the additional cards of this combination + for i := range is { + candidateHand.Revealed = append(candidateHand.Revealed, curPlayersHand.Unrevealed[i]) + candidateHand.displayUnrevealedCards[i].IsHole = true + } + // 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 + if !g.tryAwardEnvidoPoints(a.PlayerID) { + panic("couldn't award envido score after running reveal envido score action due to a bug, this code should be unreachable") + } + 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 *ActionSayEnvido) Enrich(g GameState) { g.AnyEnvidoActionTypeEnrich(a) } func (a *ActionSayFaltaEnvido) Enrich(g GameState) { g.AnyEnvidoActionTypeEnrich(a) } func (a *ActionSayRealEnvido) Enrich(g GameState) { g.AnyEnvidoActionTypeEnrich(a) } +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() +} + +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 +} + +func (a *ActionSayEnvidoScore) GetPriority() int { + return 1 +} + +func (a ActionRevealEnvidoScore) GetPriority() int { + return 2 // Because it's higher than confirming round finished +} + func (g GameState) AnyEnvidoActionTypeIsPossible(a Action) bool { if g.IsRoundFinished { return false diff --git a/truco/actions_any_flor.go b/truco/actions_any_flor.go new file mode 100644 index 0000000..9a7e822 --- /dev/null +++ b/truco/actions_any_flor.go @@ -0,0 +1,369 @@ +package truco + +const ( + SAY_FLOR = "say_flor" + SAY_CON_FLOR_ME_ACHICO = "say_con_flor_me_achico" + SAY_CONTRAFLOR = "say_contraflor" + SAY_CONTRAFLOR_AL_RESTO = "say_contraflor_al_resto" + SAY_CON_FLOR_QUIERO = "say_con_flor_quiero" + SAY_FLOR_SCORE = "say_flor_score" + SAY_FLOR_SON_BUENAS = "say_flor_son_buenas" + SAY_FLOR_SON_MEJORES = "say_flor_son_mejores" + REVEAL_FLOR_SCORE = "reveal_flor_score" +) + +type ActionSayFlor struct { + act + QuieroCost int +} +type ActionSayConFlorMeAchico struct { + act + Cost int +} +type ActionSayContraflor struct { + act + QuieroCost int +} +type ActionSayContraflorAlResto struct { + act + QuieroCost int +} +type ActionSayConFlorQuiero struct { + act + Cost int +} +type ActionSayFlorScore struct { + act + Score int `json:"score"` +} +type ActionSayFlorSonBuenas struct { + act +} +type ActionSayFlorSonMejores struct { + act + Score int `json:"score"` +} +type ActionRevealFlorScore struct { + act + Score int `json:"score"` +} + +func (a ActionSayFlor) IsPossible(g GameState) bool { + return g.anyFlorActionIsPossible(&a) && len(_deserializeCurrentRoundActionsByPlayerID(a.PlayerID, g)) == 0 +} + +func (a ActionSayContraflor) IsPossible(g GameState) bool { + return g.anyFlorActionIsPossible(&a) +} + +func (a ActionSayContraflorAlResto) IsPossible(g GameState) bool { + return g.anyFlorActionIsPossible(&a) +} + +func (a ActionSayConFlorMeAchico) IsPossible(g GameState) bool { + return g.anyFlorActionIsPossible(&a) +} + +func (a ActionSayConFlorQuiero) IsPossible(g GameState) bool { + return g.anyFlorActionIsPossible(&a) +} + +func (a ActionSayFlorScore) IsPossible(g GameState) bool { + return g.anyFlorActionIsPossible(&a) +} + +func (a ActionSayFlorSonBuenas) IsPossible(g GameState) bool { + var ( + myScore = g.Players[a.PlayerID].Hand.FlorScore() + theirScore = g.Players[g.OpponentOf(a.PlayerID)].Hand.FlorScore() + iAmMano = g.RoundTurnPlayerID == a.PlayerID + ) + return g.anyFlorActionIsPossible(a) && (myScore < theirScore || (myScore == theirScore && !iAmMano)) +} + +func (a ActionSayFlorSonMejores) IsPossible(g GameState) bool { + var ( + myScore = g.Players[a.PlayerID].Hand.FlorScore() + theirScore = g.Players[g.OpponentOf(a.PlayerID)].Hand.FlorScore() + iAmMano = g.RoundTurnPlayerID == a.PlayerID + ) + return g.anyFlorActionIsPossible(&a) && (myScore > theirScore || (myScore == theirScore && iAmMano)) +} + +func (a ActionRevealFlorScore) IsPossible(g GameState) bool { + if !g.anyFlorActionIsPossible(&a) { + return false + } + if g.RoundsLog[g.RoundNumber].FlorWinnerPlayerID != a.PlayerID { + return false + } + if !g.IsRoundFinished { + return false + } + hand := g.Players[a.PlayerID].Hand + revealedHand := Hand{Revealed: g.Players[a.PlayerID].Hand.Revealed} + return revealedHand.FlorScore() == hand.FlorScore() +} + +func (g GameState) anyFlorActionIsPossible(a Action) bool { + if !g.RuleIsFlorEnabled { + return false + } + if !g.Players[a.GetPlayerID()].Hand.HasFlor() { + return false + } + if a.GetName() != REVEAL_FLOR_SCORE && g.IsRoundFinished { + return false + } + // For any flor action except "say_flor", both players must have flor + if a.GetName() != SAY_FLOR && !g.Players[g.OpponentOf(a.GetPlayerID())].Hand.HasFlor() { + return false + } + return g.FlorSequence.CanAddStep(a.GetName()) +} + +func (a ActionSayFlor) Run(g *GameState) error { + if err := g.anyFlorActionRun(&a); err != nil { + return err + } + if !g.Players[g.OpponentOf(a.PlayerID)].Hand.HasFlor() { + g.FlorSequence.IsSinglePlayerFlor = true + err := finalizeFlorSequence(a.PlayerID, g) + if err != nil { + return err + } + } + return nil +} + +func (a ActionSayContraflor) Run(g *GameState) error { + if err := g.anyFlorActionRun(&a); err != nil { + return err + } + return nil +} + +func (a ActionSayContraflorAlResto) Run(g *GameState) error { + if err := g.anyFlorActionRun(&a); err != nil { + return err + } + return nil +} + +func (a ActionSayConFlorMeAchico) Run(g *GameState) error { + if err := g.anyFlorActionRun(&a); err != nil { + return err + } + return finalizeFlorSequence(g.OpponentOf(a.PlayerID), g) +} + +func (a ActionSayConFlorQuiero) Run(g *GameState) error { + if err := g.anyFlorActionRun(&a); err != nil { + return err + } + return nil +} + +func (a ActionSayFlorScore) Run(g *GameState) error { + if err := g.anyFlorActionRun(&a); err != nil { + return err + } + return nil +} + +func (a ActionSayFlorSonBuenas) Run(g *GameState) error { + if err := g.anyFlorActionRun(a); err != nil { + return err + } + return finalizeFlorSequence(g.OpponentOf(a.PlayerID), g) +} + +func (a ActionSayFlorSonMejores) Run(g *GameState) error { + if err := g.anyFlorActionRun(&a); err != nil { + return err + } + return finalizeFlorSequence(a.PlayerID, g) +} + +func (a ActionRevealFlorScore) Run(g *GameState) error { + if !a.IsPossible(*g) { + return errActionNotPossible + } + g.IsEnvidoFinished = true + if !g.tryAwardFlorPoints(a.PlayerID) { + return errActionNotPossible + } + for _, c := range g.Players[a.PlayerID].Hand.Unrevealed { + _ = g.Players[a.PlayerID].Hand.RevealCard(c) + } + return nil +} + +func (g *GameState) anyFlorActionRun(a Action) error { + if !a.IsPossible(*g) { + return errActionNotPossible + } + g.IsEnvidoFinished = true + g.FlorSequence.AddStep(a.GetName()) + return nil +} + +func finalizeFlorSequence(winnerPlayerID int, g *GameState) error { + cost, err := g.FlorSequence.Cost(g.RuleMaxPoints, g.Players[winnerPlayerID].Score, g.Players[g.OpponentOf(winnerPlayerID)].Score) + if err != nil { + return err + } + g.RoundsLog[g.RoundNumber].FlorWinnerPlayerID = winnerPlayerID + g.RoundsLog[g.RoundNumber].FlorPoints = cost + g.tryAwardFlorPoints(winnerPlayerID) + return nil +} + +func (g *GameState) canAwardFlorPoints(revealedHand Hand) bool { + if !g.RuleIsFlorEnabled { + return false + } + wonBy := g.RoundsLog[g.RoundNumber].FlorWinnerPlayerID + if wonBy == -1 { + return false + } + if !g.FlorSequence.WasAccepted() { + return false + } + if g.FlorSequence.FlorPointsAwarded { + return false + } + if revealedHand.FlorScore() != g.Players[wonBy].Hand.FlorScore() { + return false + } + return true +} + +func (g *GameState) tryAwardFlorPoints(playerID int) bool { + if !g.canAwardFlorPoints(Hand{Revealed: g.Players[playerID].Hand.Revealed}) { + return false + } + wonBy := g.RoundsLog[g.RoundNumber].FlorWinnerPlayerID + score := g.RoundsLog[g.RoundNumber].FlorPoints + g.Players[wonBy].Score += score + g.FlorSequence.FlorPointsAwarded = true + return true +} + +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) + ) + 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) + ) + 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) + ) + 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) + ) + 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) + ) + a.Cost = quieroCost +} +func (a *ActionSayFlorScore) Enrich(g GameState) { + a.Score = g.Players[a.PlayerID].Hand.FlorScore() +} +func (a *ActionSayFlorSonMejores) Enrich(g GameState) { + a.Score = g.Players[a.PlayerID].Hand.FlorScore() +} +func (a *ActionRevealFlorScore) Enrich(g GameState) { + a.Score = g.Players[a.PlayerID].Hand.FlorScore() +} + +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 + return a.PlayerID != g.FlorSequence.StartingPlayerID +} + +func (a ActionSayConFlorMeAchico) 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 ActionRevealFlorScore) 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 +} + +func (a ActionSayConFlorQuiero) YieldsTurn(g GameState) bool { + // In flor_quiero, the next turn should go to whoever has to reveal the score. + // This should always be the "mano" player. + return a.PlayerID != g.RoundTurnPlayerID +} + +func (a ActionSayFlor) GetPriority() int { + return 1 +} + +func (a ActionSayConFlorMeAchico) GetPriority() int { + return 1 +} + +func (a ActionSayContraflor) GetPriority() int { + return 1 +} + +func (a ActionSayContraflorAlResto) GetPriority() int { + return 1 +} + +func (a ActionSayConFlorQuiero) GetPriority() int { + return 1 +} + +func (a ActionSayFlorScore) GetPriority() int { + return 1 +} + +func (a ActionSayFlorSonBuenas) GetPriority() int { + return 1 +} + +func (a ActionSayFlorSonMejores) GetPriority() int { + return 1 +} + +func (a ActionRevealFlorScore) GetPriority() int { + return 2 // Because it's higher than confirming round finished +} diff --git a/truco/deck.go b/truco/deck.go index 1a28ea8..f31676c 100644 --- a/truco/deck.go +++ b/truco/deck.go @@ -143,6 +143,17 @@ func (h *Hand) prepareDisplayUnrevealedCards(isYou bool) []DisplayCard { return result } +func (h Hand) HasFlor() bool { + suits := make(map[string]int) + for _, card := range append(h.Unrevealed, h.Revealed...) { + suits[card.Suit]++ + if suits[card.Suit] == 3 { + return true + } + } + return false +} + var ( errCardNotInHand = errors.New("card not in hand") errCardAlreadyRevealed = errors.New("card already revealed") @@ -233,6 +244,21 @@ func (h Hand) EnvidoScore() int { return maxScore } +func (h Hand) FlorScore() int { + if !h.HasFlor() { + return 0 + } + score := 20 + for _, card := range append(h.Unrevealed, h.Revealed...) { + cardScore := card.Number + if card.Number >= 10 { + cardScore = 0 + } + score += cardScore + } + return score +} + // CompareTrucoScore returns: // - 1 if the receiver card has a higher Truco score than the other card // - -1 if it has a lower score diff --git a/truco/envido_sequence.go b/truco/envido_sequence.go index b2f8ca8..1b416e4 100644 --- a/truco/envido_sequence.go +++ b/truco/envido_sequence.go @@ -2,7 +2,6 @@ package truco import ( "errors" - "fmt" "strings" ) @@ -26,66 +25,66 @@ const ( var ( validEnvidoSequenceCosts = map[string]int{ - SAY_ENVIDO: COST_NOT_READY, - SAY_REAL_ENVIDO: COST_NOT_READY, - SAY_FALTA_ENVIDO: COST_NOT_READY, - fmt.Sprintf("%s,%s", SAY_ENVIDO, SAY_ENVIDO): COST_NOT_READY, - fmt.Sprintf("%s,%s", SAY_ENVIDO, SAY_REAL_ENVIDO): COST_NOT_READY, - fmt.Sprintf("%s,%s", SAY_ENVIDO, SAY_FALTA_ENVIDO): COST_NOT_READY, - fmt.Sprintf("%s,%s", SAY_REAL_ENVIDO, SAY_FALTA_ENVIDO): COST_NOT_READY, - fmt.Sprintf("%s,%s,%s", SAY_ENVIDO, SAY_ENVIDO, SAY_REAL_ENVIDO): COST_NOT_READY, - fmt.Sprintf("%s,%s,%s", SAY_ENVIDO, SAY_REAL_ENVIDO, SAY_FALTA_ENVIDO): COST_NOT_READY, - fmt.Sprintf("%s,%s,%s,%s", SAY_ENVIDO, SAY_ENVIDO, SAY_REAL_ENVIDO, SAY_FALTA_ENVIDO): COST_NOT_READY, - fmt.Sprintf("%s,%s", SAY_ENVIDO, SAY_ENVIDO_QUIERO): 2, - fmt.Sprintf("%s,%s", SAY_REAL_ENVIDO, SAY_ENVIDO_QUIERO): 3, - fmt.Sprintf("%s,%s", SAY_FALTA_ENVIDO, SAY_ENVIDO_QUIERO): COST_FALTA_ENVIDO, - fmt.Sprintf("%s,%s,%s", SAY_ENVIDO, SAY_ENVIDO, SAY_ENVIDO_QUIERO): 4, - fmt.Sprintf("%s,%s,%s", SAY_ENVIDO, SAY_REAL_ENVIDO, SAY_ENVIDO_QUIERO): 5, - fmt.Sprintf("%s,%s,%s", SAY_ENVIDO, SAY_FALTA_ENVIDO, SAY_ENVIDO_QUIERO): COST_FALTA_ENVIDO, - fmt.Sprintf("%s,%s,%s", SAY_REAL_ENVIDO, SAY_FALTA_ENVIDO, SAY_ENVIDO_QUIERO): COST_FALTA_ENVIDO, - fmt.Sprintf("%s,%s,%s,%s", SAY_ENVIDO, SAY_ENVIDO, SAY_REAL_ENVIDO, SAY_ENVIDO_QUIERO): 7, - fmt.Sprintf("%s,%s,%s,%s", SAY_ENVIDO, SAY_REAL_ENVIDO, SAY_FALTA_ENVIDO, SAY_ENVIDO_QUIERO): COST_FALTA_ENVIDO, - fmt.Sprintf("%s,%s,%s,%s,%s", SAY_ENVIDO, SAY_ENVIDO, SAY_REAL_ENVIDO, SAY_FALTA_ENVIDO, SAY_ENVIDO_QUIERO): COST_FALTA_ENVIDO, - fmt.Sprintf("%s,%s,%s", SAY_ENVIDO, SAY_ENVIDO_QUIERO, SAY_ENVIDO_SCORE): 2, - fmt.Sprintf("%s,%s,%s", SAY_REAL_ENVIDO, SAY_ENVIDO_QUIERO, SAY_ENVIDO_SCORE): 3, - fmt.Sprintf("%s,%s,%s", SAY_FALTA_ENVIDO, SAY_ENVIDO_QUIERO, SAY_ENVIDO_SCORE): COST_FALTA_ENVIDO, - fmt.Sprintf("%s,%s,%s,%s", SAY_ENVIDO, SAY_ENVIDO, SAY_ENVIDO_QUIERO, SAY_ENVIDO_SCORE): 4, - fmt.Sprintf("%s,%s,%s,%s", SAY_ENVIDO, SAY_REAL_ENVIDO, SAY_ENVIDO_QUIERO, SAY_ENVIDO_SCORE): 5, - fmt.Sprintf("%s,%s,%s,%s", SAY_ENVIDO, SAY_FALTA_ENVIDO, SAY_ENVIDO_QUIERO, SAY_ENVIDO_SCORE): COST_FALTA_ENVIDO, - fmt.Sprintf("%s,%s,%s,%s", SAY_REAL_ENVIDO, SAY_FALTA_ENVIDO, SAY_ENVIDO_QUIERO, SAY_ENVIDO_SCORE): COST_FALTA_ENVIDO, - fmt.Sprintf("%s,%s,%s,%s,%s", SAY_ENVIDO, SAY_ENVIDO, SAY_REAL_ENVIDO, SAY_ENVIDO_QUIERO, SAY_ENVIDO_SCORE): 7, - fmt.Sprintf("%s,%s,%s,%s,%s", SAY_ENVIDO, SAY_REAL_ENVIDO, SAY_FALTA_ENVIDO, SAY_ENVIDO_QUIERO, SAY_ENVIDO_SCORE): COST_FALTA_ENVIDO, - fmt.Sprintf("%s,%s,%s,%s,%s,%s", SAY_ENVIDO, SAY_ENVIDO, SAY_REAL_ENVIDO, SAY_FALTA_ENVIDO, SAY_ENVIDO_QUIERO, SAY_ENVIDO_SCORE): COST_FALTA_ENVIDO, - fmt.Sprintf("%s,%s,%s,%s", SAY_ENVIDO, SAY_ENVIDO_QUIERO, SAY_ENVIDO_SCORE, SAY_SON_MEJORES): 2, - fmt.Sprintf("%s,%s,%s,%s", SAY_REAL_ENVIDO, SAY_ENVIDO_QUIERO, SAY_ENVIDO_SCORE, SAY_SON_MEJORES): 3, - fmt.Sprintf("%s,%s,%s,%s", SAY_FALTA_ENVIDO, SAY_ENVIDO_QUIERO, SAY_ENVIDO_SCORE, SAY_SON_MEJORES): COST_FALTA_ENVIDO, - fmt.Sprintf("%s,%s,%s,%s,%s", SAY_ENVIDO, SAY_ENVIDO, SAY_ENVIDO_QUIERO, SAY_ENVIDO_SCORE, SAY_SON_MEJORES): 4, - fmt.Sprintf("%s,%s,%s,%s,%s", SAY_ENVIDO, SAY_REAL_ENVIDO, SAY_ENVIDO_QUIERO, SAY_ENVIDO_SCORE, SAY_SON_MEJORES): 5, - fmt.Sprintf("%s,%s,%s,%s,%s", SAY_ENVIDO, SAY_FALTA_ENVIDO, SAY_ENVIDO_QUIERO, SAY_ENVIDO_SCORE, SAY_SON_MEJORES): COST_FALTA_ENVIDO, - fmt.Sprintf("%s,%s,%s,%s,%s", SAY_REAL_ENVIDO, SAY_FALTA_ENVIDO, SAY_ENVIDO_QUIERO, SAY_ENVIDO_SCORE, SAY_SON_MEJORES): COST_FALTA_ENVIDO, - fmt.Sprintf("%s,%s,%s,%s,%s,%s", SAY_ENVIDO, SAY_ENVIDO, SAY_REAL_ENVIDO, SAY_ENVIDO_QUIERO, SAY_ENVIDO_SCORE, SAY_SON_MEJORES): 7, - fmt.Sprintf("%s,%s,%s,%s,%s,%s", SAY_ENVIDO, SAY_REAL_ENVIDO, SAY_FALTA_ENVIDO, SAY_ENVIDO_QUIERO, SAY_ENVIDO_SCORE, SAY_SON_MEJORES): COST_FALTA_ENVIDO, - fmt.Sprintf("%s,%s,%s,%s,%s,%s,%s", SAY_ENVIDO, SAY_ENVIDO, SAY_REAL_ENVIDO, SAY_FALTA_ENVIDO, SAY_ENVIDO_QUIERO, SAY_ENVIDO_SCORE, SAY_SON_MEJORES): COST_FALTA_ENVIDO, - fmt.Sprintf("%s,%s,%s,%s", SAY_ENVIDO, SAY_ENVIDO_QUIERO, SAY_ENVIDO_SCORE, SAY_SON_BUENAS): 2, - fmt.Sprintf("%s,%s,%s,%s", SAY_REAL_ENVIDO, SAY_ENVIDO_QUIERO, SAY_ENVIDO_SCORE, SAY_SON_BUENAS): 3, - fmt.Sprintf("%s,%s,%s,%s", SAY_FALTA_ENVIDO, SAY_ENVIDO_QUIERO, SAY_ENVIDO_SCORE, SAY_SON_BUENAS): COST_FALTA_ENVIDO, - fmt.Sprintf("%s,%s,%s,%s,%s", SAY_ENVIDO, SAY_ENVIDO, SAY_ENVIDO_QUIERO, SAY_ENVIDO_SCORE, SAY_SON_BUENAS): 4, - fmt.Sprintf("%s,%s,%s,%s,%s", SAY_ENVIDO, SAY_REAL_ENVIDO, SAY_ENVIDO_QUIERO, SAY_ENVIDO_SCORE, SAY_SON_BUENAS): 5, - fmt.Sprintf("%s,%s,%s,%s,%s", SAY_ENVIDO, SAY_FALTA_ENVIDO, SAY_ENVIDO_QUIERO, SAY_ENVIDO_SCORE, SAY_SON_BUENAS): COST_FALTA_ENVIDO, - fmt.Sprintf("%s,%s,%s,%s,%s", SAY_REAL_ENVIDO, SAY_FALTA_ENVIDO, SAY_ENVIDO_QUIERO, SAY_ENVIDO_SCORE, SAY_SON_BUENAS): COST_FALTA_ENVIDO, - fmt.Sprintf("%s,%s,%s,%s,%s,%s", SAY_ENVIDO, SAY_ENVIDO, SAY_REAL_ENVIDO, SAY_ENVIDO_QUIERO, SAY_ENVIDO_SCORE, SAY_SON_BUENAS): 7, - fmt.Sprintf("%s,%s,%s,%s,%s,%s", SAY_ENVIDO, SAY_REAL_ENVIDO, SAY_FALTA_ENVIDO, SAY_ENVIDO_QUIERO, SAY_ENVIDO_SCORE, SAY_SON_BUENAS): COST_FALTA_ENVIDO, - fmt.Sprintf("%s,%s,%s,%s,%s,%s,%s", SAY_ENVIDO, SAY_ENVIDO, SAY_REAL_ENVIDO, SAY_FALTA_ENVIDO, SAY_ENVIDO_QUIERO, SAY_ENVIDO_SCORE, SAY_SON_BUENAS): COST_FALTA_ENVIDO, - fmt.Sprintf("%s,%s", SAY_ENVIDO, SAY_ENVIDO_NO_QUIERO): 1, - fmt.Sprintf("%s,%s", SAY_REAL_ENVIDO, SAY_ENVIDO_NO_QUIERO): 1, - fmt.Sprintf("%s,%s", SAY_FALTA_ENVIDO, SAY_ENVIDO_NO_QUIERO): 1, - fmt.Sprintf("%s,%s,%s", SAY_ENVIDO, SAY_ENVIDO, SAY_ENVIDO_NO_QUIERO): 2, - fmt.Sprintf("%s,%s,%s", SAY_ENVIDO, SAY_REAL_ENVIDO, SAY_ENVIDO_NO_QUIERO): 2, - fmt.Sprintf("%s,%s,%s", SAY_ENVIDO, SAY_FALTA_ENVIDO, SAY_ENVIDO_NO_QUIERO): 2, - fmt.Sprintf("%s,%s,%s", SAY_REAL_ENVIDO, SAY_FALTA_ENVIDO, SAY_ENVIDO_NO_QUIERO): 3, - fmt.Sprintf("%s,%s,%s,%s", SAY_ENVIDO, SAY_ENVIDO, SAY_REAL_ENVIDO, SAY_ENVIDO_NO_QUIERO): 4, - fmt.Sprintf("%s,%s,%s,%s", SAY_ENVIDO, SAY_REAL_ENVIDO, SAY_FALTA_ENVIDO, SAY_ENVIDO_NO_QUIERO): 5, - fmt.Sprintf("%s,%s,%s,%s,%s", SAY_ENVIDO, SAY_ENVIDO, SAY_REAL_ENVIDO, SAY_FALTA_ENVIDO, SAY_ENVIDO_NO_QUIERO): 7, + SAY_ENVIDO: COST_NOT_READY, + SAY_REAL_ENVIDO: COST_NOT_READY, + SAY_FALTA_ENVIDO: COST_NOT_READY, + _s(SAY_ENVIDO, SAY_ENVIDO): COST_NOT_READY, + _s(SAY_ENVIDO, SAY_REAL_ENVIDO): COST_NOT_READY, + _s(SAY_ENVIDO, SAY_FALTA_ENVIDO): COST_NOT_READY, + _s(SAY_REAL_ENVIDO, SAY_FALTA_ENVIDO): COST_NOT_READY, + _s(SAY_ENVIDO, SAY_ENVIDO, SAY_REAL_ENVIDO): COST_NOT_READY, + _s(SAY_ENVIDO, SAY_REAL_ENVIDO, SAY_FALTA_ENVIDO): COST_NOT_READY, + _s(SAY_ENVIDO, SAY_ENVIDO, SAY_REAL_ENVIDO, SAY_FALTA_ENVIDO): COST_NOT_READY, + _s(SAY_ENVIDO, SAY_ENVIDO_QUIERO): 2, + _s(SAY_REAL_ENVIDO, SAY_ENVIDO_QUIERO): 3, + _s(SAY_FALTA_ENVIDO, SAY_ENVIDO_QUIERO): COST_FALTA_ENVIDO, + _s(SAY_ENVIDO, SAY_ENVIDO, SAY_ENVIDO_QUIERO): 4, + _s(SAY_ENVIDO, SAY_REAL_ENVIDO, SAY_ENVIDO_QUIERO): 5, + _s(SAY_ENVIDO, SAY_FALTA_ENVIDO, SAY_ENVIDO_QUIERO): COST_FALTA_ENVIDO, + _s(SAY_REAL_ENVIDO, SAY_FALTA_ENVIDO, SAY_ENVIDO_QUIERO): COST_FALTA_ENVIDO, + _s(SAY_ENVIDO, SAY_ENVIDO, SAY_REAL_ENVIDO, SAY_ENVIDO_QUIERO): 7, + _s(SAY_ENVIDO, SAY_REAL_ENVIDO, SAY_FALTA_ENVIDO, SAY_ENVIDO_QUIERO): COST_FALTA_ENVIDO, + _s(SAY_ENVIDO, SAY_ENVIDO, SAY_REAL_ENVIDO, SAY_FALTA_ENVIDO, SAY_ENVIDO_QUIERO): COST_FALTA_ENVIDO, + _s(SAY_ENVIDO, SAY_ENVIDO_QUIERO, SAY_ENVIDO_SCORE): 2, + _s(SAY_REAL_ENVIDO, SAY_ENVIDO_QUIERO, SAY_ENVIDO_SCORE): 3, + _s(SAY_FALTA_ENVIDO, SAY_ENVIDO_QUIERO, SAY_ENVIDO_SCORE): COST_FALTA_ENVIDO, + _s(SAY_ENVIDO, SAY_ENVIDO, SAY_ENVIDO_QUIERO, SAY_ENVIDO_SCORE): 4, + _s(SAY_ENVIDO, SAY_REAL_ENVIDO, SAY_ENVIDO_QUIERO, SAY_ENVIDO_SCORE): 5, + _s(SAY_ENVIDO, SAY_FALTA_ENVIDO, SAY_ENVIDO_QUIERO, SAY_ENVIDO_SCORE): COST_FALTA_ENVIDO, + _s(SAY_REAL_ENVIDO, SAY_FALTA_ENVIDO, SAY_ENVIDO_QUIERO, SAY_ENVIDO_SCORE): COST_FALTA_ENVIDO, + _s(SAY_ENVIDO, SAY_ENVIDO, SAY_REAL_ENVIDO, SAY_ENVIDO_QUIERO, SAY_ENVIDO_SCORE): 7, + _s(SAY_ENVIDO, SAY_REAL_ENVIDO, SAY_FALTA_ENVIDO, SAY_ENVIDO_QUIERO, SAY_ENVIDO_SCORE): COST_FALTA_ENVIDO, + _s(SAY_ENVIDO, SAY_ENVIDO, SAY_REAL_ENVIDO, SAY_FALTA_ENVIDO, SAY_ENVIDO_QUIERO, SAY_ENVIDO_SCORE): COST_FALTA_ENVIDO, + _s(SAY_ENVIDO, SAY_ENVIDO_QUIERO, SAY_ENVIDO_SCORE, SAY_SON_MEJORES): 2, + _s(SAY_REAL_ENVIDO, SAY_ENVIDO_QUIERO, SAY_ENVIDO_SCORE, SAY_SON_MEJORES): 3, + _s(SAY_FALTA_ENVIDO, SAY_ENVIDO_QUIERO, SAY_ENVIDO_SCORE, SAY_SON_MEJORES): COST_FALTA_ENVIDO, + _s(SAY_ENVIDO, SAY_ENVIDO, SAY_ENVIDO_QUIERO, SAY_ENVIDO_SCORE, SAY_SON_MEJORES): 4, + _s(SAY_ENVIDO, SAY_REAL_ENVIDO, SAY_ENVIDO_QUIERO, SAY_ENVIDO_SCORE, SAY_SON_MEJORES): 5, + _s(SAY_ENVIDO, SAY_FALTA_ENVIDO, SAY_ENVIDO_QUIERO, SAY_ENVIDO_SCORE, SAY_SON_MEJORES): COST_FALTA_ENVIDO, + _s(SAY_REAL_ENVIDO, SAY_FALTA_ENVIDO, SAY_ENVIDO_QUIERO, SAY_ENVIDO_SCORE, SAY_SON_MEJORES): COST_FALTA_ENVIDO, + _s(SAY_ENVIDO, SAY_ENVIDO, SAY_REAL_ENVIDO, SAY_ENVIDO_QUIERO, SAY_ENVIDO_SCORE, SAY_SON_MEJORES): 7, + _s(SAY_ENVIDO, SAY_REAL_ENVIDO, SAY_FALTA_ENVIDO, SAY_ENVIDO_QUIERO, SAY_ENVIDO_SCORE, SAY_SON_MEJORES): COST_FALTA_ENVIDO, + _s(SAY_ENVIDO, SAY_ENVIDO, SAY_REAL_ENVIDO, SAY_FALTA_ENVIDO, SAY_ENVIDO_QUIERO, SAY_ENVIDO_SCORE, SAY_SON_MEJORES): COST_FALTA_ENVIDO, + _s(SAY_ENVIDO, SAY_ENVIDO_QUIERO, SAY_ENVIDO_SCORE, SAY_SON_BUENAS): 2, + _s(SAY_REAL_ENVIDO, SAY_ENVIDO_QUIERO, SAY_ENVIDO_SCORE, SAY_SON_BUENAS): 3, + _s(SAY_FALTA_ENVIDO, SAY_ENVIDO_QUIERO, SAY_ENVIDO_SCORE, SAY_SON_BUENAS): COST_FALTA_ENVIDO, + _s(SAY_ENVIDO, SAY_ENVIDO, SAY_ENVIDO_QUIERO, SAY_ENVIDO_SCORE, SAY_SON_BUENAS): 4, + _s(SAY_ENVIDO, SAY_REAL_ENVIDO, SAY_ENVIDO_QUIERO, SAY_ENVIDO_SCORE, SAY_SON_BUENAS): 5, + _s(SAY_ENVIDO, SAY_FALTA_ENVIDO, SAY_ENVIDO_QUIERO, SAY_ENVIDO_SCORE, SAY_SON_BUENAS): COST_FALTA_ENVIDO, + _s(SAY_REAL_ENVIDO, SAY_FALTA_ENVIDO, SAY_ENVIDO_QUIERO, SAY_ENVIDO_SCORE, SAY_SON_BUENAS): COST_FALTA_ENVIDO, + _s(SAY_ENVIDO, SAY_ENVIDO, SAY_REAL_ENVIDO, SAY_ENVIDO_QUIERO, SAY_ENVIDO_SCORE, SAY_SON_BUENAS): 7, + _s(SAY_ENVIDO, SAY_REAL_ENVIDO, SAY_FALTA_ENVIDO, SAY_ENVIDO_QUIERO, SAY_ENVIDO_SCORE, SAY_SON_BUENAS): COST_FALTA_ENVIDO, + _s(SAY_ENVIDO, SAY_ENVIDO, SAY_REAL_ENVIDO, SAY_FALTA_ENVIDO, SAY_ENVIDO_QUIERO, SAY_ENVIDO_SCORE, SAY_SON_BUENAS): COST_FALTA_ENVIDO, + _s(SAY_ENVIDO, SAY_ENVIDO_NO_QUIERO): 1, + _s(SAY_REAL_ENVIDO, SAY_ENVIDO_NO_QUIERO): 1, + _s(SAY_FALTA_ENVIDO, SAY_ENVIDO_NO_QUIERO): 1, + _s(SAY_ENVIDO, SAY_ENVIDO, SAY_ENVIDO_NO_QUIERO): 2, + _s(SAY_ENVIDO, SAY_REAL_ENVIDO, SAY_ENVIDO_NO_QUIERO): 2, + _s(SAY_ENVIDO, SAY_FALTA_ENVIDO, SAY_ENVIDO_NO_QUIERO): 2, + _s(SAY_REAL_ENVIDO, SAY_FALTA_ENVIDO, SAY_ENVIDO_NO_QUIERO): 3, + _s(SAY_ENVIDO, SAY_ENVIDO, SAY_REAL_ENVIDO, SAY_ENVIDO_NO_QUIERO): 4, + _s(SAY_ENVIDO, SAY_REAL_ENVIDO, SAY_FALTA_ENVIDO, SAY_ENVIDO_NO_QUIERO): 5, + _s(SAY_ENVIDO, SAY_ENVIDO, SAY_REAL_ENVIDO, SAY_FALTA_ENVIDO, SAY_ENVIDO_NO_QUIERO): 7, } ) @@ -140,13 +139,13 @@ func (es *EnvidoSequence) IsFinished() bool { return last == SAY_SON_BUENAS || last == SAY_SON_MEJORES || last == SAY_ENVIDO_NO_QUIERO } -func (es EnvidoSequence) Cost(maxPoints, currentPlayerScore, otherPlayerScore int) (int, error) { +func (es EnvidoSequence) Cost(maxPoints, winnerPlayerScore, loserPlayerScore int) (int, error) { if !es.isValid() { return COST_NOT_READY, errInvalidEnvidoSequence } cost := validEnvidoSequenceCosts[es.String()] if cost == COST_FALTA_ENVIDO { - return calculateFaltaEnvidoCost(maxPoints, currentPlayerScore, otherPlayerScore), nil + return calculateFaltaEnvidoCost(maxPoints, winnerPlayerScore, loserPlayerScore), nil } if !es.IsFinished() { return cost, errUnfinishedEnvidoSequence @@ -180,16 +179,16 @@ func (es EnvidoSequence) WithStep(step string) (EnvidoSequence, error) { return *newEs, nil } -func calculateFaltaEnvidoCost(maxPoints, meScore, youScore int) int { +func calculateFaltaEnvidoCost(maxPoints, winnerScore, loserScore int) int { // maxPoints is normally only 15 or 30, but if it's set to less then // use the same rule as for 15, but using maxPoints instead. if maxPoints < 15 { - return maxPoints - meScore + return maxPoints - winnerScore } - if meScore < 15 && youScore < 15 { - return 15 - meScore + if winnerScore < 15 && loserScore < 15 { + return 15 - winnerScore } - return 30 - max(meScore, youScore) + return 30 - max(winnerScore, loserScore) } var ( diff --git a/truco/flor_sequence.go b/truco/flor_sequence.go new file mode 100644 index 0000000..5532c46 --- /dev/null +++ b/truco/flor_sequence.go @@ -0,0 +1,154 @@ +package truco + +import ( + "errors" + "strings" +) + +const ( + COST_CONTRAFLOR_AL_RESTO = -2 +) + +var ( + // TODD: add son_buenas, son_mejores & say_flor_score + validFlorSequenceCosts = map[string]int{ + SAY_FLOR: 3, + _s(SAY_FLOR, SAY_CONTRAFLOR): COST_NOT_READY, + _s(SAY_FLOR, SAY_CONTRAFLOR_AL_RESTO): COST_NOT_READY, + _s(SAY_FLOR, SAY_CONTRAFLOR, SAY_CONTRAFLOR_AL_RESTO): COST_NOT_READY, + + // 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, + + // "quiero" to "flor" + _s(SAY_FLOR, SAY_CON_FLOR_QUIERO): 4, + _s(SAY_FLOR, SAY_CON_FLOR_QUIERO, SAY_FLOR_SCORE): 4, + _s(SAY_FLOR, SAY_CON_FLOR_QUIERO, SAY_FLOR_SCORE, SAY_FLOR_SON_BUENAS): 4, + _s(SAY_FLOR, SAY_CON_FLOR_QUIERO, SAY_FLOR_SCORE, SAY_FLOR_SON_MEJORES): 4, + + // "quiero" to "contraflor" + _s(SAY_FLOR, SAY_CONTRAFLOR, SAY_CON_FLOR_QUIERO): 6, + _s(SAY_FLOR, SAY_CONTRAFLOR, SAY_CON_FLOR_QUIERO, SAY_FLOR_SCORE): 6, + _s(SAY_FLOR, SAY_CONTRAFLOR, SAY_CON_FLOR_QUIERO, SAY_FLOR_SCORE, SAY_FLOR_SON_BUENAS): 6, + _s(SAY_FLOR, SAY_CONTRAFLOR, SAY_CON_FLOR_QUIERO, SAY_FLOR_SCORE, SAY_FLOR_SON_MEJORES): 6, + + // "quiero" to "contraflor" => "contraflor al resto" + _s(SAY_FLOR, SAY_CONTRAFLOR, SAY_CONTRAFLOR_AL_RESTO, SAY_CON_FLOR_QUIERO): COST_CONTRAFLOR_AL_RESTO, + _s(SAY_FLOR, SAY_CONTRAFLOR, SAY_CONTRAFLOR_AL_RESTO, SAY_CON_FLOR_QUIERO, SAY_FLOR_SCORE): COST_CONTRAFLOR_AL_RESTO, + _s(SAY_FLOR, SAY_CONTRAFLOR, SAY_CONTRAFLOR_AL_RESTO, SAY_CON_FLOR_QUIERO, SAY_FLOR_SCORE, SAY_FLOR_SON_BUENAS): COST_CONTRAFLOR_AL_RESTO, + _s(SAY_FLOR, SAY_CONTRAFLOR, SAY_CONTRAFLOR_AL_RESTO, SAY_CON_FLOR_QUIERO, SAY_FLOR_SCORE, SAY_FLOR_SON_MEJORES): COST_CONTRAFLOR_AL_RESTO, + + // "quiero" to "contraflor al resto" + _s(SAY_FLOR, SAY_CONTRAFLOR_AL_RESTO, SAY_CON_FLOR_QUIERO): COST_CONTRAFLOR_AL_RESTO, + _s(SAY_FLOR, SAY_CONTRAFLOR_AL_RESTO, SAY_CON_FLOR_QUIERO, SAY_FLOR_SCORE): COST_CONTRAFLOR_AL_RESTO, + _s(SAY_FLOR, SAY_CONTRAFLOR_AL_RESTO, SAY_CON_FLOR_QUIERO, SAY_FLOR_SCORE, SAY_FLOR_SON_BUENAS): COST_CONTRAFLOR_AL_RESTO, + _s(SAY_FLOR, SAY_CONTRAFLOR_AL_RESTO, SAY_CON_FLOR_QUIERO, SAY_FLOR_SCORE, SAY_FLOR_SON_MEJORES): COST_CONTRAFLOR_AL_RESTO, + } +) + +func _s(ss ...string) string { + return strings.Join(ss, ",") +} + +type FlorSequence struct { + Sequence []string `json:"sequence"` + + // IsSinglePlayerFlor is necessary because when only one player has a flor, + // there's no way to know if .IsFinished() is true when there's only a SAY_FLOR step. + IsSinglePlayerFlor bool `json:"isSinglePlayerFlor"` + + // This is necessary because when son_buenas/son_mejores/no_quiero is said, + // the turn goes to whoever started the sequence (i.e. affects YieldsTurn) + StartingPlayerID int `json:"startingPlayerID"` + + // FlorPointsAwarded is used to determine if the points have already been awarded. + // + // When a flor is won, points are not automatically awarded. They are when the + // winning score is revealed. This could be at the time it is won, but normally + // it is revealed later. + FlorPointsAwarded bool `json:"florPointsAwarded"` +} + +func (es FlorSequence) String() string { + return strings.Join(es.Sequence, ",") +} + +func (es FlorSequence) IsEmpty() bool { + return len(es.Sequence) == 0 +} + +func (es FlorSequence) isValid() bool { + _, ok := validFlorSequenceCosts[es.String()] + return ok +} + +func (es *FlorSequence) CanAddStep(step string) bool { + es.Sequence = append(es.Sequence, step) + isValid := es.isValid() + es.Sequence = es.Sequence[:len(es.Sequence)-1] + return isValid +} + +func (es *FlorSequence) AddStep(step string) bool { + if !es.CanAddStep(step) { + return false + } + es.Sequence = append(es.Sequence, step) + return true +} + +func (es *FlorSequence) IsFinished() bool { + if len(es.Sequence) == 0 { + return false + } + last := es.Sequence[len(es.Sequence)-1] + 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) { + if !es.isValid() { + return COST_NOT_READY, errInvalidFlorSequence + } + cost := validFlorSequenceCosts[es.String()] + if cost == COST_CONTRAFLOR_AL_RESTO { + return calculateFaltaEnvidoCost(maxPoints, winnerPlayerScore, loserPlayerScore), nil + } + if !es.IsFinished() { + return cost, errUnfinishedFlorSequence + } + return cost, nil +} + +func (es FlorSequence) WasAccepted() bool { + for _, step := range es.Sequence { + if step == SAY_CON_FLOR_QUIERO || (step == SAY_FLOR && es.IsSinglePlayerFlor) { + return true + } + } + return false +} + +func (es FlorSequence) Clone() *FlorSequence { + return &FlorSequence{ + Sequence: append([]string{}, es.Sequence...), + StartingPlayerID: es.StartingPlayerID, + FlorPointsAwarded: es.FlorPointsAwarded, + } +} + +func (es FlorSequence) WithStep(step string) (FlorSequence, error) { + if !es.CanAddStep(step) { + return es, errInvalidFlorSequence + } + newEs := es.Clone() + newEs.AddStep(step) + return *newEs, nil +} + +var ( + errInvalidFlorSequence = errors.New("invalid flor sequence") + errUnfinishedFlorSequence = errors.New("unfinished flor sequence") +) diff --git a/truco/truco.go b/truco/truco.go index 0dcca70..988888a 100644 --- a/truco/truco.go +++ b/truco/truco.go @@ -50,6 +50,8 @@ type GameState struct { // Example sequence is: [SAY_TRUCO, SAY_TRUCO_QUIERO, SAY_QUIERO_RETRUCO, SAY_TRUCO_NO_QUIERO] TrucoSequence *TrucoSequence `json:"trucoSequence"` + FlorSequence *FlorSequence `json:"florSequence"` + // CardRevealSequence is the sequence of card reveal actions that have been taken in the current round. // Each step is each card that was revealed (by both players). // `BistepWinners` (TODO: bad name) stores the result of the faceoff between each pair of cards. @@ -113,11 +115,13 @@ type RoundLog struct { // For envido/truco winners and points, note that there is still a // winner of 1 point if a player said "no quiero" to the envido/truco. // - // If envido wasn't played at all, then EnvidoWinnerPlayerID is -1. + // If envido/flor/truco wasn't played at all, then ...WinnerPlayerID is -1. // // At the end of a round, there will always be a TrucoWinnerPlayerID, // even if truco wasn't played, implicitly by revealing the cards. + FlorWinnerPlayerID int `json:"florWinnerPlayerID"` + FlorPoints int `json:"florPoints"` EnvidoWinnerPlayerID int `json:"envidoWinnerPlayerID"` EnvidoPoints int `json:"envidoPoints"` TrucoWinnerPlayerID int `json:"trucoWinnerPlayerID"` @@ -186,6 +190,7 @@ func (g *GameState) startNewRound() { g.Players[g.TurnOpponentPlayerID].Hand = g.deck.dealHand() g.EnvidoSequence = &EnvidoSequence{StartingPlayerID: -1} g.TrucoSequence = &TrucoSequence{StartingPlayerID: -1, QuieroOwnerPlayerID: -1} + g.FlorSequence = &FlorSequence{StartingPlayerID: -1} g.CardRevealSequence = &CardRevealSequence{} g.IsEnvidoFinished = false g.IsRoundFinished = false @@ -199,6 +204,8 @@ func (g *GameState) startNewRound() { EnvidoPoints: 0, TrucoWinnerPlayerID: -1, TrucoPoints: 0, + FlorWinnerPlayerID: -1, + FlorPoints: 0, ActionsLog: []ActionLog{}, }) g.PossibleActions = _serializeActions(g.CalculatePossibleActions()) @@ -328,6 +335,15 @@ type Action interface { // GameState.CalculatePossibleActions() must call this method on all actions. Enrich(g GameState) + // GetPriority is used by GameState to calculate which actions are possible. + // By default, all actions have priority 0. In principle, all actions that are + // possible will be collected. If an action with higher priority is found, + // all possible actions are removed, and only actions with this higher priority + // will be collected. And so on. + // + // For example, if Flor is possible, then it should be higher priority. + GetPriority() int + fmt.Stringer } @@ -359,25 +375,29 @@ func (g GameState) CalculatePossibleActions() []Action { NewActionConfirmRoundFinished(g.TurnPlayerID), NewActionConfirmRoundFinished(g.TurnOpponentPlayerID), NewActionRevealEnvidoScore(g.TurnPlayerID), + NewActionSayFlor(g.TurnPlayerID), + NewActionSayContraflor(g.TurnPlayerID), + NewActionSayContraflorAlResto(g.TurnPlayerID), + NewActionSayConFlorMeAchico(g.TurnPlayerID), + NewActionSayConFlorQuiero(g.TurnPlayerID), + NewActionSayFlorScore(g.TurnPlayerID), + NewActionSayFlorSonBuenas(g.TurnPlayerID), + NewActionSayFlorSonMejores(g.TurnPlayerID), + NewActionRevealFlorScore(g.TurnPlayerID), ) - // The reveal_envido_score action happens in two cases: - // 1. Round has ended and player hasn't shown envido score yet - // 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) - actionRevealEnvidoScore.Enrich(g) - if actionRevealEnvidoScore.IsPossible(g) { - allActions = []Action{actionRevealEnvidoScore} - } - possibleActions := []Action{} + priority := 0 for _, action := range allActions { action.Enrich(g) - if action.IsPossible(g) { - possibleActions = append(possibleActions, action) + if !action.IsPossible(g) { + continue + } + if action.GetPriority() > priority { + priority = action.GetPriority() + possibleActions = []Action{} } + possibleActions = append(possibleActions, action) } return possibleActions } @@ -433,6 +453,24 @@ func DeserializeAction(bs []byte) (Action, error) { action = &ActionConfirmRoundFinished{} case REVEAL_ENVIDO_SCORE: action = &ActionRevealEnvidoScore{} + case SAY_FLOR: + action = &ActionSayFlor{} + case SAY_CONTRAFLOR: + action = &ActionSayContraflor{} + case SAY_CONTRAFLOR_AL_RESTO: + action = &ActionSayContraflorAlResto{} + case SAY_CON_FLOR_ME_ACHICO: + action = &ActionSayConFlorMeAchico{} + case SAY_CON_FLOR_QUIERO: + action = &ActionSayConFlorQuiero{} + case SAY_FLOR_SCORE: + action = &ActionSayFlorScore{} + case SAY_FLOR_SON_BUENAS: + action = &ActionSayFlorSonBuenas{} + case SAY_FLOR_SON_MEJORES: + action = &ActionSayFlorSonMejores{} + case REVEAL_FLOR_SCORE: + action = &ActionRevealFlorScore{} default: return nil, fmt.Errorf("unknown action: [%v]", string(bs)) } @@ -459,6 +497,27 @@ func _deserializeCurrentRoundLastAction(g GameState) Action { return a } +func _deserializeCurrentRoundActions(g GameState) []Action { + curRoundActions := g.RoundsLog[g.RoundNumber].ActionsLog + actions := make([]Action, len(curRoundActions)) + for i, actionLog := range curRoundActions { + action, _ := DeserializeAction(actionLog.Action) + actions[i] = action + } + return actions +} + +func _deserializeCurrentRoundActionsByPlayerID(playerID int, g GameState) []Action { + actions := _deserializeCurrentRoundActions(g) + filteredActions := []Action{} + for _, a := range actions { + if a.GetPlayerID() == playerID { + filteredActions = append(filteredActions, a) + } + } + return filteredActions +} + func (g *GameState) ToClientGameState(youPlayerID int) ClientGameState { themPlayerID := g.OpponentOf(youPlayerID) @@ -491,6 +550,9 @@ func (g *GameState) ToClientGameState(youPlayerID int) ClientGameState { TrucoWinnerPlayerID: g.RoundsLog[g.RoundNumber].TrucoWinnerPlayerID, TrucoPoints: g.RoundsLog[g.RoundNumber].TrucoPoints, WasTrucoAccepted: g.TrucoSequence.WasAccepted(), + FlorWinnerPlayerID: g.RoundsLog[g.RoundNumber].FlorWinnerPlayerID, + WasFlorAccepted: g.FlorSequence.WasAccepted(), + FlorPoints: g.RoundsLog[g.RoundNumber].FlorPoints, YourDisplayUnrevealedCards: g.Players[youPlayerID].Hand.prepareDisplayUnrevealedCards(true), TheirDisplayUnrevealedCards: g.Players[themPlayerID].Hand.prepareDisplayUnrevealedCards(false), RuleMaxPoints: g.RuleMaxPoints, @@ -563,6 +625,9 @@ type ClientGameState struct { WinnerPlayerID int `json:"winnerPlayerID"` // Some state information about the current round, in case it's useful to the client. + FlorWinnerPlayerID int `json:"florWinnerPlayerID"` + WasFlorAccepted bool `json:"wasFlorAccepted"` + FlorPoints int `json:"florPoints"` EnvidoWinnerPlayerID int `json:"envidoWinnerPlayerID"` WasEnvidoAccepted bool `json:"wasEnvidoAccepted"` EnvidoPoints int `json:"envidoPoints"` diff --git a/truco/truco_test.go b/truco/truco_test.go index 5cd6df4..a91735d 100644 --- a/truco/truco_test.go +++ b/truco/truco_test.go @@ -19,7 +19,9 @@ func TestInitialOptions(t *testing.T) { NewActionSayTruco(0), NewActionSayMeVoyAlMazo(0), } - + for _, action := range expectedActions { + action.Enrich(*gameState) + } require.Equal( t, _serializeActions(expectedActions), @@ -40,9 +42,10 @@ func TestAfterRealEnvidoOptions(t *testing.T) { if err != nil { t.Fatal(err) } - require.Equal( - t, - _serializeActions(expectedActions), - gameState.PossibleActions, - ) + for _, action := range expectedActions { + action.Enrich(*gameState) + } + for i, expectedAction := range _serializeActions(expectedActions) { + require.Equal(t, string(expectedAction), string(gameState.PossibleActions[i])) + } }