This commit is contained in:
Alexander Zhirov 2023-05-01 01:22:58 +03:00
commit f0e7533e65
15 changed files with 835 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
.vscode
build
tictactoe

50
README.md Normal file
View File

@ -0,0 +1,50 @@
# TicTacToe
Крестики-нолики
![game](images/game.png)
```sh
~ $ ./tictactoe --help
Использование: tictactoe [option] [arguments] ...
-h, --help Получить информацию об использовании
-s, --size <count columns> Размер сетки N*N
-w, --width <size> Ширина/высота игрового окна
-m, --margin <size> Размер внутреннего отступа от границы окна до игрового поля
-v, --version Версия TicTacToe
```
## Сборка
```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`

BIN
data/o.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

BIN
data/x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

22
game/CMakeLists.txt Normal file
View File

@ -0,0 +1,22 @@
cmake_minimum_required(VERSION 3.0)
project(tictactoe)
set(SRC_GAME
ai.cpp
main.cpp
map.cpp
parse_args.cpp
version.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})

118
game/ai.cpp Normal file
View File

@ -0,0 +1,118 @@
#include "ai.hpp"
std::random_device rd;
std::mt19937 mt(rd());
void aiTurn(map *m)
{
std::uniform_int_distribution<int> 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;
}

20
game/ai.hpp Normal file
View File

@ -0,0 +1,20 @@
#ifndef AI_HPP_
#define AI_HPP_
#include <iostream>
#include <random>
#include <algorithm>
#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

211
game/main.cpp Normal file
View File

@ -0,0 +1,211 @@
#include <iostream>
#include <allegro5/allegro.h>
#include <allegro5/allegro_native_dialog.h>
#include <allegro5/allegro_primitives.h>
#include <allegro5/allegro_image.h>
#include "ai.hpp"
#include "map.hpp"
#include "parse_args.hpp"
#include "version.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 kv; // version
ra::key *keys[countKeys] = {&ks, &kw, &km, &kv};
ra::parse_args(argc, argv, keys);
int size = 3;
int window_wh = 600;
int margin_map = 20;
if (kv.isset)
{
std::cout << "TicTacToe " << version << std::endl;
return 0;
}
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;
}

256
game/map.cpp Normal file
View File

@ -0,0 +1,256 @@
#include "map.hpp"
#include <cstdlib>
#include <allegro5/allegro_primitives.h>
#include <allegro5/allegro_native_dialog.h>
#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;
}

53
game/map.hpp Normal file
View File

@ -0,0 +1,53 @@
#ifndef MAP_HPP_
#define MAP_HPP_
#include <allegro5/allegro.h>
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

53
game/parse_args.cpp Normal file
View File

@ -0,0 +1,53 @@
#include "parse_args.hpp"
[[ noreturn ]] void ra::print_usage_and_exit(int code)
{
puts("Использование: tictactoe [option] [arguments] ...\n");
puts(" -h, --help Получить информацию об использовании");
puts(" -s, --size <count columns> Размер сетки N*N");
puts(" -w, --width <size> Ширина/высота игрового окна");
puts(" -m, --margin <size> Размер внутреннего отступа от границы окна до игрового поля");
puts(" -v, --version Версия TicTacToe\n");
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 'v':
ra::get_argument(keys[kversion]);
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;
}

38
game/parse_args.hpp Normal file
View File

@ -0,0 +1,38 @@
#ifndef PARSE_ARGS_HPP_
#define PARSE_ARGS_HPP_
#include <iostream>
#include <getopt.h>
namespace ra // read arguments
{
enum keys {ksize, kwidth, kmargin, kversion};
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:v";
const struct option long_options[] =
{
{ "help", 0, nullptr, 'h'},
{ "size", 1, nullptr, 's'},
{ "width", 1, nullptr, 'w'},
{ "margin", 1, nullptr, 'm'},
{ "version", 0, nullptr, 'v'},
{ nullptr, 0, nullptr, 0}
};
[[ noreturn ]] void print_usage_and_exit(int); // Напечатать справку и выйти с кодом ошибки
void parse_args(int, char **, key **); // Прочитать все ключи
void get_argument(key *); // Получить аргумент ключа
}
#endif

3
game/version.cpp Normal file
View File

@ -0,0 +1,3 @@
#include "version.hpp"
std::string version = "v1.0.0";

8
game/version.hpp Normal file
View File

@ -0,0 +1,8 @@
#ifndef VERSION_HPP_
#define VERSION_HPP_
#include <string>
extern std::string version;
#endif

BIN
images/game.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB