sok: A sokoban clone for Fuzix in under 16K of memory
authorAlan Cox <alan@linux.intel.com>
Thu, 25 Oct 2018 23:49:58 +0000 (00:49 +0100)
committerAlan Cox <alan@linux.intel.com>
Thu, 25 Oct 2018 23:49:58 +0000 (00:49 +0100)
Basically works - take care your terminal width is set sensibly.

Currently the undo command is broken but restart does work

Applications/games/sok.c [new file with mode: 0644]
Applications/games/sok.h [new file with mode: 0644]
Applications/games/sokmap.c [new file with mode: 0644]

diff --git a/Applications/games/sok.c b/Applications/games/sok.c
new file mode 100644 (file)
index 0000000..b6ee52f
--- /dev/null
@@ -0,0 +1,479 @@
+/*
+ *     A tiny Sokoban clone
+ */
+
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <ctype.h>
+#include <fcntl.h>
+#include <termios.h>
+#include <unistd.h>
+#include <signal.h>
+#include <sys/ioctl.h>
+#include <pwd.h>
+#include <termcap.h>
+#include "sok.h"
+
+static struct winsize win;
+static int conf_fd = -1;
+static int level_fd;
+
+static struct level map;
+static struct level base_map;
+
+static uint16_t level;
+
+#define MAX_UNDO       64
+static uint8_t undolist[MAX_UNDO];
+static uint8_t undop;
+static uint16_t moves;
+
+static uint8_t left;
+
+#define BLOCK_MOVED    0x80
+
+#define MAP_WALL       '#'
+#define MAP_FLOOR      ' '
+#define MAP_BLOCK      '$'
+#define MAP_BLOCK_ON   '*'
+#define MAP_TARGET     '.'
+
+/* HIJKL */
+static int8_t delta[5][2] = { {0, -1}, {0, 0}, {1, 0}, {-1, 0}, {0, 1} };
+
+
+static char *t_go;
+static char *t_clreol;
+static char *t_clreos;
+
+/* str is either a space buffer for clearing or the output queue for
+   termios, but never both at once */
+
+#define MAXCOLS        132
+
+static char str[MAXCOLS];
+static char *ptr;
+
+#ifdef __linux__
+#include <stdio.h>
+static char *_uitoa(int x)
+{
+       static char buf[10];
+       sprintf(buf, "%d", x);
+       return buf;
+}
+#endif
+
+static int outbuf(int c)
+{
+       *ptr++ = c;
+       return c;
+}
+
+static void tputs_buf(char *p, int n)
+{
+       ptr = str;
+       tputs(p, n, outbuf);
+       write(1, str, ptr - str);
+}
+
+static void moveto(uint8_t y, uint8_t x)
+{
+       tputs_buf(tgoto(t_go, x, y), 1);
+}
+
+static void draw(uint8_t y, uint8_t x, uint8_t c)
+{
+       moveto(2 + y, x + left);
+       write(1, &c, 1);
+}
+
+static void clrtoeol(void)
+{
+       if (*t_clreol)
+               tputs_buf(t_clreol, 1);
+       else {
+               memset(str, ' ', win.ws_col);
+               write(1, str, win.ws_col);
+       }
+}
+
+static void clear(void)
+{
+       if (*t_clreos) {
+               moveto(0, 0);
+               tputs_buf(t_clreos, win.ws_row);
+       } else {
+               int i = 0;
+               while (i++ <= win.ws_row) {
+                       moveto(i, 0);
+                       clrtoeol();
+               }
+       }
+}
+
+static struct termios tcsave, tcnew;
+
+void is_done(void)
+{
+       if (tcsetattr(0, TCSANOW, &tcsave))
+               perror("tcsetattr:exit");
+}
+
+void quit_game(int sig)
+{
+       is_done();
+       _exit(1);
+}
+
+
+static int ival[3];
+
+static char *tnext(char *p)
+{
+       return p + strlen(p) + 1;
+}
+
+static void tty_init(void)
+{
+       int fd[2];
+       pid_t pid;
+
+       if (pipe(fd) < 0) {
+               perror("pipe");
+               exit(1);
+       }
+
+       pid = fork();
+       if (pid == -1) {
+               perror("fork");
+               exit(1);
+       }
+
+       if (pid == 0) {
+               close(fd[0]);
+               dup2(fd[1], 1);
+               execl("/usr/lib/tchelp", "tchelp", "li#co#cm$ce$cd$cl$",
+                     NULL);
+               _exit(1);
+       }
+       close(fd[1]);
+
+       if (read(fd[0], ival,sizeof(int)) != sizeof(int)) {
+               perror("tcread");
+               exit(1);
+       }
+       if (ival[0] == 0)
+               exit(1);
+
+       if (read(fd[0], ival + 1 ,2 * sizeof(int)) != 2 * sizeof(int)) {
+               perror("tcread");
+               exit(1);
+       }
+       /* Don't need space for the two integer values reported */
+       ival[0] -= 2 * sizeof(int);
+       t_go = sbrk((ival[0] + 3) & ~3);
+       if (t_go == (void *) -1) {
+               perror("sbrk");
+               exit(1);
+       }
+       if (read(fd[0], t_go, ival[0]) != ival[0]) {
+               perror("tcread2");
+               exit(1);
+       }
+       close(fd[0]);
+       t_clreol = tnext(t_go);
+       t_clreos = tnext(t_clreol);
+       if (*t_clreos == 0)     /* No clr eos - try for clr/home */
+               t_clreos++;     /* cl cap if present */
+
+       if (!*t_go) {
+               write(2, "sok: insufficient terminal control.\n", 36);
+               exit(1);
+       }
+
+       if (tcgetattr(0, &tcsave) == 0) {
+               atexit(is_done);
+               memcpy(&tcnew, &tcsave, sizeof(struct termios));
+               tcnew.c_cc[VMIN] = 1;
+               tcnew.c_cc[VTIME] = 0;
+               tcnew.c_lflag &= ~(ICANON | ECHO | ECHOE | ECHOK);
+               signal(SIGINT, quit_game);
+               /* FIXME: handle stop/cont properly */
+               signal(SIGTSTP, SIG_IGN);
+               if (tcsetattr(0, TCSADRAIN, &tcnew))
+                       perror("tcsetattr");
+       }
+       if (ioctl(0, TIOCGWINSZ, &win)) {
+               win.ws_col = 80;
+               win.ws_row = 25;
+       }
+       if (win.ws_col > MAXCOLS)
+               win.ws_col = MAXCOLS;
+       left = (win.ws_col - MAP_W) / 2;
+}
+
+static void drawplayer(void)
+{
+       if (map.map[map.py][map.px] == MAP_TARGET)
+               draw(map.py, map.px, '+');
+       else
+               draw(map.py, map.px, '@');
+}
+
+static void wipe_line(void)
+{
+       write(1, "                                ", 31);
+}
+
+static uint8_t getkey(void)
+{
+       uint8_t c;
+       if (read(0, &c, 1) < 1)
+               exit(1);
+       return toupper(c);
+}
+
+static uint8_t fixup(uint8_t code)
+{
+       if (code == MAP_BLOCK)
+               code = MAP_FLOOR;
+       if (code == MAP_BLOCK_ON)
+               code = MAP_TARGET;
+       return code;
+}
+
+static void move(uint8_t dir)
+{
+       int8_t *dp;
+       uint8_t ny, nx, n;
+
+       dp = delta[dir];
+       ny = map.py + *dp;
+       nx = map.px + dp[1];
+
+       n = map.map[ny][nx];
+
+       if (n == MAP_WALL)
+               return;
+
+       if (n == MAP_BLOCK || n == MAP_BLOCK_ON) {
+               uint8_t by = ny + *dp;
+               uint8_t bx = nx + dp[1];
+               uint8_t m = map.map[by][bx];
+               if (m == MAP_WALL || m == MAP_BLOCK || m == MAP_BLOCK_ON)
+                       return;
+               /* Adjust count of blocks positioned */
+               if (map.map[by][bx] == MAP_TARGET) {
+                       draw(by, bx, MAP_BLOCK_ON);
+                       map.done++;
+               } else
+                       draw(by, bx, MAP_BLOCK);
+
+               /* Move on the map */
+               map.map[by][bx] = MAP_BLOCK;
+               map.map[ny][nx] = fixup(base_map.map[ny][nx]);
+               if (map.map[ny][nx] == MAP_TARGET)
+                       map.done--;
+               /* We don't need to redraw dy dx as we will put the player on it */
+               dir |= BLOCK_MOVED;     /* For undo */
+       }
+       draw(map.py, map.px, map.map[map.py][map.px]);
+       map.py += *dp;
+       map.px += *++dp;
+       drawplayer();
+       moves++;
+       if (undop == MAX_UNDO) {
+               memmove(undolist, undolist + 1, MAX_UNDO - 1);
+               undop--;
+       }
+       undolist[undop++] = dir;
+}
+
+
+
+static uint8_t reverse[] = "L KJH";
+
+static void undo(void)
+{
+       uint8_t op;
+       uint8_t dir;
+       uint8_t c;
+       int8_t *dp;
+       uint8_t pry, prx;
+
+       if (undop == 0)
+               return;
+       op = undolist[--undop];
+       dir = op & 0x7F;
+       /* As we are reversing a move we know we won't be pushing any other
+          block */
+       if (op & 0x80) {
+               /* We need to reverse a block */
+               if (map.map[map.py][map.px] == MAP_TARGET)
+                       map.done++;
+               map.map[map.py][map.px] = MAP_BLOCK;
+               /* The block after the one in direction goes back to previous
+                  (we don't allow a multi-block push) */
+               dp = delta[dir];
+               pry = map.py + *dp;
+               prx = map.px + *++dp;
+               c = base_map.map[pry][prx];
+               map.map[pry][prx] = c;
+               draw(pry, prx, c);
+               if (c == MAP_TARGET)
+                       map.done--;
+               move(reverse[dir] - 'H');
+       }
+}
+
+static uint8_t notifier(const char *p)
+{
+       uint8_t c;
+       moveto(0, 0);
+       write(1, "Do you want to ", 15);
+       write(1, p, strlen(p));
+       write(1, " (Y/N)", 6);
+       c = getkey();
+       moveto(0, 0);
+       wipe_line();
+       return c == 'Y';
+}
+
+static void redraw_all(void)
+{
+       uint8_t i;
+       clear();
+       for (i = 0; i < MAP_H; i++) {
+               moveto(2 + i, left);
+               write(1, map.map[i], MAP_W);
+       }
+       drawplayer();
+}
+
+
+static void start_level(void)
+{
+       moves = 0;
+       undop = 0;
+       memcpy(&map, &base_map.map, sizeof(map));
+       redraw_all();
+}
+
+static void key(void)
+{
+       uint8_t c;
+       moveto(0,0);
+       switch (c = getkey()) {
+       case 'Q':
+               if (notifier("quit"))
+                       exit(0);
+               break;
+       case 'R' & 31:
+               redraw_all();
+               break;
+       case 'R':
+               if (notifier("restart"))
+                       start_level();
+               break;
+       case 'U':
+               undo();
+               break;
+       case 'H':
+       case 'J':
+       case 'K':
+       case 'L':
+               move(c - 'H');
+               break;
+       }
+       /* TODO arrow key handling */
+}
+
+static void play_level(void)
+{
+       start_level();
+       while (map.done != map.blocks)
+               key();
+       moveto(0, 0);
+       strcpy(str, "Level ");
+       strcpy(str + 6, _uitoa(level));
+       strcat(str + 6, " done in ");
+       strcat(str + 6, _uitoa(moves));
+       strcat(str + 6, " moves.");
+       write(1, str, strlen(str));
+       getkey();
+}
+
+/*
+ *     There are various formats that mostly try and compress data
+ *     down, notably the rather tight scheme used by Pusher, and the
+ *     much more general approach used by XSB and the like. We naturally
+ *     do our own thing for now and expect a preprocessed fixed size level
+ *     on disk.
+ */
+
+static int load_level(uint16_t level)
+{
+       if (lseek(level_fd, level * MAP_SEEK, SEEK_SET) == -1 ||
+           read(level_fd, &base_map,
+                sizeof(struct level)) != sizeof(struct level))
+               return 0;
+       return 1;
+}
+
+static void play_game(void)
+{
+       while (load_level(level)) {
+               play_level();
+               level++;
+               /* Save the new level achieved */
+               if (conf_fd != -1) {
+                       lseek(conf_fd, 0L, SEEK_SET);
+                       write(conf_fd, &level, 2);
+               }
+       }
+}
+
+int main(int argc, char *argv[])
+{
+       uint16_t n;
+       struct passwd *pw;
+       const char *path = "/usr/lib/sok/sok.levels";
+
+       if (argc > 2 && strcmp(argv[1], "-l") == 0) {
+               level = atoi(argv[2]);
+               argc -= 2;
+               argv += 2;
+       }
+       if (argc == 2)
+               path = argv[1];
+       if (argc > 2) {
+               write(2, "sok [-l level] [path to levels].\n", 21);
+               exit(1);
+       }
+       
+       level_fd = open(path, O_RDONLY);
+       if (level_fd == -1) {
+               perror(path);
+               exit(1);
+       }
+
+       if (level == 0) {
+               level = 1;
+               pw = getpwuid(getuid());
+               if (pw && chdir(pw->pw_dir) == 0) {
+                       conf_fd = open(".sokoban", O_RDWR | O_CREAT, 0600);
+                       if (conf_fd != -1) {
+                               if (read(conf_fd, &n, 2) == 2)
+                                       level = n;
+                       }
+               }
+       }
+       tty_init();
+       /* Level '0' is actually not a level but a title screen */
+       load_level(0);
+       start_level();
+       getkey();
+       play_game();
+}
diff --git a/Applications/games/sok.h b/Applications/games/sok.h
new file mode 100644 (file)
index 0000000..20fa7bc
--- /dev/null
@@ -0,0 +1,13 @@
+#define MAP_H  14
+#define MAP_W  32
+
+/* Disk align each sector for speed */
+#define MAP_SEEK       512
+
+struct level {
+       uint8_t map[MAP_H][MAP_W];
+       uint8_t py;
+       uint8_t px;
+       uint8_t done;
+       uint8_t blocks;
+};
diff --git a/Applications/games/sokmap.c b/Applications/games/sokmap.c
new file mode 100644 (file)
index 0000000..6a01990
--- /dev/null
@@ -0,0 +1,166 @@
+/*
+ *     Simple map convertor for the maps we care about
+ */
+
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include <fcntl.h>
+#include <unistd.h>
+
+#include "sok.h"
+
+static int level;
+static uint8_t x, y;
+static uint8_t height, width;
+
+static int map_fd;
+
+static struct level map;
+
+void map_line(const char *p)
+{
+    uint8_t x = 0;
+    while(*p) {
+        switch(*p) {
+        case '*':
+            map.done++;
+        case '$':
+            map.blocks++;
+        case '#':
+        case '.':
+        case ' ':
+            map.map[y][x++] = *p;
+            break;
+        case '+':
+            map.py = y;
+            map.px = x;
+            map.map[y][x++] = '.';
+            break;
+        case '@':
+            map.py = y;
+            map.px = x;
+            map.map[y][x++] = ' ';
+            break;
+        default:
+            if (level == 0)
+                map.map[y][x++] = *p;
+            else {
+                fprintf(stderr, "Bad map line at '%s'.\n",  p);
+                exit(1);
+            }
+            break;
+        }
+        p++;
+        if (x > width)
+            width = x;
+    }
+    height++;
+    y++;
+}
+
+static void next_map(void)
+{
+    /* Reset */
+    memset(&map, 0, sizeof(map));
+    memset(&map.map, ' ', sizeof(map.map));
+    x = 0;
+    y = 0;
+    width = 0;
+    height = 0;
+}
+
+static void draw_check(void)
+{
+    int i;
+    for (i = 0; i < MAP_H; i++) {
+        printf("| ");
+        fwrite(map.map[i], MAP_W, 1, stdout);
+        printf(" |\n");
+    }
+}
+
+static void level_done(void)
+{
+    uint8_t left = (MAP_W - width) / 2;
+    uint8_t top = (MAP_H - height) / 2;
+    uint8_t i;
+    /* Centre the map */
+    for (i = 0; i < height; i++) {
+        memmove(map.map[i] + left, map.map[i], MAP_W - left);
+        memset(map.map[i], ' ', left);
+    }
+    /* And vertically */
+    memmove(((uint8_t *)map.map) + top * MAP_W,
+        map.map, height * MAP_W);
+    memset((uint8_t *)map.map, ' ', top * MAP_W);
+    map.py += top;
+    map.px += left;
+    /* Write it out */
+    if (lseek(map_fd, level * MAP_SEEK, SEEK_SET) < 0) {
+        perror("lseek");
+        exit(1);
+    }
+    if (write(map_fd, &map, sizeof(map)) != sizeof(map)) {
+        perror("write");
+        exit(1);
+    }
+    draw_check();
+    level++;
+    next_map();
+}
+
+static void process(FILE *fp)
+{
+    char buf[1024];
+    int in_level = 0;
+
+    while(fgets(buf, 1024, fp)) {
+        char *p = strchr(buf, '\n');
+        if (p == NULL) {
+            fprintf(stderr, "Invalid line '%s'.\n", buf);
+            exit(1);
+        }
+        *p = 0;
+        if (in_level) {
+            if (strchr(buf, '#'))
+                map_line(buf);
+            else {
+                level_done();
+                in_level = 0;
+            }
+        } else {
+            if (strchr(buf, '#')) {
+                in_level = 1;
+                map_line(buf);
+            }
+        }
+    }
+    if (in_level == 1)
+        level_done();
+}
+
+int main(int argc, char *argv[])
+{
+    FILE *fp;
+    if (argc != 3) {
+        fprintf(stderr, "%s infile outfile\n", argv[0]);
+        exit(1);
+    }
+    fp = fopen(argv[1], "r");
+    if (fp == NULL) {
+        perror(argv[1]);
+        exit(1);
+    }
+    map_fd = open(argv[2], O_CREAT|O_TRUNC|O_WRONLY, 0600);
+    if (map_fd == -1) {
+        perror(argv[2]);
+        exit(1);
+    }
+    next_map();
+    process(fp);
+    fclose(fp);
+    close(map_fd);
+    return 0;
+}