Convert Hexapawn to common library

This commit is contained in:
Andrew Cooper
2022-03-18 07:05:27 +11:00
parent 455fea9609
commit 1a8ea5aabd
18 changed files with 527 additions and 603 deletions

View File

@@ -82,9 +82,21 @@ public interface IReadWrite
/// <param name="value">The <see cref="float" /> to be written.</param>
void WriteLine(float value);
/// <summary>
/// Writes an <see cref="object" /> to output.
/// </summary>
/// <param name="value">The <see cref="object" /> to be written.</param>
void Write(object value);
/// <summary>
/// Writes an <see cref="object" /> to output.
/// </summary>
/// <param name="value">The <see cref="object" /> to be written.</param>
void WriteLine(object value);
/// <summary>
/// Writes the contents of a <see cref="Stream" /> to output.
/// </summary>
/// <param name="stream">The <see cref="Stream" /> to be written.</param>
void Write(Stream stream);
void Write(Stream stream, bool keepOpen = false);
}

View File

@@ -95,13 +95,19 @@ public class TextIO : IReadWrite
public void WriteLine(float value) => _output.WriteLine(GetString(value));
public void Write(Stream stream)
public void Write(object value) => _output.Write(value.ToString());
public void WriteLine(object value) => _output.WriteLine(value.ToString());
public void Write(Stream stream, bool keepOpen = false)
{
using var reader = new StreamReader(stream);
while (!reader.EndOfStream)
{
_output.WriteLine(reader.ReadLine());
}
if (!keepOpen) { stream?.Dispose(); }
}
private string GetString(float value) => value < 0 ? $"{value} " : $" {value} ";

View File

@@ -6,10 +6,10 @@ using System.Text;
using static Hexapawn.Pawn;
namespace Hexapawn
namespace Hexapawn;
internal class Board : IEnumerable<Pawn>, IEquatable<Board>
{
internal class Board : IEnumerable<Pawn>, IEquatable<Board>
{
private readonly Pawn[] _cells;
public Board()
@@ -69,5 +69,4 @@ namespace Hexapawn
return hash;
}
}
}

View File

@@ -1,30 +1,25 @@
using System;
using System.Collections.Generic;
namespace Hexapawn
namespace Hexapawn;
// Represents a cell on the board, numbered 1 to 9, with support for finding the reflection of the reference around
// the middle column of the board.
internal class Cell
{
// Represents a cell on the board, numbered 1 to 9, with support for finding the reflection of the reference around
// the middle column of the board.
internal class Cell
{
private static readonly Cell[] _cells = new Cell[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
private static readonly Cell[] _reflected = new Cell[] { 3, 2, 1, 6, 5, 4, 9, 8, 7 };
private readonly int _number;
private Cell(int number)
{
if (number < 1 || number > 9)
{
throw new ArgumentOutOfRangeException(nameof(number), number, "Must be from 1 to 9");
}
_number = number;
}
// Facilitates enumerating all the cells.
public static IEnumerable<Cell> AllCells => _cells;
// Takes a value input by the user and attempts to create a Cell reference
public static bool TryCreate(float input, out Cell cell)
{
@@ -33,21 +28,14 @@ namespace Hexapawn
cell = (int)input;
return true;
}
cell = default;
return false;
static bool IsInteger(float value) => value - (int)value == 0;
}
// Returns the reflection of the cell reference about the middle column of the board.
public Cell Reflected => _reflected[_number - 1];
// Allows the cell reference to be used where an int is expected, such as the indexer in Board.
public static implicit operator int(Cell c) => c._number;
public static implicit operator Cell(int number) => new(number);
public override string ToString() => _number.ToString();
}
}

View File

