mirror of
				https://github.com/libsdl-org/SDL.git
				synced 2025-10-26 12:27:44 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			351 lines
		
	
	
		
			9.8 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
			
		
		
	
	
			351 lines
		
	
	
		
			9.8 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
| /*
 | |
|  * Logic implementation of the Snake game. It is designed to efficiently
 | |
|  * represent the state of the game in memory.
 | |
|  *
 | |
|  * This code is public domain. Feel free to use it for any purpose!
 | |
|  */
 | |
| 
 | |
| #define SDL_MAIN_USE_CALLBACKS 1 /* use the callbacks instead of main() */
 | |
| #include <SDL3/SDL.h>
 | |
| #include <SDL3/SDL_main.h>
 | |
| 
 | |
| #define STEP_RATE_IN_MILLISECONDS  125
 | |
| #define SNAKE_BLOCK_SIZE_IN_PIXELS 24
 | |
| #define SDL_WINDOW_WIDTH           (SNAKE_BLOCK_SIZE_IN_PIXELS * SNAKE_GAME_WIDTH)
 | |
| #define SDL_WINDOW_HEIGHT          (SNAKE_BLOCK_SIZE_IN_PIXELS * SNAKE_GAME_HEIGHT)
 | |
| 
 | |
| #define SNAKE_GAME_WIDTH  24U
 | |
| #define SNAKE_GAME_HEIGHT 18U
 | |
| #define SNAKE_MATRIX_SIZE (SNAKE_GAME_WIDTH * SNAKE_GAME_HEIGHT)
 | |
| 
 | |
| #define THREE_BITS  0x7U /* ~CHAR_MAX >> (CHAR_BIT - SNAKE_CELL_MAX_BITS) */
 | |
| #define SHIFT(x, y) (((x) + ((y) * SNAKE_GAME_WIDTH)) * SNAKE_CELL_MAX_BITS)
 | |
| 
 | |
| typedef enum
 | |
| {
 | |
|     SNAKE_CELL_NOTHING = 0U,
 | |
|     SNAKE_CELL_SRIGHT = 1U,
 | |
|     SNAKE_CELL_SUP = 2U,
 | |
|     SNAKE_CELL_SLEFT = 3U,
 | |
|     SNAKE_CELL_SDOWN = 4U,
 | |
|     SNAKE_CELL_FOOD = 5U
 | |
| } SnakeCell;
 | |
| 
 | |
| #define SNAKE_CELL_MAX_BITS 3U /* floor(log2(SNAKE_CELL_FOOD)) + 1 */
 | |
| 
 | |
| typedef enum
 | |
| {
 | |
|     SNAKE_DIR_RIGHT,
 | |
|     SNAKE_DIR_UP,
 | |
|     SNAKE_DIR_LEFT,
 | |
|     SNAKE_DIR_DOWN
 | |
| } SnakeDirection;
 | |
| 
 | |
| typedef struct
 | |
| {
 | |
|     unsigned char cells[(SNAKE_MATRIX_SIZE * SNAKE_CELL_MAX_BITS) / 8U];
 | |
|     char head_xpos;
 | |
|     char head_ypos;
 | |
|     char tail_xpos;
 | |
|     char tail_ypos;
 | |
|     char next_dir;
 | |
|     char inhibit_tail_step;
 | |
|     unsigned occupied_cells;
 | |
| } SnakeContext;
 | |
| 
 | |
| typedef struct
 | |
| {
 | |
|     SDL_Window *window;
 | |
|     SDL_Renderer *renderer;
 | |
|     SnakeContext snake_ctx;
 | |
|     Uint64 last_step;
 | |
| } AppState;
 | |
| 
 | |
| SnakeCell snake_cell_at(const SnakeContext *ctx, char x, char y)
 | |
| {
 | |
|     const int shift = SHIFT(x, y);
 | |
|     unsigned short range;
 | |
|     SDL_memcpy(&range, ctx->cells + (shift / 8), sizeof(range));
 | |
|     return (SnakeCell)((range >> (shift % 8)) & THREE_BITS);
 | |
| }
 | |
| 
 | |
| static void set_rect_xy_(SDL_FRect *r, short x, short y)
 | |
| {
 | |
|     r->x = (float)(x * SNAKE_BLOCK_SIZE_IN_PIXELS);
 | |
|     r->y = (float)(y * SNAKE_BLOCK_SIZE_IN_PIXELS);
 | |
| }
 | |
| 
 | |
| static void put_cell_at_(SnakeContext *ctx, char x, char y, SnakeCell ct)
 | |
| {
 | |
|     const int shift = SHIFT(x, y);
 | |
|     const int adjust = shift % 8;
 | |
|     unsigned char *const pos = ctx->cells + (shift / 8);
 | |
|     unsigned short range;
 | |
|     SDL_memcpy(&range, pos, sizeof(range));
 | |
|     range &= ~(THREE_BITS << adjust); /* clear bits */
 | |
|     range |= (ct & THREE_BITS) << adjust;
 | |
|     SDL_memcpy(pos, &range, sizeof(range));
 | |
| }
 | |
| 
 | |
| static int are_cells_full_(SnakeContext *ctx)
 | |
| {
 | |
|     return ctx->occupied_cells == SNAKE_GAME_WIDTH * SNAKE_GAME_HEIGHT;
 | |
| }
 | |
| 
 | |
| static void new_food_pos_(SnakeContext *ctx)
 | |
| {
 | |
|     while (true) {
 | |
|         const char x = (char) SDL_rand(SNAKE_GAME_WIDTH);
 | |
|         const char y = (char) SDL_rand(SNAKE_GAME_HEIGHT);
 | |
|         if (snake_cell_at(ctx, x, y) == SNAKE_CELL_NOTHING) {
 | |
|             put_cell_at_(ctx, x, y, SNAKE_CELL_FOOD);
 | |
|             break;
 | |
|         }
 | |
|     }
 | |
| }
 | |
| 
 | |
| void snake_initialize(SnakeContext *ctx)
 | |
| {
 | |
|     int i;
 | |
|     SDL_zeroa(ctx->cells);
 | |
|     ctx->head_xpos = ctx->tail_xpos = SNAKE_GAME_WIDTH / 2;
 | |
|     ctx->head_ypos = ctx->tail_ypos = SNAKE_GAME_HEIGHT / 2;
 | |
|     ctx->next_dir = SNAKE_DIR_RIGHT;
 | |
|     ctx->inhibit_tail_step = ctx->occupied_cells = 4;
 | |
|     --ctx->occupied_cells;
 | |
|     put_cell_at_(ctx, ctx->tail_xpos, ctx->tail_ypos, SNAKE_CELL_SRIGHT);
 | |
|     for (i = 0; i < 4; i++) {
 | |
|         new_food_pos_(ctx);
 | |
|         ++ctx->occupied_cells;
 | |
|     }
 | |
| }
 | |
| 
 | |
| void snake_redir(SnakeContext *ctx, SnakeDirection dir)
 | |
| {
 | |
|     SnakeCell ct = snake_cell_at(ctx, ctx->head_xpos, ctx->head_ypos);
 | |
|     if ((dir == SNAKE_DIR_RIGHT && ct != SNAKE_CELL_SLEFT) ||
 | |
|         (dir == SNAKE_DIR_UP && ct != SNAKE_CELL_SDOWN) ||
 | |
|         (dir == SNAKE_DIR_LEFT && ct != SNAKE_CELL_SRIGHT) ||
 | |
|         (dir == SNAKE_DIR_DOWN && ct != SNAKE_CELL_SUP)) {
 | |
|         ctx->next_dir = dir;
 | |
|     }
 | |
| }
 | |
| 
 | |
| static void wrap_around_(char *val, char max)
 | |
| {
 | |
|     if (*val < 0) {
 | |
|         *val = max - 1;
 | |
|     } else if (*val > max - 1) {
 | |
|         *val = 0;
 | |
|     }
 | |
| }
 | |
| 
 | |
| void snake_step(SnakeContext *ctx)
 | |
