Skip to content

Commit 3dbf3f3

Browse files
authored
Merge pull request #31 from boraaros/feature/connect-four
Feature/connect four
2 parents 0463a06 + cf7d271 commit 3dbf3f3

14 files changed

Lines changed: 728 additions & 11 deletions

Alligator.Compact.sln

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11

22
Microsoft Visual Studio Solution File, Format Version 12.00
3-
# Visual Studio Version 17
4-
VisualStudioVersion = 17.3.32811.315
3+
# Visual Studio Version 18
4+
VisualStudioVersion = 18.4.11620.152 stable
55
MinimumVisualStudioVersion = 10.0.40219.1
66
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Alligator.Solver", "Alligator.Solver\Alligator.Solver.csproj", "{683DE064-2731-4553-81F5-813840788C74}"
77
EndProject
@@ -15,6 +15,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Alligator.Benchmark", "Alli
1515
EndProject
1616
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Alligator.TicTacToe", "Alligator.TicTacToe\Alligator.TicTacToe.csproj", "{28060B42-B42E-4C1C-895C-FD0B0C656553}"
1717
EndProject
18+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Alligator.ConnectFour", "Alligator.ConnectFour\Alligator.ConnectFour.csproj", "{9BB16125-E04A-4F2A-CDEA-DA954DCD6453}"
19+
EndProject
1820
Global
1921
GlobalSection(SolutionConfigurationPlatforms) = preSolution
2022
Debug|Any CPU = Debug|Any CPU
@@ -45,6 +47,10 @@ Global
4547
{28060B42-B42E-4C1C-895C-FD0B0C656553}.Debug|Any CPU.Build.0 = Debug|Any CPU
4648
{28060B42-B42E-4C1C-895C-FD0B0C656553}.Release|Any CPU.ActiveCfg = Release|Any CPU
4749
{28060B42-B42E-4C1C-895C-FD0B0C656553}.Release|Any CPU.Build.0 = Release|Any CPU
50+
{9BB16125-E04A-4F2A-CDEA-DA954DCD6453}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
51+
{9BB16125-E04A-4F2A-CDEA-DA954DCD6453}.Debug|Any CPU.Build.0 = Debug|Any CPU
52+
{9BB16125-E04A-4F2A-CDEA-DA954DCD6453}.Release|Any CPU.ActiveCfg = Release|Any CPU
53+
{9BB16125-E04A-4F2A-CDEA-DA954DCD6453}.Release|Any CPU.Build.0 = Release|Any CPU
4854
EndGlobalSection
4955
GlobalSection(SolutionProperties) = preSolution
5056
HideSolutionNode = FALSE
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<ItemGroup>
4+
<ProjectReference Include="..\Alligator.Solver\Alligator.Solver.csproj" />
5+
</ItemGroup>
6+
7+
<PropertyGroup>
8+
<OutputType>Exe</OutputType>
9+
<TargetFramework>net10.0</TargetFramework>
10+
<ImplicitUsings>enable</ImplicitUsings>
11+
<Nullable>enable</Nullable>
12+
</PropertyGroup>
13+
14+
</Project>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
using Alligator.Solver;
2+
3+
namespace Alligator.ConnectFour
4+
{
5+
internal class Configuration : IConfiguration
6+
{
7+
public int MaxDepth => 13;
8+
}
9+
}

Alligator.ConnectFour/Disk.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
namespace Alligator.ConnectFour
2+
{
3+
public enum Disk
4+
{
5+
None = 0,
6+
Red,
7+
Yellow
8+
}
9+
}

Alligator.ConnectFour/Drop.cs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
namespace Alligator.ConnectFour
2+
{
3+
public class Drop
4+
{
5+
public int Column { get; }
6+
7+
private static readonly Drop[] instances;
8+
9+
static Drop()
10+
{
11+
instances = new Drop[Position.Columns];
12+
for (int c = 0; c < Position.Columns; c++)
13+
{
14+
instances[c] = new Drop(c);
15+
}
16+
}
17+
18+
private Drop(int column)
19+
{
20+
Column = column;
21+
}
22+
23+
public static Drop At(int column) => instances[column];
24+
25+
public override bool Equals(object? obj)
26+
{
27+
return obj is Drop other && Column == other.Column;
28+
}
29+
30+
public override int GetHashCode() => Column;
31+
32+
public override string ToString() => $"Col({Column})";
33+
}
34+
}

