Skip to content

Level_01

muffintrap ^ FCCCF edited this page Jun 26, 2022 · 16 revisions

Level 1

Variables

Syntax of variable declaration is variable_name = value or variable_name = other_variable Value can be a number, boolean, string of characters or Object. If other variable is given as a value, the value of that variable is copied.

Variables can be declared anywhere. Preceding the variable name with keyword local creates a local variable that is only accessible until the next end. Variables that are not local, are global variables and are accessible everywhere. It is a good practice to use local variables whenever possible.

The example code declares different variables and writes their values to the TIC-80 command line by using the trace() function. This function is useful for testing your code and seeing the results of operations. At the very end there is call to exit() which will change from graphical mode back to the command line. Since we are not drawing any graphics yet you need to remember to place this in the end or press ESC to see the output of trace().

Lines starting with -- are comments and are ignored when the code is run.

-- Numbers
integer=20
negative=-5
fraction=1.99

function TIC()
	trace(integer)
	-- Operator .. concatenates values to text
	trace("Value of negative is: " .. negative)
	-- Numbers support the basic math operations + - * and /
	trace(2+2)
	trace(1-3)
	trace(5*2)
	trace(10/3)
	local product=integer*negative
	trace("Product is " .. product)
	-- For integer division use //
	trace(10//3)
	-- For getting the remainder of integer division, use % aka modulo
	trace(10%3)
	exit()
end

Exercise

Write your own program that translates days to seconds and seconds to days. Use variables to store the multipliers and dividers and present the calculation using variables. Don't account for leap yeas :)

function TIC()
	local input_days=1
	local input_seconds=5000
	local out_seconds = 0
	local out_days = 0

	-- Your code here

	trace("Days to seconds: " .. input_days .. " days is " .. out_seconds .. " seconds")
	trace("Seconds to days: " .. input_seconds .. " seconds is " .. out_days .. " days")
	exit()
end

function TIC()

The previous examples have had function TIC() in them, but what is it? This is a special function that your code must always have. It is what the TIC-80 calls or executes each frame. That means that whatever code is inside the TIC() function happens 60 times every second. Usually the first thing to do in TIC() is to call cls() to clear the screen before drawing anything else. The code inside the function TIC() is any line that is between lines function TIC() and end.

On the contrary, whatever code is outside function TIC() happens only once, before the function TIC() is called for the first time.

If in the future exercises you find that either nothing happens or too much happens, it is usually because of mistake made regarding this function. Here is a short troubleshooting list for that kind of situation:

  • Did you call cls() before drawing anything?
  • Did you place the function call outside or inside function TIC()?
  • Did you assign to a variable outside or inside function TIC()?
  • Did the things you draw move, and if they move, did they instantly fly outside the screen area?

The example code below demonstrates what it means when things happen 60 times per second. It prints the amount of frames and moves a rectangle one pixel every frame. The other rectangle changes colour every frame.

frame=0
function TIC()
	cls(13)
	local x=frame%240
	local c=frame%16
	print(frame, 1,1,4)
	rect(x,20,10,10,5)
	rect(0,30,10,10,c)
	frame=frame+1
end

Exercises

  • What happens if the line frame=0 is inside function TIC()?
  • What happens if you move the print() line or either of rect() lines outside function TIC()?
  • What happens if you remove the line cls(13)?

Calling functions

TIC-80 comes with a selection of functions that can be used to draw things, read input, measure time etc. Functions require zero or more parameters to specify what the function should do. Some parameters are optional, meaning that if they are not provided, a default is used. Documentation of the functions is found in the TIC-80 website. The documentation gives the function name and the list of parameters. Optional parameters and their default values are surrounded with brackets. For example the documentation for screen clearing function cls [color=0] means that the cls() function takes one optional parameter. If the parameter is not given, it gets the default value 0.

This code first clears the screen to dark blue and then draws different shapes over it. To see what color is what, press F2 to change to sprite editor and mouse over the colors in the palette. The screen size of TIC-80 is 240 by 136 pixels.

The example code calls different drawing functions. Change the numbers around and read the documentation for each one to become familiar with them.

function TIC()
	cls(9)
	rect(70,30,60,40,4)
	elli(180,30,16,40,2)
	circ(100,100,10,6)
	tri(4,130, 80,80, 20,40, 11)
end

Exercise

Using the drawing functions, draw your favourite flag. See the gallery at Wikipedia for inspiration

Random numbers

All of the random functions are perfectly predictable in a sense that they will do the same thing every time. presuming the parameters given are the same every time. No matter how many times you call circ(100,00,10,6), the circle will be drawn in the same position and will be of same size. In all other ways too, the computer is predictable and that is great. But for games it is important to have the element of chance and randomness. Without a way to randomize things, the only kind of games possible would be deterministic games, such as chess. Dice games and card games, would be impossible (or someone would have to write very long lists of numbers (and someone else would try to memorize those numbers to get an advantage)).

Luckily, Lua offers a way to get a random number (or at least, a random enough). Calling a function math.random() will return a fractional number between 0 and almost 1, meaning that the largest number possible is as close to one as possible. But calling it with a single argument returns an integer between 1 and that number. Calling with two arguments works similarly, but the returned number is between the given ones.

Here is a summary:

Call Smallest possible Largest Possible Examples
math.random( ) 0.0 0.9999999... 0.14, 0.562, 0.94
math.random(N) 1 N (7)-> 1, 5, 7, 3
math.random(B,N B N (3,12)-> 5, 3, 10, 7, 12

Uses of random numbers in games are many but here are some useful ones:

  • Randomizing the starting state, positions, resources, map etc.
  • Randomizing the outcome of action, dice roll, the next position to appear in, size of explosion etc.
  • Randomizing between similar things: multiple sounds or effects for the same thing.
  • Making decisions for the AI.

To test the randomness righ now, you can leave out the call to cls() inside the TIC() so that the things drawn the previous frame are not erased. And then on each run of TIC() draw a new rectangle to random position. This also demonstrates how often the TIC() is called.

SCR_W=240
SCR_H=136

cls(0)
function TIC()
	local w=math.random(2,SCR_W/2)
	local h=math.random(2,SCR_H/2)	
	local x=math.random(0,SCR_W-w)
	local y=math.random(0,SCR_H-h)
	local color=math.random(1,15)
	rect(x,y,w,h,color)
end

Exercises

  • Use a different drawing function to fill the screen randomly
  • Use rect() function to randomize a new rect that fits to the previous one so that the screen is filled with a kind of twisting corridor.

Creating custom functions

One of the main purpose of functions is to enable reusing code and building abstractions. The functions we used in drawing the basic shapes are also reusing code. All of them put pixels on the screen, but do it in a different way. It would be difficult and error prone to put individual pixels on the screen with the [pix()(https://github.com/nesbox/TIC-80/wiki/pix) function every time we wanted to draw a circle. Having the circ() function enables us to draw circles in any place, in any color and size. In turn, we can use the drawing functions provided by TIC-80 to compose our own functions for drawing more complex shapes as easily.

The following code creates two custom functions which draw the symbols Diamond and Heart that are used in playing cards. The functions work similarly to the functions provided by TIC-80 in that they also take x, y, size and color as parameters. Note that the functions are placed outside the function TIC(). This way they are on the same level or in the same scope and we can be sure that we can call them inside the TIC() function.

function diamond(x,y,size,color)
	local left=x
	local center=x+size/2
	local right=x+size

	local top=y
	local middle=y+size/2
	local bottom=y+size

	tri(left,middle, center,top, right,middle, color)
	tri(left,middle, center,bottom, right,middle, color)
end

function heart(x,y,size,color)
	local left=x
	local center=x+size/2
	local right=x+size
	
	local top=y
	local bottom=y+size
	local radius=size/4
	--left circle center x and y
	local lccx=left+radius
	local lccy=top+radius
	-- same for right
	local rccx=right-radius
	local rccy=top+radius

	circ(lccx,lccy,radius,color)
	circ(rccx,rccy,radius,color)
	tri(left,lccy, right,rccy, center,bottom, color)
end

function TIC()
	cls(12)
	diamond(40,40,10,2)
	diamond(80,40,12,6)
	diamond(120,40,22,8)
 
	heart(40,100,10,2)
	heart(80,100,12,6)
	heart(120,100,22,8)
end

Exercise

Create the functions for drawing the Club and Spade. If you want to make them really fancy, use the background color to carve out from other shapes. You can also try to create the symbols for Bamboos and Circles from Mahjong.

Conditions and IF

The keyword if allows to choose what code to execute based on a condition. A condition is expressed as a truth value, also called a Boolean. A boolean value is either true or false. Boolean values are created by comparison operators. If the value is false, the code between if and end is not executed.

The example declares 4 variables and demonstrates the if and comparison operators. The last parameter to trace() is the color of the output.

function TIC()
	local a=3
	local b=5
	local c=3
	local d=4
	trace("a: "..a, 6)
	trace("b: "..b, 6)
	trace("c: "..c, 6)
	trace("d: "..d, 6)
	
	-- boolean variables
	local a_to_b=a<b
	trace("a < b: ", 13)
	trace(a_to_b, 13)
	
	local b_to_a=b<a
	trace("b < a: ", 13)
	trace(b_to_a, 13)
	
	if a<b then
		trace("a is less than b", 5)
	end
	if a>b then
		trace("a is greater than b", 5)
	end
	if b>a then
		trace("b is greater than a", 5)
	end
	if a~=b then
		trace("a is not equal to b", 3)
	end
	if a==c then
		trace("a is equal to c", 3)
	end
	if a>=c then
		trace("a is equal or greater than c", 10)
	end
	if a<=d then
		trace("a is equal or less than d", 10)
	end
	
	exit()
end

Exercise

Write a program that tells whether a given temperature is warm or cold. You can decide the limits yourself. For example with temperature value of -16 the program could print "Temperature of -16 feels Cold".

cold="Cold"
neutral="Neutral"
warm="Warm"

function TIC()
	local temp=20
	local result=""
	-- your code here assigns one of the temperature text variables to result based on comparisons
	trace("Temperature of: " .. temp .. " feels: " .. result)
	exit()
end

Boolean logic and operators

If condition is dependent on multiple comparisons, the comparisons can be combined together using boolean operators. There operators take 1 or 2 boolean values and return one. The operators are and, or, not. The rules are:

Logical operations
a operator b result
true and true true
true and false false
false and false false
true or true true
true or false true
false or false false
- not false true
- not true false

Use the logical operators between if and else to combine multiple comparisons. You can chain together more than 2 comparisons and use parenthesis too.

function TIC()
	local cold=-10
	local nice=0
	local warm=10
	local temperature=8
	if temperature > cold and temperature < nice then
		trace("Not cold enough")
	end
	if temperature < warm and temperature > nice then
		trace("Not warm enough")
	end
	if temperature < cold or temperature > warm then
		trace("Temperature outside limits")
	end
	if temperature==nice or temperature==cold or temperature==warm then
		trace("Exact temperature")
	end
	exit()
end

In games, the boolean operators are often used to calculate state of the game objects. There is a hierarchy of logic usually in the form of:

  1. Collision tests and comparing numbers to other numbers and limits
  2. Setting truth values based on those comparisons
  3. Combining truth values using boolean operators
  4. Setting or changing the values of variables based on the result of combining truth values
  5. Repeat.

An example of this could be a situation where the code calculates if the player character is getting wet. In the game the character can be:

  • Inside or outside
  • Weather can be sunny or rainy
  • The character can have an umbrella. From these variables we can find out if character is getting wet or not. If they are outside check if it is rainy. If it is rainy, check if they have the umbrella. If they don't have the umbrella, they are getting wet. Otherwise, or else they are not getting wet. You can use the keyword else to denote what happens if the opposite of the given condition is true.
function TIC()
	local getting_wet=false

	local outside=true
	local rainy=true
	local umbrella=false

	if outside then
		if rainy then
			if not umbrella then
				getting_wet=true
			else
				getting_wet=false
			end
		end
	end
	
	if getting_wet then
		trace("Player is getting wet")
	else
		trace("Player is safe and dry")
	end
	exit()
end

Writing tests like this takes a lot of space and it is easy to make a mistake. This is where boolean operators are useful. The previous check can be compressed down to:

local getting_wet = outside and rainy and not umbrella

This way it is easy to see in what condition the player is getting wet. If any of the three conditions is not true, then player is dry. Note that the not operator is applied before the and operators.

Exercise

Add one more variable describing what the player is doing inside. They can be on the sofa or in the shower. Now it is possible to be getting wet in the inside as well. Add that variable to the example check and try to compress everything down to one or two lines of boolean logic. Decide for yourself if having an umbrella in the shower is allowed ☔

How could you use boolean logic to notice impossible situations? What should happen in those cases?

Branching

If is very useful combined with functions. Continuing from earlier card example, you can use if inside a custom function to draw different playing cards with just one one function card(), that takes position, card number and symbol as parameters. Since playing cards differ only on what symbol they have, it is handy to have one function to draw any kind of card.

To draw text and numbers on the screen, use the print() function, which is similar to trace(), but also needs to know the position of the text.

The example code creates a card function that draws a white background with a black border, and then the symbol and number in the middle. The heart and diamond are drawn in red, club and spade in black. Note the new keyword elseif that combines else and if. It is useful in situations like this where only one of the symbols should be drawn.

-- constants for symbols
HEART=0
DIAMOND=1
CLUB=2
SPADE=3

function card(x,y,number,symbol)
	local width=20
	local height=32
	-- White card with black border
	rect(x,y,width,height,12)
	rectb(x,y,width,height,0)

	-- Symbol and number vertically in the middle. Calculate the upper left position for them
	local size=8
	local center=x+width/2-size/2
	local top=y+size
	-- Color is black unless symbol is heart or diamond
	local color=0
	if symbol==HEART or symbol==DIAMOND then
		color=2
	end
	if symbol==HEART then
		heart(center,top,size,color)
	elseif symbol==DIAMOND then
		diamond(center,top,size,color)
	elseif symbol==CLUB then
		club(center,top,size,color)
	elseif symbol==SPADE then
		spade(center,top,size,color)
	end

	-- Number goes under symbol. 
	local bottom=y+height-size
	print(number,center,bottom,color)
end

Exercises

  • Call the card() function in TIC() to draw cards on the screen.
  • Draw your favourite poker hand.
  • What happens when card number 10 is drawn? How to center the number?
  • Change the function so that number 1 is shown as A and numbers 11 to 13 are J, Q and K.

Sprites

Instead of drawing complex images with a combination of drawing functions, you can use sprites. Press F2 to open the sprite editor. The default cartridge has two robot sprites. A sprite is a 8x8 pixels square, but multiple sprites can be drawn together as composite sprites. To draw sprites use the spr() function.

Note that when drawing sprites, all the pixels in the 8x8 rectangle are drawn. When drawing the symbols either draw the sprites on a white background or you can select one color of the 16 color palette to be treated as transparent when drawing a sprite.

This code draws the default robot sprite, first just one part and then the whole robot as a composite sprite. The size of the composite sprite are the last two parameters to the spr() function.

function TIC()
	cls(13)
	-- Single sprite
	spr(1,40,40)
	-- 2x2 Composite sprite
	local w=2
	local h=2
	spr(1,40,60,-1,1,0,0,w,h)
end

Exercises

  • Draw the card symbols in the sprite editor and change the symbol constants to match the indices. Change your card() function so that instead of having an if-elseif-end chain with the shape functions, just call the spr() with the given sprite index.
  • Create a function to draw composite sprites more easily. The function signature should be function cmp_spr(sprite_index, x, y, width, height)

Simple button input

This far the programs have not been interactive: they do the same thing every time. Now we are adding input listening so that player can press some buttons. The TIC-80 emulates 8 button game pads with directional buttons and A, B, X, Y. In this example we are using the directional buttons of player one to change the number and symbol on the card.

The basic idea is that reading the input changes variables and those variables are then used in drawing the screen. Here the variables are the card number and symbol. In the example code, pressing up and down changes the number on the card.

Here also it is useful to employ '''elseif''' instead of having multiple if sections for each buttons. This is because we want to allow player to only press one button at a time. Pressing up and down simultaneously would be meaningless in this situation.

The function btnp() returns true when the button given as a parameter is released. The btn() version returns true if the button is down at the moment and is useful in movement.

To keep track of the input, you need to call this function every frame. You can place the function call directly between if and then, it is equivalent to having

local up_pressed = btnp(UP)
if up_pressed then
	number=number+1
end

Add the code below to your program that has the card() function.

-- Global variables changed by input
number=1
symbol=HEART

-- Constants for buttons
UP=0
DOWN=1
LEFT=2
RIGHT=3

-- inside function TIC()
	if btnp(UP) then
		number=number+1
	elseif btnp(DOWN) then
		number=number-1
	end
	
	cls(7)
	card(10,10,number,symbol)
end

Exercises

  • Use if checks to prevent player from choosing numbers other than 1 to 13
  • Listen for the left and right buttons to change the symbols. NOTE! Attempting to draw sprite index -1 will crash the TIC-80
  • Similarly prevent selecting symbols that are not available. Alternatively you can make both number and symbol selection loop around so that pressing up on number 13 goes back to 1 and vice versa.

Simple mouse input

TIC-80 also emulates a mouse. The mouse() function is different from functions we have used before, since it returns multiple variables. In the example code a simple cross cursor is drawn on the mouse position and the color changes when the left mouse button is down.

function TIC()
	local x,y,left,middle,right,scrollx,scrolly=mouse()

	cls()
	local color=12
	if left then
		color=2
	end
	line(x-4,y,x+4,y,color)
	line(x,y-4,x,y+4,color)
end

Exercises

  • Draw a nicer mouse cursor by using the drawing functions or by using a sprite. Make the cursor change when user is holding down the left mouse button.
  • Stop the mouse cursor from leaving the screen area.

Simple collision detection

A simple collision test is to test if a point is inside a rectangle. The example code shows a program that gets the mouse position and sets variables for a rectangle. If the mouse position is inside the rectangle, the rectangle color changes before the rectangle is drawn.

function TIC()
	cls()
	--Rectangle
	local rx=10
	local ry=20
	local rw=60
	local rh=40
	local rc=3
	--Mouse variables
	local x,y,left,middle,right,scrollx,scrolly=mouse()

 	--Check if cursor is inside rectangle
 	local inside=false
 	if x>rx and x<rx+rw and
 	y>ry and y<ry+rh then
		inside=true
	end
	
	if inside then
		rc=5
	end
	
	rectb(rx,ry,rw,rh,rc)
	
	line(x-4,y,x+4,y,12)
	line(x,y-4,x,y+4,12)
end

Exercises

  • Check what happens when the cursor is just on the border of the rectangle. Does it work correctly? How to fix it?
  • Make a check to see if the cursor is outside the rectangle.
  • Make a check to see if the cursor is exactly on the border of the rectangle.

Simple button states

Thus far, the programs have had very little state. Meaning that there are only one or two states the program can be in. In the previous example the rectangle was different depending on the state of the mouse and also the card was drawn differently depending on the variables.

In this example we look at different kinds of buttons and the state information needed to implement them. The example code demonstrates:

  • A buzzer button. This kind of button is either pressed down or released. When pressed down, it makes a sound. Like a door buzzer.
  • A toggle button. Every time a toggle is clicked it changes state. A computer's power button is a toggle button. It turns the computer on and off. Also a ball point pen works like a toggle.
  • A switch button. This kind of button has two positions, on and off. A light switch is a switch button. When the button is in ON position, setting it to ON again does nothing.

The thing we are interested in is when the something changes. To detect any kind of change, we need to know how the thing was before, how it is now and compare the two. For example if a cheeseburger used to cost 2 euro, but now it costs 5, it is clear that the price has changed. By comparing the old and new information, we can deduct how the information has changed.

Here is a table showing all the possible states of the toggle button and the conclusion.

Old state New state Change?
Up Up No change
Up Down Button was pressed!
Down Down No change
Down Up Button was released!

From this we can see that when Old state differs from the new state, a change has taken place. And then depending on the states, we can know whethet the button was pressed down or released. In the case of toggle button, the only change we are interested in is when button is released.

Study how each of the buttons work. Notice how the computer goes on and off only after you release the button.

buzzer_down=false
toggle_prev=false
toggle_on=false
switch_on=false

UP=0
DOWN=1
LEFT=2
RIGHT=3

function onoff(state)
	if state then 
		return "ON"
	else
		return "OFF"
	end
end

function TIC()
	buzzer_down=btn(UP)
	
	-- Read current state
	local toggle_now = btn(DOWN)
	-- Check that and the previous state
	if toggle_now==false and toggle_prev==true then
		-- Set ON or OFF
		if toggle_on==true then
			toggle_on=false
		else
			toggle_on=true
		end
	end
	-- Replace old state with new state
	toggle_prev=toggle_now
	
	if btn(LEFT) and switch_on==true then
		switch_on=false
	end
	if btn(RIGHT) and switch_on==false then
		switch_on=true
	end


	cls(13)
	if buzzer_down then
		print("BZZZZ!",10,10)
	end
	print("Computer is "..onoff(toggle_on),10,20)
	print("Light is " .. onoff(switch_on),10,30)
end

Exercises

  • There is no ready made function to know when user has clicked a mouse button. Add a new toggle button that reacts to left button on the mouse.
  • Implement a three-way switch similar to automatic transmission selector in cars. It has 3 positions: Reverse, Park and Drive
  • Implement a toggle that can only be operated while a secondary button is held down. Think of a real world example.
  • Are there other kinds of buttons in the real world? How would you implement them?

Mouse input functions

The mouse works quite differently from buttons. It is a good idea to create custom functions that work similarly so that it is easy to use the mouse to control the game.

The functions would be:

function mbtn(button)
function mbtnp(button)

Where the button is either left, middle or right. And they would work similarly to the btn() and btpn() functions that TIC-80 provides. To facilitate this, we need to read the mouse input at the start of each frame and keep track of the state of all the buttons.

The example code shows a short program that uses these functions and other state of the mouse to register mouse buttons and draw a selection area. All the code is not given, but left as an exercise.

-- Names for mouse buttons
MOUSE_LEFT=0
MOUSE_MIDDLE=1
MOUSE_RIGHT=2

-- Mouse state
-- Store what buttons are down
mouse_left_down=false
mouse_middle_down=false
mouse_right_down=false
-- Store if button was clicked
mouse_left_clicked=false
mouse_right_clicked=false

--[[ 
	Read the mouse state
	and check if any of the buttons
	was clicked and store it
	Then update state
]]--
function update_mouse()
	mouse_x,mouse_y,mouse_left,mouse_middle,mouse_right,mouse_scroll_x,mouse_scroll_y=mouse()
	--[[ 
	 If mouse button is no longer down,
	 but it WAS down last time, then it
	must have been released:
	record a click
	]]-- 
	if mouse_left==false -- no longer down
	and mouse_left_down then -- was down
		mouse_left_clicked=true
	else
		mouse_left_clicked=false
	end
	-- and so on for middle and right buttons
	-- Store current state
	mouse_left_down=mouse_left
end

-- Return true if given button is down
function mbtn(button)
	if button==MOUSE_LEFT then
		return mouse_left_donw	
	end
	-- add code for middle and right
end

-- Return true if given button was
-- clicked
function mbtnp(button)
	if button==MOUSE_LEFT then
		return mouse_left_clicked
	end
		-- add checks for middle and right
end

mouse_clicks=0
kb_clicks=0

function TIC()
	--[[
	Call read_mouse at the start of
	frame so that the other functions
	work correctly
	--]]
	update_mouse()
	cls()	
	if btnp(0) then
		kb_clicks=kb_clicks+1
	end
	print("UP button clicked "..kb_clicks.." times!",10,10,5)
	
	if mbtnp(MOUSE_LEFT) then
		mouse_clicks=mouse_clicks+1
	end
	print("Mouse left clicked "..mouse_clicks.." times!",10,20,4)
end

Exercise

Develop the code further so that you can also use the middle and right buttons of the mouse.