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.
Type: design computation study
Role: code implementation
Date: 2018
Simulation Demo
GH Python Code (Pond Slime 2D):
__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)
GH Python Code (Pond Slime 3D):
__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)