commit e4f46053509b70ac12f3a18a56944ba009a9eb82 Author: Alexander Zhirov Date: Mon May 1 00:30:15 2023 +0300 v1.0.0 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dfb20d7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.vscode +build +tictactoe diff --git a/README.md b/README.md new file mode 100644 index 0000000..d74c9bf --- /dev/null +++ b/README.md @@ -0,0 +1,39 @@ +# TicTacToe + +Крестики-нолики + +![game](images/game.png) + +## Сборка + +```sh +mkdir build +cd build +cmake -B . -S ../game +make +``` + +Основные данные, необходимые для запуска игры, находятся в каталоге [data](data/). + +### Для Windows + +#### Ключи для сборки + +`-lallegro_dialog-static -lallegro_image-static -lallegro_primitives-static -lallegro-static -ljpeg -lpng16 -lwebp -lwinmm -lopengl32 -lcomdlg32 -lgdi32 -lole32 -lshlwapi -lz -mwindows` + +#### Статическая сборка + +`-static` + +#### Дополнительные библиотеки для сборки + +- `jpeg` +- `png16` +- `webp` +- `winmm` +- `opengl32` +- `comdlg32` +- `gdi32` +- `ole32` +- `shlwapi` +- `z` diff --git a/data/o.png b/data/o.png new file mode 100644 index 0000000..761cd67 Binary files /dev/null and b/data/o.png differ diff --git a/data/x.png b/data/x.png new file mode 100644 index 0000000..8243d78 Binary files /dev/null and b/data/x.png differ diff --git a/game/CMakeLists.txt b/game/CMakeLists.txt new file mode 100644 index 0000000..d8c889b --- /dev/null +++ b/game/CMakeLists.txt @@ -0,0 +1,23 @@ +cmake_minimum_required(VERSION 3.0) +project(tictactoe) + +include_directories("lib/" "network/" "objects/") + +set(SRC_GAME + ai.cpp + main.cpp + map.cpp + parse_args.cpp) + +find_library(ALLEGRO_LIB NAMES allegro) +find_library(ALLEGRO_PRIMITIVES_LIB NAMES allegro_primitives) +find_library(ALLEGRO_DIALOG_LIB NAMES allegro_dialog) +find_library(ALLEGRO_IMAGE_LIB allegro_image) + +add_executable(${PROJECT_NAME} ${SRC_GAME}) + +target_link_libraries(${PROJECT_NAME} + ${ALLEGRO_LIB} + ${ALLEGRO_PRIMITIVES_LIB} + ${ALLEGRO_DIALOG_LIB} + ${ALLEGRO_IMAGE_LIB}) diff --git a/game/ai.cpp b/game/ai.cpp new file mode 100644 index 0000000..bd8aa16 --- /dev/null +++ b/game/ai.cpp @@ -0,0 +1,118 @@ +#include "ai.hpp" + +std::random_device rd; +std::mt19937 mt(rd()); + +void aiTurn(map *m) +{ + std::uniform_int_distribution dist(0, m->size - 1); + + if (aiWinCheck(m)) return; + if (humWinCheck(m)) return; + int x; + int y; + do { + y = dist(mt); + x = dist(mt); + } while (m->cells[x][y]->p != EMPTY); + setVal(m->cells[x][y], AI, true); +} + +bool aiWinCheck(map *m) +{ + for (int y = 0; y < m->size; y++) + { + for (int x = 0; x < m->size; x++) + { + cell *c = m->cells[y][x]; + + if (c->p == EMPTY) + { + setVal(c, AI, true); + if (checkWin(m, AI)) + return true; + setVal(c, EMPTY); + } + } + } + return false; +} + +bool humWinCheck(map *m) +{ + for (int y = 0; y < m->size; y++) + { + for (int x = 0; x < m->size; x++) + { + cell *c = m->cells[y][x]; + + if (c->p == EMPTY) + { + setVal(c, HUMAN, true); + if (checkWin(m, HUMAN)) + { + setVal(c, AI, true); + return true; + } + setVal(c, EMPTY); + } + } + } + + return false; +} + +void setVal(cell *c, PLAYER p, bool draw) +{ + c->p = p; + c->is_draw = draw; +} + +bool checkWin(map *m, PLAYER p) +{ + for (int y = 0; y < m->size; y++) + { + for (int x = 0; x < m->size; x++) + { + if (m->cells[y][x]->p == EMPTY) continue; + if (checkLine(m, y, x, 0, 1, m->toWin, p)) return true; + if (checkLine(m, y, x, 1, 1, m->toWin, p)) return true; + if (checkLine(m, y, x, 1, 0, m->toWin, p)) return true; + if (checkLine(m, y, x, -1, 1, m->toWin, p)) return true; + } + } + + return false; +} + +bool checkLine(map *m, int y, int x, int vy, int vx, int len, PLAYER p) +{ + const int endX = x + (len - 1) * vx; + const int endY = y + (len - 1) * vy; + + if (!isValid(m, endX, endY)) + return false; + + for (int i = 0; i < len; i++) + { + if (m->cells[y + i * vy][x + i * vx]->p != p) + return false; + } + + return true; +} + +bool isValid(map *m, int y, int x) +{ + return CHECK_DOT(y, m->size) && CHECK_DOT(x, m->size); +} + +bool isDraw(map *m) +{ + for (int y = 0; y < m->size; y++) + for (int x = 0; x < m->size; x++) + if (m->cells[y][x]->p == EMPTY) + return false; + return true; +} + diff --git a/game/ai.hpp b/game/ai.hpp new file mode 100644 index 0000000..405bc95 --- /dev/null +++ b/game/ai.hpp @@ -0,0 +1,20 @@ +#ifndef AI_HPP_ +#define AI_HPP_ + +#include +#include +#include +#include "map.hpp" + +#define CHECK_DOT(X, Y) ((X) >= 0 && (X) < (Y)) + +void aiTurn(map *); +bool aiWinCheck(map *); +bool humWinCheck(map *); +void setVal(cell *, PLAYER, bool = false); +bool checkWin(map *, PLAYER); +bool checkLine(map *, int, int, int, int, int, PLAYER); +bool isValid(map *, int, int); +bool isDraw(map *m); + +#endif diff --git a/game/main.cpp b/game/main.cpp new file mode 100644 index 0000000..bf87acf --- /dev/null +++ b/game/main.cpp @@ -0,0 +1,203 @@ +#include +#include +#include +#include +#include + +#include "ai.hpp" +#include "map.hpp" +#include "parse_args.hpp" + +int main(int argc, char **argv) +{ + int countKeys = 3; + + ra::key ks; // size + ra::key kw; // width/height window + ra::key km; // margin map + + ra::key *keys[countKeys] = {&ks, &kw, &km}; + + ra::parse_args(argc, argv, keys); + + int size = 3; + int window_wh = 600; + int margin_map = 20; + + if (ks.isset) + { + size = atoi(ks.arguments[0]); + if (size > 10 || size < 3) + size = 3; + } + + if (kw.isset) + { + window_wh = atoi(kw.arguments[0]); + if (window_wh > 1000 || window_wh < 300) + window_wh = 600; + } + + if (km.isset) + { + margin_map = atoi(km.arguments[0]); + if (margin_map > 100 || size < 20) + margin_map = 20; + } + + map *m = init_map(size, window_wh, margin_map); + + bool done = false; + bool redraw = true; + bool isdraw = true; + int FPS = 60; + bool move_ai = false; + + int mouse_x = 0; + int mouse_y = 0; + + ALLEGRO_DISPLAY *display = NULL; + ALLEGRO_EVENT_QUEUE *event_queue = NULL; + ALLEGRO_TIMER *timer = NULL; + ALLEGRO_BITMAP *img_x = NULL; + ALLEGRO_BITMAP *img_o = NULL; + + if (!al_init()) + { + al_show_native_message_box(NULL, NULL, NULL, + "Не удается инициализировать allegro!", NULL, ALLEGRO_MESSAGEBOX_ERROR); + return (-1); + } + + display = al_create_display(window_wh, window_wh); + + if(!display) + { + al_show_native_message_box(NULL, NULL, "Ошибка!", "Не удается инициализировать дисплей!", NULL, ALLEGRO_MESSAGEBOX_ERROR); + return (-1); + } + + al_init_primitives_addon(); + al_install_keyboard(); + al_install_mouse(); + al_init_image_addon(); + + img_x = al_load_bitmap("data/x.png"); + + if(!img_x) + { + al_show_native_message_box(display, NULL, "Ошибка!", "Не удается инициализировать \"x.png\"", NULL, ALLEGRO_MESSAGEBOX_ERROR); + return (-1); + } + + img_o = al_load_bitmap("data/o.png"); + + if(!img_o) + { + al_show_native_message_box(display, NULL, "Ошибка!", "Не удается инициализировать \"o.png\"!", NULL, ALLEGRO_MESSAGEBOX_ERROR); + return (-1); + } + + timer = al_create_timer(1.0 / FPS); + event_queue = al_create_event_queue(); + + if(!event_queue) + { + al_show_native_message_box(display, NULL, "Ошибка!", + "Не удается инициализировать событие!", NULL, ALLEGRO_MESSAGEBOX_ERROR); + return (-1); + } + + al_register_event_source(event_queue, al_get_keyboard_event_source()); + al_register_event_source(event_queue, al_get_display_event_source(display)); + al_register_event_source(event_queue, al_get_timer_event_source(timer)); + al_register_event_source(event_queue, al_get_mouse_event_source()); + + al_start_timer(timer); + + while (!done) + { + ALLEGRO_EVENT ev; + al_wait_for_event(event_queue, &ev); + + if (ev.type == ALLEGRO_EVENT_KEY_UP) + { + switch (ev.keyboard.keycode) + { + case ALLEGRO_KEY_ESCAPE: + if (exit_game(display)) + { + done = true; + continue; + } + al_flush_event_queue(event_queue); + } + } + else if (ev.type == ALLEGRO_EVENT_DISPLAY_CLOSE) + { + if (exit_game(display)) + { + done = true; + continue; + } + al_flush_event_queue(event_queue); + } + else if (ev.type == ALLEGRO_EVENT_MOUSE_BUTTON_UP) + { + if (isdraw && enter_cell(m, mouse_x, mouse_y, HUMAN)) + { + move_ai = true; + isdraw = false; + } + } + else if (ev.type == ALLEGRO_EVENT_MOUSE_AXES) + { + mouse_x = ev.mouse.x; + mouse_y = ev.mouse.y; + } + else if (ev.type == ALLEGRO_EVENT_TIMER) + { + if (isdraw) + { + if (game_check(m, display)) + { + al_flush_event_queue(event_queue); + done = true; + continue; + } + + if (move_ai) + { + aiTurn(m); + move_ai = false; + isdraw = false; + } + } + + select_cell(m, mouse_x, mouse_y); + + redraw = true; + } + + if (redraw && al_is_event_queue_empty(event_queue)) + { + redraw = false; + isdraw = true; + + draw_map(m, img_x, img_o); + + al_flip_display(); + al_clear_to_color(al_map_rgb(255, 255, 255)); + } + } + + free_map(m); + + al_destroy_display(display); + al_destroy_bitmap(img_x); + al_destroy_bitmap(img_o); + al_destroy_timer(timer); + al_destroy_event_queue(event_queue); + + return (0); +} diff --git a/game/map.cpp b/game/map.cpp new file mode 100644 index 0000000..11bba90 --- /dev/null +++ b/game/map.cpp @@ -0,0 +1,256 @@ +#include "map.hpp" +#include +#include +#include +#include "ai.hpp" + +map *init_map(const int size, const int window_wh, const int margin_map) +{ + map *m = (map *)malloc(sizeof(map)); + + float map_wh = 0; // размер карты с учётом отступов + float cell_width = 0; // ширина ячейки + float cell_margin = 0; // отступ внутри ячейки + float line_count = 0; // количество линий на одно направление + float line_width = 0; // ширина линии + float cell_sym_width = 0; // ширина сивола внутри ячейки + + m->size = size; + m->window_wh = window_wh; + m->margin_map = margin_map; + m->toWin = size; + + line_count = size - 1; + map_wh = window_wh - margin_map * 2; + line_width = map_wh * 0.0036; + cell_width = map_wh / size; + cell_margin = cell_width * 0.0538; + cell_sym_width = cell_width - (line_width * 2) - (cell_margin * 2); + + m->sym_width = cell_sym_width; + + m->cells = (cell ***)malloc(sizeof(cell **) * (size * size)); + + for (int i = 0; i < size; ++i) + { + m->cells[i] = (cell **)malloc(sizeof(cell *) * size); + + for (int j = 0; j < size; ++j) + { + m->cells[i][j] = create_cell(j, i, cell_width, cell_margin, line_width, margin_map); + } + } + + m->grid = (line ***)malloc(sizeof(line **) * (line_count * line_count)); + + for (int i = 0; i < 2; ++i) + { + m->grid[i] = (line **)malloc(sizeof(line *) * line_count); + + DIRECTION d = (i == 0 ? HORIZONTAL : VERTICAL); + + for (int j = 0; j < line_count; ++j) + { + m->grid[i][j] = create_line(d, j, line_width, map_wh, cell_width, margin_map); + } + } + + return m; +} + +cell *create_cell(const float row, const float col, const float cell_width, const float cell_margin, const float line_width, const float margin_map) +{ + cell *c = (cell *)malloc(sizeof(cell)); + c->select = false; + c->is_draw = false; + c->pos_x = row * cell_width + margin_map; + c->pos_y = col * cell_width + margin_map; + c->width = cell_width; + c->sym_pos_x = c->pos_x + line_width + cell_margin; + c->sym_pos_y = c->pos_y + line_width + cell_margin; + c->p = EMPTY; + return c; +} + +line *create_line(DIRECTION d, const float row, const float line_width, const float map_wh, const float cell_width, const float margin_map) +{ + line *l = (line *)malloc(sizeof(line)); + l->d = d; + l->height = map_wh; + l->width = line_width; + if (d == HORIZONTAL) + { + l->pos_x = margin_map; + l->pos_y = (row + 1) * cell_width + margin_map - line_width; + } + else + { + l->pos_x = (row + 1) * cell_width + margin_map - line_width; + l->pos_y = margin_map; + } + + return l; +} + +void draw_map(const map *m, ALLEGRO_BITMAP *bx, ALLEGRO_BITMAP *bo) +{ + int size = m->size; + + int swh = al_get_bitmap_width(bx); + + for (int i = 0; i < size; ++i) + { + for (int j = 0; j < size; ++j) + { + cell *c = m->cells[i][j]; + + if (c->is_draw) + { + if (c->p == HUMAN) + al_draw_scaled_bitmap(bx, 0, 0, swh, swh, c->sym_pos_x, c->sym_pos_y, m->sym_width, m->sym_width, 0); + else + al_draw_scaled_bitmap(bo, 0, 0, swh, swh, c->sym_pos_x, c->sym_pos_y, m->sym_width, m->sym_width, 0); + } + + if (c->select) + { + if (c->is_draw) + al_draw_tinted_scaled_bitmap(bx, al_map_rgba_f(255, 0, 0, 0.3), 0, 0, swh, swh, c->sym_pos_x, c->sym_pos_y, m->sym_width, m->sym_width, 0); + else + al_draw_tinted_scaled_bitmap(bx, al_map_rgba_f(0, 255, 0, 0.3), 0, 0, swh, swh, c->sym_pos_x, c->sym_pos_y, m->sym_width, m->sym_width, 0); + } + } + } + + for (int i = 0; i < 2; ++i) + { + for (int j = 0; j < size - 1; ++j) + { + line *l = m->grid[i][j]; + + if (l->d == HORIZONTAL) + al_draw_filled_rectangle(l->pos_x, l->pos_y, l->pos_x + l->height, l->pos_y + l->width, al_map_rgb(0, 0, 0)); + else + al_draw_filled_rectangle(l->pos_x, l->pos_y, l->pos_x + l->width, l->pos_y + l->height, al_map_rgb(0, 0, 0)); + } + } +} + +void select_cell(map *m, const int mouse_x, const int mouse_y) +{ + int size = m->size; + + for (int i = 0; i < size; ++i) + { + for (int j = 0; j < size; ++j) + { + cell *c = m->cells[i][j]; + + if ((mouse_x >= c->pos_x && mouse_y >= c->pos_y) && + (mouse_x <= (c->pos_x + c->width) && mouse_y <= (c->pos_y + c->width)) && c->p != HUMAN) + c->select = true; + else + c->select = false; + } + } +} + +bool enter_cell(map *m, const int mouse_x, const int mouse_y, PLAYER p) +{ + int size = m->size; + + for (int i = 0; i < size; ++i) + { + for (int j = 0; j < size; ++j) + { + cell *c = m->cells[i][j]; + + if (!c->is_draw && (mouse_x >= c->pos_x && mouse_y >= c->pos_y) && + (mouse_x <= (c->pos_x + c->width) && mouse_y <= (c->pos_y + c->width))) + { + c->is_draw = true; + c->p = p; + return true; + } + } + } + + return false; +} + +void clear_map(map *m) +{ + int size = m->size; + + for (int i = 0; i < size; ++i) + { + for (int j = 0; j < size; ++j) + { + cell *c = m->cells[i][j]; + + c->is_draw = false; + c->select = false; + c->p = EMPTY; + } + } +} + +bool game_check(map *m, ALLEGRO_DISPLAY *d) +{ + int answer = -1; + + if (checkWin(m, HUMAN)) + answer = al_show_native_message_box(d, "Игра окончена!", "Вы победили!", + "Начать игру сначала?", NULL, ALLEGRO_MESSAGEBOX_YES_NO); + + if (checkWin(m, AI)) + answer = al_show_native_message_box(d, "Игра окончена!", "Вы проиграли!", + "Начать игру сначала?", NULL, ALLEGRO_MESSAGEBOX_YES_NO); + + if (isDraw(m)) + answer = al_show_native_message_box(d, "Игра окончена!", "Ничья!", + "Начать игру сначала?", NULL, ALLEGRO_MESSAGEBOX_YES_NO); + + if (answer == 1) + clear_map(m); + else if (answer == 0 || answer == 2) + return true; + + return false; +} + +void free_map(map *m) +{ + for (int i = 0; i < m->size; ++i) + { + for (int j = 0; j < m->size; ++j) + { + free(m->cells[i][j]); + } + free(m->cells[i]); + } + free(m->cells); + + for (int i = 0; i < 2; ++i) + { + for (int j = 0; j < m->size - 1; ++j) + { + free(m->grid[i][j]); + } + free(m->grid[i]); + } + free(m->grid); + + free(m); +} + +bool exit_game(ALLEGRO_DISPLAY *d) +{ + int answer = al_show_native_message_box(d, NULL, "Выход из игры", + "Вы хотите закончить игру?", NULL, ALLEGRO_MESSAGEBOX_YES_NO); + + if (answer == 1) + return true; + else + return false; +} diff --git a/game/map.hpp b/game/map.hpp new file mode 100644 index 0000000..c6a383a --- /dev/null +++ b/game/map.hpp @@ -0,0 +1,53 @@ +#ifndef MAP_HPP_ +#define MAP_HPP_ + +#include + +typedef enum {HUMAN, AI, EMPTY} PLAYER; + +typedef struct +{ + bool is_draw; + bool select; + float pos_x; + float pos_y; + float width; + float sym_pos_x; + float sym_pos_y; + PLAYER p; +} cell; + +typedef enum {HORIZONTAL, VERTICAL} DIRECTION; + +typedef struct +{ + DIRECTION d; + float pos_x; + float pos_y; + float width; + float height; +} line; + +typedef struct +{ + cell ***cells; + line ***grid; + int toWin; + int size; + int window_wh; + int margin_map; + int sym_width; +} map; + +map *init_map(const int, const int, const int); +cell *create_cell(const float, const float, const float, const float, const float, const float); +line *create_line(DIRECTION, const float, const float, const float, const float, const float); +void draw_map(const map *, ALLEGRO_BITMAP *, ALLEGRO_BITMAP *); +void select_cell(map *, const int, const int); +bool enter_cell(map *, const int, const int, PLAYER); +bool game_check(map *, ALLEGRO_DISPLAY *); +void clear_map(map *); +void free_map(map *); +bool exit_game(ALLEGRO_DISPLAY *); + +#endif diff --git a/game/parse_args.cpp b/game/parse_args.cpp new file mode 100644 index 0000000..f58cb65 --- /dev/null +++ b/game/parse_args.cpp @@ -0,0 +1,49 @@ +#include "parse_args.hpp" + +[[ noreturn ]] void ra::print_usage_and_exit(int code) +{ + puts("Использование: lesson_8 [option] [arguments] ...\n"); + puts(" -h, --help Получить информацию об использовании"); + puts(" -s, --size Размер сетки N*N"); + puts(" -w, --width Ширина/высота игрового окна"); + puts(" -m, --margin Размер внутреннего отступа от границы окна до игрового поля"); + exit(code); +} + +void ra::parse_args(int argc, char *argv[], key **keys) +{ + int next_option = 0; + + do{ + next_option = getopt_long(argc, argv, short_options, long_options, nullptr); + + switch(next_option) + { + case 's': + ra::get_argument(keys[ksize]); + break; + case 'w': + ra::get_argument(keys[kwidth]); + break; + case 'm': + ra::get_argument(keys[kmargin]); + break; + case 'h': + ra::print_usage_and_exit(0); + break; + case '?': + ra::print_usage_and_exit(1); + break; + } + } while (next_option != -1); +} + +void ra::get_argument(key *curKey) +{ + if (curKey->isset) + ra::print_usage_and_exit(3); + + curKey->arguments[0] = optarg; + curKey->isset = true; + curKey->count = 1; +} diff --git a/game/parse_args.hpp b/game/parse_args.hpp new file mode 100644 index 0000000..773fda0 --- /dev/null +++ b/game/parse_args.hpp @@ -0,0 +1,37 @@ +#ifndef PARSE_ARGS_HPP_ +#define PARSE_ARGS_HPP_ + +#include +#include + +namespace ra // read arguments +{ + enum keys {ksize, kwidth, kmargin}; + + typedef struct + { + bool required = false; // Ключ является обязательным для установки + bool isset = false; // Ключ был установлен при запуске программы + int count = 0; // Количество аргументов переданных для текущего ключа + char *arguments[1] = {nullptr}; // Переданные аргументы (до 10 аругментов на один ключ) + } key; + + const char* const short_options = "hs:w:m:"; + + const struct option long_options[] = + { + { "help", 0, nullptr, 'h'}, + { "size", 1, nullptr, 's'}, + { "width", 1, nullptr, 'w'}, + { "margin", 1, nullptr, 'm'}, + { nullptr, 0, nullptr, 0} + }; + + [[ noreturn ]] void print_usage_and_exit(int); // Напечатать справку и выйти с кодом ошибки + void parse_args(int, char **, key **); // Прочитать все ключи + void get_argument(key *); // Получить аргумент ключа +} + + + +#endif diff --git a/images/game.png b/images/game.png new file mode 100644 index 0000000..af04779 Binary files /dev/null and b/images/game.png differ