Kinship

Six Ages is a game about clans and kingdoms and world-shattering events. But it’s also a game about people. In the first game, “Ride Like the Wind,” you watch (and help guide) some characters pass through their youth to adulthood, and possibly even to old age.

The second game, “Lights Going Out,” is no different. It goes a step further by presenting characters who are related to each other more directly than belonging to the same family.

I have friends who raised their children otherwise, but I think in most families, children don’t refer to their parents by their given names. So the bug report complaining that this can happen made sense. Here’s what the line of script looked like (<a> is a placeholder, filled in with a character’s name):

[Diplomacy ≥ 4] The Wheels have more to worry about than what <a> thinks of them. [1]

If this advice was given about the advisor’s mother, it would feel out of place or even rude.

Another example has the advisor directly talking to someone who might be their elder:

[Default] I’m sorry, <i>. [3]

How to deal with this? In many events, we have advice specific to various characters, such as the king and the prince. But this situation could happen with a number of people, and that felt cumbersome (as well as potentially blocking useful skill-related advice in favor of character-specific advice).

We could simply live with this, since it’s pretty rare (we only noticed an example this week) and doesn’t impact game play. But it does weaken the idea that your advisors are real people who behave in complex ways. So I decided to add a few special properties to our scripting language. Those lines of advice are now

[Diplomacy ≥ 4] The Wheels have more to worry about than what <a.relative> thinks of them. [1]

[Default] I’m sorry, <i.relativeDirect>. [3]
Advisor says, “The Wheels have more to worry about than what my mother thinks of them.”

If most characters give the advice, the placeholder <a.relative> is still the name. But if the speaker is related to the person in question, it will be filled in with something like “my mother” or “my son.” The second form, when one character talks to another, essentially drops the “my,” so the complete sentence could be “I’m sorry, Father.”

This is a pretty specialized substitution, but it’s something that’s really only practical because the game uses its own scripting language.

Word Gets Around

In Six Ages, your relationships with other clans are vital. The game tracks a number of factors for each clan, including how much they like and respect you, any slights they hold against you, and whether they have a formal alliance or feud.

These factors are all dynamic. They’re adjusted not only when you interact with a specific clan, but also when you do something that is likely to be newsworthy and spread to other clan. For example, in one event a combat hero can gain the respect of a clan and its neighbors. This OSL code handles that:

otherClan.fearsUs += 40
otherClan.likesUs -= 40
# all clans adjacent to otherClan .fearsUs +=20
a = AdjacentClans(AllClans, otherClan)
foreach c in a {
    c.fearsUs += 20
}

Other events have even wider impact (this one on all Rider clans):

# LikesUs+=3 * q (all Hyaloring otherClans)
h = HyaloringClans
foreach c in h {
    c.likesUs += 3 * q
}

But these are just numeric values. As Sin Vega discovered in her attempt to play as a petty jerk, your deeds can earn you a more specific reputation:

“We are known for our rudeness”, says the lawspeaker later that year. It’s nice to feel appreciated.

“We are known as backstabbing weasels”, says the lawspeaker at Sacred Time. I might have gone too far this year.

These reputations reflect a suggestion (case 8801) from our QA lead Liana Kerr, who wanted a way to give players a stronger connection to their clan.

Behind the scenes, there are a number of trends or events that you can become known for. A single act of mockery towards guests won’t give you a valley-wide reputation for rudeness. But they accumulate, and if you keep at it, sooner or later this will be the most significant thing you’ve done. By the same token, attacking your allies is a shocking breach of tradition and protocol (not to mention good sense), and that gets everyone’s notice. Over time though, any of these can be less notable in people’s memory if you no longer do them.

Our explorers visit the Great Glacier

So each “known for” item has a numeric value, which indicates its significance. Visiting the Glacier is a big deal, but not as unusual as managing to defeat the Alkothi. But if you raid several times each year, the raid score will accumulate until it outweighs defeating the Hell Men. Or perhaps by then that feat was too far back to matter any longer, since the score for each item is reduced every year.

BecomeKnownFor("glacier")
An advisor says, “The clans of the valley know us for our many shrines.”

An advisor says, “The clans of the valley know us for having heroic clanfolk.”

Most of these actions are also tracked numerically through the standard relationship system (if you can defeat the Alkothi, you will gain much respect throughout the Valley), but others are a great way to reflect other player accomplishments like temple building or nurturing heroes. And the next chapter, “Lights Going Out,” will have items specific to its story. It’s a flexible system (we added several more late during development), and is a colorful and concise way to comment on the player’s story.

Tags are Magic

I was going through some playtester comments, one of which noted that diplomacy-related magic wasn’t as useful as it might be. So I did a quick review of the four blessings that seemed like they would relate to diplomacy. And while I’m not sure I was looking for exactly what was reported, it did seem like they could be more important.

Understanding: Helps our dealing with foreignersI’ve mentioned before that scene tags have been very useful. One of the diplomacy-related blessings is called “Understanding.” It’s implemented as

+1 in scenes tagged @foreigners

Diplomatic missions can be sent to a variety of people, so scripts like news_GiveGifts (which reports on simple gift-giving) can’t simply have the tag. But tags can be dynamically added, so making the magic more broadly useful was a matter of

RemoveSceneTag(ThisScene, "@*")    # Any previous dynamic tags
[otherClan.culture = 'other] AddSceneTag(ThisScene, "@foreigners")

Even though it takes two lines of OSL, I like this better than something like

[HasBlessing(ourClan, "Silvertongue")] b += 1

(which another blessing needed) because it affects the entire script, rather than just a specific branch.

The game makes extensive use of tags. The scene compiler uses a few to make sure scripts with very particular conditions are triggered from a single spot. Unit testing uses ten tags so it can set up the right context for running scripts. The UI code checks for tags that determine that a scene needs special elements like a text field. And there are over 100 tags that help categorize scripts, including whether magic applies to them.

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.