aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Duncan <pabs@pablotron.org>2019-01-13 08:29:12 -0500
committerPaul Duncan <pabs@pablotron.org>2019-01-13 08:29:12 -0500
commitcd0f70f86d1cedffcd1bacf57d9250fdff1f7af3 (patch)
treea7be11a817a89e47700691f5042ba005e756bc4a
parenta3f788b6a64bbf89342fec2111eb1e1b478db13f (diff)
downloadsok-cd0f70f86d1cedffcd1bacf57d9250fdff1f7af3.tar.bz2
sok-cd0f70f86d1cedffcd1bacf57d9250fdff1f7af3.zip
refactor sok-sdl, add zoom and warp_buf support
-rw-r--r--meson.build3
-rw-r--r--src/sdl/action.c37
-rw-r--r--src/sdl/action.h5
-rw-r--r--src/sdl/color.c25
-rw-r--r--src/sdl/color.h20
-rw-r--r--src/sdl/draw.c169
-rw-r--r--src/sdl/draw.h21
-rw-r--r--src/sdl/main.c335
-rw-r--r--src/sdl/util.h13
-rw-r--r--src/sdl/warp-buf.c52
-rw-r--r--src/sdl/warp-buf.h17
11 files changed, 473 insertions, 224 deletions
diff --git a/meson.build b/meson.build
index d8c3b35..b3c7cbd 100644
--- a/meson.build
+++ b/meson.build
@@ -19,7 +19,10 @@ executable('sok-text', sources + [
# sdl exe
executable('sok-sdl', sources + [
'src/text/levels.c',
+ 'src/sdl/warp-buf.c',
+ 'src/sdl/color.c',
'src/sdl/action.c',
+ 'src/sdl/draw.c',
'src/sdl/main.c',
], dependencies: [
dependency('SDL2'),
diff --git a/src/sdl/action.c b/src/sdl/action.c
index a4dcb79..8307f19 100644
--- a/src/sdl/action.c
+++ b/src/sdl/action.c
@@ -24,8 +24,8 @@ keycode_to_dir(const SDL_Keycode code) {
}
}
-action_t
-get_action(
+static action_t
+get_key_action(
const SDL_Keycode code
) {
switch (code) {
@@ -61,3 +61,36 @@ get_action(
return (action_t) { .type = ACTION_NONE };
}
}
+
+static action_t
+get_wheel_action(
+ const Sint32 y
+) {
+ if (y > 0) {
+ return (action_t) { .type = ACTION_ZOOM_IN, .data = y };
+ } else if (y < 0) {
+ return (action_t) { .type = ACTION_ZOOM_OUT, .data = -y };
+ } else {
+ return (action_t) { .type = ACTION_NONE };
+ }
+}
+
+action_t
+get_action(
+ const SDL_Event * const ev
+) {
+ switch (ev->type) {
+ case SDL_QUIT:
+ return (action_t) { .type = ACTION_QUIT };
+ break;
+ case SDL_KEYUP:
+ return get_key_action(ev->key.keysym.sym);
+ break;
+ case SDL_MOUSEWHEEL:
+ return get_wheel_action(ev->wheel.y);
+ break;
+ default:
+ // ignore event
+ return (action_t) { .type = ACTION_NONE };
+ }
+}
diff --git a/src/sdl/action.h b/src/sdl/action.h
index d6d9870..4713f62 100644
--- a/src/sdl/action.h
+++ b/src/sdl/action.h
@@ -10,6 +10,9 @@ typedef enum {
ACTION_WARP,
ACTION_WARP_BUF_PUSH,
ACTION_WARP_BUF_POP,
+ ACTION_ZOOM_IN,
+ ACTION_ZOOM_OUT,
+ ACTION_ZOOM_RESET,
ACTION_UNDO,
ACTION_NEXT,
ACTION_SOLVE,
@@ -22,6 +25,6 @@ typedef struct {
uint64_t data;
} action_t;
-action_t get_action(const SDL_Keycode);
+action_t get_action(const SDL_Event * const);
#endif /* ACTION_H */
diff --git a/src/sdl/color.c b/src/sdl/color.c
new file mode 100644
index 0000000..1e75cb6
--- /dev/null
+++ b/src/sdl/color.c
@@ -0,0 +1,25 @@
+#include <stdbool.h>
+#include "color.h"
+
+static const SDL_Color
+PALETTE[] = {
+ { .r = 0x00, .g = 0x00, .b = 0x00, .a = 0xFF }, // COLOR_BG
+ { .r = 0x00, .g = 0xFF, .b = 0x00, .a = 0xFF }, // COLOR_BG_WON
+ { .r = 0x66, .g = 0x66, .b = 0x66, .a = 0xFF }, // COLOR_WALL
+ { .r = 0x00, .g = 0xFF, .b = 0x00, .a = 0xFF }, // COLOR_GOAL
+ { .r = 0xFF, .g = 0x00, .b = 0x00, .a = 0xFF }, // COLOR_HOME
+ { .r = 0xFF, .g = 0xFF, .b = 0x00, .a = 0xFF }, // COLOR_HOME_GOAL
+ { .r = 0x00, .g = 0x00, .b = 0xFF, .a = 0xFF }, // COLOR_BOX
+ { .r = 0x00, .g = 0xFF, .b = 0xFF, .a = 0xFF }, // COLOR_BOX_GOAL
+ { 0, 0, 0, 0 }, // COLOR_LAST
+};
+
+bool
+set_color(
+ SDL_Renderer * const renderer,
+ const color_t ofs
+) {
+ // FIXME: check for error?
+ const SDL_Color c = PALETTE[ofs];
+ return SDL_SetRenderDrawColor(renderer, c.r, c.g, c.b, c.a) < 0;
+}
diff --git a/src/sdl/color.h b/src/sdl/color.h
new file mode 100644
index 0000000..5cff762
--- /dev/null
+++ b/src/sdl/color.h
@@ -0,0 +1,20 @@
+#ifndef COLOR_H
+#define COLOR_H
+
+#include <SDL.h>
+
+typedef enum {
+ COLOR_BG,
+ COLOR_BG_WON,
+ COLOR_WALL,
+ COLOR_GOAL,
+ COLOR_HOME,
+ COLOR_HOME_GOAL,
+ COLOR_BOX,
+ COLOR_BOX_GOAL,
+ COLOR_LAST,
+} color_t;
+
+_Bool set_color(SDL_Renderer * const, const color_t);
+
+#endif /* COLOR_H */
diff --git a/src/sdl/draw.c b/src/sdl/draw.c
new file mode 100644
index 0000000..f6e350b
--- /dev/null
+++ b/src/sdl/draw.c
@@ -0,0 +1,169 @@
+#include <stdbool.h> // bool
+#include "util.h" // warn()/die()
+#include "color.h" // set_color()
+#include "draw.h"
+
+static size_t
+get_cell_size(
+ const draw_ctx_t * const draw_ctx
+) {
+ return 32 + 8 * *draw_ctx->zoom;
+}
+
+static SDL_Rect
+get_cell_rect(
+ const draw_ctx_t * const draw_ctx,
+ const sok_pos_t pos
+) {
+ const size_t cell_size = get_cell_size(draw_ctx);
+
+ return (SDL_Rect) {
+ draw_ctx->render_ofs.x + pos.x * cell_size,
+ draw_ctx->render_ofs.y + pos.y * cell_size,
+ cell_size,
+ cell_size
+ };
+}
+
+static bool
+draw_on_size(
+ const sok_ctx_t * const ctx,
+ const sok_pos_t level_size,
+ void * const data
+) {
+ draw_ctx_t * const draw_ctx = data;
+ const size_t cell_size = get_cell_size(draw_ctx);
+
+ // get renderer size
+ int renderer_x, renderer_y;
+ if (SDL_GetRendererOutputSize(draw_ctx->renderer, &renderer_x, &renderer_y)) {
+ die("SDL_GetRendererOutputSize(): %s", SDL_GetError());
+ }
+
+ // calculate renderer offset
+ draw_ctx->render_ofs.x = (renderer_x - level_size.x * cell_size) / 2;
+ draw_ctx->render_ofs.y = (renderer_y - level_size.y * cell_size) / 2;
+
+ return true;
+}
+
+static bool
+draw_on_walls_start(
+ const sok_ctx_t * const ctx,
+ void * const data
+) {
+ draw_ctx_t * const draw_ctx = data;
+ set_color(draw_ctx->renderer, COLOR_WALL);
+
+ return true;
+}
+
+static bool
+draw_on_wall(
+ const sok_ctx_t * const ctx,
+ const sok_pos_t pos,
+ void * const data
+) {
+ draw_ctx_t * const draw_ctx = data;
+ const SDL_Rect rect = get_cell_rect(draw_ctx, pos);
+
+ if (SDL_RenderFillRect(draw_ctx->renderer, &rect)) {
+ die("SDL_RenderFillRect(): %s", SDL_GetError());
+ }
+
+ return true;
+}
+
+static bool
+draw_on_goals_start(
+ const sok_ctx_t * const ctx,
+ void * const data
+) {
+ draw_ctx_t * const draw_ctx = data;
+ set_color(draw_ctx->renderer, COLOR_GOAL);
+
+ return true;
+}
+
+static bool
+draw_on_goal(
+ const sok_ctx_t * const ctx,
+ const sok_pos_t pos,
+ const bool has_player,
+ const bool has_box,
+ void * const data
+) {
+ draw_ctx_t * const draw_ctx = data;
+ const SDL_Rect rect = get_cell_rect(draw_ctx, pos);
+
+ if (SDL_RenderFillRect(draw_ctx->renderer, &rect)) {
+ die("SDL_RenderFillRect(): %s", SDL_GetError());
+ }
+
+ return true;
+}
+
+static bool
+draw_on_home(
+ const sok_ctx_t * const ctx,
+ const sok_pos_t pos,
+ const bool has_goal,
+ void * const data
+) {
+ draw_ctx_t * const draw_ctx = data;
+ const SDL_Rect rect = get_cell_rect(draw_ctx, pos);
+
+ set_color(draw_ctx->renderer, has_goal ? COLOR_HOME_GOAL : COLOR_HOME);
+
+ if (SDL_RenderFillRect(draw_ctx->renderer, &rect)) {
+ die("SDL_RenderFillRect(): %s", SDL_GetError());
+ }
+
+ return true;
+}
+
+static bool
+draw_on_box(
+ const sok_ctx_t * const ctx,
+ const sok_pos_t pos,
+ const bool has_goal,
+ void * const data
+) {
+ draw_ctx_t * const draw_ctx = data;
+ const SDL_Rect rect = get_cell_rect(draw_ctx, pos);
+
+ set_color(draw_ctx->renderer, has_goal ? COLOR_BOX_GOAL : COLOR_BOX);
+
+ if (SDL_RenderFillRect(draw_ctx->renderer, &rect)) {
+ die("SDL_RenderFillRect(): %s", SDL_GetError());
+ }
+
+ return true;
+}
+
+static const sok_ctx_walk_cbs_t
+DRAW_CBS = {
+ .on_size = draw_on_size,
+ .on_walls_start = draw_on_walls_start,
+ .on_wall = draw_on_wall,
+ .on_goals_start = draw_on_goals_start,
+ .on_goal = draw_on_goal,
+ .on_home = draw_on_home,
+ .on_box = draw_on_box,
+};
+
+void
+draw(
+ draw_ctx_t * const draw_ctx
+) {
+ // clear background
+ set_color(draw_ctx->renderer, sok_ctx_is_done(draw_ctx->ctx) ? COLOR_BG_WON : COLOR_BG);
+ SDL_RenderClear(draw_ctx->renderer);
+
+ // render
+ sok_ctx_walk(draw_ctx->ctx, &DRAW_CBS, draw_ctx);
+
+ // flip
+ SDL_RenderPresent(draw_ctx->renderer);
+}
+
diff --git a/src/sdl/draw.h b/src/sdl/draw.h
new file mode 100644
index 0000000..304d972
--- /dev/null
+++ b/src/sdl/draw.h
@@ -0,0 +1,21 @@
+#ifndef DRAW_H
+#define DRAW_H
+
+#include <stddef.h> // size_t
+#include <SDL.h>
+#include "../libsok/sok.h"
+#include "../text/levels.h"
+
+typedef struct {
+ SDL_Renderer * const renderer;
+ const sok_ctx_t * const ctx;
+ const size_t * const level_num;
+ const level_t *level;
+ const size_t * const zoom;
+
+ sok_pos_t render_ofs;
+} draw_ctx_t;
+
+void draw(draw_ctx_t * const);
+
+#endif /* DRAW_H */
diff --git a/src/sdl/main.c b/src/sdl/main.c
index 831d3f7..882eefb 100644
--- a/src/sdl/main.c
+++ b/src/sdl/main.c
@@ -6,171 +6,47 @@
#include <SDL_image.h>
#include "../text/levels.h"
#include "../libsok/sok.h"
+#include "util.h"
#include "action.h"
-
-#define warn(...) do { \
- SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__); \
-} while (0)
-
-#define die(...) do { \
- SDL_LogCritical(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__); \
- exit(EXIT_FAILURE); \
-} while (0)
-
-static void
-solve_on_error(
- const char * const err
-) {
- die("Error solving level: %s", err);
-}
+#include "warp-buf.h"
+#include "draw.h"
static void
draw_moves(
const sok_ctx_t * const ctx,
const size_t skip_moves
) {
- printf("Solution (%zu moves): ", ctx->num_moves - skip_moves);
+ char buf[1024] = { 0 };
+ size_t ofs = 0;
+
for (size_t i = skip_moves; i < ctx->num_moves; i++) {
if (ctx->moves[i].dir >= SOK_DIR_LAST) {
die("invalid move: %u", ctx->moves[i].dir);
}
- fputs(SOK_DIR_TO_STR(ctx->moves[i].dir), stdout);
- }
- printf("\n");
-}
-
-static const SDL_Color
-RED = { .r = 0x00, .g = 0xFF, .b = 0x00, .a = 0xFF },
-BLACK = { .r = 0x00, .g = 0x00, .b = 0x00, .a = 0xFF };
-
-static SDL_Color
-get_bg(
- const sok_ctx_t * const ctx
-) {
- return (sok_ctx_is_done(ctx)) ? RED : BLACK;
-}
-
-bool draw_on_wall(
- const sok_ctx_t * const ctx,
- const sok_pos_t pos,
- void * const data
-) {
- SDL_Renderer *renderer = data;
- SDL_Rect rect = { 32 * pos.x, 32 * pos.y, 32, 32 };
-
- SDL_SetRenderDrawColor(renderer, 0x66, 0x66, 0x66, 0xFF);
-
- if (SDL_RenderFillRect(renderer, &rect)) {
- die("SDL_RenderFillRect(): %s", SDL_GetError());
- }
-
- return true;
-}
-
-bool draw_on_goal(
- const sok_ctx_t * const ctx,
- const sok_pos_t pos,
- const bool has_player,
- const bool has_box,
- void * const data
-) {
- SDL_Renderer *renderer = data;
- SDL_Rect rect = { 32 * pos.x, 32 * pos.y, 32, 32 };
-
- SDL_SetRenderDrawColor(renderer, 0x00, 0xFF, 0x00, 0xFF);
-
- if (SDL_RenderFillRect(renderer, &rect)) {
- die("SDL_RenderFillRect(): %s", SDL_GetError());
- }
-
- return true;
-}
-
-bool draw_on_home(
- const sok_ctx_t * const ctx,
- const sok_pos_t pos,
- const bool has_goal,
- void * const data
-) {
- SDL_Renderer *renderer = data;
- SDL_Rect rect = { 32 * pos.x, 32 * pos.y, 32, 32 };
-
- SDL_SetRenderDrawColor(renderer, 0xFF, has_goal ? 0xFF : 0x00, 0x00, 0xFF);
-
- if (SDL_RenderFillRect(renderer, &rect)) {
- die("SDL_RenderFillRect(): %s", SDL_GetError());
- }
-
- return true;
-}
-
-bool draw_on_box(
- const sok_ctx_t * const ctx,
- const sok_pos_t pos,
- const bool has_goal,
- void * const data
-) {
- SDL_Renderer *renderer = data;
- SDL_Rect rect = { 32 * pos.x, 32 * pos.y, 32, 32 };
-
- SDL_SetRenderDrawColor(renderer, 0x00, has_goal ? 0xFF : 0x00, 0xFF, 0xFF);
-
- if (SDL_RenderFillRect(renderer, &rect)) {
- die("SDL_RenderFillRect(): %s", SDL_GetError());
+ buf[ofs++] = SOK_DIR_TO_CHAR(ctx->moves[i].dir);
}
- return true;
+ SDL_Log("Solution (%zu moves): %s", ctx->num_moves - skip_moves, buf);
}
-static sok_ctx_walk_cbs_t
-DRAW_CBS = {
- .on_wall = draw_on_wall,
- .on_goal = draw_on_goal,
- .on_home = draw_on_home,
- .on_box = draw_on_box,
-};
-/*
- * typedef struct {
- * sok_ctx_walk_pos_cb_t on_size,
- * on_wall;
- * sok_ctx_walk_tile_cb_t on_home,
- * on_box;
- * sok_ctx_walk_goal_cb_t on_goal;
- * sok_ctx_walk_move_cb_t on_move;
- * } sok_ctx_walk_cbs_t;
- *
- * _Bool sok_ctx_walk(
- * const sok_ctx_t * const,
- * const sok_ctx_walk_cbs_t * const,
- * void * const
- * );
- */
-
static void
-draw(
- SDL_Renderer * const renderer,
- const sok_ctx_t * const ctx,
- const size_t level_num,
- const level_t * const level
+solve_on_error(
+ const char * const err
) {
- // clear
- const SDL_Color c = get_bg(ctx);
- SDL_SetRenderDrawColor(renderer, c.r, c.g, c.b, c.a);
- SDL_RenderClear(renderer);
-
- sok_ctx_walk(ctx, &DRAW_CBS, renderer);
-
- // flip
- SDL_RenderPresent(renderer);
+ die("Error solving level: %s", err);
}
int main(int argc, char *argv[]) {
- size_t warp_buf = 0;
- size_t level_num = (argc > 1) ? atoi(argv[1]) : 0;
+ size_t level_num = (argc > 1) ? atoi(argv[1]) : 0,
+ zoom = 0;
const level_t *level = levels_get_level(level_num);
- // init context
+ // init warp buffer
+ warp_buf_t warp_buf;
+ warp_buf_clear(&warp_buf);
+
+ // init sok context
sok_ctx_t ctx;
sok_ctx_init(&ctx, NULL);
@@ -195,102 +71,119 @@ int main(int argc, char *argv[]) {
die("SDL_CreateWindowAndRenderer(): %s", SDL_GetError());
}
+ // init draw context
+ draw_ctx_t draw_ctx = {
+ .level_num = &level_num,
+ .level = level,
+ .ctx = &ctx,
+ .renderer = renderer,
+ .zoom = &zoom,
+ };
+
bool done = false;
SDL_Event ev;
while (!done) {
while (SDL_PollEvent(&ev)) {
- switch (ev.type) {
- case SDL_QUIT:
+ const action_t action = get_action(&ev);
+
+ switch (action.type) {
+ case ACTION_NONE:
+ // do nothing
+ break;
+ case ACTION_QUIT:
done = true;
break;
- case SDL_KEYUP:
+ case ACTION_MOVE:
+ if (!sok_ctx_move(&ctx, (sok_dir_t) action.data)) {
+ warn("move %s failed", SOK_DIR_TO_STR((sok_dir_t) action.data));
+ }
+
+ break;
+ case ACTION_UNDO:
+ if (!sok_ctx_undo(&ctx)) {
+ warn("undo failed");
+ }
+
+ break;
+ case ACTION_NEXT:
+ if (sok_ctx_is_done(&ctx)) {
+ // advance level
+ level_num++;
+ level = levels_get_level(level_num);
+ draw_ctx.level = level;
+
+ // load next level
+ if (!sok_ctx_set_level(&ctx, level->data)) {
+ die("Couldn't load level %zu", level_num);
+ }
+ } else {
+ warn("cannot advance to next level");
+ }
+
+ break;
+ case ACTION_RESET:
+ // reset level
+ if (!sok_ctx_set_level(&ctx, level->data)) {
+ die("Couldn't load level %zu", level_num);
+ }
+
+ break;
+ case ACTION_WARP:
+ if (warp_buf_get(&warp_buf, &level_num)) {
+ level = levels_get_level(level_num);
+ draw_ctx.level = level;
+
+ // load level
+ if (!sok_ctx_set_level(&ctx, level->data)) {
+ die("Couldn't load level %zu", level_num);
+ }
+
+ // clear warp buf
+ warp_buf_clear(&warp_buf);
+ }
+
+ break;
+ case ACTION_WARP_BUF_PUSH:
+ warp_buf_push_num(&warp_buf, action.data);
+
+ break;
+ case ACTION_WARP_BUF_POP:
+ warp_buf_pop_num(&warp_buf);
+
+ break;
+ case ACTION_SOLVE:
{
- const action_t action = get_action(ev.key.keysym.sym);
-
- switch (action.type) {
- case ACTION_QUIT:
- done = true;
- break;
- case ACTION_MOVE:
- if (!sok_ctx_move(&ctx, (sok_dir_t) action.data)) {
- warn("move %s failed", SOK_DIR_TO_STR((sok_dir_t) action.data));
- }
-
- break;
- case ACTION_UNDO:
- if (!sok_ctx_undo(&ctx)) {
- warn("undo failed");
- }
-
- break;
- case ACTION_NEXT:
- if (sok_ctx_is_done(&ctx)) {
- // advance level
- level_num++;
- level = levels_get_level(level_num);
-
- // load next level
- if (!sok_ctx_set_level(&ctx, level->data)) {
- die("Couldn't load level %zu", level_num);
- }
- } else {
- warn("cannot advance to next level");
- }
-
- break;
- case ACTION_RESET:
- // reset level
- if (!sok_ctx_set_level(&ctx, level->data)) {
- die("Couldn't load level %zu", level_num);
- }
-
- break;
- case ACTION_WARP:
- level_num = warp_buf;
- level = levels_get_level(level_num);
-
- // load level
- if (!sok_ctx_set_level(&ctx, level->data)) {
- die("Couldn't load level %zu", level_num);
- }
-
- warp_buf = 0;
-
- break;
- case ACTION_WARP_BUF_PUSH:
- warp_buf = 10 * warp_buf + action.data;
-
- break;
- case ACTION_WARP_BUF_POP:
- warp_buf /= 10;
-
- break;
- case ACTION_SOLVE:
- {
- // get current number of moves
- const size_t old_num_moves = ctx.num_moves;
-
- if (sok_solve(&ctx, solve_on_error)) {
- // found solution, print it
- draw_moves(&ctx, old_num_moves);
- } else {
- warn("Couldn't solve level");
- }
- }
- default:
- // ignore
- break;
+ // get current number of moves
+ const size_t old_num_moves = ctx.num_moves;
+
+ if (sok_solve(&ctx, solve_on_error)) {
+ // found solution, print it
+ draw_moves(&ctx, old_num_moves);
+ } else {
+ warn("Couldn't solve level");
}
}
break;
+ case ACTION_ZOOM_IN:
+ if (zoom < 10) {
+ zoom++;
+ }
+
+ break;
+ case ACTION_ZOOM_OUT:
+ if (zoom > 0) {
+ zoom--;
+ }
+
+ break;
default:
// ignore
break;
}
}
- draw(renderer, &ctx, level_num, level);
+ draw(&draw_ctx);
}
// fini renderer, window
diff --git a/src/sdl/util.h b/src/sdl/util.h
new file mode 100644
index 0000000..8d7e130
--- /dev/null
+++ b/src/sdl/util.h
@@ -0,0 +1,13 @@
+#ifndef UTIL_H
+#define UTIL_H
+
+#define warn(...) do { \
+ SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__); \
+} while (0)
+
+#define die(...) do { \
+ SDL_LogCritical(SDL_LOG_CATEGORY_APPLICATION, __VA_ARGS__); \
+ exit(EXIT_FAILURE); \
+} while (0)
+
+#endif /* UTIL_H */
diff --git a/src/sdl/warp-buf.c b/src/sdl/warp-buf.c
new file mode 100644
index 0000000..5168c7f
--- /dev/null
+++ b/src/sdl/warp-buf.c
@@ -0,0 +1,52 @@
+#include <stdbool.h>
+#include "warp-buf.h"
+
+void
+warp_buf_clear(
+ warp_buf_t * const buf
+) {
+ buf->len = 0;
+ buf->dst = 0;
+}
+
+bool
+warp_buf_has_num(
+ const warp_buf_t * const buf
+) {
+ return buf->len > 0;
+}
+
+void
+warp_buf_push_num(
+ warp_buf_t * const buf,
+ const size_t num
+) {
+ buf->dst = buf->dst * 10 + num;
+ buf->len++;
+}
+
+void
+warp_buf_pop_num(
+ warp_buf_t * const buf
+) {
+ if (buf->len > 0) {
+ buf->len--;
+ buf->dst /= 10;
+ }
+}
+
+bool
+warp_buf_get(
+ const warp_buf_t * const buf,
+ size_t * const r
+) {
+ if (!buf->len) {
+ return false;
+ }
+
+ if (r) {
+ *r = buf->dst;
+ }
+
+ return true;
+}
diff --git a/src/sdl/warp-buf.h b/src/sdl/warp-buf.h
new file mode 100644
index 0000000..5e00e5d
--- /dev/null
+++ b/src/sdl/warp-buf.h
@@ -0,0 +1,17 @@
+#ifndef WARP_BUF_H
+#define WARP_BUF_H
+
+#include <stddef.h> // size_t
+
+typedef struct {
+ size_t len,
+ dst;
+} warp_buf_t;
+
+void warp_buf_clear(warp_buf_t * const);
+_Bool warp_buf_has_num(const warp_buf_t * const);
+void warp_buf_push_num(warp_buf_t * const, const size_t);
+void warp_buf_pop_num(warp_buf_t * const);
+_Bool warp_buf_get(const warp_buf_t * const, size_t * const);
+
+#endif /* WARP_BUF_H */