diff --git a/NS.py b/NS.py index afef657..53baf7d 100644 --- a/NS.py +++ b/NS.py @@ -1,13 +1,12 @@ # -*- coding: utf-8 -*- +import argparse from random import randint, choice, random from math import pi from copy import copy from glob import iglob from os.path import basename, join from threading import Thread -from serial import Serial, SerialException -from serial.tools import list_ports from time import sleep from PIL import Image @@ -48,8 +47,22 @@ class NS(Game, Animation): NO_RESET_TIMEOUT = 3000 def __init__(self): + # Specify possible arguments and parse the command line. If the -h flag is passed, the argparse library will + # print a help message and end the program. + parser = argparse.ArgumentParser() + parser.add_argument("--minimize-load-time", action="store_true") + parser.add_argument("--serial-port") + parser.add_argument("--audio-buffer-size", type=int, default=1024) + parser.add_argument("--list-serial-ports", action="store_true") + parser.add_argument("--no-serial", action="store_true") + parser.add_argument("--show-config", action="store_true") + arguments = parser.parse_known_args()[0] + # Pre-initialize the mixer to use the specified buffer size in bytes. The default is set to 1024 to prevent lagging + # on the Raspberry Pi. pygame.mixer.pre_init(44100, -16, 2, 1024) + # Pygame will be loaded in here. Game.__init__(self) + # Add type declarations for non-string config name/value pairs that aren't in the default PGFW config dict. self.get_configuration().type_declarations.add_chart( { "time": @@ -63,15 +76,57 @@ class NS(Game, Animation): }, "display": { - "float": "attract-gif-alpha" + "float": "attract-gif-alpha", + "bool": "effects" + }, + "system": + { + "bool": "minimize-load-time" } }) + # If a serial port was passed on the command line, override the config file setting + if arguments.serial_port is not None: + self.get_configuration().set("input", "arduino-port", arguments.serial_port) + # Command line flag requesting minimal load time overrides config file setting + if arguments.minimize_load_time: + self.get_configuration().set("system", "minimize-load-time", True) + # Turn off effects if minimal load time is requested. Minimal load time setting overrides display effects setting. + if self.get_configuration("system", "minimize-load-time"): + self.get_configuration().set("display", "effects", False) + # Apply the no serial flag from the command line if requested + if arguments.no_serial: + self.get_configuration().set("input", "serial", False) + # Print the configuration if requested on the command line + if arguments.show_config: + print(self.get_configuration()) + # Initialize the serial reader and launch a thread for reading from the serial port + if self.serial_enabled(): + from serial import Serial, SerialException + from serial.tools import list_ports + # If a list of serial ports was requested, print detected ports and exit. + if arguments.list_serial_ports: + for port in list_ports.comports(): + print(f"Detected serial port: {port.device}") + exit() + # Open the port specified by the configuration or command line if it is found. If the specified port is not found, + # open the first found serial port. If no serial ports are found, raise an exception. + requested_port = self.get_configuration("input", "arduino-port") + devices = [port.device for port in list_ports.comports()] + if requested_port in devices: + self.serial_reader = Serial(requested_port, timeout=.3) + elif devices: + self.serial_reader = Serial(devices[0], timeout=.3) + else: + raise SerialException("No serial port devices were detected. Use --no-serial for keyboard-only mode.") + self.serial_kill = False + self.serial_data = 0 + self.reset_arduino() + self.serial_thread = Thread(target=self.read_serial) + self.serial_thread.start() Animation.__init__(self, self) self.subscribe(self.respond, KEYDOWN) self.subscribe(self.respond, KEYUP) self.subscribe(self.respond) - # for bgm in self.audio.bgm.values(): - # bgm.volume = 0.65 ds = self.get_display_surface() self.background = Surface(ds.get_size()) self.background.fill((0, 0, 0)) @@ -79,26 +134,11 @@ class NS(Game, Animation): self.tony = Tony(self) self.logo = Logo(self) self.title = Title(self) - self.introduction = Introduction(self) self.ending = Ending(self) self.wipe = Wipe(self) self.dialogue = Dialogue(self) self.chemtrails = Chemtrails(self) self.boss = Boss(self) - if self.serial_enabled(): - self.serial_kill = False - self.serial_data = 0 - self.serial_reader = Serial(self.get_configuration("input", "arduino-port"), - timeout=.3) - self.reset_arduino() - # for port in list_ports.comports(): - # print port.device - # print "---" - # ports = list_ports.grep(self.get_configuration("input", "arduino-port")) - # for port in ports: - # print port.device - self.serial_thread = Thread(target=self.read_serial) - self.serial_thread.start() self.last_press = get_ticks() self.register(self.blink_score, interval=500) self.play(self.blink_score) @@ -107,7 +147,7 @@ class NS(Game, Animation): clear() def serial_enabled(self): - return self.get_configuration("input", "serial") and not self.check_command_line("-no-serial") + return self.get_configuration("input", "serial") def read_serial(self): while not self.serial_kill: @@ -154,7 +194,7 @@ class NS(Game, Animation): def apply_serial(self): for ii, light in enumerate(self.platform.lights): light.pressed = bool(self.serial_data & (2 ** ii)) - # reset idle timer is a light is detected as pressed in serial data + # reset idle timer if a light is detected as pressed in serial data if light.pressed: self.idle_elapsed = 0 @@ -165,7 +205,6 @@ class NS(Game, Animation): self.title.reset() if not leave_wipe_running: self.wipe.reset() - self.introduction.reset() self.ending.reset() self.boss.reset() self.chemtrails.reset() @@ -188,6 +227,18 @@ class NS(Game, Animation): self.suppressing_input = False def respond(self, event): + """ + Respond to keyboard input. + + ___ ___ + | O| P| These keyboard keys correspond to the floor pads. + |___|___| (O = top left pad, P = top right pad, L = bottom left pad, ; = bottom right pad) + | L| ;| Arrow keys can also be used. + |___|___| (UP = top left pad, RIGHT = top right pad, DOWN = bottom left pad, LEFT = bottom right pad) + + The Z key is a shortcut for reset (F8 also resets). + The A key force resets the connected Arduino (or does nothing if no Arduino is connected). + """ if not self.suppressing_input and event.type in (KEYDOWN, KEYUP): if self.last_press <= get_ticks() - int(self.get_configuration("input", "buffer")): pressed = True if event.type == KEYDOWN else False @@ -215,8 +266,7 @@ class NS(Game, Animation): last_frame_duration = self.time_filter.get_last_frame_duration() if self.serial_enabled(): self.apply_serial() - if self.title.active or self.introduction.active or self.ending.active or \ - self.dialogue.active: + if self.title.active or self.ending.active or self.dialogue.active: self.no_reset_elapsed += last_frame_duration # if we received good input, reset the auto reset timer if 0b11 <= self.serial_data <= 0b1100: @@ -226,7 +276,6 @@ class NS(Game, Animation): self.reset_arduino() self.no_reset_elapsed = 0 self.title.update() - self.introduction.update() self.ending.update() self.boss.update() if not self.title.active: @@ -337,18 +386,19 @@ class Tony(Sprite): self.board.load_from_path(self.get_resource("newTony/TonyArms"), True) self.effect = Sprite(self) dsr = self.get_display_surface().get_rect() - for offset in range(12): - w, h = dsr.w + 40, int(dsr.h * .65) - glow = Surface((w, h), SRCALPHA) - for ii, y in enumerate(range(h, 0, -8)): - hue = range(240, 200, -2)[(ii - offset) % 12] - alpha = min(100, int(round(y / float(h - 10) * 100))) - color = get_hsla_color(hue, 100, 50, alpha) - if ii == 0: - aaellipse(glow, w // 2, y, w // 2 - 4, h // 20, color) - ellipse(glow, w // 2, y, w // 2 - 4, h // 20, color) - filled_ellipse(glow, w // 2, y, w // 2 - 4, h // 20, color) - self.effect.add_frame(glow) + if self.get_configuration("display", "effects"): + for offset in range(12): + w, h = dsr.w + 40, int(dsr.h * .65) + glow = Surface((w, h), SRCALPHA) + for ii, y in enumerate(range(h, 0, -8)): + hue = range(240, 200, -2)[(ii - offset) % 12] + alpha = min(100, int(round(y / float(h - 10) * 100))) + color = get_hsla_color(hue, 100, 50, alpha) + if ii == 0: + aaellipse(glow, w // 2, y, w // 2 - 4, h // 20, color) + ellipse(glow, w // 2, y, w // 2 - 4, h // 20, color) + filled_ellipse(glow, w // 2, y, w // 2 - 4, h // 20, color) + self.effect.add_frame(glow) self.effect.location.topleft = -20, int(dsr.h * .35) self.add_frame(load(self.get_resource("Big_Tony.png")).convert_alpha()) self.load_from_path(self.get_resource("newTony/TonyShirtHead"), True) @@ -416,7 +466,8 @@ class Video(Sprite): filled_circle(self.mask, rect.centerx, rect.centery, rect.centerx, (0, 0, 0, alpha)) filled_circle(self.mask, rect.centerx, rect.centery, rect.centerx - 2, (255, 255, 255, alpha)) self.add_frame(self.mask) - self.play() + if not self.get_configuration("system", "minimize-load-time"): + self.play() def shift_frame(self): Sprite.shift_frame(self) @@ -430,17 +481,14 @@ class Video(Sprite): frame = smoothscale( fromstring(self.gif.convert("RGBA").tobytes(), self.gif.size, "RGBA"), (self.mask.get_width(), int(self.gif.width * self.gif.height / self.mask.get_width()))) -# frame = scale( -# fromstring(self.gif.convert("RGBA").tobytes(), self.gif.size, "RGBA"), -# (self.mask.get_width(), int(self.gif.width * self.gif.height / self.mask.get_width()))) copy = self.mask.copy() rect = frame.get_rect() rect.bottom = copy.get_rect().bottom copy.blit(frame, rect, None, BLEND_RGBA_MIN) - # copy.blit(frame, rect) self.clear_frames() self.add_frame(copy) + class Logo(Sprite): def __init__(self, parent): @@ -708,180 +756,6 @@ class Dialogue(Animation): message.update() -class Introduction(Animation): - - TEXT = ( - "Hey, you lizard slime bag. It's me Giant Tony. " + \ - "Do you think you\ncan skate like me? Prove it!", - "I'll even give you my board for this adventure. And ink my name\n" + \ - "on it. Now the power of Giant Tony pulses through you.", - # "Before you go, show me you can scrape! Use your board to touch\n" + \ - "Before you play, show me you can scrape! Use your board to touch\n" + \ - "the glowing pads on the platform!", \ - # "Good job, lizard scum! Maybe now you're ready to take on Kool\n" + \ - "Good job, slime bag! Maybe now you're ready to take on Kool\n" + \ - "Man and his friends. Don't let me down!") - SKATEBOARD_START = -30, -20 - TUTORIAL_MOVES = NS.S, NS.NE, NS.N, NS.E - - def __init__(self, parent): - Animation.__init__(self, parent) - self.words = [] - for word in "hey you lizard slime bag show me you can scrape".split(" "): - font = Font(self.get_resource(Dialogue.FONT_PATH), 96) - sprite = RainbowSprite(self, font.render(word, True, (255, 0, 0)).convert_alpha(), 30) - self.words.append(sprite) - self.skateboard = Sprite(self) - self.skateboard.load_from_path(self.get_resource("Introduction_skateboard.png"), True) - self.slime_bag = Sprite(self) - self.slime_bag.load_from_path(self.get_resource("Introduction_slime_bag.png"), True) - self.slime_bag.load_from_path(self.get_resource("Introduction_slime_bag_board.png"), True) - self.slime_bag.add_frameset([0], name="standing", switch=True) - self.slime_bag.add_frameset([1], name="board") - self.slime_bag.location.center = self.get_display_surface().get_rect().center - self.tony_avatar = load(self.get_resource("Introduction_tony_avatar.png")).convert() - self.advance_prompt = AdvancePrompt(self) - self.skip_prompt = SkipPrompt(self, self.start_wipe) - self.register(self.start, self.move_board, self.take_board, self.speak) - - def reset(self): - self.deactivate() - self.slime_bag.set_frameset("standing") - self.slime_bag.unhide() - self.halt() - self.skateboard.hide() - self.text_index = 0 - self.tutorial_index = 0 - self.words_index = 0 - self.advance_prompt.reset() - self.skip_prompt.reset() - for word in self.words: - word.location.center = self.get_display_surface().get_rect().centerx, 100 - word.hide() - - def deactivate(self): - self.active = False - - def activate(self): - self.active = True - self.play(self.start, delay=3000, play_once=True) - self.words[0].unhide() - self.play(self.speak) - # self.get_game().platform.unpress() - - def speak(self): - for ii in range(self.words_index + 1): - self.words[ii].move(0, 12) - if ii == self.words_index and self.words[ii].location.bottom > self.get_display_surface().get_rect().bottom - 40: - if self.words_index < len(self.words) - 1: - self.words_index += 1 - self.words[self.words_index].unhide() - self.get_audio().play_sfx("talk") - - def start(self): - self.advance_prompt.cancel_first_press() - dialogue = self.get_game().dialogue - dialogue.activate() - dialogue.set_avatar(self.tony_avatar) - dialogue.set_name("???") - # dialogue.show_text(self.TEXT[0]) - dialogue.show_text(self.TEXT[2]) - self.text_index = 0 - # temporary dialogue skip - dialogue.set_name("Tony") - self.slime_bag.hide() - self.halt(self.move_board) - self.take_board() - platform = self.get_game().platform - platform.activate() - platform.set_glowing(platform.get_buttons_from_edges( - [self.TUTORIAL_MOVES[self.tutorial_index]])) - self.get_game().chemtrails.activate() - self.text_index = 2 - - def give_board(self): - self.skateboard.location.center = self.SKATEBOARD_START - self.skateboard_step = get_step(self.skateboard.location.center, self.slime_bag.location.center, 2) - self.skateboard.unhide() - self.play(self.move_board) - - def move_board(self): - self.skateboard.move(*self.skateboard_step) - if self.skateboard.location.colliderect(self.slime_bag.location.inflate(-30, -30)): - self.halt(self.move_board) - self.play(self.take_board, delay=2000, play_once=True) - self.get_audio().play_sfx("go") - - def take_board(self): - self.skateboard.hide() - self.slime_bag.set_frameset("board") - - def activate_boss(self): - self.deactivate() - self.get_game().boss.start_level(0) - - def start_wipe(self): - self.get_game().wipe.start(self.activate_boss) - - def update(self): - if self.active: - Animation.update(self) - # dialogue = self.get_game().dialogue - wipe = self.get_game().wipe - if not wipe.is_playing() and not self.is_playing(self.start) and not self.text_index == 2: - if self.advance_prompt.check_first_press(): - self.advance_prompt.press_first() - elif self.advance_prompt.check_second_press(): - if dialogue.is_playing(): - dialogue.show_all() - else: - if self.text_index < len(self.TEXT) - 1: - self.text_index += 1 - if self.text_index == 1: - dialogue.set_name("Tony") - self.give_board() - elif self.text_index == 2: - self.slime_bag.hide() - self.halt(self.move_board) - self.take_board() - platform = self.get_game().platform - platform.activate() - platform.set_glowing(platform.get_buttons_from_edges( - [self.TUTORIAL_MOVES[self.tutorial_index]])) - self.get_game().chemtrails.activate() - dialogue.show_text(self.TEXT[self.text_index]) - else: - self.start_wipe() - # self.get_game().platform.unpress() - self.advance_prompt.cancel_first_press() - elif not wipe.is_playing() and self.text_index == 2: - platform = self.get_game().platform - if platform.get_edge_pressed() == self.TUTORIAL_MOVES[self.tutorial_index]: - self.tutorial_index += 1 - self.get_audio().play_sfx("land_0") - if self.tutorial_index == len(self.TUTORIAL_MOVES): - # self.text_index += 1 - # self.advance_prompt.cancel_first_press() - platform.set_glowing([]) - self.start_wipe() - # dialogue.show_text(self.TEXT[self.text_index]) - else: - platform.set_glowing(platform.get_buttons_from_edges( - [self.TUTORIAL_MOVES[self.tutorial_index]])) - self.get_game().tony.update() - self.slime_bag.update() - self.skateboard.update() - for word in self.words: - word.update() - self.get_game().platform.update() - # self.get_game().dialogue.update() - # if not wipe.is_playing() and not self.is_playing(self.start) and \ - # not self.text_index == 2: - # self.advance_prompt.update() - if not wipe.is_playing() and not self.text_index == 2: - self.skip_prompt.update() - - class SkipPrompt(GameChild): def __init__(self, parent, callback): @@ -1469,9 +1343,17 @@ class Boss(Animation): def __init__(self, parent): Animation.__init__(self, parent) - self.kool_man = RainbowSprite(self, load(self.get_resource("Kool_man_waah.png")).convert_alpha(), 30) - self.visitor = RainbowSprite(self, load(self.get_resource("Visitor.png")).convert_alpha(), 30) - self.spoopy = RainbowSprite(self, load(self.get_resource("Spoopy.png")).convert_alpha(), 30) + if self.get_configuration("display", "effects"): + self.kool_man = RainbowSprite(self, load(self.get_resource("Kool_man_waah.png")).convert_alpha(), hue_shift) + self.visitor = RainbowSprite(self, load(self.get_resource("Visitor.png")).convert_alpha(), hue_shift) + self.spoopy = RainbowSprite(self, load(self.get_resource("Spoopy.png")).convert_alpha(), hue_shift) + else: + self.kool_man = Sprite(self) + self.kool_man.load_from_path("Kool_man_waah.png", True) + self.visitor = Sprite(self) + self.visitor.load_from_path("Visitor.png", True) + self.spoopy = Sprite(self) + self.spoopy.load_from_path("Spoopy.png", True) for sprite in self.kool_man, self.visitor, self.spoopy: sprite.location.topleft = 100, 0 self.health = Health(self) @@ -1925,7 +1807,10 @@ class Sword(Animation): swords = self.swords = [] for root in "Sword_kool_man/", "Sword_visitor/", "Sword_spoopy/": swords.append([[], [], [], [], [], []]) - for path in sorted(iglob(join(self.get_resource(root), "*.png"))): + base_image_paths = sorted(iglob(join(self.get_resource(root), "*.png"))) + if not self.get_configuration("display", "effects"): + base_image_paths = [base_image_paths[0]] + for path in base_image_paths: base = load(self.get_resource(path)).convert_alpha() for position in range(6): if position == NS.N or position == NS.S: diff --git a/README b/README index cb2353e..3d9301e 100644 --- a/README +++ b/README @@ -1,5 +1,78 @@ -Requires pyserial (https://pypi.org/project/pyserial/) +Scrapeboard is an arcade game in development by Frank DeMarco (@diskmem) and Blake Andrews (@snakesandrews) -If you have Python Package Installer, you can run +It requires a custom pad and board to play. To learn more about the project visit https://scrape.nugget.fun -pip install pyserial +This repository can be used to run either the full arcade version or the keyboard-only mode for testing + +################ +# REQUIREMENTS # +################ + +The game requires Python and Pygame. The Python version used for development is Python 3.9. The Pygame version +is 1.9.6. + +To install python with pip: + + pip install pygame + +Once Python and Pygame are installed, you should be able to run either: + + ./OPEN-GAME + +or + + ./OPEN-GAME --no-serial + +to start the game in either full arcade mode or keyboard only mode. See below for more about serial input and +keyboard input modes. + +########## +# SERIAL # +########## + +To run the game using the custom skateboard and dance pads, the Arduino attached to the pads must be plugged +into USB, and the pyserial package must be installed on this computer (https://pypi.org/project/pyserial/) + +If you have Python Package Installer, you can run this to install pyserial: + + pip install pyserial + +The Arduino must be loaded with the program at serial/serial2/serial2.ino and connected to USB. The game +will try to detect the Arduino, but to specify a specific port you can use the config file or command +line. + +If you don't have the board, pad and Arduino, you can test the game using keyboard-only mode. + +######## +# KEYS # +######## + +For testing, there is keyboard input. To run in keyboard only mode use: + + ./OPEN-GAME --no-serial + +The O, P, L, and ; keys simulate the dance pads and your fingers simulate the board + + ___ ___ +| O| P| <-- These keyboard keys correspond to the floor pads +|___|___| O = top left pad, P = top right pad, L = bottom left pad, ; = bottom right pad + | L| ;| + |___|___| or you can use arrow keys + UP = top left pad, RIGHT = top right pad, DOWN = bottom left pad, LEFT = bottom right pad + +Other keys: + +The Z key is a shortcut for reset (F8 also resets). +The A key force resets the connected Arduino (or does nothing if no Arduino is connected). + +########### +# OPTIONS # +########### + +The full list of configurable values is in the file called `config`. There are also command line flags that +can override config values: + + ./OPEN-GAME -h + +The --minimize-load-time flag can be useful when testing because it sacrifices some effects to load the game +quickly. diff --git a/config b/config index 53f6c7a..1b4b33d 100644 --- a/config +++ b/config @@ -1,7 +1,7 @@ [setup] license = Public Domain title = Scrapeboard -url = http://shampoo.ooo/games/esb +url = https://scrape.nugget.fun version = 0.2.3 init-script = OPEN-GAME additional-packages = lib @@ -9,13 +9,18 @@ data-exclude = local/, *.pyc, .git*, README, build/, dist/, *.egg-info, *.py, MA [display] caption = Scrapeboard -show-framerate = False +show-framerate = no dimensions = 640, 480 -fullscreen = False +fullscreen = no attract-gif-alpha = 0.95 +effects = yes + +[system] +# will force set display->effects to off +minimize-load-time = yes [mouse] -visible = False +visible = no [keys] quit = K_ESCAPE @@ -23,12 +28,12 @@ up = K_u [audio] sfx-volume = 0.8 -panel-enabled = True +panel-enabled = yes volume = 1.0 [input] buffer = 0 -arduino-port = /dev/ttyACM1 +arduino-port = /dev/ttyACM0 serial = True [time] @@ -40,11 +45,11 @@ attract-gif-length = 10000 attract-board-length = 3600 [bgm] -title = resource/bgm/title.ogg, 1.00 -level_0 = /home/frank/storage/audio/bgm/bat-tree-habitat-key/level-0.wav, 1.00 -level_1 = /home/frank/storage/audio/bgm/esp-hadouken/Cube-Levers.ogg, 1.00 -level_2 = /home/frank/storage/audio/bgm/esp-hadouken/Bog.ogg, 1.00 -end = /home/frank/storage/audio/bgm/phone-call-from-dark-magnet.wav, 1.00 +title = resource/bgm/title.ogg, .65 +level_0 = /home/frank/storage/audio/bgm/bat-tree-habitat-key/level-0.wav, .65 +level_1 = /home/frank/storage/audio/bgm/esp-hadouken/Cube-Levers.ogg, .65 +level_2 = /home/frank/storage/audio/bgm/esp-hadouken/Bog.ogg, .65 +end = /home/frank/storage/audio/bgm/phone-call-from-magnet.wav, .65 [pads] nw_color = #00FF88