Tiny N level generator
Posted: 2009.04.10 (17:43)
So I wanted to make the smallest N level generator I possibly could, and this is what I came up with. (The goal was actually to reach 140 characters, but 190 is as low as I could go.) The maps put out have gold, mines and a ninja inside a fairly random tileset, but the tiles and mines are almost always spread out enough to get some a good gameplay experience out of them. The generator uses some of the concepts I found in the Legacy pack stats released at the start of the year: The tiles are 60% empty, 30% solid, 10% other; The gold and mine counts match the Legacy averages of about 15 mines and 30 gold per level (plus extras to make up for the ones places under tiles and just to fill out the levels).
While none of the levels are going to be amazing, or inspired, or beautiful, the fact that they're playable at all goes to show how versatile a game N is. Anyway, here's the Python 2.5 code and a bunch of sample levels.
Generate Now
(Refresh the page for more levels)
THE TECHNICAL DESCRIPTION
-----
Here's the code expanded to a (more) readable form.
First we import the random module and rename a few functions to save space. The range function (now just n() ) creates a set of consecutive numbers. n(5) makes a set five numbers long, [0, 1, 2, 3, 4]. n(4,8) does the same starting at 4, [4, 5, 6, 7]. The choice function from the random module (now just c() ) chooses a random object from a set. So c(deck_of_cards) would choose a random card out of the deck.
If you've never seen a Python list comprehension, it's basically a for loop compressed into a single line. This line is the same as writing:
t = []
for i in range(713):
. . t.append(c('001' * 12 + '2345'))
Er, and for even *further* clarity, that up there is a loop that runs 713 times, and each time appends a certain object to the end of the list t. That object happens to be an N tile, but let's look at how it's chosen.
Remember that the c() function is random choice. The set passed in to choose from is just a string, but it's in a strange form to save space. '001' * 12 + '2345'. In Python, multiplying a string duplicates it that many times. 'a' * 5 = 'aaaaa'. So this string is '001' * 12 + '2345' = '0010010010010010010010010010010010012345'. Those characters just so happen to be N tiles in a ratio of 60% empty (0), 30% solid (1), and 10% other (2,3,4,5). So the choice function grabs one of those tiles at random each time through the loop and, assuming perfect randomness, chooses the Legacy ratio of tiles until a full tileset is created.
Now that the tileset is done, we add the | and a ninja. The ninja is at the exact center of the map, pretty much just for familiarity. I experimented with putting the ninja in a corner, but nearly every map was unplayable because the ninja locked in place with tiles.
You'll have to imagine how much this line drives me crazy. It's such a long line, and when every character removed means your program is 5% shorter, long lines stick out. Anyway, here's what the line does and what I've done to keep it short.
First of all, this creates the N objects. You already know list comprehensions a bit from earlier, so you'll see that this loops 63 times to create 63 objects. This number was derived from the Legacy stats to give about 42 gold and 21 mines (some of which will be lost under tiles).
So this is a formatted string. Formatted strings take the form of '%s' % (string) and have several cool features, none of which I use here. But I'll explain why it saves space in a minute anyway. In the string part, you'll recognize some N data characters ' !__^__,__' with %s placeholders in each of the number slots. The first placeholder is the object's type, so c([12,0,0]) chooses either a 12 or a 0 (mine or a gold), in a one mine to two gold ratio.
The next two placeholders are the x- and y-coordinates. The functions that fill them are a bit complicated. First, what it does as is: choice(range(number))*24. The range in the middle creates a list of numbers from 1 to 31 for width and 1 to 23 for height to choose from, and then the choice function picks one of each to form the position of the gold or mine. Now, 31x23 is the tileset dimensions, so we then multiply the number by 24 to convert from tileset coordinates to object coordinates. Beautiful. These objects are always full-snapped and on the edges of tiles, mostly to get mines at the sides of chimneys to make levels more playable.
Some technical things I did and didn't do to save space: First, I know what you're thinking. "But LV, '!'+x+'^'+y+','+z is two characters shorter than '!%s^%s,%s'%(x,y,z)," to which I reply "NO. COULD NOT CONCAT INT WITH STRING." You can't add an integer to a string in Python, but the formatted string automatically converts integer inputs to strings. This saved me from several str() operations. Second, choice(range(number)) is a strange way to get a random integer within an interval. In fact, the random module has randrange() and even randint() functions, but I went this route to avoid importing them. I already spent an annoying amount of characters just importing the one function, let alone two or three. Third, the range function (n() to us) has support for jumping over intervals, so I shouldn't have to generate small numbers and multiply them by 24. I could use n(24,744,24) to get a list [24,48,72, ... ,744] and have the same randomization as I have now. But notice that 24 and 744 are each one digit longer than 1 and 31! I saved 4 characters by choosing from small numbers and multiplying after.
Anyway. Moving on. Character #356 in the mapdata happens to be exact center of the tileset, right where the ninja was placed. We set this tile to empty so the player never starts out embedded in a tile. (If we didn't do this, the player would start in a tile 40% of the time. Terrible!)
Eagle-eyed programmers will have noticed that we've been working with a list/array the whole time, not a string. So we have to get this data into string form for it to be any use. ''.join() joins a list into a string. So ['0','0','1','2','0','|'] becomes just '00120|'. And with that, we have a finished N level, joined and printed for consumption.
While none of the levels are going to be amazing, or inspired, or beautiful, the fact that they're playable at all goes to show how versatile a game N is. Anyway, here's the Python 2.5 code and a bunch of sample levels.
Generate Now
(Refresh the page for more levels)
Code: Select all
import random as r;n,c=range,r.choice;t=[c('001'*12+'2345') for i in n(713)]+['|5^396,300']+['!%s^%s,%s'%(c([12,0,0]),c(n(1,32))*24,c(n(1,24))*24) for i in n(63)];t[356]='0';print ''.join(t)
THE TECHNICAL DESCRIPTION
-----
Code: Select all
import random as r;
n, c = range, r.choice;
t = [c('001'*12 + '2345') for i in n(713)] + \
['|5^396,300'] + \
['!%s^%s,%s' % (c([12,0,0]), c(n(1,32))*24, c(n(1,24))*24) for i in n(63)]
t[356] = '0'
print ''.join(t)
Code: Select all
import random as r;
n, c = range, r.choice;
Code: Select all
t = [c('001'*12 + '2345') for i in n(713)] + \
t = []
for i in range(713):
. . t.append(c('001' * 12 + '2345'))
Er, and for even *further* clarity, that up there is a loop that runs 713 times, and each time appends a certain object to the end of the list t. That object happens to be an N tile, but let's look at how it's chosen.
Code: Select all
c('001'*12 + '2345')
Code: Select all
['|5^396,300'] + \
Code: Select all
['!%s^%s,%s' % (c([12,0,0]), c(n(1,32))*24, c(n(1,24))*24) for i in n(63)]
First of all, this creates the N objects. You already know list comprehensions a bit from earlier, so you'll see that this loops 63 times to create 63 objects. This number was derived from the Legacy stats to give about 42 gold and 21 mines (some of which will be lost under tiles).
Code: Select all
'!%s^%s,%s' % (c([12,0,0]), c(n(1,32))*24, c(n(1,24))*24)
The next two placeholders are the x- and y-coordinates. The functions that fill them are a bit complicated. First, what it does as is: choice(range(number))*24. The range in the middle creates a list of numbers from 1 to 31 for width and 1 to 23 for height to choose from, and then the choice function picks one of each to form the position of the gold or mine. Now, 31x23 is the tileset dimensions, so we then multiply the number by 24 to convert from tileset coordinates to object coordinates. Beautiful. These objects are always full-snapped and on the edges of tiles, mostly to get mines at the sides of chimneys to make levels more playable.
Some technical things I did and didn't do to save space: First, I know what you're thinking. "But LV, '!'+x+'^'+y+','+z is two characters shorter than '!%s^%s,%s'%(x,y,z)," to which I reply "NO. COULD NOT CONCAT INT WITH STRING." You can't add an integer to a string in Python, but the formatted string automatically converts integer inputs to strings. This saved me from several str() operations. Second, choice(range(number)) is a strange way to get a random integer within an interval. In fact, the random module has randrange() and even randint() functions, but I went this route to avoid importing them. I already spent an annoying amount of characters just importing the one function, let alone two or three. Third, the range function (n() to us) has support for jumping over intervals, so I shouldn't have to generate small numbers and multiply them by 24. I could use n(24,744,24) to get a list [24,48,72, ... ,744] and have the same randomization as I have now. But notice that 24 and 744 are each one digit longer than 1 and 31! I saved 4 characters by choosing from small numbers and multiplying after.
Code: Select all
t[356] = '0'
Code: Select all
print ''.join(t)