--- /dev/null
+#!/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()
#!/usr/bin/env python3
-import re
import sys
from pathlib import Path
from secrets import choice, randbelow
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:
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()
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),
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(
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,
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)
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:
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
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
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
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