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:
@@ -14,7 +14,7 @@ import "testing"
|
||||
// a +2.
|
||||
func oneCardAway(t *testing.T, seed uint64) State {
|
||||
t.Helper()
|
||||
s := deal(t, duel(), 100, seed)
|
||||
s := deal(t, duel(), seed)
|
||||
s.Color = Red
|
||||
s.Discard = []Card{{Red, Five}}
|
||||
s.Hands[You] = []Card{{Red, One}, {Red, Two}}
|
||||
@@ -33,7 +33,7 @@ func oneCardAway(t *testing.T, seed uint64) State {
|
||||
func TestCallingUnoKeepsYouSafe(t *testing.T) {
|
||||
for seed := uint64(0); seed < 200; seed++ {
|
||||
s := oneCardAway(t, seed)
|
||||
next, evs, err := ApplyMove(s, Move{Kind: MovePlay, Index: 0, Uno: true})
|
||||
next, evs, err := ApplyMove(s, You, Move{Kind: MovePlay, Index: 0, Uno: true})
|
||||
if err != nil {
|
||||
t.Fatalf("seed %d: play: %v", seed, err)
|
||||
}
|
||||
@@ -56,7 +56,7 @@ func TestForgettingUnoGetsYouCaught(t *testing.T) {
|
||||
caught, games := 0, 400
|
||||
for seed := uint64(0); seed < uint64(games); seed++ {
|
||||
s := oneCardAway(t, seed)
|
||||
next, evs, err := ApplyMove(s, Move{Kind: MovePlay, Index: 0})
|
||||
next, evs, err := ApplyMove(s, You, Move{Kind: MovePlay, Index: 0})
|
||||
if err != nil {
|
||||
t.Fatalf("seed %d: play: %v", seed, err)
|
||||
}
|
||||
@@ -83,13 +83,13 @@ func TestForgettingUnoGetsYouCaught(t *testing.T) {
|
||||
// forgetting at a full table is very nearly always punished.
|
||||
func TestMoreBotsMeansLessGettingAwayWithIt(t *testing.T) {
|
||||
away := func(tier Tier, seed uint64) bool {
|
||||
s := deal(t, tier, 100, seed)
|
||||
s := deal(t, tier, seed)
|
||||
s.Color = Red
|
||||
s.Discard = []Card{{Red, Five}}
|
||||
s.Hands[You] = []Card{{Red, One}, {Red, Two}}
|
||||
s.Turn = You
|
||||
s.Phase = PhasePlay
|
||||
_, evs, err := ApplyMove(s, Move{Kind: MovePlay, Index: 0})
|
||||
_, evs, err := ApplyMove(s, You, Move{Kind: MovePlay, Index: 0})
|
||||
if err != nil {
|
||||
t.Fatalf("play: %v", err)
|
||||
}
|
||||
@@ -114,7 +114,7 @@ func TestMoreBotsMeansLessGettingAwayWithIt(t *testing.T) {
|
||||
// quietBot puts a bot on one card it never called, with the turn back on you.
|
||||
func quietBot(t *testing.T, called bool) State {
|
||||
t.Helper()
|
||||
s := deal(t, duel(), 100, 21)
|
||||
s := deal(t, duel(), 21)
|
||||
s.Color = Red
|
||||
s.Discard = []Card{{Red, Five}}
|
||||
s.Hands[You] = []Card{{Red, One}, {Blue, Two}, {Green, Three}}
|
||||
@@ -131,7 +131,7 @@ func TestCatchingAQuietBot(t *testing.T) {
|
||||
s := quietBot(t, false)
|
||||
before := total(census(s))
|
||||
|
||||
next, evs, err := ApplyMove(s, Move{Kind: MoveCatch, Seat: 1})
|
||||
next, evs, err := ApplyMove(s, You, Move{Kind: MoveCatch, Seat: 1})
|
||||
if err != nil {
|
||||
t.Fatalf("catch: %v", err)
|
||||
}
|
||||
@@ -167,7 +167,7 @@ func TestCatchingACleanBotCostsYou(t *testing.T) {
|
||||
}()},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
next, evs, err := ApplyMove(tc.state, Move{Kind: MoveCatch, Seat: 1})
|
||||
next, evs, err := ApplyMove(tc.state, You, Move{Kind: MoveCatch, Seat: 1})
|
||||
if err != nil {
|
||||
t.Fatalf("catch: %v", err)
|
||||
}
|
||||
@@ -188,7 +188,7 @@ func TestCatchingACleanBotCostsYou(t *testing.T) {
|
||||
func TestYouCannotCatchYourself(t *testing.T) {
|
||||
s := quietBot(t, false)
|
||||
for _, seat := range []int{You, -1, 9} {
|
||||
if _, _, err := ApplyMove(s, Move{Kind: MoveCatch, Seat: seat}); err != ErrNoCatch {
|
||||
if _, _, err := ApplyMove(s, You, Move{Kind: MoveCatch, Seat: seat}); err != ErrNoCatch {
|
||||
t.Errorf("catching seat %d: got %v, want ErrNoCatch", seat, err)
|
||||
}
|
||||
}
|
||||
@@ -198,7 +198,7 @@ func TestYouCannotCatchYourself(t *testing.T) {
|
||||
// your way back down to one: that is a new call you owe, not the old one still
|
||||
// standing. Without this a seat could be caught out once and never again.
|
||||
func TestACallIsSpentWhenTheHandGrows(t *testing.T) {
|
||||
s := deal(t, duel(), 100, 5)
|
||||
s := deal(t, duel(), 5)
|
||||
s.Color = Red
|
||||
s.Discard = []Card{{Red, Five}}
|
||||
s.Hands[You] = []Card{{Red, One}}
|
||||
@@ -208,7 +208,7 @@ func TestACallIsSpentWhenTheHandGrows(t *testing.T) {
|
||||
|
||||
// Draw, and the hand is two: the word you said was about a card you no longer
|
||||
// hold on its own.
|
||||
next, _, err := ApplyMove(s, Move{Kind: MoveDraw})
|
||||
next, _, err := ApplyMove(s, You, Move{Kind: MoveDraw})
|
||||
if err != nil {
|
||||
t.Fatalf("draw: %v", err)
|
||||
}
|
||||
@@ -220,17 +220,17 @@ func TestACallIsSpentWhenTheHandGrows(t *testing.T) {
|
||||
// TestCatchableIsWhatTheTableCanSee — a quiet bot on one card, and nobody else.
|
||||
func TestCatchable(t *testing.T) {
|
||||
s := quietBot(t, false)
|
||||
if got := s.Catchable(); len(got) != 1 || got[0] != 1 {
|
||||
if got := s.Catchable(You); len(got) != 1 || got[0] != 1 {
|
||||
t.Errorf("Catchable() = %v, want [1]", got)
|
||||
}
|
||||
clean := quietBot(t, true)
|
||||
if got := clean.Catchable(); len(got) != 0 {
|
||||
if got := clean.Catchable(You); len(got) != 0 {
|
||||
t.Errorf("Catchable() = %v on a bot that called, want none", got)
|
||||
}
|
||||
// And not on somebody else's turn: you can only call it out when it's on you.
|
||||
off := quietBot(t, false)
|
||||
off.Turn = 1
|
||||
if got := off.Catchable(); len(got) != 0 {
|
||||
if got := off.Catchable(You); len(got) != 0 {
|
||||
t.Errorf("Catchable() = %v off-turn, want none", got)
|
||||
}
|
||||
}
|
||||
@@ -240,7 +240,7 @@ func TestCatchable(t *testing.T) {
|
||||
// all" takes every card of its colour with it, so a six-card hand can land on
|
||||
// one, and a browser subtracting one from six gets a player caught.
|
||||
func TestUnoAtSeesThroughDiscardAll(t *testing.T) {
|
||||
s := deal(t, nmDuel(), 100, 3)
|
||||
s := deal(t, nmDuel(), 3)
|
||||
s.Color = Red
|
||||
s.Discard = []Card{{Red, Five}}
|
||||
s.Hands[You] = []Card{{Red, DiscardAll}, {Red, One}, {Red, Nine}, {Red, Seven}, {Blue, Two}}
|
||||
@@ -249,7 +249,7 @@ func TestUnoAtSeesThroughDiscardAll(t *testing.T) {
|
||||
|
||||
// Index 0 dumps itself and the three other reds: five cards become one.
|
||||
// Index 4 is an ordinary play: five become four.
|
||||
got := s.UnoAt()
|
||||
got := s.UnoAt(You)
|
||||
if len(got) != 1 || got[0] != 0 {
|
||||
t.Errorf("UnoAt() = %v, want [0]: only the discard-all lands you on one card", got)
|
||||
}
|
||||
@@ -258,7 +258,7 @@ func TestUnoAtSeesThroughDiscardAll(t *testing.T) {
|
||||
// TestUnoAtIsTheOrdinaryCaseToo — two cards in hand, and either of them is a call.
|
||||
func TestUnoAtIsTheOrdinaryCaseToo(t *testing.T) {
|
||||
s := oneCardAway(t, 1)
|
||||
got := s.UnoAt()
|
||||
got := s.UnoAt(You)
|
||||
if len(got) != 2 {
|
||||
t.Errorf("UnoAt() = %v, want both cards: either one leaves you holding one", got)
|
||||
}
|
||||
@@ -267,19 +267,19 @@ func TestUnoAtIsTheOrdinaryCaseToo(t *testing.T) {
|
||||
// TestGoingOutNeedsNoCall — your last card is not one card, it's none. Nobody
|
||||
// owes the table a word for winning.
|
||||
func TestGoingOutNeedsNoCall(t *testing.T) {
|
||||
s := deal(t, duel(), 100, 9)
|
||||
s := deal(t, duel(), 9)
|
||||
s.Color = Red
|
||||
s.Discard = []Card{{Red, Five}}
|
||||
s.Hands[You] = []Card{{Red, One}}
|
||||
s.Turn = You
|
||||
s.Phase = PhasePlay
|
||||
|
||||
next, evs, err := ApplyMove(s, Move{Kind: MovePlay, Index: 0})
|
||||
next, evs, err := ApplyMove(s, You, Move{Kind: MovePlay, Index: 0})
|
||||
if err != nil {
|
||||
t.Fatalf("play the last card: %v", err)
|
||||
}
|
||||
if !next.Outcome.Won() {
|
||||
t.Fatalf("outcome %q, want a win", next.Outcome)
|
||||
if next.Winner != You {
|
||||
t.Fatalf("winner %d outcome %q, want a win for you", next.Winner, next.Outcome)
|
||||
}
|
||||
if hasKind(evs, EvCaught) {
|
||||
t.Error("caught for not calling UNO on the card that won the game")
|
||||
@@ -292,7 +292,7 @@ func TestGoingOutNeedsNoCall(t *testing.T) {
|
||||
func TestABotThatForgetsSaysNothing(t *testing.T) {
|
||||
quiet := 0
|
||||
for seed := uint64(0); seed < 300 && quiet < 1; seed++ {
|
||||
s := deal(t, duel(), 100, seed)
|
||||
s := deal(t, duel(), seed)
|
||||
s.Color = Red
|
||||
s.Discard = []Card{{Red, Five}}
|
||||
s.Hands[You] = []Card{{Blue, Two}, {Blue, Three}, {Blue, Four}}
|
||||
@@ -302,7 +302,7 @@ func TestABotThatForgetsSaysNothing(t *testing.T) {
|
||||
s.Turn = You // the bot plays on the back of your move
|
||||
|
||||
// Draw, handing the turn over: the bot plays a red and lands on one card.
|
||||
next, evs, err := ApplyMove(s, Move{Kind: MoveDraw})
|
||||
next, evs, err := ApplyMove(s, You, Move{Kind: MoveDraw})
|
||||
if err != nil {
|
||||
t.Fatalf("draw: %v", err)
|
||||
}
|
||||
@@ -313,7 +313,7 @@ func TestABotThatForgetsSaysNothing(t *testing.T) {
|
||||
if hasKind(evs, EvUno) {
|
||||
t.Error("a bot that forgot to call still announced it")
|
||||
}
|
||||
if len(next.Catchable()) != 1 {
|
||||
if len(next.Catchable(You)) != 1 {
|
||||
t.Error("a quiet bot on one card isn't catchable")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user