Stupidly Ambitious

Argus, regrettably, doesn’t have a specialised authoring tool – not yet, anyway. Authoring is done with just a text editor. Within the last few days, I’ve added file-inclusion, so that I’m no longer chasing my tail across 13,000 lines of source-text trying to find a particular scene. More on the files later.

If I had to characterise the project, I’d call it “stupidly ambitious.”

Continue reading Stupidly Ambitious

Making inferences

Earlier on in the development of Argus, I made the decision to not bother checking if a character in the story already knew a piece of information before they ‘learned’ it.

My thinking at the time was that the act of looking up the information was essentially the same cost in time and resources as learning it. So, we may as well just learn it. If it altered what they knew, fine. If they already knew it, still fine. Nothing changed.

Then the knowledge inferences/assumptions system got more advanced. Story characters learn about each-other as the narrative progresses. Knowledge about the gender and approximate age-group of speakers is acquired by listeners, when the character speaks, and information is combined with other pieces of knowledge to determine, for example, just how characters should refer to each-other during the narrative.

Eventually the process of looking up lots of disparate pieces of knowledge, filling in with inferences and assumptions, and then producing a reference-model for referring to other characters (in a variety of levels of formality) started to get a bit expensive.

So, I finally bit the bullet, and added a method to the knowledge graph to test if something was already known before we tried to get a character to learn it. That way we can assess if knowledge the character consists of any new knowledge that might require updating their assumptions/inferences.

That means double the workload for any learning a character does (which happens a lot), but the reduction in reference-model updates actually pays back that cost almost fifty times over. The story can be processed even faster than before, with fewer spikes in CPU usage.

Overall, that’s a win.

It did lead me to code this peculiar little construct, though:

KnStore* KnStore::find(statement_t &statement, const std::wstring &p)
{
 if (statement.size() == 0 && this == nullptr)
   return (KnStore*)1;
...

DO NOT TRY THIS AT HOME!

Yes, I’m intentionally creating a bad pointer. I already had a find() method that is used to locate knowledge insertion points. Producing a contains() method (does the knowledge already exist) could use exactly the same logic (that is, call find() and test the returned pointer), but always ran off the end of the tree to a null KnStore instance, if and only if it was successful.

That was easy to subvert, and I can return a dummy pointer that no code path ever dereferences (I could also have thrown an exception, but at an additional cost).

Honestly, if I ever saw code like this in someone else’s software, that would immediately raise questions and not a few red flags. Surprisingly, in this case, it’s the perfect solution for a very specific problem.

 

Decoupling

As a result on doing some more work on Argus’ story compiler (which makes the module sound far more impressive than it actually is), I discovered that the early development prototyping had made the core engine code (the really portable workhorse library) far too tightly coupled with the platform-specific UI code.

That is, there were sections of the engine library that you couldn’t pull in without having to satisfy a lot of platform UI dependencies.

Thankfully, Observer patterns came to the rescue, and I was able to use these to decouple the engine library from having to have any direct knowledge or linkage to the UI module.

Well, mostly. Almost every case has been handled, but there’s still two parts left to do: The display of interstitial images, and managing the UI elements for character selection.

That’s all just a matter of dog-work, though. Sit down and actually cut the code. Bum on seat, code in editor.

Inheriting from STL containers

There’s actually a lot of argument, and frequently a lot of doubt as to whether you can create a class that inherits from a C++ STL container.

You absolutely can!

If you weren’t allowed to do it, the containers would be marked final, and they most certainly are not.

That said, STL containers don’t have virtual destructors, which means that while you can inherit from them quite safely and effectively, you must absolutely not perform polymorphism on them (do not try to access or delete them from a pointer to the base class. Period).

For most of the cases you might want to inherit from an STL container, that’s not really an issue.

I have several cases in Argus where I want to put some extra code into one or more of a container’s methods. I do it for certain kinds of error-checking or data-enforcement, where I don’t want to write variations of data-tests in dozens of places, or write dozens of exception handlers, or insert dozens of calls to data-validation functions.

Because Argus is data-driven, the original data is prone to have typos, duplicate data, references to things that don’t properly exist, and more. It is important to handle these cases as gracefully as possible, rather than taking the traditional route of aborting when something unexpected happens, or the slightly more modern version of just polluting your containers with junk and then behaving strangely.

Here’s how one of them looks:

class OidList final : public std::vector<Oid>
{
public:
 OidList() = default;
 OidList(const OidList&) = default;
 ~OidList() = default;
 inline void push_back(const Oid& val) {
 if (val == Oid::None)
 return;
 if (std::find(std::vector<Oid>::begin(), std::vector<Oid>::end(), val) != std::vector<Oid>::end())
 return; // Already in the list
 std::vector<Oid>::push_back(val);
 }
};

In this case, I wanted a vector of Oids (an internal Object type I use) to have certain properties. That is, I wanted all the elements to be unique (no duplicate Oids!), and I wanted one specific value of Oid (Oid::None) to never appear in the list.

There are other methods I might have modified, but my code only ever adds elements to an OidList via push_back() and never by any other method, so I’ve only got one place my checks are needed.

OidList itself started out as a simple typedef (typedef std::vector<Oid> OidList;), but with this redeclaration has been promoted to a first-class … well, class. And it is drop-in compatible – that is, I was able to simply remove the typedef, put this in, and the code compiles and runs without complaint or issue.

So, next time someone tells you that you can’t or shouldn’t inherit from an STL container, remember that you can, and that it is perfectly safe to do so, so long as you keep two things in mind:

  • No polymorphism. Never use pointers to the base class.
  • If you’re overriding methods, take care not to break the way basic container functionality works (begin/end/iterators, etc) or you’ll find that your standard algorithms will have some trouble trying to operate on your new class.

 

Argus and SNAFU

Argus

Argus is the Argus Story-Engine, and is a hand-rolled engine for choice-based interactive-fiction, not dissimilar to visual-novels.

One of the key features of Argus is that rather than the story progressing for a single protagonist character based solely on player-choices, Argus allows for multiple protagonist characters, each with story-defining choices to make during the course of play. The human player only experiences the story from the perspective of one of the protagonist characters at a time.

This does mean that the outcome of the story is never entirely in the hands of the player. Each of the other main characters will be making their own choices based on their own knowledge, opinions and preferences. You could progress through a story the same way many times and experience quite different narratives, depending on the choices other main characters in the story are making.

Likewise, the human player may choose any of the available main characters at the beginning of the story, and progress through the story from that character’s perspective, making choices for them. All of the main characters experience their parts of the storyline simultaneously, and every character’s choices influence the emerging narrative.

Argus does not (well, not at this stage, anyway) support the sort of graphics that you’d expect from most visual novels. Instead, it mostly uses still images for thematic illustration. That may change if more art support comes along, but Argus is currently focused on the mechanics of choice, and a kinetic style of narrative presentation, which can foster a great deal more immediacy than more traditional written-word works.

SNAFU

SNAFU is the prototype story/novel for Argus, and consists of (presently) thirteen main (that is, playable) characters, each with their own story and part in the larger story, plus the usual supporting cast of minor (non-playable) characters. This number may or may not grow as large as eighteen before the work is done.

SNAFU is a story for adults. It contains mature themes, strong language, and tobacco and alcohol use, but contains almost no violence, and no explicit sexual content.

The story is set around a diplomatic incident between two fictional nations (Westfell and Fracia) during a period not dissimilar to Earth during the early years of the 20th Century when technology was still crude, but advancing in rapid leaps. Borderline industrial steampunk, if you like.

The (spoiler-free) diplomatic incident in question is based on a melange of international disputes that have taken place in our own history within the last two centuries, many of which are far more absurd than this particular tale will likely turn out to be.

SNAFU currently consists of a shade over 128,000 words, which is not as impressive as it seems, as those words are divided up between so many characters.

Nevertheless, the project has gotten through a rather weighty selection of prologues and is well on the way to the end of the first chapter, and progress is steady.