From be5479e73c1c338cd369ddd99d19563c7349cda6 Mon Sep 17 00:00:00 2001 From: Menase Teshale Date: Tue, 18 Aug 2026 22:21:29 +0300 Subject: Initial Commit --- .gitmodules | 6 + CMakeLists.txt | 25 ++ assets/Full-background.png | Bin 0 -> 64128 bytes assets/match3_sheet.png | Bin 0 -> 20377 bytes src/app.h | 20 ++ src/game.c | 592 +++++++++++++++++++++++++++++++++++++++++++++ src/main.c | 92 +++++++ vendored/SDL | 1 + vendored/SDL_image | 1 + 9 files changed, 737 insertions(+) create mode 100644 .gitmodules create mode 100644 CMakeLists.txt create mode 100644 assets/Full-background.png create mode 100644 assets/match3_sheet.png create mode 100644 src/app.h create mode 100644 src/game.c create mode 100644 src/main.c create mode 160000 vendored/SDL create mode 160000 vendored/SDL_image diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..ce7f8c5 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "vendored/SDL"] + path = vendored/SDL + url = https://github.com/libsdl-org/SDL.git +[submodule "vendored/SDL_image"] + path = vendored/SDL_image + url = https://github.com/libsdl-org/SDL_image.git diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..e16158b --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,25 @@ +cmake_minimum_required(VERSION 3.15) +project(Match3 LANGUAGES C) + +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED ON) + +add_subdirectory(vendored/SDL EXCLUDE_FROM_ALL) +add_subdirectory(vendored/SDL_image EXCLUDE_FROM_ALL) + +add_executable(game + "src/app.h" + "src/main.c" + "src/game.c" +) + +target_link_libraries(game PRIVATE + SDL3::SDL3 + SDL3::Headers + SDL3_image::SDL3_image + ) +if(EMSCRIPTEN) + set(CMAKE_EXECUTABLE_SUFFIX ".js" CACHE INTERNAL "") + target_link_options(game PRIVATE -sINITIAL_MEMORY=64MB) + target_link_options(game PRIVATE --preload-file ../assets) +endif() diff --git a/assets/Full-background.png b/assets/Full-background.png new file mode 100644 index 0000000..a59ef2b Binary files /dev/null and b/assets/Full-background.png differ diff --git a/assets/match3_sheet.png b/assets/match3_sheet.png new file mode 100644 index 0000000..001f436 Binary files /dev/null and b/assets/match3_sheet.png differ diff --git a/src/app.h b/src/app.h new file mode 100644 index 0000000..acbd2dd --- /dev/null +++ b/src/app.h @@ -0,0 +1,20 @@ +#ifndef APP_H_ +#define APP_H_ + +#include + +typedef enum { + menu_state, + game_state +} application_state; + +void init_app(SDL_Renderer* renderer); +void free_app(); + +void init_game(SDL_Renderer* renderer); +void free_game(); +void update_game(float dt); +void draw_game(SDL_Renderer* renderer); +void game_event(SDL_Event* e); + +#endif // APP_H_ diff --git a/src/game.c b/src/game.c new file mode 100644 index 0000000..9c4d22e --- /dev/null +++ b/src/game.c @@ -0,0 +1,592 @@ +#include +#include +#include +#include "app.h" + +#define MIN_TILES_TO_CLEAR 3 //More than this number of tiles per row or column will trigger clear + +#define MAX_BOARD_WIDTH 10 +#define MAX_BOARD_HEIGHT 10 +#define MAX_PIECES 300 + +#define PIECE_SOURCE_YOFFSET 4 +#define PIECE_SOURCE_WIDTH 32 +#define PIECE_SOURCE_HEIGHT 32 +#define PIECE_DEST_WIDTH 50 +#define PIECE_DEST_HEIGHT 50 +#define PIECE_LINEAR_SPEED 1.5 +#define PIECE_SWAP_SPEED 3 +#define PIECE_MAX_ROTATION 40 +#define PIECE_ROTATION_SPEED 200 +#define GRAVITY 2000.0 +#define PIECE_MAX_FALL_XSPEED 200 +#define PIECE_FALL_YSPEED_INITIAL -15 +#define DELETE_PIECE_OFFSET 50 + +#define BOARD_WIDTH 8 +#define BOARD_HEIGHT 6 + +#define BOARD_XOFFSET 200 +#define BOARD_YOFFSET 150 +#define BOARD_PADDING 4 + +#define BACKGROUND_SCROLL_SPEED 40 + +typedef enum { + linear_move_state, + fall_state, + static_state +} piece_state; + +typedef struct { + int piece_id; + piece_state state; + + SDL_FPoint position; + float orientation; + + //Data for linear movement + SDL_FPoint start_position; + SDL_FPoint dest_position; + float travel_speed; + float fraction_travelled; + + //Data for falling animation + float xvel, yvel; + + int piece_color; + int piece_face; +} piece; + +typedef enum { + tile_filled, + tile_currently_empty, + tile_always_empty +} tile_state; + +typedef struct { + tile_state state; + int piece_id; //Only if tile_state == tile_filled +} tile; + +typedef enum { + intro_state, + neutral_state, + piece_held_state +} game_scene_state; + +typedef struct { + bool inited; + SDL_Texture* spritesheet; + game_scene_state state; + + piece pieces[MAX_PIECES]; + int num_pieces; + + tile board[MAX_BOARD_HEIGHT][MAX_BOARD_WIDTH]; + int board_width; + int board_height; + + int selected_piece_id; + + SDL_Texture* background_texture; + float background_offset; + + float background_scaled_width; + float background_scaled_height; +} s_game; + +extern SDL_Window* window; + +s_game game = {0}; + +extern SDL_Window* window; + +static inline float random_float_0to1() { + return (float)rand() / RAND_MAX; +} + +static inline bool filled_tile_at(int y, int x) { + if (x < 0 || x >= game.board_width || y < 0 || y >= game.board_height) + return false; + + return game.board[y][x].state == tile_filled; +} + +SDL_FPoint board_position(int y, int x) { + return (SDL_FPoint) { + .x = (PIECE_DEST_WIDTH + BOARD_PADDING) * x + BOARD_XOFFSET, + .y = (PIECE_DEST_HEIGHT + BOARD_PADDING) * y + BOARD_YOFFSET + }; +} + +void piece_start_moving_to(piece* piece, SDL_FPoint dest_position, float speed) { + piece->start_position = piece->position; + piece->dest_position = dest_position; + piece->travel_speed = speed; + piece->state = linear_move_state; + piece->fraction_travelled = 0; +} + +int add_piece(piece piece) { + static int last_piece_id = 0; + + if (game.num_pieces + 1 >= MAX_PIECES) { + return -1; + } + + piece.piece_id = last_piece_id++; + game.pieces[game.num_pieces++] = piece; + + return piece.piece_id; +} + +piece* retreive_piece(int piece_id) { + for (int i = 0; i < game.num_pieces; i++) { + if (game.pieces[i].piece_id == piece_id) { + return &game.pieces[i]; + } + } + + return NULL; +} + +void delete_piece(int piece_id) { + piece* to_be_deleted = retreive_piece(piece_id); + + if (to_be_deleted == NULL) { + return; + } + + SDL_memcpy(to_be_deleted, &game.pieces[game.num_pieces - 1], sizeof(piece)); + game.num_pieces--; +} + +float rand_float() { + return rand() / (float)RAND_MAX; +} + +void add_piece_at(int y, int x) { + int new_piece_id = add_piece((piece) { + .position = { + .x = (800 - PIECE_DEST_WIDTH) * rand_float(), + .y = (600 - PIECE_DEST_HEIGHT) * rand_float() + }, + .orientation = 0, + .piece_color = rand() % 9, + .piece_face = rand() % 6 + }); + + if (new_piece_id == -1) { + return; + } + + float speed_diff = PIECE_LINEAR_SPEED * rand_float() - PIECE_LINEAR_SPEED / 2.0; + + piece* new_piece = retreive_piece(new_piece_id); + piece_start_moving_to(new_piece, board_position(y, x), PIECE_LINEAR_SPEED + speed_diff); + + game.board[y][x] = (tile) { + .state = tile_filled, + .piece_id = new_piece_id + }; +} + +void fill_board() { + for (int i = 0; i < game.board_height; i++) { + for (int j = 0; j < game.board_width; j++) { + add_piece_at(i, j); + } + } +} + +void clear_board() { + game.num_pieces = 0; +} + +void init_game(SDL_Renderer* renderer) { + SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255); + + if (game.spritesheet == NULL) { + game.spritesheet = IMG_LoadTexture(renderer, "assets/match3_sheet.png"); + SDL_SetTextureScaleMode(game.spritesheet, SDL_SCALEMODE_NEAREST); + } + + if (game.background_texture == NULL) { + game.background_texture = IMG_LoadTexture(renderer, "assets/Full-background.png"); + /* SDL_SetTextureScaleMode(game.background_texture, SDL_SCALEMODE_NEAREST); */ + + int window_height; + float texture_width, texture_height; + SDL_GetWindowSizeInPixels(window, NULL, &window_height); + SDL_GetTextureSize(game.background_texture, &texture_width, &texture_height); + + float scale = window_height / texture_height; + game.background_scaled_width = scale * texture_width; + game.background_scaled_height = scale * texture_height; + } + + game.board_width = BOARD_WIDTH; + game.board_height = BOARD_HEIGHT; + + game.state = intro_state; + game.selected_piece_id = -1; + + fill_board(); +} + +void free_game() { + SDL_DestroyTexture(game.spritesheet); + SDL_DestroyTexture(game.background_texture); +} + +SDL_FPoint lerp_points(SDL_FPoint a, SDL_FPoint b, float fraction) { + SDL_FPoint target; + + target.x = fraction * b.x + (1 - fraction) * a.x; + target.y = fraction * b.y + (1 - fraction) * a.y; + + return target; +} + +void piece_start_falling(piece* piece) { + float xvel_multiplier = 2*random_float_0to1() - 1; + + piece->xvel = xvel_multiplier * PIECE_MAX_FALL_XSPEED; + piece->yvel = PIECE_FALL_YSPEED_INITIAL; + piece->state = fall_state; +} + +void update_piece(piece* piece, float dt) { + switch (piece->state) { + + case linear_move_state: + piece->fraction_travelled += dt * piece->travel_speed; + + if (piece->fraction_travelled >= 1) { + piece->fraction_travelled = 1; + piece->position = piece->dest_position; + piece->state = static_state; + } else { + piece->position = + lerp_points(piece->start_position, piece->dest_position, piece->fraction_travelled); + } + break; + + case fall_state: + piece->position.x += piece->xvel * dt; + piece->yvel += GRAVITY * dt / 2; + piece->position.y += piece->yvel * dt; + piece->yvel += GRAVITY * dt / 2; + + break; + + default: + break; + } +} + +int mouse_on_piece() { + SDL_FPoint mouse_position; + SDL_GetMouseState(&mouse_position.x, &mouse_position.y); + + for (int i = 0; i < game.num_pieces; i++) { + const piece* piece = &game.pieces[i]; + + SDL_FRect piece_box = (SDL_FRect){ + .x = piece->position.x, + .y = piece->position.y, + .w = PIECE_DEST_WIDTH, + .h = PIECE_DEST_HEIGHT + }; + + if (SDL_PointInRectFloat(&mouse_position, &piece_box)) { + for (int i = 0; i < game.board_height; i++) { + for (int j = 0; j < game.board_width; j++) { + if (game.board[i][j].state == tile_filled && game.board[i][j].piece_id == piece->piece_id) + return piece->piece_id; + } + } + } + } + + return -1; +} + +void update_intro_state() { + for (int i = 0; i < game.num_pieces; i++) { + if (game.pieces[i].state != static_state && game.pieces[i].state != fall_state) { + return; + } + } + + game.state = neutral_state; +} + +void handle_piece_rotation(float dt) { + int selected_piece_id = mouse_on_piece(); + + for (int i = 0; i < game.num_pieces; i++) { + piece* piece = &game.pieces[i]; + + if (piece->piece_id == selected_piece_id) { + piece->orientation = + SDL_min(piece->orientation + PIECE_ROTATION_SPEED * dt, + PIECE_MAX_ROTATION); + } else { + piece->orientation = + SDL_max(piece->orientation - PIECE_ROTATION_SPEED * dt, + 0); + } + } +} + +void scroll_background(float dt) { + game.background_offset -= BACKGROUND_SCROLL_SPEED * dt; + + while (game.background_offset < -game.background_scaled_width) { + game.background_offset += game.background_scaled_width; + } +} + +void update_game(float dt) { + int window_width, window_height; + SDL_GetWindowSizeInPixels(window, &window_width, &window_height); + + for (int i = 0; i < game.num_pieces; i++) { + update_piece(&game.pieces[i], dt); + + if (game.pieces[i].position.y > window_height + DELETE_PIECE_OFFSET) { + delete_piece(game.pieces[i].piece_id); + } + } + + handle_piece_rotation(dt); + + scroll_background(dt); + + if (game.state == intro_state) { + update_intro_state(); + } +} + +void draw_piece(SDL_Renderer* renderer, const piece* piece) { + const int piece_source_xoffs[] = {4, 209}; + + SDL_RenderTextureRotated(renderer, + game.spritesheet, + &(SDL_FRect){ + .x = piece_source_xoffs[piece->piece_color / 9] + piece->piece_face * (PIECE_SOURCE_WIDTH + 1), + .y = PIECE_SOURCE_YOFFSET + (piece->piece_color % 9) * (PIECE_SOURCE_HEIGHT + 1), + .w = PIECE_SOURCE_WIDTH, + .h = PIECE_SOURCE_HEIGHT + }, + &(SDL_FRect){ + .x = piece->position.x, + .y = piece->position.y, + .w = PIECE_DEST_WIDTH, + .h = PIECE_DEST_HEIGHT + }, + piece->orientation, + NULL, + SDL_FLIP_NONE + ); +} + +void draw_background(SDL_Renderer* renderer) { + int x_offset = game.background_offset; + + while (x_offset <= 800) { + SDL_RenderTexture(renderer, + game.background_texture, + NULL, + &(SDL_FRect){ + .x = x_offset, + .y = 0, + .w = game.background_scaled_width, + .h = game.background_scaled_height + }); + + x_offset += game.background_scaled_width; + } +} + +void draw_game(SDL_Renderer* renderer) { + SDL_RenderClear(renderer); + + draw_background(renderer); + + piece* selected_piece = NULL; + for (int i = 0; i < game.num_pieces; i++) { + if (game.pieces[i].piece_id == game.selected_piece_id) { + selected_piece = &game.pieces[i]; + continue; + } + draw_piece(renderer, &game.pieces[i]); + } + + //Gives it the apearance that you are sliding your tiles over + if (selected_piece) { + draw_piece(renderer, selected_piece); + } + + SDL_RenderPresent(renderer); +} + +void neutral_state_event(SDL_Event* e) { + if (e->type == SDL_EVENT_MOUSE_BUTTON_DOWN && e->button.button == SDL_BUTTON_LEFT) { + int selected_piece_id = mouse_on_piece(); + + if (selected_piece_id >= 0) { + game.selected_piece_id = selected_piece_id; + game.state = piece_held_state; + } + } +} + +void swap_pieces_at(int x1, int y1, int x2 , int y2) { + int id1 = game.board[y1][x1].piece_id; + int id2 = game.board[y2][x2].piece_id; + + piece* selected = retreive_piece(id1); + piece* swap = retreive_piece(id2); + + if (selected && swap) { + piece_start_moving_to(selected, board_position(y2, x2), PIECE_SWAP_SPEED); + piece_start_moving_to(swap, board_position(y1, x1), PIECE_SWAP_SPEED); + + game.board[y2][x2].piece_id = id1; + game.board[y1][x1].piece_id = id2; + } +} + +void find_last_color_matching_tile_in_direction(int* sy, int* sx, int dy, int dx) { + if (!filled_tile_at(*sy, *sx)) + return; + + piece* start_piece = retreive_piece(game.board[*sy][*sx].piece_id); + + for(;;) { + int ny = *sy + dy, nx = *sx + dx; + + if (!filled_tile_at(ny, nx)) + return; + + piece* next_piece = retreive_piece(game.board[ny][nx].piece_id); + + if (!next_piece || next_piece->piece_color != start_piece->piece_color) + return; + + *sy = ny; *sx = nx; + } +} + +bool try_clear_at(int y, int x) { + const int axes[2][2] = { + {1, 0}, //Horizontal + {0, 1} //Vertical + }; + + for (int a = 0; a < 2; a++) { + int sy = y, sx = x, dy = axes[a][1], dx = axes[a][0]; + find_last_color_matching_tile_in_direction(&sy, &sx, dy, dx); + + int ey = sy, ex = sx; + find_last_color_matching_tile_in_direction(&ey, &ex, -dy, -dx); + + if (SDL_abs(ey-sy) + SDL_abs(ex-sx) + 1 < MIN_TILES_TO_CLEAR) + continue; + + for (;;) { + piece_start_falling(retreive_piece(game.board[ey][ex].piece_id)); + game.board[ey][ex].state = tile_currently_empty; + + if (ey == sy && ex == sx) + break; + + ey += dy; ex += dx; + } + + return true; + } + + return false; +} + +void cascade_pieces() { + for (int j = 0; j < game.board_width; j++) { + for (int i = 0; i < game.board_height; i++) { + if (game.board[i][j].state != tile_currently_empty) + continue; + + //Look for the first filled tile above and start moving it down + for (int c = i-1; c >= 0; c--) { + if (game.board[c][j].state == tile_filled) { + piece* piece = retreive_piece(game.board[c][j].piece_id); + piece_start_moving_to(piece, + board_position(c+1, j), + PIECE_LINEAR_SPEED + ); + game.board[c+1][j] = game.board[c][j]; + } + } + + add_piece_at(0, j); + } + } +} + +void piece_held_event(SDL_Event* e) { + if (e->type == SDL_EVENT_MOUSE_BUTTON_UP && e->button.button == SDL_BUTTON_LEFT) { + int swap_piece_id = mouse_on_piece(); + int selected_piece_id = game.selected_piece_id; + + if (swap_piece_id >= 0) { + int selected_x, selected_y; + int swap_x, swap_y; + + for (int i = 0; i < game.board_height; i++) { + for (int j = 0; j < game.board_width; j++) { + if (game.board[i][j].state != tile_filled) + continue; + + if (game.board[i][j].piece_id == selected_piece_id) { + selected_x = j; + selected_y = i; + } if (game.board[i][j].piece_id == swap_piece_id) { + swap_x = j; + swap_y = i; + } + + } + } + + if (SDL_abs(selected_x-swap_x) + SDL_abs(selected_y-swap_y) != 1 || + retreive_piece(selected_piece_id)->piece_color == retreive_piece(swap_piece_id)->piece_color) { + game.state = neutral_state; + return; + } + + swap_pieces_at(selected_x, selected_y, swap_x, swap_y); + + bool tiles_cleared = try_clear_at(selected_y, selected_x); + tiles_cleared = try_clear_at(swap_y, swap_x) || tiles_cleared; //sneaky short circuit glitch + + if (tiles_cleared) { + cascade_pieces(); + game.state = intro_state; + return; + } + } + + game.state = neutral_state; + } +} + +void game_event(SDL_Event* e) { + if (game.state == neutral_state) { + neutral_state_event(e); + } else if (game.state == piece_held_state) { + piece_held_event(e); + } +} diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..726e781 --- /dev/null +++ b/src/main.c @@ -0,0 +1,92 @@ +#include +#include +#include +#include +#define SDL_MAIN_USE_CALLBACKS +#include + +#include "app.h" + +#define WINDOW_WIDTH 800 +#define WINDOW_HEIGHT 600 +#define WINDOW_TITLE "Match 3" +#define MAX_DELTATIME (1000.0 / 30) + +SDL_Window* window; +SDL_Renderer* renderer; + +application_state app_state; + +SDL_AppResult SDL_AppInit(void**, int, char**) { + SDL_Init(SDL_INIT_VIDEO); + srand(time(NULL)); + + window = SDL_CreateWindow(WINDOW_TITLE, WINDOW_WIDTH, WINDOW_HEIGHT, 0); + renderer = SDL_CreateRenderer(window, NULL); +#ifndef SDL_PLATFORM_EMSCRIPTEN + SDL_SetRenderVSync(renderer, true); +#endif + + init_app(renderer); + + return SDL_APP_CONTINUE; +} + +SDL_AppResult SDL_AppEvent(void*, SDL_Event* e) { + if (e->type == SDL_EVENT_QUIT) { + return SDL_APP_SUCCESS; + } + + switch (app_state) { + case game_state: + game_event(e); + break; + + default: + break; + } + + return SDL_APP_CONTINUE; +} + +SDL_AppResult SDL_AppIterate(void*) { + static uint64_t then = 0, now = 0; + + then = now; + now = SDL_GetTicks(); + + float dt = (now - then) / 1000.0f; + + switch (app_state) { + + case game_state: + update_game(SDL_min(dt, MAX_DELTATIME)); + draw_game(renderer); + break; + + default: + break; + + } + + return SDL_APP_CONTINUE; +} + +void SDL_AppQuit(void*, SDL_AppResult) { + SDL_DestroyWindow(window); + SDL_DestroyRenderer(renderer); + + free_app(); + + SDL_Quit(); +} + +void init_app(SDL_Renderer* renderer) { + init_game(renderer); + + app_state = game_state; +} + +void free_app() { + free_game(); +} diff --git a/vendored/SDL b/vendored/SDL new file mode 160000 index 0000000..77cd387 --- /dev/null +++ b/vendored/SDL @@ -0,0 +1 @@ +Subproject commit 77cd3872c42d71c59c85ccaa08994cb5e0c9eecc diff --git a/vendored/SDL_image b/vendored/SDL_image new file mode 160000 index 0000000..fcb9d0b --- /dev/null +++ b/vendored/SDL_image @@ -0,0 +1 @@ +Subproject commit fcb9d0b15f6bc4e22e9badedc5cdccee92eddcf4 -- cgit v1.2.3