-
-
Notifications
You must be signed in to change notification settings - Fork 564
Level Generation꞉ Drunk Walk
Lately I've been interested (<--read "obsessed") with procedural level generation. A method of creating levels where instead of spending time building game levels yourself, you spend time teaching the software to build them for you.
It actually started when I had an idea for a puzzle runner that needed unlimited, random levels. sort of like a bigger, badder version of Push-em
well pretty soon the game became a side project, and writing level generators took precedence!
Here's my first, and simplest generator.

It's an algorithm I found called the "Drunk Walk" algorithm. (for obvious reasons)
Here's how it works: fill the level with walls spawn a bot in the area bot walks randomly without leaving the level wherever the bot walks "carve" pathways stop when the desired amount of floor space is reached eliminate unnecessary wall blocks
Now let's look at how to make each step happen
To do this we need to iterate over every cell in the level. I like to log the level area into variables for flexibility:
MINX=0 --start of x cells
MAXX=29 --end of x cells
MINY=0 --start of y cells
MAXY=16 --end of y cellsin this way you can scale your map just by changing these values
now we iterate by using "nested for loops":
--for every x cell
for x=MINX,MAXX do
--for every y cell
for y=MINY,MAXY do
--set the current cell to wall
mset(x,y,WALL)
end
end"WALL" is just a variable with the id for the wall sprite.
Spawning a bot in a random spot is super simple. First let's create an easy way to get random numbers:
function r(a,b)
return math.random(a,b)
endNow we just randomize the bot's (x,y) when we initialize him:
bot={
x=r(MINX+2,MAXX-2)
y=r(MINY+2,MAXX-2)
ld=nil} --last direction...don't worry about it right nowNotice we added added a 2 cell buffer around the border to keep the bot from spawning too close to the edge
To do this we need four things: a table of directions a function for retrieving a random direction define what "leaving the board" means move the bot without "leaving the board"
TABLE OF DIRECTIONS
DIRS={
{x= 0,y=-1},--up
{x= 0,y= 1},--d0wn
{x=-1,y= 0},--left
{x= 1,y= 0},--right
{x=-1,y= 0},--left
{x= 1,y= 0}}--right
--adding left and right twice makes the
--bot more likely to move sideways
--resulting in a level that fits better
--in the screenFUNCTION TO RETRIEVE RANDOM DIRECTION
function get_dir()
local td=DIRS[r(1,6)]
if bot.ld==nil then
return td
else
if bot.ld==td then
get_dir()
else
return td
end
end
endHere I decided to not let the bot pick the same direction he just came from. this makes the bot move around the level instead of dancing back and forth between two tiles
DEFINE "LEAVING THE BOARD"
--check if the move is within the board
--boundaries
function move_is_valid(d)
if bot.x+d.x>MAXX-2 or bot.x+d.x<MINX+1
or bot.y+d.y>MAXY-2 or bot.y+d.y<MINY+1 then
return false
else
return true
end
endSo we can feed the bot's current direction into the function, and it tests if the next cell is out of bounds. From there it returns TRUE or FALSE so we can use it for "if" statement testing.
MOVE THE BOT WITHOUT LEAVING THE BOARD
function move()
--make it 1 in 4 chance for the bot
--to travel in the same direction
if r(1,2)==r(1,2) or d==nil then
d=get_dir()
end
if move_is_valid(d) then
bot.d=d
bot.x=bot.x+d.x
bot.y=bot.y+d.y
end
endNotice that I made it more likely for the bot to keep moving in his current direction than to change directions. If you leave this out the levels turn out more open with less walkways. Also see how I used the move_is_valid() function.
to do this we need to add a single line of code into the bot's move() function:
mset(bot.x,bot.y,FLOOR)We'll put this in before we make him move. So he sets the map cell he's standing on to floor. If he walks over the same cell twice, it's already set to floor so setting it again does nothing. Here's the updated move() function:
function move()
mset(bot.x,bot.y,FLOOR)
--make it 1 in 2 chance for the bot
--to travel in the same direction
if r(1,2)==r(1,2) or d==nil then
d=get_dir()
end
if move_is_valid(d) then
bot.d=d
bot.x=bot.x+d.x
bot.y=bot.y+d.y
end
endTo do this we will make a global variable for the amount of floor tiles we want to end up with:
MAX_FLOORS=200 --how many tiles in the levelNow we can iterate over every cell and count how many are floor tiles:
function count()
local temp=0
for x=MINX,MAXX do
for y=MINY,MAXY do
if mget(x,y)==FLOOR then
temp=temp+1
end
end
end
floors=temp
endEverytime this is called it sets temp to 0, and increments it for every floor tile. Once it's checked every cell it assigns temp to floors. This way we can test how many floor tiles are in the level.
The point of this is more or less asthetic. It'll make a clean perimeter by removing the extra wall blocks. The way we do this is pretty simple, once again we iterate over every cell, this time we test if it has any floor tiles immediatly around it. If it does than it's either a floor tile, or a perimeter wall. any cell that doesn't meet this criteria is set to empty space.
function wallbreak()
for x=MINX,MAXX do
for y=MINY,MAXY do
--check adjacent cells
if mget(x-1,y+0)~=FLOOR and --left
mget(x+1,y+0)~=FLOOR and --right
mget(x+0,y+1)~=FLOOR and --bottom
mget(x+0,y-1)~=FLOOR and --top
--also check diagonals
mget(x-1,y-1)~=FLOOR and --top-left
mget(x+1,y-1)~=FLOOR and --top-right
mget(x-1,y+1)~=FLOOR and --bottom-left
mget(x+1,y+1)~=FLOOR then--bottom-right
--set the cell to empty
mset(x,y,0)
end
end
end
endLast we write a function that bundles all of these steps together.
function build_level()
::redo::
move()
count()
if floors<MAX_FLOORS then
goto redo
end
mode="initializing"
endYou can see that as long as the floor count is less than we asked for, it will continue to loop over move() and count(). Be careful about asking for more floor tiles than is in the level because then the bot will be doomed to wander aimlessly for eternity! forever doing a task he can never complete!! OH YOU EVIL OVERLORD!! (plus it'll just crash TIC-80)
After the level is complete, the mode is changed to "initializing". This is the step where we clean up the walls and add anything we need to before letting the player start. (coins, enemyies, etc...)
That's is all the steps involved. Here's the completed code to demonstrate how they're used.
-- title: D-Walk
-- author: Bear Thorne
-- desc: drunk walk algorithm
-- script: lua
---------------------------------------
---beginning of the generator script---
---------------------------------------
--sprite constants
FLOOR=1
WALL=17
DUDE=33
MINX=0
MAXX=29
MINY=0
MAXY=16
MAX_FLOORS=200 --how many tiles in the level
--directions
DIRS={
{x= 0,y=-1},--up
{x= 0,y= 1},--d0wn
{x=-1,y= 0},--left
{x= 1,y= 0},--right
{x=-1,y= 0},--left
{x= 1,y= 0}}--right
--adding left and right twice makes the
--bot more likely to move sideways
--resulting in a level that fits better
--in the screen
function init()
mode="generating"
t=0
bot={
x=r(MINX+2,MAXX-2),
y=r(MINY+2,MAXY-2),
ld=nil}
floors=0
--set the level to all walls
for x=MINX,MAXX do
for y=MINY,MAXY do
mset(x,y,WALL)
end
end
end
--random number shorthand
function r(a,b)
return math.random(a,b)
end
--get a direction from the DIRS table
function get_dir()
local td=DIRS[r(1,6)]
if bot.ld==nil then
return td
else
if bot.ld==td then
get_dir()
else
return td
end
end
end
--check if the move is within the board
--boundaries
function move_is_valid(d)
if bot.x+d.x>MAXX-2 or bot.x+d.x<MINX+1
or bot.y+d.y>MAXY-2 or bot.y+d.y<MINY+1 then
return false
else
return true
end
end
--move the carving bot
function move()
mset(bot.x,bot.y,FLOOR)
--make it 1 in 2 chance for the bot
--to travel in the same direction
if r(1,2)==r(1,2) or d==nil then
d=get_dir()
end
if move_is_valid(d) then
bot.d=d
bot.x=bot.x+d.x
bot.y=bot.y+d.y
end
end
--count the floor tiles in the level
function count()
local temp=0
for x=MINX,MAXX do
for y=MINY,MAXY do
if mget(x,y)==FLOOR then
temp=temp+1
end
end
end
floors=temp
end
--remove large blocks of walls to make
--a clean perimeter
function wallbreak()
for x=MINX,MAXX do
for y=MINY,MAXY do
--check adjacent cells
if mget(x-1,y+0)~=FLOOR and --left
mget(x+1,y+0)~=FLOOR and --right
mget(x+0,y+1)~=FLOOR and --bottom
mget(x+0,y-1)~=FLOOR and --top
--also check diagonals
mget(x-1,y-1)~=FLOOR and --top-left
mget(x+1,y-1)~=FLOOR and --top-right
mget(x-1,y+1)~=FLOOR and --bottom-left
mget(x+1,y+1)~=FLOOR then--bottom-right
--set the cell to empty
mset(x,y,0)
end
end
end
end
function build_level()
::redo::
move()
count()
if floors<MAX_FLOORS then
goto redo
end
mode="initializing"
end
--------------------------
--end of generation code--
--------------------------
--update the onscreen graphics
function draw()
cls()
map(MINX,MINY,MAXX+1,MAXY+1)
end
init()
function TIC()
if mode=="generating" then
--level generation code
build_level()
elseif mode=="initializing" then
--level initializing code
wallbreak()
mode="game"
elseif mode=="game" then
--game code here--
draw()
end
t=t+1
endAs great as this generator turned out, it sadly didn't build suitable levels for my game...So instead I've started making a dungeon crawler built around it!...I'll link to it here when it's done! And the search for that perfect Level Generator continues!!!
Please fell free to use any portion of this code in you own projects! Just throw a credit to me at the top of your file and we'll be cool. :)
if you have questions or requests for future tutorials, email me at bearknucklesketching@gmail.com
TIC-80 tiny computer https://tic80.com | Twitter | Telegram | Terms
Built-in Editors
Console
Platform
RAM & VRAM | Display | Palette | Bits per Pixel (BPP) |
.ticFormat | Supported Languages
Other
Tutorials | Code Snippets | Libraries | External Tools | FFT
API
- BDR (0.90)
- BOOT (1.0)
- MENU
- OVR (deprecated)
- SCN (deprecated)
- TIC
- btn & btnp
- circ & circb
- clip
- cls
- elli & ellib (0.90)
- exit
- fget & fset (0.80)
- font
- key & keyp
- line
- map
- memcpy & memset
- mget & mset
- mouse
- music
- peek, peek4
- peek1, peek2 (1.0)
- pix
- pmem
- poke, poke4
- poke1, poke2 (1.0)
- rect & rectb
- reset
- sfx
- spr
- sync
- ttri (1.0)
- time
- trace
- tri & trib (0.90)
- tstamp (0.80)
- vbank (1.0)