feat: betting is now exact-or-nothing, not free upside (#1004)#1006
Conversation
Before, a bet doubled the score on any points and yielded 0 otherwise — but scoring 0 already breaks the streak and banks 0, so a "lost" bet cost nothing. Betting was pure free upside; every rational player just always bet. Now (#1004, Design A): a bet wins only on the EXACT year → round score x3 (BET_WIN_MULTIPLIER). A non-exact guess forfeits the round score entirely — that forfeit is the stake that makes it a real gamble. `apply_bet_multiplier` takes an `is_exact` flag; the caller passes `player.years_off == 0`. README "Double or Nothing" section updated. In-app ×2 labels still need a copy sweep — tracked separately. Closes #1004 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request redesigns the betting mechanic from a double-or-nothing system to an "exact or nothing" gamble. Players now receive a triple-point multiplier for guessing the exact year but forfeit all points for the round if they miss the exact year while a bet is active. The changes include updates to the scoring logic, documentation in the README, and corresponding unit tests. A critical issue was identified where lost bets fail to reset the player's streak because the streak calculation incorrectly uses the pre-bet score; a fix was suggested to use the final round score instead.
| player.round_score, player.bet_outcome = apply_bet_multiplier( | ||
| speed_score, player.bet | ||
| speed_score, player.bet, player.years_off == 0 | ||
| ) |
There was a problem hiding this comment.
The new "exact or nothing" betting logic introduces a bug where a player's streak is not reset when they lose a bet on a "close" guess.
In the previous implementation, a bet only resulted in a "lost" outcome if the base score was already 0. Now, a bet can be lost even if the guess was within the scoring range (i.e., speed_score > 0).
Because _apply_streak (on line 613) is called with speed_score instead of the final player.round_score, the streak will incorrectly increment and potentially award a streak bonus despite the player scoring 0 for the round. Updating speed_score to match the post-bet round_score ensures that any round resulting in 0 points correctly resets the streak.
| player.round_score, player.bet_outcome = apply_bet_multiplier( | |
| speed_score, player.bet | |
| speed_score, player.bet, player.years_off == 0 | |
| ) | |
| player.round_score, player.bet_outcome = apply_bet_multiplier( | |
| speed_score, player.bet, player.years_off == 0 | |
| ) | |
| # Use post-bet score for streak calculation to ensure lost bets reset streaks | |
| speed_score = player.round_score |
Follow-up to the betting-mechanic change: the feature is renamed Double → Triple or Nothing, and every user-facing string is corrected from the old ×2 / "double" / "you're close" wording to ×3 / "triple" / "exact year". Covers the onboarding tour (player.html), the bet button, won-bet labels, stats and hints across all five locales, and the README. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Why
Discussion #950 / issue #1004: the double-or-nothing bet had zero downside. A bet doubled the score on any points and gave 0 otherwise — but a round scoring 0 already breaks the streak and banks 0, so a "lost" bet cost nothing. Betting was free upside; the rational play was to always bet.
Change — Design A "exact or nothing" + rename to Triple or Nothing
Mechanic (
scoring.py,test_scoring.py):BET_WIN_MULTIPLIER).apply_bet_multiplier(round_score, bet, is_exact); caller passesplayer.years_off == 0.Copy — the feature is renamed Double → Triple or Nothing, and every user-facing string moves from ×2 / "double" / "bet you're close" to ×3 / "triple" / "exact year":
player.htmlonboarding tour (title, caption, bet button, score mockup 12×3=36)i18n/{en,de,es,fr,nl}.json— bet, betShort, betWon, betMultiplier, betsWonDesc, bettingHint, onboarding title/captionFrontend not browser-tested — please smoke-test the onboarding tour + bet flow before merge.
Closes #1004
🤖 Generated with Claude Code