Previous: Civilization II Objects and If Statements Next: Variable Scope and Sharing Data

Loops and Tables 🔗

Introduction 🔗

Over the past lessons, we’ve been gradually learning about different parts of the Lua Programming Language, and the basics of how they are used. In this lesson, we’ll introduce the last two major components of the language: Loops and Tables. Loops are a way to execute a section of code multiple times and tables are a way to manipulate multiple values at once and to store data.

The Numeric For Loop 🔗

In programming, a “loop” is how you tell the program to run the same portion of code more than once. In Lua, loops come in a couple different varieties. We’ll introduce loops by looking at the numeric for loop. In VS Code, open the lua folder within your Test of Time directory, which we worked with during lessons 2 and 3. Open a tab with the conversion.lua file. It should look something like this:

conversion.lua

In this file, we convert 3 numbers between miles and kilometres. Until now, if we wanted to print many conversions, we would have to copy and paste each command over and over, and then change the numbers. Now, however, we can use a loop to simplify the process, and reduce typos.

for numKm=1,10 do
    print(numKm.." kilometres is "..kmToMiles(numKm).." miles.")
end

Let’s break down this code:

for 

The for keyword tells Lua that a for loop is being created.

numKm=1,10

numKm is a local variable to be created for use within the loop. =1,10 means that numKm will first take on a value of 1, then 2, then 3, and so on, incrementing by 1 each time until 10 is reached.

            do
    print(numKm.." kilometres is "..kmToMiles(numKm).." miles.")
end

The code between do and end will be executed for each value of numKm that the loop specifies. In this case, a message with a unit conversion is printed to the console for each value of numKm. conversion.lua will look like this:

conversion.lua - 2

Load a saved game in the Original folder (not ClassicRome) and run the script. (The ClassicRome scenario disables global variables, and this file has some.)

km to miles chart

For loop variables do not have to increment by 1. In fact, they can decrement if you prefer. Let’s look at an example:

for numMi=10,1,-2 do
    print(numMi.." miles is "..milesToKm(numMi).." kilometres.")
end

The following part is different from the previous example:

numMi=10,1,-2

In this case, numMi starts with a value of 10, and has an ending value of 1. What’s new is that the third number, -2, is the increment value for numMi. In this case, the “increment” actually reduces the value by 2 each time. So, numMi will be 10, then 8, then 6, 4, and, finally 2. The next value after 2 would be 0, but 0 is outside of the values of 10 through 1, so the loop stops there. The loop is not run for the case when numMi is 1 because the increment and starting value didn’t cause it to take on a value of exactly 1.

Add this loop to conversion.lua, save, and run the script again.

two loops run

A For Loop in a Function 🔗

Now, let’s do something slightly more complicated. In math, $6!$ is called “six factorial” and means $6 \times 5 \times 4 \times 3 \times 2 \times 1=720$. In general, $N!=N\times(N-1)\times(N-2)…3\times2\times1$. We will write a Lua function to compute factorials:

function factorial(N)
    local resultSoFar = 1
    for i=N,1,-1 do
        resultSoFar = resultSoFar * i
    end
    return resultSoFar
end

Let’s look at this code:

function factorial(N)

We’ve seen this before. Since there is no local keyword, we’re writing a global function, called factorial, and which has a parameter N.

    local resultSoFar = 1

In this function, we’re going to perform the multiplication over multiple “steps,” and at each step, we’re going to keep track of the result that we have computed thus far into the computation. Since the factorial involves multiplication, it is natural to initialize resultSoFar to 1, because anything multiplied by 1 gets itself back.

    for i=N,1,-1 do

This time, the loop variable is called i. i is a standard name for a loop variable if you can’t think of anything better. The N means that the initial value of i is set to whatever the current value of N is. The factorial definition requires us to multiply each integer value between $N$ and $1$ inclusive, so we increment by -1 and end the loop at 1.

        resultSoFar = resultSoFar * i
    end

This is the actual “work” of the function. The current integer i is multiplied by the current resultSoFar in order to get a new resultSoFar. When i is N, resultSoFar is 1, and it is set to N to start the calculation. After that, the code effectively progresses left to right on the calculation $N\times(N-1)\times(N-2)…3\times2\times1$ multiplying the next integer into the result, until 1 is reached.

    return resultSoFar

After the loop is complete, the resultSoFar is, in fact, the result, so it can be returned.

We can add a nice loop to conversion.lua to print the factorials for 1 through 10.

conversion.lua with factorial

factorial list

While Loops 🔗

Now, let’s look at a different kind of loop, the while loop. The numeric for loop repeated the loop body a predetermined number of times. The while loop, on the other hand, keeps performing a set of instructions as long as a certain condition is met, and then stops. In fact, if the condition is not met when the while loop is first reached, it won’t be executed even once.

Let’s look at while loop version of the factorial function:

function factorialWhile(N)
    local resultSoFar = 1
    while N >= 1 do
        resultSoFar = resultSoFar*N
        N = N-1
    end
    return resultSoFar
end
function factorialWhile(N)
    local resultSoFar = 1

These are the same as the function with the numeric for loop.

    while N >= 1 do

This line checks that N is greater than or equal to 1. The >= operator returns true if the number to the left is at least as much as the the number on the right, and false otherwise. If true is returned, the loop body (between do and end) is executed, otherwise, the code skips to the end.

        resultSoFar = resultSoFar*N
        N = N-1
    end

This is the loop body. Once again, we’re multiplying the resultSoFar by the current integer. However, this time we must also increment N, since there is no loop variable this time. This may be the first time in these lessons that I’ve changed the value of a variable that is defined in the function parameters list, but it is allowed. (We could also have just defined another variable if we wanted.)

Once the end is reached, the N >=1 check is made again, and the code keeps executing as long as N is greater than 1.

    return resultSoFar
end

As with the numeric for loop, once we exit the loop, the resultSoFar is the final result.

Adding a loop to test this function gives the code:

factorialWhile

two factorial charts

In this example, the while loop is a bit more work than the numeric for loop, since we had to increment a counter ourselves. While loops are useful in situations where you don’t know ahead of time how many times you will need to execute the loop body. This is more likely to be the case when solving more complicated problems, so we will have to wait for future lessons to see some in action.

You should also note that if you choose your condition poorly the while loop will never stop, and you will be forced to close Civ II through the task manager.

Later in this lesson, we’ll look at the generic for loop, but, for now we will turn our attention to Lua tables.

Tables 🔗

