From 05061161e296ba2e54f3fc7d60a88adac9cd2761 Mon Sep 17 00:00:00 2001 From: Zoltan Szabatin Date: Sun, 2 Mar 2025 22:39:48 -0800 Subject: feat: Add split mode toggle for debugging --- src/splitscreen_duo/__init__.py | 104 ++++++++++++++++++++++++++------- src/splitscreen_duo/command.py | 4 ++ src/splitscreen_duo/games/breakout.py | 14 +++-- src/splitscreen_duo/games/game_base.py | 55 ++++++++++++----- src/splitscreen_duo/games/pong.py | 54 ++++++++--------- src/splitscreen_duo/games/snake.py | 18 ++++-- src/splitscreen_duo/menu.py | 21 ++++++- 7 files changed, 192 insertions(+), 78 deletions(-) diff --git a/src/splitscreen_duo/__init__.py b/src/splitscreen_duo/__init__.py index 309135c..defba37 100644 --- a/src/splitscreen_duo/__init__.py +++ b/src/splitscreen_duo/__init__.py @@ -19,8 +19,9 @@ def main() -> int: serial = Serial(os.getenv("SERIAL_DEVICE", "/dev/null"), 115200) INSTANCE = os.getenv("INSTANCE", "Unknown") + is_joint_mode = [False] - pygame.display.set_caption(f"SplitScreen Duo Menu ({INSTANCE})") + pygame.display.set_caption(f"SplitScreen Duo ({INSTANCE})") logging.basicConfig( level=(logging.DEBUG if os.getenv("DEVELOPMENT", "").lower() else logging.INFO) ) @@ -28,6 +29,10 @@ def main() -> int: logger.info(f"running as {os.getenv('INSTANCE', 'unknown')}") menu.init_display() + font = pygame.font.Font(None, 36) + BLACK = (0, 0, 0) + WHITE = (255, 255, 255) + score = 0 game_handlers = { Game.BENCHMARK.value: ( "benchmark", @@ -35,15 +40,17 @@ def main() -> int: ), Game.SNAKE.value: ( "snake", - lambda: Snake(menu.screen, serial, INSTANCE).main_loop(), + lambda: Snake(menu.screen, serial, INSTANCE, is_joint_mode[0]).main_loop(), ), Game.BREAKOUT.value: ( "breakout", - lambda: Breakout(menu.screen, serial, INSTANCE).main_loop(), + lambda: Breakout( + menu.screen, serial, INSTANCE, is_joint_mode[0] + ).main_loop(), ), Game.PONG.value: ( "pong", - lambda: Pong(menu.screen, serial, INSTANCE).main_loop(), + lambda: Pong(menu.screen, serial, INSTANCE, is_joint_mode[0]).main_loop(), ), } @@ -59,6 +66,11 @@ def main() -> int: game_result = game_func() + if is_joint_mode[0] and INSTANCE == "primary": + serial.write( + json.dumps({"command": Command.GAME_ENDED.value}).encode("utf-8") + ) + if game_result and game_result.get("command") == Command.QUIT.value: logger.info(f"{game_name} game returned QUIT, shutting down") pygame.quit() @@ -70,20 +82,49 @@ def main() -> int: is_running = True while is_running: - if INSTANCE == "secondary" and serial.in_waiting() > 0: + if serial.in_waiting() > 0: + data = serial.readline().decode("utf-8").strip() + try: - data = serial.readline().decode("utf-8").strip() + message = json.loads(data) + command = message.get("command") - logger.debug(f"received serial data: {data}") + if command == Command.ENTER_JOINT_MODE.value: + is_joint_mode[0] = True - message = json.loads(data) + logger.info("secondary entering joint mode") + elif command == Command.EXIT_JOINT_MODE.value: + is_joint_mode[0] = False - if message.get("command") == Command.QUIT.value: - logger.info("received quit command from primary, shutting down") + logger.info("secondary exiting joint mode") + elif command == Command.QUIT.value: pygame.quit() return 0 - elif message.get("command") == Command.SELECT_GAME.value: + elif INSTANCE == "secondary" and is_joint_mode[0]: + if command == Command.STATS.value: + score = message.get("value", 0) + elif command == Command.GAME_ENDED.value: + menu.screen.fill(BLACK) + + text = font.render("Game Ended", True, WHITE) + + menu.screen.blit( + text, + ( + menu.screen.get_width() // 2 - text.get_width() // 2, + menu.screen.get_height() // 2, + ), + ) + pygame.display.flip() + pygame.time.wait(2000) + + score = 0 + elif ( + INSTANCE == "secondary" + and not is_joint_mode[0] + and command == Command.SELECT_GAME.value + ): result = handle_game_selection(message, is_secondary=True) if result == 0: @@ -95,24 +136,43 @@ def main() -> int: except Exception as e: logger.error(f"error processing serial data: {e}") - serial_command = menu.process_events(serial, INSTANCE) + if INSTANCE == "secondary" and is_joint_mode[0]: + menu.screen.fill(BLACK) - if serial_command: - serial.write(json.dumps(serial_command).encode("utf-8")) + text = font.render(f"Score: {score}", True, WHITE) - if serial_command.get("command") == Command.QUIT.value: - logger.info("primary sent quit, shutting down") - pygame.quit() + menu.screen.blit( + text, + ( + menu.screen.get_width() // 2 - text.get_width() // 2, + menu.screen.get_height() // 2, + ), + ) + pygame.display.flip() + pygame.time.delay(100) + else: + serial_command = menu.process_events(serial, INSTANCE, is_joint_mode) - return 0 - elif serial_command.get("command") == Command.SELECT_GAME.value: - result = handle_game_selection(serial_command) + if serial_command: + if not is_joint_mode[0] or INSTANCE == "primary": + serial.write(json.dumps(serial_command).encode("utf-8")) + + if serial_command.get("command") == Command.QUIT.value: + pygame.quit() - if result == 0: return 0 + elif serial_command.get("command") == Command.SELECT_GAME.value: + result = handle_game_selection(serial_command) - menu.draw_menu() + if result == 0: + return 0 + + menu.draw_menu() pygame.quit() return 0 + + +if __name__ == "__main__": + main() diff --git a/src/splitscreen_duo/command.py b/src/splitscreen_duo/command.py index df70b93..79cd716 100644 --- a/src/splitscreen_duo/command.py +++ b/src/splitscreen_duo/command.py @@ -5,3 +5,7 @@ class Command(Enum): QUIT = 0 SELECT_GAME = 1 SCORE = 2 + STATS = 3 + GAME_ENDED = 4 + ENTER_JOINT_MODE = 5 + EXIT_JOINT_MODE = 6 diff --git a/src/splitscreen_duo/games/breakout.py b/src/splitscreen_duo/games/breakout.py index 734b394..ef369de 100644 --- a/src/splitscreen_duo/games/breakout.py +++ b/src/splitscreen_duo/games/breakout.py @@ -21,8 +21,8 @@ logger = logging.getLogger(__name__) class Breakout(GameBase): - def __init__(self, screen, serial, instance): - super().__init__(screen, serial, instance) + def __init__(self, screen, serial, instance, is_joint_mode=False): + super().__init__(screen, serial, instance, is_joint_mode) self.screen_width = screen.get_width() self.screen_height = screen.get_height() @@ -65,6 +65,7 @@ class Breakout(GameBase): if self.ball[0] <= BALL_SIZE or self.ball[0] >= self.screen_width - BALL_SIZE: self.ball_dx *= -1 + if self.ball[1] <= BALL_SIZE: self.ball_dy *= -1 @@ -90,6 +91,9 @@ class Breakout(GameBase): self.bricks.remove(brick) self.score += 1 + + self.send_stats(self.score) + self.ball_dy *= -1 logger.debug(f"brick hit, score: {self.score}") @@ -156,9 +160,11 @@ class Breakout(GameBase): for brick in self.bricks: pygame.draw.rect(self.screen, RED, brick) - score_text = self.font.render(f"Score: {self.score}", True, WHITE) + if not self.is_joint_mode or self.instance != "primary": + score_text = self.font.render(f"Score: {self.score}", True, WHITE) + + self.screen.blit(score_text, (10, 10)) - self.screen.blit(score_text, (10, 10)) pygame.display.flip() clock.tick(60) diff --git a/src/splitscreen_duo/games/game_base.py b/src/splitscreen_duo/games/game_base.py index 547b27e..8ef02ec 100644 --- a/src/splitscreen_duo/games/game_base.py +++ b/src/splitscreen_duo/games/game_base.py @@ -9,10 +9,11 @@ logger = logging.getLogger(__name__) class GameBase: - def __init__(self, screen, serial, instance): + def __init__(self, screen, serial, instance, is_joint_mode=False): self.screen = screen self.serial = serial self.instance = instance + self.is_joint_mode = is_joint_mode self.font = pygame.font.Font(None, 36) self.is_running = True self.opponent_dead = False @@ -37,7 +38,6 @@ class GameBase: def check_serial(self): if self.serial.in_waiting() > 0: data = self.serial.readline().decode("utf-8").strip() - logger.debug(f"received serial data during check_serial: {data}") message = json.loads(data) @@ -53,26 +53,50 @@ class GameBase: return False def end_game(self, score): - self.my_score = score + if self.is_joint_mode: + self.is_running = False + + if self.instance == "primary": + self.serial.write( + json.dumps({"command": Command.GAME_ENDED.value}).encode("utf-8") + + b"\n" + ) + else: + self.my_score = score - logger.debug(f"ending game, sending score: {self.my_score}") + self.serial.write( + json.dumps( + { + "command": Command.SCORE.value, + "action": None, + "value": self.my_score, + } + ).encode("utf-8") + + b"\n" + ) - self.serial.write( - json.dumps( - {"command": Command.SCORE.value, "action": None, "value": self.my_score} - ).encode("utf-8") - ) + if self.opponent_dead: + self.score_display_time = pygame.time.get_ticks() - if self.opponent_dead: - self.score_display_time = pygame.time.get_ticks() + logger.debug("opponent already dead, starting score display") + else: + self.waiting = True - logger.debug("opponent already dead, starting score display") - else: - self.waiting = True + logger.debug("waiting for opponent to finish") - logger.debug("waiting for opponent to finish") + def send_stats(self, value): + if self.is_joint_mode and self.instance == "primary": + self.serial.write( + json.dumps({"command": Command.STATS.value, "value": value}).encode( + "utf-8" + ) + + b"\n" + ) def update(self): + if self.is_joint_mode: + return None + if self.waiting: self.screen.fill(self.BLACK) @@ -108,7 +132,6 @@ class GameBase: "action": None, "value": None, } - elif self.score_display_time: self.screen.fill(self.BLACK) diff --git a/src/splitscreen_duo/games/pong.py b/src/splitscreen_duo/games/pong.py index 7f0d4e9..464e529 100644 --- a/src/splitscreen_duo/games/pong.py +++ b/src/splitscreen_duo/games/pong.py @@ -17,9 +17,8 @@ logger = logging.getLogger(__name__) class Pong(GameBase): - def __init__(self, screen, serial, instance): - super().__init__(screen, serial, instance) - + def __init__(self, screen, serial, instance, is_joint_mode=False): + super().__init__(screen, serial, instance, is_joint_mode) self.screen_width = screen.get_width() self.screen_height = screen.get_height() self.player_score = 0 @@ -33,14 +32,8 @@ class Pong(GameBase): self.screen_width // 2 - PADDLE_WIDTH // 2, self.screen_height - 40, ] - self.ai_paddle = [ - self.screen_width // 2 - PADDLE_WIDTH // 2, - 40, - ] - self.ball = [ - self.screen_width // 2, - self.screen_height // 2, - ] + self.ai_paddle = [self.screen_width // 2 - PADDLE_WIDTH // 2, 40] + self.ball = [self.screen_width // 2, self.screen_height // 2] self.ball_dx = random.choice([-BALL_SPEED, BALL_SPEED]) self.ball_dy = BALL_SPEED @@ -61,6 +54,7 @@ class Pong(GameBase): if self.ai_paddle[0] < 0: self.ai_paddle[0] = 0 + if self.ai_paddle[0] > self.screen_width - PADDLE_WIDTH: self.ai_paddle[0] = self.screen_width - PADDLE_WIDTH @@ -72,16 +66,10 @@ class Pong(GameBase): self.ball_dx *= -1 player_rect = pygame.Rect( - self.player_paddle[0], - self.player_paddle[1], - PADDLE_WIDTH, - PADDLE_HEIGHT, + self.player_paddle[0], self.player_paddle[1], PADDLE_WIDTH, PADDLE_HEIGHT ) ai_rect = pygame.Rect( - self.ai_paddle[0], - self.ai_paddle[1], - PADDLE_WIDTH, - PADDLE_HEIGHT, + self.ai_paddle[0], self.ai_paddle[1], PADDLE_WIDTH, PADDLE_HEIGHT ) ball_rect = pygame.Rect( self.ball[0] - BALL_SIZE, @@ -102,17 +90,16 @@ class Pong(GameBase): if self.ball[1] <= 0: self.player_score += 1 + self.send_stats(self.player_score) self.reset_ball() elif self.ball[1] >= self.screen_height: self.ai_score += 1 + self.send_stats(self.player_score) self.reset_ball() def reset_ball(self): - self.ball = [ - self.screen_width // 2, - self.screen_height // 2, - ] + self.ball = [self.screen_width // 2, self.screen_height // 2] self.ball_dx = random.choice([-BALL_SPEED, BALL_SPEED]) self.ball_dy = random.choice([-BALL_SPEED, BALL_SPEED]) @@ -201,9 +188,10 @@ class Pong(GameBase): game_over = self.check_game_over() if game_over: - self.score_display_time = pygame.time.get_ticks() + if self.is_joint_mode and self.instance == "primary": + self.send_stats(self.player_score) - logger.debug(f"game over: {game_over}, starting score display") + self.end_game(self.player_score) continue @@ -230,13 +218,17 @@ class Pong(GameBase): BALL_SIZE, ) - player_score_text = self.font.render( - f"Player: {self.player_score}", True, WHITE - ) - ai_score_text = self.font.render(f"AI: {self.ai_score}", True, WHITE) + if not self.is_joint_mode or self.instance != "primary": + player_score_text = self.font.render( + f"Player: {self.player_score}", True, WHITE + ) + ai_score_text = self.font.render( + f"AI: {self.ai_score}", True, WHITE + ) + + self.screen.blit(player_score_text, (10, self.screen_height - 40)) + self.screen.blit(ai_score_text, (10, 10)) - self.screen.blit(player_score_text, (10, self.screen_height - 40)) - self.screen.blit(ai_score_text, (10, 10)) pygame.display.flip() clock.tick(60) diff --git a/src/splitscreen_duo/games/snake.py b/src/splitscreen_duo/games/snake.py index 51c0bcc..b593ea1 100644 --- a/src/splitscreen_duo/games/snake.py +++ b/src/splitscreen_duo/games/snake.py @@ -15,8 +15,8 @@ logger = logging.getLogger(__name__) class Snake(GameBase): - def __init__(self, screen, serial, instance): - super().__init__(screen, serial, instance) + def __init__(self, screen, serial, instance, is_joint_mode=False): + super().__init__(screen, serial, instance, is_joint_mode) self.screen_width = screen.get_width() self.screen_height = screen.get_height() @@ -73,6 +73,9 @@ class Snake(GameBase): ) self.score += 1 + + self.send_stats(self.score) + self.food = self.spawn_food() else: self.body.pop() @@ -117,6 +120,11 @@ class Snake(GameBase): self.check_serial() self.move() + if self.check_collision(): + self.end_game(self.score) + + continue + if self.check_collision(): self.end_game(self.score) @@ -137,9 +145,11 @@ class Snake(GameBase): [self.food[0], self.food[1], BLOCK_SIZE, BLOCK_SIZE], ) - score_text = self.font.render(f"Score: {self.score}", True, WHITE) + if not self.is_joint_mode or self.instance != "primary": + score_text = self.font.render(f"Score: {self.score}", True, WHITE) + + self.screen.blit(score_text, (10, 10)) - self.screen.blit(score_text, (10, 10)) pygame.display.flip() clock.tick(SPEED) diff --git a/src/splitscreen_duo/menu.py b/src/splitscreen_duo/menu.py index a7bda82..9517d3d 100644 --- a/src/splitscreen_duo/menu.py +++ b/src/splitscreen_duo/menu.py @@ -17,6 +17,7 @@ WIDTH = HEIGHT = FONT = screen = selected_index = None def init_display(): global WIDTH, HEIGHT, FONT, screen, selected_index + WIDTH, HEIGHT = ( pygame.display.Info().current_w // 2, pygame.display.Info().current_h // 2, @@ -40,7 +41,7 @@ def draw_menu(): pygame.display.flip() -def process_events(serial, instance): +def process_events(serial, instance, is_joint_mode): global selected_index input_handler = Input(debug=IS_DEVELOPMENT_MODE) @@ -75,4 +76,22 @@ def process_events(serial, instance): elif action == "QUIT": return {"command": Command.QUIT.value, "action": None, "value": None} + if ( + event.type == pygame.KEYDOWN + and event.key == pygame.K_b + and instance == "primary" + and IS_DEVELOPMENT_MODE + ): + is_joint_mode[0] = not is_joint_mode[0] + + logger.info(f"Toggled joint mode to {is_joint_mode[0]}") + + command = ( + Command.ENTER_JOINT_MODE.value + if is_joint_mode[0] + else Command.EXIT_JOINT_MODE.value + ) + + serial.write(json.dumps({"command": command}).encode("utf-8")) + return None -- cgit v1.2.3