Alligator.ConnectFour/Position.cs

Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
using Alligator.Solver;
2+
3+
namespace Alligator.ConnectFour
4+
{
5+
public class Position : IPosition<Drop>
6+
{
7+
public const int Columns = 7;
8+
public const int Rows = 6;
9+
public const int WinLength = 4;
10+
11+
private readonly Disk[,] board; // [row, col] — row 0 is the bottom
12+
private readonly int[] heights;
13+
14+
private Disk nextDisk;
15+
private ulong identifier;
16+
17+
private readonly Stack<(int Column, ulong PrevIdentifier)> history;
18+
19+
private static readonly ulong[,,] zobristTable;
20+
private static readonly ulong zobristTurn;
21+
22+
private static readonly int[,] cellWeights =
23+
{
24+
{ 8, 9, 10, 12, 10, 9, 8 },
25+
{ 7, 8, 9, 11, 9, 8, 7 },
26+
{ 6, 7, 8, 10, 8, 7, 6 },
27+
{ 5, 6, 7, 9, 7, 6, 5 },
28+
{ 4, 5, 6, 8, 6, 5, 4 },
29+
{ 3, 4, 5, 7, 5, 4, 3 }
30+
};
31+
32+
static Position()
33+
{
34+
var rng = new Random(42);
35+
zobristTable = new ulong[2, Rows, Columns];
36+
for (int color = 0; color < 2; color++)
37+
{
38+
for (int r = 0; r < Rows; r++)
39+
{
40+
for (int c = 0; c < Columns; c++)
41+
{
42+
zobristTable[color, r, c] = NextRandomULong(rng);
43+
}
44+
}
45+
}
46+
zobristTurn = NextRandomULong(rng);
47+
}
48+
49+
public Position()
50+
{
51+
board = new Disk[Rows, Columns];
52+
heights = new int[Columns];
53+
nextDisk = Disk.Red;
54+
identifier = 0UL;
55+
history = new Stack<(int, ulong)>();
56+
}
57+
58+
public ulong Identifier => identifier;
59+
60+
public Disk Next => nextDisk;
61+
62+
public int MoveCount => history.Count;
63+
64+
// Always from Red's perspective (absolute convention expected by solver)
65+
public sbyte Value => Evaluate();
66+
67+
public void Take(Drop step)
68+
{
69+
int col = step.Column;
70+
int row = heights[col];
71+
72+
history.Push((col, identifier));
73+
74+
board[row, col] = nextDisk;
75+
heights[col] = row + 1;
76+
77+
int colorIndex = nextDisk == Disk.Red ? 0 : 1;
78+
identifier ^= zobristTable[colorIndex, row, col];
79+
identifier ^= zobristTurn;
80+
81+
nextDisk = nextDisk == Disk.Red ? Disk.Yellow : Disk.Red;
82+
}
83+
84+
public void TakeBack()
85+
{
86+
var (col, prevId) = history.Pop();
87+
88+
int row = heights[col] - 1;
89+
board[row, col] = Disk.None;
90+
heights[col] = row;
91+
92+
identifier = prevId;
93+
nextDisk = nextDisk == Disk.Red ? Disk.Yellow : Disk.Red;
94+
}
95+
96+
public int HeightAt(int col) => heights[col];
97+
98+
public Disk DiskAt(int row, int col) => board[row, col];
99+
100+
public bool IsBoardFull => history.Count == Rows * Columns;
101+
102+
public bool HasWinner()
103+
{
104+
if (history.Count == 0)
105+
{
106+
return false;
107+
}
108+
109+
var (col, _) = history.Peek();
110+
int row = heights[col] - 1;
111+
Disk disk = board[row, col];
112+
113+
return CountDirection(row, col, disk, 0, 1) + CountDirection(row, col, disk, 0, -1) >= WinLength - 1
114+
|| CountDirection(row, col, disk, 1, 0) + CountDirection(row, col, disk, -1, 0) >= WinLength - 1
115+
|| CountDirection(row, col, disk, 1, 1) + CountDirection(row, col, disk, -1, -1) >= WinLength - 1
116+
|| CountDirection(row, col, disk, 1, -1) + CountDirection(row, col, disk, -1, 1) >= WinLength - 1;
117+
}
118+
119+
private int CountDirection(int row, int col, Disk disk, int dRow, int dCol)
120+
{
121+
int count = 0;
122+
int r = row + dRow;
123+
int c = col + dCol;
124+
125+
while (r >= 0 && r < Rows && c >= 0 && c < Columns && board[r, c] == disk)
126+
{
127+
count++;
128+
r += dRow;
129+
c += dCol;
130+
}
131+
132+
return count;
133+
}
134+
135+
private sbyte Evaluate()
136+
{
137+
int score = 0;
138+
139+
for (int r = 0; r < Rows; r++)
140+
{
141+
for (int c = 0; c < Columns; c++)
142+
{
143+
if (board[r, c] == Disk.Red) score += cellWeights[r, c];
144+
else if (board[r, c] == Disk.Yellow) score -= cellWeights[r, c];
145+
}
146+
}
147+
148+
int redThreats = 0, yellowThreats = 0;
149+
score += EvaluateAllWindows(ref redThreats, ref yellowThreats);
150+
151+
if (redThreats >= 2) score += 80;
152+
if (yellowThreats >= 2) score -= 80;
153+
154+
// sbyte.MaxValue is reserved for wins
155+
score = Math.Clamp(score, -126, 126);
156+
157+
return (sbyte)score;
158+
}
159+
160+
private int EvaluateAllWindows(ref int redThreats, ref int yellowThreats)
161+
{
162+
int score = 0;
163+
164+
for (int r = 0; r < Rows; r++)
165+
{
166+
for (int c = 0; c <= Columns - WinLength; c++)
167+
{
168+
score += EvaluateWindow(r, c, 0, 1, ref redThreats, ref yellowThreats);
169+
}
170+
}
171+
172+
for (int c = 0; c < Columns; c++)
173+
{
174+
for (int r = 0; r <= Rows - WinLength; r++)
175+
{
176+
score += EvaluateWindow(r, c, 1, 0, ref redThreats, ref yellowThreats);
177+
}
178+
}
179+
180+
for (int r = 0; r <= Rows - WinLength; r++)
181+
{
182+
for (int c = 0; c <= Columns - WinLength; c++)
183+
{
184+
score += EvaluateWindow(r, c, 1, 1, ref redThreats, ref yellowThreats);
185+
}
186+
}
187+
188+
for (int r = WinLength - 1; r < Rows; r++)
189+
{
190+
for (int c = 0; c <= Columns - WinLength; c++)
191+
{
192+
score += EvaluateWindow(r, c, -1, 1, ref redThreats, ref yellowThreats);
193+
}
194+
}
195+
196+
return score;
197+
}
198+
199+
private int EvaluateWindow(int startRow, int startCol, int dRow, int dCol,
200+
ref int redThreats, ref int yellowThreats)
201+
{
202+
int red = 0, yellow = 0;
203+
int minDistance = int.MaxValue;
204+
205+
for (int i = 0; i < WinLength; i++)
206+
{
207+
int r = startRow + i * dRow;
208+
int c = startCol + i * dCol;
209+
Disk d = board[r, c];
210+
if (d == Disk.Red) red++;
211+
else if (d == Disk.Yellow) yellow++;
212+
else
213+
{
214+
int distance = r - heights[c];
215+
if (distance < minDistance) minDistance = distance;
216+
}
217+
}
218+
219+
if (red > 0 && yellow > 0) return 0;
220+
221+
if (red == 3)
222+
{
223+
if (minDistance == 0) redThreats++;
224+
return ScoreThreeInRow(minDistance);
225+
}
226+
if (yellow == 3)
227+
{
228+
if (minDistance == 0) yellowThreats++;
229+
return -ScoreThreeInRow(minDistance);
230+
}
231+
if (red == 2) return ScoreTwoInRow(minDistance);
232+
if (yellow == 2) return -ScoreTwoInRow(minDistance);
233+
234+
return 0;
235+
}
236+
237+
private static int ScoreThreeInRow(int distanceToPlayable)
238+
{
239+
return distanceToPlayable switch
240+
{
241+
0 => 50,
242+
1 => 20,
243+
2 => 8,
244+
_ => 3
245+
};
246+
}
247+
248+
private static int ScoreTwoInRow(int distanceToPlayable)
249+
{
250+
return distanceToPlayable switch
251+
{
252+
0 => 8,
253+
1 => 4,
254+
_ => 2
255+
};
256+
}
257+
258+
private static ulong NextRandomULong(Random rng)
259+
{
260+
byte[] buffer = new byte[8];
261+
rng.NextBytes(buffer);
262+
return BitConverter.ToUInt64(buffer, 0);
263+
}
264+
}
265+
}

0 commit comments

Comments
 (0)