diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/all.c | 692 | ||||
-rw-r--r-- | src/img.c | 95 | ||||
-rw-r--r-- | src/index.html.in | 234 | ||||
-rw-r--r-- | src/levels.c | 488 | ||||
-rw-r--r-- | src/tick.c | 376 | ||||
-rw-r--r-- | src/types.c | 166 |
6 files changed, 1921 insertions, 130 deletions
@@ -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 |