@@ -1,22 +1,26 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Games.Common.IO;
using Games.Common.Randomness;
using static Hexapawn.Pawn;
using static Hexapawn.Cell;
namespace Hexapawn
namespace Hexapawn;
/// <summary>
/// Encapsulates the logic of the computer player.
/// </summary>
internal class Computer
{
/// <summary>
/// Encapsulates the logic of the computer player.
/// </summary>
internal class Computer : IPlayer
{
private readonly Random _random = new();
private readonly TextIO _io;
private readonly IRandom _random;
private readonly Dictionary<Board, List<Move>> _potentialMoves;
private (List<Move>, Move) _lastMove;
public Computer()
public Computer(TextIO io, IRandom random)
{
_io = io;
_random = random;
// This dictionary implements the data in the original code, which encodes board positions for which the
// computer has a legal move, and the list of possible moves for each position:
// 900 DATA -1,-1,-1,1,0,0,0,1,1,-1,-1,-1,0,1,0,1,0,1
@@ -67,10 +71,6 @@ namespace Hexapawn
};
}
public int Wins { get; private set; }
public void AddWin() => Wins++;
// Try to make a move. We first try to find a legal move for the current board position.
public bool TryMove(Board board)
{
@@ -79,20 +79,16 @@ namespace Hexapawn
{
// We've found a move, so we record it as the last move made, and then announce and make the move.
_lastMove = (moves, move);
// If we found the move from a reflacted match of the board we need to make the reflected move.
if (reflected) { move = move.Reflected; }
Console.WriteLine($"I move {move}");
_io.WriteLine($"I move {move}");
move.Execute(board);
return true;
}
// We haven't found a move for this board position, so remove the previous move that led to this board
// position from future consideration. We don't want to make that move again, because we now know it's a
// non-winning move.
ExcludeLastMoveFromFuturePlay();
return false;
}
@@ -106,13 +102,11 @@ namespace Hexapawn
reflected = false;
return true;
}
if (_potentialMoves.TryGetValue(board.Reflected, out moves))
{
reflected = true;
return true;
}
reflected = default;
return false;
}
@@ -126,8 +120,7 @@ namespace Hexapawn
move = moves[_random.Next(moves.Count)];
return true;
}
Console.Write("I resign.");
_io.WriteLine("I resign.");
move = null;
return false;
}
@@ -142,5 +135,4 @@ namespace Hexapawn
public bool IsFullyAdvanced(Board board) =>
board[9] == Black || board[8] == Black || board[7] == Black;
}
}

View File

@@ -1,48 +1,40 @@
using System;
using Games.Common.IO;
namespace Hexapawn
namespace Hexapawn;
// A single game of Hexapawn
internal class Game
{
// Runs a single game of Hexapawn
internal class Game
{
private readonly TextIO _io;
private readonly Board _board;
private readonly Human _human;
private readonly Computer _computer;
public Game(Human human, Computer computer)
public Game(TextIO io)
{
_board = new Board();
_human = human;
_computer = computer;
_io = io;
}
public IPlayer Play()
public object Play(Human human, Computer computer)
{
Console.WriteLine(_board);
_io.WriteLine(_board);
while(true)
{
_human.Move(_board);
Console.WriteLine(_board);
if (!_computer.TryMove(_board))
human.Move(_board);
_io.WriteLine(_board);
if (!computer.TryMove(_board))
{
return _human;
return human;
}
Console.WriteLine(_board);
if (_computer.IsFullyAdvanced(_board) || _human.HasNoPawns(_board))
_io.WriteLine(_board);
if (computer.IsFullyAdvanced(_board) || human.HasNoPawns(_board))
{
return _computer;
return computer;
}
if (!_human.HasLegalMove(_board))
if (!human.HasLegalMove(_board))
{
Console.Write("You can't move, so ");
return _computer;
}
_io.Write("You can't move, so ");
return computer;
}
}
}

View File

@@ -1,27 +1,47 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Games.Common.IO;
using Games.Common.Randomness;
using Hexapawn.Resources;
namespace Hexapawn
namespace Hexapawn;
// Runs series of games between the computer and the human player
internal class GameSeries
{
// Runs series of games between the computer and the human player
internal class GameSeries
private readonly TextIO _io;
private readonly Computer _computer;
private readonly Human _human;
private readonly Dictionary<object, int> _wins;
public GameSeries(TextIO io, IRandom random)
{
private readonly Computer _computer = new();
private readonly Human _human = new();
_io = io;
_computer = new(io, random);
_human = new(io);
_wins = new() { [_computer] = 0, [_human] = 0 };
}
public void Play()
{
_io.Write(Resource.Streams.Title);
if (_io.GetYesNo("Instructions") == 'Y')
{
_io.Write(Resource.Streams.Instructions);
}
while (true)
{
var game = new Game(_human, _computer);
var game = new Game(_io);
var winner = game.Play();
winner.AddWin();
Console.WriteLine(winner == _computer ? "I win." : "You win.");
var winner = game.Play(_human, _computer);
_wins[winner]++;
_io.WriteLine(winner == _computer ? "I win." : "You win.");
Console.Write($"I have won {_computer.Wins} and you {_human.Wins}");
Console.WriteLine($" out of {_computer.Wins + _human.Wins} games.");
Console.WriteLine();
}
_io.Write($"I have won {_wins[_computer]} and you {_wins[_human]}");
_io.WriteLine($" out of {_wins.Values.Sum()} games.");
_io.WriteLine();
}
}
}