| {
 | |
|     const SnakeCell dir_as_cell = (SnakeCell)(ctx->next_dir + 1);
 | |
|     SnakeCell ct;
 | |
|     char prev_xpos;
 | |
|     char prev_ypos;
 | |
|     /* Move tail forward */
 | |
|     if (--ctx->inhibit_tail_step == 0) {
 | |
|         ++ctx->inhibit_tail_step;
 | |
|         ct = snake_cell_at(ctx, ctx->tail_xpos, ctx->tail_ypos);
 | |
|         put_cell_at_(ctx, ctx->tail_xpos, ctx->tail_ypos, SNAKE_CELL_NOTHING);
 | |
|         switch (ct) {
 | |
|         case SNAKE_CELL_SRIGHT:
 | |
|             ctx->tail_xpos++;
 | |
|             break;
 | |
|         case SNAKE_CELL_SUP:
 | |
|             ctx->tail_ypos--;
 | |
|             break;
 | |
|         case SNAKE_CELL_SLEFT:
 | |
|             ctx->tail_xpos--;
 | |
|             break;
 | |
|         case SNAKE_CELL_SDOWN:
 | |
|             ctx->tail_ypos++;
 | |
|             break;
 | |
|         default:
 | |
|             break;
 | |
|         }
 | |
|         wrap_around_(&ctx->tail_xpos, SNAKE_GAME_WIDTH);
 | |
|         wrap_around_(&ctx->tail_ypos, SNAKE_GAME_HEIGHT);
 | |
|     }
 | |
|     /* Move head forward */
 | |
|     prev_xpos = ctx->head_xpos;
 | |
|     prev_ypos = ctx->head_ypos;
 | |
|     switch (ctx->next_dir) {
 | |
|     case SNAKE_DIR_RIGHT:
 | |
|         ++ctx->head_xpos;
 | |
|         break;
 | |
|     case SNAKE_DIR_UP:
 | |
|         --ctx->head_ypos;
 | |
|         break;
 | |
|     case SNAKE_DIR_LEFT:
 | |
|         --ctx->head_xpos;
 | |
|         break;
 | |
|     case SNAKE_DIR_DOWN:
 | |
|         ++ctx->head_ypos;
 | |
|         break;
 | |
|     }
 | |
|     wrap_around_(&ctx->head_xpos, SNAKE_GAME_WIDTH);
 | |
|     wrap_around_(&ctx->head_ypos, SNAKE_GAME_HEIGHT);
 | |
|     /* Collisions */
 | |
|     ct = snake_cell_at(ctx, ctx->head_xpos, ctx->head_ypos);
 | |
|     if (ct != SNAKE_CELL_NOTHING && ct != SNAKE_CELL_FOOD) {
 | |
|         snake_initialize(ctx);
 | |
|         return;
 | |
|     }
 | |
|     put_cell_at_(ctx, prev_xpos, prev_ypos, dir_as_cell);
 | |
|     put_cell_at_(ctx, ctx->head_xpos, ctx->head_ypos, dir_as_cell);
 | |
|     if (ct == SNAKE_CELL_FOOD) {
 | |
|         if (are_cells_full_(ctx)) {
 | |
|             snake_initialize(ctx);
 | |
|             return;
 | |
|         }
 | |
|         new_food_pos_(ctx);
 | |
|         ++ctx->inhibit_tail_step;
 | |
|         ++ctx->occupied_cells;
 | |
|     }
 | |
| }
 | |
| 
 | |
| static int handle_key_event_(SnakeContext *ctx, SDL_Scancode key_code)
 | |
| {
 | |
|     switch (key_code) {
 | |
|     /* Quit. */
 | |
|     case SDL_SCANCODE_ESCAPE:
 | |
|     case SDL_SCANCODE_Q:
 | |
|         return SDL_APP_SUCCESS;
 | |
|     /* Restart the game as if the program was launched. */
 | |
|     case SDL_SCANCODE_R:
 | |
|         snake_initialize(ctx);
 | |
|         break;
 | |
|     /* Decide new direction of the snake. */
 | |
|     case SDL_SCANCODE_RIGHT:
 | |
|         snake_redir(ctx, SNAKE_DIR_RIGHT);
 | |
|         break;
 | |
|     case SDL_SCANCODE_UP:
 | |
|         snake_redir(ctx, SNAKE_DIR_UP);
 | |
|         break;
 | |
|     case SDL_SCANCODE_LEFT:
 | |
|         snake_redir(ctx, SNAKE_DIR_LEFT);
 | |
|         break;
 | |
|     case SDL_SCANCODE_DOWN:
 | |
|         snake_redir(ctx, SNAKE_DIR_DOWN);
 | |
|         break;
 | |
|     default:
 | |
|         break;
 | |
|     }
 | |
|     return SDL_APP_CONTINUE;
 | |
| }
 | |
| 
 | |
| SDL_AppResult SDL_AppIterate(void *appstate)
 | |
