Simplify world updates and expose root season

This commit is contained in:
Jaculabilis 2022-03-28 22:34:57 -07:00
parent aa9c9c548b
commit d4e68844c6
7 changed files with 78 additions and 72 deletions

View File

@ -357,10 +357,10 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
// TODO provide more structured information about order outcomes // TODO provide more structured information about order outcomes
World updated = world World updated = world.Update(
.WithSeasons(world.Seasons.Concat(createdFutures.Values)) seasons: world.Seasons.Concat(createdFutures.Values),
.WithUnits(world.Units.Concat(createdUnits)) units: world.Units.Concat(createdUnits),
.WithRetreats(retreats); retreats: retreats);
return updated; return updated;
} }

View File

@ -22,6 +22,11 @@ public class World
/// </summary> /// </summary>
public ReadOnlyCollection<Season> Seasons { get; } public ReadOnlyCollection<Season> Seasons { get; }
/// <summary>
/// The first season of the game.
/// </summary>
public Season RootSeason { get; }
/// <summary> /// <summary>
/// All units in the multiverse. /// All units in the multiverse.
/// </summary> /// </summary>
@ -37,10 +42,14 @@ public class World
/// </summary> /// </summary>
public Options Options { get; } public Options Options { get; }
/// <summary>
/// Create a new World, providing all state data.
/// </summary>
private World( private World(
ReadOnlyCollection<Province> provinces, ReadOnlyCollection<Province> provinces,
ReadOnlyCollection<Power> powers, ReadOnlyCollection<Power> powers,
ReadOnlyCollection<Season> seasons, ReadOnlyCollection<Season> seasons,
Season rootSeason,
ReadOnlyCollection<Unit> units, ReadOnlyCollection<Unit> units,
ReadOnlyCollection<RetreatingUnit> retreatingUnits, ReadOnlyCollection<RetreatingUnit> retreatingUnits,
Options options) Options options)
@ -48,22 +57,49 @@ public class World
this.Provinces = provinces; this.Provinces = provinces;
this.Powers = powers; this.Powers = powers;
this.Seasons = seasons; this.Seasons = seasons;
this.RootSeason = rootSeason;
this.Units = units; this.Units = units;
this.RetreatingUnits = retreatingUnits; this.RetreatingUnits = retreatingUnits;
this.Options = options; this.Options = options;
} }
/// <summary> /// <summary>
/// Create a new world with specified provinces and powers. /// Create a new World from a previous one, replacing some state data.
/// </summary>
private World(
World previous,
ReadOnlyCollection<Province>? provinces = null,
ReadOnlyCollection<Power>? powers = null,
ReadOnlyCollection<Season>? seasons = null,
ReadOnlyCollection<Unit>? units = null,
ReadOnlyCollection<RetreatingUnit>? retreatingUnits = null,
Options? options = null)
: this(
provinces ?? previous.Provinces,
powers ?? previous.Powers,
seasons ?? previous.Seasons,
previous.RootSeason, // Can't change the root season
units ?? previous.Units,
retreatingUnits ?? previous.RetreatingUnits,
options ?? previous.Options)
{
}
/// <summary>
/// Create a new world with specified provinces and powers and an initial season.
/// </summary> /// </summary>
public static World WithMap(IEnumerable<Province> provinces, IEnumerable<Power> powers) public static World WithMap(IEnumerable<Province> provinces, IEnumerable<Power> powers)
=> new World( {
Season root = Season.MakeRoot();
return new World(
new(provinces.ToList()), new(provinces.ToList()),
new(powers.ToList()), new(powers.ToList()),
new(new List<Season>()), new(new List<Season> { root }),
root,
new(new List<Unit>()), new(new List<Unit>()),
new(new List<RetreatingUnit>()), new(new List<RetreatingUnit>()),
new Options()); new Options());
}
/// <summary> /// <summary>
/// Create a new world with the standard Diplomacy provinces and powers. /// Create a new world with the standard Diplomacy provinces and powers.
@ -71,42 +107,22 @@ public class World
public static World WithStandardMap() public static World WithStandardMap()
=> WithMap(StandardProvinces, StandardPowers); => WithMap(StandardProvinces, StandardPowers);
/// <summary> public World Update(
/// Create a new world with new seasons. IEnumerable<Season>? seasons = null,
/// </summary> IEnumerable<Unit>? units = null,
public World WithSeasons(IEnumerable<Season> seasons) IEnumerable<RetreatingUnit>? retreats = null)
=> new World( => new World(
this.Provinces, previous: this,
this.Powers, seasons: seasons == null ? this.Seasons : new(seasons.ToList()),
new(seasons.ToList()), units: units == null ? this.Units : new(units.ToList()),
this.Units, retreatingUnits: retreats == null ? this.RetreatingUnits : new(retreats.ToList()));
this.RetreatingUnits,
this.Options);
/// <summary>
/// Create a new world with an initial season.
/// </summary>
public World WithInitialSeason()
=> WithSeasons(new List<Season> { Season.MakeRoot() });
/// <summary>
/// Create a new world with new units.
/// </summary>
public World WithUnits(IEnumerable<Unit> units)
=> new World(
this.Provinces,
this.Powers,
this.Seasons,
new(units.ToList()),
this.RetreatingUnits,
this.Options);
/// <summary> /// <summary>
/// Create a new world with new units created from unit specs. Units specs are in the format /// Create a new world with new units created from unit specs. Units specs are in the format
/// "<power> <A/F> <province> [<coast>]". If the province or coast name has a space in it, the /// "<power> <A/F> <province> [<coast>]". If the province or coast name has a space in it, the
/// abbreviation should be used. /// abbreviation should be used. Unit specs always describe units in the root season.
/// </summary> /// </summary>
public World WithUnits(params string[] unitSpecs) public World AddUnits(params string[] unitSpecs)
{ {
IEnumerable<Unit> units = unitSpecs.Select(spec => IEnumerable<Unit> units = unitSpecs.Select(spec =>
{ {
@ -123,18 +139,18 @@ public class World
: splits.Length == 3 : splits.Length == 3
? this.GetWater(splits[2]) ? this.GetWater(splits[2])
: this.GetWater(splits[2], splits[3]); : this.GetWater(splits[2], splits[3]);
Unit unit = Unit.Build(location, this.Seasons.First(), power, type); Unit unit = Unit.Build(location, this.RootSeason, power, type);
return unit; return unit;
}); });
return this.WithUnits(units); return this.Update(units: units);
} }
/// <summary> /// <summary>
/// Create a new world with standard Diplomacy initial unit placements. /// Create a new world with standard Diplomacy initial unit placements.
/// </summary> /// </summary>
public World WithStandardUnits() public World AddStandardUnits()
{ {
return this.WithUnits( return this.AddUnits(
"Austria A Bud", "Austria A Bud",
"Austria A Vir", "Austria A Vir",
"Austria F Tri", "Austria F Tri",
@ -160,22 +176,12 @@ public class World
); );
} }
public World WithRetreats(IEnumerable<RetreatingUnit> retreatingUnits)
=> new World(
this.Provinces,
this.Powers,
this.Seasons,
this.Units,
new(retreatingUnits.ToList()),
this.Options);
/// <summary> /// <summary>
/// A standard Diplomacy game setup. /// A standard Diplomacy game setup.
/// </summary> /// </summary>
public static World Standard => World public static World Standard => World
.WithStandardMap() .WithStandardMap()
.WithInitialSeason() .AddStandardUnits();
.WithStandardUnits();
/// <summary> /// <summary>
/// Get a province by name. Throws if the province is not found. /// Get a province by name. Throws if the province is not found.

View File

@ -9,7 +9,7 @@ namespace MultiversalDiplomacyTests;
public class DATC_A public class DATC_A
{ {
private World StandardEmpty { get; } = World.WithStandardMap().WithInitialSeason(); private World StandardEmpty { get; } = World.WithStandardMap();
[Test] [Test]
public void DATC_6_A_1_MoveToAnAreaThatIsNotANeighbor() public void DATC_6_A_1_MoveToAnAreaThatIsNotANeighbor()

View File

@ -11,72 +11,72 @@ public class MovementAdjudicatorTest
[Test] [Test]
public void Validation_ValidHold() public void Validation_ValidHold()
{ {
TestCaseBuilder setup = new TestCaseBuilder(World.WithStandardMap().WithInitialSeason()); TestCaseBuilder setup = new TestCaseBuilder(World.WithStandardMap());
setup["Germany"] setup["Germany"]
.Army("Mun").Holds().GetReference(out var order); .Army("Mun").Holds().GetReference(out var order);
setup.ValidateOrders(MovementPhaseAdjudicator.Instance); setup.ValidateOrders(MovementPhaseAdjudicator.Instance);
Assert.That(order.Validation, Is.Valid, "Unexpected validation result"); Assert.That(order, Is.Valid, "Unexpected validation result");
Assert.That(order.Replacement, Is.Null, "Unexpected order replacement"); Assert.That(order.Replacement, Is.Null, "Unexpected order replacement");
} }
[Test] [Test]
public void Validation_ValidMove() public void Validation_ValidMove()
{ {
TestCaseBuilder setup = new TestCaseBuilder(World.WithStandardMap().WithInitialSeason()); TestCaseBuilder setup = new TestCaseBuilder(World.WithStandardMap());
setup["Germany"] setup["Germany"]
.Army("Mun").MovesTo("Tyr").GetReference(out var order); .Army("Mun").MovesTo("Tyr").GetReference(out var order);
setup.ValidateOrders(MovementPhaseAdjudicator.Instance); setup.ValidateOrders(MovementPhaseAdjudicator.Instance);
Assert.That(order.Validation, Is.Valid, "Unexpected validation result"); Assert.That(order, Is.Valid, "Unexpected validation result");
Assert.That(order.Replacement, Is.Null, "Unexpected order replacement"); Assert.That(order.Replacement, Is.Null, "Unexpected order replacement");
} }
[Test] [Test]
public void Validation_ValidConvoy() public void Validation_ValidConvoy()
{ {
TestCaseBuilder setup = new TestCaseBuilder(World.WithStandardMap().WithInitialSeason()); TestCaseBuilder setup = new TestCaseBuilder(World.WithStandardMap());
setup["Germany"] setup["Germany"]
.Fleet("Nth").Convoys.Army("Hol").To("Lon").GetReference(out var order); .Fleet("Nth").Convoys.Army("Hol").To("Lon").GetReference(out var order);
setup.ValidateOrders(MovementPhaseAdjudicator.Instance); setup.ValidateOrders(MovementPhaseAdjudicator.Instance);
Assert.That(order.Validation, Is.Valid, "Unexpected validation result"); Assert.That(order, Is.Valid, "Unexpected validation result");
Assert.That(order.Replacement, Is.Null, "Unexpected order replacement"); Assert.That(order.Replacement, Is.Null, "Unexpected order replacement");
} }
[Test] [Test]
public void Validation_ValidSupportHold() public void Validation_ValidSupportHold()
{ {
TestCaseBuilder setup = new TestCaseBuilder(World.WithStandardMap().WithInitialSeason()); TestCaseBuilder setup = new TestCaseBuilder(World.WithStandardMap());
setup["Germany"] setup["Germany"]
.Army("Mun").Supports.Army("Kie").Hold().GetReference(out var order); .Army("Mun").Supports.Army("Kie").Hold().GetReference(out var order);
setup.ValidateOrders(MovementPhaseAdjudicator.Instance); setup.ValidateOrders(MovementPhaseAdjudicator.Instance);
Assert.That(order.Validation, Is.Valid, "Unexpected validation result"); Assert.That(order, Is.Valid, "Unexpected validation result");
Assert.That(order.Replacement, Is.Null, "Unexpected order replacement"); Assert.That(order.Replacement, Is.Null, "Unexpected order replacement");
} }
[Test] [Test]
public void Validation_ValidSupportMove() public void Validation_ValidSupportMove()
{ {
TestCaseBuilder setup = new TestCaseBuilder(World.WithStandardMap().WithInitialSeason()); TestCaseBuilder setup = new TestCaseBuilder(World.WithStandardMap());
setup["Germany"] setup["Germany"]
.Army("Mun").Supports.Army("Kie").MoveTo("Ber").GetReference(out var order); .Army("Mun").Supports.Army("Kie").MoveTo("Ber").GetReference(out var order);
setup.ValidateOrders(MovementPhaseAdjudicator.Instance); setup.ValidateOrders(MovementPhaseAdjudicator.Instance);
Assert.That(order.Validation, Is.Valid, "Unexpected validation result"); Assert.That(order, Is.Valid, "Unexpected validation result");
Assert.That(order.Replacement, Is.Null, "Unexpected order replacement"); Assert.That(order.Replacement, Is.Null, "Unexpected order replacement");
} }
[Test] [Test]
public void Adjudication_Hold() public void Adjudication_Hold()
{ {
TestCaseBuilder setup = new TestCaseBuilder(World.WithStandardMap().WithInitialSeason()); TestCaseBuilder setup = new TestCaseBuilder(World.WithStandardMap());
setup["Germany"] setup["Germany"]
.Army("Mun").Holds().GetReference(out var order); .Army("Mun").Holds().GetReference(out var order);
@ -96,7 +96,7 @@ public class MovementAdjudicatorTest
[Test] [Test]
public void Adjudication_Move() public void Adjudication_Move()
{ {
TestCaseBuilder setup = new TestCaseBuilder(World.WithStandardMap().WithInitialSeason()); TestCaseBuilder setup = new TestCaseBuilder(World.WithStandardMap());
setup["Germany"] setup["Germany"]
.Army("Mun").MovesTo("Tyr").GetReference(out var order); .Army("Mun").MovesTo("Tyr").GetReference(out var order);
@ -122,7 +122,7 @@ public class MovementAdjudicatorTest
[Test] [Test]
public void Adjudication_Support() public void Adjudication_Support()
{ {
TestCaseBuilder setup = new TestCaseBuilder(World.WithStandardMap().WithInitialSeason()); TestCaseBuilder setup = new TestCaseBuilder(World.WithStandardMap());
setup["Germany"] setup["Germany"]
.Army("Mun").MovesTo("Tyr").GetReference(out var move) .Army("Mun").MovesTo("Tyr").GetReference(out var move)
.Army("Boh").Supports.Army("Mun").MoveTo("Tyr").GetReference(out var support); .Army("Boh").Supports.Army("Mun").MoveTo("Tyr").GetReference(out var support);

View File

@ -217,7 +217,7 @@ public class TestCaseBuilder
// Not found // Not found
Unit newUnit = Unit.Build(location, season, power, type); Unit newUnit = Unit.Build(location, season, power, type);
this.World = this.World.WithUnits(this.World.Units.Append(newUnit)); this.World = this.World.Update(units: this.World.Units.Append(newUnit));
return newUnit; return newUnit;
} }

View File

@ -12,7 +12,7 @@ class TestCaseBuilderTest
[Test] [Test]
public void BuilderCreatesUnits() public void BuilderCreatesUnits()
{ {
TestCaseBuilder setup = new(World.WithStandardMap().WithInitialSeason()); TestCaseBuilder setup = new(World.WithStandardMap());
Assert.That(setup.World.Powers.Count(), Is.EqualTo(7), "Unexpected power count"); Assert.That(setup.World.Powers.Count(), Is.EqualTo(7), "Unexpected power count");
Assert.That(setup.World.Units, Is.Empty, "Expected no units to be created yet"); Assert.That(setup.World.Units, Is.Empty, "Expected no units to be created yet");
@ -50,7 +50,7 @@ class TestCaseBuilderTest
[Test] [Test]
public void BuilderCreatesOrders() public void BuilderCreatesOrders()
{ {
TestCaseBuilder setup = new(World.WithStandardMap().WithInitialSeason()); TestCaseBuilder setup = new(World.WithStandardMap());
Assert.That(setup.World.Powers.Count(), Is.EqualTo(7), "Unexpected power count"); Assert.That(setup.World.Powers.Count(), Is.EqualTo(7), "Unexpected power count");
Assert.That(setup.World.Units, Is.Empty, "Expected no units to be created yet"); Assert.That(setup.World.Units, Is.Empty, "Expected no units to be created yet");
@ -120,7 +120,7 @@ class TestCaseBuilderTest
{ {
IPhaseAdjudicator rubberStamp = new TestAdjudicator(validate: TestAdjudicator.RubberStamp); IPhaseAdjudicator rubberStamp = new TestAdjudicator(validate: TestAdjudicator.RubberStamp);
TestCaseBuilder setup = new TestCaseBuilder(World.WithStandardMap().WithInitialSeason()); TestCaseBuilder setup = new TestCaseBuilder(World.WithStandardMap());
setup["Germany"] setup["Germany"]
.Army("Mun").Holds().GetReference(out var orderMun); .Army("Mun").Holds().GetReference(out var orderMun);
@ -164,7 +164,7 @@ class TestCaseBuilderTest
validate: TestAdjudicator.RubberStamp, validate: TestAdjudicator.RubberStamp,
adjudicate: TestAdjudicator.NoMoves); adjudicate: TestAdjudicator.NoMoves);
TestCaseBuilder setup = new TestCaseBuilder(World.WithStandardMap().WithInitialSeason()); TestCaseBuilder setup = new TestCaseBuilder(World.WithStandardMap());
setup["Germany"] setup["Germany"]
.Army("Mun").Holds().GetReference(out var orderMun); .Army("Mun").Holds().GetReference(out var orderMun);

View File

@ -9,7 +9,7 @@ public class UnitTests
[Test] [Test]
public void MovementTest() public void MovementTest()
{ {
World world = World.WithStandardMap().WithInitialSeason(); World world = World.WithStandardMap();
Location Mun = world.GetLand("Mun"), Location Mun = world.GetLand("Mun"),
Boh = world.GetLand("Boh"), Boh = world.GetLand("Boh"),
Tyr = world.GetLand("Tyr"); Tyr = world.GetLand("Tyr");