No Kicking While Down

Back in May, I wrote

By the way, the @help/@hurt idea is not implemented at the moment. I’m not sure if it will be, but the flexible tagging system makes it easy to add at any time.

This was indeed easy to add, and it was in fact handled mostly with script tags.

furrows inundatedThe basic idea behind these tags is that we want to avoid death spirals. If you lose a lot of cattle due to disease, the game won’t feel as fun if you then have a large herd mysteriously disappear. You’ll end up in a hole you won’t be able to dig out of. The occasional flood is fine, but if it happens when you are short of food, it’s just kicking you when you’re down.

Of course, mysterious disappearances and floods need to occur, so they can happen more often when you are able to recover. (This may still set back your plans for cattle trading or gifting, of course.)

One issue with implementing this adaptive difficulty is figuring out what constitutes a bad or good situation. The game takes a number of factors into account (herds, wealth, military, and even issues like curses or magical bounties). And scenes have to be tagged. Some were designed to be helpful or harmful, but many are more of a grey area.

It’s worth noting that we don’t actually force or prevent anything, just alter the odds for picking scenes randomly. You can think of this process loosely as “pick a card, any card” each season, where cards are returned to the deck after several years pass. If times are bad, we essentially put multiple copies of the favorable scenes into the deck, and replace and reshuffle if a disaster is pulled.

As in King of Dragon Pass, we also do something similar for the yearly omens. Maybe an ancient economy experiences crop failure about once very seven harvests, but two in a row seems like a bit much to hit the player with. The goal is to give them challenges, not an accurate simulation of widespread famine.

I added this feature only recently. It required going through all the scenes and determining which would be reasonable to use to help the player, or to make less common in bad times, and adding the right tag. The determination of good and bad times was already used to give advice, and needed only a little tweaking for the new use.

Since this is only just in the game, it hasn’t been tuned. So I’m also tracking the values in the metrics we capture.

Hopefully all this will keep the game challenging but not too frustrating.

Proofreading

Like King of Dragon Pass before it, Six Ages will have a lot of text. Which means a lot of opportunity for typos or other misspellings.

Most of the text is in OSL scripts, such as these excerpts:

saga: <expeditionLeader> was attacked, but returned home with <his/her> escort.
sagaText: The worshipers suffered the same fate.
text: We eventually pieced this together from stories told by wandering traders.

The scene compiler outputs all strings into a single text file. It looks something like this:

<expeditionLeader> was attacked, but returned home with <his/her> escort.
<expeditionLeader> was attacked and wounded, but returned home with <his/her> escort.
Unfortunately, <he/she> lost the livestock they were driving home.
<expeditionLeader> and <his/her> escort <disappeared mysteriously/were ambushed by trolls and completely devoured>.
The worshipers suffered the same fate.
We eventually pieced this together from stories told by wandering traders.

And that file can be spellchecked. I just use TextEdit. The biggest issue with proofreading is that the game uses a lot of proper names and jargon specific to Glorantha. Luckily it’s easy enough to add “Orlanth” to the dictionary (or the ignore list). More problematic is that variable names (like expeditionLeader) also show up here, though ignoring them usually works too.

Another issue is that it’s a big file. It may be generated by our tool but it still takes a human a while to review, so that doesn’t happen often (in fact the first complete review was today).

This is just a brute force pass. Many typos end up with words that are spelled correctly. And once in a while game-specific names get misspelled. So QA still needs to keep an eye open for problems.

Debugging: The Log

Like King of Dragon Pass, Six Ages logs all scripts, which helps show exactly what happened and the state of the game.

Right now a lot of my time is spent fixing bugs in the code. Given that the game is large and complex, it can be hard to know just what happened that led up to a bug. So we keep track of everything important.

This is actually something that we did in King of Dragon Pass. Shawn Steele was implementing our scripting language, OSL, and wanted a way to check that things like conditionals and calculations worked. So he came up with a way to log this to a file. You can see his focus from some of the options:

kAllBranches = 0x01,
 kListSizeing = 0x02,
 kTraceOSL = 0x04,
 kTraceMath = 0x08, // Extra COSL::StartMath debugging information
 kSetVariables = 0x10,
 kTraceFixed = 0x20, // Extra CFixed debugging information
 kLoadVariables = 0x40,
 kGetStrings = 0x80,
 kTraceTribes = 0x100,
 kTraceOSLVariables = 0x200, // CFixed::PrintFileDebug should print OSL Variable Names the hard way
 kTraceMathResult = 0x400,
 kTraceList = 0x800, // Attempt to show list content

The output shows addresses and opcodes, showing its focus on debugging the language:

OSL 0xd8b694 Running from 0, fRunDepth 0:
-------------------------------------------
0000 : 0202 019E Picture "scene018" 
0002 : 0203 0001 Position 0001 
0005 : 4005 E
 Loading Variable 4005 (E) 0.000 (gValue)
0006 : 0800 = 
0007 : 068D RandomElement 
0008 : 0480 ( 
0009 : 070E ClanMembers 
000A : 0841 - (subtract) 
000B : 0718 RingMembers 7ffa0000 -- P00000000:0021a700 (gValue)
 Result: 7ffa0000 -- P00000000:005e18fe (gValue)
000C : 0481 ) -- gValue = 7ffa0000 -- P00000000:005e18fe (gValue)7ff20001.0000 : Brenna (gValue)
 Setting Variable 4005 (E)
 Result: 7ff20001.0002 : Brenna (gValue)
000E : 4021 otherClan
 Loading Variable 4021 (otherClan)7ff30017.0000 : Blue Jay (gValue)
000F : 0800 = 
0010 : 068D RandomElement 
0011 : 0480 ( 
0012 : 068A NeighboringClans 
 4 COSL::Neighbors:
Clans:
 9 Boskovi
 15 Blackrock
 18 Greydog
 23 Blue Jay

0013 : 0481 ) -- gValue = 7ffb0000 -- C00000000:00848200 (gValue)7ff30012.0000 : Greydog (gValue)
 Setting Variable 4021 (otherClan)
 Result: 7ff30012.0000 : Greydog (gValue)
0015 : 4012 R
 Loading Variable 4012 (R) 0.000 (gValue)
0016 : 0800 = 
0017 : 0600 FALSE 0.000 (gValue)
 Setting Variable 4012 (R)
 Result: 0.000 (gValue)
0018 : 0212 01A4 Saga "<4005> told us we should take in Orlkensor Bronzebones, a warrior outlawed by the <21>.plural." 
 Loading Variable 4005 (E)7ff20001.0002 : Brenna (gValue)
ReplacePlaceHolders...7ff20001.0002 : Brenna (gValue)
 Loading Variable c321 (otherClan)00000000.0000 : Greydogs (gValue)
ReplacePlaceHolders...00000000.0000 : Greydogs (gValue)
001A : 0201 01CF Music "IsItAdventure" 
001C : 0A00 NewChoices

Once OSL was reliable, it turned out that the log was useful to help debug the OSL scripts themselves. If something weird happened, you could see what code branch was taken, and what some of the variables were. For example, the output above shows all the neighboring clans

And we output other information to the log, such as the scene queue, some of the clan decisions, and more.

The debug log can grow quite large (a quick search shows one at 7.7 MB), so the game makes a new one every time you launch. But if you had to relaunch because of a crash, that would mean that any evidence would be deleted. So actually, we rename the log, and actually delete the previous one.

While I reworked OSL for Six Ages, I based it on Shawn’s work, and wasn’t so concerned about debugging the language itself. Instead, I wanted to focus on the scripts, since that seemed like where most of the bugs would be.

<OSL: 0x170194ec0> ® 2 Affiance her to …
-----------------
: Saga "We affianced her to <.an> <ourClan> groom." {a} {C_1 Arrowstone}
: (ourClan).commoners {277}
: += 
: # 1 {277} {278}
 ourClan.commoners ← 278
: ChooseYesNo
: ( 
: String "Do you accept the 10 cows?" 
: ) ↖︎ = Do you accept the 10 cows?
sendToCurrentScene: kNewChoice
… exploded
Restarting OSL (self.result=kNewChoice)

This is more compact (thus easier to read): more values are shown on the same line, and it doesn’t bother to show addresses or opcodes.

The next post will talk more about how to make use of this.

Sweeps Week

s235-pencils-cropSince essentially all of the game’s scenes have been written and coded, I figured it’s time to start sweeping.

“Sweep” is the term we use when we find an issue, and realize that it could apply to every scene in the game.

For example, the bug report

I noticed a [q > 4] which is probably not possible

is something that could affect any scene. So today I searched every script (i.e. swept the code) for similar patterns (e.g. [q = 4] was also a problem, because in rare cases, the special variable q can indeed be set to 5).

And the task

Tag @cattleWard for Cattle Ward blessing

meant I had to go through every script and see if the blessing (“Protects our cattle herds from predators”) was relevant. If so, the script needed the tag. Going through everything, I realized that the description actually needed to change, to “Protects our cattle herds from raids and predators.”

The sweep that’s had the biggest impact has been

Sweep to be sure ChooseLeader is followed by a leader test

