From 91d2886d190fdaade85ad3e086b89346cf49bea3 Mon Sep 17 00:00:00 2001 From: Jaculabilis Date: Sat, 31 Dec 2022 12:06:03 -0800 Subject: [PATCH] Add repl cli and script handling framework --- MultiversalDiplomacy/GameController.cs | 29 ++++++ .../MultiversalDiplomacy.csproj | 4 + MultiversalDiplomacy/Program.cs | 98 ++++++++++++++++++- MultiversalDiplomacy/ReplOptions.cs | 13 +++ .../Script/GameScriptHandler.cs | 75 ++++++++++++++ MultiversalDiplomacy/Script/IScriptHandler.cs | 21 ++++ .../Script/OrderSetScriptHandler.cs | 39 ++++++++ .../Script/ReplScriptHandler.cs | 46 +++++++++ 8 files changed, 320 insertions(+), 5 deletions(-) create mode 100644 MultiversalDiplomacy/GameController.cs create mode 100644 MultiversalDiplomacy/ReplOptions.cs create mode 100644 MultiversalDiplomacy/Script/GameScriptHandler.cs create mode 100644 MultiversalDiplomacy/Script/IScriptHandler.cs create mode 100644 MultiversalDiplomacy/Script/OrderSetScriptHandler.cs create mode 100644 MultiversalDiplomacy/Script/ReplScriptHandler.cs 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