parent
ae1cf18d6d
commit
566de4066b
33
game.go
33
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())
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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"`
|
||||
|
Loading…
Reference in New Issue
Block a user