using System; using System.Collections.Generic; using System.Linq; using Microsoft.Xna.Framework; using V3.Objects; namespace V3.AI.Internal { /// /// Default implementation of IAiPlayer. /// // ReSharper disable once ClassNeverInstantiated.Global public class AiPlayer : IAiPlayer { /// /// The current world view of the player. It stores the knowledge of /// the computer player based on the previous percepts. /// public IWorldView WorldView { get; } = new WorldView(); /// /// The strategy of the player. The strategy is a state machine that /// defines the current state. /// public IStrategy Strategy { get; } = new AttackStrategy(); /// /// The current state of the player. The state is one step of the /// strategy, and defines the specific actions to take. /// public AiState State { get; set; } = AiState.Idle; /// /// The actions that the player wants to be executed. Updated by /// Act. /// public IList Actions { get; } = new List(); 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; /// /// Creates a new AI player. /// public AiPlayer(IActionFactory actionFactory, IBasicCreatureFactory creatureFactory, IObjectsManager objectsManager) { mActionFactory = actionFactory; mCreatureFactory = creatureFactory; mObjectsManager = objectsManager; } /// /// Executes one update cycle -- perception, acting and the execution /// of actions. /// /// the time since the last update 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(); } } /// /// Update the AI's view of the game world. /// 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)); } /// /// Take actions based on the previous percepts, the current strategy /// and state. /// 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().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); } } }