Game: Pumpkin Push Part II#
Level Map#
Now that we’ve got a player character walking around an empty world, it’s time to add walls to turn that empty world into a game level.
First, let’s make sure we can load and draw a single wall. Add a line in love.load() to load the wall image:
function love.load()
img_player = love.graphics.newImage("images/ghost.png")
img_ground = love.graphics.newImage("images/ground.png")
img_wall = love.graphics.newImage("images/wall.png")
-- ...
endAnd a couple of lines in love.draw() to draw it:
function love.draw()
-- ...
local wall_x, wall_y = tile_to_screen(1, 1)
love.graphics.draw(img_wall, wall_x, wall_y)
end
Loading Walls#
So far so good, but we want more than a single wall tile. It would be handy to have some kind of level map that lets us easily place wall tiles anywhere on the playing field.
One of the simplest ways to represent a level map is using an array table, containing a number of items equal to the width times the height of the level. Our NUM_ROWS and NUM_COLS are both 10, so 10 x 10 equals an array with 100 entries.
We’ll add an array level_map at the top of our file to contain the level map, using spaces ' ' to indicate empty tiles and 'w' to mark wall tiles.
local level_map = {
'w', 'w', 'w', 'w', 'w', 'w', 'w', 'w', 'w', 'w', -- row 1
'w', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'w', -- row 2
'w', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'w', -- etc...
'w', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'w',
'w', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'w',
'w', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'w',
'w', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'w',
'w', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'w',
'w', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'w',
'w', 'w', 'w', 'w', 'w', 'w', 'w', 'w', 'w', 'w',
}The lines are wrapped at the same number of items as the width of the level, and as many lines as the height of the level, so that it’s easy to see where the tiles should go, but this is a convenience for the person editing the map. It would mean the same thing if it was all on one line – or one entry per line – it would just be more confusing to read and edit.
What matters is the order of the items in the array… all of the tiles for the first row, followed by all of the tiles for the second row, the third row, and so on.
We need to to able to map the column and row for a tile into a single index into the array.
We can use the calculation local tile_index = column + (row - 1) * NUM_COLS to convert our tile coordinates into an array index. This comes up all the time when dealing with grid based data stored in an array, whether they are tiles in a level map, or the pixels in an image or texture.
As the value of column increases it simply gives us values 1 through 10. But as row increases, (row - 1) * NUM_COLS will start off as 0, then 10, then 20, 30, up to 90. By adding these two values together, we can reach any value between 1 and 100 in the array. Tiles (1, 1) through (10, 1) become index 1 through 10; tiles (1, 2) through (10, 2) become array index 11 through 20 and so on until we reach the last tile (10, 10) which gives us the index 100, the final entry in the array.
Now we can write a short helper function to check if a particular tile on the map is a wall. We look up that index and check if it contains 'w':
function tile_is_wall(column, row)
local tile_index = column + (row - 1) * NUM_COLS
return level_map[tile_index] == 'w'
endAnd then in love.draw() we’ll draw the wall graphic on each tile that is a wall:
function love.draw()
love.graphics.clear(156/255, 172/255, 141/255)
for row = 1, NUM_ROWS do
for column = 1, NUM_COLS do
local tile_x, tile_y = tile_to_screen(column, row)
love.graphics.draw(img_ground, tile_x, tile_y)
if tile_is_wall(column, row) then
love.graphics.draw(img_wall, tile_x, tile_y)
end
end
end
local player_x, player_y = tile_to_screen(player_col, player_row)
love.graphics.draw(img_player, player_x, player_y)
-- removed the test tile draw call
end
And we can make the level more interesting by re-arranging the walls in level_map:
local level_map = {
' ', 'w', 'w', 'w', 'w', 'w', ' ', ' ', ' ', ' ',
'w', 'w', ' ', ' ', ' ', 'w', 'w', 'w', 'w', 'w',
'w', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'w',
'w', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'w', 'w',
'w', ' ', ' ', ' ', 'w', 'w', ' ', ' ', 'w', ' ',
'w', ' ', ' ', ' ', ' ', 'w', 'w', ' ', 'w', 'w',
'w', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'w',
'w', 'w', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'w',
' ', 'w', 'w', ' ', ' ', 'w', 'w', 'w', ' ', 'w',
' ', ' ', 'w', 'w', 'w', 'w', ' ', 'w', 'w', 'w',
}
Player Start Position#
The position our player starts in is currently hard-coded, but now that we have a level map, we can mark the start position there instead.
Let’s change the defaults back to something simple:
local player_col = 1
local player_row = 1And then mark the position we actually want the player to start on the map with a 'g' (for ghost, 'p' will be for pumpkin):
local level_map = {
' ', 'w', 'w', 'w', 'w', 'w', ' ', ' ', ' ', ' ',
'w', 'w', ' ', ' ', ' ', 'w', 'w', 'w', 'w', 'w',
'w', ' ', ' ', 'g', ' ', ' ', ' ', ' ', ' ', 'w', -- added a 'g' here
'w', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'w', 'w',
'w', ' ', ' ', ' ', 'w', 'w', ' ', ' ', 'w', ' ',
'w', ' ', ' ', ' ', ' ', 'w', 'w', ' ', 'w', 'w',
'w', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'w',
'w', 'w', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'w',
' ', 'w', 'w', ' ', ' ', 'w', 'w', 'w', ' ', 'w',
' ', ' ', 'w', 'w', 'w', 'w', ' ', 'w', 'w', 'w',
}When the game loads, we can search for the 'g' in the map, and set the player’s starting coordinates based on that.
Finding a specific value in the map sounds like a useful thing you might want to do in general, so let’s write a new function to do that. We’ll pass in the character that we’re looking for, and it will return the coordinates of the first instance of that character in the map.
function find_map_coords(character)
for row = 1, NUM_ROWS do
for column = 1, NUM_COLS do
local tile_index = column + (row - 1) * NUM_COLS
if level_map[tile_index] == character then
return column, row
end
end
end
endThen when the game loads we can call find_map_coords to find the player’s starting position.
function love.load()
-- ...
player_col, player_row = find_map_coords('g')
end
If the level doesn’t contain a 'g' at all then find_map_coords will return nil and the game will crash. (Try and see!)
This isn’t necessarily a problem… you’re the developer so it’s your responsibility to make sure your levels are valid. But if you wanted to handle this situation more gracefully, you could check the return value first before using the values, and print a warning message to the console.
function love.load()
-- ...
local start_col, start_row = find_map_coords('g')
if start_col and start_row then
player_col, player_row = start_col, start_row
else
print("could not find player start position in level map!")
end
endCentering The Map#
So far we’ve been drawing the game level aligned to the top left corner of the window, which is the (0, 0) pixel coordinate in Love2D.
It would be more aesthetically pleasing to have the game centered in our window, so let’s do that now. We’ll update tile_to_screen() to calculate an offset that we can use to center the map.
We’ll do this in two steps which will hopefully make it more obvious what each step is doing.
First, we’ll call love.graphics.getDimensions() to get the width and height of the game in pixels, which in our case will be the area inside our window (that we specified in conf.lua). We want to center the map so lets take half of each dimension and add that to the screen position we draw our game tiles at.
function tile_to_screen(column, row)
local graphics_width, graphics_height = love.graphics.getDimensions()
local offset_x = graphics_width/2
local offset_y = graphics_height/2
local screen_x = offset_x + (column - 1) * TILE_SIZE
local screen_y = offset_y + (row - 1) * TILE_SIZE
return screen_x, screen_y
end
That gets us part of the way, it’s certainly centered by one definition: we’ve moved the top left corner of the map from the top left corner of the window into the center of the window.
But we really want the center of the map to align to the center of the window, not the corner. We can accomplish this by calculating the width of the map in pixels, and then subtracting half of that amount from our offset, shifting the map left and up by half of its size.
function tile_to_screen(column, row)
local graphics_width, graphics_height = love.graphics.getDimensions()
local map_width = NUM_COLS * TILE_SIZE
local map_height = NUM_ROWS * TILE_SIZE
local offset_x = graphics_width/2 - map_width/2
local offset_y = graphics_height/2 - map_height/2
local screen_x = offset_x + (column - 1) * TILE_SIZE
local screen_y = offset_y + (row - 1) * TILE_SIZE
return screen_x, screen_y
end
Ta daa!
Code So Far
local TILE_SIZE = 64
local NUM_COLS = 10
local NUM_ROWS = 10
local player_col = 1
local player_row = 1
local sounds = {
footstep = {
files = {
"sounds/footstep1.ogg",
"sounds/footstep2.ogg",
}
},
blocked = {
files = {
"sounds/blocked1.ogg",
"sounds/blocked2.ogg",
"sounds/blocked3.ogg",
}
},
}
local level_map = {
' ', 'w', 'w', 'w', 'w', 'w', ' ', ' ', ' ', ' ',
'w', 'w', ' ', ' ', ' ', 'w', 'w', 'w', 'w', 'w',
'w', ' ', ' ', 'g', ' ', ' ', ' ', ' ', ' ', 'w',
'w', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'w', 'w',
'w', ' ', ' ', ' ', 'w', 'w', ' ', ' ', 'w', ' ',
'w', ' ', ' ', ' ', ' ', 'w', 'w', ' ', 'w', 'w',
'w', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'w',
'w', 'w', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'w',
' ', 'w', 'w', ' ', ' ', 'w', 'w', 'w', ' ', 'w',
' ', ' ', 'w', 'w', 'w', 'w', ' ', 'w', 'w', 'w',
}
function love.load()
img_player = love.graphics.newImage("images/ghost.png")
img_ground = love.graphics.newImage("images/ground.png")
img_wall = love.graphics.newImage("images/wall.png")
for name, sound in pairs(sounds) do
sound.sources = {}
for i, filename in ipairs(sound.files) do
local source = love.audio.newSource(filename, "static")
table.insert(sound.sources, source)
end
end
local start_col, start_row = find_map_coords('g')
if start_col and start_row then
player_col, player_row = start_col, start_row
else
print("could not find player start position in level map!")
end
end
function love.draw()
love.graphics.clear(156/255, 172/255, 141/255)
for row = 1, NUM_ROWS do
for column = 1, NUM_COLS do
local tile_x, tile_y = tile_to_screen(column, row)
love.graphics.draw(img_ground, tile_x, tile_y)
if tile_is_wall(column, row) then
love.graphics.draw(img_wall, tile_x, tile_y)
end
end
end
local player_x, player_y = tile_to_screen(player_col, player_row)
love.graphics.draw(img_player, player_x, player_y)
end
function love.keypressed( key, scancode, isrepeat )
print(string.format("love.keypressed %s, %s, %s", key, scancode, isrepeat))
if key == "a" or key == "left" then
if player_col > 1 then
print(" move left")
player_col = player_col - 1
play_sound("footstep")
else
play_sound("blocked")
end
elseif key == "d" or key == "right" then
if player_col < NUM_COLS then
print(" move right")
player_col = player_col + 1
play_sound("footstep")
else
play_sound("blocked")
end
elseif key == "w" or key == "up" then
if player_row > 1 then
print(" move up")
player_row = player_row - 1
play_sound("footstep")
else
play_sound("blocked")
end
elseif key == "s" or key == "down" then
if player_row < NUM_ROWS then
print(" move down")
player_row = player_row + 1
play_sound("footstep")
else
play_sound("blocked")
end
end
print(string.format(" player pos %d, %d", player_col, player_row))
end
function tile_to_screen(column, row)
local graphics_width, graphics_height = love.graphics.getDimensions()
local map_width = NUM_COLS * TILE_SIZE
local map_height = NUM_ROWS * TILE_SIZE
local offset_x = graphics_width/2 - map_width/2
local offset_y = graphics_height/2 - map_height/2
local screen_x = offset_x + (column - 1) * TILE_SIZE
local screen_y = offset_y + (row - 1) * TILE_SIZE
return screen_x, screen_y
end
function play_sound(name)
local effect = sounds[name]
local rnd_index = math.random(#effect.sources)
local source = effect.sources[rnd_index]
love.audio.play(source)
end
function tile_is_wall(column, row)
local tile_index = column + (row - 1) * NUM_COLS
return level_map[tile_index] == 'w'
end
function find_map_coords(character)
for row = 1, NUM_ROWS do
for column = 1, NUM_COLS do
local tile_index = column + (row - 1) * NUM_COLS
if level_map[tile_index] == character then
return column, row
end
end
end
endHitting A Wall#
Now that we’ve got our walls loaded, we want to block the player from being able to walk through them.
Wait… if the player is a ghost so shouldn’t they be able to walk through walls…? It’s a game, it doesn’t need to obey the laws of ghost physics.
We’re already checking to see if the player is trying to move beyond the bounds of the level and preventing them from doing so. Now we’ll extend that functionality to also check for walls, and prevent them from walking into them.
We’ll also take this opportunity to refactor the movement code in love.keypressed() to reduce the amount of duplicate code that’s already there, and avoid adding more. Instead of handling each direction separately, we can write some functions to handle moving in any direction.
The first thing we want to do is figure out which direction the player wants to move (if any). We can write a function key_to_direction() to convert the key they pressed into two direction values:
function key_to_direction(key)
if key == "a" or key == "left" then
return -1, 0
elseif key == "d" or key == "right" then
return 1, 0
elseif key == "w" or key == "up" then
return 0, -1
elseif key == "s" or key == "down" then
return 0, 1
end
end…and replace all of the key checks we had in love.keypressed() with a single call to key_to_direction():
function love.keypressed( key, scancode, isrepeat )
print(string.format("love.keypressed %s, %s, %s", key, scancode, isrepeat))
local move_x, move_y = key_to_direction(key)
if move_x and move_y then
print(string.format(" player wants to move %d, %d", move_x, move_y))
end
print(string.format(" player pos %d, %d", player_col, player_row))
endAt this point the player won’t actually be able to move, but we can see what they’re trying to do in the console output:
love.keypressed w, w, false
player wants to move 0, -1
player pos 4, 3
love.keypressed a, a, false
player wants to move -1, 0
player pos 4, 3
love.keypressed s, s, false
player wants to move 0, 1
player pos 4, 3
love.keypressed d, d, false
player wants to move 1, 0
player pos 4, 3Next we want to check if the player is allowed to move in the direction they requested. player_can_move() will add the desired movement to the player’s current position, and check if that position is still within the level bounds.
function player_can_move(move_x, move_y)
local new_col = player_col + move_x
local new_row = player_row + move_y
if new_col < 1 or new_col > NUM_COLS then
return false
end
if new_row < 1 or new_row > NUM_ROWS then
return false
end
return true
endIf the move is allowed then we update the player’s real position and play a footstep, and if not we play the blocked sound like we were doing before, except that now we only need to do it in one place instead of four.
We’ll also move the player position debug line up so it only prints if the player has pressed a valid movement key.
function love.keypressed( key, scancode, isrepeat )
print(string.format("love.keypressed %s, %s, %s", key, scancode, isrepeat))
local move_x, move_y = key_to_direction(key)
if move_x and move_y then
print(string.format(" player wants to move %d, %d", move_x, move_y))
if player_can_move(move_x, move_y) then
play_sound("footstep")
player_col = player_col + move_x
player_row = player_row + move_y
else
play_sound("blocked")
print(" blocked!")
end
print(string.format(" player pos %d, %d", player_col, player_row))
end
endThe player can only move on one axis (X or Y) at a time. Since key_to_direction() returns a 1 or -1 for either the X or Y value with the other being 0, we can add both the move_x and move_y values to the player’s current position to find their new desired position, and whichever value contains 0 will have no effect.
Now it’s a simple matter to add a check if the new position contains a wall by calling tile_is_wall, and prevent movement in that case as well.
function player_can_move(move_x, move_y)
local new_col = player_col + move_x
local new_row = player_row + move_y
if new_col < 1 or new_col > NUM_COLS then
return false
end
if new_row < 1 or new_row > NUM_ROWS then
return false
end
if tile_is_wall(new_col, new_row) then
return false
end
return true
endTry moving the ghost all around the level… they shouldn’t be able to pass a wall in any direction.
Code So Far
local TILE_SIZE = 64
local NUM_COLS = 10
local NUM_ROWS = 10
local player_col = 1
local player_row = 1
local sounds = {
footstep = {
files = {
"sounds/footstep1.ogg",
"sounds/footstep2.ogg",
}
},
blocked = {
files = {
"sounds/blocked1.ogg",
"sounds/blocked2.ogg",
"sounds/blocked3.ogg",
}
},
}
local level_map = {
' ', 'w', 'w', 'w', 'w', 'w', ' ', ' ', ' ', ' ',
'w', 'w', ' ', ' ', ' ', 'w', 'w', 'w', 'w', 'w',
'w', ' ', ' ', 'g', ' ', ' ', ' ', ' ', ' ', 'w',
'w', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'w', 'w',
'w', ' ', ' ', ' ', 'w', 'w', ' ', ' ', 'w', ' ',
'w', ' ', ' ', ' ', ' ', 'w', 'w', ' ', 'w', 'w',
'w', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'w',
'w', 'w', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'w',
' ', 'w', 'w', ' ', ' ', 'w', 'w', 'w', ' ', 'w',
' ', ' ', 'w', 'w', 'w', 'w', ' ', 'w', 'w', 'w',
}
function love.load()
img_player = love.graphics.newImage("images/ghost.png")
img_ground = love.graphics.newImage("images/ground.png")
img_wall = love.graphics.newImage("images/wall.png")
for name, sound in pairs(sounds) do
sound.sources = {}
for i, filename in ipairs(sound.files) do
local source = love.audio.newSource(filename, "static")
table.insert(sound.sources, source)
end
end
local start_col, start_row = find_map_coords('g')
if start_col and start_row then
player_col, player_row = start_col, start_row
else
print("could not find player start position in level map!")
end
end
function love.draw()
love.graphics.clear(156/255, 172/255, 141/255)
for row = 1, NUM_ROWS do
for column = 1, NUM_COLS do
local tile_x, tile_y = tile_to_screen(column, row)
love.graphics.draw(img_ground, tile_x, tile_y)
if tile_is_wall(column, row) then
love.graphics.draw(img_wall, tile_x, tile_y)
end
end
end
local player_x, player_y = tile_to_screen(player_col, player_row)
love.graphics.draw(img_player, player_x, player_y)
end
function love.keypressed( key, scancode, isrepeat )
print(string.format("love.keypressed %s, %s, %s", key, scancode, isrepeat))
local move_x, move_y = key_to_direction(key)
if move_x and move_y then
print(string.format(" player wants to move %d, %d", move_x, move_y))
if player_can_move(move_x, move_y) then
play_sound("footstep")
player_col = player_col + move_x
player_row = player_row + move_y
else
play_sound("blocked")
print(" blocked!")
end
print(string.format(" player pos %d, %d", player_col, player_row))
end
end
function tile_to_screen(column, row)
local graphics_width, graphics_height = love.graphics.getDimensions()
local map_width = NUM_COLS * TILE_SIZE
local map_height = NUM_ROWS * TILE_SIZE
local offset_x = graphics_width/2 - map_width/2
local offset_y = graphics_height/2 - map_height/2
local screen_x = offset_x + (column - 1) * TILE_SIZE
local screen_y = offset_y + (row - 1) * TILE_SIZE
return screen_x, screen_y
end
function play_sound(name)
local effect = sounds[name]
local rnd_index = math.random(#effect.sources)
local source = effect.sources[rnd_index]
love.audio.play(source)
end
function tile_is_wall(column, row)
local tile_index = column + (row - 1) * NUM_COLS
return level_map[tile_index] == 'w'
end
function find_map_coords(character)
for row = 1, NUM_ROWS do
for column = 1, NUM_COLS do
local tile_index = column + (row - 1) * NUM_COLS
if level_map[tile_index] == character then
return column, row
end
end
end
end
function key_to_direction(key)
if key == "a" or key == "left" then
return -1, 0
elseif key == "d" or key == "right" then
return 1, 0
elseif key == "w" or key == "up" then
return 0, -1
elseif key == "s" or key == "down" then
return 0, 1
end
end
function player_can_move(move_x, move_y)
local new_col = player_col + move_x
local new_row = player_row + move_y
if new_col < 1 or new_col > NUM_COLS then
return false
end
if new_row < 1 or new_row > NUM_ROWS then
return false
end
if tile_is_wall(new_col, new_row) then
return false
end
return true
endCompleting The Map#
Time to get the final image assets loaded into the game, and marked on the map.
Place a few pumpkins and vines on the map, marked with 'p' and 'v' respectively:
local level_map = {
' ', 'w', 'w', 'w', 'w', 'w', ' ', ' ', ' ', ' ',
'w', 'w', ' ', ' ', 'v', 'w', 'w', 'w', 'w', 'w',
'w', ' ', ' ', 'g', ' ', ' ', ' ', ' ', ' ', 'w',
'w', ' ', ' ', ' ', ' ', 'p', ' ', 'p', 'w', 'w',
'w', ' ', ' ', ' ', 'w', 'w', ' ', ' ', 'w', ' ',
'w', 'v', ' ', ' ', ' ', 'w', 'w', ' ', 'w', 'w',
'w', ' ', 'p', ' ', ' ', ' ', ' ', ' ', 'v', 'w',
'w', 'w', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'w',
' ', 'w', 'w', ' ', ' ', 'w', 'w', 'w', ' ', 'w',
' ', ' ', 'w', 'w', 'w', 'w', ' ', 'w', 'w', 'w',
}Load the new images in love.load():
function love.load()
img_player = love.graphics.newImage("images/ghost.png")
img_ground = love.graphics.newImage("images/ground.png")
img_wall = love.graphics.newImage("images/wall.png")
img_pumpkin_sad = love.graphics.newImage("images/pumpkin_sad.png")
img_pumpkin_happy = love.graphics.newImage("images/pumpkin_happy.png")
img_vines = love.graphics.newImage("images/vines.png")
-- ...
end Add new tile_is_pumpkin() and tile_is_vine() helpers like the existing tile_is_wall():
function tile_is_wall(column, row)
local tile_index = column + (row - 1) * NUM_COLS
return level_map[tile_index] == 'w'
end
function tile_is_pumpkin(column, row)
local tile_index = column + (row - 1) * NUM_COLS
return level_map[tile_index] == 'p'
end
function tile_is_vine(column, row)
local tile_index = column + (row - 1) * NUM_COLS
return level_map[tile_index] == 'v'
endAnd draw them in love.draw().
function love.draw()
love.graphics.clear(156/255, 172/255, 141/255)
for row = 1, NUM_ROWS do
for column = 1, NUM_COLS do
local tile_x, tile_y = tile_to_screen(column, row)
love.graphics.draw(img_ground, tile_x, tile_y)
if tile_is_wall(column, row) then
love.graphics.draw(img_wall, tile_x, tile_y)
end
if tile_is_vine(column, row) then
love.graphics.draw(img_vines, tile_x, tile_y)
end
if tile_is_pumpkin(column, row) then
love.graphics.draw(img_pumpkin_sad, tile_x, tile_y)
end
end
end
local player_x, player_y = tile_to_screen(player_col, player_row)
love.graphics.draw(img_player, player_x, player_y)
endWe’re only drawing sad pumpkins for now, because that’s how they start off. Once they are pushed onto a tile with vines they’ll become happy.

You may have noticed that we’re repeating the math in tile_is_wall(), tile_is_pumpkin(), and tile_is_vine(). This is a good indication that it’s time to separate that code into it’s own utility function, so let’s do that now. get_tile_at() will do the math to look up the tile at a particular column and row, and return the character stored in that map location.
function get_tile_at(column, row)
local tile_index = column + (row - 1) * NUM_COLS
return level_map[tile_index]
endNow we can use get_tile_at instead of copy and pasting the same formula:
function tile_is_wall(column, row)
return get_tile_at(column, row) == 'w'
end
function tile_is_pumpkin(column, row)
return get_tile_at(column, row) == 'p'
end
function tile_is_vine(column, row)
return get_tile_at(column, row) == 'v'
endWe can also update find_map_coords() to use get_tile_at() as well:
function find_map_coords(character)
for row = 1, NUM_ROWS do
for column = 1, NUM_COLS do
if get_tile_at(column, row) == character then
return column, row
end
end
end
endCode So Far
local TILE_SIZE = 64
local NUM_COLS = 10
local NUM_ROWS = 10
local player_col = 1
local player_row = 1
local sounds = {
footstep = {
files = {
"sounds/footstep1.ogg",
"sounds/footstep2.ogg",
}
},
blocked = {
files = {
"sounds/blocked1.ogg",
"sounds/blocked2.ogg",
"sounds/blocked3.ogg",
}
},
}
local level_map = {
' ', 'w', 'w', 'w', 'w', 'w', ' ', ' ', ' ', ' ',
'w', 'w', ' ', ' ', 'v', 'w', 'w', 'w', 'w', 'w',
'w', ' ', ' ', 'g', ' ', ' ', ' ', ' ', ' ', 'w',
'w', ' ', ' ', ' ', ' ', 'p', ' ', 'p', 'w', 'w',
'w', ' ', ' ', ' ', 'w', 'w', ' ', ' ', 'w', ' ',
'w', 'v', ' ', ' ', ' ', 'w', 'w', ' ', 'w', 'w',
'w', ' ', 'p', ' ', ' ', ' ', ' ', ' ', 'v', 'w',
'w', 'w', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'w',
' ', 'w', 'w', ' ', ' ', 'w', 'w', 'w', ' ', 'w',
' ', ' ', 'w', 'w', 'w', 'w', ' ', 'w', 'w', 'w',
}
function love.load()
img_player = love.graphics.newImage("images/ghost.png")
img_ground = love.graphics.newImage("images/ground.png")
img_wall = love.graphics.newImage("images/wall.png")
img_pumpkin_sad = love.graphics.newImage("images/pumpkin_sad.png")
img_pumpkin_happy = love.graphics.newImage("images/pumpkin_happy.png")
img_vines = love.graphics.newImage("images/vines.png")
for name, sound in pairs(sounds) do
sound.sources = {}
for i, filename in ipairs(sound.files) do
local source = love.audio.newSource(filename, "static")
table.insert(sound.sources, source)
end
end
local start_col, start_row = find_map_coords('g')
if start_col and start_row then
player_col, player_row = start_col, start_row
else
print("could not find player start position in level map!")
end
end
function love.draw()
love.graphics.clear(156/255, 172/255, 141/255)
for row = 1, NUM_ROWS do
for column = 1, NUM_COLS do
local tile_x, tile_y = tile_to_screen(column, row)
love.graphics.draw(img_ground, tile_x, tile_y)
if tile_is_wall(column, row) then
love.graphics.draw(img_wall, tile_x, tile_y)
end
if tile_is_vine(column, row) then
love.graphics.draw(img_vines, tile_x, tile_y)
end
if tile_is_pumpkin(column, row) then
love.graphics.draw(img_pumpkin_sad, tile_x, tile_y)
end
end
end
local player_x, player_y = tile_to_screen(player_col, player_row)
love.graphics.draw(img_player, player_x, player_y)
end
function love.keypressed( key, scancode, isrepeat )
print(string.format("love.keypressed %s, %s, %s", key, scancode, isrepeat))
local move_x, move_y = key_to_direction(key)
if move_x and move_y then
print(string.format(" player wants to move %d, %d", move_x, move_y))
if player_can_move(move_x, move_y) then
play_sound("footstep")
player_col = player_col + move_x
player_row = player_row + move_y
else
play_sound("blocked")
print(" blocked!")
end
print(string.format(" player pos %d, %d", player_col, player_row))
end
end
function tile_to_screen(column, row)
local graphics_width, graphics_height = love.graphics.getDimensions()
local map_width = NUM_COLS * TILE_SIZE
local map_height = NUM_ROWS * TILE_SIZE
local offset_x = graphics_width/2 - map_width/2
local offset_y = graphics_height/2 - map_height/2
local screen_x = offset_x + (column - 1) * TILE_SIZE
local screen_y = offset_y + (row - 1) * TILE_SIZE
return screen_x, screen_y
end
function play_sound(name)
local effect = sounds[name]
local rnd_index = math.random(#effect.sources)
local source = effect.sources[rnd_index]
love.audio.play(source)
end
function tile_is_wall(column, row)
return get_tile_at(column, row) == 'w'
end
function tile_is_pumpkin(column, row)
return get_tile_at(column, row) == 'p'
end
function tile_is_vine(column, row)
return get_tile_at(column, row) == 'v'
end
function find_map_coords(character)
for row = 1, NUM_ROWS do
for column = 1, NUM_COLS do
if get_tile_at(column, row) == character then
return column, row
end
end
end
end
function key_to_direction(key)
if key == "a" or key == "left" then
return -1, 0
elseif key == "d" or key == "right" then
return 1, 0
elseif key == "w" or key == "up" then
return 0, -1
elseif key == "s" or key == "down" then
return 0, 1
end
end
function player_can_move(move_x, move_y)
local new_col = player_col + move_x
local new_row = player_row + move_y
if new_col < 1 or new_col > NUM_COLS then
return false
end
if new_row < 1 or new_row > NUM_ROWS then
return false
end
if tile_is_wall(new_col, new_row) then
return false
end
return true
end
function get_tile_at(column, row)
local tile_index = column + (row - 1) * NUM_COLS
return level_map[tile_index]
endCompleting Gameplay#
Now that we have pumpkins loaded into the game world, it’s time to make them interactive.
How do we allow pumpkins to be moved around in the game level? The player already moves, and their current position is stored in the variables player_col and player_row, but currently the position of the pumpkins is fixed in the level map.
One option would be to modify the level_map array itself. We could, for example, write a ' ' (space) character into level_map where the pumpkin’s old position was, and then write a 'p' at the new position. On the next frame, the pumpkin would draw in it’s new location.
-- example only
level_map[pumpkin_old_index] = ' '
level_map[pumpkin_new_index] = 'p'One issue with doing this is we want to be able to push pumpkins on top of tiles with vines, indicated with 'v'. If we overwrite this index in the array with a 'p' then the 'v' would be erased.
So instead of modifying the map while the player is playing, we’ll create a new array to hold the positions of all of the pumpkins. When the game loads, we’ll read the map to populate the pumpkin list with all of the starting positions. If the player pushes a pumpkin we’ll find it in the pumpkin list and update its position there.
A Table Full Of Pumpkins#
First we’ll declare a new table near the top of our file to store the positions of all of the pumpkins. It will start off empty.
local player_col = 1
local player_row = 1
local pumpkins = {}In love.load(), we’ll loop over the tiles in the map, and for each 'p' we find, we’ll create a new table storing the position, and insert that table into the pumpkins array.
function love.load()
-- ...
for row = 1, NUM_ROWS do
for column = 1, NUM_COLS do
if get_tile_at(column, row) == 'p' then
local pumpkin = {}
pumpkin.column = column
pumpkin.row = row
print(string.format("loading pumpkin at (%d, %d)", pumpkin.column, pumpkin.row))
table.insert(pumpkins, pumpkin)
end
end
end
endRunning the game should display the list of loaded pumpkins in the debug console.
loading pumpkin at (6, 4)
loading pumpkin at (8, 4)
loading pumpkin at (3, 7)We’ll also need to update love.draw so that we are drawing each pumpkin in the pumpkins list, which will change during gameplay, instead of from the level_map which only has the fixed starting positions.
After looping over the map to draw the fixed position ground, walls and vines, we’ll do a separate loop using ipairs over the dynamic pumpkins array to draw all of the pumpkins in their current positions.
function love.draw()
love.graphics.clear(156/255, 172/255, 141/255)
for row = 1, NUM_ROWS do
for column = 1, NUM_COLS do
local tile_x, tile_y = tile_to_screen(column, row)
love.graphics.draw(img_ground, tile_x, tile_y)
if tile_is_wall(column, row) then
love.graphics.draw(img_wall, tile_x, tile_y)
end
if tile_is_vine(column, row) then
love.graphics.draw(img_vines, tile_x, tile_y)
end
-- removed tile based pumpkin drawing
end
end
for i, pumpkin in ipairs(pumpkins) do
local tile_x, tile_y = tile_to_screen(pumpkin.column, pumpkin.row)
love.graphics.draw(img_pumpkin_sad, tile_x, tile_y)
end
local player_x, player_y = tile_to_screen(player_col, player_row)
love.graphics.draw(img_player, player_x, player_y)
endRunning the game at this point should look the same as in the last section, but now we’re drawing the pumpkins from the positions in the pumpkins array, instead of from the level_map.
Pushing Pumpkins#
The rules for pushing pumpkins are simple enough: a player can move onto a tile occupied by a pumpkin, which pushes the pumpkin into the next tile over, but only if the next tile over is not a wall, not off the map, and not already occupied by a pumpkin. Pushing multiple pumpkins in a line is not allowed.
Let’s modify player_can_move() to include a check to see if there is a pumpkin occupying the tile the player intends to move into. If there’s a pumpkin in the destination tile, we need to test the next tile over as well, to see if it is vacant.
First let’s split the out of bounds checks from player_can_move() into its own function out_of_bounds(), since we’ll want to use this same logic from multiple places.
function out_of_bounds(column, row)
if column < 1 or column > NUM_COLS then
return true
end
if row < 1 or row > NUM_ROWS then
return true
end
return false
endfunction player_can_move(move_x, move_y)
local new_col = player_col + move_x
local new_row = player_row + move_y
if out_of_bounds(new_col, new_row) then
return false
end
if tile_is_wall(new_col, new_row) then
return false
end
return true
endNow that the current positions of pumpkins are stored in pumpkins instead of the level map, we’ll need a new way to look them up. Let’s replace tile_is_pumpkin() with a new function get_pumpkin_at() which will return the individual pumpkin table if there is one found at the requested coordinates, or nil if there is no pumpkin there.
function get_pumpkin_at(column, row)
for i, pumpkin in ipairs(pumpkins) do
if pumpkin.column == column and pumpkin.row == row then
return pumpkin
end
end
endNext we write a function tile_blocks_pumpkin() to tell us if a pumpkin could move into a particular column and row. If that tile would be out of bounds, if it’s a wall, or if there is already a pumpkin in that location then it would block pumpkins from being pushed onto it.
function tile_blocks_pumpkin(column, row)
if out_of_bounds(column, row) then
return true
end
if tile_is_wall(column, row) then
return true
end
if get_pumpkin_at(column, row) then
return true
end
return false
endAnd then we incorporate these new checks into player_can_move() …
After determining there’s a pumpkin in the next tile over, we add move_x and move_y again to the new position, to get the next tile over, and call tile_blocks_pumpkin() to determine if we could push a pumpkin into it. If we can’t, then the player also can’t move in that direction.
function player_can_move(move_x, move_y)
local new_col = player_col + move_x
local new_row = player_row + move_y
if out_of_bounds(new_col, new_row) then
return false
end
if tile_is_wall(new_col, new_row) then
return false
end
if get_pumpkin_at(new_col, new_row) then
local next_col = new_col + move_x
local next_row = new_row + move_y
if tile_blocks_pumpkin(next_col, next_row) then
return false
end
end
return true
endFinally, we need to actually update the position of any pumpkin that we want to push.
In love.keypressed() once we’ve determined that the player can move into a particular tile and updated the player position accordingly, we’ll look up the pumpkin occupying that tile and, if there is one, push it over in the same direction.
Previously we’ve been treating the return value of get_pumpkin_at() as through it were a true/false value (either it contains a table, or nil) because all we cared about was if there was a pumpkin there or not. This time we’ll store the return value, which is the table containing the coordinates of the pumpkin, so that we can update its position.
Note that we aren’t checking if there is space to push the pumpkin over at this point, since we’ve already performed that check inside player_can_move(), so we’re relying on the result of that call being correct.
function love.keypressed( key, scancode, isrepeat )
print(string.format("love.keypressed %s, %s, %s", key, scancode, isrepeat))
local move_x, move_y = key_to_direction(key)
if move_x and move_y then
print(string.format(" player wants to move %d, %d", move_x, move_y))
if player_can_move(move_x, move_y) then
play_sound("footstep")
player_col = player_col + move_x
player_row = player_row + move_y
local pumpkin = get_pumpkin_at(player_col, player_row)
if pumpkin then
pumpkin.column = pumpkin.column + move_x
pumpkin.row = pumpkin.row + move_y
print(string.format(" pushed pumpkin to %d, %d", pumpkin.column, pumpkin.row))
end
else
play_sound("blocked")
print(" blocked!")
end
print(string.format(" player pos %d, %d", player_col, player_row))
end
endNow that the player can push pumpkins, we want to draw any pumpkin that is on a vine tile using the happy image instead of the sad one.
When drawing each pumpkin in love.draw() we can check tile_is_vine() to decide which image to draw:
function love.draw()
--- ...
for i, pumpkin in ipairs(pumpkins) do
local tile_x, tile_y = tile_to_screen(pumpkin.column, pumpkin.row)
local pumpkin_img = img_pumpkin_sad
if tile_is_vine(pumpkin.column, pumpkin.row) then
pumpkin_img = img_pumpkin_happy
end
love.graphics.draw(pumpkin_img, tile_x, tile_y)
end
--- ...
end
Win Condition#
Let’s recognize the player’s achievement if they manage to push all of the pumpkins onto a vine, winning the game.
Add this new sound to the sounds table:
local sounds = {
footstep = {
files = {
"sounds/footstep1.ogg",
"sounds/footstep2.ogg",
}
},
blocked = {
files = {
"sounds/blocked1.ogg",
"sounds/blocked2.ogg",
"sounds/blocked3.ogg",
}
},
win = {
files = {
"sounds/congratulations.ogg",
}
},
}We’ll write all_pumpkins_happy() which will loop through all of the pumpkins and check if all of them are on vine tiles. If we find one that isn’t, the player still has some work to do.
function all_pumpkins_happy()
for i, pumpkin in ipairs(pumpkins) do
if not tile_is_vine(pumpkin.column, pumpkin.row) then
return false
end
end
return true
endAfter processing a key press, check if all of the pumpkins are happy, and if they are then the player has won, time to play the win sound!
function love.keypressed( key, scancode, isrepeat )
-- ...
if all_pumpkins_happy() then
print(" player wins!")
play_sound("win")
end
end
Congratulations!
Code So Far
local TILE_SIZE = 64
local NUM_COLS = 10
local NUM_ROWS = 10
local player_col = 1
local player_row = 1
local pumpkins = {}
local sounds = {
footstep = {
files = {
"sounds/footstep1.ogg",
"sounds/footstep2.ogg",
}
},
blocked = {
files = {
"sounds/blocked1.ogg",
"sounds/blocked2.ogg",
"sounds/blocked3.ogg",
}
},
win = {
files = {
"sounds/congratulations.ogg",
}
},
}
local level_map = {
' ', 'w', 'w', 'w', 'w', 'w', ' ', ' ', ' ', ' ',
'w', 'w', ' ', ' ', 'v', 'w', 'w', 'w', 'w', 'w',
'w', ' ', ' ', 'g', ' ', ' ', ' ', ' ', ' ', 'w',
'w', ' ', ' ', ' ', ' ', 'p', ' ', 'p', 'w', 'w',
'w', ' ', ' ', ' ', 'w', 'w', ' ', ' ', 'w', ' ',
'w', 'v', ' ', ' ', ' ', 'w', 'w', ' ', 'w', 'w',
'w', ' ', 'p', ' ', ' ', ' ', ' ', ' ', 'v', 'w',
'w', 'w', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'w',
' ', 'w', 'w', ' ', ' ', 'w', 'w', 'w', ' ', 'w',
' ', ' ', 'w', 'w', 'w', 'w', ' ', 'w', 'w', 'w',
}
function love.load()
img_player = love.graphics.newImage("images/ghost.png")
img_ground = love.graphics.newImage("images/ground.png")
img_wall = love.graphics.newImage("images/wall.png")
img_pumpkin_sad = love.graphics.newImage("images/pumpkin_sad.png")
img_pumpkin_happy = love.graphics.newImage("images/pumpkin_happy.png")
img_vines = love.graphics.newImage("images/vines.png")
for name, sound in pairs(sounds) do
sound.sources = {}
for i, filename in ipairs(sound.files) do
local source = love.audio.newSource(filename, "static")
table.insert(sound.sources, source)
end
end
local start_col, start_row = find_map_coords('g')
if start_col and start_row then
player_col, player_row = start_col, start_row
else
print("could not find player start position in level map!")
end
for row = 1, NUM_ROWS do
for column = 1, NUM_COLS do
if get_tile_at(column, row) == 'p' then
local pumpkin = {}
pumpkin.column = column
pumpkin.row = row
print(string.format("loading pumpkin at (%d, %d)", pumpkin.column, pumpkin.row))
table.insert(pumpkins, pumpkin)
end
end
end
end
function love.draw()
love.graphics.clear(156/255, 172/255, 141/255)
for row = 1, NUM_ROWS do
for column = 1, NUM_COLS do
local tile_x, tile_y = tile_to_screen(column, row)
love.graphics.draw(img_ground, tile_x, tile_y)
if tile_is_wall(column, row) then
love.graphics.draw(img_wall, tile_x, tile_y)
end
if tile_is_vine(column, row) then
love.graphics.draw(img_vines, tile_x, tile_y)
end
end
end
for i, pumpkin in ipairs(pumpkins) do
local tile_x, tile_y = tile_to_screen(pumpkin.column, pumpkin.row)
local pumpkin_img = img_pumpkin_sad
if tile_is_vine(pumpkin.column, pumpkin.row) then
pumpkin_img = img_pumpkin_happy
end
love.graphics.draw(pumpkin_img, tile_x, tile_y)
end
local player_x, player_y = tile_to_screen(player_col, player_row)
love.graphics.draw(img_player, player_x, player_y)
end
function love.keypressed( key, scancode, isrepeat )
print(string.format("love.keypressed %s, %s, %s", key, scancode, isrepeat))
local move_x, move_y = key_to_direction(key)
if move_x and move_y then
print(string.format(" player wants to move %d, %d", move_x, move_y))
if player_can_move(move_x, move_y) then
play_sound("footstep")
player_col = player_col + move_x
player_row = player_row + move_y
local pumpkin = get_pumpkin_at(player_col, player_row)
if pumpkin then
pumpkin.column = pumpkin.column + move_x
pumpkin.row = pumpkin.row + move_y
print(string.format(" pushed pumpkin to %d, %d", pumpkin.column, pumpkin.row))
end
else
play_sound("blocked")
print(" blocked!")
end
print(string.format(" player pos %d, %d", player_col, player_row))
end
if all_pumpkins_happy() then
print(" player wins!")
play_sound("win")
end
end
function tile_to_screen(column, row)
local graphics_width, graphics_height = love.graphics.getDimensions()
local map_width = NUM_COLS * TILE_SIZE
local map_height = NUM_ROWS * TILE_SIZE
local offset_x = graphics_width/2 - map_width/2
local offset_y = graphics_height/2 - map_height/2
local screen_x = offset_x + (column - 1) * TILE_SIZE
local screen_y = offset_y + (row - 1) * TILE_SIZE
return screen_x, screen_y
end
function play_sound(name)
local effect = sounds[name]
local rnd_index = math.random(#effect.sources)
local source = effect.sources[rnd_index]
love.audio.play(source)
end
function tile_is_wall(column, row)
return get_tile_at(column, row) == 'w'
end
function get_pumpkin_at(column, row)
for i, pumpkin in ipairs(pumpkins) do
if pumpkin.column == column and pumpkin.row == row then
return pumpkin
end
end
end
function tile_is_vine(column, row)
return get_tile_at(column, row) == 'v'
end
function find_map_coords(character)
for row = 1, NUM_ROWS do
for column = 1, NUM_COLS do
if get_tile_at(column, row) == character then
return column, row
end
end
end
end
function key_to_direction(key)
if key == "a" or key == "left" then
return -1, 0
elseif key == "d" or key == "right" then
return 1, 0
elseif key == "w" or key == "up" then
return 0, -1
elseif key == "s" or key == "down" then
return 0, 1
end
end
function player_can_move(move_x, move_y)
local new_col = player_col + move_x
local new_row = player_row + move_y
if out_of_bounds(new_col, new_row) then
return false
end
if tile_is_wall(new_col, new_row) then
return false
end
if get_pumpkin_at(new_col, new_row) then
local next_col = new_col + move_x
local next_row = new_row + move_y
if tile_blocks_pumpkin(next_col, next_row) then
return false
end
end
return true
end
function get_tile_at(column, row)
local tile_index = column + (row - 1) * NUM_COLS
return level_map[tile_index]
end
function out_of_bounds(column, row)
if column < 1 or column > NUM_COLS then
return true
end
if row < 1 or row > NUM_ROWS then
return true
end
return false
end
function tile_blocks_pumpkin(column, row)
if out_of_bounds(column, row) then
return true
end
if tile_is_wall(column, row) then
return true
end
if get_pumpkin_at(column, row) then
return true
end
return false
end
function all_pumpkins_happy()
for i, pumpkin in ipairs(pumpkins) do
if not tile_is_vine(pumpkin.column, pumpkin.row) then
return false
end
end
return true
endPolish#
Of course there are a lot of things that we could add from this point to make the game more complete and like a “real game”, but let’s take a look at a couple of simple things to polish up.
Game Over#
First off, as it stands, the player can keep interacting with the game even after they’ve won. Keep pressing keys and you’ll keep being congratulated, which is nice but a bit unusual. You can also push the pumpkins off their vines and un-win the game.
We can prevent this by keeping track of whether the game is over and preventing any further moves.
Create a variable called game_over at the top of the file to track whether the game has ended or not.
local game_over = falseAnd make a couple of changes to love.keypressed().
First check if game_over is true at the top of the function before we handle the keyboard input, so if the game is over, we just return without doing anything else.
Then when we determine the player has won, we can set game_over to true, ending the game.
function love.keypressed( key, scancode, isrepeat )
print(string.format("love.keypressed %s, %s, %s", key, scancode, isrepeat))
if game_over then
return
end
-- ...
if all_pumpkins_happy() then
print(" player wins!")
game_over = true
play_sound("win")
end
endRestarting#
If the player wants to play again, or if they get into an unwinnable situation such as pushing a pumpkin into a corner, it would be nice to allow them to start over without fully exiting and re-launching the game.
We’ll take the code we had in love.load() that sets the position of the player and the pumpkins, and move that out into a separate function reset_game(). Then we can call reset_game() when the game loads, and also when the player wants to play again to put everything back the way it started.
This is another reason it’s useful to not modify the level_map directly, we still have all of the original map data to work with.
function love.load()
img_player = love.graphics.newImage("images/ghost.png")
img_ground = love.graphics.newImage("images/ground.png")
img_wall = love.graphics.newImage("images/wall.png")
img_pumpkin_sad = love.graphics.newImage("images/pumpkin_sad.png")
img_pumpkin_happy = love.graphics.newImage("images/pumpkin_happy.png")
img_vines = love.graphics.newImage("images/vines.png")
for name, sound in pairs(sounds) do
sound.sources = {}
for i, filename in ipairs(sound.files) do
local source = love.audio.newSource(filename, "static")
table.insert(sound.sources, source)
end
end
reset_game()
endfunction reset_game()
print("resetting game state")
game_over = false
pumpkins = {}
local start_col, start_row = find_map_coords('g')
if start_col and start_row then
player_col, player_row = start_col, start_row
else
print("could not find player start position in level map!")
end
for row = 1, NUM_ROWS do
for column = 1, NUM_COLS do
if get_tile_at(column, row) == 'p' then
local pumpkin = {}
pumpkin.column = column
pumpkin.row = row
print(string.format("loading pumpkin at (%d, %d)", pumpkin.column, pumpkin.row))
table.insert(pumpkins, pumpkin)
end
end
end
endNote that in addition to the code we moved from love.load(), we also set game_over to false in case the player had already won, and we’ll clear the pumpkins table by assigning it to a new empty table, which we then re-populate from the map.
Now we can also call reset_game() when the player presses the R key at any time to replay the level from the start.
function love.keypressed( key, scancode, isrepeat )
print(string.format("love.keypressed %s, %s, %s", key, scancode, isrepeat))
if key == "r" then
reset_game()
return
end
-- ...
endExiting#
We can also allow the player to exit the game at any time by pressing the Esc key, in addition to using the mouse to close the window (or using a system hotkey like Alt-F4).
function love.keypressed( key, scancode, isrepeat )
print(string.format("love.keypressed %s, %s, %s", key, scancode, isrepeat))
if key == "escape" then
print("quitting!")
love.event.quit()
return
end
-- ...
endAnd there we have it, a pretty functional Sokoban clone!
Code So Far
local TILE_SIZE = 64
local NUM_COLS = 10
local NUM_ROWS = 10
local player_col = 1
local player_row = 1
local game_over = false
local pumpkins = {}
local sounds = {
footstep = {
files = {
"sounds/footstep1.ogg",
"sounds/footstep2.ogg",
}
},
blocked = {
files = {
"sounds/blocked1.ogg",
"sounds/blocked2.ogg",
"sounds/blocked3.ogg",
}
},
win = {
files = {
"sounds/congratulations.ogg",
}
},
}
local level_map = {
' ', 'w', 'w', 'w', 'w', 'w', ' ', ' ', ' ', ' ',
'w', 'w', ' ', ' ', 'v', 'w', 'w', 'w', 'w', 'w',
'w', ' ', ' ', 'g', ' ', ' ', ' ', ' ', ' ', 'w',
'w', ' ', ' ', ' ', ' ', 'p', ' ', 'p', 'w', 'w',
'w', ' ', ' ', ' ', 'w', 'w', ' ', ' ', 'w', ' ',
'w', 'v', ' ', ' ', ' ', 'w', 'w', ' ', 'w', 'w',
'w', ' ', 'p', ' ', ' ', ' ', ' ', ' ', 'v', 'w',
'w', 'w', ' ', ' ', ' ', ' ', ' ', ' ', ' ', 'w',
' ', 'w', 'w', ' ', ' ', 'w', 'w', 'w', ' ', 'w',
' ', ' ', 'w', 'w', 'w', 'w', ' ', 'w', 'w', 'w',
}
function love.load()
img_player = love.graphics.newImage("images/ghost.png")
img_ground = love.graphics.newImage("images/ground.png")
img_wall = love.graphics.newImage("images/wall.png")
img_pumpkin_sad = love.graphics.newImage("images/pumpkin_sad.png")
img_pumpkin_happy = love.graphics.newImage("images/pumpkin_happy.png")
img_vines = love.graphics.newImage("images/vines.png")
for name, sound in pairs(sounds) do
sound.sources = {}
for i, filename in ipairs(sound.files) do
local source = love.audio.newSource(filename, "static")
table.insert(sound.sources, source)
end
end
reset_game()
end
function reset_game()
print("resetting game state")
game_over = false
pumpkins = {}
local start_col, start_row = find_map_coords('g')
if start_col and start_row then
player_col, player_row = start_col, start_row
else
print("could not find player start position in level map!")
end
for row = 1, NUM_ROWS do
for column = 1, NUM_COLS do
if get_tile_at(column, row) == 'p' then
local pumpkin = {}
pumpkin.column = column
pumpkin.row = row
print(string.format("loading pumpkin at (%d, %d)", pumpkin.column, pumpkin.row))
table.insert(pumpkins, pumpkin)
end
end
end
end
function love.draw()
love.graphics.clear(156/255, 172/255, 141/255)
for row = 1, NUM_ROWS do
for column = 1, NUM_COLS do
local tile_x, tile_y = tile_to_screen(column, row)
love.graphics.draw(img_ground, tile_x, tile_y)
if tile_is_wall(column, row) then
love.graphics.draw(img_wall, tile_x, tile_y)
end
if tile_is_vine(column, row) then
love.graphics.draw(img_vines, tile_x, tile_y)
end
end
end
for i, pumpkin in ipairs(pumpkins) do
local tile_x, tile_y = tile_to_screen(pumpkin.column, pumpkin.row)
local pumpkin_img = img_pumpkin_sad
if tile_is_vine(pumpkin.column, pumpkin.row) then
pumpkin_img = img_pumpkin_happy
end
love.graphics.draw(pumpkin_img, tile_x, tile_y)
end
local player_x, player_y = tile_to_screen(player_col, player_row)
love.graphics.draw(img_player, player_x, player_y)
end
function love.keypressed( key, scancode, isrepeat )
print(string.format("love.keypressed %s, %s, %s", key, scancode, isrepeat))
if key == "escape" then
print("quitting!")
love.event.quit()
return
end
if key == "r" then
reset_game()
return
end
if game_over then
return
end
local move_x, move_y = key_to_direction(key)
if move_x and move_y then
print(string.format(" player wants to move %d, %d", move_x, move_y))
if player_can_move(move_x, move_y) then
play_sound("footstep")
player_col = player_col + move_x
player_row = player_row + move_y
local pumpkin = get_pumpkin_at(player_col, player_row)
if pumpkin then
pumpkin.column = pumpkin.column + move_x
pumpkin.row = pumpkin.row + move_y
print(string.format(" pushed pumpkin to %d, %d", pumpkin.column, pumpkin.row))
end
else
play_sound("blocked")
print(" blocked!")
end
print(string.format(" player pos %d, %d", player_col, player_row))
end
if all_pumpkins_happy() then
print(" player wins!")
game_over = true
play_sound("win")
end
end
function tile_to_screen(column, row)
local graphics_width, graphics_height = love.graphics.getDimensions()
local map_width = NUM_COLS * TILE_SIZE
local map_height = NUM_ROWS * TILE_SIZE
local offset_x = graphics_width/2 - map_width/2
local offset_y = graphics_height/2 - map_height/2
local screen_x = offset_x + (column - 1) * TILE_SIZE
local screen_y = offset_y + (row - 1) * TILE_SIZE
return screen_x, screen_y
end
function play_sound(name)
local effect = sounds[name]
local rnd_index = math.random(#effect.sources)
local source = effect.sources[rnd_index]
love.audio.play(source)
end
function tile_is_wall(column, row)
return get_tile_at(column, row) == 'w'
end
function get_pumpkin_at(column, row)
for i, pumpkin in ipairs(pumpkins) do
if pumpkin.column == column and pumpkin.row == row then
return pumpkin
end
end
end
function tile_is_vine(column, row)
return get_tile_at(column, row) == 'v'
end
function find_map_coords(character)
for row = 1, NUM_ROWS do
for column = 1, NUM_COLS do
if get_tile_at(column, row) == character then
return column, row
end
end
end
end
function key_to_direction(key)
if key == "a" or key == "left" then
return -1, 0
elseif key == "d" or key == "right" then
return 1, 0
elseif key == "w" or key == "up" then
return 0, -1
elseif key == "s" or key == "down" then
return 0, 1
end
end
function player_can_move(move_x, move_y)
local new_col = player_col + move_x
local new_row = player_row + move_y
if out_of_bounds(new_col, new_row) then
return false
end
if tile_is_wall(new_col, new_row) then
return false
end
if get_pumpkin_at(new_col, new_row) then
local next_col = new_col + move_x
local next_row = new_row + move_y
if tile_blocks_pumpkin(next_col, next_row) then
return false
end
end
return true
end
function get_tile_at(column, row)
local tile_index = column + (row - 1) * NUM_COLS
return level_map[tile_index]
end
function out_of_bounds(column, row)
if column < 1 or column > NUM_COLS then
return true
end
if row < 1 or row > NUM_ROWS then
return true
end
return false
end
function tile_blocks_pumpkin(column, row)
if out_of_bounds(column, row) then
return true
end
if tile_is_wall(column, row) then
return true
end
if get_pumpkin_at(column, row) then
return true
end
return false
end
function all_pumpkins_happy()
for i, pumpkin in ipairs(pumpkins) do
if not tile_is_vine(pumpkin.column, pumpkin.row) then
return false
end
end
return true
end
wall.png
pumpkin_sad.png
pumpkin_happy.png
vines.png