Thus far, we have always stored a single value in a variable. This can be inconvenient for a couple of reasons. The first reason is that sometimes it is much more convenient to manipulate multiple values at once. The second reason is that sometimes we don’t know how how many variables that we’ll need ahead of time. For example, if we’re gathering a list of cities, we will probably not know ahead of time how many cities will be in that list. Lua tables help us get around both of these problems.

A Lua table is a programming object that associates keys with values. Every key can have an associated value. Several keys can have the same value associated with each of them, but the same key can’t have multiple different values. Another way of saying it is that values can be repeated in a table, but there can only be one of each key. I think that Lua uses the name table because behind the scenes they are hash tables, but reading up on hash tables won’t be of any help programming in Lua.

We will begin by creating a new table. In the lua folder, create a new file called tables.lua, which we will program in during this next section. You should also reload your saved game in the Original folder, to clear the console from earlier in the lesson.

In order to create a table, we use the following syntax:

local myTable = {}

At the moment, myTable doesn’t have any key-value associations. Although any data type except nil can be a table key, we will usually restrict ourselves to numbers and strings. To start, we will assign the value "one" to the key 1:

myTable[1] = "one"

The [] brackets are used to specify that what is inside of them is a table key, which is also sometimes called an index. So, [1] tells Lua that we’re interested in the key 1 of a table, and myTable[1] tells Lua that myTable is the table in question. Since this is all on the left hand side of the = sign, Lua knows that a value is being assigned to that table and key. In this case, the value is "one".

In order to get the value associated with a table’s key, very similar syntax is used:

print(myTable[1])

Once again, the [1] tells Lua that we’re interested in the value assigned to the index 1, and myTable[1] tells that we want the value from the table stored in the myTable variable. Since it is not to the left of an = sign, Lua knows that we want to get the value rather than assign it.

Save and run the script.

tables.lua

myTable[1]

Next, let’s assign another value to myTable. This time, we’ll assign the value 2 to the key "two":

myTable["two"] = 2

print(myTable["two"].." is 2")

We can also use the values of variables as keys or values in a table.

local trois = "trois"
local three = 3

local four = "quatre"

myTable[3] = trois
myTable[four] = 4

print(myTable[three].." is trois")
print(myTable["quatre"].." is 4")

Another characteristic of tables is that if you try to get the value of a key that doesn’t have an assigned value, nil will be returned as the corresponding value.

print(tostring(myTable[5]).." is nil")

myTable 5 values

Counting Animals 🔗

Let’s create a little example where we record animals that we’ve “seen”. Whenever we “see” an animal, we’ll call the function spotted to record the animal.

We comment out the previous code, and add this new code:

caesarTotals = {["horse"]=0,["cat"]=0,["dog"]=0}
hannibalTotals = {["horse"]=0,["cat"]=0,["dog"]=0}

function spotted(totals,animal)
    totals[animal] = totals[animal] + 1
end

spotted(caesarTotals,"dog")
spotted(caesarTotals,"dog")
spotted(hannibalTotals,"cat")
spotted(caesarTotals,"horse")
spotted(hannibalTotals,"horse")
spotted(hannibalTotals,"dog")
spotted(caesarTotals,"cat")
spotted(hannibalTotals,"cat")

function printTotals()
    print("Caesar's totals are: Horses: "..caesarTotals["horse"].." Cats: "..caesarTotals["cat"].." Dogs: "..caesarTotals["dog"])
    print("Hannibal's totals are: Horses: "..hannibalTotals["horse"].." Cats: "..hannibalTotals["cat"].." Dogs: "..hannibalTotals["dog"])
end

printTotals()

Let’s go through the code:

caesarTotals = {["horse"]=0,["cat"]=0,["dog"]=0}
hannibalTotals = {["horse"]=0,["cat"]=0,["dog"]=0,}

Here, we’re defining two tables, one table to keep track of how many animals Caesar spotted, and the other to keep track of how many animals Hannibal spotted. These have been made into global variables, so that they can be accessed from the Lua Console if desired.

This time, we’re defining a table with some stuff between the {} brackets. ["horse"]=0 means that the key "horse" should be created with the new table, and that it should be initialised with a value of 0. Each of these initialisations must be separated with a ,. It is allowed to have or not have a , after the last key (["dog"]=0 vs ["dog"]=0,). So, for these two tables, the keys "horse", "cat", and "dog" are all assigned values of 0.

function spotted(totals,animal)
    totals[animal] = totals[animal] + 1
end

Here, we define the global function spotted, which has two parameters: totals and animal. totals is a table that keeps track of the number of each animal spotted, and animal is a string that represents a particular kind of animal, and which is also a key in a totals table.

The animal key of the totals table is updated by adding 1 to the previous value.

There is something important to note here: the totals table is not returned by the spotted function. It doesn’t have to be. When a variable containing an integer or string is updated, the new value must be assigned to the variable to replace the existing one:

updating a string value

However, tables are different. It is best to think of tables as “things” that just exist, and that what we’re storing in the variable is just the name of the table. We don’t need to update the name of the table when we make a change to the table itself.

spotted(caesarTotals,"dog")
spotted(caesarTotals,"dog")
spotted(hannibalTotals,"cat")
spotted(caesarTotals,"horse")
spotted(hannibalTotals,"horse")
spotted(hannibalTotals,"dog")
spotted(caesarTotals,"cat")
spotted(hannibalTotals,"cat")

Here, Caesar and Hannibal are “spotting” different animals.

function printTotals()
    print("Caesar's totals are: Horses: "..caesarTotals["horse"].." Cats: "..caesarTotals["cat"].." Dogs: "..caesarTotals["dog"])
    print("Hannibal's totals are: Horses: "..hannibalTotals["horse"].." Cats: "..hannibalTotals["cat"].." Dogs: "..hannibalTotals["dog"])
end

This function just prints the contents of the two tables.

printTotals()

Here, we print the the totals of the tables, based on the “spotted” animals in our script.

Save the file, and run the script. (Remember to comment out the code from the previous section.)

animal count code count output

In the console, we can call the spotted and printTotals functions to increment the values further, and see the new ones. Here are some sample commands that I’ve done:

more animals spotted

Tip: You can use the up and down arrows to bring up previous commands, so you don’t have to type the entire command each time.

Handling Absent Table Keys 🔗

Continuing from the previous section, let’s cause an error. Try to “spot” a "cow":

arithmetic on nil value error

C:\Games\Test of Time\lua\tables.lua:29: attempt to perform arithmetic on a nil value (field '?')
stack traceback:
	C:\Games\Test of Time\lua\tables.lua:29: in function 'spotted'
	(...tail calls...)

This is line 29:

    totals[animal] = totals[animal] + 1

