games: UNO becomes a table you sit at, and the pot that pays whoever goes out first
Phase D backend: UNO is now a session like hold'em, not a single stake. You sit
with a buy-in stack, ante into a pot each hand, and leave with what's in front of
you. The engine lost its `You` constant and its measured multiples: ApplyMove
takes the acting seat, New takes a seat list, a Tier carries an ante instead of a
Base, and a hand settles by moving the pot to the winner (less rake, and never
when a bot takes it) rather than paying a multiple. A mercy kill puts a seat out
of the hand, not out of the game — the last one standing takes the pot.
The redaction moved to the web layer, where hold'em's already lives: the engine
now stamps every seat's hand onto its events, and viewUno/viewUnoEvents strip
everything that isn't the viewer's own. TestUnoViewNeverLeaksAnotherSeatsCards is
the wall. unoTable implements tableGame; /uno/{sit,move,leave,tables,stream,chat,
say} mirror hold'em, with stream/chat/say now shared game-agnostic handlers.
The frontend is not done: uno.js still calls the retired solo endpoint, so the
page renders but is not yet playable. All engine and web tests are green.
Claude-Session: https://claude.ai/code/session_013M5nD7PgUboJXoDcYHzpuJ
This commit is contained in:
@@ -109,10 +109,10 @@ func (s *State) absorb(seat int, evs *[]Event, rng *rand.Rand) {
|
||||
s.Pending = 0
|
||||
s.deal(seat, n, true, evs, rng)
|
||||
|
||||
// The seat can die paying the bill, and a mercy kill can end the whole game —
|
||||
// the player dying, or the last bot dying and leaving you alone at the table.
|
||||
// So the phase is only reset if there is still a game to have a phase.
|
||||
if s.mercy(seat, evs, rng) && s.Phase == PhaseDone {
|
||||
// The seat can die paying the bill, and a mercy kill can end the hand — the last
|
||||
// seat but one dying leaves somebody alone to take the pot. So the phase is only
|
||||
// reset if there is still a hand to have a phase.
|
||||
if s.mercy(seat, evs, rng) && !s.playing() {
|
||||
return
|
||||
}
|
||||
if !s.live(seat) {
|
||||
@@ -188,13 +188,13 @@ func (s *State) discardAll(seat int, color Color, evs *[]Event) int {
|
||||
}
|
||||
|
||||
// mercy checks a seat against the limit and, if it has crossed it, takes it out
|
||||
// of the game: its cards go back into the deck and it never plays again. It
|
||||
// of the hand: its cards go back into the deck and it plays no more this hand. It
|
||||
// reports whether the seat died.
|
||||
//
|
||||
// What that *means* depends on who it was. You dying is the game over — the
|
||||
// stake is gone whatever the bots do next. A bot dying leaves a table with one
|
||||
// fewer seat, and if it leaves you alone at it, you have won: everybody who could
|
||||
// have beaten you to the last card is dead.
|
||||
// A mercy kill is no longer game over for anyone — it is one seat out of the
|
||||
// current hand, human or bot alike, and the hand plays on among the living. When
|
||||
// it leaves exactly one seat alive, that seat has outlived the table and takes the
|
||||
// pot; whoever it is, they win it the same way going out first would.
|
||||
func (s *State) mercy(seat int, evs *[]Event, rng *rand.Rand) bool {
|
||||
if !s.Tier.NoMercy || !s.live(seat) || len(s.Hands[seat]) < MercyLimit {
|
||||
return false
|
||||
@@ -207,12 +207,8 @@ func (s *State) mercy(seat int, evs *[]Event, rng *rand.Rand) bool {
|
||||
s.Pending = 0 // a dead seat pays no bill, and leaves none behind
|
||||
*evs = append(*evs, s.mine(Event{Kind: EvMercy, Seat: seat, N: n, Left: 0}))
|
||||
|
||||
if seat == You {
|
||||
s.lose(evs)
|
||||
return true
|
||||
}
|
||||
if alive := s.alive(); len(alive) == 1 {
|
||||
s.settle(alive[0], evs) // you outlived the table
|
||||
s.settle(alive[0], OutcomeWon, evs) // the last one standing takes the pot
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -233,17 +229,9 @@ func (s State) alive() []int {
|
||||
return out
|
||||
}
|
||||
|
||||
// live reports whether a seat is still playing. Out is empty in a normal game and
|
||||
// in any game saved before No Mercy existed, so a missing entry is a living seat.
|
||||
// live reports whether a seat is still in the current hand. Out is empty between
|
||||
// hands and in any game saved before No Mercy existed, so a missing entry is a
|
||||
// living seat.
|
||||
func (s State) live(seat int) bool {
|
||||
return seat >= len(s.Out) || !s.Out[seat]
|
||||
}
|
||||
|
||||
// lose ends the game against the player without anybody having gone out — which
|
||||
// is what a mercy kill on seat zero is.
|
||||
func (s *State) lose(evs *[]Event) {
|
||||
s.Phase = PhaseDone
|
||||
s.Outcome = OutcomeLost
|
||||
s.Payout = 0
|
||||
*evs = append(*evs, Event{Kind: EvSettle, Seat: You, Text: string(OutcomeLost)})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user