diff --git a/.gitignore b/.gitignore index 4e99d6dc..7ba513a5 100644 --- a/.gitignore +++ b/.gitignore @@ -159,6 +159,12 @@ cython_debug/ # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ +# Project-specific artifacts +baselines/vxp/ +reports/ +**/vxp_gauntlet/**/summary.md +**/vxp_gauntlet/**/journal.csv + # VScode .vscode # === Generated Reports (do not commit) === diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 0cf29f3e..00000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,8 +0,0 @@ -### Generated Reports -Artifacts under any `reports/` directory are generated and should not be committed. They are ignored via `.gitignore`. -If previously tracked, run: - -git rm -r --cached reports/ || true -git ls-files -z | grep -z "/reports/" | xargs -0 git rm --cached -r --ignore-unmatch || true - -This removes them from the index while keeping files on disk. diff --git a/crapssim/bet.py b/crapssim/bet.py index de9e0b5d..0f383960 100644 --- a/crapssim/bet.py +++ b/crapssim/bet.py @@ -16,14 +16,6 @@ class SupportsFloat(Protocol): def __float__(self) -> float: """Return a float representation.""" - - -def compute_commission(base_amount: float) -> float: - """Compute standard 5% commission for Buy/Lay style bets.""" - - return base_amount * 0.05 - - def _compute_commission( table: "Table", *, gross_win: float, bet_amount: float ) -> float: @@ -42,15 +34,15 @@ def _compute_commission( rounding = table.settings.get("commission_rounding", "none") floor = float(table.settings.get("commission_floor", 0.0) or 0.0) - if bet_amount < floor: - base = 0.0 - else: - if mode == "on_bet": - base = bet_amount - else: - base = gross_win + # Commission parity: regardless of timing (``on_bet`` vs ``on_win``), the + # amount charged is always based on the wagered amount. ``gross_win`` is + # ignored for fee size, but retained in the signature for compatibility. + if mode not in {"on_bet", "on_win"}: + # Unknown future toggles still fall back to wager-based calculation. + mode = "on_win" + _ = gross_win - fee = compute_commission(base) + fee = bet_amount * 0.05 if rounding == "ceil_dollar": import math @@ -58,11 +50,14 @@ def _compute_commission( elif rounding == "nearest_dollar": fee = round(fee) - return float(fee) + fee = float(fee) + if fee < floor: + fee = floor + + return fee __all__ = [ - "compute_commission", "BetResult", "Bet", "_WinningLosingNumbersBet", diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index ed3c5223..8d628988 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## Development version + +- Simplify commission logic: single `_compute_commission` (fixed 5%), consistent across "on_bet"/"on_win"; enforce floor as minimum. +- Add parametrized unit tests for commission calculations. +- Stop committing generated baselines/reports; add to `.gitignore`. +- Minor: Horn example amount set to 4.0. + ## v0.3.1 ### What's Changed diff --git a/tests/integration/test_vxp_baseline.py b/tests/integration/test_vxp_baseline.py index 000a2bd5..075d6fe3 100644 --- a/tests/integration/test_vxp_baseline.py +++ b/tests/integration/test_vxp_baseline.py @@ -47,10 +47,10 @@ def do_roll(outcome: tuple[int, int]): assert not player.bets, "All bets should be resolved" # Bankroll continuity — ensure deterministic ending bankroll. - expected_final_bankroll = pytest.approx(229.5, rel=1e-9, abs=1e-9) + expected_final_bankroll = pytest.approx(230.0, rel=1e-9, abs=1e-9) assert player.bankroll == expected_final_bankroll # Net profit should equal bankroll delta. assert player.bankroll - starting_bankroll == pytest.approx( - 129.5, rel=1e-9, abs=1e-9 + 130.0, rel=1e-9, abs=1e-9 ) diff --git a/tests/unit/test_bet.py b/tests/unit/test_bet.py index b49a4a61..a1c326c1 100644 --- a/tests/unit/test_bet.py +++ b/tests/unit/test_bet.py @@ -52,10 +52,10 @@ (crapssim.bet.World(1), -0.2667), (crapssim.bet.Big6(1), -0.0278), (crapssim.bet.Big8(1), -0.0278), - (crapssim.bet.Buy(4, 1), -0.0083), - (crapssim.bet.Buy(6, 1), -0.0083), - (crapssim.bet.Lay(4, 1), -0.0042), - (crapssim.bet.Lay(6, 1), -0.0069), + (crapssim.bet.Buy(4, 1), -0.0042), + (crapssim.bet.Buy(6, 1), -0.0069), + (crapssim.bet.Lay(4, 1), -0.0083), + (crapssim.bet.Lay(6, 1), -0.0083), (crapssim.bet.Put(4, 1), -0.0833), (crapssim.bet.Put(5, 1), -0.0556), (crapssim.bet.Put(6, 1), -0.0278), @@ -402,7 +402,8 @@ def test_lay_commission_floor(): player.add_bet(crapssim.bet.Lay(10, 20)) TableUpdate.roll(t, fixed_outcome=(4, 3)) TableUpdate.update_bets(t) - assert player.bankroll == pytest.approx(starting_bankroll + 10.0) + # Floor enforces a minimum commission even when the wager is smaller. + assert player.bankroll == pytest.approx(starting_bankroll - 15.0) def test_put_odds_allowed_when_point_on(): diff --git a/tests/unit/test_commission.py b/tests/unit/test_commission.py index 5907e44c..486938bf 100644 --- a/tests/unit/test_commission.py +++ b/tests/unit/test_commission.py @@ -1,9 +1,69 @@ +from __future__ import annotations + import math +from dataclasses import dataclass + +import pytest + +from crapssim.bet import _compute_commission + + +@dataclass +class _StubTable: + settings: dict + + +@pytest.mark.parametrize( + ("mode", "rounding", "floor", "bet", "expected"), + [ + ("on_bet", "ceil_dollar", 0.0, 20.0, 1.0), + ("on_win", "ceil_dollar", 0.0, 20.0, 1.0), + ("on_bet", "nearest_dollar", 1.0, 20.0, 1.0), + ("on_win", "nearest_dollar", 1.0, 25.0, 1.0), + ("on_bet", "nearest_dollar", 1.0, 5.0, 1.0), + ("on_bet", "none", 0.0, 20.0, 1.0), + ], +) +def test_compute_commission_expected_values(mode, rounding, floor, bet, expected): + table = _StubTable( + settings={ + "commission_mode": mode, + "commission_rounding": rounding, + "commission_floor": floor, + } + ) + + result = _compute_commission(table, gross_win=bet * 3.5, bet_amount=bet) + + assert math.isclose(result, expected, rel_tol=0.0, abs_tol=1e-9) + -from crapssim.bet import compute_commission +@pytest.mark.parametrize( + ("rounding", "floor", "bet"), + [ + ("ceil_dollar", 0.0, 20.0), + ("nearest_dollar", 1.0, 20.0), + ("nearest_dollar", 1.0, 5.0), + ("none", 0.0, 12.5), + ], +) +def test_commission_mode_parity(rounding, floor, bet): + table_on_bet = _StubTable( + settings={ + "commission_mode": "on_bet", + "commission_rounding": rounding, + "commission_floor": floor, + } + ) + table_on_win = _StubTable( + settings={ + "commission_mode": "on_win", + "commission_rounding": rounding, + "commission_floor": floor, + } + ) + on_bet = _compute_commission(table_on_bet, gross_win=bet * 20, bet_amount=bet) + on_win = _compute_commission(table_on_win, gross_win=bet * 0.5, bet_amount=bet) -def test_compute_commission_fixed_five_percent(): - assert compute_commission(100.0) == 5.0 - assert compute_commission(40.0) == 2.0 - assert math.isclose(compute_commission(12.34), 0.617, rel_tol=1e-9) + assert on_bet == pytest.approx(on_win, abs=1e-9)