Developer diary

Dev Diary #5: A scripting language for strategy game rules

How are the content and rules of a strategy game written in text files in an engine? Condition, effect and formula languages, mod patches, limits and localization.

Contents

In short: In MKA Engine the content of a game (countries, technologies, tax rules, events) is written in text files with the .bsm extension. The scripting language has three forms: a condition tests something, an effect changes the state, a formula produces a number. The language is deliberately limited: there is no floating point, formulas have no loops and no assignment, every error is reported with file, line and column, and no input can crash the engine.

Think of a board game. The rulebook says what the game is: "a country can do this if its gold is above 100", "tax is a fifth of income". The person who writes the rulebook does not need to know how the referee who sets up the table works, but every sentence of the rulebook must be clear enough to leave no room for doubt. In MKA Engine the referee is the engine and the rulebook is the game's content files. This diary covers the language of that rulebook.

Why is content text?

In earlier diaries we saw the deterministic core of the engine. The game is a mod on top of the core: the game defines the tables, rules and numbers, and the engine only provides the machinery. Burying the game's content in Rust code would mean recompiling for every change of a number and would take the only tool from the player who mods. So content is plain text: it opens in an editor, goes into version control and its differences can be read.

All text files the engine reads carry the .bsm extension; a manifest named project.bsm at the root of the game says which content type lives in which folder and what its fields are. For example goods are defined in files under common/goods/; a file belongs to whichever type folder it sits under.

The file format

A file is made of key = value entries. A value can be a word, quoted text, a number, a date (YYYY.MM.DD) or a block in curly braces. # makes the rest of the line a comment. @name = value writes a definition valid only in that file, and $ARG$ is a macro parameter.

Decimal numbers are read not as floating point but as the fixed-point number we described in Diary #1; there is no float anywhere in the content language.

