commit 1837bd4f07b01c9e72e864d48412bed8c0cec539 Author: Saeed Jamali Date: Mon Apr 13 17:56:29 2026 +0330 add main.cpp diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..d5f910f --- /dev/null +++ b/main.cpp @@ -0,0 +1,912 @@ +#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; +} \ No newline at end of file