From 706dfc3b9fc2aef2427fda5e40c2d5dae9e894b1 Mon Sep 17 00:00:00 2001
From: jjanzen <jjanzen@jjanzen.ca>
Date: Mon, 20 Jan 2025 17:30:19 -0600
Subject: start migration to zig

---
 .github/workflows/c-cpp.yml |  25 -----
 .gitignore                  |   2 +
 Makefile.am                 |   1 -
 build.zig                   |  93 ++++++++++++++++++
 build.zig.zon               |  72 ++++++++++++++
 configure.ac                |  16 ---
 src/Makefile.am             |  13 ---
 src/actions.zig             |  12 +++
 src/cavegen.c               | 103 -------------------
 src/cavegen.h               |  37 -------
 src/common.h                |  20 ----
 src/display.c               | 223 -----------------------------------------
 src/display.h               |  54 ----------
 src/ecs/entity.zig          |   3 +
 src/entity.h                |  24 -----
 src/frontend/ncurses.zig    | 186 +++++++++++++++++++++++++++++++++++
 src/ht.c                    | 235 --------------------------------------------
 src/ht.h                    |  40 --------
 src/main.c                  | 192 ------------------------------------
 src/main.zig                |  21 ++++
 src/root.zig                |  10 ++
 21 files changed, 399 insertions(+), 983 deletions(-)
 delete mode 100644 .github/workflows/c-cpp.yml
 delete mode 100644 Makefile.am
 create mode 100644 build.zig
 create mode 100644 build.zig.zon
 delete mode 100644 configure.ac
 delete mode 100644 src/Makefile.am
 create mode 100644 src/actions.zig
 delete mode 100644 src/cavegen.c
 delete mode 100644 src/cavegen.h
 delete mode 100644 src/common.h
 delete mode 100644 src/display.c
 delete mode 100644 src/display.h
 create mode 100644 src/ecs/entity.zig
 delete mode 100644 src/entity.h
 create mode 100644 src/frontend/ncurses.zig
 delete mode 100644 src/ht.c
 delete mode 100644 src/ht.h
 delete mode 100644 src/main.c
 create mode 100644 src/main.zig
 create mode 100644 src/root.zig

