summaryrefslogtreecommitdiff
path: root/src/game.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/game.c')
-rw-r--r--src/game.c592
1 files changed, 592 insertions, 0 deletions
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 <SDL3/SDL.h>
+#include <SDL3_image/SDL_image.h>
+#include <stdlib.h>
+#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);
+ }
+}