Module pacai.util.mazeGenerator

Maze Generator

Algorithm: - Start with an empty grid. - Draw a wall with gaps, dividing the grid in 2. - Repeat recursively for each sub-grid.

Pacman Details: - Players 1 and 3 always start in the bottom left; 2 and 4 in the top right. - Food is placed in dead ends and then randomly (though not too close to the pacmen starting positions).

Notes: - The final map includes a symmetric, flipped copy. - The first wall has k gaps, the next wall has k / 2 gaps, etc. (min=1).

@author: Dan Gillick @author: Jie Tang

Expand source code
"""
Maze Generator

Algorithm:
 - Start with an empty grid.
 - Draw a wall with gaps, dividing the grid in 2.
 - Repeat recursively for each sub-grid.

Pacman Details:
 - Players 1 and 3 always start in the bottom left; 2 and 4 in the top right.
 - Food is placed in dead ends and then randomly
    (though not too close to the pacmen starting positions).

Notes:
 - The final map includes a symmetric, flipped copy.
 - The first wall has k gaps, the next wall has k / 2 gaps, etc. (min=1).

@author: Dan Gillick
@author: Jie Tang
"""

import logging
import random
import sys

WALL = '%'
FOOD = '.'
CAPSULE = 'o'
EMPTY = ' '

MAX_DIFFERENT_MAZES = 10000

class Maze(object):
    def __init__(self, rows, cols, anchor=(0, 0), root=None):
        """
        Generate an empty maze.
        Anchor is the top left corner of this grid's position in its parent grid.
        """

        self.r = rows
        self.c = cols
        self.grid = [[EMPTY for col in range(cols)] for row in range(rows)]
        self.anchor = anchor
        self.rooms = []

        self.root = root
        if not self.root:
            self.root = self

    def to_map(self):
        """
        Add a flipped symmetric copy on the right.
        Add a border.
        """

        # Add a flipped symmetric copy
        for row in range(self.r):
            for col in range(self.c - 1, -1, -1):
                self.grid[self.r - row - 1].append(self.grid[row][col])
        self.c *= 2

        # Add a border
        for row in range(self.r):
            self.grid[row] = [WALL] + self.grid[row] + [WALL]
        self.c += 2

        self.grid.insert(0, [WALL for c in range(self.c)])
        self.grid.append([WALL for c in range(self.c)])
        self.r += 2

    def __str__(self):
        s = ''

        for row in range(self.r):
            for col in range(self.c):
                s += self.grid[row][col]
            s += '\n'

        return s[:-1]

    def add_wall(self, rng, i, gaps=1, vert=True):
        """
        Add a wall with gaps.
        """

        add_r, add_c = self.anchor
        if vert:
            gaps = min(self.r, gaps)
            slots = [add_r + x for x in range(self.r)]
            if 0 not in slots:
                if self.root.grid[min(slots) - 1][add_c + i] == EMPTY:
                    slots.remove(min(slots))

                if len(slots) <= gaps:
                    return 0
            if (self.root.c - 1) not in slots:
                if self.root.grid[max(slots) + 1][add_c + i] == EMPTY:
                    slots.remove(max(slots))

            if len(slots) <= gaps:
                return 0

            rng.shuffle(slots)
            for row in slots[int(round(gaps)):]:
                self.root.grid[row][add_c + i] = WALL

            self.rooms.append(Maze(self.r, i, (add_r, add_c), self.root))
            self.rooms.append(Maze(self.r, self.c - i - 1, (add_r, add_c + i + 1), self.root))
        else:
            gaps = min(self.c, gaps)
            slots = [add_c + x for x in range(self.c)]

            if 0 not in slots:
                if self.root.grid[add_r + i][min(slots) - 1] == EMPTY:
                    slots.remove(min(slots))

                if len(slots) <= gaps:
                    return 0

            if (self.root.r - 1) not in slots:
                if self.root.grid[add_r + i][max(slots) + 1] == EMPTY:
                    slots.remove(max(slots))

            if len(slots) <= gaps:
                return 0

            rng.shuffle(slots)
            for col in slots[int(round(gaps)):]:
                self.root.grid[add_r + i][col] = WALL

            self.rooms.append(Maze(i, self.c, (add_r, add_c), self.root))
            self.rooms.append(Maze(self.r - i - 1, self.c, (add_r + i + 1, add_c), self.root))

        return 1

def make_with_prison(rng, room, depth, gaps=1, vert=True, min_width=1, gapfactor=0.5):
    """
    Build a maze with 0,1,2 layers of prison (randomly).
    """

    p = rng.randint(0, 2)
    proll = rng.random()
    if proll < 0.5:
        p = 1
    elif proll < 0.7:
        p = 0
    elif proll < 0.9:
        p = 2
    else:
        p = 3

    add_r, add_c = room.anchor
    for j in range(p):
        cur_col = 2 * (j + 1) - 1
        for row in range(room.r):
            room.root.grid[row][cur_col] = WALL

        if j % 2 == 0:
            room.root.grid[0][cur_col] = EMPTY
        else:
            room.root.grid[room.r - 1][cur_col] = EMPTY

    room.rooms.append(Maze(room.r, room.c - (2 * p), (add_r, add_c + (2 * p)), room.root))
    for sub_room in room.rooms:
        make(rng, sub_room, depth + 1, gaps, vert, min_width, gapfactor)

    return 2 * p

def make(rng, room, depth, gaps=1, vert=True, min_width=1, gapfactor=0.5):
    """
    Recursively build a maze.
    """

    # Extreme base case
    if room.r <= min_width and room.c <= min_width:
        return

    # Decide between vertical and horizontal wall
    if vert:
        num = room.c
    else:
        num = room.r

    if num < min_width + 2:
        vert = not vert
        if vert:
            num = room.c
        else:
            num = room.r

    # Add a wall to the current room
    if depth == 0:
        wall_slots = [num - 2]  # Fix the first wall
    else:
        wall_slots = range(1, num - 1)

    if len(wall_slots) == 0:
        return

    choice = rng.choice(wall_slots)
    if not room.add_wall(rng, choice, gaps, vert):
        return

    # Recursively add walls
    for sub_room in room.rooms:
        make(rng, sub_room, depth + 1, max(1, gaps * gapfactor), not vert, min_width, gapfactor)

def copy_grid(grid):
    new_grid = []

    for row in range(len(grid)):
        new_grid.append([])
        for col in range(len(grid[row])):
            new_grid[row].append(grid[row][col])

    return new_grid

def add_pacman_stuff(rng, maze, max_food=60, max_capsules=4, toskip=0):
    """
    Add pacmen starting position.
    Add food at dead ends plus some extra.
    """

    # Parameters
    max_depth = 2

    # Add food at dead ends
    depth = 0
    total_food = 0

    while True:
        new_grid = copy_grid(maze.grid)
        depth += 1
        num_added = 0

        for row in range(1, maze.r - 1):
            for col in range(1 + toskip, int(maze.c / 2) - 1):
                if (row > maze.r - 6) and (col < 6):
                    continue

                if maze.grid[row][col] != EMPTY:
                    continue

                neighbors = 0
                neighbors += (maze.grid[row - 1][col] == EMPTY)
                neighbors += (maze.grid[row][col - 1] == EMPTY)
                neighbors += (maze.grid[row + 1][col] == EMPTY)
                neighbors += (maze.grid[row][col + 1] == EMPTY)

                if neighbors == 1:
                    new_grid[row][col] = FOOD
                    new_grid[maze.r - row - 1][maze.c - col - 1] = FOOD
                    num_added += 2
                    total_food += 2

        maze.grid = new_grid
        if num_added == 0:
            break

        if depth >= max_depth:
            break

    # Starting pacmen positions
    maze.grid[maze.r - 2][1] = '3'
    maze.grid[maze.r - 3][1] = '1'
    maze.grid[1][maze.c - 2] = '4'
    maze.grid[2][maze.c - 2] = '2'

    # Add capsules
    total_capsules = 0
    while (total_capsules < max_capsules):
        row = rng.randint(1, maze.r - 1)
        col = rng.randint(1 + toskip, int(maze.c / 2) - 2)

        if (row > maze.r - 6) and (col < 6):
            continue

        if (abs(col - int(maze.c / 2)) < 3):
            continue

        if maze.grid[row][col] == EMPTY:
            maze.grid[row][col] = CAPSULE
            maze.grid[maze.r - row - 1][maze.c - col - 1] = CAPSULE
            total_capsules += 2

    # Extra random food
    while (total_food < max_food):
        row = rng.randint(1, maze.r - 1)
        col = rng.randint(1 + toskip, int(maze.c / 2) - 1)

        if (row > maze.r - 6) and (col < 6):
            continue

        if (abs(col - int(maze.c / 2)) < 3):
            continue

        if maze.grid[row][col] == EMPTY:
            maze.grid[row][col] = FOOD
            maze.grid[maze.r - row - 1][maze.c - col - 1] = FOOD
            total_food += 2

