#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'>>');
}
;
Adv3Lite Library Reference Manual
Generated on 03/07/2024 from adv3Lite version 2.1