|
|
|
|
|
|
|
|
import random, sys, os |
|
|
|
|
|
|
|
|
""" A tool for randomly generating maps. |
|
|
|
|
|
It starts by populating a grid with randomized True/False values and |
|
|
|
|
|
then uses a "cellular automata"-based smoothing algorithm to build a |
|
|
|
|
|
map. |
|
|
|
|
|
|
|
|
|
|
|
Maps are rectangles, but can be of any size. Naturally, larger maps |
|
|
|
|
|
take longer to generate. |
|
|
|
|
|
|
|
|
|
|
|
By default, the mapper will print to the screen as a grid of "I"s (walls) |
|
|
|
|
|
and spaces (paths). You can tell the mapper to print to an image instead. |
|
|
|
|
|
If you do, the following apply: |
|
|
|
|
|
|
|
|
|
|
|
You can tell the mapper to make a map "chunky", which keeps the T/F |
|
|
|
|
|
grid the same size but uses four pixels instead of one for each point |
|
|
|
|
|
on the grid, doubling the size of the final generated image. |
|
|
|
|
|
|
|
|
|
|
|
Maps are two-color: black and white by default, but it will use random |
|
|
|
|
|
contrasting colors if told to. |
|
|
|
|
|
|
|
|
|
|
|
You can tell the mapper to insert treasure, which appears as a third |
|
|
|
|
|
color on the map. |
|
|
|
|
|
""" |
|
|
|
|
|
|
|
|
|
|
|
__author__ = "Noëlle Anthony" |
|
|
|
|
|
__version__ = "1.0.0" |
|
|
|
|
|
|
|
|
|
|
|
import random |
|
|
|
|
|
import sys |
|
|
|
|
|
import os |
|
|
|
|
|
|
|
|
from PIL import Image |
|
|
from PIL import Image |
|
|
|
|
|
|
|
|
class CellMap: |
|
|
class CellMap: |
|
|
|
|
|
|
|
|
self.__treasure = bool(treasure) |
|
|
self.__treasure = bool(treasure) |
|
|
|
|
|
|
|
|
def generateFullMap(self): |
|
|
def generateFullMap(self): |
|
|
""" Puts everything together. |
|
|
|
|
|
|
|
|
""" Puts everything together. Runs the smoothing routine a number |
|
|
|
|
|
of times equal to self.reps, generates treasure (if self.treasure |
|
|
|
|
|
is set), and creates and saves an image of the map if self.out is |
|
|
|
|
|
set or prints the map to stdout if it isn't. |
|
|
""" |
|
|
""" |
|
|
self.createMap() |
|
|
self.createMap() |
|
|
for _ in range(self.reps): |
|
|
for _ in range(self.reps): |
|
|
self.smoothMap() |
|
|
self.smoothMap() |
|
|
|
|
|
if self.treasure: |
|
|
|
|
|
self.generateTreasure() |
|
|
if self.out: |
|
|
if self.out: |
|
|
if self.treasure: |
|
|
|
|
|
self.generateTreasure() |
|
|
|
|
|
self.createImage() |
|
|
self.createImage() |
|
|
else: |
|
|
else: |
|
|
self.printScreen() |
|
|
self.printScreen() |
|
|
|
|
|
|
|
|
def createMap(self): |
|
|
def createMap(self): |
|
|
""" Initializes an x by y grid. |
|
|
""" Initializes an x by y grid. |
|
|
x is width, y is height |
|
|
x is width, y is height |
|
|
seed is the chance that a given cell will be "live" and should be an integer between 1-99. |
|
|
|
|
|
|
|
|
seed is the chance that a given cell will be "live" and should be |
|
|
|
|
|
an integer between 1-99. |
|
|
If True is equivalent to "wall", then higher seeds make more walls. |
|
|
If True is equivalent to "wall", then higher seeds make more walls. |
|
|
""" |
|
|
""" |
|
|
if self.__height == 0 or self.__width == 0 or self.__seed == 0: |
|
|
if self.__height == 0 or self.__width == 0 or self.__seed == 0: |
|
|
|
|
|
|
|
|
self.genmap = new_map |
|
|
self.genmap = new_map |
|
|
|
|
|
|
|
|
def smoothMap(self): |
|
|
def smoothMap(self): |
|
|
""" Refines the grid. |
|
|
|
|
|
|
|
|
""" Refines the grid using cellular-automaton rules. |
|
|
|
|
|
If a wall doesn't have enough wall neighbors, it "dies" and |
|
|
|
|
|
becomes a path. If a path has too many wall neighbors, it turns |
|
|
|
|
|
into a wall. This is controlled by the values in self.death and |
|
|
|
|
|
self.birth, respectively. |
|
|
""" |
|
|
""" |
|
|
if self.death == 0 or self.birth == 0: |
|
|
if self.death == 0 or self.birth == 0: |
|
|
print("The 'death' limit is currently {} and the 'birth' limit is {}.".format(self.death,self.birth)) |
|
|
print("The 'death' limit is currently {} and the 'birth' limit is {}.".format(self.death,self.birth)) |
|
|
|
|
|
|
|
|
self.genmap = new_map |
|
|
self.genmap = new_map |
|
|
|
|
|
|
|
|
def countWalls(self, x, y): |
|
|
def countWalls(self, x, y): |
|
|
|
|
|
""" Counts the number of wall neighbors a cell has and returns that count. |
|
|
|
|
|
""" |
|
|
count = 0 |
|
|
count = 0 |
|
|
for j in range(-1,2): |
|
|
for j in range(-1,2): |
|
|
for i in range(-1,2): |
|
|
for i in range(-1,2): |
|
|
|
|
|
|
|
|
# So we make this neighbor count as a wall. |
|
|
# So we make this neighbor count as a wall. |
|
|
count += 1 |
|
|
count += 1 |
|
|
#pass |
|
|
#pass |
|
|
elif self.genmap[n_y][n_x] and self.genmap[n_y][n_x] != "Gold": |
|
|
|
|
|
|
|
|
elif self.genmap[n_y][n_x] and self.genmap[n_y][n_x] not in ("Gold","Diam"): |
|
|
# This neighbor is on the map and is a wall. |
|
|
# This neighbor is on the map and is a wall. |
|
|
count += 1 |
|
|
count += 1 |
|
|
return count |
|
|
return count |
|
|
|
|
|
|
|
|
def generateTreasure(self): |
|
|
def generateTreasure(self): |
|
|
self.treasurelist = [] |
|
|
|
|
|
walledin = False |
|
|
|
|
|
|
|
|
""" If a path cell has 5 wall neighbors, put a treasure there. |
|
|
|
|
|
If a path cell has at least 6 wall neighbors, put a rare treasure. |
|
|
|
|
|
""" |
|
|
for j in range(len(self.genmap)): |
|
|
for j in range(len(self.genmap)): |
|
|
for i in range(len(self.genmap[j])): |
|
|
for i in range(len(self.genmap[j])): |
|
|
if not self.genmap[j][i]: |
|
|
if not self.genmap[j][i]: |
|
|
walledin = True if self.countWalls(i,j) >= 5 else False |
|
|
|
|
|
if walledin: |
|
|
|
|
|
self.genmap[j][i] = "Gold" |
|
|
|
|
|
walledin = False |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
self.genmap[j][i] = "Gold" if self.countWalls(i,j) == 5 else self.genmap[j][i] |
|
|
|
|
|
self.genmap[j][i] = "Diam" if self.countWalls(i,j) >= 6 else self.genmap[j][i] |
|
|
|
|
|
|
|
|
def printScreen(self): |
|
|
def printScreen(self): |
|
|
|
|
|
""" Prints the map to standard out, using "II" for a wall |
|
|
|
|
|
and " " for a path. |
|
|
|
|
|
|
|
|
|
|
|
The "color", "chunky", and "treasure" options don't affect |
|
|
|
|
|
this mode. |
|
|
|
|
|
""" |
|
|
wall = "II" |
|
|
wall = "II" |
|
|
path = " " |
|
|
path = " " |
|
|
gold = "GG" |
|
|
gold = "GG" |
|
|
|
|
|
diam = "DD" |
|
|
for line in self.genmap: |
|
|
for line in self.genmap: |
|
|
print("".join([path if not x else (gold if x == "Gold" else wall) for x in line])) |
|
|
|
|
|
|
|
|
print("".join([path if not x |
|
|
|
|
|
else (gold if x == "Gold" |
|
|
|
|
|
else (diam if x == "Diam" |
|
|
|
|
|
else wall)) for x in line])) |
|
|
print() |
|
|
print() |
|
|
|
|
|
|
|
|
def createImage(self): |
|
|
def createImage(self): |
|
|
|
|
|
""" Creates and saves an image of the map. |
|
|
|
|
|
|
|
|
|
|
|
If self.color is True, the map uses randomized complementary |
|
|
|
|
|
colors; otherwise, it uses black for walls, white for paths, and |
|
|
|
|
|
light grey for treasures. |
|
|
|
|
|
|
|
|
|
|
|
If self.chunky is True, the map uses 4 pixels for each cell |
|
|
|
|
|
instead of one. This results in an image that's twice as large, |
|
|
|
|
|
and is useful for enlarging smaller maps without the added runtime |
|
|
|
|
|
of actually generating a larger map. |
|
|
|
|
|
|
|
|
|
|
|
If an image with the current map's name already exists, the script |
|
|
|
|
|
will add digits after the filename but before the extension, to |
|
|
|
|
|
avoid a collision. While the possibility of a name collision is |
|
|
|
|
|
low, this allows you to make several copies of a given map (for |
|
|
|
|
|
example, with different settings) without fear of overwriting |
|
|
|
|
|
your previous maps. |
|
|
|
|
|
""" |
|
|
x, y = len(self.genmap[0]), len(self.genmap) |
|
|
x, y = len(self.genmap[0]), len(self.genmap) |
|
|
if self.chunky: |
|
|
if self.chunky: |
|
|
true_x, true_y = x*2, y*2 |
|
|
true_x, true_y = x*2, y*2 |
|
|
|
|
|
|
|
|
# Paths are white by default |
|
|
# Paths are white by default |
|
|
c_space = [255-x for x in c_wall] |
|
|
c_space = [255-x for x in c_wall] |
|
|
c_gold = [(x+64)%255 for x in c_space] |
|
|
c_gold = [(x+64)%255 for x in c_space] |
|
|
|
|
|
c_diam = [(x+64)%255 for x in c_gold] |
|
|
if self.chunky: |
|
|
if self.chunky: |
|
|
for line in self.genmap: |
|
|
for line in self.genmap: |
|
|
for _ in range(2): |
|
|
for _ in range(2): |
|
|
for val in line: |
|
|
for val in line: |
|
|
for _ in range(2): |
|
|
for _ in range(2): |
|
|
lst.append(tuple(c_space) if not val else (tuple(c_gold) if val == "Gold" else tuple(c_wall))) |
|
|
|
|
|
|
|
|
if not val: |
|
|
|
|
|
lst.append(tuple(c_space)) |
|
|
|
|
|
elif val == "Gold": |
|
|
|
|
|
lst.append(tuple(c_gold)) |
|
|
|
|
|
elif val == "Diam": |
|
|
|
|
|
lst.append(tuple(c_diam)) |
|
|
|
|
|
else: |
|
|
|
|
|
lst.append(tuple(c_wall)) |
|
|
else: |
|
|
else: |
|
|
for line in self.genmap: |
|
|
for line in self.genmap: |
|
|
for val in line: |
|
|
for val in line: |
|
|
lst.append(tuple(c_space) if not val else (tuple(c_gold) if val == "Gold" else tuple(c_wall))) |
|
|
|
|
|
|
|
|
if not val: |
|
|
|
|
|
lst.append(tuple(c_space)) |
|
|
|
|
|
elif val == "Gold": |
|
|
|
|
|
lst.append(tuple(c_gold)) |
|
|
|
|
|
elif val == "Diam": |
|
|
|
|
|
lst.append(tuple(c_diam)) |
|
|
|
|
|
else: |
|
|
|
|
|
lst.append(tuple(c_wall)) |
|
|
img.putdata(lst) |
|
|
img.putdata(lst) |
|
|
if not os.path.exists("maps"): |
|
|
if not os.path.exists("maps"): |
|
|
os.makedirs("maps") |
|
|
os.makedirs("maps") |
|
|
|
|
|
|
|
|
print("Saved maps/{}.png".format(fn)) |
|
|
print("Saved maps/{}.png".format(fn)) |
|
|
|
|
|
|
|
|
def printArray(self): |
|
|
def printArray(self): |
|
|
|
|
|
""" This prints the map as a list of lists of True/False values, |
|
|
|
|
|
possibly useful for importing into other scripts or for uses |
|
|
|
|
|
other than generating maps. |
|
|
|
|
|
""" |
|
|
print("[",end="\n") |
|
|
print("[",end="\n") |
|
|
for line in self.genmap: |
|
|
for line in self.genmap: |
|
|
print("\t{},".format(line)) |
|
|
print("\t{},".format(line)) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def filename(): |
|
|
def filename(): |
|
|
|
|
|
""" Creates a 16-character hexadecimal ID. |
|
|
|
|
|
Since the number of results is so large (16^16), the chance of |
|
|
|
|
|
a collision is very small. |
|
|
|
|
|
""" |
|
|
hexes = ["0","1","2","3","4","5","6","7","8","9","a","b","c","d","e","f"] |
|
|
hexes = ["0","1","2","3","4","5","6","7","8","9","a","b","c","d","e","f"] |
|
|
fn = [] |
|
|
fn = [] |
|
|
for _ in range(16): |
|
|
for _ in range(16): |
|
|
|
|
|
|
|
|
return "".join(fn) |
|
|
return "".join(fn) |
|
|
|
|
|
|
|
|
def parseArgs(args): |
|
|
def parseArgs(args): |
|
|
|
|
|
""" Parses the command-line arguments sent to the script. |
|
|
|
|
|
Discards anything that isn't a recognized as a valid flag. |
|
|
|
|
|
""" |
|
|
flags = { |
|
|
flags = { |
|
|
"--height" : 20, |
|
|
|
|
|
"--width" : 20, |
|
|
|
|
|
"--seed" : 45, |
|
|
|
|
|
"--death" : 4, |
|
|
|
|
|
"--birth" : 4, |
|
|
|
|
|
"--reps" : 2, |
|
|
|
|
|
"--out" : False, |
|
|
|
|
|
"--color" : False, |
|
|
|
|
|
"--chunky" : False, |
|
|
|
|
|
"--treas" : False, |
|
|
|
|
|
|
|
|
"--height" : 20, |
|
|
|
|
|
"--width" : 20, |
|
|
|
|
|
"--seed" : 45, |
|
|
|
|
|
"--death" : 4, |
|
|
|
|
|
"--birth" : 4, |
|
|
|
|
|
"--reps" : 2, |
|
|
|
|
|
"--out" : False, |
|
|
|
|
|
"--color" : False, |
|
|
|
|
|
"--chunky" : False, |
|
|
|
|
|
"--treasure": False, |
|
|
} |
|
|
} |
|
|
for flag, default in flags.items(): |
|
|
for flag, default in flags.items(): |
|
|
if flag in args: |
|
|
if flag in args: |
|
|
|
|
|
|
|
|
flags["--color"] = True |
|
|
flags["--color"] = True |
|
|
elif flag == "--chunky": |
|
|
elif flag == "--chunky": |
|
|
flags["--chunky"] = True |
|
|
flags["--chunky"] = True |
|
|
|
|
|
elif flag == "--treasure": |
|
|
|
|
|
flags["--treasure"] = True |
|
|
else: |
|
|
else: |
|
|
flags[flag] = args[args.index(flag) + 1] |
|
|
flags[flag] = args[args.index(flag) + 1] |
|
|
return flags |
|
|
return flags |