def generateMaze(seed = None):
    rng = random.Random()
    if seed is None:
        seed = rng.randint(1, MAX_DIFFERENT_MAZES)
    logging.debug('Seed value for Maze Generation: ' + str(seed))

    rng.seed(seed)
    maze = Maze(16, 16)
    gapfactor = min(0.65, rng.gauss(0.5, 0.1))
    skip = make_with_prison(rng, maze, depth=0, gaps=3, vert=True, min_width=1, gapfactor=gapfactor)
    maze.to_map()
    add_pacman_stuff(rng, maze, 2 * (maze.r * int(maze.c / 20)), 4, skip)

    return str(maze)

if __name__ == '__main__':
    seed = None
    if len(sys.argv) > 1:
        seed = int(sys.argv[1])
    print(generateMaze(seed))

Functions

def add_pacman_stuff(rng, maze, max_food=60, max_capsules=4, toskip=0)

Add pacmen starting position. Add food at dead ends plus some extra.

Expand source code
def add_pacman_stuff(rng, maze, max_food=60, max_capsules=4, toskip=0):
    """
    Add pacmen starting position.
    Add food at dead ends plus some extra.
    """

    # Parameters
    max_depth = 2

    # Add food at dead ends
    depth = 0
    total_food = 0

    while True:
        new_grid = copy_grid(maze.grid)
        depth += 1
        num_added = 0

        for row in range(1, maze.r - 1):
            for col in range(1 + toskip, int(maze.c / 2) - 1):
                if (row > maze.r - 6) and (col < 6):
                    continue

                if maze.grid[row][col] != EMPTY:
                    continue

                neighbors = 0
                neighbors += (maze.grid[row - 1][col] == EMPTY)
                neighbors += (maze.grid[row][col - 1] == EMPTY)
                neighbors += (maze.grid[row + 1][col] == EMPTY)
                neighbors += (maze.grid[row][col + 1] == EMPTY)

                if neighbors == 1:
                    new_grid[row][col] = FOOD
                    new_grid[maze.r - row - 1][maze.c - col - 1] = FOOD
                    num_added += 2
                    total_food += 2

        maze.grid = new_grid
        if num_added == 0:
            break

        if depth >= max_depth:
            break

    # Starting pacmen positions
    maze.grid[maze.r - 2][1] = '3'
    maze.grid[maze.r - 3][1] = '1'
    maze.grid[1][maze.c - 2] = '4'
    maze.grid[2][maze.c - 2] = '2'

    # Add capsules
    total_capsules = 0
    while (total_capsules < max_capsules):
        row = rng.randint(1, maze.r - 1)
        col = rng.randint(1 + toskip, int(maze.c / 2) - 2)

        if (row > maze.r - 6) and (col < 6):
            continue

        if (abs(col - int(maze.c / 2)) < 3):
            continue

        if maze.grid[row][col] == EMPTY:
            maze.grid[row][col] = CAPSULE
            maze.grid[maze.r - row - 1][maze.c - col - 1] = CAPSULE
            total_capsules += 2

    # Extra random food
    while (total_food < max_food):
        row = rng.randint(1, maze.r - 1)
        col = rng.randint(1 + toskip, int(maze.c / 2) - 1)

        if (row > maze.r - 6) and (col < 6):
            continue

        if (abs(col - int(maze.c / 2)) < 3):
            continue

        if maze.grid[row][col] == EMPTY:
            maze.grid[row][col] = FOOD
            maze.grid[maze.r - row - 1][maze.c - col - 1] = FOOD
            total_food += 2
def copy_grid(grid)
Expand source code
def copy_grid(grid):
    new_grid = []

    for row in range(len(grid)):
        new_grid.append([])
        for col in range(len(grid[row])):
            new_grid[row].append(grid[row][col])

    return new_grid
def generateMaze(seed=None)
Expand source code
def generateMaze(seed = None):
    rng = random.Random()
    if seed is None:
        seed = rng.randint(1, MAX_DIFFERENT_MAZES)
    logging.debug('Seed value for Maze Generation: ' + str(seed))

    rng.seed(seed)
    maze = Maze(16, 16)
    gapfactor = min(0.65, rng.gauss(0.5, 0.1))
    skip = make_with_prison(rng, maze, depth=0, gaps=3, vert=True, min_width=1, gapfactor=gapfactor)
    maze.to_map()
    add_pacman_stuff(rng, maze, 2 * (maze.r * int(maze.c / 20)), 4, skip)

    return str(maze)
def make(rng, room, depth, gaps=1, vert=True, min_width=1, gapfactor=0.5)

Recursively build a maze.

Expand source code
def make(rng, room, depth, gaps=1, vert=True, min_width=1, gapfactor=0.5):
    """
    Recursively build a maze.
    """

    # Extreme base case
    if room.r <= min_width and room.c <= min_width:
        return

    # Decide between vertical and horizontal wall
    if vert:
        num = room.c
    else:
        num = room.r

    if num < min_width + 2:
        vert = not vert
        if vert:
            num = room.c
        else:
            num = room.r

    # Add a wall to the current room
    if depth == 0:
        wall_slots = [num - 2]  # Fix the first wall
    else:
        wall_slots = range(1, num - 1)

    if len(wall_slots) == 0:
        return

    choice = rng.choice(wall_slots)
    if not room.add_wall(rng, choice, gaps, vert):
        return

    # Recursively add walls
    for sub_room in room.rooms:
        make(rng, sub_room, depth + 1, max(1, gaps * gapfactor), not vert, min_width, gapfactor)
def make_with_prison(rng, room, depth, gaps=1, vert=True, min_width=1, gapfactor=0.5)

Build a maze with 0,1,2 layers of prison (randomly).

Expand source code
def make_with_prison(rng, room, depth, gaps=1, vert=True, min_width=1, gapfactor=0.5):
    """
    Build a maze with 0,1,2 layers of prison (randomly).
    """

    p = rng.randint(0, 2)
    proll = rng.random()
    if proll < 0.5:
        p = 1
    elif proll < 0.7:
        p = 0
    elif proll < 0.9:
        p = 2
    else:
        p = 3

    add_r, add_c = room.anchor
    for j in range(p):
        cur_col = 2 * (j + 1) - 1
        for row in range(room.r):
            room.root.grid[row][cur_col] = WALL

        if j % 2 == 0:
            room.root.grid[0][cur_col] = EMPTY
        else:
            room.root.grid[room.r - 1][cur_col] = EMPTY

    room.rooms.append(Maze(room.r, room.c - (2 * p), (add_r, add_c + (2 * p)), room.root))
    for sub_room in room.rooms:
        make(rng, sub_room, depth + 1, gaps, vert, min_width, gapfactor)

    return 2 * p

Classes

class Maze (rows, cols, anchor=(0, 0), root=None)

Generate an empty maze. Anchor is the top left corner of this grid's position in its parent grid.

