This is one of those things where I’ve have existing code for the individual bits, but tying them all together is proving unexpectedly complicated (TADS3, everybody).
Infinite(-ish) Dispensers
First, here’s some code for an infinite resource dispenser, involving two classes: ResourceFactory for the dispensers; and Resource for the things dispensed. It’s kinda like the stock Dispenser class, but instances are created as needed.
The gimmick I’m using is to hide an instance of the Resource in each ResourceFactory and use it to remap >TAKE [resource] to >TAKE [resource] FROM [resource factory] if there are no other takeable instances of the resource in scope (but will default to taking a resource instance on the floor if there is one).
Preliminary nonsense
First a couple library messages:
modify playerActionMessages
// Default success message.
okayTakeResource = '{You/He} take{s} {an dobj/her} from
{the iobj/him}. '
okayTakeResourceCount(n) {
return('{You/He} take{s} <<spellInt(n)>>
<<((n == 1) ? gDobj.name : gDobj.pluralName)>>
from {the iobj/him}. ');
}
// Factory doesn't accept anything.
cantPutAnythingInFactory = '{You/He} can\'t put anything in
{the iobj/him}. '
// Factory accepts returns, but dobj isn't its resource.
cantPutInNonResource = '{You/He} can\'t put {that dobj/him}
in {the iobj/him}. '
;
The second message there, the one with the count, is mostly aspirational at the moment, which we’ll see later.
Next we’ll declare a few utility macros. This is based on the isType.h file, which is available from a tiny git repo. The only bit we really care about is the isType macro itself, which is:
#define isType(obj, cls) ((obj != nil) && (dataType(obj) == TypeObject) && obj.ofKind(cls))
That is, it evaluates to boolean true if the first arg is non-nil and is an instance of the class given in the second arg. We use that to define our resource-specific checks:
#define isResource(obj) isType(obj, Resource)
#define isResourceFactory(obj) isType(obj, ResourceFactory)
#define isInFactory(obj) (isResource(obj) && isResourceFactory(obj.location) \
&& (obj.ofKind(obj.location.resourceClass)))
The first two are straightforward. The third is used to test if the arg a) is a non-nil instance of the Resource class, and b) is currently in a ResourceFactory instance that dispenses resources of its type.
The Resource class
We now define the Resource class:
class Resource: Thing
// Tweak vocabulary likelihood so we don't show up in disambiguation
// prompts if we're still in the factory.
vocabLikelihood = (isInFactory(self) ? -30 : 0)
isEquivalent = true
// The factory handles things for >TAKE [resource] FROM [factory],
// but we also have to handle >TAKE [resource] by itself when
// there's no other instance in scope besides the one in the
// factory.
dobjFor(Take) {
verify() {
if(isInFactory(self))
logicalRank(50, 'factory');
inherited();
}
action() {
if(isInFactory(self))
replaceAction(TakeFrom, self, self.location);
inherited();
}
}
;
We use the isInFactory macro to twiddle the vocabLikelihood of Resource instances. This is so an instance in a factory won’t show up in a disambig prompt, but if there are no other instances in scope trying to >TAKE the resource will grab one from the factory (instead of requiring the player to specify >TAKE [resource] FROM [factory].
The latter behavior handled by the dobjFor(TAKE) stanza.
The ResourceFactory class
Finally we define the ResourceFactory class:
class ResourceFactory: Container
// Class of resource we dispense. Should be overidden by instances.
resourceClass = Resource
// We don't list our contents because we always contain a phantom
// resource.
contentsListed = nil
contentsListedInExamine = nil
// Boolean. If true we can put resource instances back in the factory.
resourceReturnable = nil
// Actual resource dispensing logic.
iobjFor(TakeFrom) {
action() {
local obj;
obj = createResource();
obj.moveInto(gActor);
defaultReport(&okayTakeResource);
exit;
}
}
// Handle returns.
iobjFor(PutIn) {
verify() {
// If we don't accept returls, fail.
if(!resourceReturnable)
illogical(&cantPutAnythingInFactory);
// We accept returns but the dobj isn't our
// resource.
if(!isResource(gDobj) || !gDobj.ofKind(resourceClass))
illogical(&cantPutInNonResource);
}
// To do a return we do the default stuff--display message,
// remove object from actor's inventory--and then we move
// the object into nil; we don't try to cache instances,
// just dump and create a new one if we have to.
action() {
inherited();
gDobj.moveInto(nil);
}
}
// Resource creation method. By default we just create an instance
// of our resource class, but subclasses can fancy this up if
// needed.
createResource() {
return(resourceClass.createInstance());
}
initializeThing() {
inherited();
initializeFactory();
}
// Factory initialization. We create an instance of our resource
// and add it to ourselves. This is what saves us from
// >TAKE [resource] failing with "You see no [resource] here. ".
initializeFactory() {
if(contains({ x: x.ofKind(resourceClass) }))
return;
createResource().moveInto(self);
}
;
The class’ initializeThing() insures that there’s a “phantom” Resource instance in the factory to provide a match in scope for the resource’s vocabulary. We prevent the factory from listing its contents so it’s mostly invisble.
The additional logic is just the iobjFor(TakeFrom) and iobjFor(PutIn) stanzas, which handle creating new resource instances and disposing of them, respectively. The last behavior being optional and controlled by the resourceReturnable flag.
A working(-ish) example
Putting this all together into a complete example “game”:
#include <adv3.h>
#include <en_us.h>
#include "adv3Utils.h"
versionInfo: GameID;
gameMain: GameMainDef initialPlayerChar = me;
class Pebble: Resource '(small) (round) pebble*pebbles' 'pebble'
"A small, round pebble. "
;
startRoom: Room 'Void'
"This is a formless void containing a bucket of infinite pebbles. "
;
+me: Person;
+bucket: Fixture, ResourceFactory '(infinite) (pebbles) bucket' 'bucket'
"It contains...let's see...looks like an infinite number of
pebbles. "
resourceClass = Pebble
resourceReturnable = true
;
This gets us a plausible infinite pebble dispenser dispenser:
Void
This is a formless void containing a bucket of infinite pebbles.
>i
You are empty-handed.
>take pebble
You take a pebble from the bucket.
>take pebble
You take a pebble from the bucket.
>i
You are carrying two pebbles.
>drop pebble
You drop the pebble.
>l
Void
This is a formless void containing a bucket of infinite pebbles.
You see a pebble here.
>take pebble
You take the pebble.
>l
Void
This is a formless void containing a bucket of infinite pebbles.
>i
You are carrying two pebbles.
We can take as many pebbles as we want, and deference is given to other in-scope pebbles over the “phantom” pebble in the bucket.
However…
Trying again with counts
The problem comes when we try to take more than one pebble:
>restart
Do you really want to start over? (Y is affirmative) > y
Void
This is a formless void containing a bucket of infinite pebbles.
>take 3 pebbles
You don't see that many pebbles here.
…or, alternately…
>restart
Do you really want to start over? (Y is affirmative) > y
Void
This is a formless void containing a bucket of infinite pebbles.
>take pebble; take pebble; take pebble
You take a pebble from the bucket.
You take a pebble from the bucket.
You take a pebble from the bucket.
>take 3 pebbles
the pebble in the bucket: You take a pebble from the bucket.
your pebble: You are already carrying the pebble.
your pebble: You are already carrying the pebble.
>i
You are carrying four pebbles.
I’ve previously implemented workarounds for actions with counts when there aren’t enough simulation objects to satisfy noun resolution. For example, >DRAW 5 CARDS from a deck of cards, where the individual cards aren’t simulation objects.
This approach works fine if the noun/vocabulary being matched is only used for “virtual”/non-simulation objects. The problem here is that we have a mix of “real” and “virtual” objects.
Here’s what that looks like, although like I said I do not have this working:
Horrible mess that doesn’t actually work, don’t copy this
First create a couple actions with counts that shadow the stock >TAKE and >TAKE [dobj] FROM [iobj] actions:
DefineTAction(TakeCount) countAction = true;
VerbRule(TakeCount)
( 'take' | 'pick' 'up' | 'get' ) singleNumber dobjList
| 'pick' singleNumber dobjList 'up'
: TakeCountAction
verbPhrase = 'take/taking (what)'
;
DefineTIAction(TakeCountFrom) countAction = true;
VerbRule(TakeCountFrom)
( 'take' | 'get' ) singleNumber dobjList
( 'from' | 'out' 'of' | 'off' | 'off' 'of') singleIobj
| 'remove' singleNumber dobjList 'from' singleIobj
: TakeCountFromAction
verbPhrase = 'take/taking (what) (from what)'
;
Whenever these guys are used, the singleNumber in the productions will mean that gAction will have a non-nil numMatch property, and calling getval() on it will return the numeric value.
We also put a countAction flag on both of these actions. This could be handled more cleanly, but this is just to make writing checks easier.
Anyway, now we modify the resource and factory classes:
modify Resource
vocabLikelihood() {
if(!isInFactory(self)) return(0);
if(gAction && gAction.countAction) return(-100);
return(0);
}
dobjFor(TakeCount) {
verify() {
if(!isInFactory(self))
nonObvious;
}
action() {
if(!gAction.numMatch) return;
self.location.resourceCount = gAction.numMatch.getval();
replaceAction(TakeCountFrom, self, self.location);
}
}
dobjFor(TakeCountFrom) {
verify() { dangerous; }
}
;
modify ResourceFactory
resourceCount = nil
iobjFor(TakeCountFrom) {
verify() {}
action() {
local i, obj;
if(!resourceCount) return;
for(i = 0; i < resourceCount; i++) {
obj = createResource();
obj.moveInto(gActor);
}
defaultReport(&okayTakeResourceCount, resourceCount);
resourceCount = nil;
}
}
;
With the same gameworld as before, that gets us:
Void
This is a formless void containing a bucket of infinite pebbles.
>take 3 pebbles
You take three pebbles from the bucket.
Hurray! But try it again…
>undo
Taking back one turn: "take 3 pebbles".
Void
This is a formless void containing a bucket of infinite pebbles.
>take 3 pebbles
You take three pebbles from the bucket.
>take 3 pebbles
your pebble:
your pebble:
your pebble:
the pebble in the bucket: You take three pebbles from the bucket.
Ooof.
Thanks for coming to my TED Talk
That’s where I’m at. So…is there an obvious/clever way to handle this I’m missing?
I was half hoping typing all this up would rubber duck debug a solution for me but so far nope.