[TOTPP] Get Started With Lua Events

Prof. Garfield

Deity
Supporter
Joined
Mar 6, 2004
Messages
4,366
Location
Ontario
Note: I've started (but not got too far) a new version here.

This thread is meant to be a resource for learning how to use the Lua functionality of the Test of Time Patch Project. The first 10 posts will (eventually) contain "lessons" which are intended to teach enough about programming, Lua, and ToTPP Lua interaction so that a scenario maker can write basic events functionality and use more complicated functionality written by others. It is also intended to provide enough knowledge about programming and lua to enable the student to use other lua resources to learn more about the language.

Questions, comments, and criticisms are all welcome. Post feedback here for the time being. If you're interested in learning Lua "soon" (say, a 50% chance of wanting to do so within the next 6 months) please like this post, so I know to make this project a priority.

Lesson 1: "Hello World"

Start a new game of Civilization II with a version of the Test of Time Patch Project that includes Lua functionality.

Activate Cheat Mode.

Press Ctrl+Shift+F3 to open the Lua Console.

In the Input Bar at the bottom of the Lua Console type:


Code:
 print("Hello World")

HelloWorld1.jpg


And press Enter.

Your Console should now look like this:

HelloWorld2.jpg


The command print() displays to the console, and this will be useful later for debuging. However, the point of Lua is to interact with the game itself. Therefore, enter the code

Code:
 civ.ui.text("Hello World!")

A text box should appear in the game like this:

HelloWorld3.jpg


Now, let us make an error in typing a command. We will type C instead of c:

Code:
 Civ.ui.text("Hello World!")

An error message is printed in the console, which now looks like this:

HelloWorld4.jpg


We can access a previous command and make changes to it by pressing the up arrow in the console command line. Do this, and make 'C' lower case. Press Enter, and the "Hello World" text box should appear.

Thus far, we've only got the game to do something by writing a command in the Lua Console. Now, we will write an event. Don't worry about why the event is written this way for now, that will be explained in a later lesson.

Code:
 civ.scen.onActivateUnit(function() civ.ui.text("Hello World!") end)

If no error is printed, close the Lua Console (the X in the top left corner). Activate a unit (clicking on it will do). You should get a "Hello World!" text box message.

HelloWorld5.jpg


Open the Lua Console again, and enter the following code:

Code:
 civ.scen.onUnitKilled(function() civ.ui.text("Goodbye Cruel World!") end)

Now, create 2 warriors, one from a different tribe, and make them fight.

You should get a text box like this:

HelloWorld6.jpg


This concludes lesson 1.
 
Last edited:
Lesson 2: Giving Instructions to Your Computer

In lesson 1, we typed every single instruction into the Lua Console and it was executed right away. Usually, however, we want to specify our instructions ahead of time and then run them all at once. The button "Load Script" in the Civ II Lua Console lets you choose a file containing instructions and launch that.

We need a program that will allow us to create instructions for the Lua interpreter that the Test of Time Patch Project has introduced. A program that does this is called a "text editor." This is NOT the same thing as a "word processor." Libre Office Writer or Microsoft Word will not work for our purposes.

For these lessons I will be using Notepad++ as the text editor. It is available for Windows only, but if you've installed ToT on Linux using WINE, you should be able be able to get Notepad++ working as well (as I have). The version of Notepad++ I'm using is v7.4.2 (32-bit), but this is unlikely to matter. In fact, you can use a different text editor if you prefer (I do), just be aware that the syntax colouring will be different. I don't intend to use any Notepad++ specific features in these lessons.

Notepad++ can be found at https://notepad-plus-plus.org/ . It can be downloaded free of charge, and has a GPL License, so it is fully legal to redistribute (so if the website disappears, you can probably still get a copy from someone else).

Another useful resource is https://www.lua.org/demo.html , which is an online Lua interpreter. This is useful for running small scripts that do not depend on ToTPP specific functionality.

Download and install Notepad++ or some other text editor that you prefer.

Open Notepad++ and get a new (blank) file.

Set the Language to Lua, to get syntax highlighting.

Lesson2pic1.jpg



We first begin by writing some "comments." Comments are ignored by the Lua interpreter, but they can be helpful to people trying to understand, use, or modify your code. You might one day be one of those people, so at the very least leave the comments you would want to see six months after you wrote the code.

Two dashes (--) will make everything to the end of the current line a comment

--[[ will comment out everything until a corresponding ]] is reached.

Lesson2pic2.jpg


Commenting can also be useful to stop Lua from executing code that you might still want to include later.

Let us now write some print commands (in an empty file):

Code:
-- This is the first verse of The Maple Leaf Forever
print("In Days of yore, from Britain's shore,")
print("Wolfe, the dauntless hero, came")
print("And planted firm Britannia's flag")
print("On Canada's fair domain.")
print("Here may it wave, our boast, our pride")
print("And, joined in love together,")
print("The thistle, shamrock, rose entwine")
print("The Maple Leaf forever!")

Save this as MapleLeafForever.lua

Lesson2pic3.jpg



First, notice the colours of the text

Line 1 is a comment, so it appears in green.

print appears in light blue, since it is a command that always comes with a Lua interpreter. The words we want to print appear in grey, since they are strings. Exactly what that means will be explained later in this lesson.

Note that colouring will change with different text editors, and even the 'classification' of certain keywords might change. The colouring is only meant as an aid to the programmer (it is not a fundamental part of Lua), and different text editors will have different ideas about what is helpful.

When you run a script, the Lua interpreter will start at line 1 and go line by line executing commands.

Save your copy of MapleLeafForever.lua in your Test of Time directory. (You don't actually have to do this, but the next step will be easier if you do.)

Open Test of Time, start a new game, and open the Lua Console.

Press the 'Load Script' button, navigate to your Test of Time directory (probably just 'up one level') and select MapleLeafForever.lua

The Maple Leaf Forever Lyrics should appear in your Console.

You may leave Test of Time and the lua console open as you do the following. Open MapleLeafForever.lua in your text editor and change the lines around. For example:

Lesson2pic5.jpg



Note that the Lua interpreter will simply ignore empty lines when executing instructions. Save MapleLeafForever.lua and use Load Script to load it back into the TOT Lua Console.

Your Lua Console will now look something like this:

Lesson2pic6.jpg



The Lua interpreter simply followed each instruction as it was written. It doesn't care that the output makes no sense. It is your job as a programmer to give the computer exact instructions. It isn't going to try to figure out from context what you really want, unless someone else has already written a program trying to mimic human understanding.

Restore MapleLeafForever.lua to print the song lyrics in the correct order, but have it only print the first four lines of the song. Use comments to disable the last four lines, so they can be quickly re-introduced to the program if we decide we want them later. Use the two different methods of commenting described earlier.

Additionally, add two lines of

Code:
 print(" ")

to the top of MapleLeafForever.lua in order to leave 2 lines between each printing of the lyrics. I will not show a picture of the code, but if you did it correctly, your console will look like this. If you made a mistake, you will have some extra text displayed in the console. Don't worry about that.

Lesson2pic7.jpg


More Commands

Thus far, the only instruction we've given the Lua Console is to print some text. Now, let us learn some more instructions.

The first instruction we need is the ability to create a variable and assign a value to it. For example

Code:
 myVariable = "My Value"

assigns "My Value" to myVariable. Whenever we write myVariable later in our code, the Lua interpreter will replace it with "My Value" unless we assign something else to myVariable.

Go to the lua demo site https://www.lua.org/demo.html and run the following code. (You could write a script and use the ToT lua console, but this will probably be easier.)

Code:
myVariable = "My Value"
print(myVariable)

The output will be My Value

Lesson2pic8.jpg


Now, reverse the two commands.

Code:
print(myVariable)
myVariable = "My Value"

You will find that even though "My Value" was assigned to myVariable in the code, print(myVariable) didn't print it. Instead it printed nil, which is the special value in Lua that represents an absence of a value. This is because "My Value" was assigned to myVariable after myVariable was used in the print command.

Look at this code, and predict what it will do. Be careful, there is a trick that touches on something mentioned in lesson 1. Write down what you think will happen. Once you've done that, cut and paste this code into the Lua Demo and compare your prediction to the actual result.

Code:
myVariable = "Ten"
print(myVariable)
print(myVariable)
myVariable = "Eleven"
myVariable = "Twelve"
print(myVariable)
myvariable = "Thirteen"
print(myVariable)

Spoiler :

The "trick" is that lua commands are case sensitive. myvariable is not the same thing as myVariable, as we saw when we deliberately made an error in lesson 1 by using Civ instead of civ


This page https://www.lua.org/pil/1.3.html tells what is a valid variable name. Basically, "any string of letters, digits, and underscores, not beginning with a digit." There are some reserved words as well (they all appear as bold dark blue in Notepad++).

Now, let us do a little arithmetic. Per https://www.lua.org/pil/3.1.html

Lua supports the usual arithmetic operators: the binary `+´ (addition), `-´ (subtraction), `*´ (multiplication), `/´ (division), and the unary `-´ (negation). All of them operate on real numbers.

Lua also offers partial support for `^´ (exponentiation).

These follow the normal order of operations. You can use parentheses to make sure the behaviour is exactly what you want.

Let us convert kilometers to miles, using the conversion 2.54 cm equals 1 inch.

Code:
distanceInKM = 12
cmPerKM = 100000
kmPerCM = 1/cmPerKM
cmPerIN = 2.54
inPerCM = 1/cmPerIN
inPerFT = 12
ftPerIN = 1/inPerFT
ftPerMI = 5280
miPerFT = 1/ftPerMI
distanceInMI = distanceInKM *cmPerKM*inPerCM*ftPerIN*miPerFT
print(distanceInMI)

Since distanceInKM was set to 12, the output is the mile equivalent.

Specifying all the conversion ratios was a bit of overkill for this one unit conversion, though if we had to do several different kinds, it might have eventually made things easier.

Distance conversions are essentially one line operations. After defining the conversion ratios at the top of the script, all that has to be done is to write the multiplication for the conversion. Let's do something slightly more complicated, converting from Fahrenheit to Celsius

Code:
tempInF = 68  -- Temperature we want to convert
fFreezeTemp = 32 -- Water freezing point in Fahrenheit
fPerC = 1.8 -- This is the number of Fahrenheit degrees per degree Celsius
fFreezeAt0Temp = tempInF-fFreezeTemp -- This would be the temperature if 0 Fahrenheit were freezing temperature
tempInC = fFreezeAt0Temp/fPerC
print(tempInC)

With this calculation set up, subsequent conversions would need to be written as

Code:
fFreezeTemp = 32 -- Water freezing point in Fahrenheit
fPerC = 1.8 -- This is the number of Fahrenheit degrees per degree Celsius

tempInF = 72
fFreezeAt0Temp = tempInF-fFreezeTemp
tempInC = fFreezeAt0Temp/fPerC
print(tempInC)

tempInF = 100
fFreezeAt0Temp = tempInF-fFreezeTemp
tempInC = fFreezeAt0Temp/fPerC
print(tempInC)

tempInF = 212
fFreezeAt0Temp = tempInF-fFreezeTemp
tempInC = fFreezeAt0Temp/fPerC
print(tempInC)

This would get annoying very fast, although we could cut and paste. However, there is a way to specify instructions in advance. We do this by creating a function.

Code:
function myFunction(input1,input2)

end

The key word "function" tells lua that we are specifying instructions to be used later, and that we will want those instructions when we write myFunction. When we call the function, we replace input1 and input2 with the values we wish to evaluate the function with. A function can be defined with as many input values as you would like, or even 0 input values. Calling myFunction(10,12) will set input1=10 and input2=12, and run the commands found after myFunction until the word end is reached. (Later, we'll see end used in other places. Think of end as a sort of ')', where there are several things equivalent to '('. The function ends when the 'end' matching 'function' is reached.)

Code:
function fToC(tempInF)
local fFreezeTemp = 32 -- Water freezing point in Fahrenheit
local fPerC = 1.8 -- This is the number of Fahrenheit degrees per degree Celsius
local fFreezeAt0Temp = tempInF -fFreezeTemp
local tempInC = fFreezeAt0Temp/fPerC
return tempInC
end

There are a couple of things to address now. First, the command 'return' immediately ends the function execution, regardless of what other commands might still be available, and turns myFunction(inputA,inputB) into the value to the right of 'return'. 'void' means the function didn't return anything, either because it didn't have a return function, or because it returned no value.

The second is the use of 'local' when defining a variable. 'local' restricts what parts of your program can access the variable you just created (and it makes accessing that variable faster). Global variables can be accessed and changed by any part of the program.

For now, remember that local inside a script means that only other code in that script can access and make changes to the variable. They will not be accessible by command in the lua console! A local variable inside a function will only be accessible to that function, and variables specified in a function declaration are also local. It is strongly recommended to initialize all your variables as local variables, in order to avoid strange bugs. If your variables are initialized as local, you don't have to worry about whether somewhere else in the code there is a variable with the same name (unless they were both initialized at the same 'level').

Code:
function buggy(input)
    j = 3
    print(input)
end
print(j)
j = 7
buggy(8)
print(j)

You would probably expect print(j) to yield 7, but instead it yields 3. Remember this before you decide to make a global variable.

Types of Values

Now that we're starting to use commands and functions, we need to know about the different types of values. It is important to understand types of variables since most functions can only accept a particular type of value as input.

For example:

Code:
print("Maple"+"Leaf"+"Forever")

Yields an error, since it doesn't make sense to add strings.

Per https://www.lua.org/pil/2.html ,

There are eight basic types in Lua: nil, boolean, number, string, userdata, function, thread, and table. The type function gives the type name of a given value:

These lessons will not cover the types userdata and thread. Tables are very important and will be covered in a later lesson, since they are more complicated.

nil, as mentioned earlier, is the type representing 'nothing' in lua. Notepad++ colours this in bold dark blue.

boolean is a type with exactly two values, true and false. They're mostly used in logical statements, but can be used for anything which naturally has two values (such as flags). Notepad++ colours these in bold dark blue.

number is pretty straightforward, it is a real number. Lua doesn't distinguish between integers and decimal numbers, but if you want to be extra sure you have an integer, you can use the functions math.floor(number) and math.ceil(number) to round down and up respectively. Notepad++ colours these in orange.

string is text. Enclose it in '' or in "" or in [[]]. Certain characters, (e.g. newline '\n') are written with a backslash, so the backslash character is '\\' Notepad++ colours these in grey.

function is as we've defined it earlier. But, as a type, they can be stored in variables, passed as arguments to other functions, or returned as results.

In addition to these types native to Lua, there are several types that the Test of Time Patch Project provides so that we can interact with the game. These are described in the thread [TOTPP] Lua function reference https://forums.civfanatics.com/threads/totpp-lua-function-reference.557527/

You will most often use the 'unit object', 'unittype object' and 'tile object', but all these 'objects' provide a means of interacting with some 'piece' of the game. You shouldn't think of a unit object as the unit itself, but rather as a way of "accessing" the unit.

if we have

Code:
unitA = civ.getActiveUnit()
unitB = unitA

we haven't duplicated the active unit, but instead made a second reference to the same 'game piece,' and changes to unitA will be reflected in unitB.

We access different characteristics of the unit by using a '.' (the meaning and usage of '.' will be clearer after we explain tables, so, for the moment, just accept that sometimes '.' are used in commands)

If a command is a 'get' command, that means we can use that characteristic of the unit when writing our program. For example

Code:
 unit.hitpoints

is a 'get' command, letting us base our event on the remaining hitpoints of a particular unit.

If a command is a 'set' command, we can change the characteristic of the 'game piece' using that command.

Code:
 unit.damage = unit.damage + 3

The unit.damage on the Right Hand Side of the '=' 'gets' the current damage the unit has taken, in order to add 3 to it. The unit.damage on the Left Hand Side is 'setting' the units damage to the result of the Right Hand Side calculation.

It is important to understand the kind of value/object the function you are trying to use is expecting. It may sometimes make sense to refer to a settler unit type by its corresponding integer id (0), sometimes by its name (in string form) "settler" and sometimes by its unittype object. It is important to understand what kind of data the function you are using expects to get. If the function is expecting to receive a unittype object and you give it an integer, 0, the function will not work and you will (hopefully) get an error.

.id is usually the command to convert from a TOTPP 'object' to an integer, and if you need to go from an integer to the 'object', look for commands starting with civ.get, since those will usually allow you to go the other way.

Persistent Events

To end this lesson, we will now write some events in an Events.lua file. Attached to this post is a TOT conversion of the Rome scenario that shipped with the original civ 2 game. We will write some events for it. Put it into your Test of Time directory (or sub directory) as you would with any other scenario

First note that there is an empty events.txt file in the folder. At the time of writing, this must be in the folder for the game to recognize the Events.lua file.

At the moment, the events file is just about as empty as it can be.

When you load the game, the code in lua events will immediately run. Most of the code will be to initialize data that won't change throughout the game.

the functions civ.scen.triggerType(function) tell the game to run the function given as the argument whenever the trigger Type occurs.

Start the scenario as the Romans, and save a game (that way you don't have to start a scenario every time).

Let us write an event to make campaigning more expensive. Every time combat happens, the attacker and defender will each lose 1 gold.

Insert a line defining the variable campaignCost and set it to 1
Code:
local campaignCost = 1

Now, let us write a function to be run after a unit is killed. From the TOTPP Lua Function Reference, we find that

onUnitKilled
civ.scen.onUnitKilled(function (defender, attacker) -> void)

This means that the function we use should not return anything, and will be passed two arguments, the defending unit (i.e. the unit that was killed) and the attacking unit (the unit that won). These will be the unit objects of these units.

Since defender and attacker are a little ambiguous, we'll write our function using the terms loser and winner

Code:
local function doWhenUnitKilled(loser,winner)
    loser.owner.money = math.max(loser.owner.money -campaignCost,0)
    winner.owner.money = math.max(winner.owner.money-campaignCost,0)
end

loser.owner returns the tribe object of the loser's tribe. From there, .money gives us access to the treasury value, which we can set to the Right hand side. We take the tribe's money and subtract the campaign cost to get the new value of money. We use math.max to ensure that the tribe never has a negative treasury (though, as a consequence, the tribe can campaign for "free" on an empty treasury).

We need to tell the game to run the code when a unit is killed, so write
Code:
civ.scen.onUnitKilled(doWhenUnitKilled)

Lesson2Pic9.jpg


Reload the game and open the cheat menu to put a unit in direct position to attack someone else. Make the attack and check that the treasury has 1 gold deducted afterwards. You should check that the victim also had one gold deducted (you'll have to give yourself an embassy through cheat mode, or change players).

Now, change the campaign cost to 5, load your saved game, and try the test again, this time checking that 5 gold is deducted per attack.

Notice that we could change the deduction for both the attacker and defender in one place since we created and used the variable campaignCost.

Now, let us add a little flavour, and alternate the leader of the Romans every turn, to reflect the alternating nature of the consulship. We'll use Scipio and Fabius as the two leaders.

Code:
local evenTurnLeader = "Scipio"
local oddTurnLeader = "Fabius"

We'll use the tribe and leader objects to make the change. However, we need code that will execute only if certain conditions are met. For that, we use an "if statement".

The most basic type of if statement is as follows
Code:
if condition then
--instructions
end

When the line if condition then is met, the lua interpreter determines if condition is true or false. If it is true, the code between then and end is executed. If the condition is false, the program skips to the 'end' and continues after that. In lua, all values except false and nil are considered true. Even 0 is considered true (which is different from some other programming languages). This will be discussed in a later lesson.

The operator == is the equality test operator.

Code:
 a==b
returns true if a and b are equal, and false otherwise.

To determine if the turn is even or odd, we'll use the modulo operator %. a%b returns the remainder after dividing a by b. This will allow us to test whether the current turn is even or odd.

First, let us make it easy to reference the roman tribe
Code:
local romans = civ.getTribe(1)
Now, let us write a function for our event
Code:
local function doThisOnTurn(turn)
    if turn % 2 == 0 then
        --turn is even
        romans.leader.name = evenTurnLeader
    end
    if turn %2 == 1 then
        -- turn is odd
        romans.leader.name = oddTurnLeader
    end
end--doThisOnTurn

civ.scen.onTurn(doThisOnTurn)

Load the game, and end your turn. In 277 B.C., you should see that King Fabius is the leader in your various reports.

Let us give the Celts a bonus against the Romans. If the Celts own a city at (38,12) (Milan) and the square (37,15) is either empty or defended by the Celts, then the Celts get a chariot at that square every turn.

We have to add this to the doThisOnTurn function

Code:
local celts = civ.getTribe(7)
local chariot =civ.getUnitType(16)
local milan = civ.getTile(38,12,0).city
local chariotSquare = civ.getTile(37,15,0)

Code:
local function doThisOnTurn(turn)
    if turn % 2 == 0 then
        --turn is even
        romans.leader.name = evenTurnLeader
    end
    if turn %2 == 1 then
        -- turn is odd
        romans.leader.name = oddTurnLeader
    end
    if (milan.owner == celts) and (chariotSquare.defender == nil or chariotSquare.defender == celts) then
        local newChariot = civ.createUnit(chariot,celts,chariotSquare)
        newChariot.veteran = false
        newChariot.homeCity = nil
    end
  
end--doThisOnTurn

a and b returns true if a and b are both true. a or b returns true if either a or b is true, and also if they are both true.

Note that we have to add this code to our existing doThisOnTurn function. newChariot.veteran = false means that the new chariot is not a veteran, and newChariot.homeCity=nil means that it has no home city.

lesson2pic11.jpg


Re-load the game and test out the various conditions for this event.

This concludes lesson 2.

EDIT: Changed 2 variables in the function fToC from global to local, as there was no reason for them to be global.
 

Attachments

  • ClassicRome.zip
    345.3 KB · Views: 295
Last edited:
Lesson 3: Tables, Loops, Logic, and Debugging

Introduction to Tables


Thus far, we've stored one value at a time in a variable. However, we often want to deal with multiple values of data at once, either because they go together naturally, or because we want to keep them in a list (and often times, both reasons together).

A table contains pairs of 'keys' and 'values'. A 'key' in a table can be a number or a string (The key can also be other values, but we'll ignore that in these lessons. Strings and numbers are powerful enough.). The value can be anything, including TOTPP specific objects and other tables.

We can declare an empty table as

Code:
 myTable = {}

We can give the table a value by the following assignment syntax

Code:
myTable[keyOne] = myValue

We can then access myValue by

Code:
myTable[keyOne]

We can create a table with initialized data as follows
Code:
myOtherTable = {[keyOne] = valueOne,[keyTwo] = valueTwo, [keyThree] = valueThree,}

The ',' after valueThree isn't necessary, but it doesn't hurt anything either. We can also write

Code:
myThirdTable = {valueOne, valueTwo,valueThree}

which is equivalent to
Code:
myThirdTable = {[1]=valueOne,[2]=valueTwo,[3]=valueThree}

What's more, we can define tables inside of tables, e.g.
Code:
coordinates = {{110,44,0},{111,45,0},{109,43,0}}

We would access the 2nd coordinate (and get the tile object) by

Code:
tileTwo = civ.getTile(coordinates[2][1],coordinates[2][2],coordinates[2][3])

When dealing with nested tables, the leftmost key is is for the outermost table.

If our key is a string that satisfies the conditions of a variable name (no spaces, starts with a letter), we can use the following syntax instead of square brackets:

Code:
newExampleTable = {firstEntry = "MyFirstString",secondEntry="MySecondString",["thirdEntry"] = "Three"}
newExampleTable.firstEntry == "MyFirstString" -- returns true
newExampleTable["secondEntry"] == "MySecondString" -- returns true
newExampleTable.thirdEntry =="Three" -- returns true

Note that if a table doesn't have a key defined, it is equivalent to having nil stored as that key's value.

Let us return for now to the Classic Rome scenario from last time. We will now have the Seleucid kingdom change ruler name in the same year that the change happened historically. We'll use the function civ.getGameYear (which returns negative values for the BC years) and an appropriately indexed table to write the function.

Code:
local seleucids = civ.getTribe(5)
local seleucidRulers = {
[-281] = {name = "Antiochus I Soter", female = false},
[-278] = {name = "Antiochus I Soter", female = false}, -- this is the year the game starts
[-261] = {name = "Antiochus II Theos", female = false},
[-246] = {name = "Seleucus II Callinicus", female = false},
[-225] = {name = "Seleucus III Ceraunus", female = false},
-- this makes the point
[-126] = {name = "Cleopatra Thea", female = true},
-- have a female ruler to test, though
}--close seleucidRulers

-- put this in the onTurn function
if seleucidRulers[civ.getGameYear()] then
    -- if there is no entry for this index, it returns nil, which the if statement counts as false.
    seleucids.leader.name = seleucidRulers[civ.getGameYear()].name
    seleucids.leader.female = seleucidRulers[civ.getGameYear()].female
end --if seleucidRulers[civ.getGameYear()]

Lesson3Pic1.jpg

Start a new scenario, so that the Seleucid leader is changed for the first turn. Then use cheat mode to change the game year to see if the changes work as anticipated.

Try adding extra rulers to see if you understand the basics of this table. If you want to use actuall rulers, try this Wiki Page: https://en.wikipedia.org/wiki/List_of_Seleucid_rulers

A table value is like a unit value, in that what is stored is a reference to the data, and not the data itself. If you want to actually have two copies of a table, you have to write a specific funciton to make the copy. If you don't intend to change the table after you create it, then this doesn't matter. However, it can lead to bugs if you try to make changes.

A couple of examples will help.

Code:
table = {a=1,b=2,c=3}
print(table.a)
copy = table
print(copy.a)
table.a = 4
print(table.a)
print(copy.a)

Run this code at https://www.lua.org/cgi-bin/demo and you will find that copy.a was changed to 4 when table.a was modified

Code:
function myNumberFunction(num)
num = 4
end

function myTableFunction(table)
table.a = 5
table.d = 4
end

myNumber = 7
myNumberFunction(myNumber)
print("My Number is "..tostring(myNumber))

myTable={a=1,b=2,c=3}
print("The keys a,b,c,d of myTable have the following values")
print(myTable.a)
print(myTable.b)
print(myTable.c)
print(myTable.d)

myTableFunction(myTable)
print("The keys a,b,c,d of myTable have the following values")
print(myTable.a)
print(myTable.b)
print(myTable.c)
print(myTable.d)


Notice that changing the input value in myNumberFunction didn't make a change to myNumber, but myTableFunction could make changes to myTable. This is something to keep in mind if you decide to write functions that modify entries in a table.

You may have also noticed my use of '..' Two periods is the function for joining strings together. the function tostring converts the input to a string, so that it can be joined to another string.

Introduction to Loops

Often, we want to execute the same lines of code many times, perhaps with small changes to the code each time. Cutting and pasting the same lines of code dozens or hundreds (or more) of times is impractical. Instead, we use a loop.

Perhaps the easiest kind of loop to understand is the 'while' loop.

Code:
while condition do
-- instructions
end

When the lua interpreter reaches the line with 'while', it checks if the condition evaluates to true (the same way an if statement would). If the condition is false, the interpreter skips everything between 'do' and the corresponding 'end' and continues executing code from there.

If the condition evaluates to true, the lua interpreter executes all the code between 'do' and 'end' and then jumps back to the 'while' line and checks the condition again. The lua interpreter will keep executing the code in the loop until the condition becomes false. Be careful when writing while loops, because if the condition doesn't eventually become false, the lua interpreter will run the code in the loop forever (and you will have to stop it manually).

The key word 'break' can also be used to immediately exit any loop, and 'return' can be used to stop the entire function and return a value.

We can use a while loop to calculate factorials. Recall that N! = N*(N-1)*(N-2)*...*3*2*1
Code:
function factorial(N)
local productSoFar = 1
while N >= 1 do
    productSoFar = N*productSoFar
    N = N-1
end
return productSoFar
end
print(factorial(0))
print(factorial(1))
print(factorial(2))
print(factorial(3))
print(factorial(4))
print(factorial(5))
print(factorial(6))

The other type of loop we will discuss is the 'for' loop, but in lua there are actually several different 'kinds' of for loop.

'Numeric For Loop'

Code:
for index=startVal,stopVal,increment do
-- instructions
end

This kind of for loop defines a variable (in this case 'index') which can be used in the loop instructions. The loop index will be initialized to startVal, and after every loop will be incremented by the amount 'increment'. Once index > stopVal, the loop body is no longer executed and the program moves on.

If increment is omitted, 1 is used as the increment. If the increment is less than 0, the loop stops when index < stopVal.

Returning to our factorial example, we could write it as
Code:
function factorial(N)
productSoFar = 1
for i=1,N do
productSoFar = i*productSoFar
end
return productSoFar
end
for i=0,12 do
print(factorial(i))
end

Notice how the print lines could also be grouped into a loop

The next kind of for loop operates on tables, and is defined in the following way

'Generic For Loop'
Code:
for key,value in pairs(table) do
    --instructions
end

This will execute the loop body (the part between do and end) for each key and value pair in the table. The order of the execution is whatever is most convenient for the lua interpreter, not 'random', but also not necessarily in the order you would expect. If you need a particular order,use integers as keys and write either a numeric for loop or a while loop (or look up how to use 'ipairs').

Let us have an event that creates barbarian units on certain tiles, if those tiles are empty or already contain barbarians. We'll specify each location using a table with two entries (since we only have one map) and the unit with a ToTPP unittype object. Since we'll be using a generic for loop, we don't have to worry about the index, so we'll leave the defaults in place.

Code:
local barbarians = civ.getTribe(0)
local archers = civ.getUnitType(4)
local horsemen = civ.getUnitType(15)

local barbTable = {
{location = {44,12}, unit = archers},
{location = {35,39}, unit = horsemen},
{location = {64,16}, unit = horsemen},
}-- close barbTable

for __, barbCreate in pairs(barbTable) do
    local tile = civ.getTile(barbCreate.location[1],barbCreate.location[2],0)
    if tile.defender == barbarians or tile.defender == nil then
        civ.createUnit(barbCreate.unit,barbarians,tile)
    end
end


The final type of 'for loop' is also considered a 'Generic' for loop and it is used with a special type of function called an 'iterator.' Iterators are a part of lua (in fact pairs technically creates an iterator) and can be written by the user, but for our purposes we will get iterators when using certain ToTPP built in functionality.

for example, we have

iterateCities
civ.iterateCities() -> iterator
Returns an iterator yielding all cities in the game.

iterateUnits
civ.iterateUnits() -> iterator
Returns an iterator yielding all units in the game.

units (get)
tile.units -> iterator
Returns an iterator yielding all units at the tile's location.

Let us make it difficult for an army to cross the desert. Between turns, we will half the unit's remaining hitpoints, rounded up (so the unit will always have at least 1 hp).

Code:
local desertTerrain = 0

for unit in civ.iterateUnits() do
    if unit.location.terrainType % 16 == desertTerrain then
        local newDamage = math.floor(unit.hitpoints/2)
        unit.damage = unit.damage+newDamage
    end
end

The terrainType value is simply an integer, and the terrain types are given values 0-15. These are the last 4 bits of the byte. The first 4 bits are flags, which we eliminate by the modulo operation. This isn't crucial to understand now.

Start the scenario, this time as the Ptolemaic Greeks, and test out the desert functionality. After the first turn, you will have to move the units to make sure they don't heal up an amount equal to the damage they take.

Lesson3Pic2.jpg


More Logic


Last lesson, we used 'if' statements along with 'and' and 'or' to specify code that should only be executed if certain conditions are met. Let us look at logic a bit more closely.

Thus far, we've used if statements without else. The structure of else in an if statement is as follows

Code:
if condition then
-- execute code here only if condition is true
else
-- execute code here only if condition is false
end

An if-else statement will only execute one body of code, never both. It is useful when we want to choose between two options, and it may be clearer what we are doing than if we write two separate if statements. To use our alternating Roman rulers example from last lesson, we can write our event as

Code:
if turn %2 == 0 then
    --turn is even
    romans.leader.name = evenTurnLeader
else
    --turn is odd
    romans.leader.name = oddTurnLeader
end

Next, we consider 'elseif'. 'elseif' is a combination of else and if, as the name implies. If the original if statement is false, the lua interpreter will check the condition on the first elseif statement. If that is true, the code is executed and then all other elseif (and else) bodies are ignored, even if another condition would be met.

Code:
if condition1 then
--code
elseif condition2 then
--code
elseif condition3 then
--code
elseif condition4 then
--code
else
-code
end

In the above if statement, exactly one section of code would be run, depending on which condition was true first. If there is no 'else' before the end, then it is possible that no section of code will execute, if all the conditions are false.

We can use elseif to change our campaigning costs depending on the unit.

Elephants are expensive to move and supply, so they'll cost 5 gold per combat. Catapults will cost 4 gold to battle. All other units will have a combat cost equal to their maximum movement points.

Code:
local elephant = civ.getUnitType(17)
local catapult = civ.getUnitType(23)
local elephantCampaignCost = 5
local catapultCampaignCost = 4
for __,unit in pairs({loser,winner}) do
    if unit.type == elephant then
        unit.owner.money = math.max(unit.owner.money-elephantCampaignCost,0)
    elseif unit.type == catapult then
        unit.owner.money = math.max(unit.owner.money-catapultCampaignCost,0)
    else
        unit.owner.money = math.max(unit.owner.money - unit.type.move/totpp.movementMultipliers.aggregate,0)
    end
end

Note that Civ II does not keep track of fractional movement points, but instead expends several movement points when moving without benefit of road/rail/river. totpp.movementMultipliers.aggregate finds that multiple.

The logical operator 'not' is quite straightforward. It returns true if it operates on false or nil, and false otherwise. That is, false becomes true and true becomes false.

Code:
print(not 7)  -- false
print(not false) -- true
print(not true) -- false
print(not nil) -- true
print(not {}) -- false

The logical operator 'and' returns its first argument if that argument is false, otherwise it returns its second argument. This can be useful for 'guarding' against trying to access something that does not exist, since if the variable you're accessing is nil, and will return nil (i.e. false) and not try to do something with the value. For example,

Code:
animals = {cats = 4, dogs = 3, pigs = 2}

if animals.horses >= 3 then
    print("There are at least three horses")
end

results in an error (try it in the lua demo).

We can guard against this in two ways:

Code:
animals = {cats = 4, dogs = 3, pigs = 2}
if animals.horses ~= nil then
    if animals.horses >= 3 then
    print("There are at least three horses")
    end
end
or
Code:
animals = {cats = 4, dogs = 3, pigs = 2}
if animals.horses and animals.horses >= 3 then
    print("There are at least three horses")
end

Using 'and' as a 'guard' will be a problem if the value you are making sure is there happens to be 'false', but this should be a rather uncommon problem.

The logical operator 'or' returns its first argument if that argument is true, otherwise it returns the second argument. One application of this is that you can assign a value to a variable if it doesn't exist, but leave the variable alone if it does already. For example

Code:
animals = {cats = 4, dogs = 3, pigs = 2}
animals.horses = animals.horses + 4
print(animals.horses)

results in an error.

Code:
animals = {cats = 4, dogs = 3, pigs = 2}
animals.horses = animals.horses or 0
animals.horses = animals.horses + 4
print(animals.horses)

This way, if animals.horses is set to 0 if it doesn't already exist, and is left alone if it does.

Code:
animals = {cats = 4, dogs = 3, pigs = 2, horses = 2}
animals.horses = animals.horses or 0
animals.horses = animals.horses + 4
print(animals.horses)

For reference, we could have done the assignment/addition on one line:

Code:
animals = {cats = 4, dogs = 3, pigs = 2, horses = 2}
animals.horses = (animals.horses or 0) + 4
print(animals.horses)


Fixing Coding Errors

For our purposes, there are three classes of errors we can make when writing code in lua. The first kind are syntax errors that will be caught when you try to load a script into the Lua interpreter. The TOTPP lua interpreter will warn you of the first error and print the kind of error it thinks it is in the console, along with a line number, where it realized the error exists. Usually, this is the result of forgetting to insert a 'do' or 'then' or a comma, or to close a table, or string, or something similar. The fix will usually be at the line number specified in the error message, and if not, somewhere close by.

The second class of errors are the run-time errors. Code that passed the 'syntax check' turns out to be bad at run time, usually because the wrong type of value was passed to a function. Often, these happen because you misspell a variable name (either at use or at assignment), or you accidentally misused a function.

The final class of errors are the logic errors. Logic errors mean that your code is running properly and without run time errors, but it is not doing what you want it to do. These are caught by carefully testing your code and fixed by carefully analyzing your algorithm for performing the event to make sure that your proposed solution actually solves the problem and then by analyzing your code to make sure it actually does what your proposed solution requires. One thing to try if you have a logic error that you can't figure out how to correct is to put 'print' at various places in your code to figure out what is being executed and on what arguments. You can also ask for help on the forums; sometimes a fresh eye can spot mistakes that you missed.

For now, let us consider a more complicated piece of functionality for our ClassicRome scenario, which will provide us with some errors to correct. We will attempt to force land units to disembark from a ship only at a city. We will make an exception for settlers and engineers in order to allow them to build cities on new continents.

Here is what we will try: If a ground unit activates on water, all adjacent land squares that do not contain a city will be populated by a barbarian bomber (if we wanted to, we would later change the name/picture of that unit. Since we don't want the bombers to stick around, we will go through every unit in the game on activation and delete all barbarian bombers (there are less computationally expensive ways to clear the bombers, but we need functionality not yet covered to make sure they still work after a save and a load).

Here is our sample code. Attached to this post is an Events.lua file with this code.

Code:
local function doOnActivation(unit,source)
-- remove all barbarian bombers
local blockerunit = civ.getUnitType(28)
for checkUnit in civ.iterateUnits
    if checkUnit.owner == civ.getTribe(7) and checkUnit.type ==blockerUnit then
        civ.deleteUnit(checkUnit)
    end
end

if unit.domain == 0 and unit.location.terrainType % 16 == 10 and not(unit.role ==5)
    for __, offsetPair in pairs({{2,0},{1,1},{0,2},{-1,1},{-2,0},{-1,-1},{0,-2},{1,-1},}) do
        local x = unit.location.x
        local y = unit.location.y
        local z = unit.location.z
        local tlie = civ.getTile(x+offetPair[1],y+offsetPair[2],z)
        if tile.terrainType % 16 ~= 10 then
            civ.createUnit(blockerUnit, civ.getTribe(7),tile)
        end
end
end

Save a backup copy of the existing Events.lua file in Classic Rome, then replace the ClassicRome Events.lua with the Events.lua file attached to this post. Load a Classic Rome game. I suggest loading a game where the cheat mode has already been activated, to speed up the process.

You will get the following message

Lesson3Pic3.jpg


Open the console, and you will find this message


Lesson3Pic4.jpg


If we look near line 109, we indeed find this:

Lesson3Pic5.jpg


Line 108 does, indeed, need a 'do'. Make this correction, save Events.lua, and load the game again.

Loading again, the lua console tells us that 'then' is expected near line 115, and, indeed, line 114 is missing a 'then'.

The next error we find is

Code:
 C:\Test of Time\Scenario\ClassicRome\events.lua:137: 'end' expected (to close 'function' at line 105) near <eof>

This message means that we've forgotten to include an 'end'. The Lua interpreter is suggesting it should be somewhere near line 137 (since that is when it "realized" that an 'end' is missing), however that is incorrect. Lua is correct, however, that the function at line 105 does indeed need to be closed. However, that is not the whole story either. I didn't leave out an 'end' at the end of the 'doOnActivation' function, but rather, forgot the 'end' for the 'for loop' beginning at line 115.

Insert a new line after line 122, and put an end there.

Missing 'end's are some of the more difficult mistakes to track down and correct, because nested loops and if functions make it difficult to figure out where the missing 'end' should go. One strategy to use is proper indentation. If you open a new if or loop, indent all the code that is within that block. Not only is it clearer what code is actually in the loop, it is easier to spot a missing 'end'. Another strategy is to put a comment after each 'end' telling the contents of the line for which this 'end' corresponds. A third strategy is to write 'end' at the same time as the if, for, while, or function that requires it, so you don't forget later. These are just my suggested strategies, and it is perfectly reasonable to use other strategies instead.

Lesson3Pic6.jpg



When you next load a game, your events will load properly. However, we're dealing with an onActivation event, so there is a good change the Lua console will immediately open with an error. If not, activate a unit, and then it should.

Lesson3Pic7final.jpg



The error you get is actually more complicated than I intended. The cause of this error is that I wrote civ.iterateUnits instead of civ.iterateUnits(), which is to say I didn't actually call the function civ.iterateUnits. The reason that the error is so complicated is that civ.iterateUnits returns a function, and "for units in value do" expects 'value' to be a function, which civ.iterateUnits actually is.

So, the loop proceeded as normal, and checkUnit was assigned the value of civ.iterateUnits(), which is a function (in particular, an iterator). Since the first thing we did was to do checkUnit.owner, we tried to 'index' the function as we would a table or a lua object, which generated a run-time error.

Add the () to civ.iterateUnits and load the game again. At this point, we can move units around as we wish, so we have to test our event to make sure it works. Load 2 legions into the trireme at Rome, and go one square out to sea. Activate one of the legions.

You will find that nothing happens. Go through the code and see if you can spot the logic error.

Spoiler :

The reason that nothing happens is because because the unit object doesn't have a 'domain' entry, therefore unit.domain is nil, which 'if' counts as false. What we want is unit.type.domain, since we want the unit type of the unit. Similarly, we want unit.type.role, not unit.role



Make the necessary corrections, and try again.

The next error happens at line 119, and it is an 'attempt to index a nil value', with that value being global 'offetPair'. An attempt to index something means we are trying to work on a table or ToTPP object. Since the value is 'nil', that thing we are trying to work on probably doesn't exist (it might, if we defined a variable as nil so we would be able to use it later). Since the variable is a global variable, that is another indication that we tried to access something we didn't create (assuming we've remembered to make our variables local variables).

It turns out that 'offetPair' should be 'offsetPair' and that this particular error was the result of a typo that did, indeed, happen on line 119. Make the correction.

Our Next error is at line 120, which is again an attempt to index a nil value, this time the global 'tile'. This time, the typo was in the declaration of the variable. It happened at line 119, but it could have happened further away. In this case, line 119 declared a variable 'tlie', which should have been 'tile'. Make the correction.

Lesson3Pic8final.jpg


Our next error is detected at line 121, which explains that there is a 'bad argument #1' to 'createUnit', and that the function expected a civ.unittype, but got a nil.

A bad argument means that the wrong kind of value was given to a function as one of its arguments (a function 'argument' is a value that is "put into" the function in order to be acted upon). Since this function was given a 'nil' value, that value probably doesn't exist.

The first argument to civ.createUnit at line 121 is 'blockerUnit'. It turns out the reason for the error is that on line 107, we declare 'blockerunit' instead of 'blockerUnit'. The error is in the declaration since line 109 also uses 'blockerUnit'.

Make the correction.

Lesson3Pic9final.jpg



Now, when we activate a unit on the ocean, bombers are created on adjacent land squares. However, these are owned by the Celts, not the barbarians. This is a logic error, and has happened because the barbarians are tribe 7 and not tribe 0. Find both places where tribe 7 was used instead of tribe 0 , and make the switch.

When you again activate a ground unit on the water, the bombers will appear, and will be barbarian. And, in fact, when you activate another unit, the bombers disappear.

Try out this event, see where it fails, and how a player might circumvent it.

Spoiler :

You should try this event with a settler, to make sure settlers are not blocked form disembarking. They shouldn't be.

You should also find that the barbarian unit won't be created on top of an already existing friendly unit, nor will the barbarian unit be created in a Roman city, despite the fact that we didn't exclude those possibilities in the code. The createUnit function, it would appear, won't create units on top of another civ's units.

If you try this event beside an empty city, you will, in fact, find that the createUnit function will create a unit inside another civ's city if no unit is currently present.

You should also find that moving a trireme into the shore and unloading a unit will circumvent this particular event.

Perhaps with more knowledge and creativity, these difficulties can be overcome.




This concludes Lesson Three.
 

Attachments

  • EventsToDebug.zip
    1.8 KB · Views: 180
Last edited:

Attachments

  • ClassicRomeStartOfLesson4.zip
    447.6 KB · Views: 192
Last edited:

Attachments

  • ClassicRomeEventsLesson5Start.zip
    3.5 KB · Views: 135
Last edited:

Attachments

  • GetStartedWithLuaLesson6StartEvents.zip
    5.4 KB · Views: 141
  • Lesson6EndEventCode.zip
    17.2 KB · Views: 231
Last edited:

Attachments

  • ClassicRomeEndOfLesson7.zip
    592.1 KB · Views: 267
Last edited:
Lesson 4 added to the Scenario League Wiki http://sleague.civfanatics.com/inde...a_Events_Lesson_4:_Examples_and_More_Examples

If you think it would be useful to have it actually written out in this thread, please say so.

I'm interested in feedback from this lesson. Was it too long? I have more content that could have fit in this lesson, but I decided to move to lesson 5 in order to release this lesson. Should I expand lesson 4 when I complete that content instead? At the times where I instruct the reader to try to program, are the tasks too challenging (or too easy)? Should there be more of them, or fewer?

I had trouble putting the "answers" into collapsible sections, so I left whitespace. Should I have put the answers on separate pages instead?

Did I make any typos? Were there places where things were unclear?
 
Well, I can follow along and I think this is brilliant and exactly what we need.... But just working with your and others over the past year taught me a ton so my head was never spinning reading this. I'm curious what others think, but I'll bet it is well received.

This is 1000 times better than the non-civ lua tutorials out there. I found it impossible to follow those as I had no point of reference.

Many thanks to you for taking the time to do this!
 
Hi Prof. Garfield,

I reviewed your lesson 4 and I concur with John that it is very well written and accessible. I too was able to follow most of it without too much difficulty and agree that having concrete examples related to Civilization makes learning the language easier.

One of the aspects I struggled understanding in the beginning, as I was adding my own events to Knighttime's Napoleon code, was the importance of the different, for lack of a better word, "sections" of the code, i.e. civ.scen.onCityProduction, civ.scen.onCityTaken, civ.scen.onTurn or civ.scen.onUnitKilled for example, and how it was important to place your bits of code in the appropriate sections if they were to work or trigger properly.

Fortunately, Knighttime was there to guide me, but I especially found it tricky when you had two complimentary triggers, say OnCityTaken and onTurn, related to the same event and within which section should you place your relevant code.

I hope I was making sense but I think explaining this in more detail in your lessons could be beneficial for programming novices like myself.
 
This is 1000 times better than the non-civ lua tutorials out there. I found it impossible to follow those as I had no point of reference.

Thanks. I looked around at one point (not too extensively) for a beginner's guide to programming, with Lua as the language, but didn't find anything I could recommend. Lua has its place, but it doesn't seem popular as an "introductory" language, so stuff that is out there tends to assume that you already have some experience programming, and need to know the specifics of Lua. I think Python is fairly similar to Lua, and is a more popular language to write beginner tutorials for (I think it is just more popular in general). Of course, it is a bit much to expect someone to learn to program in Python in order to understand a lua tutorial just so they can make some events in a 20 year old game

One of the aspects I struggled understanding in the beginning, as I was adding my own events to Knighttime's Napoleon code, was the importance of the different, for lack of a better word, "sections" of the code, i.e. civ.scen.onCityProduction, civ.scen.onCityTaken, civ.scen.onTurn or civ.scen.onUnitKilled for example, and how it was important to place your bits of code in the appropriate sections if they were to work or trigger properly.

Fortunately, Knighttime was there to guide me, but I especially found it tricky when you had two complimentary triggers, say OnCityTaken and onTurn, related to the same event and within which section should you place your relevant code.

I hope I was making sense but I think explaining this in more detail in your lessons could be beneficial for programming novices like myself.

Thanks for the feedback. I didn't realize how significant it was to introduce code that depends on multiple different event triggers, but on a second look I can see how that could be confusing. I'll add some additional explanation. Most events requiring portions of code in different places also need to use the state table in order to save extra data to the game, so we probably won't see more examples for a couple lessons. This event just happened to be one where persistent data was unnecessary, but it's a good thing I included it, so that I can take more care for more complicated functionality.
 
Hi Prof,

I forgot to ask you, in your 4th lesson you indicated: "You can only have 200 local variables in a Lua file (module)."

Knighttime had mentioned this limit to me based on some comments from Grishnach when she was working on Caesar. In his last count, he counted 223 local variables in Napoleon, though I believe I probably added another 10 or so after that, without detecting any adverse effects or events not triggering.

So my question is, how hard is this 200 limit? Is it more a suggestion of best practices? Or can there be unexpected results as soon as you pass that limit?
 
@Prof. Garfield To follow up on that: is the actual limit 200 local variables in scope at any one given time? Based on a few online searches, it's my understanding that 200 is indeed a hard limit, but it's much less clear to me how these are counted. Is this a compile-time limit (when the file is loaded) or a run-time limit (meaning that the count accumulates gradually as it encounters local variable definitions during execution, and would crash at the point it finally hits the 200th variable)? As Tootall said, the events.lua file for Napoleon contains well over 200 instances of "local ___" defining a variable, and it loads just fine, but many of those are scoped to a particular and relatively short function, loop, or block. If the limit is 200 active (in scope) variables at any one point in time, then I have no concerns. But if the count of local variable definition statements increments continually as code executes, and isn't decremented when a local variable moves out of scope, then it seems like someone playing the Napoleon scenario might hit the limit at any point, unpredictably. That hasn't happened yet, though, as far as we know. I realize variable scope might be a more advanced programming topic than you intended to cover in this particular lesson, but it seems like it might be relevant to the limit. Thanks for any details you can provide.
 
Last edited:
Top Bottom