From b526d4265676a6af65e50674c0c809fc36065b3b Mon Sep 17 00:00:00 2001 From: mar77i Date: Sat, 15 Mar 2025 16:48:13 +0100 Subject: [PATCH] add vs_memory --- vs_memory/vs_memory.py | 232 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 232 insertions(+) create mode 100755 vs_memory/vs_memory.py diff --git a/vs_memory/vs_memory.py b/vs_memory/vs_memory.py new file mode 100755 index 0000000..c09cf50 --- /dev/null +++ b/vs_memory/vs_memory.py @@ -0,0 +1,232 @@ +#!/usr/bin/env python3 + +import re +import sys +from pathlib import Path +from secrets import choice, randbelow +from time import time + +import pygame + +sys.path.append(str(Path(__file__).parents[1])) + +from ui import Button, Child, Label, Root, TextInput + + +class Team: + def __init__(self, path): + self.name = path.stem + self.logo = pygame.image.load(path) + + +class MemoryDestination(Child): + def __init__(self, parent, rect, enabled=True): + super().__init__(parent, enabled) + self.rect = rect + self.team = None + self.team_visible = True + self.highlight = False + + def draw(self): + if self.team is not None and self.team_visible: + self.surf.blit(self.team.logo, self.rect.topleft) + else: + pygame.draw.rect( + self.surf, "red" if self.highlight else "dimgray", self.rect, 16 + ) + + +class MemoryCard(Child): + def __init__(self, parent, rect, team, callback, enabled=True): + super().__init__(parent, enabled) + self.rect = rect + self.team = team + self.callback = callback + self.pushed = None + self.frozen = False + + def handle_mousebuttondown(self, ev): + if self.frozen or ev.button != 1 or not self.rect.collidepoint(ev.pos): + return + self.pushed = (ev.pos[0] - self.rect.left, ev.pos[1] - self.rect.top) + self.root.stop_event = True + + def update_pos(self, pos): + left = pos[0] - self.pushed[0] + top = pos[1] - self.pushed[1] + if (left, top) != self.rect.topleft: + self.dirty = True + self.rect.left = left + self.rect.top = top + + def handle_mousemotion(self, ev): + if self.pushed is None: + return + self.update_pos(ev.pos) + if self.frozen or ev.buttons[0] == 0: + self.callback() + return + + def handle_mousebuttonup(self, ev): + if ev.button != 1 or self.pushed is None: + return + self.update_pos(ev.pos) + self.pushed = None + self.callback() + + def draw(self): + self.surf.blit(self.team.logo, self.rect.topleft) + + +class VSMemory(Root): + BACKGROUND_COLOR = "black" + MEMORY_RECT = pygame.Rect((640, 384), (512, 1190)) + NUMBER_REGEX = re.compile(r"[-+]?\d*(\.\d+)?") + + def get_memory_destinations(self): + height = self.surf.get_height() + y_cell = (height - 384) // (len(self.teams) // 2 + 1) + for y in range(len(self.teams) // 2): + y = 384 + y * y_cell + yield MemoryDestination(self, pygame.Rect((256, y), (128, 128)), False) + yield MemoryDestination(self, pygame.Rect((512, y), (128, 128)), False) + + def get_score(self): + return f"Score: {self.failed} failed, {self.successful} successful" + + def __init__(self): + self.look_timeout = 5 + pygame.init() + super().__init__( + pygame.display.set_mode((0, 0), pygame.FULLSCREEN), + pygame.font.Font(None, size=96), + ) + self.teams = tuple( + Team(file) for file in (Path(__file__).parent / "logos").iterdir() + ) + assert len(self.teams) < 15 # fits one column of memory destinations + self.failed = 0 + self.successful = 0 + self.score = Label( + self, + pygame.Rect((256, 192), (512, 128)), + self.get_score(), + ) + self.start_button = Button( + self, + pygame.Rect((256, 384), (256, 192)), + "Start", + self.start_round, + True, + ) + self.look_timeout_input = TextInput( + self, + pygame.Rect((640, 416), (512, 128)), + self.set_look_timeout, + str(self.look_timeout), + self.NUMBER_REGEX.fullmatch, + ) + self.end_round_button = Button( + self, + pygame.Rect((768, self.surf.get_height() // 2 - 128), (256, 256)), + "End", + self.end_round, + ) + self.end_round_button.enabled = False + self.memory_destinations = tuple(self.get_memory_destinations()) + self.memory_cards = tuple( + MemoryCard( + self, + pygame.Rect(self.MEMORY_RECT.topleft, (128, 128)), + team, + self.drop_card, + False, + ) + for i, team in enumerate(self.teams) + ) + self.started_timer = None + + def start_round(self): + self.start_button.enabled = False + self.look_timeout_input.enabled = False + available_teams = list(self.teams) + for child in self.memory_destinations: + child.enabled = True + child.highlight = False + child.team = choice(available_teams) + child.team_visible = True + available_teams.remove(child.team) + self.started_timer = time() + self.dirty = True + + @classmethod + def place_memory_card(cls, rect): + rect.left = cls.MEMORY_RECT.left + randbelow(cls.MEMORY_RECT.width - rect.width) + rect.top = cls.MEMORY_RECT.top + randbelow(cls.MEMORY_RECT.height - rect.height) + + def interactive_round(self): + for child in self.memory_destinations: + child.team_visible = False + for memory_card in self.memory_cards: + memory_card.enabled = True + memory_card.frozen = False + self.place_memory_card(memory_card.rect) + while any( + c.rect.colliderect(memory_card.rect) + for c in self.memory_cards if c != memory_card + ): + self.place_memory_card(memory_card.rect) + self.dirty = True + + def end_interactive_round(self, assignments): + failed = False + for dest, memory_card in assignments.items(): + memory_card.frozen = True + if memory_card.team != dest.team: + dest.highlight = True + failed = True + if failed: + self.failed += 1 + else: + self.successful += 1 + self.score.value = self.get_score() + self.end_round_button.enabled = True + self.dirty = True + + def end_round(self): + self.end_round_button.enabled = False + self.start_button.enabled = True + for child in (*self.memory_cards, *self.memory_destinations): + child.enabled = False + self.dirty = True + + def update(self): + if ( + self.started_timer is not None + and self.started_timer + self.look_timeout < time() + ): + self.interactive_round() + self.started_timer = None + + def drop_card(self): + memory_cards = list(self.memory_cards) + assignments = {} + for dest in self.memory_destinations: + for memory_card in memory_cards: + if dest.rect.colliderect(memory_card.rect): + break + else: + continue + assignments[dest] = memory_card + memory_cards.remove(memory_card) + if len(memory_cards) != 0: + return + self.end_interactive_round(assignments) + + def set_look_timeout(self, value): + self.look_timeout = float(value) + self.dirty = True + + +if __name__ == "__main__": + VSMemory().run() -- 2.51.0