// 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")
	ErrMustStack   = errors.New("uno: answer the stack with a draw card, or take it")
	ErrNoStack     = errors.New("uno: there's no stack to take")
	ErrNoCatch     = errors.New("uno: there's nobody to catch there")
	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

// The faces. The first fifteen are the ones on a normal box, and their numbers
// are load-bearing: a game in flight is a JSON blob of these integers, so the No
// Mercy faces are *appended*. Renumbering them would deal a live table a
// different card.
const (
	Zero Value = iota
	One
	Two
	Three
	Four
	Five
	Six
	Seven
	Eight
	Nine
	Skip
	Reverse
	DrawTwo
	WildCard
	WildDrawFour

	// No Mercy only, all of them.
	SkipAll      // skip everyone: you go again
	DrawFour     // a *coloured* +4, which the normal deck doesn't have
	DiscardAll   // play it, and every other card of its colour goes with it
	WildRevFour  // reverse, and the seat that lands next takes four
	WildDrawSix  // +6
	WildDrawTen  // +10
	WildRoulette // the next seat flips until your colour turns up, and keeps the lot
)

var valueNames = [22]string{"0", "1", "2", "3", "4", "5", "6", "7", "8", "9",
	"skip", "reverse", "+2", "wild", "+4",
	"skip all", "+4", "discard all", "rev +4", "+6", "+10", "roulette"}

func (v Value) String() string {
	if v > WildRoulette {
		return "?"
	}
	return valueNames[v]
}

// Action reports whether a card does something beyond being a number.
func (v Value) Action() bool { return v >= Skip }

// Wild reports whether the face has no colour of its own. Note DrawFour is *not*
// one: No Mercy prints a coloured +4, which is a different card from the wild +4
// sitting next to it in the same deck.
func (v Value) Wild() bool {
	switch v {
	case WildCard, WildDrawFour, WildRevFour, WildDrawSix, WildDrawTen, WildRoulette:
		return true
	}
	return false
}

// Draw is how many cards the face makes somebody take, and zero if it doesn't.
// It is also what makes a card stackable, so Roulette is deliberately zero: it
// hands over a random number of cards, and you cannot stack onto a number nobody
// knows yet.
func (v Value) Draw() int {
	switch v {
	case DrawTwo:
		return 2
	case DrawFour, WildDrawFour, WildRevFour:
		return 4
	case WildDrawSix:
		return 6
	case WildDrawTen:
		return 10
	}
	return 0
}

// 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.Wild() }

// 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.
// No Mercy rides on the same struct rather than a second one, because it is the
// tier that lands in the state and the payload — so a game carries which rules it
// is playing by, and cannot be reloaded into the other set.
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"`
	NoMercy bool    `json:"no_mercy"`
}

// Deck is the deck this tier plays with.
func (t Tier) Deck() []Card {
	if t.NoMercy {
		return NewNoMercyDeck()
	}
	return NewDeck()
}

// 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.
// Re-measured 2026-07-14, and they moved: the naive strategy now wins 40.3% /
// 29.2% / 23.3%, not the 43 / 32 / 27 these were originally priced off. The bots
// got better at some point after the multiples were set and nobody re-ran the
// measurement, so Table and Full House had quietly been charging an 18–19% edge
// instead of the 8% they were meant to. The numbers below are the honest ones.
//
// This is exactly the drift TestTheMultiplesAreStillPriced now exists to stop.
//
// Re-measured again when the UNO call went in (call.go): 40.1% / 28.5% / 23.1%,
// which is the same game. That is the point — the naive player *calls*, because
// calling is a button and not a strategy, so the rule costs them nothing and
// these multiples stand. What the rule adds is upside for a player who watches
// the counts and catches a quiet bot, which is the good play these tiers are
// meant to leave room for.
var Tiers = []Tier{
	{Slug: "duel", Name: "Duel", Bots: 1, Base: 2.4,
		Blurb: "One bot, head to head. A reverse is a skip with two at the table."},
	{Slug: "table", Name: "Table", Bots: 2, Base: 3.3,
		Blurb: "Two bots. Twice the +4s pointed at you."},
	{Slug: "full", Name: "Full House", Bots: 3, Base: 4.1,
		Blurb: "Three bots, and any of them going out first takes your stake."},
}

// NoMercyTiers are the same three tables playing the other rules.
//
// The multiples are measured, not guessed, and they are *not* the normal ones —
// the naive strategy (play the first legal card; take a stack you can't answer)
// wins 45.6% / 31.8% / 27.4% here, against 40.1 / 28.5 / 23.1 on the normal deck.
//
// Which is to say: **No Mercy is easier than UNO**, at every table size, and so
// it pays less. That reads backwards until you see why. The mercy rule kills
// *bots* — it does not care whose hand hits twenty-five — and every bot it buries
// is one fewer seat that can beat you to the last card. Three opponents burying
// each other is a game you win by outliving, and the deck that was built to be
// merciless turns out to be merciless mostly to the table.
//
// So a nastier game pays a smaller multiple, which is the correct answer and a
// slightly funny one. TestTheMultiplesAreStillPriced is what keeps it honest:
// change the bots, the deck or a rule, and it fails until these are measured
// again. It is the test the normal tiers never had, which is how they drifted.
var NoMercyTiers = []Tier{
	{Slug: "nm-duel", Name: "No Mercy Duel", Bots: 1, Base: 2.0, NoMercy: true,
		Blurb: "One bot, 168 cards. Stack the draws or eat them."},
	{Slug: "nm-table", Name: "No Mercy Table", Bots: 2, Base: 3.1, NoMercy: true,
		Blurb: "Two bots. A +10 answered twice is somebody's whole hand."},
	// Re-priced 2026-07-14 with the call rule in: 3.8 was paying a *negative* house
	// edge here (-0.2%), which is the house paying you to sit down. The naive win
	// rate at this table is 27.4%, not the 25.3% it was priced off — three bots
	// burying each other is even better for you than the last measurement caught.
	{Slug: "nm-full", Name: "No Mercy Full House", Bots: 3, Base: 3.5, NoMercy: true,
		Blurb: "Three bots. Twenty-five cards and you're out of the game."},
}

// AllTiers is every table in the room, both dials.
func AllTiers() []Tier {
	return append(append([]Tier(nil), Tiers...), NoMercyTiers...)
}

// TierBySlug finds a tier by the name the browser sent, across both rule sets.
func TierBySlug(slug string) (Tier, error) {
	for _, t := range AllTiers() {
		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
	PhaseStack Phase = "stack" // No Mercy: a draw card is pointed at you — answer it or take it
	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

	// No Mercy only. Out is the seats the mercy rule has killed, and it is what
	// the turn order steps over — a dead seat is skipped, not merely empty.
	// Pending is the bill a stack of draw cards has run up: whoever stops
	// stacking pays it.
	Out     []bool `json:"out,omitempty"`
	Pending int    `json:"pending,omitempty"`

	// Called is who, holding one card, said so. It is only ever meaningful for a
	// seat on exactly one card — every other seat's entry is false and means
	// nothing — and it is what a catch is tested against. See call.go.
	Called []bool `json:"called,omitempty"`

	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"`            // cards left in that seat's hand afterwards
	By    int    `json:"by"`              // who caught them, on a catch. Seat zero is a real answer here, so never omitempty
	Text  string `json:"text,omitempty"`

	// Hand is *your* hand as it stands after this event, and it is only ever set
	// on an event that changed it. The table plays a lap of events back over
	// several seconds, and for all of them the hand on screen has to be the hand
	// you actually hold — a +4 that lands on you at the top of the lap must show
	// up in your fan as it lands, not when the lap ends. There is no leak here:
	// this is the one hand the browser is already entitled to see.
	Hand []Card `json:"hand,omitempty"`
}

// mine stamps the player's hand onto an event that just changed it. Events about
// a bot's hand carry nothing — the browser never learns those, and stamping is
// scoped to seat zero precisely so it can't start.
func (s *State) mine(e Event) Event {
	if e.Seat == You {
		e.Hand = append([]Card(nil), s.Hands[You]...)
	}
	return e
}

// 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, and its owner said so
//	caught    a seat went down to one card *quietly*, and somebody noticed: +2
//	miscall   you called a seat that had nothing to hide, and paid for it: +2
//	reshuffle the discard goes back under
//	settle    it's over
//
// And the No Mercy ones:
//
//	stack      a draw card is pointed at a seat: N is the bill so far
//	skipall    everybody else loses their turn
//	discard    a whole colour left a hand at once
//	roulette   a seat flipped N cards looking for a colour, and kept them
//	mercy      a seat hit 25 cards and is out of the game
const (
	EvDeal      = "deal"
	EvPlay      = "play"
	EvDraw      = "draw"
	EvForced    = "forced"
	EvPass      = "pass"
	EvSkip      = "skip"
	EvReverse   = "reverse"
	EvUno       = "uno"
	EvCaught    = "caught"
	EvMiscall   = "miscall"
	EvReshuffle = "reshuffle"
	EvSettle    = "settle"

	EvStack      = "stack"
	EvSkipAll    = "skipall"
	EvDiscardAll = "discard"
	EvRoulette   = "roulette"
	EvMercy      = "mercy"
)

// 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" | "take" | "catch"
	Index int    `json:"index"` // which card of your hand, for a play
	Color Color  `json:"color"` // the colour you name, for a wild
	Uno   bool   `json:"uno"`   // "…and UNO!", for a play that leaves you on one card
	Seat  int    `json:"seat"`  // whose silence you're calling, for a catch
}

// Move kinds. Take is No Mercy's: it is how you give in to a stack you can't
// answer, and it is a *decision*, so it gets a name of its own rather than being
// bolted onto draw.
//
// Catch is the other half of the UNO call — see call.go. It is a move you make
// out of turn order (it costs you nothing but the risk of being wrong), so it is
// a kind rather than a flag on the moves that do cost a turn.
const (
	MovePlay  = "play"
	MoveDraw  = "draw"
	MovePass  = "pass"
	MoveTake  = "take"
	MoveCatch = "catch"
)

// 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 := t.Deck()
	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.Out = make([]bool, seats)
	s.Called = make([]bool, seats)
	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{s.mine(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++
	next.ensureCalled()
	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()
	case MoveTake:
		evs, err = next.playerTakes(rng)
	case MoveCatch:
		evs, err = next.playerCatches(m, rng)
	default:
		return s, nil, ErrUnknownMove
	}
	if err != nil {
		return s, nil, err // the caller's state, untouched
	}

	// Before anybody moves: did you go down to one card without saying so? This is
	// the only window there is. The bots are about to take their turns, and a bot
	// that has played on is a bot that has stopped looking at your hand.
	next.botsCatch(&evs, rng)

	// 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.
	//
	// A catch is not a turn — it leaves the turn where it was, which is with you —
	// so this is a no-op after one, and that is the whole mechanism by which
	// catching a bot costs you nothing but the risk of being wrong.
	next.runBots(&evs, rng)

	// And if that left a table nobody can move at, it ends here rather than
	// handing back a turn that has nothing in it. See stalled().
	if next.Phase != PhaseDone && next.stalled() {
		next.stuck(&evs)
	}
	next.tidyCalls()
	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]

	// With a stack pointed at you, the only cards that exist are the ones that
	// answer it. Everything else in your hand is unplayable until the bill is
	// settled — by you, or by the seat you pass it to.
	if s.Phase == PhaseStack {
		if !card.CanStackOn(s.Color) {
			return nil, ErrMustStack
		}
	} else 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)
	// Whether you called is checked *after* the card has finished resolving, not
	// after it left your hand. No Mercy's "discard all" takes every card of a
	// colour with it, so it can drop you from six cards to one in a single play —
	// and the seat that gets there that way owes the call just the same.
	s.declare(You, m.Uno, &evs)
	return evs, nil
}

