Rule Based Modeling: The Frankenstein Fish, It’s Alive!

Nick Kunz
Data Mining the City
4 min readOct 31, 2017

Using the originally developed agent based simulation by Allan William Martin in Processing + Python, I utilized a similar set of originally assigned behaviors for the small blue fish. However, in order to mix it up a bit, I added another “species” of fish into the existing fish population.

The new fish I added was the larger dark gray fish, I named “Frankenstein.” As you may have noticed in the animation, the Frankenstein fish is not only larger, but exhibits slightly more aggressive behavior, slightly more indifferent to schooling, and is a little bit faster than its small blue fish counterpart. Also, the Frankenstein fish is less common. Ergo, they’re fewer in numbers. Beware…

Happy Halloween! :)

Window A: FishesSchooling

import math
import random
from behaviors import *
def setup():
size(1200, 600)
number_of_sm_fish = 85
number_of_bg_fish = 15
global sm_behaviors
sm_behaviors = (
MoveTowardsCenterOfNearbyFish(closeness=5.0, threshold=25.0, speedfactor=10RR0.0, weight=20.0),
TurnAwayFromClosestFish(threshold=5.0, speedfactor=5.0, weight=20.0),
TurnToAverageDirection(closeness=50.0, weight=50.0),
Swim(speedlimit=3.0, turnratelimit=math.pi / 50.0),
WrapAroundWindowEdges()
)

global lg_behaviors
lg_behaviors = (
MoveTowardsCenterOfNearbyFish(closeness=25.0, threshold=50.0, speedfactor=100.0, weight=10.0),
TurnAwayFromClosestFish(threshold=35.0, speedfactor=50.0, weight=10.0),
TurnToAverageDirection(closeness=40.0, weight=25.0),
Swim(speedlimit=50.0, turnratelimit=math.pi / 10.0),
WrapAroundWindowEdges()
)
global smallfishes
smallfishes = []
for i in xrange(0, number_of_sm_fish):
smallfishes.append(SmallFish())

global bigfishes
bigfishes = []
for i in xrange(0, number_of_bg_fish):
bigfishes.append(BigFish())
def draw():
background(0, 0, 0)

for fish in smallfishes:
fish.move()
fish.draw()

for fish in bigfishes:
fish.move()
fish.draw()
class SmallFish(object):
fishcolors = (color(204, 229, 255), color(102, 178, 255), color(0, 128, 255))
def __init__(self):
self.position = [random.randrange(0, width), random.randrange(0, height)]
self.speed = 1
self.direction = random.random() * 2.0 * math.pi - math.pi
self.turnrate = 0
self.fishcolor = SmallFish.fishcolors[random.randrange(0, len(SmallFish.fishcolors))]def move(self):
global smallfishes, behaviors
state = {}for fish in smallfishes:
for sm_behavior in sm_behaviors:
sm_behavior.setup(self, fish, state)
for sm_behavior in sm_behaviors:
sm_behavior.apply(self, state)
def draw(self):
pushMatrix()
translate(*self.position)
rotate(self.direction)
stroke(self.fishcolor)
strokeWeight(2)
fill(self.fishcolor)
bezier(0, 0, 10, 7, 15, 0, 25, 0)
bezier(0, 0, 10, -7, 15, 0, 25, 0)
line(7, 3, 12, 8)
line(7, -3, 12, -8)

popMatrix()
class BigFish(object):
def __init__(self):
self.position = [random.randrange(0, width), random.randrange(0, height)]
self.speed = 1
self.direction = random.random() * 2.0 * math.pi - math.pi
self.turnrate = 0
def move(self):
global smallfishes, behaviors
state = {}for fish in smallfishes:
for lg_behavior in lg_behaviors:
lg_behavior.setup(self, fish, state)
for lg_behavior in lg_behaviors:
lg_behavior.apply(self, state)
def draw(self):
pushMatrix()
translate(*self.position)
rotate(self.direction)
stroke(255)
strokeWeight(1)
fill(64,64,64)
bezier(0, 0, 20, 10, 23, 0, 50, 0)
bezier(0, 0, 20, -10, 23, 0, 50, 0)
line(13, 4, 20, 10)
line(13, -4, 20, -10)

popMatrix()

Window B: behaviors.py

