This commit is contained in:
Pavel Flegr 2026-05-07 17:29:58 +02:00
commit b3f02ea15e
10 changed files with 3567 additions and 0 deletions

View file

@ -0,0 +1,273 @@
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)
}
}