Skip to content

Instantly share code, notes, and snippets.

@cellularmitosis
Created May 4, 2023 04:35
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save cellularmitosis/e5f91fdcda2df58c9200780b2834b054 to your computer and use it in GitHub Desktop.
Save cellularmitosis/e5f91fdcda2df58c9200780b2834b054 to your computer and use it in GitHub Desktop.
Snake in C and SDL 1.2, revisited

Snake in C and SDL 1.2, revisited

  • implemented pause
  • draw the background by over
SDL=$(shell sdl-config --cflags --libs)
default: snake run
run: snake
./snake
snake: snake.c
gcc -std=c99 -Wall -Werror -O2 $(SDL) snake.c -o snake
web: snake.html
snake.html:
emcc -O3 -s ASYNCIFY snake.c -o snake.html
clean:
rm -f snake snake.js snake.wasm snake.html
.PHONY: default run web clean
// A simple "snake" game written in C / SDL 1.2.
// Copyright 2020 Jason Pepas
// Released under the terms of the MIT license
// See https://opensource.org/licenses/MIT
// Code style:
// - camelCase for identifiers.
// - 'p' suffix used for pointers.
#include <stdint.h> // uint8_t
#include <assert.h> // assert
#include <stdbool.h> // true
#include <stdlib.h> // exit, srand, rand
#include <time.h> // time
#include <string.h> // memcpy
#define unreachable assert(false);exit(99);
#ifdef __APPLE__
#include <SDL.h>
#else
#include <SDL/SDL.h>
#endif
#define UP 1
#define RIGHT 2
#define DOWN 3
#define LEFT 4
// A ring buffer.
struct _RingBuf {
void* firstp;
void* lastp;
size_t unitSize;
};
typedef struct _RingBuf RingBuf;
// Initialize a ring buffer.
void ringInit(RingBuf* ringp, size_t unitSize, size_t count) {
ringp->unitSize = unitSize;
size_t size = unitSize * count;
ringp->firstp = malloc(size);
assert(ringp->firstp != NULL);
ringp->lastp = (uint8_t*)(ringp->firstp) + size - unitSize;
}
// Return the next slot in a ring buffer.
void* ringNext(RingBuf* ringp, void* currentp) {
void* nextp = ((uint8_t*)currentp) + ringp->unitSize;
assert(nextp > currentp); // check for overflow.
if (nextp > ringp->lastp) {
nextp = ringp->firstp;
}
return nextp;
}
// Return the previous slot in a ring buffer.
void* ringPrev(RingBuf* ringp, void* currentp) {
void* prevp = ((uint8_t*)currentp) - ringp->unitSize;
assert(prevp < currentp); // check for underflow.
if (prevp < ringp->firstp) {
prevp = ringp->lastp;
}
return prevp;
}
// One block of a snake body.
struct _SnakeNode {
uint8_t x;
uint8_t y;
};
typedef struct _SnakeNode SnakeNode;
// A block of food.
struct _Food {
uint8_t x;
uint8_t y;
};
typedef struct _Food Food;
// A rectangular portion of the screen.
struct _Rect {
int16_t x;
int16_t y;
uint16_t w;
uint16_t h;
};
typedef struct _Rect Rect;
// An array of Rects.
struct _RectList {
Rect* arrayp;
uint16_t capacity;
uint16_t nextIndex;
};
typedef struct _RectList RectList;
// The game state.
struct _State {
uint8_t cellSize;
uint8_t gridWidth;
uint8_t gridHeight;
bool crashed;
bool paused;
uint8_t direction;
RingBuf ring;
SnakeNode* headp;
SnakeNode* tailp;
Food food;
SDL_Surface* screenp;
uint32_t framePeriod;
uint32_t lastFrame;
RectList dirtyRects;
uint32_t bgColor;
uint32_t snakeColor;
};
typedef struct _State State;
// Initialize an array of Rects.
void rectListInit(State* statep, size_t count) {
assert(count > 0);
size_t size = sizeof(Rect) * count;
statep->dirtyRects.arrayp = malloc(size);
assert(statep->dirtyRects.arrayp != NULL);
statep->dirtyRects.capacity = count;
statep->dirtyRects.nextIndex = 0;
}
// Grow an array of Rects.
void rectListGrow(State* statep) {
Rect* oldRects = statep->dirtyRects.arrayp;
size_t oldSize = sizeof(Rect) * statep->dirtyRects.capacity;
size_t newCount = statep->dirtyRects.capacity * 2;
size_t newSize = sizeof(Rect) * newCount;
statep->dirtyRects.arrayp = malloc(newSize);
assert(statep->dirtyRects.arrayp != NULL);
memcpy(statep->dirtyRects.arrayp, oldRects, oldSize);
statep->dirtyRects.capacity = newCount;
free(oldRects);
}
// Push a new Rect onto the list of dirty Rects.
void rectListPush(State* statep, Rect* r) {
if (statep->dirtyRects.nextIndex == statep->dirtyRects.capacity) {
rectListGrow(statep);
}
memcpy(statep->dirtyRects.arrayp + statep->dirtyRects.nextIndex, r, sizeof(Rect));
statep->dirtyRects.nextIndex += 1;
}
// Pop the last dirty Rect from the list.
Rect* rectListPop(State* statep) {
if (statep->dirtyRects.nextIndex == 0) {
return NULL;
}
int i = statep->dirtyRects.nextIndex - 1;
statep->dirtyRects.nextIndex = i;
return &(statep->dirtyRects.arrayp[i]);
}
// Exit the game (terminate the process).
void quit(int status) {
SDL_Quit();
exit(status);
}
// Iterate to the next block of the snake body.
// Returns NULL when you run off the end.
SnakeNode* snakeNext(State* statep, SnakeNode* snakep) {
if (snakep == statep->tailp) {
return NULL;
} else {
return ringNext(&(statep->ring), snakep);
}
}
// Does the snake head collide with the food block?
bool foodCollidesWithSnake(State* statep) {
SnakeNode* cursorp = statep->headp;
while (cursorp != NULL) {
if (statep->food.x == cursorp->x && statep->food.y == cursorp->y) {
return true;
}
cursorp = snakeNext(statep, cursorp);
}
return false;
}
// Respawn the food into a new location which does not collide with the snake.
void respawnFood(State* statep) {
// TODO: this approach will become non-performant as the snake fills
// the screen (more and more re-rolls will be required to find an open
// block).
while (true) {
statep->food.x = rand() % statep->gridWidth;
statep->food.y = rand() % statep->gridHeight;
if (foodCollidesWithSnake(statep)) {
continue;
} else {
break;
}
}
}
// Does the snake head collide with its body?
bool snakeCollidesWithSnake(State* statep) {
if (statep->headp == statep->tailp) {
return false;
}
SnakeNode* cursorp = snakeNext(statep, statep->headp);
while (cursorp != NULL) {
if (statep->headp->x == cursorp->x && statep->headp->y == cursorp->y) {
return true;
}
cursorp = snakeNext(statep, cursorp);
}
return false;
}
// Would the snake be out of bounds after advancing the snake head?
bool wouldBeOutOfBounds(State* statep) {
uint8_t direction = statep->direction;
SnakeNode* headp = statep->headp;
switch (direction) {
case UP:
if (headp->y == 0) {
return true;
}
break;
case DOWN:
if (headp->y == statep->gridHeight - 1) {
return true;
}
break;
case LEFT:
if (headp->x == 0) {
return true;
}
break;
case RIGHT:
if (headp->x == statep->gridWidth - 1) {
return true;
}
break;
default:
unreachable;
}
return false;
}
// Advance the snake by one block.
void moveSnake(State* statep) {
if (wouldBeOutOfBounds(statep)) {
statep->crashed = true;
}
if (statep->crashed) {
return;
}
SnakeNode* newHeadp = ringPrev(&(statep->ring), statep->headp);
newHeadp->x = statep->headp->x;
newHeadp->y = statep->headp->y;
if (statep->direction == UP) {
newHeadp->y--;
} else if (statep->direction == DOWN) {
newHeadp->y++;
} else if (statep->direction == LEFT) {
newHeadp->x--;
} else if (statep->direction == RIGHT) {
newHeadp->x++;
} else {
unreachable;
}
statep->headp = newHeadp;
bool didEat = false;
if (statep->headp->x == statep->food.x && statep->headp->y == statep->food.y) {
didEat = true;
respawnFood(statep);
}
if (didEat == false) {
statep->tailp = ringPrev(&(statep->ring), statep->tailp);
}
if (snakeCollidesWithSnake(statep)) {
statep->crashed = true;
}
}
// Restart: start a new game.
void restart(State* statep) {
statep->headp = statep->ring.firstp;
statep->tailp = statep->headp;
statep->headp->x = rand() % statep->gridWidth;
statep->headp->y = rand() % statep->gridHeight;
respawnFood(statep);
statep->crashed = false;
statep->paused = false;
if (statep->headp->x > statep->gridWidth / 2) {
statep->direction = LEFT;
} else {
statep->direction = RIGHT;
}
}
// Deduce the direction, based on two snake blocks.
uint8_t getDirection(SnakeNode* s1p, SnakeNode* s2p) {
if (s1p->x < s2p->x && s1p->y == s2p->y) {
return RIGHT;
} else if (s2p->x < s1p->x && s1p->y == s2p->y) {
return LEFT;
} else if (s1p->x == s2p->x && s1p->y < s2p->y) {
return DOWN;
} else if (s1p->x == s2p->x && s2p->y < s1p->y) {
return UP;
} else {
unreachable;
}
}
uint32_t randomColor(State* statep) {
uint8_t r = rand() & 0xFF;
uint8_t g = rand() & 0xFF;
uint8_t b = rand() & 0xFF;
return SDL_MapRGB(statep->screenp->format, r, g, b);
}
// Draw a Rect.
void drawRect(State* statep, Rect* r, uint32_t color) {
SDL_Rect rect;
rect.x = r->x;
rect.y = r->y;
rect.w = r->w;
rect.h = r->h;
SDL_FillRect(statep->screenp, &rect, color);
rectListPush(statep, r);
}
// Draw the snake.
void drawSnake(State* statep) {
SnakeNode* cursorp = statep->headp;
int16_t x;
int16_t y;
uint16_t w;
uint16_t h;
uint8_t cellSize = statep->cellSize;
uint32_t color = statep->snakeColor;
// draw two nodes at a time, so that the connecting link is also drawn.
while (cursorp != statep->tailp) {
if (statep->crashed) {
color = randomColor(statep);
}
SnakeNode* snake2p = ringNext(&(statep->ring), cursorp);
uint8_t direction = getDirection(cursorp, snake2p);
// x, y
switch (direction) {
case RIGHT:
case DOWN:
x = cursorp->x * cellSize;
y = cursorp->y * cellSize;
break;
case LEFT:
case UP:
x = snake2p->x * cellSize;
y = snake2p->y * cellSize;
break;
default:
unreachable;
}
// w, h
switch (direction) {
case RIGHT:
case LEFT:
w = cellSize * 2;
h = cellSize;
break;
case UP:
case DOWN:
w = cellSize;
h = cellSize * 2;
break;
default:
unreachable;
}
// inset
x += 1;
y += 1;
w -= 2;
h -= 2;
Rect rect = {x, y, w, h};
drawRect(statep, &rect, color);
cursorp = snake2p;
continue;
}
// draw the last block.
if (statep->crashed) {
color = randomColor(statep);
}
x = cursorp->x * cellSize;
y = cursorp->y * cellSize;
w = cellSize;
h = cellSize;
// inset
x += 1;
y += 1;
w -= 2;
h -= 2;
Rect rect = {x, y, w, h};
drawRect(statep, &rect, color);
}
// Draw the food.
void drawFood(State* statep) {
int16_t x = statep->food.x * statep->cellSize;
int16_t y = statep->food.y * statep->cellSize;
uint16_t w = statep->cellSize;
uint16_t h = statep->cellSize;
Rect rect = {x, y, w, h};
uint32_t color = randomColor(statep);
drawRect(statep, &rect, color);
}
// Draw the background.
void drawBG(State* statep) {
int16_t x = 0;
int16_t y = 0;
uint16_t w = statep->gridWidth * statep->cellSize;
uint16_t h = statep->gridHeight * statep->cellSize;
SDL_Rect rect = {x, y, w, h};
SDL_FillRect(statep->screenp, &rect, statep->bgColor);
}
// Draw the background by filling over all of the previously drawn rects.
void drawBG_opt(State* statep) {
Rect* rect;
while (true) {
rect = rectListPop(statep);
if (rect == NULL) {
break;
}
SDL_Rect srect = {rect->x, rect->y, rect->w, rect->h};
SDL_FillRect(statep->screenp, &srect, statep->bgColor);
}
}
// Perform all drawing. Called once per frame.
void draw(State* statep) {
drawBG_opt(statep);
drawSnake(statep);
if (statep->crashed == false) {
drawFood(statep);
}
SDL_Flip(statep->screenp);
}
// Update the game state. Called once per frame.
void update(State* statep) {
SDL_Event event;
while (SDL_PollEvent(&event) == 1) {
if (event.type == SDL_QUIT) {
// the window was closed.
quit(0);
} else if (event.type == SDL_KEYDOWN) {
SDLKey k = event.key.keysym.sym;
// check if we need to quit.
if (k == SDLK_ESCAPE || k == SDLK_q) {
quit(0);
}
// if crashed, any key (other than quit) restarts.
if (statep->crashed == true) {
restart(statep);
return;
}
// pause / unpause
if (k == SDLK_SPACE) {
statep->paused = !(statep->paused);
break;
}
// check if we need to change direction.
// (but only process one direction change per frame).
if (k == SDLK_UP && statep->direction != DOWN) {
statep->direction = UP;
break;
} else if (k == SDLK_DOWN && statep->direction != UP) {
statep->direction = DOWN;
break;
} else if (k == SDLK_LEFT && statep->direction != RIGHT) {
statep->direction = LEFT;
break;
} else if (k == SDLK_RIGHT && statep->direction != LEFT) {
statep->direction = RIGHT;
break;
}
}
continue;
}
if (!statep->crashed && !statep->paused) {
moveSnake(statep);
}
}
// Perform all initialization.
void init(State* statep) {
srand(time(NULL));
int ret = SDL_Init(SDL_INIT_VIDEO);
assert(ret == 0);
rectListInit(statep, 1);
statep->cellSize = 32;
statep->gridWidth = 20;
statep->gridHeight = 13;
int width = statep->gridWidth * statep->cellSize;
int height = statep->gridHeight * statep->cellSize;
int bpp = 0; // use current bits per pixel.
uint32_t flags = SDL_HWSURFACE;
statep->screenp = SDL_SetVideoMode(width, height, bpp, flags);
assert(statep->screenp != NULL);
uint8_t r = 255;
uint8_t g = 0;
uint8_t b = 0;
statep->snakeColor = SDL_MapRGB(statep->screenp->format, r, g, b);
statep->framePeriod = 200; // in milliseconds
statep->lastFrame = 0;
size_t unitSize = sizeof(SnakeNode);
size_t count = statep->gridWidth * statep->gridHeight;
ringInit(&(statep->ring), unitSize, count);
r = 0;
g = 0;
b = 0;
statep->bgColor = SDL_MapRGB(statep->screenp->format, r, g, b);
restart(statep);
}
// The process entry point.
int main(int argc, char** argv) {
State state;
init(&state);
drawBG(&state);
while (true) {
uint32_t ticks = SDL_GetTicks();
uint32_t elapsed = ticks - state.lastFrame;
if (elapsed >= state.framePeriod) {
update(&state);
draw(&state);
if (elapsed > state.framePeriod * 2) {
// catch up.
state.lastFrame = ticks;
} else {
state.lastFrame += state.framePeriod;
}
} else {
uint32_t remaining = state.framePeriod - elapsed;
SDL_Delay(remaining);
}
continue;
}
return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment