From c428a543f31b5059b34091a76c45c45d23cd3282 Mon Sep 17 00:00:00 2001 From: Vibe Nuage Agent Date: Wed, 19 Aug 2026 21:28:39 +0000 Subject: [PATCH] feat(range): add effective stack, extended positions, and defense actions - Add effective_stack_bb field to Range (50, 100, 30, 20, 19, 18, 17, ..., 5 BB) - Extend Position enum with UTG+1, LJ, HJ - Add new ActionType values: DEFENSE, DEFENSE_3BET, DEFENSE_4BET - Update SQLAlchemy models and adapters - Update TypeScript types and frontend components - Add comprehensive unit tests for new features Closes # Co-authored-by: polmichel --- .../poker_tool/adapters/sqlalchemy/models.py | 2 + .../poker_tool/adapters/sqlalchemy/ranges.py | 2 + backend/poker_tool/objects/action.py | 9 ++ backend/poker_tool/objects/effective_stack.py | 77 ++++++++++++++++++ backend/poker_tool/objects/position.py | 44 +++++++++- backend/poker_tool/objects/range.py | 15 +++- .../tests/unit/objects/test_action.py | 27 ++++++ .../unit/objects/test_effective_stack.py | 72 ++++++++++++++++ .../tests/unit/objects/test_position.py | 29 ++++++- .../tests/unit/objects/test_range.py | 44 +++++++++- frontend/src/components/RangeForm.tsx | 25 +++++- frontend/src/components/RangeStats.tsx | 7 ++ frontend/src/pages/RangeView.tsx | 23 ++++-- frontend/src/types/index.ts | Bin 3856 -> 3963 bytes frontend/src/utils/constants.ts | 16 +++- frontend/src/utils/helpers.ts | 3 + 16 files changed, 375 insertions(+), 20 deletions(-) create mode 100644 backend/poker_tool/objects/effective_stack.py create mode 100644 backend/poker_tool/tests/unit/objects/test_effective_stack.py diff --git a/backend/poker_tool/adapters/sqlalchemy/models.py b/backend/poker_tool/adapters/sqlalchemy/models.py index 1aa4488..3006d6d 100644 --- a/backend/poker_tool/adapters/sqlalchemy/models.py +++ b/backend/poker_tool/adapters/sqlalchemy/models.py @@ -24,6 +24,7 @@ class RangeModel(db.Model): description = db.Column(db.Text, default='') range_type = db.Column(db.String(20), default='preflop') position = db.Column(db.String(20), default='undefined') + effective_stack_bb = db.Column(db.Integer, nullable=True) hands = db.Column(db.JSON, default={}) # Dict[str, str] (hand_str -> action_str) user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=True) created_at = db.Column(db.DateTime, default=_utcnow_naive) @@ -48,6 +49,7 @@ def to_domain(self): hands=hands, user_id=self.user_id, range_id=self.id, + effective_stack_bb=self.effective_stack_bb, ) diff --git a/backend/poker_tool/adapters/sqlalchemy/ranges.py b/backend/poker_tool/adapters/sqlalchemy/ranges.py index 9e7bd3d..f1c053a 100644 --- a/backend/poker_tool/adapters/sqlalchemy/ranges.py +++ b/backend/poker_tool/adapters/sqlalchemy/ranges.py @@ -24,6 +24,7 @@ def add(self, range_obj: Range) -> Range: model.description = range_obj.description model.range_type = range_obj.type.name.lower() model.position = range_obj.position.name + model.effective_stack_bb = range_obj.effective_stack_bb model.hands = hands_dict model.user_id = range_obj.user_id else: @@ -32,6 +33,7 @@ def add(self, range_obj: Range) -> Range: description=range_obj.description, range_type=range_obj.type.name.lower(), position=range_obj.position.name, + effective_stack_bb=range_obj.effective_stack_bb, hands=hands_dict, user_id=range_obj.user_id, ) diff --git a/backend/poker_tool/objects/action.py b/backend/poker_tool/objects/action.py index 87d7c89..f9ad95e 100644 --- a/backend/poker_tool/objects/action.py +++ b/backend/poker_tool/objects/action.py @@ -11,6 +11,9 @@ class ActionType(Enum): CALL = auto() FOLD = auto() ALL_IN = auto() + DEFENSE = auto() + DEFENSE_3BET = auto() + DEFENSE_4BET = auto() UNDEFINED = auto() @@ -34,6 +37,9 @@ def color(self) -> str: ActionType.CALL: "#FF9800", ActionType.FOLD: "#F44336", ActionType.ALL_IN: "#9C27B0", + ActionType.DEFENSE: "#FF5722", + ActionType.DEFENSE_3BET: "#E91E63", + ActionType.DEFENSE_4BET: "#9C27B0", ActionType.UNDEFINED: "#607D8B", } return colors[self._type] @@ -47,6 +53,9 @@ def label(self) -> str: ActionType.CALL: "Suivre", ActionType.FOLD: "Passer", ActionType.ALL_IN: "All-In", + ActionType.DEFENSE: "Défense", + ActionType.DEFENSE_3BET: "Défense 3Bet", + ActionType.DEFENSE_4BET: "Défense 4Bet", ActionType.UNDEFINED: "Non défini", } return labels[self._type] diff --git a/backend/poker_tool/objects/effective_stack.py b/backend/poker_tool/objects/effective_stack.py new file mode 100644 index 0000000..03fbaff --- /dev/null +++ b/backend/poker_tool/objects/effective_stack.py @@ -0,0 +1,77 @@ +""" +Immutable effective stack value object (Elegant Objects). +""" + +from enum import Enum + +# Valeurs de stack effectif en BB (Big Blinds) +EFFECTIVE_STACK_VALUES = [ + 50, 100, 30, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, + 9, 8, 7, 6, 5 +] + + +class EffectiveStack(Enum): + """Effective stack sizes in BB.""" + + BB_5 = 5 + BB_6 = 6 + BB_7 = 7 + BB_8 = 8 + BB_9 = 9 + BB_10 = 10 + BB_11 = 11 + BB_12 = 12 + BB_13 = 13 + BB_14 = 14 + BB_15 = 15 + BB_16 = 16 + BB_17 = 17 + BB_18 = 18 + BB_19 = 19 + BB_20 = 20 + BB_30 = 30 + BB_50 = 50 + BB_100 = 100 + + @property + def value(self) -> int: + """Numeric value in BB.""" + return self._value_ # type: ignore + + @property + def label(self) -> str: + """Human-readable label.""" + return f"{self.value} BB" + + @classmethod + def from_int(cls, stack_value: int) -> 'EffectiveStack': + """Factory method from integer value.""" + for stack in cls: + if stack.value == stack_value: + return stack + # Default to 100 BB if not found + return cls.BB_100 + + @classmethod + def from_string(cls, stack_str: str) -> 'EffectiveStack': + """Factory method from string.""" + try: + # Try to parse as integer first + stack_value = int(stack_str) + return cls.from_int(stack_value) + except ValueError: + try: + return cls[stack_str.upper()] + except KeyError: + return cls.BB_100 + + @classmethod + def all_values(cls) -> list[int]: + """Return all available stack values as integers.""" + return [stack.value for stack in cls] + + @classmethod + def all_labels(cls) -> list[str]: + """Return all available stack labels.""" + return [stack.label for stack in cls] diff --git a/backend/poker_tool/objects/position.py b/backend/poker_tool/objects/position.py index 8028afb..f339426 100644 --- a/backend/poker_tool/objects/position.py +++ b/backend/poker_tool/objects/position.py @@ -6,8 +6,11 @@ class Position(Enum): """Table positions in poker.""" + UTG = auto() - MP = auto() + UTG_PLUS_1 = auto() + LJ = auto() + HJ = auto() CO = auto() BTN = auto() SB = auto() @@ -19,7 +22,9 @@ def label(self) -> str: """Human-readable label.""" labels = { Position.UTG: "UTG", - Position.MP: "MP", + Position.UTG_PLUS_1: "UTG+1", + Position.LJ: "LJ", + Position.HJ: "HJ", Position.CO: "CO", Position.BTN: "BTN", Position.SB: "SB", @@ -33,8 +38,10 @@ def color(self) -> str: """Color associated with this position.""" colors = { Position.UTG: "#FF5722", - Position.MP: "#FF9800", - Position.CO: "#FFC107", + Position.UTG_PLUS_1: "#FF7043", + Position.LJ: "#FF9800", + Position.HJ: "#FFC107", + Position.CO: "#FFE0B2", Position.BTN: "#4CAF50", Position.SB: "#2196F3", Position.BB: "#9C27B0", @@ -45,6 +52,35 @@ def color(self) -> str: @classmethod def from_string(cls, position_str: str) -> 'Position': """Factory method from string.""" + # Map common string representations to enum names + string_to_enum = { + "UTG": "UTG", + "UTG+1": "UTG_PLUS_1", + "LJ": "LJ", + "HJ": "HJ", + "CO": "CO", + "BTN": "BTN", + "SB": "SB", + "BB": "BB", + } + + # Try to normalize the input string + normalized = position_str.upper().replace("+", "_PLUS_").replace("1", "_1") + + # Try direct enum access first + try: + return Position[normalized] + except KeyError: + pass + + # Try with the string mapping + try: + enum_name = string_to_enum[position_str.upper()] + return Position[enum_name] + except KeyError: + pass + + # Try with normalized string try: return Position[position_str.upper()] except KeyError: diff --git a/backend/poker_tool/objects/range.py b/backend/poker_tool/objects/range.py index d7cfac6..57c9ae6 100644 --- a/backend/poker_tool/objects/range.py +++ b/backend/poker_tool/objects/range.py @@ -20,6 +20,7 @@ def __init__( hands: dict[str, Action] | None = None, user_id: int | None = None, range_id: int | None = None, + effective_stack_bb: int | None = None, ): self._name = name self._description = description @@ -28,6 +29,7 @@ def __init__( self._hands = hands or {} self._user_id = user_id self._id = range_id + self._effective_stack_bb = effective_stack_bb @property def id(self) -> int | None: @@ -64,6 +66,11 @@ def user_id(self) -> int | None: """User ID.""" return self._user_id + @property + def effective_stack_bb(self) -> int | None: + """Effective stack in BB.""" + return self._effective_stack_bb + def with_hand(self, hand_str: str, action: Action) -> 'Range': """Return new Range with added/updated hand (immutable).""" new_hands = dict(self._hands) @@ -76,6 +83,7 @@ def with_hand(self, hand_str: str, action: Action) -> 'Range': hands=new_hands, user_id=self._user_id, range_id=self._id, + effective_stack_bb=self._effective_stack_bb, ) def without_hand(self, hand_str: str) -> 'Range': @@ -90,6 +98,7 @@ def without_hand(self, hand_str: str) -> 'Range': hands=new_hands, user_id=self._user_id, range_id=self._id, + effective_stack_bb=self._effective_stack_bb, ) def grid(self) -> list[list[dict]]: @@ -123,7 +132,7 @@ def statistics(self) -> dict: def to_dict(self) -> dict: """Serialize to dictionary.""" - return { + result = { "id": self._id, "name": self._name, "description": self._description, @@ -132,6 +141,9 @@ def to_dict(self) -> dict: "hands": {k: str(v) for k, v in self._hands.items()}, "user_id": self._user_id, } + if self._effective_stack_bb is not None: + result["effective_stack_bb"] = self._effective_stack_bb + return result @classmethod def from_dict(cls, data: dict) -> 'Range': @@ -148,4 +160,5 @@ def from_dict(cls, data: dict) -> 'Range': hands=hands, user_id=data.get("user_id"), range_id=data.get("id"), + effective_stack_bb=data.get("effective_stack_bb"), ) diff --git a/backend/poker_tool/tests/unit/objects/test_action.py b/backend/poker_tool/tests/unit/objects/test_action.py index 7c7cbfc..9be3c5a 100644 --- a/backend/poker_tool/tests/unit/objects/test_action.py +++ b/backend/poker_tool/tests/unit/objects/test_action.py @@ -16,6 +16,9 @@ def test_action_type_values(self): self.assertEqual(ActionType.CALL.name, "CALL") self.assertEqual(ActionType.FOLD.name, "FOLD") self.assertEqual(ActionType.ALL_IN.name, "ALL_IN") + self.assertEqual(ActionType.DEFENSE.name, "DEFENSE") + self.assertEqual(ActionType.DEFENSE_3BET.name, "DEFENSE_3BET") + self.assertEqual(ActionType.DEFENSE_4BET.name, "DEFENSE_4BET") self.assertEqual(ActionType.UNDEFINED.name, "UNDEFINED") @@ -35,6 +38,9 @@ def test_action_color(self): ActionType.CALL: "#FF9800", ActionType.FOLD: "#F44336", ActionType.ALL_IN: "#9C27B0", + ActionType.DEFENSE: "#FF5722", + ActionType.DEFENSE_3BET: "#E91E63", + ActionType.DEFENSE_4BET: "#9C27B0", ActionType.UNDEFINED: "#607D8B", } @@ -50,6 +56,9 @@ def test_action_label(self): ActionType.CALL: "Suivre", ActionType.FOLD: "Passer", ActionType.ALL_IN: "All-In", + ActionType.DEFENSE: "Défense", + ActionType.DEFENSE_3BET: "Défense 3Bet", + ActionType.DEFENSE_4BET: "Défense 4Bet", ActionType.UNDEFINED: "Non défini", } @@ -65,6 +74,15 @@ def test_action_string_representation(self): action = Action(ActionType.RAISE) self.assertEqual(str(action), "raise") + action = Action(ActionType.DEFENSE) + self.assertEqual(str(action), "defense") + + action = Action(ActionType.DEFENSE_3BET) + self.assertEqual(str(action), "defense_3bet") + + action = Action(ActionType.DEFENSE_4BET) + self.assertEqual(str(action), "defense_4bet") + def test_action_equality(self): """Test equality comparison.""" action1 = Action(ActionType.OPEN) @@ -100,6 +118,15 @@ def test_action_from_string(self): action = Action.from_string("Raise") self.assertEqual(action.type, ActionType.RAISE) + action = Action.from_string("DEFENSE") + self.assertEqual(action.type, ActionType.DEFENSE) + + action = Action.from_string("defense_3bet") + self.assertEqual(action.type, ActionType.DEFENSE_3BET) + + action = Action.from_string("DEFENSE_4BET") + self.assertEqual(action.type, ActionType.DEFENSE_4BET) + # Invalid action string action = Action.from_string("INVALID") self.assertEqual(action.type, ActionType.UNDEFINED) diff --git a/backend/poker_tool/tests/unit/objects/test_effective_stack.py b/backend/poker_tool/tests/unit/objects/test_effective_stack.py new file mode 100644 index 0000000..dd2bacb --- /dev/null +++ b/backend/poker_tool/tests/unit/objects/test_effective_stack.py @@ -0,0 +1,72 @@ +""" +Unit tests for EffectiveStack value object. +""" +import unittest + +from poker_tool.objects.effective_stack import EFFECTIVE_STACK_VALUES, EffectiveStack + + +class TestEffectiveStack(unittest.TestCase): + """Tests for EffectiveStack enum.""" + + def test_effective_stack_values(self): + """Test EffectiveStack enum values.""" + self.assertEqual(EffectiveStack.BB_5.value, 5) + self.assertEqual(EffectiveStack.BB_10.value, 10) + self.assertEqual(EffectiveStack.BB_20.value, 20) + self.assertEqual(EffectiveStack.BB_30.value, 30) + self.assertEqual(EffectiveStack.BB_50.value, 50) + self.assertEqual(EffectiveStack.BB_100.value, 100) + + def test_effective_stack_label(self): + """Test label property.""" + self.assertEqual(EffectiveStack.BB_5.label, "5 BB") + self.assertEqual(EffectiveStack.BB_10.label, "10 BB") + self.assertEqual(EffectiveStack.BB_20.label, "20 BB") + self.assertEqual(EffectiveStack.BB_100.label, "100 BB") + + def test_effective_stack_from_int(self): + """Test factory method from integer.""" + stack = EffectiveStack.from_int(5) + self.assertEqual(stack, EffectiveStack.BB_5) + + stack = EffectiveStack.from_int(100) + self.assertEqual(stack, EffectiveStack.BB_100) + + stack = EffectiveStack.from_int(25) + self.assertEqual(stack, EffectiveStack.BB_100) # Default to 100 if not found + + def test_effective_stack_from_string(self): + """Test factory method from string.""" + stack = EffectiveStack.from_string("5") + self.assertEqual(stack, EffectiveStack.BB_5) + + stack = EffectiveStack.from_string("100") + self.assertEqual(stack, EffectiveStack.BB_100) + + stack = EffectiveStack.from_string("BB_100") + self.assertEqual(stack, EffectiveStack.BB_100) + + stack = EffectiveStack.from_string("invalid") + self.assertEqual(stack, EffectiveStack.BB_100) # Default to 100 if not found + + def test_all_values(self): + """Test all_values method.""" + values = EffectiveStack.all_values() + expected = [5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 30, 50, 100] + self.assertEqual(sorted(values), sorted(expected)) + + def test_all_labels(self): + """Test all_labels method.""" + labels = EffectiveStack.all_labels() + expected = [f"{v} BB" for v in [5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 30, 50, 100]] + self.assertEqual(sorted(labels), sorted(expected)) + + def test_effective_stack_values_constant(self): + """Test EFFECTIVE_STACK_VALUES constant.""" + expected = [50, 100, 30, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5] + self.assertEqual(EFFECTIVE_STACK_VALUES, expected) + + +if __name__ == '__main__': + unittest.main() diff --git a/backend/poker_tool/tests/unit/objects/test_position.py b/backend/poker_tool/tests/unit/objects/test_position.py index 5975df4..97b7a13 100644 --- a/backend/poker_tool/tests/unit/objects/test_position.py +++ b/backend/poker_tool/tests/unit/objects/test_position.py @@ -12,7 +12,9 @@ class TestPosition(unittest.TestCase): def test_position_values(self): """Test Position enum values.""" self.assertEqual(Position.UTG.name, "UTG") - self.assertEqual(Position.MP.name, "MP") + self.assertEqual(Position.UTG_PLUS_1.name, "UTG_PLUS_1") + self.assertEqual(Position.LJ.name, "LJ") + self.assertEqual(Position.HJ.name, "HJ") self.assertEqual(Position.CO.name, "CO") self.assertEqual(Position.BTN.name, "BTN") self.assertEqual(Position.SB.name, "SB") @@ -23,7 +25,9 @@ def test_position_label(self): """Test label property.""" labels = { Position.UTG: "UTG", - Position.MP: "MP", + Position.UTG_PLUS_1: "UTG+1", + Position.LJ: "LJ", + Position.HJ: "HJ", Position.CO: "CO", Position.BTN: "BTN", Position.SB: "SB", @@ -38,8 +42,10 @@ def test_position_color(self): """Test color property.""" colors = { Position.UTG: "#FF5722", - Position.MP: "#FF9800", - Position.CO: "#FFC107", + Position.UTG_PLUS_1: "#FF7043", + Position.LJ: "#FF9800", + Position.HJ: "#FFC107", + Position.CO: "#FFE0B2", Position.BTN: "#4CAF50", Position.SB: "#2196F3", Position.BB: "#9C27B0", @@ -58,6 +64,21 @@ def test_position_from_string(self): position = Position.from_string("utg") self.assertEqual(position, Position.UTG) + position = Position.from_string("UTG+1") + self.assertEqual(position, Position.UTG_PLUS_1) + + position = Position.from_string("utg_plus_1") + self.assertEqual(position, Position.UTG_PLUS_1) + + position = Position.from_string("LJ") + self.assertEqual(position, Position.LJ) + + position = Position.from_string("lj") + self.assertEqual(position, Position.LJ) + + position = Position.from_string("HJ") + self.assertEqual(position, Position.HJ) + position = Position.from_string("BTN") self.assertEqual(position, Position.BTN) diff --git a/backend/poker_tool/tests/unit/objects/test_range.py b/backend/poker_tool/tests/unit/objects/test_range.py index 362c4fc..9bbd7e92 100644 --- a/backend/poker_tool/tests/unit/objects/test_range.py +++ b/backend/poker_tool/tests/unit/objects/test_range.py @@ -22,6 +22,7 @@ def test_range_creation(self): hands={"AKs": Action(ActionType.RAISE)}, user_id=1, range_id=1, + effective_stack_bb=100, ) self.assertEqual(range_obj.name, "Test Range") @@ -31,6 +32,7 @@ def test_range_creation(self): self.assertEqual(range_obj.hands, {"AKs": Action(ActionType.RAISE)}) self.assertEqual(range_obj.user_id, 1) self.assertEqual(range_obj.id, 1) + self.assertEqual(range_obj.effective_stack_bb, 100) def test_range_creation_minimal(self): """Test Range creation with minimal parameters.""" @@ -43,15 +45,17 @@ def test_range_creation_minimal(self): self.assertEqual(range_obj.hands, {}) self.assertIsNone(range_obj.user_id) self.assertIsNone(range_obj.id) + self.assertIsNone(range_obj.effective_stack_bb) def test_range_with_hand(self): """Test with_hand method (immutable).""" - range_obj = Range(name="Test Range") + range_obj = Range(name="Test Range", effective_stack_bb=50) new_range = range_obj.with_hand("AKs", Action(ActionType.RAISE)) self.assertEqual(len(new_range.hands), 1) self.assertEqual(new_range.hands["AKs"].type, ActionType.RAISE) + self.assertEqual(new_range.effective_stack_bb, 50) # Original should be unchanged self.assertEqual(len(range_obj.hands), 0) @@ -60,6 +64,7 @@ def test_range_without_hand(self): range_obj = Range( name="Test Range", hands={"AKs": Action(ActionType.RAISE), "TT": Action(ActionType.OPEN)}, + effective_stack_bb=30, ) new_range = range_obj.without_hand("AKs") @@ -67,6 +72,7 @@ def test_range_without_hand(self): self.assertEqual(len(new_range.hands), 1) self.assertNotIn("AKs", new_range.hands) self.assertIn("TT", new_range.hands) + self.assertEqual(new_range.effective_stack_bb, 30) # Original should be unchanged self.assertEqual(len(range_obj.hands), 2) @@ -120,6 +126,7 @@ def test_range_to_dict(self): hands={"AKs": Action(ActionType.RAISE)}, user_id=1, range_id=1, + effective_stack_bb=100, ) range_dict = range_obj.to_dict() @@ -131,6 +138,23 @@ def test_range_to_dict(self): self.assertEqual(range_dict["position"], "BTN") self.assertEqual(range_dict["hands"], {"AKs": "raise"}) self.assertEqual(range_dict["user_id"], 1) + self.assertEqual(range_dict["effective_stack_bb"], 100) + + def test_range_to_dict_without_effective_stack(self): + """Test serialization to dictionary without effective_stack_bb.""" + range_obj = Range( + name="Test Range", + description="A test range", + range_type=RangeType.PREFLOP, + position=Position.BTN, + hands={"AKs": Action(ActionType.RAISE)}, + user_id=1, + range_id=1, + ) + + range_dict = range_obj.to_dict() + + self.assertNotIn("effective_stack_bb", range_dict) def test_range_from_dict(self): """Test creation from dictionary.""" @@ -142,6 +166,7 @@ def test_range_from_dict(self): "position": "BTN", "hands": {"AKs": "raise", "TT": "open"}, "user_id": 1, + "effective_stack_bb": 50, } range_obj = Range.from_dict(data) @@ -155,6 +180,23 @@ def test_range_from_dict(self): self.assertEqual(range_obj.hands["AKs"].type, ActionType.RAISE) self.assertEqual(range_obj.hands["TT"].type, ActionType.OPEN) self.assertEqual(range_obj.user_id, 1) + self.assertEqual(range_obj.effective_stack_bb, 50) + + def test_range_from_dict_without_effective_stack(self): + """Test creation from dictionary without effective_stack_bb.""" + data = { + "id": 1, + "name": "Test Range", + "description": "A test range", + "range_type": "preflop", + "position": "BTN", + "hands": {"AKs": "raise", "TT": "open"}, + "user_id": 1, + } + + range_obj = Range.from_dict(data) + + self.assertIsNone(range_obj.effective_stack_bb) if __name__ == '__main__': diff --git a/frontend/src/components/RangeForm.tsx b/frontend/src/components/RangeForm.tsx index 8f19f8f..077a7ea 100644 --- a/frontend/src/components/RangeForm.tsx +++ b/frontend/src/components/RangeForm.tsx @@ -13,7 +13,7 @@ import { Chip, } from '@mui/material'; import { Range, RangeType, Position } from '../types'; -import { RANGE_TYPES, POSITIONS, ACTION_LABELS } from '../utils/constants'; +import { RANGE_TYPES, POSITIONS, ACTION_LABELS, EFFECTIVE_STACK_VALUES } from '../utils/constants'; import { generateUniqueRangeName } from '../utils/helpers'; interface RangeFormProps { @@ -33,6 +33,7 @@ const RangeForm: React.FC = ({ const [description, setDescription] = useState(''); const [rangeType, setRangeType] = useState('preflop'); const [position, setPosition] = useState('UTG'); + const [effectiveStackBB, setEffectiveStackBB] = useState(100); const [errors, setErrors] = useState>({}); // Initialiser le formulaire avec les valeurs de la range existante @@ -42,12 +43,14 @@ const RangeForm: React.FC = ({ setDescription(range.description || ''); setRangeType(range.range_type); setPosition(range.position); + setEffectiveStackBB(range.effective_stack_bb || 100); } else { // Générer un nom unique pour une nouvelle range setName(generateUniqueRangeName(existingRangeNames)); setDescription(''); setRangeType('preflop'); setPosition('UTG'); + setEffectiveStackBB(100); } }, [range, existingRangeNames]); @@ -76,11 +79,12 @@ const RangeForm: React.FC = ({ description, range_type: rangeType, position, + effective_stack_bb: effectiveStackBB === '' ? null : Number(effectiveStackBB), hands: range?.hands || {}, }); } }, - [name, description, rangeType, position, range, validate, onSubmit], + [name, description, rangeType, position, effectiveStackBB, range, validate, onSubmit], ); const handleCancel = useCallback(() => { @@ -168,6 +172,23 @@ const RangeForm: React.FC = ({ + {/* Stack Effectif en BB */} + + Stack Effectif (BB) + + + {/* Actions disponibles (info) */} diff --git a/frontend/src/components/RangeStats.tsx b/frontend/src/components/RangeStats.tsx index d76b94b..8d29f13 100644 --- a/frontend/src/components/RangeStats.tsx +++ b/frontend/src/components/RangeStats.tsx @@ -54,6 +54,13 @@ const RangeStats: React.FC = ({ range }) => { /> + {range.effective_stack_bb && ( + + )} {/* Graphique en secteurs */} diff --git a/frontend/src/pages/RangeView.tsx b/frontend/src/pages/RangeView.tsx index bdedf9d..e2ded81 100644 --- a/frontend/src/pages/RangeView.tsx +++ b/frontend/src/pages/RangeView.tsx @@ -53,7 +53,6 @@ const RangeView: React.FC = () => { // Supprimer la range const handleDelete = useCallback(async () => { if (!range) return; - if (window.confirm(`Êtes-vous sûr de vouloir supprimer la range "${range.name}" ?`)) { await deleteRange(range.id!); navigate('/ranges'); @@ -63,7 +62,6 @@ const RangeView: React.FC = () => { // Dupliquer la range const handleDuplicate = useCallback(() => { if (!range) return; - navigate('/ranges/new', { state: { duplicateFrom: range, @@ -124,6 +122,9 @@ const RangeView: React.FC = () => { all_in: 0, fold: 0, check: 0, + defense: 0, + defense_3bet: 0, + defense_4bet: 0, undefined: 0, }; for (const action of Object.values(range.hands)) { @@ -149,7 +150,6 @@ const RangeView: React.FC = () => { {range.name} - - - - @@ -206,6 +203,13 @@ const RangeView: React.FC = () => { + {range.effective_stack_bb && ( + + )} { sx={{ backgroundColor: ACTION_COLORS[action as ActionType] || 'grey.500', color: - action === 'open' || action === 'raise' || action === 'all_in' + action === 'open' || + action === 'raise' || + action === 'all_in' || + action === 'defense' || + action === 'defense_3bet' || + action === 'defense_4bet' ? 'white' : 'black', }} diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index b703d2f575c479f2273c858161ad65b75a0d2ef3..c2968d3e00984c714c5af14205b03136293d0134 100644 GIT binary patch delta 322 zcmbOr_gij55Mw=;fS!PAN4lEj76$ zvn(~fxFj(-J3cAN-bx{_G&d==2&g!(G$&{BKBnc2oSV~`D;OF1CO>3pWMr6}!up?) zb8;%1sxX5>PNG73QD#m~szPE~T4r(v=j8cp{v4b|nTa`>#hjCWu<5XHrYbOO)?#;K z0-96AX~WFMSy-A`!nt`Hk1V4QXF)#DLkcDamI_6wd1d*PoRgpPo&XxZkuM$q+V^0I delta 215 zcmew@H$iSg5Tk{HdVWD_p1MMff_iddP7at+l$cqZ3T6RC;xi$#Y56%R5M>#u$=T|Y z&oOo|`36kRWLh`bmU%JL;gy>oGFLD%9bPv%inWpH@Pf&ASpNe>C*NUH6+OH_AtzBG zy(lv$CsiRaEiE%S11KZH?$332Wl?5gPG&KXo6WAnc6eo~!r=v*XS2I8flPeFX~WEM tcx7Q}W(km|!Yj)ta(HDyeqM2^f{B5pLQ!g7S$-u@YcSslkX17L@c = { all_in: 'All-In', fold: 'Passer', check: 'Checker', + defense: 'Défense', + defense_3bet: 'Défense 3Bet', + defense_4bet: 'Défense 4Bet', undefined: 'Non défini', }; // Positions export const POSITIONS = [ { value: 'UTG', label: 'UTG' }, - { value: 'MP', label: 'MP' }, + { value: 'UTG+1', label: 'UTG+1' }, + { value: 'LJ', label: 'LJ' }, + { value: 'HJ', label: 'HJ' }, { value: 'CO', label: 'CO' }, { value: 'BTN', label: 'BTN' }, { value: 'SB', label: 'SB' }, @@ -56,6 +64,11 @@ export const TRAINING_MODES = [ { value: 'complete', label: 'Compléter une range' }, ]; +// Valeurs de stack effectif en BB +export const EFFECTIVE_STACK_VALUES = [ + 100, 50, 30, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, +]; + // Ranks (ordres des cartes) export const RANKS: Rank[] = ['A', 'K', 'Q', 'J', 'T', '9', '8', '7', '6', '5', '4', '3', '2']; @@ -142,6 +155,7 @@ export const DEFAULT_RANGE = { description: '', range_type: 'preflop' as const, position: 'UTG' as const, + effective_stack_bb: 100 as const, hands: {} as Record, }; diff --git a/frontend/src/utils/helpers.ts b/frontend/src/utils/helpers.ts index cdcbf80..db19e9d 100644 --- a/frontend/src/utils/helpers.ts +++ b/frontend/src/utils/helpers.ts @@ -237,6 +237,9 @@ export function calculateRangeStats(hands: Record): { const total = Object.keys(hands).length; const byAction: Record = { open: 0, + defense: 0, + defense_3bet: 0, + defense_4bet: 0, call: 0, raise: 0, all_in: 0,