In other words, if you pick a leader, then it’s that leader’s Bargaining or Combat that will be used to overcome opposition. We had tried to catch these cases, but missed a few. And this was by far the most brute force sweep, since ChooseLeader is very common.

It looks like there are about 15 of these in our bug tracker, and about half are done. I expect to finish them this week.

 

Scene Tags

Since Six Ages is our second iteration of a storytelling game, I wanted to make sure it would be easier to do certain things. Being able to categorize scenes was something that was easy to improve on, since in King of Dragon Pass, everything was a special case. For example, when we wanted to make sure you got to deal with tribal disputes when you were a king, we had to list them all.

switch (COpal::DieRoll(4)) {
 case 1:
 scene = scene_R397TradeDispute;
 break;

case 2:
 scene = scene_R398MagicalDispute;
 break;

case 3:
 scene = scene_R399PoliticalDispute;
 break;

case 4:
 scene = scene_218ComplainORama;
 break;
}

This was fragile — scene_218ComplainORama was actually a fairly late addition in version 2.x, and I had to make sure that it was included in that code.

Likewise any news that was considered a random rumor had to be listed. This at least was data instead of being buried in C++ code, but it was still separate from the actual news.

SceneIDT CScene::gRumors[] = {
 news_FF13MoreChaos,
 news_FF14LessChaos,
 news_FF15MoreUndead,
 news_FF16LessUndead,

So I came up with the idea of tagging scenes. Here’s what the OSL documentation says:

Tags begin with @. They can be any word (e.g. @help, @hurt, @tutorial, @actThree, @endgame, @notEnd, @codeTrigger, @frequent, @infrequent). Tags can be purely informational, only a few have predefined meanings. Feel free to invent tags. Tags could be used when triggering scenes at random. (For example, if the player is doing poorly, there could be an increased chance of an @help scene. If the player is very wealthy, it might be time for an @hurt scene.)

So there could be an @kingly tag and an @rumor tag. Implementing a random rumor can start with something like

rumors = [self scriptsWithTag: @"rumor" ofType: type_News];

which searches all scripts for the tag (also choosing only news scripts). A typical rumor will include the tag

news: news_R14Quake
@rumor
speaker: WhoHasHighestSkill(ClanMembers, 'magic)
text: I don’t have to tell you about the earthquake, since everyone felt it.

Most of the tags are indeed invented (usually by Robin Laws as he writes scenes). To help check on consistency, the scene compiler outputs a report of tag usage, which lets us make sure it’s consistently @dwarves and not sometimes @mostali or @dwarfs. A few are used internally (e.g. @nameEntry marks a script that needs a name field added to the UI), but most of these let us control the story.

For example, Robin can create a scene where you perform anti-dwarf magic and write, “Scenes tagged @dwarves do not trigger 30 + q seasons,” which can be coded

DisableTag("@dwarves") 
t = "@dwarves"
w = 30 + q
triggerWithValue code_EnableTag t w

(This takes 4 lines instead of two because of some limitations in the OSL scripting language.) A typical dwarf-related scene would then be tagged

scene: scene_37DwarfTrade
scene037, left, random, @dwarves, @trade, mayRepeat

The arbitrary tags also help implement the magic system. For example, a magical effect can be defined as

{
 name = "Elf Friend", tag = "@elves",
 explanation = "Helps our dealings with the Aldryami"
}

which gives a bonus to any scene that has been properly tagged. One of the tasks to do once all scenes are coded is to verify the tagging for magical blessings that depend on it.

Robin extended the idea with what I call dynamic tags. For example, he came up with a scene which could have a variety of outsiders. So he wrote, “scene gains tag @dwarves.” The implementation is

AddSceneTag(ThisScene, "@dwarves")

Obviously, making sure a scene has the right tag for a random outsider is important when there are blessings that are tied to tags.

I’m also making use of tags to help ensure correctness. For example, sometimes a scene wants to trigger another scene years later, but perhaps a character in the scene is off exploring when the time falls due. We handle this by triggering a code fragment, which can handle whether the character is exploring, died, or whatever. This sort of check is usually added once we start testing, and it would be nice to make sure that we caught every place that triggered the delayed scene. So the scene compiler makes sure that any scene marked @triggerFromCode can’t be triggered directly (but only from a code fragment tagged @triggeringCode).

And our unit test that runs every script simply runs them in an arbitrary order. Scenes may rely on having been run in a particular order, so the unit test may need to do setup that would have happened in normal execution. So there are tags like @unitTestWithClan and @unitTestWithPerson. This isn’t necessarily an exact match for normal execution, but it works for this level of brute force testing.

By the way, the @help/@hurt idea is not implemented at the moment. I’m not sure if it will be, but the flexible tagging system makes it easy to add at any time.