]> granicus.if.org Git - nethack/commitdiff
Fix: possible themed room shop where shopkeeper permanently blocks entry
authorcopperwater <aosdict@gmail.com>
Sat, 1 Oct 2022 15:21:50 +0000 (11:21 -0400)
committerPatR <rankin@nethack.org>
Sat, 8 Oct 2022 23:53:53 +0000 (16:53 -0700)
Reported by every for xNetHack. This bug is latent in vanilla, but can
easily start to present itself if themed rooms of a certain shape are
added. Ultimately, it comes from an assumption that shops will always be
rectangles of at least size 2x2, and the shopkeeper will always be able
to step diagonally backwards from their normal position just inside the
door in order to get out of the player's way.

Themed rooms introduce the possibility of shops where the shopkeeper has
only 1 square adjacent to their normal position to move to -
effectively, the shop entrance is a narrow corridor. When this happens,
they have nowhere to go to allow the player to enter or leave the shop,
leaving it permanently blocked unless the hero teleports or falls in or
out.

This fixes that by adjusting the shop algorithm to detect when a shop
candidate room is set up like this, and excludes it from becoming a
shop.

src/mkroom.c

index 112e1e788813f1ef777dc80b188e91489919a700..5389e02d07f412a306066c4beaf504d32b84e68a 100644 (file)
@@ -27,6 +27,7 @@ static struct permonst *morguemon(void);
 static struct permonst *squadmon(void);
 static void save_room(NHFILE *, struct mkroom *);
 static void rest_room(NHFILE *, struct mkroom *);
+static boolean invalid_shop_shape(struct mkroom *sroom);
 
 #define sq(x) ((x) * (x))
 
@@ -150,6 +151,10 @@ mkshop(void)
 
  gottype:
     for (sroom = &g.rooms[0];; sroom++) {
+        /* return from this loop: cannot find any eligible room to be a shop
+         * continue: sroom is ineligible
+         * break: sroom is eligible
+         */
         if (sroom->hx < 0)
             return;
         if (sroom - g.rooms >= g.nroom) {
@@ -160,8 +165,12 @@ mkshop(void)
             continue;
         if (has_dnstairs(sroom) || has_upstairs(sroom))
             continue;
-        if (sroom->doorct == 1 || (wizard && ep && sroom->doorct != 0))
-            break;
+        if (sroom->doorct == 1 || (wizard && ep && sroom->doorct != 0)) {
+            if (invalid_shop_shape(sroom))
+                continue;
+            else
+                break;
+        }
     }
     if (!sroom->rlit) {
         coordxy x, y;
@@ -1009,4 +1018,70 @@ cmap_to_type(int sym)
     return typ;
 }
 
+/* With the introduction of themed rooms, there are certain room shapes that may
+ * generate a door, the square just inside the door, and only one other ROOM
+ * square touching that one. E.g.
+ *   ---
+ * ---..
+ * +....
+ * ---..
+ *   ---
+ * This means that if the room becomes a shop, the shopkeeper will move between
+ * those two squares nearest the door without ever allowing the player to get
+ * past them.
+ * Before approving sroom as a shop, check for this circumstance, and if it
+ * exists, don't consider it as valid for a shop.
+ *
+ * Note that the invalidity of the shape derives from the position of its door
+ * already being chosen. It's quite possible that if the door were somewhere
+ * else on the perimeter of this room, it would work fine as a shop.*/
+static boolean
+invalid_shop_shape(struct mkroom *sroom)
+{
+    coordxy x, y;
+    coordxy doorx = g.doors[sroom->fdoor].x;
+    coordxy doory = g.doors[sroom->fdoor].y;
+    coordxy insidex, insidey, insidect = 0;
+
+    /* First, identify squares inside the room and next to the door. */
+    for (x = max(doorx - 1, sroom->lx);
+         x <= min(doorx + 1, sroom->hx); x++) {
+        for (y = max(doory - 1, sroom->ly);
+             y <= min(doory + 1, sroom->hy); y++) {
+            if (levl[x][y].typ == ROOM) {
+                insidex = x;
+                insidey = y;
+                insidect++;
+            }
+        }
+    }
+    if (insidect < 1) {
+        impossible("invalid_shop_shape: no squares inside door?");
+        return TRUE;
+    }
+    /* if insidect > 1, then the shopkeeper already has alternate
+     * squares to move to so we don't need to check further. */
+    if (insidect == 1) {
+        /* But if it is 1, scan all adjacent squares for other squares
+         * that are part of this room. */
+        insidect = 0;
+        for (x = max(insidex - 1, sroom->lx);
+             x <= min(insidex + 1, sroom->hx); x++) {
+            for (y = max(insidey - 1, sroom->ly);
+                 y <= min(insidey + 1, sroom->hy); y++) {
+                if (x == insidex && y == insidey)
+                    continue;
+                if (levl[x][y].typ == ROOM)
+                    insidect++;
+            }
+        }
+        if (insidect == 1) {
+            /* shopkeeper standing just inside the door can only move
+             * to one other square; this cannot be a shop. */
+            return TRUE;
+        }
+    }
+    return FALSE;
+}
+
 /*mkroom.c*/