sacc

sacc - sacc(omys), simple console gopher client (config)
git clone git://git.codemadness.org/sacc
Log | Files | Refs | LICENSE

commit 90a9d3c81f27588c62faa43f22d946740042a3ce
parent cc6a6881e06ab4b851c7778842374b953e51539c
Author: Julian Schweinsberg <pazz0@0xfa.de>
Date:   Mon, 28 Oct 2024 15:17:33 +0100

Update ui_rogue

ui_rogue: Overengineered Edition

This adds a readme and notes (mostly ideas) file for ui_rogue, too.

These changes were made half a year ago and I don't do real
documentation of my changes...
(There is some git branch, but the commit messages aren't helpful:
"HuH?!", "Well, better than nothing...", "Typical
pazz0-overengineering.", ...)

Changes:
- Better menus
- Bigger maps (160x50)
  - Moving scrolls the displayed map if needed
- Rooms and corridors use ASCII characters to look like hack
- Removes look mode for now
- Replaces xorshift PRNG algorithm with ranqd1 (this means that the
  layout of gopherholes change with this update)
- Moved setupterm call out of signal handler (IIRC [you remember:
  months] there were some crashes I had in combination with heap
  allocations, my thought was that setupterm is using heap allocations)
- Parameters for room generation are randomly selected (based on the
  hostname and port) from a list of "dungeontypes"

Diffstat:
Mui_rogue.c | 1225+++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------
Aui_rogue_notes | 31+++++++++++++++++++++++++++++++
Aui_rogue_readme | 28++++++++++++++++++++++++++++
3 files changed, 912 insertions(+), 372 deletions(-)

diff --git a/ui_rogue.c b/ui_rogue.c @@ -1,3 +1,5 @@ +#include <errno.h> +#include <signal.h> #include <stdarg.h> #include <stdint.h> #include <stdio.h> @@ -6,6 +8,7 @@ #include <term.h> #include <termios.h> #include <unistd.h> +#include <sys/select.h> #include <sys/types.h> #include "common.h" @@ -22,6 +25,47 @@ #define ERR (-1) #endif +#define maplines (lines - 2) + +enum { + Blocks = 1, + Standout = 2, + Important = 4 +}; + +struct cell; +struct tile { + char c; + char flags; + char *name; + char *description; + char *afterinteract; + Item *(*interact)(struct cell *); +}; + +struct cell { + struct tile *tile; + size_t nitems; + Item **items; +}; + +struct room { + size_t x, y; + size_t w, h; +}; + +struct rect { + struct rect *next, *next2; + struct rect *p; + size_t x1, y1; + size_t x2, y2; + size_t d; + union { + void *p; + int i; + } data; +}; + static struct termios tsave; static struct termios tsacc; static Item *curentry; @@ -29,8 +73,88 @@ static int termset = ERR; static char bufout[256]; static char bufout2[256]; +size_t ox, oy; +size_t px, py; + +#define MAPHEIGHT (50) +#define MAPWIDTH (160) +struct cell map[MAPHEIGHT][MAPWIDTH]; + +enum { + DungeonScreen, + MenuScreen +} screen; + +Item *interactitem(struct cell *); +Item *interactmenu(struct cell *); + +struct tile tile_void = { ' ', Blocks, "Void", "The void. The thing which is everywhere where nothing is.", NULL, NULL }; +struct tile tile_floor = { '.', 0, "Floor", "An ordinary stone floor.", NULL, NULL }; +struct tile tile_corridor = { '#', 0, "Different Floor", "This floor looks different than the other one.", NULL, NULL }; +struct tile tile_verticalwall = { '|', Blocks, "Wall", "Wall.", NULL, NULL }; +struct tile tile_horizontalwall = { '-', Blocks, "Wall", "Wall.", NULL, NULL }; +struct tile tile_door = { '/', 0, "Door", "A door.", NULL, NULL }; +struct tile tile_bookshelf = { 'E', Important, "Bookshelf", "A bookshelf.", "A loading bar?! In a book?!", interactmenu }; +struct tile tile_book = { '?', Important, "%s", "A book: '%s'.", "A loading bar?! In a book?!", interactitem }; +struct tile tile_portal = { '0', Important, "%s", "A portal: '%s'.", "You are getting transported through time and space.", interactitem }; +struct tile tile_portalmachine = { 'O', Important, "Portal Machine", "A portal machine.", "You are getting transported through time and space.", interactmenu }; +struct tile tile_heapofstuff = { '%', Important, "Heap", "A heap of stuff.", "The thing you touches glows strangely...", interactmenu }; +struct tile tile_elevator = { 'L', Important, "Elevator", "An elevator.", "You hear elevator music...", interactmenu }; +struct tile tile_stairsdown = { '>', Important, "'%s'", "A staircase leading down: '%s'.", "Too many stairs...", interactitem }; + +struct tile tile_stairsup = { '<', Standout | Important, "'%s'", "A staircase leading up: '%s'.", "Too many stairs...", interactitem }; +struct tile tile_backportal = { '0', Standout | Important, "'%s'", "A portal leading back to wherever you came from: '%s'.", "You are getting transported through time and space.", interactitem }; + void drawscreen(void); +int +mygetchar_(void) +{ + int r; + fd_set fdset; + + FD_ZERO(&fdset); + FD_SET(0, &fdset); + + if ((r = select(1, &fdset, NULL, NULL, NULL)) == -1) { + if (errno == EINTR) + return -1; + return -2; + } + + return getchar(); +} + +volatile sig_atomic_t sigwinch; + +int +mygetchar(void) +{ + int r; + + while ((r = mygetchar_()) == -1) { + if (sigwinch) { + sigwinch = 0; + + if (termset == OK) + del_curterm(cur_term); + termset = setupterm(NULL, 1, NULL); + + drawscreen(); + } + } + + if (r == -2) + die("mygetchar: %s", strerror(errno)); + + return r; +} + +/* + FNV-1a ( http://www.isthe.com/chongo/tech/comp/fnv/ ) + FNV was published into the public domain ( https://creativecommons.org/publicdomain/zero/1.0/ ) + by Landon Curt Noll: http://www.isthe.com/chongo/tech/comp/fnv/#public_domain +*/ uint32_t fnv1a(int n,...) { @@ -41,7 +165,7 @@ fnv1a(int n,...) h = 0x811c9dc5; - va_start(l, n); + va_start(l, n); for (i = 0; i < n; i++) { for (s = va_arg(l, char*); *s; s++) { h ^= *s; @@ -53,42 +177,17 @@ fnv1a(int n,...) return h; } -uint32_t -xorshift(uint32_t *s) +/* + An LCG using the constants from "Numerical Recipes". +*/ +uint16_t +ranqd1(uint32_t *s) { - *s ^= *s << 13; - *s ^= *s >> 17; - *s ^= *s << 5; - return *s; + return (*s = 1664525 * (*s) + 1013904223) >> 16; } -struct cell { - char c; - size_t nitems; - Item **items; -}; - -#define MAPHEIGHT (25) -#define MAPWIDTH (80) -struct cell map[MAPHEIGHT][MAPWIDTH]; - -struct room { - struct room *p; - void *d; - size_t x, y; - size_t w, h; -}; - -struct rect { - struct rect *next, *next2; - struct room *room; - size_t x1, y1; - size_t x2, y2; - size_t d; -}; - struct rect * -randomneighbor(struct rect *x, struct rect *rs, uint32_t *prng) +randomneighbor(struct rect *x, struct rect *rs, uint32_t *prng, int (*filter)(struct rect *, struct rect *)) { struct rect *r, *result; size_t n; @@ -96,35 +195,54 @@ randomneighbor(struct rect *x, struct rect *rs, uint32_t *prng) n = 0; result = NULL; for (r = rs; r; r = r->next) { + if (r == x) + continue; if (r->y2 < x->y1 || r->y1 > x->y2 || r->x2 < x->x1 || r->x1 > x->x2) continue; if ((r->y2 == x->y1 || r->y1 == x->y2) && (r->x2 == x->x1 || r->x1 == x->x2)) continue; + if (!filter(x, r)) + continue; n++; - if (xorshift(prng) / (1. + UINT32_MAX) < 1. / n) + if (ranqd1(prng) / (1. + UINT16_MAX) < 1. / n) result = r; } return result; } -#define ROOM_HEIGHT_MIN 3 -#define ROOM_WIDTH_MIN 5 -#define ROOM_MARGIN_MIN 1 -#define CELL_HEIGHT_MIN (ROOM_HEIGHT_MIN + ROOM_MARGIN_MIN + 3) -#define CELL_WIDTH_MIN (ROOM_WIDTH_MIN + ROOM_MARGIN_MIN + 3) size_t -generaterooms_gnarf(uint32_t prng, struct room *rs, size_t l) +min(size_t a, size_t b) +{ + if (a < b) + return a; + return b; +} + +size_t +max(size_t a, size_t b) +{ + if (a > b) + return a; + return b; +} + +/* + Creates an uneven grid by splitting the map recursively. + Returns an array containing the cells (rects) of the grid. +*/ +struct rect * +generaterects(size_t heightmin, size_t widthmin, uint32_t prng) { struct rect *queuehead, *queuetail; struct rect *r, *t; - struct rect *rects, *walk; - size_t w, h, i, j, rl, n; - int vertical; - struct room *room; + struct rect *rects; + size_t w, h; + int vertical, spaceforvertical, spaceforhorizontal; r = malloc(sizeof(*r)); - r->x1 = r->y1 = ROOM_MARGIN_MIN; + memset(r, 0, sizeof(*r)); + r->x1 = r->y1 = 0; r->x2 = MAPWIDTH; r->y2 = MAPHEIGHT; r->d = 0; @@ -134,7 +252,6 @@ generaterooms_gnarf(uint32_t prng, struct room *rs, size_t l) queuehead = r; rects = NULL; - rl = 0; while (queuehead) { r = queuehead; @@ -142,35 +259,36 @@ generaterooms_gnarf(uint32_t prng, struct room *rs, size_t l) queuetail = NULL; queuehead = queuehead->next; - if (r->x2 - r->x1 >= CELL_WIDTH_MIN * 2 && r->y2 - r->y1 >= CELL_HEIGHT_MIN * 2) { - vertical = xorshift(&prng) & 1; - } else if (r->x2 - r->x1 >= CELL_WIDTH_MIN * 2) { + spaceforvertical = r->y2 - r->y1 >= heightmin * 2; + spaceforhorizontal = r->x2 - r->x1 >= widthmin * 2; + + if (spaceforhorizontal && spaceforvertical) { + vertical = ranqd1(&prng) & 1; + } else if (spaceforhorizontal) { vertical = 0; - } else if (r->y2 - r->y1 >= CELL_HEIGHT_MIN * 2) { + } else if (spaceforvertical) { vertical = 1; } else { r->next = rects; rects = r; - rl++; continue; } if (vertical) { w = r->x2 - r->x1; - h = CELL_HEIGHT_MIN + xorshift(&prng) % (1 + r->y2 - r->y1 - CELL_HEIGHT_MIN * 2); + h = heightmin + ranqd1(&prng) % (1 + r->y2 - r->y1 - heightmin * 2); } else { - w = CELL_WIDTH_MIN + xorshift(&prng) % (1 + r->x2 - r->x1 - CELL_WIDTH_MIN * 2); + w = widthmin + ranqd1(&prng) % (1 + r->x2 - r->x1 - widthmin * 2); h = r->y2 - r->y1; } t = malloc(sizeof(*t)); + memset(t, 0, sizeof(*t)); t->x1 = r->x1; t->y1 = r->y1; t->x2 = r->x1 + w; t->y2 = r->y1 + h; t->d = r->d + 1; - t->next = NULL; - t->room = NULL; if (!queuetail) { queuehead = t; @@ -181,6 +299,7 @@ generaterooms_gnarf(uint32_t prng, struct room *rs, size_t l) } t = malloc(sizeof(*t)); + memset(t, 0, sizeof(*t)); if (vertical) { t->x1 = r->x1; t->y1 = r->y1 + h; @@ -191,8 +310,6 @@ generaterooms_gnarf(uint32_t prng, struct room *rs, size_t l) t->x2 = r->x2; t->y2 = r->y2; t->d = r->d + 1; - t->next = NULL; - t->room = NULL; queuetail->next = t; queuetail = t; @@ -200,75 +317,107 @@ generaterooms_gnarf(uint32_t prng, struct room *rs, size_t l) free(r); } - if (l > rl) - l = rl; + return rects; +} - for (r = rects; r; r = r->next) { - if (MAPHEIGHT / 2 >= r->y1 && MAPHEIGHT / 2 < r->y2 && - MAPWIDTH / 2 >= r->x1 && MAPWIDTH / 2 < r->x2) - break; - } - - i = 0; - rs[i].w = ROOM_WIDTH_MIN + xorshift(&prng) % (1 + r->x2 - r->x1 - ROOM_MARGIN_MIN - ROOM_WIDTH_MIN); - rs[i].h = ROOM_HEIGHT_MIN + xorshift(&prng) % (1 + r->y2 - r->y1 - ROOM_MARGIN_MIN - ROOM_HEIGHT_MIN); - rs[i].x = r->x1 + xorshift(&prng) % (1 + r->x2 - r->x1 - ROOM_MARGIN_MIN - rs[i].w); - rs[i].y = r->y1 + xorshift(&prng) % (1 + r->y2 - r->y1 - ROOM_MARGIN_MIN - rs[i].h); - rs[i].p = NULL; - r->room = &rs[i]; - - walk = r; - walk->next2 = NULL; - - i++; - for (; i < l;) { - t = randomneighbor(r, rects, &prng); - if (!t || t->room) { - n = 0; - for (t = walk; t; t = t->next2) { - n++; - if (xorshift(&prng) / (1. + UINT32_MAX) < 1. / n) - r = t; - - } - continue; - } - rs[i].w = ROOM_WIDTH_MIN + xorshift(&prng) % (1 + t->x2 - t->x1 - ROOM_MARGIN_MIN - ROOM_WIDTH_MIN); - rs[i].h = ROOM_HEIGHT_MIN + xorshift(&prng) % (1 + t->y2 - t->y1 - ROOM_MARGIN_MIN - ROOM_HEIGHT_MIN); - rs[i].x = t->x1 + xorshift(&prng) % (1 + t->x2 - t->x1 - ROOM_MARGIN_MIN - rs[i].w); - rs[i].y = t->y1 + xorshift(&prng) % (1 + t->y2 - t->y1 - ROOM_MARGIN_MIN - rs[i].h); - rs[i].p = r->room; - t->room = &rs[i]; - i++; - r = t; - r->next2 = walk; - walk = r; - } +void +connectpoints_horizontal(size_t y, + size_t ax, int ea, struct tile *at, + size_t bx, int eb, struct tile *bt, + struct tile *t) +{ + size_t i, s, e; + ssize_t ii; - for (r = rects; r;) { - t = r->next; - free(r); - r = t; - } + if (ax < bx) + ii = 1; + else if (ax > bx) + ii = -1; + else + ii = 0; - return l; + s = ax; + if (ea) + s += ii; + e = bx + ii; + if (eb) + e -= ii; + + for (i = s; i != e; i += ii) + map[y][i].tile = t; + + if (e - ii == s) { + if (at != t) + map[y][s].tile = at; + if (bt != t) + map[y][s].tile = bt; + } else { + map[y][s].tile = at; + map[y][e - ii].tile = bt; + } } -size_t -distance(size_t x1, size_t y1, size_t x2, size_t y2) +void +connectpoints_vertical(size_t x, + size_t ay, int ea, struct tile *at, + size_t by, int eb, struct tile *bt, + struct tile *t) { - size_t d; + size_t i, s, e; + ssize_t ii; - if (y1 < y2) - d = y2 - y1; - else - d = y1 - y2; - if (x1 < x2) - d += x2 - x1; + if (ay < by) + ii = 1; + else if (ay > by) + ii = -1; else - d += x1 - x2; + ii = 0; - return d; + s = ay; + if (ea) + s += ii; + e = by + ii; + if (eb) + e -= ii; + + for (i = s; i != e; i += ii) + map[i][x].tile = t; + + if (e - ii == s) { + if (at != t) + map[s][x].tile = at; + if (bt != t) + map[s][x].tile = bt; + } else { + map[s][x].tile = at; + map[e - ii][x].tile = bt; + } +} + +void +connectpoints(size_t ax, size_t ay, int ea, struct tile *at, + size_t bx, size_t by, int eb, struct tile *bt, + int vertical, struct tile *ct) +{ + if (!vertical) { + connectpoints_horizontal(ay, + ax, ea, at, + bx, 0, ct, + ct); + connectpoints_vertical(bx, + ay, 0, ct, + by, eb, bt, + ct); + } else { + connectpoints_vertical(ax, + ay, ea, at, + by, 0, ct, + ct); + connectpoints_horizontal(by, + ax, 0, ct, + bx, eb, bt, + ct); + } } void @@ -300,50 +449,246 @@ nearestpoints(struct room *a, struct room *b, size_t *ax, size_t *ay, size_t *bx } void -connectrooms(struct room *a, struct room *b) +connectadjacentrooms(struct rect *a, struct room *ar, struct rect *b, struct room *br) { - size_t i, j; - ssize_t ii; - size_t x1, y1; - size_t x2, y2; + size_t irx1, iry1, irx2, iry2; + size_t rx1, ry1, rx2, ry2; + size_t cx, cy; + struct rect *r1, *r2; + struct room *room1, *room2; + int vertical; - nearestpoints(a, b, &x1, &y1, &x2, &y2); + if (a->x2 == b->x1) { + r1 = a; + room1 = ar; + r2 = b; + room2 = br; + } else if (b->x2 == a->x1) { + r1 = b; + room1 = br; + r2 = a; + room2 = ar; + } else if (a->y2 == b->y1) { + r1 = a; + room1 = ar; + r2 = b; + room2 = br; + } else if (b->y2 == a->y1) { + r1 = b; + room1 = br; + room2 = ar; + r2 = a; + } else { + return; + } - if (y1 > y2) { - ii = -1; - } else if (y2 > y1) { - ii = 1; + if (r1->y2 == r2->y1) { + irx1 = max(r1->x1, r2->x1); + irx2 = min(r1->x2, r2->x2); + iry1 = r1->y2; + iry2 = r1->y2 + 1; } else { - ii = 0; + iry1 = max(r1->y1, r2->y1); + iry2 = min(r1->y2, r2->y2); + irx1 = r1->x2; + irx2 = r1->x2 + 1; + } + + nearestpoints(room1, room2, &rx1, &ry1, &rx2, &ry2); + + if (r1->y2 == r2->y1) { + /* both points are in the intersection */ + if (rx1 >= irx1 && rx1 < irx2 && + rx2 >= irx1 && rx2 < irx2) { + vertical = 1; + cx = (rx2 + rx1) / 2; + cy = (ry2 + ry1) / 2; + } else + /* none is in the intersection */ + if (!(rx1 >= irx1 && rx1 < irx2) && + !(rx2 >= irx1 && rx2 < irx2)) { + vertical = 0; + cx = irx1; + cy = r1->y2; + } else if (rx1 >= irx1 && rx1 < irx2) { + vertical = 1; + cx = (rx2 + rx1) / 2; + cy = r1->y2; + } else if (rx2 >= irx1 && rx2 < irx2) { + vertical = 1; + cx = rx2; + cy = r1->y2 - 1; + } + } else { + /* both points are in the intersection */ + if (ry1 >= iry1 && ry1 < iry2 && + ry2 >= iry1 && ry2 < iry2) { + vertical = 0; + cx = (rx2 + rx1) / 2; + cy = (ry2 + ry1) / 2; + } else + /* none is in the intersection */ + if (!(ry1 >= iry1 && ry1 < iry2) && + !(ry2 >= iry1 && ry2 < iry2)) { + vertical = 1; + cx = r1->x2; + cy = iry1; + } else if (ry1 >= iry1 && ry1 < iry2) { + vertical = 0; + cx = r1->x2; + cy = (ry2 + ry1) / 2; + } else if (ry2 >= iry1 && ry2 < iry2) { + vertical = 0; + cx = r1->x2 - 1; + cy = ry2; + } } + if (rx1 == rx2) { + connectpoints_vertical(rx1, + ry1, 1, &tile_door, + ry2, 1, &tile_door, + &tile_corridor); + } else if (ry1 == ry2) { + connectpoints_horizontal(ry1, + rx1, 1, &tile_door, + rx2, 1, &tile_door, + &tile_corridor); + } else { + connectpoints(rx1, ry1, 1, &tile_door, + cx, cy, 0, &tile_corridor, + vertical, &tile_corridor); + connectpoints(cx, cy, 1, &tile_corridor, + rx2, ry2, 1, &tile_door, + !vertical, &tile_corridor); + } +} + +int +rectisfull(struct rect *x, struct rect *r) +{ + return !!r->data.i; +} + +int +rectisempty(struct rect *x, struct rect *r) +{ + return !r->data.i; +} + +int +rectisnotp(struct rect *x, struct rect *r) +{ + return r->data.p && x->p != r && r->p != x; +} + +int +rectisrandom(struct rect *x, struct rect *r) +{ + return 1; +} + /* -printf("%lu\t%lu\t%d\n", y1, y2, ii); + Basically https://www.roguebasin.com/index.php/Diffusion-limited_aggregation + Returns the list of carved rooms. */ - for (i = y1; i != y2; i += ii) - map[i][x1].c = '.'; +struct rect * +dla(struct rect *rects, size_t l, uint32_t prng) { + size_t rl, i, n; + struct rect *r, *t, *walk, *p; - if (x1 > x2) { - ii = -1; - } else if (x2 > x1) { - ii = 1; - } else { - ii = 0; + for (r = rects, rl = 0; r; r = r->next) + rl++; + + if (l > rl) + l = rl; + + /* get the rect which contains the map center */ + for (r = rects; r; r = r->next) { + if (MAPHEIGHT / 2 >= r->y1 && MAPHEIGHT / 2 < r->y2 && + MAPWIDTH / 2 >= r->x1 && MAPWIDTH / 2 < r->x2) + break; } - for (i = x1; i != x2; i += ii) - map[y2][i].c = '.'; + p = NULL; + walk = NULL; + i = 0; + for (;;) { + r->p = p; + r->data.i = 1; + r->next2 = walk; + walk = r; + + if (i >= l - 1) + break; + + t = NULL; + for (r = rects, n = 0; r; r = r->next) { + if (r->data.i) + continue; + n++; + if (ranqd1(&prng) / (1. + UINT16_MAX) < 1. / n) + t = r; + } + + /* there is no free rect left */ + if (!t) + break; + + /* do a random walk starting from t until the walk collides with a carved room (r) */ + while ((r = randomneighbor(t, rects, &prng, rectisrandom)) && !r->data.i) + t = r; + + p = r; + r = t; + + i++; + } + + return walk; +} + +void +rendermapchar(size_t i, size_t j) { + if (map[i][j].tile->flags & Standout) + putp(tparm(enter_standout_mode, 0, 0, 0, 0, 0, 0, 0, 0, 0)); + putchar(map[i][j].tile->c); + if (map[i][j].tile->flags & Standout) + putp(tparm(exit_standout_mode, 0, 0, 0, 0, 0, 0, 0, 0, 0)); +} + +void +rendermapline(size_t i) +{ + size_t j; + + for (j = ox; j < min(MAPWIDTH, ox + columns); j++) + rendermapchar(i, j); } void rendermap(void) { - size_t i, j; + size_t i; - for (i = 0; i < MAPHEIGHT; i++) { - for (j = 0; j < MAPWIDTH; j++) - putchar(map[i][j].c); - putchar('\n'); + if (px < columns / 2 || MAPWIDTH <= columns) + ox = 0; + else if (px >= MAPWIDTH - columns / 2 - 1) + ox = MAPWIDTH - columns; + else + ox = px - columns / 2; + + if (py < maplines / 2 || MAPHEIGHT <= maplines) + oy = 0; + else if (py >= MAPHEIGHT - maplines / 2 - 1) + oy = MAPHEIGHT - maplines; + else + oy = py - maplines / 2; + + for (i = oy; i < min(MAPHEIGHT, oy + (lines - 2)); i++) { + if (i != oy) + putp(tparm(cursor_down, 0, 0, 0, 0, 0, 0, 0, 0, 0)); + rendermapline(i); } } @@ -366,92 +711,171 @@ placeitems_hash(Item *item, size_t *assocs, size_t k) return k; } -#define POSITIONS_LENGTH 4 +#define POSITIONS_LENGTH 5 enum { Portal, StaircaseDown, Bookshelf, + OtherStuff, Back }; -size_t px, py; +enum { + FillEntireCell = 1 +}; + +#define length(a) (sizeof(a) / sizeof(a[0])) +static struct dungeontype { + char *name; + char flags; + size_t heightmin; + size_t heightmax; + size_t widthmin; + size_t widthmax; + size_t margin; + size_t wiggle; +} dungeontypes[] = { + { "rogueish", 0, 2, 5, 3, 7, 2, 0 }, + { "rogueish-wide", 0, 2, 5, 3, 7, 3, 1 }, + { "compact", FillEntireCell, 2, 2, 3, 3, 1, 0 }, +}; void -generatemap(Item *item, int new) +generatemap(Item *item, Item *pitem) { Dir *dir; Item *citem; - size_t i, j, k, l, ir; - size_t x, y; - ssize_t n, m; + size_t l, i, j, k, ir, n, m, x, y; + struct rect *rects, *walk, *tr, *cr; + struct room *rooms, *room; size_t *cassocs; - struct room *rooms, *r; + int changedlevel, gonedown; + char buffer[10]; + uint32_t prng; struct { - unsigned char x, y; + size_t x, y; } positions[POSITIONS_LENGTH]; - uint32_t prng; - char buffer[3]; + size_t cellwidth, cellheight; + struct dungeontype *type; + + type = &dungeontypes[fnv1a(3, item->host, item->port, "dungeontype") % length(dungeontypes)]; + + cellheight = type->heightmin + 2 * type->margin + type->wiggle; + cellwidth = type->widthmin + 2 * type->margin + type->wiggle; + + rects = generaterects(cellheight, cellwidth, fnv1a(4, item->host, item->port, item->selector, "gridseed")); + + dir = item->dat; + for (j = l = 0; j < dir->nitems; j++) { + if (dir->items[j].type != 0 && + dir->items[j].type != 'i' && + dir->items[j].type != '3') + l++; + } + + k = 1 + l / 10; + walk = dla(rects, k, fnv1a(4, item->host, item->port, item->selector, "randomwalkseed")); + for (cr = walk, k = 0; cr; cr = cr->next2, k++); + + for (cr = rects; cr; cr = cr->next) + cr->data.p = NULL; + + rooms = calloc(k, sizeof(*rooms)); + for (cr = walk, i = 0; cr; cr = cr->next2, i++) + cr->data.p = &rooms[i]; + + prng = fnv1a(4, item->host, item->port, item->selector, "roomsseed"); + for (cr = walk; cr; cr = cr->next2) { + room = cr->data.p; + + if (type->flags & FillEntireCell) { + room->w = cr->x2 - cr->x1 - 2 * type->margin; + room->x = cr->x1 + type->margin; + room->h = cr->y2 - cr->y1 - 2 * type->margin; + room->y = cr->y1 + type->margin; + } else { + room->w = type->widthmin + ranqd1(&prng) % (1 + min(cr->x2 - cr->x1 - type->widthmin - 2 * type->margin, type->widthmax - type->widthmin)); + room->x = cr->x1 + type->margin + ranqd1(&prng) % (1 + cr->x2 - cr->x1 - room->w - 2 * type->margin); + room->h = type->heightmin + ranqd1(&prng) % (1 + min(cr->y2 - cr->y1 - type->heightmin - 2 * type->margin, type->heightmax - type->heightmin)); + room->y = cr->y1 + type->margin + ranqd1(&prng) % (1 + cr->y2 - cr->y1 - room->h - 2 * type->margin); + } + } for (i = 0; i < MAPHEIGHT; i++) { for (j = 0; j < MAPWIDTH; j++) { - map[i][j].c = '#'; + map[i][j].tile = &tile_void; free(map[i][j].items); map[i][j].items = NULL; map[i][j].nitems = 0; } } - dir = item->dat; - for (j = l = 0; j < dir->nitems; j++) { - if (dir->items[j].type == '0' || - dir->items[j].type == '1') - l++; + for (cr = walk; cr; cr = cr->next2) { + room = cr->data.p; + + for (x = room->x - 1; x < room->x + room->w + 1; x++) + map[room->y-1][x].tile = &tile_horizontalwall; + for (y = room->y; y < room->y + room->h; y++) { + map[y][room->x - 1].tile = &tile_verticalwall; + for (x = room->x; x < room->x + room->w; x++) + map[y][x].tile = &tile_floor; + map[y][room->x + room->w].tile = &tile_verticalwall; + } + for (x = room->x - 1; x < room->x + room->w + 1; x++) + map[room->y + room->h][x].tile = &tile_horizontalwall; } - k = 1 + l / 10; - rooms = calloc(k, sizeof(*rooms)); - if (!rooms) - return; - k = generaterooms_gnarf(fnv1a(3, item->host, item->port, item->selector), rooms, k); + for (cr = walk; cr; cr = cr->next2) { + if (cr->p) + connectadjacentrooms(cr, cr->data.p, + cr->p, cr->p->data.p); + + /* Add some loop possibility */ + if (tr = randomneighbor(cr, rects, &prng, rectisnotp)) + connectadjacentrooms(cr, cr->data.p, + tr, tr->data.p); + } cassocs = calloc(dir->nitems, sizeof(*cassocs)); - if (!cassocs) - goto cleanup; k = placeitems_hash(item, cassocs, k); - /* Insert rooms */ - for (i = 0; i < k; i++) { - for (y = rooms[i].y; y < rooms[i].y + rooms[i].h; y++) { - for (x = rooms[i].x; x < rooms[i].x + rooms[i].w; x++) - map[y][x].c = '.'; - } - } - - /* Insert connections */ - for (i = 0; i < k; i++) { - if (rooms[i].p) - connectrooms(&rooms[i], rooms[i].p); - } + changedlevel = item != pitem; + gonedown = pitem == item->entry; /* Insert items - The placement of items affects the initial placement of the player, because they could have gone back to this map, so they should appear at the elevator/portal/stair they used. + The placement of items affects the initial placement of the + player, because they could have gone back to this map, so they + should appear at the elevator/portal/stair they used. */ - ir = fnv1a(4, item->host, item->port, item->selector, "initial_room") % k; + + /* + The initial room is everytime the first one. Reason: The count + of rooms is based on how many entries are in the gophermap and + how many rooms can fit on the map. There will be at minimum 1. + So when more entries get added there will be more rooms but the + first one stays at the same position. I think about the + retrying and clownflare things on bitreich.org, the selector + doesn't change... + */ + ir = 0; for (i = 0; i < k; i++) { - snprintf(buffer, sizeof(buffer), "%d", i); + /* select random positions for different item types inside the current room */ + snprintf(buffer, sizeof(buffer), "%lu", i); prng = fnv1a(4, item->host, item->port, item->selector, buffer); - for (j = 0, n = 0, m = rooms[i].h * rooms[i].w; j < m; j++) { - if ((m - j) * (xorshift(&prng) / (double)UINT32_MAX) < POSITIONS_LENGTH - n) { + for (j = 0, m = rooms[i].h * rooms[i].w; j < m; j++) { + n = j; + if (j >= POSITIONS_LENGTH) + n *= ranqd1(&prng) / (double)UINT16_MAX; + + if (n < POSITIONS_LENGTH) { positions[n].x = rooms[i].x + j % rooms[i].w; positions[n].y = rooms[i].y + j / rooms[i].w; - n++; } - if (n == POSITIONS_LENGTH) - break; } + for (j = 0; j < dir->nitems; j++) { if (cassocs[j] != i) continue; @@ -462,35 +886,44 @@ generatemap(Item *item, int new) x = positions[Bookshelf].x; y = positions[Bookshelf].y; if (map[y][x].nitems) - map[y][x].c = 'E'; + map[y][x].tile = &tile_bookshelf; else - map[y][x].c = '?'; + map[y][x].tile = &tile_book; break; case '1': if (strcmp(citem->host, item->host) || strcmp(citem->port, item->port)) { x = positions[Portal].x; y = positions[Portal].y; if (map[y][x].nitems) - map[y][x].c = 'O'; + map[y][x].tile = &tile_portalmachine; else - map[y][x].c = '0'; + map[y][x].tile = &tile_portal; } else { x = positions[StaircaseDown].x; y = positions[StaircaseDown].y; if (map[y][x].nitems) - map[y][x].c = 'L'; + map[y][x].tile = &tile_elevator; else - map[y][x].c = '>'; + map[y][x].tile = &tile_stairsdown; } break; - default: + case 0: + case 'i': + case '3': continue; + break; + default: + x = positions[OtherStuff].x; + y = positions[OtherStuff].y; + map[y][x].tile = &tile_heapofstuff; + break; } + map[y][x].nitems++; map[y][x].items = realloc(map[y][x].items, map[y][x].nitems * sizeof(*map[y][x].items)); map[y][x].items[map[y][x].nitems-1] = citem; - if (new && j == dir->curline && citem->raw) { + if (changedlevel && citem == pitem) { px = x; py = y; } @@ -500,23 +933,28 @@ generatemap(Item *item, int new) y = positions[Back].y; x = positions[Back].x; if (strcmp(item->entry->host, item->host) || strcmp(item->entry->port, item->port)) - map[y][x].c = '0'; + map[y][x].tile = &tile_backportal; else - map[y][x].c = '<'; + map[y][x].tile = &tile_stairsup; map[y][x].nitems++; map[y][x].items = realloc(map[y][x].items, map[y][x].nitems * sizeof(*map[y][x].items)); map[y][x].items[map[y][x].nitems-1] = item->entry; } - if (i == ir && new && !dir->items[dir->curline].raw) { + if (changedlevel && i == ir && (gonedown || !pitem)) { px = positions[Back].x; py = positions[Back].y; } } - free(cassocs); -cleanup: + free(cassocs); free(rooms); + + for (cr = rects; cr;) { + tr = cr; + cr = cr->next; + free(tr); + } } void @@ -544,6 +982,7 @@ uicleanup(void) if (termset != OK) return; + putp(tparm(change_scroll_region, 0, lines-1, 0, 0, 0, 0, 0, 0, 0)); putp(tparm(clear_screen, 0, 0, 0, 0, 0, 0, 0, 0, 0)); fflush(stdout); } @@ -655,7 +1094,7 @@ uistatus(char *fmt, ...) putp(tparm(restore_cursor, 0, 0, 0, 0, 0, 0, 0, 0, 0)); fflush(stdout); - getchar(); + mygetchar(); } void @@ -668,225 +1107,287 @@ displayinfoline(char *fmt, ...) va_end(ap); } -Item * -showmenu(char *title, Item **item, size_t l) +char *menutitle; +Item **menuitems; +size_t menunitems; +volatile size_t menuoffset; +size_t menuselected; + +void +menudraw(void) { - size_t i; + size_t i, n; + putp(tparm(change_scroll_region, 1, lines-1, 0, 0, 0, 0, 0, 0, 0)); putp(tparm(clear_screen, 0, 0, 0, 0, 0, 0, 0, 0, 0)); putp(tparm(enter_standout_mode, 0, 0, 0, 0, 0, 0, 0, 0, 0)); - printf("%s\n", title); + puts(menutitle); putp(tparm(exit_standout_mode, 0, 0, 0, 0, 0, 0, 0, 0, 0)); - for (i = 0; i < l; i++) - printf("%lu\t%s\n", i, item[i]->username); - if (!scanf("%lu", &i) || i >= l) - return NULL; + if (menuselected - menuoffset >= lines - 1) + menuoffset = menuselected - (lines - 1) + 1; + + for (i = menuoffset, n = 0; i < menunitems && n < lines - 1; i++, n++) { + if (i != menuoffset) + putp(tparm(cursor_down, 0, 0, 0, 0, 0, 0, 0, 0, 0)); + if (i == menuselected) + putp(tparm(enter_standout_mode, 0, 0, 0, 0, 0, 0, 0, 0, 0)); + mbsprint(menuitems[i]->username, columns); + if (i == menuselected) + putp(tparm(exit_standout_mode, 0, 0, 0, 0, 0, 0, 0, 0, 0)); + putp(tparm(column_address, 0, 0, 0, 0, 0, 0, 0, 0, 0)); + } fflush(stdout); - - return item[i]; } Item * -prompt(char *text, Item *item) -{ - displayinfoline(text, item->username); - getchar(); - return item; -} - -Item * -interact(Item *item) +showmenu(char *title, Item **item, size_t l) { Item *selection; - selection = NULL; - - switch (map[py][px].c) { - case '?': - case '0': - case '>': - case '<': - selection = map[py][px].items[0]; - break; - case 'E': - selection = showmenu("Bookshelf", map[py][px].items, map[py][px].nitems); - break; - case 'O': - selection = showmenu("Portal machine", map[py][px].items, map[py][px].nitems); - break; - case 'L': - selection = showmenu("Elevator", map[py][px].items, map[py][px].nitems); - break; - } - + menutitle = title; + menuitems = item; + menunitems = l; + menuselected = 0; + menuoffset = 0; + screen = MenuScreen; drawscreen(); - if (selection) { - switch (map[py][px].c) { - case '?': - case 'E': - displayinfoline("A loading bar?! In a book?!"); - break; - case 'O': - case '0': - displayinfoline("You are getting transported through time and space."); + selection = NULL; + for (;;) { + switch (mygetchar()) { + case 'j': + if (menuselected + 1 < menunitems) { + putp(tparm(cursor_address, 1 + menuselected - menuoffset, 0, 0, 0, 0, 0, 0, 0, 0)); + mbsprint(menuitems[menuselected]->username, columns); + menuselected++; + putp(tparm(column_address, 0, 0, 0, 0, 0, 0, 0, 0, 0)); + if (menuselected - menuoffset >= lines - 1) { + menuoffset++; + putp(tparm(scroll_forward, 0, 0, 0, 0, 0, 0, 0, 0, 0)); + } else { + putp(tparm(cursor_down, 0, 0, 0, 0, 0, 0, 0, 0, 0)); + } + putp(tparm(enter_standout_mode, 0, 0, 0, 0, 0, 0, 0, 0, 0)); + mbsprint(menuitems[menuselected]->username, columns); + putp(tparm(exit_standout_mode, 0, 0, 0, 0, 0, 0, 0, 0, 0)); + } break; - case 'L': - displayinfoline("You hear elevator music..."); + case 'k': + if (menuselected > 0) { + putp(tparm(cursor_address, 1 + menuselected - menuoffset, 0, 0, 0, 0, 0, 0, 0, 0)); + mbsprint(menuitems[menuselected]->username, columns); + menuselected--; + putp(tparm(column_address, 0, 0, 0, 0, 0, 0, 0, 0, 0)); + if (menuselected < menuoffset) { + menuoffset = menuselected; + putp(tparm(scroll_reverse, 0, 0, 0, 0, 0, 0, 0, 0, 0)); + } else { + putp(tparm(cursor_up, 0, 0, 0, 0, 0, 0, 0, 0, 0)); + } + putp(tparm(enter_standout_mode, 0, 0, 0, 0, 0, 0, 0, 0, 0)); + mbsprint(menuitems[menuselected]->username, columns); + putp(tparm(exit_standout_mode, 0, 0, 0, 0, 0, 0, 0, 0, 0)); + } break; - case '<': - case '>': - displayinfoline("Too many stairs..."); + case ' ': + selection = menuitems[menuselected]; + case 0x1b: + goto endloop; break; } + fflush(stdout); } +endloop: + screen = DungeonScreen; + drawscreen(); + return selection; } +Item * +interactitem(struct cell *c) +{ + displayinfoline(map[py][px].tile->afterinteract); + + return map[py][px].items[0]; +} + +Item * +interactmenu(struct cell *c) +{ + Item *selection; + + if (selection = showmenu(map[py][px].tile->name, map[py][px].items, map[py][px].nitems)) + displayinfoline(map[py][px].tile->afterinteract); + + return selection; +} + +Item * +interact(Item *item) +{ + if (map[py][px].tile->interact) + return map[py][px].tile->interact(&map[py][px]); + + return NULL; +} + void describe(size_t x, size_t y, int verbose) { - switch (map[y][x].c) { - case 'E': - displayinfoline("A bookshelf."); - break; - case 'O': - displayinfoline("A portal machine."); - break; - case 'L': - displayinfoline("An elevator."); - break; - case '?': - case '>': - case '<': - case '0': + char *name; + + if (map[y][x].nitems) { if (*map[y][x].items[0]->username) { - displayinfoline("'%s'.", map[y][x].items[0]->username); + name = map[y][x].items[0]->username; } else { itemuri(map[y][x].items[0], bufout2, sizeof(bufout2)); - displayinfoline("'%s'.", bufout2); + name = bufout2; } - break; - default: - if (verbose) { - switch (map[y][x].c) { - case '.': - displayinfoline("Floor."); - break; - case '#': - displayinfoline("Wall."); - break; - } - } else { - displayinfoline(""); - } - break; + } else { + name = NULL; } + if (map[y][x].tile->flags & Important || verbose) + displayinfoline(map[y][x].tile->description, name); + else + displayinfoline(""); +} + +void +dungeondraw(void) +{ + putp(tparm(change_scroll_region, 0, lines-3, 0, 0, 0, 0, 0, 0, 0)); + putp(tparm(clear_screen, 0, 0, 0, 0, 0, 0, 0, 0, 0)); + + rendermap(); + + putp(tparm(cursor_address, py - oy, px - ox, 0, 0, 0, 0, 0, 0, 0)); + putchar('@'); + putp(tparm(cursor_address, py - oy, px - ox, 0, 0, 0, 0, 0, 0, 0)); + + if (curentry->entry != curentry) { + displaybar(curentry->username); + } else { + itemuri(curentry, bufout, sizeof(bufout)); + displaybar(bufout); + } + + describe(px, py, 0); } void move(ssize_t dx, ssize_t dy) { + ssize_t i; size_t x, y; + size_t noy, nox; + + if ((ssize_t)py + dy >= MAPHEIGHT || (ssize_t)py + dy < 0) + return; + if ((ssize_t)px + dx >= MAPWIDTH || (ssize_t)px + dx < 0) + return; - /* allow wraparound of the world for the lulz, even if it's not happening */ - y = (MAPHEIGHT + py + dy) % MAPHEIGHT; - x = (MAPWIDTH + px + dx) % MAPWIDTH; + x = px + dx; + y = py + dy; - if (map[y][x].c == '#') + if (map[y][x].tile->flags & Blocks) return; - putp(tparm(cursor_address, py, px, 0, 0, 0, 0, 0, 0, 0 )); - putchar(map[py][px].c); + if (dx) { + if (x < columns / 2 || MAPWIDTH <= columns) + nox = 0; + else if (x >= MAPWIDTH - columns / 2 - 1) + nox = MAPWIDTH - columns; + else + nox = x - columns / 2; + + if (ox != nox) { + putp(tparm(cursor_address, 0, 0, 0, 0, 0, 0, 0, 0, 0)); + rendermap(); + } else { + putp(tparm(cursor_address, py - oy, px - ox, 0, 0, 0, 0, 0, 0, 0)); + rendermapchar(py, px); + } + } else if (dy) { + putp(tparm(cursor_address, py - oy, px - ox, 0, 0, 0, 0, 0, 0, 0)); + rendermapchar(py, px); + + if (y < maplines / 2 || MAPHEIGHT <= maplines) { + noy = 0; + } else if (y >= MAPHEIGHT - maplines / 2 - 1) { + noy = MAPHEIGHT - maplines; + } else { + noy = y - maplines / 2; + } + + if (noy < oy) { + putp(tparm(cursor_address, 0, 0, 0, 0, 0, 0, 0, 0, 0)); + for (i = (ssize_t)oy - 1; i >= (ssize_t)noy; i--) { + putp(tparm(scroll_reverse, 0, 0, 0, 0, 0, 0, 0, 0, 0)); + rendermapline(i); + putp(tparm(column_address, 0, 0, 0, 0, 0, 0, 0, 0, 0)); + } + } else if (noy > oy) { + putp(tparm(cursor_address, lines-3, 0, 0, 0, 0, 0, 0, 0, 0)); + for (i = oy + 1; i <= noy; i++) { + putp(tparm(scroll_forward, 0, 0, 0, 0, 0, 0, 0, 0, 0)); + rendermapline(i + maplines - 1); + putp(tparm(column_address, 0, 0, 0, 0, 0, 0, 0, 0, 0)); + } + } + oy = noy; + } py = y; px = x; - putp(tparm(cursor_address, py, px, 0, 0, 0, 0, 0, 0, 0 )); + putp(tparm(cursor_address, py - oy, px - ox, 0, 0, 0, 0, 0, 0, 0)); putchar('@'); - putp(tparm(cursor_address, py, px, 0, 0, 0, 0, 0, 0, 0 )); + putp(tparm(cursor_address, py - oy, px - ox, 0, 0, 0, 0, 0, 0, 0)); - describe(x, y, 0); - putp(tparm(cursor_address, py, px, 0, 0, 0, 0, 0, 0, 0 )); + describe(px, py, 0); } void drawscreen(void) { - Dir *dir; - - putp(tparm(clear_screen, 0, 0, 0, 0, 0, 0, 0, 0, 0)); - rendermap(); - - if (curentry->entry != curentry && (dir = curentry->entry->dat)) { - displaybar(dir->items[dir->curline].username); - } else { - itemuri(curentry, bufout, sizeof(bufout)); - displaybar(bufout); + switch (screen) { + case DungeonScreen: + dungeondraw(); + break; + case MenuScreen: + menudraw(); + break; } - - move(0, 0); - + fflush(stdout); } void uidisplay(Item *entry) { - if (!entry || entry->type != '1') + if (!entry || !(entry->type == '1' || entry->type == '+' || entry->type == '7')) return; - generatemap(entry, curentry != entry); + if (entry != curentry) { + generatemap(entry, curentry); + curentry = entry; + } - curentry = entry; drawscreen(); } -void -lookmode(void) -{ - size_t x, y; - - x = px; - y = py; - - for (;;) { - switch (getchar()) { - case 0x1B: - case 'q': - putp(tparm(cursor_address, py, px, 0, 0, 0, 0, 0, 0, 0)); - return; - case 'h': - x = (MAPWIDTH + x - 1) % MAPWIDTH; - break; - case 'j': - y = (y + 1) % MAPHEIGHT; - break; - case 'k': - y = (MAPHEIGHT + y - 1) % MAPHEIGHT; - break; - case 'l': - x = (x + 1) % MAPWIDTH; - break; - } - putp(tparm(cursor_address, y, x, 0, 0, 0, 0, 0, 0, 0)); - describe(x, y, 1); - } -} - Item * uiselectitem(Item *entry) { - Dir *dir; Item *e; - size_t i; - if (!entry || !(dir = entry->dat)) + if (!entry || !entry->dat) return NULL; for (;;) { - switch (getchar()) { + switch (mygetchar()) { case 'h': move(-1, 0); break; @@ -899,40 +1400,20 @@ uiselectitem(Item *entry) case 'l': move(1, 0); break; - case 'L': - lookmode(); - break; case ' ': - /* Portals, stairs, bookshelfs */ - if (e = interact(entry)) { - if (e->type == '1') { - for (i = 0; i < dir->nitems; i++) { - if (e == &dir->items[i]) { - dir->curline = i; - break; - } - } - } + if (e = interact(entry)) return e; - } break; case 'q': + case 0x1b: return NULL; } + fflush(stdout); } } void uisigwinch(int signal) { - Dir *dir; - - if (termset == OK) - del_curterm(cur_term); - termset = setupterm(NULL, 1, NULL); - - if (!curentry || !(dir = curentry->dat)) - return; - - uidisplay(curentry); + sigwinch = 1; } diff --git a/ui_rogue_notes b/ui_rogue_notes @@ -0,0 +1,31 @@ +# Notes +- in need for a big gopher directory? use gopher://bitreich.org/1/memecache/ + +# Bugs +- not all corridors have 2 doors + +# Future Features? +- k-medoids clustering using selector strings in reasonable time... + - items with similar selectors should get inserted into the same rooms + - one could use some mapping (sammon mapping?) to place the rooms based on the cluster medoids + - similar clusters are near together +- examine mode + - allow the cursor to move freely and describe the tiles with describe() in verbose mode +- running using H, J, K, L + - go into the direction until the floor tile changes? + - with wall sliding to go through corridors + - needs to stop at junctions +- spells + - teleport to a random room on the current level + - teleport to a random already visited level + - inscribe a scroll to teleport later back to the current level +- visibility thingies to declutter things + - maybe like hack: keep things visible after discovery + - and what's with persisting this information? + - only show the content of the current room +- better dungeon generation + - analyse things to create clever corridors +- different dungeon themes + - so basically an extended dungeontype thing, with different dungeon generation algorithms + - maybe some special layouts for root directories (empty selector) + - i look at you, ultima castle generator! https://slash.itch.io/ultima-castle-generator diff --git a/ui_rogue_readme b/ui_rogue_readme @@ -0,0 +1,28 @@ +# Description +This UI module (?) for sacc displays directories like rogue/hack dungeon levels. + +The code is ugly. It's obvious that I (pazz0) don't know what I do. + +# Compiling +Copy ui_rogue.c in the sacc source directory. +Go in the sacc source directory and execute blindly the following commands: +``` +make clean && make UI=rogue +``` +sacc will now have the much more confusing ui_rogue interface. + +# Key mapping +h, j, k, l: left, down, up, right +space: interact (in dungeon), select (in menu) +ESC or q: exit (in dungeon), close menu + +# Map explanation +E or ?: bookshelf/book (gopher type '0') +L or >: elevator/staircase down/staircase up (gopher type '1' on the current server) +O or 0: portal machine/portal (gopher type '1' on another server) +%: heap of stuff (other non-handled gopher types, apart from 'i' and '3') +standout (inverse) 0 or <: a way to go back to the previous gopher directory + +. or #: floor +/: doors (behaves like floor) +| or -: wall