Skip to content

Commit 39977b7

Browse files
Merge pull request #40 from Wycott/main
Checkers
2 parents b991ace + a7f2c78 commit 39977b7

28 files changed

+1272
-0
lines changed

.editorconfig

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,6 @@ indent_style = tab
66

77
# IDE0160: Convert to file-scoped namespace
88
csharp_style_namespace_declarations = file_scoped:warning
9+
10+
# IDE0042: Deconstruct variable declaration
11+
csharp_style_deconstructed_variable_declaration = false
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
name: Checkers Build
2+
on:
3+
push:
4+
paths:
5+
- 'Projects/Checkers/**'
6+
branches:
7+
- main
8+
pull_request:
9+
paths:
10+
- 'Projects/Checkers/**'
11+
branches:
12+
- main
13+
workflow_dispatch:
14+
jobs:
15+
build:
16+
runs-on: ubuntu-latest
17+
steps:
18+
- uses: actions/checkout@v1
19+
- name: setup dotnet
20+
uses: actions/setup-dotnet@v1
21+
with:
22+
dotnet-version: 6.0.x
23+
- name: dotnet build
24+
run: dotnet build "Projects\Checkers\Checkers.csproj" --configuration Release

.vscode/launch.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,16 @@
362362
"console": "externalTerminal",
363363
"stopAtEntry": false,
364364
},
365+
{
366+
"name": "Checkers",
367+
"type": "coreclr",
368+
"request": "launch",
369+
"preLaunchTask": "Build Checkers",
370+
"program": "${workspaceFolder}/Projects/Checkers/bin/Debug/Checkers.dll",
371+
"cwd": "${workspaceFolder}/Projects/Checkers/bin/Debug",
372+
"console": "externalTerminal",
373+
"stopAtEntry": false,
374+
},
365375
{
366376
"name": "Blackjack",
367377
"type": "coreclr",

.vscode/tasks.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,19 @@
22
"version": "2.0.0",
33
"tasks":
44
[
5+
{
6+
"label": "Build Checkers",
7+
"command": "dotnet",
8+
"type": "process",
9+
"args":
10+
[
11+
"build",
12+
"${workspaceFolder}/Projects/Checkers/Checkers.csproj",
13+
"/property:GenerateFullPaths=true",
14+
"/consoleloggerparameters:NoSummary",
15+
],
16+
"problemMatcher": "$msCompile",
17+
},
518
{
619
"label": "Build Duck Hunt",
720
"command": "dotnet",

Projects/Checkers/Board.cs

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
namespace Checkers;
2+
3+
public class Board
4+
{
5+
public List<Piece> Pieces { get; }
6+
7+
public Piece? Aggressor { get; set; }
8+
9+
public Piece? this[int x, int y] =>
10+
Pieces.FirstOrDefault(piece => piece.X == x && piece.Y == y);
11+
12+
public Board()
13+
{
14+
Aggressor = null;
15+
Pieces = new List<Piece>
16+
{
17+
new() { NotationPosition ="A3", Color = Black},
18+
new() { NotationPosition ="A1", Color = Black},
19+
new() { NotationPosition ="B2", Color = Black},
20+
new() { NotationPosition ="C3", Color = Black},
21+
new() { NotationPosition ="C1", Color = Black},
22+
new() { NotationPosition ="D2", Color = Black},
23+
new() { NotationPosition ="E3", Color = Black},
24+
new() { NotationPosition ="E1", Color = Black},
25+
new() { NotationPosition ="F2", Color = Black},
26+
new() { NotationPosition ="G3", Color = Black},
27+
new() { NotationPosition ="G1", Color = Black},
28+
new() { NotationPosition ="H2", Color = Black},
29+
30+
new() { NotationPosition ="A7", Color = White},
31+
new() { NotationPosition ="B8", Color = White},
32+
new() { NotationPosition ="B6", Color = White},
33+
new() { NotationPosition ="C7", Color = White},
34+
new() { NotationPosition ="D8", Color = White},
35+
new() { NotationPosition ="D6", Color = White},
36+
new() { NotationPosition ="E7", Color = White},
37+
new() { NotationPosition ="F8", Color = White},
38+
new() { NotationPosition ="F6", Color = White},
39+
new() { NotationPosition ="G7", Color = White},
40+
new() { NotationPosition ="H8", Color = White},
41+
new() { NotationPosition ="H6", Color = White}
42+
};
43+
}
44+
45+
public static string ToPositionNotationString(int x, int y)
46+
{
47+
if (!IsValidPosition(x, y)) throw new ArgumentException("Not a valid position!");
48+
return $"{(char)('A' + x)}{y + 1}";
49+
}
50+
51+
public static (int X, int Y) ParsePositionNotation(string notation)
52+
{
53+
if (notation is null) throw new ArgumentNullException(nameof(notation));
54+
notation = notation.Trim().ToUpper();
55+
if (notation.Length is not 2 ||
56+
notation[0] < 'A' || 'H' < notation[0] ||
57+
notation[1] < '1' || '8' < notation[1])
58+
throw new FormatException($@"{nameof(notation)} ""{notation}"" is not valid");
59+
return (notation[0] - 'A', notation[1] - '1');
60+
}
61+
62+
public static bool IsValidPosition(int x, int y) =>
63+
0 <= x && x < 8 &&
64+
0 <= y && y < 8;
65+
66+
public (Piece A, Piece B) GetClosestRivalPieces(PieceColor priorityColor)
67+
{
68+
double minDistanceSquared = double.MaxValue;
69+
(Piece A, Piece B) closestRivals = (null!, null!);
70+
foreach (Piece a in Pieces.Where(piece => piece.Color == priorityColor))
71+
{
72+
foreach (Piece b in Pieces.Where(piece => piece.Color != priorityColor))
73+
{
74+
(int X, int Y) vector = (a.X - b.X, a.Y - b.Y);
75+
double distanceSquared = vector.X * vector.X + vector.Y * vector.Y;
76+
if (distanceSquared < minDistanceSquared)
77+
{
78+
minDistanceSquared = distanceSquared;
79+
closestRivals = (a, b);
80+
}
81+
}
82+
}
83+
return closestRivals;
84+
}
85+
86+
public List<Move> GetPossibleMoves(PieceColor color)
87+
{
88+
List<Move> moves = new();
89+
if (Aggressor is not null)
90+
{
91+
if (Aggressor.Color != color)
92+
{
93+
throw new Exception($"{nameof(Aggressor)} is not null && {nameof(Aggressor)}.{nameof(Aggressor.Color)} != {nameof(color)}");
94+
}
95+
moves.AddRange(GetPossibleMoves(Aggressor).Where(move => move.PieceToCapture is not null));
96+
}
97+
else
98+
{
99+
foreach (Piece piece in Pieces.Where(piece => piece.Color == color))
100+
{
101+
moves.AddRange(GetPossibleMoves(piece));
102+
}
103+
}
104+
return moves.Any(move => move.PieceToCapture is not null)
105+
? moves.Where(move => move.PieceToCapture is not null).ToList()
106+
: moves;
107+
}
108+
109+
public List<Move> GetPossibleMoves(Piece piece)
110+
{
111+
List<Move> moves = new();
112+
ValidateDiagonalMove(-1, -1);
113+
ValidateDiagonalMove(-1, 1);
114+
ValidateDiagonalMove( 1, -1);
115+
ValidateDiagonalMove( 1, 1);
116+
return moves.Any(move => move.PieceToCapture is not null)
117+
? moves.Where(move => move.PieceToCapture is not null).ToList()
118+
: moves;
119+
120+
void ValidateDiagonalMove(int dx, int dy)
121+
{
122+
if (!piece.Promoted && piece.Color is Black && dy is -1) return;
123+
if (!piece.Promoted && piece.Color is White && dy is 1) return;
124+
(int X, int Y) target = (piece.X + dx, piece.Y + dy);
125+
if (!IsValidPosition(target.X, target.Y)) return;
126+
PieceColor? targetColor = this[target.X, target.Y]?.Color;
127+
if (targetColor is null)
128+
{
129+
if (!IsValidPosition(target.X, target.Y)) return;
130+
Move newMove = new(piece, target);
131+
moves.Add(newMove);
132+
}
133+
else if (targetColor != piece.Color)
134+
{
135+
(int X, int Y) jump = (piece.X + 2 * dx, piece.Y + 2 * dy);
136+
if (!IsValidPosition(jump.X, jump.Y)) return;
137+
PieceColor? jumpColor = this[jump.X, jump.Y]?.Color;
138+
if (jumpColor is not null) return;
139+
Move attack = new(piece, jump, this[target.X, target.Y]);
140+
moves.Add(attack);
141+
}
142+
}
143+
}
144+
145+
/// <summary>Returns a <see cref="Move"/> if <paramref name="from"/>-&gt;<paramref name="to"/> is valid or null if not.</summary>
146+
public Move? ValidateMove(PieceColor color, (int X, int Y) from, (int X, int Y) to)
147+
{
148+
Piece? piece = this[from.X, from.Y];
149+
if (piece is null)
150+
{
151+
return null;
152+
}
153+
foreach (Move move in GetPossibleMoves(color))
154+
{
155+
if ((move.PieceToMove.X, move.PieceToMove.Y) == from && move.To == to)
156+
{
157+
return move;
158+
}
159+
}
160+
return null;
161+
}
162+
163+
public static bool IsTowards(Move move, Piece piece)
164+
{
165+
(int Dx, int Dy) a = (move.PieceToMove.X - piece.X, move.PieceToMove.Y - piece.Y);
166+
int a_distanceSquared = a.Dx * a.Dx + a.Dy * a.Dy;
167+
(int Dx, int Dy) b = (move.To.X - piece.X, move.To.Y - piece.Y);
168+
int b_distanceSquared = b.Dx * b.Dx + b.Dy * b.Dy;
169+
return b_distanceSquared < a_distanceSquared;
170+
}
171+
}

Projects/Checkers/Checkers.csproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<OutputType>Exe</OutputType>
4+
<TargetFramework>net6.0</TargetFramework>
5+
<ImplicitUsings>disable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
</PropertyGroup>
8+
</Project>

Projects/Checkers/Game.cs

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
namespace Checkers;
2+
3+
public class Game
4+
{
5+
private const int PiecesPerColor = 12;
6+
7+
public PieceColor Turn { get; private set; }
8+
public Board Board { get; }
9+
public PieceColor? Winner { get; private set; }
10+
public List<Player> Players { get; }
11+
12+
public Game(int humanPlayerCount)
13+
{
14+
if (humanPlayerCount < 0 || 2 < humanPlayerCount) throw new ArgumentOutOfRangeException(nameof(humanPlayerCount));
15+
Board = new Board();
16+
Players = new()
17+
{
18+
new Player(humanPlayerCount >= 1, Black),
19+
new Player(humanPlayerCount >= 2, White),
20+
};
21+
Turn = Black;
22+
Winner = null;
23+
}
24+
25+
public void PerformMove(Move move)
26+
{
27+
(move.PieceToMove.X, move.PieceToMove.Y) = move.To;
28+
if ((move.PieceToMove.Color is Black && move.To.Y is 7) ||
29+
(move.PieceToMove.Color is White && move.To.Y is 0))
30+
{
31+
move.PieceToMove.Promoted = true;
32+
}
33+
if (move.PieceToCapture is not null)
34+
{
35+
Board.Pieces.Remove(move.PieceToCapture);
36+
}
37+
if (move.PieceToCapture is not null &&
38+
Board.GetPossibleMoves(move.PieceToMove).Any(m => m.PieceToCapture is not null))
39+
{
40+
Board.Aggressor = move.PieceToMove;
41+
}
42+
else
43+
{
44+
Board.Aggressor = null;
45+
Turn = Turn is Black ? White : Black;
46+
}
47+
CheckForWinner();
48+
}
49+
50+
public void CheckForWinner()
51+
{
52+
if (!Board.Pieces.Any(piece => piece.Color is Black))
53+
{
54+
Winner = White;
55+
}
56+
if (!Board.Pieces.Any(piece => piece.Color is White))
57+
{
58+
Winner = Black;
59+
}
60+
if (Winner is null && Board.GetPossibleMoves(Turn).Count is 0)
61+
{
62+
Winner = Turn is Black ? White : Black;
63+
}
64+
}
65+
66+
public int TakenCount(PieceColor colour) =>
67+
PiecesPerColor - Board.Pieces.Count(piece => piece.Color == colour);
68+
}

Projects/Checkers/Move.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
namespace Checkers;
2+
3+
public class Move
4+
{
5+
public Piece PieceToMove { get; set; }
6+
7+
public (int X, int Y) To { get; set; }
8+
9+
public Piece? PieceToCapture { get; set; }
10+
11+
public Move(Piece pieceToMove, (int X, int Y) to, Piece? pieceToCapture = null)
12+
{
13+
PieceToMove = pieceToMove;
14+
To = to;
15+
PieceToCapture = pieceToCapture;
16+
}
17+
}

Projects/Checkers/Piece.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
namespace Checkers;
2+
3+
public class Piece
4+
{
5+
public int X { get; set; }
6+
7+
public int Y { get; set; }
8+
9+
public string NotationPosition
10+
{
11+
get => Board.ToPositionNotationString(X, Y);
12+
set => (X, Y) = Board.ParsePositionNotation(value);
13+
}
14+
15+
public PieceColor Color { get; set; }
16+
17+
public bool Promoted { get; set; }
18+
}

Projects/Checkers/PieceColor.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
namespace Checkers;
2+
3+
public enum PieceColor
4+
{
5+
Black = 1,
6+
White = 2,
7+
}

0 commit comments

Comments
 (0)