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>
|
||||
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>
|
||||
/// 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
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
using System.Linq;
|
||||
|
||||
using MultiversalDiplomacy.Adjudicate.Decision;
|
||||
using MultiversalDiplomacy.Adjudicate.Logging;
|
||||
using MultiversalDiplomacy.Model;
|
||||
|
@ -19,6 +21,36 @@ public class MovementPhaseAdjudicator : IPhaseAdjudicator
|
|||
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)
|
||||
{
|
||||
// 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>
|
||||
public ReadOnlyDictionary<Season, OrderHistory> OrderHistory { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Submitted order sets.
|
||||
/// </summary>
|
||||
public ReadOnlyCollection<OrderSet> OrderSets { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Immutable game options.
|
||||
/// </summary>
|
||||
|
@ -60,6 +65,7 @@ public class World
|
|||
ReadOnlyCollection<Unit> units,
|
||||
ReadOnlyCollection<RetreatingUnit> retreatingUnits,
|
||||
ReadOnlyDictionary<Season, OrderHistory> orderHistory,
|
||||
ReadOnlyCollection<OrderSet> orderSets,
|
||||
Options options)
|
||||
{
|
||||
this.Provinces = provinces;
|
||||
|
@ -69,6 +75,7 @@ public class World
|
|||
this.Units = units;
|
||||
this.RetreatingUnits = retreatingUnits;
|
||||
this.OrderHistory = orderHistory;
|
||||
this.OrderSets = orderSets;
|
||||
this.Options = options;
|
||||
}
|
||||
|
||||
|
@ -83,6 +90,7 @@ public class World
|
|||
ReadOnlyCollection<Unit>? units = null,
|
||||
ReadOnlyCollection<RetreatingUnit>? retreatingUnits = null,
|
||||
ReadOnlyDictionary<Season, OrderHistory>? orderHistory = null,
|
||||
ReadOnlyCollection<OrderSet>? orderSets = null,
|
||||
Options? options = null)
|
||||
: this(
|
||||
provinces ?? previous.Provinces,
|
||||
|
@ -92,6 +100,7 @@ public class World
|
|||
units ?? previous.Units,
|
||||
retreatingUnits ?? previous.RetreatingUnits,
|
||||
orderHistory ?? previous.OrderHistory,
|
||||
orderSets ?? previous.OrderSets,
|
||||
options ?? previous.Options)
|
||||
{
|
||||
}
|
||||
|
@ -110,6 +119,7 @@ public class World
|
|||
new(new List<Unit>()),
|
||||
new(new List<RetreatingUnit>()),
|
||||
new(new Dictionary<Season, OrderHistory>()),
|
||||
new(new List<OrderSet>()),
|
||||
new Options());
|
||||
}
|
||||
|
||||
|
@ -123,7 +133,8 @@ public class World
|
|||
IEnumerable<Season>? seasons = null,
|
||||
IEnumerable<Unit>? units = null,
|
||||
IEnumerable<RetreatingUnit>? retreats = null,
|
||||
IEnumerable<KeyValuePair<Season, OrderHistory>>? orders = null)
|
||||
IEnumerable<KeyValuePair<Season, OrderHistory>>? orders = null,
|
||||
IEnumerable<OrderSet>? orderSets = null)
|
||||
=> new World(
|
||||
previous: this,
|
||||
seasons: seasons == null
|
||||
|
@ -137,7 +148,10 @@ public class World
|
|||
: new(retreats.ToList()),
|
||||
orderHistory: orders == null
|
||||
? 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>
|
||||
/// 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(
|
||||
"Austria A Bud",
|
||||
"Austria A Vir",
|
||||
"Austria A Vie",
|
||||
"Austria F Tri",
|
||||
"England A Lvp",
|
||||
"England F Edi",
|
||||
|
@ -295,6 +309,17 @@ public class World
|
|||
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>
|
||||
/// The standard Diplomacy provinces.
|
||||
/// </summary>
|
||||
|
|
|
@ -7,4 +7,8 @@
|
|||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CommandLineParser" Version="2.9.1" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
@ -1,12 +1,99 @@
|
|||
using System;
|
||||
using CommandLine;
|
||||
|
||||
using MultiversalDiplomacy.Script;
|
||||
|
||||
namespace MultiversalDiplomacy;
|
||||
|
||||
namespace MultiversalDiplomacy
|
||||
{
|
||||
internal class Program
|
||||
{
|
||||
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.
|
||||
|
||||
(TODO)
|
||||
WIP: the sustain phase has not been implemented yet
|
||||
|
||||
### 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.
|
||||
|
||||
### 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 {
|
||||
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;
|
||||
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