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,68 +6,67 @@ 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()
{
private readonly Pawn[] _cells;
public Board()
_cells = new[]
{
_cells = new[]
Black, Black, Black,
None, None, None,
White, White, White
};
}
public Board(params Pawn[] cells)
{
_cells = cells;
}
public Pawn this[int index]
{
get => _cells[index - 1];
set => _cells[index - 1] = value;
}
public Board Reflected => new(Cell.AllCells.Select(c => this[c.Reflected]).ToArray());
public IEnumerator<Pawn> GetEnumerator() => _cells.OfType<Pawn>().GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public override string ToString()
{
var builder = new StringBuilder().AppendLine();
for (int row = 0; row < 3; row++)
{
builder.Append(" ");
for (int col = 0; col < 3; col++)
{
Black, Black, Black,
None, None, None,
White, White, White
};
}
public Board(params Pawn[] cells)
{
_cells = cells;
}
public Pawn this[int index]
{
get => _cells[index - 1];
set => _cells[index - 1] = value;
}
public Board Reflected => new(Cell.AllCells.Select(c => this[c.Reflected]).ToArray());
public IEnumerator<Pawn> GetEnumerator() => _cells.OfType<Pawn>().GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public override string ToString()
{
var builder = new StringBuilder().AppendLine();
for (int row = 0; row < 3; row++)
{
builder.Append(" ");
for (int col = 0; col < 3; col++)
{
builder.Append(_cells[row * 3 + col]);
}
builder.AppendLine();
builder.Append(_cells[row * 3 + col]);
}
return builder.ToString();
builder.AppendLine();
}
return builder.ToString();
}
public bool Equals(Board other) => other?.Zip(this).All(x => x.First == x.Second) ?? false;
public bool Equals(Board other) => other?.Zip(this).All(x => x.First == x.Second) ?? false;
public override bool Equals(object obj) => Equals(obj as Board);
public override bool Equals(object obj) => Equals(obj as Board);
public override int GetHashCode()
public override int GetHashCode()
{
var hash = 19;
for (int i = 0; i < 9; i++)
{
var hash = 19;
for (int i = 0; i < 9; i++)
{
hash = hash * 53 + _cells[i].GetHashCode();
}
return hash;
hash = hash * 53 + _cells[i].GetHashCode();
}
return hash;
}
}

View File

