docs(mystery): spec for engine prereqs (verbs, disambiguation, ending UI) #1

Merged
ejlewis merged 19 commits from feat/engine-prereqs into main 2026-05-09 15:10:20 -05:00
2 changed files with 54 additions and 0 deletions
Showing only changes of commit dac8487dbe - Show all commits
+40
View File
@@ -212,3 +212,43 @@ describe('ambiguous → disambiguation flow', () => {
expect(result.state.inventory.find((i) => i.id === 'iron-key')).toBeDefined() expect(result.state.inventory.find((i) => i.id === 'iron-key')).toBeDefined()
}) })
}) })
function readWorld(): World {
return {
startingRoom: 'r',
startingInventory: [],
rooms: { r: { id: 'r', title: '[ R ]', descriptions: { firstVisit: '.', revisit: '.', examined: '.' }, exits: {}, items: [] } },
items: {
letter: { id: 'letter', names: ['letter'], short: 'a letter', long: 'A letter.', initialState: {}, takeable: true, readable: true, readableText: 'You loved Halfstreet, the letter says.' },
rock: { id: 'rock', names: ['rock'], short: 'a rock', long: 'A rock.', initialState: {}, takeable: true },
},
encounters: {},
endings: {
true: { whenFlags: {}, narration: '' },
wrong: { whenFlags: {}, narration: '' },
bad: { whenFlags: {}, narration: '' },
},
}
}
describe('read verb', () => {
it('narrates readableText for a readable item in inventory', () => {
const world = readWorld()
let state = initialStateFor(world)
state = { ...state, inventory: [{ id: 'letter', state: {} }] }
const result = dispatch(state, {
kind: 'verb-target', verb: 'read', target: { canonical: 'letter', raw: 'letter' },
}, world)
expect(result.appended.at(-1)?.text).toBe('You loved Halfstreet, the letter says.')
})
it('errors politely on non-readable items', () => {
const world = readWorld()
let state = initialStateFor(world)
state = { ...state, inventory: [{ id: 'rock', state: {} }] }
const result = dispatch(state, {
kind: 'verb-target', verb: 'read', target: { canonical: 'rock', raw: 'rock' },
}, world)
expect(result.appended.at(-1)?.text).toBe("There's nothing to read on it.")
})
})
+14
View File
@@ -116,6 +116,7 @@ export function dispatch(state: GameState, command: ParsedCommand, world: World)
if (command.verb === 'take') return handleTake(stateWithNoun, command.target.canonical, world) if (command.verb === 'take') return handleTake(stateWithNoun, command.target.canonical, world)
if (command.verb === 'drop') return handleDrop(stateWithNoun, command.target.canonical, world) if (command.verb === 'drop') return handleDrop(stateWithNoun, command.target.canonical, world)
if (command.verb === 'examine' || command.verb === 'look') return handleExamine(stateWithNoun, command.target.canonical, world) if (command.verb === 'examine' || command.verb === 'look') return handleExamine(stateWithNoun, command.target.canonical, world)
if (command.verb === 'read') return handleRead(stateWithNoun, command.target.canonical, world)
return narrate(stateWithNoun, [{ kind: 'narration', text: `You're not sure how to ${command.verb} that.` }]) return narrate(stateWithNoun, [{ kind: 'narration', text: `You're not sure how to ${command.verb} that.` }])
} }
@@ -262,3 +263,16 @@ function handleExamine(state: GameState, itemId: string, world: World): Dispatch
if (!visible) return narrate(state, [{ kind: 'narration', text: 'You don\'t see anything like that.' }]) if (!visible) return narrate(state, [{ kind: 'narration', text: 'You don\'t see anything like that.' }])
return narrate(state, [{ kind: 'narration', text: item.long }]) return narrate(state, [{ kind: 'narration', text: item.long }])
} }
function handleRead(state: GameState, itemId: string, world: World): DispatchResult {
const item = world.items[itemId]
if (!item) return narrate(state, [{ kind: 'narration', text: "You don't see anything like that." }])
const visible =
state.inventory.find((i) => i.id === itemId) ||
getItemsInRoom(state, world, state.location).includes(itemId)
if (!visible) return narrate(state, [{ kind: 'narration', text: "You don't see anything like that." }])
if (!item.readable || !item.readableText) {
return narrate(state, [{ kind: 'narration', text: "There's nothing to read on it." }])
}
return narrate(state, [{ kind: 'narration', text: item.readableText }])
}