Compare commits
49 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
532ca2e5ee | ||
|
|
d4d99109fa | ||
|
|
2bda91134e | ||
|
|
c473bf3f7e | ||
|
|
8886f12761 | ||
|
|
d108b9a74b | ||
|
|
348cd40dcb | ||
|
|
5fedf30996 | ||
|
|
9e712ae326 | ||
|
|
ace9659ef5 | ||
|
|
ef7b492b24 | ||
|
|
218ee65e67 | ||
|
|
aa354f3258 | ||
|
|
31562af99d | ||
|
|
411b2e4bb3 | ||
|
|
2f975d8e74 | ||
|
|
7694d697e7 | ||
|
|
cc6f9bb2d1 | ||
|
|
65d2c2e287 | ||
|
|
f071558a0f | ||
|
|
4d17a14835 | ||
|
|
cbf4f6bdcd | ||
|
|
5ed3450d6f | ||
|
|
a0cec98aa7 | ||
|
|
a298b6396d | ||
|
|
98035f17a2 | ||
|
|
280cbee0a2 | ||
|
|
e58a842a43 | ||
|
|
0f2d03526e | ||
|
|
f5b65a390b | ||
|
|
e9785c4906 | ||
|
|
5fd528e22a | ||
|
|
f8e1e664a7 | ||
|
|
485ac0ca40 | ||
|
|
3c7e55dd6f | ||
|
|
6b80483129 | ||
|
|
eaafb06836 | ||
|
|
9de308925c | ||
|
|
1911d23459 | ||
|
|
f8ecb7be23 | ||
|
|
c4171dc5d5 | ||
|
|
15703b5f28 | ||
|
|
afab29ec18 | ||
|
|
b0ab522b58 | ||
|
|
851d303aab | ||
|
|
f8b74bf43d | ||
|
|
fef0d8cbe3 | ||
|
|
e20765d252 | ||
|
|
1feb55f404 |
32 changed files with 2646 additions and 75 deletions
29
.devcontainer/devcontainer.json
Normal file
29
.devcontainer/devcontainer.json
Normal 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
17
.gitignore
vendored
|
|
@ -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
|
||||||
|
|
|
||||||
122
README_RUS.md
122
README_RUS.md
|
|
@ -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 были: тетрис, танки, гонки, фроггер и змейка.
|
|
||||||
|
|
||||||

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

|
|
||||||
|
|
||||||
«Фроггер» — одна из поздних игр, выходящих на консолях Brickgame. Игра представляет собой игровое поле, по которому движутся бревна, и, перепрыгивая по ним, игроку необходимо перевести лягушку с одного берега на другой. Если игрок попадает в воду или лягушка уходит за пределы игрового поля, то лягушка погибает. Игра завершается, когда игрок доводит лягушку до другого берега или погибает последняя лягушка.
|
|
||||||
|
|
||||||
Для формализации логики данной игры можно представить следующий вариант конечного автомата:
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
Данный КА имеет следующие состояния:
|
|
||||||
|
|
||||||
- Старт — состояние, в котором игра ждет, пока игрок нажмет кнопку готовности к игре.
|
|
||||||
- Спавн — состояние, в котором создается очередная лягушка.
|
|
||||||
- Перемещение — основное игровое состояние с обработкой ввода от пользователя: движение лягушки по полосе влево/право или прыжки вперед/назад.
|
|
||||||
- Сдвиг — состояние, которое наступает после истечения таймера, при котором все объекты на полосах сдвигаются вправо вместе с лягушкой.
|
|
||||||
- Столкновение — состояние, которое наступает, если после прыжка лягушка попадает в воду, или если после смещения бревен лягушка оказывается за пределами игрового поля.
|
|
||||||
- Достигнут другой берег — состояние, которое наступает при достижении лягушкой другого берега.
|
|
||||||
- Игра окончена — состояние, которое наступает после достижения другого берега или смерти последней лягушки.
|
|
||||||
|
|
||||||
Пример реализации фроггера с использованием КА ты можешь найти в папке `code-samples`.
|
|
||||||
|
|
||||||
### Тетрис
|
### Тетрис
|
||||||
|
|
||||||

|

|
||||||
|
|
@ -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
61
flake.lock
generated
Normal 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
42
flake.nix
Normal 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
72
src/.gpskip
Normal 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
23
src/Doxyfile
Normal 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
129
src/Makefile
Normal 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)
|
||||||
165
src/brick_game/tetris/00_tetris.h
Normal file
165
src/brick_game/tetris/00_tetris.h
Normal 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 */
|
||||||
126
src/brick_game/tetris/01_automato.h
Normal file
126
src/brick_game/tetris/01_automato.h
Normal 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
|
||||||
136
src/brick_game/tetris/02_tetris.c
Normal file
136
src/brick_game/tetris/02_tetris.c
Normal 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;
|
||||||
|
}
|
||||||
91
src/brick_game/tetris/03_automato.c
Normal file
91
src/brick_game/tetris/03_automato.c
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
23
src/brick_game/tetris/04_init.c
Normal file
23
src/brick_game/tetris/04_init.c
Normal 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;
|
||||||
|
}
|
||||||
37
src/brick_game/tetris/05_spawn.c
Normal file
37
src/brick_game/tetris/05_spawn.c
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
38
src/brick_game/tetris/06_move.c
Normal file
38
src/brick_game/tetris/06_move.c
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
74
src/brick_game/tetris/07_moving.c
Normal file
74
src/brick_game/tetris/07_moving.c
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
124
src/brick_game/tetris/08_attaching.c
Normal file
124
src/brick_game/tetris/08_attaching.c
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
30
src/brick_game/tetris/09_gameover.c
Normal file
30
src/brick_game/tetris/09_gameover.c
Normal 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;
|
||||||
|
}
|
||||||
272
src/brick_game/tetris/figure_sprites.c
Normal file
272
src/brick_game/tetris/figure_sprites.c
Normal 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
BIN
src/diagram.pdf
Normal file
Binary file not shown.
BIN
src/diagram_also.pdf
Normal file
BIN
src/diagram_also.pdf
Normal file
Binary file not shown.
218
src/doc.md
Normal file
218
src/doc.md
Normal 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
41
src/gui/cli/display.c
Normal 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
81
src/gui/cli/main.c
Normal 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
86
src/test/test.c
Normal 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
76
src/test/test_collision.c
Normal 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
57
src/test/test_figures.c
Normal 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
354
src/test/test_fsm.c
Normal 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
40
src/test/test_helper.h
Normal 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
76
src/test/test_lines.c
Normal 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
21
src/test/test_main.c
Normal 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
60
src/test/test_score.c
Normal 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;
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue