diff options
| -rw-r--r-- | src/splitscreen_duo/games/benchmark.py | 50 | ||||
| -rw-r--r-- | src/splitscreen_duo/games/breakout.py | 57 | ||||
| -rw-r--r-- | src/splitscreen_duo/games/game_base.py | 120 | ||||
| -rw-r--r-- | src/splitscreen_duo/games/pong.py | 99 | ||||
| -rw-r--r-- | src/splitscreen_duo/games/snake.py | 56 | ||||
| -rw-r--r-- | src/splitscreen_duo/input.py | 50 | ||||
| -rw-r--r-- | src/splitscreen_duo/menu.py | 45 |
7 files changed, 307 insertions, 170 deletions
diff --git a/src/splitscreen_duo/games/benchmark.py b/src/splitscreen_duo/games/benchmark.py index efe48c3..13b14f7 100644 --- a/src/splitscreen_duo/games/benchmark.py +++ b/src/splitscreen_duo/games/benchmark.py @@ -1,10 +1,12 @@ import pygame import random +import os from splitscreen_duo.command import Command BLACK = (0, 0, 0) WHITE = (255, 255, 255) +ROTATION_ENABLED = os.getenv("DEVELOPMENT", "") in ["", "0"] BALL_SIZE = 25 @@ -16,10 +18,34 @@ class Ball: self.change_y = random.randrange(-2, 3) +def draw_game(surface, ball_list, fps, balls, is_vertical, screen_width, screen_height): + surface.fill(BLACK) + + for ball in ball_list: + pygame.draw.circle(surface, WHITE, [int(ball.x), int(ball.y)], BALL_SIZE) + + fps_text = pygame.font.render(f"FPS: {fps:.1f}", True, WHITE) + ball_text = pygame.font.render(f"Balls: {balls}", True, WHITE) + + if is_vertical: + surface.blit(fps_text, (screen_width // 2 - fps_text.get_width() // 2, 10)) + surface.blit(ball_text, (screen_width // 2 - ball_text.get_width() // 2, 40)) + else: + surface.blit(fps_text, (10, 10)) + surface.blit(ball_text, (10, 40)) + def main_loop(screen, serial): clock = pygame.time.Clock() screen_width = screen.get_width() screen_height = screen.get_height() + instance = os.getenv("INSTANCE", "Unknown") + rotation_angle = 0 + + if ROTATION_ENABLED: + if instance == "primary": + rotation_angle = 90 # Right is down + elif instance == "secondary": + rotation_angle = -90 # Left is down global BALL_SIZE @@ -76,21 +102,23 @@ def main_loop(screen, serial): if b.y > screen_height - BALL_SIZE: b.y = screen_height - BALL_SIZE - screen.fill(BLACK) + fps = clock.get_fps() - for ball in ball_list: - pygame.draw.circle(screen, WHITE, [int(ball.x), int(ball.y)], BALL_SIZE) + if ROTATION_ENABLED and rotation_angle != 0 and not is_vertical: + render_surface = pygame.Surface((screen_width, screen_height)) - fps = clock.get_fps() - fps_text = font.render(f"FPS: {fps:.1f}", True, WHITE) - ball_text = font.render(f"Balls: {balls}", True, WHITE) + draw_game(render_surface, ball_list, fps, balls, is_vertical, screen_width, screen_height) + + rotated = pygame.transform.rotate(render_surface, rotation_angle) + + screen.fill(BLACK) + + pos_x = (screen_width - rotated.get_width()) // 2 + pos_y = (screen_height - rotated.get_height()) // 2 - if is_vertical: - screen.blit(fps_text, (screen_width // 2 - fps_text.get_width() // 2, 10)) - screen.blit(ball_text, (screen_width // 2 - ball_text.get_width() // 2, 40)) + screen.blit(rotated, (pos_x, pos_y)) else: - screen.blit(fps_text, (10, 10)) - screen.blit(ball_text, (10, 40)) + draw_game(screen, ball_list, fps, balls, is_vertical, screen_width, screen_height) pygame.display.flip() # clock.tick(60) diff --git a/src/splitscreen_duo/games/breakout.py b/src/splitscreen_duo/games/breakout.py index 6a5e2cc..8506b75 100644 --- a/src/splitscreen_duo/games/breakout.py +++ b/src/splitscreen_duo/games/breakout.py @@ -124,6 +124,31 @@ class Breakout(GameBase): return None + def draw_game(self, surface): + surface.fill(BLACK) + pygame.draw.rect( + surface, + BLUE, + [self.paddle[0], self.paddle[1], PADDLE_WIDTH, PADDLE_HEIGHT], + ) + pygame.draw.circle( + surface, + WHITE, + [int(self.ball[0]), int(self.ball[1])], + BALL_SIZE, + ) + + for brick in self.bricks: + pygame.draw.rect(surface, RED, brick) + + if not self.is_joint_mode or self.instance != "primary": + score_text = self.font.render(f"Score: {self.score}", True, WHITE) + + if self.is_vertical: + surface.blit(score_text, (self.screen_width // 2 - score_text.get_width() // 2, 10)) + else: + surface.blit(score_text, (10, 10)) + def main_loop(self): clock = pygame.time.Clock() @@ -135,11 +160,10 @@ class Breakout(GameBase): if not (self.waiting or self.score_display_time): for event in pygame.event.get(): - result = self.handle_common_events(event) - action = self.input_handler.get_input(event) + action = self.handle_common_events(event) - if result is not None: - return result + if isinstance(action, dict) and action.get("command") is not None: + return action if action == "LEFT": self.move_paddle(-20) @@ -159,30 +183,7 @@ class Breakout(GameBase): continue - self.screen.fill(BLACK) - pygame.draw.rect( - self.screen, - BLUE, - [self.paddle[0], self.paddle[1], PADDLE_WIDTH, PADDLE_HEIGHT], - ) - pygame.draw.circle( - self.screen, - WHITE, - [int(self.ball[0]), int(self.ball[1])], - BALL_SIZE, - ) - - for brick in self.bricks: - pygame.draw.rect(self.screen, RED, brick) - - if not self.is_joint_mode or self.instance != "primary": - score_text = self.font.render(f"Score: {self.score}", True, WHITE) - - if self.is_vertical: - self.screen.blit(score_text, (self.screen_width // 2 - score_text.get_width() // 2, 10)) - else: - self.screen.blit(score_text, (10, 10)) - + self.render_rotated(self.draw_game) 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 a539ba0..20387c2 100644 --- a/src/splitscreen_duo/games/game_base.py +++ b/src/splitscreen_duo/games/game_base.py @@ -7,6 +7,8 @@ import os logger = logging.getLogger(__name__) +ROTATION_ENABLED = os.getenv("DEVELOPMENT", "") in ["", "0"] + class GameBase: def __init__(self, screen, serial, instance, is_joint_mode=False): @@ -23,20 +25,29 @@ class GameBase: self.score_display_time = 0 self.BLACK = (0, 0, 0) self.WHITE = (255, 255, 255) - self.input_handler = Input(debug=os.getenv("DEVELOPMENT", "").lower() != "") self.screen_width = screen.get_width() self.screen_height = screen.get_height() self.is_vertical = is_joint_mode + self.rotation_angle = 0 + self.rotated_surface = None + + if ROTATION_ENABLED and not is_joint_mode: + if instance == "primary": + self.rotation_angle = 90 # Right is down + elif instance == "secondary": + self.rotation_angle = -90 # Left is down + + self.input_handler = Input(debug=os.getenv("DEVELOPMENT", "").lower() != "") def handle_common_events(self, event): - action = self.input_handler.get_input(event) + action = self.input_handler.get_input(event, self.rotation_angle, self.is_joint_mode) if event.type == pygame.QUIT: return {"command": Command.QUIT.value, "action": None, "value": None} elif event.type == pygame.KEYDOWN and action == "QUIT": return None - return None + return action def check_serial(self): if self.serial.in_waiting() > 0: @@ -103,10 +114,56 @@ class GameBase: if (current_width != self.screen_width or current_height != self.screen_height): self.screen_width = current_width self.screen_height = current_height - self.is_vertical = current_height > current_width * 1.5 + self.is_vertical = self.is_joint_mode or current_height > current_width * 1.5 + + if ROTATION_ENABLED and not self.is_joint_mode: + if self.instance == "primary": + self.rotation_angle = 90 + elif self.instance == "secondary": + self.rotation_angle = -90 + else: + self.rotation_angle = 0 self.reset() - logger.debug(f"screen dimensions updated: {self.screen_width}x{self.screen_height}, vertical: {self.is_vertical}") + logger.debug(f"screen dimensions updated: {self.screen_width}x{self.screen_height}, vertical: {self.is_vertical}, rotation: {self.rotation_angle}°") + + def draw_waiting_screen(self, surface): + surface.fill(self.BLACK) + + text = self.font.render("Waiting for opponent ...", True, self.WHITE) + + surface.blit( + text, + ( + self.screen_width // 2 - text.get_width() // 2, + self.screen_height // 2, + ), + ) + + def draw_score_display(self, surface): + surface.fill(self.BLACK) + + my_text = self.font.render(f"Your Score: {self.my_score}", True, self.WHITE) + opp_text = self.font.render( + f"Your Opponent's Score: {self.opponent_score}", True, self.WHITE + ) + + spacing = 30 if self.is_vertical else 20 + + surface.blit( + my_text, + ( + self.screen_width // 2 - my_text.get_width() // 2, + self.screen_height // 2 - spacing, + ), + ) + surface.blit( + opp_text, + ( + self.screen_width // 2 - opp_text.get_width() // 2, + self.screen_height // 2 + spacing, + ), + ) def update(self): self.update_screen_dimensions() @@ -115,17 +172,7 @@ class GameBase: return None if self.waiting: - self.screen.fill(self.BLACK) - - text = self.font.render("Waiting for opponent ...", True, self.WHITE) - - self.screen.blit( - text, - ( - self.screen.get_width() // 2 - text.get_width() // 2, - self.screen.get_height() // 2, - ), - ) + self.render_rotated(self.draw_waiting_screen) pygame.display.flip() if self.serial.in_waiting() > 0: @@ -150,27 +197,7 @@ class GameBase: "value": None, } elif self.score_display_time: - self.screen.fill(self.BLACK) - - my_text = self.font.render(f"Your Score: {self.my_score}", True, self.WHITE) - opp_text = self.font.render( - f"Your Opponent's Score: {self.opponent_score}", True, self.WHITE - ) - - self.screen.blit( - my_text, - ( - self.screen.get_width() // 2 - my_text.get_width() // 2, - self.screen.get_height() // 2 - 20, - ), - ) - self.screen.blit( - opp_text, - ( - self.screen.get_width() // 2 - opp_text.get_width() // 2, - self.screen.get_height() // 2 + 20, - ), - ) + self.render_rotated(self.draw_score_display) pygame.display.flip() if pygame.time.get_ticks() - self.score_display_time > 3000: @@ -182,5 +209,24 @@ class GameBase: return None + def render_rotated(self, draw_function): + if ROTATION_ENABLED and not self.is_joint_mode and self.rotation_angle != 0: + render_surface = pygame.Surface((self.screen_width, self.screen_height)) + + render_surface.fill(self.BLACK) + + draw_function(render_surface) + + rotated = pygame.transform.rotate(render_surface, self.rotation_angle) + + self.screen.fill(self.BLACK) + + pos_x = (self.screen_width - rotated.get_width()) // 2 + pos_y = (self.screen_height - rotated.get_height()) // 2 + + self.screen.blit(rotated, (pos_x, pos_y)) + else: + draw_function(self.screen) + def main_loop(self): raise NotImplementedError diff --git a/src/splitscreen_duo/games/pong.py b/src/splitscreen_duo/games/pong.py index d82cdc7..c912197 100644 --- a/src/splitscreen_duo/games/pong.py +++ b/src/splitscreen_duo/games/pong.py @@ -127,6 +127,52 @@ class Pong(GameBase): return None + def draw_game(self, surface): + surface.fill(BLACK) + pygame.draw.rect( + surface, + WHITE, + [ + self.player_paddle[0], + self.player_paddle[1], + PADDLE_WIDTH, + PADDLE_HEIGHT, + ], + ) + pygame.draw.rect( + surface, + WHITE, + [self.ai_paddle[0], self.ai_paddle[1], PADDLE_WIDTH, PADDLE_HEIGHT], + ) + pygame.draw.circle( + surface, + WHITE, + [int(self.ball[0]), int(self.ball[1])], + BALL_SIZE, + ) + + 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 + ) + + if self.is_vertical: + surface.blit( + player_score_text, + (self.screen_width // 2 - player_score_text.get_width() // 2, + self.screen_height - 40) + ) + surface.blit( + ai_score_text, + (self.screen_width // 2 - ai_score_text.get_width() // 2, 10) + ) + else: + surface.blit(player_score_text, (10, self.screen_height - 40)) + surface.blit(ai_score_text, (10, 10)) + def update(self): self.update_screen_dimensions() @@ -182,11 +228,10 @@ class Pong(GameBase): if not self.score_display_time: for event in pygame.event.get(): - result = self.handle_common_events(event) - action = self.input_handler.get_input(event) + action = self.handle_common_events(event) - if result is not None: - return result + if isinstance(action, dict) and action.get("command") is not None: + return action if action == "LEFT": self.move_player_paddle(-PADDLE_SPEED) @@ -213,51 +258,7 @@ class Pong(GameBase): continue - self.screen.fill(BLACK) - pygame.draw.rect( - self.screen, - WHITE, - [ - self.player_paddle[0], - self.player_paddle[1], - PADDLE_WIDTH, - PADDLE_HEIGHT, - ], - ) - pygame.draw.rect( - self.screen, - WHITE, - [self.ai_paddle[0], self.ai_paddle[1], PADDLE_WIDTH, PADDLE_HEIGHT], - ) - pygame.draw.circle( - self.screen, - WHITE, - [int(self.ball[0]), int(self.ball[1])], - BALL_SIZE, - ) - - 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 - ) - - if self.is_vertical: - self.screen.blit( - player_score_text, - (self.screen_width // 2 - player_score_text.get_width() // 2, - self.screen_height - 40) - ) - self.screen.blit( - ai_score_text, - (self.screen_width // 2 - ai_score_text.get_width() // 2, 10) - ) - else: - self.screen.blit(player_score_text, (10, self.screen_height - 40)) - self.screen.blit(ai_score_text, (10, 10)) - + self.render_rotated(self.draw_game) pygame.display.flip() clock.tick(60) diff --git a/src/splitscreen_duo/games/snake.py b/src/splitscreen_duo/games/snake.py index 11b66f7..fe34143 100644 --- a/src/splitscreen_duo/games/snake.py +++ b/src/splitscreen_duo/games/snake.py @@ -94,6 +94,30 @@ class Snake(GameBase): or head in self.body[1:] ) + def draw_game(self, surface): + surface.fill(BLACK) + + for segment in self.body: + pygame.draw.rect( + surface, + GREEN, + [segment[0], segment[1], BLOCK_SIZE, BLOCK_SIZE], + ) + + pygame.draw.rect( + surface, + RED, + [self.food[0], self.food[1], BLOCK_SIZE, BLOCK_SIZE], + ) + + if not self.is_joint_mode or self.instance != "primary": + score_text = self.font.render(f"Score: {self.score}", True, WHITE) + + if self.is_vertical: + surface.blit(score_text, (self.screen_width // 2 - score_text.get_width() // 2, 10)) + else: + surface.blit(score_text, (10, 10)) + def main_loop(self): clock = pygame.time.Clock() @@ -105,11 +129,10 @@ class Snake(GameBase): if not (self.waiting or self.score_display_time): for event in pygame.event.get(): - result = self.handle_common_events(event) - action = self.input_handler.get_input(event) + action = self.handle_common_events(event) - if result is not None: - return result + if isinstance(action, dict) and action.get("command") is not None: + return action if action == "UP": self.change_to = "UP" @@ -125,32 +148,9 @@ class Snake(GameBase): if self.check_collision(): self.end_game(self.score) - continue - self.screen.fill(BLACK) - - for segment in self.body: - pygame.draw.rect( - self.screen, - GREEN, - [segment[0], segment[1], BLOCK_SIZE, BLOCK_SIZE], - ) - - pygame.draw.rect( - self.screen, - RED, - [self.food[0], self.food[1], BLOCK_SIZE, BLOCK_SIZE], - ) - - if not self.is_joint_mode or self.instance != "primary": - score_text = self.font.render(f"Score: {self.score}", True, WHITE) - - if self.is_vertical: - self.screen.blit(score_text, (self.screen_width // 2 - score_text.get_width() // 2, 10)) - else: - self.screen.blit(score_text, (10, 10)) - + self.render_rotated(self.draw_game) pygame.display.flip() clock.tick(SPEED) diff --git a/src/splitscreen_duo/input.py b/src/splitscreen_duo/input.py index 29f97cb..f5a8233 100644 --- a/src/splitscreen_duo/input.py +++ b/src/splitscreen_duo/input.py @@ -1,4 +1,7 @@ import pygame +import os + +ROTATION_ENABLED = os.getenv("DEVELOPMENT", "") in ["", "0"] class Input: @@ -6,6 +9,7 @@ class Input: pygame.joystick.init() self.controller = None + self.instance = os.getenv("INSTANCE", "Unknown") if pygame.joystick.get_count() > 0: self.controller = pygame.joystick.Joystick(0) @@ -17,14 +21,18 @@ class Input: self.prev_axis_states = {"x": 0, "y": 0} self.prev_hat_states = (0, 0) # D-pad - def get_input(self, event): + def get_input(self, event, rotation_angle=0, is_joint_mode=False): + input_action = None + if self.controller: - controller_input = self._get_controller_input(event) + input_action = self._get_controller_input(event) + + if input_action: + return self._apply_rotation(input_action, rotation_angle, is_joint_mode) - if controller_input: - return controller_input + input_action = self._get_keyboard_input(event) - return self._get_keyboard_input(event) + return self._apply_rotation(input_action, rotation_angle, is_joint_mode) def _get_keyboard_input(self, event): if event.type == pygame.KEYDOWN: @@ -54,10 +62,10 @@ class Input: y = self.controller.get_axis(1) # Y-axis if x <= -0.1: - return "RIGHT" + return "LEFT" if x >= 0.1: - return "LEFT" + return "RIGHT" if y <= -0.1: return "UP" @@ -122,3 +130,31 @@ class Input: self.prev_button_states = self.button_states.copy() return None + + def _apply_rotation(self, action, rotation_angle, is_joint_mode): + if not action or not ROTATION_ENABLED or is_joint_mode or rotation_angle == 0: + return action + + # Primary: 90 degrees, right is down + if self.instance == "primary" and rotation_angle == 90: + if action == "RIGHT": + return "DOWN" + elif action == "LEFT": + return "UP" + elif action == "UP": + return "LEFT" + elif action == "DOWN": + return "RIGHT" + + # Secondary, -90 degrees, left is down + elif self.instance == "secondary" and rotation_angle == -90: + if action == "RIGHT": + return "UP" + elif action == "LEFT": + return "DOWN" + elif action == "UP": + return "RIGHT" + elif action == "DOWN": + return "LEFT" + + return action diff --git a/src/splitscreen_duo/menu.py b/src/splitscreen_duo/menu.py index bd292e7..66c4573 100644 --- a/src/splitscreen_duo/menu.py +++ b/src/splitscreen_duo/menu.py @@ -9,11 +9,14 @@ import json WHITE = (255, 255, 255) BLACK = (0, 0, 0) -IS_DEVELOPMENT_MODE = os.getenv("DEVELOPMENT", "").lower() != "" +IS_DEVELOPMENT_MODE = os.getenv("DEVELOPMENT", "") not in ["", "0"] +ROTATION_ENABLED = not IS_DEVELOPMENT_MODE OPTIONS = Game.all() + ["Quit"] logger = logging.getLogger(__name__) WIDTH = HEIGHT = FONT = screen = selected_index = None ORIGINAL_WIDTH = ORIGINAL_HEIGHT = None +rotated_surface = None +rotation_angle = 0 def init_display(): @@ -31,30 +34,52 @@ def init_display(): def draw_menu(is_joint_mode=False): - screen.fill(WHITE) + global rotated_surface - is_vertical = HEIGHT > WIDTH * 1.5 + base_surface = pygame.Surface((WIDTH, HEIGHT)) + + base_surface.fill(WHITE) + + is_vertical = is_joint_mode start_y = 100 if is_vertical else 150 spacing = 40 if is_vertical else 50 - title_text = FONT.render("SplitScreen Duo", True, BLACK) - screen.blit(title_text, (WIDTH // 2 - title_text.get_width() // 2, start_y - 50)) + title_text = FONT.render("SplitScreen Duo", True, BLACK) + base_surface.blit(title_text, (WIDTH // 2 - title_text.get_width() // 2, start_y - 50)) for i, option in enumerate(OPTIONS): text = FONT.render(option, True, BLACK if i != selected_index else (200, 0, 0)) + base_surface.blit(text, (WIDTH // 2 - text.get_width() // 2, start_y + i * spacing)) - screen.blit(text, (WIDTH // 2 - text.get_width() // 2, start_y + i * spacing)) + if ROTATION_ENABLED and not is_joint_mode and rotation_angle != 0: + rotated_surface = pygame.transform.rotate(base_surface, rotation_angle) + pos_x = (WIDTH - rotated_surface.get_width()) // 2 + pos_y = (HEIGHT - rotated_surface.get_height()) // 2 + + screen.fill(WHITE) + screen.blit(rotated_surface, (pos_x, pos_y)) + else: + screen.blit(base_surface, (0, 0)) pygame.display.flip() def set_screen_orientation(is_joint_mode): - global WIDTH, HEIGHT, screen, ORIGINAL_WIDTH, ORIGINAL_HEIGHT + global WIDTH, HEIGHT, screen, ORIGINAL_WIDTH, ORIGINAL_HEIGHT, rotation_angle + + instance = os.getenv("INSTANCE", "Unknown") + rotation_angle = 0 # Reset rotation angle if is_joint_mode: WIDTH = ORIGINAL_WIDTH HEIGHT = ORIGINAL_HEIGHT * 2 else: + if ROTATION_ENABLED and not is_joint_mode: + if instance == "primary": + rotation_angle = 90 # Primary: Right is down + elif instance == "secondary": + rotation_angle = -90 # Secondary: Left is down + WIDTH = ORIGINAL_WIDTH HEIGHT = ORIGINAL_HEIGHT @@ -63,18 +88,19 @@ def set_screen_orientation(is_joint_mode): pygame.RESIZABLE if IS_DEVELOPMENT_MODE else pygame.FULLSCREEN, ) - logger.info(f"screen orientation changed to {'vertical' if is_joint_mode else 'horizontal'}") + logger.info(f"screen orientation changed: is_joint={is_joint_mode}, rotation={rotation_angle}, instance={instance}") def process_events(serial, instance, is_joint_mode): global selected_index input_handler = Input(debug=IS_DEVELOPMENT_MODE) + is_joint_mode_value = is_joint_mode[0] if isinstance(is_joint_mode, list) else is_joint_mode for event in pygame.event.get(): if event.type == pygame.QUIT: return {"command": Command.QUIT.value, "action": None, "value": None} - action = input_handler.get_input(event) + action = input_handler.get_input(event, rotation_angle, is_joint_mode_value) if action == "UP": selected_index = (selected_index - 1) % len(OPTIONS) @@ -104,7 +130,6 @@ def process_events(serial, instance, is_joint_mode): 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] |