Interactive fiction in Lua – Part 03

 Third part – Object IDs and object properties

For my interactive fiction game I want to access my objects via ID codes. The benefit of this approach is that you don’t need to move around a lot of data when moving the objects in the game. While you could compress the object IDs to hexadecimal format or using bits and bytes, I prefer having the IDs clearly readable for code debugging reasons.

    object 1 = ‘ID000001’
    object 2 = ‘ID000002’
    object 3 = ‘ID000003’

Below is a simple function I wrote that transforms numbers into an ID code format like the above example.

function get_ID_format(id_short) -- Returns a long ID
    local maxlength = 6
    local num = tostring(id_short)
    local zeros = string.rep("0", maxlength - #num)
    return "ID" .. zeros .. num
end

local id_long = get_ID_format(1)
print(id_long)

Above, tostring(id_short) transforms the value of id_short into a string.

string.rep replicates the first argument “0” as many times as needed; maxlength – #num.

#num returns the length of the number string, similarly like writing string.len(num).

Finally the function returns the ID. The string is comprised of “ID” + the number of zero’s + the short id number in string form. The double periods “..” concatenates the strings together as a single string. Print result: ID000001.

With the object ID approach, moving objects from a room to the player inventory, or vice versa, is relatively simple and mainly involves moving the object IDs around, combined with changing a few properties.

 Object properties

At this point I would like to store object properties in a global table, which we could call g_objData.

g_objData = {} -- initializes 'g_objData'

In Lua it is useful to store data not only in numbered dimensions but also by using keys (strings).

Let’s say that I want to set up my object properties like this;
two functions are listed below, with the main code that calls the functions placed last:

function add_obj(room, name, liftable, held, visible, child_of, contain)
    local id_short = g_nextObjectID
    local id_long = get_ID_format(id_short)
    g_objData[id_long] = {
        room = room,
        name = name,
        child = child_of,
        carry = liftable,
        held = held,
        visible = visible,
        container = contain
        }
    g_nextObjectID += 1 -- increase ID for next object
end

function get_ID_format(id_short) -- Returns a long string ID
    local maxlength = 6
    local num = tostring(id_short)
    local zeros = string.rep("0", maxlength - #num)
    return "ID" .. zeros .. num
end

g_objData = {} -- initializes 'g_objData'
g_nextObjectID = 1 -- assigns ID 1 to the first object

add_obj(1, "box", 1, 0, 1, "", 10)

local id_long = "ID000001"
print(g_objData[id_long].name)

Above, the function add_obj() is called with a few parameters that can be used for creating a simple box object. It has a property that tells us it can be carried, though the object is not yet held by anyone. It is visible and have a container property of 10, which could be used to define weight capacity, volume or a certain number of items.

Every time the add_obj() function finishes, it increments the global variable g_nextObjectID with one so that whenever the function is called, the next object will have a unique ID.

After the function add_obj() is called, the global object property table g_objData will be filled like this:

g_objData.ID000001.room = 1 -- Object room location
g_objData.ID000001.name = "box" -- Object name
g_objData.ID000001.child = "" -- Child of another object
g_objData.ID000001.carry = 1 -- The object can be carried
g_objData.ID000001.held = 0 -- Player holding the object
g_objData.ID000001.visible = 1 -- Object visibility
g_objData.ID000001.container = 10 -- Container capacity

This table could also be shown as such:

local id_long = "ID000001"

g_objData[id_long]["room"] = 1
g_objData[id_long]["name"] = "box"
g_objData[id_long]["child"] = ""
g_objData[id_long]["carry"] = 1
g_objData[id_long]["held"] = 0
g_objData[id_long]["visible"] = 1
g_objData[id_long]["container"] = 10

print(g_objData[id_long].name)

The last line in the above example would print out “box” in the console window, provided that the table g_objData[id_long] has been filled using the above function add_obj(), or a similar function.

Next, I would like to access the object data in the game through at least two functions;

        1. reading the data
        2. setting the data

Let’s create two functions for that:

        ‘get_obj_property()’
        ‘set_obj_property()’

Below is an example of a function returning the object property data:

function get_obj_property(id_long, type)
    local data = g_objData -- 'data' points to table g_objData
    if id_long and type and data then
        if id_long == "" then
            return 0
        else
            local data_id = data[id_long]
            if type == "name" then
                return data_id.name
            elseif type == "room" then
                return data_id.room
            elseif type == "held" or type == "carried" then
                return data_id.held
            elseif type == "liftable" then
                return data_id.carry
            elseif type == "visible" then
                return data_id.visible
            elseif type == "container" then
                return data_id.container
            elseif type == "child" then
                return data_id.child
            else
                print("Error. No '"..type.."' in '"..id_long.."'.")
                return 0
            end
        end
    else
        return 0
    end
end

print(g_objData[id_long].name)
print(get_obj_property(id_long, "name"))

The function above is called with two arguments; first the long ID string, then the string type, here providing the object property key that have already been defined in function add_obj().

First the function checks to make sure that three variables are not nil; id_long, type and data, since variables that hold a nil property will throw an error when they are accessed. If any of them are nil then a zero value will be returned. The main part of the function returns the object properties of g_objData[id_long][type] by using the local pointers.

Notice that there is only one reference to the table g_objData at the start; the rest of the function uses local pointers. The local table data points to the global table g_objData and local variable data_id points to g_objData[id_long]

Using a few central functions gives more flexibility than asking for

        g_objData[id_long].name
        g_objData[id_long].held
        g_objData[id_long].child
        g_objData[id_long].container

from numerous other functions or places in the code. Instead, you just call your designated function/s and let it handle the method of accessing the data you need, without burdening the rest of the program to bother about the exact details involved or which table is used to achieve the result.

 A few thoughts

I created the “child” property in order to place objects inside or upon other objects. The child property is intended to hold the ID of the object which it is a child of, i.e. parented to.

When I designed my room descriptions I decided to not mention objects that are on top of, or inside, other objects; I want the player to look at objects in order to find out what they contain.

If the player wants to see objects on a desk for example, they will have to examine the desk first. When the player inspects an object, a function checks which objects in the room have a child property matching the ID of the object the player is looking at. Those object names are then listed in a string after which another function is called for grammar correction before the description is shown to the player.

———

In later posts I will talk about room arrays, inventory handling, room layout, portals between rooms, player input and analysis, functions for grammar correction, placing room prefabs and adding new regions that can be traversed between.

Each new region will come with its own region-specific tables of data for rooms, portals, objects, room descriptions, illustrations, and so on. By changing pointers at a later stage, the data from one region can easily be swapped out by pointing to the data set of another region.

Interactive fiction in Lua – Part 02

  Second part – variable naming and starting out

When you code you want to set up an easy-to-read naming convention for your game. There are a number of Lua style guides for suggestions, but at the end of the day it’s up to you design the structure of your code.

Personally I like to write a few short notes at the start of my program to keep me organized. Choose whatever naming conventions you like but make sure to keep order in your naming rules. Example:

–– Variable naming and notes:
––
–– global variables = g_camelCaseNames
–– functions = silent_snake_case()
––
–– FIXME = Issues that need to be fixed
–– DEBUG = Temporary debugging code; delete when not needed
–– DELETEME = Old functions or sections that are due for deletion

It is my preference to make global variables visible by prefixing them with ”g_”. While this convention makes the variables longer, it also pressures me into reducing the number of global variables overall, which is a good thing.

Having the global variables clearly visible at an early stage is preferable for me rather than having the hassle of changing their names at a later stage. It reduces the cluttering of the namespace, especially when you are working in an environment that gives suggestions for the variable you are typing.

 Method independence

It is helpful to aim at making the code as independent of methods as possible. When you change the architecture of an object property, a player stat or how you handle queries for game data, it is generally preferable to have the method being handled by a designated function independently.

The reason for this is that you will likely change the methods of data access during development, perhaps several times, and you want to make the actual nitty-gritty details of the method handling centralized;

Instead of re-writing, let’s say, 100 lines of code when changing your data structure, you would rather just change a central function and its method. Or at least, reduce the workload and the risks for bugs being introduced as far as possible.

In the next post I will continue with example functions for object ID’s and examples of method independence.

Writing interactive fiction in Lua

I wanted to write a story in the vein of the text adventures and fighting fantasy books that I grew up with during the 1980’ies. There was something magical in opening up a new book in the Lone Wolf series by Joe Dever and become immersed in a story with simple format; text, rudimentary player decisions and a set of compelling illustrations to set the atmosphere.

My first exploration of ideas were made in Twine, which is a free software that is easy to use for starting up an interactive fiction experience. However, I soon realized that my ideas involved some more complex gameplay mechanics that I would like to implement, and while some of them would be achievable in Twine, I decided I would rather get back into programming; the benefit is having far greater control over the story development and player experience.

Why Lua?

The short answer is that I find it an enjoyable environment to work in, it’s free and it has easy readability. If I am going to spend a few years in the code I want to buckle up with not only coffee, but making sure I am having as much fun as possible during the journey. If Lua isn’t giving you the right spark for your creative ideas then make sure to pick something that does.

While Lua doesn’t have classes like C, it offers tables and meta-tables where you can build complex class-like structures when necessary, with the option to integrate C functions for additional speed, if needed. The main example that comes to my mind is NPC pathfinding, but apart from that, speed is not a central requirement in a traditional style interactive fiction game.

Lua options

There are many easy-to-use free options for writing in Lua, such as Love2d, Corona, Gideros and a ton more. I have had positive experiences with Löve/Love2d, but decided I will write my early game prototype in Gideros since it is a platform that is easy to get started with and I have found it rewarding so far.

My initial focus is building the data structure and basic mechanics that the game will rely on. At a later point I may migrate my code to another Lua platform so I will attempt to keep the code flexible and easy to change.

For this type of game I consider the data structure to be of a more fundamental value than the particular language I use for coding; developing the data structure is what takes most time and consideration. The actual coding is more or less just spending time implementing the desired structure.

When it comes to graphics my needs for the game are simple; illustrations combined with text and a user input field. While Love2d have better graphical capabilities than my chosen platform, Gideros provides the necessary functionalities that I currently require for my project and I find its framework fairly effective and fun to work within.

————

The fun part – starting out!

I will be using both standard and Gideros Lua for my examples. Gideros have a couple of Lua enhancements and I will list a few of them here:

–– Standard Lua code example:

playerHealth = playerHealth + 1
playerHealth = playerHealth + 10

-- Gideros mutation operators with the same result:

playerHealth += 1
playerHealth += 10

Documentation link: Gideros enhancement: mutation operators

–– Syntax for arrays, standard Lua code:

array[1][5] = 2

–– In Gideros, the array dimensions can be written as either:

array[1, 5] = 2
array[1][5] = 2

Documentation link: Gideros enhancement: easy arrays

–– Gideros larger and smaller operators examples:
x = a < > b –– Compares a with b, return the largest number.
      –– This method is faster than x = math.max(a, b)
x = a > < b –– Compares a with b, return the smallest number.
      –– This method is faster than x = math.min(a, b)
x = (x – 1) < > 5 –– Decrement x, but not below 5.

x = (x + 1) > < 15 –– Increment x, but not above 15.


Documentation link: Gideros enhancement: greater-smaller

———

In the next post I will continue with some more direct examples for writing an interactive fiction game in Lua.