Maruyama’s Pond Slime






Coursework for Design Computation II


Implementation of Maruyama's pond slime algorithm in both 2d and 3d using Rhino and GH Python.  A simple cell-based algorithm for simluating pond slime growth using the following rules:

1.  If there are more than or equal to four cells in a straight line by the active cell, make a left turn, then a right one. 


2. If unblocked and there are less than or equal to four cells in the same line, active cells grow stright. If active cells hit boundary, make a turn by the last turning direction.


3. If blocked by itself, make a left turn. 


4. If blocked by others, make a right turn.


The 3d simulation appends the following modifications:

1. Cells may navigate on XY, XZ, and YZ planes. A cell can only turn on planes that is not perpendicular to its moving direction.

2. A cell turns left or right following the original algorith. At each turn, it picks a plane from random.


Program: Design Computation
Date: 2018







Demonstration of simulation.




GH Python Code:
__author__ = "Vincent Mai"
__version__ = "2018.11.10"

"""
 Design Computation II
 Maruyama's Pond Slime 2D
""" 
import random
import ghpythonlib.treehelpers as th

# GH data input:
n = num
rows, cols = size, size


class Slime(object):
    def __init__(self):
        self.locs = set()    # a set of location the slime occupies
        self.frontiers = []  # stores two activeCells
        self.active = True
        self.color = (random.randint(0, 255),
                      random.randint(0, 255),
                      random.randint(0, 255))
        
    def initCells(self, emptyGrid):
        """
        initialize the first two cells of slime
        """
        loc1 = random.sample(emptyGrid, 1)[0]
        loc2Options = []
        for d in [(1, 0), (-1, 0), (0, -1), (0, 1)]:
            option = (loc1[0]+d[0], loc1[1]+d[1])
            if option in emptyGrid: loc2Options.append(option)
        loc2 = random.choice(loc2Options)
        dir1, dir2 = (loc1[0]-loc2[0], loc1[1]-loc2[1]), 
                     (loc2[0]-loc1[0], loc2[1]-loc1[1])
        cell1 = ActiveCell(loc1, dir1)
        cell2 = ActiveCell(loc2, dir2)
        self.frontiers += [cell1, cell2]
        self.update(emptyGrid)

    def update(self, emptyGrid):
        """
        update slime occupied locations and emptygrid cells
        """
        self.locs |= {self.frontiers[0].loc, self.frontiers[1].loc}
        emptyGrid -= self.locs
        if not self.frontiers[0].active and not self.frontiers[1].active:
            self.active = False

    def grow(self, emptyGrid):
        """
        grow slime if cells in frontiers are active
        """
        for cell in self.frontiers:
            if cell.active == True:
                cell.turn(self.locs, emptyGrid, boundaryGrid)
                nextCell = cell.getNextCell()
                if nextCell not in emptyGrid:
                    cell.active = False
                else:
                    cell.loc = nextCell
                    cell.curLen += 1 
        self.update(emptyGrid)     

class ActiveCell(object):
    def __init__(self, location, direction):
        self.loc = location
        self.dir = direction
        self.curTurn = 'left'
        self.curLen = 3    # length of current line
        self.active = True

    def getNextCell(self):
        """
        return the next potential cell to grow to
        """
        nextCell = (self.loc[0]+self.dir[0], self.loc[1]+self.dir[1])
        return nextCell

    def changeDir(self, turn):
        """
        update direction as specified by turn
        """
        if turn == 'left':
            self.dir = (-self.dir[1], self.dir[0])
        elif turn == 'right':
            self.dir = (self.dir[1], -self.dir[0])

    def turn(self, slimeLocs, emptyGrid, boundaryGrid):
        """
        turn based on the next cell encountered
        """
        nextCell = self.getNextCell()
        if nextCell in emptyGrid:
            if self.curLen >= 4 and self.curTurn == 'left':
                self.changeDir(self.curTurn)
                self.curTurn = 'right'
                self.curLen = 1
            elif self.curLen >= 4 and self.curTurn == 'right':
                self.changeDir(self.curTurn)
                self.curTurn = 'left'
                self.curLen = 1
        if nextCell not in boundaryGrid:
            self.changeDir(self.curTurn)
            self.curLen = 1
        elif nextCell in slimeLocs:
            self.changeDir('left')
            self.curTurn == 'left'
            self.curLen = 1
        elif nextCell not in emptyGrid:
            self.changeDir('right')
            self.curTurn == 'right'
            self.curLen = 1

if reset:
    # generate grid
    boundaryGrid = set((row, col) for row in range(rows) for col in range(cols))
    emptyGrid = set((row, col) for row in range(rows) for col in range(cols))
    random.seed(0)
    slimeList = []
    colorsList = []
    branchesList = []
    indicesList = []
    for i in range(n):
        newSlime = Slime()
        newSlime.initCells(emptyGrid)
        slimeList.append(newSlime)

else:
    branchesList = []
    indicesList = []
    colorsList = []
    for slime in slimeList:
        slime.grow(emptyGrid)
        branchesList.append([e[0] for e in slime.locs])
        indicesList.append([e[1] for e in slime.locs])
        colorsList.append(str(slime.color))

branches = th.list_to_tree(branchesList)
indices = th.list_to_tree(indicesList)
color = th.list_to_tree(colorsList)



__author__ = "Vincent Mai"
__version__ = "2018.11.10"

"""
 Design Computation II
 Maruyama's Pond Slime 3D
""" 
import random
import ghpythonlib.treehelpers as th

# GH data input:
n = num
rows, cols, heis = size, size, size


class Slime(object):
    def __init__(self):
        self.locs = set()    # a set of location the slime occupies
        self.frontiers = []  # stores two activeCells
        self.active = True
        self.color = (random.randint(0, 255),
                      random.randint(0, 255),
                      random.randint(0, 255))
        
    def initCells(self, emptyGrid):
        """
        initialize the first two cells of slime
        """
        loc1 = random.sample(emptyGrid, 1)[0]
        loc2Options = []
        for d in [(1, 0, 0), (-1, 0, 0), (0, -1, 0), 
                  (0, 1, 0), (0, 0, 1), (0, 0, -1)]:
            option = (loc1[0]+d[0], loc1[1]+d[1], loc1[2]+d[2])
            if option in emptyGrid: loc2Options.append(option)
        loc2 = random.choice(loc2Options)
        dir1, dir2 = (loc1[0]-loc2[0], loc1[1]-loc2[1], loc1[2]-loc2[2]), 
                     (loc2[0]-loc1[0], loc2[1]-loc1[1], loc2[2]-loc1[2])
        plane1, plane2 = random.choice(ActiveCell.getPlanes(dir1)), 
                         random.choice(ActiveCell.getPlanes(dir2))
        cell1 = ActiveCell(loc1, dir1, plane1)
        cell2 = ActiveCell(loc2, dir2, plane2)
        self.frontiers += [cell1, cell2]
        self.update(emptyGrid)
        

    def update(self, emptyGrid):
        """
        update slime occupied locations and emptygrid cells
        """
        self.locs |= {self.frontiers[0].loc, self.frontiers[1].loc}
        emptyGrid -= self.locs
        if not self.frontiers[0].active and not self.frontiers[1].active:
            self.active = False

    def grow(self, emptyGrid):
        """
        grow slime if cells in frontiers are active
        """
        for cell in self.frontiers:
            if cell.active == True:
                cell.turn(self.locs, emptyGrid, boundaryGrid)
                nextCell = cell.getNextCell()
                if nextCell not in emptyGrid:
                    cell.active = False
                else:
                    cell.loc = nextCell
                    cell.curLen += 1 
        self.update(emptyGrid)     

