JavaScriptでオセロを作る(完成版)
タグ:
JavaScript
オセロ
オセロを作る過程を記事にしようと思って書いてたら思いの外難解だったので。とりあえず先に完成版を載せます。 (検索上位にくる〇〇アカデミーの記事とかよりはいい感じに書けた気がする。)
盤面を作りクリックすると間に挟まれたものが裏返るアルゴリズムを書くまでで2時間。
修正を繰り返しながら、クリック可能箇所を管理して置く石がないときのパスの処理を書いて仕上げるまでに2時間かかりました。
(クリック可能箇所の管理でバグの沼にハマった)
ポイントとしては、
- 各マスにidを振ったこと。
- クリックしたらまず状態を管理する配列だけを変更するようにしたこと。
- その後に毎回配列を参照して全体を描き直す処理にしたこと。
- 各要素の関数はなるべく小さくし、一つの関数には一つのことだけをさせること。長くなった関数は分割すること。
- なるべく再帰処理を使いコードの長さを短くしたこと。
特にこの4は
html
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <style> #board { display: grid; grid-template-columns: repeat(8,1fr); width: 800px; } #board div { height: 100px; width: 100px; border: 1px solid black; background-color: green; } #board .black{ color: black; font-size: 60px; text-align: center; line-height: 100px; } #board .white{ color: white; font-size: 60px; text-align: center; line-height: 100px; } #board .clickable{ background-color: aquamarine; opacity: 0.5; } </style> </head> <body> <div id="board"></div> </body> <script src="./othello.js"></script> </html>
javascript
let count = 0; // 黒石を置く const setBlack = (i, j) => { const element = document.getElementById(`${i}${j}`); element.setAttribute("class", "black"); element.textContent = "●"; } // 白石を置く const setWhite = (i, j) => { const element = document.getElementById(`${i}${j}`); element.setAttribute("class", "white"); element.textContent = "●"; } // 石を置くことが可能なマスであることを表す const setClickable = (i, j) => { const element = document.getElementById(`${i}${j}`); element.setAttribute("class", "clickable"); } // ターン交代時に、前ターンのsetClickableを削除する const removeClickable = (i, j) => { const element = document.getElementById(`${i}${j}`); element.removeAttribute("class"); } // クリックできることをbooleanで表す(クリック可能場所以外を触らせない) const hasClickable = (i, j) => { const element = document.getElementById(`${i}${j}`); return element.getAttribute("class") === "clickable"; } // マスの配置 const setSquare = (i, j) => { const square = document.createElement("div"); // 各マスのid要素に行と列の番号を振る square.setAttribute("id", `${i}${j}`) square.addEventListener("click", () => { if (hasClickable(i, j)) { if (count % 2 === 0) { squareArrays[i][j] = 1; } else { squareArrays[i][j] = -1; } count++; } }) document.querySelector("#board").appendChild(square) } // 1~8の盤と角を表すために0行・列と9行・列を作る(アルゴリズムの番兵) // 10*10の二次元配列を初期化する const squareArrays = Array.from(new Array(10), () => new Array(10).fill(0)) for (let i = 1; i < 9; i++) { for (let j = 1; j < 9; j++) { setSquare(i, j); } } // 盤面の初期化 squareArrays[4][4] = 1 squareArrays[5][5] = 1 squareArrays[4][5] = -1 squareArrays[5][4] = -1 setBlack(4, 4); setBlack(5, 5); setWhite(4, 5); setWhite(5, 4); // squareArraysから盤面を再現する const makeBoard = () => { for (let i = 1; i < 9; i++) { for (let j = 1; j < 9; j++) { // clickableクラスを毎回削除する removeClickable(i, j); switch (squareArrays[i][j]) { case 0: break; case 1: setBlack(i, j); break; case -1: setWhite(i, j); break; default: console.log("squareArraysに想定外の数値が入っている"); } } } } const isSetArrays = Array.from(new Array(10), () => new Array(10).fill(0)) // 挟まったマスを裏返す処理 const reverse = (x, y, dx, dy, countSquare, square) => { squareArrays[x][y] = square; // 間に挟んだマスの数だけ再帰処理 if (countSquare > 0) { countSquare--; reverse(x - dx, y - dy, dx, dy, countSquare, square); } } // checkAll関数をクリック可能場所を示すときと実際にクリックした時で使い回すための変数 let clicked = false const checkSquare = (x, y, dx, dy) => { const thisSquare = squareArrays[x][y] const thisX = x; const thisY = y; let countSquare = 0; const checkNext = (x, y, dx, dy) => { // 最初は隣のマス const nextSquare = squareArrays[x + dx][y + dy] // クリックしたマスとnextSquare(最初は隣のマス)を比較 switch (nextSquare) { // nextSquareが空欄だった場合 case 0: break; // nextSquareが同じ色だった場合 case thisSquare: // 間にマスが一つ以上ある場合 if (countSquare > 0) { // 裏返しの処理 // 実施にクリックしたとき if (clicked === true) { reverse(x, y, dx, dy, countSquare, thisSquare); } else { setClickable(thisX, thisY); } }; break; // nextSquareが異なる色だった場合 case -(thisSquare): // クリックしたマスと if (nextSquare !== thisSquare) { // 間に入るマスを数える countSquare++; // 再帰呼び出し nextSquareが一つズレる checkNext(x + dx, y + dy, dx, dy); } break; default: console.log("nextSquareに想定外の数値が入っている"); } } checkNext(x, y, dx, dy); } // 全ての方向でチェックする const checkAll = (x, y) => { checkSquare(x, y, 1, 0) checkSquare(x, y, -1, 0) checkSquare(x, y, 0, 1) checkSquare(x, y, 0, -1) checkSquare(x, y, 1, 1) checkSquare(x, y, 1, -1) checkSquare(x, y, -1, -1) checkSquare(x, y, -1, 1) } // 指定したマスに石が置けるかをチェック const checkClickable = (x, y) => { // checkAll()を流用するために、空白のマスにそのターンの色の石を仮に置く if (squareArrays[x][y] === 0) { clicked = false squareArrays[x][y] = count % 2 === 0 ? 1 : -1 checkAll(x, y) // 仮に置いた石を取り除く squareArrays[x][y] = 0 } } // 全てのマスで石が置けるかをチェックする const checkClickableAll = () => { for (let i = 1; i < 9; i++) { for (let j = 1; j < 9; j++) { checkClickable(i, j) } } } // 開始ターンのチェック checkClickableAll() const check = document.getElementById("board").addEventListener("click", (e) => { const x = parseInt(e.target.getAttribute("id")[0]); const y = parseInt(e.target.getAttribute("id")[1]); const blackNum = document.body.querySelectorAll(".black").length; const whiteNum = document.body.querySelectorAll(".white").length; if (hasClickable(x, y)) { // 実際にクリックした時のチェック clicked = true checkAll(x, y); makeBoard(x, y); console.log(`黒:${blackNum},白:${whiteNum}`) // 毎ターンの置けるかのチェック checkClickableAll() } const clickableNum = document.body.querySelectorAll(".clickable").length; console.log(`置けるマス:${clickableNum}個`) if (clickableNum === 0) { alert("置けるマスがありません。") count++; // パスした時のチェック checkClickableAll() // 手番が変わっても置けるマスがない場合 if (clickableNum === 0) { if (blackNum > whiteNum) { alert("黒の勝ち!") } else if (blackNum < whiteNum) { alert("白の勝ち!") } else { alert("引き分け!") } } } })