Source code for yasuki_gui.visuals.hand

import tkinter as tk

from yasuki_core.engine.players import PlayerId
from yasuki_gui import theme
from yasuki_gui.constants import CARD_W, CARD_H, HAND_GAP, HAND_PADDING
from yasuki_gui.ui.images import ImageProvider, load_image as _li, load_back_image as _lbi
from yasuki_gui.visuals.cardface import RenderCard
from yasuki_gui.visuals.visual import Visual


[docs] class HandVisual(Visual):
[docs] def __init__( self, cards: list[RenderCard], owner: PlayerId | None, x: int, y: int, w: int, h: int, tag: str, images: ImageProvider | None = None, selected_ids: frozenset[str] = frozenset(), ): self.cards = cards self.owner = owner self.x = x self.y = y self.w = w self.h = h self.tag = tag self.images = images self.selected_ids = selected_ids
@property def size(self) -> tuple[int, int]: return self.w, self.h @property def bbox(self) -> tuple[int, int, int, int]: w, h = self.size return self.x - w // 2, self.y - h // 2, self.x + w // 2, self.y + h // 2 def _start_x(self) -> int: # Center-justify cards within the inner padded width when possible; otherwise left-align. n = len(self.cards) inner_left = self.x - self.w // 2 + HAND_PADDING inner_right = self.x + self.w // 2 - HAND_PADDING inner_width = max(0, inner_right - inner_left) if n <= 0 or inner_width <= 0: return inner_left + CARD_W // 2 step = self._step() # Total content width for n cards laid side-by-side with gaps content_width = CARD_W + (n - 1) * step base_first_center = inner_left + CARD_W // 2 if content_width >= inner_width: # Not enough space to center; left align at inner-left return base_first_center # Center content horizontally within inner bounds free_space = inner_width - content_width desired_first_center = inner_left + (free_space // 2) + CARD_W // 2 # Clamp so the last card also stays within inner bounds max_first_center = inner_right - CARD_W // 2 - (n - 1) * step return max(base_first_center, min(desired_first_center, max_first_center)) def _step(self) -> int: return CARD_W + HAND_GAP
[docs] def center_for_index(self, idx: int) -> tuple[int, int]: return (self._start_x() + idx * self._step(), self.y)
[docs] def index_at(self, x: int) -> int | None: if not self.cards: return None x0 = self._start_x() step = self._step() # compute nearest index by division rel = x - x0 idx = round(rel / step) if idx < 0 or idx >= len(self.cards): return None # Also ensure the x is not too far from the index center: allow half step center_x = x0 + idx * step if abs(x - center_x) > step // 2: return None return idx
[docs] def draw(self, canvas: tk.Canvas) -> None: x, y = self.x, self.y w, h = self.size # A faint frame marks the hand strip and its empty drop area. canvas.create_rectangle( x - w // 2, y - h // 2, x + w // 2, y + h // 2, outline=theme.LINE_SOFT, width=1, tags=(self.tag, "zone", "hand"), ) viewer = getattr(canvas, "local_player", None) owner = self.owner # An opponent's hand card shows its back unless its owner has shown it. for i, card in enumerate(self.cards): cx, cy = self.center_for_index(i) show_front = not ( owner is not None and viewer is not None and owner != viewer and not card.shown ) front_art = card.active_face.image_front if self.images is not None: photo = ( self.images.front(front_art, card.bowed, card.inverted) if show_front else self.images.back(card.side, card.bowed, card.inverted, card.image_back) ) else: photo = ( _li(front_art, card.bowed, card.inverted, master=canvas) if show_front else _lbi(card.side, card.bowed, card.inverted, card.image_back, master=canvas) ) cw, ch = (CARD_H, CARD_W) if card.bowed else (CARD_W, CARD_H) if photo is not None: canvas.create_image(cx, cy, image=photo, tags=(self.tag, "zone", "hand")) else: canvas.create_rectangle( cx - cw // 2, cy - ch // 2, cx + cw // 2, cy + ch // 2, fill=theme.CARD_FACE if show_front else theme.CARD_BACK, outline="", tags=(self.tag, "zone", "hand"), ) if show_front: canvas.create_text( cx, cy, text=card.active_face.name, fill=theme.INK, font=theme.serif(9, "bold"), width=cw - 10, justify="center", tags=(self.tag, "zone", "hand"), ) selected = card.id in self.selected_ids canvas.create_rectangle( cx - cw // 2, cy - ch // 2, cx + cw // 2, cy + ch // 2, outline=theme.SELECT if selected else theme.CARD_BORDER, width=3 if selected else 1, tags=(self.tag, "zone", "hand"), )