This commit is contained in:
Rorikstr | Rust Dev 2025-10-19 00:55:14 +03:00
parent 411b2e4bb3
commit 31562af99d
6 changed files with 193 additions and 111 deletions

View file

@ -1,81 +1,6 @@
# BrickGame Тетрис
Резюме: в данном проекте тебе предстоит реализовать игру «Тетрис» на языке программирования С с использованием структурного подхода.
💡 [Нажми сюда](https://new.oprosso.net/p/4cb31ec3f47a4596bc758ea1861fb624), **чтобы поделиться с нами обратной связью на этот проект**. Это анонимно и поможет нашей команде сделать обучение лучше. Рекомендуем заполнить опрос сразу после выполнения проекта.
## Содержание
- [BrickGame Тетрис](#brickgame-тетрис)
- [Содержание](#содержание)
- [Введение](#введение)
- [Chapter I ](#chapter-i-)
- [Общая информация](#общая-информация)
- [BrickGame](#brickgame)
- [История тетриса](#история-тетриса)
- [Конечные автоматы](#конечные-автоматы)
- [Фроггер](#фроггер)
- [Тетрис](#тетрис)
- [Chapter II ](#chapter-ii-)
- [Требования к проекту](#требования-к-проекту)
- [Часть 1. Основное задание](#часть-1-основное-задание)
- [Часть 2. Дополнительно. Подсчет очков и рекорд в игре](#часть-2-дополнительно-подсчет-очков-и-рекорд-в-игре)
- [Часть 3. Дополнительно. Механика уровней](#часть-3-дополнительно-механика-уровней)
## Введение
Для реализации игры «Тетрис» проект должен состоять из двух частей: библиотеки, реализующей логику работы игры, которую можно в будущем подключать к различным GUI, и терминального интерфейса. Логика работы библиотеки должна быть реализована с использованием конечных автоматов, одно из возможных описаний которого будет дано ниже.
## Chapter I <div id="chapter-i"></div>
## Общая информация
### BrickGame
BrickGame — популярная портативная консоль 90-ых годов с несколькими ~~тысячами~~ встроенными играми, разработана она была в Китае. Изначально эта игра была копией, разработанной в СССР и выпущенной Nintendo в рамках платформы GameBoy игры «Тетрис», но включала в себя также и множество других игр, которые добавлялись с течением времени. Консоль имела небольшой экранчик с игровым полем размера 10 х 20, представляющим из себя матрицу «пикселей». Справа от поля находилось табло с цифровой индикацией состояния текущей игры, рекордами и прочей дополнительной информацией. Самыми распространенными играми на BrickGame были: тетрис, танки, гонки, фроггер и змейка.
![BrickGameConsole](misc/images/brickgame-console.jpg)
### История тетриса
«Тетрис» был написан Алексеем Пажитновым 6 июня 1984 года на компьютере Электроника-60. Игра представляла собой головоломку, построенную на использовании геометрических фигур «тетрамино», состоящих из четырех квадратов. Первая коммерческая версия игры была выпущена в Америке в 1987 году. В последующие годы «Тетрис» был портирован на множество различных устройств, в том числе на мобильные телефоны, калькуляторы и карманные персональные компьютеры.
Наибольшую популярность приобрела реализация «Тетриса» для игровой консоли Game Boy и видеоприставки NES. Но кроме нее существуют различные версии игры. Например, есть версия с трехмерными фигурами или дуэльная версия, в которой два игрока получают одинаковые фигуры и пытаются обойти друг друга по очкам.
### Конечные автоматы
Конечный автомат (КА) в теории алгоритмов — математическая абстракция, модель дискретного устройства, имеющего один вход, один выход и в каждый момент времени находящегося в одном состоянии из множества возможных.
При работе КА на вход последовательно поступают входные воздействия, а на выходе КА формирует выходные сигналы. Переход из одного внутреннего состояния КА в другое может происходить не только от внешнего воздействия, но и самопроизвольно.
КА можно использовать для описания алгоритмов, позволяющих решать те или иные задачи, а также для моделирования практически любого процесса. Несколько примеров:
- Логика искусственного интеллекта для игр;
- Синтаксический и лексический анализ;
- Сложные прикладные сетевые протоколы;
- Потоковая обработка данных.
Ниже представлены примеры использования КА для формализации игровой логики нескольких игр из BrickGame.
### Фроггер
![Фроггер](misc/images/frogger-game.png)
«Фроггер» — одна из поздних игр, выходящих на консолях Brickgame. Игра представляет собой игровое поле, по которому движутся бревна, и, перепрыгивая по ним, игроку необходимо перевести лягушку с одного берега на другой. Если игрок попадает в воду или лягушка уходит за пределы игрового поля, то лягушка погибает. Игра завершается, когда игрок доводит лягушку до другого берега или погибает последняя лягушка.
Для формализации логики данной игры можно представить следующий вариант конечного автомата:
![Конечный автомат фроггера](misc/images/frogger.jpg)
Данный КА имеет следующие состояния:
- Старт — состояние, в котором игра ждет, пока игрок нажмет кнопку готовности к игре.
- Спавн — состояние, в котором создается очередная лягушка.
- Перемещение — основное игровое состояние с обработкой ввода от пользователя: движение лягушки по полосе влево/право или прыжки вперед/назад.
- Сдвиг — состояние, которое наступает после истечения таймера, при котором все объекты на полосах сдвигаются вправо вместе с лягушкой.
- Столкновение — состояние, которое наступает, если после прыжка лягушка попадает в воду, или если после смещения бревен лягушка оказывается за пределами игрового поля.
- Достигнут другой берег — состояние, которое наступает при достижении лягушкой другого берега.
- Игра окончена — состояние, которое наступает после достижения другого берега или смерти последней лягушки.
Пример реализации фроггера с использованием КА ты можешь найти в папке `code-samples`.
### Тетрис
![Тетрис](misc/images/tetris-game.png)
@ -160,3 +85,50 @@ BrickGame — популярная портативная консоль 90-ых
### Часть 3. Дополнительно. Механика уровней
Добавь в игру механику уровней. Каждый раз, когда игрок набирает 600 очков, уровень увеличивается на 1. Повышение уровня увеличивает скорость движения фигур. Максимальное количество уровней — 10.
# Tetris Game
Classic Tetris implementation in C11 with ncurses interface.
## Requirements
- GCC or Clang
- ncurses library
- Check framework (for tests)
## Building
make # Build the game
make run # Build and run
make test # Run unit tests
make install # Install to ~/.local/bin
text
## Controls
- **Arrow Keys**: Move left/right, rotate (up)
- **Down Arrow**: Instant drop
- **Space/R**: Rotate figure
- **P**: Pause
- **S**: Restart game
- **Q**: Quit
## Features
- 7 classic Tetris figures
- Score tracking with persistent high score
- 10 speed levels
- Smooth time-based movement using POSIX `clock_gettime()`
- Structured programming principles
## Architecture
- **FSM-based game logic** with states: Init, Spawn, Move, Moving, Attaching, GameOver
- **Separated frontend/backend**: `brick_game/` (logic) and `gui/cli/` (display)
- **Time-based delays** for precise falling speed
## License
School 21 educational project

View file

@ -1,8 +1,7 @@
.PHONY: all clean valgrind test style format gcov_report all install uninstall
.PHONY: all clean install uninstall test gcov_report dvi dist run style format
# Автоопределение компилятора и флагов
CC ?= gcc
CFLAGS ?= -Wall -Wextra -std=c11 -g -D_POSIX_C_SOURCE=199309L
CFLAGS ?= -Wall -Wextra -Werror -std=c11 -g -D_POSIX_C_SOURCE=199309L -MMD -MP
CHECK_CFLAGS ?= -I/usr/include/check
# Проверяем наличие библиотек
@ -14,11 +13,7 @@ else
LDFLAGS ?= -lcheck -lrt -lpthread -lm -lncurses
endif
SRCDIR = .
TESTDIR = test
BUILDDIR = build
# src/Makefile
TETRISDIR = brick_game/tetris
CLIDIR = gui/cli
@ -32,10 +27,10 @@ LIB_TETRIS = $(BUILDDIR)/libtetris.a
TARGET = $(BUILDDIR)/tetris_bin.out
# Установка
PREFIX ?= /usr/local
PREFIX ?= $(HOME)/.local
BINDIR = $(PREFIX)/bin
all: clean $(TARGET)
all: $(TARGET)
$(LIB_TETRIS): $(TETRIS_OBJ)
mkdir -p $(BUILDDIR)
@ -43,7 +38,6 @@ $(LIB_TETRIS): $(TETRIS_OBJ)
$(TARGET): $(LIB_TETRIS) $(CLI_OBJ)
$(CC) $(CLI_OBJ) -L$(BUILDDIR) -ltetris -o $@ $(LDFLAGS)
rm -f $(CLI_OBJ) $(TETRIS_OBJ)
brick_game/tetris/%.o: brick_game/tetris/%.c
$(CC) $(CFLAGS) -c $< -o $@
@ -51,36 +45,51 @@ brick_game/tetris/%.o: brick_game/tetris/%.c
gui/cli/%.o: gui/cli/%.c
$(CC) $(CFLAGS) -c $< -o $@
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
install: $(TARGET)
install -m 755 $(TARGET) $(BINDIR)/
install -m 755 $(TARGET) $(BINDIR)/tetris_bin.out
uninstall:
rm -f $(BINDIR)/$(TARGET)
rm -f $(BINDIR)/tetris_bin.out
clean:
rm -f $(CLI_OBJ) $(TETRIS_OBJ) $(TARGET) $(LIB_TETRIS) *.gcda *.gcno *.gcov
rm -rf $(CLI_OBJ) $(TETRIS_OBJ) $(TARGET) $(LIB_TETRIS)
rm -rf $(BUILDDIR)/*.gcda $(BUILDDIR)/*.gcno *.gcda *.gcno *.gcov $(TETRISDIR)/*.d $(CLIDIR)/*.d
rm -rf coverage.info coverage_report
test:
@echo "Running tests..."
# Здесь будет вызов тестов, если они есть
test: $(LIB_TETRIS)
$(CC) $(CFLAGS) test/test.c -L$(BUILDDIR) -ltetris $(LDFLAGS) -o $(BUILDDIR)/test.out
./$(BUILDDIR)/test.out
gcov_report: CFLAGS += --coverage
gcov_report: LDFLAGS += --coverage
gcov_report: clean $(TARGET)
./$(TARGET)
@echo "Note: Run automated tests for proper coverage"
gcov $(TETRIS_SRC)
lcov --capture --directory . --output-file coverage.info
genhtml coverage.info --output-directory coverage_report
@echo "Coverage report generated in coverage_report/index.html"
@echo "Coverage report: coverage_report/index.html"
dvi:
doxygen Doxyfile 2>/dev/null || echo "Doxyfile not found"
dist: clean
tar -czf tetris.tar.gz Makefile $(TETRISDIR) $(CLIDIR) README.md
run: $(TARGET)
./$(TARGET)
style:
@cp ../materials/linters/.clang-format .
@clang-format -n *.c *.h
@rm .clang-format
@if [ -f .clang-format ]; then \
clang-format -n $(TETRIS_SRC) $(CLI_SRC); \
else \
echo ".clang-format not found"; \
fi
format:
cp ../materials/linters/.clang-format .
clang-format -i *.c *.h
rm .clang-format
@if [ -f .clang-format ]; then \
clang-format -i $(TETRIS_SRC) $(CLI_SRC); \
else \
echo ".clang-format not found"; \
fi
-include $(TETRIS_OBJ:.o=.d) $(CLI_OBJ:.o=.d)

View file

@ -8,6 +8,21 @@
#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,

View file

@ -5,10 +5,10 @@ int get_milliseconds_to_wait(void) {
int result = 0;
if (state->moving_type == ToDown) {
result = 30;
result = INSTANT_DROP_DELAY_MS;
} else {
int base_delay = 1100 - (state->info->speed * 100);
result = (base_delay > 100) ? base_delay : 100;
int base_delay = BASE_FALL_DELAY_MS - (state->info->speed * SPEED_MULTIPLIER_MS);
result = (base_delay > SPEED_MULTIPLIER_MS) ? base_delay : SPEED_MULTIPLIER_MS;
}
return result;

View file

@ -15,7 +15,7 @@ void do_attaching(void) {
}
// Проверяем, прошло ли 350мс
if (current_time - state->attach_start_time >= 350) {
if (current_time - state->attach_start_time >= ATTACH_DELAY_MS) {
state->attach_completed = 1;
state->attach_start_time = 0; // Сбрасываем таймер
@ -108,21 +108,21 @@ void clear_lines() {
}
if (lines_cleared > 0) {
int points[] = {0, 100, 300, 700, 1500};
int points[] = {0, POINTS_ONE_LINE, POINTS_TWO_LINES, POINTS_THREE_LINES, POINTS_FOUR_LINES};
state->info->score += points[lines_cleared];
if (state->info->score > state->info->high_score) {
state->info->high_score = state->info->score;
}
int new_level = (state->info->score / 600) + 1;
if (new_level > 10) {
new_level = 10;
int new_level = (state->info->score / SCORE_PER_LEVEL) + 1;
if (new_level > MAX_LEVEL) {
new_level = MAX_LEVEL;
}
if (new_level > state->info->level) {
state->info->level = new_level;
state->info->speed = new_level * 3;
state->info->speed = new_level + (new_level / 2);
}
}
}

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

@ -0,0 +1,86 @@
#include <check.h>
#include "../brick_game/tetris/01_automato.h"
START_TEST(test_collision_bottom_boundary) {
GameState_t* state = get_game_state();
state->curr.y = FIELD_HEIGHT - 1;
state->curr.x = 5;
state->curr.mtrx[0][0] = 1;
state->curr.y++; // Выходим за границу
ck_assert_int_eq(check_collision(), 1);
}
END_TEST
START_TEST(test_collision_left_boundary) {
GameState_t* state = get_game_state();
state->curr.x = -1;
state->curr.mtrx[0][0] = 1;
ck_assert_int_eq(check_collision(), 1);
}
END_TEST
START_TEST(test_collision_with_placed_block) {
GameState_t* state = get_game_state();
state->field[10][5] = 2; // Размещённый блок
state->curr.y = 10;
state->curr.x = 5;
state->curr.mtrx[0][0] = 1;
ck_assert_int_eq(check_collision(), 1);
}
END_TEST
START_TEST(test_no_collision) {
GameState_t* state = get_game_state();
state->curr.y = 5;
state->curr.x = 5;
state->curr.mtrx[0][0] = 1;
ck_assert_int_eq(check_collision(), 0);
}
END_TEST
START_TEST(test_game_over_detection) {
GameState_t* state = get_game_state();
state->field[0][5] = 2; // Блок в верхней строке
ck_assert_int_eq(is_game_over(), 1);
}
END_TEST
START_TEST(test_clear_single_line) {
GameState_t* state = get_game_state();
state->info->score = 0;
// Заполняем нижнюю линию
for (int j = 0; j < FIELD_WIDTH; j++) {
state->field[FIELD_HEIGHT - 1][j] = 2;
}
clear_lines();
ck_assert_int_eq(state->info->score, 100);
}
END_TEST
Suite* tetris_suite(void) {
Suite* s = suite_create("Tetris");
TCase* tc_core = tcase_create("Core");
tcase_add_test(tc_core, test_collision_bottom_boundary);
tcase_add_test(tc_core, test_collision_left_boundary);
tcase_add_test(tc_core, test_collision_with_placed_block);
tcase_add_test(tc_core, test_no_collision);
tcase_add_test(tc_core, test_game_over_detection);
tcase_add_test(tc_core, test_clear_single_line);
suite_add_tcase(s, tc_core);
return s;
}
int main(void) {
Suite* s = tetris_suite();
SRunner* sr = srunner_create(s);
srunner_run_all(sr, CK_NORMAL);
int number_failed = srunner_ntests_failed(sr);
srunner_free(sr);
return (number_failed == 0) ? 0 : 1;
}