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.
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.
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
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:
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:
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
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.
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
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
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)
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
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
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)
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.
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.
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.