1500 lines
40 KiB
C++
1500 lines
40 KiB
C++
/*
|
||
============================================================
|
||
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();
|
||
}
|
||
|
||
|