summary refs log tree commit diff homepage
diff options
context:
space:
mode:
-rw-r--r--snake.c206
1 files changed, 206 insertions, 0 deletions
diff --git a/snake.c b/snake.c
new file mode 100644
index 0000000..3eb28f5
--- /dev/null
+++ b/snake.c
@@ -0,0 +1,206 @@
+/* Copyright (C) 2021  C. McEnroe <june@causal.agency>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <curses.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+typedef unsigned uint;
+typedef unsigned char byte;
+
+enum {
+	Rows = 24,
+	Cols = 48,
+};
+
+static uint score;
+static const char *over;
+
+static struct {
+	uint len;
+	byte y[Rows * Cols];
+	byte x[Rows * Cols];
+} snake = { .len = 1 };
+
+static struct {
+	int y, x;
+	int dy, dx;
+} head = { Rows / 2, Cols / 2, 0, 1 };
+
+enum {
+	FoodCap = 25,
+	FoodChance = 15,
+	FoodRipe = Rows + Cols,
+	FoodSpoil = FoodRipe + Cols,
+	FoodMulch = FoodSpoil * 10,
+};
+
+static struct {
+	uint len;
+	byte y[FoodCap];
+	byte x[FoodCap];
+	uint age[FoodCap];
+} food;
+
+static void tick(void) {
+	for (uint i = 0; i < food.len; ++i) {
+		if (head.y + head.dy != food.y[i]) continue;
+		if (head.x + head.dx != food.x[i]) continue;
+		if (food.age[i] > FoodSpoil) {
+			over = "You ate spoiled food!";
+			return;
+		}
+		score += snake.len * (food.age[i] > FoodRipe ? 2 : 1);
+		food.len--;
+		food.y[i] = food.y[food.len];
+		food.x[i] = food.x[food.len];
+		food.age[i] = food.age[food.len];
+		snake.len++;
+		break;
+	}
+	for (uint i = food.len - 1; i < food.len; --i) {
+		if (food.age[i]++ < FoodMulch) continue;
+		food.len--;
+		food.y[i] = food.y[food.len];
+		food.x[i] = food.x[food.len];
+		food.age[i] = food.age[food.len];
+	}
+	if (!food.len || (food.len < FoodCap && !arc4random_uniform(FoodChance))) {
+		int y = arc4random_uniform(Rows);
+		int x = arc4random_uniform(Cols);
+		bool empty = true;
+		if (y == head.y && x == head.x) empty = false;
+		for (uint i = 0; i < snake.len; ++i) {
+			if (y == snake.y[i] && x == snake.x[i]) empty = false;
+		}
+		for (uint i = 0; i < food.len; ++i) {
+			if (y == food.y[i] && x == food.x[i]) empty = false;
+		}
+		if (empty) {
+			food.y[food.len] = y;
+			food.x[food.len] = x;
+			food.age[food.len] = 0;
+			food.len++;
+		}
+	}
+	for (uint i = snake.len - 1; i < snake.len; --i) {
+		if (i) {
+			snake.y[i] = snake.y[i-1];
+			snake.x[i] = snake.x[i-1];
+		} else {
+			snake.y[i] = head.y;
+			snake.x[i] = head.x;
+		}
+	}
+	head.y += head.dy;
+	head.x += head.dx;
+	if (head.y < 0 || head.x < 0 || head.y >= Rows || head.x >= Cols) {
+		over = "You eated the wall D:";
+	}
+	for (uint i = 0; i < snake.len; ++i) {
+		if (head.y != snake.y[i] || head.x != snake.x[i]) continue;
+		over = "You eated yourself :(";
+	}
+}
+
+enum { KeyOK = KEY_MAX + 1 };
+static void curse(void) {
+	initscr();
+	cbreak();
+	noecho();
+	curs_set(0);
+	keypad(stdscr, true);
+	nodelay(stdscr, true);
+	define_key("\33[0n", KeyOK);
+	start_color();
+	use_default_colors();
+	init_pair(1, COLOR_GREEN, -1);
+	init_pair(2, COLOR_YELLOW, -1);
+	init_pair(3, COLOR_RED, -1);
+	mvhline(Rows, 0, ACS_HLINE, Cols);
+	mvvline(0, Cols, ACS_VLINE, Rows);
+	mvaddch(Rows, Cols, ACS_LRCORNER);
+}
+
+static void draw(void) {
+	char buf[16];
+	snprintf(buf, sizeof(buf), "%u", score);
+	mvaddstr(0, Cols + 2, buf);
+	if (over) {
+		mvaddstr(2, Cols + 2, over);
+		mvaddstr(3, Cols + 2, "Press any key to");
+		mvaddstr(4, Cols + 2, "view the scoreboard.");
+	}
+	for (int y = 0; y < Rows; ++y) {
+		mvhline(y, 0, ' ', Cols);
+	}
+	for (uint i = 0; i < food.len; ++i) {
+		if (food.age[i] > FoodSpoil) {
+			mvaddch(food.y[i], food.x[i], '*' | COLOR_PAIR(3));
+		} else if (food.age[i] > FoodRipe) {
+			mvaddch(food.y[i], food.x[i], '%' | COLOR_PAIR(2));
+		} else {
+			mvaddch(food.y[i], food.x[i], '&' | COLOR_PAIR(1));
+		}
+	}
+	for (uint i = 0; i < snake.len; ++i) {
+		mvaddch(snake.y[i], snake.x[i], '#' | COLOR_PAIR(2));
+	}
+	mvaddch(head.y, head.x, '@' | A_BOLD);
+	move(head.y, head.x);
+	refresh();
+}
+
+static void input(void) {
+	int ch = getch();
+	if (ch == ERR) {
+		putp("\33[5n");
+		fflush(stdout);
+		nodelay(stdscr, false);
+		ch = getch();
+		nodelay(stdscr, true);
+	}
+	switch (ch) {
+		break; case 'h': case KEY_LEFT:  head.dy =  0; head.dx = -1;
+		break; case 'j': case KEY_DOWN:  head.dy = +1; head.dx =  0;
+		break; case 'k': case KEY_UP:    head.dy = -1; head.dx =  0;
+		break; case 'l': case KEY_RIGHT: head.dy =  0; head.dx = +1;
+		break; case 'q': over = "You are satisfied.";
+		break; case ERR: exit(EXIT_FAILURE);
+	}
+}
+
+uint playSnake(void) {
+	curse();
+	do {
+		tick();
+		draw();
+		usleep(150000);
+		input();
+	} while (!over);
+	draw();
+	while (ERR != getch());
+	nodelay(stdscr, false);
+	getch();
+	return score;
+}
+
+int main(void) {
+	playSnake();
+	endwin();
+}