From cc2b54eac305d52ddd5aa309089888594e90e532 Mon Sep 17 00:00:00 2001 From: Branden J Brown Date: Fri, 26 Jan 2024 11:28:14 -0600 Subject: [PATCH] add game actor --- game.go | 96 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 game.go diff --git a/game.go b/game.go new file mode 100644 index 0000000..9ab0f6a --- /dev/null +++ b/game.go @@ -0,0 +1,96 @@ +package main + +import ( + "context" + "errors" + "time" + + "nhooyr.io/websocket" + "nhooyr.io/websocket/wsjson" + + "git.sunturtle.xyz/studio/shotgun/game" + "git.sunturtle.xyz/studio/shotgun/player" +) + +// An action is a request to change the game state. +type action struct { + Action string `json:"action"` + Param string `json:"param"` + Player player.ID `json:"-"` +} + +// gameActor is an actor that updates a game's state and relays changes to all +// observers. +func gameActor(ctx context.Context, g *game.Game, dealer, chall person, join chan person) { + // Games should generally be on the order of minutes. A four hour game is + // definitely expired. + ctx, stop := context.WithTimeoutCause(ctx, 4*time.Hour, errGameExpired) + defer stop() + var obs []person + actions := make(chan action, 2) + go playerActor(ctx, dealer, actions) + go playerActor(ctx, chall, actions) + // TODO(zeph): send round start info + for { + select { + case <-ctx.Done(): + return + case p := <-join: + // Deliver the game state just to the new observer. + if err := wsjson.Write(ctx, p.conn, g.DTO(p.id)); err != nil { + // We don't need to track them as an observer. Just close their + // conn and move on. + go p.conn.Close(websocket.StatusNormalClosure, "looks like you dropped") + continue + } + obs = append(obs, p) + // TODO(zeph): CloseRead returns a context we can use to drop the + // observer when they disconnect + p.conn.CloseRead(ctx) + case a := <-actions: + // TODO(zeph): transform the action into a game state change + _ = a + broadcast(ctx, g, dealer, chall, obs) + } + } +} + +func playerActor(ctx context.Context, p person, actions chan<- action) { + for { + a := action{Player: p.id} + if err := wsjson.Read(ctx, p.conn, &a); err != nil { + // If we fail to read, then we consider them to have quit. + a.Action = "quit" + select { + case <-ctx.Done(): + case actions <- a: + } + return + } + select { + case <-ctx.Done(): + return + case actions <- a: + } + } +} + +func broadcast(ctx context.Context, g *game.Game, dealer, chall person, obs []person) { + // TODO(zeph): this probably should return an error or some other signal + // if a player drops so that the actor knows to quit + if err := wsjson.Write(ctx, dealer.conn, g.DTO(dealer.id)); err != nil { + // TODO(zeph): concede, but we need to be careful not to recurse + // TODO(zeph): log + } + if err := wsjson.Write(ctx, chall.conn, g.DTO(chall.id)); err != nil { + // TODO(zeph): concede, but we need to be careful not to recurse + // TODO(zeph): log + } + for _, p := range obs { + if err := wsjson.Write(ctx, p.conn, g.DTO(p.id)); err != nil { + // TODO(zeph): log + } + } +} + +var errGameExpired = errors.New("there is a time limit on games please")