diff --git a/.github/workflows/c-cpp.yml b/.github/workflows/c-cpp.yml
deleted file mode 100644
index 3342e3d..0000000
--- a/.github/workflows/c-cpp.yml
+++ /dev/null
@@ -1,25 +0,0 @@
-name: C/C++ CI
-
-on:
-  push:
-    branches: [ "main" ]
-  pull_request:
-    branches: [ "main" ]
-
-jobs:
-  build:
-
-    runs-on: ubuntu-latest
-
-    steps:
-    - uses: actions/checkout@v3
-    - name: autoreconf
-      run: autoreconf --install
-    - name: configure
-      run: ./configure
-    - name: make
-      run: make
-    - name: make check
-      run: make check
-    - name: make distcheck
-      run: make distcheck
diff --git a/.gitignore b/.gitignore
index 686c83a..b47d81c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,3 +19,5 @@ Makefile
 /stamp-h1
 /*.gz
 /src/urlg
+/.zig-cache/
+/zig-out/
diff --git a/Makefile.am b/Makefile.am
deleted file mode 100644
index af437a6..0000000
--- a/Makefile.am
+++ /dev/null
@@ -1 +0,0 @@
-SUBDIRS = src
diff --git a/build.zig b/build.zig
new file mode 100644
index 0000000..0f62653
--- /dev/null
+++ b/build.zig
@@ -0,0 +1,93 @@
+const std = @import("std");
+
+// Although this function looks imperative, note that its job is to
+// declaratively construct a build graph that will be executed by an external
+// runner.
+pub fn build(b: *std.Build) void {
+    // Standard target options allows the person running `zig build` to choose
+    // what target to build for. Here we do not override the defaults, which
+    // means any target is allowed, and the default is native. Other options
+    // for restricting supported target set are available.
+    const target = b.standardTargetOptions(.{});
+
+    // Standard optimization options allow the person running `zig build` to select
+    // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not
+    // set a preferred release mode, allowing the user to decide how to optimize.
+    const optimize = b.standardOptimizeOption(.{});
+
+    const lib = b.addStaticLibrary(.{
+        .name = "simple-lib-use",
+        // In this case the main source file is merely a path, however, in more
+        // complicated build scripts, this could be a generated file.
+        .root_source_file = b.path("src/root.zig"),
+        .target = target,
+        .optimize = optimize,
+    });
+
+    // This declares intent for the library to be installed into the standard
+    // location when the user invokes the "install" step (the default step when
+    // running `zig build`).
+    b.installArtifact(lib);
+
+    const exe = b.addExecutable(.{
+        .name = "simple-lib-use",
+        .root_source_file = b.path("src/main.zig"),
+        .target = target,
+        .optimize = optimize,
+    });
+    exe.linkSystemLibrary("c");
+    exe.linkSystemLibrary("ncurses");
+
+    // This declares intent for the executable to be installed into the
+    // standard location when the user invokes the "install" step (the default
+    // step when running `zig build`).
+    b.installArtifact(exe);
+
+    // This *creates* a Run step in the build graph, to be executed when another
+    // step is evaluated that depends on it. The next line below will establish
+    // such a dependency.
+    const run_cmd = b.addRunArtifact(exe);
+
+    // By making the run step depend on the install step, it will be run from the
+    // installation directory rather than directly from within the cache directory.
+    // This is not necessary, however, if the application depends on other installed
+    // files, this ensures they will be present and in the expected location.
+    run_cmd.step.dependOn(b.getInstallStep());
+
+    // This allows the user to pass arguments to the application in the build
+    // command itself, like this: `zig build run -- arg1 arg2 etc`
+    if (b.args) |args| {
+        run_cmd.addArgs(args);
+    }
+
+    // This creates a build step. It will be visible in the `zig build --help` menu,
+    // and can be selected like this: `zig build run`
+    // This will evaluate the `run` step rather than the default, which is "install".
+    const run_step = b.step("run", "Run the app");
+    run_step.dependOn(&run_cmd.step);
+
+    // Creates a step for unit testing. This only builds the test executable
+    // but does not run it.
+    const lib_unit_tests = b.addTest(.{
+        .root_source_file = b.path("src/root.zig"),
+        .target = target,
+        .optimize = optimize,
+    });
+
+    const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests);
+
+    const exe_unit_tests = b.addTest(.{
+        .root_source_file = b.path("src/main.zig"),
+        .target = target,
+        .optimize = optimize,
+    });
+
+    const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests);
+
+    // Similar to creating the run step earlier, this exposes a `test` step to
+    // the `zig build --help` menu, providing a way for the user to request
+    // running the unit tests.
+    const test_step = b.step("test", "Run unit tests");
+    test_step.dependOn(&run_lib_unit_tests.step);
+    test_step.dependOn(&run_exe_unit_tests.step);
+}
diff --git a/build.zig.zon b/build.zig.zon
new file mode 100644
index 0000000..cf22c40
--- /dev/null
+++ b/build.zig.zon
@@ -0,0 +1,72 @@
+.{
+    // This is the default name used by packages depending on this one. For
+    // example, when a user runs `zig fetch --save <url>`, this field is used
+    // as the key in the `dependencies` table. Although the user can choose a
+    // different name, most users will stick with this provided value.
+    //
+    // It is redundant to include "zig" in this name because it is already
+    // within the Zig package namespace.
+    .name = "simple-lib-use",
+
+    // This is a [Semantic Version](https://semver.org/).
+    // In a future version of Zig it will be used for package deduplication.
+    .version = "0.0.0",
+
+    // This field is optional.
+    // This is currently advisory only; Zig does not yet do anything
+    // with this value.
+    //.minimum_zig_version = "0.11.0",
+
+    // This field is optional.
+    // Each dependency must either provide a `url` and `hash`, or a `path`.
+    // `zig build --fetch` can be used to fetch all dependencies of a package, recursively.
+    // Once all dependencies are fetched, `zig build` no longer requires
+    // internet connectivity.
+    .dependencies = .{
+        // See `zig fetch --save <url>` for a command-line interface for adding dependencies.
+        //.example = .{
+        //    // When updating this field to a new URL, be sure to delete the corresponding
+        //    // `hash`, otherwise you are communicating that you expect to find the old hash at
+        //    // the new URL.
+        //    .url = "https://example.com/foo.tar.gz",
+        //
+        //    // This is computed from the file contents of the directory of files that is
+        //    // obtained after fetching `url` and applying the inclusion rules given by
+        //    // `paths`.
+        //    //
+        //    // This field is the source of truth; packages do not come from a `url`; they
+        //    // come from a `hash`. `url` is just one of many possible mirrors for how to
+        //    // obtain a package matching this `hash`.
+        //    //
+        //    // Uses the [multihash](https://multiformats.io/multihash/) format.
+        //    .hash = "...",
+        //
+        //    // When this is provided, the package is found in a directory relative to the
+        //    // build root. In this case the package's hash is irrelevant and therefore not
+        //    // computed. This field and `url` are mutually exclusive.
+        //    .path = "foo",
+
+        //    // When this is set to `true`, a package is declared to be lazily
+        //    // fetched. This makes the dependency only get fetched if it is
+        //    // actually used.
+        //    .lazy = false,
+        //},
+    },
+
+    // Specifies the set of files and directories that are included in this package.
+    // Only files and directories listed here are included in the `hash` that
+    // is computed for this package. Only files listed here will remain on disk
+    // when using the zig package manager. As a rule of thumb, one should list
+    // files required for compilation plus any license(s).
+    // Paths are relative to the build root. Use the empty string (`""`) to refer to
+    // the build root itself.
+    // A directory listed here means that all files within, recursively, are included.
+    .paths = .{
+        "build.zig",
+        "build.zig.zon",
+        "src",
+        // For example...
+        //"LICENSE",
+        //"README.md",
+    },
+}
diff --git a/configure.ac b/configure.ac
deleted file mode 100644
index 5ded62c..0000000
--- a/configure.ac
+++ /dev/null
@@ -1,16 +0,0 @@
-AC_PREREQ([2.65])
-AC_INIT([urlg], [0.0.1-alpha], [jacob.a.s.janzen@gmail.com])
-AC_CONFIG_SRCDIR([src/main.c])
-AC_CONFIG_AUX_DIR([build-aux])
-
-AC_PROG_CC
-AC_CHECK_LIB([ncurses], [initscr])
-AC_CHECK_LIB([tinfo], [stdscr])
-AC_CHECK_HEADERS([ncurses.h])
-
-AM_INIT_AUTOMAKE([foreign -Wall -Werror])
-
-AC_CONFIG_HEADERS([config.h])
-AC_CONFIG_FILES([Makefile src/Makefile])
-
-AC_OUTPUT
diff --git a/src/Makefile.am b/src/Makefile.am
deleted file mode 100644
index f08ba63..0000000
--- a/src/Makefile.am
+++ /dev/null
@@ -1,13 +0,0 @@
-bin_PROGRAMS = urlg
-
-urlg_SOURCES = \
-		cavegen.c \
-		cavegen.h \
-		display.c \
-		display.h \
-		ht.c \
-		ht.h \
-		common.h \
-		entity.h \
-		main.c
-
diff --git a/src/actions.zig b/src/actions.zig
new file mode 100644
index 0000000..a801e28
--- /dev/null
+++ b/src/actions.zig
@@ -0,0 +1,12 @@
+const std = @import("std");
+
+pub const Action = enum {
+    illegal,
+    exit,
+    move_up,
+    move_down,
+    move_left,
+    move_right,
+    down_stair,
+    up_stair,
+};
diff --git a/src/cavegen.c b/src/cavegen.c
deleted file mode 100644
index 8a74836..0000000
--- a/src/cavegen.c
+++ /dev/null
@@ -1,103 +0,0 @@
-/*
-This file is part of urlg.
-urlg is free software: you can redistribute it and/or modify it under the terms
-of the GNU General Public License as published by the Free Software Foundation,
-either version 3 of the License, or (at your option) any later version. urlg is
-distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
-without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
-PURPOSE. See the GNU General Public License for more details. You should have
-received a copy of the GNU General Public License along with urlg. If not, see
-<https://www.gnu.org/licenses/>.
-*/
-#include "cavegen.h"
-
-#include <stdbool.h>
-#include <stdio.h>
-#include <stdlib.h>
-
-#define MAX_STEPS   200
-#define NUM_WALKERS 100
-
-enum direction {
-    UP,
-    LEFT,
-    RIGHT,
-    DOWN,
-    NUM_DIRS,
-};
-
-void create_cave(struct map *map)
-{
-    map->width  = WIDTH;
-    map->height = HEIGHT;
-
-    // create a map consisting entirely of walls
-    map->map = malloc(sizeof(enum tile_type) * HEIGHT * WIDTH);
-    for (int i = 0; i < HEIGHT; ++i) {
-        for (int j = 0; j < WIDTH; ++j) {
-            map->map[i * WIDTH + j] = WALL;
-        }
-    }
-
-    // start in the middle of the screen
-    int start_x = WIDTH / 2;
-    int start_y = HEIGHT / 2;
-
-    // make the starting point GROUND
-    map->map[start_y * WIDTH + start_x] = GROUND;
-
-    // setup the open tiles
-    struct point *open_tiles = malloc(sizeof(struct point) * HEIGHT * WIDTH);
-
-    int num_open_tiles = 1;
-    open_tiles[0].x    = start_x;
-    open_tiles[0].y    = start_y;
-
-    for (int i = 0; i < NUM_WALKERS; ++i) {
-        // get a random open point
-        struct point curr_point = open_tiles[rand() % num_open_tiles];
-
-        int x_pos = curr_point.x;
-        int y_pos = curr_point.y;
-
-        // iterate until the walk exits the array or MAX_STEPS is reached
-        for (int j = 1; j < MAX_STEPS - 1 && x_pos < WIDTH - 1 && x_pos >= 1 &&
-                        y_pos < HEIGHT - 1 && y_pos >= 1;
-             ++j) {
-            // add new open point if the current point is still a wall
-            if (map->map[y_pos * WIDTH + x_pos] == WALL) {
-                open_tiles[num_open_tiles].x = x_pos;
-                open_tiles[num_open_tiles].y = y_pos;
-                ++num_open_tiles;
-            }
-
-            map->map[y_pos * WIDTH + x_pos] = GROUND; // assign ground
-
-            // move in a random direction
-            enum direction dir = rand() % NUM_DIRS;
-            switch (dir) {
-            case UP : --y_pos; break;
-            case LEFT : --x_pos; break;
-            case RIGHT : ++x_pos; break;
-            case DOWN : ++y_pos; break;
-            default : exit(EXIT_FAILURE); // should not occur
-            }
-        }
-    }
-
-    // assign the up stair and remove from open tiles
-    int in           = rand() % num_open_tiles;
-    map->entry_point = open_tiles[in];
-    open_tiles[in]   = open_tiles[num_open_tiles - 1];
-    --num_open_tiles;
-    map->map[map->entry_point.y * WIDTH + map->entry_point.x] = UP_STAIR;
-
-    // assign the down stair and remove from open tiles
-    in                = rand() % num_open_tiles;
-    struct point down = open_tiles[in];
-    open_tiles[in]    = open_tiles[num_open_tiles - 1];
-    --num_open_tiles;
-    map->map[down.y * WIDTH + down.x] = DOWN_STAIR;
-
-    free(open_tiles);
-}
diff --git a/src/cavegen.h b/src/cavegen.h
deleted file mode 100644
index ae5f698..0000000
--- a/src/cavegen.h
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
-This file is part of urlg.
-urlg is free software: you can redistribute it and/or modify it under the terms
-of the GNU General Public License as published by the Free Software Foundation,
-either version 3 of the License, or (at your option) any later version. urlg is
-distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
-without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
-PURPOSE. See the GNU General Public License for more details. You should have
-received a copy of the GNU General Public License along with urlg. If not, see
-<https://www.gnu.org/licenses/>.
-*/
-#ifndef CAVEGEN_H_
-#define CAVEGEN_H_
-
-#include "common.h"
-
-#define HEIGHT 100
-#define WIDTH  180
-
-enum tile_type {
-    WALL,
-    GROUND,
-    UP_STAIR,
-    DOWN_STAIR,
-};
-
-struct map {
-    enum tile_type *map;
-    struct point    entry_point;
-
-    int width;
-    int height;
-};
-
-void create_cave(struct map *map);
-
-#endif // CAVEGEN_H_
diff --git a/src/common.h b/src/common.h
deleted file mode 100644
index c36f0f2..0000000
--- a/src/common.h
+++ /dev/null
@@ -1,20 +0,0 @@
-/*
-This file is part of urlg.
-urlg is free software: you can redistribute it and/or modify it under the terms
-of the GNU General Public License as published by the Free Software Foundation,
-either version 3 of the License, or (at your option) any later version. urlg is
-distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
-without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
-PURPOSE. See the GNU General Public License for more details. You should have
-received a copy of the GNU General Public License along with urlg. If not, see
-<https://www.gnu.org/licenses/>.
-*/
-#ifndef COMMON_H_
-#define COMMON_H_
-
-struct point {
-    int x;
-    int y;
-};
-
-#endif // COMMON_H_
diff --git a/src/display.c b/src/display.c
deleted file mode 100644
index e79ed2c..0000000
--- a/src/display.c
+++ /dev/null
@@ -1,223 +0,0 @@
-/*
-This file is part of urlg.
-urlg is free software: you can redistribute it and/or modify it under the terms
-of the GNU General Public License as published by the Free Software Foundation,
-either version 3 of the License, or (at your option) any later version. urlg is
-distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
-without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
-PURPOSE. See the GNU General Public License for more details. You should have
-received a copy of the GNU General Public License along with urlg. If not, see
-<https://www.gnu.org/licenses/>.
-*/
-#include "display.h"
-
-#include <curses.h>
-#include <locale.h>
-#include <stdlib.h>
-#include <string.h>
-
-#include "entity.h"
-
-struct windows {
-    WINDOW *main;
-    WINDOW *inst;
-    WINDOW *msgs;
-    WINDOW *stat;
-};
-
-static WINDOW *create_newwin(int height, int width, int y, int x)
-{
-    WINDOW *local_win = newwin(height, width, y, x);
-    box(local_win, 0, 0);
-    wrefresh(local_win);
-
-    return local_win;
-}
-
-static display_t *create_windows(void)
-{
-    display_t *wins = malloc(sizeof(display_t));
-
-    wins->inst = create_newwin(
-        INSTRUCTION_PANEL_HEIGHT, INSTRUCTION_PANEL_WIDTH,
-        (LINES - INSTRUCTION_PANEL_HEIGHT - STATUS_PANEL_HEIGHT) / 2 + 1,
-        (COLS - MAIN_PANEL_WIDTH - INSTRUCTION_PANEL_WIDTH) / 2 +
-            MAIN_PANEL_WIDTH - 1
-    );
-    wins->msgs = create_newwin(
-        MESSAGE_PANEL_HEIGHT, MESSAGE_PANEL_WIDTH,
-        (LINES - MAIN_PANEL_HEIGHT - MESSAGE_PANEL_HEIGHT) / 2 +
-            MAIN_PANEL_HEIGHT,
-        (COLS - MESSAGE_PANEL_WIDTH - INSTRUCTION_PANEL_WIDTH) / 2
-    );
-    wins->stat = create_newwin(
-        STATUS_PANEL_HEIGHT, STATUS_PANEL_WIDTH,
-        (LINES - INSTRUCTION_PANEL_HEIGHT - STATUS_PANEL_HEIGHT) / 2 +
-            INSTRUCTION_PANEL_HEIGHT,
-        (COLS - MAIN_PANEL_WIDTH - INSTRUCTION_PANEL_WIDTH) / 2 +
-            MAIN_PANEL_WIDTH - 1
-    );
-    wins->main = create_newwin(
-        MAIN_PANEL_HEIGHT, MAIN_PANEL_WIDTH,
-        (LINES - MAIN_PANEL_HEIGHT - MESSAGE_PANEL_HEIGHT) / 2 + 1,
-        (COLS - MAIN_PANEL_WIDTH - INSTRUCTION_PANEL_WIDTH) / 2
-    );
-
-    return wins;
-}
-
-display_t *display_init(void)
-{
-    setlocale(LC_ALL, ""); // allow extended ASCII
-
-    initscr(); // initialize curses
-
-    // exit on unsupported consoles
-    if (LINES < MAIN_PANEL_HEIGHT + MESSAGE_PANEL_HEIGHT ||
-        COLS < MAIN_PANEL_WIDTH + INSTRUCTION_PANEL_WIDTH || !has_colors()) {
-        endwin();
-        fprintf(
-            stderr,
-            "a color terminal is required with at least %dx%d characters\n",
-            INSTRUCTION_PANEL_WIDTH + MAIN_PANEL_WIDTH,
-            MAIN_PANEL_HEIGHT + MESSAGE_PANEL_HEIGHT
-        );
-        return NULL;
-    }
-
-    // configure curses if startup was successful
-    raw();                // disable line buffering
-    keypad(stdscr, TRUE); // enable reading function keys
-    noecho();             // don't print input
-    curs_set(0);          // disable the cursor
-    start_color();        // enable colours
-
-    // setup colours
-    init_pair(1, COLOR_WHITE, COLOR_BLACK);
-    init_pair(2, COLOR_BLACK, COLOR_RED);
-    wattron(stdscr, COLOR_PAIR(1));
-    refresh();
-
-    return create_windows();
-}
-
-void display_destroy(display_t *disp)
-{
-    delwin(disp->main);
-    delwin(disp->inst);
-    delwin(disp->msgs);
-    delwin(disp->stat);
-
-    free(disp);
-}
-
-void display_map(display_t *disp, struct map *map, ht_t *entities)
-{
-    // print map
-    struct entity *camera = ht_find(entities, "camera");
-
-    for (int i = 1; i < MAIN_PANEL_HEIGHT - 1; ++i) {
-        for (int j = 1; j < MAIN_PANEL_WIDTH - 1; ++j) {
-            int map_i = i - 1 + camera->p.y;
-            int map_j = j - 1 + camera->p.x;
-
-            if (map_i > map->height || map_j > map->width || map_i < 0 ||
-                map_j < 0) {
-                mvwaddch(disp->main, i, j, ' ');
-            } else {
-                switch (map->map[map_i * map->width + map_j]) {
-                case GROUND : mvwaddch(disp->main, i, j, '.'); break;
-                case UP_STAIR : mvwaddch(disp->main, i, j, '<'); break;
-                case DOWN_STAIR :
-                    wattron(disp->main, COLOR_PAIR(2));
-                    mvwaddch(disp->main, i, j, '>');
-                    wattroff(disp->main, COLOR_PAIR(2));
-                    break;
-                case WALL :
-                    if (map_i > 0 &&
-                        map->map[(map_i - 1) * map->width + map_j] != WALL) {
-                        mvwaddch(disp->main, i, j, ACS_BLOCK);
-                    } else if (map_i < map->width - 1 && map->map[(map_i + 1) * map->width + map_j] != WALL) {
-                        mvwaddch(disp->main, i, j, ACS_BLOCK);
-                    } else if (map_j > 0 && map->map[map_i * map->width + map_j - 1] != WALL) {
-                        mvwaddch(disp->main, i, j, ACS_BLOCK);
-                    } else if (map_j < map->width - 1 && map->map[map_i * map->width + map_j + 1] != WALL) {
-                        mvwaddch(disp->main, i, j, ACS_BLOCK);
-                    } else {
-                        mvwaddch(disp->main, i, j, ' ');
-                    }
-                    break;
-                default : mvwaddch(disp->main, i, j, ' ');
-                }
-            }
-        }
-    }
-
-    // print entities
-    ht_iter_init(entities);
-    struct kvp kvp = ht_iter_next(entities);
-    while (kvp.key) {
-        struct entity *e = kvp.val;
-        if (e->visible) {
-            mvwaddch(
-                disp->main, e->p.y - camera->p.y + 1, e->p.x - camera->p.x + 1,
-                e->disp_ch[0]
-            );
-        }
-
-        kvp = ht_iter_next(entities);
-    }
-
-    wrefresh(disp->main);
-}
-
-void display_instructions(display_t *disp)
-{
-    mvwprintw(disp->inst, 1, 2, "h - move left");
-    mvwprintw(disp->inst, 2, 2, "j - move down");
-    mvwprintw(disp->inst, 3, 2, "k - move up");
-    mvwprintw(disp->inst, 4, 2, "l - move right");
-    mvwprintw(disp->inst, 5, 2, "> - move down staircase");
-    mvwprintw(disp->inst, 6, 2, "< - exit via staircase");
-    wrefresh(disp->inst);
-}
-
-void display_message(display_t *disp, char *msg)
-{
-    for (int i = 1; i < MESSAGE_PANEL_WIDTH - 1; ++i) {
-        mvwaddch(disp->msgs, 1, i, ' ');
-    }
-
-    mvwprintw(disp->msgs, 1, 1, msg);
-    wrefresh(disp->msgs);
-}
-
-void display_status(display_t *disp, struct entity *entity)
-{
-    for (int i = 1; i < STATUS_PANEL_HEIGHT - 1; ++i) {
-        for (int j = 1; j < STATUS_PANEL_WIDTH - 1; ++j) {
-            mvwaddch(disp->stat, i, j, ' ');
-        }
-    }
-
-    mvwprintw(disp->stat, 1, 2, "HP:");
-    mvwprintw(disp->stat, 2, 2, "STAMINA:");
-    mvwprintw(disp->stat, 3, 2, "MANA:");
-
-    wrefresh(disp->stat);
-}
-
-enum action display_process_input(void)
-{
-    int ch = getch();
-    switch (ch) {
-    case 'k' : return ACTION_UP;
-    case 'j' : return ACTION_DOWN;
-    case 'h' : return ACTION_LEFT;
-    case 'l' : return ACTION_RIGHT;
-    case '>' : return ACTION_STAIR_DOWN;
-    case '<' : return ACTION_STAIR_UP;
-    case KEY_F(1) : return ACTION_EXIT;
-    default : return ACTION_NONE;
-    }
-}
diff --git a/src/display.h b/src/display.h
deleted file mode 100644
index b5d7d53..0000000
--- a/src/display.h
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
-This file is part of urlg.
-urlg is free software: you can redistribute it and/or modify it under the terms
-of the GNU General Public License as published by the Free Software Foundation,
-either version 3 of the License, or (at your option) any later version. urlg is
-distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
-without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
-PURPOSE. See the GNU General Public License for more details. You should have
-received a copy of the GNU General Public License along with urlg. If not, see
-<https://www.gnu.org/licenses/>.
-*/
-#ifndef DISPLAY_H_
-#define DISPLAY_H_
-
-#include <stdbool.h>
-
-#include "cavegen.h"
-#include "entity.h"
-#include "ht.h"
-
-#define MAIN_PANEL_WIDTH         100
-#define MAIN_PANEL_HEIGHT        41
-#define INSTRUCTION_PANEL_WIDTH  32
-#define INSTRUCTION_PANEL_HEIGHT 39
-#define MESSAGE_PANEL_WIDTH      100
-#define MESSAGE_PANEL_HEIGHT     3
-#define STATUS_PANEL_WIDTH       32
-#define STATUS_PANEL_HEIGHT      5
-
-typedef struct windows display_t;
-
-enum action {
-    ACTION_NONE,
-    ACTION_EXIT,
-    ACTION_DOWN,
-    ACTION_UP,
-    ACTION_LEFT,
-    ACTION_RIGHT,
-    ACTION_STAIR_DOWN,
-    ACTION_STAIR_UP,
-    NUM_ACTIONS,
-};
-
-display_t *display_init(void);
-void       display_destroy(display_t *disp);
-
-void display_map(display_t *disp, struct map *map, ht_t *entities);
-void display_instructions(display_t *disp);
-void display_message(display_t *disp, char *msg);
-void display_status(display_t *disp, struct entity *entity);
-
-enum action display_process_input(void);
-
-#endif // DISPLAY_H_
diff --git a/src/ecs/entity.zig b/src/ecs/entity.zig
new file mode 100644
index 0000000..6131cff
--- /dev/null
+++ b/src/ecs/entity.zig
@@ -0,0 +1,3 @@
+const std = @import("std");
+
+pub const Entity = struct {};
diff --git a/src/entity.h b/src/entity.h
deleted file mode 100644
index f59d868..0000000
--- a/src/entity.h
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
-This file is part of urlg.
-urlg is free software: you can redistribute it and/or modify it under the terms
-of the GNU General Public License as published by the Free Software Foundation,
-either version 3 of the License, or (at your option) any later version. urlg is
-distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
-without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
-PURPOSE. See the GNU General Public License for more details. You should have
-received a copy of the GNU General Public License along with urlg. If not, see
-<https://www.gnu.org/licenses/>.
-*/
-#ifndef ENTITY_H_
-#define ENTITY_H_
-
-#include "common.h"
-
-struct entity {
-    struct point p;
-    char        *disp_ch;
-    bool         solid;
-    bool         visible;
-};
-
-#endif // ENTITY_H_
diff --git a/src/frontend/ncurses.zig b/src/frontend/ncurses.zig
new file mode 100644
index 0000000..1586cf0
--- /dev/null
+++ b/src/frontend/ncurses.zig
@@ -0,0 +1,186 @@
+const std = @import("std");
+const Entity = @import("../ecs/entity.zig").Entity;
+const Action = @import("../actions.zig").Action;
+const ncurses = @cImport({
+    @cInclude("ncurses.h");
+});
+const locale = @cImport({
+    @cInclude("locale.h");
+});
+
+const MAIN_PANEL_WIDTH = 100;
+const MAIN_PANEL_HEIGHT = 41;
+const INSTRUCTION_PANEL_WIDTH = 32;
+const INSTRUCTION_PANEL_HEIGHT = 39;
+const MESSAGE_PANEL_WIDTH = 100;
+const MESSAGE_PANEL_HEIGHT = 3;
+const STATUS_PANEL_WIDTH = 32;
+const STATUS_PANEL_HEIGHT = 5;
+
+const KEY_MOVE_UP: i32 = 'k';
+const KEY_MOVE_DOWN: i32 = 'j';
+const KEY_MOVE_LEFT: i32 = 'h';
+const KEY_MOVE_RIGHT: i32 = 'l';
+const KEY_STAIR_UP: i32 = '<';
+const KEY_STAIR_DOWN: i32 = '>';
+const KEY_QUIT: i32 = ncurses.KEY_F(1);
+
+pub const Display = struct {
+    main: ?*ncurses.WINDOW,
+    inst: ?*ncurses.WINDOW,
+    msgs: ?*ncurses.WINDOW,
+    stat: ?*ncurses.WINDOW,
+    allocator: std.mem.Allocator,
+
+    fn validTermSize() bool {
+        return ncurses.LINES >= MAIN_PANEL_HEIGHT + MESSAGE_PANEL_HEIGHT and
+            ncurses.COLS >= MAIN_PANEL_WIDTH + INSTRUCTION_PANEL_WIDTH;
+    }
+
+    fn colorSupport() bool {
+        return ncurses.has_colors();
+    }
+
+    fn createNewWin(height: i32, width: i32, y: i32, x: i32) ?*ncurses.WINDOW {
+        const local_win = ncurses.newwin(height, width, y, x);
+        _ = ncurses.box(local_win, 0, 0);
+        _ = ncurses.wrefresh(local_win);
+        return local_win;
+    }
+
+    fn formatInstruction(self: *Display, pos: i32, key: i32, description: [:0]const u8) void {
+        const c_description: ?[*:0]const u8 = description.ptr;
+
+        if (ncurses.KEY_F0 <= key and key <= ncurses.KEY_F(64)) {
+            _ = ncurses.mvwprintw(self.inst, pos, 2, "F%d - %s", key - ncurses.KEY_F0, c_description);
+        } else if (key < 128) {
+            _ = ncurses.mvwprintw(self.inst, pos, 2, "%c - %s", key, c_description);
+        } else {
+            self.displayMessage("Invalid key name: {d}", .{key});
+        }
+    }
+
+    pub fn displayInstructions(self: *Display) void {
+        self.formatInstruction(1, KEY_MOVE_LEFT, "move left");
+        self.formatInstruction(2, KEY_MOVE_DOWN, "move down");
+        self.formatInstruction(3, KEY_MOVE_UP, "move up");
+        self.formatInstruction(4, KEY_MOVE_RIGHT, "move right");
+        self.formatInstruction(5, KEY_STAIR_DOWN, "move down staircase");
+        self.formatInstruction(6, KEY_STAIR_UP, "move up staircase");
+        self.formatInstruction(7, KEY_QUIT, "quit");
+        _ = ncurses.wrefresh(self.inst);
+    }
+
+    pub fn displayMessage(self: *Display, comptime fmt: []const u8, args: anytype) void {
+        for (1..MESSAGE_PANEL_WIDTH - 1) |i| {
+            const i_32 = @as(i32, @intCast(i));
+            _ = ncurses.mvwaddch(self.msgs, 1, i_32, ' ');
+        }
+
+        var buf: [MESSAGE_PANEL_WIDTH:0]u8 = undefined;
+        const data = std.fmt.bufPrint(&buf, fmt, args) catch {
+            self.displayMessage("Message too long", .{});
+            return;
+        };
+        buf[data.len] = 0;
+
+        const msg: [:0]u8 = &buf;
+
+        const c_msg: ?[*:0]const u8 = msg.ptr;
+        _ = ncurses.mvwprintw(self.msgs, 1, 1, c_msg);
+        _ = ncurses.wrefresh(self.msgs);
+    }
+
+    pub fn displayStatus(self: *Display, entity: *const Entity) void {
+        for (1..STATUS_PANEL_HEIGHT - 1) |i| {
+            for (1..STATUS_PANEL_WIDTH - 1) |j| {
+                const i_32 = @as(i32, @intCast(i));
+                const j_32 = @as(i32, @intCast(j));
+                _ = ncurses.mvwaddch(self.stat, i_32, j_32, ' ');
+            }
+        }
+
+        _ = entity;
+        _ = ncurses.wrefresh(self.stat);
+    }
+
+    pub fn processInput(self: *Display) Action {
+        _ = self;
+
+        const ch = ncurses.getch();
+        return switch (ch) {
+            KEY_MOVE_UP => Action.move_up,
+            KEY_MOVE_DOWN => Action.move_down,
+            KEY_MOVE_LEFT => Action.move_left,
+            KEY_MOVE_RIGHT => Action.move_right,
+            KEY_STAIR_DOWN => Action.down_stair,
+            KEY_STAIR_UP => Action.up_stair,
+            KEY_QUIT => Action.exit,
+            else => Action.illegal,
+        };
+    }
+
+    pub fn init() !Display {
+        _ = locale.setlocale(locale.LC_ALL, "");
+        if (ncurses.initscr() == null) {
+            return error.CursesInitFail;
+        }
+
+        if (!validTermSize()) {
+            _ = ncurses.endwin();
+            return error.InvalidTermSize;
+        }
+
+        if (!colorSupport()) {
+            _ = ncurses.endwin();
+            return error.NoColorSupport;
+        }
+
+        _ = ncurses.raw();
+        _ = ncurses.keypad(ncurses.stdscr, true);
+        _ = ncurses.noecho();
+        _ = ncurses.curs_set(0);
+        _ = ncurses.start_color();
+
+        _ = ncurses.init_pair(1, ncurses.COLOR_WHITE, ncurses.COLOR_BLACK);
+        _ = ncurses.init_pair(2, ncurses.COLOR_BLACK, ncurses.COLOR_RED);
+        _ = ncurses.wattron(ncurses.stdscr, ncurses.COLOR_PAIR(1));
+        _ = ncurses.refresh();
+
+        return Display{
+            .inst = createNewWin(
+                INSTRUCTION_PANEL_HEIGHT,
+                INSTRUCTION_PANEL_WIDTH,
+                @divTrunc(ncurses.LINES - INSTRUCTION_PANEL_HEIGHT - STATUS_PANEL_HEIGHT, 2) + 1,
+                @divTrunc(ncurses.COLS - MAIN_PANEL_WIDTH - INSTRUCTION_PANEL_WIDTH, 2) + MAIN_PANEL_WIDTH - 1,
+            ),
+            .msgs = createNewWin(
+                MESSAGE_PANEL_HEIGHT,
+                MESSAGE_PANEL_WIDTH,
+                @divTrunc(ncurses.LINES - MAIN_PANEL_HEIGHT - MESSAGE_PANEL_HEIGHT, 2) + MAIN_PANEL_HEIGHT,
+                @divTrunc(ncurses.COLS - MESSAGE_PANEL_WIDTH - INSTRUCTION_PANEL_WIDTH, 2),
+            ),
+            .stat = createNewWin(
+                STATUS_PANEL_HEIGHT,
+                STATUS_PANEL_WIDTH,
+                @divTrunc(ncurses.LINES - INSTRUCTION_PANEL_HEIGHT - STATUS_PANEL_HEIGHT, 2) + INSTRUCTION_PANEL_HEIGHT,
+                @divTrunc(ncurses.COLS - MAIN_PANEL_WIDTH - INSTRUCTION_PANEL_WIDTH, 2) + MAIN_PANEL_WIDTH - 1,
+            ),
+            .main = createNewWin(
+                MAIN_PANEL_HEIGHT,
+                MAIN_PANEL_WIDTH,
+                @divTrunc(ncurses.LINES - MAIN_PANEL_HEIGHT - MESSAGE_PANEL_HEIGHT, 2) + 1,
+                @divTrunc(ncurses.COLS - MAIN_PANEL_WIDTH - INSTRUCTION_PANEL_WIDTH, 2),
+            ),
+            .allocator = undefined,
+        };
+    }
+
+    pub fn deinit(self: *Display) void {
+        _ = ncurses.delwin(self.main);
+        _ = ncurses.delwin(self.inst);
+        _ = ncurses.delwin(self.msgs);
+        _ = ncurses.delwin(self.stat);
+        _ = ncurses.endwin();
+    }
+};
diff --git a/src/ht.c b/src/ht.c
deleted file mode 100644
index ff0a3a8..0000000
--- a/src/ht.c
+++ /dev/null
@@ -1,235 +0,0 @@
-/*
-This file is part of urlg.
-urlg is free software: you can redistribute it and/or modify it under the terms
-of the GNU General Public License as published by the Free Software Foundation,
-either version 3 of the License, or (at your option) any later version. urlg is
-distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
-without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
-PURPOSE. See the GNU General Public License for more details. You should have
-received a copy of the GNU General Public License along with urlg. If not, see
-<https://www.gnu.org/licenses/>.
-*/
-#include "ht.h"
-
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-
-struct node {
-    char        *key;
-    void        *val;
-    struct node *next;
-};
-
-struct hash_table {
-    struct node **vals;
-    int           max_size;
-    int           size;
-
-    int          curr_index;
-    struct node *curr_node;
-    bool         iterating;
-};
-
-static void rehash(ht_t *h, int newsize)
-{
-    ht_t *new_h = ht_create(newsize);
-
-    ht_iter_init(h);
-
-    struct kvp kvp = ht_iter_next(h);
-    while (kvp.key) {
-        ht_insert(new_h, kvp.key, kvp.val);
-        kvp = ht_iter_next(h);
-    }
-
-    // only destroy vals if it isn't NULL
-    if (h->vals) {
-        // iterate through the hash values
-        for (int i = 0; i < h->max_size; ++i) {
-            // iterate through the linked list and remove the value
-            struct node *curr = h->vals[i];
-            struct node *prev;
-            while (curr) {
-                prev = curr;
-                curr = curr->next;
-                free(prev->key);
-                free(prev);
-            }
-        }
-        free(h->vals);
-    }
-
-    h->max_size   = newsize;
-    h->vals       = new_h->vals;
-    h->curr_index = 0;
-    h->curr_node  = NULL;
-    h->iterating  = false;
-
-    free(new_h);
-}
-
-static unsigned long djb2_hash(char *str)
-{
-    unsigned long hash = 5381;
-    int           c;
-
-    // hash * 33 + c
-    while ((c = *str++)) hash = ((hash << 5) + hash) + c;
-
-    return hash;
-}
-
-ht_t *ht_create(int max_size)
-{
-    ht_t *h       = malloc(sizeof(ht_t));
-    h->max_size   = max_size;
-    h->size       = 0;
-    h->vals       = malloc(sizeof(struct node) * max_size);
-    h->curr_index = 0;
-    h->curr_node  = NULL;
-    h->iterating  = false;
-
-    return h;
-}
-
-void ht_destroy(ht_t *h)
-{
-    // only destroy if h isn't NULL
-    if (h) {
-        // only destroy vals if it isn't NULL
-        if (h->vals) {
-            // iterate through the hash values
-            for (int i = 0; i < h->max_size; ++i) {
-                // iterate through the linked list and remove the value
-                struct node *curr = h->vals[i];
-                struct node *prev;
-                while (curr) {
-                    prev = curr;
-                    curr = curr->next;
-                    free(prev->key);
-                    free(prev);
-                }
-            }
-            free(h->vals);
-        }
-        free(h);
-    }
-}
-
-void *ht_find(ht_t *h, char *key)
-{
-    unsigned int hash = djb2_hash(key) % h->max_size;
-
-    // exit early if hash isn't in table
-    if (!h->vals[hash])
-        return NULL;
-
-    // iterate through values stored at `hash`; exit early if found
-    struct node *curr = h->vals[hash];
-    while (curr) {
-        if (strcmp(key, curr->key) == 0)
-            return curr->val;
-        curr = curr->next;
-    }
-
-    return NULL;
-}
-
-void ht_insert(ht_t *h, char *key, void *val)
-{
-    if (h->size > h->max_size * 0.75)
-        rehash(h, h->max_size * 2);
-
-    unsigned int hash = djb2_hash(key) % h->max_size;
-
-    // create a node
-    int len          = strlen(key);
-    struct node *new = malloc(sizeof(struct node));
-    new->key         = malloc(sizeof(char) * (len + 1));
-    for (int i = 0; i < len; ++i) {
-        new->key[i] = key[i];
-    }
-    new->key[len] = 0;
-    new->val      = val;
-
-    // insert node at beginning of list
-    if (h->vals[hash]) {
-        new->next = h->vals[hash];
-    }
-    h->vals[hash] = new;
-
-    ++(h->size);
-}
-
-void ht_delete(ht_t *h, char *key)
-{
-    if (h->size < h->max_size * 0.25)
-        rehash(h, h->max_size * 0.5);
-
-    unsigned int hash = djb2_hash(key) % h->max_size;
-
-    if (!h->vals[hash])
-        return; // exit if the hash is not found
-
-    // remove the key from the front of the list if it's there
-    if (strcmp(key, h->vals[hash]->key) == 0) {
-        struct node *temp = h->vals[hash];
-        h->vals[hash]     = h->vals[hash]->next;
-        free(temp->key);
-        free(temp);
-    } else {
-        // iterate through list and remove once a match is found
-        struct node *prev = h->vals[hash];
-        struct node *curr = h->vals[hash]->next;
-        while (curr) {
-            if (strcmp(key, curr->key) == 0) {
-                prev->next = curr->next;
-                free(curr->key);
-                free(curr);
-                return;
-            }
-
-            prev = curr;
-            curr = curr->next;
-        }
-    }
-}
-
-int ht_size(ht_t *h)
-{
-    return h->size;
-}
-
-void ht_iter_init(ht_t *h)
-{
-    h->curr_index = 0;
-    h->curr_node  = h->vals[0];
-}
-
-struct kvp ht_iter_next(ht_t *h)
-{
-    struct kvp out = {
-        .key = NULL,
-        .val = NULL,
-    };
-
-    // return NULL if we've reached the end
-    if (!h->curr_node && h->curr_index >= h->max_size)
-        return out;
-
-    // look for next index with node if current node is NULL
-    if (!h->curr_node) {
-        while (!h->curr_node) {
-            h->curr_node = h->vals[h->curr_index++];
-            if (h->curr_index >= h->max_size)
-                return out;
-        }
-    }
-
-    // get the value and move to the next node
-    out.key      = h->curr_node->key;
-    out.val      = h->curr_node->val;
-    h->curr_node = h->curr_node->next;
-    return out;
-}
diff --git a/src/ht.h b/src/ht.h
deleted file mode 100644
index 236ae4e..0000000
--- a/src/ht.h
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
-This file is part of urlg.
-urlg is free software: you can redistribute it and/or modify it under the terms
-of the GNU General Public License as published by the Free Software Foundation,
-either version 3 of the License, or (at your option) any later version. urlg is
-distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
-without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
-PURPOSE. See the GNU General Public License for more details. You should have
-received a copy of the GNU General Public License along with urlg. If not, see
-<https://www.gnu.org/licenses/>.
-*/
-#ifndef HT_H_
-#define HT_H_
-
-#include <stdbool.h>
-
-typedef struct hash_table ht_t;
-
-struct kvp {
-    char *key;
-    void *val;
-};
-
-// construct and destructor
-ht_t *ht_create(int size);
-void  ht_destroy(ht_t *h);
-
-// accessors
-void *ht_find(ht_t *h, char *key);
-void  ht_insert(ht_t *h, char *key, void *val);
-void  ht_delete(ht_t *h, char *key);
-
-// queries
-int ht_size(ht_t *h);
-
-// iterator
-void       ht_iter_init(ht_t *h);
-struct kvp ht_iter_next(ht_t *h);
-
-#endif // HT_H_
diff --git a/src/main.c b/src/main.c
deleted file mode 100644
index 5278d3e..0000000
--- a/src/main.c
+++ /dev/null
@@ -1,192 +0,0 @@
-/*
-This file is part of urlg.
-urlg is free software: you can redistribute it and/or modify it under the terms
-of the GNU General Public License as published by the Free Software Foundation,
-either version 3 of the License, or (at your option) any later version. urlg is
-distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
-without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
-PURPOSE. See the GNU General Public License for more details. You should have
-received a copy of the GNU General Public License along with urlg. If not, see
-<https://www.gnu.org/licenses/>.
-*/
-#include <curses.h>
-#include <getopt.h>
-#include <locale.h>
-#include <stdlib.h>
-#include <time.h>
-
-#include "../config.h"
-#include "cavegen.h"
-#include "common.h"
-#include "display.h"
-#include "entity.h"
-#include "ht.h"
-
-bool entity_set_pos(struct entity *e, struct point p, struct map *map)
-{
-    if (e->solid) {
-        if (p.y < 0 || p.x < 0 || p.y >= map->height || p.x >= map->width) {
-            return false;
-        }
-        if (map->map[p.y * map->width + p.x] == WALL) {
-            return false;
-        }
-    }
-
-    e->p = p;
-
-    return true;
-}
-
-bool game_update(
-    display_t *disp, enum action action, ht_t *entities, struct map *map
-)
-{
-    struct entity *player = ht_find(entities, "player");
-    struct entity *camera = ht_find(entities, "camera");
-
-    struct point newp     = player->p;
-    struct point newp_cam = camera->p;
-    switch (action) {
-    case ACTION_EXIT : return true;
-    case ACTION_UP :
-        --newp.y;
-        --newp_cam.y;
-        display_message(disp, "moving up");
-        break;
-    case ACTION_DOWN :
-        ++newp.y;
-        ++newp_cam.y;
-        display_message(disp, "moving down");
-        break;
-    case ACTION_LEFT :
-        --newp.x;
-        --newp_cam.x;
-        display_message(disp, "moving left");
-        break;
-    case ACTION_RIGHT :
-        ++newp.x;
-        ++newp_cam.x;
-        display_message(disp, "moving right");
-        break;
-    case ACTION_STAIR_DOWN :
-        if (map->map[player->p.y * map->width + player->p.x] == DOWN_STAIR) {
-            free(map->map);
-            create_cave(map);
-
-            newp       = map->entry_point;
-            newp_cam.x = map->entry_point.x - MAIN_PANEL_WIDTH / 2 + 1;
-            newp_cam.y = map->entry_point.y - MAIN_PANEL_HEIGHT / 2 + 1;
-            display_message(disp, "moving down stairs");
-        } else {
-            display_message(disp, "no stairs to go down");
-        }
-        break;
-    case ACTION_STAIR_UP :
-        if (map->map[player->p.y * WIDTH + player->p.x] == UP_STAIR) {
-            display_message(disp, "moving up stairs");
-            return true;
-        } else {
-            display_message(disp, "no stairs to go up");
-        }
-        break;
-    default : display_message(disp, "unrecognized command"); break;
-    }
-
-    if (entity_set_pos(player, newp, map))
-        entity_set_pos(camera, newp_cam, map);
-
-    return false;
-}
-
-void print_version(void)
-{
-    printf("%s\n", PACKAGE_STRING);
-    printf("Copyright (C) 2024  Jacob Janzen\n");
-    printf("This is free software; see the source for copying conditions.\n");
-    printf(
-        "There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A\n"
-    );
-    printf("PARTICULAR PURPOSE.\n");
-}
-
-int main(int argc, char **argv)
-{
-    int           option_index = 0;
-    int           ch;
-    int           version_flag = 0;
-    struct option longopts[]   = {
-        {"version", no_argument, &version_flag, 'v'},
-    };
-    while ((ch = getopt_long(argc, argv, ":v", longopts, &option_index)) != -1
-    ) {
-        switch (ch) {
-        case 'v' : version_flag = 1; break;
-        case 0 : break;
-        default : break;
-        }
-    }
-    if (version_flag) {
-        print_version();
-        return EXIT_SUCCESS;
-    }
-
-    unsigned int seed = time(NULL);
-    srand(seed);
-
-    display_t *disp = display_init();
-
-    if (!disp) {
-        return EXIT_FAILURE;
-    }
-
-    // create the map
-    struct map map;
-    create_cave(&map);
-
-    // create the entity map
-    ht_t *entities = ht_create(1);
-
-    // create the camera
-    struct entity camera;
-    camera.disp_ch = " ";
-    camera.solid   = false;
-    camera.visible = false;
-    ht_insert(entities, "camera", &camera);
-
-    // create the player
-    struct entity player;
-    player.disp_ch = "@";
-    player.solid   = true;
-    player.visible = true;
-    ht_insert(entities, "player", &player);
-
-    // set starting point
-    struct point cam_p = {
-        .x = map.entry_point.x - MAIN_PANEL_WIDTH / 2 + 1,
-        .y = map.entry_point.y - MAIN_PANEL_HEIGHT / 2 + 1,
-    };
-    entity_set_pos(&player, map.entry_point, &map);
-    entity_set_pos(&camera, cam_p, &map);
-
-    // start displaying things
-    display_map(disp, &map, entities);
-    display_instructions(disp);
-    display_status(disp, &player);
-    display_message(disp, "");
-
-    bool done = false;
-    while (!done) {
-        enum action action = display_process_input();
-        done               = game_update(disp, action, entities, &map);
-        display_map(disp, &map, entities);
-    }
-
-    free(map.map);
-    ht_destroy(entities);
-    display_destroy(disp);
-
-    endwin();
-
-    return 0;
-}
diff --git a/src/main.zig b/src/main.zig
new file mode 100644
index 0000000..63cc2db
--- /dev/null
+++ b/src/main.zig
@@ -0,0 +1,21 @@
+const std = @import("std");
+const Entity = @import("ecs/entity.zig").Entity;
+const Display = @import("frontend/ncurses.zig").Display;
+const Action = @import("actions.zig").Action;
+
+pub fn main() u8 {
+    var d = Display.init() catch |err| {
+        std.log.err("{}", .{err});
+        return 1;
+    };
+    d.displayMessage("Initialized", .{});
+    d.displayInstructions();
+    d.displayStatus(&Entity{});
+    var action = Action.illegal;
+    while (action != Action.exit) {
+        action = d.processInput();
+    }
+    d.deinit();
+
+    return 0;
+}
diff --git a/src/root.zig b/src/root.zig
new file mode 100644
index 0000000..ecfeade
--- /dev/null
+++ b/src/root.zig
@@ -0,0 +1,10 @@
+const std = @import("std");
+const testing = std.testing;
+
+export fn add(a: i32, b: i32) i32 {
+    return a + b;
+}
+
+test "basic add functionality" {
+    try testing.expect(add(3, 7) == 10);
+}
-- 
cgit v1.2.3