asd
This commit is contained in:
parent
e2c4d4cb83
commit
ef045f9353
4 changed files with 76 additions and 108 deletions
|
|
@ -531,18 +531,24 @@ func (g *Game) buildState(p *Player, chosenSuit Suit) GameScriptState {
|
||||||
copy(handCopy, p.Hand)
|
copy(handCopy, p.Hand)
|
||||||
|
|
||||||
counts := make([]int, len(g.Players))
|
counts := make([]int, len(g.Players))
|
||||||
|
hands := make([][]Card, len(g.Players))
|
||||||
for i, pl := range g.Players {
|
for i, pl := range g.Players {
|
||||||
counts[i] = len(pl.Hand)
|
counts[i] = len(pl.Hand)
|
||||||
|
h := make([]Card, len(pl.Hand))
|
||||||
|
copy(h, pl.Hand)
|
||||||
|
hands[i] = h
|
||||||
}
|
}
|
||||||
|
|
||||||
return GameScriptState{
|
return GameScriptState{
|
||||||
ActiveSuit: g.ActiveSuit,
|
ActiveSuit: g.ActiveSuit,
|
||||||
PenaltyCards: g.PenaltyCards,
|
PenaltyCards: g.PenaltyCards,
|
||||||
SkipNext: g.SkipNext,
|
SkipNext: g.SkipNext,
|
||||||
ChosenSuit: chosenSuit,
|
ChosenSuit: chosenSuit,
|
||||||
Hand: handCopy,
|
Hand: handCopy,
|
||||||
PlayerCount: len(g.Players),
|
PlayerHands: hands,
|
||||||
HandCounts: counts,
|
CurrentPlayerIndex: g.CurrentPlayer,
|
||||||
Direction: g.Direction,
|
PlayerCount: len(g.Players),
|
||||||
|
HandCounts: counts,
|
||||||
|
Direction: g.Direction,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -95,30 +95,9 @@ func TestPlayCardsRejectsMultipleSelection(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPendingChoiceBlocksTurnUntilChoiceApplied(t *testing.T) {
|
func TestPendingChoiceBlocksTurnUntilChoiceApplied(t *testing.T) {
|
||||||
script := `
|
script := "function canPlay(cards, topCard, state){return cards.length===1;}\n" +
|
||||||
function canPlay(cards, topCard, state) {
|
"function onPlayed(cards, state){return {penaltyCards:state.penaltyCards,skipNext:state.skipNext,activeSuit:cards[0].suit,choicePrompt:'Pick mode',choiceOptions:['keep','reverse']};}\n" +
|
||||||
return cards.length === 1;
|
"function onChoice(choice, state){return {penaltyCards:state.penaltyCards,skipNext:state.skipNext,activeSuit:state.activeSuit,reverseDir:choice==='reverse'};}"
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
g, err := NewGame("room-6", script)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -139,9 +118,6 @@ function onChoice(choice, state) {
|
||||||
if g.PendingChoice == nil {
|
if g.PendingChoice == nil {
|
||||||
t.Fatalf("expected pending choice")
|
t.Fatalf("expected pending choice")
|
||||||
}
|
}
|
||||||
if g.CurrentPlayer != 0 {
|
|
||||||
t.Fatalf("turn should not advance before choice")
|
|
||||||
}
|
|
||||||
if err := g.Draw("p1"); err == nil {
|
if err := g.Draw("p1"); err == nil {
|
||||||
t.Fatalf("draw should be blocked while choice is pending")
|
t.Fatalf("draw should be blocked while choice is pending")
|
||||||
}
|
}
|
||||||
|
|
@ -158,35 +134,9 @@ function onChoice(choice, state) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestOnChoiceCanDiscardExtraCardByIndex(t *testing.T) {
|
func TestOnChoiceCanDiscardExtraCardByIndex(t *testing.T) {
|
||||||
script := `
|
script := "function canPlay(cards, topCard, state){return cards.length===1;}\n" +
|
||||||
function canPlay(cards, topCard, state) {
|
"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;}\n" +
|
||||||
return cards.length === 1;
|
"function onChoice(choice, state){var out={penaltyCards:state.penaltyCards,skipNext:state.skipNext,activeSuit:state.activeSuit};if(choice==='yes') out.discardIndex=0;return out;}"
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
g, err := NewGame("room-7", script)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -204,43 +154,17 @@ function onChoice(choice, state) {
|
||||||
if err := g.PlayCards("p1", []int{0}, Hearts); err != nil {
|
if err := g.PlayCards("p1", []int{0}, Hearts); err != nil {
|
||||||
t.Fatalf("PlayCards failed: %v", err)
|
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 {
|
if err := g.ApplyChoice("p1", "yes"); err != nil {
|
||||||
t.Fatalf("ApplyChoice failed: %v", err)
|
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" {
|
if g.State != Ended || g.Winner == nil || g.Winner.ID != "p1" {
|
||||||
t.Fatalf("expected player to win after discarding last card")
|
t.Fatalf("expected player to win after discarding last card")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestOnPlayedCanReplaceHandsAndDiscardPile(t *testing.T) {
|
func TestOnPlayedCanReplaceHandsAndDiscardPile(t *testing.T) {
|
||||||
script := `
|
script := "function canPlay(cards, topCard, state){return cards.length===1;}\n" +
|
||||||
function canPlay(cards, topCard, state) {
|
"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};}"
|
||||||
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)
|
g, err := NewGame("room-8", script)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -271,3 +195,27 @@ function onPlayed(cards, state) {
|
||||||
t.Fatalf("expected current player override to 1, got %d", g.CurrentPlayer)
|
t.Fatalf("expected current player override to 1, got %d", g.CurrentPlayer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestStateExposesPlayerHandsAndCurrentPlayerIndex(t *testing.T) {
|
||||||
|
script := "function canPlay(cards, topCard, state){return cards.length===1;}\n" +
|
||||||
|
"function onPlayed(cards, state){var next=(state.currentPlayerIndex+(state.direction||1)+state.playerCount)%state.playerCount;var hands=state.playerHands.slice();var tmp=hands[state.currentPlayerIndex];hands[state.currentPlayerIndex]=hands[next];hands[next]=tmp;return {penaltyCards:state.penaltyCards,skipNext:state.skipNext,activeSuit:cards[0].suit,playerHands:hands};}"
|
||||||
|
|
||||||
|
g, err := NewGame("room-9", 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.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 len(g.Players[0].Hand) != 1 || g.Players[0].Hand[0].Suit != Spades || g.Players[0].Hand[0].Value != Seven {
|
||||||
|
t.Fatalf("expected player 0 to receive player 1 hand")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -86,14 +86,16 @@ function onChoice(choice, state) {
|
||||||
|
|
||||||
// GameScriptState is the rich context object passed into every JS function call.
|
// GameScriptState is the rich context object passed into every JS function call.
|
||||||
type GameScriptState struct {
|
type GameScriptState struct {
|
||||||
ActiveSuit Suit
|
ActiveSuit Suit
|
||||||
PenaltyCards int
|
PenaltyCards int
|
||||||
SkipNext bool
|
SkipNext bool
|
||||||
ChosenSuit Suit
|
ChosenSuit Suit
|
||||||
Hand []Card // acting player's full hand (including cards being played)
|
Hand []Card // acting player's full hand (including cards being played)
|
||||||
PlayerCount int
|
PlayerHands [][]Card
|
||||||
HandCounts []int // cards held by each player, in seat order
|
CurrentPlayerIndex int
|
||||||
Direction int // 1 = clockwise, -1 = counter-clockwise
|
PlayerCount int
|
||||||
|
HandCounts []int // cards held by each player, in seat order
|
||||||
|
Direction int // 1 = clockwise, -1 = counter-clockwise
|
||||||
}
|
}
|
||||||
|
|
||||||
// OnPlayedResult carries every mutation onPlayed() can request.
|
// OnPlayedResult carries every mutation onPlayed() can request.
|
||||||
|
|
@ -209,19 +211,29 @@ func (sr *ScriptRuntime) stateVal(s GameScriptState) goja.Value {
|
||||||
for i, c := range s.Hand {
|
for i, c := range s.Hand {
|
||||||
hand[i] = map[string]string{"suit": string(c.Suit), "value": string(c.Value)}
|
hand[i] = map[string]string{"suit": string(c.Suit), "value": string(c.Value)}
|
||||||
}
|
}
|
||||||
|
playerHands := make([]interface{}, len(s.PlayerHands))
|
||||||
|
for i, h := range s.PlayerHands {
|
||||||
|
cards := make([]interface{}, len(h))
|
||||||
|
for j, c := range h {
|
||||||
|
cards[j] = map[string]string{"suit": string(c.Suit), "value": string(c.Value)}
|
||||||
|
}
|
||||||
|
playerHands[i] = cards
|
||||||
|
}
|
||||||
counts := make([]interface{}, len(s.HandCounts))
|
counts := make([]interface{}, len(s.HandCounts))
|
||||||
for i, n := range s.HandCounts {
|
for i, n := range s.HandCounts {
|
||||||
counts[i] = n
|
counts[i] = n
|
||||||
}
|
}
|
||||||
return sr.vm.ToValue(map[string]interface{}{
|
return sr.vm.ToValue(map[string]interface{}{
|
||||||
"activeSuit": string(s.ActiveSuit),
|
"activeSuit": string(s.ActiveSuit),
|
||||||
"penaltyCards": s.PenaltyCards,
|
"penaltyCards": s.PenaltyCards,
|
||||||
"skipNext": s.SkipNext,
|
"skipNext": s.SkipNext,
|
||||||
"chosenSuit": string(s.ChosenSuit),
|
"chosenSuit": string(s.ChosenSuit),
|
||||||
"hand": hand,
|
"hand": hand,
|
||||||
"playerCount": s.PlayerCount,
|
"playerHands": playerHands,
|
||||||
"handCounts": counts,
|
"currentPlayerIndex": s.CurrentPlayerIndex,
|
||||||
"direction": s.Direction,
|
"playerCount": s.PlayerCount,
|
||||||
|
"handCounts": counts,
|
||||||
|
"direction": s.Direction,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,8 @@ The script MUST export these functions (no classes, no imports):
|
||||||
skipNext bool — true when the next player must skip unless they counter with an Ace
|
skipNext bool — true when the next player must skip unless they counter with an Ace
|
||||||
chosenSuit string — suit the player chose when playing an Upper (wildcard)
|
chosenSuit string — suit the player chose when playing an Upper (wildcard)
|
||||||
hand Card[] — the acting player's FULL hand, including cards being played
|
hand Card[] — the acting player's FULL hand, including cards being played
|
||||||
|
playerHands Card[][] — full hands of all players in seat order
|
||||||
|
currentPlayerIndex number — acting player seat index
|
||||||
playerCount number — total number of players
|
playerCount number — total number of players
|
||||||
handCounts number[] — cards held by each player (seat order)
|
handCounts number[] — cards held by each player (seat order)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue