/* ============================================================ OTHELLO – Console Game (C++) ============================================================ Author: Amirmohammad Tayefe Course: Basic Programming Language: C++ Platform: Windows Console ------------------------------------------------------------ DESCRIPTION ------------------------------------------------------------ This project is a complete console-based implementation of the Othello board game. Features: - Single-player (Human vs Bot) - Two-player (Human vs Human) - Multiple bot difficulty levels - Cursor-based menu navigation (arrow keys / WASD) - Non-flickering ANSI-rendered UI - Game save & load support - Game history with pagination - Persistent player statistics with ranking - Contextual in-game help - Input validation and error handling - Colored and styled text using ANSI escape codes ------------------------------------------------------------ DESIGN NOTES ------------------------------------------------------------ - Rendering is done using ANSI escape sequences. - Cursor-based menus avoid numeric input. - Player statistics are stored persistently in text files. - Bots are excluded from statistics tracking. - Screen flickering is avoided by cursor repositioning. ============================================================ */ // ANSI escape codes for text styling and colors // Used to enhance visual clarity in the console UI #define RESET "\033[0m" #define BLACK_P "\033[30;1m" #define WHITE_P "\033[37;1m" #define BOARD_C "\033[36m" #define HINT_C "\033[33m" #define CURSOR_BG "\033[47m" #define GOLD_C "\033[38;5;220m" #define SILVER_C "\033[38;5;250m" #define BRONZE_C "\033[38;5;172m" // Includes and namespaces #include #include #include #include #include #include #include #include using namespace std; /* ========= STRUCTS ========= */ struct Player { char name[32]; bool isBot; }; struct GameConfig { int boardSize; // 4 to 10, even only bool hintsEnabled; }; struct Game { int size; char board[10][10]; Player players[2]; int currentPlayer; bool hintsEnabled; int botLevel; }; struct PlayerStats { char name[30]; int totalScore; int wins; int losses; int draws; int games; }; // used for MiniMax algorithm int positionWeight[10][10] = {{100, -20, 10, 5, 5, 10, -20, 100}, {-20, -50, -2, -2, -2, -2, -50, -20}, {10, -2, 5, 1, 1, 5, -2, 10}, {5, -2, 1, 1, 1, 1, -2, 5}, {5, -2, 1, 1, 1, 1, -2, 5}, {10, -2, 5, 1, 1, 5, -2, 10}, {-20, -50, -2, -2, -2, -2, -50, -20}, {100, -20, 10, 5, 5, 10, -20, 100}}; const int MAX_PLAYERS = 100; PlayerStats players[MAX_PLAYERS]; int playerCount = 0; // Functions Declarations void clearScreen(); void waitForKey(); void seedRandom(); void initializeGame(Game &game, const GameConfig &config, const Player &p1, const Player &p2); bool isOnBoard(int row, int col, int size); char opponentDisk(char disk); bool isValidMove(const Game &game, int row, int col, char playerDisk); bool hasAnyValidMove(const Game &game, char playerDisk); void applyMove(Game &game, int row, int col, char playerDisk); int countDisks(const Game &game, char playerDisk); void showHints(Game &game, char playerDisk); void gameLoop(Game &game); void newGame(); void gameHistory(); void saveCurrentGame(const Game &game); bool loadSavedGame(Game &game); bool askYesNo(const char *message); void loadGame(); void moveCursor(int &row, int &col, int dRow, int dCol, int size); void drawBoard(const Game &game, int cursorRow, int cursorCol); void clearLinesBelow(int startRow, int n); void showHelp(); void saveGameHistory(const Game &game, int blackScore, int whiteScore); void drawHelpPage(int page); int chooseBotDifficulty(); void showMenu(); void drawTitle(); void showMenuOptions(); int findPlayer(const char *name); void saveStatistics(); void loadStatistics(); void sortPlayers(); void showStatistics(); void updateStatistics(const Game &game, int blackScore, int whiteScore); void printCentered(const char *text, int width); void printCenteredInt(int value, int width); void printCenteredFloat(float value, int width); /* ========= Utility Functions ========= */ // Clears the console screen. // NOTE: Uses Windows-specific command. void clearScreen() { system("cls"); } // Waits until the user presses a key. // Used to pause screens such as Help or Statistics. void waitForKey() { getch(); } // Seeds the random number generator. // Must be called once at program startup. void seedRandom() { srand(static_cast(time(0))); } /* ========= GAME LOGIC ========= */ // Initializes a new game with the given configuration and players. // Sets up the board, initial pieces, and game state. void initializeGame(Game &game, const GameConfig &config, const Player &p1, const Player &p2) { game.size = config.boardSize; game.players[0] = p1; game.players[1] = p2; game.currentPlayer = 0; game.hintsEnabled = config.hintsEnabled; // Empty board for (int r = 0; r < game.size; r++) for (int c = 0; c < game.size; c++) game.board[r][c] = ' '; // Initial placement int mid = game.size / 2; game.board[mid - 1][mid - 1] = 'W'; game.board[mid][mid] = 'W'; game.board[mid - 1][mid] = 'B'; game.board[mid][mid - 1] = 'B'; } // Checks if the given (row, col) is within the board boundaries. bool isOnBoard(int row, int col, int size) { return row >= 0 && row < size && col >= 0 && col < size; } // Returns the opponent's disk character. char opponentDisk(char disk) { return (disk == 'B') ? 'W' : 'B'; } // Checks whether a move at (row, col) is valid for the given player. // A move is valid if it flips at least one opponent disk. bool isValidMove(const Game &game, int row, int col, char playerDisk) { if (!isOnBoard(row, col, game.size) || game.board[row][col] != ' ') return false; char opp = opponentDisk(playerDisk); int dx[] = {-1, -1, 0, 1, 1, 1, 0, -1}; int dy[] = {0, 1, 1, 1, 0, -1, -1, -1}; for (int dir = 0; dir < 8; dir++) { int r = row + dx[dir], c = col + dy[dir]; bool foundOpp = false; while (isOnBoard(r, c, game.size) && game.board[r][c] == opp) { r += dx[dir]; c += dy[dir]; foundOpp = true; } if (foundOpp && isOnBoard(r, c, game.size) && game.board[r][c] == playerDisk) return true; } return false; } // Checks if the specified player has any valid moves remaining. bool hasAnyValidMove(const Game &game, char playerDisk) { for (int r = 0; r < game.size; r++) for (int c = 0; c < game.size; c++) if (isValidMove(game, r, c, playerDisk)) return true; return false; } // Applies a valid move to the board. // Places the disk and flips all captured opponent disks. void applyMove(Game &game, int row, int col, char playerDisk) { game.board[row][col] = playerDisk; char opp = opponentDisk(playerDisk); // Check all 8 directions for flips // dx represents row change, dy represents column change int dx[] = {-1, -1, 0, 1, 1, 1, 0, -1}; int dy[] = {0, 1, 1, 1, 0, -1, -1, -1}; for (int dir = 0; dir < 8; dir++) { int r = row + dx[dir], c = col + dy[dir]; bool foundOpp = false; int flipR[10], flipC[10], count = 0; while (isOnBoard(r, c, game.size) && game.board[r][c] == opp) { flipR[count] = r; flipC[count] = c; count++; r += dx[dir]; c += dy[dir]; foundOpp = true; } if (foundOpp && isOnBoard(r, c, game.size) && game.board[r][c] == playerDisk) { for (int i = 0; i < count; i++) game.board[flipR[i]][flipC[i]] = playerDisk; } } } // Counts the number of disks on the board for the specified player. int countDisks(const Game &game, char playerDisk) { int cnt = 0; for (int r = 0; r < game.size; r++) for (int c = 0; c < game.size; c++) if (game.board[r][c] == playerDisk) cnt++; return cnt; } // Executes a bot move based on the selected difficulty level. // Easy: random // Normal: greedy heuristic // Hard: limited look-ahead evaluation using MiniMax principles void botMove(Game &game, int &row, int &col) { char botDisk = (game.currentPlayer == 0) ? 'B' : 'W'; char oppDisk = opponentDisk(botDisk); int bestScore = -100000; bool found = false; for (int r = 0; r < game.size; r++) { for (int c = 0; c < game.size; c++) { if (!isValidMove(game, r, c, botDisk)) continue; found = true; // easy: random movement if (game.botLevel == 1) { if (rand() % 2 == 0) { row = r; col = c; return; } } Game temp = game; applyMove(temp, r, c, botDisk); int score = 0; // normal: greedy algorithm and position analysis if (game.botLevel == 2) { score = countDisks(temp, botDisk) - countDisks(temp, oppDisk); score += positionWeight[r][c]; } // hard: minimax algorithm (simple version) else if (game.botLevel == 3) { score = positionWeight[r][c] * 2; for (int rr = 0; rr < game.size; rr++) for (int cc = 0; cc < game.size; cc++) if (isValidMove(temp, rr, cc, oppDisk)) score -= positionWeight[rr][cc]; } if (score > bestScore) { bestScore = score; row = r; col = c; } } } // fallback safety // should not happen if there are valid moves if (!found) { row = 0; col = 0; } } // Main game execution loop. // Handles turns, input, rendering, skipping, and end-game detection. void gameLoop(Game &game) { cout << "\033[?25l"; // hide terminal cursor int msgRow = game.size * 2 + 4; // first line below the board while (true) { char playerDisk = (game.currentPlayer == 0) ? 'B' : 'W'; Player &player = game.players[game.currentPlayer]; if (!hasAnyValidMove(game, playerDisk)) { drawBoard(game, -1, -1); clearLinesBelow(msgRow, 2); cout << "\033[" << msgRow << ";0H"; cout << player.name << " has no valid moves. Turn skipped.\n"; game.currentPlayer = 1 - game.currentPlayer; if (!hasAnyValidMove(game, opponentDisk(playerDisk))) break; this_thread::sleep_for(chrono::seconds(2)); continue; } int row, col; if (player.isBot) { drawBoard(game, -1, -1); clearLinesBelow(msgRow, 2); cout << "\033[" << msgRow << ";0H" << "Robot is thinking...\n"; cout.flush(); // Simulate thinking time this_thread::sleep_for(chrono::seconds(1)); botMove(game, row, col); // Robot move message display clearLinesBelow(msgRow, 2); cout << "\033[" << msgRow << ";0H" << "Robot moved at: " << row << ", " << col << "\n"; cout.flush(); // Pause before applying move for better UX this_thread::sleep_for(chrono::seconds(1)); } else { int cursorRow = 0, cursorCol = 0; while (true) { drawBoard(game, cursorRow, cursorCol); clearLinesBelow(msgRow, 3); // clear previous player prompts cout << "\033[" << msgRow << ";0H"; cout << player.name << "'s turn: " << ((playerDisk == 'B') ? "Black ●" : "White ○") << "\n"; int ch = getch(); // Arrow keys if (ch == 0 || ch == 224) { int arrow = getch(); switch (arrow) { case 72: moveCursor(cursorRow, cursorCol, -1, 0, game.size); break; case 80: moveCursor(cursorRow, cursorCol, 1, 0, game.size); break; case 75: moveCursor(cursorRow, cursorCol, 0, -1, game.size); break; case 77: moveCursor(cursorRow, cursorCol, 0, 1, game.size); break; } } // WASD keys and others else if (ch == 'w' || ch == 'W') moveCursor(cursorRow, cursorCol, -1, 0, game.size); else if (ch == 's' || ch == 'S') moveCursor(cursorRow, cursorCol, 1, 0, game.size); else if (ch == 'a' || ch == 'A') moveCursor(cursorRow, cursorCol, 0, -1, game.size); else if (ch == 'd' || ch == 'D') moveCursor(cursorRow, cursorCol, 0, 1, game.size); else if (ch == 13 && isValidMove(game, cursorRow, cursorCol, playerDisk)) { row = cursorRow; col = cursorCol; break; } else if (ch == 'q' || ch == 'Q') { bool quitGame = askYesNo("Do you wanna quit the game?"); if (!quitGame) continue; bool saveGame = askYesNo("Do you wanna save the game?"); if (saveGame) saveCurrentGame(game); return; } } } applyMove(game, row, col, playerDisk); game.currentPlayer = 1 - game.currentPlayer; int total = countDisks(game, 'B') + countDisks(game, 'W'); if (total == game.size * game.size) break; } drawBoard(game, -1, -1); clearLinesBelow(msgRow, 3); int blackCount = countDisks(game, 'B'); int whiteCount = countDisks(game, 'W'); cout << "\033[" << msgRow << ";0H"; cout << "Game Over!\n"; cout << "Black ●: " << blackCount << " | White ○: " << whiteCount << "\n"; if (blackCount > whiteCount) cout << game.players[0].name << " wins!\n"; else if (whiteCount > blackCount) cout << game.players[1].name << " wins!\n"; else cout << "It's a draw!\n"; saveGameHistory(game, blackCount, whiteCount); updateStatistics(game, blackCount, whiteCount); cout.flush(); cout << "Press any key to return to menu..." << endl; waitForKey(); } void newGame() { bool inNewGameMenu = true; Player p1, p2; GameConfig config; Game game; while (inNewGameMenu) { clearScreen(); cout << "*************************" << endl; cout << "* NEW GAME *" << endl; cout << "*************************" << endl; cout << "1. Single Player" << endl; cout << "2. Two Player" << endl; cout << "3. Back" << endl; char choice = getch(); switch (choice) { case '1': clearScreen(); cout << "[Single Player]\n\n"; cout << "Enter your name: "; cin >> p1.name; p1.isBot = false; game.botLevel = chooseBotDifficulty(); strcpy(p2.name, "BOT"); p2.isBot = true; inNewGameMenu = false; break; case '2': clearScreen(); cout << "[Two Player]\n\n"; cout << "Enter Player 1 name: "; cin >> p1.name; cout << endl; cout << "Enter Player 2 name: "; cin >> p2.name; p1.isBot = false; p2.isBot = false; inNewGameMenu = false; break; case '3': return; default: continue; } } while (true) { clearScreen(); cout << "Enter board size (even numbers from 4 to 10): "; cin >> config.boardSize; if (config.boardSize >= 4 && config.boardSize <= 10 && config.boardSize % 2 == 0) break; cout << "Invalid board size.\n"; waitForKey(); } while (true) { clearScreen(); cout << "Do you want to Enable the move hints? (Y/N)" << endl; char h = getch(); if (h == 'y' || h == 'Y') { config.hintsEnabled = true; break; } if (h == 'n' || h == 'N') { config.hintsEnabled = false; break; } } clearScreen(); initializeGame(game, config, p1, p2); drawBoard(game, -1, -1); gameLoop(game); } // Saves a completed game data to the history file. void saveGameHistory(const Game &game, int blackScore, int whiteScore) { ofstream file("game_history.txt", ios::app); // append mode if (!file) return; time_t now = time(0); char *dt = ctime(&now); // get current date/time as string dt[strlen(dt) - 1] = '\0'; // remove trailing newline char winner[32]; if (blackScore > whiteScore) strcpy(winner, game.players[0].name); else if (whiteScore > blackScore) strcpy(winner, game.players[1].name); else strcpy(winner, "Draw"); file << "Date: " << dt << "\n"; file << "Player 1: " << game.players[0].name << " | Score: " << blackScore << "\n"; file << "Player 2: " << game.players[1].name << " | Score: " << whiteScore << "\n"; file << "Winner: " << winner << "\n"; file << "------------------------\n"; file.close(); } // Displays paginated game history records. void gameHistory() { clearScreen(); ifstream file("game_history.txt"); if (!file) { cout << "No game history found.\n"; waitForKey(); return; } const int MAX_ENTRIES = 1000; const int MAX_LINES = 5; const int RECORDS_PER_PAGE = 5; string history[MAX_ENTRIES][MAX_LINES]; int entryCount = 0; int lineCount = 0; string line; // Read file into records while (getline(file, line)) { history[entryCount][lineCount++] = line; if (line == "------------------------") { entryCount++; lineCount = 0; if (entryCount >= MAX_ENTRIES) break; } } file.close(); if (entryCount == 0) { cout << "No game history available.\n"; waitForKey(); return; } int totalPages = (entryCount + RECORDS_PER_PAGE - 1) / RECORDS_PER_PAGE; int page = 0; while (true) { clearScreen(); cout << "===== GAME HISTORY =====\n"; cout << "Total Records: " << entryCount << "\n"; cout << "Page " << (page + 1) << " / " << totalPages << "\n\n"; int start = page * RECORDS_PER_PAGE; int end = start + RECORDS_PER_PAGE; if (end > entryCount) end = entryCount; // Display records (latest first) for (int i = start; i < end; i++) { int recordIndex = entryCount - 1 - i; for (int j = 0; j < MAX_LINES; j++) { if (!history[recordIndex][j].empty()) cout << history[recordIndex][j] << "\n"; } cout << "\n"; } cout << "---------------------------------\n"; cout << "← Prev | → Next | Q Back\n"; int ch = getch(); if (ch == 0 || ch == 224) { int arrow = getch(); if (arrow == 75 && page > 0) page--; // Left else if (arrow == 77 && page < totalPages - 1) page++; // Right } else if (ch == 'q' || ch == 'Q') return; } } // Saves the current game state to a file. // In case of saving more than one game, only the last played game will be saved void saveCurrentGame(const Game &game) { ofstream file("saved_game.txt", ios::out); if (!file) return; file << game.size << "\n"; file << game.hintsEnabled << "\n"; file << game.currentPlayer << "\n"; // Save player info for (int i = 0; i < 2; i++) { file << game.players[i].name << "\n"; file << game.players[i].isBot << "\n"; } // Save board for (int r = 0; r < game.size; r++) { for (int c = 0; c < game.size; c++) file << game.board[r][c]; file << "\n"; } file.close(); } // Loads a saved game from file. bool loadSavedGame(Game &game) { ifstream file("saved_game.txt"); if (!file) return false; // Read basic config file >> game.size; file >> game.hintsEnabled; file >> game.currentPlayer; file.ignore(1024, '\n'); // flush rest of line // Validate board size if (game.size < 4 || game.size > 10 || game.size % 2 != 0) return false; // Validate current player if (game.currentPlayer < 0 || game.currentPlayer > 1) game.currentPlayer = 0; for (int i = 0; i < 2; i++) { file.getline(game.players[i].name, 32); int botFlag = 0; file >> botFlag; game.players[i].isBot = (botFlag != 0); file.ignore(1024, '\n'); } for (int r = 0; r < game.size; r++) { char line[32]; file.getline(line, 32); for (int c = 0; c < game.size; c++) { char ch = line[c]; if (ch == 'B' || ch == 'W' || ch == ' ') game.board[r][c] = ch; else game.board[r][c] = ' '; } } file.close(); return true; } // Used for Yes/No dialogs. bool askYesNo(const char *message) { char choice; while (true) { cout << message << " (Y/N): "; cin >> choice; if (choice == 'Y' || choice == 'y') return true; if (choice == 'N' || choice == 'n') return false; cout << "Invalid input. Please enter Y or N." << endl; } } // Loads a previously saved game from file. // Validates data before restoring state. void loadGame() { Game game; if (!loadSavedGame(game)) { clearScreen(); cout << "No saved game found or save file is corrupted." << endl; waitForKey(); return; } clearScreen(); drawBoard(game, -1, -1); int msgRow = game.size * 2 + 4; clearLinesBelow(msgRow, 2); cout << "\033[" << msgRow << ";0H"; cout << "Saved game loaded successfully." << endl; cout << "Press any key to resume the game..."; cout.flush(); waitForKey(); gameLoop(game); } // Moves the cursor within the board boundaries. void moveCursor(int &row, int &col, int dRow, int dCol, int size) { row += dRow; col += dCol; if (row < 0) row = 0; if (row >= size) row = size - 1; if (col < 0) col = 0; if (col >= size) col = size - 1; } // Used to set bot difficulty level. int chooseBotDifficulty() { clearScreen(); cout << "Choose difficulty level:" << endl; cout << "1. Easy" << endl; cout << "2. Normal" << endl; cout << "3. Hard" << endl; while (true) { char ch = getch(); if (ch >= '1' && ch <= '3') return ch - '0'; } } /* ========= Graphical Functions ========= */ // Displays the main menu and handles user navigation. void showMenu() { while (true) { clearScreen(); showMenuOptions(); } } // Draws the game title in 3D style with colors. void drawTitle() { // ANSI color codes const char *BLUE = "\033[1;34m"; // elegant blue // 3D title const char *lines[] = { " ██████╗ ████████╗██╗ ██╗███████╗██╗ ██╗ ██████╗ ", "██╔═══██╗╚══██╔══╝██║ ██║██╔════╝██║ ██║ ██╔═══██╗", "██║ ██║ ██║ ███████║█████╗ ██║ ██║ ██║ ██║", "██║ ██║ ██║ ██╔══██║██╔══╝ ██║ ██║ ██║ ██║", "╚██████╔╝ ██║ ██║ ██║███████╗███████╗███████╗╚██████╔╝", " ╚═════╝ ╚═╝ ╚═╝ ╚═╝╚══════╝╚══════╝╚══════╝ ╚═════╝ "}; // Print each line with color for (int i = 0; i < 6; i++) cout << BLUE << lines[i] << RESET << endl; cout << endl; } // Displays the interactive menu options with cursor navigation. void showMenuOptions() { cout << "\033[?25l"; // hide terminal cursor const char *SELECT_BG = "\033[47m"; // white background const char *SELECT_FG = "\033[30;1m"; // black bold text const char *NORMAL = "\033[1;34m"; // elegant blue const char *items[] = { "New Game", "Load Game", "Help", "Game History", "Statistics", "Exit"}; const int itemCount = 6; const int menuRow = 12; const int menuCol = 3; int selected = 0; clearScreen(); drawTitle(); // Initial draw for (int i = 0; i < itemCount; i++) { cout << "\033[" << (menuRow + i) << ";0H"; cout << "\033[2K"; // clear line cout << "\033[" << (menuRow + i) << ";" << menuCol << "H"; if (i == selected) cout << SELECT_BG << SELECT_FG; else cout << NORMAL; cout << " " << items[i] << " " << RESET; } while (true) { int ch = getch(); int prev = selected; if (ch == 0 || ch == 224) { int arrow = getch(); if (arrow == 72) selected--; else if (arrow == 80) selected++; } else if (ch == 'w' || ch == 'W') selected--; else if (ch == 's' || ch == 'S') selected++; else if (ch == 13) { clearScreen(); switch (selected) { case 0: newGame(); break; case 1: loadGame(); break; case 2: showHelp(); break; case 3: gameHistory(); break; case 4: showStatistics(); break; case 5: exit(0); } clearScreen(); drawTitle(); selected = 0; prev = -1; // force redraw to update menu for (int i = 0; i < itemCount; i++) { cout << "\033[" << (menuRow + i) << ";0H"; cout << "\033[2K"; cout << "\033[" << (menuRow + i) << ";" << menuCol << "H"; if (i == selected) cout << SELECT_BG << SELECT_FG; else cout << NORMAL; cout << " " << items[i] << " " << RESET; } continue; } if (selected < 0) selected = itemCount - 1; if (selected >= itemCount) selected = 0; // redraw when moving cursor if (prev != selected) { cout << "\033[" << (menuRow + prev) << ";0H"; cout << "\033[2K"; cout << "\033[" << (menuRow + prev) << ";" << menuCol << "H"; cout << NORMAL << " " << items[prev] << " " << RESET; cout << "\033[" << (menuRow + selected) << ";0H"; cout << "\033[2K"; cout << "\033[" << (menuRow + selected) << ";" << menuCol << "H"; cout << SELECT_BG << SELECT_FG << " " << items[selected] << " " << RESET; } } } // Displays the game board with hints for valid moves using the asterisk symbol (*). void showHints(Game &game, char playerDisk) { clearScreen(); cout << BOARD_C << " "; for (int c = 0; c < game.size; c++) cout << c << " "; cout << RESET << "\n"; cout << BOARD_C << " ┌"; for (int c = 0; c < game.size; c++) { cout << "───"; if (c < game.size - 1) cout << "┬"; } cout << "┐\n" << RESET; for (int r = 0; r < game.size; r++) { cout << BOARD_C << r << " │" << RESET; for (int c = 0; c < game.size; c++) { cout << " "; if (game.board[r][c] == 'B') cout << BLACK_P << "●" << RESET; else if (game.board[r][c] == 'W') cout << WHITE_P << "○" << RESET; else if (game.hintsEnabled && isValidMove(game, r, c, playerDisk)) cout << HINT_C << "*" << RESET; else cout << " "; cout << " "; if (c < game.size - 1) cout << BOARD_C << "│" << RESET; } cout << BOARD_C << "│\n" << RESET; if (r < game.size - 1) { cout << BOARD_C << " ├"; for (int c = 0; c < game.size; c++) { cout << "───"; if (c < game.size - 1) cout << "┼"; } cout << "┤\n" << RESET; } } cout << BOARD_C << " └"; for (int c = 0; c < game.size; c++) { cout << "───"; if (c < game.size - 1) cout << "┴"; } cout << "┘\n" << RESET; } // Draws the current game board with ANSI colors. // Highlights cursor position and valid moves if enabled. void drawBoard(const Game &game, int cursorRow, int cursorCol) { char playerDisk = (game.currentPlayer == 0) ? 'B' : 'W'; // Move to top-left before redrawing cout << "\033[H"; // Column numbers cout << BOARD_C << " "; for (int c = 0; c < game.size; c++) cout << c << " "; cout << RESET << "\n"; // Top border cout << BOARD_C << " ┌"; for (int c = 0; c < game.size; c++) { cout << "───"; if (c < game.size - 1) cout << "┬"; } cout << "┐\n" << RESET; // Board rows for (int r = 0; r < game.size; r++) { cout << BOARD_C << r << " │" << RESET; for (int c = 0; c < game.size; c++) { bool isCursor = (r == cursorRow && c == cursorCol); if (isCursor) cout << CURSOR_BG; cout << " "; if (game.board[r][c] == 'B') cout << BLACK_P << "●" << RESET; else if (game.board[r][c] == 'W') cout << WHITE_P << "○" << RESET; else if (game.hintsEnabled && isValidMove(game, r, c, playerDisk)) cout << HINT_C << "*" << RESET; else cout << " "; if (isCursor) cout << RESET; cout << " "; if (c < game.size - 1) cout << BOARD_C << "│" << RESET; } cout << BOARD_C << "│\n" << RESET; if (r < game.size - 1) { cout << BOARD_C << " ├"; for (int c = 0; c < game.size; c++) { cout << "───"; if (c < game.size - 1) cout << "┼"; } cout << "┤\n" << RESET; } } // Bottom border cout << BOARD_C << " └"; for (int c = 0; c < game.size; c++) { cout << "───"; if (c < game.size - 1) cout << "┴"; } cout << "┘\n" << RESET; cout.flush(); } // Clears n lines starting from the given row void clearLinesBelow(int startRow, int n) { for (int i = 0; i < n; i++) { cout << "\033[" << (startRow + i) << ";0H"; // move to line cout << "\033[2K"; // clear entire line } cout.flush(); } // Displays the help section with multiple pages and navigation. void showHelp() { int page = 0; const int maxPages = 5; while (true) { clearScreen(); drawHelpPage(page); int ch = getch(); if (ch == 0 || ch == 224) { int arrow = getch(); if (arrow == 75 && page > 0) page--; // Left else if (arrow == 77 && page < maxPages - 1) page++; // Right } else if (ch == 'q' || ch == 'Q' || ch == 27) break; } } // Draws a specific help page based on the page number. void drawHelpPage(int page) { cout << "========== OTHELLO HELP ==========\n\n"; switch (page) { case 0: cout << "WHAT IS OTHELLO?\n\n"; cout << "Othello is a two-player strategy board game.\n"; cout << "Players take turns placing disks on the board.\n"; cout << "Any opponent disks trapped between yours flip.\n"; cout << "\nYour Goal: finish the game with more disks than your opponent.\n"; break; case 1: cout << "GAME RULES\n\n"; cout << "- A move is legal only if it flips at least one disk.\n"; cout << "- Disks flip in straight lines (horizontal, vertical, diagonal).\n"; cout << "- If a player has no legal move, the turn is skipped.\n"; cout << "- Game ends when no player can move.\n"; break; case 2: cout << "GAME MODES & FEATURES\n\n"; cout << "- Single Player: play against the bot (in different difficulties).\n"; cout << "- Two Player: human vs human.\n"; cout << "- Board sizes: 4x4 to 10x10 (even numbers only).\n"; cout << "- Hints: highlight legal moves by asterisk (*).\n"; cout << "- Save & Load supported.\n"; break; case 3: cout << "CONTROLS\n\n"; cout << "Move Cursor:\n"; cout << " W / A / S / D or Arrow Keys\n\n"; cout << "Place Disk: ENTER\n"; cout << "Quit Game: Q\n"; break; case 4: cout << "VISUAL GUIDE & TIPS\n\n"; cout << "● Black Disk ○ White Disk\n"; cout << "Highlighted white cell = cursor position\n\n"; cout << "Tips:\n"; cout << "- Corners are powerful.\n"; cout << "- Edges are safer than center.\n"; cout << "- Mobility matters more than disk count early on.\n"; break; } cout << "\n---------------------------------\n"; cout << " ← Prev | → Next | q Exit\n"; } // ========= Statistics Functions ========= // Reads player statistics from file into memory. void loadStatistics() { ifstream in("stats.txt"); playerCount = 0; if (!in) return; while (playerCount < MAX_PLAYERS && in >> players[playerCount].name >> players[playerCount].totalScore >> players[playerCount].wins >> players[playerCount].losses >> players[playerCount].draws >> players[playerCount].games) { playerCount++; } in.close(); } // Saves player statistics from memory to file. void saveStatistics() { ofstream out("stats.txt"); for (int i = 0; i < playerCount; i++) { out << players[i].name << " " << players[i].totalScore << " " << players[i].wins << " " << players[i].losses << " " << players[i].draws << " " << players[i].games << "\n"; } out.close(); } // Finds a player by name. If not found, creates a new entry in the file. int findPlayer(const char *name) { for (int i = 0; i < playerCount; i++) { if (strcmp(players[i].name, name) == 0) return i; } if (playerCount < MAX_PLAYERS) { strcpy(players[playerCount].name, name); players[playerCount].totalScore = 0; players[playerCount].wins = 0; players[playerCount].losses = 0; players[playerCount].draws = 0; players[playerCount].games = 0; return playerCount++; } return -1; } // Sorts players by ranking in the order: (score, wins, losses). void sortPlayers() { for (int i = 0; i < playerCount - 1; i++) { for (int j = i + 1; j < playerCount; j++) { if ( players[j].totalScore > players[i].totalScore || (players[j].totalScore == players[i].totalScore && players[j].wins > players[i].wins) || (players[j].totalScore == players[i].totalScore && players[j].wins == players[i].wins && players[j].losses < players[i].losses)) { PlayerStats temp = players[i]; players[i] = players[j]; players[j] = temp; } } } } // Displays the statistics table. // Top 3 players are highlighted with colors for better visibility. void showStatistics() { system("cls"); sortPlayers(); cout << "RANK NAME SCORE W L D GAMES WIN Rate\n"; cout << "--------------------------------------------------------------------\n"; for (int i = 0; i < playerCount; i++) { float winRate = 0.0f; if (players[i].games > 0) winRate = (players[i].wins * 100.0f) / players[i].games; const char *rowColor = RESET; if (i == 0) rowColor = GOLD_C; else if (i == 1) rowColor = SILVER_C; else if (i == 2) rowColor = BRONZE_C; cout << rowColor; printCenteredInt(i + 1, 6); printCentered(players[i].name, 16); printCenteredInt(players[i].totalScore, 10); printCenteredInt(players[i].wins, 6); printCenteredInt(players[i].losses, 6); printCenteredInt(players[i].draws, 6); printCenteredInt(players[i].games, 10); printCenteredFloat(winRate, 8); cout << RESET << '\n'; } cout << "\nPress any key to return..."; waitForKey(); } // Updates persistent player statistics after a completed game. // Bot players are excluded from statistics tracking. void updateStatistics(const Game &game, int blackScore, int whiteScore) { // Player 0 → Black, Player 1 → White for (int i = 0; i < 2; i++) { // Skips bots if (game.players[i].isBot) continue; int idx = findPlayer(game.players[i].name); if (idx == -1) continue; players[idx].games++; int myScore = (i == 0) ? blackScore : whiteScore; int oppScore = (i == 0) ? whiteScore : blackScore; players[idx].totalScore += myScore; if (myScore > oppScore) players[idx].wins++; else if (myScore < oppScore) players[idx].losses++; else players[idx].draws++; } saveStatistics(); } // These three are utility functions to print centered text within a given width (used in statistics table). void printCentered(const char *text, int width) { int len = strlen(text); if (len >= width) { cout << text; return; } int left = (width - len) / 2; int right = width - len - left; for (int i = 0; i < left; i++) cout << ' '; cout << text; for (int i = 0; i < right; i++) cout << ' '; } void printCenteredInt(int value, int width) { char buf[20]; sprintf(buf, "%d", value); printCentered(buf, width); } void printCenteredFloat(float value, int width) { char buf[20]; sprintf(buf, "%.1f%%", value); printCentered(buf, width); } // Program entry point int main() { seedRandom(); loadStatistics(); showMenu(); }