/JTacticalSim.Service/AIService.cs
C# | 1150 lines | 767 code | 210 blank | 173 comment | 106 complexity | 76d8608981e013bc0874e9eebeded0b6 MD5 | raw file
- using System;
- using System.Configuration;
- using System.Threading.Tasks;
- using System.Collections.Generic;
- using System.Linq;
- using System.Transactions;
- using System.ServiceModel;
- using JTacticalSim.API;
- using JTacticalSim.API.Component;
- using JTacticalSim.API.Data;
- using JTacticalSim.API.InfoObjects;
- using JTacticalSim.API.Service;
- using JTacticalSim.Component.Battle;
- using JTacticalSim.Service;
- using JTacticalSim.Utility;
- using JTacticalSim.DataContext.Repository;
- using ctxUtil = JTacticalSim.DataContext.Utility;
- using JTacticalSim.Component.GameBoard;
- namespace JTacticalSim.Service
- {
- [ServiceBehavior]
- public sealed class AIService : BaseGameService, IAIService
- {
- static readonly object padlock = new object();
- private static volatile IAIService _instance = null;
- public static IAIService Instance
- {
- get
- {
- if (_instance == null)
- {
- lock (padlock)
- if (_instance == null) _instance = new AIService();
- }
- return _instance;
- }
- }
- // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- private AIService()
- {}
- #region Service Methods
- // Unit Orders
- [OperationBehavior]
- public IServiceResult<IUnit> DeployUnitsFromTransportToNode(IUnit transport, IEnumerable<IUnit> units, INode destinationNode)
- {
- var r = new ServiceResult<IUnit>{Status = ResultStatus.SUCCESS};
- r.Messages.Add("All units deployed.");
- var currentNode = transport.GetNode();
- // Deployment node must be only 1 space from water transport : same space for other types
- // Units can be moved after if they have remaining movement points
- var deployDistance = RulesService.Instance.GetAllowableDeployDistanceForTransport(transport).Result;
-
- var distanceToNodeResult = CalculateNodeCountToNode(currentNode, destinationNode, deployDistance);
- if (distanceToNodeResult.Status == ResultStatus.FAILURE)
- {
- r.Status = ResultStatus.FAILURE;
- r.Messages.Add("Deployment node is not within allowed deployment distance.");
- r.FailedObjects.AddRange(units);
- return r;
- }
- if (!transport.GetTransportedUnits().Any())
- {
- r.Status = ResultStatus.FAILURE;
- r.Messages.Add("No units currently transported.");
- return r;
- }
- using (var txn = new TransactionScope())
- {
- Action<IUnit> componentAction = u =>
- {
- // Deploy the unit if the destination node is compatible and
- // the unit is actually being transported,
- if (!transport.GetTransportedUnits().Any(tu => tu.Equals(u)) || !JTSServices.RulesService.UnitIsDeployableToNode(u, destinationNode).Result)
- {
- r.FailedObjects.Add(u);
- return;
- }
- u.MoveToLocation(destinationNode, currentNode);
- r.SuccessfulObjects.Add(u);
- // Remove from the transport table
- JTSServices.DataService.RemoveUnitTransport(u, transport);
- };
- if (Convert.ToBoolean(ConfigurationManager.AppSettings["run_multithreaded"]))
- {
- Parallel.ForEach(units, componentAction);
- }
- else
- {
- units.ToList().ForEach(u =>
- {
- componentAction(u);
- });
- }
- JTSServices.UnitService.UpdateUnits(new List<IUnit> { transport });
- txn.Complete();
- }
- if (!r.SuccessfulObjects.Any())
- {
- r.Status = ResultStatus.FAILURE;
- r.Messages.Add("No units were deployed due to incompatibility with the deployment node.");
- }
- // If there are any failed objects and some successful objects return a SOME_FAILURE result
- if (r.FailedObjects.Any() && r.SuccessfulObjects.Any())
- {
- r.Status = ResultStatus.SOME_FAILURE;
- r.Messages.Add("Some units were not deployed due to incompatibility with the deployment node.");
- }
- return r;
- }
- [OperationBehavior]
- public IServiceResult<IUnit> LoadUnitsToTransport(IUnit transport, List<IUnit> units)
- {
- var r = new ServiceResult<IUnit>{Status = ResultStatus.SUCCESS};
- INode transportNode = transport.GetNode();
- // units to load must be only 1 space from water transport : same space for other types
- // Units can be moved before they're loaded onto the transport
- var loadDistance = RulesService.Instance.GetAllowableLoadDistanceForTransport(transport).Result;
- var distanceToNodeResult = CalculateNodeCountToNode(units.FirstOrDefault().GetNode(), transportNode, loadDistance);
-
- if (distanceToNodeResult.Status == ResultStatus.FAILURE)
- {
- r.Status = ResultStatus.FAILURE;
- r.Messages.Add("Units are not within maximum load distance of transport.");
- r.FailedObjects.AddRange(units);
- return r;
- }
- // Prune incompatible units for this transport
- units.RemoveAll(u => !u.IsCompatibleWithTransport(transport));
- // Prune any units already transporting other units
- units.RemoveAll(u => u.GetTransportedUnits().Any());
- if (!units.Any())
- {
- r.Status = ResultStatus.FAILURE;
- r.Messages.Add("No units selected can be loaded on this transport.");
- r.FailedObjects.AddRange(units);
- return r;
- }
- // Validate weight limits
- var totalTransportWeightToLoad = units.Sum(u => u.TotalUnitWeight);
- var totalTransportWeight = totalTransportWeightToLoad + (transport.GetTransportedUnits().Sum(u => u.GetWeight()));
-
- if (transport.GetAllowableTransportWeight() < totalTransportWeight)
- {
- r.Status = ResultStatus.FAILURE;
- r.Messages.Add("Total unit weight exceeds allowable transport weight.");
- r.FailedObjects.AddRange(units);
- return r;
- }
- using (var txn = new TransactionScope())
- {
- Action<IUnit> componentAction = u =>
- {
- if (!JTSServices.RulesService.UnitCanTransportUnitTypeAndClass(transport, u).Result)
- {
- r.FailedObjects.Add(u);
- return;
- }
- u.LoadToLocation(transportNode, u.GetNode());
- JTSServices.DataService.SaveUnitTransport(u, transport);
- r.SuccessfulObjects.Add(u);
- };
- if (Convert.ToBoolean(ConfigurationManager.AppSettings["run_multithreaded"]))
- {
- Parallel.ForEach(units, componentAction);
- }
- else
- {
- units.ForEach(u =>
- {
- componentAction(u);
- });
- }
- JTSServices.UnitService.UpdateUnits(new List<IUnit> { transport });
- txn.Complete();
- }
-
- if (!r.SuccessfulObjects.Any())
- {
- r.Status = ResultStatus.FAILURE;
- r.Messages.Add("No units were loaded due to incompatiblity with the transport type.");
- return r;
- }
- // If there are any failed objects and some successful objects return a SOME_FAILURE result
- if (r.FailedObjects.Any() && r.SuccessfulObjects.Any())
- {
- r.Status = ResultStatus.SOME_FAILURE;
- r.Messages.Add("Some units were not loaded due to incompatibility with the transport type.");
- return r;
- }
- r.Messages.Add("All units loaded.");
- return r;
- }
- [OperationBehavior]
- public IServiceResult<IUnit> AttachUnitToUnit (IUnit parent, IUnit unit)
- {
- var r = new ServiceResult<IUnit>{Status = ResultStatus.SUCCESS, Result = unit};
- // First, check that we can attach to the parent
- var canAttach = JTSServices.RulesService.UnitCanAttachToUnit(unit, parent);
-
- if (!canAttach.Result)
- {
- r.Status = ResultStatus.FAILURE;
- r.Messages.Add(canAttach.Message);
- r.FailedObjects.Add(unit);
- return r;
- }
- // First, make sure we're not already attached to a unit (can only be attached to one)
- if (unit.AttachedToUnit != null)
- {
- r.Status = ResultStatus.FAILURE;
- r.Messages.Add("Unit already attached. You must detach from the current unit first.");
- r.FailedObjects.Add(unit);
- return r;
- }
- var saveResult = JTSServices.DataService.SaveUnitAssignment(unit, parent);
- r.ConvertServiceResultData(saveResult);
- r.Messages.Add("Unit attached.");
- return r;
- }
- [OperationBehavior]
- public IServiceResult<IUnit> DetachUnitFromUnit(IUnit unit)
- {
- var r = new ServiceResult<IUnit>{Status = ResultStatus.SUCCESS, Result = unit};
- // Can only detach if we are, indeed, attached. Makes sense...
- if (unit.AttachedToUnit == null)
- {
- r.Status = ResultStatus.FAILURE;
- r.Messages.Add("Unit not currently attached.");
- r.FailedObjects.Add(unit);
- return r;
- }
- var saveResult = JTSServices.DataService.RemoveUnitAssignment(unit, unit.AttachedToUnit);
- r.ConvertServiceResultData(saveResult);
- r.Messages.Add("Unit detached.");
- return r;
- }
- // Pathfinding
- /// <summary>
- /// A* Implementation
- /// </summary>
- /// <param name="source"></param>
- /// <param name="faction"></param>
- /// <param name="supplyDistance"></param>
- /// <returns></returns>
- [OperationBehavior]
- public RouteInfo FindSupplyPath(INode source, IFaction faction, int supplyDistance)
- {
- // Use only friendly nodes
- var map = JTSServices.NodeService.GetAllNodesWithinDistance(source, supplyDistance, false, false)
- .Where(n => n.Faction.Equals(faction));
-
- // First, check to see if we have any targets
- var targetNodes = (from node in map
- where node.TotalUnitCount() > 0
- let units = JTSServices.UnitService.GetUnitsAt(node.Location, new[] {faction})
- where units.Any(u => u.IsUnitClass("supply"))
- select node).ToList();
- // Skip if we don't have any supply units
- if (!targetNodes.Any())
- return null;
- var paths = new List<RouteInfo>();
- Action<INode> componentAction = n =>
- {
- paths.Add(FindPathToSupplyTarget(source, n, map, supplyDistance));
- };
- if (Convert.ToBoolean(ConfigurationManager.AppSettings["run_multithreaded"]))
- {
- Parallel.ForEach(targetNodes, componentAction);
- }
- else
- {
- targetNodes.ForEach(n =>
- {
- componentAction(n);
- });
- }
- return paths.FirstOrDefault(p => p != null);
- }
- private RouteInfo FindPathToSupplyTarget(INode source,
- INode target,
- IEnumerable<INode> map,
- int supplyDistance)
- {
- source.Target = target;
- var openList = new Queue<INode>();
- var closedList = new Queue<INode>();
-
- openList.Enqueue(source);
- while (openList.Any())
- {
- // Pushes the Lower F Value nodes to the top.
- openList.Sort();
- var currentNode = openList.Dequeue();
- currentNode.Source = source;
- // Check each node if it is faction for the path we're looking for
- // and see if it has any units
- if (currentNode.Equals(target))
- {
- return new RouteInfo(ReconstructPath(currentNode), source, target);
- }
- //openList.Remove(currentNode);
- closedList.Enqueue(currentNode);
- // Get all surrounding nodes that are also in the master move map
- var neighborNodes = JTSServices.NodeService.GetAllNodesAtDistance(currentNode, 1, false)
- .Where(n => map.Any(mn => mn.Equals(n)))
- .Where(n => n.G <= supplyDistance)
- .ToList();
- // remove nodes if exist in the closed list
- neighborNodes.ForEach(n1 =>
- {
- if (closedList.Any(cn => cn.Equals(n1))) neighborNodes.RemoveAll(n2 => n1.Equals(n2));
- });
- neighborNodes.ForEach(openList.Enqueue);
- }
- // No available path
- return null;
- }
- /// <summary>
- /// A* Implementation
- /// The map param has already been pruned to include only traverseable nodes for the unit
- /// </summary>
- /// <param name="source"></param>
- /// <param name="target"></param>
- /// <param name="map"></param>
- /// <param name="unit"></param>
- /// <returns></returns>
- [OperationBehavior]
- public IServiceResult<RouteInfo> FindPath(INode source,
- INode target,
- IEnumerable<IPathableObject> map,
- IUnit unit)
- {
- var r = new ServiceResult<RouteInfo>() {Status = ResultStatus.SUCCESS};
-
- // If the selected node is outside the movement bounds for the unit, save some processing
- var distanceToTarget = CalculateNodeCountToNode(source, target, unit.CurrentMoveStats.MovementPoints).Result;
- if (distanceToTarget == null || Convert.ToInt32(distanceToTarget) > unit.CurrentMoveStats.MovementPoints)
- {
- r.Status = ResultStatus.FAILURE;
- r.Messages.Add("Target node is outside the current movement bounds for the selected unit.");
- r.Result = null;
- return r;
- }
- if (!map.Any(n => n.Equals(target)))
- {
- r.Status = ResultStatus.FAILURE;
- r.Messages.Add("target node is not in searchable nodes.");
- r.Result = null;
- return r;
- }
- var mapList = map.ToList();
- try
- {
- source.Target = target;
- var openList = new Queue<INode>();
- var closedList = new Queue<INode>();
- openList.Enqueue(source);
- while (openList.Any())
- {
- // Pushes the Lower F Value nodes to the top.
- // When we're searching for all possible routes, we need to
- // allow for for all nodes, so we take the highest weight.
- // When looking for the shortest path we want the lowest weighted node
-
- openList.Sort();
- var currentNode = openList.Dequeue();
- currentNode.Source = source;
- // Magic sauce. We've reached the target, return the path
- if (currentNode.Equals(target))
- {
- r.Result = new RouteInfo(ReconstructPath(currentNode), source, target);
- return r;
- }
- closedList.Enqueue(currentNode);
- mapList.Remove(currentNode);
- // When searching for a new path, we don't want the unit's current node location to
- // hinder the finding of a path OFF of the current node. Otherwise, it's possible for a unit to actually
- // become 'stuck' on a node
- //var movementMod = (JTSServices.RulesService.TileHasMovementOverrideForUnit(unit, currentNode.DefaultTile()).Result)
- // ? 0
- // : currentNode.DefaultTile().GetNetMovementAdjustment();
- var movementMod = currentNode.DefaultTile().GetNetMovementAdjustment();
- // Get all surrounding nodes that are also in the master move map
- // but not further away than the selected unit can travel
- var allowable = JTSServices.NodeService.GetAllowableNeighborNodesForGrid(unit, currentNode, mapList);
- var neighborNodes = allowable.Where(n => n.G <= (unit.CurrentMoveStats.MovementPoints + movementMod + n.DefaultTile().GetNetMovementAdjustment()));
- var keepNodes = new List<INode>();
- keepNodes.AddRange(neighborNodes.Where(n => !Enumerable.Contains(closedList, n)));
- keepNodes.ForEach(openList.Enqueue);
- }
- }
- catch (Exception ex)
- {
- r.ex = ex;
- r.Status = ResultStatus.EXCEPTION;
- r.Result = null;
- return r;
- }
- // No available path
- r.Status = ResultStatus.FAILURE;
- r.Result = null;
- r.Messages.Add("No Available path from {0} to {1} for unit {2}".F(source.Location.ToStringForName(),
- target.Location.ToStringForName(),
- unit.Name));
- return r;
- }
- [OperationBehavior]
- public IServiceResult<int?> CalculateNodeCountToNode(INode source, INode target, int maxDistance)
- {
- var r = new ServiceResult<int?>(){Status = ResultStatus.SUCCESS};
- for (var i = 0; i <= maxDistance; i++)
- {
- var nodesToSearch = JTSServices.NodeService.GetNodesAtDistance(source.GetNode(), i);
- if (Convert.ToBoolean(ConfigurationManager.AppSettings["run_multithreaded"]))
- {
- Parallel.ForEach(nodesToSearch, n =>
- {
- if (n.LocationEquals(target.Location)) r.Result = i;
- return;
- });
- }
- else
- {
- foreach (var n in nodesToSearch)
- {
- if (n.Equals(target)) r.Result = i;
- continue;
- }
- }
- }
- // Unit was not within the max distance
- if (r.Result == null)
- {
- r.Status = ResultStatus.FAILURE;
- r.Messages.Add("Target was not within the provided distance.");
- }
- return r;
- }
- [OperationBehavior]
- public IServiceResult<int?> CalculateNodeCountToUnit(IUnit source, IUnit target, int maxDistance)
- {
- var r = new ServiceResult<int?>(){Status = ResultStatus.SUCCESS};
- for (var i = 1; i <= maxDistance; i++)
- {
- var nodesToSearch = JTSServices.NodeService.GetNodesAtDistance(source.GetNode(), i);
-
- if (Convert.ToBoolean(ConfigurationManager.AppSettings["run_multithreaded"]))
- {
- Parallel.ForEach(nodesToSearch, n =>
- {
- var units = JTSServices.UnitService.GetUnitsAt(n.Location, new[] { target.Faction });
- if (units.Any(u => u.Equals(target)))
- {
- r.Result = i;
- return;
- }
- });
- }
- else
- {
- foreach (var n in nodesToSearch)
- {
- var units = JTSServices.UnitService.GetUnitsAt(n.Location, new[] { target.Faction });
- if (units.Any(u => u.Equals(target)))
- {
- r.Result = i;
- continue;
- }
- }
- }
- }
- // Unit was not within the max distance
- if (r.Result == null)
- {
- r.Status = ResultStatus.FAILURE;
- r.Messages.Add("Target was not within the provided distance.");
- }
- return r;
- }
- // Battle
- [OperationBehavior]
- public IServiceResult<IUnit> GetPrimeUnitTargetForUnit(List<IUnit> candidates,
- IUnit attacker,
- BattleType battleType)
- {
- // Based solely on whether the unit can do battle and the highest unit target desirability
- // which is highly based on unit strength.
- // This SHOULD work as the current attacker should be the strongest in the queue.....
- // but....
- // TODO: This may need to change.....
- var r = new ServiceResult<IUnit> {Status = ResultStatus.SUCCESS};
- try
- {
- // pull only defenders that this attacker can do battle with (attacking)
- var shortList = candidates.Where(u => JTSServices.RulesService.UnitCanDoBattleWithUnit(attacker, u, battleType).Result).ToList();
-
- // No targets, return
- if (shortList.Count == 0)
- {
- r.Status = ResultStatus.FAILURE;
- r.Messages.Add("No suitable target found for {0}".F(attacker.Name));
- }
- // Remote battle attackers will target other remote types with remaining fire points first
- // TODO: this should really be getting the unit with the max remaining fire points
- // If none are found, then it gets the best target as normal.
- if (battleType == BattleType.BARRAGE)
- {
- var remoteUnit = shortList.OrderByDescending(u => u.GetUnitTargetDesirabilityFactor())
- .FirstOrDefault(u => u.CurrentMoveStats.RemoteFirePoints > 0);
-
- r.Result = remoteUnit;
- }
- if (r.Result == null)
- r.Result = shortList.OrderByDescending(u => u.GetUnitTargetDesirabilityFactor()).FirstOrDefault();
- }
- catch (Exception ex)
- {
- r.Status = ResultStatus.EXCEPTION;
- r.ex = ex;
- return r;
- }
- return r;
- }
- [OperationBehavior]
- public IServiceResult<List<ISkirmish>> CreateFullCombatSkirmishes(IBattle battle)
- {
- var attackers = new List<IUnit>();
- var defenders = new List<IUnit>();
- battle.Attackers.ForEach(u => attackers.Add(u));
- battle.Defenders.ForEach(u => defenders.Add(u));
- var r = new ServiceResult<List<ISkirmish>>
- {
- Status = ResultStatus.SUCCESS,
- Result = new List<ISkirmish>()
- };
- // ---------------------------------------------------------------
- // Handle remote attacks
- // ---------------------------------------------------------------
- if (battle.BattleType == BattleType.BARRAGE)
- {
- // Remove any attackers that can no longer fire
- attackers.ForEach(u =>
- {
- if (u.CurrentMoveStats.RemoteFirePoints == 0) attackers.Remove(u);
- });
- }
- // ------------------------------------------------------------------
- // Handle removing all attackers that have already engaged this turn
- // ------------------------------------------------------------------
- attackers.ForEach(u =>
- {
- if (!u.CanDoBattleThisTurn()) attackers.Remove(u);
- });
- attackers.OrderByDescending(u => u.GetUnitTargetDesirabilityFactor());
- // make sure the attackers are ordered by strength to get them in highest to lowest order
- Action<IUnit> componentAction = a =>
- {
- lock (defenders)
- {
- var defenderResult = GetPrimeUnitTargetForUnit(defenders, a, battle.BattleType);
- if (defenderResult.Status == ResultStatus.SUCCESS)
- {
- // a suitable defender was found
- var s = new Skirmish(a, defenderResult.Result, battle, SkirmishType.FULL);
- defenders.Remove(defenderResult.Result);
- r.Result.Add(s);
- }
- }
- };
- // Create as many skirmishes as possible
- if (Convert.ToBoolean(ConfigurationManager.AppSettings["run_multithreaded"]))
- {
- Parallel.ForEach(attackers, componentAction);
- }
- else
- {
- foreach (var a in attackers)
- {
- componentAction(a);
- }
- }
- // If none could be created, battle is done
- if (!r.Result.Any())
- {
- r.Status = ResultStatus.FAILURE;
- r.Messages.Add("No skirmishes could be created with the provided units.");
- }
- return r;
- }
- [OperationBehavior]
- public IServiceResult<List<ISkirmish>> CreateAirDefenceSkirmishes(IBattle battle)
- {
- var attackers = new List<IUnit>();
- var defenders = new List<IUnit>();
- battle.Defenders.ForEach(u => defenders.Add(u));
- battle.Attackers.ForEach(u => attackers.Add(u));
- // Keep only our attacking air units
- attackers.RemoveAll(u => !(u.IsUnitBaseType("plane") || u.IsUnitBaseType("helicopter")));
- defenders.RemoveAll(u => !(u.IsUnitBaseType("AirDefence")));
- return CreateSpecialDefenceSkirmishes(attackers, defenders, battle, SkirmishType.AIR_DEFENCE);
- }
- [OperationBehavior]
- public IServiceResult<List<ISkirmish>> CreateMissileDefenceSkirmishes(IBattle battle)
- {
- var attackers = new List<IUnit>();
- var defenders = new List<IUnit>();
- battle.Defenders.ForEach(u => defenders.Add(u));
- battle.Attackers.ForEach(u => attackers.Add(u));
- // Keep only our attacking air units
- attackers.RemoveAll(u => !(u.IsUnitBaseType("missile")));
- return CreateSpecialDefenceSkirmishes(attackers, defenders, battle, SkirmishType.MISSILE_DEFENCE);
-
- }
- [OperationBehavior]
- private IServiceResult<List<ISkirmish>> CreateSpecialDefenceSkirmishes( List<IUnit> attackers,
- List<IUnit> defenders,
- IBattle battle,
- SkirmishType skirmishType)
- {
- var r = new ServiceResult<List<ISkirmish>>
- {
- Status = ResultStatus.SUCCESS,
- Result = new List<ISkirmish>()
- };
- // Remove all defenders that can not do battle with the attackers
- defenders.RemoveAll(d => attackers.Any(a => !JTSServices.RulesService.UnitCanDoBattleWithUnit(d, a, battle.BattleType).Result));
- // Create as many skirmishes as possible
- Action<IUnit> componentAction = a =>
- {
- lock (defenders)
- lock (attackers)
- {
- defenders.ForEach(d =>
- {
- var s = new Skirmish(a, d, battle, skirmishType);
- r.Result.Add(s);
- defenders.Remove(d);
- });
- }
- };
- if (Convert.ToBoolean(ConfigurationManager.AppSettings["run_multithreaded"]))
- {
- Parallel.ForEach(attackers, componentAction);
- }
- else
- {
- foreach (var a in attackers)
- {
- componentAction(a);
- }
- }
- // If none could be created, battle is done
- if (!r.Result.Any())
- {
- r.Status = ResultStatus.FAILURE;
- r.Messages.Add("No skirmishes could be created with the provided units.");
- }
- return r;
- }
- [OperationBehavior]
- public IServiceResult<ISkirmish> ResolveSkirmish(ISkirmish skirmish, BattleType battleType)
- {
- // -------------------------------------------------------
- // Simultaneous play a'la Axis and Allies ----------------
- // -------------------------------------------------------
- // -----------------------------------------------------------------------------------------------
- // This should work with both local and remote battle
- // should work with all skirmish types
- // Logic is that local battle will have a 0 distance to target for all units so the RemoteFirePoints
- // will remain at 0
- // Whereas remote battle will inherently take into account the remote distance
- // and adjust RemoteFirePoints in the cache accordingly
- // -----------------------------------------------------------------------------------------------
- var r = new ServiceResult<ISkirmish>();
- r.Result = skirmish;
- r.Status = ResultStatus.SUCCESS;
- try
- {
- var roundInfo = new SkirmishRoundInfo();
- roundInfo.Skirmish = skirmish;
- // Get these up front.
- // This is an expensive method and they don't change within the scope of a skirmish
- roundInfo.AttackerNetAttackValue = skirmish.Attacker.GetFullNetAttackValue();
- // Special Defence skirmish has the defenders on the offensive for this fir round
- roundInfo.DefenderNetDefenceValue = (skirmish.Type == SkirmishType.FULL)
- ? skirmish.Defender.GetFullNetDefenceValue()
- : skirmish.Defender.GetFullNetAttackValue();
- // while no victor
- while(skirmish.Destroyed.Count == 0)
- {
- // Check for battle compatibility -- this includes accounting for distance
- roundInfo.AttackerCanDoBattle = JTSServices.RulesService.UnitCanDoBattleWithUnit(skirmish.Attacker, skirmish.Defender, battleType).Result;
- roundInfo.DefenderCanDoBattle = JTSServices.RulesService.UnitCanDoBattleWithUnit(skirmish.Defender, skirmish.Attacker, battleType).Result;
-
- // Check for special Defence skirmish. Attackers can not fire.
- roundInfo.AttackerCanDoBattle = (skirmish.Type == SkirmishType.FULL);
- // Default for local battle
- roundInfo.DistanceToTarget = 0;
- // This isn't actually necessary as local battles will always have a distance of 0
- // I'm leaving this in here to
- // a) as a gate to save some processing time
- // b) assist in testing where the sim may be assembling units at different locations to simulate a local battle
- if (battleType == BattleType.BARRAGE)
- {
- // We'll need to know this during remote battle
- // Is the target within the attackers attack radius and if so, how far?
- // If not abandon the skirmish with a stalemate
- var distanceToTargetResult = CalculateNodeCountToUnit(skirmish.Attacker, skirmish.Defender, skirmish.Attacker.CurrentMoveStats.RemoteFirePoints);
- if (distanceToTargetResult.Status == ResultStatus.FAILURE)
- {
- r.Status = ResultStatus.FAILURE;
- r.Messages.Add("{0} is not within attack radius of {1}.".F(skirmish.Defender.Name, skirmish.Attacker.Name));
- return r;
- }
- // Necessary to decrement cache RemoteFirePoints after remote firing
- roundInfo.DistanceToTarget = Convert.ToInt32(distanceToTargetResult.Result);
- }
- // -------------------------- Attacker roll
- var attackResult = AttackRoll(roundInfo);
- roundInfo.Skirmish.DefenderEvaded = attackResult.StealthEffective;
- if (!attackResult.ContinueSkirmish) return r; // break out of the skirmish loop
- // --------------------------- Defender roll
- var defendResult = DefendRoll(roundInfo);
- roundInfo.Skirmish.DefenderEvaded = defendResult.StealthEffective;
- if (!defendResult.ContinueSkirmish) return r; // break out of the skirmish loop
- // --------------------------- Finally
- // Special Defence skirmish - attacker does not fire
- if (skirmish.Type != SkirmishType.FULL) return r;
- }
- }
- catch (Exception ex)
- {
- r.Status = ResultStatus.EXCEPTION;
- r.ex = ex;
- }
- return r;
- }
- [OperationBehavior]
- public IServiceResult<INode> ClaimNodeForVictorFaction(List<IUnit> units, INode node)
- {
- var r = new ServiceResult<INode>{Status = ResultStatus.SUCCESS};
- if (units == null || units.Count == 0)
- {
- r.Status = ResultStatus.FAILURE;
- r.Messages.Add("Can not claim node for faction with no units present.");
- return r;
- }
- var country = units.First().Country;
- var enemiesAtNode = node.GetAllUnits().Any(u => !u.Faction.Equals(country.Faction));
- if (enemiesAtNode)
- {
- r.Status = ResultStatus.FAILURE;
- r.Messages.Add("Can not claim node for faction. There are still enemies present");
- return r;
- }
- var canClaimNode = units.Any(u => u.CanClaimLocationForFaction());
- if (!canClaimNode)
- {
- r.Status = ResultStatus.FAILURE;
- r.Messages.Add("No occupying units are able to claim this node.");
- return r;
- }
- if (!enemiesAtNode && !node.Faction.Equals(country.Faction))
- {
- node.Country = country;
- JTSServices.NodeService.UpdateNodes(new List<INode> { node });
- }
- return r;
- }
- // AI player control
- [OperationBehavior]
- public IServiceResult<StrategicAssessmentInfo> DetermineTileStrategicValue(ITile tile)
- {
- // 1:
- // AssessmentInfos calculate as follows:
- // tile's comparison value is equal to (the net for that metric - the lowest value for that metric on the board)
- // the comparison value range is the range between the lowest and highest values for that metric on the board (Max - Min)
- // This gives us an absolte comparative value for any given tile between the lowest and the highest metric values on the board
- // highest and lowest metric values are calculated on game start up.
- var r = new ServiceResult<StrategicAssessmentInfo> {Status = ResultStatus.SUCCESS};
- var info = new StrategicAssessmentInfo();
- var defenseValue = (tile.GetNetDefenceAdjustment() - TheGame.GameBoard.StrategicValuesAttributes.Defense.Min);
- var offenseValue = (tile.GetNetAttackAdjustment() - TheGame.GameBoard.StrategicValuesAttributes.Offense.Min);
- var stealthValue = (tile.GetNetStealthAdjustment() - TheGame.GameBoard.StrategicValuesAttributes.Stealth.Min);
- var MovementValue = (tile.GetNetMovementAdjustment() - TheGame.GameBoard.StrategicValuesAttributes.Movement.Min);
- // Determine Defensive Value
- info.DefensibleRating = ConvertToStrategicAssessmentRating(defenseValue, TheGame.GameBoard.StrategicValuesAttributes.Defense.Range);
- // Determine Offensive Value
- info.OffensibleRating = ConvertToStrategicAssessmentRating(offenseValue, TheGame.GameBoard.StrategicValuesAttributes.Offense.Range);
- // Determing Stealth Value
- info.StealthRating = ConvertToStrategicAssessmentRating(stealthValue, TheGame.GameBoard.StrategicValuesAttributes.Stealth.Range);
- // Determine Movement Value
- info.MovementRating = ConvertToStrategicAssessmentRating(MovementValue, TheGame.GameBoard.StrategicValuesAttributes.Movement.Range);
- // Determine Other Values
- // -- Strategic Chokepoint
- info.OtherAggragateRating = (tile.IsGeographicChokePoint) ? StrategicAssessmentRating.HIGH : StrategicAssessmentRating.NONE;
- r.Result = info;
- return r;
- }
- #region Private Methods
- /// <summary>
- /// Perform game operations for the attacker's skirmish round
- /// </summary>
- /// <param name="roundInfo"></param>
- /// <param name="ctx"></param>
- /// <returns></returns>
- private SkirmishRollResult AttackRoll(SkirmishRoundInfo roundInfo)
- {
- var bpv = DataRepository.GetGameBasePointValues();
- var r = new SkirmishRollResult();
- var attackerRoll = Die.Roll(bpv.CombatRoll);
- // Decrement RemoteFirePoints hit or miss - if necessary
- // DistanceToTarget is just 0 for local battle
- if (roundInfo.Skirmish.Attacker.CurrentMoveStats.RemoteFirePoints > 0)
- roundInfo.Skirmish.Attacker.CurrentMoveStats.RemoteFirePoints -= roundInfo.DistanceToTarget;
- if (attackerRoll <= roundInfo.AttackerNetAttackValue && roundInfo.AttackerCanDoBattle) // Attack Hit
- {
- r.MedicalEffective = MedicalEffective(roundInfo.Skirmish.Defender);
- r.StealthEffective = StealthEffective(roundInfo.Skirmish.Defender);
- // Check for medical effective and stealth evasion for defender
- // This is the only logical variance for stealth. It's assumed that once the attacker fires, they have lost any stealth
- // advantage. Conversely, when the defender fires back (DefendRoll), they have also lost any stealth advantage for
- // this round... regained on the next round.
- if (!r.MedicalEffective && !r.StealthEffective)
- {
- roundInfo.Skirmish.Destroyed.Add(roundInfo.Skirmish.Defender);
- // Defending roll only if not medical effective
- var defenderRoll = Die.Roll(bpv.CombatRoll);
- //Decrement RemoteFirePoints hit or miss - if necessary
- if (roundInfo.Skirmish.Defender.CurrentMoveStats.RemoteFirePoints > 0)
- roundInfo.Skirmish.Defender.CurrentMoveStats.RemoteFirePoints -= roundInfo.DistanceToTarget;
- if (defenderRoll <= roundInfo.DefenderNetDefenceValue && roundInfo.DefenderCanDoBattle) // Defence hit
- {
- // Check for medical effective for attacker. Attackers are assumed to have revealed their position, so no stealth evasion.
- if (!MedicalEffective(roundInfo.Skirmish.Attacker))
- roundInfo.Skirmish.Destroyed.Add(roundInfo.Skirmish.Attacker);
- else
- roundInfo.Skirmish.Victor = roundInfo.Skirmish.Attacker;
- }
- else
- {
- roundInfo.Skirmish.Victor = roundInfo.Skirmish.Attacker;
- }
- }
- // If the defender or both are destroyed,
- // or this is a special Defence skirmish (one fire round), skirmish ends
- r.DestroyedUnits = roundInfo.Skirmish.Destroyed.Any();
- return r;
- }
- // Default is to continue the skirmish
- return r;
- }
- /// <summary>
- /// Perform game operations for the defender's skirmish round
- /// </summary>
- /// <param name="roundInfo"></param>
- /// <param name="ctx"></param>
- private SkirmishRollResult DefendRoll(SkirmishRoundInfo roundInfo)
- {
- var bpv = DataRepository.GetGameBasePointValues();
- var r = new SkirmishRollResult();
- var defenderRoll = Die.Roll(bpv.CombatRoll);
- // Decrement RemoteFirePoints hit or miss - if necessary
- // DistanceToTarget is just 0 for local battle
- if (roundInfo.Skirmish.Defender.CurrentMoveStats.RemoteFirePoints > 0)
- roundInfo.Skirmish.Defender.CurrentMoveStats.RemoteFirePoints -= roundInfo.DistanceToTarget;
- if (defenderRoll <= roundInfo.DefenderNetDefenceValue && roundInfo.DefenderCanDoBattle) // Defence Hit
- {
- r.MedicalEffective = MedicalEffective(roundInfo.Skirmish.Attacker);
- // Attacker has medical support... defender has fired, so no stealth advantage this round
- if (!r.MedicalEffective)
- {
- // Attacker is removed immediately
- roundInfo.Skirmish.Destroyed.Add(roundInfo.Skirmish.Attacker);
- roundInfo.Skirmish.Victor = roundInfo.Skirmish.Defender;
- r.DestroyedUnits = true;
- }
- }
- return r;
- }
- /// <summary>
- /// Checks to see if medical is available and attempts to recover the unit if so
- /// </summary>
- /// <param name="ctx"></param>
- /// <param name="unit"></param>
- /// <returns></returns>
- private bool MedicalEffective(IUnit unit)
- {
- var bpv = DataRepository.GetGameBasePointValues();
- if (!unit.HasMedicalSupport()) return false;
- var medicalRoll = Die.Roll(bpv.CombatRoll);
- return (medicalRoll <= bpv.MedicalSupportBase);
- }
- private bool StealthEffective(IUnit unit)
- {
- var bpv = DataRepository.GetGameBasePointValues();
- var stealthRoll = Die.Roll(bpv.StealthRoll);
- return (stealthRoll <= unit.GetFullNetStealthValue());
- }
- /// <summary>
- /// Reconstructs a path from the target object back to the source object as a collection of PathNodes
- /// </summary>
- /// <param name="o"></param>
- /// <returns></returns>
- private IEnumerable<IPathNode> ReconstructPath(IPathableObject o)
- {
- List<PathNode> path = new List<PathNode>();
- while (o.G != 0)
- {
- var n = JTSServices.NodeService.GetNodeAt(o.Location).Clone().ToPathableObject<PathNode>();
- path.Add(n as PathNode);
- o = o.Parent;
- }
- // Add current node
- path.Add(JTSServices.NodeService.GetNodeAt(o.Location).Clone().ToPathableObject<PathNode>() as PathNode);
- return path;
- }
- private StrategicAssessmentRating ConvertToStrategicAssessmentRating(double value, double range)
- {
- if (!(range > 0)) return StrategicAssessmentRating.NONE;
- var items = Enum.GetValues(typeof(StrategicAssessmentRating));
- var enumUpper = Convert.ToInt32(items.GetValue(items.GetUpperBound(0)));
- var r = (int)Math.Round(Convert.ToDouble(((value * enumUpper)) / range));
- return (StrategicAssessmentRating)r;
- }
- #endregion
- #endregion
-
- }
- sealed class SkirmishRoundInfo
- {
- public ISkirmish Skirmish { get; set; }
- public double AttackerNetAttackValue { get; set; }
- public double DefenderNetDefenceValue { get; set; }
- public bool AttackerCanDoBattle { get; set; }
- public bool DefenderCanDoBattle { get; set; }
- public int DistanceToTarget { get; set; }
- }
- /// <summary>
- /// Used to return messages through to the skirmish
- /// </summary>
- sealed class SkirmishRollResult
- {
- public bool DestroyedUnits { get; set; }
- public bool MedicalEffective { get; set; }
- public bool StealthEffective { get; set; }
- public bool ContinueSkirmish { get {return !DestroyedUnits && !StealthEffective;} }
- public SkirmishRollResult()
- {
- DestroyedUnits = false; // Default to continue skirmish;
- MedicalEffective = false;
- StealthEffective = false;
- }
- }
- }