オセロ作ってみよう
以前にVBAでオセロしたが、JSでも作ってみようと思い早5年www 重すぎる腰を、うっ!腰痛だ。
index.htm
<!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>
<div id="game-options">
<label for="highlight-moves"><br>
<input type="checkbox" id="highlight-moves" checked>
有効な手をハイライト表示
</label>
<br><br>
<label for="ai-level">AIレベル:</label>
<select id="ai-level">
<option value="none">none:人間と対戦</option>
<option value="easy">easyAI:ランダムな場所に置くAI</option>
<option value="smart">smartAI:最も多くの石をひっくり返せる場所に置くAI</option>
<option value="hard">hardAI:強いAI</option> <!-- 強いAIの追加 -->
</select>
</div>
<h1>JavaScriptオセロ</h1>
<div id="game-info">
<p>現在のターン: <span id="current-player">黒</span></p>
<p>黒: <span id="black-score">2</span> / 白: <span id="white-score">2</span></p>
</div>
<div id="board"></div>
<script src="main.js"></script>
</body>
</html>
style.css
#board {
display: grid;
grid-template-columns: repeat(8, 50px);
grid-template-rows: repeat(8, 50px);
width: 400px;
height: 400px;
border: 2px solid #333;
margin: 20px auto;
}
.cell {
width: 50px;
height: 50px;
background-color: green;
border: 1px solid #333;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
}
.cell:hover {
background-color: #2e8b57;
}
.stone {
width: 45px;
height: 45px;
border-radius: 50%;
}
.black {
background-color: black;
}
.white {
background-color: white;
}
.highlight {
background-color: lightblue !important; /* !important で強制的に上書き */
}
main.js
document.addEventListener('DOMContentLoaded', () => {
const boardElement = document.getElementById('board');
const currentPlayerElement = document.getElementById('current-player');
const blackScoreElement = document.getElementById('black-score');
const whiteScoreElement = document.getElementById('white-score');
const highlightMovesCheckbox = document.getElementById('highlight-moves');
const aiLevelSelect = document.getElementById('ai-level');
// 盤面の状態を管理する配列(0: 空, 1: 黒, -1: 白)
let board = [];
// 現在のプレイヤー(1: 黒, -1: 白)
let currentPlayer = 1;
// AIのレベル
let aiLevel = 'none';
// 有効な手をハイライト表示するか
let highlightMoves = highlightMovesCheckbox.checked;
// ゲームの初期化
function initGame() {
board = Array.from({ length: 8 }, () => Array(8).fill(0));
board[3][3] = -1;
board[3][4] = 1;
board[4][3] = 1;
board[4][4] = -1;
currentPlayer = 1;
renderBoard();
updateGameInfo();
aiTurn();
}
// イベントリスナーの設定
highlightMovesCheckbox.addEventListener('change', () => {
highlightMoves = highlightMovesCheckbox.checked;
renderBoard();
});
aiLevelSelect.addEventListener('change', (e) => {
aiLevel = e.target.value;
initGame();
});
// 盤面を描画
function renderBoard() {
boardElement.innerHTML = '';
const validMoves = getAllValidMoves(currentPlayer, board);
for (let y = 0; y < 8; y++) {
for (let x = 0; x < 8; x++) {
const cell = document.createElement('div');
cell.classList.add('cell');
cell.dataset.x = x;
cell.dataset.y = y;
// 有効な手があるマスにのみイベントリスナーを追加
if (validMoves.some(move => move.x === x && move.y === y)) {
cell.addEventListener('click', handleCellClick);
}
if (highlightMoves && validMoves.some(move => move.x === x && move.y === y)) {
cell.classList.add('highlight');
}
if (board[y][x] !== 0) {
const stone = document.createElement('div');
stone.classList.add('stone', board[y][x] === 1 ? 'black' : 'white');
cell.appendChild(stone);
}
boardElement.appendChild(cell);
}
}
}
// セルがクリックされた時の処理
function handleCellClick(event) {
if (aiLevel !== 'none' && currentPlayer === -1) return; // 人間vsAIの場合、AIのターンはクリック無効
const x = parseInt(event.target.dataset.x);
const y = parseInt(event.target.dataset.y);
if (board[y][x] !== 0) return;
const flippedStones = getFlippableStones(x, y, currentPlayer, board);
if (flippedStones.length > 0) {
placeStone(x, y, currentPlayer, board);
flipStones(flippedStones, board);
switchPlayer();
renderBoard();
updateGameInfo();
aiTurn();
}
}
// プレイヤーを交代
function switchPlayer() {
currentPlayer *= -1;
}
// AIのターン
function aiTurn() {
if (aiLevel !== 'none' && currentPlayer === -1) {
setTimeout(() => {
let move;
switch (aiLevel) {
case 'easy':
move = getRandomMove(getAllValidMoves(currentPlayer, board));
break;
case 'smart':
move = getSmartMove(getAllValidMoves(currentPlayer, board));
break;
case 'hard':
move = getHardMove();
break;
default:
return;
}
if (move) {
// AIの行動を反映
const x = move.x;
const y = move.y;
const flippedStones = getFlippableStones(x, y, currentPlayer, board);
placeStone(x, y, currentPlayer, board);
flipStones(flippedStones, board);
switchPlayer();
renderBoard();
updateGameInfo();
aiTurn();
} else {
passTurn();
}
}, 100);
} else if (!canPlaceStone(currentPlayer, board)) {
passTurn();
}
}
// パス処理
function passTurn() {
if (canPlaceStone(-currentPlayer, board)) {
alert(`${currentPlayer === 1 ? '黒' : '白'}は石を置く場所がないのでパスします。`);
switchPlayer();
renderBoard();
updateGameInfo();
aiTurn();
} else {
endGame();
}
}
// 石を置く処理
function placeStone(x, y, player, targetBoard) {
targetBoard[y][x] = player;
}
// 石をひっくり返す処理
function flipStones(stones, targetBoard) {
stones.forEach(({ x, y }) => {
targetBoard[y][x] *= -1;
});
}
// ひっくり返せる石を判定
function getFlippableStones(x, y, player, targetBoard) {
const flipped = [];
if (targetBoard[y][x] !== 0) return [];
const directions = [
[-1, -1], [-1, 0], [-1, 1],
[0, -1], [0, 1],
[1, -1], [1, 0], [1, 1]
];
directions.forEach(([dx, dy]) => {
const currentFlippable = [];
let [nx, ny] = [x + dx, y + dy];
while (nx >= 0 && nx < 8 && ny >= 0 && ny < 8 && targetBoard[ny][nx] === -player) {
currentFlippable.push({ x: nx, y: ny });
[nx, ny] = [nx + dx, ny + dy];
}
if (nx >= 0 && nx < 8 && ny >= 0 && ny < 8 && targetBoard[ny][nx] === player) {
flipped.push(...currentFlippable);
}
});
return flipped;
}
// 有効な手の座標をすべて取得
function getAllValidMoves(player, targetBoard) {
const validMoves = [];
for (let y = 0; y < 8; y++) {
for (let x = 0; x < 8; x++) {
if (targetBoard[y][x] === 0 && getFlippableStones(x, y, player, targetBoard).length > 0) {
validMoves.push({ x, y });
}
}
}
return validMoves;
}
// 石を置ける場所があるか判定
function canPlaceStone(player, targetBoard) {
return getAllValidMoves(player, targetBoard).length > 0;
}
// ゲーム情報を更新
function updateGameInfo() {
const blackCount = board.flat().filter(s => s === 1).length;
const whiteCount = board.flat().filter(s => s === -1).length;
currentPlayerElement.textContent = currentPlayer === 1 ? '黒' : '白';
blackScoreElement.textContent = blackCount;
whiteScoreElement.textContent = whiteCount;
}
// ゲーム終了処理
function endGame() {
const blackCount = board.flat().filter(s => s === 1).length;
const whiteCount = board.flat().filter(s => s === -1).length;
let message = 'ゲーム終了!';
if (blackCount > whiteCount) {
message += `黒の勝利です! (${blackCount} vs ${whiteCount})`;
} else if (whiteCount > blackCount) {
message += `白の勝利です! (${whiteCount} vs ${blackCount})`;
} else {
message += `引き分けです! (${blackCount} vs ${whiteCount})`;
}
alert(message);
}
// AIのランダムな有効手を取得
function getRandomMove(validMoves) {
if (validMoves.length === 0) return null;
const randomIndex = Math.floor(Math.random() * validMoves.length);
return validMoves[randomIndex];
}
// AIが最も多くの石をひっくり返せる手を取得
function getSmartMove(validMoves) {
if (validMoves.length === 0) return null;
let bestMove = null;
let maxFlips = -1;
for (const move of validMoves) {
const flips = getFlippableStones(move.x, move.y, currentPlayer, board).length;
if (flips > maxFlips) {
maxFlips = flips;
bestMove = move;
}
}
return bestMove;
}
// 最強AI(ミニマックス法 + α-β法)
function getHardMove() {
let bestMove = null;
let bestScore = -Infinity;
const validMoves = getAllValidMoves(currentPlayer, board);
if (validMoves.length === 0) return null;
for (const move of validMoves) {
const tempBoard = deepCopyBoard(board);
const flippedStones = getFlippableStones(move.x, move.y, currentPlayer, tempBoard);
placeStone(move.x, move.y, currentPlayer, tempBoard);
flipStones(flippedStones, tempBoard);
//既存深度は3、あまり深くすると考察が長い
const score = minimax(tempBoard, 5, -currentPlayer, -Infinity, Infinity);
if (score > bestScore) {
bestScore = score;
bestMove = move;
}
}
return bestMove;
}
// ミニマックス法(α-β法で枝刈り)
function minimax(currentBoard, depth, player, alpha, beta) {
if (depth === 0 || isGameOver(currentBoard)) {
return evaluateBoard(currentBoard, player);
}
const validMoves = getAllValidMoves(player, currentBoard);
if (validMoves.length === 0) {
if (getAllValidMoves(-player, currentBoard).length === 0) {
return evaluateBoard(currentBoard, player);
}
return -minimax(currentBoard, depth - 1, -player, -beta, -alpha);
}
let value = -Infinity;
for (const move of validMoves) {
const newBoard = deepCopyBoard(currentBoard);
const flipped = getFlippableStones(move.x, move.y, player, newBoard);
placeStone(move.x, move.y, player, newBoard);
flipStones(flipped, newBoard);
value = Math.max(value, -minimax(newBoard, depth - 1, -player, -beta, -alpha));
alpha = Math.max(alpha, value);
if (beta <= alpha) {
break;
}
}
return value;
}
// 盤面を評価
function evaluateBoard(targetBoard, player) {
const weights = [
[100,-20, 10, 5, 5, 10, -20, 100],
[-20,-50, -2, -2, -2, -2, -50, -20],
[10, -2, -1, -1, -1, -1, -2, 10],
[5, -2, -1, -1, -1, -1, -2, 5],
[5, -2, -1, -1, -1, -1, -2, 5],
[10, -2, -1, -1, -1,-1, -2, 10],
[-20, -50, -2, -2, -2, -2, -50, -20],
[100, -20, 10, 5, 5, 10, -20, 100]
];
let score = 0;
for (let y = 0; y < 8; y++) {
for (let x = 0; x < 8; x++) {
score += weights[y][x] * targetBoard[y][x];
}
}
if (isGameOver(targetBoard)) {
const myStones = targetBoard.flat().filter(s => s === player).length;
const opponentStones = targetBoard.flat().filter(s => s === -player).length;
return score + (myStones - opponentStones) * 1000;
}
return score * player;
}
// ボードのディープコピー
function deepCopyBoard(original) {
return original.map(arr => [...arr]);
}
// ゲームが終了したかを判定
function isGameOver(targetBoard) {
return getAllValidMoves(1, targetBoard).length === 0 && getAllValidMoves(-1, targetBoard).length === 0;
}
// 最初にゲームを初期化
initGame();
});
ミニマックス法 + α-β法で最強を目指した。
法は良いとして、設定はまだ詰めが甘い。
あまり強くないんだがwww
疲れたからこのへんで!