import { describe, it, expect } from 'vitest' import { parse } from './parser' import type { ParserContext } from './parser' import { dispatch, getItemsInRoom, initialStateFor } from './dispatcher' import { world } from '../world' import type { GameState } from './types' function ctxFor(state: GameState): ParserContext { const room = world.rooms[state.location] const visibleNouns: { id: string; aliases: string[] }[] = [] for (const itemId of room?.items ?? []) { const item = world.items[itemId] if (item) visibleNouns.push({ id: itemId, aliases: item.names }) } for (const inst of state.inventory) { const item = world.items[inst.id] if (item) visibleNouns.push({ id: inst.id, aliases: item.names }) } if (room?.encounter) { const encounter = world.encounters[room.encounter] visibleNouns.push({ id: room.encounter, aliases: [room.encounter, room.encounter.replace(/-/g, ' '), ...(encounter?.aliases ?? [])], }) } return { knownItems: Object.keys(world.items), knownEncounters: Object.keys(world.encounters), visibleNouns, inventoryItemIds: state.inventory.map((i) => i.id), lastNoun: state.lastNoun, awaitingDisambiguation: state.pendingDisambiguation, vocabulary: world.parser, } } function play(commands: string[]): GameState { let state = initialStateFor(world) for (const cmd of commands) { const parsed = parse(cmd, ctxFor(state)) state = dispatch(state, parsed, world).state } return state } describe('playthrough — sample world', () => { it('reaches the rat-gone flag via the canonical command sequence', () => { const state = play([ 'read letter', // verb is recognized but encounter takes priority elsewhere; here it's a no-op 'n', // gate → foyer 'n', // foyer → hallway 'take lamp', 'e', // hallway → cellar-stair (triggers rat encounter) 'attack rat', 'yes', ]) expect(state.flags['ratGone']).toBe(true) expect(state.location).toBe('cellar-stair') expect(state.encounterState['rat']).toBeUndefined() }) it('handles invalid moves gracefully', () => { const state = play([ 'go up', // gate has no up exit 'n', 's', 'flibbertigibbet', // unknown verb ]) expect(state.location).toBe('outside-gate') }) it('plays through the main-floor slice encounters', () => { const state = play([ 'n', // gate → foyer 'n', // foyer → hallway 'n', // hallway → dining-room 'close curtains', 'take candlestick', 'n', // dining-room → conservatory 'take shears', 'cut vines with shears', 's', 'w', // dining-room → hallway 'w', // hallway → smoking-room 'take lighter', 'uncover cage', 'e', 'd', // hallway → music-room 'play note', 'take tiny key', 'n', // music-room → servants-passage 'wait', 'e', // servants-passage → laundry 'wait', 'take damp sheet', ]) expect(state.flags['window-guest.resolved']).toBe(true) expect(state.flags['ivy-figure.resolved']).toBe(true) expect(state.flags['covered-cage.resolved']).toBe(true) expect(state.flags['piano-echo.resolved']).toBe(true) expect(state.flags['breathing-wall.resolved']).toBe(true) expect(state.flags['linen-shape.resolved']).toBe(true) expect(state.inventory.map((i) => i.id)).toEqual(expect.arrayContaining([ 'candlestick', 'pruning-shears', 'silver-lighter', 'music-box-key', 'damp-sheet', ])) }) it('plays through the upper-floor slice', () => { const state = play([ 'n', // gate → foyer 'n', // foyer → hallway 'u', // hallway → parlor 'u', // parlor → upper stair 'wait', 'u', // upper stair → bedroom 'e', // bedroom → nursery 'read drawing', 'take dog', 'w', 'u', // bedroom → attic ]) expect(state.flags['stair-sleeper.resolved']).toBe(true) expect(state.flags['hallwayShifted']).toBe(true) expect(state.location).toBe('attic') expect(state.inventory.map((i) => i.id)).toContain('toy-dog') }) it('plays through the garden and grounds slice', () => { const state = play([ 'n', // gate → foyer 'n', // foyer → hallway 'u', // hallway → parlor 'u', // parlor → upper stair 'wait', 'u', // upper stair → bedroom 'e', // bedroom → nursery 'take dog', 'w', 'd', // bedroom → upper stair 'd', // upper stair → parlor 'd', // parlor → hallway 'n', // hallway → dining-room 'close curtains', 'e', // dining-room → kitchen 'e', // kitchen → back-door 'e', // back-door → garden 'wait', 'n', // garden → well 'd', // well → well-shaft 'hold dog', ]) expect(state.flags['garden-procession.resolved']).toBe(true) expect(state.flags['child-beneath-well.resolved']).toBe(true) expect(state.flags['gardenQuiet']).toBe(true) expect(state.flags['childPassedWell']).toBe(true) expect(state.location).toBe('well-shaft') }) it('plays through the lower-passages slice', () => { const state = play([ 'n', // gate → foyer 'n', // foyer → hallway 'n', // hallway → dining-room 'close curtains', 'n', // dining-room → conservatory 'take shears', 'cut vines with shears', 's', // conservatory → dining-room 'w', // dining-room → hallway 'd', // hallway → music-room 'play note', 'n', // music-room → servants-passage 'wait', 'e', // servants-passage → laundry 'wait', 'take damp sheet', 'w', // laundry → servants-passage 's', // servants-passage → music-room 'u', // music-room → hallway 'n', // hallway → dining-room 'e', // dining-room → kitchen 'e', // kitchen → back-door 'e', // back-door → garden 'wait', 'n', // garden → well 'd', // well → well-shaft 'wait', 'd', // well-shaft → tunnel 'n', // tunnel → ossuary 'take ring', 'leave ring', 'e', // ossuary → flooded-passage 'use water with sheet', 'take boat', 'n', // flooded-passage → root-chamber 'listen', 'e', // root-chamber → burial-gallery 'examine portraits', 'take register', 'read register', 'e', // burial-gallery → antechamber 'e', // antechamber → vault ]) expect(state.flags['bone-keeper.resolved']).toBe(true) expect(state.flags['reflection.resolved']).toBe(true) expect(state.flags['root-movement.resolved']).toBe(true) expect(state.flags['portrait-woman.resolved']).toBe(true) expect(state.flags['burialRingPlaced']).toBe(true) expect(state.flags['reflectionObscured']).toBe(true) expect(state.flags['rootsListenedTo']).toBe(true) expect(state.flags['familyResemblanceSeen']).toBe(true) expect(state.location).toBe('vault') expect(state.inventory.map((i) => i.id)).toEqual(expect.arrayContaining([ 'damp-sheet', 'toy-boat', 'family-register', ])) }) it('plays through the conditional rain-room branch', () => { const state = play([ 'n', // gate → foyer 'n', // foyer → hallway 'u', // hallway → parlor 'u', // parlor → upper stair 'wait', 'd', // upper stair → parlor 's', // parlor → wrong hallway 'wait', 'n', // wrong hallway → rain room 'look basin', ]) expect(state.flags['distant-steps.resolved']).toBe(true) expect(state.flags['rainwater-basin.resolved']).toBe(true) expect(state.flags['rainRoomEntered']).toBe(true) expect(state.endedWith).toBe('replacement') }) it('reaches the expanded true ending through the vault choice', () => { const state = play([ 'n', // gate → foyer 'n', // foyer → hallway 'n', // hallway → dining-room 'close curtains', 'n', // dining-room → conservatory 'take shears', 'cut vines with shears', 's', // conservatory → dining-room 'w', // dining-room → hallway 'd', // hallway → music-room 'play note', 'n', // music-room → servants-passage 'wait', 'e', // servants-passage → laundry 'wait', 'take damp sheet', 'w', // laundry → servants-passage 's', // servants-passage → music-room 'u', // music-room → hallway 'n', // hallway → dining-room 'e', // dining-room → kitchen 'e', // kitchen → back-door 'e', // back-door → garden 'wait', 'n', // garden → well 'd', // well → well-shaft 'wait', 'd', // well-shaft → tunnel 'n', // tunnel → ossuary 'take ring', 'leave ring', 'e', // ossuary → flooded-passage 'use water with sheet', 'n', // flooded-passage → root-chamber 'listen', 'e', // root-chamber → burial-gallery 'examine portraits', 'take register', 'e', // burial-gallery → antechamber 'e', // antechamber → vault 'n', // vault → chapel 'take vial', 'pour vial on basilisk', 's', // chapel → vault 'read register', ]) expect(state.flags['basiliskSpared']).toBe(true) expect(state.flags['nameSpoken']).toBe(true) expect(state.endedWith).toBe('true') }) it('passes out after wandering the drunk rooms too long', () => { const state = play([ 'n', // gate → foyer 'n', // foyer → hallway 'n', // hallway → dining-room 'e', // dining-room → kitchen 'take whiskey', 'drink whiskey', 'e', 'w', 'e', 'w', 'e', 'w', 'e', 'w', 'e', 'w', 'e', 'w', 'e', 'w', 'e', 'w', 'e', 'w', 'e', 'w', ]) expect(state.location).toBe('foyer') expect(state.flags['drunk']).toBe(false) expect(state.flags['drunkMoves']).toBe(0) expect(getItemsInRoom(state, world, 'kitchen')).toContain('whiskey') }) it('finds the faceless man in the drunk rooms and wakes in the foyer', () => { const state = play([ 'n', // gate → foyer 'n', // foyer → hallway 'n', // hallway → dining-room 'e', // dining-room → kitchen 'take whiskey', 'drink whiskey', 'u', // drunk hall → drunk landing 'listen', ]) expect(state.location).toBe('foyer') expect(state.flags['facelessManMet']).toBe(true) expect(state.flags['houseDebtNamed']).toBe(true) expect(state.flags['drunk']).toBe(false) }) })