From 4faf4f73b078d0be3c493403ab472dc691c28660 Mon Sep 17 00:00:00 2001 From: Ryan Hamamura <58859899+ryanhamamura@users.noreply.github.com> Date: Mon, 2 Mar 2026 22:34:20 -1000 Subject: [PATCH] refactor: patch entire game content for snake SSE handler MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same approach as connect4 — extract GameContent component and patch it as a single element, letting DOM morphing handle the diff. --- features/snakegame/handlers.go | 57 ++++++++++++++--------------- features/snakegame/pages/game.templ | 44 ++++++++++++---------- 2 files changed, 53 insertions(+), 48 deletions(-) diff --git a/features/snakegame/handlers.go b/features/snakegame/handlers.go index effa3ff..9620377 100644 --- a/features/snakegame/handlers.go +++ b/features/snakegame/handlers.go @@ -12,7 +12,6 @@ import ( "github.com/ryanhamamura/games/chat" chatcomponents "github.com/ryanhamamura/games/chat/components" - "github.com/ryanhamamura/games/features/snakegame/components" "github.com/ryanhamamura/games/features/snakegame/pages" "github.com/ryanhamamura/games/sessions" "github.com/ryanhamamura/games/snake" @@ -100,16 +99,33 @@ func HandleSnakeEvents(snakeStore *snake.SnakeStore, nc *nats.Conn, sm *scs.Sess chatCfg := snakeChatConfig(gameID) - // Send initial render + // Chat room (multiplayer only) + var room *chat.Room sg := si.GetGame() - sse.PatchElementTempl(components.Board(sg)) //nolint:errcheck - sse.PatchElementTempl(components.StatusBanner(sg, mySlot, gameID)) //nolint:errcheck - sse.PatchElementTempl(components.PlayerList(sg, mySlot)) //nolint:errcheck if sg.Mode == snake.ModeMultiplayer { - sse.PatchElementTempl(chatcomponents.Chat(nil, chatCfg)) //nolint:errcheck - if sg.Status == snake.StatusWaitingForPlayers || sg.Status == snake.StatusCountdown { - sse.PatchElementTempl(components.InviteLink(gameID)) //nolint:errcheck + room = chat.NewRoom(nc, snake.ChatSubject(gameID)) + } + + chatMessages := func() []chat.Message { + if room == nil { + return nil } + return room.Messages() + } + + patchAll := func() error { + si, ok = snakeStore.Get(gameID) + if !ok { + return fmt.Errorf("game not found") + } + mySlot = si.GetPlayerSlot(playerID) + sg = si.GetGame() + return sse.PatchElementTempl(pages.GameContent(sg, mySlot, chatMessages(), chatCfg, gameID)) + } + + // Send initial render + if err := patchAll(); err != nil { + return } // Subscribe to game updates via NATS @@ -123,10 +139,8 @@ func HandleSnakeEvents(snakeStore *snake.SnakeStore, nc *nats.Conn, sm *scs.Sess // Chat subscription (multiplayer only) var chatCh chan *nats.Msg var chatSub *nats.Subscription - var room *chat.Room - if sg.Mode == snake.ModeMultiplayer { - room = chat.NewRoom(nc, snake.ChatSubject(gameID)) + if room != nil { chatCh, chatSub, err = room.Subscribe() if err != nil { return @@ -150,19 +164,7 @@ func HandleSnakeEvents(snakeStore *snake.SnakeStore, nc *nats.Conn, sm *scs.Sess } } drained: - si, ok = snakeStore.Get(gameID) - if !ok { - return - } - mySlot = si.GetPlayerSlot(playerID) - sg = si.GetGame() - if err := sse.PatchElementTempl(components.Board(sg)); err != nil { - return - } - if err := sse.PatchElementTempl(components.StatusBanner(sg, mySlot, gameID)); err != nil { - return - } - if err := sse.PatchElementTempl(components.PlayerList(sg, mySlot)); err != nil { + if err := patchAll(); err != nil { return } @@ -170,11 +172,8 @@ func HandleSnakeEvents(snakeStore *snake.SnakeStore, nc *nats.Conn, sm *scs.Sess if msg == nil { continue } - _, snapshot := room.Receive(msg.Data) - if snapshot == nil { - continue - } - if err := sse.PatchElementTempl(chatcomponents.Chat(snapshot, chatCfg)); err != nil { + room.Receive(msg.Data) + if err := patchAll(); err != nil { return } } diff --git a/features/snakegame/pages/game.templ b/features/snakegame/pages/game.templ index 63bda61..98b98fb 100644 --- a/features/snakegame/pages/game.templ +++ b/features/snakegame/pages/game.templ @@ -37,29 +37,35 @@ templ GamePage(sg *snake.SnakeGame, mySlot int, messages []chat.Message, chatCfg data-on:keydown.throttle_100ms={ keydownScript(gameID) } tabindex="0" > - @components.BackToLobby() -

~~~~

- @snakecomponents.PlayerList(sg, mySlot) - @snakecomponents.StatusBanner(sg, mySlot, gameID) - if sg.Status == snake.StatusInProgress || sg.Status == snake.StatusFinished { - if sg.Mode == snake.ModeMultiplayer { -
- @snakecomponents.Board(sg) - @chatcomponents.Chat(messages, chatCfg) -
- } else { - @snakecomponents.Board(sg) - } - } else if sg.Mode == snake.ModeMultiplayer { - @chatcomponents.Chat(messages, chatCfg) - } - if sg.Mode == snake.ModeMultiplayer && (sg.Status == snake.StatusWaitingForPlayers || sg.Status == snake.StatusCountdown) { - @snakecomponents.InviteLink(gameID) - } + @GameContent(sg, mySlot, messages, chatCfg, gameID) } } +templ GameContent(sg *snake.SnakeGame, mySlot int, messages []chat.Message, chatCfg chatcomponents.Config, gameID string) { +
+ @components.BackToLobby() +

~~~~

+ @snakecomponents.PlayerList(sg, mySlot) + @snakecomponents.StatusBanner(sg, mySlot, gameID) + if sg.Status == snake.StatusInProgress || sg.Status == snake.StatusFinished { + if sg.Mode == snake.ModeMultiplayer { +
+ @snakecomponents.Board(sg) + @chatcomponents.Chat(messages, chatCfg) +
+ } else { + @snakecomponents.Board(sg) + } + } else if sg.Mode == snake.ModeMultiplayer { + @chatcomponents.Chat(messages, chatCfg) + } + if sg.Mode == snake.ModeMultiplayer && (sg.Status == snake.StatusWaitingForPlayers || sg.Status == snake.StatusCountdown) { + @snakecomponents.InviteLink(gameID) + } +
+} + templ JoinPage(gameID string) { @layouts.Base("Snake - Join") { @components.GameJoinPrompt(