JWZ's TADbits (#15: some Lite overlap)

Where is the “adv3Utils” repo? I don’t see it at diegesisandmimesis. You’ve clearly been doing some advanced work, so thanks for any pointers to this repo! (And my apologies if I simply failed to spot it at your GitHub space.)

1 Like

Well crap. I thought I’d made it public around the same time I made adv3Patches public (which I just doublechecked to make sure I actually had).

Should be visible now:

Summary (it doesn’t have full documentation, but there should be an information block at the top of the individual source files, and they should be fairly verbosely internally commented):

  • actionType Adds an actionType prop to Action, used elsewhere for grouping actions (in this case sense actions and travel actions get tagged)
  • _debugObject() A function that enumerates an object’s properties. Only enabled in debugging builds
  • Deadbolt and DoorPair, recently described above
  • findMaxValue() A function that tries to find the largest number of something (like objects in a room) that the VM can handle without throwing an exception
  • gTurn A macro returning the current turn
  • interruptImplicit A macro for interrupting implicit actions
  • iobjScope Logic implementing object scoping rules for indirect objects similar to the existing ones for direct object. Described fairly recently in this thread
  • messageTokens Logic for declaring arbitrary message parameter substitution tokens that are resolved by callback
  • OrdinalThing Mixin for Thing to add ordinal vocabulary (“first pebble”, “second pebble”, and so on)
  • Resource and ResourceFactory Classes for dispensing equivalent objects (harvestable plants and that kind of thing)
  • room Adds a couple methods on Room that test for room adjacency and list a room’s exits and destinations.
  • roomVocab Tweaks Room to make it easier to refer to rooms in commands
  • searchContents Adds methods to Thing: searchContents(fn) which returns the subset of contents that satisfies fn; contains(fn) that returns boolean true if any object in contents satisfies fn, and matchContents(fn) that returns the first object in contents that matches fn or nil if none
  • thingStateDesc, thingStateMulti, and thingStateRoom Extensions to ThingState that add, respectively, a stateDesc property, the ability to have multiple active states, and room-specific tweaks
  • titleCase() A function that returns a string formatted in title case.
  • vocab Tweaks to VocabObject to make it easier to add words and specifically convenience methods for adding weak tokens

If you have any questions or notice any bugs pragmatically the best way to ping me is here; reporting issues on git works but I’m not very diligent about checking (because the main consumers of my repos appear to be me and AI scrapers).

4 Likes

Many thanks for making the repo public and for all your brilliant contributions!

1 Like

Another fairly minor tweak, this time some code to allow toggling some kinds of object announcements on Thing.

First we declare a few flags. These correspond one-to-one with library messages that we want the ability to toggle. This mechanism could in theory be extended to other kinds of object announcement, but this tweak targets a specific set of them unified by identical calling semantics. Anyway, the flags should probably go in some header file, adv3Utils.h in the module:

#define ANNOUNCE_MULTI (1 << 0)
#define ANNOUNCE_AMBIG (1 << 1)
#define ANNOUNCE_DEFAULT (1 << 2)

Then we add a property on Thing that by default has all three of these flags set (so the default behavior will be the same as in stock adv3: always show all the object announcements):

modify Thing
        announcementFlag =
                ( ANNOUNCE_MULTI | ANNOUNCE_AMBIG | ANNOUNCE_DEFAULT )
;

Then finally we tweak the corresponding message methods on libMessages to check the new property:

modify libMessages
        announceMultiActionObject(obj, whichObj, action) {
                if(obj.announcementFlag & ANNOUNCE_MULTI)
                        return(inherited(obj, whichObj, action));
                else
                        return('');
        }

        announceAmbigActionObject(obj, whichObj, action) {
                if(obj.announcementFlag & ANNOUNCE_AMBIG)
                        return(inherited(obj, whichObj, action));
                else
                        return('');
        }

        announceDefaultObject(obj, whichObj, action, resolvedAllObjects) {
                if(obj.announcementFlag & ANNOUNCE_DEFAULT)
                        return(inherited(obj, whichObj, action,
                                resolvedAllObjects));
                else
                        return('');
        }
;

This means that each of the announcement methods fall through via inherited() if the flag is set, and an empty string is returned if it isn’t (effectively suppressing the announcement).

The specific use case I have for this is some furniture-related code that provides an instanceable TableWithChairs class and additional actions such that >SIT AT TABLE is understood to mean the player wants to sit on one of the chairs at the table. But by default that produces something like:

