aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--src/cli-main.c193
-rw-r--r--src/sok-ctx.c731
-rw-r--r--src/sok-level-parser.c159
-rw-r--r--src/sok.h181
5 files changed, 1265 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..5761abc
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+*.o
diff --git a/src/cli-main.c b/src/cli-main.c
new file mode 100644
index 0000000..00853e7
--- /dev/null
+++ b/src/cli-main.c
@@ -0,0 +1,193 @@
+#include <stdbool.h> // bool
+#include <string.h> // atoi()
+#include <stdlib.h> // EXIT_{FAILURE,SUCCESS}
+#include <stdio.h>
+#include "sok.h"
+
+#define UNUSED(a) ((void) (a))
+
+static const char * const
+LEVELS[] = {
+ "#####|#@$.#|#####|",
+ "#######|###.###|###$###|#.$@$.#|###$###|###.###|#######",
+};
+#define NUM_LEVELS (sizeof(LEVELS) / sizeof(char*))
+
+static char print_buf[(SOK_LEVEL_MAX_WIDTH + 1) * SOK_LEVEL_MAX_HEIGHT + 1];
+
+static bool
+on_size(
+ const sok_ctx_t * const ctx,
+ const sok_pos_t size,
+ void * const data
+) {
+ UNUSED(ctx);
+ UNUSED(data);
+
+ // fprintf(stderr, "size: x = %u, y = %u\n", size.x, size.y);
+
+ memset(print_buf, ' ', sizeof(print_buf));
+ print_buf[(size.x + 1) * size.y + 1] = '\0';
+ for (size_t i = 0; i < size.y; i++) {
+ print_buf[(i + 1) * (size.x + 1) - 1] = '\n';
+ }
+
+ return true;
+}
+
+static bool
+on_home(
+ const sok_ctx_t * const ctx,
+ const sok_pos_t pos,
+ const bool has_goal,
+ void * const data
+) {
+ UNUSED(data);
+ print_buf[pos.y * (ctx->level.size.x + 1) + pos.x] = has_goal ? '+' : '@';
+ return true;
+}
+
+static bool
+on_wall(
+ const sok_ctx_t * const ctx,
+ const sok_pos_t pos,
+ void * const data
+) {
+ // fprintf(stderr, "wall: x = %u, y = %u\n", pos.x, pos.y);
+ UNUSED(data);
+ print_buf[pos.y * (ctx->level.size.x + 1) + pos.x] = '#';
+ return true;
+}
+
+static bool
+on_goal(
+ const sok_ctx_t * const ctx,
+ const sok_pos_t pos,
+ const bool has_player,
+ const bool has_box,
+ void * const data
+) {
+ UNUSED(data);
+ const char c = has_player ? '+' : (has_box ? '*' : '.');
+ print_buf[pos.y * (ctx->level.size.x + 1) + pos.x] = c;
+ return true;
+}
+
+static bool
+on_box(
+ const sok_ctx_t * const ctx,
+ const sok_pos_t pos,
+ const bool has_goal,
+ void * const data
+) {
+ UNUSED(data);
+ print_buf[pos.y * (ctx->level.size.x + 1) + pos.x] = has_goal ? '*' : '$';
+ return true;
+}
+
+static sok_ctx_walk_cbs_t PRINT_CBS = {
+ .on_size = on_size,
+ .on_home = on_home,
+ .on_wall = on_wall,
+ .on_goal = on_goal,
+ .on_box = on_box,
+};
+
+static void
+print_level(
+ const sok_ctx_t * const ctx
+) {
+ // fill buffer
+ if (!sok_ctx_walk(ctx, &PRINT_CBS, NULL)) {
+ fprintf(stderr, "Couldn't print level\n");
+ exit(EXIT_FAILURE);
+ }
+
+ // print level
+ printf("%s\n", print_buf);
+}
+
+int main(int argc, char *argv[]) {
+ size_t level = (argc > 1) ? atoi(argv[1]) : 0;
+
+ // init context
+ sok_ctx_t ctx;
+ sok_ctx_init(&ctx, NULL);
+
+ if (!sok_ctx_set_level(&ctx, LEVELS[level])) {
+ fprintf(stderr, "Couldn't load level %zu\n", level);
+ return EXIT_FAILURE;
+ }
+
+ char buf[1024];
+ bool done = false;
+ while (!done) {
+ print_level(&ctx);
+ for (size_t i = 0; i < ctx.level.num_boxes; i++) {
+ fprintf(stderr, "ctx->boxes[i] = {%u, %u}\n", ctx.boxes[i].x, ctx.boxes[i].y);
+ }
+ printf("%zu%s> ", ctx.num_moves, sok_ctx_is_done(&ctx) ? " (won!)" : "");
+
+ if (!fgets(buf, sizeof(buf), stdin)) {
+ done = true;
+ break;
+ }
+
+ switch (buf[0]) {
+ case EOF:
+ case 'q':
+ done = true;
+ break;
+ case 'k':
+ if (!sok_ctx_move(&ctx, SOK_DIR_UP)) {
+ fprintf(stderr, "W: move up failed\n");
+ }
+
+ break;
+ case 'h':
+ if (!sok_ctx_move(&ctx, SOK_DIR_LEFT)) {
+ fprintf(stderr, "W: move left failed\n");
+ }
+
+ break;
+ case 'j':
+ if (!sok_ctx_move(&ctx, SOK_DIR_DOWN)) {
+ fprintf(stderr, "W: move down failed\n");
+ }
+
+ break;
+ case 'l':
+ if (!sok_ctx_move(&ctx, SOK_DIR_RIGHT)) {
+ fprintf(stderr, "W: move right failed\n");
+ }
+
+ break;
+ case 'u':
+ if (!sok_ctx_undo(&ctx)) {
+ fprintf(stderr, "W: undo failed\n");
+ }
+
+ break;
+ case 'n':
+ if (sok_ctx_is_done(&ctx)) {
+ // advance level
+ level = (level + 1) % NUM_LEVELS;
+
+ // load next level
+ if (!sok_ctx_set_level(&ctx, LEVELS[level])) {
+ fprintf(stderr, "Couldn't load level %zu\n", level);
+ return EXIT_FAILURE;
+ }
+ } else {
+ fprintf(stderr, "W: cannot advance to next level\n");
+ }
+
+ break;
+ default:
+ // ignore
+ break;
+ }
+ }
+
+ return 0;
+}
diff --git a/src/sok-ctx.c b/src/sok-ctx.c
new file mode 100644
index 0000000..e4a922b
--- /dev/null
+++ b/src/sok-ctx.c
@@ -0,0 +1,731 @@
+#include <stdbool.h>
+#include <stdio.h> // debugging
+#include <string.h> // memset()
+#include "sok.h"
+
+#define UNUSED(a) ((void) (a))
+
+#define VALID_POS(pos) ( \
+ (pos).x < SOK_LEVEL_MAX_WIDTH && \
+ (pos).y < SOK_LEVEL_MAX_HEIGHT \
+)
+
+#define POINTS_EQUAL(a, b) ( \
+ ((a).x == (b).x) && \
+ ((a).y == (b).y) \
+)
+
+static bool
+on_size(
+ const sok_level_parser_t * const parser,
+ const sok_pos_t pos
+) {
+ sok_ctx_t *ctx = (sok_ctx_t*) parser->user_data;
+
+ if (!VALID_POS(pos)) {
+ // size out of bounds
+ return false;
+ }
+
+ // save width and height
+ ctx->level.size = pos;
+
+ // return success
+ return true;
+}
+
+static bool
+on_home(
+ const sok_level_parser_t * const parser,
+ const sok_pos_t pos
+) {
+ sok_ctx_t *ctx = (sok_ctx_t*) parser->user_data;
+
+ if (!VALID_POS(pos)) {
+ // position out of bounds
+ return false;
+ }
+
+ // save start position
+ ctx->level.home = pos;
+
+ // return success
+ return true;
+}
+
+static bool
+on_wall(
+ const sok_level_parser_t * const parser,
+ const sok_pos_t pos
+) {
+ sok_ctx_t *ctx = (sok_ctx_t*) parser->user_data;
+
+ if (!VALID_POS(pos)) {
+ // position out of bounds
+ return false;
+ }
+
+ // save wall
+ ctx->level.walls[pos.y * ctx->level.size.x + pos.x] = true;
+
+ // return sucess
+ return true;
+}
+
+static bool
+on_goal(
+ const sok_level_parser_t * const parser,
+ const sok_pos_t pos
+) {
+ sok_ctx_t *ctx = (sok_ctx_t*) parser->user_data;
+
+ if (!VALID_POS(pos)) {
+ // position out of bounds
+ return false;
+ }
+
+ if (ctx->level.num_goals >= SOK_LEVEL_MAX_GOALS - 1) {
+ // too many goals
+ return false;
+ }
+
+ // save goal
+ ctx->level.goals[ctx->level.num_goals++] = pos;
+
+ // return sucess
+ return true;
+}
+
+static bool
+on_box(
+ const sok_level_parser_t * const parser,
+ const sok_pos_t pos
+) {
+ sok_ctx_t *ctx = (sok_ctx_t*) parser->user_data;
+
+ if (!VALID_POS(pos)) {
+ // position out of bounds
+ return false;
+ }
+
+ if (ctx->level.num_boxes >= SOK_LEVEL_MAX_BOXES - 1) {
+ // too many boxes
+ return false;
+ }
+
+ // save box
+ ctx->level.boxes[ctx->level.num_boxes++] = pos;
+
+ // return sucess
+ return true;
+}
+
+static bool
+on_junk(
+ const sok_level_parser_t * const parser,
+ const size_t ofs,
+ const char tile
+) {
+ UNUSED(parser);
+ UNUSED(ofs);
+ UNUSED(tile);
+
+ // TODO
+
+ // return sucess
+ return true;
+}
+
+static const sok_level_parser_cbs_t
+LEVEL_PARSER_CBS = {
+ .on_size = on_size,
+ .on_home = on_home,
+ .on_wall = on_wall,
+ .on_goal = on_goal,
+ .on_box = on_box,
+ .on_junk = on_junk,
+};
+
+void
+sok_ctx_init(sok_ctx_t * const ctx, void *user_data) {
+ memset(ctx, 0, sizeof(sok_ctx_t));
+ ctx->user_data = user_data;
+}
+
+bool
+sok_ctx_set_level(
+ sok_ctx_t * const ctx,
+ const char * const str
+) {
+
+ // clear level
+ ctx->level.num_goals = 0;
+ ctx->level.num_boxes = 0;
+ memset(ctx->level.walls, 0, SOK_LEVEL_MAX_WIDTH * SOK_LEVEL_MAX_HEIGHT);
+
+ // parse level
+ sok_level_parser_t parser;
+ sok_level_parser_init(&parser, &LEVEL_PARSER_CBS, ctx);
+ if (!sok_level_parser_parse(&parser, str, strlen(str))) {
+ // return failure
+ return false;
+ }
+
+ // init moves
+ ctx->num_moves = 0;
+
+ // init player
+ ctx->home = ctx->level.home;
+
+ // init boxes
+ memcpy(ctx->boxes, ctx->level.boxes, ctx->level.num_boxes * sizeof(sok_pos_t));
+
+ // return success
+ return true;
+}
+
+static bool
+tile_is_floor(
+ const sok_ctx_t * const ctx,
+ const sok_pos_t pos
+) {
+ return (
+ (pos.x < SOK_LEVEL_MAX_WIDTH - 1) &&
+ (pos.y < SOK_LEVEL_MAX_HEIGHT - 1) &&
+ !(ctx->level.walls[pos.y * ctx->level.size.x + pos.x])
+ );
+}
+
+static bool
+tile_is_box(
+ const sok_ctx_t * const ctx,
+ const sok_pos_t pos
+) {
+ for (size_t i = 0; i < ctx->level.num_boxes; i++) {
+ // fprintf(stderr, "D: ctx->boxes[%zu] = {%u, %u}\n", i, ctx->boxes[i].x, ctx->boxes[i].y);
+ if (POINTS_EQUAL(ctx->boxes[i], pos)) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+static bool
+tile_is_goal(
+ const sok_ctx_t * const ctx,
+ const sok_pos_t pos
+) {
+ for (size_t i = 0; i < ctx->level.num_goals; i++) {
+ if (POINTS_EQUAL(pos, ctx->level.goals[i])) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+static bool
+sok_can_move(
+ sok_ctx_t * const ctx,
+ const sok_dir_t dir
+) {
+ if (ctx->num_moves >= SOK_CTX_MAX_MOVES - 1) {
+ // no more move slots available
+ return false;
+ }
+
+ if (dir == SOK_DIR_UP) {
+ const sok_pos_t ps[2] = {
+ { .x = ctx->home.x, .y = ctx->home.y - 1 },
+ { .x = ctx->home.x, .y = ctx->home.y - 2 },
+ };
+
+ fprintf(
+ stderr,
+ "D: ctx->home = {%u, %u}, ps[] = {{%u, %u}, {%u, %u}};\n"
+ "D: tile_is_floor(ctx, ps[0]) = %c, tile_is_box(ctx, ps[0]) = %c\n"
+ "D: tile_is_floor(ctx, ps[1]) = %c, tile_is_box(ctx, ps[1]) = %c\n"
+ "",
+ ctx->home.x, ctx->home.y,
+ ps[0].x, ps[0].y,
+ ps[1].x, ps[1].y,
+ tile_is_floor(ctx, ps[0]) ? 't' : 'f',
+ tile_is_box(ctx, ps[0]) ? 't' : 'f',
+ tile_is_floor(ctx, ps[1]) ? 't' : 'f',
+ tile_is_box(ctx, ps[1]) ? 't' : 'f'
+ );
+
+ return ((
+ (ctx->home.y > 0) &&
+ tile_is_floor(ctx, ps[0]) &&
+ !tile_is_box(ctx, ps[0])
+ ) || (
+ (ctx->home.y > 1) &&
+ tile_is_floor(ctx, ps[0]) &&
+ tile_is_box(ctx, ps[0]) &&
+ tile_is_floor(ctx, ps[1]) &&
+ !tile_is_box(ctx, ps[1])
+ ));
+ } else if (dir == SOK_DIR_DOWN) {
+ const sok_pos_t ps[2] = {
+ { .x = ctx->home.x, .y = ctx->home.y + 1 },
+ { .x = ctx->home.x, .y = ctx->home.y + 2 },
+ };
+
+ return ((
+ (ctx->home.y < SOK_LEVEL_MAX_HEIGHT - 1) &&
+ tile_is_floor(ctx, ps[0]) &&
+ !tile_is_box(ctx, ps[0])
+ ) || (
+ (ctx->home.y < SOK_LEVEL_MAX_HEIGHT - 2) &&
+ tile_is_floor(ctx, ps[0]) &&
+ tile_is_box(ctx, ps[0]) &&
+ tile_is_floor(ctx, ps[1]) &&
+ !tile_is_box(ctx, ps[1])
+ ));
+ } else if (dir == SOK_DIR_LEFT) {
+ const sok_pos_t ps[2] = {
+ { .x = ctx->home.x - 1, .y = ctx->home.y },
+ { .x = ctx->home.x - 2, .y = ctx->home.y },
+ };
+
+ return ((
+ (ctx->home.x > 0) &&
+ tile_is_floor(ctx, ps[0]) &&
+ !tile_is_box(ctx, ps[0])
+ ) || (
+ (ctx->home.x > 1) &&
+ tile_is_floor(ctx, ps[0]) &&
+ tile_is_box(ctx, ps[0]) &&
+ tile_is_floor(ctx, ps[1]) &&
+ !tile_is_box(ctx, ps[1])
+ ));
+ } else if (dir == SOK_DIR_RIGHT) {
+ const sok_pos_t ps[2] = {
+ { .x = ctx->home.x + 1, .y = ctx->home.y },
+ { .x = ctx->home.x + 2, .y = ctx->home.y },
+ };
+
+ return ((
+ (ctx->home.x < SOK_LEVEL_MAX_WIDTH - 1) &&
+ tile_is_floor(ctx, ps[0]) &&
+ !tile_is_box(ctx, ps[0])
+ ) || (
+ (ctx->home.x < SOK_LEVEL_MAX_WIDTH - 2) &&
+ tile_is_floor(ctx, ps[0]) &&
+ tile_is_box(ctx, ps[0]) &&
+ tile_is_floor(ctx, ps[1]) &&
+ !tile_is_box(ctx, ps[1])
+ ));
+ }
+
+ // return failure
+ return false;
+}
+
+static bool
+sok_ctx_push_move(
+ sok_ctx_t * const ctx,
+ const sok_dir_t dir,
+ const bool is_push
+) {
+ if (ctx->num_moves >= SOK_CTX_MAX_MOVES - 1) {
+ return false;
+ }
+
+ ctx->moves[ctx->num_moves].pos = ctx->home;
+ ctx->moves[ctx->num_moves].dir = dir;
+ ctx->moves[ctx->num_moves].is_push = is_push;
+ ctx->num_moves++;
+
+ return true;
+}
+
+static bool
+sok_ctx_pop_move(
+ sok_ctx_t * const ctx,
+ sok_move_t * const ret
+) {
+ if (!ctx->num_moves) {
+ // return failure
+ return false;
+ }
+
+ // decriment moves
+ ctx->num_moves--;
+
+ if (ret) {
+ // save popped move
+ *ret = ctx->moves[ctx->num_moves];
+ }
+
+ // return success
+ return true;
+}
+
+static bool
+sok_ctx_move_box(
+ sok_ctx_t * const ctx,
+ const sok_pos_t old_pos,
+ const sok_pos_t new_pos
+) {
+ for (size_t i = 0; i < ctx->level.num_boxes; i++) {
+ if (POINTS_EQUAL(ctx->boxes[i], old_pos)) {
+ ctx->boxes[i] = new_pos;
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool
+sok_ctx_is_done(
+ sok_ctx_t * const ctx
+) {
+ for (size_t i = 0; i < ctx->level.num_goals; i++) {
+ bool on_goal = false;
+
+ for (size_t j = 0; !on_goal && j < ctx->level.num_boxes; j++) {
+ if (POINTS_EQUAL(ctx->level.goals[i], ctx->boxes[j])) {
+ // found box on this goal
+ on_goal = true;
+ }
+ }
+
+ if (!on_goal) {
+ // no box on this goal, return false
+ return false;
+ }
+ }
+
+ // return success
+ return true;
+}
+
+bool
+sok_ctx_move(
+ sok_ctx_t * const ctx,
+ const sok_dir_t dir
+) {
+ if (!sok_can_move(ctx, dir)) {
+ fprintf(stderr, "can_move failed\n");
+ return false;
+ }
+
+ if (dir == SOK_DIR_UP) {
+ const sok_pos_t ps[2] = {
+ { .x = ctx->home.x, .y = ctx->home.y - 1 },
+ { .x = ctx->home.x, .y = ctx->home.y - 2 },
+ };
+
+ if (
+ (ctx->home.y > 0) &&
+ tile_is_floor(ctx, ps[0]) &&
+ !tile_is_box(ctx, ps[0])
+ ) {
+ // push move
+ if (!sok_ctx_push_move(ctx, dir, false)) {
+ return false;
+ }
+
+ // update position
+ ctx->home.y--;
+
+ // return success
+ return true;
+ } else if (
+ (ctx->home.y > 1) &&
+ tile_is_floor(ctx, ps[0]) &&
+ tile_is_box(ctx, ps[0]) &&
+ tile_is_floor(ctx, ps[1]) &&
+ !tile_is_box(ctx, ps[1])
+ ) {
+ // push move
+ if (!sok_ctx_push_move(ctx, dir, true)) {
+ return false;
+ }
+
+ // move box
+ if (!sok_ctx_move_box(ctx, ps[0], ps[1])) {
+ return false;
+ }
+
+ // update position
+ ctx->home.y--;
+
+ // return success
+ return true;
+ }
+ } else if (dir == SOK_DIR_DOWN) {
+ const sok_pos_t ps[2] = {
+ { .x = ctx->home.x, .y = ctx->home.y + 1 },
+ { .x = ctx->home.x, .y = ctx->home.y + 2 },
+ };
+
+ if (
+ (ctx->home.y < SOK_LEVEL_MAX_HEIGHT - 1) &&
+ tile_is_floor(ctx, ps[0]) &&
+ !tile_is_box(ctx, ps[0])
+ ) {
+ // push move
+ if (!sok_ctx_push_move(ctx, dir, false)) {
+ return false;
+ }
+
+ // update position
+ ctx->home.y++;
+
+ // return success
+ return true;
+ } else if (
+ (ctx->home.y < SOK_LEVEL_MAX_HEIGHT - 2) &&
+ tile_is_floor(ctx, ps[0]) &&
+ tile_is_box(ctx, ps[0]) &&
+ tile_is_floor(ctx, ps[1]) &&
+ !tile_is_box(ctx, ps[1])
+ ) {
+ // push move
+ if (!sok_ctx_push_move(ctx, dir, true)) {
+ return false;
+ }
+
+ // move box
+ if (!sok_ctx_move_box(ctx, ps[0], ps[1])) {
+ return false;
+ }
+
+ // update position
+ ctx->home.y++;
+
+ // return success
+ return true;
+ }
+ } else if (dir == SOK_DIR_LEFT) {
+ const sok_pos_t ps[2] = {
+ { .x = ctx->home.x - 1, .y = ctx->home.y },
+ { .x = ctx->home.x - 2, .y = ctx->home.y },
+ };
+
+ if (
+ (ctx->home.x > 1) &&
+ tile_is_floor(ctx, ps[0]) &&
+ !tile_is_box(ctx, ps[0])
+ ) {
+ // push move
+ if (!sok_ctx_push_move(ctx, dir, false)) {
+ return false;
+ }
+
+ // update position
+ ctx->home.x--;
+
+ // return success
+ return true;
+ } else if (
+ (ctx->home.x > 1) &&
+ tile_is_floor(ctx, ps[0]) &&
+ tile_is_box(ctx, ps[0]) &&
+ tile_is_floor(ctx, ps[1]) &&
+ !tile_is_box(ctx, ps[1])
+ ) {
+ // push move
+ if (!sok_ctx_push_move(ctx, dir, true)) {
+ return false;
+ }
+
+ // move box
+ if (!sok_ctx_move_box(ctx, ps[0], ps[1])) {
+ return false;
+ }
+
+ // update position
+ ctx->home.x--;
+
+ // return success
+ return true;
+ }
+ } else if (dir == SOK_DIR_RIGHT) {
+ const sok_pos_t ps[2] = {
+ { .x = ctx->home.x + 1, .y = ctx->home.y },
+ { .x = ctx->home.x + 2, .y = ctx->home.y },
+ };
+
+ if (
+ (ctx->home.x < SOK_LEVEL_MAX_WIDTH - 1) &&
+ tile_is_floor(ctx, ps[0]) &&
+ !tile_is_box(ctx, ps[0])
+ ) {
+ // push move
+ if (!sok_ctx_push_move(ctx, dir, false)) {
+ return false;
+ }
+
+ // update position
+ ctx->home.x++;
+
+ // return success
+ return true;
+ } else if (
+ (ctx->home.x < SOK_LEVEL_MAX_WIDTH - 2) &&
+ tile_is_floor(ctx, ps[0]) &&
+ tile_is_box(ctx, ps[0]) &&
+ tile_is_floor(ctx, ps[1]) &&
+ !tile_is_box(ctx, ps[1])
+ ) {
+ // push move
+ if (!sok_ctx_push_move(ctx, dir, true)) {
+ return false;
+ }
+
+ // move box
+ if (!sok_ctx_move_box(ctx, ps[0], ps[1])) {
+ return false;
+ }
+
+ // update position
+ ctx->home.x++;
+
+ // return success
+ return true;
+ }
+ }
+
+ // return failure
+ return false;
+}
+
+bool
+sok_ctx_undo(
+ sok_ctx_t * const ctx
+) {
+ sok_move_t move;
+ if (!sok_ctx_pop_move(ctx, &move)) {
+ // return failure
+ return false;
+ }
+
+ if (move.is_push) {
+ switch (move.dir) {
+ case SOK_DIR_UP:
+ {
+ const sok_pos_t box_pos = { move.pos.x, move.pos.y - 2 };
+ sok_ctx_move_box(ctx, box_pos, ctx->home);
+ }
+
+ break;
+ case SOK_DIR_DOWN:
+ {
+ const sok_pos_t box_pos = { move.pos.x, move.pos.y + 2 };
+ sok_ctx_move_box(ctx, box_pos, ctx->home);
+ }
+
+ break;
+ case SOK_DIR_LEFT:
+ {
+ const sok_pos_t box_pos = { move.pos.x - 2, move.pos.y };
+ sok_ctx_move_box(ctx, box_pos, ctx->home);
+ }
+
+ break;
+ case SOK_DIR_RIGHT:
+ {
+ const sok_pos_t box_pos = { move.pos.x + 2, move.pos.y };
+ sok_ctx_move_box(ctx, box_pos, ctx->home);
+ }
+
+ break;
+ default:
+ // never reached
+ return false;
+ }
+ }
+
+ // update position
+ ctx->home = move.pos;
+
+ // return success
+ return true;
+}
+
+bool sok_ctx_walk(
+ const sok_ctx_t * const ctx,
+ const sok_ctx_walk_cbs_t * const cbs,
+ void * const data
+) {
+ if (!cbs) {
+ return false;
+ }
+
+ if (cbs->on_size) {
+ // emit size
+ if (!cbs->on_size(ctx, ctx->level.size, data)) {
+ return false;
+ }
+ }
+
+ if (cbs->on_home) {
+ const bool has_goal = tile_is_goal(ctx, ctx->home);
+
+ // emit home
+ if (!cbs->on_home(ctx, ctx->home, has_goal, data)) {
+ return false;
+ }
+ }
+
+ if (cbs->on_wall) {
+ // walk walls
+ for (size_t i = 0; i < (ctx->level.size.x * ctx->level.size.y); i++) {
+ if (ctx->level.walls[i]) {
+ const sok_pos_t pos = {
+ .y = i / ctx->level.size.x,
+ .x = i % ctx->level.size.x,
+ };
+
+ // emit wall
+ if (!cbs->on_wall(ctx, pos, data)) {
+ return false;
+ }
+ }
+ }
+ }
+
+ if (cbs->on_goal) {
+ // walk goals
+ for (size_t i = 0; i < ctx->level.num_goals; i++) {
+ const bool has_player = POINTS_EQUAL(ctx->level.goals[i], ctx->home);
+ const bool has_box = tile_is_box(ctx, ctx->level.goals[i]);
+
+ // emit goal
+ if (!cbs->on_goal(ctx, ctx->level.goals[i], has_player, has_box, data)) {
+ return false;
+ }
+ }
+ }
+
+ if (cbs->on_box) {
+ // walk boxes
+ for (size_t i = 0; i < ctx->level.num_boxes; i++) {
+ const bool has_goal = tile_is_goal(ctx, ctx->boxes[i]);
+
+ // emit box
+ if (!cbs->on_box(ctx, ctx->boxes[i], has_goal, data)) {
+ return false;
+ }
+ }
+ }
+
+ if (cbs->on_move) {
+ // walk moves
+ for (size_t i = 0; i < ctx->num_moves; i++) {
+ // emit move
+ if (!cbs->on_move(ctx, ctx->moves[i], data)) {
+ return false;
+ }
+ }
+ }
+
+ // return success
+ return true;
+}
diff --git a/src/sok-level-parser.c b/src/sok-level-parser.c
new file mode 100644
index 0000000..eec08c3
--- /dev/null
+++ b/src/sok-level-parser.c
@@ -0,0 +1,159 @@
+#include <stdbool.h>
+#include <stddef.h> // size_t
+#include "sok.h"
+
+#define UNUSED(a) ((void) (a))
+
+void
+sok_level_parser_init(
+ sok_level_parser_t * const parser,
+ const sok_level_parser_cbs_t * const cbs,
+ void * const user_data
+) {
+ parser->cbs = cbs;
+ parser->user_data = user_data;
+}
+
+bool
+sok_level_parser_parse(
+ sok_level_parser_t * const parser,
+ const char * const buf,
+ const size_t buf_len
+) {
+ sok_pos_t size = { .x = 0, .y = 0 };
+
+ for (size_t i = 0, w = 0; i < buf_len; i++) {
+ if (buf[i] == '|') {
+ if (w > size.x) {
+ size.x = w;
+ }
+
+ // reset column position, increment row count
+ w = 0;
+ size.y++;
+ } else {
+ w++;
+ }
+ }
+
+ // increment row count
+ size.y++;
+
+ // emit level size
+ if (
+ parser->cbs->on_size &&
+ !parser->cbs->on_size(parser, size)
+ ) {
+ // return failure
+ return false;
+ }
+
+ sok_pos_t pos = { 0, 0 };
+ for (size_t i = 0; i < buf_len; i++) {
+ switch (buf[i]) {
+ case '|':
+ // new line
+ pos.x = 0;
+ pos.y++;
+
+ break;
+ case '-':
+ case '_':
+ case ' ':
+ // advance
+ pos.x++;
+
+ break;
+ case '+':
+ // emit goal
+ if (parser->cbs->on_goal && !parser->cbs->on_goal(parser, pos)) {
+ // return failure
+ return false;
+ }
+
+ // emit home
+ if (parser->cbs->on_home && !parser->cbs->on_home(parser, pos)) {
+ // return failure
+ return false;
+ }
+
+ // advance
+ pos.x++;
+
+ break;
+ case '@':
+ // emit home
+ if (parser->cbs->on_home && !parser->cbs->on_home(parser, pos)) {
+ // return failure
+ return false;
+ }
+
+ // advance
+ pos.x++;
+
+ break;
+ case '#':
+ // emit wall
+ if (parser->cbs->on_wall && !parser->cbs->on_wall(parser, pos)) {
+ // return failure
+ return false;
+ }
+
+ // advance
+ pos.x++;
+
+ break;
+ case '*':
+ // emit goal
+ if (parser->cbs->on_goal && !parser->cbs->on_goal(parser, pos)) {
+ // return failure
+ return false;
+ }
+
+ // emit box
+ if (parser->cbs->on_box && !parser->cbs->on_box(parser, pos)) {
+ // return failure
+ return false;
+ }
+
+ // advance
+ pos.x++;
+
+ break;
+ case '.':
+ // emit goal
+ if (parser->cbs->on_goal && !parser->cbs->on_goal(parser, pos)) {
+ // return failure
+ return false;
+ }
+
+ // advance
+ pos.x++;
+
+ break;
+ case '$':
+ // emit box
+ if (parser->cbs->on_box && !parser->cbs->on_box(parser, pos)) {
+ // return failure
+ return false;
+ }
+
+ // advance
+ pos.x++;
+
+ break;
+ default:
+ // emit junk
+ if (parser->cbs->on_junk && !parser->cbs->on_junk(parser, i, buf[i])) {
+ // return failure
+ return false;
+ }
+
+ // return failure
+ return false;
+ }
+ }
+
+ // return success
+ return true;
+}
diff --git a/src/sok.h b/src/sok.h
new file mode 100644
index 0000000..7635ead
--- /dev/null
+++ b/src/sok.h
@@ -0,0 +1,181 @@
+#ifndef SOK_H
+#define SOK_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif /* __cplusplus */
+
+#include <stdint.h> // uint16_t
+
+/*********/
+/* types */
+/*********/
+
+typedef struct {
+ uint16_t x, y;
+} sok_pos_t;
+
+typedef enum {
+ SOK_DIR_RIGHT,
+ SOK_DIR_UP,
+ SOK_DIR_LEFT,
+ SOK_DIR_DOWN,
+ SOK_DIR_LAST,
+} sok_dir_t;
+
+typedef struct {
+ sok_pos_t pos;
+ sok_dir_t dir;
+ _Bool is_push;
+} sok_move_t;
+
+/****************/
+/* level parser */
+/****************/
+
+typedef struct sok_level_parser_t_ sok_level_parser_t;
+
+typedef bool (*sok_level_parser_pos_cb_t)(
+ const sok_level_parser_t * const,
+ const sok_pos_t
+);
+
+typedef bool (*sok_level_parser_junk_cb_t)(
+ const sok_level_parser_t * const,
+ const size_t,
+ const char
+);
+
+typedef struct {
+ sok_level_parser_pos_cb_t on_size,
+ on_home,
+ on_wall,
+ on_goal,
+ on_box;
+ sok_level_parser_junk_cb_t on_junk;
+} sok_level_parser_cbs_t;
+
+struct sok_level_parser_t_ {
+ const sok_level_parser_cbs_t *cbs;
+ void *user_data;
+};
+
+void sok_level_parser_init(
+ sok_level_parser_t * const parser,
+ const sok_level_parser_cbs_t * const cbs,
+ void * const user_data
+);
+
+bool sok_level_parser_parse(
+ sok_level_parser_t * const parser,
+ const char * const buf,
+ const size_t buf_len
+);
+
+/*********/
+/* level */
+/*********/
+
+#define SOK_LEVEL_MAX_WIDTH (1 << 8)
+#define SOK_LEVEL_MAX_HEIGHT (1 << 8)
+#define SOK_LEVEL_MAX_BOXES 64
+#define SOK_LEVEL_MAX_GOALS 64
+
+typedef struct {
+ sok_pos_t size;
+ _Bool walls[SOK_LEVEL_MAX_WIDTH * SOK_LEVEL_MAX_HEIGHT];
+
+ // player home position
+ sok_pos_t home;
+
+ // boxes
+ sok_pos_t boxes[SOK_LEVEL_MAX_BOXES];
+ size_t num_boxes;
+
+ // goals
+ sok_pos_t goals[SOK_LEVEL_MAX_GOALS];
+ size_t num_goals;
+} sok_level_t;
+
+/***********/
+/* context */
+/***********/
+
+#define SOK_CTX_MAX_MOVES 1024
+
+typedef struct {
+ sok_level_t level;
+
+ sok_move_t moves[SOK_CTX_MAX_MOVES];
+ size_t num_moves;
+
+ // player position
+ sok_pos_t home;
+
+ // box positions
+ sok_pos_t boxes[SOK_LEVEL_MAX_BOXES];
+
+ // user data
+ void *user_data;
+} sok_ctx_t;
+
+void sok_ctx_init(sok_ctx_t * const ctx, void *user_data);
+
+_Bool sok_ctx_set_level(sok_ctx_t * const ctx, const char * const level);
+
+_Bool sok_ctx_is_done(sok_ctx_t * const);
+
+_Bool sok_ctx_move(sok_ctx_t * const, const sok_dir_t);
+_Bool sok_ctx_undo(sok_ctx_t * const);
+
+/******************/
+/* context walker */
+/******************/
+
+typedef _Bool (*sok_ctx_walk_pos_cb_t)(
+ const sok_ctx_t * const,
+ const sok_pos_t,
+ void * const
+);
+
+typedef _Bool (*sok_ctx_walk_tile_cb_t)(
+ const sok_ctx_t * const,
+ const sok_pos_t,
+ const _Bool,
+ void * const
+);
+
+typedef _Bool (*sok_ctx_walk_goal_cb_t)(
+ const sok_ctx_t * const,
+ const sok_pos_t,
+ const _Bool has_player,
+ const _Bool has_box,
+ void * const
+);
+
+typedef _Bool (*sok_ctx_walk_move_cb_t)(
+ const sok_ctx_t * const,
+ const sok_move_t,
+ void * const
+);
+
+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
+);
+
+#ifdef __cplusplus
+};
+#endif /* __cplusplus */
+
+#endif /* SOK_H */