package game import ( "fmt" "log" "math/rand" "strconv" "strings" "sync" "github.com/RyanCarrier/dijkstra" ) type UnitAI struct { s *State u *Unit actions chan Action target *Target syncGameState *sync.WaitGroup } func (ai *UnitAI) NextAction() Action { return <-ai.actions } func (ai *UnitAI) PostAndContinue(a Action) bool { if a == nil { return true } ai.actions <- a return ai.awaitGameStateSync() } func (ai *UnitAI) awaitGameStateSync() bool { ai.syncGameState.Wait() ai.syncGameState.Add(1) return !ai.u.IsDestroyed() } func (ai *UnitAI) promptAction() { ai.syncGameState.Done() } func (ai *UnitAI) Execute(f func(*UnitAI)) { defer close(ai.actions) if ai.awaitGameStateSync() { // TODO: Investigate hangs when the AI did not issue any actions f(ai) } } func NewUnitAI(s *State, u *Unit) *UnitAI { aiDesc := u.Card().getAI() if aiDesc == "" { return nil } c := make(chan Action) wg := new(sync.WaitGroup) wg.Add(1) ai := &UnitAI{ s: s, u: u, actions: c, syncGameState: wg, } switch aiDesc { case "aggressive": go ai.Execute(AggressiveAI) case "shy": go ai.Execute(ShyAI) case "wandering": aiDesc := u.Card().getCanonicalValues("ai")[0] x, err := strconv.Atoi(strings.Split(aiDesc, " ")[1]) if err != nil { log.Panicf("Invalid wandering AI description %s", aiDesc) } go ai.Execute(func(ai *UnitAI) { WanderingAI(ai, x) }) case "target-oriented": aiDesc := u.Card().getCanonicalValues("ai")[0] targetDesc := aiDesc[len("target-oriented")+1:] ai.target = newTarget(s, newTargetDesc(targetDesc), nil) go ai.Execute(TargetOrientedAI) } return ai } func selectRandomTargets(rand *rand.Rand, targets *Targets) error { for _, t := range targets.ts { for t.RequireSelection() { options := t.Options() if len(options) == 0 { return fmt.Errorf("No possible selection") } idx := 0 if len(options) > 1 { idx = rand.Intn(len(options) - 1) } _ = t.AddSelection(options[idx]) } } return nil } func (m *Map) generateMapGraphFor(u *Unit) *dijkstra.Graph { graph := dijkstra.NewGraph() for _, t := range m.AllTiles() { graph.AddMappedVertex(t.Position.String()) } for _, t := range m.AllTiles() { tId := t.Position.String() for _, neighbour := range TilesInRangeFromOrigin(m, t.Position, 1) { if u.IsAvailableTile(neighbour) { cost := 2 if t.OnDiagonal(neighbour) { cost = 3 } err := graph.AddMappedArc(tId, neighbour.Position.String(), int64(cost)) if err != nil { log.Panicf("Failed to add mapped arc: %s", err) } } } } return graph } // func (m *Map) generateMovementRangeGraphFor(u *Unit) *dijkstra.Graph { // origin := TileOrContainingPermTile(u).Position // graph := dijkstra.NewGraph() // for _, t := range tilesInRangeFromOrigin(m, origin, u.Movement.Range, true) { // graph.AddMappedVertex(t.Position.String()) // } // for _, t := range TilesInRange(m, u, u.Movement.Range) { // tId := t.Position.String() // for _, neighbour := range tilesInRangeFromOrigin(m, t.Position, 1, true) { // if !IsPositionInRange(neighbour.Position, origin, u.Movement.Range) { // continue // } // if u.IsAvailableTile(neighbour) { // cost := 2 // if t.OnDiagonal(neighbour) { // cost = 3 // } // log.Printf("add %v -> %v: %d", tId, neighbour.Position.String(), int64(cost)) // err := graph.AddMappedArc(tId, neighbour.Position.String(), int64(cost)) // if err != nil { // log.Panicf("Failed to add mapped arc: %s", err) // } // } // } // } // return graph // } func findPathTo(graph *dijkstra.Graph, u *Unit, pos Position) (dijkstra.BestPath, error) { srcId, err := graph.GetMapping(TileOrContainingPermTile(u).Position.String()) if err != nil { log.Panicf("No mapping %s in Graph: %s", u.Tile().Position.String(), err) } destId, err := graph.GetMapping(pos.String()) if err != nil { log.Panicf("No mapping %s in Graph: %s", pos.String(), err) } return graph.Shortest(srcId, destId) } func findPathsToUnits(graph *dijkstra.Graph, m *Map, u *Unit, units []*Unit) []*dijkstra.BestPath { paths := []*dijkstra.BestPath{} for _, unit := range units { if unit.tile == nil { continue } 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 } } paths = append(paths, bestPathToUnit) } return paths } func moveToRandomTile(ai *UnitAI) Action { if ai.u.AvailMoveActions == 0 { return nil } a := NewMoveAction(ai.u) err := selectRandomTargets(ai.s.Rand, a.Targets()) if err != nil { return nil } 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) nearest := 0 for i, p := range paths { if p == nil { continue } if paths[nearest] == nil || p.Distance < paths[nearest].Distance { nearest = i } } if paths[nearest] == nil { return nil } var target Position 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) { target = nextStep } else { break } } a := NewMoveAction(ai.u) _ = a.Target().AddSelection(ai.s.Map.TileAt(target)) return a } func attackAttackableEnemyPerm(ai *UnitAI) Action { if ai.u.AvailAttackActions == 0 { return nil } a := NewAttackAction(ai.u) err := selectRandomTargets(ai.s.Rand, a.Targets()) if err != nil { return nil } return a } func fullAction(ai *UnitAI) { fullActions := ai.u.FullActions idx := 0 if len(fullActions) > 1 { idx = ai.s.Rand.Intn(len(fullActions) - 1) } a := fullActions[idx] err := selectRandomTargets(ai.s.Rand, a.Targets()) if err == nil { ai.actions <- a } } // 1. If enemy Unit in attack Range then // attack enemy Unit in Range // 2. While move action available do // 3a. If enemy unit exists // move towards nearest enemy unit until it is in attack range // if attack action avail then proceed at 1. // 3b. Otherwise // move to random tile in movement range func AggressiveAI(ai *UnitAI) { // 1. a := attackAttackableEnemyPerm(ai) if !ai.PostAndContinue(a) { return } // 2. if ai.u.AvailMoveActions > 0 { a = moveTowardsNearestEnemyUnit(ai) if !ai.PostAndContinue(a) { return } if a != nil && ai.u.AvailAttackActions > 0 { AggressiveAI(ai) } else { // 3b. a = moveToRandomTile(ai) if a != nil { ai.actions <- a } } } } // 2.2 Shy // 1. If not in the attack Range of an enemy Unit then // activate full action if available // 2. If move action available then // move to most distant point from all enemy units // 3. If enemy Unit in attack Range then // attack enemy Unit in Range func ShyAI(ai *UnitAI) { // 1. inEnemyAttackRange := false out: for _, enemyUnit := range ai.s.EnemyUnits(ai.u.Controller()) { for _, p := range enemyUnit.AttackablePermanents(ai.s.Map) { if p == ai.u { inEnemyAttackRange = true break out } } } if !inEnemyAttackRange && ai.u.HasFullAction() { fullAction(ai) } // 2. // TODO: implement path finding // if u.AvailMoveActions > 0 { // } // 3. if a := attackAttackableEnemyPerm(ai); a != nil { ai.actions <- a } } // 2.3 Wandering X // 1. If enemy unit in Range X then // execute aggressive AI // 2. If move action available then // move to random tile in movement Range // proceed at 1. func WanderingAI(ai *UnitAI, x int) { // 1. for _, enemyUnit := range ai.s.EnemyUnits(ai.u.Controller()) { if IsPositionInRange(ai.u.Tile().Position, TileOrContainingPermTile(enemyUnit).Position, x) { AggressiveAI(ai) break } } // 2. if ai.u.AvailMoveActions > 0 { moveToRandomTile(ai) WanderingAI(ai, x) } } // 2.4 Target-oriented TARGET // 1. Use full action if possible // 2a. If a TARGET exists and is reachable // // Move onto or towards nearest TARGET // 3a. If enemy Unit in attack Range // attack enemy Unit in Range // // 2b. Otherwise // // execute wandering 3 AI func TargetOrientedAI(ai *UnitAI) { // 1. if ai.u.HasFullAction() { fullAction(ai) return } options := ai.target.Options() if len(options) > 0 { // TODO: move towards nearest target if a := attackAttackableEnemyPerm(ai); a != nil { ai.actions <- a } } else { WanderingAI(ai, 3) } }