Expand source code
class Maze(object):
    def __init__(self, rows, cols, anchor=(0, 0), root=None):
        """
        Generate an empty maze.
        Anchor is the top left corner of this grid's position in its parent grid.
        """

        self.r = rows
        self.c = cols
        self.grid = [[EMPTY for col in range(cols)] for row in range(rows)]
        self.anchor = anchor
        self.rooms = []

        self.root = root
        if not self.root:
            self.root = self

    def to_map(self):
        """
        Add a flipped symmetric copy on the right.
        Add a border.
        """

        # Add a flipped symmetric copy
        for row in range(self.r):
            for col in range(self.c - 1, -1, -1):
                self.grid[self.r - row - 1].append(self.grid[row][col])
        self.c *= 2

        # Add a border
        for row in range(self.r):
            self.grid[row] = [WALL] + self.grid[row] + [WALL]
        self.c += 2

        self.grid.insert(0, [WALL for c in range(self.c)])
        self.grid.append([WALL for c in range(self.c)])
        self.r += 2

    def __str__(self):
        s = ''

        for row in range(self.r):
            for col in range(self.c):
                s += self.grid[row][col]
            s += '\n'

        return s[:-1]

    def add_wall(self, rng, i, gaps=1, vert=True):
        """
        Add a wall with gaps.
        """

        add_r, add_c = self.anchor
        if vert:
            gaps = min(self.r, gaps)
            slots = [add_r + x for x in range(self.r)]
            if 0 not in slots:
                if self.root.grid[min(slots) - 1][add_c + i] == EMPTY:
                    slots.remove(min(slots))

                if len(slots) <= gaps:
                    return 0
            if (self.root.c - 1) not in slots:
                if self.root.grid[max(slots) + 1][add_c + i] == EMPTY:
                    slots.remove(max(slots))

            if len(slots) <= gaps:
                return 0

            rng.shuffle(slots)
            for row in slots[int(round(gaps)):]:
                self.root.grid[row][add_c + i] = WALL

            self.rooms.append(Maze(self.r, i, (add_r, add_c), self.root))
            self.rooms.append(Maze(self.r, self.c - i - 1, (add_r, add_c + i + 1), self.root))
        else:
            gaps = min(self.c, gaps)
            slots = [add_c + x for x in range(self.c)]

            if 0 not in slots:
                if self.root.grid[add_r + i][min(slots) - 1] == EMPTY:
                    slots.remove(min(slots))

                if len(slots) <= gaps:
                    return 0

            if (self.root.r - 1) not in slots:
                if self.root.grid[add_r + i][max(slots) + 1] == EMPTY:
                    slots.remove(max(slots))

            if len(slots) <= gaps:
                return 0

            rng.shuffle(slots)
            for col in slots[int(round(gaps)):]:
                self.root.grid[add_r + i][col] = WALL

            self.rooms.append(Maze(i, self.c, (add_r, add_c), self.root))
            self.rooms.append(Maze(self.r - i - 1, self.c, (add_r + i + 1, add_c), self.root))

        return 1

Methods

def add_wall(self, rng, i, gaps=1, vert=True)

Add a wall with gaps.

Expand source code
def add_wall(self, rng, i, gaps=1, vert=True):
    """
    Add a wall with gaps.
    """

    add_r, add_c = self.anchor
    if vert:
        gaps = min(self.r, gaps)
        slots = [add_r + x for x in range(self.r)]
        if 0 not in slots:
            if self.root.grid[min(slots) - 1][add_c + i] == EMPTY:
                slots.remove(min(slots))

            if len(slots) <= gaps:
                return 0
        if (self.root.c - 1) not in slots:
            if self.root.grid[max(slots) + 1][add_c + i] == EMPTY:
                slots.remove(max(slots))

        if len(slots) <= gaps:
            return 0

        rng.shuffle(slots)
        for row in slots[int(round(gaps)):]:
            self.root.grid[row][add_c + i] = WALL

        self.rooms.append(Maze(self.r, i, (add_r, add_c), self.root))
        self.rooms.append(Maze(self.r, self.c - i - 1, (add_r, add_c + i + 1), self.root))
    else:
        gaps = min(self.c, gaps)
        slots = [add_c + x for x in range(self.c)]

        if 0 not in slots:
            if self.root.grid[add_r + i][min(slots) - 1] == EMPTY:
                slots.remove(min(slots))

            if len(slots) <= gaps:
                return 0

        if (self.root.r - 1) not in slots:
            if self.root.grid[add_r + i][max(slots) + 1] == EMPTY:
                slots.remove(max(slots))

        if len(slots) <= gaps:
            return 0

        rng.shuffle(slots)
        for col in slots[int(round(gaps)):]:
            self.root.grid[add_r + i][col] = WALL

        self.rooms.append(Maze(i, self.c, (add_r, add_c), self.root))
        self.rooms.append(Maze(self.r - i - 1, self.c, (add_r + i + 1, add_c), self.root))

    return 1
def to_map(self)

Add a flipped symmetric copy on the right. Add a border.

Expand source code
def to_map(self):
    """
    Add a flipped symmetric copy on the right.
    Add a border.
    """

    # Add a flipped symmetric copy
    for row in range(self.r):
        for col in range(self.c - 1, -1, -1):
            self.grid[self.r - row - 1].append(self.grid[row][col])
    self.c *= 2

    # Add a border
    for row in range(self.r):
        self.grid[row] = [WALL] + self.grid[row] + [WALL]
    self.c += 2

    self.grid.insert(0, [WALL for c in range(self.c)])
    self.grid.append([WALL for c in range(self.c)])
    self.r += 2