Advertisement
DarkArtsAndSciences

Trigger Warning (Ludum Dare 27: 10 seconds)

Aug 26th, 2013
107
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 14.64 KB | None | 0 0
  1. """
  2. Ludum Dare 27: 10 Seconds
  3. Trigger Warning
  4.   by
  5. Dark Arts and Sciences
  6.  
  7. Boids pseudocode from [http://www.kfish.org/boids/pseudocode.html], ported during the competition.
  8.  
  9. Requires python and pygame.
  10.  
  11. Controls:
  12.     Hold down the mouse to attract boids.
  13.     Boids set off triggers that they touch.
  14.     Boids are yellow if they have touched a trigger, blue if they're angry about other boids flying too close, and red if they're very angry.
  15.  
  16. Luckily for you, I ran out of time.
  17. Turning boids yellow is the ONLY thing triggers do.
  18. I did not implement any painful triggers. (You're welcome.)
  19.  
  20. For future pain-enhanced versions, visit:
  21.     darkartsandsciences.com
  22. """
  23.  
  24. import math
  25. import platform
  26. import random
  27. import time
  28.  
  29. class Trigger:
  30.     def __init__(self, x, y, start, end=None, live=True):
  31.         self.x = x
  32.         self.y = y
  33.         self.start = start
  34.         self.end = end or float('inf')
  35.         self.live = live
  36.         self.triggered = False
  37.         #print "created {}".format(self)
  38.  
  39.     def __str__(self):
  40.         return "trigger at {},{} {}-{}".format(self.x, self.y, self.start, self.end)
  41.  
  42.     def get_rect(self):
  43.         return (self.x, self.y, trigger_size, trigger_size)
  44.  
  45. class Warning:
  46.     def __init__(self, string, trigger):
  47.         self.string = string
  48.         self.start = trigger.start
  49.         self.end = trigger.start + 10
  50.         #print "created {}".format(self)
  51.  
  52.     def __str__(self):
  53.         return "warning {} at {}-{}".format(self.string, self.start, self.end)
  54.  
  55.     def draw(self, screen, fade=1):
  56.         render = warning_font.render(self.string, warning_aa, warning_color)
  57.         location = [center[0] - render.get_width()/2, center[1] - warning_font.get_ascent()]
  58.         surface = pygame.surface.Surface((render.get_width(), render.get_height()))
  59.         surface.set_alpha(255*fade)
  60.         surface.blit(render, (0,0))
  61.         screen.blit(surface, location)
  62.  
  63. class Effect:
  64.     def __init__(self, effect, trigger, live=False):
  65.         self.effect = effect
  66.         self.trigger = trigger
  67.         self.start = trigger.start + 10
  68.         self.end = trigger.end + 10
  69.         self.live = live
  70.         #print "created {}".format(self)
  71.  
  72.     def __str__(self):
  73.         return "effect at {}-{}: {}".format(self.start, self.end, self.effect)
  74.  
  75. import pygame
  76. pygame.init()
  77.  
  78. # colors
  79. black = [  0,  0,  0]
  80. gray  = [127,127,127]
  81. white = [255,255,255]
  82. blue  = [  0,127,255]
  83. green = [  0,255,  0]
  84. red   = [255,  0,  0]
  85. yellow = [255, 255, 0]
  86.  
  87. # settings
  88. frame_rate = 30
  89. size = [800, 600]
  90. center = [400, 300]
  91. screen = pygame.display.set_mode(size)
  92.  
  93. title = "Trigger Warning"
  94. pygame.display.set_caption(title)
  95.  
  96. command_key = pygame.KMOD_CTRL
  97. if platform.system() == 'Darwin': command_key = pygame.KMOD_META
  98.  
  99. background_color = black
  100. live_trigger_color = green
  101. dead_trigger_color = blue
  102. trigger_size = size[0]/40
  103.  
  104. warning_font  = pygame.font.Font(None, 72)
  105. fps_font      = pygame.font.Font(None, 14)
  106. clock_font    = pygame.font.Font(None, 72)
  107. intro_font    = pygame.font.Font(None, 36)
  108. warning_aa    = True  # text antialiasing
  109. fps_aa        = True
  110. clock_aa      = True
  111. intro_aa      = True
  112. warning_color = red
  113. fps_color     = white
  114. clock_color   = white
  115. intro_color   = white
  116.  
  117. # intro / help
  118. text_pause = " "*10
  119. intro_time = 20  # TODO: 10 for test, 20-30 for release
  120. intro_fade_time = 2
  121. intro_strings = ["These are Triggers."+text_pause*5,
  122.     "A Trigger has a Warning and an Effect."+text_pause,
  123.     "The Effect happens ten seconds after the Warning."+text_pause,
  124.     "This time can not be changed."+text_pause*5,
  125.     "Time itself can be changed."]
  126. intro_length = sum([len(s) for s in intro_strings])
  127. letters_per_second = intro_length/intro_time
  128. intro_pauses = [(0.95, 0.985), (1.0, 1.1)]
  129. intro_pauses = [(x*intro_time, y*intro_time) for x,y in intro_pauses]
  130. line_spacing = 2.5
  131. intro_offset = 100
  132.  
  133. # boids
  134. class Point:
  135.     def __init__(self, x, y):
  136.         self.x = float(x)
  137.         self.y = float(y)
  138.     def __str__(self):
  139.         return "{},{}".format(self.x, self.y)
  140.     def __repr__(self):
  141.         return "Point({},{})".format(self.x, self.y)
  142.     def __add__(self, other):
  143.         return Point(self.x + other.x, self.y + other.y)
  144.     def __sub__(self, other):
  145.         return Point(self.x - other.x, self.y - other.y)
  146.     def __mul__(self, multiplier):
  147.         multiplier = float(multiplier)
  148.         return Point(self.x * multiplier, self.y * multiplier)
  149.     def __div__(self, divisor):
  150.         divisor = float(divisor)
  151.         return Point(self.x / divisor, self.y / divisor)
  152.     def __abs__(self):
  153.         return Point(abs(self.x), abs(self.y))
  154.     def distance(self, other):
  155.         d = other - self
  156.         return math.hypot(d.x, d.y)
  157.     def velocity(self):
  158.         return Point(0,0).distance(self)
  159.     def speed(self):
  160.         return math.fabs(self.velocity())
  161. zero_point = Point(0,0)
  162. center_point = Point(center[0], center[1])
  163.  
  164. boids = []
  165. class Boid:
  166.     max_size = 5
  167.  
  168.     def __init__(self):
  169.         # physics
  170.         self.p = Point(random.randrange(size[0]), random.randrange(size[1]))
  171.         self.v = zero_point
  172.         self.size = random.randrange(1, Boid.max_size)
  173.  
  174.         # display
  175.         self.color = white
  176.  
  177.         # mood
  178.         self.triggered = False
  179.         self.collisions = 0
  180.         self.last_mood_change = 0
  181.  
  182.         # boids change their mood no more often than once per second,
  183.         # but some boids may wait up to five seconds longer
  184.         self.mood_length = 1 + 5*random.random()
  185.  
  186.         boids.append(self)
  187.  
  188.     def __str__(self):
  189.         return "boid at {},{}".format(self.p.x, self.p.y)
  190.  
  191.     def __repr__(self):
  192.         return "[Boid x={}, y={}]".format(self.p.x, self.p.y)
  193.  
  194.     def get_rect(self):
  195.         return (self.p.x, self.p.y, self.size, self.size)
  196.  
  197.     def draw(self, screen, fade=1):
  198.         surface = pygame.surface.Surface((self.size, self.size))
  199.         surface.set_alpha(255*fade)
  200.         pygame.draw.rect(surface, self.color, surface.get_rect())
  201.         screen.blit(surface, self.get_rect())
  202.  
  203.     # update this boids position
  204.     # v: new velocity
  205.     # b: other boid
  206.     def update(self):
  207.  
  208.         # given how to react to one other boid, calculate the outer loop
  209.         # flock rules may have side effects for moods; don't touch the physics
  210.         #
  211.         # to return the flock's average, pass flocksize-1 to average
  212.         # to scale the result, pass a multiplier to scale
  213.         # type 'as-is' returns the resulting point
  214.         # type 'towards' returns a vector from self.p to the point
  215.         def flock_rule(per_boid=None, average=1.0, scale=1.0, type='as-is'):
  216.             v = zero_point
  217.             for other_boid in boids:
  218.                 if other_boid is self: continue
  219.                 if per_boid: v += (per_boid(other_boid) or zero_point)
  220.             v /= average
  221.             #print "flock rule: returning {}/{}*{} {}".format(v, average, scale, type)
  222.             if type == 'as-is': return v * scale
  223.             if type == 'towards': return(v - self.p) * scale
  224.             if type == 'velocity': return(v - self.v) * scale
  225.  
  226.         # Rule random.random(): Boids move randomly
  227.         def random_rule(scale=1.0):
  228.             return Point((random.random()-0.5)*scale, (random.random()-0.5)*scale)
  229.         v = random_rule(0.001)
  230.  
  231.         # Rule 1: Boids fly towards the flock's center
  232.         v += flock_rule(per_boid=lambda b: b.p, average=len(boids)-1, scale=1.0/100, type='towards')
  233.  
  234.         # Rule 2: Boids keep a min distance away from other boids
  235.         collision_distance = self.size * 2
  236.         def avoid_collision(b):
  237.             # if this boid is too close to that boid
  238.             if self.p.distance(b.p) < collision_distance:
  239.                 # count near misses and get angry
  240.                 self.collisions += 1
  241.                 # move towards smaller birds and away from larger birds
  242.                 distance = b.p - self.p
  243.                 if (self.size == b.size):
  244.                     scale = -1
  245.                 else:
  246.                     scale = self.size / (self.size - b.size) - 1
  247.                 #print "near miss with boid sizes {}/{}, moving {}".format(self.size, b.size, distance*scale)
  248.                 return distance*scale
  249.         v += flock_rule(avoid_collision)
  250.  
  251.         # Rule 3: Boids try to fly as fast as nearby boids
  252.         match_velocity_distance = self.size * 50
  253.         def match_velocity(b):
  254.             # if this boid is close to that boid
  255.             distance = self.p.distance(b.p)
  256.             if distance < match_velocity_distance:
  257.                 return b.v * (match_velocity_distance - distance)/match_velocity_distance
  258.         near_boids = len([b for b in boids if self.p.distance(b.p) < match_velocity_distance])
  259.         v += flock_rule(match_velocity, average=near_boids, type='velocity')
  260.  
  261.         # Rule: Boids try to stay on screen
  262.         def stay_on_screen(border=0, speed=1):
  263.             if self.p.x < border:
  264.                 x = (border - self.p.x)*speed
  265.             elif self.p.x > size[0]-border:
  266.                 x = (size[0]-border - self.p.x)*speed
  267.             else:
  268.                 x = 0
  269.             if self.p.y < border:
  270.                 y = (border - self.p.y)*speed
  271.             elif self.p.y > size[1]-border:
  272.                 y = (size[1]-border - self.p.y)*speed
  273.             else:
  274.                 y = 0
  275.             return Point(x,y)
  276.         on_screen = stay_on_screen(100,0.1)
  277.         v += on_screen
  278.  
  279.         # Rule: Boids fly towards or away from locations
  280.         def towards_location(p=center_point, scale=1):
  281.             return (p - self.p) * scale
  282.  
  283.         # if the mouse is down, move towards it
  284.         mouse_attract = 0.00005
  285.         mouse_attracted = zero_point
  286.         if pygame.mouse.get_pressed()[0]:
  287.             mouse_point = Point(*pygame.mouse.get_pos())
  288.             mouse_attracted = towards_location(mouse_point, mouse_attract*mouse_point.speed())
  289.             v += mouse_attracted
  290.  
  291.         # convert from per second to per frame
  292.         v /= frame_rate
  293.  
  294.         # update velocity
  295.         self.v += v
  296.  
  297.         # speed limit
  298.         min_speed = self.size * 1.5
  299.         max_speed = min_speed + (1*Boid.max_size-self.size)
  300.         max_speed += self.collisions/20.0  # anger
  301.         max_speed += mouse_attracted.speed()  # don't limit mouse attraction
  302.         max_speed += on_screen.speed()  # or keep-on-screen
  303.         current_speed = self.v.speed()
  304.         if current_speed > max_speed:
  305.             self.v /= current_speed * max_speed
  306.         if current_speed < min_speed:
  307.             self.v *= min_speed - current_speed
  308.  
  309.         # update position
  310.         self.p += self.v
  311.  
  312.     def change_mood(self, now_raw):
  313.         # boids have short memories
  314.         if now_raw < boid.last_mood_change + boid.mood_length: return
  315.         boid.last_mood_change = now_raw
  316.  
  317.         # forget collisions
  318.         if self.collisions: self.collisions /= 2
  319.  
  320.         # update color
  321.         if (self.collisions > 10): self.color = blue
  322.         if (self.collisions > 20): self.color = red
  323.         if (self.triggered): self.color = yellow
  324.  
  325. def generate_boids(number=20):
  326.     for n in range(number): Boid()
  327.  
  328. # triggers, warnings, and effects
  329. trigger_y = intro_offset + intro_font.get_linesize() * (1+(line_spacing-1)/2) - trigger_size/2
  330. triggers = [Trigger(None, None, intro_time + intro_fade_time + 1),  # start game
  331.             Trigger(size[0]/4,   trigger_y, 1, float('inf'), False),  # left demo
  332.             Trigger(size[0]*3/4, trigger_y, 1, float('inf'), False)]  # right demo
  333. def enable_triggers():
  334.     for trigger in triggers:
  335.         trigger.live = True
  336.  
  337. warnings = [Warning("Warning!", triggers[0])]  # start game
  338.  
  339. def rewind_time():
  340.     pass
  341.     #print "rewind time"
  342.  
  343. effects = [Effect(generate_boids, triggers[0]),
  344.            Effect(enable_triggers, triggers[0]),
  345.            Effect(rewind_time, triggers[1]),
  346.            Effect(rewind_time, triggers[2])]
  347.  
  348. # loop until quit
  349. done = False
  350. paused = False
  351. reset = False
  352. start = time.time()
  353. clock = pygame.time.Clock()
  354. offset = 0
  355. while done == False:
  356.  
  357.     # limit frame rate and cpu usage
  358.     clock.tick(frame_rate)
  359.  
  360.     # game time
  361.     if paused: offset -= 1.0/frame_rate
  362.     now_raw = time.time()
  363.     now = now_raw + offset
  364.     since_raw = now_raw - start
  365.     since = now - start
  366.  
  367.     # handle events
  368.     for event in pygame.event.get():
  369.         if event.type == pygame.QUIT:
  370.             done = True
  371.  
  372.         if event.type == pygame.KEYDOWN:
  373.             if pygame.key.get_mods() & command_key:
  374.                 if event.key == pygame.K_q:  # quit
  375.                     done = True
  376.             else:
  377.                 if event.key == pygame.K_p:  # pause
  378.                     paused = True
  379.                 if event.key == pygame.K_r:  # rewind
  380.                     offset -= 0.25
  381.  
  382.         if event.type == pygame.KEYUP:
  383.             if pygame.key.get_mods() & command_key:
  384.                 pass
  385.             else:
  386.                 if event.key == pygame.K_p:  # unpause
  387.                     paused = False
  388.  
  389.     # draw
  390.  
  391.     # alpha multiplier, 0.0 < fade < 1.0
  392.     def fade(show_since, fade_since=None, fade_length=10):
  393.         if not fade_since: fade_since = show_since
  394.         if since < show_since: return 0
  395.         if since < fade_since: return 1
  396.         if since > fade_since + fade_length: return 0
  397.         return 1 - (since-fade_since)/fade_length
  398.  
  399.     # background
  400.     screen.fill(background_color)
  401.  
  402.     # intro
  403.     if (since < intro_time + intro_fade_time):
  404.         for pause_start, pause_end in intro_pauses:
  405.             if not paused and pause_start < since_raw < pause_end:
  406.                 paused = True
  407.                 reset = True
  408.             if paused and reset and since_raw > pause_end:
  409.                 paused = False
  410.                 reset = False
  411.  
  412.         letters = int(since * letters_per_second)
  413.         i = 0
  414.         used = 0
  415.         while (i < len(intro_strings) and used + len(intro_strings[i]) < letters):
  416.             try: used += len(intro_strings[i])
  417.             except IndexError: break
  418.             i += 1
  419.         intro_surface = pygame.surface.Surface((size[0], size[1]))
  420.         intro_surface.set_alpha(255*fade(0, intro_time, intro_fade_time))
  421.         j = 0
  422.         try: strings = intro_strings[:i] + [intro_strings[i][:letters-used]]
  423.         except IndexError: strings = intro_strings
  424.         for string in strings:
  425.             string_render = intro_font.render(string, intro_aa, intro_color)
  426.             string_location = [intro_offset, intro_offset + j*intro_font.get_linesize()*line_spacing]
  427.             intro_surface.blit(string_render, string_location)
  428.             j += 1
  429.         screen.blit(intro_surface, (0,0))
  430.  
  431.     # triggers
  432.     for trigger in [t for t in triggers if t.start < since < t.end and t.x and t.y]:
  433.         if trigger.live:
  434.             trigger_color = live_trigger_color
  435.             for b in boids:
  436.                 t_rect = pygame.Rect(trigger.get_rect())
  437.                 b_rect = pygame.Rect(b.get_rect())
  438.                 if t_rect.colliderect(b_rect):
  439.                     #print "{} hit {}".format(boid, trigger)
  440.                     trigger.triggered = True
  441.                     b.triggered = True
  442.                     for effect in effects:
  443.                         if effect.trigger is trigger:
  444.                             effect.effect()
  445.         else:
  446.             trigger_color = dead_trigger_color
  447.         pygame.draw.ellipse(screen, trigger_color, trigger.get_rect())
  448.  
  449.     # boids
  450.     for boid in boids:
  451.         boid.change_mood(now_raw)
  452.         boid.update()
  453.         boid.draw(screen)
  454.  
  455.     # warnings
  456.     for warning in warnings:
  457.         wf = fade(warning.start)
  458.         if 0 < wf <= 1: warning.draw(screen, wf)
  459.  
  460.     # effects
  461.     for effect in effects:
  462.         if (not effect.live) and (effect.start < since < effect.end):
  463.             effect.live = True
  464.             effect.effect()
  465.  
  466.     # clock, bottom center
  467.     minutes = int(math.floor(since/60))
  468.     sign = "-"*(minutes < 0)
  469.     seconds = abs(int(math.floor(since)%60))
  470.     seconds_zero = "0"*(seconds < 10)
  471.     tenths = int(since*10%10)
  472.     clock_string = "{}{}:{}{}.{}".format(sign, abs(minutes), seconds_zero, seconds, tenths)
  473.     clock_render = clock_font.render(clock_string, clock_aa, clock_color)
  474.     clock_width = 140  # fixes flicker, TODO: precalc from typical string
  475.     clock_location = [center[0]-clock_width/2, size[1]-50 - clock_font.get_ascent()]
  476.     screen.blit(clock_render, clock_location)
  477.  
  478.     # fps, bottom right
  479.     fps_string = "fps: {}".format(int(clock.get_fps()))
  480.     fps_render = fps_font.render(fps_string, fps_aa, fps_color)
  481.     fps_location = [size[0]-10 - fps_render.get_width(), size[1]-10 - fps_font.get_ascent()]
  482.     screen.blit(fps_render, fps_location)
  483.  
  484.     pygame.display.flip()
  485.  
  486. # quit
  487. pygame.quit()
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement