aboutsummaryrefslogtreecommitdiff
path: root/V3/AI/Internal
diff options
context:
space:
mode:
Diffstat (limited to 'V3/AI/Internal')
-rw-r--r--V3/AI/Internal/AbstractAction.cs38
-rw-r--r--V3/AI/Internal/AiPlayer.cs244
-rw-r--r--V3/AI/Internal/AttackStrategy.cs48
-rw-r--r--V3/AI/Internal/IActionFactory.cs27
-rw-r--r--V3/AI/Internal/MoveAction.cs53
-rw-r--r--V3/AI/Internal/SpawnAction.cs42
-rw-r--r--V3/AI/Internal/WorldView.cs19
7 files changed, 471 insertions, 0 deletions
diff --git a/V3/AI/Internal/AbstractAction.cs b/V3/AI/Internal/AbstractAction.cs
new file mode 100644
index 0000000..1b5f73e
--- /dev/null
+++ b/V3/AI/Internal/AbstractAction.cs
@@ -0,0 +1,38 @@
+namespace V3.AI.Internal
+{
+ /// <summary>
+ /// Abstract implementation of IAction.
+ /// </summary>
+ public abstract class AbstractAction : IAction
+ {
+ /// <summary>
+ /// The current state of the action.
+ /// </summary>
+ public ActionState State { get; private set; } = ActionState.Waiting;
+
+ /// <summary>
+ /// Start the execution of the action.
+ /// </summary>
+ public virtual void Start()
+ {
+ State = ActionState.Executing;
+ }
+
+ /// <summary>
+ /// Update the execution state. This method should be repateatingly
+ /// called as long as State is Executing.
+ /// </summary>
+ public virtual void Update()
+ {
+ if (State != ActionState.Executing)
+ return;
+ State = GetNextState();
+ }
+
+ /// <summary>
+ /// Returns the next state of this action. It is guaranteed that the
+ /// current state is Executing.
+ /// </summary>
+ protected abstract ActionState GetNextState();
+ }
+}
diff --git a/V3/AI/Internal/AiPlayer.cs b/V3/AI/Internal/AiPlayer.cs
new file mode 100644
index 0000000..6040348
--- /dev/null
+++ b/V3/AI/Internal/AiPlayer.cs
@@ -0,0 +1,244 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.Xna.Framework;
+using V3.Objects;
+
+namespace V3.AI.Internal
+{
+ /// <summary>
+ /// Default implementation of IAiPlayer.
+ /// </summary>
+ // ReSharper disable once ClassNeverInstantiated.Global
+ public class AiPlayer : IAiPlayer
+ {
+ /// <summary>
+ /// The current world view of the player. It stores the knowledge of
+ /// the computer player based on the previous percepts.
+ /// </summary>
+ public IWorldView WorldView { get; } = new WorldView();
+ /// <summary>
+ /// The strategy of the player. The strategy is a state machine that
+ /// defines the current state.
+ /// </summary>
+ public IStrategy Strategy { get; } = new AttackStrategy();
+ /// <summary>
+ /// The current state of the player. The state is one step of the
+ /// strategy, and defines the specific actions to take.
+ /// </summary>
+ public AiState State { get; set; } = AiState.Idle;
+ /// <summary>
+ /// The actions that the player wants to be executed. Updated by
+ /// Act.
+ /// </summary>
+ public IList<IAction> Actions { get; } = new List<IAction>();
+
+ private readonly IActionFactory mActionFactory;
+ private readonly IBasicCreatureFactory mCreatureFactory;
+ private readonly IObjectsManager mObjectsManager;
+ private readonly UpdatesPerSecond mUpS = new UpdatesPerSecond(1);
+ private readonly Random mRandom = new Random();
+ private TimeSpan mTimeSpan = TimeSpan.Zero;
+ private TimeSpan mTimeSpanSpawn = TimeSpan.Zero;
+ private int mMaxWaitBaseMs = (int) TimeSpan.FromSeconds(20).TotalMilliseconds;
+ private int mMaxWaitAddMs = (int) TimeSpan.FromSeconds(60).TotalMilliseconds;
+
+ /// <summary>
+ /// Creates a new AI player.
+ /// </summary>
+ public AiPlayer(IActionFactory actionFactory, IBasicCreatureFactory creatureFactory,
+ IObjectsManager objectsManager)
+ {
+ mActionFactory = actionFactory;
+ mCreatureFactory = creatureFactory;
+ mObjectsManager = objectsManager;
+ }
+
+ /// <summary>
+ /// Executes one update cycle -- perception, acting and the execution
+ /// of actions.
+ /// </summary>
+ /// <param name="gameTime">the time since the last update</param>
+ public void Update(GameTime gameTime)
+ {
+ mTimeSpan += gameTime.ElapsedGameTime;
+ if (!mUpS.IsItTime(gameTime))
+ return;
+ Percept();
+ Act();
+ foreach (var action in Actions)
+ {
+ if (action.State == ActionState.Waiting)
+ action.Start();
+ action.Update();
+ }
+ }
+
+ /// <summary>
+ /// Update the AI's view of the game world.
+ /// </summary>
+ public void Percept()
+ {
+ WorldView.EnemyCount = 0;
+ WorldView.PlebsCount = 0;
+ WorldView.NecromancerHealth = 0;
+ WorldView.IdlingKnights.Clear();
+ WorldView.Targets.Clear();
+ WorldView.Plebs.Clear();
+
+ foreach (var creature in mObjectsManager.CreatureList)
+ {
+ if (creature.Faction == Faction.Kingdom)
+ {
+ if (!creature.IsDead)
+ {
+ if (creature is Knight)
+ {
+ if (creature.MovementState == MovementState.Idle && creature.IsAttacking == null)
+ WorldView.IdlingKnights.Add(creature);
+ }
+ }
+ }
+ else if (creature.Faction == Faction.Undead)
+ {
+ if (!creature.IsDead)
+ {
+ if (!(creature is Necromancer))
+ WorldView.EnemyCount++;
+ else
+ WorldView.NecromancerHealth = (float) creature.Life / creature.MaxLife;
+ WorldView.Targets.Add(creature);
+ }
+ }
+ else if (creature.Faction == Faction.Plebs)
+ {
+ if (!creature.IsDead)
+ {
+ WorldView.Plebs.Add(creature);
+ }
+ }
+
+ WorldView.PlebsCount = WorldView.Plebs.Count;
+ if (WorldView.InitialPlebsCount < WorldView.PlebsCount)
+ WorldView.InitialPlebsCount = WorldView.PlebsCount;
+ }
+ }
+
+ private TimeSpan GetRandomTimeSpanSpawn()
+ {
+ var factor = Math.Max(0, 500 - WorldView.EnemyCount) / 500;
+ return TimeSpan.FromMilliseconds(mRandom.Next(mMaxWaitBaseMs)) +
+ TimeSpan.FromSeconds(mRandom.Next(factor * mMaxWaitAddMs));
+ }
+
+ /// <summary>
+ /// Take actions based on the previous percepts, the current strategy
+ /// and state.
+ /// </summary>
+ public void Act()
+ {
+ State = Strategy.Update(State, WorldView);
+
+ var completedActions = Actions.Where(
+ a => a.State == ActionState.Done || a.State == ActionState.Failed).ToList();
+ completedActions.ForEach(a => Actions.Remove(a));
+
+ if (State != AiState.Idle)
+ {
+ if (mTimeSpan >= mTimeSpanSpawn)
+ {
+ mTimeSpan -= mTimeSpanSpawn;
+ mTimeSpanSpawn = GetRandomTimeSpanSpawn();
+ SpawnKnight();
+ }
+ }
+
+ switch (State)
+ {
+ case AiState.Idle:
+ // nothing do to when idling
+ return;
+ case AiState.AttackCreatures:
+ // let all idling soldiers attack some creatures
+ if (WorldView.Targets.Count > 0)
+ {
+ foreach (var creature in WorldView.IdlingKnights)
+ {
+ ICreature target = null;
+ var distance = float.MaxValue;
+ foreach (var c in WorldView.Targets)
+ {
+ var d = Vector2.Distance(c.Position, creature.Position);
+ if (d < distance)
+ {
+ distance = d;
+ target = c;
+ }
+ }
+ creature.IsAttacking = target;
+ }
+ }
+ break;
+ case AiState.DefendPeasants:
+ if (WorldView.Plebs.Count > 0)
+ {
+ foreach (var creature in WorldView.IdlingKnights)
+ {
+ ICreature target = null;
+ var distance = float.MaxValue;
+ var threshold = (int) (creature.AttackRadius * 0.8);
+ foreach (var c in WorldView.Plebs)
+ {
+ var d = Vector2.Distance(c.Position, creature.Position);
+ if (d <= threshold)
+ {
+ target = null;
+ break;
+ }
+ // attempt to avoid clustering
+ /*if (mObjectsManager.GetObjectsInRectangle(c.SelectionRectangle).OfType<Knight>().Count() > 0)
+ {
+ continue;
+ }*/
+ if (d < distance)
+ {
+ distance = d;
+ target = c;
+ }
+ }
+ if (target != null)
+ {
+ var offset = target.Position - creature.Position;
+ offset.Normalize();
+ offset *= threshold;
+ Move(creature, target.Position + offset);
+ }
+ }
+ }
+ break;
+ case AiState.AttackNecromancer:
+ foreach (var c in WorldView.IdlingKnights)
+ {
+ c.IsAttacking = mObjectsManager.PlayerCharacter;
+ }
+ break;
+ }
+ }
+
+ private void SpawnKnight()
+ {
+ var knight = mCreatureFactory.CreateKnight();
+ var position = new Vector2(500, 500);
+ if (mObjectsManager.Castle != null)
+ position = mObjectsManager.Castle.Position;
+ var spawnAction = mActionFactory.CreateSpawnAction(knight, position);
+ Actions.Add(spawnAction);
+ }
+
+ private void Move(ICreature creature, Vector2 destination)
+ {
+ var moveAction = mActionFactory.CreateMoveAction(creature, destination);
+ Actions.Add(moveAction);
+ }
+ }
+}
diff --git a/V3/AI/Internal/AttackStrategy.cs b/V3/AI/Internal/AttackStrategy.cs
new file mode 100644
index 0000000..d108b1f
--- /dev/null
+++ b/V3/AI/Internal/AttackStrategy.cs
@@ -0,0 +1,48 @@
+namespace V3.AI.Internal
+{
+ /// <summary>
+ /// A simple strategy for the computer player that tells him to attack the
+ /// enemy creatures.
+ /// </summary>
+ internal class AttackStrategy : IStrategy
+ {
+ /// <summary>
+ /// Updates the current state according to the game situtation.
+ /// </summary>
+ /// <param name="state">the current state</param>
+ /// <param name="worldView">the current view of the game world</param>
+ /// <returns>the next state indicated by this strategy</returns>
+ public AiState Update(AiState state, IWorldView worldView)
+ {
+ switch (state)
+ {
+ case AiState.Idle:
+ if (worldView.InitialPlebsCount - worldView.PlebsCount > 3)
+ {
+ return AiState.DefendPeasants;
+ }
+ break;
+ case AiState.DefendPeasants:
+ if (worldView.PlebsCount < worldView.InitialPlebsCount * 0.75 || worldView.EnemyCount > 20)
+ {
+ return AiState.AttackCreatures;
+ }
+ break;
+ case AiState.AttackCreatures:
+ if (worldView.NecromancerHealth < 0.1)
+ {
+ return AiState.AttackNecromancer;
+ }
+ break;
+ case AiState.AttackNecromancer:
+ if (worldView.NecromancerHealth >= 0.1)
+ {
+ return AiState.AttackCreatures;
+ }
+ break;
+ }
+
+ return state;
+ }
+ }
+}
diff --git a/V3/AI/Internal/IActionFactory.cs b/V3/AI/Internal/IActionFactory.cs
new file mode 100644
index 0000000..eb62222
--- /dev/null
+++ b/V3/AI/Internal/IActionFactory.cs
@@ -0,0 +1,27 @@
+using Microsoft.Xna.Framework;
+using V3.Objects;
+
+namespace V3.AI.Internal
+{
+ /// <summary>
+ /// Creates IAction instances. Automatically implemented by Ninject.
+ /// </summary>
+ public interface IActionFactory
+ {
+ /// <summary>
+ /// Creates a new MoveAction to move the given creature to the given
+ /// destination.
+ /// </summary>
+ /// <param name="creature">the creature to mvoe</param>
+ /// <param name="destination">the destination of the creature</param>
+ MoveAction CreateMoveAction(ICreature creature, Vector2 destination);
+
+ /// <summary>
+ /// Creates a new SpawnAction that spawns the given creature at the
+ /// given position.
+ /// </summary>
+ /// <param name="creature">the creature to spawn</param>
+ /// <param name="position">the spawn position</param>
+ SpawnAction CreateSpawnAction(ICreature creature, Vector2 position);
+ }
+}
diff --git a/V3/AI/Internal/MoveAction.cs b/V3/AI/Internal/MoveAction.cs
new file mode 100644
index 0000000..fcbba54
--- /dev/null
+++ b/V3/AI/Internal/MoveAction.cs
@@ -0,0 +1,53 @@
+using Microsoft.Xna.Framework;
+using V3.Objects;
+
+namespace V3.AI.Internal
+{
+ /// <summary>
+ /// Moves a creature to a destination point.
+ /// </summary>
+ // ReSharper disable once ClassNeverInstantiated.Global
+ public class MoveAction : AbstractAction
+ {
+ private ICreature mCreature;
+ private Vector2 mDestination;
+
+ /// <summary>
+ /// Creates a new MoveAction to move the given creature to the given
+ /// destination.
+ /// </summary>
+ /// <param name="creature">the creature to mvoe</param>
+ /// <param name="destination">the destination of the creature</param>
+ public MoveAction(ICreature creature, Vector2 destination)
+ {
+ mCreature = creature;
+ mDestination = destination;
+ }
+
+ /// <summary>
+ /// Start the execution of the action.
+ /// </summary>
+ public override void Start()
+ {
+ mCreature.Move(mDestination);
+ base.Start();
+ }
+
+ protected override ActionState GetNextState()
+ {
+ switch (mCreature.MovementState)
+ {
+ case MovementState.Idle:
+ return ActionState.Done;
+ case MovementState.Attacking:
+ case MovementState.Dying:
+ return ActionState.Failed;
+ case MovementState.Moving:
+ return ActionState.Executing;
+ default:
+ return ActionState.Failed;
+ }
+ }
+
+ }
+}
diff --git a/V3/AI/Internal/SpawnAction.cs b/V3/AI/Internal/SpawnAction.cs
new file mode 100644
index 0000000..4df6225
--- /dev/null
+++ b/V3/AI/Internal/SpawnAction.cs
@@ -0,0 +1,42 @@
+using Microsoft.Xna.Framework;
+using V3.Objects;
+
+namespace V3.AI.Internal
+{
+ /// <summary>
+ /// Spawns a creature at a given position.
+ /// </summary>
+ // ReSharper disable once ClassNeverInstantiated.Global
+ public class SpawnAction : AbstractAction
+ {
+ private readonly IObjectsManager mObjectsManager;
+ private ICreature mCreature;
+ private Vector2 mPosition;
+
+ /// <summary>
+ /// Creates a new SpawnAction that spawns the given creature at the
+ /// given position.
+ /// </summary>
+ /// <param name="objectsManager">the linked objects manager</param>
+ /// <param name="creature">the creature to spawn</param>
+ /// <param name="position">the spawn position</param>
+ public SpawnAction(IObjectsManager objectsManager, ICreature creature, Vector2 position)
+ {
+ mObjectsManager = objectsManager;
+ mCreature = creature;
+ mPosition = position;
+ }
+
+ public override void Start()
+ {
+ mCreature.Position = mPosition;
+ mObjectsManager.CreateCreature(mCreature);
+ base.Start();
+ }
+
+ protected override ActionState GetNextState()
+ {
+ return ActionState.Done;
+ }
+ }
+}
diff --git a/V3/AI/Internal/WorldView.cs b/V3/AI/Internal/WorldView.cs
new file mode 100644
index 0000000..cedd0ae
--- /dev/null
+++ b/V3/AI/Internal/WorldView.cs
@@ -0,0 +1,19 @@
+using System.Collections.Generic;
+using V3.Objects;
+
+namespace V3.AI.Internal
+{
+ /// <summary>
+ /// Default implementation of IWorldView.
+ /// </summary>
+ internal class WorldView : IWorldView
+ {
+ public int EnemyCount { get; set; }
+ public int InitialPlebsCount { get; set; }
+ public int PlebsCount { get; set; }
+ public float NecromancerHealth { get; set; }
+ public List<ICreature> IdlingKnights { get; } = new List<ICreature>();
+ public List<ICreature> Targets { get; } = new List<ICreature>();
+ public List<ICreature> Plebs { get; } = new List<ICreature>();
+ }
+}