package casino import "testing" type testWallet struct { chips, credits, paid int open []Wager } func (w *testWallet) Wager(amount int) (Wager, error) { if w.chips+w.credits < amount { return Wager{}, ErrInsufficientFunds } chips := amount if chips > w.chips { chips = w.chips } w.chips -= chips credits := amount - chips w.credits -= credits wager := Wager{Total: amount, Chips: chips, Credits: credits, ID: string(rune('a' + len(w.open)))} w.open = append(w.open, wager) return wager, nil } func (w *testWallet) PayCredits(amount int) error { w.paid += amount return nil } // PayStakeCover routes through PayCredits so a wallet rejecting payouts // (failingPayoutWallet) rejects stake-covering settlements identically. func (w *testWallet) PayStakeCover(amount int, _ []string) error { return w.PayCredits(amount) } // ResolveStake settles one outstanding stake by handle, mirroring the // production wallet. func (w *testWallet) ResolveStake(stakeID string) { kept := w.open[:0] for _, wager := range w.open { if wager.ID != stakeID { kept = append(kept, wager) } } w.open = kept } // ResolveStakes mirrors the production wallet: every outstanding stake is // settled (no longer refundable). func (w *testWallet) ResolveStakes() { w.open = nil } // RefundOutstanding returns every outstanding stake to the balances and // resolves the stakes, mirroring the production refund path: credits are // paid through PayCredits (recorded in paid), chips return as chips. func (w *testWallet) RefundOutstanding() (chips, credits int, err error) { for _, wager := range w.open { chips += wager.Chips credits += wager.Credits } w.open = nil w.chips += chips if credits > 0 { w.paid += credits } return chips, credits, nil } func TestMixedWagerUsesChipsFirst(t *testing.T) { w := &testWallet{chips: 7, credits: 50} wager, err := w.Wager(12) if err != nil || wager.Chips != 7 || wager.Credits != 5 || w.chips != 0 || w.credits != 45 { t.Fatalf("unexpected wager: %+v wallet=%+v", wager, w) } } func TestJoinUnknownTableGameRejected(t *testing.T) { m := NewManager(1) events := m.Join(1, TableConfig{ID: "slots", Game: GameSlots}, "alice") if len(events) != 1 || !contains(events[0].Message, "not available") { t.Fatalf("joining an unsupported table game: %+v", events) } if m.Table(1, "slots") != nil { t.Fatal("unsupported game was cached as a table") } } func TestMachineAutobetCountdownAndStop(t *testing.T) { m := NewManager(1) cfg := MachineConfig{ID: "slots", Game: GameSlots, SpinTicks: 1} m.JoinMachine(1, cfg, "alice") w := &testWallet{credits: 100} if events := m.SetMachineBet(1, "slots", "alice", 10, w); len(events) == 0 { t.Fatalf("setting a bet did not start a spin: %+v", events) } for m.MachineBusy("alice") { m.Tick() } if events := m.EnableMachineAutoBet(1, "slots", "alice", w); len(events) != 0 { t.Fatalf("enabling autobet emitted events before countdown: %+v", events) } for i, want := range []string{"Autobet enabled!", `Type "stop" to stop playing`, "Betting... 10 credits."} { events := m.Tick() if len(events) == 0 || eventText(events) != want { t.Fatalf("tick %d message=%q events=%+v, want %q", i+1, eventText(events), events, want) } } if !m.MachineBusy("alice") { t.Fatal("autobet did not start a spin after countdown") } if events, stopped := m.StopMachineAutoBet(1, "slots", "alice"); !stopped || len(events) != 1 { t.Fatalf("stop autobet events=%+v stopped=%v", events, stopped) } if _, playing := m.MachineFor("alice"); !playing { t.Fatal("stopping autobet removed the player from the machine") } } func eventText(events []Event) string { for _, event := range events { if event.PrivateMessage != "" { return event.PrivateMessage } if event.Message != "" { return event.Message } } return "" }