#include #include #include #include #include #include #include #include #include #include #include #define SIZE 4 #define MOVE_DURATION 0.12f #define BORDER_WIDTH 10 typedef enum { LEFT, RIGHT, UP, DOWN } dir_e; typedef struct { uint32_t value; int row; int col; } line_tile_t; typedef enum { GAME_PLAYING, GAME_WON, GAME_LOST } game_status_t; static game_status_t game_status; typedef struct { uint32_t area[SIZE][SIZE]; dir_e direction; uint32_t score; } game_t; static game_t game; static anim_t movement; static anim_track_t move_tracks[SIZE * SIZE]; static size_t move_track_count; static double last_frame_time; static bool board_is_full(void) { for (int r = 0; r < SIZE; ++r) for (int c = 0; c < SIZE; ++c) if (game.area[r][c] == 0) return false; return true; } /* Pixel size is calculated from the current window size while drawing. */ static uint32_t colors(uint32_t number) { switch (number) { case 0: return 0xFFFFFF; case 2: return 0x00FF00; case 4: return 0xFF0000; case 8: return 0x0000FF; default: return 0xFFFFFF; } } /* * Return the board coordinate for position p in a line, where p=0 is * the side toward which tiles move. */ static void line_coordinate(dir_e direction, int line, int p, int *row, int *col) { switch (direction) { case LEFT: *row = line; *col = p; break; case RIGHT: *row = line; *col = SIZE - 1 - p; break; case UP: *row = p; *col = line; break; case DOWN: *row = SIZE - 1 - p; *col = line; break; } } /* * Convert a board cell into the pixel rectangle used to draw its tile. * Keep this consistent with draw_tile_rect() and the cell backgrounds. */ static anim_rect_t tile_rect(int row, int col, uint32_t tile_size) { float side = (float)(tile_size - BORDER_WIDTH); return (anim_rect_t) { .x = BORDER_WIDTH / 2.0f + col * (float)tile_size, .y = BORDER_WIDTH / 2.0f + row * (float)tile_size, .w = side, .h = side }; } static void add_move_track(uint32_t value, int from_row, int from_col, int to_row, int to_col) { if (move_track_count >= SIZE * SIZE) return; uint32_t tile_size = game_state->width / SIZE; move_tracks[move_track_count++] = (anim_track_t) { .payload = value, .from = tile_rect(from_row, from_col, tile_size), .to = tile_rect(to_row, to_col, tile_size) }; } /* * Process one row or column in movement order. Update the board and record * pixel-space animation tracks for every original tile. */ static bool process_line(dir_e direction, int line, uint32_t output[SIZE]) { line_tile_t input[SIZE]; int input_count = 0; int output_count = 0; for (int p = 0; p < SIZE; ++p) { int row, col; line_coordinate(direction, line, p, &row, &col); uint32_t value = game.area[row][col]; if (value != 0) { input[input_count++] = (line_tile_t) { .value = value, .row = row, .col = col }; } } memset(output, 0, sizeof(uint32_t) * SIZE); for (int i = 0; i < input_count; ) { int dest_row, dest_col; line_coordinate(direction, line, output_count, &dest_row, &dest_col); if (i + 1 < input_count && input[i].value == input[i + 1].value) { uint32_t merged_value = input[i].value * 2; if (merged_value >= 2048) game_status = GAME_WON; output[output_count] = merged_value; game.score += merged_value; /* Both source tiles travel to the same merge destination. */ add_move_track(input[i].value, input[i].row, input[i].col, dest_row, dest_col); add_move_track(input[i + 1].value, input[i + 1].row, input[i + 1].col, dest_row, dest_col); i += 2; } else { output[output_count] = input[i].value; add_move_track(input[i].value, input[i].row, input[i].col, dest_row, dest_col); ++i; } ++output_count; } bool changed = false; for (int p = 0; p < SIZE; ++p) { int row, col; line_coordinate(direction, line, p, &row, &col); if (game.area[row][col] != output[p]) changed = true; game.area[row][col] = output[p]; } return changed; } static bool slide(dir_e direction) { if (game_status != GAME_PLAYING || anim_is_active(&movement)) return false; if (anim_is_active(&movement)) return false; game.direction = direction; move_track_count = 0; bool changed = false; for (int line = 0; line < SIZE; ++line) { uint32_t output[SIZE]; if (process_line(direction, line, output)) changed = true; } if (!changed) { move_track_count = 0; return false; } return anim_start(&movement, move_tracks, move_track_count, MOVE_DURATION); } static void new_2(void) { int empty_rows[SIZE * SIZE]; int empty_cols[SIZE * SIZE]; int count = 0; for (int r = 0; r < SIZE; ++r) { for (int c = 0; c < SIZE; ++c) { if (game.area[r][c] == 0) { empty_rows[count] = r; empty_cols[count] = c; ++count; } } } if (count == 0) return; int pick = rand() % count; game.area[empty_rows[pick]][empty_cols[pick]] = 2; } /* Polling events every frame. */ void client_poll_event(void) { SDL_Event ev = game_state->event; if (ev.type == SDL_EVENT_QUIT) { game_state->is_running = false; return; } if (ev.type != SDL_EVENT_KEY_DOWN) return; switch (ev.key.key) { case SDLK_Q: game_state->is_running = false; break; case SDLK_W: case SDLK_UP: if (game_status == GAME_PLAYING) slide(UP); break; case SDLK_S: case SDLK_DOWN: if (game_status == GAME_PLAYING) slide(DOWN); break; case SDLK_A: case SDLK_LEFT: if (game_status == GAME_PLAYING) slide(LEFT); break; case SDLK_D: case SDLK_RIGHT: if (game_status == GAME_PLAYING) slide(RIGHT); break; default: break; } } static double monotonic_seconds(void) { struct timespec ts; timespec_get(&ts, TIME_UTC); return (double)ts.tv_sec + (double)ts.tv_nsec / 1e9; } static void draw_tile_rect(uint32_t value, float x, float y, float w, float h) { uint32_t ix = (uint32_t)x; uint32_t iy = (uint32_t)y; uint32_t iw = (uint32_t)w; uint32_t ih = (uint32_t)h; uint32_t color = colors(value); put_rect(OVERDRAW, ix, iy, iw, ih, color); if (value != 0) { char text[16]; snprintf(text, sizeof text, "%u", value); put_string(OVERDRAW, text, ix + BORDER_WIDTH, iy + BORDER_WIDTH, 3, 1, 12, 0x000000, color); } } static void draw_tile(uint32_t value, int row, int col, uint32_t tile_size) { anim_rect_t rect = tile_rect(row, col, tile_size); draw_tile_rect(value, rect.x, rect.y, rect.w, rect.h); } void client_update_framebuffer(void) { double now = monotonic_seconds(); float dt = last_frame_time == 0.0 ? 0.0f : (float)(now - last_frame_time); last_frame_time = now; if (dt > 0.05f) dt = 0.05f; if (anim_is_active(&movement) && anim_update(&movement, dt)) { if (game_status == GAME_PLAYING) { new_2(); if (board_is_full()) game_status = GAME_LOST; } } uint32_t tile_size = game_state->width / SIZE; uint32_t border_width = BORDER_WIDTH; clear_framebuffer(OVERDRAW, 0x000000); char score_text[32]; snprintf(score_text, sizeof score_text, "score: %u", game.score); put_string(OVERDRAW, score_text, border_width / 2, tile_size * SIZE + border_width / 2, 6, 1, 12, 0xFFFFFF, 0x000000); if (game_status != GAME_PLAYING) { clear_framebuffer(BASE, 0x000000); const char *message = game_status == GAME_WON ? "YOU WIN!" : "YOU LOSE!"; put_string(OVERDRAW, message, 0 + border_width * 2, game_state->height / 2, 6, 1, 12, 0xFFFFFF, 0x000000); return; } /* Draw all cell backgrounds. */ for (int r = 0; r < SIZE; ++r) { for (int c = 0; c < SIZE; ++c) { uint32_t x = border_width / 2 + (uint32_t)c * tile_size; uint32_t y = border_width / 2 + (uint32_t)r * tile_size; uint32_t side = tile_size - border_width; put_rect(BASE, x, y, side, side, 0xFFFFFF); put_rect(OVERDRAW, x, y, side, side, 0xFFFFFF); } } if (anim_is_active(&movement)) { const anim_item_t *items = anim_items(&movement); size_t count = anim_count(&movement); for (size_t i = 0; i < count; ++i) { draw_tile_rect(items[i].payload, items[i].rect.x, items[i].rect.y, items[i].rect.w, items[i].rect.h); } } else { for (int r = 0; r < SIZE; ++r) { for (int c = 0; c < SIZE; ++c) { draw_tile(game.area[r][c], r, c, tile_size); } } } } static void game_init(void) { game_status = GAME_PLAYING; memset(&game, 0, sizeof game); anim_init(&movement); srand((unsigned)time(NULL)); /* Start with two 2-tiles. */ new_2(); new_2(); } int main(void) { uint32_t window_width = 500; uint32_t window_height = (uint32_t)(window_width * 1.12f); game_init(); if (engine_init(window_width, window_height, "Dangerous 2048", client_poll_event, client_update_framebuffer)) { engine_start(); engine_stop(); return 0; } return 1; }