Back to shtanton's homepage
summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/all.c692
-rw-r--r--src/img.c95
-rw-r--r--src/index.html.in234
-rw-r--r--src/levels.c488
-rw-r--r--src/tick.c376
-rw-r--r--src/types.c166
6 files changed, 1921 insertions, 130 deletions
diff --git a/src/all.c b/src/all.c
index e8e5b11..cf52eca 100644
--- a/src/all.c
+++ b/src/all.c
@@ -1,103 +1,525 @@
+#ifndef INCLUDE_ALL_C
+#define INCLUDE_ALL_C
+
#include <stddef.h>
#include <stdint.h>
-typedef struct {
- int x, y, w, h;
- unsigned char r, g, b, a;
-} DrawElement;
+#include "types.c"
+#include "levels.c"
+#include "tick.c"
+
+static const Color colors[N_COLORS][4] = {
+ {
+ {255, 255, 255, 255},
+ {255, 255, 255, 255},
+ {255, 255, 255, 255},
+ {255, 255, 255, 255},
+ },
+ {
+ {0, 0, 0, 255},
+ {0, 0, 0, 255},
+ {0, 0, 0, 255},
+ {0, 0, 0, 255},
+ },
+ {
+ {255, 0, 0, 255},
+ {255, 0, 0, 255},
+ {255, 0, 0, 255},
+ {255, 0, 0, 255},
+ },
+ {
+ {255, 255, 0, 255},
+ {255, 255, 0, 255},
+ {255, 255, 0, 255},
+ {255, 255, 0, 255},
+ },
+ {
+ {255, 0, 0, 255},
+ {255, 0, 0, 255},
+ {255, 255, 0, 255},
+ {255, 255, 0, 255},
+ },
+ {
+ {255, 255, 0, 255},
+ {255, 255, 0, 255},
+ {255, 0, 0, 255},
+ {255, 0, 0, 255},
+ },
+ {
+ {255, 0, 0, 255},
+ {255, 255, 0, 255},
+ {255, 0, 0, 255},
+ {255, 255, 0, 255},
+ },
+ {
+ {255, 255, 0, 255},
+ {255, 0, 0, 255},
+ {255, 255, 0, 255},
+ {255, 0, 0, 255},
+ },
+ {
+ {0, 0, 255, 255},
+ {0, 0, 255, 255},
+ {0, 0, 255, 255},
+ {0, 0, 255, 255},
+ },
+ {
+ {0, 0, 255, 255},
+ {0, 0, 255, 255},
+ {255, 255, 255, 255},
+ {255, 255, 255, 255},
+ },
+ {
+ {255, 255, 255, 255},
+ {255, 255, 255, 255},
+ {0, 0, 255, 255},
+ {0, 0, 255, 255},
+ },
+ {
+ {0, 0, 255, 255},
+ {255, 255, 255, 255},
+ {0, 0, 255, 255},
+ {255, 255, 255, 255},
+ },
+ {
+ {255, 255, 255, 255},
+ {0, 0, 255, 255},
+ {255, 255, 255, 255},
+ {0, 0, 255, 255},
+ },
+};
-typedef struct {
- int len;
- DrawElement els[256];
-} DrawList;
+#define BUTTON_SIZE 32
+#define BUTTON_SPACING 16
+#define BUTTON_SPACER(i) (BUTTON_SPACING + BUTTON_SIZE + (BUTTON_SIZE * (i) + (i) * BUTTON_SPACING))
-#define MEM_SIZE (1<<16)
+// reverse order as they're anchored to bottom
+static const Button buttons[N_BUTTONS] = {
+ [BUTTON_BACK] = {.x = BUTTON_SPACING, .y = BUTTON_SPACER(BUTTON_BACK), .w = BUTTON_SIZE, .h = BUTTON_SIZE},
+ [BUTTON_RETRY] = {.x = BUTTON_SPACING, .y = BUTTON_SPACER(BUTTON_RETRY), .w = BUTTON_SIZE, .h = BUTTON_SIZE},
+ [BUTTON_PLAY] = {.x = BUTTON_SPACING, .y = BUTTON_SPACER(BUTTON_PLAY), .w = BUTTON_SIZE, .h = BUTTON_SIZE},
+ [BUTTON_CONTINUE] = {.x = BUTTON_SPACING, .y = BUTTON_SPACER(BUTTON_CONTINUE), .w = BUTTON_SIZE, .h = BUTTON_SIZE},
+};
-typedef struct {
- char *start;
- char *end;
-} Arena;
-
-#define new(a, c, t) ((t *) alloc(a, c, sizeof(t), _Alignof(t)))
-#define affirm(c) while (!(c)) *(volatile int *)0 = 0
-
-static void *alloc(Arena *a, ptrdiff_t count, ptrdiff_t size, ptrdiff_t align) {
- ptrdiff_t pad = -(size_t) a->start & (align - 1);
- affirm(count < (a->end - a->start - pad) / size);
- char *r = a->start + pad;
- a->start += pad + size * count;
- for (ptrdiff_t i = 0; i < count * size; i++) {
- r[i] = 0;
+// This returns a positive color index or a negated image index
+static int getPlaceAction(int currentColor, int cellx, int celly, float cellWidth, float cellHeight) {
+ int hoverColor = EMPTY;
+
+ // TODO yellow when there are no reds available
+ switch (currentColor) {
+ case BLACK:
+ if (cellx < cellWidth / 4 && cellx < celly && cellx < cellHeight - celly) {
+ hoverColor = RED_LEFT;
+ } else if (celly < cellHeight / 4 && celly < cellx && celly < cellWidth - cellx) {
+ hoverColor = RED_UP;
+ } else if (cellx > cellWidth / 4 * 3 && cellWidth - cellx < celly && cellWidth - cellx < cellHeight - celly) {
+ hoverColor = RED_RIGHT;
+ } else if (celly > cellHeight / 4 * 3 && cellHeight - celly < cellx && cellHeight - celly < cellWidth - cellx) {
+ hoverColor = RED_DOWN;
+ } else {
+ hoverColor = RED;
+ }
+ break;
+ case EMPTY:
+ hoverColor = BLACK;
+ break;
}
- return r;
+ return hoverColor;
}
-typedef struct {
- int width, height;
-} UI;
+static DrawList *render(State *state, UI *ui, Arena *a) {
+ DrawList *drawList = new(a, 1, DrawList);
-typedef struct {
- int x, y;
-} State;
-
-// Mirror these in src/index.html.in
-enum {
- INPUT_NONE,
- INPUT_UP,
- INPUT_DOWN,
- INPUT_LEFT,
- INPUT_RIGHT,
-};
+ float cellWidth = (float) (ui->width - GRID_OFFSET_X) / GRIDWIDTH;
+ float cellHeight = (float) ui->height / GRIDHEIGHT;
+
+ // Actual cells
+ for (int x = 0; x < GRIDWIDTH; x++) {
+ for (int y = 0; y < GRIDHEIGHT; y++) {
+ if (colorImages[(int) state->grid[x + GRIDWIDTH * y]]) {
+ drawList->els[drawList->len++] = (DrawElement) {
+ .x = cellWidth * x + GRID_OFFSET_X,
+ .y = cellHeight * y,
+ .w = cellWidth,
+ .h = cellHeight,
+ .image = (ImageRef) {
+ .index = colorImages[(int) state->grid[x + GRIDWIDTH * y]],
+ .opacity = 255,
+ },
+ };
+ } else {
+ for (int subx = 0; subx < 2; subx++) {
+ for (int suby = 0; suby < 2; suby++) {
+ drawList->els[drawList->len++] = (DrawElement) {
+ .x = cellWidth * x + GRID_OFFSET_X + subx * cellWidth / 2,
+ .y = cellHeight * y + suby * cellHeight / 2,
+ .w = cellWidth / 2 + (1 - subx),
+ .h = cellHeight / 2 + (1 - suby),
+ .fill = colors[(int) state->grid[x + GRIDWIDTH * y]][subx + 2 * suby],
+ };
+ }
+ }
+ }
+ }
+ }
-typedef struct {
- State state;
- UI ui;
- int input;
-} Game;
+ // Vertical grid lines
+ for (int x = 1; x < GRIDWIDTH; x++) {
+ drawList->els[drawList->len++] = (DrawElement) {
+ .x = cellWidth * x + GRID_OFFSET_X,
+ .y = 0,
+ .w = 1,
+ .h = ui->height,
+ .fill = {0, 0, 0, 255},
+ };
+ }
+
+ // Horizontal grid lines
+ for (int y = 1; y < GRIDHEIGHT; y++) {
+ drawList->els[drawList->len++] = (DrawElement) {
+ .x = GRID_OFFSET_X,
+ .y = y * cellHeight,
+ .w = ui->width - GRID_OFFSET_X,
+ .h = 1,
+ .fill = {0, 0, 0, 255},
+ };
+ }
-static DrawList *render(State state, UI *ui, Arena *a) {
- (void) ui;
+ // Spawner
+ if (state->spawnerx != 0 && state->spawnery != 0) {
+ drawList->els[drawList->len++] = (DrawElement) {
+ .x = cellWidth * state->spawnerx + GRID_OFFSET_X,
+ .y = cellHeight * state->spawnery,
+ .w = cellWidth,
+ .h = cellHeight,
+ .fill = {0, 0, 255, 63},
+ .border = {0, 0, 255, 255},
+ };
+ }
- DrawList *drawList = new(a, 1, DrawList);
- drawList->len = 1;
- drawList->els[0] = (DrawElement) {
- .x = state.x,
- .y = state.y,
- .w = 100,
- .h = 100,
- .r = 255,
- .g = 0,
- .b = 0,
- .a = 255,
+ // Goal
+ if (state->levelComplete) {
+ drawList->els[drawList->len++] = (DrawElement) {
+ .x = cellWidth * state->goalx + GRID_OFFSET_X,
+ .y = cellHeight * state->goaly,
+ .w = cellWidth,
+ .h = cellHeight,
+ .fill = {0, 255, 0, 63},
+ .border = {0, 255, 0, 255},
+ };
+ } else {
+ drawList->els[drawList->len++] = (DrawElement) {
+ .x = cellWidth * state->goalx + GRID_OFFSET_X,
+ .y = cellHeight * state->goaly,
+ .w = cellWidth,
+ .h = cellHeight,
+ .fill = {255, 0, 0, 63},
+ .border = {255, 0, 0, 255},
+ };
+
+ if (state->need9) {
+ for (int i = 0; i < state->blues; i++) {
+ drawList->els[drawList->len++] = (DrawElement) {
+ .x = cellWidth * state->goalx + GRID_OFFSET_X + cellWidth / 3 * (i % 3),
+ .y = cellHeight * state->goaly + cellHeight / 3 * (i / 3),
+ .w = cellWidth / 3,
+ .h = cellHeight / 3,
+ .fill = {196, 255, 196, 255},
+ .border = {0, 200, 0, 255},
+ };
+ }
+ }
+ }
+
+ // Hover
+ if (ui->mousex >= GRID_OFFSET_X) {
+ int hoverx = (ui->mousex - GRID_OFFSET_X) * GRIDWIDTH / (ui->width - GRID_OFFSET_X);
+ int hovery = ui->mousey * GRIDHEIGHT / ui->height;
+
+ int cellx = ui->mousex - GRID_OFFSET_X - hoverx * cellWidth;
+ int celly = ui->mousey - hovery * cellHeight;
+
+ int hoverColor = getPlaceAction(
+ state->grid[hoverx + hovery * GRIDWIDTH],
+ cellx,
+ celly,
+ cellWidth,
+ cellHeight
+ );
+
+ if (hoverColor != EMPTY) {
+ for (int i = 0; i < MAX_PLACEABLE_CELLS; i++) {
+ if (state->placeableCells[i] == hoverColor) {
+ if (colorImages[hoverColor]) {
+ drawList->els[drawList->len++] = (DrawElement) {
+ .x = cellWidth * hoverx + GRID_OFFSET_X,
+ .y = cellHeight * hovery,
+ .w = cellWidth,
+ .h = cellHeight,
+ .image = {
+ .index = colorImages[hoverColor],
+ .opacity = 127,
+ },
+ };
+ } else {
+ int subCellWidth = cellWidth / 2;
+ int subCellHeight = cellHeight / 2;
+ for (int subx = 0; subx < 2; subx++) {
+ for (int suby = 0; suby < 2; suby++) {
+ Color fill = colors[hoverColor][subx + 2 * suby];
+ fill.a = 127;
+ drawList->els[drawList->len++] = (DrawElement) {
+ .x = cellWidth * hoverx + GRID_OFFSET_X + (subx * subCellWidth),
+ .y = cellHeight * hovery + (suby * subCellHeight),
+ .w = subCellWidth,
+ .h = subCellHeight,
+ .fill = fill,
+ };
+ }
+ }
+ }
+ break;
+ }
+ }
+ }
+ }
+
+ // Side panel
+ drawList->els[drawList->len++] = (DrawElement) {
+ .x = 0,
+ .y = 0,
+ .w = GRID_OFFSET_X,
+ .h = ui->height,
+ .fill = {100, 100, 100, 255},
+ .border = {0, 0, 0, 255},
};
+ // render buttons
+ for (int i = 0; i < N_BUTTONS; i++) {
+ Color colour = {0, 0, 0, 255};
+ switch (state->buttonStates[i]) {
+ case BUTTON_STATE_IDLE:
+ colour = (Color) {0, 0, 0, 255};
+ break;
+ case BUTTON_STATE_HOVERED:
+ colour = (Color) {255, 255, 0, 255};
+ break;
+ case BUTTON_STATE_PRESSED:
+ colour = (Color) {255, 0, 0, 255};
+ break;
+ }
+ int image = IMAGE_NULL;
+ switch (i) {
+ case BUTTON_CONTINUE:
+ image = IMAGE_CONTINUE;
+ break;
+ case BUTTON_BACK:
+ image = IMAGE_EXIT;
+ break;
+ case BUTTON_RETRY:
+ image = IMAGE_RETRY;
+ break;
+ case BUTTON_PLAY:
+ image = state->playing ? IMAGE_PAUSE : IMAGE_PLAY;
+ break;
+ }
+ Color fill = {0};
+ if (i == BUTTON_CONTINUE && state->levelComplete) {
+ fill = (Color) {127, 255, 127, 200};
+ };
+ drawList->els[drawList->len++] = (DrawElement) {
+ .x = buttons[i].x,
+ .y = ui->height - buttons[i].y,
+ .w = buttons[i].w,
+ .h = buttons[i].h,
+ .fill = fill,
+ .image = (ImageRef) {
+ .index = image,
+ .opacity = 255,
+ },
+ };
+ }
+
+ // Placeable cells
+ for (int i = 0; i < MAX_PLACEABLE_CELLS; i++) {
+ if (state->placeableCells[i] == EMPTY) {
+ break;
+ }
+
+ if (colorImages[(int) state->placeableCells[i]]) {
+ drawList->els[drawList->len++] = (DrawElement) {
+ .x = BUTTON_SIZE / 2,
+ .y = BUTTON_SIZE / 2 + i * (BUTTON_SIZE + BUTTON_SPACING),
+ .w = BUTTON_SIZE,
+ .h = BUTTON_SIZE,
+ .image = {
+ .index = colorImages[(int) state->placeableCells[i]],
+ .opacity = 255,
+ },
+ };
+ } else {
+ for (int x = 0; x < 2; x++) {
+ for (int y = 0; y < 2; y++) {
+ drawList->els[drawList->len++] = (DrawElement) {
+ .x = BUTTON_SIZE / 2 + x * (BUTTON_SIZE / 2),
+ .y = BUTTON_SIZE / 2 + i * (BUTTON_SIZE + BUTTON_SPACING) + y * (BUTTON_SIZE / 2),
+ .w = BUTTON_SIZE / 2,
+ .h = BUTTON_SIZE / 2,
+ .fill = colors[(int) state->placeableCells[i]][x + 2 * y],
+ };
+ }
+ }
+ }
+ }
+
return drawList;
}
-static void update(Game *game, Arena a) {
+static void restart_level(Game* game) {
+ const int level = game->state.currentLevel;
+ xmemcpy(&game->state.grid, &levels[level].grid, sizeof(game->state.grid));
+ game->state.goalx = levels[level].goalx;
+ game->state.goaly = levels[level].goaly;
+ game->state.spawnerx = levels[level].spawnerx;
+ game->state.spawnery = levels[level].spawnery;
+ game->state.spawnerCount = 0;
+ game->state.playing = 0;
+ game->state.blues = 0;
+ game->state.need9 = levels[level].need9;
+ xmemcpy(&game->state.placeableCells, &levels[level].placeableCells, sizeof(game->state.placeableCells));
+}
+
+static int update(Game *game, uint64_t now, Arena a) {
+ const int offset_width = game->ui.width - GRID_OFFSET_X;
+
+ int tutorial = 0;
+
switch (game->input) {
- case INPUT_UP:
- game->state.y -= 20;
+ int x, y;
+ case INPUT_CLICK:
+ x = (game->ui.mousex - GRID_OFFSET_X) * GRIDWIDTH / offset_width;
+ y = game->ui.mousey * GRIDHEIGHT / game->ui.height;
+ if (game->ui.mousex >= GRID_OFFSET_X) {
+ float cellWidth = (float) (game->ui.width - GRID_OFFSET_X) / GRIDWIDTH;
+ float cellHeight = (float) game->ui.height / GRIDHEIGHT;
+ int cellx = game->ui.mousex - GRID_OFFSET_X - x * cellWidth;
+ int celly = game->ui.mousey - y * cellHeight;
+ // Keeping this around for testing purposes
+ // game->state.grid[x + y * GRIDWIDTH] = (game->state.grid[x + y * GRIDWIDTH] + 1) % (sizeof(colors) / sizeof(colors[0]));
+ char placed = getPlaceAction(
+ game->state.grid[x + y * GRIDWIDTH],
+ cellx,
+ celly,
+ cellWidth,
+ cellHeight
+ );
+
+ if (placed != EMPTY) {
+ for (int i = 0; i < MAX_PLACEABLE_CELLS; i++) {
+ if (game->state.placeableCells[i] == placed) {
+ game->state.grid[x + y * GRIDWIDTH] = placed;
+ for (int j = i; j < MAX_PLACEABLE_CELLS - 1; j++) {
+ game->state.placeableCells[j] = game->state.placeableCells[j + 1];
+ }
+ game->state.placeableCells[MAX_PLACEABLE_CELLS - 1] = EMPTY;
+ break;
+ }
+ }
+ }
+ }
+
+ for (int i = 0; i < N_BUTTONS; i++) {
+ if (
+ game->ui.mousex > buttons[i].x && game->ui.mousex < buttons[i].x + buttons[i].w &&
+ game->ui.mousey > game->ui.height - buttons[i].y &&
+ game->ui.mousey < game->ui.height - buttons[i].y + buttons[i].h
+ ) {
+ // TODO - CLICK THINGS
+ //game->state.buttonStates[i] = BUTTON_STATE_PRESSED;
+ switch (i) {
+ case BUTTON_RETRY:
+ restart_level(game);
+ break;
+ case BUTTON_CONTINUE:
+ if (
+ !game->state.levelComplete ||
+ game->state.currentLevel + 1 >= sizeof(levels) / sizeof(levels[0])
+ ) {
+ break;
+ }
+ game->state.levelComplete = 0;
+ game->state.currentLevel++;
+ restart_level(game);
+ break;
+ case BUTTON_BACK:
+ tutorial = 1;
+ break;
+ case BUTTON_PLAY:
+ game->state.playing = !game->state.playing;
+ break;
+ }
+ }
+ }
break;
- case INPUT_DOWN:
- game->state.y += 20;
+ case INPUT_RCLICK:
+ if (game->ui.mousex > GRID_OFFSET_X) {
+ x = (game->ui.mousex - GRID_OFFSET_X) * GRIDWIDTH / (game->ui.width - GRID_OFFSET_X);
+ y = game->ui.mousey * GRIDHEIGHT / game->ui.height;
+
+ if (game->state.grid[x + y * GRIDWIDTH] != BLACK) {
+ break;
+ }
+
+ for (int i = 0; i < MAX_PLACEABLE_CELLS; i++) {
+ if (game->state.placeableCells[i] == YELLOW) {
+ game->state.grid[x + y * GRIDWIDTH] = YELLOW;
+ for (int j = i; j < MAX_PLACEABLE_CELLS - 1; j++) {
+ game->state.placeableCells[j] = game->state.placeableCells[j + 1];
+ }
+ game->state.placeableCells[MAX_PLACEABLE_CELLS - 1] = EMPTY;
+ break;
+ }
+ }
+ }
break;
- case INPUT_LEFT:
- game->state.x -= 20;
+ case INPUT_PAUSE_PLAY:
+ game->state.playing = !game->state.playing;
break;
- case INPUT_RIGHT:
- game->state.x += 20;
+ case INPUT_RESTART:
+ restart_level(game);
break;
default:
break;
}
+
+ if (game->state.playing && game->state.lastTick + TICK_LENGTH <= now) {
+ game->state.lastTick = now;
+
+ tick(game, a);
+ }
+
+ return tutorial;
}
#if SDL
#include <SDL3/SDL.h>
+typedef struct {
+ int width, height;
+ Color colors[16];
+ const char *pixels;
+} Image;
+
+#include "../build/images.c"
+
+SDL_Texture *textures[sizeof(images) / sizeof(images[0])];
+
+#include "../build/music.c"
+
+SDL_AudioStream *stream;
+
int main(int argc, char **argv) {
(void) argc;
(void) argv;
@@ -109,76 +531,138 @@ int main(int argc, char **argv) {
};
Game *game = new(&a, 1, Game);
- game->state = (State) {
- .x = 100,
- .y = 100,
- };
+ game->state.currentLevel = 0;
+ restart_level(game);
game->ui = (UI) {
.width = 640,
.height = 480,
+ .mousex = 0,
+ .mousey = 0,
};
+ for (int i = 0; i < N_BUTTONS; i++) {
+ game->state.buttonStates[i] = BUTTON_STATE_IDLE;
+ }
- SDL_Init(SDL_INIT_VIDEO);
+ SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO);
SDL_Window *w = SDL_CreateWindow(
"LDJam 57",
game->ui.width,
game->ui.height,
- 0
+ SDL_WINDOW_RESIZABLE
);
SDL_PropertiesID renderProps = SDL_CreateProperties();
SDL_SetPointerProperty(renderProps, SDL_PROP_RENDERER_CREATE_WINDOW_POINTER, w);
SDL_SetNumberProperty(renderProps, SDL_PROP_RENDERER_CREATE_PRESENT_VSYNC_NUMBER, 1);
SDL_Renderer *r = SDL_CreateRendererWithProperties(renderProps);
SDL_DestroyProperties(renderProps);
+ SDL_SetRenderDrawBlendMode(r, SDL_BLENDMODE_BLEND);
+
+ for (int j = 1; j < (int) (sizeof(images) / sizeof(images[0])); j++) {
+ textures[j] = SDL_CreateTexture(
+ r,
+ SDL_PIXELFORMAT_ABGR8888,
+ SDL_TEXTUREACCESS_STATIC,
+ images[j].width,
+ images[j].height
+ );
+ SDL_SetTextureScaleMode(textures[j], SDL_SCALEMODE_NEAREST);
+ Arena scratch = a;
+ Color *pixels = new(&scratch, images[j].width * images[j].height, Color);
+ for (int i = 0; i < images[j].width * images[j].height; i++) {
+ pixels[i] = images[j].colors[(int) images[j].pixels[i]];
+ }
+ SDL_UpdateTexture(textures[j], NULL, pixels, images[j].width * 4);
+ }
+
+ SDL_AudioSpec audioSpec = {
+ .format = SDL_AUDIO_S16LE,
+ .channels = 2,
+ .freq = 48000,
+ };
+ stream = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &audioSpec, NULL, NULL);
+ SDL_PutAudioStreamData(stream, musicBytes, sizeof(musicBytes));
+ SDL_ResumeAudioStreamDevice(stream);
for (;;) {
+ uint64_t now = SDL_GetTicks();
game->input = INPUT_NONE;
SDL_Event e = {0};
while (SDL_PollEvent(&e)) {
switch (e.type) {
case SDL_EVENT_QUIT:
return 0;
+ case SDL_EVENT_MOUSE_BUTTON_DOWN:
+ game->ui.mousex = (int) e.button.x;
+ game->ui.mousey = (int) e.button.y;
+ if (e.button.button == 1) {
+ game->input = INPUT_CLICK;
+ } else if (e.button.button == 3) {
+ game->input = INPUT_RCLICK;
+ }
+ break;
+ case SDL_EVENT_MOUSE_MOTION:
+ game->ui.mousex = (int) e.motion.x;
+ game->ui.mousey = (int) e.motion.y;
+ game->input = INPUT_MOVE;
+ break;
case SDL_EVENT_KEY_DOWN:
switch (e.key.key) {
- case 'w':
- game->input = INPUT_UP;
- break;
- case 'a':
- game->input = INPUT_LEFT;
+ case SDLK_SPACE:
+ game->input = INPUT_PAUSE_PLAY;
break;
- case 's':
- game->input = INPUT_DOWN;
- break;
- case 'd':
- game->input = INPUT_RIGHT;
+ case SDLK_R:
+ game->input = INPUT_RESTART;
break;
}
break;
+ case SDL_EVENT_WINDOW_RESIZED:
+ SDL_GetWindowSize(w, &game->ui.width, &game->ui.height);
+ break;
}
}
Arena scratch = a;
- DrawList *drawList = render(game->state, &game->ui, &scratch);
+ DrawList *drawList = render(&game->state, &game->ui, &scratch);
SDL_SetRenderDrawColor(r, 0, 0, 0, 255);
SDL_RenderFillRect(r, NULL);
+
for (int i = 0; i < drawList->len; i++) {
- SDL_SetRenderDrawColor(
- r,
- drawList->els[i].r,
- drawList->els[i].g,
- drawList->els[i].b,
- drawList->els[i].a
- );
SDL_FRect rect = {
.x = (float) drawList->els[i].x,
.y = (float) drawList->els[i].y,
.w = (float) drawList->els[i].w,
.h = (float) drawList->els[i].h
};
+
+ SDL_SetRenderDrawColor(
+ r,
+ drawList->els[i].fill.r,
+ drawList->els[i].fill.g,
+ drawList->els[i].fill.b,
+ drawList->els[i].fill.a
+ );
SDL_RenderFillRect(r, &rect);
+
+ SDL_SetRenderDrawColor(
+ r,
+ drawList->els[i].border.r,
+ drawList->els[i].border.g,
+ drawList->els[i].border.b,
+ drawList->els[i].border.a
+ );
+ SDL_RenderRect(r, &rect);
+
+ if (drawList->els[i].image.index != 0) {
+ SDL_SetTextureAlphaMod(textures[(int) drawList->els[i].image.index], drawList->els[i].image.opacity);
+ SDL_RenderTexture(r, textures[(int) drawList->els[i].image.index], NULL, &rect);
+ }
}
- update(game, a);
+ update(game, now, a);
+
+ if (SDL_GetAudioStreamQueued(stream) < (int) sizeof(musicBytes) / 8) {
+ SDL_PutAudioStreamData(stream, musicBytes, sizeof(musicBytes));
+ }
SDL_RenderPresent(r);
}
@@ -198,24 +682,32 @@ void game_init(void) {
perm.end = heap + MEM_SIZE;
game = new(&perm, 1, Game);
- game->state = (State) {
- .x = 100,
- .y = 100,
- };
+ game->state.currentLevel = 0;
+ restart_level(game);
+
+ for (int i = 0; i < N_BUTTONS; i++) {
+ game->state.buttonStates[i] = BUTTON_STATE_IDLE;
+ }
}
__attribute((export_name("game_render")))
-DrawList *game_render(int width, int height) {
+DrawList *game_render(int width, int height, int mousex, int mousey) {
game->ui.width = width;
game->ui.height = height;
+ game->ui.mousex = mousex;
+ game->ui.mousey = mousey;
Arena scratch = perm;
- return render(game->state, &game->ui, &scratch);
+ return render(&game->state, &game->ui, &scratch);
}
__attribute((export_name("game_update")))
-void game_update(int input) {
+int game_update(int input, int mousex, int mousey, int now) {
game->input = input;
- update(game, perm);
+ game->ui.mousex = mousex;
+ game->ui.mousey = mousey;
+ return update(game, now, perm);
}
#endif
+
+#endif
diff --git a/src/img.c b/src/img.c
new file mode 100644
index 0000000..09f000b
--- /dev/null
+++ b/src/img.c
@@ -0,0 +1,95 @@
+#define QOI_IMPLEMENTATION
+#include "../lib/qoi/qoi.h"
+
+#include <stdio.h>
+
+typedef struct {
+ unsigned char r, g, b, a;
+} Color;
+
+int pixels(char *index, char *filename) {
+ qoi_desc desc;
+ Color *pixels = qoi_read(filename, &desc, 4);
+
+ Color colors[16];
+ int colorsLen = 0;
+ char *compressed = malloc(desc.width * desc.height);
+
+ for (int i = 0; i < (int) (desc.width * desc.height); i++) {
+ char c = -1;
+ for (int j = 0; j < colorsLen; j++) {
+ if (*((int *) &colors[j]) == *((int *) pixels + i)) {
+ c = j;
+ break;
+ }
+ }
+ if (c == -1) {
+ c = colorsLen;
+ if (colorsLen >= 16) {
+ return 1;
+ }
+ colors[colorsLen++] = pixels[i];
+ }
+ compressed[i] = c;
+ }
+
+ printf("const char imagePixels%s[%d * %d] = {\n", index, desc.width, desc.height);
+ for (int y = 0; y < (int) desc.height; y++) {
+ for (int x = 0; x < (int) desc.width; x++) {
+ printf("%d,", compressed[x + y * desc.width]);
+ }
+ printf("\n");
+ }
+ printf("};\n");
+
+ return 0;
+}
+
+int image(char *index, char *filename) {
+ qoi_desc desc;
+ Color *pixels = qoi_read(filename, &desc, 4);
+
+ Color colors[16];
+ int colorsLen = 0;
+ char *compressed = malloc(desc.width * desc.height);
+
+ for (int i = 0; i < (int) (desc.width * desc.height); i++) {
+ char c = -1;
+ for (int j = 0; j < colorsLen; j++) {
+ if (*((int *) &colors[j]) == *((int *) pixels + i)) {
+ c = j;
+ break;
+ }
+ }
+ if (c == -1) {
+ c = colorsLen;
+ if (colorsLen >= 16) {
+ return 1;
+ }
+ colors[colorsLen++] = pixels[i];
+ }
+ compressed[i] = c;
+ }
+
+ printf("{\n.width = %d,\n.height = %d,\n.colors = {\n", desc.width, desc.height);
+ for (int i = 0; i < colorsLen; i++) {
+ printf("\t{%u, %u, %u, %u},\n", colors[i].r, colors[i].g, colors[i].b, colors[i].a);
+ }
+ printf("},\n.pixels = imagePixels%s,\n},\n", index);
+
+ return 0;
+}
+
+int main(int argc, char **argv) {
+ if (argc != 4) {
+ return 1;
+ }
+
+ if (!strcmp(argv[1], "pixels")) {
+ return pixels(argv[3], argv[2]);
+ } else if (!strcmp(argv[1], "image")) {
+ return image(argv[3], argv[2]);
+ } else {
+ return 1;
+ }
+}
diff --git a/src/index.html.in b/src/index.html.in
index fcfb8ef..5414f09 100644
--- a/src/index.html.in
+++ b/src/index.html.in
@@ -11,9 +11,9 @@ html, body {
overflow: hidden;
text-align: center;
width: 100%;
+ color: white;
}
h1 {
- color: white;
margin: 0;
}
button {
@@ -22,21 +22,123 @@ button {
margin: 0.3em;
width: 25%;
}
+
+canvas {
+ margin: auto;
+}
</style>
-<canvas></canvas>
+<div id="tutorial" style="overflow-y: scroll; height: 90vh">
+ <h1>Depths of the Mind</h1>
+
+ <p>
+ The mind is an intricate thing, with many tiny components working together
+ in perfect harmony... Until something came in and messed everything up!
+ </p>
+
+ <p>
+ If you want to set all the pieces of the mind right, you'll need to know how
+ it works!
+ </p>
+
+ <img src=
+#include "../build/tutorial_black_white.png.b64"
+ >
+
+ <p>
+ The structure of the brain consists of black blocks filling a white void.
+ </p>
+
+ <img src=
+#include "../build/tutorial_red_yellow.png.b64"
+ >
+
+ <p>
+ Black blocks will turn red when clicked (if you have red in your inventory on the left).
+ While the brain is running, black blocks next to red blocks will turn red!
+ Red blocks will eventually tire to yellow, and then back to black. They don't stay red very long!
+ This is what allows the brain to send signals along a series of black blocks.
+ Yellow blocks can be placed with a right click on a black block (if you have one in your inventory)
+ </p>
+
+ <img src=
+#include "../build/tutorial_direction_red.png.b64"
+ >
+
+ <p>
+ Sometimes, only one side of a block turns red.
+ When this happens, it will try to move in that direction (and leave a yellow behind it)
+ but if it cannot it will turn clockwise and try again.
+ </p>
+
+ <span>
+ <img src=
+#include "../build/tutorial_blue_1.png.b64"
+ >
+ <img src=
+#include "../build/tutorial_blue_2.png.b64"
+ >
+
+ <p>
+ Blue blocks need to be moved around to keep things running smoothly!
+ When a blue block is touched by a single red block, it will start rolling
+ away from it.
+ Each level has a goal in faint red which needs at least one blue block to be delivered to it.
+ Some levels will also have faint blue areas that create new blue blocks for you!
+ </p>
+
+ <img src=
+#include "../build/tutorial_blue_clone.png.b64"
+ >
+
+ <p>
+ Finally, if a blue block touches a red block <em>while in motion</em> it will be cloned!
+ One will be left behind and the other will continue.
+ </p>
+
+ <button id="play-button">Play!</button>
+</div>
+
+<canvas style="display: none;"></canvas>
<script>
// Mirror these in src/all.c
const INPUT_NONE = 0;
-const INPUT_UP = 1;
-const INPUT_DOWN = 2;
-const INPUT_LEFT = 3;
-const INPUT_RIGHT = 4;
+const INPUT_CLICK = 1;
+const INPUT_RCLICK = 2;
+const INPUT_PAUSE_PLAY = 3;
+const INPUT_MOVE = 4;
+const INPUT_RESTART = 5;
const WASM =
#include "../build/main.wasm.b64"
+const MUSIC =
+#include "../build/music.mp3.b64"
+
+const IMAGES = [
+#include "../build/continue.png.b64"
+,
+#include "../build/exit.png.b64"
+,
+#include "../build/pause.png.b64"
+,
+#include "../build/play.png.b64"
+,
+#include "../build/restart.png.b64"
+,
+#include "../build/nelson.png.b64"
+,
+#include "../build/nelson_left.png.b64"
+,
+#include "../build/nelson_up.png.b64"
+,
+#include "../build/nelson_right.png.b64"
+,
+#include "../build/nelson_down.png.b64"
+,
+];
+
async function main() {
let bytes = Uint8Array.from(atob(WASM), function(c) {
return c.charCodeAt(0);
@@ -46,54 +148,126 @@ async function main() {
let exports = wasm.exports;
let html = document.querySelector("html");
let canvas = document.querySelector("canvas");
+ const tutorial = document.querySelector("#tutorial");
let ctx = canvas.getContext("2d");
let memory = exports.memory;
+ let mousex = 0;
+ let mousey = 0;
+
+ const audio = new Audio();
+ audio.src = MUSIC;
+ audio.volume = 0.2;
+ audio.loop = true;
+ let musicPlaying = false;
+
+ let images = [null];
+ for (let i = 0; i < IMAGES.length; i++) {
+ const image = new Image();
+ image.src = IMAGES[i];
+ images.push(image);
+ }
+
+ const start = Date.now();
+ function now() {
+ return Date.now() - start;
+ }
+
function min(a, b) {
return b<a ? b : a;
}
- function max_width() {
- return html.clientHeight * 0.7 | 0;
- }
-
function render() {
- let width = canvas.width = min(html.clientWidth, max_width());
- let height = canvas.height = width;
- let ptr = exports.game_render(width, height);
+ if (64 + html.clientHeight * (20 / 16) > html.clientWidth) {
+ canvas.width = html.clientWidth;
+ canvas.height = (html.clientWidth - 64) * (16 / 20);
+ } else {
+ canvas.width = 64 + html.clientHeight * (20 / 16);
+ canvas.height = html.clientHeight;
+ }
+ let width = canvas.width;
+ let height = canvas.height;
+ let ptr = exports.game_render(width, height, mousex, mousey);
let dl = new Int32Array(memory.buffer, ptr);
let len = dl[0];
let ops = dl.subarray(1);
+ ctx.fillStyle = "#000000";
+ ctx.fillRect(0, 0, width, height);
for (let i = 0; i < len; i++) {
- let op = ops.subarray(5*i, 5*i+5);
- const color = new Uint8Array(new Uint32Array(ops.subarray(4, 5)).buffer);
- let style = `#${color[0].toString(16).padStart(2, "0")}${color[1].toString(16).padStart(2, "0")}${color[2].toString(16).padStart(2, "0")}`;
- ctx.fillStyle = style;
+ let op = ops.subarray(7*i, 7*i+7);
+ const color = new Uint8Array(new Uint32Array(op.subarray(4, 6)).buffer);
+ ctx.fillStyle = `#${color[0].toString(16).padStart(2, "0")}${color[1].toString(16).padStart(2, "0")}${color[2].toString(16).padStart(2, "0")}`;
+ ctx.globalAlpha = color[3] / 255;
ctx.fillRect(op[0], op[1], op[2], op[3]);
+ ctx.strokeStyle = `#${color[4].toString(16).padStart(2, "0")}${color[5].toString(16).padStart(2, "0")}${color[6].toString(16).padStart(2, "0")}`;
+ ctx.globalAlpha = color[7] / 255;
+ ctx.strokeRect(op[0], op[1], op[2], op[3]);
+ const image = new Uint8Array(new Uint32Array(op.subarray(6, 7)).buffer);
+ if (image[0] !== 0) {
+ console.log(image);
+ ctx.globalAlpha = image[1] / 255;
+ ctx.drawImage(images[image[0]], op[0], op[1], op[2], op[3]);
+ }
}
}
- function onresize() { html.style.maxWidth = `${max_width()}px`; }
+ function onresize() { /*html.style.maxWidth = `${max_width()}px`; */}
window.addEventListener("resize", onresize);
onresize();
- document.addEventListener("keydown", function(e) {
- if (e.key == "w") {
- exports.game_update(INPUT_UP);
- } else if (e.key == "a") {
- exports.game_update(INPUT_LEFT);
- } else if (e.key == "s") {
- exports.game_update(INPUT_DOWN);
- } else if (e.key == "d") {
- exports.game_update(INPUT_RIGHT);
- }
- })
+ function doUpdate(input) {
+ if (exports.game_update(input, mousex, mousey, now()) !== 0) {
+ canvas.style.display = "none";
+ tutorial.style.display = "block";
+ }
+ }
+
+ canvas.addEventListener("mousemove", function(e) {
+ const rect = e.target.getBoundingClientRect();
+ mousex = e.clientX - rect.left;
+ mousey = e.clientY - rect.top;
+ doUpdate(INPUT_MOVE);
+ });
+
+ canvas.addEventListener("mousedown", function(e) {
+ const rect = e.target.getBoundingClientRect();
+ mousex = e.clientX - rect.left;
+ mousey = e.clientY - rect.top;
+ if (e.button == 0) {
+ doUpdate(INPUT_CLICK);
+ } else if (e.button == 2) {
+ e.preventDefault();
+ doUpdate(INPUT_RCLICK);
+ }
+ });
+
+ document.getElementById("play-button").addEventListener("click", function (e) {
+ if (!musicPlaying) {
+ musicPlaying = true;
+ audio.play();
+ }
+
+ canvas.style.display = "block";
+ tutorial.style.display = "none";
+ });
+
+ canvas.addEventListener("contextmenu", function (e) {
+ e.preventDefault();
+ });
+
+ document.addEventListener("keydown", function (e) {
+ if (e.key === " ") {
+ doUpdate(INPUT_PAUSE_PLAY);
+ } else if (e.key == "r") {
+ doUpdate(INPUT_RESTART);
+ }
+ });
function animate() {
// TODO: stop requesting frames when state is static
requestAnimationFrame(animate);
- exports.game_update(INPUT_NONE);
+ doUpdate(INPUT_NONE);
render();
}
requestAnimationFrame(animate);
diff --git a/src/levels.c b/src/levels.c
new file mode 100644
index 0000000..a5f1d15
--- /dev/null
+++ b/src/levels.c
@@ -0,0 +1,488 @@
+#ifndef INCLUDE_LEVELS_C
+#define INCLUDE_LEVELS_C
+
+#include "all.c"
+
+#define _ EMPTY,
+#define B BLACK,
+#define O BLUE,
+#define R RED,
+#define r YELLOW,
+#define RL RED_LEFT,
+#define RR RED_RIGHT,
+#define RU RED_UP,
+#define RD RED_DOWN,
+static Level levels[] = {
+ {
+ .grid = {
+ // level 1
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ RU _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ B O _ _ _ _ _ _ _ _ _ _ _ _ _ B
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ },
+ .goalx = 18,
+ .goaly = 7,
+ .placeableCells = {
+ RED,
+ },
+ },
+ {
+ // level 2
+ .grid = {
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ B _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ O _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ B _ _ _ _ _ _ _ _ _ _ _ _ _ _ B
+ _ _ _ _ _ B _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ },
+ .goalx = 18,
+ .goaly = 10,
+ .placeableCells = {
+ RED,
+ RED,
+ },
+ },
+ {
+ // level 3
+ .grid = {
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ B B _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ B O _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ B _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ B _ _ _ _ _ _ _ _ _ _ _ _ _ _ B
+ _ _ _ _ B B _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ },
+ .goalx = 18,
+ .goaly = 10,
+ .placeableCells = {
+ BLACK,
+ RED,
+ },
+ },
+ {
+ // level 4
+ .grid = {
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ B _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ B _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ B _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ B _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ B _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ B B B B B B B B B B B _ _ _ _ _ _ _
+ _ _ B _ B O _ _ _ _ _ O B _ _ _ _ _ _ _
+ _ _ _ _ B _ _ _ _ _ B B B _ _ _ _ _ _ _
+ _ _ _ _ B _ _ _ _ _ B _ _ _ _ _ _ _ _ _
+ _ _ _ _ B _ _ _ _ _ _ B _ _ _ _ _ _ _ _
+ _ _ _ _ _ B B _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ B _ _ _ _ O B _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ B _ _ _ _ _ _ _ _ _ _
+ },
+ .goalx = 10,
+ .goaly = 13,
+ .placeableCells = {
+ RED,
+ },
+ },
+ {
+ // level 5
+ .grid = {
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ R B _ _ _ _ _ _
+ _ _ _ _ _ _ B _ _ _ _ _ r B _ _ _ _ _ _
+ _ _ _ _ r RD _ _ _ _ _ _ _ _ B _ _ _ _ _
+ _ _ _ _ B B _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ RU _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ B _ _ _ _ _ _ _ _ _ _ _
+ _ _ R r _ _ _ _ B _ _ _ _ _ _ _ _ _ _ _
+ _ _ B B _ _ _ _ B _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ B _ _ _ RU _ _ _ _ B _ _
+ _ _ _ _ _ _ _ _ B _ _ _ _ B _ _ _ _ _ _
+ _ _ _ _ _ _ O _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ B _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ },
+ .goalx = 16,
+ .goaly = 10,
+ .placeableCells = {
+ RED,
+ },
+ },
+ {
+ // level 6
+ .grid = {
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ RU _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ B _ _ _ _ O B _ _ _ _ _ _ _ _
+ _ _ _ _ _ B _ _ _ _ O B _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ B _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ },
+ .goalx = 6,
+ .goaly = 11,
+ .placeableCells = {
+ RED,
+ RED_UP,
+ RED_DOWN,
+ RED_LEFT,
+ RED_RIGHT,
+ },
+ },
+ {
+ // level 7
+ .grid = {
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ B B _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ B B _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ B _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ },
+ .need9 = 1,
+ .goalx = 6,
+ .goaly = 11,
+ .spawnerx = 10,
+ .spawnery = 6,
+ .spawnRate = 6,
+ .placeableCells = {
+ RED,
+ YELLOW,
+ RED_UP,
+ BLACK,
+ },
+ },
+ {
+ // level 8
+ .grid = {
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ B B B _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ B B _ _ _ _ _ _ B _ _ _ _ _ _ _
+ _ _ _ _ B B B _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ },
+ .goalx = 11,
+ .goaly = 7,
+ .spawnerx = 6,
+ .spawnery = 7,
+ .need9 = 1,
+ .placeableCells = {
+ RED,
+ YELLOW,
+ },
+ },
+ {
+ // level 9
+ .grid = {
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ RD _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ O _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ B O _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ RU _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ },
+ .goalx = 12,
+ .goaly = 7,
+ .placeableCells = {
+ RED,
+ },
+ },
+ {
+ // level 10
+ .grid = {
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ RU _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ B _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ B _ _ _ _ B _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ B B _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ B RD _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ },
+ .goalx = 7,
+ .goaly = 9,
+ .spawnerx = 4,
+ .spawnery = 3,
+ .need9 = 1,
+ .spawnRate = 7,
+ .placeableCells = {
+ RED,
+ YELLOW,
+ BLACK,
+ BLACK,
+ BLACK,
+ BLACK,
+ BLACK,
+ BLACK,
+ },
+ },
+ {
+ // level 11
+ .grid = {
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ B _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ B _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ B _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ B B B B B B B B RL _ _ _ _ _ _ _ _ _ _ _
+ },
+ .goalx = 15,
+ .goaly = 15,
+ .spawnerx = 9,
+ .spawnery = 15,
+ .need9 = 1,
+ .spawnRate = 7,
+ .placeableCells = {
+ RED,
+ YELLOW,
+ BLACK,
+ BLACK,
+ },
+ },
+ {
+ // level 12
+ .grid = {
+ _ _ _ _ _ B _ _ _ _ _ _ RU _ _ _ _ _ _ _
+ _ _ _ _ RU _ _ _ _ _ _ _ _ B _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ B B _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ B B _ _ _ _ _ _ _ _ _ _ _
+ _ B _ _ _ _ _ _ O B B _ _ _ _ _ _ _ _ _
+ RU _ _ _ _ _ _ _ _ B B _ _ _ _ _ _ _ _ _
+ _ _ _ _ B _ _ _ _ _ _ _ _ _ RU _ _ _ _ _
+ _ _ _ _ _ RU _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ B _ _ _ _ _ _ _ _ RU _ _ _ _ _ _ _ _ _ _
+ _ RU _ _ _ _ _ _ B _ _ _ _ _ _ _ _ _ _ _
+ },
+ .goalx = 8,
+ .goaly = 7,
+ .spawnerx = 13,
+ .spawnery = 7,
+ .need9 = 1,
+ .spawnRate = 3,
+ .placeableCells = {
+ RED,
+ RED,
+ YELLOW,
+ YELLOW,
+ },
+ },
+ {
+ // level 14
+ // An attempt at a red herring
+ // This one has some similarities to level 12 so I didn't want it to be level 13
+ .grid = {
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ B B _ R B _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ B B _ r B _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ B O _ _ _ _ _ _ _ _ _ _ _ RU _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ },
+ .goalx = 10,
+ .goaly = 7,
+ .spawnerx = 15,
+ .spawnery = 7,
+ .need9 = 1,
+ .spawnRate = 3,
+ .placeableCells = {
+ RED,
+ RED_UP,
+ YELLOW,
+ },
+ },
+ {
+ // level 15
+ // This one is a bit tricky
+ .grid = {
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ RU _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ B _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ B _ _ _ _ B _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ B _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ },
+ .goalx = 7,
+ .goaly = 9,
+ .spawnerx = 4,
+ .spawnery = 3,
+ .need9 = 1,
+ .spawnRate = 2,
+ .placeableCells = {
+ RED,
+ RED,
+ YELLOW,
+ YELLOW,
+ BLACK,
+ BLACK,
+ BLACK,
+ BLACK,
+ BLACK,
+ BLACK,
+ },
+ },
+ {
+ // level ?
+ // Must go after blues turning yellows into reds is introduced
+ .grid = {
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ O B B _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ B B _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ B _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
+ },
+ .need9 = 1,
+ .goalx = 6,
+ .goaly = 11,
+ .placeableCells = {
+ RED,
+ RED_UP,
+ RED_DOWN,
+ YELLOW,
+ BLACK,
+ BLACK,
+ },
+ },
+};
+#undef _
+#undef B
+#undef O
+#undef R
+#undef r
+#undef RR
+#undef RL
+#undef RU
+#undef RD
+
+#endif // INCLUDE_LEVELS_C
diff --git a/src/tick.c b/src/tick.c
new file mode 100644
index 0000000..338017b
--- /dev/null
+++ b/src/tick.c
@@ -0,0 +1,376 @@
+#ifndef INCLUDE_TICK_C
+#define INCLUDE_TICK_C
+
+#include "all.c"
+
+static int isRed(int cell) {
+ return (
+ cell == RED ||
+ cell == RED_LEFT ||
+ cell == RED_RIGHT ||
+ cell == RED_UP ||
+ cell == RED_DOWN
+ );
+}
+
+static int isBlue(int cell) {
+ return (
+ cell == BLUE ||
+ cell == BLUE_LEFT ||
+ cell == BLUE_RIGHT ||
+ cell == BLUE_UP ||
+ cell == BLUE_DOWN
+ );
+}
+
+static void tick(Game *game, Arena a) {
+ State *lastState = new(&a, 1, State);
+ xmemcpy(lastState, game, sizeof(State));
+
+ if (lastState->spawnerCount > 0) {
+ game->state.spawnerCount = lastState->spawnerCount - 1;
+ }
+
+ for (int x = 0; x < GRIDWIDTH; x++) {
+ for (int y = 0; y < GRIDHEIGHT; y++) {
+ switch (lastState->grid[x + y * GRIDWIDTH]) {
+ int reds, red;
+ int blues, blue;
+ case BLACK:
+ reds = 0;
+ if (
+ x < GRIDWIDTH - 1 && (
+ lastState->grid[x + 1 + y * GRIDWIDTH] == RED_LEFT ||
+ lastState->grid[x + 1 + y * GRIDWIDTH] == RED
+ )
+ ) {
+ reds++;
+ if (
+ x < GRIDWIDTH - 2 &&
+ y > 0 &&
+ y < GRIDHEIGHT - 1 &&
+ lastState->grid[x + 2 + y * GRIDWIDTH] == YELLOW &&
+ lastState->grid[x + 1 + (y - 1) * GRIDWIDTH] == YELLOW &&
+ lastState->grid[x + 1 + (y + 1) * GRIDWIDTH] == YELLOW
+ ) {
+ red = RED_LEFT;
+ } else {
+ red = lastState->grid[x + 1 + y * GRIDWIDTH];
+ }
+ }
+
+ if (
+ x > 0 && (
+ lastState->grid[x - 1 + y * GRIDWIDTH] == RED_RIGHT ||
+ lastState->grid[x - 1 + y * GRIDWIDTH] == RED
+ )
+ ) {
+ reds++;
+ if (
+ x > 1 &&
+ y > 0 &&
+ y < GRIDHEIGHT - 1 &&
+ lastState->grid[x - 2 + y * GRIDWIDTH] == YELLOW &&
+ lastState->grid[x - 1 + (y - 1) * GRIDWIDTH] == YELLOW &&
+ lastState->grid[x - 1 + (y + 1) * GRIDWIDTH] == YELLOW
+ ) {
+ red = RED_RIGHT;
+ } else {
+ red = lastState->grid[x - 1 + y * GRIDWIDTH];
+ }
+ }
+
+ if (
+ y > 0 && (
+ lastState->grid[x + (y - 1) * GRIDWIDTH] == RED_DOWN ||
+ lastState->grid[x + (y - 1) * GRIDWIDTH] == RED
+ )
+ ) {
+ reds++;
+ if (
+ x > 0 &&
+ x < GRIDWIDTH - 1 &&
+ y > 1 &&
+ lastState->grid[x + (y - 2) * GRIDWIDTH] == YELLOW &&
+ lastState->grid[x - 1 + (y - 1) * GRIDWIDTH] == YELLOW &&
+ lastState->grid[x + 1 + (y - 1) * GRIDWIDTH] == YELLOW
+ ) {
+ red = RED_DOWN;
+ } else {
+ red = lastState->grid[x + (y - 1) * GRIDWIDTH];
+ }
+ }
+
+ if (
+ y < GRIDHEIGHT - 1 && (
+ lastState->grid[x + (y + 1) * GRIDWIDTH] == RED_UP ||
+ lastState->grid[x + (y + 1) * GRIDWIDTH] == RED
+ )
+ ) {
+ reds++;
+ if (
+ x > 0 &&
+ x < GRIDWIDTH - 1 &&
+ y < GRIDHEIGHT - 2 &&
+ lastState->grid[x + (y + 2) * GRIDWIDTH] == YELLOW &&
+ lastState->grid[x - 1 + (y + 1) * GRIDWIDTH] == YELLOW &&
+ lastState->grid[x + 1 + (y + 1) * GRIDWIDTH] == YELLOW
+ ) {
+ red = RED_UP;
+ } else {
+ red = lastState->grid[x + (y + 1) * GRIDWIDTH];
+ }
+ }
+
+ if (reds == 1) {
+ game->state.grid[x + y * GRIDWIDTH] = red;
+ } else if (reds >= 2) {
+ game->state.grid[x + y * GRIDWIDTH] = RED;
+ }
+ break;
+ case RED:
+ game->state.grid[x + y * GRIDWIDTH] = YELLOW;
+ break;
+ case RED_LEFT:
+ if (
+ x > 0 &&
+ lastState->grid[x - 1 + y * GRIDWIDTH] == BLACK
+ ) {
+ game->state.grid[x + y * GRIDWIDTH] = YELLOW;
+ } else {
+ game->state.grid[x + y * GRIDWIDTH] = RED_UP;
+ }
+ break;
+ case RED_RIGHT:
+ if (
+ x < GRIDWIDTH - 1 &&
+ lastState->grid[x + 1 + y * GRIDWIDTH] == BLACK
+ ) {
+ game->state.grid[x + y * GRIDWIDTH] = YELLOW;
+ } else {
+ game->state.grid[x + y * GRIDWIDTH] = RED_DOWN;
+ }
+ break;
+ case RED_UP:
+ if (
+ y > 0 &&
+ lastState->grid[x + (y - 1) * GRIDWIDTH] == BLACK
+ ) {
+ game->state.grid[x + y * GRIDWIDTH] = YELLOW;
+ } else {
+ game->state.grid[x + y * GRIDWIDTH] = RED_RIGHT;
+ }
+ break;
+ case RED_DOWN:
+ if (
+ y < GRIDHEIGHT - 1 &&
+ lastState->grid[x + (y + 1) * GRIDWIDTH] == BLACK
+ ) {
+ game->state.grid[x + y * GRIDWIDTH] = YELLOW;
+ } else {
+ game->state.grid[x + y * GRIDWIDTH] = RED_LEFT;
+ }
+ break;
+ case YELLOW:
+ if (
+ (x > 0 && lastState->grid[x - 1 + y * GRIDWIDTH] == BLUE_RIGHT) ||
+ (x < GRIDWIDTH - 1 && lastState->grid[x + 1 + y * GRIDWIDTH] == BLUE_LEFT) ||
+ (y > 0 && lastState->grid[x + (y - 1) * GRIDWIDTH] == BLUE_DOWN) ||
+ (y < GRIDHEIGHT - 1 && lastState->grid[x + (y + 1) * GRIDWIDTH] == BLUE_UP)
+ ) {
+ game->state.grid[x + y * GRIDWIDTH] = BLACK; // used to be RED
+ } else {
+ game->state.grid[x + y * GRIDWIDTH] = BLACK;
+ }
+ break;
+ case BLUE:
+ // Win condition
+ if (game->state.goalx == x && game->state.goaly == y) {
+ if (!game->state.need9) {
+ game->state.levelComplete = 1;
+ } else {
+ if (game->state.blues < 9) {
+ game->state.blues++;
+ if (game->state.blues == 9) {
+ game->state.levelComplete = 1;
+ }
+ }
+ }
+ game->state.grid[x + y * GRIDWIDTH] = EMPTY;
+ }
+ blues = 0;
+ if (
+ x > 0 && (
+ lastState->grid[x - 1 + y * GRIDWIDTH] == RED ||
+ lastState->grid[x - 1 + y * GRIDWIDTH] == RED_LEFT ||
+ lastState->grid[x - 1 + y * GRIDWIDTH] == RED_RIGHT ||
+ lastState->grid[x - 1 + y * GRIDWIDTH] == RED_DOWN ||
+ lastState->grid[x - 1 + y * GRIDWIDTH] == RED_UP
+ )
+ ) {
+ blues++;
+ blue = BLUE_RIGHT;
+ }
+ if (
+ x < GRIDWIDTH - 1 && (
+ lastState->grid[x + 1 + y * GRIDWIDTH] == RED ||
+ lastState->grid[x + 1 + y * GRIDWIDTH] == RED_LEFT ||
+ lastState->grid[x + 1 + y * GRIDWIDTH] == RED_RIGHT ||
+ lastState->grid[x + 1 + y * GRIDWIDTH] == RED_DOWN ||
+ lastState->grid[x + 1 + y * GRIDWIDTH] == RED_UP
+ )
+ ) {
+ blues++;
+ blue = BLUE_LEFT;
+ }
+ if (
+ y > 0 && (
+ lastState->grid[x + (y - 1) * GRIDWIDTH] == RED ||
+ lastState->grid[x + (y - 1) * GRIDWIDTH] == RED_LEFT ||
+ lastState->grid[x + (y - 1) * GRIDWIDTH] == RED_RIGHT ||
+ lastState->grid[x + (y - 1) * GRIDWIDTH] == RED_DOWN ||
+ lastState->grid[x + (y - 1) * GRIDWIDTH] == RED_UP
+ )
+ ) {
+ blues++;
+ blue = BLUE_DOWN;
+ }
+ if (
+ y < GRIDHEIGHT - 1 && (
+ lastState->grid[x + (y + 1) * GRIDWIDTH] == RED ||
+ lastState->grid[x + (y + 1) * GRIDWIDTH] == RED_LEFT ||
+ lastState->grid[x + (y + 1) * GRIDWIDTH] == RED_RIGHT ||
+ lastState->grid[x + (y + 1) * GRIDWIDTH] == RED_DOWN ||
+ lastState->grid[x + (y + 1) * GRIDWIDTH] == RED_UP
+ )
+ ) {
+ blues++;
+ blue = BLUE_UP;
+ }
+ if (blues == 1) {
+ game->state.grid[x + y * GRIDWIDTH] = blue;
+ }
+ break;
+ case BLUE_LEFT:
+ if (
+ (x > 0 && isRed(lastState->grid[x - 1 + y * GRIDWIDTH])) ||
+ // (x < GRIDWIDTH - 1 && isRed(lastState->grid[x + 1 + y * GRIDWIDTH])) ||
+ (y > 0 && isRed(lastState->grid[x + (y - 1) * GRIDWIDTH])) ||
+ (y < GRIDHEIGHT - 1 && isRed(lastState->grid[x + (y + 1) * GRIDWIDTH]))
+ ) {
+ game->state.grid[x + y * GRIDWIDTH] = BLUE;
+ } else if (
+ x > 0 &&
+ lastState->grid[x - 1 + y * GRIDWIDTH] == EMPTY
+ ) {
+ game->state.grid[x + y * GRIDWIDTH] = EMPTY;
+ } else {
+ game->state.grid[x + y * GRIDWIDTH] = BLUE;
+ }
+ break;
+ case BLUE_RIGHT:
+ if (
+ // (x > 0 && isRed(lastState->grid[x - 1 + y * GRIDWIDTH])) ||
+ (x < GRIDWIDTH - 1 && isRed(lastState->grid[x + 1 + y * GRIDWIDTH])) ||
+ (y > 0 && isRed(lastState->grid[x + (y - 1) * GRIDWIDTH])) ||
+ (y < GRIDHEIGHT - 1 && isRed(lastState->grid[x + (y + 1) * GRIDWIDTH]))
+ ) {
+ game->state.grid[x + y * GRIDWIDTH] = BLUE;
+ } else if (
+ x < GRIDWIDTH - 1 &&
+ lastState->grid[x + 1 + y * GRIDWIDTH] == EMPTY
+ ) {
+ game->state.grid[x + y * GRIDWIDTH] = EMPTY;
+ } else {
+ game->state.grid[x + y * GRIDWIDTH] = BLUE;
+ }
+ break;
+ case BLUE_UP:
+ if (
+ (x > 0 && isRed(lastState->grid[x - 1 + y * GRIDWIDTH])) ||
+ (x < GRIDWIDTH - 1 && isRed(lastState->grid[x + 1 + y * GRIDWIDTH])) ||
+ (y > 0 && isRed(lastState->grid[x + (y - 1) * GRIDWIDTH]))// ||
+ // (y < GRIDHEIGHT - 1 && isRed(lastState->grid[x + (y + 1) * GRIDWIDTH]))
+ ) {
+ game->state.grid[x + y * GRIDWIDTH] = BLUE;
+ } else if (
+ y > 0 &&
+ lastState->grid[x + (y - 1) * GRIDWIDTH] == EMPTY
+ ) {
+ game->state.grid[x + y * GRIDWIDTH] = EMPTY;
+ } else {
+ game->state.grid[x + y * GRIDWIDTH] = BLUE;
+ }
+ break;
+ case BLUE_DOWN:
+ if (
+ (x > 0 && isRed(lastState->grid[x - 1 + y * GRIDWIDTH])) ||
+ (x < GRIDWIDTH - 1 && isRed(lastState->grid[x + 1 + y * GRIDWIDTH])) ||
+ // (y > 0 && isRed(lastState->grid[x + (y - 1) * GRIDWIDTH])) ||
+ (y < GRIDHEIGHT - 1 && isRed(lastState->grid[x + (y + 1) * GRIDWIDTH]))
+ ) {
+ game->state.grid[x + y * GRIDWIDTH] = BLUE;
+ } else if (
+ y < GRIDHEIGHT - 1 &&
+ lastState->grid[x + (y + 1) * GRIDWIDTH] == EMPTY
+ ) {
+ game->state.grid[x + y * GRIDWIDTH] = EMPTY;
+ } else {
+ game->state.grid[x + y * GRIDWIDTH] = BLUE;
+ }
+ break;
+ case EMPTY:
+ blues = 0;
+ if (
+ x > 0 &&
+ lastState->grid[x - 1 + y * GRIDWIDTH] == BLUE_RIGHT
+ ) {
+ blues++;
+ blue = BLUE_RIGHT;
+ }
+ if (
+ x < GRIDWIDTH - 1 &&
+ lastState->grid[x + 1 + y * GRIDWIDTH] == BLUE_LEFT
+ ) {
+ blues++;
+ blue = BLUE_LEFT;
+ }
+ if (
+ y > 0 &&
+ lastState->grid[x + (y - 1) * GRIDWIDTH] == BLUE_DOWN
+ ) {
+ blues++;
+ blue = BLUE_DOWN;
+ }
+ if (
+ y < GRIDHEIGHT - 1 &&
+ lastState->grid[x + (y + 1) * GRIDWIDTH] == BLUE_UP
+ ) {
+ blues++;
+ blue = BLUE_UP;
+ }
+ if (blues == 1) {
+ game->state.grid[x + y * GRIDWIDTH] = blue;
+ } else if (blues >= 2) {
+ game->state.grid[x + y * GRIDWIDTH] = BLUE;
+ }
+
+ // Spawner
+ if (
+ game->state.spawnerx != 0 &&
+ game->state.spawnery != 0 &&
+ game->state.grid[x + y * GRIDWIDTH] == EMPTY &&
+ x == game->state.spawnerx &&
+ y == game->state.spawnery &&
+ lastState->spawnerCount == 0
+ ) {
+ game->state.grid[x + y * GRIDWIDTH] = BLUE;
+ game->state.spawnerCount = levels[game->state.currentLevel].spawnRate;
+ }
+ break;
+ }
+ }
+ }
+}
+
+#endif // INCLUDE_TICK_C
diff --git a/src/types.c b/src/types.c
new file mode 100644
index 0000000..c40d9c3
--- /dev/null
+++ b/src/types.c
@@ -0,0 +1,166 @@
+#ifndef INCLUDE_TYPES_C
+#define INCLUDE_TYPES_C
+
+#include "all.c"
+
+#define MEM_SIZE (1<<24)
+#define GRIDWIDTH 20
+#define GRIDHEIGHT 16
+#define TICK_LENGTH 200
+#define GRID_OFFSET_X 64
+#define MAX_PLACEABLE_CELLS 16
+
+typedef struct {
+ unsigned char r, g, b, a;
+} Color;
+
+typedef struct {
+ char index;
+ unsigned char opacity;
+ char padding[2];
+} ImageRef;
+
+typedef struct {
+ int x, y, w, h;
+ Color fill;
+ Color border;
+ ImageRef image;
+} DrawElement;
+
+typedef struct {
+ int len;
+ DrawElement els[2 * GRIDWIDTH * GRIDHEIGHT * 4];
+} DrawList;
+
+typedef struct {
+ char *start;
+ char *end;
+} Arena;
+
+enum {
+ EMPTY,
+ BLACK,
+ RED,
+ YELLOW,
+ RED_UP,
+ RED_DOWN,
+ RED_LEFT,
+ RED_RIGHT,
+ BLUE,
+ BLUE_UP,
+ BLUE_DOWN,
+ BLUE_LEFT,
+ BLUE_RIGHT,
+ N_COLORS,
+};
+
+enum {
+ IMAGE_NULL,
+ IMAGE_CONTINUE,
+ IMAGE_EXIT,
+ IMAGE_PAUSE,
+ IMAGE_PLAY,
+ IMAGE_RETRY,
+ IMAGE_NELSON,
+ IMAGE_NELSON_LEFT,
+ IMAGE_NELSON_UP,
+ IMAGE_NELSON_RIGHT,
+ IMAGE_NELSON_DOWN,
+ N_IMAGES,
+};
+
+static const char colorImages[N_COLORS] = {
+ /*[RED] = IMAGE_NELSON,
+ [RED_LEFT] = IMAGE_NELSON_LEFT,
+ [RED_UP] = IMAGE_NELSON_UP,
+ [RED_RIGHT] = IMAGE_NELSON_RIGHT,
+ [RED_DOWN] = IMAGE_NELSON_DOWN,*/
+};
+
+enum {
+ BUTTON_BACK,
+ BUTTON_RETRY,
+ BUTTON_PLAY,
+ BUTTON_CONTINUE,
+ N_BUTTONS,
+};
+
+typedef struct {
+ int width, height;
+ int mousex, mousey;
+} UI;
+
+typedef enum {
+ BUTTON_STATE_IDLE,
+ BUTTON_STATE_HOVERED,
+ BUTTON_STATE_PRESSED
+} ButtonState;
+
+typedef struct {
+ int x, y, w, h;
+} Button;
+
+typedef struct {
+ // 0 if only 1 blue is needed, 1 if 9 are needed
+ char need9;
+ char spawnRate;
+ char grid[GRIDWIDTH * GRIDHEIGHT];
+ int goalx, goaly;
+ int spawnerx, spawnery;
+ char placeableCells[MAX_PLACEABLE_CELLS]; // Color
+} Level;
+
+typedef struct {
+ uint64_t lastTick;
+ char playing;
+ char levelComplete;
+ // How many blues have reached the end
+ char blues;
+ char need9;
+ char grid[GRIDWIDTH * GRIDHEIGHT];
+ int goalx, goaly;
+ int spawnerx, spawnery;
+ int spawnerCount;
+ ButtonState buttonStates[N_BUTTONS];
+ int currentLevel;
+ char placeableCells[MAX_PLACEABLE_CELLS];
+} State;
+
+// Mirror these in src/index.html.in
+enum {
+ INPUT_NONE,
+ INPUT_CLICK,
+ INPUT_RCLICK,
+ INPUT_PAUSE_PLAY,
+ INPUT_MOVE,
+ INPUT_RESTART,
+};
+
+typedef struct {
+ State state;
+ UI ui;
+ int input;
+} Game;
+
+#define new(a, c, t) ((t *) alloc(a, c, sizeof(t), _Alignof(t)))
+#define affirm(c) while (!(c)) *(volatile int *)0 = 0
+
+static void xmemcpy(void *dst, void *src, ptrdiff_t size) {
+ for (ptrdiff_t i = 0; i < size; i++) {
+ ((char *) dst)[i] = ((char *) src)[i];
+ }
+}
+
+static void *alloc(Arena *a, ptrdiff_t count, ptrdiff_t size, ptrdiff_t align) {
+ ptrdiff_t pad = -(size_t) a->start & (align - 1);
+ affirm(count < (a->end - a->start - pad) / size);
+ char *r = a->start + pad;
+ a->start += pad + size * count;
+ for (ptrdiff_t i = 0; i < count * size; i++) {
+ r[i] = 0;
+ }
+
+ return r;
+}
+
+#endif // INCLUDE_TYPES_C