diff --git a/README.md b/README.md index 1bfa3e81..97839648 100644 --- a/README.md +++ b/README.md @@ -156,13 +156,13 @@ After finishing, you land on the **"Ready to host"** screen: big Beatify wordmar **Learn the Game in 20 Seconds** -Players who scan the QR code drop into a swipeable 4-card tour that teaches the core mechanics in order: guess the year, double-or-nothing bet, steal an answer, guess the artist. Skip/Next always visible, auto-advances after 4 seconds per card. By the time the host hits Start, everyone knows what to do. +Players who scan the QR code drop into a swipeable 4-card tour that teaches the core mechanics in order: guess the year, triple-or-nothing bet, steal an answer, guess the artist. Skip/Next always visible, auto-advances after 4 seconds per card. By the time the host hits Start, everyone knows what to do. **The Rush** A song starts playing. The clock is ticking. You *know* this song... but was it '85 or '87? **The Strategy** -Answer fast for bonus points. Hit a streak for multipliers. Feeling confident? Bet double-or-nothing. +Answer fast for bonus points. Hit a streak for multipliers. Feeling confident? Bet triple-or-nothing. **The Reveal** The year drops. The room erupts. Someone nailed it. Someone was *way* off. Everyone's laughing. @@ -234,10 +234,10 @@ Linear scale in between. Hesitation costs points. Miss one? Streak resets. The pressure is real. -### Double or Nothing +### Triple or Nothing Feeling confident? Toggle the bet before submitting. -Score points: **Double them.** -Score zero: **Lose it all.** +Nail the **exact** year: **triple your points.** +Miss it — even by a year: **score nothing that round.** ### Artist Challenge (Optional) Know your artists? Enable this mode in game setup. diff --git a/custom_components/beatify/game/scoring.py b/custom_components/beatify/game/scoring.py index b261014a..a235bf83 100644 --- a/custom_components/beatify/game/scoring.py +++ b/custom_components/beatify/game/scoring.py @@ -36,6 +36,9 @@ POINTS_EXACT = 10 POINTS_WRONG = 0 +# A won bet (exact year) multiplies the round score by this (#1004). +BET_WIN_MULTIPLIER = 3 + def calculate_accuracy_score( guess: int, @@ -136,18 +139,23 @@ def calculate_round_score( def apply_bet_multiplier( round_score: int, bet: bool, # noqa: FBT001 + is_exact: bool, # noqa: FBT001 ) -> tuple[int, str | None]: """ - Apply bet multiplier to round score (Story 5.3). + Apply bet multiplier to round score (Story 5.3, redesigned #1004). - Betting is double-or-nothing: - - If bet and scored points (>0): double the score, outcome="won" - - If bet and 0 points: score stays 0, outcome="lost" - - If no bet: score unchanged, outcome=None + Betting is a real "exact or nothing" gamble: + - If bet and the guess is the EXACT year: round_score x BET_WIN_MULTIPLIER, + outcome="won". + - If bet and the guess is not exact: score becomes 0, outcome="lost" — + the player forfeits the points a close guess would otherwise have + earned. That forfeit is the stake that makes the bet a real risk. + - If no bet: score unchanged, outcome=None. Args: round_score: Points earned before bet (accuracy x speed) bet: Whether player placed a bet + is_exact: Whether the guess matched the correct year exactly Returns: Tuple of (final_score, bet_outcome) @@ -157,8 +165,8 @@ def apply_bet_multiplier( if not bet: return round_score, None - if round_score > 0: - return round_score * 2, "won" + if is_exact: + return round_score * BET_WIN_MULTIPLIER, "won" return 0, "lost" @@ -599,7 +607,7 @@ def score_player_round( player.years_off = abs(player.current_guess - correct_year) player.missed_round = False player.round_score, player.bet_outcome = apply_bet_multiplier( - speed_score, player.bet + speed_score, player.bet, player.years_off == 0 ) _apply_streak(player, speed_score, streak_achievements) diff --git a/custom_components/beatify/www/i18n/de.json b/custom_components/beatify/www/i18n/de.json index 801d353d..f9de69ed 100644 --- a/custom_components/beatify/www/i18n/de.json +++ b/custom_components/beatify/www/i18n/de.json @@ -86,8 +86,8 @@ "submittedCount": "{count}/{total} abgegeben", "round": "Runde {current} von {total}", "timeRemaining": "Noch {seconds}s", - "bet": "Alles oder Nichts", - "betShort": "×2", + "bet": "Dreifach oder nichts", + "betShort": "×3", "betActive": "Wette aktiv!", "noBonusThisRound": "Kein Bonus diese Runde — hau das Jahr rein", "lockedIn": "Tipp abgegeben · warte auf andere", @@ -156,7 +156,7 @@ "streakLost": "{count}er Serie verloren", "baseScore": "Basispunkte", "speedBonus": "Schnellbonus", - "betWon": "Wette gewonnen! 2x Punkte", + "betWon": "Wette gewonnen! 3x Punkte", "betLost": "Wette verloren", "noSubmission": "Kein Tipp abgegeben", "allPlayers": "Alle Spieler", @@ -180,7 +180,7 @@ "noGuess": "—" }, "chip": { - "betWon": "Wette gewonnen · ×2", + "betWon": "Wette gewonnen · ×3", "betLost": "Wette verloren", "streakBonus": "{count}-Serie · +{bonus}" }, @@ -192,7 +192,7 @@ "artistBonus": "Künstler-Challenge", "movieBonus": "Film-Challenge", "introBonus": "Intro-Speed-Bonus", - "betMultiplier": "Alles oder nichts", + "betMultiplier": "Dreifach oder nichts", "betLost": "Wette verloren", "total": "Gesamt diese Runde", "noSubmission": "Keinen Tipp abgegeben" @@ -620,11 +620,11 @@ "totalBets": "Wetten Gesamt", "totalBetsDesc": "Platzierte Wetten", "betsWon": "Gewonnene Wetten", - "betsWonDesc": "Doppelte Punkte!", + "betsWonDesc": "Dreifache Punkte!", "winRate": "Gewinnrate", "winRateDesc": "Erfolgsprozentsatz", "noBettingData": "Noch keine Wettdaten", - "bettingHint": "Spieler koennen wetten, um ihre Punkte bei sicheren Schaetzungen zu verdoppeln", + "bettingHint": "Spieler koennen wetten, um ihre Punkte zu verdreifachen - es gewinnt aber nur das exakte Jahr", "overview": "Übersicht", "games": "Spiele", "avgPlayersShort": "Ø Spieler", @@ -926,8 +926,8 @@ "letsPlay": "Los geht's", "yearTitle": "Rate das Jahr", "yearCaption": "Näher = mehr Punkte", - "betTitle": "Alles oder nichts", - "betCaption": "Wette, dass du nah dran bist. Verdopple die Punkte.", + "betTitle": "Dreifach oder nichts", + "betCaption": "Wette auf das exakte Jahr. Dreifache Punkte.", "stealTitle": "Antwort klauen", "stealCaption": "Kopiere einen anderen Spieler. Einmal pro Spiel.", "stealHint": "Nutz es, wenn du keine Ahnung hast.", diff --git a/custom_components/beatify/www/i18n/en.json b/custom_components/beatify/www/i18n/en.json index eeb46d46..fb324e7b 100644 --- a/custom_components/beatify/www/i18n/en.json +++ b/custom_components/beatify/www/i18n/en.json @@ -86,8 +86,8 @@ "submittedCount": "{count}/{total} submitted", "round": "Round {current} of {total}", "timeRemaining": "{seconds}s remaining", - "bet": "Double or Nothing", - "betShort": "×2", + "bet": "Triple or Nothing", + "betShort": "×3", "betActive": "Bet Active!", "noBonusThisRound": "No bonus this round — nail the year", "lockedIn": "Locked in · waiting for others", @@ -156,7 +156,7 @@ "streakLost": "Lost {count}-streak", "baseScore": "Base score", "speedBonus": "Speed bonus", - "betWon": "Bet won! 2x points", + "betWon": "Bet won! 3x points", "betLost": "Bet lost", "noSubmission": "No guess submitted", "allPlayers": "All Players", @@ -180,7 +180,7 @@ "noGuess": "—" }, "chip": { - "betWon": "Bet won · ×2", + "betWon": "Bet won · ×3", "betLost": "Bet lost", "streakBonus": "{count}-streak · +{bonus}" }, @@ -192,7 +192,7 @@ "artistBonus": "Artist challenge", "movieBonus": "Movie challenge", "introBonus": "Intro speed bonus", - "betMultiplier": "Double or Nothing", + "betMultiplier": "Triple or Nothing", "betLost": "Bet lost", "total": "Total this round", "noSubmission": "No guess this round" @@ -620,11 +620,11 @@ "totalBets": "Total Bets", "totalBetsDesc": "Bets placed", "betsWon": "Bets Won", - "betsWonDesc": "Double points!", + "betsWonDesc": "Triple points!", "winRate": "Win Rate", "winRateDesc": "Success percentage", "noBettingData": "No betting data yet", - "bettingHint": "Players can bet to double their points on confident guesses", + "bettingHint": "Players can bet to triple their points — but only an exact-year guess wins", "overview": "Overview", "games": "Games", "avgPlayersShort": "Avg Plrs", @@ -926,8 +926,8 @@ "letsPlay": "Let's play", "yearTitle": "Guess the year", "yearCaption": "Closer = more points", - "betTitle": "Double or nothing", - "betCaption": "Bet you're close. Win double.", + "betTitle": "Triple or nothing", + "betCaption": "Nail the exact year. Win triple.", "stealTitle": "Steal an answer", "stealCaption": "Copy another player. Once per game.", "stealHint": "Use it when you have no idea.", diff --git a/custom_components/beatify/www/i18n/es.json b/custom_components/beatify/www/i18n/es.json index ad09f275..56c88fd9 100644 --- a/custom_components/beatify/www/i18n/es.json +++ b/custom_components/beatify/www/i18n/es.json @@ -86,8 +86,8 @@ "submittedCount": "{count}/{total} enviados", "round": "Ronda {current} de {total}", "timeRemaining": "{seconds}s restantes", - "bet": "Doble o nada", - "betShort": "×2", + "bet": "Triple o nada", + "betShort": "×3", "noBonusThisRound": "Sin bonus esta ronda — acierta el año", "lockedIn": "Bloqueado · esperando a los demás", "lockedInWaitingCount": "Bloqueado · esperando a {count} más", @@ -156,7 +156,7 @@ "streakLost": "Perdiste racha de {count}", "baseScore": "Puntuacion base", "speedBonus": "Bonus de velocidad", - "betWon": "Apuesta ganada! 2x puntos", + "betWon": "Apuesta ganada! 3x puntos", "betLost": "Apuesta perdida", "noSubmission": "No enviaste respuesta", "allPlayers": "Todos los jugadores", @@ -180,7 +180,7 @@ "noGuess": "—" }, "chip": { - "betWon": "Apuesta ganada · ×2", + "betWon": "Apuesta ganada · ×3", "betLost": "Apuesta perdida", "streakBonus": "Racha de {count} · +{bonus}" }, @@ -192,7 +192,7 @@ "artistBonus": "Desafío de artista", "movieBonus": "Desafío de película", "introBonus": "Bonus de intro rápida", - "betMultiplier": "Dobla o nada", + "betMultiplier": "Triplica o nada", "betLost": "Apuesta perdida", "total": "Total de esta ronda", "noSubmission": "No enviaste respuesta" @@ -620,11 +620,11 @@ "totalBets": "Apuestas Totales", "totalBetsDesc": "Apuestas realizadas", "betsWon": "Apuestas Ganadas", - "betsWonDesc": "Puntos dobles!", + "betsWonDesc": "Puntos triples!", "winRate": "Tasa de Victoria", "winRateDesc": "Porcentaje de exito", "noBettingData": "Sin datos de apuestas todavia", - "bettingHint": "Los jugadores pueden apostar para duplicar sus puntos en adivinanzas seguras", + "bettingHint": "Los jugadores pueden apostar para triplicar sus puntos, pero solo gana el año exacto", "overview": "Resumen", "games": "Juegos", "avgPlayersShort": "Prom Jug", @@ -926,8 +926,8 @@ "letsPlay": "¡A jugar!", "yearTitle": "Adivina el año", "yearCaption": "Más cerca = más puntos", - "betTitle": "Dobla o nada", - "betCaption": "Apuesta a que estás cerca. Gana el doble.", + "betTitle": "Triple o nada", + "betCaption": "Apuesta por el año exacto. Gana el triple.", "stealTitle": "Roba una respuesta", "stealCaption": "Copia a otro jugador. Una vez por partida.", "stealHint": "Úsalo cuando no tengas ni idea.", diff --git a/custom_components/beatify/www/i18n/fr.json b/custom_components/beatify/www/i18n/fr.json index 4d0e22e4..d9c0243a 100644 --- a/custom_components/beatify/www/i18n/fr.json +++ b/custom_components/beatify/www/i18n/fr.json @@ -86,8 +86,8 @@ "submittedCount": "{count}/{total} envoyés", "round": "Manche {current} sur {total}", "timeRemaining": "{seconds}s restantes", - "bet": "Quitte ou double", - "betShort": "×2", + "bet": "Quitte ou triple", + "betShort": "×3", "noBonusThisRound": "Pas de bonus ce tour — trouve l'année", "lockedIn": "Verrouillé · en attente des autres", "lockedInWaitingCount": "Verrouillé · en attente de {count} joueur(s)", @@ -156,7 +156,7 @@ "streakLost": "Série de {count} perdue", "baseScore": "Score de base", "speedBonus": "Bonus de vitesse", - "betWon": "Pari gagné ! 2x points", + "betWon": "Pari gagné ! 3x points", "betLost": "Pari perdu", "noSubmission": "Pas de réponse envoyée", "allPlayers": "Tous les joueurs", @@ -180,7 +180,7 @@ "noGuess": "—" }, "chip": { - "betWon": "Pari gagné · ×2", + "betWon": "Pari gagné · ×3", "betLost": "Pari perdu", "streakBonus": "Série de {count} · +{bonus}" }, @@ -192,7 +192,7 @@ "artistBonus": "Défi artiste", "movieBonus": "Défi film", "introBonus": "Bonus intro rapide", - "betMultiplier": "Quitte ou double", + "betMultiplier": "Quitte ou triple", "betLost": "Pari perdu", "total": "Total du tour", "noSubmission": "Aucune réponse" @@ -620,11 +620,11 @@ "totalBets": "Paris totaux", "totalBetsDesc": "Paris effectués", "betsWon": "Paris gagnés", - "betsWonDesc": "Points doublés !", + "betsWonDesc": "Points triplés !", "winRate": "Taux de victoire", "winRateDesc": "Pourcentage de réussite", "noBettingData": "Pas encore de données de paris", - "bettingHint": "Les joueurs peuvent parier pour doubler leurs points sur des réponses sûres", + "bettingHint": "Les joueurs peuvent parier pour tripler leurs points, mais seule l'année exacte gagne", "overview": "Aperçu", "games": "Parties", "avgPlayersShort": "Moy Jou", @@ -926,8 +926,8 @@ "letsPlay": "C'est parti", "yearTitle": "Devine l'année", "yearCaption": "Plus près = plus de points", - "betTitle": "Quitte ou double", - "betCaption": "Parie que tu es proche. Gagne le double.", + "betTitle": "Quitte ou triple", + "betCaption": "Parie sur l'année exacte. Gagne le triple.", "stealTitle": "Vole une réponse", "stealCaption": "Copie un autre joueur. Une fois par partie.", "stealHint": "Utilise-le quand tu n'as aucune idée.", diff --git a/custom_components/beatify/www/i18n/nl.json b/custom_components/beatify/www/i18n/nl.json index e61980eb..2d045107 100644 --- a/custom_components/beatify/www/i18n/nl.json +++ b/custom_components/beatify/www/i18n/nl.json @@ -86,8 +86,8 @@ "submittedCount": "{count}/{total} verstuurd", "round": "Ronde {current} van {total}", "timeRemaining": "{seconds}s over", - "bet": "Dubbel of niets", - "betShort": "×2", + "bet": "Driedubbel of niets", + "betShort": "×3", "noBonusThisRound": "Geen bonus deze ronde — raad het jaar", "lockedIn": "Vergrendeld · wachten op anderen", "lockedInWaitingCount": "Vergrendeld · wachten op {count} meer", @@ -156,7 +156,7 @@ "streakLost": "Reeks van {count} kwijt", "baseScore": "Basisscore", "speedBonus": "Snelheidsbonus", - "betWon": "Inzet gewonnen! 2x punten", + "betWon": "Inzet gewonnen! 3x punten", "betLost": "Inzet verloren", "noSubmission": "Geen gok ingestuurd", "allPlayers": "Alle spelers", @@ -180,7 +180,7 @@ "noGuess": "—" }, "chip": { - "betWon": "Weddenschap gewonnen · ×2", + "betWon": "Weddenschap gewonnen · ×3", "betLost": "Weddenschap verloren", "streakBonus": "{count}-reeks · +{bonus}" }, @@ -192,7 +192,7 @@ "artistBonus": "Artiest-uitdaging", "movieBonus": "Film-uitdaging", "introBonus": "Intro-snelheidsbonus", - "betMultiplier": "Dubbel of niks", + "betMultiplier": "Driedubbel of niks", "betLost": "Weddenschap verloren", "total": "Totaal deze ronde", "noSubmission": "Geen gok ingediend" @@ -620,11 +620,11 @@ "totalBets": "Totaal aantal inzetten", "totalBetsDesc": "Geplaatste inzetten", "betsWon": "Gewonnen inzetten", - "betsWonDesc": "Dubbele punten!", + "betsWonDesc": "Drievoudige punten!", "winRate": "Winstpercentage", "winRateDesc": "Succespercentage", "noBettingData": "Nog geen inzetgegevens", - "bettingHint": "Spelers kunnen inzetten om hun punten te verdubbelen bij een zekere gok", + "bettingHint": "Spelers kunnen inzetten om hun punten te verdrievoudigen, maar alleen het exacte jaar wint", "overview": "Overzicht", "games": "Spellen", "avgPlayersShort": "Gem. splrs", @@ -926,8 +926,8 @@ "letsPlay": "Laten we spelen", "yearTitle": "Raad het jaar", "yearCaption": "Dichterbij = meer punten", - "betTitle": "Dubbel of niks", - "betCaption": "Wed dat je dichtbij zit. Verdubbel je punten.", + "betTitle": "Driedubbel of niks", + "betCaption": "Wed op het exacte jaar. Win het drievoudige.", "stealTitle": "Steel een antwoord", "stealCaption": "Kopieer een andere speler. Eén keer per spel.", "stealHint": "Gebruik het als je geen idee hebt.", diff --git a/custom_components/beatify/www/player.html b/custom_components/beatify/www/player.html index 102a5c43..6b2b6acc 100644 --- a/custom_components/beatify/www/player.html +++ b/custom_components/beatify/www/player.html @@ -110,21 +110,21 @@

