Open Hexagon

Open Hexagon

Not enough ratings
Lua Scripting #1 - How to make basic patterns
By The Sun XIX
DISCLAIMER: Docents of computer science are forbidden to read.
I'm going to give the dumbest possible explanations to tell about the basic coding of this game as simple as possible. I'm not going to teach anyone the nuances of programming or anything good and useful.
And anyway, the only reason why I write another wall of text is to quell my another burst of graphomania.

P.S. actually the explanations aren't as detailed as possible, mostly I just show examples and sometimes tell why they work, but I hope on your ingenuity :p
   
Award
Favorite
Favorited
Unfavorite
What you need to know before you start
Currently I can't and don't want to write a guide from explaining how to turn on a computer, so I can't help people who don't know about level creation at all.

If you have already tried to do something elementary, for example change the style colors or level parameters (speed, rotation and so on), maybe you even created your first levelpack with recolored default levels, you will have a chance to understand something from here.

If you don't, then:
1) install something for code editing (EXCEPT WINDOWS NOTEPAD, ITS NOT GOOD FOR YOUR PSYCHOLOGICAL HEALTH), for example Visual Studio Code;
2) create a copy of any levelpack, open any file from Pack/Styles or Pack/Scripts/Levels, change any numbers here and check in the game what happens after that;
3) continue experimenting until you get a sense of what you are doing. At the very least, read the documentation.[github.com]

Turn on debug mode in the settings and press F4 in the level selection menu to apply any changes in the game.
1. Preparing a Lua file to run the level
For the beginning we need a file for the Lua script. I hope you know how to add files to the Levels/level.json configuration (just change luaFile here).

For maximum ease let the file look like this:
function onInit() l_setSpeedMult(2) l_setSpeedInc(0.25) l_setRotationSpeed(0.2) l_setRotationSpeedInc(0) l_setRotationSpeedMax(99) l_setDelayMult(1) l_setDelayInc(0) l_setSides(6) l_setSidesMin(6) l_setSidesMax(6) l_setIncTime(15) l_setPulseMin(70) l_setPulseMax(70) l_setPulseSpeed(0) l_setPulseSpeedR(0) l_setPulseDelayMax(0) l_setBeatPulseMax(0) l_setBeatPulseDelayMax(0) l_setBeatPulseSpeedMult(0) end function onLoad() end function onStep() end function onIncrement() end function onUnload() end function onUpdate(mFrameTime) end
This thing won't do anything just because it's almost empty (which is what we need just to start the level).
From all of this now we only need onLoad(), which will launch the interesting to us things one time when the level starts.

You can read about other stuff in the documentation, we don't need it now.

Example of how it might look
function onLoad() w_wall(0, 40) t_wait(60) w_wall(0, 40) t_wait(60) w_wall(0, 40) end
This thing will create three walls in the same position at the start of the level, with delays between them (delays will be called two times). After that nothing will happen.
Here in the future we will check our $hitcode that we create.

Let's move on.
2. About variables and functions separately
This is the minimum for creating levels that is used about absolutely always.
Let's start with describing the variables.

In the context of something far from programming and computer science, just consider the variable as a piece of memory in which we store data.

You can store data as numeric or string:
numberVariable = 5 textVariable = "Some text"

In Lua you can put almost everything into a variable just because there is a dynamic typing, however I strongly don't recommend you to use the same variables for storing text and numbers at the same time because the chance that you will lose and break something tends to infinity.

And yes, if you have already put something in a variable, you can still change its value to another:
numberVariable = 5 -- equals 5 numberVariable = 7 -- equals 7 numberVariable = numberVariable + 3 -- equals 7 + 3 = 10

If you've never done any coding, the last line of code might break your brain. Let me explain: any code is executed sequentially from top to bottom and each line is executed only once, so even if you add a variable to itself you won't get an infinite cycle because this operation will be done only once.

Just in case in Lua you use -- to indicate comments. It allow you to write random disinformation nonsense that won't affect how the code works. I.e., all the text in the line that comes after -- won't have any effect.

Variables in our case are needed for two things:
1) performing arithmetical operations with values contained in variables;
2) transferring values to required positions.

The first point kind of makes sense:
x = 5 y = 15 z = x + y -- equals 20

With the second one we will deal later.



Functions... what can we say about functions...
Function is a piece of code that can be used many times from another part of code.

If you have already seen scripts in this game before, you can guess that here functions are almost everything except variables and rare numbers.

They look something like this:
function createSomeWalls() w_wall(0, 40) w_wall(1, 40) w_wall(2, 40) end

The point of it is something like this: we say that we want to create a function named createSomeWalls without parameters () (WE WILL TALK ABOUT PARAMETERS LATER). This is a header of function.
Next we write some code of wall calls and define a function ending with keyword "end" .
What happens between the function header and the end keyword is a list of actions the function performs. This is a body of function.

When we call this function we create three walls in three different positions. WE DON'T NEED TO WRITE ANYWHERE CALLS OF THREE WALLS INSTEAD OF ONE CALL OF THIS FUNCTION.

How we can use it:
function createSomeWalls() w_wall(0, 40) w_wall(1, 40) w_wall(2, 40) end function onLoad() createSomeWalls() t_wait(60) createSomeWalls() t_wait(60) createSomeWalls() end

The result will be the same as here:
function onLoad() w_wall(0, 40) w_wall(1, 40) w_wall(2, 40) t_wait(60) w_wall(0, 40) w_wall(1, 40) w_wall(2, 40) t_wait(60) w_wall(0, 40) w_wall(1, 40) w_wall(2, 40) end

But that's not very good, right?

What we used before: w_wall(0, 40) и t_wait(60), IS ALSO FUNCTIONS! Only we don't create them, they are already exist in the game.

Because of that almost all work in scripts is either usage of existing functions or combining them into other (user) functions to create something useful.

And yes, onLoad() is also a function whose content we define by ourselves. It's called by the game when the level starts and we can't change that, but we can define what it will do.
3. More about functions and their combination with variables
Let's look at the first useful for us game function with parameters:
w_wall(side, thickness)
We have used it previously to create walls. Now let's talk more about meaning of these magic numbers that we put in the parameters. But first, about the parameters themselves.

Function parameter - value transferred from the calling code to the function.
If you want a mathematical analogy, a parabola in the code would looks something like this:
function y(x) y = x ^ 2 return y end
If you don't want the mathematical analogy, I don't have another analogy for you.

For better sense I had to mention the return keyword. It means that the function will return some value at the point of its call. It's like parameters, only vice versa lol.

Example of the usage of this function is as follows:
function y(x) y = x ^ 2 return y end value1 = y(5) value2 = y(15) value3 = y(25)

I.e., we put numbers 5, 15 and 25 into the function y as the values of the parameter x and then we transfer its results of calculations to value1, value2, value3 variables after each call.

This code would be equivalent to this:
value1 = 5 ^ 2 value2 = 15 ^ 2 value3 = 25 ^ 2

However, I remind you that living without functions is not our way.

Let's go back to w_wall(side, thickness)
This function has two parameters:
1) side - determines the position (side) of the wall;
2) thickness - determines the thickness (height?) of the wall.

When we created this thing in the beginning:
function onLoad() w_wall(0, 40) t_wait(60) w_wall(0, 40) t_wait(60) w_wall(0, 40) end
we used values to set the side and thickness parameters.
Also, the "t_wait" function mentioned here creates a delay that equal to its only parameter.

Now we will set parameters with usage of variables, but to make it easier for me to explain let's take another game function:
e_messageAddImportantSilent(message, duration)
It displays some message without sound. Parameters:
1) message - the message itself (text line);
2) duration - the duration of the message display.

We can use it this way:
function onLoad() e_messageAddImportantSilent("IM THE BEST PLAYER EVER", 180) end
that will cause a message when you start the level.

And also we can do this:
function onLoad() message = "IM THE BEST PLAYER EVER" e_messageAddImportantSilent(message, 180) end

And even this:
function getSomeFact() return "IM THE BEST PLAYER EVER" end function onLoad() e_messageAddImportantSilent(getSomeFact(), 180) end

So we can transfer values between functions with variables or even with functions themselves via the return values.
4. Creating a basic pattern (NOOB SKILL LEVEL)
Problem statement: we have an empty level with six sides. We want to make a single pattern with one open side.


What do we know about this pattern? It consists of five walls in a hexagonal level. That's enough information for now.
function onLoad() w_wall(0, 40) w_wall(1, 40) w_wall(2, 40) w_wall(3, 40) w_wall(4, 40) end

And now we have got what we want.
But what do we already know? We should use functions!
function patternWithOneOpenSide() w_wall(0, 40) w_wall(1, 40) w_wall(2, 40) w_wall(3, 40) w_wall(4, 40) end function onLoad() patternWithOneOpenSide() end

It's better now, but it should not work this way. Move the pattern function call from onLoad() to onStep() with some delay that allows the pattern to repeat after some time.
function onLoad() end function onStep() patternWithOneOpenSide() t_wait(60) end

It works, but you might be upset that the patterns always appear in the same position. Let's fix this.
function patternWithOneOpenSide() position = math.random(0, 5) w_wall(position + 0, 40) w_wall(position + 1, 40) w_wall(position + 2, 40) w_wall(position + 3, 40) w_wall(position + 4, 40) end

math.random(param1, param2) - returns a randon value in the interval [param1, param2].

Now we have a pattern that appears in random positions at intervals. BUT WE CAN DO BETTER
5. Creating a basic pattern. Loops (ADVANCED NOOB SKILL LEVEL)
What will you do if you need to create a pattern not for the hexagonal level, but for 3-4-5-7-8 and so on?
Will you create new functions for that? It's unnecessary.

A new thing to learn - loops. For creating patterns in Lua we use FOR - DO - END loops.
It looks like this:
for i = 0, 9, 1 do *do something* end

How it works:
1) the first line consists of three magic numbers at once:
    1a) i = 0 (we create a variable that equals zero, it's a counter);
    1b) 9 (the value for the loop exit condition, as soon as counter i exceeds number 9, the loop will end);
    1c) 1 (the increment, the value by which the counter is incremented after one iteration).
2) the loop body from its header to the end keyword (just like in functions), what will be performed in each iteration.

Most often people don't clearly define the increment and just leave the loop as [for i = 0, 9 do]. It will work too because the default increment is 1.

Let's change our pattern code with a loop:
function patternWithOneOpenSide() position = math.random(0, 5) for i = 0, 4 do w_wall(position + i, 40) end end

At each iteration the wall position will be moved by 1 due to the counter. That's the way it should be. As a result, we got the same 5 walls, but without code repetitions.

What we need to do to get the required number depending on the level shape? Use l_getSides() - it returns the current number of sides in the level.
function patternWithOneOpenSide() position = math.random(0, l_getSides() - 1) for i = 0, l_getSides() - 2 do w_wall(position + i, 40) end end

Why -2?
• because the condition is <=, (i = 0, N do will give N + 1 iteration). Therefore we need -1
• because we need one open side, it's already -2

Why do I initialize the loop from zero (i = 0) and not one (i = 1)?
Because I'm used to counting from zero in coding. Lua is a special language in this case because it uses <= loop condition and arrays are indexed from one, but in most PL counts from zero.
You can write [for i = 1, l_getSides() - 1 do], but it will confuse me.

Does the pattern work? It does. Is it universal for the different sides? Quite. What else do we need? Something more meaningful.
6. Creating a basic pattern (ELEMENTARY SKILL LEVEL)
Since just random patterns are used rarely, let's take a look at creating common patterns with a certain sequence of positions.

As an example, we will make a series of "inverses".


We will take the pattern we created earlier as a basis and rework it a bit for better variable control:
function patternWithOneOpenSide(side, thickness) for i = 0, l_getSides() - 2 do w_wall(side + i, thickness) end end
Changes: the initial position (side) and thickness were moved to the parameters.

It's not very interesting for us by itself, but we can create series based on it:
function inversedPatternSeries(thickness, timesDo, delay) side = math.random(0, l_getSides() - 1) move = l_getSides() / 2 for i = 0, timesDo do patternWithOneOpenSide(side + move * i, thickness) t_wait(delay) end end
The variable "move" will move the pattern during the loop after each iteration by multiplying by the counter i.

If you will use series of patterns, remove the delay from the onStep() function because with one series call you will create several patterns between which you will need to create a delay in the function itself.

Now more about what's going on:
1) we get a random pattern position on any of the sides;
2) the loop starts, first iteration begins: a pattern appears in a random position + delay call;
3) the condition i <= timesDo is checked, if it's true, the next pattern is created, but with another position: half of the sides is added to the previous position (side + move), so we get an "inverse" pattern;
4) patterns contunue to appear and alternate opposite sides until the loop and series ends (+ move * i).

Separately about the added parameters:
1) thickness - thickness, as usual. However, you can create a global variable in the main file, something like thickness = 40 and then use its value in the function itself without parameter;
2) timesDo - how much patterns you want to create per series. Usually defined with math.random(a, b). Remember that the condition is <=, so timesDo = 0 will create one pattern;
3) delay - delay duration, I have no idea how to choose the right delay for each pattern.

With a slight modification you can make a different pattern:
function leftRightPatternSeries(thickness, timesDo, delay) side = math.random(0, l_getSides() - 1) for i = 0, timesDo do move = i % 2 patternWithOneOpenSide(side - move, thickness) t_wait(delay) end end
It's something similar to left-right patterns. % - remainder of division, i % 2 will give +1 on odd counter values and +0 on even, which will allow us to alternate adjacent sides in iterations.


As you can see, the semantic difference is only in the way of changing the pattern position during the series. If you want to create something else, try to change the way of changing "move" variable to get something new.
For example, multiple the "move" variable from leftRight pattern by two and what you get.
Just replace move = i % 2 with move = i % 2 * 2
7. Generating a sequence of different patterns (INTERMEDIATE SKILL LEVEL)
What do we do when we want to create sequences of different kinds of patterns?
First, we need conditions, their syntax is something like this:
if conditional_statement1 then do_smth1 elseif conditional_statement2 then do_smth2 elseif conditional_statement3 then do_smth3 else do_smth_else end
• if then and end - main parts of the condition;
• elseif - (optional) additional condition if the previous one didn't work;
• else - (optional) action if none of the conditions worked.

• code after "then" is executed only if the preceding statement check is true;
• conditional statement creates using the logical operations ==, !=, >, >=, <, <=. For example, a == b - returns true if the values of a and b are the same;
• only the code of the first condition with a "true" statement is executed, after that the rest are ignored.

We need the conditions to create a function for pattern selection, which will call the required pattern function depending on the value of the "key":
function addPattern(key) if key == 1 then inversedPatternSeries(40, math.random(2, 3), l_getDelayMult() * 30) elseif key == 2 then leftRightPatternSeries(40, math.random(3, 4), l_getDelayMult() * 25) end end
l_getDelayMult() - returns the current level delay multiplier.

Now we need a list of pattern keys that we want to use:
keys = { 1, 1, 2, 2, 2 }

It's created as an array (a variable storing several values). The elements are numbered from 1, accessed through index keys[value]. The arrangement of elements in the array looks like this:
keys = { 1, 1, 2, 2, 2 } keys[0] -- null keys[1] -- 1 keys[2] -- 1 keys[3] -- 2 keys[4] -- 1 keys[5] -- 2 keys[6] -- null

We can use the key array directly to call patterns:
keys = { 1, 1, 2, 2, 2 } index = 1 function onStep() addPattern(keys[index]) index = index + 1 if index - 1 == #keys then index = 1 end end

This kind of code is used in the vast majority of levels, including the official ones. I'll explain what's going on here:
1) the pattern is called via addPattern(keys[index]); since index = 1, keys[index] = 1, then addPattern(key) will call inversedPatternSeries(...) via the statement key == 1;
2) the index increased by one;
3) index = 2, keys[index] = 1, inversedPatternSeries(...) is called again via the statement key == 1;
...
N) #keys - the number of elements in the array, in this case #keys = 5. When the index becomes greater than the number of elements in the array (index - 1 == #keys), its value becomes 1 again and the sequence repeats again.

There is a disadvantage: the order of the keys is always the same, the original sequence of patterns will be repeated. For variety, you need to "shuffle" the order of the keys in the array, use the function shuffle(x) for that:
function shuffle(x)
for i = #x, 2, -1 do
local j = u_rndIntUpper(i)
x[i], x[j] = x[j], x[i]
end
end
It works like this: you use an array of keys as a parameter when you call this function, the function changes the arrangement of values in the transferred array. It does this globally, you don't need to use the return value.

The full code for generating pattern sequences will be as follows:
function shuffle(x)
for i = #x, 2, -1 do
local j = u_rndIntUpper(i)
x[i], x[j] = x[j], x[i]
end
end function addPattern(key) if key == 1 then inversedPatternSeries(40, math.random(2, 3), l_getDelayMult() * 30) elseif key == 2 then leftRightPatternSeries(40, math.random(3, 4), l_getDelayMult() * 25) end end keys = { 1, 1, 2, 2, 2 } index = 1 shuffle(keys) function onStep() addPattern(keys[index]) index = index + 1 if index - 1 == #keys then index = 1 shuffle(keys) end end

keys = { 1, 1, 2, 2, 2 } + shuffle(keys) = {2, 2, 1, 2, 1} or something else
Final working code for tests
function patternWithOneOpenSide(side, thickness)
for i = 0, l_getSides() - 2 do
w_wall(side + i, thickness)
end
end

function inversedPatternSeries(thickness, timesDo, delay)
side = math.random(0, l_getSides() - 1)
move = l_getSides() / 2

for i = 0, timesDo do
patternWithOneOpenSide(side + move * i, thickness)
t_wait(delay)
end
end

function leftRightPatternSeries(thickness, timesDo, delay)
side = math.random(0, l_getSides() - 1)

for i = 0, timesDo do
move = i % 2
patternWithOneOpenSide(side - move, thickness)
t_wait(delay)
end
end

function shuffle(x)
for i = #x, 2, -1 do
local j = u_rndIntUpper(i)
x[i], x[j] = x[j], x[i]
end
end

function addPattern(key)
if key == 1 then inversedPatternSeries(40, math.random(2, 3), l_getDelayMult() * 30)
elseif key == 2 then leftRightPatternSeries(40, math.random(3, 4), l_getDelayMult() * 25)
end
end

keys = { 1, 1, 2, 2, 2 }
shuffle(keys)
index = 1

function onInit()
l_setSpeedMult(2)
l_setSpeedInc(0.25)

l_setRotationSpeed(0.2)
l_setRotationSpeedInc(0)
l_setRotationSpeedMax(99)

l_setDelayMult(1)
l_setDelayInc(0)

l_setSides(6)
l_setSidesMin(6)
l_setSidesMax(6)
l_setIncTime(15)

l_setPulseMin(70)
l_setPulseMax(70)
l_setPulseSpeed(0)
l_setPulseSpeedR(0)
l_setPulseDelayMax(0)

l_setBeatPulseMax(0)
l_setBeatPulseDelayMax(0)
l_setBeatPulseSpeedMult(0)
end

function onLoad()
end

function onStep()
addPattern(keys[index])
index = index + 1

if index - 1 == #keys then
index = 1
shuffle(keys)
end
end

function onIncrement()
end

function onUnload()
end

function onUpdate(mFrameTime)
end

This is an approximate result of what you can do. It's not very good, you could do a lot of improvements for patterns and add some delay calculations with some formulas, but it's a good start.
Conclusion
That's enough for now.
Maybe I'll add something more later, I don't know.
Have a nice day.

To be (not) continued...
I realize that I wrote this myself as an example simply because that's how almost everyone does it (and it's a little easier to explain that way), but you have to wonder: why?
Why do it this way?

Wrong:
createSomePattern() t_wait(someDelay)

Right:
t_wait(someDelay) createSomePattern()

Prove me wrong :)
To be (not) continued part 2...
That's a bad text quality :(
Explain to me please, what is the point of this?

Calculation of delay based on pattern thickness divided by speed?
So what does that mean? That a higher speed will change the delay even if you don't change the delay parameter itself? That doubling the thickness of the pattern will increase the space between the patterns, which should usually be several times larger than their thickness?

You can throw rocks at me for my levels all you want, but I will never agree to this delay function.