From: mar77i Date: Mon, 5 May 2025 19:42:28 +0000 (+0200) Subject: add a regular memory game X-Git-Url: https://git.mar77i.info/?a=commitdiff_plain;h=c0f90ad5fd1ad954b9e8c916a381d62ebe9df81c;p=zenbook_gui add a regular memory game --- diff --git a/memory.py b/memory.py new file mode 100755 index 0000000..dfea625 --- /dev/null +++ b/memory.py @@ -0,0 +1,301 @@ +#!/usr/bin/env python3 + +from functools import partial +from pathlib import Path +from secrets import choice + +import pygame + +from ui import Button, CenterLabel, Child, Modal, Root, TextInput + + +class QuittableModal(Modal): + def handle_quit(self, ev): + self.parent.handle_quit(ev) + + KEY_METHODS = {frozenset(): {pygame.K_ESCAPE: handle_quit}} + + +class NameMenu(QuittableModal): + def __init__(self, parent): + # add title on top + # add exit button + super().__init__(parent) + self.lw = parent.surf.get_width() / 3 + CenterLabel( + self, + pygame.Rect((int(self.lw / 2), 144), (self.lw, 128)), + "Enter player names", + ) + self.inputs = [ + TextInput( + self, + pygame.Rect((int(self.lw / 2), 288), (int(self.lw * .8), 128)), + partial(self.set_player_name, 0), + ), + TextInput( + self, + pygame.Rect((int(self.lw / 2), 288 + 144), (int(self.lw * .8), 128)), + partial(self.set_player_name, 1), + ), + ] + self.add_button = Button( + self, + pygame.Rect((int(self.lw * 1.32), 288 + 144), (128, 128)), + "+", + self.add_player_input, + ) + Button( + self, + pygame.Rect((int(self.lw * 1.5), 288), (self.lw * .8, 144)).inflate(-16, -16), + "START GAME", + self.deactivate + ) + self.player_names = [] + + def set_player_name(self, i, name): + while i >= len(self.player_names): + self.player_names.append("") + self.player_names[i] = name + + def deactivate(self): + player_names = [p for p in (q.strip() for q in self.player_names) if p] + if len(player_names) == 0: + return + super().deactivate() + self.parent.start_game(player_names) + + def add_player_input(self): + num = len(self.inputs) + self.inputs.append( + TextInput( + self, + pygame.Rect( + (int(self.lw / 2), 288 + 144 * num), (int(self.lw * .8), 128) + ), + partial(self.set_player_name, num) + ) + ) + self.add_button.rect.top += 144 + self.dirty = True + + def draw_modal(self): + super().draw_modal() + pygame.draw.rect( + self.surf, + "black", + pygame.Rect( + (int(self.lw / 2), 128), + (int(self.lw) * 1.8, self.surf.get_height() - 256), + ).inflate(64, 0), + ) + + +class Player: + def __init__(self, name): + self.name = name + self.score = 0 + + def __str__(self): + return f"{self.name} ({self.score})" + + +class EndGameMenu(QuittableModal): + def __init__(self, parent, rect): + super().__init__(parent) + self.rect = rect + th = rect.height // 2 + self.labels = ( + CenterLabel(self, pygame.Rect(rect.topleft, (rect.width, th)), ""), + CenterLabel( + self, + pygame.Rect((rect.left, rect.top + th), (rect.width, th)), + "NEW GAME", + ), + ) + + def activate(self): + super().activate() + max_score = max(player.score for player in self.parent.players) + winners = [ + player for player in self.parent.players if player.score == max_score + ] + if len(winners) == 1: + self.labels[0].value = f"WINNER: {str(winners[0])} !!!" + else: + self.labels[0].value = ( + f"WINNERS: {', '.join(str(winner) for winner in winners)} !!!" + ) + + def draw_modal(self): + super().draw_modal() + pygame.draw.rect(self.surf, "black", self.rect) + + def handle_mousebuttondown(self, ev): + if ev.button == 1 and self.rect.collidepoint(ev.pos): + cards = [] + for card in self.parent.cards: + card.reset() + cards.append(card) + self.parent.cards.clear() + self.parent.shuffle_cards(cards) + self.parent.restart_game() + self.deactivate() + + +class MemoryCard(Child): + CARD_SIZE = (256, 256) + BACKSIDE = "royalblue4" + FRONTSIDE = "white" + + def __init__(self, parent, image, name, pos=(0, 0)): + super().__init__(parent) + if image.get_size() != self.CARD_SIZE: + image = pygame.transform.scale(image, self.CARD_SIZE) + self.image = image + self.name = name + self.rect = pygame.Rect(pos, self.CARD_SIZE) + self.covered = True + self.taken = False + + @property + def pos(self): + return self.rect.topleft + + @pos.setter + def pos(self, value): + self.rect.topleft = value + + def reset(self): + self.covered = self.enabled = True + self.taken = False + + def draw(self): + if self.covered: + pygame.draw.rect(self.surf, self.BACKSIDE, self.rect) + else: + pygame.draw.rect(self.surf, self.FRONTSIDE, self.rect) + self.surf.blit(self.image, self.pos) + + def handle_mousebuttondown(self, ev): + if ( + ev.button == 1 + and self.rect.collidepoint(ev.pos) + and self.covered + and not self.taken + and len(self.parent.cards_uncovered) < 2 + ): + self.covered = False + self.dirty = True + self.parent.check_turn() + + +class MemoryGame(Root): + BACKGROUND_COLOR = "gray34" + + def __init__(self): + pygame.init() + super().__init__( + pygame.display.set_mode((0, 0), flags=pygame.FULLSCREEN), + pygame.font.Font(None, size=96), + ) + self.players = [] + self.current_player = 0 + self.name_menu = NameMenu(self) + size = self.surf.get_size() + thirds = (size[0] // 3, size[1] //3) + self.end_game_menu = EndGameMenu(self, pygame.Rect(thirds, thirds)) + self.cards = [] + initial_cards = [] + for item in (Path(__file__).parent / "logos").iterdir(): + surf = pygame.image.load(item) + initial_cards.append(MemoryCard(self, surf, item.name)) + initial_cards.append(MemoryCard(self, initial_cards[-1].image, item.name)) + self.shuffle_cards(initial_cards) + self.turn_label = Button( + self, + pygame.Rect((512, 144), (self.surf.get_width() - 1024, 128)), + "", + self.next_turn + ) + self.turn_done = False + self.name_menu.activate() + + def shuffle_cards(self, initial_cards): + row_width = 6 + i = 0 + card_size = MemoryCard.CARD_SIZE + padding = 32 + left_offset = ( + self.surf.get_width() - row_width * card_size[0] - (row_width - 1) * padding + ) // 2 + while len(initial_cards): + card = choice(initial_cards) + initial_cards.remove(card) + card.pos = ( + left_offset + (i % row_width) * (card_size[0] + padding), + 288 + (i // row_width) * (card_size[1] + padding), + ) + self.cards.append(card) + i += 1 + + def update_turn_label(self): + self.dirty = True + self.turn_label.value = str(self.players[self.current_player]) + + def start_game(self, player_names): + self.current_player = 0 + self.turn_done = False + for player_name in player_names: + self.players.append(Player(player_name)) + self.update_turn_label() + + def restart_game(self): + self.current_player = 0 + self.turn_done = False + for player in self.players: + player.score = 0 + self.update_turn_label() + + def check_turn(self): + uncovered = self.cards_uncovered + if len(uncovered) == 2: + if uncovered[0].name == uncovered[1].name: + uncovered[0].taken = uncovered[1].taken = True + self.players[self.current_player].score += 1 + self.update_turn_label() + if all(not card.enabled or card.taken for card in self.cards): + self.end_game_menu.activate() + return + else: + self.turn_done = True + + def next_turn(self): + if not self.turn_done: + return + self.turn_done = False + for card in self.cards_taken: + card.enabled = False + for card in self.cards_uncovered: + card.covered = True + if self.current_player == len(self.players) - 1: + self.current_player = 0 + else: + self.current_player += 1 + self.update_turn_label() + + @property + def cards_uncovered(self): + return [ + card + for card in self.cards + if card.enabled and not card.covered and not card.taken + ] + + @property + def cards_taken(self): + return [card for card in self.cards if card.taken] + + +if __name__ == "__main__": + MemoryGame().run() diff --git a/rps.py b/rps.py index a63c65b..7cf8645 100755 --- a/rps.py +++ b/rps.py @@ -13,4 +13,6 @@ with redirect_stdout(StringIO()): # ruff: noqa: F401 import pygame # type: ignore -RockPaperScissors().run() + +if __name__ == "__main__": + RockPaperScissors().run() diff --git a/ui/__init__.py b/ui/__init__.py index 381fda0..13086f2 100644 --- a/ui/__init__.py +++ b/ui/__init__.py @@ -14,7 +14,7 @@ from .rect import Rect from .root import Root from .scroll import Scroll from .slider import Slider -from .spinner import RepeatButton, Spinner +from .spinner import FloatSpinner, RepeatButton, Spinner from .switch import Switch from .tab_bar import TabBar from .text_input import TextInput @@ -26,6 +26,7 @@ __all__ = [ "ColorButton", "DropDown", "EventMethodDispatcher", + "FloatSpinner", "FPSWidget", "Icon", "IconButton", diff --git a/ui/modal.py b/ui/modal.py index e7f612c..4639ab3 100644 --- a/ui/modal.py +++ b/ui/modal.py @@ -25,8 +25,11 @@ class Modal(Focusable, Parent, Child): self.root.stop_event = True self.dirty = True - def draw(self): + def draw_modal(self): tintsurf = pygame.Surface(self.surf.get_size(), pygame.SRCALPHA, 32) tintsurf.fill(pygame.Color(0x80)) self.surf.blit(tintsurf, (0, 0)) + + def draw(self): + self.draw_modal() super().draw() diff --git a/ui/root.py b/ui/root.py index 3a04f51..1403fdc 100644 --- a/ui/root.py +++ b/ui/root.py @@ -22,7 +22,7 @@ class Root(Parent): def handle_quit(self, _=None): self.running = False - KEY_METHODS = {frozenset(set()): {pygame.K_ESCAPE: handle_quit}} + KEY_METHODS = {frozenset(): {pygame.K_ESCAPE: handle_quit}} @property def focused(self): diff --git a/ui/spinner.py b/ui/spinner.py index ff66c0a..40b0b21 100644 --- a/ui/spinner.py +++ b/ui/spinner.py @@ -123,10 +123,26 @@ class Spinner(Parent, Child): str_value = str(self.value) if str_value != self.text_input.value: self.text_input.value = str(self.value) - self.root.dirty = True + self.dirty = True def spin_callback(self, value): self.value += value self.text_input.value = str(self.value) self.callback(self.value) - self.root.dirty = True + self.dirty = True + + +class FloatSpinner(Spinner): + def call_callback(self, value): + try: + float_value = float(value) + except ValueError: + pass + else: + if float_value != self.value: + self.value = float_value + self.callback(float_value) + str_value = str(self.value) + if str_value != self.text_input.value: + self.text_input.value = str(self.value) + self.dirty = True diff --git a/vs_memory/vs_memory.py b/vs_memory.py similarity index 78% rename from vs_memory/vs_memory.py rename to vs_memory.py index 6620362..7a4c52c 100755 --- a/vs_memory/vs_memory.py +++ b/vs_memory.py @@ -1,6 +1,5 @@ #!/usr/bin/env python3 -import re import sys from pathlib import Path from secrets import choice, randbelow @@ -10,7 +9,7 @@ import pygame sys.path.append(str(Path(__file__).parents[1])) -from ui import Button, Child, Label, Root, TextInput +from ui import Button, Child, FloatSpinner, Label, Root, Spinner class Team: @@ -86,8 +85,6 @@ class VSMemory(Root): TOP = 186 LEFT = 256 MEMORY_LEFT = 640 - #MEMORY_RECT = pygame.Rect((640, 184), (512, 896)) - NUMBER_REGEX = re.compile(r"[-+]?\d*(\.\d+)?") def get_memory_destinations(self): height = self.surf.get_height() @@ -105,7 +102,6 @@ class VSMemory(Root): 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), @@ -114,7 +110,10 @@ class VSMemory(Root): self.teams = tuple( Team(file) for file in (Path(__file__).parent / "logos").iterdir() ) - assert len(self.teams) < 15 # fits one column of memory destinations + len_teams = len(self.teams) + # fits one column of memory destinations (todo) + assert len_teams < 15 and len_teams % 2 == 0 + self.num_pairs = len_teams // 2 self.failed = 0 self.successful = 0 self.score = Label( @@ -129,12 +128,27 @@ class VSMemory(Root): self.start_round, True, ) - self.look_timeout_input = TextInput( + self.look_timeout_label = Label( self, - pygame.Rect((640, 216), (512, 128)), - self.set_look_timeout, - str(self.look_timeout), - self.NUMBER_REGEX.fullmatch, + pygame.Rect((640, self.TOP), (128, 128)), + "Look Timeout", + ) + self.look_timeout = FloatSpinner( + self, + pygame.Rect((768, self.TOP + 192), (512, 128)), + self.set_dirty, + 5, + ) + self.num_pairs_label = Label( + self, + pygame.Rect((640, self.TOP + 384), (128, 128)), + "Number of Pairs", + ) + self.num_pairs = Spinner( + self, + pygame.Rect((768, self.TOP + 576), (512, 128)), + self.set_dirty, + 3, ) self.end_round_button = Button( self, @@ -158,9 +172,12 @@ class VSMemory(Root): def start_round(self): self.start_button.enabled = False - self.look_timeout_input.enabled = False + self.look_timeout_label.enabled = False + self.look_timeout.enabled = False + self.num_pairs_label.enabled = False + self.num_pairs.enabled = False available_teams = list(self.teams) - for child in self.memory_destinations: + for child in self.memory_destinations[:2 * self.num_pairs.value]: child.enabled = True child.highlight = False child.team = choice(available_teams) @@ -189,11 +206,16 @@ class VSMemory(Root): def end_interactive_round(self, assignments): failed = False + assigned_memory_cards = set() for dest, memory_card in assignments.items(): + assigned_memory_cards.add(memory_card) memory_card.frozen = True if memory_card.team != dest.team: dest.highlight = True failed = True + for memory_card in self.memory_cards: + if memory_card not in assigned_memory_cards: + memory_card.enabled = False if failed: self.failed += 1 else: @@ -205,6 +227,10 @@ class VSMemory(Root): def end_round(self): self.end_round_button.enabled = False self.start_button.enabled = True + self.look_timeout_label.enabled = True + self.look_timeout.enabled = True + self.num_pairs_label.enabled = True + self.num_pairs.enabled = True for child in (*self.memory_cards, *self.memory_destinations): child.enabled = False self.dirty = True @@ -212,7 +238,7 @@ class VSMemory(Root): def update(self): if ( self.started_timer is not None - and self.started_timer + self.look_timeout < time() + and self.started_timer + self.look_timeout.value < time() ): self.interactive_round() self.started_timer = None @@ -221,6 +247,8 @@ class VSMemory(Root): memory_cards = list(self.memory_cards) assignments = {} for dest in self.memory_destinations: + if not dest.enabled: + continue for memory_card in memory_cards: if dest.rect.colliderect(memory_card.rect): break @@ -228,12 +256,10 @@ class VSMemory(Root): continue assignments[dest] = memory_card memory_cards.remove(memory_card) - if len(memory_cards) != 0: - return - self.end_interactive_round(assignments) + if len(assignments) == 2 * self.num_pairs.value: + self.end_interactive_round(assignments) - def set_look_timeout(self, value): - self.look_timeout = float(value) + def set_dirty(self, _): self.dirty = True