// playerDraws takes cards off the deck.
//
// The normal game takes one: if it can be played you get the choice — that's
// PhaseDrawn, the only place a turn pauses mid-move — and if it can't, the turn
// passes on the spot, because there is nothing to decide.
//
// No Mercy makes you draw *until* you can play. There is no drawing one card and
// shrugging, which is most of why hands there get big enough for the mercy rule
// to have something to kill. The card you end on is a card you must then play, so
// there is still nothing to decide — but the deck can be dry, and a hand can hit
// twenty-five on the way, and both of those end the drawing.
func (s *State) playerDraws(rng *rand.Rand) ([]Event, error) {
	if s.Phase == PhaseDrawn {
		return nil, ErrMustPlayNow // you already drew; play it or pass
	}
	if s.Phase == PhaseStack {
		return nil, ErrMustStack // answer it or take it; you cannot draw out of a stack
	}
	var evs []Event

	if !s.Tier.NoMercy {
		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
	}

	for {
		drawn := s.deal(You, 1, false, &evs, rng)
		if len(drawn) == 0 {
			break // the table has nothing left to draw
		}
		if s.mercy(You, &evs, rng) {
			return evs, nil // twenty-five cards, and you are out of the game
		}
		if 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
}

// playerTakes gives in to a stack: you take every card it has run up, and you
// lose your turn.
func (s *State) playerTakes(rng *rand.Rand) ([]Event, error) {
	if s.Phase != PhaseStack {
		return nil, ErrNoStack
	}
	var evs []Event
	s.absorb(You, &evs, rng)
	return evs, nil
}

// playerPasses declines the card you just drew.
//
// In No Mercy you may not: you drew until you found a card that plays, and that
// card is the price of having drawn. Passing there would make drawing a way to
// buy a look at the deck and put nothing down.
func (s *State) playerPasses() ([]Event, error) {
	if s.Phase != PhaseDrawn {
		return nil, ErrCantPass
	}
	if s.Tier.NoMercy {
		return nil, ErrMustPlayNow
	}
	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, the turn comes back round, or the table dies under
// it — a stalled table would otherwise pass the turn round and round forever
// without ever reaching you.
func (s *State) runBots(evs *[]Event, rng *rand.Rand) {
	for s.Phase != PhaseDone && s.Turn != You && !s.stalled() {
		s.botTurn(s.Turn, evs, rng)
	}
}

// botTurn plays one bot's turn.
func (s *State) botTurn(seat int, evs *[]Event, rng *rand.Rand) {
	// A stack pointed at this bot is not a turn, it is a bill. It answers with a
	// draw card if it holds one, and takes the lot if it doesn't.
	if s.Phase == PhaseStack {
		card, idx := botStack(s.Hands[seat], s.Color, rng)
		if idx < 0 {
			s.absorb(seat, evs, rng)
			return
		}
		s.botPlays(seat, card, idx, evs, rng)
		return
	}

	card, idx := botPick(s.Hands[seat], s.top(), s.Color, s.minOpponent(seat), rng)
	if idx < 0 {
		// Nothing playable: draw. The normal game draws one and shrugs; No Mercy
		// draws until something goes, which is what buries a bot as surely as it
		// buries you — the mercy rule cuts both ways, and a bot can die on the deck.
		for {
			drawn := s.deal(seat, 1, false, evs, rng)
			if len(drawn) != 1 {
				*evs = append(*evs, Event{Kind: EvPass, Seat: seat})
				s.advance(1)
				return
			}
			if s.Tier.NoMercy && s.mercy(seat, evs, rng) {
				return
			}
			if drawn[0].CanPlayOn(s.top(), s.Color) {
				card, idx = drawn[0], len(s.Hands[seat])-1
				break
			}
			if !s.Tier.NoMercy {
				*evs = append(*evs, Event{Kind: EvPass, Seat: seat})
				s.advance(1)
				return
			}
		}
	}
	s.botPlays(seat, card, idx, evs, rng)
}

// botPlays puts a bot's chosen card down and resolves it.
func (s *State) botPlays(seat int, card Card, idx int, evs *[]Event, rng *rand.Rand) {
	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)
		if card.Value == WildRoulette {
			// The roulette is not a card you play a colour *from*, it is a card you
			// point at somebody. So the bot names the colour it holds least of, which
			// is the one the deck is least likely to turn up quickly.
			color = botRouletteColor(s.Hands[seat], rng)
		}
	}
	s.discard(seat, card, color, evs)
	s.after(seat, card, evs, rng)
	// A bot that has just gone down to one card mostly remembers to say so. When it
	// doesn't, it says nothing at all — no event, no badge — and the only thing on
	// the table that gives it away is the count beside its fan reading "1 card".
	// Spotting that is the player's to do.
	s.declare(seat, !botForgets(rng), evs)
}

// stalled reports whether the table is dead: nothing left to draw anywhere, and
// not one seat holding a card that goes on the pile.
//
// This is the condition, tested directly. It used to be guessed at by counting
// how many bots had passed in a row, which could not work: runBots hands the
// turn back the moment it comes round to you, so the count never got as high as
// the number of seats, and your own empty-handed pass was never in it. The guard
// never fired once. A game that can't end is worse than one that ends badly —
// and worse than either, a live game you can't finish is chips you can't cash
// out, because the cage won't let you leave a hand half-played.
func (s State) stalled() bool {
	if s.Pending > 0 {
		return false // a stack is a move somebody still has to make: taking it
	}
	if len(s.Deck) > 0 || len(s.Discard) > 1 {
		return false // there is a card to draw, or a discard to make one out of
	}
	for _, seat := range s.alive() {
		for _, c := range s.Hands[seat] {
			if c.CanPlayOn(s.top(), s.Color) {
				return false
			}
		}
	}
	return true
}

// 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, s.mine(e))
	// No UNO event here any more. Going down to one card used to *be* the call,
	// which meant nobody at this table could ever fail to make it. Now the call is
	// a thing a seat does or doesn't do, and it is announced — or conspicuously not
	// — by the two functions that know whether it was made. See call.go.
}

// 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

	// A draw card. In No Mercy this doesn't land yet: it opens a stack, and the
	// seat it points at gets the choice of answering it. In the normal game it
	// lands where it always did.
	if n := card.Value.Draw(); n > 0 {
		if card.Value == WildRevFour {
			s.flip(seat, evs) // it reverses *first*: the seat it hits is the one after that
		}
		if s.Tier.NoMercy {
			s.Pending += n
			s.advance(1)
			s.Phase = PhaseStack
			*evs = append(*evs, Event{Kind: EvStack, Seat: s.Turn, N: s.Pending})
			return
		}
		s.punish(s.seatAt(1), n, evs, rng)
		return
	}

	switch card.Value {
	case Skip:
		victim := s.seatAt(1)
		*evs = append(*evs, Event{Kind: EvSkip, Seat: victim})
		s.advance(2)

	case Reverse:
		s.flip(seat, evs)

	case SkipAll:
		// Everyone else loses their turn, which means it comes straight back to the
		// seat that played it. The turn does not move at all.
		*evs = append(*evs, Event{Kind: EvSkipAll, Seat: seat})

	case DiscardAll:
		// Every other card of this colour goes down with it. That can empty the
		// hand, which is a win — and the reason this can't lean on the empty-hand
		// check at the top of the function, which already ran.
		s.discardAll(seat, card.Color, evs)
		if len(s.Hands[seat]) == 0 {
			s.settle(seat, evs)
			return
		}
		s.advance(1)

	case WildRoulette:
		s.roulette(s.seatAt(1), s.Color, evs, rng)

	default:
		s.advance(1)
	}
}

// flip turns the direction round — or, at a table of two, skips the only other
// player, because a reverse with nobody to hand the turn back to is a card that
// means you go again.
func (s *State) flip(seat int, evs *[]Event) {
	if len(s.alive()) == 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)
}

// punish makes the next seat eat a draw card and lose its turn. This is the
// normal game's rule: no stacking, because a +2 played onto a +2 is a house rule
// and the one on the box is the one this deck plays. No Mercy prints the stacking
// rule on its own box, and takes the other road out of after().
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 := s.drawCards(seat, n, evs, rng)
	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, s.mine(e))
	return got
}

// drawCards is deal without the announcement: cards come off the deck (with a
// reshuffle under them if it runs dry) and go into a hand, and nothing is said
// about it. deal wraps it to say the usual thing; a catch wraps it to say a
// different thing. It hands back what was actually drawn, which can be fewer
// cards than asked for when the table has nothing left to give.
func (s *State) drawCards(seat, n int, 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)
	}
	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) {
	live := s.alive()
	if len(live) == 0 {
		s.lose(evs) // can't happen: a mercy kill that empties the table settles first
		return
	}
	best, tied := live[0], false
	for _, seat := range live {
		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
	}
	// Under a stack, the only cards that light up are the ones that answer it.
	// Everything else in the hand is dead until the bill is paid.
	if s.Phase == PhaseStack {
		var out []int
		for i, c := range hand {
			if c.CanStackOn(s.Color) {
				out = append(out, i)
			}
		}
		return out
	}
	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 *live* places round from the one whose turn it is.
//
// A seat the mercy rule has killed is not there any more: it is stepped over, not
// landed on and skipped. So this counts living seats rather than doing the
// arithmetic on the index — which is the same thing in a normal game, where
// nobody is ever out, and the only thing that keeps a No Mercy table from
// handing the turn to a corpse.
func (s State) seatAt(n int) int {
	seats := len(s.Hands)
	at := s.Turn
	for moved := 0; moved < n; {
		at = ((at+s.Dir)%seats + seats) % seats
		if at == s.Turn && !s.live(at) {
			return at // nobody left alive to hand it to; the caller ends the game
		}
		if s.live(at) {
			moved++
		}
	}
	return at
}

// 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...)
	s.Out = append([]bool(nil), s.Out...)
	s.Called = append([]bool(nil), s.Called...)
	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)))
}
