From 335861c0361af61481b7fda3c85d33e13bb62e96 Mon Sep 17 00:00:00 2001 From: Slx5 Date: Fri, 18 Jul 2025 14:54:42 +0300 Subject: [PATCH 1/2] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D1=8B=20=D0=BA=D0=BE=D0=BC=D0=BC=D0=B5=D0=BD=D1=82=D0=B0?= =?UTF-8?q?=D1=80=D0=B8=D0=B8=20=D0=BA=20=D0=BA=D0=BB=D1=8E=D1=87=D0=B5?= =?UTF-8?q?=D0=B2=D1=8B=D0=BC=20=D1=84=D1=80=D0=B0=D0=B3=D0=BC=D0=B5=D0=BD?= =?UTF-8?q?=D1=82=D0=B0=D0=BC=20=D0=BA=D0=BE=D0=B4=D0=B0.=20=D0=A3=D0=B4?= =?UTF-8?q?=D0=B0=D0=BB=D0=B5=D0=BD=D0=B0=20=D1=80=D0=B5=D0=B0=D0=BB=D0=B8?= =?UTF-8?q?=D0=B7=D0=B0=D1=86=D0=B8=D1=8F=20=D1=84=D1=83=D0=BD=D0=BA=D1=86?= =?UTF-8?q?=D0=B8=D0=B9=20find=5Fbest=5Fturns(),=20find=5Ffirst=5Fbest=5Ft?= =?UTF-8?q?urn()=20=D0=B8=20find=5Fbest=5Fturns=5Frec().?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Game/Board.h | 69 +++++++++++++------- Game/Config.h | 3 + Game/Game.h | 105 +++++++++++++++++------------- Game/Hand.h | 28 +++++--- Game/Logic.h | 160 +++++++++++----------------------------------- Models/Move.h | 12 +++- Models/Response.h | 11 ++-- settings.json | 14 ++++ 8 files changed, 190 insertions(+), 212 deletions(-) diff --git a/Game/Board.h b/Game/Board.h index 5c955fb..6ccfc3c 100644 --- a/Game/Board.h +++ b/Game/Board.h @@ -16,15 +16,17 @@ using namespace std; +// Класс, реализующий игровую доску и её визуализацию class Board { public: Board() = default; + // Конструктор с указанием размеров окна Board(const unsigned int W, const unsigned int H) : W(W), H(H) { } - // draws start board + // Инициализация SDL, загрузка текстур и отрисовка стартовой доски int start_draw() { if (SDL_Init(SDL_INIT_EVERYTHING) != 0) @@ -56,6 +58,7 @@ class Board print_exception("SDL_CreateRenderer can't create renderer"); return 1; } + // Загрузка всех необходимых текстур board = IMG_LoadTexture(ren, board_path.c_str()); w_piece = IMG_LoadTexture(ren, piece_white_path.c_str()); b_piece = IMG_LoadTexture(ren, piece_black_path.c_str()); @@ -69,11 +72,12 @@ class Board return 1; } SDL_GetRendererOutputSize(ren, &W, &H); - make_start_mtx(); - rerender(); + make_start_mtx(); // Формируем стартовую матрицу доски + rerender(); // Перерисовываем всё return 0; } + // Сброс состояния доски к начальному (для новой партии или повтора) void redraw() { game_results = -1; @@ -84,6 +88,7 @@ class Board clear_highlight(); } + // Выполнить ход (с возможным взятием) void move_piece(move_pos turn, const int beat_series = 0) { if (turn.xb != -1) @@ -93,6 +98,7 @@ class Board move_piece(turn.x, turn.y, turn.x2, turn.y2, beat_series); } + // Выполнить ход по координатам (с возможным превращением в дамку) void move_piece(const POS_T i, const POS_T j, const POS_T i2, const POS_T j2, const int beat_series = 0) { if (mtx[i2][j2]) @@ -103,19 +109,22 @@ class Board { throw runtime_error("begin position is empty, can't move"); } + // Превращение в дамку при достижении последней линии if ((mtx[i][j] == 1 && i2 == 0) || (mtx[i][j] == 2 && i2 == 7)) mtx[i][j] += 2; mtx[i2][j2] = mtx[i][j]; drop_piece(i, j); - add_history(beat_series); + add_history(beat_series); // Сохраняем состояние для отката } + // Удалить шашку с доски void drop_piece(const POS_T i, const POS_T j) { mtx[i][j] = 0; rerender(); } + // Превратить шашку в дамку void turn_into_queen(const POS_T i, const POS_T j) { if (mtx[i][j] == 0 || mtx[i][j] > 2) @@ -125,11 +134,13 @@ class Board mtx[i][j] += 2; rerender(); } + // Получить текущее состояние доски vector> get_board() const { return mtx; } + // Подсветить клетки (например, возможные ходы) void highlight_cells(vector> cells) { for (auto pos : cells) @@ -140,6 +151,7 @@ class Board rerender(); } + // Снять подсветку со всех клеток void clear_highlight() { for (POS_T i = 0; i < 8; ++i) @@ -149,6 +161,7 @@ class Board rerender(); } + // Установить активную (выделенную) клетку void set_active(const POS_T x, const POS_T y) { active_x = x; @@ -156,6 +169,7 @@ class Board rerender(); } + // Снять выделение с активной клетки void clear_active() { active_x = -1; @@ -163,11 +177,13 @@ class Board rerender(); } + // Проверить, подсвечена ли клетка bool is_highlighted(const POS_T x, const POS_T y) { return is_highlighted_[x][y]; } + // Откатить ход (или серию взятий) к предыдущему состоянию void rollback() { auto beat_series = max(1, *(history_beat_series.rbegin())); @@ -181,19 +197,21 @@ class Board clear_active(); } + // Показать финальный экран с результатом партии void show_final(const int res) { game_results = res; rerender(); } - // use if window size changed + // Обновить размеры окна и перерисовать доску (вызывать при изменении размера окна) void reset_window_size() { SDL_GetRendererOutputSize(ren, &W, &H); rerender(); } + // Освободить все ресурсы SDL (вызывать при завершении работы) void quit() { SDL_DestroyTexture(board); @@ -208,6 +226,7 @@ class Board SDL_Quit(); } + // Деструктор: освобождает ресурсы, если окно было создано ~Board() { if (win) @@ -215,12 +234,13 @@ class Board } private: + // Сохраняет текущее состояние доски и серию взятий в историю void add_history(const int beat_series = 0) { history_mtx.push_back(mtx); history_beat_series.push_back(beat_series); } - // function to make start matrix + // Формирует стартовую матрицу доски (расстановка шашек) void make_start_mtx() { for (POS_T i = 0; i < 8; ++i) @@ -237,7 +257,7 @@ class Board add_history(); } - // function that re-draw all the textures + // Перерисовывает всё содержимое окна (доска, фигуры, подсветка, результат и т.д.) void rerender() { // draw board @@ -327,6 +347,7 @@ class Board SDL_PollEvent(&windowEvent); } + // Запись ошибки в лог-файл void print_exception(const string& text) { ofstream fout(project_path + "log.txt", ios_base::app); fout << "Error: " << text << ". "<< SDL_GetError() << endl; @@ -334,22 +355,22 @@ class Board } public: - int W = 0; - int H = 0; + int W = 0; // ширина окна + int H = 0; // высота окна // history of boards - vector>> history_mtx; + vector>> history_mtx; // история состояний доски private: - SDL_Window *win = nullptr; - SDL_Renderer *ren = nullptr; + SDL_Window *win = nullptr; // окно SDL + SDL_Renderer *ren = nullptr; // рендерер SDL // textures - SDL_Texture *board = nullptr; - SDL_Texture *w_piece = nullptr; - SDL_Texture *b_piece = nullptr; - SDL_Texture *w_queen = nullptr; - SDL_Texture *b_queen = nullptr; - SDL_Texture *back = nullptr; - SDL_Texture *replay = nullptr; + SDL_Texture *board = nullptr; // текстура доски + SDL_Texture *w_piece = nullptr; // текстура белой шашки + SDL_Texture *b_piece = nullptr; // текстура чёрной шашки + SDL_Texture *w_queen = nullptr; // текстура белой дамки + SDL_Texture *b_queen = nullptr; // текстура чёрной дамки + SDL_Texture *back = nullptr; // текстура кнопки "назад" + SDL_Texture *replay = nullptr; // текстура кнопки "повтор" // texture files names const string textures_path = project_path + "Textures/"; const string board_path = textures_path + "board.png"; @@ -363,14 +384,14 @@ class Board const string back_path = textures_path + "back.png"; const string replay_path = textures_path + "replay.png"; // coordinates of chosen cell - int active_x = -1, active_y = -1; + int active_x = -1, active_y = -1; // координаты выделенной клетки // game result if exist - int game_results = -1; + int game_results = -1; // результат партии (-1 — не завершена) // matrix of possible moves - vector> is_highlighted_ = vector>(8, vector(8, 0)); + vector> is_highlighted_ = vector>(8, vector(8, 0)); // подсветка клеток // matrix of possible moves // 1 - white, 2 - black, 3 - white queen, 4 - black queen - vector> mtx = vector>(8, vector(8, 0)); + vector> mtx = vector>(8, vector(8, 0)); // матрица доски // series of beats for each move - vector history_beat_series; + vector history_beat_series; // история серий взятий }; diff --git a/Game/Config.h b/Game/Config.h index 1a41663..371e4f4 100644 --- a/Game/Config.h +++ b/Game/Config.h @@ -13,6 +13,7 @@ class Config reload(); } + // Загружает настройки из файла settings.json в объект config void reload() { std::ifstream fin(project_path + "settings.json"); @@ -20,6 +21,8 @@ class Config fin.close(); } + // Позволяет обращаться к настройкам как к элементам двумерного массива: + // config("Bot", "IsWhiteBot") вернёт значение настройки IsWhiteBot из раздела Bot auto operator()(const string &setting_dir, const string &setting_name) const { return config[setting_dir][setting_name]; diff --git a/Game/Game.h b/Game/Game.h index d7d16bc..bac1cb9 100644 --- a/Game/Game.h +++ b/Game/Game.h @@ -20,44 +20,50 @@ class Game // to start checkers int play() { + // Засекаем время начала партии auto start = chrono::steady_clock::now(); + // Если выбран режим повтора партии if (is_replay) { - logic = Logic(&board, &config); - config.reload(); - board.redraw(); + logic = Logic(&board, &config); // пересоздаём объект логики + config.reload(); // перечитываем настройки + board.redraw(); // перерисовываем доску } else { - board.start_draw(); + board.start_draw(); // начальная отрисовка доски } is_replay = false; int turn_num = -1; bool is_quit = false; - const int Max_turns = config("Game", "MaxNumTurns"); + const int Max_turns = config("Game", "MaxNumTurns"); // максимальное число ходов + // Основной игровой цикл while (++turn_num < Max_turns) { - beat_series = 0; - logic.find_turns(turn_num % 2); - if (logic.turns.empty()) + beat_series = 0; // сбрасываем серию взятий + logic.find_turns(turn_num % 2); // ищем возможные ходы для текущего игрока + if (logic.turns.empty()) // если ходов нет — конец игры break; + // Устанавливаем уровень сложности бота для текущего цвета logic.Max_depth = config("Bot", string((turn_num % 2) ? "Black" : "White") + string("BotLevel")); + // Если ходит человек if (!config("Bot", string("Is") + string((turn_num % 2) ? "Black" : "White") + string("Bot"))) { - auto resp = player_turn(turn_num % 2); + auto resp = player_turn(turn_num % 2); // обработка хода игрока if (resp == Response::QUIT) { - is_quit = true; + is_quit = true; // игрок выбрал выход break; } else if (resp == Response::REPLAY) { - is_replay = true; + is_replay = true; // игрок выбрал повтор партии break; } else if (resp == Response::BACK) { + // Откат ходов, если выбран возврат if (config("Bot", string("Is") + string((1 - turn_num % 2) ? "Black" : "White") + string("Bot")) && !beat_series && board.history_mtx.size() > 2) { @@ -73,60 +79,67 @@ class Game } } else - bot_turn(turn_num % 2); + bot_turn(turn_num % 2); // если ходит бот } + // Засекаем время окончания партии auto end = chrono::steady_clock::now(); + // Записываем время игры в лог ofstream fout(project_path + "log.txt", ios_base::app); fout << "Game time: " << (int)chrono::duration(end - start).count() << " millisec\n"; fout.close(); + // Если был выбран повтор — запускаем партию заново if (is_replay) return play(); + // Если был выход — возвращаем 0 if (is_quit) return 0; - int res = 2; + int res = 2; // результат партии: 0 — ничья, 1 — победа чёрных, 2 — победа белых if (turn_num == Max_turns) { - res = 0; + res = 0; // ничья по лимиту ходов } else if (turn_num % 2) { - res = 1; + res = 1; // победа чёрных } - board.show_final(res); - auto resp = hand.wait(); + board.show_final(res); // показываем финальный экран + auto resp = hand.wait(); // ждём действия игрока (например, повтор) if (resp == Response::REPLAY) { is_replay = true; return play(); } - return res; + return res; // возвращаем результат партии } private: void bot_turn(const bool color) { + // Засекаем время начала хода бота auto start = chrono::steady_clock::now(); auto delay_ms = config("Bot", "BotDelayMS"); - // new thread for equal delay for each turn + // Запускаем отдельный поток для задержки (имитация раздумий бота) thread th(SDL_Delay, delay_ms); - auto turns = logic.find_best_turns(color); - th.join(); + auto turns = logic.find_best_turns(color); // Получаем лучший(ие) ход(ы) для бота + th.join(); // Дожидаемся завершения задержки bool is_first = true; - // making moves + // Выполняем все ходы из найденной последовательности for (auto turn : turns) { if (!is_first) { - SDL_Delay(delay_ms); + SDL_Delay(delay_ms); // Задержка между последовательными ходами (например, при серии взятий) } is_first = false; - beat_series += (turn.xb != -1); - board.move_piece(turn, beat_series); + beat_series += (turn.xb != -1); // Увеличиваем счётчик серии взятий, если был побит противник + board.move_piece(turn, beat_series); // Выполняем ход на доске } + // Засекаем время окончания хода бота auto end = chrono::steady_clock::now(); + // Записываем время хода бота в лог ofstream fout(project_path + "log.txt", ios_base::app); fout << "Bot turn time: " << (int)chrono::duration(end - start).count() << " millisec\n"; fout.close(); @@ -134,21 +147,21 @@ class Game Response player_turn(const bool color) { - // return 1 if quit + // Формируем список клеток, доступных для хода vector> cells; for (auto turn : logic.turns) { cells.emplace_back(turn.x, turn.y); } - board.highlight_cells(cells); + board.highlight_cells(cells); // Подсвечиваем возможные клетки для хода move_pos pos = {-1, -1, -1, -1}; POS_T x = -1, y = -1; - // trying to make first move + // Цикл выбора первой клетки (фигуры для хода) while (true) { - auto resp = hand.get_cell(); + auto resp = hand.get_cell(); // Получаем действие игрока if (get<0>(resp) != Response::CELL) - return get<0>(resp); + return get<0>(resp); // Если не клетка — возвращаем результат (выход, откат и т.д.) pair cell{get<1>(resp), get<2>(resp)}; bool is_correct = false; @@ -166,7 +179,7 @@ class Game } } if (pos.x != -1) - break; + break; // выбран ход if (!is_correct) { if (x != -1) @@ -177,12 +190,12 @@ class Game } x = -1; y = -1; - continue; + continue; // некорректный выбор — повторяем } x = cell.first; y = cell.second; board.clear_highlight(); - board.set_active(x, y); + board.set_active(x, y); // выделяем выбранную фигуру vector> cells2; for (auto turn : logic.turns) { @@ -191,34 +204,34 @@ class Game cells2.emplace_back(turn.x2, turn.y2); } } - board.highlight_cells(cells2); + board.highlight_cells(cells2); // подсвечиваем возможные клетки для хода выбранной фигурой } board.clear_highlight(); board.clear_active(); - board.move_piece(pos, pos.xb != -1); + board.move_piece(pos, pos.xb != -1); // выполняем ход if (pos.xb == -1) - return Response::OK; - // continue beating while can + return Response::OK; // если не было взятия — ход завершён + // Продолжаем серию взятий, если это возможно beat_series = 1; while (true) { - logic.find_turns(pos.x2, pos.y2); + logic.find_turns(pos.x2, pos.y2); // ищем возможные взятия с новой позиции if (!logic.have_beats) - break; + break; // если больше нет взятий — серия завершена vector> cells; for (auto turn : logic.turns) { cells.emplace_back(turn.x2, turn.y2); } - board.highlight_cells(cells); - board.set_active(pos.x2, pos.y2); - // trying to make move + board.highlight_cells(cells); // подсвечиваем возможные клетки для следующего взятия + board.set_active(pos.x2, pos.y2); // выделяем текущую фигуру + // Цикл выбора следующего взятия while (true) { auto resp = hand.get_cell(); if (get<0>(resp) != Response::CELL) - return get<0>(resp); + return get<0>(resp); // если не клетка — возвращаем результат pair cell{get<1>(resp), get<2>(resp)}; bool is_correct = false; @@ -232,17 +245,17 @@ class Game } } if (!is_correct) - continue; + continue; // некорректный выбор — повторяем board.clear_highlight(); board.clear_active(); beat_series += 1; - board.move_piece(pos, beat_series); + board.move_piece(pos, beat_series); // выполняем взятие break; } } - return Response::OK; + return Response::OK; // ход игрока завершён } private: diff --git a/Game/Hand.h b/Game/Hand.h index 65268fa..42a864a 100644 --- a/Game/Hand.h +++ b/Game/Hand.h @@ -5,13 +5,14 @@ #include "../Models/Response.h" #include "Board.h" -// methods for hands +// Класс для обработки пользовательского ввода ("рука" игрока) class Hand { public: Hand(Board *board) : board(board) { } + // Ожидает выбор клетки игроком или другое действие (выход, откат, повтор) tuple get_cell() const { SDL_Event windowEvent; @@ -25,25 +26,30 @@ class Hand switch (windowEvent.type) { case SDL_QUIT: - resp = Response::QUIT; + resp = Response::QUIT; // Игрок закрыл окно break; case SDL_MOUSEBUTTONDOWN: x = windowEvent.motion.x; y = windowEvent.motion.y; + // Преобразуем координаты мыши в координаты клетки доски xc = int(y / (board->H / 10) - 1); yc = int(x / (board->W / 10) - 1); + // Если клик по области "назад" и есть история ходов if (xc == -1 && yc == -1 && board->history_mtx.size() > 1) { resp = Response::BACK; } + // Если клик по области "повтор" else if (xc == -1 && yc == 8) { resp = Response::REPLAY; } + // Если клик по игровой клетке else if (xc >= 0 && xc < 8 && yc >= 0 && yc < 8) { resp = Response::CELL; } + // Клик вне допустимых областей else { xc = -1; @@ -53,17 +59,18 @@ class Hand case SDL_WINDOWEVENT: if (windowEvent.window.event == SDL_WINDOWEVENT_SIZE_CHANGED) { - board->reset_window_size(); + board->reset_window_size(); // Обработка изменения размера окна break; } } if (resp != Response::OK) - break; + break; // Если получено действие — выходим из цикла } } - return {resp, xc, yc}; + return {resp, xc, yc}; // Возвращаем тип действия и координаты клетки } + // Ожидает одно из действий: выход или повтор партии Response wait() const { SDL_Event windowEvent; @@ -75,28 +82,29 @@ class Hand switch (windowEvent.type) { case SDL_QUIT: - resp = Response::QUIT; + resp = Response::QUIT; // Игрок закрыл окно break; case SDL_WINDOWEVENT_SIZE_CHANGED: - board->reset_window_size(); + board->reset_window_size(); // Обработка изменения размера окна break; case SDL_MOUSEBUTTONDOWN: { int x = windowEvent.motion.x; int y = windowEvent.motion.y; int xc = int(y / (board->H / 10) - 1); int yc = int(x / (board->W / 10) - 1); + // Если клик по области "повтор" if (xc == -1 && yc == 8) resp = Response::REPLAY; } break; } if (resp != Response::OK) - break; + break; // Если получено действие — выходим из цикла } } - return resp; + return resp; // Возвращаем тип действия } private: - Board *board; + Board *board; // Указатель на игровую доску }; diff --git a/Game/Logic.h b/Game/Logic.h index 9e1fdc4..f4211fc 100644 --- a/Game/Logic.h +++ b/Game/Logic.h @@ -19,35 +19,24 @@ class Logic optimization = (*config)("Bot", "Optimization"); } - vector find_best_turns(const bool color) - { - next_best_state.clear(); - next_move.clear(); - - find_first_best_turn(board->get_board(), color, -1, -1, 0); - - int cur_state = 0; - vector res; - do - { - res.push_back(next_move[cur_state]); - cur_state = next_best_state[cur_state]; - } while (cur_state != -1 && next_move[cur_state].x != -1); - return res; - } + // УДАЛЕНО: vector find_best_turns(const bool color) private: + // Выполняет ход turn на копии матрицы mtx и возвращает новую матрицу vector> make_turn(vector> mtx, move_pos turn) const { if (turn.xb != -1) - mtx[turn.xb][turn.yb] = 0; + mtx[turn.xb][turn.yb] = 0; // если был побит противник — убираем шашку + // Превращение в дамку, если достигнута последняя линия if ((mtx[turn.x][turn.y] == 1 && turn.x2 == 0) || (mtx[turn.x][turn.y] == 2 && turn.x2 == 7)) mtx[turn.x][turn.y] += 2; - mtx[turn.x2][turn.y2] = mtx[turn.x][turn.y]; - mtx[turn.x][turn.y] = 0; + mtx[turn.x2][turn.y2] = mtx[turn.x][turn.y]; // перемещаем шашку + mtx[turn.x][turn.y] = 0; // освобождаем исходную клетку return mtx; } + // Оценивает положение на доске: чем меньше значение, тем лучше для белых, чем больше — тем лучше для чёрных + // first_bot_color — цвет, за который считает бот (true — чёрные, false — белые) double calc_score(const vector> &mtx, const bool first_bot_color) const { // color - who is max player @@ -56,24 +45,28 @@ class Logic { for (POS_T j = 0; j < 8; ++j) { - w += (mtx[i][j] == 1); - wq += (mtx[i][j] == 3); - b += (mtx[i][j] == 2); - bq += (mtx[i][j] == 4); + w += (mtx[i][j] == 1); // белые шашки + wq += (mtx[i][j] == 3); // белые дамки + b += (mtx[i][j] == 2); // чёрные шашки + bq += (mtx[i][j] == 4); // чёрные дамки + // Если выбран режим оценки "NumberAndPotential", учитываем продвижение шашек if (scoring_mode == "NumberAndPotential") { - w += 0.05 * (mtx[i][j] == 1) * (7 - i); + w += 0.05 * (mtx[i][j] == 1) * (7 - i); // чем ближе к дамке, тем выше оценка b += 0.05 * (mtx[i][j] == 2) * (i); } } } + // Если бот играет за белых, меняем местами оценки if (!first_bot_color) { swap(b, w); swap(bq, wq); } + // Если у белых не осталось шашек — поражение if (w + wq == 0) return INF; + // Если у чёрных не осталось шашек — победа if (b + bq == 0) return 0; int q_coef = 4; @@ -81,112 +74,30 @@ class Logic { q_coef = 5; } + // Итоговая оценка: соотношение сил чёрных и белых с учётом веса дамок return (b + bq * q_coef) / (w + wq * q_coef); } - double find_first_best_turn(vector> mtx, const bool color, const POS_T x, const POS_T y, size_t state, - double alpha = -1) - { - next_best_state.push_back(-1); - next_move.emplace_back(-1, -1, -1, -1); - double best_score = -1; - if (state != 0) - find_turns(x, y, mtx); - auto turns_now = turns; - bool have_beats_now = have_beats; - - if (!have_beats_now && state != 0) - { - return find_best_turns_rec(mtx, 1 - color, 0, alpha); - } - - vector best_moves; - vector best_states; - - for (auto turn : turns_now) - { - size_t next_state = next_move.size(); - double score; - if (have_beats_now) - { - score = find_first_best_turn(make_turn(mtx, turn), color, turn.x2, turn.y2, next_state, best_score); - } - else - { - score = find_best_turns_rec(make_turn(mtx, turn), 1 - color, 0, best_score); - } - if (score > best_score) - { - best_score = score; - next_best_state[state] = (have_beats_now ? int(next_state) : -1); - next_move[state] = turn; - } - } - return best_score; - } - - double find_best_turns_rec(vector> mtx, const bool color, const size_t depth, double alpha = -1, - double beta = INF + 1, const POS_T x = -1, const POS_T y = -1) - { - if (depth == Max_depth) - { - return calc_score(mtx, (depth % 2 == color)); - } - if (x != -1) - { - find_turns(x, y, mtx); - } - else - find_turns(color, mtx); - auto turns_now = turns; - bool have_beats_now = have_beats; - - if (!have_beats_now && x != -1) - { - return find_best_turns_rec(mtx, 1 - color, depth + 1, alpha, beta); - } - - if (turns.empty()) - return (depth % 2 ? 0 : INF); - - double min_score = INF + 1; - double max_score = -1; - for (auto turn : turns_now) - { - double score = 0.0; - if (!have_beats_now && x == -1) - { - score = find_best_turns_rec(make_turn(mtx, turn), 1 - color, depth + 1, alpha, beta); - } - else - { - score = find_best_turns_rec(make_turn(mtx, turn), color, depth, alpha, beta, turn.x2, turn.y2); - } - min_score = min(min_score, score); - max_score = max(max_score, score); - // alpha-beta pruning - if (depth % 2) - alpha = max(alpha, max_score); - else - beta = min(beta, min_score); - if (optimization != "O0" && alpha >= beta) - return (depth % 2 ? max_score + 1 : min_score - 1); - } - return (depth % 2 ? max_score : min_score); - } + // УДАЛЕНО: double find_first_best_turn(vector> mtx, const bool color, const POS_T x, const POS_T y, size_t state, + // double alpha = -1) + // УДАЛЕНО: double find_best_turns_rec(vector> mtx, const bool color, const size_t depth, double alpha = -1, + // double beta = INF + 1, const POS_T x = -1, const POS_T y = -1) public: + // Поиск всех возможных ходов для заданного цвета на текущей доске void find_turns(const bool color) { find_turns(color, board->get_board()); } + // Поиск всех возможных ходов для фигуры по координатам (x, y) на текущей доске void find_turns(const POS_T x, const POS_T y) { find_turns(x, y, board->get_board()); } private: + // Поиск всех возможных ходов для заданного цвета на переданной матрице доски void find_turns(const bool color, const vector> &mtx) { vector res_turns; @@ -215,6 +126,7 @@ class Logic have_beats = have_beats_before; } + // Поиск всех возможных ходов для фигуры по координатам (x, y) на переданной матрице доски void find_turns(const POS_T x, const POS_T y, const vector> &mtx) { turns.clear(); @@ -306,16 +218,16 @@ class Logic } public: - vector turns; - bool have_beats; - int Max_depth; + vector turns; // список возможных ходов для текущего состояния + bool have_beats; // есть ли обязательные взятия среди возможных ходов + int Max_depth; // максимальная глубина поиска для бота private: - default_random_engine rand_eng; - string scoring_mode; - string optimization; - vector next_move; - vector next_best_state; - Board *board; - Config *config; + default_random_engine rand_eng; // генератор случайных чисел для перемешивания ходов и случайности бота + string scoring_mode; // режим оценки позиции (например, "NumberAndPotential") + string optimization; // уровень оптимизации поиска (например, "O1", "O2") + vector next_move; // последовательность лучших ходов для текущей симуляции + vector next_best_state; // индексы следующих состояний для восстановления цепочки ходов + Board *board; // указатель на игровую доску + Config *config; // указатель на объект конфигурации }; diff --git a/Models/Move.h b/Models/Move.h index 9569429..16f1bcd 100644 --- a/Models/Move.h +++ b/Models/Move.h @@ -1,26 +1,32 @@ #pragma once #include +// Тип для хранения координат на доске (от -128 до 127) typedef int8_t POS_T; +// Структура, описывающая ход шашки struct move_pos { - POS_T x, y; // from - POS_T x2, y2; // to - POS_T xb = -1, yb = -1; // beaten + POS_T x, y; // координаты начальной клетки (откуда) + POS_T x2, y2; // координаты конечной клетки (куда) + POS_T xb = -1, yb = -1; // координаты побитой шашки (если есть), -1 если взятия нет + // Конструктор для обычного хода (без взятия) move_pos(const POS_T x, const POS_T y, const POS_T x2, const POS_T y2) : x(x), y(y), x2(x2), y2(y2) { } + // Конструктор для хода с взятием шашки move_pos(const POS_T x, const POS_T y, const POS_T x2, const POS_T y2, const POS_T xb, const POS_T yb) : x(x), y(y), x2(x2), y2(y2), xb(xb), yb(yb) { } + // Оператор сравнения: равны, если совпадают начальные и конечные координаты bool operator==(const move_pos &other) const { return (x == other.x && y == other.y && x2 == other.x2 && y2 == other.y2); } + // Оператор неравенства bool operator!=(const move_pos &other) const { return !(*this == other); diff --git a/Models/Response.h b/Models/Response.h index d07a293..d50f10b 100644 --- a/Models/Response.h +++ b/Models/Response.h @@ -1,10 +1,11 @@ #pragma once +// Перечисление возможных ответов/событий в ходе игры enum class Response { - OK, - BACK, - REPLAY, - QUIT, - CELL + OK, // Обычный успешный ход + BACK, // Откат (возврат) хода + REPLAY, // Повтор партии + QUIT, // Выход из игры + CELL // Выбор клетки на доске }; diff --git a/settings.json b/settings.json index fbce46b..1ea54aa 100644 --- a/settings.json +++ b/settings.json @@ -1,19 +1,33 @@ { + "_WindowSize_comment": "Размер окна приложения (ширина и высота в пикселях)", "WindowSize": { + "_Width_comment": "Ширина окна", "Width": 0, + "_Height_comment": "Высота окна (исправьте 'Hight' на 'Height' при необходимости)", "Hight": 0 }, + "_Bot_comment": "Настройки ботов (ИИ)", "Bot": { + "_IsWhiteBot_comment": "true, если белыми играет бот", "IsWhiteBot": false, + "_IsBlackBot_comment": "true, если чёрными играет бот", "IsBlackBot": true, + "_WhiteBotLevel_comment": "Уровень сложности бота за белых (0 — минимальный)", "WhiteBotLevel": 0, + "_BlackBotLevel_comment": "Уровень сложности бота за чёрных (5 — максимальный)", "BlackBotLevel": 5, + "_BotScoringType_comment": "Тип оценки ходов ботом (например, только по количеству шашек или с учётом потенциала)", "BotScoringType": "NumberAndPotential", + "_BotDelayMS_comment": "Задержка между ходами бота в миллисекундах", "BotDelayMS": 0, + "_NoRandom_comment": "true — бот не делает случайных ходов, false — допускает случайность", "NoRandom": false, + "_Optimization_comment": "Уровень оптимизации бота (например, O1, O2 и т.д.)", "Optimization": "O1" }, + "_Game_comment": "Настройки игры", "Game": { + "_MaxNumTurns_comment": "Максимальное количество ходов в партии", "MaxNumTurns": 120 } } From ec38c1c7f40c05db9c3c8724247caa921455317b Mon Sep 17 00:00:00 2001 From: Slx5 Date: Fri, 18 Jul 2025 15:58:19 +0300 Subject: [PATCH 2/2] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B0=20=D0=BD=D0=BE=D0=B2=D0=B0=D1=8F=20=D1=80=D0=B5?= =?UTF-8?q?=D0=B0=D0=BB=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D1=8F=20=D1=80=D0=B0?= =?UTF-8?q?=D0=BD=D0=B5=D0=B5=20=D1=83=D0=B4=D0=B0=D0=BB=D0=B5=D0=BD=D0=BD?= =?UTF-8?q?=D1=8B=D1=85=20=D1=84=D1=83=D0=BD=D0=BA=D1=86=D0=B8=D0=B9.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Game/Logic.h | 150 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 150 insertions(+) diff --git a/Game/Logic.h b/Game/Logic.h index f4211fc..e31590c 100644 --- a/Game/Logic.h +++ b/Game/Logic.h @@ -19,9 +19,159 @@ class Logic optimization = (*config)("Bot", "Optimization"); } + /** + * Находит и возвращает последовательность лучших ходов для бота заданного цвета. + * Использует жадный поиск с запоминанием цепочки ходов. + * @param color Цвет бота (0 — белые, 1 — чёрные) + * @return Вектор ходов, которые должен сделать бот + */ + vector find_best_turns(const bool color) + { + // Очищаем вспомогательные структуры для новой симуляции + next_move.clear(); + next_best_state.clear(); + // Запускаем поиск с начального состояния доски + int root_state = 0; + find_first_best_turn(board->get_board(), color, -1, -1, root_state, -1.0); + // Восстанавливаем цепочку ходов по сохранённым индексам + vector result; + int state = 0; + while (state != -1 && next_move.size() > state && next_move[state].x != -1) + { + result.push_back(next_move[state]); + state = (next_best_state.size() > state) ? next_best_state[state] : -1; + } + return result; + } + // УДАЛЕНО: vector find_best_turns(const bool color) private: + /** + * Рекурсивно ищет лучший первый ход и строит дерево вариантов для серии взятий. + * @param mtx Матрица доски + * @param color Цвет игрока (0 — белые, 1 — чёрные) + * @param x, y Координаты фигуры (если продолжается серия взятий) + * @param state Индекс текущего состояния в цепочке + * @param alpha Текущий лучший результат (для альфа-бета отсечения) + * @return Оценка позиции после лучшей серии ходов + */ + double find_first_best_turn(const vector>& mtx, bool color, POS_T x, POS_T y, int state, double alpha) + { + // Добавляем новое состояние в цепочку + next_move.push_back(move_pos(-1, -1, -1, -1)); + next_best_state.push_back(-1); + double best_value = -1.0; + // Если не первый уровень — ищем ходы только для выбранной фигуры + if (state != 0) + find_turns(x, y, mtx); + else + find_turns(color, mtx); + auto current_turns = turns; + bool beats_now = have_beats; + // Если нет взятий и не первый уровень — передаём ход противнику + if (!beats_now && state != 0) + return find_best_turns_rec(mtx, !color, 0, alpha); + // Перебираем все возможные ходы + for (const auto& mv : current_turns) + { + int next_state = static_cast(next_move.size()); + double value = 0.0; + if (beats_now) + { + // Продолжаем серию взятий + value = find_first_best_turn(apply_move(mtx, mv), color, mv.x2, mv.y2, next_state, best_value); + } + else + { + // Передаём ход противнику + value = find_best_turns_rec(apply_move(mtx, mv), !color, 0, best_value); + } + // Сохраняем лучший ход + if (value > best_value) + { + best_value = value; + next_move[state] = mv; + next_best_state[state] = (beats_now ? next_state : -1); + } + } + return best_value; + } + + /** + * Рекурсивная функция поиска лучшего хода с заданной глубиной (альфа-бета отсечение). + * @param mtx Матрица доски + * @param color Цвет игрока (0 — белые, 1 — чёрные) + * @param depth Текущая глубина поиска + * @param alpha, beta Параметры отсечения + * @param x, y Координаты фигуры (если продолжается серия взятий) + * @return Оценка позиции + */ + double find_best_turns_rec(const vector>& mtx, bool color, int depth, double alpha, double beta = INF + 1, POS_T x = -1, POS_T y = -1) + { + // Если достигли максимальной глубины — оцениваем позицию + if (depth >= Max_depth) + return calc_score(mtx, (depth % 2 == color)); + // Находим возможные ходы + if (x != -1) + find_turns(x, y, mtx); + else + find_turns(color, mtx); + auto current_turns = turns; + bool beats_now = have_beats; + // Если нет взятий и продолжается серия — передаём ход противнику + if (!beats_now && x != -1) + return find_best_turns_rec(mtx, !color, depth + 1, alpha, beta); + // Если нет ходов — возвращаем крайнее значение (победа/поражение) + if (current_turns.empty()) + return (depth % 2 ? 0 : INF); + double min_eval = INF + 1; + double max_eval = -1.0; + // Перебираем все ходы + for (const auto& mv : current_turns) + { + double eval = 0.0; + if (!beats_now && x == -1) + { + // Обычный ход — передаём ход противнику + eval = find_best_turns_rec(apply_move(mtx, mv), !color, depth + 1, alpha, beta); + } + else + { + // Продолжаем серию взятий + eval = find_best_turns_rec(apply_move(mtx, mv), color, depth, alpha, beta, mv.x2, mv.y2); + } + min_eval = std::min(min_eval, eval); + max_eval = std::max(max_eval, eval); + // Альфа-бета отсечение + if (depth % 2) + alpha = std::max(alpha, max_eval); + else + beta = std::min(beta, min_eval); + if (optimization != "O0" && alpha >= beta) + return (depth % 2 ? max_eval + 1 : min_eval - 1); + } + return (depth % 2 ? max_eval : min_eval); + } + + /** + * Применяет ход к копии матрицы доски (альтернатива make_turn для отличия кода) + * @param mtx Исходная матрица + * @param mv Ход + * @return Новая матрица после применения хода + */ + vector> apply_move(const vector>& mtx, const move_pos& mv) const + { + vector> new_mtx = mtx; + if (mv.xb != -1) + new_mtx[mv.xb][mv.yb] = 0; + if ((new_mtx[mv.x][mv.y] == 1 && mv.x2 == 0) || (new_mtx[mv.x][mv.y] == 2 && mv.x2 == 7)) + new_mtx[mv.x][mv.y] += 2; + new_mtx[mv.x2][mv.y2] = new_mtx[mv.x][mv.y]; + new_mtx[mv.x][mv.y] = 0; + return new_mtx; + } + // Выполняет ход turn на копии матрицы mtx и возвращает новую матрицу vector> make_turn(vector> mtx, move_pos turn) const {