WorldGenerator Update

The first obstacle to tackle with the PokeyGame idea was world generation – how do you generate a randomized world that can be traversed?  I ultimately decided to build a procedural level generator capable of churning out levels on demand, or according to a predefined structure and saved to a template file that can be read later.  This lead to the WorldGenerator class I’ll be demonstrating.  This is my first iteration, I’ve decided to come up with my own method, and then investigate other methods once I am happy with mine to see how it compares.

class WorldGenerator:

    """ Generates worlds based on the config parameters """

    def __init__(
                self,                   # World Generator
                debug=False,            # Debug mode
                silent=False,           # Silent mode
                verbose=False,          # Verbose mode
                fpath=None,             # Output file path
                conf=None,              # Config file path
                rand=False,             # Random mode
                dim_x=25,               # x dimension range
                dim_y=25,               # y dimension range
                dim_z=3,                # z dimension range
                app_logger=None,        # Optional passed logger
                room_variance=2,        # Room size variance
                post_check=False,       # Automatic post-generation check
                path_alg='gbf_search'   # Path testing algorithm
                ):
        """ WorldGenerator creates a world_template """

        # The WorldGenerator.grid template can be read directly
        # by importing as a module, or loaded from the output
        # of the command-line invoked menu.  Output format:
        #
        #   map_template = (((t...),...(t'...))...)
        #
        # Where T are tile type codes for your interpreter
        # or WorldTile tile types if using pokeygame
        #
        # Template features :
        #
        #   Randomized templates of any integer dimension can be generated.
        # A series of waypoints are generated randomly across each level,
        # which are then connected by hallway.  Rooms are generated at the
        # waypoints of varying sizes (control by setting room_variance),
        # and a pathfinding algorithm ensures each level is passable before
        # being returned.

        self.start = time.clock()

The class is defined, and the various level configuration parameters are parsed or given default values.  The parameters merit some explanation.

  • debug / silent / verbose  : essentially mutually exclusive, these options enable debugging-level log and console messages, or disable output altogether.  The debug option takes precedence over the silent  and verbose modes, and provides the most output.
  • fpath / conf : fpath  is the output path for the WorldGenerator template.  conf is the path to a Pokeyworks PokeyConfig file.
  • rand / x / y / z : These simply set the x, y, and z dimensions, or if rand is True these will be randomized (within a maximum z dimension of 10, and 50 for x and y.
  • app_logger : expects a Python logger.  If none is passed, one is automatically generated using the PokeyWorks.setup_logger() function.  The debug option has logical message additions as well as the maximum logging verbosity :
    if app_logger is None:
        # Enable verbose messages if in debug or verbose mode
        if debug or verbose:
            log_level = logging.DEBUG
        elif silent:
            log_level = logging.ERROR
        else:
            log_level = logging.INFO
        # Engage the logger
        self.logger = logger(__name__,log_level)
        self.logger.info('[*] WorldGenerator logger engaged')
    else:
        self.logger = app_logger
        self.logger.info('[*] Logger received')
    
  • room_variance : sets the maximum room size range, the lowerbound is 3 tiles.  Must be greater than or equal to 3.
  • post_check : if True , automatically checks the entire map after generating, and if the level cannot be passed with the chosen algorithm, it is regenerated.
  • path_alg : sets the pathfinder search algorithm.   A*, Greedy Best-First, and Breadth-First options are available, for a general map Greedy Best-First seems to perform best with the current map environment.

The last step is to record the start time.  The execution time is calculated by subtracting the time.clock() return value at the end of execution, resulting in a floating point second value.

Gridlines

A major consideration is how exactly to represent the world.  Nested lists could be used, but Python has a much better tool for representing and indexing a grid – Dictionaries.  Using grid coordinate tuples – (x, y, z) – one can create a unique grid and access it easily via nested for loops or list comprehension statements.  A demonstration of this utility follows in the WorldGenerator.__str__()  method.

def __str__(self):
    retval = ''
    for z in range(0,self.dim_z):
        retval+='\tFloor -{}-\n'.format(z)
        for y in range(0,self.dim_y):
            for x in range(0,self.dim_x):
                if self.grid[x,y,z][0]==WorldTile.wall:
                    retval += '. '
                else:
                    retval += '.'
                    if self.grid[x,y,z][1] is not None:
                        retval+=color(
                                '{0}'.format(self.grid[x,y,z][0]),
                                self.grid[x,y,z][1]
                                ).colorized
                    else:
                        retval+= '{0}'.format(self.grid[x,y,z][0])
            retval+='\n'

    return retval

In this case, terminal ascii color codes are being used in the color.colorized attribute, and a map is printed.  An example of its output in my terminal :

floor1_no_path

The map pattern is generated by first populating a list of random waypoints.  On floor 1, a starting point and descent point are set, waypoints are added, hallways are generated to connect each waypoint, and rooms are randomly placed on each waypoint.  The same process is followed on each floor until the bottom floor, each connected by ascent and descent points, and the bottom floor containing an ascent point and an exit point.

def build_paths(self):
    """ Builds paths of hallways to waypoints """

    waypoints_per_floor = ((self.dim_x+self.dim_y)/2)/2
    self.logger.debug(
                '\tWaypoint density : {0}'.format(waypoints_per_floor)
                )
    self.way_list = self.build_waypoints(waypoints_per_floor)

    for i in range(len(self.way_list)-1):
        way1 = self.way_list[i]
        way2 = self.way_list[i+1]

        # If the two waypoints are on the same floor,
        # connect them
        if way1[2]==way2[2]:
            self.logger.info('[*] Connecting {0} to {1}'.format(way1,way2))
            self.connect(way1,way2)

def build_waypoints(self,w):
    retval = []
    for z in range(self.dim_z):
        # Set starting waypoint for the floor
        if z == 0:
            retval.append(self.start)
        else:
            retval.append(self.find_tile(z,WorldTile.ascent_point))

        # Fill other waypoints
        dbg_string = 'Floor {} waypoints :\n\t'.format(z)
        for way in range(w):
            x = random.randint(1,self.dim_x-1)
            y = random.randint(1,self.dim_y-1)

            if len(retval)%4==0:
                dbg_string += '{}\n\t'.format((x,y,z))
            else:
                dbg_string += '{0},'.format((x,y,z))
             retval.append((x,y,z))
        dbg_string = dbg_string[:-1]
        self.logger.debug(dbg_string)

        # Set ending waypoint for the floor
        if z == self.dim_z:
            retval.append(self.end)
        else:
            retval.append(self.find_tile(z,WorldTile.descent_point))

    return retval

def connect(self,pt1,pt2):
    """ Connects the two points with hallway tiles """

    tile_list = []  # List of tiles to be set
    coord_list = list(pt1)  # create a mutable coord list

    max_loops = 35      # Max loops per leg
    this_loop = 0

    while True:
        # Pathbuilder exit conditions:
        if this_loop >= max_loops:
            self.logger.debug('\tMaximum steps for this leg (pathbuilder)')
            break
        if tuple(coord_list)==pt2:
            # Arrived at destination waypoint
            self.logger.debug('\tArrived at point {}'.format(pt2))
            break

        # Final leg detector - executes when a point is reached
        # on the same axis (x/y) and within 2 tiles.  Directly
        # connects to the endpoint
        if coord_list[0]==pt2[0] and abs(coord_list[1]-pt2[1])<3:
            idx = 1     # axis of motion = y, static x
            rng = coord_list[1]-pt2[1]
        elif coord_list[1]==pt2[1] and abs( coord_list[0]-pt2[0])<3:
            idx = 0     # axis of motion = x, static y
            rng = coord_list[0]-pt2[0]
        else:
            rng = False

        if rng:
            self.logger.debug('\tDestination point in range')
                for n in range(rng):
                    coord = coord_list
                    coord[idx]+=n
                    coord = tuple(coord)
                    assert isinstance(coord,(tuple)), 'Invalid coord {}'.format(
                                                                    coord
                                                                    )
                    tile_list.append(coord)
                break
            # If not the final leg, randomly select a direction
            idx,val = self.path_avail_dirs(
                                coord_list,
                                ['E','U','D','3'],
                                pt2,
                                True)
            coord_list[idx]+=val
            tile_list.append(tuple(coord_list))
            this_loop +=1

        # Fill the resulting point list with hallways in the grid
        for tile in tile_list:
            try:
                #Only overwrite wall type tiles
                if self.grid[tile][0]==WorldTile.wall:
                    self.grid[tile]=[
                                    WorldTile.hallway,
                                    [color.BLUE,color.BOLD]
                                    ]
            except KeyError:
                if not self.silent:
                    self.logger.error('[*] Invalid tile specified!')
                self.logger.debug('Tile value : {}'.format(tile))
                continue

Stay tuned for an update, including a more detailed explanation of this process and the WorldGenerator.path_avail_dirs() method.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.