Files
minesweeper/src/Minesweeper.Cli/CommandParser.cs
T
2026-09-05 21:53:46 +00:00

96 lines
4.0 KiB
C#

using System.Globalization;
using Minesweeper.Core;
namespace Minesweeper.Cli;
/// <summary>Turns typed text and raw keystrokes into <see cref="Command"/> instances. This is the
/// only place that translates the one-based coordinates a player sees into engine coordinates.</summary>
public static class CommandParser
{
/// <summary>Parses a typed line. Coordinates are one-based, e.g. <c>reveal 3 4</c>.</summary>
public static Command Parse(string input)
{
var text = input.Trim();
if (text.Length == 0)
{
return new Command.Noop();
}
var parts = text.Split([' ', ',', ';', '\t'], StringSplitOptions.RemoveEmptyEntries);
var verb = parts[0].ToLowerInvariant();
switch (verb)
{
case "q" or "quit" or "exit":
return new Command.Quit();
case "n" or "new":
return new Command.NewGame();
case "h" or "help" or "?":
return new Command.ShowHelp();
case "d" or "difficulty":
return ParseDifficulty(parts, text);
case "r" or "reveal" or "open":
return ParseCell(parts, 1, cell => new Command.Reveal(cell), text);
case "f" or "flag" or "mark":
return ParseCell(parts, 1, cell => new Command.Flag(cell), text);
case "c" or "chord":
return ParseCell(parts, 1, cell => new Command.Chord(cell), text);
case "go" or "goto":
return ParseCell(parts, 1, cell => new Command.SetCursor(cell), text);
}
// A bare "3 4" reveals that cell, since revealing is by far the most common action.
return ParseCell(parts, 0, cell => new Command.Reveal(cell), text);
}
/// <summary>Maps a keystroke from keyboard-driven mode onto a command.</summary>
public static Command FromKey(ConsoleKeyInfo key) => key.Key switch
{
ConsoleKey.UpArrow => new Command.MoveCursor(-1, 0),
ConsoleKey.DownArrow => new Command.MoveCursor(1, 0),
ConsoleKey.LeftArrow => new Command.MoveCursor(0, -1),
ConsoleKey.RightArrow => new Command.MoveCursor(0, 1),
ConsoleKey.Enter or ConsoleKey.Spacebar => new Command.RevealAtCursor(),
ConsoleKey.F => new Command.FlagAtCursor(),
ConsoleKey.C => new Command.ChordAtCursor(),
ConsoleKey.N => new Command.NewGame(),
ConsoleKey.H => new Command.ShowHelp(),
ConsoleKey.D1 => new Command.ChangeDifficulty(GameDifficulty.Beginner),
ConsoleKey.D2 => new Command.ChangeDifficulty(GameDifficulty.Intermediate),
ConsoleKey.D3 => new Command.ChangeDifficulty(GameDifficulty.Expert),
ConsoleKey.Escape or ConsoleKey.Q => new Command.Quit(),
_ => new Command.Noop(),
};
private static Command ParseDifficulty(string[] parts, string text)
{
if (parts.Length < 2)
{
return new Command.ShowHelp();
}
return parts[1].ToLowerInvariant() switch
{
"1" or "b" or "beginner" or "easy" => new Command.ChangeDifficulty(GameDifficulty.Beginner),
"2" or "i" or "intermediate" or "medium" => new Command.ChangeDifficulty(GameDifficulty.Intermediate),
"3" or "e" or "expert" or "hard" => new Command.ChangeDifficulty(GameDifficulty.Expert),
_ => new Command.Unknown(text),
};
}
private static Command ParseCell(string[] parts, int offset, Func<CellLocation, Command> factory, string text)
{
if (parts.Length - offset < 2 ||
!TryParseCoordinate(parts[offset], out var row) ||
!TryParseCoordinate(parts[offset + 1], out var column))
{
return new Command.Unknown(text);
}
return factory(new CellLocation(row - 1, column - 1));
}
private static bool TryParseCoordinate(string token, out int value) =>
int.TryParse(token, NumberStyles.None, CultureInfo.InvariantCulture, out value) && value > 0;
}