add main.cpp
This commit is contained in:
@@ -0,0 +1,912 @@
|
|||||||
|
#include <iostream>
|
||||||
|
#include <conio.h>
|
||||||
|
#include <ctime>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <fstream>
|
||||||
|
#ifdef _WIN32
|
||||||
|
#include <windows.h>
|
||||||
|
#endif
|
||||||
|
using namespace std;
|
||||||
|
|
||||||
|
const int N = 8;
|
||||||
|
const char EMPTY = '.';
|
||||||
|
const char BLACK = 'B';
|
||||||
|
const char WHITE = 'W';
|
||||||
|
const string BLACK_GLYPH = "○";
|
||||||
|
const string WHITE_GLYPH = "●";
|
||||||
|
const string EMPTY_GLYPH = ".";
|
||||||
|
const string LEGAL_GLYPH = "+";
|
||||||
|
const string SAVE_FILE = "othello_saves.txt";
|
||||||
|
const string HISTORY_FILE = "othello_history.txt";
|
||||||
|
const int DR[8] = {-1,-1,-1, 0,0, +1,+1,+1};
|
||||||
|
const int DC[8] = {-1, 0,+1,-1,+1,-1, 0,+1};
|
||||||
|
bool vsBot = false;
|
||||||
|
string player1_name, player2_name;
|
||||||
|
|
||||||
|
struct PlayerInfo {
|
||||||
|
char id; // 'B' or 'W'
|
||||||
|
const char* name; // "Black", "White"
|
||||||
|
const char* glyph; // "○", "●"
|
||||||
|
};
|
||||||
|
PlayerInfo BLACK_PLAYER = {'B', "Black", "○"};
|
||||||
|
PlayerInfo WHITE_PLAYER = {'W', "White", "●"};
|
||||||
|
|
||||||
|
|
||||||
|
bool in_bounds(int r, int c) { //checks if a tile is in the board or out of bounds
|
||||||
|
return r >= 0 && r < 8 && c >= 0 && c < 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
string trim_newline(string s) { //just makes sure the user input is clean (trims \n and \r)
|
||||||
|
while (!s.empty() && (s.back() == '\n' || s.back() == '\r')) {
|
||||||
|
s.pop_back();
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
long long now_epoch() { //gets epoch time. used for creating save files
|
||||||
|
return (long long)time(nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
int idx(int row, int col) { //converts row and column to an index between 0 and 63
|
||||||
|
return row * 8 + col;
|
||||||
|
}
|
||||||
|
char other_player(char p) { //finds out who the opponent is based on the player given
|
||||||
|
return (p == BLACK) ? WHITE : BLACK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// for Unicode symbols. I first created the program with B and W for black and white but
|
||||||
|
/// when including Unicode symbols I had to add this to avoid hard-coding///
|
||||||
|
string glyph_for_cell(char cell) {
|
||||||
|
if (cell == BLACK) return BLACK_GLYPH;
|
||||||
|
if (cell == WHITE) return WHITE_GLYPH;
|
||||||
|
if (cell == EMPTY) return EMPTY_GLYPH;
|
||||||
|
if (cell == '+') return LEGAL_GLYPH;
|
||||||
|
// fallback
|
||||||
|
string s;
|
||||||
|
s.push_back(cell);
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// this next function receives a tile coordinates and a direction then finds out how many opposing cell
|
||||||
|
/// would flip if the cell is played///
|
||||||
|
int count_flips_dir(const char board[64], int r, int c, int dr, int dc, char player) {
|
||||||
|
char opponent = (player == BLACK) ? 'W' : 'B';
|
||||||
|
|
||||||
|
int rr = r + dr;
|
||||||
|
int cc = c + dc;
|
||||||
|
// 1) first step must be inside board
|
||||||
|
if (!in_bounds(rr, cc)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
// 2) first cell must be opponent
|
||||||
|
if (board[idx(rr, cc)] != opponent)
|
||||||
|
return 0;
|
||||||
|
int count = 0;
|
||||||
|
// 3) walk while we keep seeing opponent
|
||||||
|
while (in_bounds(rr, cc) && board[idx(rr, cc)] == opponent) {
|
||||||
|
count++;
|
||||||
|
rr += dr;
|
||||||
|
cc += dc;
|
||||||
|
}
|
||||||
|
// 4) must end on player's own piece
|
||||||
|
if (in_bounds(rr, cc) && board[idx(rr, cc)] == player) {
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
// otherwise: open-ended or out-of-bounds
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int total_flips(const char board[64], int r, int c, char player) { //finds the sum of flips in all directions
|
||||||
|
|
||||||
|
if (board[idx(r, c)] != '.') //first checks if the cell is not empty
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
int total = 0;
|
||||||
|
|
||||||
|
for (int k = 0; k < 8; k++) { //using the previous function, finds the total sum of flips
|
||||||
|
total += count_flips_dir(board, r, c, DR[k], DC[k], player);
|
||||||
|
}
|
||||||
|
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// finds out if a cell is a valid move by first checking if it's empty then finding if flip count is positive///
|
||||||
|
bool is_legal_move(const char board[64], int r, int c, char player) {
|
||||||
|
if (board[idx(r, c)] != '.')
|
||||||
|
return false;
|
||||||
|
return total_flips(board, r, c, player) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// stores all of the legal moves into an array of length 64, called moves[], does this using
|
||||||
|
/// an index between 0 and 63 with the help of idx function. no 2D indexing here///
|
||||||
|
int collect_legal_moves(const char board[64], char player, int moves[64]) {
|
||||||
|
int count = 0;
|
||||||
|
for (int r = 0; r < 8; r++) {
|
||||||
|
for (int c = 0; c < 8; c++) {
|
||||||
|
if (is_legal_move(board, r, c, player)) {
|
||||||
|
moves[count++] = idx(r, c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
///this is my core game logic. this function first finds out player move in 2D indexing so
|
||||||
|
///we can find out how many cells would flip. then applies it by flipping exactly that number of cells
|
||||||
|
///in the given direction///
|
||||||
|
void apply_move(char board[64], int pos, char player) {
|
||||||
|
// 1) Place the player's piece on the chosen position.
|
||||||
|
board[pos] = player;
|
||||||
|
|
||||||
|
// Convert linear index to row/col so my directional logic can be used.
|
||||||
|
int r0 = pos / 8;
|
||||||
|
int c0 = pos % 8;
|
||||||
|
|
||||||
|
// 3) For each direction:
|
||||||
|
// - ask how many flips happen in that direction
|
||||||
|
// - if flips > 0, walk that many steps and overwrite those tiles to 'player'
|
||||||
|
for (int k = 0; k < 8; k++) {
|
||||||
|
int flips = count_flips_dir(board, r0, c0, DR[k], DC[k], player);
|
||||||
|
if (flips <= 0) {
|
||||||
|
continue;
|
||||||
|
} // no captured line in this direction
|
||||||
|
|
||||||
|
// Start at the neighbor cell (first step in the direction)
|
||||||
|
int r = r0 + DR[k];
|
||||||
|
int c = c0 + DC[k];
|
||||||
|
|
||||||
|
// Flip exactly 'flips' opponent pieces
|
||||||
|
for (int i = 0; i < flips; i++) {
|
||||||
|
board[idx(r, c)] = player;
|
||||||
|
r += DR[k];
|
||||||
|
c += DC[k];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool has_any_legal_move(const char board[64], char player) {
|
||||||
|
int tmp[64];
|
||||||
|
return collect_legal_moves(board, player, tmp) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// first finds out which moves are possible using our collect_legal_moves function, then picks one
|
||||||
|
/// with the help of the rand() method///
|
||||||
|
int bot_choose_random_move(const char board[64], char botPlayer) {
|
||||||
|
int moves[64];
|
||||||
|
int cnt = collect_legal_moves(board, botPlayer, moves);
|
||||||
|
if (cnt <= 0) return -1;
|
||||||
|
int pick = rand() % cnt;
|
||||||
|
return moves[pick];
|
||||||
|
}
|
||||||
|
|
||||||
|
// used for the end of the game, counts how many cells belong to each player
|
||||||
|
int count_pieces(const char board[64], char who) {
|
||||||
|
int cnt = 0;
|
||||||
|
for (int i = 0; i < 64; i++) {
|
||||||
|
if (board[i] == who) cnt++;
|
||||||
|
}
|
||||||
|
return cnt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// if a player has no legal move then it passes to the other player and if both have no legal
|
||||||
|
/// moves then it changes the game state to 'E' which is short for End///
|
||||||
|
void normalize_turn_with_pass_and_end(const char board[64], char &turn) {
|
||||||
|
if (turn == 'E') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (has_any_legal_move(board, turn)) {
|
||||||
|
return;
|
||||||
|
} // current player can play
|
||||||
|
|
||||||
|
char opp = other_player(turn);
|
||||||
|
if (!has_any_legal_move(board, opp)) {
|
||||||
|
// nobody can move => end
|
||||||
|
turn = 'E';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// current player must pass
|
||||||
|
cout << "\n" << turn << " has no legal moves. Press any key to pass...\n";
|
||||||
|
(void)getch();
|
||||||
|
turn = opp;
|
||||||
|
}
|
||||||
|
|
||||||
|
void print_final_score(const char board[64]) {
|
||||||
|
int b = count_pieces(board, BLACK);
|
||||||
|
int w = count_pieces(board, WHITE);
|
||||||
|
|
||||||
|
cout << "\n=== GAME OVER ===\n";
|
||||||
|
cout << "Black (" << BLACK_GLYPH << "): " << b << "\n";
|
||||||
|
cout << "White (" << WHITE_GLYPH << "): " << w << "\n";
|
||||||
|
|
||||||
|
if (b > w) {
|
||||||
|
cout << "Winner: Black\n";
|
||||||
|
} else if (w > b) {
|
||||||
|
cout << "Winner: White\n";
|
||||||
|
} else {
|
||||||
|
cout << "Result: Draw\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
cout << "Press any key...\n";
|
||||||
|
(void)getch();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// receives a board[] and creates a string based on it which is used for saving
|
||||||
|
string serialize_state(char turn, const char board[64]) {
|
||||||
|
string s;
|
||||||
|
s.reserve(65);
|
||||||
|
s.push_back(turn);
|
||||||
|
for (int i = 0; i < 64; ++i) {
|
||||||
|
s.push_back(board[i]);
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// receives a string (probably from a save file) then finds the game state from it.
|
||||||
|
/// first checks if the string is valid by checking the length of the string
|
||||||
|
/// also the starting letter should be B/W/E.
|
||||||
|
bool deserialize_state (const string& s, char &turn, char board[64]) {
|
||||||
|
if (static_cast<int>(s.size()) != 65) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
char t = s[0];
|
||||||
|
if (!(t == 'B' || t == 'W' || t == 'E')) { // B= black's turn - W= white's turn - E= end of the game
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 1; i < 65; i++) {
|
||||||
|
char ch = s[i];
|
||||||
|
if (!(ch == EMPTY || ch == BLACK || ch == WHITE)) return false;
|
||||||
|
board[i - 1] = ch;
|
||||||
|
}
|
||||||
|
turn = t;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// save file includes more data than the state string, this function parses out everything from a
|
||||||
|
/// save entry including id, epoch, state string itself///
|
||||||
|
bool parse_save_line_v2(const string& line, int &id, long long &epoch, string &state65) {
|
||||||
|
// Format: id|epoch|mode|state65
|
||||||
|
// mode is BOT or PVP
|
||||||
|
// first we find '|'s in the save entry and remember their positions
|
||||||
|
size_t p1 = line.find('|');
|
||||||
|
if (p1 == string::npos) return false;
|
||||||
|
size_t p2 = line.find('|', p1 + 1);
|
||||||
|
if (p2 == string::npos) return false;
|
||||||
|
size_t p3 = line.find('|', p2 + 1);
|
||||||
|
if (p3 == string::npos) return false;
|
||||||
|
|
||||||
|
string sid = line.substr(0, p1); //extracting strings for id, epoch, mode and state
|
||||||
|
string sep = line.substr(p1 + 1, p2 - (p1 + 1));
|
||||||
|
string smode = line.substr(p2 + 1, p3 - (p2 + 1));
|
||||||
|
string sst = line.substr(p3 + 1);
|
||||||
|
|
||||||
|
if ((int)sst.size() != 65) return false;
|
||||||
|
if (!(smode == "BOT" || smode == "PVP")) return false;
|
||||||
|
|
||||||
|
if (sid.empty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int idv = 0; //casting characters into numbers
|
||||||
|
for (char ch : sid) {
|
||||||
|
if (ch < '0' || ch > '9') { //only valid characters (numbers from 0 to 9)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
idv = idv * 10 + (ch - '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sep.empty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
long long ev = 0;
|
||||||
|
for (char ch : sep) { //casting characters into numbers
|
||||||
|
if (ch < '0' || ch > '9') return false;
|
||||||
|
ev = ev * 10 + (ch - '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
id = idv;
|
||||||
|
epoch = ev;
|
||||||
|
state65 = sst;
|
||||||
|
|
||||||
|
vsBot = (smode == "BOT");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int next_save_id_1_to_1000(const string& filename) {
|
||||||
|
|
||||||
|
bool oldVsBot = vsBot;
|
||||||
|
bool used[1001];
|
||||||
|
for (int i = 0; i <= 1000; i++) {
|
||||||
|
used[i] = false;
|
||||||
|
}
|
||||||
|
ifstream in(filename);
|
||||||
|
if (in) {
|
||||||
|
string line;
|
||||||
|
while (getline(in, line)) {
|
||||||
|
line = trim_newline(line);
|
||||||
|
if (line.empty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
int id; long long epoch; string state;
|
||||||
|
if (parse_save_line_v2(line, id, epoch, state)) {
|
||||||
|
if (id >= 1 && id <= 1000) {
|
||||||
|
used[id] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int id = 1; id <= 1000; id++) {
|
||||||
|
if (!used[id]) {
|
||||||
|
vsBot = oldVsBot;
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
vsBot = oldVsBot;
|
||||||
|
return -1; // full-no id available
|
||||||
|
}
|
||||||
|
bool save_game(const string& filename, int id, const string& state65) {
|
||||||
|
ofstream out(filename, ios::app);
|
||||||
|
if (!out) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Format: id|epoch|mode|state65
|
||||||
|
out << id << "|" << now_epoch() << "|" << (vsBot ? "BOT" : "PVP") << "|" << state65 << "\n";
|
||||||
|
out.flush();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// keeps searching for valid games then holds on to the last unfinished game
|
||||||
|
bool load_latest_unfinished(const string& filename, int &outId, string &outState65) {
|
||||||
|
ifstream in(filename);
|
||||||
|
if (!in) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool found = false;
|
||||||
|
string line;
|
||||||
|
|
||||||
|
while (getline(in, line)) {
|
||||||
|
line = trim_newline(line);
|
||||||
|
if (line.empty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
int id; long long epoch; string state;
|
||||||
|
if (!parse_save_line_v2(line, id, epoch, state)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
char turn; char board[64];
|
||||||
|
if (!deserialize_state(state, turn, board)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (turn != 'E') {
|
||||||
|
outId = id;
|
||||||
|
outState65 = state;
|
||||||
|
found = true; // keep last one
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// receives a target ID and tries to find it in the save file so it can load that game.
|
||||||
|
bool load_game_by_id(const string& filename, int targetId, string &outState65) {
|
||||||
|
ifstream in(filename);
|
||||||
|
if (!in) return false;
|
||||||
|
|
||||||
|
string line;
|
||||||
|
while (getline(in, line)) {
|
||||||
|
line = trim_newline(line);
|
||||||
|
if (line.empty()) continue;
|
||||||
|
|
||||||
|
int id;
|
||||||
|
long long epoch;
|
||||||
|
string state;
|
||||||
|
|
||||||
|
if (!parse_save_line_v2(line, id, epoch, state)) continue;
|
||||||
|
|
||||||
|
if (id == targetId) {
|
||||||
|
outState65 = state;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// parsing history file looking for '|'s. se: string epoch, sb: string black count, sw: string white count,
|
||||||
|
/// ev: epoch value (numeric), etc...
|
||||||
|
bool parse_history_line(const string& line, long long &epoch, int &b, int &w, string &result) {
|
||||||
|
size_t p1 = line.find('|');
|
||||||
|
if (p1 == string::npos) return false;
|
||||||
|
size_t p2 = line.find('|', p1 + 1);
|
||||||
|
if (p2 == string::npos) return false;
|
||||||
|
size_t p3 = line.find('|', p2 + 1);
|
||||||
|
if (p3 == string::npos) return false;
|
||||||
|
|
||||||
|
string se = line.substr(0, p1);
|
||||||
|
string sb = line.substr(p1 + 1, p2 - (p1 + 1));
|
||||||
|
string sw = line.substr(p2 + 1, p3 - (p2 + 1));
|
||||||
|
result = line.substr(p3 + 1);
|
||||||
|
|
||||||
|
if (se.empty()) return false;
|
||||||
|
long long ev = 0;
|
||||||
|
for (char ch : se) { //casting characters to numbers
|
||||||
|
if (ch < '0' || ch > '9') return false;
|
||||||
|
ev = ev * 10 + (ch - '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sb.empty()) return false;
|
||||||
|
int bv = 0;
|
||||||
|
for (char ch : sb) { //casting characters to numbers
|
||||||
|
if (ch < '0' || ch > '9') return false;
|
||||||
|
bv = bv * 10 + (ch - '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sw.empty()) return false;
|
||||||
|
int wv = 0;
|
||||||
|
for (char ch : sw) { //casting characters to numbers
|
||||||
|
if (ch < '0' || ch > '9') return false;
|
||||||
|
wv = wv * 10 + (ch - '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
epoch = ev;
|
||||||
|
b = bv;
|
||||||
|
w = wv;
|
||||||
|
result = trim_newline(result);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void append_history(const string& filename, const char board[64]) {
|
||||||
|
int b = 0, w = 0;
|
||||||
|
for (int i = 0; i < 64; i++) {
|
||||||
|
if (board[i] == BLACK) b++;
|
||||||
|
else if (board[i] == WHITE) w++;
|
||||||
|
}
|
||||||
|
string result;
|
||||||
|
if (b > w) {
|
||||||
|
result = "B";
|
||||||
|
}
|
||||||
|
else if (w > b) {
|
||||||
|
result = "W";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
result = "DRAW";
|
||||||
|
}
|
||||||
|
|
||||||
|
ofstream out(filename, ios::app);
|
||||||
|
if (!out) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
out << now_epoch() << "|" << b << "|" << w << "|" << result << "\n";
|
||||||
|
out.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
void init_board(char board[64], char &turn) {
|
||||||
|
for (int i = 0; i < 64; ++i) {
|
||||||
|
board[i] = EMPTY;
|
||||||
|
}
|
||||||
|
board[idx(3,3)] = WHITE;
|
||||||
|
board[idx(4,4)] = WHITE;
|
||||||
|
board[idx(3,4)] = BLACK;
|
||||||
|
board[idx(4,3)] = BLACK;
|
||||||
|
turn = BLACK;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// print board with coordinate guides
|
||||||
|
void print_board(const char board[64]) {
|
||||||
|
for (int i = -1; i < 8; ++i) {
|
||||||
|
for (int j = -1; j < 8; ++j) {
|
||||||
|
if (i == -1 && j == -1) {
|
||||||
|
cout << " ";
|
||||||
|
} else if (i == -1) {
|
||||||
|
cout << " " << j + 1 << " ";
|
||||||
|
} else if (j == -1) {
|
||||||
|
cout << " " << static_cast<char>('A' + i) << " ";
|
||||||
|
} else {
|
||||||
|
cout << " " << glyph_for_cell(board[idx(i, j)]) << " ";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cout << '\n' << '\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void clear_screen() {
|
||||||
|
for (int i = 0; i < 40; ++i)
|
||||||
|
cout << "\n";
|
||||||
|
}
|
||||||
|
void press_any_key() {
|
||||||
|
cout << "\nPress any key...\n";
|
||||||
|
(void)getch();
|
||||||
|
}
|
||||||
|
int read_key() {
|
||||||
|
int k = getch();
|
||||||
|
if (k == 0 || k == 224) return 1000 + getch(); // extended keys
|
||||||
|
return k;
|
||||||
|
}
|
||||||
|
void show_final_score(const char board[64]) {
|
||||||
|
int b = count_pieces(board, BLACK);
|
||||||
|
int w = count_pieces(board, WHITE);
|
||||||
|
|
||||||
|
clear_screen();
|
||||||
|
cout << "=== GAME OVER ===\n\n";
|
||||||
|
cout << "Black (" << BLACK_GLYPH << "): " << b << "\n";
|
||||||
|
cout << "White (W): " << w << "\n\n";
|
||||||
|
|
||||||
|
if (b > w) {
|
||||||
|
cout << "Winner: Black\n";
|
||||||
|
} else if (w > b) {
|
||||||
|
cout << "Winner: White\n";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
cout << "Result: Draw\n";
|
||||||
|
}
|
||||||
|
press_any_key();
|
||||||
|
}
|
||||||
|
|
||||||
|
int show_menu_and_get_choice() {
|
||||||
|
clear_screen();
|
||||||
|
cout << "=== MENU ===\n\n";
|
||||||
|
cout << "1) New Game\n";
|
||||||
|
cout << "2) Load Game\n";
|
||||||
|
cout << "3) Help\n";
|
||||||
|
cout << "4) Game History\n";
|
||||||
|
cout << "5) Exit\n\n";
|
||||||
|
cout << "Choose (1-5): ";
|
||||||
|
int ch = 0;
|
||||||
|
if (!(cin >> ch)) {
|
||||||
|
cin.clear();
|
||||||
|
cin.ignore(1000000, '\n');
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
cin.ignore(1000000, '\n');
|
||||||
|
return ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// draw the board. showing where does the keyboard point at(cursor) and where are the legal moves
|
||||||
|
void draw_board_with_cursor(const char board[64],
|
||||||
|
const int legalMoves[64], int legalCount,
|
||||||
|
int cursorR, int cursorC, char turn) {
|
||||||
|
bool legal[64] = {false};
|
||||||
|
for (int i = 0; i < legalCount; i++) legal[legalMoves[i]] = true;
|
||||||
|
|
||||||
|
clear_screen();
|
||||||
|
if (vsBot) {
|
||||||
|
cout << player1_name << " VS " << "BOT" << "\n";
|
||||||
|
} else {
|
||||||
|
cout << player1_name << " VS " << player2_name << "\n";
|
||||||
|
}
|
||||||
|
cout << "Turn: " << turn << " (Enter = Play, S = Save, L = Load, Q = Quit)\n";
|
||||||
|
cout << "Arrows = Move Cursor | Legal moves marked with '+'\n";
|
||||||
|
cout << BLACK_PLAYER.name << ": " << BLACK_PLAYER.glyph
|
||||||
|
<< " "
|
||||||
|
<< WHITE_PLAYER.name << ": " << WHITE_PLAYER.glyph
|
||||||
|
<< "\n\n";
|
||||||
|
|
||||||
|
|
||||||
|
cout << " 1 2 3 4 5 6 7 8\n";
|
||||||
|
for (int r = 0; r < 8; r++) {
|
||||||
|
cout << " " << char('A' + r) << " ";
|
||||||
|
for (int c = 0; c < 8; c++) {
|
||||||
|
int p = idx(r,c);
|
||||||
|
char cell = board[p];
|
||||||
|
|
||||||
|
// show legal marker on empty tiles
|
||||||
|
if (cell == '.' && legal[p]) {
|
||||||
|
cell = '+';
|
||||||
|
}
|
||||||
|
|
||||||
|
// cursor highlight
|
||||||
|
if (r == cursorR && c == cursorC) {
|
||||||
|
cout << "[" << glyph_for_cell(cell) << "]";
|
||||||
|
} else {
|
||||||
|
cout << " " << glyph_for_cell(cell) << " ";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cout << "\n\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void draw_board(const char board[64],
|
||||||
|
const int* legalMoves, int legalCount,
|
||||||
|
int cursorR, int cursorC) {
|
||||||
|
// draw_board_with_cursor expects an array of size 64.
|
||||||
|
// If we got nullptr, provide a dummy array and show no legal moves. useful for when the game has ended
|
||||||
|
int dummy[64];
|
||||||
|
for (int i = 0; i < 64; i++) dummy[i] = 0;
|
||||||
|
|
||||||
|
if (legalMoves == nullptr) {
|
||||||
|
draw_board_with_cursor(board, dummy, 0, cursorR, cursorC, 'E');
|
||||||
|
} else {
|
||||||
|
// Copy into fixed-size array so signature matches (no vectors)
|
||||||
|
int tmp[64];
|
||||||
|
for (int i = 0; i < legalCount; i++) tmp[i] = legalMoves[i];
|
||||||
|
draw_board_with_cursor(board, tmp, legalCount, cursorR, cursorC, 'E');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void run_game(char board[64], char &turn) {
|
||||||
|
int cursorR = 3, cursorC = 3; //default cursor at position row=3 col=3
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
normalize_turn_with_pass_and_end(board, turn);
|
||||||
|
|
||||||
|
|
||||||
|
if (turn == 'E') {
|
||||||
|
draw_board(board, nullptr, 0, cursorR, cursorC);
|
||||||
|
show_final_score(board);
|
||||||
|
append_history(HISTORY_FILE, board);
|
||||||
|
return; // back to menu
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
int legalMoves[64];
|
||||||
|
int legalCount = collect_legal_moves(board, turn, legalMoves);
|
||||||
|
|
||||||
|
draw_board_with_cursor(board, legalMoves, legalCount, cursorR, cursorC, turn);
|
||||||
|
|
||||||
|
// BOT TURN: board is already drawn at this point.
|
||||||
|
// We show the move, pause, then apply the move, so the user sees
|
||||||
|
// their move first and bot move second (with a pause in between).
|
||||||
|
if (vsBot && turn == WHITE) {
|
||||||
|
int pos = bot_choose_random_move(board, WHITE);
|
||||||
|
if (pos == -1) {
|
||||||
|
cout << "\nBot has no legal moves. Passing...\n";
|
||||||
|
press_any_key();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
int r = pos / 8;
|
||||||
|
int c = pos % 8;
|
||||||
|
cout << "\nBot played: " << char('A' + r) << (c + 1) << "\n";
|
||||||
|
press_any_key();
|
||||||
|
|
||||||
|
apply_move(board, pos, WHITE);
|
||||||
|
turn = BLACK;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
int key = read_key();
|
||||||
|
|
||||||
|
if (key == 'q' || key == 'Q') return;
|
||||||
|
|
||||||
|
if (key == 's' || key == 'S') {
|
||||||
|
if (turn == 'E') {
|
||||||
|
continue; // no saving allowed after game over
|
||||||
|
}
|
||||||
|
|
||||||
|
string state = serialize_state(turn, board);
|
||||||
|
int id = next_save_id_1_to_1000(SAVE_FILE);
|
||||||
|
|
||||||
|
if (id == -1) {
|
||||||
|
cout << "\nSave failed (IDs full).\n";
|
||||||
|
press_any_key();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
save_game(SAVE_FILE, id, state);
|
||||||
|
cout << "\nSaved. Game ID = " << id << "\n";
|
||||||
|
press_any_key();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key == 'l' || key == 'L') {
|
||||||
|
cout << "\nEnter game ID to load: ";
|
||||||
|
int id;
|
||||||
|
cin >> id;
|
||||||
|
cin.ignore(1000000, '\n');
|
||||||
|
|
||||||
|
string state;
|
||||||
|
if (!load_game_by_id(SAVE_FILE, id, state)) {
|
||||||
|
cout << "Invalid game ID.\n";
|
||||||
|
press_any_key();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!deserialize_state(state, turn, board)) {
|
||||||
|
cout << "Corrupted save.\n";
|
||||||
|
press_any_key();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
normalize_turn_with_pass_and_end(board, turn);
|
||||||
|
cursorR = 3;
|
||||||
|
cursorC = 3;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (key == 1000 + 72 && cursorR > 0) cursorR--; // up
|
||||||
|
else if (key == 1000 + 80 && cursorR < 7) cursorR++; // down
|
||||||
|
else if (key == 1000 + 75 && cursorC > 0) cursorC--; // left
|
||||||
|
else if (key == 1000 + 77 && cursorC < 7) cursorC++; // right
|
||||||
|
else if (key == 13) { // Enter
|
||||||
|
int pos = idx(cursorR, cursorC);
|
||||||
|
if (is_legal_move(board, cursorR, cursorC, turn)) {
|
||||||
|
apply_move(board, pos, turn);
|
||||||
|
turn = other_player(turn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void screen_new_game() {
|
||||||
|
clear_screen();
|
||||||
|
char board[64];
|
||||||
|
char turn;
|
||||||
|
init_board(board, turn);
|
||||||
|
|
||||||
|
clear_screen();
|
||||||
|
cout << "[New Game]\n";
|
||||||
|
cout << "1) Single Player (vs Bot)\n";
|
||||||
|
cout << "2) Two Player\n";
|
||||||
|
cout << "Choose (1-2): ";
|
||||||
|
int mode = 2;
|
||||||
|
cin >> mode;
|
||||||
|
cin.ignore(1000000, '\n');
|
||||||
|
|
||||||
|
if (mode == 1) {
|
||||||
|
vsBot = true;
|
||||||
|
cout << "Please enter your name: ";
|
||||||
|
getline(cin, player1_name);
|
||||||
|
}
|
||||||
|
if (mode == 2) {
|
||||||
|
cout << "Please enter player 1 name: ";
|
||||||
|
getline(cin, player1_name);
|
||||||
|
cout << "Please enter player 2 name: ";
|
||||||
|
getline(cin, player2_name);
|
||||||
|
}
|
||||||
|
run_game(board, turn);
|
||||||
|
}
|
||||||
|
|
||||||
|
void screen_load_game() {
|
||||||
|
clear_screen();
|
||||||
|
cout << "[Load Game]\n\n";
|
||||||
|
|
||||||
|
cout << "1) Load latest unfinished\n";
|
||||||
|
cout << "2) Load by ID\n";
|
||||||
|
cout << "3) Back\n\n";
|
||||||
|
cout << "Choose (1-3): ";
|
||||||
|
|
||||||
|
int choice = 0;
|
||||||
|
if (!(cin >> choice)) {
|
||||||
|
cin.clear();
|
||||||
|
cin.ignore(1000000, '\n');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cin.ignore(1000000, '\n');
|
||||||
|
|
||||||
|
if (choice == 3) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int id = 0;
|
||||||
|
string state65;
|
||||||
|
|
||||||
|
bool ok = false;
|
||||||
|
if (choice == 1) {
|
||||||
|
ok = load_latest_unfinished(SAVE_FILE, id, state65);
|
||||||
|
if (!ok) {
|
||||||
|
cout << "No unfinished saved game found.\n";
|
||||||
|
press_any_key();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (choice == 2) {
|
||||||
|
cout << "Enter game ID to load: ";
|
||||||
|
cin >> id;
|
||||||
|
cin.ignore(1000000, '\n');
|
||||||
|
ok = load_game_by_id(SAVE_FILE, id, state65);
|
||||||
|
if (!ok) {
|
||||||
|
cout << "Invalid game ID.\n";
|
||||||
|
press_any_key();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
char board[64];
|
||||||
|
char turn;
|
||||||
|
|
||||||
|
if (!deserialize_state(state65, turn, board)) {
|
||||||
|
cout << "Save file is corrupted (state invalid).\n";
|
||||||
|
press_any_key();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
normalize_turn_with_pass_and_end(board, turn);
|
||||||
|
|
||||||
|
cout << "Loaded save: ID = " << id << "\n\n";
|
||||||
|
press_any_key();
|
||||||
|
run_game(board, turn);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void screen_help() {
|
||||||
|
clear_screen();
|
||||||
|
cout << "[Help]\n";
|
||||||
|
cout << "Controls:\n";
|
||||||
|
cout << " Arrows : move cursor\n";
|
||||||
|
cout << " Enter : play at cursor (if legal)\n";
|
||||||
|
cout << " S : save (creates a new ID 1..1000)\n";
|
||||||
|
cout << " L : load by ID (during game)\n";
|
||||||
|
cout << " Q : quit current game to menu\n\n";
|
||||||
|
cout << "Rules: place a piece to flip opponent pieces in any direction.\n";
|
||||||
|
press_any_key();
|
||||||
|
}
|
||||||
|
|
||||||
|
void screen_history() {
|
||||||
|
clear_screen();
|
||||||
|
|
||||||
|
ifstream in(HISTORY_FILE);
|
||||||
|
if (!in) {
|
||||||
|
cout << "No history yet.\n";
|
||||||
|
press_any_key();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int n = 0;
|
||||||
|
string line;
|
||||||
|
while (getline(in, line)) n++;
|
||||||
|
|
||||||
|
if (n == 0) {
|
||||||
|
cout << "No history yet.\n";
|
||||||
|
press_any_key();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
in.clear();
|
||||||
|
in.seekg(0);
|
||||||
|
|
||||||
|
string* lines = new string[n];
|
||||||
|
int i = 0;
|
||||||
|
while (i < n && getline(in, line)) {
|
||||||
|
lines[i++] = trim_newline(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
cout << "=== GAME HISTORY (newest first) ===\n\n";
|
||||||
|
|
||||||
|
// Print from last to first
|
||||||
|
for (int k = n - 1; k >= 0; k--) {
|
||||||
|
long long epoch;
|
||||||
|
int b, w;
|
||||||
|
string result;
|
||||||
|
|
||||||
|
if (!parse_history_line(lines[k], epoch, b, w, result)) continue;
|
||||||
|
|
||||||
|
cout << epoch << " | " << "Black: " << b << " White: " << w << " | ";
|
||||||
|
|
||||||
|
if (result == "DRAW") cout << "Draw";
|
||||||
|
else if (result == "B") cout << "Winner: Black";
|
||||||
|
else if (result == "W") cout << "Winner: White";
|
||||||
|
else cout << "Result: " << result;
|
||||||
|
|
||||||
|
cout << "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
delete[] lines;
|
||||||
|
press_any_key();
|
||||||
|
}
|
||||||
|
|
||||||
|
int main () {
|
||||||
|
#ifdef _WIN32
|
||||||
|
SetConsoleOutputCP(CP_UTF8);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
srand((unsigned)time(nullptr)); //helps generate random numbers
|
||||||
|
while (true) {
|
||||||
|
int ch = show_menu_and_get_choice();
|
||||||
|
switch (ch) {
|
||||||
|
case 1: screen_new_game(); break;
|
||||||
|
case 2: screen_load_game(); break;
|
||||||
|
case 3: screen_help(); break;
|
||||||
|
case 4: screen_history(); break;
|
||||||
|
case 5: return 0;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user