Skip to content

Add comprehensive WebSocket learning curriculum with 12 progressive steps#1

Merged
NAGA-29 merged 5 commits intomainfrom
claude/websocket-learning-report-Wwt4X
Apr 5, 2026
Merged

Add comprehensive WebSocket learning curriculum with 12 progressive steps#1
NAGA-29 merged 5 commits intomainfrom
claude/websocket-learning-report-Wwt4X

Conversation

@NAGA-29
Copy link
Copy Markdown
Owner

@NAGA-29 NAGA-29 commented Mar 31, 2026

Summary

This PR adds a complete educational curriculum for learning WebSocket development through building a multiplayer snake game. The curriculum progresses from basic WebSocket concepts through advanced topics like room management, latency handling, interpolation, reconnection, and optimization.

Key Changes

  • Step 0: Conceptual foundation explaining HTTP vs WebSocket differences
  • Step 1: Basic WebSocket echo server and client implementation
  • Step 2: Multi-client broadcast messaging with connection management
  • Step 3: Input handling pattern (sending intent rather than state)
  • Step 4: Server-authoritative state management architecture
  • Step 5: Game loop implementation with tick-based updates
  • Step 6: Full snake game with food mechanics and multi-player support
  • Step 7: Latency and jitter simulation to understand network delays
  • Step 8: Client-side interpolation for smooth movement between server updates
  • Step 9: Room-based game partitioning to support multiple concurrent games
  • Step 10: Heartbeat/ping-pong mechanism for detecting disconnections
  • Step 11: Optimization techniques including message size reduction and delta compression
  • Step 12: Advanced concepts for multi-server architectures and Redis Pub/Sub

Implementation Details

  • Technology Stack: Go (echo framework + gorilla/websocket) for backend, vanilla JavaScript + Canvas API for frontend
  • Architecture Pattern: Server-authoritative design where the server maintains game state truth and clients only send input
  • Game Mechanics: Grid-based snake movement, food spawning, collision detection, and score tracking
  • Network Optimization: Demonstrates both full-state and delta-based message transmission with size comparisons
  • Robustness: Includes connection lifecycle management, graceful disconnection handling, and reconnection patterns

Each step includes both server and client code with detailed README documentation explaining the concepts, design decisions, and implementation patterns.

https://claude.ai/code/session_01Cg4waeVYhPHpatXEsQMbfB

Summary by CodeRabbit

  • 新機能
    • 日本語の12段階WebSocket学習カリキュラムを追加。実践的なブラウザデモ群(エコー、ブロードキャスト、入力同期、ゲームループ、補間、遅延可視化、スネーク系ゲーム、部屋/再接続、最適化など)を収録。
  • ドキュメント
    • 各ステップに詳細なREADME、実習課題、検証手順、理解チェックリストを日本語で追加。

Full learning repository for WebSocket fundamentals using a
Slither.io-style snake game as the teaching subject. Each step
includes README with concept explanation, full Go server code,
and HTML/JS client code.

Steps covered:
- Step 0: HTTP vs WebSocket theory
- Step 1: Echo server (first connection)
- Step 2: Broadcast (1-to-many)
- Step 3: Input sending (intent over state)
- Step 4: Server-authoritative state management
- Step 5: Game loop (tick-based updates)
- Step 6: Full snake game with food and growth
- Step 7: Lag and sync drift experience
- Step 8: Client-side interpolation
- Step 9: 10-player room management
- Step 10: Disconnection handling and heartbeat
- Step 11: Optimization and size measurement
- Step 12: Advanced topics (multi-server, Redis, WebRTC)

https://claude.ai/code/session_01Cg4waeVYhPHpatXEsQMbfB
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 31, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Step0〜Step12の段階的WebSocket学習カリキュラムを追加。各ステップは教材(README)、ブラウザクライアント(HTML/JS)、Goサーバ(Echo + gorilla/websocket)を用い、エコー→ブロードキャスト→入力同期→サーバ状態→ゲームループ→Snake→遅延→補間→ルーム→再接続→最適化→スケーリングを順に扱う。

Changes

Cohort / File(s) Summary
Repository README
README.md
全体カリキュラム、Step0–12の学習ロードマップ、進行ルール、出力要件、標準フォルダ構成、最終チェックリストを追加(ドキュメントのみ)。
Step 0: HTTP vs WebSocket
step-00-http-vs-websocket/README.md
HTTPとWebSocketの概念比較、ハンドシェイク説明、演習/自己チェックを追加(ドキュメント)。
Step 1: Echo (基本接続)
step-01-echo/README.md, step-01-echo/client/index.html, step-01-echo/server/main.go, step-01-echo/server/go.mod
ブラウザのエコークライアントUIとGoサーバを追加。/wsでUpgradeし受信メッセージをそのまま返す実装。
Step 2: Broadcast
step-02-broadcast/README.md, step-02-broadcast/client/index.html, step-02-broadcast/server/main.go, step-02-broadcast/server/go.mod
複数接続管理とブロードキャスト実装、接続集合(map)とmutex保護、JSONメッセージの送受信。
Step 3: Input (操作送信)
step-03-input/README.md, step-03-input/client/index.html, step-03-input/server/main.go, step-03-input/server/go.mod
クライアントが方向入力を送信し、サーバが最新入力を保持しack/stateをブロードキャストする実装(入力検証・排他制御)。
Step 4: Server State
step-04-server-state/README.md, step-04-server-state/client/index.html, step-04-server-state/server/main.go, step-04-server-state/server/go.mod
サーバが位置/方向の権威を持つ実装。Player構造、Canvas描画クライアント、即時状態更新とブロードキャストを追加。
Step 5: Game Loop
step-05-game-loop/README.md, step-05-game-loop/client/index.html, step-05-game-loop/server/main.go, step-05-game-loop/server/go.mod
tickerベースのゲームループ追加。入力処理とtick更新の分離、tickごとのbroadcast実装。
Step 6: Snake (グリッド実装)
step-06-snake/README.md, step-06-snake/client/index.html, step-06-snake/server/main.go, step-06-snake/server/go.mod
グリッドベースのSnake実装(body管理、食料、スコア、描画)。衝突/成長/方向反転防止などを実装。
Step 7: Lag (遅延実験)
step-07-lag/README.md, step-07-lag/client/index.html, step-07-lag/server/main.go, step-07-lag/server/go.mod
サーバ側で遅延・ジッタをシミュレートする実験環境。クライアントは受信間隔統計や可視化を行う。
Step 8: Interpolation
step-08-interpolation/README.md, step-08-interpolation/client/index.html, step-08-interpolation/server/main.go, step-08-interpolation/server/go.mod
クライアント補間の導入(prev/next snapshot、線形補間)、即時描画との比較ビューを追加。
Step 9: Rooms
step-09-rooms/README.md, step-09-rooms/client/index.html, step-09-rooms/server/main.go, step-09-rooms/server/go.mod
部屋単位の管理(Room作成・割当・per-room game loop)を導入。roomライフサイクルと同期ロジックを追加。
Step 10: Reconnect (再接続/Heartbeat)
step-10-reconnect/README.md, step-10-reconnect/client/index.html, step-10-reconnect/server/main.go, step-10-reconnect/server/go.mod
Ping/Pongハートビート、ReadDeadline/SetPongHandler、ping goroutineと停止ロジック、クライアント自動再接続を追加。
Step 11: Optimization (差分/統計)
step-11-optimization/README.md, step-11-optimization/client/index.html, step-11-optimization/server/main.go, step-11-optimization/server/go.mod
全量/軽量(JSON差分)比較、サーバでのシリアライズサイズ計測とstatsメッセージ送信を実装。
Step 12: Advanced (スケーリング設計)
step-12-advanced/README.md
マルチサーバ設計、Room配置とセッションアフィニティ、Redis Pub/Subの利用場面・代替手段の解説(ドキュメントのみ)。

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant FrontendServer as Server
    participant RoomLoop as Room/GameLoop

    Client->>FrontendServer: WebSocket接続 /ws
    FrontendServer->>FrontendServer: findOrCreateRoom()
    FrontendServer->>RoomLoop: register client into room
    Note right of RoomLoop: Room runs ticker loop
    RoomLoop->>RoomLoop: tick → update game state
    RoomLoop->>FrontendServer: broadcast state to room clients
    FrontendServer->>Client: state (ws message)
    Client->>FrontendServer: input (ws message)
    FrontendServer->>RoomLoop: apply input (update direction)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 ぴょんと繋いだ ws の糸、
エコーから部屋へ、みんなで走る。
遅延も補間も学びつつ、
差分で軽く、再接続は優しく。
次のステップへ、しっぽを揺らし跳ぶ。

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 36.21% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed プルリクエストのタイトルはチェンジセット全体の主要な変更を明確に要約しており、12段階のプログレッシブなWebSocket学習カリキュラムを追加することを正確に説明しています。

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/websocket-learning-report-Wwt4X

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@NAGA-29 NAGA-29 requested a review from Copilot March 31, 2026 14:01
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a 12-step WebSocket learning curriculum (Go + Echo + gorilla/websocket backend, vanilla JS + Canvas frontend) that progressively builds toward a simple multiplayer snake game and then expands into real-world concerns like rooms, reconnection, and basic optimization.

Changes:

  • Added Step 0–12 curriculum documentation, including quizzes/exercises and progression guidance in the root README.
  • Implemented step-by-step server/client examples for echo, broadcast, input intent, server-authoritative state, tick loop, snake mechanics, lag simulation, interpolation, rooms, reconnect/heartbeat, and optimization instrumentation.
  • Added per-step Go modules for reproducible builds.

Reviewed changes

Copilot reviewed 47 out of 47 changed files in this pull request and generated 21 comments.

Show a summary per file
File Description
README.md Top-level curriculum overview and step index
step-00-http-vs-websocket/README.md Conceptual foundation (HTTP vs WebSocket)
step-01-echo/README.md Step 1 explanation and run instructions
step-01-echo/server/go.mod Step 1 Go module dependencies
step-01-echo/server/main.go Step 1 echo server implementation
step-01-echo/client/index.html Step 1 client UI for echo testing
step-02-broadcast/README.md Step 2 explanation (broadcast)
step-02-broadcast/server/go.mod Step 2 Go module dependencies
step-02-broadcast/server/main.go Step 2 multi-client broadcast server
step-02-broadcast/client/index.html Step 2 client UI for broadcast chat
step-03-input/README.md Step 3 explanation (send input/intent)
step-03-input/server/go.mod Step 3 Go module dependencies
step-03-input/server/main.go Step 3 input tracking + broadcast
step-03-input/client/index.html Step 3 client UI for input sending/display
step-04-server-state/README.md Step 4 explanation (server-authoritative state)
step-04-server-state/server/go.mod Step 4 Go module dependencies
step-04-server-state/server/main.go Step 4 authoritative state + broadcast
step-04-server-state/client/index.html Step 4 client rendering of server state
step-05-game-loop/README.md Step 5 explanation (tick loop)
step-05-game-loop/server/go.mod Step 5 Go module dependencies
step-05-game-loop/server/main.go Step 5 tick loop + periodic broadcast
step-05-game-loop/client/index.html Step 5 client UI with tick/debug display
step-06-snake/README.md Step 6 explanation (snake body/food)
step-06-snake/server/go.mod Step 6 Go module dependencies
step-06-snake/server/main.go Step 6 snake mechanics + food + state broadcast
step-06-snake/client/index.html Step 6 client rendering + scoreboard
step-07-lag/README.md Step 7 explanation (latency/jitter)
step-07-lag/server/go.mod Step 7 Go module dependencies
step-07-lag/server/main.go Step 7 server-side delay simulation + debug fields
step-07-lag/client/index.html Step 7 client stats panel + interval graph
step-08-interpolation/README.md Step 8 explanation (interpolation)
step-08-interpolation/server/go.mod Step 8 Go module dependencies
step-08-interpolation/server/main.go Step 8 server emitting tick/time for interpolation experiments
step-08-interpolation/client/index.html Step 8 side-by-side raw vs interpolated rendering
step-09-rooms/README.md Step 9 explanation (rooms)
step-09-rooms/server/go.mod Step 9 Go module dependencies
step-09-rooms/server/main.go Step 9 room partitioning + per-room loop
step-09-rooms/client/index.html Step 9 client showing room metadata + gameplay
step-10-reconnect/README.md Step 10 explanation (heartbeat/reconnect patterns)
step-10-reconnect/server/go.mod Step 10 Go module dependencies
step-10-reconnect/server/main.go Step 10 heartbeat ping/pong + disconnection detection
step-10-reconnect/client/index.html Step 10 client with auto-reconnect + status/log
step-11-optimization/README.md Step 11 explanation (message size / delta concepts)
step-11-optimization/server/go.mod Step 11 Go module dependencies
step-11-optimization/server/main.go Step 11 state size measurement + stats message
step-11-optimization/client/index.html Step 11 client stats panel + size graph
step-12-advanced/README.md Step 12 advanced discussion (multi-server, Redis Pub/Sub, alternatives)

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread step-10-reconnect/server/main.go Outdated
Comment on lines +144 to +147
data, _ := json.Marshal(GameState{Type: "state", Snakes: sc})
for _, conn := range cc {
conn.WriteMessage(websocket.TextMessage, data)
}
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

broadcastState() と ping 送信用 goroutine が同じ *websocket.Conn に対して同時に WriteMessage を呼びうるため、gorilla/websocket の「同一コネクションへの同時書き込み禁止」に抵触します(データ競合/破損/panic の原因)。各接続ごとに write を 1 goroutine に集約するか、sync.Mutex 等で write を直列化し、ping は WriteControl(+ write deadline)で送る構成にしてください。

