docs(mystery): spec for engine prereqs (verbs, disambiguation, ending UI) #1
@@ -158,7 +158,7 @@ describe('parser — verb + target', () => {
|
||||
})
|
||||
|
||||
describe('parser — disambiguation', () => {
|
||||
it('returns disambiguation request when two candidates match', () => {
|
||||
it('returns ambiguous when two candidates match', () => {
|
||||
const ctx: ParserContext = {
|
||||
knownItems: ['brass-key', 'iron-key'],
|
||||
knownEncounters: [],
|
||||
@@ -171,12 +171,11 @@ describe('parser — disambiguation', () => {
|
||||
awaitingDisambiguation: null,
|
||||
}
|
||||
const result = parse('take key', ctx)
|
||||
expect(result.kind).toBe('unknown')
|
||||
if (result.kind === 'unknown') {
|
||||
// Parser flags ambiguity by returning unknown-noun; the dispatcher
|
||||
// turns this into a PendingDisambiguation. (Keeping parser pure: it
|
||||
// signals; the dispatcher decides UI flow.)
|
||||
expect(result.reason).toBe('unknown-noun')
|
||||
expect(result.kind).toBe('ambiguous')
|
||||
if (result.kind === 'ambiguous') {
|
||||
expect(result.verb).toBe('take')
|
||||
expect(result.rawNoun).toBe('key')
|
||||
expect(result.candidates).toEqual(['brass-key', 'iron-key'])
|
||||
}
|
||||
})
|
||||
|
||||
@@ -268,3 +267,33 @@ describe('stop-word stripping', () => {
|
||||
expect(cmd.kind).toBe('verb-target')
|
||||
})
|
||||
})
|
||||
|
||||
describe('ambiguous noun', () => {
|
||||
const ctx: ParserContext = {
|
||||
knownItems: ['iron-key', 'brass-key'],
|
||||
knownEncounters: [],
|
||||
visibleNouns: [
|
||||
{ id: 'iron-key', aliases: ['key', 'iron key'] },
|
||||
{ id: 'brass-key', aliases: ['key', 'brass key'] },
|
||||
],
|
||||
inventoryItemIds: [],
|
||||
lastNoun: null,
|
||||
awaitingDisambiguation: null,
|
||||
}
|
||||
|
||||
it('returns ambiguous when two aliases match the same noun phrase', () => {
|
||||
const cmd = parse('take key', ctx)
|
||||
expect(cmd).toEqual({
|
||||
kind: 'ambiguous',
|
||||
verb: 'take',
|
||||
rawNoun: 'key',
|
||||
candidates: ['iron-key', 'brass-key'],
|
||||
})
|
||||
})
|
||||
|
||||
it('still returns verb-target when the phrase is unambiguous', () => {
|
||||
const cmd = parse('take iron key', ctx)
|
||||
expect(cmd.kind).toBe('verb-target')
|
||||
if (cmd.kind === 'verb-target') expect(cmd.target.canonical).toBe('iron-key')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -173,11 +173,16 @@ export function parse(rawInput: string, ctx: ParserContext): ParsedCommand {
|
||||
return { kind: 'unknown', raw: trimmed, reason: 'unknown-noun' }
|
||||
}
|
||||
|
||||
// Multiple candidates → ambiguous. Parser signals; the dispatcher records the
|
||||
// PendingDisambiguation in state so the next turn's input is interpreted as
|
||||
// a disambiguation reply.
|
||||
// Multiple candidates → ambiguous. Dedupe by id; if only one distinct id
|
||||
// remains, two aliases of the same item matched — not truly ambiguous.
|
||||
if (candidates.length > 1) {
|
||||
return { kind: 'unknown', raw: trimmed, reason: 'unknown-noun' }
|
||||
const uniqueIds = [...new Set(candidates.map((c) => c.id))]
|
||||
if (uniqueIds.length === 1) {
|
||||
// Two aliases of the same item — not actually ambiguous.
|
||||
const id = uniqueIds[0]!
|
||||
return { kind: 'verb-target', verb, target: { canonical: id, raw: candidates[0]!.alias } }
|
||||
}
|
||||
return { kind: 'ambiguous', verb, rawNoun: targetRaw, candidates: uniqueIds }
|
||||
}
|
||||
|
||||
const target = candidates[0]!
|
||||
|
||||
@@ -24,6 +24,7 @@ export type ParsedCommand =
|
||||
| { kind: 'verb-only'; verb: Verb | 'look' | 'inventory' | 'wait' }
|
||||
| { 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[] }
|
||||
| { kind: 'go'; direction: Direction }
|
||||
| { kind: 'meta'; verb: MetaVerb }
|
||||
| { kind: 'disambiguation'; chosen: string }
|
||||
|
||||
Reference in New Issue
Block a user