Files are loaded in a fixed order: first the order of the mods, then the byte order of the file path, then the order of writing in the file. Because the order is fixed, identities are assigned the same on every computer. That is a condition of multiplayer (Diary #2).

Three forms: condition, effect, formula

The work of the language is split into three questions. "Is this true?", "What should change?", "How much?" The form of each is separate and they cannot be mixed, so a condition cannot change something by accident.

The examples come from the example world in the engine's guide; they are not the rule of a real game.

A condition tests the state of a scope. A scope is a table row (a country, a province). For example a "rich" condition asks "is gold above 100?"; a "crowded" condition asks "is the development of one of the country's provinces above 2?".

Entries are combined with AND; there are logic blocks such as OR and NOT. If a link is empty the condition counts as false; it does not crash.

An effect changes the state. It runs in the order written and each effect sees the state the previous one left. For example a "collect tax" effect works like this: if gold is below 100 add 5 gold, otherwise add 1 gold.

A formula produces a number. Operations run in the order written in a single accumulator. For example a "tax base" is a fifth of gold and never goes below zero; "income" adds the tax base to 100.

FormIts questionDoes it change state?Can it draw a random number?
ConditionIs this true?NoNo
FormulaHow much?NoNo
EffectWhat should change?YesOnly at an event, command or hook
Three forms

Why does the formula language have no loops?

The formula language is deliberately without loops and assignment. The reason is twofold. First, everybody's computer must do the same calculation in the same order; an expression without loops is known to finish from the start. Second, because the game is a mod, others will write content too; a formula that cannot freeze means that a modder's mistake does not hang the whole game.

Effects can walk: with the prefixes any_, every_, random_ and ordered_ the members of a collection are visited. But these walks are limited too: at most 1,000,000 members are visited in a single evaluation and while runs at most 1,000 turns. If a limit is passed the script stops and records an error; the game does not crash. These limits are a setting of the game and, because they affect the result in multiplayer, they must be the same for all players; that is why they enter the digest of the mod set.

How does one mod patch another?

A later mod can change the record of an earlier mod in three ways:

  • key = { ... } replaces the whole record,
  • key.field = value changes only one field,
  • key = delete removes the record.

If two mods change the same record the conflict is not passed over silently, it is reported. This prevents the surprise of "my mod overwrote yours".

When does content run?

This relates to the order of a game day: events and actions that run every day work in the content phase of the day, after commands and timers and before systems. The small but important consequence: a condition sees what systems and hooks wrote in the previous step, never in the same step. A mark a system puts today acts tomorrow. What content writes, the systems of the same day do see.

Randomness follows the same discipline: a draw is made only with random and random_list, only at an event, command or hook, and it comes from the stateless generator of Diary #1. A script cannot produce random numbers any other way.

Three contexts: who reads what?

The same script can be evaluated in three different contexts:

  • Simulation: reads only data that is the same on every machine; the result is the same for everyone.
  • Local (interface): can read interface state but writes nothing and draws no random number.
  • Hypothetical: a "what would happen if I did this" calculation, and it does not disturb the state.

Machine-specific information enters the simulation only as a command; because a command goes over the network to everyone, it is the same for everyone.

Multilingual text

The visible texts of the game are not in the content files but in one localization file per language: localization/<language>.bsm, with <language> = { key = "text" } inside. The first language is the reference language; a key missing from the reference language gives a diagnostic, so a text is not shipped forgotten.

How do we check the rule we wrote?

Two tools are used while writing content. bsm check reads the files: an unknown field, a value of the wrong type or a missing reference is reported with file:line:column. It also warns about costly patterns, for example walking every row in work that runs often or walking again inside a walk. bsm test runs the small content checks we write for every new rule: a starting state, a few steps and the expected result, for example "if a country triggers this rule its gold should rise by this much". Writing the check together with the rule is our habit.

Limits

  • The language is deliberately narrow: No loops and no assignment in formulas. If a complex calculation is needed it is left to a WebAssembly mod written in Rust or to engine primitives.
  • The keyword list follows the example world: The list of keywords is produced from the engine's example world. The scopes and content types of your own game are defined in project.bsm; in the first version this guide is based on that example world.
  • Limits are a safety net: Limits such as 1,000,000 members and 1,000 turns prevent freezes, but they do not make a costly rule fast. Slow patterns are caught by bsm check warnings and by measurement.
  • No measurement: In this diary we give no measurement of the speed of the language; what we describe is the form and rules of the language.
  • The engine is in development: The details and keywords of the language may change.

Frequently asked questions

Are strategy game rules written in code?

In MKA Engine the content and rules of the game are written with a scripting language in text files with the .bsm extension; the engine core stays in Rust. If a new system is needed it is added with a WebAssembly mod written in Rust.

Do you need to know programming to write a mod?

Not for content: a good, an event or a tax rule is written as key-value in a text file. A mod that wants to add a new system to the engine needs Rust.

Does the scripting language have loops?

Not in formulas. In effects there are prefixes like every_ that walk a collection, and while, but both have an upper limit, and when it is passed the game does not crash; the script stops and records an error.

Can a script error crash the game?

No. Every error is reported with file, line and column and no input crashes the engine. If a link is empty the condition counts as false; if a limit is passed the script stops.

Sources and records

External sources:

Our internal records (the engine repository is closed, so we cannot link them):

  • The game is a mod too: logic is written in Rust as WebAssembly, content as data files and a formula language without loops.
  • Content language decision: an ordered effect language, a formula language without loops and assignment; effects run only at an event, command or hook.
  • Script contexts and limits: three evaluation contexts, freeze-preventing limits and their management as a setting of the game.
  • Text format and multilingual support: the .bsm extension, the manifest project.bsm, one localization file per language.

More on this will come

MKA Engine is still in development. What we have to share about the scripting language does not end with this diary: events and multipliers, writing the AI with content, the content side of war and economy, and modding tools will come in separate diaries, each with its own detail. As the language grows, we will add to this with new diaries rather than update the information here.

If you have questions or ideas about this diary, write to us from the contact page. For an overview of the engine see the MKA Engine page, and for all diaries the Developer Diaries list.

Sources and related docs

You may also like

  • How is a strategy game's world map made?

    For MKA Engine we built a 27,357-location world map of 1337 from real data: how terrain, rivers, straits, population and location borders were made.

  • The road to a game engine

    How we started MKA Engine: reading four games, treating determinism as law, moving phase by phase with measurements, and where we are now.