🏠 Home page > 🌖 LÖVE tutorials
A tutorial for Lua and LÖVE 11
There is a board with 15 pieces and an empty space. Move the pieces around until they are in sequential order by using the arrow keys to move pieces into the empty space.
Arrow keys | Move piece |
The pieces are stored as a grid of numbers.
The number 16 represents the empty space.
The other numbers are swapped with the empty space when an arrow key is pressed.
At the start of the game, the grid is initially in sorted order, and random moves are made to shuffle it. (If the piece positions were totally random instead, it could result in an unsolvable board.)
After a piece has been moved, the pieces are looped through, and if they all have their initial sorted values, then the game is over.
The pieces are drawn as squares.
For now, a piece is drawn where the empty space should be.
function love.draw() for y = 1, 4 do for x = 1, 4 do local pieceSize = 100 local pieceDrawSize = pieceSize - 1 love.graphics.setColor(.4, .1, .6) love.graphics.rectangle( 'fill', (x - 1) * pieceSize, (y - 1) * pieceSize, pieceDrawSize, pieceDrawSize ) end end end
The piece numbers are drawn on top of the pieces.
A piece number is calculated by adding the X position (i.e. column number) to the Y position minus one (i.e. one less than the row number) multiplied by the number of pieces in a row.
For example, on the first row, the Y position minus one is 0, so nothing is added to each X position, so the first number on the first row is 1. On the second row, 4 is added to each X position, so the first number on the second row is 5.
function love.draw() for y = 1, 4 do for x = 1, 4 do local pieceSize = 100 local pieceDrawSize = pieceSize - 1 love.graphics.setColor(.4, .1, .6) love.graphics.rectangle( 'fill', (x - 1) * pieceSize, (y - 1) * pieceSize, pieceDrawSize, pieceDrawSize ) love.graphics.setColor(1, 1, 1) love.graphics.print( ((y - 1) * 4) + x, (x - 1) * pieceSize, (y - 1) * pieceSize ) end end end
The font is set to size 30.
function love.load() love.graphics.setNewFont(30) end
A grid is created with each piece's number stored at its position on the grid, and this number is drawn.
The number of pieces on the X and Y axes are reused from drawing the pieces, so they are made into variables.
function love.load() -- etc. gridXCount = 4 gridYCount = 4 grid = {} for y = 1, gridYCount do grid[y] = {} for x = 1, gridXCount do grid[y][x] = ((y - 1) * gridXCount) + x end end end function love.draw() for y = 1, gridYCount do for x = 1, gridXCount do -- etc. love.graphics.print( grid[y][x], (x - 1) * pieceSize, (y - 1) * pieceSize ) end end end
The number of pieces on each axis multiplied together gives the total number of pieces (i.e. 4 times 4 means 16 pieces), and a piece is drawn only if it isn't this number.
function love.draw() for y = 1, gridYCount do for x = 1, gridXCount do if grid[y][x] ~= gridXCount * gridYCount then local pieceSize = 100 local pieceDrawSize = pieceSize - 1 love.graphics.setColor(.4, .1, .6) love.graphics.rectangle( 'fill', (x - 1) * pieceSize, (y - 1) * pieceSize, pieceDrawSize, pieceDrawSize ) love.graphics.setColor(1, 1, 1) love.graphics.print( grid[y][x], (x - 1) * pieceSize, (y - 1) * pieceSize ) end end end end
The first step in moving a piece is finding the position of the empty space.
When a key is pressed, the grid is looped through, and if a piece is equal to the number of pieces on each axis multiplied together (i.e. it's the empty space), then, for now, its position is printed.
function love.keypressed(key) local emptyX local emptyY for y = 1, gridYCount do for x = 1, gridXCount do if grid[y][x] == gridXCount * gridYCount then emptyX = x emptyY = y end end end -- Temporary print('empty x: '..emptyX..', empty y: '..emptyY) end
empty x: 4, empty y: 4
For now, when a is key is pressed, the empty space is swapped with the piece above it (i.e. minus 1 on the Y axis).
Currently this will error when a key is pressed and the empty space is at the top of the grid.
function love.keypressed(key) -- etc. grid[emptyY - 1][emptyX], grid[emptyY][emptyX] = grid[emptyY][emptyX], grid[emptyY - 1][emptyX] end
If grid[emptyY - 1] is nil (i.e. emptyY is 1 so emptyY - 1 is 0), then grid[empty - 1][emptyX] will cause an error because the [emptyX] part is trying to index nil.
To prevent this, moving is only possible if grid[emptyY - 1] is a non-nil value.
function love.keypressed(key) -- etc. if grid[emptyY - 1] then grid[emptyY - 1][emptyX], grid[emptyY][emptyX] = grid[emptyY][emptyX], grid[emptyY - 1][emptyX] end end
The Y position of the piece that the empty space swaps with is made into a variable. When the up key is pressed, it is set to the position below the empty space (i.e. plus 1 on the Y axis).
function love.keypressed(key) -- etc. local newEmptyY = emptyY if key == 'down' then newEmptyY = emptyY - 1 elseif key == 'up' then newEmptyY = emptyY + 1 end if grid[newEmptyY] then grid[newEmptyY][emptyX], grid[emptyY][emptyX] = grid[emptyY][emptyX], grid[newEmptyY][emptyX] end end
The X position of the piece that the empty space swaps with is made into a variable, and it is changed when the left or right arrow is pressed.
Currently this will error when the left/right key is pressed and the empty space is already at the rightmost/leftmost position.
function love.keypressed(key) -- etc. local newEmptyY = emptyY local newEmptyX = emptyX if key == 'down' then newEmptyY = emptyY - 1 elseif key == 'up' then newEmptyY = emptyY + 1 elseif key == 'right' then newEmptyX = emptyX - 1 elseif key == 'left' then newEmptyX = emptyX + 1 end if grid[newEmptyY] then grid[newEmptyY][newEmptyX], grid[emptyY][emptyX] = grid[emptyY][emptyX], grid[newEmptyY][newEmptyX] end end
If grid[newEmptyY][newEmptyX] is nil (for example when newEmptyX is 0), then grid[emptyY][emptyX] will be assigned nil, and love.graphics.print will error when it is given nil instead of a number.
To prevent this, moving is only possible if grid[newEmptyY][newEmptyX] is a non-nil value.
function love.keypressed(key) -- etc. if grid[newEmptyY] and grid[newEmptyY][newEmptyX] then grid[newEmptyY][newEmptyX], grid[emptyY][emptyX] = grid[emptyY][emptyX], grid[newEmptyY][newEmptyX] end end
At the beginning of the game, a number of random moves are made to shuffle the board.
A random number between 1 and 4 is generated and a move is made in one of the four movement directions based on this number.
function love.load() -- etc. for moveNumber = 1, 1000 do local emptyX local emptyY for y = 1, gridYCount do for x = 1, gridXCount do if grid[y][x] == gridXCount * gridYCount then emptyX = x emptyY = y end end end local newEmptyY = emptyY local newEmptyX = emptyX local roll = love.math.random(4) if roll == 1 then newEmptyY = emptyY - 1 elseif roll == 2 then newEmptyY = emptyY + 1 elseif roll == 3 then newEmptyX = emptyX - 1 elseif roll == 4 then newEmptyX = emptyX + 1 end if grid[newEmptyY] and grid[newEmptyY][newEmptyX] then grid[newEmptyY][newEmptyX], grid[emptyY][emptyX] = grid[emptyY][emptyX], grid[newEmptyY][newEmptyX] end end end
The only difference between the shuffling code and the keyboard controlled code is how the direction of the move is determined, so a function is made with the direction as a parameter.
function love.load() -- etc. function move(direction) local emptyX local emptyY for y = 1, gridYCount do for x = 1, gridXCount do if grid[y][x] == gridXCount * gridYCount then emptyX = x emptyY = y end end end local newEmptyY = emptyY local newEmptyX = emptyX if direction == 'down' then newEmptyY = emptyY - 1 elseif direction == 'up' then newEmptyY = emptyY + 1 elseif direction == 'right' then newEmptyX = emptyX - 1 elseif direction == 'left' then newEmptyX = emptyX + 1 end if grid[newEmptyY] and grid[newEmptyY][newEmptyX] then grid[newEmptyY][newEmptyX], grid[emptyY][emptyX] = grid[emptyY][emptyX], grid[newEmptyY][newEmptyX] end end for moveNumber = 1, 1000 do local roll = love.math.random(4) if roll == 1 then move('down') elseif roll == 2 then move('up') elseif roll == 3 then move('right') elseif roll == 4 then move('left') end end end function love.keypressed(key) if key == 'down' then move('down') elseif key == 'up' then move('up') elseif key == 'right' then move('right') elseif key == 'left' then move('left') end end
So that the empty space always starts in the bottom-right corner, the pieces are moved left and up repeatedly. The number of pieces on an axis minus 1 is the maximum number of moves it would take to move the space from one side to the other.
function love.load() -- etc. for moveNumber = 1, gridXCount - 1 do move('left') end for moveNumber = 1, gridYCount - 1 do move('up') end end
After a move is made, the pieces are looped through, and if none of the pieces are not equal to the number they were initially given (i.e. they are all in their sorted positions), then the game is complete.
For now, love.load is called to start a new game.
function love.keypressed(key) -- etc. local complete = true for y = 1, gridYCount do for x = 1, gridXCount do if grid[y][x] ~= ((y - 1) * gridXCount) + 1 then complete = false end end end if complete then love.load() end end
The code for calculating the initial value of a piece is reused, so it is made into a function.
function love.load() -- etc. function getInitialValue(x, y) return ((y - 1) * gridXCount) + x end grid = {} for y = 1, gridYCount do grid[y] = {} for x = 1, gridXCount do grid[y][x] = getInitialValue(x, y) end end -- etc. function love.keypressed(key) -- etc. for y = 1, gridYCount do for x = 1, gridXCount do if grid[y][x] ~= getInitialValue(x, y) then complete = false end end end -- etc. end
If the pieces are still in their initial order after shuffling, the shuffling process happens again.
The code for checking if the pieces are in their initial order is reused, so it is made into a function.
function love.load() -- etc. function isComplete() for y = 1, gridYCount do for x = 1, gridXCount do if grid[y][x] ~= getInitialValue(x, y) then return false end end end return true end repeat for moveNumber = 1, 1000 do local roll = love.math.random(4) if roll == 1 then move('down') elseif roll == 2 then move('up') elseif roll == 3 then move('right') elseif roll == 4 then move('left') end end for moveNumber = 1, gridXCount - 1 do move('left') end for moveNumber = 1, gridYCount - 1 do move('up') end until not isComplete() end function love.keypressed(key) if key == 'down' then move('down') elseif key == 'up' then move('up') elseif key == 'right' then move('right') elseif key == 'left' then move('left') end if isComplete() then love.load() end end
When the game is over only some variables need to be reset, so a function is made.
function love.load() love.graphics.setNewFont(30) gridXCount = 4 gridYCount = 4 function getInitialValue(x, y) -- etc. end function move(direction) -- etc. end function isComplete() -- etc. end function reset() grid = {} for y = 1, gridYCount do grid[y] = {} for x = 1, gridXCount do grid[y][x] = getInitialValue(x, y) end end repeat for moveNumber = 1, 1000 do local roll = love.math.random(4) if roll == 1 then move('down') elseif roll == 2 then move('up') elseif roll == 3 then move('right') elseif roll == 4 then move('left') end end for moveNumber = 1, gridXCount - 1 do move('left') end for moveNumber = 1, gridYCount - 1 do move('up') end until not isComplete() end reset() end function love.keypressed(key) -- etc. if isComplete() then reset() end end