package game import ( "fmt" "log" "strconv" "strings" "sync" "github.com/RyanCarrier/dijkstra" ) type UnitAI struct { s *State u *Unit actions chan Action 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() { 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, u, c, wg} switch aiDesc { case "aggressive": go ai.Execute(AggressiveAI) case "shy": go ai.Execute(ShyAI) case "wandering": aiDesc := u.Card.getAI() x, err := strconv.Atoi(strings.Split(u.Card.getAI(), " ")[1]) if err != nil { log.Fatalf("Invalid wandering AI description %s", aiDesc) } go ai.Execute(func(ai *UnitAI) { WanderingAI(ai, x) }) case "target-oriented": go ai.Execute(TargetOrientedAI) } return ai } 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.Fatalf("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(u.GetTile().Position.String()) if err != nil { log.Fatalf("No mapping %s in Graph: %s", u.GetTile().Position.String(), err) } destId, err := graph.GetMapping(pos.String()) if err != nil { log.Fatalf("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 { var bestPathToUnit *dijkstra.BestPath = nil for _, adjacentTile := range TilesInRangeFromOrigin(m, unit.GetTile().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 } moveTargets := ai.u.MoveRangeTiles(ai.s.Map) if len(moveTargets) == 0 { return nil } idx := 0 if len(moveTargets) > 1 { idx = ai.s.Rand.Intn(len(moveTargets) - 1) } moveTarget := moveTargets[idx] a := NewMoveActionPrototype(ai.u).Specialize(moveTarget) 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.GetController()) 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.GetTile().Position, nextStep, ai.u.Movement.Range) { target = nextStep } else { break } } a := NewMoveActionPrototype(ai.u).Specialize(ai.s.Map.TileAt(target)) return a } func attackAttackableEnemyPerm(ai *UnitAI) Action { attackablePerms := ai.u.AttackableEnemyPermanents(ai.s.Map) if ai.u.AvailAttackActions == 0 || len(attackablePerms) == 0 { return nil } idx := 0 if len(attackablePerms) > 1 { idx = ai.s.Rand.Intn(len(attackablePerms) - 1) } attackablePerm := attackablePerms[idx] a := NewAttackActionPrototype(ai.u).Specialize(attackablePerm) return 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.GetController()) { for _, p := range enemyUnit.AttackablePermanents(ai.s.Map) { if p == ai.u { inEnemyAttackRange = true break out } } } if !inEnemyAttackRange && ai.u.HasFullAction() { // return NewFullActionPrototype(u).Specialize(u.FullActionResolveFunc()) a := NewFullActionPrototype(ai.u).Specialize() ai.actions <- a } // 2. // TODO: implement path finding // if u.AvailMoveActions > 0 { // } // 3. if ai.u.AvailAttackActions > 0 { attackAttackableEnemyPerm(ai) } } // 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.GetController()) { if IsPositionInRange(ai.u.GetTile().Position, enemyUnit.GetTile().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() { ai.u.FullAction() } // if ai.t != nil { // // TODO: path finding again // attackAttackableEnemyPerm(ai) // } else { WanderingAI(ai, 3) // } }