Files
MiniProject/main.cpp
T
2026-04-13 17:56:29 +03:30

912 lines
25 KiB
C++

#include <iostream>
#include <conio.h>
#include <ctime>
#include <cstdlib>
#include <fstream>
#ifdef _WIN32
#include <windows.h>
#endif
using namespace std;
const int N = 8;
const char EMPTY = '.';
const char BLACK = 'B';
const char WHITE = 'W';
const string BLACK_GLYPH = "○";
const string WHITE_GLYPH = "●";
const string EMPTY_GLYPH = ".";
const string LEGAL_GLYPH = "+";
const string SAVE_FILE = "othello_saves.txt";
const string HISTORY_FILE = "othello_history.txt";
const int DR[8] = {-1,-1,-1, 0,0, +1,+1,+1};
const int DC[8] = {-1, 0,+1,-1,+1,-1, 0,+1};
bool vsBot = false;
string player1_name, player2_name;
struct PlayerInfo {
char id; // 'B' or 'W'
const char* name; // "Black", "White"
const char* glyph; // "○", "●"
};
PlayerInfo BLACK_PLAYER = {'B', "Black", "○"};
PlayerInfo WHITE_PLAYER = {'W', "White", "●"};
bool in_bounds(int r, int c) { //checks if a tile is in the board or out of bounds
return r >= 0 && r < 8 && c >= 0 && c < 8;
}
string trim_newline(string s) { //just makes sure the user input is clean (trims \n and \r)
while (!s.empty() && (s.back() == '\n' || s.back() == '\r')) {
s.pop_back();
}
return s;
}
long long now_epoch() { //gets epoch time. used for creating save files
return (long long)time(nullptr);
}
int idx(int row, int col) { //converts row and column to an index between 0 and 63
return row * 8 + col;
}
char other_player(char p) { //finds out who the opponent is based on the player given
return (p == BLACK) ? WHITE : BLACK;
}
/// for Unicode symbols. I first created the program with B and W for black and white but
/// when including Unicode symbols I had to add this to avoid hard-coding///
string glyph_for_cell(char cell) {
if (cell == BLACK) return BLACK_GLYPH;
if (cell == WHITE) return WHITE_GLYPH;
if (cell == EMPTY) return EMPTY_GLYPH;
if (cell == '+') return LEGAL_GLYPH;
// fallback
string s;
s.push_back(cell);
return s;
}
/// this next function receives a tile coordinates and a direction then finds out how many opposing cell
/// would flip if the cell is played///
int count_flips_dir(const char board[64], int r, int c, int dr, int dc, char player) {
char opponent = (player == BLACK) ? 'W' : 'B';
int rr = r + dr;
int cc = c + dc;
// 1) first step must be inside board
if (!in_bounds(rr, cc)) {
return 0;
}
// 2) first cell must be opponent
if (board[idx(rr, cc)] != opponent)
return 0;
int count = 0;
// 3) walk while we keep seeing opponent
while (in_bounds(rr, cc) && board[idx(rr, cc)] == opponent) {
count++;
rr += dr;
cc += dc;
}
// 4) must end on player's own piece
if (in_bounds(rr, cc) && board[idx(rr, cc)] == player) {
return count;
}
// otherwise: open-ended or out-of-bounds
return 0;
}
int total_flips(const char board[64], int r, int c, char player) { //finds the sum of flips in all directions
if (board[idx(r, c)] != '.') //first checks if the cell is not empty
return 0;
int total = 0;
for (int k = 0; k < 8; k++) { //using the previous function, finds the total sum of flips
total += count_flips_dir(board, r, c, DR[k], DC[k], player);
}
return total;
}
/// finds out if a cell is a valid move by first checking if it's empty then finding if flip count is positive///
bool is_legal_move(const char board[64], int r, int c, char player) {
if (board[idx(r, c)] != '.')
return false;
return total_flips(board, r, c, player) > 0;
}
/// stores all of the legal moves into an array of length 64, called moves[], does this using
/// an index between 0 and 63 with the help of idx function. no 2D indexing here///
int collect_legal_moves(const char board[64], char player, int moves[64]) {
int count = 0;
for (int r = 0; r < 8; r++) {
for (int c = 0; c < 8; c++) {
if (is_legal_move(board, r, c, player)) {
moves[count++] = idx(r, c);
}
}
}
return count;
}
///this is my core game logic. this function first finds out player move in 2D indexing so
///we can find out how many cells would flip. then applies it by flipping exactly that number of cells
///in the given direction///
void apply_move(char board[64], int pos, char player) {
// 1) Place the player's piece on the chosen position.
board[pos] = player;
// Convert linear index to row/col so my directional logic can be used.
int r0 = pos / 8;
int c0 = pos % 8;
// 3) For each direction:
// - ask how many flips happen in that direction
// - if flips > 0, walk that many steps and overwrite those tiles to 'player'
for (int k = 0; k < 8; k++) {
int flips = count_flips_dir(board, r0, c0, DR[k], DC[k], player);
if (flips <= 0) {
continue;
} // no captured line in this direction
// Start at the neighbor cell (first step in the direction)
int r = r0 + DR[k];
int c = c0 + DC[k];
// Flip exactly 'flips' opponent pieces
for (int i = 0; i < flips; i++) {
board[idx(r, c)] = player;
r += DR[k];
c += DC[k];
}
}
}
bool has_any_legal_move(const char board[64], char player) {
int tmp[64];
return collect_legal_moves(board, player, tmp) > 0;
}
/// first finds out which moves are possible using our collect_legal_moves function, then picks one
/// with the help of the rand() method///
int bot_choose_random_move(const char board[64], char botPlayer) {
int moves[64];
int cnt = collect_legal_moves(board, botPlayer, moves);
if (cnt <= 0) return -1;
int pick = rand() % cnt;
return moves[pick];
}
// used for the end of the game, counts how many cells belong to each player
int count_pieces(const char board[64], char who) {
int cnt = 0;
for (int i = 0; i < 64; i++) {
if (board[i] == who) cnt++;
}
return cnt;
}
/// if a player has no legal move then it passes to the other player and if both have no legal
/// moves then it changes the game state to 'E' which is short for End///
void normalize_turn_with_pass_and_end(const char board[64], char &turn) {
if (turn == 'E') {
return;
}
if (has_any_legal_move(board, turn)) {
return;
} // current player can play
char opp = other_player(turn);
if (!has_any_legal_move(board, opp)) {
// nobody can move => end
turn = 'E';
return;
}
// current player must pass
cout << "\n" << turn << " has no legal moves. Press any key to pass...\n";
(void)getch();
turn = opp;
}
void print_final_score(const char board[64]) {
int b = count_pieces(board, BLACK);
int w = count_pieces(board, WHITE);
cout << "\n=== GAME OVER ===\n";
cout << "Black (" << BLACK_GLYPH << "): " << b << "\n";
cout << "White (" << WHITE_GLYPH << "): " << w << "\n";
if (b > w) {
cout << "Winner: Black\n";
} else if (w > b) {
cout << "Winner: White\n";
} else {
cout << "Result: Draw\n";
}
cout << "Press any key...\n";
(void)getch();
}
/// receives a board[] and creates a string based on it which is used for saving
string serialize_state(char turn, const char board[64]) {
string s;
s.reserve(65);
s.push_back(turn);
for (int i = 0; i < 64; ++i) {
s.push_back(board[i]);
}
return s;
}
/// receives a string (probably from a save file) then finds the game state from it.
/// first checks if the string is valid by checking the length of the string
/// also the starting letter should be B/W/E.
bool deserialize_state (const string& s, char &turn, char board[64]) {
if (static_cast<int>(s.size()) != 65) {
return false;
}
char t = s[0];
if (!(t == 'B' || t == 'W' || t == 'E')) { // B= black's turn - W= white's turn - E= end of the game
return false;
}
for (int i = 1; i < 65; i++) {
char ch = s[i];
if (!(ch == EMPTY || ch == BLACK || ch == WHITE)) return false;
board[i - 1] = ch;
}
turn = t;
return true;
}
/// save file includes more data than the state string, this function parses out everything from a
/// save entry including id, epoch, state string itself///
bool parse_save_line_v2(const string& line, int &id, long long &epoch, string &state65) {
// Format: id|epoch|mode|state65
// mode is BOT or PVP
// first we find '|'s in the save entry and remember their positions
size_t p1 = line.find('|');
if (p1 == string::npos) return false;
size_t p2 = line.find('|', p1 + 1);
if (p2 == string::npos) return false;
size_t p3 = line.find('|', p2 + 1);
if (p3 == string::npos) return false;
string sid = line.substr(0, p1); //extracting strings for id, epoch, mode and state
string sep = line.substr(p1 + 1, p2 - (p1 + 1));
string smode = line.substr(p2 + 1, p3 - (p2 + 1));
string sst = line.substr(p3 + 1);
if ((int)sst.size() != 65) return false;
if (!(smode == "BOT" || smode == "PVP")) return false;
if (sid.empty()) {
return false;
}
int idv = 0; //casting characters into numbers
for (char ch : sid) {
if (ch < '0' || ch > '9') { //only valid characters (numbers from 0 to 9)
return false;
}
idv = idv * 10 + (ch - '0');
}
if (sep.empty()) {
return false;
}
long long ev = 0;
for (char ch : sep) { //casting characters into numbers
if (ch < '0' || ch > '9') return false;
ev = ev * 10 + (ch - '0');
}
id = idv;
epoch = ev;
state65 = sst;
vsBot = (smode == "BOT");
return true;
}
int next_save_id_1_to_1000(const string& filename) {
bool oldVsBot = vsBot;
bool used[1001];
for (int i = 0; i <= 1000; i++) {
used[i] = false;
}
ifstream in(filename);
if (in) {
string line;
while (getline(in, line)) {
line = trim_newline(line);
if (line.empty()) {
continue;
}
int id; long long epoch; string state;
if (parse_save_line_v2(line, id, epoch, state)) {
if (id >= 1 && id <= 1000) {
used[id] = true;
}
}
}
}
for (int id = 1; id <= 1000; id++) {
if (!used[id]) {
vsBot = oldVsBot;
return id;
}
}
vsBot = oldVsBot;
return -1; // full-no id available
}
bool save_game(const string& filename, int id, const string& state65) {
ofstream out(filename, ios::app);
if (!out) {
return false;
}
// Format: id|epoch|mode|state65
out << id << "|" << now_epoch() << "|" << (vsBot ? "BOT" : "PVP") << "|" << state65 << "\n";
out.flush();
return true;
}
/// keeps searching for valid games then holds on to the last unfinished game
bool load_latest_unfinished(const string& filename, int &outId, string &outState65) {
ifstream in(filename);
if (!in) {
return false;
}
bool found = false;
string line;
while (getline(in, line)) {
line = trim_newline(line);
if (line.empty()) {
continue;
}
int id; long long epoch; string state;
if (!parse_save_line_v2(line, id, epoch, state)) {
continue;
}
char turn; char board[64];
if (!deserialize_state(state, turn, board)) {
continue;
}
if (turn != 'E') {
outId = id;
outState65 = state;
found = true; // keep last one
}
}
return found;
}
// receives a target ID and tries to find it in the save file so it can load that game.
bool load_game_by_id(const string& filename, int targetId, string &outState65) {
ifstream in(filename);
if (!in) return false;
string line;
while (getline(in, line)) {
line = trim_newline(line);
if (line.empty()) continue;
int id;
long long epoch;
string state;
if (!parse_save_line_v2(line, id, epoch, state)) continue;
if (id == targetId) {
outState65 = state;
return true;
}
}
return false;
}
/// parsing history file looking for '|'s. se: string epoch, sb: string black count, sw: string white count,
/// ev: epoch value (numeric), etc...
bool parse_history_line(const string& line, long long &epoch, int &b, int &w, string &result) {
size_t p1 = line.find('|');
if (p1 == string::npos) return false;
size_t p2 = line.find('|', p1 + 1);
if (p2 == string::npos) return false;
size_t p3 = line.find('|', p2 + 1);
if (p3 == string::npos) return false;
string se = line.substr(0, p1);
string sb = line.substr(p1 + 1, p2 - (p1 + 1));
string sw = line.substr(p2 + 1, p3 - (p2 + 1));
result = line.substr(p3 + 1);
if (se.empty()) return false;
long long ev = 0;
for (char ch : se) { //casting characters to numbers
if (ch < '0' || ch > '9') return false;
ev = ev * 10 + (ch - '0');
}
if (sb.empty()) return false;
int bv = 0;
for (char ch : sb) { //casting characters to numbers
if (ch < '0' || ch > '9') return false;
bv = bv * 10 + (ch - '0');
}
if (sw.empty()) return false;
int wv = 0;
for (char ch : sw) { //casting characters to numbers
if (ch < '0' || ch > '9') return false;
wv = wv * 10 + (ch - '0');
}
epoch = ev;
b = bv;
w = wv;
result = trim_newline(result);
return true;
}
void append_history(const string& filename, const char board[64]) {
int b = 0, w = 0;
for (int i = 0; i < 64; i++) {
if (board[i] == BLACK) b++;
else if (board[i] == WHITE) w++;
}
string result;
if (b > w) {
result = "B";
}
else if (w > b) {
result = "W";
}
else {
result = "DRAW";
}
ofstream out(filename, ios::app);
if (!out) {
return;
}
out << now_epoch() << "|" << b << "|" << w << "|" << result << "\n";
out.flush();
}
void init_board(char board[64], char &turn) {
for (int i = 0; i < 64; ++i) {
board[i] = EMPTY;
}
board[idx(3,3)] = WHITE;
board[idx(4,4)] = WHITE;
board[idx(3,4)] = BLACK;
board[idx(4,3)] = BLACK;
turn = BLACK;
}
/// print board with coordinate guides
void print_board(const char board[64]) {
for (int i = -1; i < 8; ++i) {
for (int j = -1; j < 8; ++j) {
if (i == -1 && j == -1) {
cout << " ";
} else if (i == -1) {
cout << " " << j + 1 << " ";
} else if (j == -1) {
cout << " " << static_cast<char>('A' + i) << " ";
} else {
cout << " " << glyph_for_cell(board[idx(i, j)]) << " ";
}
}
cout << '\n' << '\n';
}
}
void clear_screen() {
for (int i = 0; i < 40; ++i)
cout << "\n";
}
void press_any_key() {
cout << "\nPress any key...\n";
(void)getch();
}
int read_key() {
int k = getch();
if (k == 0 || k == 224) return 1000 + getch(); // extended keys
return k;
}
void show_final_score(const char board[64]) {
int b = count_pieces(board, BLACK);
int w = count_pieces(board, WHITE);
clear_screen();
cout << "=== GAME OVER ===\n\n";
cout << "Black (" << BLACK_GLYPH << "): " << b << "\n";
cout << "White (W): " << w << "\n\n";
if (b > w) {
cout << "Winner: Black\n";
} else if (w > b) {
cout << "Winner: White\n";
}
else {
cout << "Result: Draw\n";
}
press_any_key();
}
int show_menu_and_get_choice() {
clear_screen();
cout << "=== MENU ===\n\n";
cout << "1) New Game\n";
cout << "2) Load Game\n";
cout << "3) Help\n";
cout << "4) Game History\n";
cout << "5) Exit\n\n";
cout << "Choose (1-5): ";
int ch = 0;
if (!(cin >> ch)) {
cin.clear();
cin.ignore(1000000, '\n');
return 0;
}
cin.ignore(1000000, '\n');
return ch;
}
/// draw the board. showing where does the keyboard point at(cursor) and where are the legal moves
void draw_board_with_cursor(const char board[64],
const int legalMoves[64], int legalCount,
int cursorR, int cursorC, char turn) {
bool legal[64] = {false};
for (int i = 0; i < legalCount; i++) legal[legalMoves[i]] = true;
clear_screen();
if (vsBot) {
cout << player1_name << " VS " << "BOT" << "\n";
} else {
cout << player1_name << " VS " << player2_name << "\n";
}
cout << "Turn: " << turn << " (Enter = Play, S = Save, L = Load, Q = Quit)\n";
cout << "Arrows = Move Cursor | Legal moves marked with '+'\n";
cout << BLACK_PLAYER.name << ": " << BLACK_PLAYER.glyph
<< " "
<< WHITE_PLAYER.name << ": " << WHITE_PLAYER.glyph
<< "\n\n";
cout << " 1 2 3 4 5 6 7 8\n";
for (int r = 0; r < 8; r++) {
cout << " " << char('A' + r) << " ";
for (int c = 0; c < 8; c++) {
int p = idx(r,c);
char cell = board[p];
// show legal marker on empty tiles
if (cell == '.' && legal[p]) {
cell = '+';
}
// cursor highlight
if (r == cursorR && c == cursorC) {
cout << "[" << glyph_for_cell(cell) << "]";
} else {
cout << " " << glyph_for_cell(cell) << " ";
}
}
cout << "\n\n";
}
}
void draw_board(const char board[64],
const int* legalMoves, int legalCount,
int cursorR, int cursorC) {
// draw_board_with_cursor expects an array of size 64.
// If we got nullptr, provide a dummy array and show no legal moves. useful for when the game has ended
int dummy[64];
for (int i = 0; i < 64; i++) dummy[i] = 0;
if (legalMoves == nullptr) {
draw_board_with_cursor(board, dummy, 0, cursorR, cursorC, 'E');
} else {
// Copy into fixed-size array so signature matches (no vectors)
int tmp[64];
for (int i = 0; i < legalCount; i++) tmp[i] = legalMoves[i];
draw_board_with_cursor(board, tmp, legalCount, cursorR, cursorC, 'E');
}
}
void run_game(char board[64], char &turn) {
int cursorR = 3, cursorC = 3; //default cursor at position row=3 col=3
while (true) {
normalize_turn_with_pass_and_end(board, turn);
if (turn == 'E') {
draw_board(board, nullptr, 0, cursorR, cursorC);
show_final_score(board);
append_history(HISTORY_FILE, board);
return; // back to menu
}
int legalMoves[64];
int legalCount = collect_legal_moves(board, turn, legalMoves);
draw_board_with_cursor(board, legalMoves, legalCount, cursorR, cursorC, turn);
// BOT TURN: board is already drawn at this point.
// We show the move, pause, then apply the move, so the user sees
// their move first and bot move second (with a pause in between).
if (vsBot && turn == WHITE) {
int pos = bot_choose_random_move(board, WHITE);
if (pos == -1) {
cout << "\nBot has no legal moves. Passing...\n";
press_any_key();
continue;
}
int r = pos / 8;
int c = pos % 8;
cout << "\nBot played: " << char('A' + r) << (c + 1) << "\n";
press_any_key();
apply_move(board, pos, WHITE);
turn = BLACK;
continue;
}
int key = read_key();
if (key == 'q' || key == 'Q') return;
if (key == 's' || key == 'S') {
if (turn == 'E') {
continue; // no saving allowed after game over
}
string state = serialize_state(turn, board);
int id = next_save_id_1_to_1000(SAVE_FILE);
if (id == -1) {
cout << "\nSave failed (IDs full).\n";
press_any_key();
continue;
}
save_game(SAVE_FILE, id, state);
cout << "\nSaved. Game ID = " << id << "\n";
press_any_key();
continue;
}
if (key == 'l' || key == 'L') {
cout << "\nEnter game ID to load: ";
int id;
cin >> id;
cin.ignore(1000000, '\n');
string state;
if (!load_game_by_id(SAVE_FILE, id, state)) {
cout << "Invalid game ID.\n";
press_any_key();
continue;
}
if (!deserialize_state(state, turn, board)) {
cout << "Corrupted save.\n";
press_any_key();
continue;
}
normalize_turn_with_pass_and_end(board, turn);
cursorR = 3;
cursorC = 3;
continue;
}
if (key == 1000 + 72 && cursorR > 0) cursorR--; // up
else if (key == 1000 + 80 && cursorR < 7) cursorR++; // down
else if (key == 1000 + 75 && cursorC > 0) cursorC--; // left
else if (key == 1000 + 77 && cursorC < 7) cursorC++; // right
else if (key == 13) { // Enter
int pos = idx(cursorR, cursorC);
if (is_legal_move(board, cursorR, cursorC, turn)) {
apply_move(board, pos, turn);
turn = other_player(turn);
}
}
}
}
void screen_new_game() {
clear_screen();
char board[64];
char turn;
init_board(board, turn);
clear_screen();
cout << "[New Game]\n";
cout << "1) Single Player (vs Bot)\n";
cout << "2) Two Player\n";
cout << "Choose (1-2): ";
int mode = 2;
cin >> mode;
cin.ignore(1000000, '\n');
if (mode == 1) {
vsBot = true;
cout << "Please enter your name: ";
getline(cin, player1_name);
}
if (mode == 2) {
cout << "Please enter player 1 name: ";
getline(cin, player1_name);
cout << "Please enter player 2 name: ";
getline(cin, player2_name);
}
run_game(board, turn);
}
void screen_load_game() {
clear_screen();
cout << "[Load Game]\n\n";
cout << "1) Load latest unfinished\n";
cout << "2) Load by ID\n";
cout << "3) Back\n\n";
cout << "Choose (1-3): ";
int choice = 0;
if (!(cin >> choice)) {
cin.clear();
cin.ignore(1000000, '\n');
return;
}
cin.ignore(1000000, '\n');
if (choice == 3) {
return;
}
int id = 0;
string state65;
bool ok = false;
if (choice == 1) {
ok = load_latest_unfinished(SAVE_FILE, id, state65);
if (!ok) {
cout << "No unfinished saved game found.\n";
press_any_key();
return;
}
}
else if (choice == 2) {
cout << "Enter game ID to load: ";
cin >> id;
cin.ignore(1000000, '\n');
ok = load_game_by_id(SAVE_FILE, id, state65);
if (!ok) {
cout << "Invalid game ID.\n";
press_any_key();
return;
}
}
else {
return;
}
char board[64];
char turn;
if (!deserialize_state(state65, turn, board)) {
cout << "Save file is corrupted (state invalid).\n";
press_any_key();
return;
}
normalize_turn_with_pass_and_end(board, turn);
cout << "Loaded save: ID = " << id << "\n\n";
press_any_key();
run_game(board, turn);
}
void screen_help() {
clear_screen();
cout << "[Help]\n";
cout << "Controls:\n";
cout << " Arrows : move cursor\n";
cout << " Enter : play at cursor (if legal)\n";
cout << " S : save (creates a new ID 1..1000)\n";
cout << " L : load by ID (during game)\n";
cout << " Q : quit current game to menu\n\n";
cout << "Rules: place a piece to flip opponent pieces in any direction.\n";
press_any_key();
}
void screen_history() {
clear_screen();
ifstream in(HISTORY_FILE);
if (!in) {
cout << "No history yet.\n";
press_any_key();
return;
}
int n = 0;
string line;
while (getline(in, line)) n++;
if (n == 0) {
cout << "No history yet.\n";
press_any_key();
return;
}
in.clear();
in.seekg(0);
string* lines = new string[n];
int i = 0;
while (i < n && getline(in, line)) {
lines[i++] = trim_newline(line);
}
cout << "=== GAME HISTORY (newest first) ===\n\n";
// Print from last to first
for (int k = n - 1; k >= 0; k--) {
long long epoch;
int b, w;
string result;
if (!parse_history_line(lines[k], epoch, b, w, result)) continue;
cout << epoch << " | " << "Black: " << b << " White: " << w << " | ";
if (result == "DRAW") cout << "Draw";
else if (result == "B") cout << "Winner: Black";
else if (result == "W") cout << "Winner: White";
else cout << "Result: " << result;
cout << "\n";
}
delete[] lines;
press_any_key();
}
int main () {
#ifdef _WIN32
SetConsoleOutputCP(CP_UTF8);
#endif
srand((unsigned)time(nullptr)); //helps generate random numbers
while (true) {
int ch = show_menu_and_get_choice();
switch (ch) {
case 1: screen_new_game(); break;
case 2: screen_load_game(); break;
case 3: screen_help(); break;
case 4: screen_history(); break;
case 5: return 0;
default: break;
}
}
return 0;
}