TADS3 coding best practices

For commerical coding, I am accustomed to a “Best Practices” or “Code Guidelines” document that codifies code style expectations. These guidelines mostly fall into two categories:

  1. Readability/maintainability and
  2. Code robustness (ie coding that is least susceptible to unexpected/unwanted interactions)

A quick search did not turn up a previous discussion, though maybe it was called something different?

This early in my authorship, coding in a one-man bubble, I would like to test a few informal #2 patterns I seem to have evolved, and further solicit any additional rules others find useful. Note these are guidelines, not hard and fast rules. Some of these are objectively not great.

  • Create a new Class if cut’n’paste > 2 (some might use a lower number!)
  • use gActor rather than me in almost all cases, especially new Verb definition
  • use obj.isDirectlyIn(gActor/me) rather than obj.location == me/gActor
  • use message parameter strings ie {it dobj/he} in dobjFor/iobjFor(Verb) implementations added to Classes. Use them rarely inside specific object instances
  • use double quotes instead of mainReport (mainly because it looks less cluttered to me)
  • use StringPreParser when I can’t figure out how else to do something. Rarely go back and fix it if I figure it out later.
  • When creating new objects, localize new verb definitions, including Thing modifications with them. This is a two-edged sword. It allows me to conceptualize and see all the relevant code together. It also means a crap-ton of Thing modifications sprinkled everywhere throughout my code.
  • A minimum of three source files: game/map/objects, hints and npcs. Bigger games may have multiples of each of those.
  • Define Topics immediately above the object/npc most likely to use them, and define that object before any others that might use them. The compiler seems to need to see topics defined before referenced? I am awaiting the day I trip over multiple file dependencies biting me.
  • Almost always use Component instead of Fixture due to the reporting differences.
  • General purpose verbs and functions defined at the bottom of a file, probably the Game file (usually not enough to push into its own file)
  • use inherited even when in doubt. change if looks wonky
  • heavy use of MulitInstance for new class’s scenery, acknowledging the result can be samey

Those are probably the big ones so far. If some above are misguided, head me off now, before my codebase gets too big! Def let me know if there is a relevant thread I couldn’t find. Also, love to hear any internalized practices ya’ll are benefiting from.

2 Likes

Had to smile at one source file being designated for game/map/objects… I have 10 or 11 game world files, ranging from 2k-4k lines apiece. Plus two separate files for portable objects, a few more for npcs, a dedicated file for custom verbs, a couple files for library mods and generic functions, one for custom classes, one for hints, one for builtin walkthrough, etc. :slight_smile:
Agreed: I almost never use ‘me’, almost never use mainReport. A few instances where reportBefore or extraReport get what I want. Something I haven’t taken the trouble to solve, is that I have occasionally wished to have a combo reportBefore/extraReport, but there isn't a simple tweak for getting that into the transcript mechanics.
I also define some Topics by the NPC they’re relevant to, but some are more general and go in my general mods file.
I’m curious about what you meant by putting modify Things all over your code. Was it just new verbs that you meant? In my verb file, I do what you’re saying and add a modifyThing under each verb definition, but otherwise I have one master modifyThing block in my mods file. Other tweaks seem to be appropriate to put directly into subclasses.
One thing I do heavily, that is the opposite of “best practice” according to the letter of the law, is use macros. My header file has about 300 #defines turning (among other things) the most common TADS methods and expressions into two- or three-letter codes, like:
gpc (gPlayerChar)
gac (gActor)
gor (gpc.getOutermostRoom)
mI (moveInto)
oK (ofKind)
gR (gRevealed)
rA (replaceAction)
sD (specialDesc)
dF (dobjFor)
SHUF (ShuffledEventList)
Fix (Fixture)
Dec (Decoration)
acT(x) (DefineTAction(x))
etc. etc.

Perhaps oddly, I have made virtually no use of parameter substitutions, partially because I ignored them when I was first learning TADS and didn’t want to go back and change all that, but also my game doesn’t really need to make use of NPCs carrying out actions (other than Travel) through the standard Action processing. A few will obey some commands, but what they do is all handled through TCommand topicResponse, AgendaItems, or some alternative.
I wish I had put together a compendium of the pitfalls to watch out for that I have found through almost 4 years of tinkering with adv3. I might post some of them to the cookbook after I finish my game…

