unexport things that don't need to be exported

Most of all, this lets staticcheck point out things that are unused.
Also delete an unused thing found as a result of this.
This commit is contained in:
Branden J Brown 2024-02-04 10:37:15 -06:00
parent 754da18c99
commit b70abac827
9 changed files with 108 additions and 118 deletions

View File

@ -19,10 +19,10 @@ import (
type Match struct { type Match struct {
// rng is the PRNG state used for this match. // 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 // players are the players in the match. The first element is the dealer
// and the second is the challenger. // 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 // shells is the list of remaining shells in the current game. It always
// uses shellArray as its backing array. // uses shellArray as its backing array.
shells []bool shells []bool
@ -50,8 +50,8 @@ type Match struct {
// New creates a new match started at round 1. // New creates a new match started at round 1.
func New(dealer, challenger player.ID) *Match { func New(dealer, challenger player.ID) *Match {
g := &Match{ g := &Match{
rng: NewRNG(), rng: newRNG(),
players: [2]Player{ players: [2]playerState{
{id: dealer}, {id: dealer},
{id: challenger}, {id: challenger},
}, },
@ -63,8 +63,8 @@ func New(dealer, challenger player.ID) *Match {
// NextRound starts the next round of a match. // NextRound starts the next round of a match.
func (g *Match) NextRound() { func (g *Match) NextRound() {
g.hp = int8(g.rng.Intn(3) + 2) g.hp = int8(g.rng.Intn(3) + 2)
g.players[0].StartRound(g.hp) g.players[0].startRound(g.hp)
g.players[1].StartRound(g.hp) g.players[1].startRound(g.hp)
g.round++ g.round++
g.NextGame() g.NextGame()
} }
@ -72,8 +72,8 @@ func (g *Match) NextRound() {
// NextGame starts the next game of a round. // NextGame starts the next game of a round.
func (g *Match) NextGame() { func (g *Match) NextGame() {
items := g.rng.Intn(4) + 1 items := g.rng.Intn(4) + 1
g.players[0].StartGroup(&g.rng, items) g.players[0].startGame(&g.rng, items)
g.players[1].StartGroup(&g.rng, items) g.players[1].startGame(&g.rng, items)
shells := g.rng.Intn(6) + 2 shells := g.rng.Intn(6) + 2
for i := 0; i < shells/2; i++ { for i := 0; i < shells/2; i++ {
g.shellArray[i] = true g.shellArray[i] = true
@ -82,37 +82,37 @@ func (g *Match) NextGame() {
g.shellArray[i] = false g.shellArray[i] = false
} }
g.shells = g.shellArray[:shells] g.shells = g.shellArray[:shells]
ShuffleSlice(&g.rng, g.shells) shuffle(&g.rng, g.shells)
g.turn = 0 g.turn = 0
g.prev = nil g.prev = nil
g.NextTurn() g.nextTurn()
} }
// NextTurn advances the turn but not the match or round state. // nextTurn advances the turn but not the match or round state.
func (g *Match) NextTurn() { func (g *Match) nextTurn() {
g.turn++ g.turn++
g.ResetTurn() g.resetTurn()
cur := g.CurrentPlayer() cur := g.CurrentPlayer()
skip := cur.cuffs == CuffedSkip skip := cur.cuffs == cuffedSkip
cur.cuffs = cur.cuffs.NextState() cur.cuffs = cur.cuffs.nextState()
if skip { if skip {
g.NextTurn() g.nextTurn()
} }
} }
// ResetTurn performs start-of-turn model resets. // resetTurn performs start-of-turn model resets.
func (g *Match) ResetTurn() { func (g *Match) resetTurn() {
g.damage = 1 g.damage = 1
g.reveal = false g.reveal = false
} }
// CurrentPlayer gets the current player. // CurrentPlayer gets the current player.
func (g *Match) CurrentPlayer() *Player { func (g *Match) CurrentPlayer() *playerState {
return &g.players[g.turn&1] return &g.players[g.turn&1]
} }
// Opponent returns the player who is not the current player. // 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] 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) { if item < 0 || item >= len(cur.items) {
return errors.New("item index out of bounds") return errors.New("item index out of bounds")
} }
if cur.items[item].Apply(g) { if cur.items[item].apply(g) {
cur.items[item] = ItemNone cur.items[item] = itemNone
} }
if g.Empty() { if g.Empty() {
return ErrGameEnded return ErrGameEnded
@ -162,7 +162,7 @@ func (g *Match) Empty() bool {
// RoundWinner returns the player who won the current round, or nil if the // RoundWinner returns the player who won the current round, or nil if the
// round is not over. // round is not over.
func (g *Match) RoundWinner() *Player { func (g *Match) RoundWinner() *playerState {
if g.players[0].hp <= 0 { if g.players[0].hp <= 0 {
return &g.players[1] 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 // MatchWinner returns the player who has won the match, or nil if the match
// is not over. // is not over.
func (g *Match) MatchWinner() *Player { func (g *Match) MatchWinner() *playerState {
if g.round < 3 { if g.round < 3 {
return nil return nil
} }
@ -215,9 +215,9 @@ func (g *Match) Shoot(id player.ID, self bool) error {
return ErrGameEnded return ErrGameEnded
} }
if !self || live { if !self || live {
g.NextTurn() g.nextTurn()
} else { } else {
g.ResetTurn() g.resetTurn()
} }
return nil return nil
} }
@ -262,8 +262,8 @@ func (g *Match) DTO(id player.ID, deadline time.Time) serve.Game {
} }
return serve.Game{ return serve.Game{
Players: [2]serve.Player{ Players: [2]serve.Player{
g.players[0].DTO(), g.players[0].dto(),
g.players[1].DTO(), g.players[1].dto(),
}, },
Action: g.action.String(), Action: g.action.String(),
Winner: w, Winner: w,

View File

@ -87,8 +87,8 @@ func TestGameStartGroup(t *testing.T) {
msg string msg string
args []any args []any
}{ }{
{g.players[0].items[0] == ItemNone, "dealer has no first item: %v", []any{g.players[0].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}}, {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)}}, {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.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}}, {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. // H*ck with the game state.
g.players[0].items[0] = ItemNone g.players[0].items[0] = itemNone
g.players[1].items[0] = ItemNone g.players[1].items[0] = itemNone
g.popShell() g.popShell()
g.popShell() g.popShell()
g.turn = 3 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.turn != i, "turn is %d, want %d", []any{g.turn, i}},
{g.damage != 1, "damage is %d, want 1", []any{g.damage}}, {g.damage != 1, "damage is %d, want 1", []any{g.damage}},
{g.reveal, "revealed at start", nil}, {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 { for _, c := range checks {
if c.failed { if c.failed {
@ -137,8 +137,8 @@ func TestGameNextTurn(t *testing.T) {
} }
g.damage = 2 g.damage = 2
g.reveal = true g.reveal = true
g.Opponent().cuffs = Cuffed g.Opponent().cuffs = cuffed
g.NextTurn() g.nextTurn()
} }
} }
@ -155,14 +155,14 @@ func TestGamePlayers(t *testing.T) {
if g.Opponent().id != dealer { if g.Opponent().id != dealer {
t.Errorf("dealer isn't opponent at start") t.Errorf("dealer isn't opponent at start")
} }
g.NextTurn() g.nextTurn()
if g.CurrentPlayer().id != dealer { if g.CurrentPlayer().id != dealer {
t.Errorf("dealer isn't current player after turn") t.Errorf("dealer isn't current player after turn")
} }
if g.Opponent().id != chall { if g.Opponent().id != chall {
t.Errorf("challenger isn't opponent after turn") t.Errorf("challenger isn't opponent after turn")
} }
g.NextTurn() g.nextTurn()
if g.CurrentPlayer().id != chall { if g.CurrentPlayer().id != chall {
t.Errorf("challenger isn't current player after two turns") 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() me, you := g.CurrentPlayer(), g.Opponent()
for i := 3; i < 1000; i++ { for i := 3; i < 1000; i++ {
g.NextTurn() g.nextTurn()
if g.CurrentPlayer() != you { if g.CurrentPlayer() != you {
t.Errorf("wrong player after %d turns", i) t.Errorf("wrong player after %d turns", i)
} }
@ -240,7 +240,7 @@ func TestGamePeek(t *testing.T) {
if g.Peek(chall) != &g.shellArray[0] { if g.Peek(chall) != &g.shellArray[0] {
t.Errorf("challenger couldn't peek on own turn") t.Errorf("challenger couldn't peek on own turn")
} }
g.NextTurn() g.nextTurn()
g.reveal = true g.reveal = true
if g.Peek(dealer) != &g.shellArray[0] { if g.Peek(dealer) != &g.shellArray[0] {
t.Errorf("dealer couldn't peek on own turn") t.Errorf("dealer couldn't peek on own turn")
@ -444,8 +444,8 @@ func TestGameDTO(t *testing.T) {
{ {
want := serve.Game{ want := serve.Game{
Players: [2]serve.Player{ Players: [2]serve.Player{
g.players[0].DTO(), g.players[0].dto(),
g.players[1].DTO(), g.players[1].dto(),
}, },
Action: "Start", Action: "Start",
Round: g.round, Round: g.round,
@ -470,8 +470,8 @@ func TestGameDTO(t *testing.T) {
{ {
want := serve.Game{ want := serve.Game{
Players: [2]serve.Player{ Players: [2]serve.Player{
g.players[0].DTO(), g.players[0].dto(),
g.players[1].DTO(), g.players[1].dto(),
}, },
Action: "Shoot", Action: "Shoot",
Round: g.round, Round: g.round,

View File

@ -1,30 +1,30 @@
package game package game
type Item int8 type item int8
const ( const (
ItemNone Item = iota itemNone item = iota
ItemLens itemLens
ItemCig itemCig
ItemBeer itemBeer
ItemCuff itemCuff
ItemKnife itemKnife
) )
func NewItem(rng *RNG) Item { func newItem(rng *xoshiro) item {
return Item(rng.Intn(5)) + 1 return item(rng.Intn(5)) + 1
} }
func (i Item) Apply(g *Match) bool { func (i item) apply(g *Match) bool {
switch i { switch i {
case ItemNone: case itemNone:
return false return false
case ItemLens: case itemLens:
// Lenses are always used, even if they have no effect. // Lenses are always used, even if they have no effect.
g.action = actLens g.action = actLens
g.reveal = true g.reveal = true
return true return true
case ItemCig: case itemCig:
cur := g.CurrentPlayer() cur := g.CurrentPlayer()
if cur.hp < g.hp { if cur.hp < g.hp {
cur.hp++ cur.hp++
@ -32,7 +32,7 @@ func (i Item) Apply(g *Match) bool {
} }
g.action = actCig g.action = actCig
return true return true
case ItemBeer: case itemBeer:
g.popShell() g.popShell()
g.action = actBeer g.action = actBeer
if g.Empty() { if g.Empty() {
@ -40,15 +40,15 @@ func (i Item) Apply(g *Match) bool {
g.NextGame() g.NextGame()
} }
return true return true
case ItemCuff: case itemCuff:
opp := g.Opponent() opp := g.Opponent()
if opp.cuffs != Uncuffed { if opp.cuffs != uncuffed {
return false return false
} }
g.action = actCuff g.action = actCuff
opp.cuffs = CuffedSkip opp.cuffs = cuffedSkip
return true return true
case ItemKnife: case itemKnife:
if g.damage != 1 { if g.damage != 1 {
return false return false
} }
@ -62,6 +62,6 @@ func (i Item) Apply(g *Match) bool {
var itemNames = [...]string{"", "🔍", "🚬", "🍺", "👮", "🔪"} var itemNames = [...]string{"", "🔍", "🚬", "🍺", "👮", "🔪"}
func (i Item) String() string { func (i item) String() string {
return itemNames[i] return itemNames[i]
} }

View File

@ -1,22 +1,18 @@
package game_test package game
import ( import "testing"
"testing"
"git.sunturtle.xyz/studio/shotgun/game"
)
func TestItemStrings(t *testing.T) { func TestItemStrings(t *testing.T) {
cases := []struct { cases := []struct {
item game.Item item item
want string want string
}{ }{
{game.ItemNone, ""}, {itemNone, ""},
{game.ItemLens, "🔍"}, {itemLens, "🔍"},
{game.ItemCig, "🚬"}, {itemCig, "🚬"},
{game.ItemBeer, "🍺"}, {itemBeer, "🍺"},
{game.ItemCuff, "👮"}, {itemCuff, "👮"},
{game.ItemKnife, "🔪"}, {itemKnife, "🔪"},
} }
for _, c := range cases { for _, c := range cases {
got := c.item.String() got := c.item.String()

View File

@ -7,33 +7,33 @@ import (
"git.sunturtle.xyz/studio/shotgun/serve" "git.sunturtle.xyz/studio/shotgun/serve"
) )
type Player struct { type playerState struct {
id player.ID id player.ID
hp int8 hp int8
items [8]Item items [8]item
cuffs CuffState cuffs cuffState
} }
func (p *Player) StartRound(hp int8) { func (p *playerState) startRound(hp int8) {
p.hp = hp p.hp = hp
clear(p.items[:]) 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++ { for i := 0; i < items; i++ {
k := slices.Index(p.items[:], ItemNone) k := slices.Index(p.items[:], itemNone)
if k < 0 { if k < 0 {
break 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{ r := serve.Player{
HP: p.hp, HP: p.hp,
Cuffs: p.cuffs != Uncuffed, Cuffs: p.cuffs != uncuffed,
} }
for k, i := range p.items { for k, i := range p.items {
r.Items[k] = i.String() r.Items[k] = i.String()
@ -41,22 +41,16 @@ func (p *Player) DTO() serve.Player {
return r return r
} }
// Is returns whether the player has the same ID as other. type cuffState uint8
// This exists for tests.
func (p *Player) Is(other *Player) bool {
return p.id == other.id
}
type CuffState uint8
const ( const (
Uncuffed CuffState = iota uncuffed cuffState = iota
Cuffed cuffed
CuffedSkip cuffedSkip
) )
func (c CuffState) NextState() CuffState { func (c cuffState) nextState() cuffState {
if c != Uncuffed { if c != uncuffed {
c-- c--
} }
return c return c

View File

@ -6,16 +6,16 @@ import (
"math/bits" "math/bits"
) )
// RNG is a random number generator for shotgun. // xoshiro is a random number generator for shotgun.
// //
// Currently xoshiro256**. Subject to change. // Currently xoshiro256**. Subject to change.
type RNG struct { type xoshiro struct {
w, x, y, z uint64 w, x, y, z uint64
} }
// NewRNG produces a new, uniquely seeded RNG. // newRNG produces a new, uniquely seeded RNG.
func NewRNG() RNG { func newRNG() xoshiro {
r := RNG{} r := xoshiro{}
b := make([]byte, 8*4) b := make([]byte, 8*4)
// Loop to ensure the state is never all-zero, which is an invalid state // Loop to ensure the state is never all-zero, which is an invalid state
// for xoshiro. // for xoshiro.
@ -30,7 +30,7 @@ func NewRNG() RNG {
} }
// Uint64 produces a 64-bit pseudo-random value. // 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 w, x, y, z := rng.w, rng.x, rng.y, rng.z
r := bits.RotateLeft64(x*5, 7) * 9 r := bits.RotateLeft64(x*5, 7) * 9
t := x << 17 t := x << 17
@ -45,7 +45,7 @@ func (rng *RNG) Uint64() uint64 {
} }
// Intn produces an int in [0, n). Panics if n <= 0. // 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 { if n <= 0 {
panic("shotgun: rng.Intn max below zero") panic("shotgun: rng.Intn max below zero")
} }
@ -57,7 +57,7 @@ func (rng *RNG) Intn(n int) int {
return int(x % uint(n)) 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-- { for i := len(s) - 1; i > 0; i-- {
j := rng.Intn(i + 1) j := rng.Intn(i + 1)
s[i], s[j] = s[j], s[i] s[i], s[j] = s[j], s[i]

10
main.go
View File

@ -48,11 +48,11 @@ func main() {
} }
r := chi.NewRouter() r := chi.NewRouter()
r.Post("/user/register", s.Register) r.Post("/user/register", s.register)
r.Post("/user/login", s.Login) r.Post("/user/login", s.login)
r.With(serve.WithSession(db)).Get("/user/logout", s.Logout) 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("/user/me", s.me)
r.With(serve.WithSession(db)).Get("/queue", s.Queue) 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))) 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) slog.Info("listening", "addr", addr, "public", public)
http.ListenAndServe(addr, r) http.ListenAndServe(addr, r)

View File

@ -44,7 +44,7 @@ type Player struct {
Cuffs bool `json:"cuffs,omitempty"` 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. // GameStart is the JSON DTO given to each player when their game starts.
// Observers do not receive it. // Observers do not receive it.

View File

@ -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 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() ctx := r.Context()
slog := slog.With( slog := slog.With(
slog.String("remote", r.RemoteAddr), 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) 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() ctx := r.Context()
slog := slog.With( slog := slog.With(
slog.String("remote", r.RemoteAddr), 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) 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() ctx := r.Context()
slog := slog.With( slog := slog.With(
slog.String("remote", r.RemoteAddr), 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) 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()) id := serve.ReqPlayer(r.Context())
if id == (player.ID{}) { if id == (player.ID{}) {
panic("missing 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) 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]. // 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()) p := serve.ReqPlayer(r.Context())
person, err := s.accept(w, r, p) person, err := s.accept(w, r, p)
if err != nil { if err != nil {
@ -201,7 +201,7 @@ func (s *Server) joinAndServe(p person) {
} }
// Reply with the game ID so they can share. // Reply with the game ID so they can share.
r := serve.GameStart{ r := serve.GameStart{
ID: id, ID: serve.GameID{UUID: id},
Dealer: deal, Dealer: deal,
} }
if err := wsjson.Write(context.TODO(), p.conn, r); err != nil { if err := wsjson.Write(context.TODO(), p.conn, r); err != nil {