Copilot uses AI. Check for mistakes.
Comment on lines +195 to +199
case <-ticker.C:
// PingMessage を送信
if err := conn.WriteMessage(websocket.PingMessage, nil); err != nil {
log.Printf("ping 送信エラー: %v", err)
return
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ping 送信が conn.WriteMessage(websocket.PingMessage, ...) を呼ぶ一方で、他 goroutine(broadcastState や ack 送信)が同じ conn に書き込む可能性があり、同時 write になります。各接続で write を 1 goroutine に集約するか、write 用 mutex で WriteMessage/WriteControl を直列化してください。

Copilot uses AI. Check for mistakes.
Comment on lines +168 to +170
fullMsg := FullState{Type: "state", Snakes: sc}
fullData, _ := json.Marshal(fullMsg)

Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fullData, _ := json.Marshal(fullMsg) として Marshal エラーを無視しています。失敗すると fullData が nil(len=0)になり、以降の統計や送信が壊れるので、エラーを扱って失敗時はログ + 送信スキップ等にしてください。

Copilot uses AI. Check for mistakes.
Comment thread step-11-optimization/server/main.go Outdated
Comment on lines +196 to +199
log.Printf("tick %d: full=%d bytes, light=%d bytes, ratio=%.1f%%, players=%d",
tick, len(fullData), len(lightData),
float64(len(lightData))/float64(len(fullData))*100,
count)
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

この ratio 計算は len(fullData) が 0 の場合に 0 除算になります(上で Marshal エラーを無視しているので発生し得ます)。fullSize > 0 を確認してから計算するか、0 の場合は ratio を 0/NA 扱いにしてください。

Copilot uses AI. Check for mistakes.
Comment thread step-11-optimization/server/main.go Outdated
Comment on lines +202 to +206
// 全クライアントに送信(全量版 + 統計)
for _, conn := range cc {
conn.WriteMessage(websocket.TextMessage, fullData)
conn.WriteMessage(websocket.TextMessage, statsData)
}
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

クライアント送信で WriteMessage の戻り値エラーを無視しているため、切断済み接続が clients に残り続けたり、送信失敗が検知できません。送信エラー時はその接続を close して map から削除する等の処理を入れると、教材としても「切断処理」の重要性が伝わりやすいです。

Copilot uses AI. Check for mistakes.
Comment thread step-07-lag/server/main.go Outdated
Comment on lines +213 to +214
clients[snakeID] = conn
mu.Unlock()
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clients[snakeID] = conn を先に登録してから ack を WriteMessage しているため、別 goroutine の gameLoop()broadcastState() で同じ conn に書き込む可能性があり、接続直後に同時 write が発生します。ack 送信後に clients 登録するか、write を直列化してください。

Copilot uses AI. Check for mistakes.
Comment on lines +190 to +192
snakes[id] = &Snake{ID: id, Body: body, Direction: "right", Color: color}
clients[id] = conn
mu.Unlock()
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clients[id] = conn を行った直後に ack を WriteMessage しているため、別 goroutine の gameLoop()broadcastState() で同じ conn に書き込む可能性があり、接続直後に同時 write になります。ack 送信後に clients に登録するか、write を直列化してください。

Copilot uses AI. Check for mistakes.
Comment on lines +326 to +330
room.addPlayer(snakeID, conn, snake)
log.Printf("プレイヤー %s が部屋 %s に入室(現在%d人)", snakeID, room.ID, room.PlayerCount())

// 自分のID と 部屋IDを通知
ack, _ := json.Marshal(GameState{
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

room.addPlayer(..., conn, ...) で接続を部屋の Clients に登録した直後に ack を conn.WriteMessage していますが、部屋のゲームループ(別 goroutine)が同じ connbroadcastState() で書き込み得るため、接続直後に同時 write が起きます。ack 送信後に Clients 登録するか、write を直列化してください。

Copilot uses AI. Check for mistakes.
Comment thread step-11-optimization/server/main.go Outdated
Comment on lines +229 to +233
snakes[id] = &FullSnake{ID: id, Body: body, Direction: "right", Color: color}
clients[id] = conn
mu.Unlock()

ack, _ := json.Marshal(FullState{Type: "state", MyID: id, Snakes: map[string]*FullSnake{}})
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clients[id] = conn で登録後すぐに ack を conn.WriteMessage しているため、別 goroutine の gameLoop()broadcastWithStats() で同じ conn に書き込む可能性があり、接続直後に同時 write が起きます。ack 送信後に clients 登録するか、write を直列化してください。

Copilot uses AI. Check for mistakes.
Comment thread step-03-input/server/main.go Outdated
Comment on lines +45 to +48
func generatePlayerID() string {
idCounter++
return fmt.Sprintf("player-%d", idCounter)
}
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

generatePlayerID() がグローバル変数 idCounter をロックなしでインクリメントしているため、同時接続時にデータレース/ID重複が起こり得ます。mu で保護するか、atomic.AddInt64 などのアトミック操作にしてください。

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

🟠 Major comments (14)
step-08-interpolation/client/index.html-41-46 (1)

41-46: ⚠️ Potential issue | 🟠 Major

サーバー座標系(800x600)とキャンバスサイズ(390x300)が不一致です。

step-08-interpolation/server/main.go の Line 29-30 と不整合で、描画が途中で切れます。少なくとも同一サイズに揃えるか、座標スケーリングを入れてください。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@step-08-interpolation/client/index.html` around lines 41 - 46, The client
canvas elements rawCanvas and interpCanvas are sized 390x300 but the server
sends coordinates in an 800x600 coordinate system, causing clipped/incorrect
drawing; either set both canvases to 800x600 to match the server coordinates or
implement client-side scaling: compute scaleX = canvas.width / 800 and scaleY =
canvas.height / 600 and multiply incoming x/y before drawing (apply same scaling
for interpolation logic), and ensure any hit-testing or bounds checks use the
scaled coordinates so rendering is not cut off.
step-08-interpolation/client/index.html-89-91 (1)

89-91: ⚠️ Potential issue | 🟠 Major

接続URLの固定値と多重接続を同時に防いでください。

ws://localhost:8080/ws 固定だと配信環境で壊れます。また onopen 前に連打すると複数接続が張られます。

🔧 提案差分
 btnConn.addEventListener('click', () => {
-  ws = new WebSocket('ws://localhost:8080/ws');
+  if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) return;
+  const scheme = location.protocol === 'https:' ? 'wss' : 'ws';
+  ws = new WebSocket(`${scheme}://${location.host}/ws`);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@step-08-interpolation/client/index.html` around lines 89 - 91, btnConn
のクリックハンドラで直接 ws = new WebSocket('ws://localhost:8080/ws')
としているため接続URLが固定され配信環境で壊れるのと、onopen 前に連打すると複数接続が張られる問題があります。修正: 接続先は
location.protocol と location.host を使って動的に組み立てる(例: (location.protocol ===
'https:' ? 'wss:' : 'ws:') + '//' + location.host + '/ws')し、既存の ws が存在して
readyState が CONNECTING(0) や OPEN(1) の場合は新しく作らない(既存を使うか一度 close()
してから再接続する)、またはボタンを一時無効化して onopen/onclose で状態を管理するように実装してください(参照: btnConn, ws,
WebSocket, onopen, onclose)。
step-08-interpolation/server/main.go-20-22 (1)

20-22: ⚠️ Potential issue | 🟠 Major

CheckOrigin が全許可になっており、クロスサイト接続を無制限に許可しています。

Line 21 の CheckOrigin: func(r *http.Request) bool { return true } は、学習用の環境であってもセキュリティ上危険なデフォルトです。許可するOriginを明示的に制限してください。

🔧 提案差分
 var upgrader = websocket.Upgrader{
-	CheckOrigin: func(r *http.Request) bool { return true },
+	CheckOrigin: func(r *http.Request) bool {
+		origin := r.Header.Get("Origin")
+		return origin == "http://localhost:8080" || origin == "http://127.0.0.1:8080"
+	},
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@step-08-interpolation/server/main.go` around lines 20 - 22, The current
websocket.Upgrader named upgrader uses CheckOrigin: func(r *http.Request) bool {
return true }, which allows all cross-origin connections; replace this with a
CheckOrigin implementation that validates r.Header.Get("Origin") against a
whitelist of allowed origins (e.g., a slice or map of strings loaded from
constants or environment config) and only returns true when the Origin header
matches an entry; locate websocket.Upgrader upgrader and change CheckOrigin to
call a helper like isAllowedOrigin(origin string) or allowedOrigin(r
*http.Request) that performs the whitelist check and rejects everything else.
step-08-interpolation/server/main.go-166-169 (1)

166-169: ⚠️ Potential issue | 🟠 Major

WriteMessageのエラーを無視しており、切断済みクライアントが残留したまま毎tickで送信失敗が繰り返されます。

WriteMessageが失敗した接続は、handleWebSocketのReadMessageでエラーが検出されるまで、clientsマップに残ったままになります。その間、毎tickで同じ接続への送信が失敗し続けます。失敗時は接続をクローズして、clientsマップから即座に除去してください。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@step-08-interpolation/server/main.go` around lines 166 - 169, WriteMessage
errors are being ignored causing disconnected sockets to stay in the clients map
and repeatedly fail; when websocket.WriteMessage returns an error you must close
that connection and immediately remove it from the shared clients map (the same
map used in handleWebSocket/ReadMessage) under the clients mutex. Modify the
broadcast loop (where data is written to each conn from clientsCopy) to check
the error return, call conn.Close(), acquire the clients mutex, delete the
corresponding entry from clients, release the mutex, and continue so failed
connections are cleaned up immediately.
step-08-interpolation/server/main.go-214-218 (1)

214-218: ⚠️ Potential issue | 🟠 Major

direction の許可値チェックが不足しています。

逆方向禁止だけでは不十分で、up/down/left/right 以外を弾く必要があります。現在は不正値で移動計算が崩れます。

🔧 提案差分
 		if msg.Type == "input" {
+			valid := map[string]bool{"up": true, "down": true, "left": true, "right": true}
+			if !valid[msg.Direction] {
+				continue
+			}
 			opposites := map[string]string{"up": "down", "down": "up", "left": "right", "right": "left"}
 			mu.Lock()
 			if s, ok := snakes[id]; ok && opposites[s.Direction] != msg.Direction {
 				s.Direction = msg.Direction
 			}
 			mu.Unlock()
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@step-08-interpolation/server/main.go` around lines 214 - 218, The code only
prevents reversing direction but doesn't validate that msg.Direction is one of
the allowed values; update the check around the opposites map and snake update
(the block using opposites, mu.Lock(), snakes[id], s.Direction and
msg.Direction) to first verify msg.Direction is in the allowed set
{"up","down","left","right"} (e.g., by checking membership in the opposites map)
and only then apply the existing reverse-direction guard before assigning
s.Direction; ensure you still hold mu while reading/updating snakes[id].
step-06-snake/client/index.html-67-70 (1)

67-70: ⚠️ Potential issue | 🟠 Major

接続ボタン連打でソケットを多重生成できます。

Line 67 で CONNECTING/OPEN のガードがないため、onopen 前の連打で接続リークが起きます。接続開始時点で再入防止を入れてください。

🔧 修正例
 btnConn.addEventListener('click', () => {
+  if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) {
+    return;
+  }
+  btnConn.disabled = true;
   ws = new WebSocket('ws://localhost:8080/ws');

   ws.onopen = () => setUI(true);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@step-06-snake/client/index.html` around lines 67 - 70, ボタン連打で複数の WebSocket
が作られる問題は、btnConn の click ハンドラで既に接続中か開いているかをチェックしていないため発生しています;btnConn の click
ハンドラ内でグローバル変数 ws の readyState を確認し(WebSocket.CONNECTING /
WebSocket.OPEN)、接続中または既に開いている場合は新規接続処理を無視するか btnConn を一時無効化してから ws = new
WebSocket(...) を作成し、onopen/onclose で setUI とボタン有効化を制御するように変更してください(参照シンボル:
btnConn, ws, WebSocket, setUI)。
step-06-snake/server/main.go-242-246 (1)

242-246: ⚠️ Potential issue | 🟠 Major

ブロードキャストの同期的な書き込みがゲームループをブロックする可能性があります。

Line 242 の json.Marshal と Line 245 の conn.WriteMessage のエラーが無視されています。より深刻なのは、broadcastState() が 150ms ごとのゲームループから同期的に呼ばれ、遅延クライアントへの WriteMessage が他すべてのプレイヤーへの配信を遅延させることです。SetWriteDeadline でタイムアウトを設定し、失敗した接続を clients マップから削除してください。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@step-06-snake/server/main.go` around lines 242 - 246, broadcastState()
currently ignores errors from json.Marshal and conn.WriteMessage and performs
synchronous writes which can block the 150ms game loop; change it to set a
per-connection write deadline using conn.SetWriteDeadline before each
WriteMessage, check and handle errors from json.Marshal and conn.WriteMessage,
and if a WriteMessage or deadline set fails remove that connection from the
shared clients map (safely, e.g. via the same mutex used when iterating
clients). Ensure json.Marshal error is handled (return/log and skip broadcasting
on failure) and treat per-connection write failures as dead connections to be
cleaned up to avoid blocking the loop.
step-10-reconnect/README.md-71-77 (1)

71-77: ⚠️ Potential issue | 🟠 Major

ping ループは接続終了時に停止する必要があります。

Line 71-77 の例は WriteMessage のエラーをチェックしておらず、接続が切断された後も goroutine が実行され続けるため、goroutine リークのリスクがあります。ticker.Stop() を defer で呼び出し、write エラー時に goroutine を終了させるべきです。

go func() {
    ticker := time.NewTicker(pingInterval)
    defer ticker.Stop()
    for {
        select {
        case <-ticker.C:
            if err := conn.WriteMessage(websocket.PingMessage, nil); err != nil {
                return
            }
        }
    }
}()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@step-10-reconnect/README.md` around lines 71 - 77, The ping goroutine uses a
Ticker and calls conn.WriteMessage(websocket.PingMessage, nil) but doesn't check
for errors or stop the ticker, causing a goroutine leak when the connection
closes; update the anonymous goroutine that creates ticker :=
time.NewTicker(pingInterval) to defer ticker.Stop(), perform the WriteMessage
call inside a select or directly check its returned error, and return from the
goroutine when WriteMessage returns an error so the goroutine exits cleanly on
connection close.
step-01-echo/server/main.go-16-18 (1)

16-18: ⚠️ Potential issue | 🟠 Major

CheckOrigin: return true は CSRF 脆弱性を招くため避けてください。

gorilla/websocket 公式ドキュメントは、CheckOrigin で「リクエストオリジンを慎重に検証して CSRF を防ぐ」ことを明記しており、すべてのオリジンを許可する実装は本番環境で推奨されていません。教材コードでも localhost に制限するのが安全な初期値です。

提案差分
 var upgrader = websocket.Upgrader{
 	CheckOrigin: func(r *http.Request) bool {
-		return true
+		origin := r.Header.Get("Origin")
+		return origin == "http://localhost:8080" ||
+			origin == "http://127.0.0.1:8080"
 	},
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@step-01-echo/server/main.go` around lines 16 - 18, The current CheckOrigin
function unconditionally returns true and allows any origin; replace it with
origin validation that only permits trusted origins (e.g., localhost or an
explicit allowlist). In the websocket Upgrader's CheckOrigin callback, read
r.Header.Get("Origin") and compare it against a set of allowed origins (or
validate the host is "localhost" / "127.0.0.1") and return true only on match;
otherwise return false. Update the Upgrader's CheckOrigin implementation (the
CheckOrigin function) to perform this check so CSRF is mitigated.
step-07-lag/server/main.go-124-140 (1)

124-140: ⚠️ Potential issue | 🟠 Major

未知の direction を弾かないと蛇が (0,0) に飛びます。

ここは逆方向だけを拒否しているので、"foo" や空文字でも s.Direction に入ります。すると moveSnake()switch がどの case にも入らず、次 tick でゼロ値の newHead が使われます。up/down/left/right だけを受け付けてください。

最小修正例
 		if msg.Type == "input" {
+			switch msg.Direction {
+			case "up", "down", "left", "right":
+			default:
+				continue
+			}
 			opposites := map[string]string{
 				"up": "down", "down": "up", "left": "right", "right": "left",
 			}

Also applies to: 241-249

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@step-07-lag/server/main.go` around lines 124 - 140, The moveSnake function
currently assumes snake.Direction is one of "up","down","left","right" and falls
back to a zero-valued newHead for unknown values; update moveSnake to explicitly
validate Direction and return early (or ignore the tick) when it is not one of
the four allowed values, e.g., check snake.Direction against the set
{"up","down","left","right"} before the switch and bail out if invalid; also
apply the same strict validation where direction changes are handled (the
reverse-direction check code referenced around lines 241-249) so only these four
literals can be assigned to Snake.Direction.
step-03-input/server/main.go-96-117 (1)

96-117: ⚠️ Potential issue | 🟠 Major

参加/離脱が他クライアントへ反映されません。

このステップは入力時にしか broadcast() していないため、接続直後のクライアントは既存 inputs を受け取れず、既存クライアントも新規参加や切断を次の入力まで知れません。tick 配信がない構成なので、参加/離脱時にもスナップショットを送ってください。

Also applies to: 144-145

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@step-03-input/server/main.go` around lines 96 - 117, The server only calls
broadcast() on input messages, so new connections and disconnects are not
propagated; after registering a new client in the connection handler (where you
set clients[playerID] and playerInputs[playerID] and send the ack via
ServerMessage) call the same broadcast() path to send a full snapshot to all
clients so the newcomer receives current inputs and others see the join;
likewise, inside the deferred disconnect cleanup (where you delete from clients
and playerInputs) invoke broadcast() after the deletion to notify others of the
leave (ensure broadcast() reads the current playerInputs map under mu to build
the snapshot).
step-07-lag/server/main.go-95-111 (1)

95-111: ⚠️ Potential issue | 🟠 Major

ジッター模擬がサーバー tick 自体を止めています。

ここで time.Sleep すると通信遅延ではなく gameLoop() 全体が停止します。delay > tickRate では ticker の tick も取りこぼすので、全クライアントのゲーム速度まで変わってしまいます。遅らせたいのが配信だけなら、その tick のスナップショットを別 goroutine / 送信キューで遅延送信してください。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@step-07-lag/server/main.go` around lines 95 - 111, The current gameLoop uses
time.Sleep inside the tick loop which blocks the ticker and halts updateGame;
instead, capture the tick's state snapshot immediately after updateGame (use the
same variables/struct you pass to broadcastState), then enqueue or spawn a
separate goroutine to perform the simulated send delay and call broadcastState
with that snapshot; keep the ticker loop non-blocking (remove time.Sleep from
gameLoop), reference gameLoop, updateGame, broadcastState, ticker and
maxRandomDelayMs and implement a send-queue or goroutine-per-tick that sleeps
and sends so ticks are never lost.
step-05-game-loop/server/main.go-132-160 (1)

132-160: ⚠️ Potential issue | 🟠 Major

tick ループでブロッキングな送信を直列実行しないでください。

broadcastState() は game loop の中で全接続へ順番に WriteMessage していますが、write deadline も失敗時の退避もありません。1 本の遅い接続で tick が止まり、全プレイヤーの進行が巻き込まれます。送信は専用 goroutine に切り出すか、最低でも deadline を付けて失敗接続を外してください。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@step-05-game-loop/server/main.go` around lines 132 - 160, broadcastState
currently does blocking sequential WriteMessage calls that can stall the game
loop; for each connection (from clients or clientsCopy) set a short write
deadline (conn.SetWriteDeadline(time.Now().Add(...))) before WriteMessage, check
the error return and on failure close the connection and remove its entry from
the shared clients map under mu.Lock (use mu.Lock()/Unlock() when mutating
clients), and optionally offload sends to per-connection goroutines so slow
clients don't block the tick loop—ensure you still enforce the deadline and
clean up closed connections (references: broadcastState, clients/clientsCopy,
WriteMessage, mu, tickCount, ServerMessage).
step-10-reconnect/server/main.go-98-113 (1)

98-113: ⚠️ Potential issue | 🟠 Major

direction のホワイトリスト検証が抜けています。

逆方向以外は何でも通るので、不正値で s.Direction が壊れます。moveSnake() は default を持たないため、次 tick で nh のゼロ値 (0,0) が使われます。Step 05 と同様に up/down/left/right のみ受け付けてください。

最小修正例
 		if msg.Type == "input" {
+			switch msg.Direction {
+			case "up", "down", "left", "right":
+			default:
+				continue
+			}
 			opposites := map[string]string{
 				"up": "down", "down": "up", "left": "right", "right": "left",
 			}

Also applies to: 239-247

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@step-10-reconnect/server/main.go` around lines 98 - 113, The switch on
s.Direction in moveSnake must whitelist allowed directions and avoid falling
through to a zero-value nh; update the switch in moveSnake to explicitly handle
only "up", "down", "left", "right" and add a default branch that returns (or
otherwise aborts movement) if s.Direction is invalid so nh is never left as
(0,0); apply the same whitelist+default fix to the other identical
direction-switch block elsewhere in this file (the other snake-movement switch
around the later movement code).
🟡 Minor comments (12)
step-08-interpolation/client/index.html-119-120 (1)

119-120: ⚠️ Potential issue | 🟡 Minor

初回ACK受信時に tickundefined 表示になります。

tickCount が数値のときだけUIを更新してください。

🔧 提案差分
-          tickEl.textContent = msg.tickCount;
+          if (typeof msg.tickCount === 'number') {
+            tickEl.textContent = String(msg.tickCount);
+          }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@step-08-interpolation/client/index.html` around lines 119 - 120, 初回ACKで
msg.tickCount が undefined のときに UI を更新してしまうため、tickEl.textContent = msg.tickCount
の代わりに msg.tickCount が数値であることを確認してから更新するよう修正してください(例: typeof msg.tickCount ===
'number' または Number.isFinite(msg.tickCount) を使ってチェック)。対象箇所は tickEl.textContent
を代入している行(tickEl.textContent = msg.tickCount;)を探して、条件判定を追加して数値の場合のみ textContent
を設定するようにしてください。
step-08-interpolation/client/index.html-123-129 (1)

123-129: ⚠️ Potential issue | 🟡 Minor

切断時にID/Tick/補間係数の表示をリセットしてください。

状態は消えているのに表示だけ残るので、再接続時に混乱しやすいです。

🔧 提案差分
       ws.onclose = () => {
         setUI(false);
         ws = null;
+        myId = null;
         rawState = { snakes: {} };
         prevSnapshot = null;
         nextSnapshot = null;
+        myIdEl.textContent = 'なし';
+        tickEl.textContent = '0';
+        tEl.textContent = '0.00';
       };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@step-08-interpolation/client/index.html` around lines 123 - 129, ws.onclose
ハンドラで接続切断時に表示だけ残る「ID / Tick / 補間係数」をリセットしていないため、再接続時に混乱します。ws.onclose の中(または
setUI(false) を呼ぶ直後)で画面表示をクリアする処理を追加してください:具体的には ws.onclose ハンドラ内で ID 表示、Tick
表示、補間係数(alpha) 表示に対応する DOM 要素や表示変数を空文字またはデフォルト値に設定してクリアする(あるいは setUI
を拡張してこれらのフィールドもリセットする)ように修正してください。参照箇所:ws.onclose
ハンドラ、setUI、prevSnapshot/nextSnapshot をリセットしている箇所。
step-06-snake/client/index.html-97-104 (1)

97-104: ⚠️ Potential issue | 🟡 Minor

切断後の表示リセットが不足しています。

Line 97 以降で state は空に戻していますが、myScoreplayerCountscoreboard が前回値のまま残ります。切断時にUI表示も初期化してください。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@step-06-snake/client/index.html` around lines 97 - 104, In the ws.onclose
handler you reset state and UI partially but forget to clear myScore,
playerCount and the scoreboard display; update the ws.onclose block (the handler
for ws.onclose) to reset myScore and playerCount variables to their initial
values, clear or rebuild the scoreboard DOM (scoreboard element) and any score
display elements (e.g., myScoreEl, playerCountEl or equivalents), and call
setUI(false) / draw() after those resets so the UI shows the initial empty state
on disconnect.
step-11-optimization/README.md-15-15 (1)

15-15: ⚠️ Potential issue | 🟡 Minor

コードフェンスに言語指定を追加してください(MD040)。

Line 15 / Line 38 / Line 116 のフェンスは言語未指定です。text などを付けて markdownlint 警告を解消してください。

📝 修正例
-```
+```text
 ...
-```
+```

Also applies to: 38-38, 116-116

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@step-11-optimization/README.md` at line 15, README.md contains three fenced
code blocks that use plain ``` without a language tag; update each of those
fences (the plain "```" occurrences in the step-11-optimization README) to
include a language specifier such as "text" (i.e., change ``` to ```text) so
markdownlint MD040 warnings are resolved.
step-06-snake/server/main.go-17-19 (1)

17-19: ⚠️ Potential issue | 🟡 Minor

CheckOrigin の無条件許可は開発用途に限定してください。

Step 1 で既に「開発用に全てのオリジンを許可(本番では制限すること)」とコメント記載されていますが、本番環境へのデプロイ時には必ず許可オリジンを明示してください。現在はローカルホストのみ(http://localhost:8080)での使用のため許容範囲ですが、Web に公開する際には以下のように修正が必要です。

修正例
 var upgrader = websocket.Upgrader{
-	CheckOrigin: func(r *http.Request) bool { return true },
+	CheckOrigin: func(r *http.Request) bool {
+		origin := r.Header.Get("Origin")
+		return origin == "https://yourdomain.com"
+	},
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@step-06-snake/server/main.go` around lines 17 - 19, upgrader の CheckOrigin
を無条件許可から限定許可に変更してください: websocket.Upgrader の変数名 upgrader とその CheckOrigin
ハンドラを修正して、r.Header.Get("Origin") を取得し whitelist(例: "http://localhost:8080"
または環境変数で指定したホスト群)と照合して合致する場合のみ true
を返すように実装し、本番では環境変数や設定ファイルで許可オリジンを管理するようにしてください。
step-04-server-state/README.md-15-15 (1)

15-15: ⚠️ Potential issue | 🟡 Minor

コードブロックに言語指定を追加してください(MD040)。

Line 15 / 29 / 46 / 129 のフェンスは text 等を指定してください。ドキュメント品質と CI 安定性が上がります。

Also applies to: 29-29, 46-46, 129-129

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@step-04-server-state/README.md` at line 15, README.md
の複数のフェンス付きコードブロックに言語指定が抜けているため MarkdownLint(MD040)
に引っかかっています。該当するすべてのフェンス付きコードブロックを探し、開始バックティックの直後に適切な言語タグ(例: text, json, js, bash
など)を追加して下さい(例: ```text)。これによりドキュメント品質と CI の安定性が改善されます。
step-03-input/README.md-16-16 (1)

16-16: ⚠️ Potential issue | 🟡 Minor

フェンス言語未指定を解消してください(MD040)。

Line 16 / 30 / 57 / 104 / 116 のフェンスは言語指定を付けると lint を通せます(例: json, go, text)。

Also applies to: 30-30, 57-57, 104-104, 116-116

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@step-03-input/README.md` at line 16, README.md の複数のフェンス付きコードブロックが言語指定なしで
MD040 を引いているため、該当するフェンス(現在バックティック3つで始まっている未指定のコードブロック)に適切な言語タグ(例: json, go, text
など)を追加して lint を通すようにしてください; 具体的には各未指定フェンスの開始行の ``` の後に適切な言語名を付与し(例:
```json)、内容に合わせた言語を選んで保存してください。
step-06-snake/README.md-15-15 (1)

15-15: ⚠️ Potential issue | 🟡 Minor

未指定フェンス言語の追記が必要です(MD040)。

Line 15 / Line 27 / Line 75 / Line 104 のフェンスに言語指定がありません。図やツリーは text 指定で十分です。

Also applies to: 27-27, 75-75, 104-104

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@step-06-snake/README.md` at line 15, The README's fenced code blocks that
contain diagrams/trees are missing a language tag (MD040); update each bare
fence to include a language (use "text" for diagrams/trees) so replace
occurrences of triple-backticks like ``` with ```text in the README's fenced
blocks (the diagram/tree code blocks) to satisfy the linter.
step-02-broadcast/README.md-15-15 (1)

15-15: ⚠️ Potential issue | 🟡 Minor

コードフェンスに言語指定を付けてください(MD040)。

Line 15 / Line 67 / Line 83 のフェンスが言語未指定です。text を付けるだけで markdownlint 警告を解消できます。

🛠️ 例(抜粋)
-```
+```text
 クライアントA → サーバー → クライアントA
                         → クライアントB
                         → クライアントC

- +text
step-02-broadcast/
├── README.md
├── server/
│ ├── main.go
│ └── go.mod
└── client/
└── index.html


-```
+```text
新しい接続 → clients に追加 → goroutine でメッセージ待機
                                   ↓
                               メッセージ受信
                                   ↓
                           全クライアントに送信
                                   ↓
                               切断 → clients から削除
</details>


Also applies to: 67-67, 83-83

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against the current code and only fix it if needed.

In @step-02-broadcast/README.md at line 15, README.md contains triple-backtick
code fences without a language tag; update each offending fence (the three shown
in the diff) by replacing the bare withtext so the markdownlint MD040
warnings are resolved (i.e., change each opening fence like totext in
the README.md code blocks).


</details>

</blockquote></details>
<details>
<summary>step-11-optimization/client/index.html-142-143 (1)</summary><blockquote>

`142-143`: _⚠️ Potential issue_ | _🟡 Minor_

**ゼロ除算の可能性があります**

`msg.fullSize` が 0 の場合、`msg.lightSize / msg.fullSize` でゼロ除算が発生し、`NaN` や `Infinity` が表示されます。サーバー側で常に正の値が送られることが想定されていても、防御的なチェックを追加することを検討してください。


<details>
<summary>🛡️ 修正案</summary>

```diff
-          const ratio = ((1 - msg.lightSize / msg.fullSize) * 100).toFixed(1);
+          const ratio = msg.fullSize > 0
+            ? ((1 - msg.lightSize / msg.fullSize) * 100).toFixed(1)
+            : '0.0';
```
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@step-11-optimization/client/index.html` around lines 142 - 143, The current
ratio calculation can divide by zero when msg.fullSize is 0; update the logic
around the ratio computation (the lines using msg.lightSize, msg.fullSize and
setting ratioEl.textContent) to defensively check msg.fullSize > 0 before doing
the division and to assign a sensible fallback (e.g., "0.0" or a placeholder)
when fullSize is zero or invalid so ratioEl.textContent never becomes
NaN/Infinity.
```

</details>

</blockquote></details>
<details>
<summary>step-07-lag/client/index.html-155-160 (1)</summary><blockquote>

`155-160`: _⚠️ Potential issue_ | _🟡 Minor_

**RTT(往復遅延)ではなく片道遅延です。**

`Date.now() - msg.serverTime` はサーバー送信時刻からクライアント受信時刻までの片道遅延(+クロックスキュー)を測定しています。コメントの「往復遅延」は技術的に不正確です。実際のRTTを測定するにはクライアントからpingを送信し、pongを受け取る時間を計測する必要があります。

教育目的のコードなので、コメントを「片道遅延(概算)」に修正するか、RTTの意味について注釈を追加することを検討してください。


<details>
<summary>📝 コメント修正の提案</summary>

```diff
-      // 往復遅延の概算(サーバー送信時刻 vs 受信時刻)
+      // 片道遅延の概算(サーバー送信時刻 vs クライアント受信時刻、クロック差含む)
       if (msg.serverTime) {
-        const rtt = Date.now() - msg.serverTime;
+        const oneWayDelay = Date.now() - msg.serverTime;
```
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@step-07-lag/client/index.html` around lines 155 - 160, The comment is
incorrect: Date.now() - msg.serverTime measures one-way delay (plus clock skew),
not RTT. Update the comment/label around the code that computes rtt (the rtt
variable computed from Date.now() - msg.serverTime and displayed via
rttEl.innerHTML) to say "片道遅延(概算)" or add a brief note that this is one-way
delay and not true RTT, and optionally mention that true RTT requires a
client-side ping/pong measurement; no logic changes needed.
```

</details>

</blockquote></details>
<details>
<summary>step-04-server-state/server/main.go-116-128 (1)</summary><blockquote>

`116-128`: _⚠️ Potential issue_ | _🟡 Minor_

**ラップアラウンドの境界値が非対称です。**

`X < 0` の場合は `fieldWidth` に設定されますが、これはフィールド外の座標になります(有効範囲は `0` から `fieldWidth - 1`)。一方、`X > fieldWidth` の場合は `0` に設定されます。

一貫性のため、`fieldWidth - moveSpeed` または左端の座標に設定することを検討してください。ただし、視覚的な影響は小さいです。


<details>
<summary>📐 境界値の修正提案</summary>

```diff
 	if p.X < 0 {
-		p.X = fieldWidth
+		p.X = fieldWidth - moveSpeed
 	}
-	if p.X > fieldWidth {
+	if p.X >= fieldWidth {
 		p.X = 0
 	}
 	if p.Y < 0 {
-		p.Y = fieldHeight
+		p.Y = fieldHeight - moveSpeed
 	}
-	if p.Y > fieldHeight {
+	if p.Y >= fieldHeight {
 		p.Y = 0
 	}
```
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@step-04-server-state/server/main.go` around lines 116 - 128,
ラップアラウンドの境界が非対称でフィールド外(有効範囲は 0..fieldWidth-1 /
0..fieldHeight-1)を生成しているので、p.X/p.Y のラップ処理を修正してください: 現在の p.X < 0 / p.X >
fieldWidth / p.Y < 0 / p.Y > fieldHeight の分岐を、範囲が 0 から fieldWidth-1(または
fieldHeight-1)になるように変え、負側は fieldWidth-1 / fieldHeight-1 に設定し、超過側は >= fieldWidth
/ >= fieldHeight を検出して 0 に戻す(もしくは moveSpeed の影響を考慮するなら fieldWidth - moveSpeed
などを代入する)—該当箇所は p.X, p.Y, fieldWidth, fieldHeight(および moveSpeed を使う場合は
moveSpeed)を扱うラップアラウンドのブロックを修正してください。
```

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (21)</summary><blockquote>

<details>
<summary>README.md (1)</summary><blockquote>

`74-96`: **フェンスコードブロックに言語指定を追加してください。**

静的解析ツール(markdownlint)がフェンスコードブロックに言語指定がないことを警告しています。プレーンテキストの場合は `text` または `plaintext` を指定することで、マークダウンレンダリングの一貫性が向上します。


<details>
<summary>📝 修正案</summary>

```diff
-```
+```text
 Step 0〜2 → 最速で終わらせる(接続の感触をつかむ)
```

```diff
-```
+```text
 step-XX-name/
 ├── README.md      # 概念説明・コード解説・練習問題
```

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against the current code and only fix it if needed.

In @README.md around lines 74 - 96, Update the README.md fenced code blocks to
include a language token (e.g., text or plaintext) so markdownlint stops
warning; specifically modify the two blocks that show the step descriptions and
the folder tree (the triple-backtick fences surrounding "Step 0〜2 → ..." and the
"step-XX-name/ ├── README.md ..." tree) to start with text (or plaintext)
instead of just ```, preserving the existing content and spacing.


</details>

</blockquote></details>
<details>
<summary>step-03-input/client/index.html (1)</summary><blockquote>

`90-107`: **`JSON.parse` にエラーハンドリングを追加することを検討してください。**

サーバーから不正なJSONが送信された場合、`JSON.parse` が例外をスローし、`onmessage` ハンドラ全体が失敗します。教材としては現状でも問題ありませんが、堅牢性を高めるためにtry-catchの追加を検討してください。


<details>
<summary>🛡️ 修正案</summary>

```diff
       ws.onmessage = (event) => {
-        const msg = JSON.parse(event.data);
+        let msg;
+        try {
+          msg = JSON.parse(event.data);
+        } catch (e) {
+          console.error('JSONパースエラー:', e);
+          return;
+        }
 
         if (msg.type === 'ack') {
```

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@step-03-input/client/index.html` around lines 90 - 107, Wrap the JSON.parse
call inside the ws.onmessage handler with try-catch to prevent a malformed
server message from throwing and breaking the handler: catch the error,
optionally log it (e.g., via console.error) and return early so subsequent logic
(the ack/state branches that use myId, updatePlayersDisplay, myDirEl, ARROWS)
only runs for valid parsed messages; ensure you still handle valid msg.type
values as before.
```

</details>

</blockquote></details>
<details>
<summary>step-07-lag/README.md (1)</summary><blockquote>

`94-102`: **ファイル構成のコードブロックに言語指定を追加してください。**

静的解析ツール(markdownlint MD040)が言語指定のないフェンスコードブロックを警告しています。


<details>
<summary>📝 修正案</summary>

```diff
-```
+```text
 step-07-lag/
 ├── README.md
 ├── server/
 │   ├── main.go
 │   └── go.mod
 └── client/
     └── index.html
 ```
```

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against the current code and only fix it if needed.

In @step-07-lag/README.md around lines 94 - 102, The fenced code block in
README.md showing the file tree lacks a language tag which triggers markdownlint
MD040; update that block (the triple-backtick fence surrounding the tree) to
include a language identifier such as "text" (e.g., change totext) so
the code fence is explicit and the linter warning is resolved.


</details>

</blockquote></details>
<details>
<summary>step-02-broadcast/client/index.html (1)</summary><blockquote>

`80-94`: **`JSON.parse` にエラーハンドリングを追加することを検討してください。**

step-03と同様に、サーバーから不正なJSONが送信された場合に備えてtry-catchの追加を検討してください。


<details>
<summary>🛡️ 修正案</summary>

```diff
       ws.onmessage = (event) => {
-        // サーバーから受け取るのは JSON 形式
-        const msg = JSON.parse(event.data);
+        // サーバーから受け取るのは JSON 形式
+        let msg;
+        try {
+          msg = JSON.parse(event.data);
+        } catch (e) {
+          addLog('JSONパースエラー', 'error');
+          return;
+        }
 
         if (msg.type === 'system') {
```

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@step-02-broadcast/client/index.html` around lines 80 - 94, Wrap the
ws.onmessage handler's JSON.parse in a try-catch to guard against invalid JSON:
inside ws.onmessage, attempt const msg = JSON.parse(event.data) in a try block
and on catch call addLog with a system/error message (e.g.,
"受信したメッセージの解析に失敗しました") and return early; only access msg.type,
addLog(msg.content, 'chat'/'system') and update counter.textContent when parsing
succeeds and msg.count is present to avoid runtime errors when msg is malformed.
```

</details>

</blockquote></details>
<details>
<summary>step-10-reconnect/README.md (1)</summary><blockquote>

`118-121`: **再接続は固定間隔より指数バックオフ+ジッター推奨です。**

Line 120 の固定 3 秒リトライだと同時再接続が集中しやすいです。指数バックオフと上限を入れると実運用に近づきます。  
 

<details>
<summary>📶 改善イメージ</summary>

```diff
 ws.onclose = () => {
-    setTimeout(connect, 3000);
+    retryCount++;
+    const base = Math.min(30000, 1000 * (2 ** retryCount));
+    const jitter = Math.floor(Math.random() * 500);
+    setTimeout(connect, base + jitter);
 };
```
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@step-10-reconnect/README.md` around lines 118 - 121, Replace the fixed 3s
retry in the ws.onclose handler with an exponential backoff plus jitter
strategy: introduce a retry state (e.g., retryDelayMs starting at something like
500ms), cap (maxRetryMs), and multiply delay on each failed connect attempt
inside connect(), add randomized jitter (± some percent or random value) to
avoid thundering herd, schedule reconnects using the computed delay instead of
setTimeout(connect, 3000), and reset retryDelayMs back to initial value when the
socket successfully opens (e.g., in ws.onopen).
```

</details>

</blockquote></details>
<details>
<summary>step-01-echo/server/main.go (1)</summary><blockquote>

`55-59`: **WebSocket メッセージの受信サイズ上限を設定すると安全です。**

接続アップグレード後、メッセージ受信ループの前に `SetReadLimit` を設定することで、過大フレームによるメモリ圧迫やDoS攻撃のリスクを低減できます。この設定により、制限を超えるメッセージを受信した場合、接続は自動的にWebSocket の close コード 1009(message too big)を送信して接続を閉じます。

<details>
<summary>提案差分</summary>

```diff
 	log.Println("新しいクライアントが接続しました")
+	conn.SetReadLimit(4 * 1024) // 教材用途の小さな上限例

 	// メッセージを受け取り続けるループ
 	for {
```
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@step-01-echo/server/main.go` around lines 55 - 59, Set a read limit on the
WebSocket connection before entering the receive loop to prevent oversized
frames from exhausting memory: call conn.SetReadLimit with an appropriate max
size (e.g., a few kilobytes) immediately after the upgrade/successful connection
and before the for { ... conn.ReadMessage() } loop in main.go so that
ReadMessage will close the connection with close code 1009 when a message
exceeds the limit.
```

</details>

</blockquote></details>
<details>
<summary>step-01-echo/client/index.html (1)</summary><blockquote>

`179-185`: **`ws.send()` 前に `readyState` を確認してください。**

Line 184 で `ws` が存在していても、`CONNECTING` 状態なら `InvalidStateError` が発生します。また、`CLOSING` や `CLOSED` 状態ではエラーは起こりませんが、メッセージは無視されます。送信前に `WebSocket.OPEN` の状態を確認することで、これらの問題を防げます。

<details>
<summary>🔧 提案差分</summary>

```diff
 function sendMessage() {
   const text = input.value.trim();
-  if (!text || !ws) return;
+  if (!text || !ws || ws.readyState !== WebSocket.OPEN) return;

   // メッセージを送信
   ws.send(text);
   addLog(`送信: ${text}`, 'send');
```
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@step-01-echo/client/index.html` around lines 179 - 185, The sendMessage
function currently calls ws.send(text) without checking the socket state; before
sending, check ws.readyState === WebSocket.OPEN to ensure the socket is open,
and if not call addLog with a warning (e.g., "送信失敗: 接続が開いていません") or handle
reconnection; update the sendMessage function (reference: sendMessage, ws.send,
ws.readyState, WebSocket.OPEN, addLog) to perform this readiness check and
early-return or log when the socket is not open.
```

</details>

</blockquote></details>
<details>
<summary>step-10-reconnect/client/index.html (2)</summary><blockquote>

`136-142`: **再接続ロジックは機能しますが、指数バックオフの検討を**

現在は固定3秒間隔で再接続を試みますが、サーバーがダウンしている場合、頻繁な再接続試行が続きます。教育目的であれば問題ありませんが、本番環境では指数バックオフ(例: 3秒 → 6秒 → 12秒...)が推奨されます。


<details>
<summary>💡 指数バックオフの例</summary>

```javascript
const BASE_DELAY = 3000;
const MAX_DELAY = 30000;

// onclose内で
if (autoReconnect) {
  reconnectCount++;
  const delay = Math.min(BASE_DELAY * Math.pow(2, reconnectCount - 1), MAX_DELAY);
  addLog(`${delay/1000}秒後に再接続します(${reconnectCount}回目)`, 'info');
  reconnectTimer = setTimeout(connect, delay);
}
```
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@step-10-reconnect/client/index.html` around lines 136 - 142, The reconnect
logic currently always uses RECONNECT_DELAY causing repeated fixed-interval
retries; change it to an exponential backoff by introducing constants (e.g.,
BASE_DELAY and MAX_DELAY), increment reconnectCount as now, compute delay =
Math.min(BASE_DELAY * 2 ** (reconnectCount - 1), MAX_DELAY), use that delay for
addLog and setTimeout (replacing RECONNECT_DELAY), and ensure reconnectTimer is
assigned accordingly; keep existing symbols autoReconnect, reconnectCount,
reconnectCountEl, reconnectTimer, and connect so the change is localized.
```

</details>

---

`76-83`: **ログ要素の無制限な成長**

長時間の接続/切断サイクルでログエントリが蓄積し続けます。教育目的のデモでは問題になりにくいですが、履歴を制限する(例: 最新100件のみ保持)ことを検討してもよいでしょう。

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@step-10-reconnect/client/index.html` around lines 76 - 83, The addLog
function allows unbounded growth of logEl which can exhaust memory over long
reconnect cycles; modify addLog (and any code that appends to logEl) to enforce
a maximum history (e.g., 100 entries): after appending the new div, if
logEl.childElementCount exceeds the cap, remove the oldest children (shift out
from the top) until the count is at or below the cap, then keep the existing
scroll behavior. Ensure you reference addLog and the logEl element when
implementing the trimming logic.
```

</details>

</blockquote></details>
<details>
<summary>step-05-game-loop/README.md (1)</summary><blockquote>

`15-17`: **コードブロックの言語指定子**

擬似コードやファイル構成を示すコードブロックに `text` の言語指定子を追加すると、Markdownリンターの警告が解消されます。




Also applies to: 26-34, 85-93

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against the current code and only fix it if needed.

In @step-05-game-loop/README.md around lines 15 - 17, Add the Markdown language
specifier "text" to the code fences that contain pseudo-code or file-structure
snippets (for example the block containing "キー入力 → サーバーへ送信 → サーバーが状態更新 → 全員に配信"
and the other similar blocks later in the README) so the linter stops warning;
update each triple-backtick fence to start with ```text and keep the contents
unchanged.


</details>

</blockquote></details>
<details>
<summary>step-09-rooms/README.md (3)</summary><blockquote>

`40-51`: **Room構造体に `running` フィールドが欠けています**

実際の実装(`step-09-rooms/server/main.go:73-80`)では `running bool` フィールドが含まれており、これはゲームループの重複起動を防ぐために重要です。ドキュメントでも「`room.running` フラグで管理し、起動済みなら起動しない」と説明しているため、構造体の例に含めると一貫性が保たれます。


<details>
<summary>📝 修正案</summary>

```diff
 type Room struct {
     ID      string
     Snakes  map[string]*Snake
     Clients map[string]*websocket.Conn
     Foods   []Food
     mu      sync.RWMutex
     stopCh  chan struct{} // ゲームループを止めるためのチャンネル
+    running bool          // ゲームループが起動済みかどうか
 }
```
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@step-09-rooms/README.md` around lines 40 - 51, The Room struct example is
missing the running bool field used to prevent duplicated game-loop starts;
update the Room declaration to include a running bool (alongside existing fields
ID, Snakes, Clients, Foods, mu, stopCh) and ensure code that starts the loop
(e.g., StartGame/Run loop) checks and sets room.running under the room.mu lock
to avoid concurrent starts and clears it when the loop stops.
```

</details>

---

`65-75`: **`findOrCreateRoom` の例にミューテックスロックが欠けています**

教育目的で簡略化されていますが、実際の実装(`step-09-rooms/server/main.go:244-280`)では `globalMu.Lock()` が必須です。後の「よくある設計ミス」セクションでロック順序の問題を説明しているため、この例でもロックの存在を示した方が学習者の理解に役立ちます。

また、`len(room.Snakes)` は実装の `room.PlayerCount()` メソッドと異なります。


<details>
<summary>📝 修正案</summary>

```diff
 func findOrCreateRoom() *Room {
+    globalMu.Lock()
+    defer globalMu.Unlock()
+
     for _, room := range rooms {
-        if len(room.Snakes) < MaxPlayersPerRoom {
+        if room.PlayerCount() < MaxPlayersPerRoom {
             return room
         }
     }
     // 満室なら新しい部屋を作る
     return createRoom()
 }
```
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@step-09-rooms/README.md` around lines 65 - 75, The example findOrCreateRoom
function is missing the global mutex and uses len(room.Snakes) instead of the
room's PlayerCount() method; update findOrCreateRoom to acquire globalMu.Lock()
at the start and defer globalMu.Unlock() around the scan/create block, and
replace len(room.Snakes) with room.PlayerCount(); keep the call to createRoom()
when no room has capacity and ensure the lock covers both the check and the
create to match the real implementation.
```

</details>

---

`15-27`: **コードブロックに言語指定子を追加することを検討してください**

Markdownのコードブロックに言語指定子(例: `text` や `plaintext`)を追加すると、リンターの警告を解消し、一部のレンダラーでの表示も改善されます。




Also applies to: 30-34, 104-110

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against the current code and only fix it if needed.

In @step-09-rooms/README.md around lines 15 - 27, Add language specifiers to the
Markdown code fences in the README code blocks (the fenced blocks that currently
start with and contain the three issue bullets) to silence linter warnings and improve rendering; update each fence (notably the blocks around the problem list and the other ranges called out) to use a neutral specifier like "text" or "plaintext" (i.e., change to ```text) so the code blocks are explicitly
typed while leaving the block contents unchanged.


</details>

</blockquote></details>
<details>
<summary>step-12-advanced/README.md (1)</summary><blockquote>

`15-28`: **コードブロックの言語指定子**

一部のコードブロックに `text` または `plaintext` の言語指定子を追加すると、Markdownリンターの警告が解消されます。これは必須ではありませんが、ドキュメントの一貫性向上に役立ちます。




Also applies to: 36-52, 114-124

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against the current code and only fix it if needed.

In @step-12-advanced/README.md around lines 15 - 28, Several fenced code blocks
in the README lack a language specifier (e.g., the block showing the step list
from the diff and the other blocks at ranges 36-52 and 114-124); add a
consistent language tag like text (or plaintext) to each of those fenced
code blocks so the Markdown linter warnings are resolved and formatting is
consistent across the README.md; update each fence around the step list and the
other mentioned blocks to include the chosen specifier.


</details>

</blockquote></details>
<details>
<summary>step-07-lag/client/index.html (1)</summary><blockquote>

`196-196`: **未使用の変数 `startIdx` があります。**

`startIdx` は宣言されていますが使用されていません。削除するか、意図した用途があれば実装を確認してください。


<details>
<summary>🧹 削除提案</summary>

```diff
-      const startIdx = Math.max(0, intervals.length - MAX_HISTORY);
       for (let i = 0; i < intervals.length; i++) {
```
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@step-07-lag/client/index.html` at line 196, The local variable startIdx is
declared but never used; either remove the declaration or apply it to limit
history by slicing intervals. Locate the declaration of startIdx (const startIdx
= Math.max(0, intervals.length - MAX_HISTORY)) and either delete that line, or
use startIdx when building the history (e.g., replace usages of intervals with
intervals.slice(startIdx) where the recent MAX_HISTORY entries are intended) so
intervals and MAX_HISTORY are actually applied.
```

</details>

</blockquote></details>
<details>
<summary>step-04-server-state/server/main.go (1)</summary><blockquote>

`97-99`: **`WriteMessage` のエラーを無視すると、切断済み接続がマップに残る可能性があります。**

送信に失敗した接続をクリーンアップしないと、`broadcast` が毎回失敗した接続に書き込みを試み、パフォーマンスに影響する可能性があります。教育目的のコードなので許容範囲ですが、Step 2の `broadcast` 実装のようにエラー時に接続を削除するパターンを参照してください。


<details>
<summary>♻️ エラーハンドリングの追加</summary>

```diff
 	for _, conn := range clientsCopy {
-		conn.WriteMessage(websocket.TextMessage, data)
+		if err := conn.WriteMessage(websocket.TextMessage, data); err != nil {
+			log.Printf("送信エラー: %v", err)
+			// Note: クリーンアップはhandleWebSocketのdeferで行われる
+		}
 	}
```
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@step-04-server-state/server/main.go` around lines 97 - 99, The loop that
sends data with conn.WriteMessage ignores errors and can leave disconnected
clients in the clients map; update the broadcast/send loop (the for _, conn :=
range clientsCopy { conn.WriteMessage(websocket.TextMessage, data) }) to check
the returned error, and on error remove that client from the underlying clients
map (the same pattern used in the earlier broadcast implementation) and close
the connection so failed connections are cleaned up and not retried on
subsequent broadcasts.
```

</details>

</blockquote></details>
<details>
<summary>step-09-rooms/server/main.go (1)</summary><blockquote>

`164-168`: **重複したコードブロックがあります。**

両方の条件分岐で同じ処理(尾の切り詰め)を行っています。条件を簡略化できます。


<details>
<summary>♻️ 条件の簡略化</summary>

```diff
-	if !ate && len(snake.Body) > initialSnakeLen {
-		snake.Body = snake.Body[:len(snake.Body)-1]
-	} else if !ate {
+	if !ate {
 		snake.Body = snake.Body[:len(snake.Body)-1]
 	}
```
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@step-09-rooms/server/main.go` around lines 164 - 168, The two branches both
trim the snake tail, so simplify the logic by removing the duplicated branch and
just trim when the snake did not eat: replace the if/else-if that checks !ate
and len(snake.Body) > initialSnakeLen with a single check for !ate (and ensure
you guard against empty body before slicing); update the code that references
snake.Body, ate, and initialSnakeLen to perform a single snake.Body =
snake.Body[:len(snake.Body)-1] only when !ate and length > 0.
```

</details>

</blockquote></details>
<details>
<summary>step-11-optimization/server/main.go (2)</summary><blockquote>

`203-206`: **`WriteMessage` のエラーを無視しています。**

他のステップと同様、送信エラーのログ出力を追加することを検討してください。ゲームループからの送信なので、エラー時の接続クリーンアップは `handleWebSocket` の defer で行われますが、デバッグ目的でのログ出力は有用です。

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@step-11-optimization/server/main.go` around lines 203 - 206, The loop calling
conn.WriteMessage on each connection (variables cc, fullData, statsData, and
websocket.TextMessage) currently ignores returned errors; update it to check the
error return from each WriteMessage and log failures (including which
payload—fullData or statsData—and identifying info about the connection) so send
failures are visible for debugging; connection cleanup can remain handled by
handleWebSocket's defer, but ensure you log the WriteMessage error with context
to aid investigation.
```

</details>

---

`167-206`: **軽量版データ(lightData)は計算されますが送信されていません。**

`lightData` はサイズ比較のために計算されていますが、実際にクライアントに送信されるのは `fullData` のみです。これが意図的な設計(サイズ比較のデモ目的)であれば問題ありませんが、README や実装コメントで明示すると学習者の混乱を避けられます。


<details>
<summary>📝 コメント追加の提案</summary>

```diff
+	// 注意: 軽量版はサイズ比較のために計算するが、実際には送信しない
+	// 教育目的でサイズ削減効果を可視化するためのデモ
 	// 全クライアントに送信(全量版 + 統計)
 	for _, conn := range cc {
 		conn.WriteMessage(websocket.TextMessage, fullData)
```
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Verify each finding against the current code and only fix it if needed.

In `@step-11-optimization/server/main.go` around lines 167 - 206, The code builds
a lightweight payload (lightData from lightMsg/LightState) but only sends
fullData to clients (loop over cc using conn.WriteMessage), which is confusing;
either send the lightweight payload to clients or clearly document that
lightData is only computed for size comparison. Fix by one of two options: 1) If
you intend to deliver the light payload, add a send call alongside fullData in
the client broadcast loop (use conn.WriteMessage(websocket.TextMessage,
lightData) referencing cc and conn.WriteMessage), or 2) if it’s only for
measurement, add a brief comment above the lightData/lightMsg construction
and/or update README to state that lightData is computed only for size
comparison and intentionally not transmitted. Ensure references to lightData,
lightMsg/LightState, fullData, cc, and conn.WriteMessage are updated
accordingly.
```

</details>

</blockquote></details>
<details>
<summary>step-01-echo/README.md (1)</summary><blockquote>

`70-77`: **フェンスドコードブロックに言語指定を追加することを検討してください。**

静的解析ツールがMD040警告を出しています。疑似コードやフロー説明でも、`text`や`plaintext`を指定するとリンターの警告が解消されます。


<details>
<summary>📝 提案</summary>

```diff
-```
+```text
 接続の流れ:
 1. クライアントが ws://localhost:8080/ws に接続を要求
```
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against the current code and only fix it if needed.

In @step-01-echo/README.md around lines 70 - 77, Update the fenced code block
that begins with "" before the Japanese flow text "接続の流れ:" to include a language specifier (e.g., text or plaintext) so the lint warning MD040 is resolved; edit the README.md block containing the flow steps (the triple-backtick block with the lines "1. クライアントが ws://localhost:8080/ws に接続を要求" through "5. 受け取ったメッセージをそのまま返す") and change the opening fence to text (or

step-02-broadcast/server/main.go (1)

136-141: getClientCount()broadcast の間で競合状態が発生する可能性があります。

ログ出力(Line 136)と getClientCount()(Line 140)の間で接続数が変わる可能性があります。教育目的のコードでは許容範囲ですが、実運用では単一のロックスコープ内で両方の操作を行うことを検討してください。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@step-02-broadcast/server/main.go` around lines 136 - 141, The log and
broadcast call can observe a different client count due to a race between
log.Printf, getClientCount() and broadcast; fix by performing the client-count
read and the broadcast under the same lock or atomic snapshot: acquire the same
clients mutex used by getClientCount() (or add a function like
broadcastWithLockedCount) and while holding it compute the count and call
broadcast (or call broadcast with the precomputed count) so log.Printf, count
computation (getClientCount) and the Message send occur within a single critical
section; update references to getClientCount(), broadcast and Message
accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9a4ea5eb-b266-4b2b-b1fc-57cb11cf8739

📥 Commits

Reviewing files that changed from the base of the PR and between 3e8eaef and 6db03fb.

📒 Files selected for processing (47)
  • README.md
  • step-00-http-vs-websocket/README.md
  • step-01-echo/README.md
  • step-01-echo/client/index.html
  • step-01-echo/server/go.mod
  • step-01-echo/server/main.go
  • step-02-broadcast/README.md
  • step-02-broadcast/client/index.html
  • step-02-broadcast/server/go.mod
  • step-02-broadcast/server/main.go
  • step-03-input/README.md
  • step-03-input/client/index.html
  • step-03-input/server/go.mod
  • step-03-input/server/main.go
  • step-04-server-state/README.md
  • step-04-server-state/client/index.html
  • step-04-server-state/server/go.mod
  • step-04-server-state/server/main.go
  • step-05-game-loop/README.md
  • step-05-game-loop/client/index.html
  • step-05-game-loop/server/go.mod
  • step-05-game-loop/server/main.go
  • step-06-snake/README.md
  • step-06-snake/client/index.html
  • step-06-snake/server/go.mod
  • step-06-snake/server/main.go
  • step-07-lag/README.md
  • step-07-lag/client/index.html
  • step-07-lag/server/go.mod
  • step-07-lag/server/main.go
  • step-08-interpolation/README.md
  • step-08-interpolation/client/index.html
  • step-08-interpolation/server/go.mod
  • step-08-interpolation/server/main.go
  • step-09-rooms/README.md
  • step-09-rooms/client/index.html
  • step-09-rooms/server/go.mod
  • step-09-rooms/server/main.go
  • step-10-reconnect/README.md
  • step-10-reconnect/client/index.html
  • step-10-reconnect/server/go.mod
  • step-10-reconnect/server/main.go
  • step-11-optimization/README.md
  • step-11-optimization/client/index.html
  • step-11-optimization/server/go.mod
  • step-11-optimization/server/main.go
  • step-12-advanced/README.md

github.com/mattn/go-isatty v0.0.20 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
golang.org/x/crypto v0.17.0 // indirect
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

golang.org/x/crypto v0.17.0 vulnerability CRITICAL CVE

💡 Result:

golang.org/x/crypto v0.17.0 is affected by a critical vulnerability CVE-2024-45337 (GO-2024-3321, GHSA-v778-237x-gjrc). This is an authorization bypass in the SSH package due to misuse of ServerConfig.PublicKeyCallback. Affected versions are before v0.31.0. CVSS score: 9.1 (CRITICAL). Multiple projects and scanners flag v0.17.0 specifically as vulnerable to this CVE. No other critical CVEs are explicitly tied to exactly v0.17.0; later CVEs like CVE-2025-47914 and CVE-2025-58181 affect versions before v0.45.0 but are moderate severity. Recommendation: Update to v0.31.0 or later (latest as of 2026-03-31 is around v0.49.0).

Citations:


🏁 Script executed:

# Check if there are Go vulnerability databases we can query
# Let's also look at what version the code is using and common vulnerabilities
curl -s https://api.github.com/repos/golang/crypto/releases 2>/dev/null | head -100 || echo "API unavailable in sandbox"

Repository: NAGA-29/WebSocket-Learn

Length of output: 346


🏁 Script executed:

# Search for known Go crypto library vulnerabilities
# Check common sources for golang.org/x/crypto issues
rg "golang\.org/x/crypto" --type go -A 2 -B 2 2>/dev/null || echo "Searching..."

Repository: NAGA-29/WebSocket-Learn

Length of output: 79


golang.org/x/crypto v0.17.0 を修正済みバージョンに更新してください。

Line 16 の golang.org/x/crypto v0.17.0CVE-2024-45337 に該当する既知のCRITICAL脆弱性を含んでいます(CVSS 9.1)。SSHパッケージの認可バイパスに関する脆弱性であり、v0.31.0未満のすべてのバージョンが影響を受けます。v0.31.0以上(推奨:最新の v0.49.0)へ更新してください。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@step-03-input/server/go.mod` at line 16, Update the vulnerable dependency
declaration "golang.org/x/crypto v0.17.0" in go.mod to a patched release >=
v0.31.0 (recommended v0.49.0) to address CVE-2024-45337; edit the module version
entry in go.mod (the line containing "golang.org/x/crypto") to the new version
and run module tidy (or use "go get golang.org/x/crypto@v0.49.0") to ensure
go.sum is updated and dependencies are re-resolved.

Comment thread step-03-input/server/main.go Outdated
Comment thread step-03-input/server/main.go
Comment thread step-05-game-loop/server/main.go
Comment thread step-06-snake/server/main.go Outdated
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
golang.org/x/crypto v0.17.0 // indirect
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 依存バージョンの現状確認(read-only)
fd -a go.mod --exec rg -n 'golang.org/x/crypto'
fd -a go.sum --exec rg -n 'golang.org/x/crypto'

Repository: NAGA-29/WebSocket-Learn

Length of output: 550


🌐 Web query:

golang.org/x/crypto GO-2024-3321 GO-2025-3487 の修正済み最小バージョンは何ですか?

💡 Result:

golang.org/x/crypto の GO-2024-3321 の修正済み最小バージョンは v0.31.0、GO-2025-3487 の修正済み最小バージョンは v0.35.0 です。両方を修正するには v0.35.0 以上が必要です。

Citations:


golang.org/x/crypto v0.17.0 は GO-2024-3321 と GO-2025-3487 の既知脆弱性を含むため更新が必須です。

この脆弱性を完全に修正するには v0.35.0 以上への更新が必要です。Line 16 のバージョンを go get golang.org/x/crypto@latest && go mod tidy で最新版に更新してください。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@step-08-interpolation/server/go.mod` at line 16, The dependency
golang.org/x/crypto is pinned at v0.17.0 and contains known vulnerabilities
(GO-2024-3321, GO-2025-3487); update the golang.org/x/crypto entry in go.mod to
a secure release (v0.35.0 or newer) and refresh module metadata (update the
dependency and run module tidy) so the go.mod/go.sum reflect the upgraded
version.

Comment thread step-08-interpolation/server/main.go Outdated
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
golang.org/x/crypto v0.17.0 // indirect
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 各stepの go.mod で同じ脆弱性があるか確認
echo "=== golang.org/x/crypto バージョン一覧 ==="
fd -t f 'go.mod' --exec grep -l 'golang.org/x/crypto' {} \; | xargs -I{} sh -c 'echo "File: {}"; grep "golang.org/x/crypto" {}'

Repository: NAGA-29/WebSocket-Learn

Length of output: 980


🌐 Web query:

golang.org/x/crypto v0.17.0 CVE GO-2024-3321 vulnerabilities

💡 Result:

golang.org/x/crypto v0.17.0, released on December 18, 2023, is vulnerable to GO-2024-3321 (CVE-2024-45337). This vulnerability affects the ssh subpackage and enables authorization bypass due to misuse of ServerConfig.PublicKeyCallback in SSH servers. It impacts all versions before v0.31.0, released December 11, 2024. Upgrade to v0.31.0 or later to mitigate. No other CVEs directly tied to v0.17.0 were identified in the searches.

Citations:


🌐 Web query:

golang.org/x/crypto GHSA-v778-237x-gjrc CRITICAL vulnerability

💡 Result:

GHSA-v778-237x-gjrc (CVE-2024-45337, GO-2024-3321) is a critical vulnerability (CVSS 9.1) in golang.org/x/crypto/ssh affecting versions before v0.31.0. It allows authorization bypass in SSH servers misusing ServerConfig.PublicKeyCallback. The SSH protocol permits clients to query acceptability of multiple public keys before authenticating with one. The callback may be invoked for several keys, but vulnerable applications might authorize based on a queried key (e.g., key B) while the client authenticates with another (e.g., key A) they control, bypassing checks. Fix: Upgrade to golang.org/x/crypto v0.31.0 or later, which implements a 1-entry FIFO cache for the public key, ensuring the last callback key matches the authentication key. Use Permissions.Extensions for authorization. As of 2026-03-31, later versions like v0.38.0 and v0.49.0 exist and include the fix.

Citations:


🌐 Web query:

golang.org/x/crypto v0.31.0 safe version release notes

💡 Result:

golang.org/x/crypto v0.31.0 was released on December 11, 2024. It addresses CVE-2024-45337 (GO-2024-3321), a security vulnerability in the ssh package where misuse of ServerConfig.PublicKeyCallback could lead to authorization bypass. The fix changes the public key cache to a 1-entry FIFO cache, ensuring the last key passed to PublicKeyCallback is the one used for authentication when public key auth succeeds. This is a partial mitigation; users should use the Permissions.Extensions field for authorization decisions. Commit: b4f1988a35dee11ec3e05d6bf3e90b695fbd8909 (ssh: make the public key cache a 1-entry FIFO cache, fixes golang/go#70779, CVE-2024-45337). The version is safe regarding this vulnerability and is recommended over earlier versions like v0.30.0. As of 2026-03-31, newer versions (e.g., v0.49.0) exist, but v0.31.0 resolves the key issue.

Citations:


golang.org/x/crypto に重大な認可バイパス脆弱性が存在します。

golang.org/x/crypto v0.17.0CRITICAL 級の脆弱性 (GHSA-v778-237x-gjrc / GO-2024-3321) が存在します。SSH サーバーが ServerConfig.PublicKeyCallback を誤用する際に、認可チェックを回避されるリスクがあります。

このプロジェクトはSSH機能を直接使用していませんが、間接依存として含まれています。v0.31.0 以降へのアップグレードを強く推奨します。

修正方法
cd step-09-rooms/server
go get -u golang.org/x/crypto@v0.31.0
go mod tidy

または go.mod を直接編集:

-	golang.org/x/crypto v0.17.0 // indirect
+	golang.org/x/crypto v0.31.0 // indirect
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
golang.org/x/crypto v0.17.0 // indirect
golang.org/x/crypto v0.31.0 // indirect
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@step-09-rooms/server/go.mod` at line 16, Update the indirect dependency
golang.org/x/crypto from v0.17.0 to at least v0.31.0 to address the
GHSA-v778-237x-gjrc (GO-2024-3321) vulnerability: edit the go.mod entry for
golang.org/x/crypto to v0.31.0 (or run `go get -u golang.org/x/crypto@v0.31.0`)
and then run `go mod tidy` to refresh modules and ensure the updated version is
recorded; verify the final go.mod lists golang.org/x/crypto v0.31.0 (or newer).

github.com/mattn/go-isatty v0.0.20 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
golang.org/x/crypto v0.17.0 // indirect
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "[1/2] OSV: golang.org/x/crypto@0.17.0 の脆弱性確認"
curl -s https://api.osv.dev/v1/query \
  -H 'Content-Type: application/json' \
  -d '{"version":"0.17.0","package":{"ecosystem":"Go","name":"golang.org/x/crypto"}}' \
  | jq -r '.vulns[] | "\(.id)\t\(.summary)"'

echo
echo "[2/2] モジュールの最新公開版確認"
curl -s https://proxy.golang.org/golang.org/x/crypto/@latest

Repository: NAGA-29/WebSocket-Learn

Length of output: 1185


脆弱な golang.org/x/crypto v0.17.0 を解消してください。

Line 16 は複数の重大な脆弱性の対象バージョンです(認可回避、DoS、メモリ無制限消費など)。最新版 v0.49.0 への更新が必須です。更新後、go mod tidy を実行し、ロック内容を再生成してください。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@step-10-reconnect/server/go.mod` at line 16, Replace the vulnerable module
version golang.org/x/crypto v0.17.0 with the fixed release golang.org/x/crypto
v0.49.0 in go.mod, then run go mod tidy to refresh the module graph and
regenerate the lockfile (go.sum); ensure any indirect references are updated and
verify builds/tests succeed after updating.

Comment on lines +124 to +147
func broadcastState() {
mu.RLock()
sc := make(map[string]*Snake, len(snakes))
for id, s := range snakes {
cp := *s
bc := make([]Point, len(s.Body))
copy(bc, s.Body)
cp.Body = bc
sc[id] = &cp
}
cc := make(map[string]*websocket.Conn, len(clients))
for id, conn := range clients {
cc[id] = conn
}
mu.RUnlock()

if len(cc) == 0 {
return
}

data, _ := json.Marshal(GameState{Type: "state", Snakes: sc})
for _, conn := range cc {
conn.WriteMessage(websocket.TextMessage, data)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, let's find and examine the main.go file structure
fd -t f main.go | grep step-10-reconnect

# Then examine the file to understand the concurrent write patterns

Repository: NAGA-29/WebSocket-Learn

Length of output: 99


🏁 Script executed:

# Examine the main.go file to see the full implementation
cat -n step-10-reconnect/server/main.go | head -250

Repository: NAGA-29/WebSocket-Learn

Length of output: 7128


🏁 Script executed:

# Verify Gorilla WebSocket documentation on concurrent writes
# Check if there are any mutex patterns in the codebase for connection writes
rg -i "mutex|channel|lock" step-10-reconnect/server/main.go | head -20

Repository: NAGA-29/WebSocket-Learn

Length of output: 222


🏁 Script executed:

# Check if there are any write synchronization patterns around WriteMessage calls
rg "WriteMessage" step-10-reconnect/server/main.go -B 2 -A 1

Repository: NAGA-29/WebSocket-Learn

Length of output: 508


複数の goroutine が同じ WebSocket 接続に並行書き込みしており、フレームが壊れるリスクがあります。

broadcastState()(行 146)、初期 ack メッセージ(行 186)、ping goroutine(行 197)がそれぞれ異なる goroutine から conn.WriteMessage() を呼び出しています。Gorilla WebSocket は同一接続への並行書き込みをサポートしていないため、フレーム破損の原因になります。

解決策:

  • 送信専用の goroutine を 1 つ作成し、すべての書き込みをチャネル経由で集約する
  • ping については WriteControl() と読み取り deadline を活用した方式に切り替える(現在のテキストフレームベースの ping ではなく)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@step-10-reconnect/server/main.go` around lines 124 - 147, broadcastState, the
initial ack send, and the existing ping goroutine all call conn.WriteMessage
concurrently which can corrupt frames; fix by creating a per-connection
dedicated writer goroutine and an outbound channel (e.g. outbound chan []byte)
for each websocket connection, have all places (broadcastState, ack send, other
senders) send bytes to that channel instead of calling conn.WriteMessage
directly, and let the writer goroutine serially call conn.WriteMessage for
TextMessage; change the ping logic to use conn.WriteControl with a control
timeout and rely on read deadlines for detecting dead peers rather than sending
ping frames from multiple goroutines.

step-10-reconnect/server/main.go:
- Add Client struct with per-connection writeMu (sync.Mutex) to
  serialize all WriteMessage calls; gorilla/websocket forbids
  concurrent writes on the same connection
- Switch ping goroutine from WriteMessage to WriteControl, which is
  explicitly safe to call concurrently with WriteMessage per gorilla docs
- Handle json.Marshal and WriteMessage errors; remove failed
  connections from the clients map

step-11-optimization/server/main.go:
- Check json.Marshal errors instead of ignoring them with _; return
  early on failure to avoid sending nil/empty data
- Guard the ratio log line against division by zero when fullSize == 0
- Handle WriteMessage errors in broadcastWithStats; close and remove
  failed connections so disconnected clients do not accumulate

https://claude.ai/code/session_01Cg4waeVYhPHpatXEsQMbfB
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (4)
step-10-reconnect/server/main.go (3)

244-250: 初回 ack 送信失敗時の挙動を確認してください。

writeText(ack) が失敗した場合、クライアントは myId を受け取れず、サーバー側では接続が登録されたままになります。読み取りループがタイムアウトでクリーンアップするため機能的には問題ありませんが、即座に return することで不要な処理を避けられます。

🔧 提案:エラー時に早期リターン
 	// 初回 ack を送信(writeMu で直列化)
 	ack, err := json.Marshal(GameState{Type: "state", MyID: id, Snakes: map[string]*Snake{}})
 	if err != nil {
 		log.Printf("ack: json.Marshal エラー: %v", err)
+		return err
 	} else if err := client.writeText(ack); err != nil {
 		log.Printf("ack 送信エラー: %v", err)
+		return err
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@step-10-reconnect/server/main.go` around lines 244 - 250, The initial ack
marshal/send block currently logs errors but continues; change it so that after
preparing ack (GameState with Type:"state", MyID:id) if json.Marshal fails log
and return, and if client.writeText(ack) fails log and immediately return to
avoid leaving the client registered; modify the ack send section (referencing
GameState, id, client.writeText, and any writeMu-protected code) to perform
early returns on both marshal and write failures.

256-273: ping goroutine のエラー時に接続クローズを検討してください。

WriteControl が失敗した場合(行 264-266)、goroutine は終了しますが、接続自体はクローズされません。読み取りループのタイムアウトで最終的にクリーンアップされますが、明示的に conn.Close() を呼ぶことで、より早く切断を検出できます。

🔧 提案:エラー時に接続をクローズ
 			case <-ticker.C:
 				// WriteControl は deadline を引数に取る
 				deadline := time.Now().Add(5 * time.Second)
 				if err := conn.WriteControl(websocket.PingMessage, nil, deadline); err != nil {
 					log.Printf("ping 送信エラー: %v", err)
+					conn.Close() // ReadMessage をエラーにして読み取りループを終了させる
 					return
 				}
 				log.Printf("ping 送信 → %s", id)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@step-10-reconnect/server/main.go` around lines 256 - 273, The ping goroutine
currently returns on WriteControl error but doesn't close the websocket, so
update the goroutine handling (the anonymous goroutine that uses
conn.WriteControl and pingStop) to explicitly close the connection when
WriteControl fails: after logging the error from conn.WriteControl (the existing
log.Printf line), call conn.Close() (and optionally signal pingStop or any
cleanup channel if used) to ensure the connection is terminated promptly and
readers wake up; keep the existing return after closing to stop the goroutine.

16-18: CheckOrigin がすべてのオリジンを許可しています(学習用途では許容)。

本番環境では、CORS オリジンを適切に検証する必要があります。学習カリキュラムとしては問題ありませんが、README にこの点を注記しておくと良いでしょう。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@step-10-reconnect/server/main.go` around lines 16 - 18, The
websocket.Upgrader currently sets CheckOrigin to allow all origins via the
upgrader variable; replace this permissive CheckOrigin with a validation that
checks the request Origin against an allowlist (e.g., compare
r.Header.Get("Origin") to an allowedOrigins slice or a isAllowedOrigin(origin
string) helper) in the upgrader.CheckOrigin function for production, and also
add a clear note in the README that the current CheckOrigin is permissive for
learning purposes and must be tightened before deployment.
step-11-optimization/server/main.go (1)

16-18: WebSocket の Origin 検証が無効化されており、Cross-Site WebSocket Hijacking (CSWSH) の対象になります。

CheckOrigin を常に true にすると、任意の Origin からの WebSocket 接続を許可してしまいます。gorilla/websocket のデフォルト動作(CheckOrigin が nil)では、Origin ヘッダーをリクエストの Host ヘッダーと照合する安全な検証が行われます。この設定を上書きすると、その検証が完全に無効化され、クロスオリジンからの接続が可能になります。

これにより悪意のあるサイトから無断で WebSocket 接続を開かれ、データの盗聴や不正なメッセージ送信のリスクが生じます。教材コードはそのまま流用されやすいため、本番環境で使用される際の脆弱性につながる可能性があります。

🔧 修正案
 var upgrader = websocket.Upgrader{
-	CheckOrigin: func(r *http.Request) bool { return true },
+	CheckOrigin: func(r *http.Request) bool {
+		origin := r.Header.Get("Origin")
+		return origin == "http://"+r.Host || origin == "https://"+r.Host
+	},
 }

または、デモ専用であることを明確に文書化し、本番環境での使用を禁止する注釈を追加してください。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@step-11-optimization/server/main.go` around lines 16 - 18, The Upgrader
variable currently disables origin checks by setting CheckOrigin to always
return true (var upgrader = websocket.Upgrader{CheckOrigin: func(r
*http.Request) bool { return true }}), which enables CSWSH; fix by removing or
replacing that CheckOrigin: either set CheckOrigin to nil (use the
gorilla/websocket default behavior) or implement a strict validator that
compares r.Header.Get("Origin") against r.Host or a configured allowed-origins
list (and reference the upgrader variable and its CheckOrigin func when
updating). If this is demo-only, instead add a clear comment near the upgrader
declaration documenting that the permissive CheckOrigin is for demo only and
must not be used in production.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@step-11-optimization/server/main.go`:
- Around line 303-307: The code currently only checks opposites when assigning
s.Direction (in the block handling msg.Type == "input" where opposites is
defined and snakes[id] is accessed), allowing unknown values like "northwest"
through and later causing moveSnake to fall back to a zero-vector; fix this by
whitelisting allowed directions (e.g., "up","down","left","right") before
assigning: after confirming msg.Type == "input" and obtaining s from snakes,
validate that msg.Direction is one of the allowed strings and also not the
opposite via opposites[s.Direction] != msg.Direction, then set s.Direction; keep
the mutex (mu.Lock) semantics and do not change moveSnake.
- Around line 227-236: You must serialize writes to each websocket connection by
introducing a per-connection write-lock wrapper and using it everywhere a
WriteMessage may be called: create a small wrapper struct (e.g., type lockedConn
struct { conn *websocket.Conn; mu sync.Mutex }) with a method WriteMessage that
locks, delegates to conn.WriteMessage, then unlocks; replace usages of raw
*websocket.Conn in the global clients map with *lockedConn (or have clients
store the wrapper alongside the conn) and update the registration code (the spot
that registers into clients after mu.Unlock and the ack WriteMessage call),
broadcastWithStats (the loop that calls conn.WriteMessage for fullData and
statsData), and gameLoop ack sends to call lockedConn.WriteMessage instead of
websocket.Conn.WriteMessage so no two goroutines write the same Conn
concurrently.

---

Nitpick comments:
In `@step-10-reconnect/server/main.go`:
- Around line 244-250: The initial ack marshal/send block currently logs errors
but continues; change it so that after preparing ack (GameState with
Type:"state", MyID:id) if json.Marshal fails log and return, and if
client.writeText(ack) fails log and immediately return to avoid leaving the
client registered; modify the ack send section (referencing GameState, id,
client.writeText, and any writeMu-protected code) to perform early returns on
both marshal and write failures.
- Around line 256-273: The ping goroutine currently returns on WriteControl
error but doesn't close the websocket, so update the goroutine handling (the
anonymous goroutine that uses conn.WriteControl and pingStop) to explicitly
close the connection when WriteControl fails: after logging the error from
conn.WriteControl (the existing log.Printf line), call conn.Close() (and
optionally signal pingStop or any cleanup channel if used) to ensure the
connection is terminated promptly and readers wake up; keep the existing return
after closing to stop the goroutine.
- Around line 16-18: The websocket.Upgrader currently sets CheckOrigin to allow
all origins via the upgrader variable; replace this permissive CheckOrigin with
a validation that checks the request Origin against an allowlist (e.g., compare
r.Header.Get("Origin") to an allowedOrigins slice or a isAllowedOrigin(origin
string) helper) in the upgrader.CheckOrigin function for production, and also
add a clear note in the README that the current CheckOrigin is permissive for
learning purposes and must be tightened before deployment.

In `@step-11-optimization/server/main.go`:
- Around line 16-18: The Upgrader variable currently disables origin checks by
setting CheckOrigin to always return true (var upgrader =
websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }}),
which enables CSWSH; fix by removing or replacing that CheckOrigin: either set
CheckOrigin to nil (use the gorilla/websocket default behavior) or implement a
strict validator that compares r.Header.Get("Origin") against r.Host or a
configured allowed-origins list (and reference the upgrader variable and its
CheckOrigin func when updating). If this is demo-only, instead add a clear
comment near the upgrader declaration documenting that the permissive
CheckOrigin is for demo only and must not be used in production.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 52d0e69c-1138-4002-8fff-89a4bdad2900

📥 Commits

Reviewing files that changed from the base of the PR and between 6db03fb and e8818e2.

📒 Files selected for processing (2)
  • step-10-reconnect/server/main.go
  • step-11-optimization/server/main.go

Comment on lines +227 to +236
for id, conn := range cc {
if err := conn.WriteMessage(websocket.TextMessage, fullData); err != nil {
log.Printf("broadcastWithStats: 送信エラー(切断扱い): %s: %v", id, err)
failed = append(failed, id)
continue
}
if err := conn.WriteMessage(websocket.TextMessage, statsData); err != nil {
log.Printf("broadcastWithStats: stats 送信エラー: %s: %v", id, err)
failed = append(failed, id)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

According to gorilla/websocket v1.5.0 documentation, are concurrent calls to WriteMessage on the same *websocket.Conn safe, and is WriteControl treated differently?

💡 Result:

No, concurrent calls to WriteMessage on the same *websocket.Conn are not safe. Applications must ensure only one goroutine calls write methods like WriteMessage concurrently. The library detects violations and panics with "concurrent write to websocket connection". WriteControl is treated differently: it can be called concurrently with other methods, including write methods.

Citations:


🏁 Script executed:

# First, let's check the file structure and examine the critical lines
cd step-11-optimization/server && wc -l main.go

Repository: NAGA-29/WebSocket-Learn

Length of output: 78


🏁 Script executed:

# Read the relevant sections to understand the race condition
sed -n '220,290p' step-11-optimization/server/main.go

Repository: NAGA-29/WebSocket-Learn

Length of output: 1875


🏁 Script executed:

# Also check the gameLoop function to understand when broadcast happens
grep -n "func gameLoop" step-11-optimization/server/main.go

Repository: NAGA-29/WebSocket-Learn

Length of output: 87


🏁 Script executed:

# Search for all WriteMessage calls to identify all write paths
rg "WriteMessage" step-11-optimization/server/main.go -n -B2 -A2

Repository: NAGA-29/WebSocket-Learn

Length of output: 723


同一 Conn への書き込みを直列化してください。

Line 278-279 で接続を clients に登録した直後、mu.Unlock() で global lock を解放しますが、その直後 Line 283 の ack WriteMessage が実行される前に、別 goroutine で動作する gameLoop の tick が走る可能性があります。このとき Line 228 / Line 233 の broadcast が同じ Conn に対して同時 WriteMessage を呼び出してしまい、gorilla/websocket が concurrent write を検出してパニックを起こします。gorilla/websocket は 1 接続あたり同時 writer 1 本だけを前提にしているため、write mutex 付きラッパーへの変更が必須です。

🔧 修正案
+type Client struct {
+	conn *websocket.Conn
+	mu   sync.Mutex
+}
+
 var (
 	snakes    = make(map[string]*FullSnake)
-	clients   = make(map[string]*websocket.Conn)
+	clients   = make(map[string]*Client)
 	mu        sync.RWMutex
 	counter   int
 	tickCount int64
 )
+
+func writeText(c *Client, payload []byte) error {
+	c.mu.Lock()
+	defer c.mu.Unlock()
+	return c.conn.WriteMessage(websocket.TextMessage, payload)
+}
-	cc := make(map[string]*websocket.Conn, len(clients))
-	for id, conn := range clients {
-		cc[id] = conn
+	cc := make(map[string]*Client, len(clients))
+	for id, client := range clients {
+		cc[id] = client
 	}
-	for id, conn := range cc {
-		if err := conn.WriteMessage(websocket.TextMessage, fullData); err != nil {
+	for id, client := range cc {
+		if err := writeText(client, fullData); err != nil {
 			...
 		}
-		if err := conn.WriteMessage(websocket.TextMessage, statsData); err != nil {
+		if err := writeText(client, statsData); err != nil {
 			...
 		}
 	}
-	clients[id] = conn
+	client := &Client{conn: conn}
+	clients[id] = client
 	mu.Unlock()
 
 	ack, err := json.Marshal(FullState{Type: "state", MyID: id, Snakes: map[string]*FullSnake{}})
 	if err != nil {
 		log.Printf("ack: json.Marshal エラー: %v", err)
-	} else if err := conn.WriteMessage(websocket.TextMessage, ack); err != nil {
+	} else if err := writeText(client, ack); err != nil {
 		log.Printf("ack 送信エラー: %v", err)
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for id, conn := range cc {
if err := conn.WriteMessage(websocket.TextMessage, fullData); err != nil {
log.Printf("broadcastWithStats: 送信エラー(切断扱い): %s: %v", id, err)
failed = append(failed, id)
continue
}
if err := conn.WriteMessage(websocket.TextMessage, statsData); err != nil {
log.Printf("broadcastWithStats: stats 送信エラー: %s: %v", id, err)
failed = append(failed, id)
}
for id, client := range cc {
if err := writeText(client, fullData); err != nil {
log.Printf("broadcastWithStats: 送信エラー(切断扱い): %s: %v", id, err)
failed = append(failed, id)
continue
}
if err := writeText(client, statsData); err != nil {
log.Printf("broadcastWithStats: stats 送信エラー: %s: %v", id, err)
failed = append(failed, id)
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@step-11-optimization/server/main.go` around lines 227 - 236, You must
serialize writes to each websocket connection by introducing a per-connection
write-lock wrapper and using it everywhere a WriteMessage may be called: create
a small wrapper struct (e.g., type lockedConn struct { conn *websocket.Conn; mu
sync.Mutex }) with a method WriteMessage that locks, delegates to
conn.WriteMessage, then unlocks; replace usages of raw *websocket.Conn in the
global clients map with *lockedConn (or have clients store the wrapper alongside
the conn) and update the registration code (the spot that registers into clients
after mu.Unlock and the ack WriteMessage call), broadcastWithStats (the loop
that calls conn.WriteMessage for fullData and statsData), and gameLoop ack sends
to call lockedConn.WriteMessage instead of websocket.Conn.WriteMessage so no two
goroutines write the same Conn concurrently.

Comment on lines +303 to +307
if msg.Type == "input" {
opposites := map[string]string{"up": "down", "down": "up", "left": "right", "right": "left"}
mu.Lock()
if s, ok := snakes[id]; ok && opposites[s.Direction] != msg.Direction {
s.Direction = msg.Direction
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

不正な方向値を state に入れないでください。

Line 306 は「逆方向でない」だけを見ているので、"northwest" のような未知の値も通ります。その状態で次 tick の moveSnake に入ると switch がどれにも一致せず、nh のゼロ値 (0,0) にジャンプします。サーバー authoritative を保つなら、ここで許可済み方向だけを whitelist した方がいいです。

🔧 修正案
 	if msg.Type == "input" {
 		opposites := map[string]string{"up": "down", "down": "up", "left": "right", "right": "left"}
+		if _, ok := opposites[msg.Direction]; !ok {
+			continue
+		}
 		mu.Lock()
 		if s, ok := snakes[id]; ok && opposites[s.Direction] != msg.Direction {
 			s.Direction = msg.Direction
 		}
 		mu.Unlock()
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if msg.Type == "input" {
opposites := map[string]string{"up": "down", "down": "up", "left": "right", "right": "left"}
mu.Lock()
if s, ok := snakes[id]; ok && opposites[s.Direction] != msg.Direction {
s.Direction = msg.Direction
if msg.Type == "input" {
opposites := map[string]string{"up": "down", "down": "up", "left": "right", "right": "left"}
if _, ok := opposites[msg.Direction]; !ok {
continue
}
mu.Lock()
if s, ok := snakes[id]; ok && opposites[s.Direction] != msg.Direction {
s.Direction = msg.Direction
}
mu.Unlock()
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@step-11-optimization/server/main.go` around lines 303 - 307, The code
currently only checks opposites when assigning s.Direction (in the block
handling msg.Type == "input" where opposites is defined and snakes[id] is
accessed), allowing unknown values like "northwest" through and later causing
moveSnake to fall back to a zero-vector; fix this by whitelisting allowed
directions (e.g., "up","down","left","right") before assigning: after confirming
msg.Type == "input" and obtaining s from snakes, validate that msg.Direction is
one of the allowed strings and also not the opposite via opposites[s.Direction]
!= msg.Direction, then set s.Direction; keep the mutex (mu.Lock) semantics and
do not change moveSnake.

step-11-optimization/client/index.html:
- The receivedSize variable was computed but never used; now displayed
  in a new "実際の受信サイズ" stat card so learners can compare the
  client-side Blob-measured bytes against the server-reported fullSize
- Guard ratio calculation with msg.fullSize > 0 to prevent Infinity/NaN
  when the server sends fullSize=0

step-07-lag/client/index.html:
- Remove unused startIdx variable; intervals is already trimmed to
  MAX_HISTORY on every push, so the loop correctly starts at i=0

step-09-rooms/server/main.go:
- Collapse redundant "if !ate && len > initial ... else if !ate"
  branches into a single "if !ate" block; both branches were identical

step-01-echo/README.md:
- Fix echo version in the go.mod example from v4.11.0 to v4.11.4
  to match the actual server/go.mod file

https://claude.ai/code/session_01Cg4waeVYhPHpatXEsQMbfB
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
step-01-echo/README.md (3)

70-77: フェンスドコードブロックに言語指定子を追加してください。

接続の流れの説明にも言語指定子を追加することを推奨します。

♻️ 提案される修正
-```
+```text
 接続の流れ:
 1. クライアントが ws://localhost:8080/ws に接続を要求
 2. echo がリクエストを受け取る
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@step-01-echo/README.md` around lines 70 - 77, Update the fenced code block
that starts with "接続の流れ:" to include a language/info string (e.g., change ``` to
```text) so renderers treat it as plain text; also add the same language tag to
any other fenced blocks in that README section to keep formatting consistent and
explicit (look for the block containing the numbered steps and modify its
opening fence to ```text).

51-60: フェンスドコードブロックに言語指定子を追加してください。

go.modの例に適切な言語指定子を追加することで、シンタックスハイライトが有効になります。

♻️ 提案される修正
-```
+```go
 module step01-echo
 
 go 1.21
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@step-01-echo/README.md` around lines 51 - 60, The fenced code block
containing the go.mod sample (showing "module step01-echo", "go 1.21", and the
require block) needs a language specifier to enable syntax highlighting; update
the opening fence from ``` to ```go so the code block is marked as Go source
(e.g., change the fence that precedes "module step01-echo" to ```go).

36-44: フェンスドコードブロックに言語指定子を追加してください。

マークダウンのレンダリングとアクセシビリティを向上させるため、コードブロックに言語指定子を追加することを推奨します。

♻️ 提案される修正
-```
+```text
 step-01-echo/
 ├── README.md
 ├── server/
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@step-01-echo/README.md` around lines 36 - 44, Update the fenced code block in
README.md that shows the directory tree for step-01-echo to include a language
specifier (e.g., change ``` to ```text) so the block is rendered/accessed
correctly; locate the block containing the "step-01-echo/ ├── README.md ├──
server/ ..." tree and add the language token immediately after the opening
backticks to preserve content but improve markdown rendering and accessibility.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@step-01-echo/README.md`:
- Around line 70-77: Update the fenced code block that starts with "接続の流れ:" to
include a language/info string (e.g., change ``` to ```text) so renderers treat
it as plain text; also add the same language tag to any other fenced blocks in
that README section to keep formatting consistent and explicit (look for the
block containing the numbered steps and modify its opening fence to ```text).
- Around line 51-60: The fenced code block containing the go.mod sample (showing
"module step01-echo", "go 1.21", and the require block) needs a language
specifier to enable syntax highlighting; update the opening fence from ``` to
```go so the code block is marked as Go source (e.g., change the fence that
precedes "module step01-echo" to ```go).
- Around line 36-44: Update the fenced code block in README.md that shows the
directory tree for step-01-echo to include a language specifier (e.g., change
``` to ```text) so the block is rendered/accessed correctly; locate the block
containing the "step-01-echo/ ├── README.md ├── server/ ..." tree and add the
language token immediately after the opening backticks to preserve content but
improve markdown rendering and accessibility.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6a662f4e-7623-46cd-b4e1-22f22ae587b7

📥 Commits

Reviewing files that changed from the base of the PR and between e8818e2 and ce47e7f.

📒 Files selected for processing (4)
  • step-01-echo/README.md
  • step-07-lag/client/index.html
  • step-09-rooms/server/main.go
  • step-11-optimization/client/index.html
✅ Files skipped from review due to trivial changes (1)
  • step-07-lag/client/index.html
🚧 Files skipped from review as they are similar to previous changes (2)
  • step-11-optimization/client/index.html
  • step-09-rooms/server/main.go

README.md:
- Split "使わないもの" into "実装では使わないもの" and "概念として
  触れるもの" with a note that Redis is covered conceptually in Step 12
  only; removes the contradiction between the tech stack table and the
  Step 12 content

step-10-reconnect/server/main.go:
- Add a prominent comment on CheckOrigin explaining the CSWSH risk,
  that this setting is development-only, and provide a minimal example
  of origin validation for production

step-03-input/server/main.go:
- Add Client struct with per-connection writeMu (sync.Mutex) to
  serialize WriteMessage calls; broadcast() is called from multiple
  handler goroutines so concurrent writes to the same conn are possible
- Move idCounter increment inside mu.Lock() to fix the data race on
  idCounter (was modified by generatePlayerID outside any lock)
- Handle json.Marshal and writeText errors instead of ignoring them

step-04-server-state/server/main.go:
- Same Client struct + writeMu pattern as step-03; broadcast() is
  called from multiple goroutines (on connect, on input, and on
  disconnect) so per-connection write serialization is required
- Handle json.Marshal and writeText errors

https://claude.ai/code/session_01Cg4waeVYhPHpatXEsQMbfB
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@README.md`:
- Around line 78-84: フェンスドコードブロックに言語指定がなくMD040警告が出ているので、該当のバックティック囲み(例: the
triple backticks enclosing the "Step 0〜2…"/the tree example)
に言語タグを追加してlintを解消してください(例: ```text)。README.md内の両該当ブロック(先に示した箇所と「Also applies to:
93-100」のブロック)に同様の変更を適用してください。

In `@step-03-input/server/main.go`:
- Around line 117-123: After assigning/removing players you need to immediately
notify others: in the connect path where you lock mu, increment idCounter, set
clients[playerID] and playerInputs[playerID], then mu.Unlock() and call
broadcast() so joined players show up; likewise in the disconnect path(s) where
you delete from clients and playerInputs (the blocks around the regions
referenced by lines 139-145 and 172-174), perform the deletion under mu,
mu.Unlock(), and then call broadcast() to propagate the updated state. Ensure
the broadcast() calls occur after unlocking to avoid holding mu during
network/write operations.

In `@step-10-reconnect/server/main.go`:
- Around line 138-147: The switch on s.Direction assigns nh but allows unknown
values which leave nh at its zero value causing the snake to jump to (0,0);
update the handling in the same function (e.g., moveSnake / the switch that
references s.Direction and variable nh) to whitelist only "up", "down", "left",
"right" and add a default branch that returns early (or otherwise
ignores/validates the input) so invalid directions are rejected; apply the same
whitelist+default-return pattern to the other similar switch block referenced
around the 316-323 area to prevent using an uninitialized nh.
- Around line 247-258: Currently the server sends an initial GameState with
Type:"state" and empty Snakes which can overwrite a just-broadcast real state;
change the initial ack to a distinct ack message so it doesn't clobber state:
create/send a GameState (or separate struct) with Type:"ack" (or use a dedicated
Ack struct) containing MyID only, and use client.writeText to send that after
clients[id] = client; ensure functions/structs referenced are Client, writeText,
GameState (or new Ack type) and leave broadcastState() behavior unchanged.
- Around line 82-85: The writeText method lacks a write deadline causing
blocking; modify Client.writeText to set a write deadline before calling
c.conn.WriteMessage (mirror the pattern used by WriteControl) by calling
c.conn.SetWriteDeadline(time.Now().Add(writeWait)) then perform the WriteMessage
while keeping the existing c.writeMu lock and defer; ensure any error from
SetWriteDeadline/WriteMessage is returned so broadcastState/updateGame aren't
blocked by a single slow client.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f0e9af7e-7bf6-4434-bd66-515e3652529c

📥 Commits

Reviewing files that changed from the base of the PR and between ce47e7f and 2b40e9c.

📒 Files selected for processing (4)
  • README.md
  • step-03-input/server/main.go
  • step-04-server-state/server/main.go
  • step-10-reconnect/server/main.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • step-04-server-state/server/main.go

Comment thread README.md
Comment on lines +78 to +84
```
Step 0〜2 → 最速で終わらせる(接続の感触をつかむ)
Step 3〜5 → 本質理解(ここが最重要)
Step 6 → ゲームらしさを入れる
Step 7〜8 → 「リアルタイムの現実」を学ぶ
Step 9〜12 → 設計者の視点に上げる
```
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

フェンスドコードブロックに言語指定がなく、Markdown lint(MD040)警告になります。

コード例ブロックに言語を付けておくと、lint 警告を解消できます。

🛠 修正例
-```
+```text
 Step 0〜2 → 最速で終わらせる(接続の感触をつかむ)
 Step 3〜5 → 本質理解(ここが最重要)
 Step 6    → ゲームらしさを入れる
 Step 7〜8 → 「リアルタイムの現実」を学ぶ
 Step 9〜12 → 設計者の視点に上げる

@@
- +text
step-XX-name/
├── README.md # 概念説明・コード解説・練習問題
├── server/
│ └── main.go # Goサーバー
└── client/
└── index.html # フロントエンド

Also applies to: 93-100

🧰 Tools
🪛 markdownlint-cli2 (0.22.0)

[warning] 78-78: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 78 - 84,
フェンスドコードブロックに言語指定がなくMD040警告が出ているので、該当のバックティック囲み(例: the triple backticks
enclosing the "Step 0〜2…"/the tree example) に言語タグを追加してlintを解消してください(例:
```text)。README.md内の両該当ブロック(先に示した箇所と「Also applies to:
93-100」のブロック)に同様の変更を適用してください。

Comment on lines +117 to +123
// プレイヤーIDの割り当てと登録を同じロック内で行い、競合を防ぐ
mu.Lock()
idCounter++
playerID := fmt.Sprintf("player-%d", idCounter)
clients[playerID] = client
playerInputs[playerID] = "right" // 初期方向
mu.Unlock()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

接続・切断イベントで状態配信していないため、他クライアントの表示状態が古く残ります。

現在は入力イベント時のみ broadcast() されるため、参加/離脱が即時反映されません。参加直後と離脱直後にも状態配信を入れてください。

🧩 最小修正案
  log.Printf("プレイヤー接続: %s", playerID)

  // 自分のIDをクライアントに通知
  ack, err := json.Marshal(ServerMessage{
      Type:     "ack",
      PlayerID: playerID,
  })
  if err != nil {
      log.Printf("ack: json.Marshal エラー: %v", err)
  } else if err := client.writeText(ack); err != nil {
      log.Printf("ack 送信エラー: %v", err)
  }
+ // 参加イベントを全員へ即時反映
+ broadcast()

  // 切断時の後処理
  defer func() {
      mu.Lock()
      delete(clients, playerID)
      delete(playerInputs, playerID)
      mu.Unlock()
+     // 離脱イベントを全員へ即時反映
+     broadcast()
      log.Printf("プレイヤー切断: %s", playerID)
  }()

Also applies to: 139-145, 172-174

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@step-03-input/server/main.go` around lines 117 - 123, After
assigning/removing players you need to immediately notify others: in the connect
path where you lock mu, increment idCounter, set clients[playerID] and
playerInputs[playerID], then mu.Unlock() and call broadcast() so joined players
show up; likewise in the disconnect path(s) where you delete from clients and
playerInputs (the blocks around the regions referenced by lines 139-145 and
172-174), perform the deletion under mu, mu.Unlock(), and then call broadcast()
to propagate the updated state. Ensure the broadcast() calls occur after
unlocking to avoid holding mu during network/write operations.

Comment on lines +82 to +85
func (c *Client) writeText(data []byte) error {
c.writeMu.Lock()
defer c.writeMu.Unlock()
return c.conn.WriteMessage(websocket.TextMessage, data)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="step-10-reconnect/server/main.go"

echo "== gameLoop -> broadcastState -> writeText =="
sed -n '80,86p;114,121p;166,198p' "$FILE"

echo
echo "== ping write path =="
sed -n '262,276p' "$FILE"

echo
echo "== write deadline の参照 =="
rg -n 'SetWriteDeadline|WriteMessage|WriteControl' "$FILE"

Repository: NAGA-29/WebSocket-Learn

Length of output: 2330


テキスト送信に write deadline がなく、遅い 1 接続で全員の tick が詰まります。

broadcastState() は game loop 上で各クライアントに同期送信しているため、WriteMessage() がブロックされると次の updateGame() 呼び出しまで進行が止まります。WriteControl() 側は deadline 付きですが、writeText() 側だけ無期限なので、ここにも書き込み期限を設定してください。

推奨修正
 func (c *Client) writeText(data []byte) error {
 	c.writeMu.Lock()
 	defer c.writeMu.Unlock()
+	if err := c.conn.SetWriteDeadline(time.Now().Add(5 * time.Second)); err != nil {
+		return err
+	}
 	return c.conn.WriteMessage(websocket.TextMessage, data)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@step-10-reconnect/server/main.go` around lines 82 - 85, The writeText method
lacks a write deadline causing blocking; modify Client.writeText to set a write
deadline before calling c.conn.WriteMessage (mirror the pattern used by
WriteControl) by calling c.conn.SetWriteDeadline(time.Now().Add(writeWait)) then
perform the WriteMessage while keeping the existing c.writeMu lock and defer;
ensure any error from SetWriteDeadline/WriteMessage is returned so
broadcastState/updateGame aren't blocked by a single slow client.

Comment on lines +138 to +147
switch s.Direction {
case "up":
nh = Point{head.X, head.Y - gridSize}
case "down":
nh = Point{head.X, head.Y + gridSize}
case "left":
nh = Point{head.X - gridSize, head.Y}
case "right":
nh = Point{head.X + gridSize, head.Y}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

未知の direction をそのまま保存すると、次 tick で蛇が (0,0) に飛びます。

ここは逆走だけを弾いているので、up/down/left/right 以外の値も通ります。その値が moveSnake()switch に載らないと nh が zero value のまま使われるので、入力を whitelist したうえで default: return も入れておく方が安全です。

🛡️ 受信値のバリデーションを追加する案
 	switch s.Direction {
 	case "up":
 		nh = Point{head.X, head.Y - gridSize}
 	case "down":
 		nh = Point{head.X, head.Y + gridSize}
 	case "left":
 		nh = Point{head.X - gridSize, head.Y}
 	case "right":
 		nh = Point{head.X + gridSize, head.Y}
+	default:
+		return
 	}
@@
 		if msg.Type == "input" {
 			opposites := map[string]string{
 				"up": "down", "down": "up", "left": "right", "right": "left",
 			}
 			mu.Lock()
-			if s, ok := snakes[id]; ok && opposites[s.Direction] != msg.Direction {
-				s.Direction = msg.Direction
+			if s, ok := snakes[id]; ok {
+				if _, valid := opposites[msg.Direction]; valid && opposites[s.Direction] != msg.Direction {
+					s.Direction = msg.Direction
+				}
 			}
 			mu.Unlock()
 		}

Also applies to: 316-323

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@step-10-reconnect/server/main.go` around lines 138 - 147, The switch on
s.Direction assigns nh but allows unknown values which leave nh at its zero
value causing the snake to jump to (0,0); update the handling in the same
function (e.g., moveSnake / the switch that references s.Direction and variable
nh) to whitelist only "up", "down", "left", "right" and add a default branch
that returns early (or otherwise ignores/validates the input) so invalid
directions are rejected; apply the same whitelist+default-return pattern to the
other similar switch block referenced around the 316-323 area to prevent using
an uninitialized nh.

Comment on lines +247 to +258
client := &Client{conn: conn}
snakes[id] = &Snake{ID: id, Body: body, Direction: "right", Color: color}
clients[id] = client
mu.Unlock()

log.Printf("プレイヤー接続: %s", id)

// 初回 ack を送信(writeMu で直列化)
ack, err := json.Marshal(GameState{Type: "state", MyID: id, Snakes: map[string]*Snake{}})
if err != nil {
log.Printf("ack: json.Marshal エラー: %v", err)
} else if err := client.writeText(ack); err != nil {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

初回 ack を state として空の snakes 付きで送ると、接続直後の最新 state を空で上書きできます。

clients[id] = client のあとに type: "state" を送っているので、先に broadcastState() が届いたケースでは、後着の ack が step-10-reconnect/client/index.html:114-124state = msg を空状態で上書きします。myIdtype に依存せず読まれているので、ここは専用の ack メッセージに分けるのが安全です。

💡 サーバー側だけで収まる修正案
-	ack, err := json.Marshal(GameState{Type: "state", MyID: id, Snakes: map[string]*Snake{}})
+	ack, err := json.Marshal(struct {
+		Type string `json:"type"`
+		MyID string `json:"myId"`
+	}{
+		Type: "ack",
+		MyID: id,
+	})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@step-10-reconnect/server/main.go` around lines 247 - 258, Currently the
server sends an initial GameState with Type:"state" and empty Snakes which can
overwrite a just-broadcast real state; change the initial ack to a distinct ack
message so it doesn't clobber state: create/send a GameState (or separate
struct) with Type:"ack" (or use a dedicated Ack struct) containing MyID only,
and use client.writeText to send that after clients[id] = client; ensure
functions/structs referenced are Client, writeText, GameState (or new Ack type)
and leave broadcastState() behavior unchanged.

Introduced Client struct with per-connection writeMu sync.Mutex and
writeText() helper in step-05, step-06, step-07, and step-08 servers.
Prevents simultaneous WriteMessage calls from the gameLoop goroutine
(broadcastState) and the handleWebSocket goroutine (initial ack send),
which violates gorilla/websocket's single-writer constraint and causes
a data race on connection startup.

Also fixed json.Marshal errors being silently ignored (_, _) and added
the ⚠️ development-only comment on upgrader.CheckOrigin where missing.

https://claude.ai/code/session_01Cg4waeVYhPHpatXEsQMbfB
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
step-05-game-loop/server/main.go (1)

136-148: ラップアラウンドの境界値が非対称です。

X < 0 の場合に fieldWidth (800) に設定していますが、これは X > fieldWidth で 0 に戻る条件と組み合わせると、X=800 の位置が一瞬だけ有効になります。fieldWidth - moveSpeed に設定するか、条件を >= に統一することで対称性を保てます。

教育目的のコードなので動作上の問題は軽微ですが、学習者が境界処理のベストプラクティスを学ぶ機会として修正を検討してください。

🔧 対称的なラップアラウンドの例
 	// ラップアラウンド(画面の端を超えたら反対側へ)
 	if p.X < 0 {
-		p.X = fieldWidth
+		p.X = fieldWidth - moveSpeed
 	}
-	if p.X > fieldWidth {
+	if p.X >= fieldWidth {
 		p.X = 0
 	}
 	if p.Y < 0 {
-		p.Y = fieldHeight
+		p.Y = fieldHeight - moveSpeed
 	}
-	if p.Y > fieldHeight {
+	if p.Y >= fieldHeight {
 		p.Y = 0
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@step-05-game-loop/server/main.go` around lines 136 - 148, The wrap-around
boundary checks for p.X and p.Y are asymmetric; update the conditions and resets
in the block that manipulates p.X/p.Y so the comparisons use >= and <=
consistently (or use X <= 0 / X >= fieldWidth and similarly for Y) and set the
reset positions symmetrically (e.g., use fieldWidth - moveSpeed or fieldWidth-1
when wrapping left, and 0 when wrapping right) so p.X, p.Y, fieldWidth,
fieldHeight and moveSpeed are handled consistently; locate and update the wrap
logic around the p.X/p.Y assignments in main.go to make the comparisons and
reset values symmetric.
step-08-interpolation/server/main.go (1)

245-252: opposites マップの再作成は軽微な非効率です。

入力メッセージごとにマップを再作成しています。Step 6 のように isOppositeDirection ヘルパー関数を使用するか、マップをパッケージレベルの定数として定義することで、一貫性と効率が向上します。

教育目的では問題ありませんが、ステップ間でのコードの一貫性を保つために検討してください。

♻️ ヘルパー関数への統一例
+// isOppositeDirection は反対方向への転換かどうかをチェックする
+func isOppositeDirection(current, next string) bool {
+	opposites := map[string]string{
+		"up": "down", "down": "up",
+		"left": "right", "right": "left",
+	}
+	return opposites[current] == next
+}

 // handleWebSocket 内
 		if msg.Type == "input" {
-			opposites := map[string]string{"up": "down", "down": "up", "left": "right", "right": "left"}
 			mu.Lock()
-			if s, ok := snakes[id]; ok && opposites[s.Direction] != msg.Direction {
+			if s, ok := snakes[id]; ok && !isOppositeDirection(s.Direction, msg.Direction) {
 				s.Direction = msg.Direction
 			}
 			mu.Unlock()
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@step-08-interpolation/server/main.go` around lines 245 - 252, The code
recreates the opposites map on every input message; instead, define a
package-level constant map (e.g., opposites := map[string]string{...}) or
add/use an isOppositeDirection(direction, newDir string) helper and replace the
inline map; update the input handling block (where msg.Type == "input",
accessing snakes, mu, s.Direction) to call the helper or reference the
package-level opposites map so the map is not reconstructed per message and the
opposite-check remains opposites[s.Direction] != msg.Direction.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@step-05-game-loop/server/main.go`:
- Around line 136-148: The wrap-around boundary checks for p.X and p.Y are
asymmetric; update the conditions and resets in the block that manipulates
p.X/p.Y so the comparisons use >= and <= consistently (or use X <= 0 / X >=
fieldWidth and similarly for Y) and set the reset positions symmetrically (e.g.,
use fieldWidth - moveSpeed or fieldWidth-1 when wrapping left, and 0 when
wrapping right) so p.X, p.Y, fieldWidth, fieldHeight and moveSpeed are handled
consistently; locate and update the wrap logic around the p.X/p.Y assignments in
main.go to make the comparisons and reset values symmetric.

In `@step-08-interpolation/server/main.go`:
- Around line 245-252: The code recreates the opposites map on every input
message; instead, define a package-level constant map (e.g., opposites :=
map[string]string{...}) or add/use an isOppositeDirection(direction, newDir
string) helper and replace the inline map; update the input handling block
(where msg.Type == "input", accessing snakes, mu, s.Direction) to call the
helper or reference the package-level opposites map so the map is not
reconstructed per message and the opposite-check remains opposites[s.Direction]
!= msg.Direction.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 28cdeb0a-a7e7-4981-b39c-f4da92d5e598

📥 Commits

Reviewing files that changed from the base of the PR and between 2b40e9c and 0094c07.

📒 Files selected for processing (4)
  • step-05-game-loop/server/main.go
  • step-06-snake/server/main.go
  • step-07-lag/server/main.go
  • step-08-interpolation/server/main.go
✅ Files skipped from review due to trivial changes (1)
  • step-07-lag/server/main.go

@NAGA-29 NAGA-29 merged commit 4c7347e into main Apr 5, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants