Compare commits
12 Commits
e580b91509
...
5c2fce0fa7
Author | SHA1 | Date |
---|---|---|
Jaculabilis | 5c2fce0fa7 | |
Jaculabilis | 171a675595 | |
Jaculabilis | 3cde558d33 | |
Jaculabilis | 26329e9186 | |
Jaculabilis | 66a85f2360 | |
Jaculabilis | f61982d91e | |
Jaculabilis | 8ec727498a | |
Jaculabilis | 77347734c4 | |
Jaculabilis | cdef096477 | |
Jaculabilis | 4b8cf48567 | |
Jaculabilis | 0bf59387a2 | |
Jaculabilis | 73fc94e5c7 |
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
namespace MultiversalDiplomacy.Model;
|
||||||
|
|
||||||
|
public enum PhaseType
|
||||||
|
{
|
||||||
|
Movement = 1,
|
||||||
|
Retreat = 2,
|
||||||
|
Build = 3,
|
||||||
|
Sustain = 4,
|
||||||
|
}
|
|
@ -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>
|
||||||
|
|
|
@ -7,4 +7,8 @@
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="CommandLineParser" Version="2.9.1" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
@ -1,12 +1,99 @@
|
||||||
using System;
|
using CommandLine;
|
||||||
|
|
||||||
|
using MultiversalDiplomacy.Script;
|
||||||
|
|
||||||
|
namespace MultiversalDiplomacy;
|
||||||
|
|
||||||
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");
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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; }
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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}");
|
||||||
|
}
|
||||||
|
}
|
96
README.md
96
README.md
|
@ -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.
|
|
@ -10,6 +10,7 @@
|
||||||
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$ '
|
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 ];
|
packages = [ pkgs.dotnet-sdk pkgs.dotnetPackages.NUnit3 ];
|
||||||
|
|
|
@ -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
|
Loading…
Reference in New Issue