An Overview of Tridek’s Scripting Workflow
This post describes the way we script the cards and balancing in Tridek. It is a quick walkthrough of our scripting system from a technical point of view, written by our Lead Developer Sebastian.
Our goal for our scripting system is to be able to on-the-fly modify all aspects of the cards that are not core gameplay. So the core rules and posibilites are implemented in C#. But all card behaviour is scripted and therefore it’s possible to adjust it without rebuilding the game. You can change the config and card definitions at runtime.
Excel to CSV
At first there is a CSV file containing global configurations like the number of cards everybody draws at the beginning of the game. It is like an INI file or a name-value-map. We used a CSV file because it easy so read, parse and you can store Excel files as CSV. Parsing is done via some regexp magic. Data is adressed via column name (stored in the first line of the CSV) and line. This way the file can contain any number of extra columns (like comments) that do not interfere with the game mechanics.
The card definitions are stored in another CSV with similar technical structure (using the same parser). Currently a card definition consists of 15-20 fixed basic values like color, costs and type. Its behaviour is described in another bunch of fields with some lines of code each. The number of these code fields depends on the card definition’s complexity.
Lua as our scripting language
We use Lua as a scripting language because it is small, powerful and well known. In most cases integration is also very easy. More easy than writing a custom parser and interpreter even if one uses something like ANTLR. One annyoing thing with Lua stored in CSV files generated by Excel are the different variations of “. Excel seems to have a habbit of not using the non-ASCII-space ” if you type a string. Now we replace all kinds or ” in the Lua code prior to sending it to the Lua interpreter.
We started using SharpLua (a Lua reimplementation) but encountered some problems. This interpreter lacks some features (eg. anonymous functions). So we switched to KopiLua (a line by line Uua port). Not the fastest one but fast enough. And the integration was nearly only standart Lua C stack calls. In our case the data flowing from C# to Lua and back consists mostly of scalar types (eg. attack value, card id) and global methods. So it whould be quite easy to switch to the nativ Lua c interpreter using NDK (if kopi gets too slow).
Most of the time the game core calls these Lua snippets to modify the basic card data (eg. attack value, cost) or trigger some actions (eg. draw a card, kill all red creatures).
This is an example of a Lua snipped changing card values:
atk = atk + 400 * countCards(function (c) return get_type(c) == CREATURE and is_in_playfield(c) and get_owner(c) == owner end)
The cards atk (=attack) value gets increased by 400 times number-of-own-creatures. countCards returns them number of cards that match the given filter function (function (c) …). The filter function matches only cards that are creatures (get_type(c) == CREATURE) belong to myself (get_owner(c) == owner) and are currently in play (is_in_playfield(c)). Prior to running this code the basic values of the affected cards get loaded into Lua’s global scope. “atk” and “owner” get their values this way. The other functions and constants are independent of the affected card. After running this line the modified “atk” value gets stored back into the cards current values. Each card stores a basic unmodified value and a copy of the value with all scripts applied.
This is an example of a Lua snipped that runs on a specified game event:
The matching card’s text is “Draw a card if you put this creature into play”. This snippet is bound to the “on_enters_play” event. The Lua code is stored in the “effect0” column and the trigger in “trigger0” in the cards.csv file.
The trigger is not the only additional info a script can have. There are also fields like conditions that act as an early out filter. Most card effects are only relevant if the card is in play so there is no need for running an atk-patching-script if the card in in the player’s hand. This way it is not necessary to write
if (is_in_playfield(id)) then atk = atk + 400 .... end
It is enough to write something like “ATK_ROW, RES_ROW” in the “active_in0” field.
The overall workflow from card idea to playable card on the device (just gameplay, no art) looks like this:
- the game designer adjusts values and scripts or create new cards in excel
- saves excel as cards.csv
- uploads the cards.csv (via Dropbox)
- starts the game and select the uploaded card definitions
- game retreives cards.csv from the net
- game parses cards.csv (basic sanity checks)
- optionally one can run runtime checks of all lua snippets to find typos and lua parse errors
- start a match with the loaded card definitions
So to sum this up: All in all we use some Excel spreadsheets stored as CSV files containing balancing values and small Lua scripts that specify the card behaviour.
Manually exporting the Excel docs to CSV every couple of minutes sucks so we use a couple of tiny VBS and batch files to automate this:
if WScript.Arguments.Count < 2 Then
WScript.Echo "Error! Please specify the source path and the destination. Usage: XlsToCsv SourcePath.xls Destination.csv"
Set oExcel = CreateObject("Excel.Application")
oExcel.DisplayAlerts = FALSE 'to avoid prompts
Dim oBook, local
Set oBook = oExcel.Workbooks.Open(Wscript.Arguments.Item(0))
local = true
call oBook.SaveAs(WScript.Arguments.Item(1), 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, local) 'this changed
"C:\[PATH]\tridek_git\balancing\XlsToCsv.vbs" "C:\[PATH]\tridek_git\balancing\cards.xlsx" "C:\[PATH]\tridek_balancing\cards.csv"