Files

1500 lines
40 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
============================================================
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 <iostream>
#include <cstring>
#include <cstdlib>
#include <ctime>
#include <conio.h>
#include <fstream>
#include <thread>
#include <chrono>
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<unsigned int>(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();
}