├── .gitignore ├── .clang-format ├── LICENSE ├── tests ├── test.h ├── test-types.c ├── test-block.c ├── driver.c ├── test-nalloc.c ├── test-move.c └── test-shape.c ├── Makefile ├── block.c ├── README.md ├── main.c ├── utils.h ├── nalloc.c ├── shape.c ├── train.c └── tetris.h /.gitignore: -------------------------------------------------------------------------------- 1 | *.o 2 | .*.o.d 3 | .tests 4 | tetris 5 | tests/test-runner 6 | train 7 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | BasedOnStyle: Chromium 2 | Language: Cpp 3 | MaxEmptyLinesToKeep: 3 4 | IndentCaseLabels: false 5 | AllowShortIfStatementsOnASingleLine: false 6 | AllowShortCaseLabelsOnASingleLine: false 7 | AllowShortLoopsOnASingleLine: false 8 | DerivePointerAlignment: false 9 | PointerAlignment: Right 10 | SpaceAfterCStyleCast: true 11 | TabWidth: 4 12 | UseTab: Never 13 | IndentWidth: 4 14 | BreakBeforeBraces: Linux 15 | AccessModifierOffset: -4 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022, 2025 National Cheng Kung University, Taiwan 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 16 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /tests/test.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | /* ANSI color macros for test output */ 8 | #define COLOR_RESET "\033[0m" 9 | #define COLOR_RED "\033[91m" 10 | #define COLOR_GREEN "\033[92m" 11 | #define COLOR_YELLOW "\033[93m" 12 | #define COLOR_CYAN "\033[96m" 13 | #define COLOR_WHITE "\033[42m" 14 | #define COLOR_BOLD "\033[1m" 15 | 16 | /* Test output wrapper macros */ 17 | #define TEST_FAIL_PREFIX COLOR_RED "FAIL:" COLOR_RESET " " 18 | #define TEST_PASS_PREFIX COLOR_GREEN "PASS:" COLOR_RESET " " 19 | #define TEST_SUMMARY_FORMAT \ 20 | COLOR_YELLOW "%zu TESTS" COLOR_RESET " / " COLOR_GREEN \ 21 | "%zu PASSED" COLOR_RESET " / " COLOR_RED \ 22 | "%zu FAILED" COLOR_RESET "\n" 23 | #define TEST_ALL_PASSED COLOR_WHITE "ALL %zu TESTS PASSED" COLOR_RESET "\n" 24 | #define TEST_CATEGORY_HEADER COLOR_BOLD COLOR_CYAN "=== %s ===" COLOR_RESET "\n" 25 | #define TEST_CATEGORY_FOOTER COLOR_CYAN "%s completed" COLOR_RESET "\n\n" 26 | 27 | /* Test function declarations */ 28 | bool assert_test(bool condition, const char *format, ...); 29 | void start_test_category(const char *name); 30 | void end_test_category(const char *name); 31 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CFLAGS = -Wall -O2 -g 2 | LDFLAGS = 3 | 4 | PROG = tetris 5 | TEST_PROG = tests/test-runner 6 | TRAIN = train 7 | 8 | # Main program source files 9 | MAIN_SRCS = main.c 10 | 11 | # Common source files shared between main program and tests 12 | COMMON_SRCS = \ 13 | nalloc.c \ 14 | block.c \ 15 | shape.c \ 16 | grid.c \ 17 | move.c \ 18 | tui.c \ 19 | game.c 20 | 21 | # Test source files 22 | TEST_SRCS = \ 23 | tests/test-types.c \ 24 | tests/test-nalloc.c \ 25 | tests/test-block.c \ 26 | tests/test-shape.c \ 27 | tests/test-grid.c \ 28 | tests/test-move.c \ 29 | tests/test-game.c \ 30 | tests/driver.c 31 | 32 | # Training program source 33 | TRAIN_SRCS = \ 34 | train.c 35 | 36 | # Object files 37 | MAIN_OBJS = $(MAIN_SRCS:.c=.o) 38 | COMMON_OBJS = $(COMMON_SRCS:.c=.o) 39 | TEST_OBJS = $(TEST_SRCS:.c=.o) 40 | TRAIN_OBJS = $(TRAIN_SRCS:.c=.o) 41 | 42 | # All object files for dependency tracking 43 | ALL_OBJS = $(MAIN_OBJS) $(COMMON_OBJS) $(TEST_OBJS) $(TRAIN_OBJS) 44 | deps := $(ALL_OBJS:%.o=.%.o.d) 45 | 46 | # Control the build verbosity 47 | ifeq ("$(VERBOSE)","1") 48 | Q := 49 | VECHO = @true 50 | else 51 | Q := @ 52 | VECHO = @printf 53 | endif 54 | 55 | all: $(PROG) 56 | 57 | # Generic compilation rule for source files 58 | %.o: %.c 59 | $(VECHO) " CC\t$@\n" 60 | $(Q)$(CC) -o $@ $(CFLAGS) -c -MMD -MF .$@.d $< 61 | 62 | # Main program 63 | $(PROG): $(COMMON_OBJS) $(MAIN_OBJS) 64 | $(VECHO) " LD\t$@\n" 65 | $(Q)$(CC) -o $@ $^ $(LDFLAGS) 66 | 67 | check: $(TEST_PROG) 68 | $(VECHO) " RUN\t$<\n" 69 | $(Q)./$(TEST_PROG) 70 | 71 | $(TEST_PROG): $(TEST_OBJS) $(COMMON_OBJS) 72 | $(VECHO) " LD\t$@\n" 73 | $(Q)$(CC) -o $@ $^ $(LDFLAGS) 74 | 75 | $(TRAIN): $(COMMON_OBJS) $(TRAIN_OBJS) 76 | $(VECHO) " LD\t$@\n" 77 | $(Q)$(CC) -o $@ $^ $(LDFLAGS) -lm 78 | 79 | # Benchmark target 80 | bench: $(PROG) 81 | $(VECHO) " BENCH\t$<\n" 82 | $(Q)./$(PROG) -b 83 | 84 | clean: 85 | $(RM) $(PROG) $(ALL_OBJS) $(deps) 86 | -$(RM) $(TEST_PROG) 87 | -$(RM) $(TRAIN) 88 | -$(RM) -r .tests 89 | 90 | .PHONY: all check bench clean 91 | 92 | -include $(deps) 93 | -------------------------------------------------------------------------------- /block.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "tetris.h" 5 | #include "utils.h" 6 | 7 | void block_init(block_t *b, shape_t *s) 8 | { 9 | b->rot = 0; 10 | b->offset.x = 0, b->offset.y = 0; 11 | b->shape = s; 12 | } 13 | 14 | block_t *block_new(void) 15 | { 16 | block_t *b = nalloc(sizeof(block_t), NULL); 17 | block_init(b, NULL); 18 | return b; 19 | } 20 | 21 | void block_get(const block_t *b, int i, coord_t *result) 22 | { 23 | /* Add bounds checking to prevent memory corruption */ 24 | if (!b || !b->shape || !result || i < 0 || i >= MAX_BLOCK_LEN) { 25 | if (result) { 26 | result->x = -1; 27 | result->y = -1; 28 | } 29 | return; 30 | } 31 | 32 | /* Validate rotation index */ 33 | if (b->rot < 0 || b->rot >= 4) { 34 | result->x = -1; 35 | result->y = -1; 36 | return; 37 | } 38 | 39 | /* Fast path for default rotation */ 40 | if (b->rot == 0) { 41 | int x = b->shape->rot_flat[0][i][0]; 42 | int y = b->shape->rot_flat[0][i][1]; 43 | 44 | /* Check for invalid coordinates: negative values mark invalid */ 45 | if (x < 0 || y < 0) { 46 | result->x = -1; 47 | result->y = -1; 48 | return; 49 | } 50 | 51 | result->x = x + b->offset.x; 52 | result->y = y + b->offset.y; 53 | return; 54 | } 55 | 56 | /* Slower path for non-default rotations with pointer validation */ 57 | if (!b->shape->rot[b->rot] || !b->shape->rot[b->rot][i]) { 58 | result->x = -1; 59 | result->y = -1; 60 | return; 61 | } 62 | 63 | int *rot = b->shape->rot[b->rot][i]; 64 | result->x = rot[0] + b->offset.x; 65 | result->y = rot[1] + b->offset.y; 66 | } 67 | 68 | int block_extreme(const block_t *b, direction_t d) 69 | { 70 | switch (d) { 71 | case LEFT: 72 | return b->offset.x; 73 | case BOT: 74 | return b->offset.y; 75 | case RIGHT: 76 | return b->shape->rot_wh[b->rot].x + b->offset.x - 1; 77 | case TOP: 78 | return b->shape->rot_wh[b->rot].y + b->offset.y - 1; 79 | default: 80 | return 0; 81 | } 82 | }; 83 | 84 | void block_move(block_t *b, direction_t d, int amount) 85 | { 86 | switch (d) { 87 | case LEFT: 88 | b->offset.x -= amount; 89 | break; 90 | case RIGHT: 91 | b->offset.x += amount; 92 | break; 93 | case BOT: 94 | b->offset.y -= amount; 95 | break; 96 | case TOP: 97 | b->offset.y += amount; 98 | break; 99 | } 100 | } 101 | 102 | void block_rotate(block_t *b, int amount) 103 | { 104 | int rot = b->shape->n_rot; 105 | b->rot = (b->rot + amount) % rot; 106 | if (b->rot < 0) 107 | b->rot += rot; 108 | } 109 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # auto-tetris 2 | An advanced Tetris implementation with AI and human play modes, featuring seamless mode switching and classic gameplay mechanics. 3 | 4 | ## Features 5 | - Dual-mode gameplay: Seamless switching between AI and human control during gameplay 6 | - Classic Tetris mechanics: Line clearing, NES-authentic scoring, progressive difficulty 7 | - Intelligent AI: Multi-factor piece placement evaluation with human-like timing 8 | - Terminal UI: Color-coded pieces, real-time stats, animations, next piece preview 9 | 10 | ## Build and Run 11 | 12 | ### Quick Start 13 | Prerequisites: C compiler, GNU make, ANSI terminal support 14 | 15 | ```shell 16 | $ make # Build 17 | $ ./tetris # Run interactive game 18 | ``` 19 | 20 | **Controls:** 21 | 22 | | Key | Action | Mode | 23 | |-----|--------|------| 24 | | Space | Toggle AI/Human mode | Both | 25 | | P | Pause/unpause game | Both | 26 | | Q | Quit game | Both | 27 | | ↑ | Rotate piece | Human only | 28 | | ← | Move piece left | Human only | 29 | | → | Move piece right | Human only | 30 | | ↓ | Drop piece to bottom | Human only | 31 | 32 | Game Modes: 33 | - Human: Classic controls with progressive speed increases 34 | - AI: Optimal piece placement with strategic thinking delays 35 | 36 | NES Scoring: 1-line: 40×(level+1), 2-line: 100×(level+1), 3-line: 300×(level+1), Tetris: 1200×(level+1) 37 | 38 | ### Benchmark Mode 39 | ```shell 40 | $ ./tetris -b # Single game 41 | $ ./tetris -b 10 # Multi-game analysis 42 | ``` 43 | 44 | **Benchmark Metrics:** 45 | - Lines Cleared: Total lines cleared before game over 46 | - Score: Final score achieved using standard Tetris scoring 47 | - Pieces Placed: Number of tetromino pieces used 48 | - LCPP: Lines Cleared Per Piece (efficiency metric) 49 | - Game Duration: Time taken to complete the game 50 | - Search Speed: AI decision-making speed (pieces/second) 51 | 52 | ### AI Training 53 | ```shell 54 | $ make train && ./train # Basic genetic algorithm training 55 | $ ./train -g 50 -p 12 -e 5 # Custom: 50 generations, 12 population, 5 eval games 56 | ``` 57 | 58 | **Training Options:** 59 | 60 | | Option | Description | Default | 61 | |--------|-------------|---------| 62 | | `-g N` | Maximum generations (-1 for infinite) | 100 | 63 | | `-p N` | Population size (2-50) | 8 | 64 | | `-e N` | Evaluation games per individual (1-20) | 3 | 65 | | `-m RATE` | Mutation rate (0.0-1.0) | 0.3 | 66 | | `-s SEED` | Random seed for reproducibility | time-based | 67 | | `-h` | Show help and usage | - | 68 | 69 | **Training Process:** 70 | - Genetic Evolution: Uses tournament selection, crossover, and mutation 71 | - Fitness Evaluation: Prioritizes lines-cleared-per-piece (LCPP) efficiency 72 | - Progress Tracking: Real-time colored progress bars and fitness statistics 73 | - Weight Export: Automatically saves evolved weights as C header files 74 | 75 | ## TODO 76 | * Add customizable AI difficulty levels 77 | * Create configuration file for key bindings 78 | 79 | ## License 80 | `auto-tetris` is available under a permissive MIT-style license. 81 | Use of this source code is governed by a MIT license that can be found in the [LICENSE](LICENSE) file. 82 | -------------------------------------------------------------------------------- /main.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #include "tetris.h" 8 | 9 | static void print_usage(const char *program_name) 10 | { 11 | printf("Usage: %s [options]\n", program_name); 12 | printf("Options:\n"); 13 | printf( 14 | " -b [N] Run benchmark mode with N games (default: 1, max: " 15 | "1000)\n"); 16 | printf(" -h Show this help message\n"); 17 | printf("\nBenchmark mode measures AI performance with these metrics:\n"); 18 | printf(" - Lines Cleared: Total lines cleared before game over\n"); 19 | printf(" - Score: Final score achieved\n"); 20 | printf(" - Pieces Placed: Number of pieces used\n"); 21 | printf(" - LCPP: Lines Cleared Per Piece (efficiency metric)\n"); 22 | printf(" - Game Duration: Time taken to complete the game\n"); 23 | printf(" - Pieces per Second: Decision-making speed\n"); 24 | printf("\nUsage examples:\n"); 25 | printf(" %s -b # Single test (1 game)\n", program_name); 26 | printf(" %s -b 10 # Comprehensive test (10 games)\n", program_name); 27 | printf("\nEvaluation features:\n"); 28 | printf(" - Performance rating against known AI benchmarks\n"); 29 | printf(" - Consistency analysis (natural vs artificial game endings)\n"); 30 | printf(" - Speed analysis for real-time gameplay suitability\n"); 31 | printf(" - Statistical analysis with standard deviation\n"); 32 | printf(" - Personalized recommendations for improvement\n"); 33 | } 34 | 35 | int main(int argc, char *argv[]) 36 | { 37 | bool bench_mode = false; 38 | int bench_games = 1; /* Default number of benchmark games */ 39 | 40 | /* Parse command line arguments */ 41 | for (int i = 1; i < argc; i++) { 42 | if (!strcmp(argv[i], "-b")) { 43 | bench_mode = true; 44 | 45 | /* Check if next argument is a number */ 46 | if (i + 1 < argc) { 47 | char *endptr; 48 | long games = strtol(argv[i + 1], &endptr, 10); 49 | 50 | /* If it's a valid number, use it */ 51 | if (*endptr == '\0' && games > 0 && games <= 1000) { 52 | bench_games = (int) games; 53 | i++; /* Skip the number argument */ 54 | } else if (*endptr != '\0') { 55 | /* Not a number, treat as next option */ 56 | /* Don't increment i, let next iteration handle it */ 57 | } 58 | } 59 | } else if (!strcmp(argv[i], "-h") || !strcmp(argv[i], "--help")) { 60 | print_usage(argv[0]); 61 | return 0; 62 | } else { 63 | fprintf(stderr, "Unknown option: %s\n", argv[i]); 64 | print_usage(argv[0]); 65 | return 1; 66 | } 67 | } 68 | 69 | /* Initialize grid system */ 70 | grid_init(); 71 | 72 | /* Initialize shapes */ 73 | if (!shape_init()) { 74 | fprintf(stderr, "Failed to initialize shapes\n"); 75 | return 1; 76 | } 77 | 78 | /* Initialize random seed */ 79 | srand(time(NULL) ^ getpid()); 80 | 81 | /* Get default AI weights */ 82 | float *w = move_defaults(); 83 | if (!w) { 84 | fprintf(stderr, "Failed to allocate weights\n"); 85 | return 1; 86 | } 87 | 88 | if (bench_mode) { 89 | /* Run benchmark mode */ 90 | printf("Tetris AI Benchmark Mode\n"); 91 | printf("========================\n"); 92 | printf("Grid Size: %dx%d\n", GRID_WIDTH, GRID_HEIGHT); 93 | 94 | bench_results_t results = bench_run_multi(w, bench_games); 95 | bench_print(&results); 96 | 97 | /* Cleanup benchmark results */ 98 | free(results.games); 99 | } else { 100 | /* Run normal interactive mode */ 101 | game_run(w); 102 | } 103 | 104 | free(w); 105 | return 0; 106 | } 107 | -------------------------------------------------------------------------------- /utils.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | /* High-resolution timing 8 | * get_time_ns() returns a monotonic timestamp in nanoseconds. 9 | */ 10 | #if defined(__APPLE__) && defined(__MACH__) 11 | #include 12 | static inline uint64_t get_time_ns(void) 13 | { 14 | static mach_timebase_info_data_t tb; 15 | if (tb.denom == 0) 16 | (void) mach_timebase_info(&tb); 17 | return mach_absolute_time() * tb.numer / tb.denom; 18 | } 19 | #else 20 | #include 21 | static inline uint64_t get_time_ns(void) 22 | { 23 | struct timespec ts; 24 | #if defined(CLOCK_MONOTONIC_RAW) 25 | clock_gettime(CLOCK_MONOTONIC_RAW, &ts); 26 | #else 27 | clock_gettime(CLOCK_MONOTONIC, &ts); 28 | #endif 29 | return (uint64_t) ts.tv_sec * 1000000000ULL + (uint64_t) ts.tv_nsec; 30 | } 31 | #endif 32 | 33 | /* Platform detection for random number generation */ 34 | #if defined(__GLIBC__) && \ 35 | (__GLIBC__ > 2 || (__GLIBC__ == 2 && __GLIBC_MINOR__ >= 36)) 36 | #include /* arc4random_uniform in glibc >=2.36 */ 37 | #endif 38 | 39 | /* Compiler Hints and Optimization Macros */ 40 | 41 | /* Branch prediction hints for better optimization */ 42 | #if defined(__GNUC__) || defined(__clang__) 43 | #define LIKELY(x) __builtin_expect(!!(x), 1) 44 | #define UNLIKELY(x) __builtin_expect(!!(x), 0) 45 | #else 46 | #define LIKELY(x) (x) 47 | #define UNLIKELY(x) (x) 48 | #endif 49 | 50 | /* Common utility macros */ 51 | #define MAX(a, b) ((a) > (b) ? (a) : (b)) 52 | #define MIN(a, b) ((a) < (b) ? (a) : (b)) 53 | #define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0])) 54 | 55 | /* Bit Manipulation Operations */ 56 | 57 | /* Count Trailing Zeros (CTZ) 58 | * Returns the number of trailing zero bits in x. 59 | * Undefined behavior if x == 0. 60 | */ 61 | #if defined(__GNUC__) || defined(__clang__) 62 | #define CTZ(x) __builtin_ctz(x) 63 | #else 64 | static inline int __ctz_fallback(unsigned x) 65 | { 66 | if (UNLIKELY(x == 0)) 67 | return 32; 68 | 69 | int count = 0; 70 | while ((x & 1) == 0) { 71 | x >>= 1; 72 | count++; 73 | } 74 | return count; 75 | } 76 | #define CTZ(x) __ctz_fallback(x) 77 | #endif 78 | 79 | /* Population Count (POPCOUNT) 80 | * Returns the number of set bits in x. 81 | */ 82 | #if defined(__GNUC__) || defined(__clang__) 83 | #define POPCOUNT(x) __builtin_popcount(x) 84 | #elif defined(_MSC_VER) 85 | #include 86 | #define POPCOUNT(x) __popcnt(x) 87 | #else 88 | static inline int __popcount_fallback(unsigned x) 89 | { 90 | int count = 0; 91 | while (x) { 92 | x &= x - 1; /* Clear the lowest set bit */ 93 | count++; 94 | } 95 | return count; 96 | } 97 | #define POPCOUNT(x) __popcount_fallback(x) 98 | #endif 99 | 100 | /* High-Quality Random Number Generation */ 101 | 102 | /* Generate bias-free uniform random integer in range [0, upper) 103 | * 104 | * Uses platform-specific high-quality generators when available: 105 | * - arc4random_uniform() on BSD/macOS and modern glibc 106 | * - Rejection sampling fallback to eliminate modulo bias 107 | * @upper : Upper bound (exclusive), must be > 0 108 | * 109 | * Return Random integer in [0, upper) 110 | */ 111 | static inline uint32_t rand_range(uint32_t upper) 112 | { 113 | if (UNLIKELY(upper == 0)) 114 | return 0; 115 | 116 | #if defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__) || \ 117 | defined(__NetBSD__) || \ 118 | (defined(__GLIBC__) && \ 119 | (__GLIBC__ > 2 || (__GLIBC__ == 2 && __GLIBC_MINOR__ >= 36))) 120 | /* arc4random_uniform() is cryptographically secure and bias-free */ 121 | return arc4random_uniform(upper); 122 | #else 123 | /* Rejection sampling to eliminate modulo bias */ 124 | uint32_t limit = RAND_MAX - (RAND_MAX % upper); 125 | uint32_t r; 126 | 127 | do { 128 | r = (uint32_t) rand(); 129 | } while (UNLIKELY(r >= limit)); 130 | 131 | return r % upper; 132 | #endif 133 | } 134 | 135 | /** 136 | * Structure-aware memory allocator with automatic cleanup. 137 | * 138 | * Nalloc provides a hierarchical memory management system where each allocation 139 | * can have a parent dependency. When a parent is freed, all its children are 140 | * automatically freed as well. 141 | * 142 | * Key features: 143 | * - Automatic cleanup of dependent allocations 144 | * - Tree-structured memory dependencies 145 | * - Compatible replacement for malloc/calloc/realloc/free 146 | * - Zero overhead when used correctly 147 | * 148 | * Warning: Do not mix standard malloc/free with nalloc functions 149 | * on the same memory chunk. 150 | * 151 | * Example usage: 152 | * // Create a matrix with automatic cleanup 153 | * matrix_t *m = nalloc(sizeof(matrix_t), NULL); 154 | * m->rows = ncalloc(height, sizeof(int*), m); 155 | * for (int i = 0; i < height; i++) { 156 | * m->rows[i] = nalloc(width * sizeof(int), m->rows); 157 | * } 158 | * nfree(m); // Frees everything automatically 159 | */ 160 | 161 | /** 162 | * Allocate memory with optional parent dependency. 163 | * 164 | * @size : Number of bytes to allocate 165 | * @parent : Parent allocation, or NULL for root allocation 166 | * Return pointer to allocated memory, or NULL on failure 167 | */ 168 | void *nalloc(size_t size, void *parent); 169 | 170 | /** 171 | * Allocate zero-initialized memory with optional parent dependency. 172 | * 173 | * @count : Number of elements to allocate 174 | * @size : Size of each element in bytes 175 | * @parent : Parent allocation, or NULL for root allocation 176 | * Return pointer to allocated memory, or NULL on failure 177 | */ 178 | void *ncalloc(size_t count, size_t size, void *parent); 179 | 180 | /** 181 | * Resize an existing nalloc allocation. 182 | * 183 | * @ptr : Previously allocated memory, or NULL 184 | * @size : New size in bytes 185 | * Return pointer to resized memory, or NULL on failure 186 | * 187 | * @note : Parent relationships are preserved across realloc 188 | */ 189 | void *nrealloc(void *ptr, size_t size); 190 | 191 | /** 192 | * Free memory and all dependent allocations. 193 | * 194 | * @ptr : Memory to free, or NULL (safe to call) 195 | * Return always NULL for convenience 196 | */ 197 | void *nfree(void *ptr); 198 | 199 | /** 200 | * Change the parent of an existing allocation. 201 | * 202 | * This moves the entire subtree rooted at @ptr to become 203 | * a child of @parent. 204 | * 205 | * @ptr : Memory whose parent to change 206 | * @parent : New parent, or NULL to make it a root 207 | */ 208 | void nalloc_set_parent(void *ptr, void *parent); 209 | 210 | /** 211 | * Get allocation statistics (debug/profiling helper). 212 | * 213 | * @ptr : Allocation to query 214 | * @stats : Structure to fill with statistics 215 | * Return 0 on success, -1 if ptr is invalid 216 | */ 217 | typedef struct { 218 | size_t total_size; /* Total bytes including children */ 219 | size_t direct_size; /* Direct allocation size */ 220 | int child_count; /* Number of direct children */ 221 | int depth; /* Depth in dependency tree */ 222 | } nalloc_stats_t; 223 | 224 | int nalloc_get_stats(const void *ptr, nalloc_stats_t *stats); 225 | -------------------------------------------------------------------------------- /tests/test-types.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "../tetris.h" 4 | #include "../utils.h" 5 | #include "test.h" 6 | 7 | void test_coordinate_operations(void) 8 | { 9 | /* Test coord_t structure properties */ 10 | coord_t coord; 11 | 12 | /* Test coordinate assignment and retrieval */ 13 | coord.x = 5; 14 | coord.y = 10; 15 | assert_test(coord.x == 5, "coordinate x should be assignable"); 16 | assert_test(coord.y == 10, "coordinate y should be assignable"); 17 | 18 | /* Test coordinate bounds within uint8_t range */ 19 | coord.x = 0; 20 | coord.y = 0; 21 | assert_test(coord.x == 0 && coord.y == 0, 22 | "coordinates should support zero values"); 23 | 24 | coord.x = 255; 25 | coord.y = 255; 26 | assert_test(coord.x == 255 && coord.y == 255, 27 | "coordinates should support maximum uint8_t values"); 28 | 29 | /* Test coordinate comparison and copying */ 30 | coord_t coord1 = {10, 20}; 31 | coord_t coord2 = {10, 20}; 32 | coord_t coord3 = {15, 25}; 33 | 34 | assert_test(coord1.x == coord2.x && coord1.y == coord2.y, 35 | "identical coordinates should have equal components"); 36 | assert_test(!(coord1.x == coord3.x && coord1.y == coord3.y), 37 | "different coordinates should not be equal"); 38 | 39 | /* Test coordinate arithmetic safety */ 40 | coord_t result; 41 | result.x = coord1.x + 5; 42 | result.y = coord1.y + 5; 43 | assert_test(result.x == 15 && result.y == 25, 44 | "coordinate arithmetic should work correctly"); 45 | 46 | /* Test typical Tetris coordinate usage */ 47 | coord_t grid_coord = {GRID_WIDTH / 2, GRID_HEIGHT / 2}; 48 | assert_test(grid_coord.x < GRID_WIDTH && grid_coord.y < GRID_HEIGHT, 49 | "coordinates should fit within grid bounds"); 50 | } 51 | 52 | void test_direction_constants(void) 53 | { 54 | /* Test direction enumeration values */ 55 | assert_test(BOT == 0, "BOT direction should be 0"); 56 | assert_test(LEFT == 1, "LEFT direction should be 1"); 57 | assert_test(TOP == 2, "TOP direction should be 2"); 58 | assert_test(RIGHT == 3, "RIGHT direction should be 3"); 59 | 60 | /* Test direction uniqueness */ 61 | direction_t directions[] = {BOT, LEFT, TOP, RIGHT}; 62 | bool all_unique = true; 63 | 64 | for (int i = 0; i < 4; i++) { 65 | for (int j = i + 1; j < 4; j++) { 66 | if (directions[i] == directions[j]) { 67 | all_unique = false; 68 | break; 69 | } 70 | } 71 | if (!all_unique) 72 | break; 73 | } 74 | assert_test(all_unique, "all direction constants should be unique"); 75 | 76 | /* Test direction range validity */ 77 | for (int i = 0; i < 4; i++) { 78 | assert_test(directions[i] >= 0 && directions[i] < 4, 79 | "direction %d should be in valid range 0-3", i); 80 | } 81 | 82 | /* Test opposite direction relationships */ 83 | assert_test((BOT + 2) % 4 == TOP, "BOT and TOP should be opposite"); 84 | assert_test((LEFT + 2) % 4 == RIGHT, "LEFT and RIGHT should be opposite"); 85 | 86 | /* Test direction cycling for rotation operations */ 87 | direction_t current = BOT; 88 | direction_t next = (current + 1) % 4; 89 | assert_test(next == LEFT, "BOT + 1 should cycle to LEFT"); 90 | 91 | current = RIGHT; 92 | next = (current + 1) % 4; 93 | assert_test(next == BOT, "RIGHT + 1 should cycle to BOT"); 94 | } 95 | 96 | void test_grid_constants_validation(void) 97 | { 98 | /* Test grid dimension constants */ 99 | assert_test(GRID_WIDTH > 0, "GRID_WIDTH should be positive"); 100 | assert_test(GRID_HEIGHT > 0, "GRID_HEIGHT should be positive"); 101 | 102 | /* Test grid dimensions are reasonable for Tetris */ 103 | assert_test(GRID_WIDTH >= 10 && GRID_WIDTH <= 20, 104 | "GRID_WIDTH should be reasonable for Tetris (10-20)"); 105 | assert_test(GRID_HEIGHT >= 15 && GRID_HEIGHT <= 25, 106 | "GRID_HEIGHT should be reasonable for Tetris (15-25)"); 107 | 108 | /* Test that grid can accommodate tetrominoes */ 109 | assert_test(GRID_WIDTH >= 4, 110 | "GRID_WIDTH should accommodate widest tetromino (4 blocks)"); 111 | assert_test(GRID_HEIGHT >= 4, 112 | "GRID_HEIGHT should accommodate tallest tetromino (4 blocks)"); 113 | 114 | /* Test grid size relationships */ 115 | int total_cells = GRID_WIDTH * GRID_HEIGHT; 116 | assert_test(total_cells > 0, "total grid cells should be positive"); 117 | assert_test(total_cells < 10000, 118 | "total grid cells should be reasonable (< 10000)"); 119 | 120 | /* Test constants can be used in array declarations */ 121 | static char test_grid[GRID_HEIGHT][GRID_WIDTH]; 122 | test_grid[0][0] = 1; 123 | test_grid[GRID_HEIGHT - 1][GRID_WIDTH - 1] = 1; 124 | assert_test( 125 | test_grid[0][0] == 1 && test_grid[GRID_HEIGHT - 1][GRID_WIDTH - 1] == 1, 126 | "grid constants should work in array declarations"); 127 | } 128 | 129 | void test_shape_constants_validation(void) 130 | { 131 | /* Test maximum block length constant */ 132 | assert_test(MAX_BLOCK_LEN > 0, "MAX_BLOCK_LEN should be positive"); 133 | assert_test(MAX_BLOCK_LEN >= 4, 134 | "MAX_BLOCK_LEN should accommodate tetromino blocks (4)"); 135 | assert_test(MAX_BLOCK_LEN <= 10, 136 | "MAX_BLOCK_LEN should be reasonable (not excessive)"); 137 | 138 | /* Test number of tetris shapes constant */ 139 | assert_test(NUM_TETRIS_SHAPES > 0, "NUM_TETRIS_SHAPES should be positive"); 140 | assert_test(NUM_TETRIS_SHAPES == 7, 141 | "NUM_TETRIS_SHAPES should be 7 for standard Tetris"); 142 | 143 | /* Test constants work with shape system */ 144 | bool shapes_ok = shape_init(); 145 | if (shapes_ok) { 146 | /* Test that NUM_TETRIS_SHAPES matches actual available shapes */ 147 | int available_shapes = 0; 148 | for (int i = 0; i < NUM_TETRIS_SHAPES; i++) { 149 | if (shape_get(i)) 150 | available_shapes++; 151 | } 152 | assert_test(available_shapes == NUM_TETRIS_SHAPES, 153 | "NUM_TETRIS_SHAPES should match available shapes"); 154 | 155 | /* Test MAX_BLOCK_LEN is sufficient for all shapes */ 156 | bool all_shapes_fit = true; 157 | for (int i = 0; i < NUM_TETRIS_SHAPES; i++) { 158 | shape_t *shape = shape_get(i); 159 | if (shape) { 160 | /* Check that shape coordinates fit within MAX_BLOCK_LEN */ 161 | for (int block = 0; block < MAX_BLOCK_LEN; block++) { 162 | if (shape->rot_flat[0][block][0] >= 0 && 163 | shape->rot_flat[0][block][1] >= 0) { 164 | /* This block is used, so MAX_BLOCK_LEN should be 165 | * sufficient */ 166 | if (block >= MAX_BLOCK_LEN) { 167 | all_shapes_fit = false; 168 | break; 169 | } 170 | } 171 | } 172 | if (!all_shapes_fit) 173 | break; 174 | } 175 | } 176 | assert_test(all_shapes_fit, 177 | "MAX_BLOCK_LEN should be sufficient for all tetromino " 178 | "shapes"); 179 | 180 | shape_free(); 181 | } 182 | 183 | /* Test constants can be used in array declarations */ 184 | static int test_blocks[MAX_BLOCK_LEN][2]; 185 | test_blocks[0][0] = 1; 186 | test_blocks[MAX_BLOCK_LEN - 1][1] = 1; 187 | assert_test( 188 | test_blocks[0][0] == 1 && test_blocks[MAX_BLOCK_LEN - 1][1] == 1, 189 | "MAX_BLOCK_LEN should work in array declarations"); 190 | 191 | static shape_t *test_shapes[NUM_TETRIS_SHAPES]; 192 | test_shapes[0] = NULL; 193 | test_shapes[NUM_TETRIS_SHAPES - 1] = NULL; 194 | assert_test( 195 | test_shapes[0] == NULL && test_shapes[NUM_TETRIS_SHAPES - 1] == NULL, 196 | "NUM_TETRIS_SHAPES should work in array declarations"); 197 | 198 | /* Test relationship between constants */ 199 | assert_test(MAX_BLOCK_LEN >= 4, 200 | "MAX_BLOCK_LEN should be at least 4 for tetromino blocks"); 201 | assert_test(NUM_TETRIS_SHAPES <= MAX_BLOCK_LEN + 3, 202 | "NUM_TETRIS_SHAPES should be reasonable relative to " 203 | "MAX_BLOCK_LEN"); 204 | } 205 | -------------------------------------------------------------------------------- /nalloc.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Structure-aware memory allocator implementation. 3 | * 4 | * Memory layout per allocation: 5 | * +---------+---------+---------+--------+--------··· 6 | * | first | next | prev | size | user 7 | * | child | sibling | sibling | (opt) | data 8 | * +---------+---------+---------+--------+--------··· 9 | * 10 | * Tree structure relationships: 11 | * - Each node can have multiple children (linked list via next/prev) 12 | * - Children are organized as doubly-linked sibling lists 13 | * - Parent reference is implicit (stored in prev when is_first_child) 14 | */ 15 | 16 | #include 17 | #include 18 | #include 19 | #include 20 | 21 | #include "utils.h" 22 | 23 | /* Compiler hints for better optimization */ 24 | #if defined(__GNUC__) || defined(__clang__) 25 | #define LIKELY(x) __builtin_expect(!!(x), 1) 26 | #define UNLIKELY(x) __builtin_expect(!!(x), 0) 27 | #else 28 | #define LIKELY(x) (x) 29 | #define UNLIKELY(x) (x) 30 | #endif 31 | 32 | /* Memory alignment for better performance */ 33 | #define ALIGNMENT sizeof(void *) 34 | #define ALIGN_SIZE(size) (((size) + ALIGNMENT - 1) & ~(ALIGNMENT - 1)) 35 | 36 | /* Header layout */ 37 | typedef struct nalloc_header { 38 | void *first_child; /* First child in the dependency tree */ 39 | void *next_sibling; /* Next sibling in parent's child list */ 40 | void *prev_sibling; /* Previous sibling or parent if first child */ 41 | } nalloc_header_t; 42 | 43 | #define HEADER_SIZE (sizeof(nalloc_header_t)) 44 | 45 | /* Pointer arithmetic macros */ 46 | #define raw_to_user(raw) ((char *) (raw) + HEADER_SIZE) 47 | #define user_to_raw(user) ((char *) (user) - HEADER_SIZE) 48 | #define get_header(user) ((nalloc_header_t *) user_to_raw(user)) 49 | 50 | /* Tree navigation helpers */ 51 | #define first_child(ptr) (get_header(ptr)->first_child) 52 | #define next_sibling(ptr) (get_header(ptr)->next_sibling) 53 | #define prev_sibling(ptr) (get_header(ptr)->prev_sibling) 54 | 55 | /* Tree state queries */ 56 | #define is_root(ptr) (prev_sibling(ptr) == NULL) 57 | #define is_first_child(ptr) \ 58 | (prev_sibling(ptr) != NULL && next_sibling(prev_sibling(ptr)) != (ptr)) 59 | #define get_parent(ptr) (is_first_child(ptr) ? prev_sibling(ptr) : NULL) 60 | 61 | /* Initialize header for a newly allocated chunk. */ 62 | static inline void *init_allocation(void *raw_mem, void *parent) 63 | { 64 | if (UNLIKELY(!raw_mem)) 65 | return NULL; 66 | 67 | /* Clear header */ 68 | nalloc_header_t *header = (nalloc_header_t *) raw_mem; 69 | memset(header, 0, HEADER_SIZE); 70 | 71 | void *user_ptr = raw_to_user(raw_mem); 72 | 73 | /* Set up parent relationship if specified */ 74 | if (parent) 75 | nalloc_set_parent(user_ptr, parent); 76 | 77 | return user_ptr; 78 | } 79 | 80 | /* Allocate raw memory with proper alignment. */ 81 | static inline void *allocate_raw(size_t user_size) 82 | { 83 | size_t total_size = ALIGN_SIZE(HEADER_SIZE + user_size); 84 | return malloc(total_size); 85 | } 86 | 87 | /* Allocate zero-initialized raw memory with proper alignment. */ 88 | static inline void *callocate_raw(size_t user_size) 89 | { 90 | size_t total_size = ALIGN_SIZE(HEADER_SIZE + user_size); 91 | return calloc(1, total_size); 92 | } 93 | 94 | void *nalloc(size_t size, void *parent) 95 | { 96 | if (UNLIKELY(size == 0)) 97 | return NULL; 98 | 99 | void *raw_mem = allocate_raw(size); 100 | return init_allocation(raw_mem, parent); 101 | } 102 | 103 | void *ncalloc(size_t count, size_t size, void *parent) 104 | { 105 | if (UNLIKELY(count == 0 || size == 0)) 106 | return NULL; 107 | 108 | /* Check for overflow */ 109 | if (UNLIKELY(count > SIZE_MAX / size)) 110 | return NULL; 111 | 112 | size_t total_user_size = count * size; 113 | void *raw_mem = callocate_raw(total_user_size); 114 | return init_allocation(raw_mem, parent); 115 | } 116 | 117 | void *nrealloc(void *ptr, size_t size) 118 | { 119 | if (UNLIKELY(size == 0)) { 120 | nfree(ptr); 121 | return NULL; 122 | } 123 | 124 | if (UNLIKELY(!ptr)) 125 | return nalloc(size, NULL); 126 | 127 | /* Before realloc, the ptr is valid. We must save the header and its 128 | * relationships. 129 | */ 130 | nalloc_header_t old_header = *get_header(ptr); 131 | void *parent = NULL; 132 | void *prev_sib = NULL; 133 | if (!is_root(ptr)) { 134 | if (is_first_child(ptr)) { 135 | parent = get_parent(ptr); 136 | } else { 137 | prev_sib = prev_sibling(ptr); 138 | } 139 | } 140 | 141 | void *old_raw = user_to_raw(ptr); 142 | void *new_raw = realloc(old_raw, ALIGN_SIZE(HEADER_SIZE + size)); 143 | 144 | if (UNLIKELY(!new_raw)) { 145 | /* realloc failed, but ptr is still valid. */ 146 | return NULL; 147 | } 148 | 149 | void *new_ptr = raw_to_user(new_raw); 150 | 151 | if (new_ptr == ptr) { 152 | /* The block was not moved, so all pointers are still valid. */ 153 | return new_ptr; 154 | } 155 | 156 | /* The block was moved. We must update all references to ptr. 157 | * The header of new_ptr is currently undefined, so restore it. 158 | */ 159 | *get_header(new_ptr) = old_header; 160 | 161 | /* Update the parent's 'first_child' or the previous sibling's 162 | * 'next_sibling'. 163 | */ 164 | if (parent) /* ptr was the first child */ 165 | first_child(parent) = new_ptr; 166 | else if (prev_sib) /* ptr was a subsequent child */ 167 | next_sibling(prev_sib) = new_ptr; 168 | 169 | /* Update the next sibling's 'prev_sibling' to point to new_ptr. */ 170 | if (old_header.next_sibling) 171 | prev_sibling(old_header.next_sibling) = new_ptr; 172 | 173 | /* Update the first child's 'prev_sibling' (which points to its parent). */ 174 | if (old_header.first_child) 175 | prev_sibling(old_header.first_child) = new_ptr; 176 | 177 | return new_ptr; 178 | } 179 | 180 | /* Remove node from sibling list without freeing. */ 181 | static inline void unlink_from_siblings(void *ptr) 182 | { 183 | if (is_root(ptr)) 184 | return; 185 | 186 | /* Update next sibling's prev pointer */ 187 | if (next_sibling(ptr)) 188 | prev_sibling(next_sibling(ptr)) = prev_sibling(ptr); 189 | 190 | /* Update previous element's next pointer */ 191 | if (is_first_child(ptr)) { 192 | /* We are first child, update parent's first_child pointer */ 193 | void *parent = get_parent(ptr); 194 | if (LIKELY(parent)) 195 | first_child(parent) = next_sibling(ptr); 196 | } else { 197 | /* We're not first child, update previous sibling's next pointer */ 198 | next_sibling(prev_sibling(ptr)) = next_sibling(ptr); 199 | } 200 | 201 | /* Clear our pointers */ 202 | next_sibling(ptr) = NULL; 203 | prev_sibling(ptr) = NULL; 204 | } 205 | 206 | /* Recursively free all children of a node. */ 207 | static void free_children_recursive(void *ptr) 208 | { 209 | if (UNLIKELY(!ptr)) 210 | return; 211 | 212 | void *child = first_child(ptr); 213 | while (child) { 214 | void *next_child = next_sibling(child); 215 | 216 | /* Recursively free this child's subtree */ 217 | free_children_recursive(child); 218 | free(user_to_raw(child)); 219 | 220 | child = next_child; 221 | } 222 | 223 | /* Clear the first_child pointer */ 224 | first_child(ptr) = NULL; 225 | } 226 | 227 | void *nfree(void *ptr) 228 | { 229 | if (UNLIKELY(!ptr)) 230 | return NULL; 231 | 232 | /* Remove from parent's child list */ 233 | unlink_from_siblings(ptr); 234 | 235 | /* Free all children recursively */ 236 | free_children_recursive(ptr); 237 | 238 | /* Free the node itself */ 239 | free(user_to_raw(ptr)); 240 | 241 | return NULL; 242 | } 243 | 244 | void nalloc_set_parent(void *ptr, void *parent) 245 | { 246 | if (UNLIKELY(!ptr)) 247 | return; 248 | 249 | /* Remove from current parent */ 250 | unlink_from_siblings(ptr); 251 | 252 | /* Add to new parent if specified */ 253 | if (parent) { 254 | /* Insert as first child */ 255 | void *old_first = first_child(parent); 256 | if (old_first) 257 | prev_sibling(old_first) = ptr; 258 | 259 | next_sibling(ptr) = old_first; 260 | prev_sibling(ptr) = parent; 261 | first_child(parent) = ptr; 262 | } 263 | } 264 | 265 | /* Calculate subtree statistics recursively. */ 266 | static void calculate_stats_recursive(const void *ptr, 267 | nalloc_stats_t *stats, 268 | int depth) 269 | { 270 | if (!ptr) 271 | return; 272 | 273 | stats->child_count++; 274 | if (depth > stats->depth) 275 | stats->depth = depth; 276 | 277 | /* Visit all children */ 278 | const void *child = first_child(ptr); 279 | while (child) { 280 | calculate_stats_recursive(child, stats, depth + 1); 281 | child = next_sibling(child); 282 | } 283 | } 284 | 285 | int nalloc_get_stats(const void *ptr, nalloc_stats_t *stats) 286 | { 287 | if (UNLIKELY(!ptr || !stats)) 288 | return -1; 289 | 290 | memset(stats, 0, sizeof(*stats)); 291 | 292 | /* Calculate statistics for this subtree */ 293 | calculate_stats_recursive(ptr, stats, 0); 294 | 295 | /* Note: We can't easily get allocation sizes without storing them, so 296 | * direct_size and total_size remain 0 in this implementation. 297 | */ 298 | 299 | return 0; 300 | } 301 | -------------------------------------------------------------------------------- /tests/test-block.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "../tetris.h" 4 | #include "../utils.h" 5 | #include "test.h" 6 | 7 | void test_block_basic_allocation(void) 8 | { 9 | /* Test basic block allocation */ 10 | block_t *block = block_new(); 11 | assert_test(block, "block_new should return non-NULL pointer"); 12 | 13 | if (!block) 14 | return; /* Skip if allocation failed */ 15 | 16 | /* Test initial state */ 17 | assert_test(block->rot == 0, "new block should have rotation 0"); 18 | assert_test(block->offset.x == 0, "new block should have offset.x = 0"); 19 | assert_test(block->offset.y == 0, "new block should have offset.y = 0"); 20 | assert_test(block->shape == NULL, "new block should have NULL shape"); 21 | 22 | /* Test deallocation */ 23 | nfree(block); 24 | assert_test(true, "block deallocation completed"); 25 | } 26 | 27 | void test_block_initialization_with_shapes(void) 28 | { 29 | /* Initialize shapes first */ 30 | bool shapes_ok = shape_init(); 31 | assert_test(shapes_ok, "shape_init should succeed"); 32 | 33 | if (!shapes_ok) 34 | return; /* Skip if shapes initialization failed */ 35 | 36 | /* Test block initialization with various shapes */ 37 | for (int shape_idx = 0; shape_idx < NUM_TETRIS_SHAPES; shape_idx++) { 38 | shape_t *test_shape = shape_get(shape_idx); 39 | if (!test_shape) { 40 | assert_test(false, "shape_get(%d) should return valid shape", 41 | shape_idx); 42 | continue; 43 | } 44 | 45 | block_t *block = block_new(); 46 | assert_test(block, "block allocation for shape %d should succeed", 47 | shape_idx); 48 | 49 | if (!block) 50 | continue; 51 | 52 | /* Test initialization */ 53 | block_init(block, test_shape); 54 | assert_test(block->shape == test_shape, 55 | "block_init should set shape correctly"); 56 | assert_test(block->rot == 0, "block_init should reset rotation to 0"); 57 | assert_test(block->offset.x == 0, 58 | "block_init should reset offset.x to 0"); 59 | assert_test(block->offset.y == 0, 60 | "block_init should reset offset.y to 0"); 61 | 62 | nfree(block); 63 | } 64 | 65 | /* Test initialization with NULL shape */ 66 | block_t *block = block_new(); 67 | if (block) { 68 | block_init(block, NULL); 69 | assert_test(block->shape == NULL, 70 | "block_init with NULL shape should be safe"); 71 | nfree(block); 72 | } 73 | 74 | /* Cleanup shapes */ 75 | shape_free(); 76 | } 77 | 78 | void test_block_coordinate_retrieval(void) 79 | { 80 | /* Initialize shapes */ 81 | bool shapes_ok = shape_init(); 82 | assert_test(shapes_ok, "shape_init should succeed for coordinate tests"); 83 | 84 | if (!shapes_ok) 85 | return; 86 | 87 | /* Test coordinate retrieval with first shape */ 88 | shape_t *test_shape = shape_get(0); 89 | if (!test_shape) { 90 | shape_free(); 91 | return; 92 | } 93 | 94 | block_t *block = block_new(); 95 | if (!block) { 96 | shape_free(); 97 | return; 98 | } 99 | 100 | block_init(block, test_shape); 101 | 102 | /* Test basic coordinate retrieval */ 103 | coord_t result; 104 | for (int i = 0; i < MAX_BLOCK_LEN; i++) { 105 | block_get(block, i, &result); 106 | assert_test( 107 | result.x < 100 || result.x == 255, 108 | "block_get coordinate x should be valid or 255 for invalid"); 109 | assert_test( 110 | result.y < 100 || result.y == 255, 111 | "block_get coordinate y should be valid or 255 for invalid"); 112 | } 113 | 114 | /* Test with offset */ 115 | block->offset.x = 5; 116 | block->offset.y = 10; 117 | 118 | coord_t offset_result; 119 | block_get(block, 0, &offset_result); 120 | 121 | /* Reset offset and get original coordinate */ 122 | block->offset.x = 0; 123 | block->offset.y = 0; 124 | coord_t original_result; 125 | block_get(block, 0, &original_result); 126 | 127 | if (original_result.x != 255 && original_result.y != 255) { 128 | assert_test(offset_result.x == original_result.x + 5, 129 | "block_get should apply x offset correctly"); 130 | assert_test(offset_result.y == original_result.y + 10, 131 | "block_get should apply y offset correctly"); 132 | } 133 | 134 | /* Test bounds checking */ 135 | block_get(block, -1, &result); 136 | assert_test(result.x == 255 && result.y == 255, 137 | "block_get with negative index should return (255, 255)"); 138 | 139 | block_get(block, MAX_BLOCK_LEN, &result); 140 | assert_test( 141 | result.x == 255 && result.y == 255, 142 | "block_get with index >= MAX_BLOCK_LEN should return (255, 255)"); 143 | 144 | /* Test with NULL result pointer */ 145 | block_get(block, 0, NULL); 146 | assert_test(true, "block_get with NULL result should not crash"); 147 | 148 | /* Test with NULL block */ 149 | block_get(NULL, 0, &result); 150 | assert_test(result.x == 255 && result.y == 255, 151 | "block_get with NULL block should return (255, 255)"); 152 | 153 | nfree(block); 154 | shape_free(); 155 | } 156 | 157 | void test_block_rotation_operations(void) 158 | { 159 | /* Initialize shapes */ 160 | bool shapes_ok = shape_init(); 161 | assert_test(shapes_ok, "shape_init should succeed for rotation tests"); 162 | 163 | if (!shapes_ok) 164 | return; 165 | 166 | shape_t *test_shape = shape_get(0); 167 | if (!test_shape) { 168 | shape_free(); 169 | return; 170 | } 171 | 172 | block_t *block = block_new(); 173 | if (!block) { 174 | shape_free(); 175 | return; 176 | } 177 | 178 | block_init(block, test_shape); 179 | 180 | /* Test basic rotation */ 181 | int initial_rot = block->rot; 182 | block_rotate(block, 1); 183 | assert_test(block->rot == (initial_rot + 1) % test_shape->n_rot, 184 | "block_rotate(1) should increment rotation correctly"); 185 | 186 | /* Test multiple rotations */ 187 | block_rotate(block, 2); 188 | assert_test(block->rot == (initial_rot + 3) % test_shape->n_rot, 189 | "block_rotate(2) should work correctly"); 190 | 191 | /* Test negative rotation */ 192 | block_rotate(block, -1); 193 | assert_test(block->rot == (initial_rot + 2) % test_shape->n_rot, 194 | "block_rotate(-1) should decrement rotation correctly"); 195 | 196 | /* Test wraparound */ 197 | block->rot = 0; 198 | block_rotate(block, -1); 199 | assert_test(block->rot == test_shape->n_rot - 1, 200 | "negative rotation should wrap around correctly"); 201 | 202 | /* Test full rotation cycle */ 203 | block->rot = 0; 204 | int n_rot = test_shape->n_rot; 205 | block_rotate(block, n_rot); 206 | assert_test(block->rot == 0, 207 | "rotating by n_rot should return to original rotation"); 208 | 209 | /* NOTE: block_rotate(NULL, 1) would cause segfault - implementation doesn't 210 | * handle NULL 211 | */ 212 | 213 | nfree(block); 214 | shape_free(); 215 | } 216 | 217 | void test_block_movement_operations(void) 218 | { 219 | /* Initialize shapes */ 220 | bool shapes_ok = shape_init(); 221 | assert_test(shapes_ok, "shape_init should succeed for movement tests"); 222 | 223 | if (!shapes_ok) 224 | return; 225 | 226 | shape_t *test_shape = shape_get(0); 227 | if (!test_shape) { 228 | shape_free(); 229 | return; 230 | } 231 | 232 | block_t *block = block_new(); 233 | if (!block) { 234 | shape_free(); 235 | return; 236 | } 237 | 238 | block_init(block, test_shape); 239 | 240 | /* Test LEFT movement */ 241 | block->offset.x = 10; 242 | block->offset.y = 10; 243 | block_move(block, LEFT, 3); 244 | assert_test(block->offset.x == 7, 245 | "LEFT movement should decrease x coordinate"); 246 | assert_test(block->offset.y == 10, 247 | "LEFT movement should not affect y coordinate"); 248 | 249 | /* Test RIGHT movement */ 250 | block_move(block, RIGHT, 5); 251 | assert_test(block->offset.x == 12, 252 | "RIGHT movement should increase x coordinate"); 253 | assert_test(block->offset.y == 10, 254 | "RIGHT movement should not affect y coordinate"); 255 | 256 | /* Test BOT movement */ 257 | block_move(block, BOT, 2); 258 | assert_test(block->offset.x == 12, 259 | "BOT movement should not affect x coordinate"); 260 | assert_test(block->offset.y == 8, 261 | "BOT movement should decrease y coordinate"); 262 | 263 | /* Test TOP movement */ 264 | block_move(block, TOP, 4); 265 | assert_test(block->offset.x == 12, 266 | "TOP movement should not affect x coordinate"); 267 | assert_test(block->offset.y == 12, 268 | "TOP movement should increase y coordinate"); 269 | 270 | /* Test zero movement */ 271 | int saved_x = block->offset.x; 272 | int saved_y = block->offset.y; 273 | block_move(block, LEFT, 0); 274 | assert_test(block->offset.x == saved_x && block->offset.y == saved_y, 275 | "zero movement should not change position"); 276 | 277 | /* Test negative movement */ 278 | block_move(block, LEFT, -3); 279 | assert_test(block->offset.x == saved_x + 3, 280 | "negative LEFT movement should move RIGHT"); 281 | 282 | /* NOTE: block_move(NULL, LEFT, 1) would cause segfault - implementation 283 | * doesn't handle NULL 284 | */ 285 | 286 | nfree(block); 287 | shape_free(); 288 | } 289 | 290 | void test_block_extreme_calculations(void) 291 | { 292 | /* Initialize shapes */ 293 | bool shapes_ok = shape_init(); 294 | assert_test(shapes_ok, "shape_init should succeed for extreme tests"); 295 | 296 | if (!shapes_ok) 297 | return; 298 | 299 | shape_t *test_shape = shape_get(0); 300 | if (!test_shape) { 301 | shape_free(); 302 | return; 303 | } 304 | 305 | block_t *block = block_new(); 306 | if (!block) { 307 | shape_free(); 308 | return; 309 | } 310 | 311 | block_init(block, test_shape); 312 | 313 | /* Test basic extreme calculations */ 314 | block->offset.x = 5; 315 | block->offset.y = 10; 316 | 317 | int left_extreme = block_extreme(block, LEFT); 318 | int right_extreme = block_extreme(block, RIGHT); 319 | int bot_extreme = block_extreme(block, BOT); 320 | int top_extreme = block_extreme(block, TOP); 321 | 322 | assert_test(left_extreme == block->offset.x, 323 | "LEFT extreme should equal x offset"); 324 | assert_test(bot_extreme == block->offset.y, 325 | "BOT extreme should equal y offset"); 326 | 327 | /* RIGHT and TOP extremes depend on shape dimensions */ 328 | if (test_shape->rot_wh[block->rot].x > 0) { 329 | assert_test(right_extreme == 330 | block->offset.x + test_shape->rot_wh[block->rot].x - 1, 331 | "RIGHT extreme should be offset + width - 1"); 332 | } 333 | 334 | if (test_shape->rot_wh[block->rot].y > 0) { 335 | assert_test(top_extreme == 336 | block->offset.y + test_shape->rot_wh[block->rot].y - 1, 337 | "TOP extreme should be offset + height - 1"); 338 | } 339 | 340 | /* Test with different rotation */ 341 | block_rotate(block, 1); 342 | int new_right_extreme = block_extreme(block, RIGHT); 343 | int new_top_extreme = block_extreme(block, TOP); 344 | 345 | /* Extremes should update with rotation if shape dimensions change */ 346 | if (test_shape->rot_wh[0].x != test_shape->rot_wh[1].x) { 347 | assert_test( 348 | new_right_extreme != right_extreme, 349 | "RIGHT extreme should change with rotation if width changes"); 350 | } 351 | 352 | if (test_shape->rot_wh[0].y != test_shape->rot_wh[1].y) { 353 | assert_test( 354 | new_top_extreme != top_extreme, 355 | "TOP extreme should change with rotation if height changes"); 356 | } 357 | 358 | /* NOTE: block_extreme(NULL, LEFT) would cause segfault - implementation 359 | * doesn't handle NULL 360 | */ 361 | 362 | nfree(block); 363 | shape_free(); 364 | } 365 | 366 | void test_block_edge_cases(void) 367 | { 368 | /* Test operations on uninitialized block */ 369 | block_t *block = block_new(); 370 | if (block) { 371 | coord_t result; 372 | 373 | /* These should handle NULL shape gracefully */ 374 | block_get(block, 0, &result); 375 | assert_test( 376 | result.x == 255 && result.y == 255, 377 | "block_get on uninitialized block should return (255, 255)"); 378 | 379 | /* NOTE: block_rotate and block_move on uninitialized block would 380 | * segfault */ 381 | /* block_rotate(block, 1); - would crash due to NULL shape->n_rot access 382 | */ 383 | /* block_move(block, LEFT, 1); - would crash due to NULL shape access */ 384 | 385 | /* block_extreme should work for LEFT/BOT (only accesses offset), but 386 | * not RIGHT/TOP (accesses shape) 387 | */ 388 | int extreme = block_extreme(block, LEFT); 389 | assert_test( 390 | extreme == 0, 391 | "block_extreme(LEFT) on uninitialized block should return 0"); 392 | 393 | nfree(block); 394 | } 395 | 396 | /* NOTE: Testing with corrupted/invalid pointers would cause segfault 397 | * block_get() only checks for NULL, not invalid pointers like 0xDEADBEEF 398 | */ 399 | 400 | assert_test(true, "edge case tests completed"); 401 | } 402 | -------------------------------------------------------------------------------- /tests/driver.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "test.h" 9 | 10 | /* Include the main tetris headers for testing */ 11 | #include "../tetris.h" 12 | #include "../utils.h" 13 | 14 | /* Memory allocation tests */ 15 | void test_nalloc_basic_allocation(void); 16 | void test_nalloc_simple_realloc(void); 17 | void test_nalloc_edge_cases(void); 18 | void test_nalloc_realloc_tree_integrity(void); 19 | void test_nalloc_realloc_stress(void); 20 | void test_nalloc_realloc_edge_cases(void); 21 | void test_nalloc_parent_child_relationships(void); 22 | 23 | /* Basic types and constants tests */ 24 | void test_coordinate_operations(void); 25 | void test_direction_constants(void); 26 | void test_grid_constants_validation(void); 27 | void test_shape_constants_validation(void); 28 | 29 | /* Block operation tests */ 30 | void test_block_basic_allocation(void); 31 | void test_block_initialization_with_shapes(void); 32 | void test_block_coordinate_retrieval(void); 33 | void test_block_rotation_operations(void); 34 | void test_block_movement_operations(void); 35 | void test_block_extreme_calculations(void); 36 | void test_block_edge_cases(void); 37 | 38 | /* Grid operation tests */ 39 | void test_grid_system_initialization(void); 40 | void test_grid_basic_allocation(void); 41 | void test_grid_allocation_edge_cases(void); 42 | void test_grid_copy_operations(void); 43 | void test_grid_block_intersection_detection(void); 44 | void test_grid_block_add_remove_operations(void); 45 | void test_grid_block_spawn(void); 46 | void test_grid_block_drop_operation(void); 47 | void test_grid_block_movement_validation(void); 48 | void test_grid_block_rotation_validation(void); 49 | void test_grid_line_clearing(void); 50 | void test_grid_tetris_ready_detection(void); 51 | void test_grid_apply_block_and_rollback(void); 52 | void test_grid_edge_cases_and_robustness(void); 53 | 54 | /* Move/AI system tests */ 55 | void test_move_defaults_allocation(void); 56 | void test_move_defaults_consistency(void); 57 | void test_move_find_best_basic_functionality(void); 58 | void test_move_find_best_edge_cases(void); 59 | void test_move_find_best_multiple_shapes(void); 60 | void test_move_find_best_weight_sensitivity(void); 61 | void test_ai_decision_quality(void); 62 | void test_move_structure_properties(void); 63 | void test_ai_performance_characteristics(void); 64 | 65 | /* Shape system tests */ 66 | void test_shape_system_initialization(void); 67 | void test_shape_index_bounds_checking(void); 68 | void test_shape_properties_validation(void); 69 | void test_shape_rotation_consistency(void); 70 | void test_shape_crust_data_validation(void); 71 | void test_shape_stream_basic_operations(void); 72 | void test_shape_stream_bounds_and_edge_cases(void); 73 | void test_shape_stream_7bag_randomization(void); 74 | void test_shape_stream_multiple_bags_distribution(void); 75 | void test_shape_stream_reset_functionality(void); 76 | void test_shape_stream_gameplay_sequence(void); 77 | void test_shape_stream_memory_management(void); 78 | void test_shape_multiple_init_cleanup_cycles(void); 79 | void test_shape_edge_cases(void); 80 | 81 | /* Game mechanics tests */ 82 | void test_game_stats_structure_validation(void); 83 | void test_game_benchmark_results_structure(void); 84 | void test_game_input_enumeration_validation(void); 85 | void test_game_basic_piece_placement_sequence(void); 86 | void test_game_block_coordinate_retrieval(void); 87 | void test_game_grid_copy_operations(void); 88 | void test_game_line_clearing_mechanics(void); 89 | void test_game_over_detection_logic(void); 90 | void test_game_ai_vs_human_decision_making(void); 91 | void test_game_ai_weight_system_validation(void); 92 | void test_game_scoring_and_statistics_logic(void); 93 | void test_game_piece_stream_continuity(void); 94 | void test_game_multi_piece_sequence_validation(void); 95 | void test_game_comprehensive_tetromino_placement_validation(void); 96 | void test_game_grid_boundary_collision_detection(void); 97 | void test_game_complex_grid_state_validation(void); 98 | void test_game_tetromino_rotation_state_consistency(void); 99 | void test_game_line_clearing_pattern_validation(void); 100 | void test_game_memory_cleanup_validation(void); 101 | void test_game_grid_different_dimensions(void); 102 | void test_game_edge_cases_and_robustness(void); 103 | void test_game_complete_lifecycle_state_transitions(void); 104 | void test_game_grid_internal_state_consistency(void); 105 | void test_game_block_add_remove_symmetry(void); 106 | void test_game_collision_detection_accuracy(void); 107 | void test_game_movement_validation_comprehensive(void); 108 | void test_game_rotation_validation_comprehensive(void); 109 | void test_game_shape_stream_state_transitions(void); 110 | 111 | /* Global test statistics */ 112 | static size_t tested = 0, passed = 0; 113 | static const char *current_test = "initialization"; 114 | 115 | /* Test category management */ 116 | void start_test_category(const char *name) 117 | { 118 | printf(TEST_CATEGORY_HEADER, name); 119 | } 120 | 121 | void end_test_category(const char *name) 122 | { 123 | printf(TEST_CATEGORY_FOOTER, name); 124 | } 125 | 126 | bool assert_test(bool condition, const char *format, ...) 127 | { 128 | va_list args; 129 | va_start(args, format); 130 | 131 | tested++; 132 | if (!condition) { 133 | fprintf(stderr, TEST_FAIL_PREFIX); 134 | vfprintf(stderr, format, args); 135 | } else { 136 | passed++; 137 | printf(TEST_PASS_PREFIX); 138 | vprintf(format, args); 139 | } 140 | va_end(args); 141 | printf("\n"); 142 | return condition; 143 | } 144 | 145 | static void test_reset_global_state(void) 146 | { 147 | /* Reset any global state between test categories 148 | * Defensive reset - avoid calling functions that might cause issues. 149 | * shape_bag_reset() is safe - just sets bag_pos = 7 150 | */ 151 | shape_bag_reset(); 152 | } 153 | 154 | static void print_test_report(void) 155 | { 156 | size_t failed = tested - passed; 157 | if (failed) { 158 | printf(TEST_SUMMARY_FORMAT, tested, passed, failed); 159 | } else { 160 | printf(TEST_ALL_PASSED, tested); 161 | } 162 | } 163 | 164 | /* Signal handling for crash detection */ 165 | #define SIGNAL_HEADER COLOR_RED "=== SIGNAL CAUGHT ===" COLOR_RESET 166 | #define SIGNAL_TERMINATE \ 167 | COLOR_RED "Test suite terminated due to signal." COLOR_RESET 168 | 169 | static const char *signal_name(int sig) 170 | { 171 | switch (sig) { 172 | case SIGSEGV: 173 | return "SIGSEGV (Segmentation Fault)"; 174 | case SIGABRT: 175 | return "SIGABRT (Abort)"; 176 | case SIGFPE: 177 | return "SIGFPE (Floating Point Exception)"; 178 | case SIGBUS: 179 | return "SIGBUS (Bus Error)"; 180 | case SIGILL: 181 | return "SIGILL (Illegal Instruction)"; 182 | case SIGTRAP: 183 | return "SIGTRAP (Trace Trap)"; 184 | default: 185 | return "Unknown Signal"; 186 | } 187 | } 188 | 189 | static void signal_handler(int sig) 190 | { 191 | printf("\n\n" SIGNAL_HEADER "\n"); 192 | printf("Signal: %s (%d)\n", signal_name(sig), sig); 193 | printf("Current test: %s\n", current_test); 194 | printf("Tests completed so far:\n"); 195 | print_test_report(); 196 | 197 | printf("\n" SIGNAL_TERMINATE "\n"); 198 | 199 | /* Reset signal handler to default and re-raise to get core dump */ 200 | signal(sig, SIG_DFL); 201 | exit(1); 202 | } 203 | 204 | static void setup_signal_handlers(void) 205 | { 206 | signal(SIGSEGV, signal_handler); 207 | signal(SIGABRT, signal_handler); 208 | signal(SIGFPE, signal_handler); 209 | signal(SIGBUS, signal_handler); 210 | signal(SIGILL, signal_handler); 211 | signal(SIGTRAP, signal_handler); 212 | } 213 | 214 | #define RUN(test_func) \ 215 | do { \ 216 | current_test = #test_func; \ 217 | test_func(); \ 218 | } while (0) 219 | 220 | #define RUN_CATEGORY(category_name, test_functions) \ 221 | do { \ 222 | start_test_category(category_name); \ 223 | test_functions end_test_category(category_name); \ 224 | test_reset_global_state(); \ 225 | } while (0) 226 | 227 | int main(void) 228 | { 229 | printf(COLOR_BOLD "Running auto-tetris test suite...\n" COLOR_RESET); 230 | printf("Testing against tetris.h public interface\n\n"); 231 | 232 | /* Set up signal handling for crash detection */ 233 | setup_signal_handlers(); 234 | 235 | /* Initialize core game systems before any tests */ 236 | current_test = "grid system initialization"; 237 | grid_init(); 238 | printf(TEST_PASS_PREFIX "grid system initialized successfully\n"); 239 | 240 | /* Memory allocation tests */ 241 | RUN_CATEGORY("Memory Allocation Tests", { 242 | RUN(test_nalloc_basic_allocation); 243 | RUN(test_nalloc_simple_realloc); 244 | RUN(test_nalloc_edge_cases); 245 | RUN(test_nalloc_realloc_tree_integrity); 246 | RUN(test_nalloc_realloc_stress); 247 | RUN(test_nalloc_realloc_edge_cases); 248 | RUN(test_nalloc_parent_child_relationships); 249 | }); 250 | 251 | /* Basic types and constants tests */ 252 | RUN_CATEGORY("Basic Types and Constants Tests", { 253 | RUN(test_coordinate_operations); 254 | RUN(test_direction_constants); 255 | RUN(test_grid_constants_validation); 256 | RUN(test_shape_constants_validation); 257 | }); 258 | 259 | /* Block operation tests */ 260 | RUN_CATEGORY("Block Operation Tests", { 261 | RUN(test_block_basic_allocation); 262 | RUN(test_block_initialization_with_shapes); 263 | RUN(test_block_coordinate_retrieval); 264 | RUN(test_block_rotation_operations); 265 | RUN(test_block_movement_operations); 266 | RUN(test_block_extreme_calculations); 267 | RUN(test_block_edge_cases); 268 | }); 269 | 270 | /* Grid operation tests */ 271 | RUN_CATEGORY("Grid Operation Tests", { 272 | RUN(test_grid_system_initialization); 273 | RUN(test_grid_basic_allocation); 274 | RUN(test_grid_allocation_edge_cases); 275 | RUN(test_grid_copy_operations); 276 | RUN(test_grid_block_intersection_detection); 277 | RUN(test_grid_block_add_remove_operations); 278 | RUN(test_grid_block_spawn); 279 | RUN(test_grid_block_drop_operation); 280 | RUN(test_grid_block_movement_validation); 281 | RUN(test_grid_block_rotation_validation); 282 | RUN(test_grid_line_clearing); 283 | RUN(test_grid_tetris_ready_detection); 284 | RUN(test_grid_apply_block_and_rollback); 285 | RUN(test_grid_edge_cases_and_robustness); 286 | }); 287 | 288 | /* Move/AI system tests */ 289 | RUN_CATEGORY("Move/AI System Tests", { 290 | RUN(test_move_defaults_allocation); 291 | RUN(test_move_defaults_consistency); 292 | RUN(test_move_find_best_basic_functionality); 293 | RUN(test_move_find_best_edge_cases); 294 | RUN(test_move_find_best_multiple_shapes); 295 | RUN(test_move_find_best_weight_sensitivity); 296 | RUN(test_ai_decision_quality); 297 | RUN(test_move_structure_properties); 298 | RUN(test_ai_performance_characteristics); 299 | }); 300 | 301 | /* Shape system tests */ 302 | RUN_CATEGORY("Shape System Tests", { 303 | RUN(test_shape_system_initialization); 304 | RUN(test_shape_index_bounds_checking); 305 | RUN(test_shape_properties_validation); 306 | RUN(test_shape_rotation_consistency); 307 | RUN(test_shape_crust_data_validation); 308 | RUN(test_shape_stream_basic_operations); 309 | RUN(test_shape_stream_bounds_and_edge_cases); 310 | RUN(test_shape_stream_7bag_randomization); 311 | RUN(test_shape_stream_multiple_bags_distribution); 312 | RUN(test_shape_stream_reset_functionality); 313 | RUN(test_shape_stream_gameplay_sequence); 314 | RUN(test_shape_stream_memory_management); 315 | RUN(test_shape_multiple_init_cleanup_cycles); 316 | RUN(test_shape_edge_cases); 317 | }); 318 | 319 | /* Game mechanics tests */ 320 | RUN_CATEGORY("Game Mechanics Tests", { 321 | RUN(test_game_stats_structure_validation); 322 | RUN(test_game_benchmark_results_structure); 323 | RUN(test_game_input_enumeration_validation); 324 | RUN(test_game_basic_piece_placement_sequence); 325 | RUN(test_game_block_coordinate_retrieval); 326 | RUN(test_game_grid_copy_operations); 327 | RUN(test_game_line_clearing_mechanics); 328 | RUN(test_game_over_detection_logic); 329 | RUN(test_game_ai_vs_human_decision_making); 330 | RUN(test_game_ai_weight_system_validation); 331 | RUN(test_game_scoring_and_statistics_logic); 332 | RUN(test_game_piece_stream_continuity); 333 | RUN(test_game_multi_piece_sequence_validation); 334 | RUN(test_game_comprehensive_tetromino_placement_validation); 335 | RUN(test_game_grid_boundary_collision_detection); 336 | RUN(test_game_complex_grid_state_validation); 337 | RUN(test_game_tetromino_rotation_state_consistency); 338 | RUN(test_game_line_clearing_pattern_validation); 339 | RUN(test_game_memory_cleanup_validation); 340 | RUN(test_game_grid_different_dimensions); 341 | RUN(test_game_edge_cases_and_robustness); 342 | RUN(test_game_complete_lifecycle_state_transitions); 343 | RUN(test_game_grid_internal_state_consistency); 344 | RUN(test_game_block_add_remove_symmetry); 345 | RUN(test_game_collision_detection_accuracy); 346 | RUN(test_game_movement_validation_comprehensive); 347 | RUN(test_game_rotation_validation_comprehensive); 348 | RUN(test_game_shape_stream_state_transitions); 349 | }); 350 | 351 | /* Final cleanup and report */ 352 | current_test = "test suite completion"; 353 | 354 | printf(COLOR_BOLD "Test suite execution completed.\n" COLOR_RESET); 355 | print_test_report(); 356 | 357 | /* Don't call shape_free() here to avoid potential exit handler conflicts 358 | */ 359 | 360 | /* Ensure clean exit */ 361 | fflush(stdout); 362 | fflush(stderr); 363 | 364 | /* Use _exit() to bypass atexit handlers that may cause segfaults */ 365 | _exit((tested == passed) ? 0 : 1); 366 | } 367 | -------------------------------------------------------------------------------- /shape.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #include "tetris.h" 8 | #include "utils.h" 9 | 10 | /* 7-bag piece generator for fair distribution */ 11 | static int bag[7]; /* Holds a shuffled permutation 0-6 */ 12 | static int bag_pos = 7; /* 7 = bag empty, needs refill */ 13 | 14 | static const int base_shapes[][4][2] = { 15 | /* Square (O-piece) 16 | * ██ 17 | * ██ 18 | */ 19 | {{0, 0}, {0, 1}, {1, 0}, {1, 1}}, 20 | 21 | /* T-piece 22 | * ███ 23 | * █ 24 | */ 25 | {{0, 1}, {1, 1}, {1, 0}, {2, 1}}, 26 | 27 | /* I-piece 28 | * ████ 29 | */ 30 | {{0, 1}, {1, 1}, {2, 1}, {3, 1}}, 31 | 32 | /* J-piece 33 | * █ 34 | * ███ 35 | */ 36 | {{0, 1}, {1, 1}, {2, 1}, {2, 2}}, 37 | 38 | /* L-piece 39 | * ███ 40 | * █ 41 | */ 42 | {{0, 1}, {1, 1}, {2, 1}, {2, 0}}, 43 | 44 | /* S-piece 45 | * █ 46 | * ██ 47 | * █ 48 | */ 49 | {{1, 1}, {2, 1}, {2, 0}, {1, 2}}, 50 | 51 | /* Z-piece 52 | * ██ 53 | * ██ 54 | */ 55 | {{1, 1}, {2, 1}, {0, 2}, {1, 2}}, 56 | }; 57 | 58 | #define N_SHAPES (sizeof(base_shapes) / sizeof(base_shapes[0])) 59 | 60 | /* Fisher-Yates shuffle of 0..6 */ 61 | static void shuffle_bag(void) 62 | { 63 | for (int i = 0; i < 7; i++) 64 | bag[i] = i; 65 | 66 | for (int i = 6; i > 0; i--) { 67 | int j = rand_range(i + 1); /* 0 <= j <= i */ 68 | if (i == j) 69 | continue; 70 | 71 | int tmp = bag[i]; 72 | bag[i] = bag[j]; 73 | bag[j] = tmp; 74 | } 75 | bag_pos = 0; 76 | } 77 | 78 | /* Return next piece ID, refilling & shuffling when the bag is empty */ 79 | static inline int bag_next(void) 80 | { 81 | if (bag_pos >= 7) 82 | shuffle_bag(); 83 | return bag[bag_pos++]; 84 | } 85 | 86 | /* Reset bag state for testing purposes */ 87 | void shape_bag_reset(void) 88 | { 89 | bag_pos = 7; /* Force refill on next bag_next() call */ 90 | } 91 | 92 | static inline void sort_coords(int **coords, int count, int max_dim_len) 93 | { 94 | /* Bit-twiddling coordinate sort: map each (x,y) to a bit position, 95 | * then use compiler builtins to traverse in sorted order. */ 96 | int *pos_ptr[16] = {0}; 97 | unsigned mask = 0; 98 | 99 | for (int i = 0; i < count; i++) { 100 | int pos = coords[i][1] * max_dim_len + coords[i][0]; 101 | pos_ptr[pos] = coords[i]; 102 | mask |= 1u << pos; 103 | } 104 | 105 | int idx = 0; 106 | for (int y = max_dim_len - 1; y >= 0 && mask; y--) { 107 | unsigned row_mask = 108 | (mask >> (y * max_dim_len)) & ((1u << max_dim_len) - 1); 109 | 110 | while (row_mask) { 111 | int x = CTZ(row_mask); /* lowest x first */ 112 | int pos = y * max_dim_len + x; 113 | coords[idx++] = pos_ptr[pos]; 114 | 115 | row_mask &= row_mask - 1; /* clear the lowest set bit */ 116 | mask &= ~(1u << pos); /* mirror clear in the global mask */ 117 | } 118 | } 119 | } 120 | 121 | static int max_dim(int **coords, int count, int dim) 122 | { 123 | if (count <= 0) 124 | return 0; 125 | 126 | int mx = coords[0][dim]; 127 | for (int i = 1; i < count; i++) { 128 | int curr = coords[i][dim]; 129 | if (curr > mx) 130 | mx = curr; 131 | } 132 | return mx; 133 | } 134 | 135 | static int min_dim(int **coords, int count, int dim) 136 | { 137 | if (count <= 0) 138 | return 0; 139 | 140 | int mn = coords[0][dim]; 141 | for (int i = 1; i < count; i++) { 142 | int curr = coords[i][dim]; 143 | if (curr < mn) 144 | mn = curr; 145 | } 146 | return mn; 147 | } 148 | 149 | static inline int max_ab(int a, int b) 150 | { 151 | return a > b ? a : b; 152 | } 153 | 154 | /* Compute geometry signature for shape */ 155 | static unsigned compute_shape_sig(const shape_t *s) 156 | { 157 | if (!s) 158 | return 0; 159 | 160 | unsigned sig = 0; 161 | 162 | /* Use normalized first rotation for consistent signature */ 163 | for (int i = 0; i < MAX_BLOCK_LEN; i++) { 164 | int x = s->rot_flat[0][i][0]; 165 | int y = s->rot_flat[0][i][1]; 166 | 167 | /* Skip invalid coordinates */ 168 | if (x < 0 || y < 0 || x >= 4 || y >= 4) 169 | continue; 170 | 171 | sig |= 1u << (y * 4 + x); /* Set bit for each occupied cell */ 172 | } 173 | return sig; 174 | } 175 | 176 | static shape_t *shape_new(int **shape_rot) 177 | { 178 | if (!shape_rot) 179 | return NULL; 180 | 181 | /* shape_rot is one rotation of the shape */ 182 | shape_t *s = nalloc(sizeof(shape_t), shape_rot); 183 | if (!s) 184 | return NULL; 185 | 186 | /* Normalize to (0, 0) */ 187 | int left = min_dim(shape_rot, 4, 0); 188 | int bot = min_dim(shape_rot, 4, 1); 189 | 190 | /* Define all rotations */ 191 | s->rot[0] = ncalloc(4, sizeof(*s->rot[0]), s); 192 | if (!s->rot[0]) { 193 | nfree(s); 194 | return NULL; 195 | } 196 | 197 | /* First rotation: normalize to (0, 0) */ 198 | for (int i = 0; i < 4; i++) { 199 | s->rot[0][i] = ncalloc(2, sizeof(*s->rot[0][i]), s->rot[0]); 200 | if (!s->rot[0][i]) { 201 | nfree(s); 202 | return NULL; 203 | } 204 | s->rot[0][i][0] = shape_rot[i][0] - left; 205 | s->rot[0][i][1] = shape_rot[i][1] - bot; 206 | } 207 | s->max_dim_len = 208 | max_ab(max_dim(s->rot[0], 4, 0), max_dim(s->rot[0], 4, 1)) + 1; 209 | 210 | /* Define 1-4 rotations */ 211 | for (int roti = 1; roti < 4; roti++) { 212 | s->rot[roti] = ncalloc(4, sizeof(*s->rot[roti]), s); 213 | if (!s->rot[roti]) { 214 | nfree(s); 215 | return NULL; 216 | } 217 | 218 | for (int i = 0; i < 4; i++) { 219 | s->rot[roti][i] = 220 | ncalloc(2, sizeof(*s->rot[roti][i]), s->rot[roti]); 221 | if (!s->rot[roti][i]) { 222 | nfree(s); 223 | return NULL; 224 | } 225 | s->rot[roti][i][0] = s->rot[roti - 1][i][1]; 226 | s->rot[roti][i][1] = s->max_dim_len - 1 - s->rot[roti - 1][i][0]; 227 | } 228 | 229 | /* Need to normalize to detect uniqueness later */ 230 | left = min_dim(s->rot[roti], 4, 0); 231 | bot = min_dim(s->rot[roti], 4, 1); 232 | for (int i = 0; i < 4; i++) { 233 | s->rot[roti][i][0] -= left; 234 | s->rot[roti][i][1] -= bot; 235 | } 236 | } 237 | 238 | /* Initialize s->rot_wh */ 239 | for (int roti = 0; roti < 4; roti++) { 240 | s->rot_wh[roti].x = max_dim(s->rot[roti], 4, 0) + 1; 241 | s->rot_wh[roti].y = max_dim(s->rot[roti], 4, 1) + 1; 242 | } 243 | 244 | /* Determine number of unique rotations using canonical bit representation 245 | */ 246 | unsigned rot_sig[4]; 247 | s->n_rot = 0; 248 | for (int roti = 0; roti < 4; roti++) { 249 | /* Generate canonical signature for this rotation */ 250 | rot_sig[roti] = 0; 251 | for (int i = 0; i < 4; i++) { 252 | int x = s->rot[roti][i][0], y = s->rot[roti][i][1]; 253 | 254 | /* Ensure coordinates are within bounds */ 255 | if (x >= 0 && x < s->max_dim_len && y >= 0 && y < s->max_dim_len) 256 | rot_sig[roti] |= 1u << (y * s->max_dim_len + x); 257 | } 258 | 259 | /* Check if this rotation signature matches any previous one */ 260 | bool is_duplicate = false; 261 | for (int i = 0; i < roti; i++) { 262 | if (rot_sig[i] == rot_sig[roti]) { 263 | is_duplicate = true; 264 | break; 265 | } 266 | } 267 | 268 | if (!is_duplicate) { 269 | s->n_rot++; 270 | } 271 | } 272 | 273 | /* Define crusts */ 274 | for (int roti = 0; roti < 4; roti++) { 275 | for (direction_t d = 0; d < 4; d++) { 276 | int extremes[s->max_dim_len][2]; // value, index 277 | int dim = (d == BOT || d == TOP) ? 1 : 0; 278 | bool keep_max = (d == TOP || d == RIGHT); 279 | 280 | for (int i = 0; i < s->max_dim_len; i++) 281 | extremes[i][0] = -1; 282 | 283 | int crust_len = 0; 284 | for (int i = 0; i < 4; i++) { 285 | int key = s->rot[roti][i][(dim + 1) % 2]; 286 | int val = s->rot[roti][i][dim]; 287 | 288 | if (key >= 0 && key < s->max_dim_len) { 289 | int curr = extremes[key][0]; 290 | bool replace = curr == -1 || (keep_max && val > curr) || 291 | (!keep_max && val < curr); 292 | if (curr == -1) 293 | crust_len++; 294 | 295 | if (replace) { 296 | extremes[key][0] = val; 297 | extremes[key][1] = i; 298 | } 299 | } 300 | } 301 | s->crust_len[roti][d] = crust_len; 302 | s->crust[roti][d] = ncalloc(crust_len, sizeof(*s->crust[roti]), s); 303 | if (!s->crust[roti][d] && crust_len > 0) { 304 | nfree(s); 305 | return NULL; 306 | } 307 | 308 | int ii = 0; 309 | for (int i = 0; i < s->max_dim_len && ii < crust_len; i++) { 310 | if (extremes[i][0] != -1) { 311 | int index = extremes[i][1]; 312 | s->crust[roti][d][ii] = ncalloc( 313 | 2, sizeof(*s->crust[roti][d][ii]), s->crust[roti][d]); 314 | if (!s->crust[roti][d][ii]) { 315 | nfree(s); 316 | return NULL; 317 | } 318 | s->crust[roti][d][ii][0] = s->rot[roti][index][0]; 319 | s->crust[roti][d][ii][1] = s->rot[roti][index][1]; 320 | ii++; 321 | } 322 | } 323 | 324 | if (crust_len > 0) 325 | sort_coords(s->crust[roti][d], crust_len, s->max_dim_len); 326 | } 327 | } 328 | 329 | /* Initialize the flat, more efficient versions */ 330 | for (int r = 0; r < s->n_rot && r < 4; r++) { 331 | for (int dim = 0; dim < 2; dim++) { 332 | for (int i = 0; i < MAX_BLOCK_LEN; i++) 333 | s->rot_flat[r][i][dim] = s->rot[r][i][dim]; 334 | for (direction_t d = 0; d < 4; d++) { 335 | int len = s->crust_len[r][d]; 336 | for (int i = 0; i < len && i < MAX_BLOCK_LEN; i++) 337 | s->crust_flat[r][d][i][dim] = s->crust[r][d][i][dim]; 338 | } 339 | } 340 | } 341 | 342 | /* Compute and store geometry signature for color lookup optimization */ 343 | s->sig = compute_shape_sig(s); 344 | 345 | return s; 346 | } 347 | 348 | static int n_shapes; 349 | static shape_t **shapes; 350 | 351 | bool shape_init(void) 352 | { 353 | shapes = nalloc(N_SHAPES * sizeof(shape_t *), NULL); 354 | if (!shapes) 355 | return false; 356 | 357 | /* Initialize shapes array */ 358 | for (int i = 0; i < N_SHAPES; i++) 359 | shapes[i] = NULL; 360 | 361 | n_shapes = 0; 362 | 363 | /* Create all shapes - fail fast on any allocation failure */ 364 | for (int idx = 0; idx < N_SHAPES; idx++) { 365 | /* Create rotation data structure */ 366 | int **rot = ncalloc(4, sizeof(*rot), shapes); 367 | if (!rot) 368 | goto cleanup_fail; 369 | 370 | /* Allocate coordinate arrays */ 371 | for (int i = 0; i < 4; i++) { 372 | rot[i] = ncalloc(2, sizeof(*rot[i]), rot); 373 | if (!rot[i]) { 374 | nfree(rot); 375 | goto cleanup_fail; 376 | } 377 | rot[i][0] = base_shapes[idx][i][0]; 378 | rot[i][1] = base_shapes[idx][i][1]; 379 | } 380 | 381 | /* Create the shape */ 382 | shape_t *new_shape = shape_new(rot); 383 | if (!new_shape) { 384 | nfree(rot); 385 | goto cleanup_fail; 386 | } 387 | 388 | shapes[n_shapes++] = new_shape; 389 | } 390 | 391 | return true; 392 | 393 | cleanup_fail: 394 | /* Clean up any allocated shapes */ 395 | for (int i = 0; i < n_shapes; i++) 396 | nfree(shapes[i]); 397 | nfree(shapes); 398 | shapes = NULL; 399 | n_shapes = 0; 400 | return false; 401 | } 402 | 403 | /* Return a shape by index (for falling pieces effect) */ 404 | shape_t *shape_get(int index) 405 | { 406 | if (index < 0 || index >= NUM_TETRIS_SHAPES || !shapes || 407 | n_shapes != NUM_TETRIS_SHAPES) 408 | return NULL; 409 | return shapes[index]; 410 | } 411 | 412 | /* FIXME: Can we eliminate? */ 413 | #define SS_MAX_LEN 3 414 | 415 | shape_stream_t *shape_stream_new() 416 | { 417 | shape_stream_t *s = nalloc(sizeof(*s), NULL); 418 | if (!s) 419 | return NULL; 420 | 421 | s->max_len = SS_MAX_LEN; 422 | s->iter = 0; 423 | s->defined = ncalloc(s->max_len, sizeof(*s->defined), s); 424 | s->stream = ncalloc(s->max_len, sizeof(*s->stream), s); 425 | 426 | if (!s->defined || !s->stream) { 427 | nfree(s); 428 | return NULL; 429 | } 430 | 431 | memset(s->defined, false, s->max_len * sizeof(*s->defined)); 432 | return s; 433 | } 434 | 435 | static shape_t *stream_access(shape_stream_t *stream, int idx) 436 | { 437 | if (!stream || n_shapes <= 0 || !shapes) 438 | return NULL; 439 | 440 | bool pop = false; 441 | if (idx == -1) { 442 | idx = 0; 443 | pop = true; 444 | } 445 | 446 | if (idx < 0 || idx >= stream->max_len) 447 | return NULL; 448 | 449 | int i = (stream->iter + idx) % stream->max_len; 450 | if (!stream->defined[i]) { 451 | /* Use the 7-bag but adapt to available shapes */ 452 | int shape_idx = bag_next(); /* values 0-6 */ 453 | 454 | /* Map to available shapes if we have fewer than 7 */ 455 | if (n_shapes < NUM_TETRIS_SHAPES) 456 | shape_idx = shape_idx % n_shapes; 457 | 458 | /* Validate that shape index is within bounds */ 459 | if (shape_idx >= n_shapes || shape_idx < 0 || !shapes[shape_idx]) { 460 | /* Fallback to first available shape */ 461 | shape_idx = 0; 462 | if (n_shapes <= 0 || !shapes[0]) /* No valid shapes available */ 463 | return NULL; 464 | } 465 | 466 | stream->stream[i] = shapes[shape_idx]; 467 | stream->defined[i] = true; 468 | } 469 | 470 | if (pop) { 471 | stream->defined[i] = false; 472 | stream->iter++; 473 | } 474 | 475 | return stream->stream[i]; 476 | } 477 | 478 | shape_t *shape_stream_peek(const shape_stream_t *stream, int idx) 479 | { 480 | return stream_access((shape_stream_t *) stream, idx); 481 | } 482 | 483 | shape_t *shape_stream_pop(shape_stream_t *stream) 484 | { 485 | return stream_access(stream, -1); 486 | } 487 | 488 | void shape_free(void) 489 | { 490 | if (shapes) { 491 | nfree(shapes); 492 | shapes = NULL; 493 | } 494 | n_shapes = 0; 495 | } 496 | -------------------------------------------------------------------------------- /tests/test-nalloc.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "../utils.h" 5 | #include "test.h" 6 | 7 | void test_nalloc_basic_allocation(void) 8 | { 9 | /* Test standard allocation and deallocation */ 10 | void *ptr = nalloc(100, NULL); 11 | assert_test(ptr, "nalloc should allocate memory successfully"); 12 | 13 | if (!ptr) 14 | return; 15 | 16 | /* Test memory accessibility */ 17 | unsigned char *bytes = (unsigned char *) ptr; 18 | 19 | /* Write pattern to memory */ 20 | for (int i = 0; i < 100; i++) 21 | bytes[i] = (unsigned char) (i % 256); 22 | 23 | /* Verify pattern */ 24 | bool pattern_correct = true; 25 | for (int i = 0; i < 100; i++) { 26 | if (bytes[i] != (unsigned char) (i % 256)) { 27 | pattern_correct = false; 28 | break; 29 | } 30 | } 31 | assert_test(pattern_correct, 32 | "allocated memory should be readable and writable"); 33 | 34 | /* Test deallocation */ 35 | void *result = nfree(ptr); 36 | assert_test(result == NULL, "nfree should return NULL"); 37 | 38 | /* Test different allocation sizes */ 39 | void *small = nalloc(1, NULL); 40 | void *medium = nalloc(1024, NULL); 41 | void *large = nalloc(65536, NULL); 42 | 43 | assert_test(small && medium && large, 44 | "various allocation sizes should succeed"); 45 | 46 | if (small && medium && large) { 47 | assert_test(small != medium && medium != large && small != large, 48 | "different allocations should return unique pointers"); 49 | } 50 | 51 | nfree(large); 52 | nfree(medium); 53 | nfree(small); 54 | } 55 | 56 | void test_nalloc_simple_realloc(void) 57 | { 58 | /* Test realloc functionality with careful pointer management */ 59 | void *ptr = nalloc(50, NULL); 60 | assert_test(ptr, "initial allocation should succeed"); 61 | 62 | if (!ptr) 63 | return; 64 | 65 | /* Write test pattern */ 66 | unsigned char *bytes = (unsigned char *) ptr; 67 | for (int i = 0; i < 50; i++) 68 | bytes[i] = (unsigned char) (i + 1); 69 | 70 | /* Test expanding allocation */ 71 | void *expanded = nrealloc(ptr, 100); 72 | assert_test(expanded, "realloc expansion should succeed"); 73 | 74 | if (!expanded) { 75 | /* If realloc failed, original pointer should still be valid */ 76 | nfree(ptr); 77 | return; 78 | } 79 | 80 | /* After successful realloc, ptr is now invalid, use expanded */ 81 | /* Verify original data preserved */ 82 | unsigned char *new_bytes = (unsigned char *) expanded; 83 | bool data_preserved = true; 84 | for (int i = 0; i < 50; i++) { 85 | if (new_bytes[i] != (unsigned char) (i + 1)) { 86 | data_preserved = false; 87 | break; 88 | } 89 | } 90 | assert_test(data_preserved, "realloc should preserve existing data"); 91 | 92 | /* Test shrinking allocation - be careful with pointer management */ 93 | void *shrunk = nrealloc(expanded, 25); 94 | if (!shrunk) { 95 | /* If shrink failed, expanded is still valid */ 96 | nfree(expanded); 97 | return; 98 | } 99 | 100 | /* After successful realloc, expanded is now invalid, use shrunk */ 101 | /* Verify data still preserved in shrunk area */ 102 | unsigned char *shrunk_bytes = (unsigned char *) shrunk; 103 | bool shrunk_data_ok = true; 104 | for (int i = 0; i < 25; i++) { 105 | if (shrunk_bytes[i] != (unsigned char) (i + 1)) { 106 | shrunk_data_ok = false; 107 | break; 108 | } 109 | } 110 | assert_test(shrunk_data_ok, "realloc shrinking should preserve data"); 111 | 112 | nfree(shrunk); 113 | 114 | /* Test realloc with NULL (should work like nalloc) */ 115 | void *null_realloc = nrealloc(NULL, 200); 116 | assert_test(null_realloc, "nrealloc(NULL, size) should work like nalloc"); 117 | 118 | if (null_realloc) { 119 | /* Write to it to ensure it's valid */ 120 | unsigned char *null_bytes = (unsigned char *) null_realloc; 121 | null_bytes[0] = 0xFF; 122 | null_bytes[199] = 0xAA; 123 | assert_test(null_bytes[0] == 0xFF && null_bytes[199] == 0xAA, 124 | "null realloc memory should be writable"); 125 | nfree(null_realloc); 126 | } 127 | 128 | /* Test realloc to size 0 (should work like nfree) */ 129 | void *to_free = nalloc(100, NULL); 130 | if (to_free) { 131 | void *zero_result = nrealloc(to_free, 0); 132 | assert_test(zero_result == NULL, 133 | "nrealloc(ptr, 0) should work like nfree"); 134 | /* Note: to_free is now invalid after nrealloc(to_free, 0) */ 135 | } 136 | } 137 | 138 | void test_nalloc_edge_cases(void) 139 | { 140 | /* Test NULL safety */ 141 | assert_test(nfree(NULL) == NULL, 142 | "nfree(NULL) should be safe and return NULL"); 143 | 144 | /* Test zero-size allocation */ 145 | void *zero_ptr = nalloc(0, NULL); 146 | assert_test(nfree(zero_ptr) == NULL, 147 | "zero-size allocation should be handled gracefully"); 148 | 149 | /* Test multiple allocations for uniqueness */ 150 | const int alloc_count = 20; 151 | void *ptrs[alloc_count]; 152 | int successful_allocs = 0; 153 | 154 | /* Allocate multiple blocks */ 155 | for (int i = 0; i < alloc_count; i++) { 156 | ptrs[i] = nalloc(64, NULL); 157 | if (ptrs[i]) { 158 | successful_allocs++; 159 | } 160 | } 161 | 162 | assert_test(successful_allocs > 0, 163 | "should successfully allocate some blocks"); 164 | 165 | /* Verify uniqueness of successful allocations */ 166 | bool all_unique = true; 167 | for (int i = 0; i < alloc_count && all_unique; i++) { 168 | if (!ptrs[i]) 169 | continue; 170 | 171 | for (int j = i + 1; j < alloc_count; j++) { 172 | if (ptrs[j] && ptrs[i] == ptrs[j]) { 173 | all_unique = false; 174 | break; 175 | } 176 | } 177 | } 178 | assert_test(all_unique, "all successful allocations should be unique"); 179 | 180 | /* Cleanup all allocations */ 181 | for (int i = 0; i < alloc_count; i++) 182 | nfree(ptrs[i]); 183 | 184 | /* Test ncalloc functionality */ 185 | void *zero_mem = ncalloc(10, sizeof(int), NULL); 186 | if (zero_mem) { 187 | int *ints = (int *) zero_mem; 188 | bool all_zero = true; 189 | for (int i = 0; i < 10; i++) { 190 | if (ints[i] != 0) { 191 | all_zero = false; 192 | break; 193 | } 194 | } 195 | assert_test(all_zero, "ncalloc should zero-initialize memory"); 196 | nfree(zero_mem); 197 | } 198 | 199 | /* Test ncalloc overflow protection */ 200 | void *overflow_test = ncalloc(SIZE_MAX, SIZE_MAX, NULL); 201 | assert_test(overflow_test == NULL, "ncalloc should handle overflow safely"); 202 | } 203 | 204 | void test_nalloc_realloc_tree_integrity(void) 205 | { 206 | /* 207 | * Critical test: Verify that nrealloc preserves tree structure 208 | * when memory blocks are moved. This test specifically targets 209 | * the memory corruption bugs that existed in the original implementation. 210 | */ 211 | 212 | /* Create a complex tree structure */ 213 | void *root = nalloc(64, NULL); 214 | assert_test(root, "root allocation should succeed"); 215 | if (!root) 216 | return; 217 | 218 | void *child1 = nalloc(32, root); 219 | void *child2 = nalloc(32, root); 220 | void *grandchild1 = nalloc(16, child1); 221 | void *grandchild2 = nalloc(16, child1); 222 | void *grandchild3 = nalloc(16, child2); 223 | 224 | assert_test(child1 && child2 && grandchild1 && grandchild2 && grandchild3, 225 | "complex tree allocation should succeed"); 226 | 227 | if (!(child1 && child2 && grandchild1 && grandchild2 && grandchild3)) { 228 | nfree(root); 229 | return; 230 | } 231 | 232 | /* Write test patterns to verify memory integrity */ 233 | memset(root, 0xAA, 64); 234 | memset(child1, 0xBB, 32); 235 | memset(child2, 0xCC, 32); 236 | memset(grandchild1, 0xDD, 16); 237 | memset(grandchild2, 0xEE, 16); 238 | memset(grandchild3, 0xFF, 16); 239 | 240 | /* Force realloc to move memory by expanding significantly */ 241 | void *new_child1 = nrealloc(child1, 2048); /* Large expansion */ 242 | assert_test(new_child1, "nrealloc should succeed"); 243 | 244 | if (!new_child1) { 245 | nfree(root); 246 | return; 247 | } 248 | 249 | /* Verify data integrity after realloc */ 250 | unsigned char *child1_bytes = (unsigned char *) new_child1; 251 | bool data_preserved = true; 252 | for (int i = 0; i < 32; i++) { 253 | if (child1_bytes[i] != 0xBB) { 254 | data_preserved = false; 255 | break; 256 | } 257 | } 258 | assert_test(data_preserved, "nrealloc should preserve existing data"); 259 | 260 | /* Critical test: Verify tree structure is still intact */ 261 | nalloc_stats_t stats; 262 | int stats_result = nalloc_get_stats(root, &stats); 263 | assert_test(stats_result == 0, "statistics should work after realloc"); 264 | assert_test(stats.child_count >= 5, 265 | "tree should still contain all nodes after realloc"); 266 | 267 | /* Test that parent-child relationships are preserved */ 268 | void *great_grandchild = nalloc(8, grandchild1); 269 | assert_test(great_grandchild, 270 | "should be able to allocate grandchild after parent realloc"); 271 | 272 | /* Verify cleanup still works correctly */ 273 | nfree(root); 274 | assert_test(true, "hierarchical cleanup should work after realloc"); 275 | } 276 | 277 | void test_nalloc_realloc_stress(void) 278 | { 279 | /* 280 | * Stress test nrealloc with multiple pointer updates to ensure 281 | * no references are lost or corrupted when memory moves. 282 | */ 283 | 284 | const int num_nodes = 10; 285 | void *nodes[num_nodes]; 286 | 287 | /* Create root */ 288 | nodes[0] = nalloc(100, NULL); 289 | assert_test(nodes[0], "root allocation should succeed"); 290 | if (!nodes[0]) 291 | return; 292 | 293 | /* Create chain of dependent allocations */ 294 | for (int i = 1; i < num_nodes; i++) { 295 | nodes[i] = nalloc(50 + i, nodes[i - 1]); 296 | assert_test(nodes[i], "chain allocation %d should succeed", i); 297 | if (!nodes[i]) { 298 | nfree(nodes[0]); 299 | return; 300 | } 301 | } 302 | 303 | /* Randomly realloc nodes to force memory movement */ 304 | for (int i = 0; i < num_nodes; i++) { 305 | size_t new_size = 200 + (i * 100); /* Force significant expansion */ 306 | void *new_ptr = nrealloc(nodes[i], new_size); 307 | assert_test(new_ptr, "stress realloc %d should succeed", i); 308 | 309 | if (new_ptr) { 310 | nodes[i] = new_ptr; /* Update our reference */ 311 | 312 | /* Write pattern to verify accessibility */ 313 | memset(nodes[i], 0x10 + i, 50 + i); 314 | 315 | /* Verify we can still allocate children */ 316 | void *test_child = nalloc(10, nodes[i]); 317 | assert_test(test_child, "should allocate child after realloc %d", 318 | i); 319 | } 320 | } 321 | 322 | /* Verify tree integrity is maintained */ 323 | nalloc_stats_t final_stats; 324 | int stats_result = nalloc_get_stats(nodes[0], &final_stats); 325 | assert_test(stats_result == 0, "final statistics should work"); 326 | assert_test(final_stats.child_count >= num_nodes, 327 | "all nodes should still be in tree"); 328 | 329 | /* Cleanup should work without crashes */ 330 | nfree(nodes[0]); 331 | assert_test(true, "stress test cleanup completed"); 332 | } 333 | 334 | void test_nalloc_realloc_edge_cases(void) 335 | { 336 | /* Test realloc on nodes with complex sibling relationships */ 337 | void *parent = nalloc(100, NULL); 338 | assert_test(parent, "parent allocation should succeed"); 339 | if (!parent) 340 | return; 341 | 342 | /* Create multiple siblings */ 343 | void *sibling1 = nalloc(50, parent); 344 | void *sibling2 = nalloc(50, parent); 345 | void *sibling3 = nalloc(50, parent); 346 | 347 | assert_test(sibling1 && sibling2 && sibling3, 348 | "sibling allocations should succeed"); 349 | 350 | /* Realloc the middle sibling to test sibling pointer updates */ 351 | void *new_sibling2 = nrealloc(sibling2, 500); 352 | assert_test(new_sibling2, "middle sibling realloc should succeed"); 353 | 354 | /* Verify we can still traverse the tree */ 355 | nalloc_stats_t stats; 356 | int stats_result = nalloc_get_stats(parent, &stats); 357 | assert_test(stats_result == 0 && stats.child_count >= 3, 358 | "sibling relationships should be preserved"); 359 | 360 | /* Test realloc with size 0 (should work like nfree) */ 361 | void *test_node = nalloc(100, parent); 362 | if (test_node) { 363 | void *result = nrealloc(test_node, 0); 364 | assert_test(result == NULL, "nrealloc(ptr, 0) should return NULL"); 365 | } 366 | 367 | /* Cleanup */ 368 | nfree(parent); 369 | assert_test(true, "edge case cleanup completed"); 370 | } 371 | 372 | void test_nalloc_parent_child_relationships(void) 373 | { 374 | /* Test basic parent-child allocation */ 375 | void *parent = nalloc(100, NULL); 376 | assert_test(parent, "parent allocation should succeed"); 377 | 378 | if (!parent) 379 | return; 380 | 381 | /* Allocate children with parent dependency */ 382 | void *child1 = nalloc(50, parent); 383 | void *child2 = nalloc(30, parent); 384 | void *child3 = ncalloc(5, sizeof(int), parent); 385 | 386 | assert_test(child1 && child2 && child3, "child allocations should succeed"); 387 | assert_test(child1 != parent && child2 != parent && child3 != parent, 388 | "children should be distinct from parent"); 389 | assert_test(child1 != child2 && child2 != child3 && child1 != child3, 390 | "children should be distinct from each other"); 391 | 392 | /* Test multi-level hierarchy */ 393 | void *grandchild1 = nalloc(20, child1); 394 | void *grandchild2 = nalloc(15, child1); 395 | 396 | if (grandchild1 && grandchild2) { 397 | assert_test(grandchild1 != grandchild2, 398 | "grandchildren should be distinct"); 399 | 400 | /* Test great-grandchild */ 401 | void *great_grandchild = nalloc(10, grandchild1); 402 | if (great_grandchild) { 403 | assert_test(great_grandchild != grandchild1, 404 | "great-grandchild should be distinct"); 405 | } 406 | 407 | /* Test nalloc statistics functionality */ 408 | nalloc_stats_t stats; 409 | int stats_result = nalloc_get_stats(parent, &stats); 410 | 411 | if (stats_result == 0) { 412 | assert_test(stats.child_count >= 3, 413 | "statistics should count children in subtree"); 414 | assert_test(stats.depth >= 2, 415 | "statistics should report correct depth"); 416 | } 417 | 418 | /* Test statistics edge cases */ 419 | assert_test(nalloc_get_stats(NULL, &stats) != 0, 420 | "statistics with NULL pointer should fail safely"); 421 | assert_test(nalloc_get_stats(parent, NULL) != 0, 422 | "statistics with NULL stats should fail safely"); 423 | } 424 | 425 | /* Test automatic cleanup: freeing parent should free entire subtree */ 426 | nfree(parent); 427 | 428 | assert_test(true, "hierarchical cleanup completed successfully"); 429 | 430 | /* Test orphan adoption with nalloc_set_parent */ 431 | void *new_parent = nalloc(200, NULL); 432 | void *orphan = nalloc(50, NULL); 433 | 434 | if (new_parent && orphan) { 435 | /* Move orphan to new parent */ 436 | nalloc_set_parent(orphan, new_parent); 437 | assert_test(true, "nalloc_set_parent should not crash"); 438 | 439 | /* Test that orphan is now dependent on new_parent */ 440 | nfree(new_parent); /* Should also free orphan */ 441 | assert_test(true, "parent cleanup should handle adopted children"); 442 | } 443 | 444 | /* Test setting NULL parent (making allocation independent) */ 445 | void *independent = nalloc(30, NULL); 446 | void *dependent = nalloc(20, independent); 447 | 448 | if (independent && dependent) { 449 | nalloc_set_parent(dependent, NULL); /* Make dependent independent */ 450 | nfree(independent); /* Should not free dependent now */ 451 | nfree(dependent); /* Must free explicitly */ 452 | assert_test(true, "NULL parent should make allocation independent"); 453 | } 454 | 455 | /* Test complex scenarios */ 456 | void *stress_root = nalloc(1000, NULL); 457 | if (stress_root) { 458 | /* Create many children for stress testing */ 459 | int successful_children = 0; 460 | for (int i = 0; i < 20; i++) { 461 | void *stress_child = nalloc(10 + i, stress_root); 462 | if (stress_child) 463 | successful_children++; 464 | } 465 | 466 | assert_test(successful_children > 15, 467 | "stress test should succeed for most allocations (%d/20)", 468 | successful_children); 469 | 470 | /* Single free should cleanup all */ 471 | nfree(stress_root); 472 | assert_test(true, "stress cleanup completed"); 473 | } 474 | } 475 | -------------------------------------------------------------------------------- /tests/test-move.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "../tetris.h" 6 | #include "../utils.h" 7 | #include "test.h" 8 | 9 | /* Helper functions for packed bit grid access in tests */ 10 | static inline void test_set_cell(grid_t *g, int x, int y) 11 | { 12 | if (!g || x < 0 || y < 0 || x >= g->width || y >= g->height || 13 | y >= GRID_HEIGHT) 14 | return; 15 | g->rows[y] |= (1ULL << x); 16 | } 17 | 18 | static inline void test_clear_cell(grid_t *g, int x, int y) 19 | { 20 | if (!g || x < 0 || y < 0 || x >= g->width || y >= g->height || 21 | y >= GRID_HEIGHT) 22 | return; 23 | g->rows[y] &= ~(1ULL << x); 24 | } 25 | 26 | void test_move_defaults_allocation(void) 27 | { 28 | /* Test AI weight system allocation */ 29 | float *weights = move_defaults(); 30 | assert_test(weights, "default AI weights should be allocated successfully"); 31 | 32 | if (!weights) 33 | return; 34 | 35 | /* Validate weights are reasonable for Tetris AI */ 36 | bool weights_valid = true; 37 | for (int i = 0; i < 6; i++) { /* N_FEATIDX features */ 38 | if (isnan(weights[i]) || isinf(weights[i]) || 39 | fabs(weights[i]) > 100.0f) { 40 | weights_valid = false; 41 | break; 42 | } 43 | } 44 | assert_test(weights_valid, "AI weights should be finite and reasonable"); 45 | 46 | /* Ensure weights are configured (not all zero) */ 47 | bool has_configuration = false; 48 | for (int i = 0; i < 6; i++) { 49 | if (weights[i] != 0.0f) { 50 | has_configuration = true; 51 | break; 52 | } 53 | } 54 | assert_test(has_configuration, 55 | "AI should have non-zero weight configuration"); 56 | 57 | free(weights); 58 | } 59 | 60 | void test_move_defaults_consistency(void) 61 | { 62 | /* Test AI weight system consistency */ 63 | float *weights1 = move_defaults(); 64 | float *weights2 = move_defaults(); 65 | 66 | assert_test(weights1 && weights2, 67 | "multiple weight allocations should succeed"); 68 | 69 | if (weights1 && weights2) { 70 | /* Weights should be deterministic */ 71 | bool weights_consistent = true; 72 | for (int i = 0; i < 6; i++) { 73 | if (weights1[i] != weights2[i]) { 74 | weights_consistent = false; 75 | break; 76 | } 77 | } 78 | assert_test(weights_consistent, "AI weights should be deterministic"); 79 | 80 | /* Each allocation should be independent */ 81 | assert_test(weights1 != weights2, 82 | "weight allocations should be independent"); 83 | 84 | free(weights1); 85 | free(weights2); 86 | } 87 | } 88 | 89 | void test_move_find_best_basic_functionality(void) 90 | { 91 | /* Test core AI decision making functionality */ 92 | bool shapes_ok = shape_init(); 93 | assert_test(shapes_ok, "shape_init should succeed for AI tests"); 94 | if (!shapes_ok) 95 | return; 96 | 97 | grid_t *grid = grid_new(GRID_HEIGHT, GRID_WIDTH); 98 | block_t *block = block_new(); 99 | shape_stream_t *stream = shape_stream_new(); 100 | float *weights = move_defaults(); 101 | 102 | if (!grid || !block || !stream || !weights) { 103 | free(weights); 104 | nfree(stream); 105 | nfree(block); 106 | nfree(grid); 107 | shape_free(); 108 | return; 109 | } 110 | 111 | shape_t *test_shape = shape_get(0); 112 | if (!test_shape) { 113 | free(weights); 114 | nfree(stream); 115 | nfree(block); 116 | nfree(grid); 117 | shape_free(); 118 | return; 119 | } 120 | 121 | /* Test AI decision on empty grid */ 122 | block_init(block, test_shape); 123 | grid_block_spawn(grid, block); 124 | 125 | move_t *ai_move = move_find_best(grid, block, stream, weights); 126 | assert_test(ai_move, "AI should generate move on empty grid"); 127 | 128 | if (ai_move) { 129 | /* Validate move is within Tetris grid bounds */ 130 | assert_test(ai_move->col >= 0 && ai_move->col < GRID_WIDTH, 131 | "AI move column should be within grid bounds (%d)", 132 | ai_move->col); 133 | assert_test(ai_move->rot >= 0, 134 | "AI move rotation should be non-negative (%d)", 135 | ai_move->rot); 136 | 137 | /* Test move executability */ 138 | block_t test_execution = *block; 139 | test_execution.rot = ai_move->rot % test_shape->n_rot; 140 | test_execution.offset.x = ai_move->col; 141 | grid_block_drop(grid, &test_execution); 142 | 143 | assert_test(!grid_block_collides(grid, &test_execution), 144 | "AI move should be executable without collision"); 145 | } 146 | 147 | free(weights); 148 | nfree(stream); 149 | nfree(block); 150 | nfree(grid); 151 | shape_free(); 152 | } 153 | 154 | void test_move_find_best_edge_cases(void) 155 | { 156 | /* Test AI robustness under edge conditions */ 157 | bool shapes_ok = shape_init(); 158 | assert_test(shapes_ok, "shape_init should succeed for edge case tests"); 159 | if (!shapes_ok) 160 | return; 161 | 162 | grid_t *grid = grid_new(GRID_HEIGHT, GRID_WIDTH); 163 | block_t *block = block_new(); 164 | shape_stream_t *stream = shape_stream_new(); 165 | float *weights = move_defaults(); 166 | 167 | if (!grid || !block || !stream || !weights) { 168 | free(weights); 169 | nfree(stream); 170 | nfree(block); 171 | nfree(grid); 172 | shape_free(); 173 | return; 174 | } 175 | 176 | shape_t *test_shape = shape_get(0); 177 | if (test_shape) { 178 | block_init(block, test_shape); 179 | grid_block_spawn(grid, block); 180 | } 181 | 182 | /* Test NULL parameter handling */ 183 | assert_test(move_find_best(NULL, block, stream, weights) == NULL, 184 | "AI should handle NULL grid gracefully"); 185 | assert_test(move_find_best(grid, NULL, stream, weights) == NULL, 186 | "AI should handle NULL block gracefully"); 187 | assert_test(move_find_best(grid, block, NULL, weights) == NULL, 188 | "AI should handle NULL stream gracefully"); 189 | assert_test(move_find_best(grid, block, stream, NULL) == NULL, 190 | "AI should handle NULL weights gracefully"); 191 | 192 | /* Test near-game-over scenario */ 193 | /* Fill grid to near-top, leaving narrow channel */ 194 | for (int row = 0; row < GRID_HEIGHT - 2; row++) { 195 | for (int col = 0; col < GRID_WIDTH; col++) { 196 | if (col < 2 || col >= GRID_WIDTH - 2) { 197 | /* Leave narrow channels */ 198 | continue; 199 | } 200 | test_set_cell(grid, col, row); 201 | } 202 | } 203 | 204 | if (test_shape) { 205 | move_t *endgame_move = move_find_best(grid, block, stream, weights); 206 | /* AI should either find valid move or fail gracefully */ 207 | if (endgame_move) { 208 | assert_test( 209 | endgame_move->col >= 0 && endgame_move->col < GRID_WIDTH, 210 | "endgame AI move should be valid if generated"); 211 | } 212 | assert_test( 213 | true, "AI should handle near-game-over scenarios without crashing"); 214 | } 215 | 216 | free(weights); 217 | nfree(stream); 218 | nfree(block); 219 | nfree(grid); 220 | shape_free(); 221 | } 222 | 223 | void test_move_find_best_multiple_shapes(void) 224 | { 225 | /* Test AI performance across all tetromino types */ 226 | bool shapes_ok = shape_init(); 227 | assert_test(shapes_ok, "shape_init should succeed for multi-shape tests"); 228 | if (!shapes_ok) 229 | return; 230 | 231 | grid_t *grid = grid_new(GRID_HEIGHT, GRID_WIDTH); 232 | block_t *block = block_new(); 233 | shape_stream_t *stream = shape_stream_new(); 234 | float *weights = move_defaults(); 235 | 236 | if (!grid || !block || !stream || !weights) { 237 | free(weights); 238 | nfree(stream); 239 | nfree(block); 240 | nfree(grid); 241 | shape_free(); 242 | return; 243 | } 244 | 245 | /* Test AI with each of the 7 standard tetrominoes */ 246 | int successful_decisions = 0; 247 | for (int shape_idx = 0; shape_idx < NUM_TETRIS_SHAPES; shape_idx++) { 248 | shape_t *tetromino = shape_get(shape_idx); 249 | if (!tetromino) 250 | continue; 251 | 252 | block_init(block, tetromino); 253 | grid_block_spawn(grid, block); 254 | 255 | move_t *decision = move_find_best(grid, block, stream, weights); 256 | if (!decision) 257 | continue; 258 | 259 | /* Validate decision for this tetromino */ 260 | bool decision_valid = 261 | (decision->col >= 0 && decision->col < GRID_WIDTH && 262 | decision->rot >= 0); 263 | 264 | /* Test move execution for this tetromino */ 265 | if (decision_valid) { 266 | block_t execution_test = *block; 267 | execution_test.rot = decision->rot % tetromino->n_rot; 268 | execution_test.offset.x = decision->col; 269 | grid_block_drop(grid, &execution_test); 270 | 271 | if (!grid_block_collides(grid, &execution_test)) 272 | successful_decisions++; 273 | } 274 | } 275 | 276 | assert_test(successful_decisions >= NUM_TETRIS_SHAPES - 1, 277 | "AI should handle most tetromino types successfully (%d/%d)", 278 | successful_decisions, NUM_TETRIS_SHAPES); 279 | 280 | free(weights); 281 | nfree(stream); 282 | nfree(block); 283 | nfree(grid); 284 | shape_free(); 285 | } 286 | 287 | void test_move_find_best_weight_sensitivity(void) 288 | { 289 | /* Test AI weight system sensitivity and configuration */ 290 | bool shapes_ok = shape_init(); 291 | assert_test(shapes_ok, "shape_init should succeed for weight tests"); 292 | if (!shapes_ok) 293 | return; 294 | 295 | grid_t *grid = grid_new(GRID_HEIGHT, GRID_WIDTH); 296 | block_t *block = block_new(); 297 | shape_stream_t *stream = shape_stream_new(); 298 | 299 | if (!grid || !block || !stream) { 300 | nfree(stream); 301 | nfree(block); 302 | nfree(grid); 303 | shape_free(); 304 | return; 305 | } 306 | 307 | shape_t *test_shape = shape_get(0); 308 | if (!test_shape) { 309 | nfree(stream); 310 | nfree(block); 311 | nfree(grid); 312 | shape_free(); 313 | return; 314 | } 315 | 316 | /* Create differentiated grid scenario */ 317 | /* Bottom-heavy with gaps to make weight differences matter */ 318 | for (int row = 0; row < 3; row++) { 319 | for (int col = 0; col < GRID_WIDTH; col++) { 320 | if ((row + col) % 3 != 0) { /* Create pattern with gaps */ 321 | test_set_cell(grid, col, row); 322 | } 323 | } 324 | } 325 | 326 | block_init(block, test_shape); 327 | grid_block_spawn(grid, block); 328 | 329 | /* Test default AI configuration */ 330 | float *default_w = move_defaults(); 331 | move_t *default_decision = NULL; 332 | if (default_w) 333 | default_decision = move_find_best(grid, block, stream, default_w); 334 | 335 | /* Test aggressive line-clearing configuration */ 336 | float aggressive_weights[6] = {-1.0f, -2.0f, -0.5f, -5.0f, 1.0f, -1.5f}; 337 | move_t *aggressive_decision = 338 | move_find_best(grid, block, stream, aggressive_weights); 339 | 340 | /* Test defensive height-minimizing configuration */ 341 | float defensive_weights[6] = {-3.0f, -1.0f, -0.1f, -2.0f, -0.5f, -0.5f}; 342 | move_t *defensive_decision = 343 | move_find_best(grid, block, stream, defensive_weights); 344 | 345 | /* Validate that different weight configurations produce valid moves */ 346 | int valid_configs = 0; 347 | if (default_decision && default_decision->col >= 0 && 348 | default_decision->col < GRID_WIDTH) { 349 | valid_configs++; 350 | } 351 | if (aggressive_decision && aggressive_decision->col >= 0 && 352 | aggressive_decision->col < GRID_WIDTH) { 353 | valid_configs++; 354 | } 355 | if (defensive_decision && defensive_decision->col >= 0 && 356 | defensive_decision->col < GRID_WIDTH) { 357 | valid_configs++; 358 | } 359 | 360 | assert_test( 361 | valid_configs >= 2, 362 | "multiple weight configurations should produce valid moves (%d/3)", 363 | valid_configs); 364 | 365 | free(default_w); 366 | nfree(stream); 367 | nfree(block); 368 | nfree(grid); 369 | shape_free(); 370 | } 371 | 372 | /* Helper function to properly set up grid state for testing */ 373 | static void setup_grid_with_blocks(grid_t *grid, 374 | block_t *temp_block, 375 | int row, 376 | int start_col, 377 | int end_col) 378 | { 379 | if (!grid || !temp_block || !temp_block->shape) 380 | return; 381 | 382 | for (int col = start_col; col <= end_col; col++) { 383 | if (col >= 0 && col < grid->width && row >= 0 && row < grid->height) { 384 | /* Manually place block and update grid state properly */ 385 | test_set_cell(grid, col, row); 386 | 387 | /* Update relief (highest occupied row per column) */ 388 | if (grid->relief[col] < row) 389 | grid->relief[col] = row; 390 | } 391 | } 392 | 393 | /* Check if this row is now complete using our optimized bitmask detection 394 | */ 395 | if (grid->rows[row] == grid->full_mask) { 396 | /* Add to full rows list if not already there */ 397 | bool already_full = false; 398 | for (int i = 0; i < grid->n_full_rows; i++) { 399 | if (grid->full_rows[i] == row) { 400 | already_full = true; 401 | break; 402 | } 403 | } 404 | if (!already_full && grid->n_full_rows < grid->height) 405 | grid->full_rows[grid->n_full_rows++] = row; 406 | } 407 | } 408 | 409 | void test_ai_decision_quality(void) 410 | { 411 | /* Test AI strategic decision making quality */ 412 | bool shapes_ok = shape_init(); 413 | assert_test(shapes_ok, 414 | "shape_init should succeed for decision quality tests"); 415 | if (!shapes_ok) 416 | return; 417 | 418 | grid_t *grid = grid_new(GRID_HEIGHT, GRID_WIDTH); 419 | block_t *block = block_new(); 420 | shape_stream_t *stream = shape_stream_new(); 421 | float *weights = move_defaults(); 422 | 423 | if (!grid || !block || !stream || !weights) { 424 | free(weights); 425 | nfree(stream); 426 | nfree(block); 427 | nfree(grid); 428 | shape_free(); 429 | return; 430 | } 431 | 432 | shape_t *test_shape = shape_get(0); 433 | if (!test_shape) { 434 | free(weights); 435 | nfree(stream); 436 | nfree(block); 437 | nfree(grid); 438 | shape_free(); 439 | return; 440 | } 441 | 442 | /* Test 1: Line clearing opportunity detection */ 443 | /* Create almost-complete line using proper grid setup */ 444 | block_init(block, test_shape); 445 | setup_grid_with_blocks(grid, block, 0, 0, 446 | GRID_WIDTH - 2); /* Leave last column empty */ 447 | 448 | grid_block_spawn(grid, block); 449 | 450 | move_t *line_clear_move = move_find_best(grid, block, stream, weights); 451 | if (line_clear_move) { 452 | assert_test( 453 | line_clear_move->col >= 0 && line_clear_move->col < GRID_WIDTH, 454 | "AI should handle line clearing opportunities"); 455 | 456 | /* Test the suggested move */ 457 | block_t test_block = *block; 458 | test_block.rot = line_clear_move->rot % test_shape->n_rot; 459 | test_block.offset.x = line_clear_move->col; 460 | grid_block_drop(grid, &test_block); 461 | 462 | assert_test(!grid_block_collides(grid, &test_block), 463 | "AI line clearing move should be executable"); 464 | } 465 | 466 | /* Test 2: Hole avoidance */ 467 | /* Clear grid and create hole-prone scenario */ 468 | for (int row = 0; row < GRID_HEIGHT; row++) { 469 | grid->rows[row] = 0; /* Clear entire row */ 470 | } 471 | for (int col = 0; col < GRID_WIDTH; col++) { 472 | grid->relief[col] = -1; 473 | grid->gaps[col] = 0; 474 | grid->stack_cnt[col] = 0; 475 | } 476 | grid->n_full_rows = 0; 477 | 478 | /* Create overhang that could create holes */ 479 | setup_grid_with_blocks(grid, block, 1, 2, GRID_WIDTH - 3); 480 | /* Create bottom row with gap */ 481 | setup_grid_with_blocks(grid, block, 0, 0, GRID_WIDTH / 2 - 1); 482 | setup_grid_with_blocks(grid, block, 0, GRID_WIDTH / 2 + 1, GRID_WIDTH - 1); 483 | 484 | move_t *hole_avoid_move = move_find_best(grid, block, stream, weights); 485 | if (hole_avoid_move) { 486 | assert_test( 487 | hole_avoid_move->col >= 0 && hole_avoid_move->col < GRID_WIDTH, 488 | "AI should make valid moves in hole-prone scenarios"); 489 | } 490 | 491 | /* Test 3: Height minimization */ 492 | /* Clear grid and create height differential */ 493 | for (int row = 0; row < GRID_HEIGHT; row++) { 494 | grid->rows[row] = 0; /* Clear entire row */ 495 | } 496 | for (int col = 0; col < GRID_WIDTH; col++) { 497 | grid->relief[col] = -1; 498 | grid->gaps[col] = 0; 499 | grid->stack_cnt[col] = 0; 500 | } 501 | grid->n_full_rows = 0; 502 | 503 | /* Create tall stack on one side */ 504 | for (int row = 0; row < 8; row++) { 505 | setup_grid_with_blocks(grid, block, row, 0, 1); 506 | } 507 | 508 | move_t *height_move = move_find_best(grid, block, stream, weights); 509 | if (height_move) { 510 | assert_test(height_move->col >= 0 && height_move->col < GRID_WIDTH, 511 | "AI should handle height-differential scenarios"); 512 | 513 | /* AI should generally prefer flatter placements */ 514 | /* This is a soft preference test - AI might have good reasons for other 515 | * choices 516 | */ 517 | assert_test(true, "AI should complete height-differential analysis"); 518 | } 519 | 520 | free(weights); 521 | nfree(stream); 522 | nfree(block); 523 | nfree(grid); 524 | shape_free(); 525 | } 526 | 527 | void test_move_structure_properties(void) 528 | { 529 | /* Test move_t structure used for AI decisions */ 530 | move_t test_move = {0}; 531 | 532 | /* Test structure field assignment */ 533 | test_move.rot = 1; 534 | test_move.col = 7; 535 | test_move.shape = NULL; 536 | 537 | assert_test(test_move.rot == 1 && test_move.col == 7, 538 | "move structure should store rotation and column"); 539 | assert_test(test_move.shape == NULL, 540 | "move structure should handle NULL shape reference"); 541 | 542 | /* Test with actual shape system */ 543 | bool shapes_ok = shape_init(); 544 | if (shapes_ok) { 545 | shape_t *tetromino = shape_get(0); 546 | if (tetromino) { 547 | test_move.shape = tetromino; 548 | test_move.rot = 2; 549 | test_move.col = 5; 550 | 551 | assert_test(test_move.shape == tetromino, 552 | "move should reference tetromino correctly"); 553 | assert_test(test_move.rot == 2 && test_move.col == 5, 554 | "move should store updated rotation and column"); 555 | 556 | /* Test move validation */ 557 | bool move_reasonable = 558 | (test_move.col >= 0 && test_move.col < GRID_WIDTH && 559 | test_move.rot >= 0); 560 | assert_test(move_reasonable, 561 | "move structure should contain reasonable values"); 562 | } 563 | shape_free(); 564 | } 565 | } 566 | 567 | void test_ai_performance_characteristics(void) 568 | { 569 | /* Test AI performance and reliability characteristics */ 570 | bool shapes_ok = shape_init(); 571 | assert_test(shapes_ok, "shape_init should succeed for performance tests"); 572 | if (!shapes_ok) 573 | return; 574 | 575 | grid_t *grid = grid_new(GRID_HEIGHT, GRID_WIDTH); 576 | block_t *block = block_new(); 577 | shape_stream_t *stream = shape_stream_new(); 578 | float *weights = move_defaults(); 579 | 580 | if (!grid || !block || !stream || !weights) { 581 | free(weights); 582 | nfree(stream); 583 | nfree(block); 584 | nfree(grid); 585 | shape_free(); 586 | return; 587 | } 588 | 589 | shape_t *test_shape = shape_get(0); 590 | if (!test_shape) { 591 | free(weights); 592 | nfree(stream); 593 | nfree(block); 594 | nfree(grid); 595 | shape_free(); 596 | return; 597 | } 598 | 599 | block_init(block, test_shape); 600 | grid_block_spawn(grid, block); 601 | 602 | /* Test AI consistency across multiple calls */ 603 | int reliable_calls = 0; 604 | int total_calls = 8; 605 | 606 | for (int i = 0; i < total_calls; i++) { 607 | /* Modify grid state slightly to test different scenarios */ 608 | if (i > 0) { 609 | int test_row = i % 4; 610 | int test_col = (i * 3) % (GRID_WIDTH - 1); 611 | if (i % 2 == 0) { 612 | test_set_cell(grid, test_col, test_row); 613 | } else { 614 | test_clear_cell(grid, test_col, test_row); 615 | } 616 | } 617 | 618 | move_t *performance_move = move_find_best(grid, block, stream, weights); 619 | if (performance_move) { 620 | /* Validate move quality */ 621 | bool move_valid = (performance_move->col >= 0 && 622 | performance_move->col < GRID_WIDTH && 623 | performance_move->rot >= 0); 624 | 625 | if (move_valid) { 626 | /* Test executability */ 627 | block_t exec_test = *block; 628 | exec_test.rot = performance_move->rot % test_shape->n_rot; 629 | exec_test.offset.x = performance_move->col; 630 | grid_block_drop(grid, &exec_test); 631 | 632 | if (!grid_block_collides(grid, &exec_test)) 633 | reliable_calls++; 634 | } 635 | } 636 | } 637 | 638 | /* AI should be reliable across different scenarios */ 639 | assert_test(reliable_calls >= total_calls - 2, 640 | "AI should be reliable across scenarios (%d/%d successful)", 641 | reliable_calls, total_calls); 642 | 643 | /* Test AI behavior in complex grid state */ 644 | /* Create challenging scenario */ 645 | for (int row = 0; row < 10; row++) { 646 | for (int col = 0; col < GRID_WIDTH; col++) { 647 | if ((row * 7 + col * 5) % 13 < 8) /* Semi-random pattern */ 648 | test_set_cell(grid, col, row); 649 | } 650 | } 651 | 652 | move_t *complex_move = move_find_best(grid, block, stream, weights); 653 | assert_test(complex_move != NULL || complex_move == NULL, 654 | "AI should handle complex scenarios without crashing"); 655 | 656 | if (complex_move) { 657 | assert_test(complex_move->col >= 0 && complex_move->col < GRID_WIDTH, 658 | "AI moves in complex scenarios should be valid"); 659 | } 660 | 661 | free(weights); 662 | nfree(stream); 663 | nfree(block); 664 | nfree(grid); 665 | shape_free(); 666 | } 667 | -------------------------------------------------------------------------------- /tests/test-shape.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "../tetris.h" 5 | #include "../utils.h" 6 | #include "test.h" 7 | 8 | void test_shape_system_initialization(void) 9 | { 10 | /* Test shape_init functionality */ 11 | bool init_result = shape_init(); 12 | assert_test(init_result, "shape_init should succeed"); 13 | 14 | if (!init_result) 15 | return; 16 | 17 | /* Test that we can get all 7 standard tetromino shapes */ 18 | for (int i = 0; i < NUM_TETRIS_SHAPES; i++) { 19 | shape_t *shape = shape_get(i); 20 | assert_test(shape, "shape_get(%d) should return valid shape", i); 21 | 22 | if (shape) { 23 | /* Validate basic tetromino properties */ 24 | assert_test(shape->n_rot > 0 && shape->n_rot <= 4, 25 | "tetromino %d should have 1-4 rotations", i); 26 | assert_test(shape->max_dim_len >= 2 && shape->max_dim_len <= 4, 27 | "tetromino %d should fit in 2x2 to 4x4 bounding box", 28 | i); 29 | assert_test(shape->rot[0], 30 | "tetromino %d should have rotation 0 data", i); 31 | } 32 | } 33 | 34 | /* Test cleanup */ 35 | shape_free(); 36 | assert_test(shape_get(0) == NULL, 37 | "shape_get should return NULL after shape_free"); 38 | } 39 | 40 | void test_shape_index_bounds_checking(void) 41 | { 42 | bool init_result = shape_init(); 43 | assert_test(init_result, "shape_init should succeed for bounds tests"); 44 | 45 | if (!init_result) 46 | return; 47 | 48 | /* Test valid indices (standard 7 tetrominoes) */ 49 | for (int i = 0; i < NUM_TETRIS_SHAPES; i++) { 50 | shape_t *shape = shape_get(i); 51 | assert_test(shape, "valid tetromino index %d should return shape", i); 52 | } 53 | 54 | /* Test invalid indices */ 55 | assert_test(shape_get(-1) == NULL, "negative index should return NULL"); 56 | assert_test(shape_get(NUM_TETRIS_SHAPES) == NULL, 57 | "index >= NUM_TETRIS_SHAPES should return NULL"); 58 | assert_test(shape_get(1000) == NULL, "very large index should return NULL"); 59 | 60 | /* Test boundary case */ 61 | shape_t *boundary_shape = shape_get(NUM_TETRIS_SHAPES - 1); 62 | assert_test(boundary_shape, "boundary index should return valid shape"); 63 | 64 | shape_free(); 65 | } 66 | 67 | void test_shape_properties_validation(void) 68 | { 69 | bool init_result = shape_init(); 70 | assert_test(init_result, "shape_init should succeed for property tests"); 71 | 72 | if (!init_result) 73 | return; 74 | 75 | /* Test tetromino-specific properties */ 76 | for (int i = 0; i < NUM_TETRIS_SHAPES; i++) { 77 | shape_t *shape = shape_get(i); 78 | if (!shape) 79 | continue; 80 | 81 | /* Validate rotation properties for Tetris gameplay */ 82 | assert_test(shape->n_rot >= 1 && shape->n_rot <= 4, 83 | "tetromino %d should have 1-4 unique rotations", i); 84 | 85 | /* Validate all rotations have proper dimensions */ 86 | for (int rot = 0; rot < shape->n_rot; rot++) { 87 | assert_test(shape->rot_wh[rot].x > 0 && shape->rot_wh[rot].x <= 4, 88 | "tetromino %d rotation %d width should be 1-4", i, rot); 89 | assert_test(shape->rot_wh[rot].y > 0 && shape->rot_wh[rot].y <= 4, 90 | "tetromino %d rotation %d height should be 1-4", i, 91 | rot); 92 | 93 | /* Validate block coordinates are within bounding box */ 94 | for (int block = 0; block < MAX_BLOCK_LEN; block++) { 95 | int x = shape->rot_flat[rot][block][0]; 96 | int y = shape->rot_flat[rot][block][1]; 97 | 98 | if (x >= 0 && y >= 0) { /* Valid coordinates */ 99 | assert_test( 100 | x < shape->rot_wh[rot].x && y < shape->rot_wh[rot].y, 101 | "tetromino %d rotation %d block %d coordinates (%d,%d) " 102 | "should be within bounds (%d,%d)", 103 | i, rot, block, x, y, shape->rot_wh[rot].x, 104 | shape->rot_wh[rot].y); 105 | } 106 | } 107 | } 108 | 109 | /* Validate collision detection data (crust) */ 110 | for (int rot = 0; rot < shape->n_rot; rot++) { 111 | for (direction_t d = 0; d < 4; d++) { 112 | assert_test(shape->crust_len[rot][d] >= 0 && 113 | shape->crust_len[rot][d] <= MAX_BLOCK_LEN, 114 | "tetromino %d rotation %d direction %d crust " 115 | "length should be reasonable", 116 | i, rot, d); 117 | } 118 | } 119 | } 120 | 121 | shape_free(); 122 | } 123 | 124 | void test_shape_rotation_consistency(void) 125 | { 126 | bool init_result = shape_init(); 127 | assert_test(init_result, "shape_init should succeed for rotation tests"); 128 | 129 | if (!init_result) 130 | return; 131 | 132 | /* Test that rot and rot_flat contain consistent data */ 133 | for (int i = 0; i < NUM_TETRIS_SHAPES; i++) { 134 | shape_t *shape = shape_get(i); 135 | if (!shape) 136 | continue; 137 | 138 | for (int rot = 0; rot < shape->n_rot && rot < 4; rot++) { 139 | /* Compare rot array with rot_flat array */ 140 | for (int block = 0; block < MAX_BLOCK_LEN; block++) { 141 | if (!shape->rot[rot] || !shape->rot[rot][block]) 142 | continue; 143 | 144 | int rot_x = shape->rot[rot][block][0]; 145 | int rot_y = shape->rot[rot][block][1]; 146 | int flat_x = shape->rot_flat[rot][block][0]; 147 | int flat_y = shape->rot_flat[rot][block][1]; 148 | 149 | assert_test(rot_x == flat_x && rot_y == flat_y, 150 | "tetromino %d rotation %d block %d: rot and " 151 | "rot_flat should match (%d,%d) vs (%d,%d)", 152 | i, rot, block, rot_x, rot_y, flat_x, flat_y); 153 | } 154 | } 155 | } 156 | 157 | shape_free(); 158 | } 159 | 160 | void test_shape_crust_data_validation(void) 161 | { 162 | bool init_result = shape_init(); 163 | assert_test(init_result, "shape_init should succeed for crust tests"); 164 | 165 | if (!init_result) 166 | return; 167 | 168 | /* Test collision detection crust data consistency */ 169 | for (int i = 0; i < NUM_TETRIS_SHAPES; i++) { 170 | shape_t *shape = shape_get(i); 171 | if (!shape) 172 | continue; 173 | 174 | for (int rot = 0; rot < shape->n_rot && rot < 4; rot++) { 175 | for (direction_t d = 0; d < 4; d++) { 176 | int crust_len = shape->crust_len[rot][d]; 177 | 178 | /* Verify crust and crust_flat consistency */ 179 | for (int c = 0; c < crust_len && c < MAX_BLOCK_LEN; c++) { 180 | if (shape->crust[rot][d] && shape->crust[rot][d][c]) { 181 | int crust_x = shape->crust[rot][d][c][0]; 182 | int crust_y = shape->crust[rot][d][c][1]; 183 | int flat_x = shape->crust_flat[rot][d][c][0]; 184 | int flat_y = shape->crust_flat[rot][d][c][1]; 185 | 186 | assert_test( 187 | crust_x == flat_x && crust_y == flat_y, 188 | "tetromino %d rotation %d direction %d crust %d: " 189 | "crust and crust_flat should match (%d,%d) vs " 190 | "(%d,%d)", 191 | i, rot, d, c, crust_x, crust_y, flat_x, flat_y); 192 | 193 | /* Verify crust coordinates are within shape bounds */ 194 | assert_test(crust_x >= 0 && crust_y >= 0 && 195 | crust_x < shape->rot_wh[rot].x && 196 | crust_y < shape->rot_wh[rot].y, 197 | "tetromino %d rotation %d direction %d " 198 | "crust %d coordinates (%d,%d) should be " 199 | "within bounds (%d,%d)", 200 | i, rot, d, c, crust_x, crust_y, 201 | shape->rot_wh[rot].x, shape->rot_wh[rot].y); 202 | } 203 | } 204 | } 205 | } 206 | } 207 | 208 | shape_free(); 209 | } 210 | 211 | void test_shape_stream_basic_operations(void) 212 | { 213 | bool init_result = shape_init(); 214 | assert_test(init_result, "shape_init should succeed for stream tests"); 215 | 216 | if (!init_result) 217 | return; 218 | 219 | /* Test stream creation and basic operations */ 220 | shape_stream_t *stream = shape_stream_new(); 221 | assert_test(stream, "shape_stream_new should return valid stream"); 222 | 223 | if (!stream) { 224 | shape_free(); 225 | return; 226 | } 227 | 228 | /* Test peek operations for next piece preview */ 229 | shape_t *next_piece = shape_stream_peek(stream, 0); 230 | assert_test(next_piece, "peek at next piece should return valid shape"); 231 | 232 | shape_t *preview_piece = shape_stream_peek(stream, 1); 233 | assert_test(preview_piece, 234 | "peek at preview piece should return valid shape"); 235 | 236 | /* Test that peek doesn't advance stream */ 237 | shape_t *next_again = shape_stream_peek(stream, 0); 238 | assert_test(next_again == next_piece, 239 | "multiple peeks should return same piece"); 240 | 241 | /* Test piece consumption (pop) */ 242 | shape_t *current_piece = shape_stream_pop(stream); 243 | assert_test(current_piece == next_piece, 244 | "popped piece should match peeked piece"); 245 | 246 | /* Test stream advancement */ 247 | shape_t *new_next = shape_stream_peek(stream, 0); 248 | assert_test(new_next == preview_piece, 249 | "after pop, next piece should be old preview piece"); 250 | 251 | nfree(stream); 252 | shape_free(); 253 | } 254 | 255 | void test_shape_stream_bounds_and_edge_cases(void) 256 | { 257 | bool init_result = shape_init(); 258 | assert_test(init_result, "shape_init should succeed for edge case tests"); 259 | 260 | if (!init_result) 261 | return; 262 | 263 | shape_stream_t *stream = shape_stream_new(); 264 | if (!stream) { 265 | shape_free(); 266 | return; 267 | } 268 | 269 | /* Test bounds checking for peek operations */ 270 | assert_test(shape_stream_peek(stream, -2) == NULL, 271 | "negative peek index should return NULL"); 272 | assert_test(shape_stream_peek(stream, stream->max_len) == NULL, 273 | "peek beyond max_len should return NULL"); 274 | 275 | /* Test NULL stream handling */ 276 | assert_test(shape_stream_peek(NULL, 0) == NULL, 277 | "peek with NULL stream should return NULL"); 278 | assert_test(shape_stream_pop(NULL) == NULL, 279 | "pop with NULL stream should return NULL"); 280 | 281 | /* Test sustained piece generation (multiple bags) */ 282 | bool sustained_generation = true; 283 | for (int i = 0; i < 21; i++) { /* 3 complete 7-bags */ 284 | shape_t *piece = shape_stream_pop(stream); 285 | if (!piece) { 286 | sustained_generation = false; 287 | break; 288 | } 289 | } 290 | assert_test(sustained_generation, 291 | "stream should provide pieces across multiple 7-bags"); 292 | 293 | nfree(stream); 294 | shape_free(); 295 | } 296 | 297 | void test_shape_stream_7bag_randomization(void) 298 | { 299 | bool init_result = shape_init(); 300 | assert_test(init_result, "shape_init should succeed for 7-bag tests"); 301 | 302 | if (!init_result) 303 | return; 304 | 305 | /* Reset bag to ensure fresh test state */ 306 | shape_bag_reset(); 307 | 308 | shape_stream_t *stream = shape_stream_new(); 309 | if (!stream) { 310 | shape_free(); 311 | return; 312 | } 313 | 314 | /* Test the 7-bag algorithm: every 7 pieces should contain each tetromino 315 | * exactly once 316 | */ 317 | const int bag_size = 7; 318 | shape_t *bag_pieces[bag_size]; 319 | int piece_counts[NUM_TETRIS_SHAPES] = {0}; 320 | 321 | /* Get first complete bag */ 322 | bool valid_bag = true; 323 | for (int i = 0; i < bag_size; i++) { 324 | bag_pieces[i] = shape_stream_pop(stream); 325 | if (!bag_pieces[i]) { 326 | valid_bag = false; 327 | break; 328 | } 329 | 330 | /* Count occurrences of each tetromino type */ 331 | for (int shape_idx = 0; shape_idx < NUM_TETRIS_SHAPES; shape_idx++) { 332 | if (bag_pieces[i] == shape_get(shape_idx)) { 333 | piece_counts[shape_idx]++; 334 | break; 335 | } 336 | } 337 | } 338 | 339 | assert_test(valid_bag, "should be able to get complete 7-piece bag"); 340 | 341 | if (valid_bag) { 342 | /* Verify 7-bag property: each tetromino appears exactly once */ 343 | int unique_pieces = 0; 344 | bool perfect_distribution = true; 345 | 346 | for (int i = 0; i < NUM_TETRIS_SHAPES; i++) { 347 | if (piece_counts[i] == 1) { 348 | unique_pieces++; 349 | } else if (piece_counts[i] != 1) { 350 | perfect_distribution = false; 351 | } 352 | } 353 | 354 | assert_test(unique_pieces == NUM_TETRIS_SHAPES, 355 | "7-bag should contain all %d tetromino types (got %d)", 356 | NUM_TETRIS_SHAPES, unique_pieces); 357 | assert_test(perfect_distribution, 358 | "each tetromino should appear exactly once per bag"); 359 | } 360 | 361 | nfree(stream); 362 | shape_free(); 363 | } 364 | 365 | void test_shape_stream_multiple_bags_distribution(void) 366 | { 367 | bool init_result = shape_init(); 368 | assert_test(init_result, 369 | "shape_init should succeed for multi-bag distribution tests"); 370 | 371 | if (!init_result) 372 | return; 373 | 374 | /* Reset bag for consistent test state */ 375 | shape_bag_reset(); 376 | 377 | shape_stream_t *stream = shape_stream_new(); 378 | if (!stream) { 379 | shape_free(); 380 | return; 381 | } 382 | 383 | /* Test distribution across multiple complete 7-bags */ 384 | const int num_bags = 5; 385 | const int total_pieces = num_bags * 7; 386 | int total_counts[NUM_TETRIS_SHAPES] = {0}; 387 | 388 | bool multi_bag_valid = true; 389 | for (int piece_num = 0; piece_num < total_pieces; piece_num++) { 390 | shape_t *piece = shape_stream_pop(stream); 391 | if (!piece) { 392 | multi_bag_valid = false; 393 | break; 394 | } 395 | 396 | /* Count each tetromino type across all bags */ 397 | for (int shape_idx = 0; shape_idx < NUM_TETRIS_SHAPES; shape_idx++) { 398 | if (piece == shape_get(shape_idx)) { 399 | total_counts[shape_idx]++; 400 | break; 401 | } 402 | } 403 | } 404 | 405 | assert_test(multi_bag_valid, 406 | "should be able to get pieces from multiple bags"); 407 | 408 | if (multi_bag_valid) { 409 | /* Verify perfect distribution: each piece should appear exactly 410 | * num_bags times */ 411 | bool perfect_multi_bag_distribution = true; 412 | for (int i = 0; i < NUM_TETRIS_SHAPES; i++) { 413 | if (total_counts[i] != num_bags) { 414 | perfect_multi_bag_distribution = false; 415 | break; 416 | } 417 | } 418 | 419 | assert_test(perfect_multi_bag_distribution, 420 | "across %d bags, each tetromino should appear exactly %d " 421 | "times", 422 | num_bags, num_bags); 423 | } 424 | 425 | nfree(stream); 426 | shape_free(); 427 | } 428 | 429 | void test_shape_stream_reset_functionality(void) 430 | { 431 | bool init_result = shape_init(); 432 | assert_test(init_result, "shape_init should succeed for reset tests"); 433 | 434 | if (!init_result) 435 | return; 436 | 437 | shape_stream_t *stream = shape_stream_new(); 438 | if (!stream) { 439 | shape_free(); 440 | return; 441 | } 442 | 443 | /* Get some pieces to advance the bag state */ 444 | shape_t *piece1 = shape_stream_pop(stream); 445 | shape_t *piece2 = shape_stream_pop(stream); 446 | shape_t *piece3 = shape_stream_pop(stream); 447 | 448 | assert_test(piece1 && piece2 && piece3, 449 | "should be able to get initial pieces"); 450 | 451 | /* Reset the bag and test that we can still get pieces */ 452 | shape_bag_reset(); 453 | 454 | /* Create new stream to test post-reset behavior */ 455 | shape_stream_t *new_stream = shape_stream_new(); 456 | if (!new_stream) { 457 | nfree(stream); 458 | shape_free(); 459 | return; 460 | } 461 | 462 | shape_t *reset_piece = shape_stream_pop(new_stream); 463 | assert_test(reset_piece, "should be able to get piece after reset"); 464 | 465 | /* Test that reset doesn't affect existing streams immediately */ 466 | shape_t *old_stream_piece = shape_stream_pop(stream); 467 | assert_test(old_stream_piece, 468 | "existing stream should continue working after reset"); 469 | 470 | nfree(stream); 471 | nfree(new_stream); 472 | shape_free(); 473 | } 474 | 475 | void test_shape_stream_gameplay_sequence(void) 476 | { 477 | bool init_result = shape_init(); 478 | assert_test(init_result, 479 | "shape_init should succeed for gameplay sequence tests"); 480 | 481 | if (!init_result) 482 | return; 483 | 484 | shape_stream_t *stream = shape_stream_new(); 485 | if (!stream) { 486 | shape_free(); 487 | return; 488 | } 489 | 490 | /* Simulate realistic Tetris gameplay: get pieces, preview next pieces */ 491 | const int game_pieces = 50; /* Simulate reasonable game length */ 492 | bool gameplay_valid = true; 493 | int pieces_seen[NUM_TETRIS_SHAPES] = {0}; 494 | 495 | for (int piece_num = 0; piece_num < game_pieces; piece_num++) { 496 | /* Get current piece (like starting a new piece drop) */ 497 | shape_t *current = shape_stream_pop(stream); 498 | if (!current) { 499 | gameplay_valid = false; 500 | break; 501 | } 502 | 503 | /* Preview next piece (typical Tetris feature) */ 504 | shape_t *preview = shape_stream_peek(stream, 0); 505 | if (!preview) { 506 | gameplay_valid = false; 507 | break; 508 | } 509 | 510 | /* Count piece distribution for fairness analysis */ 511 | for (int shape_idx = 0; shape_idx < NUM_TETRIS_SHAPES; shape_idx++) { 512 | if (current == shape_get(shape_idx)) { 513 | pieces_seen[shape_idx]++; 514 | break; 515 | } 516 | } 517 | } 518 | 519 | assert_test(gameplay_valid, 520 | "gameplay sequence should provide valid pieces"); 521 | 522 | if (gameplay_valid) { 523 | /* Verify fair distribution over longer gameplay */ 524 | int min_seen = game_pieces; 525 | int max_seen = 0; 526 | 527 | for (int i = 0; i < NUM_TETRIS_SHAPES; i++) { 528 | if (pieces_seen[i] < min_seen) 529 | min_seen = pieces_seen[i]; 530 | if (pieces_seen[i] > max_seen) 531 | max_seen = pieces_seen[i]; 532 | } 533 | 534 | /* In fair 7-bag system, distribution should be relatively even */ 535 | int distribution_range = max_seen - min_seen; 536 | assert_test(distribution_range <= NUM_TETRIS_SHAPES, 537 | "piece distribution should be fair (range: %d)", 538 | distribution_range); 539 | } 540 | 541 | nfree(stream); 542 | shape_free(); 543 | } 544 | 545 | void test_shape_stream_memory_management(void) 546 | { 547 | bool init_result = shape_init(); 548 | assert_test(init_result, 549 | "shape_init should succeed for memory management tests"); 550 | 551 | if (!init_result) 552 | return; 553 | 554 | /* Test multiple stream creation and cleanup */ 555 | const int num_streams = 10; 556 | shape_stream_t *streams[num_streams]; 557 | 558 | /* Create multiple streams */ 559 | bool creation_success = true; 560 | for (int i = 0; i < num_streams; i++) { 561 | streams[i] = shape_stream_new(); 562 | if (!streams[i]) { 563 | creation_success = false; 564 | break; 565 | } 566 | } 567 | 568 | assert_test(creation_success, "should be able to create multiple streams"); 569 | 570 | if (creation_success) { 571 | /* Test that all streams work independently */ 572 | bool all_streams_work = true; 573 | for (int i = 0; i < num_streams; i++) { 574 | shape_t *piece = shape_stream_peek(streams[i], 0); 575 | if (!piece) { 576 | all_streams_work = false; 577 | break; 578 | } 579 | } 580 | 581 | assert_test(all_streams_work, 582 | "all created streams should provide valid pieces"); 583 | 584 | /* Clean up all streams */ 585 | for (int i = 0; i < num_streams; i++) { 586 | if (streams[i]) 587 | nfree(streams[i]); 588 | } 589 | } 590 | 591 | shape_free(); 592 | } 593 | 594 | void test_shape_multiple_init_cleanup_cycles(void) 595 | { 596 | /* Test robustness of multiple init/cleanup cycles */ 597 | for (int cycle = 0; cycle < 3; cycle++) { 598 | bool init_result = shape_init(); 599 | assert_test(init_result, "init cycle %d should succeed", cycle); 600 | 601 | if (init_result) { 602 | /* Quick validation in each cycle */ 603 | shape_t *test_shape = shape_get(0); 604 | assert_test(test_shape, "shape access should work in cycle %d", 605 | cycle); 606 | 607 | shape_stream_t *test_stream = shape_stream_new(); 608 | if (test_stream) { 609 | shape_t *test_piece = shape_stream_peek(test_stream, 0); 610 | assert_test(test_piece, "stream should work in cycle %d", 611 | cycle); 612 | nfree(test_stream); 613 | } 614 | 615 | shape_free(); 616 | } 617 | } 618 | } 619 | 620 | void test_shape_edge_cases(void) 621 | { 622 | /* Test operations before initialization */ 623 | assert_test(shape_get(0) == NULL, 624 | "shape access before init should return NULL"); 625 | 626 | /* Test shape stream before initialization */ 627 | shape_stream_t *uninit_stream = shape_stream_new(); 628 | if (uninit_stream) { 629 | /* Stream creation might work, but should handle uninitialized shapes */ 630 | shape_stream_peek(uninit_stream, 0); /* Should not crash */ 631 | assert_test(true, "stream operations before init should not crash"); 632 | nfree(uninit_stream); 633 | } 634 | 635 | /* Test double cleanup safety */ 636 | shape_free(); 637 | shape_free(); /* Should be safe */ 638 | assert_test(true, "multiple shape_free calls should be safe"); 639 | 640 | /* Test operations after cleanup */ 641 | assert_test(shape_get(0) == NULL, 642 | "shape access after cleanup should return NULL"); 643 | 644 | /* Test shape_bag_reset without initialization */ 645 | shape_bag_reset(); /* Should not crash */ 646 | assert_test(true, "shape_bag_reset before init should not crash"); 647 | } 648 | -------------------------------------------------------------------------------- /train.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Genetic Algorithm Training Program for Tetris AI Weights 3 | * 4 | * Evolves feature weights through competitive survival tournaments. 5 | * Uses existing benchmark infrastructure for fitness evaluation. 6 | */ 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | #include "tetris.h" 16 | #include "utils.h" 17 | 18 | /* Genetic Algorithm Parameters */ 19 | #define POPULATION_SIZE 8 /* Number of AI candidates per generation */ 20 | #define ELITE_COUNT 2 /* Top performers kept each generation */ 21 | #define MUTATION_RATE 0.3f /* Probability of mutating each feature */ 22 | #define MUTATION_STRENGTH 0.5f /* Maximum change per mutation */ 23 | #define CROSSOVER_RATE 0.7f /* Probability of crossover vs pure mutation */ 24 | #define EVALUATION_GAMES 3 /* Games per fitness evaluation */ 25 | #define CLEAR_RATE_WEIGHT 0.1f /* Weight for clear rate efficiency */ 26 | #define MAX_GENERATIONS 100 /* Training generations (use -1 for infinite) */ 27 | /* Max pieces per fitness test game (need longer for LCPP) */ 28 | #define FITNESS_GAMES_LIMIT 1000 29 | 30 | /* Feature names for logging (using existing FEAT_LIST macro) */ 31 | static const char *FEATURE_NAMES[] = { 32 | #define _(feat) #feat, 33 | FEAT_LIST 34 | #undef _ 35 | }; 36 | 37 | /* Individual AI candidate */ 38 | typedef struct { 39 | float weights[N_FEATIDX]; 40 | float fitness; 41 | int generation; 42 | int games_won; 43 | float avg_lcpp; 44 | int avg_lines; 45 | float clear_rate; 46 | } ai_individual_t; 47 | 48 | /* Training statistics */ 49 | typedef struct { 50 | int generation; 51 | float best_fitness; 52 | float avg_fitness; 53 | ai_individual_t best_individual; 54 | int evaluations_done; 55 | } training_stats_t; 56 | 57 | /* Silent single game evaluation for training (no progress output) */ 58 | static game_stats_t train_evaluate_single_game(const float *weights) 59 | { 60 | game_stats_t stats = {0}; /* Initialize all fields to 0 */ 61 | uint64_t start_ns = get_time_ns(); 62 | 63 | grid_t *g = grid_new(GRID_HEIGHT, GRID_WIDTH); 64 | block_t *b = block_new(); 65 | shape_stream_t *ss = shape_stream_new(); 66 | 67 | if (!g || !b || !ss) { 68 | nfree(g); 69 | nfree(b); 70 | nfree(ss); 71 | return stats; 72 | } 73 | 74 | int total_points = 0; 75 | int total_lines_cleared = 0; 76 | int pieces_placed = 0; 77 | const int MAX_PIECES = FITNESS_GAMES_LIMIT; 78 | const int MAX_MOVE_ATTEMPTS = 50; 79 | 80 | /* Initialize first block */ 81 | shape_stream_pop(ss); 82 | shape_t *first_shape = shape_stream_peek(ss, 0); 83 | if (!first_shape) 84 | goto cleanup; 85 | 86 | block_init(b, first_shape); 87 | grid_block_spawn(g, b); 88 | 89 | if (grid_block_collides(g, b)) 90 | goto cleanup; 91 | 92 | pieces_placed = 1; 93 | 94 | /* Main game loop - silent evaluation */ 95 | while (pieces_placed < MAX_PIECES) { 96 | /* Early termination only for extremely poor performers 97 | * Allow more pieces for better measurement accuracy 98 | */ 99 | if (pieces_placed > 800 && total_lines_cleared == 0) 100 | break; 101 | 102 | move_t *best = move_find_best(g, b, ss, weights); 103 | if (!best) 104 | break; 105 | 106 | /* Apply AI move */ 107 | block_t test_block = *b; 108 | test_block.rot = best->rot; 109 | test_block.offset.x = best->col; 110 | 111 | if (grid_block_collides(g, &test_block)) 112 | break; 113 | 114 | /* Rotation */ 115 | int rotation_attempts = 0; 116 | while (b->rot != best->rot && rotation_attempts < MAX_MOVE_ATTEMPTS) { 117 | int old_rot = b->rot; 118 | grid_block_rotate(g, b, 1); 119 | if (b->rot == old_rot) 120 | break; 121 | rotation_attempts++; 122 | } 123 | 124 | /* Movement */ 125 | int movement_attempts = 0; 126 | while (b->offset.x != best->col && 127 | movement_attempts < MAX_MOVE_ATTEMPTS) { 128 | int old_x = b->offset.x; 129 | int target_col = best->col; 130 | 131 | /* Move multiple steps at once if far away */ 132 | int distance = abs(target_col - b->offset.x); 133 | int steps = (distance > 5) ? distance / 2 : 1; 134 | 135 | if (b->offset.x < target_col) { 136 | grid_block_move(g, b, RIGHT, steps); 137 | } else { 138 | grid_block_move(g, b, LEFT, steps); 139 | } 140 | if (b->offset.x == old_x) 141 | break; 142 | movement_attempts++; 143 | } 144 | 145 | /* Drop and place */ 146 | grid_block_drop(g, b); 147 | if (grid_block_collides(g, b)) 148 | break; 149 | 150 | grid_block_add(g, b); 151 | 152 | /* Clear lines */ 153 | int cleared = grid_clear_lines(g); 154 | if (cleared > 0) { 155 | total_lines_cleared += cleared; 156 | 157 | /* Track line distribution */ 158 | if (cleared >= 1 && cleared <= 4) { 159 | stats.line_distribution[cleared]++; 160 | stats.total_clears++; 161 | } 162 | 163 | int line_points = cleared * 100; 164 | if (cleared > 1) 165 | line_points *= cleared; 166 | total_points += line_points; 167 | } 168 | 169 | /* Track maximum height */ 170 | int current_max = 0; 171 | for (int col = 0; col < GRID_WIDTH; col++) { 172 | int height = g->relief[col] + 1; 173 | if (height > current_max) 174 | current_max = height; 175 | } 176 | if (current_max > stats.max_height_reached) 177 | stats.max_height_reached = current_max; 178 | 179 | /* Next piece */ 180 | shape_stream_pop(ss); 181 | shape_t *next_shape = shape_stream_peek(ss, 0); 182 | if (!next_shape) 183 | break; 184 | 185 | block_init(b, next_shape); 186 | grid_block_spawn(g, b); 187 | 188 | if (grid_block_collides(g, b)) 189 | break; 190 | 191 | pieces_placed++; 192 | } 193 | 194 | if (pieces_placed >= MAX_PIECES) 195 | stats.hit_piece_limit = true; 196 | 197 | cleanup:; 198 | uint64_t duration_ns = get_time_ns() - start_ns; 199 | double duration = (double) duration_ns / 1e9; 200 | 201 | stats.lines_cleared = total_lines_cleared; 202 | stats.score = total_points; 203 | stats.pieces_placed = pieces_placed; 204 | stats.lcpp = 205 | pieces_placed > 0 ? (float) total_lines_cleared / pieces_placed : 0.0f; 206 | stats.game_duration = duration; 207 | stats.pieces_per_second = duration > 0 ? pieces_placed / duration : 0.0f; 208 | nfree(ss); 209 | nfree(g); 210 | nfree(b); 211 | 212 | return stats; 213 | } 214 | 215 | /* Efficient batch fitness evaluation for entire population */ 216 | static void evaluate_population_fitness(ai_individual_t *population, 217 | int pop_size, 218 | int eval_games) 219 | { 220 | printf(" Evaluating population:\n"); 221 | 222 | /* Pre-allocate statistics arrays for batch processing */ 223 | game_stats_t *batch_stats = malloc(eval_games * sizeof(game_stats_t)); 224 | if (!batch_stats) { 225 | fprintf(stderr, "Failed to allocate batch statistics\n"); 226 | return; 227 | } 228 | 229 | /* Initial progress bar display */ 230 | printf(" [ ] 0%%"); 231 | fflush(stdout); 232 | 233 | for (int i = 0; i < pop_size; i++) { 234 | /* Batch evaluate multiple games for this individual */ 235 | int total_lines = 0; 236 | int total_pieces = 0; 237 | float total_lcpp = 0.0f; 238 | int games_completed = 0; 239 | 240 | int total_max_heights = 0; 241 | int total_clears_events = 0; 242 | int total_survival_lines = 0; 243 | int total_recovery_lines = 0; 244 | 245 | for (int game = 0; game < eval_games; game++) { 246 | batch_stats[game] = 247 | train_evaluate_single_game(population[i].weights); 248 | 249 | total_lines += batch_stats[game].lines_cleared; 250 | total_pieces += batch_stats[game].pieces_placed; 251 | total_lcpp += batch_stats[game].lcpp; 252 | total_max_heights += batch_stats[game].max_height_reached; 253 | total_clears_events += batch_stats[game].total_clears; 254 | 255 | if (batch_stats[game].pieces_placed >= FITNESS_GAMES_LIMIT * 0.8f) 256 | games_completed++; 257 | } 258 | 259 | /* Add survival and recovery tests for comprehensive evaluation */ 260 | if (i % 4 == 0) { /* Test every 4th individual for performance */ 261 | total_survival_lines = bench_run_survival(population[i].weights); 262 | total_recovery_lines = bench_run_recovery(population[i].weights); 263 | } 264 | 265 | /* Calculate composite fitness score with optimized formula */ 266 | population[i].avg_lines = total_lines / eval_games; 267 | population[i].avg_lcpp = total_lcpp / eval_games; 268 | population[i].games_won = games_completed; 269 | 270 | /* Clear rate: clears per piece as efficiency proxy */ 271 | population[i].clear_rate = 272 | (total_pieces > 0) ? (float) total_clears_events / total_pieces 273 | : 0.0f; 274 | 275 | /* Enhanced fitness formula prioritizing efficiency */ 276 | float survival_ratio = 277 | (float) total_pieces / (FITNESS_GAMES_LIMIT * eval_games); 278 | float completion_ratio = (float) games_completed / eval_games; 279 | 280 | /* Weighted fitness components */ 281 | float lcpp_score = population[i].avg_lcpp * 2000.0f; 282 | float efficiency_bonus = 283 | (population[i].avg_lcpp > 0.25f) ? 200.0f : 0.0f; 284 | float line_score = (float) total_lines * 0.5f; 285 | 286 | /* Enhanced survival bonus considering max height reached */ 287 | float avg_max_height = (float) total_max_heights / eval_games; 288 | float height_factor = (avg_max_height > 5.0f) 289 | ? fmaxf(0.2f, 10.0f / avg_max_height) 290 | : 2.0f; 291 | float survival_bonus = survival_ratio * 50.0f * height_factor; 292 | 293 | float completion_bonus = completion_ratio * 25.0f; 294 | 295 | /* Clear efficiency bonus (proxy for good search efficiency) */ 296 | float clear_efficiency_bonus = population[i].clear_rate * 100.0f; 297 | 298 | /* Height penalty - penalize high stacks more severely */ 299 | float height_penalty = 300 | (avg_max_height > 15.0f) ? -20.0f * (avg_max_height - 15.0f) : 0.0f; 301 | 302 | /* Survival and recovery bonuses for comprehensive evaluation */ 303 | float survival_score = 304 | (total_survival_lines > 0) ? total_survival_lines * 0.5f : 0.0f; 305 | float recovery_score = 306 | (total_recovery_lines > 0) ? total_recovery_lines * 1.0f : 0.0f; 307 | 308 | float efficiency_penalty = 309 | (population[i].avg_lcpp < 0.15f) ? -300.0f : 0.0f; 310 | 311 | population[i].fitness = 312 | lcpp_score + efficiency_bonus + line_score + survival_bonus + 313 | completion_bonus + clear_efficiency_bonus + height_penalty + 314 | survival_score + recovery_score + efficiency_penalty; 315 | 316 | /* Update progress bar after evaluation with colors */ 317 | int progress_chars = ((i + 1) * 20) / pop_size; 318 | int percent = ((i + 1) * 100) / pop_size; 319 | printf("\r ["); 320 | 321 | /* Green filled portion */ 322 | printf("\x1b[32m"); /* Green color */ 323 | for (int j = 0; j < progress_chars; j++) 324 | printf("█"); 325 | printf("\x1b[0m"); /* Reset color */ 326 | 327 | /* Empty portion */ 328 | for (int j = progress_chars; j < 20; j++) 329 | printf(" "); 330 | printf("] %3d%%", percent); 331 | fflush(stdout); 332 | } 333 | 334 | /* Complete the progress bar and move to next line */ 335 | printf(" - Complete!\n"); 336 | free(batch_stats); 337 | } 338 | 339 | /* Initialize individual with optimized weight selection */ 340 | static void init_individual(ai_individual_t *individual, int generation) 341 | { 342 | /* Use existing default weights as baseline */ 343 | float *defaults = move_defaults(); 344 | if (!defaults) { 345 | /* Fallback if defaults unavailable */ 346 | for (int i = 0; i < N_FEATIDX; i++) 347 | individual->weights[i] = ((float) rand() / RAND_MAX - 0.5f) * 2.0f; 348 | } else { 349 | for (int i = 0; i < N_FEATIDX; i++) { 350 | /* Add controlled random variation to proven weights */ 351 | float variation = ((float) rand() / RAND_MAX - 0.5f) * 0.05f; 352 | individual->weights[i] = defaults[i] + variation; 353 | } 354 | free(defaults); 355 | } 356 | 357 | individual->fitness = 0.0f; 358 | individual->generation = generation; 359 | individual->games_won = 0; 360 | individual->avg_lcpp = 0.0f; 361 | individual->avg_lines = 0; 362 | } 363 | 364 | /* Adaptive mutation with feature-specific constraints */ 365 | static void mutate_individual(ai_individual_t *individual, 366 | int generation, 367 | float mut_rate) 368 | { 369 | /* Feature-specific constraints based on Tetris AI analysis */ 370 | const float min_bounds[] = { 371 | [FEATIDX_RELIEF_MAX] = -2.0f, [FEATIDX_RELIEF_AVG] = -5.0f, 372 | [FEATIDX_RELIEF_VAR] = -2.0f, [FEATIDX_GAPS] = -4.0f, 373 | [FEATIDX_OBS] = -3.0f, [FEATIDX_DISCONT] = -2.0f, 374 | [FEATIDX_CREVICES] = -4.0f, /* Crevices strongly penalized */ 375 | }; 376 | const float max_bounds[] = { 377 | [FEATIDX_RELIEF_MAX] = 1.0f, [FEATIDX_RELIEF_AVG] = -0.5f, 378 | [FEATIDX_RELIEF_VAR] = 1.0f, [FEATIDX_GAPS] = 0.0f, 379 | [FEATIDX_OBS] = 0.0f, [FEATIDX_DISCONT] = 1.0f, 380 | [FEATIDX_CREVICES] = -0.5f, /* Always negative penalty */ 381 | }; 382 | 383 | for (int i = 0; i < N_FEATIDX; i++) { 384 | if ((float) rand() / RAND_MAX < mut_rate) { 385 | float change = 386 | ((float) rand() / RAND_MAX - 0.5f) * 2.0f * MUTATION_STRENGTH; 387 | individual->weights[i] += change; 388 | 389 | /* Apply bounds */ 390 | if (individual->weights[i] > max_bounds[i]) 391 | individual->weights[i] = max_bounds[i]; 392 | if (individual->weights[i] < min_bounds[i]) 393 | individual->weights[i] = min_bounds[i]; 394 | } 395 | } 396 | individual->generation = generation; 397 | } 398 | 399 | /* Optimized crossover with blend ratio variation */ 400 | static void crossover_individuals(const ai_individual_t *parent1, 401 | const ai_individual_t *parent2, 402 | ai_individual_t *child, 403 | int generation) 404 | { 405 | /* Use fitness-weighted blending for better offspring */ 406 | float total_fitness = parent1->fitness + parent2->fitness; 407 | float alpha = (total_fitness > 0) ? parent1->fitness / total_fitness : 0.5f; 408 | 409 | /* Add some randomness to prevent premature convergence */ 410 | alpha += ((float) rand() / RAND_MAX - 0.5f) * 0.2f; 411 | alpha = fmaxf(0.1f, fminf(0.9f, alpha)); /* Clamp to reasonable range */ 412 | 413 | for (int i = 0; i < N_FEATIDX; i++) { 414 | child->weights[i] = 415 | alpha * parent1->weights[i] + (1.0f - alpha) * parent2->weights[i]; 416 | } 417 | child->generation = generation; 418 | } 419 | 420 | /* Tournament selection with size adaptation */ 421 | static int tournament_select(const ai_individual_t *population, 422 | int pop_size, 423 | int tournament_size) 424 | { 425 | int best_idx = rand_range(pop_size); 426 | float best_fitness = population[best_idx].fitness; 427 | 428 | for (int i = 1; i < tournament_size; i++) { 429 | int candidate = rand_range(pop_size); 430 | if (population[candidate].fitness > best_fitness) { 431 | best_idx = candidate; 432 | best_fitness = population[candidate].fitness; 433 | } 434 | } 435 | return best_idx; 436 | } 437 | 438 | /* Efficient sorting comparison */ 439 | static int compare_fitness(const void *a, const void *b) 440 | { 441 | const ai_individual_t *ia = (const ai_individual_t *) a; 442 | const ai_individual_t *ib = (const ai_individual_t *) b; 443 | 444 | /* Use epsilon for floating point comparison */ 445 | float diff = ia->fitness - ib->fitness; 446 | if (diff > 0.001f) 447 | return -1; 448 | if (diff < -0.001f) 449 | return 1; 450 | return 0; 451 | } 452 | 453 | /* Optimized individual printing */ 454 | static void print_individual(const ai_individual_t *individual, 455 | const char *label) 456 | { 457 | printf("%s (Gen %d, Fitness: %.2f, LCPP: %.3f, Lines: %d, Won: %d):\n", 458 | label, individual->generation, individual->fitness, 459 | individual->avg_lcpp, individual->avg_lines, individual->games_won); 460 | 461 | printf(" Weights: ["); 462 | for (int i = 0; i < N_FEATIDX; i++) { 463 | printf("%.3f%s", individual->weights[i], 464 | (i < N_FEATIDX - 1) ? ", " : "]\n"); 465 | } 466 | } 467 | 468 | /* Generate optimized C code output */ 469 | static void print_c_weights(const ai_individual_t *best) 470 | { 471 | printf("\n/* Evolved weights (Generation %d, Fitness: %.2f) */\n", 472 | best->generation, best->fitness); 473 | printf("static const float evolved_weights[N_FEATIDX] = {\n"); 474 | for (int i = 0; i < N_FEATIDX; i++) { 475 | printf(" [FEATIDX_%s] = %.4ff,\n", FEATURE_NAMES[i], 476 | best->weights[i]); 477 | } 478 | printf("};\n"); 479 | } 480 | 481 | /* Main training loop with performance optimizations */ 482 | static void train_weights(int max_generations, 483 | int pop_size, 484 | int eval_games, 485 | float mut_rate) 486 | { 487 | /* Use nalloc for better memory management */ 488 | ai_individual_t *population = 489 | nalloc(pop_size * sizeof(ai_individual_t), NULL); 490 | ai_individual_t *next_population = 491 | nalloc(pop_size * sizeof(ai_individual_t), NULL); 492 | 493 | if (!population || !next_population) { 494 | fprintf(stderr, "Failed to allocate population memory\n"); 495 | nfree(population); 496 | nfree(next_population); 497 | return; 498 | } 499 | 500 | training_stats_t stats = {0}; 501 | int elite_count = (pop_size >= 4) ? 2 : 1; 502 | 503 | printf("Tetris AI Weight Evolution Training\n"); 504 | printf("===================================\n"); 505 | printf("Population: %d, Evaluation Games: %d, Max Generations: %d\n", 506 | pop_size, eval_games, max_generations); 507 | printf("Elite Count: %d, Mutation Rate: %.2f\n\n", elite_count, mut_rate); 508 | 509 | /* Initialize population */ 510 | for (int i = 0; i < pop_size; i++) 511 | init_individual(&population[i], 0); 512 | 513 | /* Evolution loop */ 514 | for (int generation = 0; 515 | generation < max_generations || max_generations < 0; generation++) { 516 | printf("Generation %d:\n", generation); 517 | printf("-------------\n"); 518 | 519 | /* Efficient batch evaluation */ 520 | evaluate_population_fitness(population, pop_size, eval_games); 521 | stats.evaluations_done += pop_size; 522 | 523 | /* Sort by fitness */ 524 | qsort(population, pop_size, sizeof(ai_individual_t), compare_fitness); 525 | 526 | /* Update statistics */ 527 | stats.generation = generation; 528 | stats.best_fitness = population[0].fitness; 529 | 530 | float total_fitness = 0.0f; 531 | for (int i = 0; i < pop_size; i++) 532 | total_fitness += population[i].fitness; 533 | stats.avg_fitness = total_fitness / pop_size; 534 | stats.best_individual = population[0]; 535 | 536 | /* Report results */ 537 | print_individual(&population[0], "Best"); 538 | printf(" Average Fitness: %.2f\n", stats.avg_fitness); 539 | printf(" Evaluations Done: %d\n\n", stats.evaluations_done); 540 | 541 | /* Early termination for excellent solutions */ 542 | if (population[0].fitness > 600.0f && population[0].avg_lcpp > 0.32f) { 543 | printf("Excellent solution found! Stopping early.\n\n"); 544 | break; 545 | } 546 | 547 | /* Create next generation with elitism */ 548 | for (int i = 0; i < elite_count; i++) 549 | next_population[i] = population[i]; 550 | 551 | /* Generate offspring */ 552 | for (int i = elite_count; i < pop_size; i++) { 553 | if ((float) rand() / RAND_MAX < CROSSOVER_RATE) { 554 | /* Crossover with adaptive tournament size */ 555 | int tournament_size = (generation < 10) ? 2 : 3; 556 | int parent1 = 557 | tournament_select(population, pop_size, tournament_size); 558 | int parent2 = 559 | tournament_select(population, pop_size, tournament_size); 560 | crossover_individuals(&population[parent1], 561 | &population[parent2], &next_population[i], 562 | generation + 1); 563 | } else { 564 | /* Pure mutation */ 565 | int parent = tournament_select(population, pop_size, 2); 566 | next_population[i] = population[parent]; 567 | } 568 | 569 | /* Apply mutation */ 570 | mutate_individual(&next_population[i], generation + 1, mut_rate); 571 | } 572 | 573 | /* Replace population */ 574 | memcpy(population, next_population, pop_size * sizeof(ai_individual_t)); 575 | 576 | /* Periodic weight saving */ 577 | if (generation % 10 == 9) { 578 | char filename[64]; 579 | snprintf(filename, sizeof(filename), "weights_gen_%d.txt", 580 | generation + 1); 581 | FILE *f = fopen(filename, "w"); 582 | if (f) { 583 | for (int i = 0; i < N_FEATIDX; i++) 584 | fprintf(f, "%.6f\n", population[0].weights[i]); 585 | fclose(f); 586 | printf("Saved weights to %s\n\n", filename); 587 | } 588 | } 589 | } 590 | 591 | /* Final results */ 592 | printf("\nTraining Complete!\n"); 593 | printf("==================\n"); 594 | print_individual(&stats.best_individual, "Final Best Individual"); 595 | print_c_weights(&stats.best_individual); 596 | 597 | /* Save final weights */ 598 | FILE *f = fopen("evolved_weights.h", "w"); 599 | if (f) { 600 | fprintf(f, "/* Evolved weights (Generation %d, Fitness: %.2f) */\n", 601 | stats.best_individual.generation, 602 | stats.best_individual.fitness); 603 | fprintf(f, "#pragma once\n\n"); 604 | fprintf(f, "#include \"tetris.h\"\n\n"); 605 | fprintf(f, "static const float evolved_weights[N_FEATIDX] = {\n"); 606 | for (int i = 0; i < N_FEATIDX; i++) { 607 | fprintf(f, " [FEATIDX_%s] = %.4ff,\n", FEATURE_NAMES[i], 608 | stats.best_individual.weights[i]); 609 | } 610 | fprintf(f, "};\n"); 611 | fclose(f); 612 | printf("\nEvolved weights saved to evolved_weights.h\n"); 613 | } 614 | 615 | nfree(population); 616 | nfree(next_population); 617 | } 618 | 619 | /* Usage information */ 620 | static void print_usage(const char *program_name) 621 | { 622 | printf("Usage: %s [options]\n", program_name); 623 | printf("Options:\n"); 624 | printf(" -g N Maximum generations (default: %d, -1 for infinite)\n", 625 | MAX_GENERATIONS); 626 | printf(" -p N Population size (default: %d)\n", POPULATION_SIZE); 627 | printf(" -e N Evaluation games per individual (default: %d)\n", 628 | EVALUATION_GAMES); 629 | printf(" -m RATE Mutation rate 0.0-1.0 (default: %.2f)\n", 630 | MUTATION_RATE); 631 | printf(" -s SEED Random seed (default: time-based)\n"); 632 | printf(" -h Show this help\n"); 633 | printf("\nExample:\n"); 634 | printf( 635 | " %s -g 50 -p 12 -e 5 # 50 generations, 12 individuals, 5 games " 636 | "each\n", 637 | program_name); 638 | } 639 | 640 | /* Main program */ 641 | int main(int argc, char *argv[]) 642 | { 643 | int max_generations = MAX_GENERATIONS; 644 | int population_size = POPULATION_SIZE; 645 | int evaluation_games = EVALUATION_GAMES; 646 | float mutation_rate = MUTATION_RATE; 647 | unsigned int seed = (unsigned int) time(NULL); 648 | 649 | /* Parse command line options */ 650 | int opt; 651 | while ((opt = getopt(argc, argv, "g:p:e:m:s:h")) != -1) { 652 | switch (opt) { 653 | case 'g': 654 | max_generations = atoi(optarg); 655 | break; 656 | case 'p': 657 | population_size = atoi(optarg); 658 | if (population_size < 2 || population_size > 50) { 659 | fprintf(stderr, "Population size must be between 2 and 50\n"); 660 | return 1; 661 | } 662 | break; 663 | case 'e': 664 | evaluation_games = atoi(optarg); 665 | if (evaluation_games < 1 || evaluation_games > 20) { 666 | fprintf(stderr, "Evaluation games must be between 1 and 20\n"); 667 | return 1; 668 | } 669 | break; 670 | case 'm': 671 | mutation_rate = atof(optarg); 672 | if (mutation_rate < 0.0f || mutation_rate > 1.0f) { 673 | fprintf(stderr, "Mutation rate must be between 0.0 and 1.0\n"); 674 | return 1; 675 | } 676 | break; 677 | case 's': 678 | seed = (unsigned int) atoi(optarg); 679 | break; 680 | case 'h': 681 | print_usage(argv[0]); 682 | return 0; 683 | default: 684 | fprintf(stderr, "Unknown option. Use -h for help.\n"); 685 | return 1; 686 | } 687 | } 688 | 689 | /* Initialize systems */ 690 | grid_init(); 691 | if (!shape_init()) { 692 | fprintf(stderr, "Failed to initialize shapes\n"); 693 | return 1; 694 | } 695 | 696 | srand(seed); 697 | atexit(shape_free); 698 | 699 | /* Start optimized training */ 700 | train_weights(max_generations, population_size, evaluation_games, 701 | mutation_rate); 702 | 703 | return 0; 704 | } 705 | -------------------------------------------------------------------------------- /tetris.h: -------------------------------------------------------------------------------- 1 | /** 2 | * auto-tetris: AI-powered Tetris game engine with terminal user interface 3 | * 4 | * Key components: 5 | * - Shape system: Standard 7-piece tetromino set with rotation support 6 | * - Grid system: Game field with collision detection and line clearing 7 | * - Block system: Individual piece positioning and movement 8 | * - AI system: Multi-ply search with evaluation heuristics 9 | * - TUI system: Terminal-based rendering with color support 10 | * - Benchmark system: Performance measurement and statistics 11 | */ 12 | 13 | #pragma once 14 | 15 | #include 16 | #include 17 | 18 | /* 19 | * Core Types and Constants 20 | */ 21 | 22 | /** 23 | * Direction enumeration for movement and rotation operations 24 | * 25 | * Used for block movement, grid queries, and shape crust calculations. 26 | * Values correspond to standard geometric directions. 27 | */ 28 | typedef enum { 29 | BOT, /**< Downward/bottom direction (gravity) */ 30 | LEFT, /**< Leftward direction */ 31 | TOP, /**< Upward/top direction */ 32 | RIGHT /**< Rightward direction */ 33 | } direction_t; 34 | 35 | /** 36 | * 2D coordinate structure for grid positions 37 | * 38 | * Uses unsigned 8-bit integers for memory efficiency. 39 | * Coordinates are in grid space: (0,0) = bottom-left. 40 | */ 41 | typedef struct { 42 | uint8_t x, y; 43 | } coord_t; 44 | 45 | /** Maximum number of cells in any tetromino piece */ 46 | #define MAX_BLOCK_LEN 4 47 | 48 | /** Number of standard Tetris shapes (tetrominoes: I, J, L, O, S, T, Z) */ 49 | #define NUM_TETRIS_SHAPES 7 50 | 51 | /** Default game grid width (wider than standard for AI breathing room) */ 52 | #define GRID_WIDTH 14 53 | 54 | /** Default game grid height (standard Tetris height) */ 55 | #define GRID_HEIGHT 20 56 | 57 | /** Packed row representation - each row fits in a single machine word */ 58 | typedef uint64_t row_t; 59 | 60 | /* 61 | * Shape System 62 | */ 63 | 64 | /** 65 | * Complete tetromino shape definition with all rotations 66 | * 67 | * Contains precomputed rotation data, boundary information, and optimization 68 | * structures for fast collision detection and AI evaluation. 69 | */ 70 | typedef struct { 71 | int n_rot; /**< Number of unique rotations (1-4) */ 72 | coord_t rot_wh[4]; /**< Width/height for each rotation */ 73 | int **crust[4][4]; /**< Edge cells for collision detection */ 74 | int crust_len[4][4]; /**< Number of crust cells per direction */ 75 | int crust_flat[4][4][MAX_BLOCK_LEN][2]; /**< Flattened crust data */ 76 | int max_dim_len; /**< Maximum dimension across all rotations */ 77 | int **rot[4]; /**< Cell coordinates for each rotation */ 78 | int rot_flat[4][MAX_BLOCK_LEN][2]; /**< Flattened rotation data */ 79 | unsigned sig; /**< Geometry signature for color lookup optimization */ 80 | } shape_t; 81 | 82 | /** 83 | * Initialize the shape system with standard tetromino set 84 | * 85 | * Must be called before using any shape-related functions. 86 | * Creates all 7 standard tetrominoes with rotation data. 87 | * 88 | * Return true on success, false on memory allocation failure 89 | */ 90 | bool shape_init(void); 91 | 92 | /** 93 | * Reset the 7-bag random piece generator 94 | * 95 | * Forces the next piece selection to start a new shuffled bag. 96 | * Used primarily for testing and reproducible sequences. 97 | */ 98 | void shape_bag_reset(void); 99 | 100 | /** 101 | * Get shape by index for special effects 102 | * @index : Shape index (0 to NUM_TETRIS_SHAPES-1) 103 | * 104 | * Return Pointer to shape, or NULL if invalid index 105 | */ 106 | shape_t *shape_get(int index); 107 | 108 | /** 109 | * Cleanup all shape system memory 110 | * 111 | * Should be called at program exit to free shape resources. 112 | */ 113 | void shape_free(void); 114 | 115 | /* 116 | * Block System 117 | */ 118 | 119 | /** 120 | * Active tetromino piece with position and rotation 121 | * 122 | * Represents a falling or placed piece on the grid. 123 | * Combines shape reference with current position/orientation. 124 | */ 125 | typedef struct { 126 | coord_t offset; /**< Grid position (bottom-left of bounding box) */ 127 | int rot; /**< Current rotation index (0 to n_rot-1) */ 128 | shape_t *shape; /**< Reference to tetromino shape definition */ 129 | } block_t; 130 | 131 | /** 132 | * Allocate a new block instance 133 | * 134 | * Creates an uninitialized block. Must call block_init() before use. 135 | * 136 | * Return Pointer to new block, or NULL on allocation failure 137 | */ 138 | block_t *block_new(void); 139 | 140 | /** 141 | * Initialize block with shape and default position 142 | * 143 | * Sets block to rotation 0 at position (0,0) with given shape. 144 | * @b : Block to initialize 145 | * @s : Shape to assign (can be NULL for empty block) 146 | */ 147 | void block_init(block_t *b, shape_t *s); 148 | 149 | /** 150 | * Get absolute grid coordinates of block cell 151 | * 152 | * Calculates the grid position of the i-th cell in the block, 153 | * accounting for current position and rotation. 154 | * @b : Block to query 155 | * @i : Cell index (0 to MAX_BLOCK_LEN-1) 156 | * @result : Output coordinate (set to (-1,-1) if invalid) 157 | */ 158 | void block_get(const block_t *b, int i, coord_t *result); 159 | 160 | /** 161 | * Rotate block by specified amount 162 | * 163 | * Positive amounts rotate clockwise, negative counter-clockwise. 164 | * Rotation wraps around within the shape's valid rotations. 165 | * @b : Block to rotate 166 | * @amount : Rotation steps (typically 1 or -1) 167 | */ 168 | void block_rotate(block_t *b, int amount); 169 | 170 | /** 171 | * Move block in specified direction 172 | * 173 | * Updates block position without collision checking. 174 | * Use grid_block_move() for validated movement. 175 | * @b : Block to move 176 | * @d : Direction of movement 177 | * @amount : Distance to move (grid cells) 178 | */ 179 | void block_move(block_t *b, direction_t d, int amount); 180 | 181 | /** 182 | * Get extreme coordinate in specified direction 183 | * 184 | * Returns the furthest coordinate of the block in the given direction. 185 | * Useful for boundary checking and collision detection. 186 | * @b : Block to query 187 | * @d : Direction to check 188 | * 189 | * Return Extreme coordinate value 190 | */ 191 | int block_extreme(const block_t *b, direction_t d); 192 | 193 | /* 194 | * Grid System 195 | */ 196 | 197 | /** 198 | * Game grid with optimized line clearing and collision detection 199 | * 200 | * Uses packed bit representation for fast operations: 201 | * - Each row stored as single uint64_t for up to 64 columns 202 | * - Collision detection via bitwise AND operations 203 | * - Line clearing via simple mask comparison 204 | * - Maintains auxiliary data structures for AI evaluation 205 | */ 206 | typedef struct { 207 | row_t rows[GRID_HEIGHT]; /**< Packed cell occupancy: bit x in rows[y] */ 208 | row_t full_mask; /**< Precomputed mask: (1ULL << width) - 1 */ 209 | 210 | /* Auxiliary structures for AI evaluation - preserved for compatibility */ 211 | int **stacks; /**< Column stacks for fast height queries */ 212 | int *stack_cnt; /**< Number of blocks in each column */ 213 | int *relief; /**< Highest occupied row per column (-1 if empty) */ 214 | int *full_rows; /**< Array of completed row indices */ 215 | int n_full_rows; /**< Number of currently completed rows */ 216 | int width, height; /**< Grid dimensions */ 217 | int n_total_cleared; /**< Total lines cleared (lifetime) */ 218 | int n_last_cleared; /**< Lines cleared in last operation */ 219 | int *gaps; /**< Empty cells below relief per column */ 220 | uint64_t hash; /**< Incremental Zobrist hash for fast AI lookup */ 221 | } grid_t; 222 | 223 | /** 224 | * Compact snapshot for efficient rollback of grid changes 225 | * 226 | * Records minimal state needed to undo block placement and line clearing. 227 | * Much more efficient than full grid copying for AI search. 228 | */ 229 | typedef struct { 230 | /* Complete grid state backup for line clearing cases */ 231 | bool needs_full_restore; /**< Whether full restore is needed */ 232 | 233 | /* Full backup for complex cases (line clearing) */ 234 | row_t full_rows_backup[GRID_HEIGHT]; /**< Complete packed row backup */ 235 | int full_relief[GRID_WIDTH]; /**< Column heights backup */ 236 | int full_gaps[GRID_WIDTH]; /**< Gap counts backup */ 237 | int full_stack_cnt[GRID_WIDTH]; /**< Stack counts backup */ 238 | int full_stacks[GRID_WIDTH][GRID_HEIGHT]; /**< Stack contents backup */ 239 | int full_n_full_rows; /**< Full row count backup */ 240 | int full_full_rows[GRID_HEIGHT]; /**< Full row list backup */ 241 | uint64_t full_hash; /**< Hash backup */ 242 | int full_n_total_cleared; /**< Total cleared backup */ 243 | int full_n_last_cleared; /**< Last cleared backup */ 244 | 245 | /* Simple backup for non-line-clearing cases */ 246 | bool simple_cells[MAX_BLOCK_LEN]; /**< Original cell states */ 247 | coord_t simple_coords[MAX_BLOCK_LEN]; /**< Cell coordinates */ 248 | int simple_count; /**< Number of cells */ 249 | 250 | /* Result tracking */ 251 | int lines_cleared; /**< Lines cleared by this operation */ 252 | } grid_snapshot_t; 253 | 254 | /** 255 | * Initialize grid system and Zobrist hash tables 256 | * 257 | * Must be called once before creating any grids. Initializes the Zobrist hash 258 | * table used for fast grid state comparison. 259 | */ 260 | void grid_init(void); 261 | 262 | /** 263 | * Create new game grid 264 | * 265 | * Allocates and initializes a grid with specified dimensions. 266 | * All cells start empty, auxiliary structures are initialized. 267 | * @height : Grid height in cells 268 | * @width : Grid width in cells 269 | * 270 | * Return Pointer to new grid, or NULL on allocation failure 271 | */ 272 | grid_t *grid_new(int height, int width); 273 | 274 | /** 275 | * Copy grid state to another grid 276 | * 277 | * Performs deep copy of all grid data including auxiliary structures. 278 | * Destination grid must have same dimensions as source. 279 | * @dest : Destination grid (must be pre-allocated) 280 | * @src : Source grid to copy from 281 | */ 282 | void grid_copy(grid_t *dest, const grid_t *src); 283 | 284 | /** 285 | * Add block to grid permanently 286 | * 287 | * Places all block cells into the grid and updates auxiliary structures. 288 | * Block should be in final position (use grid_block_drop first). 289 | * @g : Grid to modify 290 | * @b : Block to add 291 | */ 292 | void grid_block_add(grid_t *g, const block_t *b); 293 | 294 | /** 295 | * Remove block from grid 296 | * 297 | * Removes all block cells from grid and updates auxiliary structures. 298 | * Used for undoing moves during AI search. 299 | * @g : Grid to modify 300 | * @b : Block to remove 301 | */ 302 | void grid_block_remove(grid_t *g, const block_t *b); 303 | 304 | /** 305 | * Apply block placement with snapshot for efficient rollback 306 | * 307 | * Places block, clears any completed lines, and records minimal state 308 | * needed for rollback. Much more efficient than grid copying for AI search. 309 | * @g : Grid to modify in-place 310 | * @b : Block to place (should be in final dropped position) 311 | * @snap : Snapshot structure to fill with rollback information 312 | * 313 | * Return Number of lines cleared 314 | */ 315 | int grid_apply_block(grid_t *g, const block_t *b, grid_snapshot_t *snap); 316 | 317 | /** 318 | * Rollback changes recorded in snapshot 319 | * 320 | * Efficiently undoes all changes made by grid_apply_block, restoring 321 | * the grid to its exact state before the block was applied. 322 | * @g : Grid to restore 323 | * @snap : Snapshot containing rollback information 324 | */ 325 | void grid_rollback(grid_t *g, const grid_snapshot_t *snap); 326 | 327 | /** 328 | * Position block at top-center of grid 329 | * 330 | * Places block at standard starting position: horizontally centered 331 | * and elevated above any existing pieces. 332 | * @g : Grid for positioning reference 333 | * @b : Block to position 334 | * 335 | * Return 1 if positioning successful, 0 if immediate collision 336 | */ 337 | int grid_block_spawn(const grid_t *g, block_t *b); 338 | 339 | /** 340 | * Check if block intersects with grid or boundaries 341 | * 342 | * Tests whether block at current position would collide with 343 | * occupied cells or extend outside grid boundaries. 344 | * @g : Grid to test against 345 | * @b : Block to test 346 | * 347 | * Return true if intersection/collision detected 348 | */ 349 | bool grid_block_collides(const grid_t *g, const block_t *b); 350 | 351 | /** 352 | * Drop block to lowest valid position 353 | * 354 | * Moves block downward until it would collide with grid or bottom. 355 | * Used for hard drop and AI move execution. 356 | * @g : Grid for collision testing 357 | * @b : Block to drop (position modified) 358 | * 359 | * Return Number of cells dropped 360 | */ 361 | int grid_block_drop(const grid_t *g, block_t *b); 362 | 363 | /** 364 | * Move block with collision validation 365 | * 366 | * Attempts to move block in specified direction. If move would cause 367 | * collision, the block position remains unchanged. 368 | * @g : Grid for collision testing 369 | * @b : Block to move 370 | * @d : Direction of movement 371 | * @amount : Distance to move 372 | */ 373 | void grid_block_move(const grid_t *g, block_t *b, direction_t d, int amount); 374 | 375 | /** 376 | * Rotate block with collision validation 377 | * 378 | * Attempts to rotate block. If rotation would cause collision, 379 | * the block orientation remains unchanged. 380 | * @g : Grid for collision testing 381 | * @b : Block to rotate 382 | * @amount : Rotation steps (positive = clockwise) 383 | */ 384 | void grid_block_rotate(const grid_t *g, block_t *b, int amount); 385 | 386 | /** 387 | * Clear completed lines and update grid 388 | * 389 | * Removes all completely filled rows, compacts remaining rows downward, 390 | * and updates all auxiliary data structures. 391 | * @g : Grid to process 392 | * 393 | * Return Number of lines cleared 394 | */ 395 | int grid_clear_lines(grid_t *g); 396 | 397 | /** 398 | * Check if grid is in Tetris-ready state 399 | * 400 | * Detects if the grid has a well suitable for a 4-line Tetris clear. 401 | * A Tetris-ready grid has one column significantly lower than its 402 | * neighbors, forming a deep well that can accommodate an I-piece. 403 | * @g : Grid to analyze 404 | * @well_col : Output parameter for well column index (0-based), or -1 if none 405 | * 406 | * Return true if grid is Tetris-ready, false otherwise 407 | */ 408 | bool grid_is_tetris_ready(const grid_t *g, int *well_col); 409 | 410 | /** 411 | * Get the actual depth of a well at the specified column. 412 | * @g : Grid to analyze 413 | * @col : Column index to check (0-based) 414 | * 415 | * Return well depth in rows, or 0 if column is not a well 416 | */ 417 | int grid_get_well_depth(const grid_t *g, int col); 418 | 419 | /** 420 | * Check if a well is accessible (not blocked from above). 421 | * @g : Grid to analyze 422 | * @col : Column index of the well 423 | * @piece_width : Width of piece that would enter the well 424 | * 425 | * Return true if well can be accessed, false if blocked 426 | */ 427 | bool grid_is_well_accessible(const grid_t *g, int col, int piece_width); 428 | 429 | /* 430 | * Shape Stream System 431 | */ 432 | 433 | /** 434 | * Shape sequence generator with 7-bag randomization 435 | * 436 | * Provides fair tetromino distribution using the "bag" system: 437 | * each set of 7 pieces contains exactly one of each tetromino type. 438 | */ 439 | typedef struct { 440 | uint8_t max_len; /**< Maximum lookahead length */ 441 | int iter; /**< Current iteration counter */ 442 | bool *defined; /**< Which preview positions are populated */ 443 | shape_t **stream; /**< Array of upcoming shapes */ 444 | } shape_stream_t; 445 | 446 | /** 447 | * Create new shape stream 448 | * 449 | * Initializes empty shape sequence. Shapes are generated on-demand 450 | * using 7-bag randomization for fair distribution. 451 | * 452 | * Return Pointer to new stream, or NULL on allocation failure 453 | */ 454 | shape_stream_t *shape_stream_new(void); 455 | 456 | /** 457 | * Preview upcoming shape without consuming it 458 | * 459 | * Returns shape at specified preview distance without advancing stream. 460 | * Index 0 = next shape, 1 = shape after next, etc. 461 | * @stream : Shape stream to query 462 | * @idx : Preview distance (0-based) 463 | * 464 | * Return Pointer to shape, or NULL if invalid index 465 | */ 466 | shape_t *shape_stream_peek(const shape_stream_t *stream, int idx); 467 | 468 | /** 469 | * Get next shape and advance stream 470 | * 471 | * Returns the next shape in sequence and advances stream position. 472 | * This is how pieces are "consumed" during gameplay. 473 | * @stream : Shape stream to advance 474 | * 475 | * Return Pointer to next shape, or NULL on error 476 | */ 477 | shape_t *shape_stream_pop(shape_stream_t *stream); 478 | 479 | /* 480 | * Move Calculation and AI 481 | */ 482 | 483 | /* List of feature indices */ 484 | #define FEAT_LIST \ 485 | _(RELIEF_MAX) /* Maximum column height */ \ 486 | _(RELIEF_AVG) /* Average column height */ \ 487 | _(RELIEF_VAR) /* Variance in column heights */ \ 488 | _(GAPS) /* Empty cells below blocks */ \ 489 | _(OBS) /* Total occupied cells */ \ 490 | _(DISCONT) /* Height discontinuities */ \ 491 | _(CREVICES) /* Narrow gaps that are difficult to fill */ 492 | 493 | /* Feature indices for grid evaluation */ 494 | /* clang-format off */ 495 | enum { 496 | #define _(feat) FEATIDX_##feat, 497 | FEAT_LIST 498 | #undef _ 499 | N_FEATIDX 500 | }; 501 | /* clang-format on */ 502 | 503 | /** 504 | * AI move decision with position and rotation 505 | * 506 | * Represents the AI's chosen placement for a tetromino piece. 507 | * Contains final rotation and column position. 508 | */ 509 | typedef struct { 510 | shape_t *shape; /**< Shape this move applies to */ 511 | int rot; /**< Target rotation (0 to n_rot-1) */ 512 | int col; /**< Target column position */ 513 | } move_t; 514 | 515 | /** 516 | * Get default AI evaluation weights 517 | * 518 | * Returns tuned weights for the AI evaluation function. 519 | * Caller must free() the returned array. 520 | * 521 | * Return Pointer to weight array, or NULL on allocation failure 522 | */ 523 | float *move_defaults(void); 524 | 525 | /** 526 | * Calculate best move for current game state 527 | * 528 | * Uses multi-ply search with heuristic evaluation to find optimal 529 | * placement for the current piece. Considers upcoming pieces for 530 | * deeper strategic planning. 531 | * @g : Current game grid 532 | * @b : Current falling block 533 | * @ss : Shape stream for preview pieces 534 | * @w : Evaluation weights array 535 | * 536 | * Return Pointer to best move, or NULL if no valid moves 537 | */ 538 | move_t *move_find_best(const grid_t *g, 539 | const block_t *b, 540 | const shape_stream_t *ss, 541 | const float *w); 542 | 543 | /* 544 | * Game Logic 545 | */ 546 | 547 | /** 548 | * Run interactive game with AI/human mode switching 549 | * 550 | * Main game loop with terminal UI. Supports: 551 | * - Human play with keyboard controls 552 | * - AI demonstration mode 553 | * - Real-time mode switching 554 | * - Statistics tracking 555 | * 556 | * @w : AI evaluation weights 557 | */ 558 | void game_run(const float *w); 559 | 560 | /* 561 | * Benchmark System 562 | */ 563 | 564 | /** 565 | * Statistics for a single game run 566 | * 567 | * Captures performance metrics for AI evaluation and comparison. 568 | */ 569 | typedef struct { 570 | int lines_cleared; /**< Total lines cleared before game over */ 571 | int score; /**< Final score achieved */ 572 | int pieces_placed; /**< Number of pieces successfully placed */ 573 | float lcpp; /**< Lines cleared per piece (efficiency) */ 574 | double game_duration; /**< Game duration in seconds */ 575 | bool hit_piece_limit; /**< Whether game ended due to artificial limit */ 576 | float pieces_per_second; /**< AI decision speed metric */ 577 | int line_distribution[5]; /**< Distribution of clears: [0]=unused, 578 | [1]=singles, [2]=doubles, [3]=triples, 579 | [4]=tetrises */ 580 | int max_height_reached; /**< Maximum stack height during game */ 581 | int total_clears; /**< Total number of clear events (not lines) */ 582 | } game_stats_t; 583 | 584 | /** 585 | * Results from multiple benchmark games 586 | * 587 | * Aggregates statistics across multiple game runs for performance analysis. 588 | */ 589 | typedef struct { 590 | game_stats_t *games; /**< Individual game statistics */ 591 | int num_games; /**< Total number of games requested */ 592 | game_stats_t avg; /**< Average performance across games */ 593 | game_stats_t best; /**< Best single game performance */ 594 | int total_games_completed; /**< Games successfully completed */ 595 | int natural_endings; /**< Games that ended naturally vs limits */ 596 | } bench_results_t; 597 | 598 | /** 599 | * Run single benchmark game without UI 600 | * 601 | * Executes one complete game run for performance measurement. 602 | * Used internally by bench_run_multi() for multiple game analysis. 603 | * @weights : AI evaluation weights 604 | * @total_pieces_so_far : Running total for progress tracking 605 | * @total_expected_pieces : Expected total pieces across all games 606 | * 607 | * Return Game statistics for this run 608 | */ 609 | game_stats_t bench_run_single(const float *weights, 610 | int *total_pieces_so_far, 611 | int total_expected_pieces); 612 | 613 | /** 614 | * Run survival benchmark until game over 615 | * 616 | * Runs AI until natural game over and measures total lines cleared. 617 | * @weights : AI evaluation weights 618 | * 619 | * Return Total lines cleared before game over 620 | */ 621 | int bench_run_survival(const float *weights); 622 | 623 | /** 624 | * Run recovery benchmark from chaotic initial state 625 | * 626 | * Tests AI's ability to recover from difficult board situations. 627 | * @weights : AI evaluation weights 628 | * 629 | * Return Lines cleared during recovery test 630 | */ 631 | int bench_run_recovery(const float *weights); 632 | 633 | /** 634 | * Run multiple benchmark games for statistical analysis 635 | * 636 | * Executes specified number of games and collects performance statistics. 637 | * Provides progress indication and aggregated results. 638 | * @weights : AI evaluation weights 639 | * @num_games : Number of games to run 640 | * 641 | * Return Benchmark results with statistics 642 | */ 643 | bench_results_t bench_run_multi(const float *weights, int num_games); 644 | 645 | /** 646 | * Print formatted benchmark results 647 | * 648 | * Displays comprehensive statistics from benchmark run including 649 | * averages, best performance, and consistency metrics. 650 | * 651 | * @results : Benchmark results to display 652 | */ 653 | void bench_print(const bench_results_t *results); 654 | 655 | /* 656 | * Terminal User Interface 657 | */ 658 | 659 | /** 660 | * Input event types from terminal 661 | * 662 | * Represents user input actions for game control. 663 | */ 664 | typedef enum { 665 | INPUT_INVALID, /**< No valid input or unknown key */ 666 | INPUT_TOGGLE_MODE, /**< Switch between AI and human mode */ 667 | INPUT_PAUSE, /**< Pause/unpause game */ 668 | INPUT_QUIT, /**< Exit game */ 669 | INPUT_ROTATE, /**< Rotate current piece */ 670 | INPUT_MOVE_LEFT, /**< Move piece left */ 671 | INPUT_MOVE_RIGHT, /**< Move piece right */ 672 | INPUT_DROP, /**< Hard drop piece */ 673 | } input_t; 674 | 675 | /** 676 | * Initialize terminal UI system 677 | * 678 | * Sets up terminal for game display: raw mode, colors, borders. 679 | * Must be called before other TUI functions. 680 | * @g : Grid for layout reference 681 | */ 682 | void tui_setup(const grid_t *g); 683 | 684 | /** 685 | * Build internal display buffer 686 | * 687 | * Prepares off-screen buffer with grid state and falling block. 688 | * Call before tui_render_buffer() for optimal performance. 689 | * @g : Current grid state 690 | * @falling_block : Active piece (can be NULL) 691 | */ 692 | void tui_build_buffer(const grid_t *g, const block_t *falling_block); 693 | 694 | /** 695 | * Render display buffer to terminal 696 | * 697 | * Updates terminal with current display buffer contents. 698 | * Only redraws changed areas for performance. 699 | * @g : Grid for layout reference 700 | */ 701 | void tui_render_buffer(const grid_t *g); 702 | 703 | /** 704 | * Force complete display refresh 705 | * 706 | * Invalidates display cache and forces full redraw. 707 | * Use after major state changes or layout updates. 708 | */ 709 | void tui_refresh_force(void); 710 | 711 | /** 712 | * Display preview of next piece 713 | * 714 | * Shows upcoming tetromino in sidebar preview area. 715 | * @b : Block to preview (can be NULL to clear, ) 716 | * @color : Display color for the piece 717 | */ 718 | void tui_show_preview(const block_t *b, int color); 719 | 720 | /** 721 | * Assign color to placed block 722 | * 723 | * Associates color with block cells for persistent display. 724 | * Call when permanently placing a piece. 725 | * @b : Block to color 726 | * @color : ANSI color code (2-7) 727 | */ 728 | void tui_add_block_color(const block_t *b, int color); 729 | 730 | /** 731 | * Prepare for line clearing animation 732 | * 733 | * Captures current color state before clearing lines. 734 | * Call before grid_clear_lines() to preserve colors. 735 | * @g : Grid to preserve colors from 736 | */ 737 | void tui_save_colors(const grid_t *g); 738 | 739 | /** 740 | * Apply preserved colors after line clearing 741 | * 742 | * Restores colors to remaining blocks after lines are cleared. 743 | * Call after grid_clear_lines() to maintain visual consistency. 744 | * @g : Grid to apply colors to 745 | */ 746 | void tui_restore_colors(const grid_t *g); 747 | 748 | /** 749 | * Force complete display redraw 750 | * 751 | * Clears game area and rebuilds entire display. 752 | * Use sparingly due to performance impact. 753 | * @g : Grid for layout reference 754 | */ 755 | void tui_force_redraw(const grid_t *g); 756 | 757 | /** 758 | * Perform periodic display maintenance 759 | * 760 | * Cleans up display artifacts and refreshes borders. 761 | * Call periodically during gameplay. 762 | * @g : Grid for layout reference 763 | */ 764 | void tui_cleanup_display(const grid_t *g); 765 | 766 | /** 767 | * Refresh game borders 768 | * 769 | * Redraws the border around the game area. 770 | * @g : Grid for layout reference 771 | */ 772 | void tui_refresh_borders(const grid_t *g); 773 | 774 | /** 775 | * Update game statistics display 776 | * 777 | * Refreshes sidebar with current game statistics. 778 | * @level : Current game level 779 | * @points : Current score 780 | * @lines_cleared : Total lines cleared 781 | */ 782 | void tui_update_stats(int level, int points, int lines_cleared); 783 | 784 | /** 785 | * Update mode indicator display 786 | * 787 | * Shows current play mode (AI/Human) in sidebar. 788 | * @ai_mode : true for AI mode, false for human 789 | */ 790 | void tui_update_mode_display(bool ai_mode); 791 | 792 | /** 793 | * Animate completed line clearing 794 | * 795 | * Shows flashing animation for cleared lines. 796 | * @g : Grid reference 797 | * @completed_rows : Array of completed row indices 798 | * @num_completed : Number of rows to animate 799 | */ 800 | void tui_flash_lines(const grid_t *g, 801 | const int *completed_rows, 802 | int num_completed); 803 | 804 | /** 805 | * Display message to user 806 | * 807 | * Shows text message in game area (e.g., "Game Over"). 808 | * @g : Grid for positioning reference 809 | * @msg : Message string to display 810 | */ 811 | void tui_prompt(const grid_t *g, const char *msg); 812 | 813 | /** 814 | * Flush terminal output 815 | * 816 | * Ensures all pending output is displayed immediately. 817 | */ 818 | void tui_refresh(void); 819 | 820 | /** 821 | * Get user input with timeout 822 | * 823 | * Checks for keyboard input with short timeout for responsive gameplay. 824 | * 825 | * Return Input event type 826 | */ 827 | input_t tui_scankey(void); 828 | 829 | /** 830 | * Show falling pieces animation 831 | * 832 | * Displays animated falling pieces effect for game over. 833 | * @g : Grid reference 834 | */ 835 | void tui_animate_gameover(const grid_t *g); 836 | 837 | /** 838 | * Get consistent color for shape type 839 | * 840 | * Returns the assigned color for a tetromino shape, assigning 841 | * one if not already colored. 842 | * @shape : Shape to get color for 843 | * 844 | * Return ANSI color code (2-7) 845 | */ 846 | int tui_get_shape_color(const shape_t *shape); 847 | 848 | /** 849 | * Cleanup and restore terminal 850 | * 851 | * Restores terminal to original state and cleans up resources. 852 | * Should be called before program exit. 853 | */ 854 | void tui_quit(void); 855 | --------------------------------------------------------------------------------