mirror of
https://github.com/coding-horror/basic-computer-games.git
synced 2025-12-22 07:10:42 -08:00
Merge pull request #804 from drewjcooper/csharp-56-life-for-two
C# 56 life for two
This commit is contained in:
74
56_Life_for_Two/csharp/Board.cs
Normal file
74
56_Life_for_Two/csharp/Board.cs
Normal file
@@ -0,0 +1,74 @@
|
||||
using System.Collections;
|
||||
using System.Text;
|
||||
|
||||
namespace LifeforTwo;
|
||||
|
||||
internal class Board : IEnumerable<Coordinates>
|
||||
{
|
||||
private readonly Piece[,] _cells = new Piece[7, 7];
|
||||
private readonly Dictionary<int, int> _cellCounts =
|
||||
new() { [Piece.None] = 0, [Piece.Player1] = 0, [Piece.Player2] = 0 };
|
||||
|
||||
public Piece this[Coordinates coordinates]
|
||||
{
|
||||
get => this[coordinates.X, coordinates.Y];
|
||||
set => this[coordinates.X, coordinates.Y] = value;
|
||||
}
|
||||
|
||||
private Piece this[int x, int y]
|
||||
{
|
||||
get => _cells[x, y];
|
||||
set
|
||||
{
|
||||
if (!_cells[x, y].IsEmpty) { _cellCounts[_cells[x, y]] -= 1; }
|
||||
_cells[x, y] = value;
|
||||
_cellCounts[value] += 1;
|
||||
}
|
||||
}
|
||||
|
||||
public int Player1Count => _cellCounts[Piece.Player1];
|
||||
public int Player2Count => _cellCounts[Piece.Player2];
|
||||
|
||||
internal bool IsEmptyAt(Coordinates coordinates) => this[coordinates].IsEmpty;
|
||||
|
||||
internal void ClearCell(Coordinates coordinates) => this[coordinates] = Piece.NewNone();
|
||||
internal void AddPlayer1Piece(Coordinates coordinates) => this[coordinates] = Piece.NewPlayer1();
|
||||
internal void AddPlayer2Piece(Coordinates coordinates) => this[coordinates] = Piece.NewPlayer2();
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
|
||||
for (var y = 0; y <= 6; y++)
|
||||
{
|
||||
builder.AppendLine();
|
||||
for (var x = 0; x <= 6; x++)
|
||||
{
|
||||
builder.Append(GetCellDisplay(x, y));
|
||||
}
|
||||
}
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private string GetCellDisplay(int x, int y) =>
|
||||
(x, y) switch
|
||||
{
|
||||
(0 or 6, _) => $" {y % 6} ",
|
||||
(_, 0 or 6) => $" {x % 6} ",
|
||||
_ => $" {this[x, y]} "
|
||||
};
|
||||
|
||||
public IEnumerator<Coordinates> GetEnumerator()
|
||||
{
|
||||
for (var x = 1; x <= 5; x++)
|
||||
{
|
||||
for (var y = 1; y <= 5; y++)
|
||||
{
|
||||
yield return new(x, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
}
|
||||
31
56_Life_for_Two/csharp/Coordinates.cs
Normal file
31
56_Life_for_Two/csharp/Coordinates.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
namespace LifeforTwo;
|
||||
|
||||
internal record Coordinates (int X, int Y)
|
||||
{
|
||||
public static bool TryCreate((float X, float Y) values, out Coordinates coordinates)
|
||||
{
|
||||
if (values.X <= 0 || values.X > 5 || values.Y <= 0 || values.Y > 5)
|
||||
{
|
||||
coordinates = new(0, 0);
|
||||
return false;
|
||||
}
|
||||
|
||||
coordinates = new((int)values.X, (int)values.Y);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static Coordinates operator +(Coordinates coordinates, int value) =>
|
||||
new (coordinates.X + value, coordinates.Y + value);
|
||||
|
||||
public IEnumerable<Coordinates> GetNeighbors()
|
||||
{
|
||||
yield return new(X - 1, Y);
|
||||
yield return new(X + 1, Y);
|
||||
yield return new(X, Y - 1);
|
||||
yield return new(X, Y + 1);
|
||||
yield return new(X - 1, Y - 1);
|
||||
yield return new(X + 1, Y - 1);
|
||||
yield return new(X - 1, Y + 1);
|
||||
yield return new(X + 1, Y + 1);
|
||||
}
|
||||
}
|
||||
26
56_Life_for_Two/csharp/Game.cs
Normal file
26
56_Life_for_Two/csharp/Game.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
internal class Game
|
||||
{
|
||||
private readonly IReadWrite _io;
|
||||
|
||||
public Game(IReadWrite io)
|
||||
{
|
||||
_io = io;
|
||||
}
|
||||
|
||||
public void Play()
|
||||
{
|
||||
_io.Write(Streams.Title);
|
||||
|
||||
var life = new Life(_io);
|
||||
|
||||
_io.Write(life.FirstGeneration);
|
||||
|
||||
foreach (var generation in life)
|
||||
{
|
||||
_io.WriteLine();
|
||||
_io.Write(generation);
|
||||
}
|
||||
|
||||
_io.WriteLine(life.Result ?? "No result");
|
||||
}
|
||||
}
|
||||
89
56_Life_for_Two/csharp/Generation.cs
Normal file
89
56_Life_for_Two/csharp/Generation.cs
Normal file
@@ -0,0 +1,89 @@
|
||||
internal class Generation
|
||||
{
|
||||
private readonly Board _board;
|
||||
|
||||
public Generation(Board board)
|
||||
{
|
||||
_board = board;
|
||||
CountNeighbours();
|
||||
}
|
||||
|
||||
public Board Board => _board;
|
||||
|
||||
public int Player1Count => _board.Player1Count;
|
||||
public int Player2Count => _board.Player2Count;
|
||||
|
||||
public string? Result =>
|
||||
(Player1Count, Player2Count) switch
|
||||
{
|
||||
(0, 0) => Strings.Draw,
|
||||
(_, 0) => string.Format(Formats.Winner, 1),
|
||||
(0, _) => string.Format(Formats.Winner, 2),
|
||||
_ => null
|
||||
};
|
||||
|
||||
public static Generation Create(IReadWrite io)
|
||||
{
|
||||
var board = new Board();
|
||||
|
||||
SetInitialPieces(1, coord => board.AddPlayer1Piece(coord));
|
||||
SetInitialPieces(2, coord => board.AddPlayer2Piece(coord));
|
||||
|
||||
return new Generation(board);
|
||||
|
||||
void SetInitialPieces(int player, Action<Coordinates> setPiece)
|
||||
{
|
||||
io.WriteLine(Formats.InitialPieces, player);
|
||||
for (var i = 1; i <= 3; i++)
|
||||
{
|
||||
setPiece(io.ReadCoordinates(board));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Generation CalculateNextGeneration()
|
||||
{
|
||||
var board = new Board();
|
||||
|
||||
foreach (var coordinates in _board)
|
||||
{
|
||||
board[coordinates] = _board[coordinates].GetNext();
|
||||
}
|
||||
|
||||
return new(board);
|
||||
}
|
||||
|
||||
public void AddPieces(IReadWrite io)
|
||||
{
|
||||
var player1Coordinate = io.ReadCoordinates(1, _board);
|
||||
var player2Coordinate = io.ReadCoordinates(2, _board);
|
||||
|
||||
if (player1Coordinate == player2Coordinate)
|
||||
{
|
||||
io.Write(Streams.SameCoords);
|
||||
// This is a bug existing in the original code. The line should be _board[_coordinates[_player]] = 0;
|
||||
_board.ClearCell(player1Coordinate + 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
_board.AddPlayer1Piece(player1Coordinate);
|
||||
_board.AddPlayer2Piece(player2Coordinate);
|
||||
}
|
||||
}
|
||||
|
||||
private void CountNeighbours()
|
||||
{
|
||||
foreach (var coordinates in _board)
|
||||
{
|
||||
var piece = _board[coordinates];
|
||||
if (piece.IsEmpty) { continue; }
|
||||
|
||||
foreach (var neighbour in coordinates.GetNeighbors())
|
||||
{
|
||||
_board[neighbour] = _board[neighbour].AddNeighbour(piece);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString() => _board.ToString();
|
||||
}
|
||||
22
56_Life_for_Two/csharp/IOExtensions.cs
Normal file
22
56_Life_for_Two/csharp/IOExtensions.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
internal static class IOExtensions
|
||||
{
|
||||
internal static Coordinates ReadCoordinates(this IReadWrite io, int player, Board board)
|
||||
{
|
||||
io.Write(Formats.Player, player);
|
||||
return io.ReadCoordinates(board);
|
||||
}
|
||||
|
||||
internal static Coordinates ReadCoordinates(this IReadWrite io, Board board)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
io.WriteLine("X,Y");
|
||||
var values = io.Read2Numbers("&&&&&&\r");
|
||||
if (Coordinates.TryCreate(values, out var coordinates) && board.IsEmptyAt(coordinates))
|
||||
{
|
||||
return coordinates;
|
||||
}
|
||||
io.Write(Streams.IllegalCoords);
|
||||
}
|
||||
}
|
||||
}
|
||||
31
56_Life_for_Two/csharp/Life.cs
Normal file
31
56_Life_for_Two/csharp/Life.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using System.Collections;
|
||||
|
||||
internal class Life : IEnumerable<Generation>
|
||||
{
|
||||
private readonly IReadWrite _io;
|
||||
|
||||
public Life(IReadWrite io)
|
||||
{
|
||||
_io = io;
|
||||
FirstGeneration = Generation.Create(io);
|
||||
}
|
||||
|
||||
public Generation FirstGeneration { get; }
|
||||
public string? Result { get; private set; }
|
||||
|
||||
public IEnumerator<Generation> GetEnumerator()
|
||||
{
|
||||
var current = FirstGeneration;
|
||||
while (current.Result is null)
|
||||
{
|
||||
current = current.CalculateNextGeneration();
|
||||
yield return current;
|
||||
|
||||
if (current.Result is null) { current.AddPieces(_io); }
|
||||
}
|
||||
|
||||
Result = current.Result;
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
}
|
||||
@@ -6,4 +6,12 @@
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Resources/*.txt" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\00_Common\dotnet\Games.Common\Games.Common.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
54
56_Life_for_Two/csharp/Piece.cs
Normal file
54
56_Life_for_Two/csharp/Piece.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace LifeforTwo;
|
||||
|
||||
public struct Piece
|
||||
{
|
||||
public const int None = 0x0000;
|
||||
public const int Player1 = 0x0100;
|
||||
public const int Player2 = 0x1000;
|
||||
private const int PieceMask = Player1 | Player2;
|
||||
private const int NeighbourValueOffset = 8;
|
||||
|
||||
private static readonly ImmutableHashSet<int> _willBePlayer1 =
|
||||
new[] { 0x0003, 0x0102, 0x0103, 0x0120, 0x0130, 0x0121, 0x0112, 0x0111, 0x0012 }.ToImmutableHashSet();
|
||||
private static readonly ImmutableHashSet<int> _willBePlayer2 =
|
||||
new[] { 0x0021, 0x0030, 0x1020, 0x1030, 0x1011, 0x1021, 0x1003, 0x1002, 0x1012 }.ToImmutableHashSet();
|
||||
|
||||
private int _value;
|
||||
|
||||
private Piece(int value) => _value = value;
|
||||
|
||||
public int Value => _value & PieceMask;
|
||||
public bool IsEmpty => (_value & PieceMask) == None;
|
||||
|
||||
public static Piece NewNone() => new(None);
|
||||
public static Piece NewPlayer1() => new(Player1);
|
||||
public static Piece NewPlayer2() => new(Player2);
|
||||
|
||||
public Piece AddNeighbour(Piece neighbour)
|
||||
{
|
||||
_value += neighbour.Value >> NeighbourValueOffset;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Piece GetNext() => new(
|
||||
_value switch
|
||||
{
|
||||
_ when _willBePlayer1.Contains(_value) => Player1,
|
||||
_ when _willBePlayer2.Contains(_value) => Player2,
|
||||
_ => None
|
||||
});
|
||||
|
||||
public override string ToString() =>
|
||||
(_value & PieceMask) switch
|
||||
{
|
||||
Player1 => "*",
|
||||
Player2 => "#",
|
||||
_ => " "
|
||||
};
|
||||
|
||||
public static implicit operator Piece(int value) => new(value);
|
||||
public static implicit operator int(Piece piece) => piece.Value;
|
||||
}
|
||||
5
56_Life_for_Two/csharp/Program.cs
Normal file
5
56_Life_for_Two/csharp/Program.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
global using Games.Common.IO;
|
||||
global using static LifeforTwo.Resources.Resource;
|
||||
global using LifeforTwo;
|
||||
|
||||
new Game(new ConsoleIO()).Play();
|
||||
2
56_Life_for_Two/csharp/Resources/Draw.txt
Normal file
2
56_Life_for_Two/csharp/Resources/Draw.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
|
||||
A draw
|
||||
1
56_Life_for_Two/csharp/Resources/IllegalCoords.txt
Normal file
1
56_Life_for_Two/csharp/Resources/IllegalCoords.txt
Normal file
@@ -0,0 +1 @@
|
||||
Illegal coords. Retype
|
||||
2
56_Life_for_Two/csharp/Resources/InitialPieces.txt
Normal file
2
56_Life_for_Two/csharp/Resources/InitialPieces.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
|
||||
Player {0} - 3 live pieces
|
||||
3
56_Life_for_Two/csharp/Resources/Player.txt
Normal file
3
56_Life_for_Two/csharp/Resources/Player.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
|
||||
|
||||
Player {0}
|
||||
37
56_Life_for_Two/csharp/Resources/Resource.cs
Normal file
37
56_Life_for_Two/csharp/Resources/Resource.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace LifeforTwo.Resources;
|
||||
|
||||
internal static class Resource
|
||||
{
|
||||
internal static class Streams
|
||||
{
|
||||
public static Stream Title => GetStream();
|
||||
public static Stream IllegalCoords => GetStream();
|
||||
public static Stream SameCoords => GetStream();
|
||||
}
|
||||
|
||||
internal static class Formats
|
||||
{
|
||||
public static string InitialPieces => GetString();
|
||||
public static string Player => GetString();
|
||||
public static string Winner => GetString();
|
||||
}
|
||||
|
||||
internal static class Strings
|
||||
{
|
||||
public static string Draw => GetString();
|
||||
}
|
||||
|
||||
private static string GetString([CallerMemberName] string? name = null)
|
||||
{
|
||||
using var stream = GetStream(name);
|
||||
using var reader = new StreamReader(stream);
|
||||
return reader.ReadToEnd();
|
||||
}
|
||||
|
||||
private static Stream GetStream([CallerMemberName] string? name = null) =>
|
||||
Assembly.GetExecutingAssembly().GetManifestResourceStream($"{typeof(Resource).Namespace}.{name}.txt")
|
||||
?? throw new Exception($"Could not find embedded resource stream '{name}'.");
|
||||
}
|
||||
1
56_Life_for_Two/csharp/Resources/SameCoords.txt
Normal file
1
56_Life_for_Two/csharp/Resources/SameCoords.txt
Normal file
@@ -0,0 +1 @@
|
||||
Same coord. Set to 0
|
||||
6
56_Life_for_Two/csharp/Resources/Title.txt
Normal file
6
56_Life_for_Two/csharp/Resources/Title.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
Life2
|
||||
Creative Computing Morristown, New Jersey
|
||||
|
||||
|
||||
|
||||
U.B. Life Game
|
||||
2
56_Life_for_Two/csharp/Resources/Winner.txt
Normal file
2
56_Life_for_Two/csharp/Resources/Winner.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
|
||||
Player {0} is the winner
|
||||
Reference in New Issue
Block a user