aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--go/game/ai.go148
-rw-r--r--go/game/ai_test.go31
-rw-r--r--go/game/pileOfCards.go11
-rw-r--r--go/game/pileOfCards_test.go11
4 files changed, 199 insertions, 2 deletions
diff --git a/go/game/ai.go b/go/game/ai.go
index 4860a13d..bd5a5e86 100644
--- a/go/game/ai.go
+++ b/go/game/ai.go
@@ -59,6 +59,10 @@ func NewUnitAI(s *LocalState, u *Unit) *UnitAI {
return nil
}
+ return NewUnitAIFromDesc(s, u, aiDesc)
+}
+
+func NewUnitAIFromDesc(s *LocalState, u *Unit, aiDesc string) *UnitAI {
c := make(chan Action)
wg := new(sync.WaitGroup)
wg.Add(1)
@@ -224,7 +228,7 @@ func findPathsToUnits(graph *dijkstra.Graph, m *Map, u *Unit, units []*Unit) []*
return findPathsToPermanents(graph, m, u, perms)
}
-func findPathsToInterface(graph *dijkstra.Graph, m *Map, u *Unit, options []interface{}) []*dijkstra.BestPath {
+func findPathsToInterface(graph *dijkstra.Graph, m *Map, u *Unit, options []any) []*dijkstra.BestPath {
paths := []*dijkstra.BestPath{}
for _, option := range options {
var bestPath *dijkstra.BestPath
@@ -347,7 +351,7 @@ func moveTowardsNearestEnemyUnit(ai *UnitAI) Action {
return actionFromPaths(ai, graph, paths)
}
-func moveTowardsNearestTargetOption(ai *UnitAI, options []interface{}) Action {
+func moveTowardsNearestTargetOption(ai *UnitAI, options []any) Action {
if ai.u.AvailMoveActions == 0 {
return nil
}
@@ -522,3 +526,143 @@ func TargetOrientedAI(ai *UnitAI) {
WanderingAI(ai, 3)
}
}
+
+// SuggestUnitAI returns a suggested UnitAI variant for the specified card.
+// If there is no special case for a unit, a rough heuristic is used to determinw the AI.
+// Units with full actions are shy and everything else is aggresive.
+func SuggestUnitAI(unit *Unit) string {
+ log.Println(unit.card.Name)
+ switch unit.card.Name {
+ case "King":
+ return "shy"
+ case "Farmer":
+ return "target-oriented farm tile"
+ default:
+ if unit.HasFullAction() {
+ return "shy"
+ } else {
+ return "aggressive"
+ }
+ }
+}
+
+// SimpleAiControl implements a simple state-based AI.
+type SimpleAiControl struct {
+ p *Player
+ last PlayerNotification
+ uAis map[*Unit]*UnitAI
+}
+
+func NewSimpleAiControl(p *Player) *SimpleAiControl {
+ aiCtrl := &SimpleAiControl{
+ p: p,
+ uAis: make(map[*Unit]*UnitAI),
+ }
+ return aiCtrl
+}
+
+func (ai *SimpleAiControl) Player() *Player {
+ return ai.p
+}
+
+// RecvAction implements the actual AI's logic.
+func (ai *SimpleAiControl) RecvAction() (Action, error) {
+ switch ai.last.Notification {
+ case PriorityNotification:
+ return ai.handlePriority(), nil
+ case TargetSelectionPrompt:
+ return ai.handleTargetSelection(), nil
+ }
+ return NewPassPriority(ai.p), nil
+}
+
+func (ai *SimpleAiControl) handlePriority() Action {
+ s := ai.p.gameState
+ active := s.ActivePlayer() == ai.p
+
+ if !active {
+ spells := ai.p.Hand.FilterCards(func(c *Card) bool { return c.Type == CardTypes.Spell })
+ // s.FilterPermanents(func(p Permanent) { return p.HasFreeAction()
+ if len(spells) != 0 {
+ }
+
+ freeActions := []Action{}
+ if len(freeActions) != 0 {
+ }
+ } else {
+ switch s.ActivePhase() {
+ case Phases.UpkeepPhase:
+ // Select units to disband
+ case Phases.ActionPhase:
+ // Slow Actions
+ if s.stack.IsEmpty() {
+ for _, u := range s.OwnUnits(ai.p) {
+ if len(u.AvailSlowActions()) == 0 {
+ continue
+ }
+
+ var uAi *UnitAI
+ var ok bool
+ if uAi, ok = ai.uAis[u]; !ok {
+ uAi = NewUnitAI(s, u)
+ ai.uAis[u] = uAi
+ }
+ uAi.promptAction()
+ return uAi.NextAction()
+ }
+ }
+ case Phases.BuyPhase:
+ // TODO: support buying stuff
+ case Phases.DiscardStep:
+ // TODO: support "smarter" discarding
+ t := ai.last.Context.(*Targets)
+ for _, c := range ai.p.Hand.Cards() {
+ t.AddSelection(c)
+ }
+ return newTargetSelection(ai.p, t)
+ }
+ }
+ return NewPassPriority(ai.p)
+}
+
+func (ai *SimpleAiControl) handleTargetSelection() Action {
+ s := ai.p.gameState
+ ctx := ai.last.Context.(TargetSelectionCtx)
+ t := ctx.Action.Targets()
+ switch s.ActivePhase() {
+ case Phases.UpkeepPhase:
+ // Select units to disband
+ case Phases.ActionPhase:
+ case Phases.BuyPhase:
+ // TODO: support buying stuff
+ case Phases.DiscardStep:
+ // TODO: support "smarter" discarding
+ for _, c := range ai.p.Hand.Cards() {
+ t.AddSelection(c)
+ }
+ return ctx.Action
+ }
+
+ err := selectRandomTargets(s.Rand, t)
+ if err != nil {
+ return nil
+ }
+ return ctx.Action
+}
+
+func (*SimpleAiControl) SendAction(Action) error {
+ return nil
+}
+
+// SendNotification simply stores the last notification sent by the game.
+func (ai *SimpleAiControl) SendNotification(n PlayerNotification) error {
+ ai.last = n
+ return nil
+}
+
+func (ai *SimpleAiControl) RecvNotification() (n PlayerNotification, err error) {
+ return
+}
+
+func (ai *SimpleAiControl) Close() {
+}
diff --git a/go/game/ai_test.go b/go/game/ai_test.go
index 08cef2cb..632c6991 100644
--- a/go/game/ai_test.go
+++ b/go/game/ai_test.go
@@ -168,3 +168,34 @@ symbols:
t.Fatal("Target is not a Tile")
}
}
+
+func TestSuggestUnitAi(t *testing.T) {
+ mapDef := `map: |1-
+ HST
+ HSF
+ TST
+symbols:
+ T: tower
+ H: house
+ F: farm
+ S: street
+`
+ r := strings.NewReader(mapDef)
+ m, _ := readMap(r)
+ a := NewUnit(NewCard("base/archer"), m.TileAt(Position{0, 0}), nil)
+ if SuggestUnitAI(a) != "aggressive" {
+ t.Fatal("expected aggressive")
+ }
+
+ f := NewUnit(NewCard("misc/farmer"), m.TileAt(Position{2, 2}), nil)
+ exp := "target-oriented farm tile"
+ is := SuggestUnitAI(f)
+ if exp != is {
+ t.Fatal(exp, " != ", is)
+ }
+
+ tc := NewUnit(NewCard("base/tax_collector"), m.TileAt(Position{3, 3}), nil)
+ if SuggestUnitAI(tc) != "shy" {
+ t.Fatal("expected shy")
+ }
+}
diff --git a/go/game/pileOfCards.go b/go/game/pileOfCards.go
index 27ecb09d..0be45c6b 100644
--- a/go/game/pileOfCards.go
+++ b/go/game/pileOfCards.go
@@ -14,6 +14,7 @@ type PileOfCards interface {
IsEmpty() bool
Contains(*Card) bool
Cards() []*Card
+ FilterCards(func(*Card) bool) []*Card
AddCards(cards []*Card)
AddCard(card *Card)
RemoveCard(card *Card)
@@ -78,6 +79,16 @@ func (poc *PileOfCardsBase) Cards() []*Card {
return poc.cards
}
+func (poc *PileOfCardsBase) FilterCards(f func(*Card) bool) []*Card {
+ cards := []*Card{}
+ for _, c := range poc.Cards() {
+ if f(c) {
+ cards = append(cards, c)
+ }
+ }
+ return cards
+}
+
func (poc *PileOfCardsBase) AddPoc(toAdd PileOfCards) {
poc.AddCards(toAdd.Cards())
}
diff --git a/go/game/pileOfCards_test.go b/go/game/pileOfCards_test.go
index 45203953..55f68c7d 100644
--- a/go/game/pileOfCards_test.go
+++ b/go/game/pileOfCards_test.go
@@ -103,3 +103,14 @@ func TestPocToList(t *testing.T) {
t.Fatal("is != exp:", is, exp)
}
}
+
+
+func TestPocFilterCards(t *testing.T) {
+ base := NewDeckFromCardPaths(Sets.Base.CardPaths())
+ units := base.FilterCards(func(c *Card) bool { return c.Type == CardTypes.Unit })
+ for _, u := range units {
+ if u.Type != CardTypes.Unit {
+ t.Fatal(u, " is not unit")
+ }
+ }
+}