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

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


コードはこちら
※コードを同じフォルダに保存しindex.htmlをブラウザで表示すれば遊べます!
index.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');
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);
}
let board;
let currentPlayer;
let gameOver;
let playerColor = 1;
let cpuLevel = 'strong';
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() {
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)') + 'の番です';
}
}
}
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;
}
doragon.svg
<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週間かけて挫折したのに、、、恐ろしい時代になった、、、(;・∀・)