package engine import "testing" func TestStartReinitializesEndedGameWithSamePlayers(t *testing.T) { g, err := NewGame("room-1", DefaultScript) if err != nil { t.Fatalf("NewGame failed: %v", err) } if err := g.AddPlayer("p1", "Alice"); err != nil { t.Fatalf("AddPlayer p1 failed: %v", err) } if err := g.AddPlayer("p2", "Bob"); err != nil { t.Fatalf("AddPlayer p2 failed: %v", err) } if err := g.Start(); err != nil { t.Fatalf("first Start failed: %v", err) } if g.State != Playing { t.Fatalf("expected state=%q, got %q", Playing, g.State) } g.Mu.Lock() g.State = Ended g.Winner = g.Players[0] g.Mu.Unlock() if err := g.Start(); err != nil { t.Fatalf("restart Start failed: %v", err) } if g.State != Playing { t.Fatalf("expected restarted state=%q, got %q", Playing, g.State) } if g.Winner != nil { t.Fatalf("winner should be reset on restart") } if len(g.Players) != 2 { t.Fatalf("expected players to be preserved, got %d", len(g.Players)) } } func TestStartFailsWithNotEnoughPlayers(t *testing.T) { g, err := NewGame("room-2", DefaultScript) if err != nil { t.Fatalf("NewGame failed: %v", err) } if err := g.AddPlayer("p1", "Alice"); err != nil { t.Fatalf("AddPlayer failed: %v", err) } if err := g.Start(); err == nil { t.Fatalf("expected Start to fail with one player") } } func TestDrawWhenDeckAndDiscardAreExhaustedDoesNotPanic(t *testing.T) { g, err := NewGame("room-3", DefaultScript) if err != nil { t.Fatalf("NewGame failed: %v", err) } g.Players = []*Player{{ID: "p1", Name: "Alice"}, {ID: "p2", Name: "Bob"}} g.State = Playing g.CurrentPlayer = 0 g.Deck = nil g.DiscardPile = []Card{{Suit: Hearts, Value: Seven}} defer func() { if r := recover(); r != nil { t.Fatalf("Draw panicked with exhausted deck/discard: %v", r) } }() err = g.Draw("p1") if err == nil { t.Fatalf("expected Draw to return an error when no cards are available") } } func TestPlayCardsRejectsMultipleSelection(t *testing.T) { g, err := NewGame("room-5", DefaultScript) if err != nil { t.Fatalf("NewGame failed: %v", err) } g.Players = []*Player{{ID: "p1", Name: "Alice"}, {ID: "p2", Name: "Bob"}} g.State = Playing g.CurrentPlayer = 0 g.ActiveSuit = Hearts g.DiscardPile = []Card{{Suit: Hearts, Value: Nine}} g.Players[0].Hand = []Card{{Suit: Hearts, Value: King}, {Suit: Hearts, Value: Ace}} if err := g.PlayCards("p1", []int{0, 1}, Hearts); err == nil { t.Fatalf("expected multi-card play to be rejected") } } func TestPendingChoiceBlocksTurnUntilChoiceApplied(t *testing.T) { script := ` function canPlay(cards, topCard, state) { return cards.length === 1; } function onPlayed(cards, state) { return { penaltyCards: state.penaltyCards, skipNext: state.skipNext, activeSuit: cards[0].suit, choicePrompt: "Pick mode", choiceOptions: ["keep", "reverse"] }; } function onChoice(choice, state) { return { penaltyCards: state.penaltyCards, skipNext: state.skipNext, activeSuit: state.activeSuit, reverseDir: choice === "reverse" }; } ` g, err := NewGame("room-6", script) if err != nil { t.Fatalf("NewGame failed: %v", err) } g.Players = []*Player{{ID: "p1", Name: "Alice"}, {ID: "p2", Name: "Bob"}} g.State = Playing g.CurrentPlayer = 0 g.ActiveSuit = Hearts g.Direction = 1 g.DiscardPile = []Card{{Suit: Hearts, Value: Seven}} g.Players[0].Hand = []Card{{Suit: Hearts, Value: Nine}, {Suit: Clubs, Value: Ten}} g.Players[1].Hand = []Card{{Suit: Clubs, Value: Nine}} if err := g.PlayCards("p1", []int{0}, Hearts); err != nil { t.Fatalf("PlayCards failed: %v", err) } if g.PendingChoice == nil { t.Fatalf("expected pending choice") } if g.CurrentPlayer != 0 { t.Fatalf("turn should not advance before choice") } if err := g.Draw("p1"); err == nil { t.Fatalf("draw should be blocked while choice is pending") } if err := g.ApplyChoice("p1", "reverse"); err != nil { t.Fatalf("ApplyChoice failed: %v", err) } if g.PendingChoice != nil { t.Fatalf("pending choice should be cleared") } if g.Direction != -1 { t.Fatalf("expected reverse direction after choice") } } func TestOnChoiceCanDiscardExtraCardByIndex(t *testing.T) { script := ` function canPlay(cards, topCard, state) { return cards.length === 1; } function onPlayed(cards, state) { var c = cards[0]; var out = { penaltyCards: state.penaltyCards, skipNext: state.skipNext, activeSuit: c.suit }; if (c.value === "king") { out.choicePrompt = "Discard extra?"; out.choiceOptions = ["yes", "no"]; } return out; } function onChoice(choice, state) { var out = { penaltyCards: state.penaltyCards, skipNext: state.skipNext, activeSuit: state.activeSuit }; if (choice === "yes") out.discardIndex = 0; return out; } ` g, err := NewGame("room-7", script) if err != nil { t.Fatalf("NewGame failed: %v", err) } g.Players = []*Player{{ID: "p1", Name: "Alice"}, {ID: "p2", Name: "Bob"}} g.State = Playing g.CurrentPlayer = 0 g.ActiveSuit = Hearts g.Direction = 1 g.DiscardPile = []Card{{Suit: Hearts, Value: Nine}} g.Players[0].Hand = []Card{{Suit: Hearts, Value: King}, {Suit: Clubs, Value: Ten}} g.Players[1].Hand = []Card{{Suit: Spades, Value: Seven}} if err := g.PlayCards("p1", []int{0}, Hearts); err != nil { t.Fatalf("PlayCards failed: %v", err) } if g.PendingChoice == nil { t.Fatalf("expected pending choice after king") } if len(g.Players[0].Hand) != 1 { t.Fatalf("expected one remaining card before choice, got %d", len(g.Players[0].Hand)) } if err := g.ApplyChoice("p1", "yes"); err != nil { t.Fatalf("ApplyChoice failed: %v", err) } if len(g.Players[0].Hand) != 0 { t.Fatalf("expected extra card to be discarded, hand=%d", len(g.Players[0].Hand)) } if g.State != Ended || g.Winner == nil || g.Winner.ID != "p1" { t.Fatalf("expected player to win after discarding last card") } } func TestOnPlayedCanReplaceHandsAndDiscardPile(t *testing.T) { script := ` function canPlay(cards, topCard, state) { return cards.length === 1; } function onPlayed(cards, state) { var me = state.hand.slice(); if (me.length > 0) me = me.slice(1); return { penaltyCards: 0, skipNext: false, activeSuit: "clubs", playerHands: [me, []], playedPile: [{ suit: "clubs", value: "10" }], currentPlayerIndex: 1 }; } ` g, err := NewGame("room-8", script) if err != nil { t.Fatalf("NewGame failed: %v", err) } g.Players = []*Player{{ID: "p1", Name: "Alice"}, {ID: "p2", Name: "Bob"}} g.State = Playing g.CurrentPlayer = 0 g.ActiveSuit = Hearts g.DiscardPile = []Card{{Suit: Hearts, Value: Nine}} g.Deck = []Card{{Suit: Spades, Value: Seven}} g.Players[0].Hand = []Card{{Suit: Hearts, Value: Ace}, {Suit: Clubs, Value: King}} g.Players[1].Hand = []Card{{Suit: Diamonds, Value: Ten}} if err := g.PlayCards("p1", []int{0}, Hearts); err != nil { t.Fatalf("PlayCards failed: %v", err) } if g.ActiveSuit != Clubs { t.Fatalf("expected active suit clubs, got %q", g.ActiveSuit) } if len(g.DiscardPile) != 1 || g.DiscardPile[0].Suit != Clubs || g.DiscardPile[0].Value != Ten { t.Fatalf("discard pile was not replaced by script") } if len(g.Players[1].Hand) != 0 { t.Fatalf("expected player 2 hand replaced to empty") } if g.CurrentPlayer != 1 { t.Fatalf("expected current player override to 1, got %d", g.CurrentPlayer) } }