aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorZoltan Szabatin <[email protected]>2025-03-02 18:38:27 -0800
committerZoltan Szabatin <[email protected]>2025-03-02 18:38:29 -0800
commit225f4a227013443b09a205cabe73886facab4d2b (patch)
tree939ef33031bb2c31cfc3eec96fbc7982ce585784 /src
parentrefactor: Move games to proper game enumeration (diff)
downloadsplitscreen-duo-225f4a227013443b09a205cabe73886facab4d2b.tar.xz
splitscreen-duo-225f4a227013443b09a205cabe73886facab4d2b.zip
feat: Add snake and benchmark games
Diffstat (limited to 'src')
-rw-r--r--src/splitscreen_duo/__init__.py51
-rw-r--r--src/splitscreen_duo/command.py1
-rw-r--r--src/splitscreen_duo/game.py16
-rw-r--r--src/splitscreen_duo/games/benchmark.py65
-rw-r--r--src/splitscreen_duo/games/snake.py229
-rw-r--r--src/splitscreen_duo/menu.py2
6 files changed, 361 insertions, 3 deletions
diff --git a/src/splitscreen_duo/__init__.py b/src/splitscreen_duo/__init__.py
index e47c7cb..8233caf 100644
--- a/src/splitscreen_duo/__init__.py
+++ b/src/splitscreen_duo/__init__.py
@@ -1,3 +1,4 @@
+from .game import Game
from . import menu
from .command import Command
import os
@@ -5,6 +6,8 @@ import logging
import pygame
from .serial import Serial
import json
+from .games import benchmark, snake
+
def main() -> int:
logger = logging.getLogger(__name__)
@@ -19,9 +22,9 @@ def main() -> int:
level=(logging.DEBUG if os.getenv("DEVELOPMENT", "").lower() else logging.INFO)
)
print("The Dual Screen Console")
- logger.info(f"running as {os.getenv("INSTANCE", "unknown")}")
+ logger.info(f"running as {os.getenv('INSTANCE', 'unknown')}")
menu.init_display()
-
+
is_running = True
while is_running:
@@ -38,6 +41,31 @@ def main() -> int:
pygame.quit()
return 0
+ elif message.get("command") == Command.SELECT_GAME.value:
+ if message.get("value") == Game.BENCHMARK.value:
+ logger.info("received benchmark game selection from primary")
+
+ game_result = benchmark.main_loop(menu.screen, serial)
+
+ if (
+ game_result
+ and game_result.get("command") == Command.QUIT.value
+ ):
+ pygame.quit()
+
+ return 0
+ elif message.get("value") == Game.SNAKE.value:
+ logger.info("received snake game selection from primary")
+
+ game_result = snake.main_loop(menu.screen, serial, INSTANCE)
+
+ if (
+ game_result
+ and game_result.get("command") == Command.QUIT.value
+ ):
+ pygame.quit()
+
+ return 0
except json.JSONDecodeError:
logger.error("failed to decode serial message")
@@ -55,6 +83,25 @@ def main() -> int:
pygame.quit()
return 0
+ elif serial_command.get("command") == Command.SELECT_GAME.value:
+ if serial_command.get("value") == Game.BENCHMARK.value:
+ logger.info("starting benchmark game")
+
+ game_result = benchmark.main_loop(menu.screen, serial)
+
+ if game_result and game_result.get("command") == Command.QUIT.value:
+ pygame.quit()
+
+ return 0
+ elif serial_command.get("value") == Game.SNAKE.value:
+ logger.info("starting snake game")
+
+ game_result = snake.main_loop(menu.screen, serial, INSTANCE)
+
+ if game_result and game_result.get("command") == Command.QUIT.value:
+ pygame.quit()
+
+ return 0
menu.draw_menu()
diff --git a/src/splitscreen_duo/command.py b/src/splitscreen_duo/command.py
index df42a5a..df70b93 100644
--- a/src/splitscreen_duo/command.py
+++ b/src/splitscreen_duo/command.py
@@ -4,3 +4,4 @@ from enum import Enum
class Command(Enum):
QUIT = 0
SELECT_GAME = 1
+ SCORE = 2
diff --git a/src/splitscreen_duo/game.py b/src/splitscreen_duo/game.py
new file mode 100644
index 0000000..4c5ea45
--- /dev/null
+++ b/src/splitscreen_duo/game.py
@@ -0,0 +1,16 @@
+from enum import Enum
+
+
+class Game(Enum):
+ BREAKOUT = "Breakout"
+ PONG = "Pong v.s. Computer"
+ SNAKE = "Snake"
+ BENCHMARK = "Benchmark"
+
+ def all():
+ return [
+ Game.BREAKOUT.value,
+ Game.PONG.value,
+ Game.SNAKE.value,
+ Game.BENCHMARK.value,
+ ]
diff --git a/src/splitscreen_duo/games/benchmark.py b/src/splitscreen_duo/games/benchmark.py
new file mode 100644
index 0000000..3c8ea81
--- /dev/null
+++ b/src/splitscreen_duo/games/benchmark.py
@@ -0,0 +1,65 @@
+import pygame
+import random
+
+from splitscreen_duo.command import Command
+
+BLACK = (0, 0, 0)
+WHITE = (255, 255, 255)
+BALL_SIZE = 25
+
+
+class Ball:
+ def __init__(self, screen_width, screen_height):
+ self.x = random.randrange(BALL_SIZE, screen_width - BALL_SIZE)
+ self.y = random.randrange(BALL_SIZE, screen_height - BALL_SIZE)
+ self.change_x = random.randrange(-2, 3)
+ self.change_y = random.randrange(-2, 3)
+
+
+def main_loop(screen, serial):
+ clock = pygame.time.Clock()
+ ball_list = []
+ balls = 1
+ ball = Ball(screen.get_width(), screen.get_height())
+ is_running = True
+
+ ball_list.append(ball)
+
+ while is_running:
+ for event in pygame.event.get():
+ if event.type == pygame.QUIT:
+ return {"command": Command.QUIT.value, "action": None, "value": None}
+
+ elif event.type == pygame.KEYDOWN:
+ if event.key == pygame.K_SPACE:
+ balls = balls * 2
+
+ for _ in range(balls - 1):
+ ball = Ball(screen.get_width(), screen.get_height())
+
+ ball_list.append(ball)
+
+ elif event.key == pygame.K_ESCAPE:
+ return None
+
+ for ball in ball_list:
+ ball.x += ball.change_x
+ ball.y += ball.change_y
+
+ if ball.y > screen.get_height() - BALL_SIZE or ball.y < BALL_SIZE:
+ ball.change_y *= -1
+
+ if ball.x > screen.get_width() - BALL_SIZE or ball.x < BALL_SIZE:
+ ball.change_x *= -1
+
+ screen.fill(BLACK)
+
+ for ball in ball_list:
+ pygame.draw.circle(screen, WHITE, [int(ball.x), int(ball.y)], BALL_SIZE)
+
+ pygame.display.flip()
+ # clock.tick(60)
+ clock.tick()
+ # print(f"fps: {clock.get_fps():.2f}, balls: {balls}")
+
+ return None
diff --git a/src/splitscreen_duo/games/snake.py b/src/splitscreen_duo/games/snake.py
new file mode 100644
index 0000000..64c6edd
--- /dev/null
+++ b/src/splitscreen_duo/games/snake.py
@@ -0,0 +1,229 @@
+import pygame
+import random
+from ..command import Command
+import logging
+import json
+
+BLACK = (0, 0, 0)
+WHITE = (255, 255, 255)
+RED = (255, 0, 0)
+GREEN = (0, 255, 0)
+BLOCK_SIZE = 20
+SPEED = 15
+
+logger = logging.getLogger(__name__)
+
+
+class Snake:
+ def __init__(self, screen_width, screen_height):
+ self.screen_width = screen_width
+ self.screen_height = screen_height
+
+ self.reset()
+
+ def reset(self):
+ self.body = [
+ [
+ self.screen_width // 2 - self.screen_width // 2 % BLOCK_SIZE,
+ self.screen_height // 2 - self.screen_height // 2 % BLOCK_SIZE,
+ ]
+ ]
+ self.direction = "RIGHT"
+ self.change_to = self.direction
+ self.score = 0
+ self.food = self.spawn_food()
+
+ def spawn_food(self):
+ while True:
+ food = [
+ random.randrange(0, self.screen_width // BLOCK_SIZE) * BLOCK_SIZE,
+ random.randrange(0, self.screen_height // BLOCK_SIZE) * BLOCK_SIZE,
+ ]
+
+ if food not in self.body:
+ return food
+
+ def move(self):
+ if self.change_to == "UP" and self.direction != "DOWN":
+ self.direction = "UP"
+ if self.change_to == "DOWN" and self.direction != "UP":
+ self.direction = "DOWN"
+ if self.change_to == "LEFT" and self.direction != "RIGHT":
+ self.direction = "LEFT"
+ if self.change_to == "RIGHT" and self.direction != "LEFT":
+ self.direction = "RIGHT"
+
+ head = [self.body[0][0], self.body[0][1]]
+
+ if self.direction == "UP":
+ head[1] -= BLOCK_SIZE
+ if self.direction == "DOWN":
+ head[1] += BLOCK_SIZE
+ if self.direction == "LEFT":
+ head[0] -= BLOCK_SIZE
+ if self.direction == "RIGHT":
+ head[0] += BLOCK_SIZE
+
+ self.body.insert(0, head)
+
+ if head[0] == self.food[0] and head[1] == self.food[1]:
+ logger.debug(
+ f"snake ate food at {self.food}, score increased to {self.score + 1}"
+ )
+
+ self.score += 1
+ self.food = self.spawn_food()
+ else:
+ self.body.pop()
+
+ def check_collision(self):
+ head = self.body[0]
+
+ if (
+ head[0] < 0
+ or head[0] >= self.screen_width
+ or head[1] < 0
+ or head[1] >= self.screen_height
+ or head in self.body[1:]
+ ):
+ return True
+
+ return False
+
+
+def main_loop(screen, serial, instance):
+ clock = pygame.time.Clock()
+ snake = Snake(screen.get_width(), screen.get_height())
+ font = pygame.font.Font(None, 36)
+ is_running = True
+ opponent_dead = False
+ my_score = 0
+ opponent_score = 0
+ waiting = False
+ score_display_time = 0
+
+ while is_running:
+ if waiting:
+ screen.fill(BLACK)
+
+ text = font.render("Waiting for opponent...", True, WHITE)
+
+ screen.blit(
+ text,
+ (
+ screen.get_width() // 2 - text.get_width() // 2,
+ screen.get_height() // 2,
+ ),
+ )
+ pygame.display.flip()
+
+ if serial.in_waiting() > 0:
+ data = serial.readline().decode("utf-8").strip()
+ message = json.loads(data)
+
+ if message.get("command") == Command.SCORE.value:
+ opponent_score = message.get("value", 0)
+ waiting = False
+ score_display_time = pygame.time.get_ticks()
+ elif message.get("command") == Command.QUIT.value:
+ return {
+ "command": Command.QUIT.value,
+ "action": None,
+ "value": None,
+ }
+
+ elif score_display_time:
+ screen.fill(BLACK)
+
+ my_text = font.render(f"Your Score: {my_score}", True, WHITE)
+ opp_text = font.render(
+ f"Your Opponent's Score: {opponent_score}", True, WHITE
+ )
+
+ screen.blit(
+ my_text,
+ (
+ screen.get_width() // 2 - my_text.get_width() // 2,
+ screen.get_height() // 2 - 20,
+ ),
+ )
+ screen.blit(
+ opp_text,
+ (
+ screen.get_width() // 2 - opp_text.get_width() // 2,
+ screen.get_height() // 2 + 20,
+ ),
+ )
+ pygame.display.flip()
+
+ if pygame.time.get_ticks() - score_display_time > 3000:
+ return None
+
+ else:
+ for event in pygame.event.get():
+ if event.type == pygame.QUIT:
+ return {
+ "command": Command.QUIT.value,
+ "action": None,
+ "value": None,
+ }
+ elif event.type == pygame.KEYDOWN:
+ if event.key == pygame.K_UP:
+ snake.change_to = "UP"
+ elif event.key == pygame.K_DOWN:
+ snake.change_to = "DOWN"
+ elif event.key == pygame.K_LEFT:
+ snake.change_to = "LEFT"
+ elif event.key == pygame.K_RIGHT:
+ snake.change_to = "RIGHT"
+ elif event.key == pygame.K_ESCAPE:
+ return None
+
+ if serial.in_waiting() > 0:
+ data = serial.readline().decode("utf-8").strip()
+ message = json.loads(data)
+
+ if message.get("command") == Command.SCORE.value:
+ opponent_dead = True
+ opponent_score = message.get("value", 0)
+
+ snake.move()
+
+ if snake.check_collision():
+ my_score = snake.score
+
+ serial.write(
+ json.dumps(
+ {
+ "command": Command.SCORE.value,
+ "action": None,
+ "value": my_score,
+ }
+ ).encode("utf-8")
+ )
+
+ if opponent_dead:
+ score_display_time = pygame.time.get_ticks()
+ else:
+ waiting = True
+
+ continue
+
+ screen.fill(BLACK)
+
+ for segment in snake.body:
+ pygame.draw.rect(
+ screen, GREEN, [segment[0], segment[1], BLOCK_SIZE, BLOCK_SIZE]
+ )
+
+ pygame.draw.rect(
+ screen, RED, [snake.food[0], snake.food[1], BLOCK_SIZE, BLOCK_SIZE]
+ )
+
+ score_text = font.render(f"Score: {snake.score}", True, WHITE)
+
+ screen.blit(score_text, (10, 10))
+ pygame.display.flip()
+ clock.tick(SPEED)
+
+ return None
diff --git a/src/splitscreen_duo/menu.py b/src/splitscreen_duo/menu.py
index e1d97f5..a7bda82 100644
--- a/src/splitscreen_duo/menu.py
+++ b/src/splitscreen_duo/menu.py
@@ -3,9 +3,9 @@ import pygame
import os
from .input import Input
from .command import Command
+from .game import Game
import logging
import json
-from .game import Game
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)