-
Notifications
You must be signed in to change notification settings - Fork 0
/
console_interface.cpp
324 lines (263 loc) · 11.7 KB
/
console_interface.cpp
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
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
#include "console_interface.h"
#include <chrono>
#include <thread>
#include <climits>
#include "board.h"
bool is_integer(const std::string& s) {
std::string::const_iterator it = s.begin();
while (it != s.end() && std::isdigit(*it)) ++it;
return !s.empty() && it == s.end();
}
char get_yes_or_no_response(const std::string& prompt) {
char response;
while (true) {
std::cout << prompt;
std::cin >> response;
if (std::cin.fail()) {
std::cin.clear(); // clear the error state
std::cin.ignore(std::numeric_limits<std::streamsize>::max(),
'\n'); // ignore the rest of the line
std::cout << "Invalid input. Please enter 'y' or 'n'.\n";
} else if (response != 'y' && response != 'n' && response != 'Y' &&
response != 'N') {
std::cin.ignore(std::numeric_limits<std::streamsize>::max(),
'\n'); // clear the rest of the line
std::cout << "Invalid response. Please enter 'y' or 'n'.\n";
} else {
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
return std::tolower(response);
}
}
}
template <>
int get_parameter_within_bounds<int>(const std::string& prompt, int lower_bound,
int upper_bound) {
std::string input;
int value;
while (true) {
std::cout << prompt;
std::cin >> input;
// Check if input is a valid integer
if (!is_integer(input)) {
std::cout << "Invalid input. Please enter a valid integer.\n";
continue;
}
// Convert string to int
value = std::stoi(input);
// Check if value is within bounds
if (!is_in_bounds(value, lower_bound, upper_bound)) {
std::cout << "Invalid value. Please try again.\n";
} else {
break;
}
}
return value;
}
template <>
double get_parameter_within_bounds<double>(const std::string& prompt,
double lower_bound,
double upper_bound) {
std::string input;
double value;
while (true) {
std::cout << prompt;
std::cin >> input;
// Check if input is a valid double
try {
value = std::stod(input);
} catch (std::invalid_argument&) {
std::cout << "Invalid input. Please enter a valid number.\n";
continue;
}
// Check if value is within bounds
if (!is_in_bounds(value, lower_bound, upper_bound)) {
std::cout << "Invalid value. Please try again.\n";
} else {
break;
}
}
return value;
}
std::unique_ptr<Mcts_player> create_mcts_agent(
const std::string& agent_prompt) {
std::cout << "\nInitializing " << agent_prompt << ":\n";
int max_decision_time_ms = get_parameter_within_bounds(
"Enter max decision time in milliseconds (at least 100): ", 100, INT_MAX);
double exploration_constant = 1.41;
if (get_yes_or_no_response("Would you like to change the default exploration "
"constant (1.41)? (y/n): ") == 'y') {
exploration_constant = get_parameter_within_bounds(
"Enter exploration constant (between 0.1 and 2): ", 0.1, 2.0);
}
bool is_parallelized =
(get_yes_or_no_response(
"Would you like to parallelize the agent? (y/n): ") == 'y');
bool is_verbose = false;
if (!is_parallelized) {
is_verbose = (get_yes_or_no_response(
"Would you like to enable verbose mode? (y/n): ") == 'y');
}
return std::make_unique<Mcts_player>(
exploration_constant, std::chrono::milliseconds(max_decision_time_ms),
is_parallelized, is_verbose);
}
void countdown(int seconds) {
while (seconds > 0) {
std::cout << "The agent will start thinking loudly in " << seconds
<< " ...\n";
std::this_thread::sleep_for(std::chrono::seconds(1));
--seconds;
}
}
void start_match_against_robot() {
int human_player_number = get_parameter_within_bounds(
"Enter '1' if you want to be Player 1 (Blue, Vertical) or '2' if you "
"want to be "
"Player 2 (Red, Horizontal): ",
1, 2);
int board_size = get_parameter_within_bounds(
"Enter board size (between 2 and 11): ", 2, 11);
auto mcts_agent = create_mcts_agent("agent");
auto human_player = std::make_unique<Human_player>();
if (human_player_number == 1) {
Game game(board_size, std::move(human_player), std::move(mcts_agent));
game.play();
} else {
if (mcts_agent->get_is_verbose()) {
countdown(3);
}
Game game(board_size, std::move(mcts_agent), std::move(human_player));
game.play();
}
}
void start_robot_arena() {
int board_size = get_parameter_within_bounds(
"Enter board size (between 2 and 11): ", 2, 11);
auto mcts_agent_1 = create_mcts_agent("first agent");
auto mcts_agent_2 = create_mcts_agent("second agent");
Game game(board_size, std::move(mcts_agent_1), std::move(mcts_agent_2));
game.play();
}
void start_human_arena() {
int board_size = get_parameter_within_bounds(
"Enter board size (between 2 and 11): ", 2, 11);
auto human_player_1 = std::make_unique<Human_player>();
auto human_player_2 = std::make_unique<Human_player>();
Game game(board_size, std::move(human_player_1), std::move(human_player_2));
game.play();
}
void run_console_interface() {
print_welcome_ascii_art();
std::cout << "Welcome.\n";
bool is_running = true;
while (is_running) {
try {
int option = 0;
std::cout << "\nMENU:\n"
<< "\n[1] Play against a robot\n"
<< "[2] Robot arena\n"
<< "[3] Human arena\n"
<< "[4] Read the docs\n"
<< "[5] (H)Exit\n";
option = get_parameter_within_bounds("Option: ", 1, 5);
std::cout << "\n";
switch (option) {
case 1:
start_match_against_robot();
break;
case 2:
start_robot_arena();
break;
case 3:
start_human_arena();
break;
case 4:
print_docs();
break;
case 5:
is_running = false;
break;
default:
break;
}
} catch (const std::invalid_argument& e) {
std::cout << "Error: " << e.what() << "\n";
} catch (const std::logic_error& e) {
std::cout << "Error: " << e.what() << "\n";
} catch (const std::runtime_error& e) {
std::cout << "Error: " << e.what() << "\n";
}
}
print_exit_ascii_art();
}
void print_welcome_ascii_art() {
std::cout << R"(
__ __ _____ _______ _____ ___ _
| \/ |/ ____|__ __/ ____| / / | | |
| \ / | | | | | (___ / /| |__| | _____ __
| |\/| | | | | \___ \ / / | __ |/ _ \ \/ /
| | | | |____ | | ____) / / | | | | __/> <
|_| |_|\_____| |_| |_____/_/ |_| |_|\___/_/\_\
)" << '\n';
}
void print_board_and_winner(Board& board) {
board.display_board(std::cout);
Cell_state winner = board.check_winner();
std::cout << "Winner: " << winner << std::endl;
std::cout << "------------------" << std::endl;
}
void display_winning_condition() {
// Demo case 1: 3x3 board, player Cell_state::Blue wins
Board board_1(3);
board_1.make_move(0, 2, Cell_state::Blue);
board_1.make_move(1, 1, Cell_state::Blue);
board_1.make_move(2, 1, Cell_state::Blue);
print_board_and_winner(board_1);
// Demo case 2: 3x3 board, player Cell_state::Red wins
Board board_2(3);
board_2.make_move(1, 0, Cell_state::Red);
board_2.make_move(1, 1, Cell_state::Red);
board_2.make_move(0, 2, Cell_state::Red);
print_board_and_winner(board_2);
// Demo case 2: 3x3 board, player Cell_state::Red wins
Board board3(5);
board3.make_move(3, 0, Cell_state::Red);
board3.make_move(3, 1, Cell_state::Red);
board3.make_move(2, 2, Cell_state::Red);
board3.make_move(1, 3, Cell_state::Red);
board3.make_move(1, 4, Cell_state::Red);
print_board_and_winner(board3);
}
void print_docs() {
std::cout << R"(
Hex is a two-player, zero-sum, perfect information game invented by the Danish mathematician Piet Hein and independently by the American mathematician John Nash. As it is a deterministic strategy game, chance plays no part in Hex, and unlike in chess or checkers, there are no 'draw' outcomes in Hex - there is always a winner and a loser.
The game is played on a rhombus-shaped board divided into hexagonal cells. The standard game board sizes are 11x11 or 13x13, but the size can be any square board from 2x2 up to 19x19 for tournament rules.
Each player is assigned a pair of opposite sides of the board, and the goal of each player is to form a connected path of their own stones linking their two sides. Usually, the blue player goes first and tries to create a vertical path, while the red player goes second and tries to create horizontal path. The player who completes their path first is the winner. The game does not allow for ties, and, given perfect play by both players, the first player can always win.
The game requires strategic depth as players must balance between extending their own path and blocking their opponent. Although the rules are straightforward, the strategic complexity becomes apparent as you gain experience.
In this console implementation, the connections between the cells are displayed by hyphens and slashes. Let's look at how the board is displayed and some sample winning conditions:
)" << std::endl;
display_winning_condition();
std::cout << R"(
The robots in this implementation are powered by a AI agent using a powerful strategy known as Monte Carlo Tree Search (MCTS). The MCTS is a heuristic search algorithm known for its effectiveness in decision-making problems, particularly in games like Hex.
This implementation of MCTS consists of four main phases:
1. Expansion: From the root node (representing the current game state), child nodes are found by detecting the moves allowed by the game state.
2. Selection: A child with the most promising score of Upper Confidence Bound applied to Trees (UCT) is selected for a random playout.
3. Simulation: A simulation is run from the child according to the default policy; in this case, a random game is played out.
4. Backpropagation: The result of the simulation is backpropagated through the tree. The parent and the chosen child node have their visit count incremented and their value updated.
This process is repeated until the computational budget (based on time) is exhausted. The agent then selects the move that leads to the most promising child node.
In this implementation, the MCTS agent also supports parallel simulations by running multiple threads, each executing an MCTS iteration. The non-parallelised agent can run in verbose mode, outputting detailed information about each MCTS iteration, which can be a valuable tool for understanding the decision-making process of the AI.
It should be noted that while MCTS does incorporate randomness (through the simulation phase), it is not a purely random algorithm. It uses the results of previous iterations to make informed decisions, and over time it builds a more accurate representation of the search space.
Remember - defense is offense. Good luck!
Author: Patrikas Vanagas, 2023
)" << std::endl;
}
void print_exit_ascii_art() {
std::cout << R"(
__ ___ __ __ ___ ____ __ _ __ __
/ |/ /___ ___ __ / /_/ /_ ___ / | / _/ / /_ ___ _ __(_) /_/ /_ __ ______ __ __
/ /|_/ / __ `/ / / / / __/ __ \/ _ \ / /| | / / / __ \/ _ \ | | /| / / / __/ __ \ / / / / __ \/ / / /
/ / / / /_/ / /_/ / / /_/ / / / __/ / ___ |_/ / / /_/ / __/ | |/ |/ / / /_/ / / / / /_/ / /_/ / /_/ /
/_/ /_/\__,_/\__, / \__/_/ /_/\___/ /_/ |_/___/ /_.___/\___/ |__/|__/_/\__/_/ /_/ \__, /\____/\__,_/
/____/ /____/
)" << '\n';
}