Compare commits

...

18 Commits

Author SHA1 Message Date
Jaculabilis e580b91509 Temp commit of working state 2024-08-09 07:13:30 -07:00
Jaculabilis 158c952b6b Update README with order parsing grammar 2024-08-09 07:13:30 -07:00
Jaculabilis fa04aab6c6 Add adjudicate command and implement AdjudicateOrders 2024-08-09 07:13:30 -07:00
Jaculabilis c5bd74ae1e Add a basic status command 2024-08-09 07:13:30 -07:00
Jaculabilis 36e2224324 Add game setup script handler
This will allow setting up arbitrary test cases when writing test scripts
2024-08-09 07:13:30 -07:00
Jaculabilis 79cfbc2666 Add subcommands to new 2024-08-09 07:13:30 -07:00
Jaculabilis 73a8934201 Check for valid power names 2024-08-09 07:13:30 -07:00
Jaculabilis c1237e1f67 Remove file name prompt when reading from file 2024-08-09 07:13:30 -07:00
Jaculabilis 6f03c5ada1 Add repl alias to nix shell 2024-08-09 07:13:24 -07:00
Jaculabilis 91d2886d19 Add repl cli and script handling framework 2024-08-09 07:11:57 -07:00
Jaculabilis 2c677f4f36 Fix incorrect province name in standard unit list 2024-08-09 07:11:57 -07:00
Jaculabilis 771d390409 Add OrderSet model 2024-08-09 07:11:57 -07:00
Tim Van Baak 3db01c0ffd Update projects to dotnet 8 2024-08-09 06:53:22 -07:00
Tim Van Baak 0fd9c93a70 Get nix-ld to work with the Roslyn analyzer 2024-08-08 07:51:08 -07:00
Tim Van Baak 5b4758a4ed Update to dotnet 8 2024-08-08 07:49:39 -07:00
Tim Van Baak 7b2176b1d2 Update nixpkgs pin 2024-08-08 07:49:26 -07:00
Tim Van Baak 4bbd29ac93 Fix broken VS code shells
See https://github.com/NixOS/nix/issues/6982#issuecomment-1236743200
2024-08-08 07:47:37 -07:00
Tim Van Baak 26e268c3a0 Remove custom PS1 2024-08-08 07:45:27 -07:00
23 changed files with 830 additions and 19 deletions

27
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,27 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": ".NET Core Launch (console)",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
"program": "${workspaceFolder}/MultiversalDiplomacy/bin/Debug/net6.0/MultiversalDiplomacy.dll",
"args": ["repl"],
"cwd": "${workspaceFolder}/MultiversalDiplomacy",
"console": "internalConsole",
"stopAtEntry": true
},
{
"name": ".NET Core Attach",
"type": "coreclr",
"request": "attach"
}
],
"logging": {
"engineLogging": true
}
}

View File

