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.
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):

3
config
View File

@ -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