@@ -1,53 +1,41 @@
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)
{
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)
{
if (number < 1 || number > 9)
{
throw new ArgumentOutOfRangeException(nameof(number), number, "Must be from 1 to 9");
}
_number = number;
throw new ArgumentOutOfRangeException(nameof(number), number, "Must be from 1 to 9");
}
// 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)
{
if (IsInteger(input) && input >= 1 && input <= 9)
{
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();
_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)
{
if (IsInteger(input) && input >= 1 && input <= 9)
{
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,146 +1,138 @@
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 TextIO _io;
private readonly IRandom _random;
private readonly Dictionary<Board, List<Move>> _potentialMoves;
private (List<Move>, Move) _lastMove;
public Computer(TextIO io, IRandom random)
{
private readonly Random _random = new();
private readonly Dictionary<Board, List<Move>> _potentialMoves;
private (List<Move>, Move) _lastMove;
_io = io;
_random = random;
public Computer()
// 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
// 905 DATA -1,0,-1,-1,1,0,0,0,1,0,-1,-1,1,-1,0,0,0,1
// 910 DATA -1,0,-1,1,1,0,0,1,0,-1,-1,0,1,0,1,0,0,1
// 915 DATA 0,-1,-1,0,-1,1,1,0,0,0,-1,-1,-1,1,1,1,0,0
// 920 DATA -1,0,-1,-1,0,1,0,1,0,0,-1,-1,0,1,0,0,0,1
// 925 DATA 0,-1,-1,0,1,0,1,0,0,-1,0,-1,1,0,0,0,0,1
// 930 DATA 0,0,-1,-1,-1,1,0,0,0,-1,0,0,1,1,1,0,0,0
// 935 DATA 0,-1,0,-1,1,1,0,0,0,-1,0,0,-1,-1,1,0,0,0
// 940 DATA 0,0,-1,-1,1,0,0,0,0,0,-1,0,1,-1,0,0,0,0
// 945 DATA -1,0,0,-1,1,0,0,0,0
// 950 DATA 24,25,36,0,14,15,36,0,15,35,36,47,36,58,59,0
// 955 DATA 15,35,36,0,24,25,26,0,26,57,58,0
// 960 DATA 26,35,0,0,47,48,0,0,35,36,0,0,35,36,0,0
// 965 DATA 36,0,0,0,47,58,0,0,15,0,0,0
// 970 DATA 26,47,0,0,47,58,0,0,35,36,47,0,28,58,0,0,15,47,0,0
//
// The original code loaded this data into two arrays.
// 40 FOR I=1 TO 19: FOR J=1 TO 9: READ B(I,J): NEXT J: NEXT I
// 45 FOR I=1 TO 19: FOR J=1 TO 4: READ M(I,J): NEXT J: NEXT I
//
// When finding moves for the computer the first array was searched for the current board position, or the
// reflection of it, and the resulting index was used in the second array to get the possible moves.
// With this dictionary we can just use the current board as the index, and retrieve a list of moves for
// consideration by the computer.
_potentialMoves = new()
{
// 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
// 905 DATA -1,0,-1,-1,1,0,0,0,1,0,-1,-1,1,-1,0,0,0,1
// 910 DATA -1,0,-1,1,1,0,0,1,0,-1,-1,0,1,0,1,0,0,1
// 915 DATA 0,-1,-1,0,-1,1,1,0,0,0,-1,-1,-1,1,1,1,0,0
// 920 DATA -1,0,-1,-1,0,1,0,1,0,0,-1,-1,0,1,0,0,0,1
// 925 DATA 0,-1,-1,0,1,0,1,0,0,-1,0,-1,1,0,0,0,0,1
// 930 DATA 0,0,-1,-1,-1,1,0,0,0,-1,0,0,1,1,1,0,0,0
// 935 DATA 0,-1,0,-1,1,1,0,0,0,-1,0,0,-1,-1,1,0,0,0
// 940 DATA 0,0,-1,-1,1,0,0,0,0,0,-1,0,1,-1,0,0,0,0
// 945 DATA -1,0,0,-1,1,0,0,0,0
// 950 DATA 24,25,36,0,14,15,36,0,15,35,36,47,36,58,59,0
// 955 DATA 15,35,36,0,24,25,26,0,26,57,58,0
// 960 DATA 26,35,0,0,47,48,0,0,35,36,0,0,35,36,0,0
// 965 DATA 36,0,0,0,47,58,0,0,15,0,0,0
// 970 DATA 26,47,0,0,47,58,0,0,35,36,47,0,28,58,0,0,15,47,0,0
//
// The original code loaded this data into two arrays.
// 40 FOR I=1 TO 19: FOR J=1 TO 9: READ B(I,J): NEXT J: NEXT I
// 45 FOR I=1 TO 19: FOR J=1 TO 4: READ M(I,J): NEXT J: NEXT I
//
// When finding moves for the computer the first array was searched for the current board position, or the
// reflection of it, and the resulting index was used in the second array to get the possible moves.
// With this dictionary we can just use the current board as the index, and retrieve a list of moves for
// consideration by the computer.
_potentialMoves = new()
{
[new(Black, Black, Black, White, None, None, None, White, White)] = Moves((2, 4), (2, 5), (3, 6)),
[new(Black, Black, Black, None, White, None, White, None, White)] = Moves((1, 4), (1, 5), (3, 6)),
[new(Black, None, Black, Black, White, None, None, None, White)] = Moves((1, 5), (3, 5), (3, 6), (4, 7)),
[new(None, Black, Black, White, Black, None, None, None, White)] = Moves((3, 6), (5, 8), (5, 9)),
[new(Black, None, Black, White, White, None, None, White, None)] = Moves((1, 5), (3, 5), (3, 6)),
[new(Black, Black, None, White, None, White, None, None, White)] = Moves((2, 4), (2, 5), (2, 6)),
[new(None, Black, Black, None, Black, White, White, None, None)] = Moves((2, 6), (5, 7), (5, 8)),
[new(None, Black, Black, Black, White, White, White, None, None)] = Moves((2, 6), (3, 5)),
[new(Black, None, Black, Black, None, White, None, White, None)] = Moves((4, 7), (4, 8)),
[new(None, Black, Black, None, White, None, None, None, White)] = Moves((3, 5), (3, 6)),
[new(None, Black, Black, None, White, None, White, None, None)] = Moves((3, 5), (3, 6)),
[new(Black, None, Black, White, None, None, None, None, White)] = Moves((3, 6)),
[new(None, None, Black, Black, Black, White, None, None, None)] = Moves((4, 7), (5, 8)),
[new(Black, None, None, White, White, White, None, None, None)] = Moves((1, 5)),
[new(None, Black, None, Black, White, White, None, None, None)] = Moves((2, 6), (4, 7)),
[new(Black, None, None, Black, Black, White, None, None, None)] = Moves((4, 7), (5, 8)),
[new(None, None, Black, Black, White, None, None, None, None)] = Moves((3, 5), (3, 6), (4, 7)),
[new(None, Black, None, White, Black, None, None, None, None)] = Moves((2, 8), (5, 8)),
[new(Black, None, None, Black, White, None, None, None, None)] = Moves((1, 5), (4, 7))
};
}
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)
{
if (TryGetMoves(board, out var moves, out var reflected) &&
TrySelectMove(moves, out var move))
{
// 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}");
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;
}
// Looks up the given board and its reflection in the potential moves dictionary. If it's found then we have a
// list of potential moves. If the board is not found in the dictionary then the computer has no legal moves,
// and the human player wins.
private bool TryGetMoves(Board board, out List<Move> moves, out bool reflected)
{
if (_potentialMoves.TryGetValue(board, out moves))
{
reflected = false;
return true;
}
if (_potentialMoves.TryGetValue(board.Reflected, out moves))
{
reflected = true;
return true;
}
reflected = default;
return false;
}
// Get a random move from the list. If the list is empty, then we've previously eliminated all the moves for
// this board position as being non-winning moves. We therefore resign the game.
private bool TrySelectMove(List<Move> moves, out Move move)
{
if (moves.Any())
{
move = moves[_random.Next(moves.Count)];
return true;
}
Console.Write("I resign.");
move = null;
return false;
}
private void ExcludeLastMoveFromFuturePlay()
{
var (moves, move) = _lastMove;
moves.Remove(move);
}
private static List<Move> Moves(params Move[] moves) => moves.ToList();
public bool IsFullyAdvanced(Board board) =>
board[9] == Black || board[8] == Black || board[7] == Black;
[new(Black, Black, Black, White, None, None, None, White, White)] = Moves((2, 4), (2, 5), (3, 6)),
[new(Black, Black, Black, None, White, None, White, None, White)] = Moves((1, 4), (1, 5), (3, 6)),
[new(Black, None, Black, Black, White, None, None, None, White)] = Moves((1, 5), (3, 5), (3, 6), (4, 7)),
[new(None, Black, Black, White, Black, None, None, None, White)] = Moves((3, 6), (5, 8), (5, 9)),
[new(Black, None, Black, White, White, None, None, White, None)] = Moves((1, 5), (3, 5), (3, 6)),
[new(Black, Black, None, White, None, White, None, None, White)] = Moves((2, 4), (2, 5), (2, 6)),
[new(None, Black, Black, None, Black, White, White, None, None)] = Moves((2, 6), (5, 7), (5, 8)),
[new(None, Black, Black, Black, White, White, White, None, None)] = Moves((2, 6), (3, 5)),
[new(Black, None, Black, Black, None, White, None, White, None)] = Moves((4, 7), (4, 8)),
[new(None, Black, Black, None, White, None, None, None, White)] = Moves((3, 5), (3, 6)),
[new(None, Black, Black, None, White, None, White, None, None)] = Moves((3, 5), (3, 6)),
[new(Black, None, Black, White, None, None, None, None, White)] = Moves((3, 6)),
[new(None, None, Black, Black, Black, White, None, None, None)] = Moves((4, 7), (5, 8)),
[new(Black, None, None, White, White, White, None, None, None)] = Moves((1, 5)),
[new(None, Black, None, Black, White, White, None, None, None)] = Moves((2, 6), (4, 7)),
[new(Black, None, None, Black, Black, White, None, None, None)] = Moves((4, 7), (5, 8)),
[new(None, None, Black, Black, White, None, None, None, None)] = Moves((3, 5), (3, 6), (4, 7)),
[new(None, Black, None, White, Black, None, None, None, None)] = Moves((2, 8), (5, 8)),
[new(Black, None, None, Black, White, None, None, None, None)] = Moves((1, 5), (4, 7))
};
}
// Try to make a move. We first try to find a legal move for the current board position.
public bool TryMove(Board board)
{
if (TryGetMoves(board, out var moves, out var reflected) &&
TrySelectMove(moves, out var move))
{
// 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; }
_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;
}
// Looks up the given board and its reflection in the potential moves dictionary. If it's found then we have a
// list of potential moves. If the board is not found in the dictionary then the computer has no legal moves,
// and the human player wins.
private bool TryGetMoves(Board board, out List<Move> moves, out bool reflected)
{
if (_potentialMoves.TryGetValue(board, out moves))
{
reflected = false;
return true;
}
if (_potentialMoves.TryGetValue(board.Reflected, out moves))
{
reflected = true;
return true;
}
reflected = default;
return false;
}
// Get a random move from the list. If the list is empty, then we've previously eliminated all the moves for
// this board position as being non-winning moves. We therefore resign the game.
private bool TrySelectMove(List<Move> moves, out Move move)
{
if (moves.Any())
{
move = moves[_random.Next(moves.Count)];
return true;
}
_io.WriteLine("I resign.");
move = null;
return false;
}
private void ExcludeLastMoveFromFuturePlay()
{
var (moves, move) = _lastMove;
moves.Remove(move);
}
private static List<Move> Moves(params Move[] moves) => moves.ToList();
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;
public Game(TextIO io)
{
private readonly Board _board;
private readonly Human _human;
private readonly Computer _computer;
_board = new Board();
_io = io;
}
public Game(Human human, Computer computer)
public object Play(Human human, Computer computer)
{
_io.WriteLine(_board);
while(true)
{
_board = new Board();
_human = human;
_computer = computer;
}
public IPlayer Play()
{
Console.WriteLine(_board);
while(true)
human.Move(_board);
_io.WriteLine(_board);
if (!computer.TryMove(_board))
{
_human.Move(_board);
Console.WriteLine(_board);
if (!_computer.TryMove(_board))
{
return _human;
}
Console.WriteLine(_board);
if (_computer.IsFullyAdvanced(_board) || _human.HasNoPawns(_board))
{
return _computer;
}
if (!_human.HasLegalMove(_board))
{
Console.Write("You can't move, so ");
return _computer;
}
return human;
}
_io.WriteLine(_board);
if (computer.IsFullyAdvanced(_board) || human.HasNoPawns(_board))
{
return computer;
}
if (!human.HasLegalMove(_board))
{
_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()
public void Play()
{
_io.Write(Resource.Streams.Title);
if (_io.GetYesNo("Instructions") == 'Y')
{
while (true)
{
var game = new Game(_human, _computer);
_io.Write(Resource.Streams.Instructions);
}
var winner = game.Play();
winner.AddWin();
Console.WriteLine(winner == _computer ? "I win." : "You win.");
while (true)
{
var game = new Game(_io);
Console.Write($"I have won {_computer.Wins} and you {_human.Wins}");
Console.WriteLine($" out of {_computer.Wins + _human.Wins} games.");
Console.WriteLine();
}
var winner = game.Play(_human, _computer);
_wins[winner]++;
_io.WriteLine(winner == _computer ? "I win." : "You win.");
_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,64 +1,67 @@
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)
public void Move(Board board)
{
while (true)
{
while (true)
{
var move = Input.GetMove("Your move");
var move = _io.ReadMove("Your move");
if (TryExecute(board, move)) { return; }
if (TryExecute(board, move)) { return; }
Console.WriteLine("Illegal move.");
}
}
public void AddWin() => Wins++;
public bool HasLegalMove(Board board)
{
foreach (var from in AllCells.Where(c => c > 3))
{
if (board[from] != White) { continue; }
if (HasLegalMove(board, from))
{
return true;
}
}
return false;
}
private bool HasLegalMove(Board board, Cell from) =>
Right(from).IsRightDiagonalToCapture(board) ||
Straight(from).IsStraightMoveToEmptySpace(board) ||
from > 4 && Left(from).IsLeftDiagonalToCapture(board);
public bool HasNoPawns(Board board) => board.All(c => c != White);
public bool TryExecute(Board board, Move move)
{
if (board[move.From] != White) { return false; }
if (move.IsStraightMoveToEmptySpace(board) ||
move.IsLeftDiagonalToCapture(board) ||
move.IsRightDiagonalToCapture(board))
{
move.Execute(board);
return true;
}
return false;
_io.WriteLine("Illegal move.");
}
}
public bool HasLegalMove(Board board)
{
foreach (var from in AllCells.Where(c => c > 3))
{
if (board[from] != White) { continue; }
if (HasLegalMove(board, from))
{
return true;
}
}
return false;
}
private bool HasLegalMove(Board board, Cell from) =>
Right(from).IsRightDiagonalToCapture(board) ||
Straight(from).IsStraightMoveToEmptySpace(board) ||
from > 4 && Left(from).IsLeftDiagonalToCapture(board);
public bool HasNoPawns(Board board) => board.All(c => c != White);
public bool TryExecute(Board board, Move move)
{
if (board[move.From] != White) { return false; }
if (move.IsStraightMoveToEmptySpace(board) ||
move.IsLeftDiagonalToCapture(board) ||
move.IsRightDiagonalToCapture(board))
{
move.Execute(board);
return true;
}
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,68 +1,67 @@
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;
public Move(Cell from, Cell to)
{
private readonly Cell _from;
private readonly Cell _to;
private readonly int _metric;
public Move(Cell from, Cell to)
{
_from = from;
_to = to;
_metric = _from - _to;
}
public void Deconstruct(out Cell from, out Cell to)
{
from = _from;
to = _to;
}
public Cell From => _from;
// Produces the mirror image of the current moved, reflected around the central column of the board.
public Move Reflected => (_from.Reflected, _to.Reflected);
// Allows a tuple of two ints to be implicitly converted to a Move.
public static implicit operator Move((int From, int To) value) => new(value.From, value.To);
// Takes floating point coordinates, presumably from keyboard input, and attempts to create a Move object.
public static bool TryCreate(float input1, float input2, out Move move)
{
if (Cell.TryCreate(input1, out var from) &&
Cell.TryCreate(input2, out var to))
{
move = (from, to);
return true;
}
move = default;
return false;
}
public static Move Right(Cell from) => (from, from - 2);
public static Move Straight(Cell from) => (from, from - 3);
public static Move Left(Cell from) => (from, from - 4);
public bool IsStraightMoveToEmptySpace(Board board) => _metric == 3 && board[_to] == None;
public bool IsLeftDiagonalToCapture(Board board) => _metric == 4 && _from != 7 && board[_to] == Black;
public bool IsRightDiagonalToCapture(Board board) =>
_metric == 2 && _from != 9 && _from != 6 && board[_to] == Black;
public void Execute(Board board)
{
board[_to] = board[_from];
board[_from] = None;
}
public override string ToString() => $"from {_from} to {_to}";
_from = from;
_to = to;
_metric = _from - _to;
}
public void Deconstruct(out Cell from, out Cell to)
{
from = _from;
to = _to;
}
public Cell From => _from;
// Produces the mirror image of the current moved, reflected around the central column of the board.
public Move Reflected => (_from.Reflected, _to.Reflected);
// Allows a tuple of two ints to be implicitly converted to a Move.
public static implicit operator Move((int From, int To) value) => new(value.From, value.To);
// Takes floating point coordinates, presumably from keyboard input, and attempts to create a Move object.
public static bool TryCreate(float input1, float input2, out Move move)
{
if (Cell.TryCreate(input1, out var from) &&
Cell.TryCreate(input2, out var to))
{
move = (from, to);
return true;
}
move = default;
return false;
}
public static Move Right(Cell from) => (from, from - 2);
public static Move Straight(Cell from) => (from, from - 3);
public static Move Left(Cell from) => (from, from - 4);
public bool IsStraightMoveToEmptySpace(Board board) => _metric == 3 && board[_to] == None;
public bool IsLeftDiagonalToCapture(Board board) => _metric == 4 && _from != 7 && board[_to] == Black;
public bool IsRightDiagonalToCapture(Board board) =>
_metric == 2 && _from != 9 && _from != 6 && board[_to] == Black;
public void Execute(Board board)
{
board[_to] = board[_from];
board[_from] = None;
}
public override string ToString() => $"from {_from} to {_to}";
}

View File

@@ -1,19 +1,19 @@
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('.');
private readonly char _symbol;
private Pawn(char symbol)
{
public static readonly Pawn Black = new('X');
public static readonly Pawn White = new('O');
public static readonly Pawn None = new('.');
private readonly char _symbol;
private Pawn(char symbol)
{
_symbol = symbol;
}
public override string ToString() => _symbol.ToString();
_symbol = symbol;
}
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