diff options
| author | Florian Fischer <florian.fischer@muhq.space> | 2024-12-27 16:51:24 +0100 |
|---|---|---|
| committer | Florian Fischer <florian.fischer@muhq.space> | 2025-08-20 15:57:13 +0200 |
| commit | d29bdcd1eca581af24b236a4529e7d67956aedb8 (patch) | |
| tree | d28ae93b3a41f8a90045ba4def491ce19a8fcbc9 | |
| parent | 5779c8439848a1d42204b30f6bb3c422ca7c6b86 (diff) | |
| download | muhqs-game-d29bdcd1eca581af24b236a4529e7d67956aedb8.tar.gz muhqs-game-d29bdcd1eca581af24b236a4529e7d67956aedb8.zip | |
finish the target-oriented ai
Fix path selection choosing a to big step, since the next step
was determined by the moving range and not the actually available
movement tiles.
Fix generating a target for the AI by introducing a dummy action
to support "enemy" target constraints.
| -rw-r--r-- | go/TODO | 1 | ||||
| -rw-r--r-- | go/game/ai.go | 144 | ||||
| -rw-r--r-- | go/game/ai_test.go | 50 | ||||
| -rw-r--r-- | go/game/state.go | 5 | ||||
| -rw-r--r-- | go/game/targets.go | 6 |
5 files changed, 167 insertions, 39 deletions
@@ -9,4 +9,3 @@ * implement game log * finish AIs * shy ai - * target-oriented diff --git a/go/game/ai.go b/go/game/ai.go index 84b5973e..ec535182 100644 --- a/go/game/ai.go +++ b/go/game/ai.go @@ -1,6 +1,7 @@ package game import ( + "errors" "fmt" "log" "math/rand" @@ -8,6 +9,8 @@ import ( "strings" "sync" + "golang.org/x/exp/slices" + "github.com/RyanCarrier/dijkstra" ) @@ -81,7 +84,7 @@ func NewUnitAI(s *LocalState, u *Unit) *UnitAI { case "target-oriented": aiDesc := u.Card().getCanonicalValues("ai")[0] targetDesc := aiDesc[len("target-oriented")+1:] - ai.target = newTarget(s, newTargetDesc(targetDesc), nil) + ai.target = newUnitAiTarget(s, newTargetDesc(targetDesc), ai) go ai.Execute(TargetOrientedAI) } @@ -174,24 +177,69 @@ func findPathTo(graph *dijkstra.Graph, u *Unit, pos Position) (dijkstra.BestPath return graph.Shortest(srcId, destId) } -func findPathsToUnits(graph *dijkstra.Graph, m *Map, u *Unit, units []*Unit) []*dijkstra.BestPath { - paths := []*dijkstra.BestPath{} +func findPathToPermanent(graph *dijkstra.Graph, m *Map, u *Unit, target Permanent) (*dijkstra.BestPath, error) { + if target.Tile() == nil { + return nil, errors.New("no path found") + } - for _, unit := range units { - if unit.tile == nil { + var bestPathToUnit *dijkstra.BestPath = nil + for _, adjacentTile := range TilesInRangeFromOrigin(m, target.Tile().Position, 1) { + if !u.IsAvailableTile(adjacentTile) { continue } + path, err := findPathTo(graph, u, adjacentTile.Position) + if err != nil { + continue + } else if bestPathToUnit == nil || path.Distance < bestPathToUnit.Distance { + bestPathToUnit = &path + } + } + + if bestPathToUnit != nil { + return bestPathToUnit, nil + } else { + return nil, errors.New("no path found") + } +} + +func findPathsToPermanents(graph *dijkstra.Graph, m *Map, u *Unit, permanents []Permanent) []*dijkstra.BestPath { + paths := []*dijkstra.BestPath{} + + for _, perm := range permanents { + if bestPath, err := findPathToPermanent(graph, m, u, perm); err == nil { + paths = append(paths, bestPath) + } + } + return paths +} - var bestPathToUnit *dijkstra.BestPath = nil - for _, adjacentTile := range TilesInRangeFromOrigin(m, unit.Tile().Position, 1) { - path, err := findPathTo(graph, u, adjacentTile.Position) - if err != nil { - continue - } else if bestPathToUnit == nil || path.Distance < bestPathToUnit.Distance { - bestPathToUnit = &path +func findPathsToUnits(graph *dijkstra.Graph, m *Map, u *Unit, units []*Unit) []*dijkstra.BestPath { + perms := []Permanent{} + for _, unit := range units { + perms = append(perms, unit) + } + + return findPathsToPermanents(graph, m, u, perms) +} + +func findPathsToInterface(graph *dijkstra.Graph, m *Map, u *Unit, options []interface{}) []*dijkstra.BestPath { + paths := []*dijkstra.BestPath{} + for _, option := range options { + var bestPath *dijkstra.BestPath + switch o := option.(type) { + case Permanent: + bestPath, _ = findPathToPermanent(graph, m, u, o) + case *Tile: + if bestPathToTile, err := findPathTo(graph, u, o.Position); err != nil { + bestPath = &bestPathToTile } + default: + log.Fatalf("path finding towards %t not implemented: %s", o, o) + } + + if bestPath != nil { + paths = append(paths, bestPath) } - paths = append(paths, bestPathToUnit) } return paths } @@ -210,18 +258,7 @@ func moveToRandomTile(ai *UnitAI) Action { return a } -func moveTowardsNearestEnemyUnit(ai *UnitAI) Action { - if ai.u.AvailMoveActions == 0 { - return nil - } - - graph := ai.s.Map().generateMapGraphFor(ai.u) - enemyUnits := ai.s.EnemyUnits(ai.u.Controller()) - if len(enemyUnits) == 0 { - return nil - } - - paths := findPathsToUnits(graph, ai.s.Map(), ai.u, enemyUnits) +func actionFromPaths(ai *UnitAI, graph *dijkstra.Graph, paths []*dijkstra.BestPath) Action { nearest := 0 for i, p := range paths { if p == nil { @@ -237,13 +274,14 @@ func moveTowardsNearestEnemyUnit(ai *UnitAI) Action { return nil } - var target Position + m := ai.s.Map() + var target *Tile for _, id := range paths[nearest].Path[1:] { mapping, _ := graph.GetMapped(id) var x, y int fmt.Sscanf(mapping, "(%d, %d)", &x, &y) - nextStep := Position{X: x, Y: y} - if IsPositionInRange(ai.u.Tile().Position, nextStep, ai.u.Movement.Range) { + nextStep := m.TileAt(Position{X: x, Y: y}) + if slices.Contains(ai.u.MoveRangeTiles(m), nextStep) { target = nextStep } else { break @@ -251,10 +289,36 @@ func moveTowardsNearestEnemyUnit(ai *UnitAI) Action { } a := NewMoveAction(ai.u) - _ = a.Target().AddSelection(ai.s.Map().TileAt(target)) + _ = a.Target().AddSelection(target) return a } +func moveTowardsNearestEnemyUnit(ai *UnitAI) Action { + if ai.u.AvailMoveActions == 0 { + return nil + } + + graph := ai.s.Map().generateMapGraphFor(ai.u) + enemyUnits := ai.s.EnemyUnits(ai.u.Controller()) + if len(enemyUnits) == 0 { + return nil + } + + paths := findPathsToUnits(graph, ai.s.Map(), ai.u, enemyUnits) + return actionFromPaths(ai, graph, paths) +} + +func moveTowardsNearestTargetOption(ai *UnitAI, options []interface{}) Action { + if ai.u.AvailMoveActions == 0 { + return nil + } + + graph := ai.s.Map().generateMapGraphFor(ai.u) + + paths := findPathsToInterface(graph, ai.s.Map(), ai.u, options) + return actionFromPaths(ai, graph, paths) +} + func attackAttackableEnemyPerm(ai *UnitAI) Action { if ai.u.AvailAttackActions == 0 { return nil @@ -366,7 +430,10 @@ func WanderingAI(ai *UnitAI, x int) { // 2. if ai.u.AvailMoveActions > 0 { - moveToRandomTile(ai) + a := moveToRandomTile(ai) + if !ai.PostAndContinue(a) { + return + } WanderingAI(ai, x) } } @@ -389,13 +456,22 @@ func TargetOrientedAI(ai *UnitAI) { return } + // 2a. options := ai.target.Options() if len(options) > 0 { - // TODO: move towards nearest target - if a := attackAttackableEnemyPerm(ai); a != nil { - ai.actions <- a + if ai.u.AvailMoveActions > 0 { + a := moveTowardsNearestTargetOption(ai, options) + if !ai.PostAndContinue(a) { + return + } } - } else { + + // 3a. + a := attackAttackableEnemyPerm(ai) + if !ai.PostAndContinue(a) { + return + } + } else { // 2b. WanderingAI(ai, 3) } } diff --git a/go/game/ai_test.go b/go/game/ai_test.go index 5b68ae27..880c1efc 100644 --- a/go/game/ai_test.go +++ b/go/game/ai_test.go @@ -32,7 +32,7 @@ symbols: } paths := findPathsToUnits(graph, m, a, []*Unit{f}) - if paths[0] == nil { + if len(paths) == 0 || paths[0] == nil { t.Fatal("No path found") } @@ -60,7 +60,53 @@ symbols: graph := m.generateMapGraphFor(s) paths := findPathsToUnits(graph, m, s, []*Unit{f}) - if paths[0] == nil { + if len(paths) == 0 || paths[0] == nil { t.Fatal("No path found") } } + + +func TestBauernFeind(t *testing.T) { + mapDef := `map: |1- + WWW + W + WWW + + +symbols: + W: wall +` + r := strings.NewReader(mapDef) + m, _ := readMap(r) + s := NewLocalState() + s.SetMap(m) + p1 := s.AddNewPlayer("tyrant", NewDeck(), nil) + p2 := s.AddNewPlayer("player", NewDeck(), nil) + b := s.addNewUnit(NewCard("tyrant/bauernfeind"), Position{1, 1}, p1) + s.addNewUnit(NewCard("misc/farmer"), Position{1, 3}, p2) + + bAi := NewUnitAI(s, b) + if bAi == nil { + t.Fatal("No AI available for Bauernfeind") + } + + bAi.promptAction() + a := <- bAi.actions + if a == nil { + t.Fatal("No action reported") + } + + var ma *MoveAction + var ok bool + if ma, ok = a.(*MoveAction); !ok { + t.Fatal("AI not moving towards target") + } + + if tile, ok := ma.targets.Cur().sel[0].(*Tile); ok { + if tile != m.TileAt(Position{3,2}) { + t.Fatalf("Unexpected Bauernfeind move: expected tile(3,2), got: %s", tile) + } + } else { + t.Fatal("Target is not a Tile") + } +} diff --git a/go/game/state.go b/go/game/state.go index 577a0cb8..734bddd2 100644 --- a/go/game/state.go +++ b/go/game/state.go @@ -17,7 +17,7 @@ type State interface { Players() []*Player PlayerByName(name string) *Player PlayerById(id int) *Player - AddNewPlayer(name string, deck *Deck, color color.Color) + AddNewPlayer(name string, deck *Deck, color color.Color) *Player AddNewAiPlayer(name string, color color.Color) ActivePlayer() *Player ActivePhase() PhaseType @@ -110,9 +110,10 @@ func NewLocalState() *LocalState { return s } -func (s *LocalState) AddNewPlayer(name string, deck *Deck, color color.Color) { +func (s *LocalState) AddNewPlayer(name string, deck *Deck, color color.Color) *Player { p := NewPlayer(len(s.players)+1, name, deck, s, color) s.players = append(s.players, p) + return s.players[len(s.players)-1] } func (s *LocalState) AddNewAiPlayer(name string, color color.Color) { diff --git a/go/game/targets.go b/go/game/targets.go index c5acf514..02f63378 100644 --- a/go/game/targets.go +++ b/go/game/targets.go @@ -85,6 +85,12 @@ func newTarget(s *LocalState, desc TargetDesc, action Action) *Target { return newTargetWithSel(s, desc, action, []interface{}{}) } +func newUnitAiTarget(s *LocalState, desc TargetDesc, ai *UnitAI) *Target { + // Dummy action allowing the parsing code to determine the controller of the AI + dummyAction := NewPassPriority(ai.u.controller) + return newTargetWithSel(s, desc, dummyAction, []interface{}{}) +} + func newConstraintTarget(s *LocalState, desc TargetDesc, constraint TargetConstraintFunc, action Action, ) *Target { return &Target{ |
