Skip to content

Commit

Permalink
Irresponsibly half implement flor on a Sunday.
Browse files Browse the repository at this point in the history
  • Loading branch information
marianogappa committed Jul 28, 2024
1 parent bcfafea commit ac3332f
Show file tree
Hide file tree
Showing 14 changed files with 1,016 additions and 224 deletions.
150 changes: 130 additions & 20 deletions examplebot/bot.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand All @@ -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,
Expand All @@ -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:
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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.")
Expand All @@ -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
Expand Down
116 changes: 0 additions & 116 deletions truco/action_any_quiero.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package truco

import (
"fmt"
"slices"
)

Expand All @@ -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"`
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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()
}
4 changes: 4 additions & 0 deletions truco/action_confirm_round_ended.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
5 changes: 5 additions & 0 deletions truco/action_reveal_card.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
4 changes: 4 additions & 0 deletions truco/action_son_buenas.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
4 changes: 4 additions & 0 deletions truco/action_son_mejores.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Loading

0 comments on commit ac3332f

Please sign in to comment.