# -*- coding: utf-8 -*- import sys, random, subprocess from random import randint, randrange, choice from time import time from operator import itemgetter import pygame from pygame import Surface, PixelArray, Rect from pygame.draw import aalines, polygon from pygame.font import Font from pygame.mixer import Sound from pygame.locals import * from lib.pgfw.pgfw.Game import Game from lib.pgfw.pgfw.GameChild import GameChild from lib.pgfw.pgfw.Sprite import Sprite, BlinkingSprite from lib.pgfw.pgfw.Vector import Vector from lib.pgfw.pgfw.extension import render_box from .land.Land import Land # Import GPIO library if available try: import RPi.GPIO as GPIO except ImportError: pass class ElectricSieve(Game): # The GPIO pins corresponding to the buttons and LED indicators PIN_BUTTON_LEFT = 17 PIN_BUTTON_RIGHT = 27 PIN_LED_UP = 22 PIN_LED_DOWN = 23 def __init__(self): """ Initialize super class, GPIO input, background, and activate the title screen. """ # Initialize super Game.__init__(self) # Add type declarations for custom config entries self.get_configuration().type_declarations.add_chart({ "display": { "bool": "rotate" }, "input": { "int": ["title-hold", "initials-hold", "initials-idle"] }, "audio": { "int": "title-fade" }, "land": { "int": ["gradient", "height", "x-step"], "float": ["altitude-ratio", "spacing-factor", "velocity-ratio", "fade-speed"] }}) # Member dict for tracking pin state changes. Start at 1 because high means unpressed. self.pin_states = { self.PIN_BUTTON_LEFT: 1, self.PIN_BUTTON_RIGHT: 1 } # Rotate the display if requested on the command line or in the configuration if self.check_command_line("-rotate"): self.configuration.set("display", "rotate", True) self.rotated = False if self.get_configuration("display", "rotate"): self.display.rotate() self.rotated = True # Initialize GPIO input and callbacks if GPIO library is loaded if "RPi.GPIO" in sys.modules: self.initialize_gpio() self.velocity = Vector(0, 0) # Create an intermediate surface to draw the triangles to and create a trail effect self.trail_effect = Surface(self.get_display_surface().get_size(), pygame.SRCALPHA) # Create a black background self.background = Surface(self.get_display_surface().get_size()) self.background.fill((0, 0, 0)) # Alpha filter self.alpha_filter = Surface(self.get_display_surface().get_size(), pygame.SRCALPHA) self.alpha_filter.fill(Color(0, 0, 0, 80)) # Create game objects self.title = Title(self) self.sieve = Sieve(self) self.triangles = Triangles(self) self.acid = Acid(self) self.static = Static(self) self.land = Land(self) # Start the title screen self.title.activate() def gpio_enabled(self): """ @return True if GPIO mode was requested at launch, False otherwise """ return "--gpio" in sys.argv def initialize_gpio(self): """ Set pin numbering mode to GPIO, initialize all buttons to input pullup. """ # Use GPIO numbering GPIO.setmode(GPIO.BCM) # Set button pins to pullup and attach to each a callback that runs on press or release for pin in self.PIN_BUTTON_LEFT, self.PIN_BUTTON_RIGHT: GPIO.setup(pin, GPIO.IN, pull_up_down=GPIO.PUD_UP) GPIO.add_event_detect(pin, GPIO.BOTH, self.gpio_input) def gpio_input(self, pin): """ Translate GPIO input into PGFW commands, so Raspberry Pi wired controllers be used for input. Compare the pin state to what is stored in memory. Only fire an event if there has been a change in state. A change from high to low triggers a press event. A change from low to high triggers a press cancel event. @param pin Raspberry Pi pin number as read by the RPi.GPIO library """ if "--debug" in sys.argv: pin_name = "left" if pin == ElectricSieve.PIN_BUTTON_LEFT else "right" left_pin_state = GPIO.input(ElectricSieve.PIN_BUTTON_LEFT) right_pin_state = GPIO.input(ElectricSieve.PIN_BUTTON_RIGHT) print(f"Received {pin} ({pin_name}) input. Left state is {left_pin_state}. Right state is {right_pin_state}") # If the saved state of the pin is the same, there hasn't been a real button press or release, so don't continue if self.pin_states[pin] != GPIO.input(pin): self.pin_states[pin] = GPIO.input(pin) # A high signal means the button is released, and a low signal means it is pressed cancel = not (GPIO.input(pin) == GPIO.LOW) if pin == ElectricSieve.PIN_BUTTON_LEFT: self.input.post_command("left", cancel=cancel) elif pin == ElectricSieve.PIN_BUTTON_RIGHT: self.input.post_command("right", cancel=cancel) self.input.post_any_command(id=pin, cancel=cancel) def orient(self, geometry): """ Orient the passed pgfw.Vector, pygame.Rect, or pygame.Surface so it is rotated if necessary. @param geometry A pgfw.Vector or pygame.Rect to rotate @return Either a new pgfw.Vector or pygame.Rect depending on which was passed """ if not self.rotated: return geometry else: if isinstance(geometry, Vector): return self.rotated_point(geometry) elif isinstance(geometry, pygame.Rect): return self.rotated_rect(geometry) elif isinstance(geometry, pygame.Surface): return pygame.transform.rotate(geometry, 90) def rotated_point(self, point): """ Return a new pgfw.Vector with the X and Y values of a pgfw.Vector rotated 90 degrees. @param point pgfw.Vector to rotate @return rotated pgfw.Vector """ return Vector(self.get_display_surface().get_height() - point.x, point.y) def rotated_rect(self, rect): """ Return a new pygame.Rect rotated 90 degrees. @param rect pygame.Rect to rotate @return rotated pygame.Rect """ rotated = pygame.Rect(0, 0, 0, 0) rotated.x = rect.y rotated.y = self.get_display_surface().get_height() - rect.x + rect.w rotated.w = rect.h rotated.h = rect.w return rotated def end(self, event): """ Extend the parent end method to try adding a permanent quit feature in case there is a Raspbian Lite systemd autostart service running """ if event.type == QUIT or self.delegate.compare(event, "quit"): if self.confirming_quit or not self.get_configuration("input", "confirm-quit"): # If SHIFT is pressed, try permanently stopping the systemd service to get a console back in case this is running on # Raspbian Lite if pygame.key.get_mods() & pygame.KMOD_SHIFT: try: subprocess.run(["sudo", "systemctl", "stop", "ibitfit"]) print("Killing with permanent stop sent to systemd ibitfit.service") except: print("No iBitFit system service detected, so permanent quit either failed or was unnecessary") super().end(event) def update(self): # Test if the level is being played if self.triangles.active: # Draw grid effect self.land.update() # Draw triangles onto the trail effect surface, update position self.triangles.update() if not self.title.active: # Draw bottom layer background self.get_display_surface().blit(self.background, (0, 0)) # Draw static behind objects on title screen if self.title.active: self.static.update() # Draw the triangles to the screen, using the intermediate trail effect surface self.get_display_surface().blit(self.trail_effect, (0, 0)) self.title.update() # Draw the sieve self.sieve.update() if not self.title.active: # Draw the static self.static.update() class Title(GameChild): def __init__(self, parent): GameChild.__init__(self, parent) self.display_surface = self.get_display_surface() self.delegate = self.parent.delegate # bg_color = (255, 222, 173) # bg_color = (220, 220, 148) bg_color = 200, 168, 122 self.background = surface = Surface(self.display_surface.get_size()) tile = Surface((2, 2)) tile.fill(bg_color) # tile.set_at(Vector(0, 1), (220, 119, 41)) # tile.set_at(Vector(0, 0), (220, 119, 41)) for y in range(0, surface.get_height(), 2): for x in range(0, surface.get_width(), 2): surface.blit(self.get_game().orient(tile), (x, y)) # font = Font(self.get_resource("display", "title-font-path"), 20) # font.set_italic(True) # font.set_bold(True) # self.captions = captions = Sprite(self), Sprite(self) # colors = (0, 68, 170), (255, 255, 255), (128, 128, 128), \ # (220, 119, 41), (255, 80, 80), (0, 90, 110) # texts = ["", ""] # for ii, text in \ # enumerate(self.get_configuration("display", # "caption").upper().split()): # texts[ii] += "•" * (5 if ii else 3) # for ch in text: # texts[ii] += ch + " " # texts[ii] = texts[ii].strip() + "•" * (5 if ii else 3) # for _ in range(25): # color = choice(colors) # captions[0].add_frame(font.render(texts[0], True, color, (220, 208, 255))) # captions[1].add_frame(font.render(texts[1], True, color, (220, 208, 255))) # cx = self.display_surface.get_rect().centerx # captions[0].location.center = cx, 301 # captions[1].location.center = cx, 398 self.button_prompt = BlinkingSprite(self, 500) font = pygame.font.Font(self.get_resource("terminus/Terminus.ttf"), 32) self.button_prompt.add_frame(self.get_game().orient(font.render("HOLD LEFT OR RIGHT TO PLAY", True, pygame.Color(0, 0, 0), pygame.Color(255, 255, 255)))) if not self.get_game().rotated: self.button_prompt.location.midbottom = self.get_display_surface().get_rect().midbottom else: self.button_prompt.location.midright = self.get_display_surface().get_rect().midright self.scoreboard = Scoreboard(self) self.music = Sound(self.get_resource("audio", "title")) self.advance = Sound(self.get_resource("audio", "title-advance")) self.subscribe(self.respond) def respond(self, event): if self.active: self.idle_time = 0 if not self.music.get_num_channels(): self.music.play(-1, 0, 1000) self.get_game().static.noise.fadeout(1000) if self.delegate.compare(event, "left", cancel=False) or self.delegate.compare(event, "right", cancel=False): self.holding_button = True elif self.delegate.compare(event, "left", cancel=True) or self.delegate.compare(event, "right", cancel=True): self.holding_button = False self.holding_button_elapsed = 0 def activate(self): self.active = True self.holding_button = False self.holding_button_elapsed = 0 self.idle_time = 0 self.music.play(-1) self.get_game().static.activate() self.get_game().static.full() self.get_game().static.noise.stop() self.get_game().sieve.activate() self.get_game().triangles.activate(music=False) def deactivate(self): self.active = False self.music.fadeout(500) def update(self): if self.active: if self.holding_button_elapsed > self.get_configuration("input", "title-hold"): self.deactivate() self.parent.triangles.reset() while self.parent.triangles: self.parent.triangles.pop() self.parent.triangles.activate() self.parent.sieve.activate() self.parent.static.reset() self.parent.static.activate() self.advance.play() self.get_game().trail_effect.fill(Color(0, 0, 0, 0)) self.get_display_surface().blit(self.get_game().background, (0, 0)) else: if self.idle_time > self.get_configuration("audio", "title-fade"): self.music.fadeout(5000) if not self.get_game().static.noise.get_num_channels(): self.get_game().static.noise.set_volume(0.25) self.get_game().static.noise.play(-1, 0, 5000) else: self.idle_time += self.get_game().time_filter.get_last_frame_duration() if self.holding_button: self.holding_button_elapsed += self.get_game().time_filter.get_last_frame_duration() logo = Sprite(self) font = pygame.font.Font(self.get_resource("terminus/TerminusItalic.ttf"), 62 - int(self.holding_button_elapsed / self.get_configuration("input", "title-hold") * 38)) logo.add_frame(self.get_game().orient(font.render("iBITFIT", True, pygame.Color(0, 0, 0), pygame.Color(255, 255, 255)))) if not self.get_game().rotated: logo.location.midtop = self.get_display_surface().get_rect().midtop else: logo.location.midleft = self.get_display_surface().get_rect().midleft logo.update() self.button_prompt.update() self.scoreboard.update() class Strip(Sprite): LEFT, RIGHT = range(2) def __init__(self, parent, interval): Sprite.__init__(self, parent, interval) self.deactivate() self.display_surface = self.get_display_surface() self.delegate = self.get_game().delegate if not self.get_game().rotated: self.hshifts = Shift(self, 1, "shift-2"), Shift(self, -1, "shift-2") else: self.hshifts = Shift(self, -1, "shift-2"), Shift(self, 1, "shift-2") self.add_frames() self.subscribe(self.respond) def deactivate(self): self.active = False def reset(self): for shift in self.hshifts: shift.reset() def add_frames(self): pass def respond(self, event): """ Translate input events into movement of the sieve object. This function is usually set as a callback, but it can be called directly. @param event `pygame.event.Event` with input """ if self.active: compare = self.delegate.compare if compare(event, "left") or compare(event, "left", True): self.hshifts[self.LEFT].active = not event.cancel elif compare(event, "right") or compare(event, "right", True): self.hshifts[self.RIGHT].active = not event.cancel def activate(self): self.active = True def update(self): if self.active: for shift in self.hshifts: shift.update() if shift.time: if not self.get_game().rotated: self.move(shift.get_change()) else: self.move(dy=shift.get_change()) Sprite.update(self) class Shift(GameChild): def __init__(self, parent, direction, nodeset): GameChild.__init__(self, parent) self.direction = direction self.reset() self.timer = self.get_game().time_filter self.nodeset = self.get_game().interpolator.get_nodeset(nodeset) def reset(self): self.active = False self.time = 0 def update(self): least, greatest = self.nodeset[0].x, self.nodeset[-1].x if self.active and self.time < greatest: self.time = min(self.time + self.timer.get_last_frame_duration(), greatest) elif not self.active and self.time > least: self.time = max(self.time - self.timer.get_last_frame_duration(), least) def get_change(self): return self.nodeset.get_y(self.time) * self.direction class Scoreboard(GameChild): BACKGROUND = 255, 255, 255 FOREGROUND = 27, 27, 27 NEW = 27, 27, 27 SPACING = 45 MARGIN = 0 BLINK_INTERVAL = 400 PADDING = 0 BORDER = 1 SCORE_COUNT = 11 SIZES = [32, 28, 24, 22, 22, 20, 20, 20, 18, 18, 18] def __init__(self, parent): GameChild.__init__(self, parent) ds = self.display_surface = self.get_display_surface() self.scores_path = self.get_resource("score", "path") self.most_recent_score = None self.set_scores() self.load() def set_scores(self): self.scores = [] try: with open(self.scores_path, "r") as fp: for line in fp: fields = line.split() self.scores.append((float(fields[0]), int(fields[1]), fields[2])) fp.close() except: print("Warning: error while reading scores file. Ignoring for now.") self.scores = sorted(self.scores, key=itemgetter(0)) self.scores = sorted(self.scores, key=itemgetter(1), reverse=True) def load(self): self.sprites = sprites = [] font_path = self.get_resource("display", "scoreboard-font-path") blink = False for ii, score in enumerate(self.get_scores()[:len(self.SIZES)]): font = Font(font_path, self.SIZES[ii]) sprites.append((Sprite(self, self.BLINK_INTERVAL), Sprite(self, self.BLINK_INTERVAL))) score_text = str(score[1]) color = self.BACKGROUND if (self.most_recent_score and not blink and score[1:] == self.most_recent_score) else self.FOREGROUND score_plate = font.render(score_text, False, color, self.BACKGROUND) rect = score_plate.get_rect() surface = Surface(rect.inflate((2, 2)).size) surface.fill(self.FOREGROUND) rect.center = surface.get_rect().center surface.blit(score_plate, rect) width = 80 sprites[ii][1].add_frame(self.get_game().orient( render_box(font, score_text, True, color, self.BACKGROUND, (0, 0, 0), padding=self.PADDING, width=width))) sprites[ii][0].add_frame(self.get_game().orient( render_box(font, score[2], True, color, self.BACKGROUND, (0, 0, 0), padding=self.PADDING, width=width))) if self.most_recent_score and not blink and score[1:] == self.most_recent_score: sprites[ii][1].add_frame(self.get_game().orient( render_box(font, score_text, True, self.NEW, self.BACKGROUND, (0, 0, 0), padding=self.PADDING, width=width))) sprites[ii][0].add_frame(self.get_game().orient( render_box(font, score[2], True, self.NEW, self.BACKGROUND, (0, 0, 0), padding=self.PADDING, width=width))) blink = True if not self.get_game().rotated: sprites[ii][0].location.left = self.MARGIN sprites[ii][1].location.right = self.get_display_surface().get_rect().right - self.MARGIN y = self.get_display_surface().get_rect().centery + self.SPACING * (ii - len(self.SIZES) / 2) for sprite in sprites[ii]: sprite.location.centery = y else: sprites[ii][0].location.bottom = self.get_display_surface().get_height() - self.MARGIN sprites[ii][1].location.top = self.MARGIN x = self.get_display_surface().get_rect().centerx + self.SPACING * (ii - len(self.SIZES) / 2) for sprite in sprites[ii]: sprite.location.centerx = x def get_scores(self): return self.scores def write(self, initials): score = int(round(self.get_game().triangles.score)) fields = str(time()), str(score), initials with open(self.scores_path, "a") as fp: fp.write(fields[0] + " " + fields[1] + " " + fields[2] + "\n") fp.close() self.most_recent_score = score, initials self.scores.append((float(fields[0]), int(fields[1]), fields[2])) self.scores = sorted(self.scores, key=itemgetter(0)) self.scores = sorted(self.scores, key=itemgetter(1), reverse=True) self.load() def update(self): for pair in self.sprites: for sprite in pair: sprite.update() class Sieve(Strip): UP, DOWN = range(2) def __init__(self, parent): Strip.__init__(self, parent, 400) self.delegate = self.get_game().delegate self.electric = Electric(self) if not self.get_game().rotated: self.location.left = 0 self.add_location(offset=(self.location.w, 0)) else: self.location.bottom = self.get_display_surface().get_height() self.add_location(offset=(0, -self.location.h)) def add_frames(self): bar_locations = [] self.bar_rects = bar_rects = [] x = 0 sh = 30 nodeset = self.get_game().interpolator.get_nodeset("scale") self.bar_w = bar_w = 3 self.gaps = gaps = [] while x < nodeset[-1].x: bar_locations.append(x) bar_rects.append(Rect(x, 0, bar_w, sh)) gaps.append(nodeset.get_y(x, natural=True)) x += gaps[-1] surface = Surface((x, sh)) transparent_color = (255, 0, 255) surface.fill(transparent_color) surface.set_colorkey(transparent_color) frames = surface, surface.copy() # colors = (0, 255, 0), (153, 0, 204) colors = (255, 255, 255), (255, 255, 255) for x in bar_locations: bar_rects.append(Rect(x + surface.get_width(), 0, bar_w, sh)) for ii, frame in enumerate(frames): frame.fill(colors[ii], (x, 0, bar_w, sh)) frame.fill(colors[ii - 1], (x + 1, 1, 1, sh - 2)) if self.get_game().rotated: for ii, rect in enumerate(bar_rects): bar_rects[ii] = self.get_game().orient(rect) bar_rects[ii].move_ip(0, -6) for frame in frames: self.add_frame(self.get_game().orient(frame)) def reset(self): Strip.reset(self) if not self.get_game().rotated: self.location.centerx = self.display_surface.get_rect().centerx self.locations[1].centerx = self.location.centerx + self.location.w else: self.location.centery = self.display_surface.get_rect().centery self.locations[1].centery = self.location.centery - self.location.h def update(self): if self.active: if not self.get_game().rotated: if self.location.right < 0: self.move(self.location.w) if self.locations[1].left > self.display_surface.get_width(): self.move(-self.location.w) for location in self.locations: location.bottom = self.parent.acid.get_top() self.electric.location.centery = self.location.centery + 13 else: if self.location.top > self.display_surface.get_height(): self.move(dy=-self.location.h) if self.locations[1].bottom < 0: self.move(dy=self.location.h) for location in self.locations: location.right = self.parent.acid.get_top() self.electric.location.centerx = self.location.centerx + 13 self.electric.update() for rect in self.bar_rects: if not self.get_game().rotated: rect.centery = self.location.centery else: rect.centerx = self.location.centerx Strip.update(self) class Electric(Sprite): def __init__(self, parent): Sprite.__init__(self, parent) self.display_surface = self.get_display_surface() self.add_frames() def add_frames(self): if not self.get_game().rotated: surface = Surface((self.display_surface.get_width(), self.parent.location.h - 10)) else: surface = Surface((self.display_surface.get_height(), self.parent.location.w - 10)) frames = surface, surface.copy() # colors = (255, 255, 0), (100, 89, 213) # colors = (180, 152, 111), (180, 152, 111) colors = (255, 255, 255), (255, 255, 255) pixel_arrays = PixelArray(frames[0]), PixelArray(frames[1]) for x in range(len(pixel_arrays[0])): for y in range( len(pixel_arrays[0][0])): pixel_arrays[0][x][y] = colors[(y + x) // 5 % 2] pixel_arrays[1][x][y] = colors[(y + x + 1) // 5 % 2] for pixels in pixel_arrays: del pixels for frame in frames: self.add_frame(self.get_game().orient(frame)) class Triangles(GameChild, list): def __init__(self, parent): GameChild.__init__(self, parent) self.hue = 0 self.music = Sound(self.get_resource("audio", "triangles")) self.deactivate() self.display_surface = self.get_game().trail_effect self.delegate = self.get_game().delegate self.booster = Shift(self, 1, "boost") self.hit = Sound(self.get_resource("audio", "hit")) self.miss = Sound(self.get_resource("audio", "miss")) self.reset() self.subscribe(self.respond) def deactivate(self): self.active = False self.music.fadeout(500) def reset(self): list.__init__(self, []) self.streak = 0 self.score = 0 self.booster.reset() def populate(self): if not self: self.append(Triangle(self)) if not self.get_game().rotated: self[-1].location.bottom = 0 else: self[-1].location.right = 0 self.set_next_gap() if not self.get_game().rotated: while self[-1].location.top > -self.display_surface.get_height(): self.append(Triangle(self)) self[-1].location.bottom = self[-2].location.top - self.next_gap self.set_next_gap() else: while self[-1].location.left > -self.display_surface.get_width(): self.append(Triangle(self)) self[-1].location.right = self[-2].location.left - self.next_gap self.set_next_gap() def set_next_gap(self): self.next_gap = randint(500, 800) def respond(self, event): if self.active: compare = self.delegate.compare if compare(event, "down") or compare(event, "down", True): self.booster.active = not event.cancel def get_boost(self): return self.booster.get_change() def activate(self, music=True): self.active = True if music: self.music.play(-1, 0, 500) def update(self): if self.active: self.populate() self.booster.update() if self[0].location.collidelist(self.parent.sieve.locations) != -1: sieve = self.parent.sieve removed = False if self[0].location.colliderect(sieve.electric.location): if not self.get_game().title.active: self.parent.acid.increase() self.streak += 1 self.score += self.streak ** .8 + self.parent.acid.get_volume() * 5 + self[0].count self.remove(self[0]) self.hit.play() removed = True else: for br in sieve.bar_rects: for tr in self[0].collision_rects: tr_offset = (self[0].location.left, 0) if not self.get_game().rotated else \ (0, self[0].location.bottom - self.get_display_surface().get_height()) br_offset = (sieve.location.left, 0) if not self.get_game().rotated else \ (0, sieve.location.bottom - self.get_display_surface().get_height()) if tr.move(tr_offset).colliderect(br.move(br_offset)): if not self.get_game().title.active: self.parent.static.increase() self.streak = 0 self.remove(self[0]) self.miss.play() removed = True break if removed: self.get_display_surface().blit(self.get_game().alpha_filter, (0, 0), None, pygame.BLEND_RGBA_SUB) for triangle in self: triangle.update() class Triangle(Sprite): def __init__(self, parent): Sprite.__init__(self, parent, 100) mark = randint(112, 328) sieve = self.parent.parent.sieve gaps = sieve.gaps start = randrange(0, len(gaps)) widths = [gaps[start]] while sum(widths) < mark: widths.append(gaps[(start + len(widths)) % len(gaps)]) surface = Surface((sum(widths), 20)) surface.set_colorkey((0, 0, 0)) height = surface.get_height() margin = 26 self.collision_rects = collision_rects = [] for ii, lightness in enumerate(range(30, 110, 10)): color = pygame.Color(0, 0, 0) color.hsla = parent.hue, 100, lightness, 100 # opposite_color = pygame.Color(0, 0, 0) # opposite_color.hsla = (parent.hue + 180) % 360, 100, lightness, 100 x = 0 surface = surface.copy() for width in widths: x += sieve.bar_w points = ((x + margin // 2, height - 2), (x + width - margin // 2 - 1, height - 2), (x + width / 2.0, 1)) polygon(surface, color, points) if ii == 0: if not self.get_game().rotated: collision_rects.append(Rect(points[0], (width - margin - 1, 1))) else: collision_rects.append(Rect(height - 2 - 1, self.get_display_surface().get_height() - x - width + margin // 2 + 1, 1, width - margin - 1)) # points = ((x + margin // 2 + (width * .1), height - 2 - 2), # (x + width - margin // 2 - 1 - (width * .1), height - 2 - 2), # (x + width / 2.0, 1 + 5)) # polygon(surface, opposite_color, points) x += width - sieve.bar_w self.add_frame(self.get_game().orient(surface)) next_hue = parent.hue while abs(next_hue - parent.hue) < 60: next_hue = random.randint(0, 359) parent.hue = next_hue if not self.get_game().rotated: self.location.centerx = self.get_display_surface().get_rect().centerx else: self.location.centery = self.get_display_surface().get_rect().centery self.count = len(widths) def update(self): step = 9.5 * self.get_game().acid.get_volume() + 3.8 + self.parent.get_boost() if not self.get_game().rotated: self.move(dy=step) else: self.move(dx=step) for rect in self.collision_rects: if not self.get_game().rotated: rect.bottom = self.location.bottom else: rect.right = self.location.right Sprite.update(self) class Acid(GameChild): def __init__(self, parent): GameChild.__init__(self, parent) self.display_surface = self.get_display_surface() self.level_r = 80, 320 self.nodeset = self.get_game().interpolator.get_nodeset("volume") self.reset() def reset(self): self.substance = 0 def get_top(self): if not self.get_game().rotated: return self.display_surface.get_height() - self.get_level() else: return self.display_surface.get_width() - self.get_level() def get_level(self): return self.get_volume() * (self.level_r[1] - self.level_r[0]) + self.level_r[0] def get_volume(self): return self.nodeset.get_y(self.substance) def increase(self): self.substance += 1 class Static(Sprite): def __init__(self, parent): Sprite.__init__(self, parent, 120) self.advance_automatically = 0 self.noise = Sound(self.get_resource("audio", "noise")) self.end = Sound(self.get_resource("audio", "end")) self.deactivate() self.delegate = self.get_game().delegate self.increaser = Shift(self, 1, "intensity") self.total = Total(self) self.initials = Initials(self) self.reset() self.add_frames() def deactivate(self): self.active = False self.end.fadeout(500) def reset(self): self.advance_automatically = 0 self.complete = False self.intensity = 0 self.noise.set_volume(0) self.increaser.reset() def add_frames(self): surface = Surface(self.get_display_surface().get_size()) frames = surface, surface.copy(), surface.copy(), surface.copy() tiles = [] for _ in range(32): tiles.append(Surface((16, 16))) pixel_arrays = [] for tile in tiles: pixel_arrays.append(PixelArray(tile)) colors = (0, 0, 0), (64, 64, 64), (128, 128, 128), (196, 196, 196), (255, 255, 255) for x in range(len(pixel_arrays[0])): for y in range(len(pixel_arrays[0][0])): for pixels in pixel_arrays: pixels[x][y] = choice(colors) for pixels in pixel_arrays: del pixels del pixel_arrays for frame in frames: for y in range(0, frame.get_height(), tiles[0].get_height()): for x in range(0, frame.get_width(), tiles[0].get_width()): frame.blit(choice(tiles), (x, y)) self.add_frame(frame) def finish(self, text="---", wipe=False): if wipe: self.parent.title.scoreboard.most_recent_score = None self.parent.title.scoreboard.write(text) self.total.deactivate() self.deactivate() self.reset() self.parent.acid.reset() self.parent.triangles.reset() self.parent.sieve.reset() self.parent.title.activate() def increase(self): self.intensity += self.increaser.get_change() if self.intensity > 1: self.intensity = 1 self.increaser.time += 12000 if self.increaser.time >= self.increaser.nodeset[-1].x + 5000: self.increaser.time = self.increaser.nodeset[-1].x + 5000 def full(self): self.intensity = 1 def activate(self): self.active = True self.noise.play(-1) def update(self): if self.active: if not self.get_game().title.active: if not self.complete and self.intensity >= .65: self.complete = True self.parent.sieve.deactivate() self.parent.triangles.deactivate() self.set_alpha(255) self.noise.fadeout(6000) self.end.play(-1, 0, 4000) self.total.load() elif not self.complete: self.set_alpha(min(150, int(self.intensity * 1.15 * 255))) if self.intensity > 0: self.intensity *= .998 self.increaser.update() self.noise.set_volume(self.intensity) if self.total.active: if self.advance_automatically > 3000: self.advance_automatically = 0 if self.get_game().triangles.score > self.get_game().title.scoreboard.get_scores()[Scoreboard.SCORE_COUNT - 1][1]: self.total.deactivate() self.initials.activate() self.get_game().suppress_input_temporarily() else: self.finish(wipe=True) else: self.advance_automatically += self.get_game().time_filter.get_last_frame_duration() if self.intensity > .1: Sprite.update(self) self.total.update() self.initials.update() class Initials(GameChild): LETTER_SIZE = 24 FOREGROUND = 27, 27, 27 BACKGROUND = 255, 255, 255 PADDING = 10 ARROW_MARGIN = 20 ARROW_HEIGHT = 10 def __init__(self, parent): GameChild.__init__(self, parent) self.left_last_pressed = 0 self.button_prompt = BlinkingSprite(self, 500) font = pygame.font.Font(self.get_resource("terminus/Terminus.ttf"), 32) self.button_prompt.add_frame(self.get_game().orient(font.render("HOLD RIGHT TO ENTER", True, pygame.Color(0, 0, 0), pygame.Color(255, 255, 255)))) if not self.get_game().rotated: self.button_prompt.location.midbottom = self.get_display_surface().get_rect().midbottom else: self.button_prompt.location.midright = self.get_display_surface().get_rect().midright self.reset() self.deactivate() self.font = Font(self.get_resource("display", "initials-font"), self.LETTER_SIZE) self.subscribe(self.respond) def reset(self): self.idle_time = 0 self.text = "---" self.index = 0 self.holding_button = False self.holding_button_elapsed = 0 def deactivate(self): self.active = False def respond(self, event): if self.active: self.idle_time = 0 compare = self.get_game().delegate.compare if compare(event, "left", cancel=False): self.left_last_pressed = pygame.time.get_ticks() if compare(event, "right", cancel=False): self.holding_button = True elif compare(event, "right", cancel=True): self.holding_button = False self.holding_button_elapsed = 0 if (compare(event, "right", cancel=True) and pygame.time.get_ticks() - self.left_last_pressed > 200) or compare(event, "left", cancel=True): if compare(event, "left", cancel=True): increment = -1 elif compare(event, "right", cancel=True): increment = 1 letter = self.text[self.index] if letter == '-': letter = 'A' if increment == 1 else 'Z' else: letter = chr(ord(letter) + increment) if ord(letter) == 91 or ord(letter) == 64: letter = '-' replacement = "" for ii in range(len(self.text)): if ii == self.index: replacement += letter else: replacement += self.text[ii] self.text = replacement def activate(self): self.active = True self.idle_time = 0 def submit(self): self.deactivate() self.parent.finish(self.text) self.reset() def update(self): if self.active: if self.idle_time > self.get_configuration("input", "initials-idle"): self.submit() else: self.idle_time += self.get_game().time_filter.get_last_frame_duration() ds = self.get_display_surface() self.button_prompt.update() if self.holding_button: self.holding_button_elapsed += self.get_game().time_filter.get_last_frame_duration() if self.holding_button_elapsed > self.get_configuration("input", "initials-hold"): self.index += 1 if self.index == len(self.text): self.submit() else: self.holding_button = False self.holding_button_elapsed = 0 self.get_game().suppress_input_temporarily() for ii, letter in enumerate(self.text): box = self.get_game().orient(render_box( self.font, letter, False, self.FOREGROUND, self.BACKGROUND, self.FOREGROUND, padding=self.PADDING)) rect = box.get_rect() if not self.get_game().rotated: rect.centery = ds.get_rect().centery rect.centerx = ii * ds.get_width() / 3 + ds.get_width() / 6 else: rect.centerx = ds.get_rect().centerx rect.centery = (len(self.text) - 1 - ii) * ds.get_height() / 3 + ds.get_height() / 6 ds.blit(box, rect) if ii == self.index: hold_offset = self.holding_button_elapsed / self.get_configuration("input", "initials-hold") * 10 if not self.get_game().rotated: x = rect.left - self.ARROW_MARGIN left_points = ((x, rect.top), (x, rect.bottom), (x - self.ARROW_HEIGHT, rect.centery)) x = rect.right + self.ARROW_MARGIN + hold_offset right_points = ((x, rect.top), (x, rect.bottom), (x + self.ARROW_HEIGHT, rect.centery)) else: y = rect.top - self.ARROW_MARGIN - hold_offset left_points = ((rect.left, y), (rect.right, y), (rect.centerx, y - self.ARROW_HEIGHT)) y = rect.bottom + self.ARROW_MARGIN right_points = ((rect.left, y), (rect.right, y), (rect.centerx, y + self.ARROW_HEIGHT)) pygame.draw.polygon(ds, pygame.Color(0, 0, 0), left_points) pygame.draw.polygon(ds, pygame.Color(0, 0, 0), right_points) class Total(Sprite): def __init__(self, parent): Sprite.__init__(self, parent, 68) self.deactivate() self.font = Font(self.get_resource("display", "score-font-path"), 72) # self.font.set_italic(True) def deactivate(self): self.active = False def load(self): self.clear_frames() score = "" for ch in str(int(round(self.get_game().triangles.score))): score += ch + " " colors = (255, 255, 180), (180, 255, 255), (255, 180, 255), \ (255, 220, 160), (160, 255, 220), (220, 160, 255) template = Surface((self.display_surface.get_width(), 100)) transparent_color = (255, 0, 255) template.fill(transparent_color) template.set_colorkey(transparent_color) tr = template.get_rect() template.fill((255, 0, 0), (0, 20, tr.w, 1)) template.fill((255, 128, 128), (0, 21, tr.w, 1)) for y in range(22, 78, 2): template.fill((255, 255, 255), (0, y, tr.w, 1)) template.fill((255, 128, 128), (0, 78, tr.w, 1)) template.fill((255, 0, 0), (0, 79, tr.w, 1)) for _ in range(20): # surface = template.copy() surface = Surface(template.get_size(), SRCALPHA) # polygon(surface, choice(colors), ((tr.centerx - 7, 19), # (tr.centerx, 0), # (tr.centerx + 7, 19))) text = self.font.render(score, True, choice(colors)) rect = text.get_rect() rect.center = tr.centerx, tr.centery + 2 surface.blit(text, rect) # polygon(surface, choice(colors), ((tr.centerx - 7, 80), # (tr.centerx, tr.h - 1), # (tr.centerx + 7, 80))) self.add_frame(self.get_game().orient(surface)) self.location.center = self.display_surface.get_rect().center self.active = True def update(self): if self.active: Sprite.update(self)