Add rolling boulder traps in Sokoban to reduce wrist strain

When playtesting NetHack heavily, I observed that most of the time
it wasn't placing much strain on my wrists, but Sokoban was an
exception: travel, farmove, etc. can normally be used to avoid the
need to spam keys, but they don't work while pushing a boulder, and
the boulders often need to be pushed along precise routes, so you
have to tap out every movement. This becomes particularly straining
when pushing in the last few boulders, as you have to push them a
long way along the goal corridor.

This commit adds rolling boulder traps to Sokoban that will
automatically roll boulders along the goal corridor, meaning that
you don't have to push them there manually. This considerably
reduces the number of keystrokes needed to solve Sokoban, without
making any significant change to the difficulty of the levels.
Some of the designs had to change slightly in order to make room
for them, but not in a way that meaningfully changes the solution.
This commit is contained in:
Alex Smith
2026-04-14 12:47:13 +01:00
parent bc94d7a355
commit acfbd6d0e4
9 changed files with 47 additions and 30 deletions

View File

@@ -60,9 +60,10 @@ des.object("boulder", 09, 12);
des.object("boulder", 03, 14);
-- prevent monster generation over the (filled) holes
des.exclusion({ type = "monster-generation", region = { 08,01, 23,01 } });
des.exclusion({ type = "monster-generation", region = { 07,01, 23,01 } });
-- Traps
des.trap("hole", 08, 01);
des.trap("hole", 07, 01);
des.trap("rolling boulder", 08, 01);
des.trap("hole", 09, 01);
des.trap("hole", 10, 01);
des.trap("hole", 11, 01);

View File

@@ -61,9 +61,9 @@ des.object("boulder",12,09);
des.object("boulder",11,10);
-- prevent monster generation over the (filled) holes
des.exclusion({ type = "monster-generation", region = { 05,01, 22,01 } });
des.exclusion({ type = "monster-generation", region = { 05,01, 23,01 } });
-- Traps
des.trap("hole",05,01)
des.trap("rolling boulder",05,01)
des.trap("hole",06,01)
des.trap("hole",07,01)
des.trap("hole",08,01)
@@ -81,6 +81,7 @@ des.trap("hole",19,01)
des.trap("hole",20,01)
des.trap("hole",21,01)
des.trap("hole",22,01)
des.trap("hole",23,01)
des.monster({ id = "giant mimic", appear_as = "obj:boulder" });
des.monster({ id = "giant mimic", appear_as = "obj:boulder" });

View File

@@ -49,6 +49,7 @@ des.object("boulder",06,06)
-- prevent monster generation over the (filled) holes
des.exclusion({ type = "monster-generation", region = { 07,09, 18,09 } });
-- Traps
des.trap("rolling boulder",07,09)
des.trap("hole",08,09)
des.trap("hole",09,09)
des.trap("hole",10,09)

View File

@@ -7,26 +7,27 @@ des.level_init({ style = "solidfill", fg = " " });
des.level_flags("mazelevel", "noteleport", "premapped", "sokoban", "solidify");
des.map([[
--------
--|.|....|
|........|----------
|.-...-..|.|.......|
|...-......|.......|
|.-....|...|.......|
|....-.--.-|.......|
|..........|.......|
|.--...|...|.......|
|....-.|---|.......|
--|....|----------+|
|................|
------------------
--------
--|.|....|
|........|----------
|.-...-..|.|.......|
|...-......|.......|
|.-....|...|.......|
|....-.--.-|.......|
|..........|.......|
|.--...|...|.......---
|....-.|---|.......+.|
--|....|------------.|
|................+.|
--------------------
]]);
des.stair("down", 06,11)
des.stair("up", 15,06)
des.door("locked",18,10)
des.region(selection.area(00,00,19,12), "lit");
des.non_diggable(selection.area(00,00,19,12));
des.non_passwall(selection.area(00,00,19,12));
des.door("locked",19,09)
des.door("locked",19,11)
des.region(selection.area(00,00,21,12), "lit");
des.non_diggable(selection.area(00,00,21,12));
des.non_passwall(selection.area(00,00,21,12));
-- Boulders
des.object("boulder",04,02)
@@ -49,7 +50,7 @@ des.object("boulder",05,11)
-- prevent monster generation over the (filled) holes
des.exclusion({ type = "monster-generation", region = { 06,11, 18,11 } });
-- Traps
des.trap("hole",07,11)
des.trap("rolling boulder",07,11)
des.trap("hole",08,11)
des.trap("hole",09,11)
des.trap("hole",10,11)
@@ -60,6 +61,7 @@ des.trap("hole",14,11)
des.trap("hole",15,11)
des.trap("hole",16,11)
des.trap("hole",17,11)
des.trap("hole",18,11)
-- Random objects
des.object({ class = "%" });

View File

@@ -55,6 +55,7 @@ des.object("boulder",10,10)
-- prevent monster generation over the (filled) holes
des.exclusion({ type = "monster-generation", region = { 11,10, 27,10 } });
-- Traps
des.trap("rolling boulder",11,10)
des.trap("hole",12,10)
des.trap("hole",13,10)
des.trap("hole",14,10)

View File

@@ -48,8 +48,9 @@ des.object("boulder",10,10)
des.object("boulder",03,11)
-- prevent monster generation over the (filled) holes
des.exclusion({ type = "monster-generation", region = { 12,10, 24,10 } });
des.exclusion({ type = "monster-generation", region = { 11,10, 24,10 } });
-- Traps
des.trap("rolling boulder",11,10)
des.trap("hole",12,10)
des.trap("hole",13,10)
des.trap("hole",14,10)

View File

@@ -74,16 +74,21 @@ des.object("boulder",10,10)
-- prevent monster generation over the (filled) pits
des.exclusion({ type = "monster-generation", region = { 01,06, 07,11 } });
-- Traps
des.trap("pit",03,06)
des.trap("pit",04,06)
des.trap("pit",05,06)
des.trap("pit",02,06)
des.trap("pit",02,07)
des.trap("pit",02,08)
des.trap("pit",02,09)
des.trap("rolling boulder",02,09)
des.trap("pit",02,10)
des.trap("pit",03,10)
des.trap("pit",04,10)
des.trap("pit",05,10)
des.trap("pit",06,10)
des.trap("pit",07,10)
des.trap("rolling boulder",07,10)
-- A little help
des.object("scroll of earth",02,11)

View File

@@ -50,11 +50,14 @@ des.trap("pit",01,03)
des.trap("pit",01,04)
des.trap("pit",01,05)
des.trap("pit",01,06)
des.trap("pit",01,07)
des.trap("rolling boulder",01,07)
des.trap("pit",01,08)
des.trap("pit",02,08)
des.trap("pit",03,08)
des.trap("pit",04,08)
des.trap("pit",05,08)
des.trap("pit",06,08)
des.trap("rolling boulder",06,08)
-- A little help
des.object("scroll of earth",01,09)

View File

@@ -2820,6 +2820,8 @@ immune_to_trap(struct monst *mon, unsigned ttype)
hanging to the ceiling */
if (Sokoban && (is_pit(ttype) || is_hole(ttype)))
return TRAP_NOT_IMMUNE;
if (In_sokoban(&u.uz) && ttype == ROLLING_BOULDER_TRAP)
return TRAP_CLEARLY_IMMUNE; /* not dangerous in Sokoban */
if (is_floater(pm) || is_flyer(pm)
|| (is_clinger(pm) && has_ceiling(&u.uz)))
return TRAP_CLEARLY_IMMUNE;
@@ -3589,7 +3591,7 @@ find_random_launch_coord(struct trap *ttmp, coord *cc)
coordxy dx, dy;
coordxy x, y;
if (!ttmp || !cc)
if (!ttmp || !cc || Sokoban)
return FALSE;
x = ttmp->tx;