This commit is contained in:
@@ -178,6 +178,7 @@ export function dispatch(state: GameState, command: ParsedCommand, world: World)
|
||||
if (command.verb === 'look') return withEndingCheck(handleLook(state, world), world)
|
||||
if (command.verb === 'inventory') return withEndingCheck(handleInventory(state, world), world)
|
||||
if (command.verb === 'wait') return withEndingCheck(handleWait(state, world), world)
|
||||
if (command.verb === 'listen') return withEndingCheck(narrate(state, [{ kind: 'narration', text: 'You listen. The house listens back.' }]), world)
|
||||
}
|
||||
|
||||
if (command.kind === 'verb-target') {
|
||||
|
||||
@@ -10,7 +10,7 @@ const world: World = {
|
||||
id: 'foyer',
|
||||
title: '[ Foyer ]',
|
||||
descriptions: { firstVisit: 'Foyer.', revisit: 'Foyer.', examined: 'Foyer.' },
|
||||
exits: { n: 'stair' },
|
||||
exits: { n: 'stair', e: 'chapel' },
|
||||
items: [],
|
||||
safe: true,
|
||||
},
|
||||
@@ -29,10 +29,19 @@ const world: World = {
|
||||
exits: { u: 'stair' },
|
||||
items: [],
|
||||
},
|
||||
chapel: {
|
||||
id: 'chapel',
|
||||
title: '[ Chapel ]',
|
||||
descriptions: { firstVisit: 'Chapel.', revisit: 'Chapel.', examined: 'Chapel.' },
|
||||
exits: { s: 'foyer' },
|
||||
items: ['vial'],
|
||||
encounter: 'basilisk',
|
||||
},
|
||||
},
|
||||
items: {
|
||||
mirror: { id: 'mirror', names: ['mirror', 'tarnished mirror'], short: 'a tarnished mirror', long: 'A small mirror, tarnished black.', initialState: {}, takeable: true },
|
||||
sword: { id: 'sword', names: ['sword', 'cane sword'], short: 'a cane sword', long: 'A slim cane sword.', initialState: {}, takeable: true },
|
||||
vial: { id: 'vial', names: ['vial'], short: 'a vial', long: 'A small vial.', initialState: {}, takeable: true },
|
||||
},
|
||||
encounters: {
|
||||
revenant: {
|
||||
@@ -59,6 +68,22 @@ const world: World = {
|
||||
onFailed: { narration: 'You stagger back.', retreatTo: 'foyer' },
|
||||
defaultWrongVerbNarration: 'The revenant does not seem to notice.',
|
||||
},
|
||||
basilisk: {
|
||||
id: 'basilisk',
|
||||
aliases: ['basilisk'],
|
||||
startsIn: 'chapel',
|
||||
initialPhase: 'sleeping',
|
||||
phases: {
|
||||
sleeping: {
|
||||
description: 'An eye opens beneath the altar.',
|
||||
transitions: [
|
||||
{ verb: 'pour', target: 'vial', requires: { item: 'vial' }, narration: 'The eye closes.', to: 'resolved' },
|
||||
],
|
||||
},
|
||||
},
|
||||
onResolved: { setFlags: { basiliskSpared: true } },
|
||||
defaultWrongVerbNarration: 'The eye watches.',
|
||||
},
|
||||
},
|
||||
endings: {
|
||||
true: { whenFlags: { _never: true }, narration: '' },
|
||||
@@ -116,4 +141,27 @@ describe('encounters — phase advancement', () => {
|
||||
s = dispatch(s, { kind: 'go', direction: 's' }, world).state
|
||||
expect(s.resolveLevel).toBe('steady')
|
||||
})
|
||||
|
||||
it('allows a required item to be the direct target in a target-preposition encounter command', () => {
|
||||
let s = initialStateFor(world)
|
||||
s = {
|
||||
...s,
|
||||
inventory: [...s.inventory, { id: 'vial', state: {} }],
|
||||
roomState: { ...s.roomState, chapel: { takenItems: ['vial'] } },
|
||||
}
|
||||
s = dispatch(s, { kind: 'go', direction: 'e' }, world).state
|
||||
const r = dispatch(
|
||||
s,
|
||||
{
|
||||
kind: 'verb-target-prep',
|
||||
verb: 'pour',
|
||||
target: { canonical: 'vial', raw: 'vial' },
|
||||
preposition: 'on',
|
||||
indirect: { canonical: 'basilisk', raw: 'basilisk' },
|
||||
},
|
||||
world,
|
||||
)
|
||||
expect(r.state.flags['basiliskSpared']).toBe(true)
|
||||
expect(r.appended.some((l) => l.text.includes('eye closes'))).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -96,7 +96,7 @@ export function applyVerbToEncounter(
|
||||
}
|
||||
}
|
||||
}
|
||||
if (t.requires && instrumentId && t.requires.item !== instrumentId) return false
|
||||
if (t.requires && instrumentId && t.requires.item !== instrumentId && t.requires.item !== targetId) return false
|
||||
return true
|
||||
})
|
||||
|
||||
|
||||
@@ -16,6 +16,10 @@ describe('parser — verb-only commands', () => {
|
||||
expect(parse('look', emptyCtx)).toEqual({ kind: 'verb-only', verb: 'look' })
|
||||
})
|
||||
|
||||
it('recognizes bare "listen"', () => {
|
||||
expect(parse('listen', emptyCtx)).toEqual({ kind: 'verb-only', verb: 'listen' })
|
||||
})
|
||||
|
||||
it('recognizes bare "inventory" and short forms', () => {
|
||||
expect(parse('inventory', emptyCtx)).toEqual({ kind: 'verb-only', verb: 'inventory' })
|
||||
expect(parse('inv', emptyCtx)).toEqual({ kind: 'verb-only', verb: 'inventory' })
|
||||
@@ -100,6 +104,24 @@ describe('parser — verb + target', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('recognizes pour commands with an indirect target', () => {
|
||||
const ctx: ParserContext = {
|
||||
knownItems: ['silver-vial'],
|
||||
knownEncounters: ['basilisk'],
|
||||
visibleNouns: [{ id: 'basilisk', aliases: ['basilisk'] }],
|
||||
inventoryItemIds: ['silver-vial'],
|
||||
lastNoun: null,
|
||||
awaitingDisambiguation: null,
|
||||
}
|
||||
expect(parse('pour silver-vial on basilisk', ctx)).toEqual({
|
||||
kind: 'verb-target-prep',
|
||||
verb: 'pour',
|
||||
target: { canonical: 'silver-vial', raw: 'silver-vial' },
|
||||
preposition: 'on',
|
||||
indirect: { canonical: 'basilisk', raw: 'basilisk' },
|
||||
})
|
||||
})
|
||||
|
||||
it('resolves a single visible noun', () => {
|
||||
const ctx: ParserContext = {
|
||||
knownItems: ['torch'],
|
||||
|
||||
@@ -34,6 +34,8 @@ const VERB_SYNONYMS: Record<string, Verb> = {
|
||||
pull: 'pull',
|
||||
cut: 'cut', trim: 'cut',
|
||||
play: 'play',
|
||||
listen: 'listen',
|
||||
pour: 'pour',
|
||||
uncover: 'open',
|
||||
wait: 'wait', z: 'wait',
|
||||
}
|
||||
@@ -57,7 +59,7 @@ const META_VERBS: Record<string, MetaVerb> = {
|
||||
}
|
||||
|
||||
/** Verbs that legally take no target. */
|
||||
const VERB_ONLY_VERBS = new Set<string>(['look', 'inventory', 'wait'])
|
||||
const VERB_ONLY_VERBS = new Set<string>(['look', 'inventory', 'wait', 'listen'])
|
||||
|
||||
/** Two-word verb prefixes (e.g. "pick up X"). */
|
||||
const TWO_WORD_VERBS = ['pick up']
|
||||
@@ -157,7 +159,7 @@ export function parse(rawInput: string, ctx: ParserContext): ParsedCommand {
|
||||
|
||||
if (rest.length === 0) {
|
||||
if (VERB_ONLY_VERBS.has(verb)) {
|
||||
return { kind: 'verb-only', verb: verb as 'look' | 'inventory' | 'wait' }
|
||||
return { kind: 'verb-only', verb: verb as 'look' | 'inventory' | 'wait' | 'listen' }
|
||||
}
|
||||
return { kind: 'unknown', raw: trimmed, reason: 'malformed' }
|
||||
}
|
||||
|
||||
@@ -128,4 +128,98 @@ describe('playthrough — sample world', () => {
|
||||
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',
|
||||
]))
|
||||
})
|
||||
})
|
||||
|
||||
+2
-2
@@ -9,7 +9,7 @@ export type Direction = 'n' | 's' | 'e' | 'w' | 'u' | 'd'
|
||||
export type Verb =
|
||||
| 'go' | 'look' | 'examine' | 'take' | 'drop' | 'use' | 'open' | 'close'
|
||||
| 'read' | 'light' | 'extinguish' | 'attack' | 'inventory' | 'wait'
|
||||
| 'hold' | 'push' | 'pull' | 'cut' | 'play'
|
||||
| 'hold' | 'push' | 'pull' | 'cut' | 'play' | 'listen' | 'pour'
|
||||
|
||||
export type MetaVerb = 'restart' | 'undo' | 'hint' | 'save' | 'quit' | 'theme'
|
||||
|
||||
@@ -21,7 +21,7 @@ export interface NounRef {
|
||||
}
|
||||
|
||||
export type ParsedCommand =
|
||||
| { kind: 'verb-only'; verb: Verb | 'look' | 'inventory' | 'wait' }
|
||||
| { kind: 'verb-only'; verb: Verb | 'look' | 'inventory' | 'wait' | 'listen' }
|
||||
| { kind: 'verb-target'; verb: Verb; target: NounRef }
|
||||
| { kind: 'verb-target-prep'; verb: Verb; target: NounRef; preposition: string; indirect: NounRef }
|
||||
| { kind: 'ambiguous'; verb: Verb; rawNoun: string; candidates: string[] }
|
||||
|
||||
Reference in New Issue
Block a user