Trollbridge is a simple demo game in which the protagonist needs to get a troll out of the way so that he can cross the bridge that leads to his village.
The version given below uses the full version of the adv3Lite library, i.e. adv3Lite with all (or at least, most of) the optional modules. If you compare it with the minimal version you'll see that it uses a substantially wider range of classes to define simulation objects, at the gain of having to define fewer properties on those objects. This version is almost functionally identical to the other, however, except that it includes scoring, a status-line exit lister, an ABOUT menu, and a more sophisticated way of presenting the hints.
#charset "us-ascii" #include <tads.h> #include "advlite.h" versionInfo: GameID IFID = '23781944-8bbf-437f-9c67-4b1fafc1ed35' name = 'Trollbridge2' byline = 'by Eric Eve' htmlByline = 'by <a href="mailto:eric.eve@somewhere.com">Eric Eve</a>' version = '1' authorEmail = 'Eric Eve <eric.eve@somewhere.com>' desc = 'A version of the Trollbridge game using more of the adv3Lite library, as would probably more normally be the case.' htmlDesc = 'A version of the Trollbridge game using more of the adv3Lite library, as would probably more normally be the case.' showAbout() { aboutMenu.display(); "Done. "; } ; gameMain: GameMainDef /* Define the initial player character; this is compulsory */ initialPlayerChar = me showIntro() { "All you want to do is return home after a hard day's work in the fields, but just as you arrive at the bridge over the river that runs by your village, an unexpected obstacle presents itself...\b"; } ; /* * We don't define or use the aHdir function in this version of the game, * since the included exitList module will effectively do the job for us. */ /* The starting location; this can be called anything you like */ startroom: Room 'Road by Bridge' "The road from the south comes to an end at the edge of a bridge leading north across the river, whose bank continues to east and west. " north = bridge south = road east = eastBank west = westBank ; /* * The player character object. This doesn't have to be called me, but me is a * convenient name. If you change it to something else, remember to change * gameMain.initialPlayerChar accordingly. */ + me: Thing 'you' "You're a gangling young lad of eighteen. " isFixed = true person = 2 // change to 1 for a first-person game contType = Carrier ; /* * We use the Enterable class for the bridge so that an attempt to ENTER it. * will automatically be translated into an attempt to travel via the * object defined on its connector property. */ + Enterable 'bridge; big stone' "The big stone bridge spans the river. " connector = bridge vocabLikelihood = 10 dobjFor(Cross) asDobjFor(Enter) dobjFor(Board) asDobjFor(Enter) ; /* The custom River class is defined below */ + River ; bridge: Room 'Bridge' "From the centre of the bridge you can carry on north home to your village or back south down the road through the fields. Meanwhile the river rushes rapidly under the bridge, carrying the waters downstream to the east. " south = startroom /* * In adv3Lite we can attach a method directly to a directional exit * property; the method is then executed when the player character * attempts to move in the relevant direction. Here the effect is to * produce a winning ending to the game. */ north() { "You carry on across the bridge and head on to the village, back to home, hearth, and a good hearty meal. "; /* * Calling awardPointsOnce(); on an Achievement (defined below) causes * the points to be awarded, but only the first time the method is * called. */ homeAchievement.awardPointsOnce(); /* We use the finishGameMsg() function to end the game. */ finishGameMsg(ftVictory, [finishOptionUndo, finishOptionScore, finishOptionFullScore]); } ; /* * We make the river a Decoration so that attempting to do anything with it * except examining it is met with the response defined in its * notImportantMsg. */ + Decoration 'river; rushing; water' "The river is in full flood, carrying the water down from recent heavy rainfall in the highlands off to the west. " notImportantMsg = 'The river is too far beneath you. ' ; eastBank: Room 'River Bank (East)' "This section of riverbank is in danger of becoming flooded by the river pouring in full torrent from under the bridge to the east. Further south the field is quite dry, however. " west = startroom south = eastField ; /* The River class is defined below. */ + River ; /* The bottle is now a Container, which means that things can be put in it. */ + bottle: Container 'green bottle; (wine)' "It looks like it may be a wine bottle, but there's no label on it; perhaps the label washed off in the water. " smellDesc = "The bottle smells strongly of vinegar. " /* isFloating is a custom property we define just for this object */ isFloating = true initSpecialDesc = "A brown bottle floats in the river just out of reach, apparently caught by the reeds, perhaps after being thrown into the river. " dobjFor(Take) { /* * In adv3Lite a check() routine need only display some text to * prevent the action from going ahead. */ check() { if(isFloating) "The bottle is just out of reach. "; } } dobjFor(MoveWith) { action() { if(isFloating) { "You manage to snag the bottle with {the iobj} and bring it ashore. "; isFloating = nil; moved = true; bottleAchievement.awardPointsOnce(); } else inherited; } } contentsListed = !isFloating ; ++ vinegar: Thing 'some wine; murky red ;vinegar liquid' "It's a kind of murky red colour. " isPourable = true dobjFor(PourInto) { action() { "You pour the vinegar into {the iobj}. "; moveInto(gIobj); } } smellDesc = "The wine smells sour; it must be turning to vinegar. " tasteDesc = "It tastes very sour, more like vinegar than wine. " cannotDrinkMsg = 'The wine is undrinkable; it\'s more or less vinegar now. ' /* * If isProminentSmell is true, then the smellDesc of this object will be * displayed in response to an intransitive SMELL command (as well as * SMELL VINEGAR). */ isProminentSmell = !isIn(bottle) ; + Decoration 'reeds;submerged;plants;them' "The reeds are more than half submerged by the river. " ; westBank: Room 'River Bank (West)' "The riverbank, which continues to the east is particularly thick with plants at this point. The way north is blocked by the river running rapidly by, but to the south lies an open field. " east = startroom south = westField ; + River ; /* By making the plants a Fixture we ensure that they can't be taken. */ + plants: Fixture 'plants;green;reeds;them' "Most of them are just reeds<<if monkshood.isIn(nil)>>, but every now and again you fancy you detect a hint of purple among them<<end>>. " /* * Objects listed in the hiddenIn property will be discovered in response * to a SEARCH or LOOK IN command. */ hiddenIn = [monkshood] /* * If autoTakeOnFindHidden were true then SEARCHing the plants would * result in the contents of its hiddenIn list being automatically taken * by the player character. By making it nil we instead move the items in * the hiddenIn list to the location of the plants. */ autoTakeOnFindHidden = nil lookInMsg() { if(monkshood.isIn(location)) return 'As you\'ve already discovered, there\'s some monkshood growing among the reeds. '; else return 'Now you\'ve taken the monkshood there\'s not much there but reeds. '; } ; + purple: Decoration 'hint of purple' "It looks like there may be something purple there. " notImportantMsg = 'You\'ll need to take a closer look first. ' ; monkshood: Thing 'some monkshood; purple; flowers aconitum' "You recognize the purple leaves as Monkshood, a plant that can be deadly poisonous in any quantity, especially if made up into a tincture. " dobjFor(Take) { check() { if(gloves.wornBy != gActor) "You know better than to pick monkshood with your bare hands; it would be too easy for the poison to seep into your skin. "; } } dobjFor(Pick) asDobjFor(Take) actionMoveInto(cont) { inherited(cont); monksAchievement.awardPointsOnce(); } initSpecialDesc = "Some purple monkshood grows among the plants. " ; road: Room 'Road' "This long road runs north -- south through cornfields to east and west. " north = startroom south { "That would take you too far in the wrong direction; there has to be a solution closer to hand. "; } east = eastField west = westField ; eastField: Room 'East Field' "The field has been recently harvested, leaving only the stubble of corn behind. A small hut stands off to one side; to the west is the road, while the river is a little distance to the north. " west = road north = eastBank in = hutDoorOutside ; /* * Making the hut an Enterable means that any attempt to enter the hut will be * translated into an attempt to travel via the object defined on its * connector property, in this case the hut's outside door. */ + Enterable 'small hut; wooden (tool); shed toolshed' "The small wooden hut is nothing special; it's probably little more than a toolshed for the people working this field. " connector = hutDoorOutside /* * The remapIn property is a quick way of redirecting a whole set of * actions to an associated container object, but it works just as well * with a door object. This definition will redirect commands like OPEN, * CLOSE, LOCK, UNLOCK and ENTER from the hut to the door. */ remapIn = hutDoorOutside ; /* * Any door must be of the Door class, and must be linked to the object * representing the other side of the door through its otherSide property. */ ++ hutDoorOutside: Door '(hut) door' otherSide = hutDoorInside keyList = [ironKey] ; + rock: Thing 'large rock; irregular dark grey gray of[prep]; lump' "It's a large irregular dark grey lump of rock. " dobjFor(Take) { check() { if(isStuck) "You can't quite lift it; it seems to be stuck to the ground, probably by the dried mud encrusted around it. "; } } dobjFor(LookUnder) { check() { checkDobjTake();} } initSpecialDesc = "A large rock lies on the ground a short distance from the hut. " /* * Anything in the hiddenUnder property will be discovered and moved into * the location of the rock when the player character either takes or * looks under the rock. */ hiddenUnder = [ironKey] /* * By default we can't pour things on other things, but if allowPourOntoMe * is true then we can pour something that's pourable onto this Thing. */ allowPourOntoMe = true /* Here, isStuck is a custom property */ isStuck = true ; /* * Again we use the Fixture class to ensure the mud can't actually be taken. * This is equivalent to defining isFixed = true on the object. Either way an * attempt to take it will be met with the response definined in the * cannotTakeMsg property. */ + driedMud: Fixture 'dried mud; encrusted hard solid' "The mud encrusted round the rock looks like it's dried as hard as cement. " feelDesc = "The mud feels very hard indeed. " cannotTakeMsg = 'It\'s dried solid, making it too hard to move. ' /* * This short form of remap (the only legal use of remap in adv3Lite) * remaps POUR X ON MUD to POUR X ON ROCK. */ iobjFor(PourOnto) { remap = rock } ; + Decoration 'stubble; harvested; corn' "Only the stubble is left. " notImportantMsg = 'What\'s left of the corn now that it\'s been harvested isn\'t worth bothering with. ' ; /* Anything that is to act as a key must be of the Key class. */ ironKey: Key 'iron key' moveInto(cont) { inherited(cont); keyAchievement.awardPointsOnce(); } ; hut: Room 'Hut' "This small hut looks like it's used as a convenient storage place for whoever works this field. A single wooden shelf runs along one wall opposite the door. " out = hutDoorInside ; + hutDoorInside: Door 'door' otherSide = hutDoorOutside keyList = [ironKey] ; /* * Making something a Surface means that objects can be placed on top of it. * Making it a Fixture prevents it from being taken. We can use multiple * inheritance as here to define an object that belongs to both classes. We * could have got the same effect by defining isFixed = true and contType = On * on a Thing, but here we're illustrating the use of the additional classes * defined in the full version of adv3Lite. */ + Surface, Fixture 'shelf; plain wooden' ; /* * Making the gloves of class Wearable allows them to be worn. This is * equivalent to defining isWearable = true. */ ++ gloves: Wearable 'gloves; worn leather of[prep]; pair; them it' "They're just a pair of worn leather gloves. " ; ++ shears: Thing 'pair of shears; sharp sharpened;;it them' "They look like they've been recently sharpened. " canCutWithMe = true canMoveWithMe = true ; westField: Room 'West Field' "The field to the west of the road has yet to be harvested; the high standing corn waves gently in the evening sunshine all around. Off to the north the field slopes down to the riverbank, while the main road lies just to the east. A chicken coop squats in a corner of the field. " east = road north = westBank ; /* * Making the coop a Container means that things can be put in it; making it a * Fixture ensures that it can't be taken. */ + coop: Container, Fixture 'chicken coop;; wire' "The chicken coop is made mainly of wire. It looks as if it has recently been repaired, perhaps after a fox managed to get in. " /* * A Container is open by default, but we want this one to start out * closed, so that the player character can't immediately get at the dead * hen. */ isOpen = nil /* * We make the coop transparent so that the player character can see that * there's a dead hen in it even when it's closed. */ isTransparent = true cannotOpenMsg = 'Whoever carried out the repairs seems to have been a bit over-zealous; there\'s now no obvious way to open the coop. ' isCuttable = true dobjFor(CutWith) { check() { if(isOpen) "You've already cut your way into the coop; there's no need to cut it any further. "; } action() { isOpen = true; "You cut an opening in the coop with {the iobj}. "; } } ; ++ hen: Thing 'dead hen;; carcass' "It's dead; it looks like a fox must have got it. " specialDesc = "A hen lies on the floor of the coop. " isPoisoned = nil moveInto(cont) { inherited(cont); henAchievement.awardPointsOnce(); } ; /* * We make the bucket a Container so that things can be put in it. This is * equivalent to defining contType = In and isOpen = true (plain Containers, * unlike OpenableContainers, start out open by default). */ + bucket: Container 'bucket; plain old wooden' "It's just a plain old wooden bucket. " isFillable = true afterAction() { if(vinegar.isIn(self) && monkshood.isIn(self) && hen.isIn(self) && !hen.isPoisoned) { "The noxious tincture of monskshood in vinegar starts seeping into the carcass of the hen. "; hen.isPoisoned = true; } } allowPourIntoMe = true canMoveWithMe = true initSpecialDesc = "A bucket lies on the ground by the side of the chicken coop. " ; + Decoration 'some standing corn; ripe' "It's ripe and ready to be harvested. " notImportantMsg = 'The corn is best left to the harvesters. ' ; /* * Water is rather an odd sort of object to simulate. Here we define it with * isFixed = true so that it can't be picked up but isListed = true so that * it's listed when present (normally making isFixed true would make isListed * nil, i.e. false). */ water: Thing 'some water' "It's reasonably clear, without too much silt from the river. " isFixed = true isListed = true cannotTakeMsg = 'The water runs through your fingers. ' isPourable = true dobjFor(Pour) { action() { askForIobj(PourOnto); } } dobjFor(PourOnto) { action() { "You pour the water onto {the iobj}. "; if(gIobj is in (rock, driedMud) && rock.isStuck) { rock.isStuck = nil; "This appears to loosen some of the dried mud holding the rock. "; } moveInto(nil); driedMud.moveInto(nil); } } dobjFor(Drink) { action() { "You scoop up a few handfuls of the water to slake your thirst. "; } } tasteDesc = "It doesn't taste too bad. " isDrinkable = true ; /* * Define our custom River class. Objects of this class can't be taken or * entered but they can be used at as the indirect objects of a FillWith * command (e.g. FILL BUCKET WITH WATER or FILL BUCKET FROM RIVER). */ class River: Fixture 'river;;water current' "The river is in full flood, carrying the water down from recent heavy rainfall in the highlands off to the west. Even a strong swimmer would be in danger of being carried away by that current, and you're not any kind of swimmer. The river is in any case far too deep to ford. " isFixed = true cannotTakeMsg = 'The water runs through your fingers. ' cannotEnterMsg = 'You never learned how to swim. ' iobjFor(FillWith) { verify() { if(water.isIn(bucket)) illogicalNow('The bucket is already full of water. '); } action() { "{I} fill{s/ed} {the dobj} with water from the river. "; water.moveInto(gDobj); } } dobjFor(Cross) asDobjFor(Enter) ; /* * The troll is a fairly simple NPC, but in this version we'll implement him * using the adv3Lite Actor class etc. */ troll: Actor 'troll; big ugly;; him' @startroom "Like all full-grown trolls, this one is very big and very ugly. " allowAttack = true dobjFor(Attack) { check() { "The troll is almost twice your size and four times your strength; in any attempt at combat you'd be sure to come off second best. "; } } iobjFor(ThrowAt) { action() { gDobj.actionMoveInto(location); "The troll swats {the dobj} angrily aside and snarls at you. "; } } ; /* * An ActorState represents one of a number of states an Actor can be in. The * troll is a simple NPC with only one state, but a more complex NPC would * have several, reflecting its changing behaviour and appearance at different * times. By also making this ActorState a StopEventList we can have it * display each of the messages in its eventList property in turn (repeating * the final one ad infinitum); this provides a neater method of achieving * what was done with the afterAction() method and switch statement in the * advLiter version. */ + trollBridgeState: ActorState, StopEventList isInitState = true specialDesc = "A big ugly troll guards the bridge. " beforeTravel(traveler, connector) { if(traveler == gPlayerChar && connector == bridge) { "The trolls snarls and blocks your path. "; exit; } } eventList = [ 'The troll finishes gnawing on the bones of some defunct animal --- a hen perhaps --- and tosses them contemptuously into the river. ', 'The troll lets out a loud belch. ', 'The troll looks around for something else to eat. ', 'The troll starts staring at you with a worryingly hungry look. ', 'The troll continues to stare hungrily at you as if he\'s pondering how tasty you might be. ' ] ; /* * A GiveShowTopic defines the actor's response to being given or shown * something, in this case a hen. The AltTopic that's defined immediately * after defines the alternative response that's used when its isActive * property is true (i.e. what happens when we give the hen to the troll when * the hen is poisoned). In adv3Liter we had to define special handling for * the GiveTo action on the troll, with an additional method on the hen. */ ++ GiveShowTopic @hen topicResponse() { "The troll snatches the hen from your grasp and starts chewing on it. He finishes it off in a few mouthfuls, then his hunger apparently provoked rather than sated, he makes a grab for you and starts tearing your limbs off to start chewing on them. Mercifully you quickly lose consciousness. "; finishGameMsg(ftDeath, [finishOptionUndo, finishOptionScore, finishOptionFullScore]); } ; +++ AltTopic topicResponse() { "The troll grabs the hen from you and stuffs it into his mouth. Almost immediately he clutches at his stomach and groans in agony, his cries crescendoing to shrieks as he leans over the parapet to empty the contents of his stomach into the river. Either because he leans too far, or because the poison has already done its work, he falls over the parapet into the river and is rapidly carried off by the current, disappearing out of sight as he's pulled under the water. "; hen.moveInto(nil); troll.moveInto(nil); trollAchievement.awardPointsOnce(); } isActive = hen.isPoisoned ; /* The troll's response to being offered anything other than the hen. */ ++ DefaultGiveShowTopic topicResponse() { "The troll takes one look at {the dobj} and casts {him dobj} contemptuously aside. "; gDobj.moveInto(location); } ; /* The troll's response to all other conversational commands. */ ++ DefaultAnyTopic "The troll merely grunts threateningly; trolls never were great conversationalists. " ; /* Define a custom FillWith action */ DefineTIAction(FillWith) ; /* Define the grammar for the FillWith action */ VerbRule(FillWith) 'fill' singleDobj ('with' | 'from') singleIobj : VerbProduction action = FillWith verbPhrase = 'fill/filling (what) (with what)' missinqQ = 'what do you want to fill; what do you want to fill it with' ; /* * In the adv3Liter version we handled PICK MONKSOOD and CROSS BRIDGE via a * StringPreParser, which is a less than fully adequate way of doing it. Here * we do it properly by defining custom PICK and CROSS actions. */ DefineTAction(Pick) ; DefineTAction(Cross) ; VerbRule(Pick) 'pick' singleDobj : VerbProduction action = Pick verbPhrase = 'pick/picking (what)' missingQ = 'What do you want to pick' ; VerbRule(Cross) 'cross' singleDobj : VerbProduction action = Cross verbPhrase = 'cross/crossing (what)' missingQ = 'What do you want to cross' ; /* * Modifications to the base Thing class to define default handling for our * custom FillWith, Pick and Cross actions. */ modify Thing dobjFor(FillWith) { preCond = [objHeld] verify() { if(!isFillable) illogical(cannotFillMsg); if(gIobj == self) illogicalSelf(cannotFillWithSelfMsg); } } iobjFor(FillWith) { preCond = [touchObj] verify() { illogical(cannotFillWithMsg); } } isFillable = nil cannotFillMsg = '{The subj dobj} {is}n\'t something that can be filled. ' cannotFillWithMsg = '{I} {can\'t} fill anything with {that iobj}. ' cannotFillWithSelfMsg = '{I} {can\'t} fill {the dobj} with {himself dobj}. ' dobjFor(Pick) { preCond = [touchObj] verify() { illogical(cannotPickMsg); } } dobjFor(Cross) { preCond = [touchObj] verify() { illogical(cannotCrossMsg); } } cannotPickMsg = '{I} {can\'t} pick {that dobj}. ' cannotCrossMsg = '{I} {can\'t} cross {that dobj}. ' ; /* * A Doer is an object that can intercept a command and make it do something * different from the normal action handling. */ Doer 'put bucket in River' execAction(c) { doInstead(FillWith, bucket, gIobj); } ; /* * Instead of using a Doer we can now use the built-in hint system to set up * the same simple series of hints */ hintMenu: TopHintMenu 'Hints'; + Goal 'How do I get past the troll?' [ 'The troll is hungry; this is one troll you should feed, preferably with something that won\'t do him any good. ', 'You\'ve seen the troll polish off a dead hen, and it shouldn\'t be too hard to find another one nearby; but you\'ll have to poison it somehow. ', 'If you search along the river bank you should find the means of poisoning the hen -- a poisonous plant that can be made into a tincture with the aid of some sour wine. ', 'In order to pick the poisonous plant and get into the hen coop you need some items that are in the hut. The key is hidden not far from the hut, but you need to loosen something to get at it. How might you dispose of the dried mud? ', 'Fill the bucket with water from the river and pour it on the rock (or the mud).', 'You can use either the bucket or the shears to move the bottle close enough inshore to take. ', 'Use the bucket to mix your tincture: put the monkshood in the bucket, pour the wine/vinegar in and then put the hen in. The hen will then be a meal fit for a troll! ' ] goalState = OpenGoal ; /* * We'll also add some scoring; each Achievement object marks something the * player has to do to obtain the associated points. The points are awarded by * calling the awardPointsOnce() method on the Achievement object. */ keyAchievement: Achievement +1 "finding the key to the hut"; bottleAchievement: Achievement +1 "getting the bottle"; trollAchievement: Achievement +1 "poisoning the troll"; henAchievement: Achievement +1 "obtaining the dead hen"; monksAchievement: Achievement +1 "picking the monkshood"; homeAchievement: Achievement +1 "getting safely back home"; /* * Finally we'll create a menu for use with the ABOUT command. In addition to * displaying general instructions on playing IF and our hints menu, it'll * offer our custom MenuLongTopicItem giving a brief description of the game. */ aboutMenu: MenuItem contents = [topInstructionsMenu, hintMenu] ; + MenuLongTopicItem 'About Trollbridge 2' 'Trollbridge 2 is a demonstration of adv3Lite using more of the core library than the basic Trollbridge game and making use of a few more features.\b The immediate difference you\'ll see is the status line exit lister. This version of the game also keeps score, and in this version you can use the GO TO command (e.g. GO TO BRIDGE) to navigate round the map to rooms you\'ve already visited. ' ;