--- /dev/null
+#!/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()