forked from EricEve/adv3lite
-
Notifications
You must be signed in to change notification settings - Fork 0
/
score.t
589 lines (501 loc) · 19 KB
/
score.t
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
#charset "us-ascii"
/*
* Copyright (c) 2000, 2006 by Michael J. Roberts. All Rights Reserved.
* Lightly adapted for adv3Lite by Eric Eve
*
* TADS 3 Library - scoring
*
* This module defines objects related to keeping track of the player's
* score, which indicates the player's progress through the game.
*/
/* include the library header */
#include "advlite.h"
/* ------------------------------------------------------------------------ */
/*
* An Achievement is an object used to award points in the score. For
* most purposes, an achievement can be described simply by a string,
* but the Achievement object provides more flexibility in describing
* combined scores when a set of similar achievements are to be grouped.
*
* There are two ways to use the scoring system.
*
* 1. You can use a mix of string names and Achievement objects for
* scoring items; each time you award a scoring item, you call the
* function addToScore() to specify the achievement (by name or by
* Achievement object) and the number of points to award. You can also
* call the method addToScoreOnce() on an Achievement object to award
* the scoring item, ensuring that the item is only awarded once in the
* entire game (saving you the trouble of checking to see if the event
* that triggered the scoring item has happened before already in the
* same game). If you do this, you MUST set the property
* gameMain.maxScore to reflect the maximum score possible in the game.
*
* 2. You can use EXCLUSIVELY Achievement objects to represents scoring
* items, and give each Achievement object a 'points' property
* indicating the number of points it's worth. To award a scoring item,
* you call the method awardPoints() on an Achievement object. If you
* use this style of scoring, the library AUTOMATICALLY computes the
* gameMain.maxScore value, by adding up the 'points' values of all of
* the Achievement objects in the game. For this to work properly, you
* have to obey the following rules:
*
*. - use ONLY Achievement objects (never strings) to award points;
*. - set the 'points' property of each Achievement to its score;
*. - define Achievement objects statically only (never use 'new' to
*. create an Achievement dynamically)
*. - if an Achievement can be awarded more than once, you must override
*. its 'maxPoints' property to reflect the total number of points it
*. will be worth when it is awarded the maximum number of times;
*. - always award an Achievement through its awardPoints() or
*. awardPointsOnce() method;
*. - there exists at least one solution of the game in which every
*. Achievement object is awarded
*/
class Achievement: object
/*
* The number of points this Achievement scores individually. By
* default, we set this to nil. If you use the awardPoints() or
* awardPointsOnce() methods, you MUST set this to a non-nil value.
*
* If you set this to a non-nil value, the library will use it
* pre-compute the maximum possible score in the game, saving you the
* trouble of figuring out the maximum score by hand.
*/
points = nil
/*
* The MAXIMUM number of points this Achievement can award. This is
* by default just our 'points' value, on the assumption that the
* achievement is scored only once. The library uses this value
* during pre-initialization to compute the maximum possible score in
* the game.
*/
maxPoints = (points)
/*
* Describe the achievement - this must display a string explaining
* the reason the points associated with this achievement were
* awarded.
*
* Note that this description can make use of the scoreCount
* information to show different descriptions depending on how many
* times the item has scored. For example, an achievement for
* finding various treasure items might want to display "finding a
* treasure" if only one treasure was found and "finding five
* treasures" if five were found.
*
* In some cases, it might be desirable to keep track of additional
* custom information, and use that information in generating the
* description. For example, the game might keep a list of
* treasures found with the achievement, adding to the list each
* time the achievement is scored, and displaying the contents of
* the list when the description is shown.
*/
desc = ""
/* show myself in a full-score listing */
listFullScoreItem()
{
/* show the number of points I'm worth */
DMsg(full score item points, '\n <<totalPoints>> point<<totalPoints == 1
? '' : 's'>> for ');
/* show my description */
desc;
}
/*
* The number of times the achievement has been awarded. Each time
* the achievement is passed to addToScore(), this is incremented.
* Note that this is distinct from the number of points.
*/
scoreCount = 0
/*
* the number of points awarded for the achievement; if this
* achievement has been accomplished multiple times, this reflects
* the aggregate number of points awarded for all of the times it
* has been accomplished
*/
totalPoints = 0
/*
* Add this achievement to the score one time only, awarding the
* given number of points. This can be used to score an achievement
* without separately tracking whether or not the achievement has
* been accomplished previously. If the achievement has already been
* scored before, this will do nothing at all; otherwise, it'll score
* the achievement with the given number of points. Returns true if
* we do award the points, nil if not (because we've awarded them
* before).
*/
addToScoreOnce(points)
{
/* if I've never been scored before, score me now */
if (scoreCount == 0)
{
/* add the points to the score */
addToScore(points, self);
/* tell the caller we awarded the points as requested */
return true;
}
else
{
/* tell the caller we didn't do anything */
return nil;
}
}
/*
* Award this Achievement's score, using the score value specified in
* my 'points' property.
*/
awardPoints()
{
/* add me to the score, using my 'points' property */
addToScore(points, self);
}
/*
* Award this Achievement's score, but ensure that we're never
* awarded more than one time. If this Achievement has already been
* awarded, this does nothing at all. Returns true if we do award
* the points, nil if not (because we've awarded them before).
*/
awardPointsOnce()
{
/* award my 'points' value only if we haven't score before */
return addToScoreOnce(points);
}
/*
* Has this achievement been achieved? By default it has if our scoreCount
* is greater than 0 (i.e. if points have been awarded for this
* achievement at least once.
*/
achieved = (scoreCount > 0)
listOrder = 0
lastListOrder = 0
;
/*
* Generic text achievement. When we add an achievement to the full
* score list and the achievement is a simple string description, we'll
* create one of these to encapsulate the achievement.
*/
class SimpleAchievement: Achievement
/* create dynamically with a given string as our description */
construct(str) { desc_ = str; }
/* show my description */
desc { say(desc_); }
/* my description string */
desc_ = ''
;
/*
* List interface for showing the full score list
*/
fullScoreLister: Lister
showListPrefix(lst, pl, paraCnt)
{
/* show the full score list intro message */
DMsg(full score prefix, 'Your score consists of:');
}
/* every achievement is listed */
listed(obj) { return true; }
showList(lst, pl, paraCnt)
{
foreach(local obj in lst)
obj.listFullScoreItem();
}
;
/*
* Score notification daemon handler. We'll receive a
* checkNotification() call each turn; we'll display a notification
* message each time the score has changed since the last time we ran.
*/
scoreNotifier: object
/* the score as it was the last time we displayed a notification */
lastScore = static (libScore.totalScore)
/* we've never generated a notification about the score before */
everNotified = nil
/* daemon entrypoint */
checkNotification()
{
/*
* if the score has changed since the last time we checked,
* possibly generate a notification
*/
if (libScore.totalScore != lastScore)
{
/* only show a message if we're allowed to */
if (libScore.scoreNotify.isOn)
{
local delta;
/* calculate the change since the last notification */
delta = libScore.totalScore - lastScore;
/*
* generate the first or non-first notification, as
* appropriate
*/
if (everNotified)
scoreChange(delta);
else
firstScoreChange(delta);
/*
* note that we've ever generated a score change
* notification, so that we don't generate the more
* verbose first-time message on subsequent
* notifications
*/
everNotified = true;
}
/*
* Remember the current score, so that we don't generate
* another notification until the score has changed again.
* Note that we note the new score even if we aren't
* displaying a message this time, because we don't want to
* generate a message upon re-enabling notifications.
*/
lastScore = libScore.totalScore;
}
}
/* score change - first notification */
firstScoreChange(delta)
{
scoreChange(delta);
}
/* score change - notification other than the first time */
scoreChange(delta)
{
"<.p><.notification><<
basicScoreChange(delta)>><./notification> ";
}
/*
* basic score change notification message - this is an internal
* service routine for scoreChange and firstScoreChange
*/
basicScoreChange(delta)
{
cquoteOutputFilter.deactivate();
DMsg(basic score change,
'''Your <<aHref('full score', 'score',
'Show full score')>>
has just <<delta > 0 ? 'in' : 'de'>>creased by
<<spellNumber(delta > 0 ? delta : -delta)>>
point<<delta is in (1, -1) ? '' : 's'>>. ''');
cquoteOutputFilter.activate();
}
;
/*
* Add points to the total score. This is a convenience function that
* simply calls libScore.addToScore_().
*/
addToScore(points, desc)
{
/* simply call the libScore method to handle it */
libScore.addToScore_(points, desc);
}
/*
* The main game score object.
*/
libScore: PreinitObject
/*
* Add to the score. 'points' is the number of points to add to the
* score, and 'desc' is a string describing the reason the points
* are being awarded, or an Achievement object describing the points.
*
* We keep a list of each unique achievement. If 'desc' is already
* in this list, we'll simply add the given number of points to the
* existing entry for the same description.
*
* Note that, if 'desc' is an Achievement object, it will match a
* previous item only if it's exactly the same Achievement instance.
*/
addToScore_(points, desc)
{
local idx;
/*
* if the description is a string, encapsulate it in a
* SimpleAchievement object
*/
if (dataType(desc) == TypeSString)
{
local newDesc;
/*
* look for an existing SimpleAchievement in our list with
* the same descriptive text - if we find one, reuse it,
* since this is another instance of the same group of
* achievements and thus can be combined into the same
* achievement object
*/
newDesc = fullScoreList.valWhich(
{ x: x.ofKind(SimpleAchievement) && x.desc_ == desc });
/*
* if we didn't find it, create a new simple achievement to
* wrap the descriptive text
*/
if (newDesc == nil)
newDesc = new SimpleAchievement(desc);
/*
* for the rest of our processing, use the wrapper simple
* achievement object instead of the original text string
*/
desc = newDesc;
}
/* increase the use count for the achievement */
desc.scoreCount++;
/* add the points to the total */
totalScore += points;
/* try to find a match in our list of past achievements */
idx = fullScoreList.indexOf(desc);
/* if we didn't find it, add it to the list */
if (idx == nil)
{
fullScoreList.append(desc);
desc.listOrder = ++Achievement.lastListOrder;
}
/*
* combine the points awarded this time into the total for this
* achievement
*/
desc.totalPoints += points;
}
/*
* Explicitly run the score notification daemon.
*/
runScoreNotifier()
{
/* explicitly run the notification */
scoreNotifier.checkNotification();
}
/*
* Show the simple score
*/
showScore()
{
/*
* Show the basic score statistics. Use the appropriate form of
* the message, depending on whether or not there's a maximum
* score value.
*/
if (gameMain.maxScore != nil)
showScoreMessage(totalScore, gameMain.maxScore,
libGlobal.totalTurns);
else
showScoreNoMaxMessage(totalScore, libGlobal.totalTurns);
/* show the score ranking */
showScoreRank(totalScore);
}
/* show the basic score message */
showScoreMessage(points, maxPoints, turns)
{
DMsg(show score, 'In {1} turn<<turns == 1 ? '' : 's'>> you have scored
{2} of a total {3} point<<maxPoints == 1 ? '' : 's'>>. ',
turns, points, maxPoints);
}
/* show the basic score message with no maximum */
showScoreNoMaxMessage(points, turns)
{
DMsg(show score no max, 'In {1} turn<<turns == 1 ? '' : 's'>> you have
scored {2} point<<points == 1 ? '' : 's'>>. ', turns, points);
}
/*
* show the score rank message
*/
showScoreRank(points)
{
local idx;
local tab = gameMain.scoreRankTable;
/* if there's no rank table, skip the ranking */
if (tab == nil)
return;
/*
* find the last item for which our score is at least the
* minimum - the table is in ascending order of minimum score,
* so we want the last item for which our score is sufficient
*/
idx = tab.lastIndexWhich({x: points >= x[1]});
/* if we didn't find an item, use the first by default */
if (idx == nil)
idx = 1;
/* show the description from the item we found */
showScoreRankMessage(tab[idx][2]);
}
/* show the full message for a given score rank string */
showScoreRankMessage(msg)
{
DMsg(show score rank, 'This makes you {1}. ', msg );
}
/*
* Display the full score. 'explicit' is true if the player asked
* for the full score explicitly, as with a FULL SCORE command; if
* we're showing the full score automatically in the course of some
* other action, 'explicit' should be nil.
*/
showFullScore()
{
/* show the basic score statistics */
showScore();
/* list the achievements in 'tall' mode */
fullScoreLister.show(fullScoreList.toList(), 0);
}
/*
* Vector for the full score achievement list. This is a list of
* all of the Achievement objects awarded for accomplishments so
* far.
*/
fullScoreList = static new Vector(32)
/* the total number of points scored so far */
totalScore = 0
/*
* current score notification status - if on, we'll show a message at
* the end of each turn where the score changes, otherwise we won't
* mention anything
*/
scoreNotify = scoreNotifySettingsItem
/*
* Compute the sum of the maximum point values of the Achievement
* objects in the game. Point values are optional in Achievement
* objects; if there are no Achievement objects with non-nil point
* values, this will simply return nil.
*/
calcMaxScore()
{
local sum;
local found;
/* start with a running total of zero */
sum = 0;
/* we haven't found any non-nil point values yet */
found = nil;
/*
* Run through all of the Achievement objects to see if we can
* derive a maximum score for the game.
*/
forEachInstance(Achievement, function(obj) {
local m;
/*
* If this object has a non-nil maxPoints value, add it to
* the running total.
*/
if ((m = obj.maxPoints) != nil)
{
/* add this one to the sum */
sum += m;
/* note that we found one with a non-nil point value */
found = true;
}
});
/*
* If we found any Achievements with point values, return the sum
* of those point values; otherwise, return nil.
*/
return (found ? sum : nil);
}
/* execute pre-initialization */
execute()
{
/* register as the global score handler */
libGlobal.scoreObj = self;
}
;
/* settings item for score notification mode */
scoreNotifySettingsItem: object
/* the "factory setting" for NOTIFY is ON */
isOn = true
/* our configuration file variable ID */
settingID = 'adv3.notify'
/* show our description */
settingDesc()
{
DMsg(short notify status, 'NOTIFY <<isOn ? 'ON' : 'OFF'>>');
}
;