【AI】cursor+gpt-4.1でブラウザで動作するオセロゲームを作ってみた

cursor+gpt-4.1でオセロゲームを作ってみた
準備
下記からcursorをダウンロードしインストール
www.cursor.com
実践!
cursorの右ウインドウがAIとの会話窓のため、ここからいろいろ指示をする。
「ブラウザで動作するオセロゲームを作りたい」
「CPU対戦機能を加えて」
などなど

ものの15分程度で下記が完成!
※いっさいコードには触っていない・・・


コードはこちら
※コードを同じフォルダに保存しindex.htmlをブラウザで表示すれば遊べます!
index.html
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>オセロゲーム</title> <link rel="stylesheet" href="style.css"> </head> <body> <h1>オセロゲーム</h1> <div id="choose-side"> <div style="margin-bottom:10px;"> <span>CPUの強さ:</span> <label><input type="radio" name="cpu-level" value="strong" checked><img src="dragon.svg" alt="竜" style="height:1.5em;vertical-align:middle;">竜</label> <label><input type="radio" name="cpu-level" value="normal"><img src="dog.svg" alt="犬" style="height:1.5em;vertical-align:middle;">犬</label> <label><input type="radio" name="cpu-level" value="weak"><img src="mouse.svg" alt="ねずみ" style="height:1.5em;vertical-align:middle;">ねずみ</label> </div> <button id="choose-black">先手(黒)で始める</button> <button id="choose-white">後手(白)で始める</button> </div> <div id="cpu-image-container" style="display:none; text-align:center; margin-bottom:10px;"></div> <div id="board" style="display:none;"></div> <div style="text-align:center;"> <button id="back-to-title" style="display:none; margin-top:20px;">最初の画面に戻る</button> <button id="reset-game" style="display:none; margin-top:20px; margin-left:10px;">リセット</button> </div> <script src="main.js"></script> </body> </html>
main.js
const BOARD_SIZE = 8; const boardElement = document.getElementById('board'); const chooseSideDiv = document.getElementById('choose-side'); const chooseBlackBtn = document.getElementById('choose-black'); const chooseWhiteBtn = document.getElementById('choose-white'); const backToTitleBtn = document.getElementById('back-to-title'); const resetGameBtn = document.getElementById('reset-game'); const cpuLevelRadios = document.getElementsByName('cpu-level'); const cpuImageContainer = document.getElementById('cpu-image-container'); // メッセージ表示用divを追加 let messageDiv = document.getElementById('message'); if (!messageDiv) { messageDiv = document.createElement('div'); messageDiv.id = 'message'; messageDiv.style.marginTop = '20px'; messageDiv.style.fontWeight = 'bold'; boardElement.parentNode.appendChild(messageDiv); } // 0:空, 1:黒, 2:白 let board; let currentPlayer; // 1:黒(人/CPU), 2:白(人/CPU) let gameOver; let playerColor = 1; // 1:黒, 2:白 let cpuLevel = 'strong'; // 'strong', 'normal', 'weak' function getSelectedCpuLevel() { for (const radio of cpuLevelRadios) { if (radio.checked) return radio.value; } return 'strong'; } function initGame() { board = Array.from({ length: BOARD_SIZE }, () => Array(BOARD_SIZE).fill(0)); board[3][3] = 2; board[3][4] = 1; board[4][3] = 1; board[4][4] = 2; currentPlayer = 1; gameOver = false; } chooseBlackBtn.onclick = () => { playerColor = 1; cpuLevel = getSelectedCpuLevel(); chooseSideDiv.style.display = 'none'; boardElement.style.display = ''; backToTitleBtn.style.display = ''; resetGameBtn.style.display = ''; showCpuImage(); initGame(); renderBoard(); if (currentPlayer === 2 && !gameOver) setTimeout(cpuMove, 700); }; chooseWhiteBtn.onclick = () => { playerColor = 2; cpuLevel = getSelectedCpuLevel(); chooseSideDiv.style.display = 'none'; boardElement.style.display = ''; backToTitleBtn.style.display = ''; resetGameBtn.style.display = ''; showCpuImage(); initGame(); renderBoard(); if (currentPlayer === 1 && !gameOver) setTimeout(cpuMove, 700); }; backToTitleBtn.onclick = () => { chooseSideDiv.style.display = ''; boardElement.style.display = 'none'; backToTitleBtn.style.display = 'none'; resetGameBtn.style.display = 'none'; cpuImageContainer.style.display = 'none'; cpuImageContainer.innerHTML = ''; messageDiv.textContent = ''; }; resetGameBtn.onclick = () => { chooseSideDiv.style.display = 'none'; boardElement.style.display = ''; backToTitleBtn.style.display = ''; resetGameBtn.style.display = ''; showCpuImage(); initGame(); // 先手・後手の設定はそのまま if (playerColor === 2 && currentPlayer === 1) { renderBoard(); setTimeout(cpuMove, 700); } else if (playerColor === 1 && currentPlayer === 2) { renderBoard(); setTimeout(cpuMove, 700); } else { renderBoard(); } }; function renderBoard() { boardElement.innerHTML = ''; for (let y = 0; y < BOARD_SIZE; y++) { const row = document.createElement('div'); for (let x = 0; x < BOARD_SIZE; x++) { const cell = document.createElement('div'); cell.className = 'cell'; cell.dataset.x = x; cell.dataset.y = y; if (board[y][x] === 1) { const disk = document.createElement('div'); disk.className = 'disk black'; cell.appendChild(disk); } else if (board[y][x] === 2) { const disk = document.createElement('div'); disk.className = 'disk white'; cell.appendChild(disk); } cell.addEventListener('click', onCellClick); row.appendChild(cell); } boardElement.appendChild(row); } updateMessage(); } function onCellClick(e) { if (gameOver) return; if (currentPlayer !== playerColor) return; // プレイヤーの手番のみクリック可 const x = parseInt(e.currentTarget.dataset.x); const y = parseInt(e.currentTarget.dataset.y); if (board[y][x] !== 0) return; const flipped = getFlippableDisks(x, y, currentPlayer); if (flipped.length === 0) return; board[y][x] = currentPlayer; for (const [fx, fy] of flipped) { board[fy][fx] = currentPlayer; } nextTurn(); } function nextTurn() { currentPlayer = currentPlayer === 1 ? 2 : 1; if (getAllValidMoves(currentPlayer).length === 0) { // パス判定 if (getAllValidMoves(currentPlayer === 1 ? 2 : 1).length === 0) { // 両者とも置けない→終了 gameOver = true; renderBoard(); return; } else { // 相手も置けない→パス currentPlayer = currentPlayer === 1 ? 2 : 1; showPassMessage(); renderBoard(); if (currentPlayer !== playerColor && !gameOver) setTimeout(cpuMove, 700); return; } } renderBoard(); if (currentPlayer !== playerColor && !gameOver) setTimeout(cpuMove, 700); } function cpuMove() { // CPUの手番 const moves = getAllValidMoves(currentPlayer); if (moves.length === 0) return; let move; if (cpuLevel === 'weak') { // 弱:ランダム move = moves[Math.floor(Math.random() * moves.length)]; } else if (cpuLevel === 'normal') { // 中:ひっくり返せる石が多い手を選ぶ let max = -1; let bestMoves = []; for (const [x, y] of moves) { const flipped = getFlippableDisks(x, y, currentPlayer).length; if (flipped > max) { max = flipped; bestMoves = [[x, y]]; } else if (flipped === max) { bestMoves.push([x, y]); } } move = bestMoves[Math.floor(Math.random() * bestMoves.length)]; } else { // 強:角優先、なければ中と同じ const corners = moves.filter(([x, y]) => (x === 0 && y === 0) || (x === 0 && y === BOARD_SIZE - 1) || (x === BOARD_SIZE - 1 && y === 0) || (x === BOARD_SIZE - 1 && y === BOARD_SIZE - 1) ); if (corners.length > 0) { move = corners[Math.floor(Math.random() * corners.length)]; } else { // 角がなければ中と同じ let max = -1; let bestMoves = []; for (const [x, y] of moves) { const flipped = getFlippableDisks(x, y, currentPlayer).length; if (flipped > max) { max = flipped; bestMoves = [[x, y]]; } else if (flipped === max) { bestMoves.push([x, y]); } } move = bestMoves[Math.floor(Math.random() * bestMoves.length)]; } } const [x, y] = move; const flipped = getFlippableDisks(x, y, currentPlayer); board[y][x] = currentPlayer; for (const [fx, fy] of flipped) { board[fy][fx] = currentPlayer; } nextTurn(); } function showPassMessage() { messageDiv.textContent = (currentPlayer === 1 ? '白' : '黒') + 'は置ける場所がないのでパスします'; setTimeout(updateMessage, 1200); } function updateMessage() { if (gameOver) { const [black, white] = countDisks(); let result = ''; if (black > white) result = '黒の勝ち!'; else if (white > black) result = '白の勝ち!'; else result = '引き分け!'; messageDiv.textContent = `ゲーム終了 黒:${black} 白:${white} → ${result}`; } else { if (currentPlayer === playerColor) { messageDiv.textContent = (playerColor === 1 ? '黒(あなた)' : '白(あなた)') + 'の番です'; } else { messageDiv.textContent = (playerColor === 1 ? '白(CPU)' : '黒(CPU)') + 'の番です'; } } } // 8方向のベクトル const directions = [ [0, -1], [1, -1], [1, 0], [1, 1], [0, 1], [-1, 1], [-1, 0], [-1, -1] ]; function getFlippableDisks(x, y, player) { const opponent = player === 1 ? 2 : 1; let flippable = []; for (const [dx, dy] of directions) { let nx = x + dx; let ny = y + dy; let disks = []; while ( nx >= 0 && nx < BOARD_SIZE && ny >= 0 && ny < BOARD_SIZE && board[ny][nx] === opponent ) { disks.push([nx, ny]); nx += dx; ny += dy; } if ( disks.length > 0 && nx >= 0 && nx < BOARD_SIZE && ny >= 0 && ny < BOARD_SIZE && board[ny][nx] === player ) { flippable = flippable.concat(disks); } } return flippable; } function getAllValidMoves(player) { let moves = []; for (let y = 0; y < BOARD_SIZE; y++) { for (let x = 0; x < BOARD_SIZE; x++) { if (board[y][x] === 0 && getFlippableDisks(x, y, player).length > 0) { moves.push([x, y]); } } } return moves; } function countDisks() { let black = 0, white = 0; for (let y = 0; y < BOARD_SIZE; y++) { for (let x = 0; x < BOARD_SIZE; x++) { if (board[y][x] === 1) black++; if (board[y][x] === 2) white++; } } return [black, white]; } function showCpuImage() { let img = ''; if (cpuLevel === 'strong') { img = '<img src="dragon.svg" alt="竜" style="height:48px;vertical-align:middle;"> <span style="font-size:1.2em;">竜</span>'; } else if (cpuLevel === 'normal') { img = '<img src="dog.svg" alt="犬" style="height:48px;vertical-align:middle;"> <span style="font-size:1.2em;">犬</span>'; } else { img = '<img src="mouse.svg" alt="ねずみ" style="height:48px;vertical-align:middle;"> <span style="font-size:1.2em;">ねずみ</span>'; } cpuImageContainer.innerHTML = img; cpuImageContainer.style.display = ''; } // 初期状態は選択画面のみ表示 boardElement.style.display = 'none'; chooseSideDiv.style.display = ''; backToTitleBtn.style.display = 'none'; resetGameBtn.style.display = 'none'; renderBoard();
style.css
body { font-family: sans-serif; text-align: center; background: #f0f0f0; } #board { display: inline-block; margin-top: 20px; } .cell { width: 40px; height: 40px; background: #2e8b57; border: 2px solid #333; display: inline-block; vertical-align: top; position: relative; } .disk { width: 32px; height: 32px; border-radius: 50%; position: absolute; top: 4px; left: 4px; } .disk.black { background: #111; } .disk.white { background: #fff; border: 1px solid #aaa; }
<svg width="128" height="128" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg"> <circle cx="64" cy="64" r="60" fill="#aee9f7" stroke="#333" stroke-width="4"/> <ellipse cx="64" cy="80" rx="32" ry="24" fill="#fff" stroke="#333" stroke-width="3"/> <ellipse cx="64" cy="60" rx="28" ry="24" fill="#7fdc8a" stroke="#333" stroke-width="3"/> <ellipse cx="54" cy="60" rx="4" ry="6" fill="#333"/> <ellipse cx="74" cy="60" rx="4" ry="6" fill="#333"/> <path d="M64 84 Q64 92 72 92" stroke="#333" stroke-width="3" fill="none"/> <path d="M40 40 Q32 24 48 32" stroke="#333" stroke-width="3" fill="none"/> <path d="M88 40 Q96 24 80 32" stroke="#333" stroke-width="3" fill="none"/> </svg>
dog.svg
<svg width="128" height="128" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg"> <ellipse cx="64" cy="80" rx="36" ry="32" fill="#fff" stroke="#333" stroke-width="4"/> <ellipse cx="44" cy="60" rx="10" ry="16" fill="#bfa77f" stroke="#333" stroke-width="3"/> <ellipse cx="84" cy="60" rx="10" ry="16" fill="#bfa77f" stroke="#333" stroke-width="3"/> <ellipse cx="64" cy="80" rx="24" ry="20" fill="#ffe4b5" stroke="#333" stroke-width="3"/> <ellipse cx="58" cy="80" rx="3" ry="5" fill="#333"/> <ellipse cx="70" cy="80" rx="3" ry="5" fill="#333"/> <ellipse cx="64" cy="92" rx="6" ry="3" fill="#333"/> </svg>
mouse.svg
<svg width="128" height="128" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg"> <ellipse cx="64" cy="80" rx="36" ry="28" fill="#eee" stroke="#333" stroke-width="4"/> <ellipse cx="44" cy="60" rx="8" ry="14" fill="#ccc" stroke="#333" stroke-width="3"/> <ellipse cx="84" cy="60" rx="8" ry="14" fill="#ccc" stroke="#333" stroke-width="3"/> <ellipse cx="64" cy="80" rx="20" ry="16" fill="#fff" stroke="#333" stroke-width="3"/> <ellipse cx="58" cy="80" rx="2" ry="4" fill="#333"/> <ellipse cx="70" cy="80" rx="2" ry="4" fill="#333"/> <ellipse cx="64" cy="92" rx="4" ry="2" fill="#333"/> <path d="M100 110 Q120 120 110 100" stroke="#333" stroke-width="3" fill="none"/> </svg>
README.md
# オセロWebアプリ メンテナンス用マニュアル・仕様書 ## 概要 - ブラウザで動作するシンプルなオセロ(リバーシ)ゲームです。 - プレイヤー vs CPU(強さ3段階)で対戦できます。 - 先手・後手、CPU強さを選択可能。 - 盤面下に「リセット」「最初の画面に戻る」ボタンあり。 - CPU強さは「竜(強)」「犬(中)」「ねずみ(弱)」のイラスト付き。 ## ファイル構成 - `index.html` … 画面レイアウト・UI - `main.js` … ゲームロジック・UI制御 - `style.css` … 盤面や石の見た目 - `dragon.svg`, `dog.svg`, `mouse.svg` … CPU強さイラスト ## 主な仕様 ### ゲームルール - 8x8の標準オセロルール - 石を置ける場所のみクリック可能 - 挟んだ相手の石を自動でひっくり返す - 置ける場所がなければ自動パス - 両者とも置けなければ終了・勝敗表示 ### CPUの強さ - 竜(強):角優先、なければひっくり返せる石が多い手 - 犬(中):ひっくり返せる石が多い手 - ねずみ(弱):完全ランダム ### UI - ゲーム開始時に「先手/後手」「CPU強さ」を選択 - 対局中は盤面上にCPU強さのイラストとラベルを表示 - 「リセット」:現在の設定のまま新規対局 - 「最初の画面に戻る」:設定選択画面に戻る ## メンテナンス・カスタマイズ - 盤面サイズやルール変更は`main.js`の定数・ロジックを修正 - CPUの思考ルーチンは`cpuMove()`関数を編集 - イラスト差し替えはSVGファイルを入れ替え - UI文言やデザインは`index.html`と`style.css`で調整 ## 動作環境 - モダンなWebブラウザ(Chrome, Edge, Firefox, Safari等) - サーバ不要、ローカルで`index.html`を開くだけで動作 ## 注意事項 - 画像ファイル(SVG)は`index.html`と同じディレクトリに配置 - JavaScriptの仕様変更時は動作確認を十分に行うこと ---
感想
昔1週間かけて挫折したのに、、、恐ろしい時代になった、、、(;・∀・)