Skip to content

About

Resurrection and improvement of the original Java Hammerwatch roguelike dungeon generator on the original Hammerwatch forum from https://web.archive.org/web/20191207045849/http://hammerwatch.com/forum/index.php?topic=1658.30

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Hammerwatch Rogue-like Dungeon Generator

CI

A cross-platform desktop app (Windows / Linux / macOS) that generates random rogue-like campaigns for Hammerwatch. It is a remake of the terminal-only Java tool posted on the Hammerwatch forums (archived thread), rebuilt with Electron + React + Vite + TypeScript, with a GUI, a live map preview, and parameter validation so bad settings can no longer crash or hang the generator.

Credit for the original generator and its algorithm goes to the author of the forum tool; this project ports the (slightly modified, expanded-monster-roster) Java source found in reference/original-java/.

What it does

The app generates a multi-level campaign and produces exactly the folder the Hammerwatch editor's LevelPacker.exe expects:

dungeon<seed>/
├── info.xml            campaign name & lives
├── levels.xml          the act/level list
├── levels/            one file per slot, numbered in play order (Floor order tab)
│   ├── 01_lobby1.xml       optional shop room, placed anywhere in the order
│   ├── 02_floor1_boss.xml  a dungeon floor (_boss when it hosts a floor boss)
│   ├── 03_floor2.xml
│   ├── …
│   └── 10_arena_boss1.xml  optional arena (arena_survivalN for a survival round);
│                           the last slot holds the reward orb
└── tweak/
    └── shared.xml      optional player-balance overrides

The lobbies, the boss finale and the player tweaks are all optional; with them off you get exactly the classic dungeon-only campaign the original tool made.

If you point the app at your Hammerwatch install, it will write that folder into <Hammerwatch>/editor/, run LevelPacker.exe on it, and move the resulting .hwm into <Hammerwatch>/levels/ — after which the campaign shows up in the game's level list. You can also export the folder (or a zip) and pack it yourself.

How generation works

For each floor, seeded by the campaign seed (same seed → same dungeon):

  1. Rooms — between minRoomCount and maxRoomCount rectangular rooms are placed at random positions/sizes, rejecting overlaps (roomPadding apart, edgePadding from the map border).
  2. Passages — rooms are connected spanning-tree style: a random unconnected room is linked to a random connected one with a straight or L-shaped corridor of random width, rejecting corridors that would cut through other rooms or corridors.
  3. Special rooms — one room becomes the entrance (stairs up prefab), one the exit (stairs down) — or, on the final floor, the victory orb. A shop, a locked treasure vault, an extra locked room and a matching key each appear with their configured chance. Every remaining room becomes a monster lair.
  4. Population — each lair picks a monster type from that floor's pool and spawns a horde (about max/5…max of that type, times monsterMultiplier) scattered in drifting clusters, plus spawners, treasure piles, breakables and food; multipliers scale the amounts.
  5. Walls — rooms and corridors are rasterized into a wall/floor grid, and a 3×5 pattern matcher picks the right themed wall piece (corner, T-piece, cap, cross…) for every wall tile.
  6. Reachability — the finished grid is flood-filled and the floor is thrown away unless the player can actually walk from the entrance to the exit (or orb) and to every key. Floor in the tilemap is not enough: themed wall sprites are three tiles tall, so the two rows beneath any wall are dead space, and a corridor that meets a room only inside that band is sealed in game while looking open on the map. About 6% of first rolls are discarded here and re-rolled.
  7. XML — the tile grid (in 20×20 blocks), doodads, actors, items and script nodes (level start/exit triggers, shop area, end-game trigger) are serialized into Hammerwatch's level XML dialect.

If a floor can't be assembled (e.g. the rooms won't fit), the app retries with new rolls a bounded number of times and then reports a friendly error — the original tool would retry forever or crash; this remake validates parameters up front and keeps every retry loop bounded.

The optional levels

