
目次
前回のおさらい
Controller実装
セッションの有効化
View実装
実行
まとめ
前回のおさらい
前回はModel部分の作成をしました。
具体的には以下のクラスとなります。
1,Tetrimino.cs テトリミノ(落ちてくるブロック)
2,GameBoard.cs ゲーム盤の管理
3,TetrisGame.cs ゲーム全体の制御
今回はControllerとViewを作成していきます。
Controller実装
まずは GameControllerを実装していきます。
Controllersフォルダに新しいクラスを作成し、以下のコードを書きます。
<code>using Microsoft.AspNetCore.Mvc; using TetrisGame.Models; using System.Text.Json; using System; using System.Threading.Tasks; namespace TetrisGame.Controllers { /// <summary> /// テトリスゲームのWebAPI制御を行うコントローラー /// </summary> public class GameController : Controller { // セッションキー定数 private const string GAME_SESSION_KEY = "TetrisGameState"; private const string GAME_ID_KEY = "TetrisGameId"; /// <summary> /// ゲームのメイン画面を表示 /// </summary> /// <returns>ゲーム画面のView</returns> public IActionResult Index() { try { // 新しいゲームセッションを開始 var gameId = Guid.NewGuid().ToString(); HttpContext.Session.SetString(GAME_ID_KEY, gameId); // セッション情報をViewに渡す ViewBag.GameId = gameId; ViewBag.SessionTimeout = 30; // デフォルト値30分 Console.WriteLine($"[DEBUG] Index: 新しいセッション開始 - GameId: {gameId}"); return View(); } catch (Exception ex) { Console.WriteLine($"[ERROR] Index例外: {ex.Message}"); // エラーページまたはデフォルト状態でViewを返す ViewBag.GameId = Guid.NewGuid().ToString(); ViewBag.SessionTimeout = 30; return View(); } } /// <summary> /// 新しいゲームを開始 /// </summary> /// <returns>ゲーム初期状態のJSON</returns> [HttpPost] public IActionResult StartNewGame() { Console.WriteLine("[DEBUG] StartNewGame開始"); try { var game = new Models.TetrisGame(); Console.WriteLine($"[DEBUG] ゲーム作成成功: Board={game.Board.Board?.Length}x{game.Board.Board?[0]?.Length}"); Console.WriteLine($"[DEBUG] CurrentPiece: {game.CurrentPiece?.Type} at ({game.CurrentPiece?.X},{game.CurrentPiece?.Y})"); SaveGameToSession(game); Console.WriteLine("[DEBUG] セッション保存完了"); var gameState = GetGameStateForClient(game); Console.WriteLine("[DEBUG] クライアント状態作成完了"); return Json(new { success = true, gameState = gameState, message = "新しいゲームを開始しました" }); } catch (Exception ex) { Console.WriteLine($"[ERROR] StartNewGame例外: {ex.Message}"); Console.WriteLine($"[ERROR] スタックトレース: {ex.StackTrace}"); return Json(new { success = false, error = ex.Message }); } } /// <summary> /// 現在のゲーム状態を取得 /// </summary> /// <returns>ゲーム状態のJSON</returns> [HttpGet] public IActionResult GetGameState() { try { var game = LoadGameFromSession(); if (game == null) { return Json(new { success = false, error = "ゲームが見つかりません。新しいゲームを開始してください。", needsNewGame = true }); } var gameState = GetGameStateForClient(game); return Json(new { success = true, gameState = gameState }); } catch (Exception ex) { Console.WriteLine($"[ERROR] GetGameState例外: {ex.Message}"); return Json(new { success = false, error = ex.Message, needsNewGame = true }); } } /// <summary> /// テトロミノを左に移動 /// </summary> /// <returns>移動後のゲーム状態</returns> [HttpPost] public IActionResult MoveLeft() { return HandleGameAction(game => game.MoveLeft(), "左移動"); } /// <summary> /// テトロミノを右に移動 /// </summary> /// <returns>移動後のゲーム状態</returns> [HttpPost] public IActionResult MoveRight() { return HandleGameAction(game => game.MoveRight(), "右移動"); } /// <summary> /// テトロミノを下に移動(ソフトドロップ) /// </summary> /// <returns>移動後のゲーム状態</returns> [HttpPost] public IActionResult MoveDown() { return HandleGameAction(game => game.MoveDown(), "下移動"); } /// <summary> /// テトロミノを回転 /// </summary> /// <returns>回転後のゲーム状態</returns> [HttpPost] public IActionResult RotatePiece() { return HandleGameAction(game => game.RotatePiece(), "回転"); } /// <summary> /// ハードドロップ(一気に底まで) /// </summary> /// <returns>ドロップ後のゲーム状態</returns> [HttpPost] public IActionResult HardDrop() { return HandleGameAction(game => { var dropDistance = game.HardDrop(); return dropDistance >= 0; // 0でも成功とする }, "ハードドロップ"); } /// <summary> /// ゲームを一時停止/再開 /// </summary> /// <returns>操作後のゲーム状態</returns> [HttpPost] public IActionResult TogglePause() { try { var game = LoadGameFromSession(); if (game == null) { return Json(new { success = false, error = "ゲームが見つかりません", needsNewGame = true }); } game.TogglePause(); SaveGameToSession(game); var gameState = GetGameStateForClient(game); return Json(new { success = true, gameState = gameState, message = game.IsPaused ? "ゲームを一時停止しました" : "ゲームを再開しました" }); } catch (Exception ex) { Console.WriteLine($"[ERROR] TogglePause例外: {ex.Message}"); return Json(new { success = false, error = ex.Message }); } } /// <summary> /// ゲームをリセット /// </summary> /// <returns>リセット後のゲーム状態</returns> [HttpPost] public IActionResult ResetGame() { try { var game = LoadGameFromSession(); if (game == null) { // ゲームが存在しない場合は新規作成 return StartNewGame(); } game.Reset(); SaveGameToSession(game); var gameState = GetGameStateForClient(game); return Json(new { success = true, gameState = gameState, message = "ゲームをリセットしました" }); } catch (Exception ex) { Console.WriteLine($"[ERROR] ResetGame例外: {ex.Message}"); return Json(new { success = false, error = ex.Message }); } } /// <summary> /// ゲーム状態を更新(自動落下など) /// </summary> /// <returns>更新後のゲーム状態</returns> [HttpPost] public IActionResult UpdateGame() { try { var game = LoadGameFromSession(); if (game == null) { return Json(new { success = false, error = "ゲームが見つかりません", needsNewGame = true }); } // ゲーム状態を更新(自動落下処理) game.Update(); SaveGameToSession(game); var gameState = GetGameStateForClient(game); return Json(new { success = true, gameState = gameState }); } catch (Exception ex) { Console.WriteLine($"[ERROR] UpdateGame例外: {ex.Message}"); return Json(new { success = false, error = ex.Message }); } } #region プライベートヘルパーメソッド /// <summary> /// ゲームアクションの共通処理 /// </summary> /// <param name="action">実行するアクション</param> /// <param name="actionName">アクション名(ログ用)</param> /// <returns>処理結果のJSON</returns> private IActionResult HandleGameAction(Func<Models.TetrisGame, bool> action, string actionName) { try { var game = LoadGameFromSession(); if (game == null) { return Json(new { success = false, error = "ゲームが見つかりません", needsNewGame = true }); } if (game.IsGameOver) { return Json(new { success = false, error = "ゲームオーバーです", gameState = GetGameStateForClient(game) }); } if (game.IsPaused) { return Json(new { success = false, error = "ゲームが一時停止中です", gameState = GetGameStateForClient(game) }); } // アクションを実行 bool actionResult = action(game); // セッションに保存 SaveGameToSession(game); // クライアント用の状態データを取得 var gameState = GetGameStateForClient(game); return Json(new { success = true, actionResult = actionResult, gameState = gameState, message = $"{actionName}を実行しました" }); } catch (Exception ex) { Console.WriteLine($"[ERROR] HandleGameAction ({actionName}) 例外: {ex.Message}"); return Json(new { success = false, error = ex.Message }); } } /// <summary> /// セッションからゲーム状態を読み込み /// </summary> /// <returns>ゲームインスタンス、または null</returns> private Models.TetrisGame? LoadGameFromSession() { try { var gameJson = HttpContext.Session.GetString(GAME_SESSION_KEY); if (string.IsNullOrEmpty(gameJson)) { Console.WriteLine("[DEBUG] セッションにゲームデータが見つかりません"); return null; } // JSONからゲームオブジェクトを復元 var options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, WriteIndented = false }; var game = JsonSerializer.Deserialize<Models.TetrisGame>(gameJson, options); Console.WriteLine($"[DEBUG] セッションからゲーム復元成功: {game?.ToString()}"); return game; } catch (Exception ex) { Console.WriteLine($"[ERROR] セッション読み込みエラー: {ex.Message}"); // デシリアライズに失敗した場合はnullを返す return null; } } /// <summary> /// ゲーム状態をセッションに保存 /// </summary> /// <param name="game">保存するゲームインスタンス</param> private void SaveGameToSession(Models.TetrisGame game) { try { var options = new JsonSerializerOptions { WriteIndented = false, PropertyNamingPolicy = JsonNamingPolicy.CamelCase, DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull }; var gameJson = JsonSerializer.Serialize(game, options); HttpContext.Session.SetString(GAME_SESSION_KEY, gameJson); Console.WriteLine($"[DEBUG] セッション保存成功: データサイズ {gameJson.Length} 文字"); } catch (Exception ex) { Console.WriteLine($"[ERROR] セッション保存エラー: {ex.Message}"); throw; // エラーを再スロー } } /// <summary> /// クライアント用のゲーム状態データを作成 /// </summary> /// <param name="game">ゲームインスタンス</param> /// <returns>クライアント用の状態オブジェクト</returns> private object GetGameStateForClient(Models.TetrisGame game) { try { var gameState = new { // ゲーム盤の状態 board = game.Board.Board, // 現在のピース情報 currentPiece = game.CurrentPiece != null ? new { type = game.CurrentPiece.Type.ToString(), x = game.CurrentPiece.X, y = game.CurrentPiece.Y, rotation = game.CurrentPiece.Rotation, shape = game.CurrentPiece.Shape } : null, // 次のピース情報 nextPiece = game.NextPiece != null ? new { type = game.NextPiece.Type.ToString(), shape = game.NextPiece.Shape } : null, // スコア等の情報 score = game.Score, lines = game.Lines, level = game.Level, // ゲーム状態 isGameOver = game.IsGameOver, isPaused = game.IsPaused, // タイミング情報 dropInterval = game.DropInterval, lastDropTime = game.LastDropTime.ToString("yyyy-MM-dd HH:mm:ss.fff"), // 追加情報 ghostY = game.GetGhostPieceY() // ゴーストピース用 }; return gameState; } catch (Exception ex) { Console.WriteLine($"[ERROR] GetGameStateForClient例外: {ex.Message}"); throw; } } #endregion } }</code>
セッションの有効化
続けてセッション機能の有効化を行います。
セッションを有効化するためにProgram.cs の修正を以下のように修正します。
<code>var builder = WebApplication.CreateBuilder(args); // MVC サービスを追加 builder.Services.AddControllersWithViews(); // セッション機能を追加 builder.Services.AddSession(options => { options.IdleTimeout = TimeSpan.FromMinutes(30); // 30分でタイムアウト options.Cookie.HttpOnly = true; // XSS対策 options.Cookie.IsEssential = true; // GDPR対策 options.Cookie.Name = "TetrisGame.Session"; // セッションCookie名 }); // メモリキャッシュを追加(セッション用) builder.Services.AddMemoryCache(); var app = builder.Build(); // Configure the HTTP request pipeline. if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Home/Error"); // The default HSTS value is 30 days. You may want to change this for production scenarios. app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseRouting(); // セッション機能を有効化(重要:UseRoutingの後、UseEndpointsの前) app.UseSession(); app.UseAuthorization(); app.MapControllerRoute( name: "default", pattern: "{controller=Game}/{action=Index}/{id?}"); // デフォルトをGameControllerに変更 app.Run();</code>
View実装
続けてViewの実装を行っていきます。
まずは「Views」フォルダを右クリックして追加>新しいフォルダーから「Game」フォルダを作成します。
続けて「Game」フォルダを右クリックして追加>ビューを作成し名前をIndexとして作成します。
作成したIndexに以下のコードを書きます。
<code>@{ ViewData["Title"] = "テトリスゲーム"; } <!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>@ViewData["Title"]</title> <style type="text/css"> /* ========== 基本スタイル ========== */ * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: 'メイリオ', 'Hiragino Sans', Arial, sans-serif; background-color: #0a0a0a; color: white; padding: 20px; } .container { max-width: 1000px; margin: 0 auto; } /* ========== ヘッダー ========== */ .game-header { text-align: center; margin-bottom: 30px; } .game-title { font-size: 2.5rem; color: #00ffff; text-shadow: 0 0 10px #00ffff; margin-bottom: 10px; } /* ========== レイアウト ========== */ .game-content { display: flex; flex-direction: row; gap: 30px; margin-bottom: 30px; justify-content: center; } .game-area { display: flex; flex-direction: column; align-items: center; } #tetris-board { border: 3px solid #00ffff; background-color: #111; box-shadow: 0 0 20px rgba(0, 255, 255, 0.3); } /* ========== 情報パネル ========== */ .info-panel { background-color: #1a1a1a; border: 2px solid #333; border-radius: 10px; padding: 20px; width: 300px; height: fit-content; } .info-section { margin-bottom: 25px; padding-bottom: 15px; border-bottom: 1px solid #333; } .info-section:last-child { border-bottom: none; margin-bottom: 0; } .info-title { font-size: 0.9rem; color: #00ffff; margin-bottom: 8px; text-transform: uppercase; letter-spacing: 1px; } .info-value { font-size: 1.5rem; font-weight: bold; color: white; } .next-piece-area { text-align: center; } #next-piece { border: 2px solid #555; background-color: #0f0f0f; margin: 10px auto; display: block; } /* ========== ボタン ========== */ .button-area { text-align: center; margin-bottom: 30px; } .game-button { background: linear-gradient(135deg, #00ffff, #0080ff); border: none; border-radius: 8px; color: white; padding: 12px 24px; margin: 0 5px 5px 5px; font-size: 1rem; font-weight: bold; cursor: pointer; transition: all 0.3s ease; text-transform: uppercase; letter-spacing: 1px; } .game-button:hover { background: linear-gradient(135deg, #0080ff, #0060df); transform: translateY(-2px); box-shadow: 0 5px 15px rgba(0, 255, 255, 0.4); } .game-button:active { transform: translateY(0); } .game-button:disabled { background: #666; cursor: not-allowed; transform: none; box-shadow: none; } /* ========== 操作説明 ========== */ .controls-section { background-color: #1a1a1a; border-radius: 10px; padding: 20px; margin-bottom: 20px; } .controls-title { color: #00ffff; margin-bottom: 15px; font-size: 1.2rem; } .control-list { display: flex; flex-direction: column; gap: 10px; } .control-item { display: flex; justify-content: space-between; align-items: center; padding: 8px 0; border-bottom: 1px solid #333; } .control-item:last-child { border-bottom: none; } .control-key { font-weight: bold; color: #00ffff; font-family: 'Courier New', monospace; } .control-action { color: #ccc; } /* ========== ステータスメッセージ ========== */ .status-message { padding: 15px; border-radius: 8px; margin: 20px auto; max-width: 500px; text-align: center; font-weight: bold; display: none; } .status-success { background-color: rgba(76, 175, 80, 0.2); border: 1px solid #4caf50; color: #4caf50; } .status-error { background-color: rgba(244, 67, 54, 0.2); border: 1px solid #f44336; color: #f44336; } .status-info { background-color: rgba(33, 150, 243, 0.2); border: 1px solid #2196f3; color: #2196f3; } /* ========== レスポンシブデザイン(修正版) ========== */ /* タブレット用 */ @@media screen and (max-width: 1024px) { .container { padding: 0 10px; } .game-content { gap: 20px; } .info-panel { width: 250px; } } /* スマートフォン用 */ @@media screen and (max-width: 768px) { body { padding: 10px; } .game-title { font-size: 2rem; } .game-content { flex-direction: column; align-items: center; gap: 20px; } .info-panel { width: 100%; max-width: 400px; } #tetris-board { width: 280px; height: 560px; } .game-button { display: block; margin: 5px auto; width: 200px; padding: 15px 24px; } .control-list { display: grid; grid-template-columns: 1fr; gap: 8px; } } /* 小さいスマートフォン用 */ @@media screen and (max-width: 480px) { .game-title { font-size: 1.5rem; } #tetris-board { width: 250px; height: 500px; } .info-panel { padding: 15px; } .info-value { font-size: 1.2rem; } .game-button { width: 100%; max-width: 280px; } } </style> </head> <body> <div class="container"> <header class="game-header"> <h1 class="game-title">🎮 TETRIS GAME 🎮</h1> </header> <main class="game-content"> <div class="game-area"> <canvas id="tetris-board" width="300" height="600"> お使いのブラウザはCanvasをサポートしていません。 </canvas> </div> <aside class="info-panel"> <div class="info-section"> <div class="info-title">Score</div> <div class="info-value" id="score-display">0</div> </div> <div class="info-section"> <div class="info-title">Level</div> <div class="info-value" id="level-display">1</div> </div> <div class="info-section"> <div class="info-title">Lines</div> <div class="info-value" id="lines-display">0</div> </div> <div class="info-section next-piece-area"> <div class="info-title">Next Piece</div> <canvas id="next-piece" width="120" height="120"></canvas> </div> <div class="info-section"> <div class="info-title">Status</div> <div class="info-value" id="game-status">準備中</div> </div> </aside> </main> <div class="button-area"> <button id="start-btn" class="game-button">🎮 ゲーム開始</button> <button id="pause-btn" class="game-button" disabled>⏸️ 一時停止</button> <button id="reset-btn" class="game-button">🔄 リセット</button> </div> <section class="controls-section"> <h3 class="controls-title">🎯 操作方法</h3> <div class="control-list"> <div class="control-item"> <span class="control-key">← → (A D)</span> <span class="control-action">左右移動</span> </div> <div class="control-item"> <span class="control-key">↓ (S)</span> <span class="control-action">高速落下</span> </div> <div class="control-item"> <span class="control-key">↑ (W)</span> <span class="control-action">回転</span> </div> <div class="control-item"> <span class="control-key">Space</span> <span class="control-action">ハードドロップ</span> </div> <div class="control-item"> <span class="control-key">P</span> <span class="control-action">一時停止</span> </div> </div> </section> <div id="status-message" class="status-message"></div> </div> <script> console.log('🎮 テトリス読み込み開始'); // グローバル変数 let gameState = null; let isGameRunning = false; let gameLoopTimer = null; let autoDropTimer = null; let lastUpdateTime = 0; let keyPressed = {}; // DOM要素 const elements = { tetrisBoard: document.getElementById('tetris-board'), nextPiece: document.getElementById('next-piece'), scoreDisplay: document.getElementById('score-display'), levelDisplay: document.getElementById('level-display'), linesDisplay: document.getElementById('lines-display'), gameStatus: document.getElementById('game-status'), startBtn: document.getElementById('start-btn'), pauseBtn: document.getElementById('pause-btn'), resetBtn: document.getElementById('reset-btn'), statusMessage: document.getElementById('status-message') }; const boardCtx = elements.tetrisBoard.getContext('2d'); const nextCtx = elements.nextPiece.getContext('2d'); // 初期化 document.addEventListener('DOMContentLoaded', function() { console.log('📋 DOM読み込み完了'); setupEventListeners(); initializeCanvas(); console.log('✅ 初期化完了'); }); function setupEventListeners() { elements.startBtn.addEventListener('click', startNewGame); elements.pauseBtn.addEventListener('click', togglePause); elements.resetBtn.addEventListener('click', resetGame); // キーボードイベント(重複防止機能付き) document.addEventListener('keydown', (e) => { if (keyPressed[e.key]) return; // 重複防止 keyPressed[e.key] = true; handleKeyPress(e); }); document.addEventListener('keyup', (e) => { keyPressed[e.key] = false; }); window.addEventListener('beforeunload', stopAutoDropSystem); } function initializeCanvas() { // メインボード初期化 boardCtx.fillStyle = '#111'; boardCtx.fillRect(0, 0, elements.tetrisBoard.width, elements.tetrisBoard.height); // ネクストピース初期化 nextCtx.fillStyle = '#0f0f0f'; nextCtx.fillRect(0, 0, elements.nextPiece.width, elements.nextPiece.height); drawGrid(); } function drawGrid() { const cellWidth = elements.tetrisBoard.width / 10; const cellHeight = elements.tetrisBoard.height / 20; boardCtx.strokeStyle = '#333'; boardCtx.lineWidth = 1; // 縦線 for (let i = 0; i <= 10; i++) { const x = i * cellWidth; boardCtx.beginPath(); boardCtx.moveTo(x, 0); boardCtx.lineTo(x, elements.tetrisBoard.height); boardCtx.stroke(); } // 横線 for (let i = 0; i <= 20; i++) { const y = i * cellHeight; boardCtx.beginPath(); boardCtx.moveTo(0, y); boardCtx.lineTo(elements.tetrisBoard.width, y); boardCtx.stroke(); } } async function startNewGame() { console.log('🎮 ゲーム開始'); elements.gameStatus.textContent = '開始中...'; try { const response = await fetch('/Game/StartNewGame', { method: 'POST', headers: { 'Content-Type': 'application/json' } }); const data = await response.json(); console.log('📡 レスポンス:', data); if (data.success) { gameState = data.gameState; isGameRunning = true; lastUpdateTime = Date.now(); updateUI(); updateButtonStates(true); startAutoDropSystem(); showStatusMessage('ゲーム開始!', 'success'); console.log('✅ ゲーム開始成功'); } else { showStatusMessage('エラー: ' + data.error, 'error'); } } catch (error) { console.error('❌ 通信エラー:', error); showStatusMessage('通信エラー', 'error'); } } function startAutoDropSystem() { console.log('⏰ 自動落下システム開始'); stopAutoDropSystem(); // 描画ループ(60FPS) gameLoopTimer = setInterval(() => { if (isGameRunning && gameState && !gameState.isGameOver && !gameState.isPaused) { updateUI(); } }, 1000 / 60); // 自動落下ループ(0.5秒間隔) autoDropTimer = setInterval(async () => { if (isGameRunning && gameState && !gameState.isGameOver && !gameState.isPaused) { await performAutoUpdate(); } }, 500); } function stopAutoDropSystem() { if (gameLoopTimer) { clearInterval(gameLoopTimer); gameLoopTimer = null; console.log('⏰ 描画ループ停止'); } if (autoDropTimer) { clearInterval(autoDropTimer); autoDropTimer = null; console.log('⏰ 自動落下ループ停止'); } } async function performAutoUpdate() { try { const response = await fetch('/Game/UpdateGame', { method: 'POST', headers: { 'Content-Type': 'application/json' } }); const data = await response.json(); if (data.success) { const oldY = gameState.currentPiece?.y; gameState = data.gameState; const newY = gameState.currentPiece?.y; if (oldY !== newY) { console.log(`⬇️ 自動落下: Y ${oldY} → ${newY}`); } if (gameState.isGameOver) { handleGameOver(); } lastUpdateTime = Date.now(); } else if (data.needsNewGame) { console.log('🔄 ゲーム状態が見つかりません'); showStatusMessage('ゲームセッションが切れました', 'error'); isGameRunning = false; updateButtonStates(false); stopAutoDropSystem(); } } catch (error) { console.error('❌ 自動更新エラー:', error); } } function handleGameOver() { console.log('💀 ゲームオーバー'); stopAutoDropSystem(); isGameRunning = false; updateUI(); updateButtonStates(false); setTimeout(() => { const result = confirm(`ゲームオーバー!\n最終スコア: ${gameState.score.toLocaleString()}\n消去ライン数: ${gameState.lines}\nレベル: ${gameState.level}\n\nもう一度プレイしますか?`); if (result) { startNewGame(); } }, 1000); } async function handleKeyPress(event) { if (!isGameRunning || !gameState || gameState.isGameOver) return; // 一時停止中でもPキーは有効 if (event.key.toLowerCase() === 'p') { togglePause(); return; } if (gameState.isPaused) return; let endpoint = null; switch (event.key.toLowerCase()) { case 'arrowleft': case 'a': endpoint = '/Game/MoveLeft'; break; case 'arrowright': case 'd': endpoint = '/Game/MoveRight'; break; case 'arrowdown': case 's': endpoint = '/Game/MoveDown'; break; case 'arrowup': case 'w': endpoint = '/Game/RotatePiece'; break; case ' ': case 'spacebar': endpoint = '/Game/HardDrop'; break; default: return; // 無関係なキーは無視 } if (endpoint) { event.preventDefault(); await sendGameAction(endpoint); } } async function sendGameAction(endpoint) { try { const response = await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' } }); const data = await response.json(); if (data.success) { gameState = data.gameState; updateUI(); lastUpdateTime = Date.now(); // ゲームオーバーチェック if (gameState.isGameOver) { handleGameOver(); } } else if (data.needsNewGame) { showStatusMessage('ゲームセッションが切れました', 'error'); isGameRunning = false; updateButtonStates(false); stopAutoDropSystem(); } } catch (error) { console.error('❌ 操作エラー:', error); } } async function togglePause() { if (!gameState) return; try { const response = await fetch('/Game/TogglePause', { method: 'POST', headers: { 'Content-Type': 'application/json' } }); const data = await response.json(); if (data.success) { gameState = data.gameState; updateUI(); updateButtonStates(isGameRunning); showStatusMessage(data.message, 'info'); } } catch (error) { console.error('❌ 一時停止エラー:', error); } } async function resetGame() { console.log('🔄 リセット開始'); stopAutoDropSystem(); try { const response = await fetch('/Game/ResetGame', { method: 'POST', headers: { 'Content-Type': 'application/json' } }); const data = await response.json(); if (data.success) { gameState = data.gameState; isGameRunning = true; lastUpdateTime = Date.now(); updateUI(); updateButtonStates(true); startAutoDropSystem(); showStatusMessage('リセット完了', 'success'); console.log('✅ リセット完了'); } else { showStatusMessage('リセットに失敗しました', 'error'); } } catch (error) { console.error('❌ リセットエラー:', error); showStatusMessage('リセットエラー', 'error'); } } function updateUI() { if (!gameState) return; // スコア情報の更新 elements.scoreDisplay.textContent = gameState.score.toLocaleString(); elements.levelDisplay.textContent = gameState.level; elements.linesDisplay.textContent = gameState.lines; // ゲーム状態の表示 if (gameState.isGameOver) { elements.gameStatus.textContent = 'ゲームオーバー'; elements.gameStatus.style.color = '#ff4444'; } else if (gameState.isPaused) { elements.gameStatus.textContent = '一時停止中'; elements.gameStatus.style.color = '#ffaa44'; } else { elements.gameStatus.textContent = 'プレイ中'; elements.gameStatus.style.color = '#44ff44'; } drawGameBoard(); drawNextPiece(); } function drawGameBoard() { // 背景をクリア boardCtx.fillStyle = '#111'; boardCtx.fillRect(0, 0, elements.tetrisBoard.width, elements.tetrisBoard.height); if (gameState && gameState.board) { const cellWidth = elements.tetrisBoard.width / 10; const cellHeight = elements.tetrisBoard.height / 20; // 固定されたブロックを描画 for (let row = 0; row < 20; row++) { for (let col = 0; col < 10; col++) { const cellValue = gameState.board[row][col]; if (cellValue > 0) { boardCtx.fillStyle = getBlockColor(cellValue); boardCtx.fillRect( col * cellWidth + 1, row * cellHeight + 1, cellWidth - 2, cellHeight - 2 ); } } } // ゴーストピースを描画(現在のピースがある場合) if (gameState.currentPiece && gameState.ghostY !== undefined && gameState.ghostY > gameState.currentPiece.y) { const piece = gameState.currentPiece; boardCtx.fillStyle = 'rgba(255, 255, 255, 0.3)'; // 半透明の白 for (let row = 0; row < 4; row++) { for (let col = 0; col < 4; col++) { if (piece.shape[row] && piece.shape[row][col]) { const boardX = piece.x + col; const boardY = gameState.ghostY + row; if (boardX >= 0 && boardX < 10 && boardY >= 0 && boardY < 20) { boardCtx.fillRect( boardX * cellWidth + 2, boardY * cellHeight + 2, cellWidth - 4, cellHeight - 4 ); } } } } } // 現在のピースを描画 if (gameState.currentPiece) { const piece = gameState.currentPiece; boardCtx.fillStyle = getBlockColor(getTetrominoTypeNumber(piece.type)); for (let row = 0; row < 4; row++) { for (let col = 0; col < 4; col++) { if (piece.shape[row] && piece.shape[row][col]) { const boardX = piece.x + col; const boardY = piece.y + row; if (boardX >= 0 && boardX < 10 && boardY >= 0 && boardY < 20) { boardCtx.fillRect( boardX * cellWidth + 1, boardY * cellHeight + 1, cellWidth - 2, cellHeight - 2 ); } } } } } } drawGrid(); } function drawNextPiece() { // 背景をクリア nextCtx.fillStyle = '#0f0f0f'; nextCtx.fillRect(0, 0, elements.nextPiece.width, elements.nextPiece.height); if (gameState && gameState.nextPiece) { const piece = gameState.nextPiece; const cellSize = 25; // ネクストピース用のセルサイズ const offsetX = (elements.nextPiece.width - cellSize * 4) / 2; const offsetY = (elements.nextPiece.height - cellSize * 4) / 2; nextCtx.fillStyle = getBlockColor(getTetrominoTypeNumber(piece.type)); for (let row = 0; row < 4; row++) { for (let col = 0; col < 4; col++) { if (piece.shape[row] && piece.shape[row][col]) { nextCtx.fillRect( offsetX + col * cellSize + 1, offsetY + row * cellSize + 1, cellSize - 2, cellSize - 2 ); } } } } } function getBlockColor(typeNum) { const colors = { 1: '#00ffff', // I - シアン 2: '#ffff00', // O - 黄色 3: '#800080', // T - 紫 4: '#00ff00', // S - 緑 5: '#ff0000', // Z - 赤 6: '#0000ff', // J - 青 7: '#ffa500' // L - オレンジ }; return colors[typeNum] || '#666'; } function getTetrominoTypeNumber(type) { const typeMap = { 'I': 1, 'O': 2, 'T': 3, 'S': 4, 'Z': 5, 'J': 6, 'L': 7 }; return typeMap[type] || 1; } function updateButtonStates(gameRunning) { elements.startBtn.disabled = gameRunning; elements.pauseBtn.disabled = !gameRunning || (gameState && gameState.isGameOver); if (gameState && gameState.isPaused) { elements.pauseBtn.textContent = '▶️ 再開'; } else { elements.pauseBtn.textContent = '⏸️ 一時停止'; } } function showStatusMessage(message, type = 'info') { elements.statusMessage.textContent = message; elements.statusMessage.className = `status-message status-${type}`; elements.statusMessage.style.display = 'block'; setTimeout(() => { elements.statusMessage.style.display = 'none'; }, 3000); } // ページが閉じられる時の処理 window.addEventListener('beforeunload', () => { stopAutoDropSystem(); }); console.log('✅ スクリプト読み込み完了'); </script> </body> </html></code>
実行
これでテトリスのゲームが完成しました。
実行して実際にゲームが出来るか試してみましょう!!
実行してみるとウェブブラウザが立ち上がりテトリスの画面が出てきます。
実際にプレイしてみると問題なくテトリスで遊ぶことが出来ました。
ちなみに一番上までブロックが行くとゲームオーバーとなり、画像のような表記が出てスコアが表示されます。



まとめ
今回はテトリスを作成してみました。
記載出来ていませんが、途中では実行してもエラーがでてなかなか原因が分からず、Claudeに聞いて修正してもらったり、
修正してもらっても、さらに別のエラーが出たりとなかなか完成せずに苦労しました。
しかし、何とか完成までこぎつけられたので良かったと思います。
皆さんも是非機会があれば作成してみてはいかがでしょうか。