The docs had drifted from the code and described things that never existed.
- architecture.md documented a QuitCommand class (quit/exit are aliases on
MenuCommand) and a GameIO interface of read()/write(String); the real one is
readLine() plus print(StyledText) with presentation hooks. Both corrected,
along with the package diagram and the game-loop sketch.
- conventions.md listed a package tree missing save, menu, io.text and
command.impl. Updated, including why MapLayout lives in game.
- data-structures.md claimed LinkedList was deliberately avoided (Pathfinder
uses one for O(1) addFirst during path reconstruction), named the wrong queue
for the Swing bridge, and listed an input history that was never built.
- Removed enhancement-ideas.md and implementation-status.md: a backlog and a
phase tracker, neither of which documents the delivered project.
Also stop tracking the built jars. They still ship in the submission, but a
22 MB binary that changes on every build does not belong in git history.
The assignment requires Javadoc on classes, methods and instance variables.
Coverage was at roughly 45%: enum constants were undocumented across the board
(0 of 37), record components — which are a record's instance variables — sat at
24%, and instance fields at 52%. GameSession was @Getter-exposed with all six
fields undocumented, the exact case docs/conventions.md calls out by name.
Backfilled across model, io, game, loader, command, save and menu:
all enum constants, all record components (@param blocks), all instance fields,
and the public/protected methods and constructors that lacked tags.
'javadoc -Xdoclint:missing' now reports zero missing-doc warnings for the public
API. The 30 remaining warnings are all "use of default constructor" on
Lombok-generated constructors, which would require adding code to silence.
The 'map' package held a single class, which breaks the project's own rule in
docs/conventions.md: a subpackage only pays for itself once a class family
reaches three classes and forms its own abstraction.
MapLayout is BFS grid-layout logic and belongs next to Pathfinder in 'game'.
- OggMusicBackend shared one 'stopping' flag across playbacks and nulled the
thread handle without checking whether the join had actually succeeded. A
thread that outlived its join budget saw the flag flip back to false and
resumed playing, so two tracks played at once and the orphan never died. Each
playback now owns its stop token, and fades abort when it is set.
- SaveService.slug mapped "Bedroom Run" and "Bedroom_Run" to the same filename,
so the second save silently overwrote the first. The slug is now injective: a
short hash is appended whenever sanitising was lossy. Clean names keep clean
filenames, so existing saves still load.
- The Swing worker is the only consumer of the input queue, so when it died on a
loader error the window stayed open and ignored every command forever. It now
reports the error and closes. Typing while the menu overlay is up no longer
queues commands that get replayed once the menu closes.
The assignment's examples are German ("Gehe nach Norden", "Nimm Brief",
"Lies Brief", "Benutze Schaufel") but every verb was English-only, so none of
them parsed.
- Register German aliases alongside the English ones (gehe, nimm, lies, benutze,
lege, gib, rede, schau, karte, inventar, hilfe, beenden)
- Direction.fromString accepts norden/sueden/süden/osten/westen and the n/s/e/w
shorthands; YAML exits keep the canonical English names
- Treat "nach", "den", "die", "das" as filler words so "Gehe nach Norden" parses
Two independent world-logic bugs:
- Game.publishHud ticked QuestEngine before EscortEngine, so the twins_reunited
flag set by the escort was only observed a full turn later. On the turn the
twins were reunited, 'quests' still listed the objective as active.
- The valve_wheel/valve recipe had neither 'consume' nor 'requires', so it could
be repeated indefinitely, minting a fresh ceramic fuse every time. It now
requires notFlag cellar_drained and explains itself once the cellar is dry.
Only look, examine, take and go checked whether the room was lit. In a pitch
black room the player could still read a letter, talk to and bribe an NPC, drop
items, and even 'use valve_wheel on valve' to drain the cellar and complete a
quest objective — all in total darkness.
Light.canSeeRoom() now gates every command that resolves room contents. The
inventory stays reachable in the dark on purpose: otherwise switching the lamp
off inside a dark room would be an unrecoverable soft-lock.
Room-item lookups in UseCommand and Combinations are gated too, and the
"too dark" message does not reveal whether the item is actually there.
Nothing stopped the player from picking up scenery. The front door is an
ordinary switchable item in the foyer whose on-transition sets the 'left_manor'
flag that every ending keys off, so it could be carried into the cellar and used
there to end the game two floors underground. The chapel shrine could likewise
be carried out and the locket/photograph ritual performed anywhere.
Item gains a 'fixed' flag; TakeCommand refuses fixed items and leaves them in
the room. Fixed items stay usable in place, so all recipes still work.
Marked fixed: front_door, shrine, generator, valve, portrait — their
descriptions already claimed they were bolted down.
WorldValidator only checked id syntax, startRoom and NPC greetings. Quests,
endings and combinations were never even passed to it, so a broken world could
load "successfully" and then fail during play:
- A room without a 'name' loaded cleanly and threw an NPE in ConsoleIO.frameTop
the moment the player walked in.
- A typo'd item id in a quest condition silently evaluated to false forever,
leaving the stage uncompletable with no diagnostic.
The validator now receives quests, endings and combinations, and resolves every
item and quest id used in conditions, effects and recipes. Rooms and items must
carry a non-blank name and description. All problems are collected and reported
together instead of one per run.
maven.compiler.source/target were set to 25, but the newest language feature
actually used is List.getFirst() (SequencedCollection, Java 21). On a JDK 17 or
21 the project would not compile at all.
- Use maven.compiler.release=21 instead of the source/target pair
- README: update prerequisites, tech stack and test count
- .gitignore: ignore the Nextcloud sync file and release/*.jar
Replace the single HauntedManor.jar with two self-contained jars, one per
entry point (HauntedManor-Console.jar -> App, HauntedManor-GUI.jar -> AppGui),
each with its own Main-Class so both run via plain `java -jar`.
The console jar omits the OGG decode libraries and the ~11 MB of music tracks
(GameIO.setMusic is a no-op in text mode), dropping it from 17 MB to ~5 MB.
README documents running each jar.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two separated brothers, Marlon (conservatory) and Malte (boiler room), can be
reunited as optional content. Carry each twin's marble to the other as proof
he's alive and that twin follows you; lead both to the foyer and they escape
together, granting a Brass Compass keepsake and unlocking a new top-priority
"No One Left Behind" ending. Purely optional — no main win condition depends
on it.
The new EscortEngine drives following and the foyer reunion from flags alone,
so save/load needs no format change (it reconciles follower placement on the
first tick after loading). Adds Room.removeNpc, one tick call in the game loop,
and a follower note appended by LookCommand. descriptionStates swap the twins'
home-room prose once they leave.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Add ./mvnw wrapper (script-only) so the project builds/runs without a
system Maven install.
- Add maven-shade-plugin producing a self-contained release/HauntedManor.jar
(merges audio SPI services; no dependency-reduced-pom).
- Document running via ./mvnw and via the jar directly in the README.
- Ignore docs/superpowers/ (planning specs kept locally, untracked).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
App.run ended its menu loop on Quit and just returned. The console exits then,
but in the GUI run() is on a daemon worker while the Swing JFrame stays
displayable on the EDT, keeping the JVM alive — and the dismissed menu overlay
just revealed the running game underneath ("back into the game"). EXIT_ON_CLOSE
only fires for the native window X, not a programmatic quit.
Add a GameIO.shutdown() teardown hook (default no-op) and call it after the menu
loop ends. SwingIO overrides it to stop music, dispose the window, and exit the
JVM, mirroring EXIT_ON_CLOSE; console keeps the no-op. Extract an injectable
App.run overload and add AppQuitTest asserting the Quit path invokes shutdown().
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Give the map/quests sidebar more room: raise the width fraction (0.28 -> 0.34)
and minimum (180 -> 220), and remove the SIDE_MAX upper clamp so the panel
keeps scaling with the window instead of stopping at 460px.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Install the downloaded Cascadia Mono Nerd Font as src/main/resources/fonts/
game.ttf — the exact name SwingIO.loadFont() looks up on the classpath
(/fonts/game.ttf), so the GUI now uses a real monospaced Nerd Font (icon
glyphs included) instead of falling back to the logical monospaced font.
Include the font's SIL Open Font License as required for redistribution.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Switch OggMusicBackend from streaming an external, git-ignored music/ folder
(relative to the run directory) to streaming bundled classpath resources under
/music/, so the four manor tracks ship inside the JAR and play out of the box —
consistent with how fonts are bundled. The play loop reopens the resource each
iteration via a BufferedInputStream (the vorbisspi SPI needs mark/reset to read
the OGG header). Missing tracks still fall back to silence.
Add the four converted OGG Vorbis tracks (manor-theme/cellar-drone/musicbox/
lament) under src/main/resources/music/, un-ignore that path, and re-anchor the
gitignore so only the top-level MP3 source folder stays ignored. Update
docs/music.md for the bundled approach and conversion recipes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the 5-room demo with a condensed 14-room manor on one spine
(restore power -> escape) that stacks all five mechanics, plus an optional
spirit thread feeding 3 prioritized endings (no hard-fail). Pure YAML on the
existing engines: 14 rooms across 3 levels, ~16 items, 5 combinations, the
old_man hint NPC, restore_power + lay_to_rest quests, and 3 endings.
Consolidate to one world: remove the toy "Test Manor" test fixture so the
loader tests exercise the real shipped world. Adapt WorldLoaderTest and
TutorialLoaderTest to form-invariants of the real world, and add
WorldSolvableTest, which walks the critical path (light/lock gating +
Combinations.tryUse + flags) to guarantee power_on, ghost_banished, and a
victory ending are reachable — an unsolvable world now fails the build.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Guard colorMode and glyphMode against null in the Settings compact
constructor (alongside the existing musicLevel guard), so a hand-edited
settings.json with a missing field no longer NPEs in SettingsMenu.
Remove the unused MusicLevel import from SettingsMenu. Assert that the
defaulted musicLevel survives a round-trip in SettingsStoreTest.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
A command during the walkthrough (e.g. use front_door -> fled) no longer ends
the game mid-tutorial; a pending ending fires once the tutorial finishes or is
skipped.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Alias-aware, stateful engine with verb/minArgs matching, go_direction/go_to_room
room-change detection, skip, and closing-tips on completion. 7 tests, all green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- UX: unknown-target message now reads "don't know any direction or place
called '...'" so direction typos (e.g. 'go nroth') make sense
- clarity: resolveVisited uses equalsIgnoreCase for id/name matching;
add null-world guard comment pointing to CommandTestSupport
- Javadoc: note that 'to' filler word is stripped, making go-to forms equivalent
- tests: assert 'through the Hallway' in 3-node route; update unknown message
assertion; add adjacentWalkUsesShortSummary covering the n==2 short-form path
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Combinations.tryUse: only write response when non-null and non-blank,
preventing NPE/IllegalArgument for effects-only recipes.
- CombinationFactory.fromDto: throw WorldLoadException when a == b,
since item singletons make a self-combination impossible at runtime.
- Tests: +effectsOnlyRecipeAppliesEffectsWithoutProduceOrResponse
(CombinationsTest 7→8), +rejectsSelfCombination
(CombinationFactoryTest 4→5).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>