]> git.mar77i.info Git - zenbook_gui/commitdiff
add a regular memory game
authormar77i <mar77i@protonmail.ch>
Mon, 5 May 2025 19:42:28 +0000 (21:42 +0200)
committermar77i <mar77i@protonmail.ch>
Mon, 5 May 2025 19:42:28 +0000 (21:42 +0200)
memory.py [new file with mode: 0755]
rps.py
ui/__init__.py
ui/modal.py
ui/root.py
ui/spinner.py
vs_memory.py [moved from vs_memory/vs_memory.py with 78% similarity]

diff --git a/memory.py b/memory.py
new file mode 100755 (executable)
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 a63c65b1def6c111f88d88ef34d03485600b601a..7cf8645ac0e5cedbfd127be1d675208e28522f85 100755 (executable)
--- 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()
index 381fda04861de81e6f7d91c475fd38db43f3b3de..13086f2c850b7ffd4a7493311907df9623c3b268 100644 (file)
@@ -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",
index e7f612c241d53160cc85282343c24767fcaf5d26..4639ab3caa869c98d548207934a9d41c15fcf753 100644 (file)
@@ -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()
index 3a04f5118bdc1db0550a238d5aac27f9d262ffc5..1403fdca02c2e80e18b23aa554e7b0ef7828c405 100644 (file)
@@ -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):
index ff66c0a9332912504b5b25527748a1d5d9591817..40b0b21ab6fc477bf9283f3d9cc0f1cc228baac7 100644 (file)
@@ -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
similarity index 78%
rename from vs_memory/vs_memory.py
rename to vs_memory.py
index 6620362d2d61950518f45ef4e609e07fdabb6447..7a4c52c1cbdc3017bbb925339af7141024e7642f 100755 (executable)
@@ -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