aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Fischer <florian.fischer@muhq.space>2024-12-27 16:51:24 +0100
committerFlorian Fischer <florian.fischer@muhq.space>2025-08-20 15:57:13 +0200
commitd29bdcd1eca581af24b236a4529e7d67956aedb8 (patch)
treed28ae93b3a41f8a90045ba4def491ce19a8fcbc9
parent5779c8439848a1d42204b30f6bb3c422ca7c6b86 (diff)
downloadmuhqs-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/TODO1
-rw-r--r--go/game/ai.go144
-rw-r--r--go/game/ai_test.go50
-rw-r--r--go/game/state.go5
-rw-r--r--go/game/targets.go6
5 files changed, 167 insertions, 39 deletions
diff --git a/go/TODO b/go/TODO
index f4a9d728..0f82a126 100644
--- a/go/TODO
+++ b/go/TODO
@@ -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{