diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index d7b6df9..2349f67 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -58,7 +58,7 @@ jobs: - uses: actions/upload-artifact@v4 with: name: ${{ env.ARTIFACT_PREFIX }}-windows - path: pyinstaller/dist + path: pyinstaller/dist/windows/hh-creator release: needs: [build-linux, build-windows] @@ -69,8 +69,12 @@ jobs: - name: Check what artifacts have been downloaded run: | ls -la - ls -la ${{ env.ARTIFACT_PREFIX }}* + ls -la ${{ env.ARTIFACT_PREFIX }} + - uses: vimtor/action-zip@v1.2 + with: + files: ${{ env.ARTIFACT_PREFIX }}-windows + dest: ${{ env.ARTIFACT_PREFIX }}-windows.zip - name: Github release uses: softprops/action-gh-release@v2 with: - files: ${{ env.ARTIFACT_PREFIX }}* + files: ${{ env.ARTIFACT_PREFIX }}-windows.zip diff --git a/.gitignore b/.gitignore index 80fd895..9b35074 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,6 @@ artifacts/ .venv/ requirements.txt .python-version +junk +.gram +.zed diff --git a/hh_creator/__main__.py b/hh_creator/__main__.py index b0cc3b3..cbf0ef3 100644 --- a/hh_creator/__main__.py +++ b/hh_creator/__main__.py @@ -13,7 +13,7 @@ from hh_creator.util import init_sounds -def main(): +def main() -> None: parser = ArgumentParser() parser.add_argument( diff --git a/hh_creator/animations.py b/hh_creator/animations.py index 265d327..8e02e57 100644 --- a/hh_creator/animations.py +++ b/hh_creator/animations.py @@ -10,17 +10,17 @@ class Animations: animations = [] @classmethod - def add_callback(cls, callback): + def add_callback(cls, callback) -> None: if not cls.animations: raise ValueError cls.animations[-1].finished.connect(callback) @classmethod - def add(cls, animation): + def add(cls, animation) -> None: cls.animations.append(animation) @classmethod - def start(cls): + def start(cls) -> None: group = QtCore.QParallelAnimationGroup() for i, a in enumerate(cls.animations): if i == 0: @@ -31,7 +31,7 @@ def start(cls): group.start(group.DeleteWhenStopped) @classmethod - def reset(cls): + def reset(cls) -> None: cls.animations = [] @classmethod @@ -42,17 +42,22 @@ def text( duration: int, scene, content=None, - callbacks=tuple(), + callbacks=(), target_font=False, - ): + target_item_center: bool = False, + font_kwargs: dict | None = None, + ) -> None: # print(f"Animating {source} to {target}") if content is None: content = source.content + if font_kwargs is None: + font_kwargs = {} + if target_font: - font_kwargs = target.font_kwargs() + font_kwargs = target.font_kwargs() | font_kwargs else: - font_kwargs = source.font_kwargs() + font_kwargs = source.font_kwargs() | font_kwargs item_to_animate = TextItem( hide_if_empty=source.hide_if_empty, @@ -69,6 +74,12 @@ def text( else: target_pos = target.get_pos_if_content(content) + if target_item_center: + rect = target.boundingRect() + target_pos = QtCore.QPointF( + target_pos.x() + rect.width() / 2, target_pos.y() + ) + animation = QtCore.QPropertyAnimation(scene) animation.setTargetObject(item_to_animate) animation.setPropertyName(b"pos") diff --git a/hh_creator/card.py b/hh_creator/card.py index 6d882a0..3a30306 100644 --- a/hh_creator/card.py +++ b/hh_creator/card.py @@ -1,9 +1,10 @@ +import contextlib import itertools import logging from dataclasses import dataclass from functools import total_ordering from pathlib import Path -from typing import TYPE_CHECKING, List, Union +from typing import TYPE_CHECKING from deuces import Card as DeucesCard from deuces import Evaluator @@ -62,7 +63,7 @@ def one_letter_format(self): class CardLook(QtWidgets.QGraphicsItemGroup): - instances = [] + instances: list["CardLook"] = [] def __init__( self, @@ -70,7 +71,7 @@ def __init__( crop_bottom=False, *a, **kw, - ): + ) -> None: root = Path("cards_cut") if crop_bottom else Path("cards") super().__init__(*a, **kw) self.scale_factor = scale_factor @@ -85,13 +86,16 @@ def __init__( self.addToGroup(k) k.setVisible(False) k.setScale(scale_factor) - self.back = Image.get(root / "back-red") - self.back.setScale(scale_factor) + self.crop_bottom = crop_bottom + variant = ("back-" + config.config["look"]["card-back"]) or "back-red" + self.back = Image.get(root / variant) + self._crop_back_pixmap() + self._apply_back_scale_factor() self.scale_factor = scale_factor self.addToGroup(self.back) self.instances.append(self) - def _update_look(self): + def _update_look(self) -> None: if self._rank is None or self._suit is None: self.back.setVisible(True) return @@ -99,38 +103,55 @@ def _update_look(self): for (rank, suit), item in self.faces.items(): item.setVisible((rank, suit) == (self._rank, self._suit)) + def _apply_back_scale_factor(self) -> None: + mult = 1 if isinstance(self.back, Qt.QGraphicsSvgItem) else 0.5 + self.back.setScale(self.scale_factor * mult) + + def _crop_back_pixmap(self) -> None: + if isinstance(self.back, Qt.QGraphicsPixmapItem) and self.crop_bottom: + pixmap = self.back.pixmap() + cropped = pixmap.copy(0, 0, pixmap.width(), int(0.62 * pixmap.height())) + self.back.setPixmap(cropped) + def boundingRect(self): rect_f = super().boundingRect() scaled = Qt.QRectF(*(x * self.scale_factor for x in rect_f.getCoords())) return scaled @classmethod - def change_back(cls, color): + def change_back(cls, color: str) -> None: config.config["look"]["card-back"] = color config.save_config() for i in cls.instances: visible = i.back.isVisible() - i.back.deleteLater() pos = i.back.scenePos() - i.back = Image.get(Path("cards") / f"back-{color}") - i.back.setScale(i.scale_factor) + scene = i.back.scene() + if scene is not None: + scene.removeItem(i.back) + if isinstance(i, Qt.QGraphicsSvgItem): + i.back.deleteLater() + i.back = Image.get( + Path("cards" + ("_cut" if i.crop_bottom else "")) / f"back-{color}" + ) + i._crop_back_pixmap() + i._apply_back_scale_factor() i.back.setPos(pos) i.back.setVisible(visible) i.addToGroup(i.back) - def hide_face(self): + def hide_face(self) -> None: self.back.setVisible(True) - def discover(self): + def discover(self) -> None: if self._rank is not None and self._suit is not None: self.back.setVisible(False) class CardItem(CardLook): - def __init__(self, *a, **kw): + def __init__(self, *a, **kw) -> None: super().__init__(*a, **kw) - self._suit: Union[Suit, None] = None - self._rank: Union[Rank, None] = None + self._suit: Suit | None = None + self._rank: Rank | None = None self._update_look() @property @@ -138,7 +159,7 @@ def suit(self): return self._suit @suit.setter - def suit(self, suit): + def suit(self, suit) -> None: self._suit = suit self._update_look() @@ -147,23 +168,21 @@ def rank(self): return self._rank @rank.setter - def rank(self, rank): + def rank(self, rank) -> None: self._rank = rank self._update_look() - def wheelEvent(self, event: QtWidgets.QGraphicsSceneWheelEvent): + def wheelEvent(self, event: QtWidgets.QGraphicsSceneWheelEvent) -> None: log.debug("Wheel on card") mw = self.scene().parent() if mw.state != mw.State.ACTIONS: return if self.rank is not None: attr = "next" if event.delta() > 0 else "prev" - try: + with contextlib.suppress(ValueError): self.rank = getattr(self.rank, attr)() - except ValueError: - pass - def mousePressEvent(self, event: QtWidgets.QGraphicsSceneMouseEvent): + def mousePressEvent(self, event: QtWidgets.QGraphicsSceneMouseEvent) -> None: mw = self.scene().parent() if mw.state != mw.State.ACTIONS: return @@ -183,7 +202,7 @@ def mousePressEvent(self, event: QtWidgets.QGraphicsSceneMouseEvent): else: self.suit = None - def contextMenuEvent(self, event: QtWidgets.QGraphicsSceneContextMenuEvent): + def contextMenuEvent(self, event: QtWidgets.QGraphicsSceneContextMenuEvent) -> None: mw = self.scene().parent() if mw.state != mw.State.ACTIONS: return @@ -207,7 +226,7 @@ def contextMenuEvent(self, event: QtWidgets.QGraphicsSceneContextMenuEvent): menu.addActions(actions) menu.exec(event.screenPos()) - def deuces_format(self): + def deuces_format(self) -> str | None: try: return f"{self.rank.one_letter_format()}{self.suit.one_letter_format()}" except AttributeError: @@ -217,7 +236,7 @@ def to_deuces(self): return DeucesCard.new(self.deuces_format()) @classmethod - def reset(cls): + def reset(cls) -> None: for c in cls.instances: c.rank = None c.suit = None @@ -226,8 +245,8 @@ def reset(cls): @total_ordering @dataclass class Hand: - cards: List[CardItem] - board: List[CardItem] + cards: list[CardItem] + board: list[CardItem] def __gt__(self, other: "Hand"): return self.deuces_score() < other.deuces_score() @@ -241,7 +260,7 @@ def deuces_score(self): ) -def get_winners(player_items: List["PlayerItemGroup"], board: List["CardItem"]): +def get_winners(player_items: list["PlayerItemGroup"], board: list["CardItem"]): scores = [] for player in player_items: if player.n_cards == 2: diff --git a/hh_creator/config.py b/hh_creator/config.py index a8df708..433a746 100644 --- a/hh_creator/config.py +++ b/hh_creator/config.py @@ -4,7 +4,7 @@ from pathlib import Path -def save_config(window_geometry=None, window_state=None): +def save_config(window_geometry=None, window_state=None) -> None: log.info(f"Writing config to {config_filename}") with config_filename.open("w", encoding="utf-8") as fp: config.write(fp) @@ -16,7 +16,7 @@ def save_config(window_geometry=None, window_state=None): fp.write(bytes(window_state)) -def restore_defaults(): +def restore_defaults() -> None: config = ConfigParser(interpolation=ExtendedInterpolation()) config.read(RESOURCE_PATH / "default.ini", encoding="utf-8") with config_filename.open("w", encoding="utf-8") as fp: diff --git a/hh_creator/dialog.py b/hh_creator/dialog.py index fb1f346..9a6042f 100644 --- a/hh_creator/dialog.py +++ b/hh_creator/dialog.py @@ -4,7 +4,7 @@ from decimal import Decimal from PyQt5 import QtCore, QtWidgets -from PyQt5.QtCore import pyqtSlot +from PyQt5.QtCore import pyqtSlot, QLocale from . import config, hh from .util import AutoUI, amount_validator, decimal_conversion @@ -38,7 +38,7 @@ class NewHandDialog(QtWidgets.QDialog, AutoUI): N_CARDS = {"Texas": 2, "Omaha": 4} - def __init__(self, *a, **kw): + def __init__(self, *a, **kw) -> None: super().__init__(*a, **kw) self.ok_button = self.widgets[0] @@ -61,14 +61,14 @@ def __init__(self, *a, **kw): self.open_instead = False self.update_ok() - def _auto_decimals(self): + def _auto_decimals(self) -> None: sb = self.get_field_value("SB", Decimal()) ante = self.get_field_value("Ante", Decimal()) self._get_widget("Decimals").setText( str(max(-sb.as_tuple().exponent, -ante.as_tuple().exponent, 1)) ) - def _auto_sb(self): + def _auto_sb(self) -> None: self._get_widget("SB").setText(str(self.get_field_value("BB") / 2)) def _get_widget(self, field_name): @@ -103,25 +103,25 @@ def get_n_cards(self): return self.N_CARDS[self.get_field_value("Variant")] @pyqtSlot(str) - def on_lineEditSB_textEdited(self, value): + def on_lineEditSB_textEdited(self, value) -> None: if not self.widgets["checkBoxDecimals"].isChecked(): self._auto_decimals() self.update_ok() @pyqtSlot(bool) - def on_checkBoxSB_toggled(self, checked): + def on_checkBoxSB_toggled(self, checked) -> None: if not checked: self._auto_sb() self.update_ok() @pyqtSlot(bool) - def on_checkBoxDecimals_toggled(self, checked): + def on_checkBoxDecimals_toggled(self, checked) -> None: if not checked: self._auto_decimals() self.update_ok() @pyqtSlot(str) - def on_lineEditBB_textEdited(self, value): + def on_lineEditBB_textEdited(self, value) -> None: if not self.widgets["checkBoxSB"].isChecked(): self._auto_sb() if not self.widgets["checkBoxDecimals"].isChecked(): @@ -129,42 +129,42 @@ def on_lineEditBB_textEdited(self, value): self.update_ok() @pyqtSlot(str) - def on_lineEditStraddle_textEdited(self, value): + def on_lineEditStraddle_textEdited(self, value) -> None: self.update_ok() @pyqtSlot(bool) - def on_checkBoxStraddle_toggled(self, checked): + def on_checkBoxStraddle_toggled(self, checked) -> None: if not checked: self._get_widget("Straddle").setText("0") self.update_ok() @pyqtSlot(str) - def on_lineEditAnte_textEdited(self, value): + def on_lineEditAnte_textEdited(self, value) -> None: self._auto_decimals() self.update_ok() @pyqtSlot(bool) - def on_checkBoxAnte_toggled(self, checked): + def on_checkBoxAnte_toggled(self, checked) -> None: if not checked: self._get_widget("Ante").setText("0") self.update_ok() @pyqtSlot(bool) - def on_checkBoxBBAnte_toggled(self, checked): + def on_checkBoxBBAnte_toggled(self, checked) -> None: if not checked: self._get_widget("BBAnte").setText("0") self.update_ok() @pyqtSlot(str) - def on_lineEditPlayers_textEdited(self, value): + def on_lineEditPlayers_textEdited(self, value) -> None: self.update_ok() @pyqtSlot() - def on_pushButtonOpen_clicked(self): + def on_pushButtonOpen_clicked(self) -> None: self.open_instead = True self.close() - def update_ok(self): + def update_ok(self) -> None: sb = self.get_field_value("SB", default=0) bb = self.get_field_value("BB", default=0) straddle = self.get_field_value("Straddle", default=0) @@ -175,12 +175,12 @@ def update_ok(self): stack_ok = stack > 0 sb_ok = 0 <= sb <= bb - bb_ok = 0 < bb + bb_ok = bb > 0 straddle_ok = ( not self._get_checkbox("Straddle").isChecked() or 0 < straddle <= players - 2 ) - ante_ok = not self._get_checkbox("Ante").isChecked() or 0 < ante + ante_ok = not self._get_checkbox("Ante").isChecked() or ante > 0 players_ok = 2 <= players <= 10 decimals_ok = decimals >= 0 @@ -190,7 +190,7 @@ def update_ok(self): class NameDialog(QtWidgets.QDialog, AutoUI): - def __init__(self, parent, content): + def __init__(self, parent, content) -> None: super().__init__(parent=parent) line_edit = self.widgets["lineEdit"] @@ -199,15 +199,17 @@ def __init__(self, parent, content): self.show() @pyqtSlot(str) - def on_lineEdit_textEdited(self, text): + def on_lineEdit_textEdited(self, text) -> None: self.findChild(QtWidgets.QPushButton).setEnabled(bool(text)) class StackDialog(QtWidgets.QDialog, AutoUI): - def __init__(self, parent, value): + def __init__(self, parent, value: Decimal) -> None: super().__init__(parent) self.line_edit: QtWidgets.QLineEdit = self.widgets["lineEdit"] - self.line_edit.setText(str(value)) + self.line_edit.setText( + QLocale().toString(float(value), "f", abs(value.as_tuple().exponent)) + ) self.line_edit.setValidator(amount_validator) self.line_edit.selectAll() self.show() @@ -217,14 +219,14 @@ def get_value(self): class ActionWidget(QtWidgets.QWidget, AutoUI): - def __init__(self, player_item: "PlayerItemGroup", parent): + def __init__(self, player_item: "PlayerItemGroup", parent) -> None: super().__init__(parent) self.setAttribute(QtCore.Qt.WA_TranslucentBackground) self.slider: QtWidgets.QSlider = self.widgets["horizontalSlider"] self.slider_values = [] self.player_item = player_item - def set_min_max_step(self, min_, max_, step): + def set_min_max_step(self, min_, max_, step) -> None: values = [min_] while values[-1] < max_: values.append(values[-1] + step) @@ -238,7 +240,7 @@ def set_min_max_step(self, min_, max_, step): self.slider_values = values self.widgets["lineEdit"].setText(str(min_)) - def set_possible_actions(self, action_types): + def set_possible_actions(self, action_types) -> None: self.widgets["call"].setEnabled(hh.ActionType.CALL in action_types) self.widgets["check"].setEnabled(hh.ActionType.CHECK in action_types) bet = hh.ActionType.BET in action_types or hh.ActionType.RAISE in action_types @@ -250,7 +252,7 @@ def amount(self): return decimal_conversion(self.widgets["lineEdit"].text()) @pyqtSlot(int) - def on_horizontalSlider_valueChanged(self, index): + def on_horizontalSlider_valueChanged(self, index) -> None: try: linevalue = float(self.widgets["lineEdit"].text()) except ValueError: @@ -269,7 +271,7 @@ def on_horizontalSlider_valueChanged(self, index): self.widgets["bet"].setEnabled(True) @pyqtSlot(str) - def on_lineEdit_textEdited(self, value): + def on_lineEdit_textEdited(self, value) -> None: try: value = float(value) except ValueError: @@ -293,19 +295,19 @@ def on_lineEdit_textEdited(self, value): self.widgets["bet"].setEnabled(False) @pyqtSlot() - def on_bet_clicked(self): + def on_bet_clicked(self) -> None: self.player_item.add_action(hh.ActionType.BET, self.amount()) @pyqtSlot() - def on_fold_clicked(self): + def on_fold_clicked(self) -> None: self.player_item.add_action(hh.ActionType.FOLD) @pyqtSlot() - def on_check_clicked(self): + def on_check_clicked(self) -> None: self.player_item.add_action(hh.ActionType.CHECK) @pyqtSlot() - def on_call_clicked(self): + def on_call_clicked(self) -> None: self.player_item.add_action(hh.ActionType.CALL) diff --git a/hh_creator/hh.py b/hh_creator/hh.py index b0f11bb..868f2cd 100644 --- a/hh_creator/hh.py +++ b/hh_creator/hh.py @@ -3,7 +3,7 @@ from copy import deepcopy from dataclasses import dataclass, field from decimal import Decimal -from typing import List, Union +from typing import Union from hh_creator.util import BLINDS, ActionType, IncrementableEnum @@ -16,6 +16,7 @@ class Position(PokerEnum): UTG2 = "UTG2", "utg+2", "utg + 2" UTG3 = "UTG3", "utg+3", "utg + 3" UTG4 = "UTG4", "utg+4", "utg + 4" + LJ = "LJ", "lowjack" HJ = "HJ", "hijack", "utg+5", "utg + 5" CO = "CO", "cutoff", "cut off" BTN = "BTN", "bu", "button" @@ -28,12 +29,12 @@ class Position(PokerEnum): 3: [Position.SB, Position.BB, Position.BTN], 4: [Position.SB, Position.BB, Position.UTG, Position.BTN], 5: [Position.SB, Position.BB, Position.UTG, Position.CO, Position.BTN], - 6: [Position.SB, Position.BB, Position.UTG, Position.HJ, Position.CO, Position.BTN], + 6: [Position.SB, Position.BB, Position.LJ, Position.HJ, Position.CO, Position.BTN], 7: [ Position.SB, Position.BB, Position.UTG, - Position.UTG1, + Position.LJ, Position.HJ, Position.CO, Position.BTN, @@ -43,7 +44,7 @@ class Position(PokerEnum): Position.BB, Position.UTG, Position.UTG1, - Position.UTG2, + Position.LJ, Position.HJ, Position.CO, Position.BTN, @@ -54,7 +55,7 @@ class Position(PokerEnum): Position.UTG, Position.UTG1, Position.UTG2, - Position.UTG3, + Position.LJ, Position.HJ, Position.CO, Position.BTN, @@ -66,7 +67,7 @@ class Position(PokerEnum): Position.UTG1, Position.UTG2, Position.UTG3, - Position.UTG4, + Position.LJ, Position.HJ, Position.CO, Position.BTN, @@ -75,10 +76,10 @@ class Position(PokerEnum): class HandHistoryException(Exception): - def __init__(self, message=""): + def __init__(self, message="") -> None: self.message = message - def __str__(self): + def __str__(self) -> str: return f"{self.__class__.__name__}: {self.message}" @@ -101,9 +102,9 @@ class Street(IncrementableEnum): @dataclass class Action: - street: Union[Street, None] = None + street: Street | None = None player: Union["Player", None] = None - action_type: Union[ActionType, None] = None + action_type: ActionType | None = None amount: Decimal = Decimal("0") added_to_pot: Decimal = Decimal("0") @@ -112,16 +113,16 @@ class Action: class Player: position: Position hand_history: "HandHistory" - actions: List[Action] = field(default_factory=list) + actions: list[Action] = field(default_factory=list) stack: Decimal = Decimal("100") def __post_init__(self): self.initial_stack = self.stack - def __repr__(self): + def __repr__(self) -> str: return self.__str__() - def __str__(self): + def __str__(self) -> str: return f"{self.position} ({self.stack})" def __eq__(self, other): @@ -146,7 +147,7 @@ def has_folded(self): return False return self.actions[-1].action_type == ActionType.FOLD - def add_action(self, action): + def add_action(self, action) -> None: self.actions.append(action) self.stack -= action.added_to_pot log.debug(f"{self.position} now has {self.stack}") @@ -177,18 +178,18 @@ def last_action(self): class HandHistory: def __init__( self, - stacks: Union[List[Decimal], None] = None, + stacks: list[Decimal] | None = None, small_blind: Decimal = Decimal("0.5"), ante: Decimal = Decimal("0."), - big_blind: Union[Decimal, None] = None, - bb_ante: Union[Decimal, None] = None, + big_blind: Decimal | None = None, + bb_ante: Decimal | None = None, n_straddle: int = 0, - ): + ) -> None: self.small_blind = small_blind if big_blind is None: big_blind = 2 * small_blind self.big_blind = big_blind - self.actions: List[Action] = [] + self.actions: list[Action] = [] self.ante = ante self.bb_ante = bb_ante @@ -198,17 +199,17 @@ def __init__( self.set_stacks(stacks) self.current_street = Street.ANTE - self.current_player: Union[Player, None] = None + self.current_player: Player | None = None self.total_pot = Decimal("0") self._blinds_posted = False - self.winner: Union[Player, None] = None + self.winner: Player | None = None self.n_straddle = n_straddle self.largest_blind = 0 - def set_stacks(self, stacks: List[Decimal]): + def set_stacks(self, stacks: list[Decimal]) -> None: for stack, pos in zip(stacks, POSITIONS[len(stacks)]): self.players.append( Player(position=pos, stack=Decimal(stack), hand_history=self) @@ -218,7 +219,7 @@ def set_stacks(self, stacks: List[Decimal]): def is_hu(self) -> bool: return len(self.players) == 2 - def post_blinds_and_antes(self): + def post_blinds_and_antes(self) -> None: self.current_player = self.players[0] if self.ante: for _ in range(len(self.players)): @@ -249,12 +250,18 @@ def get_player_by_position(self, position: Position): return p def _non_folded_players_after_current(self): - start = self.players.index(self.current_player) + 1 + if self.is_hu: + if self.current_street <= Street.PRE_FLOP: + start = self.players.index(self.get_player_by_position(Position.SB)) + elif self.current_street > Street.PRE_FLOP: + start = self.players.index(self.get_player_by_position(Position.BB)) + else: + start = self.players.index(self.current_player) + 1 players = self.players[start:] + self.players[:start] players = [p for p in players if not p.has_folded()] return players - def _next_player(self): + def _next_player(self) -> None: players = self._non_folded_players_after_current() if len(players) == 1: self.winner = players[0] @@ -281,10 +288,7 @@ def _next_player(self): else: self._next_street() - def _next_street(self): - # HU special case - if self.is_hu and self.current_street == Street.PRE_FLOP: - self.players = self.players[::-1] + def _next_street(self) -> None: self.current_street = self.current_street.next() if self.current_street == Street.SHOWDOWN: log.info("No more action possible, showdown time") @@ -337,7 +341,7 @@ def total_amount_to_call(self): return res @property - def last_action(self): + def last_action(self) -> Action | None: if self.actions: return self.actions[-1] @@ -357,7 +361,9 @@ def possible_action_types(self): actions.append(ActionType.RAISE) return actions - def add_action(self, action_type: ActionType, amount: Union[None, Decimal] = None): + def add_action( + self, action_type: ActionType, amount: None | Decimal = None + ) -> None: if self._blinds_posted and action_type not in self.possible_action_types(): raise InvalidAction if action_type == ActionType.BET: @@ -405,7 +411,7 @@ def add_action(self, action_type: ActionType, amount: Union[None, Decimal] = Non f"side_pots: {self.side_pots()}, current_street:{self.current_street}" ) - def remove_last_action(self): + def remove_last_action(self) -> None: action = self.actions.pop() action.player.stack += action.added_to_pot action.player.actions.pop() @@ -527,7 +533,7 @@ def from_dict(cls, obj): return hh def to_json(self): - return json.dumps(self, cls=HHJSONEncoder) + return json.dumps(self, cls=HHJSONEncoder, indent=2) def to_dict(self): return json.loads(self.to_json(), object_hook=json_hook) @@ -541,10 +547,18 @@ def n_pseudo_actions(self): # used by replayer to delay apparition of turn and river if self.last_action is None: return 0 + if self.last_action.action_type == ActionType.FOLD: + return 0 return Street.RIVER - self.last_action.street def play_length(self): - return 2 + len(self.editable_actions()) + self.n_pseudo_actions() + return 1 + len(self.editable_actions()) + self.n_pseudo_actions() + + @property + def went_to_showdown(self) -> bool: + if self.last_action: + return self.last_action.action_type != ActionType.FOLD + return False @dataclass @@ -556,9 +570,9 @@ class SidePotPlayer: @dataclass class SidePot: - players: List[SidePotPlayer] + players: list[SidePotPlayer] amount: Decimal - folded: List[SidePotPlayer] + folded: list[SidePotPlayer] def get_player_by_position(self, position: Position): for p in self.players: @@ -599,7 +613,7 @@ def json_hook(o): # return "\n".join(str(el) for el in list_) -def test(): +def test() -> None: logging.basicConfig(level=logging.DEBUG) hh = HandHistory( stacks=[Decimal(10), Decimal(50), Decimal(10), Decimal(10)], diff --git a/hh_creator/main_window.py b/hh_creator/main_window.py index 75f1d67..0ea11f9 100644 --- a/hh_creator/main_window.py +++ b/hh_creator/main_window.py @@ -1,11 +1,15 @@ import json import logging +from functools import partial from pathlib import Path -from typing import Union from PyQt5 import QtCore, QtGui, QtWidgets -from PyQt5.QtCore import pyqtSlot -from PyQt5.QtWidgets import QMessageBox +from PyQt5.QtCore import Qt, QTimer, pyqtSlot +from PyQt5.QtWidgets import ( + QDialog, + QMessageBox, + QVBoxLayout, +) from . import config from .animations import Animations @@ -17,22 +21,9 @@ from .util import AutoUI, IncrementableEnum, sounds -class KeyboardShortcutsMixin: - def keyPressEvent(self, event: QtGui.QKeyEvent): - if event.key() == QtCore.Qt.Key_Escape: - self.close() - elif event.key() in (QtCore.Qt.Key_Right, QtCore.Qt.Key_Space): - if self.main_window.widgets["pushButtonNext"].isEnabled(): - self.main_window.on_pushButtonNext_clicked() - elif event.key() in (QtCore.Qt.Key_Left, QtCore.Qt.Key_Backspace): - if self.main_window.widgets["pushButtonBack"].isEnabled(): - self.main_window.on_pushButtonBack_clicked() - elif event.key() == QtCore.Qt.Key_Home: - if self.main_window.widgets["pushButtonStart"].isEnabled(): - self.main_window.on_pushButtonStart_clicked() - - class MainWindow(QtWidgets.QMainWindow, AutoUI): + scene: TableScene + class State(IncrementableEnum): LAUNCH = 0 INIT = 1 @@ -54,17 +45,16 @@ class State(IncrementableEnum): State.WAIT_FOR_RIVER: "Suivant = afficher river", } - def __init__(self, show_new_hh_dialog: bool = True): + def __init__(self, show_new_hh_dialog: bool = True) -> None: super().__init__() + self.fullscreen_dialog = None - self.full_screen_widget = None - - self.background_color = "black" - self.webcam = "plain" + self.background_color = config.config["look"].get("background", "black") + self.webcam = config.config["look"].get("webcam", "plain") self.graphics_view: QtWidgets.QGraphicsView = self.widgets["graphicsView"] - self.hand_history: Union[None, HandHistory] = None + self.hand_history: None | HandHistory = None self.hh_settings = {} if config.config["behavior"].getboolean("replay_start_with_blinds_posted"): @@ -88,14 +78,14 @@ def __init__(self, show_new_hh_dialog: bool = True): if show_new_hh_dialog: self.on_actionNew_triggered() - def _make_table_scene(self): + def _make_table_scene(self) -> None: table_scene = TableScene(self) self.scene = table_scene if self.actionOpenGL.isChecked(): self.graphics_view.setViewport(QtWidgets.QOpenGLWidget()) self.graphics_view.setScene(table_scene) - def _initialize_hh(self): + def _initialize_hh(self) -> None: player_items = self.scene.get_active_players_after_button() stacks = [p.stack_item.stack for p in player_items] hand_history = HandHistory( @@ -114,12 +104,47 @@ def _initialize_hh(self): self.scene.sync_with_hh(self.hand_history) self.scene.request_action(self.hand_history) - def _fit_scene(self): - g = self.graphics_view - g.fitInView(self.scene.sceneRect(), QtCore.Qt.KeepAspectRatio) - self.scene.transform = g.transform() + def _fit_scene(self) -> None: + if self.fullscreen_dialog is None: + view = self.graphics_view + view.fitInView(self.scene.sceneRect(), QtCore.Qt.KeepAspectRatio) + else: + view = self.graphics_view + view.resetTransform() + + view.setTransformationAnchor(QtWidgets.QGraphicsView.NoAnchor) + view.setResizeAnchor(QtWidgets.QGraphicsView.NoAnchor) + view.setAlignment(Qt.AlignLeft | Qt.AlignTop) - def resizeEvent(self, event: QtGui.QResizeEvent): + view.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + view.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + view.setFrameShape(QtWidgets.QFrame.NoFrame) + + scene = self.scene + + dlg = self.fullscreen_dialog + + sw, sh = float(scene.width()), float(scene.height()) + vw, vh = float(dlg.width()), float(dlg.height()) + if sw <= 0 or sh <= 0 or vw <= 0 or vh <= 0: + return + + sx = vw / sw + sy = vh / sh + s = min(sx, sy) + + view.scale(s, s) + disp_w = sw * s + disp_h = sh * s + offset_x = (vw - disp_w) / 2.0 + offset_y = (vh - disp_h) / 2.0 + + view.setSceneRect(0, 0, sw, sh) + view.translate(offset_x / s, offset_y / s) + + self.scene.transform = view.transform() + + def resizeEvent(self, event: QtGui.QResizeEvent) -> None: self._fit_scene() def closeEvent(self, event: QtGui.QCloseEvent) -> None: @@ -131,12 +156,12 @@ def state(self): return self._state @state.setter - def state(self, v): + def state(self, v) -> None: self._state = v self.statusBar().showMessage(self.STATUS_MESSAGES[v]) @pyqtSlot() - def on_actionNew_triggered(self): + def on_actionNew_triggered(self) -> None: dialog = NewHandDialog(parent=self) code = dialog.exec() if code: @@ -171,10 +196,7 @@ def on_actionNew_triggered(self): conf = dialog.conf for name, field in dialog.FIELDS.items(): - if name == "Stack": - conf_name = "default_stack_in_bb" - else: - conf_name = name + conf_name = "default_stack_in_bb" if name == "Stack" else name conf[conf_name] = config.escape_dollar(dialog.get_field_value(name)) if field.checkable: conf[f"{name}_checked"] = str( @@ -189,13 +211,15 @@ def on_actionNew_triggered(self): self.update_buttons() @pyqtSlot() - def on_pushButtonNext_clicked(self): + def on_pushButtonNext_clicked(self) -> None: if self.state == self.State.INIT: self.scene.init_hh(self.hand_history) self.state = self.state.next() elif self.state == self.State.REPLAY: + assert isinstance(self.hand_history, HandHistory) self.replay_action_cursor += 1 if self.replay_action_cursor > len(self.hand_history.editable_actions()): + # = action closes before river but showdown is possible, eg, multiple allins preflop if ( self.replay_action_cursor == len(self.hand_history.editable_actions()) + 1 @@ -208,19 +232,27 @@ def on_pushButtonNext_clicked(self): self.scene.update_total_pot(self.hand_history) Animations.start() play_len = self.hand_history.play_length() - if self.replay_action_cursor == play_len - 4: + if self.hand_history.went_to_showdown: sounds["street"].play() - self.scene.show_flop() - elif self.replay_action_cursor == play_len - 3: - self.scene.show_turn() - sounds["street"].play() - elif self.replay_action_cursor == play_len - 2: - self.scene.show_river() - sounds["street"].play() - elif self.replay_action_cursor == play_len - 1: - self.scene.show_known_hands() + if self.replay_action_cursor == play_len - 4: + self.scene.show_known_hands() + elif self.replay_action_cursor == play_len - 3: + self.scene.show_flop() + elif self.replay_action_cursor == play_len - 2: + self.scene.show_turn() + elif self.replay_action_cursor == play_len - 1: + self.scene.show_river() + else: + sounds["call_closing"].play() + self.scene.update_winners(self.hand_history) else: - self.scene.update_winners(self.hand_history) + sounds["call_closing"].play() + QTimer.singleShot( + config.config["animation"].getint( + "BETS_TO_POT_ANIMATION_DURATION" + ), + partial(self.scene.update_winners, self.hand_history), + ) else: hand_history = self.hand_history.at_action(self.replay_action_cursor) self.scene.sync_with_hh(hand_history, update_board=False) @@ -249,7 +281,7 @@ def on_pushButtonNext_clicked(self): self.update_buttons() @pyqtSlot() - def on_pushButtonBack_clicked(self): + def on_pushButtonBack_clicked(self) -> None: if self.state == self.State.ACTIONS: self.hand_history.remove_last_action() self.scene.request_action(self.hand_history) @@ -286,7 +318,7 @@ def on_pushButtonBack_clicked(self): self.update_buttons() @pyqtSlot() - def on_pushButtonStart_clicked(self): + def on_pushButtonStart_clicked(self) -> None: self.replay_action_cursor = -1 hand_history = self.hand_history.at_action(self.replay_action_cursor) self.checkBoxEditMode.setChecked(False) @@ -297,7 +329,7 @@ def on_pushButtonStart_clicked(self): self.update_buttons() @pyqtSlot(bool) - def on_checkBoxEditMode_toggled(self, checked): + def on_checkBoxEditMode_toggled(self, checked) -> None: if checked: if self.state == self.State.INIT: return @@ -308,83 +340,129 @@ def on_checkBoxEditMode_toggled(self, checked): self.update_buttons() else: self.graphics_view.setInteractive(False) - self.on_actionFullScreen_triggered() self.state = self.State.REPLAY self.on_pushButtonStart_clicked() @pyqtSlot() - def on_actionFullScreen_triggered(self): - log.debug("Toggling full screen") - self.checkBoxEditMode.setChecked(False) - self.full_screen_widget = FullScreenView(main_window=self, scene=self.scene) + def on_actionFullScreen_triggered(self) -> None: + self.enter_fullscreen() + + def enter_fullscreen(self) -> None: + if self.fullscreen_dialog is None: + log.info("Entering full screen") + self.checkBoxEditMode.setChecked(False) + self.fullscreen_dialog = FullscreenDialog(self.graphics_view, parent=self) + self.fullscreen_dialog.resizeEvent = self.resizeEvent + screen = QtWidgets.QApplication.primaryScreen() + available = screen.availableGeometry() # excludes taskbar / OS panels + self.fullscreen_dialog.setGeometry(available) + self.fullscreen_dialog.showFullScreen() + self._fit_scene() + + def exit_fullscreen(self) -> None: + # Restore view back to main window layout + log.info("Exiting full screen") + dlg = self.fullscreen_dialog + if dlg is not None: + dlg.hide() + self.graphics_view.setParent(self.centralWidget()) + self.centralWidget().layout().insertWidget(0, self.graphics_view) + dlg.deleteLater() + self.fullscreen_dialog = None + self.fullscreen_dialog = None + self._fit_scene() @pyqtSlot() - def on_actionTableGreen_triggered(self): + def on_actionTableGreen_triggered(self) -> None: self.scene.change_table("green") @pyqtSlot() - def on_actionTableBlue_triggered(self): + def on_actionTableBlue_triggered(self) -> None: self.scene.change_table("blue") @pyqtSlot() - def on_actionWebcamPlain_triggered(self): + def on_actionTableNewGreen_triggered(self) -> None: + self.scene.change_table("new-green") + + @pyqtSlot() + def on_actionTableNewBlue_triggered(self) -> None: + self.scene.change_table("new-blue") + + @pyqtSlot() + def on_actionTableNewRed_triggered(self) -> None: + self.scene.change_table("new-red") + + @pyqtSlot() + def on_actionWebcamPlain_triggered(self) -> None: self.webcam = "plain" self.update_background() @pyqtSlot() - def on_actionWebcamBoth_triggered(self): + def on_actionWebcamBoth_triggered(self) -> None: self.webcam = "both" self.update_background() @pyqtSlot() - def on_actionWebcamLeft_triggered(self): + def on_actionWebcamLeft_triggered(self) -> None: self.webcam = "left" self.update_background() @pyqtSlot() - def on_actionWebcamRight_triggered(self): + def on_actionWebcamRight_triggered(self) -> None: self.webcam = "right" self.update_background() @pyqtSlot() - def on_actionBackgroundBlack_triggered(self): + def on_actionBackgroundBlack_triggered(self) -> None: self.background_color = "black" self.update_background() @pyqtSlot() - def on_actionBackgroundViolet_triggered(self): + def on_actionBackgroundViolet_triggered(self) -> None: self.background_color = "violet" self.update_background() @pyqtSlot() - def on_actionBackgroundBlue_triggered(self): + def on_actionBackgroundBlue_triggered(self) -> None: self.background_color = "blue" self.update_background() @pyqtSlot() - def on_actionBackgroundRed_triggered(self): + def on_actionBackgroundRed_triggered(self) -> None: self.background_color = "red" self.update_background() @pyqtSlot() - def on_actionBackBlue_triggered(self): + def on_actionBackRedNew1_triggered(self) -> None: + CardLook.change_back("red-new1") + + @pyqtSlot() + def on_actionBackRedNew2_triggered(self) -> None: + CardLook.change_back("red-new2") + + @pyqtSlot() + def on_actionBackRedNew3_triggered(self) -> None: + CardLook.change_back("red-new3") + + @pyqtSlot() + def on_actionBackBlue_triggered(self) -> None: CardLook.change_back("blue") @pyqtSlot() - def on_actionBackRed_triggered(self): + def on_actionBackRed_triggered(self) -> None: CardLook.change_back("red") @pyqtSlot() - def on_actionQuit_triggered(self): + def on_actionQuit_triggered(self) -> None: self.close() @pyqtSlot() - def on_actionSave_triggered(self): + def on_actionSave_triggered(self) -> None: log.info(f"Saving to {self.current_filename}") self.save_hh(self.current_filename) @pyqtSlot() - def on_actionSaveAs_triggered(self): + def on_actionSaveAs_triggered(self) -> None: result = QtWidgets.QFileDialog.getSaveFileName( self, "Choisissez le fichier HH à écrire", @@ -400,7 +478,7 @@ def on_actionSaveAs_triggered(self): self.save_hh(filename) @pyqtSlot() - def on_actionOpen_triggered(self): + def on_actionOpen_triggered(self) -> None: result = QtWidgets.QFileDialog.getOpenFileName( self, "Choisissez le fichier HH à charger", @@ -416,11 +494,11 @@ def on_actionOpen_triggered(self): self.load_hh(filename) @pyqtSlot(bool) - def on_actionHideHandsBeforeShowdown_triggered(self): + def on_actionHideHandsBeforeShowdown_triggered(self) -> None: self.scene.sync_with_hh(self.hand_history) @pyqtSlot(bool) - def on_actionOpenGL_triggered(self, checked): + def on_actionOpenGL_triggered(self, checked) -> None: if checked: self.graphics_view.setViewport(QtWidgets.QOpenGLWidget()) else: @@ -428,7 +506,7 @@ def on_actionOpenGL_triggered(self, checked): config.config["animation"]["opengl"] = str(checked) @pyqtSlot() - def on_actionRestoreConfig_triggered(self): + def on_actionRestoreConfig_triggered(self) -> None: log.info("Restoring config defaults") config.restore_defaults() self.statusBar().showMessage( @@ -446,7 +524,7 @@ def hide_cards_before_showdown(self): ).isChecked() ) - def save_hh(self, filename): + def save_hh(self, filename) -> None: hh_dict = self.hand_history.to_dict() hh_dict["n_decimals"] = TextItem.n_decimals hh_dict["player_names"] = [ @@ -465,12 +543,12 @@ def save_hh(self, filename): hh_dict["currency"] = self.scene.currency hh_dict["currency_is_after"] = self.scene.currency_is_after with open(filename, "w", encoding="utf-8") as fp: - json.dump(hh_dict, fp, cls=HHJSONEncoder) + json.dump(hh_dict, fp, cls=HHJSONEncoder, indent=2) - def load_hh(self, filename): + def load_hh(self, filename) -> None: log.info(f"Loading HH file: {filename}") self.current_filename = filename - with open(filename, "r", encoding="utf-8") as fp: + with open(filename, encoding="utf-8") as fp: hh_dict = json.load(fp, object_hook=json_hook) self.hand_history = HandHistory.from_dict(hh_dict) self.scene.load_dict(hh_dict, self.hand_history) @@ -484,7 +562,7 @@ def load_hh(self, filename): TextItem.n_decimals = n_digits self.pushButtonStart.clicked.emit() - def update_buttons(self): + def update_buttons(self) -> None: next_ = self.widgets["pushButtonNext"] back = self.widgets["pushButtonBack"] edit = self.widgets["checkBoxEditMode"] @@ -523,7 +601,9 @@ def update_buttons(self): start.setEnabled(True) - def update_background(self): + def update_background(self) -> None: + config.config["look"]["background"] = self.background_color + config.config["look"]["webcam"] = self.webcam file_name = f"{self.background_color}-{self.webcam}" try: self.scene.change_background(file_name) @@ -538,49 +618,36 @@ def update_background(self): msg.exec_() -class FullScreenView(QtWidgets.QGraphicsView, KeyboardShortcutsMixin): - def __init__(self, main_window: MainWindow, scene: TableScene): - super().__init__() - self.setInteractive(False) - self.setWindowFlag(QtCore.Qt.Window) - - if main_window.actionOpenGL.isChecked(): - self.setViewport(QtWidgets.QOpenGLWidget()) - - self.setRenderHint(QtGui.QPainter.HighQualityAntialiasing, True) - self.setRenderHint(QtGui.QPainter.SmoothPixmapTransform, True) - self.setRenderHint(QtGui.QPainter.Antialiasing, True) - self.setRenderHint(QtGui.QPainter.LosslessImageRendering, True) - - self.setOptimizationFlag(self.DontAdjustForAntialiasing, True) - self.setOptimizationFlag(self.DontClipPainter, True) - self.setOptimizationFlag(self.DontSavePainterState, True) - - self.setScene(scene) - self.scene = scene - self.main_window = main_window - app = QtWidgets.QApplication.instance() - - try: - size = app.screenAt(main_window.pos()).size() - except AttributeError: # sometimes it's not on any screen - size = app.primaryScreen().size() - - self.showFullScreen() - self.resize(size) - - xratio = size.width() / scene.sceneRect().width() - yratio = size.height() / scene.sceneRect().height() - xratio = yratio = min(xratio, yratio) - - self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) - self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) - - self.scale(xratio, yratio) +class FullscreenDialog(QDialog): + def __init__(self, view, parent: MainWindow) -> None: + super().__init__(parent) + self.setWindowFlags(Qt.Window | Qt.FramelessWindowHint) + self.setWindowState(Qt.WindowFullScreen) + self.setFocusPolicy(Qt.StrongFocus) + self.setFocus() + layout = QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(0) + layout.addWidget(view) + self.view = view + self.main_window = parent + + def keyPressEvent(self, event: QtGui.QKeyEvent) -> None: + if event.key() == QtCore.Qt.Key_Escape: + self.close() + self.main_window.exit_fullscreen() + elif event.key() in (QtCore.Qt.Key_Right, QtCore.Qt.Key_Space): + if self.main_window.widgets["pushButtonNext"].isEnabled(): + self.main_window.on_pushButtonNext_clicked() + elif event.key() in (QtCore.Qt.Key_Left, QtCore.Qt.Key_Backspace): + if self.main_window.widgets["pushButtonBack"].isEnabled(): + self.main_window.on_pushButtonBack_clicked() + elif event.key() == QtCore.Qt.Key_Home: + if self.main_window.widgets["pushButtonStart"].isEnabled(): + self.main_window.on_pushButtonStart_clicked() - def wheelEvent(self, event: QtGui.QWheelEvent) -> None: - # prevent scrolling on the scene in full screen - pass + def closeEvent(self, event) -> None: + self.main_window.exit_fullscreen() log = logging.getLogger(__name__) diff --git a/hh_creator/player.py b/hh_creator/player.py index b5011d9..c799b67 100644 --- a/hh_creator/player.py +++ b/hh_creator/player.py @@ -1,6 +1,6 @@ +import contextlib import logging from decimal import Decimal -from typing import Union from PyQt5 import QtCore, QtGui, QtWidgets @@ -13,7 +13,7 @@ class PlayerItemGroup(QtWidgets.QGraphicsItemGroup): - def __init__(self, id, n_cards=4, *a, **kw): + def __init__(self, id, n_cards=4, *a, **kw) -> None: super().__init__(*a, **kw) self.card_items = [] @@ -25,11 +25,11 @@ def __init__(self, id, n_cards=4, *a, **kw): hide_if_empty=True, content_is_number=True, point_size=config.config["text"].getint("player_bet_size"), - color=config.config["text"].get("player_bet_color"), + color="yellow", ) self.action_widget = ActionWidget(self, None) self.stack_item = StackItem() - self.name_item = NameItem() + self.name_item = NameItem(weight=75, point_size=20) self._adjust_positions() self._place_cards() @@ -37,7 +37,7 @@ def __init__(self, id, n_cards=4, *a, **kw): self.id = id self.active = True self.has_button = False - self.hh_position: Union[None, hh.Position] = None + self.hh_position: None | hh.Position = None self.addToGroup(self.bet_item) self.addToGroup(self.seat_item) @@ -45,7 +45,7 @@ def __init__(self, id, n_cards=4, *a, **kw): self.addToGroup(self.stack_item) self.addToGroup(self.action_widget_item) - def __repr__(self): + def __repr__(self) -> str: return ( f"" ) @@ -55,7 +55,7 @@ def n_cards(self): return self._n_cards @n_cards.setter - def n_cards(self, n): + def n_cards(self, n) -> None: self._n_cards = n self._place_cards() @@ -71,7 +71,7 @@ def _identify_item(self, event): log.debug(f"Passing event to {group}") return group - def _place_cards(self): + def _place_cards(self) -> None: seat_rect = self.seat_item.boundingRect() cards_width = self.card_items[0].boundingRect().width() + 60 * ( @@ -81,7 +81,7 @@ def _place_cards(self): card.setPos(seat_rect.width() / 2 - cards_width / 2 + 60 * i, -91) card.setVisible(i < self.n_cards) - def _adjust_positions(self): + def _adjust_positions(self) -> None: log.debug("Positioning player items") self.seat_item = Image.get("seat") self.seat_item.setPos(0, 0) @@ -110,14 +110,14 @@ def active(self): return self._active @active.setter - def active(self, active): + def active(self, active) -> None: if active: self.setOpacity(1) else: self.setOpacity(0.5) self._active = active - def reset(self): + def reset(self) -> None: self.bet_item.content = 0 self.name_item.content = "" self.hh_position = None @@ -125,10 +125,10 @@ def reset(self): self.action_widget_item.setVisible(False) self.addToGroup(self.action_widget_item) - def hide_actions_widget(self): + def hide_actions_widget(self) -> None: self.action_widget_item.setVisible(False) - def animate_stack_to_bet(self, amount, street_bet_amount=0, target=None): + def animate_stack_to_bet(self, amount, street_bet_amount=0, target=None) -> None: if target is None: target = self.bet_item @@ -140,9 +140,11 @@ def animate_stack_to_bet(self, amount, street_bet_amount=0, target=None): scene=self.scene(), callbacks=[lambda: setattr(self.bet_item, "content", street_bet_amount)], target_font=True, + target_item_center=target is not self.bet_item, + font_kwargs={"color": "yellow"}, ) - def sync_with_hh(self, hand_history): + def sync_with_hh(self, hand_history) -> None: log.debug("Syncing player with HH") hh_player = hand_history.get_player_by_position(self.hh_position) @@ -174,15 +176,15 @@ def sync_with_hh(self, hand_history): else: self.show_cards() - def show_cards(self): + def show_cards(self) -> None: for i, c in enumerate(self.card_items): c.setVisible(i < self.n_cards) - def hide_cards(self): + def hide_cards(self) -> None: for c in self.card_items: c.setVisible(False) - def show_actions_widget(self, hand_history: hh.HandHistory): + def show_actions_widget(self, hand_history: hh.HandHistory) -> None: possible = hand_history.possible_action_types() if hh.ActionType.RAISE in possible: min_raise = hand_history.minimum_raise() @@ -200,7 +202,7 @@ def show_actions_widget(self, hand_history: hh.HandHistory): self.action_widget.set_possible_actions(possible) self.action_widget_item.setVisible(True) - def add_action(self, action_type, amount=Decimal(0)): + def add_action(self, action_type, amount=Decimal(0)) -> None: hand_history: hh.HandHistory = self.scene().parent().hand_history hh_player = hand_history.get_player_by_position(self.hh_position) @@ -234,27 +236,23 @@ def add_action(self, action_type, amount=Decimal(0)): self.scene().parent().update_buttons() self.scene().request_action(hand_history) - def contextMenuEvent(self, event: QtWidgets.QGraphicsSceneContextMenuEvent): + def contextMenuEvent(self, event: QtWidgets.QGraphicsSceneContextMenuEvent) -> None: if not self.active: return item = self._identify_item(event) if item is not self: - try: + with contextlib.suppress(RuntimeError, AttributeError): item.contextMenuEvent(event) - except (RuntimeError, AttributeError): - pass - def wheelEvent(self, event: QtWidgets.QGraphicsSceneWheelEvent): + def wheelEvent(self, event: QtWidgets.QGraphicsSceneWheelEvent) -> None: if not self.active: return item = self._identify_item(event) if item is not self: - try: + with contextlib.suppress(RuntimeError, AttributeError): item.wheelEvent(event) - except (RuntimeError, AttributeError): - pass - def mousePressEvent(self, event: QtWidgets.QGraphicsSceneMouseEvent): + def mousePressEvent(self, event: QtWidgets.QGraphicsSceneMouseEvent) -> None: main_window = self.scene().parent() if main_window.state == main_window.State.INIT: if event.button() == QtCore.Qt.MiddleButton: @@ -271,31 +269,25 @@ def mousePressEvent(self, event: QtWidgets.QGraphicsSceneMouseEvent): if self.active: item = self._identify_item(event) if item is not self: - try: + with contextlib.suppress(RuntimeError, AttributeError): item.mousePressEvent(event) - except (RuntimeError, AttributeError): - pass - def mouseReleaseEvent(self, event: QtWidgets.QGraphicsSceneMouseEvent): + def mouseReleaseEvent(self, event: QtWidgets.QGraphicsSceneMouseEvent) -> None: item = self._identify_item(event) if item is not self: - try: + with contextlib.suppress(RuntimeError, AttributeError): item.mouseReleaseEvent(event) - except (RuntimeError, AttributeError): - pass - def mouseMoveEvent(self, event: QtWidgets.QGraphicsSceneMouseEvent): + def mouseMoveEvent(self, event: QtWidgets.QGraphicsSceneMouseEvent) -> None: item = self._identify_item(event) if item is not self: - try: + with contextlib.suppress(RuntimeError, AttributeError): item.mouseMoveEvent(event) - except (RuntimeError, AttributeError): - pass - def keyPressEvent(self, event: QtGui.QKeyEvent): + def keyPressEvent(self, event: QtGui.QKeyEvent) -> None: self.action_widget_item.keyPressEvent(event) - def keyReleaseEvent(self, event: QtGui.QKeyEvent): + def keyReleaseEvent(self, event: QtGui.QKeyEvent) -> None: self.action_widget_item.keyReleaseEvent(event) # def hoverEnterEvent(self, event: 'QGraphicsSceneHoverEvent'): diff --git a/hh_creator/poker_enum.py b/hh_creator/poker_enum.py index 4b9bc68..16867ed 100644 --- a/hh_creator/poker_enum.py +++ b/hh_creator/poker_enum.py @@ -9,7 +9,7 @@ class _PokerEnumMeta(enum.EnumMeta): - def __init__(self, clsname, bases, classdict): + def __init__(self, clsname, bases, classdict) -> None: # make sure we only have tuple values, not single values for member in self.__members__.values(): values = member._value_ @@ -57,15 +57,15 @@ def __lt__(self, other): class PokerEnum(_OrderableMixin, enum.Enum, metaclass=_PokerEnumMeta): - def __str__(self): + def __str__(self) -> str: return str(self._value_[0]) - def __repr__(self): + def __repr__(self) -> str: val = self._value_[0] apostrophe = "'" if isinstance(val, str) else "" return f"{self.__class__.__name__}({apostrophe}{val}{apostrophe})" - def __format__(self, format_spec): + def __format__(self, format_spec) -> str: return str(self._value_[0]) @property @@ -75,7 +75,7 @@ def val(self): class _ReprMixin: - def __repr__(self): + def __repr__(self) -> str: return f"{self.__class__.__name__}('{self}')" diff --git a/hh_creator/resource/img/cards/back-red-new1.png b/hh_creator/resource/img/cards/back-red-new1.png new file mode 100644 index 0000000..8254a80 Binary files /dev/null and b/hh_creator/resource/img/cards/back-red-new1.png differ diff --git a/hh_creator/resource/img/cards/back-red-new2.png b/hh_creator/resource/img/cards/back-red-new2.png new file mode 100644 index 0000000..986dd5e Binary files /dev/null and b/hh_creator/resource/img/cards/back-red-new2.png differ diff --git a/hh_creator/resource/img/cards/back-red-new3.png b/hh_creator/resource/img/cards/back-red-new3.png new file mode 100644 index 0000000..04fa182 Binary files /dev/null and b/hh_creator/resource/img/cards/back-red-new3.png differ diff --git a/hh_creator/resource/img/cards_cut/back-red-new1.png b/hh_creator/resource/img/cards_cut/back-red-new1.png new file mode 100644 index 0000000..8254a80 Binary files /dev/null and b/hh_creator/resource/img/cards_cut/back-red-new1.png differ diff --git a/hh_creator/resource/img/cards_cut/back-red-new2.png b/hh_creator/resource/img/cards_cut/back-red-new2.png new file mode 100644 index 0000000..986dd5e Binary files /dev/null and b/hh_creator/resource/img/cards_cut/back-red-new2.png differ diff --git a/hh_creator/resource/img/cards_cut/back-red-new3.png b/hh_creator/resource/img/cards_cut/back-red-new3.png new file mode 100644 index 0000000..04fa182 Binary files /dev/null and b/hh_creator/resource/img/cards_cut/back-red-new3.png differ diff --git a/hh_creator/resource/img/chips/dealer.png b/hh_creator/resource/img/chips/dealer.png new file mode 100644 index 0000000..76e68c6 Binary files /dev/null and b/hh_creator/resource/img/chips/dealer.png differ diff --git a/hh_creator/resource/img/table/new-blue.png b/hh_creator/resource/img/table/new-blue.png new file mode 100644 index 0000000..105b810 Binary files /dev/null and b/hh_creator/resource/img/table/new-blue.png differ diff --git a/hh_creator/resource/img/table/new-green.png b/hh_creator/resource/img/table/new-green.png new file mode 100644 index 0000000..f56fe63 Binary files /dev/null and b/hh_creator/resource/img/table/new-green.png differ diff --git a/hh_creator/resource/img/table/new-red.png b/hh_creator/resource/img/table/new-red.png new file mode 100644 index 0000000..b45accd Binary files /dev/null and b/hh_creator/resource/img/table/new-red.png differ diff --git a/hh_creator/resource/ui/MainWindow.ui b/hh_creator/resource/ui/MainWindow.ui index 7670346..7b7c704 100644 --- a/hh_creator/resource/ui/MainWindow.ui +++ b/hh_creator/resource/ui/MainWindow.ui @@ -117,6 +117,9 @@ Couleur de la table + + + @@ -124,6 +127,9 @@ Dos des cartes + + + @@ -218,6 +224,21 @@ F11 + + + Vert (nouveau) + + + + + Bleu (nouveau) + + + + + Rouge (nouveau) + + Vert @@ -228,6 +249,21 @@ Bleu + + + Rouge (nouveau #1) + + + + + Rouge (nouveau #2) + + + + + Rouge (nouveau #3) + + Rouge diff --git a/hh_creator/scene.py b/hh_creator/scene.py index c386abe..1bc91f4 100644 --- a/hh_creator/scene.py +++ b/hh_creator/scene.py @@ -13,7 +13,9 @@ class TableScene(QtWidgets.QGraphicsScene): - def __init__(self, parent): + table_item: Qt.QGraphicsSvgItem | Qt.QGraphicsPixmapItem + + def __init__(self, parent) -> None: super().__init__(parent) self._create_background() self._create_button() @@ -35,7 +37,7 @@ def __init__(self, parent): self.hide_board() - def _create_text_items(self): + def _create_text_items(self) -> None: self.central_pot_item = TextItem( prefix=config.config["text"].get("main_pot_prefix") + " ", content_is_number=True, @@ -85,13 +87,13 @@ def _create_text_items(self): self.central_pot_item, ] + self.side_pot_items - def _create_button(self): - self.button_item = Image.get(Path("chips") / "dealer") + def _create_button(self) -> None: + self.button_item = Image.get(Path("chips") / "dealer", force_png=True) self.button_item.setVisible(False) - self.button_item.setScale(config.config["look"].getfloat("button_scale")) + self.button_item.setScale(config.config["look"].getfloat("button_scale") * 2) self.addItem(self.button_item) - def _create_board(self): + def _create_board(self) -> None: self.board = [ CardItem(scale_factor=config.config["look"].getfloat("board_scale")) for _ in range(5) @@ -111,7 +113,7 @@ def _create_board(self): ) self.addItem(card) - def _update_currency(self): + def _update_currency(self) -> None: items = [self.central_pot_item, self.total_pot_item] + self.side_pot_items items.extend(p.stack_item.stack_item for p in self.player_items) items.extend(p.bet_item for p in self.player_items) @@ -120,7 +122,7 @@ def _update_currency(self): setattr(i, "currency", self._currency) setattr(i, "currency_is_after", self._currency_is_after) - def _get_highlight_effect(self): + def _get_highlight_effect(self) -> None: highlight_effect = QtWidgets.QGraphicsDropShadowEffect() highlight_effect.setColor(QtGui.QColor("white")) highlight_effect.setOffset(0) @@ -128,45 +130,53 @@ def _get_highlight_effect(self): self.highlight_effect = highlight_effect - def _get_player_item_from_hh_position(self, position: hh.Position): + def _get_player_item_from_hh_position( + self, position: hh.Position + ) -> PlayerItemGroup: for p in self.active_players(): if p.hh_position == position: return p else: raise ValueError(f"{position} not found in {self.player_items}") - def _create_background(self): + def _create_background(self) -> None: table_item = Image.get(Path("table") / config.config["look"].get("table")) - webcam = config.config["look"].get("webcam") - if "-" not in webcam: - webcam = f"black-{webcam}" + webcam = config.config["look"].get("webcam", "plain") + bg_color = config.config["look"].get("background", "black") try: - background = Image.get(Path("background") / webcam) + background = Image.get(Path("background") / f"{bg_color}-{webcam}") except FileNotFoundError: + config.config["look"]["background"] = "black" + config.config["look"]["webcam"] = "plain" background = Image.get(Path("background") / "black-plain") - shadow = QtWidgets.QGraphicsDropShadowEffect() - shadow.setBlurRadius(config.config["look"].getfloat("TABLE_SHADOW_RADIUS")) - table_item.setGraphicsEffect(shadow) - table_rectf = Qt.QRectF(table_item.boundingRect()) + table_item.setGraphicsEffect(self._create_table_shadow()) + table_rectf = Qt.QRectF(background.boundingRect()) self.background_item = background self.background_item.setZValue(-100) - self.table_shadow = shadow self.table_item = table_item self.setSceneRect(table_rectf) self.addItem(background) + self.resize_table() self.addItem(table_item) - def _place_players(self): + @staticmethod + def _create_table_shadow() -> QtWidgets.QGraphicsDropShadowEffect: + shadow = QtWidgets.QGraphicsDropShadowEffect() + shadow.setBlurRadius(config.config["look"].getfloat("TABLE_SHADOW_RADIUS")) + return shadow + + def _place_players(self) -> None: self.reset_button() self._clear_text() self.board_street = hh.Street.ANTE log.debug("Placing players") self.button_position = [] conf = config.config[f"{self.n_seats}players"] - center_pos = get_center(self.table_item) + center_pos = [960.0, 540.0] + # print(center_pos) center_pos[1] += 100 for i, player in enumerate(self.player_items, start=1): @@ -239,14 +249,14 @@ def _place_players(self): ] ) - def _clear_text(self): + def _clear_text(self) -> None: for i in self.text_items: if i.content_is_number: i.content = 0 else: i.content = "" - def mouseDoubleClickEvent(self, event: QtWidgets.QGraphicsSceneMouseEvent): + def mouseDoubleClickEvent(self, event: QtWidgets.QGraphicsSceneMouseEvent) -> None: # TODO: replace this with full screen print(event.scenePos()) pass @@ -256,7 +266,7 @@ def currency(self): return self._currency @currency.setter - def currency(self, currency): + def currency(self, currency) -> None: self._currency = currency self._update_currency() @@ -265,7 +275,7 @@ def currency_is_after(self): return self._currency_is_after @currency_is_after.setter - def currency_is_after(self, val): + def currency_is_after(self, val) -> None: self._currency_is_after = val self._update_currency() @@ -282,34 +292,61 @@ def n_seats(self): return self._n_seats @n_seats.setter - def n_seats(self, value): + def n_seats(self, value) -> None: self._n_seats = value self._place_players() self.hide_board() CardItem.reset() - def set_all_stacks(self, value): + def set_all_stacks(self, value) -> None: for p in self.player_items: p.stack_item.stack = value - def set_n_cards(self, value): + def set_n_cards(self, value) -> None: for p in self.player_items: p.n_cards = value - def change_table(self, color): + def change_table(self, color) -> None: config.config["look"]["table"] = color self.removeItem(self.table_item) - self.table_item.deleteLater() + if isinstance(self.table_item, Qt.QGraphicsSvgItem): + self.table_item.deleteLater() self.table_item = Image.get(Path("table") / color) - self.table_item.setGraphicsEffect(self.table_shadow) + self.table_item.setGraphicsEffect(self._create_table_shadow()) self.table_item.setZValue(-50) + self.resize_table() self.addItem(self.table_item) + def resize_table(self) -> None: + if not isinstance(self.table_item, Qt.QGraphicsPixmapItem): + return + target_w, target_h = self.width(), self.height() + pixmap_w, pixmap_h = ( + self.table_item.pixmap().width(), + self.table_item.pixmap().height(), + ) + if pixmap_w == 0 or pixmap_h == 0: + return + scale_x = target_w / pixmap_w + scale_y = target_h / pixmap_h + scale = min(scale_x, scale_y) + self.table_item.setScale(scale) + def change_background(self, name): - config.config["look"]["webcam"] = name + try: + bg, webcam = name.split("-") + except ValueError: + # workaround for bogus values in config in previous version + log.warning( + "Invalid value for background: %s, resetting to black-plain", name + ) + bg = "black" + webcam = "plain" + config.config["look"]["background"] = bg + config.config["look"]["webcam"] = webcam self.background_item.setPixmap(Image.get(Path("background") / name).pixmap()) - def load_dict(self, hh_dict, hand_history): + def load_dict(self, hh_dict, hand_history) -> None: self._clear_text() self.hero_idx = hh_dict["hero"] @@ -371,21 +408,21 @@ def get_active_players_after_button(self): players = players[::-1] return players - def show_all_players(self): + def show_all_players(self) -> None: for i, p in enumerate(self.player_items): p.setVisible(i < self.n_seats) - def hide_inactive_players(self): + def hide_inactive_players(self) -> None: for p in self.player_items: if not p.active: p.setVisible(False) - def reset_button(self): + def reset_button(self) -> None: for p in self.player_items: p.has_button = False self.button_item.setVisible(False) - def give_button(self, player): + def give_button(self, player) -> None: self.reset_button() player.has_button = True i = self.player_items.index(player) @@ -395,13 +432,14 @@ def give_button(self, player): self.button_item.setPos(*pos) self.parent().update_buttons() - def bets_to_pot_animations(self, hand_history, add_last_call=True): + def bets_to_pot_animations(self, hand_history, add_last_call=True) -> None: log.info("Animating bets to pot") last_action = hand_history.last_action if add_last_call and last_action.action_type == hh.ActionType.CALL: p = self._get_player_item_from_hh_position(last_action.player.position) p.animate_stack_to_bet(last_action.amount, 0, target=self.central_pot_item) p.bet_item.content = last_action.amount + p.bet_item.setVisible(False) side_pots = hand_history.side_pots() @@ -414,6 +452,9 @@ def bets_to_pot_animations(self, hand_history, add_last_call=True): ) bet_item = player_item.bet_item + if not bet_item.isVisible(): + continue + Animations.text( source=bet_item, target=pot_item, @@ -421,6 +462,7 @@ def bets_to_pot_animations(self, hand_history, add_last_call=True): "BETS_TO_POT_ANIMATION_DURATION" ), scene=self, + target_item_center=True, ) pot_item.content = side_pot.amount @@ -430,7 +472,7 @@ def animate_pot_to_winner( position: hh.Position, pot_item=None, split=1, - ): + ) -> None: player_item = self._get_player_item_from_hh_position(position) if pot_item is None: @@ -467,7 +509,7 @@ def animate_pot_to_winner( # print(amount) - def ante_animations(self, hand_history: hh.HandHistory): + def ante_animations(self, hand_history: hh.HandHistory) -> None: for i, p in enumerate(self.active_players()): Animations.text( source=p.stack_item.stack_item, @@ -479,7 +521,7 @@ def ante_animations(self, hand_history: hh.HandHistory): scene=self, ) - def bb_ante_animation(self, hand_history: hh.HandHistory): + def bb_ante_animation(self, hand_history: hh.HandHistory) -> None: Animations.text( source=self.bb_player().stack_item.stack_item, content=hand_history.bb_ante, @@ -490,11 +532,11 @@ def bb_ante_animation(self, hand_history: hh.HandHistory): scene=self, ) - def clear_side_pots(self): + def clear_side_pots(self) -> None: for pot_item in [self.central_pot_item] + self.side_pot_items: pot_item.content = 0 - def show_down(self, hand_history): + def show_down(self, hand_history) -> None: for side_pot, side_pot_item in zip( hand_history.side_pots(), [self.central_pot_item] + self.side_pot_items ): @@ -514,11 +556,11 @@ def show_down(self, hand_history): w.hh_position, side_pot_item, split=len(winners) ) - def clear_bet_items(self): + def clear_bet_items(self) -> None: for p in self.player_items: p.bet_item.content = 0 - def update_winners(self, hand_history): + def update_winners(self, hand_history) -> None: Animations.reset() self.show_known_hands() if hand_history.winner is None: @@ -530,7 +572,7 @@ def update_winners(self, hand_history): self._clear_text() Animations.start() - def update_total_pot(self, hand_history): + def update_total_pot(self, hand_history) -> None: central_pot = hand_history.central_pot total_pot = hand_history.total_pot @@ -539,7 +581,7 @@ def update_total_pot(self, hand_history): else: self.total_pot_item.content = 0 - def sync_with_hh(self, hand_history, rebuild_pots=False, update_board=True): + def sync_with_hh(self, hand_history, rebuild_pots=False, update_board=True) -> None: log.debug("Syncing table with HH") Animations.reset() @@ -646,34 +688,34 @@ def sync_with_hh(self, hand_history, rebuild_pots=False, update_board=True): Animations.start() - def show_known_hands(self): + def show_known_hands(self) -> None: for p in self.active_players(): for c in p.card_items: c.discover() - def hide_hands(self, hide_hero=False): + def hide_hands(self, hide_hero=False) -> None: for i, p in enumerate(self.active_players()): if not hide_hero and i == self.hero_idx: continue for c in p.card_items: c.hide_face() - def hide_board(self): + def hide_board(self) -> None: for c in self.board: c.setVisible(False) - def show_flop(self): + def show_flop(self) -> None: for c in self.board[:3]: c.setVisible(True) for c in self.board[3:]: c.setVisible(False) - def show_turn(self): + def show_turn(self) -> None: for c in self.board[:4]: c.setVisible(True) self.board[4].setVisible(False) - def show_river(self): + def show_river(self) -> None: for c in self.board: c.setVisible(True) @@ -688,15 +730,15 @@ def bb_player(self): if p.hh_position == hh.Position.BB: return p - def reset_bet_items(self): + def reset_bet_items(self) -> None: for p in self.active_players(): p.bet_item.content = 0 - def hide_all_actions_widget(self): + def hide_all_actions_widget(self) -> None: for p in self.player_items: p.hide_actions_widget() - def request_action(self, hand_history: hh.HandHistory): + def request_action(self, hand_history: hh.HandHistory) -> None: self.sync_with_hh(hand_history) self.hide_all_actions_widget() next_hh_player = hand_history.current_player @@ -707,7 +749,7 @@ def request_action(self, hand_history: hh.HandHistory): ) next_player_item.show_actions_widget(hand_history) - def init_hh(self, hand_history: hh.HandHistory): + def init_hh(self, hand_history: hh.HandHistory) -> None: players = self.get_active_players_after_button() hand_history.set_stacks([p.stack_item.stack for p in players]) for p, hhp in zip(players, hand_history.players): @@ -719,7 +761,7 @@ def init_hh(self, hand_history: hh.HandHistory): self.request_action(hand_history) self.hide_inactive_players() - def show_all_active_players_cards(self): + def show_all_active_players_cards(self) -> None: for p in self.active_players(): p.show_cards() diff --git a/hh_creator/text.py b/hh_creator/text.py index b53be39..375dc9a 100644 --- a/hh_creator/text.py +++ b/hh_creator/text.py @@ -1,6 +1,5 @@ import logging import time -import typing from PyQt5 import QtCore, QtGui, QtWidgets @@ -29,7 +28,7 @@ def __init__( italic=False, *a, **kwa, - ): + ) -> None: super().__init__(*a, **kwa) if self._fontstr is None: _load_font() @@ -69,7 +68,7 @@ def content(self): return self._content @content.setter - def content(self, value): + def content(self, value) -> None: log.debug(f"Updating content of {self}") if self.hide_if_empty and not value: log.debug(f"Hiding {self} because empty value") @@ -108,25 +107,24 @@ def get_pos_if_content(self, content): def set_center( self, - pos: typing.Union[QtCore.QPointF, QtCore.QPoint, float], + pos: QtCore.QPointF | QtCore.QPoint | float, y: float = None, scene=False, - ): + ) -> None: if y is not None: pos = QtCore.QPointF(pos, y) rect = self.boundingRect() - if scene: - rpos = self.scenePos() - else: - rpos = self.pos() + rpos = self.scenePos() if scene else self.pos() offset = pos - rect.center() - rpos super().moveBy(offset.x(), offset.y()) class StackItem(QtWidgets.QGraphicsItemGroup): - def __init__(self, *a, **kw): + def __init__(self, *a, **kw) -> None: super().__init__(*a, **kw) - self.stack_item = TextItem(hide_if_empty=False, content_is_number=True) + self.stack_item = TextItem( + hide_if_empty=False, content_is_number=True, color="yellow" + ) self.action_item = TextItem() self.addToGroup(self.stack_item) self.addToGroup(self.action_item) @@ -138,17 +136,17 @@ def stack(self): return decimal_conversion(self.stack_item.content) @stack.setter - def stack(self, value): + def stack(self, value) -> None: self.timer.timeout.connect(lambda: setattr(self.stack_item, "content", value)) if not self.timer.isActive(): self.stack_item.content = value @property - def action(self): + def action(self) -> None: return @action.setter - def action(self, value): + def action(self, value) -> None: log.debug(f"Setting timer to briefly display action {value}") self.action_item.content = value self.stack_item.setVisible(False) @@ -162,17 +160,17 @@ def action(self, value): ) timer.start(config.config["animation"].getint("LAST_ACTION_DURATION")) - def set_center(self, *a): + def set_center(self, *a) -> None: self.stack_item.set_center(*a) self.action_item.set_center(*a) - def dialog(self): + def dialog(self) -> None: win = self.scene().parent() dialog = StackDialog(win, self.stack) if dialog.exec(): self.stack = dialog.get_value() - def mousePressEvent(self, event: QtWidgets.QGraphicsSceneMouseEvent): + def mousePressEvent(self, event: QtWidgets.QGraphicsSceneMouseEvent) -> None: log.debug("Click on stack") win = self.scene().parent() if win.state != win.State.INIT: @@ -182,7 +180,7 @@ def mousePressEvent(self, event: QtWidgets.QGraphicsSceneMouseEvent): class NameItem(TextItem): - def mousePressEvent(self, event: QtWidgets.QGraphicsSceneMouseEvent): + def mousePressEvent(self, event: QtWidgets.QGraphicsSceneMouseEvent) -> None: log.debug(f"Click on {self}") win = self.scene().parent() # noinspection PyTypeChecker @@ -191,7 +189,7 @@ def mousePressEvent(self, event: QtWidgets.QGraphicsSceneMouseEvent): self.content = dialog.widgets["lineEdit"].text() -def _load_font(): +def _load_font() -> None: _id = QtGui.QFontDatabase.addApplicationFont(str(RESOURCE_PATH / "Lato-Black.ttf")) _fontstr = QtGui.QFontDatabase.applicationFontFamilies(_id) try: diff --git a/hh_creator/util.py b/hh_creator/util.py index aa5faa8..fc269b8 100644 --- a/hh_creator/util.py +++ b/hh_creator/util.py @@ -1,8 +1,10 @@ import logging from decimal import Decimal, InvalidOperation from enum import Enum +from functools import cache, total_ordering +from pathlib import Path -from PyQt5 import Qt, QtCore, QtGui, QtWidgets, uic +from PyQt5 import Qt, QtGui, QtWidgets, uic from .config import RESOURCE_PATH from .poker_enum import PokerEnum @@ -27,6 +29,7 @@ class ActionType(PokerEnum): STRADDLE = ("straddle",) +@total_ordering class IncrementableEnum(Enum): def next(self): return self.__class__(self._value_ + 1) @@ -34,38 +37,51 @@ def next(self): def prev(self): return self.__class__(self._value_ - 1) - def __str__(self): + def __str__(self) -> str: return self._name_ - def __gt__(self, other): - return self._value_ > other._value_ + def __lt__(self, other): + return self._value_ < other._value_ + + def __eq__(self, other): + return self._value_ == other._value_ def __sub__(self, other): return self._value_ - other._value_ + def __hash__(self) -> int: + return self._value_ + class Image: IMG_PATH = RESOURCE_PATH / "img" @staticmethod - def get(filename, parent=None): + def get( + filename: str, force_png: bool = False + ) -> Qt.QGraphicsSvgItem | Qt.QGraphicsPixmapItem: path = Image.IMG_PATH / f"{filename}" - if path.with_suffix(".svg").exists(): + if path.with_suffix(".svg").exists() and not force_png: log.debug(f"Loading {path}") - item = Qt.QGraphicsSvgItem(str(path.with_suffix(".svg")), parent) + item = Qt.QGraphicsSvgItem(str(path.with_suffix(".svg")), parent=None) return item elif path.with_suffix(".png").exists(): log.debug(f"Loading {path}") - img = QtGui.QPixmap(str(path.with_suffix(".png")), parent) - return Qt.QGraphicsPixmapItem(img) + img = Image._get_pixmap(path) + return Qt.QGraphicsPixmapItem(img, parent=None) else: - raise FileNotFoundError + raise FileNotFoundError(path) + + @staticmethod + @cache + def _get_pixmap(path: Path) -> QtGui.QPixmap: + return QtGui.QPixmap(str(path.with_suffix(".png")), None) class AutoUI: UI_PATH = RESOURCE_PATH / "ui" - def __init__(self): + def __init__(self) -> None: self._load_ui() self.widgets = {} i = 0 @@ -77,20 +93,16 @@ def __init__(self): i += 1 self.widgets[key] = obj - def _load_ui(self): + def _load_ui(self) -> None: uic.loadUi(self.UI_PATH / f"{type(self).__name__}.ui", self) class AmountValidatorWithBounds(QtGui.QDoubleValidator): # Forbid "," that Decimal() does not like. - LOCALE = QtCore.QLocale() - log.debug(f"Locale is {LOCALE}") - LOCALE.setNumberOptions(QtCore.QLocale.RejectGroupSeparator) - def __init__(self, minimum=None, maximum=None, *a, **kw): + def __init__(self, minimum=None, maximum=None, *a, **kw) -> None: super().__init__(*a, **kw) - self.setNotation(QtGui.QDoubleValidator.StandardNotation) - self.setLocale(self.LOCALE) + if minimum is not None: self.setBottom(minimum) if maximum is not None: @@ -98,7 +110,7 @@ def __init__(self, minimum=None, maximum=None, *a, **kw): class IntValidator(QtGui.QIntValidator): - def __init__(self): + def __init__(self) -> None: super().__init__() self.setBottom(1) @@ -126,10 +138,7 @@ def barycenter(x1, y1, x2, y2, w1=1, w2=2): def get_center(item, scene=False): - if scene: - pos = item.scenePos() - else: - pos = item.pos() + pos = item.scenePos() if scene else item.pos() x = pos.x() y = pos.y() rect = item.boundingRect().center() @@ -150,7 +159,7 @@ def amount_format(x, n_decimals=3): BLINDS = [ActionType.SB, ActionType.BB, ActionType.STRADDLE] -def init_sounds(): +def init_sounds() -> None: # we need to import it here or else tests cannot be played in CI: # ImportError: libpulse-mainloop-glib.so.0: cannot open shared object file: No such file or directory from PyQt5 import QtMultimedia @@ -170,6 +179,7 @@ def init_sounds(): ActionType.BB: _sounds["bet"], ActionType.ANTE: _sounds["bet"], ActionType.STRADDLE: _sounds["bet"], + # "win": _sounds["bet"], "street": _sounds["street"], "call_closing": _sounds["call_closing"], } diff --git a/pyproject.toml b/pyproject.toml index edd0136..72b13be 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,6 +26,8 @@ dev = [ "pre-commit>=4.5.1", "pyproject-pre-commit>=0.3.6", "pytest>=8.3.4", + "ruff>=0.15.0", + "ty>=0.0.15", ] [build-system] @@ -34,5 +36,5 @@ build-backend = "setuptools.build_meta" [tool.setuptools_scm] -[tool.ruff.lint] -extend-select = ["I"] +# [tool.ruff.lint] +# extend-select = ["ANN", "C4", "I", "SIM", "TRY", "UP"] diff --git a/test/fold_flop.hh b/test/fold_flop.hh new file mode 100644 index 0000000..76d170c --- /dev/null +++ b/test/fold_flop.hh @@ -0,0 +1 @@ +{"small_blind": {"decimal": "0.5"}, "big_blind": {"decimal": "1"}, "actions": [{"type": "post SB", "amount": {"decimal": "0.5"}}, {"type": "post BB", "amount": {"decimal": "1"}}, {"type": "call", "amount": {"decimal": "0.5"}}, {"type": "check", "amount": {"decimal": "0"}}, {"type": "bet", "amount": {"decimal": "1"}}, {"type": "fold", "amount": {"decimal": "0"}}], "ante": {"decimal": "0"}, "bb_ante": {"decimal": "0"}, "players": [{"decimal": "15"}, {"decimal": "15"}], "current_street": null, "current_player": null, "total_pot": {"decimal": "3.0"}, "_blinds_posted": true, "winner": {"decimal": "15"}, "n_straddle": 0, "largest_blind": {"decimal": "1"}, "n_decimals": 1, "player_names": ["SB", "BB"], "n_seats": 2, "n_cards": 2, "active_seats": [0, 1], "button_idx": 0, "hero": 0, "hands": [["9d", "3s", "xx", "xx"], ["xx", "xx", "xx", "xx"]], "board": ["Ah", "Kd", "2c", "xx", "xx"], "currency": "", "currency_is_after": true} diff --git a/test/fold_preflop.hh b/test/fold_preflop.hh new file mode 100644 index 0000000..e3af43e --- /dev/null +++ b/test/fold_preflop.hh @@ -0,0 +1 @@ +{"small_blind": {"decimal": "0.5"}, "big_blind": {"decimal": "1"}, "actions": [{"type": "post ante", "amount": {"decimal": "0.125"}}, {"type": "post ante", "amount": {"decimal": "0.125"}}, {"type": "post SB", "amount": {"decimal": "0.5"}}, {"type": "post BB", "amount": {"decimal": "1"}}, {"type": "raise", "amount": {"decimal": "26.0"}}, {"type": "fold", "amount": {"decimal": "0"}}], "ante": {"decimal": "0.125"}, "bb_ante": {"decimal": "0"}, "players": [{"decimal": "100"}, {"decimal": "100"}], "current_street": null, "current_player": null, "total_pot": {"decimal": "28.250"}, "_blinds_posted": true, "winner": {"decimal": "100"}, "n_straddle": 0, "largest_blind": {"decimal": "1"}, "n_decimals": 3, "player_names": ["SB", "BB"], "n_seats": 2, "n_cards": 2, "active_seats": [0, 1], "button_idx": 0, "hero": 0, "hands": [["xx", "xx", "xx", "xx"], ["xx", "xx", "xx", "xx"]], "board": ["xx", "xx", "xx", "xx", "xx"], "currency": "BB", "currency_is_after": true} diff --git a/test/hu1.hh b/test/hu1.hh new file mode 100644 index 0000000..be73c95 --- /dev/null +++ b/test/hu1.hh @@ -0,0 +1 @@ +{"small_blind": {"decimal": "0.5"}, "big_blind": {"decimal": "1"}, "actions": [{"type": "post SB", "amount": {"decimal": "0.5"}}, {"type": "post BB", "amount": {"decimal": "1"}}, {"type": "call", "amount": {"decimal": "0.5"}}, {"type": "check", "amount": {"decimal": "0"}}, {"type": "check", "amount": {"decimal": "0"}}, {"type": "check", "amount": {"decimal": "0"}}, {"type": "bet", "amount": {"decimal": "1"}}, {"type": "call", "amount": {"decimal": "1"}}, {"type": "check", "amount": {"decimal": "0"}}, {"type": "bet", "amount": {"decimal": "1"}}, {"type": "call", "amount": {"decimal": "1"}}], "ante": {"decimal": "0"}, "bb_ante": {"decimal": "0"}, "players": [{"decimal": "10"}, {"decimal": "10"}], "current_street": null, "current_player": null, "total_pot": {"decimal": "6.0"}, "_blinds_posted": true, "winner": null, "n_straddle": 0, "largest_blind": {"decimal": "1"}, "n_decimals": 1, "player_names": ["SB", "BB"], "n_seats": 2, "n_cards": 2, "active_seats": [0, 1], "button_idx": 0, "hero": 0, "hands": [["Jc", "7d", "xx", "xx"], ["xx", "xx", "xx", "xx"]], "board": ["6c", "2d", "9h", "2c", "Ac"], "currency": "", "currency_is_after": true} diff --git a/test/hu2.hh b/test/hu2.hh new file mode 100644 index 0000000..888cde6 --- /dev/null +++ b/test/hu2.hh @@ -0,0 +1 @@ +{"small_blind": {"decimal": "0.5"}, "big_blind": {"decimal": "1"}, "actions": [{"type": "post SB", "amount": {"decimal": "0.5"}}, {"type": "post BB", "amount": {"decimal": "1"}}, {"type": "call", "amount": {"decimal": "0.5"}}, {"type": "check", "amount": {"decimal": "0"}}, {"type": "bet", "amount": {"decimal": "1"}}, {"type": "fold", "amount": {"decimal": "0"}}], "ante": {"decimal": "0"}, "bb_ante": {"decimal": "0"}, "players": [{"decimal": "20"}, {"decimal": "20"}], "current_street": null, "current_player": null, "total_pot": {"decimal": "3.0"}, "_blinds_posted": true, "winner": {"decimal": "20"}, "n_straddle": 0, "largest_blind": {"decimal": "1"}, "n_decimals": 1, "player_names": ["SB", "BB"], "n_seats": 2, "n_cards": 2, "active_seats": [0, 1], "button_idx": 0, "hero": 0, "hands": [["8h", "4c", "xx", "xx"], ["xx", "xx", "xx", "xx"]], "board": ["Jh", "6d", "3s", "xx", "xx"], "currency": "", "currency_is_after": true} diff --git a/test/q9.hh b/test/q9.hh new file mode 100644 index 0000000..2da54d6 --- /dev/null +++ b/test/q9.hh @@ -0,0 +1,63 @@ +{ + "small_blind": { "decimal": "0.5" }, + "big_blind": { "decimal": "1" }, + "actions": [ + { "type": "post ante", "amount": { "decimal": "0.125" } }, + { "type": "post ante", "amount": { "decimal": "0.125" } }, + { "type": "post ante", "amount": { "decimal": "0.125" } }, + { "type": "post ante", "amount": { "decimal": "0.125" } }, + { "type": "post ante", "amount": { "decimal": "0.125" } }, + { "type": "post ante", "amount": { "decimal": "0.125" } }, + { "type": "post SB", "amount": { "decimal": "0.5" } }, + { "type": "post BB", "amount": { "decimal": "1" } }, + { "type": "fold", "amount": { "decimal": "0" } }, + { "type": "fold", "amount": { "decimal": "0" } }, + { "type": "fold", "amount": { "decimal": "0" } }, + { "type": "raise", "amount": { "decimal": "1.5" } }, + { "type": "fold", "amount": { "decimal": "0" } }, + { "type": "call", "amount": { "decimal": "1.5" } }, + { "type": "check", "amount": { "decimal": "0" } }, + { "type": "bet", "amount": { "decimal": "2.1" } }, + { "type": "call", "amount": { "decimal": "2.1" } }, + { "type": "check", "amount": { "decimal": "0" } }, + { "type": "check", "amount": { "decimal": "0" } }, + { "type": "check", "amount": { "decimal": "0" } }, + { "type": "bet", "amount": { "decimal": "7" } }, + { "type": "call", "amount": { "decimal": "7" } } + ], + "ante": { "decimal": "0.125" }, + "bb_ante": { "decimal": "0" }, + "players": [ + { "decimal": "100.1" }, + { "decimal": "100.1" }, + { "decimal": "100.1" }, + { "decimal": "100.1" }, + { "decimal": "100.1" }, + { "decimal": "100.1" } + ], + "current_street": null, + "current_player": null, + "total_pot": { "decimal": "24.450" }, + "_blinds_posted": true, + "winner": null, + "n_straddle": 0, + "largest_blind": { "decimal": "1" }, + "n_decimals": 1, + "player_names": ["SB", "BB", "UTG", "HJ", "CO", "BTN"], + "n_seats": 6, + "n_cards": 2, + "active_seats": [0, 1, 2, 3, 4, 5], + "button_idx": 0, + "hero": 0, + "hands": [ + ["xx", "xx", "xx", "xx"], + ["Jd", "8d", "xx", "xx"], + ["xx", "xx", "xx", "xx"], + ["xx", "xx", "xx", "xx"], + ["xx", "xx", "xx", "xx"], + ["Qc", "9h", "xx", "xx"] + ], + "board": ["Jc", "7h", "2s", "6s", "Qd"], + "currency": "", + "currency_is_after": true +} diff --git a/test/showdown1.hh b/test/showdown1.hh new file mode 100644 index 0000000..6a910d9 --- /dev/null +++ b/test/showdown1.hh @@ -0,0 +1,207 @@ +{ + "small_blind": { + "decimal": "0.5" + }, + "big_blind": { + "decimal": "1" + }, + "actions": [ + { + "type": "post ante", + "amount": { + "decimal": "0.125" + } + }, + { + "type": "post ante", + "amount": { + "decimal": "0.125" + } + }, + { + "type": "post ante", + "amount": { + "decimal": "0.125" + } + }, + { + "type": "post ante", + "amount": { + "decimal": "0.125" + } + }, + { + "type": "post ante", + "amount": { + "decimal": "0.125" + } + }, + { + "type": "post ante", + "amount": { + "decimal": "0.125" + } + }, + { + "type": "post SB", + "amount": { + "decimal": "0.5" + } + }, + { + "type": "post BB", + "amount": { + "decimal": "1" + } + }, + { + "type": "fold", + "amount": { + "decimal": "0" + } + }, + { + "type": "fold", + "amount": { + "decimal": "0" + } + }, + { + "type": "raise", + "amount": { + "decimal": "1.5" + } + }, + { + "type": "raise", + "amount": { + "decimal": "6.0" + } + }, + { + "type": "fold", + "amount": { + "decimal": "0" + } + }, + { + "type": "fold", + "amount": { + "decimal": "0" + } + }, + { + "type": "fold", + "amount": { + "decimal": "0" + } + } + ], + "ante": { + "decimal": "0.125" + }, + "bb_ante": { + "decimal": "0" + }, + "players": [ + { + "decimal": "100.1" + }, + { + "decimal": "100.1" + }, + { + "decimal": "100.1" + }, + { + "decimal": "100.1" + }, + { + "decimal": "100.1" + }, + { + "decimal": "100.1" + } + ], + "current_street": null, + "current_player": null, + "total_pot": { + "decimal": "13.250" + }, + "_blinds_posted": true, + "winner": { + "decimal": "100.1" + }, + "n_straddle": 0, + "largest_blind": { + "decimal": "1" + }, + "n_decimals": 1, + "player_names": [ + "SB", + "BB", + "LJ", + "HJ", + "CO", + "BTN" + ], + "n_seats": 6, + "n_cards": 2, + "active_seats": [ + 0, + 1, + 2, + 3, + 4, + 5 + ], + "button_idx": 0, + "hero": 0, + "hands": [ + [ + "xx", + "xx", + "xx", + "xx" + ], + [ + "xx", + "xx", + "xx", + "xx" + ], + [ + "xx", + "xx", + "xx", + "xx" + ], + [ + "xx", + "xx", + "xx", + "xx" + ], + [ + "xx", + "xx", + "xx", + "xx" + ], + [ + "Qd", + "7d", + "xx", + "xx" + ] + ], + "board": [ + "xx", + "xx", + "xx", + "xx", + "xx" + ], + "currency": "", + "currency_is_after": true +} diff --git a/test/showdown2.hh b/test/showdown2.hh new file mode 100644 index 0000000..08aa03e --- /dev/null +++ b/test/showdown2.hh @@ -0,0 +1,53 @@ +{ + "small_blind": { "decimal": "50" }, + "big_blind": { "decimal": "100" }, + "actions": [ + { "type": "post SB", "amount": { "decimal": "50" } }, + { "type": "post BB", "amount": { "decimal": "100" } }, + { "type": "fold", "amount": { "decimal": "0" } }, + { "type": "fold", "amount": { "decimal": "0" } }, + { "type": "fold", "amount": { "decimal": "0" } }, + { "type": "raise", "amount": { "decimal": "150" } }, + { "type": "fold", "amount": { "decimal": "0" } }, + { "type": "call", "amount": { "decimal": "150" } }, + { "type": "check", "amount": { "decimal": "0" } }, + { "type": "bet", "amount": { "decimal": "200" } }, + { "type": "raise", "amount": { "decimal": "500" } }, + { "type": "fold", "amount": { "decimal": "0" } } + ], + "ante": { "decimal": "0" }, + "bb_ante": { "decimal": "0" }, + "players": [ + { "decimal": "10000" }, + { "decimal": "10000" }, + { "decimal": "10000" }, + { "decimal": "10000" }, + { "decimal": "10000" }, + { "decimal": "10000" } + ], + "current_street": null, + "current_player": null, + "total_pot": { "decimal": "1450" }, + "_blinds_posted": true, + "winner": { "decimal": "10000" }, + "n_straddle": 0, + "largest_blind": { "decimal": "100" }, + "n_decimals": 1, + "player_names": ["SB", "BB", "UTG", "HJ", "CO", "BTN"], + "n_seats": 6, + "n_cards": 2, + "active_seats": [0, 1, 2, 3, 4, 5], + "button_idx": 4, + "hero": 0, + "hands": [ + ["xx", "xx", "xx", "xx"], + ["7h", "8h", "xx", "xx"], + ["xx", "xx", "xx", "xx"], + ["xx", "xx", "xx", "xx"], + ["xx", "xx", "xx", "xx"], + ["xx", "xx", "xx", "xx"] + ], + "board": ["4h", "5h", "Td", "xx", "xx"], + "currency": "", + "currency_is_after": true +} diff --git a/test/test_hh.py b/test/test_hh.py new file mode 100644 index 0000000..6337766 --- /dev/null +++ b/test/test_hh.py @@ -0,0 +1,26 @@ +from pathlib import Path + +import pytest + +from hh_creator.hh import HandHistory, Position, Street + + +@pytest.mark.parametrize("file_name", ["hu1.hh", "hu2.hh"]) +def test_hu_player_order(file_name: str) -> None: + hh = HandHistory.from_json((Path(__file__).parent / file_name).read_text()) + streets = set() + for action in hh.actions: + if action.street in streets: + continue + streets.add(action.street) + first_player_for_street = action.player + if action.street <= Street.PRE_FLOP: + assert first_player_for_street.position == Position.SB, action.street + else: + assert action.player.position == Position.BB, action.street + + +@pytest.mark.parametrize("file_name", ["fold_flop.hh", "fold_preflop.hh"]) +def test_remaining_action(file_name: str) -> None: + hh = HandHistory.from_json((Path(__file__).parent / "fold_flop.hh").read_text()) + assert hh.n_pseudo_actions() == 0 diff --git a/uv.lock b/uv.lock index 9cd1637..8b409ae 100644 --- a/uv.lock +++ b/uv.lock @@ -239,7 +239,7 @@ name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ @@ -278,6 +278,8 @@ dev = [ { name = "pre-commit" }, { name = "pyproject-pre-commit" }, { name = "pytest" }, + { name = "ruff" }, + { name = "ty" }, ] [package.metadata] @@ -292,6 +294,8 @@ dev = [ { name = "pre-commit", specifier = ">=4.5.1" }, { name = "pyproject-pre-commit", specifier = ">=0.3.6" }, { name = "pytest", specifier = ">=8.3.4" }, + { name = "ruff", specifier = ">=0.15.0" }, + { name = "ty", specifier = ">=0.0.15" }, ] [[package]]