cfad47cfa3/t3compiler/tads3/lib/adv3/disambig.t

4b825dc642cb6eb9a060e54bf8d69288fbee4904cfad47cfa334b206c65f22086bcc5d63e6f70944
1
#charset "us-ascii"
2
3
/* 
4
 *   Copyright (c) 2000, 2006 Michael J. Roberts.  All Rights Reserved. 
5
 *   
6
 *   TADS 3 Library: disambiguation
7
 *   
8
 *   This module defines classes related to resolving ambiguity in noun
9
 *   phrases in command input.  
10
 */
11
12
#include "adv3.h"
13
14
15
/* ------------------------------------------------------------------------ */
16
/*
17
 *   Distinguisher.  This object encapsulates logic that determines
18
 *   whether or not we can tell two objects apart.
19
 *   
20
 *   Each game object has a list of distinguishers.  For most objects, the
21
 *   distinguisher list contains only BasicDistinguisher, since most game
22
 *   objects are unique and thus are inherently distinguishable from all
23
 *   other objects.  
24
 */
25
class Distinguisher: object
26
    /* can we distinguish the given two objects? */
27
    canDistinguish(a, b) { return true; }
28
29
    /* 
30
     *   Note that we're showing a prompt to the player asking for help in
31
     *   narrowing the object list, based on this distinguisher.  'lst' is
32
     *   the list of ResolveInfo objects which we're mentioning in the
33
     *   prompt.
34
     *   
35
     *   By default, we do nothing.  Some types of distinguishers might
36
     *   want to do something special here.  For example, an ownership
37
     *   distinguisher might want to set pronoun antecedents based on the
38
     *   owners mentioned in the disambiguation prompt, so that the
39
     *   player's response can refer anaphorically to the nouns in the
40
     *   prompt.  
41
     */
42
    notePrompt(lst) { }
43
44
    /*
45
     *   Is the object in scope for the purposes of the disambiguation
46
     *   reply from the player?  By default, any object in the full match
47
     *   list is in scope.
48
     *   
49
     *   Distinguishers that can use related objects to qualify the name
50
     *   should add those related objects to the scope by returning true
51
     *   here.  For example, the locational distinguisher can use the
52
     *   location name as a qualifying phrase, so the location name is in
53
     *   scope.  
54
     */
55
    objInScope(obj, matchList, fullMatchList)
56
    {
57
        /* it's in scope if it's in the full match list */
58
        return fullMatchList.indexWhich({x: x.obj_ == obj}) != nil;
59
    }
60
61
    /*
62
     *   Try matching an object to a noun phrase in a disambiguation reply
63
     *   from the player (that is, the player's response to a "Which foo
64
     *   did you mean" question).  By default, we call the object's
65
     *   matchNameDisambig() method to let it try to match its
66
     *   disambiguation name.
67
     *   
68
     *   Subclasses can override this to check for additional phrasing
69
     *   specific to the subclass.  For example, the locational
70
     *   distinguisher checks for a match to the container or owner name,
71
     *   so that the player can simply respond to the question with the
72
     *   location name rather than typing in a whole locational phrase.
73
     *   Note that subclasses will usually want to inherit the default
74
     *   handling if they don't find a match to their own special phrasing,
75
     *   because the player might respond with a simple adjective
76
     *   pertaining to the base object even if there's some external
77
     *   distinguishing characteristic handled by the subclass.
78
     */
79
    matchName(obj, origTokens, adjustedTokens, matchList, fullMatchList)
80
    {
81
        /* try matching the object's disambiguation name */
82
        return obj.matchNameDisambig(origTokens, adjustedTokens);
83
    }
84
;
85
86
/*
87
 *   A "null" distinguisher.  This can tell two objects apart if they have
88
 *   different names (so it's inherently language-specific).  
89
 */
90
nullDistinguisher: Distinguisher
91
;
92
93
/*
94
 *   "Basic" Distinguisher.  This distinguisher can tell two objects apart
95
 *   if one or the other object is not marked as isEquivalent, OR if the
96
 *   two objects don't have an identical superclass list.  This
97
 *   distinguisher thus can tell apart objects unless they're "basic
98
 *   equivalents," marked with isEquivalent and having the same equivalence
99
 *   keys.  
100
 */
101
basicDistinguisher: Distinguisher
102
    canDistinguish(a, b)
