maze-maker-1.py

'''\
Python Maze Maker.

Experimenting with computer maze generation.
Maze model splits doors into North/South and West/East arrays.

      0   1   2   3   4   5   6   7
    +---+---+---+---+---+---+---+---+
   0|   #   #   #   #   #   #   #   |
    +-@-+-@-+-@-+-@-+-@-+-@-+-@-+-@-+  @ n/s door
   1|   #   #   # X #   #   #   #   |
    +-@-+-@-+-@-+-@-+-@-+-@-+-@-+-@-+
   2|   #   #   #   #   #   #   #   |  # e/w door
    +-@-+-@-+-@-+-@-+-@-+-@-+-@-+-@-+
   3|   #   #   #   #   #   #   #   |
    +---+---+---+---+---+---+---+---+

    Cells    = [4,8]     X   = [1,3]
    NS Doors = [3,8]     N/S = [0,3] / [1,3]
    WE Doors = [4,7]     W/E = [1,2] / [1,3]

Coder@Sonnack.com
November 2013
'''
####################################################################################################
from sys import argvstdoutstderrpath as syspath
from datetime import datetimedatetimedelta
from random import randomshuffle
import ImageImageDraw
from logger import loggerinfodebugtrace
from colors import *
####################################################################################################
Log = logger('MazeMaker1')

NumberCols = 40
NumberRows = 30

RandomWall = lambda f: random() < f

DirectionString = ['North''East''South''West']
DirectionAbbrev = ['N''E''S''W']

ShowImageBeforeSave = False


##------------------------------------------------------------------------------------------------##
class RandomWalk1 (object):
    max_steps   = 100   # max steps allows for a given path
    wall_change = 0.1   # probability of NOT changing direction when hitting wall
    occu_change = 0.1   # probability of NOT changing direction when hitting an occupied cell
    dir_change  = 1     ##TODO: this obviously needs rethinking

    def __init__ (selfmz):
        self.mz = mz
        self.row = 0
        self.col = 0
        self.direction = 2  # N,E,S,W
        self.tot_steps = 0
        self.dir_steps = 0
        self.path_list = []

    def carve_a_path (selfpath_color):
        Log.debug()
        Log.debug('NEW PATH...')

        if 0 < len(self.path_list):
            if not self.find_a_path():
                return
            Log.debug('using: (%d, %d)' % (self.rowself.col))

        # loop while forging a path...
        self.tot_steps = 0
        self.dir_steps = 0
        current_path = []
        while self.tot_steps < self.max_steps:
            s = DirectionAbbrev[self.direction]
            t = (self.tot_stepsself.dir_stepssself.rowself.col)
            Log.debug('STEP: %d (%d:%s)  [%d, %d]' % t)
            current_path.append([self.rowself.col2])

            # test next cell...
            tc = self.test_cell[self.direction](self)
            if tc < 0:
                # hit a wall...
                Log.trace('Hit the wall!')
                if self.wall_change < random():
                    if self.change_direction():
                        continue
                break
            if 0 < tc:
                # hit a traveled cell...
                Log.trace('Hit another path!')
                if self.occu_change < random():
                    if self.change_direction():
                        continue
                break

            # next cell isn't occupied, knock down the wall and go there...
            mc = self.move_to_next_cell[self.direction](self)
            # tag new cell...
            self.mz.set_cell(self.row,self.colpath_color)

            # increment counters...
            self.tot_steps = self.tot_steps + 1
            self.dir_steps = self.dir_steps + 1

            # consider a change of direction...
            if self.dir_change < (random() * self.dir_steps):
                if self.change_direction():
                    self.dir_steps = 0

        # add path to pat list...
        self.path_list.append(current_path)

    def find_a_path (self):
        '''Travel existing paths to find a new row,col and direction.'''

        # create a random order to try paths from the path list...
        maze_path_seq = [x for x in range(len(self.path_list))]
        shuffle(maze_path_seq)
        # pick a maze path (from the random list)...
        for mx,maze_path in map(lambda ix: (ixself.path_list[ix]), maze_path_seq):
            Log.trace('MazePath: %d  (%d cells)' % (mxlen(maze_path)))

            # create a ramdom order to try cells from the maze path...
            maze_cell_seq = [x for x in range(len(maze_path))]
            shuffle(maze_cell_seq)
            # pick a maze cell (from the random list)...
            for cx,path_cell in map(lambda ix: (ix,maze_path[ix]), maze_cell_seq):
                Log.trace('PathCell: %d  [%d,%d (%d)]' % (cxpath_cell[0], path_cell[1], path_cell[2]))

                # test the cell for a possible branch...
                if path_cell[2] < 4:
                    self.row = path_cell[0]
                    self.col = path_cell[1]
                    Log.trace('testing: (%d, %d)' % (self.rowself.col))
                    if self.test_north_cell() == 0:
                        self.direction = 0
                        path_cell[2] = path_cell[2] + 1
                        return True
                    if self.test_east_cell() == 0:
                        self.direction = 1
                        path_cell[2] = path_cell[2] + 1
                        return True
                    if self.test_south_cell() == 0:
                        self.direction = 2
                        path_cell[2] = path_cell[2] + 1
                        return True
                    if self.test_west_cell() == 0:
                        self.direction = 3
                        path_cell[2] = path_cell[2] + 1
                        return True

        Log.debug('Unable to find a path!')
        return False

    def change_direction (self):
        if random() < 0.5:
            # try turning left...
            d = (self.direction - 1) % 4
            if self.test_cell[d](self):
                # can't, try right...
                d = (self.direction + 1) % 4
                if self.test_cell[d](self):
                    # nope!
                    return False
            # cell is clear, this is good...
            Log.debug('NEW DIRECTION: %d' % d)
            self.direction = d
        else:
            # try turning right...
            d = (self.direction + 1) % 4
            if self.test_cell[d](self):
                # can't, try left...
                d = (self.direction - 1) % 4
                if self.test_cell[d](self):
                    # nope!
                    return False
            # cell is clear, this is good...
            Log.debug('NEW DIRECTION: %d' % d)
            self.direction = d
        return True

    def test_north_cell (self):
        # special north edge test...
        if self.row == 0:
            return -1
        return self.mz.get_cell(self.row-1self.col)

    def test_south_cell (self):
        if self.row == (NumberRows - 1):
        # special south edge test...
            return -1
        return self.mz.get_cell(self.row+1self.col)

    def test_west_cell (self):
        # special west edge test...
        if self.col == 0:
            return -1
        return self.mz.get_cell(self.rowself.col-1)

    def test_east_cell (self):
        # special east edge test...
        if self.col == (NumberCols - 1):
            return -1
        return self.mz.get_cell(self.rowself.col+1)

    def move_to_north_cell (self):
        # knock down the wall...
        self.mz.set_north_door(self.rowself.col0)
        self.row = self.row - 1

    def move_to_south_cell (self):
        # knock down the wall...
        self.mz.set_south_door(self.rowself.col0)
        self.row = self.row + 1

    def move_to_west_cell (self):
        # knock down the wall...
        self.mz.set_west_door(self.rowself.col0)
        self.col = self.col - 1

    def move_to_east_cell (self):
        # knock down the wall...
        self.mz.set_east_door(self.rowself.col0)
        self.col = self.col + 1

    test_cell = [test_north_celltest_east_celltest_south_celltest_west_cell]

    move_to_next_cell = [move_to_north_cellmove_to_east_cellmove_to_south_cellmove_to_west_cell]



##~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~##
class Maze (object):
    '''\
Maze class.
The maze is stored as three two-dimensional arrays:

    cells     - stores the state of the maze cell
    ns_doors  - stores the state of the north and south doors
    we_doors  - stores the state of the west and east doors

All data values are integers.  Array[row][col]: each row is an array of cols.

A cell non-zero cell value indicates some non-default status.
A non-zero value for a door means the door is closed (locked).
The value -1 indicates a maze outer wall, values above 0 indicate a locked state.
(The potential exists for multiple locked states; e.g. closed, closed+locked.)

The NS_Doors array has one less row -- no south-edge doors.
The WE_Doors array has one less column -- no east-edge doors.

There are no west-edge or north-edge doors, either, but the Doors arrays can
be thought of as EastDoors and SouthDoors arrays; except for the eastern and
southern edges, all cells have an east and south door. The west and north
doors are the east and south doors of the west and north cells, respectively.

For a given cell(row,col), the doors to that cell are:

    north door = NS_Doors(row-1, col  )  {{south door of north cell}}
    south door = NS_Doors(row  , col  )
    west door  = WE_Doors(row  , col-1)  {{east door of west cell}}
    east door  = WE_Doors(row  , col  )

'''
    def __init__ (selfrowscols):
        self.dims = (rowscols)
        self.cells  = [ [int(0for col in range(self.dims[1]) ] for row in range(self.dims[0])  ]
        self.ns_doors = [ [int(1for col in range(self.dims[1])   ] for row in range(self.dims[0]-1) ]
        self.we_doors = [ [int(1for col in range(self.dims[1]-1) ] for row in range(self.dims[0])   ]

    def write_image(selffn):
        im = MazeImg(self.dims)
        # paint the maze...
        for rx,row in enumerate(self.cells):
            for cx,col in enumerate(row):
                # set current cell...
                im.set_current_cell(rxcx)
                if 1 < col:
                    im.paint_cell(ColorPalette[col])
                # north wall...
                if (rx == 0and self.get_north_door(rx,cx):
                    im.draw_north_wall()
                # south wall...
                if self.get_south_door(rx,cx):
                    im.draw_south_wall()
                # west wall...
                if (cx == 0and self.get_west_door(rx,cx):
                    im.draw_west_wall()
                # east wall...
                if self.get_east_door(rx,cx):
                    im.draw_east_wall()
        im.save(fn)
        if ShowImageBeforeSave: im.show()
        return im

    def all_cells_occupied (self):
        for maze_row in self.cells:
            for maze_cell in maze_row:
                if maze_cell == 0:
                    return False
        return True

    def get_cell (selfrowcol):
        '''Get state of cell(row,col).'''
        self._test_row_col(rowcol)
        return self.cells[row][col]

    def set_cell (selfrowcolstate):
        '''Set state of cell(row,col).'''
        self._test_row_col(rowcol)
        self.cells[row][col] = state

    def set_north_door (selfrowcolstate):
        '''Set state of north door of cell(row,col).'''
        self._test_row_col(rowcol)
        if row == 0:
            raise Exception"Can't set north wall door!"
        self._test_ns_door_row_col(row-1,col)
        self.ns_doors[row-1][col] = state

    def get_north_door (selfrowcol):
        '''Get state of north door of cell(row,col).'''
        if row == 0:
            return -1 # north wall
        self._test_ns_door_row_col(row-1,col)
        return self.ns_doors[row-1][col]

    def set_south_door (selfrowcolstate):
        '''Set state of south door of cell(row,col).'''
        self._test_row_col(rowcol)
        if (self.dims[0]-1) <= row:
            raise Exception"Can't set south wall door!"
        self._test_ns_door_row_col(row,col)
        self.ns_doors[row][col] = state

    def get_south_door (selfrowcol):
        '''Get state of south door of cell(row,col).'''
        if (self.dims[0]-1) == row:
            return -1 # south wall
        self._test_ns_door_row_col(row,col)
        return self.ns_doors[row][col]

    def set_west_door (selfrowcolstate):
        '''Set state of west door of cell(row,col).'''
        self._test_row_col(rowcol)
        if col <= 0:
            raise Exception"Can't set west wall door!"
        self._test_we_door_row_col(row,col-1)
        self.we_doors[row][col-1] = state

    def get_west_door (selfrowcol):
        '''Get state of west door of cell(row,col).'''
        if col == 0:
            return -1 # west wall
        self._test_we_door_row_col(row,col-1)
        return self.we_doors[row][col-1]

    def set_east_door (selfrowcolstate):
        '''Set state of east door of cell(row,col).'''
        self._test_row_col(rowcol)
        if (self.dims[1]-1) <= col:
            raise Exception"Can't set east wall door!"
        self._test_we_door_row_col(row,col)
        self.we_doors[row][col] = state

    def get_east_door (selfrowcol):
        '''Get state of east door of cell(row,col).'''
        if (self.dims[1]-1) == col:
            return -1 # east wall
        self._test_we_door_row_col(row,col)
        return self.we_doors[row][col]

    def _test_row_col (selfrowcol):
        '''Test (row,col) for access to cell state data.'''
        if (row < 0or (self.dims[0] <= row):
            raise Exception, ("Row (%d) is out of bounds!" % row)
        if (col < 0or (self.dims[1] <= col):
            raise Exception, ("Col (%d) is out of bounds!" % col)

    def _test_ns_door_row_col (selfrowcol):
        '''Test (row,col) for access to north/south door data.'''
        if (row < 0or ((self.dims[0]-1) <= row):
            raise Exception, ("Row (%d) is out of bounds!" % row)
        if (col < 0or (self.dims[1] <= col):
            raise Exception, ("Col (%d) is out of bounds!" % col)

    def _test_we_door_row_col (selfrowcol):
        '''Test (row,col) for access to east/west door data.'''
        if (row < 0or (self.dims[0] <= row):
            raise Exception, ("Row (%d) is out of bounds!" % row)
        if (col < 0or ((self.dims[1]-1) <= col):
            raise Exception, ("Col (%d) is out of bounds!" % col)


##~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~##
class MazeImg (object):
    '''Image class.'''
    HorzSpace = 20
    VertSpace = 20
    WallColor = ColorBlack

    def __init__ (self_dims):
        '''Create new Maze Image instance.  NOTE: x=cols, y=rows!'''
        # Calculate pixel parameters based on rows and columns...
        self.dims = _dims
        self.pixels = (self.HorzSpace * (self.dims[1]+2), self.VertSpace * (self.dims[0]+2))
        self.pmin = (self.HorzSpaceself.VertSpace)
        self.pmax = (self.pixels[0] - self.HorzSpaceself.pixels[1] - self.VertSpace)
        # current square is ULC...
        self.p0 = (self.pmin[0]               , self.pmin[1])
        self.p1 = (self.pmin[0]+self.HorzSpaceself.pmin[1]+self.VertSpace)
        # Create image object...
        self.im = Image.new('RGB'self.pixelsColorWhite)
        self.draw = ImageDraw.Draw(self.im)
        # Draw outer border...
        self.draw.rectangle((0,0)+(self.pixels[0]-1,self.pixels[1]-1), outline=ColorBlack)
        self.draw.rectangle((1,1)+(self.pixels[0]-2,self.pixels[1]-2), outline=ColorGray100)
        self.draw.rectangle((2,2)+(self.pixels[0]-3,self.pixels[1]-3), outline=ColorGray200)
        # vertical rules...
        for ix in range(1self.dims[1]):
            pp = ix * self.HorzSpace
            self.draw.line([(self.pmin[0]+ppself.pmin[1]), (self.pmin[0]+ppself.pmax[1])], fill=ColorLightBlue)
        # horizontal rules...
        for ix in range(1self.dims[0]):
            pp = ix * self.VertSpace
            self.draw.line([(self.pmin[0], self.pmin[1]+pp), (self.pmax[0], self.pmin[1]+pp)], fill=ColorLightBlue)
        # margins
        self.draw.line([(self.pmin[0], self.pmin[1]), (self.pmax[0], self.pmin[1])], fill=ColorDarkGreenwidth=2# th
        self.draw.line([(self.pmin[0], self.pmax[1]), (self.pmax[0], self.pmax[1])], fill=ColorDarkGreenwidth=2# bh
        self.draw.line([(self.pmin[0], self.pmin[1]), (self.pmin[0], self.pmax[1])], fill=ColorDarkGreenwidth=2# lv
        self.draw.line([(self.pmax[0], self.pmin[1]), (self.pmax[0], self.pmax[1])], fill=ColorDarkGreenwidth=2# rv

    def set_current_cell (selfrowcol):
        '''Set current cell for drawing operations.'''
        if (row < 0or (self.dims[0] <= row):
            raise Exception"Row is out of bounds!"
        if (col < 0or (self.dims[1] <= col):
            raise Exception"Col is out of bounds!"
        cx = self.pmin[0] + (col * self.HorzSpace)
        cy = self.pmin[1] + (row * self.VertSpace)
        self.p0 = (cx,cy)
        self.p1 = (cx+self.HorzSpacecy+self.VertSpace)

    def paint_cell (selfcolor):
        self.draw.rectangle([self.p0self.p1], fill=coloroutline=ColorBlack)

    def draw_north_wall (self):
        '''Draw north wall in current cell.'''
        self.draw.line([(self.p0[0], self.p0[1]), (self.p1[0], self.p0[1])], fill=self.WallColorwidth=3)

    def draw_south_wall (self):
        '''Draw south wall in current cell.'''
        self.draw.line([(self.p0[0], self.p1[1]), (self.p1[0], self.p1[1])], fill=self.WallColorwidth=3)

    def draw_west_wall (self):
        '''Draw west wall in current cell.'''
        self.draw.line([(self.p0[0], self.p0[1]), (self.p0[0], self.p1[1])], fill=self.WallColorwidth=3)

    def draw_east_wall (self):
        '''Draw east wall in current cell.'''
        self.draw.line([(self.p1[0], self.p0[1]), (self.p1[0], self.p1[1])], fill=self.WallColorwidth=3)

    def show (self):
        self.im.show()
        return self

    def save (selffnmode='GIF'):
        self.im.save(fnmode)
        return self

    def __str__ (self):
        s = '%s (%d,%d) %s'
        t = (self.im.formatself.im.size[0], self.im.size[1], self.im.mode)
        return s % t

    def __repr__ (self):
        s = '{Img:{format:"%s", size:%s, mode:"%s", addr:%x}}'
        t = (self.im.formatstr(self.im.size), self.im.modeself.__hash__())
        return s % t



##------------------------------------------------------------------------------------------------##
def simple_manual_demo (mz):
    # open doors...
    mz.set_east_door (0,00)
    mz.set_east_door (0,10)
    mz.set_east_door (0,20)
    mz.set_south_door(0,30)
    mz.set_west_door (1,30)
    mz.set_south_door(1,20)
    mz.set_south_door(2,20)
    mz.set_south_door(3,20)
    mz.set_west_door (4,20)
    mz.set_south_door(4,10## 4,1
    mz.set_south_door(5,10)
    mz.set_south_door(6,10)
    mz.set_east_door (7,10)
    mz.set_north_door(7,20)
    mz.set_north_door(6,20)
    # dead end
    mz.set_north_door(4,10## 4,1
    mz.set_north_door(3,10)
    mz.set_north_door(2,10)
    mz.set_west_door (1,10)
    mz.set_south_door(1,00)
    mz.set_south_door(2,00)
    mz.set_south_door(3,00)
    mz.set_south_door(4,00)
    mz.set_south_door(5,00)
    mz.set_south_door(6,00)
    # etc...


##================================================================================================##
def Main ():
    fns = []
    for tx in range(50):
        Log.info(tx)
        mz = Maze(NumberRowsNumberCols)
        # Set start cell...
        mz.set_cell(0,0ixColorGreen)
        # set end cell...
        mz.set_cell(NumberRows-1NumberCols-1ixColorLightMagenta)
        mz.set_north_door (NumberRows-1NumberCols-10)

        #simple_manual_demo(mz)

        maze_maker = RandomWalk1(mz)
        Log.info()

        for ix in range(2000):
            maze_maker.carve_a_path(1)
            if mz.all_cells_occupied():
                Log.info('Finished after %d iterations.' % ix)
                break

        Log.info('Path List:')
        for ix,p in enumerate(maze_maker.path_list):
            #Log.info('%: %s' % (ix, str(p)))
            Log.info(p)
            Log..info()
        Log.info()

        fn = 'out\\maze-%02d.gif' % tx
        fns.append(fn)
        mz.write_image(fn)

    return fns


##================================================================================================##
if __name__ == '__main__':
    print 'autorun:',argv[0]
    Log.start('maze-maker-1.log')
    Log.level(trace())
    try:
        obj = Main()
        Log.info()
        Log.info('done:',type(obj))
    except Exception as e:
        print >> stderre
        Log.error(e)
    finally:
        Log.end()

####################################################################################################
'''eof'''