Simulationist IF in TADS?

13 views
Skip to first unread message

>cox.net

unread,
May 8, 2002, 1:23:58 AM5/8/02
to
The game I'm currently working on, because of its plot, requires quite
a bit of simulationist type coding. So far I have object interactions
based on a range of properties like liquid, wood, hard, earth, fragile
etc. (ie breaking wood with a hard object (metal or stone) works just
fine but trying to break a hard object with a wood object just sends
one heck of a shock up the PC's arm) I also have some burning
functions that deal with these properties but the burning bit still
has a long way to go.

I know there are a few well done examples of simulationist IF written
in INFORM but I was wondering if anyone has done something similar in
TADS? It would be quite helpful to have some code to compare to what
I've got so far to get some new ideas and to help predict possible
problems farther down the line.


---Daniel

Left, left, I hadda good brain but it left...

Jim Aikin

unread,
May 8, 2002, 1:49:06 AM5/8/02
to
Seems to me your main problem is probably going to be our old friend the
combinatorial explosion. Once you implement "break," every object in the
game needs either to be breakable or to have a sensible "you can't break
it" message.

Trouble is, most things in the real world are breakable, given enough
force. And those that aren't breakable may have different reasons for
not being breakable. "break the water" needs a different "you can't do
that" message from "break the girder", while "break the schoolteacher"
requires yet a third response.

But let's suppose the object is breakable. Here there are at least two
basic options, I guess. Option 1: When the vase gets broken, move it to
nil and replace it with a "broken vase" object. This is preferable with
a vase, because what you want the user to encounter is no longer a vase
at all but a bunch of porcelain shards. Option 2: Write all of the code
for the vase in a way that's sensitive to whether self.broken is true.

Both options are ugly. If you allow objects to be broken, burnt,
painted, wet, etc., you're going to have to use option 2, because
otherwise you have one dry/painted/broken object, another
wet/unpainted/broken object, etc.

Right now I'm trying to code some little critters that can be alive or
dead, and that's entirely bad enough, because every verb you can use on
them has to differentiate between the two states, so as to avoid this:

> pet the dead chipmunk
The chipmunk wriggles ecstatically as you stroke it.

Or worse (if your code uses <<self.thedesc>> carelessly), this:

> pet the dead chipmunk
The dead chipmunk wriggles ecstatically as you stroke it.

You may just be talking about fixed qualities in the objects, though.
That's easier, because you can modify the 'thing' class with stuff like
this:

verDoPaint (actor) =
{
if (self.painted) "It's already been painted. ";
}

That will cover every object in the game in 2 lines of code.

This is an interesting general problem/topic, though. I'll be curious
how others have handled it.

--Jim Aikin

>cox.net

unread,
May 8, 2002, 2:08:49 AM5/8/02
to
On Wed, 08 May 2002 05:49:06 GMT, Jim Aikin
<kill_spammers@kill_spammers.org> wrote:

>Seems to me your main problem is probably going to be our old friend the
>combinatorial explosion. Once you implement "break," every object in the
>game needs either to be breakable or to have a sensible "you can't break
>it" message.

Actually when you get right down to writing the code you'd be suprised
how few different possibilities there are. I already have a
successfully functioning break property that works for every item in
the game. And I did it without writing specific code for every item. I
also have a working Burn function that can handle everything except
liquids (which I plan to code separately since they do weird things).

>Trouble is, most things in the real world are breakable, given enough
>force. And those that aren't breakable may have different reasons for
>not being breakable. "break the water" needs a different "you can't do
>that" message from "break the girder", while "break the schoolteacher"
>requires yet a third response.

Most things in the real world are breakable, but I'm begining to find
that the percentage of breakable things in the average IF game is
significantly less than this.

>But let's suppose the object is breakable. Here there are at least two
>basic options, I guess. Option 1: When the vase gets broken, move it to
>nil and replace it with a "broken vase" object. This is preferable with
>a vase, because what you want the user to encounter is no longer a vase
>at all but a bunch of porcelain shards. Option 2: Write all of the code
>for the vase in a way that's sensitive to whether self.broken is true.

I'm currently using option one but with some characteristics of option
two. Also I plan to share code between object states; by redirecting
function calls what happens to one state of the object happens to both
so painting a chair that is intact also paints a hypothetical future
chair in a broken state that exists after the player starts swinging
an axe around. I've used similar approaches to this type of code
problem before so it should work fine.

<chipmunks snipped>

Stephen L Breslin

unread,
May 8, 2002, 4:22:45 PM5/8/02
to
Hi folks, Daniel,

I think you are going about it in the right way. I would recommend
(and I think this is what you're doing) that you approach the task as
a library extension rather than simply as a game. Library extensions
attempt to provide "generic" or general-purpose functionality, without
trying to address specific problems with unique game objects, whereas
games sometimes tend to streamline the functionality based on what is
needed in the game, and don't bother to generalise. The latter
technique works fine too, but it's often messier, it's harder to
playtest, and finally it's harder to read the code. (That's probably a
prejudice on my part.)

Jim wrote, above on this thread:

>[...] You can modify the 'thing' class with stuff like
>this:

>verDoPaint (actor) =
>{
> if (self.painted) "It's already been painted. ";
>}

>That will cover every object in the game in 2 lines of code.

This is an essential approach, especially if you are going to have a
large number of objects. It will make things work properly on a
general level, and burden your playtesters with less responsibility
(and less work).
Then, working your way down the inheritance tree, you'd get to
liquids, for instance, which would have a different verification
scheme for painting ("You can't paint liquids." would be a nice and
general statement, which subclasses of liquids could modify further --
or even reverse in object-specific cases). You might want an
Unpaintable class, with a generic verification method ("You can't
paint that."); you might want a PaintRepellant class ("The paint
doesn't stick to that object."). Then for doPaint(actor), you'd want a
generic something like this:

doPaint(actor, iobj) = {
"You paint <<self.thedesc>> a shade of <<iobj.color>>. ";
self.color := iobj.color;
}

This then would be replaced by subclasses of Thing wherever such
replacement would make sense. Finally, for the game objects, you would
override this where you want unique-object-specific behavior. I
wouldn't skip to this final step without doing the generic behavior
stuff. Again, I expect this is what you're already doing, at least in
part.

You quoted Jim, who wrote:
>>[...] Once you implement "break," every object in the

>>game needs either to be breakable or to have a sensible "you can't break
>>it" message.

Right. And as you mentioned in your initial post, you have different
break actions for different materials (checking what material you're
trying to break the object with, and what material the object is).
This is great. I'm not sure how you're implementing this, but I have a
suggestion for dampening what Jim calls the pains of "combinatorial
expansion". You could modify Thing along these lines:

modify thing

isBroken = nil
hardness = 2 // this will be overriden by subclasses.
fragile = nil // you could economize: fragile object=0 hardness

verDoBreak(actor) {
if (self.isBroken) {
"<<self.thedesc>> is already broken. "
}
}

doBreak(actor, iobj) {
if (self.hardness < iobj.hardness || self.fragile) {
"<<self.thedesc>> breaks.";
self.isBroken := true;
}
else
"<<iobj.thedesc>> is not hard enough to break
<<self.thedesc>>. ";
if (iobj.hardness < self.hardness || iobj.fragile) {
"<<iobj.thedesc>> is broken in the attempt. ";
iobj.isBroken := true;
}
}
;

Now you're in a position to set up different classes of materials with
different hardnesses. You can always customize the behavior of
specific subclasses of materials, but you'll have a generic mechanism
to fall back on.

>Actually when you get right down to writing the code you'd be suprised
>how few different possibilities there are. I already have a
>successfully functioning break property that works for every item in
>the game. And I did it without writing specific code for every item. I
>also have a working Burn function that can handle everything except
>liquids (which I plan to code separately since they do weird things).

I expect that what you did is similar to my suggestion above (if not
better).

>>But let's suppose the object is breakable. Here there are at least two
>>basic options, I guess. Option 1: When the vase gets broken, move it to
>>nil and replace it with a "broken vase" object. This is preferable with
>>a vase, because what you want the user to encounter is no longer a vase
>>at all but a bunch of porcelain shards. Option 2: Write all of the code
>>for the vase in a way that's sensitive to whether self.broken is true.

>I'm currently using option one but with some characteristics of option
>two. Also I plan to share code between object states; by redirecting
>function calls what happens to one state of the object happens to both
>so painting a chair that is intact also paints a hypothetical future
>chair in a broken state that exists after the player starts swinging
>an axe around. I've used similar approaches to this type of code
>problem before so it should work fine.

You could modify class Chair to behave properly when broken. The
verDoSitIn(actor) would check (self.isBroken), and give appropriate
responses. You'd probably want to change verDoBreak(actor) to check if
the actor is sitting in the chair, reporting "You'll have to stand up
first." as appropriate.
Under certain circumstances, you will decide it's better to move the
chair into nil, and move a replacement object 'broken chair' into the
chair's initial place. That would be something I would do on a
case-specific basis, and I'd have a generic system for handling broken
objects which does something much simpler (as simple as possible),
which you could override in particular cases.

One final technique I've seen in some simulationist IF: a gradual
modification of state. Your Burnable might deal with this. I will use
for an example a Bleachable class.

class Bleachable: thing // mix-in class
color = 'burple' // overriden by inheritors of this class.
noun = 'thing' // overriden by inheritors of this class.
bleachedCounter = 1
bleachedColorList = ['dark', 'medium-colored', 'lightening',
'light', 'very light', '']

/* You could handle this more cleverly, but here's the idea. */
sdesc = "<<self.bleachedColorList[bleachedCounter]>>
<<self.color>> <<self.noun>>"

verDoBleach(actor) {
if (self.color = white) {
"It's already as white as can be. ";
}
}

doBleach(actor) {
"You bleach the <<self.noun>>. ";
++self.bleachedCounter;
if (bleachedCounter = 6) {
self.color := 'white';
}
}
;


Food for thought,
Steve Breslin

>---Daniel

(I didn't test any of the code here, so there's probably errors, and
maybe egregious ones. I only wrote the code to clarify conceptual
stuff; it's not for actual use in games or libraries.)

Ally

unread,
May 8, 2002, 2:50:37 PM5/8/02
to
On 08 Mai 2002, Stephen L Breslin <bre...@acsu.buffalo.edu> wrote in
news:3cd97619...@news.buffalo.edu:

<snip snap snup>

The way I'm doing it, that is, how I set up the library extension stuff
that deals with breaking, breaking apart, tearing, slicing, etc. pp., is
through dynamic object creation, which saves me the hassle of coding up
overly verbose verDoCutWith() etc. methods as there is no need for
the (say) the original chair to know if it's been broken or can still be
sat on or used as a surface. Every "debris" object is an object in its own
right and with its own verb handlers, but equipped with some or all of the
vocabulary of the object's previous incarnation. If I (say) charred a
wooden chair, adding "charred" to its adjectives, and then broke it, its
doBreak() would produce a (say) wood_debris class object that could then
be referred to as "debris of the charred wooden chair", "broken chair",
"wooden debris", and so on. More complex objects can specify pre-coded
debris objects rather than creating instances of default classes for
glass, wood, porcelain, ... ...and of course you have to take into account
that breaking (or burning, throwing, etc.) containers might affect their
contents as well, and the resulting broken container might still be
useable as a container, albeit no longer lockable, opaque, or waterproof
...

~Ally (throwing about small change...)

--
"It is bone-deep with her, though buried and frozen."

>cox.net

unread,
May 8, 2002, 3:24:51 PM5/8/02
to
On Wed, 8 May 2002 20:50:37 +0200, Ally <kitzapoo...@gmx.co.uk>
wrote:


>The way I'm doing it, that is, how I set up the library extension stuff
>that deals with breaking, breaking apart, tearing, slicing, etc. pp., is
>through dynamic object creation, which saves me the hassle of coding up
>overly verbose verDoCutWith() etc. methods as there is no need for
>the (say) the original chair to know if it's been broken or can still be
>sat on or used as a surface. Every "debris" object is an object in its own
>right and with its own verb handlers, but equipped with some or all of the
>vocabulary of the object's previous incarnation. If I (say) charred a
>wooden chair, adding "charred" to its adjectives, and then broke it, its
>doBreak() would produce a (say) wood_debris class object that could then
>be referred to as "debris of the charred wooden chair", "broken chair",
>"wooden debris", and so on. More complex objects can specify pre-coded
>debris objects rather than creating instances of default classes for
>glass, wood, porcelain, ... ...and of course you have to take into account
>that breaking (or burning, throwing, etc.) containers might affect their
>contents as well, and the resulting broken container might still be
>useable as a container, albeit no longer lockable, opaque, or waterproof
>...
>
>~Ally (throwing about small change...)
>
>--
>"It is bone-deep with her, though buried and frozen."


That sounds as if it might be a bit nicer than what I have. I was
thinking about doing this with the burn function since most burned
items end up with similar characteristics but I hadn't thought of a
completely dynamic system.

My current setup defines objects by five types: wood, liquid, hard,
earth, or fragile. Not all objects fall into these categories
conceptually but in practice just about every object falls into one of
these categories so far as how it reacts to burning and breaking etc.
The functions then print out appropriate messages for the various
combinations and if something is broken or burned or otherwise changed
the property of the particular item corresponding to what has happened
to it is used to replace the item.

So for instance if a wooden table is chopped up with an axe the
program returns "You swing the axe violently and chop the table into
bits." Then it moves the table to nil and Table.broken gets moved into
Table.location (or actually the other way around since that wouldn't
work out very well if you moved the original table to nil first.)

Combinational state changes are handled the exact same way TADS
handles doors with the otherside property. That is anything that
happens to the table that would still be noticeable after the table
were chopped to bits also happens to Table.broken which is defined as
a BrokenTable object that gets handled as I described in the previous
paragraph.

All of this is fine and works beautifully but I suspect your dynamic
creation approach requires a significantly smaller amount of code.
I'll have to spend some time playing around with the idea and see how
it compares to what I have at the moment.

Phil Lewis

unread,
May 12, 2002, 2:25:17 AM5/12/02
to
Ally wrote:
> The way I'm doing it, that is, how I set up the library
> extension stuff that deals with breaking, breaking
> apart, tearing, slicing, etc. pp., is through dynamic
> object creation, which saves me the hassle of coding up
> overly verbose verDoCutWith() etc. methods as there is
> no need for the (say) the original chair to know if it's
> been broken or can still be sat on or used as a surface.
> Every "debris" object is an object in its own right and
> with its own verb handlers, but equipped with some or
> all of the vocabulary of the object's previous
> incarnation. If I (say) charred a wooden chair, adding
> "charred" to its adjectives, and then broke it, its
> doBreak() would produce a (say) wood_debris class object
> that could then be referred to as "debris of the charred
> wooden chair", "broken chair", "wooden debris", and so
> on. More complex objects can specify pre-coded debris
> objects rather than creating instances of default classes
> for glass, wood, porcelain, ... ...and of course you have
> to take into account that breaking (or burning, throwing,
> etc.) containers might affect their contents as well, and
> the resulting broken container might still be useable as
> a container, albeit no longer lockable, opaque, or
> waterproof

FWIW, I used ally's approach as a model for simple fragile/debris classes in
TADS 3 and have found it to be very flexible (though I haven't delved into
it as deeply as breaking contained objects etc.) Once the generic classes
are set-up and working properly, the game code can create lots of breakable
things with very little coding in many cases.

Also, I suggest making all the default text that gets output by the generic
classes into properties. Instead of printing specific text when the action
is performed, use a dobjBreaksDesc property, for example, and it makes life
easier when specific instances need to override the defaults.

Phil


>cox.net

unread,
May 12, 2002, 2:29:57 AM5/12/02
to
On Sun, 12 May 2002 02:25:17 -0400, "Phil Lewis"
<phillewis@remove_this.fuse.net> wrote:

<snip


>Also, I suggest making all the default text that gets output by the generic
>classes into properties. Instead of printing specific text when the action
>is performed, use a dobjBreaksDesc property, for example, and it makes life
>easier when specific instances need to override the defaults.
>
>Phil
>
>

A perfectly timed post! I was just recently having trouble with this
and looking for a way to avoid rewritting entire functions to change
one or two inappropriate default outputs. Using property values is an
excellent solution. Thanks.

Reply all
Reply to author
Forward
0 new messages