From e7846d4826c8a7f7fd634960b4f7b7307f043ba0 Mon Sep 17 00:00:00 2001 From: frank Date: Wed, 10 Jan 2024 15:16:10 -0800 Subject: [PATCH] use level select vote messages to determine if peers are playing versus and sync random seed for level --- NS.py | 144 +++++++++++++++++++++++++++++++++++++++++++-------------- config | 3 +- 2 files changed, 112 insertions(+), 35 deletions(-) diff --git a/NS.py b/NS.py index b056a9c..f973096 100644 --- a/NS.py +++ b/NS.py @@ -10,7 +10,7 @@ # # 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 try: @@ -18,7 +18,6 @@ try: except ImportError: pass -from random import randint, choice, random from math import pi from copy import copy from glob import iglob @@ -154,6 +153,7 @@ class NS(Game, Animation): result = None versus = False level = None + seed = None def __init__(self, address, port): self.address = address @@ -210,7 +210,7 @@ class NS(Game, Animation): }, "network": { - "int": ["port", "diagnostics-size"], + "int": ["port", "diagnostics-size", "join-time-limit"], "bool": "diagnostics", "path": "diagnostics-font", "list": "peers" @@ -234,7 +234,7 @@ class NS(Game, Animation): "system": { "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": { @@ -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_text = "" - # Initialize networking + # Initialize networking. Include self as a peer located at "localhost". self.server = socket.create_server(("", self.get_configuration("network", "port"))) - self.peers = {} - self.player_count = 1 + self.peers = {"localhost": NS.Peer("localhost", self.get_configuration("network", "port"))} + self.peers["localhost"].versus = True + print(f"Added peer 'localhost'") 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}") + 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() @@ -470,21 +471,37 @@ class NS(Game, Animation): # Determine this game's status if self.title.active: status = "title" + message = status 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}" + message = status + 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: status = "lost" + message = status 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. for peer in self.peers.values(): - try: - socket.create_connection((peer.address, peer.port)).send(str.encode(status)) - except: - pass + if peer.address != "localhost": + try: + socket.create_connection((peer.address, peer.port)).send(str.encode(message)) + except: + pass # Send status every 1/2 second time.sleep(0.5) @@ -502,6 +519,8 @@ class NS(Game, Animation): peer = self.peers[incoming[1][0]] # All messages are less than 64 characters message = incoming[0].recv(64).decode() + if message.startswith("title") or message.startswith("level select"): + peer.versus = False if message.startswith("complete"): try: peer.result = int(message.split()[-1]) @@ -514,20 +533,36 @@ class NS(Game, Animation): peer.level = int(message.split()[-1]) peer.status = "playing" 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 else: peer.status = message 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(): count += peer.versus 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): self.idle_elapsed = 0 self.suppressing_input = False @@ -693,6 +728,7 @@ class NS(Game, Animation): if y > 0: sprite = Sprite(self) sprite.add_frame(full_surface) + sprite.set_alpha(200) if self.get_configuration("pop-up", "center"): sprite.location.center = self.get_display_surface().get_rect().center sprite.update() @@ -706,9 +742,14 @@ class NS(Game, Animation): surface = font.render( f"{peer.address} {peer.status} [PvP {peer.versus}, lvl {peer.level}, result {peer.result}]", True, (255, 255, 255), (0, 0, 0)) + surface.set_alpha(200) y -= surface.get_height() 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 self.idle_elapsed += self.time_filter.get_last_frame_duration() if self.idle_elapsed >= self.IDLE_TIMEOUT: @@ -744,7 +785,7 @@ class LevelSelect(Animation): def __init__(self, parent): Animation.__init__(self, parent) self.subscribe(self.respond, KEYDOWN) - self.register(self.timeout) + self.register(self.timeout, self.force_launch) y = 250 indent = 10 dsr = self.get_display_surface().get_rect() @@ -812,6 +853,8 @@ class LevelSelect(Animation): def reset(self): self.deactivate() self.level_index_selected = None + self.level_launched = False + self.launch_forced = False self.zoom = 1.0 self.grow_sound_channel = None 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) + def force_launch(self): + self.launch_forced = True + def update(self): if self.active: Animation.update(self) @@ -870,11 +916,21 @@ class LevelSelect(Animation): for level_index, platform in enumerate(self.platforms): 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"): - # 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 if self.grow_sound_channel is not None: self.grow_sound_channel.stop() 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 else: if self.grow_sound_channel is None: @@ -890,15 +946,34 @@ class LevelSelect(Animation): if offset < angle: pygame.draw.arc(self.get_display_surface(), (255, 255, 255), rect, offset, angle, 14) offset += .01 - if self.level_index_selected is not None: - # Launch the level - 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") + + # Check if peers are still deciding + elif not self.level_launched: + + # Launch if time is up or the lobby is empty + if all(peer.status != "level select" or peer.address == "localhost" for peer in self.get_game().peers.values()) or \ + 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): + + # 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) + for platform in self.platforms: platform.update() if self.level_index_selected is not None: @@ -1099,7 +1174,7 @@ class Tony(Sprite): Sprite.shift_frame(self) frameset = self.get_current_frameset() 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): """ @@ -1196,9 +1271,9 @@ class Video(Sprite): def shift_frame(self): Sprite.shift_frame(self) - if random() < self.next_video_chance: + if random.random() < self.next_video_chance: 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: self.gif_index = selection self.load_selection() @@ -2707,6 +2782,7 @@ class Boss(Animation): def brandish(self): self.queue = [] platform = self.get_game().platform + choice = random.choice if self.level_index == 0: if self.health.amount > 90: first = choice(platform.get_steps_from_edge(self.last_attack)) @@ -2859,7 +2935,7 @@ class Boss(Animation): length = 8 while len(self.queue) < length: while True: - orientation = randint(0, 5) + orientation = random.randint(0, 5) if (not self.queue and orientation != self.last_attack) or \ (len(self.queue) > 0 and orientation != self.queue[-1]): self.queue.append(orientation) @@ -2876,7 +2952,7 @@ class Boss(Animation): def choose_new_edge(self, edges): while True: - edge = choice(edges) + edge = random.choice(edges) if edge != self.last_attack: return edge @@ -3468,7 +3544,7 @@ class Ending(Animation): self.deactivate() self.halt() 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() def deactivate(self): diff --git a/config b/config index d3d478f..ebc49cd 100644 --- a/config +++ b/config @@ -40,12 +40,13 @@ enable-level-select = yes lives-boss-rush-mode = 3 lives-level-select-mode = 1 optimize-title-screen = no +max-seed = 2147483647 [network] peers = port = 8080 delimeter = | -timeout = 10.0 +join-time-limit = 5999 diagnostics = no diagnostics-font = resource/BPmono.ttf diagnostics-size = 15