Advertisement
here2share

# CodeSkulptor_Photorealistic3D_Auto_Race_demo.py

Jan 11th, 2015
337
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 63.22 KB | None | 0 0
  1. # CodeSkulptor_online_visual_demo.py ### Not a Pys60 project, but does great for simulations
  2. # http://www.codeskulptor.org/#demos-riceracer.py ### Note: detailed images began to appear AFTER a minute of start.
  3.  
  4. # "Rice Racer" - A tribute to Sega's 1988 Arcade Game "Power Drift"
  5. #
  6. # I was trying to think of which classic arcade game would be a good match for the tools provided
  7. # by CodeSkulptor and SimpleGUI. SimpleGUI's most exciting graphical feature is the ability to
  8. # scale and rotate images, so I started thinking of the sprite-scalers from the mid-80's, like
  9. # Space Harrier, Out Run and Afterburner. However, the one I remember most vividly is an outrageous
  10. # driving game called Power Drift, which challenged the player to race around tracks that resembled
  11. # roller coasters more than motor racing circuits.
  12. #
  13. # Programming - Steven Knock
  14. # Music       - Andy Denton
  15. # Graphics    - Car, courtesy of Pete Carpenter of http://www.rc-airplane-world.com
  16. #             - Trees, from http://www.immediateentourage.com
  17. #             - Horizon, http://en.wikipedia.org/wiki/File:Spitzkoppe_360_Panorama.jpg
  18. #             - Track, generated using Paint Shop Pro
  19. #
  20. # Current Version (v1.4 - 11th July 2013):
  21. #   in which I updated the code to work with CodeSkulptor's new integer and float rules.
  22. #
  23. # Earlier versions:
  24. # v1.3 - 13th March 2013:
  25. #   in which I added the horizon, music, sound effects, player images, new track, new car images
  26. #   and increased the difficulty.
  27. #
  28. # v1.2: http://www.codeskulptor.org/#user8-gRnuT0e3vD-0.py
  29. #   in which I updated the code to work with the latest version of CodeSkulptor.
  30. #
  31. # v1.1: http://www.codeskulptor.org/#user6-UbZVApbT6ahIJnI.py
  32. #   in which I added a mini-map and increased the frame rate.
  33. #
  34. # v1.0: http://www.codeskulptor.org/#user6-oD2uRenZ5yXlpWp.py
  35. #
  36. # There are a few important points:
  37. #
  38. # The game requires a moderately quick computer to run at an acceptable rate, and I strongly recommend
  39. # running it in Chrome rather than Firefox. In Chrome, I get a steady 20fps (v1.1), which is fine,
  40. # whereas in Firefox I get 5fps which is unplayable.
  41. #
  42. # To add additional opponents, simply add names to the PLAYERS array defined at the top of the code.
  43. #
  44. # It's also straightforward to create your own tracks. The pre-defined tracks are defined inside a
  45. # routine called _define_tracks() and consist of pairs of coordinates and tangent vectors, collectively
  46. # called "control points". Search the web for "Hermite Curves" for more information.
  47. #
  48. # Apologies for the single character indents, but this was necessary to reduce code size since I
  49. # exceeded CodeSkulptor's 64k limit.
  50. #
  51. # The game loads several images when it starts. Sometimes, an image inexplicably doesn't get loaded
  52. # and so the game will keep telling you that it is waiting for images to load. If this happens, the
  53. # easiest thing is just to restart the game.
  54. #
  55. # Burn rubber!
  56.  
  57. import math
  58. import random
  59. import simplegui
  60. import time
  61.  
  62. # Player Roster - Feel free to add more names to this list.
  63. # You always control the first player, and they are always placed last on the starting grid.
  64. PLAYERS = (
  65.  'Racer X',
  66.  'Joe',
  67.  'Scott',
  68.  'John',
  69.  'Stephen'
  70. )
  71.  
  72. # Image Constants
  73. IMAGES = (
  74.  'racerx',
  75.  'jwarren',
  76.  'srixner',
  77.  'jgreiner',
  78.  'swong',
  79.  'logo',
  80.  'log',
  81.  'gravel',
  82.  'sand',
  83.  'rock',
  84.  'start_line',
  85.  'start',
  86.  'backdrop.jpg',
  87.  ('banner', (1, 0.479)),
  88.  ('tree_1', (5.94, 9.3)),
  89.  ('tree_2', (6.59, 7.52)),
  90.  ('tree_3', (5.73, 12)),
  91.  ('tree_4', (7.4, 7.4)),
  92.  ('tree_5', (6.2, 8.7)),
  93.  ('tree_6', (2.5, 5.4)),
  94.  'car_1',
  95.  'car_2',
  96.  'car_3',
  97.  'car_4',
  98. )
  99.  
  100. IMG_HEAD = 0
  101. IMG_LOGO = 5
  102. IMG_LOG = 6
  103. IMG_GRAVEL = 7
  104. IMG_SAND = 8
  105. IMG_ROCK = 9
  106. IMG_START_LINE = 10
  107. IMG_START = 11
  108. IMG_BACKDROP = 12
  109. IMG_BANNER = 13
  110. IMG_TREE = 14
  111. IMG_CAR = 20
  112.  
  113. # Music Constants
  114. MUSIC_TRACKS = (
  115.  ('menu', 75),
  116.  'start',
  117.  'win',
  118.  'lose',
  119.  ('race1', 50.717),
  120.  ('race2', 68.246)
  121. )
  122.  
  123. MUSIC_MENU = 0
  124. MUSIC_START = 1
  125. MUSIC_WIN = 2
  126. MUSIC_LOSE = 3
  127. MUSIC_RACE = 4
  128.  
  129. # Asset Constants
  130. ASSET_BASE = 'http://commondatastorage.googleapis.com/codeskulptor-demos/riceracer_assets/'
  131. IMAGE_BASE = ASSET_BASE + 'img/'
  132. IMAGE_TYPE = '.png'
  133. MUSIC_BASE = ASSET_BASE + 'music/'
  134. MUSIC_TYPE = '.ogg'
  135. SFX_BASE = ASSET_BASE + 'fx/'
  136. SFX_TYPE = '.ogg'
  137. SFX_VOLUME = 0.2
  138.  
  139. FONT_STYLE = 'sans-serif'
  140.  
  141. # Sanity saving constants
  142. X = 0
  143. Y = 1
  144. Z = 2
  145.  
  146. def is_tuple(x):
  147.  return 'tuple' in str(type(x))
  148.  
  149. # Classes
  150.  
  151. class Sort:
  152.  # Quicksort a list of tuples whose first element is the sorted key
  153.  def quick_sort(list):
  154.   if list == []:
  155.    return []
  156.   else:
  157.    pivot = list[0]
  158.    pivot_value = pivot[0]
  159.    lesser = Sort.quick_sort([x for x in list[1 :] if x[0] < pivot_value])
  160.    greater = Sort.quick_sort([x for x in list[1 :] if x[0] >= pivot_value])
  161.    return lesser + [pivot] + greater
  162.  
  163. # A class to keep track of time intervals and provide an average of them
  164. class TimeCounter:
  165.  MAX_INTERVALS = 5
  166.  
  167.  def __init__(self):
  168.   self.reset()
  169.  
  170.  def __str__(self):
  171.   return str(self.intervals)
  172.  
  173.  def record_time(self):
  174.   current_time = time.time()
  175.   if self.last_time > 0:
  176.    interval = current_time - self.last_time
  177.    if len(self.intervals) < TimeCounter.MAX_INTERVALS:
  178.     self.intervals.append(0)
  179.    self.intervals[self.index] = interval
  180.    self.index = (self.index + 1) % TimeCounter.MAX_INTERVALS
  181.   else:
  182.    self.initial_time = current_time
  183.   self.last_time = current_time
  184.   self.last_average_time = 0
  185.  
  186.  def get_average_time(self):
  187.   if self.last_average_time != 0:
  188.    return self.last_average_time
  189.  
  190.   total = 0
  191.   l = len(self.intervals)
  192.   if l > 0:
  193.    for interval in self.intervals:
  194.     total += interval
  195.    total /= l
  196.  
  197.   self.last_average_time = total
  198.   return total
  199.  
  200.  def get_current_time(self):
  201.   return self.last_time
  202.  
  203.  def get_total_time(self):
  204.   return self.last_time - self.initial_time
  205.  
  206.  def reset(self):
  207.   self.intervals = []
  208.   self.index = 0
  209.   self.last_time = 0
  210.   self.last_average_time = 0
  211.   self.initial_time = 0
  212.  
  213. # Utility methods relating to maths
  214. class Math:
  215.  METRES_PER_SECOND_TO_MILES_PER_HOUR = 2.237
  216.  
  217.  # Define a rectangle in the form suitable for the polygon function
  218.  def rect(x, y, w, h):
  219.   return [(x, y), (x + w, y), (x + w, y + h), (x, y + h)]
  220.  
  221.  # Normalises a 3d vector
  222.  def normalise(v):
  223.   magnitude = math.sqrt((v[X] ** 2) + (v[Y] ** 2) + (v[Z] ** 2))
  224.   return [v[0] / magnitude, v[1] / magnitude, v[2] / magnitude]
  225.  
  226.  # Returns the squared distance between two points
  227.  def distance_sq(p1, p2):
  228.   return (p1[X] - p2[X]) ** 2 + (p1[Y] - p2[Y]) ** 2 + (p1[Z] - p2[Z]) ** 2
  229.  
  230.  # Returns the distance between two points
  231.  def distance(p1, p2):
  232.   return math.sqrt(Math.distance_sq(p1, p2))
  233.  
  234.  # Linearly calculates a value between two values based on t (0 <= t <= 1)
  235.  def interpolate(t, v1, v2):
  236.   return v1 + t * (v2 - v1)
  237.  
  238.  def get_orientation_from_tangent_vector(t):
  239.   return math.atan2(t[X], t[Z])
  240.  
  241.  def get_angle_between_orientations(o1, o2):
  242.   angle = o2 - o1
  243.   if abs(angle) > math.pi:
  244.    angle = 2 * math.pi - abs(angle)
  245.   return angle
  246.  
  247. class BoundingBox:
  248.  def __init__(self):
  249.   self.box = None
  250.  
  251.  def __str__(self):
  252.   return str(self.box)
  253.  
  254.  def add(self, point):
  255.   if self.box == None:
  256.    # If this is the first point to be added to the box, then initialise the box extents based on it
  257.    self.box = []
  258.    for i in range(2):
  259.     self.box.append(list(point))
  260.    return
  261.  
  262.   # Grow the box to accommodate the new point
  263.   min_pt = self.box[0]
  264.   for a in range(3):
  265.    if point[a] < min_pt[a]:
  266.     min_pt[a] = point[a]
  267.   max_pt = self.box[1]
  268.   for a in range(3):
  269.    if point[a] > max_pt[a]:
  270.     max_pt[a] = point[a]
  271.  
  272.  def get_centre(self):
  273.   b = self.box
  274.   return ((b[1][X] + b[0][X]) / 2.0, (b[1][Y] + b[0][Y]) / 2.0, (b[1][Z] + b[0][Z]) / 2.0)
  275.  
  276.  def get_extent(self, axis):
  277.   return self.box[1][axis] - self.box[0][axis]
  278.  
  279. class ImageManager:
  280.  def __init__(self):
  281.   self.images = []
  282.   for image_data in IMAGES:
  283.    image_name = image_data[0] if is_tuple(image_data) else image_data
  284.    image_url = IMAGE_BASE + image_name
  285.    if '.' not in image_name:
  286.     image_url += IMAGE_TYPE
  287.    self.images.append(simplegui.load_image(image_url))
  288.  
  289.  def get_number_of_pending_images(self):
  290.   count = 0
  291.   for image in self.images:
  292.    if image.get_width() == 0:
  293.     count += 1
  294.   return count
  295.  
  296. class MusicTrack:
  297.  def __init__(self, name, length):
  298.   self.name = name
  299.   self.length = length
  300.   self.sound = simplegui.load_sound(MUSIC_BASE + name + MUSIC_TYPE);
  301.  
  302. class MusicManager:
  303.  def __init__(self, time_counter):
  304.   self.time_counter = time_counter
  305.   self.active_track = -1
  306.   self.selected_track = -1
  307.   self.tracks = []
  308.   self.mute = False
  309.   for track in MUSIC_TRACKS:
  310.    name = track[0] if is_tuple(track) else track
  311.    length = track[1] if is_tuple(track) else 0
  312.    self.tracks.append(MusicTrack(name, length))
  313.  
  314.  def play(self, track_index):
  315.   self.stop()
  316.   self.active_track = track_index;
  317.   self.selected_track = track_index;
  318.   self.sound_start_time = self.time_counter.get_current_time()
  319.   track = self.tracks[track_index];
  320.   track.sound.rewind()
  321.   if not self.mute:
  322.    track.sound.play()
  323.  
  324.  def stop(self):
  325.   if self.active_track >= 0:
  326.    self.tracks[self.active_track].sound.rewind()
  327.   self.active_track = -1
  328.  
  329.  def process_sound(self):
  330.   if self.active_track >= 0:
  331.    track = self.tracks[self.active_track]
  332.    if track.length > 0:
  333.     current_time = self.time_counter.get_current_time()
  334.     if current_time - self.sound_start_time >= track.length:
  335.      self.play(self.active_track)
  336.  
  337.  def toggle(self):
  338.   self.mute = not self.mute
  339.   if self.mute:
  340.    self.stop()
  341.   else:
  342.    self.play(self.selected_track)
  343.  
  344. class EngineManager:
  345.  INTERVALS = 12
  346.  SAMPLE_LENGTH_S = 11.436
  347.  FUDGE_FACTOR_S = 0.2
  348.  
  349.  def __init__(self, time_counter):
  350.   self.time_counter = time_counter
  351.   self.active_sound = -1
  352.   self.desired_sound = -1
  353.   self.sounds = []
  354.   self.sound_lengths = []
  355.   self.mute = False
  356.   length = EngineManager.SAMPLE_LENGTH_S
  357.   factor = 2.0 ** (1.0 / 12.0)
  358.   for i in range(1, EngineManager.INTERVALS + 1):
  359.    sound_url = SFX_BASE + 'engine-' + str(i) + SFX_TYPE
  360.    sound = simplegui.load_sound(sound_url)
  361.    sound.set_volume(SFX_VOLUME)
  362.    self.sounds.append(sound)
  363.    self.sound_lengths.append(length)
  364.    length /= factor
  365.  
  366.  def process_sound(self):
  367.   if self.mute:
  368.    return
  369.   has_active_sound = self.active_sound >= 0
  370.   current_time = self.time_counter.get_current_time()
  371.   if self.desired_sound != self.active_sound:
  372.    self.sounds[self.desired_sound].play()
  373.    if has_active_sound:
  374.     self.sounds[self.active_sound].rewind()
  375.    self.active_sound = self.desired_sound
  376.    self.sound_start_time = current_time
  377.   elif has_active_sound:
  378.    elapsed = current_time - self.sound_start_time
  379.    if elapsed + EngineManager.FUDGE_FACTOR_S >= self.sound_lengths[self.active_sound]:
  380.     self.sounds[self.active_sound].rewind()
  381.     self.sounds[self.active_sound].play()
  382.     self.sound_start_time = current_time
  383.  
  384.  def set_pitch(self, pitch):
  385.   intervals = EngineManager.INTERVALS
  386.   self.desired_sound = int(max(0, min(pitch * intervals, intervals - 1)))
  387.  
  388.  def stop(self):
  389.   self.desired_sound = -1
  390.   if self.active_sound >= 0:
  391.    self.sounds[ self.active_sound ].rewind()
  392.    self.active_sound = -1
  393.  
  394.  def toggle(self):
  395.   self.mute = not self.mute
  396.   if self.mute:
  397.    self.stop()
  398.  
  399. class Player:
  400.  HUMAN = 0
  401.  COMPUTER = 1
  402.  
  403.  def __init__(self, name):
  404.   # Work around bug in CodeSkulptor that doesn't allow 0 class members.
  405.   Player.HUMAN = 0
  406.  
  407.   self.name = name
  408.   self.reset()
  409.  
  410.  def reset(self):
  411.   self.position = [0, 0]
  412.   self.velocity = [0, 0]
  413.   self.acceleration = [0, 0]
  414.  
  415.  def get_track_position_in_metres(self):
  416.   return self.position[1] * TrackDef.DISTANCE_BETWEEN_SEGMENTS_M
  417.  
  418. class Sprite:
  419.  ORIENTATION_BILLBOARD = 10000
  420.  
  421.  def __init__(self, position, size, orientation, image):
  422.   self.position = position
  423.   self.size = size
  424.   self.orientation = orientation
  425.   self.image = image
  426.  
  427. class HermiteCurve:
  428.  HERMITE_BASE = ((2, -3, 0, 1), (-2, 3, 0, 0), (1, -2, 1, 0), (1, -1, 0, 0))
  429.  HERMITE_DERIVATIVE = ((6, -6, 0, 0), (-6, 6, 0, 0), (3, -4, 1, 0), (3, -2, 0, 0))
  430.  
  431.  # Intiialise and create the coefficients for interpolating a Hermite curve
  432.  # Parameters are: p1, p2 - Start and End points of the curve
  433.  #      : t1, t2 - Tangent Vectors at the start and end of the curve
  434.  def __init__(self, p1, p2, t1, t2):
  435.   geometry = (p1, p2, t1, t2)
  436.   self.coefs = []
  437.   self.deriv_coefs = []
  438.   for axis in range(3):
  439.    coef = []
  440.    deriv_coef = []
  441.    for j in range(4):
  442.     tot = 0
  443.     deriv_tot = 0
  444.     for k in range(4):
  445.      tot += HermiteCurve.HERMITE_BASE[k][j] * geometry[k][axis]
  446.      deriv_tot += HermiteCurve.HERMITE_DERIVATIVE[k][j] * geometry[k][axis]
  447.     coef.append(tot)
  448.     deriv_coef.append(deriv_tot)
  449.    self.coefs.append(coef)
  450.    self.deriv_coefs.append(deriv_coef)
  451.  
  452.  # Calculate a point on a 3d-curve using the coefficients previously calculated and t, where 0 <= t <= 1
  453.  def calculate_point(self, t):
  454.   t2 = t * t
  455.   t3 = t2 * t
  456.   p = []
  457.   for i in range(3):
  458.    c = self.coefs[i]
  459.    p.append(c[0] * t3 + c[1] * t2 + c[2] * t + c[3])
  460.   return p
  461.  
  462.  # Calculate the tangent vector at a point on the 3rd-curve
  463.  def calculate_tangent(self, t):
  464.   t2 = t * t
  465.   p = []
  466.   for i in range(3):
  467.    c = self.deriv_coefs[i]
  468.    p.append(c[0] * t2 + c[1] * t + c[2])
  469.   return p
  470.  
  471. class TrackSegment:
  472.  def __init__(self, position, orientation, image_name):
  473.   self.position = position
  474.   self.orientation = orientation
  475.   self.sprite = Sprite(position, TrackDef.TRACK_SIZE_M, orientation, image_name)
  476.  
  477. class ControlPoint:
  478.  def __init__(self, position, vector, image_name = None):
  479.   self.position = position
  480.   self.vector = vector
  481.   self.image_name = image_name
  482.   self.curve = None
  483.  
  484. class TrackDef:
  485.  DEFAULT_GROUND_IMAGE = IMG_GRAVEL
  486.  DEFAULT_ELEVATED_IMAGE = IMG_LOG
  487.  
  488.  DISTANCE_BETWEEN_SEGMENTS_M = 0.4
  489.  
  490.  TRACK_SIZE_M = (5, 0.7, DISTANCE_BETWEEN_SEGMENTS_M)
  491.  
  492.  def __init__(self, name, laps):
  493.   self.name = name
  494.   self.laps = laps
  495.   self.control_points = []
  496.   self.track = None
  497.   self.bounding_box = BoundingBox()
  498.  
  499.  def add(self, control_point):
  500.   self.control_points.append(control_point)
  501.  
  502.  # Returns the angle of the bend of the track at the specified position, in radians.
  503.  def get_track_angle(self, position):
  504.   track_index = int(position)
  505.   track_length = len(self.track)
  506.   track1 = self.track[track_index % track_length]
  507.   track2 = self.track[(track_index + 1) % track_length]
  508.   return Math.get_angle_between_orientations(track1.orientation, track2.orientation)
  509.  
  510.  def get_length_m(self):
  511.   return len(self.track) * TrackDef.DISTANCE_BETWEEN_SEGMENTS_M
  512.  
  513.  # Create Track
  514.  # Takes the control points specified in the track_def array and creates curves from them.
  515.  def create_track(self):
  516.   self.track = []
  517.   track_def = self.control_points
  518.  
  519.   ground_level = 0.02
  520.  
  521.   # Store the starting point
  522.   current_point = track_def[0].position
  523.  
  524.   # Distance in metres between each step on the curve
  525.   max_distance = TrackDef.DISTANCE_BETWEEN_SEGMENTS_M
  526.  
  527.   current_ground_image = TrackDef.DEFAULT_GROUND_IMAGE
  528.   current_elevated_image = TrackDef.DEFAULT_ELEVATED_IMAGE
  529.  
  530.   i = 0
  531.   l = len(track_def)
  532.   while i < l:
  533.    # cp1 = Start Control Point. cp2 = End Control Point
  534.    cp1 = track_def[i]
  535.    i += 1
  536.    cp2 = track_def[i % l]
  537.  
  538.    # Apply image for this track section
  539.    if cp1.image_name != None:
  540.     if cp1.position[Y] <= ground_level:
  541.      current_ground_image = cp1.image_name
  542.     else:
  543.      current_elevated_image = cp1.image_name
  544.  
  545.    # Initialise object for curve calculations
  546.    cp1.curve = HermiteCurve(cp1.position, cp2.position, cp1.vector, cp2.vector)
  547.  
  548.    # Estimate roughly how many line segments to break the curve into
  549.    lines = Math.distance(cp1.position, cp2.position) * math.pi / (4 * max_distance)
  550.  
  551.    j = 1.0
  552.    while j <= lines:
  553.     t = j / lines
  554.     next_point = cp1.curve.calculate_point(t)
  555.     current_distance = Math.distance(current_point, next_point)
  556.     if current_distance >= max_distance:
  557.      dt = max_distance / current_distance
  558.      vector = []
  559.      new_point = []
  560.      for a in range(3):
  561.       vector.append(next_point[a] - current_point[a])
  562.       new_point.append(current_point[a] + vector[a] * dt)
  563.      orientation = Math.get_orientation_from_tangent_vector(vector)
  564.  
  565.      current_point = tuple(new_point)
  566.  
  567.      # Calculate track image
  568.      if len(self.track) == 0:
  569.       image_name = IMG_START_LINE
  570.      elif current_point[Y] <= ground_level:
  571.       image_name = current_ground_image
  572.      else:
  573.       image_name = current_elevated_image
  574.  
  575.      self.track.append(TrackSegment(current_point, orientation, image_name))
  576.      self.bounding_box.add(current_point)
  577.     else:
  578.      j += 1
  579.  
  580. class Camera:
  581.  def __init__(self):
  582.   self.position = [0, 0, 0]
  583.   self.set_yaw(0)
  584.   self.set_pitch(0)
  585.   self.set_roll(0)
  586.   self._vbuff = [[0, 0, 0], [0, 0, 0]]
  587.  
  588.  def __str__(self):
  589.   return str(self.position) + " [Yaw=" + str(round(self.yaw, 3)) + ", Roll=" + str(round(self.roll, 3)) + "]"
  590.  
  591.  def set_yaw(self, yaw):
  592.   self.yaw = yaw
  593.   self.sine_yaw = math.sin(yaw)
  594.   self.cosine_yaw = math.cos(yaw)
  595.  
  596.  def set_pitch(self, pitch):
  597.   self.pitch = pitch
  598.   self.sine_pitch = math.sin(pitch)
  599.   self.cosine_pitch = math.cos(pitch)
  600.  
  601.  def set_roll(self, roll):
  602.   self.roll = roll
  603.   self.sine_roll = math.sin(roll)
  604.   self.cosine_roll = math.cos(roll)
  605.  
  606.  # Transformations
  607.  def world_to_view(self, pos):
  608.   # Translate pos from world coordinates into view coordinates (relative to camera)
  609.   vbx = pos[0] - self.position[0]
  610.   vby = pos[1] - self.position[1]
  611.   vbz = pos[2] - self.position[2]
  612.  
  613.   # Rotate the view coordinates according to the camera's yaw
  614.   vbx, vbz = vbx * self.cosine_yaw - vbz * self.sine_yaw, vbx * self.sine_yaw + vbz * self.cosine_yaw
  615.  
  616.   # This saves a bit of time during the game when pitch isn't used
  617.   if self.pitch != 0:
  618.    # Rotate the view coordinates according to the camera's pitch
  619.    vby, vbz = vby * self.cosine_pitch - vbz * self.sine_pitch, vby * self.sine_pitch + vbz * self.cosine_pitch
  620.  
  621.   return (vbx, vby, vbz)
  622.  
  623. class Mechanics:
  624.  CAR_SIZE_M = (1, 0.7, 0.5)
  625.  CAR_VELOCITY_MAX_MS = (14, 45)
  626.  CAR_ACCELERATION_MSS = (80, 30)
  627.  CAR_DECELERATION_FACTOR = 2.5
  628.  CAR_CENTRIFUGAL_MSS = 80
  629.  CAR_VELOCITY_DAMPEN = (0.9, 0.96)
  630.  CAR_VELOCITY_DAMPEN_TRACK_EDGE = 0.85
  631.  COLLISION_DAMPEN = 0.9
  632.  COLLISION_RELAXATION = 0.8
  633.  
  634.  def __init__(self, race):
  635.   self.race = race
  636.  
  637.  # Simulate centrifugal force being applied to the human player as they take corners
  638.  def apply_force(self):
  639.   player = self.race.players[Player.HUMAN]
  640.   vel = player.velocity
  641.   acc = player.acceleration
  642.   track_angle = self.race.track_def.get_track_angle(player.position[1])
  643.   acc[0] -= vel[1] * track_angle * Mechanics.CAR_CENTRIFUGAL_MSS
  644.  
  645.  def move_players(self, delta):
  646.   position_along_track_factor = delta / TrackDef.DISTANCE_BETWEEN_SEGMENTS_M
  647.   car_vel_max = Mechanics.CAR_VELOCITY_MAX_MS
  648.   acc_factor = [0, 0]
  649.   for player in self.race.players:
  650.    pos = player.position
  651.    vel = player.velocity
  652.    acc = player.acceleration
  653.  
  654.    # Update Position. pos[1] is not measured in metres, which is why it must be multipled by position_factor_along_track
  655.    pos[0] += vel[0] * delta
  656.    pos[1] += vel[1] * position_along_track_factor
  657.  
  658.    # Calculate lateral acceleration strength based on forward velocity
  659.    acc_factor[0] = min(2 * vel[1] / car_vel_max[1], 1)
  660.  
  661.    # Calculate forward acceleration strength - this gives greater acceleration at lower speeds and boosts deceleration
  662.    if acc[1] > 0:
  663.     acc_factor[1] = math.cos(vel[1] / car_vel_max[1] * math.pi / 2)
  664.    else:
  665.     acc_factor[1] = Mechanics.CAR_DECELERATION_FACTOR
  666.  
  667.    # Update Velocity
  668.    vel[0] += acc[0] * delta * acc_factor[0]
  669.    vel[1] += acc[1] * delta * acc_factor[1]
  670.  
  671.    # Constrain Velocity
  672.    if vel[1] < 0:
  673.     vel[1] = 0
  674.    elif vel[1] > car_vel_max[1]:
  675.     vel[1] = car_vel_max[1]
  676.    if vel[0] < -car_vel_max[0]:
  677.     vel[0] = -car_vel_max[0]
  678.    elif vel[0] > car_vel_max[0]:
  679.     vel[0] = car_vel_max[0]
  680.  
  681.    # Dampen velocity
  682.    if acc[0] == 0 or vel[1] < 10:
  683.     vel[0] *= Mechanics.CAR_VELOCITY_DAMPEN[0]
  684.    if acc[1] == 0:
  685.     vel[1] *= Mechanics.CAR_VELOCITY_DAMPEN[1]
  686.  
  687.  def process_collisions(self):
  688.   players = self.race.players
  689.   bb = []
  690.   for player in players:
  691.    bb.append(self._get_bounding_box(player))
  692.  
  693.   l = len(players)
  694.   for i in range(l):
  695.    p1 = players[i]
  696.    bb1 = bb[i]
  697.    for j in range(i + 1, l):
  698.     p2 = players[j]
  699.     bb2 = bb[j]
  700.  
  701.     dx0 = bb2[1][X] - bb1[0][X]
  702.     if dx0 < 0:
  703.      continue
  704.     dx1 = bb1[1][X] - bb2[0][X]
  705.     if dx1 < 0:
  706.      continue
  707.     dz0 = bb2[1][Y] - bb1[0][Y]
  708.     if dz0 < 0:
  709.      continue
  710.     dz1 = bb1[1][Y] - bb2[0][Y]
  711.     if dz1 < 0:
  712.      continue
  713.  
  714.     # There has been a collision
  715.     mtd_x = dx0 if dx0 < dx1 else -dx1
  716.     mtd_z = dz0 if dz0 < dz1 else -dz1
  717.     if abs(mtd_x) < abs(mtd_z):
  718.      mtd_z = 0
  719.     else:
  720.      mtd_x = 0
  721.  
  722.     # Intersection Response - Separate the objects so that they just touch each other
  723.     relaxation = Mechanics.COLLISION_RELAXATION
  724.     fx = mtd_x * 0.5 * relaxation
  725.     fz = mtd_z * 0.5 * relaxation / TrackDef.DISTANCE_BETWEEN_SEGMENTS_M
  726.     p1.position[X] += fx
  727.     p1.position[Y] += fz
  728.     p2.position[X] -= fx
  729.     p2.position[Y] -= fz
  730.  
  731.     # Swap and dampen velocities if hitting back to front
  732.     if mtd_z != 0:
  733.      dampen = Mechanics.COLLISION_DAMPEN
  734.      v = p1.velocity[Y]
  735.      p1.velocity[Y] = p2.velocity[Y] * dampen
  736.      p2.velocity[Y] = v * 0.9
  737.  
  738.  def constrain_players_to_track(self):
  739.   car_half_width = Mechanics.CAR_SIZE_M[0] / 2
  740.   track_half_width = TrackDef.TRACK_SIZE_M[0] / 2
  741.   track_edge = (-track_half_width, track_half_width)
  742.   for player in self.race.players:
  743.    # Check if player is touching the edge of the track
  744.    x = player.position[0]
  745.    hit = False
  746.    if x - car_half_width < track_edge[0]:
  747.     x = track_edge[0] + car_half_width
  748.     hit = True
  749.    elif x + car_half_width > track_edge[1]:
  750.     x = track_edge[1] - car_half_width
  751.     hit = True
  752.    if hit:
  753.     player.position[0] = x
  754.     player.velocity[0] = 0
  755.     player.velocity[1] *= Mechanics.CAR_VELOCITY_DAMPEN_TRACK_EDGE
  756.  
  757.  # Returns a 2d bounding box for a player in 'track space', which is conceptually
  758.  #   an infinitely long line whose origin is at the start of the race.
  759.  def _get_bounding_box(self, player):
  760.   pos = player.position
  761.   hx = Mechanics.CAR_SIZE_M[X] / 2.0
  762.   hz = Mechanics.CAR_SIZE_M[Z] / 2.0
  763.   cx = pos[0]
  764.   cz = pos[1] * TrackDef.DISTANCE_BETWEEN_SEGMENTS_M
  765.   return ((cx - hx, cz - hz), (cx + hx, cz + hz))
  766.  
  767. class Intelligence:
  768.  class PlayerState:
  769.   OVERTAKING_DISTANCE_M = 6
  770.   LATERAL_SHUFFLE_MS = Mechanics.CAR_VELOCITY_MAX_MS[0] * 0.4
  771.  
  772.   def __init__(self, race, player):
  773.    self.race = race
  774.    self.player = player
  775.    self.target_x = player.position[0]
  776.    self.aggression = 0.9
  777.    self.evaluate_count = 0
  778.    self.think(player, player)
  779.  
  780.   def think(self, player_ahead, player_behind):
  781.    self._calculate_relative_positions(player_ahead, player_behind)
  782.    self._consider_overtaking(player_ahead, player_behind)
  783.    self._apply_forward_acceleration()
  784.    self._apply_lateral_velocity()
  785.  
  786.    self.evaluate_count -= 1
  787.    if self.evaluate_count <= 0:
  788.     if not self.close_to_player_behind and not self.close_to_player_ahead:
  789.      available_width = (TrackDef.TRACK_SIZE_M[X] - Mechanics.CAR_SIZE_M[X]) * 0.4
  790.      self.target_x = (random.random() * available_width) - (available_width / 2.0)
  791.     self.aggression = max(min(self.aggression + random.random() * 0.04 - 0.02, 1), 0)
  792.     self.evaluate_count = random.randrange(60) + 60
  793.  
  794.   def _apply_forward_acceleration(self):
  795.    target_velocity = self._calculate_target_velocity()
  796.    actual_velocity = self.player.velocity[1]
  797.    acceleration_factor = 1 if target_velocity > actual_velocity else -Mechanics.CAR_DECELERATION_FACTOR
  798.    self.player.acceleration[1] = Mechanics.CAR_ACCELERATION_MSS[1] * acceleration_factor
  799.  
  800.   def _apply_lateral_velocity(self):
  801.    target_x = self.target_x
  802.    actual_x = self.player.position[0]
  803.    difference = target_x - actual_x
  804.    if abs(difference) > 0.1:
  805.     shuffle_factor = Intelligence.PlayerState.LATERAL_SHUFFLE_MS
  806.     if difference < 0:
  807.      shuffle_factor = -shuffle_factor
  808.    else:
  809.     shuffle_factor = 0
  810.    self.player.velocity[0] = shuffle_factor
  811.  
  812.   def _calculate_target_velocity(self):
  813.    track = self.race.track_def.track
  814.    track_length = len(track)
  815.  
  816.    position = self.player.position[1]
  817.    track_index = int(position)
  818.    track1 = track[track_index % track_length]
  819.    track2 = track[(track_index + Intelligence.TRACK_LOOK_AHEAD) % track_length]
  820.  
  821.    track_angle = Math.get_angle_between_orientations(track1.orientation, track2.orientation)
  822.    target_velocity = Mechanics.CAR_VELOCITY_MAX_MS[1] * math.cos(min(abs(track_angle) * 1.4, 1)) * self.aggression
  823.    return target_velocity
  824.  
  825.   def _calculate_relative_positions(self, player_ahead, player_behind):
  826.    track_length = len(self.race.track_def.track)
  827.  
  828.    # See how close we are to the car in front and the car behind
  829.    distance_to_player = int(player_ahead.position[1] - self.player.position[1])
  830.    distance_to_player %= track_length
  831.    distance_to_player *= TrackDef.DISTANCE_BETWEEN_SEGMENTS_M
  832.    self.close_to_player_ahead = (distance_to_player <= Intelligence.PlayerState.OVERTAKING_DISTANCE_M)
  833.  
  834.    # Make sure that we have cleared the car behind before making a manoeuvre
  835.    distance_to_player = int(self.player.position[1] - player_behind.position[1])
  836.    distance_to_player %= track_length
  837.    distance_to_player *= TrackDef.DISTANCE_BETWEEN_SEGMENTS_M
  838.    self.close_to_player_behind = (distance_to_player <= Mechanics.CAR_SIZE_M[Z] * 2)
  839.  
  840.   def _consider_overtaking(self, player_ahead, player_behind):
  841.    self.overtaking = None
  842.  
  843.    # Only consider overtaking if we are travelling faster than the player ahead of us
  844.    if self.player.velocity[1] <= player_ahead.velocity[1]:
  845.     return
  846.  
  847.    # Make sure that we are close enough to the car ahead to consider overtaking
  848.    if not self.close_to_player_ahead:
  849.     return
  850.  
  851.    # Make sure that we have cleared the car behind before making a manoeuvre
  852.    if self.close_to_player_behind:
  853.     return
  854.  
  855.    # We are going to position ourselves for overtaking
  856.    self.overtaking = player_ahead
  857.  
  858.    opponent_x = player_ahead.position[0]
  859.    car_double_width = 2 * Mechanics.CAR_SIZE_M[X]
  860.    if opponent_x >= 0:
  861.     target_x = opponent_x - car_double_width
  862.     if target_x < self.target_x:
  863.      self.target_x = target_x
  864.    else:
  865.     target_x = opponent_x + car_double_width
  866.     if target_x > self.target_x:
  867.      self.target_x = target_x
  868.  
  869.  # The number of track segments to look ahead to determine the appropriate velocity
  870.  TRACK_LOOK_AHEAD = 10
  871.  
  872.  def __init__(self, race):
  873.   self.race = race
  874.   self.player_states = [Intelligence.PlayerState(race, player) for player in race.players]
  875.  
  876.  def process_players(self):
  877.   track_length = len(self.race.track_def.track)
  878.  
  879.   # Create a list of players sorted by their position on the track, rather than their position in the race.
  880.   # If all are on the same lap, this is the same thing, but otherwise it will be different.
  881.   # This is necessary for players to work out who is physically in front or behind of them.
  882.   players = []
  883.   i = 0
  884.   for player in self.race.players:
  885.    players.append([player.position[1] - track_length * self.race.get_player_lap(player), i, player])
  886.    i += 1
  887.   sorted_players = Sort.quick_sort(players)
  888.  
  889.   i = 0
  890.   l = len(sorted_players)
  891.   while i < l:
  892.    sorted_player = sorted_players[i]
  893.    player_index = sorted_player[Race.SORTED_PLAYER_INDEX]
  894.    if player_index != Player.HUMAN:
  895.     player_state = self.player_states[player_index]
  896.     player_ahead = sorted_players[(i + 1) % l][Race.SORTED_PLAYER_PLAYER]
  897.     player_behind = sorted_players[(i - 1) % l][Race.SORTED_PLAYER_PLAYER]
  898.     player_state.think(player_ahead, player_behind)
  899.    i += 1
  900.  
  901. class Race:
  902.  STARTING_GRID_SPACE_M = 1.2
  903.  
  904.  SORTED_PLAYER_POSITION = 0
  905.  SORTED_PLAYER_INDEX = 1
  906.  SORTED_PLAYER_PLAYER = 2
  907.  
  908.  # Initialise a Race specifying the players and the track definition
  909.  def __init__(self, players, track_def):
  910.   # Work around bug in CodeSkulptor that doesn't allow 0 class members.
  911.   Race.SORTED_PLAYER_POSITION = 0
  912.  
  913.   self.players = players
  914.   self.sorted_players = []
  915.   self.track_def = track_def
  916.   self.track_objects = [None] * len(track_def.track)
  917.   self._dynamic_sprites = []
  918.   self._create_track_objects()
  919.   self._init_players()
  920.   self._sort_players()
  921.   self.mechanics = Mechanics(self)
  922.   self.intelligence = Intelligence(self)
  923.  
  924.  # Initialise the players and position them on the starting grid
  925.  def _init_players(self):
  926.   offset = (TrackDef.TRACK_SIZE_M[X] / 4.0, -Race.STARTING_GRID_SPACE_M / TrackDef.DISTANCE_BETWEEN_SEGMENTS_M)
  927.   x = (-offset[0], offset[0])
  928.   z = offset[1]
  929.  
  930.   i = 0
  931.   player_indices = range(Player.COMPUTER, len(self.players))
  932.   random.shuffle(player_indices)
  933.  
  934.   # Ensure that the player starts at the rear of the grid
  935.   player_indices.append(Player.HUMAN)
  936.   for player_index in player_indices:
  937.    player = self.players[player_index]
  938.    player.reset()
  939.    player.position[0] = x[i % 2]
  940.    player.position[1] = z
  941.    self.sorted_players.append([-z, player_index, player])
  942.    i += 1
  943.    z += offset[1]
  944.  
  945.  # Returns the 'lap' that a player is on
  946.  def get_player_lap(self, player):
  947.   return 1 + int(player.position[1] // len(self.track_def.track))
  948.  
  949.  # Returns the position in the race of the specified player
  950.  def get_player_position(self, player_index):
  951.   for i in range(len(self.sorted_players)):
  952.    sorted_player = self.sorted_players[i]
  953.    if sorted_player[Race.SORTED_PLAYER_INDEX] == player_index:
  954.     return i + 1
  955.   return 0
  956.  
  957.  # Returns a player's position on the track, from 0 at the start to len(track) at the end
  958.  def get_player_track_position(self, player):
  959.   track_position = player.position[1]
  960.   track_length = len(self.track_def.track)
  961.   while track_position < 0:
  962.    track_position += track_length
  963.   return track_position
  964.  
  965.  # Define all of the objects that decorate the track
  966.  def _create_track_objects(self):
  967.   track_width = TrackDef.TRACK_SIZE_M[X] * 1.2
  968.   track_edge = track_width / 2
  969.  
  970.   # Create Starting Banner
  971.   height = 2
  972.   image_name = IMG_BANNER
  973.   image_size = IMAGES[image_name][1]
  974.   self._create_track_object(0, (0, height / 2, 0), (track_width, height), image_name)
  975.  
  976.   i = 0
  977.   for segment in self.track_def.track:
  978.    if random.randrange(4) == 0:
  979.     tree = random.randrange(6)
  980.     image_name = IMG_TREE + tree
  981.     image_size = IMAGES[image_name][1]
  982.     x = random.random() * 5 + track_edge
  983.     y = image_size[1] / 2
  984.     z = random.random()
  985.     if random.randrange(2) == 0:
  986.      x = -x
  987.     self._create_track_object(i, (x, y, z), image_size, image_name, True)
  988.    i += 1
  989.  
  990.  # Sorts the sorted_players list to reflect the relative positions of the players.
  991.  def _sort_players(self):
  992.   for sorted_player in self.sorted_players:
  993.    sorted_player[Race.SORTED_PLAYER_POSITION] = -sorted_player[Race.SORTED_PLAYER_PLAYER].position[1]
  994.   self.sorted_players = Sort.quick_sort(self.sorted_players)
  995.  
  996.  # Inserts a track object (a sprite associated with a particular position on the track)
  997.  # The Sprite's position and orientation is relative to the track_position with which it is associated.
  998.  def _create_track_object(self, track_position, position, size, image, absolute_y = False):
  999.   track = self.track_def.track
  1000.  
  1001.   # Find the section of track with which this sprite will be associated
  1002.   track_index = int(track_position)
  1003.   modular_track_index = track_index % len(track)
  1004.   track1 = track[modular_track_index]
  1005.   track2 = track[(track_index + 1) % len(track)]
  1006.  
  1007.   # If track_position specifies a position between two segments of track, we need to
  1008.   #   interpolate the centre point and orientation
  1009.   t = track_position - track_index
  1010.   o1 = track1.orientation
  1011.   o2 = track2.orientation
  1012.   orientation = Math.interpolate(t, o1, o1 + Math.get_angle_between_orientations(o1, o2))
  1013.   centre = []
  1014.   for a in range(3):
  1015.    centre.append(Math.interpolate(t, track1.position[a], track2.position[a]))
  1016.  
  1017.   # Calculate world coordinates for the sprite, based on the centre of the track and the supplied coordinates
  1018.   sine = math.sin(orientation)
  1019.   cosine = math.cos(orientation)
  1020.  
  1021.   # Rotate the local coordinates according to the track segment's orientation
  1022.   px = position[X] * cosine + position[Z] * sine
  1023.   pz = position[Z] * cosine - position[X] * sine
  1024.   world_pos = (px + centre[X], position[Y] + (centre[Y] if not absolute_y else 0), pz + centre[Z])
  1025.   sprite = Sprite(world_pos, size, Sprite.ORIENTATION_BILLBOARD, image)
  1026.  
  1027.   # Check if sprites have already been assigned to this track_position
  1028.   sprite_bucket = self.track_objects[modular_track_index]
  1029.   if sprite_bucket != None:
  1030.    sprite_bucket.append(sprite)
  1031.   else:
  1032.    # Otherwise, start a new sprite bucket
  1033.    sprite_bucket = [sprite]
  1034.    self.track_objects[modular_track_index] = sprite_bucket
  1035.  
  1036.   # Return the bucket in which the sprite was added together with the index
  1037.   return (sprite_bucket, len(sprite_bucket) - 1)
  1038.  
  1039.  # Inserts sprites representing the players into the track_objects dictionary
  1040.  def add_player_sprites(self):
  1041.   # Calculate where the car appears above the track
  1042.   y = TrackDef.TRACK_SIZE_M[Y] / 2.0 + Mechanics.CAR_SIZE_M[Y] / 2.0
  1043.   for player in self.players:
  1044.    track_position = self.get_player_track_position(player)
  1045.    pos = (player.position[0], y, 0)
  1046.    frame = int(player.position[1] * 8) % 4
  1047.    self.add_dynamic_sprite(track_position, pos, Mechanics.CAR_SIZE_M, IMG_CAR + frame)
  1048.  
  1049.  # Registers a dynamic sprite that will be associated with a part of the track and which can be removed at the end of the frame
  1050.  def add_dynamic_sprite(self, track_position, position, size, image):
  1051.   self._dynamic_sprites.append(self._create_track_object(track_position, position, size, image))
  1052.  
  1053.  # Removes sprites representing dynamic objects that were inserted just for this frame
  1054.  def remove_dynamic_sprites(self):
  1055.   i = len(self._dynamic_sprites) - 1
  1056.   while i >= 0:
  1057.    sprite_bucket_info = self._dynamic_sprites[i]
  1058.    sprite_bucket_info[0].pop(sprite_bucket_info[1])
  1059.    i -= 1
  1060.   self._dynamic_sprites = []
  1061.  
  1062.  # Process a slice of time in the race
  1063.  def process_tick(self, delta):
  1064.   m = self.mechanics
  1065.   m.apply_force()
  1066.   m.move_players(delta)
  1067.   m.process_collisions()
  1068.   m.constrain_players_to_track()
  1069.   self._sort_players()
  1070.   self.intelligence.process_players()
  1071.  
  1072. class Renderer:
  1073.  # Note, if changing anything here, please update the view_to_canvas() method, which
  1074.  # uses hard-coded values to gain extra speed, since it is such an important routine.
  1075.  CANVAS_WIDTH = 800
  1076.  CANVAS_HEIGHT = 600
  1077.  CANVAS_HALF_WIDTH = CANVAS_WIDTH // 2
  1078.  CANVAS_HALF_HEIGHT = CANVAS_HEIGHT // 2
  1079.  SCALE_WIDTH = CANVAS_HALF_WIDTH
  1080.  SCALE_HEIGHT = CANVAS_HALF_HEIGHT
  1081.  NEAR_PLANE_M = 0.1
  1082.  FAR_PLANE_M = 200
  1083.  
  1084.  # Projects a 3d view-space coordinate into a 2d canvas coordinate
  1085.  # This routine is included as a reference, but is not used in the game.
  1086.  # Instead, see the method below it.
  1087.  def view_to_canvas_slow(pos):
  1088.   distance = pos[Z] + Renderer.NEAR_PLANE_M
  1089.   x = pos[X] / distance
  1090.   y = -pos[Y] / distance
  1091.   x *= Renderer.SCALE_WIDTH
  1092.   y *= Renderer.SCALE_HEIGHT
  1093.   x += Renderer.CANVAS_HALF_WIDTH
  1094.   y += Renderer.CANVAS_HALF_HEIGHT
  1095.   return (x, y)
  1096.  
  1097.  # Projects a 3d view-space coordinate into a 2d canvas coordinate
  1098.  # This is the optimised version of the view_to_canvas method.
  1099.  # It runs about twice as quickly as the 'readable' version.
  1100.  def view_to_canvas(pos):
  1101.   distance = pos[2] + 0.1
  1102.   return (400.0 * (pos[0] / distance + 1), 300.0 * (-pos[1] / distance + 1))
  1103.  
  1104.  def render_shadow_text(canvas, text, position, size, colour, shadow_colour = "#000"):
  1105.   canvas.draw_text(text, (position[0] + 2, position[1] + 2), size, shadow_colour, FONT_STYLE)
  1106.   canvas.draw_text(text, position, size, colour, FONT_STYLE)
  1107.  
  1108.  def render_image(canvas, image, pos):
  1109.   size = (image.get_width(), image.get_height())
  1110.   centre = (size[0] / 2, size[1] / 2)
  1111.   canvas.draw_image(image, centre, size, (pos[0] + centre[0], pos[1] + centre[1]), size)
  1112.  
  1113. class FPSRenderer(Renderer):
  1114.  def __init__(self, time_counter):
  1115.   self.time_counter = time_counter
  1116.  
  1117.  def render_fps(self, canvas):
  1118.   delta = self.time_counter.get_average_time()
  1119.   if delta > 0:
  1120.    fps = 1.0 / delta
  1121.    canvas.draw_text("FPS: " + str(int(round(fps))), (10, Renderer.CANVAS_HEIGHT - 10), 15, "#fff", FONT_STYLE)
  1122.  
  1123. class RaceRenderer(Renderer):
  1124.  class Message:
  1125.   def __init__(self, text, position):
  1126.    self.text = text
  1127.    self.position = position
  1128.  
  1129.  CAMERA_HEIGHT_ABOVE_TRACK_M = 1.1
  1130.  CAMERA_DISTANCE_BEHIND_PLAYER_M = 1.1
  1131.  
  1132.  TRACK_RENDER_MIN_DEPTH = 60   # The minimum amount of track segments to draw each frame
  1133.  COLOUR_BACKGROUND = "#22470b"
  1134.  COLOUR_ROSTER = "#fff"
  1135.  COLOUR_ROSTER_PLAYER = "#ff0"
  1136.  COLOUR_VELOCITY = "#ff0"
  1137.  
  1138.  def __init__(self, image_manager, race):
  1139.   self.camera = Camera()
  1140.   self.image_manager = image_manager
  1141.   self.race = race
  1142.   self.render_depth = RaceRenderer.TRACK_RENDER_MIN_DEPTH
  1143.   self.message = None
  1144.  
  1145.  def get_camera_track_position(self):
  1146.   track_position = self.race.get_player_track_position(self.race.players[Player.HUMAN])
  1147.   track_position -= RaceRenderer.CAMERA_DISTANCE_BEHIND_PLAYER_M / TrackDef.DISTANCE_BETWEEN_SEGMENTS_M
  1148.   return track_position
  1149.  
  1150.  def _get_track(self):
  1151.   return self.race.track_def.track
  1152.  
  1153.  # Rotate 2d canvas coordinates about the centre of the screen based on the value of roll
  1154.  def _canvas_to_roll(self, pos):
  1155.   camera = self.camera
  1156.   cw = Renderer.CANVAS_HALF_WIDTH
  1157.   ch = Renderer.CANVAS_HALF_HEIGHT
  1158.   tx = pos[0] - cw
  1159.   ty = pos[1] - ch
  1160.   tx, ty = tx * camera.cosine_roll - ty * camera.sine_roll, tx * camera.sine_roll + ty * camera.cosine_roll
  1161.   return (tx + cw, ty + ch)
  1162.  
  1163.  def _render_background(self, canvas):
  1164.   c = self.camera
  1165.  
  1166.   # Calculate Horizon's y coordinate
  1167.   hy = -(RaceRenderer.CAMERA_HEIGHT_ABOVE_TRACK_M + c.position[Y])
  1168.   hz = Renderer.FAR_PLANE_M
  1169.  
  1170.   # Take into account the pitch of the camera
  1171.   horizon_vw = (0, hy * c.cosine_pitch - hz * c.sine_pitch, hy * c.sine_pitch + hz * c.cosine_pitch)
  1172.   horizon_cv = Renderer.view_to_canvas(horizon_vw)
  1173.   hy = horizon_cv[Y]
  1174.  
  1175.   # Draw Sky
  1176.   # The Sky image is 4480x360 pixels. The first 3200 are a panorama, then the first 1280 pixels are repeated.
  1177.   # Only 800 pixels ought to be repeated, but in order for the rolling effect to work, an additional margin is required.
  1178.   # The roll_factor is maximum amount of extra space needed in the upper margins when rolling.
  1179.   # Messy, but empirically ok when camera is pitching too. The original image height was 300, but 60 additional pixels added for margin.
  1180.   roll_factor = 1.7
  1181.   image = self.image_manager.images[IMG_BACKDROP]
  1182.   iw = 3200.0
  1183.   ih = 360.0
  1184.   if ih > 0:
  1185.    angle = (c.yaw + math.pi / 4.0)
  1186.    if c.yaw - abs(c.roll) < 0:
  1187.     angle += 2.0 * math.pi
  1188.    margin = abs(c.sine_roll) * ih * 2.0
  1189.    sp = (iw * angle / (math.pi * 2.0), ih / 2.0)
  1190.    ss = (iw / 4.0 + margin, ih)
  1191.    dih = ih * roll_factor
  1192.    dp = (Renderer.CANVAS_HALF_WIDTH, hy - dih / 2.0)
  1193.    ds = (ss[ 0 ], dih)
  1194.    dp = self._canvas_to_roll(dp)
  1195.    canvas.draw_image(image, sp, ss, dp, ds, c.roll)
  1196.  
  1197.  def _render_sprite(self, canvas, sprite):
  1198.   view_pos = self.camera.world_to_view(sprite.position)
  1199.   if (view_pos[Z] < Renderer.NEAR_PLANE_M) or (view_pos[Z] >= Renderer.FAR_PLANE_M):
  1200.    return
  1201.  
  1202.   centre = Renderer.view_to_canvas(view_pos)
  1203.  
  1204.   sprite_orientation = sprite.orientation
  1205.   if (sprite_orientation != Sprite.ORIENTATION_BILLBOARD):
  1206.    rotation = sprite_orientation - self.camera.yaw
  1207.    apparent_width = abs(sprite.size[X] * math.cos(rotation)) + abs(sprite.size[Z] * math.sin(rotation))
  1208.   else:
  1209.    apparent_width = sprite.size[X]
  1210.  
  1211.   horizontal_extent = Renderer.view_to_canvas((view_pos[X] - apparent_width / 2.0, view_pos[Y], view_pos[Z]))[X]
  1212.   vertical_extent = Renderer.view_to_canvas((view_pos[X], view_pos[Y] + sprite.size[Y] / 2.0, view_pos[Z]))[Y]
  1213.   width = (centre[X] - horizontal_extent) * 2.0
  1214.   height = (centre[Y] - vertical_extent) * 2.0
  1215.  
  1216.   # Rotate the centre point around the centre of the screen to simulate the roll effect
  1217.   centre = self._canvas_to_roll(centre)
  1218.  
  1219.   image = self.image_manager.images[sprite.image]
  1220.   image_size = (image.get_width(), image.get_height())
  1221.   if image_size[0] != 0:
  1222.    # Prevent images that didn't load from causing a crash
  1223.    image_centre = (image_size[0] / 2.0, image_size[1] / 2.0)
  1224.    canvas.draw_image(image, image_centre, image_size, centre, (width, height), self.camera.roll)
  1225.  
  1226.  # Render the track
  1227.  def _render_track(self, canvas):
  1228.   track = self._get_track()
  1229.   track_objects = self.race.track_objects
  1230.   track_length = len(track)
  1231.   length = max(min(self.render_depth, track_length), RaceRenderer.TRACK_RENDER_MIN_DEPTH)
  1232.   track_position = int(self.get_camera_track_position()) - 1
  1233.   for i in range(length):
  1234.    pos = (track_position + length - i) % track_length
  1235.    self._render_sprite(canvas, track[pos].sprite)
  1236.  
  1237.    sprite_bucket = track_objects[pos]
  1238.    if sprite_bucket != None:
  1239.     for sprite in sprite_bucket:
  1240.      self._render_sprite(canvas, sprite)
  1241.  
  1242.   self.render_depth = length
  1243.  
  1244.  # Render the roster of players
  1245.  def _render_player_roster(self, canvas):
  1246.   race = self.race
  1247.   x = 32
  1248.   xname = 56
  1249.   y = 50
  1250.   yd = 28
  1251.  
  1252.   height = yd * (len(race.players) + 0.5)
  1253.  
  1254.   rect = Math.rect(x - 12, y - yd, 320, height)
  1255.   colour = 'rgba(0,0,0,0.5)'
  1256.   canvas.draw_polygon(rect, 1, colour, colour)
  1257.  
  1258.   position = 1
  1259.   total_laps = race.track_def.laps
  1260.   for i in range(len(race.sorted_players)):
  1261.    sorted_player = race.sorted_players[i]
  1262.    player = sorted_player[Race.SORTED_PLAYER_PLAYER]
  1263.    lap = race.get_player_lap(player)
  1264.    message = str(position) + " - " + player.name + " - Lap " + str(lap) + " of " + str(total_laps)
  1265.    if i > 0:
  1266.     previous_player = race.sorted_players[i - 1][Race.SORTED_PLAYER_PLAYER]
  1267.     difference = previous_player.position[1] - player.position[1]
  1268.     difference_metres = difference * TrackDef.DISTANCE_BETWEEN_SEGMENTS_M
  1269.     message += " - " + str(round(difference_metres, 1)) + "m"
  1270.  
  1271.    index = sorted_player[Race.SORTED_PLAYER_INDEX]
  1272.    colour = RaceRenderer.COLOUR_ROSTER_PLAYER if index == Player.HUMAN else RaceRenderer.COLOUR_ROSTER
  1273.    canvas.draw_text(message, (xname, y), 16, colour, FONT_STYLE)
  1274.    if index < IMG_LOGO:
  1275.     Renderer.render_image(canvas, self.image_manager.images[IMG_HEAD + index], (x, y - 20))
  1276.    y += yd
  1277.    position += 1
  1278.  
  1279.  # Render the Player's position and speed
  1280.  def _render_player_status(self, canvas):
  1281.   velocity = self.race.players[Player.HUMAN].velocity[1] * Math.METRES_PER_SECOND_TO_MILES_PER_HOUR
  1282.   message = str(int(round(velocity)))
  1283.  
  1284.   x2 = Renderer.CANVAS_WIDTH - 60
  1285.   x1 = x2 - len(message) * 40
  1286.   y = Renderer.CANVAS_HEIGHT - 30
  1287.   Renderer.render_shadow_text(canvas, message, (x1, y), 60, RaceRenderer.COLOUR_VELOCITY)
  1288.   Renderer.render_shadow_text(canvas, "mph", (x2, y), 18, RaceRenderer.COLOUR_VELOCITY)
  1289.  
  1290.  def _render_message(self, canvas):
  1291.   if self.message == None:
  1292.    return
  1293.   Renderer.render_shadow_text(canvas, self.message.text, (self.message.position, Renderer.CANVAS_HEIGHT - 80), 24, "White", "Black")
  1294.  
  1295.  # Render the race
  1296.  def render(self, canvas):
  1297.   self._render_background(canvas)
  1298.   self._render_track(canvas)
  1299.   self._render_player_roster(canvas)
  1300.   self._render_player_status(canvas)
  1301.   self._render_message(canvas)
  1302.  
  1303. class MiniMapRenderer(Renderer):
  1304.  COLOUR_TRACK = "#000"
  1305.  COLOUR_PLAYER = "#f00"
  1306.  
  1307.  def __init__(self, race, rect):
  1308.   bbox = race.track_def.bounding_box
  1309.   self.race = race
  1310.   self.track_centre = bbox.get_centre()
  1311.   self.scale = min(rect[2] / bbox.get_extent(X), rect[3] / bbox.get_extent(Z))
  1312.   self.origin = (rect[0] + (rect[2] - bbox.get_extent(X) * self.scale) / 2, rect[1] - (rect[3] - bbox.get_extent(Z) * self.scale) / 2)
  1313.   self.points = []
  1314.   self._calculate_track()
  1315.  
  1316.  def _calculate_track(self):
  1317.   control_points = self.race.track_def.control_points
  1318.   lines_per_def = max(6, 80 / len(control_points))
  1319.   self.points = []
  1320.   for cp in control_points:
  1321.    for j in range(lines_per_def):
  1322.     self.points.append(self._project(cp.curve.calculate_point(float(j) / lines_per_def)))
  1323.   self.points.append(self.points[0])
  1324.  
  1325.  def _project(self, point):
  1326.   x = self.origin[0] + (point[X] - self.track_centre[X]) * self.scale
  1327.   y = self.origin[1] - (point[Z] - self.track_centre[Z]) * self.scale
  1328.   return (x, y)
  1329.  
  1330.  def render(self, canvas):
  1331.   canvas.draw_polyline(self.points, 8, MiniMapRenderer.COLOUR_TRACK)
  1332.  
  1333.   race = self.race
  1334.   track = race.track_def.track
  1335.   index = 1
  1336.   for sorted_player in self.race.sorted_players:
  1337.    player = sorted_player[Race.SORTED_PLAYER_PLAYER]
  1338.    player_position = self.race.get_player_track_position(player) % len(track)
  1339.    pos = self._project(track[int(player_position)].position)
  1340.  
  1341.    colour = RaceRenderer.COLOUR_ROSTER_PLAYER if sorted_player[Race.SORTED_PLAYER_INDEX] == Player.HUMAN else RaceRenderer.COLOUR_ROSTER
  1342.    canvas.draw_circle(pos, 3, 1, MiniMapRenderer.COLOUR_PLAYER, colour)
  1343.    canvas.draw_text(str(index), (pos[0] - 4, pos[1] - 8), 14, colour, FONT_STYLE)
  1344.    index += 1
  1345.  
  1346. class TrackOverviewRenderer(Renderer):
  1347.  COLOUR_TRACK_BASE = "#4a4a19"
  1348.  COLOUR_TRACK_BASE_OUTLINE = "#2fa206"
  1349.  COLOUR_TRACK = "#fef126"
  1350.  TRACK_HALF_WIDTH = TrackDef.TRACK_SIZE_M[0] / 2
  1351.  TRACK_POSITION = (Renderer.CANVAS_HALF_WIDTH / 2, Renderer.CANVAS_HALF_HEIGHT / 2)
  1352.  
  1353.  def __init__(self, track_def):
  1354.   self.track_def = track_def
  1355.   self.camera = Camera()
  1356.   self.camera.set_pitch(-math.pi * 45  / 180)
  1357.   self.points = None
  1358.  
  1359.   bb = self.track_def.bounding_box.box
  1360.   self.radius = max(bb[1][X] - bb[0][X], bb[1][Z] - bb[0][Z]) + 20
  1361.   self.track_centre = self.track_def.bounding_box.get_centre()
  1362.   self.camera.position[Y] = self.radius
  1363.   self.set_track_rotation(0)
  1364.   self._calculate_track()
  1365.  
  1366.  def set_track_rotation(self, theta):
  1367.   c = self.camera
  1368.   c.set_yaw(theta)
  1369.   c.position[X] = -self.radius * c.sine_yaw
  1370.   c.position[Z] = -self.radius * c.cosine_yaw
  1371.  
  1372.  def render(self, canvas):
  1373.   self._render_base(canvas)
  1374.   self._render_track(canvas)
  1375.  
  1376.  def _render_base(self, canvas):
  1377.   track_offset = TrackOverviewRenderer.TRACK_POSITION
  1378.   bb = self.track_def.bounding_box
  1379.   box = bb.box
  1380.   c = bb.get_centre()
  1381.  
  1382.   ts = 15 + TrackOverviewRenderer.TRACK_HALF_WIDTH
  1383.   y = box[0][Y] - c[Y]
  1384.   x1 = box[0][X] - c[X] - ts
  1385.   z1 = box[0][Z] - c[Z] - ts
  1386.   x2 = box[1][X] - c[X] + ts
  1387.   z2 = box[1][Z] - c[Z] + ts
  1388.  
  1389.   w = []
  1390.   w.append((x1, y, z1))
  1391.   w.append((x2, y, z1))
  1392.   w.append((x2, y, z2))
  1393.   w.append((x1, y, z2))
  1394.  
  1395.   v = []
  1396.   for p in w:
  1397.    vp = Renderer.view_to_canvas(self.camera.world_to_view(p))
  1398.    vp = (vp[0] + track_offset[0], vp[1] + track_offset[1])
  1399.    v.append(vp)
  1400.   v.append(v[0])
  1401.   canvas.draw_polygon(v, 3, TrackOverviewRenderer.COLOUR_TRACK_BASE_OUTLINE, TrackOverviewRenderer.COLOUR_TRACK_BASE)
  1402.  
  1403.  def _calculate_track(self):
  1404.   control_points = self.track_def.control_points
  1405.   lines_per_def = max(6, 80 / len(control_points))
  1406.  
  1407.   self.points = [[], []]
  1408.   for cp in control_points:
  1409.    for j in range(lines_per_def):
  1410.     t = float(j) / lines_per_def
  1411.     p = self._calculate_track_points(cp.curve, t)
  1412.     for e in range(2):
  1413.      self.points[e].append(p[e])
  1414.  
  1415.  def _calculate_track_points(self, curve, t):
  1416.   p = curve.calculate_point(t)
  1417.   p[X] -= self.track_centre[X]
  1418.   p[Y] -= self.track_centre[Y]
  1419.   p[Z] -= self.track_centre[Z]
  1420.   v = Math.normalise(curve.calculate_tangent(t))
  1421.   v[0] *= TrackOverviewRenderer.TRACK_HALF_WIDTH
  1422.   v[2] *= TrackOverviewRenderer.TRACK_HALF_WIDTH
  1423.   return ((p[X] - v[Z], p[Y], p[Z] + v[X]), (p[X] + v[Z], p[Y], p[Z] - v[X]))
  1424.  
  1425.  def _render_track(self, canvas):
  1426.   camera = self.camera
  1427.   track_offset = TrackOverviewRenderer.TRACK_POSITION
  1428.  
  1429.   start_line = []
  1430.  
  1431.   # Draw both edges of the track
  1432.   for e in range(2):
  1433.    points = []
  1434.    for p in self.points[e]:
  1435.     vp = Renderer.view_to_canvas(camera.world_to_view(p))
  1436.     vp = (vp[0] + track_offset[0], vp[1] + track_offset[1])
  1437.     points.append(vp)
  1438.    start_point = points[0]
  1439.    points.append(start_point)
  1440.    start_line.append(start_point)
  1441.    canvas.draw_polyline(points, 2, TrackOverviewRenderer.COLOUR_TRACK)
  1442.  
  1443.   # Draw Start / Finish Line
  1444.   canvas.draw_line(start_line[0], start_line[1], 2, TrackOverviewRenderer.COLOUR_TRACK)
  1445.  
  1446. class IntroRenderer(Renderer):
  1447.  COLOUR_BACKGROUND = "#312194"
  1448.  COLOUR_PANEL = "#1e155f"
  1449.  COLOUR_PANEL_EDGE = "#271c7c"
  1450.  COLOUR_TEXT_LABEL = "#fef126"
  1451.  COLOUR_TEXT_VALUE = "#fff"
  1452.  COLOUR_TEXT_ADVICE = "#ef9673"
  1453.  COLOUR_TEXT_LOADING = "#f00"
  1454.  
  1455.  def __init__(self, image_manager):
  1456.   self.image_manager = image_manager
  1457.   self.track_rotation = 0
  1458.   self.track = None
  1459.  
  1460.  def set_track(self, track):
  1461.   self.track = track
  1462.   self._track_renderer = TrackOverviewRenderer(track)
  1463.  
  1464.  def render(self, canvas):
  1465.   Renderer.render_image(canvas, self.image_manager.images[IMG_LOGO], (Renderer.CANVAS_HALF_WIDTH - 230, 16))
  1466.   if self.track:
  1467.    rect = Math.rect(40, Renderer.CANVAS_HALF_HEIGHT + 70, Renderer.CANVAS_WIDTH - 80, Renderer.CANVAS_HALF_HEIGHT - 120)
  1468.    canvas.draw_polygon(rect, 4, IntroRenderer.COLOUR_PANEL_EDGE, IntroRenderer.COLOUR_PANEL)
  1469.  
  1470.    self._track_renderer.set_track_rotation(self.track_rotation)
  1471.    self._track_renderer.render(canvas)
  1472.  
  1473.    Renderer.render_shadow_text(canvas, "Choose Track with LEFT and RIGHT", (Renderer.CANVAS_HALF_WIDTH - 155, Renderer.CANVAS_HALF_HEIGHT + 50), 18, IntroRenderer.COLOUR_TEXT_ADVICE)
  1474.  
  1475.    x1 = 70
  1476.    x2 = 190
  1477.    y = Renderer.CANVAS_HALF_HEIGHT + 125
  1478.    Renderer.render_shadow_text(canvas, "Track:", (x1, y), 22, IntroRenderer.COLOUR_TEXT_LABEL)
  1479.    Renderer.render_shadow_text(canvas, self.track.name, (x2, y), 22, IntroRenderer.COLOUR_TEXT_VALUE)
  1480.  
  1481.    y += 40
  1482.    laps = str(self.track.laps)
  1483.    Renderer.render_shadow_text(canvas, "Laps:", (x1, y), 22, IntroRenderer.COLOUR_TEXT_LABEL)
  1484.    Renderer.render_shadow_text(canvas, laps, (x2, y), 22, IntroRenderer.COLOUR_TEXT_VALUE)
  1485.  
  1486.    y += 40
  1487.    distance = str(int(round(self.track.get_length_m()))) + " metres"
  1488.    Renderer.render_shadow_text(canvas, "Distance:", (x1, y), 22, IntroRenderer.COLOUR_TEXT_LABEL)
  1489.    Renderer.render_shadow_text(canvas, distance, (x2, y), 22, IntroRenderer.COLOUR_TEXT_VALUE)
  1490.  
  1491.    pending_images = self.image_manager.get_number_of_pending_images()
  1492.    ready = (pending_images == 0)
  1493.    message = "Press SPACE to RACE!" if ready else "Waiting for " + str(pending_images) + " images"
  1494.    colour = IntroRenderer.COLOUR_TEXT_ADVICE if ready else IntroRenderer.COLOUR_TEXT_LOADING
  1495.    Renderer.render_shadow_text(canvas, message, (Renderer.CANVAS_HALF_WIDTH - 100, Renderer.CANVAS_HEIGHT - 18), 18, colour)
  1496.  
  1497.    Renderer.render_shadow_text(canvas, Game.VERSION, (Renderer.CANVAS_WIDTH - 130, 15), 13, IntroRenderer.COLOUR_TEXT_LABEL)
  1498.    Renderer.render_shadow_text(canvas, "'A' = Toggle Music", (Renderer.CANVAS_WIDTH - 124, Renderer.CANVAS_HEIGHT - 25), 14, IntroRenderer.COLOUR_TEXT_ADVICE)
  1499.    Renderer.render_shadow_text(canvas, "'S' = Toggle SFX", (Renderer.CANVAS_WIDTH - 124, Renderer.CANVAS_HEIGHT - 10), 14, IntroRenderer.COLOUR_TEXT_ADVICE)
  1500.  
  1501. class BuildTracksRenderer(Renderer):
  1502.  COLOUR_BACKGROUND = "#333"
  1503.  
  1504.  def __init__(self, game):
  1505.   self.game = game
  1506.   self.track_index = 0
  1507.   self.log = []
  1508.  
  1509.  def render(self, canvas):
  1510.   track_defs = self.game.track_defs
  1511.   if self.track_index < len(track_defs):
  1512.    track_def = track_defs[self.track_index];
  1513.    start_time = time.time()
  1514.    track_def.create_track()
  1515.    finish_time = time.time()
  1516.    self.log.append('Building track "' + track_def.name + '" ... ' + str(round(finish_time - start_time, 2)) + ' seconds')
  1517.    self.track_index += 1
  1518.  
  1519.    y = 50
  1520.    for line in self.log:
  1521.     canvas.draw_text(line, (50, y), 18, '#0a0', FONT_STYLE)
  1522.     y += 24
  1523.   else:
  1524.    self.game.show_introduction()
  1525.  
  1526. class Key:
  1527.  UP = simplegui.KEY_MAP['up']
  1528.  DOWN = simplegui.KEY_MAP['down']
  1529.  LEFT = simplegui.KEY_MAP['left']
  1530.  RIGHT = simplegui.KEY_MAP['right']
  1531.  SPACE = simplegui.KEY_MAP['space']
  1532.  MAP = simplegui.KEY_MAP['m']
  1533.  SFX = simplegui.KEY_MAP['s']
  1534.  MUSIC = simplegui.KEY_MAP['a']
  1535.  ESCAPE = 27
  1536.  
  1537. class Game:
  1538.  VERSION = 'v1.4 11th July 2013'
  1539.  
  1540.  STATE_INTRODUCTION = 1
  1541.  STATE_PRE_RACE = 2
  1542.  STATE_RACE = 3
  1543.  STATE_POST_RACE = 4
  1544.  STATE_BUILD_TRACKS = 5
  1545.  
  1546.  PRE_RACE_HEIGHT_M = 15
  1547.  PRE_RACE_DELAY_S = 3.5
  1548.  
  1549.  FPS = 60
  1550.  
  1551.  def __init__(self):
  1552.   self.state = Game.STATE_BUILD_TRACKS
  1553.   self.frame = simplegui.create_frame("Power Drift", Renderer.CANVAS_WIDTH, Renderer.CANVAS_HEIGHT)
  1554.   self.frame.set_draw_handler(self.on_render)
  1555.   self.frame.set_keydown_handler(self.on_keydown)
  1556.   self.frame.set_keyup_handler(self.on_keyup)
  1557.   self.frame.set_canvas_background(BuildTracksRenderer.COLOUR_BACKGROUND)
  1558.   self.frame.start()
  1559.  
  1560.   self.time_counter = TimeCounter()
  1561.   self.image_manager = ImageManager()
  1562.   self.music_manager = MusicManager(self.time_counter)
  1563.   self.engine_manager = EngineManager(self.time_counter)
  1564.   self.players = []
  1565.   self.track_defs = []
  1566.   self.active_keys = {}
  1567.   self._build_tracks_renderer = BuildTracksRenderer(self)
  1568.   self._intro_renderer = IntroRenderer(self.image_manager)
  1569.   self._fps_renderer = FPSRenderer(self.time_counter)
  1570.   self._race_renderer = None
  1571.   self._map_renderer = None
  1572.   self._show_map = True
  1573.   self._define_players()
  1574.   self._define_tracks()
  1575.  
  1576.  # Define Players
  1577.  def _define_players(self):
  1578.   for name in PLAYERS:
  1579.    self.players.append(Player(name))
  1580.  
  1581.  # Define Track
  1582.  # A Track is defined as a list of control points and their tangent vectors.
  1583.  def _define_tracks(self):
  1584.   t = TrackDef('Infinity', 3)
  1585.   t.add(ControlPoint((7, 0, -84), (5, 0, 20)))
  1586.   t.add(ControlPoint((21, 0, -13), (5, 0, 35)))
  1587.   t.add(ControlPoint((13, 0, 0), (-20, 0, 0)))
  1588.   t.add(ControlPoint((4, 0, -13), (0, 0, -20)))
  1589.   t.add(ControlPoint((8, 0, -26), (10, 0, -25)))
  1590.   t.add(ControlPoint((14, 7, -52), (5, -2, -25)))
  1591.   t.add(ControlPoint((17, 3, -64), (5, 3, -25)))
  1592.   t.add(ControlPoint((22, 9, -79), (5, -2, -15)))
  1593.   t.add(ControlPoint((27, 0, -100), (2, 0, -10), IMG_SAND))
  1594.   t.add(ControlPoint((28, 0, -115), (0, 0, -30)))
  1595.   t.add(ControlPoint((16, 0, -140), (-30, 0, 0)))
  1596.   t.add(ControlPoint((4, 0, -115), (0, 0, 30)))
  1597.   self.track_defs.append(t)
  1598.  
  1599.   t = TrackDef('Orion', 4)
  1600.   m = 40
  1601.   t.add(ControlPoint((0, 0, 20), (0, 0, m)))
  1602.   t.add(ControlPoint((20, 0, 40), (m, 0, 0), IMG_SAND))
  1603.   t.add(ControlPoint((40, 0, 20), (0, 0, -m), IMG_GRAVEL))
  1604.   t.add(ControlPoint((20, 0, 0), (-m, 0, 0)))
  1605.   t.add(ControlPoint((0, 4, 0), (-m, 0, 0)))
  1606.   t.add(ControlPoint((-20, 0, 0), (-m, 0, 0)))
  1607.   t.add(ControlPoint((-40, 0, -20), (0, 0, -m), IMG_SAND))
  1608.   t.add(ControlPoint((-20, 0, -40), (m, 0, 0), IMG_GRAVEL))
  1609.   t.add(ControlPoint((0, 0, -20), (0, 0, m)))
  1610.   self.track_defs.append(t)
  1611.  
  1612.   t = TrackDef('Saddle', 3)
  1613.   t.add(ControlPoint((107, 0, 37), (0, 0, -30)))
  1614.   t.add(ControlPoint((91, 0, 13), (-30, 0, 0)))
  1615.   t.add(ControlPoint((75, 0, 27), (-8, 0, 15), IMG_SAND))
  1616.   t.add(ControlPoint((57, 0, 39), (-30, 0, 0)))
  1617.   t.add(ControlPoint((39, 0, 27), (-8, 0, -15), IMG_GRAVEL))
  1618.   t.add(ControlPoint((23, 0, 13), (-30, 0, 0)))
  1619.   t.add(ControlPoint((7, 2, 37), (0, 10, 30)))
  1620.   t.add(ControlPoint((7, 5, 45), (0, 3, 10)))
  1621.   t.add(ControlPoint((7, 3, 52), (0, -3, 10)))
  1622.   t.add(ControlPoint((7, 5, 63), (0, 10, 10)))
  1623.   t.add(ControlPoint((23, 0, 87), (30, 0, 0)))
  1624.   t.add(ControlPoint((39, 0, 73), (8, 0, -15), IMG_SAND))
  1625.   t.add(ControlPoint((57, 0, 61), (30, 0, 0)))
  1626.   t.add(ControlPoint((75, 0, 73), (8, 0, 15), IMG_GRAVEL))
  1627.   t.add(ControlPoint((91, 0, 87), (30, 0, 0)))
  1628.   t.add(ControlPoint((107, 0, 63), (0, 0, -30)))
  1629.   self.track_defs.append(t)
  1630.  
  1631.   t = TrackDef('Tree-Tops', 4)
  1632.   t.add(ControlPoint((74, 0, 16), (12, 0, 10)))
  1633.   t.add(ControlPoint((86, 3, 39), (0, 0, 30)))
  1634.   t.add(ControlPoint((72, 1, 55), (-30, 0, 0)))
  1635.   t.add(ControlPoint((46, 4, 36), (-30, 0, 0)))
  1636.   t.add(ControlPoint((20, 1, 55), (-30, 0, 0)))
  1637.   t.add(ControlPoint((4, 3, 39), (0, 0, -30)))
  1638.   t.add(ControlPoint((18, 0, 16), (12, 0, -10)))
  1639.   t.add(ControlPoint((46, 2, 3), (40, 0, 0)))
  1640.   self.track_defs.append(t)
  1641.  
  1642.   t = TrackDef('Oval', 5)
  1643.   t.add(ControlPoint((0, 0, 0), (0, 0, 100)))
  1644.   t.add(ControlPoint((100, 0, 0), (0, 0, -100)))
  1645.   self.track_defs.append(t)
  1646.  
  1647.   t = TrackDef('Inside-Out', 3)
  1648.   t.add(ControlPoint((15, 0, 15), (25, 0, -30), IMG_ROCK))
  1649.   t.add(ControlPoint((55, 0, -10), (30, 0, 30)))
  1650.   t.add(ControlPoint((40, 0, 26), (-25, 0, 25)))
  1651.   t.add(ControlPoint((22, 2, 49), (0, 0, 25)))
  1652.   t.add(ControlPoint((35, 3, 65), (25, 0, 0)))
  1653.   t.add(ControlPoint((47, 2, 49), (0, 0, -25)))
  1654.   t.add(ControlPoint((64, 0, 27), (40, 0, 15)))
  1655.   t.add(ControlPoint((43, 0, 84), (-90, 0, 0)))
  1656.   self.track_defs.append(t)
  1657.  
  1658.   # Add your own tracks here...
  1659.  
  1660.  # Apply the player's input to their car's acceleration
  1661.  def _apply_input(self):
  1662.   acc = self.players[Player.HUMAN].acceleration
  1663.   car_acc = Mechanics.CAR_ACCELERATION_MSS
  1664.   acc[0] = (car_acc[0] if self.is_key_pressed(Key.RIGHT) else 0) + (-car_acc[0] if self.is_key_pressed(Key.LEFT) else 0)
  1665.   acc[1] = (car_acc[1] if self.is_key_pressed(Key.UP) else 0) + (-car_acc[1] if self.is_key_pressed(Key.DOWN) else 0)
  1666.  
  1667.  def _calculate_roll(self):
  1668.   desired_roll = 0
  1669.   strength = 0
  1670.  
  1671.   # Adjust roll based on player's applied horizontal acceleration and their forward velocity
  1672.   player = self.players[Player.HUMAN]
  1673.   acc = player.acceleration[0]
  1674.   if acc != 0:
  1675.    acc /= -Mechanics.CAR_ACCELERATION_MSS[0]
  1676.    vel = player.velocity[1] / Mechanics.CAR_VELOCITY_MAX_MS[1]
  1677.    strength = acc * vel
  1678.    desired_roll = strength * math.pi / 6
  1679.  
  1680.   c = self._race_renderer.camera
  1681.   actual_roll = c.roll
  1682.  
  1683.   if actual_roll != desired_roll:
  1684.    theta = math.pi / (70 if strength == 0 else 40)
  1685.    if actual_roll < desired_roll:
  1686.     actual_roll = min(actual_roll + theta, desired_roll)
  1687.    else:
  1688.     actual_roll = max(actual_roll - theta, desired_roll)
  1689.    c.set_roll(actual_roll)
  1690.  
  1691.  def _get_number_suffix(self, number):
  1692.   n = number % 10
  1693.   if n == 1 and number != 11:
  1694.    return 'st'
  1695.   if n == 2 and number != 12:
  1696.    return 'nd'
  1697.   if n == 3 and number != 13:
  1698.    return 'rd'
  1699.   return 'th'
  1700.  
  1701.  def is_key_pressed(self, key):
  1702.   return key in self.active_keys and self.active_keys[key] == True
  1703.  
  1704.  def _set_selected_track_index(self, index):
  1705.   index %= len(self.track_defs)
  1706.   self.selected_track_index = index
  1707.   self._intro_renderer.set_track(self.track_defs[index])
  1708.  
  1709.  def start_race(self, track_def):
  1710.   self.state = Game.STATE_PRE_RACE
  1711.   self.time_counter.reset()
  1712.   self.frame.set_canvas_background(RaceRenderer.COLOUR_BACKGROUND)
  1713.   self.race = Race(self.players, track_def)
  1714.   self._race_renderer = RaceRenderer(self.image_manager, self.race)
  1715.   self._race_renderer.message = RaceRenderer.Message('Use Cursor Keys to Accelerate, Brake and Steer', 150)
  1716.   self._map_renderer = MiniMapRenderer(self.race, (Renderer.CANVAS_WIDTH * 0.8, Renderer.CANVAS_HEIGHT * 0.2, Renderer.CANVAS_WIDTH * 0.3, Renderer.CANVAS_HEIGHT * 0.3))
  1717.   self.music_manager.play(MUSIC_START)
  1718.  
  1719.  def show_introduction(self):
  1720.   if self.state == Game.STATE_BUILD_TRACKS:
  1721.    self._set_selected_track_index(0)
  1722.   self.state = Game.STATE_INTRODUCTION
  1723.   self.frame.set_canvas_background(IntroRenderer.COLOUR_BACKGROUND)
  1724.   self.engine_manager.stop()
  1725.   self.music_manager.play(MUSIC_MENU)
  1726.   self.race = None
  1727.   self._race_renderer = None
  1728.   self._map_renderer = None
  1729.  
  1730.  # Position the camera behind the player's car, facing along the track
  1731.  def set_camera_position(self):
  1732.   race = self.race
  1733.   player = self.players[Player.HUMAN]
  1734.   track = race.track_def.track
  1735.   track_position = race.get_player_track_position(player)
  1736.   track_index = int(track_position)
  1737.   track1 = track[track_index % len(track)]
  1738.   track2 = track[(track_index + 1) % len(track)]
  1739.  
  1740.   t = track_position - track_index
  1741.   o1 = track1.orientation
  1742.   o2 = track2.orientation
  1743.  
  1744.   c = self._race_renderer.camera
  1745.   c.set_yaw(Math.interpolate(t, o1, o1 + Math.get_angle_between_orientations(o1, o2)))
  1746.  
  1747.   for a in range(3):
  1748.    c.position[a] = Math.interpolate(t, track1.position[a], track2.position[a])
  1749.  
  1750.   # Tilt and re-position the camera when going up and down hills
  1751.   ydiff = track2.position[Y] - track1.position[Y]
  1752.   c.set_pitch(ydiff * math.pi / 2)
  1753.   c.position[Y] += -ydiff * 5
  1754.  
  1755.   player_x = player.position[0]
  1756.   player_z = RaceRenderer.CAMERA_DISTANCE_BEHIND_PLAYER_M
  1757.   player_z += player.velocity[1] / Game.FPS
  1758.   c.position[X] += c.cosine_yaw * player_x - c.sine_yaw * player_z
  1759.   c.position[Y] += RaceRenderer.CAMERA_HEIGHT_ABOVE_TRACK_M
  1760.   c.position[Z] -= c.sine_yaw * player_x + c.cosine_yaw * player_z
  1761.  
  1762.   if self.state == Game.STATE_PRE_RACE:
  1763.    delta = self.time_counter.get_total_time() / Game.PRE_RACE_DELAY_S
  1764.    if delta < 1:
  1765.     c.position[Y] += Game.PRE_RACE_HEIGHT_M * (1 - delta)
  1766.  
  1767.     # Add "Start" sprite
  1768.     track_position = self._race_renderer.get_camera_track_position() + 1
  1769.     x = player.position[X]
  1770.     y = self._race_renderer.camera.position[Y] + 0.2
  1771.     z = 20 * (1 - delta)
  1772.     self.race.add_dynamic_sprite(track_position, (x, y, z), (4.43, 3.07), IMG_START)
  1773.    else:
  1774.     self.state = Game.STATE_RACE
  1775.     self.music_manager.play(MUSIC_RACE + self.selected_track_index % 2)
  1776.     self._race_renderer.message = None
  1777.  
  1778.  def process_tick(self):
  1779.   self.time_counter.record_time()
  1780.   delta = self.time_counter.get_average_time()
  1781.   self.music_manager.process_sound()
  1782.  
  1783.   if self.state != Game.STATE_INTRODUCTION:
  1784.    # Adjust render depth to try to maintain a frame rate around 30fps
  1785.    self._race_renderer.render_depth += 1 if delta < 0.031 else -1 if delta > 0.035 else 0
  1786.  
  1787.    if self.state == Game.STATE_RACE or self.state == Game.STATE_POST_RACE:
  1788.     self._apply_input()
  1789.     self._calculate_roll()
  1790.  
  1791.     race = self.race
  1792.     race.process_tick(1.0 / Game.FPS)
  1793.  
  1794.     self.engine_manager.set_pitch(self.players[Player.HUMAN].velocity[1] / Mechanics.CAR_VELOCITY_MAX_MS[1])
  1795.     self.engine_manager.process_sound()
  1796.  
  1797.     # Check if race has finished
  1798.     if self.state == Game.STATE_RACE:
  1799.      if race.get_player_lap(self.players[Player.HUMAN]) > race.track_def.laps:
  1800.       final_position = self.race.get_player_position(Player.HUMAN)
  1801.       suffix = self._get_number_suffix(final_position)
  1802.       message = 'You finished ' + str(final_position) + suffix + ' - Press ESC to race again'
  1803.       self.music_manager.play(MUSIC_WIN if final_position == 1 else MUSIC_LOSE)
  1804.       self._race_renderer.message = RaceRenderer.Message(message, 160)
  1805.       self.state = Game.STATE_POST_RACE
  1806.   else:
  1807.    # Rotate the track 60 degrees per second
  1808.    self._intro_renderer.track_rotation += delta * math.pi / 3
  1809.  
  1810.  def on_render(self, canvas):
  1811.   if self.state == Game.STATE_INTRODUCTION:
  1812.    self.process_tick()
  1813.    self._intro_renderer.render(canvas)
  1814.   elif self.state == Game.STATE_BUILD_TRACKS:
  1815.    self._build_tracks_renderer.render(canvas)
  1816.   else:
  1817.    self.process_tick()
  1818.    self.race.add_player_sprites()
  1819.    self.set_camera_position()
  1820.    self._race_renderer.render(canvas)
  1821.    if self._show_map:
  1822.     self._map_renderer.render(canvas)
  1823.    self.race.remove_dynamic_sprites()
  1824.   self._fps_renderer.render_fps(canvas)
  1825.  
  1826.  def on_keydown(self, key):
  1827.   self.active_keys[key] = True
  1828.   if self.state == Game.STATE_INTRODUCTION:
  1829.    if self.is_key_pressed(Key.LEFT):
  1830.     self._set_selected_track_index(self.selected_track_index - 1)
  1831.    elif self.is_key_pressed(Key.RIGHT):
  1832.     self._set_selected_track_index(self.selected_track_index + 1)
  1833.    elif self.is_key_pressed(Key.SPACE):
  1834.     self.start_race(self.track_defs[self.selected_track_index])
  1835.   elif self.state != Game.STATE_BUILD_TRACKS:
  1836.    if self.is_key_pressed(Key.ESCAPE):
  1837.     self.show_introduction()
  1838.   if self.is_key_pressed(Key.MAP):
  1839.    self._show_map = not self._show_map
  1840.   elif self.is_key_pressed(Key.SFX):
  1841.    self.engine_manager.toggle()
  1842.   elif self.is_key_pressed(Key.MUSIC):
  1843.    self.music_manager.toggle()
  1844.  
  1845.  def on_keyup(self, key):
  1846.   self.active_keys[key] = False
  1847.  
  1848. # Initialisation
  1849. Game()
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement