Home page > LÖVE tutorials

Sokoban

A tutorial for Lua and LÖVE 11

Download sokoban.love

The levels used in this tutorial are from Rockbox.

Rules

Push all the boxes on to the storage locations.

Boxes can only be moved if there is a free space beyond it (not a wall or another box).

Legend

Player
Player on storage
Box
Box on storage
Storage
Wall

Controls

Arrow keysMove
rReset level
nNext level
pPrevious level

Overview

The different states a cell can be in are represented by the following strings:

'@'Player
'+'Player on storage
'$'Box
'*'Box on storage
'.'Storage
'#'Wall

The level is stored as a grid of these strings.

When an arrow key is pressed, the grid is looped through to find where the player is.

If the position on the grid adjacent to the player in the direction of the arrow pressed is movable (i.e. empty or a storage location), the values of the grid are changed to reflect the new player position.

If the position adjacent to the player is a box and the position beyond the box is movable, the grid is changed to reflect the new player and new box positions.

If there are no boxes left which aren't on storage locations then the level is complete.

Coding

Drawing a level

Each level is stored as a grid of strings. For now, a single level is stored, and a square is drawn for every cell which isn't a space (i.e. empty).

Full code at this point

function love.load()
    level = {
        {' ', ' ', '#', '#', '#'},
        {' ', ' ', '#', '.', '#'},
        {' ', ' ', '#', ' ', '#', '#', '#', '#'},
        {'#', '#', '#', '$', ' ', '$', '.', '#'},
        {'#', '.', ' ', '$', '@', '#', '#', '#'},
        {'#', '#', '#', '#', '$', '#'},
        {' ', ' ', ' ', '#', '.', '#'},
        {' ', ' ', ' ', '#', '#', '#'},
    }
end

function love.draw()
    for y, row in ipairs(level) do
        for x, cell in ipairs(row) do
            if cell ~= ' ' then
                local cellSize = 23

                love.graphics.rectangle(
                    'fill',
                    (x - 1) * cellSize,
                    (y - 1) * cellSize,
                    cellSize,
                    cellSize
                )
            end
        end
    end
end

Drawing cell types

The cell's string is drawn on top of the cell.

Full code at this point

function love.draw()
    for y, row in ipairs(level) do
        for x, cell in ipairs(row) do
            if cell ~= ' ' then
                local cellSize = 23

                love.graphics.setColor(1, 1, 1)
                love.graphics.rectangle(
                    'fill',
                    (x - 1) * cellSize,
                    (y - 1) * cellSize,
                    cellSize,
                    cellSize
                )
                love.graphics.setColor(0, 0, 0)
                love.graphics.print(
                    level[y][x],
                    (x - 1) * cellSize,
                    (y - 1) * cellSize
                )
            end
        end
    end
end

Setting colors

The background color is set, and the color of each cell is set based on its type.

Full code at this point

function love.load()
    love.graphics.setBackgroundColor(1, 1, .75)

    -- etc.
end

function love.draw()
    for y, row in ipairs(level) do
        for x, cell in ipairs(row) do
            if cell ~= ' ' then
                local cellSize = 23

                local colors = {
                    ['@'] = {.64, .53, 1},
                    ['+'] = {.62, .47, 1},
                    ['$'] = {1, .79, .49},
                    ['*'] = {.59, 1, .5},
                    ['.'] = {.61, .9, 1},
                    ['#'] = {1, .58, .82},
                }

                love.graphics.setColor(colors[cell])
                love.graphics.rectangle(
                    'fill',
                    (x - 1) * cellSize,
                    (y - 1) * cellSize,
                    cellSize,
                    cellSize
                )
                love.graphics.setColor(1, 1, 1)
                love.graphics.print(
                    level[y][x],
                    (x - 1) * cellSize,
                    (y - 1) * cellSize
                )
            end
        end
    end
end

Naming cell types

So we don't have to remember which string refers to which cell type, the cell type strings are stored in variables.

Full code at this point

function love.load()
    -- etc.

    player = '@'
    playerOnStorage = '+'
    box = '$'
    boxOnStorage = '*'
    storage = '.'
    wall = '#'
    empty = ' '
end

function love.draw()
    for y, row in ipairs(level) do
        for x, cell in ipairs(row) do
            if cell ~= empty then
                local cellSize = 23

                local colors = {
                    [player] = {.64, .53, 1},
                    [playerOnStorage] = {.62, .47, 1},
                    [box] = {1, .79, .49},
                    [boxOnStorage] = {.59, 1, .5},
                    [storage] = {.61, .9, 1},
                    [wall] = {1, .58, .82},
                }

                -- etc.
            end
        end
    end
end

Finding player cell

The first step in moving the player is finding which cell position they are at.

The cell type of the player's current position and the cell type of the adjacent position in the direction of the arrow key pressed are stored in variables, and, for now, are printed.

Full code at this point

function love.keypressed(key)
    if key == 'up' or key == 'down' or key == 'left' or key == 'right' then
        local playerX
        local playerY

        for testY, row in ipairs(level) do
            for testX, cell in ipairs(row) do
                if cell == player or cell == playerOnStorage then
                    playerX = testX
                    playerY = testY
                end
            end
        end

        -- Temporary
        print(playerX, playerY)
    end
end
5       5

Finding cell type in direction of key pressed

For now, the cell adjacent to the player in the direction of the key pressed has its type printed out.

Full code at this point

function love.keypressed(key)
    if key == 'up' or key == 'down' or key == 'left' or key == 'right' then

        local dx = 0
        local dy = 0
        if key == 'left' then
            dx = -1
        elseif key == 'right' then
            dx = 1
        elseif key == 'up' then
            dy = -1
        elseif key == 'down' then
            dy = 1
        end

        local current = level[playerY][playerX]
        local adjacent = level[playerY + dy][playerX + dx]

        -- Temporary
        print('current = level['..playerY..']['..playerX..'] ('..current..')')
        print('adjacent = level['..playerY + dy..']['..playerX + dx..'] ('..adjacent..')')
        print()
    end
end
current = level[5][5] (@)
adjacent = level[4][5] ( )

current = level[5][5] (@)
adjacent = level[5][6] (#)

current = level[5][5] (@)
adjacent = level[6][5] ($)

current = level[5][5] (@)
adjacent = level[5][4] ($)

Creating test level

A test level is made for testing player movement.

Full code at this point

function love.load()
    level = {
        {'#', '#', '#', '#', '#'},
        {'#', '@', ' ', '.', '#'},
        {'#', ' ', '$', ' ', '#'},
        {'#', '.', '$', ' ', '#'},
        {'#', ' ', '$', '.', '#'},
        {'#', '.', '$', '.', '#'},
        {'#', '.', '*', ' ', '#'},
        {'#', ' ', '*', '.', '#'},
        {'#', ' ', '*', ' ', '#'},
        {'#', '.', '*', '.', '#'},
        {'#', '#', '#', '#', '#'},
    }

    -- etc.
end

Moving player to empty location

If the value at the player's current position is player (i.e. not playerOnStorage) and the adjacent cell is empty, then the player's position becomes empty and the adjacent position becomes player.

Full code at this point

function love.keypressed(key)
        -- etc.

        if current == player and adjacent == empty then
            level[playerY][playerX] = empty
            level[playerY + dy][playerX + dx] = player
        end
    end
end

Moving player to storage

If the adjacent position is storage, then the new adjacent position becomes playerOnStorage.

Currently the player can move on to storage, but not off storage.

Full code at this point

function love.keypressed(key)
        -- etc.

        if current == player then
            if adjacent == empty then
                level[playerY][playerX] = empty
                level[playerY + dy][playerX + dx] = player
            elseif adjacent == storage then
                level[playerY][playerX] = empty
                level[playerY + dy][playerX + dx] = playerOnStorage
            end
        end
    end
end

Simplifying code

The new adjacent position (either player or playerOnStorage) is set based on the type of adjacent, so a table is made which returns the next adjacent cell type when indexed by the current adjacent cell type.

Indexing by anything other than the values empty or storage will be result in nil, so it is also be used to check if the player can move to the adjacent position.

Full code at this point

function love.keypressed(key)
        -- etc.

        local nextAdjacent = {
            [empty] = player,
            [storage] = playerOnStorage,
        }

        if current == player and nextAdjacent[adjacent] then
            level[playerY][playerX] = empty
            level[playerY + dy][playerX + dx] = nextAdjacent[adjacent]
        end

        -- etc.
end

Moving player from storage

If the player is on storage, then the player's current position is set to storage.

Full code at this point

function love.keypressed(key)
        -- etc.

        if nextAdjacent[adjacent] then
            if current == player then
                level[playerY][playerX] = empty
                level[playerY + dy][playerX + dx] = nextAdjacent[adjacent]
            elseif current == playerOnStorage then
                level[playerY][playerX] = storage
                level[playerY + dy][playerX + dx] = nextAdjacent[adjacent]
            end
        end
    end
end

Simplifying code

A table is made which returns the next cell type for the player's previous position when indexed by the current player cell type.

Full code at this point

function love.keypressed(key)
        -- etc.

        local nextCurrent = {
            [player] = empty,
            [playerOnStorage] = storage,
        }

        if nextAdjacent[adjacent] then
            level[playerY][playerX] = nextCurrent[current]
            level[playerY + dy][playerX + dx] = nextAdjacent[adjacent]
        end
    end
end

Pushing box on to empty location

The cell beyond the adjacent cell is stored in a variable.

level[playerY + dy + dy] is checked to see if it is not nil before it is indexed by [playerX + dx + dx].

(The adjacent position isn't checked in the same way because there is always a border of walls around each level, so level[playerY + dy] won't ever be nil.)

If the adjacent cell is a box and the beyond cell is empty, then the adjacent position is set to player and the beyond position is set to box.

Full code at this point

function love.keypressed(key)
        -- etc.

        local current = level[playerY][playerX]
        local adjacent = level[playerY + dy][playerX + dx]
        local beyond
        if level[playerY + dy + dy] then
            beyond = level[playerY + dy + dy][playerX + dx + dx]
        end

        -- etc.

        if nextAdjacent[adjacent] then
            level[playerY][playerX] = nextCurrent[current]
            level[playerY + dy][playerX + dx] = nextAdjacent[adjacent]

        elseif adjacent == box and beyond == empty then
            level[playerY][playerX] = nextCurrent[current]
            level[playerY + dy][playerX + dx] = player
            level[playerY + dy + dy][playerX + dx + dx] = box
        end
    end
end

Pushing box on to storage

If the beyond position is storage, then beyond position is set to boxOnStorage.

Full code at this point

function love.keypressed(key)
        -- etc.

        if nextAdjacent[adjacent] then
            level[playerY][playerX] = nextCurrent[current]
            level[playerY + dy][playerX + dx] = nextAdjacent[adjacent]

        elseif adjacent == box then
            if beyond == empty then
                level[playerY][playerX] = nextCurrent[current]
                level[playerY + dy][playerX + dx] = player
                level[playerY + dy + dy][playerX + dx + dx] = box
            elseif beyond == storage then
                level[playerY][playerX] = nextCurrent[current]
                level[playerY + dy][playerX + dx] = player
                level[playerY + dy + dy][playerX + dx + dx] = boxOnStorage
            end
        end
    end
end

Simplifying code

A table is made which returns the next beyond cell type when indexed by the current beyond cell type.

Full code at this point

function love.keypressed(key)
        -- etc.

        local nextBeyond = {
            [empty] = box,
            [storage] = boxOnStorage,
        }

        if nextAdjacent[adjacent] then
            level[playerY][playerX] = nextCurrent[current]
            level[playerY + dy][playerX + dx] = nextAdjacent[adjacent]

        elseif adjacent == box and nextBeyond[beyond] then
            level[playerY][playerX] = nextCurrent[current]
            level[playerY + dy][playerX + dx] = player
            level[playerY + dy + dy][playerX + dx + dx] = nextBeyond[beyond]
        end
    end
end

Pushing box on storage

If the adjacent cell is a box on storage, then the adjacent position is set to boxOnStorage.

Full code at this point

function love.keypressed(key)
        -- etc.

        if nextAdjacent[adjacent] then
            level[playerY][playerX] = nextCurrent[current]
            level[playerY + dy][playerX + dx] = nextAdjacent[adjacent]

        elseif nextBeyond[beyond] then
            level[playerY][playerX] = nextCurrent[current]

            if adjacent == box then
                level[playerY + dy][playerX + dx] = player
            elseif adjacent == boxOnStorage then
                level[playerY + dy][playerX + dx] = playerOnStorage
            end

            level[playerY + dy + dy][playerX + dx + dx] = nextBeyond[beyond]
        end
    end
end

Simplifying code

A table is made which returns the next adjacent cell type when a box is pushed when indexed by the current adjacent cell type.

Full code at this point

function love.keypressed(key)
        -- etc.

        local nextAdjacentPush = {
            [box] = player,
            [boxOnStorage] = playerOnStorage,
        }

        if nextAdjacent[adjacent] then
            level[playerY][playerX] = nextCurrent[current]
            level[playerY + dy][playerX + dx] = nextAdjacent[adjacent]

        elseif nextBeyond[beyond] and nextAdjacentPush[adjacent] then
            level[playerY][playerX] = nextCurrent[current]
            level[playerY + dy][playerX + dx] = nextAdjacentPush[adjacent]
            level[playerY + dy + dy][playerX + dx + dx] = nextBeyond[beyond]
        end
    end
end

Loading level from levels table

The levels are stored in a table.

The number of the current level is also stored.

The cells of the current level are copied from the table containing all the levels into the current level table.

Full code at this point

function love.load()
    -- etc.

    levels = {
        {
            {' ', ' ', '#', '#', '#'},
            {' ', ' ', '#', '.', '#'},
            {' ', ' ', '#', ' ', '#', '#', '#', '#'},
            {'#', '#', '#', '$', ' ', '$', '.', '#'},
            {'#', '.', ' ', '$', '@', '#', '#', '#'},
            {'#', '#', '#', '#', '$', '#'},
            {' ', ' ', ' ', '#', '.', '#'},
            {' ', ' ', ' ', '#', '#', '#'},
        },
        {
            {'#', '#', '#', '#', '#'},
            {'#', ' ', ' ', ' ', '#'},
            {'#', '@', '$', '$', '#', ' ', '#', '#', '#'},
            {'#', ' ', '$', ' ', '#', ' ', '#', '.', '#'},
            {'#', '#', '#', ' ', '#', '#', '#', '.', '#'},
            {' ', '#', '#', ' ', ' ', ' ', ' ', '.', '#'},
            {' ', '#', ' ', ' ', ' ', '#', ' ', ' ', '#'},
            {' ', '#', ' ', ' ', ' ', '#', '#', '#', '#'},
            {' ', '#', '#', '#', '#', '#'},
        },
        {
            {' ', '#', '#', '#', '#', '#', '#', '#'},
            {' ', '#', ' ', ' ', ' ', ' ', ' ', '#', '#', '#'},
            {'#', '#', '$', '#', '#', '#', ' ', ' ', ' ', '#'},
            {'#', ' ', '@', ' ', '$', ' ', ' ', '$', ' ', '#'},
            {'#', ' ', '.', '.', '#', ' ', '$', ' ', '#', '#'},
            {'#', '#', '.', '.', '#', ' ', ' ', ' ', '#'},
            {' ', '#', '#', '#', '#', '#', '#', '#', '#'},
        },
    }

    currentLevel = 1

    level = {}
    for y, row in ipairs(levels[currentLevel]) do
        level[y] = {}
        for x, cell in ipairs(row) do
            level[y][x] = cell
        end
    end
end

Resetting level

When the r key is pressed the level is reset.

The code for copying the current level from the levels table is reused, so a function is made.

Full code at this point

function love.load()
    -- etc.

    function loadLevel()
        level = {}
        for y, row in ipairs(levels[currentLevel]) do
            level[y] = {}
            for x, cell in ipairs(row) do
                level[y][x] = cell
            end
        end
    end

    loadLevel()
end

function love.keypressed(key)
    -- etc.

    elseif key == 'r' then
        loadLevel()
    end
end

Next and previous level

When the n key is pressed, the next level is loaded, and when the p key is pressed, the previous level is loaded.

Full code at this point

function love.keypressed(key)
    -- etc.

    elseif key == 'n' then
        currentLevel = currentLevel + 1
        loadLevel()

    elseif key == 'p' then
        currentLevel = currentLevel - 1
        loadLevel()
    end
end

Wrapping next and previous level

If the next level is after the last level, then the first level is loaded.

If the previous level is before the first level, then the last level is loaded.

Full code at this point

function love.keypressed(key)
    -- etc.

    elseif key == 'n' then
        currentLevel = currentLevel + 1
        if currentLevel > #levels then
            currentLevel = 1
        end
        loadLevel()

    elseif key == 'p' then
        currentLevel = currentLevel - 1
        if currentLevel < 1 then
            currentLevel = #levels
        end
        loadLevel()
    end
end

Go to next level when complete

After the player has moved, all of the cells in the level are looped through, and if none of the cells are boxes (i.e. all the boxes are on storage), then the level is complete and the next level is loaded.

Full code at this point

function love.keypressed(key)
    if key == 'up' or key == 'down' or key == 'left' or key == 'right' then
        -- etc.

        local complete = true

        for y, row in ipairs(level) do
            for x, cell in ipairs(row) do
                if cell == box then
                    complete = false
                end
            end
        end

        if complete then
            currentLevel = currentLevel + 1
            if currentLevel > #levels then
                currentLevel = 1
            end
            loadLevel()
        end

    elseif key == 'r' then
    -- etc.
end

More levels

Full code at this point