summary refs log tree commit diff homepage
diff options
context:
space:
mode:
authorJune McEnroe <june@causal.agency>2018-12-25 15:56:01 -0500
committerJune McEnroe <june@causal.agency>2018-12-25 15:56:01 -0500
commit1d2ff4c4f6b9feec493b5c49e318a30040d2476a (patch)
treed862b8786e756fb966db3b37c2bdfc2659c2a9d2
downloadplay-1d2ff4c4f6b9feec493b5c49e318a30040d2476a.tar.gz
play-1d2ff4c4f6b9feec493b5c49e318a30040d2476a.zip
Add 2048 game
-rw-r--r--.gitignore4
-rw-r--r--2048.c247
-rw-r--r--Makefile13
-rw-r--r--play.c171
4 files changed, 435 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..c7156d8
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+*.o
+2048.scores
+play
+tags
diff --git a/2048.c b/2048.c
new file mode 100644
index 0000000..8546d4b
--- /dev/null
+++ b/2048.c
@@ -0,0 +1,247 @@
+/* Copyright (C) 2018  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>
+
+typedef unsigned uint;
+
+static uint score;
+static uint grid[4][4];
+
+static void spawn(void) {
+	uint y, x;
+	do {
+		y = arc4random_uniform(4);
+		x = arc4random_uniform(4);
+	} while (grid[y][x]);
+	grid[y][x] = (arc4random_uniform(10) ? 1 : 2);
+}
+
+static bool slideLeft(void) {
+	bool slid = false;
+	for (uint y = 0; y < 4; ++y) {
+		uint x = 0;
+		for (uint i = 0; i < 4; ++i) {
+			if (grid[y][i] != grid[y][x]) slid = true;
+			if (grid[y][i]) grid[y][x++] = grid[y][i];
+		}
+		while (x < 4) grid[y][x++] = 0;
+	}
+	return slid;
+}
+static bool slideRight(void) {
+	bool slid = false;
+	for (uint y = 0; y < 4; ++y) {
+		uint x = 3;
+		for (uint i = 3; i < 4; --i) {
+			if (grid[y][i] != grid[y][x]) slid = true;
+			if (grid[y][i]) grid[y][x--] = grid[y][i];
+		}
+		while (x < 4) grid[y][x--] = 0;
+	}
+	return slid;
+}
+static bool slideUp(void) {
+	bool slid = false;
+	for (uint x = 0; x < 4; ++x) {
+		uint y = 0;
+		for (uint i = 0; i < 4; ++i) {
+			if (grid[i][x] != grid[y][x]) slid = true;
+			if (grid[i][x]) grid[y++][x] = grid[i][x];
+		}
+		while (y < 4) grid[y++][x] = 0;
+	}
+	return slid;
+}
+static bool slideDown(void) {
+	bool slid = false;
+	for (uint x = 0; x < 4; ++x) {
+		uint y = 3;
+		for (uint i = 3; i < 4; --i) {
+			if (grid[i][x] != grid[y][x]) slid = true;
+			if (grid[i][x]) grid[y--][x] = grid[i][x];
+		}
+		while (y < 4) grid[y--][x] = 0;
+	}
+	return slid;
+}
+
+static bool mergeLeft(void) {
+	bool merged = false;
+	for (uint y = 0; y < 4; ++y) {
+		for (uint x = 0; x < 3; ++x) {
+			if (!grid[y][x]) continue;
+			if (grid[y][x] != grid[y][x + 1]) continue;
+			score += 1 << ++grid[y][x];
+			grid[y][x + 1] = 0;
+			merged = true;
+		}
+	}
+	return merged;
+}
+static bool mergeRight(void) {
+	bool merged = false;
+	for (uint y = 0; y < 4; ++y) {
+		for (uint x = 3; x > 0; --x) {
+			if (!grid[y][x]) continue;
+			if (grid[y][x] != grid[y][x - 1]) continue;
+			score += 1 << ++grid[y][x];
+			grid[y][x - 1] = 0;
+			merged = true;
+		}
+	}
+	return merged;
+}
+static bool mergeUp(void) {
+	bool merged = false;
+	for (uint x = 0; x < 4; ++x) {
+		for (uint y = 0; y < 3; ++y) {
+			if (!grid[y][x]) continue;
+			if (grid[y][x] != grid[y + 1][x]) continue;
+			score += 1 << ++grid[y][x];
+			grid[y + 1][x] = 0;
+			merged = true;
+		}
+	}
+	return merged;
+}
+static bool mergeDown(void) {
+	bool merged = false;
+	for (uint x = 0; x < 4; ++x) {
+		for (uint y = 3; y > 0; --y) {
+			if (!grid[y][x]) continue;
+			if (grid[y][x] != grid[y - 1][x]) continue;
+			score += 1 << ++grid[y][x];
+			grid[y - 1][x] = 0;
+			merged = true;
+		}
+	}
+	return merged;
+}
+
+static bool left(void) {
+	return slideLeft() | mergeLeft() | slideLeft();
+}
+static bool right(void) {
+	return slideRight() | mergeRight() | slideRight();
+}
+static bool up(void) {
+	return slideUp() | mergeUp() | slideUp();
+}
+static bool down(void) {
+	return slideDown() | mergeDown() | slideDown();
+}
+
+static void curse(void) {
+	initscr();
+	cbreak();
+	noecho();
+	curs_set(0);
+	keypad(stdscr, true);
+	leaveok(stdscr, true);
+	start_color();
+	use_default_colors();
+	short bright = (COLORS > 8 ? 8 : 0);
+	init_pair(1,  COLOR_WHITE, COLOR_RED);
+	init_pair(2,  COLOR_WHITE, COLOR_GREEN);
+	init_pair(3,  COLOR_WHITE, COLOR_YELLOW);
+	init_pair(4,  COLOR_WHITE, COLOR_BLUE);
+	init_pair(5,  COLOR_WHITE, COLOR_MAGENTA);
+	init_pair(6,  COLOR_WHITE, COLOR_CYAN);
+	init_pair(7,  COLOR_WHITE, bright + COLOR_RED);
+	init_pair(8,  COLOR_WHITE, bright + COLOR_GREEN);
+	init_pair(9,  COLOR_WHITE, bright + COLOR_YELLOW);
+	init_pair(10, COLOR_WHITE, bright + COLOR_BLUE);
+	init_pair(11, COLOR_WHITE, bright + COLOR_MAGENTA);
+	init_pair(12, COLOR_WHITE, bright + COLOR_CYAN);
+	init_pair(13, COLOR_WHITE, COLOR_BLACK);
+}
+
+static void addchn(char ch, uint n) {
+	for (uint i = 0; i < n; ++i) {
+		addch(ch);
+	}
+}
+
+enum {
+	TileHeight = 3,
+	TileWidth = 7,
+	GridY = 2,
+	GridX = 2,
+	ScoreY = 0,
+	ScoreX = GridX + 4 * TileWidth - 10,
+};
+
+static void drawTile(uint y, uint x) {
+	if (grid[y][x]) {
+		attr_set(A_BOLD, 1 + (grid[y][x] - 1) % 12, NULL);
+	} else {
+		attr_set(A_NORMAL, 13, NULL);
+	}
+
+	char buf[8];
+	int len = snprintf(buf, sizeof(buf), "%d", 1 << grid[y][x]);
+	if (!grid[y][x]) buf[0] = '.';
+
+	move(GridY + TileHeight * y, GridX + TileWidth * x);
+	addchn(' ', TileWidth);
+
+	move(GridY + TileHeight * y + 1, GridX + TileWidth * x);
+	addchn(' ', (TileWidth - len + 1) / 2);
+	addstr(buf);
+	addchn(' ', (TileWidth - len) / 2);
+
+	move(GridY + TileHeight * y + 2, GridX + TileWidth * x);
+	addchn(' ', TileWidth);
+}
+
+static void draw(void) {
+	char buf[11];
+	snprintf(buf, sizeof(buf), "%10d", score);
+
+	attr_set(A_NORMAL, 0, NULL);
+	mvaddstr(ScoreY, ScoreX, buf);
+
+	for (uint y = 0; y < 4; ++y) {
+		for (uint x = 0; x < 4; ++x) {
+			drawTile(y, x);
+		}
+	}
+}
+
+static bool input(void) {
+	switch (getch()) {
+		break; case 'h': case KEY_LEFT: if (left()) spawn();
+		break; case 'j': case KEY_DOWN: if (down()) spawn();
+		break; case 'k': case KEY_UP: if (up()) spawn();
+		break; case 'l': case KEY_RIGHT: if (right()) spawn();
+		break; case 'q': return false;
+	}
+	return true;
+}
+
+uint play2048(void) {
+	curse();
+	spawn();
+	spawn();
+	do {
+		draw();
+	} while (input());
+	return score;
+}
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..162783a
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,13 @@
+CFLAGS += -std=c11 -Wall -Wextra
+LDLIBS = -lcurses
+
+OBJS += play.o
+OBJS += 2048.o
+
+all: tags play
+
+play: $(OBJS)
+	$(CC) $(LDFLAGS) $(OBJS) $(LDLIBS) -o $@
+
+tags: *.c
+	ctags -w *.c
diff --git a/play.c b/play.c
new file mode 100644
index 0000000..b062a0f
--- /dev/null
+++ b/play.c
@@ -0,0 +1,171 @@
+/* Copyright (C) 2018  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 <err.h>
+#include <fcntl.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/file.h>
+#include <sysexits.h>
+#include <time.h>
+
+typedef unsigned uint;
+
+uint play2048(void);
+
+enum { ScoresLen = 1000 };
+static struct Score {
+	time_t date;
+	uint score;
+	char name[32];
+} scores[ScoresLen];
+
+static FILE *scoresOpen(const char *path) {
+	int fd = open(path, O_RDWR | O_CREAT);
+	if (fd < 0) err(EX_CANTCREAT, "%s", path);
+	FILE *file = fdopen(fd, "r+");
+	if (!file) err(EX_CANTCREAT, "%s", path);
+	return file;
+}
+
+static void scoresLock(FILE *file) {
+	int error = flock(fileno(file), LOCK_EX);
+	if (error) err(EX_IOERR, "flock");
+}
+
+static void scoresRead(FILE *file) {
+	memset(scores, 0, sizeof(scores));
+	rewind(file);
+	fread(scores, sizeof(struct Score), ScoresLen, file);
+	if (ferror(file)) err(EX_IOERR, "fread");
+}
+
+static void scoresWrite(FILE *file) {
+	rewind(file);
+	fwrite(scores, sizeof(struct Score), ScoresLen, file);
+	if (ferror(file)) err(EX_IOERR, "fwrite");
+}
+
+static size_t scoresInsert(struct Score new) {
+	if (!new.score) return ScoresLen;
+	for (size_t i = 0; i < ScoresLen; ++i) {
+		if (scores[i].score > new.score) continue;
+		memmove(
+			&scores[i + 1], &scores[i],
+			sizeof(struct Score) * (ScoresLen - i - 1)
+		);
+		scores[i] = new;
+		return i;
+	}
+	return ScoresLen;
+}
+
+static void curse(void) {
+	initscr();
+	cbreak();
+	echo();
+	curs_set(1);
+	keypad(stdscr, true);
+	leaveok(stdscr, false);
+	start_color();
+	use_default_colors();
+	attr_set(A_NORMAL, 0, NULL);
+	erase();
+}
+
+static void addchn(char ch, uint n) {
+	for (uint i = 0; i < n; ++i) {
+		addch(ch);
+	}
+}
+
+enum {
+	ScoresTop = 15,
+	ScoresY = 0,
+	ScoresX = 2,
+	ScoreX = ScoresX + 5,
+	NameX = ScoreX + 12,
+	DateX = NameX + 33,
+	ScoresWidth = DateX + 11 - ScoresX,
+};
+static const char ScoresTitle[] = "TOP SCORES";
+
+static int scoreY(size_t i) {
+	if (i < ScoresTop) return ScoresY + 2 + i;
+	return ScoresY + 3 + ScoresTop;
+}
+
+static void drawScore(size_t i) {
+	char buf[11];
+	snprintf(buf, sizeof(buf), "%3zu.", 1 + i);
+	mvaddstr(scoreY(i), ScoresX, buf);
+
+	snprintf(buf, sizeof(buf), "%10d", scores[i].score);
+	mvaddstr(scoreY(i), ScoreX, buf);
+
+	mvaddstr(scoreY(i), NameX, scores[i].name);
+
+	struct tm *time = localtime(&scores[i].date);
+	if (!time) err(EX_SOFTWARE, "localtime");
+	char date[sizeof("YYYY-MM-DD")];
+	strftime(date, sizeof(date), "%F", time);
+	mvaddstr(scoreY(i), DateX, date);
+}
+
+static void draw(size_t new) {
+	move(ScoresY, ScoresX + (ScoresWidth - sizeof(ScoresTitle) + 2) / 2);
+	addstr(ScoresTitle);
+	move(ScoresY + 1, ScoresX);
+	addchn('=', ScoresWidth);
+	for (size_t i = 0; i < ScoresTop; ++i) {
+		if (!scores[i].score) break;
+		attr_set(i == new ? A_BOLD : A_NORMAL, 0, NULL);
+		drawScore(i);
+	}
+	if (new == ScoresLen) return;
+	if (new >= ScoresTop) {
+		attr_set(A_BOLD, 0, NULL);
+		drawScore(new);
+	}
+}
+
+int main(void) {
+	struct Score new = { .date = time(NULL) };
+	new.score = play2048();
+	FILE *file = scoresOpen("2048.scores");
+	scoresRead(file);
+	size_t index = scoresInsert(new);
+
+	curse();
+	draw(index);
+	if (index < ScoresLen) {
+		attr_set(A_BOLD, 0, NULL);
+		mvgetnstr(scoreY(index), NameX, new.name, sizeof(new.name) - 1);
+
+		scoresLock(file);
+		scoresRead(file);
+		scoresInsert(new);
+		scoresWrite(file);
+		fclose(file);
+	}
+
+	curs_set(0);
+	getch();
+	endwin();
+}