| {
 | |
|     AppState *as = (AppState *)appstate;
 | |
|     SnakeContext *ctx = &as->snake_ctx;
 | |
|     const Uint64 now = SDL_GetTicks();
 | |
|     SDL_FRect r;
 | |
|     unsigned i;
 | |
|     unsigned j;
 | |
|     int ct;
 | |
| 
 | |
|     // run game logic if we're at or past the time to run it.
 | |
|     // if we're _really_ behind the time to run it, run it
 | |
|     // several times.
 | |
|     while ((now - as->last_step) >= STEP_RATE_IN_MILLISECONDS) {
 | |
|         snake_step(ctx);
 | |
|         as->last_step += STEP_RATE_IN_MILLISECONDS;
 | |
|     }
 | |
| 
 | |
|     r.w = r.h = SNAKE_BLOCK_SIZE_IN_PIXELS;
 | |
|     SDL_SetRenderDrawColor(as->renderer, 0, 0, 0, SDL_ALPHA_OPAQUE);
 | |
|     SDL_RenderClear(as->renderer);
 | |
|     for (i = 0; i < SNAKE_GAME_WIDTH; i++) {
 | |
|         for (j = 0; j < SNAKE_GAME_HEIGHT; j++) {
 | |
|             ct = snake_cell_at(ctx, i, j);
 | |
|             if (ct == SNAKE_CELL_NOTHING)
 | |
|                 continue;
 | |
|             set_rect_xy_(&r, i, j);
 | |
|             if (ct == SNAKE_CELL_FOOD)
 | |
|                 SDL_SetRenderDrawColor(as->renderer, 80, 80, 255, SDL_ALPHA_OPAQUE);
 | |
|             else /* body */
 | |
|                 SDL_SetRenderDrawColor(as->renderer, 0, 128, 0, SDL_ALPHA_OPAQUE);
 | |
|             SDL_RenderFillRect(as->renderer, &r);
 | |
|         }
 | |
|     }
 | |
|     SDL_SetRenderDrawColor(as->renderer, 255, 255, 0, SDL_ALPHA_OPAQUE); /*head*/
 | |
|     set_rect_xy_(&r, ctx->head_xpos, ctx->head_ypos);
 | |
|     SDL_RenderFillRect(as->renderer, &r);
 | |
|     SDL_RenderPresent(as->renderer);
 | |
|     return SDL_APP_CONTINUE;
 | |
| }
 | |
| 
 | |
| static const struct
 | |
| {
 | |
|     const char *key;
 | |
|     const char *value;
 | |
| } extended_metadata[] =
 | |
| {
 | |
|     { SDL_PROP_APP_METADATA_URL_STRING, "https://examples.libsdl.org/SDL3/demo/01-snake/" },
 | |
|     { SDL_PROP_APP_METADATA_CREATOR_STRING, "SDL team" },
 | |
|     { SDL_PROP_APP_METADATA_COPYRIGHT_STRING, "Placed in the public domain" },
 | |
|     { SDL_PROP_APP_METADATA_TYPE_STRING, "game" }
 | |
| };
 | |
| 
 | |
| SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[])
 | |
| {
 | |
|     size_t i;
 | |
| 
 | |
|     if (!SDL_SetAppMetadata("Example Snake game", "1.0", "com.example.Snake")) {
 | |
|         return SDL_APP_FAILURE;
 | |
|     }
 | |
| 
 | |
|     for (i = 0; i < SDL_arraysize(extended_metadata); i++) {
 | |
|         if (!SDL_SetAppMetadataProperty(extended_metadata[i].key, extended_metadata[i].value)) {
 | |
|             return SDL_APP_FAILURE;
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     if (!SDL_Init(SDL_INIT_VIDEO)) {
 | |
|         return SDL_APP_FAILURE;
 | |
|     }
 | |
| 
 | |
|     AppState *as = SDL_calloc(1, sizeof(AppState));
 | |
|     if (!as) {
 | |
|         return SDL_APP_FAILURE;
 | |
|     }
 | |
| 
 | |
|     *appstate = as;
 | |
| 
 | |
|     if (!SDL_CreateWindowAndRenderer("examples/demo/snake", SDL_WINDOW_WIDTH, SDL_WINDOW_HEIGHT, 0, &as->window, &as->renderer)) {
 | |
|         return SDL_APP_FAILURE;
 | |
|     }
 | |
| 
 | |
|     snake_initialize(&as->snake_ctx);
 | |
| 
 | |
|     as->last_step = SDL_GetTicks();
 | |
| 
 | |
|     return SDL_APP_CONTINUE;
 | |
| }
 | |
| 
 | |
| SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event)
 | |
| {
 | |
|     SnakeContext *ctx = &((AppState *)appstate)->snake_ctx;
 | |
|     switch (event->type) {
 | |
|     case SDL_EVENT_QUIT:
 | |
|         return SDL_APP_SUCCESS;
 | |
|     case SDL_EVENT_KEY_DOWN:
 | |
|         return handle_key_event_(ctx, event->key.scancode);
 | |
|     }
 | |
|     return SDL_APP_CONTINUE;
 | |
| }
 | |
| 
 | |
| void SDL_AppQuit(void *appstate, SDL_AppResult result)
 | |
| {
 | |
|     if (appstate != NULL) {
 | |
|         AppState *as = (AppState *)appstate;
 | |
|         SDL_DestroyRenderer(as->renderer);
 | |
|         SDL_DestroyWindow(as->window);
 | |
|         SDL_free(as);
 | |
|     }
 | |
| }
 | 
