Skip to content

Commit

Permalink
Merge pull request #14 from marianogappa/improve-bot-logic
Browse files Browse the repository at this point in the history
Improve bot logic
  • Loading branch information
marianogappa authored Aug 7, 2024
2 parents a35e324 + 10426df commit 660c948
Show file tree
Hide file tree
Showing 28 changed files with 2,020 additions and 16 deletions.
68 changes: 68 additions & 0 deletions examplebot/newbot/bot.go
Original file line number Diff line number Diff line change
@@ -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!")
}
11 changes: 11 additions & 0 deletions examplebot/newbot/loggers.go
Original file line number Diff line number Diff line change
@@ -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) {}
157 changes: 157 additions & 0 deletions examplebot/newbot/rule_init_state.go
Original file line number Diff line number Diff line change
@@ -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
}
88 changes: 88 additions & 0 deletions examplebot/newbot/rule_initiate_envido.go
Original file line number Diff line number Diff line change
@@ -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
}
31 changes: 31 additions & 0 deletions examplebot/newbot/rule_initiate_flor.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit 660c948

Please sign in to comment.