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 были: тетрис, танки, гонки, фроггер и змейка.
-
-
-
-### История тетриса
-
-«Тетрис» был написан Алексеем Пажитновым 6 июня 1984 года на компьютере Электроника-60. Игра представляла собой головоломку, построенную на использовании геометрических фигур «тетрамино», состоящих из четырех квадратов. Первая коммерческая версия игры была выпущена в Америке в 1987 году. В последующие годы «Тетрис» был портирован на множество различных устройств, в том числе на мобильные телефоны, калькуляторы и карманные персональные компьютеры.
-
-Наибольшую популярность приобрела реализация «Тетриса» для игровой консоли Game Boy и видеоприставки NES. Но кроме нее существуют различные версии игры. Например, есть версия с трехмерными фигурами или дуэльная версия, в которой два игрока получают одинаковые фигуры и пытаются обойти друг друга по очкам.
-
-### Конечные автоматы
-
-Конечный автомат (КА) в теории алгоритмов — математическая абстракция, модель дискретного устройства, имеющего один вход, один выход и в каждый момент времени находящегося в одном состоянии из множества возможных.
-
-При работе КА на вход последовательно поступают входные воздействия, а на выходе КА формирует выходные сигналы. Переход из одного внутреннего состояния КА в другое может происходить не только от внешнего воздействия, но и самопроизвольно.
-
-КА можно использовать для описания алгоритмов, позволяющих решать те или иные задачи, а также для моделирования практически любого процесса. Несколько примеров:
-
-- Логика искусственного интеллекта для игр;
-- Синтаксический и лексический анализ;
-- Сложные прикладные сетевые протоколы;
-- Потоковая обработка данных.
-
-Ниже представлены примеры использования КА для формализации игровой логики нескольких игр из BrickGame.
-
-### Фроггер
-
-
-
-«Фроггер» — одна из поздних игр, выходящих на консолях Brickgame. Игра представляет собой игровое поле, по которому движутся бревна, и, перепрыгивая по ним, игроку необходимо перевести лягушку с одного берега на другой. Если игрок попадает в воду или лягушка уходит за пределы игрового поля, то лягушка погибает. Игра завершается, когда игрок доводит лягушку до другого берега или погибает последняя лягушка.
-
-Для формализации логики данной игры можно представить следующий вариант конечного автомата:
-
-
-
-Данный КА имеет следующие состояния:
-
-- Старт — состояние, в котором игра ждет, пока игрок нажмет кнопку готовности к игре.
-- Спавн — состояние, в котором создается очередная лягушка.
-- Перемещение — основное игровое состояние с обработкой ввода от пользователя: движение лягушки по полосе влево/право или прыжки вперед/назад.
-- Сдвиг — состояние, которое наступает после истечения таймера, при котором все объекты на полосах сдвигаются вправо вместе с лягушкой.
-- Столкновение — состояние, которое наступает, если после прыжка лягушка попадает в воду, или если после смещения бревен лягушка оказывается за пределами игрового поля.
-- Достигнут другой берег — состояние, которое наступает при достижении лягушкой другого берега.
-- Игра окончена — состояние, которое наступает после достижения другого берега или смерти последней лягушки.
-
-Пример реализации фроггера с использованием КА ты можешь найти в папке `code-samples`.
-
### Тетрис

@@ -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;
+}