diff options
| author | Florian Fischer <florian.fischer@muhq.space> | 2025-07-19 18:39:59 -0400 |
|---|---|---|
| committer | Florian Fischer <florian.fischer@muhq.space> | 2025-08-20 15:57:25 +0200 |
| commit | 0be3f6cb2373a71e301bb693fa947d7fa258fba0 (patch) | |
| tree | 871d5ecc64e0d5e09047737ed1e9e3389eeaee2a | |
| parent | 5af1145d2cfe50fcd61098f3bbc853ef06cbf04e (diff) | |
| download | muhqs-game-0be3f6cb2373a71e301bb693fa947d7fa258fba0.tar.gz muhqs-game-0be3f6cb2373a71e301bb693fa947d7fa258fba0.zip | |
add simple ai implementation
| -rw-r--r-- | go/game/ai.go | 148 | ||||
| -rw-r--r-- | go/game/ai_test.go | 31 | ||||
| -rw-r--r-- | go/game/pileOfCards.go | 11 | ||||
| -rw-r--r-- | go/game/pileOfCards_test.go | 11 |
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") + } + } +} |
