diff --git a/.gitignore b/.gitignore index 82d53e8..36a149e 100644 --- a/.gitignore +++ b/.gitignore @@ -62,3 +62,5 @@ src/high_score.txt src/build/high_score.txt code-samples/frogger/project.md dvi/ +gcov_report/ +build/ diff --git a/src/Makefile b/src/Makefile index e020c1b..1670361 100644 --- a/src/Makefile +++ b/src/Makefile @@ -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) diff --git a/src/test/test_collision.c b/src/test/test_collision.c new file mode 100644 index 0000000..4a32c05 --- /dev/null +++ b/src/test/test_collision.c @@ -0,0 +1,77 @@ +#include +#include "test_helper.h" + +START_TEST(test_collision_bottom) { + test_setup(); + GameState_t* state = get_game_state(); + + state->curr.y = FIELD_HEIGHT; + state->curr.x = 5; + state->curr.mtrx[0][0] = 1; + + ck_assert_int_eq(check_collision(), 1); +} +END_TEST + +START_TEST(test_collision_left) { + test_setup(); + GameState_t* state = get_game_state(); + + state->curr.x = -1; + state->curr.y = 5; + state->curr.mtrx[0][0] = 1; + + ck_assert_int_eq(check_collision(), 1); +} +END_TEST + +START_TEST(test_collision_right) { + test_setup(); + GameState_t* state = get_game_state(); + + state->curr.x = FIELD_WIDTH; + state->curr.y = 5; + state->curr.mtrx[0][0] = 1; + + ck_assert_int_eq(check_collision(), 1); +} +END_TEST + +START_TEST(test_collision_placed_block) { + test_setup(); + GameState_t* state = get_game_state(); + + state->field[10][5] = 2; + state->curr.y = 10; + state->curr.x = 5; + state->curr.mtrx[0][0] = 1; + + ck_assert_int_eq(check_collision(), 1); +} +END_TEST + +START_TEST(test_no_collision) { + test_setup(); + GameState_t* state = get_game_state(); + + state->curr.y = 5; + state->curr.x = 5; + state->curr.mtrx[0][0] = 1; + + ck_assert_int_eq(check_collision(), 0); +} +END_TEST + +Suite* collision_suite(void) { + Suite* s = suite_create("Collision"); + TCase* tc = tcase_create("Core"); + + tcase_add_test(tc, test_collision_bottom); + tcase_add_test(tc, test_collision_left); + tcase_add_test(tc, test_collision_right); + tcase_add_test(tc, test_collision_placed_block); + tcase_add_test(tc, test_no_collision); + + suite_add_tcase(s, tc); + return s; +} diff --git a/src/test/test_figures.c b/src/test/test_figures.c new file mode 100644 index 0000000..f51186f --- /dev/null +++ b/src/test/test_figures.c @@ -0,0 +1,57 @@ +#include +#include "test_helper.h" + +START_TEST(test_generate_figure) { + generate_next_figure(); + GameState_t* state = get_game_state(); + + ck_assert(state->next.sprite >= I && state->next.sprite < FIGURE_COUNT); + ck_assert_int_eq(state->next.rotation, 0); +} +END_TEST + +START_TEST(test_all_shapes_exist) { + for (int sprite = I; sprite < FIGURE_COUNT; sprite++) { + for (int rotation = 0; rotation < 4; rotation++) { + const int (*shape)[4] = get_figure_shape(sprite, rotation); + ck_assert_ptr_nonnull(shape); + } + } +} +END_TEST + +START_TEST(test_i_figure_horizontal) { + const int (*shape)[4] = get_figure_shape(I, 0); + + int count = 0; + for (int j = 0; j < 4; j++) { + if (shape[1][j]) count++; + } + ck_assert_int_eq(count, 4); +} +END_TEST + +START_TEST(test_o_figure_unchanged) { + const int (*s1)[4] = get_figure_shape(O, 0); + const int (*s2)[4] = get_figure_shape(O, 2); + + for (int i = 0; i < 4; i++) { + for (int j = 0; j < 4; j++) { + ck_assert_int_eq(s1[i][j], s2[i][j]); + } + } +} +END_TEST + +Suite* figures_suite(void) { + Suite* s = suite_create("Figures"); + TCase* tc = tcase_create("Core"); + + tcase_add_test(tc, test_generate_figure); + tcase_add_test(tc, test_all_shapes_exist); + tcase_add_test(tc, test_i_figure_horizontal); + tcase_add_test(tc, test_o_figure_unchanged); + + suite_add_tcase(s, tc); + return s; +} diff --git a/src/test/test_helper.h b/src/test/test_helper.h new file mode 100644 index 0000000..2577eb1 --- /dev/null +++ b/src/test/test_helper.h @@ -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 diff --git a/src/test/test_lines.c b/src/test/test_lines.c new file mode 100644 index 0000000..946a394 --- /dev/null +++ b/src/test/test_lines.c @@ -0,0 +1,77 @@ +#include +#include "test_helper.h" + +START_TEST(test_clear_one_line) { + test_setup(); + fill_line(FIELD_HEIGHT - 1); + + clear_lines(); + + ck_assert_int_eq(get_game_state()->info->score, 100); +} +END_TEST + +START_TEST(test_clear_two_lines) { + test_setup(); + fill_line(FIELD_HEIGHT - 1); + fill_line(FIELD_HEIGHT - 2); + + clear_lines(); + + ck_assert_int_eq(get_game_state()->info->score, 300); +} +END_TEST + +START_TEST(test_clear_three_lines) { + test_setup(); + fill_line(FIELD_HEIGHT - 1); + fill_line(FIELD_HEIGHT - 2); + fill_line(FIELD_HEIGHT - 3); + + clear_lines(); + + ck_assert_int_eq(get_game_state()->info->score, 700); +} +END_TEST + +START_TEST(test_clear_four_lines) { + test_setup(); + fill_line(FIELD_HEIGHT - 1); + fill_line(FIELD_HEIGHT - 2); + fill_line(FIELD_HEIGHT - 3); + fill_line(FIELD_HEIGHT - 4); + + clear_lines(); + + ck_assert_int_eq(get_game_state()->info->score, 1500); +} +END_TEST + +START_TEST(test_incomplete_line) { + test_setup(); + GameState_t* state = get_game_state(); + + // Не полная линия + for (int j = 0; j < FIELD_WIDTH - 1; j++) { + state->field[FIELD_HEIGHT - 1][j] = 2; + } + + clear_lines(); + + ck_assert_int_eq(state->info->score, 0); +} +END_TEST + +Suite* lines_suite(void) { + Suite* s = suite_create("ClearLines"); + TCase* tc = tcase_create("Core"); + + tcase_add_test(tc, test_clear_one_line); + tcase_add_test(tc, test_clear_two_lines); + tcase_add_test(tc, test_clear_three_lines); + tcase_add_test(tc, test_clear_four_lines); + tcase_add_test(tc, test_incomplete_line); + + suite_add_tcase(s, tc); + return s; +} diff --git a/src/test/test_main.c b/src/test/test_main.c new file mode 100644 index 0000000..d0c45a1 --- /dev/null +++ b/src/test/test_main.c @@ -0,0 +1,20 @@ +#include + +// Объявления 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; +} diff --git a/src/test/test_score.c b/src/test/test_score.c new file mode 100644 index 0000000..5b38dfb --- /dev/null +++ b/src/test/test_score.c @@ -0,0 +1,62 @@ +#include +#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; +}