and the error is “attempt to perform arithmetic on a nil value (field ‘?’)”.

This error means that you’re trying to do arithmetic (basic math like addition or subtraction) but one of the values is nil. Why did we get this error? Let’s perform the calculations. First, we replace totals with caesarTotals and animal with "cow", since those are the values that the spotted function was called with:

    caesarTotals["cow"] = caesarTotals["cow"] +1

On the Left Hand Side, we’re trying to assign a value to the "cow" key of caesarTotals, so that’s fully simplified. On the Right Hand Side, however, we still have to replace caesarTotals["cow"] with the corresponding value. Since the table caesarTotals does not yet have anything assigned to the "cow" key, nil is considered to be the value:

    caesarTotals["cow"] = nil +1

And, here, we see that nil can’t be added to 1, so we get an error.

We should do something when the spotted function is called for a key that isn’t already in the totals table.

One option is to use the error function. This will still cause an error, but the error will be more informative:

function spotted(totals,animal)
    if totals[animal] == nil then
        error(animal.." is not an animal that can be spotted.")
    end
    totals[animal] = totals[animal] + 1
end
    if totals[animal] == nil then

This line checks if the value for the key animal in the totals table is nil. If it is, we go inside the if statement to find

        error(animal.." is not an animal that can be spotted.")

The error command will create an error on this line, and print its argument as part of the error. Change the spotted function, save tables.lua, load the script again, and try to “spot” a "cow" once again:

custom error

Now, we get the more helpful error that “cow is not an animal that can be spotted.”

However, maybe we don’t want to generate an error. An alternative is to simply do nothing. Let’s look at a different version of spotted:

function spotted(totals,animal)
    if totals[animal] then
        totals[animal] = totals[animal] + 1
    end
end

We only need to look at

    if totals[animal] then

In the last lesson I wrote that an if statement of the form

if value then

executes if the value is true and explained the way if statements worked as if the value must be a boolean. I did that for simplicity. In fact, the value between if and then can be any data type. The body of the if statement is executed if the value is “truthy,” and is’t executed if the value is “falsy.” In Lua, all values are “truthy” except false and nil. (This is different from a lot of other programming languages, where some other values like 0 and "" are falsy.)

Since only nil and false are falsy, if table[key] then is a compact way to execute code only if a table has an assigned value for key, as long as false isn’t a value in the table.

That means that this version of the spotted function increments the total if animal is a key in the totals table, and does nothing if it isn’t.

Save this version of spotted, reload tables.lua and try to “spot” a "cow" again. Call caesarTotals["cow"] to check that nothing has been assigned to the "cow" key:

ignore cow version

We have another option for dealing with a new key: we can initialize it to 0 before adding 1 to it:

function spotted(totals,animal)
    if not totals[animal] then
        totals[animal] = 0
    end
    totals[animal] = totals[animal] + 1
end
    if not totals[animal] then

The not operator converts a truthy value to false, and a falsy value to true. If the totals table already has a value for the animal key, then totals[animal] is truthy, so not totals[animal] is false, and the body of the if statement is ignored.

If, on the other hand, totals[animal] is nil, it is falsy and so not totals[animal] is true, and the body of the if statement is executed.

        totals[animal] = 0
    end

This sets the value of the animal key of the totals table to 0. As long as we don’t assign a false value to any key of our totals tables, we will only get to this section of code when the animal key has not yet been initialised. This line sets the value of the key to 0, so that the next line can do arithmetic on it.

    totals[animal] = totals[animal] + 1

This line adds 1 to the existing total for animal. If there was already a value for animal, the above if statement was ignored, so it was not set to 0, and so we add 1 to the total (as long as other code didn’t assign a non-numeric value to the key).

If there was no value for the animal key, the if statement assigned it a value of 0, to which we now add 1. The resulting value is 1 for the animal key, which makes sense, since we’ve just “spotted” the first instance of the animal.

Running the script, and spotting a couple "cow"s gives:

initialisation version of code

You will notice that the "cow" count wasn’t displayed by the printTotals function call. This is because we didn’t know to check for "cow"s when we wrote the function. The next section will provide us the tools to correct this deficiency.

pairs, Iterators, and the Generic For Loop 🔗

We ended the last section with a problem. We wrote code that could add arbitrary keys to a table, but we couldn’t write a function to print that information without knowing what the keys would be ahead of time.

A print call on the table itself is no help:

result of printing a table

The solution to our problem is to write a loop that will execute code for every key-value pair in the table. We will use a generic for loop and the function pairs to achieve this.

Here is the new version of printTotals, which we shall examine:

function printTableValues(totals)
    for key,value in pairs(totals) do
        print("\t"..key..": "..value)
    end
end

function printTotals()
    print("Caesar's totals are:")
    printTableValues(caesarTotals)
    print("Hannibal's totals are:")
    printTableValues(hannibalTotals)
end
    for key,value in pairs(totals) do

This is the generic for loop syntax. There are two loop variables, key and value, and they will take on the values of each (non-nil) key-value pair in the table. These variables serve the same role as the numKm or i variables we saw earlier in the lesson, with the difference being that there are two loop variables instead of one.

The function pairs takes a table and returns an iterator. An iterator is a special kind of function that makes a generic for loop work. All you need to know about iterators for now is that they make generic for loops work, and, so, if the documentation says “iterator,” you use it as part of the generic for loop.

The basic TOTPP functionality provide these three ways to create iterators (although a few more have been written):

iterateCities 🔗

function civ.iterateCities()
 -> fun():cityObject

Returns an iterator yielding all cities in the game.

@return

iterateUnits 🔗

function civ.iterateUnits()
  -> fun():unitObject

Returns an iterator yielding all units in the game.

@return

units 🔗

tileObject.units --> fun():unitObject

(get) Returns an iterator yielding all units at the tile’s location.

We will see these used in due course. Returning to the code at hand, we have the loop body:

        print("\t"..key..": "..value)
    end

For each key and value, a line is printed that shows the value associated with the key. Since the keys and values are strings and integers, we don’t worry about using the tostring function. Note that the character \t is the “tab” character, which indents the lines. This will just make the presentation a little bit nicer.

function printTotals()
    print("Caesar's totals are:")
    printTableValues(caesarTotals)
    print("Hannibal's totals are:")
    printTableValues(hannibalTotals)
end

This function has been changed to print “preamble” lines and then run the printTableValues function that was defined above. Here are the results, after running tables.lua and adding some extra “spotting” afterward.

code with pairs

printed code with pairs

