ludic/packages/ludic.rpg/puzzles.ludic
Orkuncakilkaya bf36bc8a8f refactor(runtime,packages,examples): named constants, package enums, idiom sweep
- runtime: HEADLESS_FRAME_PATH, STICK_DEADZONE / STICK_LEFT_X/Y, key and
  byte codes as char literals throughout (`k == 'w'`, `fill(rt_map, ' ', …)`)
- ludic.gameplay/stats: drop the duplicate `stat_field` (it answered "atk"
  for every build stat); Stats.base uses stats_field_name
- ludic.shooter: compare aim modes and fire patterns with AimMode.* and
  WeaponPattern.* instead of raw ints; STICK_RIGHT_X/Y
- ludic.npcai: DecisionMade / brain_set_state use AiState.*
- examples/games/menu.ludic uses Font.load / Ui.* with FONT_PATH and
  BACKDROP named; strings.ludic header says what it prints
- whole tree: `x = x + 1` → `x += 1` (single-term right-hand sides only),
  `0 - x` → `-x`, ASCII codes → char literals; every .ludic and every
  ```ludic fence reformatted with the fixed formatter (whitespace only)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-05 01:12:26 +03:00

152 lines
5.5 KiB
Text

# puzzles.ludic — RPG Module F: the recurring 2D puzzle primitives (#59).
#
# Sokoban pushables on the grid, plus a signal graph of switches / pressure plates
# driving gates — logic puzzles with no code: wire a Gate to a source and the gate
# opens when the source is on. Composes with the grid Mover (Module A).
const PZ_Q: int = 65536
property Pushable { }
property Switch { on: int = 0 }
property Pressure { on: int = 0, step: int = 16 }
property Gate { open: int = 0, src: int = -1 }
event BlockPushed { block: int = 0, dx: int = 0, dy: int = 0 }
event cancellable PushBlocked { block: int = 0 }
event SignalChanged { gate: int = 0, open: int = 0 }
function pz_pos(e: int, name: pointer) -> int {
let P = World.prop_id("Position"); if P < 0 { return 0 }
return World.get(e, P, World.field_id(P, name))
}
function pz_pos_set(e: int, name: pointer, v: int) -> void {
let P = World.prop_id("Position"); if P < 0 { return }
World.set(e, P, World.field_id(P, name), v)
}
# the pushable whose cell is exactly (x, y), or -1.
function pz_block_at(x: int, y: int) -> int {
let P = World.prop_id("Pushable")
if P < 0 { return -1 }
var e = World.query_next(P, 0)
while e >= 0 {
if (pz_pos(e, "x") == x) and (pz_pos(e, "y") == y) { return e }
e = World.query_next(P, e + 1)
}
return -1
}
# reuse the Mover module's tilemap walkability check when present.
function pz_walkable(nx: int, ny: int, step: int) -> int {
let ps = World.prop_id("Solids")
if ps < 0 { return 1 }
let se = World.query_next(ps, 0)
if se < 0 { return 1 }
let ft = World.field_id(ps, "tile")
let fw = World.field_id(ps, "wall")
if (ft < 0) or (fw < 0) { return 1 }
let ts = World.get(se, ps, ft)
if ts <= 0 { return 1 }
let wall = World.get(se, ps, fw)
if Grid.blocked(nx / ts, ny / ts, wall) { return 0 }
return 1
}
# Puzzle.push(bx, by, dx, dy, step) — push the block at (bx,by) one cell along
# (dx,dy) if the destination is free (no wall, no other block). Returns 1 on a move.
@Namespace(Puzzle) function puzzle_push(bx: int, by: int, dx: int, dy: int, step: int) -> int {
let b = pz_block_at(bx, by)
if b < 0 { return 0 }
let nx = bx + dx * step
let ny = by + dy * step
if (pz_walkable(nx, ny, step) == 0) or (pz_block_at(nx, ny) >= 0) {
emit PushBlocked(block: b)
return 0
}
pz_pos_set(b, "x", nx); pz_pos_set(b, "y", ny)
emit BlockPushed(block: b, dx: dx, dy: dy)
return 1
}
# Puzzle.link(gate, source) — a gate opens when its source (a Switch or Pressure) is on.
@Namespace(Puzzle) function puzzle_link(gate: int, source: int) -> void {
let P = World.prop_id("Gate")
if P < 0 { return }
World.set(gate, P, World.field_id(P, "src"), source)
}
# Puzzle.switch(e, on) — set a manual switch.
@Namespace(Puzzle) function puzzle_switch(e: int, on: int) -> void {
let P = World.prop_id("Switch")
if P < 0 { return }
World.set(e, P, World.field_id(P, "on"), on)
}
@Namespace(Puzzle) function puzzle_is_open(gate: int) -> int {
let P = World.prop_id("Gate")
if P < 0 { return 0 }
return World.get(gate, P, World.field_id(P, "open"))
}
# read the "on" state of a signal source (Switch or Pressure), 0 if neither.
function pz_source_on(src: int) -> int {
if src < 0 { return 0 }
let PS = World.prop_id("Switch")
if (PS >= 0) and (World.has(src, PS) != 0) { return World.get(src, PS, World.field_id(PS, "on")) }
let PP = World.prop_id("Pressure")
if (PP >= 0) and (World.has(src, PP) != 0) { return World.get(src, PP, World.field_id(PP, "on")) }
return 0
}
# ===========================================================================
# esys_pressure (Update) — a plate is on while a Pushable or a Position-carrying
# body sits on its cell.
# ===========================================================================
@EngineSystem(Pressure, Update) function esys_pressure() -> void {
let P = World.prop_id("Pressure")
if P < 0 { return }
let fon = World.field_id(P, "on")
let fstep = World.field_id(P, "step")
var e = World.query_next(P, 0)
while e >= 0 {
let px = pz_pos(e, "x"); let py = pz_pos(e, "y")
let step = World.get(e, P, fstep)
var on = 0
# a pushable on the plate?
if pz_block_at(px, py) >= 0 { on = 1 }
# or any other Position entity within the same cell?
if on == 0 {
let PP = World.prop_id("Position")
var o = World.query_next(PP, 0)
while o >= 0 {
if (o != e) and (World.has(o, P) == 0) {
let ox = World.get(o, PP, World.field_id(PP, "x"))
let oy = World.get(o, PP, World.field_id(PP, "y"))
var adx = ox - px; if adx < 0 { adx = -adx }
var ady = oy - py; if ady < 0 { ady = -ady }
if (adx < step) and (ady < step) { on = 1; o = -1 }
}
if o >= 0 { o = World.query_next(PP, o + 1) }
}
}
World.set(e, P, fon, on)
e = World.query_next(P, e + 1)
}
}
# ===========================================================================
# esys_gate (Update, after pressure) — a gate mirrors its source's on-state.
# ===========================================================================
@EngineSystem(Gate, Update) function esys_gate() -> void {
let P = World.prop_id("Gate")
if P < 0 { return }
let fopen = World.field_id(P, "open")
let fsrc = World.field_id(P, "src")
var e = World.query_next(P, 0)
while e >= 0 {
let want = pz_source_on(World.get(e, P, fsrc))
if World.get(e, P, fopen) != want {
World.set(e, P, fopen, want)
emit SignalChanged(gate: e, open: want)
}
e = World.query_next(P, e + 1)
}
}