#! /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)