diff --git a/MultiversalDiplomacy/GameController.cs b/MultiversalDiplomacy/GameController.cs
new file mode 100644
index 0000000..21864b8
--- /dev/null
+++ b/MultiversalDiplomacy/GameController.cs
@@ -0,0 +1,29 @@
+using MultiversalDiplomacy.Model;
+
+namespace MultiversalDiplomacy;
+
+///
+/// The game controller is a stateless class that defines the basic, high-level operations that can be taken to modify
+/// the game state.
+///
+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)
+ {
+ // TODO: Parse the order sets into orders
+ // TODO: Execute the correct adjudicator for the current world state
+ // TODO: Update the world
+ return world;
+ }
+}
\ No newline at end of file
diff --git a/MultiversalDiplomacy/MultiversalDiplomacy.csproj b/MultiversalDiplomacy/MultiversalDiplomacy.csproj
index 91b464a..dbb9910 100644
--- a/MultiversalDiplomacy/MultiversalDiplomacy.csproj
+++ b/MultiversalDiplomacy/MultiversalDiplomacy.csproj
@@ -7,4 +7,8 @@
enable
+
+
+
+
diff --git a/MultiversalDiplomacy/Program.cs b/MultiversalDiplomacy/Program.cs
index f6d0fcb..f4c8c4a 100644
--- a/MultiversalDiplomacy/Program.cs
+++ b/MultiversalDiplomacy/Program.cs
@@ -1,12 +1,100 @@
-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)
+ var parser = Parser.Default;
+ var parseResult = parser.ParseArguments(args, typeof(ReplOptions));
+
+ parseResult
+ .WithParsed(ExecuteRepl);
+ }
+
+ static void ExecuteRepl(ReplOptions args)
+ {
+ // Create a writer to the output file, if specified.
+ StreamWriter? outputWriter = null;
+ if (args.OutputFile is not null)
{
- Console.WriteLine("stab");
+ 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 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 GetFileInputs(string inputFile)
+ {
+ // Read all lines from the input file.
+ var fullPath = Path.GetFullPath(inputFile);
+ var fileName = Path.GetFileName(fullPath);
+ foreach (string line in File.ReadLines(fullPath))
+ {
+ var trimmed = line.Trim();
+ Console.WriteLine($"{fileName}> {trimmed.TrimEnd()}");
+ yield return trimmed;
+ }
+ // Don't return a null, since this will be followed by GetReplInputs.
+ }
+
+ // Set up the input enumerator.
+ IEnumerable 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");
}
}
\ No newline at end of file
diff --git a/MultiversalDiplomacy/ReplOptions.cs b/MultiversalDiplomacy/ReplOptions.cs
new file mode 100644
index 0000000..7198e35
--- /dev/null
+++ b/MultiversalDiplomacy/ReplOptions.cs
@@ -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; }
+}
\ No newline at end of file
diff --git a/MultiversalDiplomacy/Script/GameScriptHandler.cs b/MultiversalDiplomacy/Script/GameScriptHandler.cs
new file mode 100644
index 0000000..5d871d9
--- /dev/null
+++ b/MultiversalDiplomacy/Script/GameScriptHandler.cs
@@ -0,0 +1,75 @@
+using MultiversalDiplomacy.Model;
+
+namespace MultiversalDiplomacy.Script;
+
+///
+/// A script handler for interacting with a loaded game.
+///
+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(" list: list things in a game category");
+ Console.WriteLine(" orders: submit order sets");
+ 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":
+ var handler = new OrderSetScriptHandler(this, World, input);
+ return handler;
+
+ default:
+ Console.WriteLine("Unrecognized command");
+ break;
+ }
+
+ return this;
+ }
+}
\ No newline at end of file
diff --git a/MultiversalDiplomacy/Script/IScriptHandler.cs b/MultiversalDiplomacy/Script/IScriptHandler.cs
new file mode 100644
index 0000000..20ef562
--- /dev/null
+++ b/MultiversalDiplomacy/Script/IScriptHandler.cs
@@ -0,0 +1,21 @@
+namespace MultiversalDiplomacy.Script;
+
+///
+/// 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.
+///
+public interface IScriptHandler
+{
+ ///
+ /// When used interactively, the prompt that should be displayed.
+ ///
+ public string Prompt { get; }
+
+ ///
+ /// Process a line of input.
+ ///
+ ///
+ /// The handler that should handle the next line of input, or null if script handling should end.
+ ///
+ public IScriptHandler? HandleInput(string input);
+}
diff --git a/MultiversalDiplomacy/Script/OrderSetScriptHandler.cs b/MultiversalDiplomacy/Script/OrderSetScriptHandler.cs
new file mode 100644
index 0000000..e2bc5b5
--- /dev/null
+++ b/MultiversalDiplomacy/Script/OrderSetScriptHandler.cs
@@ -0,0 +1,39 @@
+using MultiversalDiplomacy.Model;
+
+namespace MultiversalDiplomacy.Script;
+
+///
+/// A script handler for defining order sets.
+///
+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 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;
+ }
+ }
+}
\ No newline at end of file
diff --git a/MultiversalDiplomacy/Script/ReplScriptHandler.cs b/MultiversalDiplomacy/Script/ReplScriptHandler.cs
new file mode 100644
index 0000000..1bb4895
--- /dev/null
+++ b/MultiversalDiplomacy/Script/ReplScriptHandler.cs
@@ -0,0 +1,46 @@
+namespace MultiversalDiplomacy.Script;
+
+///
+/// A script handler for the interactive repl.
+///
+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":
+ var world = GameController.InitializeWorld();
+ var handler = new GameScriptHandler(world);
+ Console.WriteLine("Started a new game");
+ return handler;
+
+ default:
+ Console.WriteLine("Unrecognized command");
+ break;
+ }
+
+ return this;
+ }
+}
\ No newline at end of file