Table and Chair Room 02
This room contains a TableWithChairs.

>sit at table
(the table)
Okay, you're now sitting on the first chair.

…where I think the (the table) announcement is ugly.

4 Likes

Yeah, I did some suppressing of ambig announcements too…

2 Likes

… which I stole, eh, appropriated, hrm, HOMAGED into my WIP! (so this becomes yet ANOTHER post-WIP tweak to adv3_jjmcc. I better start a list…)

2 Likes

Random observation: lexical inheritance is order-preserving…but only if the objects are anonymous.

If you have something like:

myWidget: WidgetClass;
+Foo val = 1;
+Foo val = 22;
+Foo val = 6;

…and there’s some preinit logic that iterates over Foo instances and adds them to a list in myWidget, then the Foo instance with val = 1 will be added first, the one with val = 22 will be second, and val = 6 will be third.

If you use:

myWidget: WidgetClass;
+foo1: Foo val = 1;
+foo2: Foo val = 22;
+foo3: Foo val = 6;

…then the order will not be consistent.

EDIT: Upon further experimentation it appears that the order is consistent…it evaluates the objects in asciibetical order according to the object name. Simple demo:

#charset "us-ascii"
#include <adv3.h>
#include <en_us.h>

versionInfo: GameID;
gameMain: GameMainDef
        newGame() {
                myWidget.logInventory();
        }
;

class Foo: object
        val = nil

        initFoo() {
                if((location != nil) && location.ofKind(WidgetClass))
                        location.addFoo(self);
        }

        log() {
                "\nval = <<toString(val)>>\n ";
        }
;
class WidgetClass: PreinitObject
        fooInventory = nil

        execute() {
                forEachInstance(Foo, { x: x.initFoo() });
        }

        addFoo(obj) {
                if((obj == nil) || !obj.ofKind(Foo))
                        return(nil);

                if(fooInventory == nil)
                        fooInventory = new Vector();

                fooInventory.append(obj);

                return(true);
        }

        logInventory() {
                fooInventory.forEach({ x: x.log() });
        }
;

myWidget: WidgetClass;
+a: Foo val = 1;
+c: Foo val = 2;
+e: Foo val = 3;
+g: Foo val = 4;
+i: Foo val = 5;
+h: Foo val = 6;
+f: Foo val = 7;
+d: Foo val = 8;
+b: Foo val = 9;

…which produces:

val = 1
val = 9
val = 2
val = 8
val = 3
val = 7
val = 4
val = 6
val = 5

A micro-observation not warranting a thread of its own, so piled in here!

I really, really like to tame big lists of objects in my WIP, especially similar ones. Ie rather than

You see an A, a B, a C and a D.

instead

You see a group of Q.

TADS provisions a variety of ways to do this, with escalating levels of complexity/pre-requisite tradeoffs. I put isEquivalent at one end: low complexity but pretty rigid object definitional requirements. CollectiveGroup is at the other end where you can alias very different objects and fine tune collective verb handling. My journey with TADS was kind of mapless in this regard. It usually involved trying one or the other, finding it wanting (or worse, bending it to my will with EXTREME effort) then exploring the space.

There is a middle ground solution which, the more I play with it, the more I like: the ListGroup. This unassuming little object allows you to collect groups of items and summarize them in inventory and room descriptions. There are seven different subclasses, each with specific intent, but the two I find myself using the most are
ListGroupSorted and ListGroupCustom. The former breaks a common category into a list of its visible constituents; the latter replaces the entire group with a summary phrase.

Actually, the one I use most is a custom combination of the two. The former summarizes the group as a list. While unambiguous, it arguably defeats the way I want to use it - trimming object enumerations. The latter very specifically does this, but has a (sometimes) undesired artifact: you can NEVER get the constituent list, at least not intuitively. This is what I mean, first with ListGroupSorted:

You see an Ace of Spades, a Queen of Hearts and a seven of clubs here.

>GET CARDS
Ace of Spades: Taken.
Queen of Hearts: Taken.
seven of clubs:  Taken.

>I
You are carrying an Ace of Spades, a Queen of Hearts and a seven of clubs.

>X CARDS
You are carrying an Ace of Spades, a Queen of Hearts and a seven of clubs.

Unambiguous, yes, but completely fails to reduce an object list! (ListGroupParen would make it even wordier, adding a group name then fully listing them anyway!) Contrast to how ListGroupCustom deals with this:

You see some playing cards here.

>GET CARDS
Ace of Spades: Taken.
Queen of Hearts: Taken.
seven of clubs:  Taken.

>I
You are carrying some playing cards.

>X CARDS
You are carrying some playing cards.

Now, they are collectively summarized in descriptions, but the player has no easy way to drill down into them. My solution?

class ListGroupCustomSorted : ListGroupSorted
    showGroupList(pov, lister, lst, options, indent, infoTab) {

        // list items if examined directly, otherwise use summary
        if (gActionIs(Examine))
            inherited(pov, lister, lst, options, indent, infoTab);
        
        // optionally expand category in inventory
        else if (parenInInventory && gActionIs(Inventory)) {
            showGroupMsg(nil);
            inherited ListGroupParen(pov, lister, lst, options, indent, infoTab);
        }
        else inherited ListGroupCustom(pov, lister, lst, options, indent, infoTab);
    }
    showGroupMsg(lst) { "<<groupMsgMsg>>"; }
    groupMsgMsg = '' // set on instantiation
    parenInInventory = nil  // set if desire enumeration in inventory
;

class PlayingCard : Thing
    listWith = [playingCardList]
;

playingCardList : ListGroupCustomSorted
    groupMsgMsg = 'some playing cards'
    parenInInventory = true
;

This basically lets you situationaly use different flavors of ListGroups for different purposes. Resulting in:

You see some playing cards here.

>GET CARDS
Ace of Spades: Taken.
Queen of Hearts: Taken.
seven of clubs:  Taken.

>I
You are carrying some playing cards (an Ace of Spades, a Queen of Hearts
and a seven of clubs).

>X CARDS
You are carrying an Ace of Spades, a Queen of Hearts and a seven of clubs.

Lotta explanation for a pretty minor observation!

1 Like

Another micro-tip. Some room descriptions can be pretty dense - in addition to the overall description, lots of portable objects, lots of specialDesc items. Let’s assume this is not a game design problem (or ignore it if it is). How do you meter your initial room description to not overwhelm the player on initial viewing?

Let’s start with the description itself. Pretty much from day one, I provisioned two descriptions - a first view impression and a terser summary, assuming the player is not going to be interested in repeating a wall of text. This is done pretty simply with a combination of <<first time>><<only>> and <<one of>><<or>><<stopping>> in the desc field.

gloriousRoom : Room 'Glorious Room' 'glorious room'
    "<<one of>>An excessive, flowery description of the wonders of your
        imagination.  <<or>>An amazing room, you get it.  <<stopping>>
        <<first time>>You\'ve never seen anything like it before.  <<only>>"
    // &etc
;

Where this becomes awkward is on first view. Not only do you have a massive, indulgent room description, then it is followed by paragraphs of specialDesc’s and portable item presence. It is fair to think ‘hey, they are just gobsmacked by this amazing thing I created, would they even notice the paperclips on the floor?’ Or more pragmatically, 'This is really annoying to read, bleeding over into multiple pages. How do I focus the player’s attention on wonder, but not overwhelm them or hide gameplay elements? Here’s a way!

gloriousRoom : Room 'Glorious Room' 'glorious room'
    "<<one of>>An excessive, flowery description of the wonders of your
        imagination.  <<or>>An amazing room, you get it.  <<stopping>>
        <<first time>>You\'ve never seen anything like it before.  <<only>>
        <<first time>>Almost lost among the wonders are a variety of
            mysterious things.  <<only>>"
    lookAroundWithin(actor, pov, verbose) {
        // first time, do not show LookListSpecials, Portables
        if (!seen && (verbose == true))
            verbose = (LookRoomName | LookRoomDesc
                        /* | LookListSpecials | LookListPortables */);
        inherited(actor, pov, verbose);
    }
    // &etc
;

Basically, you are complementing first-time descriptions with contents lists that only
appear on a second viewing. The trick, of course, is to craft your descriptions to not HIDE that there is more going on here. Ie, the above would yield

Glorious Room
An excessive, flowery description of the wonders of your imagination.
You've never seen anything like it before.  Almost lost among the
wonders are a variety of mysterious things.  

>L
Glorious Room
An amazing room, you get it.

A complicated machine is humming in the corner.

The eyes in the painting on the wall seem to follow you around the room.

There are some paperclips here.

The heavy lifting is in the lookAroundWithin override. The default method takes a verbose parameter that either reflects desired reporting fields, or provides ‘canned’ lists if set to true or nil. We simply provide a reduced list if seen is nil (and where verbose is typically true), which is only true on the first viewing. Complementing this with text in the desc.

2 Likes

That’s an interesting approach… might have been useful to employ in Prince Quisborne! I wonder: are you going to handle the player’s attempt to x mysterious things, or perhaps parenthetically nudge them to look a second time, or just assume that they will inevitably look again?

I also used (as I suspect you well remember) a verbose room desc and a conciseDesc, but I couldn’t make use of the <<stopping>> simplicity because I wanted the player to be able to recall the detailed description if desired, since world-painting was especially important for me in that game. So I had to add some extra mechanics about when to select which desc.

1 Like

Oh, for sure i would provision a mysteriousThings object, perhaps as a CollectiveGroup in the room! In simpler cases, I might just refer to objects being present in the room desc, well short of a separate specialDesc paragraph.

Oh, I def remember! I really wanted to steal your “LongLook” command, but belatedly realized I would have to refactor ALL MY ROOM DESCRIPTIONS to do so! Next project. I think the lookAroundWithin trick would still work there, just use the desc-pointing-property instead of seen to mask the contents lists.

1 Like

I definitely think there are at least four or five places in PQ that would especially benefit from using your tactic here… I might slip it in to an eventual version!

1 Like

I’ve been doing a lot of this kind of thing using a little tweak to ThingState to extend it to include things like the various Room-specific properties.

In addition to doing first/subsequent description changes, I’ve been using a lot of “knowledge”-based automagic ThingState switching. So the a room might be “A Dusty Attic” or “The Attic in Bob’s House” depending on whether or not the player read the name on the mailbox before entering, and that kind of thing. And a lot of updating descriptions to include important information…“…and in the corner is a dusty steamer trunk.” becomes “…and in the corner is the trunk where you found the photo of the old ironworks.” and so on.

2 Likes

Sideways related to that, is Room.afterTravel() the cleanest hook for events that should be triggered when the player enters the location?

I’ve got a bunch of automation scripting that fires at various points in the turn lifecycle (before/after action, turn, and prompt), but I think beforeTravel() and afterTravel() are the most reliable way to trigger specifically on travel actions. Although I haven’t really tried to dive in to identify all the fiddly corner cases yet.

1 Like

TADS gives you get a lot of options, doesn’t it? In addition to before/afterTravel off the top of my head: travelerArriving, enteringRoom, noteTraversal, probably many more. I can’t keep the various timings and nuances of each of them in my head. For me, I break arrival code into three cases: things I need to happen BEFORE the room description (because it might influence it); thing that must happen AFTER (because I want to print/adjust things after the description, but don’t care where); and things that must specifically happen after every other incidental message (ie specialDesc’s, portable desc’s, NPC actions, auto-closing &etc), basically right before the next command prompt.

For me, rather than continually refresh myself on WHICH tool is the best for which, I got lazy. travelerArriving lets you (kind of) manage all those! By default it adjusts postures, calls enteringRoom then describeArrival in that order. Which very conveniently lets you

    travelerArriving(traveler, origin, connector, backConnector) {
        // put code here that must run BEFORE room description
        roomDescFlag = true;

        // standard business, must not omit!
        inherited(traveler, origin, connector, backConnector);

        // code that must run somewhere AFTER room description
        "\"Well, this certainly is a room!\" proclaims a voice from nowhere.  ";

        // code that must run right before the next command prompt
        new OneTimePromptDaemon(self, &lastNotify);
    }
    lastNotify() {
        "Quite a lot to take in, no?  ";
    }

It might not be the most elegant, most nuanced solution, but it has the benefit of being something I DEFINITELY REMEMBER! (In some cases, it may be necessary to test for if (traveler == gPlayerChar) to manage things correctly)

It belatedly occurs to me that a more elegant way to accomplish this might be to tweak its definition:

modify BasicLocation
    travelerArriving(traveler, origin, connector, backConnector) {
        // standard business, must not omit!
        inherited(traveler, origin, connector, backConnector);

        // new placeholder method to execute after room description
        enteredRoom(traveler);
    }
    // post-description routine, override as needed
    enteredRoom(traveler) { }
;

This would let you use enteringRoom for pre-desc, and enteredRoom for post-desc, where OneTimePromptDaemons can also live. WAAAAY too late now! Next project perhaps.

2 Likes

That’s fairly similar to what I’ve been using, with the exception of the underlying hook:

// Changes to the Room class to provide hooks for random events that trigger
// before entering or leaving.
modify Room
        randomEventType = nil           // type of event we start
        randomEvent = nil               // current event, if any

        beforeTravel(actor, conn) {
                inherited(actor, conn);
                handleRandomEvent(actor, conn, eBeforeTravel);
        }

        afterTravel(actor, conn) {
                inherited(actor, conn);
                handleRandomEvent(actor, conn, eAfterTravel);
        }

        handleRandomEvent(actor, conn, when?) {
                local ev;

                // If we don't have an event type declared, do nothing.
                if(randomEventType == nil)
                        return;

                // If we have a current event, don't start another.
                if(randomEvent != nil)
                        return;

                // Get a random event matching the given ID and which
                // corresponds to the timing (before or after travel)
                // that we care about.
                if((ev = randomEvents.getRandomEvent(randomEventType, nil,
                        { x: (when ? x.randomEventWhen == when : true) }))
                        == nil) {
                        return;
                }

                // Remember this event, and start it.
                randomEvent = ev;
                ev.start();
        }
;

Where randomEvents is a global singleton not defined here but the idea should be apparent.

I think the only difference between hanging this off afterTravel() versus travelerArriving() is travelerArriving() is just for locations and all objects in scope after the travel action have their afterTravel() pinged (and similarly for everything in-scope before movement and beforeTravel()). The loop that calls afterTravel() is immediately after the call to the location’s travelerArriving() in travelerTravelTo().

In my case I think I want to use afterTravel() just because I want to be able to hang the same logic on non-location objects. Although most of the events are tied to the Room subclassing. Basically (some) rooms get a list of random events. Something kinda like an encounter deck from the Arkham Horror card game, if you’re familiar with it. The first time the player enters certain locations there’s a random event, which could be nearly anything from just spawning an item (like a harvestable resource or something) to advancing the next step in a multi-part quest, to something like an encounter or puzzle that needs to be resolved before the player can leave the location. And some trigger when the player tries to exit instead of when the enter. This is mostly happening in procgen areas (exploring the woods outside of town and that kind of thing), and part of what the player is trying to do (over time) is influence what kind of events they’re getting by changing how the procgen logic works…picking which trails to follow depending on who you have with you at the time and what skills everyone has and so on.

Which all means that there’s a whole bunch or fiddly special cases for when, where, and how various rules get fired. So having a reliable layer of abstraction that does most of the heavy lifting in terms of integrating with the basic T3 turn/event loop is a big win.

1 Like

Lol, so what you’re saying is that I could have achieved what I wanted with enteringRoom() pre-desc and afterTravel() post-desc? No gyrations needed? The perils of Many Ways to Do Things™

What a great pull. My kids, niece and I play this every Christmas, kind of a holiday tradition. Along with a Krampus screening. It is possible our holidays have evolved waay beyond their stodgy, doctrinaire roots.

EDIT: actually, we play the old Fantasy Flight boardgame, though per-location encounter decks are very much a key mechanic.

I think that, pseudocode-ishly, it looks like…

  • Call beforeTravel() on everything in the old location
  • Call travelerLeaving() on the old location
  • […]
  • Call travelerArriving() on the new location
  • Call afterTravel() on everything in the new location

…with everything else in the middle. That is, that’s the start and end of the process.

I think the main gotcha is that this is all only called for “formal” travel. If things get moved around as a side-effect of a non-travel action or other scripting stuff none of this will happen automatically.

Pretty much the same idea. FFG also makes the Arkham Horror card game (“living card game”, or LCG…which means there’s deckbuilding for play but no randomness in buying the cards, like a trading card game). And it’s based on the older (from the '80s, originally) board game.

In the board game (at least through 2nd edition; I don’t know about the current 3rd edition) you have static decks for each location: every time you visit the Silver Twilight Lodge you’re drawing from the same set of cards. In the LCG, you build the deck(s) per scenario, from a combination of cards from the core set and scenario-specific cards.

There’s also no board, the locations are also cards. At the start of the scenario some specific set of location cards are set out, all (usually) with their “unrevealed” side up. When someone enters a location (possibly paying some cost or having to pass a skill check), it’s flipped over and whatever action is on the other side is resolved. It’s also common for location cards to be replaced over the course of the scenario: the old house on the corner turns into the blazing inferno on the corner turns into the smoking ruin on the corner, and that kind of thing.

Anyway, the thing I’m doing/trying to do with events is a little less…self-contained?..than most of the encounter cards in either of the Arkham Horror games. Less of a “draw a card and resolve its effects” and more something like advancing a bunch of independent(-ish) timers. Basically one of the things I’m trying to do is make the just-traversing-the-gameworld part of the game less static. If the game is about birdwatching (which it isn’t) and the player is going into the forest every day to try to fill out their logbook, they’re also hearing rumors about bigfoot sightings or fairy circles or a film crew that went missing, and each time the player goes into the woods to look for birds, they’re also randomly getting fed little bits and pieces of the side stuff which unlock additional interactions in town.

2 Likes

On the sequence of the action, from the press of the return key to the return of the prompt, even myself got confused, to the point of humbly asking the collective knowledge of Intfiction:

which led to a point worth of (perhaps separate) debate: both rather extensive set of documentation, adv3 and adv3lite, apparently lacks a table of the sequence in which the various calls are done during the turn loop, and this can be an issue, if even experienced TADS coders find themselves wondering about when a certain method is called.

Best regards from Italy,
dott. Piergiorgio.

1 Like

A couple of random performance-related notes, coming out of a bunch of testing/refactoring/tuning I’ve been doing.

Most of this is only noticeable if you’ve got a lot of compute-heavy code, which I assume is a minority of IF games. But throwing this out there anyway.

There’s a measurable performance advantage to hoisting repeatedly-accessed properties out of loops

For example, consider a method that computes the dot product of two vectors, v0 and v1 (type checking assumed to be done elsewhere):

        dotProduct(v0, v1) {
                local i, r;
                
                if(v0.length != v1.length)
                        return(nil);
                
                r = 0;
                for(i = 1; i <= v0.length; i++)  
                        r += v0[i] * v1[i];
                        
                return(r);
        }

That will be slower than hoisting the length reference out of the loop:

        dotProduct(v0, v1) {
                local i, n, r;
                
                if(v0.length != v1.length)
                        return(nil);
                
                r = 0;
                n = v0.length;
                for(i = 1; i <= n; i++)          
                        r += v0[i] * v1[i];
                        
                return(r);
        }

This is also true for properties on the method’s object. So if you were calling foo.dotProduct() and foo.scale = 1000, then calling scale inside the loop will be slower than doing something like local sc = scale outside the loop and then using sc inside the loop.

Subclassing introduces similar amounts of overhead as a context switch, even with methods that don’t use inherited()

Kinda self-explanatory. Whenever you call a method, there’s a little bit of overhead associated with the call. So if you have a method on a subclass that calls inherited() to invoke the method on the parent, it makes sense there would be the same overhead as any other method call.

The thing that might a little counter-intuitive is that there’s overhead on over-ridden methods even if they don’t call inherited(). So if you have a class Foo:

class Foo: object
        foozle() {                
                "\ncalled Foo.foozle()\n ";
                return(true);  
        }
;

…and two subclasses…

class Bar: Foo
        foozle() {
                "\ncalled Bar.foozle()\n ";
                return(inherited());
        }       
;       

class Baz: Foo
        foozle() {
                "\ncalled Baz.foozle()\n ";
                return(nil);
        }
;       

…calling both Bar.foozle() and Baz.foozle() will be slower than Foo.foozle().

TADS3 games may run marginally slower on some newer CPUs compared to slightly older CPUs

I discovered this by accident because I’ve got a desktop that’s just a couple years old and a laptop that’s nearly twice as old.

Part of it is that every T3 interpreter I’ve seen is single-threaded, so they won’t take advantage of things like additional cores. That part’s not surprising, and won’t make things run slower, just not much faster.

But my nominally ~2.5x faster desktop CPU is actually ~2x slower than my laptop when running a TADS3 game.

After a lot of digging, I’m fairly confident the culprit is kernel/firmware-level workarounds for the Specter V2 vulnerability(-ies). The desktop CPU is recent enough to be vulnerable, and so applies the workarounds, the laptop CPU is old enough to be immune and so doesn’t.

This isn’t a massive deal. It’s something that turned up in performance testing where I was doing on the order of tens of thousands of NPC decision-making choices all at once (where in the actual game it’ll probably top out at around a dozen per turn). But instead of ~250 μs an update it was ~500 μs, which is a pretty serious relative cost, even if the absolute numbers are tiny.

None of these are a big thing, but I’m doing a lot of background stuff per turn, and a lot of it is comparatively number-crunch heavy (compared to what I imagine is true of the typical T3 game). So I’m kinda doing the “converting a street car for drag racing” thing of looking at everything to see what I can pull out and/or replace with a lighter-weight version.

1 Like