〇✕ゲーム【CPU戦】
Web
2024年12月25日
みなさんメリークリスマスです。
ケミストのWeb担当みやのです。
「Web」記事では、これまでに得たWebに関する知識を記録として残していきたいと思います。
今回は「〇✕ゲーム」の「CPU戦」を作ってみました!クリスマスに何をやっているんだろう
難易度は「ふつう」です。さあレッツトライ!
もう一回遊ぶ
つくってみよう
とりあえずプレイヤーは〇で先攻、CPUは✕で後攻で固定です。
HTMLは前回とほぼ同じです。
<div class="wrapper">
<div class="game-container">
<div class="message-container">
<ul class="message-list">
<li id="maru-turn" class="js-hidden">あなたの番です</li>
<li id="batsu-turn" class="js-hidden">相手の番です</li>
<li id="maru-win" class="js-hidden">勝利ッ!</li>
<li id="batsu-win" class="js-hidden">敗北した...</li>
<li id="draw" class="js-hidden">引き分け...</li>
</ul>
</div>
<div class="squares-container">
<div class="squares-box">
<div id="1-1" class="square"></div>
<div id="1-2" class="square"></div>
<div id="1-3" class="square"></div>
<div id="2-1" class="square"></div>
<div id="2-2" class="square"></div>
<div id="2-3" class="square"></div>
<div id="3-1" class="square"></div>
<div id="3-2" class="square"></div>
<div id="3-3" class="square"></div>
</div>
</div>
<div class="btn-container">
<div class="js-hidden" id="reset-btn">
<span class="btn btn-reset">もう一回遊ぶ</span>
</div>
</div>
</div>
</div>
「〇の番」を「あなたの番です」に変えてみました あな番
CSSは変更なしです。
スクリプトは(ChatGPTが)頑張りました。
document.addEventListener('DOMContentLoaded', () => {
const squares = document.querySelectorAll('.square');
const messageList = document.querySelector('.message-list');
const maruTurnMessage = document.getElementById('maru-turn');
const batsuTurnMessage = document.getElementById('batsu-turn');
const maruWinMessage = document.getElementById('maru-win');
const batsuWinMessage = document.getElementById('batsu-win');
const drawMessage = document.getElementById('draw');
const resetBtn = document.getElementById('reset-btn');
let currentTurn = 'maru';
let gameActive = true;
let winningLine = [];
const gameState = [
['', '', ''],
['', '', ''],
['', '', '']
];
const winningPatterns = [
[[0, 0], [0, 1], [0, 2]],
[[1, 0], [1, 1], [1, 2]],
[[2, 0], [2, 1], [2, 2]],
[[0, 0], [1, 0], [2, 0]],
[[0, 1], [1, 1], [2, 1]],
[[0, 2], [1, 2], [2, 2]],
[[0, 0], [1, 1], [2, 2]],
[[0, 2], [1, 1], [2, 0]]
];
function checkWinner() {
for (const pattern of winningPatterns) {
const [a, b, c] = pattern;
if (
gameState[a[0]][a[1]] !== '' &&
gameState[a[0]][a[1]] === gameState[b[0]][b[1]] &&
gameState[a[0]][a[1]] === gameState[c[0]][c[1]]
) {
winningLine = [a, b, c];
return gameState[a[0]][a[1]];
}
}
return '';
}
function checkDraw() {
return gameState.flat().every(cell => cell !== '');
}
function handleSquareClick(e) {
const square = e.target;
const row = square.id.split('-')[0] - 1;
const col = square.id.split('-')[1] - 1;
if (gameState[row][col] !== '' || !gameActive) {
return;
}
if (currentTurn === 'maru') {
square.classList.add('js-maru-checked');
gameState[row][col] = 'maru';
}
const winner = checkWinner();
if (winner !== '') {
endGame(winner === 'maru' ? '〇の勝ちです!' : '✕の勝ちです!');
if (winner === 'maru') {
fireworks();
}
drawWinningLine();
return;
}
if (checkDraw()) {
endGame('引き分けです!');
return;
}
currentTurn = 'batsu';
updateTurnMessage();
if (currentTurn === 'batsu') {
setTimeout(cpuTurn, 500);
}
}
function updateTurnMessage() {
maruTurnMessage.classList.add('js-hidden');
batsuTurnMessage.classList.add('js-hidden');
if (currentTurn === 'maru') {
maruTurnMessage.classList.remove('js-hidden');
} else {
batsuTurnMessage.classList.remove('js-hidden');
}
}
function cpuTurn() {
if (!gameActive) return;
let square;
const availableSquares = Array.from(squares).filter(sq => !sq.classList.contains('js-maru-checked') && !sq.classList.contains('js-batsu-checked'));
for (const sq of availableSquares) {
const row = sq.id.split('-')[0] - 1;
const col = sq.id.split('-')[1] - 1;
gameState[row][col] = 'batsu';
if (checkWinner() === 'batsu') {
square = sq;
break;
}
gameState[row][col] = '';
}
if (!square) {
for (const sq of availableSquares) {
const row = sq.id.split('-')[0] - 1;
const col = sq.id.split('-')[1] - 1;
gameState[row][col] = 'maru';
if (checkWinner() === 'maru') {
square = sq;
gameState[row][col] = '';
break;
}
gameState[row][col] = '';
}
}
if (!square) {
square = availableSquares[Math.floor(Math.random() * availableSquares.length)];
}
square.classList.add('js-batsu-checked');
const row = square.id.split('-')[0] - 1;
const col = square.id.split('-')[1] - 1;
gameState[row][col] = 'batsu';
const winner = checkWinner();
if (winner !== '') {
endGame(winner === 'maru' ? '〇の勝ちです!' : '✕の勝ちです!');
drawWinningLine();
return;
}
if (checkDraw()) {
endGame('引き分けです!');
return;
}
currentTurn = 'maru';
updateTurnMessage();
}
function endGame(message) {
gameActive = false;
maruTurnMessage.classList.add('js-hidden');
batsuTurnMessage.classList.add('js-hidden');
if (message.includes('勝ち')) {
if (message.includes('〇')) {
maruWinMessage.classList.remove('js-hidden');
} else {
batsuWinMessage.classList.remove('js-hidden');
}
} else {
drawMessage.classList.remove('js-hidden');
}
resetBtn.classList.remove('js-hidden');
squares.forEach(square => square.classList.add('js-unclickable'));
}
const drawWinningLine = () => {
const container = document.querySelector('.squares-container');
const lineContainer = document.createElement('div');
lineContainer.classList.add('line-container');
container.appendChild(lineContainer);
const [a, b, c] = winningLine;
if (a[0] === 0 && b[0] === 0 && c[0] === 0) {
const line = document.createElement('div');
line.classList.add('line-horizontal');
line.style.top = `calc(33.33% / 2 - 4px)`;
lineContainer.appendChild(line);
} else if (a[0] === 1 && b[0] === 1 && c[0] === 1) {
const line = document.createElement('div');
line.classList.add('line-horizontal');
line.style.top = `calc(33.33% + 33.33% / 2 - 4px)`;
lineContainer.appendChild(line);
} else if (a[0] === 2 && b[0] === 2 && c[0] === 2) {
const line = document.createElement('div');
line.classList.add('line-horizontal');
line.style.top = `calc(66.66% + 33.33% / 2 - 4px)`;
lineContainer.appendChild(line);
} else if (a[1] === 0 && b[1] === 0 && c[1] === 0) {
const line = document.createElement('div');
line.classList.add('line-vertical');
line.style.left = `calc(33.33% / 2 - 4px)`;
lineContainer.appendChild(line);
} else if (a[1] === 1 && b[1] === 1 && c[1] === 1) {
const line = document.createElement('div');
line.classList.add('line-vertical');
line.style.left = `calc(33.33% + 33.33% / 2 - 4px)`;
lineContainer.appendChild(line);
} else if (a[1] === 2 && b[1] === 2 && c[1] === 2) {
const line = document.createElement('div');
line.classList.add('line-vertical');
line.style.left = `calc(66.66% + 33.33% / 2 - 4px)`;
lineContainer.appendChild(line);
} else if (a[0] === 0 && a[1] === 0 && b[0] === 1 && b[1] === 1 && c[0] === 2 && c[1] === 2) {
const line = document.createElement('div');
line.classList.add('line-diagonal');
lineContainer.appendChild(line);
} else if (a[0] === 0 && a[1] === 2 && b[0] === 1 && b[1] === 1 && c[0] === 2 && c[1] === 0) {
const line = document.createElement('div');
line.classList.add('line-diagonal-reverse');
lineContainer.appendChild(line);
}
};
resetBtn.addEventListener('click', () => {
stopFireworks();
gameActive = true;
currentTurn = 'maru';
gameState.forEach(row => row.fill(''));
squares.forEach(square => {
square.textContent = '';
square.classList.remove('js-maru-checked', 'js-batsu-checked', 'js-unclickable');
});
maruTurnMessage.classList.remove('js-hidden');
batsuTurnMessage.classList.add('js-hidden');
maruWinMessage.classList.add('js-hidden');
batsuWinMessage.classList.add('js-hidden');
drawMessage.classList.add('js-hidden');
resetBtn.classList.add('js-hidden');
const lineContainer = document.querySelector('.line-container');
if (lineContainer) {
lineContainer.remove();
}
});
squares.forEach(square => {
square.addEventListener('click', handleSquareClick);
});
updateTurnMessage();
});
CPUの行動パターンを考えるのが難しかったです。
とりあえず「〇が3つ揃いそうなら邪魔してくる」「✕が3つ揃いそうなら勝ちに行く」という感じにしてみました。
fireworksはこちらが勝利した場合のみ発動するようにしました。
枠線を「井」にしてアニメーションさせる
〇✕ゲームっぽく、枠線を漢字の「井」のようにして、さらに1本ずつ「横横縦縦」とアニメーション表示させてみようと思います。
まずCSSに追記します。
.squares-box {
border: solid 2px transparent;
}
.square {
border: solid 2px transparent;
}
.line-horizontal2, .line-vertical2 {
position: absolute;
background-color: #333;
z-index: 1;
}
.line-horizontal2 {
height: 4px;
width: 100%;
left: 0;
transform: scaleX(0);
transform-origin: left;
}
.line-vertical2 {
width: 4px;
height: 100%;
top: 0;
transform: scaleY(0);
transform-origin: top;
}
@keyframes drawLine2 {
to {
transform: scale(1);
}
}
.line-horizontal2 {
animation: drawLine2 0.3s ease forwards;
}
.line-vertical2 {
animation: drawLine2 0.3s ease forwards;
}
.squares-boxと.squareの枠線を透明にしておきます。
スクリプトに追記します。
function draw井Animation() {
const container = document.querySelector('.squares-container');
const existingLines = container.querySelectorAll('.line-horizontal2, .line-vertical2');
existingLines.forEach(line => line.remove());
const line1 = document.createElement('div');
line1.classList.add('line-horizontal2');
line1.style.top = 'calc(33.33% - 2px)';
container.appendChild(line1);
const line2 = document.createElement('div');
line2.classList.add('line-horizontal2');
line2.style.top = 'calc(66.66% - 2px)';
const line3 = document.createElement('div');
line3.classList.add('line-vertical2');
line3.style.left = 'calc(33.33% - 2px)';
const line4 = document.createElement('div');
line4.classList.add('line-vertical2');
line4.style.left = 'calc(66.66% - 2px)';
setTimeout(() => {
container.appendChild(line2);
}, 300);
setTimeout(() => {
container.appendChild(line3);
}, 600);
setTimeout(() => {
container.appendChild(line4);
}, 900);
}
window.addEventListener('load', draw井Animation);
document.querySelector('#reset-btn').addEventListener('click', () => {
const container = document.querySelector('.squares-container');
const existingLines = container.querySelectorAll('.line-horizontal2, .line-vertical2');
existingLines.forEach(line => {
line.remove();
});
setTimeout(() => {
draw井Animation();
}, 300);
});
今後の課題
・難易度を「かんたん」「ふつう」「むずかしい」から選べるようにする
・先攻、後攻を選べるようにする
・「現在〇連勝中」とか「〇勝〇敗」みたいに戦績を表示
気が向いたらやります!
2024年もお世話になりました。
2025年もよろしくお願いします。


