Page 1 of 1

Tiny N level generator

Posted: 2009.04.10 (17:43)
by LittleViking
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)

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)
Here's the code expanded to a (more) readable form.

Code: Select all

import random as r;
n, c = range, r.choice;
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.

Code: Select all

t = [c('001'*12 + '2345') for i in n(713)] + \
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.

Code: Select all

c('001'*12 + '2345')
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.

Code: Select all

['|5^396,300'] + \
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.

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

Code: Select all

'!%s^%s,%s' % (c([12,0,0]), c(n(1,32))*24, c(n(1,24))*24)
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.

Code: Select all

t[356] = '0'
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!)

Code: Select all

print ''.join(t)
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.

Re: Tiny N level generator

Posted: 2009.04.10 (18:39)
by sidke
Word.
:D

Re: Tiny N level generator

Posted: 2009.04.10 (18:44)
by toasters
I'd like to know how the code works.

Re: Tiny N level generator

Posted: 2009.04.10 (19:27)
by unoriginal name
I don't like you anymore. These maps are too much better than mine. ;-;

Re: Tiny N level generator

Posted: 2009.04.10 (20:30)
by T3chno
Ahah. These sample maps are better than 95% of whats submitted to NUMA these days.

Re: Tiny N level generator

Posted: 2009.04.10 (22:08)
by aids
Ha, interpreted the title as Tiny N Level Generator, not Tiny N Level Generator. I was expecting to see some 3X3 and 4X4 maps. If you can make a generator for that, you have earned my applause.

To me, this code is completely a novelty, albeit a usable one. Good job.

Re: Tiny N level generator

Posted: 2009.04.11 (11:40)
by EdoI
Actually, a huge applause from me could be earned by improving that N Level Generator (TheRealN.com/nlevel). This is... cool, but I don't think I'll use it ever again.

Re: Tiny N level generator

Posted: 2009.04.12 (02:33)
by otters
Techno wrote:Ahah. These sample maps are better than 95% of whats submitted to NUMA these days.
Correction: this sample map.

Re: Tiny N level generator

Posted: 2009.04.12 (23:50)
by blackbelmoral
i loved them :)
they make me hope some n00bs discoer this and make better maps!

Re: Tiny N level generator

Posted: 2009.04.19 (20:52)
by LittleViking
This script is on the server now, by the way, so it'll be much easier for anyone who wants a level to generate one. It's the same 190-char script, unedited.

http://therealn.com/cgi-bin/190level.py

Re: Tiny N level generator

Posted: 2009.09.02 (08:20)
by scythe
http://twitter.com/_scythe/status/3707614295
139 characters, in lua. Not the prettiest levels ever, but they're kinda playable.

Re: Tiny N level generator

Posted: 2009.09.03 (05:13)
by smartalco
o______O

Winner.

Re: Tiny N level generator

Posted: 2009.09.03 (14:04)
by Tunco
You're too good.

Good for you. I don't think will use this every again, but hey, this was nice.

Re: Tiny N level generator

Posted: 2009.10.05 (01:44)
by Yoshimo
My rendition, from scratch. I could hardly tell what you were doing, anyways, plus this one is a ton more random. The one thing I could see is that you used a multi-digit string once ro twice for tiles. Not here.

Code: Select all

def tiles():
	import random
	x = (['0']*428)+(['1']*214)+(['G', 'F', 'I', 'H', '?', '>', 'A', '@']*9)
	random.shuffle(x)
	x += '|5^396,300'
	ite = 0
	while ite <= 30:
		ite += 1
		y = (random.randint(1, 23))*24+24
		z = (random.randint(1, 31))*24+24
		mines = ('!12^'+str(y)+','+str(z))
		"".join(mines)
		x += mines
	ite2 = 0
	while ite2 <= 40:
		ite2 +=1
		a = (random.randint(1, 23))*24+24
		b = (random.randint(1, 31))*24+24
		gold = ('!0^'+str(a)+','+str(b))
		"".join(gold)
		x += gold
	print(''.join(x))
Yay!

Re: Tiny N level generator

Posted: 2009.10.05 (18:37)
by scythe
http://twitter.com/_scythe/status/4633388829
http://www.nmaps.net/182579
139 characters again, thanks to short-circuit evaluation. Now with gauss turrets!

Anyway, I'll do what I can to explain what's going on here:

Code: Select all

r,t=math.random,"|5^96,9";
a,b=c,d really means a = c and b = d, so we have r = math.random and t = "|5^96,9". In the first case, we can do this because lua supports first-class functions; that is to say that functions are objects (similar to Ruby). t, of course, holds the ninja's position: we'll eventually concatenate "0," onto this, putting the ninja at 96, 90.

Code: Select all

for i=1,713 do 
Hopefully self-explanatory. 'do' is one of lua's little annoyances.

Code: Select all

t=r(0,1)*r(0,4)..t..(i%12==0 and "0!"..(r(0,3)%3)^2*3 .."^"..r(76).."0,"..r(57) or "")
There is a lot of magic going on here. First, ".." concatenates strings. It can also concatenate numbers.

Code: Select all

r(0,1)*r(0,4)
is responsible for the tileset. This part gets executed 713 times, generating all the tiles we'll need. Now, the idea here is that the first part is 0 half the time, so half of all the tiles generated here will be empty (a necessity if we want to play the level). Then we select randomly from D tiles, E tiles, and 1 tiles, if the first part was 1.

Code: Select all