View File

@@ -5,6 +5,10 @@
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Include="Resources\*.txt" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\00_Common\dotnet\Games.Common\Games.Common.csproj" />
</ItemGroup>

View File

@@ -1,29 +1,33 @@
using System;
using System.Linq;
using Games.Common.IO;
using static Hexapawn.Cell;
using static Hexapawn.Move;
using static Hexapawn.Pawn;
namespace Hexapawn
namespace Hexapawn;
internal class Human
{
internal class Human : IPlayer
private readonly TextIO _io;
public Human(TextIO io)
{
public int Wins { get; private set; }
_io = io;
}
public void Move(Board board)
{
while (true)
{
var move = Input.GetMove("Your move");
var move = _io.ReadMove("Your move");
if (TryExecute(board, move)) { return; }
Console.WriteLine("Illegal move.");
_io.WriteLine("Illegal move.");
}
}
public void AddWin() => Wins++;
public bool HasLegalMove(Board board)
{
foreach (var from in AllCells.Where(c => c > 3))
@@ -60,5 +64,4 @@ namespace Hexapawn
return false;
}
}
}

View File

@@ -1,8 +0,0 @@
namespace Hexapawn
{
// An interface implemented by a player of the game to track the number of wins.
internal interface IPlayer
{
void AddWin();
}
}

View File

@@ -0,0 +1,42 @@
using System;
using System.Linq;
using Games.Common.IO;
namespace Hexapawn;
// Provides input methods which emulate the BASIC interpreter's keyboard input routines
internal static class IReadWriteExtensions
{
internal static char GetYesNo(this IReadWrite io, string prompt)
{
while (true)
{
var response = io.ReadString($"{prompt} (Y-N)").FirstOrDefault();
if ("YyNn".Contains(response))
{
return char.ToUpperInvariant(response);
}
}
}
// Implements original code:
// 120 PRINT "YOUR MOVE";
// 121 INPUT M1,M2
// 122 IF M1=INT(M1)AND M2=INT(M2)AND M1>0 AND M1<10 AND M2>0 AND M2<10 THEN 130
// 123 PRINT "ILLEGAL CO-ORDINATES."
// 124 GOTO 120
internal static Move ReadMove(this IReadWrite io, string prompt)
{
while(true)
{
var (from, to) = io.Read2Numbers(prompt);
if (Move.TryCreate(from, to, out var move))
{
return move;
}
io.WriteLine("Illegal Coordinates.");
}
}
}

View File

