From c6d4702c0be4d2b37e1d82ff96265c52847c3f9a Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Sat, 3 Oct 2026 17:36:06 -0400 Subject: feat: partial commands now always work --- internal/casino/big_six.go | 43 ++++++++++++++++++++++++++++++++++---- internal/casino/big_six_test.go | 46 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 4 deletions(-) (limited to 'internal/casino') diff --git a/internal/casino/big_six.go b/internal/casino/big_six.go index 6d16bbc..b17bde4 100644 --- a/internal/casino/big_six.go +++ b/internal/casino/big_six.go @@ -34,9 +34,43 @@ func bigSixHouseEdge(_ TableConfig, bet string) HouseEdge { func bigSixTarget(s string) string { return strings.TrimPrefix(strings.ToUpper(strings.TrimSpace(s)), "$") } + +// bigSixResolveTarget maps a written target to a betting symbol. An exact +// label always wins over a longer label that shares its prefix, so "1" is the +// $1 symbol and never an ambiguous prefix of $10. Otherwise an unambiguous +// prefix is accepted, so "SW", "SWO" and "SWORD" all name the sword and "SH" +// names the shield. DOUBLE is not a betting target, so "DO" resolves to +// nothing. It reports false when nothing or more than one symbol matches. +func bigSixResolveTarget(s string) (string, bool) { + t := bigSixTarget(s) + if t == "" { + return "", false + } + match := "" + for _, spot := range BigSixSpots() { + if spot.Pays <= 0 { + continue + } + if spot.Label == t { + return spot.Label, true + } + if strings.HasPrefix(spot.Label, t) { + if match != "" { + return "", false + } + match = spot.Label + } + } + return match, match != "" +} + func bigSixPayout(s string) int { + target, ok := bigSixResolveTarget(s) + if !ok { + return 0 + } for _, spot := range BigSixSpots() { - if spot.Label == bigSixTarget(s) && spot.Pays > 0 { + if spot.Label == target { return spot.Pays + 1 } } @@ -225,10 +259,11 @@ func (t *bigSixTable) bet(name, spot string, amount int, w Wallet) []Event { if spot == "" { spot = p.lastSpot } - spot = bigSixTarget(spot) - if bigSixPayout(spot) == 0 { - return []Event{t.private(name, "Bet on 1, 2, 5, 10, 20, SWORD or SHIELD. DOUBLE is automatic, not a bet.")} + resolved, ok := bigSixResolveTarget(spot) + if !ok { + return []Event{t.private(name, "Bet on 1, 2, 5, 10, 20, SWORD or SHIELD — an unambiguous prefix is fine. DOUBLE is automatic, not a bet.")} } + spot = resolved if amount < t.Config.MinBet || amount > t.Config.MaxBet { return []Event{t.private(name, "Big 6 bets must be "+betRangeText(t.Config.MinBet, t.Config.MaxBet)+".")} } diff --git a/internal/casino/big_six_test.go b/internal/casino/big_six_test.go index cccad77..04bc7e3 100644 --- a/internal/casino/big_six_test.go +++ b/internal/casino/big_six_test.go @@ -291,3 +291,49 @@ func TestBigSixSettlementFailureAndDeparture(t *testing.T) { t.Fatal("departing player lost result visibility") } } + +func TestBigSixPartialTargets(t *testing.T) { + for _, tc := range []struct { + in string + want string + ok bool + }{ + {"1", "1", true}, + {"10", "10", true}, + {"$20", "20", true}, + {"sw", "SWORD", true}, + {"SWO", "SWORD", true}, + {"sword", "SWORD", true}, + {"sh", "SHIELD", true}, + {"shi", "SHIELD", true}, + {"shield", "SHIELD", true}, + {"s", "", false}, // SWORD or SHIELD + {"do", "", false}, // DOUBLE is automatic, not a wager + {"double", "", false}, // and its full name is not a wager either + {"", "", false}, + {"99", "", false}, + } { + got, ok := bigSixResolveTarget(tc.in) + if got != tc.want || ok != tc.ok { + t.Fatalf("bigSixResolveTarget(%q) = (%q,%v), want (%q,%v)", tc.in, got, ok, tc.want, tc.ok) + } + } + // A partial target must behave exactly like the full label at bet time, + // both in the stored wager and in the house-edge reference. + cfg := TableConfig{Game: GameBigSix} + if e := cfg.HouseEdge("sw"); len(e.Parts) != 1 || e.Parts[0].Loss != 1.0/49 { + t.Fatalf("partial house edge: %+v", e) + } + table := newBigSixTest() + table.join("a") + w := &recordingWallet{testWallet: testWallet{credits: 100}} + table.bet("a", "sw", 5, w) + table.bet("a", "sh", 7, w) + bets := table.players.get("a").bets + if len(w.open) != 2 || len(bets) != 2 || bets[0].spot != "SWORD" || bets[1].spot != "SHIELD" { + t.Fatalf("partial targets not canonicalized: %+v", bets) + } + if bigSixPayout("s") != 0 || bigSixPayout("sw") != 48 || bigSixPayout("sh") != 48 { + t.Fatal("partial payout") + } +} -- cgit v1.2.3