-
Notifications
You must be signed in to change notification settings - Fork 0
/
Player.cs
267 lines (247 loc) · 10.4 KB
/
Player.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
namespace Othello
{
/// <summary>
/// Defines a single player (human or computer).
/// </summary>
internal class Player
{
public bool canPlay;
private bool isHuman;
private int roundsPlayed;
private readonly Color color;
private readonly PlayerSettings settings;
public Player(Color color, PlayerSettings settings)
{
canPlay = true;
this.color = color;
isHuman = true;
this.settings = settings;
}
/// <summary>
/// Initializes a new player with black pieces.
/// </summary>
/// <param name="settings"></param>
/// <returns>Black player.</returns>
public static Player Black(PlayerSettings settings)
{
return new Player(Color.Black, settings);
}
/// <summary>
/// Initializes a new player with white pieces.
/// </summary>
/// <param name="settings"></param>
/// <returns>White player.</returns>
public static Player White(PlayerSettings settings)
{
return new Player(Color.White, settings);
}
#nullable enable
/// <summary>
/// Plays one round of the given player.
/// </summary>
/// <param name="board"></param>
/// <returns></returns>
public string? PlayOneMove(Board board)
{
Console.WriteLine($"Turn: {color.Name()}");
var moves = board.PossibleMoves(color);
if (moves.Count != 0)
{
canPlay = true;
if (isHuman && settings.ShowHelpers)
{
board.PrintPossibleMoves(moves);
}
var chosenMove = isHuman ? GetHumanMove(moves) : GetComputerMove(moves, board);
board.PlacePiece(chosenMove);
board.PrintScore();
++roundsPlayed;
if (!settings.TestMode)
{
Thread.Sleep(1000);
}
return chosenMove.ToLogEntry();
}
canPlay = false;
ConsoleVisuals.WriteLine(" No moves available...", System.Drawing.Color.Yellow);
return null;
}
#nullable disable
/// <summary>
/// Sets the player to be controlled by human or computer.
/// </summary>
/// <param name="isHuman"></param>
public void SetHuman(bool isHuman)
{
this.isHuman = isHuman;
}
/// <summary>
/// Resets the player's status for a new game.
/// </summary>
public void Reset()
{
canPlay = true;
roundsPlayed = 0;
}
/// <summary>
/// Determines the best move for the computer player using the Alpha-Beta pruning algorithm.
/// </summary>
/// <param name="moves">The list of possible moves.</param>
/// <param name="board">The current state of the board.</param>
/// <returns>The best move determined for the computer player.</returns>
private Move GetComputerMove(IReadOnlyList<Move> moves, Board board)
{
Console.WriteLine(" Computer plays...");
// Alpha represents the best score that the maximizing player can guarantee at current or higher levels.
float alpha = float.MinValue;
// Beta represents the best score that the minimizing player can guarantee at current or higher levels.
float beta = float.MaxValue;
// Best move found for the computer.
Move bestMove = moves[0];
// Best score found for the computer, initialized to the lowest possible value.
float bestScore = float.MinValue;
// The depth of the search tree to explore.
int depth = 5;
// Loop through all possible moves to find the best one.
foreach (var move in moves)
{
// Create a deep clone of the board to simulate the move without affecting the actual game board.
Board newBoard = (Board)board.Clone();
// Apply the move to the cloned board.
newBoard.PlacePiece(move);
// Perform the Alpha-Beta search recursively to evaluate the move.
float score = AlphaBeta(newBoard, depth - 1, alpha, beta, false);
// If the move has a better score than the best found so far, update the best move and score.
if (score > bestScore)
{
bestScore = score;
bestMove = move;
}
// Update alpha if a better score is found for the maximizing player.
alpha = Math.Max(alpha, score);
}
Console.WriteLine($" {bestMove.Square} -> {bestMove.Value}");
return bestMove;
}
/// <summary>
/// Alpha-Beta pruning algorithm to evaluate the best score that can be achieved from the current board state.
/// </summary>
/// <param name="board">The board to evaluate.</param>
/// <param name="depth">The depth of the search tree to explore.</param>
/// <param name="alpha">The best score the maximizing player can guarantee so far.</param>
/// <param name="beta">The best score the minimizing player can guarantee so far.</param>
/// <param name="maximizingPlayer">Whether the current turn is for the maximizing player.</param>
/// <returns>The best score that can be achieved from the current board state.</returns>
private float AlphaBeta(Board board, int depth, float alpha, float beta, bool maximizingPlayer)
{
// Base case: if we have reached the search tree's depth limit or no moves are possible.
if (depth == 0 || !board.CanPlay())
{
// Call EvaluateBoard to get the heuristic value of the board state
return GameStateEvaluation.EvaluateBoard(board, maximizingPlayer ? color : color.Opponent());
}
if (maximizingPlayer)
{
// Best evaluation for maximizing player, starts at the worst case (lowest value).
float maxEval = int.MinValue;
// Consider all possible moves for the maximizing player.
foreach (var move in board.PossibleMoves(color))
{
// Simulate the move on a clone of the board.
Board newBoard = (Board)board.Clone();
newBoard.PlacePiece(move);
// Recursively perform Alpha-Beta pruning to evaluate the move.
float eval = AlphaBeta(newBoard, depth - 1, alpha, beta, false);
// Find the best evaluation value.
maxEval = Math.Max(maxEval, eval);
// Update alpha if a better evaluation is found.
alpha = Math.Max(alpha, eval);
// Alpha-Beta pruning condition: stop evaluating if we find a move that's worse than
// the best option for the minimizing player.
if (beta <= alpha)
break;
}
return maxEval;
}
else
{
// Best evaluation for minimizing player, starts at the worst case (highest value).
float minEval = int.MaxValue;
// Consider all possible moves for the minimizing player.
foreach (var move in board.PossibleMoves(color.Opponent()))
{
// Simulate the move on a clone of the board.
Board newBoard = (Board)board.Clone();
newBoard.PlacePiece(move);
// Recursively perform Alpha-Beta pruning to evaluate the move.
float eval = AlphaBeta(newBoard, depth - 1, alpha, beta, true);
// Find the best evaluation value.
minEval = Math.Min(minEval, eval);
// Update beta if a better evaluation is found.
beta = Math.Min(beta, eval);
// Alpha-Beta pruning condition: stop evaluating if we find a move that's worse than
// the best option for the minimizing player.
if (beta <= alpha)
break;
}
return minEval;
}
}
/// <summary>
///
/// </summary>
/// <param name="moves"></param>
/// <returns>Move chosen by a human player.</returns>
private Move GetHumanMove(List<Move> moves)
{
while (true)
{
var square = GetSquare();
// check if given square is one of the possible moves
if (moves.Exists(x => square.Equals(x.Square)))
{
return moves.Find(x => square.Equals(x.Square));
}
ConsoleVisuals.Error($" Can't place a {color.Name()} disk in square {square}!");
}
}
/// <summary>
/// Asks human player for square coordinates.
/// </summary>
/// <returns>The chosen square coordinates.</returns>
private static Square GetSquare()
{
while (true)
{
try
{
Console.Write(" Give disk position (x,y): ");
var coords = Console.ReadLine();
if (string.IsNullOrEmpty(coords) || coords.Length != 3 || coords[1] != ',')
{
throw new FormatException("Invalid coordinates");
}
var x = int.Parse(coords[0..1]);
var y = int.Parse(coords[2..3]);
return new Square(x, y);
}
catch (FormatException)
{
ConsoleVisuals.Error(" Give coordinates in the form 'x,y'");
}
}
}
/// <summary>
///
/// </summary>
/// <returns>Player type description string</returns>
private string TypeString()
{
return isHuman ? "Human " : "Computer";
}
public override string ToString()
{
return $"{color.Name()} | {TypeString()} | Moves: {roundsPlayed}";
}
}
}