diff --git a/README_RUS.md b/README_RUS.md index e4f84dc..0a9de77 100644 --- a/README_RUS.md +++ b/README_RUS.md @@ -1,81 +1,6 @@ # BrickGame Тетрис Резюме: в данном проекте тебе предстоит реализовать игру «Тетрис» на языке программирования С с использованием структурного подхода. -💡 [Нажми сюда](https://new.oprosso.net/p/4cb31ec3f47a4596bc758ea1861fb624), **чтобы поделиться с нами обратной связью на этот проект**. Это анонимно и поможет нашей команде сделать обучение лучше. Рекомендуем заполнить опрос сразу после выполнения проекта. - -## Содержание - -- [BrickGame Тетрис](#brickgame-тетрис) - - [Содержание](#содержание) - - [Введение](#введение) - - [Chapter I ](#chapter-i-) - - [Общая информация](#общая-информация) - - [BrickGame](#brickgame) - - [История тетриса](#история-тетриса) - - [Конечные автоматы](#конечные-автоматы) - - [Фроггер](#фроггер) - - [Тетрис](#тетрис) - - [Chapter II ](#chapter-ii-) - - [Требования к проекту](#требования-к-проекту) - - [Часть 1. Основное задание](#часть-1-основное-задание) - - [Часть 2. Дополнительно. Подсчет очков и рекорд в игре](#часть-2-дополнительно-подсчет-очков-и-рекорд-в-игре) - - [Часть 3. Дополнительно. Механика уровней](#часть-3-дополнительно-механика-уровней) - -## Введение - -Для реализации игры «Тетрис» проект должен состоять из двух частей: библиотеки, реализующей логику работы игры, которую можно в будущем подключать к различным GUI, и терминального интерфейса. Логика работы библиотеки должна быть реализована с использованием конечных автоматов, одно из возможных описаний которого будет дано ниже. - -## Chapter I
-## Общая информация -### BrickGame - -BrickGame — популярная портативная консоль 90-ых годов с несколькими ~~тысячами~~ встроенными играми, разработана она была в Китае. Изначально эта игра была копией, разработанной в СССР и выпущенной Nintendo в рамках платформы GameBoy игры «Тетрис», но включала в себя также и множество других игр, которые добавлялись с течением времени. Консоль имела небольшой экранчик с игровым полем размера 10 х 20, представляющим из себя матрицу «пикселей». Справа от поля находилось табло с цифровой индикацией состояния текущей игры, рекордами и прочей дополнительной информацией. Самыми распространенными играми на BrickGame были: тетрис, танки, гонки, фроггер и змейка. - -![BrickGameConsole](misc/images/brickgame-console.jpg) - -### История тетриса - -«Тетрис» был написан Алексеем Пажитновым 6 июня 1984 года на компьютере Электроника-60. Игра представляла собой головоломку, построенную на использовании геометрических фигур «тетрамино», состоящих из четырех квадратов. Первая коммерческая версия игры была выпущена в Америке в 1987 году. В последующие годы «Тетрис» был портирован на множество различных устройств, в том числе на мобильные телефоны, калькуляторы и карманные персональные компьютеры. - -Наибольшую популярность приобрела реализация «Тетриса» для игровой консоли Game Boy и видеоприставки NES. Но кроме нее существуют различные версии игры. Например, есть версия с трехмерными фигурами или дуэльная версия, в которой два игрока получают одинаковые фигуры и пытаются обойти друг друга по очкам. - -### Конечные автоматы - -Конечный автомат (КА) в теории алгоритмов — математическая абстракция, модель дискретного устройства, имеющего один вход, один выход и в каждый момент времени находящегося в одном состоянии из множества возможных. - -При работе КА на вход последовательно поступают входные воздействия, а на выходе КА формирует выходные сигналы. Переход из одного внутреннего состояния КА в другое может происходить не только от внешнего воздействия, но и самопроизвольно. - -КА можно использовать для описания алгоритмов, позволяющих решать те или иные задачи, а также для моделирования практически любого процесса. Несколько примеров: - -- Логика искусственного интеллекта для игр; -- Синтаксический и лексический анализ; -- Сложные прикладные сетевые протоколы; -- Потоковая обработка данных. - -Ниже представлены примеры использования КА для формализации игровой логики нескольких игр из BrickGame. - -### Фроггер - -![Фроггер](misc/images/frogger-game.png) - -«Фроггер» — одна из поздних игр, выходящих на консолях Brickgame. Игра представляет собой игровое поле, по которому движутся бревна, и, перепрыгивая по ним, игроку необходимо перевести лягушку с одного берега на другой. Если игрок попадает в воду или лягушка уходит за пределы игрового поля, то лягушка погибает. Игра завершается, когда игрок доводит лягушку до другого берега или погибает последняя лягушка. - -Для формализации логики данной игры можно представить следующий вариант конечного автомата: - -![Конечный автомат фроггера](misc/images/frogger.jpg) - -Данный КА имеет следующие состояния: - -- Старт — состояние, в котором игра ждет, пока игрок нажмет кнопку готовности к игре. -- Спавн — состояние, в котором создается очередная лягушка. -- Перемещение — основное игровое состояние с обработкой ввода от пользователя: движение лягушки по полосе влево/право или прыжки вперед/назад. -- Сдвиг — состояние, которое наступает после истечения таймера, при котором все объекты на полосах сдвигаются вправо вместе с лягушкой. -- Столкновение — состояние, которое наступает, если после прыжка лягушка попадает в воду, или если после смещения бревен лягушка оказывается за пределами игрового поля. -- Достигнут другой берег — состояние, которое наступает при достижении лягушкой другого берега. -- Игра окончена — состояние, которое наступает после достижения другого берега или смерти последней лягушки. - -Пример реализации фроггера с использованием КА ты можешь найти в папке `code-samples`. - ### Тетрис ![Тетрис](misc/images/tetris-game.png) @@ -160,3 +85,50 @@ BrickGame — популярная портативная консоль 90-ых ### Часть 3. Дополнительно. Механика уровней Добавь в игру механику уровней. Каждый раз, когда игрок набирает 600 очков, уровень увеличивается на 1. Повышение уровня увеличивает скорость движения фигур. Максимальное количество уровней — 10. + +# Tetris Game + +Classic Tetris implementation in C11 with ncurses interface. + +## Requirements + +- GCC or Clang +- ncurses library +- Check framework (for tests) + +## Building + +make # Build the game +make run # Build and run +make test # Run unit tests +make install # Install to ~/.local/bin + +text + +## Controls + +- **Arrow Keys**: Move left/right, rotate (up) +- **Down Arrow**: Instant drop +- **Space/R**: Rotate figure +- **P**: Pause +- **S**: Restart game +- **Q**: Quit + +## Features + +- 7 classic Tetris figures +- Score tracking with persistent high score +- 10 speed levels +- Smooth time-based movement using POSIX `clock_gettime()` +- Structured programming principles + +## Architecture + +- **FSM-based game logic** with states: Init, Spawn, Move, Moving, Attaching, GameOver +- **Separated frontend/backend**: `brick_game/` (logic) and `gui/cli/` (display) +- **Time-based delays** for precise falling speed + +## License + +School 21 educational project + diff --git a/src/Makefile b/src/Makefile index d0899a4..3d82150 100644 --- a/src/Makefile +++ b/src/Makefile @@ -1,8 +1,7 @@ -.PHONY: all clean valgrind test style format gcov_report all install uninstall +.PHONY: all clean install uninstall test gcov_report dvi dist run style format -# Автоопределение компилятора и флагов CC ?= gcc -CFLAGS ?= -Wall -Wextra -std=c11 -g -D_POSIX_C_SOURCE=199309L +CFLAGS ?= -Wall -Wextra -Werror -std=c11 -g -D_POSIX_C_SOURCE=199309L -MMD -MP CHECK_CFLAGS ?= -I/usr/include/check # Проверяем наличие библиотек @@ -14,11 +13,7 @@ else LDFLAGS ?= -lcheck -lrt -lpthread -lm -lncurses endif -SRCDIR = . -TESTDIR = test BUILDDIR = build - -# src/Makefile TETRISDIR = brick_game/tetris CLIDIR = gui/cli @@ -32,10 +27,10 @@ LIB_TETRIS = $(BUILDDIR)/libtetris.a TARGET = $(BUILDDIR)/tetris_bin.out # Установка -PREFIX ?= /usr/local +PREFIX ?= $(HOME)/.local BINDIR = $(PREFIX)/bin -all: clean $(TARGET) +all: $(TARGET) $(LIB_TETRIS): $(TETRIS_OBJ) mkdir -p $(BUILDDIR) @@ -43,7 +38,6 @@ $(LIB_TETRIS): $(TETRIS_OBJ) $(TARGET): $(LIB_TETRIS) $(CLI_OBJ) $(CC) $(CLI_OBJ) -L$(BUILDDIR) -ltetris -o $@ $(LDFLAGS) - rm -f $(CLI_OBJ) $(TETRIS_OBJ) brick_game/tetris/%.o: brick_game/tetris/%.c $(CC) $(CFLAGS) -c $< -o $@ @@ -51,36 +45,51 @@ brick_game/tetris/%.o: brick_game/tetris/%.c gui/cli/%.o: gui/cli/%.c $(CC) $(CFLAGS) -c $< -o $@ -%.o: %.c - $(CC) $(CFLAGS) -c $< -o $@ - install: $(TARGET) - install -m 755 $(TARGET) $(BINDIR)/ + install -m 755 $(TARGET) $(BINDIR)/tetris_bin.out uninstall: - rm -f $(BINDIR)/$(TARGET) + rm -f $(BINDIR)/tetris_bin.out clean: - rm -f $(CLI_OBJ) $(TETRIS_OBJ) $(TARGET) $(LIB_TETRIS) *.gcda *.gcno *.gcov + rm -rf $(CLI_OBJ) $(TETRIS_OBJ) $(TARGET) $(LIB_TETRIS) + rm -rf $(BUILDDIR)/*.gcda $(BUILDDIR)/*.gcno *.gcda *.gcno *.gcov $(TETRISDIR)/*.d $(CLIDIR)/*.d + rm -rf coverage.info coverage_report -test: - @echo "Running tests..." - # Здесь будет вызов тестов, если они есть +test: $(LIB_TETRIS) + $(CC) $(CFLAGS) test/test.c -L$(BUILDDIR) -ltetris $(LDFLAGS) -o $(BUILDDIR)/test.out + ./$(BUILDDIR)/test.out gcov_report: CFLAGS += --coverage +gcov_report: LDFLAGS += --coverage gcov_report: clean $(TARGET) - ./$(TARGET) + @echo "Note: Run automated tests for proper coverage" gcov $(TETRIS_SRC) lcov --capture --directory . --output-file coverage.info genhtml coverage.info --output-directory coverage_report - @echo "Coverage report generated in coverage_report/index.html" + @echo "Coverage report: coverage_report/index.html" + +dvi: + doxygen Doxyfile 2>/dev/null || echo "Doxyfile not found" + +dist: clean + tar -czf tetris.tar.gz Makefile $(TETRISDIR) $(CLIDIR) README.md + +run: $(TARGET) + ./$(TARGET) style: - @cp ../materials/linters/.clang-format . - @clang-format -n *.c *.h - @rm .clang-format + @if [ -f .clang-format ]; then \ + clang-format -n $(TETRIS_SRC) $(CLI_SRC); \ + else \ + echo ".clang-format not found"; \ + fi format: - cp ../materials/linters/.clang-format . - clang-format -i *.c *.h - rm .clang-format \ No newline at end of file + @if [ -f .clang-format ]; then \ + clang-format -i $(TETRIS_SRC) $(CLI_SRC); \ + else \ + echo ".clang-format not found"; \ + fi + +-include $(TETRIS_OBJ:.o=.d) $(CLI_OBJ:.o=.d) \ No newline at end of file diff --git a/src/brick_game/tetris/01_automato.h b/src/brick_game/tetris/01_automato.h index f771752..3df3a11 100644 --- a/src/brick_game/tetris/01_automato.h +++ b/src/brick_game/tetris/01_automato.h @@ -8,6 +8,21 @@ #include #include +// Константы времени +#define ATTACH_DELAY_MS 350 +#define INSTANT_DROP_DELAY_MS 30 +#define BASE_FALL_DELAY_MS 1100 +#define SPEED_MULTIPLIER_MS 100 +#define MAX_LEVEL 10 + +// Константы очков +#define SCORE_PER_LEVEL 600 +#define POINTS_ONE_LINE 100 +#define POINTS_TWO_LINES 300 +#define POINTS_THREE_LINES 700 +#define POINTS_FOUR_LINES 1500 + + typedef enum { Init, Spawn, diff --git a/src/brick_game/tetris/06_move.c b/src/brick_game/tetris/06_move.c index 97461ee..7081b76 100644 --- a/src/brick_game/tetris/06_move.c +++ b/src/brick_game/tetris/06_move.c @@ -5,10 +5,10 @@ int get_milliseconds_to_wait(void) { int result = 0; if (state->moving_type == ToDown) { - result = 30; + result = INSTANT_DROP_DELAY_MS; } else { - int base_delay = 1100 - (state->info->speed * 100); - result = (base_delay > 100) ? base_delay : 100; + int base_delay = BASE_FALL_DELAY_MS - (state->info->speed * SPEED_MULTIPLIER_MS); + result = (base_delay > SPEED_MULTIPLIER_MS) ? base_delay : SPEED_MULTIPLIER_MS; } return result; diff --git a/src/brick_game/tetris/08_attaching.c b/src/brick_game/tetris/08_attaching.c index 5843883..0cb9439 100644 --- a/src/brick_game/tetris/08_attaching.c +++ b/src/brick_game/tetris/08_attaching.c @@ -15,7 +15,7 @@ void do_attaching(void) { } // Проверяем, прошло ли 350мс - if (current_time - state->attach_start_time >= 350) { + if (current_time - state->attach_start_time >= ATTACH_DELAY_MS) { state->attach_completed = 1; state->attach_start_time = 0; // Сбрасываем таймер @@ -108,21 +108,21 @@ void clear_lines() { } if (lines_cleared > 0) { - int points[] = {0, 100, 300, 700, 1500}; + int points[] = {0, POINTS_ONE_LINE, POINTS_TWO_LINES, POINTS_THREE_LINES, POINTS_FOUR_LINES}; state->info->score += points[lines_cleared]; if (state->info->score > state->info->high_score) { state->info->high_score = state->info->score; } - int new_level = (state->info->score / 600) + 1; - if (new_level > 10) { - new_level = 10; + int new_level = (state->info->score / SCORE_PER_LEVEL) + 1; + if (new_level > MAX_LEVEL) { + new_level = MAX_LEVEL; } if (new_level > state->info->level) { state->info->level = new_level; - state->info->speed = new_level * 3; + state->info->speed = new_level + (new_level / 2); } } } diff --git a/src/test/test.c b/src/test/test.c new file mode 100644 index 0000000..fd41a36 --- /dev/null +++ b/src/test/test.c @@ -0,0 +1,86 @@ +#include +#include "../brick_game/tetris/01_automato.h" + +START_TEST(test_collision_bottom_boundary) { + GameState_t* state = get_game_state(); + state->curr.y = FIELD_HEIGHT - 1; + state->curr.x = 5; + state->curr.mtrx[0][0] = 1; + state->curr.y++; // Выходим за границу + ck_assert_int_eq(check_collision(), 1); +} +END_TEST + +START_TEST(test_collision_left_boundary) { + GameState_t* state = get_game_state(); + state->curr.x = -1; + state->curr.mtrx[0][0] = 1; + ck_assert_int_eq(check_collision(), 1); +} +END_TEST + +START_TEST(test_collision_with_placed_block) { + GameState_t* state = get_game_state(); + state->field[10][5] = 2; // Размещённый блок + state->curr.y = 10; + state->curr.x = 5; + state->curr.mtrx[0][0] = 1; + ck_assert_int_eq(check_collision(), 1); +} +END_TEST + +START_TEST(test_no_collision) { + GameState_t* state = get_game_state(); + state->curr.y = 5; + state->curr.x = 5; + state->curr.mtrx[0][0] = 1; + ck_assert_int_eq(check_collision(), 0); +} +END_TEST + +START_TEST(test_game_over_detection) { + GameState_t* state = get_game_state(); + state->field[0][5] = 2; // Блок в верхней строке + ck_assert_int_eq(is_game_over(), 1); +} +END_TEST + +START_TEST(test_clear_single_line) { + GameState_t* state = get_game_state(); + state->info->score = 0; + + // Заполняем нижнюю линию + for (int j = 0; j < FIELD_WIDTH; j++) { + state->field[FIELD_HEIGHT - 1][j] = 2; + } + + clear_lines(); + ck_assert_int_eq(state->info->score, 100); +} +END_TEST + +Suite* tetris_suite(void) { + Suite* s = suite_create("Tetris"); + TCase* tc_core = tcase_create("Core"); + + tcase_add_test(tc_core, test_collision_bottom_boundary); + tcase_add_test(tc_core, test_collision_left_boundary); + tcase_add_test(tc_core, test_collision_with_placed_block); + tcase_add_test(tc_core, test_no_collision); + tcase_add_test(tc_core, test_game_over_detection); + tcase_add_test(tc_core, test_clear_single_line); + + suite_add_tcase(s, tc_core); + return s; +} + +int main(void) { + Suite* s = tetris_suite(); + SRunner* sr = srunner_create(s); + + srunner_run_all(sr, CK_NORMAL); + int number_failed = srunner_ntests_failed(sr); + srunner_free(sr); + + return (number_failed == 0) ? 0 : 1; +}