Here you will find the source code for the pirate animation, as described in the upcoming book Graphic Guide to Python

Copyright 2018 Antony Lees

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
 def setup():
  size(600, 1000, P3D)
  
  # game board
  global standard_tile
  standard_tile = "standard"
  global hole_tile
  hole_tile = "hole"
  global hole_hit_tile
  hole_hit_tile = "hit"
  global target_tile
  target_tile = "target"
  global number_of_columns
  number_of_columns = 5
  global tiles
  tiles = []
  global tile_size
  tile_size = width/number_of_columns/2
  global number_of_rows
  number_of_rows = (height/tile_size) * 2 
  global x_offset
  x_offset = (width-tile_size * number_of_columns)/2
  grace_period = 1
  
  # initialise tiles
  for i in range(number_of_rows):
  # set y coordinates (-y so scrolls downwards)
      y = -i * tile_size
      for j in range(number_of_columns):
          # set x coordinate
          x = j * tile_size + x_offset
          if i > grace_period:
            tile_type = random_tile_type()
            tile = Tile(x, y, tile_type)
            tiles.append(tile)
          
  #cat
  global cat_size
  cat_size = tile_size
  global cat
  cat = Cat(cat_size)
  # initial position
  cat.x = width/2
  cat.y = height/2
  
  # display
  global score
  score = 0
  global lives
  lives = 3
  
  # speed
  global lowest_speed
  lowest_speed = 5
  global highest_speed
  highest_speed = 20
  global speed
  speed = lowest_speed

def draw():
   background(0)
   lights()
   
   global score 
   fill(255)
   textSize(tile_size/2.5)
   text("Score: {}".format(score), 10, tile_size/2)
   
   global lives
   text("Lives: {}".format(lives), width-(tile_size*1.5), tile_size/2)

   # set speed but keep within limits
   speed = constrain(score/2, lowest_speed, highest_speed)

   pushMatrix()
   rotateX(radians(50))
   cat.draw_cat()
           
   for tile in tiles:
     if lives >= 1:
        tile.y = tile.y + speed
        tile.draw_tile()
        # check each tile for hits
        if tile.target_hit(cat.x, cat.y):
            score += 1
        if tile.hole_hit(cat.x, cat.y):
            lives -=1
            
   popMatrix()
   recycle_tiles()
   
# recycle tiles off the screen
def recycle_tiles():
    # get y coord of last tile
    y = 0
    global tiles
    for tile in tiles:
        if tile.y < y:
            y = tile.y
        
    # moves tiles to the end (x coord can stay the same)
    new_y = y - tile_size
    for tile in tiles:
    # if off the screen
        if tile.y > height:
            tile.y = new_y
            tile.type = random_tile_type()
     
# generate a random type of tile
def random_tile_type():
    tile_type = standard_tile
    # randomised tile chances
    if random(0, 5) > 4:
        tile_type = hole_tile
    if random(0, 50) > 45:
        tile_type = target_tile
    return tile_type

class Cat:

  def __init__(self, cat_size):
    self.x = 0
    self.y = 0
    self.cat_colour = color(255, 200, 0)
    self.cat_size = cat_size
    self.tail_length = cat_size/4
    self.tail_thickness = cat_size/7
    self.tail = []
    # initialise tail parts using 2 dimensional list
    for i in range (self.tail_length):
        self.tail.append([0, 0])

  def draw_cat(self):
    # draw body in separate matrix so can rotate
    pushMatrix()
    noStroke()
    translate(self.x, self.y)
    rotateX(frameCount/20.0) # roll
    sphereDetail(7)
    fill(self.cat_colour)
    sphere(cat_size * 0.4) 
    popMatrix()

    pushMatrix()
    noStroke()
    # draw head
    translate(self.x, self.y - (cat_size/2))
    sphereDetail(8)
    sphere(self.cat_size * 0.2)
    # draw ears
    # left
    pushMatrix()
    #fill(0, 0, 255)
    translate(-cat_size*0.1, -cat_size*0.05, cat_size*0.15)
    rotateY(0.5)
    rotateX(4)
    sphereDetail(1)
    sphere(cat_size*0.1)
    popMatrix()
    # right
    pushMatrix()
    translate(cat_size*0.1, -cat_size*0.05, cat_size*0.15)
    rotateY(3)
    rotateX(2)
    sphereDetail(1)
    sphere(cat_size*0.1)
    popMatrix()
    #triangle(-size*0.1, -size*0.05, -size*0.05, -size*0.2, -size*0.25, -size*0.3)
    #triangle(size*0.1, -size*0.05, size*0.05, -size*0.20, size*0.25, -size*0.3)
    # draw tail
    sphereDetail(8)
    # tail end
    self.tail[self.tail_length-1][0] = (sin(frameCount * 0.2) * 10)
    self.tail[self.tail_length-1][1] = cat_size/2
    # rest of tail
    for i in range(self.tail_length-1):
        self.tail[i][0] = self.tail[i+1][0]
        self.tail[i][1] = self.tail[i+1][1] + (self.tail_thickness/2)
        pushMatrix()
        #ellipse(tail[i].x, tail[i].y, tailThickness, tailThickness)
        translate(self.tail[i][0], self.tail[i][1])
        sphere(self.tail_thickness)
        popMatrix()
    popMatrix()
    
# class for tiles
class Tile:
    
  global standard_tile_colour
  standard_tile_colour = color(255) # white
  global hole_tile_colour
  hole_tile_colour = color(0) # black
  global hole_hit_tile_colour
  hole_hit_tile_colour = color(255, 0, 0) #red
  global target_tile_colour
  target_tile_colour = color(255, 255, 0) # yellow
  
  def __init__(self, x, y, type):
      self.x = x
      self.y = y
      self.type = type

  def draw_tile(self):
      stroke(0)
      if self.type == standard_tile:
        tile_colour = fill(standard_tile_colour)
      elif self.type == hole_tile:
        tile_colour = fill(hole_tile_colour)
      elif self.type == hole_hit_tile:
        tile_colour = fill(hole_hit_tile_colour)
      elif self.type == target_tile:
        tile_colour = fill(target_tile_colour)
      square(self.x, self.y, tile_size)
  
  def target_hit(self, cat_x, cat_y):
    target_hit = self.hit(cat_x, cat_y) and self.type == target_tile
    if target_hit:
      self.type = standard_tile
    return target_hit

  def hole_hit(self, cat_x, cat_y):
    hole_hit = self.hit(cat_x, cat_y) and self.type == hole_tile
    if hole_hit:
      self.type = hole_hit_tile
    return hole_hit
  
  # check if we hit a tile
  def hit(self, cat_x, cat_y):
      hit = cat_y >= self.y and cat_y <= self.y + tile_size and cat_x >= self.x and cat_x <= self.x + tile_size
      return hit

    
def mouseMoved():
    cat.x = mouseX
    cat.y = mouseY
    
def setup():
    # set the size of the window
    size(600, 400)
    global ships
    ships = []
    
    for i in range(10):
        ships.append(PirateShip())
        
    # waves
    global wave_max_height
    wave_max_height = 20
    global wave_min_height
    wave_min_height = 5
    global wave_height
    wave_height = wave_max_height
    global up
    up = 1
    global down
    down = -1
    global wave_direction
    wave_direction = up

def draw():
    # light blue background
    background(102, 205, 170)
    
    # waves
    global wave_max_height
    global wave_min_height
    global wave_height
    global wave_direction
    noFill()
    # determine of the vwaes should be going up or down
    if wave_height <= wave_min_height:
        wave_direction = up
    if wave_height >= wave_max_height:
        wave_direction = down
    # change the wave height for next time
    wave_height += wave_direction
    # wave y coordinate
    wave_y = wave_max_height//2
    # while the y coordinate is less than the height of the window
    while wave_y <= height:
        # wave x coordinate
        wave_x = 0
        # while the x coordinate is less than the width of the window
        while wave_x <= width:
            # draw a semi=circle arc
            arc(wave_x, wave_y, wave_max_height, wave_height, 0, PI)
            # increase the x coordinate by 20
            wave_x += wave_max_height
        
        # increase the y coordinate by 20
        wave_y += wave_max_height
    
    global ships
    for ship in ships:
        ship.display()
        ship.move()
    
class PirateShip:
    # pirate ship variables
    x_position = 0
    y_position = 0
    ship_length = 0
    sails = 0
    sail_colour = 0
    porthole_count = 0
    sail_spacing = 0
    speed = 0
    
    def __init__(self):
        # initialise the ship
        self.initialise()
        
    def initialise(self):
        # initialise the ship at a random-ish location
        self.x_position = random(600, 700)
        self.y_position = random(50, 350)
        self.ship_length = random(40, 100)
        # speed
        self.speed = self.ship_length//20
        # sails
        self.sails = int(min((self.ship_length//30), 3))
        self.sail_colour = color(random(255), random(255), random(255))
        self.sail_spacing = floor(self.ship_length / 3)
        # portholes
        self.porthole_count = int(self.ship_length//12)

    def display(self):
        # calculate ship coordinates
        front_of_ship_x_position = self.x_position - (self.ship_length / 2)
        back_of_ship_x_position = self.x_position + self.ship_length + (self.ship_length / 3)

        # ship hull
        # brown lines and fill
        stroke(139, 71, 38)
        fill(139, 71, 38)
        # middle rectangle
        rect(self.x_position, self.y_position, self.ship_length, 20)
        # front and back
        triangle(front_of_ship_x_position, self.y_position, self.x_position, self.y_position, self.x_position, self.y_position + 20)
        triangle(self.x_position + self.ship_length, self.y_position, back_of_ship_x_position, self.y_position, self.x_position + self.ship_length, self.y_position + 20)

        # draw the sails
        initial_sail_x_position = self.x_position + 15
        # black lines
        stroke(0)
        for i in range(self.sails):
            # sail
            fill(self.sail_colour)
            quad(initial_sail_x_position + (self.sail_spacing * i), self.y_position - 40, initial_sail_x_position + (self.sail_spacing * i) + 10, self.y_position - 40, initial_sail_x_position + (self.sail_spacing * i) + 20, self.y_position - 10, initial_sail_x_position + (self.sail_spacing * i), self.y_position - 10)
            # mast
            fill(0)
            line(initial_sail_x_position + (self.sail_spacing * i), self.y_position - 10, initial_sail_x_position + (self.sail_spacing * i), self.y_position)
            line(initial_sail_x_position + (self.sail_spacing * i), self.y_position - 40, initial_sail_x_position + (self.sail_spacing * i), self.y_position - 50)
            # flag
            fill(0)
            rect(initial_sail_x_position + (self.sail_spacing * i), self.y_position - 50, 10, 5)
            # jolly roger
            stroke(255)
            line(initial_sail_x_position + (self.sail_spacing * i) + 2, self.y_position - 49, initial_sail_x_position + (self.sail_spacing * i) + 8, self.y_position - 46)
            line(initial_sail_x_position + (self.sail_spacing * i) + 8, self.y_position - 49, initial_sail_x_position + (self.sail_spacing * i) + 2, self.y_position - 46)
            stroke(0)
        
        # draw the portholes
        for i in range(self.porthole_count):
            # portholes
            fill(0)
            circle(self.x_position+(15*i),self.y_position+10,5)
            
        # ship has left the screen
        if back_of_ship_x_position <= 0:
            # start again
            self.initialise()
            
    def move(self):
        self.x_position -= self.speed