This commit is contained in:
Rorikstr | Rust Dev 2025-10-19 19:56:50 +03:00
parent 218ee65e67
commit ef7b492b24
8 changed files with 355 additions and 18 deletions

2
.gitignore vendored
View file

@ -62,3 +62,5 @@ src/high_score.txt
src/build/high_score.txt
code-samples/frogger/project.md
dvi/
gcov_report/
build/

View file

@ -36,25 +36,17 @@ BINDIR = $(PREFIX)/bin
all: $(TARGET)
$(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 $@
run: $(TARGET)
./$(TARGET)
install: $(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)
@ -70,11 +62,12 @@ gcov_report: LDFLAGS += --coverage
gcov_report: clean test
@mkdir -p $(GCOV_DIR)
gcov $(TETRIS_SRC) -o $(TETRISDIR)
lcov --capture --directory . --output-file $(GCOV_DIR)/coverage.info
lcov --remove $(GCOV_DIR)/coverage.info '/usr/*' '*/nix/store/*' -o $(GCOV_DIR)/coverage.info
lcov --capture --directory $(TETRISDIR) --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)
mv *.gcov $(GCOV_DIR)/ 2>/dev/null || true
@mv *.gcov $(GCOV_DIR)/ 2>/dev/null || true
@echo "Report: $(GCOV_DIR)/index.html"
xdg-open $(GCOV_DIR)/index.html
dvi:
@mkdir -p $(DVI_DIR)
@ -92,9 +85,6 @@ dvi:
dist: clean
tar -czf tetris.tar.gz Makefile $(TETRISDIR) $(CLIDIR) $(TESTDIR) README.md doc.md
run: $(TARGET)
./$(TARGET)
style:
@if [ -f .clang-format ]; then \
clang-format -n $(TETRIS_SRC) $(CLI_SRC); \
@ -109,4 +99,17 @@ format:
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)

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

@ -0,0 +1,77 @@
#include <check.h>
#include "test_helper.h"
START_TEST(test_collision_bottom) {
test_setup();
GameState_t* state = get_game_state();
state->curr.y = FIELD_HEIGHT;
state->curr.x = 5;
state->curr.mtrx[0][0] = 1;
ck_assert_int_eq(check_collision(), 1);
}
END_TEST
START_TEST(test_collision_left) {
test_setup();
GameState_t* state = get_game_state();
state->curr.x = -1;
state->curr.y = 5;
state->curr.mtrx[0][0] = 1;
ck_assert_int_eq(check_collision(), 1);
}
END_TEST
START_TEST(test_collision_right) {
test_setup();
GameState_t* state = get_game_state();
state->curr.x = FIELD_WIDTH;
state->curr.y = 5;
state->curr.mtrx[0][0] = 1;
ck_assert_int_eq(check_collision(), 1);
}
END_TEST
START_TEST(test_collision_placed_block) {
test_setup();
GameState_t* state = get_game_state();
state->field[10][5] = 2;
state->curr.y = 10;
state->curr.x = 5;
state->curr.mtrx[0][0] = 1;
ck_assert_int_eq(check_collision(), 1);
}
END_TEST
START_TEST(test_no_collision) {
test_setup();
GameState_t* state = get_game_state();
state->curr.y = 5;
state->curr.x = 5;
state->curr.mtrx[0][0] = 1;
ck_assert_int_eq(check_collision(), 0);
}
END_TEST
Suite* collision_suite(void) {
Suite* s = suite_create("Collision");
TCase* tc = tcase_create("Core");
tcase_add_test(tc, test_collision_bottom);
tcase_add_test(tc, test_collision_left);
tcase_add_test(tc, test_collision_right);
tcase_add_test(tc, test_collision_placed_block);
tcase_add_test(tc, test_no_collision);
suite_add_tcase(s, tc);
return s;
}

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

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

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

@ -0,0 +1,39 @@
#ifndef TEST_HELPER_H
#define TEST_HELPER_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

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

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

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

@ -0,0 +1,20 @@
#include <check.h>
// Объявления Suite из других файлов
Suite* collision_suite(void);
Suite* lines_suite(void);
Suite* figures_suite(void);
Suite* score_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_run_all(sr, CK_VERBOSE);
int failed = srunner_ntests_failed(sr);
srunner_free(sr);
return (failed == 0) ? 0 : 1;
}

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

@ -0,0 +1,62 @@
#include <check.h>
#include "test_helper.h"
START_TEST(test_level_up) {
test_setup();
GameState_t* state = get_game_state();
// Набираем 700 очков
fill_line(FIELD_HEIGHT - 1);
clear_lines(); // +100
fill_line(FIELD_HEIGHT - 1);
fill_line(FIELD_HEIGHT - 2);
clear_lines(); // +300 = 400
fill_line(FIELD_HEIGHT - 1);
fill_line(FIELD_HEIGHT - 2);
clear_lines(); // +300 = 700
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;
}