Skip to content

Commit

Permalink
Add order validator and fix formatting check
Browse files Browse the repository at this point in the history
  • Loading branch information
Oliveriver committed Aug 1, 2024
1 parent 4b1e3c0 commit d80edd5
Show file tree
Hide file tree
Showing 16 changed files with 343 additions and 31 deletions.
3 changes: 2 additions & 1 deletion server/.editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ dotnet_diagnostic.IDE0077.severity = error
dotnet_diagnostic.IDE0043.severity = error
dotnet_diagnostic.IDE0059.severity = error
dotnet_diagnostic.IDE0058.severity = none
dotnet_diagnostic.IDE0048.severity = none

# Xml files
[*.xml]
Expand Down Expand Up @@ -142,7 +143,7 @@ dotnet_style_allow_statement_immediately_after_block_experimental = false:error
dotnet_code_quality_unused_parameters = all:error
dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:error
dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:error
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:error
dotnet_style_parentheses_in_other_binary_operators = never_if_unnecessary:error
dotnet_style_parentheses_in_other_operators = never_if_unnecessary:error
dotnet_style_qualification_for_field = false:error
dotnet_style_qualification_for_property = false:error
Expand Down
22 changes: 9 additions & 13 deletions server/Adjudication/Adjudicator.cs
Original file line number Diff line number Diff line change
@@ -1,27 +1,21 @@
using Entities;
using Enums;
using Utilities;

namespace Adjudication;

public class Adjudicator
public class Adjudicator(Validator validator)
{
public void Adjudicate(World world, List<Region> map)
private readonly Validator validator = validator;

public void Adjudicate(World world, List<Region> regions)
{
foreach (var order in world.Orders.Where(o => o.Status == OrderStatus.New))
{
order.Status = OrderStatus.Failure;
}
validator.Validate(world, regions);

var previousBoard = world.Boards.Last();

var year = previousBoard.Phase == Phase.Winter ? previousBoard.Year + 1 : previousBoard.Year;
var phase = previousBoard.Phase switch
{
Phase.Spring => Phase.Fall,
Phase.Fall => Phase.Winter,
Phase.Winter => Phase.Spring,
_ => throw new ArgumentOutOfRangeException("Phase not found")
};
var phase = previousBoard.Phase.NextPhase();

world.Boards.Add(new Board
{
Expand Down Expand Up @@ -54,5 +48,7 @@ public void Adjudicate(World world, List<Region> map)
},
}).ToList(),
});

world.Iteration++;
}
}
64 changes: 64 additions & 0 deletions server/Adjudication/Validation/AdjacencyValidator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using Entities;
using Enums;
using Utilities;

namespace Adjudication;

public class AdjacencyValidator(List<Region> regions)
{
private readonly List<Region> regions = regions;

public bool IsValidDirectMove(Unit unit, Location location, Location destination)
{
if (location.Phase == Phase.Winter || destination.Phase == Phase.Winter)
{
return false;
}

var isSameBoard = location.Timeline == destination.Timeline
&& location.Year == destination.Year
&& location.Phase == destination.Phase;

return isSameBoard
? IsValidIntraBoardMove(unit, location, destination)
: !unit.MustRetreat && IsValidInterBoardMove(unit, location, destination);
}

public bool IsValidInterBoardMove(Unit unit, Location location, Location destination)
{
var locationId = location.RegionId;
var destinationId = destination.RegionId;

if (Constants.UseStrictAdjacencies ? locationId != destinationId : !IsValidIntraBoardMove(unit, location, destination))
{
return false;
}

var yearDistance = location.Year - destination.Year;
var phaseDistance = (int)location.Phase - (int)destination.Phase;
var timeDistance = Math.Abs(2 * yearDistance + phaseDistance);

var multiverseDistance = Math.Abs(location.Timeline - destination.Timeline);

return timeDistance <= 1 && multiverseDistance <= 1 && (timeDistance == 0 || multiverseDistance == 0);
}

public bool IsValidIntraBoardMove(Unit unit, Location location, Location destination)
{
var locationId = location.RegionId;
var destinationId = destination.RegionId;

var region = regions.First(r => r.Id == locationId);

var connection = region.Connections.FirstOrDefault(c => c.Regions.Any(r => r.Id == destinationId));
if (connection == null)
{
return false;
}

var isValidArmyMove = unit.Type == UnitType.Army && connection.Type != ConnectionType.Sea;
var isValidFleetMove = unit.Type == UnitType.Fleet && connection.Type != ConnectionType.Land;
return isValidArmyMove || isValidFleetMove;
}
}

72 changes: 72 additions & 0 deletions server/Adjudication/Validation/ConvoyPathValidator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
using Entities;
using Enums;

namespace Adjudication;

public class ConvoyPathValidator(World world, List<Region> regions, AdjacencyValidator adjacencyValidator)
{
private readonly World world = world;
private readonly List<Region> regions = regions;

private readonly AdjacencyValidator adjacencyValidator = adjacencyValidator;

public bool HasPath(Unit unit, Location location, Location destination)
{
if (unit.Type == UnitType.Fleet)
{
return false;
}

var startRegion = regions.First(r => r.Id == location.RegionId);
var endRegion = regions.First(r => r.Id == destination.RegionId);

var startsOnCoast = startRegion.Type == RegionType.Coast
|| regions.Where(r => r.ParentId == startRegion.Id).Any(r => r.Type == RegionType.Coast);
var endsOnCoast = endRegion.Type == RegionType.Coast
|| regions.Where(r => r.ParentId == endRegion.Id).Any(r => r.Type == RegionType.Coast);

if (!startsOnCoast || !endsOnCoast)
{
return false;
}

var convoysInPath = world.Orders.OfType<Convoy>().Where(c =>
c.NeedsValidation
&& c.Midpoint == location
&& c.Destination == destination).ToList();

if (convoysInPath.Count == 0)
{
return false;
}

var depthFirstSearch = new DepthFirstSearch(convoysInPath, adjacencyValidator);
return depthFirstSearch.HasPath(unit, location, destination);
}

private class DepthFirstSearch(List<Convoy> convoys, AdjacencyValidator adjacencyValidator)
{
private readonly List<Convoy> convoys = convoys;
private readonly AdjacencyValidator adjacencyValidator = adjacencyValidator;

private readonly List<Convoy> visitedConvoys = [];

public bool HasPath(Unit unit, Location location, Location destination)
{
if (adjacencyValidator.IsValidDirectMove(unit, location, destination))
{
return true;
}

var convoy = convoys.FirstOrDefault(c => c.Location == location);
if (convoy != null)
{
visitedConvoys.Add(convoy);
}

return convoys
.Where(c => !visitedConvoys.Contains(c) && adjacencyValidator.IsValidDirectMove(c.Unit!, c.Location, location))
.Any(c => HasPath(c.Unit!, c.Location, destination));
}
}
}
138 changes: 138 additions & 0 deletions server/Adjudication/Validation/Validator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
using Entities;
using Enums;
using Factories;

namespace Adjudication;

public class Validator(DefaultWorldFactory defaultWorldFactory)
{
private readonly DefaultWorldFactory defaultWorldFactory = defaultWorldFactory;

private AdjacencyValidator adjacencyValidator = null!;
private ConvoyPathValidator convoyPathValidator = null!;
private World world = null!;
private List<Region> regions = null!;

public void Validate(World world, List<Region> regions)
{
this.world = world;
this.regions = regions;
adjacencyValidator = new(regions);
convoyPathValidator = new(world, regions, adjacencyValidator);

ValidateMoves();
ValidateSupports();
ValidateConvoys();
ValidateBuilds();
ValidateDisbands();
ValidateRetreats();
}

private void ValidateMoves()
{
var moves = world.Orders.Where(o => o.NeedsValidation && !o.Unit!.MustRetreat).OfType<Move>();

foreach (var move in moves)
{
var canDirectMove = adjacencyValidator.IsValidDirectMove(move.Unit!, move.Location, move.Destination);
var canConvoyMove = convoyPathValidator.HasPath(move.Unit!, move.Location, move.Destination);

move.Status = canDirectMove || canConvoyMove ? OrderStatus.New : OrderStatus.Invalid;
}
}

private void ValidateSupports()
{
var supports = world.Orders.Where(o => o.NeedsValidation && !o.Unit!.MustRetreat).OfType<Support>();

foreach (var support in supports)
{
var canSupport = adjacencyValidator.IsValidDirectMove(support.Unit!, support.Location, support.Destination);
var hasMatchingMove = world.Orders
.OfType<Move>()
.Any(m => m.Location == support.Midpoint && m.Destination == support.Destination);

support.Status = canSupport && hasMatchingMove ? OrderStatus.New : OrderStatus.Invalid;
}
}

private void ValidateConvoys()
{
var convoys = world.Orders.Where(o => o.NeedsValidation && !o.Unit!.MustRetreat).OfType<Convoy>();

foreach (var convoy in convoys)
{
// TODO fix for convoying to/from land regions with child coasts
var convoysFromCoast = regions.First(r => r.Id == convoy.Midpoint.RegionId);
var convoysToCoast = regions.First(r => r.Id == convoy.Destination.RegionId);
var hasMatchingMove = world.Orders
.OfType<Move>()
.Any(m => m.Location == convoy.Midpoint && m.Destination == convoy.Destination);

convoy.Status = hasMatchingMove ? OrderStatus.New : OrderStatus.Invalid;
}
}

private void ValidateBuilds()
{
var builds = world.Orders.Where(o => o.NeedsValidation && !o.Unit!.MustRetreat).OfType<Build>();
var homeCentres = defaultWorldFactory.CreateCentres();

foreach (var build in builds)
{
if (build.Location.Phase != Phase.Winter)
{
build.Status = OrderStatus.Invalid;
continue;
}

var board = world.Boards
.FirstOrDefault(b => b.Timeline == build.Location.Timeline && b.Year == build.Location.Year && b.Phase == Phase.Winter);
var region = regions.First(r => r.Id == build.Location.RegionId);
var centre = homeCentres.FirstOrDefault(c => c.Location == build.Location);
var unit = build.Unit!;

if (board == null || centre == null)
{
build.Status = OrderStatus.Invalid;
continue;
}

var isCompatibleRegion = centre.Owner == unit.Owner;
var isCompatibleUnit = unit.Type == UnitType.Army && region.Type != RegionType.Sea
|| unit.Type == UnitType.Fleet && region.Type == RegionType.Coast;
build.Status = isCompatibleRegion && isCompatibleUnit ? OrderStatus.New : OrderStatus.Invalid;

// NB validation based on available build count to be done as part of adjudication
}
}

private void ValidateDisbands()
{
var disbands = world.Orders.Where(o => o.NeedsValidation && !o.Unit!.MustRetreat).OfType<Disband>();

foreach (var disband in disbands)
{
disband.Status = disband.Location.Phase != Phase.Winter ? OrderStatus.New : OrderStatus.Invalid;

// NB validation based on available build count to be done as part of adjudication
}
}

private void ValidateRetreats()
{
var retreats = world.Orders.Where(o => o.NeedsValidation && o.Unit!.MustRetreat);

foreach (var retreat in retreats)
{
retreat.Status = retreat switch
{
Move move => adjacencyValidator.IsValidDirectMove(move.Unit!, move.Location, move.Destination)
? OrderStatus.New
: OrderStatus.Invalid,
Disband => OrderStatus.New,
_ => OrderStatus.Invalid,
};
}
}
}
8 changes: 7 additions & 1 deletion server/Entities/Orders/Convoy.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
namespace Entities;
using Enums;
using System.ComponentModel.DataAnnotations.Schema;

namespace Entities;

public class Convoy : Order
{
public Location Midpoint { get; set; } = null!;
public Location Destination { get; set; } = null!;

[NotMapped]
public override bool NeedsValidation => Status is OrderStatus.Invalid or OrderStatus.New;
}
6 changes: 5 additions & 1 deletion server/Entities/Orders/Order.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Enums;
using System.ComponentModel.DataAnnotations.Schema;

namespace Entities;

Expand All @@ -11,6 +12,9 @@ public abstract class Order

public OrderStatus Status { get; set; }
public int? UnitId { get; set; }
public virtual Unit? Unit { get; set; }
public virtual Unit? Unit { get; set; } // Nullability of this is annoying...
public Location Location { get; set; } = null!;

[NotMapped]
public virtual bool NeedsValidation => Status == OrderStatus.New;
}
8 changes: 7 additions & 1 deletion server/Entities/Orders/Support.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
namespace Entities;
using Enums;
using System.ComponentModel.DataAnnotations.Schema;

namespace Entities;

public class Support : Order
{
public Location Midpoint { get; set; } = null!;
public Location Destination { get; set; } = null!;

[NotMapped]
public override bool NeedsValidation => Status is OrderStatus.Invalid or OrderStatus.New;
}
Loading

0 comments on commit d80edd5

Please sign in to comment.