#! /usr/bin/env python
import collections
import math
import random
import processing
import vector
################################################################################
# QVGA Resolution
WIDTH = 320
HEIGHT = 240
################################################################################
class Demo(processing.Process):
"Demo.main(width, height) -> Starts the demonstration"
NOT_CREATING_FISH = -1
SCHOOLS = ('red', 'yellow'), ('blue', 'green')
def setup(self, background):
"Setup the screen and boids before starting simulation."
background('black')
self.schools = []
self.sources = []
for body_color, trim_color in self.SCHOOLS:
new_school = School(body_color, trim_color)
new_source = vector.Vector2(random.random() * WIDTH,
random.random() * HEIGHT)
self.schools.append(new_school)
self.sources.append(new_source)
self.pointer = 0
def render(self, graphics):
"Displays the boids on the graphics every time called."
self.add_fish()
graphics.clear()
fish = 0
for school in self.schools:
school.render(graphics)
fish += school.size()
graphics.write(5, 5, fish, 'white')
def add_fish(self):
"Add a fish to the next school while creating more fish."
if self.pointer != self.NOT_CREATING_FISH:
new_fish = Fish(self.sources[self.pointer])
self.schools[self.pointer].add_fish(new_fish)
self.pointer = (self.pointer + 1) % len(self.SCHOOLS)
def update(self, interval):
"Run one step of the physics simulation for the interval."
for school in self.schools:
school.update(interval)
def mouse_pressed(self, event):
"Create fish at cursor and add to the smallest school."
new_fish = Fish(vector.Vector2(event.x, event.y))
min(self.schools, key=School.size).add_fish(new_fish)
def speed_warning(self):
"Stop creating fish and remove fish from largest school."
self.pointer = self.NOT_CREATING_FISH
max(self.schools, key=School.size).kill_fish()
################################################################################
class Fish:
"Fish(location) -> Fish"
HOW_WIDE = 6 # Width of the fish
HOW_LONG = 12 # Length of the fish
MAX_FORCE = 0.05 # Maximum directional steering force
MAX_SPEED = 60.0 # Maximum speed at which to travel
SEP_FACTOR = 1.5 # Arbitrary separation mutliplier
ALI_FACTOR = 1.0 # Arbitrary alignment mutliplier
COH_FACTOR = 1.0 # Arbitrary cohesion mutliplier
DESIRED_SEPARATION = 7 # Turn from each other when closer
NEIGHBOR_DISTANCE = 17 # Maximum distance for interactions
########################################################################
# DO NOT CHANGE THE FOLLOWING SECTION
limits = math.hypot(HOW_LONG, HOW_WIDE)
radius = limits / 2
DESIRED_SEPARATION += limits
NEIGHBOR_DISTANCE += limits
TOP = 0 - radius
LEFT = 0 - radius
RIGHT = WIDTH + radius
BOTTOM = HEIGHT + radius
SHAPE = processing.Polygon(vector.Vector2(HOW_LONG / +2, 0),
vector.Vector2(HOW_LONG / -2, HOW_WIDE / +2),
vector.Vector2(HOW_LONG / -2, HOW_WIDE / -2))
del limits, radius, HOW_LONG, HOW_WIDE
__slots__ = 'location', 'velocity', 'steering', 'body_color', 'trim_color'
# END OF PRECALCULATED FISH VARIABLES
########################################################################
def __init__(self, location):
"Initialize the fish with several vectors and colors."
self.location = location.copy()
self.velocity = vector.Polar2(random.random() * self.MAX_SPEED,
random.random() * 360)
self.body_color = ''
self.trim_color = ''
def paint(self, body_color, trim_color):
"Assign colors to the fish's body and trim (outline)."
self.body_color = body_color
self.trim_color = trim_color
def render(self, graphics):
"Draw the fish's shape on the given graphics context."
polygon = self.SHAPE.copy()
polygon.rotate(self.velocity.direction)
polygon.translate(self.location)
graphics.draw(polygon, self.body_color, self.trim_color)
def run_AI(self, school):
"Execute the three boid rules and store in steering."
self.steering = vector.Vector2(0, 0)
# Follow rules of separation, alignment, and cohesion.
separation = vector.Vector2(0, 0)
alignment = vector.Vector2(0, 0)
cohesion = vector.Vector2(0, 0)
# Track fish that are too close along with neighbours.
too_close = False
neighbors = 0
# Loop over all other fish from the school fish is in.
for fish in school:
if fish is not self:
# Get the difference in location and distance.
offset = self.location - fish.location
length = offset.magnitude
# Find fish that are too close to current one.
if length < self.DESIRED_SEPARATION:
separation += offset.normalize() / length
too_close = True
# Try joining fish in fish's present vicinity.
if length < self.NEIGHBOR_DISTANCE:
alignment += fish.velocity
cohesion += fish.location
neighbors += 1
# Steer away from fish in this school that are nearby.
if too_close:
self.steering += self.correction(separation) * self.SEP_FACTOR
# Gather with and align to schoolmates detected above.
if neighbors:
self.steering += self.correction(alignment) * self.ALI_FACTOR
cohesion /= neighbors
cohesion -= self.location
self.steering += self.correction(cohesion) * self.ALI_FACTOR
def correction(self, target):
"Create a force towards the direction of the target."
target.magnitude = self.MAX_SPEED
return (target - self.velocity).limit(self.MAX_FORCE)
def update(self, interval):
"Change velocity and location with respect to time."
self.velocity += self.steering / interval
self.location += self.velocity.limit(self.MAX_SPEED) * interval
self.wraparound()
def wraparound(self):
"Move the fish to wrap around the edges of the screen."
if self.location.y < self.TOP:
self.location.y = self.BOTTOM
elif self.location.y > self.BOTTOM:
self.location.y = self.TOP
if self.location.x < self.LEFT:
self.location.x = self.RIGHT
elif self.location.x > self.RIGHT:
self.location.x = self.LEFT
################################################################################
class School:
"School(body_color, trim_color) -> School"
__slots__ = 'body_color', 'trim_color', 'fish_deque'
def __init__(self, body_color, trim_color):
"Initialize school with color identity and fish container."
self.body_color = body_color
self.trim_color = trim_color
self.fish_deque = collections.deque()
def add_fish(self, fish):
"Paint the fish with identity before adding to fish list."
fish.paint(self.body_color, self.trim_color)
self.fish_deque.append(fish)
def remove_fish(self):
"Take a fish from this school and return fish to caller."
return self.fish_deque.popleft()
def size(self):
"Get number of fish in school and return the total count."
return len(self.fish_deque)
def render(self, graphics):
"Draw each fish in this school to the graphics context."
for fish in self.fish_deque:
fish.render(graphics)
def update(self, interval):
"Run the AI code of each fish before updating positions."
for fish in self.fish_deque:
fish.run_AI(self.fish_deque)
for fish in self.fish_deque:
fish.update(interval)
def kill_fish(self):
"If there are any fish in this school, remove one of them."
if self.size() > 0:
self.remove_fish()
################################################################################
import recipe576904; recipe576904.bind_all(globals())
################################################################################
if __name__ == '__main__':
Demo.main(WIDTH, HEIGHT)