From 13ca3e933d4741756367c177cc974baaa47253b7 Mon Sep 17 00:00:00 2001 From: Mariano Gappa Date: Fri, 2 Aug 2024 10:31:42 +0100 Subject: [PATCH 1/2] Fix broken method. --- truco/envido_sequence_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/truco/envido_sequence_test.go b/truco/envido_sequence_test.go index 4827ef1..8e75181 100644 --- a/truco/envido_sequence_test.go +++ b/truco/envido_sequence_test.go @@ -171,7 +171,7 @@ func TestEnvidoSequence(t *testing.T) { continue } - cost, err := gameState.EnvidoSequence.Cost(gameState.RuleMaxPoints, gameState.Players[gameState.TurnPlayerID].Score, gameState.Players[gameState.TurnOpponentPlayerID].Score) + cost, err := gameState.EnvidoSequence.Cost(gameState.RuleMaxPoints, gameState.Players[gameState.TurnPlayerID].Score, gameState.Players[gameState.TurnOpponentPlayerID].Score, true) require.NoError(t, err) assert.Equal(t, step.expectedCostAfterRunning, cost, "at step %v expected cost %v but got %v", i, step.expectedCostAfterRunning, cost) } From 10426df880a3e69ffb854de65123c704988d0cf1 Mon Sep 17 00:00:00 2001 From: Mariano Gappa Date: Wed, 7 Aug 2024 19:00:36 +0100 Subject: [PATCH 2/2] Add new bot implementation. Fix falta calculation. --- examplebot/newbot/bot.go | 68 ++ examplebot/newbot/loggers.go | 11 + examplebot/newbot/rule_init_state.go | 157 +++++ examplebot/newbot/rule_initiate_envido.go | 88 +++ examplebot/newbot/rule_initiate_flor.go | 31 + examplebot/newbot/rule_initiate_truco.go | 42 ++ examplebot/newbot/rule_no_more_actions.go | 27 + .../newbot/rule_respond_to_contraflor.go | 66 ++ .../rule_respond_to_contraflor_al_resto.go | 63 ++ examplebot/newbot/rule_respond_to_envido.go | 92 +++ .../newbot/rule_respond_to_falta_envido.go | 80 +++ examplebot/newbot/rule_respond_to_flor.go | 69 ++ .../newbot/rule_respond_to_quiero_retruco.go | 53 ++ .../rule_respond_to_quiero_vale_cuatro.go | 47 ++ .../newbot/rule_respond_to_real_envido.go | 86 +++ examplebot/newbot/rule_respond_to_truco.go | 53 ++ examplebot/newbot/rule_reveal_card.go | 44 ++ examplebot/newbot/rules.go | 83 +++ examplebot/newbot/truco_analysis.go | 656 ++++++++++++++++++ examplebot/newbot/utils.go | 54 ++ main.go | 4 +- main_wasm.go | 4 +- truco/action_any_quiero.go | 2 +- truco/actions_any_flor.go | 10 +- truco/actions_any_flor_test.go | 16 + truco/actions_any_truco_test.go | 108 +++ truco/envido_sequence.go | 20 +- 27 files changed, 2019 insertions(+), 15 deletions(-) create mode 100644 examplebot/newbot/bot.go create mode 100644 examplebot/newbot/loggers.go create mode 100644 examplebot/newbot/rule_init_state.go create mode 100644 examplebot/newbot/rule_initiate_envido.go create mode 100644 examplebot/newbot/rule_initiate_flor.go create mode 100644 examplebot/newbot/rule_initiate_truco.go create mode 100644 examplebot/newbot/rule_no_more_actions.go create mode 100644 examplebot/newbot/rule_respond_to_contraflor.go create mode 100644 examplebot/newbot/rule_respond_to_contraflor_al_resto.go create mode 100644 examplebot/newbot/rule_respond_to_envido.go create mode 100644 examplebot/newbot/rule_respond_to_falta_envido.go create mode 100644 examplebot/newbot/rule_respond_to_flor.go create mode 100644 examplebot/newbot/rule_respond_to_quiero_retruco.go create mode 100644 examplebot/newbot/rule_respond_to_quiero_vale_cuatro.go create mode 100644 examplebot/newbot/rule_respond_to_real_envido.go create mode 100644 examplebot/newbot/rule_respond_to_truco.go create mode 100644 examplebot/newbot/rule_reveal_card.go create mode 100644 examplebot/newbot/rules.go create mode 100644 examplebot/newbot/truco_analysis.go create mode 100644 examplebot/newbot/utils.go create mode 100644 truco/actions_any_truco_test.go diff --git a/examplebot/newbot/bot.go b/examplebot/newbot/bot.go new file mode 100644 index 0000000..f8edef6 --- /dev/null +++ b/examplebot/newbot/bot.go @@ -0,0 +1,68 @@ +package newbot + +import ( + "fmt" + "os" + + "log" + + "github.com/marianogappa/truco/truco" +) + +type Bot struct { + orderedRules []rule + st state + logger Logger +} + +func WithDefaultLogger(b *Bot) { + b.logger = log.New(os.Stderr, "", log.LstdFlags) +} + +func New(opts ...func(*Bot)) *Bot { + // Rules organically form a DAG. Kahn flattens them into a linear order. + // If this is not possible (i.e. it's not a DAG), it blows up. + orderedRules, err := topologicalSortKahn(rules) + if err != nil { + panic(fmt.Errorf("couldn't sort rules: %w; bot is defective! please report this bug!", err)) + } + + b := &Bot{orderedRules: orderedRules, logger: NoOpLogger{}, st: state{}} + for _, opt := range opts { + opt(b) + } + + return b +} + +func (m Bot) ChooseAction(gs truco.ClientGameState) truco.Action { + // Trivial cases + if len(gs.PossibleActions) == 0 { + return nil + } + if len(gs.PossibleActions) == 1 { + return _deserializeActions(gs.PossibleActions)[0] + } + + // If trickier, run rules + for _, r := range m.orderedRules { + if !r.isApplicable(m.st, gs) { + continue + } + res, err := r.run(m.st, gs) + if err != nil { + panic(fmt.Errorf("Running rule %v found bug: %w. Please report this bug.", r.name, err)) + } + m.logger.Printf("Running applicable rule %v: %s, result: %v", r.name, r.description, res.resultDescription) + for _, sc := range res.stateChanges { + m.logger.Printf("State change: %s", sc.description) + sc.fn(&m.st) + } + if res.action != nil { + return res.action + } + } + + // Running all rules MUST always result in an action being chosen + panic("no action chosen after running all rules; bot is defective! please report this bug!") +} diff --git a/examplebot/newbot/loggers.go b/examplebot/newbot/loggers.go new file mode 100644 index 0000000..413956f --- /dev/null +++ b/examplebot/newbot/loggers.go @@ -0,0 +1,11 @@ +package newbot + +type Logger interface { + Printf(format string, v ...any) + Println(v ...any) +} + +type NoOpLogger struct{} + +func (NoOpLogger) Printf(format string, v ...any) {} +func (NoOpLogger) Println(v ...any) {} diff --git a/examplebot/newbot/rule_init_state.go b/examplebot/newbot/rule_init_state.go new file mode 100644 index 0000000..20abfd7 --- /dev/null +++ b/examplebot/newbot/rule_init_state.go @@ -0,0 +1,157 @@ +package newbot + +import ( + "fmt" + + "github.com/marianogappa/truco/truco" +) + +var ( + ruleInitState = rule{ + name: "ruleInitState", + description: "Initialises the bot's state", + isApplicable: ruleInitStateIsApplicable, + dependsOn: []rule{}, + run: ruleInitStateRun, + } +) + +func init() { + registerRule(ruleInitState) +} + +func ruleInitStateIsApplicable(state, truco.ClientGameState) bool { + return true +} + +func ruleInitStateRun(_ state, gs truco.ClientGameState) (ruleResult, error) { + var ( + aggresiveness = calculateAggresiveness(gs) + possibleActions = possibleActionsMap(gs) + possibleActionNameSet = possibleActionNameSet(possibleActions) + envidoScore = calculateEnvidoScore(gs) + florScore = calculateFlorScore(gs) + faceoffResults = calculateFaceoffResults(gs) + pointsToLose = calculatePointsToLose(gs) + ) + + var ( + stateChangeAggresiveness = stateChange{ + fn: func(st *state) { + (*st)["aggresiveness"] = aggresiveness + }, + description: fmt.Sprintf("Set aggresiveness to %v", aggresiveness), + } + statePossibleActions = stateChange{ + fn: func(st *state) { + (*st)["possibleActions"] = possibleActions + }, + description: fmt.Sprintf("Set possibleActions to %v", possibleActions), + } + statePossibleActionNameSet = stateChange{ + fn: func(st *state) { + (*st)["possibleActionNameSet"] = possibleActionNameSet + }, + description: fmt.Sprintf("Set possibleActionNameSet to %v", possibleActionNameSet), + } + stateEnvidoScore = stateChange{ + fn: func(st *state) { + (*st)["envidoScore"] = envidoScore + }, + description: fmt.Sprintf("Set envidoScore to %v", envidoScore), + } + stateFlorScore = stateChange{ + fn: func(st *state) { + (*st)["florScore"] = florScore + }, + description: fmt.Sprintf("Set florScore to %v", florScore), + } + stateFaceoffResults = stateChange{ + fn: func(st *state) { + (*st)["faceoffResults"] = faceoffResults + }, + description: fmt.Sprintf("Set faceoffResults to %v", faceoffResults), + } + statePointsToLose = stateChange{ + fn: func(st *state) { + (*st)["pointsToLose"] = pointsToLose + }, + description: fmt.Sprintf("Set pointsToLose to %v", pointsToLose), + } + ) + + return ruleResult{ + action: nil, + stateChanges: []stateChange{ + stateChangeAggresiveness, + statePossibleActions, + statePossibleActionNameSet, + stateEnvidoScore, + stateFlorScore, + stateFaceoffResults, + statePointsToLose, + }, + resultDescription: "Initialised bot's state.", + }, nil +} + +func calculateAggresiveness(gs truco.ClientGameState) string { + aggresiveness := "normal" + if gs.YourScore-gs.TheirScore >= 5 { + aggresiveness = "low" + } + if gs.YourScore-gs.TheirScore <= -5 { + aggresiveness = "high" + } + return aggresiveness +} + +func possibleActionsMap(gs truco.ClientGameState) map[string]truco.Action { + possibleActions := make(map[string]truco.Action) + for _, action := range _deserializeActions(gs.PossibleActions) { + possibleActions[action.GetName()] = action + } + return possibleActions +} + +func possibleActionNameSet(mp map[string]truco.Action) map[string]struct{} { + possibleActionNames := make(map[string]struct{}) + for name := range mp { + possibleActionNames[name] = struct{}{} + } + return possibleActionNames +} + +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 calculateFaceoffResults(gs truco.ClientGameState) []int { + results := []int{} + for i := 0; i < min(len(gs.YourRevealedCards), len(gs.TheirRevealedCards)); i++ { + results = append(results, gs.YourRevealedCards[i].CompareTrucoScore(gs.TheirRevealedCards[i])) + } + return results +} + +const ( + FACEOFF_WIN = 1 + FACEOFF_LOSS = -1 + FACEOFF_TIE = 0 +) + +func calculatePointsToLose(gs truco.ClientGameState) int { + return gs.RuleMaxPoints - gs.TheirScore +} + +func pointsToWin(gs truco.ClientGameState) int { + return gs.RuleMaxPoints - gs.YourScore +} + +func youMano(gs truco.ClientGameState) bool { + return gs.RoundTurnPlayerID == gs.YouPlayerID +} diff --git a/examplebot/newbot/rule_initiate_envido.go b/examplebot/newbot/rule_initiate_envido.go new file mode 100644 index 0000000..642e820 --- /dev/null +++ b/examplebot/newbot/rule_initiate_envido.go @@ -0,0 +1,88 @@ +package newbot + +import ( + "fmt" + "math/rand" + + "github.com/marianogappa/truco/truco" +) + +var ( + ruleInitiateEnvido = rule{ + name: "ruleInitiateEnvido", + description: "Decides whether to initiate an Envido action", + isApplicable: ruleInitiateEnvidoIsApplicable, + dependsOn: []rule{ruleRespondToQuieroValeCuatro}, + run: ruleInitiateEnvidoRun, + } +) + +func init() { + registerRule(ruleInitiateEnvido) +} + +func ruleInitiateEnvidoIsApplicable(st state, _ truco.ClientGameState) bool { + return isPossibleAll(st, truco.SAY_ENVIDO, truco.SAY_REAL_ENVIDO, truco.SAY_FALTA_ENVIDO) +} + +func ruleInitiateEnvidoRun(st state, gs truco.ClientGameState) (ruleResult, error) { + agg := aggresiveness(st) + envidoScore := envidoScore(st) + + decisionTree := map[string]map[string][2]int{ + "low": { + truco.SAY_ENVIDO: [2]int{26, 28}, + truco.SAY_REAL_ENVIDO: [2]int{29, 30}, + truco.SAY_FALTA_ENVIDO: [2]int{31, 33}, + }, + "normal": { + truco.SAY_ENVIDO: [2]int{25, 27}, + truco.SAY_REAL_ENVIDO: [2]int{28, 29}, + truco.SAY_FALTA_ENVIDO: [2]int{30, 33}, + }, + "high": { + truco.SAY_ENVIDO: [2]int{23, 25}, + truco.SAY_REAL_ENVIDO: [2]int{26, 28}, + truco.SAY_FALTA_ENVIDO: [2]int{29, 33}, + }, + } + + decisionTreeForAgg := decisionTree[agg] + lied := false + + for actionName, scoreRange := range decisionTreeForAgg { + if envidoScore >= scoreRange[0] && envidoScore <= scoreRange[1] { + // Lie one out of 3 times + if rand.Intn(3) == 0 { + lied = true + break + } + + // Exception: if pointsToWin == 1, choose SAY_FALTA_ENVIDO + if pointsToWin(gs) == 1 { + actionName = truco.SAY_FALTA_ENVIDO + } + + return ruleResult{ + action: getAction(st, actionName), + stateChanges: []stateChange{}, + resultDescription: fmt.Sprintf("Decided to initiate %v given decision tree for %v aggressiveness and envido score of %v.", actionName, agg, envidoScore), + }, nil + } + } + + // If didn't lie before, one out of 3 times decide to initiate envido as a lie + if !lied && rand.Intn(3) == 0 { + return ruleResult{ + action: getAction(st, truco.SAY_ENVIDO), + stateChanges: []stateChange{}, + resultDescription: fmt.Sprintf("Decided to lie (33%% chance) and say envido even though I shouldn't according to rules."), + }, nil + } + + return ruleResult{ + action: nil, + stateChanges: []stateChange{}, + resultDescription: fmt.Sprintf("Decided not to initiate an envido action given decision tree for %v aggressiveness and envido score of %v.", agg, envidoScore), + }, nil +} diff --git a/examplebot/newbot/rule_initiate_flor.go b/examplebot/newbot/rule_initiate_flor.go new file mode 100644 index 0000000..39532e7 --- /dev/null +++ b/examplebot/newbot/rule_initiate_flor.go @@ -0,0 +1,31 @@ +package newbot + +import ( + "github.com/marianogappa/truco/truco" +) + +var ( + ruleInitiateFlor = rule{ + name: "ruleInitiateFlor", + description: "Decides whether to initiate Flor action", + isApplicable: ruleInitiateFlorIsApplicable, + dependsOn: []rule{ruleRespondToQuieroValeCuatro}, + run: ruleInitiateFlorRun, + } +) + +func init() { + registerRule(ruleInitiateFlor) +} + +func ruleInitiateFlorIsApplicable(st state, _ truco.ClientGameState) bool { + return isPossibleAll(st, truco.SAY_FLOR) +} + +func ruleInitiateFlorRun(st state, gs truco.ClientGameState) (ruleResult, error) { + return ruleResult{ + action: getAction(st, truco.SAY_FLOR), + stateChanges: []stateChange{}, + resultDescription: "Always initiate Flor", + }, nil +} diff --git a/examplebot/newbot/rule_initiate_truco.go b/examplebot/newbot/rule_initiate_truco.go new file mode 100644 index 0000000..3c69243 --- /dev/null +++ b/examplebot/newbot/rule_initiate_truco.go @@ -0,0 +1,42 @@ +package newbot + +import ( + "github.com/marianogappa/truco/truco" +) + +var ( + ruleInitiateTruco = rule{ + name: "ruleInitiateTruco", + description: "Decides whether to initiate a Truco action", + isApplicable: ruleInitiateTrucoIsApplicable, + dependsOn: []rule{ruleInitiateEnvido}, + run: ruleInitiateTrucoRun, + } +) + +func init() { + registerRule(ruleInitiateTruco) +} + +func ruleInitiateTrucoIsApplicable(st state, _ truco.ClientGameState) bool { + return isPossibleAll(st, truco.SAY_TRUCO) +} + +func ruleInitiateTrucoRun(st state, gs truco.ClientGameState) (ruleResult, error) { + result := analyzeTruco(st, gs, false) + + switch { + case result.shouldInitiate: + return ruleResult{ + action: getAction(st, truco.SAY_TRUCO), + stateChanges: []stateChange{}, + resultDescription: result.description, + }, nil + default: + return ruleResult{ + action: nil, + stateChanges: []stateChange{}, + resultDescription: result.description, + }, nil + } +} diff --git a/examplebot/newbot/rule_no_more_actions.go b/examplebot/newbot/rule_no_more_actions.go new file mode 100644 index 0000000..dfaa9e8 --- /dev/null +++ b/examplebot/newbot/rule_no_more_actions.go @@ -0,0 +1,27 @@ +package newbot + +import ( + "github.com/marianogappa/truco/truco" +) + +var ( + ruleNoMoreActions = rule{ + name: "ruleNoMoreActions", + description: "Blows up because no more actions are possible", + isApplicable: ruleNoMoreActionsIsApplicable, + dependsOn: []rule{ruleRevealCard}, + run: ruleNoMoreActionsRun, + } +) + +func init() { + registerRule(ruleNoMoreActions) +} + +func ruleNoMoreActionsIsApplicable(st state, _ truco.ClientGameState) bool { + return true +} + +func ruleNoMoreActionsRun(st state, gs truco.ClientGameState) (ruleResult, error) { + panic("No more actions are possible.") +} diff --git a/examplebot/newbot/rule_respond_to_contraflor.go b/examplebot/newbot/rule_respond_to_contraflor.go new file mode 100644 index 0000000..33472ca --- /dev/null +++ b/examplebot/newbot/rule_respond_to_contraflor.go @@ -0,0 +1,66 @@ +package newbot + +import ( + "errors" + "fmt" + + "github.com/marianogappa/truco/truco" +) + +var ( + ruleRespondToContraflor = rule{ + name: "ruleRespondToContraflor", + description: "Responds to a Contraflor action", + isApplicable: ruleRespondToContraflorIsApplicable, + dependsOn: []rule{ruleRespondToFlor}, + run: ruleRespondToContraflorRun, + } +) + +func init() { + registerRule(ruleRespondToContraflor) +} + +// TODO: rule for when saying no quiero makes you lose the game +// TODO: rule for when less actions are possible (e.g. if opponent said truco.SAY_CONTRAFLOR) + +func ruleRespondToContraflorIsApplicable(st state, _ truco.ClientGameState) bool { + return isPossibleAll(st, truco.SAY_CON_FLOR_ME_ACHICO, truco.SAY_CON_FLOR_QUIERO, truco.SAY_CONTRAFLOR_AL_RESTO) +} + +func ruleRespondToContraflorRun(st state, gs truco.ClientGameState) (ruleResult, error) { + agg := aggresiveness(st) + florScore := florScore(st) + + decisionTree := map[string]map[string][2]int{ + "low": { + truco.SAY_CON_FLOR_ME_ACHICO: [2]int{20, 28}, + truco.SAY_CON_FLOR_QUIERO: [2]int{29, 33}, + truco.SAY_CONTRAFLOR_AL_RESTO: [2]int{34, 38}, + }, + "normal": { + truco.SAY_CON_FLOR_ME_ACHICO: [2]int{20, 26}, + truco.SAY_CON_FLOR_QUIERO: [2]int{27, 31}, + truco.SAY_CONTRAFLOR_AL_RESTO: [2]int{32, 38}, + }, + "high": { + truco.SAY_CON_FLOR_ME_ACHICO: [2]int{20, 23}, + truco.SAY_CON_FLOR_QUIERO: [2]int{24, 27}, + truco.SAY_CONTRAFLOR_AL_RESTO: [2]int{28, 38}, + }, + } + + decisionTreeForAgg := decisionTree[agg] + + for actionName, scoreRange := range decisionTreeForAgg { + if florScore >= scoreRange[0] && florScore <= scoreRange[1] { + return ruleResult{ + action: getAction(st, actionName), + stateChanges: []stateChange{}, + resultDescription: fmt.Sprintf("Responded to Contraflor with %v given decision tree for %v aggressiveness and flor score of %v.", actionName, agg, florScore), + }, nil + } + } + + return ruleResult{}, errors.New("Couldn't find a suitable action to respond to Contraflor.") +} diff --git a/examplebot/newbot/rule_respond_to_contraflor_al_resto.go b/examplebot/newbot/rule_respond_to_contraflor_al_resto.go new file mode 100644 index 0000000..a1f72f4 --- /dev/null +++ b/examplebot/newbot/rule_respond_to_contraflor_al_resto.go @@ -0,0 +1,63 @@ +package newbot + +import ( + "errors" + "fmt" + + "github.com/marianogappa/truco/truco" +) + +var ( + ruleRespondToContraflorAlResto = rule{ + name: "ruleRespondToContraflorAlResto", + description: "Responds to a Contraflor Al Resto action", + isApplicable: ruleRespondToContraflorAlRestoIsApplicable, + dependsOn: []rule{ruleRespondToContraflor}, + run: ruleRespondToContraflorAlRestoRun, + } +) + +func init() { + registerRule(ruleRespondToContraflorAlResto) +} + +// TODO: rule for when saying no quiero makes you lose the game +// TODO: rule for when less actions are possible (e.g. if opponent said truco.SAY_CONTRAFLOR) + +func ruleRespondToContraflorAlRestoIsApplicable(st state, _ truco.ClientGameState) bool { + return isPossibleAll(st, truco.SAY_CON_FLOR_ME_ACHICO, truco.SAY_CON_FLOR_QUIERO) +} + +func ruleRespondToContraflorAlRestoRun(st state, gs truco.ClientGameState) (ruleResult, error) { + agg := aggresiveness(st) + florScore := florScore(st) + + decisionTree := map[string]map[string][2]int{ + "low": { + truco.SAY_CON_FLOR_ME_ACHICO: [2]int{20, 33}, + truco.SAY_CON_FLOR_QUIERO: [2]int{34, 38}, + }, + "normal": { + truco.SAY_CON_FLOR_ME_ACHICO: [2]int{20, 30}, + truco.SAY_CON_FLOR_QUIERO: [2]int{30, 38}, + }, + "high": { + truco.SAY_CON_FLOR_ME_ACHICO: [2]int{20, 26}, + truco.SAY_CON_FLOR_QUIERO: [2]int{27, 38}, + }, + } + + decisionTreeForAgg := decisionTree[agg] + + for actionName, scoreRange := range decisionTreeForAgg { + if florScore >= scoreRange[0] && florScore <= scoreRange[1] { + return ruleResult{ + action: getAction(st, actionName), + stateChanges: []stateChange{}, + resultDescription: fmt.Sprintf("Responded to Contraflor with %v given decision tree for %v aggressiveness and flor score of %v.", actionName, agg, florScore), + }, nil + } + } + + return ruleResult{}, errors.New("Couldn't find a suitable action to respond to Contraflor.") +} diff --git a/examplebot/newbot/rule_respond_to_envido.go b/examplebot/newbot/rule_respond_to_envido.go new file mode 100644 index 0000000..b7bd383 --- /dev/null +++ b/examplebot/newbot/rule_respond_to_envido.go @@ -0,0 +1,92 @@ +package newbot + +import ( + "errors" + "fmt" + + "github.com/marianogappa/truco/truco" +) + +var ( + ruleRespondToEnvido = rule{ + name: "ruleRespondToEnvido", + description: "Responds to an Envido action", + isApplicable: ruleRespondToEnvidoIsApplicable, + dependsOn: []rule{ruleInitState}, + run: ruleRespondToEnvidoRun, + } +) + +func init() { + registerRule(ruleRespondToEnvido) +} + +// TODO: replace envido action with falta_envido when with either bot wins, but if they lose they lose less points. + +func ruleRespondToEnvidoIsApplicable(st state, _ truco.ClientGameState) bool { + return isPossibleAll(st, truco.SAY_ENVIDO_NO_QUIERO, truco.SAY_ENVIDO_QUIERO, truco.SAY_REAL_ENVIDO, truco.SAY_FALTA_ENVIDO) +} + +func ruleRespondToEnvidoRun(st state, gs truco.ClientGameState) (ruleResult, error) { + agg := aggresiveness(st) + envidoScore := envidoScore(st) + pointsToLose := pointsToLose(st) + pointsToWin := pointsToWin(gs) + costOfNoQuiero := getAction(st, truco.SAY_ENVIDO_NO_QUIERO).(*truco.ActionSayEnvidoNoQuiero).Cost + + if costOfNoQuiero >= pointsToLose { + return ruleResult{ + action: getAction(st, truco.SAY_ENVIDO_QUIERO), + stateChanges: []stateChange{}, + resultDescription: fmt.Sprintf("Responded to Envido with quiero because no quiero costs %v and I will lose in %v points.", costOfNoQuiero, pointsToLose), + }, nil + } + if pointsToWin == 1 { + return ruleResult{ + action: getAction(st, truco.SAY_FALTA_ENVIDO), + stateChanges: []stateChange{}, + resultDescription: "When there's only 1 point left to win, always respond with Falta Envido.", + }, nil + } + + decisionTree := map[string]map[string][2]int{ + "low": { + truco.SAY_ENVIDO_NO_QUIERO: [2]int{0, 25}, + truco.SAY_ENVIDO_QUIERO: [2]int{26, 28}, + truco.SAY_REAL_ENVIDO: [2]int{29, 30}, + truco.SAY_FALTA_ENVIDO: [2]int{31, 33}, + }, + "normal": { + truco.SAY_ENVIDO_NO_QUIERO: [2]int{0, 24}, + truco.SAY_ENVIDO_QUIERO: [2]int{25, 27}, + truco.SAY_REAL_ENVIDO: [2]int{28, 29}, + truco.SAY_FALTA_ENVIDO: [2]int{30, 33}, + }, + "high": { + truco.SAY_ENVIDO_NO_QUIERO: [2]int{0, 22}, + truco.SAY_ENVIDO_QUIERO: [2]int{23, 26}, + truco.SAY_REAL_ENVIDO: [2]int{27, 28}, + truco.SAY_FALTA_ENVIDO: [2]int{29, 33}, + }, + } + + decisionTreeForAgg := decisionTree[agg] + + for actionName, scoreRange := range decisionTreeForAgg { + if envidoScore >= scoreRange[0] && envidoScore <= scoreRange[1] { + + // Exception: if pointsToWin == 1, SAY_FALTA_ENVIDO is the same as any other action. + if pointsToLose == 1 { + actionName = truco.SAY_ENVIDO_QUIERO + } + + return ruleResult{ + action: getAction(st, actionName), + stateChanges: []stateChange{}, + resultDescription: fmt.Sprintf("Responded to Envido with %v given decision tree for %v aggressiveness and envido score of %v.", actionName, agg, envidoScore), + }, nil + } + } + + return ruleResult{}, errors.New("Couldn't find a suitable action to respond to Envido.") +} diff --git a/examplebot/newbot/rule_respond_to_falta_envido.go b/examplebot/newbot/rule_respond_to_falta_envido.go new file mode 100644 index 0000000..a3482f7 --- /dev/null +++ b/examplebot/newbot/rule_respond_to_falta_envido.go @@ -0,0 +1,80 @@ +package newbot + +import ( + "errors" + "fmt" + + "github.com/marianogappa/truco/truco" +) + +var ( + ruleRespondToFaltaEnvido = rule{ + name: "ruleRespondToFaltaEnvido", + description: "Responds to a Falta Envido action", + isApplicable: ruleRespondToFaltaEnvidoIsApplicable, + dependsOn: []rule{ruleRespondToRealEnvido}, + run: ruleRespondToFaltaEnvidoRun, + } +) + +func init() { + registerRule(ruleRespondToFaltaEnvido) +} + +func ruleRespondToFaltaEnvidoIsApplicable(st state, _ truco.ClientGameState) bool { + return isPossibleAll(st, truco.SAY_ENVIDO_NO_QUIERO, truco.SAY_ENVIDO_QUIERO) +} + +func ruleRespondToFaltaEnvidoRun(st state, gs truco.ClientGameState) (ruleResult, error) { + var ( + agg = aggresiveness(st) + envidoScore = envidoScore(st) + pointsToLose = pointsToLose(st) + pointsToWin = pointsToWin(gs) + costOfNoQuiero = getAction(st, truco.SAY_ENVIDO_NO_QUIERO).(*truco.ActionSayEnvidoNoQuiero).Cost + ) + + if costOfNoQuiero >= pointsToLose { + return ruleResult{ + action: getAction(st, truco.SAY_ENVIDO_QUIERO), + stateChanges: []stateChange{}, + resultDescription: fmt.Sprintf("Responded to Falta Envido with quiero because no quiero costs %v and I will lose in %v points.", costOfNoQuiero, pointsToLose), + }, nil + } + if pointsToWin == 1 { + return ruleResult{ + action: getAction(st, truco.SAY_ENVIDO_QUIERO), + stateChanges: []stateChange{}, + resultDescription: "When there's only 1 point left to win, always accept Falta Envido.", + }, nil + } + + decisionTree := map[string]map[string][2]int{ + "low": { + truco.SAY_ENVIDO_NO_QUIERO: [2]int{0, 29}, + truco.SAY_ENVIDO_QUIERO: [2]int{30, 33}, + }, + "normal": { + truco.SAY_ENVIDO_NO_QUIERO: [2]int{0, 27}, + truco.SAY_ENVIDO_QUIERO: [2]int{28, 33}, + }, + "high": { + truco.SAY_ENVIDO_NO_QUIERO: [2]int{0, 26}, + truco.SAY_ENVIDO_QUIERO: [2]int{27, 33}, + }, + } + + decisionTreeForAgg := decisionTree[agg] + + for actionName, scoreRange := range decisionTreeForAgg { + if envidoScore >= scoreRange[0] && envidoScore <= scoreRange[1] { + return ruleResult{ + action: getAction(st, actionName), + stateChanges: []stateChange{}, + resultDescription: fmt.Sprintf("Responded to Falta Envido with %v given decision tree for %v aggressiveness and envido score of %v.", actionName, agg, envidoScore), + }, nil + } + } + + return ruleResult{}, errors.New("Couldn't find a suitable action to respond to Falta Envido.") +} diff --git a/examplebot/newbot/rule_respond_to_flor.go b/examplebot/newbot/rule_respond_to_flor.go new file mode 100644 index 0000000..6d5af5d --- /dev/null +++ b/examplebot/newbot/rule_respond_to_flor.go @@ -0,0 +1,69 @@ +package newbot + +import ( + "errors" + "fmt" + + "github.com/marianogappa/truco/truco" +) + +var ( + ruleRespondToFlor = rule{ + name: "ruleRespondToFlor", + description: "Responds to a Flor action", + isApplicable: ruleRespondToFlorIsApplicable, + dependsOn: []rule{ruleInitState}, + run: ruleRespondToFlorRun, + } +) + +func init() { + registerRule(ruleRespondToFlor) +} + +// TODO: rule for when saying no quiero makes you lose the game +// TODO: rule for when less actions are possible (e.g. if opponent said truco.SAY_CONTRAFLOR) + +func ruleRespondToFlorIsApplicable(st state, _ truco.ClientGameState) bool { + return isPossibleAll(st, truco.SAY_CON_FLOR_ME_ACHICO, truco.SAY_CON_FLOR_QUIERO, truco.SAY_CONTRAFLOR, truco.SAY_CONTRAFLOR_AL_RESTO) +} + +func ruleRespondToFlorRun(st state, gs truco.ClientGameState) (ruleResult, error) { + agg := aggresiveness(st) + florScore := florScore(st) + + decisionTree := map[string]map[string][2]int{ + "low": { + truco.SAY_CON_FLOR_ME_ACHICO: [2]int{20, 26}, + truco.SAY_CON_FLOR_QUIERO: [2]int{27, 29}, + truco.SAY_CONTRAFLOR: [2]int{30, 33}, + truco.SAY_CONTRAFLOR_AL_RESTO: [2]int{34, 38}, + }, + "normal": { + truco.SAY_CON_FLOR_ME_ACHICO: [2]int{20, 24}, + truco.SAY_CON_FLOR_QUIERO: [2]int{25, 27}, + truco.SAY_CONTRAFLOR: [2]int{28, 30}, + truco.SAY_CONTRAFLOR_AL_RESTO: [2]int{31, 38}, + }, + "high": { + truco.SAY_CON_FLOR_ME_ACHICO: [2]int{20, 23}, + truco.SAY_CON_FLOR_QUIERO: [2]int{24, 25}, + truco.SAY_CONTRAFLOR: [2]int{26, 28}, + truco.SAY_CONTRAFLOR_AL_RESTO: [2]int{29, 38}, + }, + } + + decisionTreeForAgg := decisionTree[agg] + + for actionName, scoreRange := range decisionTreeForAgg { + if florScore >= scoreRange[0] && florScore <= scoreRange[1] { + return ruleResult{ + action: getAction(st, actionName), + stateChanges: []stateChange{}, + resultDescription: fmt.Sprintf("Responded to Flor with %v given decision tree for %v aggressiveness and flor score of %v.", actionName, agg, florScore), + }, nil + } + } + + return ruleResult{}, errors.New("Couldn't find a suitable action to respond to Flor.") +} diff --git a/examplebot/newbot/rule_respond_to_quiero_retruco.go b/examplebot/newbot/rule_respond_to_quiero_retruco.go new file mode 100644 index 0000000..ffe635d --- /dev/null +++ b/examplebot/newbot/rule_respond_to_quiero_retruco.go @@ -0,0 +1,53 @@ +package newbot + +import ( + "github.com/marianogappa/truco/truco" +) + +var ( + ruleRespondToQuieroRetruco = rule{ + name: "ruleRespondToQuieroRetruco", + description: "Responds to a Quiero Retruco action", + isApplicable: ruleRespondToQuieroRetrucoIsApplicable, + dependsOn: []rule{ruleRespondToTruco}, + run: ruleRespondToQuieroRetrucoRun, + } +) + +func init() { + registerRule(ruleRespondToQuieroRetruco) +} + +func ruleRespondToQuieroRetrucoIsApplicable(st state, _ truco.ClientGameState) bool { + return isPossibleAll(st, truco.SAY_TRUCO_NO_QUIERO, truco.SAY_TRUCO_QUIERO, truco.SAY_QUIERO_VALE_CUATRO) +} + +func ruleRespondToQuieroRetrucoRun(st state, gs truco.ClientGameState) (ruleResult, error) { + var ( + pointsToLose = pointsToLose(st) + costOfNoQuiero = getAction(st, truco.SAY_TRUCO_NO_QUIERO).(*truco.ActionSayTrucoNoQuiero).Cost + noQuieroLosesGame = costOfNoQuiero >= pointsToLose + ) + + result := analyzeTruco(st, gs, noQuieroLosesGame) + switch { + case result.shouldRaise: + return ruleResult{ + action: getAction(st, truco.SAY_QUIERO_VALE_CUATRO), + stateChanges: []stateChange{}, + resultDescription: result.description, + }, nil + case result.shouldQuiero: + return ruleResult{ + action: getAction(st, truco.SAY_TRUCO_QUIERO), + stateChanges: []stateChange{}, + resultDescription: result.description, + }, nil + default: + return ruleResult{ + action: getAction(st, truco.SAY_TRUCO_NO_QUIERO), + stateChanges: []stateChange{}, + resultDescription: result.description, + }, nil + } +} diff --git a/examplebot/newbot/rule_respond_to_quiero_vale_cuatro.go b/examplebot/newbot/rule_respond_to_quiero_vale_cuatro.go new file mode 100644 index 0000000..daf7418 --- /dev/null +++ b/examplebot/newbot/rule_respond_to_quiero_vale_cuatro.go @@ -0,0 +1,47 @@ +package newbot + +import ( + "github.com/marianogappa/truco/truco" +) + +var ( + ruleRespondToQuieroValeCuatro = rule{ + name: "ruleRespondToQuieroValeCuatro", + description: "Responds to a Quiero Vale Cuatro action", + isApplicable: ruleRespondToQuieroValeCuatroIsApplicable, + dependsOn: []rule{ruleRespondToQuieroRetruco}, + run: ruleRespondToQuieroValeCuatroRun, + } +) + +func init() { + registerRule(ruleRespondToQuieroValeCuatro) +} + +func ruleRespondToQuieroValeCuatroIsApplicable(st state, _ truco.ClientGameState) bool { + return isPossibleAll(st, truco.SAY_TRUCO_NO_QUIERO, truco.SAY_TRUCO_QUIERO) +} + +func ruleRespondToQuieroValeCuatroRun(st state, gs truco.ClientGameState) (ruleResult, error) { + var ( + pointsToLose = pointsToLose(st) + costOfNoQuiero = getAction(st, truco.SAY_TRUCO_NO_QUIERO).(*truco.ActionSayTrucoNoQuiero).Cost + noQuieroLosesGame = costOfNoQuiero >= pointsToLose + ) + + result := analyzeTruco(st, gs, noQuieroLosesGame) + switch { + case result.shouldRaise, result.shouldQuiero: + return ruleResult{ + action: getAction(st, truco.SAY_TRUCO_QUIERO), + stateChanges: []stateChange{}, + resultDescription: result.description, + }, nil + default: + return ruleResult{ + action: getAction(st, truco.SAY_TRUCO_NO_QUIERO), + stateChanges: []stateChange{}, + resultDescription: result.description, + }, nil + } +} diff --git a/examplebot/newbot/rule_respond_to_real_envido.go b/examplebot/newbot/rule_respond_to_real_envido.go new file mode 100644 index 0000000..77cd125 --- /dev/null +++ b/examplebot/newbot/rule_respond_to_real_envido.go @@ -0,0 +1,86 @@ +package newbot + +import ( + "errors" + "fmt" + + "github.com/marianogappa/truco/truco" +) + +var ( + ruleRespondToRealEnvido = rule{ + name: "ruleRespondToRealEnvido", + description: "Responds to a Real Envido action", + isApplicable: ruleRespondToRealEnvidoIsApplicable, + dependsOn: []rule{ruleRespondToEnvido}, + run: ruleRespondToRealEnvidoRun, + } +) + +func init() { + registerRule(ruleRespondToRealEnvido) +} + +// TODO: rule for when saying no quiero makes you lose the game +// TODO: rule for when less actions are possible (e.g. if opponent said truco.SAY_CONTRAFLOR) + +func ruleRespondToRealEnvidoIsApplicable(st state, _ truco.ClientGameState) bool { + return isPossibleAll(st, truco.SAY_ENVIDO_NO_QUIERO, truco.SAY_ENVIDO_QUIERO, truco.SAY_FALTA_ENVIDO) +} + +func ruleRespondToRealEnvidoRun(st state, gs truco.ClientGameState) (ruleResult, error) { + var ( + agg = aggresiveness(st) + envidoScore = envidoScore(st) + pointsToLose = pointsToLose(st) + pointsToWin = pointsToWin(gs) + costOfNoQuiero = getAction(st, truco.SAY_ENVIDO_NO_QUIERO).(*truco.ActionSayEnvidoNoQuiero).Cost + ) + + if costOfNoQuiero >= pointsToLose { + return ruleResult{ + action: getAction(st, truco.SAY_ENVIDO_QUIERO), + stateChanges: []stateChange{}, + resultDescription: fmt.Sprintf("Responded to Real Envido with quiero because no quiero costs %v and I will lose in %v points.", costOfNoQuiero, pointsToLose), + }, nil + } + if pointsToWin == 1 { + return ruleResult{ + action: getAction(st, truco.SAY_FALTA_ENVIDO), + stateChanges: []stateChange{}, + resultDescription: "When there's only 1 point left to win, always respond with Falta Envido.", + }, nil + } + + decisionTree := map[string]map[string][2]int{ + "low": { + truco.SAY_ENVIDO_NO_QUIERO: [2]int{0, 28}, + truco.SAY_ENVIDO_QUIERO: [2]int{29, 30}, + truco.SAY_FALTA_ENVIDO: [2]int{31, 33}, + }, + "normal": { + truco.SAY_ENVIDO_NO_QUIERO: [2]int{0, 27}, + truco.SAY_ENVIDO_QUIERO: [2]int{28, 29}, + truco.SAY_FALTA_ENVIDO: [2]int{30, 33}, + }, + "high": { + truco.SAY_ENVIDO_NO_QUIERO: [2]int{0, 26}, + truco.SAY_ENVIDO_QUIERO: [2]int{27, 28}, + truco.SAY_FALTA_ENVIDO: [2]int{29, 33}, + }, + } + + decisionTreeForAgg := decisionTree[agg] + + for actionName, scoreRange := range decisionTreeForAgg { + if envidoScore >= scoreRange[0] && envidoScore <= scoreRange[1] { + return ruleResult{ + action: getAction(st, actionName), + stateChanges: []stateChange{}, + resultDescription: fmt.Sprintf("Responded to Real Envido with %v given decision tree for %v aggressiveness and envido score of %v.", actionName, agg, envidoScore), + }, nil + } + } + + return ruleResult{}, errors.New("Couldn't find a suitable action to respond to Real Envido.") +} diff --git a/examplebot/newbot/rule_respond_to_truco.go b/examplebot/newbot/rule_respond_to_truco.go new file mode 100644 index 0000000..dacca9d --- /dev/null +++ b/examplebot/newbot/rule_respond_to_truco.go @@ -0,0 +1,53 @@ +package newbot + +import ( + "github.com/marianogappa/truco/truco" +) + +var ( + ruleRespondToTruco = rule{ + name: "ruleRespondToTruco", + description: "Responds to a Truco action", + isApplicable: ruleRespondToTrucoIsApplicable, + dependsOn: []rule{ruleInitState}, + run: ruleRespondToTrucoRun, + } +) + +func init() { + registerRule(ruleRespondToTruco) +} + +func ruleRespondToTrucoIsApplicable(st state, _ truco.ClientGameState) bool { + return isPossibleAll(st, truco.SAY_TRUCO_NO_QUIERO, truco.SAY_TRUCO_QUIERO, truco.SAY_QUIERO_RETRUCO) +} + +func ruleRespondToTrucoRun(st state, gs truco.ClientGameState) (ruleResult, error) { + var ( + pointsToLose = pointsToLose(st) + costOfNoQuiero = getAction(st, truco.SAY_TRUCO_NO_QUIERO).(*truco.ActionSayTrucoNoQuiero).Cost + noQuieroLosesGame = costOfNoQuiero >= pointsToLose + ) + + result := analyzeTruco(st, gs, noQuieroLosesGame) + switch { + case result.shouldRaise: + return ruleResult{ + action: getAction(st, truco.SAY_QUIERO_RETRUCO), + stateChanges: []stateChange{}, + resultDescription: result.description, + }, nil + case result.shouldQuiero: + return ruleResult{ + action: getAction(st, truco.SAY_TRUCO_QUIERO), + stateChanges: []stateChange{}, + resultDescription: result.description, + }, nil + default: + return ruleResult{ + action: getAction(st, truco.SAY_TRUCO_NO_QUIERO), + stateChanges: []stateChange{}, + resultDescription: result.description, + }, nil + } +} diff --git a/examplebot/newbot/rule_reveal_card.go b/examplebot/newbot/rule_reveal_card.go new file mode 100644 index 0000000..65cc6f6 --- /dev/null +++ b/examplebot/newbot/rule_reveal_card.go @@ -0,0 +1,44 @@ +package newbot + +import ( + "github.com/marianogappa/truco/truco" +) + +var ( + ruleRevealCard = rule{ + name: "ruleRevealCard", + description: "Decides whether to reveal a card (or leave)", + isApplicable: ruleRevealCardIsApplicable, + dependsOn: []rule{ruleInitiateTruco}, + run: ruleRevealCardRun, + } +) + +func init() { + registerRule(ruleRevealCard) +} + +func ruleRevealCardIsApplicable(st state, _ truco.ClientGameState) bool { + return isPossibleAll(st, truco.REVEAL_CARD, truco.SAY_ME_VOY_AL_MAZO) +} + +func ruleRevealCardRun(st state, gs truco.ClientGameState) (ruleResult, error) { + result := analyzeTruco(st, gs, false) + + switch { + case result.shouldLeave: + return ruleResult{ + action: getAction(st, truco.SAY_ME_VOY_AL_MAZO), + stateChanges: []stateChange{}, + resultDescription: result.description, + }, nil + default: + act := getAction(st, truco.REVEAL_CARD).(*truco.ActionRevealCard) + act.Card = result.revealCard + return ruleResult{ + action: act, + stateChanges: []stateChange{}, + resultDescription: result.description, + }, nil + } +} diff --git a/examplebot/newbot/rules.go b/examplebot/newbot/rules.go new file mode 100644 index 0000000..84cf797 --- /dev/null +++ b/examplebot/newbot/rules.go @@ -0,0 +1,83 @@ +package newbot + +import ( + "errors" + + "github.com/marianogappa/truco/truco" +) + +var ( + rules []rule + _ruleset = map[string]struct{}{} +) + +// Every rule must be registered in order to be considered by the bot. +func registerRule(r rule) { + if _, ok := _ruleset[r.name]; ok { + panic("rule already registered; is the name unique?") + } + rules = append(rules, r) + _ruleset[r.name] = struct{}{} +} + +type state map[string]any +type stateChange struct { + fn func(*state) + description string +} + +type ruleResult struct { + action truco.Action + stateChanges []stateChange + resultDescription string +} + +type rule struct { + name string + description string + isApplicable func(state, truco.ClientGameState) bool + dependsOn []rule + run func(state, truco.ClientGameState) (ruleResult, error) +} + +func topologicalSortKahn(rules []rule) ([]rule, error) { + nameToRule := map[string]rule{} + for _, r := range rules { + nameToRule[r.name] = r + } + + indegree := map[string]int{} + graph := map[string][]rule{} + for _, r := range rules { + indegree[r.name] = 0 + graph[r.name] = []rule{} + } + for _, r := range rules { + for _, d := range r.dependsOn { + graph[d.name] = append(graph[d.name], r) + indegree[r.name]++ + } + } + queue := []rule{} + for r, d := range indegree { + if d == 0 { + queue = append(queue, nameToRule[r]) + } + } + sorted := []rule{} + for len(queue) > 0 { + r := queue[0] + queue = queue[1:] + sorted = append(sorted, r) + for _, d := range graph[r.name] { + indegree[d.name]-- + if indegree[d.name] == 0 { + queue = append(queue, d) + } + } + } + if len(sorted) != len(rules) { + return nil, errors.New("circular dependency") + } + return sorted, nil +} diff --git a/examplebot/newbot/truco_analysis.go b/examplebot/newbot/truco_analysis.go new file mode 100644 index 0000000..c791c92 --- /dev/null +++ b/examplebot/newbot/truco_analysis.go @@ -0,0 +1,656 @@ +package newbot + +import ( + "fmt" + "strings" + + "math/rand" + + "github.com/marianogappa/truco/truco" +) + +type trucoResult struct { + shouldQuiero bool + shouldInitiate bool + shouldRaise bool + shouldLeave bool + revealCard truco.Card + description string +} + +func t_raise(c truco.Card, description string) trucoResult { + return trucoResult{shouldQuiero: true, shouldInitiate: true, shouldRaise: true, revealCard: c, description: description} +} + +func t_quiero(c truco.Card, description string) trucoResult { + return trucoResult{shouldQuiero: true, shouldInitiate: true, shouldRaise: false, revealCard: c, description: description} +} + +func t_leave(c truco.Card, description string) trucoResult { + return trucoResult{shouldQuiero: false, shouldInitiate: false, shouldRaise: false, shouldLeave: true, revealCard: c, description: description} +} + +func t_noquiero(c truco.Card, description string) trucoResult { + return trucoResult{shouldQuiero: false, shouldInitiate: false, shouldRaise: false, revealCard: c, description: description} +} + +func analyzeTruco(st state, gs truco.ClientGameState, noQuieroLosesGame bool) trucoResult { + result := _analyzeTruco(st, gs) + if noQuieroLosesGame && (!result.shouldQuiero || result.shouldLeave) { + result.shouldQuiero = true + result.shouldLeave = false + result.shouldRaise = true + } + // Exception: bot should never raise (but still accept) if points to win is 1 + if pointsToWin(gs) == 1 { + result.shouldRaise = false + } + return result +} + +func _analyzeTruco(st state, gs truco.ClientGameState) trucoResult { + agg := aggresiveness(st) + faceoffResults := calculateFaceoffResults(gs) + + revealedCardPairs := [2]int{len(gs.YourRevealedCards), len(gs.TheirRevealedCards)} + switch revealedCardPairs { + case [2]int{0, 0}, [2]int{1, 0}: // No cards on table, or only our card on table + var revealCard truco.Card + if len(gs.YourUnrevealedCards) == 1 { + revealCard = gs.YourRevealedCards[1] + } else { + revealCard = randomCardExceptSpecial(gs.YourUnrevealedCards) + } + power := newPower(gs.YourUnrevealedCards) + switch agg { + case "low": + switch { + case power.gte(TWO_GOOD_ONE_MEDIUM): + return t_raise(revealCard, "no cards on table (or mine only), low agg, two good cards and one medium or better, good to raise") + case (power.gte(ONE_GOOD_TWO_MEDIUM) && youMano(gs)) || (power.gte(TWO_GOOD)): + return t_quiero(revealCard, "no cards on table (or mine only), low agg, one good card & too medium as mano (or two good) or better, good to accept") + default: + return t_noquiero(revealCard, "no cards on table (or mine only), low agg, not even two good cards, should not accept") + } + case "normal": + switch { + case power.gte(TWO_GOOD): + return t_raise(revealCard, "no cards on table (or mine only), normal agg, two good cards or better, good to raise") + case power.gte(THREE_MEDIUM): + return t_quiero(revealCard, "no cards on table (or mine only), normal agg, three medium cards or better, good to accept") + default: + return t_noquiero(revealCard, "no cards on table (or mine only), normal agg, not even three medium cards, should not accept") + } + case "high": + switch { + case power.gte(ONE_GOOD_ONE_MEDIUM): + return t_raise(revealCard, "no cards on table (or mine only), high agg, one good card and one medium or better, good to raise") + case power.gte(TWO_MEDIUM): + return t_quiero(revealCard, "no cards on table (or mine only), high agg, two medium cards or better, good to accept") + default: + return t_noquiero(revealCard, "no cards on table (or mine only), high agg, not even two medium cards, should not accept") + } + } + panic("unreachable") + case [2]int{0, 1}: // Only their card on table + canBeat := canBeatCard(gs.TheirRevealedCards[0], gs.YourUnrevealedCards) + canTie := canTieCard(gs.TheirRevealedCards[0], gs.YourUnrevealedCards) + switch { + case canBeat: + revealCard := lowestCardThatBeats(gs.TheirRevealedCards[0], gs.YourUnrevealedCards) + power := newPower(cardsWithout(gs.YourUnrevealedCards, revealCard)) + switch agg { + case "low": + if power.gte(ONE_GOOD) { + return t_raise(revealCard, "only their card on table, can beat it, low agg, one good card or better, good to raise") + } + if power.lte(TWO_BAD) { + return t_noquiero(revealCard, "only their card on table, can beat it, low agg, two bad cards, should not accept") + } + return t_quiero(revealCard, "only their card on table, can beat it, low agg, at least one medium card, should accept") + case "normal": + if power.gte(TWO_MEDIUM) { + return t_raise(revealCard, "only their card on table, can beat it, normal agg, two medium cards or better, good to raise") + } + if power.lte(TWO_BAD) { + return t_noquiero(revealCard, "only their card on table, can beat it, normal agg, two bad cards, should not accept") + } + return t_quiero(revealCard, "only their card on table, can beat it, normal agg, at least one medium card, should accept") + case "high": + if power.gte(TWO_MEDIUM) { + return t_raise(revealCard, "only their card on table, can beat it, high agg, two medium cards or better, good to raise") + } + return t_quiero(revealCard, "only their card on table, can beat it, high agg, less than two medium cards, should accept") + } + panic("unreachable") + case canTie: + revealCard := cardThatTies(gs.TheirRevealedCards[0], gs.YourUnrevealedCards) + power := newPower(cardsWithout(gs.YourUnrevealedCards, revealCard)) + switch agg { + case "low": + if power.gte(ONE_SPECIAL) { + return t_raise(revealCard, "only their card on table, can tie it, low agg, one special card or better, good to raise") + } + if power.lt(ONE_GOOD) { + return t_noquiero(revealCard, "only their card on table, can tie it, low agg, less than one good card, should not accept") + } + return t_quiero(revealCard, "only their card on table, can tie it, low agg, at least one good card, should accept") + + case "normal": + if power.gte(TWO_GOOD) { + return t_raise(revealCard, "only their card on table, can tie it, normal agg, two good cards or better, good to raise") + } + if power.lte(TWO_MEDIUM) { + return t_noquiero(revealCard, "only their card on table, can tie it, normal agg, up to two medium cards, should not accept") + } + return t_quiero(revealCard, "only their card on table, can tie it, normal agg, at least one good card, should accept") + case "high": + if power.gte(TWO_MEDIUM) { + return t_raise(revealCard, "only their card on table, can tie it, high agg, two medium cards or better, good to raise") + } + if power.gte(ONE_MEDIUM) { + return t_quiero(revealCard, "only their card on table, can tie it, high agg, at least one medium card, should accept") + } + return t_noquiero(revealCard, "only their card on table, can tie it, high agg, less than one medium card, should not accept") + } + default: // will lose faceoff + revealCard := lowestOf(gs.YourUnrevealedCards) + power := newPower(cardsWithout(gs.YourUnrevealedCards, revealCard)) + switch agg { + case "low": + if power.gte(ONE_SPECIAL_ONE_GOOD) { + return t_raise(revealCard, "only their card on table, will lose faceoff, low agg, one special and one good card or better, good to raise") + } + if power.lte(TWO_GOOD) { + return t_noquiero(revealCard, "only their card on table, will lose faceoff, low agg, up to two good cards, should not accept") + } + return t_quiero(revealCard, "only their card on table, will lose faceoff, low agg, at least one medium card, should accept") + + case "normal": + if power.gte(TWO_GOOD) { + return t_raise(revealCard, "only their card on table, will lose faceoff, normal agg, two good cards or better, good to raise") + } + if power.lte(ONE_GOOD_ONE_MEDIUM) { + return t_noquiero(revealCard, "only their card on table, will lose faceoff, normal agg, up to one good and one medium card, should not accept") + } + return t_quiero(revealCard, "only their card on table, will lose faceoff, normal agg, at least one good card, should accept") + case "high": + if power.gte(ONE_GOOD_ONE_MEDIUM) { + return t_raise(revealCard, "only their card on table, will lose faceoff, high agg, one good and one medium card or better, good to raise") + } + if power.gte(ONE_GOOD) { + return t_quiero(revealCard, "only their card on table, will lose faceoff, high agg, at least one good card, should accept") + } + return t_noquiero(revealCard, "only their card on table, will lose faceoff, high agg, less than one good card, should not accept") + } + } + panic("unreachable") + case [2]int{1, 1}, [2]int{2, 1}: // One faceoff done + var revealCard truco.Card + if len(gs.YourUnrevealedCards) == 1 { + revealCard = gs.YourRevealedCards[1] + } + switch faceoffResults[0] { + case FACEOFF_WIN: + if revealCard == (truco.Card{}) { + revealCard = lowestOf(gs.YourUnrevealedCards) + } + power := newPower(cardsWithout(gs.YourUnrevealedCards, revealCard)) + switch agg { + case "low": + if power.gte(TWO_MEDIUM) { + return t_raise(revealCard, "won faceoff, low agg, two medium cards or better, good to raise") + } + if power.lt(ONE_MEDIUM) { + return t_noquiero(revealCard, "won faceoff, low agg, less than one medium card, should not accept") + } + return t_quiero(revealCard, "won faceoff, low agg, one medium card, should accept") + + case "normal": + if power.gte(ONE_MEDIUM) { + return t_raise(revealCard, "won faceoff, normal agg, one medium card or better, good to raise") + } + if power.lte(TWO_BAD) { + return t_noquiero(revealCard, "won faceoff, normal agg, two bad cards, should not accept") + } + return t_quiero(revealCard, "won faceoff, normal agg, at least one good card, should accept") + case "high": + if power.gte(ONE_MEDIUM) { + return t_raise(revealCard, "won faceoff, high agg, one medium card or better, good to raise") + } + return t_quiero(revealCard, "won faceoff, high agg, less than one medium card, should accept") + } + case FACEOFF_TIE: + if revealCard == (truco.Card{}) { + revealCard = highestOf(gs.YourUnrevealedCards) + } + power := newPower(cardsWithout(gs.YourUnrevealedCards, revealCard)) + switch agg { + case "low": + if power.gte(ONE_SPECIAL) { + return t_raise(revealCard, "tied faceoff, low agg, one special card or better, good to raise") + } + if power.lt(ONE_GOOD) { + return t_noquiero(revealCard, "tied faceoff, low agg, less than one good card, should not accept") + } + return t_quiero(revealCard, "tied faceoff, low agg, at least one good card, should accept") + + case "normal": + if power.gte(ONE_GOOD_ONE_MEDIUM) { + return t_raise(revealCard, "tied faceoff, normal agg, one good and one medium card or better, good to raise") + } + if power.lte(TWO_MEDIUM) { + return t_noquiero(revealCard, "tied faceoff, normal agg, up to two medium cards, should not accept") + } + return t_quiero(revealCard, "tied faceoff, normal agg, at least one good card, should accept") + case "high": + if power.gte(ONE_MEDIUM) { + return t_raise(revealCard, "tied faceoff, high agg, one medium card or better, good to raise") + } + return t_quiero(revealCard, "tied faceoff, high agg, less than one medium card, should accept") + } + panic("unreachable") + case FACEOFF_LOSS: + if revealCard == (truco.Card{}) { + revealCard = highestOf(gs.YourUnrevealedCards) + } + power := newPower(cardsWithout(gs.YourUnrevealedCards, revealCard)) + switch agg { + case "low": + if power.gte(TWO_SPECIALS) { + return t_raise(revealCard, "lost faceoff, low agg, two special cards or better, good to raise") + } + if power.lt(TWO_GOOD) { + return t_noquiero(revealCard, "lost faceoff, low agg, less than two good cards, should not accept") + } + return t_quiero(revealCard, "lost faceoff, low agg, at least two good cards, should accept") + + case "normal": + if power.gte(ONE_SPECIAL_ONE_GOOD) { + return t_raise(revealCard, "lost faceoff, normal agg, one special and one good card or better, good to raise") + } + if power.lte(ONE_GOOD_ONE_MEDIUM) { + return t_noquiero(revealCard, "lost faceoff, normal agg, up to one good and one medium card, should not accept") + } + return t_quiero(revealCard, "lost faceoff, normal agg, two good cards, should accept") + case "high": + if power.gte(ONE_GOOD) { + return t_raise(revealCard, "lost faceoff, high agg, one good card or better, good to raise") + } + if power.lte(TWO_MEDIUM) { + return t_noquiero(revealCard, "lost faceoff, high agg, up to two medium cards, should not accept") + } + return t_quiero(revealCard, "lost faceoff, high agg, at least one medium card, should accept") + } + } + panic("unreachable") + case [2]int{1, 2}: // One faceoff done, their card on the table + canBeat := canBeatCard(gs.TheirRevealedCards[1], gs.YourUnrevealedCards) + canTie := canTieCard(gs.TheirRevealedCards[1], gs.YourUnrevealedCards) + switch faceoffResults[0] { + case FACEOFF_WIN: + switch { + case canBeat: + revealCard := lowestCardThatBeats(gs.TheirRevealedCards[1], gs.YourUnrevealedCards) + return t_raise(revealCard, "won faceoff, their card on table, can beat it, good to raise") + case canTie: + revealCard := cardThatTies(gs.TheirRevealedCards[1], gs.YourUnrevealedCards) + return t_raise(revealCard, "won faceoff, their card on table, can tie it, good to raise") + default: // can't beat their card + revealCard := lowestOf(gs.YourUnrevealedCards) + power := newPower(cardsWithout(gs.YourUnrevealedCards, revealCard)) + switch agg { + case "low": + if power.gte(ONE_GOOD) { + return t_raise(revealCard, "won faceoff, their card on table, can't beat it, low agg, one good card or better, good to raise") + } + return t_noquiero(revealCard, "won faceoff, their card on table, can't beat it, low agg, less than one good card, should not accept") + case "normal": + if power.gte(ONE_GOOD) { + return t_raise(revealCard, "won faceoff, their card on table, can't beat it, normal agg, one good card or better, good to raise") + } + if power.lte(ONE_BAD) { + return t_noquiero(revealCard, "won faceoff, their card on table, can't beat it, normal agg, up to one bad card, should not accept") + } + return t_quiero(revealCard, "won faceoff, their card on table, can't beat it, normal agg, at least one medium card, should accept") + case "high": + return t_raise(revealCard, "won faceoff, their card on table, can't beat it, high agg, good to raise") + } + panic("unreachable") + } + case FACEOFF_TIE: + switch { + case canBeat: + revealCard := lowestCardThatBeats(gs.TheirRevealedCards[1], gs.YourUnrevealedCards) + return t_raise(revealCard, "tied faceoff, their card on table, can beat it, good to raise") + case canTie: + revealCard := cardThatTies(gs.TheirRevealedCards[1], gs.YourUnrevealedCards) + return t_raise(revealCard, "tied faceoff, their card on table, can tie it, good to raise") + default: // can't beat their card, so about to lose hand + revealCard := lowestOf(gs.YourUnrevealedCards) + return t_leave(revealCard, "tied faceoff, their card on table, can't beat it, should leave") + } + case FACEOFF_LOSS: + switch { + case canBeat: + revealCard := lowestCardThatBeats(gs.TheirRevealedCards[1], gs.YourUnrevealedCards) + power := newPower(cardsWithout(gs.YourUnrevealedCards, revealCard)) + switch agg { + case "low": + if power.gte(ONE_SPECIAL_ONE_GOOD) { + return t_raise(revealCard, "lost faceoff, their card on table, can beat it, low agg, one special and one good card or better, good to raise") + } + if power.lt(TWO_GOOD) { + return t_noquiero(revealCard, "lost faceoff, their card on table, can beat it, low agg, less than two good cards, should not accept") + } + return t_quiero(revealCard, "lost faceoff, their card on table, can beat it, low agg, at least two good cards, should accept") + case "normal": + if power.gte(TWO_GOOD) { + return t_raise(revealCard, "lost faceoff, their card on table, can beat it, normal agg, two good cards or better, good to raise") + } + if power.lt(ONE_GOOD_ONE_MEDIUM) { + return t_noquiero(revealCard, "lost faceoff, their card on table, can beat it, normal agg, less than one good and one medium card, should not accept") + } + return t_quiero(revealCard, "lost faceoff, their card on table, can beat it, normal agg, at least one good card and one medium card, should accept") + case "high": + if power.gte(ONE_GOOD_ONE_MEDIUM) { + return t_raise(revealCard, "lost faceoff, their card on table, can beat it, high agg, one good and one medium card or better, good to raise") + } + if power.lt(ONE_GOOD) { + return t_noquiero(revealCard, "lost faceoff, their card on table, can beat it, high agg, less than one good card, should not accept") + } + return t_quiero(revealCard, "lost faceoff, their card on table, can beat it, high agg, at least one good card, should accept") + } + default: // tie or lose, either way it's a loss + revealCard := lowestOf(gs.YourUnrevealedCards) + return t_leave(revealCard, "lost faceoff, their card on table, can't beat it, should leave") + } + } + panic("unreachable") + case [2]int{2, 2}, [2]int{3, 2}: // Two faceoffs done + // TODO: at this point envido or flor info could tell bot what human has + var revealCard truco.Card + if len(gs.YourUnrevealedCards) == 0 { + revealCard = gs.YourRevealedCards[2] + } else { + revealCard = gs.YourUnrevealedCards[0] + } + power := newPower([]truco.Card{revealCard}) + switch agg { + case "low": + if power.gte(ONE_GOOD) { + return t_raise(revealCard, "two faceoffs done, low agg, one good card or better, good to raise") + } + if power.lt(ONE_MEDIUM) { + return t_noquiero(revealCard, "two faceoffs done, low agg, less than one medium card, should not accept") + } + return t_quiero(revealCard, "two faceoffs done, low agg, at least one medium card, should accept") + case "normal": + if power.gte(ONE_MEDIUM) { + return t_raise(revealCard, "two faceoffs done, normal agg, one medium card or better, good to raise") + } + if power.lte(ONE_BAD) { + return t_noquiero(revealCard, "two faceoffs done, normal agg, one bad card, should not accept") + } + case "high": + return t_quiero(revealCard, "two faceoffs done, high agg, should accept") + } + panic("unreachable") + case [2]int{2, 3}: // Two faceoffs done, their card on the table + canBeat := canBeatCard(gs.TheirRevealedCards[2], gs.YourUnrevealedCards) + canTie := canTieCard(gs.TheirRevealedCards[2], gs.YourUnrevealedCards) + switch { + case canBeat, canTie && youMano(gs): // sure win + return t_raise(gs.YourUnrevealedCards[0], "two faceoffs done, their card on table, can beat it (or tie as mano), good to raise") + default: // sure loss + return t_leave(gs.YourUnrevealedCards[0], "two faceoffs done, their card on table, can't beat it, should leave") + } + default: + panic(fmt.Sprintf("Unexpected number of revealed card pairs: %v", revealedCardPairs)) + } +} + +const ( + POWER_SPECIAL = 4 + POWER_GOOD = 3 + POWER_MEDIUM = 2 + POWER_BAD = 1 +) + +var ( + TWO_SPECIALS = newPower([]truco.Card{{Suit: truco.ESPADA, Number: 1}, {Suit: truco.BASTO, Number: 1}}, "two_specials cards") + ONE_SPECIAL_ONE_GOOD = newPower([]truco.Card{{Suit: truco.ESPADA, Number: 1}, {Suit: truco.ORO, Number: 3}}, "one special card, one good card") + TWO_GOOD = newPower([]truco.Card{{Suit: truco.ORO, Number: 3}, {Suit: truco.COPA, Number: 3}}, "two good cards") + TWO_GOOD_ONE_MEDIUM = newPower([]truco.Card{{Suit: truco.ORO, Number: 3}, {Suit: truco.COPA, Number: 3}, {Suit: truco.BASTO, Number: 11}}, "two good cards, one medium card") + ONE_GOOD_TWO_MEDIUM = newPower([]truco.Card{{Suit: truco.ORO, Number: 3}, {Suit: truco.BASTO, Number: 11}, {Suit: truco.COPA, Number: 11}}, "one good card, two medium cards") + ONE_GOOD_ONE_MEDIUM = newPower([]truco.Card{{Suit: truco.ORO, Number: 3}, {Suit: truco.BASTO, Number: 11}}, "one good card, one medium card") + THREE_MEDIUM = newPower([]truco.Card{{Suit: truco.BASTO, Number: 11}, {Suit: truco.COPA, Number: 11}, {Suit: truco.ORO, Number: 11}}, "three medium cards") + TWO_MEDIUM = newPower([]truco.Card{{Suit: truco.BASTO, Number: 11}, {Suit: truco.COPA, Number: 11}}, "two medium cards") + TWO_BAD = newPower([]truco.Card{{Suit: truco.ORO, Number: 4}, {Suit: truco.COPA, Number: 4}}, "two bad cards") + ONE_GOOD = newPower([]truco.Card{{Suit: truco.ORO, Number: 3}}, "one good card") + ONE_SPECIAL = newPower([]truco.Card{{Suit: truco.ESPADA, Number: 1}}, "one special card") + ONE_MEDIUM = newPower([]truco.Card{{Suit: truco.BASTO, Number: 11}}, "one medium card") + ONE_BAD = newPower([]truco.Card{{Suit: truco.ORO, Number: 4}}, "one bad card") +) + +type trucoPower struct { + power map[int]int + totalPower int + count int + description string +} + +func newPower(cards []truco.Card, description ...string) trucoPower { + return trucoPower{ + power: cardsToPowers(cards), + totalPower: _sumPowers(cardsToPowers(cards)), + count: len(cards), + description: strings.Join(description, " "), + } +} + +func (tp trucoPower) _cmp(tp2 trucoPower) int { + // If the count is a mismatch, fill the smaller one with BADs (i.e. +1) + fixTP, fixTP2 := 0, 0 + if tp.count > tp2.count { + fixTP2 = tp.count - tp2.count + } else if tp.count < tp2.count { + fixTP = tp2.count - tp.count + } + + if tp.totalPower+fixTP > tp2.totalPower+fixTP2 { + return 1 + } + if tp.totalPower+fixTP < tp2.totalPower+fixTP2 { + return -1 + } + return 0 +} + +func (tp trucoPower) lt(tp2 trucoPower) bool { + return tp._cmp(tp2) == -1 +} + +func (tp trucoPower) gte(tp2 trucoPower) bool { + return tp._cmp(tp2) >= 0 +} + +func (tp trucoPower) lte(tp2 trucoPower) bool { + return tp._cmp(tp2) <= 0 +} + +func _sumPowers(powers map[int]int) int { + sum := 0 + for power, count := range powers { + sum += power * count + } + return sum +} + +func cardsToPowers(cards []truco.Card) map[int]int { + powers := map[int]int{} + for _, c := range cards { + powers[cardToPower(c)]++ + } + return powers +} + +func cardToPower(c truco.Card) int { + if isCardSpecial(c) { + return POWER_SPECIAL + } + if c.Number == 3 || c.Number == 2 { + return POWER_GOOD + } + if c.Number == 1 || c.Number == 12 || c.Number == 11 { + return POWER_MEDIUM + } + return POWER_BAD +} + +// func calculateCardStrength(gs truco.Card) int { +// specialValues := map[truco.Card]int{ +// {Suit: truco.ESPADA, Number: 1}: 15, +// {Suit: truco.BASTO, Number: 1}: 14, +// {Suit: truco.ESPADA, Number: 7}: 13, +// {Suit: truco.ORO, Number: 7}: 12, +// } +// if _, ok := specialValues[gs]; ok { +// return specialValues[gs] +// } +// if gs.Number <= 3 { +// return gs.Number + 12 - 4 +// } +// return gs.Number - 4 +// } + +// func sortCardsByValue(cards []truco.Card) []truco.Card { +// sort.Slice(cards, func(i, j int) bool { +// return calculateCardStrength(cards[i]) > calculateCardStrength(cards[j]) +// }) +// return cards +// } + +func isCardSpecial(card truco.Card) bool { + return card == (truco.Card{Suit: truco.ESPADA, Number: 1}) || card == (truco.Card{Suit: truco.BASTO, Number: 1}) || card == (truco.Card{Suit: truco.ESPADA, Number: 7}) || card == (truco.Card{Suit: truco.ORO, Number: 7}) +} + +func forAllCards(cards []truco.Card, f func(truco.Card) bool) bool { + for _, c := range cards { + if !f(c) { + return false + } + } + return true +} + +func randomCardExceptSpecial(cards []truco.Card) truco.Card { + if forAllCards(cards, isCardSpecial) { + return cards[rand.Intn(len(cards))] + } + for { + card := cards[rand.Intn(len(cards))] + if !isCardSpecial(card) { + return card + } + } +} + +// func randomCard(cards []truco.Card) truco.Card { +// return cards[rand.Intn(len(cards))] +// } + +func canBeatCard(card truco.Card, cards []truco.Card) bool { + for _, c := range cards { + if c.CompareTrucoScore(card) == 1 { + return true + } + } + return false +} + +func canTieCard(card truco.Card, cards []truco.Card) bool { + for _, c := range cards { + if c.CompareTrucoScore(card) == 0 { + return true + } + } + return false +} + +// func cardsWithoutLowest(cards []truco.Card) []truco.Card { +// lowest := cards[0] +// for _, card := range cards { +// if card.CompareTrucoScore(lowest) == -1 { +// lowest = card +// } +// } + +// unrevealed := []truco.Card{} +// for _, card := range cards { +// if card != lowest { +// unrevealed = append(unrevealed, card) +// } +// } +// return unrevealed +// } + +func lowestOf(cards []truco.Card) truco.Card { + lowest := cards[0] + for _, card := range cards { + if card.CompareTrucoScore(lowest) == -1 { + lowest = card + } + } + return lowest +} + +func highestOf(cards []truco.Card) truco.Card { + highest := cards[0] + for _, card := range cards { + if card.CompareTrucoScore(highest) == 1 { + highest = card + } + } + return highest +} + +func cardsWithout(cards []truco.Card, without truco.Card) []truco.Card { + filtered := []truco.Card{} + for _, card := range cards { + if card != without { + filtered = append(filtered, card) + } + } + return filtered +} + +// func cardsWithoutLowestCardThatBeats(card truco.Card, cards []truco.Card) []truco.Card { +// return cardsWithout(cards, lowestCardThatBeats(card, cards)) +// } + +// func cardsWithoutCardThatTies(card truco.Card, cards []truco.Card) []truco.Card { +// return cardsWithout(cards, cardThatTies(card, cards)) +// } + +func cardThatTies(card truco.Card, cards []truco.Card) truco.Card { + for _, c := range cards { + if c.CompareTrucoScore(card) == 0 { + return c + } + } + return truco.Card{} // This should be unreachable +} + +func lowestCardThatBeats(card truco.Card, cards []truco.Card) truco.Card { + cardsThatBeatCard := []truco.Card{} + for _, c := range cards { + if c.CompareTrucoScore(card) == 1 { + cardsThatBeatCard = append(cardsThatBeatCard, c) + } + } + if len(cardsThatBeatCard) == 0 { + return truco.Card{} + } + return lowestOf(cardsThatBeatCard) +} diff --git a/examplebot/newbot/utils.go b/examplebot/newbot/utils.go new file mode 100644 index 0000000..052eb51 --- /dev/null +++ b/examplebot/newbot/utils.go @@ -0,0 +1,54 @@ +package newbot + +import ( + "encoding/json" + + "github.com/marianogappa/truco/truco" +) + +func _deserializeActions(as []json.RawMessage) []truco.Action { + _as := []truco.Action{} + for _, a := range as { + _a, _ := truco.DeserializeAction(a) + _as = append(_as, _a) + } + return _as +} + +func isPossibleAll(st state, actionNames ...string) bool { + for _, actionName := range actionNames { + if _, ok := st["possibleActionNameSet"].(map[string]struct{})[actionName]; !ok { + return false + } + } + return true +} + +// func isPossibleAny(st state, actionNames ...string) bool { +// for _, actionName := range actionNames { +// if _, ok := st["possibleActionNameSet"].(map[string]struct{})[actionName]; ok { +// return true +// } +// } +// return false +// } + +func getAction(st state, actionName string) truco.Action { + return st["possibleActions"].(map[string]truco.Action)[actionName] +} + +func aggresiveness(st state) string { + return st["aggresiveness"].(string) +} + +func florScore(st state) int { + return st["florScore"].(int) +} + +func envidoScore(st state) int { + return st["envidoScore"].(int) +} + +func pointsToLose(st state) int { + return st["pointsToLose"].(int) +} diff --git a/main.go b/main.go index ac4ec9a..3e86f1a 100644 --- a/main.go +++ b/main.go @@ -9,7 +9,7 @@ import ( "strconv" "github.com/marianogappa/truco/botclient" - "github.com/marianogappa/truco/examplebot" + "github.com/marianogappa/truco/examplebot/newbot" "github.com/marianogappa/truco/exampleclient" "github.com/marianogappa/truco/server" ) @@ -48,7 +48,7 @@ func main() { case "player": exampleclient.Player(playerNum-1, address) case "bot": - botclient.Bot(playerNum-1, address, examplebot.New()) + botclient.Bot(playerNum-1, address, newbot.New(newbot.WithDefaultLogger)) default: fmt.Println("Invalid argument. Please provide either server or client.") } diff --git a/main_wasm.go b/main_wasm.go index 1cad258..993e1db 100644 --- a/main_wasm.go +++ b/main_wasm.go @@ -8,7 +8,7 @@ import ( "fmt" "syscall/js" - "github.com/marianogappa/truco/examplebot" + "github.com/marianogappa/truco/examplebot/newbot" "github.com/marianogappa/truco/truco" ) @@ -45,7 +45,7 @@ func trucoNew(this js.Value, p []js.Value) interface{} { } state = truco.New(opts...) - bot = examplebot.New() + bot = (newbot.New(newbot.WithDefaultLogger)) nbs, err := json.Marshal(state.ToClientGameState(0)) if err != nil { diff --git a/truco/action_any_quiero.go b/truco/action_any_quiero.go index 02f16d5..45117e8 100644 --- a/truco/action_any_quiero.go +++ b/truco/action_any_quiero.go @@ -210,5 +210,5 @@ func _doesTrucoActionRequireReminder(g GameState) bool { lastAction := _deserializeCurrentRoundLastAction(g) // If the last action wasn't a truco action, then an envido sequence // 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()) + return !slices.Contains([]string{SAY_TRUCO, SAY_QUIERO_RETRUCO, SAY_QUIERO_VALE_CUATRO}, lastAction.GetName()) } diff --git a/truco/actions_any_flor.go b/truco/actions_any_flor.go index a3d4968..0e9c9bf 100644 --- a/truco/actions_any_flor.go +++ b/truco/actions_any_flor.go @@ -365,12 +365,14 @@ func (a ActionSayFlor) YieldsTurn(g GameState) bool { 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" + // If opponent just said "envido", answering "flor" should also yield turn actionsOpponent := _deserializeCurrentRoundActionsByPlayerID(g.OpponentOf(a.PlayerID), g) - if len(actionsOpponent) > 0 && actionsOpponent[len(actionsOpponent)-1].GetName() == SAY_TRUCO { - return false + if len(actionsOpponent) > 0 && actionsOpponent[len(actionsOpponent)-1].GetName() == SAY_ENVIDO { + return true } - return g.RoundTurnPlayerID != a.PlayerID + // Otherwise, don't yield, since "flor" is just a declaration, and current player continues + // by revealing a card, saying "truco" or saying "me voy al mazo". + return false } func (a ActionSayFlorSonBuenas) YieldsTurn(g GameState) bool { diff --git a/truco/actions_any_flor_test.go b/truco/actions_any_flor_test.go index 11d0278..0f9e742 100644 --- a/truco/actions_any_flor_test.go +++ b/truco/actions_any_flor_test.go @@ -174,6 +174,22 @@ func TestFlor(t *testing.T) { }, }, }, + { + name: "mano reveals card, opponent says flor, turn should stay with opponent", + 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: NewActionRevealCard(Card{Number: 1, Suit: COPA}, 0), + }, + { + action: NewActionSayFlor(1), + expectedPlayerTurnAfterRunning: _p(1), + }, + }, + }, } for _, tt := range tests { diff --git a/truco/actions_any_truco_test.go b/truco/actions_any_truco_test.go new file mode 100644 index 0000000..7b47011 --- /dev/null +++ b/truco/actions_any_truco_test.go @@ -0,0 +1,108 @@ +package truco + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestTrucoRequiresReminder(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: "truco, then flor, so the truco_quiero should have requires_reminder = true", + steps: []testStep{ + { + action: NewActionSayTruco(0), + }, + { + action: NewActionSayFlor(1), + expectedPlayerTurnAfterRunning: _p(1), + }, + { + expectedCustomValidationBeforeAction: func(g *GameState) { + actions := g.CalculatePossibleActions() + found := false + for _, a := range actions { + if a.GetName() == SAY_TRUCO_QUIERO { + found = true + assert.True(t, a.(*ActionSayTrucoQuiero).RequiresReminder, "expected say_truco_quiero to have requires_reminder = true") + } + } + assert.True(t, found, "expected to find truco_quiero with requires_reminder = true") + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // if tt.name != "if first player says flor and opponent does'nt have flor, actions are different" { + // t.Skip() + // } + defaultHands := []Hand{ + {Unrevealed: []Card{{Number: 1, Suit: 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 + } + 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/envido_sequence.go b/truco/envido_sequence.go index 9f08904..a339936 100644 --- a/truco/envido_sequence.go +++ b/truco/envido_sequence.go @@ -183,16 +183,24 @@ func (es EnvidoSequence) WithStep(step string) (EnvidoSequence, error) { return *newEs, nil } +func _calculateFaltaEnvidoCost15PointsStrategy(maxPoints, winnerScore, loserScore int) int { + return maxPoints - max(winnerScore, loserScore) +} + +func _calculateFaltaEnvidoCost30PointsStrategy(maxPoints, winnerScore, loserScore int) int { + if winnerScore < 15 && loserScore < 15 { + return 15 - winnerScore + } + return maxPoints - max(winnerScore, loserScore) +} + 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 - winnerScore - } - if winnerScore < 15 && loserScore < 15 { - return 15 - winnerScore + if maxPoints <= 15 { + return _calculateFaltaEnvidoCost15PointsStrategy(maxPoints, winnerScore, loserScore) } - return 30 - max(winnerScore, loserScore) + return _calculateFaltaEnvidoCost30PointsStrategy(maxPoints, winnerScore, loserScore) } var (