refactor: rename game package to connect4, drop Game prefix from types
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:
@@ -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).
|
||||||
@@ -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 {
|
||||||
@@ -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()
|
||||||
|
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 == "" {
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
6
main.go
6
main.go
@@ -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)
|
||||||
|
|||||||
@@ -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,
|
||||||
) {
|
) {
|
||||||
|
|||||||
Reference in New Issue
Block a user