PageRenderTime 55ms CodeModel.GetById 15ms RepoModel.GetById 0ms app.codeStats 0ms

/JTacticalSim.Service/AIService.cs

https://github.com/Queztionmark/JTacticalSim
C# | 1150 lines | 767 code | 210 blank | 173 comment | 106 complexity | 76d8608981e013bc0874e9eebeded0b6 MD5 | raw file
  1. using System;
  2. using System.Configuration;
  3. using System.Threading.Tasks;
  4. using System.Collections.Generic;
  5. using System.Linq;
  6. using System.Transactions;
  7. using System.ServiceModel;
  8. using JTacticalSim.API;
  9. using JTacticalSim.API.Component;
  10. using JTacticalSim.API.Data;
  11. using JTacticalSim.API.InfoObjects;
  12. using JTacticalSim.API.Service;
  13. using JTacticalSim.Component.Battle;
  14. using JTacticalSim.Service;
  15. using JTacticalSim.Utility;
  16. using JTacticalSim.DataContext.Repository;
  17. using ctxUtil = JTacticalSim.DataContext.Utility;
  18. using JTacticalSim.Component.GameBoard;
  19. namespace JTacticalSim.Service
  20. {
  21. [ServiceBehavior]
  22. public sealed class AIService : BaseGameService, IAIService
  23. {
  24. static readonly object padlock = new object();
  25. private static volatile IAIService _instance = null;
  26. public static IAIService Instance
  27. {
  28. get
  29. {
  30. if (_instance == null)
  31. {
  32. lock (padlock)
  33. if (_instance == null) _instance = new AIService();
  34. }
  35. return _instance;
  36. }
  37. }
  38. // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  39. private AIService()
  40. {}
  41. #region Service Methods
  42. // Unit Orders
  43. [OperationBehavior]
  44. public IServiceResult<IUnit> DeployUnitsFromTransportToNode(IUnit transport, IEnumerable<IUnit> units, INode destinationNode)
  45. {
  46. var r = new ServiceResult<IUnit>{Status = ResultStatus.SUCCESS};
  47. r.Messages.Add("All units deployed.");
  48. var currentNode = transport.GetNode();
  49. // Deployment node must be only 1 space from water transport : same space for other types
  50. // Units can be moved after if they have remaining movement points
  51. var deployDistance = RulesService.Instance.GetAllowableDeployDistanceForTransport(transport).Result;
  52. var distanceToNodeResult = CalculateNodeCountToNode(currentNode, destinationNode, deployDistance);
  53. if (distanceToNodeResult.Status == ResultStatus.FAILURE)
  54. {
  55. r.Status = ResultStatus.FAILURE;
  56. r.Messages.Add("Deployment node is not within allowed deployment distance.");
  57. r.FailedObjects.AddRange(units);
  58. return r;
  59. }
  60. if (!transport.GetTransportedUnits().Any())
  61. {
  62. r.Status = ResultStatus.FAILURE;
  63. r.Messages.Add("No units currently transported.");
  64. return r;
  65. }
  66. using (var txn = new TransactionScope())
  67. {
  68. Action<IUnit> componentAction = u =>
  69. {
  70. // Deploy the unit if the destination node is compatible and
  71. // the unit is actually being transported,
  72. if (!transport.GetTransportedUnits().Any(tu => tu.Equals(u)) || !JTSServices.RulesService.UnitIsDeployableToNode(u, destinationNode).Result)
  73. {
  74. r.FailedObjects.Add(u);
  75. return;
  76. }
  77. u.MoveToLocation(destinationNode, currentNode);
  78. r.SuccessfulObjects.Add(u);
  79. // Remove from the transport table
  80. JTSServices.DataService.RemoveUnitTransport(u, transport);
  81. };
  82. if (Convert.ToBoolean(ConfigurationManager.AppSettings["run_multithreaded"]))
  83. {
  84. Parallel.ForEach(units, componentAction);
  85. }
  86. else
  87. {
  88. units.ToList().ForEach(u =>
  89. {
  90. componentAction(u);
  91. });
  92. }
  93. JTSServices.UnitService.UpdateUnits(new List<IUnit> { transport });
  94. txn.Complete();
  95. }
  96. if (!r.SuccessfulObjects.Any())
  97. {
  98. r.Status = ResultStatus.FAILURE;
  99. r.Messages.Add("No units were deployed due to incompatibility with the deployment node.");
  100. }
  101. // If there are any failed objects and some successful objects return a SOME_FAILURE result
  102. if (r.FailedObjects.Any() && r.SuccessfulObjects.Any())
  103. {
  104. r.Status = ResultStatus.SOME_FAILURE;
  105. r.Messages.Add("Some units were not deployed due to incompatibility with the deployment node.");
  106. }
  107. return r;
  108. }
  109. [OperationBehavior]
  110. public IServiceResult<IUnit> LoadUnitsToTransport(IUnit transport, List<IUnit> units)
  111. {
  112. var r = new ServiceResult<IUnit>{Status = ResultStatus.SUCCESS};
  113. INode transportNode = transport.GetNode();
  114. // units to load must be only 1 space from water transport : same space for other types
  115. // Units can be moved before they're loaded onto the transport
  116. var loadDistance = RulesService.Instance.GetAllowableLoadDistanceForTransport(transport).Result;
  117. var distanceToNodeResult = CalculateNodeCountToNode(units.FirstOrDefault().GetNode(), transportNode, loadDistance);
  118. if (distanceToNodeResult.Status == ResultStatus.FAILURE)
  119. {
  120. r.Status = ResultStatus.FAILURE;
  121. r.Messages.Add("Units are not within maximum load distance of transport.");
  122. r.FailedObjects.AddRange(units);
  123. return r;
  124. }
  125. // Prune incompatible units for this transport
  126. units.RemoveAll(u => !u.IsCompatibleWithTransport(transport));
  127. // Prune any units already transporting other units
  128. units.RemoveAll(u => u.GetTransportedUnits().Any());
  129. if (!units.Any())
  130. {
  131. r.Status = ResultStatus.FAILURE;
  132. r.Messages.Add("No units selected can be loaded on this transport.");
  133. r.FailedObjects.AddRange(units);
  134. return r;
  135. }
  136. // Validate weight limits
  137. var totalTransportWeightToLoad = units.Sum(u => u.TotalUnitWeight);
  138. var totalTransportWeight = totalTransportWeightToLoad + (transport.GetTransportedUnits().Sum(u => u.GetWeight()));
  139. if (transport.GetAllowableTransportWeight() < totalTransportWeight)
  140. {
  141. r.Status = ResultStatus.FAILURE;
  142. r.Messages.Add("Total unit weight exceeds allowable transport weight.");
  143. r.FailedObjects.AddRange(units);
  144. return r;
  145. }
  146. using (var txn = new TransactionScope())
  147. {
  148. Action<IUnit> componentAction = u =>
  149. {
  150. if (!JTSServices.RulesService.UnitCanTransportUnitTypeAndClass(transport, u).Result)
  151. {
  152. r.FailedObjects.Add(u);
  153. return;
  154. }
  155. u.LoadToLocation(transportNode, u.GetNode());
  156. JTSServices.DataService.SaveUnitTransport(u, transport);
  157. r.SuccessfulObjects.Add(u);
  158. };
  159. if (Convert.ToBoolean(ConfigurationManager.AppSettings["run_multithreaded"]))
  160. {
  161. Parallel.ForEach(units, componentAction);
  162. }
  163. else
  164. {
  165. units.ForEach(u =>
  166. {
  167. componentAction(u);
  168. });
  169. }
  170. JTSServices.UnitService.UpdateUnits(new List<IUnit> { transport });
  171. txn.Complete();
  172. }
  173. if (!r.SuccessfulObjects.Any())
  174. {
  175. r.Status = ResultStatus.FAILURE;
  176. r.Messages.Add("No units were loaded due to incompatiblity with the transport type.");
  177. return r;
  178. }
  179. // If there are any failed objects and some successful objects return a SOME_FAILURE result
  180. if (r.FailedObjects.Any() && r.SuccessfulObjects.Any())
  181. {
  182. r.Status = ResultStatus.SOME_FAILURE;
  183. r.Messages.Add("Some units were not loaded due to incompatibility with the transport type.");
  184. return r;
  185. }
  186. r.Messages.Add("All units loaded.");
  187. return r;
  188. }
  189. [OperationBehavior]
  190. public IServiceResult<IUnit> AttachUnitToUnit (IUnit parent, IUnit unit)
  191. {
  192. var r = new ServiceResult<IUnit>{Status = ResultStatus.SUCCESS, Result = unit};
  193. // First, check that we can attach to the parent
  194. var canAttach = JTSServices.RulesService.UnitCanAttachToUnit(unit, parent);
  195. if (!canAttach.Result)
  196. {
  197. r.Status = ResultStatus.FAILURE;
  198. r.Messages.Add(canAttach.Message);
  199. r.FailedObjects.Add(unit);
  200. return r;
  201. }
  202. // First, make sure we're not already attached to a unit (can only be attached to one)
  203. if (unit.AttachedToUnit != null)
  204. {
  205. r.Status = ResultStatus.FAILURE;
  206. r.Messages.Add("Unit already attached. You must detach from the current unit first.");
  207. r.FailedObjects.Add(unit);
  208. return r;
  209. }
  210. var saveResult = JTSServices.DataService.SaveUnitAssignment(unit, parent);
  211. r.ConvertServiceResultData(saveResult);
  212. r.Messages.Add("Unit attached.");
  213. return r;
  214. }
  215. [OperationBehavior]
  216. public IServiceResult<IUnit> DetachUnitFromUnit(IUnit unit)
  217. {
  218. var r = new ServiceResult<IUnit>{Status = ResultStatus.SUCCESS, Result = unit};
  219. // Can only detach if we are, indeed, attached. Makes sense...
  220. if (unit.AttachedToUnit == null)
  221. {
  222. r.Status = ResultStatus.FAILURE;
  223. r.Messages.Add("Unit not currently attached.");
  224. r.FailedObjects.Add(unit);
  225. return r;
  226. }
  227. var saveResult = JTSServices.DataService.RemoveUnitAssignment(unit, unit.AttachedToUnit);
  228. r.ConvertServiceResultData(saveResult);
  229. r.Messages.Add("Unit detached.");
  230. return r;
  231. }
  232. // Pathfinding
  233. /// <summary>
  234. /// A* Implementation
  235. /// </summary>
  236. /// <param name="source"></param>
  237. /// <param name="faction"></param>
  238. /// <param name="supplyDistance"></param>
  239. /// <returns></returns>
  240. [OperationBehavior]
  241. public RouteInfo FindSupplyPath(INode source, IFaction faction, int supplyDistance)
  242. {
  243. // Use only friendly nodes
  244. var map = JTSServices.NodeService.GetAllNodesWithinDistance(source, supplyDistance, false, false)
  245. .Where(n => n.Faction.Equals(faction));
  246. // First, check to see if we have any targets
  247. var targetNodes = (from node in map
  248. where node.TotalUnitCount() > 0
  249. let units = JTSServices.UnitService.GetUnitsAt(node.Location, new[] {faction})
  250. where units.Any(u => u.IsUnitClass("supply"))
  251. select node).ToList();
  252. // Skip if we don't have any supply units
  253. if (!targetNodes.Any())
  254. return null;
  255. var paths = new List<RouteInfo>();
  256. Action<INode> componentAction = n =>
  257. {
  258. paths.Add(FindPathToSupplyTarget(source, n, map, supplyDistance));
  259. };
  260. if (Convert.ToBoolean(ConfigurationManager.AppSettings["run_multithreaded"]))
  261. {
  262. Parallel.ForEach(targetNodes, componentAction);
  263. }
  264. else
  265. {
  266. targetNodes.ForEach(n =>
  267. {
  268. componentAction(n);
  269. });
  270. }
  271. return paths.FirstOrDefault(p => p != null);
  272. }
  273. private RouteInfo FindPathToSupplyTarget(INode source,
  274. INode target,
  275. IEnumerable<INode> map,
  276. int supplyDistance)
  277. {
  278. source.Target = target;
  279. var openList = new Queue<INode>();
  280. var closedList = new Queue<INode>();
  281. openList.Enqueue(source);
  282. while (openList.Any())
  283. {
  284. // Pushes the Lower F Value nodes to the top.
  285. openList.Sort();
  286. var currentNode = openList.Dequeue();
  287. currentNode.Source = source;
  288. // Check each node if it is faction for the path we're looking for
  289. // and see if it has any units
  290. if (currentNode.Equals(target))
  291. {
  292. return new RouteInfo(ReconstructPath(currentNode), source, target);
  293. }
  294. //openList.Remove(currentNode);
  295. closedList.Enqueue(currentNode);
  296. // Get all surrounding nodes that are also in the master move map
  297. var neighborNodes = JTSServices.NodeService.GetAllNodesAtDistance(currentNode, 1, false)
  298. .Where(n => map.Any(mn => mn.Equals(n)))
  299. .Where(n => n.G <= supplyDistance)
  300. .ToList();
  301. // remove nodes if exist in the closed list
  302. neighborNodes.ForEach(n1 =>
  303. {
  304. if (closedList.Any(cn => cn.Equals(n1))) neighborNodes.RemoveAll(n2 => n1.Equals(n2));
  305. });
  306. neighborNodes.ForEach(openList.Enqueue);
  307. }
  308. // No available path
  309. return null;
  310. }
  311. /// <summary>
  312. /// A* Implementation
  313. /// The map param has already been pruned to include only traverseable nodes for the unit
  314. /// </summary>
  315. /// <param name="source"></param>
  316. /// <param name="target"></param>
  317. /// <param name="map"></param>
  318. /// <param name="unit"></param>
  319. /// <returns></returns>
  320. [OperationBehavior]
  321. public IServiceResult<RouteInfo> FindPath(INode source,
  322. INode target,
  323. IEnumerable<IPathableObject> map,
  324. IUnit unit)
  325. {
  326. var r = new ServiceResult<RouteInfo>() {Status = ResultStatus.SUCCESS};
  327. // If the selected node is outside the movement bounds for the unit, save some processing
  328. var distanceToTarget = CalculateNodeCountToNode(source, target, unit.CurrentMoveStats.MovementPoints).Result;
  329. if (distanceToTarget == null || Convert.ToInt32(distanceToTarget) > unit.CurrentMoveStats.MovementPoints)
  330. {
  331. r.Status = ResultStatus.FAILURE;
  332. r.Messages.Add("Target node is outside the current movement bounds for the selected unit.");
  333. r.Result = null;
  334. return r;
  335. }
  336. if (!map.Any(n => n.Equals(target)))
  337. {
  338. r.Status = ResultStatus.FAILURE;
  339. r.Messages.Add("target node is not in searchable nodes.");
  340. r.Result = null;
  341. return r;
  342. }
  343. var mapList = map.ToList();
  344. try
  345. {
  346. source.Target = target;
  347. var openList = new Queue<INode>();
  348. var closedList = new Queue<INode>();
  349. openList.Enqueue(source);
  350. while (openList.Any())
  351. {
  352. // Pushes the Lower F Value nodes to the top.
  353. // When we're searching for all possible routes, we need to
  354. // allow for for all nodes, so we take the highest weight.
  355. // When looking for the shortest path we want the lowest weighted node
  356. openList.Sort();
  357. var currentNode = openList.Dequeue();
  358. currentNode.Source = source;
  359. // Magic sauce. We've reached the target, return the path
  360. if (currentNode.Equals(target))
  361. {
  362. r.Result = new RouteInfo(ReconstructPath(currentNode), source, target);
  363. return r;
  364. }
  365. closedList.Enqueue(currentNode);
  366. mapList.Remove(currentNode);
  367. // When searching for a new path, we don't want the unit's current node location to
  368. // hinder the finding of a path OFF of the current node. Otherwise, it's possible for a unit to actually
  369. // become 'stuck' on a node
  370. //var movementMod = (JTSServices.RulesService.TileHasMovementOverrideForUnit(unit, currentNode.DefaultTile()).Result)
  371. // ? 0
  372. // : currentNode.DefaultTile().GetNetMovementAdjustment();
  373. var movementMod = currentNode.DefaultTile().GetNetMovementAdjustment();
  374. // Get all surrounding nodes that are also in the master move map
  375. // but not further away than the selected unit can travel
  376. var allowable = JTSServices.NodeService.GetAllowableNeighborNodesForGrid(unit, currentNode, mapList);
  377. var neighborNodes = allowable.Where(n => n.G <= (unit.CurrentMoveStats.MovementPoints + movementMod + n.DefaultTile().GetNetMovementAdjustment()));
  378. var keepNodes = new List<INode>();
  379. keepNodes.AddRange(neighborNodes.Where(n => !Enumerable.Contains(closedList, n)));
  380. keepNodes.ForEach(openList.Enqueue);
  381. }
  382. }
  383. catch (Exception ex)
  384. {
  385. r.ex = ex;
  386. r.Status = ResultStatus.EXCEPTION;
  387. r.Result = null;
  388. return r;
  389. }
  390. // No available path
  391. r.Status = ResultStatus.FAILURE;
  392. r.Result = null;
  393. r.Messages.Add("No Available path from {0} to {1} for unit {2}".F(source.Location.ToStringForName(),
  394. target.Location.ToStringForName(),
  395. unit.Name));
  396. return r;
  397. }
  398. [OperationBehavior]
  399. public IServiceResult<int?> CalculateNodeCountToNode(INode source, INode target, int maxDistance)
  400. {
  401. var r = new ServiceResult<int?>(){Status = ResultStatus.SUCCESS};
  402. for (var i = 0; i <= maxDistance; i++)
  403. {
  404. var nodesToSearch = JTSServices.NodeService.GetNodesAtDistance(source.GetNode(), i);
  405. if (Convert.ToBoolean(ConfigurationManager.AppSettings["run_multithreaded"]))
  406. {
  407. Parallel.ForEach(nodesToSearch, n =>
  408. {
  409. if (n.LocationEquals(target.Location)) r.Result = i;
  410. return;
  411. });
  412. }
  413. else
  414. {
  415. foreach (var n in nodesToSearch)
  416. {
  417. if (n.Equals(target)) r.Result = i;
  418. continue;
  419. }
  420. }
  421. }
  422. // Unit was not within the max distance
  423. if (r.Result == null)
  424. {
  425. r.Status = ResultStatus.FAILURE;
  426. r.Messages.Add("Target was not within the provided distance.");
  427. }
  428. return r;
  429. }
  430. [OperationBehavior]
  431. public IServiceResult<int?> CalculateNodeCountToUnit(IUnit source, IUnit target, int maxDistance)
  432. {
  433. var r = new ServiceResult<int?>(){Status = ResultStatus.SUCCESS};
  434. for (var i = 1; i <= maxDistance; i++)
  435. {
  436. var nodesToSearch = JTSServices.NodeService.GetNodesAtDistance(source.GetNode(), i);
  437. if (Convert.ToBoolean(ConfigurationManager.AppSettings["run_multithreaded"]))
  438. {
  439. Parallel.ForEach(nodesToSearch, n =>
  440. {
  441. var units = JTSServices.UnitService.GetUnitsAt(n.Location, new[] { target.Faction });
  442. if (units.Any(u => u.Equals(target)))
  443. {
  444. r.Result = i;
  445. return;
  446. }
  447. });
  448. }
  449. else
  450. {
  451. foreach (var n in nodesToSearch)
  452. {
  453. var units = JTSServices.UnitService.GetUnitsAt(n.Location, new[] { target.Faction });
  454. if (units.Any(u => u.Equals(target)))
  455. {
  456. r.Result = i;
  457. continue;
  458. }
  459. }
  460. }
  461. }
  462. // Unit was not within the max distance
  463. if (r.Result == null)
  464. {
  465. r.Status = ResultStatus.FAILURE;
  466. r.Messages.Add("Target was not within the provided distance.");
  467. }
  468. return r;
  469. }
  470. // Battle
  471. [OperationBehavior]
  472. public IServiceResult<IUnit> GetPrimeUnitTargetForUnit(List<IUnit> candidates,
  473. IUnit attacker,
  474. BattleType battleType)
  475. {
  476. // Based solely on whether the unit can do battle and the highest unit target desirability
  477. // which is highly based on unit strength.
  478. // This SHOULD work as the current attacker should be the strongest in the queue.....
  479. // but....
  480. // TODO: This may need to change.....
  481. var r = new ServiceResult<IUnit> {Status = ResultStatus.SUCCESS};
  482. try
  483. {
  484. // pull only defenders that this attacker can do battle with (attacking)
  485. var shortList = candidates.Where(u => JTSServices.RulesService.UnitCanDoBattleWithUnit(attacker, u, battleType).Result).ToList();
  486. // No targets, return
  487. if (shortList.Count == 0)
  488. {
  489. r.Status = ResultStatus.FAILURE;
  490. r.Messages.Add("No suitable target found for {0}".F(attacker.Name));
  491. }
  492. // Remote battle attackers will target other remote types with remaining fire points first
  493. // TODO: this should really be getting the unit with the max remaining fire points
  494. // If none are found, then it gets the best target as normal.
  495. if (battleType == BattleType.BARRAGE)
  496. {
  497. var remoteUnit = shortList.OrderByDescending(u => u.GetUnitTargetDesirabilityFactor())
  498. .FirstOrDefault(u => u.CurrentMoveStats.RemoteFirePoints > 0);
  499. r.Result = remoteUnit;
  500. }
  501. if (r.Result == null)
  502. r.Result = shortList.OrderByDescending(u => u.GetUnitTargetDesirabilityFactor()).FirstOrDefault();
  503. }
  504. catch (Exception ex)
  505. {
  506. r.Status = ResultStatus.EXCEPTION;
  507. r.ex = ex;
  508. return r;
  509. }
  510. return r;
  511. }
  512. [OperationBehavior]
  513. public IServiceResult<List<ISkirmish>> CreateFullCombatSkirmishes(IBattle battle)
  514. {
  515. var attackers = new List<IUnit>();
  516. var defenders = new List<IUnit>();
  517. battle.Attackers.ForEach(u => attackers.Add(u));
  518. battle.Defenders.ForEach(u => defenders.Add(u));
  519. var r = new ServiceResult<List<ISkirmish>>
  520. {
  521. Status = ResultStatus.SUCCESS,
  522. Result = new List<ISkirmish>()
  523. };
  524. // ---------------------------------------------------------------
  525. // Handle remote attacks
  526. // ---------------------------------------------------------------
  527. if (battle.BattleType == BattleType.BARRAGE)
  528. {
  529. // Remove any attackers that can no longer fire
  530. attackers.ForEach(u =>
  531. {
  532. if (u.CurrentMoveStats.RemoteFirePoints == 0) attackers.Remove(u);
  533. });
  534. }
  535. // ------------------------------------------------------------------
  536. // Handle removing all attackers that have already engaged this turn
  537. // ------------------------------------------------------------------
  538. attackers.ForEach(u =>
  539. {
  540. if (!u.CanDoBattleThisTurn()) attackers.Remove(u);
  541. });
  542. attackers.OrderByDescending(u => u.GetUnitTargetDesirabilityFactor());
  543. // make sure the attackers are ordered by strength to get them in highest to lowest order
  544. Action<IUnit> componentAction = a =>
  545. {
  546. lock (defenders)
  547. {
  548. var defenderResult = GetPrimeUnitTargetForUnit(defenders, a, battle.BattleType);
  549. if (defenderResult.Status == ResultStatus.SUCCESS)
  550. {
  551. // a suitable defender was found
  552. var s = new Skirmish(a, defenderResult.Result, battle, SkirmishType.FULL);
  553. defenders.Remove(defenderResult.Result);
  554. r.Result.Add(s);
  555. }
  556. }
  557. };
  558. // Create as many skirmishes as possible
  559. if (Convert.ToBoolean(ConfigurationManager.AppSettings["run_multithreaded"]))
  560. {
  561. Parallel.ForEach(attackers, componentAction);
  562. }
  563. else
  564. {
  565. foreach (var a in attackers)
  566. {
  567. componentAction(a);
  568. }
  569. }
  570. // If none could be created, battle is done
  571. if (!r.Result.Any())
  572. {
  573. r.Status = ResultStatus.FAILURE;
  574. r.Messages.Add("No skirmishes could be created with the provided units.");
  575. }
  576. return r;
  577. }
  578. [OperationBehavior]
  579. public IServiceResult<List<ISkirmish>> CreateAirDefenceSkirmishes(IBattle battle)
  580. {
  581. var attackers = new List<IUnit>();
  582. var defenders = new List<IUnit>();
  583. battle.Defenders.ForEach(u => defenders.Add(u));
  584. battle.Attackers.ForEach(u => attackers.Add(u));
  585. // Keep only our attacking air units
  586. attackers.RemoveAll(u => !(u.IsUnitBaseType("plane") || u.IsUnitBaseType("helicopter")));
  587. defenders.RemoveAll(u => !(u.IsUnitBaseType("AirDefence")));
  588. return CreateSpecialDefenceSkirmishes(attackers, defenders, battle, SkirmishType.AIR_DEFENCE);
  589. }
  590. [OperationBehavior]
  591. public IServiceResult<List<ISkirmish>> CreateMissileDefenceSkirmishes(IBattle battle)
  592. {
  593. var attackers = new List<IUnit>();
  594. var defenders = new List<IUnit>();
  595. battle.Defenders.ForEach(u => defenders.Add(u));
  596. battle.Attackers.ForEach(u => attackers.Add(u));
  597. // Keep only our attacking air units
  598. attackers.RemoveAll(u => !(u.IsUnitBaseType("missile")));
  599. return CreateSpecialDefenceSkirmishes(attackers, defenders, battle, SkirmishType.MISSILE_DEFENCE);
  600. }
  601. [OperationBehavior]
  602. private IServiceResult<List<ISkirmish>> CreateSpecialDefenceSkirmishes( List<IUnit> attackers,
  603. List<IUnit> defenders,
  604. IBattle battle,
  605. SkirmishType skirmishType)
  606. {
  607. var r = new ServiceResult<List<ISkirmish>>
  608. {
  609. Status = ResultStatus.SUCCESS,
  610. Result = new List<ISkirmish>()
  611. };
  612. // Remove all defenders that can not do battle with the attackers
  613. defenders.RemoveAll(d => attackers.Any(a => !JTSServices.RulesService.UnitCanDoBattleWithUnit(d, a, battle.BattleType).Result));
  614. // Create as many skirmishes as possible
  615. Action<IUnit> componentAction = a =>
  616. {
  617. lock (defenders)
  618. lock (attackers)
  619. {
  620. defenders.ForEach(d =>
  621. {
  622. var s = new Skirmish(a, d, battle, skirmishType);
  623. r.Result.Add(s);
  624. defenders.Remove(d);
  625. });
  626. }
  627. };
  628. if (Convert.ToBoolean(ConfigurationManager.AppSettings["run_multithreaded"]))
  629. {
  630. Parallel.ForEach(attackers, componentAction);
  631. }
  632. else
  633. {
  634. foreach (var a in attackers)
  635. {
  636. componentAction(a);
  637. }
  638. }
  639. // If none could be created, battle is done
  640. if (!r.Result.Any())
  641. {
  642. r.Status = ResultStatus.FAILURE;
  643. r.Messages.Add("No skirmishes could be created with the provided units.");
  644. }
  645. return r;
  646. }
  647. [OperationBehavior]
  648. public IServiceResult<ISkirmish> ResolveSkirmish(ISkirmish skirmish, BattleType battleType)
  649. {
  650. // -------------------------------------------------------
  651. // Simultaneous play a'la Axis and Allies ----------------
  652. // -------------------------------------------------------
  653. // -----------------------------------------------------------------------------------------------
  654. // This should work with both local and remote battle
  655. // should work with all skirmish types
  656. // Logic is that local battle will have a 0 distance to target for all units so the RemoteFirePoints
  657. // will remain at 0
  658. // Whereas remote battle will inherently take into account the remote distance
  659. // and adjust RemoteFirePoints in the cache accordingly
  660. // -----------------------------------------------------------------------------------------------
  661. var r = new ServiceResult<ISkirmish>();
  662. r.Result = skirmish;
  663. r.Status = ResultStatus.SUCCESS;
  664. try
  665. {
  666. var roundInfo = new SkirmishRoundInfo();
  667. roundInfo.Skirmish = skirmish;
  668. // Get these up front.
  669. // This is an expensive method and they don't change within the scope of a skirmish
  670. roundInfo.AttackerNetAttackValue = skirmish.Attacker.GetFullNetAttackValue();
  671. // Special Defence skirmish has the defenders on the offensive for this fir round
  672. roundInfo.DefenderNetDefenceValue = (skirmish.Type == SkirmishType.FULL)
  673. ? skirmish.Defender.GetFullNetDefenceValue()
  674. : skirmish.Defender.GetFullNetAttackValue();
  675. // while no victor
  676. while(skirmish.Destroyed.Count == 0)
  677. {
  678. // Check for battle compatibility -- this includes accounting for distance
  679. roundInfo.AttackerCanDoBattle = JTSServices.RulesService.UnitCanDoBattleWithUnit(skirmish.Attacker, skirmish.Defender, battleType).Result;
  680. roundInfo.DefenderCanDoBattle = JTSServices.RulesService.UnitCanDoBattleWithUnit(skirmish.Defender, skirmish.Attacker, battleType).Result;
  681. // Check for special Defence skirmish. Attackers can not fire.
  682. roundInfo.AttackerCanDoBattle = (skirmish.Type == SkirmishType.FULL);
  683. // Default for local battle
  684. roundInfo.DistanceToTarget = 0;
  685. // This isn't actually necessary as local battles will always have a distance of 0
  686. // I'm leaving this in here to
  687. // a) as a gate to save some processing time
  688. // b) assist in testing where the sim may be assembling units at different locations to simulate a local battle
  689. if (battleType == BattleType.BARRAGE)
  690. {
  691. // We'll need to know this during remote battle
  692. // Is the target within the attackers attack radius and if so, how far?
  693. // If not abandon the skirmish with a stalemate
  694. var distanceToTargetResult = CalculateNodeCountToUnit(skirmish.Attacker, skirmish.Defender, skirmish.Attacker.CurrentMoveStats.RemoteFirePoints);
  695. if (distanceToTargetResult.Status == ResultStatus.FAILURE)
  696. {
  697. r.Status = ResultStatus.FAILURE;
  698. r.Messages.Add("{0} is not within attack radius of {1}.".F(skirmish.Defender.Name, skirmish.Attacker.Name));
  699. return r;
  700. }
  701. // Necessary to decrement cache RemoteFirePoints after remote firing
  702. roundInfo.DistanceToTarget = Convert.ToInt32(distanceToTargetResult.Result);
  703. }
  704. // -------------------------- Attacker roll
  705. var attackResult = AttackRoll(roundInfo);
  706. roundInfo.Skirmish.DefenderEvaded = attackResult.StealthEffective;
  707. if (!attackResult.ContinueSkirmish) return r; // break out of the skirmish loop
  708. // --------------------------- Defender roll
  709. var defendResult = DefendRoll(roundInfo);
  710. roundInfo.Skirmish.DefenderEvaded = defendResult.StealthEffective;
  711. if (!defendResult.ContinueSkirmish) return r; // break out of the skirmish loop
  712. // --------------------------- Finally
  713. // Special Defence skirmish - attacker does not fire
  714. if (skirmish.Type != SkirmishType.FULL) return r;
  715. }
  716. }
  717. catch (Exception ex)
  718. {
  719. r.Status = ResultStatus.EXCEPTION;
  720. r.ex = ex;
  721. }
  722. return r;
  723. }
  724. [OperationBehavior]
  725. public IServiceResult<INode> ClaimNodeForVictorFaction(List<IUnit> units, INode node)
  726. {
  727. var r = new ServiceResult<INode>{Status = ResultStatus.SUCCESS};
  728. if (units == null || units.Count == 0)
  729. {
  730. r.Status = ResultStatus.FAILURE;
  731. r.Messages.Add("Can not claim node for faction with no units present.");
  732. return r;
  733. }
  734. var country = units.First().Country;
  735. var enemiesAtNode = node.GetAllUnits().Any(u => !u.Faction.Equals(country.Faction));
  736. if (enemiesAtNode)
  737. {
  738. r.Status = ResultStatus.FAILURE;
  739. r.Messages.Add("Can not claim node for faction. There are still enemies present");
  740. return r;
  741. }
  742. var canClaimNode = units.Any(u => u.CanClaimLocationForFaction());
  743. if (!canClaimNode)
  744. {
  745. r.Status = ResultStatus.FAILURE;
  746. r.Messages.Add("No occupying units are able to claim this node.");
  747. return r;
  748. }
  749. if (!enemiesAtNode && !node.Faction.Equals(country.Faction))
  750. {
  751. node.Country = country;
  752. JTSServices.NodeService.UpdateNodes(new List<INode> { node });
  753. }
  754. return r;
  755. }
  756. // AI player control
  757. [OperationBehavior]
  758. public IServiceResult<StrategicAssessmentInfo> DetermineTileStrategicValue(ITile tile)
  759. {
  760. // 1:
  761. // AssessmentInfos calculate as follows:
  762. // tile's comparison value is equal to (the net for that metric - the lowest value for that metric on the board)
  763. // the comparison value range is the range between the lowest and highest values for that metric on the board (Max - Min)
  764. // This gives us an absolte comparative value for any given tile between the lowest and the highest metric values on the board
  765. // highest and lowest metric values are calculated on game start up.
  766. var r = new ServiceResult<StrategicAssessmentInfo> {Status = ResultStatus.SUCCESS};
  767. var info = new StrategicAssessmentInfo();
  768. var defenseValue = (tile.GetNetDefenceAdjustment() - TheGame.GameBoard.StrategicValuesAttributes.Defense.Min);
  769. var offenseValue = (tile.GetNetAttackAdjustment() - TheGame.GameBoard.StrategicValuesAttributes.Offense.Min);
  770. var stealthValue = (tile.GetNetStealthAdjustment() - TheGame.GameBoard.StrategicValuesAttributes.Stealth.Min);
  771. var MovementValue = (tile.GetNetMovementAdjustment() - TheGame.GameBoard.StrategicValuesAttributes.Movement.Min);
  772. // Determine Defensive Value
  773. info.DefensibleRating = ConvertToStrategicAssessmentRating(defenseValue, TheGame.GameBoard.StrategicValuesAttributes.Defense.Range);
  774. // Determine Offensive Value
  775. info.OffensibleRating = ConvertToStrategicAssessmentRating(offenseValue, TheGame.GameBoard.StrategicValuesAttributes.Offense.Range);
  776. // Determing Stealth Value
  777. info.StealthRating = ConvertToStrategicAssessmentRating(stealthValue, TheGame.GameBoard.StrategicValuesAttributes.Stealth.Range);
  778. // Determine Movement Value
  779. info.MovementRating = ConvertToStrategicAssessmentRating(MovementValue, TheGame.GameBoard.StrategicValuesAttributes.Movement.Range);
  780. // Determine Other Values
  781. // -- Strategic Chokepoint
  782. info.OtherAggragateRating = (tile.IsGeographicChokePoint) ? StrategicAssessmentRating.HIGH : StrategicAssessmentRating.NONE;
  783. r.Result = info;
  784. return r;
  785. }
  786. #region Private Methods
  787. /// <summary>
  788. /// Perform game operations for the attacker's skirmish round
  789. /// </summary>
  790. /// <param name="roundInfo"></param>
  791. /// <param name="ctx"></param>
  792. /// <returns></returns>
  793. private SkirmishRollResult AttackRoll(SkirmishRoundInfo roundInfo)
  794. {
  795. var bpv = DataRepository.GetGameBasePointValues();
  796. var r = new SkirmishRollResult();
  797. var attackerRoll = Die.Roll(bpv.CombatRoll);
  798. // Decrement RemoteFirePoints hit or miss - if necessary
  799. // DistanceToTarget is just 0 for local battle
  800. if (roundInfo.Skirmish.Attacker.CurrentMoveStats.RemoteFirePoints > 0)
  801. roundInfo.Skirmish.Attacker.CurrentMoveStats.RemoteFirePoints -= roundInfo.DistanceToTarget;
  802. if (attackerRoll <= roundInfo.AttackerNetAttackValue && roundInfo.AttackerCanDoBattle) // Attack Hit
  803. {
  804. r.MedicalEffective = MedicalEffective(roundInfo.Skirmish.Defender);
  805. r.StealthEffective = StealthEffective(roundInfo.Skirmish.Defender);
  806. // Check for medical effective and stealth evasion for defender
  807. // This is the only logical variance for stealth. It's assumed that once the attacker fires, they have lost any stealth
  808. // advantage. Conversely, when the defender fires back (DefendRoll), they have also lost any stealth advantage for
  809. // this round... regained on the next round.
  810. if (!r.MedicalEffective && !r.StealthEffective)
  811. {
  812. roundInfo.Skirmish.Destroyed.Add(roundInfo.Skirmish.Defender);
  813. // Defending roll only if not medical effective
  814. var defenderRoll = Die.Roll(bpv.CombatRoll);
  815. //Decrement RemoteFirePoints hit or miss - if necessary
  816. if (roundInfo.Skirmish.Defender.CurrentMoveStats.RemoteFirePoints > 0)
  817. roundInfo.Skirmish.Defender.CurrentMoveStats.RemoteFirePoints -= roundInfo.DistanceToTarget;
  818. if (defenderRoll <= roundInfo.DefenderNetDefenceValue && roundInfo.DefenderCanDoBattle) // Defence hit
  819. {
  820. // Check for medical effective for attacker. Attackers are assumed to have revealed their position, so no stealth evasion.
  821. if (!MedicalEffective(roundInfo.Skirmish.Attacker))
  822. roundInfo.Skirmish.Destroyed.Add(roundInfo.Skirmish.Attacker);
  823. else
  824. roundInfo.Skirmish.Victor = roundInfo.Skirmish.Attacker;
  825. }
  826. else
  827. {
  828. roundInfo.Skirmish.Victor = roundInfo.Skirmish.Attacker;
  829. }
  830. }
  831. // If the defender or both are destroyed,
  832. // or this is a special Defence skirmish (one fire round), skirmish ends
  833. r.DestroyedUnits = roundInfo.Skirmish.Destroyed.Any();
  834. return r;
  835. }
  836. // Default is to continue the skirmish
  837. return r;
  838. }
  839. /// <summary>
  840. /// Perform game operations for the defender's skirmish round
  841. /// </summary>
  842. /// <param name="roundInfo"></param>
  843. /// <param name="ctx"></param>
  844. private SkirmishRollResult DefendRoll(SkirmishRoundInfo roundInfo)
  845. {
  846. var bpv = DataRepository.GetGameBasePointValues();
  847. var r = new SkirmishRollResult();
  848. var defenderRoll = Die.Roll(bpv.CombatRoll);
  849. // Decrement RemoteFirePoints hit or miss - if necessary
  850. // DistanceToTarget is just 0 for local battle
  851. if (roundInfo.Skirmish.Defender.CurrentMoveStats.RemoteFirePoints > 0)
  852. roundInfo.Skirmish.Defender.CurrentMoveStats.RemoteFirePoints -= roundInfo.DistanceToTarget;
  853. if (defenderRoll <= roundInfo.DefenderNetDefenceValue && roundInfo.DefenderCanDoBattle) // Defence Hit
  854. {
  855. r.MedicalEffective = MedicalEffective(roundInfo.Skirmish.Attacker);
  856. // Attacker has medical support... defender has fired, so no stealth advantage this round
  857. if (!r.MedicalEffective)
  858. {
  859. // Attacker is removed immediately
  860. roundInfo.Skirmish.Destroyed.Add(roundInfo.Skirmish.Attacker);
  861. roundInfo.Skirmish.Victor = roundInfo.Skirmish.Defender;
  862. r.DestroyedUnits = true;
  863. }
  864. }
  865. return r;
  866. }
  867. /// <summary>
  868. /// Checks to see if medical is available and attempts to recover the unit if so
  869. /// </summary>
  870. /// <param name="ctx"></param>
  871. /// <param name="unit"></param>
  872. /// <returns></returns>
  873. private bool MedicalEffective(IUnit unit)
  874. {
  875. var bpv = DataRepository.GetGameBasePointValues();
  876. if (!unit.HasMedicalSupport()) return false;
  877. var medicalRoll = Die.Roll(bpv.CombatRoll);
  878. return (medicalRoll <= bpv.MedicalSupportBase);
  879. }
  880. private bool StealthEffective(IUnit unit)
  881. {
  882. var bpv = DataRepository.GetGameBasePointValues();
  883. var stealthRoll = Die.Roll(bpv.StealthRoll);
  884. return (stealthRoll <= unit.GetFullNetStealthValue());
  885. }
  886. /// <summary>
  887. /// Reconstructs a path from the target object back to the source object as a collection of PathNodes
  888. /// </summary>
  889. /// <param name="o"></param>
  890. /// <returns></returns>
  891. private IEnumerable<IPathNode> ReconstructPath(IPathableObject o)
  892. {
  893. List<PathNode> path = new List<PathNode>();
  894. while (o.G != 0)
  895. {
  896. var n = JTSServices.NodeService.GetNodeAt(o.Location).Clone().ToPathableObject<PathNode>();
  897. path.Add(n as PathNode);
  898. o = o.Parent;
  899. }
  900. // Add current node
  901. path.Add(JTSServices.NodeService.GetNodeAt(o.Location).Clone().ToPathableObject<PathNode>() as PathNode);
  902. return path;
  903. }
  904. private StrategicAssessmentRating ConvertToStrategicAssessmentRating(double value, double range)
  905. {
  906. if (!(range > 0)) return StrategicAssessmentRating.NONE;
  907. var items = Enum.GetValues(typeof(StrategicAssessmentRating));
  908. var enumUpper = Convert.ToInt32(items.GetValue(items.GetUpperBound(0)));
  909. var r = (int)Math.Round(Convert.ToDouble(((value * enumUpper)) / range));
  910. return (StrategicAssessmentRating)r;
  911. }
  912. #endregion
  913. #endregion
  914. }
  915. sealed class SkirmishRoundInfo
  916. {
  917. public ISkirmish Skirmish { get; set; }
  918. public double AttackerNetAttackValue { get; set; }
  919. public double DefenderNetDefenceValue { get; set; }
  920. public bool AttackerCanDoBattle { get; set; }
  921. public bool DefenderCanDoBattle { get; set; }
  922. public int DistanceToTarget { get; set; }
  923. }
  924. /// <summary>
  925. /// Used to return messages through to the skirmish
  926. /// </summary>
  927. sealed class SkirmishRollResult
  928. {
  929. public bool DestroyedUnits { get; set; }
  930. public bool MedicalEffective { get; set; }
  931. public bool StealthEffective { get; set; }
  932. public bool ContinueSkirmish { get {return !DestroyedUnits && !StealthEffective;} }
  933. public SkirmishRollResult()
  934. {
  935. DestroyedUnits = false; // Default to continue skirmish;
  936. MedicalEffective = false;
  937. StealthEffective = false;
  938. }
  939. }
  940. }