use level select vote messages to determine if peers are playing versus and sync random seed for level

This commit is contained in:
ohsqueezy 2024-01-10 15:16:10 -08:00
parent a085eaff6d
commit e7846d4826
2 changed files with 112 additions and 35 deletions

144
NS.py
View File

@ -10,7 +10,7 @@
# #
# This is the main file containing all the Pygame code. # This is the main file containing all the Pygame code.
import argparse, pathlib, operator, subprocess, sys, os, socket, select, time import argparse, pathlib, operator, subprocess, sys, os, socket, select, time, random
# Auto-detect GPIO library # Auto-detect GPIO library
try: try:
@ -18,7 +18,6 @@ try:
except ImportError: except ImportError:
pass pass
from random import randint, choice, random
from math import pi from math import pi
from copy import copy from copy import copy
from glob import iglob from glob import iglob
@ -154,6 +153,7 @@ class NS(Game, Animation):
result = None result = None
versus = False versus = False
level = None level = None
seed = None
def __init__(self, address, port): def __init__(self, address, port):
self.address = address self.address = address
@ -210,7 +210,7 @@ class NS(Game, Animation):
}, },
"network": "network":
{ {
"int": ["port", "diagnostics-size"], "int": ["port", "diagnostics-size", "join-time-limit"],
"bool": "diagnostics", "bool": "diagnostics",
"path": "diagnostics-font", "path": "diagnostics-font",
"list": "peers" "list": "peers"
@ -234,7 +234,7 @@ class NS(Game, Animation):
"system": "system":
{ {
"bool": ["minimize-load-time", "enable-level-select", "optimize-title-screen"], "bool": ["minimize-load-time", "enable-level-select", "optimize-title-screen"],
"int": ["lives-boss-rush-mode", "lives-level-select-mode"] "int": ["lives-boss-rush-mode", "lives-level-select-mode", "max-seed"]
}, },
"pads": "pads":
{ {
@ -373,15 +373,16 @@ class NS(Game, Animation):
self.pop_up_font = pygame.font.Font(self.get_resource(Dialogue.FONT_PATH), self.get_configuration("pop-up", "size")) self.pop_up_font = pygame.font.Font(self.get_resource(Dialogue.FONT_PATH), self.get_configuration("pop-up", "size"))
self.pop_up_text = "" self.pop_up_text = ""
# Initialize networking # Initialize networking. Include self as a peer located at "localhost".
self.server = socket.create_server(("", self.get_configuration("network", "port"))) self.server = socket.create_server(("", self.get_configuration("network", "port")))
self.peers = {} self.peers = {"localhost": NS.Peer("localhost", self.get_configuration("network", "port"))}
self.player_count = 1 self.peers["localhost"].versus = True
print(f"Added peer 'localhost'")
if self.get_configuration("network", "peers"): if self.get_configuration("network", "peers"):
for peer in 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 # Store peers in a dictionary where the key is the peer address
self.peers[peer] = NS.Peer(peer, self.get_configuration("network", "port")) self.peers[peer] = NS.Peer(peer, self.get_configuration("network", "port"))
print(f"Added peer {peer}") print(f"Added peer '{peer}'")
# Launch separate threads for listing and posting to peers # Launch separate threads for listing and posting to peers
self.listen_thread = Thread(target=self.listen_to_peers, daemon=True) self.listen_thread = Thread(target=self.listen_to_peers, daemon=True)
self.listen_thread.start() self.listen_thread.start()
@ -470,21 +471,37 @@ class NS(Game, Animation):
# Determine this game's status # Determine this game's status
if self.title.active: if self.title.active:
status = "title" status = "title"
message = status
elif self.level_select.active and self.level_select.level_index_selected is None: elif self.level_select.active and self.level_select.level_index_selected is None:
status = "level select" status = "level select"
elif not self.boss.battle_finished: message = status
status = f"playing {self.level_select.level_index_selected}" elif self.level_select.active and not self.level_select.level_launched:
status = "voted"
level = self.level_select.level_index_selected
self.peers["localhost"].level = level
message = f"{status} {level} {self.peers['localhost'].seed}"
elif self.level_select.active or not self.boss.battle_finished:
status = "playing"
level = self.level_select.level_index_selected
self.peers["localhost"].level = level
message = f"{status} {level}"
elif self.boss.player_defeated: elif self.boss.player_defeated:
status = "lost" status = "lost"
message = status
else: else:
status = f"complete {self.most_recent_score.milliseconds}" status = "complete"
result = self.most_recent_score.milliseconds
self.peers["localhost"].result = result
message = f"{status} {result}"
self.peers["localhost"].status = status
# Connect and send status message to each peer. If sending fails, pass and wait until the next iteration. # Connect and send status message to each peer. If sending fails, pass and wait until the next iteration.
for peer in self.peers.values(): for peer in self.peers.values():
try: if peer.address != "localhost":
socket.create_connection((peer.address, peer.port)).send(str.encode(status)) try:
except: socket.create_connection((peer.address, peer.port)).send(str.encode(message))
pass except:
pass
# Send status every 1/2 second # Send status every 1/2 second
time.sleep(0.5) time.sleep(0.5)
@ -502,6 +519,8 @@ class NS(Game, Animation):
peer = self.peers[incoming[1][0]] peer = self.peers[incoming[1][0]]
# All messages are less than 64 characters # All messages are less than 64 characters
message = incoming[0].recv(64).decode() message = incoming[0].recv(64).decode()
if message.startswith("title") or message.startswith("level select"):
peer.versus = False
if message.startswith("complete"): if message.startswith("complete"):
try: try:
peer.result = int(message.split()[-1]) peer.result = int(message.split()[-1])
@ -514,20 +533,36 @@ class NS(Game, Animation):
peer.level = int(message.split()[-1]) peer.level = int(message.split()[-1])
peer.status = "playing" peer.status = "playing"
except: except:
# Improperly formatted message received pass
elif message.startswith("voted"):
try:
status, level, seed = message.split()
peer.level = int(level)
peer.seed = int(seed)
peer.status = status
except:
pass pass
else: else:
peer.status = message peer.status = message
def count_players(self): def count_players(self):
""" """
@return count of peers playing versus against this @return count of peers committed to a match with this peer
""" """
count = 1 count = 0
for peer in self.peers.values(): for peer in self.peers.values():
count += peer.versus count += peer.versus
return count return count
def count_lobby(self):
"""
@return count of peers at the level select screen
"""
count = 0
for peer in self.peers.values():
count += peer.status == "level select" or peer.status == "voted"
return count
def reset(self, leave_wipe_running=False): def reset(self, leave_wipe_running=False):
self.idle_elapsed = 0 self.idle_elapsed = 0
self.suppressing_input = False self.suppressing_input = False
@ -693,6 +728,7 @@ class NS(Game, Animation):
if y > 0: if y > 0:
sprite = Sprite(self) sprite = Sprite(self)
sprite.add_frame(full_surface) sprite.add_frame(full_surface)
sprite.set_alpha(200)
if self.get_configuration("pop-up", "center"): if self.get_configuration("pop-up", "center"):
sprite.location.center = self.get_display_surface().get_rect().center sprite.location.center = self.get_display_surface().get_rect().center
sprite.update() sprite.update()
@ -706,9 +742,14 @@ class NS(Game, Animation):
surface = font.render( surface = font.render(
f"{peer.address} {peer.status} [PvP {peer.versus}, lvl {peer.level}, result {peer.result}]", f"{peer.address} {peer.status} [PvP {peer.versus}, lvl {peer.level}, result {peer.result}]",
True, (255, 255, 255), (0, 0, 0)) True, (255, 255, 255), (0, 0, 0))
surface.set_alpha(200)
y -= surface.get_height() y -= surface.get_height()
self.get_display_surface().blit(surface, (0, y)) self.get_display_surface().blit(surface, (0, y))
surface = font.render(f"players: {self.count_players()} lobby: {self.count_lobby()}", True, (255, 255, 255), (0, 0, 0))
surface.set_alpha(200)
y -= surface.get_height()
self.get_display_surface().blit(surface, (0, y))
# Reset the game when idle # Reset the game when idle
self.idle_elapsed += self.time_filter.get_last_frame_duration() self.idle_elapsed += self.time_filter.get_last_frame_duration()
if self.idle_elapsed >= self.IDLE_TIMEOUT: if self.idle_elapsed >= self.IDLE_TIMEOUT:
@ -744,7 +785,7 @@ class LevelSelect(Animation):
def __init__(self, parent): def __init__(self, parent):
Animation.__init__(self, parent) Animation.__init__(self, parent)
self.subscribe(self.respond, KEYDOWN) self.subscribe(self.respond, KEYDOWN)
self.register(self.timeout) self.register(self.timeout, self.force_launch)
y = 250 y = 250
indent = 10 indent = 10
dsr = self.get_display_surface().get_rect() dsr = self.get_display_surface().get_rect()
@ -812,6 +853,8 @@ class LevelSelect(Animation):
def reset(self): def reset(self):
self.deactivate() self.deactivate()
self.level_index_selected = None self.level_index_selected = None
self.level_launched = False
self.launch_forced = False
self.zoom = 1.0 self.zoom = 1.0
self.grow_sound_channel = None self.grow_sound_channel = None
for level_index in range(3): for level_index in range(3):
@ -859,6 +902,9 @@ class LevelSelect(Animation):
""" """
self.get_game().wipe.start(self.get_game().reset, leave_wipe_running=True) self.get_game().wipe.start(self.get_game().reset, leave_wipe_running=True)
def force_launch(self):
self.launch_forced = True
def update(self): def update(self):
if self.active: if self.active:
Animation.update(self) Animation.update(self)
@ -870,11 +916,21 @@ class LevelSelect(Animation):
for level_index, platform in enumerate(self.platforms): for level_index, platform in enumerate(self.platforms):
if platform.get_glowing_edge() == self.get_game().platform.get_edge_pressed(): if platform.get_glowing_edge() == self.get_game().platform.get_edge_pressed():
if self.get_game().platform.press_elapsed > self.get_configuration("time", "level-select-press-length"): if self.get_game().platform.press_elapsed > self.get_configuration("time", "level-select-press-length"):
# This will cause the level to launch # This will cause a vote to be cast to peers if there are any. If there are others in the lobby,
# the game will wait for other votes to be cast or the lobby to clear. Otherwise, the level will
# launch.
self.level_index_selected = level_index self.level_index_selected = level_index
if self.grow_sound_channel is not None: if self.grow_sound_channel is not None:
self.grow_sound_channel.stop() self.grow_sound_channel.stop()
self.grow_sound_channel = None self.grow_sound_channel = None
self.get_game().peers["localhost"].seed = random.randint(0, self.get_configuration("system", "max-seed"))
self.play(self.force_launch, delay=self.get_configuration("network", "join-time-limit"))
# Wipe away other levels and zoom selected
for level_index in range(3):
if level_index != self.level_index_selected:
self.platforms[level_index].view.play(self.platforms[level_index].view.wipe_out)
self.previews[level_index].play(self.previews[level_index].wipe_out, interval=100)
self.get_audio().play_sfx("complete_pattern_3")
break break
else: else:
if self.grow_sound_channel is None: if self.grow_sound_channel is None:
@ -890,15 +946,34 @@ class LevelSelect(Animation):
if offset < angle: if offset < angle:
pygame.draw.arc(self.get_display_surface(), (255, 255, 255), rect, offset, angle, 14) pygame.draw.arc(self.get_display_surface(), (255, 255, 255), rect, offset, angle, 14)
offset += .01 offset += .01
if self.level_index_selected is not None:
# Launch the level # Check if peers are still deciding
for level_index in range(3): elif not self.level_launched:
if level_index != self.level_index_selected:
self.platforms[level_index].view.play(self.platforms[level_index].view.wipe_out) # Launch if time is up or the lobby is empty
self.previews[level_index].play(self.previews[level_index].wipe_out, interval=100) if all(peer.status != "level select" or peer.address == "localhost" for peer in self.get_game().peers.values()) or \
self.get_audio().play_sfx("complete_pattern_3") self.launch_forced:
seed = self.get_game().peers["localhost"].seed
for peer in self.get_game().peers.values():
if peer.address != "localhost" and peer.status == "voted" and peer.level == self.level_index_selected:
peer.versus = True
seed = (seed + peer.seed) % self.get_configuration("system", "max-seed")
random.seed(seed)
self.halt(self.force_launch)
self.get_game().pop_up("", clear=True)
self.level_launched = True
# Update displayed wait message
else:
self.get_game().pop_up(
f"Waiting {self.accounts[self.force_launch].delay // 1000 + 1}s for players to join", clear=True)
# Second half of launch animation
elif not self.get_game().wipe.is_playing() and any(preview.is_hidden() for preview in self.previews): elif not self.get_game().wipe.is_playing() and any(preview.is_hidden() for preview in self.previews):
# Final animation before game will launch, launch is attached to the animation and will be triggered automatically
self.get_game().wipe.start(self.launch_selected_index) self.get_game().wipe.start(self.launch_selected_index)
for platform in self.platforms: for platform in self.platforms:
platform.update() platform.update()
if self.level_index_selected is not None: if self.level_index_selected is not None:
@ -1099,7 +1174,7 @@ class Tony(Sprite):
Sprite.shift_frame(self) Sprite.shift_frame(self)
frameset = self.get_current_frameset() frameset = self.get_current_frameset()
if frameset.name == "board" and frameset.current_index == 1: if frameset.name == "board" and frameset.current_index == 1:
self.get_audio().play_sfx(choice(self.taunts)) self.get_audio().play_sfx(random.choice(self.taunts))
def update(self): def update(self):
""" """
@ -1196,9 +1271,9 @@ class Video(Sprite):
def shift_frame(self): def shift_frame(self):
Sprite.shift_frame(self) Sprite.shift_frame(self)
if random() < self.next_video_chance: if random.random() < self.next_video_chance:
while True: while True:
selection = choice(range(0, len(self.gif_frames_scaled))) selection = random.choice(range(0, len(self.gif_frames_scaled)))
if selection != self.gif_index: if selection != self.gif_index:
self.gif_index = selection self.gif_index = selection
self.load_selection() self.load_selection()
@ -2707,6 +2782,7 @@ class Boss(Animation):
def brandish(self): def brandish(self):
self.queue = [] self.queue = []
platform = self.get_game().platform platform = self.get_game().platform
choice = random.choice
if self.level_index == 0: if self.level_index == 0:
if self.health.amount > 90: if self.health.amount > 90:
first = choice(platform.get_steps_from_edge(self.last_attack)) first = choice(platform.get_steps_from_edge(self.last_attack))
@ -2859,7 +2935,7 @@ class Boss(Animation):
length = 8 length = 8
while len(self.queue) < length: while len(self.queue) < length:
while True: while True:
orientation = randint(0, 5) orientation = random.randint(0, 5)
if (not self.queue and orientation != self.last_attack) or \ if (not self.queue and orientation != self.last_attack) or \
(len(self.queue) > 0 and orientation != self.queue[-1]): (len(self.queue) > 0 and orientation != self.queue[-1]):
self.queue.append(orientation) self.queue.append(orientation)
@ -2876,7 +2952,7 @@ class Boss(Animation):
def choose_new_edge(self, edges): def choose_new_edge(self, edges):
while True: while True:
edge = choice(edges) edge = random.choice(edges)
if edge != self.last_attack: if edge != self.last_attack:
return edge return edge
@ -3468,7 +3544,7 @@ class Ending(Animation):
self.deactivate() self.deactivate()
self.halt() self.halt()
self.text_index = 0 self.text_index = 0
self.angle = choice((pi / 4, 3 * pi / 4, 5 * pi / 4, 7 * pi / 4)) self.angle = random.choice((pi / 4, 3 * pi / 4, 5 * pi / 4, 7 * pi / 4))
self.slime_bag.reset() self.slime_bag.reset()
def deactivate(self): def deactivate(self):

3
config
View File

@ -40,12 +40,13 @@ enable-level-select = yes
lives-boss-rush-mode = 3 lives-boss-rush-mode = 3
lives-level-select-mode = 1 lives-level-select-mode = 1
optimize-title-screen = no optimize-title-screen = no
max-seed = 2147483647
[network] [network]
peers = peers =
port = 8080 port = 8080
delimeter = | delimeter = |
timeout = 10.0 join-time-limit = 5999
diagnostics = no diagnostics = no
diagnostics-font = resource/BPmono.ttf diagnostics-font = resource/BPmono.ttf
diagnostics-size = 15 diagnostics-size = 15