..t..
concatenates the tileset to the left side of the level data (where it belongs) and begins to concatenate the next part, which will be the objects.

Code: Select all

i%12==0 and "0!"..(r(0,3)%3)^2*3 .."^"..r(76).."0,"..r(57) or ""
If you're coming from C land, this construct will look familiar. a and b or c in lua is equivalent to a ? b : c in C. The way it works is taken straight out of Scheme (this also works in Python, Ruby, and Tcl, as I recall):

a and b returns false if a is false and b if a is true. In addition, the expression b is only evaluated if a is true (lazy evaluation). This is the basis of LISP and other functional languages.

a or b returns b if a is false and a if a is true. b is only evaluated if a is false, similar to the previous example.

So, a and b or c first:

evaluates a

if a is false, b is skipped - a and returns false now. then, c is returned, since the first argument to or was false

if a is true, b is returned, since the first argument to and is false, but the first argument to or is true.

It's basically an if statement, except significantly more versatile, especially when combined with lambda expressions, forming the lambda calculus.

What this means for us is that

Code: Select all

"0!"..(r(0,3)%3)^2*3 .."^"..r(76).."0,"..r(57)
is attached to the right side of the string every twelfth iteration, so we have 59 objects. Come to think of it, I could have made it add an object every ninth iteration and saved a character.

Anyway...

Code: Select all

"0!" and "0,"
both concatenate a 0 onto the end of the previous item's x or y location before continuing. The idea here is that things get added to the grid at every tenth pixel rather than fully z-snapped, which helps make things more even. Also, starting with "0!" takes care of the "|5^96,9" from earlier by sticking a 0 on the end. Clever, huh?

Code: Select all

(r(0,3)%3)^2*3

picks the object type: 0 means gold, 3 means gauss, and 12 means mine. So, if random returns 0, we get a gold piece, if random returns 1, we get a gauss turret, 2 gives us a mine, and 3 gives us another gold piece (thanks to the modulus operator).

Code: Select all

r(76) and r(57)
pick the object location randomly. We can go up to 760,570, so we use 76 and 57 and add a zero to the end.

Code: Select all

end;print(t)
ends the loop and prints the level data. The end.

Re: Tiny N level generator

Posted: 2009.10.05 (21:29)
by Yoshimo
Latest revamp:

AG is possible, and I swapped out the 2/3 for 1/5. woot woot. Current code, x is 0 for my original, 1 for new.

Code: Select all

def tiles(tile_seed___determines_other_tiles):
	ts = tile_seed___determines_other_tiles
	import random
	if ts == 1:
		x = (['0']*500)+(['1']*151)+(['3', '2', '5', '4', 'Q', 'P', 'O', 'N']*9)
	if ts == 0:
		x = (['0']*428)+(['1']*214)+(['G', 'F', 'I', 'H', '?', '>', 'A', '@']*9)
	else:
		x = (['0']*508)+(['1']*44)+(['8', '9', '6', '7', '<', '=', ':', ';']*20)
	random.shuffle(x)
	x += '|5^396,300'
	ite = 0
	while ite <= 30:
		ite += 1
		y = (random.randint(1, 22))*24+24
		z = (random.randint(1, 30))*24+24
		mines = ('!12^'+str(y)+','+str(z))
		"".join(mines)
		x += mines
	ite2 = 0
	while ite2 <= 40:
		ite2 +=1
		a = (random.randint(1, 22))*24+24
		b = (random.randint(1, 30))*24+24
		gold = ('!0^'+str(a)+','+str(b))
		"".join(gold)
		x += gold
	print(''.join(x))
Revamped, with better mine/gold placing, and new tileset type. Anything other then 0 or 1 will produce a curvy set with tons of free space.

Re: Tiny N level generator

Posted: 2009.11.18 (10:55)
by AlliedEnvy
When I saw this thread months ago, I started writing a duplicate of LV's generator, but in the J programming language. I only got halfway before giving up. Tonight, I went back and finished it.

Code: Select all

('0'356}(?713#40){'2345',12#'001'),'|5^396,300',;".&.>63#<'''!'',(":12 0 0{~?3),''^'',(":24*1+?31),'','',":24*1+?23'
One line. 116 characters. 117 if you count the newline.

(Oh, and if you want to know, I got to just before the ,; before giving up -- that's where the object generation starts.)

Re: Tiny N level generator

Posted: 2009.11.19 (06:50)
by aids
AlliedEnvy wrote:When I saw this thread months ago, I started writing a duplicate of LV's generator, but in the J programming language. I only got halfway before giving up. Tonight, I went back and finished it.

Code: Select all

('0'356}(?713#40){'2345',12#'001'),'|5^396,300',;".&.>63#<'''!'',(":12 0 0{~?3),''^'',(":24*1+?31),'','',":24*1+?23'
One line. 116 characters. 117 if you count the newline.

(Oh, and if you want to know, I got to just before the ,; before giving up -- that's where the object generation starts.)
How do you use it? (Sorry to sound stupid.)

Re: Tiny N level generator

Posted: 2009.11.19 (14:48)
by AlliedEnvy
Kablizzy Sucks wrote: How do you use it? (Sorry to sound stupid.)
You download the J interpreter, run the interpreter, run something like

Code: Select all

9!:37]0 1500 0 222
to set the interpreter to display enough characters (otherwise it'll cut off the map), and then run my code.

But seriously, it ought to produce maps indistinguishable from the ones produced by LV's generator. If you just want to see new generated maps, use his, it's easier.