Something important to note is that the order of the key-value pairs provided by the pairs function isn’t guaranteed. The generic for loop will go through the keys and values in whatever order is convenient for the Lua Interpreter. The order isn’t random, but it also isn’t necessarily what you think it might be.

Also, a loop generated by the pairs function will never process a nil value, even if you explicitly assign nil to a key in the table, like this:

myTable["someKey"] = nil

The table.key Syntax for Tables 🔗

Thus far, when we’ve wanted to get or set the value associated with a key, we have used syntax of the form:

table[key]

However, there is an alternate, and sometimes more convenient syntax. If a table’s key is a string that is also a valid variable name, we can use a syntax of the form:

table.key

Let’s make the details clearer with an example:

local trois = "three"
local sampleTable = {[1] = 1, ["two"] = 2, [trois] = 3, ["4"] = 4, ["one thousand"] = 1000}

Given this setup, these are legal commands:

print(sampleTable.two)
print(sampleTable.three)
sampleTable.two = "2"
sampleTable.three = "3"

However, these commands are illegal or won’t do what is expected:

print(sampleTable.1)

The key 1 is not a string, so it can’t be used with . syntax.

print(sampleTable.trois) 

This is nil; the . syntax doesn’t evaluate variables to get a key. This code is equivalent to print(sampleTable["trois"])

print(sampleTable.4)

The key "4" is a string, but 4 is not a valid variable name in Lua, so the . syntax is invalid.

print(sampleTable.one thousand)

The key "one thousand" is a string, but one thousand is not a valid variable name in Lua, so the . syntax can’t be used.

If you haven’t noticed it yet, this . syntax is the same syntax that is used for the special civilization objects that were introduced last lesson. In fact, if you want to, you can also use the [] syntax with civ objects, but it is unusual for that to be convenient.

As a final note on the alternate syntax for tables, it can also be used in a table constructor, like this:

local trois = "three"
local sampleTable = {[1] = 1, two = 2, [trois] = 3, ["4"] = 4, ["one thousand"] = 1000}

Note that ["two"] = 2 was replaced by two = 2 in this table constructor.

The Twelve Days of Christmas 🔗

In this section, we will put together code to efficiently print the lyrics to the Christmas carol “The Twelve Days of Christmas”. If you’re not familiar with it, you can read the first two sections of the Wikipedia article. The song is highly repetitive, so it is a good candidate to generate programmatically.

We will first begin by creating a new file twelveDays.lua.

Here is the code to print the song:

local dayInfo = {
    [1] = {day="first", line="And a partridge in a pear tree."},
    [2] = {day="second", line="Two turtle doves,"},
    [3] = {day="third", line="Three French hens,"},
    [4] = {day="fourth", line="Four calling birds,"},
    [5] = {day="fifth", line="Five gold rings,"},
    [6] = {day="sixth", line="Six geese a-laying,"},
    [7] = {day="seventh", line="Seven swans a-swimming,"},
    [8] = {day="eighth", line="Eight maids a-milking,"},
    [9] = {day="ninth", line="Nine ladies dancing,"},
    [10] = {day="tenth", line="Ten lords a-leaping,"},
    [11] = {day="eleventh", line="Eleven pipers piping,"},
    [12] = {day="twelfth ", line="Twelve drummers drumming,"},
}

local function makeVerse(verse)
    if verse == 1 then
        print("On the first day of Christmas, my true love sent to me")
        print("A partridge in a pear tree.")
        return
    end
    print("On the "..dayInfo[verse].day.." day of Christmas, my true love sent to me")
    for i=verse,1,-1 do
        print(dayInfo[i]["line"])
    end
end

local function printTwelveDaysOfChristmas()
    for i=1,12 do
        print("")
        makeVerse(i)
    end
end

printTwelveDaysOfChristmas()

Let’s begin with the dayInfo table:

local dayInfo = {
    [1] = {day="first", line="And a partridge in a pear tree."},
    [2] = {day="second", line="Two turtle doves,"},
    [3] = {day="third", line="Three French hens,"},
    [4] = {day="fourth", line="Four calling birds,"},
    [5] = {day="fifth", line="Five gold rings,"},
    [6] = {day="sixth", line="Six geese a-laying,"},
    [7] = {day="seventh", line="Seven swans a-swimming,"},
    [8] = {day="eighth", line="Eight maids a-milking,"},
    [9] = {day="ninth", line="Nine ladies dancing,"},
    [10] = {day="tenth", line="Ten lords a-leaping,"},
    [11] = {day="eleventh", line="Eleven pipers piping,"},
    [12] = {day="twelfth ", line="Twelve drummers drumming,"},
}

This table has integers as keys, and each key corresponds to one of the twelve days of Christmas. The values for the dayInfo table are also tables, since we want to store more than one piece of information for each of the days. In this case, there are two pieces of information, the adjective for the day, stored in the day key, and the song line corresponding to that day’s gift, in the line key.

Next, let’s look at the makeVerse function:

local function makeVerse(verse)
    if verse == 1 then
        print("On the first day of Christmas, my true love sent to me")
        print("A partridge in a pear tree.")
        return
    end
    print("On the "..dayInfo[verse].day.." day of Christmas, my true love sent to me")
    for i=verse,1,-1 do
        print(dayInfo[i]["line"])
    end
end

The verse is an integer, so we know what verse of the song this is.

    if verse == 1 then
        print("On the first day of Christmas, my true love sent to me")
        print("A partridge in a pear tree.")
        return
    end

We have to treat the first verse differently, because the second line is “A partridge in a pear tree.” instead of “And a partridge in a pear tree.” Since the first verse is so short, it makes sense to just print it, rather than trying to work with the loop in any way.

    print("On the "..dayInfo[verse].day.." day of Christmas, my true love sent to me")

The new thing here is repeatedly applying keys to a table. Let’s do an example evaluation, with verse 3.

dayInfo[verse].day
dayInfo[3].day
{day="third", line="Three French hens,"}.day
"third"

Now, for the loop that prints the rest of the verse:

    for i=verse,1,-1 do
        print(dayInfo[i]["line"])
    end

This is a loop, starting at the verse, and being reduced by 1 each time until 1 is reached. Within the body, we again have multiple keysindexing” the table, since dayInfo’s values are also tables. In this case, I used the [] syntax twice, even though the line key can use . syntax.

local function printTwelveDaysOfChristmas()
    for i=1,12 do
        print("")
        makeVerse(i)
    end
end

printTwelveDaysOfChristmas()

This function is pretty simple. We loop from verse 1 to verse 12, and for each verse, we print an empty line and then the corresponding verse. I called the loop variable i because I was lazy and didn’t take the time to remember to call it verse or something. If the loop body were more complicated, I would have taken the time to give it a more useful name.

Finally, the script runs printTwelveDaysOfChristmas to actually print the song to the console. This is the result:

code to print the Twelve Days of Christmas

days one through eight

days nine through twelve

A Create Unit Event 🔗

Thus far in this lesson, we haven’t done anything specific to Test of Time Events. It has all been generic Lua programming. However, we now have the tools to do some interesting events that create new units.

Load a saved game in the ClassicRome scenario, and open the ClassicRome scenario folder in VS Code as well.

We will begin by writing an event that generates reinforcements for a tribe whenever that tribe loses a city, as long as they still control their original capital. The unit type will be different for each tribe, and the number of units created will depend on the size of the captured city. The Independent Greeks, Celts and the Barbarians will not benefit from this event, because they aren’t centralized empires.

Here is the code for the event, which I will go through section by section below:

local reinforcementData = {}
reinforcementData[1] = {capitalTile = {40,28,0}, 
    unitType = civ.getUnitType(5)} -- Rome/Legion
reinforcementData[2] = {capitalTile = {36,62,0}, 
    unitType = civ.getUnitType(17)} -- Carthage/Elephant
reinforcementDate[3] = {capitalTile = {57,31,0}, 
    unitType = civ.getUnitType(3)} -- Pella/Phalanx
reinforcementData[4] = {capitalTile = {69,69,0}, 
    unitType = civ.getUnitType(3)} -- Alexandria/Phalanx
reinforcementData[5] = {capitalTile = {78,46,0}, 
    unitType = civ.getUnitType(3)} -- Antioch/Phalanx

discreteEvents.onCityTaken(function (city, defender)
    if not reinforcementData[defender.id] then
        return
    end
    local coords = reinforcementData[defender.id].capitalTile
    local capitalLocation = civ.getTile(coords[1],coords[2],coords[3]) --[[@as tileObject]]
    if not capitalLocation.city then
        return
    end
    if capitalLocation.city.owner ~= defender then
        return
    end
    local quantity = city.size
    local unitType = reinforcementData[defender.id].unitType
    local plural = ""
    if quantity > 1 then
        plural = "s"
    end
    civ.ui.text("Alarmed by the loss of "..city.name..
    ", the "..defender.name.." recruit "..quantity.." "
    ..unitType.name.." unit"..plural.." in "..
    capitalLocation.city.name..".")
    for i=1,quantity do
        local newUnit = civ.createUnit(unitType,defender,capitalLocation)
        newUnit.homeCity = nil
    end
end)

We begin with the data table:

local reinforcementData = {}
reinforcementData[1] = {capitalTile = {40,28,0}, 
    unitType = civ.getUnitType(5)} -- Rome/Legion

The id number for the Roman tribe is 1, so we use that as the key for the reinforcementData table. Why not use the Roman tribeObject itself as the key? The answer is that tables don’t work the way we’ve come to expect when you use TOTPP objects as keys. Open the Lua Console and begin by typing this command, to allow global variables to be used:

console.restoreGlobal()

Now, let’s create a table, which we’ll used to show the “problem”:

tribeKeys = {}

Next, enter these commands:

tribeKeys[civ.getTribe(1)] = "Romans 1"
tribeKeys[civ.getTribe(1)] = "Romans 2"

Now, applying the rules governing tables that we’ve learned in this lesson, what has happened is that the tribeKeys table has had the value "Romans 1" assigned to the key romanTribeObject. Then, the romanTribeObject key had its value replaced with "Romans 2".

