refactor: extract shared player.ID type and GenerateID to player package

Both game and snake packages had identical PlayerID types and the snake
package imported game.GenerateID. Now both use player.ID and
player.GenerateID from the shared player package.
This commit is contained in:
Ryan Hamamura
2026-03-02 19:09:01 -10:00
parent f47eb4cdf3
commit 063b03ce25
9 changed files with 61 additions and 45 deletions

View File

@@ -18,6 +18,7 @@ import (
"github.com/ryanhamamura/c4/features/c4game/components" "github.com/ryanhamamura/c4/features/c4game/components"
"github.com/ryanhamamura/c4/features/c4game/pages" "github.com/ryanhamamura/c4/features/c4game/pages"
"github.com/ryanhamamura/c4/game" "github.com/ryanhamamura/c4/game"
"github.com/ryanhamamura/c4/player"
) )
func HandleGamePage(store *game.GameStore, sessions *scs.SessionManager, queries *repository.Queries) http.HandlerFunc { func HandleGamePage(store *game.GameStore, sessions *scs.SessionManager, queries *repository.Queries) http.HandlerFunc {
@@ -30,15 +31,15 @@ func HandleGamePage(store *game.GameStore, sessions *scs.SessionManager, queries
return return
} }
playerID := game.PlayerID(sessions.GetString(r.Context(), "player_id")) playerID := player.ID(sessions.GetString(r.Context(), "player_id"))
if playerID == "" { if playerID == "" {
playerID = game.PlayerID(game.GenerateID(8)) playerID = player.ID(player.GenerateID(8))
sessions.Put(r.Context(), "player_id", string(playerID)) sessions.Put(r.Context(), "player_id", string(playerID))
} }
userID := sessions.GetString(r.Context(), "user_id") userID := sessions.GetString(r.Context(), "user_id")
if userID != "" { if userID != "" {
playerID = game.PlayerID(userID) playerID = player.ID(userID)
} }
nickname := sessions.GetString(r.Context(), "nickname") nickname := sessions.GetString(r.Context(), "nickname")
@@ -95,10 +96,10 @@ func HandleGameEvents(store *game.GameStore, nc *nats.Conn, sessions *scs.Sessio
return return
} }
playerID := game.PlayerID(sessions.GetString(r.Context(), "player_id")) playerID := player.ID(sessions.GetString(r.Context(), "player_id"))
userID := sessions.GetString(r.Context(), "user_id") userID := sessions.GetString(r.Context(), "user_id")
if userID != "" { if userID != "" {
playerID = game.PlayerID(userID) playerID = player.ID(userID)
} }
myColor := gi.GetPlayerColor(playerID) myColor := gi.GetPlayerColor(playerID)
@@ -185,10 +186,10 @@ func HandleDropPiece(store *game.GameStore, sessions *scs.SessionManager) http.H
return return
} }
playerID := game.PlayerID(sessions.GetString(r.Context(), "player_id")) playerID := player.ID(sessions.GetString(r.Context(), "player_id"))
userID := sessions.GetString(r.Context(), "user_id") userID := sessions.GetString(r.Context(), "user_id")
if userID != "" { if userID != "" {
playerID = game.PlayerID(userID) playerID = player.ID(userID)
} }
myColor := gi.GetPlayerColor(playerID) myColor := gi.GetPlayerColor(playerID)
@@ -229,10 +230,10 @@ func HandleSendChat(store *game.GameStore, nc *nats.Conn, sessions *scs.SessionM
return return
} }
playerID := game.PlayerID(sessions.GetString(r.Context(), "player_id")) playerID := player.ID(sessions.GetString(r.Context(), "player_id"))
userID := sessions.GetString(r.Context(), "user_id") userID := sessions.GetString(r.Context(), "user_id")
if userID != "" { if userID != "" {
playerID = game.PlayerID(userID) playerID = player.ID(userID)
} }
myColor := gi.GetPlayerColor(playerID) myColor := gi.GetPlayerColor(playerID)
@@ -298,10 +299,10 @@ func HandleSetNickname(store *game.GameStore, sessions *scs.SessionManager) http
sessions.Put(r.Context(), "nickname", signals.Nickname) sessions.Put(r.Context(), "nickname", signals.Nickname)
playerID := game.PlayerID(sessions.GetString(r.Context(), "player_id")) playerID := player.ID(sessions.GetString(r.Context(), "player_id"))
userID := sessions.GetString(r.Context(), "user_id") userID := sessions.GetString(r.Context(), "user_id")
if userID != "" { if userID != "" {
playerID = game.PlayerID(userID) playerID = player.ID(userID)
} }
if gi.GetPlayerColor(playerID) == 0 { if gi.GetPlayerColor(playerID) == 0 {

View File

@@ -13,21 +13,21 @@ import (
"github.com/ryanhamamura/c4/features/snakegame/components" "github.com/ryanhamamura/c4/features/snakegame/components"
"github.com/ryanhamamura/c4/features/snakegame/pages" "github.com/ryanhamamura/c4/features/snakegame/pages"
"github.com/ryanhamamura/c4/game" "github.com/ryanhamamura/c4/player"
"github.com/ryanhamamura/c4/snake" "github.com/ryanhamamura/c4/snake"
) )
func getPlayerID(sessions *scs.SessionManager, r *http.Request) snake.PlayerID { func getPlayerID(sessions *scs.SessionManager, r *http.Request) player.ID {
pid := sessions.GetString(r.Context(), "player_id") pid := sessions.GetString(r.Context(), "player_id")
if pid == "" { if pid == "" {
pid = game.GenerateID(8) pid = player.GenerateID(8)
sessions.Put(r.Context(), "player_id", pid) sessions.Put(r.Context(), "player_id", pid)
} }
userID := sessions.GetString(r.Context(), "user_id") userID := sessions.GetString(r.Context(), "user_id")
if userID != "" { if userID != "" {
return snake.PlayerID(userID) return player.ID(userID)
} }
return snake.PlayerID(pid) return player.ID(pid)
} }
func HandleSnakePage(snakeStore *snake.SnakeStore, sessions *scs.SessionManager) http.HandlerFunc { func HandleSnakePage(snakeStore *snake.SnakeStore, sessions *scs.SessionManager) http.HandlerFunc {

View File

@@ -4,6 +4,7 @@ import (
"context" "context"
"github.com/ryanhamamura/c4/db/repository" "github.com/ryanhamamura/c4/db/repository"
"github.com/ryanhamamura/c4/player"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
@@ -109,19 +110,19 @@ func gameFromRow(row *repository.Game) (*Game, error) {
func playersFromRows(rows []*repository.GamePlayer) []*Player { func playersFromRows(rows []*repository.GamePlayer) []*Player {
players := make([]*Player, 0, len(rows)) players := make([]*Player, 0, len(rows))
for _, row := range rows { for _, row := range rows {
player := &Player{ p := &Player{
Nickname: row.Nickname, Nickname: row.Nickname,
Color: int(row.Color), Color: int(row.Color),
} }
if row.UserID != nil { if row.UserID != nil {
player.UserID = row.UserID p.UserID = row.UserID
player.ID = PlayerID(*row.UserID) p.ID = player.ID(*row.UserID)
} else if row.GuestPlayerID != nil { } else if row.GuestPlayerID != nil {
player.ID = PlayerID(*row.GuestPlayerID) p.ID = player.ID(*row.GuestPlayerID)
} }
players = append(players, player) players = append(players, p)
} }
return players return players
} }

View File

@@ -2,11 +2,10 @@ package game
import ( import (
"context" "context"
"crypto/rand"
"encoding/hex"
"sync" "sync"
"github.com/ryanhamamura/c4/db/repository" "github.com/ryanhamamura/c4/db/repository"
"github.com/ryanhamamura/c4/player"
) )
type PlayerSession struct { type PlayerSession struct {
@@ -40,7 +39,7 @@ func (gs *GameStore) makeNotify(gameID string) func() {
} }
func (gs *GameStore) Create() *GameInstance { func (gs *GameStore) Create() *GameInstance {
id := GenerateID(4) id := player.GenerateID(4)
gi := NewGameInstance(id) gi := NewGameInstance(id)
gi.queries = gs.queries gi.queries = gs.queries
gi.notify = gs.makeNotify(id) gi.notify = gs.makeNotify(id)
@@ -107,12 +106,6 @@ func (gs *GameStore) Delete(id string) error {
return nil return nil
} }
func GenerateID(size int) string {
b := make([]byte, size)
_, _ = rand.Read(b)
return hex.EncodeToString(b)
}
type GameInstance struct { type GameInstance struct {
game *Game game *Game
gameMu sync.RWMutex gameMu sync.RWMutex
@@ -166,7 +159,7 @@ func (gi *GameInstance) GetGame() *Game {
return gi.game return gi.game
} }
func (gi *GameInstance) GetPlayerColor(pid PlayerID) int { func (gi *GameInstance) GetPlayerColor(pid player.ID) int {
gi.gameMu.RLock() gi.gameMu.RLock()
defer gi.gameMu.RUnlock() defer gi.gameMu.RUnlock()
for _, p := range gi.game.Players { for _, p := range gi.game.Players {

View File

@@ -1,11 +1,13 @@
package game package game
import "encoding/json" import (
"encoding/json"
type PlayerID string "github.com/ryanhamamura/c4/player"
)
type Player struct { type Player struct {
ID PlayerID ID player.ID
UserID *string // UUID for authenticated users, nil for guests UserID *string // UUID for authenticated users, nil for guests
Nickname string Nickname string
Color int // 1 = Red, 2 = Yellow Color int // 1 = Red, 2 = Yellow

18
player/player.go Normal file
View File

@@ -0,0 +1,18 @@
// Package player provides shared identity types used across game packages.
package player
import (
"crypto/rand"
"encoding/hex"
)
// ID uniquely identifies a player within a session. For authenticated users
// this is their user UUID; for guests it's a random hex string.
type ID string
// GenerateID returns a random hex string of 2*size characters.
func GenerateID(size int) string {
b := make([]byte, size)
_, _ = rand.Read(b)
return hex.EncodeToString(b)
}

View File

@@ -4,6 +4,7 @@ import (
"context" "context"
"github.com/ryanhamamura/c4/db/repository" "github.com/ryanhamamura/c4/db/repository"
"github.com/ryanhamamura/c4/player"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
@@ -122,19 +123,19 @@ func snakeGameFromRow(row *repository.Game) (*SnakeGame, error) {
func snakePlayersFromRows(rows []*repository.GamePlayer) []*Player { func snakePlayersFromRows(rows []*repository.GamePlayer) []*Player {
players := make([]*Player, 0, len(rows)) players := make([]*Player, 0, len(rows))
for _, row := range rows { for _, row := range rows {
player := &Player{ p := &Player{
Nickname: row.Nickname, Nickname: row.Nickname,
Slot: int(row.Slot), Slot: int(row.Slot),
} }
if row.UserID != nil { if row.UserID != nil {
player.UserID = row.UserID p.UserID = row.UserID
player.ID = PlayerID(*row.UserID) p.ID = player.ID(*row.UserID)
} else if row.GuestPlayerID != nil { } else if row.GuestPlayerID != nil {
player.ID = PlayerID(*row.GuestPlayerID) p.ID = player.ID(*row.GuestPlayerID)
} }
players = append(players, player) players = append(players, p)
} }
return players return players
} }

View File

@@ -5,7 +5,7 @@ import (
"sync" "sync"
"github.com/ryanhamamura/c4/db/repository" "github.com/ryanhamamura/c4/db/repository"
"github.com/ryanhamamura/c4/game" "github.com/ryanhamamura/c4/player"
) )
type SnakeStore struct { type SnakeStore struct {
@@ -38,7 +38,7 @@ func (ss *SnakeStore) Create(width, height int, mode GameMode, speed int) *Snake
if speed <= 0 { if speed <= 0 {
speed = DefaultSpeed speed = DefaultSpeed
} }
id := game.GenerateID(4) id := player.GenerateID(4)
sg := &SnakeGame{ sg := &SnakeGame{
ID: id, ID: id,
State: &GameState{ State: &GameState{
@@ -172,7 +172,7 @@ func (si *SnakeGameInstance) GetGame() *SnakeGame {
return si.game.snapshot() return si.game.snapshot()
} }
func (si *SnakeGameInstance) GetPlayerSlot(pid PlayerID) int { func (si *SnakeGameInstance) GetPlayerSlot(pid player.ID) int {
si.gameMu.RLock() si.gameMu.RLock()
defer si.gameMu.RUnlock() defer si.gameMu.RUnlock()
for i, p := range si.game.Players { for i, p := range si.game.Players {

View File

@@ -3,6 +3,8 @@ package snake
import ( import (
"encoding/json" "encoding/json"
"time" "time"
"github.com/ryanhamamura/c4/player"
) )
type Direction int type Direction int
@@ -78,10 +80,8 @@ const (
StatusFinished StatusFinished
) )
type PlayerID string
type Player struct { type Player struct {
ID PlayerID ID player.ID
UserID *string UserID *string
Nickname string Nickname string
Slot int // 0-7 Slot int // 0-7