class ActiveCell(object):
    def __init__(self, location, direction, plane):
        self.loc = location
        self.dir = direction
        self.curTurn = 'left'
        self.curPlane = plane
        self.curLen = 3    # length of current line
        self.active = True
        
    @staticmethod
    def getPlanes(dir):
        """
        return all possible planes from a given direction
        planes should not be perpendicular to direction
        """
        allPlanes = ('yzPlane', 'xzPlane', 'xyPlane')
        planes = []
        for i in range(len(dir)):
            if dir[i] == 0:
                planes.append(allPlanes[i])
        return planes

    def getNextCell(self):
        """
        return the next potential cell to grow to
        """
        nextCell = (self.loc[0]+self.dir[0], self.loc[1]+self.dir[1],
                    self.loc[2]+self.dir[2])
        return nextCell

    def changeDir(self, turn, plane):
        """
        update direction as specified by turn
        in 3D the turns are left, and the plane
        on which the slime is currently growing
        """
        
        if plane == 'xyPlane':
            if turn == 'left':
                self.dir = (-self.dir[1], self.dir[0], self.dir[2])
            elif turn == 'right':
                self.dir = (self.dir[1], -self.dir[0], self.dir[2])
        if plane == 'xzPlane':
            if turn == 'left':
                self.dir = (-self.dir[2], self.dir[1], self.dir[0])
            elif turn == 'right':
                self.dir = (self.dir[2], self.dir[1], -self.dir[0])
        if plane == 'yzPlane':
            if turn == 'left':
                self.dir = (self.dir[0], -self.dir[2], self.dir[1])
            elif turn == 'right':
                self.dir = (self.dir[0], self.dir[2], -self.dir[1])

    def turn(self, slimeLocs, emptyGrid, boundaryGrid):
        """
        turn based on the next cell encountered
        """
        nextCell = self.getNextCell()
        if nextCell in emptyGrid and self.curLen >= 4:
            self.changeDir(self.curTurn, self.curPlane)
            self.curPlane = random.choice(ActiveCell.getPlanes(self.dir))
            if self.curTurn == 'left':
                self.curTurn = 'right'
                self.curLen = 1
            elif self.curTurn == 'right':
                self.curTurn = 'left'
                self.curLen = 1
        if nextCell not in boundaryGrid:
            self.changeDir(self.curTurn, self.curPlane)
            self.curPlane = random.choice(ActiveCell.getPlanes(self.dir))
            self.curLen = 1
        elif nextCell in slimeLocs:
            self.changeDir('left', self.curPlane)
            self.curPlane = random.choice(ActiveCell.getPlanes(self.dir))
            self.curTurn == 'left'
            self.curLen = 1
        elif nextCell not in emptyGrid:
            self.changeDir('right', self.curPlane)
            self.curPlane = random.choice(ActiveCell.getPlanes(self.dir))
            self.curTurn == 'right'
            self.curLen = 1

if reset:
    # generate grid
    boundaryGrid = set((row, col, hei) for row in range(rows) 
                                       for col in range(cols) 
                                       for hei in range(heis))
    emptyGrid = set((row, col, hei) for row in range(rows)
                                    for col in range(cols) 
                                    for hei in range(heis))
    random.seed(0)
    slimeList = []
    colorsList = []
    rowsList = []
    colsList = []
    heisList = []
    for i in range(n):
        newSlime = Slime()
        newSlime.initCells(emptyGrid)
        slimeList.append(newSlime)

else:
    rowsList = []
    colsList = []
    heisList = []
    colorsList = []
    for slime in slimeList:
        slime.grow(emptyGrid)
        rowsList.append([e[0] for e in slime.locs])
        colsList.append([e[1] for e in slime.locs])
        heisList.append([e[2] for e in slime.locs])
        colorsList.append(str(slime.color))

x = th.list_to_tree(rowsList)
y = th.list_to_tree(colsList)
z = th.list_to_tree(heisList)
color = th.list_to_tree(colorsList)