import mathclass Behavior(object):
def __init__(self, **parameters):
self.parameters = parameters
def setup(self, fish, otherfish, state):
pass
def apply(self, fish, state):
pass
def draw(self, fish, state):
pass
class MoveTowardsCenterOfNearbyFish(Behavior):
def setup(self, fish, otherfish, state):
if fish is otherfish:
return
if 'closecount' not in state:
state['closecount'] = 0.0
if 'center' not in state:
state['center'] = [0.0, 0.0]
closeness = self.parameters['closeness']
distance_to_otherfish = dist(
otherfish.position[0], otherfish.position[1],
fish.position[0], fish.position[1])
if distance_to_otherfish < closeness:
if state['closecount'] == 0:
state['center'] = otherfish.position
state['closecount'] += 1.0
else:
state['center'][0] *= state['closecount']
state['center'][1] *= state['closecount']
state['center'] = [state['center'][0] + otherfish.position[0], state['center'][1] + otherfish.position[1]]
state['closecount'] += 1.0state['center'][0] /= state['closecount']
state['center'][1] /= state['closecount']
def apply(self, fish, state):
if state['closecount'] == 0:
return
center = state['center']
distance_to_center = dist(
center[0], center[1],
fish.position[0], fish.position[1]
)
if distance_to_center > self.parameters['threshold']:
angle_to_center = math.atan2(fish.position[1] - center[1], fish.position[0] - center[0])
fish.turnrate += (angle_to_center - fish.direction) / self.parameters['weight']
fish.speed += distance_to_center / self.parameters['speedfactor']
def draw(self, fish, state):
closeness = self.parameters['closeness']
stroke(200, 200, 255)
noFill()
ellipse(fish.position[0], fish.position[1], closeness * 2, closeness * 2)
class TurnAwayFromClosestFish(Behavior):
def setup(self, fish, otherfish, state):
if fish is otherfish:
return
if 'closest_fish' not in state:
state['closest_fish'] = None
if 'distance_to_closest_fish' not in state:
state['distance_to_closest_fish'] = 1000000
distance_to_otherfish = dist(otherfish.position[0], otherfish.position[1],fish.position[0], fish.position[1])if distance_to_otherfish < state['distance_to_closest_fish']:
state['distance_to_closest_fish'] = distance_to_otherfish
state['closest_fish'] = otherfish
def apply(self, fish, state):
closest_fish = state['closest_fish']
if closest_fish is None:
return
distance_to_closest_fish = state['distance_to_closest_fish']
if distance_to_closest_fish < self.parameters['threshold']:
angle_to_closest_fish = math.atan2(fish.position[1] - closest_fish.position[1], fish.position[0] - closest_fish.position[0])
fish.turnrate -= (angle_to_closest_fish - fish.direction) / self.parameters['weight']
fish.speed += self.parameters['speedfactor'] / distance_to_closest_fish
def draw(self, fish, state):
stroke(100, 255, 100)
closest = state['closest_fish']
line(fish.position[0], fish.position[1], closest.position[0], closest.position[1])
class TurnToAverageDirection(Behavior):
def setup(self, fish, otherfish, state):
if fish is otherfish:
return
if 'average_direction' not in state:
state['average_direction'] = 0.0
if 'closecount_for_avg' not in state:
state['closecount_for_avg'] = 0.0
distance_to_otherfish = dist(
otherfish.position[0], otherfish.position[1],
fish.position[0], fish.position[1]
)
closeness = self.parameters['closeness']
if distance_to_otherfish < closeness:
if state['closecount_for_avg'] == 0:
state['average_direction'] = otherfish.direction
state['closecount_for_avg'] += 1.0
else:
state['average_direction'] *= state['closecount_for_avg']
state['average_direction'] += otherfish.direction
state['closecount_for_avg'] += 1.0
state['average_direction'] /= state['closecount_for_avg']
def apply(self, fish, state):
if state['closecount_for_avg'] == 0:
return
average_direction = state['average_direction']
fish.turnrate += (average_direction - fish.direction) / self.parameters['weight']
class Swim(Behavior):
def setup(self, fish, otherfish, state):
fish.speed = 1
fish.turnrate = 0
def apply(self, fish, state):
if fish.speed > self.parameters['speedlimit']:
fish.speed = self.parameters['speedlimit']
fish.position[0] -= math.cos(fish.direction) * fish.speed
fish.position[1] -= math.sin(fish.direction) * fish.speed
if fish.turnrate > self.parameters['turnratelimit']:
fish.turnrate = self.parameters['turnratelimit']
if fish.turnrate < -self.parameters['turnratelimit']:
fish.turnrate = -self.parameters['turnratelimit']
fish.direction += fish.turnrate
if fish.direction > math.pi:
fish.direction -= 2 * math.pi
if fish.direction < -math.pi:
fish.direction += 2 * math.pi
class WrapAroundWindowEdges(Behavior):
def apply(self, fish, state):
if fish.position[0] > width:
fish.position[0] = 0
if fish.position[0] < 0:
fish.position[0] = width
if fish.position[1] > height:
fish.position[1] = 0
if fish.position[1] < 0:
fish.position[1] = height

--

--