diff --git a/game/game.go b/game/game.go index c3624c1..1db6a28 100644 --- a/game/game.go +++ b/game/game.go @@ -19,10 +19,10 @@ import ( type Match struct { // rng is the PRNG state used for this match. - rng RNG + rng xoshiro // players are the players in the match. The first element is the dealer // and the second is the challenger. - players [2]Player + players [2]playerState // shells is the list of remaining shells in the current game. It always // uses shellArray as its backing array. shells []bool @@ -50,8 +50,8 @@ type Match struct { // New creates a new match started at round 1. func New(dealer, challenger player.ID) *Match { g := &Match{ - rng: NewRNG(), - players: [2]Player{ + rng: newRNG(), + players: [2]playerState{ {id: dealer}, {id: challenger}, }, @@ -63,8 +63,8 @@ func New(dealer, challenger player.ID) *Match { // NextRound starts the next round of a match. func (g *Match) NextRound() { g.hp = int8(g.rng.Intn(3) + 2) - g.players[0].StartRound(g.hp) - g.players[1].StartRound(g.hp) + g.players[0].startRound(g.hp) + g.players[1].startRound(g.hp) g.round++ g.NextGame() } @@ -72,8 +72,8 @@ func (g *Match) NextRound() { // NextGame starts the next game of a round. func (g *Match) NextGame() { items := g.rng.Intn(4) + 1 - g.players[0].StartGroup(&g.rng, items) - g.players[1].StartGroup(&g.rng, items) + g.players[0].startGame(&g.rng, items) + g.players[1].startGame(&g.rng, items) shells := g.rng.Intn(6) + 2 for i := 0; i < shells/2; i++ { g.shellArray[i] = true @@ -82,37 +82,37 @@ func (g *Match) NextGame() { g.shellArray[i] = false } g.shells = g.shellArray[:shells] - ShuffleSlice(&g.rng, g.shells) + shuffle(&g.rng, g.shells) g.turn = 0 g.prev = nil - g.NextTurn() + g.nextTurn() } -// NextTurn advances the turn but not the match or round state. -func (g *Match) NextTurn() { +// nextTurn advances the turn but not the match or round state. +func (g *Match) nextTurn() { g.turn++ - g.ResetTurn() + g.resetTurn() cur := g.CurrentPlayer() - skip := cur.cuffs == CuffedSkip - cur.cuffs = cur.cuffs.NextState() + skip := cur.cuffs == cuffedSkip + cur.cuffs = cur.cuffs.nextState() if skip { - g.NextTurn() + g.nextTurn() } } -// ResetTurn performs start-of-turn model resets. -func (g *Match) ResetTurn() { +// resetTurn performs start-of-turn model resets. +func (g *Match) resetTurn() { g.damage = 1 g.reveal = false } // CurrentPlayer gets the current player. -func (g *Match) CurrentPlayer() *Player { +func (g *Match) CurrentPlayer() *playerState { return &g.players[g.turn&1] } // Opponent returns the player who is not the current player. -func (g *Match) Opponent() *Player { +func (g *Match) Opponent() *playerState { return &g.players[g.turn&1^1] } @@ -128,8 +128,8 @@ func (g *Match) Apply(id player.ID, item int) error { if item < 0 || item >= len(cur.items) { return errors.New("item index out of bounds") } - if cur.items[item].Apply(g) { - cur.items[item] = ItemNone + if cur.items[item].apply(g) { + cur.items[item] = itemNone } if g.Empty() { return ErrGameEnded @@ -162,7 +162,7 @@ func (g *Match) Empty() bool { // RoundWinner returns the player who won the current round, or nil if the // round is not over. -func (g *Match) RoundWinner() *Player { +func (g *Match) RoundWinner() *playerState { if g.players[0].hp <= 0 { return &g.players[1] } @@ -174,7 +174,7 @@ func (g *Match) RoundWinner() *Player { // MatchWinner returns the player who has won the match, or nil if the match // is not over. -func (g *Match) MatchWinner() *Player { +func (g *Match) MatchWinner() *playerState { if g.round < 3 { return nil } @@ -215,9 +215,9 @@ func (g *Match) Shoot(id player.ID, self bool) error { return ErrGameEnded } if !self || live { - g.NextTurn() + g.nextTurn() } else { - g.ResetTurn() + g.resetTurn() } return nil } @@ -262,8 +262,8 @@ func (g *Match) DTO(id player.ID, deadline time.Time) serve.Game { } return serve.Game{ Players: [2]serve.Player{ - g.players[0].DTO(), - g.players[1].DTO(), + g.players[0].dto(), + g.players[1].dto(), }, Action: g.action.String(), Winner: w, diff --git a/game/game_test.go b/game/game_test.go index 583a029..4f5cfb9 100644 --- a/game/game_test.go +++ b/game/game_test.go @@ -87,8 +87,8 @@ func TestGameStartGroup(t *testing.T) { msg string args []any }{ - {g.players[0].items[0] == ItemNone, "dealer has no first item: %v", []any{g.players[0].items}}, - {g.players[1].items[0] == ItemNone, "challenger has no first item: %v", []any{g.players[1].items}}, + {g.players[0].items[0] == itemNone, "dealer has no first item: %v", []any{g.players[0].items}}, + {g.players[1].items[0] == itemNone, "challenger has no first item: %v", []any{g.players[1].items}}, {len(g.shells) < 2 || len(g.shells) > 8, "bad shells count %d, want 2-8", []any{len(g.shells)}}, {&g.shells[0] != &g.shellArray[0], "shells[0] is %p, want %p", []any{&g.shells[0], &g.shellArray[0]}}, {g.round != 1, "round is %d, want 1", []any{g.round}}, @@ -103,8 +103,8 @@ func TestGameStartGroup(t *testing.T) { } } // H*ck with the game state. - g.players[0].items[0] = ItemNone - g.players[1].items[0] = ItemNone + g.players[0].items[0] = itemNone + g.players[1].items[0] = itemNone g.popShell() g.popShell() g.turn = 3 @@ -128,7 +128,7 @@ func TestGameNextTurn(t *testing.T) { {g.turn != i, "turn is %d, want %d", []any{g.turn, i}}, {g.damage != 1, "damage is %d, want 1", []any{g.damage}}, {g.reveal, "revealed at start", nil}, - {g.CurrentPlayer().cuffs != Uncuffed, "cuffs is %d, want %d", []any{g.CurrentPlayer().cuffs, Uncuffed}}, + {g.CurrentPlayer().cuffs != uncuffed, "cuffs is %d, want %d", []any{g.CurrentPlayer().cuffs, uncuffed}}, } for _, c := range checks { if c.failed { @@ -137,8 +137,8 @@ func TestGameNextTurn(t *testing.T) { } g.damage = 2 g.reveal = true - g.Opponent().cuffs = Cuffed - g.NextTurn() + g.Opponent().cuffs = cuffed + g.nextTurn() } } @@ -155,14 +155,14 @@ func TestGamePlayers(t *testing.T) { if g.Opponent().id != dealer { t.Errorf("dealer isn't opponent at start") } - g.NextTurn() + g.nextTurn() if g.CurrentPlayer().id != dealer { t.Errorf("dealer isn't current player after turn") } if g.Opponent().id != chall { t.Errorf("challenger isn't opponent after turn") } - g.NextTurn() + g.nextTurn() if g.CurrentPlayer().id != chall { t.Errorf("challenger isn't current player after two turns") } @@ -171,7 +171,7 @@ func TestGamePlayers(t *testing.T) { } me, you := g.CurrentPlayer(), g.Opponent() for i := 3; i < 1000; i++ { - g.NextTurn() + g.nextTurn() if g.CurrentPlayer() != you { t.Errorf("wrong player after %d turns", i) } @@ -240,7 +240,7 @@ func TestGamePeek(t *testing.T) { if g.Peek(chall) != &g.shellArray[0] { t.Errorf("challenger couldn't peek on own turn") } - g.NextTurn() + g.nextTurn() g.reveal = true if g.Peek(dealer) != &g.shellArray[0] { t.Errorf("dealer couldn't peek on own turn") @@ -444,8 +444,8 @@ func TestGameDTO(t *testing.T) { { want := serve.Game{ Players: [2]serve.Player{ - g.players[0].DTO(), - g.players[1].DTO(), + g.players[0].dto(), + g.players[1].dto(), }, Action: "Start", Round: g.round, @@ -470,8 +470,8 @@ func TestGameDTO(t *testing.T) { { want := serve.Game{ Players: [2]serve.Player{ - g.players[0].DTO(), - g.players[1].DTO(), + g.players[0].dto(), + g.players[1].dto(), }, Action: "Shoot", Round: g.round, diff --git a/game/item.go b/game/item.go index 7787af1..577ce87 100644 --- a/game/item.go +++ b/game/item.go @@ -1,30 +1,30 @@ package game -type Item int8 +type item int8 const ( - ItemNone Item = iota - ItemLens - ItemCig - ItemBeer - ItemCuff - ItemKnife + itemNone item = iota + itemLens + itemCig + itemBeer + itemCuff + itemKnife ) -func NewItem(rng *RNG) Item { - return Item(rng.Intn(5)) + 1 +func newItem(rng *xoshiro) item { + return item(rng.Intn(5)) + 1 } -func (i Item) Apply(g *Match) bool { +func (i item) apply(g *Match) bool { switch i { - case ItemNone: + case itemNone: return false - case ItemLens: + case itemLens: // Lenses are always used, even if they have no effect. g.action = actLens g.reveal = true return true - case ItemCig: + case itemCig: cur := g.CurrentPlayer() if cur.hp < g.hp { cur.hp++ @@ -32,7 +32,7 @@ func (i Item) Apply(g *Match) bool { } g.action = actCig return true - case ItemBeer: + case itemBeer: g.popShell() g.action = actBeer if g.Empty() { @@ -40,15 +40,15 @@ func (i Item) Apply(g *Match) bool { g.NextGame() } return true - case ItemCuff: + case itemCuff: opp := g.Opponent() - if opp.cuffs != Uncuffed { + if opp.cuffs != uncuffed { return false } g.action = actCuff - opp.cuffs = CuffedSkip + opp.cuffs = cuffedSkip return true - case ItemKnife: + case itemKnife: if g.damage != 1 { return false } @@ -62,6 +62,6 @@ func (i Item) Apply(g *Match) bool { var itemNames = [...]string{"", "🔍", "🚬", "🍺", "👮", "🔪"} -func (i Item) String() string { +func (i item) String() string { return itemNames[i] } diff --git a/game/item_test.go b/game/item_test.go index 2bd9bdc..482dd26 100644 --- a/game/item_test.go +++ b/game/item_test.go @@ -1,22 +1,18 @@ -package game_test +package game -import ( - "testing" - - "git.sunturtle.xyz/studio/shotgun/game" -) +import "testing" func TestItemStrings(t *testing.T) { cases := []struct { - item game.Item + item item want string }{ - {game.ItemNone, ""}, - {game.ItemLens, "🔍"}, - {game.ItemCig, "🚬"}, - {game.ItemBeer, "🍺"}, - {game.ItemCuff, "👮"}, - {game.ItemKnife, "🔪"}, + {itemNone, ""}, + {itemLens, "🔍"}, + {itemCig, "🚬"}, + {itemBeer, "🍺"}, + {itemCuff, "👮"}, + {itemKnife, "🔪"}, } for _, c := range cases { got := c.item.String() diff --git a/game/player.go b/game/player.go index e98e42f..b4444e7 100644 --- a/game/player.go +++ b/game/player.go @@ -7,33 +7,33 @@ import ( "git.sunturtle.xyz/studio/shotgun/serve" ) -type Player struct { +type playerState struct { id player.ID hp int8 - items [8]Item - cuffs CuffState + items [8]item + cuffs cuffState } -func (p *Player) StartRound(hp int8) { +func (p *playerState) startRound(hp int8) { p.hp = hp clear(p.items[:]) } -func (p *Player) StartGroup(rng *RNG, items int) { +func (p *playerState) startGame(rng *xoshiro, items int) { for i := 0; i < items; i++ { - k := slices.Index(p.items[:], ItemNone) + k := slices.Index(p.items[:], itemNone) if k < 0 { break } - p.items[k] = NewItem(rng) + p.items[k] = newItem(rng) } - p.cuffs = Uncuffed + p.cuffs = uncuffed } -func (p *Player) DTO() serve.Player { +func (p *playerState) dto() serve.Player { r := serve.Player{ HP: p.hp, - Cuffs: p.cuffs != Uncuffed, + Cuffs: p.cuffs != uncuffed, } for k, i := range p.items { r.Items[k] = i.String() @@ -41,22 +41,16 @@ func (p *Player) DTO() serve.Player { return r } -// Is returns whether the player has the same ID as other. -// This exists for tests. -func (p *Player) Is(other *Player) bool { - return p.id == other.id -} - -type CuffState uint8 +type cuffState uint8 const ( - Uncuffed CuffState = iota - Cuffed - CuffedSkip + uncuffed cuffState = iota + cuffed + cuffedSkip ) -func (c CuffState) NextState() CuffState { - if c != Uncuffed { +func (c cuffState) nextState() cuffState { + if c != uncuffed { c-- } return c diff --git a/game/rng.go b/game/rng.go index 76926c4..a4533be 100644 --- a/game/rng.go +++ b/game/rng.go @@ -6,16 +6,16 @@ import ( "math/bits" ) -// RNG is a random number generator for shotgun. +// xoshiro is a random number generator for shotgun. // // Currently xoshiro256**. Subject to change. -type RNG struct { +type xoshiro struct { w, x, y, z uint64 } -// NewRNG produces a new, uniquely seeded RNG. -func NewRNG() RNG { - r := RNG{} +// newRNG produces a new, uniquely seeded RNG. +func newRNG() xoshiro { + r := xoshiro{} b := make([]byte, 8*4) // Loop to ensure the state is never all-zero, which is an invalid state // for xoshiro. @@ -30,7 +30,7 @@ func NewRNG() RNG { } // Uint64 produces a 64-bit pseudo-random value. -func (rng *RNG) Uint64() uint64 { +func (rng *xoshiro) Uint64() uint64 { w, x, y, z := rng.w, rng.x, rng.y, rng.z r := bits.RotateLeft64(x*5, 7) * 9 t := x << 17 @@ -45,7 +45,7 @@ func (rng *RNG) Uint64() uint64 { } // Intn produces an int in [0, n). Panics if n <= 0. -func (rng *RNG) Intn(n int) int { +func (rng *xoshiro) Intn(n int) int { if n <= 0 { panic("shotgun: rng.Intn max below zero") } @@ -57,7 +57,7 @@ func (rng *RNG) Intn(n int) int { return int(x % uint(n)) } -func ShuffleSlice[E any, S ~[]E](rng *RNG, s S) { +func shuffle[E any, S ~[]E](rng *xoshiro, s S) { for i := len(s) - 1; i > 0; i-- { j := rng.Intn(i + 1) s[i], s[j] = s[j], s[i] diff --git a/main.go b/main.go index 5b4c06c..effe11b 100644 --- a/main.go +++ b/main.go @@ -48,11 +48,11 @@ func main() { } r := chi.NewRouter() - r.Post("/user/register", s.Register) - r.Post("/user/login", s.Login) - r.With(serve.WithSession(db)).Get("/user/logout", s.Logout) - r.With(serve.WithSession(db)).Get("/user/me", s.Me) - r.With(serve.WithSession(db)).Get("/queue", s.Queue) + r.Post("/user/register", s.register) + r.Post("/user/login", s.login) + r.With(serve.WithSession(db)).Get("/user/logout", s.logout) + r.With(serve.WithSession(db)).Get("/user/me", s.me) + r.With(serve.WithSession(db)).Get("/queue", s.queue) r.With(middleware.Compress(5, "text/html", "text/javascript", "text/css")).Method("GET", "/*", http.FileServer(http.Dir(public))) slog.Info("listening", "addr", addr, "public", public) http.ListenAndServe(addr, r) diff --git a/serve/dto.go b/serve/dto.go index 79ef3ef..4d7351b 100644 --- a/serve/dto.go +++ b/serve/dto.go @@ -44,7 +44,7 @@ type Player struct { Cuffs bool `json:"cuffs,omitempty"` } -type GameID = uuid.UUID +type GameID struct{ uuid.UUID } // GameStart is the JSON DTO given to each player when their game starts. // Observers do not receive it. diff --git a/server.go b/server.go index 7de5429..9fc4e57 100644 --- a/server.go +++ b/server.go @@ -49,7 +49,7 @@ func (s *Server) accept(w http.ResponseWriter, r *http.Request, p player.ID) (pe return person{conn: conn, id: p}, nil } -func (s *Server) Register(w http.ResponseWriter, r *http.Request) { +func (s *Server) register(w http.ResponseWriter, r *http.Request) { ctx := r.Context() slog := slog.With( slog.String("remote", r.RemoteAddr), @@ -90,7 +90,7 @@ func (s *Server) Register(w http.ResponseWriter, r *http.Request) { slog.InfoContext(ctx, "registered", "user", user) } -func (s *Server) Login(w http.ResponseWriter, r *http.Request) { +func (s *Server) login(w http.ResponseWriter, r *http.Request) { ctx := r.Context() slog := slog.With( slog.String("remote", r.RemoteAddr), @@ -125,7 +125,7 @@ func (s *Server) Login(w http.ResponseWriter, r *http.Request) { slog.InfoContext(ctx, "logged in", "player", p, "id", id) } -func (s *Server) Logout(w http.ResponseWriter, r *http.Request) { +func (s *Server) logout(w http.ResponseWriter, r *http.Request) { ctx := r.Context() slog := slog.With( slog.String("remote", r.RemoteAddr), @@ -149,7 +149,7 @@ func (s *Server) Logout(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "/", http.StatusSeeOther) } -func (s *Server) Me(w http.ResponseWriter, r *http.Request) { +func (s *Server) me(w http.ResponseWriter, r *http.Request) { id := serve.ReqPlayer(r.Context()) if id == (player.ID{}) { panic("missing player ID") @@ -160,9 +160,9 @@ func (s *Server) Me(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(q) } -// Queue connects players to games. The connection immediately upgrades. +// queue connects players to games. The connection immediately upgrades. // This handler MUST be wrapped in [serve.WithPlayerID]. -func (s *Server) Queue(w http.ResponseWriter, r *http.Request) { +func (s *Server) queue(w http.ResponseWriter, r *http.Request) { p := serve.ReqPlayer(r.Context()) person, err := s.accept(w, r, p) if err != nil { @@ -201,7 +201,7 @@ func (s *Server) joinAndServe(p person) { } // Reply with the game ID so they can share. r := serve.GameStart{ - ID: id, + ID: serve.GameID{UUID: id}, Dealer: deal, } if err := wsjson.Write(context.TODO(), p.conn, r); err != nil {