diff --git a/Makefile b/Makefile index d87b396..48ce1e9 100644 --- a/Makefile +++ b/Makefile @@ -8,3 +8,6 @@ tests: ## run all tests test: ## name=[test name]: run a single test with logging dotnet test MultiversalDiplomacyTests -l "console;verbosity=normal" --filter $(name) + +repl: ## execute the repl + dotnet run --project MultiversalDiplomacy repl diff --git a/MultiversalDiplomacy/Script/ReplScriptHandler.cs b/MultiversalDiplomacy/Script/ReplScriptHandler.cs index c8ed795..4827621 100644 --- a/MultiversalDiplomacy/Script/ReplScriptHandler.cs +++ b/MultiversalDiplomacy/Script/ReplScriptHandler.cs @@ -1,3 +1,5 @@ +using MultiversalDiplomacy.Model; + namespace MultiversalDiplomacy.Script; /// @@ -9,7 +11,53 @@ public class ReplScriptHandler : IScriptHandler public IScriptHandler? HandleInput(string input) { - Console.WriteLine($"[{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(" map : start a new game of the given variant"); + Console.WriteLine(" stab: stab"); + break; + + case "stab": + Console.WriteLine("stab"); + break; + + case "map" when args.Length == 1: + Console.WriteLine("Usage:"); + Console.WriteLine(" map "); + Console.WriteLine("Available variants:"); + Console.WriteLine($" {string.Join(", ", Enum.GetNames().Select(s => s.ToLowerInvariant()))}"); + break; + + case "map" when args.Length > 1: + string mapType = args[1].Trim(); + if (!Enum.TryParse(mapType, ignoreCase: true, out MapType map)) { + Console.WriteLine($"Unknown variant {mapType}"); + Console.WriteLine("Available variants:"); + Console.WriteLine($" {string.Join(", ", Enum.GetNames().Select(s => s.ToLowerInvariant()))}"); + break; + } + World world = World.WithMap(Map.FromType(map)); + Console.WriteLine($"Created a new {map} game"); + return new SetupScriptHandler(world); + + default: + // noop on comments that begin with # + if (!command.StartsWith('#')) { + Console.WriteLine($"Unrecognized command: {command}"); + } + break; + } return this; } diff --git a/MultiversalDiplomacy/Script/SetupScriptHandler.cs b/MultiversalDiplomacy/Script/SetupScriptHandler.cs new file mode 100644 index 0000000..45f5b0f --- /dev/null +++ b/MultiversalDiplomacy/Script/SetupScriptHandler.cs @@ -0,0 +1,167 @@ +using System.Diagnostics.CodeAnalysis; + +using MultiversalDiplomacy.Model; + +namespace MultiversalDiplomacy.Script; + +/// +/// A script handler for modifying a game before it begins. +/// +public class SetupScriptHandler(World world) : IScriptHandler +{ + public string Prompt => "5dp> "; + + public World World { get; private set; } = world; + + 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(" begin: complete setup and start the game"); + Console.WriteLine(" list : list things in a game category"); + Console.WriteLine(" option : set a game option"); + Console.WriteLine(" unit [location]: add a unit to the game"); + Console.WriteLine(" may be \"province/location\""); + break; + + case "begin": + return null; // TODO + + 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 (string powerName in World.Powers) + { + Console.WriteLine($" {powerName}"); + } + break; + + case "list" when args[1] == "units": + Console.WriteLine("Units:"); + foreach (Unit unit in World.Units) + { + Console.WriteLine($" {unit}"); + } + break; + + case "option" when args.Length < 3: + throw new NotImplementedException(); + + case "unit" when args.Length < 4: + Console.WriteLine("usage: unit [power] [type] [province] "); + break; + + case "unit": + string power = args[1]; + string type = args[2]; + string province = args[3]; + string? location = args.Length >= 5 ? args[4] : null; + if (ParseUnit(power, type, province, location, out Unit? newUnit)) { + World = World.Update(units: World.Units.Append(newUnit)); + Console.WriteLine($"Created {newUnit}"); + } + break; + + default: + // noop on comments that begin with # + if (!command.StartsWith('#')) { + Console.WriteLine($"Unrecognized command: {command}"); + } + break; + } + + return this; + } + + private bool ParseUnit( + string powerArg, + string typeArg, + string provinceArg, + string? locationArg, + [NotNullWhen(true)] out Unit? newUnit) + { + newUnit = null; + + // Parse the power name by substring matching. + var matchingPowers = World.Powers.Where(p => p.StartsWithAnyCase(powerArg)); + if (!matchingPowers.Any()) + { + Console.WriteLine($"No power named \"{powerArg}\""); + return false; + } + if (matchingPowers.Count() > 1) + { + Console.WriteLine($"Ambiguous power \"{powerArg}\" between {string.Join(", ", matchingPowers)}"); + return false; + } + string power = matchingPowers.First(); + + // Parse the unit type by substring matching. + var matchingTypes = Enum.GetNames().Where(t => t.StartsWithAnyCase(typeArg)); + if (!matchingTypes.Any()) + { + Console.WriteLine($"No unit type \"{typeArg}\""); + return false; + } + if (matchingTypes.Count() > 1) + { + Console.WriteLine($"Ambiguous unit type \"{typeArg}\" between {string.Join(", ", matchingTypes)}"); + return false; + } + UnitType type = Enum.Parse(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. + if (provinceArg.Contains('/')) { + var split = provinceArg.Split('/', 2); + locationArg ??= split[1]; + provinceArg = split[0]; + } + var matchingProvs = World.Provinces.Where(pr + => pr.Abbreviations.Any(abv => abv.EqualsAnyCase(provinceArg)) + || pr.Name.EqualsAnyCase(provinceArg)); + if (!matchingProvs.Any()) + { + Console.WriteLine($"No province matches \"{provinceArg}\""); + return false; + } + if (matchingProvs.Count() > 1) + { + Console.WriteLine($"Ambiguous province \"{provinceArg}\" between {string.Join(", ", matchingProvs)}"); + return false; + } + Province province = matchingProvs.First(); + Location location; + locationArg ??= type == UnitType.Army ? "land" : "water"; + var matchingLocs = locationArg is null + ? province.Locations.Where(loc + => type == UnitType.Army && loc.Type == LocationType.Land + || type == UnitType.Fleet && loc.Type == LocationType.Water) + : province.Locations.Where(loc + => loc.Name.EqualsAnyCase(locationArg) + || loc.Abbreviation.EqualsAnyCase(locationArg)); + if (!matchingLocs.Any()) { + Console.WriteLine($"Province {province.Name} has no {locationArg} location"); + return false; + } + location = matchingLocs.First(); + + newUnit = Unit.Build(location.Key, Season.First, powerArg, type); + return true; + } +}