Five additions sit outside the floor loop above. None of them draw from the dungeon's RNG stream — the arena draws from a third seeded stream of its own, the lobbies, tweaks and timer mode draw nothing — so turning any of them on or off leaves a seed's floors byte-identical (timer mode appends script nodes to the floors it arms, but moves nothing that was already there). Two lobbies and the boss finale are on by default; timer mode is off.

  • Lobbies — hand-authored shop rooms you can place anywhere in the campaign order, as many as you like: one at the front as a starting hub, one before each boss fight, one halfway through the floors, or none at all. Each has its own preset — the house (the default, after the Anniversary Edition house lobby), sun-bazaar, castle-bridge or sky-shop — its own vendor stalls for the shop columns you pick, its own pile of starting gold, and its own free upgrade pickups (any number of each of the game's eight upgrade items, none by default). Anyone who died on the way in is revived on arrival, so a co-op partner never has to sit out the shopping. A lobby cannot be the campaign's last slot, because it carries no victory orb.
  • Boss finale — one arena per boss fight, and a campaign can chain as many fights as you like. Put a lobby in front of a fight and you get the shopping stop this tool used to weld on automatically. The boss arena is a walled room with one boss, five monster waves — four keyed to the boss's health (100 / 75 / 50 / 25%, switching on and never off) and a fifth that spawns the moment the boss dies — scattered cover pillars, and a sealed alcove holding the victory orb. Killing the boss destroys the alcove seals; the death wave fights you on the walk to the orb, and touching the orb ends the game. Only the campaign's last slot carries the orb, so there is exactly one way to win. With several fights, each gets its own fully independent arena, and beating one leads to whatever the order puts next rather than to the orb — put a lobby between two fights and the run reads fight, shop, fight. Each of the five tiers can also carry arena-wide buffs aimed at players, monsters or both; unlike the spawns, each tier's buffs replace the previous tier's, so only one tier's aura is ever live and the fight reads as phases. A tier can also line the walls with traps — projectile spewers firing straight across the room, chosen from 45 of the game's own projectiles (the 22 that do no damage without a character behind them, plus one that crashes the game, are left out), each with its own spread, fire rate and count. Like the buffs, a tier's traps replace the previous tier's, so the hazard changes with the phase. Traps are off unless you add them.
  • Floor order — the campaign's floors and boss fights can be rearranged from its own tab, between Boss and Player: a row of chips reading 1 2 B1 3 4 B2 that the ◀ ▶ buttons shuffle. A fight can go anywhere — before the first floor, between two floors, or at the end. Floors keep their own order and so do fights, so only the interleaving is up to you and the arrows refuse a move that would break it. Whichever slot ends up last carries the victory orb, so a run can equally well finish on a dungeon floor as in an arena.
  • Player tweaks — tweak/*.xml overrides for class stats, upgrade costs and shop contents, edited per field or through bulk knobs. Purely a balance layer: it draws no random values at all.
  • Buff auras — hang any of the game's 41 buffs on a floor and choose who it catches: players, monsters, or both. Configured per floor and empty everywhere by default. A buff aura covers the whole floor and is live the moment the party arrives — no countdown, and it never switches off. Slow the party with frost, enrage the horde with bloodlust, poison everything, or drain the party's mana the whole way down. A floor may carry several at once, each with its own target, so the same floor can slow the party and hurry the monsters. Every buff in the list explains itself on hover — what it does, how hard, and for how long.
  • Timer mode — optional time pressure, configured per floor and off everywhere by default. Give a floor a countdown and, when it runs out, the whole floor starts damaging the party every few hundred milliseconds until they take the stairs. The damage can be negative, which heals instead — a floor that starts patching you up on a clock. Monsters are never affected. A M:SS countdown ticks down on screen while it runs, and can be switched off if you would rather the deadline stayed a surprise.
  • Mystery buttons — floor plates hidden around a floor, configured on the Dungeon tab's Mystery Buttons sub-tab and off by default. You build a pool of buttons, each of which can spawn loot (chests, bronze/silver/gold coins, four sizes of diamond, upgrades, potions) beside the plate, spawn monsters a few tiles out, turn the walls of its room into spewers (optionally for a limited time), announce a message, or do nothing at all. Then, per floor, you choose how many plates to hide and which pool buttons they may be, each with a weight. Every plate looks the same, so the party only finds out by stepping on one. Buttons use a seeded stream of their own and are added after the floor is built, so arming a floor never changes its layout.

Verified in game

The generated campaign has been played end to end in Hammerwatch 1.41 — a full run finished on the boss arena's orb (YOU WIN!!), and separately with the boss disabled, finishing on the dungeon's own orb room. Several facts about the game's level format were only discoverable that way and are recorded in .claude/skills/hammerwatch-modding/references/, notably that tilemap block origins must be multiples of 20 (the engine snaps them), that themed wall sprites are three tiles tall and overhang downward (which is why floors are now reachability-checked), that monsters spawned off the engine's Boss Died event really do appear after the kill, and that destroying a wall doodad does not create ground beneath it.

Known issues in co-op

When hosting a generated campaign in Hammerwatch 1.41:

  • Rare host crash deep in a boss fight. This is a Hammerwatch engine issue with networked item pickup, not a fault in the generated campaign. Single-player runs clean. If it happens often, reduce wave counts or disable item drops on the later tiers (see the Boss tab).
  • Dead players who reconnect after a crash stay dead for one floor. They will revive at the next stairs. This is a rare edge case specific to crash-reconnects.
  • Thief DivideByZero crash on held Autofire (1.41, unpatched engine bug). A divide-by-zero in Hammerwatch's own Autofire fires when the Autofire control is held while attacking as the Thief — including on level load. It is a stock 1.41 defect; the reporter hit it in a singleplayer and co-op session. In-game fix only: Options → Controls → P1 → Autofire → clear it, then rebind it once (or delete the <Autofire> key from the game's config.xml) once you're loaded in game. Cost: no hold-to-fire. No generator change reaches this — nothing we emit touches either operand of the divide. See the crash-triage skill.

Using the app

  1. Set your Hammerwatch folder (bottom panel) — the folder containing editor/ and levels/. It's saved for next time.
  2. Optionally start from a preset: Load preset… in the header lists every preset with a description. The Campaign presets tab is grouped by family (only Classic starts open); the Player presets tab holds player builds that replace only the Player tab. The (i) at the left of the header explains every header button, including how parameters.txt and playersettings.txt differ.
  3. Tweak parameters in the left panel, across five tabs — Lobby (the shop rooms and their presets), Dungeon (floors, rooms, monsters), Boss (the arena, its waves and its spawn modes), Floor order (how the lobbies, floors and boss fights interleave) and Player (class stats, upgrade costs, shop contents). Invalid combinations show inline errors and disable the Generate button, with an explanation of what to fix; purely cosmetic caveats show as warnings and still generate.
  4. Optionally enter a seed to reproduce a dungeon; leave blank for random.
  5. Press Generate dungeon and browse the per-floor map preview (rooms are color-coded — entrance, exit, orb, shop, vault, lairs, locks). Re-roll until you like the layout.
  6. Press Install into Hammerwatch (runs LevelPacker automatically), or Export folder… / Export .zip… to pack manually.
  7. In Hammerwatch: the campaign appears in the level list as Dungeon #<seed>.

Note (Linux/macOS): LevelPacker.exe is a Windows tool that ships with the game. On other platforms the app tries wine automatically; without wine, use Export folder and run the packer on a Windows machine (or in Proton's prefix).

parameters.txt

The original tool was configured through a parameters.txt file; the GUI replaces it, but the format is still fully supported as a defaults override:

  • Place a parameters.txt in the app's user-data folder (shown below) or next to the app's working directory and it is loaded as the starting parameters on launch. parameters.default.txt in this repo documents the format and the built-in defaults.
  • Import parameters.txt / Export parameters.txt buttons in the header load/save the same format, so configs from the original tool carry over (its old monster keys like maxBats are reported and ignored — the modified roster uses maxBats1 etc.).
  • Import playersettings.txt / Export playersettings.txt (the row below) do the same for the Player tab alone — its player.* lines, same format. Importing one replaces the Player tab and nothing else, so the intended order is parameters.txt (or a preset) first, then a player build on top. A key the file leaves out is stock.

User-data folder: %APPDATA%/hammerwatch-roguelike-dungeon-generator (Windows), ~/.config/hammerwatch-roguelike-dungeon-generator (Linux), ~/Library/Application Support/hammerwatch-roguelike-dungeon-generator (macOS).

AI preset assistant (optional)

A chat icon in the header (left of Load preset…) opens an assistant that turns "4 icy floors, a boss on the last one, lots of traps" into settings. It is strictly opt-in: the icon only opens a setup screen, and the app makes no network request until you pick a provider and send a message (or press Refresh models). To remove the icon, tick Hide the chat icon in its setup screen; the checkbox under Hammerwatch install folder brings it back.

  • Providers (all OpenAI chat-completions compatible): Pollinations (no key, the default; your prompts and current settings go to pollinations.ai, limit 1 message / 15 s), Ollama (local, nothing leaves your machine), Groq, OpenRouter (use a :free model) and Google Gemini (free keys), or any custom OpenAI-compatible URL such as LM Studio.
  • Claude (your own Anthropic API key) gives the best presets but is paid: the Claude API has no free tier, so each message is billed to your account. It defaults to claude-opus-5-5; Refresh models lists the others, such as claude-sonnet-5-5 or claude-haiku-4-5 for cheaper runs.
  • Keys are stored only by the main process in llm.json in the user-data folder, encrypted with the OS keyring (Electron safeStorage); the UI warns when no keyring exists. The renderer is only ever told whether a key exists.
  • How it edits: the model writes parameters.txt lines for just what you asked to change, on top of your current settings. The app parses and validates them like an imported file, asks the model once to fix any errors, and shows the changed keys and remaining errors. Apply is disabled while errors remain, and the toast after applying has an Undo.

Parameters reference

Parameter Default Meaning
levels 8 Number of floors — the stock campaign is seven, then the boss fight, then an eighth "escape floor" played after it
mapWidth, mapHeight 80 × 60 Map size in tiles (multiples of 20 recommended)
minRoomSize, maxRoomSize 6–20 Room width range in tiles (heights roll size+2); max must be ≥ 7 so stairs fit
minRoomCount, maxRoomCount 12–15 Rooms per floor
minPassageWidth, maxPassageWidth 3–6 Corridor width range; max must be ≤ minRoomSize
edgePadding 2 Empty border around the map
roomPadding 2 Minimum gap between rooms
themes a_mixed,b_mixed,c_mixed,d_mixed,e_mixed,f_mixed,g_mixed Tileset per level. Bases: a–d classic, e–g castle, h–i desert (h outdoors, i indoors), plus bonus1–bonus5. Most bases also offer overlay variants that layer a second tileset over the floor (c_tiles, d_carpet, f_frozen, …) and a _mixed variant that varies the surface room by room — that is the default
shopChance 1.0 Chance per floor of a shop
vaultChance 0.3 Chance per floor of a locked treasure vault
lockChance 0.8 Chance per floor of an extra locked room (with a powerup)
keyChance 1.0 Chance per floor that a key spawns for the last lock
lockFloors 6,7 Floors (0-based, comma-separated) whose way out is locked, each optionally floor:buttons (2:3 = three buttons, all needed; each press announces how many remain; 2:0 = no button). A plain floor number has one button. it sits in a dead-end room behind a destructible wall, with a floor button hidden elsewhere on the floor — stepping on it plays the hatch sound and blows the wall open. No key is involved, so a party that hoarded gold keys (or spent this floor's on another gold door) can still finish. A locked floor that would lead on by stairs gets the blue teleport instead; on a boss floor the wall waits for the boss and the button. Empty locks nothing. A legacy lockFinalRoom=1 still imports, as the floors it used to seal
monsterMultiplier 1.0 Scales horde sizes
goldMultiplier 1.1 Scales treasure amounts
foodMultiplier 1.2 Scales health/mana drops
monsters0…N see defaults Monster pool per floor (repeat an id to weight it)
buff0…N absent Buff auras for that floor: <id>:<target> entries separated by |, e.g. buff2=frost:players|bloodlust:monsters. Targets are players, monsters, both; an omitted target means players. Written only for floors that carry one, so a stock file has none
timer0…N absent Timer mode for that floor: `enabled
mysteryButtons absent How many buttons the mystery-button pool holds (no limit). Each pool button i (0-based) has mysteryButton<i>Name, mysteryButton<i>Text (announced on press; no =, <, >, &), mysteryButton<i>Loot (`:
mysteryFloor0…N absent Mystery buttons on that floor: <count>:<pool buttons>, 0-based and repeated to weight, e.g. mysteryFloor2=8:0,0,1,3 hides 8 plates, each twice as likely to be button 0
max<Monster> see defaults Horde-size cap per monster type; 0 disables the type

The optional levels add their own keys. The lobbies, the boss finale and the one stock player tweak are all on by default:

Parameter Default Meaning
lobbies 2 How many shop rooms the campaign has. 0 means none. Every key below carries its lobby index — lobby0Gold is the first lobby's, lobby1Gold the second's. No upper limit, same as levels
lobby<i>Preset house Which room this lobby uses: house, sun-bazaar, castle-bridge or sky-shop
lobby<i>Gold 10000, 20000 Gold paid out in this lobby, a multiple of 500 (one red diamond each). Past the room's floor spots the diamonds simply stack — there is no upper cap beyond a safety limit that stops a typo emitting millions of items
lobby<i>Shops all 21 columns Space-separated shop columns this lobby's stalls sell
lobby<i>Upgrades 0 0 0 0 0 0 0 0 Free upgrade pickups lying on this lobby's floor: eight space-separated counts, in the order damage, defense, health, mana, then the four tier-2 versions. Each kind has one spot on the floor, so a count above one simply stacks there — there is no gameplay cap, only a safety limit. 0 means the kind is not laid out at all
levelOrder L1,1,…,7,L2,B1,8 The order the campaign is played in: floors by their 1-based number, boss fights as B1, B2, …, lobbies as L1, L2, … — e.g. L1,1,2,B1,3 or B1,1,2,3. Each kind keeps its own order; only the interleaving is free. Whichever entry is last holds the victory orb, so a campaign can end on a dungeon floor — but not on a lobby, which carries no orb. Omit for the default order (every lobby, then every floor, then every fight)
boss 1 Append a boss arena to the campaign. Put a lobby<i> in front of it for the shopping stop that used to come welded on
bossFights 1 How many boss fights the campaign chains. Each has its own arena, and every key below carries its fight index — boss0Theme is the first fight's, boss1Theme the second's. No upper limit, same as levels
boss<i>Theme g_mixed Tileset for the arena — any dungeon theme, independent of the floors' (h warns: its cliff art needs a thicker wall band and overlapping corners to stay sealed)
boss<i>FloorPattern random How a _mixed arena theme arranges its floor palette: random, checker, bandsH, bandsV, bandsDiag, rings, diamond, cross, triangle. Ignored by every other theme
boss<i>Width, boss<i>Height 42–64, 42–64 Arena size range in tiles. Bigger is not better: on a much larger floor a scattered wave arrives dispersed and never re-forms, so the horde trickles into the party and gets picked off
boss<i>Pool the 4 castle bosses Comma-separated boss ids out of the seven; the seed picks one per fight
boss<i>Cover symmetric,0.08,4,3 pattern,density,ringSpacing,clusters. Pattern is random/ring/gaussian/symmetric; density is capped at 0.25 — it is the fraction of free floor filled with pillars, and denser than that leaves nowhere to fight
boss<i>Wave1…5 see defaults Waves 1–4 are the health tiers (100/75/50/25%); wave 5 fires when the boss dies, spawning a last stand into the walk to the orb. Each is monsters|defaultIntervalMs|monsterMax|intervalMs|spawnMode, the last three being comma-separated id:value pairs. Tiers switch on and never off, so by 25% all four health tiers are spawning at once. A monster's pool entry may be a variant key (lich#2, slime#0); an empty wave is legal and emits nothing
boss<i>SpawnMode per monster anchors Inside a boss<i>WaveN line. anchors trickles the horde in on a timer from the nine spawn anchors; random / ring / gaussian / symmetric scatter it across the arena and spawn it all at once, ignoring the intervals. A monster whose wreck still blocks movement (the nova/frost/tracking towers) may not be scattered — it could wall the arena off
boss<i>WaveBuff1…5 boss0WaveBuff5=bloodlust:monsters Arena-wide buffs for that tier, <id>:<target> entries separated by |, e.g. boss0WaveBuff3=frost:players|bloodlust:monsters. Its own key rather than a sixth boss<i>WaveN field, so files exported before the feature still round-trip. Tiers replace one another: each threshold switches the previous buff off as it switches its own on, so exactly one is ever live. Tier 1 is live from the moment the fight starts. Written only for tiers that carry one — the stock defaults buff tier 5 only, so the horde that spawns on the kill fights at +50% damage and move speed
boss<i>Spawn 2,4,3,8,1500 spacing,ringSpacing,clusters,batchSize,batchIntervalMs for the scatter modes; separate from boss<i>Cover so pillars and monsters can be spaced differently. batchSize caps how many spawn points one monster entry gets per tier and anything past it trickles in on batchIntervalMs, which is what keeps a big tier off a single frame. The three-field form still parses
boss<i>Invuln 30,30,30 Seconds the boss is immortal each time its health crosses 75%, 50% and 25%. One value sets all three; 0 disables that one threshold; off disables the feature. Stops a fully upgraded party bursting the boss down before the fight happens, and keeps the three thresholds from firing in the same frame — which would switch every wave tier on at once and flood the arena
boss<i>InvulnCountdown 1 Announce a ticking M:SS countdown for the length of each invulnerability window. 0 keeps the windows silent
boss<i>MonsterMultiplier 1.0 Scales every wave's max counts (an endless -1 stays endless). Separate from the dungeon's monsterMultiplier
boss<i>FoodMultiplier 1.2 Scales the arena's health/mana pickups
player.<class>.<group>.<field> — Player balance overrides emitted to tweak/*.xml; only values that differ from stock are written
player.shared.remove.life 1 Ships on by default — removes the repeatable extra-life shop upgrade

Whatever the arena's cover settings, a connectivity pass guarantees the boss, all nine spawn anchors and the alcove stay reachable from the entrance; pillars that would wall something off are pruned.

A pool entry may name a variant rather than a plain type: lich#2 is the elite lich, slime#0 the slime hive, bat1#0 the bats spawner. The bare id always means that type's standard variant, so older configs keep working.

Monster ids include the classic set (bat1, tick1, maggot, slime, skeleton1/2/3, archer1/2/3, eye, wisp1/2, lich), the desert set (mummy_desert, mummy_ranged, guard_desert, guard_desert_range, lich_desert), towers (tower_*), specials (spider, floater_fire, pillar_fire, special_beheaded_kamikaze), bosses (mb_*) and the bonus campaign's weaker variants (bonus_skeleton1, bonus_archer1) — the full list with actor files is in src/generator/objects/monsterTypes.ts.

Project structure

├── src/
│   ├── generator/          Pure TypeScript port of the generator — no
│   │   │                   Electron/DOM/fs imports, fully unit-tested
│   │   ├── core/           Seeded RNG (java.util.Random-compatible) and the
│   │   │                   GenerationContext that replaces the Java statics
│   │   ├── config/         Parameter schema & defaults, parameters.txt
│   │   │                   parser/serializer, validation rules
│   │   ├── xml/            Hammerwatch XML dialect building blocks
│   │   │                   (dictionary/array/int/float/bool/string/int-arr)
│   │   ├── map/            Level assembly: rooms, passages, tile grid,
│   │   │                   wall pattern matching, reachability, floor
│   │   │                   tileset overlays
│   │   ├── objects/        Things placed on levels: monsters (roster data +
│   │   │                   class), items, doodads, script nodes, prefab
│   │   │                   object sets (stairs, shop, orb)
│   │   ├── levelTemplate/  Shared helpers for the hand-authored levels below
│   │   ├── lobby/          Shop rooms: presets, vendor stalls, gold, portal
│   │   ├── boss/           The generated arena: geometry, spawn anchors,
│   │   │                   cover pillars, wave rig, boss roster
│   │   ├── tweak/          player tweak/*.xml emitters and bulk editors —
│   │   │                   RNG-free, so they never move a seed's dungeon
│   │   ├── buffs/         Buff auras: the optional per-floor buff fields.
│   │   │                   RNG-free; appends always-on DangerAreas after a
│   │   │                   floor is built, so it moves nothing already placed
│   │   ├── timer/          Timer mode: the optional per-floor timed damage
│   │   │                   field. RNG-free; appends nodes after a floor is
│   │   │                   built, so it moves nothing already placed
│   │   └── index.ts        generateDungeon(params, seed) → files + previews
│   ├── main/               Electron main process: window, IPC handlers,
│   │   │                   settings persistence, LevelPacker invocation,
│   │   │                   folder/zip export
│   ├── preload/            contextBridge exposing the typed window.api
│   ├── renderer/           React GUI: the five parameter tabs, validation UX,
│   │                       canvas map preview, output panel
│   └── shared/             IPC types shared between main and renderer
├── tests/                  Vitest suite for the generator
├── reference/original-java/  The original Java source, for reference
└── parameters.default.txt  Documented defaults in the original file format

Design notes:

  • The generator is deliberately pure and in-memory: it takes parameters + seed and returns file contents and preview geometry. All file writing, dialogs and process spawning live in the Electron main process, so the generator can be tested (and even reused in a plain website) without Electron.
  • The RNG reimplements java.util.Random's 48-bit LCG including Java's 32-bit float arithmetic. For the same seed and defaults, the output is structurally identical to the original Java tool (verified by diff — only ±0.00002 float rounding in entity positions differs, plus the cosmetic floor-tile variants, which the original drew from an unseeded RNG and this port draws from a seeded one so results are reproducible).

Development

npm install          # once
npm run dev          # launch the app with hot reload
npm test             # run the generator test suite (972 tests)
npm run typecheck    # strict TypeScript across main/preload/renderer/generator
npm run build        # typecheck + production build into out/
npm run dist         # build a distributable for the current platform
npm run dist:win     # or target a platform explicitly (also :mac, :linux)

Distributables are produced with electron-builder (NSIS installer on Windows, DMG on macOS, AppImage on Linux) into release/.

Tests

tests/ covers: RNG parity against reference java.util.Random vectors, parameters.txt round-tripping, the validation matrix (every crash path of the original is a test case), and fixed-seed generation — determinism, map bounds, entrance/exit/orb presence per floor, and the XML section structure.

The optional levels add their own suites, and two properties are worth calling out because they are what keeps the feature safe to leave on:

  • Nothing new touches the dungeon's RNG. The arena draws only from a third seeded stream, asserted directly; the lobbies and the player tweaks draw nothing at all. Turning any of them on or off leaves every floor byte-identical.
  • Geometry is asserted against the game's rules, not the emitter's own. The alignment test that shipped first compared the emitter to itself and passed for two rounds while the arena was visibly broken in game. Its replacements assert what the shipped levels actually demonstrate — block origins land on the 20-grid (checked against a dungeon floor too, so the claim cannot be vacuous), no wall sprite's overhang reaches the orb, and the arena band has no gap a player can walk through.

About

Resurrection and improvement of the original Java Hammerwatch roguelike dungeon generator on the original Hammerwatch forum from https://web.archive.org/web/20191207045849/http://hammerwatch.com/forum/index.php?topic=1658.30

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages