package game import ( "fmt" "log" "regexp" ) type PlayerNotificationType int const ( InvalidNotification PlayerNotificationType = iota DeclaredActionNotification ResolvedActionNotification PriorityNotification TargetSelectionPrompt DeclareTriggeredActionsPrompt DraftPickPrompt JoinedPlayerNotification ReadyPlayerNotification ConcededNotification ) func (n PlayerNotificationType) String() string { switch n { case InvalidNotification: return "InvalidNotification" case DeclaredActionNotification: return "DeclaredActionNotification" case ResolvedActionNotification: return "ResolvedActionNotification" case PriorityNotification: return "PriorityNotification" case TargetSelectionPrompt: return "TargetSelectionPrompt" case DeclareTriggeredActionsPrompt: return "DeclareTriggeredActionsPrompt" case DraftPickPrompt: return "DraftPickPrompt" case JoinedPlayerNotification: return "JoinedPlayerNotification" case ReadyPlayerNotification: return "ReadyPlayerNotification" case ConcededNotification: return "ConcededNotification" default: log.Panicf("Unhandled notification %d", n) return "" } } func (n PlayerNotification) IsPriorityNotification() bool { return n.Notification == PriorityNotification } type PlayerNotification struct { Notification PlayerNotificationType Context any Error error } func (n PlayerNotification) String() string { if n.Error != nil { return fmt.Sprintf("error %v: %v", n.Notification, n.Error) } return fmt.Sprintf("%v: %v", n.Notification, n.Context) } func (n PlayerNotification) Valid() bool { return n.Notification != InvalidNotification } type TargetSelectionCtx struct { Action Action Prompt string } func newPriorityNotification() PlayerNotification { return PlayerNotification{PriorityNotification, nil, nil} } func newDeclaredActionNotification(a Action, err error) PlayerNotification { return PlayerNotification{DeclaredActionNotification, a, err} } func newResolvedActionNotification(a Action, err error) PlayerNotification { return PlayerNotification{ResolvedActionNotification, a, err} } func newTargetSelectionPrompt(a Action, desc string) PlayerNotification { return PlayerNotification{TargetSelectionPrompt, TargetSelectionCtx{a, desc}, nil} } func newUpkeepPrompt(p *Player) PlayerNotification { a := newUpkeepAction(p) return newTargetSelectionPrompt(a, "Select units to disband") } func newBuyPrompt(p *Player) PlayerNotification { a := newBuyAction(p) prompt := newTargetSelectionPrompt(a, "Select a card to buy") return prompt } func newHandCardSelectionPrompt(p *Player, min, max int) PlayerNotification { a := newHandCardSelection(p, min, max) desc := fmt.Sprintf("Select between %d and %d hand cards", min, max) return newTargetSelectionPrompt(a, desc) } func newAlliedUnitSelectionPrompt(p *Player, min, max int) PlayerNotification { a := newAlliedUnitSelection(p, min, max) desc := fmt.Sprintf("Select between %d and %d allied units", min, max) return newTargetSelectionPrompt(a, desc) } func newTileSelectionPrompt(p *Player, c TargetConstraintFunc, min, max int) PlayerNotification { a := newTileSelection(p, c, min, max) desc := fmt.Sprintf("Select between %d and %d tiles", min, max) return newTargetSelectionPrompt(a, desc) } func newDeclareTriggeredActionsPrompt(triggeredActions []*TriggeredAction) PlayerNotification { return PlayerNotification{DeclareTriggeredActionsPrompt, triggeredActions, nil} } func newDraftPickPrompt(pack PileOfCards) PlayerNotification { return PlayerNotification{DraftPickPrompt, pack, nil} } // Create a notification about a joined new player. // The notification only contains the name since a full `game.Player` may not be // reconstructable for clients without a game state (e.g. draft client). func NewJoinedPlayerNotification(p *Player) PlayerNotification { return PlayerNotification{JoinedPlayerNotification, p.Name, nil} } // Create a notification about a player's readiness. // The notification only contains the name since a full `game.Player` may not be // reconstructable for clients without a game state (e.g. draft client). func NewReadyPlayerNotification(p *Player) PlayerNotification { return PlayerNotification{ReadyPlayerNotification, p.Name, nil} } // newConcededNotification creates a notification about a player's concession. func newConcededNotification(p *Player) PlayerNotification { return PlayerNotification{ConcededNotification, p, nil} } // Marshal marshals a PlayerNotification to plain text. // // The marshaled PlayerNotification has the form `!TYPE{CONTEXT}`. func (n PlayerNotification) Marshal(s State) []byte { out := []byte{'!'} ctx := []byte{} switch n.Notification { case DraftPickPrompt: out = append(out, []byte("pick")...) ctx = []byte(n.Context.(PileOfCards).String()) case PriorityNotification: out = append(out, []byte("priority")...) case DeclaredActionNotification: out = append(out, []byte("declared")...) ctx = MarshalAction(n.Context.(Action)) case ResolvedActionNotification: out = append(out, []byte("resolved")...) if n.Error != nil { ctx = []byte(n.Error.Error()) } case JoinedPlayerNotification: out = append(out, []byte("joined")...) ctx = []byte(n.Context.(string)) case ReadyPlayerNotification: out = append(out, []byte("ready")...) ctx = []byte(n.Context.(string)) case ConcededNotification: out = append(out, []byte("ready")...) ctx = []byte(n.Context.(*Player).Name) default: log.Fatalf("Marshal(%s) not implement\n", n) } out = fmt.Appendf(out, "{%s}", ctx) return out } var PlayerNotificationRegex = regexp.MustCompile(`!(.*)\{(.*)\}`) type InvalidPlayerNotificationError struct { ErrorString string } func (err InvalidPlayerNotificationError) Error() string { return err.ErrorString } var ( ErrBadFormat = InvalidPlayerNotificationError{"bad format"} ErrUnknownNotification = InvalidPlayerNotificationError{"unknown notification"} ErrNoContextExpected = InvalidPlayerNotificationError{"no context expected"} ) // UnmarshalPlayerNotification creates a UnmarshalPlayerNotification from plain text. // For the PlayerNotification plain text format see MarshalPlayerNotification. func UnmarshalPlayerNotification(s State, in []byte) (n PlayerNotification, err error) { m := PlayerNotificationRegex.FindSubmatch(in) if len(m) != 3 { return n, ErrBadFormat } t := string(m[1]) ctx := m[2] switch t { case "pick": pack := NewPileOfCards() err = pack.FromString(string(ctx)) if err != nil { return } n = newDraftPickPrompt(pack) case "priority": if len(ctx) > 0 { err = ErrNoContextExpected } n = newPriorityNotification() case "resolved": var err error if len(ctx) > 0 { err = ErrNoContextExpected } n = newResolvedActionNotification(s.Stack().Actions[len(s.Stack().Actions)-1], err) case "joined": n = PlayerNotification{JoinedPlayerNotification, string(ctx), nil} case "ready": n = PlayerNotification{ReadyPlayerNotification, string(ctx), nil} case "conceded": p := s.PlayerByName(string(ctx)) if p == nil { err = ErrUnknownPlayer return } n = newConcededNotification(p) default: return n, ErrUnknownNotification } return } type PlayerControl interface { Player() *Player // SendAction sends an Action and blocks until it is sent. SendAction(Action) error // RecvAction returns the next Action. // It blocks until the Action is received or an error occurs. RecvAction() (Action, error) // SendNotification sends an PlayerNotification and blocks until it is sent. SendNotification(PlayerNotification) error // RecvNotification returns a new PlayerNotification. // If no new notification is available an zero/invalid PlayerNotification is returned and err == nil. // RecvNotification never blocks. RecvNotification() (PlayerNotification, error) Close() } // ChanPlayerControl implements a PlayerControl using two go channels. // Objects are transferred directly. // Therefore ChanPlayerControl is only suitable for communication in the same process. type ChanPlayerControl struct { player *Player actions chan Action notifications chan PlayerNotification } func (c *ChanPlayerControl) Player() *Player { return c.player } func (c *ChanPlayerControl) SendAction(a Action) error { c.actions <- a return nil } func (c *ChanPlayerControl) RecvAction() (Action, error) { return <-c.actions, nil } func (c *ChanPlayerControl) SendNotification(n PlayerNotification) error { c.notifications <- n return nil } func (c *ChanPlayerControl) RecvNotification() (PlayerNotification, error) { var n PlayerNotification select { case n = <-c.notifications: default: } return n, nil } func (c *ChanPlayerControl) Close() { close(c.actions) close(c.notifications) } func NewChanPlayerControl(p *Player) *ChanPlayerControl { a := make(chan Action) n := make(chan PlayerNotification) return &ChanPlayerControl{p, a, n} } func prompt(ctrl PlayerControl, notification PlayerNotification) (Action, error) { ctrl.SendNotification(notification) return ctrl.RecvAction() } func promptBuy(ctrl PlayerControl) (Action, error) { return prompt(ctrl, newBuyPrompt(ctrl.Player())) } func promptAction(ctrl PlayerControl) (Action, error) { return prompt(ctrl, newPriorityNotification()) }