diff options
Diffstat (limited to 'V3/AI')
-rw-r--r-- | V3/AI/ActionState.cs | 25 | ||||
-rw-r--r-- | V3/AI/AiState.cs | 27 | ||||
-rw-r--r-- | V3/AI/IAction.cs | 24 | ||||
-rw-r--r-- | V3/AI/IAiPlayer.cs | 51 | ||||
-rw-r--r-- | V3/AI/IStrategy.cs | 17 | ||||
-rw-r--r-- | V3/AI/IWorldView.cs | 21 | ||||
-rw-r--r-- | V3/AI/Internal/AbstractAction.cs | 38 | ||||
-rw-r--r-- | V3/AI/Internal/AiPlayer.cs | 244 | ||||
-rw-r--r-- | V3/AI/Internal/AttackStrategy.cs | 48 | ||||
-rw-r--r-- | V3/AI/Internal/IActionFactory.cs | 27 | ||||
-rw-r--r-- | V3/AI/Internal/MoveAction.cs | 53 | ||||
-rw-r--r-- | V3/AI/Internal/SpawnAction.cs | 42 | ||||
-rw-r--r-- | V3/AI/Internal/WorldView.cs | 19 |
13 files changed, 636 insertions, 0 deletions
diff --git a/V3/AI/ActionState.cs b/V3/AI/ActionState.cs new file mode 100644 index 0000000..18996c8 --- /dev/null +++ b/V3/AI/ActionState.cs @@ -0,0 +1,25 @@ +namespace V3.AI +{ + /// <summary> + /// The state of an action taken by the computer player. + /// </summary> + public enum ActionState + { + /// <summary> + /// The action is waiting to be executed. + /// </summary> + Waiting, + /// <summary> + /// The action is currently being executed. + /// </summary> + Executing, + /// <summary> + /// The action has been done successfully. + /// </summary> + Done, + /// <summary> + /// The action failed. + /// </summary> + Failed + } +} diff --git a/V3/AI/AiState.cs b/V3/AI/AiState.cs new file mode 100644 index 0000000..1bc0ab0 --- /dev/null +++ b/V3/AI/AiState.cs @@ -0,0 +1,27 @@ +namespace V3.AI +{ + /// <summary> + /// An action state for the AI player that is part of a strategy. A state + /// defines the specific actions to take (for example, defend peasants, or + /// attack enemy creatures). + /// </summary> + public enum AiState + { + /// <summary> + /// Waiting for the player actions. + /// </summary> + Idle, + /// <summary> + /// Defend peasants so that they don't become zombies. + /// </summary> + DefendPeasants, + /// <summary> + /// Attack enemy creatures. + /// </summary> + AttackCreatures, + /// <summary> + /// Attack the necromancer directly. + /// </summary> + AttackNecromancer + } +} diff --git a/V3/AI/IAction.cs b/V3/AI/IAction.cs new file mode 100644 index 0000000..b68355f --- /dev/null +++ b/V3/AI/IAction.cs @@ -0,0 +1,24 @@ +namespace V3.AI +{ + /// <summary> + /// An action that can be taken by the computer player. + /// </summary> + public interface IAction + { + /// <summary> + /// The current state of the action. + /// </summary> + ActionState State { get; } + + /// <summary> + /// Start the execution of the action. + /// </summary> + void Start(); + + /// <summary> + /// Update the execution state. This method should be repateatingly + /// called as long as State is Executing. + /// </summary> + void Update(); + } +} diff --git a/V3/AI/IAiPlayer.cs b/V3/AI/IAiPlayer.cs new file mode 100644 index 0000000..7f5f81b --- /dev/null +++ b/V3/AI/IAiPlayer.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; +using Microsoft.Xna.Framework; + +namespace V3.AI +{ + /// <summary> + /// A computer player that takes actions according to a specified strategy. + /// </summary> + public interface IAiPlayer + { + /// <summary> + /// The current world view of the player. It stores the knowledge of + /// the computer player based on the previous percepts. + /// </summary> + IWorldView WorldView { get; } + /// <summary> + /// The strategy of the player. The strategy is a state machine that + /// defines the current state. + /// </summary> + IStrategy Strategy { get; } + /// <summary> + /// The current state of the player. The state is one step of the + /// strategy, and defines the specific actions to take. + /// </summary> + AiState State { get; set; } + /// <summary> + /// The actions that the player wants to be executed. Updated by + /// Act. + /// </summary> + IList<IAction> Actions { get; } + + /// <summary> + /// Executes one update cycle -- perception, acting and the execution + /// of actions. + /// </summary> + /// <param name="gameTime">the time since the last update</param> + void Update(GameTime gameTime); + + /// <summary> + /// Update the AI's view of the game world. + /// </summary> + void Percept(); + + /// <summary> + /// Take actions based on the previous percepts, the current strategy + /// and state. Updates the list of actions stored in Actions. These + /// should be executed by the caller. + /// </summary> + void Act(); + } +} diff --git a/V3/AI/IStrategy.cs b/V3/AI/IStrategy.cs new file mode 100644 index 0000000..4bfb99c --- /dev/null +++ b/V3/AI/IStrategy.cs @@ -0,0 +1,17 @@ +namespace V3.AI +{ + /// <summary> + /// A strategy for the computer player. A strategy is a finite state + /// machine. + /// </summary> + public interface 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> + AiState Update(AiState state, IWorldView worldView); + } +} diff --git a/V3/AI/IWorldView.cs b/V3/AI/IWorldView.cs new file mode 100644 index 0000000..b47322f --- /dev/null +++ b/V3/AI/IWorldView.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using V3.Objects; + +namespace V3.AI +{ + /// <summary> + /// Stores the knowledge of the computer player about the game world, and + /// is used for the evaluation of the strategy. It is also used to decide + /// which actions to take based on the current state. + /// </summary> + public interface IWorldView + { + int EnemyCount { get; set; } + int InitialPlebsCount { get; set; } + int PlebsCount { get; set; } + float NecromancerHealth { get; set; } + List<ICreature> IdlingKnights { get; } + List<ICreature> Targets { get; } + List<ICreature> Plebs { get; } + } +} 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>(); + } +} |