#include #include #include #include #include #ifdef _WIN32 #include #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(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('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; }