| 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 |