summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRichard Braun <rbraun@sceen.net>2024-03-30 21:58:46 +0100
committerRichard Braun <rbraun@sceen.net>2024-03-30 21:58:46 +0100
commit7fbe749f9aee6c8c0ac74a409fdfd152f554e6a6 (patch)
treeedb434a7cbd8d96ff9df3591b5dd4f7c42482fa5
Initial commit
-rw-r--r--.gitignore4
-rw-r--r--LICENSE10
-rw-r--r--Makefile30
-rw-r--r--src/eetg.c719
-rw-r--r--src/eetg.h114
-rw-r--r--src/ei.c1184
-rw-r--r--src/ei.h91
-rw-r--r--src/macros.h27
-rw-r--r--src/main.c98
9 files changed, 2277 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..9783bf6
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+.*
+*.o
+cscope.out
+/embedded_invaders
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..7ae8865
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,10 @@
+Permission to use, copy, modify, and/or distribute this software for
+any purpose with or without fee is hereby granted.
+
+THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL
+WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES
+OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE
+FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY
+DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
+AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..b681393
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,30 @@
+MAKEFLAGS += --no-builtin-rules
+MAKEFLAGS += --no-builtin-variables
+
+CC = gcc
+
+BINARY = embedded_invaders
+
+CFLAGS = -std=gnu11
+CFLAGS += -O0 -g
+CFLAGS += -m32
+CFLAGS += -Wall -Wextra -Werror=implicit
+CFLAGS += -Wmissing-prototypes -Wstrict-prototypes -Wshadow
+
+SOURCES = \
+ src/main.c \
+ src/eetg.c \
+ src/ei.c
+
+OBJECTS = $(patsubst %.S,%.o,$(patsubst %.c,%.o,$(SOURCES)))
+
+$(BINARY): $(OBJECTS)
+ $(CC) -o $@ $(CPPFLAGS) $(CFLAGS) $(LDFLAGS) $^ $(LIBS)
+
+%.o: %.c
+ $(CC) $(CPPFLAGS) $(CFLAGS) -c -o $@ $<
+
+clean:
+ rm -f $(BINARY) $(OBJECTS)
+
+.PHONY: clean $(SOURCES)
diff --git a/src/eetg.c b/src/eetg.c
new file mode 100644
index 0000000..ebfd394
--- /dev/null
+++ b/src/eetg.c
@@ -0,0 +1,719 @@
+/*
+ * Copyright (c) 2024 Richard Braun.
+ *
+ * Permission to use, copy, modify, and/or distribute this software for
+ * any purpose with or without fee is hereby granted.
+ *
+ * THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL
+ * WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE
+ * FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY
+ * DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
+ * AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+ * OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ *
+ *
+ * Engine for embedded text-based games.
+ */
+
+#include <stdio.h>
+
+#include <assert.h>
+#include <limits.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <string.h>
+
+#include "eetg.h"
+#include "macros.h"
+
+#define EETG_RENDERING_DISABLED 0
+
+#define EETG_BG_COLOR EETG_COLOR_BLACK
+#define EETG_FG_COLOR EETG_COLOR_WHITE
+
+#define EETG_CSI "\e["
+
+static unsigned int eetg_rand_next = 1;
+
+static void
+eetg_object_set(struct eetg_object *object, struct eetg_world *world,
+ int x, int y)
+{
+ assert(object);
+ assert(!object->world);
+ assert(world);
+
+ object->world = world;
+ object->x = x;
+ object->y = y;
+}
+
+static void
+eetg_object_unset(struct eetg_object *object)
+{
+ assert(object);
+ assert(object->world);
+
+ object->world = NULL;
+ object->next = NULL;
+}
+
+static char
+eetg_object_get_char(const struct eetg_object *object, int x, int y)
+{
+ int index;
+
+ index = eetg_object_get_cell(object, x, y);
+
+ if (index == -1)
+ {
+ return index;
+ }
+
+ return object->sprite[index];
+}
+
+static void
+eetg_object_check_collision(struct eetg_object *object1,
+ struct eetg_object *object2,
+ eetg_handle_collision_fn handle_collision_fn,
+ void *handle_collision_fn_arg)
+{
+ int o1xbr, o1ybr, o2xbr, o2ybr;
+ int xtl, ytl, xbr, ybr;
+
+ assert(object1);
+ assert(object2);
+ assert(handle_collision_fn);
+
+ o1xbr = object1->x + object1->width - 1;
+ o1ybr = object1->y + object1->height - 1;
+
+ o2xbr = object2->x + object2->width - 1;
+ o2ybr = object2->y + object2->height - 1;
+
+ if ((object2->x > o1xbr) || (object1->x > o2xbr) ||
+ (object2->y > o1ybr) || (object1->y > o2ybr)) {
+ return;
+ }
+
+ xtl = (object1->x > object2->x) ? object1->x : object2->x;
+ ytl = (object1->y > object2->y) ? object1->y : object2->y;
+ xbr = (o1xbr < o2xbr) ? o1xbr : o2xbr;
+ ybr = (o1ybr < o2ybr) ? o1ybr : o2ybr;
+
+ assert(xtl <= xbr);
+ assert(ytl <= ybr);
+
+ for (int i = xtl; i <= xbr; i++) {
+ for (int j = ytl; j <= ybr; j++) {
+ char c1, c2;
+
+ c1 = eetg_object_get_char(object1, i, j);
+ c2 = eetg_object_get_char(object2, i, j);
+
+ if ((c1 <= 0) || (c2 <= 0)) {
+ return;
+ } else if ((c1 == ' ') || (c2 == ' ')) {
+ continue;
+ }
+
+ handle_collision_fn(object1, object2,
+ i, j, handle_collision_fn_arg);
+ return;
+ }
+ }
+}
+
+static void
+eetg_view_cell_set(struct eetg_view_cell *view_cell,
+ char c, int color)
+{
+ assert(view_cell);
+
+ view_cell->c = c;
+ view_cell->color = color;
+}
+
+static char
+eetg_view_cell_get_c(const struct eetg_view_cell *view_cell)
+{
+ assert(view_cell);
+
+ return view_cell->c;
+}
+
+static int
+eetg_view_cell_get_color(const struct eetg_view_cell *view_cell)
+{
+ assert(view_cell);
+
+ return view_cell->color;
+}
+
+static struct eetg_view_cell *
+eetg_view_row_get_cell(struct eetg_view_row *view_row, int index)
+{
+ assert(view_row);
+ assert(index >= 0);
+ assert(index < (int)ARRAY_SIZE(view_row->columns));
+
+ return &view_row->columns[index];
+}
+
+static void
+eetg_view_row_init(struct eetg_view_row *view_row)
+{
+ assert(view_row);
+
+ for (size_t i = 0; i < ARRAY_SIZE(view_row->columns); i++) {
+ struct eetg_view_cell *view_cell;
+
+ view_cell = eetg_view_row_get_cell(view_row, (int)i);
+ eetg_view_cell_set(view_cell, ' ', EETG_BG_COLOR);
+ }
+}
+
+static struct eetg_view_row *
+eetg_view_get_row(struct eetg_view *view, int index)
+{
+ assert(view);
+ assert(index >= 0);
+ assert(index < (int)ARRAY_SIZE(view->rows));
+
+ return &view->rows[index];
+}
+
+static void
+eetg_view_init(struct eetg_view *view)
+{
+ assert(view);
+
+ for (size_t i = 0; i < ARRAY_SIZE(view->rows); i++) {
+ eetg_view_row_init(eetg_view_get_row(view, (int)i));
+ }
+}
+
+void
+eetg_world_init(struct eetg_world *world, eetg_write_fn write_fn, void *arg)
+{
+ assert(world);
+ assert(write_fn);
+
+ world->write_fn = write_fn;
+ world->write_fn_arg = arg;
+
+ world->handle_collision_fn = NULL;
+ world->objects = NULL;
+
+ eetg_view_init(&world->views[0]);
+ eetg_view_init(&world->views[1]);
+
+ world->view = &world->views[0];
+ world->prev_view = &world->views[1];
+
+ world->cursor_row = -1;
+ world->cursor_column = -1;
+ world->current_color = EETG_FG_COLOR;
+}
+
+void
+eetg_world_clear(struct eetg_world *world)
+{
+ struct eetg_object *object;
+
+ assert(world);
+
+ object = world->objects;
+
+ while (object) {
+ struct eetg_object *next = object->next;
+
+ eetg_object_unset(object);
+ object = next;
+ }
+
+ world->objects = NULL;
+}
+
+void
+eetg_world_register_collision_fn(struct eetg_world *world,
+ eetg_handle_collision_fn handle_collision_fn,
+ void *arg)
+{
+ assert(world);
+ assert(handle_collision_fn);
+
+ world->handle_collision_fn = handle_collision_fn;
+ world->handle_collision_fn_arg = arg;
+}
+
+static void
+eetg_world_scan_collisions(struct eetg_world *world, struct eetg_object *object)
+{
+ assert(world);
+
+ if (!world->handle_collision_fn) {
+ return;
+ }
+
+ for (struct eetg_object *tmp = world->objects; tmp; tmp = tmp->next) {
+ if (tmp == object) {
+ continue;
+ }
+
+ eetg_object_check_collision(object, tmp,
+ world->handle_collision_fn,
+ world->handle_collision_fn_arg);
+ }
+}
+
+void
+eetg_world_add(struct eetg_world *world, struct eetg_object *object,
+ int x, int y)
+{
+ assert(world);
+ assert(eetg_object_get_world(object) == NULL);
+
+ object->next = world->objects;
+ world->objects = object;
+
+ eetg_object_set(object, world, x, y);
+ eetg_world_scan_collisions(world, object);
+}
+
+void
+eetg_world_remove(struct eetg_world *world, struct eetg_object *object)
+{
+ assert(world);
+ assert(eetg_object_get_world(object) == world);
+
+ if (world->objects == object) {
+ world->objects = object->next;
+ goto out;
+ }
+
+ for (struct eetg_object *tmp = world->objects; tmp->next; tmp = tmp->next) {
+ if (tmp->next == object) {
+ tmp->next = tmp->next->next;
+ break;
+ }
+ }
+
+out:
+ eetg_object_unset(object);
+}
+
+static void
+eetg_object_render_row(struct eetg_object *object, int row,
+ struct eetg_view_row *view_row)
+{
+ const char *line;
+
+ assert(object);
+ assert(row < object->height);
+
+ line = &object->sprite[(object->width + 1) * row];
+
+ for (int obj_column = 0; obj_column < object->width; obj_column++) {
+ int column = object->x + obj_column;
+ char c;
+
+ if ((column < 0) || (column >= EETG_COLUMNS)) {
+ continue;
+ }
+
+ c = line[obj_column];
+
+ if (c != ' ') {
+ struct eetg_view_cell *view_cell;
+
+ view_cell = eetg_view_row_get_cell(view_row, column);
+ eetg_view_cell_set(view_cell, c, object->color);
+ }
+ }
+}
+
+static void
+eetg_object_render(struct eetg_object *object, struct eetg_view *view)
+{
+ assert(object);
+
+ for (int obj_row = 0; obj_row < object->height; obj_row++) {
+ int row = object->y + obj_row;
+
+ if ((row < 0) || (row >= EETG_ROWS)) {
+ continue;
+ }
+
+ eetg_object_render_row(object, obj_row, eetg_view_get_row(view, row));
+ }
+}
+
+static void
+eetg_world_write(struct eetg_world *world, const void *buffer, size_t size)
+{
+ assert(world);
+ assert(world->write_fn);
+ assert(buffer);
+
+#if EETG_RENDERING_DISABLED
+ (void)buffer;
+ (void)size;
+#else
+ world->write_fn(buffer, size, world->write_fn_arg);
+#endif
+}
+
+static void
+eetg_world_write_str(struct eetg_world *world, const char *str)
+{
+ eetg_world_write(world, str, strlen(str));
+}
+
+static void
+eetg_world_set_cursor(struct eetg_world *world, int row, int column)
+{
+ char str[32];
+
+ assert(world);
+ assert(row >= 0);
+ assert(row < EETG_ROWS);
+ assert(column >= 0);
+ assert(column < EETG_COLUMNS);
+
+ if ((world->cursor_row == row) && (world->cursor_column == column)) {
+ return;
+ }
+
+ snprintf(str, sizeof(str), EETG_CSI "%d;%dH", row + 1, column + 1);
+ eetg_world_write_str(world, str);
+
+ world->cursor_row = row;
+ world->cursor_column = column;
+}
+
+static int
+eetg_convert_fg_color(int color)
+{
+ return color + 30;
+}
+
+static int
+eetg_convert_bg_color(int color)
+{
+ return eetg_convert_fg_color(color) + 10;
+}
+
+static void
+eetg_world_set_color(struct eetg_world *world, int color, bool force)
+{
+ char str[16];
+
+ if ((color == world->current_color) && !force) {
+ return;
+ }
+
+ snprintf(str, sizeof(str), EETG_CSI "%d;%dm",
+ eetg_convert_fg_color(color),
+ eetg_convert_bg_color(EETG_BG_COLOR));
+
+ eetg_world_write_str(world, str);
+
+ world->current_color = color;
+}
+
+static void
+eetg_world_write_char(struct eetg_world *world, char c)
+{
+ eetg_world_write(world, &c, sizeof(c));
+
+ world->cursor_column++;
+
+ if (world->cursor_column == EETG_COLUMNS) {
+ if (world->cursor_row == EETG_ROWS) {
+ world->cursor_column = EETG_COLUMNS - 1;
+ } else {
+ world->cursor_column = 0;
+ world->cursor_row++;
+ }
+ }
+}
+
+static void
+eetg_world_swap_views(struct eetg_world *world)
+{
+ struct eetg_view *tmp;
+
+ assert(world);
+
+ tmp = world->view;
+ world->view = world->prev_view;
+ world->prev_view = tmp;
+}
+
+static void
+eetg_world_render_sync(struct eetg_world *world)
+{
+ assert(world);
+
+ eetg_world_write_str(world, EETG_CSI "?25l"); /* cursor invisible */
+ eetg_world_set_color(world, EETG_FG_COLOR, true);
+ eetg_world_write_str(world, EETG_CSI "2J"); /* clear screen */
+ eetg_world_set_cursor(world, 0, 0);
+
+ for (int row = 0; row < EETG_ROWS; row++) {
+ struct eetg_view_row *view_row;
+
+ view_row = eetg_view_get_row(world->view, row);
+
+ for (int column = 0; column < EETG_COLUMNS; column++) {
+ struct eetg_view_cell *view_cell;
+ int color;
+ char c;
+
+ view_cell = eetg_view_row_get_cell(view_row, column);
+ color = eetg_view_cell_get_color(view_cell);
+ c = eetg_view_cell_get_c(view_cell);
+
+ if (c == ' ') {
+ continue;
+ }
+
+ eetg_world_set_cursor(world, row, column);
+ eetg_world_set_color(world, color, false);
+ eetg_world_write_char(world, c);
+ }
+ }
+}
+
+static void
+eetg_world_render_delta(struct eetg_world *world)
+{
+ assert(world);
+
+ for (int row = 0; row < EETG_ROWS; row++) {
+ struct eetg_view_row *view_row, *prev_view_row;
+
+ view_row = eetg_view_get_row(world->view, row);
+ prev_view_row = eetg_view_get_row(world->prev_view, row);
+
+ for (int column = 0; column < EETG_COLUMNS; column++) {
+ struct eetg_view_cell *view_cell, *prev_view_cell;
+ int color, prev_color;
+ char c, prev_c;
+
+ view_cell = eetg_view_row_get_cell(view_row, column);
+ color = eetg_view_cell_get_color(view_cell);
+ c = eetg_view_cell_get_c(view_cell);
+
+ prev_view_cell = eetg_view_row_get_cell(prev_view_row, column);
+ prev_color = eetg_view_cell_get_color(prev_view_cell);
+ prev_c = eetg_view_cell_get_c(prev_view_cell);
+
+ if ((color != prev_color) || (c != prev_c)) {
+ eetg_world_set_cursor(world, row, column);
+ eetg_world_set_color(world, color, false);
+ eetg_world_write_char(world, c);
+ }
+ }
+ }
+}
+
+void
+eetg_world_render(struct eetg_world *world, bool sync)
+{
+ for (int row = 0; row < EETG_ROWS; row++) {
+ struct eetg_view_row *view_row;
+
+ view_row = eetg_view_get_row(world->view, row);
+
+ for (int column = 0; column < EETG_COLUMNS; column++) {
+ struct eetg_view_cell *view_cell;
+
+ view_cell = eetg_view_row_get_cell(view_row, column);
+ eetg_view_cell_set(view_cell, ' ', EETG_FG_COLOR);
+ }
+ }
+
+ for (struct eetg_object *obj = world->objects; obj; obj = obj->next) {
+ eetg_object_render(obj, world->view);
+ }
+
+ if (sync) {
+ eetg_world_render_sync(world);
+ } else {
+ eetg_world_render_delta(world);
+ }
+
+ eetg_world_set_cursor(world, 0, 0);
+ eetg_world_swap_views(world);
+}
+
+void
+eetg_object_init(struct eetg_object *object, int type, const char *sprite)
+{
+ size_t width;
+ char *ptr;
+
+ assert(object);
+
+ object->world = NULL;
+ object->next = NULL;
+ object->sprite = sprite;
+ object->type = type;
+ object->x = 0;
+ object->y = 0;
+ object->color = EETG_FG_COLOR;
+
+ ptr = strchr(sprite, '\n');
+ assert(ptr);
+
+ width = ptr - sprite;
+ assert(width <= INT_MAX);
+
+ object->width = (int)width;
+ object->height = 1;
+
+ for (;;) {
+ char *next;
+
+ ptr = &ptr[1];
+ next = strchr(ptr, '\n');
+
+ if (!next) {
+ break;
+ }
+
+ width = next - ptr;
+ assert(width <= INT_MAX);
+ assert((int)width == object->width);
+
+ assert(object->height < INT_MAX);
+ object->height++;
+
+ ptr = next;
+ }
+}
+
+void
+eetg_object_set_color(struct eetg_object *object, int color)
+{
+ assert(object);
+
+ object->color = color;
+}
+
+int
+eetg_object_get_type(const struct eetg_object *object)
+{
+ assert(object);
+
+ return object->type;
+}
+
+const char *
+eetg_object_get_sprite(const struct eetg_object *object)
+{
+ assert(object);
+
+ return object->sprite;
+}
+
+int
+eetg_object_get_x(const struct eetg_object *object)
+{
+ assert(object);
+
+ return object->x;
+}
+
+int
+eetg_object_get_y(const struct eetg_object *object)
+{
+ assert(object);
+
+ return object->y;
+}
+
+int
+eetg_object_get_width(const struct eetg_object *object)
+{
+ assert(object);
+
+ return object->width;
+}
+
+int
+eetg_object_get_height(const struct eetg_object *object)
+{
+ assert(object);
+
+ return object->height;
+}
+
+bool
+eetg_object_is_empty(const struct eetg_object *object)
+{
+ assert(object);
+
+ for (const char *ptr = object->sprite; ptr; ptr++) {
+ char c = *ptr;
+
+ if (c == '\0') {
+ break;
+ } else if ((c != ' ') && (c != '\n')) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+void
+eetg_object_move(struct eetg_object *object, int x, int y)
+{
+ assert(object);
+
+ object->x = x;
+ object->y = y;
+
+ if (object->world) {
+ eetg_world_scan_collisions(object->world, object);
+ }
+}
+
+int
+eetg_object_get_cell(const struct eetg_object *object, int x, int y)
+{
+ assert(object);
+
+ x -= object->x;
+ y -= object->y;
+
+ if ((x < 0) || (x >= object->width) || (y < 0) || (y >= object->height)) {
+ return -1;
+ }
+
+ return ((object->width + 1) * y) + (x % object->width);
+}
+
+struct eetg_world *
+eetg_object_get_world(const struct eetg_object *object)
+{
+ assert(object);
+
+ return object->world;
+}
+
+void eetg_init_rand(unsigned int seed)
+{
+ eetg_rand_next = seed;
+}
+
+int eetg_rand(void)
+{
+ eetg_rand_next = (eetg_rand_next * 1103515245) + 12345;
+ return (eetg_rand_next / 65536) % (EETG_RAND_MAX + 1);
+}
diff --git a/src/eetg.h b/src/eetg.h
new file mode 100644
index 0000000..411eb83
--- /dev/null
+++ b/src/eetg.h
@@ -0,0 +1,114 @@
+/*
+ * Copyright (c) 2024 Richard Braun.
+ *
+ * Permission to use, copy, modify, and/or distribute this software for
+ * any purpose with or without fee is hereby granted.
+ *
+ * THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL
+ * WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE
+ * FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY
+ * DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
+ * AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+ * OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ *
+ *
+ * Engine for embedded text-based games.
+ */
+
+#ifndef EETG_H
+#define EETG_H
+
+#include <stdbool.h>
+#include <stdint.h>
+
+#define EETG_COLUMNS 80
+#define EETG_ROWS 24
+
+#define EETG_RAND_MAX 32767
+
+#define EETG_COLOR_BLACK 0
+#define EETG_COLOR_RED 1
+#define EETG_COLOR_GREEN 2
+#define EETG_COLOR_YELLOW 3
+#define EETG_COLOR_BLUE 4
+#define EETG_COLOR_MAGENTA 5
+#define EETG_COLOR_CYAN 6
+#define EETG_COLOR_WHITE 7
+
+struct eetg_world;
+
+struct eetg_object;
+
+typedef void (*eetg_write_fn)(const void *buffer, size_t size, void *arg);
+
+typedef void (*eetg_handle_collision_fn)(struct eetg_object *object1,
+ struct eetg_object *object2,
+ int x, int y, void *arg);
+
+struct eetg_object {
+ struct eetg_world *world;
+ struct eetg_object *next;
+ const char *sprite;
+ int8_t color;
+ int8_t type;
+ int8_t x;
+ int8_t y;
+ int8_t width;
+ int8_t height;
+};
+
+struct eetg_view_cell {
+ char c;
+ int8_t color;
+};
+
+struct eetg_view_row {
+ struct eetg_view_cell columns[EETG_COLUMNS];
+};
+
+struct eetg_view {
+ struct eetg_view_row rows[EETG_ROWS];
+};
+
+struct eetg_world {
+ eetg_write_fn write_fn;
+ void *write_fn_arg;
+ eetg_handle_collision_fn handle_collision_fn;
+ void *handle_collision_fn_arg;
+ struct eetg_object *objects;
+ struct eetg_view views[2];
+ struct eetg_view *view;
+ struct eetg_view *prev_view;
+ int8_t cursor_row;
+ int8_t cursor_column;
+ int8_t current_color;
+};
+
+void eetg_world_init(struct eetg_world *world,
+ eetg_write_fn write_fn, void *arg);
+void eetg_world_clear(struct eetg_world *world);
+void eetg_world_register_collision_fn(struct eetg_world *world,
+ eetg_handle_collision_fn handle_collision_fn, void *arg);
+void eetg_world_add(struct eetg_world *world, struct eetg_object *object,
+ int x, int y);
+void eetg_world_remove(struct eetg_world *world, struct eetg_object *object);
+void eetg_world_render(struct eetg_world *world, bool sync);
+
+void eetg_object_init(struct eetg_object *object, int type, const char *sprite);
+void eetg_object_set_color(struct eetg_object *object, int color);
+int eetg_object_get_type(const struct eetg_object *object);
+const char *eetg_object_get_sprite(const struct eetg_object *object);
+int eetg_object_get_x(const struct eetg_object *object);
+int eetg_object_get_y(const struct eetg_object *object);
+int eetg_object_get_width(const struct eetg_object *object);
+int eetg_object_get_height(const struct eetg_object *object);
+bool eetg_object_is_empty(const struct eetg_object *object);
+void eetg_object_move(struct eetg_object *object, int x, int y);
+int eetg_object_get_cell(const struct eetg_object *object, int x, int y);
+struct eetg_world *eetg_object_get_world(const struct eetg_object *object);
+
+void eetg_init_rand(unsigned int seed);
+int eetg_rand(void);
+
+#endif /* EETG_H */
diff --git a/src/ei.c b/src/ei.c
new file mode 100644
index 0000000..da2331e
--- /dev/null
+++ b/src/ei.c
@@ -0,0 +1,1184 @@
+/*
+ * Copyright (c) 2024 Richard Braun.
+ *
+ * Permission to use, copy, modify, and/or distribute this software for
+ * any purpose with or without fee is hereby granted.
+ *
+ * THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL
+ * WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE
+ * FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY
+ * DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
+ * AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+ * OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ *
+ *
+ * Embedded invaders.
+ */
+
+#include <assert.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "eetg.h"
+#include "ei.h"
+#include "macros.h"
+
+#define EI_NR_LIVES 3
+
+#define EI_ALIEN_STARTING_ROW 3
+
+#define EI_PLAYER_MISSILE_SPEED 25
+#define EI_ALIENS_SPEED 10
+#define EI_ALIEN_MISSILE_SPEED 10
+#define EI_FIRST_ALIEN_MISSILE_DELAY 2
+#define EI_UFO_SPEED 10
+
+#define EI_SCORE_MISSILE 40
+#define EI_SCORE_ALIENS0 30
+#define EI_SCORE_ALIENS12 20
+#define EI_SCORE_ALIENS34 10
+#define EI_SCORE_UFO_BASE 100
+
+#define EI_TITLE_SPRITE \
+" _____ _____ \n" \
+"( ___ )-------------------------------------------------( ___ )\n" \
+" | | | | \n" \
+" | | ____ __ __ __ __ | | \n" \
+" | | / __/_ _ / / ___ ___/ /__/ /__ ___/ / | | \n" \
+" | | / _// ' \\/ _ \\/ -_) _ / _ / -_) _ / | | \n" \
+" | | /___/_/_/_/_.__/\\__/\\_,_/\\_,_/\\__/\\_,_/ | | \n" \
+" | | _ __ | | \n" \
+" | | (_)__ _ _____ ____/ /__ _______ | | \n" \
+" | | / / _ \\ |/ / _ `/ _ / -_) __(_-< | | \n" \
+" | | /_/_//_/___/\\_,_/\\_,_/\\__/_/ /___/ | | \n" \
+" | | | | \n" \
+" |___| |___| \n" \
+"(_____)-------------------------------------------------(_____)\n"
+
+#define EI_HELP_SPRITE \
+" s = left \n" \
+" f = right \n" \
+" space = shoot \n"
+
+#define EI_START_SPRITE \
+"Press SPACE to start\n" \
+"Press X to leave \n"
+
+#define EI_PLAYER_SPRITE "/-^-\\\n"
+
+#define EI_BUNKER_SPRITE \
+" ### \n" \
+" ##### \n" \
+"#######\n" \
+"## ##\n"
+
+#define EI_ALIENS0_SPRITE_1 ",^,\n"
+#define EI_ALIENS0_SPRITE_2 ".-.\n"
+#define EI_ALIENS12_SPRITE_1 "-O_\n"
+#define EI_ALIENS12_SPRITE_2 "_O-\n"
+#define EI_ALIENS34_SPRITE_1 "/^\\\n"
+#define EI_ALIENS34_SPRITE_2 "-^-\n"
+
+#define EI_STATUS_SPRITE_FORMAT "SCORE: %08d Lives: %d\n"
+
+#define EI_END_TITLE_SPRITE \
+" ________ __ _______ ____ _ _________ \n" \
+" / ___/ _ | / |/ / __/ / __ \\ | / / __/ _ \\\n" \
+"/ (_ / __ |/ /|_/ / _/ / /_/ / |/ / _// , _/\n" \
+"\\___/_/ |_/_/ /_/___/ \\____/|___/___/_/|_| \n"
+
+#define EI_TYPE_TITLE 0
+#define EI_TYPE_HELP 1
+#define EI_TYPE_START 2
+#define EI_TYPE_PLAYER 3
+#define EI_TYPE_PLAYER_MISSILE 4
+#define EI_TYPE_BUNKER 5
+#define EI_TYPE_ALIEN 6
+#define EI_TYPE_ALIEN_MISSILE 7
+#define EI_TYPE_UFO 8
+#define EI_TYPE_STATUS 9
+#define EI_TYPE_END_TITLE 10
+
+static const char *
+ei_get_group_sprite1(size_t group)
+{
+ const char *sprite;
+
+ if (group == 0) {
+ sprite = EI_ALIENS0_SPRITE_1;
+ } else if ((group == 1) || (group == 2)) {
+ sprite = EI_ALIENS12_SPRITE_1;
+ } else {
+ sprite = EI_ALIENS34_SPRITE_1;
+ }
+
+ return sprite;
+}
+
+static const char *
+ei_get_group_sprite2(size_t group)
+{
+ const char *sprite;
+
+ if (group == 0) {
+ sprite = EI_ALIENS0_SPRITE_2;
+ } else if ((group == 1) || (group == 2)) {
+ sprite = EI_ALIENS12_SPRITE_2;
+ } else {
+ sprite = EI_ALIENS34_SPRITE_2;
+ }
+
+ return sprite;
+}
+
+static int
+ei_get_group_color(size_t group)
+{
+ int color;
+
+ if (group == 0) {
+ color = EETG_COLOR_RED;
+ } else if ((group == 1) || (group == 2)) {
+ color = EETG_COLOR_GREEN;
+ } else {
+ color = EETG_COLOR_BLUE;
+ }
+
+ return color;
+}
+
+static struct eetg_object *
+ei_get_object(struct eetg_object *object1,
+ struct eetg_object *object2,
+ int type)
+{
+ struct eetg_object *object = NULL;
+
+ assert(object1);
+ assert(object2);
+
+ if (eetg_object_get_type(object1) == type) {
+ object = object1;
+ } else if (eetg_object_get_type(object2) == type) {
+ object = object2;
+ }
+
+ return object;
+}
+
+static bool
+ei_has_type(const struct eetg_object *object1,
+ const struct eetg_object *object2,
+ int type)
+{
+ return (eetg_object_get_type(object1) == type)
+ || (eetg_object_get_type(object2) == type);
+}
+
+static void
+ei_bunker_reset_sprite(struct ei_bunker *bunker)
+{
+ assert(bunker);
+
+ snprintf(bunker->sprite, sizeof(bunker->sprite), "%s", EI_BUNKER_SPRITE);
+}
+
+static void
+ei_bunker_init(struct ei_bunker *bunker)
+{
+ assert(bunker);
+
+ ei_bunker_reset_sprite(bunker);
+
+ eetg_object_init(&bunker->object, EI_TYPE_BUNKER, bunker->sprite);
+ eetg_object_set_color(&bunker->object, EETG_COLOR_CYAN);
+}
+
+static struct ei_bunker *
+ei_bunker_get(struct eetg_object *object)
+{
+ assert(eetg_object_get_type(object) == EI_TYPE_BUNKER);
+
+ return structof(object, struct ei_bunker, object);
+}
+
+static struct eetg_object *
+ei_bunker_get_object(struct ei_bunker *bunker)
+{
+ assert(bunker);
+
+ return &bunker->object;
+}
+
+static bool
+ei_bunker_damage(struct ei_bunker *bunker, int x, int y)
+{
+ char *sprite;
+ int index;
+
+ assert(bunker);
+
+ index = eetg_object_get_cell(&bunker->object, x, y);
+
+ sprite = (char *)eetg_object_get_sprite(&bunker->object);
+
+ sprite[index] = ' ';
+
+ return eetg_object_is_empty(&bunker->object);
+}
+
+static void
+ei_alien_init(struct ei_alien *alien, const char *sprite, int color)
+{
+ assert(alien);
+
+ eetg_object_init(&alien->object, EI_TYPE_ALIEN, sprite);
+ eetg_object_set_color(&alien->object, color);
+}
+
+static struct ei_alien *
+ei_alien_get(struct eetg_object *object)
+{
+ assert(eetg_object_get_type(object) == EI_TYPE_ALIEN);
+
+ return structof(object, struct ei_alien, object);
+}
+
+static struct eetg_object *
+ei_alien_get_object(struct ei_alien *alien)
+{
+ assert(alien);
+
+ return &alien->object;
+}
+
+static bool
+ei_alien_is_dead(const struct ei_alien *alien)
+{
+ assert(alien);
+
+ return (eetg_object_get_world(&alien->object) == NULL);
+}
+
+static int
+ei_alien_get_x(const struct ei_alien *alien)
+{
+ assert(alien);
+
+ return eetg_object_get_x(&alien->object);
+}
+
+static int
+ei_alien_get_width(const struct ei_alien *alien)
+{
+ assert(alien);
+
+ return eetg_object_get_width(&alien->object);
+}
+
+static bool
+ei_alien_move_down(struct ei_alien *alien)
+{
+ struct eetg_object *object;
+ bool game_over = false;
+ int y;
+
+ assert(alien);
+
+ object = &alien->object;
+
+ y = eetg_object_get_y(object) + 1;
+
+ eetg_object_move(object, eetg_object_get_x(object), y);
+
+ if (y >= (EETG_ROWS - 1)) {
+ game_over = true;
+ }
+
+ return game_over;
+}
+
+static void
+ei_alien_move_left(struct ei_alien *alien)
+{
+ struct eetg_object *object;
+
+ assert(alien);
+
+ object = &alien->object;
+
+ eetg_object_move(object, eetg_object_get_x(object) - 1,
+ eetg_object_get_y(object));
+}
+
+static void
+ei_alien_move_right(struct ei_alien *alien)
+{
+ struct eetg_object *object;
+
+ assert(alien);
+
+ object = &alien->object;
+
+ eetg_object_move(object, eetg_object_get_x(object) + 1,
+ eetg_object_get_y(object));
+}
+
+static void
+ei_alien_group_init(struct ei_alien_group *group, const char *sprite1,
+ const char *sprite2, int color)
+{
+ assert(group);
+ assert(sprite1);
+ assert(strlen(sprite1) == (EI_ALIEN_WIDTH + 1));
+ assert(sprite2);
+ assert(strlen(sprite2) == (EI_ALIEN_WIDTH + 1));
+
+ group->sprites[0] = sprite1;
+ group->sprites[1] = sprite2;
+ group->sprite_index = 0;
+
+ memcpy(group->sprite, group->sprites[group->sprite_index],
+ sizeof(group->sprite));
+
+ for (size_t i = 0; i < ARRAY_SIZE(group->aliens); i++) {
+ ei_alien_init(&group->aliens[i], group->sprite, color);
+ }
+}
+
+static void
+ei_alien_group_attach(struct ei_alien_group *group,
+ struct eetg_world *world, int y)
+{
+ int x = 0;
+
+ assert(group);
+
+ for (size_t i = 0; i < ARRAY_SIZE(group->aliens); i++) {
+ struct ei_alien *alien = &group->aliens[i];
+
+ eetg_world_add(world, ei_alien_get_object(alien), x, y);
+
+ x += EI_ALIEN_WIDTH;
+ }
+}
+
+static bool
+ei_alien_group_has_alien(const struct ei_alien_group *group,
+ const struct ei_alien *alien)
+{
+ bool alien_present = false;
+ const struct ei_alien *end;
+
+ assert(group);
+
+ end = &group->aliens[ARRAY_SIZE(group->aliens)];
+
+ if ((alien >= group->aliens) && (alien < end)) {
+ alien_present = true;
+ }
+
+ return alien_present;
+}
+
+static struct ei_alien *
+ei_alien_group_get(struct ei_alien_group *group, size_t index)
+{
+ assert(group);
+ assert(index < ARRAY_SIZE(group->aliens));
+
+ return &group->aliens[index];
+}
+
+static void
+ei_alien_group_twerk(struct ei_alien_group *group)
+{
+ assert(group);
+
+ group->sprite_index = (group->sprite_index + 1) & 1;
+ memcpy(group->sprite, group->sprites[group->sprite_index],
+ sizeof(group->sprite));
+}
+
+static bool
+ei_alien_group_move_down(struct ei_alien_group *group)
+{
+ bool game_over = false;
+
+ ei_alien_group_twerk(group);
+
+ for (size_t i = 0; i < ARRAY_SIZE(group->aliens); i++) {
+ struct ei_alien *alien = &group->aliens[i];
+
+ if (!ei_alien_is_dead(alien)) {
+ bool tmp;
+
+ tmp = ei_alien_move_down(alien);
+
+ if (tmp) {
+ game_over = true;
+ }
+ }
+ }
+
+ return game_over;
+}
+
+static bool
+ei_alien_group_move_left(struct ei_alien_group *group)
+{
+ bool border_reached = false;
+
+ ei_alien_group_twerk(group);
+
+ for (size_t i = 0; i < ARRAY_SIZE(group->aliens); i++) {
+ struct ei_alien *alien = &group->aliens[i];
+
+ if (!ei_alien_is_dead(alien)) {
+ ei_alien_move_left(alien);
+
+ if (!border_reached && (ei_alien_get_x(alien) == 0)) {
+ border_reached = true;
+ }
+ }
+ }
+
+ return border_reached;
+}
+
+static bool
+ei_alien_group_move_right(struct ei_alien_group *group)
+{
+ bool border_reached = false;
+
+ ei_alien_group_twerk(group);
+
+ for (size_t i = ARRAY_SIZE(group->aliens) - 1;
+ i < ARRAY_SIZE(group->aliens); i--) {
+ struct ei_alien *alien = &group->aliens[i];
+
+ if (!ei_alien_is_dead(alien)) {
+ ei_alien_move_right(alien);
+
+ if (!border_reached) {
+ int x;
+
+ x = ei_alien_get_x(alien) + ei_alien_get_width(alien) - 1;
+
+ if (x == (EETG_COLUMNS - 1)) {
+ border_reached = true;
+ }
+ }
+ }
+ }
+
+ return border_reached;
+}
+
+static struct ei_alien_group *
+ei_game_find_alien_group(struct ei_game *game, struct ei_alien *alien)
+{
+ struct ei_alien_group *group = NULL;
+
+ assert(alien);
+
+ for (size_t i = 0; i < ARRAY_SIZE(game->aliens); i++) {
+ struct ei_alien_group *tmp = &game->aliens[i];
+
+ if (ei_alien_group_has_alien(tmp, alien)) {
+ group = tmp;
+ break;
+ }
+ }
+
+ return group;
+}
+
+static void
+ei_game_add_bunkers(struct ei_game *game)
+{
+ int x = 6;
+
+ assert(game);
+
+ for (size_t i = 0; i < ARRAY_SIZE(game->bunkers); i++) {
+ struct ei_bunker *bunker = &game->bunkers[i];
+
+ ei_bunker_reset_sprite(bunker);
+ eetg_world_add(&game->world, ei_bunker_get_object(bunker), x, 17);
+
+ x += 20;
+ }
+}
+
+static void
+ei_game_add_aliens(struct ei_game *game)
+{
+ assert(game);
+
+ for (size_t i = 0; i < ARRAY_SIZE(game->aliens); i++) {
+ struct ei_alien_group *group = &game->aliens[i];
+
+ ei_alien_group_attach(group, &game->world,
+ EI_ALIEN_STARTING_ROW + (i * 2));
+ }
+}
+
+static void
+ei_game_update_status(struct ei_game *game)
+{
+ assert(game);
+
+ snprintf(game->status_sprite, sizeof(game->status_sprite),
+ EI_STATUS_SPRITE_FORMAT, game->score, game->nr_lives);
+}
+
+static void
+ei_game_prepare(struct ei_game *game)
+{
+ assert(game);
+
+ eetg_world_clear(&game->world);
+
+ game->state = EI_STATE_PREPARED;
+}
+
+static void
+ei_game_start(struct ei_game *game)
+{
+ assert(game);
+
+ eetg_world_clear(&game->world);
+
+ eetg_world_add(&game->world, &game->player, 37, 23);
+
+ ei_game_add_bunkers(game);
+ ei_game_add_aliens(game);
+
+ eetg_world_add(&game->world, &game->status, 26, 0);
+
+ game->player_missile_counter_reload = EI_FPS / EI_PLAYER_MISSILE_SPEED;
+ game->player_missile_counter = game->player_missile_counter_reload;
+ game->aliens_speed_counter_reload = EI_FPS / EI_ALIENS_SPEED;
+ game->aliens_speed_counter = game->aliens_speed_counter_reload;
+ game->first_alien_missile_counter = EI_FPS * EI_FIRST_ALIEN_MISSILE_DELAY;
+ game->alien_missile_counter_reload = EI_FPS / EI_ALIEN_MISSILE_SPEED;
+ game->ufo_counter_reload = EI_FPS / EI_UFO_SPEED;
+ game->nr_dead_aliens = 0;
+
+ game->aliens_move_left = false;
+ game->aliens_move_down = false;
+
+ game->state = EI_STATE_PLAYING;
+}
+
+static void
+ei_game_kill_alien(struct ei_game *game, struct ei_alien *alien)
+{
+ int group, score;
+
+ assert(alien);
+
+ group = ei_game_find_alien_group(game, alien) - game->aliens;
+
+ if (group == 0) {
+ score = EI_SCORE_ALIENS0;
+ } else if ((group == 1) || (group == 2)) {
+ score = EI_SCORE_ALIENS12;
+ } else {
+ score = EI_SCORE_ALIENS34;
+ }
+
+ game->score += score;
+
+ ei_game_update_status(game);
+
+ eetg_world_remove(&game->world, ei_alien_get_object(alien));
+
+ game->nr_dead_aliens++;
+
+ if (game->nr_dead_aliens == (EI_NR_ALIEN_GROUPS * EI_ALIEN_GROUP_SIZE)) {
+ ei_game_prepare(game);
+ } else {
+ int aliens_speed;
+
+ aliens_speed = game->nr_dead_aliens / 2;
+
+ if (aliens_speed < (EI_FPS / 5)) {
+ aliens_speed = EI_FPS / 5;
+ } else if (aliens_speed > ((EI_FPS * 4) / 5)) {
+ aliens_speed = ((EI_FPS * 4) / 5);
+ }
+
+ game->aliens_speed_counter_reload = EI_FPS / aliens_speed;
+ }
+}
+
+static void
+ei_game_damage_bunker(struct ei_game *game, struct ei_bunker *bunker,
+ int x, int y)
+{
+ bool destroyed;
+
+ destroyed = ei_bunker_damage(bunker, x, y);
+
+ if (destroyed) {
+ eetg_world_remove(&game->world, ei_bunker_get_object(bunker));
+ }
+}
+
+static void
+ei_game_terminate(struct ei_game *game)
+{
+ assert(game);
+
+ eetg_world_clear(&game->world);
+
+ eetg_world_add(&game->world, &game->end_title, 12, 10);
+ eetg_world_add(&game->world, &game->status, 26, 6);
+ eetg_world_add(&game->world, &game->start, 30, 20);
+
+ game->state = EI_STATE_GAME_OVER;
+}
+
+static void
+ei_game_kill_player(struct ei_game *game, bool game_over)
+{
+ assert(game);
+ assert(game->nr_lives > 0);
+
+ game->nr_lives--;
+
+ ei_game_update_status(game);
+
+ if (game_over || (game->nr_lives == 0))
+ {
+ ei_game_terminate(game);
+ }
+}
+
+static void
+ei_game_handle_player_missile_collision(struct ei_game *game,
+ struct eetg_object *missile,
+ struct eetg_object *object,
+ int x, int y)
+{
+ assert(game);
+
+ eetg_world_remove(&game->world, missile);
+
+ if (eetg_object_get_type(object) == EI_TYPE_BUNKER) {
+ ei_game_damage_bunker(game, ei_bunker_get(object), x, y);
+ } else if (eetg_object_get_type(object) == EI_TYPE_ALIEN_MISSILE) {
+ game->score += EI_SCORE_MISSILE;
+ eetg_world_remove(&game->world, object);
+ } else if (eetg_object_get_type(object) == EI_TYPE_ALIEN) {
+ ei_game_kill_alien(game, ei_alien_get(object));
+ } else if (eetg_object_get_type(object) == EI_TYPE_UFO) {
+ game->score += EI_SCORE_UFO_BASE * ((eetg_rand() % 5) + 1);
+ eetg_world_remove(&game->world, object);
+ }
+}
+
+static void
+ei_game_handle_alien_collision(struct ei_game *game,
+ struct eetg_object *object,
+ int x, int y)
+{
+ if (eetg_object_get_type(object) == EI_TYPE_BUNKER) {
+ ei_game_damage_bunker(game, ei_bunker_get(object), x, y);
+ } else if (eetg_object_get_type(object) == EI_TYPE_PLAYER) {
+ ei_game_kill_player(game, true);
+ }
+}
+
+static void
+ei_game_handle_alien_missile_collision(struct ei_game *game,
+ struct eetg_object *missile,
+ struct eetg_object *object,
+ int x, int y)
+{
+ assert(game);
+
+ if (eetg_object_get_type(object) == EI_TYPE_ALIEN) {
+ return;
+ }
+
+ eetg_world_remove(&game->world, missile);
+
+ if (eetg_object_get_type(object) == EI_TYPE_BUNKER) {
+ ei_game_damage_bunker(game, ei_bunker_get(object), x, y);
+ } else if (eetg_object_get_type(object) == EI_TYPE_PLAYER) {
+ ei_game_kill_player(game, false);
+ }
+}
+
+static void
+ei_game_handle_collision(struct eetg_object *object1,
+ struct eetg_object *object2,
+ int x, int y, void *arg)
+{
+ if (ei_has_type(object1, object2, EI_TYPE_PLAYER_MISSILE)) {
+ struct eetg_object *missile, *other;
+
+ missile = ei_get_object(object1, object2, EI_TYPE_PLAYER_MISSILE);
+ other = (object1 == missile) ? object2 : object1;
+
+ ei_game_handle_player_missile_collision(arg, missile, other, x, y);
+ } else if (ei_has_type(object1, object2, EI_TYPE_ALIEN)) {
+ struct eetg_object *alien, *other;
+
+ alien = ei_get_object(object1, object2, EI_TYPE_ALIEN);
+ other = (object1 == alien) ? object2 : object1;
+
+ ei_game_handle_alien_collision(arg, other, x, y);
+ } else if (ei_has_type(object1, object2, EI_TYPE_ALIEN_MISSILE)) {
+ struct eetg_object *missile, *other;
+
+ missile = ei_get_object(object1, object2, EI_TYPE_ALIEN_MISSILE);
+ other = (object1 == missile) ? object2 : object1;
+
+ ei_game_handle_alien_missile_collision(arg, missile, other, x, y);
+ }
+}
+
+static void
+ei_game_reset_history(struct ei_game *game)
+{
+ assert(game);
+
+ game->score = 0;
+ game->nr_lives = EI_NR_LIVES;
+
+ ei_game_update_status(game);
+}
+
+static bool
+ei_game_process_intro_input(struct ei_game *game, char c)
+{
+ assert(game);
+
+ if (c == 'x') {
+ return true;
+ }
+
+ if (c != ' ') {
+ return false;
+ }
+
+ ei_game_reset_history(game);
+ ei_game_prepare(game);
+
+ return false;
+}
+
+static bool
+ei_game_process_game_input(struct ei_game *game, char c)
+{
+ assert(game);
+
+ if (c == 'x') {
+ return true;
+ }
+
+ if (c == 's') {
+ struct eetg_object *player_object = &game->player;
+ int x;
+
+ x = eetg_object_get_x(player_object);
+
+ if (x > 0) {
+ eetg_object_move(player_object, x - 1,
+ eetg_object_get_y(player_object));
+ }
+ } else if (c == 'f') {
+ struct eetg_object *player_object = &game->player;
+ int x, width;
+
+ x = eetg_object_get_x(player_object);
+ width = eetg_object_get_width(player_object);
+
+ if ((x + width) < EETG_COLUMNS) {
+ eetg_object_move(player_object, x + 1,
+ eetg_object_get_y(player_object));
+ }
+ } else if (c == ' ') {
+ struct eetg_object *player_missile = &game->player_missile;
+
+ if (eetg_object_get_world(player_missile) == NULL) {
+ struct eetg_object *player_object = &game->player;
+ int x, y;
+
+ x = eetg_object_get_x(player_object);
+ y = eetg_object_get_y(player_object);
+
+ eetg_world_add(&game->world, player_missile, x + 2, y - 1);
+
+ game->player_missile_counter = game->player_missile_counter_reload;
+ }
+ }
+
+ return false;
+}
+
+static void
+ei_game_process_player_missile(struct ei_game *game)
+{
+ int x, y;
+
+ assert(game);
+
+ if (eetg_object_get_world(&game->player_missile) == NULL) {
+ return;
+ }
+
+ assert(game->player_missile_counter > 0);
+ game->player_missile_counter--;
+
+ if (game->player_missile_counter != 0) {
+ return;
+ }
+
+ game->player_missile_counter = game->player_missile_counter_reload;
+
+ x = eetg_object_get_x(&game->player_missile);
+ y = eetg_object_get_y(&game->player_missile) - 1;
+
+ if (y == 0) {
+ eetg_world_remove(&game->world, &game->player_missile);
+ } else {
+ eetg_object_move(&game->player_missile, x, y);
+ }
+}
+
+static struct ei_alien *
+ei_game_select_firing_alien(struct ei_game *game)
+{
+ struct ei_alien *alien = NULL;
+ int nr_firing_aliens = 0;
+
+ assert(game);
+
+ for (size_t i = 0; i < EI_ALIEN_GROUP_SIZE; i++) {
+ for (size_t j = 0; j < ARRAY_SIZE(game->aliens); j++) {
+ struct ei_alien *tmp = ei_alien_group_get(&game->aliens[j], i);
+
+ if (!ei_alien_is_dead(tmp)) {
+ nr_firing_aliens++;
+ break;
+ }
+ }
+ }
+
+ if (nr_firing_aliens > 0) {
+ int index;
+
+ index = eetg_rand() % nr_firing_aliens;
+
+ for (size_t i = ARRAY_SIZE(game->aliens) - 1;
+ i < ARRAY_SIZE(game->aliens); i--) {
+ struct ei_alien *tmp = ei_alien_group_get(&game->aliens[i], index);
+
+ if (!ei_alien_is_dead(tmp)) {
+ alien = tmp;
+ break;
+ }
+ }
+ }
+
+ return alien;
+}
+
+static void
+ei_game_process_alien_missile(struct ei_game *game)
+{
+ assert(game);
+
+ if (game->first_alien_missile_counter != 0) {
+ assert(game->first_alien_missile_counter > 0);
+ game->first_alien_missile_counter--;
+ return;
+ }
+
+ if (eetg_object_get_world(&game->alien_missile) != NULL) {
+ assert(game->alien_missile_counter > 0);
+ game->alien_missile_counter--;
+
+ if (game->alien_missile_counter == 0) {
+ int x, y;
+
+ game->alien_missile_counter = game->alien_missile_counter_reload;
+
+ x = eetg_object_get_x(&game->alien_missile);
+ y = eetg_object_get_y(&game->alien_missile);
+
+ if (y == EETG_ROWS) {
+ eetg_world_remove(&game->world, &game->alien_missile);
+ } else {
+ eetg_object_move(&game->alien_missile, x, y + 1);
+ }
+ }
+ } else {
+ struct ei_alien *alien;
+ int x, y;
+
+ alien = ei_game_select_firing_alien(game);
+
+ if (alien) {
+ x = eetg_object_get_x(&alien->object);
+ y = eetg_object_get_y(&alien->object);
+
+ eetg_world_add(&game->world, &game->alien_missile, x, y + 1);
+
+ game->alien_missile_counter = game->alien_missile_counter_reload;
+ }
+ }
+}
+
+static void
+ei_game_process_aliens(struct ei_game *game)
+{
+ assert(game);
+ assert(game->aliens_speed_counter > 0);
+
+ game->aliens_speed_counter--;
+
+ if (game->aliens_speed_counter != 0) {
+ return;
+ }
+
+ game->aliens_speed_counter = game->aliens_speed_counter_reload;
+
+ if (game->aliens_move_down) {
+ for (size_t i = ARRAY_SIZE(game->aliens) - 1;
+ i < ARRAY_SIZE(game->aliens); i--) {
+ bool game_over;
+
+ game_over = ei_alien_group_move_down(&game->aliens[i]);
+
+ if (game_over) {
+ ei_game_terminate(game);
+ }
+ }
+
+ game->aliens_move_down = false;
+
+ if (eetg_object_get_world(&game->ufo) == NULL) {
+ int n;
+
+ n = eetg_rand() % 3;
+
+ if (n == 0) {
+ int x;
+
+ n = eetg_rand() % 2;
+
+ if (n == 0) {
+ x = EETG_COLUMNS;
+ game->ufo_moves_left = true;
+ } else {
+ x = -eetg_object_get_width(&game->ufo);
+ game->ufo_moves_left = false;
+ }
+
+ eetg_world_add(&game->world, &game->ufo, x, 2);
+
+ game->ufo_counter = game->ufo_counter_reload;
+ }
+ }
+ } else {
+ bool border_reached = false;
+
+ for (size_t i = 0; i < ARRAY_SIZE(game->aliens); i++) {
+ struct ei_alien_group *group = &game->aliens[i];
+ bool border_reached_by_group;
+
+ if (game->aliens_move_left) {
+ border_reached_by_group = ei_alien_group_move_left(group);
+ } else {
+ border_reached_by_group = ei_alien_group_move_right(group);
+ }
+
+ if (border_reached_by_group) {
+ border_reached = true;
+ }
+ }
+
+ if (border_reached) {
+ game->aliens_move_down = true;
+ game->aliens_move_left = !game->aliens_move_left;
+ }
+ }
+}
+
+static void
+ei_game_process_ufo(struct ei_game *game)
+{
+ struct eetg_object *ufo;
+
+ assert(game);
+
+ ufo = &game->ufo;
+
+ if (eetg_object_get_world(ufo) == NULL) {
+ return;
+ }
+
+ assert(game->ufo_counter > 0);
+
+ game->ufo_counter--;
+
+ if (game->ufo_counter != 0) {
+ return;
+ }
+
+ game->ufo_counter = game->ufo_counter_reload;
+
+ if (game->ufo_moves_left) {
+ int x;
+
+ x = eetg_object_get_x(ufo) - 1;
+
+ if ((x + eetg_object_get_width(ufo)) <= 0) {
+ eetg_world_remove(&game->world, ufo);
+ } else {
+ eetg_object_move(ufo, x, eetg_object_get_y(ufo));
+ }
+ } else {
+ int x;
+
+ x = eetg_object_get_x(ufo) + 1;
+
+ if (x >= EETG_COLUMNS) {
+ eetg_world_remove(&game->world, ufo);
+ } else {
+ eetg_object_move(ufo, x, eetg_object_get_y(ufo));
+ }
+ }
+}
+
+static void
+ei_game_init_bunkers(struct ei_game *game)
+{
+ assert(game);
+
+ for (size_t i = 0; i < ARRAY_SIZE(game->bunkers); i++) {
+ ei_bunker_init(&game->bunkers[i]);
+ }
+}
+
+static void
+ei_game_init_aliens(struct ei_game *game)
+{
+ assert(game);
+
+ for (size_t i = 0; i < ARRAY_SIZE(game->aliens); i++) {
+ struct ei_alien_group *group = &game->aliens[i];
+ const char *sprite1, *sprite2;
+ int color;
+
+ sprite1 = ei_get_group_sprite1(i);
+ sprite2 = ei_get_group_sprite2(i);
+ color = ei_get_group_color(i);
+
+ ei_alien_group_init(group, sprite1, sprite2, color);
+ }
+}
+
+void
+ei_game_init(struct ei_game *game, eetg_write_fn write_fn, void *arg)
+{
+ assert(game);
+
+ game->sync_counter_reload = EI_FPS * 2;
+ game->sync_counter = 1;
+
+ game->state = EI_STATE_INTRO;
+
+ ei_game_reset_history(game);
+
+ eetg_world_init(&game->world, write_fn, arg);
+ eetg_world_register_collision_fn(&game->world,
+ ei_game_handle_collision,
+ game);
+
+ eetg_object_init(&game->title, EI_TYPE_TITLE, EI_TITLE_SPRITE);
+ eetg_object_set_color(&game->title, EETG_COLOR_BLUE);
+
+ eetg_object_init(&game->help, EI_TYPE_HELP, EI_HELP_SPRITE);
+ eetg_object_set_color(&game->help, EETG_COLOR_RED);
+
+ eetg_object_init(&game->start, EI_TYPE_START, EI_START_SPRITE);
+ eetg_object_set_color(&game->start, EETG_COLOR_RED);
+
+ eetg_object_init(&game->player, EI_TYPE_PLAYER, EI_PLAYER_SPRITE);
+ eetg_object_set_color(&game->player, EETG_COLOR_YELLOW);
+
+ eetg_object_init(&game->player_missile, EI_TYPE_PLAYER_MISSILE, "!\n");
+ eetg_object_set_color(&game->player_missile, EETG_COLOR_WHITE);
+
+ ei_game_init_bunkers(game);
+ ei_game_init_aliens(game);
+
+ eetg_object_init(&game->alien_missile, EI_TYPE_ALIEN_MISSILE, ":\n");
+ eetg_object_set_color(&game->alien_missile, EETG_COLOR_MAGENTA);
+
+ eetg_object_init(&game->ufo, EI_TYPE_UFO, "<o~o>\n");
+ eetg_object_set_color(&game->ufo, EETG_COLOR_MAGENTA);
+
+ eetg_object_init(&game->status, EI_TYPE_STATUS, game->status_sprite);
+ eetg_object_set_color(&game->status, EETG_COLOR_RED);
+
+ eetg_object_init(&game->end_title, EI_TYPE_END_TITLE, EI_END_TITLE_SPRITE);
+ eetg_object_set_color(&game->end_title, EETG_COLOR_WHITE);
+
+ eetg_world_add(&game->world, &game->title, 8, 1);
+ eetg_world_add(&game->world, &game->help, 30, 16);
+ eetg_world_add(&game->world, &game->start, 30, 20);
+}
+
+bool
+ei_game_process(struct ei_game *game, int8_t c)
+{
+ bool sync = false;
+ bool leave = false;
+
+ game->sync_counter--;
+
+ if (game->sync_counter == 0) {
+ sync = true;
+ game->sync_counter = game->sync_counter_reload;
+ }
+
+ eetg_world_render(&game->world, sync);
+
+ switch (game->state) {
+ case EI_STATE_INTRO:
+ case EI_STATE_GAME_OVER:
+ if (c >= 0) {
+ leave = ei_game_process_intro_input(game, (char)c);
+ }
+
+ break;
+ case EI_STATE_PREPARED:
+ ei_game_start(game);
+ break;
+ case EI_STATE_PLAYING:
+ ei_game_process_player_missile(game);
+ ei_game_process_aliens(game);
+ ei_game_process_ufo(game);
+ ei_game_process_alien_missile(game);
+
+ if (c >= 0) {
+ leave = ei_game_process_game_input(game, (char)c);
+ }
+
+ break;
+ }
+
+ return leave;
+}
diff --git a/src/ei.h b/src/ei.h
new file mode 100644
index 0000000..4772db2
--- /dev/null
+++ b/src/ei.h
@@ -0,0 +1,91 @@
+/*
+ * Copyright (c) 2024 Richard Braun.
+ *
+ * Permission to use, copy, modify, and/or distribute this software for
+ * any purpose with or without fee is hereby granted.
+ *
+ * THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL
+ * WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE
+ * FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY
+ * DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
+ * AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+ * OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ *
+ *
+ * Embedded invaders.
+ */
+
+#ifndef EI_H
+#define EI_H
+
+#include <stdbool.h>
+#include <stdint.h>
+
+#include "eetg.h"
+
+#define EI_FPS 50
+
+#define EI_NR_ALIEN_GROUPS 5
+#define EI_ALIEN_GROUP_SIZE 10
+#define EI_ALIEN_WIDTH 3
+
+#define EI_STATE_INTRO 0
+#define EI_STATE_PREPARED 1
+#define EI_STATE_PLAYING 2
+#define EI_STATE_GAME_OVER 3
+
+struct ei_bunker {
+ struct eetg_object object;
+ char sprite[33];
+};
+
+struct ei_alien {
+ struct eetg_object object;
+};
+
+struct ei_alien_group {
+ struct ei_alien aliens[EI_ALIEN_GROUP_SIZE];
+ const char *sprites[2];
+ char sprite[EI_ALIEN_WIDTH + 2];
+ int8_t sprite_index;
+};
+
+struct ei_game {
+ struct eetg_world world;
+ struct eetg_object title;
+ struct eetg_object help;
+ struct eetg_object start;
+ struct eetg_object player;
+ struct eetg_object player_missile;
+ struct ei_bunker bunkers[4];
+ struct ei_alien_group aliens[EI_NR_ALIEN_GROUPS];
+ struct eetg_object alien_missile;
+ struct eetg_object ufo;
+ struct eetg_object status;
+ struct eetg_object end_title;
+ int score;
+ int8_t sync_counter_reload;
+ int8_t sync_counter;
+ int8_t nr_lives;
+ int8_t player_missile_counter_reload;
+ int8_t player_missile_counter;
+ int8_t aliens_speed_counter_reload;
+ int8_t aliens_speed_counter;
+ int8_t first_alien_missile_counter;
+ int8_t alien_missile_counter_reload;
+ int8_t alien_missile_counter;
+ int8_t ufo_counter_reload;
+ int8_t ufo_counter;
+ int8_t nr_dead_aliens;
+ int8_t state;
+ bool aliens_move_left;
+ bool aliens_move_down;
+ bool ufo_moves_left;
+ char status_sprite[32];
+};
+
+void ei_game_init(struct ei_game *game, eetg_write_fn write_fn, void *arg);
+bool ei_game_process(struct ei_game *game, int8_t c);
+
+#endif /* EI_H */
diff --git a/src/macros.h b/src/macros.h
new file mode 100644
index 0000000..6910ae7
--- /dev/null
+++ b/src/macros.h
@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2024 Richard Braun.
+ *
+ * Permission to use, copy, modify, and/or distribute this software for
+ * any purpose with or without fee is hereby granted.
+ *
+ * THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL
+ * WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE
+ * FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY
+ * DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
+ * AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+ * OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ *
+ *
+ * Macros.
+ */
+
+#ifndef MACROS_H
+#define MACROS_H
+
+#define ARRAY_SIZE(x) (sizeof(x) / sizeof((x)[0]))
+
+#define structof(ptr, type, member) \
+ ((type *)((char *)(ptr) - offsetof(type, member)))
+
+#endif /* MACROS_H */
diff --git a/src/main.c b/src/main.c
new file mode 100644
index 0000000..bdd0c3a
--- /dev/null
+++ b/src/main.c
@@ -0,0 +1,98 @@
+/*
+ * Copyright (c) 2024 Richard Braun.
+ *
+ * Permission to use, copy, modify, and/or distribute this software for
+ * any purpose with or without fee is hereby granted.
+ *
+ * THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL
+ * WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE
+ * FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY
+ * DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
+ * AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+ * OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <assert.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <termios.h>
+#include <time.h>
+#include <unistd.h>
+
+#include "eetg.h"
+#include "ei.h"
+
+static struct termios orig_ios;
+
+static struct ei_game game;
+
+static void
+restore_termios(void)
+{
+ tcsetattr(STDIN_FILENO, TCSANOW, &orig_ios);
+ write(STDOUT_FILENO, "\ec", 2);
+}
+
+static void
+setup_io(void)
+{
+ struct termios ios;
+
+ setvbuf(stdin, NULL, _IONBF, 0);
+
+ fcntl(STDIN_FILENO, F_SETFL, fcntl(STDIN_FILENO, F_GETFL) | O_NONBLOCK);
+
+ tcgetattr(STDIN_FILENO, &orig_ios);
+ atexit(restore_termios);
+ ios = orig_ios;
+ ios.c_lflag &= ~(ICANON | ECHO);
+ ios.c_cc[VMIN] = 1;
+ ios.c_cc[VTIME] = 0;
+ tcsetattr(STDIN_FILENO, TCSANOW, &ios);
+}
+
+static void
+write_terminal(const void *buffer, size_t size, void *arg)
+{
+ (void)arg;
+
+ write(STDOUT_FILENO, buffer, size);
+}
+
+int
+main(void)
+{
+ bool leave;
+
+ setup_io();
+
+ eetg_init_rand(time(NULL));
+
+ ei_game_init(&game, write_terminal, NULL);
+
+ do {
+ ssize_t nr_bytes;
+ int8_t c;
+
+ usleep(1000000 / EI_FPS);
+
+ nr_bytes = read(STDIN_FILENO, &c, 1);
+
+ if (nr_bytes == -1) {
+ if (errno == EAGAIN) {
+ c = -1;
+ } else {
+ break;
+ }
+ }
+
+ leave = ei_game_process(&game, c);
+ } while (!leave);
+
+ return EXIT_SUCCESS;
+}