ibitfit/electric_sieve/ElectricSieve.py

749 lines
27 KiB
Python

# -*- coding: utf-8 -*-
from random import randint, randrange, choice
from time import time
from operator import itemgetter
import RPi.GPIO as GPIO
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
from lib.pgfw.pgfw.extension import render_box
class ElectricSieve(Game):
# The GPIO pins corresponding to the buttons and LED indicators
PIN_BUTTON_UP = 17
PIN_BUTTON_DOWN = 27
PIN_LED_UP = 22
PIN_LED_DOWN = 23
def __init__(self):
self.initialize_gpio()
Game.__init__(self)
self.background = Surface(self.display.screen.get_size())
self.background.fill((255, 80, 190))
self.title.activate()
def initialize_gpio(self):
"""
Set pin numbering mode to GPIO, initialize all buttons to input pullup.
"""
# Use GPIO numbering
GPIO.setmode(GPIO.BCM)
# Set all button pins to pullup
for pin in self.PIN_BUTTON_UP, self.PIN_BUTTON_DOWN:
GPIO.setup(pin, GPIO.IN, pull_up_down=GPIO.PUD_UP)
def set_children(self):
Game.set_children(self)
self.title = Title(self)
self.sieve = Sieve(self)
self.triangles = Triangles(self)
self.acid = Acid(self)
self.static = Static(self)
def update(self):
self.title.update()
if self.triangles.active:
self.display.screen.blit(self.background, (0, 0))
self.triangles.update()
self.sieve.update()
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)
self.background = surface = Surface(self.display_surface.get_size())
tile = Surface((2, 2))
tile.fill(bg_color)
tile.set_at((0, 1), (220, 119, 41))
tile.set_at((1, 0), (220, 119, 41))
for y in range(0, surface.get_height(), 2):
for x in range(0, surface.get_width(), 2):
surface.blit(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.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:
if self.delegate.compare(event, "advance"):
self.deactivate()
self.parent.triangles.activate()
self.parent.sieve.activate()
self.parent.static.activate()
self.advance.play()
def activate(self):
self.active = True
self.music.play(-1)
def deactivate(self):
self.active = False
self.music.fadeout(500)
def update(self):
if self.active:
self.display_surface.blit(self.background, (0, 0))
# for caption in self.captions:
# caption.update()
self.scoreboard.update()
class Strip(Sprite):
LEFT, RIGHT = range(2)
def __init__(self, parent):
Sprite.__init__(self, parent)
GPIO.add_event_detect(ElectricSieve.PIN_BUTTON_UP, GPIO.BOTH, self.respond)
GPIO.add_event_detect(ElectricSieve.PIN_BUTTON_DOWN, GPIO.BOTH, self.respond)
self.deactivate()
self.display_surface = self.get_display_surface()
self.delegate = self.get_game().delegate
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):
if type(event) == int:
pressed = "pressed" if GPIO.input(event) == GPIO.LOW else "released"
print(f"pin {event} {pressed}")
if self.active:
move = False
if type(event) == int:
if event == ElectricSieve.PIN_BUTTON_UP:
direction = self.LEFT
move = True
elif event == ElectricSieve.PIN_BUTTON_DOWN:
direction = self.RIGHT
move = True
active = GPIO.input(event) == GPIO.LOW
else:
compare = self.delegate.compare
if compare(event, "left") or compare(event, "left", True):
direction = self.LEFT
move = True
elif compare(event, "right") or compare(event, "right", True):
direction = self.RIGHT
move = True
active = not event.cancel
if move:
self.hshifts[direction].active = active
def activate(self):
self.active = True
def update(self):
if self.active:
for shift in self.hshifts:
shift.update()
if shift.time:
self.move(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 = 70
MARGIN = 30
BLINK_INTERVAL = 400
PADDING = 6
BORDER = 1
SCORE_COUNT = 9
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.load()
def load(self):
self.sprites = sprites = []
font_path = self.get_resource("display", "scoreboard-font-path")
sizes = [24] * self.SCORE_COUNT
blink = False
for ii, score in enumerate(self.get_scores()[:len(sizes)]):
font = Font(font_path, 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)
sprites[ii][1].add_frame(render_box(font, score_text, False, color,
self.BACKGROUND, self.FOREGROUND, self.BORDER, self.PADDING))
sprites[ii][0].add_frame(render_box(font, score[2], False, color,
self.BACKGROUND, self.FOREGROUND, self.BORDER, self.PADDING))
if self.most_recent_score and not blink and score[1:] == self.most_recent_score:
sprites[ii][1].add_frame(render_box(font, score_text, False, self.NEW,
self.BACKGROUND, self.FOREGROUND, self.BORDER, self.PADDING))
sprites[ii][0].add_frame(render_box(font, score[2], False, self.NEW,
self.BACKGROUND, self.FOREGROUND, self.BORDER, self.PADDING))
blink = True
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(sizes) / 2)
for sprite in sprites[ii]:
sprite.location.centery = y
def get_scores(self):
scores = []
for line in open(self.scores_path, "r"):
fields = line.split()
scores.append((float(fields[0]), int(fields[1]), fields[2]))
scores = sorted(scores, key=itemgetter(0))
return sorted(scores, key=itemgetter(1), reverse=True)
def write(self, initials):
score = int(round(self.get_game().triangles.score))
fields = str(time()), str(score), initials
open(self.scores_path, "a").write(fields[0] + " " + fields[1] + " " + fields[2] + "\n")
self.most_recent_score = score, initials
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)
self.delegate = self.get_game().delegate
self.electric = Electric(self)
self.add_location(offset=(self.location.w, 0))
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)
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))
for frame in frames:
self.add_frame(frame)
def reset(self):
Strip.reset(self)
self.location.centerx = self.display_surface.get_rect().centerx
self.locations[1].centerx = self.location.centerx + self.location.w
def update(self):
if self.active:
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
self.electric.update()
for rect in self.bar_rects:
rect.centery = self.location.centery
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):
surface = Surface((self.display_surface.get_width(),
self.parent.location.h - 10))
frames = surface, surface.copy()
colors = (255, 255, 0), (100, 89, 213)
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) % 2]
pixel_arrays[1][x][y] = colors[(y + x + 1) % 2]
for pixels in pixel_arrays:
del pixels
for frame in frames:
self.add_frame(frame)
class Triangles(GameChild, list):
def __init__(self, parent):
GameChild.__init__(self, parent)
self.music = Sound(self.get_resource("audio", "triangles"))
self.deactivate()
self.display_surface = self.get_display_surface()
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))
self[-1].location.bottom = 0
self.set_next_gap()
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()
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):
self.active = True
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
if self[0].location.colliderect(sieve.electric.location):
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()
else:
for br in sieve.bar_rects:
for tr in self[0].collision_rects:
if tr.move((self[0].location.left,
0)).colliderect(br.move((sieve.location.left,
0))):
self.remove(self[0])
self.parent.static.increase()
self.streak = 0
self.miss.play()
break
for triangle in self:
triangle.update()
class Triangle(Sprite):
def __init__(self, parent):
Sprite.__init__(self, parent)
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))
x = 0
height = surface.get_height()
margin = 26
self.collision_rects = collision_rects = []
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, (60, 255, 220), points)
collision_rects.append(Rect(points[0], (width - margin - 1, 1)))
x += width - sieve.bar_w
self.add_frame(surface)
self.location.centerx = self.get_display_surface().get_rect().centerx
self.count = len(widths)
def update(self):
self.move(dy=9.5 * self.get_game().acid.get_volume() + 3.8 + \
self.parent.get_boost())
for rect in self.collision_rects:
rect.bottom = self.location.bottom
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):
return self.display_surface.get_height() - 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.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()
self.subscribe(self.respond)
def deactivate(self):
self.active = False
self.end.fadeout(500)
def reset(self):
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 respond(self, event):
if self.active and self.complete and not self.initials.active:
if self.delegate.compare(event, "advance"):
if self.get_game().triangles.score > \
self.get_game().title.scoreboard.get_scores()[Scoreboard.SCORE_COUNT - 1][1]:
self.total.deactivate()
self.initials.activate()
else:
self.finish(wipe=True)
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 activate(self):
self.active = True
self.noise.play(-1)
def update(self):
if self.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(int(self.intensity * 255))
if self.intensity > 0:
self.intensity *= .998
self.increaser.update()
self.noise.set_volume(self.intensity)
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 = 40
ARROW_HEIGHT = 10
def __init__(self, parent):
GameChild.__init__(self, parent)
self.reset()
self.deactivate()
self.font = Font(self.get_resource("display", "initials-font"), self.LETTER_SIZE)
self.subscribe(self.respond)
def reset(self):
self.text = "---"
self.index = 0
def deactivate(self):
self.active = False
def respond(self, event):
if self.active:
compare = self.get_game().delegate.compare
if compare(event, "right") or compare(event, "advance"):
self.index += 1
if self.index == len(self.text):
self.index = 0
if compare(event, "advance"):
self.deactivate()
self.parent.finish(self.text)
elif compare(event, "left"):
self.index -= 1
if self.index == -1:
self.index = len(self.text) - 1
elif compare(event, "up") or compare(event, "down"):
if compare(event, "up"):
increment = 1
elif compare(event, "down"):
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
def update(self):
if self.active:
ds = self.get_display_surface()
for ii, letter in enumerate(self.text):
box = render_box(self.font, letter, False, self.FOREGROUND, self.BACKGROUND,
self.FOREGROUND, padding=self.PADDING)
rect = box.get_rect()
rect.centery = ds.get_rect().centery
rect.centerx = ii * ds.get_width() / 3 + ds.get_width() / 6
ds.blit(box, rect)
if ii == self.index:
margin = self.ARROW_MARGIN
polygon(ds, (0, 255, 0), ((rect.left, rect.top - margin),
(rect.right, rect.top - margin),
(rect.centerx,
rect.top - margin - self.ARROW_HEIGHT)))
polygon(ds, (0, 255, 0), ((rect.left, rect.bottom + margin),
(rect.right, rect.bottom + margin),
(rect.centerx,
rect.bottom + margin + self.ARROW_HEIGHT)))
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(surface)
self.location.center = self.display_surface.get_rect().center
self.active = True
def update(self):
if self.active:
Sprite.update(self)