Files
Pete/internal/games/uno/uno.go
prosolis 79c857023f games: a table of bots you have to beat to the last card
UNO, played for chips. You stake once, sit down against one to three bots,
and going out first pays the table: 2.2x heads up, 3.6x against a full house.
Anybody else going out first takes the stake. The table size is the tier,
because it is the only dial UNO has.

The bots move inside ApplyMove. A game with opponents is normally where you
reach for a socket, and the plan says solo UNO must not — so one request plays
your move and every bot turn behind it, and hands back the whole lap as a
script the felt plays in order.

The RNG is in the state rather than an argument to it: the bots choose and a
spent deck reshuffles, so the engine needs randomness mid-game, and there is no
generator alive across requests to pass in. The seed rides in the state and each
step derives its own. The game still replays exactly as it fell.

The zero value of Color is Wild, and that is the whole point of it: a wild
played with the colour field missing from the JSON must be refused, not
quietly played as a red one. It was red for an hour.

The browser never sees a bot's card — not the deck, not a hand, not the face of
a card a bot drew, which is most of the deck. Seats cross the wire as a name and
a count.

The multiples are measured, not guessed: playing the first legal card you hold
wins 43/32/27% of the time against these bots, so the tiers price that to lose
about 8% a game and leave good play worth roughly the house's edge.

PeteFX.flyNode is the throw with the chip taken out of it, so a card can be
thrown across the felt the same way. fly() is now that with a chip in it.

Not yet driven in a browser, which in this room means not yet finished.

Claude-Session: https://claude.ai/code/session_013M5nD7PgUboJXoDcYHzpuJ
2026-07-14 07:07:17 -07:00

811 lines
25 KiB
Go

// Package uno is a pure UNO engine, played for chips against bots.
//
// Same seam as the other four tables: ApplyMove(state, move) (state, events,
// error), where an error means the move was illegal and nothing else. No HTTP,
// no timers, no sockets, no player names off the wire. The state is a plain
// value, so a game survives a redeploy and replays from its seed.
//
// Two things make UNO different from the tables already on the felt.
//
// The bots move inside ApplyMove. A turn-based game against opponents is
// normally where you reach for a socket, and the plan says solo UNO must not:
// so one call from the browser plays the player's move *and* every bot turn that
// follows it, and hands back the whole run as events. The table animates them in
// order. The browser is never waiting on the server to think of something.
//
// The RNG is in the state, not an argument. The bots make choices and a spent
// deck gets reshuffled, so the engine needs randomness mid-game — but a reducer
// that takes an rng is a reducer whose caller has to keep one alive across
// requests, and there isn't one: every move is a fresh process for all it knows.
// So the seed rides in the state (which never leaves the server; the deck is in
// there too) and each step derives its own generator from seed and step count.
// Value in, value out, and the game still replays exactly as it was dealt.
package uno
import (
"errors"
"math"
"math/rand/v2"
)
// Errors an illegal move can produce.
var (
ErrGameOver = errors.New("uno: the game is already over")
ErrNotYourTurn = errors.New("uno: it isn't your turn")
ErrNoSuchCard = errors.New("uno: you don't have that card")
ErrCantPlay = errors.New("uno: that card can't go on this one")
ErrNeedColor = errors.New("uno: pick a colour for the wild")
ErrCantPass = errors.New("uno: you can only pass on a card you just drew")
ErrMustPlayNow = errors.New("uno: play the card you drew, or pass")
ErrUnknownMove = errors.New("uno: unknown move")
ErrBadBet = errors.New("uno: bet must be positive")
ErrUnknownTier = errors.New("uno: no such tier")
)
// You are always seat zero. The bots are the seats after you.
const You = 0
// HandSize is the deal. Seven each, as printed on the box.
const HandSize = 7
// Color is a card's colour. Wild has none until it's played.
//
// Wild is deliberately the zero value. A wild played with no colour named is the
// one move in this game that must never be allowed to mean something, and a
// browser that leaves `color` out of the JSON sends a zero — so the zero has to
// be "no colour", not red. It was red for about an hour, and a wild with the
// field missing quietly went down as a red one.
type Color uint8
const (
Wild Color = iota
Red
Blue
Yellow
Green
)
var colorNames = [5]string{"wild", "red", "blue", "yellow", "green"}
func (c Color) String() string {
if c > Green {
return "?"
}
return colorNames[c]
}
// Playable reports whether a colour is one a wild may name — Wild itself isn't.
func (c Color) Playable() bool { return c >= Red && c <= Green }
// Value is what's printed on the face.
type Value uint8
const (
Zero Value = iota
One
Two
Three
Four
Five
Six
Seven
Eight
Nine
Skip
Reverse
DrawTwo
WildCard
WildDrawFour
)
var valueNames = [15]string{"0", "1", "2", "3", "4", "5", "6", "7", "8", "9",
"skip", "reverse", "+2", "wild", "+4"}
func (v Value) String() string {
if v > WildDrawFour {
return "?"
}
return valueNames[v]
}
// Action reports whether a card does something beyond being a number.
func (v Value) Action() bool { return v >= Skip }
// Card is one card. Short JSON keys: a hand of these crosses the wire on every
// poll, and a state holds all 108.
type Card struct {
Color Color `json:"c"`
Value Value `json:"v"`
}
// IsWild reports whether the card has no colour of its own.
func (c Card) IsWild() bool { return c.Value == WildCard || c.Value == WildDrawFour }
// CanPlayOn is the whole rule of UNO: match the colour in play, or match the
// face, or be a wild. Note it takes the colour *in play* rather than the top
// card's own colour — after a wild those are different, and the one that counts
// is the colour that was named.
func (c Card) CanPlayOn(top Card, topColor Color) bool {
if c.IsWild() {
return true
}
return c.Color == topColor || c.Value == top.Value
}
// NewDeck builds the 108: one zero and two each of 1-9, skip, reverse and +2 in
// every colour, plus four wilds and four wild draw fours. Unshuffled — Deal
// shuffles, and a test wants the fixed order.
func NewDeck() []Card {
d := make([]Card, 0, 108)
for _, col := range []Color{Red, Blue, Yellow, Green} {
d = append(d, Card{col, Zero})
for v := One; v <= DrawTwo; v++ {
d = append(d, Card{col, v}, Card{col, v})
}
}
for i := 0; i < 4; i++ {
d = append(d, Card{Wild, WildCard}, Card{Wild, WildDrawFour})
}
return d
}
// Tier is a table, and the table size *is* the difficulty. More bots is a longer
// shot — three of them going out before you is three ways to lose — so it pays
// more. This is the tier dial every other game here has, pointed at the one knob
// UNO actually has.
type Tier struct {
Slug string `json:"slug"`
Name string `json:"name"`
Bots int `json:"bots"`
Base float64 `json:"base"` // what going out first pays, before the rake
Blurb string `json:"blurb"`
}
// Tiers are the three tables.
//
// The multiples are not guesses. A player who simply plays the first legal card
// they hold — which is a real strategy, and a bad one — goes out first 43% of
// the time heads up, 32% at three seats and 27% at four. These pay a little
// under what that costs, so bad play loses slowly and good play (holding the
// wilds, dumping the colour you're long in, counting what a bot picked up) is
// worth roughly the house's edge. That is the game being about something.
var Tiers = []Tier{
{Slug: "duel", Name: "Duel", Bots: 1, Base: 2.2,
Blurb: "One bot, head to head. A reverse is a skip with two at the table."},
{Slug: "table", Name: "Table", Bots: 2, Base: 2.9,
Blurb: "Two bots. Twice the +4s pointed at you."},
{Slug: "full", Name: "Full House", Bots: 3, Base: 3.6,
Blurb: "Three bots, and any of them going out first takes your stake."},
}
// TierBySlug finds a tier by the name the browser sent.
func TierBySlug(slug string) (Tier, error) {
for _, t := range Tiers {
if t.Slug == slug {
return t, nil
}
}
return Tier{}, ErrUnknownTier
}
// Phase is where the game is.
type Phase string
const (
PhasePlay Phase = "play" // your turn, play or draw
PhaseDrawn Phase = "drawn" // you drew a card you can play: play it or pass
PhaseDone Phase = "done"
)
// Outcome is how it ended.
type Outcome string
const (
OutcomeNone Outcome = ""
OutcomeWon Outcome = "won" // you went out first
OutcomeLost Outcome = "lost" // a bot did
OutcomeStuck Outcome = "stuck" // nobody can move and there are no cards left
)
// Won reports whether this outcome pays.
func (o Outcome) Won() bool { return o == OutcomeWon }
// State is one game. The bots' hands and the deck are in here, which is exactly
// why this value never crosses the wire — the browser gets counts instead.
type State struct {
Tier Tier `json:"tier"`
Hands [][]Card `json:"hands"` // seat 0 is you; the rest are bots
Bots []string `json:"bots"` // their names, one per bot seat (seat i is Bots[i-1])
Deck []Card `json:"deck"`
Discard []Card `json:"discard"` // the top card is the last one
Color Color `json:"color"` // the colour in play, which a wild renames
Turn int `json:"turn"`
Dir int `json:"dir"` // +1 clockwise, -1 after a reverse
Seed1 uint64 `json:"seed1"`
Seed2 uint64 `json:"seed2"`
Step uint64 `json:"step"` // how many moves have been applied; the rng's other half
RakePct float64 `json:"rake_pct"`
Bet int64 `json:"bet"`
Phase Phase `json:"phase"`
Outcome Outcome `json:"outcome"`
Payout int64 `json:"payout"`
Rake int64 `json:"rake"`
}
// Event is something the table animates. The bots' turns arrive as a run of
// these on the back of the player's own move, and the felt plays them in order.
type Event struct {
Kind string `json:"kind"` // see below
Seat int `json:"seat"` // who it happened to
Card *Card `json:"card,omitempty"` // the card played, or the one *you* drew
Color Color `json:"color,omitempty"` // the colour now in play, on a wild
N int `json:"n,omitempty"` // how many cards were drawn
Left int `json:"left,omitempty"` // cards left in that seat's hand afterwards
Text string `json:"text,omitempty"`
}
// The kinds an Event comes in.
//
// deal the hands are dealt and the first card turned over
// play a card goes on the pile
// wild the colour was named (rides with the play it belongs to)
// draw cards come off the deck. A bot's are face down: Card is nil.
// forced the same, but not by choice — a +2 or a +4 landed on them
// pass the turn moves on with nothing played
// skip a seat loses its turn
// reverse the direction flips
// uno a hand is down to one card
// reshuffle the discard goes back under
// settle it's over
const (
EvDeal = "deal"
EvPlay = "play"
EvDraw = "draw"
EvForced = "forced"
EvPass = "pass"
EvSkip = "skip"
EvReverse = "reverse"
EvUno = "uno"
EvReshuffle = "reshuffle"
EvSettle = "settle"
)
// Move is what the player sends: play this card, take one off the deck, or —
// having taken one you can play — decline to play it.
type Move struct {
Kind string `json:"kind"` // "play" | "draw" | "pass"
Index int `json:"index"` // which card of your hand, for a play
Color Color `json:"color"` // the colour you name, for a wild
}
// Move kinds.
const (
MovePlay = "play"
MoveDraw = "draw"
MovePass = "pass"
)
// New deals a game: a shuffled deck, seven each, and a card turned over.
//
// The turned card is dealt until it's a number. The official rules have the
// first player eat a +2 that lands there, and turn a wild into a colour vote —
// both of which are a game that opens by doing something to you before you have
// touched it. A number card up top is the same game, minus the paperwork.
func New(bet int64, t Tier, rakePct float64, seed1, seed2 uint64) (State, []Event, error) {
if bet <= 0 {
return State{}, nil, ErrBadBet
}
if t.Bots < 1 {
return State{}, nil, ErrUnknownTier
}
rng := stepRNG(seed1, seed2, 0)
deck := NewDeck()
rng.Shuffle(len(deck), func(i, j int) { deck[i], deck[j] = deck[j], deck[i] })
s := State{
Tier: t, Deck: deck, Dir: 1, Turn: You,
Seed1: seed1, Seed2: seed2,
RakePct: rakePct, Bet: bet, Phase: PhasePlay,
Bots: botNames(t.Bots, rng),
}
seats := t.Bots + 1
s.Hands = make([][]Card, seats)
for i := range s.Hands {
s.Hands[i] = make([]Card, 0, HandSize)
}
for c := 0; c < HandSize; c++ {
for seat := 0; seat < seats; seat++ {
card, _ := s.pop()
s.Hands[seat] = append(s.Hands[seat], card)
}
}
// Turn cards over until one of them is a plain number.
for {
card, ok := s.pop()
if !ok {
return State{}, nil, errors.New("uno: deck ran out on the deal") // 108 cards; unreachable
}
if card.Value.Action() {
s.Discard = append(s.Discard, card) // it stays buried, out of play
continue
}
s.Discard = append(s.Discard, card)
s.Color = card.Color
break
}
return s, []Event{{Kind: EvDeal, Card: s.topPtr(), Color: s.Color}}, nil
}
// ApplyMove is the engine. Your move goes in; your move, and every bot turn it
// hands off to, comes back out. An error means the move was illegal and the
// caller's state is untouched.
func ApplyMove(s State, m Move) (State, []Event, error) {
if s.Phase == PhaseDone {
return s, nil, ErrGameOver
}
if s.Turn != You {
// Can't happen through this door — ApplyMove always runs the bots out
// before it returns — but a state restored from a row that predates a bug
// shouldn't wedge the player, it should say what's wrong.
return s, nil, ErrNotYourTurn
}
next := s.clone()
next.Step++
rng := stepRNG(next.Seed1, next.Seed2, next.Step)
var evs []Event
var err error
switch m.Kind {
case MovePlay:
evs, err = next.playerPlays(m, rng)
case MoveDraw:
evs, err = next.playerDraws(rng)
case MovePass:
evs, err = next.playerPasses()
default:
return s, nil, ErrUnknownMove
}
if err != nil {
return s, nil, err // the caller's state, untouched
}
// The bots take their turns on the back of yours, and the whole run comes
// back as one script. This is the reason solo UNO needs no socket.
next.runBots(&evs, rng)
return next, evs, nil
}
// playerPlays puts one of your cards on the pile.
func (s *State) playerPlays(m Move, rng *rand.Rand) ([]Event, error) {
hand := s.Hands[You]
if m.Index < 0 || m.Index >= len(hand) {
return nil, ErrNoSuchCard
}
// Having drawn a playable card, the only card you may play is that one. Being
// allowed to draw and *then* play something else would make drawing a free
// look at the deck with no cost attached.
if s.Phase == PhaseDrawn && m.Index != len(hand)-1 {
return nil, ErrMustPlayNow
}
card := hand[m.Index]
if !card.CanPlayOn(s.top(), s.Color) {
return nil, ErrCantPlay
}
if card.IsWild() && !m.Color.Playable() {
return nil, ErrNeedColor
}
s.Hands[You] = append(hand[:m.Index:m.Index], hand[m.Index+1:]...)
var evs []Event
s.discard(You, card, m.Color, &evs)
s.after(You, card, &evs, rng)
return evs, nil
}
// playerDraws takes one off the deck. If it can be played you get the choice —
// that's PhaseDrawn, and it's the only place the turn pauses mid-move. If it
// can't, the turn passes on the spot: there is nothing to decide.
func (s *State) playerDraws(rng *rand.Rand) ([]Event, error) {
if s.Phase == PhaseDrawn {
return nil, ErrMustPlayNow // you already drew; play it or pass
}
var evs []Event
drawn := s.deal(You, 1, false, &evs, rng)
if len(drawn) == 1 && drawn[0].CanPlayOn(s.top(), s.Color) {
s.Phase = PhaseDrawn
return evs, nil
}
evs = append(evs, Event{Kind: EvPass, Seat: You})
s.advance(1)
return evs, nil
}
// playerPasses declines the card you just drew.
func (s *State) playerPasses() ([]Event, error) {
if s.Phase != PhaseDrawn {
return nil, ErrCantPass
}
s.Phase = PhasePlay
s.advance(1)
return []Event{{Kind: EvPass, Seat: You}}, nil
}
// runBots plays every bot turn between you and your next one. It stops the
// moment the game is over or the turn comes back round.
func (s *State) runBots(evs *[]Event, rng *rand.Rand) {
// A bot that can neither play nor draw passes, and if every seat does that in
// a row the game is stuck: the deck is spent and the discard has nothing under
// its top card to become a new one. Rare, but a game that can't end is worse
// than one that ends badly, so it ends — see stuck().
passes := 0
for s.Phase != PhaseDone && s.Turn != You {
if s.botTurn(s.Turn, evs, rng) {
passes++
if passes > len(s.Hands) {
s.stuck(evs)
return
}
continue
}
passes = 0
}
}
// botTurn plays one bot's turn. It reports whether the bot passed with nothing.
func (s *State) botTurn(seat int, evs *[]Event, rng *rand.Rand) (passed bool) {
card, idx := botPick(s.Hands[seat], s.top(), s.Color, s.minOpponent(seat), rng)
if idx < 0 {
// Nothing playable: draw one, and play it if it happens to go.
drawn := s.deal(seat, 1, false, evs, rng)
if len(drawn) == 1 && drawn[0].CanPlayOn(s.top(), s.Color) {
card, idx = drawn[0], len(s.Hands[seat])-1
} else {
*evs = append(*evs, Event{Kind: EvPass, Seat: seat})
s.advance(1)
return len(drawn) == 0 // a bot that couldn't even draw is a stuck table
}
}
hand := s.Hands[seat]
s.Hands[seat] = append(hand[:idx:idx], hand[idx+1:]...)
color := card.Color
if card.IsWild() {
color = botColor(s.Hands[seat], rng)
}
s.discard(seat, card, color, evs)
s.after(seat, card, evs, rng)
return false
}
// discard puts a card on the pile and names the colour now in play.
//
// A wild is stamped with the colour it was played as, so the pile shows what was
// called rather than a black card and a note beside it. That stamp is undone if
// the card ever comes back out — see reshuffle, which would otherwise bleed four
// extra reds into the deck.
func (s *State) discard(seat int, card Card, color Color, evs *[]Event) {
if card.IsWild() {
s.Color = color
card.Color = color
} else {
s.Color = card.Color
}
s.Discard = append(s.Discard, card)
e := Event{Kind: EvPlay, Seat: seat, Card: &card, Color: s.Color, Left: len(s.Hands[seat])}
*evs = append(*evs, e)
if len(s.Hands[seat]) == 1 {
*evs = append(*evs, Event{Kind: EvUno, Seat: seat})
}
}
// after resolves what the card just played does, and moves the turn on. It is
// the one place the rules of skip, reverse and the draw cards live.
func (s *State) after(seat int, card Card, evs *[]Event, rng *rand.Rand) {
if len(s.Hands[seat]) == 0 {
s.settle(seat, evs)
return
}
s.Phase = PhasePlay
switch card.Value {
case Skip:
victim := s.seatAt(1)
*evs = append(*evs, Event{Kind: EvSkip, Seat: victim})
s.advance(2)
case Reverse:
// Two at the table and a reverse has nobody to hand the turn back to, so it
// is a skip — which, with two players, means you go again.
if len(s.Hands) == 2 {
*evs = append(*evs, Event{Kind: EvSkip, Seat: s.seatAt(1)})
s.advance(2)
return
}
s.Dir = -s.Dir
*evs = append(*evs, Event{Kind: EvReverse, Seat: seat})
s.advance(1)
case DrawTwo:
s.punish(s.seatAt(1), 2, evs, rng)
case WildDrawFour:
s.punish(s.seatAt(1), 4, evs, rng)
default:
s.advance(1)
}
}
// punish makes the next seat eat a draw card and lose its turn. No stacking: a
// +2 played onto a +2 is a house rule, and the one this table plays is the one
// on the box.
func (s *State) punish(victim, n int, evs *[]Event, rng *rand.Rand) {
s.deal(victim, n, true, evs, rng)
*evs = append(*evs, Event{Kind: EvSkip, Seat: victim})
s.advance(2)
}
// deal gives a seat n cards, reshuffling the discard back under the deck if it
// runs dry. The cards it hands back are the ones actually drawn — which can be
// fewer than asked for, when there is nothing left anywhere to draw.
//
// A bot's cards go face down: the event carries the count, never the card. The
// only hand whose faces cross the wire is yours.
func (s *State) deal(seat, n int, forced bool, evs *[]Event, rng *rand.Rand) []Card {
got := make([]Card, 0, n)
for i := 0; i < n; i++ {
if len(s.Deck) == 0 && !s.reshuffle(evs, rng) {
break
}
c, ok := s.pop()
if !ok {
break
}
s.Hands[seat] = append(s.Hands[seat], c)
got = append(got, c)
}
if len(got) == 0 {
return got
}
kind := EvDraw
if forced {
kind = EvForced
}
e := Event{Kind: kind, Seat: seat, N: len(got), Left: len(s.Hands[seat])}
if seat == You && len(got) == 1 {
c := got[0]
e.Card = &c // your own card, and only yours, comes face up
}
*evs = append(*evs, e)
return got
}
// reshuffle turns the discard back into a deck, keeping the card in play on top
// of the pile. It reports whether there was anything to reshuffle.
func (s *State) reshuffle(evs *[]Event, rng *rand.Rand) bool {
if len(s.Discard) < 2 {
return false // nothing under the top card: the table is out of cards
}
top := s.Discard[len(s.Discard)-1]
rest := append([]Card(nil), s.Discard[:len(s.Discard)-1]...)
rng.Shuffle(len(rest), func(i, j int) { rest[i], rest[j] = rest[j], rest[i] })
// A wild goes back in as a wild. It was played as a colour, and leaving that
// colour stamped on it would quietly bleed four extra reds into the deck.
for i := range rest {
if rest[i].Value == WildCard || rest[i].Value == WildDrawFour {
rest[i].Color = Wild
}
}
s.Deck = rest
s.Discard = []Card{top}
*evs = append(*evs, Event{Kind: EvReshuffle, N: len(rest)})
return true
}
// settle ends the game. Going out first pays the tier; anyone else going out
// takes the stake. The rake, as everywhere in this casino, comes out of the
// winnings and never out of the stake.
func (s *State) settle(winner int, evs *[]Event) {
s.Phase = PhaseDone
if winner == You {
s.Outcome = OutcomeWon
s.Payout = s.Pays()
s.Rake = s.rakeNow()
} else {
s.Outcome = OutcomeLost
s.Payout = 0
}
*evs = append(*evs, Event{Kind: EvSettle, Seat: winner, Text: string(s.Outcome)})
}
// stuck ends a game nobody can move in: the deck is spent, the discard is one
// card deep, and every seat has passed. The shortest hand takes it — and a tie
// is not a win, because a win here has to be somebody actually going out.
func (s *State) stuck(evs *[]Event) {
best, tied := 0, false
for seat := range s.Hands {
switch {
case len(s.Hands[seat]) < len(s.Hands[best]):
best, tied = seat, false
case seat != best && len(s.Hands[seat]) == len(s.Hands[best]):
tied = true
}
}
s.Phase = PhaseDone
if best == You && !tied {
s.Outcome = OutcomeWon
s.Payout = s.Pays()
s.Rake = s.rakeNow()
} else {
s.Outcome = OutcomeStuck
s.Payout = 0
}
*evs = append(*evs, Event{Kind: EvSettle, Seat: best, Text: string(s.Outcome)})
}
// Pays is what going out *right now* would put back on the player's stack: the
// stake, plus the winnings, less the house's cut of the winnings.
//
// It exists because the felt quotes this number while the game is still running,
// and settle() is the only other thing that works it out. Hangman learned this
// the hard way: two sums drift, and the table ends up advertising a payout it
// doesn't honour. So settle calls this rather than doing it again.
func (s State) Pays() int64 {
total := int64(math.Floor(float64(s.Bet) * s.Tier.Base))
if total < s.Bet {
total = s.Bet
}
profit := total - s.Bet
if profit > 0 {
profit -= s.rakeOn(profit)
}
return s.Bet + profit
}
// rakeNow is the other half of what Pays works out: the house's cut of a win
// taken right now.
func (s State) rakeNow() int64 {
total := int64(math.Floor(float64(s.Bet) * s.Tier.Base))
if total <= s.Bet {
return 0
}
return s.rakeOn(total - s.Bet)
}
func (s State) rakeOn(profit int64) int64 {
rake := int64(math.Floor(float64(profit) * s.RakePct))
if rake < 0 {
return 0
}
return rake
}
// Net is what the game did to the player's stack.
func (s State) Net() int64 {
if s.Phase != PhaseDone {
return 0
}
return s.Payout - s.Bet
}
// Playable reports which cards of your hand can legally go on the pile. The
// browser lights these up: being shown what you can play is the game teaching
// you, and the server still decides every move regardless.
//
// While you're sitting on a card you just drew, that card is the only one you
// may play — so it is the only one that lights up.
func (s State) Playable() []int {
if s.Phase == PhaseDone || s.Turn != You {
return nil
}
hand := s.Hands[You]
if s.Phase == PhaseDrawn {
if len(hand) > 0 && hand[len(hand)-1].CanPlayOn(s.top(), s.Color) {
return []int{len(hand) - 1}
}
return nil
}
var out []int
for i, c := range hand {
if c.CanPlayOn(s.top(), s.Color) {
out = append(out, i)
}
}
return out
}
// Counts is how many cards each seat holds — what the browser gets instead of
// the bots' hands.
func (s State) Counts() []int {
out := make([]int, len(s.Hands))
for i := range s.Hands {
out[i] = len(s.Hands[i])
}
return out
}
// Top is the card in play.
func (s State) Top() Card { return s.top() }
// Left is how many cards are still in the deck.
func (s State) Left() int { return len(s.Deck) }
// ---- the plumbing ---------------------------------------------------------
func (s State) top() Card {
if len(s.Discard) == 0 {
return Card{}
}
return s.Discard[len(s.Discard)-1]
}
func (s State) topPtr() *Card {
c := s.top()
return &c
}
// pop takes the next card off the deck.
func (s *State) pop() (Card, bool) {
if len(s.Deck) == 0 {
return Card{}, false
}
c := s.Deck[0]
s.Deck = s.Deck[1:]
return c, true
}
// seatAt is the seat n places round from the one whose turn it is.
func (s State) seatAt(n int) int {
seats := len(s.Hands)
return ((s.Turn+s.Dir*n)%seats + seats) % seats
}
// advance moves the turn on n places.
func (s *State) advance(n int) { s.Turn = s.seatAt(n) }
// minOpponent is the smallest hand at the table that isn't this seat's — how
// close the bot is to being beaten, which is the only thing it plays around.
func (s State) minOpponent(seat int) int {
min := -1
for i := range s.Hands {
if i == seat {
continue
}
if min < 0 || len(s.Hands[i]) < min {
min = len(s.Hands[i])
}
}
return min
}
// clone deep-copies everything a move can touch, so a derived state shares no
// backing array with the one it came from.
func (s State) clone() State {
hands := make([][]Card, len(s.Hands))
for i, h := range s.Hands {
hands[i] = append([]Card(nil), h...)
}
s.Hands = hands
s.Deck = append([]Card(nil), s.Deck...)
s.Discard = append([]Card(nil), s.Discard...)
s.Bots = append([]string(nil), s.Bots...)
return s
}
// stepRNG is the generator for one step of the game. The seed is the game's;
// the step number is what stops every move from replaying the same numbers.
// Mixing with the golden ratio's odd 64-bit constant keeps consecutive steps
// from producing streams that share a bit pattern.
func stepRNG(seed1, seed2, step uint64) *rand.Rand {
return rand.New(rand.NewPCG(seed1, seed2^(step*0x9E3779B97F4A7C15)))
}