refactor: rename game package to connect4, drop Game prefix from types
All checks were successful
CI / Deploy / test (pull_request) Successful in 16s
CI / Deploy / lint (pull_request) Successful in 25s
CI / Deploy / deploy (pull_request) Has been skipped

Rename game/ -> connect4/ to avoid c4/game stutter. Drop redundant
Game prefix from exported types (GameStore -> Store, GameInstance ->
Instance, GameStatus -> Status). Rename NATS subjects from game.{id}
to connect4.{id}. URL routes unchanged.
This commit is contained in:
Ryan Hamamura
2026-03-02 20:31:00 -10:00
parent f71acfc73e
commit 38eb9ee398
14 changed files with 125 additions and 127 deletions

View File

@@ -1,5 +1,5 @@
// Package game implements Connect 4 game logic, state management, and persistence. // Package connect4 implements Connect 4 game logic, state management, and persistence.
package game package connect4
// DropPiece attempts to drop a piece in the given column. // DropPiece attempts to drop a piece in the given column.
// Returns (row placed, success). // Returns (row placed, success).

View File

@@ -1,4 +1,4 @@
package game package connect4
import ( import (
"context" "context"
@@ -9,7 +9,7 @@ import (
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
) )
func (gi *GameInstance) save() error { func (gi *Instance) save() error {
err := saveGame(gi.queries, gi.game) err := saveGame(gi.queries, gi.game)
if err != nil { if err != nil {
log.Error().Err(err).Str("game_id", gi.game.ID).Msg("failed to save game") log.Error().Err(err).Str("game_id", gi.game.ID).Msg("failed to save game")
@@ -17,8 +17,8 @@ func (gi *GameInstance) save() error {
return err return err
} }
func (gi *GameInstance) savePlayer(player *Player, slot int) error { func (gi *Instance) savePlayer(p *Player, slot int) error {
err := saveGamePlayer(gi.queries, gi.game.ID, player, slot) err := saveGamePlayer(gi.queries, gi.game.ID, p, slot)
if err != nil { if err != nil {
log.Error().Err(err).Str("game_id", gi.game.ID).Int("slot", slot).Msg("failed to save game player") log.Error().Err(err).Str("game_id", gi.game.ID).Int("slot", slot).Msg("failed to save game player")
} }
@@ -48,12 +48,12 @@ func saveGame(queries *repository.Queries, g *Game) error {
}) })
} }
func saveGamePlayer(queries *repository.Queries, gameID string, player *Player, slot int) error { func saveGamePlayer(queries *repository.Queries, gameID string, p *Player, slot int) error {
var userID, guestPlayerID *string var userID, guestPlayerID *string
if player.UserID != nil { if p.UserID != nil {
userID = player.UserID userID = p.UserID
} else { } else {
id := string(player.ID) id := string(p.ID)
guestPlayerID = &id guestPlayerID = &id
} }
@@ -61,8 +61,8 @@ func saveGamePlayer(queries *repository.Queries, gameID string, player *Player,
GameID: gameID, GameID: gameID,
UserID: userID, UserID: userID,
GuestPlayerID: guestPlayerID, GuestPlayerID: guestPlayerID,
Nickname: player.Nickname, Nickname: p.Nickname,
Color: int64(player.Color), Color: int64(p.Color),
Slot: int64(slot), Slot: int64(slot),
}) })
} }
@@ -83,13 +83,11 @@ func loadGamePlayers(queries *repository.Queries, id string) ([]*Player, error)
return playersFromRows(rows), nil return playersFromRows(rows), nil
} }
// Domain ↔ DB mapping helpers.
func gameFromRow(row *repository.Game) (*Game, error) { func gameFromRow(row *repository.Game) (*Game, error) {
g := &Game{ g := &Game{
ID: row.ID, ID: row.ID,
CurrentTurn: int(row.CurrentTurn), CurrentTurn: int(row.CurrentTurn),
Status: GameStatus(row.Status), Status: Status(row.Status),
} }
if err := g.BoardFromJSON(row.Board); err != nil { if err := g.BoardFromJSON(row.Board); err != nil {

View File

@@ -1,4 +1,4 @@
package game package connect4
import ( import (
"context" "context"
@@ -12,67 +12,67 @@ type PlayerSession struct {
Player *Player Player *Player
} }
type GameStore struct { type Store struct {
games map[string]*GameInstance games map[string]*Instance
gamesMu sync.RWMutex gamesMu sync.RWMutex
queries *repository.Queries queries *repository.Queries
notifyFunc func(gameID string) notifyFunc func(gameID string)
} }
func NewGameStore(queries *repository.Queries) *GameStore { func NewStore(queries *repository.Queries) *Store {
return &GameStore{ return &Store{
games: make(map[string]*GameInstance), games: make(map[string]*Instance),
queries: queries, queries: queries,
} }
} }
func (gs *GameStore) SetNotifyFunc(f func(gameID string)) { func (s *Store) SetNotifyFunc(f func(gameID string)) {
gs.notifyFunc = f s.notifyFunc = f
} }
func (gs *GameStore) makeNotify(gameID string) func() { func (s *Store) makeNotify(gameID string) func() {
return func() { return func() {
if gs.notifyFunc != nil { if s.notifyFunc != nil {
gs.notifyFunc(gameID) s.notifyFunc(gameID)
} }
} }
} }
func (gs *GameStore) Create() *GameInstance { func (s *Store) Create() *Instance {
id := player.GenerateID(4) id := player.GenerateID(4)
gi := NewGameInstance(id) gi := NewInstance(id)
gi.queries = gs.queries gi.queries = s.queries
gi.notify = gs.makeNotify(id) gi.notify = s.makeNotify(id)
gs.gamesMu.Lock() s.gamesMu.Lock()
gs.games[id] = gi s.games[id] = gi
gs.gamesMu.Unlock() s.gamesMu.Unlock()
if gs.queries != nil { if s.queries != nil {
gi.save() //nolint:errcheck gi.save() //nolint:errcheck
} }
return gi return gi
} }
func (gs *GameStore) Get(id string) (*GameInstance, bool) { func (s *Store) Get(id string) (*Instance, bool) {
gs.gamesMu.RLock() s.gamesMu.RLock()
gi, ok := gs.games[id] gi, ok := s.games[id]
gs.gamesMu.RUnlock() s.gamesMu.RUnlock()
if ok { if ok {
return gi, true return gi, true
} }
if gs.queries == nil { if s.queries == nil {
return nil, false return nil, false
} }
g, err := loadGame(gs.queries, id) g, err := loadGame(s.queries, id)
if err != nil || g == nil { if err != nil || g == nil {
return nil, false return nil, false
} }
players, _ := loadGamePlayers(gs.queries, id) players, _ := loadGamePlayers(s.queries, id)
for _, p := range players { for _, p := range players {
switch p.Color { switch p.Color {
case 1: case 1:
@@ -82,51 +82,51 @@ func (gs *GameStore) Get(id string) (*GameInstance, bool) {
} }
} }
gi = &GameInstance{ gi = &Instance{
game: g, game: g,
queries: gs.queries, queries: s.queries,
notify: gs.makeNotify(id), notify: s.makeNotify(id),
} }
gs.gamesMu.Lock() s.gamesMu.Lock()
gs.games[id] = gi s.games[id] = gi
gs.gamesMu.Unlock() s.gamesMu.Unlock()
return gi, true return gi, true
} }
func (gs *GameStore) Delete(id string) error { func (s *Store) Delete(id string) error {
gs.gamesMu.Lock() s.gamesMu.Lock()
delete(gs.games, id) delete(s.games, id)
gs.gamesMu.Unlock() s.gamesMu.Unlock()
if gs.queries != nil { if s.queries != nil {
return gs.queries.DeleteGame(context.Background(), id) return s.queries.DeleteGame(context.Background(), id)
} }
return nil return nil
} }
type GameInstance struct { type Instance struct {
game *Game game *Game
gameMu sync.RWMutex gameMu sync.RWMutex
notify func() notify func()
queries *repository.Queries queries *repository.Queries
} }
func NewGameInstance(id string) *GameInstance { func NewInstance(id string) *Instance {
return &GameInstance{ return &Instance{
game: NewGame(id), game: NewGame(id),
notify: func() {}, notify: func() {},
} }
} }
func (gi *GameInstance) ID() string { func (gi *Instance) ID() string {
gi.gameMu.RLock() gi.gameMu.RLock()
defer gi.gameMu.RUnlock() defer gi.gameMu.RUnlock()
return gi.game.ID return gi.game.ID
} }
func (gi *GameInstance) Join(ps *PlayerSession) bool { func (gi *Instance) Join(ps *PlayerSession) bool {
gi.gameMu.Lock() gi.gameMu.Lock()
defer gi.gameMu.Unlock() defer gi.gameMu.Unlock()
@@ -153,13 +153,13 @@ func (gi *GameInstance) Join(ps *PlayerSession) bool {
return true return true
} }
func (gi *GameInstance) GetGame() *Game { func (gi *Instance) GetGame() *Game {
gi.gameMu.RLock() gi.gameMu.RLock()
defer gi.gameMu.RUnlock() defer gi.gameMu.RUnlock()
return gi.game return gi.game
} }
func (gi *GameInstance) GetPlayerColor(pid player.ID) int { func (gi *Instance) 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 {
@@ -170,7 +170,7 @@ func (gi *GameInstance) GetPlayerColor(pid player.ID) int {
return 0 return 0
} }
func (gi *GameInstance) CreateRematch(gs *GameStore) *GameInstance { func (gi *Instance) CreateRematch(s *Store) *Instance {
gi.gameMu.Lock() gi.gameMu.Lock()
defer gi.gameMu.Unlock() defer gi.gameMu.Unlock()
@@ -178,13 +178,13 @@ func (gi *GameInstance) CreateRematch(gs *GameStore) *GameInstance {
return nil return nil
} }
newGI := gs.Create() newGI := s.Create()
newID := newGI.ID() newID := newGI.ID()
gi.game.RematchGameID = &newID gi.game.RematchGameID = &newID
if gi.queries != nil { if gi.queries != nil {
if err := gi.save(); err != nil { if err := gi.save(); err != nil {
gs.Delete(newID) //nolint:errcheck s.Delete(newID) //nolint:errcheck
gi.game.RematchGameID = nil gi.game.RematchGameID = nil
return nil return nil
} }
@@ -194,7 +194,7 @@ func (gi *GameInstance) CreateRematch(gs *GameStore) *GameInstance {
return newGI return newGI
} }
func (gi *GameInstance) DropPiece(col int, playerColor int) bool { func (gi *Instance) DropPiece(col int, playerColor int) bool {
gi.gameMu.Lock() gi.gameMu.Lock()
defer gi.gameMu.Unlock() defer gi.gameMu.Unlock()

View File

@@ -1,4 +1,4 @@
package game package connect4
import ( import (
"encoding/json" "encoding/json"
@@ -13,10 +13,10 @@ type Player struct {
Color int // 1 = Red, 2 = Yellow Color int // 1 = Red, 2 = Yellow
} }
type GameStatus int type Status int
const ( const (
StatusWaitingForPlayer GameStatus = iota StatusWaitingForPlayer Status = iota
StatusInProgress StatusInProgress
StatusWon StatusWon
StatusDraw StatusDraw
@@ -27,7 +27,7 @@ type Game struct {
Board [6][7]int // 6 rows, 7 columns; 0=empty, 1=red, 2=yellow Board [6][7]int // 6 rows, 7 columns; 0=empty, 1=red, 2=yellow
Players [2]*Player // Index 0 = creator (Red), Index 1 = joiner (Yellow) Players [2]*Player // Index 0 = creator (Red), Index 1 = joiner (Yellow)
CurrentTurn int // 1 or 2 (matches player color) CurrentTurn int // 1 or 2 (matches player color)
Status GameStatus Status Status
Winner *Player Winner *Player
WinningCells [][2]int // Coordinates of winning 4 cells for highlighting WinningCells [][2]int // Coordinates of winning 4 cells for highlighting
RematchGameID *string // ID of the rematch game, if one was created RematchGameID *string // ID of the rematch game, if one was created

View File

@@ -3,11 +3,11 @@ package components
import ( import (
"fmt" "fmt"
"github.com/ryanhamamura/c4/game" "github.com/ryanhamamura/c4/connect4"
"github.com/starfederation/datastar-go/datastar" "github.com/starfederation/datastar-go/datastar"
) )
templ Board(g *game.Game, myColor int) { templ Board(g *connect4.Game, myColor int) {
<div id="c4-board" class="board"> <div id="c4-board" class="board">
for col := 0; col < 7; col++ { for col := 0; col < 7; col++ {
@column(g, col, myColor) @column(g, col, myColor)
@@ -15,8 +15,8 @@ templ Board(g *game.Game, myColor int) {
</div> </div>
} }
templ column(g *game.Game, colIdx int, myColor int) { templ column(g *connect4.Game, colIdx int, myColor int) {
if g.Status == game.StatusInProgress && myColor == g.CurrentTurn { if g.Status == connect4.StatusInProgress && myColor == g.CurrentTurn {
<div <div
class="column clickable" class="column clickable"
data-on:click={ datastar.PostSSE("/games/%s/drop?col=%d", g.ID, colIdx) } data-on:click={ datastar.PostSSE("/games/%s/drop?col=%d", g.ID, colIdx) }
@@ -34,14 +34,14 @@ templ column(g *game.Game, colIdx int, myColor int) {
} }
} }
templ cell(g *game.Game, row int, col int) { templ cell(g *connect4.Game, row int, col int) {
<div class={ cellClass(g, row, col) }></div> <div class={ cellClass(g, row, col) }></div>
} }
func cellClass(g *game.Game, row, col int) string { func cellClass(g *connect4.Game, row, col int) string {
color := g.Board[row][col] color := g.Board[row][col]
activeTurn := 0 activeTurn := 0
if g.Status == game.StatusInProgress { if g.Status == connect4.StatusInProgress {
activeTurn = g.CurrentTurn activeTurn = g.CurrentTurn
} }

View File

@@ -2,11 +2,11 @@ package components
import ( import (
"github.com/ryanhamamura/c4/config" "github.com/ryanhamamura/c4/config"
"github.com/ryanhamamura/c4/game" "github.com/ryanhamamura/c4/connect4"
"github.com/starfederation/datastar-go/datastar" "github.com/starfederation/datastar-go/datastar"
) )
templ StatusBanner(g *game.Game, myColor int) { templ StatusBanner(g *connect4.Game, myColor int) {
<div id="c4-status" class={ statusClass(g, myColor) }> <div id="c4-status" class={ statusClass(g, myColor) }>
{ statusMessage(g, myColor) } { statusMessage(g, myColor) }
if g.IsFinished() { if g.IsFinished() {
@@ -30,7 +30,7 @@ templ StatusBanner(g *game.Game, myColor int) {
</div> </div>
} }
templ PlayerInfo(g *game.Game, myColor int) { templ PlayerInfo(g *connect4.Game, myColor int) {
<div id="c4-players" class="flex gap-8 mb-2"> <div id="c4-players" class="flex gap-8 mb-2">
for _, info := range playerInfoPairs(g, myColor) { for _, info := range playerInfoPairs(g, myColor) {
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
@@ -61,36 +61,36 @@ script copyToClipboard(url string) {
navigator.clipboard.writeText(url) navigator.clipboard.writeText(url)
} }
func statusClass(g *game.Game, myColor int) string { func statusClass(g *connect4.Game, myColor int) string {
switch g.Status { switch g.Status {
case game.StatusWaitingForPlayer: case connect4.StatusWaitingForPlayer:
return "alert bg-base-200 text-xl font-bold" return "alert bg-base-200 text-xl font-bold"
case game.StatusInProgress: case connect4.StatusInProgress:
if g.CurrentTurn == myColor { if g.CurrentTurn == myColor {
return "alert alert-success text-xl font-bold" return "alert alert-success text-xl font-bold"
} }
return "alert bg-base-200 text-xl font-bold" return "alert bg-base-200 text-xl font-bold"
case game.StatusWon: case connect4.StatusWon:
if g.Winner != nil && g.Winner.Color == myColor { if g.Winner != nil && g.Winner.Color == myColor {
return "alert alert-success text-xl font-bold" return "alert alert-success text-xl font-bold"
} }
return "alert alert-error text-xl font-bold" return "alert alert-error text-xl font-bold"
case game.StatusDraw: case connect4.StatusDraw:
return "alert alert-warning text-xl font-bold" return "alert alert-warning text-xl font-bold"
} }
return "alert bg-base-200 text-xl font-bold" return "alert bg-base-200 text-xl font-bold"
} }
func statusMessage(g *game.Game, myColor int) string { func statusMessage(g *connect4.Game, myColor int) string {
switch g.Status { switch g.Status {
case game.StatusWaitingForPlayer: case connect4.StatusWaitingForPlayer:
return "Waiting for opponent..." return "Waiting for opponent..."
case game.StatusInProgress: case connect4.StatusInProgress:
if g.CurrentTurn == myColor { if g.CurrentTurn == myColor {
return "Your turn!" return "Your turn!"
} }
return opponentName(g, myColor) + "'s turn" return opponentName(g, myColor) + "'s turn"
case game.StatusWon: case connect4.StatusWon:
if g.Winner != nil && g.Winner.Color == myColor { if g.Winner != nil && g.Winner.Color == myColor {
return "You win!" return "You win!"
} }
@@ -98,13 +98,13 @@ func statusMessage(g *game.Game, myColor int) string {
return g.Winner.Nickname + " wins!" return g.Winner.Nickname + " wins!"
} }
return "Game over" return "Game over"
case game.StatusDraw: case connect4.StatusDraw:
return "It's a draw!" return "It's a draw!"
} }
return "" return ""
} }
func opponentName(g *game.Game, myColor int) string { func opponentName(g *connect4.Game, myColor int) string {
for _, p := range g.Players { for _, p := range g.Players {
if p != nil && p.Color != myColor { if p != nil && p.Color != myColor {
return p.Nickname return p.Nickname
@@ -118,7 +118,7 @@ type playerInfoData struct {
Label string Label string
} }
func playerInfoPairs(g *game.Game, myColor int) []playerInfoData { func playerInfoPairs(g *connect4.Game, myColor int) []playerInfoData {
var result []playerInfoData var result []playerInfoData
var myName, oppName string var myName, oppName string

View File

@@ -13,10 +13,10 @@ import (
"github.com/ryanhamamura/c4/chat" "github.com/ryanhamamura/c4/chat"
chatcomponents "github.com/ryanhamamura/c4/chat/components" chatcomponents "github.com/ryanhamamura/c4/chat/components"
"github.com/ryanhamamura/c4/connect4"
"github.com/ryanhamamura/c4/db/repository" "github.com/ryanhamamura/c4/db/repository"
"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/sessions" "github.com/ryanhamamura/c4/sessions"
) )
@@ -41,7 +41,7 @@ func c4ChatConfig(gameID string) chatcomponents.Config {
} }
} }
func HandleGamePage(store *game.GameStore, sm *scs.SessionManager, queries *repository.Queries) http.HandlerFunc { func HandleGamePage(store *connect4.Store, sm *scs.SessionManager, queries *repository.Queries) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
gameID := chi.URLParam(r, "id") gameID := chi.URLParam(r, "id")
@@ -57,14 +57,14 @@ func HandleGamePage(store *game.GameStore, sm *scs.SessionManager, queries *repo
// Auto-join if player has a nickname but isn't in the game yet // Auto-join if player has a nickname but isn't in the game yet
if nickname != "" && gi.GetPlayerColor(playerID) == 0 { if nickname != "" && gi.GetPlayerColor(playerID) == 0 {
p := &game.Player{ p := &connect4.Player{
ID: playerID, ID: playerID,
Nickname: nickname, Nickname: nickname,
} }
if userID != "" { if userID != "" {
p.UserID = &userID p.UserID = &userID
} }
gi.Join(&game.PlayerSession{Player: p}) gi.Join(&connect4.PlayerSession{Player: p})
} }
myColor := gi.GetPlayerColor(playerID) myColor := gi.GetPlayerColor(playerID)
@@ -93,7 +93,7 @@ func HandleGamePage(store *game.GameStore, sm *scs.SessionManager, queries *repo
} }
} }
func HandleGameEvents(store *game.GameStore, nc *nats.Conn, sm *scs.SessionManager, queries *repository.Queries) http.HandlerFunc { func HandleGameEvents(store *connect4.Store, nc *nats.Conn, sm *scs.SessionManager, queries *repository.Queries) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
gameID := chi.URLParam(r, "id") gameID := chi.URLParam(r, "id")
@@ -111,14 +111,14 @@ func HandleGameEvents(store *game.GameStore, nc *nats.Conn, sm *scs.SessionManag
)) ))
chatCfg := c4ChatConfig(gameID) chatCfg := c4ChatConfig(gameID)
room := chat.NewRoom(nc, "game.chat."+gameID, chat.LoadMessages(queries, gameID)) room := chat.NewRoom(nc, "connect4.chat."+gameID, chat.LoadMessages(queries, gameID))
// Send initial render // Send initial render
sendGameComponents(sse, gi, myColor, room, chatCfg) sendGameComponents(sse, gi, myColor, room, chatCfg)
// Subscribe to game state updates // Subscribe to game state updates
gameCh := make(chan *nats.Msg, 64) gameCh := make(chan *nats.Msg, 64)
gameSub, err := nc.ChanSubscribe("game."+gameID, gameCh) gameSub, err := nc.ChanSubscribe("connect4."+gameID, gameCh)
if err != nil { if err != nil {
return return
} }
@@ -152,7 +152,7 @@ func HandleGameEvents(store *game.GameStore, nc *nats.Conn, sm *scs.SessionManag
} }
} }
func HandleDropPiece(store *game.GameStore, sm *scs.SessionManager) http.HandlerFunc { func HandleDropPiece(store *connect4.Store, sm *scs.SessionManager) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
gameID := chi.URLParam(r, "id") gameID := chi.URLParam(r, "id")
@@ -181,7 +181,7 @@ func HandleDropPiece(store *game.GameStore, sm *scs.SessionManager) http.Handler
} }
} }
func HandleSendChat(store *game.GameStore, nc *nats.Conn, sm *scs.SessionManager, queries *repository.Queries) http.HandlerFunc { func HandleSendChat(store *connect4.Store, nc *nats.Conn, sm *scs.SessionManager, queries *repository.Queries) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
gameID := chi.URLParam(r, "id") gameID := chi.URLParam(r, "id")
@@ -230,7 +230,7 @@ func HandleSendChat(store *game.GameStore, nc *nats.Conn, sm *scs.SessionManager
} }
chat.SaveMessage(queries, gameID, msg) chat.SaveMessage(queries, gameID, msg)
room := chat.NewRoom(nc, "game.chat."+gameID, nil) room := chat.NewRoom(nc, "connect4.chat."+gameID, nil)
room.Send(msg) room.Send(msg)
sse := datastar.NewSSE(w, r) sse := datastar.NewSSE(w, r)
@@ -238,7 +238,7 @@ func HandleSendChat(store *game.GameStore, nc *nats.Conn, sm *scs.SessionManager
} }
} }
func HandleSetNickname(store *game.GameStore, sm *scs.SessionManager) http.HandlerFunc { func HandleSetNickname(store *connect4.Store, sm *scs.SessionManager) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
gameID := chi.URLParam(r, "id") gameID := chi.URLParam(r, "id")
@@ -269,14 +269,14 @@ func HandleSetNickname(store *game.GameStore, sm *scs.SessionManager) http.Handl
userID := sessions.GetUserID(sm, r) userID := sessions.GetUserID(sm, r)
if gi.GetPlayerColor(playerID) == 0 { if gi.GetPlayerColor(playerID) == 0 {
p := &game.Player{ p := &connect4.Player{
ID: playerID, ID: playerID,
Nickname: signals.Nickname, Nickname: signals.Nickname,
} }
if userID != "" { if userID != "" {
p.UserID = &userID p.UserID = &userID
} }
gi.Join(&game.PlayerSession{Player: p}) gi.Join(&connect4.PlayerSession{Player: p})
} }
sse := datastar.NewSSE(w, r) sse := datastar.NewSSE(w, r)
@@ -284,7 +284,7 @@ func HandleSetNickname(store *game.GameStore, sm *scs.SessionManager) http.Handl
} }
} }
func HandleRematch(store *game.GameStore, sm *scs.SessionManager) http.HandlerFunc { func HandleRematch(store *connect4.Store, sm *scs.SessionManager) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
gameID := chi.URLParam(r, "id") gameID := chi.URLParam(r, "id")
@@ -304,7 +304,7 @@ func HandleRematch(store *game.GameStore, sm *scs.SessionManager) http.HandlerFu
} }
// sendGameComponents patches all game-related SSE components. // sendGameComponents patches all game-related SSE components.
func sendGameComponents(sse *datastar.ServerSentEventGenerator, gi *game.GameInstance, myColor int, room *chat.Room, chatCfg chatcomponents.Config) { func sendGameComponents(sse *datastar.ServerSentEventGenerator, gi *connect4.Instance, myColor int, room *chat.Room, chatCfg chatcomponents.Config) {
g := gi.GetGame() g := gi.GetGame()
sse.PatchElementTempl(components.Board(g, myColor), datastar.WithSelectorID("c4-board")) //nolint:errcheck sse.PatchElementTempl(components.Board(g, myColor), datastar.WithSelectorID("c4-board")) //nolint:errcheck

View File

@@ -3,14 +3,14 @@ package pages
import ( import (
"github.com/ryanhamamura/c4/chat" "github.com/ryanhamamura/c4/chat"
chatcomponents "github.com/ryanhamamura/c4/chat/components" chatcomponents "github.com/ryanhamamura/c4/chat/components"
"github.com/ryanhamamura/c4/connect4"
"github.com/ryanhamamura/c4/features/c4game/components" "github.com/ryanhamamura/c4/features/c4game/components"
sharedcomponents "github.com/ryanhamamura/c4/features/common/components" sharedcomponents "github.com/ryanhamamura/c4/features/common/components"
"github.com/ryanhamamura/c4/features/common/layouts" "github.com/ryanhamamura/c4/features/common/layouts"
"github.com/ryanhamamura/c4/game"
"github.com/starfederation/datastar-go/datastar" "github.com/starfederation/datastar-go/datastar"
) )
templ GamePage(g *game.Game, myColor int, messages []chat.Message, chatCfg chatcomponents.Config) { templ GamePage(g *connect4.Game, myColor int, messages []chat.Message, chatCfg chatcomponents.Config) {
@layouts.Base("Connect 4") { @layouts.Base("Connect 4") {
<main <main
class="flex flex-col items-center gap-4 p-4" class="flex flex-col items-center gap-4 p-4"
@@ -25,7 +25,7 @@ templ GamePage(g *game.Game, myColor int, messages []chat.Message, chatCfg chatc
@components.Board(g, myColor) @components.Board(g, myColor)
@chatcomponents.Chat(messages, chatCfg) @chatcomponents.Chat(messages, chatCfg)
</div> </div>
if g.Status == game.StatusWaitingForPlayer { if g.Status == connect4.StatusWaitingForPlayer {
@components.InviteLink(g.ID) @components.InviteLink(g.ID)
} }
</main> </main>

View File

@@ -6,13 +6,13 @@ import (
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/nats-io/nats.go" "github.com/nats-io/nats.go"
"github.com/ryanhamamura/c4/connect4"
"github.com/ryanhamamura/c4/db/repository" "github.com/ryanhamamura/c4/db/repository"
"github.com/ryanhamamura/c4/game"
) )
func SetupRoutes( func SetupRoutes(
router chi.Router, router chi.Router,
store *game.GameStore, store *connect4.Store,
nc *nats.Conn, nc *nats.Conn,
sessions *scs.SessionManager, sessions *scs.SessionManager,
queries *repository.Queries, queries *repository.Queries,

View File

@@ -4,7 +4,7 @@ import (
"fmt" "fmt"
"time" "time"
"github.com/ryanhamamura/c4/game" "github.com/ryanhamamura/c4/connect4"
"github.com/starfederation/datastar-go/datastar" "github.com/starfederation/datastar-go/datastar"
) )
@@ -46,10 +46,10 @@ templ gameListEntry(g GameListItem) {
} }
func statusText(g GameListItem) string { func statusText(g GameListItem) string {
switch game.GameStatus(g.Status) { switch connect4.Status(g.Status) {
case game.StatusWaitingForPlayer: case connect4.StatusWaitingForPlayer:
return "Waiting for opponent" return "Waiting for opponent"
case game.StatusInProgress: case connect4.StatusInProgress:
if g.IsMyTurn { if g.IsMyTurn {
return "Your turn!" return "Your turn!"
} }
@@ -59,10 +59,10 @@ func statusText(g GameListItem) string {
} }
func statusClass(g GameListItem) string { func statusClass(g GameListItem) string {
switch game.GameStatus(g.Status) { switch connect4.Status(g.Status) {
case game.StatusWaitingForPlayer: case connect4.StatusWaitingForPlayer:
return "text-sm opacity-60" return "text-sm opacity-60"
case game.StatusInProgress: case connect4.StatusInProgress:
if g.IsMyTurn { if g.IsMyTurn {
return "text-sm text-success font-bold" return "text-sm text-success font-bold"
} }

View File

@@ -7,10 +7,10 @@ import (
"strconv" "strconv"
"time" "time"
"github.com/ryanhamamura/c4/connect4"
"github.com/ryanhamamura/c4/db/repository" "github.com/ryanhamamura/c4/db/repository"
lobbycomponents "github.com/ryanhamamura/c4/features/lobby/components" lobbycomponents "github.com/ryanhamamura/c4/features/lobby/components"
"github.com/ryanhamamura/c4/features/lobby/pages" "github.com/ryanhamamura/c4/features/lobby/pages"
"github.com/ryanhamamura/c4/game"
"github.com/ryanhamamura/c4/snake" "github.com/ryanhamamura/c4/snake"
"github.com/alexedwards/scs/v2" "github.com/alexedwards/scs/v2"
@@ -80,7 +80,7 @@ func HandleLobbyPage(queries *repository.Queries, sessions *scs.SessionManager,
} }
// HandleCreateGame reads the nickname signal, creates a connect4 game, and redirects via SSE. // HandleCreateGame reads the nickname signal, creates a connect4 game, and redirects via SSE.
func HandleCreateGame(store *game.GameStore, sessions *scs.SessionManager) http.HandlerFunc { func HandleCreateGame(store *connect4.Store, sessions *scs.SessionManager) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
type Signals struct { type Signals struct {
Nickname string `json:"nickname"` Nickname string `json:"nickname"`
@@ -104,7 +104,7 @@ func HandleCreateGame(store *game.GameStore, sessions *scs.SessionManager) http.
} }
// HandleDeleteGame deletes a connect4 game and redirects to the lobby. // HandleDeleteGame deletes a connect4 game and redirects to the lobby.
func HandleDeleteGame(store *game.GameStore, sessions *scs.SessionManager) http.HandlerFunc { func HandleDeleteGame(store *connect4.Store, sessions *scs.SessionManager) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
gameID := chi.URLParam(r, "id") gameID := chi.URLParam(r, "id")
if gameID == "" { if gameID == "" {

View File

@@ -2,8 +2,8 @@
package lobby package lobby
import ( import (
"github.com/ryanhamamura/c4/connect4"
"github.com/ryanhamamura/c4/db/repository" "github.com/ryanhamamura/c4/db/repository"
"github.com/ryanhamamura/c4/game"
"github.com/ryanhamamura/c4/snake" "github.com/ryanhamamura/c4/snake"
"github.com/alexedwards/scs/v2" "github.com/alexedwards/scs/v2"
@@ -14,7 +14,7 @@ func SetupRoutes(
router chi.Router, router chi.Router,
queries *repository.Queries, queries *repository.Queries,
sessions *scs.SessionManager, sessions *scs.SessionManager,
store *game.GameStore, store *connect4.Store,
snakeStore *snake.SnakeStore, snakeStore *snake.SnakeStore,
) { ) {
router.Get("/", HandleLobbyPage(queries, sessions, snakeStore)) router.Get("/", HandleLobbyPage(queries, sessions, snakeStore))

View File

@@ -12,9 +12,9 @@ import (
"time" "time"
"github.com/ryanhamamura/c4/config" "github.com/ryanhamamura/c4/config"
"github.com/ryanhamamura/c4/connect4"
"github.com/ryanhamamura/c4/db" "github.com/ryanhamamura/c4/db"
"github.com/ryanhamamura/c4/db/repository" "github.com/ryanhamamura/c4/db/repository"
"github.com/ryanhamamura/c4/game"
"github.com/ryanhamamura/c4/logging" "github.com/ryanhamamura/c4/logging"
appnats "github.com/ryanhamamura/c4/nats" appnats "github.com/ryanhamamura/c4/nats"
"github.com/ryanhamamura/c4/router" "github.com/ryanhamamura/c4/router"
@@ -71,9 +71,9 @@ func run(ctx context.Context) error {
defer cleanupNATS() defer cleanupNATS()
// Game stores // Game stores
store := game.NewGameStore(queries) store := connect4.NewStore(queries)
store.SetNotifyFunc(func(gameID string) { store.SetNotifyFunc(func(gameID string) {
nc.Publish("game."+gameID, nil) //nolint:errcheck // best-effort notification nc.Publish("connect4."+gameID, nil) //nolint:errcheck // best-effort notification
}) })
snakeStore := snake.NewSnakeStore(queries) snakeStore := snake.NewSnakeStore(queries)

View File

@@ -8,12 +8,12 @@ import (
"sync" "sync"
"github.com/ryanhamamura/c4/config" "github.com/ryanhamamura/c4/config"
"github.com/ryanhamamura/c4/connect4"
"github.com/ryanhamamura/c4/db/repository" "github.com/ryanhamamura/c4/db/repository"
"github.com/ryanhamamura/c4/features/auth" "github.com/ryanhamamura/c4/features/auth"
"github.com/ryanhamamura/c4/features/c4game" "github.com/ryanhamamura/c4/features/c4game"
"github.com/ryanhamamura/c4/features/lobby" "github.com/ryanhamamura/c4/features/lobby"
"github.com/ryanhamamura/c4/features/snakegame" "github.com/ryanhamamura/c4/features/snakegame"
"github.com/ryanhamamura/c4/game"
"github.com/ryanhamamura/c4/snake" "github.com/ryanhamamura/c4/snake"
"github.com/alexedwards/scs/v2" "github.com/alexedwards/scs/v2"
@@ -27,7 +27,7 @@ func SetupRoutes(
queries *repository.Queries, queries *repository.Queries,
sessions *scs.SessionManager, sessions *scs.SessionManager,
nc *nats.Conn, nc *nats.Conn,
store *game.GameStore, store *connect4.Store,
snakeStore *snake.SnakeStore, snakeStore *snake.SnakeStore,
assets embed.FS, assets embed.FS,
) { ) {