asd
This commit is contained in:
parent
ef045f9353
commit
ff5d6ab91d
7 changed files with 388 additions and 84 deletions
|
|
@ -58,28 +58,34 @@ type ActiveRule struct {
|
|||
}
|
||||
|
||||
type Game struct {
|
||||
ID string `json:"id"`
|
||||
Players []*Player `json:"players"`
|
||||
Deck []Card `json:"-"`
|
||||
DiscardPile []Card `json:"discard_pile"`
|
||||
CurrentPlayer int `json:"current_player_index"`
|
||||
ActiveSuit Suit `json:"active_suit"`
|
||||
PenaltyCards int `json:"penalty_cards"`
|
||||
SkipNext bool `json:"skip_next"`
|
||||
Direction int `json:"direction"` // 1 = clockwise, -1 = counter-clockwise
|
||||
State GameState `json:"state"`
|
||||
Winner *Player `json:"winner"`
|
||||
PendingChoice *PendingChoice `json:"pending_choice,omitempty"`
|
||||
ActiveRules []ActiveRule `json:"active_rules"`
|
||||
Mu sync.RWMutex `json:"-"`
|
||||
script *ScriptRuntime
|
||||
ID string `json:"id"`
|
||||
Players []*Player `json:"players"`
|
||||
Deck []Card `json:"-"`
|
||||
DiscardPile []Card `json:"discard_pile"`
|
||||
CurrentPlayer int `json:"current_player_index"`
|
||||
ActiveSuit Suit `json:"active_suit"`
|
||||
PenaltyCards int `json:"penalty_cards"`
|
||||
SkipNext bool `json:"skip_next"`
|
||||
Direction int `json:"direction"` // 1 = clockwise, -1 = counter-clockwise
|
||||
TurnNumber int `json:"turn_number"`
|
||||
RoundNumber int `json:"round_number"`
|
||||
LastActionType string `json:"last_action_type,omitempty"`
|
||||
LastActorIndex int `json:"last_actor_index,omitempty"`
|
||||
State GameState `json:"state"`
|
||||
Winner *Player `json:"winner"`
|
||||
PendingChoice *PendingChoice `json:"pending_choice,omitempty"`
|
||||
ChoiceQueue []PendingChoice `json:"choice_queue,omitempty"`
|
||||
ActiveRules []ActiveRule `json:"active_rules"`
|
||||
Mu sync.RWMutex `json:"-"`
|
||||
script *ScriptRuntime
|
||||
}
|
||||
|
||||
// PendingChoice is a UI prompt the current player must resolve before turn can continue.
|
||||
type PendingChoice struct {
|
||||
PlayerID string `json:"player_id"`
|
||||
Prompt string `json:"prompt"`
|
||||
Options []string `json:"options"`
|
||||
PlayerID string `json:"player_id"`
|
||||
PlayerIndex int `json:"player_index"`
|
||||
Prompt string `json:"prompt"`
|
||||
Options []string `json:"options"`
|
||||
}
|
||||
|
||||
func NewGame(id, scriptSrc string) (*Game, error) {
|
||||
|
|
@ -155,8 +161,13 @@ func (g *Game) Start() error {
|
|||
g.PenaltyCards = 0
|
||||
g.SkipNext = false
|
||||
g.Direction = 1
|
||||
g.TurnNumber = 1
|
||||
g.RoundNumber = 1
|
||||
g.LastActionType = "start"
|
||||
g.LastActorIndex = 0
|
||||
g.Winner = nil
|
||||
g.PendingChoice = nil
|
||||
g.ChoiceQueue = nil
|
||||
|
||||
for i := 0; i < 4; i++ {
|
||||
for _, p := range g.Players {
|
||||
|
|
@ -241,6 +252,7 @@ func (g *Game) PlayCards(playerID string, cardIndices []int, chosenSuit Suit) er
|
|||
if p.ID != playerID {
|
||||
return errors.New("not your turn")
|
||||
}
|
||||
actorIndex := g.CurrentPlayer
|
||||
|
||||
// Validate all indices.
|
||||
seen := make(map[int]bool)
|
||||
|
|
@ -306,6 +318,8 @@ func (g *Game) PlayCards(playerID string, cardIndices []int, chosenSuit Suit) er
|
|||
if len(p.Hand) == 0 {
|
||||
g.State = Ended
|
||||
g.Winner = p
|
||||
g.LastActionType = "win"
|
||||
g.LastActorIndex = actorIndex
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -313,15 +327,14 @@ func (g *Game) PlayCards(playerID string, cardIndices []int, chosenSuit Suit) er
|
|||
// current player goes again — don't advance
|
||||
return nil
|
||||
}
|
||||
if res.ChoicePrompt != "" && len(res.ChoiceOptions) > 0 {
|
||||
g.PendingChoice = &PendingChoice{
|
||||
PlayerID: p.ID,
|
||||
Prompt: res.ChoicePrompt,
|
||||
Options: res.ChoiceOptions,
|
||||
}
|
||||
if g.PendingChoice != nil {
|
||||
return nil
|
||||
}
|
||||
g.LastActionType = "play"
|
||||
g.LastActorIndex = actorIndex
|
||||
g.nextTurn()
|
||||
g.LastActionType = "draw"
|
||||
g.LastActorIndex = actorIndex
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -382,6 +395,7 @@ func (g *Game) ApplyChoice(playerID, choice string) error {
|
|||
if g.PendingChoice.PlayerID != playerID {
|
||||
return errors.New("not your choice")
|
||||
}
|
||||
chooserIndex := g.PendingChoice.PlayerIndex
|
||||
|
||||
valid := false
|
||||
for _, o := range g.PendingChoice.Options {
|
||||
|
|
@ -408,6 +422,9 @@ func (g *Game) ApplyChoice(playerID, choice string) error {
|
|||
g.State = Ended
|
||||
g.Winner = p
|
||||
g.PendingChoice = nil
|
||||
g.ChoiceQueue = nil
|
||||
g.LastActionType = "win"
|
||||
g.LastActorIndex = chooserIndex
|
||||
return nil
|
||||
}
|
||||
if res.SetDiscardIdx {
|
||||
|
|
@ -423,21 +440,16 @@ func (g *Game) ApplyChoice(playerID, choice string) error {
|
|||
if err != nil {
|
||||
log.Printf("script onPlayed error on extra discard: %v", err)
|
||||
} else {
|
||||
g.PenaltyCards = extraRes.PenaltyCards
|
||||
g.SkipNext = extraRes.SkipNext
|
||||
if extraRes.ActiveSuit != "" {
|
||||
g.ActiveSuit = extraRes.ActiveSuit
|
||||
}
|
||||
if extraRes.SetReverseDir {
|
||||
g.Direction = 1
|
||||
if extraRes.ReverseDir {
|
||||
g.Direction = -1
|
||||
}
|
||||
if err := g.applyScriptStateMutation(extraRes); err != nil {
|
||||
return err
|
||||
}
|
||||
if extraRes.InstantWin {
|
||||
g.State = Ended
|
||||
g.Winner = p
|
||||
g.PendingChoice = nil
|
||||
g.ChoiceQueue = nil
|
||||
g.LastActionType = "win"
|
||||
g.LastActorIndex = chooserIndex
|
||||
return nil
|
||||
}
|
||||
if len(p.Hand) == 0 {
|
||||
|
|
@ -446,12 +458,7 @@ func (g *Game) ApplyChoice(playerID, choice string) error {
|
|||
g.PendingChoice = nil
|
||||
return nil
|
||||
}
|
||||
if extraRes.ChoicePrompt != "" && len(extraRes.ChoiceOptions) > 0 {
|
||||
g.PendingChoice = &PendingChoice{
|
||||
PlayerID: p.ID,
|
||||
Prompt: extraRes.ChoicePrompt,
|
||||
Options: extraRes.ChoiceOptions,
|
||||
}
|
||||
if g.PendingChoice != nil {
|
||||
return nil
|
||||
}
|
||||
if extraRes.ExtraTurn {
|
||||
|
|
@ -464,13 +471,28 @@ func (g *Game) ApplyChoice(playerID, choice string) error {
|
|||
g.State = Ended
|
||||
g.Winner = p
|
||||
g.PendingChoice = nil
|
||||
g.ChoiceQueue = nil
|
||||
g.LastActionType = "win"
|
||||
g.LastActorIndex = chooserIndex
|
||||
return nil
|
||||
}
|
||||
|
||||
g.PendingChoice = nil
|
||||
if res.ExtraTurn {
|
||||
if len(g.ChoiceQueue) > 0 {
|
||||
next := g.ChoiceQueue[0]
|
||||
g.ChoiceQueue = g.ChoiceQueue[1:]
|
||||
g.PendingChoice = &next
|
||||
g.LastActionType = "choice_pending"
|
||||
g.LastActorIndex = chooserIndex
|
||||
return nil
|
||||
}
|
||||
if res.ExtraTurn {
|
||||
g.LastActionType = "choice"
|
||||
g.LastActorIndex = chooserIndex
|
||||
return nil
|
||||
}
|
||||
g.LastActionType = "choice"
|
||||
g.LastActorIndex = chooserIndex
|
||||
g.nextTurn()
|
||||
return nil
|
||||
}
|
||||
|
|
@ -516,12 +538,37 @@ func (g *Game) applyScriptStateMutation(res OnPlayedResult) error {
|
|||
}
|
||||
g.CurrentPlayer = res.CurrentPlayer
|
||||
}
|
||||
if res.ChoicePrompt != "" && len(res.ChoiceOptions) > 0 {
|
||||
target := g.CurrentPlayer
|
||||
if res.SetChoiceTargetPlayer {
|
||||
if res.ChoiceTargetPlayer < 0 || res.ChoiceTargetPlayer >= len(g.Players) {
|
||||
return errors.New("invalid choiceTargetPlayerIndex")
|
||||
}
|
||||
target = res.ChoiceTargetPlayer
|
||||
}
|
||||
ch := PendingChoice{
|
||||
PlayerID: g.Players[target].ID,
|
||||
PlayerIndex: target,
|
||||
Prompt: res.ChoicePrompt,
|
||||
Options: append([]string(nil), res.ChoiceOptions...),
|
||||
}
|
||||
if g.PendingChoice == nil {
|
||||
g.PendingChoice = &ch
|
||||
} else {
|
||||
g.ChoiceQueue = append(g.ChoiceQueue, ch)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *Game) nextTurn() {
|
||||
n := len(g.Players)
|
||||
prev := g.CurrentPlayer
|
||||
g.CurrentPlayer = ((g.CurrentPlayer+g.Direction)%n + n) % n
|
||||
g.TurnNumber++
|
||||
if g.CurrentPlayer == 0 && prev != 0 {
|
||||
g.RoundNumber++
|
||||
}
|
||||
}
|
||||
|
||||
// buildState constructs the GameScriptState for the current moment.
|
||||
|
|
@ -550,5 +597,27 @@ func (g *Game) buildState(p *Player, chosenSuit Suit) GameScriptState {
|
|||
PlayerCount: len(g.Players),
|
||||
HandCounts: counts,
|
||||
Direction: g.Direction,
|
||||
TurnNumber: g.TurnNumber,
|
||||
RoundNumber: g.RoundNumber,
|
||||
LastActionType: g.LastActionType,
|
||||
LastActorIndex: g.LastActorIndex,
|
||||
PendingChoices: func() []ScriptPendingChoice {
|
||||
out := make([]ScriptPendingChoice, 0, 1+len(g.ChoiceQueue))
|
||||
if g.PendingChoice != nil {
|
||||
out = append(out, ScriptPendingChoice{
|
||||
TargetPlayerIndex: g.PendingChoice.PlayerIndex,
|
||||
Prompt: g.PendingChoice.Prompt,
|
||||
Options: append([]string(nil), g.PendingChoice.Options...),
|
||||
})
|
||||
}
|
||||
for _, q := range g.ChoiceQueue {
|
||||
out = append(out, ScriptPendingChoice{
|
||||
TargetPlayerIndex: q.PlayerIndex,
|
||||
Prompt: q.Prompt,
|
||||
Options: append([]string(nil), q.Options...),
|
||||
})
|
||||
}
|
||||
return out
|
||||
}(),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,12 +34,17 @@ import (
|
|||
// reverseDir bool — toggle turn-order direction (false = clockwise, true = counter-clockwise)
|
||||
// choicePrompt string — optional UI prompt shown to current player
|
||||
// choiceOptions string[] — allowed answer options for that prompt
|
||||
// choiceTargetPlayerIndex number — optional target player for the choice prompt
|
||||
// discardIndex number — optional index of one extra card to discard from current hand
|
||||
// playerHands Card[][] — optional full replacement of all players' hands
|
||||
// discardPile Card[] — optional full replacement of played/discard pile
|
||||
// playedPile Card[] — alias for discardPile
|
||||
// deck Card[] — optional full replacement of draw deck
|
||||
// currentPlayerIndex number — optional current player override
|
||||
//
|
||||
// state also includes richer context for advanced rules:
|
||||
//
|
||||
// turnNumber, roundNumber, lastActionType, lastActorIndex, pendingChoices
|
||||
const DefaultScript = `
|
||||
// ── Standard Prší rules ───────────────────────────────────────────────
|
||||
|
||||
|
|
@ -96,30 +101,43 @@ type GameScriptState struct {
|
|||
PlayerCount int
|
||||
HandCounts []int // cards held by each player, in seat order
|
||||
Direction int // 1 = clockwise, -1 = counter-clockwise
|
||||
TurnNumber int
|
||||
RoundNumber int
|
||||
LastActionType string
|
||||
LastActorIndex int
|
||||
PendingChoices []ScriptPendingChoice
|
||||
}
|
||||
|
||||
type ScriptPendingChoice struct {
|
||||
TargetPlayerIndex int
|
||||
Prompt string
|
||||
Options []string
|
||||
}
|
||||
|
||||
// OnPlayedResult carries every mutation onPlayed() can request.
|
||||
// All fields are optional; the engine only applies what the script explicitly returns.
|
||||
type OnPlayedResult struct {
|
||||
PenaltyCards int
|
||||
SkipNext bool
|
||||
ActiveSuit Suit
|
||||
InstantWin bool // current player wins immediately
|
||||
ExtraTurn bool // current player takes another turn
|
||||
SetReverseDir bool // whether to apply the ReverseDir value
|
||||
ReverseDir bool // new direction: false=CW, true=CCW (only used when SetReverseDir=true)
|
||||
ChoicePrompt string
|
||||
ChoiceOptions []string
|
||||
SetDiscardIdx bool
|
||||
DiscardIdx int
|
||||
SetPlayerHands bool
|
||||
PlayerHands [][]Card
|
||||
SetDiscardPile bool
|
||||
DiscardPile []Card
|
||||
SetDeck bool
|
||||
Deck []Card
|
||||
SetCurrentPlayer bool
|
||||
CurrentPlayer int
|
||||
PenaltyCards int
|
||||
SkipNext bool
|
||||
ActiveSuit Suit
|
||||
InstantWin bool // current player wins immediately
|
||||
ExtraTurn bool // current player takes another turn
|
||||
SetReverseDir bool // whether to apply the ReverseDir value
|
||||
ReverseDir bool // new direction: false=CW, true=CCW (only used when SetReverseDir=true)
|
||||
ChoicePrompt string
|
||||
ChoiceOptions []string
|
||||
SetDiscardIdx bool
|
||||
DiscardIdx int
|
||||
SetPlayerHands bool
|
||||
PlayerHands [][]Card
|
||||
SetDiscardPile bool
|
||||
DiscardPile []Card
|
||||
SetDeck bool
|
||||
Deck []Card
|
||||
SetCurrentPlayer bool
|
||||
CurrentPlayer int
|
||||
SetChoiceTargetPlayer bool
|
||||
ChoiceTargetPlayer int
|
||||
}
|
||||
|
||||
// ScriptRuntime holds a compiled goja VM. NOT goroutine-safe.
|
||||
|
|
@ -223,6 +241,18 @@ func (sr *ScriptRuntime) stateVal(s GameScriptState) goja.Value {
|
|||
for i, n := range s.HandCounts {
|
||||
counts[i] = n
|
||||
}
|
||||
pending := make([]interface{}, len(s.PendingChoices))
|
||||
for i, ch := range s.PendingChoices {
|
||||
opts := make([]interface{}, len(ch.Options))
|
||||
for j, o := range ch.Options {
|
||||
opts[j] = o
|
||||
}
|
||||
pending[i] = map[string]interface{}{
|
||||
"targetPlayerIndex": ch.TargetPlayerIndex,
|
||||
"prompt": ch.Prompt,
|
||||
"options": opts,
|
||||
}
|
||||
}
|
||||
return sr.vm.ToValue(map[string]interface{}{
|
||||
"activeSuit": string(s.ActiveSuit),
|
||||
"penaltyCards": s.PenaltyCards,
|
||||
|
|
@ -234,6 +264,11 @@ func (sr *ScriptRuntime) stateVal(s GameScriptState) goja.Value {
|
|||
"playerCount": s.PlayerCount,
|
||||
"handCounts": counts,
|
||||
"direction": s.Direction,
|
||||
"turnNumber": s.TurnNumber,
|
||||
"roundNumber": s.RoundNumber,
|
||||
"lastActionType": s.LastActionType,
|
||||
"lastActorIndex": s.LastActorIndex,
|
||||
"pendingChoices": pending,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -319,6 +354,10 @@ func (sr *ScriptRuntime) parseResult(v goja.Value, defaults OnPlayedResult) OnPl
|
|||
out.CurrentPlayer = int(cp.ToInteger())
|
||||
out.SetCurrentPlayer = true
|
||||
}
|
||||
if tp := objGet(sr.vm, obj, "choiceTargetPlayerIndex"); tp != nil {
|
||||
out.ChoiceTargetPlayer = int(tp.ToInteger())
|
||||
out.SetChoiceTargetPlayer = true
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue