🏠 Home page > 🌖 LÖVE tutorials

Fifteen

A tutorial for Lua and LÖVE 11

Download fifteen.love

Rules

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.

Controls

Arrow keysMove piece

Overview

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.

Coding

Drawing pieces

The pieces are drawn as squares.

For now, a piece is drawn where the empty space should be.

Full code at this point

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

Drawing numbers

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.

Full code at this point

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

Setting the font

The font is set to size 30.

Full code at this point

function love.load()
    love.graphics.setNewFont(30)
end

Creating the grid

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.

Full code at this point

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

Not drawing the empty space

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.

Full code at this point

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

Finding position of empty space

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.

Full code at this point

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

Moving pieces down

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.

Full code at this point

function love.keypressed(key)
    -- etc.

    grid[emptyY - 1][emptyX], grid[emptyY][emptyX] =
    grid[emptyY][emptyX], grid[emptyY - 1][emptyX]
end

Only move down if possible

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.

Full code at this point

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

Moving pieces up

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

Full code at this point

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

Moving pieces left and right

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.

Full code at this point

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

Only move on X axis if possible

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.

Full code at this point

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

Shuffling

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.

Full code at this point

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

Simplifying code

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.

Full code at this point

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

Making the bottom-right position empty

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.

Full code at this point

function love.load()
    -- etc.

    for moveNumber = 1, gridXCount - 1 do
        move('left')
    end

    for moveNumber = 1, gridYCount - 1 do
        move('up')
    end
end

Check if complete

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.

Full code at this point

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

Simplifying code

The code for calculating the initial value of a piece is reused, so it is made into a function.

Full code at this point

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

Reshuffle if complete after shuffling

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.

Full code at this point

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

Resetting the game

When the game is over only some variables need to be reset, so a function is made.

Full code at this point

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