〇✕ゲーム
2024年12月15日
みなさんこんにちは。
ケミストのWeb担当みやのです。
「Web」記事では、これまでに得たWebに関する知識を記録として残していきたいと思います。
今回は「〇✕ゲーム」を作ってみました。ぜひ2人で遊んでみてください!
そんな暇な知り合いはいないって?
では自分の右手vs左手でやってみてください。
つくってみよう
参考:Qiita|[Javascript] マルバツゲームを写経してみた
「写経」とは言い得て妙だと思いました。
早速パクッ写経してみましょう。
まずはHTMLです。
<div class="wrapper">
<div class="game-container">
<div class="message-container">
<ul class="message-list">
<li id="maru-turn" class="js-hidden"><span class="maru">〇</span>の番</li>
<li id="batsu-turn" class="js-hidden"><span class="batsu">✕</span>の番</li>
<li id="maru-win" class="js-hidden"><span class="maru">〇</span>の勝利ッ!</li>
<li id="batsu-win" class="js-hidden"><span class="batsu">✕</span>の勝利ッ!</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>
.wrapperの中に.game-containerがあって、その中にmessage-container、squares-container、btn-containerの3つを配置してありますね。
CSSはちょっとアレンジしてみました。
li {
list-style: none;
}
.wrapper {
max-width: 1024px;
margin: 0 auto;
padding: 0 10px;
text-align: center;
}
.game-container {
padding: 60px 0;
}
.message-container {
margin-bottom: 20px;
font-size: 2rem;
font-weight: 700;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
}
.message-list {
list-style: none;
padding: 0;
margin: 0;
}
.batsu {
color: #dc3545;
font-weight: 700;
}
.maru {
color: #007bff;
font-weight: 700;
}
.js-hidden {
display: none;
}
.squares-container {
margin: 0 auto;
width: 100%;
}
.squares-box {
width: 100%;
aspect-ratio: 1 / 1;
display: flex;
flex-wrap: wrap;
border: solid 2px #333;
}
.square {
position: relative;
width: 33.33%;
aspect-ratio: 1 / 1;
border: solid 2px #333;
}
.js-maru-checked::before {
content: '';
width: 60%;
height: 60%;
border: solid 2.4vw #007bff;
border-radius: 50%;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.js-batsu-checked::before {
content: '';
width: 60%;
height: 10%;
background-color: #dc3545;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) rotate(45deg);
}
.js-batsu-checked::after {
content: '';
width: 10%;
height: 60%;
background-color: #dc3545;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) rotate(45deg);
}
.btn-container {
padding-top: 40px;
}
.btn-reset {
font-weight: bold;
}
.btn-reset:hover {
transition-duration: 0.4s;
}
.js-unclickable {
pointer-events: none;
}
.js-highLight {
background-color: #fff2cc;
}
@media (min-width: 768px) {
.js-maru-checked::before {
border: solid 1.6vw #007bff;
}
}
@media (orientation: landscape) and (max-width: 768px) {
.squares-container {
width: 50%;
}
.js-maru-checked::before {
border: solid 1.6vw #007bff;
}
}
3×3のマス目は幅100%、スマホ横向き時だけ幅50%にしました。
〇の太さをいい感じにしたかったのですがよい方法がわかりませんでした。スマホ縦向き時は2.4vw、スマホ横向き時とパソコン時は1.6vwにしてみました。
スクリプトは以下の通りです。
let flag = true;
let counter = 9;
let winningLine = null;
const squares = document.querySelectorAll('.square');
const squaresArray = Array.from(squares);
const messages = document.querySelectorAll('.message-list li');
const messagesArray = Array.from(messages);
const resetBtn = document.querySelector('#reset-btn');
const setMessage = (id) => {
messagesArray.forEach((message) => {
if (message.id === id) {
message.classList.remove('js-hidden');
} else {
message.classList.add('js-hidden');
}
});
};
const filterById = (targetArray, idArray) => {
return targetArray.filter((e) => {
return (e.id === idArray[0] || e.id === idArray[1] || e.id === idArray[2]);
});
};
const line1 = filterById(squaresArray, ['1-1', '1-2', '1-3']);
const line2 = filterById(squaresArray, ['2-1', '2-2', '2-3']);
const line3 = filterById(squaresArray, ['3-1', '3-2', '3-3']);
const line4 = filterById(squaresArray, ['1-1', '2-1', '3-1']);
const line5 = filterById(squaresArray, ['1-2', '2-2', '3-2']);
const line6 = filterById(squaresArray, ['1-3', '2-3', '3-3']);
const line7 = filterById(squaresArray, ['1-1', '2-2', '3-3']);
const line8 = filterById(squaresArray, ['1-3', '2-2', '3-1']);
const lineArray = [line1, line2, line3, line4, line5, line6, line7, line8];
const checkWinner = (symbol) => {
const result = lineArray.some((line) => {
const subResult = line.every((square) => {
return symbol === 'maru'
? square.classList.contains('js-maru-checked')
: square.classList.contains('js-batsu-checked');
});
if (subResult) {
winningLine = line;
}
return subResult;
});
return result;
};
const gameOver = () => {
squaresArray.forEach((square) => {
square.classList.add('js-unclickable');
});
if (winningLine) {
winningLine.forEach((square) => {
square.classList.add('js-highLight');
});
drawWinningLine();
}
resetBtn.classList.remove('js-hidden');
};
const initGame = () => {
flag = true;
counter = 9;
winningLine = null;
squaresArray.forEach((square) => {
square.classList.remove('js-maru-checked');
square.classList.remove('js-batsu-checked');
square.classList.remove('js-unclickable');
square.classList.remove('js-highLight');
});
const lineContainer = document.querySelector('.line-container');
if (lineContainer) {
lineContainer.remove();
}
setMessage('maru-turn');
resetBtn.classList.add('js-hidden');
};
resetBtn.addEventListener('click', function () {
initGame();
stopFireworks();
});
squaresArray.forEach((square) => {
square.addEventListener('click', () => {
if (flag) {
square.classList.add('js-maru-checked');
square.classList.add('js-unclickable');
if (checkWinner('maru')) {
setMessage('maru-win');
gameOver();
return;
}
setMessage('batsu-turn');
flag = false;
} else {
square.classList.add('js-batsu-checked');
square.classList.add('js-unclickable');
if (checkWinner('batsu')) {
setMessage('batsu-win');
gameOver();
return;
}
setMessage('maru-turn');
flag = true;
}
counter--;
if (counter === 0) {
setMessage('draw');
gameOver();
}
});
});
む、難しい...
とりあえず、〇を先攻に変えてみました。✕が先攻なのが一般的なんでしょうかね?
勝敗がついた時に線を引く
〇または✕が3つ揃った時に、ビーっと線が引かれるようにしてみたいと思います。
CSSに追記します。
.squares-container {
position: relative;
}
.line-container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
.line-horizontal {
position: absolute;
width: calc(100% + 8px);
height: 8px;
background: red;
top: calc(50% - 4px);
left: -4px;
transform-origin: left;
animation: draw-horizontal 0.5s ease-out;
}
.line-vertical {
position: absolute;
width: 8px;
height: calc(100% + 8px);
background: red;
left: calc(50% - 4px);
top: -4px;
transform-origin: top;
animation: draw-vertical 0.5s ease-out;
}
.line-diagonal {
position: absolute;
width: 141.4213562373095%;
height: 8px;
background: red;
top: -2.82842712474619px;
left: 2.82842712474619px;
transform: rotate(45deg);
transform-origin: top left;
animation: draw-diagonal 0.5s ease-out;
}
.line-diagonal-reverse {
position: absolute;
width: 141.4213562373095%;
height: 8px;
background: red;
top: -2.82842712474619px;
right: 2.82842712474619px;
transform: rotate(-45deg);
transform-origin: top right;
animation: draw-diagonal-reverse 0.5s ease-out;
}
@keyframes draw-horizontal {
from {
width: 0;
}
to {
width: calc(100% + 8px);
}
}
@keyframes draw-vertical {
from {
height: 0;
}
to {
height: calc(100% + 8px);
}
}
@keyframes draw-diagonal {
from {
width: 0;
}
to {
width: 141.4213562373095%;
}
}
@keyframes draw-diagonal-reverse {
from {
width: 0;
}
to {
width: 141.4213562373095%;
}
}
スクリプトに追記します。
const drawWinningLine = () => {
const container = document.querySelector('.squares-container');
const lineContainer = document.createElement('div');
lineContainer.classList.add('line-container');
container.appendChild(lineContainer);
if (winningLine[0].id === '1-1' && winningLine[1].id === '2-2' && winningLine[2].id === '3-3') {
const line = document.createElement('div');
line.classList.add('line-diagonal');
lineContainer.appendChild(line);
fireworks();
} else if (winningLine[0].id === '1-3' && winningLine[1].id === '2-2' && winningLine[2].id === '3-1') {
const line = document.createElement('div');
line.classList.add('line-diagonal-reverse');
lineContainer.appendChild(line);
fireworks();
} else if (winningLine[0].id === '1-1' && winningLine[1].id === '1-2' && winningLine[2].id === '1-3') {
const line = document.createElement('div');
line.classList.add('line-horizontal');
line.style.top = 'calc(16.6% - 4px)';
lineContainer.appendChild(line);
fireworks();
} else if (winningLine[0].id === '2-1' && winningLine[1].id === '2-2' && winningLine[2].id === '2-3') {
const line = document.createElement('div');
line.classList.add('line-horizontal');
line.style.top = 'calc(50% - 4px)';
lineContainer.appendChild(line);
fireworks();
} else if (winningLine[0].id === '3-1' && winningLine[1].id === '3-2' && winningLine[2].id === '3-3') {
const line = document.createElement('div');
line.classList.add('line-horizontal');
line.style.top = 'calc(83.3% - 4px)';
lineContainer.appendChild(line);
fireworks();
} else if (winningLine[0].id === '1-1' && winningLine[1].id === '2-1' && winningLine[2].id === '3-1') {
const line = document.createElement('div');
line.classList.add('line-vertical');
line.style.left = 'calc(16.66666666666667% - 3px)';
lineContainer.appendChild(line);
fireworks();
} else if (winningLine[0].id === '1-2' && winningLine[1].id === '2-2' && winningLine[2].id === '3-2') {
const line = document.createElement('div');
line.classList.add('line-vertical');
line.style.left = 'calc(50% - 4px)';
lineContainer.appendChild(line);
fireworks();
} else if (winningLine[0].id === '1-3' && winningLine[1].id === '2-3' && winningLine[2].id === '3-3') {
const line = document.createElement('div');
line.classList.add('line-vertical');
line.style.left = 'calc(83.33333333333333% - 5px)';
lineContainer.appendChild(line);
fireworks();
}
};
線の位置の調整に苦労しました。なかなか上手いこと真ん中を貫いてくれなかったです。
例によってconfettiのfireworksを仕込んであります。
今後やりたいこと
・CPU戦
・指で〇とか✕を描いたら認識する機能それもう紙とペンでええやん
・四目並べ、五目並べ
・オセロ
いつか挑戦してみたいです!


