commit 198d857d7c0e9b0f164c7474149f0b31909904fb Author: amirM.t Date: Mon Apr 13 20:05:04 2026 +0330 Initial commit on develop branch diff --git a/game_history.txt b/game_history.txt new file mode 100644 index 0000000..0e0cc95 --- /dev/null +++ b/game_history.txt @@ -0,0 +1,185 @@ +Date: Tue Dec 30 21:48:12 2025 +Player 1: amir | Score: 11 +Player 2: BOT | Score: 1 +Winner: amir +------------------------ +Date: Wed Dec 31 18:18:01 2025 +Player 1: amir | Score: 6 +Player 2: BOT | Score: 10 +Winner: BOT +------------------------ +Date: Wed Dec 31 19:30:39 2025 +Player 1: lucas | Score: 6 +Player 2: dustin | Score: 10 +Winner: dustin +------------------------ +Date: Wed Dec 31 19:39:55 2025 +Player 1: amir | Score: 5 +Player 2: BOT | Score: 11 +Winner: BOT +------------------------ +Date: Wed Dec 31 19:54:14 2025 +Player 1: max | Score: 12 +Player 2: holly | Score: 4 +Winner: max +------------------------ +Date: Wed Dec 31 19:55:06 2025 +Player 1: vecna | Score: 6 +Player 2: will | Score: 10 +Winner: will +------------------------ +Date: Wed Dec 31 20:03:41 2025 +Player 1: amir | Score: 6 +Player 2: BOT | Score: 10 +Winner: BOT +------------------------ +Date: Wed Dec 31 20:06:02 2025 +Player 1: amir | Score: 9 +Player 2: BOT | Score: 7 +Winner: amir +------------------------ +Date: Wed Dec 31 20:07:12 2025 +Player 1: amir | Score: 8 +Player 2: BOT | Score: 8 +Winner: Draw +------------------------ +Date: Wed Dec 31 20:08:29 2025 +Player 1: amir | Score: 13 +Player 2: BOT | Score: 3 +Winner: amir +------------------------ +Date: Wed Dec 31 20:11:09 2025 +Player 1: amir | Score: 8 +Player 2: BOT | Score: 8 +Winner: Draw +------------------------ +Date: Wed Dec 31 20:13:04 2025 +Player 1: amir | Score: 11 +Player 2: BOT | Score: 5 +Winner: amir +------------------------ +Date: Wed Dec 31 20:13:57 2025 +Player 1: amir | Score: 12 +Player 2: BOT | Score: 4 +Winner: amir +------------------------ +Date: Wed Dec 31 20:16:57 2025 +Player 1: vecna | Score: 18 +Player 2: BOT | Score: 18 +Winner: Draw +------------------------ +Date: Wed Dec 31 20:17:48 2025 +Player 1: amir | Score: 6 +Player 2: BOT | Score: 10 +Winner: BOT +------------------------ +Date: Wed Dec 31 20:25:18 2025 +Player 1: amir | Score: 3 +Player 2: BOT | Score: 13 +Winner: BOT +------------------------ +Date: Wed Dec 31 20:26:34 2025 +Player 1: 2pac | Score: 6 +Player 2: eminem | Score: 10 +Winner: eminem +------------------------ +Date: Wed Dec 31 22:19:49 2025 +Player 1: amir | Score: 2 +Player 2: BOT | Score: 11 +Winner: BOT +------------------------ +Date: Wed Dec 31 22:20:38 2025 +Player 1: amir | Score: 1 +Player 2: BOT | Score: 10 +Winner: BOT +------------------------ +Date: Wed Dec 31 22:21:37 2025 +Player 1: amir | Score: 4 +Player 2: BOT | Score: 12 +Winner: BOT +------------------------ +Date: Fri Jan 2 16:54:41 2026 +Player 1: amir | Score: 5 +Player 2: BOT | Score: 11 +Winner: BOT +------------------------ +Date: Fri Jan 2 18:03:30 2026 +Player 1: amir | Score: 15 +Player 2: BOT | Score: 1 +Winner: amir +------------------------ +Date: Fri Jan 2 20:39:47 2026 +Player 1: amir | Score: 17 +Player 2: momad | Score: 19 +Winner: momad +------------------------ +Date: Fri Jan 2 21:06:02 2026 +Player 1: amir | Score: 6 +Player 2: BOT | Score: 10 +Winner: BOT +------------------------ +Date: Fri Jan 2 22:08:39 2026 +Player 1: amir | Score: 10 +Player 2: BOT | Score: 6 +Winner: amir +------------------------ +Date: Sat Jan 3 11:48:17 2026 +Player 1: steve | Score: 6 +Player 2: dustin | Score: 10 +Winner: dustin +------------------------ +Date: Sat Jan 3 15:33:15 2026 +Player 1: amir | Score: 10 +Player 2: BOT | Score: 6 +Winner: amir +------------------------ +Date: Sat Jan 3 15:34:36 2026 +Player 1: liam | Score: 6 +Player 2: harry | Score: 10 +Winner: harry +------------------------ +Date: Sat Jan 3 18:47:04 2026 +Player 1: amir | Score: 26 +Player 2: BOT | Score: 38 +Winner: BOT +------------------------ +Date: Sun Jan 4 09:25:38 2026 +Player 1: amir | Score: 11 +Player 2: BOT | Score: 25 +Winner: BOT +------------------------ +Date: Sun Jan 4 14:20:09 2026 +Player 1: amir | Score: 37 +Player 2: BOT | Score: 27 +Winner: amir +------------------------ +Date: Mon Jan 5 11:27:18 2026 +Player 1: steve | Score: 10 +Player 2: BOT | Score: 26 +Winner: BOT +------------------------ +Date: Mon Jan 5 11:29:59 2026 +Player 1: harry | Score: 18 +Player 2: voldi | Score: 46 +Winner: voldi +------------------------ +Date: Mon Jan 5 11:30:52 2026 +Player 1: chester | Score: 5 +Player 2: mike | Score: 11 +Winner: mike +------------------------ +Date: Mon Jan 5 11:31:31 2026 +Player 1: chester | Score: 6 +Player 2: BOT | Score: 10 +Winner: BOT +------------------------ +Date: Mon Jan 5 11:32:27 2026 +Player 1: amir | Score: 6 +Player 2: BOT | Score: 10 +Winner: BOT +------------------------ +Date: Mon Jan 5 11:59:01 2026 +Player 1: amir | Score: 6 +Player 2: BOT | Score: 10 +Winner: BOT +------------------------ diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..69156bb --- /dev/null +++ b/main.cpp @@ -0,0 +1,1499 @@ +/* +============================================================ + 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(); +} + + diff --git a/main.exe b/main.exe new file mode 100644 index 0000000..405b28b Binary files /dev/null and b/main.exe differ diff --git a/saved_game.txt b/saved_game.txt new file mode 100644 index 0000000..e69de29 diff --git a/stats.txt b/stats.txt new file mode 100644 index 0000000..ad68c43 --- /dev/null +++ b/stats.txt @@ -0,0 +1,8 @@ +amir 106 3 4 0 7 +dustin 10 1 0 0 1 +steve 16 0 2 0 2 +liam 6 0 1 0 1 +harry 28 1 1 0 2 +voldi 46 1 0 0 1 +chester 11 0 2 0 2 +mike 11 1 0 0 1