aboutsummaryrefslogtreecommitdiff
path: root/internal/casino
diff options
context:
space:
mode:
Diffstat (limited to 'internal/casino')
-rw-r--r--internal/casino/baccarat.go10
-rw-r--r--internal/casino/baccarat_test.go4
-rw-r--r--internal/casino/blackjack.go50
-rw-r--r--internal/casino/blackjack_test.go20
-rw-r--r--internal/casino/config_validate.go53
-rw-r--r--internal/casino/correct_casino_refund_bank_test.go32
-rw-r--r--internal/casino/machine.go34
-rw-r--r--internal/casino/p2_hardening_test.go60
-rw-r--r--internal/casino/types.go18
9 files changed, 247 insertions, 34 deletions
diff --git a/internal/casino/baccarat.go b/internal/casino/baccarat.go
index 231a011..c9a50a1 100644
--- a/internal/casino/baccarat.go
+++ b/internal/casino/baccarat.go
@@ -369,6 +369,16 @@ func settleBaccaratBet(spot string, bet int, playerTotal, bankerTotal int, rules
case bankerTotal > playerTotal:
if spot == baccaratSpotBanker {
bankerWin, _ := MulPayout(bet, rules.BankerPayout)
+ // The banker commission rounds UP, not down: when the wager is
+ // too small for a whole credit of commission, a winning banker
+ // bet returns only the stake. MulPayout's positive-stake floor
+ // would otherwise round the commission to zero and make the
+ // banker bet +EV (it wins more often than it loses, and ties
+ // push), which the "never bet less than one commission unit"
+ // rule exists to prevent.
+ if float64(bet)*rules.BankerPayout < 1 {
+ return "push", bet
+ }
return "win", bet + bankerWin
}
default:
diff --git a/internal/casino/baccarat_test.go b/internal/casino/baccarat_test.go
index c5565b1..39941eb 100644
--- a/internal/casino/baccarat_test.go
+++ b/internal/casino/baccarat_test.go
@@ -108,8 +108,8 @@ func TestBaccaratSettlement(t *testing.T) {
if result, payout := settleBaccaratBet(baccaratSpotBanker, 100, 4, 6, rules); result != "win" || payout != 195 {
t.Fatalf("banker win: result=%s payout=%d, want win/195 (5%% commission)", result, payout)
}
- if result, payout := settleBaccaratBet(baccaratSpotBanker, 1, 4, 6, rules); result != "win" || payout != 2 {
- t.Fatalf("banker win on bet 1: result=%s payout=%d, want win/2 (a winning stake never pays zero)", result, payout)
+ if result, payout := settleBaccaratBet(baccaratSpotBanker, 1, 4, 6, rules); result != "push" || payout != 1 {
+ t.Fatalf("banker win on bet 1: result=%s payout=%d, want push/1 (a sub-credit commission rounds up, returning only the stake)", result, payout)
}
if result, payout := settleBaccaratBet(baccaratSpotTie, 100, 7, 7, rules); result != "win" || payout != 900 {
t.Fatalf("tie win: result=%s payout=%d, want win/900 (8:1)", result, payout)
diff --git a/internal/casino/blackjack.go b/internal/casino/blackjack.go
index 16a480c..fd0572b 100644
--- a/internal/casino/blackjack.go
+++ b/internal/casino/blackjack.go
@@ -618,7 +618,16 @@ func (t *blackjackTable) settle() []Event {
}
payoutFailed := false
for _, hand := range p.hands {
- result, payout := settleHand(hand, dealerTotal, dealerBlackjack, t.Rules)
+ result, payout, settleErr := settleHand(hand, dealerTotal, dealerBlackjack, t.Rules)
+ if settleErr != nil {
+ // A payout multiplier that overflows the credit cap is a
+ // configuration defect, not a payable win. Treat it like a
+ // failed settlement so the stake stays open for recovery to
+ // refund rather than paying a clamped/absurd amount.
+ payoutFailed = true
+ events = append(events, t.playerAction(p.Name, "Settlement unavailable.", payoutFailedMessage()))
+ continue
+ }
if payout > 0 && p.Wallet != nil {
if err := p.Wallet.PayStakeCover(payout, hand.stakes); err != nil {
// The payout could not be made durable. The journal
@@ -658,10 +667,17 @@ func (t *blackjackTable) settle() []Event {
}
if p.insurance > 0 {
insurancePayout := 0
+ var insuranceErr error
if dealerBlackjack {
- insurancePayout, _ = MulPayout(p.insurance, 1+t.Rules.InsurancePayout)
+ insurancePayout, insuranceErr = MulPayout(p.insurance, 1+t.Rules.InsurancePayout)
}
- if insurancePayout > 0 {
+ switch {
+ case insuranceErr != nil:
+ // Overflow is a config defect: leave the insurance stake open
+ // for recovery to refund instead of paying a clamped amount.
+ payoutFailed = true
+ events = append(events, t.private(p.Name, payoutFailedMessage()))
+ case insurancePayout > 0:
if err := p.Wallet.PayStakeCover(insurancePayout, []string{p.insuranceStakeID}); err != nil {
payoutFailed = true
events = append(events, t.private(p.Name, payoutFailedMessage()))
@@ -669,7 +685,7 @@ func (t *blackjackTable) settle() []Event {
p.Wallet.ResolveStake(p.insuranceStakeID)
events = append(events, t.private(p.Name, fmt.Sprintf("Insurance pays %d credits.", insurancePayout)))
}
- } else {
+ default:
// Insurance lost: its stake was consumed by the rules.
p.Wallet.ResolveStake(p.insuranceStakeID)
events = append(events, t.private(p.Name, "Insurance pays 0 credits."))
@@ -690,36 +706,36 @@ func (t *blackjackTable) settle() []Event {
return events
}
-func settleHand(hand *blackjackHand, dealerTotal int, dealerBlackjack bool, rules BlackjackConfig) (string, int) {
+func settleHand(hand *blackjackHand, dealerTotal int, dealerBlackjack bool, rules BlackjackConfig) (string, int, error) {
total, _ := hand.total()
if hand.surrendered {
- return "surrender", hand.bet / 2
+ return "surrender", hand.bet / 2, nil
}
if hand.bust {
- return "bust", 0
+ return "bust", 0, nil
}
playerBlackjack := hand.blackjack(rules)
if playerBlackjack && dealerBlackjack {
- return "push", hand.bet
+ return "push", hand.bet, nil
}
if dealerBlackjack {
- return "dealer blackjack", 0
+ return "dealer blackjack", 0, nil
}
if playerBlackjack {
// Winnings round down to whole credits; never bet less than two.
- blackjackWin, _ := MulPayout(hand.bet, rules.BlackjackPayout)
- return "blackjack", hand.bet + blackjackWin
+ blackjackWin, err := MulPayout(hand.bet, rules.BlackjackPayout)
+ if err != nil {
+ return "blackjack", 0, err
+ }
+ return "blackjack", hand.bet + blackjackWin, nil
}
if total > 21 || dealerTotal > total && dealerTotal <= 21 {
- return "loss", 0
+ return "loss", 0, nil
}
if dealerTotal == total {
- return "push", hand.bet
- }
- if dealerTotal > 21 {
- return "win", hand.bet * 2
+ return "push", hand.bet, nil
}
- return "win", hand.bet * 2
+ return "win", hand.bet * 2, nil
}
func (t *blackjackTable) resetRound() {
diff --git a/internal/casino/blackjack_test.go b/internal/casino/blackjack_test.go
index 0f7527c..2e2e985 100644
--- a/internal/casino/blackjack_test.go
+++ b/internal/casino/blackjack_test.go
@@ -34,22 +34,22 @@ func TestBlackjackHandScoreShowsBlackjackAs21(t *testing.T) {
func TestBlackjackSettlementRules(t *testing.T) {
rules := (BlackjackConfig{}).Normalize()
natural := &blackjackHand{bet: 10, cards: []card{{rank: "A"}, {rank: "K"}}}
- if result, payout := settleHand(natural, 20, false, rules); result != "blackjack" || payout != 25 {
- t.Fatalf("natural result=%s payout=%d", result, payout)
+ if result, payout, err := settleHand(natural, 20, false, rules); err != nil || result != "blackjack" || payout != 25 {
+ t.Fatalf("natural result=%s payout=%d err=%v", result, payout, err)
}
smallNatural := &blackjackHand{bet: 1, cards: []card{{rank: "A"}, {rank: "K"}}}
- if result, payout := settleHand(smallNatural, 20, false, rules); result != "blackjack" || payout != 2 {
- t.Fatalf("small natural result=%s payout=%d, want blackjack/2 (winnings round down)", result, payout)
+ if result, payout, err := settleHand(smallNatural, 20, false, rules); err != nil || result != "blackjack" || payout != 2 {
+ t.Fatalf("small natural result=%s payout=%d err=%v, want blackjack/2 (winnings round down)", result, payout, err)
}
dealerNatural := &blackjackHand{bet: 10, cards: []card{{rank: "A"}, {rank: "Q"}}}
- if result, payout := settleHand(dealerNatural, 21, true, rules); result != "push" || payout != 10 {
- t.Fatalf("dealer natural result=%s payout=%d", result, payout)
+ if result, payout, err := settleHand(dealerNatural, 21, true, rules); err != nil || result != "push" || payout != 10 {
+ t.Fatalf("dealer natural result=%s payout=%d err=%v", result, payout, err)
}
- if result, payout := settleHand(&blackjackHand{bet: 10, cards: []card{{rank: "10"}, {rank: "8"}}}, 21, true, rules); result != "dealer blackjack" || payout != 0 {
- t.Fatalf("dealer blackjack result=%s payout=%d", result, payout)
+ if result, payout, err := settleHand(&blackjackHand{bet: 10, cards: []card{{rank: "10"}, {rank: "8"}}}, 21, true, rules); err != nil || result != "dealer blackjack" || payout != 0 {
+ t.Fatalf("dealer blackjack result=%s payout=%d err=%v", result, payout, err)
}
- if result, payout := settleHand(&blackjackHand{bet: 10, cards: []card{{rank: "10"}, {rank: "8"}}}, 22, false, rules); result != "win" || payout != 20 {
- t.Fatalf("dealer bust result=%s payout=%d", result, payout)
+ if result, payout, err := settleHand(&blackjackHand{bet: 10, cards: []card{{rank: "10"}, {rank: "8"}}}, 22, false, rules); err != nil || result != "win" || payout != 20 {
+ t.Fatalf("dealer bust result=%s payout=%d err=%v", result, payout, err)
}
}
diff --git a/internal/casino/config_validate.go b/internal/casino/config_validate.go
index 38d426d..a03cfbf 100644
--- a/internal/casino/config_validate.go
+++ b/internal/casino/config_validate.go
@@ -50,6 +50,12 @@ func (c TableConfig) Validate() error {
if !finiteNonnegative(b.BlackjackPayout) || !finiteNonnegative(b.InsurancePayout) {
return fmt.Errorf("blackjack_payout and insurance_payout must be finite and nonnegative")
}
+ if b.BlackjackPayout > maxBlackjackPayout {
+ return fmt.Errorf("blackjack_payout %v exceeds the %d:1 cap", b.BlackjackPayout, maxBlackjackPayout)
+ }
+ if b.InsurancePayout > maxInsurancePayout {
+ return fmt.Errorf("insurance_payout %v exceeds the %d:1 cap", b.InsurancePayout, maxInsurancePayout)
+ }
rules := b.Normalize()
if rules.AllowsSurrender() && c.Normalize().MinBet < minimumSurrenderBet {
return fmt.Errorf("surrender needs min_bet >= %d: a surrendered hand returns half the bet, which rounds to zero below that", minimumSurrenderBet)
@@ -70,6 +76,12 @@ func (c TableConfig) Validate() error {
if !finiteNonnegative(b.TiePayout) || !finiteNonnegative(b.BankerPayout) {
return fmt.Errorf("tie_payout and banker_payout must be finite and nonnegative")
}
+ if b.TiePayout > maxTiePayout {
+ return fmt.Errorf("tie_payout %v exceeds the %d:1 cap", b.TiePayout, maxTiePayout)
+ }
+ if b.BankerPayout > maxBankerPayout {
+ return fmt.Errorf("banker_payout %v exceeds the %v:1 cap", b.BankerPayout, maxBankerPayout)
+ }
// Non-blocking for the same reason as blackjack above.
return nil
default:
@@ -114,6 +126,22 @@ func validateShoe(decks int, shuffleAt float64) error {
// the whole stake.
const minimumSurrenderBet = 2
+// Payout-multiplier caps. Validation bounds authored multipliers so a typo
+// (e.g. blackjack_payout: 15) cannot silently create an absurd table and so
+// MulPayout's overflow backstop is not the only thing standing between a
+// configuration error and a MaxCredits payout.
+const (
+ maxBlackjackPayout = 5
+ maxInsurancePayout = 5
+ maxTiePayout = 10
+ maxBankerPayout = 0.99
+)
+
+// maxSlotSymbolLen is the fixed width of one reel cell in the renderer
+// (" " + symbol + padding + " "); a longer id would make the renderer's
+// width padding negative and panic.
+const maxSlotSymbolLen = 7
+
// blackjackRoundCards is the heuristic per-round card budget the shoe uses
// to decide whether to reshuffle before a round: an estimated eleven cards
// per hand times the most hands the table can have in play (the dealer's
@@ -141,6 +169,31 @@ func (p SlotProfile) Validate() error {
return fmt.Errorf("unsupported slot geometry %dx%d: the runtime evaluator supports %dx%d only",
profile.Rows, profile.Reels, slotGridRows, slotGridReels)
}
+ // Symbol IDs are looked up by exact string and rendered in fixed-width
+ // reel cells, so a duplicate ID silently pays that win twice, an over-long
+ // ID panics the renderer, and a zero total weight makes weightedSymbol
+ // unable to pick anything.
+ seenSymbols := make(map[string]bool, len(profile.Symbols))
+ totalWeight := 0
+ for _, symbol := range profile.Symbols {
+ if symbol.ID == "" {
+ return fmt.Errorf("slot symbol with an empty id")
+ }
+ if len(symbol.ID) > maxSlotSymbolLen {
+ return fmt.Errorf("slot symbol id %q exceeds the %d-character reel width", symbol.ID, maxSlotSymbolLen)
+ }
+ if seenSymbols[symbol.ID] {
+ return fmt.Errorf("duplicate slot symbol id %q", symbol.ID)
+ }
+ seenSymbols[symbol.ID] = true
+ if symbol.Weight < 0 {
+ return fmt.Errorf("slot symbol %q has negative reel weight", symbol.ID)
+ }
+ totalWeight += symbol.Weight
+ }
+ if totalWeight <= 0 {
+ return fmt.Errorf("slot symbols have no total reel weight, so no symbol can ever appear")
+ }
// A named trigger must exist, or the feature silently never fires while
// the RTP model also computes a zero trigger probability and reports green.
if profile.FreeSpinTrigger != "" {
diff --git a/internal/casino/correct_casino_refund_bank_test.go b/internal/casino/correct_casino_refund_bank_test.go
index fd90ae1..4262997 100644
--- a/internal/casino/correct_casino_refund_bank_test.go
+++ b/internal/casino/correct_casino_refund_bank_test.go
@@ -99,3 +99,35 @@ func TestMachineRefundAllFailedRefundKeepsStakeOpenForRecovery(t *testing.T) {
t.Fatalf("the failed refund was not reported: %+v", events)
}
}
+
+// TestMachineDisconnectSettlesBankedFeatureWinnings: already-landed free-spin
+// winnings are not part of the forfeited in-flight wager, so a disconnect must
+// settle them through the wallet rather than silently zeroing featureBank.
+func TestMachineDisconnectSettlesBankedFeatureWinnings(t *testing.T) {
+ m := newMachine(1, MachineConfig{ID: "slots", Game: GameSlots}, newTestRand())
+ wallet := &testWallet{credits: 100}
+ m.join("alice")
+ m.beginSpinLocked(10, wallet, "", "")
+ m.spin = &SlotSpin{Payout: 25, ScatterCount: 3}
+ m.finishSpin() // triggers the feature: 25 banked, the stake stays open
+ if m.featureBank != 25 || !m.inFreeSpins {
+ t.Fatalf("setup: featureBank=%v inFreeSpins=%v, want 25/true", m.featureBank, m.inFreeSpins)
+ }
+
+ events := m.markDisconnected("alice")
+ if wallet.paid != 25 {
+ t.Fatalf("disconnect paid %d, want 25 banked free-spin credits", wallet.paid)
+ }
+ if got := wallet.credits + wallet.paid; got != 115 {
+ t.Fatalf("banked winnings were destroyed on disconnect: balance %d, want 115 (100 - 10 forfeited stake + 25 banked)", got)
+ }
+ if !hasMessage(events, "25") {
+ t.Fatalf("missing banked-winnings settlement message: %+v", events)
+ }
+ if len(wallet.open) != 0 {
+ t.Fatalf("stake still outstanding after disconnect: %+v", wallet.open)
+ }
+ if m.owns("alice") {
+ t.Fatal("machine still owns the disconnected player")
+ }
+}
diff --git a/internal/casino/machine.go b/internal/casino/machine.go
index cf0ccb9..83e018f 100644
--- a/internal/casino/machine.go
+++ b/internal/casino/machine.go
@@ -380,15 +380,47 @@ func (m *Machine) resetSeat() {
// markDisconnected removes the player from the machine, forfeiting any
// in-flight wager, so the seat is immediately free for someone else.
+//
+// Already-landed free-spin winnings (featureBank) are NOT part of the
+// in-flight wager: they are settled after resetSeat resolves the triggering
+// stake, so the unlinked settlement cannot be read at recovery as covering
+// that stake. If the round's payout could not be persisted (stakeFailed), the
+// stake is left open for recovery and the bank is reported as forfeited
+// rather than silently destroyed.
func (m *Machine) markDisconnected(name string) []Event {
m.mu.Lock()
defer m.mu.Unlock()
if !m.owns(name) {
return nil
}
+ bank := int(m.featureBank)
+ wallet := m.player.Wallet
+ stakeFailed := m.stakeFailed
m.resetSeat()
- return []Event{{RoomID: m.RoomID, TableID: m.Config.ID, Type: EventLeft, Player: name,
+ events := []Event{{RoomID: m.RoomID, TableID: m.Config.ID, Type: EventLeft, Player: name,
Message: fmt.Sprintf("%s stands up from the slots machine.", name), Color: "casino_action", Public: true}}
+ if bank <= 0 || wallet == nil {
+ return events
+ }
+ switch {
+ case stakeFailed:
+ events = append(events, Event{RoomID: m.RoomID, TableID: m.Config.ID, Type: EventResult,
+ PrivateTo: name, Player: name,
+ PrivateMessage: fmt.Sprintf("Your %d credits of banked free-spin winnings were forfeited because the round could not be persisted.", bank),
+ Color: "casino_action", Public: false})
+ case wallet.PayCredits(bank) != nil:
+ events = append(events, Event{RoomID: m.RoomID, TableID: m.Config.ID, Type: EventResult,
+ PrivateTo: name, Player: name,
+ PrivateMessage: fmt.Sprintf("Your %d credits of free-spin winnings could not be persisted; they are recorded for recovery.", bank),
+ Color: "casino_action", Public: false})
+ default:
+ events = append(events, Event{RoomID: m.RoomID, TableID: m.Config.ID, Type: EventResult,
+ PrivateTo: name, Player: name,
+ Message: fmt.Sprintf("%s's %d banked free-spin credits pay out on disconnect.", name, bank),
+ PrivateMessage: fmt.Sprintf("The house settles your %d banked free-spin winnings.", bank),
+ Color: "casino_action", Public: true})
+ }
+ return events
}
func (m *Machine) hasParticipant(name string) bool {
diff --git a/internal/casino/p2_hardening_test.go b/internal/casino/p2_hardening_test.go
new file mode 100644
index 0000000..435e102
--- /dev/null
+++ b/internal/casino/p2_hardening_test.go
@@ -0,0 +1,60 @@
+package casino
+
+import "testing"
+
+// TestTableConfigRejectsAbsurdMultipliers: authored payout multipliers are
+// bounded so a typo cannot silently create an absurd table (and so the
+// MulPayout overflow backstop is not the only line of defence).
+func TestTableConfigRejectsAbsurdMultipliers(t *testing.T) {
+ for name, c := range map[string]TableConfig{
+ "blackjack payout": {ID: "t", Game: GameBlackjack, Blackjack: BlackjackConfig{BlackjackPayout: 15}},
+ "insurance payout": {ID: "t", Game: GameBlackjack, Blackjack: BlackjackConfig{InsurancePayout: 15}},
+ "tie payout": {ID: "t", Game: GameBaccarat, Baccarat: BaccaratConfig{TiePayout: 100}},
+ "banker payout": {ID: "t", Game: GameBaccarat, Baccarat: BaccaratConfig{BankerPayout: 1.5}},
+ } {
+ t.Run(name, func(t *testing.T) {
+ if c.Validate() == nil {
+ t.Fatalf("accepted an absurd payout multiplier: %+v", c)
+ }
+ })
+ }
+ // The shipped/default values remain valid.
+ if err := (TableConfig{ID: "t", Game: GameBaccarat}).Validate(); err != nil {
+ t.Fatalf("default baccarat table rejected: %v", err)
+ }
+ if err := (TableConfig{ID: "t", Game: GameBlackjack}).Validate(); err != nil {
+ t.Fatalf("default blackjack table rejected: %v", err)
+ }
+}
+
+// TestSlotProfileRejectsDuplicateSymbol: a duplicated id would pay that win
+// twice because evaluateSlot matches symbols by id.
+func TestSlotProfileRejectsDuplicateSymbol(t *testing.T) {
+ p := DefaultHuffAndPuffProfile()
+ p.Symbols = append(p.Symbols, p.Symbols[0])
+ if err := p.Validate(); err == nil {
+ t.Fatal("accepted a duplicate slot symbol id")
+ }
+}
+
+// TestSlotProfileRejectsZeroWeight: with no total reel weight weightedSymbol
+// can never pick a symbol.
+func TestSlotProfileRejectsZeroWeight(t *testing.T) {
+ p := DefaultHuffAndPuffProfile()
+ for i := range p.Symbols {
+ p.Symbols[i].Weight = 0
+ }
+ if err := p.Validate(); err == nil {
+ t.Fatal("accepted a profile with no reel weight")
+ }
+}
+
+// TestSlotProfileRejectsOverlongSymbol: the renderer pads to a fixed 7-cell
+// width, so a longer id makes the padding negative and panics.
+func TestSlotProfileRejectsOverlongSymbol(t *testing.T) {
+ p := DefaultHuffAndPuffProfile()
+ p.Symbols[0].ID = "way-too-long"
+ if err := p.Validate(); err == nil {
+ t.Fatal("accepted a symbol id longer than the reel width")
+ }
+}
diff --git a/internal/casino/types.go b/internal/casino/types.go
index 71da01c..578aee1 100644
--- a/internal/casino/types.go
+++ b/internal/casino/types.go
@@ -44,10 +44,20 @@ func ruleBool(value *bool, defaultValue bool) bool {
func (c BlackjackConfig) DealerHitsSoft17() bool { return ruleBool(c.DealerHitSoft17, false) }
func (c BlackjackConfig) AllowsResplitAces() bool { return ruleBool(c.ResplitAces, false) }
func (c BlackjackConfig) AllowsDoubleAfterSplit() bool { return ruleBool(c.DoubleAfterSplit, true) }
+
+// CountsDoubleSplitBlackjack reports whether a 3+ card hand that reaches 21
+// after a split and double is paid as a blackjack (3:2). When true (the
+// default) this raises the table RTP above the nominal blackjack payout;
+// standard play pays 1:1 for any 21 on a split hand, doubled or not.
func (c BlackjackConfig) CountsDoubleSplitBlackjack() bool {
return ruleBool(c.DoubleSplitBlackjack, true)
}
-func (c BlackjackConfig) AllowsSurrender() bool { return ruleBool(c.Surrender, false) }
+
+func (c BlackjackConfig) AllowsSurrender() bool { return ruleBool(c.Surrender, false) }
+
+// AllowsUnlikeTenSplit reports whether any two 10-valued cards may split
+// (K+Q, 10+J). Standard casinos split only identical ranks, so a true value
+// (the default) is player-favourable.
func (c BlackjackConfig) AllowsUnlikeTenSplit() bool { return ruleBool(c.SplitUnlikeTenValues, true) }
func (c BlackjackConfig) Normalize() BlackjackConfig {
@@ -159,14 +169,14 @@ func (c TableConfig) Normalize() TableConfig {
if c.MaxBet <= 0 {
c.MaxBet = DefaultMaxBet
}
- if c.MaxBet < c.MinBet {
- c.MaxBet = c.MinBet
- }
// Normalize the nested game rule blocks too: a new game that reads
// t.Config.Blackjack/Baccarat directly (rather than only t.Rules) would
// otherwise see zero values and build an empty shoe.
c.Blackjack = c.Blackjack.Normalize()
c.Baccarat = c.Baccarat.Normalize()
+ if c.MaxBet < c.MinBet {
+ c.MaxBet = c.MinBet
+ }
return c
}