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 only the features of "adv3Litest", i.e. adv3Lite in its minimal form with no optional modules. The most striking feature of this implementation is that, with one or two exceptions noted in the comments, virtually every simulation object defined in the game is either of class Thing or of class Room. You may want to compare this minimal version with one making use of the full library.
#charset "us-ascii" #include <tads.h> #include "advlite.h" versionInfo: GameID IFID = '1ca72958-83eb-41d1-b7e6-ae3afbc9edbf' name = 'Trollbridge' 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 demonstration of writing a game in adv3Lite using only the core library modules.' htmlDesc = 'A demonstration of writing a game in adv3Lite using only the core library modules.' showAbout() { "<i><<name>></i> is a demonstration of using adv3Lite in its minimalist form, with only the compulsory core modules. "; } ; 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"; } ; /* * Since the minimalist core adv3Lite library doesn't include the exit lister * module, we'll define a function that can be used to hyperlink exits in room * description text. */ aHdir(dirn) { return aHref(dirn, dirn, 'GO ' + dirn.toUpper); } /* The starting location; this can be called anything you like */ startroom: Room 'Road by Bridge' "The road from the <<aHdir('south')>> comes to an end at the edge of a bridge leading <<aHdir('north')>> across the river, whose bank continues to <<aHdir('east')>> and <<aHdir('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 ; /* * The troll is a fairly simple NPC, and using 'minimalist' adv3Lite we can * just define him using a Thing. */ + troll: Thing 'troll; big ugly;; him' "Like all full-grown trolls, this one is very big and very ugly. " /* * The specialDesc gives the troll a paragraph of its own in the room * description. */ specialDesc = "A big ugly troll guards the bridge. " /* * The beforeTravel() method is called whenever travel is attempted by an * object in scope. Here we use it to make the troll block access to the * bridge. */ beforeTravel(traveler, connector) { if(traveler == gPlayerChar && connector == bridge) { "The trolls snarls and blocks your path. "; exit; } } /* * The cannotTalkToMsg will be used in response to any attempt to address * any conversational command to the troll. */ cannotTalkToMsg = 'The troll merely grunts threateningly; trolls never were great conversationalists. ' /* It's possible to try attacking the troll */ isAttackable = true /* But it's not advisable to attack the troll. */ dobjFor(Attack) { /* * In adv3Lite, displaying a message at the check stage is enough to * prevent the action from going ahead. */ 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. "; } } /* * The afterAction() method is called on every object in scope after an * action is carried out; here we use it to display a series of messages * about what the troll is doing. */ afterAction() { switch(++aCount) { case 1: "The troll finishes gnawing on the bones of some defunct animal --- a hen perhaps --- and tosses them contemptuously into the river. "; break; case 2: "The troll lets out a loud belch. "; break; case 3: "The troll looks around for something else to eat. "; break; case 4: "The troll starts staring at you with a worryingly hungry look. "; break; default: "The troll continues to stare hungrily at you as if he's pondering how tasty you might be. "; break; } } /* The troll's response to being given something. */ iobjFor(GiveTo) { verify() {} action() { /* * If the troll is given the hen, then let the hen's * givenToTroll() method handle it. */ if(gDobj == hen) hen.giveToTroll(); /* Otherwise the troll simply rejects the gift. */ else { "The troll takes one look at {the dobj} and casts {him dobj} contemptuously aside. "; gDobj.moveInto(location); } } } iobjFor(ThrowAt) { action() { gDobj.actionMoveInto(location); "The troll swats {the dobj} angrily aside and snarls at you. "; } } aCount = 0 ; + Thing 'bridge; big stone' "The big stone bridge spans the river. " /* By defining is fixed to be true, we ensure the bridge can't be taken. */ isFixed = true dobjFor(Enter) { verify() {} /* Turn ENTER BRIDGE into GO NORTH */ action() { goInstead(north); } } /* Make GET ON BRIDGE behave like ENTER BRIDGE */ dobjFor(Board) asDobjFor(Enter) /* * If the player refers to something that might match this object or * anything else in scope, make this object the more likely choice. */ vocabLikelihood = 10 ; /* The River class is defined below. */ + River ; bridge: Room 'Bridge' "From the centre of the bridge you can carry on <<aHdir('north')>> home to your village or back <<aHdir('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. "; finishGameMsg(ftVictory, [finishOptionUndo]); } ; + Thing 'river; rushing; water' "The river is in full flood, carrying the water down from recent heavy rainfall in the highlands off to the west. " /* * By defining isDecoration = true on an object we make it respond to * every action apart from EXAMINE by just displaying its notImportantMsg. */ isDecoration = true 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 <<aHdir('east')>>. Further <<aHdir('south')>> the field is quite dry, however. " west = startroom south = eastField ; /* The River class is defined below. */ + River ; + bottle: Thing '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. " /* The response to SMELL BOTTLE */ smellDesc = "The bottle smells strongly of vinegar. " /* Defining contType as In on an object makes it a container. */ contType = In /* 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. "; } } /* * If the bottle is still floating, provide handling for MOVE BOTTLE WITH * object. */ dobjFor(MoveWith) { action() { if(isFloating) { "You manage to snag the bottle with {the iobj} and bring it ashore. "; isFloating = nil; moved = true; } 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) ; + Thing 'reeds;submerged;plants;them' "The reeds are more than half submerged by the river. " isDecoration = true ; westBank: Room 'River Bank (West)' "The riverbank, which continues to the <<aHdir('east')>> is particularly thick with plants at this point. The way north is blocked by the river running rapidly by, but to the <<aHdir('south')>> lies an open field. " east = startroom south = westField ; + River ; + plants: Thing '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>>. " /* * By defining isFixed as true we make this object fixed in place, which * means it can't be taken and won't be shown in a room listing. */ isFixed = true /* * 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: Thing 'hint of purple' "It looks like there may be something purple there. " isDecoration = true 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. "; } } initSpecialDesc = "Some purple monkshood grows among the plants. " ; road: Room 'Road' "This long road runs <<aHdir('north')>> -- <<aHdir('south')>> through cornfields to <<aHdir('east')>> and <<aHdir('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 <<aHdir('west')>> is the road, while the river is a little distance to the <<aHdir('north')>>. " west = road north = eastBank in = hutDoorOutside ; + Thing '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. " isFixed = true /* * 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 ; /* * hutDoorOutside is one of the few simulation objects in this game that's not * of class Thing or Room. Because doors have fairly standard complex * behaviour, there's a special Door class for them even in adv3Liter */ ++ hutDoorOutside: Door '(hut) door' /* * Doors in adv3Lite are defined in pairs, each representing one side of * the door. For the Doors to work properly together each door must define * its other side in its otherSide property. */ otherSide = hutDoorInside /* * The list of keys that can be uses to lock and unlock this door. By * defining this property we (a) make the door start out locked and (b) * make it something that can be locked and unlocked with a key. */ 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 ; + driedMud: Thing 'dried mud; encrusted hard solid' "The mud encrusted round the rock looks like it's dried as hard as cement. " isFixed = true 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 } ; + Thing 'stubble; harvested; corn' "Only the stubble is left. " isDecoration = true notImportantMsg = 'What\'s left of the corn now that it\'s been harvested isn\'t worth bothering with. ' ; /* * The iron key is another of the very few simulation objects in this sample * game that's not of class Room or Thing. Because keys in adv3Lite have to * keep track not only of what they can lock and unlock but what they might * and what they're known to lock and unlock, and are thus quite complex, they * need to be defined as belonging to the Key class even in adv3Liter. */ ironKey: Key 'iron key' ; 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] ; + Thing 'shelf; plain wooden' isFixed = true contType = On ; ++ gloves: Thing 'gloves; worn leather of[prep]; pair; them it' "They're just a pair of worn leather gloves. " isWearable = true ; ++ 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 <<aHdir('north')>> the field slopes down to the riverbank, while the main road lies just to the <<aHdir('east')>>. A chicken coop squats in a corner of the field. " east = road north = westBank ; + coop: Thing '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. " isFixed = true contType = In isOpen = nil 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 /* * Custom method to handle what happens when the hen is given to the * troll. If the hen has been poisoned, the troll eats the hen and dies, * clearing the way to the bridge. Otherwise the hen merely served to whet * the troll's appetite so he goes on to eat the player character. */ giveToTroll() { "The troll snatches the hen from your grasp and starts chewing on it. "; if(isPoisoned) { "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. "; moveInto(nil); troll.moveInto(nil); } else { "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]); } } ; + bucket: Thing 'bucket; plain old wooden' "It's just a plain old wooden bucket. " contType = In 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. " ; + Thing 'some standing corn; ripe' "It's ripe and ready to be harvested. " isDecoration = true notImportantMsg = 'The corn is best left to the harvesters. ' ; 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 ; /* Our custom River class */ class River: Thing '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); } } ; /* Define a custom action FILL X WITH Y */ DefineTIAction(FillWith) ; /* Define the grammar for the new 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' ; /* Define the default handling of the FillWith action on the Thing class */ 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}. ' ; /* * A Doer is an object that can intercept a command and make it do something * different from the normal action handling. */ /* * This Doer intercepts any command of the form PUT BUCKET INand performs its own handling for that command instead of * the standard handling. */ Doer 'put bucket in River' execAction(c) { /* * Instead of putting the bucket in the indirect object, we fill it * with the indirect object. */ doInstead(FillWith, bucket, gIobj); } ; /* * Here we use a Doer that responds to the HINT command to provide a * rudimentary hint system. */ Doer 'hint' execAction(c) { "Are you sure you don't want to solve this by yourself? "; if(!yesOrNo()) abort; "The troll is hungry; this is one troll you should feed, preferably with something that won't do him any good.\b Would you like another hint? "; if(!yesOrNo()) abort; "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.\b Would you like another hint? "; if(!yesOrNo()) abort; "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.\b Would you like another hint? "; if(!yesOrNo()) abort; "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?\b Would you like another hint? "; if(!yesOrNo()) abort; "Fill the bucket with water from the river and pour it on the rock (or the mud).\b Would you like another hint? "; if(!yesOrNo()) abort; "You can use either the bucket or the shears to move the bottle close enough inshore to take.\b Would you like another hint? "; if(!yesOrNo()) abort; "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! "; abort; } ; /* * A StringPreParser can be used to change the text of a player's command * before the parser attempts to interpret it. Here we use a StringPreParser * to change PICK MONKSHOOD into GET MONKSHOOD and to change CROSS BRIDGE into * ENTER BRIDGE. This is a somewhat crude way to intercept these potentially * common phrasings, since it won't deal with near synonyms like CROSS THE * BRIDGE, but it illustrates the principle. */ StringPreParser doParsing(str, which) { if(str.toLower == 'pick monkshood') str = 'get monkshood'; if(str.toLower == 'cross bridge') str = 'enter bridge'; return str; } ;