
【ゲーム】JavaScript:04 オセロ(リバーシ)
8×8 の盤上で黒(プレイヤー)と白(コンピュータ)が交互に石を置き、相手の石を直線上で挟み返すことで自分の色にひっくり返し、最終的に盤面上に多くの石を持つほうが勝利となる、Webブラウザ上で動作する定番ボードゲームです。コンピュータレベル(簡単・普通・難しい)を選んで戦えます。
ゲームの遊び方
画面上部のプルダウンで「簡単」「普通」「難しい」のいずれかを選び、🔄リセットボタンを押すとゲーム開始。黒石がプレイヤーの手番です。置ける場所をクリックすると、その手で裏返せる相手石が自動でひっくり返ります。続いてコンピュータ(白)が同様に石を置き……を繰り返します。
ルール
- 交互に空いているマスに石を1つずつ置く。
- 石を置いた位置から見て、隣接する直線上に必ず相手の石が1枚以上あり、その先に自分の石がある場合のみ置ける。
- 置ける場所がない場合はパスし、両者ともに置ける場所がなくなるか、盤面が埋まるとゲーム終了。
- 終了時に盤面上の黒/白を数え、多いほうの勝利。
コンピュータのレベル
コンピュータのレベル設定は、次のロジックで動作します。
- easy: 完全ランダム
- medium: ひっくり返す石の数で上位半分からランダム
- hard: 最大数をひっくり返す手をランダムに
🎮ゲームプレイ
以下のリンク先から実際にプレイできます。
04 オセロ(リバーシ)
素材のダウンロード
以下のリンクから使用する素材をダウンロードできます。
othello_bg.png |
---|
![]() |
ゲーム画面イメージ

プログラム全文(othello.html)
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>⚫️⚪️ オセロゲーム 🏆</title>
<style>
/* 全体リセットと背景 */
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: Arial, sans-serif;
background: url('othello_bg.png') no-repeat center center fixed, linear-gradient(135deg, #445, #112);
background-size: cover;
color: #fff;
text-align: center;
padding: 20px;
}
/* タイトル */
h1 {
font-size: 2.5rem;
background-color: rgba(0,0,0,0.7);
display: inline-block;
padding: 10px 20px;
border-radius: 8px;
text-shadow: 2px 2px 4px #000;
margin-bottom: 20px;
}
/* コントロール */
#controls {
width: 460px;
margin: 10px auto 20px;
background: rgba(0,0,0,0.6);
padding: 10px;
border-radius: 6px;
}
#controls label { color: #fff; font-weight: bold; margin-right: 8px; }
#controls select { padding: 8px 12px; font-size: 1rem; border: none; border-radius: 4px; background: #fff; color: #000; margin-right: 10px; }
#controls button { padding: 8px 12px; font-size: 1rem; border: none; border-radius: 4px; background: #28a745; color: #fff; cursor: pointer; }
#controls button:hover { background: #218838; }
/* ゲーム盤 */
#game-board {
display: grid;
grid-template-columns: repeat(8, 50px);
grid-auto-rows: 50px;
gap: 2px;
justify-content: center;
margin: 0 auto 20px;
background-color: rgba(0,0,0,0.6);
padding: 4px;
border-radius: 8px;
/* 幅を盤サイズに合わせて自動計算 */
width: calc(8 * 50px + 7 * 2px + 2 * 4px);
}
.cell {
background: #228B22;
border: 1px solid #006400;
border-radius: 4px;
position: relative;
cursor: pointer;
}
.cell.disabled { cursor: not-allowed; opacity: 0.6; }
.disk {
width: 80%; height: 80%;
border-radius: 50%;
position: absolute; top: 10%; left: 10%;
}
.disk.black { background: black; }
.disk.white { background: white; }
.disk.flip { animation: flip 0.5s forwards; }
@keyframes flip {
0% { transform: rotateY(0deg); }
50% { transform: rotateY(90deg); background: transparent; }
100% { transform: rotateY(180deg); }
}
/* ステータス */
#status {
width: 460px;
margin: 0 auto 20px;
background: rgba(0,0,0,0.6);
padding: 10px 20px;
border-radius: 4px;
font-size: 1.2rem;
text-shadow: 1px 1px 2px #000;
}
/* ルール */
#rules {
width: 690px;
margin: 0 auto;
background: rgba(0,0,0,0.6);
padding: 12px;
border-radius: 6px;
text-align: left;
font-size: 0.95rem;
}
#rules strong { display: block; margin-bottom: 8px; }
#rules ul { list-style: disc inside; }
</style>
</head>
<body>
<h1>⚫️⚪️ オセロゲーム 🏆</h1>
<div id="controls">
<label for="difficulty">コンピュータレベル:</label>
<select id="difficulty">
<option value="easy">簡単</option>
<option value="medium">普通</option>
<option value="hard">難しい</option>
</select>
<button onclick="resetGame()">🔄 リセット</button>
</div>
<div id="game-board"></div>
<div id="status">👤 プレイヤーのターン</div>
<div id="rules">
<strong>ゲームのルール</strong>
<ul>
<li>プレイヤーは黒、コンピュータは白を交互に置きます。</li>
<li>相手の石を直線で挟むと、その間の石が自分の色にひっくり返ります。</li>
<li>置ける場所がなくなるか盤が埋まるとゲーム終了です。</li>
</ul>
</div>
<script>
const SIZE = 8;
const board = document.getElementById('game-board');
const status = document.getElementById('status');
const difficulty = document.getElementById('difficulty');
let gameState = Array.from({length:SIZE}, () => Array(SIZE).fill(null));
let currentPlayer = 'black';
let gameActive = true;
// 初期化
function initializeBoard() {
board.innerHTML = '';
for (let r = 0; r < SIZE; r++) {
for (let c = 0; c < SIZE; c++) {
const cell = document.createElement('div');
cell.className = 'cell';
cell.dataset.row = r;
cell.dataset.col = c;
cell.addEventListener('click', handleCellClick);
board.appendChild(cell);
}
}
setInitialDisks();
}
function setInitialDisks() {
placeDisk(3,3,'white'); placeDisk(4,4,'white');
placeDisk(3,4,'black'); placeDisk(4,3,'black');
}
function placeDisk(r,c,color) {
const cell = document.querySelector(`.cell[data-row='${r}'][data-col='${c}']`);
const disk = document.createElement('div'); disk.className = 'disk ' + color;
cell.appendChild(disk); gameState[r][c] = color;
}
function handleCellClick(evt) {
if (!gameActive || currentPlayer !== 'black') return;
const r = +evt.currentTarget.dataset.row;
const c = +evt.currentTarget.dataset.col;
if (gameState[r][c] || !isValidMove(r,c,'black')) return;
makeMove(r,c,'black');
if (isGameOver()) { declareWinner(); return; }
if (!hasValidMoves('white')) {
status.textContent = '🤖 コンピュータは置ける場所がありません。プレイヤー続行。';
return;
}
switchTurn();
setTimeout(computerTurn, 500);
}
function isValidMove(r,c,color) {
return getFlippableDisks(r,c,color).length > 0;
}
function getFlippableDisks(r,c,color) {
const opp = color==='black'?'white':'black';
const dirs = [[1,0],[-1,0],[0,1],[0,-1],[1,1],[1,-1],[-1,1],[-1,-1]];
let flips = [];
dirs.forEach(([dx,dy]) => {
let path = [];
let x=r+dx, y=c+dy;
while (x>=0 && x<SIZE && y>=0 && y<SIZE && gameState[x][y]===opp) {
path.push([x,y]); x+=dx; y+=dy;
}
if (x>=0 && x<SIZE && y>=0 && y<SIZE && gameState[x][y]===color) {
flips = flips.concat(path);
}
});
return flips;
}
function makeMove(r,c,color) {
const flips = getFlippableDisks(r,c,color);
flips.forEach(([x,y]) => flipDisk(x,y,color));
placeDisk(r,c,color);
}
function flipDisk(r,c,color) {
gameState[r][c]=color;
const cell = document.querySelector(`.cell[data-row='${r}'][data-col='${c}']`);
cell.innerHTML = '';
const disk = document.createElement('div'); disk.className = 'disk ' + color + ' flip';
cell.appendChild(disk);
}
function computerTurn() {
if (!gameActive) return;
const candidates = [];
for (let r=0; r<SIZE; r++) for (let c=0; c<SIZE; c++) {
if (!gameState[r][c]) {
const flips = getFlippableDisks(r,c,'white').length;
if (flips>0) candidates.push({r,c,flips});
}
}
if (candidates.length===0) {
if (!hasValidMoves('black')) { declareWinner(); return; }
status.textContent = '🤖 コンピュータは置ける場所がありません。プレイヤー続行。';
return;
}
const level = difficulty.value;
let choice;
if (level==='easy') {
choice = candidates[Math.floor(Math.random()*candidates.length)];
} else if (level==='medium') {
const sorted = [...candidates].sort((a,b)=>b.flips-a.flips);
const half = Math.ceil(sorted.length/2);
choice = sorted[Math.floor(Math.random()*half)];
} else {
const max = Math.max(...candidates.map(c=>c.flips));
const best = candidates.filter(c=>c.flips===max);
choice = best[Math.floor(Math.random()*best.length)];
}
makeMove(choice.r,choice.c,'white');
if (isGameOver()) { declareWinner(); return; }
if (!hasValidMoves('black')) {
status.textContent = '👤 プレイヤーは置ける場所がありません。コンピュータ続行。';
setTimeout(computerTurn,500);
return;
}
switchTurn();
}
function switchTurn() {
currentPlayer = currentPlayer==='black'?'white':'black';
status.textContent = currentPlayer==='black'?'👤 プレイヤーのターン':'🤖 コンピュータのターン';
}
function hasValidMoves(color) {
return gameState.some((row,r)=>row.some((cell,c)=>!cell && isValidMove(r,c,color)));
}
function isGameOver() {
return !hasValidMoves('black') && !hasValidMoves('white');
}
function declareWinner() {
gameActive=false;
const flat = gameState.flat();
const b = flat.filter(x=>x==='black').length;
const w = flat.filter(x=>x==='white').length;
if (b>w) status.textContent = `🏅 プレイヤー勝利! 黒:${b} 白:${w}`;
else if (w>b) status.textContent = `🏅 コンピュータ勝利! 黒:${b} 白:${w}`;
else status.textContent = `🤝 引き分け! 黒:${b} 白:${w}`;
}
function resetGame() {
gameState = Array.from({length:SIZE}, ()=>Array(SIZE).fill(null));
currentPlayer='black'; gameActive=true;
status.textContent='👤 プレイヤーのターン';
initializeBoard();
}
// 実行
initializeBoard();
</script>
</body>
</html>
アルゴリズムの流れ
ステップ | 処理内容 |
---|---|
盤面初期化 (initializeBoard ) | 8×8 のセルを生成し、setInitialDisks で中央4マスに黒白を配置 |
合法手判定 (isValidMove ) | getFlippableDisks で裏返せる石があれば合法手と判定 |
裏返し対象取得 (getFlippableDisks ) | 8方向に向かって相手色が連続し、その先に自色がある経路を探索し、ひっくり返す座標リストを返却 |
石配置・反転 (makeMove /flipDisk ) | 指定マスに石を placeDisk 、返却リストを flipDisk で CSS アニメ付きに置き換え |
プレイヤー操作 (handleCellClick ) | クリックで合法手判断→makeMove →終了判定→パス判定→コンピュータ手番へ |
コンピュータ操作 (computerTurn ) | 合法手リストを収集し、レベル別にランダム/上位半分/最大取得数から最適手を選択→makeMove →終了判定→次のターン |
終了判定 (isGameOver /declareWinner ) | 両者とも合法手なしでゲーム終了→黒/白をカウントし勝敗/引き分けを表示 |
組み込みメソッド・命令
メソッド | 用途 |
---|---|
document.createElement | セルや石の DOM 要素を動的に生成 |
element.addEventListener | クリック/プルダウン変更イベントの監視 |
querySelector | 特定セルの取得 |
Array.from / Array.fill | 盤面用 2D 配列の初期化 |
Array.prototype.some | 配列内に合法手があるかどうかチェック |
Array.prototype.flat | 2D 配列を平坦化し石数カウントに利用 |
setTimeout | コンピュータ番の待ち時間演出 |
関数の詳細
関数名 | 機能 |
---|---|
initializeBoard() | 盤面セルの生成・イベント登録 → setInitialDisks() で初期石配置 |
getFlippableDisks(r,c,color) | 指定マスから 8 方向の相手色連続経路を調べ、最終的に自色で挟む位置までの座標リストを返す |
makeMove(r,c,color) | 石を配置 → getFlippableDisks の結果を flipDisk でアニメ付き反転 |
flipDisk(r,c,color) | gameState の更新とともに、該当セルの石を CSS アニメ .flip でひっくり返す |
computerTurn() | 全合法手を収集 → 選択アルゴリズム(easy/medium/hard)で一手選択 → makeMove → ターン交代またはパス判定 |
hasValidMoves(color) | 盤面全セルを走査し、少なくとも1つ合法手があるかを判定 |
isGameOver() | hasValidMoves('black') と hasValidMoves('white') 両方が false の場合、ゲーム終了とする |
declareWinner() | 両色の石数をカウントし、勝敗または引き分けメッセージを #status に表示 |
resetGame() | gameState とターン・状態を初期化し、initializeBoard() を再実行 |
改造のポイント
- AI をミニマックスや重み付き評価関数に置き換えて高度化。
- 合法手ハイライト機能を追加してユーザーに分かりやすくガイド。
- 現在の石数スコアをリアルタイムで表示。
- タッチ/モバイル操作に対応するため
touchstart
イベントを追加。 - ダーク/ライトテーマ切替や盤サイズのレスポンシブ化。
これらを組み合わせて、自分だけのオリジナルオセロを作ってみてください!