Guess the year

- + diff --git a/tests/unit/test_scoring.py b/tests/unit/test_scoring.py index fb20aac9..fd00ec91 100644 --- a/tests/unit/test_scoring.py +++ b/tests/unit/test_scoring.py @@ -171,29 +171,38 @@ def test_hard_difficulty(self): class TestBetMultiplier: + """Betting is exact-or-nothing (#1004): a bet wins only on the exact + year (x3), and any non-exact guess forfeits the round score.""" + def test_no_bet_unchanged(self): - score, outcome = apply_bet_multiplier(10, False) + score, outcome = apply_bet_multiplier(10, False, is_exact=True) assert score == 10 assert outcome is None - def test_bet_won(self): - score, outcome = apply_bet_multiplier(10, True) - assert score == 20 + def test_bet_won_on_exact(self): + score, outcome = apply_bet_multiplier(10, True, is_exact=True) + assert score == 30 assert outcome == "won" + def test_bet_lost_when_not_exact_forfeits_score(self): + # A close guess that would have scored 5 — betting forfeits it. + score, outcome = apply_bet_multiplier(5, True, is_exact=False) + assert score == 0 + assert outcome == "lost" + def test_bet_lost_zero_score(self): - score, outcome = apply_bet_multiplier(0, True) + score, outcome = apply_bet_multiplier(0, True, is_exact=False) assert score == 0 assert outcome == "lost" def test_no_bet_zero_score(self): - score, outcome = apply_bet_multiplier(0, False) + score, outcome = apply_bet_multiplier(0, False, is_exact=False) assert score == 0 assert outcome is None def test_bet_won_large_score(self): - score, outcome = apply_bet_multiplier(50, True) - assert score == 100 + score, outcome = apply_bet_multiplier(50, True, is_exact=True) + assert score == 150 assert outcome == "won"