-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #14 from marianogappa/improve-bot-logic
Improve bot logic
- Loading branch information
Showing
28 changed files
with
2,020 additions
and
16 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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!") | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.