summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorcitrons <citrons@mondecitronne.com>2023-10-11 23:58:12 -0500
committercitrons <citrons@mondecitronne.com>2023-10-11 23:58:12 -0500
commitab32d51871248e0fbf6758a425eda2256712d519 (patch)
treeba48813a3a75f79eea5c867496d77fdb8a3251db
initial commit
-rw-r--r--Makefile19
-rw-r--r--die.h13
-rw-r--r--generator.c68
-rw-r--r--generator.h10
-rw-r--r--main.c92
-rw-r--r--random.c26
-rw-r--r--random.h9
-rw-r--r--save.c130
-rw-r--r--save.h14
-rw-r--r--world.c306
-rw-r--r--world.h94
11 files changed, 781 insertions, 0 deletions
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..b0f651d
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,19 @@
+SDL_HEADERS=/usr/include/SDL2
+
+CFLAGS=-g -Wall -pedantic -std=c99 $$(sdl2-config --cflags)
+LFLAGS=$$(sdl2-config --libs)
+
+findgame: main.o world.o random.o generator.o save.o
+ $(CC) -o $@ $^ $(LFLAGS)
+
+main.o: world.h random.h
+world.o: world.h random.h generator.h save.h
+generator.o: world.h random.h
+save.o: world.h die.h
+
+.SUFFIXES: .c .o
+.c.o:
+ $(CC) $(CFLAGS) -c -o $@ $<
+
+clean:
+ rm -f *.o findgame
diff --git a/die.h b/die.h
new file mode 100644
index 0000000..5048ced
--- /dev/null
+++ b/die.h
@@ -0,0 +1,13 @@
+#include <SDL.h>
+
+#include <stdlib.h>
+
+#define die(...) (SDL_Log(__VA_ARGS__), abort())
+static inline void sdl_error_assert(SDL_bool condition) {
+ if (!condition) {
+ SDL_Log("%s", SDL_GetError());
+ // banish to hell forever
+ abort();
+ }
+}
+#define sdl_error_assert(c) (sdl_error_assert(c && SDL_TRUE))
diff --git a/generator.c b/generator.c
new file mode 100644
index 0000000..6cf3ad4
--- /dev/null
+++ b/generator.c
@@ -0,0 +1,68 @@
+#include <SDL.h>
+
+#include "world.h"
+#include "random.h"
+
+#define MAX_INFLUENCE 10
+#define SPARSITY 40.0
+#define CAVERNITY 3
+
+#define SURFACE_COLLECTIBLE_RARITY 70
+
+void generate_chunk(world *w, chunk *c) {
+ SDL_memset(c->tiles, TILE_EMPTY, sizeof(c->tiles));
+ if (c->pos.y < 0) return;
+
+ static float noise_params[CHUNK_DIM * CHUNK_DIM];
+ SDL_memset(noise_params, 0, sizeof(noise_params));
+
+ for (int i = 0; i < CHUNK_DIM * CHUNK_DIM; i++) {
+ if (rand_int() % CAVERNITY == 0) {
+ noise_params[i] =
+ SDL_pow(rand_float(), SPARSITY) * MAX_INFLUENCE;
+ }
+ }
+
+ for (int y = 0; y < CHUNK_DIM; y++) {
+ for (int x = 0; x < CHUNK_DIM; x++) {
+ float influence = noise_params[tile_index((SDL_Point) {x, y})];
+ if (c->pos.y == 0) {
+ if (y < 96) influence = 0;
+ else influence *= 1.8;
+ }
+
+ int boundary = influence + 0.5;
+ int min_x = SDL_max(x - boundary, 0);
+ int max_x = SDL_min(x + boundary, CHUNK_DIM - 1);
+ int min_y = SDL_max(y - boundary, 0);
+ int max_y = SDL_min(y + boundary, CHUNK_DIM - 1);
+
+ for (int iy = min_y; iy <= max_y; iy++) {
+ for (int ix = min_x; ix <= max_x; ix++) {
+ int dx = ix - x;
+ int dy = iy - y;
+ if (dx * dx + dy * dy <= influence * influence) {
+ if (dx != 0 || dy != 0 || influence > 0.5) {
+ c->tiles[
+ tile_index((SDL_Point) {ix, iy})] = TILE_WALL;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ int rarity = c->pos.x != 0 && c->pos.x != -1 ?
+ SURFACE_COLLECTIBLE_RARITY : 8;
+ if (c->pos.y == 0) {
+ for (int x = 0; x < CHUNK_DIM; x++) {
+ if (rand_int() % rarity == 0) {
+ int y = 0;
+
+ while (!is_solid(c->tiles[tile_index((SDL_Point) {x, y + 1})]))
+ y++;
+ c->tiles[tile_index((SDL_Point) {x, y})] = rand_int() % TILE_LIGHT;
+ }
+ }
+ }
+}
diff --git a/generator.h b/generator.h
new file mode 100644
index 0000000..557b0cb
--- /dev/null
+++ b/generator.h
@@ -0,0 +1,10 @@
+#ifndef GENERATOR_H
+#define GENERATOR_H
+
+#include <SDL.h>
+
+#include "world.h"
+
+void generate_chunk(world *w, chunk *c);
+
+#endif
diff --git a/main.c b/main.c
new file mode 100644
index 0000000..766292c
--- /dev/null
+++ b/main.c
@@ -0,0 +1,92 @@
+#include <SDL.h>
+
+#include "world.h"
+#include "random.h"
+
+static SDL_Window *win;
+static SDL_Renderer *rend;
+
+static world game = {0};
+
+static int run() {
+ SDL_Event e;
+ while (SDL_PollEvent(&e)) {
+ seed_rand(e.common.timestamp);
+
+ switch (e.type) {
+ case SDL_QUIT:
+ save_world(&game);
+ return 0;
+ case SDL_KEYDOWN:
+ break;
+ case SDL_KEYUP:
+ break;
+ default:
+ break;
+ }
+ }
+
+ int nkeys;
+ const Uint8 *k = SDL_GetKeyboardState(&nkeys);
+
+ if (k[SDL_SCANCODE_W])
+ player_walk(&game, 0, -1);
+ if (k[SDL_SCANCODE_S])
+ player_walk(&game, 0, 1);
+ if (k[SDL_SCANCODE_A])
+ player_walk(&game, -1, 0);
+ if (k[SDL_SCANCODE_D])
+ player_walk(&game, 1, 0);
+
+ if (!k[SDL_SCANCODE_LSHIFT]) {
+ if (k[SDL_SCANCODE_UP])
+ player_place(&game, 0, -1);
+ if (k[SDL_SCANCODE_DOWN])
+ player_place(&game, 0, 1);
+ if (k[SDL_SCANCODE_LEFT])
+ player_place(&game, -1, 0);
+ if (k[SDL_SCANCODE_RIGHT])
+ player_place(&game, 1, 0);
+ } else {
+ if (k[SDL_SCANCODE_UP])
+ player_destroy(&game, 0, -1);
+ if (k[SDL_SCANCODE_DOWN])
+ player_destroy(&game, 0, 1);
+ if (k[SDL_SCANCODE_LEFT])
+ player_destroy(&game, -1, 0);
+ if (k[SDL_SCANCODE_RIGHT])
+ player_destroy(&game, 1, 0);
+ }
+
+ tick_world(&game);
+ draw_world(&game, win, rend);
+ SDL_RenderPresent(rend);
+ SDL_Delay(1);
+ return 1;
+}
+
+int main(int argc, char *argv[]) {
+ if (SDL_Init(SDL_INIT_VIDEO) < 0) {
+ SDL_Log("error initializing SDL: %s", SDL_GetError());
+ return 1;
+ }
+ win = SDL_CreateWindow("find",
+ SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 960, 720,
+ SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE);
+ if (!win) {
+ SDL_Log("error creating window: %s", SDL_GetError());
+ return 1;
+ }
+ rend = SDL_CreateRenderer(win, -1, SDL_RENDERER_PRESENTVSYNC);
+ if (!rend) {
+ SDL_Log("error creating renderer: %s", SDL_GetError());
+ return 1;
+ }
+
+ init_world(&game);
+ while (run());
+
+ SDL_DestroyRenderer(rend);
+ SDL_DestroyWindow(win);
+ return 0;
+}
diff --git a/random.c b/random.c
new file mode 100644
index 0000000..c53d378
--- /dev/null
+++ b/random.c
@@ -0,0 +1,26 @@
+#include <SDL.h>
+
+static Uint64 random = 0xbee;
+
+void seed_rand(Uint64 seed) {
+ random ^= seed;
+}
+
+Uint64 get_rand() {
+ random ^= random >> 7;
+ random ^= random << 9;
+ random ^= random >> 13;
+ return random;
+}
+
+int rand_int() {
+ Uint64 rand = get_rand();
+ int result;
+ memcpy(&result, &rand, sizeof(int));
+ result = SDL_abs(result);
+ return result;
+}
+
+double rand_float() {
+ return (get_rand() >> 11) * 0x1.0p-53;
+}
diff --git a/random.h b/random.h
new file mode 100644
index 0000000..18a032b
--- /dev/null
+++ b/random.h
@@ -0,0 +1,9 @@
+#ifndef RANDOM_H
+#define RANDOM_H
+
+void seed_rand(Uint64 seed);
+Uint64 get_rand(void);
+int rand_int(void);
+double rand_float(void);
+
+#endif
diff --git a/save.c b/save.c
new file mode 100644
index 0000000..13c2368
--- /dev/null
+++ b/save.c
@@ -0,0 +1,130 @@
+#include <SDL.h>
+
+#include "world.h"
+#include "die.h"
+
+typedef char filename[4096];
+
+#define MAGIC 0xBEE10D5
+#define SCHEMA 0
+
+static const char *pref_path() {
+ static char *path = NULL;
+ if (!path) path = SDL_GetPrefPath("citrons", "findgame");
+ sdl_error_assert(path);
+ return path;
+}
+
+static void get_chunk_filename(SDL_Point chunk_pos, filename name) {
+ int written = SDL_snprintf(name, sizeof(filename), "%schunk.%d.%d.dat",
+ pref_path(), chunk_pos.x, chunk_pos.y);
+ if (written >= sizeof(filename)) die("filename too large\n");
+}
+
+static void get_world_filename(filename name) {
+ int written = SDL_snprintf(name, sizeof(filename), "%sworld.dat", pref_path());
+ if (written >= sizeof(filename)) die("filename too large\n");
+}
+
+static void write_header(SDL_RWops *file) {
+ Uint32 magic = MAGIC;
+ Uint32 schema = 0;
+ sdl_error_assert(SDL_RWwrite(file, &magic, sizeof(magic), 1));
+ sdl_error_assert(SDL_RWwrite(file, &schema, sizeof(schema), 1));
+}
+
+static void read_header(SDL_RWops *file) {
+ Uint32 magic;
+ Uint32 schema;
+ sdl_error_assert(SDL_RWread(file, &magic, sizeof(magic), 1));
+ if (magic != MAGIC) die("invalid save file");
+ sdl_error_assert(SDL_RWread(file, &schema, sizeof(schema), 1));
+ if (schema != SCHEMA) die("invalid save file");
+}
+
+void save_chunk(world *w, chunk *c) {
+ SDL_assert(c->loaded);
+
+ filename name;
+ get_chunk_filename(c->pos, name);
+
+ SDL_RWops *file = SDL_RWFromFile(name, "wb");
+ sdl_error_assert(file);
+ write_header(file);
+
+ sdl_error_assert(
+ SDL_RWwrite(file, c->tiles, sizeof(c->tiles), 1));
+
+ sdl_error_assert(SDL_RWclose(file) >= 0);
+}
+
+void save_world(world *w) {
+ filename name;
+ get_world_filename(name);
+
+ SDL_RWops *file = SDL_RWFromFile(name, "wb");
+ sdl_error_assert(file);
+ write_header(file);
+
+ sdl_error_assert(
+ SDL_RWwrite(file, &w->player.pos, sizeof(w->player.pos), 1));
+ sdl_error_assert(
+ SDL_RWwrite(file, &w->player.scores, sizeof(w->player.scores), 1));
+
+ sdl_error_assert(SDL_RWclose(file) >= 0);
+
+ for (int i = 0; i < MAX_CHUNKS; i++) {
+ if (w->chunks[i].loaded)
+ save_chunk(w, &w->chunks[i]);
+ }
+}
+
+SDL_bool is_chunk_saved(world *w, SDL_Point chunk_pos) {
+ filename name;
+ get_chunk_filename(chunk_pos, name);
+
+ SDL_RWops *file = SDL_RWFromFile(name, "a+b");
+ sdl_error_assert(file);
+ Sint64 len = SDL_RWseek(file, 0, RW_SEEK_END);
+ SDL_RWclose(file);
+
+ return len != 0;
+}
+
+SDL_bool load_saved_chunk(world *w, chunk *c) {
+ if (!is_chunk_saved(w, c->pos)) return SDL_FALSE;
+
+ filename name;
+ get_chunk_filename(c->pos, name);
+
+ SDL_RWops *file = SDL_RWFromFile(name, "rb");
+ read_header(file);
+
+ sdl_error_assert(
+ SDL_RWread(file, c->tiles, sizeof(c->tiles), 1));
+
+ SDL_RWclose(file);
+ return SDL_TRUE;
+}
+
+SDL_bool load_world(world *w) {
+ filename name;
+ get_world_filename(name);
+
+ SDL_RWops *file = SDL_RWFromFile(name, "a+b");
+ sdl_error_assert(file);
+ if (SDL_RWseek(file, 0, RW_SEEK_END) == 0) {
+ SDL_RWclose(file);
+ return SDL_FALSE;
+ }
+ SDL_RWseek(file, 0, RW_SEEK_SET);
+ read_header(file);
+
+ sdl_error_assert(
+ SDL_RWread(file, &w->player.pos, sizeof(w->player.pos), 1));
+ sdl_error_assert(
+ SDL_RWread(file, &w->player.scores, sizeof(w->player.scores), 1));
+
+ SDL_RWclose(file);
+ return SDL_TRUE;
+}
diff --git a/save.h b/save.h
new file mode 100644
index 0000000..3d67b01
--- /dev/null
+++ b/save.h
@@ -0,0 +1,14 @@
+#ifndef SAVE_H
+#define SAVE_H
+
+#include <SDL.h>
+
+#include "world.h"
+
+void save_chunk(world *w, chunk *c);
+SDL_bool is_chunk_saved(world *w, SDL_Point chunk_pos);
+SDL_bool load_saved_chunk(world *w, chunk *c);
+SDL_bool load_world(world *w);
+void save_world(world *w);
+
+#endif
diff --git a/world.c b/world.c
new file mode 100644
index 0000000..8e22470
--- /dev/null
+++ b/world.c
@@ -0,0 +1,306 @@
+#include <SDL.h>
+
+#include "world.h"
+#include "random.h"
+#include "generator.h"
+#include "save.h"
+
+SDL_Color tile_colors[] = {
+ [TILE_RED] = {0xFF, 0x00, 0x00, 0xFF},
+ [TILE_GREEN] = {0x00, 0xFF, 0x00, 0xFF},
+ [TILE_BLUE] = {0x00, 0x00, 0xFF, 0xFF},
+ [TILE_LIGHT] = {0xFF, 0xFF, 0xFF, 0xFF},
+ [TILE_EMPTY] = {0x00, 0x00, 0x00, 0xFF},
+ [TILE_WALL] = {0x50, 0x50, 0x50, 0xFF},
+};
+
+chunk *get_chunk(world *w, SDL_Point chunk_pos) {
+ for (int i = 0; i < MAX_CHUNKS; i++) {
+ chunk *c = &w->chunks[i];
+ if (!c->loaded) continue;
+ if (c->pos.x == chunk_pos.x && c->pos.y == chunk_pos.y) {
+ c->active = SDL_TRUE;
+ return c;
+ }
+ }
+ return NULL;
+}
+
+SDL_bool chunk_exists(world *w, SDL_Point chunk_pos) {
+ if (get_chunk(w, chunk_pos)) return SDL_TRUE;
+ if (is_chunk_saved(w, chunk_pos)) return SDL_TRUE;
+ return SDL_FALSE;
+}
+
+static void unload_chunk(world *w, chunk *c) {
+ save_chunk(w, c);
+ c->active = SDL_FALSE;
+ c->loaded = SDL_FALSE;
+}
+
+static void deactivate_chunk(chunk *c) {
+ c->active = SDL_FALSE;
+}
+
+chunk *load_chunk(world *w, SDL_Point chunk_pos) {
+ chunk *c = get_chunk(w, chunk_pos);
+ if (c) return c;
+
+ static int last_loaded = 0;
+ for (int i = 0; i < MAX_CHUNKS; i++) {
+ chunk *wc = &w->chunks[(last_loaded + i) % MAX_CHUNKS];
+ if (!wc->active) {
+ last_loaded = i;
+ c = wc;
+ break;
+ }
+ }
+ if (!c) return NULL;
+
+ if (c->loaded) unload_chunk(w, c);
+
+ c->pos = chunk_pos;
+ c->active = SDL_TRUE;
+ c->dirty = SDL_TRUE;
+ c->loaded = SDL_TRUE;
+ if (!load_saved_chunk(w, c))
+ generate_chunk(w, c);
+
+ return c;
+}
+
+SDL_Texture *render_chunk(SDL_Renderer *rend, chunk *c) {
+ if (!c->tex) {
+ c->tex = SDL_CreateTexture(rend,
+ SDL_PIXELFORMAT_RGBA32, SDL_TEXTUREACCESS_STREAMING,
+ CHUNK_DIM, CHUNK_DIM);
+ SDL_assert(c->tex);
+ c->dirty = SDL_TRUE;
+ }
+ if (c->dirty) {
+ int pitch;
+ void *out;
+ SDL_assert(SDL_LockTexture(c->tex, NULL, &out, &pitch) >= 0);
+ SDL_Color *pixels = out;
+
+ for (int i = 0; i < CHUNK_DIM * CHUNK_DIM; i++)
+ pixels[i] = tile_colors[c->tiles[i]];
+
+ SDL_UnlockTexture(c->tex);
+ c->dirty = SDL_FALSE;
+ }
+ return c->tex;
+}
+
+tile get_tile(world *w, SDL_Point world_pos) {
+ chunk *c = load_chunk(w, chunk_pos_at(world_pos));
+ SDL_assert(c);
+ return c->tiles[tile_index(pos_in_chunk(world_pos))];
+}
+
+void set_tile(world *w, SDL_Point world_pos, tile t) {
+ chunk *c = load_chunk(w, chunk_pos_at(world_pos));
+ SDL_assert(c);
+ c->tiles[tile_index(pos_in_chunk(world_pos))] = t;
+ c->dirty = SDL_TRUE;
+}
+
+SDL_bool is_solid(tile t) {
+ return t == TILE_WALL;
+}
+
+SDL_bool player_grounded(world *w) {
+ if (SDL_TICKS_PASSED(SDL_GetTicks(), w->player.stamina_time))
+ return SDL_FALSE;
+ SDL_bool grounded = SDL_FALSE;
+ for (int y = w->player.pos.y; y < w->player.pos.y + 2; y++) {
+ for (int x = w->player.pos.x - 1; x < w->player.pos.x + 2; x++)
+ grounded = grounded || is_solid(get_tile(w, (SDL_Point) {x, y}));
+ }
+ return grounded;
+}
+
+void player_walk(world *w, int x, int y) {
+ if (!SDL_TICKS_PASSED(SDL_GetTicks(), w->player.walk_wait))
+ return;
+
+ if (x != 0 || y != 0) {
+ SDL_Point new_pos = w->player.pos;
+ new_pos.x += x;
+ if (player_grounded(w))
+ new_pos.y += y;
+ if (is_solid(get_tile(w, new_pos))) {
+ if (y == 0 && player_grounded(w)) {
+ new_pos = w->player.pos;
+ new_pos.y -= 1;
+ if (!is_solid(get_tile(w, new_pos)))
+ w->player.pos = new_pos;
+ }
+ } else w->player.pos = new_pos;
+ w->player.walk_wait = SDL_GetTicks() + PLAYER_WALK_DELAY;
+ }
+}
+
+void player_collect(world *w, SDL_Point pos) {
+ tile t = get_tile(w, pos);
+ if (t < MAX_COLLECTIBLE) {
+ w->player.scores[t] += 1;
+ set_tile(w, pos, TILE_EMPTY);
+ }
+}
+
+void player_place(world *w, int x, int y) {
+ if (!player_grounded(w)) return;
+ if (!SDL_TICKS_PASSED(SDL_GetTicks(), w->player.place_wait)) return;
+ if (w->player.scores[TILE_LIGHT] < 1) return;
+
+ w->player.scores[TILE_LIGHT]--;
+ w->player.place_wait = SDL_GetTicks() + PLAYER_PLACE_DELAY;
+
+ SDL_Point place = {w->player.pos.x + x, w->player.pos.y + y};
+ if (!is_solid(get_tile(w, place))) {
+ player_collect(w, place);
+ set_tile(w, place, TILE_WALL);
+ } else {
+ SDL_Point push = {w->player.pos.x - x, w->player.pos.y - y};
+ if (!is_solid(get_tile(w, push))) {
+ set_tile(w, w->player.pos, TILE_WALL);
+ w->player.pos = push;
+ }
+ }
+}
+
+
+void player_destroy(world *w, int x, int y) {
+ if (!SDL_TICKS_PASSED(SDL_GetTicks(), w->player.place_wait)) return;
+
+ SDL_Point destroy = {w->player.pos.x + x, w->player.pos.y + y};
+ tile t = get_tile(w, destroy);
+
+ if (t != TILE_EMPTY) {
+ if (t < MAX_COLLECTIBLE) {
+ player_collect(w, destroy);
+ } else {
+ if (w->player.scores[TILE_LIGHT] < 1) return;
+ w->player.scores[TILE_LIGHT]--;
+
+ set_tile(w, destroy, TILE_EMPTY);
+ w->player.place_wait = SDL_GetTicks() + PLAYER_PLACE_DELAY;
+ }
+ }
+}
+
+void draw_world(world *w, SDL_Window *win, SDL_Renderer *rend) {
+ int sw, sh;
+ SDL_GetWindowSize(win, &sw, &sh);
+
+ double view_scale;
+ if (sh < sw)
+ view_scale = (double) sh / (double) VIEW_DIM;
+ else
+ view_scale = (double) sw / (double) VIEW_DIM;
+
+ double view_width = (double) sw / view_scale;
+ double view_height = (double) sh / view_scale;
+ double view_x = w->view_pos.x - view_width / 2;
+ double view_y = w->view_pos.y - view_height / 2;
+
+ SDL_Rect view_rect = {view_x, view_y, view_width + 1, view_height + 1};
+ for (int i = 0; i < MAX_CHUNKS; i++) {
+ chunk *c = &w->chunks[i];
+ if (c->active) {
+ SDL_Rect chunk_rect = {
+ c->pos.x * CHUNK_DIM, c->pos.y * CHUNK_DIM,
+ CHUNK_DIM, CHUNK_DIM,
+ };
+ if (!SDL_HasIntersection(&view_rect, &chunk_rect))
+ deactivate_chunk(c);
+ }
+ }
+
+ SDL_Point min = chunk_pos_at((SDL_Point) {view_x, view_y});
+ SDL_Point max = chunk_pos_at(
+ (SDL_Point) {view_x + view_width, view_y + view_height});
+ for (int y = min.y; y <= max.y; y++) {
+ for (int x = min.x; x <= max.x; x++) {
+ chunk *c = load_chunk(w, (SDL_Point) {x, y});
+ if (!c) continue;
+ SDL_Texture *tex = render_chunk(rend, c);
+
+ int world_x = x * CHUNK_DIM;
+ int world_y = y * CHUNK_DIM;
+ int draw_x = ((double) world_x - view_x) * view_scale;
+ int draw_y = ((double) world_y - view_y) * view_scale;
+ SDL_Rect draw_rect = {
+ draw_x, draw_y,
+ (double) CHUNK_DIM * view_scale + 0.5,
+ (double) CHUNK_DIM * view_scale + 0.5,
+ };
+ SDL_RenderCopy(rend, tex, NULL, &draw_rect);
+ }
+ }
+
+ int player_x = (w->player.pos.x - view_x) * view_scale;
+ int player_y = (w->player.pos.y - view_y) * view_scale;
+ SDL_Rect player_rect = {
+ player_x, player_y, view_scale + 0.5, view_scale + 0.5,
+ };
+ SDL_SetRenderDrawColor(rend, 0xFF, 0x00, 0xFF, 0xFF);
+ SDL_RenderFillRect(rend, &player_rect);
+}
+
+void tick_world(world *w) {
+ get_rand(); // shuffle RNG
+
+ player_collect(w, w->player.pos);
+ int to_combine = SDL_min(w->player.scores[TILE_RED],
+ SDL_min(w->player.scores[TILE_GREEN], w->player.scores[TILE_BLUE]));
+ w->player.scores[TILE_LIGHT] += to_combine;
+ w->player.scores[TILE_RED] -= to_combine;
+ w->player.scores[TILE_GREEN] -= to_combine;
+ w->player.scores[TILE_BLUE] -= to_combine;
+
+ tile below = get_tile(w,
+ (SDL_Point) {w->player.pos.x, w->player.pos.y + 1});
+ if (is_solid(below))
+ w->player.stamina_time = SDL_GetTicks() + PLAYER_STAMINA;
+
+ if (!player_grounded(w)) {
+ if (SDL_TICKS_PASSED(SDL_GetTicks(), w->player.gravity_time)) {
+ SDL_Point new_pos = {w->player.pos.x, w->player.pos.y + 1};
+ if (!is_solid(get_tile(w, new_pos)))
+ w->player.pos = new_pos;
+ w->player.gravity_time = SDL_GetTicks() + PLAYER_GRAVITY;
+ }
+ }
+
+ if (player_grounded(w)) {
+ if (w->view_pos.x < w->player.pos.x)
+ w->view_pos.x += 1;
+ else if (w->view_pos.x > w->player.pos.x)
+ w->view_pos.x -= 1;
+ if (w->view_pos.y < w->player.pos.y)
+ w->view_pos.y += 1;
+ else if (w->view_pos.y > w->player.pos.y)
+ w->view_pos.y -= 1;
+ } else {
+ if (w->view_pos.x < w->player.pos.x - VIEW_DIM / 3)
+ w->view_pos.x += 1;
+ else if (w->view_pos.x > w->player.pos.x + VIEW_DIM / 3)
+ w->view_pos.x -= 1;
+ if (w->view_pos.y < w->player.pos.y - VIEW_DIM / 3)
+ w->view_pos.y += 1;
+ else if (w->view_pos.y > w->player.pos.y + VIEW_DIM / 3)
+ w->view_pos.y -= 1;
+ }
+
+ if (SDL_TICKS_PASSED(SDL_GetTicks(), w->save_time)) {
+ save_world(w);
+ w->save_time = SDL_GetTicks() + SAVE_INTERVAL;
+ }
+}
+
+void init_world(world *w) {
+ load_world(w);
+ w->view_pos = w->player.pos;
+}
diff --git a/world.h b/world.h
new file mode 100644
index 0000000..ad7acd2
--- /dev/null
+++ b/world.h
@@ -0,0 +1,94 @@
+#ifndef WORLD_H
+#define WORLD_H
+
+#include <SDL.h>
+
+#define CHUNK_DIM 128
+
+typedef enum tile {
+ TILE_RED,
+ TILE_GREEN,
+ TILE_BLUE,
+ TILE_LIGHT,
+ MAX_COLLECTIBLE,
+
+ TILE_EMPTY,
+ TILE_WALL,
+} tile;
+
+extern SDL_Color tile_colors[];
+
+#define PLAYER_STAMINA 1250
+#define PLAYER_WALK_DELAY 100
+#define PLAYER_PLACE_DELAY 175
+#define PLAYER_GRAVITY 30
+typedef struct player {
+ SDL_Point pos;
+ int scores[MAX_COLLECTIBLE];
+ int stamina_time;
+ int walk_wait;
+ int place_wait;
+ int gravity_time;
+} player;
+
+typedef struct chunk {
+ SDL_Point pos;
+ Uint8 tiles[CHUNK_DIM * CHUNK_DIM];
+ SDL_Texture *tex;
+ SDL_bool dirty;
+ SDL_bool loaded;
+ SDL_bool active;
+} chunk;
+
+#define MAX_CHUNKS 16
+#define SAVE_INTERVAL 120000
+typedef struct world {
+ player player;
+ SDL_Point view_pos;
+ chunk chunks[MAX_CHUNKS];
+ int save_time;
+} world;
+
+static inline SDL_Point chunk_pos_at(SDL_Point world_pos) {
+ return (SDL_Point) {
+ SDL_floor((double) world_pos.x / CHUNK_DIM),
+ SDL_floor((double) world_pos.y / CHUNK_DIM),
+ };
+}
+
+static inline SDL_Point pos_in_chunk(SDL_Point world_pos) {
+ SDL_Point cpos = chunk_pos_at(world_pos);
+ return (SDL_Point) {
+ world_pos.x - cpos.x * CHUNK_DIM,
+ world_pos.y - cpos.y * CHUNK_DIM,
+ };
+}
+
+static inline int tile_index(SDL_Point pos) {
+ SDL_assert(pos.x < CHUNK_DIM && pos.y < CHUNK_DIM);
+ return pos.y * CHUNK_DIM + pos.x;
+}
+
+chunk *load_chunk(world *w, SDL_Point chunk_pos);
+chunk *get_chunk(world *w, SDL_Point chunk_pos);
+SDL_bool chunk_exists(world *w, SDL_Point chunk_pos);
+
+tile get_tile(world *w, SDL_Point world_pos);
+void set_tile(world *w, SDL_Point world_pos, tile t);
+
+SDL_bool is_solid(tile t);
+
+SDL_bool player_grounded(world *w);
+void player_walk(world *w, int x, int y);
+void player_place(world *w, int x, int y);
+void player_destroy(world *w, int x, int y);
+void player_collect(world *w, SDL_Point pos);
+
+void tick_world(world *w);
+
+#define VIEW_DIM 40
+void draw_world(world *w, SDL_Window *win, SDL_Renderer *rend);
+
+void init_world(world *w);
+
+#endif