- preliminary support for listening for messages, posting messages, and displaying status of network
- make title screen scores optimization optional - add option to clear pop up messages queue when adding a message
This commit is contained in:
parent
9b9277bedb
commit
2fe057a351
253
NS.py
253
NS.py
|
@ -10,7 +10,7 @@
|
|||
#
|
||||
# This is the main file containing all the Pygame code.
|
||||
|
||||
import argparse, pathlib, operator, subprocess, sys, os
|
||||
import argparse, pathlib, operator, subprocess, sys, os, socket, select, time
|
||||
|
||||
# Auto-detect GPIO library
|
||||
try:
|
||||
|
@ -28,7 +28,7 @@ from time import sleep
|
|||
from PIL import Image
|
||||
|
||||
import pygame
|
||||
from pygame import Surface, Color, mixer
|
||||
from pygame import Surface, mixer
|
||||
from pygame.event import clear
|
||||
from pygame.mixer import Sound
|
||||
from pygame.image import load, fromstring
|
||||
|
@ -145,6 +145,20 @@ class NS(Game, Animation):
|
|||
else:
|
||||
return self.level_index < other.level_index
|
||||
|
||||
class Peer:
|
||||
"""
|
||||
Scrapeboard game on the local area network. It is expected to be sending and receiving messages using socket
|
||||
communication. It will be read and written to regularly in a separate thread.
|
||||
"""
|
||||
status = None
|
||||
result = None
|
||||
versus = False
|
||||
level = None
|
||||
|
||||
def __init__(self, address, port):
|
||||
self.address = address
|
||||
self.port = port
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
Parse the command line, set config types, initialize the serial reader, subscribe to events, and initialize child objects.
|
||||
|
@ -194,6 +208,18 @@ class NS(Game, Animation):
|
|||
"cooldown-level-3", "first-combo-delay"
|
||||
]
|
||||
},
|
||||
"network":
|
||||
{
|
||||
"int": ["port", "diagnostics-size"],
|
||||
"bool": "diagnostics",
|
||||
"path": "diagnostics-font",
|
||||
"list": "peers"
|
||||
},
|
||||
"pop-up":
|
||||
{
|
||||
"int": ["size", "length"],
|
||||
"bool": "center"
|
||||
},
|
||||
"input":
|
||||
{
|
||||
"bool": ["serial", "pi"]
|
||||
|
@ -202,11 +228,12 @@ class NS(Game, Animation):
|
|||
{
|
||||
"float": "attract-gif-alpha",
|
||||
"bool": ["effects", "alpha-effect-title", "qr-static"],
|
||||
"path": "scores-font"
|
||||
"path": "scores-font",
|
||||
"int": "scores-alpha"
|
||||
},
|
||||
"system":
|
||||
{
|
||||
"bool": ["minimize-load-time", "enable-level-select"],
|
||||
"bool": ["minimize-load-time", "enable-level-select", "optimize-title-screen"],
|
||||
"int": ["lives-boss-rush-mode", "lives-level-select-mode"]
|
||||
},
|
||||
"pads":
|
||||
|
@ -250,8 +277,7 @@ class NS(Game, Animation):
|
|||
gpio.initialize_gpio()
|
||||
|
||||
# Launch a separate thread for reading the GPIO (and allowing its custom delays/sleeps). Use the daemon flag to force
|
||||
# exit when the main thread is killed (by a sigterm from systemctl stop) (?).
|
||||
self.gpio_kill = False
|
||||
# exit automatically when the main thread is killed.
|
||||
self.gpio_thread = Thread(target=self.read_gpio, daemon=True)
|
||||
self.gpio_thread.start()
|
||||
self.gpio_data = gpio.activity()
|
||||
|
@ -292,12 +318,11 @@ class NS(Game, Animation):
|
|||
if not found:
|
||||
raise SerialException("No usable serial port devices found. Use --no-serial for keyboard-only mode.")
|
||||
print(f"Using serial device at port {self.serial_reader.port}")
|
||||
self.serial_kill = False
|
||||
self.serial_data = 0
|
||||
self.reset_arduino()
|
||||
|
||||
# Launch a separate thread for reading serial data
|
||||
self.serial_thread = Thread(target=self.read_serial)
|
||||
self.serial_thread = Thread(target=self.read_serial, daemon=True)
|
||||
self.serial_thread.start()
|
||||
|
||||
Animation.__init__(self, self)
|
||||
|
@ -340,11 +365,32 @@ class NS(Game, Animation):
|
|||
# Draw the score sprites
|
||||
self.title.draw_scores()
|
||||
|
||||
# Initialize key input buffering
|
||||
self.last_press = get_ticks()
|
||||
|
||||
# Initialize pop-up
|
||||
self.register(self.close_pop_up)
|
||||
self.reset()
|
||||
self.pop_up_font = pygame.font.Font(self.get_resource(Dialogue.FONT_PATH), 12)
|
||||
self.pop_up_font = pygame.font.Font(self.get_resource(Dialogue.FONT_PATH), self.get_configuration("pop-up", "size"))
|
||||
self.pop_up_text = ""
|
||||
|
||||
# Initialize networking
|
||||
self.server = socket.create_server(("", self.get_configuration("network", "port")))
|
||||
self.peers = {}
|
||||
self.player_count = 1
|
||||
if self.get_configuration("network", "peers"):
|
||||
for peer in self.get_configuration("network", "peers"):
|
||||
# Store peers in a dictionary where the key is the peer address
|
||||
self.peers[peer] = NS.Peer(peer, self.get_configuration("network", "port"))
|
||||
print(f"Added peer {peer}")
|
||||
# Launch separate threads for listing and posting to peers
|
||||
self.listen_thread = Thread(target=self.listen_to_peers, daemon=True)
|
||||
self.listen_thread.start()
|
||||
self.post_thread = Thread(target=self.post_to_peers, daemon=True)
|
||||
self.post_thread.start()
|
||||
|
||||
self.reset()
|
||||
|
||||
# Clear events queue
|
||||
clear()
|
||||
|
||||
def pi_enabled(self):
|
||||
|
@ -357,11 +403,11 @@ class NS(Game, Animation):
|
|||
"""
|
||||
Test all connections of GPIO input pins.
|
||||
"""
|
||||
while not self.gpio_kill:
|
||||
while True:
|
||||
self.gpio_data = gpio.activity()
|
||||
|
||||
def read_serial(self):
|
||||
while not self.serial_kill:
|
||||
while True:
|
||||
name = self.get_configuration("input", "arduino-port")
|
||||
try:
|
||||
transmission = self.serial_reader.readline().strip()
|
||||
|
@ -416,6 +462,72 @@ class NS(Game, Animation):
|
|||
if self.gpio_data[light_id]:
|
||||
self.idle_elapsed = 0
|
||||
|
||||
def post_to_peers(self):
|
||||
"""
|
||||
Update peers with current status every 1/2 second.
|
||||
"""
|
||||
while True:
|
||||
# Determine this game's status
|
||||
if self.title.active:
|
||||
status = "title"
|
||||
elif self.level_select.active and self.level_select.level_index_selected is None:
|
||||
status = "level select"
|
||||
elif not self.boss.battle_finished:
|
||||
status = f"playing {self.level_select.level_index_selected}"
|
||||
elif self.boss.player_defeated:
|
||||
status = "lost"
|
||||
else:
|
||||
status = f"complete {boss.time_elapse}"
|
||||
|
||||
# Connect and send status message to each peer. If sending fails, pass and wait until the next iteration.
|
||||
for peer in self.peers.values():
|
||||
try:
|
||||
socket.create_connection((peer.address, peer.port)).send(str.encode(status))
|
||||
except:
|
||||
pass
|
||||
|
||||
# Send status every 1/2 second
|
||||
time.sleep(0.5)
|
||||
|
||||
def listen_to_peers(self):
|
||||
"""
|
||||
Update peer statuses by processing incoming messages on the socket server.
|
||||
"""
|
||||
while True:
|
||||
# Use the server to receive messages. Update peer statuses as the messages come in.
|
||||
read_list, write_list, except_list = select.select([self.server], [], [], 0.5)
|
||||
# When there is no read list, there are no messages to accept.
|
||||
if (len(read_list) > 0):
|
||||
incoming = self.server.accept()
|
||||
peer = self.peers[incoming[1][0]]
|
||||
# All messages are less than 64 characters
|
||||
message = incoming[0].recv(64).decode()
|
||||
if message.startswith("complete"):
|
||||
try:
|
||||
peer.result = float(message.split()[-1])
|
||||
peer.status = "complete"
|
||||
except:
|
||||
# Improperly formatted message received
|
||||
pass
|
||||
elif message.startswith("playing"):
|
||||
try:
|
||||
peer.level = int(message.split()[-1])
|
||||
peer.status = "playing"
|
||||
except:
|
||||
# Improperly formatted message received
|
||||
pass
|
||||
else:
|
||||
peer.status = message
|
||||
|
||||
def count_players(self):
|
||||
"""
|
||||
@return count of peers playing versus against this
|
||||
"""
|
||||
count = 1
|
||||
for peer in self.peers.values():
|
||||
count += peer.versus
|
||||
return count
|
||||
|
||||
def reset(self, leave_wipe_running=False):
|
||||
self.idle_elapsed = 0
|
||||
self.suppressing_input = False
|
||||
|
@ -468,20 +580,29 @@ class NS(Game, Animation):
|
|||
self.reset()
|
||||
elif event.key == K_a:
|
||||
self.reset_arduino()
|
||||
elif event.type == KEYDOWN and event.key == K_n and pygame.key.get_mods() & (pygame.KMOD_CTRL | pygame.KMOD_SHIFT):
|
||||
# Toggle visibility of network diagnostics menu
|
||||
state = self.get_configuration("network", "diagnostics")
|
||||
self.configuration.set("network", "diagnostics", not state)
|
||||
self.pop_up(f"Network diagnostics visible: {not state}")
|
||||
self.last_press = get_ticks()
|
||||
else:
|
||||
if self.get_delegate().compare(event, "reset-game"):
|
||||
self.reset()
|
||||
|
||||
def pop_up(self, text):
|
||||
def pop_up(self, text, clear=False):
|
||||
"""
|
||||
Trigger a pop up message that displays for a certain amount of time before being closed automatically. Adds a line of
|
||||
text to a variable that contains all pop up messages in case there is a previously sent message that needs to continue
|
||||
being displayed.
|
||||
|
||||
@param text message to display
|
||||
@param clear if True, delete any existing messages
|
||||
"""
|
||||
self.pop_up_text += f"{text}\n"
|
||||
if not clear:
|
||||
self.pop_up_text += f"{text}\n"
|
||||
else:
|
||||
self.pop_up_text = f"{text}\n"
|
||||
self.halt(self.close_pop_up)
|
||||
self.play(self.close_pop_up, play_once=True, delay=3000)
|
||||
|
||||
|
@ -548,28 +669,58 @@ class NS(Game, Animation):
|
|||
self.chemtrails.update()
|
||||
self.boss.update_dialogue()
|
||||
self.wipe.update()
|
||||
# Draw the pop up text line by line if there is any
|
||||
pop_up_y = 0
|
||||
for line in self.pop_up_text.split("\n"):
|
||||
if line:
|
||||
surface = self.pop_up_font.render(line, False, (0, 0, 0), (255, 255, 255))
|
||||
self.get_display_surface().blit(surface, (0, pop_up_y))
|
||||
pop_up_y += surface.get_height()
|
||||
|
||||
# Draw pop up text line by line
|
||||
if self.pop_up_text:
|
||||
width = 0
|
||||
height = 0
|
||||
for line in self.pop_up_text.split("\n"):
|
||||
if line:
|
||||
line_width, line_height = self.pop_up_font.size(line)
|
||||
if line_width > width:
|
||||
width = line_width
|
||||
height += line_height
|
||||
full_surface = pygame.Surface((width, height))
|
||||
x = 0
|
||||
y = 0
|
||||
for line in self.pop_up_text.split("\n"):
|
||||
if line:
|
||||
surface = self.pop_up_font.render(
|
||||
line, True, pygame.Color(self.get_configuration("pop-up", "foreground")),
|
||||
pygame.Color(self.get_configuration("pop-up", "background")))
|
||||
full_surface.blit(surface, (x, y))
|
||||
y += surface.get_height()
|
||||
if y > 0:
|
||||
sprite = Sprite(self)
|
||||
sprite.add_frame(full_surface)
|
||||
if self.get_configuration("pop-up", "center"):
|
||||
sprite.location.center = self.get_display_surface().get_rect().center
|
||||
sprite.update()
|
||||
|
||||
# Draw network diagnostics
|
||||
if self.get_configuration("network", "diagnostics"):
|
||||
y = self.get_display_surface().get_rect().bottom
|
||||
font = pygame.font.Font(self.get_configuration("network", "diagnostics-font"),
|
||||
self.get_configuration("network", "diagnostics-size"))
|
||||
for peer in self.peers.values():
|
||||
surface = font.render(
|
||||
f"{peer.address} {peer.status} [PvP {peer.versus}, lvl {peer.level}, result {peer.result}]",
|
||||
True, (255, 255, 255), (0, 0, 0))
|
||||
y -= surface.get_height()
|
||||
self.get_display_surface().blit(surface, (0, y))
|
||||
|
||||
# Reset the game when idle
|
||||
self.idle_elapsed += self.time_filter.get_last_frame_duration()
|
||||
if self.idle_elapsed >= self.IDLE_TIMEOUT:
|
||||
self.reset()
|
||||
|
||||
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
|
||||
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"):
|
||||
|
||||
# Kill serial threads
|
||||
self.serial_kill = True
|
||||
self.gpio_kill = True
|
||||
|
||||
# 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:
|
||||
|
@ -645,6 +796,7 @@ class LevelSelect(Animation):
|
|||
self.previews[-1].add_frame(frame)
|
||||
self.previews[-1].location.midbottom = self.platforms[level_index].view.location.centerx, \
|
||||
self.platforms[level_index].view.location.top - 12
|
||||
self.reset()
|
||||
|
||||
def activate(self):
|
||||
self.active = True
|
||||
|
@ -728,7 +880,8 @@ class LevelSelect(Animation):
|
|||
if self.grow_sound_channel is None:
|
||||
self.grow_sound_channel = self.get_audio().play_sfx("grow", -1, x=platform.view.location.centerx)
|
||||
# Draw a growing ring around the currently pressed level
|
||||
angle = self.get_game().platform.press_elapsed / self.get_configuration("time", "level-select-press-length") * 2 * pi
|
||||
angle = self.get_game().platform.press_elapsed / \
|
||||
self.get_configuration("time", "level-select-press-length") * 2 * pi
|
||||
diameter = self.previews[level_index].location.height + 21
|
||||
rect = pygame.Rect(0, 0, diameter, diameter)
|
||||
rect.center = self.previews[level_index].location.center
|
||||
|
@ -1093,11 +1246,10 @@ class Title(Animation):
|
|||
Handles displaying and drawing the title screen. It draws the high scores, creates and updates an attract mode video pop-up, tracks
|
||||
the player's moves and checks if they are doing the start game pattern, and updates the background logo and giant Tony sprite.
|
||||
|
||||
Notes
|
||||
-----
|
||||
Title.draw_scores is a slow method, so the scores should only be drawn when a score is added.
|
||||
|
||||
* It should be directed to draw scores when a new score is added (and only then), and it will store the score surfaces and only blit
|
||||
them once when activated (unless the scores are updated). This way it only blits that section of the screen once.
|
||||
If the game is configured to optimize on the title screen, the scores will only be blit when Title.activate is called. Otherwise,
|
||||
they will blit every update.
|
||||
"""
|
||||
|
||||
# Sequence of moves the player must do to start the game
|
||||
|
@ -1110,6 +1262,7 @@ class Title(Animation):
|
|||
@param parent GameChild object that will connect this GameChild object to the overall tree and root Game object
|
||||
"""
|
||||
Animation.__init__(self, parent)
|
||||
self.active = False
|
||||
|
||||
# Set up attract mode pop-up
|
||||
self.angle = pi / 8
|
||||
|
@ -1145,14 +1298,17 @@ class Title(Animation):
|
|||
self.get_game().tony.set_frameset("static")
|
||||
self.get_audio().play_bgm("title")
|
||||
|
||||
# Blit the scores
|
||||
for sprite in self.score_sprites:
|
||||
sprite.update()
|
||||
# Optimization for only drawing part of the title screen
|
||||
if self.get_configuration("system", "optimize-title-screen"):
|
||||
|
||||
# Optimize by setting a clip that excludes the area where the scores are drawn
|
||||
self.get_display_surface().set_clip(
|
||||
(self.score_sprites[0].location.right, 0, self.score_sprites[1].location.left - self.score_sprites[0].location.right,
|
||||
self.get_display_surface().get_height()))
|
||||
# Blit the scores
|
||||
for sprite in self.score_sprites:
|
||||
sprite.update()
|
||||
|
||||
# Optimize by setting a clip that excludes the area where the scores are drawn
|
||||
self.get_display_surface().set_clip(
|
||||
(self.score_sprites[0].location.right, 0, self.score_sprites[1].location.left - self.score_sprites[0].location.right,
|
||||
self.get_display_surface().get_height()))
|
||||
|
||||
def deactivate(self):
|
||||
self.active = False
|
||||
|
@ -1238,9 +1394,9 @@ class Title(Animation):
|
|||
|
||||
def draw_scores(self):
|
||||
"""
|
||||
Create two columns, one for each side of the screen. Draw as many scores as can fit along each column, in order from best to worst, separating
|
||||
them evenly into categories: normal, advanced, and expert. Save the columns as sprites. Note that this doesn't support non-level select mode
|
||||
anymore.
|
||||
Create two columns, one for each side of the screen. Draw as many scores as can fit along each column, in order from
|
||||
best to worst, separating them evenly into categories: normal, advanced, and expert. Save the columns as sprites. Note
|
||||
that this doesn't support non-level select mode anymore.
|
||||
"""
|
||||
ds = self.get_display_surface()
|
||||
self.score_indicator = None
|
||||
|
@ -1290,6 +1446,10 @@ class Title(Animation):
|
|||
y += self.draw_score_to_column(score, right_column, (x, y), rank)
|
||||
right_column_sprite.add_frame(right_column)
|
||||
right_column_sprite.location.topleft = x, 0
|
||||
if not self.get_configuration("system", "optimize-title-screen") and self.get_configuration("display", "scores-alpha") < 255:
|
||||
alpha = self.get_configuration("display", "scores-alpha")
|
||||
left_column.set_alpha(alpha)
|
||||
right_column.set_alpha(alpha)
|
||||
self.score_sprites = [left_column_sprite, right_column_sprite]
|
||||
|
||||
def show_video(self):
|
||||
|
@ -1330,6 +1490,11 @@ class Title(Animation):
|
|||
self.get_audio().play_sfx("land_0")
|
||||
self.get_game().tony.update()
|
||||
|
||||
# Draw the scores unless optimized out
|
||||
if not self.get_configuration("system", "optimize-title-screen"):
|
||||
for sprite in self.score_sprites:
|
||||
sprite.update()
|
||||
|
||||
# Bounce the GIF around the screen
|
||||
if self.video.location.right > dsr.right or self.video.location.left < dsr.left:
|
||||
self.angle = reflect_angle(self.angle, 0)
|
||||
|
@ -2340,6 +2505,7 @@ class Boss(Animation):
|
|||
animations that control attacks, effects, and dialog.
|
||||
"""
|
||||
Animation.__init__(self, parent)
|
||||
self.battle_finished = False
|
||||
# Set up sprites with boil, hit, and intro animations
|
||||
self.boss_sprites = []
|
||||
self.boss_sprite_arms = []
|
||||
|
@ -2865,7 +3031,8 @@ class Boss(Animation):
|
|||
|
||||
def level_sprite(self, level_index=None):
|
||||
"""
|
||||
Return the boss sprite associated with this the given level index. If level index is not given, use the value in `self.level_index`.
|
||||
Return the boss sprite associated with this the given level index. If level index is not given, use the value in
|
||||
`self.level_index`.
|
||||
|
||||
@param level_index index of the level of the requested sprite
|
||||
"""
|
||||
|
|
18
config
18
config
|
@ -30,6 +30,7 @@ attract-gif-alpha = 1.0
|
|||
effects = True
|
||||
alpha-effect-title = True
|
||||
scores-font = BPmono.ttf
|
||||
scores-alpha = 230
|
||||
qr-static = False
|
||||
|
||||
[system]
|
||||
|
@ -38,6 +39,23 @@ minimize-load-time = no
|
|||
enable-level-select = yes
|
||||
lives-boss-rush-mode = 3
|
||||
lives-level-select-mode = 1
|
||||
optimize-title-screen = no
|
||||
|
||||
[network]
|
||||
peers = 192.168.50.50, 192.168.50.77
|
||||
port = 8080
|
||||
delimeter = |
|
||||
timeout = 10.0
|
||||
diagnostics = no
|
||||
diagnostics-font = resource/BPmono.ttf
|
||||
diagnostics-size = 15
|
||||
|
||||
[pop-up]
|
||||
size = 22
|
||||
length = 3000
|
||||
foreground = #ffffff
|
||||
background = #000000
|
||||
center = yes
|
||||
|
||||
[boss]
|
||||
damage-per-hit-level-1 = 6.0
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
40143 0
|
Loading…
Reference in New Issue