From acfbd6d0e4ae3112b66a1564943228f429413441 Mon Sep 17 00:00:00 2001 From: Alex Smith Date: Tue, 14 Apr 2026 12:47:13 +0100 Subject: [PATCH] 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. --- dat/soko1-1.lua | 5 +++-- dat/soko1-2.lua | 5 +++-- dat/soko2-1.lua | 1 + dat/soko2-2.lua | 38 ++++++++++++++++++++------------------ dat/soko3-1.lua | 1 + dat/soko3-2.lua | 3 ++- dat/soko4-1.lua | 13 +++++++++---- dat/soko4-2.lua | 7 +++++-- src/trap.c | 4 +++- 9 files changed, 47 insertions(+), 30 deletions(-) diff --git a/dat/soko1-1.lua b/dat/soko1-1.lua index ba5a9dfd7..ac9bcdf31 100644 --- a/dat/soko1-1.lua +++ b/dat/soko1-1.lua @@ -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); diff --git a/dat/soko1-2.lua b/dat/soko1-2.lua index 2ff3a9feb..36ab90721 100644 --- a/dat/soko1-2.lua +++ b/dat/soko1-2.lua @@ -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" }); diff --git a/dat/soko2-1.lua b/dat/soko2-1.lua index eb66e0fbf..e8b56efef 100644 --- a/dat/soko2-1.lua +++ b/dat/soko2-1.lua @@ -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) diff --git a/dat/soko2-2.lua b/dat/soko2-2.lua index 1302a1b5e..f11702c1e 100644 --- a/dat/soko2-2.lua +++ b/dat/soko2-2.lua @@ -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 = "%" }); diff --git a/dat/soko3-1.lua b/dat/soko3-1.lua index 2515ffca1..1c7715858 100644 --- a/dat/soko3-1.lua +++ b/dat/soko3-1.lua @@ -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) diff --git a/dat/soko3-2.lua b/dat/soko3-2.lua index 1015125d6..24236efe2 100644 --- a/dat/soko3-2.lua +++ b/dat/soko3-2.lua @@ -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) diff --git a/dat/soko4-1.lua b/dat/soko4-1.lua index 4c8f6d162..3768c1cf7 100644 --- a/dat/soko4-1.lua +++ b/dat/soko4-1.lua @@ -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) diff --git a/dat/soko4-2.lua b/dat/soko4-2.lua index de749ad37..d0606dc6a 100644 --- a/dat/soko4-2.lua +++ b/dat/soko4-2.lua @@ -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) diff --git a/src/trap.c b/src/trap.c index e44da9133..ce00d3403 100644 --- a/src/trap.c +++ b/src/trap.c @@ -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;