Therefore, when we type the command tribeKeys[civ.getTribe(1)], we will expect to get "Romans 2" printed back to us (without the " marks, of course).

Enter this command,

tribeKeys[civ.getTribe(1)]

What you get back is nil!

tribe keys commands

What’s going on? We might get an idea by using pairs to loop over the entries in the table. The following command is a loop that prints the key and value for each entry in a table, but written on one line instead of three.

for key,value in pairs(tribeKeys) do print(key,value) end

Enter this command, and you get:

tribe keys commands 2

Our commands have managed to assign 2 key-value pairs, both with the romanTribeObject as the key.

I don’t know for sure why this behaviour exists, and my explanation would be confusing to most people reading this anyway. For now, restrict yourself to using numbers and strings as table keys, unless documentation asks you to do otherwise.

Returning to the line we were analysing,

local reinforcementData = {}
reinforcementData[1] = {capitalTile = {40,28,0}, 
    unitType = civ.getUnitType(5)} -- Rome/Legion

Here, we are assigning a table as the value. The second key in the table is "unitType", and it is being assigned the unitTypeObject associated with the 5th id number (6th “slot”, since ids start counting at 0). My comment mentions that this is the Legion. The first key is "capitalTile", and its value is yet another table, given by {40,28,0}.

We’ve seen table constructors before, but they’ve always been of the form

{key1 = value1, [key2] = value2}

In this instance, however, the keys have been omitted. The Lua Interpreter deals with this by assigning the leftmost value to the key 1, the next value to key 2, and so on. This means that

{40,28,0}

is simply a compact way of writing

{[1]=40,[2]=28,[3]=0}

The entries in this table represent the coordinates of the tile where Rome is located. They’ll be converted to a tileObject during the actual event.

reinforcementData[2] = {capitalTile = {36,62,0}, 
    unitType = civ.getUnitType(17)} -- Carthage/Elephant
reinforcementDate[3] = {capitalTile = {57,31,0}, 
    unitType = civ.getUnitType(3)} -- Pella/Phalanx
reinforcementData[4] = {capitalTile = {69,69,0}, 
    unitType = civ.getUnitType(3)} -- Alexandria/Phalanx
reinforcementData[5] = {capitalTile = {78,46,0}, 
    unitType = civ.getUnitType(3)} -- Antioch/Phalanx

Here, we see the same kind of data for other tribes that we want to participate in this event. The tribes that don’t get the benefit of this event are simply omitted from this table. We’ll see how that works shortly.

Now, we look at the code that executes the event

discreteEvents.onCityTaken(function (city, defender)
    if not reinforcementData[defender.id] then
        return
    end
    local coords = reinforcementData[defender.id].capitalTile
    local capitalLocation = civ.getTile(coords[1],coords[2],coords[3]) --[[@as tileObject]]
    if not capitalLocation.city then
        return
    end
    if capitalLocation.city.owner ~= defender then
        return
    end
    local quantity = city.size
    local unitType = reinforcementData[defender.id].unitType
    local plural = ""
    if quantity > 1 then
        plural = "s"
    end
    civ.ui.text("Alarmed by the loss of "..city.name..
    ", the "..defender.name.." recruit "..quantity.." "
    ..unitType.name.." unit"..plural.." in "..
    capitalLocation.city.name..".")
    for i=1,quantity do
        local newUnit = civ.createUnit(unitType,defender,capitalLocation)
        newUnit.homeCity = nil
    end
end)
discreteEvents.onCityTaken(function (city, defender)

We’ve seen this before. We’re registering a function with two parameters, city (a cityObject representing the captured city) and defender (a tribeObject representing the tribe that lost the city).

    if not reinforcementData[defender.id] then
        return
    end

This if statement checks that reinforcementData[defender.id] is either false or nil. It won’t be false, but if it is nil, that means that the tribe in question doesn’t benefit from this event, and, so, the function can return, which stops the event.

    local coords = reinforcementData[defender.id].capitalTile

In this line, I’m simply assigning the value on the right hand side to a variable with a short name, since it will be repeated three times in the next line.

    local capitalLocation = civ.getTile(coords[1],coords[2],coords[3]) --[[@as tileObject]]

Here, I’m using the function civ.getTile to convert the coordinates for the tile to an actual tileObject. Here’s the documentation of the function:

getTile 🔗

function civ.getTile(x: integer, y: integer, z: integer)
 -> tile: tileObject|nil

Returns the tile with coordinates x, y, z, or nil if it doesn’t exist.

@param x — the ‘x’ coordinate of the tile

@param y — the ‘y’ coordinate of the tile

@param z — the ‘z’ coordinate of the tile ([0,3])

The three parameters are the three coordinates of the tile, so I get them with coords[1], coords[2], and coords[3].

The annotation --[[@as tileObject]] is necessary so that Lua LS doesn’t complain that capitalLocation might be nil later in the code. Since we’ve chosen the coordinates, we don’t need to worry about them not corresponding to a real tile.

    if not capitalLocation.city then
        return
    end
    if capitalLocation.city.owner ~= defender then
        return
    end

Here, we check if capitalLocation.city is nil. If it is, the function returns, so we execute no more code. This means that the tribe’s capital city was destroyed.

Next, we check that capitalLocation.city is still owned by the defender. If it isn’t, the function returns so that the event doesn’t take place.

    local quantity = city.size
    local unitType = reinforcementData[defender.id].unitType

Here, we get the quantity of units to be created, as well as the unitType that must be created. This information will be used later in the function. city.size returns the size of the city when it is captured. (When you test this event, you will see that the city loses the population point due to conquest before this event happens.)

    local plural = ""
    if quantity > 1 then
        plural = "s"
    end
    civ.ui.text("Alarmed by the loss of "..city.name..
    ", the "..defender.name.." recruit "..quantity.." "
    ..unitType.name.." unit"..plural.." in "..
    capitalLocation.city.name..".")

Look at civ.ui.text first. We’re joining strings together to make a message when a tribe receives reinforcements. Most of this consists of getting the names of the objects in question, but have a look at this bit of code:

" unit"..plural.." in "

Here, I want to use the plural variable to append an “s” to “unit” if more than one unit is created.

At the start of this section, I define the string plural, and initialise it as an empty string "". If it stays like this, then

" unit"..plural.." in"
" unit".."".." in"
" unit in"

However, if the quantity to create is greater than 1, the plural variable is changed to "s", and we get the following evaluation instead:

" unit"..plural.." in"
" unit".."s".." in"
" units in"

This way, our message looks good even if only one unit is created.

    for i=1,quantity do
        local newUnit = civ.createUnit(unitType,defender,capitalLocation)
        newUnit.homeCity = nil
    end

In the loop body, we use the civ.createUnit function to create a newUnit which is of type unitType, owned by defender, and created at capitalLocation. Then, we set newUnit.homeCity to nil, which is how you make sure a unit has no home city. This way, we don’t need to worry about whether the capital produces enough shields to support the extra units.

createUnit 🔗

function civ.createUnit(unitType: unitTypeObject, tribe: tribeObject, tile: tileObject)
 -> unit: unitObject

Creates a unit of type unittype, owned by tribe, at the location given by tile.

homeCity 🔗

unitObject.homeCity --> cityObject|nil

(get/set) Returns the unit’s home city, or nil if it doesn’t have one.

If you don’t change the home city, the unit is (at least as far as I can tell) assigned a home city the same way it would be if you used the cheat menu to create a unit.

We use a loop to to create the desired quantity of units, since civ.createUnit doesn’t give an option for the number of units to be created. A for loop from 1 to quantity will execute the loop body quantity times, and, hence, create quantity units.

That’s the end of the function. It should look something like this in your VS Code file. Use cheat mode to try it out and capture cities. Check that it creates units when desired, and doesn’t when not appropriate.

reinforcements code

reinforcements 2 units

reinforcements 1 unit

gen.createUnit 🔗

The civ.createUnit function will create a unit on the tile you specify, regardless of whether the tile is occupied by another tribe, or if a land unit is being placed on the ocean. This is a useful ability to have at our disposal, but it also means that it we want to have an event that behaves like the “Legacy” or “Macro” version of Create Unit, we will have to program some extra checks and conditions.

Or, at least somebody has to do that, and I’ve already done it. The General Library is a file that contains a bunch of functions that I’ve already written, many of which exist to be a more convenient way to do things. The function gen.createUnit provides the ability to use the same functionality as the old Create Unit event.

createUnit 🔗

function gen.createUnit(unitType: unitTypeObject, tribe: tribeObject, locations: table|table<integer, table|tileObject>|tileObject, options: table)
 -> table

This is a createUnit function, meant to supersede civlua.createUnit. Returns a table of units, indexed by integers starting at 1 (unless no units were created).

@param unitType — The type of unit to create.

@param tribe — The owner of the new unit or units.

@param locations — locations is one of the following:

tileObject
{xCoord,yCoord}
{xCoord,yCoord,zCoord}
{x=xCoord,y=yCoord}
{x=xCoord,y=yCoord,z=zCoord}
table<integer,above_types>

@param options — options is a table with the following keys:

count : integer|nil
The number of units to create. nil means 1.

randomize : boolean|nil
If true, randomize the list of locations. If false or nil, try to place at the tile with the smallest index in the table first.

scatter : boolean|nil
If true, and if randomize is true, each unit is created on a random tile in the location table.

inCapital : boolean|nil
If true, attempt to place in the capital before other locations. IN case of multiple capitals, capitals are ranked with smallest city id first. randomize/scatter applies to list of capitals if this is selected.

veteran : boolean|number|nil
If true, make the created units veteran. If a fraction between 0 and 1, each unit has this probability of being veteran. If number 1 or more, this many of the count are made veteran (take floor). If nil or false, no veterans.

homeCity : city|true|nil
If city, that city is the home city. If true, the game selects the home city (probably the way a city is chosen if you crate a unit using the cheat menu). If nil, no home city.

overrideCanEnter : boolean|nil
If true, the units will be placed even if unitType : canEnter(tile) returns false. False or nil means follow the restriction. civ.canEnter appears to check if the terrain is impassible, or if the unit can cross impassible.

overrideDomain : boolean|nil
If true, sea units can be created on land outside cities, and land units at sea. False or nil means units can only be created where they could travel naturally.

overrideDefender : boolean|nil
If true, unit can be placed on tiles with enemy units or cities. False or nil means the tile must have no enemy city, and no enemy defender.

This piece of documentation is much larger than those we’ve previously, mostly because there are a lot of options that need explaining.

The unitType and tribe parameters are straight forward.

The locations parameter is a bit more interesting. It specifies 5 different ways that a location can be defined. The first is a tileObject. The second is a coordinate pair, expressed as a table with key 1 for the x-coordinate, and 2 for the y-coordinate. In General Library functions, if a table representing coordinates has only 2 values, the third is set to 0. This is because the gen.toTile function is used to make the conversion.

toTile 🔗

function gen.toTile(tileAnalog: table|tileObject)
 -> tileObject

If given a tile object, returns the tile. If given coordinates for a tile, returns the tile. Causes error otherwise

@param tileAnalog — Can be:

tileObject

{[1]=xCoord,[2]=yCoord,[3]=zCoord}
Converted to civ.getTile(xCoord,yCoord,zCoord)

{[1]=xCoord,[2]=yCoord}
Converted to civ.getTile(xCoord,yCoord,0)

{x=xCoord,y=yCoord,z=zCoord}
Converted to civ.getTile(xCoord,yCoord,zCoord)

{x=xCoord,y=yCoord}
Converted to civ.getTile(xCoord,yCoord,0)

The third option for a “tile” is a coordinate triple, with the 3 key corresponding to the map, or z-coordinate. The Fourth option uses a table where the keys "x" and "y" give the x,y coordinates (with "z" interpreted as 0), and the fifth option adds a "z" value.

The final thing that can be done is to supply a table of tileObjects or coordinate tables as the argument for the locations parameter. In fact, the precursor to gen.createUnit, civlua.createUnit, required a table as the argument, even if only one location was being provided.

Now, we come to the options parameter, which is a table that contains some options for how gen.createUnit is to behave. The keys veteran, count, inCapital, and homeCity correspond directly to parameters from the CreateUnit Legacy Event.

--------------------------------------------------------------------
CreateUnit			owner=			civilization name
				unit=				name of a type of unit
				[Count=]			number from 1 to 255
				veteran=			[Yes], [No], [False], or [True]
				homecity=			city name or [None]
				[InCapital]			
				locations
								x1,y1,z1
								...
								x10,y10,z10
				endlocations
--------------------------------------------------------------------
CreateUnit:

Creates from 1 to 255 new units (at no expense) with specified 
characteristics and places them at the first of the specified 
locations. If that placement is invalid for any reason, the program 
tries the subsequent locations (there can be up to 10), in order, until
one works or it reaches the EndLocations parameter. The x and y in 
these locations represent horizontal and vertical coordinates on the
scenario map. The z is an optional coordinate specifying on which map 
the units should be created; if no z is entered, this defaults to map 0.
The optional parameter InCapital forces the unit to be created in the
capital city of the specified civilization. Even though this causes 
the locations to be ignored, you still must include the required
Locations and EndLocations parameters and at least one location. 
Finally, you can use the optional Randomize modifier to have the
location chosen at random from the list.

The randomize key in the options table also corresponds to the Randomize modifier. The three keys overrideCanEnter, overrideDomain, and overrideDefender allow you to place units on a tile, even if the Legacy CreateUnit event wouldn’t allow it. overrideDomain is probably the one you’d most likely use in practice, since that allows you to place land units on a transport.

The scatter key represents a new option. Instead of choosing one (valid) tile from the locations on which to place the entire count of units, each created unit is placed on a random tile individually. This can be particularly useful if you’re trying to create a guerrilla resistance event.

For now, let’s just write an event that would make sense in the old Macro system. We want the Carthaginians to get reinforcements every third turn once they start attacking the Romans. We’ll make a Create Unit event which attempts to place some units on each tile belonging to a Roman city at the start of the game. If any of these cities are owned by the Carthaginians, they’ll get the reinforcements.

However, to add some “Lua Flavour” we’ll damage these units, since they’ve just arrived from Carthage and so will not be fully battle ready on the current turn.

local phalanx = civ.getUnitType(3) --[[@as unitTypeObject]]
discreteEvents.onTurn(function (turn)
    if turn % 3 ~= 0 then
        return
    end
    local unitsCreated = gen.createUnit(phalanx,carthaginians,
    {
        {40,28}, -- Rome
        {42,22}, -- Sena
        {41,33}, -- Tarracina
        {45,37}, -- Neapolis
        {38,20}, -- Pisae
        {49,43}, -- Heraclea
    }, {count = 2, homeCity=nil})
    for _,unit in pairs(unitsCreated) do
        unit.damage = 3
    end
end)

Here is the code for the event.

local phalanx = civ.getUnitType(3) --[[@as unitTypeObject]]
discreteEvents.onTurn(function (turn)
    if turn % 3 ~= 0 then
        return
    end

First, we define a variable for the Phalanx unit, (which I really should have done before the previous event). A variable for the Carthaginian Tribe was defined earlier in the code.

This is an event that happens between turns, so we use discreteEvents.onTurn.

We saw the modulo calculation in the last lesson. turn % 3 will evaluate to 0, 1, or 2 depending on the current turn number. Every third turn, it will be 0. If it is 0, then we want to continue the event. If turn % 3 is not 0, then turn % 3 ~= 0 is true, and the body executes. That means the function returns, and the event doesn’t happen.

    local unitsCreated = gen.createUnit(phalanx,carthaginians,

The function gen.createUnits returns a table of unitObjects representing the units that were just created. A table is produced even if 1 or 0 units were created. The table is called unitsCreated. The unitType to create and the tribe to create them for need no further explanation.

    {
        {40,28}, -- Rome
        {42,22}, -- Sena
        {41,33}, -- Tarracina
        {45,37}, -- Neapolis
        {38,20}, -- Pisae
        {49,43}, -- Heraclea
    },

This is a table of coordinate pairs, for each of the 6 Roman cities. Since the overrideDefender option isn’t activated, no units will be created if the Carthaginians don’t own any of these cities.

    {count = 2, homeCity=nil})

Setting the value of count to 2 means that two Phalanx units will be created. Setting homeCity=nil is redundant, since it would be nil anyway, but it makes it clear that there will be no home city.

    for _,unit in pairs(unitsCreated) do
        unit.damage = 3
    end

This generic for loop loops over all the units in the unitsCreated table. It is common practice to use an underscore _ as the variable name for the key if we’re not going to use that information.

For each unitObject in the unitsCreated table, the command unit.damage = 3 sets the damage the unit has taken to 3. If the unitsCreated table is empty (because no units were created), this loop does nothing.

In VS Code, this event should look something like this:

carthaginian reinforcements

You will notice that the coordinate pairs are underlined in yellow. This is due to a mistake in my documentation, which will be corrected in a future template release. We will just ignore it for now.

Test out this event, and see how it works. You might find it convenient to delete all the existing units on the map, so that turns pass quicker, and you can observe whether the event takes place. To do that, copy this code into the console:

for unit in civ.iterateUnits() do civ.deleteUnit(unit) end

If we expand this code into multiple lines, it looks like this:

for unit in civ.iterateUnits() do
    civ.deleteUnit(unit)
end

The function civ.deleteUnit does exactly what it says: it deletes the unit. The generic for loop uses the iterator generated by the function civ.iterateUnits. Note that the () after civ.iterateUnits is important. civ.iterateUnits returns an iterator when the function is executed, it is not an iterator itself.

When you create a loop using civ.iterateUnits(), all the units in the game are looped over (any units you create during the loop might not be “visited”). In this case, it means that one loop will let us delete all the units currently on the map.

Test this event out. You should find that it works as desired, except for one thing. Although the Phalanxes have 3 damage when they are created, they are healed at the start of the Carthaginian turn.

We can avoid this by changing when the event takes place. Instead of producing the Phalanxes during the onTurn execution point, we can instead use onCityProcessingComplete. Here’s the changed code:

discreteEvents.onCityProcessingComplete(function (turn, tribe)
    if tribe ~= carthaginians then
        return
    end

The onCityProcessingComplete execution point takes place after the tribe has processed its cities, but before it starts moving units. For this event, we must check if the tribe is in fact the Carthaginians and return if it is not. Otherwise, every third turn, the Carthaginians will receive 2 Phalanxes per tribe in their city.

Change the code, and test the new event. The Phalanxes will start the turn damaged.

city processing complete

Copying Tables 🔗

I have an important point to make about tables before this lesson is finished, but I’ll do so as part of a key press event. In discreteEvents.lua, search for “onKeyPress”. You should bind this section of code around line 395 (your file may be off by a few lines if you’ve added extra spaces or something).

on key press example

The onKeyPress execution point is triggered every time a key on the keyboard is pressed, and the “code” for that key is provided as the argument to the registered function.

Replace the existing testing “event” with the following code:

discreteEvents.onKeyPress(function(keyCode)
    if keyCode ~= keyboard.backspace then
        return
    end
    local a = "Hello"
    local b = a
    a = a .." World"
    local t = {message = "Hello"}
    local u = t
    t.message = t.message.." World"
    civ.ui.text("a: "..a,"^b: "..b,
    "^t.message: "..t.message,"^u.message: "..u.message)
end)

Let’s look at the code

discreteEvents.onKeyPress(function(keyCode)
    if keyCode ~= keyboard.backspace then
        return
    end

The first line creates the onKeyPress event, and specifies the parameter name as keyCode.

The if statement checks if the keyCode is not equal to keyboard.backspace, and, if they are in fact not equal, the body returns the function and, therefore, stops the event.

keyboard.backspace gives the number that corresponds to the code when the “Backspace” button is pressed on your computer, so the rest of this event will only happen if backspace is pressed. The list of key codes is found on this page.

Now, what’s going on in the rest of the code?

    local a = "Hello"
    local b = a
    a = a .." World"
    local t = {message = "Hello"}
    local u = t
    t.message = t.message.." World"

Here, I’m creating the variable a, and assigning it the value "Hello". I then copy the contents of a into the new variable b, and afterwards, I change a into the string "Hello World".

Next, I create a variable t, and assign it a table, with a message key having the value "Hello". I copy the value of t into the u variable, and then change t.message to "Hello World".

    civ.ui.text("a: "..a,"^b: "..b,
    "^t.message: "..t.message,"^u.message: "..u.message)

In lesson 3 I introduced civ.ui.text and pointed out that it can have multiple arguments. We will use that here. If an argument to civ.ui.text begins with a ^ character, that will start a new line in the text box. This will format our information a bit more nicely, since this text box will be displaying the current values of our variables.

Save the file, reload the game, and press “Backspace” to activate this event.

backspace key press

copy code result

This is noteworthy. The line

    local b = a

assigned b the value of "Hello", because that was the value of a at the time. Changing the value of a after the fact leaves b unchanged.

However, when I changed the value of t, the value of u changed as well. The best way that I have to think about this is to say that a table is just “something” that exists, sort of like a unitObject. What we’re storing in t and u is not the table itself, but the name of the table.

If I stored the same unitObject in two variables unit1 and unit2, and damaged the unit using unit1.damage, it makes perfect sense that unit2.damage would also change, since they’re both “pointing” to the same thing. We wouldn’t expect unit2 = unit1 to create a brand new unit, instead we’re just copying the “name” of the unit.

So, what do you do if you want to copy a table and not just its “name?” The General Library has a function for that:

copyTable 🔗

function gen.copyTable(table: any)
 -> any

Constructs (and returns) a new table with the same keys as the input. Tables within the table are also copied. Note: although this is meant for copying tables, the way the function is constructed, any value can be input and returned.

So, in order to get t and u to behave like a and b, we change the line copying t to u to this:

    local u = gen.copyTable(t)

Saving the file, loading the game, and running the event gives:

copied table

Conclusion 🔗

This has been a long lesson, but we’ve completed our look at the most fundamental parts of the Lua Programming Language. In the coming lessons, we’ll gain practice with these tools to build events. We will also learn how to fill out settings tables in order to activate and customize the many game mechanics that Lua has made possible, and which are programmed into the Lua Scenario Template.

But before we get to that, I’ll have to explain variable scope, how to share information between files, and the reason why global variables are disabled in the Lua Scenario Template.

Previous: Civilization II Objects and If Statements Next: Variable Scope and Sharing Data