From 566de4066b715c7572d2babd1060462a8678633c Mon Sep 17 00:00:00 2001 From: Branden J Brown Date: Sat, 3 Feb 2024 21:11:09 -0600 Subject: [PATCH] skeleton move timer For #11. --- game.go | 33 ++++++++++++++++++++++----------- game/game.go | 7 +++++-- game/game_test.go | 13 +++++++------ serve/dto.go | 3 +++ 4 files changed, 37 insertions(+), 19 deletions(-) diff --git a/game.go b/game.go index 76a4f9a..319bfe8 100644 --- a/game.go +++ b/game.go @@ -68,7 +68,10 @@ func gameActor(ctx context.Context, g *game.Match, dealer, chall person, join <- go playerActor(ctx, chall, actions) slog.InfoContext(ctx, "start game", "dealer", dealer.id, "challenger", chall.id) - broadcast(ctx, g, dealer, chall, obs) + const timeLimit = 15 * time.Second + dl := time.Now().Add(timeLimit) + broadcast(ctx, g, dealer, chall, obs, dl) + tick := time.NewTicker(timeLimit) for { select { case <-ctx.Done(): @@ -76,7 +79,7 @@ func gameActor(ctx context.Context, g *game.Match, dealer, chall person, join <- case p := <-join: slog.InfoContext(ctx, "observer", "id", p.id) // Deliver the game state just to the new observer. - if err := wsjson.Write(ctx, p.conn, g.DTO(p.id)); err != nil { + if err := wsjson.Write(ctx, p.conn, g.DTO(p.id, dl)); 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") @@ -89,27 +92,35 @@ func gameActor(ctx context.Context, g *game.Match, dealer, chall person, join <- err := applyAction(g, a) switch err { case nil: - broadcast(ctx, g, dealer, chall, obs) + broadcast(ctx, g, dealer, chall, obs, dl) case game.ErrRoundEnded: - broadcast(ctx, g, dealer, chall, obs) + broadcast(ctx, g, dealer, chall, obs, dl) if g.MatchWinner() != nil { gameOver(ctx, dealer, chall, obs) // TODO(zeph): server needs to know the players are gone return } g.NextRound() - broadcast(ctx, g, dealer, chall, obs) + broadcast(ctx, g, dealer, chall, obs, dl) case game.ErrGameEnded: - broadcast(ctx, g, dealer, chall, obs) + broadcast(ctx, g, dealer, chall, obs, dl) g.NextGame() - broadcast(ctx, g, dealer, chall, obs) + broadcast(ctx, g, dealer, chall, obs, dl) case game.ErrWrongTurn: slog.WarnContext(ctx, "action on wrong turn", "from", a.Player, "action", a.Action) + continue case errWeirdAction: slog.WarnContext(ctx, "nonsense action", "from", a.Player, "action", a.Action) + continue default: slog.ErrorContext(ctx, "action caused a mystery error", "from", a.Player, "action", a.Action, "err", err.Error()) + continue } + dl = time.Now().Add(timeLimit) + tick.Reset(timeLimit) + case <-tick.C: + slog.InfoContext(ctx, "out of time") + // TODO(zeph): current player concedes } // Clear observers who have left. for k := len(obs) - 1; k >= 0; k-- { @@ -145,21 +156,21 @@ func playerActor(ctx context.Context, p person, actions chan<- action) { } } -func broadcast(ctx context.Context, g *game.Match, dealer, chall person, obs []observer) { +func broadcast(ctx context.Context, g *game.Match, dealer, chall person, obs []observer, deadline time.Time) { // 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 { + if err := wsjson.Write(ctx, dealer.conn, g.DTO(dealer.id, deadline)); err != nil { // TODO(zeph): concede, but we need to be careful not to recurse slog.WarnContext(ctx, "lost dealer", "player", dealer.id, "err", err.Error()) } - if err := wsjson.Write(ctx, chall.conn, g.DTO(chall.id)); err != nil { + if err := wsjson.Write(ctx, chall.conn, g.DTO(chall.id, deadline)); err != nil { // TODO(zeph): concede, but we need to be careful not to recurse slog.WarnContext(ctx, "lost challenger", "player", chall.id, "err", err.Error()) } if len(obs) == 0 { return } - d := g.DTO(player.ID{}) + d := g.DTO(player.ID{}, deadline) for _, p := range obs { if err := wsjson.Write(ctx, p.conn, &d); err != nil { slog.WarnContext(ctx, "lost observer", "err", err.Error()) diff --git a/game/game.go b/game/game.go index 7be4c00..898e282 100644 --- a/game/game.go +++ b/game/game.go @@ -11,6 +11,7 @@ package game import ( "errors" + "time" "git.sunturtle.xyz/studio/shotgun/player" "git.sunturtle.xyz/studio/shotgun/serve" @@ -239,8 +240,9 @@ func (g *Match) Concede(id player.ID) error { return ErrRoundEnded } -// DTO returns the current match state as viewed by the given player. -func (g *Match) DTO(id player.ID) serve.Game { +// DTO returns the current match state as viewed by the given player and with +// the given deadline on the current player's next move. +func (g *Match) DTO(id player.ID, deadline time.Time) serve.Game { var live, blank int if g.action.gameStart() { live = len(g.shells) / 2 @@ -265,6 +267,7 @@ func (g *Match) DTO(id player.ID) serve.Game { Damage: g.damage, Shell: g.Peek(id), Previous: g.prev, + Deadline: time.Now().UnixMilli(), Live: live, Blank: blank, } diff --git a/game/game_test.go b/game/game_test.go index 2b75dad..583a029 100644 --- a/game/game_test.go +++ b/game/game_test.go @@ -2,6 +2,7 @@ package game import ( "testing" + "time" "github.com/google/uuid" @@ -455,13 +456,13 @@ func TestGameDTO(t *testing.T) { Live: len(g.shells) / 2, Blank: (len(g.shells) + 1) / 2, } - if got := g.DTO(dealer); want != got { + if got := g.DTO(dealer, time.UnixMilli(0)); want != got { t.Errorf("dealer sees the wrong thing:\nwant %+v\ngot %+v", want, got) } - if got := g.DTO(chall); want != got { + if got := g.DTO(chall, time.UnixMilli(0)); want != got { t.Errorf("challenger sees the wrong thing:\nwant %+v\ngot %+v", want, got) } - if got := g.DTO(player.ID{}); want != got { + if got := g.DTO(player.ID{}, time.UnixMilli(0)); want != got { t.Errorf("observer sees the wrong thing:\nwant %+v\ngot %+v", want, got) } } @@ -481,13 +482,13 @@ func TestGameDTO(t *testing.T) { Live: 0, Blank: 0, } - if got := g.DTO(dealer); want != got { + if got := g.DTO(dealer, time.UnixMilli(0)); want != got { t.Errorf("dealer sees the wrong thing:\nwant %+v\ngot %+v", want, got) } - if got := g.DTO(chall); want != got { + if got := g.DTO(chall, time.UnixMilli(0)); want != got { t.Errorf("challenger sees the wrong thing:\nwant %+v\ngot %+v", want, got) } - if got := g.DTO(player.ID{}); want != got { + if got := g.DTO(player.ID{}, time.UnixMilli(0)); want != got { t.Errorf("observer sees the wrong thing:\nwant %+v\ngot %+v", want, got) } } diff --git a/serve/dto.go b/serve/dto.go index 5c092d7..79ef3ef 100644 --- a/serve/dto.go +++ b/serve/dto.go @@ -23,6 +23,9 @@ type Game struct { Shell *bool `json:"shell,omitempty"` // Previous gives whether the previously discharged shell was live. Previous *bool `json:"previous"` + // Deadline is the deadline on the current player's move in milliseconds + // since the Unix epoch. + Deadline int64 `json:"deadline"` // Live is the number of live shells this round, if it is the first turn // of the round. Live int `json:"live,omitempty"`