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)
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())

View File

@ -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,
}

View File

@ -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)
}
}

View File

@ -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"`