#include "ecex.h" #define TETRIS_W 10 #define TETRIS_H 20 #define TETRIS_CELLS (TETRIS_W * TETRIS_H) #define TETRIS_BUF "*tetris*" #define TETRIS_MODE "tetris-mode" #define TETRIS_VAR_BOARD "board" #define TETRIS_VAR_NEXT "next_piece" typedef struct tetris_state { ecex_t *ed; int *board; int piece; int rot; int x; int y; int next_piece; unsigned int rng; int score; int lines; int level; int game_over; int paused; int last_drop_ms; int drop_interval_ms; int log_tick_count; int log_draw_count; int log_state_id; } tetris_state_t; static int tetris_idx(int x, int y) { return y * TETRIS_W + x; } static int tetris_shape_cell(int piece, int rot, int col, int row) { return ecex_tetris_shape_cell(piece, rot, col, row); } static int *tetris_board(ecex_t *ed, tetris_state_t *s) { if (!ed || !s) return 0; return (int *)ecex_var_get_or_alloc(ed, s, TETRIS_VAR_BOARD, (size_t)TETRIS_CELLS, sizeof(int)); } static int tetris_next_piece(ecex_t *ed, tetris_state_t *s) { return ecex_var_i32(ed, s, TETRIS_VAR_NEXT, 0); } static void tetris_set_next_piece(ecex_t *ed, tetris_state_t *s, int piece) { if (piece < 0) piece = 0; if (piece > 6) piece = piece % 7; ecex_var_i32_set_scalar(ed, s, TETRIS_VAR_NEXT, piece); } static void tetris_clear_board(ecex_t *ed, tetris_state_t *s) { int *board; if (!s) { ecex_log("tetris_clear_board: null state"); return; } board = tetris_board(ed, s); ecex_log_ptr("tetris_clear_board: board=", board); ecex_mem_zero(board, (size_t)TETRIS_CELLS * sizeof(int)); ecex_log("tetris_clear_board: done"); } static int tetris_pick(tetris_state_t *s) { int piece; if (!s) { ecex_log("tetris_pick: null state"); return 0; } piece = ecex_random_bounded(7); if (piece < 0) piece = 0; if (piece > 6) piece = piece % 7; ecex_log_int("tetris_pick: piece=", piece); return piece; } static int tetris_collides(ecex_t *ed, tetris_state_t *s, int piece, int rot, int px, int py) { int r; int c; int *board = tetris_board(ed, s); if (!s || !board) return 1; for (r = 0; r < 4; ++r) { for (c = 0; c < 4; ++c) { int x; int y; if (!tetris_shape_cell(piece, rot, c, r)) continue; x = px + c; y = py + r; if (x < 0 || x >= TETRIS_W || y >= TETRIS_H) return 1; if (y >= 0 && ecex_i32_get(board, (size_t)tetris_idx(x, y))) return 1; } } return 0; } static void tetris_spawn(ecex_t *ed, tetris_state_t *s) { int queued; ecex_log_ptr("tetris_spawn: state=", s); if (!s) return; /* The sidebar must show the piece that will become active on the next * spawn. Keep that queued value in the host variable registry, then consume * it here and immediately replace it with a freshly-picked preview. */ queued = tetris_next_piece(ed, s); if (queued < 0 || queued > 6) queued = tetris_pick(s); s->piece = queued; tetris_set_next_piece(ed, s, tetris_pick(s)); ecex_log_int("tetris_spawn: current_piece=", s->piece); ecex_log_int("tetris_spawn: preview_piece=", tetris_next_piece(ed, s)); s->rot = 0; s->x = 3; s->y = -1; s->last_drop_ms = 0; if (tetris_collides(ed, s, s->piece, s->rot, s->x, s->y)) { ecex_log("tetris_spawn: immediate collision -> game over"); s->game_over = 1; } } static void tetris_reset(ecex_t *ed, tetris_state_t *s) { ecex_log_ptr("tetris_reset: state=", s); if (!s) return; tetris_clear_board(ed, s); ecex_log("tetris_reset: after clear"); /* Avoid calling libc/time-returning host functions from CCDJIT plugin code. * Seed from stable host-owned addresses instead; good enough for a toy game * and much safer for the tiny JIT ABI. */ s->rng = 0x9e3779b9u ^ (unsigned int)(size_t)s ^ (unsigned int)(size_t)tetris_board(ed, s); ecex_log_int("tetris_reset: rng=", (int)s->rng); s->score = 0; s->lines = 0; s->level = 1; s->game_over = 0; s->paused = 0; s->last_drop_ms = 0; s->drop_interval_ms = 650; tetris_set_next_piece(ed, s, tetris_pick(s)); s->log_tick_count = 0; s->log_draw_count = 0; ecex_log("tetris_reset: spawning first piece"); tetris_spawn(ed, s); ecex_log("tetris_reset: done"); } static void tetris_lock(ecex_t *ed, tetris_state_t *s) { ecex_log_ptr("tetris_lock: state=", s); int r; int c; int y; int cleared = 0; int *board; if (!s) return; board = tetris_board(ed, s); if (!board) return; for (r = 0; r < 4; ++r) { for (c = 0; c < 4; ++c) { int x; int yy; if (!tetris_shape_cell(s->piece, s->rot, c, r)) continue; x = s->x + c; yy = s->y + r; if (x >= 0 && x < TETRIS_W && yy >= 0 && yy < TETRIS_H) { ecex_i32_set(board, (size_t)tetris_idx(x, yy), s->piece + 1); } } } for (y = TETRIS_H - 1; y >= 0; --y) { int x; int full = 1; for (x = 0; x < TETRIS_W; ++x) { if (!ecex_i32_get(board, (size_t)tetris_idx(x, y))) { full = 0; break; } } if (!full) continue; ++cleared; { int yy; for (yy = y; yy > 0; --yy) { for (x = 0; x < TETRIS_W; ++x) { ecex_i32_set(board, (size_t)tetris_idx(x, yy), ecex_i32_get(board, (size_t)tetris_idx(x, yy - 1))); } } } for (x = 0; x < TETRIS_W; ++x) ecex_i32_set(board, (size_t)tetris_idx(x, 0), 0); ++y; } if (cleared) { int points = 0; if (cleared == 1) points = 100; else if (cleared == 2) points = 300; else if (cleared == 3) points = 500; else points = 800; s->score += points * s->level; s->lines += cleared; s->level = 1 + s->lines / 10; s->drop_interval_ms = 650 - (s->level - 1) * 45; if (s->drop_interval_ms < 80) s->drop_interval_ms = 80; } ecex_log_int("tetris_lock: cleared=", cleared); tetris_spawn(ed, s); } static int tetris_piece_has_visible_cells(tetris_state_t *s) { int r; int c; if (!s) return 0; for (r = 0; r < 4; ++r) { for (c = 0; c < 4; ++c) { int y; if (!tetris_shape_cell(s->piece, s->rot, c, r)) continue; y = s->y + r; if (y >= 0) return 1; } } return 0; } static int tetris_top_out_if_hidden(tetris_state_t *s, const char *where) { if (!s) return 1; if (tetris_piece_has_visible_cells(s)) return 0; ecex_log(where); ecex_log_int("tetris_top_out: piece=", s->piece); ecex_log_int("tetris_top_out: y=", s->y); s->game_over = 1; return 1; } static int tetris_soft_drop(ecex_t *ed, tetris_state_t *s) { if (!s) { ecex_log("tetris_soft_drop: null state"); return 0; } if (s->game_over || s->paused) return 0; if (!tetris_collides(ed, s, s->piece, s->rot, s->x, s->y + 1)) { ++s->y; return 1; } ecex_log_int("tetris_soft_drop: locking piece=", s->piece); ecex_log_int("tetris_soft_drop: y=", s->y); if (tetris_top_out_if_hidden(s, "tetris_soft_drop: hidden lock -> game over")) return 1; tetris_lock(ed, s); return 0; } static int tetris_move_horizontal(ecex_t *ed, tetris_state_t *s, int dx) { int old_x; int old_y; int old_rot; if (!s) { ecex_log("tetris_move_horizontal: null state"); return 0; } if (s->game_over || s->paused) return 0; old_x = s->x; old_y = s->y; old_rot = s->rot; if (!tetris_collides(ed, s, s->piece, s->rot, old_x + dx, old_y)) { s->x = old_x + dx; /* Horizontal movement must never vertical-kick the active piece. * Keep these assignments explicit because plugin/JIT callback bugs are * otherwise very hard to distinguish from game movement. */ s->y = old_y; s->rot = old_rot; /* Restart the gravity phase after manual lateral input. This avoids the * visual "bop" where a key repeat lands on the same frame as gravity * and appears to couple side movement with a vertical step. */ s->last_drop_ms = 0; ecex_log_int("tetris_move_horizontal: x=", s->x); ecex_log_int("tetris_move_horizontal: y=", s->y); return 1; } s->x = old_x; s->y = old_y; s->rot = old_rot; return 0; } static void tetris_hard_drop(ecex_t *ed, tetris_state_t *s) { int moved = 0; if (!s || s->game_over || s->paused) return; while (!tetris_collides(ed, s, s->piece, s->rot, s->x, s->y + 1)) { ++s->y; ++moved; } s->score += moved * 2; if (tetris_top_out_if_hidden(s, "tetris_hard_drop: hidden lock -> game over")) return; tetris_lock(ed, s); } static tetris_state_t *tetris_state_for_ed(ecex_t *ed) { buffer_t *buffer; tetris_state_t *s; ecex_log("tetris_state_for_ed: enter"); buffer = ecex_find_buffer(ed, TETRIS_BUF); ecex_log_ptr("tetris_state_for_ed: buffer=", buffer); if (!buffer) return 0; s = (tetris_state_t *)ecex_buffer_renderer_userdata(buffer); ecex_log_ptr("tetris_state_for_ed: state=", s); return s; } static int tetris_alpha8(int alpha) { if (alpha < 0) return 0; if (alpha > 255) return 255; return alpha; } static void tetris_color(ecex_draw_context_t *ctx, int cell, int alpha) { alpha = tetris_alpha8(alpha); if (cell == 1) ecex_draw_color_rgba8(ctx, 51, 191, 242, alpha); else if (cell == 2) ecex_draw_color_rgba8(ctx, 242, 217, 51, alpha); else if (cell == 3) ecex_draw_color_rgba8(ctx, 179, 89, 242, alpha); else if (cell == 4) ecex_draw_color_rgba8(ctx, 77, 217, 89, alpha); else if (cell == 5) ecex_draw_color_rgba8(ctx, 242, 64, 64, alpha); else if (cell == 6) ecex_draw_color_rgba8(ctx, 64, 102, 242, alpha); else if (cell == 7) ecex_draw_color_rgba8(ctx, 242, 140, 51, alpha); else ecex_draw_color_rgba8(ctx, 41, 41, 46, alpha); } static void tetris_draw_piece(ecex_draw_context_t *ctx, int piece, int rot, int px, int py, int ox, int oy, int cell, int alpha) { int r; int c; for (r = 0; r < 4; ++r) { for (c = 0; c < 4; ++c) { int bx; int by; if (!tetris_shape_cell(piece, rot, c, r)) continue; bx = px + c; by = py + r; if (by < 0) continue; tetris_color(ctx, piece + 1, alpha); ecex_draw_rect_i(ctx, ox + bx * cell + 1, oy + by * cell + 1, cell - 2, cell - 2); } } } static int tetris_tick(ecex_t *ed, buffer_t *buffer, int now_ms, void *userdata) { tetris_state_t *s = (tetris_state_t *)userdata; (void)ed; (void)buffer; if (!s) { ecex_log("tetris_tick: null state"); return 0; } s->log_tick_count += 1; if (s->log_tick_count <= 8 || (s->log_tick_count % 60) == 0) { ecex_log_int("tetris_tick: count=", s->log_tick_count); ecex_log_int("tetris_tick: now_ms=", now_ms); } if (s->last_drop_ms <= 0) { s->last_drop_ms = now_ms; return 1; } if (!s->game_over && !s->paused && now_ms - s->last_drop_ms >= s->drop_interval_ms) { ecex_log("tetris_tick: dropping piece"); tetris_soft_drop(ed, s); s->last_drop_ms = now_ms; return 1; } return 0; } static int tetris_draw(ecex_t *ed, buffer_t *buffer, ecex_draw_context_t *ctx, void *userdata) { tetris_state_t *s = (tetris_state_t *)userdata; int max_board_w; int max_board_h; int cell; int board_w; int board_h; int ox; int oy; int sx; int sy; int x; int y; int *board; (void)ed; (void)buffer; if (!s) { ecex_log("tetris_draw: null state"); return 0; } if (!ctx) { ecex_log("tetris_draw: null ctx"); return 0; } board = tetris_board(ed, s); if (!board) { ecex_log("tetris_draw: no registry board"); return 0; } s->log_draw_count += 1; if (s->log_draw_count <= 8 || (s->log_draw_count % 60) == 0) { ecex_log_int("tetris_draw: count=", s->log_draw_count); ecex_log_int("tetris_draw: ctx_w=", (int)ctx->w); ecex_log_int("tetris_draw: ctx_h=", (int)ctx->h); } if (s->log_draw_count <= 3) ecex_log("tetris_draw: background"); ecex_draw_color_rgba8(ctx, 20, 23, 28, 255); ecex_draw_rect_i(ctx, 0, 0, (int)ctx->w, (int)ctx->h); max_board_w = ((int)ctx->content_w * 62) / 100; max_board_h = (int)ctx->content_h - 24; cell = max_board_w / TETRIS_W; if (cell * TETRIS_H > max_board_h) cell = max_board_h / TETRIS_H; if (cell < 6) cell = 6; board_w = cell * TETRIS_W; board_h = cell * TETRIS_H; ox = (int)ctx->content_x + 12; oy = (int)ctx->content_y + 12; if (s->log_draw_count <= 3) ecex_log("tetris_draw: board frame"); ecex_draw_color_rgba8(ctx, 8, 9, 12, 255); ecex_draw_rect_i(ctx, ox - 6, oy - 6, board_w + 12, board_h + 12); ecex_draw_color_rgba8(ctx, 97, 102, 115, 255); ecex_draw_rect_outline_i(ctx, ox - 6, oy - 6, board_w + 12, board_h + 12, 2); if (s->log_draw_count <= 3) ecex_log("tetris_draw: cells"); for (y = 0; y < TETRIS_H; ++y) { for (x = 0; x < TETRIS_W; ++x) { int cell_value = ecex_i32_get(board, (size_t)tetris_idx(x, y)); tetris_color(ctx, cell_value, cell_value ? 255 : 140); ecex_draw_rect_i(ctx, ox + x * cell + 1, oy + y * cell + 1, cell - 2, cell - 2); } } if (s->log_draw_count <= 3) ecex_log("tetris_draw: active piece"); tetris_draw_piece(ctx, s->piece, s->rot, s->x, s->y, ox, oy, cell, 255); sx = ox + board_w + 28; sy = oy; if (s->log_draw_count <= 3) ecex_log("tetris_draw: sidebar"); ecex_draw_color_rgba8(ctx, 235, 235, 214, 255); ecex_log("tetris_draw: sidebar title"); ecex_draw_label_i(ctx, sx, sy, 1); sy += ((int)ctx->line_height * 16) / 10; ecex_draw_stat_i(ctx, sx, sy, 2, s->score); sy += (int)ctx->line_height; ecex_draw_stat_i(ctx, sx, sy, 3, s->lines); sy += (int)ctx->line_height; ecex_draw_stat_i(ctx, sx, sy, 4, s->level); sy += ((int)ctx->line_height * 15) / 10; ecex_draw_label_i(ctx, sx, sy, 5); sy += ((int)ctx->line_height * 8) / 10; ecex_log_int("tetris_draw: preview_piece=", tetris_next_piece(ed, s)); ecex_draw_tetris_preview_i(ctx, tetris_next_piece(ed, s), sx, sy, (cell * 72) / 100, 255); sy += cell * 4; ecex_draw_color_rgba8(ctx, 184, 189, 199, 255); ecex_log("tetris_draw: sidebar help"); ecex_draw_label_i(ctx, sx, sy, 6); sy += (int)ctx->line_height; ecex_draw_label_i(ctx, sx, sy, 7); sy += (int)ctx->line_height; ecex_draw_label_i(ctx, sx, sy, 8); sy += (int)ctx->line_height; ecex_draw_label_i(ctx, sx, sy, 9); sy += (int)ctx->line_height; ecex_draw_label_i(ctx, sx, sy, 10); sy += (int)ctx->line_height; ecex_draw_label_i(ctx, sx, sy, 11); if (s->paused || s->game_over) { ecex_draw_color_rgba8(ctx, 0, 0, 0, 174); ecex_draw_rect_i(ctx, ox, oy + (board_h * 43) / 100, board_w, ((int)ctx->line_height * 22) / 10); ecex_draw_color_rgba8(ctx, 255, 235, 89, 255); ecex_draw_label_i(ctx, ox + cell, oy + (board_h * 43) / 100 + ((int)ctx->line_height * 55) / 100, s->game_over ? 12 : 13); } if (s->log_draw_count <= 3) ecex_log("tetris_draw: done"); return 0; } static void tetris_free_state(void *userdata) { tetris_state_t *s = (tetris_state_t *)userdata; if (!s) return; ecex_var_free_owner(s->ed, s); ecex_config_free(s); } static int cmd_tetris(ecex_t *ed) { buffer_t *buffer; tetris_state_t *s; ecex_log("cmd_tetris: enter"); ecex_log_ptr("cmd_tetris: ed=", ed); if (!ed) { ecex_log("cmd_tetris: no editor"); return -1; } buffer = ecex_find_buffer(ed, TETRIS_BUF); ecex_log_ptr("cmd_tetris: existing buffer=", buffer); if (!buffer) { ecex_log("cmd_tetris: creating interactive buffer"); buffer = ecex_create_interactive_buffer(ed, TETRIS_BUF); } ecex_log_ptr("cmd_tetris: buffer=", buffer); if (!buffer) { ecex_log("cmd_tetris: buffer creation failed"); return -1; } s = (tetris_state_t *)ecex_buffer_renderer_userdata(buffer); ecex_log_ptr("cmd_tetris: existing state=", s); if (!s) { ecex_log_int("cmd_tetris: allocating state bytes=", (int)sizeof(*s)); s = (tetris_state_t *)ecex_config_calloc(1, sizeof(*s)); ecex_log_ptr("cmd_tetris: allocated state=", s); if (!s) { ecex_log("cmd_tetris: allocation failed"); return -1; } s->ed = ed; ecex_log_int("cmd_tetris: registry board bytes=", (int)((size_t)TETRIS_CELLS * sizeof(int))); ecex_log_ptr("cmd_tetris: registry board=", tetris_board(ed, s)); if (!tetris_board(ed, s)) { ecex_log("cmd_tetris: registry board allocation failed"); ecex_var_free_owner(ed, s); ecex_config_free(s); return -1; } tetris_reset(ed, s); ecex_log("cmd_tetris: setting renderer"); if (ecex_buffer_set_renderer(buffer, tetris_draw, s, tetris_free_state, ECEX_RENDER_REPLACE_CONTENT) != 0) { ecex_log("cmd_tetris: set_renderer failed"); ecex_config_free(s); return -1; } ecex_log("cmd_tetris: setting animation"); if (ecex_buffer_set_animation_ms(buffer, tetris_tick, s, 0, 60) != 0) { ecex_log("cmd_tetris: set_animation failed"); ecex_buffer_clear_renderer(buffer); return -1; } } ecex_log("cmd_tetris: replacing text"); if (ecex_buffer_replace_text(buffer, "Tetris renderer. Press n for a new game, q to quit the window.\n") != 0) { ecex_log("cmd_tetris: replace text failed"); return -1; } ecex_log("cmd_tetris: setting modified false"); ecex_buffer_set_modified(buffer, 0); ecex_log("cmd_tetris: setting major mode"); if (ecex_buffer_set_major_mode_by_name(ed, buffer, TETRIS_MODE) != 0) { ecex_log("cmd_tetris: set mode failed"); return -1; } ecex_log("cmd_tetris: switching buffer"); if (ecex_switch_buffer(ed, TETRIS_BUF) != 0) { ecex_log("cmd_tetris: switch failed"); return -1; } ecex_log("cmd_tetris: success"); return 0; } static int cmd_tetris_new(ecex_t *ed) { ecex_log("cmd_tetris_new: enter"); tetris_state_t *s = tetris_state_for_ed(ed); if (!s) return cmd_tetris(ed); tetris_reset(ed, s); return 0; } static int cmd_tetris_left(ecex_t *ed) { ecex_log("cmd_tetris_left: enter"); tetris_move_horizontal(ed, tetris_state_for_ed(ed), -1); return 0; } static int cmd_tetris_right(ecex_t *ed) { ecex_log("cmd_tetris_right: enter"); tetris_move_horizontal(ed, tetris_state_for_ed(ed), 1); return 0; } static int cmd_tetris_down(ecex_t *ed) { ecex_log("cmd_tetris_down: enter"); tetris_state_t *s = tetris_state_for_ed(ed); if (s) { if (tetris_soft_drop(ed, s)) s->score += 1; s->last_drop_ms = 0; } return 0; } static int cmd_tetris_rotate(ecex_t *ed) { ecex_log("cmd_tetris_rotate: enter"); tetris_state_t *s = tetris_state_for_ed(ed); int nr; if (!s || s->game_over || s->paused) return 0; nr = (s->rot + 1) & 3; if (!tetris_collides(ed, s, s->piece, nr, s->x, s->y)) { s->rot = nr; } else if (!tetris_collides(ed, s, s->piece, nr, s->x - 1, s->y)) { --s->x; s->rot = nr; } else if (!tetris_collides(ed, s, s->piece, nr, s->x + 1, s->y)) { ++s->x; s->rot = nr; } return 0; } static int cmd_tetris_drop(ecex_t *ed) { ecex_log("cmd_tetris_drop: enter"); tetris_hard_drop(ed, tetris_state_for_ed(ed)); return 0; } static int cmd_tetris_pause(ecex_t *ed) { ecex_log("cmd_tetris_pause: enter"); tetris_state_t *s = tetris_state_for_ed(ed); if (s && !s->game_over) { s->paused = !s->paused; s->last_drop_ms = 0; } return 0; } static int cmd_tetris_quit(ecex_t *ed) { ecex_log("cmd_tetris_quit: enter"); return ecex_execute_command(ed, "quit-window"); } int ecex_tetris_plugin(ecex_t *ed) { ecex_log("ecex_tetris_plugin: enter"); ECEX_CONFIG_MODE(TETRIS_MODE); ECEX_CONFIG_COMMAND("tetris", cmd_tetris); ECEX_CONFIG_COMMAND("tetris-new", cmd_tetris_new); ECEX_CONFIG_COMMAND("tetris-left", cmd_tetris_left); ECEX_CONFIG_COMMAND("tetris-right", cmd_tetris_right); ECEX_CONFIG_COMMAND("tetris-down", cmd_tetris_down); ECEX_CONFIG_COMMAND("tetris-rotate", cmd_tetris_rotate); ECEX_CONFIG_COMMAND("tetris-drop", cmd_tetris_drop); ECEX_CONFIG_COMMAND("tetris-pause", cmd_tetris_pause); ECEX_CONFIG_COMMAND("tetris-quit", cmd_tetris_quit); ECEX_CONFIG_MODE_BIND(TETRIS_MODE, "LEFT", "tetris-left"); ECEX_CONFIG_MODE_BIND(TETRIS_MODE, "h", "tetris-left"); ECEX_CONFIG_MODE_BIND(TETRIS_MODE, "RIGHT", "tetris-right"); ECEX_CONFIG_MODE_BIND(TETRIS_MODE, "l", "tetris-right"); ECEX_CONFIG_MODE_BIND(TETRIS_MODE, "DOWN", "tetris-down"); ECEX_CONFIG_MODE_BIND(TETRIS_MODE, "j", "tetris-down"); ECEX_CONFIG_MODE_BIND(TETRIS_MODE, "UP", "tetris-rotate"); ECEX_CONFIG_MODE_BIND(TETRIS_MODE, "k", "tetris-rotate"); ECEX_CONFIG_MODE_BIND(TETRIS_MODE, "SPC", "tetris-drop"); ECEX_CONFIG_MODE_BIND(TETRIS_MODE, "p", "tetris-pause"); ECEX_CONFIG_MODE_BIND(TETRIS_MODE, "n", "tetris-new"); ECEX_CONFIG_MODE_BIND(TETRIS_MODE, "q", "tetris-quit"); ecex_log("ecex_tetris_plugin: registered all commands and keybinds"); return 0; } #ifndef ECEX_NO_STANDALONE_CONFIG ECEX_CONFIG_BEGIN ECEX_CONFIG_INCLUDE(ecex_tetris_plugin); ECEX_CONFIG_END #endif