diff options
| author | Florian Fischer <florian.fischer@muhq.space> | 2025-07-04 09:20:48 -0400 |
|---|---|---|
| committer | Florian Fischer <florian.fischer@muhq.space> | 2025-07-06 11:49:19 -0400 |
| commit | f3e085ceccd95899d8d705a28266003709ab87b7 (patch) | |
| tree | 30869054b059b2484d44e8caf60d4755fd244318 /go/game/playerControl.go | |
| parent | 53ba6a1c0cd020f26d501912320a41e8dc736976 (diff) | |
| download | muhqs-game-f3e085ceccd95899d8d705a28266003709ab87b7.tar.gz muhqs-game-f3e085ceccd95899d8d705a28266003709ab87b7.zip | |
refactor playerControl
Allow to propagate errors in order to handle them acordingly.
Use playerNotifications by value to reduce the amount of allocations.
Add session managment playerNotifications.
Support marshalling notifications to plain text.
Diffstat (limited to 'go/game/playerControl.go')
| -rw-r--r-- | go/game/playerControl.go | 174 |
1 files changed, 159 insertions, 15 deletions
diff --git a/go/game/playerControl.go b/go/game/playerControl.go index c472f4f0..dab1af3e 100644 --- a/go/game/playerControl.go +++ b/go/game/playerControl.go @@ -1,23 +1,31 @@ package game import ( + "bufio" "fmt" + "io" "log" + "regexp" ) type PlayerNotificationType int const ( - DeclaredActionNotification PlayerNotificationType = iota + InvalidNotification PlayerNotificationType = iota + DeclaredActionNotification ResolvedActionNotification PriorityNotification TargetSelectionPrompt DeclareTriggeredActionsPrompt DraftPickPrompt + JoinedPlayerNotification + ReadyPlayerNotification ) func (n PlayerNotificationType) String() string { switch n { + case InvalidNotification: + return "InvalidNotification" case DeclaredActionNotification: return "DeclaredActionNotification" case ResolvedActionNotification: @@ -30,6 +38,10 @@ func (n PlayerNotificationType) String() string { return "DeclareTriggeredActionsPrompt" case DraftPickPrompt: return "DraftPickPrompt" + case JoinedPlayerNotification: + return "JoinedPlayerNotification" + case ReadyPlayerNotification: + return "ReadyPlayerNotification" default: log.Panicf("Unhandled notification %d", n) return "" @@ -46,7 +58,7 @@ type PlayerNotification struct { Error error } -func (n *PlayerNotification) String() string { +func (n PlayerNotification) String() string { if n.Error != nil { return fmt.Sprintf("error %v: %v", n.Notification, n.Error) } @@ -54,6 +66,10 @@ func (n *PlayerNotification) String() string { 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 @@ -100,30 +116,158 @@ 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} +} + +// 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)) + 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} + default: + return n, ErrUnknownNotification + } + return +} + type PlayerControl interface { Player() *Player - RecvAction() Action - SendNotification(PlayerNotification) + // 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) { c.actions <- a } -func (c *ChanPlayerControl) RecvAction() Action { return <-c.actions } -func (c *ChanPlayerControl) SendNotification(n PlayerNotification) { c.notifications <- n } -func (c *ChanPlayerControl) RecvNotification() *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: - return &n + case n = <-c.notifications: default: - return nil } + return n, nil } func (c *ChanPlayerControl) Close() { close(c.actions) @@ -136,15 +280,15 @@ func NewChanPlayerControl(p *Player) *ChanPlayerControl { return &ChanPlayerControl{p, a, n} } -func prompt(ctrl PlayerControl, notification PlayerNotification) Action { +func prompt(ctrl PlayerControl, notification PlayerNotification) (Action, error) { ctrl.SendNotification(notification) return ctrl.RecvAction() } -func promptBuy(ctrl PlayerControl) Action { +func promptBuy(ctrl PlayerControl) (Action, error) { return prompt(ctrl, newBuyPrompt(ctrl.Player())) } -func promptAction(ctrl PlayerControl) Action { +func promptAction(ctrl PlayerControl) (Action, error) { return prompt(ctrl, newPriorityNotification()) } |