@ -2,7 +2,7 @@
"terminal.integrated.profiles.linux": { "terminal.integrated.profiles.linux": {
"nix develop": { "nix develop": {
"path": "nix", "path": "nix",
"args": ["develop"] "args": ["develop", "--impure"]
} }
}, },
"terminal.integrated.profiles.windows": { "terminal.integrated.profiles.windows": {

41
.vscode/tasks.json vendored Normal file
View File

@ -0,0 +1,41 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "build",
"command": "dotnet",
"type": "process",
"args": [
"build",
"${workspaceFolder}/MultiversalDiplomacy/MultiversalDiplomacy.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"problemMatcher": "$msCompile"
},
{
"label": "publish",
"command": "dotnet",
"type": "process",
"args": [
"publish",
"${workspaceFolder}/MultiversalDiplomacy/MultiversalDiplomacy.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"problemMatcher": "$msCompile"
},
{
"label": "watch",
"command": "dotnet",
"type": "process",
"args": [
"watch",
"run",
"--project",
"${workspaceFolder}/MultiversalDiplomacy/MultiversalDiplomacy.csproj"
],
"problemMatcher": "$msCompile"
}
]
}

View File

@ -8,6 +8,18 @@ namespace MultiversalDiplomacy.Adjudicate;
/// </summary> /// </summary>
public interface IPhaseAdjudicator public interface IPhaseAdjudicator
{ {
/// <summary>
/// Given a list of order sets, determine which entries are comprehensible as orders.
/// An order set entry may comprehensible as an order but not valid for the current
/// phase; these orders will be rejected by <see cref="ValidateOrders"/>.
/// </summary>
/// <param name="world">The global game state.</param>
/// <param name="orderSets">The order sets to adjudicate.</param>
/// <returns>
/// A list of <see cref="Order"/> objects representing the orders parsed.
/// </returns>
public List<Order> ParseOrderSets(World world, List<OrderSet> orderSets);
/// <summary> /// <summary>
/// Given a list of orders, determine which orders are valid for this adjudicator and /// Given a list of orders, determine which orders are valid for this adjudicator and
/// which should be rejected before adjudication. Adjudication should be performed on /// which should be rejected before adjudication. Adjudication should be performed on

View File

@ -1,3 +1,5 @@
using System.Linq;
using MultiversalDiplomacy.Adjudicate.Decision; using MultiversalDiplomacy.Adjudicate.Decision;
using MultiversalDiplomacy.Adjudicate.Logging; using MultiversalDiplomacy.Adjudicate.Logging;
using MultiversalDiplomacy.Model; using MultiversalDiplomacy.Model;
@ -19,6 +21,36 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
this.logger = logger; this.logger = logger;
} }
public List<Order> ParseOrderSets(World world, List<OrderSet> orderSets)
{
foreach (OrderSet orderSet in orderSets)
{
string[] lines = orderSet.Text.Split('\n');
string powerLine = lines[0];
// TODO verify
string powerName = powerLine.Substring("orders ".Length);
foreach (string line in lines.Skip(1))
{
// Individual order components do not have spaces in them
string[] tokens = line.Split(' ');
int i = 0;
// Check for a unit type
string[] unitTypes = new string[] { "a", "f"};
if (unitTypes.Contains(tokens[i].ToLowerInvariant()))
{
// yay
i++;
}
//
}
}
throw new NotImplementedException();
}
public List<OrderValidation> ValidateOrders(World world, List<Order> orders) public List<OrderValidation> ValidateOrders(World world, List<Order> orders)
{ {
// The basic workflow of this function will be to look for invalid orders, remove these // The basic workflow of this function will be to look for invalid orders, remove these

View File

@ -0,0 +1,50 @@
using MultiversalDiplomacy.Adjudicate;
using MultiversalDiplomacy.Model;
using MultiversalDiplomacy.Orders;
namespace MultiversalDiplomacy;
/// <summary>
/// The game controller is a stateless class that defines the basic, high-level operations that can be taken to modify
/// the game state.
/// </summary>
public static class GameController
{
public static World InitializeWorld()
{
return World.Standard;
}
public static World SubmitOrderSet(World world, string text)
{
var orderSet = new OrderSet(text);
return world.Update(orderSets: world.OrderSets.Append(orderSet));
}
public static World AdjudicateOrders(World world)
{
// Determine which phase the game is in, which determines how orders should be interpreted and adjudicated.
PhaseType phaseType = world.GetNextPhaseType();
IPhaseAdjudicator adjudicator = phaseType switch {
PhaseType.Movement => MovementPhaseAdjudicator.Instance,
PhaseType.Retreat => throw new NotImplementedException(),
PhaseType.Build => throw new NotImplementedException(),
PhaseType.Sustain => throw new NotImplementedException(),
_ => throw new InvalidOperationException(phaseType.ToString()),
};
// Parse the order sets into actual orders.
List<Order> parsedOrders = adjudicator.ParseOrderSets(world, world.OrderSets.ToList());
// Validate the orders.
var orderValidations = adjudicator.ValidateOrders(world, parsedOrders);
// Adjudicate the orders.
var validOrders = orderValidations.Where(v => v.Valid).Select(v => v.Order).ToList();
var results = adjudicator.AdjudicateOrders(world, validOrders);
// Update the world.
return adjudicator.UpdateWorld(world, results);
}
}

View File

@ -0,0 +1,28 @@
namespace MultiversalDiplomacy.Model;
/// <summary>
/// A list of unit orders submitted by a power.
/// </summary>
public class OrderSet
{
/// <summary>
/// The raw text of the submitted orders, including the name of the submitting power.
/// </summary>
public string Text { get; }
/// <summary>
/// Whether the order set has already been adjudicated.
/// </summary>
public bool Adjudicated { get; }
/// <summary>
/// UTC timestamp at which the order set was submitted
/// </summary>
public DateTime SubmittedAt { get; }
public OrderSet(string text)
{
Text = text;
SubmittedAt = DateTime.UtcNow;
}
}

View File

@ -0,0 +1,9 @@
namespace MultiversalDiplomacy.Model;
public enum PhaseType
{
Movement = 1,
Retreat = 2,
Build = 3,
Sustain = 4,
}

View File

@ -44,6 +44,11 @@ public class World
/// </summary> /// </summary>
public ReadOnlyDictionary<Season, OrderHistory> OrderHistory { get; } public ReadOnlyDictionary<Season, OrderHistory> OrderHistory { get; }
/// <summary>
/// Submitted order sets.
/// </summary>
public ReadOnlyCollection<OrderSet> OrderSets { get; }
/// <summary> /// <summary>
/// Immutable game options. /// Immutable game options.
/// </summary> /// </summary>
@ -60,6 +65,7 @@ public class World
ReadOnlyCollection<Unit> units, ReadOnlyCollection<Unit> units,
ReadOnlyCollection<RetreatingUnit> retreatingUnits, ReadOnlyCollection<RetreatingUnit> retreatingUnits,
ReadOnlyDictionary<Season, OrderHistory> orderHistory, ReadOnlyDictionary<Season, OrderHistory> orderHistory,
ReadOnlyCollection<OrderSet> orderSets,
Options options) Options options)
{ {
this.Provinces = provinces; this.Provinces = provinces;
@ -69,6 +75,7 @@ public class World
this.Units = units; this.Units = units;
this.RetreatingUnits = retreatingUnits; this.RetreatingUnits = retreatingUnits;
this.OrderHistory = orderHistory; this.OrderHistory = orderHistory;
this.OrderSets = orderSets;
this.Options = options; this.Options = options;
} }
@ -83,6 +90,7 @@ public class World
ReadOnlyCollection<Unit>? units = null, ReadOnlyCollection<Unit>? units = null,
ReadOnlyCollection<RetreatingUnit>? retreatingUnits = null, ReadOnlyCollection<RetreatingUnit>? retreatingUnits = null,
ReadOnlyDictionary<Season, OrderHistory>? orderHistory = null, ReadOnlyDictionary<Season, OrderHistory>? orderHistory = null,
ReadOnlyCollection<OrderSet>? orderSets = null,
Options? options = null) Options? options = null)
: this( : this(
provinces ?? previous.Provinces, provinces ?? previous.Provinces,
@ -92,6 +100,7 @@ public class World
units ?? previous.Units, units ?? previous.Units,
retreatingUnits ?? previous.RetreatingUnits, retreatingUnits ?? previous.RetreatingUnits,
orderHistory ?? previous.OrderHistory, orderHistory ?? previous.OrderHistory,
orderSets ?? previous.OrderSets,
options ?? previous.Options) options ?? previous.Options)
{ {
} }
@ -110,6 +119,7 @@ public class World
new(new List<Unit>()), new(new List<Unit>()),
new(new List<RetreatingUnit>()), new(new List<RetreatingUnit>()),
new(new Dictionary<Season, OrderHistory>()), new(new Dictionary<Season, OrderHistory>()),
new(new List<OrderSet>()),
new Options()); new Options());
} }
@ -123,7 +133,8 @@ public class World
IEnumerable<Season>? seasons = null, IEnumerable<Season>? seasons = null,
IEnumerable<Unit>? units = null, IEnumerable<Unit>? units = null,
IEnumerable<RetreatingUnit>? retreats = null, IEnumerable<RetreatingUnit>? retreats = null,
IEnumerable<KeyValuePair<Season, OrderHistory>>? orders = null) IEnumerable<KeyValuePair<Season, OrderHistory>>? orders = null,
IEnumerable<OrderSet>? orderSets = null)
=> new World( => new World(
previous: this, previous: this,
seasons: seasons == null seasons: seasons == null
@ -137,7 +148,10 @@ public class World
: new(retreats.ToList()), : new(retreats.ToList()),
orderHistory: orders == null orderHistory: orders == null
? this.OrderHistory ? this.OrderHistory
: new(orders.ToDictionary(kvp => kvp.Key, kvp => kvp.Value))); : new(orders.ToDictionary(kvp => kvp.Key, kvp => kvp.Value)),
orderSets: orderSets == null
? this.OrderSets
: new(orderSets.ToList()));
/// <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
@ -174,7 +188,7 @@ public class World
{ {
return this.AddUnits( return this.AddUnits(
"Austria A Bud", "Austria A Bud",
"Austria A Vir", "Austria A Vie",
"Austria F Tri", "Austria F Tri",
"England A Lvp", "England A Lvp",
"England F Edi", "England F Edi",
@ -295,6 +309,17 @@ public class World
return foundUnit; return foundUnit;
} }
public PhaseType GetNextPhaseType()
{
// TODO: Figure how to order build and sustain phases in a staggered multiverse
if (RetreatingUnits.Any())
{
return PhaseType.Retreat;
}
return PhaseType.Movement;
}
/// <summary> /// <summary>
/// The standard Diplomacy provinces. /// The standard Diplomacy provinces.
/// </summary> /// </summary>

View File

@ -2,9 +2,13 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<PackageReference Include="CommandLineParser" Version="2.9.1" />
</ItemGroup>
</Project> </Project>

View File

@ -1,12 +1,99 @@
using System; using CommandLine;
namespace MultiversalDiplomacy using MultiversalDiplomacy.Script;
namespace MultiversalDiplomacy;
internal class Program
{ {
internal class Program
{
static void Main(string[] args) static void Main(string[] args)
{ {
Console.WriteLine("stab"); var parser = Parser.Default;
var parseResult = parser.ParseArguments(args, typeof(ReplOptions));
parseResult
.WithParsed<ReplOptions>(ExecuteRepl);
} }
static void ExecuteRepl(ReplOptions args)
{
// Create a writer to the output file, if specified.
StreamWriter? outputWriter = null;
if (args.OutputFile is not null)
{
string fullPath = Path.GetFullPath(args.OutputFile);
string outputPath = Directory.Exists(fullPath)
? Path.Combine(fullPath, $"{DateTime.UtcNow.ToString("yyyyMMddHHmmss")}.log")
: fullPath;
Console.WriteLine($"Echoing to {outputPath}");
outputWriter = File.AppendText(outputPath);
}
// Define an enumerator for live repl commands.
static IEnumerable<string?> GetReplInputs()
{
// Read user input until it stops (i.e. is null).
string? input = null;
do
{
input = Console.ReadLine();
yield return input;
}
while (input is not null);
// The last null is returned because an EOF means we should quit the repl.
}
// Define an enumerator for input file commands, if specified.
static IEnumerable<string?> GetFileInputs(string inputFile)
{
// Read all lines from the input file.
var fullPath = Path.GetFullPath(inputFile);
foreach (string line in File.ReadLines(fullPath))
{
var trimmed = line.Trim();
Console.WriteLine($"{trimmed}");
yield return trimmed;
}
// Don't return a null, since this will be followed by GetReplInputs.
}
// Set up the input enumerator.
IEnumerable<string?> inputs;
if (args.InputFile is not null)
{
Console.WriteLine($"Reading from {args.InputFile}");
inputs = GetFileInputs(args.InputFile).Concat(GetReplInputs());
}
else
{
inputs = GetReplInputs();
}
IScriptHandler? handler = new ReplScriptHandler();
Console.Write(handler.Prompt);
foreach (string? nextInput in inputs)
{
// Handle quitting directly.
if (nextInput is null || nextInput == "quit" || nextInput == "exit")
{
break;
}
string input = nextInput.Trim();
outputWriter?.WriteLine(input);
outputWriter?.Flush();
// Delegate all other command parsing to the handler.
handler = handler.HandleInput(input);
// Quit if the handler ends processing, otherwise prompt for the next command.
if (handler is null)
{
break;
}
Console.Write(handler.Prompt);
}
Console.WriteLine("exiting");
} }
} }

View File

@ -0,0 +1,13 @@
using CommandLine;
namespace MultiversalDiplomacy;
[Verb("repl", HelpText = "Begin an interactive 5dplomacy session.")]
public class ReplOptions
{
[Option('i', "input", HelpText = "Begin the repl session by executing the commands in this file")]
public string? InputFile { get; set; }
[Option('o', "output", HelpText = "Echo the repl session to this file. Specify a directory to autogenerate a filename")]
public string? OutputFile { get; set; }
}

View File

@ -0,0 +1,114 @@
using MultiversalDiplomacy.Model;
namespace MultiversalDiplomacy.Script;
/// <summary>
/// A script handler for interacting with a loaded game.
/// </summary>
public class GameScriptHandler : IScriptHandler
{
public GameScriptHandler(World world)
{
World = world;
}
public string Prompt => "5dp> ";
public World World { get; set; }
public IScriptHandler? HandleInput(string input)
{
var args = input.Split(' ', StringSplitOptions.RemoveEmptyEntries);
if (args.Length == 0)
{
return this;
}
var command = args[0];
switch (command)
{
case "help":
case "?":
Console.WriteLine("commands:");
Console.WriteLine(" adjudicate: adjudicate the current orders");
Console.WriteLine(" assert: assert about the state of the game");
Console.WriteLine(" list: list things in a game category");
Console.WriteLine(" orders: submit order sets");
Console.WriteLine(" status: overview of the state of the game");
break;
case "adjudicate":
World = GameController.AdjudicateOrders(World);
break;
case "assert" when args.Length == 1:
Console.WriteLine("usage:");
Console.WriteLine(" assert timeline [timeline]@[turn]: timeline exists");
Console.WriteLine(" assert unit [unit spec]: unit exists");
break;
case "assert" when args[1] == "timeline":
// TODO: raise an error if the timeline doesn't exist
Console.WriteLine("WIP");
break;
case "assert" when args[1] == "unit":
// TODO: raise an error if the unit doesn't exist
Console.WriteLine("WIP");
break;
case "list" when args.Length == 1:
Console.WriteLine("usage:");
Console.WriteLine(" list ordersets: unadjudicated order sets");
Console.WriteLine(" list powers: the powers in the game");
break;
case "list" when args[1] == "ordersets":
foreach (OrderSet orderSet in World.OrderSets.Where(os => !os.Adjudicated))
{
var lines = orderSet.Text.Split('\n');
var firstLine = lines[0].Trim();
Console.WriteLine($" {firstLine} ({lines.Length - 1} orders)");
}
break;
case "list" when args[1] == "powers":
Console.WriteLine("Powers:");
foreach (Power power in World.Powers)
{
Console.WriteLine($" {power.Name}");
}
break;
case "orders" when args.Length == 1:
Console.WriteLine("usage: orders [power]");
break;
case "orders":
if (World.Powers.Any(p => p.Name == args[1]))
{
var handler = new OrderSetScriptHandler(this, World, input);
return handler;
}
Console.WriteLine("Unrecognized power");
break;
case "status":
foreach (Season season in World.Seasons)
{
Console.WriteLine($"{season}");
foreach (Unit unit in World.Units.Where(u => u.Season == season))
{
Console.WriteLine($" {unit}");
}
}
break;
default:
Console.WriteLine("Unrecognized command");
break;
}
return this;
}
}

View File

@ -0,0 +1,21 @@
namespace MultiversalDiplomacy.Script;
/// <summary>
/// A handler that interprets and executes 5dp script commands. Script handlers may create additional script handlers
/// and delegate handling to them, allowing a sort of recursive parsing of script commands.
/// </summary>
public interface IScriptHandler
{
/// <summary>
/// When used interactively, the prompt that should be displayed.
/// </summary>
public string Prompt { get; }
/// <summary>
/// Process a line of input.
/// </summary>
/// <returns>
/// The handler that should handle the next line of input, or null if script handling should end.
/// </returns>
public IScriptHandler? HandleInput(string input);
}

View File

@ -0,0 +1,39 @@
using MultiversalDiplomacy.Model;
namespace MultiversalDiplomacy.Script;
/// <summary>
/// A script handler for defining order sets.
/// </summary>
public class OrderSetScriptHandler : IScriptHandler
{
public OrderSetScriptHandler(GameScriptHandler parent, World world, string orderCommand)
{
this.parent = parent;
this.world = world;
this.orderSet = new() { orderCommand };
}
public string Prompt => "...> ";
private GameScriptHandler parent { get; }
private World world { get; }
private List<string> orderSet { get; }
public IScriptHandler? HandleInput(string input)
{
if (string.IsNullOrEmpty(input))
{
parent.World = GameController.SubmitOrderSet(world, string.Join('\n', orderSet));
Console.WriteLine("Submitted order set");
return parent;
}
else
{
orderSet.Add(input);
return this;
}
}
}

View File

@ -0,0 +1,58 @@
namespace MultiversalDiplomacy.Script;
/// <summary>
/// A script handler for the interactive repl.
/// </summary>
public class ReplScriptHandler : IScriptHandler
{
public string Prompt => "5dp> ";
public IScriptHandler? HandleInput(string input)
{
var args = input.Split(' ', StringSplitOptions.RemoveEmptyEntries);
if (args.Length == 0)
{
return this;
}
var command = args[0];
switch (command)
{
case "help":
case "?":
Console.WriteLine("commands:");
Console.WriteLine(" help, ?: print this message");
Console.WriteLine(" stab: stab");
Console.WriteLine(" new: start a new game");
break;
case "stab":
Console.WriteLine("stab");
break;
case "new" when args.Length == 1:
Console.WriteLine("usage:");
Console.WriteLine(" new custom: standard map, declare units");
Console.WriteLine(" new standard: standard map, standard units");
break;
case "new" when args[1] == "custom":
var world = MultiversalDiplomacy.Model.World.WithStandardMap();
var setupHandler = new SetupScriptHandler(world);
Console.WriteLine("Created an empty game");
return setupHandler;
case "new" when args[1] == "standard":
world = GameController.InitializeWorld();
var gameHandler = new GameScriptHandler(world);
Console.WriteLine("Created a standard game");
return gameHandler;
default:
Console.WriteLine("Unrecognized command");
break;
}
return this;
}
}

View File

@ -0,0 +1,135 @@
using MultiversalDiplomacy.Model;
namespace MultiversalDiplomacy.Script;
/// <summary>
/// A script handler for interacting with a loaded game.
/// </summary>
public class SetupScriptHandler : IScriptHandler
{
public SetupScriptHandler(World world)
{
World = world;
}
public string Prompt => "5dp> ";
public World World { get; set; }
public IScriptHandler? HandleInput(string input)
{
var args = input.Split(' ', StringSplitOptions.RemoveEmptyEntries);
if (args.Length == 0)
{
return new GameScriptHandler(World);
}
var command = args[0];
switch (command)
{
case "help":
case "?":
Console.WriteLine("commands:");
Console.WriteLine(" unit: add a unit to the map");
Console.WriteLine(" list: list things in a game category");
Console.WriteLine(" enter a blank line to complete setup");
break;
case "list" when args.Length == 1:
Console.WriteLine("usage:");
Console.WriteLine(" list powers: the powers in the game");
Console.WriteLine(" list units: units created so far");
break;
case "list" when args[1] == "powers":
Console.WriteLine("Powers:");
foreach (Power power in World.Powers)
{
Console.WriteLine($" {power.Name}");
}
break;
case "list" when args[1] == "units":
Console.WriteLine("Units:");
foreach (Unit unit in World.Units)
{
Console.WriteLine($" {unit}");
}
break;
case "unit" when args.Length < 4:
Console.WriteLine("usage: unit [power] [type] [province]");
break;
case "unit":
ParseUnit(args);
break;
default:
Console.WriteLine("Unrecognized command");
break;
}
return this;
}
private void ParseUnit(string[] args)
{
// Parse the power name by substring matching.
string powerArg = args[1].ToLower();
var matchingPowers = World.Powers.Where(p => p.Name.ToLower().StartsWith(powerArg));
if (matchingPowers.Count() < 1)
{
Console.WriteLine($"No power named \"{args[1]}\"");
return;
}
if (matchingPowers.Count() > 1)
{
Console.WriteLine($"Ambiguous power \"{args[1]}\"");
return;
}
Power power = matchingPowers.First();
// Parse the unit type by substring matching.
string typeArg = args[2].ToLower();
var matchingTypes = Enum.GetNames<UnitType>().Where(t => t.ToLower().StartsWith(typeArg));
if (matchingTypes.Count() < 1)
{
Console.WriteLine($"No unit type \"{args[2]}\"");
return;
}
if (matchingTypes.Count() > 1)
{
Console.WriteLine($"Ambiguous unit type \"{args[2]}\"");
return;
}
UnitType type = (UnitType)Enum.Parse(typeof(UnitType), matchingTypes.First());
// Parse the province by matching the province name or one of its abbreviations,
// allowing location specifications separated by a forward slash, e.g. spa/nc.
string provinceArg = args[3].ToLower();
var matchingProvs = World.Provinces.Where(pr
=> pr.Abbreviations.Any(abv => abv.ToLower() == provinceArg)
|| pr.Name.ToLower() == provinceArg);
if (matchingProvs.Count() < 1)
{
Console.WriteLine($"No province matches \"{args[3]}\"");
return;
}
if (matchingTypes.Count() > 1)
{
Console.WriteLine($"Ambiguous province \"{args[3]}\"");
return;
}
// TODO: this does not support multi-location provinces correctly
Province province = matchingProvs.First();
Location location = province.Locations.First(loc
=> type == UnitType.Army && loc.Type == LocationType.Land
|| type == UnitType.Fleet && loc.Type == LocationType.Water);
Unit unit = Unit.Build(location, World.RootSeason, power, type);
World = World.Update(units: World.Units.Append(unit));
Console.WriteLine($"Created unit {unit}");
}
}

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>

View File

@ -22,8 +22,102 @@ When a unit changes the outcome of a battle in the past, only the timeline of th
Since there are many ways to create new timelines, the game would rapidly expand beyond all comprehension if this were not counterbalanced in some way. This happens during the _sustain phase_, which occurs after the fall movement and retreat phases and before the winter buid/disband phase. Since there are many ways to create new timelines, the game would rapidly expand beyond all comprehension if this were not counterbalanced in some way. This happens during the _sustain phase_, which occurs after the fall movement and retreat phases and before the winter buid/disband phase.
(TODO) WIP: the sustain phase has not been implemented yet
### Victory conditions ### Victory conditions
The Great Powers of Europe can only wage multiversal wars because they are lead by extradimensional beings masquerading as human politicians. When a country is eliminated in one timeline, its extradimensional leader is executed, killing them in all timelines. The Great Powers of Europe can only wage multiversal wars because they are lead by extradimensional beings masquerading as human politicians. When a country is eliminated in one timeline, its extradimensional leader is executed, killing them in all timelines.
### Unit designations
In _Diplomacy_, orders refer to provinces, such as "A Mun-Tyr". In _5D Diplomacy with Multiversal Time Travel_, this is insufficient to unambiguously identify a province, since the province exists in multiple timelines across multiple turns. The convention for identifying a multiversal location is `timeline:province:turn`, where `timeline` is the timeline's identifier and `turn` is the turn's identifier. Thus, an army in Munich in timeline "bravo" in Spring 1902 is referenced as "A b:Mun:S02".
(Why this order? Short representations for timelines and turns can be confused for each other, especially for timelines designated with `foxtrot` or `sierra` and turns in Fall or Spring. _5D Diplomacy with Multiversal Time Travel_ is already complicated enough, so the timeline and turn are put on either side of the province.)
WIP: parsing of turn designations has not been implemented yet
### Open convoys
The standard _Diplomacy_ rules require that a convoy order include the convoyed unit's origin and destination. This is hard to coordinate once there are multiple turns and timelines involved. _5D Diplomacy with Multiversal Time Travel_ thus introduces the concept of an _open convoy_, a nonspecific convoy order that can become part of a convoy later.
WIP: open convoys have not been implemented yet
## DATC compliance
WIP
## 5dp script
Order notation is case-insensitive.
### Order element grammar
A unit type is specified with a single letter.
```
<unit-spec> = "A" / "F"
```
A timeline is specified with a primary designation initial, plus a secondary designation if there are enough timelines.
```
<timeline-spec> = <tl-primary> <tl-secondary>
<tl-primary> = "a" / "b" / ...
<tl-secondary> = "" / "1" / "2" / ...
```
A province is specified with a known three-letter abbreviation. A named location in a province may optionally be specfied with its abbreviation. A named location is not necessary for the _form_ of a province to be valid, though an order may be invalid if the omission creates an ambiguity.
```
<province-spec> = <province> ["/" <location>]
<province> = "MUN" / "TYR" / ...
<location> = "nc" / "sc" / ...
```
A turn is specified with the season's initial and the short form of the year. Winter is numbered in the year of the previous Fall.
```
<turn-spec> = <season> <year>
<season> = "S" / "F" / "W"
<year> = "01" / "02" / ...
```
A multiversal location is a timeline, a province, and a turn separated by a colon.
```
<multiverse-spec> = <timeline-spec> ":" <province-spec> ":" <turn-spec>
```
Thus, `b1:IRI:F02` represents the Irish Sea, in Fall 1902, in timeline bravo-prime.
### Order formats
Note that DATC 4.C makes unit designations superfluous outside some build order cases. Thus, the `<unit-spec>` is considered optional in the orders below.
Hold orders require the unit and an indication of a hold order.
```
<hold-order> = [<unit-spec> " "] <multiverse-spec> " " <hold-token>
<hold-token> = "hold" / "holds"
```
Move orders require the unit, target, and an indication of movement instead of support.
```
<move-order> = [<unit-spec> " "] <multiverse-spec> <move-token> <multiverse-spec>
<move-token> = "-" / " to "
```
Support-hold orders require the unit, target, and an indication of support instead of movement.
```
<support-hold-order> = [<unit-spec> " "] <multiverse-spec> <support-token> <multiverse-spec>
<support-token> = " S " / " support " / " supports "
```
Support-move orders require the unit, target, and the support and move indicators.
```
<support-move-order> = [<unit-spec> " "] <multiverse-spec> <support-token> <move-order>
```
Convoy orders WIP.
Retreat orders WIP.
Build orders WIP.
Disband orders WIP.
Sustain orders WIP.

View File

@ -17,16 +17,18 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1667292599, "lastModified": 1717179513,
"narHash": "sha256-7ISOUI1aj6UKMPIL+wwthENL22L3+A9V+jS8Is3QsRo=", "narHash": "sha256-vboIEwIQojofItm2xGCdZCzW96U85l9nDW3ifMuAIdM=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "ef2f213d9659a274985778bff4ca322f3ef3ac68", "rev": "63dacb46bf939521bdc93981b4cbb7ecb58427a0",
"type": "github" "type": "github"
}, },
"original": { "original": {
"id": "nixpkgs", "owner": "NixOS",
"type": "indirect" "ref": "24.05",
"repo": "nixpkgs",
"type": "github"
} }
}, },
"root": { "root": {

View File

@ -1,6 +1,7 @@
{ {
description = "5D Diplomacy With Multiversal Time Travel"; description = "5D Diplomacy With Multiversal Time Travel";
inputs.nixpkgs.url = "github:NixOS/nixpkgs/24.05";
inputs.flake-utils.url = "github:numtide/flake-utils"; inputs.flake-utils.url = "github:numtide/flake-utils";
outputs = { self, nixpkgs, flake-utils }: outputs = { self, nixpkgs, flake-utils }:
@ -9,10 +10,16 @@
in rec { in rec {
devShell = pkgs.mkShell { devShell = pkgs.mkShell {
shellHook = '' shellHook = ''
PS1='\[\e[0;94m\]\u@\H\[\e[0;93m\] 5dplomacy \[\e[0;92m\]$(git rev-parse --short HEAD)\[\e[0m\]\n\W$ ' alias repl="dotnet run --project MultiversalDiplomacy repl"
''; '';
DOTNET_CLI_TELEMETRY_OPTOUT = 1; DOTNET_CLI_TELEMETRY_OPTOUT = 1;
packages = [ pkgs.dotnet-sdk pkgs.dotnetPackages.NUnit3 ]; NIX_LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath [ pkgs.stdenv.cc.cc ];
NIX_LD = builtins.readFile "${pkgs.stdenv.cc}/nix-support/dynamic-linker";
packages = [
pkgs.bashInteractive
pkgs.dotnet-sdk_8
pkgs.dotnetPackages.NUnit3
];
}; };
} }
); );

12
script.txt Normal file
View File

@ -0,0 +1,12 @@
new custom
unit Germany Army Berlin
unit Austria Army Tyrolia
orders Germany
BER - TYR
orders Austria
TYR hold
list ordersets
adjudicate

1
test.sh Normal file
View File

@ -0,0 +1 @@
dotnet test -l "console;verbosity=normal" MultiversalDiplomacyTests/ $@