2 Likes

I stand corrected… I found one place in my code where it did seem to make sense to add a modify Thing directly underneath a particular subclass. So there you go, your behavior is not erratic…

1 Like

I think this is a good start.

Some quick reactions:

I would expand this to “Instead of using me, use gPlayerChar for the current PC and gActor for the current action’s actor.” I know this sounds more complicated, but it becomes important in games with NPCs.

Or obj.isIn(), which will search containers in the actor’s inventory (which is more often what you want).

Source code organization has been a big fuss of mine. Maybe worthy of a separate topic to hear how others handle this problem.

This hasn’t been a problem for me. Do your source files use #include for the other source files particular to your game?

3 Likes

And in games where the PC changes.

The me convention is something that should probably be Considered Harmful, although it’s probably fine for old school “lone player in a cave” treasure hunts and that kind of thing.

I’ve currently got a whole bunch of getInnermostFoo() and getOutermostFoo() methods, a la:

modify Thing
        getInnermostVehicle() {
                if(self.ofKind(Vehicle)) return(self);
                if(location == nil) return(nil);
                if(location.ofKind(Vehicle)) return(location);
                return(location.getInnermostVehicle());
        }
;

As far as source organization goes:

  • Project gets its own directory. If the game is The Great Foozle Hunt then maybe call the top-level project directory foozle
  • The project makefile lives in the top-level directory. The “main” include file does as well. In a C project it would go in foozle/include but TADS3 include files are a lot sparser than C’s, so we’ll probably only have one so it can just live in the top level
  • The top-level gets a small number of subdirectories: foozle/game for the compiled story file(s), foozle/obj for the object files created by the make process, and foozle/src for all the source subdirectories
  • The foozle/src subdirectory gets branches for each main “kind” of thing in the game. So foozle/src/actors, foozle/src/objects, foozle/src/rooms, and so on. foozle/src/game is for the “built in” TADS game objects, like gameMain and versionInfo. foozle/src/lib is for global stuff: patches to adv3, newly-defined system actions, global changes to libMessages, and so on
  • Each of the “type” subdirectories is further subdivided into “regions”. So if The Great Foozle Hunt has a wooded area, a mine shaft, some ancient Mayan ruins, and a library at a university, then it might have foozle/src/rooms/library, foozle/src/rooms/mine, foozle/src/rooms/ruins, and foozle/src/rooms/woods, and probably foozle/src/objects would get a similar treatment, unless most of the objects end up being “cross domain”. foozle/src/actors just gets a subdirectory for each “major” NPC plus the PC, plus a junk drawer for “miscellaneous” NPCs (if any).
  • I tend to group functionally-related bits of code together in their own source file. So if The Great Foozle Hunt is somehow or other a murder mystery and one of the NPCs is Bob and Bob plays canasta, bakes bread, and is the killer, then we might have foozle/src/actors/bob/bob.t for his generic bob-ness, foozle/src/actors/bob/bobBaker.t for the bread baking logic,foozle/src/actors/bob/bobCanasta.t for his canasta-playing bits, and foozle/src/actors/bob/bobKiller.t for the murder-y bits. This is to make it easier when I reach the inevitable point in the development process where I decide that it isn’t Bob that either plays canasta, bakes bread, or is the killer, and so I have to move some of the logic from bob to alice. Or…
  • I decide everybody plays canasta, at which point the player-specific bits get moved into a new abstract class, CanastaActor, and that probably ends up being its own module. Part of this is just for modularity/reusability/whatever, general code hygiene. But it also makes it easier to do unit testing…every module gets its own l’il demo/test “game”(s) to exercise the moving parts, and this makes it waaaaay easier to troubleshoot things when Bob being the killer somehow or other interferes with the canasta logic—you can then just test the canasta and killer logic separately, verify that they’re working individually, and so the problem must be in the (couple dozen) lines of game-specific stuff instead of the hundreds or thousands of lines of game-canasta-killer code put together.
1 Like

Yeah, that’s pretty much the opposite of how I generally do things. I tend to prefer fairly chatty method and property names, in the assumption that I very seldom find myself stalled because I just can’t type code fast enough, and I very frequently find myself stalled because I’m trying to read code to figure out what it does or where a problem is, and so given a trade-off between less typing and better readability, I’ll choose readability almost all the time.

This is also a gift to my future self, who finished writing the code weeks or months ago, and is now circling back around to use it and has to figure out what present me was thinking when I came up with whatever nomenclature I decided to use.

1 Like

I use a lot of macros as well, but probably in a very different way.

Sometimes I really need to refactor something, but doing so using only objects, properties, and methods leaves too much room for accidents in the future. In those cases, I refactor code using macros. I find it helps to greatly reduce future bugs both during project use and bugfixing/additions.

Macros are also really handy in the gamedev phase in particular, when you’re using your code for filling out a map. You can reduce complex definitions down to a single line and some arguments, and greatly reduces the chances of making malformed objects because you forgot to set some property. Templates are absolutely amazing for this, too.

Otherwise I use Java-style industry naming conventions, so my macros, methods, class names, and property names are rather long. This isn’t a problem for me, tho, because I use VSCode, which has stellar sutocomplete for names.

But generally most of my habits are for refactoring, clarity, organization, and future-proofing; typing speed be damned.

(Also, please keep this thread going; I am taking so many notes here!!)

I almost exclusively use gActor, in order to keep functionality identical between the player and any (commanded) NPCs. Failing that, I use gPlayerChar, in case I have multiple player characters. (I generally try to make reusable modules for myself, as a lot of my game ideas share gameplay elements)

The only time I would use me is if there were some project-specific story reason why that character in particular would be treated a certain way, as a player or NPC.

I’m aware that macros are deprecated, but I haven’t had reason to regret it. In a game the size of mine, with so much ahead of me still to do, I find that I am deterred from adding small details and extra touches due to the typing overhead. I feel in my own case that the macros have helped my sanity and motivated me to add a number of minor things that I would have waved off if typing out in full.
I would make a case that gPC, gOR, and dF (playerchar, outermostroom, dobjfor) are rational for how heavily they’re used. I don’t see it as being so much different than a function called stoi() for stringToInteger…

1 Like

A lot of C-style super-terse method names date back to Big Iron days, when it made a lot more sense to save a few keystrokes because those method names were mostly going to be typed in on 300 baud console serial lines and the like.

2 Likes

I guess my point was, that once you’ve learned what stoi means, you’re not too likely to forget it…

2 Likes

I’m absolutely not trying to talk you out of anything that works for you, so please don’t take it that way…but screwing up on basic usage is one of the persistent problems in programming at large. Like specifically in the case of strtoi(), how wide is an integer?

2 Likes

RE: How I use macros.

They are extremely powerful and allow TADS to do some really amazing things. I will remain on the macro hill with all of my simplified, refactored, and future-proofed code. They can be used for a lot more than just shorthand, and the idea of trying to deprecate them really cuts down the real power that TADS has, both from the engineering and game design sides of things.

1 Like

Thanks guys, super helpful sanity check for me.

I appreciate the vote of confidence, but you are drawing a VERY big conclusion from one datapoint! :]

Man I would read the HECK out of that.

This is definitely where I land on that one. Though man is dobjFor working my last nerve.

These are great suggestions I will be refactoring tonight.

Lotsa good stuff but that last in particular is a super powerful idea I need to implement NOW.

3 Likes

Yeah, I’m absolutely not against macros in general (although I think some of the ways adv3 uses them are a bit…idiosyncratic), but my macro names tend to be l’il short stories in camel case. Except when they’re preprocessor/compiler flags, when they’re short stories in screaming snake case.

2 Likes

So I went back and tried to recreate the failure. It no longer failed when they were defined ‘out of order.’ I’m thinking I misdiagnosed a problem as code order a while back, inadvertently fixed the real problem concurrent to moving code around, and had my bias confirmed. This is how misconceptions become canon! Rule removed. (Though because I’ve used it for bit, it looks ‘natural’ to me this way now. So maybe it stays. :confused: )

2 Likes