Simplify codebase and fix Enter key on home page

- Enter key now triggers createGame action on home page
- Remove redundant setNickname action from home page
- Remove unused code: join channel, Leave(), Stop() methods
- Consolidate ID generation into game.GenerateID()
- Remove unused CreatedAt field from Game struct
This commit is contained in:
Ryan Hamamura
2026-01-14 14:10:18 -10:00
parent 389fc12bf2
commit 63d0773ab5
6 changed files with 34 additions and 61 deletions

View File

@@ -28,7 +28,7 @@ func NewGameStore() *GameStore {
} }
func (gs *GameStore) Create() *GameInstance { func (gs *GameStore) Create() *GameInstance {
id := generateGameID() id := GenerateID(4)
gi := NewGameInstance(id) gi := NewGameInstance(id)
gs.gamesMu.Lock() gs.gamesMu.Lock()
gs.games[id] = gi gs.games[id] = gi
@@ -44,8 +44,8 @@ func (gs *GameStore) Get(id string) (*GameInstance, bool) {
return gi, ok return gi, ok
} }
func generateGameID() string { func GenerateID(size int) string {
b := make([]byte, 4) b := make([]byte, size)
rand.Read(b) rand.Read(b)
return hex.EncodeToString(b) return hex.EncodeToString(b)
} }
@@ -55,7 +55,6 @@ type GameInstance struct {
gameMu sync.RWMutex gameMu sync.RWMutex
players map[PlayerID]Syncable players map[PlayerID]Syncable
playersMu sync.RWMutex playersMu sync.RWMutex
join chan *PlayerSession
leave chan PlayerID leave chan PlayerID
done chan struct{} done chan struct{}
dirty bool dirty bool
@@ -65,7 +64,6 @@ func NewGameInstance(id string) *GameInstance {
return &GameInstance{ return &GameInstance{
game: NewGame(id), game: NewGame(id),
players: make(map[PlayerID]Syncable), players: make(map[PlayerID]Syncable),
join: make(chan *PlayerSession, 5),
leave: make(chan PlayerID, 5), leave: make(chan PlayerID, 5),
done: make(chan struct{}), done: make(chan struct{}),
} }
@@ -101,10 +99,6 @@ func (gi *GameInstance) Join(ps *PlayerSession) bool {
return true return true
} }
func (gi *GameInstance) Leave(pid PlayerID) {
gi.leave <- pid
}
func (gi *GameInstance) GetGame() *Game { func (gi *GameInstance) GetGame() *Game {
gi.gameMu.RLock() gi.gameMu.RLock()
defer gi.gameMu.RUnlock() defer gi.gameMu.RUnlock()
@@ -186,7 +180,3 @@ func (gi *GameInstance) publish() {
sync.Sync() sync.Sync()
} }
} }
func (gi *GameInstance) Stop() {
close(gi.done)
}

View File

@@ -1,7 +1,5 @@
package game package game
import "time"
type PlayerID string type PlayerID string
type Player struct { type Player struct {
@@ -27,7 +25,6 @@ type Game struct {
Status GameStatus Status GameStatus
Winner *Player Winner *Player
WinningCells [][2]int // Coordinates of winning 4 cells for highlighting WinningCells [][2]int // Coordinates of winning 4 cells for highlighting
CreatedAt time.Time
} }
func NewGame(id string) *Game { func NewGame(id string) *Game {
@@ -36,6 +33,5 @@ func NewGame(id string) *Game {
Board: [6][7]int{}, Board: [6][7]int{},
CurrentTurn: 1, // Red goes first CurrentTurn: 1, // Red goes first
Status: StatusWaitingForPlayer, Status: StatusWaitingForPlayer,
CreatedAt: time.Now(),
} }
} }

6
go.mod
View File

@@ -2,16 +2,12 @@ module github.com/ryanhamamura/c4
go 1.25.4 go 1.25.4
require ( require github.com/ryanhamamura/via v0.2.3
github.com/go-via/via-plugin-picocss v0.1.1
github.com/ryanhamamura/via v0.2.3
)
require ( require (
github.com/CAFxX/httpcompression v0.0.9 // indirect github.com/CAFxX/httpcompression v0.0.9 // indirect
github.com/alexedwards/scs/v2 v2.9.0 // indirect github.com/alexedwards/scs/v2 v2.9.0 // indirect
github.com/andybalholm/brotli v1.2.0 // indirect github.com/andybalholm/brotli v1.2.0 // indirect
github.com/go-via/via v0.1.4 // indirect
github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/compress v1.18.0 // indirect
github.com/starfederation/datastar-go v1.0.3 // indirect github.com/starfederation/datastar-go v1.0.3 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect

4
go.sum
View File

@@ -8,10 +8,6 @@ github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUS
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-via/via v0.1.4 h1:Fz9fwaT5+TBqcetiVM33SxkuysAeFDOiiASFu3GW7WY=
github.com/go-via/via v0.1.4/go.mod h1:Y8oddRwP6SWX15Xb6UQj4HtLZwxTYI1HbWBmELtB/f8=
github.com/go-via/via-plugin-picocss v0.1.1 h1:rbA9wL9eEanT8HOOfX1b4Mr2L2VjaDrsIrUECDxV73k=
github.com/go-via/via-plugin-picocss v0.1.1/go.mod h1:npvsvG2FWeIPkzHzSSzW+uBGE0m5gnIAdlePqKcfuAQ=
github.com/google/brotli/go/cbrotli v0.0.0-20230829110029-ed738e842d2f h1:jopqB+UTSdJGEJT8tEqYyE29zN91fi2827oLET8tl7k= github.com/google/brotli/go/cbrotli v0.0.0-20230829110029-ed738e842d2f h1:jopqB+UTSdJGEJT8tEqYyE29zN91fi2827oLET8tl7k=
github.com/google/brotli/go/cbrotli v0.0.0-20230829110029-ed738e842d2f/go.mod h1:nOPhAkwVliJdNTkj3gXpljmWhjc4wCaVqbMJcPKWP4s= github.com/google/brotli/go/cbrotli v0.0.0-20230829110029-ed738e842d2f/go.mod h1:nOPhAkwVliJdNTkj3gXpljmWhjc4wCaVqbMJcPKWP4s=
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=

61
main.go
View File

@@ -1,9 +1,6 @@
package main package main
import ( import (
"crypto/rand"
"encoding/hex"
"github.com/ryanhamamura/c4/game" "github.com/ryanhamamura/c4/game"
"github.com/ryanhamamura/c4/ui" "github.com/ryanhamamura/c4/ui"
"github.com/ryanhamamura/via" "github.com/ryanhamamura/via"
@@ -29,14 +26,6 @@ func main() {
v.Page("/", func(c *via.Context) { v.Page("/", func(c *via.Context) {
nickname := c.Signal("") nickname := c.Signal("")
setNickname := c.Action(func() {
name := nickname.String()
if name != "" {
c.Session().Set("nickname", name)
c.Sync()
}
})
createGame := c.Action(func() { createGame := c.Action(func() {
name := nickname.String() name := nickname.String()
if name == "" { if name == "" {
@@ -51,7 +40,7 @@ func main() {
c.View(func() h.H { c.View(func() h.H {
return ui.LobbyView( return ui.LobbyView(
nickname.Bind(), nickname.Bind(),
setNickname.OnKeyDown("Enter"), createGame.OnKeyDown("Enter"),
createGame.OnClick(), createGame.OnClick(),
) )
}) })
@@ -66,8 +55,6 @@ func main() {
colSignal := c.Signal(0) colSignal := c.Signal(0)
var gi *game.GameInstance var gi *game.GameInstance
var player *game.Player
var playerJoined bool
var gameExists bool var gameExists bool
// Look up game (may not exist during warmup or invalid ID) // Look up game (may not exist during warmup or invalid ID)
@@ -75,6 +62,13 @@ func main() {
gi, gameExists = store.Get(gameID) gi, gameExists = store.Get(gameID)
} }
// Generate a stable player ID for this session
playerID := game.PlayerID(c.Session().GetString("player_id"))
if playerID == "" {
playerID = game.PlayerID(game.GenerateID(8))
c.Session().Set("player_id", string(playerID))
}
setNickname := c.Action(func() { setNickname := c.Action(func() {
if gi == nil { if gi == nil {
return return
@@ -85,12 +79,13 @@ func main() {
} }
c.Session().Set("nickname", name) c.Session().Set("nickname", name)
if !playerJoined { // Try to join if not already in game
player = &game.Player{ if gi.GetPlayerColor(playerID) == 0 {
ID: game.PlayerID(generatePlayerID()), player := &game.Player{
ID: playerID,
Nickname: name, Nickname: name,
} }
playerJoined = gi.Join(&game.PlayerSession{ gi.Join(&game.PlayerSession{
Player: player, Player: player,
Sync: c, Sync: c,
}) })
@@ -99,20 +94,25 @@ func main() {
}) })
dropPiece := c.Action(func() { dropPiece := c.Action(func() {
if gi == nil || player == nil { if gi == nil {
return
}
myColor := gi.GetPlayerColor(playerID)
if myColor == 0 {
return return
} }
col := colSignal.Int() col := colSignal.Int()
gi.DropPiece(col, player.Color) gi.DropPiece(col, myColor)
c.Sync()
}) })
// If nickname exists in session and game exists, join immediately // If nickname exists in session and game exists, join immediately
if gameExists && sessionNickname != "" { if gameExists && sessionNickname != "" && gi.GetPlayerColor(playerID) == 0 {
player = &game.Player{ player := &game.Player{
ID: game.PlayerID(generatePlayerID()), ID: playerID,
Nickname: sessionNickname, Nickname: sessionNickname,
} }
playerJoined = gi.Join(&game.PlayerSession{ gi.Join(&game.PlayerSession{
Player: player, Player: player,
Sync: c, Sync: c,
}) })
@@ -125,8 +125,10 @@ func main() {
return h.Div() return h.Div()
} }
// Need nickname first myColor := gi.GetPlayerColor(playerID)
if !playerJoined {
// Need nickname first / not joined yet
if myColor == 0 {
return ui.NicknamePrompt( return ui.NicknamePrompt(
nickname.Bind(), nickname.Bind(),
setNickname.OnKeyDown("Enter"), setNickname.OnKeyDown("Enter"),
@@ -135,7 +137,6 @@ func main() {
} }
g := gi.GetGame() g := gi.GetGame()
myColor := player.Color
// Create column click function // Create column click function
columnClick := func(col int) h.H { columnClick := func(col int) h.H {
@@ -164,12 +165,6 @@ func main() {
v.Start() v.Start()
} }
func generatePlayerID() string {
b := make([]byte, 8)
rand.Read(b)
return hex.EncodeToString(b)
}
const gameCSS = ` const gameCSS = `
body { margin: 0; } body { margin: 0; }

View File

@@ -4,7 +4,7 @@ import (
"github.com/ryanhamamura/via/h" "github.com/ryanhamamura/via/h"
) )
func LobbyView(nicknameBind, setNicknameKeyDown, createGameClick h.H) h.H { func LobbyView(nicknameBind, createGameKeyDown, createGameClick h.H) h.H {
return h.Main(h.Class("container"), return h.Main(h.Class("container"),
h.Div(h.Class("lobby"), h.Div(h.Class("lobby"),
h.H1(h.Text("Connect 4")), h.H1(h.Text("Connect 4")),
@@ -18,7 +18,7 @@ func LobbyView(nicknameBind, setNicknameKeyDown, createGameClick h.H) h.H {
h.Placeholder("Enter your nickname"), h.Placeholder("Enter your nickname"),
nicknameBind, nicknameBind,
h.Attr("required"), h.Attr("required"),
setNicknameKeyDown, createGameKeyDown,
), ),
), ),
h.Button( h.Button(