103
    {
104
        /*
105
         *   If the two objects are both marked isEquivalent, and they have
106
         *   the same equivalence key, they are basic equivalents, so we
107
         *   cannot distinguish them.  Otherwise, we consider them
108
         *   distinguishable.  
109
         */
110
        return !(a.isEquivalent
111
                 && b.isEquivalent
112
                 && a.equivalenceKey == b.equivalenceKey);
113
    }
114
;
115
116
/*
117
 *   Ownership Distinguisher.  This distinguisher can tell two objects
118
 *   apart if they have different owners.  "Unowned" objects are
119
 *   identified by their immediate containers instead of their owners.
120
 *   
121
 *   Note that while location *can* distinguish items with this
122
 *   distinguisher, ownership takes priority: if an object has an owner,
123
 *   the owner is the distinguishing feature.  The reason location is a
124
 *   factor at all is that we need something parallel to ownership for the
125
 *   purposes of phrasing distinguishing descriptions of unowned objects.
126
 *   The best-sounding phrasing, at least in English, is to refer to the
127
 *   unowned objects by location.  
128
 */
129
ownershipDistinguisher: Distinguisher
130
    canDistinguish(a, b)
131
    {
132
        local aOwner;
133
        local bOwner;
134
135
        /* get the nominal owner of each object */
136
        aOwner = a.getNominalOwner();
137
        bOwner = b.getNominalOwner();
138
139
        /* 
140
         *   If neither object is owned, we can't tell them apart on the
141
         *   basis of ownership, so check to see if we can tell them apart
142
         *   on the basis of their immediate locations.  
143
         */
144
        if (aOwner == nil && bOwner == nil)
145
        {
146
            /* 
147
             *   neither is owned - we can tell them apart only if they
148
             *   have different immediate containers 
149
             */
150
            return a.location != b.location;
151
        }
152
153
        /*
154
         *   One or both objects are owned, so we can tell them apart if
155
         *   and only if they have different owners.  
156
         */
157
        return aOwner != bOwner;
158
    }
159
160
    objInScope(obj, matchList, fullMatchList)
161
    {
162
        /* it's in scope if it's an owner of an object in the base list */
163
        if (matchList.indexWhich(new function(m) {
164
165
            /* get the owner, or the location if there's no owner */
166
            m = m.obj_;
167
            local l = m.getNominalOwner();
168
            if (l == nil)
169
                l = m.location;
170
171
            /* if obj matches the owner/location, consider it in scope */
172
            return obj == l;
173
        }) != nil)
174
            return true;
175
        
176
        /* otherwise, use the inherited handling */
177
        return inherited(obj, matchList, fullMatchList);
178
    }
179
180
    matchName(obj, origTokens, adjustedTokens, matchList, fullMatchList)
181
    {
182
        /* if the name matches, consider ownership relationships */
183
        if (obj.matchName(origTokens, adjustedTokens))
184
        {
185
            /* 
186
             *   Look for objects in the original list owned by 'obj'.  We
187
             *   might be matching an owner or location name rather than an
188
             *   object from the original list, in which case we want to act
189
             *   like we're matching the original list object(s) instead. 
190
             */
191
            local owned = matchList.mapAll({m: m.obj_})
192
                .subset(new function(m) {
193
                
194
                /* get the owner or location */
195
                local o = m.getNominalOwner();
196
                if (o == nil)
197
                    o = m.location;
198
                
199
                /* if the owner/location is 'obj', keep it */
200
                return o == obj;
201
            });
202
            
203
            /* if we found any matches, return them all */
204
            if (owned.length() > 0)
205
                return owned;
206
        }
207
208
        /* no match to the owner; inherit the default handling */
209
        return inherited(obj, origTokens, adjustedTokens, 
210
                         matchList, fullMatchList);
211
    }
212
;
213
214
/*
215
 *   Location Distinguisher.  This distinguisher identifies objects purely
216
 *   by their immediate locations.  
217
 */
218
locationDistinguisher: Distinguisher
219
    canDistinguish(a, b)
220
    {
221
        /* we tell the objects apart by their immediate locations */
222
        return a.location != b.location;
223
    }
224
225
    objInScope(obj, matchList, fullMatchList)
226
    {
227
        /* it's in scope if it's a location of an object in the base list */
228
        if (matchList.indexWhich({m: m.obj_.location == obj}) != nil)
229
            return true;
230
231
        /* otherwise, use the inherited handling */
232
        return inherited(obj, matchList, fullMatchList);
233
    }
234
235
    matchName(obj, origTokens, adjustedTokens, matchList, fullMatchList)
236
    {
237
        /* if the name matches, consider location relationships */
238
        if (obj.matchName(origTokens, adjustedTokens))
239
        {
240
            /* look for objects in the original list contained in 'obj' */
241
            local cont = matchList.mapAll({m: m.obj_})
242
                .subset({m: m.location == obj});
243
            
244
            /* if we found any matches, return them all */
245
            if (cont.length() > 0)
246
                return cont;
247
        }
248
249
        /* no match to the owner; inherit the default handling */
250
        return inherited(obj, origTokens, adjustedTokens, 
251
                         matchList, fullMatchList);
252
    }
253
;
254
255
/*
256
 *   Lit/unlit Distinguisher.  This distinguisher can tell two objects
257
 *   apart if one is lit (i.e., its isLit property is true) and the other
258
 *   isn't. 
259
 */
260
litUnlitDistinguisher: Distinguisher
261
    canDistinguish(a, b)
262
    {
263
        /* we can tell them apart if one is lit and the other isn't */
264
        return a.isLit != b.isLit;
265
    }
266
;
267
268
/* ------------------------------------------------------------------------ */
269
/*
270
 *   A command ranking criterion for comparing by the number of ordinal
271
 *   phrases ("first", "the second one") we find in a result. 
272
 */
273
rankByDisambigOrdinals: CommandRankingByProblem
274
    prop_ = &disambigOrdinalCount
275
;
276
277
/*
278
 *   Disambiguation Ranking.  This is a special version of the command
279
 *   ranker that we use to rank the intepretations of a disambiguation
280
 *   response.  
281
 */
282
class DisambigRanking: CommandRanking
283
    /*
284
     *   Add the ordinal count ranking criterion at the end of the
285
     *   inherited list of ranking criteria.  If we can't find any
286
     *   differences on the basis of the other criteria, choose the
287
     *   interpretation that uses fewer ordinal phrases.  (We prefer an
288
     *   non-ordinal interpretation, because this will prefer matches to
289
     *   explicit vocabulary for objects over matches for generic
290
     *   ordinals.)
291
     *   
292
     *   Insert the 'ordinal' rule just before the 'indefinite' rule -
293
     *   avoiding an ordinal match is more important.  
294
     */
295
    rankingCriteria = static (inherited().insertAt(
296
        inherited().indexOf(rankByIndefinite), rankByDisambigOrdinals))
297
    
298
    /*
299
     *   note the an ordinal response is out of range 
300
     */
301
    noteOrdinalOutOfRange(ord)
302
    {
303
        /* count it as a non-matching entry */
304
        ++nonMatchCount;
305
    }
306
307
    /* 
308
     *   note a list ordinal (i.e., "the first one" to refer to the first
309
     *   item in the ambiguous list) - we take list ordinals as less
310
     *   desirable than treating ordinal words as adjectives or nouns
311
     */
312
    noteDisambigOrdinal()
313
    {
314
        /* count it as an ordinal entry */
315
        ++disambigOrdinalCount;
316
    }
317
318
    /* number of list ordinals in the match */
319
    disambigOrdinalCount = 0
320
321
    /* 
322
     *   disambiguation commands have no verbs, so there's no verb
323
     *   structure to rank; so just use an arbitrary noun slot count
324
     */
325
    nounSlotCount = 0
326
;
327
328
/* ------------------------------------------------------------------------ */
329
/*
330
 *   Base class for resolvers used when answering interactive questions.
331
 *   This class doesn't do anything in the library directly, but it
332
 *   provides a structured point for language extensions to hook in as
333
 *   needed with 'modify'.  
334
 */
335
class InteractiveResolver: ProxyResolver
336
;
337
338
/* ------------------------------------------------------------------------ */
339
/*
340
 *   Disambiguation Resolver.  This is a special resolver that we use for
341
 *   resolving disambiguation responses.  
342
 */
343
class DisambigResolver: InteractiveResolver
344
    construct(matchText, ordinalMatchList, matchList, fullMatchList, resolver,
345
              dist)
346
    {
347
        /* inherit the base class constructor */
348
        inherited(resolver);
349
        
350
        /* remember the original match text and lists */
351
        self.matchText = matchText;
352
        self.ordinalMatchList = ordinalMatchList;
353
        self.matchList = matchList;
354
        self.fullMatchList = fullMatchList;
355
        self.distinguisher = dist;
356
    }
357
358
    /*
359
     *   Match an object's name.  We'll send this to the distinguisher for
360
     *   handling.  
361
     */
362
    matchName(obj, origTokens, adjustedTokens)
363
    {
364
        return distinguisher.matchName(obj, origTokens, adjustedTokens,
365
                                       matchList, fullMatchList);
366
    }
367
368
    /*
369
     *   Resolve qualifiers in the enclosing main scope, since qualifier
370
     *   phrases in responses are not part of the narrowed list being
371
     *   disambiguated.  
372
     */
373
    getQualifierResolver() { return origResolver; }
374
375
    /* 
376
     *   Determine if an object is in scope.  We pass this to the
377
     *   distinguisher to decide.  
378
     */
379
    objInScope(obj)
380
    {
381
        return distinguisher.objInScope(obj, matchList, fullMatchList);
382
    }
383
384
    /* 
385
     *   we allow ALL in interactive disambiguation responses, regardless
386
     *   of the verb, because we're just selecting from the list presented
387
     *   in the prompt in these cases 
388
     */
389
    allowAll = true
390
391
    /* for 'all', use the full current full match list */
392
    getAll(np) { return fullMatchList; }
393
394
    /* filter an ambiguous noun list */
395
    filterAmbiguousNounPhrase(lst, requiredNum, np)
396
    {
397
        /* 
398
         *   we're doing disambiguation, so we're only narrowing the
399
         *   original match list, which we've already filtered as well as
400
         *   we can - just return the list unchanged 
401
         */
402
        return lst;
403
    }
404
405
    /* filter a plural noun list */
406
    filterPluralPhrase(lst, np)
407
    {
408
        /* 
409
         *   we're doing disambiguation, so we're only narrowing the
410
         *   original match list, which we've already filtered as well as
411
         *   we can - just return the list unchanged 
412
         */
413
        return lst;
414
    }
415
416
    /*
417
     *   Select the match for an indefinite noun phrase.  In interactive
418
     *   disambiguation, an indefinite noun phrase simply narrows the
419
     *   list, rather than selecting any match, so treat this as still
420
     *   ambiguous.  
421
     */
422
    selectIndefinite(results, lst, requiredNumber)
423
    {
424
        /* note the ambiguous list in the results */
425
        return results.ambiguousNounPhrase(nil, ResolveAsker, '',
426
                                           lst, lst, lst,
427
                                           requiredNumber, self);
428
    }
429
430
    /* the text of the phrase we're disambiguating */
431
    matchText = ''
432
433
    /* 
434
     *   The "ordinal" match list: this includes the exact list offered as
435
     *   interactive choices in the same order as they were shown in the
436
     *   prompt.  This list can be used to correlate ordinal responses to
437
     *   the prompt list, since it contains exactly the items listed in
438
     *   the prompt.  Note that this list will only contain one of each
439
     *   indistinguishable object.  
440
     */
441
    ordinalMatchList = []
442
443
    /* 
444
     *   the original match list we are disambiguating, which includes all
445
     *   of the objects offered as interactive choices, and might include
446
     *   indistinguishable equivalents of offered items 
447
     */
448
    matchList = []
449
450
    /* 
451
     *   the full original match list, which might include items in scope
452
     *   beyond those offered as interactive choices 
453
     */
454
    fullMatchList = []
455
456
    /* 
457
     *   The distinguisher that was used to generate the prompt.  Some
458
     *   distinguishers can tell objects apart by other characteristics
459
     *   than just their names, so when parsing we want to be able to give
460
     *   the distinguisher a look at the input to see if the player is
461
     *   referring to one of the distinguishing characteristics rather than
462
     *   the object's own name.  
463
     */
464
    distinguisher = nil
465
;
466
467
/* ------------------------------------------------------------------------ */
468
/*
469
 *   General class for disambiguation exceptions 
470
 */
471
class DisambigException: Exception
472
;
473
474
/*
475
 *   Still Ambiguous Exception - this is thrown when the user answers a
476
 *   disambiguation question with insufficient specificity, so that we
477
 *   still have an ambiguous list. 
478
 */
479
class StillAmbiguousException: DisambigException
480
    construct(matchList, origText)
481
    {
482
        /* remember the new match list and text */
483
        matchList_ = matchList;
484
        origText_ = origText;
485
    }
486
487
    /* the narrowed, but still ambiguous, match list */
488
    matchList_ = []
489
490
    /* the text of the new phrasing */
491
    origText_ = ''
492
;
493
494
/*
495
 *   Unmatched disambiguation - we throw this when the user answers a
496
 *   disambiguation question with a syntactically valid response that
497
 *   doesn't refer to any of the objects in the list of choices offered. 
498
 */
499
class UnmatchedDisambigException: DisambigException
500
    construct(resp)
501
    {
502
        /* remember the response text */
503
        resp_ = resp;
504
    }
505
506
    /* the response text */
507
    resp_ = nil
508
;
509
510
511
/*
512
 *   Disambiguation Ordinal Out Of Range - this is thrown when the user
513
 *   answers a disambiguation question with an ordinal, but the ordinal is
514
 *   outside the bounds of the offered list (for example, we ask "which
515
 *   book do you mean, the red book, or the blue book?", and the user
516
 *   answers "the fourth one"). 
517
 */
518
class DisambigOrdinalOutOfRangeException: DisambigException
519
    construct(ord)
520
    {
521
        /* remember the ordinal word */
522
        ord_ = ord;
523
    }
524
525
    /* a string giving the ordinal word entered by the user */
526
    ord_ = ''
527
;
528
529
/* ------------------------------------------------------------------------ */
530
/*
531
 *   A disambiguation results gatherer object.  We use this to manage the
532
 *   results of resolution of a disambiguation response.  
533
 */
534
class DisambigResults: BasicResolveResults
535
    construct(parent)
536
    {
537
        /* copy the actor information from the parent resolver */
538
        setActors(parent.targetActor_, parent.issuingActor_);
539
    }
540
541
    ambiguousNounPhrase(keeper, asker, txt,
542
                        matchList, fullMatchList, scopeList,
543
                        requiredNum, resolver)
544
    {
545
        /* if we're resolving a sub-phrase, inherit the standard handling */
546
        if (resolver.isSubResolver)
547
            return inherited(keeper, asker, txt,
548
                             matchList, fullMatchList, scopeList,
549
                             requiredNum, resolver);
550
551
        /* 
552
         *   Before giving up, try filtering by possessive rank, in case we
553
         *   qualified by a possessive phrase. 
554
         */
555
        matchList = resolver.filterPossRank(matchList, requiredNum);
556
557
        /*
558
         *   Our disambiguation response itself requires further
559
         *   disambiguation.  Do not handle it recursively, since doing so
560
         *   could allow the user to blow the stack simply by answering
561
         *   with the same response over and over.  Instead, throw a
562
         *   "still ambiguous" exception - the original disambiguation
563
         *   loop will note the situation and iterate on the resolution
564
         *   list, ensuring that we can run forever without blowing the
565
         *   stack, if that's the game the user wants to play.  
566
         */
567
        throw new StillAmbiguousException(matchList, txt.toLower().htmlify());
568
    }
569
570
    /*
571
     *   note the an ordinal response is out of range 
572
     */
573
    noteOrdinalOutOfRange(ord)
574
    {
575
        /* this is an error */
576
        throw new DisambigOrdinalOutOfRangeException(ord);
577
    }
578
579
    /*
580
     *   show a message on not matching an object - for a disambiguation
581
     *   response, failing to match means that the combination of the
582
     *   disambiguation response plus the original text doesn't name any
583
     *   objects, not that the object in the response itself isn't present 
584
     */
585
    noMatch(action, txt)
586
    {
587
        /* throw an error indicating the problem */
588
        throw new UnmatchedDisambigException(txt.toLower().htmlify());
589
    }
590
591
    noVocabMatch(action, txt)
592
    {
593
        /* throw an error indicating the problem */
594
        throw new UnmatchedDisambigException(txt.toLower().htmlify());
595
    }
596
597
    noMatchForPossessive(owner, txt)
598
    {
599
        /* throw an error indicating the problem */
600
        throw new UnmatchedDisambigException(txt.toLower().htmlify());
601
    }
602
;
603
604