skeleton move timer

For #11.
This commit is contained in:
Branden J Brown 2024-02-03 21:11:09 -06:00
parent ae1cf18d6d
commit 566de4066b
4 changed files with 37 additions and 19 deletions

33
game.go
View File

@ -68,7 +68,10 @@ func gameActor(ctx context.Context, g *game.Match, dealer, chall person, join <-
go playerActor(ctx, chall, actions) go playerActor(ctx, chall, actions)
slog.InfoContext(ctx, "start game", "dealer", dealer.id, "challenger", chall.id) 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 { for {
select { select {
case <-ctx.Done(): case <-ctx.Done():
@ -76,7 +79,7 @@ func gameActor(ctx context.Context, g *game.Match, dealer, chall person, join <-
case p := <-join: case p := <-join:
slog.InfoContext(ctx, "observer", "id", p.id) slog.InfoContext(ctx, "observer", "id", p.id)
// Deliver the game state just to the new observer. // 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 // We don't need to track them as an observer. Just close their
// conn and move on. // conn and move on.
go p.conn.Close(websocket.StatusNormalClosure, "looks like you dropped") 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) err := applyAction(g, a)
switch err { switch err {
case nil: case nil:
broadcast(ctx, g, dealer, chall, obs) broadcast(ctx, g, dealer, chall, obs, dl)
case game.ErrRoundEnded: case game.ErrRoundEnded:
broadcast(ctx, g, dealer, chall, obs) broadcast(ctx, g, dealer, chall, obs, dl)
if g.MatchWinner() != nil { if g.MatchWinner() != nil {
gameOver(ctx, dealer, chall, obs) gameOver(ctx, dealer, chall, obs)
// TODO(zeph): server needs to know the players are gone // TODO(zeph): server needs to know the players are gone
return return
} }
g.NextRound() g.NextRound()
broadcast(ctx, g, dealer, chall, obs) broadcast(ctx, g, dealer, chall, obs, dl)
case game.ErrGameEnded: case game.ErrGameEnded:
broadcast(ctx, g, dealer, chall, obs) broadcast(ctx, g, dealer, chall, obs, dl)
g.NextGame() g.NextGame()
broadcast(ctx, g, dealer, chall, obs) broadcast(ctx, g, dealer, chall, obs, dl)
case game.ErrWrongTurn: case game.ErrWrongTurn:
slog.WarnContext(ctx, "action on wrong turn", "from", a.Player, "action", a.Action) slog.WarnContext(ctx, "action on wrong turn", "from", a.Player, "action", a.Action)
continue
case errWeirdAction: case errWeirdAction:
slog.WarnContext(ctx, "nonsense action", "from", a.Player, "action", a.Action) slog.WarnContext(ctx, "nonsense action", "from", a.Player, "action", a.Action)
continue
default: default:
slog.ErrorContext(ctx, "action caused a mystery error", "from", a.Player, "action", a.Action, "err", err.Error()) 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. // Clear observers who have left.
for k := len(obs) - 1; k >= 0; k-- { 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 // TODO(zeph): this probably should return an error or some other signal
// if a player drops so that the actor knows to quit // 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 // TODO(zeph): concede, but we need to be careful not to recurse
slog.WarnContext(ctx, "lost dealer", "player", dealer.id, "err", err.Error()) 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 // TODO(zeph): concede, but we need to be careful not to recurse
slog.WarnContext(ctx, "lost challenger", "player", chall.id, "err", err.Error()) slog.WarnContext(ctx, "lost challenger", "player", chall.id, "err", err.Error())
} }
if len(obs) == 0 { if len(obs) == 0 {
return return
} }
d := g.DTO(player.ID{}) d := g.DTO(player.ID{}, deadline)
for _, p := range obs { for _, p := range obs {
if err := wsjson.Write(ctx, p.conn, &d); err != nil { if err := wsjson.Write(ctx, p.conn, &d); err != nil {
slog.WarnContext(ctx, "lost observer", "err", err.Error()) slog.WarnContext(ctx, "lost observer", "err", err.Error())

View File

@ -11,6 +11,7 @@ package game
import ( import (
"errors" "errors"
"time"
"git.sunturtle.xyz/studio/shotgun/player" "git.sunturtle.xyz/studio/shotgun/player"
"git.sunturtle.xyz/studio/shotgun/serve" "git.sunturtle.xyz/studio/shotgun/serve"
@ -239,8 +240,9 @@ func (g *Match) Concede(id player.ID) error {
return ErrRoundEnded return ErrRoundEnded
} }
// DTO returns the current match state as viewed by the given player. // DTO returns the current match state as viewed by the given player and with
func (g *Match) DTO(id player.ID) serve.Game { // 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 var live, blank int
if g.action.gameStart() { if g.action.gameStart() {
live = len(g.shells) / 2 live = len(g.shells) / 2
@ -265,6 +267,7 @@ func (g *Match) DTO(id player.ID) serve.Game {
Damage: g.damage, Damage: g.damage,
Shell: g.Peek(id), Shell: g.Peek(id),
Previous: g.prev, Previous: g.prev,
Deadline: time.Now().UnixMilli(),
Live: live, Live: live,
Blank: blank, Blank: blank,
} }

View File

@ -2,6 +2,7 @@ package game
import ( import (
"testing" "testing"
"time"
"github.com/google/uuid" "github.com/google/uuid"
@ -455,13 +456,13 @@ func TestGameDTO(t *testing.T) {
Live: len(g.shells) / 2, Live: len(g.shells) / 2,
Blank: (len(g.shells) + 1) / 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) 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) 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) t.Errorf("observer sees the wrong thing:\nwant %+v\ngot %+v", want, got)
} }
} }
@ -481,13 +482,13 @@ func TestGameDTO(t *testing.T) {
Live: 0, Live: 0,
Blank: 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) 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) 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) t.Errorf("observer sees the wrong thing:\nwant %+v\ngot %+v", want, got)
} }
} }

View File

@ -23,6 +23,9 @@ type Game struct {
Shell *bool `json:"shell,omitempty"` Shell *bool `json:"shell,omitempty"`
// Previous gives whether the previously discharged shell was live. // Previous gives whether the previously discharged shell was live.
Previous *bool `json:"previous"` 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 // Live is the number of live shells this round, if it is the first turn
// of the round. // of the round.
Live int `json:"live,omitempty"` Live int `json:"live,omitempty"`