diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..bef6fab --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,29 @@ +{ + "name": "Tetris C Development", + "image": "ubuntu:24.04", + "features": { + "ghcr.io/devcontainers/features/common-utils:2": { + "installZsh": false + } + }, + "customizations": { + "vscode": { + "extensions": [ + "ms-vscode.cpptools", + "ms-vscode.cmake-tools", + "llvm-vs-code-extensions.vscode-clangd", + "vadimcn.vscode-lldb", + "ritwickdey.liveserver" + ], + "settings": { + "C_Cpp.default.compilerPath": "/usr/bin/gcc", + "terminal.integrated.defaultProfile.linux": "bash" + } + } + }, + "postCreateCommand": "apt-get update && apt-get install -y gcc make libncurses-dev check lcov doxygen gdb valgrind clang-format git xdg-utils", + "remoteUser": "root", + "mounts": [ + "source=${localWorkspaceFolder},target=/workspace,type=bind,consistency=cached" + ] +} diff --git a/.gitignore b/.gitignore index c6127b3..5716886 100644 --- a/.gitignore +++ b/.gitignore @@ -42,6 +42,8 @@ *.idb *.pdb +*.gc* + # Kernel Module Compile Results *.mod* *.cmd @@ -50,3 +52,18 @@ modules.order Module.symvers Mkfile.old dkms.conf +src/project.md +src/tetris_bin +src/.gpskip +.gpskip +ginpee.toml +src/ginpee.toml +.vscode/launch.json +src/tetris.log +src/high_score.txt +src/build/high_score.txt +code-samples/frogger/project.md +dvi/ +gcov_report/ +build/ +src/tetris.tar.gz 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/flake.lock b/flake.lock new file mode 100644 index 0000000..4d1227e --- /dev/null +++ b/flake.lock @@ -0,0 +1,61 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1758427187, + "narHash": "sha256-pHpxZ/IyCwoTQPtFIAG2QaxuSm8jWzrzBGjwQZIttJc=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "554be6495561ff07b6c724047bdd7e0716aa7b46", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..a030f24 --- /dev/null +++ b/flake.nix @@ -0,0 +1,42 @@ +{ + description = "C Project with Check, Valgrind, Gcov support"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = { self, nixpkgs, flake-utils }: + flake-utils.lib.eachDefaultSystem (system: let + pkgs = nixpkgs.legacyPackages.${system}; + + buildInputs = with pkgs; [ + check # для unit-тестов + valgrind # для проверки утечек + lcov # для отчетов gcov + clang-tools # clang-format + gcc # компилятор + gnumake # make + ncurses # для TUI/CLI приложений + readline # для readline поддержки + zlib # сжатие данных + libxml2 # XML parsing + curl # HTTP клиент + openssl # криптография + sqlite # база данных + ]; + in { + devShells.default = pkgs.mkShell { + inherit buildInputs; + + shellHook = '' + export PATH="$HOME/.cargo/bin:$PATH" + echo "✅ C Project Dev Environment Loaded" + echo "🔧 Available tools:" + echo " - gcc, make, clang-format" + echo " - checkmk, valgrind, lcov" + echo "🚀 Run 'make test' to build and run tests" + ''; + }; + }); +} diff --git a/src/.gpskip b/src/.gpskip new file mode 100644 index 0000000..2c29003 --- /dev/null +++ b/src/.gpskip @@ -0,0 +1,72 @@ +.git/ +.vscode/ +.idea/ +target/ +build/ +dist/ +node_modules/ +*.log +*.tmp +*.swp +*.bak +.DS_Store +Thumbs.db + +# Prerequisites +*.d + +# Object files +*.o +*.ko +*.obj +*.elf + +# Linker output +*.ilk +*.map +*.exp + +# Precompiled Headers +*.gch +*.pch + +# Libraries +*.lib +*.a +*.la +*.lo + +# Shared objects (inc. Windows DLLs) +*.dll +*.so +*.so.* +*.dylib + +# Executables +*.exe +*.out +*.app +*.i*86 +*.x86_64 +*.hex + +# Debug files +*.dSYM/ +*.su +*.idb +*.pdb + +# Kernel Module Compile Results +*.mod* +*.cmd +.tmp_versions/ +modules.order +Module.symvers +Mkfile.old +dkms.conf +src/project.md +src/tetris_bin +src/.gpskip +.gpskip +ginpee.toml + diff --git a/src/Doxyfile b/src/Doxyfile new file mode 100644 index 0000000..72a050c --- /dev/null +++ b/src/Doxyfile @@ -0,0 +1,23 @@ +# Doxyfile для генерации документации +PROJECT_NAME = "Tetris Game C API" +PROJECT_NUMBER = "1.0" +PROJECT_BRIEF = "Classic Tetris implementation with separated backend/frontend" +OUTPUT_DIRECTORY = dvi +INPUT = brick_game/tetris/00_tetris.h doc.md +RECURSIVE = NO +GENERATE_HTML = YES +GENERATE_LATEX = NO +EXTRACT_ALL = YES +EXTRACT_PRIVATE = NO +EXTRACT_STATIC = NO +FILE_PATTERNS = *.h *.md +HTML_OUTPUT = html +USE_MDFILE_AS_MAINPAGE = doc.md +JAVADOC_AUTOBRIEF = YES +OPTIMIZE_OUTPUT_FOR_C = YES +TYPEDEF_HIDES_STRUCT = YES +SHOW_INCLUDE_FILES = YES +SHOW_NAMESPACES = NO +QUIET = YES +WARNINGS = YES +WARN_IF_UNDOCUMENTED = YES diff --git a/src/Makefile b/src/Makefile new file mode 100644 index 0000000..e4afbbd --- /dev/null +++ b/src/Makefile @@ -0,0 +1,129 @@ +.PHONY: all clean install uninstall test test_coverage gcov_report dvi dist run style format + +CC ?= gcc +CFLAGS ?= -Wall -Wextra -Werror -std=c11 -g -D_POSIX_C_SOURCE=199309L -MMD -MP +CHECK_CFLAGS ?= -I/usr/include/check + +# Проверяем наличие библиотек +CHECK_LIBS_AVAILABLE := $(shell ldconfig -p 2>/dev/null | grep -q libsubunit && echo yes || echo no) + +ifeq ($(CHECK_LIBS_AVAILABLE),yes) + LDFLAGS ?= -lcheck -lrt -lpthread -lm -lncurses -lsubunit +else + LDFLAGS ?= -lcheck -lrt -lpthread -lm -lncurses +endif + +BUILDDIR = build +TETRISDIR = brick_game/tetris +CLIDIR = gui/cli +TESTDIR = test +GCOV_DIR = gcov_report +DVI_DIR = dvi + +# Файлы +TETRIS_SRC = $(shell find $(TETRISDIR) -name "*.c") +TETRIS_OBJ = $(TETRIS_SRC:.c=.o) +CLI_SRC = $(shell find $(CLIDIR) -name "*.c") +CLI_OBJ = $(CLI_SRC:.c=.o) +TEST_SRC = $(filter-out $(TESTDIR)/test.c, $(shell find $(TESTDIR) -name "*.c")) + +LIB_TETRIS = $(BUILDDIR)/libtetris.a +TARGET = $(BUILDDIR)/tetris_bin.out +TEST_TARGET = $(BUILDDIR)/test.out + +# Установка +PREFIX ?= $(HOME)/.local +BINDIR = $(PREFIX)/bin + +all: gcov_report dvi + +run: clean $(TARGET) + ./$(TARGET) + +install: clean $(TARGET) + mkdir -p $(BINDIR) + install -m 755 $(TARGET) $(BINDIR)/tetris + @echo "installed $(BINDIR)/tetris" + +uninstall: + rm -f $(BINDIR)/tetris + @echo "uninstalled $(BINDIR)/tetris" + +clean: + rm -rf $(CLI_OBJ) $(TETRIS_OBJ) $(TARGET) $(LIB_TETRIS) $(TEST_TARGET) + rm -rf $(TETRISDIR)/*.d $(CLIDIR)/*.d + rm -rf $(TETRISDIR)/*.gcda $(TETRISDIR)/*.gcno $(TETRISDIR)/*.gcov + rm -rf $(GCOV_DIR) $(DVI_DIR) + +test: clean $(LIB_TETRIS) + $(CC) $(CFLAGS) $(TEST_SRC) -L$(BUILDDIR) -ltetris $(LDFLAGS) -o $(TEST_TARGET) + ./$(TEST_TARGET) + +test_coverage: CFLAGS += --coverage +test_coverage: LDFLAGS += --coverage +test_coverage: clean $(LIB_TETRIS) + $(CC) $(CFLAGS) $(TEST_SRC) -L$(BUILDDIR) -ltetris $(LDFLAGS) -o $(TEST_TARGET) + ./$(TEST_TARGET) + +gcov_report: test_coverage + @mkdir -p $(GCOV_DIR)/obj + @find $(TETRISDIR) -name "*.gcda" -exec mv {} $(GCOV_DIR)/obj/ \; + @find $(TETRISDIR) -name "*.gcno" -exec mv {} $(GCOV_DIR)/obj/ \; + @cd $(GCOV_DIR) && for src in $(addprefix ../,$(TETRIS_SRC)); do \ + gcov $$src -o obj/ 2>/dev/null; \ + done + lcov --capture --directory $(GCOV_DIR)/obj \ + --output-file $(GCOV_DIR)/coverage.info \ + --ignore-errors unused + lcov --extract $(GCOV_DIR)/coverage.info '*/brick_game/tetris/*' \ + -o $(GCOV_DIR)/coverage.info \ + --ignore-errors unused + genhtml $(GCOV_DIR)/coverage.info \ + --output-directory $(GCOV_DIR) + @echo "Report: $(GCOV_DIR)/index.html" + xdg-open $(GCOV_DIR)/index.html + + +dvi: + @mkdir -p $(DVI_DIR) + @echo "Generating documentation with Doxygen..." + @if command -v doxygen >/dev/null 2>&1; then \ + doxygen Doxyfile && echo "HTML docs: $(DVI_DIR)/html/index.html"; \ + else \ + echo "Error: Doxygen not found. Install: nix-shell -p doxygen"; \ + echo "Copying doc.md as fallback..."; \ + cp doc.md $(DVI_DIR)/; \ + fi + xdg-open $(DVI_DIR)/html/index.html + +dist: clean + tar -czf tetris.tar.gz Makefile $(TETRISDIR) $(CLIDIR) $(TESTDIR) ../README.md doc.md + +style: + @if [ -f ../materials/linters/.clang-format ]; then \ + clang-format -n $(TETRIS_SRC) $(CLI_SRC) $(TEST_SRC); \ + else \ + echo ".clang-format not found"; \ + fi + +format: + @if [ -f ../materials/linters/.clang-format ]; then \ + clang-format -i $(TETRIS_SRC) $(CLI_SRC) $(TEST_SRC); \ + else \ + echo ".clang-format not found"; \ + fi + +$(LIB_TETRIS): $(TETRIS_OBJ) + mkdir -p $(BUILDDIR) + ar rcs $@ $^ + +$(TARGET): $(LIB_TETRIS) $(CLI_OBJ) + $(CC) $(CLI_OBJ) -L$(BUILDDIR) -ltetris -o $@ $(LDFLAGS) + +brick_game/tetris/%.o: brick_game/tetris/%.c + $(CC) $(CFLAGS) -c $< -o $@ + +gui/cli/%.o: gui/cli/%.c + $(CC) $(CFLAGS) -c $< -o $@ + +-include $(TETRIS_OBJ:.o=.d) $(CLI_OBJ:.o=.d) diff --git a/src/brick_game/tetris/00_tetris.h b/src/brick_game/tetris/00_tetris.h new file mode 100644 index 0000000..3e0624a --- /dev/null +++ b/src/brick_game/tetris/00_tetris.h @@ -0,0 +1,165 @@ +/** + * @file 00_tetris.h + * @brief Public API for Tetris game backend + * + * This header defines the public interface for Tetris game logic. + * The backend is completely separated from UI, allowing integration + * with any frontend (ncurses, SDL, Qt, Rust, etc.). + * + * @note Thread safety: NOT thread-safe! Use from single thread only. + * @note Memory management: All memory is managed internally. Call + * userInput(Terminate, false) before exit to free resources. + */ + +#ifndef TETRIS_H +#define TETRIS_H + +#include +#include + +/** + * @brief Width of the game field in blocks + */ +#define FIELD_WIDTH 10 + +/** + * @brief Height of the game field in blocks + */ +#define FIELD_HEIGHT 20 + +/** + * @enum UserAction_t + * @brief User input actions + * + * Represents all possible user actions in the game. + */ +typedef enum { + Start, /**< Initialize or restart the game */ + Pause, /**< Toggle pause state */ + Terminate, /**< Quit game and free all resources */ + Left, /**< Move figure left */ + Right, /**< Move figure right */ + Up, /**< Release instant drop flag (for next drop) */ + Down, /**< Instant drop figure to bottom */ + Action /**< Rotate figure clockwise */ +} UserAction_t; + +/** + * @struct GameInfo_t + * @brief Game state information for rendering + * + * Contains all information needed to render the game state. + * This structure is returned by updateCurrentState() every frame. + * + * @note Do not free the pointers - memory is managed internally + */ +typedef struct { + /** + * @brief 2D game field [FIELD_HEIGHT][FIELD_WIDTH] + * + * Values: + * - 0: Empty cell + * - 1: Active (falling) figure + * - 2: Placed (fixed) blocks + */ + int **field; + + /** + * @brief Preview of next figure [4][4] + * + * 4x4 matrix containing the next figure shape. + * Values: 0 (empty) or 1 (figure block) + */ + int **next; + + int score; /**< Current score */ + int high_score; /**< Best score (persistent across sessions) */ + int level; /**< Current level (1-10) */ + int speed; /**< Current speed multiplier */ + int pause; /**< Pause state: 0=playing, 1=paused */ +} GameInfo_t; + +/** + * @brief Process user input and update game state + * + * This function handles all user actions and updates the internal FSM state. + * Should be called when user performs an action (key press, button click, etc.). + * + * @param action User action from UserAction_t enum + * @param hold Reserved for future use (currently ignored) + * + * @note Actions are blocked during pause (except Pause, Terminate, Start) + * @note Actions are blocked during Attaching state (except Pause, Terminate, Start) + * @note Down action requires key release between uses to prevent double-drop + * + * @par Example: + * @code + * // Handle left arrow key press + * userInput(Left, false); + * + * // Start new game + * userInput(Start, false); + * + * // Quit and cleanup + * userInput(Terminate, false); + * @endcode + * + * @par FSI Integration (Rust example): + * @code{.rs} + * extern "C" { + * fn userInput(action: UserAction_t, hold: bool); + * } + * + * unsafe { + * userInput(UserAction_t::Left, false); + * } + * @endcode + */ +void userInput(UserAction_t action, bool hold); + +/** + * @brief Update game state and return current game information + * + * This function must be called every game loop iteration (recommended ~10ms). + * It updates the internal FSM, handles timing, figure movement, collisions, + * and returns the current state for rendering. + * + * @return GameInfo_t structure with current game state + * + * @note Call frequency: ~100 times per second (every 10ms) + * @note Timing is handled internally using POSIX clock_gettime(CLOCK_MONOTONIC) + * @note The returned structure contains pointers to internal memory - do not free! + * + * @par Example: + * @code + * // Game loop + * while (running) { + * GameInfo_t state = updateCurrentState(); + * + * // Render game field + * for (int i = 0; i < FIELD_HEIGHT; i++) { + * for (int j = 0; j < FIELD_WIDTH; j++) { + * if (state.field[i][j] == 1) { + * draw_active_block(j, i); + * } else if (state.field[i][j] == 2) { + * draw_placed_block(j, i); + * } + * } + * } + * + * // Display score + * printf("Score: %d Level: %d\n", state.score, state.level); + * + * sleep_ms(10); + * } + * @endcode + * + * @par Timing behavior: + * - Base fall delay: 1100ms - (speed × 100ms) + * - Instant drop: 30ms per step + * - Attach delay: 350ms (prevents instant spawn) + * - Pause correctly compensates all timings + */ +GameInfo_t updateCurrentState(); + +#endif /* TETRIS_H */ diff --git a/src/brick_game/tetris/01_automato.h b/src/brick_game/tetris/01_automato.h new file mode 100644 index 0000000..7551fda --- /dev/null +++ b/src/brick_game/tetris/01_automato.h @@ -0,0 +1,126 @@ +#ifndef AUTOMATO_H +#define AUTOMATO_H + +#define _POSIX_C_SOURCE 199309L + +#include "00_tetris.h" +#include +#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, + Moving, + Move, + Attaching, + GameOver +} Automato_t; + +typedef enum { + RightDown, + LeftDown, + Rotate, + ToDown, + DoNothing +} Moving_t; + +typedef enum { + I = 0, + J, + L, + O, + S, + T, + Z, + FIGURE_COUNT +} Sprite_t; + +typedef struct { + int x, y; + int mtrx[4][4]; + Sprite_t sprite; + int rotation; +} Figure_t; + +typedef struct { + Figure_t curr; + Figure_t next; + Automato_t state; + Moving_t moving_type; + int field[FIELD_HEIGHT][FIELD_WIDTH]; + GameInfo_t* info; + long long last_move_time; + long long pause_start_time; + long long attach_start_time; + int attach_completed; + int down_key_was_released; +} GameState_t; + +GameState_t* get_game_state(void); + +void do_init(void); +int load_high_score(); +void save_high_score(int score); +void generate_next_figure(void); +void terminate_and_free(void); + +long long get_current_time_ms(void); + +void do_spawn(void); + +void do_move(void); + +void do_moving(void); + +void do_attaching(void); +int check_collision(); +void place_figure(); +void clear_lines(); + +void do_gameover(void); +int is_game_over(); + +const int (*get_figure_shape(Sprite_t sprite, int rotation))[4]; + +const int (*i_fig_up())[4]; +const int (*i_fig_right())[4]; +const int (*i_fig_down())[4]; +const int (*i_fig_left())[4]; +const int (*o_fig())[4]; +const int (*t_fig_up())[4]; +const int (*t_fig_right())[4]; +const int (*t_fig_down())[4]; +const int (*t_fig_left())[4]; +const int (*l_fig_up())[4]; +const int (*l_fig_right())[4]; +const int (*l_fig_down())[4]; +const int (*l_fig_left())[4]; +const int (*j_fig_up())[4]; +const int (*j_fig_right())[4]; +const int (*j_fig_down())[4]; +const int (*j_fig_left())[4]; +const int (*s_fig_up())[4]; +const int (*s_fig_right())[4]; +const int (*s_fig_down())[4]; +const int (*s_fig_left())[4]; +const int (*z_fig_up())[4]; +const int (*z_fig_right())[4]; +const int (*z_fig_down())[4]; +const int (*z_fig_left())[4]; +const int (*empty_fig())[4]; + +#endif diff --git a/src/brick_game/tetris/02_tetris.c b/src/brick_game/tetris/02_tetris.c new file mode 100644 index 0000000..e8210c4 --- /dev/null +++ b/src/brick_game/tetris/02_tetris.c @@ -0,0 +1,136 @@ +#include "01_automato.h" + +void userInput(UserAction_t action, bool hold) { + (void)hold; + GameState_t *state = get_game_state(); + + int should_process = 1; + + if (state->info->pause) { + if (action == Left || action == Right || action == Down || action == Up || + action == Action || action == Start) { + should_process = 0; + } + } + + if (state->state == Attaching && !state->attach_completed) { + if (action == Left || action == Right || action == Down || action == Up || + action == Action) { + should_process = 0; + } + } + + if (should_process) { + switch (action) { + case Start: + if (state->info->score >= state->info->high_score) { + state->info->high_score = state->info->score; + save_high_score(state->info->high_score); + } + state->info->high_score = load_high_score(); + state->state = Init; + state->down_key_was_released = 1; + break; + case Terminate: + if (state->info->score > state->info->high_score) { + state->info->high_score = state->info->score; + save_high_score(state->info->high_score); + } + terminate_and_free(); + state->state = GameOver; + break; + case Left: + state->state = Moving; + state->moving_type = LeftDown; + break; + case Right: + state->state = Moving; + state->moving_type = RightDown; + break; + case Action: + state->state = Moving; + state->moving_type = Rotate; + break; + case Down: + if (state->down_key_was_released) { + state->state = Moving; + state->moving_type = ToDown; + state->down_key_was_released = 0; + } + break; + case Up: + state->down_key_was_released = 1; + break; + case Pause: + if (!state->info->pause) { + state->pause_start_time = get_current_time_ms(); + } else { + long long pause_duration = + get_current_time_ms() - state->pause_start_time; + state->last_move_time += pause_duration; + // Корректируем attach_start_time если мы в Attaching + state->attach_start_time += + (state->state == Attaching) * pause_duration; + } + state->info->pause = !state->info->pause; + break; + default: + break; + } + } +} + +GameInfo_t updateCurrentState() { + GameState_t *state = get_game_state(); + + int should_update = (!state->info->pause || state->state == GameOver); + if (should_update) { + switch (state->state) { + case Init: + do_init(); + break; + case Spawn: + do_spawn(); + break; + case Move: + do_move(); + break; + case Moving: + do_moving(); + break; + case Attaching: + do_attaching(); + break; + case GameOver: + do_gameover(); + break; + } + } + + for (int i = 0; i < FIELD_HEIGHT; i++) { + for (int j = 0; j < FIELD_WIDTH; j++) { + state->info->field[i][j] = state->field[i][j]; + } + } + + Figure_t *fig = &state->curr; + for (int i = 0; i < 4; i++) { + for (int j = 0; j < 4; j++) { + if (fig->mtrx[i][j]) { + int x = fig->x + j; + int y = fig->y + i; + if (y >= 0 && y < FIELD_HEIGHT && x >= 0 && x < FIELD_WIDTH) { + state->info->field[y][x] = 1; + } + } + } + } + + for (int i = 0; i < 4; i++) { + for (int j = 0; j < 4; j++) { + state->info->next[i][j] = state->next.mtrx[i][j]; + } + } + + return *state->info; +} diff --git a/src/brick_game/tetris/03_automato.c b/src/brick_game/tetris/03_automato.c new file mode 100644 index 0000000..ce84744 --- /dev/null +++ b/src/brick_game/tetris/03_automato.c @@ -0,0 +1,91 @@ +#include "01_automato.h" + +long long get_current_time_ms(void) { + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return ts.tv_sec * 1000LL + ts.tv_nsec / 1000000LL; +} + +int load_high_score() { + FILE *file = fopen("build/high_score.txt", "r"); + int high_score = 0; + if (file) { + if (fscanf(file, "%d", &high_score) != 1) { + high_score = 0; + } + fclose(file); + } + return high_score; +} + +void save_high_score(int score) { + FILE *file = fopen("build/high_score.txt", "w"); + if (file) { + fprintf(file, "%d", score); + fclose(file); + } +} + +GameState_t *get_game_state(void) { + static GameState_t state = {0}; + static int initialized = 0; + + if (!initialized) { + state.info = malloc(sizeof(GameInfo_t)); + state.info->field = malloc(FIELD_HEIGHT * sizeof(int *)); + for (int i = 0; i < FIELD_HEIGHT; i++) { + state.info->field[i] = malloc(FIELD_WIDTH * sizeof(int)); + } + + state.info->next = malloc(4 * sizeof(int *)); + for (int i = 0; i < 4; i++) { + state.info->next[i] = malloc(4 * sizeof(int)); + } + state.info->speed = 1; + state.info->score = 0; + state.info->level = 1; + state.info->pause = 0; + state.last_move_time = get_current_time_ms(); + state.pause_start_time = 0; + state.attach_start_time = 0; + state.attach_completed = 0; + state.down_key_was_released = 1; + state.info->high_score = load_high_score(); + + state.state = GameOver; + initialized = 1; + } + + return &state; +} + +void terminate_and_free() { + GameState_t *state = get_game_state(); + + if (state->info) { + if (state->info->field != NULL) { + for (int i = 0; i < FIELD_HEIGHT; i++) { + if (state->info->field[i] != NULL) { + free(state->info->field[i]); + state->info->field[i] = NULL; + } + } + free(state->info->field); + state->info->field = NULL; + } + + if (state->info->next != NULL) { + for (int i = 0; i < 4; i++) { + if (state->info->next[i] != NULL) { + free(state->info->next[i]); + state->info->next[i] = NULL; + } + } + free(state->info->next); + state->info->next = NULL; + } + + free(state->info); + state->info = NULL; + } +} diff --git a/src/brick_game/tetris/04_init.c b/src/brick_game/tetris/04_init.c new file mode 100644 index 0000000..e32e6b3 --- /dev/null +++ b/src/brick_game/tetris/04_init.c @@ -0,0 +1,23 @@ +#include "01_automato.h" + +void clear_field(void) { + GameState_t *state = get_game_state(); + for (int i = 0; i < FIELD_HEIGHT; ++i) + for (int j = 0; j < FIELD_WIDTH; ++j) + state->field[i][j] = 0; +} + +void reset_game_stats(void) { + GameState_t *state = get_game_state(); + state->info->score = 0; + state->info->level = 1; + state->info->speed = 1; + state->last_move_time = get_current_time_ms(); +} + +void do_init(void) { + clear_field(); + reset_game_stats(); + generate_next_figure(); + get_game_state()->state = Spawn; +} diff --git a/src/brick_game/tetris/05_spawn.c b/src/brick_game/tetris/05_spawn.c new file mode 100644 index 0000000..51e5874 --- /dev/null +++ b/src/brick_game/tetris/05_spawn.c @@ -0,0 +1,37 @@ +#include "01_automato.h" + +void set_current_figure_from_next(void) { + GameState_t *state = get_game_state(); + state->curr = state->next; + state->curr.x = FIELD_WIDTH / 2 - 2; + state->curr.y = 0; + state->moving_type = DoNothing; +} + +void generate_next_figure(void) { + GameState_t *state = get_game_state(); + state->next.sprite = rand() % FIGURE_COUNT; + state->next.rotation = 0; + const int(*shape)[4] = get_figure_shape(state->next.sprite, 0); + for (int i = 0; i < 4; ++i) { + for (int j = 0; j < 4; ++j) { + state->next.mtrx[i][j] = shape[i][j]; + } + } +} + +void do_spawn(void) { + GameState_t *state = get_game_state(); + + set_current_figure_from_next(); + generate_next_figure(); + + int has_collision = check_collision(); + + if (has_collision) { + state->state = GameOver; + } else { + state->state = Move; + state->down_key_was_released = 1; + } +} diff --git a/src/brick_game/tetris/06_move.c b/src/brick_game/tetris/06_move.c new file mode 100644 index 0000000..89d92cf --- /dev/null +++ b/src/brick_game/tetris/06_move.c @@ -0,0 +1,38 @@ +#include "01_automato.h" + +int get_milliseconds_to_wait(void) { + GameState_t *state = get_game_state(); + int result = 0; + + if (state->moving_type == ToDown) { + result = INSTANT_DROP_DELAY_MS; + } else { + 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; +} + +void do_move(void) { + GameState_t *state = get_game_state(); + + long long current_time = get_current_time_ms(); + int ms_to_wait = get_milliseconds_to_wait(); + + int should_move = (current_time - state->last_move_time >= ms_to_wait); + + if (should_move) { + state->last_move_time = current_time; + + state->curr.y++; + int has_collision = check_collision(); + + if (has_collision) { + state->curr.y--; + state->state = Attaching; + } + } +} diff --git a/src/brick_game/tetris/07_moving.c b/src/brick_game/tetris/07_moving.c new file mode 100644 index 0000000..14c8fe3 --- /dev/null +++ b/src/brick_game/tetris/07_moving.c @@ -0,0 +1,74 @@ +#include "01_automato.h" + +void handle_move_direction(Moving_t direction) { + GameState_t *state = get_game_state(); + switch (direction) { + case LeftDown: + state->curr.x--; + break; + case RightDown: + state->curr.x++; + break; + default: + break; + } +} + +void handle_rotate(void) { + GameState_t *state = get_game_state(); + state->curr.rotation = (state->curr.rotation + 1) % 4; + const int(*shape)[4] = + get_figure_shape(state->curr.sprite, state->curr.rotation); + for (int i = 0; i < 4; ++i) + for (int j = 0; j < 4; ++j) + state->curr.mtrx[i][j] = shape[i][j]; +} + +void handle_horizontal_rotate_move(void) { + GameState_t *state = get_game_state(); + Figure_t old = state->curr; + + switch (state->moving_type) { + case LeftDown: + case RightDown: + handle_move_direction(state->moving_type); + break; + case Rotate: + handle_rotate(); + break; + default: + break; + } + + if (check_collision()) { + state->curr = old; + } + state->state = Move; +} + +void handle_to_down_move(void) { + GameState_t *state = get_game_state(); + while (!check_collision()) { + state->curr.y++; + } + state->curr.y--; + state->state = Attaching; +} + +void do_moving(void) { + GameState_t *state = get_game_state(); + + switch (state->moving_type) { + case LeftDown: + case RightDown: + case Rotate: + handle_horizontal_rotate_move(); + break; + case ToDown: + handle_to_down_move(); + break; + case DoNothing: + state->state = Move; + break; + } +} \ No newline at end of file diff --git a/src/brick_game/tetris/08_attaching.c b/src/brick_game/tetris/08_attaching.c new file mode 100644 index 0000000..6d443a2 --- /dev/null +++ b/src/brick_game/tetris/08_attaching.c @@ -0,0 +1,124 @@ +#include "01_automato.h" + +void do_attaching(void) { + GameState_t *state = get_game_state(); + long long current_time = get_current_time_ms(); + + if (!state->attach_completed) { + if (state->attach_start_time == 0) { + place_figure(); + clear_lines(); + state->attach_start_time = current_time; + state->attach_completed = 0; + } + + if (current_time - state->attach_start_time >= ATTACH_DELAY_MS) { + state->attach_completed = 1; + state->attach_start_time = 0; + + int game_over = is_game_over(); + + if (game_over) { + state->state = GameOver; + } else { + state->state = Spawn; + } + + state->attach_completed = 0; + } + } +} + +int check_collision() { + GameState_t *state = get_game_state(); + Figure_t *fig = &state->curr; + int collision_detected = 0; + + for (int i = 0; i < 4 && !collision_detected; ++i) { + for (int j = 0; j < 4 && !collision_detected; ++j) { + if (fig->mtrx[i][j]) { + int x = fig->x + j; + int y = fig->y + i; + + int out_of_bounds = (x < 0 || x >= FIELD_WIDTH || y >= FIELD_HEIGHT); + int hits_placed_block = (y >= 0 && state->field[y][x]); + + if (out_of_bounds || hits_placed_block) { + collision_detected = 1; + } + } + } + } + + return collision_detected; +} + +void place_figure() { + GameState_t *state = get_game_state(); + Figure_t *fig = &state->curr; + + for (int i = 0; i < 4; ++i) { + for (int j = 0; j < 4; ++j) { + if (fig->mtrx[i][j]) { + int x = fig->x + j; + int y = fig->y + i; + if (y >= 0 && y < FIELD_HEIGHT && x >= 0 && x < FIELD_WIDTH) { + state->field[y][x] = 2; + } + } + } + } +} + +void shift_lines_down(int from_row) { + GameState_t *state = get_game_state(); + for (int y = from_row; y > 0; --y) { + for (int x = 0; x < FIELD_WIDTH; ++x) { + state->field[y][x] = state->field[y - 1][x]; + } + } + for (int x = 0; x < FIELD_WIDTH; ++x) { + state->field[0][x] = 0; + } +} + +void clear_lines() { + GameState_t *state = get_game_state(); + int lines_cleared = 0; + + for (int i = FIELD_HEIGHT - 1; i >= 0; --i) { + int full = 1; + + for (int j = 0; j < FIELD_WIDTH; ++j) { + if (state->field[i][j] != 2) { + full = 0; + } + } + + if (full) { + shift_lines_down(i); + lines_cleared++; + i++; + } + } + + if (lines_cleared > 0) { + 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 / 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 + (new_level / 2); + } + } +} diff --git a/src/brick_game/tetris/09_gameover.c b/src/brick_game/tetris/09_gameover.c new file mode 100644 index 0000000..6c38c32 --- /dev/null +++ b/src/brick_game/tetris/09_gameover.c @@ -0,0 +1,30 @@ +#include "01_automato.h" + +void do_gameover(void) { + GameState_t *state = get_game_state(); + + if (state->info->score > state->info->high_score) { + state->info->high_score = state->info->score; + save_high_score(state->info->high_score); + } + + const int(*shape)[4] = empty_fig(); + for (int i = 0; i < 4; ++i) { + for (int j = 0; j < 4; ++j) { + state->next.mtrx[i][j] = shape[i][j]; + } + } +} + +int is_game_over() { + GameState_t *state = get_game_state(); + int game_over = 0; + + for (int j = 0; j < FIELD_WIDTH; ++j) { + if (state->field[0][j] || state->field[1][j]) { + game_over = 1; + } + } + + return game_over; +} diff --git a/src/brick_game/tetris/figure_sprites.c b/src/brick_game/tetris/figure_sprites.c new file mode 100644 index 0000000..6803c60 --- /dev/null +++ b/src/brick_game/tetris/figure_sprites.c @@ -0,0 +1,272 @@ +#include "01_automato.h" + +const int (*get_figure_shape(Sprite_t sprite, int rotation))[4] { + const int(*result)[4] = NULL; + switch (sprite) { + case I: + switch (rotation % 4) { + case 0: + result = i_fig_up(); + break; + case 1: + result = i_fig_right(); + break; + case 2: + result = i_fig_down(); + break; + case 3: + result = i_fig_left(); + break; + } + break; + case O: + result = o_fig(); + break; + case T: + switch (rotation % 4) { + case 0: + result = t_fig_up(); + break; + case 1: + result = t_fig_right(); + break; + case 2: + result = t_fig_down(); + break; + case 3: + result = t_fig_left(); + break; + } + break; + case L: + switch (rotation % 4) { + case 0: + result = l_fig_up(); + break; + case 1: + result = l_fig_right(); + break; + case 2: + result = l_fig_down(); + break; + case 3: + result = l_fig_left(); + break; + } + break; + case J: + switch (rotation % 4) { + case 0: + result = j_fig_up(); + break; + case 1: + result = j_fig_right(); + break; + case 2: + result = j_fig_down(); + break; + case 3: + result = j_fig_left(); + break; + } + break; + case S: + switch (rotation % 4) { + case 0: + result = s_fig_up(); + break; + case 1: + result = s_fig_right(); + break; + case 2: + result = s_fig_down(); + break; + case 3: + result = s_fig_left(); + break; + } + break; + case Z: + switch (rotation % 4) { + case 0: + result = z_fig_up(); + break; + case 1: + result = z_fig_right(); + break; + case 2: + result = z_fig_down(); + break; + case 3: + result = z_fig_left(); + break; + } + break; + default: + result = NULL; + } + return result; +} + +// I +const int (*i_fig_up())[4] { + static const int shape[4][4] = { + {0, 0, 0, 0}, {1, 1, 1, 1}, {0, 0, 0, 0}, {0, 0, 0, 0}}; + return (const int(*)[4])shape; +} + +const int (*i_fig_right())[4] { + static const int shape[4][4] = { + {0, 0, 1, 0}, {0, 0, 1, 0}, {0, 0, 1, 0}, {0, 0, 1, 0}}; + return (const int(*)[4])shape; +} + +const int (*i_fig_down())[4] { + static const int shape[4][4] = { + {0, 0, 0, 0}, {0, 0, 0, 0}, {1, 1, 1, 1}, {0, 0, 0, 0}}; + return (const int(*)[4])shape; +} + +const int (*i_fig_left())[4] { + static const int shape[4][4] = { + {0, 1, 0, 0}, {0, 1, 0, 0}, {0, 1, 0, 0}, {0, 1, 0, 0}}; + return (const int(*)[4])shape; +} + +// O +const int (*o_fig())[4] { + static const int shape[4][4] = { + {0, 1, 1, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}}; + return (const int(*)[4])shape; +} + +// T +const int (*t_fig_up())[4] { + static const int shape[4][4] = { + {0, 1, 0, 0}, {1, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}}; + return (const int(*)[4])shape; +} + +const int (*t_fig_right())[4] { + static const int shape[4][4] = { + {0, 1, 0, 0}, {0, 1, 1, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}}; + return (const int(*)[4])shape; +} + +const int (*t_fig_down())[4] { + static const int shape[4][4] = { + {0, 0, 0, 0}, {1, 1, 1, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}}; + return (const int(*)[4])shape; +} + +const int (*t_fig_left())[4] { + static const int shape[4][4] = { + {0, 1, 0, 0}, {1, 1, 0, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}}; + return (const int(*)[4])shape; +} + +// L +const int (*l_fig_up())[4] { + static const int shape[4][4] = { + {0, 0, 1, 0}, {1, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}}; + return (const int(*)[4])shape; +} + +const int (*l_fig_right())[4] { + static const int shape[4][4] = { + {1, 0, 0, 0}, {1, 0, 0, 0}, {1, 1, 0, 0}, {0, 0, 0, 0}}; + return (const int(*)[4])shape; +} + +const int (*l_fig_down())[4] { + static const int shape[4][4] = { + {0, 0, 0, 0}, {1, 1, 1, 0}, {1, 0, 0, 0}, {0, 0, 0, 0}}; + return (const int(*)[4])shape; +} + +const int (*l_fig_left())[4] { + static const int shape[4][4] = { + {1, 1, 0, 0}, {0, 1, 0, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}}; + return (const int(*)[4])shape; +} + +// J +const int (*j_fig_up())[4] { + static const int shape[4][4] = { + {1, 0, 0, 0}, {1, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}}; + return (const int(*)[4])shape; +} + +const int (*j_fig_right())[4] { + static const int shape[4][4] = { + {0, 1, 1, 0}, {0, 1, 0, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}}; + return (const int(*)[4])shape; +} + +const int (*j_fig_down())[4] { + static const int shape[4][4] = { + {0, 0, 0, 0}, {1, 1, 1, 0}, {0, 0, 1, 0}, {0, 0, 0, 0}}; + return (const int(*)[4])shape; +} + +const int (*j_fig_left())[4] { + static const int shape[4][4] = { + {0, 1, 0, 0}, {0, 1, 0, 0}, {1, 1, 0, 0}, {0, 0, 0, 0}}; + return (const int(*)[4])shape; +} + +// S +const int (*s_fig_up())[4] { + static const int shape[4][4] = { + {0, 1, 1, 0}, {1, 1, 0, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}}; + return (const int(*)[4])shape; +} + +const int (*s_fig_right())[4] { + static const int shape[4][4] = { + {0, 1, 0, 0}, {0, 1, 1, 0}, {0, 0, 1, 0}, {0, 0, 0, 0}}; + return (const int(*)[4])shape; +} + +const int (*s_fig_down())[4] { + static const int shape[4][4] = { + {0, 1, 1, 0}, {1, 1, 0, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}}; + return (const int(*)[4])shape; +} + +const int (*s_fig_left())[4] { + static const int shape[4][4] = { + {0, 1, 0, 0}, {0, 1, 1, 0}, {0, 0, 1, 0}, {0, 0, 0, 0}}; + return (const int(*)[4])shape; +} + +// Z +const int (*z_fig_up())[4] { + static const int shape[4][4] = { + {1, 1, 0, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}}; + return (const int(*)[4])shape; +} + +const int (*z_fig_right())[4] { + static const int shape[4][4] = { + {0, 0, 1, 0}, {0, 1, 1, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}}; + return (const int(*)[4])shape; +} + +const int (*z_fig_down())[4] { + static const int shape[4][4] = { + {1, 1, 0, 0}, {0, 1, 1, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}}; + return (const int(*)[4])shape; +} + +const int (*z_fig_left())[4] { + static const int shape[4][4] = { + {0, 0, 1, 0}, {0, 1, 1, 0}, {0, 1, 0, 0}, {0, 0, 0, 0}}; + return (const int(*)[4])shape; +} + +const int (*empty_fig())[4] { + static const int shape[4][4] = { + {0, 0, 0, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}}; + return (const int(*)[4])shape; +} \ No newline at end of file diff --git a/src/diagram.pdf b/src/diagram.pdf new file mode 100644 index 0000000..fc85637 Binary files /dev/null and b/src/diagram.pdf differ diff --git a/src/diagram_also.pdf b/src/diagram_also.pdf new file mode 100644 index 0000000..2d8ae01 Binary files /dev/null and b/src/diagram_also.pdf differ diff --git a/src/doc.md b/src/doc.md new file mode 100644 index 0000000..847cd44 --- /dev/null +++ b/src/doc.md @@ -0,0 +1,218 @@ +# Tetris Game API Documentation + +## Overview + +This is a Tetris game implementation following FSM (Finite State Machine) pattern. +The backend is completely separated from frontend, allowing easy integration with any UI. + +## Public API + +### `void userInput(UserAction_t action, bool hold)` + +Processes user input and updates game state accordingly. + +**Parameters:** + +- `action`: User action from enum `UserAction_t` + - `Start`: Initialize/restart game + - `Pause`: Toggle pause state + - `Terminate`: Quit game and free resources + - `Left`: Move figure left + - `Right`: Move figure right + - `Up`: Release instant drop flag (for next drop) + - `Down`: Instant drop figure + - `Action`: Rotate figure +- `hold`: Reserved for future use (currently ignored) + +**Behavior:** + +- Blocked during pause (except Pause/Terminate/Start) +- Blocked during Attaching state (except Pause/Terminate/Start) +- Down action requires key release between uses (prevents double-drop) + +**Example (Rust FFI):** + +```rust +extern "C" { + fn userInput(action: UserAction_t, hold: bool); +} + +// In your Rust code: +unsafe { + userInput(UserAction_t::Left, false); +} +``` + +--- + +### `GameInfo_t updateCurrentState()` + +Updates game state machine and returns current game information for rendering. + +**Returns:** + +- `GameInfo_t` structure containing: + - `field`: 2D array `[FIELD_HEIGHT][FIELD_WIDTH]` representing game field + - `0`: Empty cell + - `1`: Active (falling) figure + - `2`: Placed (fixed) blocks + - `next`: 2D array `[4][4]` with next figure preview + - `score`: Current score + - `high_score`: Best score (persistent) + - `level`: Current level (1-10) + - `speed`: Current speed multiplier + - `pause`: Pause state (0=playing, 1=paused) + +**Call frequency:** +Should be called every game loop iteration (~10ms recommended). +The function handles timing internally using `clock_gettime(CLOCK_MONOTONIC)`. + +**Example (Rust FFI):** + +```rust +#[repr(C)] +struct GameInfo_t { + field: *mut *mut c_int, + next: *mut *mut c_int, + score: c_int, + high_score: c_int, + level: c_int, + speed: c_int, + pause: c_int, +} + +extern "C" { + fn updateCurrentState() -> GameInfo_t; +} + +// In your game loop: +loop { + let state = unsafe { updateCurrentState() }; + render_game(state); + std::thread::sleep(Duration::from_millis(10)); +} +``` + +--- + +## Game Constants + +```c +#define FIELD_WIDTH 10 +#define FIELD_HEIGHT 20 +``` + +## FSM States (Internal) + +- **Init**: Initialize game field and stats +- **Spawn**: Create new figure at top +- **Move**: Automatic figure falling +- **Moving**: User-controlled movement/rotation +- **Attaching**: Figure placement with delay +- **GameOver**: Game ended, waiting for restart + +## Timing + +- Base fall delay: 1100ms - (speed × 100ms) +- Instant drop delay: 30ms per step +- Attach delay: 350ms (prevents double-spawn) +- Pause compensates all timings correctly + +## Score System + +- 1 line: 100 points +- 2 lines: 300 points +- 3 lines: 700 points +- 4 lines: 1500 points +- Level up: every 600 points +- Speed increases with level (max level 10) + +## Memory Management + +All memory is managed internally. Call `userInput(Terminate, false)` before exit +to properly free resources. + +## Thread Safety + +**Not thread-safe!** Use from single thread only or add external synchronization. + +--- + +## Integration Example (Full Rust Frontend) + +```rust +use std::ffi::c_int; +use std::time::Duration; + +#[repr(C)] +#[derive(Copy, Clone)] +enum UserAction_t { + Start = 0, + Pause, + Terminate, + Left, + Right, + Up, + Down, + Action, +} + +#[repr(C)] +struct GameInfo_t { + field: *mut *mut c_int, + next: *mut *mut c_int, + score: c_int, + high_score: c_int, + level: c_int, + speed: c_int, + pause: c_int, +} + +#[link(name = "tetris", kind = "static")] +extern "C" { + fn userInput(action: UserAction_t, hold: bool); + fn updateCurrentState() -> GameInfo_t; +} + +fn main() { + // Initialize game + unsafe { userInput(UserAction_t::Start, false); } + + // Game loop + loop { + // Get input (your input handling) + let action = get_user_input(); + if let Some(a) = action { + unsafe { userInput(a, false); } + } + + // Update and render + let state = unsafe { updateCurrentState() }; + render_with_your_ui(state); + + std::thread::sleep(Duration::from_millis(10)); + } +} +``` + +--- + +## Building for FFI + +```sh +# Build static library +make + +# Use in your project +gcc your_frontend.c -L./build -ltetris -lncurses -lm -lrt -lpthread -o game +# or with Rust +cargo build --release +``` + +## Notes for Frontend Developers + +1. **Field coordinates**: [y][x] where y=0 is top, x=0 is left +2. **Next preview**: Always 4×4 matrix, figure may not fill entire space +3. **High score**: Automatically saved to `build/high_score.txt` +4. **No need to free** GameInfo_t - it's internal storage +5. **Call sequence**: Start → (updateCurrentState + userInput)* → Terminate diff --git a/src/gui/cli/display.c b/src/gui/cli/display.c new file mode 100644 index 0000000..8b1bbf1 --- /dev/null +++ b/src/gui/cli/display.c @@ -0,0 +1,41 @@ +// src/gui/cli/display.c +#include "../../brick_game/tetris/00_tetris.h" +#include + +void display_game(GameInfo_t game_state) { + clear(); + + for (int i = 0; i < FIELD_HEIGHT; ++i) { + for (int j = 0; j < FIELD_WIDTH; ++j) { + if (game_state.field[i][j] == 2) { + mvaddch(i + 1, j * 2 + 1, '#'); + } else if (game_state.field[i][j] == 1) { + mvaddch(i + 1, j * 2 + 1, '$'); + } else { + mvaddch(i + 1, j * 2 + 1, '.'); + } + } + } + + mvaddstr(1, FIELD_WIDTH * 2 + 5, "Next figure:"); + + for (int i = 0; i < 4; ++i) { + for (int j = 0; j < 4; ++j) { + if (game_state.next[i][j]) { + mvaddch(i + 3, (FIELD_WIDTH * 2 + 5) + j * 2, '@'); + } else { + mvaddch(i + 3, (FIELD_WIDTH * 2 + 5) + j * 2, ' '); + } + } + } + + mvprintw(FIELD_HEIGHT + 2, 1, "Score: %d", game_state.score); + mvprintw(FIELD_HEIGHT + 3, 1, "High Score: %d", game_state.high_score); + mvprintw(FIELD_HEIGHT + 4, 1, "Level: %d", game_state.level); + mvprintw(FIELD_HEIGHT + 5, 1, "Speed: %d", game_state.speed); + + if (game_state.pause) { + mvprintw(FIELD_HEIGHT / 2, FIELD_WIDTH * 2 + 1, "PAUSED"); + } + refresh(); +} \ No newline at end of file diff --git a/src/gui/cli/main.c b/src/gui/cli/main.c new file mode 100644 index 0000000..ca9ea44 --- /dev/null +++ b/src/gui/cli/main.c @@ -0,0 +1,81 @@ +#include "../../brick_game/tetris/00_tetris.h" +#include +#include +#include +#include + +void display_game(GameInfo_t game_state); + +int main() { + srand(time(NULL)); + + initscr(); + cbreak(); + noecho(); + keypad(stdscr, TRUE); + nodelay(stdscr, FALSE); + curs_set(0); + + mvprintw(FIELD_HEIGHT / 2, FIELD_WIDTH - 4, "Press F to Start"); + refresh(); + + int ch = 0; + int started = 0; + while (!started) { + ch = getch(); + if (ch == 'f' || ch == 'F') { + userInput(Start, false); + started = 1; + } + } + + nodelay(stdscr, TRUE); + timeout(10); + + UserAction_t current_action = {0}; + bool action_valid = false; + bool running = true; + + while (running) { + ch = getch(); + action_valid = false; + + if (ch == 'q') { + userInput(Terminate, false); + running = false; + } else if (ch == 'r' || ch == ' ') { + current_action = Action; + action_valid = true; + } else if (ch == KEY_LEFT) { + current_action = Left; + action_valid = true; + } else if (ch == KEY_RIGHT) { + current_action = Right; + action_valid = true; + } else if (ch == KEY_DOWN) { + current_action = Down; + action_valid = true; + } else if (ch == KEY_UP) { + current_action = Up; + action_valid = true; + } else if (ch == 's' || ch == 'S') { + current_action = Start; + action_valid = true; + } else if (ch == 'p' || ch == 'P') { + current_action = Pause; + action_valid = true; + } + + if (action_valid) { + userInput(current_action, false); + } + + if (running) { + GameInfo_t game_state = updateCurrentState(); + display_game(game_state); + } + } + + endwin(); + return 0; +} diff --git a/src/test/test.c b/src/test/test.c new file mode 100644 index 0000000..74a7640 --- /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; +} diff --git a/src/test/test_collision.c b/src/test/test_collision.c new file mode 100644 index 0000000..657639b --- /dev/null +++ b/src/test/test_collision.c @@ -0,0 +1,76 @@ +#include "test_helper.h" + +START_TEST(test_collision_bottom) { + test_setup(); + GameState_t *state = get_game_state(); + + state->curr.y = FIELD_HEIGHT; + state->curr.x = 5; + state->curr.mtrx[0][0] = 1; + + ck_assert_int_eq(check_collision(), 1); +} +END_TEST + +START_TEST(test_collision_left) { + test_setup(); + GameState_t *state = get_game_state(); + + state->curr.x = -1; + state->curr.y = 5; + state->curr.mtrx[0][0] = 1; + + ck_assert_int_eq(check_collision(), 1); +} +END_TEST + +START_TEST(test_collision_right) { + test_setup(); + GameState_t *state = get_game_state(); + + state->curr.x = FIELD_WIDTH; + state->curr.y = 5; + state->curr.mtrx[0][0] = 1; + + ck_assert_int_eq(check_collision(), 1); +} +END_TEST + +START_TEST(test_collision_placed_block) { + test_setup(); + 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) { + test_setup(); + 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 + +Suite *collision_suite(void) { + Suite *s = suite_create("Collision"); + TCase *tc = tcase_create("Core"); + + tcase_add_test(tc, test_collision_bottom); + tcase_add_test(tc, test_collision_left); + tcase_add_test(tc, test_collision_right); + tcase_add_test(tc, test_collision_placed_block); + tcase_add_test(tc, test_no_collision); + + suite_add_tcase(s, tc); + return s; +} diff --git a/src/test/test_figures.c b/src/test/test_figures.c new file mode 100644 index 0000000..c5fab70 --- /dev/null +++ b/src/test/test_figures.c @@ -0,0 +1,57 @@ +#include "test_helper.h" + +START_TEST(test_generate_figure) { + generate_next_figure(); + GameState_t *state = get_game_state(); + + ck_assert(state->next.sprite >= I && state->next.sprite < FIGURE_COUNT); + ck_assert_int_eq(state->next.rotation, 0); +} +END_TEST + +START_TEST(test_all_shapes_exist) { + for (int sprite = I; sprite < FIGURE_COUNT; sprite++) { + for (int rotation = 0; rotation < 4; rotation++) { + const int(*shape)[4] = get_figure_shape(sprite, rotation); + ck_assert_ptr_nonnull(shape); + } + } +} +END_TEST + +START_TEST(test_i_figure_horizontal) { + const int(*shape)[4] = get_figure_shape(I, 0); + + int count = 0; + for (int j = 0; j < 4; j++) { + if (shape[1][j]) + count++; + } + ck_assert_int_eq(count, 4); +} +END_TEST + +START_TEST(test_o_figure_unchanged) { + const int(*s1)[4] = get_figure_shape(O, 0); + const int(*s2)[4] = get_figure_shape(O, 2); + + for (int i = 0; i < 4; i++) { + for (int j = 0; j < 4; j++) { + ck_assert_int_eq(s1[i][j], s2[i][j]); + } + } +} +END_TEST + +Suite *figures_suite(void) { + Suite *s = suite_create("Figures"); + TCase *tc = tcase_create("Core"); + + tcase_add_test(tc, test_generate_figure); + tcase_add_test(tc, test_all_shapes_exist); + tcase_add_test(tc, test_i_figure_horizontal); + tcase_add_test(tc, test_o_figure_unchanged); + + suite_add_tcase(s, tc); + return s; +} diff --git a/src/test/test_fsm.c b/src/test/test_fsm.c new file mode 100644 index 0000000..4ceb5f7 --- /dev/null +++ b/src/test/test_fsm.c @@ -0,0 +1,354 @@ +#include "test_helper.h" +#include +#include + +// ============================================================================ +// Тесты FSM и интеграции +// ============================================================================ + +START_TEST(test_game_start) { + userInput(Start, false); + GameInfo_t state = updateCurrentState(); + + // После Start игра должна инициализироваться + ck_assert_int_eq(state.level, 1); + ck_assert_int_eq(state.score, 0); + ck_assert_int_eq(state.pause, 0); +} +END_TEST + +START_TEST(test_pause_toggle) { + userInput(Start, false); + updateCurrentState(); + + userInput(Pause, false); + GameInfo_t state = updateCurrentState(); + ck_assert_int_eq(state.pause, 1); + + userInput(Pause, false); + state = updateCurrentState(); + ck_assert_int_eq(state.pause, 0); +} +END_TEST + +START_TEST(test_field_initialization) { + userInput(Start, false); + GameInfo_t state = updateCurrentState(); + + int non_zero_count = 0; + for (int i = 0; i < FIELD_HEIGHT; i++) { + for (int j = 0; j < FIELD_WIDTH; j++) { + if (state.field[i][j] != 0) { + non_zero_count++; + } + } + } + + ck_assert_int_le(non_zero_count, 4); +} +END_TEST + +START_TEST(test_next_figure_exists) { + userInput(Start, false); + GameInfo_t state = updateCurrentState(); + + int has_blocks = 0; + for (int i = 0; i < 4; i++) { + for (int j = 0; j < 4; j++) { + if (state.next[i][j]) { + has_blocks = 1; + } + } + } + ck_assert_int_eq(has_blocks, 1); +} +END_TEST + +START_TEST(test_movement_left) { + userInput(Start, false); + updateCurrentState(); + + userInput(Left, false); + GameInfo_t state = updateCurrentState(); + + ck_assert_int_eq(state.pause, 0); +} +END_TEST + +START_TEST(test_movement_right) { + userInput(Start, false); + updateCurrentState(); + + userInput(Right, false); + GameInfo_t state = updateCurrentState(); + + ck_assert_int_eq(state.pause, 0); + ck_assert_int_ge(state.level, 1); +} +END_TEST + +START_TEST(test_rotation) { + userInput(Start, false); + updateCurrentState(); + + userInput(Action, false); + GameInfo_t state = updateCurrentState(); + + int has_figure = 0; + for (int i = 0; i < FIELD_HEIGHT; i++) { + for (int j = 0; j < FIELD_WIDTH; j++) { + if (state.field[i][j] == 1) { + has_figure = 1; + } + } + } + ck_assert_int_eq(has_figure, 1); +} +END_TEST + +START_TEST(test_user_input_actions) { + userInput(Start, false); + userInput(Left, false); + userInput(Right, false); + userInput(Action, false); + userInput(Pause, false); + userInput(Terminate, false); + + ck_assert_int_eq(1, 1); +} +END_TEST + +START_TEST(test_multiple_updates) { + userInput(Start, false); + + for (int i = 0; i < 10; i++) { + updateCurrentState(); + } + + GameInfo_t state = updateCurrentState(); + ck_assert_int_eq(state.level, 1); +} +END_TEST + +START_TEST(test_instant_drop_down_key) { + userInput(Start, false); + updateCurrentState(); + + GameState_t *state = get_game_state(); + state->down_key_was_released = 1; + + userInput(Down, false); + + ck_assert_int_eq(state->down_key_was_released, 0); +} +END_TEST + +START_TEST(test_up_key_release) { + userInput(Start, false); + updateCurrentState(); + + GameState_t *state = get_game_state(); + state->down_key_was_released = 0; + + userInput(Up, false); + updateCurrentState(); + + ck_assert_int_eq(state->down_key_was_released, 1); +} +END_TEST + +START_TEST(test_terminate_with_high_score) { + userInput(Start, false); + updateCurrentState(); + + GameState_t *state = get_game_state(); + state->info->high_score = 100; + state->info->score = 500; + + userInput(Terminate, false); + + int loaded = load_high_score(); + ck_assert_int_ge(loaded, 500); +} +END_TEST + +START_TEST(test_game_over_state) { + userInput(Start, false); + updateCurrentState(); + + GameState_t *state = get_game_state(); + + for (int i = 0; i < 2; i++) { + for (int j = 0; j < FIELD_WIDTH; j++) { + state->field[i][j] = 2; + } + } + + state->state = Spawn; + updateCurrentState(); + + ck_assert_int_eq(state->state, GameOver); +} +END_TEST + +START_TEST(test_do_gameover_clears_next) { + userInput(Start, false); + updateCurrentState(); + + GameState_t *state = get_game_state(); + + for (int i = 0; i < 2; i++) { + for (int j = 0; j < FIELD_WIDTH; j++) { + state->field[i][j] = 2; + } + } + + state->state = Spawn; + updateCurrentState(); + + state->state = GameOver; + updateCurrentState(); + + int has_blocks = 0; + for (int i = 0; i < 4; i++) { + for (int j = 0; j < 4; j++) { + if (state->next.mtrx[i][j]) { + has_blocks = 1; + } + } + } + ck_assert_int_eq(has_blocks, 0); +} +END_TEST + +START_TEST(test_place_figure_directly) { + GameState_t *state = get_game_state(); + + for (int i = 0; i < FIELD_HEIGHT; i++) + for (int j = 0; j < FIELD_WIDTH; j++) + state->field[i][j] = 0; + + state->curr.x = 5; + state->curr.y = 10; + for (int i = 0; i < 4; i++) + for (int j = 0; j < 4; j++) + state->curr.mtrx[i][j] = 0; + state->curr.mtrx[0][0] = 1; + state->curr.mtrx[0][1] = 1; + + place_figure(); + + ck_assert_int_eq(state->field[10][5], 2); + ck_assert_int_eq(state->field[10][6], 2); +} +END_TEST + +START_TEST(test_attach_state_transition) { + userInput(Start, false); + updateCurrentState(); + + GameState_t *state = get_game_state(); + + state->state = Attaching; + state->attach_start_time = 0; + state->attach_completed = 0; + + updateCurrentState(); + + ck_assert_int_gt(state->attach_start_time, 0); +} +END_TEST + +START_TEST(test_moving_to_down) { + userInput(Start, false); + updateCurrentState(); + + GameState_t *state = get_game_state(); + + state->curr.y = 5; + state->curr.x = 5; + for (int i = 0; i < 4; i++) + for (int j = 0; j < 4; j++) + state->curr.mtrx[i][j] = 0; + state->curr.mtrx[0][0] = 1; + + int initial_y = state->curr.y; + + state->state = Moving; + state->moving_type = ToDown; + + long long start = get_current_time_ms(); + updateCurrentState(); + long long elapsed = get_current_time_ms() - start; + + ck_assert(state->curr.y > initial_y || state->state == Attaching); + ck_assert_int_lt(elapsed, 1000); +} +END_TEST + +START_TEST(test_moving_do_nothing) { + userInput(Start, false); + updateCurrentState(); + + GameState_t *state = get_game_state(); + + state->state = Moving; + state->moving_type = DoNothing; + updateCurrentState(); + + ck_assert_int_eq(state->state, Move); +} +END_TEST + +START_TEST(test_automatic_falling) { + userInput(Start, false); + updateCurrentState(); + + GameState_t *state = get_game_state(); + + state->curr.y = 5; + state->curr.x = 5; + for (int i = 0; i < 4; i++) + for (int j = 0; j < 4; j++) + state->curr.mtrx[i][j] = 0; + state->curr.mtrx[0][0] = 1; + + state->state = Move; + state->last_move_time = 0; + + int initial_y = state->curr.y; + + updateCurrentState(); + + ck_assert(state->curr.y > initial_y || state->state == Attaching); +} +END_TEST + +Suite *fsm_suite(void) { + Suite *s = suite_create("FSM"); + TCase *tc = tcase_create("Core"); + + tcase_add_test(tc, test_game_start); + tcase_add_test(tc, test_pause_toggle); + tcase_add_test(tc, test_field_initialization); + tcase_add_test(tc, test_next_figure_exists); + tcase_add_test(tc, test_movement_left); + tcase_add_test(tc, test_movement_right); + tcase_add_test(tc, test_rotation); + tcase_add_test(tc, test_user_input_actions); + tcase_add_test(tc, test_multiple_updates); + + tcase_add_test(tc, test_instant_drop_down_key); + tcase_add_test(tc, test_up_key_release); + tcase_add_test(tc, test_terminate_with_high_score); + tcase_add_test(tc, test_game_over_state); + tcase_add_test(tc, test_do_gameover_clears_next); + tcase_add_test(tc, test_place_figure_directly); + tcase_add_test(tc, test_attach_state_transition); + tcase_add_test(tc, test_moving_to_down); + tcase_add_test(tc, test_moving_do_nothing); + tcase_add_test(tc, test_automatic_falling); + + suite_add_tcase(s, tc); + return s; +} diff --git a/src/test/test_helper.h b/src/test/test_helper.h new file mode 100644 index 0000000..75e1b72 --- /dev/null +++ b/src/test/test_helper.h @@ -0,0 +1,40 @@ +#ifndef TEST_HELPER_H +#define TEST_HELPER_H + +#include +#include "../brick_game/tetris/01_automato.h" + +// Хелпер для очистки поля +static inline void clear_test_field(void) { + GameState_t* state = get_game_state(); + for (int i = 0; i < FIELD_HEIGHT; i++) + for (int j = 0; j < FIELD_WIDTH; j++) + state->field[i][j] = 0; +} + +// Хелпер для очистки матрицы фигуры +static inline void clear_figure_matrix(void) { + GameState_t* state = get_game_state(); + for (int i = 0; i < 4; i++) + for (int j = 0; j < 4; j++) + state->curr.mtrx[i][j] = 0; +} + +// Хелпер для заполнения линии +static inline void fill_line(int row) { + GameState_t* state = get_game_state(); + for (int j = 0; j < FIELD_WIDTH; j++) { + state->field[row][j] = 2; + } +} + +// Setup функция (вызывается перед каждым тестом) +static inline void test_setup(void) { + GameState_t* state = get_game_state(); + clear_test_field(); + state->info->score = 0; + state->info->level = 1; + state->info->speed = 1; +} + +#endif diff --git a/src/test/test_lines.c b/src/test/test_lines.c new file mode 100644 index 0000000..617790f --- /dev/null +++ b/src/test/test_lines.c @@ -0,0 +1,76 @@ +#include "test_helper.h" + +START_TEST(test_clear_one_line) { + test_setup(); + fill_line(FIELD_HEIGHT - 1); + + clear_lines(); + + ck_assert_int_eq(get_game_state()->info->score, 100); +} +END_TEST + +START_TEST(test_clear_two_lines) { + test_setup(); + fill_line(FIELD_HEIGHT - 1); + fill_line(FIELD_HEIGHT - 2); + + clear_lines(); + + ck_assert_int_eq(get_game_state()->info->score, 300); +} +END_TEST + +START_TEST(test_clear_three_lines) { + test_setup(); + fill_line(FIELD_HEIGHT - 1); + fill_line(FIELD_HEIGHT - 2); + fill_line(FIELD_HEIGHT - 3); + + clear_lines(); + + ck_assert_int_eq(get_game_state()->info->score, 700); +} +END_TEST + +START_TEST(test_clear_four_lines) { + test_setup(); + fill_line(FIELD_HEIGHT - 1); + fill_line(FIELD_HEIGHT - 2); + fill_line(FIELD_HEIGHT - 3); + fill_line(FIELD_HEIGHT - 4); + + clear_lines(); + + ck_assert_int_eq(get_game_state()->info->score, 1500); +} +END_TEST + +START_TEST(test_incomplete_line) { + test_setup(); + GameState_t *state = get_game_state(); + + // Не полная линия + for (int j = 0; j < FIELD_WIDTH - 1; j++) { + state->field[FIELD_HEIGHT - 1][j] = 2; + } + + clear_lines(); + + ck_assert_int_eq(state->info->score, 0); +} +END_TEST + +Suite *lines_suite(void) { + Suite *s = suite_create("ClearLines"); + TCase *tc = tcase_create("Core"); + + tcase_add_test(tc, test_clear_one_line); + tcase_add_test(tc, test_clear_two_lines); + tcase_add_test(tc, test_clear_three_lines); + tcase_add_test(tc, test_clear_four_lines); + tcase_add_test(tc, test_incomplete_line); + + suite_add_tcase(s, tc); + return s; +} diff --git a/src/test/test_main.c b/src/test/test_main.c new file mode 100644 index 0000000..3476cca --- /dev/null +++ b/src/test/test_main.c @@ -0,0 +1,21 @@ +#include + +Suite *collision_suite(void); +Suite *lines_suite(void); +Suite *figures_suite(void); +Suite *score_suite(void); +Suite *fsm_suite(void); + +int main(void) { + SRunner *sr = srunner_create(collision_suite()); + srunner_add_suite(sr, lines_suite()); + srunner_add_suite(sr, figures_suite()); + srunner_add_suite(sr, score_suite()); + srunner_add_suite(sr, fsm_suite()); + + srunner_run_all(sr, CK_VERBOSE); + int failed = srunner_ntests_failed(sr); + srunner_free(sr); + + return (failed == 0) ? 0 : 1; +} diff --git a/src/test/test_score.c b/src/test/test_score.c new file mode 100644 index 0000000..8bf2f91 --- /dev/null +++ b/src/test/test_score.c @@ -0,0 +1,60 @@ +#include "test_helper.h" + +START_TEST(test_level_up) { + test_setup(); + GameState_t *state = get_game_state(); + + fill_line(FIELD_HEIGHT - 1); + clear_lines(); + + fill_line(FIELD_HEIGHT - 1); + fill_line(FIELD_HEIGHT - 2); + clear_lines(); + + fill_line(FIELD_HEIGHT - 1); + fill_line(FIELD_HEIGHT - 2); + clear_lines(); + + ck_assert_int_eq(state->info->level, 2); +} +END_TEST + +START_TEST(test_max_level) { + test_setup(); + GameState_t *state = get_game_state(); + + state->info->score = 10000; + fill_line(FIELD_HEIGHT - 1); + clear_lines(); + + ck_assert_int_le(state->info->level, 10); +} +END_TEST + +START_TEST(test_high_score_save) { + save_high_score(9999); + ck_assert_int_eq(load_high_score(), 9999); +} +END_TEST + +START_TEST(test_game_over_top) { + test_setup(); + GameState_t *state = get_game_state(); + + state->field[0][5] = 2; + ck_assert_int_eq(is_game_over(), 1); +} +END_TEST + +Suite *score_suite(void) { + Suite *s = suite_create("Score"); + TCase *tc = tcase_create("Core"); + + tcase_add_test(tc, test_level_up); + tcase_add_test(tc, test_max_level); + tcase_add_test(tc, test_high_score_save); + tcase_add_test(tc, test_game_over_top); + + suite_add_tcase(s, tc); + return s; +}