The first two pieces of games.parodia.dev, both pure: no HTTP, no timers, no euros, nothing that knows a player's name. cards/ is the shared deck gogobee never had — blackjack carried its own, UNO carried another, hold'em leaned on a third-party one. The RNG is threaded rather than the package global, so a hand is reproducible from its seed. That's what makes the engine testable, and what lets a disputed hand be dealt again exactly as it fell. blackjack/ is ApplyMove(state, move) -> (state, events, error), where an error means the move was illegal and nothing else. gogobee's engine *was* the message sender, so its errors meant "the send failed"; there was no seam to test against. State is a plain value, so a hand survives a redeploy. House terms match the Matrix table — six decks, 3:2, dealer hits soft 17 — plus a 5% rake. The rake comes off winnings only: a push returns the stake untouched and a loss is never charged for the privilege.
103 lines
2.2 KiB
Go
103 lines
2.2 KiB
Go
package cards
|
|
|
|
import "testing"
|
|
|
|
func TestNewDeck_IsAFullShoe(t *testing.T) {
|
|
d := NewDeck(6)
|
|
if len(d) != 312 {
|
|
t.Fatalf("six decks hold %d cards, want 312", len(d))
|
|
}
|
|
seen := map[Card]int{}
|
|
for _, c := range d {
|
|
seen[c]++
|
|
}
|
|
if len(seen) != 52 {
|
|
t.Fatalf("%d distinct cards, want 52", len(seen))
|
|
}
|
|
for c, n := range seen {
|
|
if n != 6 {
|
|
t.Fatalf("%s appears %d times in a six-deck shoe, want 6", c, n)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestNewDeck_ClampsToAtLeastOne(t *testing.T) {
|
|
if len(NewDeck(0)) != 52 {
|
|
t.Fatal("a zero-deck shoe should still hold one deck")
|
|
}
|
|
}
|
|
|
|
func TestShuffle_SameSeedSameOrder(t *testing.T) {
|
|
a, b := NewDeck(1), NewDeck(1)
|
|
a.Shuffle(NewRNG(99, 1))
|
|
b.Shuffle(NewRNG(99, 1))
|
|
for i := range a {
|
|
if a[i] != b[i] {
|
|
t.Fatalf("same seed diverged at %d: %s vs %s", i, a[i], b[i])
|
|
}
|
|
}
|
|
// And a different seed must not give the same order, or the RNG isn't wired up.
|
|
c := NewDeck(1)
|
|
c.Shuffle(NewRNG(100, 1))
|
|
same := true
|
|
for i := range a {
|
|
if a[i] != c[i] {
|
|
same = false
|
|
break
|
|
}
|
|
}
|
|
if same {
|
|
t.Fatal("a different seed produced an identical shuffle")
|
|
}
|
|
}
|
|
|
|
func TestShuffle_KeepsEveryCard(t *testing.T) {
|
|
d := NewDeck(1)
|
|
d.Shuffle(NewRNG(4, 4))
|
|
seen := map[Card]bool{}
|
|
for _, c := range d {
|
|
seen[c] = true
|
|
}
|
|
if len(d) != 52 || len(seen) != 52 {
|
|
t.Fatalf("shuffle lost cards: %d cards, %d distinct", len(d), len(seen))
|
|
}
|
|
}
|
|
|
|
func TestDraw_TakesFromTheTopAndRunsOut(t *testing.T) {
|
|
d := NewDeck(1)
|
|
top := d[0]
|
|
c, ok := d.Draw()
|
|
if !ok || c != top {
|
|
t.Fatalf("drew %s (ok=%v), want the top card %s", c, ok, top)
|
|
}
|
|
if len(d) != 51 {
|
|
t.Fatalf("deck has %d cards after one draw, want 51", len(d))
|
|
}
|
|
for len(d) > 0 {
|
|
d.Draw()
|
|
}
|
|
if _, ok := d.Draw(); ok {
|
|
t.Fatal("an empty deck kept dealing")
|
|
}
|
|
}
|
|
|
|
func TestCard_String(t *testing.T) {
|
|
tests := []struct {
|
|
card Card
|
|
want string
|
|
}{
|
|
{Card{Ace, Spades}, "A♠"},
|
|
{Card{10, Hearts}, "10♥"},
|
|
{Card{King, Clubs}, "K♣"},
|
|
{Card{Rank: 99, Suit: Spades}, "??"},
|
|
}
|
|
for _, tc := range tests {
|
|
if got := tc.card.String(); got != tc.want {
|
|
t.Errorf("String() = %q, want %q", got, tc.want)
|
|
}
|
|
}
|
|
if !(Card{Ace, Hearts}).Red() || (Card{Ace, Spades}).Red() {
|
|
t.Error("Red() disagrees about which suits are red")
|
|
}
|
|
}
|