
【ゲーム】JavaScript:09 三目並べ(まるぺけ)
「❌⭕️ 三目並べ(まるぺけ)」は、3×3のマス目内で「〇」と「×」を交互に置き、縦・横・斜めいずれかに自分のマークを3つ並べたほうが勝ちとなる古典的なパズルゲームです。このWeb版ではプレイヤーが「〇」、コンピュータが「×」として、3段階の難易度から対戦できます。
遊び方と操作方法
- ページを開くと難易度プルダウンが表示されるので、コンピュータの強さ(簡単/普通/難しい)を選択。
- 盤面(3×3のグリッド)が空の状態で表示されるので、プレイヤー(「〇」)のターン時に任意のマスをクリックしてマークを置きます。
- プレイヤーが置くと自動的にコンピュータ(「×」)が思考ルーチンに基づいて1手を打ちます。
- いずれかが3つ並べるか、全マスが埋まって引き分けになるまで続きます。
- 「🔄 リセット」ボタンでいつでも初期状態に戻せます。
ルール
- 空いているマスにのみマークを置けます。
- 先に自分のマークを縦・横・斜めいずれかにつなげて3つ並べたプレイヤーが勝利。
- 全マスが埋まっても勝敗がつかなければ「引き分け」。
- 難易度「easy」…ランダム手、「medium」…自分または相手の勝ちを阻止する手を優先、「hard」…(現状mediumと同じ)最善手を狙います。
🎮ゲームプレイ
以下のリンク先から実際にプレイできます。
09 三目並べ(まるぺけ)
素材のダウンロード
以下のリンクから使用する素材をダウンロードできます。
tic_tac_toe_bg.png |
---|
![]() |
ゲーム画面イメージ

プログラム全文(tic_tac_toe.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>
/* ── 全体設定 ── */
body {
margin: 0;
padding: 0;
font-family: 'Arial', sans-serif;
background: url('tic_tac_toe_bg.png') no-repeat center center fixed;
background-size: cover;
display: flex;
flex-direction: column;
align-items: center;
min-height: 100vh;
}
/* ── タイトル ── */
h1 {
margin-top: 20px;
font-size: 3rem;
color: #fff;
/* 半透明の黒背景で視認性アップ */
background-color: rgba(0, 0, 0, 0.6);
padding: 12px 24px;
border-radius: 8px;
text-shadow: 2px 2px 6px #000;
}
/* ── 難易度選択 ── */
#level {
margin: 10px;
background-color: rgba(255,255,255,0.9);
padding: 8px 12px;
border-radius: 5px;
box-shadow: 0 2px 4px rgba(0,0,0,0.3);
}
#level label {
font-size: 1.2rem;
color: #000;
text-shadow: none;
}
#difficulty {
margin-left: 10px;
padding: 5px 8px;
font-size: 1rem;
border-radius: 5px;
border: 1px solid #ccc;
background-color: #fff;
color: #000;
}
/* ── ゲーム盤 ── */
#game-board {
display: grid;
grid-template-columns: repeat(3, 100px);
grid-template-rows: repeat(3, 100px);
gap: 8px;
margin: 20px 0;
}
.cell {
width: 100px;
height: 100px;
background: rgba(255,255,255,0.95);
border: 3px solid #333;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 2.5rem;
cursor: pointer;
box-shadow: 0 4px 8px rgba(0,0,0,0.3);
transition: transform 0.2s, background 0.3s;
}
.cell:hover {
transform: scale(1.05);
background: rgba(255,255,255,1);
}
.cell.taken {
cursor: default;
opacity: 0.7;
}
/* ── ステータス表示 ── */
#status {
margin: 10px;
padding: 10px 20px;
font-size: 1.5rem;
color: #fff;
background-color: rgba(0,0,0,0.5);
border-radius: 8px;
text-shadow: 1px 1px 4px #000;
}
/* ── リセットボタン ── */
button {
padding: 10px 20px;
font-size: 1rem;
margin-bottom: 20px;
border: none;
border-radius: 5px;
background-color: #ff4081;
color: #fff;
cursor: pointer;
box-shadow: 0 4px 6px rgba(0,0,0,0.3);
transition: background 0.3s;
}
button:hover {
background-color: #e91e63;
}
</style>
</head>
<body>
<!-- タイトル -->
<h1>❌⭕️ 三目並べ 🎉</h1>
<!-- 難易度選択 -->
<div id="level">
<label for="difficulty">🛠️ コンピューターの強さ:</label>
<select id="difficulty">
<option value="easy">簡単</option>
<option value="medium">普通</option>
<option value="hard">難しい</option>
</select>
</div>
<!-- ゲーム盤 -->
<div id="game-board"></div>
<!-- ステータス表示 -->
<div id="status">🎮 プレイヤー 〇 のターン</div>
<!-- リセットボタン -->
<button onclick="resetGame()">🔄 リセット</button>
<script>
// ── 要素取得 ──
const board = document.getElementById('game-board');
const status = document.getElementById('status');
const difficulty = document.getElementById('difficulty');
// ── ゲーム状態 ──
let currentPlayer = '〇'; // 人間は「〇」
let gameActive = true; // ゲーム継続フラグ
let gameState = Array(9).fill(''); // 盤面配列
// ── 勝利パターン ──
const winningConditions = [
[0,1,2], [3,4,5], [6,7,8], // 横列
[0,3,6], [1,4,7], [2,5,8], // 縦列
[0,4,8], [2,4,6] // 斜め
];
/**
* セルクリック時のハンドラ
*/
function handleCellClick(event) {
const cell = event.target;
const index = parseInt(cell.getAttribute('data-index'));
// 埋まっていたりゲーム終了中、人のターン以外は処理しない
if (gameState[index] !== '' || !gameActive || currentPlayer !== '〇') {
return;
}
// 人間の手を反映
gameState[index] = currentPlayer;
cell.textContent = currentPlayer;
cell.classList.add('taken');
// 勝利判定
if (checkWinner()) {
status.textContent = `🎉 プレイヤー ${currentPlayer} の勝利!`;
gameActive = false;
return;
}
// 引き分け判定
if (gameState.every(cell => cell !== '')) {
status.textContent = '😲 引き分けです!';
gameActive = false;
return;
}
// コンピューターのターンへ
currentPlayer = '×';
status.textContent = '💻 コンピューターのターン';
setTimeout(computerMove, 800);
}
/**
* コンピューターの手を決定・配置
*/
function computerMove() {
if (!gameActive) return;
let moveIndex;
// 難易度ごとの思考ルーチン
if (difficulty.value === 'easy') {
moveIndex = getRandomMove();
} else if (difficulty.value === 'medium') {
moveIndex = getWinningMove('×') || getWinningMove('〇') || getRandomMove();
} else {
moveIndex = getWinningMove('×') || getWinningMove('〇') || getRandomMove();
}
// コンピューターの手を反映
gameState[moveIndex] = '×';
const cell = document.querySelector(`.cell[data-index='${moveIndex}']`);
cell.textContent = '×';
cell.classList.add('taken');
// 勝利判定
if (checkWinner()) {
status.textContent = '☠️ コンピューターの勝利!';
gameActive = false;
return;
}
// 引き分け判定
if (gameState.every(cell => cell !== '')) {
status.textContent = '😲 引き分けです!';
gameActive = false;
return;
}
// 人間のターンへ戻す
currentPlayer = '〇';
status.textContent = `🎮 プレイヤー ${currentPlayer} のターン`;
}
/**
* 勝利できる手を探索
* @param {string} player - '〇' か '×'
*/
function getWinningMove(player) {
for (let condition of winningConditions) {
const [a, b, c] = condition;
if (gameState[a] === player && gameState[b] === player && gameState[c] === '') return c;
if (gameState[a] === player && gameState[b] === '' && gameState[c] === player) return b;
if (gameState[a] === '' && gameState[b] === player && gameState[c] === player) return a;
}
return null;
}
/**
* 空きセルからランダムに選択
*/
function getRandomMove() {
const available = gameState
.map((val, idx) => val === '' ? idx : null)
.filter(idx => idx !== null);
return available[Math.floor(Math.random() * available.length)];
}
/**
* 勝利条件を満たしているかどうかチェック
*/
function checkWinner() {
return winningConditions.some(condition => {
const [a, b, c] = condition;
return (
gameState[a] === currentPlayer &&
gameState[b] === currentPlayer &&
gameState[c] === currentPlayer
);
});
}
/**
* ゲームをリセット
*/
function resetGame() {
gameActive = true;
currentPlayer = '〇';
gameState = Array(9).fill('');
status.textContent = `🎮 プレイヤー ${currentPlayer} のターン`;
board.innerHTML = '';
createBoard();
}
/**
* 盤面のセルを作成
*/
function createBoard() {
for (let i = 0; i < 9; i++) {
const cell = document.createElement('div');
cell.classList.add('cell');
cell.setAttribute('data-index', i);
cell.addEventListener('click', handleCellClick);
board.appendChild(cell);
}
}
// 初期盤面を生成
createBoard();
</script>
</body>
</html>
アルゴリズムの流れ
ステップ | 処理内容 |
---|---|
盤面初期化 (createBoard ) | 9 マスの <div> を生成し、data-index とクリックイベントを設定 |
プレイヤー手執行 (handleCellClick ) | 空きマスに「〇」を配置→勝利判定→引き分け判定→コンピュータ手番への切替 |
勝利判定 (checkWinner ) | winningConditions の各三角配列に当てはまるかチェック |
コンピュータ手 (computerMove ) | 難易度別に:• easy → getRandomMove() • medium/hard → getWinningMove('×') または getWinningMove('〇') で勝ち/阻止手候補、なければランダム |
勝ち手探索 (getWinningMove ) | 勝利/阻止可能な 3 連の空きマス位置を走査 |
ランダム手 (getRandomMove ) | 空きマスのインデックスを列挙し、ランダムに1つ返却 |
リセット (resetGame ) | ゲーム状態初期化&盤面再生成 |
関数の詳細
関数名 | 機能 |
---|---|
createBoard() | 盤面 <div> 要素を 3×3 生成し、data-index とクリック ハンドラを登録 |
handleCellClick(event) | プレイヤーの石を配置→勝敗/引き分け判定→次ターンに移行 |
computerMove() | コンピュータの手を決定し配置→勝敗/引き分け判定→次ターンに移行 |
getWinningMove(player) | 指定プレイヤーが即勝利または相手阻止可能なマスを探索し、見つかればそのインデックスを返却 |
getRandomMove() | 空きマスからランダムなインデックスを返却 |
checkWinner() | winningConditions 配列を走査し、現行プレイヤーが勝利状態にあるかを真偽で返却 |
resetGame() | ゲーム状態・UI を初期化し、盤面を再生成 |
このゲームの改造のポイント
- ミニマックスAI:medium/hard を汎用的なミニマックス法に置き換え、最適手を常に選択する強力 AI を実装。
- 先攻後攻切替:初期の先攻プレイヤーをランダム化したり、毎回交代制にして戦略の幅を広げる。
- リプレイ機能:ゲーム中の全手を記録して再生できるようにし、自分や他者の戦略を振り返る。
- UI拡張:タッチ操作対応、アニメーション演出(勝利ラインのハイライト)、カスタムテーマ(色/フォント)選択。
- ネット対戦:WebSocket を利用し、離れた友人同士でリアルタイム対戦できる機能。
- 拡張ボード:3×3 以外のサイズ(4×4、5×5)や「五目並べ」にも対応できる汎用ロジックへの一般化。
これらを加えて、自分だけの「まるぺけ」をより奥深いゲームに発展させてみてください!