@@ -1,112 +0,0 @@
using System;
using System.Linq;
namespace Hexapawn
{
// Provides input methods which emulate the BASIC interpreter's keyboard input routines
internal static class Input
{
internal static char GetYesNo(string prompt)
{
while (true)
{
Console.Write($"{prompt} (Y-N)? ");
var response = Console.ReadLine().FirstOrDefault();
if ("YyNn".Contains(response))
{
return char.ToUpperInvariant(response);
}
}
}
// Implements original code:
// 120 PRINT "YOUR MOVE";
// 121 INPUT M1,M2
// 122 IF M1=INT(M1)AND M2=INT(M2)AND M1>0 AND M1<10 AND M2>0 AND M2<10 THEN 130
// 123 PRINT "ILLEGAL CO-ORDINATES."
// 124 GOTO 120
internal static Move GetMove(string prompt)
{
while(true)
{
ReadNumbers(prompt, out var from, out var to);
if (Move.TryCreate(from, to, out var move))
{
return move;
}
Console.WriteLine("Illegal Coordinates.");
}
}
internal static void Prompt(string text = "") => Console.Write($"{text}? ");
internal static void ReadNumbers(string prompt, out float number1, out float number2)
{
while (!TryReadNumbers(prompt, out number1, out number2))
{
prompt = "";
}
}
private static bool TryReadNumbers(string prompt, out float number1, out float number2)
{
Prompt(prompt);
var inputValues = ReadStrings();
if (!TryParseNumber(inputValues[0], out number1))
{
number2 = default;
return false;
}
if (inputValues.Length == 1)
{
return TryReadNumber("?", out number2);
}
if (!TryParseNumber(inputValues[1], out number2))
{
number2 = default;
return false;
}
if (inputValues.Length > 2)
{
Console.WriteLine("!Extra input ingored");
}
return true;
}
private static bool TryReadNumber(string prompt, out float number)
{
Prompt(prompt);
var inputValues = ReadStrings();
if (!TryParseNumber(inputValues[0], out number))
{
return false;
}
if (inputValues.Length > 1)
{
Console.WriteLine("!Extra input ingored");
}
return true;
}
private static string[] ReadStrings() => Console.ReadLine().Split(',', StringSplitOptions.TrimEntries);
private static bool TryParseNumber(string text, out float number)
{
if (float.TryParse(text, out number)) { return true; }
Console.WriteLine("!Number expected - retry input line");
number = default;
return false;
}
}
}

View File

@@ -1,12 +1,12 @@
using static Hexapawn.Pawn;
namespace Hexapawn
namespace Hexapawn;
/// <summary>
/// Represents a move which may, or may not, be legal.
/// </summary>
internal class Move
{
/// <summary>
/// Represents a move which may, or may not, be legal.
/// </summary>
internal class Move
{
private readonly Cell _from;
private readonly Cell _to;
private readonly int _metric;
@@ -64,5 +64,4 @@ namespace Hexapawn
}
public override string ToString() => $"from {_from} to {_to}";
}
}

View File

@@ -1,8 +1,8 @@
namespace Hexapawn
namespace Hexapawn;
// Represents the contents of a cell on the board
internal class Pawn
{
// Represents the contents of a cell on the board
internal class Pawn
{
public static readonly Pawn Black = new('X');
public static readonly Pawn White = new('O');
public static readonly Pawn None = new('.');
@@ -15,5 +15,5 @@ namespace Hexapawn
}
public override string ToString() => _symbol.ToString();
}
}

View File

@@ -1,71 +1,6 @@
using System;
using Games.Common.IO;
using Games.Common.Randomness;
using Hexapawn;
namespace Hexapawn
{
// Hexapawn: Interpretation of hexapawn game as presented in
// Martin Gardner's "The Unexpected Hanging and Other Mathematic
// al Diversions", Chapter Eight: A Matchbox Game-Learning Machine.
// Original version for H-P timeshare system by R.A. Kaapke 5/5/76
// Instructions by Jeff Dalton
// Conversion to MITS BASIC by Steve North
// Conversion to C# by Andrew Cooper
class Program
{
static void Main()
{
DisplayTitle();
new GameSeries(new ConsoleIO(), new RandomNumberGenerator()).Play();
if (Input.GetYesNo("Instructions") == 'Y')
{
DisplayInstructions();
}
var games = new GameSeries();
games.Play();
}
private static void DisplayTitle()
{
Console.WriteLine(" Hexapawn");
Console.WriteLine(" Creative Computing Morristown, New Jersey");
Console.WriteLine();
Console.WriteLine();
Console.WriteLine();
}
private static void DisplayInstructions()
{
Console.WriteLine();
Console.WriteLine("This program plays the game of Hexapawn.");
Console.WriteLine("Hexapawn is played with Chess pawns on a 3 by 3 board.");
Console.WriteLine("The pawns are move as in Chess - one space forward to");
Console.WriteLine("an empty space, or one space forward and diagonally to");
Console.WriteLine("capture an opposing man. On the board, your pawns");
Console.WriteLine("are 'O', the computer's pawns are 'X', and empty");
Console.WriteLine("squares are '.'. To enter a move, type the number of");
Console.WriteLine("the square you are moving from, followed by the number");
Console.WriteLine("of the square you will move to. The numbers must be");
Console.WriteLine("separated by a comma.");
Console.WriteLine();
Console.WriteLine("The computer starts a series of games knowing only when");
Console.WriteLine("the game is won (a draw is impossible) and how to move.");
Console.WriteLine("It has no strategy at first and just moves randomly.");
Console.WriteLine("However, it learns from each game. Thus winning becomes");
Console.WriteLine("more and more difficult. Also, to help offset your");
Console.WriteLine("initial advantage, you will not be told how to win the");
Console.WriteLine("game but must learn this by playing.");
Console.WriteLine();
Console.WriteLine("The numbering of the board is as follows:");
Console.WriteLine(" 123");
Console.WriteLine(" 456");
Console.WriteLine(" 789");
Console.WriteLine();
Console.WriteLine("For example, to move your rightmost pawn forward,");
Console.WriteLine("you would type 9,6 in response to the question");
Console.WriteLine("'Your move ?'. Since I'm a good sport, you'll always");
Console.WriteLine("go first.");
Console.WriteLine();
}
}
}

View File

@@ -0,0 +1,30 @@
This program plays the game of Hexapawn.
Hexapawn is played with Chess pawns on a 3 by 3 board.
The pawns are move as in Chess - one space forward to
an empty space, or one space forward and diagonally to
capture an opposing man. On the board, your pawns
are 'O', the computer's pawns are 'X', and empty
squares are '.'. To enter a move, type the number of
the square you are moving from, followed by the number
of the square you will move to. The numbers must be
separated by a comma.
The computer starts a series of games knowing only when
the game is won (a draw is impossible) and how to move.
It has no strategy at first and just moves randomly.
However, it learns from each game. Thus winning becomes
more and more difficult. Also, to help offset your
initial advantage, you will not be told how to win the
game but must learn this by playing.
The numbering of the board is as follows:
123
456
789
For example, to move your rightmost pawn forward,
you would type 9,6 in response to the question
'Your move ?'. Since I'm a good sport, you'll always
go first.

View File

@@ -0,0 +1,17 @@
using System.IO;
using System.Reflection;
using System.Runtime.CompilerServices;
namespace Hexapawn.Resources;
internal static class Resource
{
internal static class Streams
{
public static Stream Instructions => GetStream();
public static Stream Title => GetStream();
}
private static Stream GetStream([CallerMemberName] string name = null)
=> Assembly.GetExecutingAssembly().GetManifestResourceStream($"Hexapawn.Resources.{name}.txt");
}

View File

@@ -0,0 +1,5 @@
Hexapawn
Creative Computing Morristown, New Jersey