diff --git a/game.go b/game.go index ec5d848..d24fb40 100644 --- a/game.go +++ b/game.go @@ -46,6 +46,11 @@ func applyAction(g *game.Match, a action) error { } } +type observer struct { + conn *websocket.Conn + ctx context.Context +} + // gameActor is an actor that updates a game's state and relays changes to all // observers. func gameActor(ctx context.Context, g *game.Match, dealer, chall person, join <-chan person) { @@ -53,7 +58,7 @@ func gameActor(ctx context.Context, g *game.Match, dealer, chall person, join <- // definitely expired. ctx, stop := context.WithTimeoutCause(ctx, 4*time.Hour, errMatchExpired) defer stop() - var obs []person + var obs []observer actions := make(chan action, 2) go playerActor(ctx, dealer, actions) go playerActor(ctx, chall, actions) @@ -72,10 +77,8 @@ func gameActor(ctx context.Context, g *game.Match, dealer, chall person, join <- 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) + q := p.conn.CloseRead(ctx) + obs = append(obs, observer{conn: p.conn, ctx: q}) case a := <-actions: slog.DebugContext(ctx, "got action", "from", a.Player, "action", a.Action) err := applyAction(g, a) @@ -102,6 +105,13 @@ func gameActor(ctx context.Context, g *game.Match, dealer, chall person, join <- slog.ErrorContext(ctx, "action caused a mystery error", "from", a.Player, "action", a.Action, "err", err.Error()) } } + // Clear observers who have left. + for k := len(obs) - 1; k >= 0; k-- { + if obs[k].ctx.Err() != nil { + obs[k], obs[len(obs)-1] = obs[len(obs)-1], observer{} + obs = obs[:len(obs)-1] + } + } } } @@ -127,7 +137,7 @@ func playerActor(ctx context.Context, p person, actions chan<- action) { } } -func broadcast(ctx context.Context, g *game.Match, dealer, chall person, obs []person) { +func broadcast(ctx context.Context, g *game.Match, dealer, chall person, obs []observer) { // 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 { @@ -138,14 +148,18 @@ func broadcast(ctx context.Context, g *game.Match, dealer, chall person, obs []p // TODO(zeph): concede, but we need to be careful not to recurse slog.DebugContext(ctx, "lost challenger", "player", chall.id, "err", err.Error()) } + if len(obs) == 0 { + return + } + d := g.DTO(player.ID{}) for _, p := range obs { - if err := wsjson.Write(ctx, p.conn, g.DTO(p.id)); err != nil { - slog.DebugContext(ctx, "lost observer", "player", p.id, "err", err.Error()) + if err := wsjson.Write(ctx, p.conn, &d); err != nil { + slog.DebugContext(ctx, "lost observer", "err", err.Error()) } } } -func gameOver(ctx context.Context, dealer, chall person, obs []person) { +func gameOver(ctx context.Context, dealer, chall person, obs []observer) { // TODO(zeph): need to communicate to the server that these have gone away go dealer.conn.Close(websocket.StatusNormalClosure, "match ended") go chall.conn.Close(websocket.StatusNormalClosure, "match ended")