Compare commits

...
Sign in to create a new pull request.

49 commits

Author SHA1 Message Date
Rorikstr | Rust Dev
532ca2e5ee feat: apply clang-format and remove nested repo 2025-10-24 19:43:39 +03:00
Rorikstr | Rust Dev
d4d99109fa Merge remote-tracking branch 'new-origin/master' into develop 2025-10-24 19:23:46 +03:00
Rorikstr | Rust Dev
2bda91134e release 2025-10-24 12:06:07 +03:00
Rorikstr | Rust Dev
c473bf3f7e prepare to merge into develop 2025-10-24 11:59:09 +03:00
Rorikstr | Rust Dev
8886f12761 removed 2025-10-20 03:01:24 +03:00
Rorikstr | Rust Dev
d108b9a74b diagrams 2025-10-20 02:58:59 +03:00
Rorikstr | Rust Dev
348cd40dcb ready 2025-10-20 02:35:00 +03:00
Rorikstr | Rust Dev
5fedf30996 tetris tar gz 2025-10-19 21:36:45 +03:00
Rorikstr | Rust Dev
9e712ae326 clang-format 2025-10-19 21:33:16 +03:00
Rorikstr | Rust Dev
ace9659ef5 stable 2025-10-19 21:30:42 +03:00
Rorikstr | Rust Dev
ef7b492b24 tests 2025-10-19 19:56:50 +03:00
Rorikstr | Rust Dev
218ee65e67 doxy 2025-10-19 19:29:21 +03:00
Rorikstr | Rust Dev
aa354f3258 dvi 2025-10-19 19:01:17 +03:00
Rorikstr | Rust Dev
31562af99d defines 2025-10-19 00:55:14 +03:00
Rorikstr | Rust Dev
411b2e4bb3 almost done 2025-10-15 18:24:43 +03:00
Rorikstr | Rust Dev
2f975d8e74 from frames speed to timespeed 2025-10-15 15:59:56 +03:00
Rorikstr | Rust Dev
7694d697e7 to build 2025-10-01 00:05:04 +03:00
Rorikstr | Rust Dev
cc6f9bb2d1 Decomposed and fixed saves 2025-10-01 00:01:44 +03:00
Rorikstr | Rust Dev
65d2c2e287 fixed bug after gameover 2025-09-30 00:09:00 +03:00
Rorikstr | Rust Dev
f071558a0f removed logger 2025-09-29 23:51:56 +03:00
Rorikstr | Rust Dev
4d17a14835 removed logger 2025-09-29 23:31:06 +03:00
Rorikstr | Rust Dev
cbf4f6bdcd c23 standard switch case issues 2025-09-29 23:24:32 +03:00
Rorikstr | Rust Dev
5ed3450d6f removed unnecessary 2025-09-29 23:19:46 +03:00
Rorikstr | Rust Dev
a0cec98aa7 it works 2025-09-29 23:06:30 +03:00
Rorikstr | Rust Dev
a298b6396d check that we chould discard time as speed issue 2025-09-29 22:10:40 +03:00
Rorikstr | Rust Dev
98035f17a2 searching speed issue 2025-09-29 21:57:33 +03:00
Rorikstr | Rust Dev
280cbee0a2 pause fixed 2025-09-29 20:24:49 +03:00
Rorikstr | Rust Dev
e58a842a43 remove log 2025-09-29 19:55:33 +03:00
Rorikstr | Rust Dev
0f2d03526e IT WORKS NOW 2025-09-29 19:23:14 +03:00
Rorikstr | Rust Dev
f5b65a390b it works but without figures 2025-09-29 17:17:35 +03:00
Rorikstr | Rust Dev
e9785c4906 segm fault 2025-09-29 16:49:02 +03:00
Rorikstr | Rust Dev
5fd528e22a arch 2025-09-29 15:20:21 +03:00
Rorikstr | Rust Dev
f8e1e664a7 Всё фигня, давай по новой 2025-09-29 13:42:58 +03:00
Rorikstr | Rust Dev
485ac0ca40 start is working 2025-09-28 19:38:54 +03:00
Rorikstr | Rust Dev
3c7e55dd6f no segs and correct points 2025-09-28 19:09:23 +03:00
Rorikstr | Rust Dev
6b80483129 works but with leaks 2025-09-28 17:01:42 +03:00
Rorikstr | Rust Dev
eaafb06836 it works without leaks and global variables except gamestate 2025-09-28 13:27:41 +03:00
Rorikstr | Rust Dev
9de308925c it's working but should fix return issue 2025-09-28 12:16:58 +03:00
Rorikstr | Rust Dev
1911d23459 demoved unn 2025-09-27 17:23:12 +03:00
Rorikstr | Rust Dev
f8ecb7be23 fixed static 2025-09-27 17:22:52 +03:00
Rorikstr | Rust Dev
c4171dc5d5 collision done 2025-09-27 00:09:18 +03:00
Rorikstr | Rust Dev
15703b5f28 renamed tetris to tetris_bin 2025-09-26 21:26:45 +03:00
Rorikstr | Rust Dev
afab29ec18 rotation is working 2025-09-26 16:23:22 +03:00
Rorikstr | Rust Dev
b0ab522b58 now it draws all seven 2025-09-26 14:23:10 +03:00
Rorikstr | Rust Dev
851d303aab fixed to more readable state 2025-09-26 14:11:45 +03:00
Rorikstr | Rust Dev
f8b74bf43d changing 2025-09-26 13:38:47 +03:00
Rorikstr | Rust Dev
fef0d8cbe3 Фигурки то рисуются! 2025-09-26 13:28:53 +03:00
Rorikstr | Rust Dev
e20765d252 makefile, deletion and for flakes 2025-09-25 21:31:59 +03:00
Rorikstr | Rust Dev
1feb55f404 init 2025-09-25 15:42:14 +03:00
32 changed files with 2646 additions and 75 deletions

View file

@ -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"
]
}

17
.gitignore vendored
View file

@ -42,6 +42,8 @@
*.idb *.idb
*.pdb *.pdb
*.gc*
# Kernel Module Compile Results # Kernel Module Compile Results
*.mod* *.mod*
*.cmd *.cmd
@ -50,3 +52,18 @@ modules.order
Module.symvers Module.symvers
Mkfile.old Mkfile.old
dkms.conf 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

View file

@ -1,81 +1,6 @@
# BrickGame Тетрис # 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 <div id="chapter-i"></div>
## Общая информация
### 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) ![Тетрис](misc/images/tetris-game.png)
@ -160,3 +85,50 @@ BrickGame — популярная портативная консоль 90-ых
### Часть 3. Дополнительно. Механика уровней ### Часть 3. Дополнительно. Механика уровней
Добавь в игру механику уровней. Каждый раз, когда игрок набирает 600 очков, уровень увеличивается на 1. Повышение уровня увеличивает скорость движения фигур. Максимальное количество уровней — 10. Добавь в игру механику уровней. Каждый раз, когда игрок набирает 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

61
flake.lock generated Normal file
View file

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

42
flake.nix Normal file
View file

@ -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"
'';
};
});
}

72
src/.gpskip Normal file
View file

@ -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

23
src/Doxyfile Normal file
View file

@ -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

129
src/Makefile Normal file
View file

@ -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)

View file

@ -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 <stdbool.h>
#include <stdio.h>
/**
* @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 */

View file

@ -0,0 +1,126 @@
#ifndef AUTOMATO_H
#define AUTOMATO_H
#define _POSIX_C_SOURCE 199309L
#include "00_tetris.h"
#include <stdbool.h>
#include <stdlib.h>
#include <time.h>
#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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

BIN
src/diagram.pdf Normal file

Binary file not shown.

BIN
src/diagram_also.pdf Normal file

Binary file not shown.

218
src/doc.md Normal file
View file

@ -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

41
src/gui/cli/display.c Normal file
View file

@ -0,0 +1,41 @@
// src/gui/cli/display.c
#include "../../brick_game/tetris/00_tetris.h"
#include <ncurses.h>
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();
}

81
src/gui/cli/main.c Normal file
View file

@ -0,0 +1,81 @@
#include "../../brick_game/tetris/00_tetris.h"
#include <ncurses.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>
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;
}

86
src/test/test.c Normal file
View file

@ -0,0 +1,86 @@
#include <check.h>
#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;
}

76
src/test/test_collision.c Normal file
View file

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

57
src/test/test_figures.c Normal file
View file

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

354
src/test/test_fsm.c Normal file
View file

@ -0,0 +1,354 @@
#include "test_helper.h"
#include <check.h>
#include <unistd.h>
// ============================================================================
// Тесты 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;
}

40
src/test/test_helper.h Normal file
View file

@ -0,0 +1,40 @@
#ifndef TEST_HELPER_H
#define TEST_HELPER_H
#include <check.h>
#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

76
src/test/test_lines.c Normal file
View file

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

21
src/test/test_main.c Normal file
View file

@ -0,0 +1,21 @@
#include <check.h>
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;
}

60
src/test/test_score.c Normal file
View file

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