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:
@@ -49,7 +49,7 @@ func botForgets(rng *rand.Rand) bool { return rng.Float64() < botForget }
|
||||
// — announces it. A seat on any other number of cards owes nothing and this does
|
||||
// nothing, which is what makes it safe to call after every play.
|
||||
func (s *State) declare(seat int, called bool, evs *[]Event) {
|
||||
if s.Phase == PhaseDone || len(s.Hands[seat]) != 1 {
|
||||
if !s.playing() || len(s.Hands[seat]) != 1 {
|
||||
return
|
||||
}
|
||||
s.ensureCalled()
|
||||
@@ -59,43 +59,45 @@ func (s *State) declare(seat int, called bool, evs *[]Event) {
|
||||
}
|
||||
}
|
||||
|
||||
// botsCatch is the window. You have just played, you are holding one card, and
|
||||
// you didn't say the word — so every bot still in the game gets one look, in seat
|
||||
// order, and the first to see it takes you for two.
|
||||
// botsCatch is the window. The seat that just played is holding one card and
|
||||
// didn't say the word — so every bot still in the hand gets one look, in seat
|
||||
// order, and the first to see it takes them for two.
|
||||
//
|
||||
// It runs before runBots on purpose. The catch has to land while the table is
|
||||
// still looking at the card you played, not three turns later.
|
||||
func (s *State) botsCatch(evs *[]Event, rng *rand.Rand) {
|
||||
if s.Phase == PhaseDone || len(s.Hands[You]) != 1 || s.called(You) {
|
||||
// still looking at the card that was played, not three turns later. Only the bots
|
||||
// catch on a roll here; a human polices the table with the catch button, which is
|
||||
// the asymmetry the whole rule turns on — attention is the player's edge.
|
||||
func (s *State) botsCatch(actor int, evs *[]Event, rng *rand.Rand) {
|
||||
if !s.playing() || len(s.Hands[actor]) != 1 || s.called(actor) {
|
||||
return
|
||||
}
|
||||
for _, seat := range s.alive() {
|
||||
if seat == You {
|
||||
continue
|
||||
if seat == actor || !s.Seats[seat].Bot {
|
||||
continue // a human catches with the button, not on a roll
|
||||
}
|
||||
if rng.Float64() >= botAlert {
|
||||
continue // this one wasn't looking
|
||||
}
|
||||
s.penalise(You, seat, EvCaught, evs, rng)
|
||||
s.penalise(actor, seat, EvCaught, evs, rng)
|
||||
return // caught once is caught. They don't queue up to take turns at you
|
||||
}
|
||||
}
|
||||
|
||||
// playerCatches calls out a seat you think is holding one card in silence.
|
||||
// seatCatches calls out a seat the caller thinks is holding one card in silence.
|
||||
//
|
||||
// It is not a turn: right or wrong, the turn stays where it was, which is with
|
||||
// you. What it costs is the risk — a seat that did call, or isn't on one card at
|
||||
// all, is a seat you have just accused of nothing, and that is two cards to you.
|
||||
func (s *State) playerCatches(m Move, rng *rand.Rand) ([]Event, error) {
|
||||
// It is not a turn: right or wrong, the turn stays where it was. What it costs is
|
||||
// the risk — a seat that did call, or isn't on one card at all, is a seat the
|
||||
// caller has accused of nothing, and that is two cards to them.
|
||||
func (s *State) seatCatches(caller int, m Move, rng *rand.Rand) ([]Event, error) {
|
||||
seat := m.Seat
|
||||
if seat == You || seat < 0 || seat >= len(s.Hands) || !s.live(seat) {
|
||||
if seat == caller || seat < 0 || seat >= len(s.Hands) || !s.live(seat) {
|
||||
return nil, ErrNoCatch
|
||||
}
|
||||
var evs []Event
|
||||
if len(s.Hands[seat]) == 1 && !s.called(seat) {
|
||||
s.penalise(seat, You, EvCaught, &evs, rng) // got them
|
||||
s.penalise(seat, caller, EvCaught, &evs, rng) // got them
|
||||
} else {
|
||||
s.penalise(You, seat, EvMiscall, &evs, rng) // they were clean, and you weren't looking
|
||||
s.penalise(caller, seat, EvMiscall, &evs, rng) // they were clean, and you weren't looking
|
||||
}
|
||||
return evs, nil
|
||||
}
|
||||
@@ -127,11 +129,11 @@ func (s *State) penalise(victim, by int, kind string, evs *[]Event, rng *rand.Ra
|
||||
//
|
||||
// It answers for every card, legal or not. The table only ever asks about a card
|
||||
// you were allowed to play anyway.
|
||||
func (s State) UnoAt() []int {
|
||||
if s.Phase == PhaseDone || s.Turn != You {
|
||||
func (s State) UnoAt(seat int) []int {
|
||||
if !s.playing() || s.Turn != seat {
|
||||
return nil
|
||||
}
|
||||
hand := s.Hands[You]
|
||||
hand := s.Hands[seat]
|
||||
var out []int
|
||||
for i, c := range hand {
|
||||
left := len(hand) - 1
|
||||
@@ -156,13 +158,13 @@ func (s State) UnoAt() []int {
|
||||
// already isn't — this is the same two facts, subtracted, and doing that
|
||||
// subtraction on the server is only so that the rule for what counts as catchable
|
||||
// lives in one place instead of two.
|
||||
func (s State) Catchable() []int {
|
||||
if s.Phase == PhaseDone || s.Turn != You {
|
||||
func (s State) Catchable(viewer int) []int {
|
||||
if !s.playing() || s.Turn != viewer {
|
||||
return nil // you can only catch a seat on your own turn
|
||||
}
|
||||
var out []int
|
||||
for _, seat := range s.alive() {
|
||||
if seat != You && len(s.Hands[seat]) == 1 && !s.called(seat) {
|
||||
if seat != viewer && len(s.Hands[seat]) == 1 && !s.called(seat) {
|
||||
out = append(out, seat)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user