diff --git a/evennia/contrib/mapbuilder.py b/evennia/contrib/mapbuilder.py index ed41dd7ec1..882a2b7304 100644 --- a/evennia/contrib/mapbuilder.py +++ b/evennia/contrib/mapbuilder.py @@ -1,207 +1,312 @@ # -*- coding: utf-8 -*- """ - Evennia World Builder Contribution - Cloud_Keeper 2016 -This is a command capable of taking a reference to a basic 2D ASCII -map stored in the Evennia directory and generating the rooms and exits -necessary to populate that world. The characters of the map are iterated -over and compared to a list of trigger characters. When a match is found -it triggers the corresponding instructions. Use by importing and including -the command in your default_cmdsets module. For example: +Build a map from a 2D ASCII map. + +This is a command which takes two inputs: + +≈≈≈≈≈ +≈♣n♣≈ MAP_LEGEND = {("♣", "♠"): build_forest, +≈∩▲∩≈ ("∩", "n"): build_mountains, +≈♠n♠≈ ("▲"): build_temple} +≈≈≈≈≈ + +A string of ASCII characters representing a map and a dictionary of functions +containing build instructions. The characters of the map are iterated over and +compared to a list of trigger characters. When a match is found the +corresponding function is executed generating the rooms, exits and objects as +defined by the users build instructions. If a character is not a match to +a provided trigger character (including spaces) it is simply skipped and the +process continues. + +For instance, the above map represents a temple (▲) amongst mountains (n,∩) +in a forest (♣,♠) on an island surrounded by water (≈). Each character on the +first line is iterated over but as there is no match with our MAP_LEGEND it +is skipped. On the second line it finds "♣" which is a match and so the +`build_forest` function is called. Next the `build_mountains` function is +called and so on until the map is completed. Building instructions are passed +the following arguments: + x - The rooms position on the maps x axis + y - The rooms position on the maps y axis + caller - The player calling the command + iteration - The current iterations number (0, 1 or 2) + room_dict - A dictionary containing room references returned by build + functions where tuple coordinates are the keys (x, y). + ie room_dict[(2, 2)] will return the temple room above. + +Building functions should return the room they create. By default these rooms +are used to create exits between valid adjacent rooms to the north, south, +east and west directions. This behaviour can turned off with the use of switch +arguments. In addition to turning off automatic exit generation the switches +allow the map to be iterated over a number of times. This is important for +something like custom exit building. Exits require a reference to both the +exits location and the exits destination. During the first iteration it is +possible that an exit is created pointing towards a destination that +has not yet been created resulting in error. By iterating over the map twice +the rooms can be created on the first iteration and room reliant code can be +be used on the second iteration. The iteration number and a dictionary of +references to rooms previously created is passed to the build commands. + +Use by importing and including the command in your default_cmdsets module. +For example: # mygame/commands/default_cmdsets.py - + from evennia.contrib import mapbuilder - + ... - + self.add(mapbuilder.CmdMapBuilder()) -You then call the command in-game using the path to the module and the -name of the variable holding the map string. The path you provide is -relative to the evennia or your mygame folder. +You then call the command in-game using the path to the MAP and MAP_LEGEND vars +The path you provide is relative to the evennia or mygame folder. - @mapbuilder +Usage: + @mapbuilder[/switch] -For example to generate from the sample map in this module: +Switches: + one - execute build instructions once without automatic exit creation. + two - execute build instructions twice without automatic exit creation. - @mapbuilder evennia.contrib.mapbuilder.EXAMPLE_MAP +Example: + @mapbuilder world.gamemap.MAP world.maplegend.MAP_LEGEND + @mapbuilder evennia.contrib.mapbuilder.EXAMPLE2_MAP EXAMPLE2_LEGEND + (Legend path defaults to map path) -Whilst this map is contained here for convenience, it is suggested -that your map be stored in a separate stand alone module in the -mygame/world folder accessed with @mapbuilder world.gamemap.MAP - -The rooms generated for each square of the map are generated using -instructions for each room type. These instructions are intended -to be changed and adapted for your purposes. In writing these -instructions you have access to the full API just like Batchcode. +Below are two examples showcasing the use of automatic exit generation and +custom exit generation. Whilst located, and can be used, from this module for +convenience The below example code should be in mymap.py in mygame/world. """ -# ---------- # -# This code should be in mymap.py in mygame/world. +from django.conf import settings +from evennia.utils import utils + +# ---------- EXAMPLE 1 ---------- # +# @mapbuilder evennia.contrib.mapbuilder.EXAMPLE1_MAP EXAMPLE1_LEGEND # -*- coding: utf-8 -*- -EXAMPLE_MAP = """\ -≈≈≈≈≈≈≈≈≈ -≈≈≈≈≈≈≈≈≈ -≈≈♣♠♣♠♣≈≈ -≈≈♠n∩n♠≈≈ -≈≈♣∩▲∩♣≈≈ -≈≈♠n≈n♠≈≈ -≈≈♣♠≈♠♣≈≈ -≈≈≈≈≈≈≈≈≈ -≈≈≈≈≈≈≈≈≈ -""" - -# ---------- # - -#Add the necessary imports for your instructions here. +# Add the necessary imports for your instructions here. from evennia import create_object from typeclasses import rooms, exits from evennia.utils import utils from random import randint import random -def build_map(caller, raw_map): - """ - This is the part of the code designed to be changed and expanded. It - contains the instructions that are called when a match is made between - the characters in your map and the characters that trigger the building - instructions. +# A map with a temple (▲) amongst mountains (n,∩) in a forest (♣,♠) on an +# island surrounded by water (≈). By giving no instructions for the water +# characters we effectively skip it and create no rooms for those squares. +EXAMPLE1_MAP = """\ +≈≈≈≈≈ +≈♣n♣≈ +≈∩▲∩≈ +≈♠n♠≈ +≈≈≈≈≈ +""" + + +def build_forest(x, y, **kwargs): + """A basic example of build instructions. Make sure to include **kwargs + in the arguments and return an instance of the room for exit generation.""" + + # Create a room and provide a basic description. + room = create_object(rooms.Room, key="forest" + str(x) + str(y)) + room.db.desc = "Basic forest room." + + # Send a message to the player + kwargs["caller"].msg("Forest Room Created.") + + # This is generally mandatory. + return room + + +def build_mountains(x, y, **kwargs): + """A room that is a little more advanced""" + + # Create the room. + room = create_object(rooms.Room, key="mountains" + str(x) + str(y)) - """ + # Generate a description by randomly selecting an entry from a list. + room_desc = ["Mountains as far as the eye can see", + "Your path is surrounded by sheer cliffs", + "Haven't you seen that rock before?"] + room.db.desc = random.choice(room_desc) - #Create a tuple containing the trigger characters - forest = ("♣", "♠") - #Create a function that contains the instructions. - def create_forest(x, y): - #This has just basic instructions, building and naming the room. - room = create_object(rooms.Room, key="forest" + str(x) + str(y)) - room.db.desc = "Basic forest room." - - #Always include this at the end. Sets up for advanced functions. - caller.msg(room.key + " " + room.dbref) - room_list.append([room, x, y]) - - - mountains = ("∩", "n") - def create_mountains(x, y): - #We'll do something fancier in this one. - room = create_object(rooms.Room, key="mountains" + str(x) + str(y)) - - #We'll select a description at random from a list. - room_desc = [ - "Mountains as far as the eye can see", - "Your path is surrounded by sheer cliffs", - "Haven't you seen that rock before?"] - room.db.desc = random.choice(room_desc) - - #Let's populate the room with a random amount of rocks. - for i in xrange(randint(0,3)): - rock = create_object(key = "Rock", location = room) - rock.db.desc = "An ordinary rock." - - #Mandatory. - caller.msg(room.key + " " + room.dbref) - room_list.append([room, x, y]) - - temple = ("▲") - def create_temple(x, y): - #This room is only used once so we can be less general. - room = create_object(rooms.Room, key="temple" + str(x) + str(y)) + # Create a random number of objects to populate the room. + for i in xrange(randint(0, 3)): + rock = create_object(key="Rock", location=room) + rock.db.desc = "An ordinary rock." - room.db.desc = "In what, from the outside, appeared to be a grand " \ - "and ancient temple you've somehow found yourself in the the " \ - "Evennia Inn! It consists of one large room filled with tables. " \ - "The bardisk extends along the east wall, where multiple barrels " \ - " and bottles line the shelves. The barkeep seems busy handing " \ - "out ale and chatting with the patrons, which are a rowdy and " \ - "cheerful lot, keeping the sound level only just below thunderous" \ - ". This is a rare spot of warmth and mirth on this dread moor." - - #Mandatory. - caller.msg(room.key + " " + room.dbref) - room_list.append([room, x, y]) + # Send a message to the player + kwargs["caller"].msg("Mountain Room Created.") - #Include your keys and instructions in the master dictionary. - master_dict = { - forest:create_forest, - mountains:create_mountains, - temple:create_temple - } + # This is generally mandatory. + return room - # --- ADVANCED USERS ONLY. Altering things below may break it. --- + +def build_temple(x, y, **kwargs): + """A unique room that does not need to be as general""" + + # Create the room. + room = create_object(rooms.Room, key="temple" + str(x) + str(y)) + + # Set the description. + room.db.desc = ("In what, from the outside, appeared to be a grand and " + "ancient temple you've somehow found yourself in the the " + "Evennia Inn! It consists of one large room filled with " + "tables. The bardisk extends along the east wall, where " + "multiple barrels and bottles line the shelves. The " + "barkeep seems busy handing out ale and chatting with " + "the patrons, which are a rowdy and cheerful lot, " + "keeping the sound level only just below thunderous. " + "This is a rare spot of mirth on this dread moor.") + + # Send a message to the player + kwargs["caller"].msg("Temple Room Created.") + + # This is generally mandatory. + return room - #Create reference list and split map string to list of rows. - room_list = [] - map = prepare_map(raw_map) +# Include your trigger characters and build functions in a legend dict. +EXAMPLE1_LEGEND = {("♣", "♠"): build_forest, + ("∩", "n"): build_mountains, + ("▲"): build_temple} - caller.msg("Creating Landmass...") - for y in xrange(len(map)): - for x in xrange(len(map[y])): - for key in master_dict: - if map[y][x] in key: - master_dict[key](x,y) - - #Creating exits - caller.msg("Connecting Areas...") - for location in room_list: - x = location[1] - y = location[2] - - for destination in room_list: - #north - if destination[1] == x and destination[2] == y-1: - exit = create_object(exits.Exit, key="north", - aliases=["n"], location=location[0], - destination=destination[0]) - - #east - if destination[1] == x+1 and destination[2] == y: - exit = create_object(exits.Exit, key="east", - aliases=["e"], location=location[0], - destination=destination[0]) - #south - if destination[1] == x and destination[2] == y+1: - exit = create_object(exits.Exit, key="south", - aliases=["s"], location=location[0], - destination=destination[0]) - - #west - if destination[1] == x-1 and destination[2] == y: - exit = create_object(exits.Exit, key="west", - aliases=["w"], location=location[0], - destination=destination[0]) +# ---------- EXAMPLE 2 ---------- # +# @mapbuilder evennia.contrib.mapbuilder.EXAMPLE2_MAP EXAMPLE2_LEGEND +# -*- coding: utf-8 -*- -from django.conf import settings +# Add the necessary imports for your instructions here. +from evennia import create_object +from typeclasses import rooms, exits from evennia.utils import utils -import imp +from random import randint +import random + +# This is the same layout as Example 1 but included are characters for exits. +# We can use these characters to determine which rooms should be connected. +EXAMPLE2_MAP = """\ +≈ ≈ ≈ ≈ ≈ + +≈ ♣-♣-♣ ≈ + | | +≈ ♣ ♣ ♣ ≈ + | | | +≈ ♣-♣-♣ ≈ + +≈ ≈ ≈ ≈ ≈ +""" + + +def build_forest(x, y, **kwargs): + """A basic room""" + # If on anything other than the first iteration - Do nothing. + if kwargs["iteration"] > 0: + return + + room = create_object(rooms.Room, key="forest" + str(x) + str(y)) + room.db.desc = "Basic forest room." + + return room + + +def build_verticle_exit(x, y, **kwargs): + """Creates two exits to and from the two rooms north and south.""" + # If on the first iteration - Do nothing. + if kwargs["iteration"] == 0: + return + + for key, value in kwargs["room_dict"].iteritems(): + kwargs["caller"].msg(str(key)) + kwargs["caller"].msg(str(value)) + + north_room = kwargs["room_dict"][(x, y-1)] + south_room = kwargs["room_dict"][(x, y+1)] + + north = create_object(exits.Exit, key="south", + aliases=["s"], location=north_room, + destination=south_room) + + south = create_object(exits.Exit, key="north", + aliases=["n"], location=south_room, + destination=north_room) + + +def build_horizontal_exit(x, y, **kwargs): + """Creates two exits to and from the two rooms east and west.""" + # If on the first iteration - Do nothing. + if kwargs["iteration"] == 0: + return + + west_room = kwargs["room_dict"][(x-1, y)] + east_room = kwargs["room_dict"][(x+1, y)] + + west = create_object(exits.Exit, key="east", + aliases=["e"], location=west_room, + destination=east_room) + + east = create_object(exits.Exit, key="west", + aliases=["w"], location=east_room, + destination=west_room) + +# Include your trigger characters and build functions in a legend dict. +EXAMPLE2_LEGEND = {("♣", "♠"): build_forest, + ("|"): build_verticle_exit, + ("-"): build_horizontal_exit} + +# ---------- END OF EXAMPLES ---------- # COMMAND_DEFAULT_CLASS = utils.class_from_module(settings.COMMAND_DEFAULT_CLASS) - + + +# Helper function for readability. +def _map_to_list(game_map): + """ + Splits multi line map string into list of rows, treats for UTF-8 encoding. + + Args: + game_map (str): An ASCII map + + Returns: + list (list): The map split into rows + + """ + list_map = game_map.split('\n') + return [character.decode('UTF-8') if isinstance(character, basestring) + else character for character in list_map] + + class CmdMapBuilder(COMMAND_DEFAULT_CLASS): """ Build a map from a 2D ASCII map. Usage: - @mapbuilder + @mapbuilder[/switch] + + Switches: + one - execute build instructions once without automatic exit creation + two - execute build instructions twice without automatic exit creation Example: - @mapbuilder evennia.contrib.mapbuilder.EXAMPLE_MAP - @mapbuilder world.gamemap.MAP + @mapbuilder evennia.contrib.mapbuilder.EXAMPLE_MAP MAP_LEGEND + @mapbuilder world.gamemap.MAP world.maplegend.MAP_LEGEND - This is a simple way of building a map of placeholder or otherwise - bare rooms from a 2D ASCII map. The command merely imports the map - and runs the build_map function in evennia.contrib.mapbuilder. - This function should be altered with keys and instructions that - suit your individual needs. + This is a command which takes two inputs: + A string of ASCII characters representing a map and a dictionary of + functions containing build instructions. The characters of the map are + iterated over and compared to a list of trigger characters. When a match + is found the corresponding function is executed generating the rooms, + exits and objects as defined by the users build instructions. If a + character is not a match to a provided trigger character (including spaces) + it is simply skipped and the process continues. By default exits are + automatically generated but is turned off by switches which also determines + how many times the map is iterated over. """ key = "@mapbuilder" aliases = ["@buildmap"] @@ -209,48 +314,145 @@ class CmdMapBuilder(COMMAND_DEFAULT_CLASS): help_category = "Building" def func(self): - "Starts the processor." - + """Starts the processor.""" + caller = self.caller - args = self.args - map = None - - #Check if arguments passed. - if not args: - caller.msg("Usage: @mapbuilder ") + args = self.args.split() + + # Check if arguments passed. + if not self.args or (len(args) != 2): + caller.msg("Usage: @mapbuilder " + "") return + + # Set up base variables. + game_map = None + legend = None + + # OBTAIN MAP FROM MODULE - #Breaks down path into PATH, VARIABLE - args = args.rsplit('.', 1) + # Breaks down path_to_map into [PATH, VARIABLE] + path_to_map = args[0] + path_to_map = path_to_map.rsplit('.', 1) try: - #Retrieves map variable from module. - map = utils.variable_from_module(args[0],args[1]) + # Retrieves map variable from module or raises error. + game_map = utils.variable_from_module(path_to_map[0], + path_to_map[1]) + if not game_map: + raise ValueError("Command Aborted!\n" + "Path to map variable failed.\n" + "Usage: @mapbuilder ") except Exception as exc: - #Or relays error message if fails. + # Or relays error message if fails. caller.msg(exc) - #Display map retrieved. - caller.msg("Creating Map...") - caller.msg(map) + # OBTAIN MAP_LEGEND FROM MODULE - #Pass map to the bulid function. - build_map(caller, map) - caller.msg("Map Created.") + # Breaks down path_to_legend into [PATH, VARIABLE] + path_to_legend = args[1] + path_to_legend = path_to_legend.rsplit('.', 1) + + # If no path given default to path_to_map's path + if len(path_to_legend) == 1: + path_to_legend.insert(0, path_to_map[0]) + + try: + # Retrieves legend variable from module or raises error if fails. + legend = utils.variable_from_module(path_to_legend[0], + path_to_legend[1]) + if not legend: + raise ValueError("Command Aborted!\n" + "Path to legend variable failed.\n" + "Usage: @mapbuilder ") + + except Exception as exc: + # Or relays error message if fails. + caller.msg(exc) -#Helper function for readability. -def prepare_map(map): + # Set up build_map arguments from switches + iterations = 1 + build_exits = True + + if "one" in self.switches: + build_exits = False + + if "two" in self.switches: + iterations = 2 + build_exits = False + + # Pass map and legend to the build function. + build_map(caller, game_map, legend, iterations, build_exits) + + +def build_map(caller, game_map, legend, iterations=1, build_exits=True): """ - Splits multi line map string into list of rows, treats for UTF-8 encoding. - - Args: - map (str): An ASCII map - - Returns: - list (list): The map split into rows + Receives the fetched map and legend vars provided by the player. The map + is iterated over character by character, comparing it to the trigger + characters in the legend var and executing the build instructions on + finding a match. The map is iterated over according to the `iterations` + value and exits are optionally generated between adjacent rooms according + to the `build_exits` value. """ - list_map = map.split('\n') - return [character.decode('UTF-8') if isinstance(character, basestring) - else character for character in list_map] + + # Split map string to list of rows and create reference list. + caller.msg("Creating Map...") + caller.msg(game_map) + game_map = _map_to_list(game_map) + + # Create a reference dictionary which be passed to build functions and + # will store obj returned by build functions so objs can be referenced. + room_dict = {} + + caller.msg("Creating Landmass...") + for iteration in xrange(iterations): + for y in xrange(len(game_map)): + for x in xrange(len(game_map[y])): + for key in legend: + if game_map[y][x] in key: + room = legend[key](x, y, iteration=iteration, + room_dict=room_dict, + caller=caller) + if iteration == 0: + room_dict[(x, y)] = room + + if build_exits: + # Creating exits. Assumes single room object in dict entry + caller.msg("Connecting Areas...") + for loc_key, location in room_dict.iteritems(): + x = loc_key[0] + y = loc_key[1] + + # north + if (x, y-1) in room_dict: + if room_dict[(x, y-1)]: + exit = create_object(exits.Exit, key="north", + aliases=["n"], location=location, + destination=room_dict[(x, y-1)]) + + # east + if (x+1, y) in room_dict: + if room_dict[(x+1, y)]: + exit = create_object(exits.Exit, key="east", + aliases=["e"], location=location, + destination=room_dict[(x+1, y)]) + + # south + if (x, y+1) in room_dict: + if room_dict[(x, y+1)]: + exit = create_object(exits.Exit, key="south", + aliases=["s"], location=location, + destination=room_dict[(x, y+1)]) + + # west + if (x-1, y) in room_dict: + if room_dict[(x-1, y)]: + exit = create_object(exits.Exit, key="west", + aliases=["w"], location=location, + destination=room_dict[(x-1, y)]) + + caller.msg("Map Created.")