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);
}
}
}