| @@ -1,4 +1,33 @@ | |||
| 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 | |||
| class CellMap: | |||
| @@ -102,14 +131,17 @@ class CellMap: | |||
| self.__treasure = bool(treasure) | |||
| 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() | |||
| for _ in range(self.reps): | |||
| self.smoothMap() | |||
| if self.treasure: | |||
| self.generateTreasure() | |||
| if self.out: | |||
| if self.treasure: | |||
| self.generateTreasure() | |||
| self.createImage() | |||
| else: | |||
| self.printScreen() | |||
| @@ -123,7 +155,8 @@ class CellMap: | |||
| def createMap(self): | |||
| """ Initializes an x by y grid. | |||
| 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 self.__height == 0 or self.__width == 0 or self.__seed == 0: | |||
| @@ -143,7 +176,11 @@ class CellMap: | |||
| self.genmap = new_map | |||
| 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: | |||
| print("The 'death' limit is currently {} and the 'birth' limit is {}.".format(self.death,self.birth)) | |||
| @@ -181,6 +218,8 @@ class CellMap: | |||
| self.genmap = new_map | |||
| def countWalls(self, x, y): | |||
| """ Counts the number of wall neighbors a cell has and returns that count. | |||
| """ | |||
| count = 0 | |||
| for j in range(-1,2): | |||
| for i in range(-1,2): | |||
| @@ -192,31 +231,58 @@ class CellMap: | |||
| # So we make this neighbor count as a wall. | |||
| count += 1 | |||
| #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. | |||
| count += 1 | |||
| return count | |||
| 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 i in range(len(self.genmap[j])): | |||
| 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): | |||
| """ 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" | |||
| path = " " | |||
| gold = "GG" | |||
| diam = "DD" | |||
| 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() | |||
| 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) | |||
| if self.chunky: | |||
| true_x, true_y = x*2, y*2 | |||
| @@ -229,16 +295,31 @@ class CellMap: | |||
| # Paths are white by default | |||
| c_space = [255-x for x in c_wall] | |||
| c_gold = [(x+64)%255 for x in c_space] | |||
| c_diam = [(x+64)%255 for x in c_gold] | |||
| if self.chunky: | |||
| for line in self.genmap: | |||
| for _ in range(2): | |||
| for val in line: | |||
| 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: | |||
| for line in self.genmap: | |||
| 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) | |||
| if not os.path.exists("maps"): | |||
| os.makedirs("maps") | |||
| @@ -251,6 +332,10 @@ class CellMap: | |||
| print("Saved maps/{}.png".format(fn)) | |||
| 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") | |||
| for line in self.genmap: | |||
| print("\t{},".format(line)) | |||
| @@ -258,6 +343,10 @@ class CellMap: | |||
| 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"] | |||
| fn = [] | |||
| for _ in range(16): | |||
| @@ -265,17 +354,20 @@ def filename(): | |||
| return "".join(fn) | |||
| def parseArgs(args): | |||
| """ Parses the command-line arguments sent to the script. | |||
| Discards anything that isn't a recognized as a valid flag. | |||
| """ | |||
| 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(): | |||
| if flag in args: | |||
| @@ -285,6 +377,8 @@ def parseArgs(args): | |||
| flags["--color"] = True | |||
| elif flag == "--chunky": | |||
| flags["--chunky"] = True | |||
| elif flag == "--treasure": | |||
| flags["--treasure"] = True | |||
| else: | |||
| flags[flag] = args[args.index(flag) + 1] | |||
| return flags | |||