Last active
December 27, 2020 12:56
-
-
Save xhackjp1/951904ec9d6cdf256637f3c213f26b9e to your computer and use it in GitHub Desktop.
簡易ゲームAIを実装
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Document</title> | |
<style> | |
body { | |
margin: auto; | |
margin-top: 16px; | |
max-width: 640px; | |
text-align: center; | |
background-color: black; | |
} | |
#main { | |
margin-top: 16px; | |
text-align: center; | |
} | |
#main div { | |
color: white; | |
} | |
</style> | |
</head> | |
<body> | |
<canvas id="reversi" width="320" height="320"></canvas> | |
<div id="main"> | |
<div><button id="button">石を変更</button></div> | |
<div id="player"></div> | |
<hr> | |
<div id="score"></div> | |
<div id="logArea"></div> | |
</div> | |
<script> | |
const canvas = document.getElementById("reversi") | |
const button = document.getElementById("button") | |
const playerName = document.getElementById("player") | |
const logArea = document.getElementById("logArea") | |
const score = document.getElementById("score") | |
const ctx = canvas.getContext("2d") | |
const fieldSize = 8 | |
const movesDirection = [ | |
{ x: 0, y: 1 }, // 上 | |
{ x: 0, y: -1 }, // 下 | |
{ x: 1, y: 0 }, // 右 | |
{ x: 1, y: 1 }, // 右下 | |
{ x: 1, y: -1 }, // 右上 | |
{ x: -1, y: 0 }, // 左 | |
{ x: -1, y: 1 }, // 左下 | |
{ x: -1, y: -1 } // 左上 | |
] | |
const StoneColor = { none: 0, white: 1, black: 2 } | |
const Player1 = { name: "プレーヤー白", stone: StoneColor.white, reversi: StoneColor.black } | |
const Player2 = { name: "プレーヤー黒", stone: StoneColor.black, reversi: StoneColor.white } | |
var playBoard = { | |
player: Player1, | |
squareSize: 40, // マスの大きさ | |
stoneSize: 16, // 石の大きさ | |
aiPutArea: { x: -1, y: -1}, | |
map: [ | |
[0, 0, 0, 0, 0, 0, 0, 0], | |
[0, 0, 0, 0, 0, 0, 0, 0], | |
[0, 0, 0, 0, 0, 0, 0, 0], | |
[0, 0, 0, 1, 2, 0, 0, 0], | |
[0, 0, 0, 2, 1, 0, 0, 0], | |
[0, 0, 0, 0, 0, 0, 0, 0], | |
[0, 0, 0, 0, 0, 0, 0, 0], | |
[0, 0, 0, 0, 0, 0, 0, 0] | |
], | |
preAreas: [], // 置ける位置を保持しておく配列 | |
gameOver: function(){ | |
// ここがごちゃごちゃしてしまったので修正 | |
// ゲーム終了判定が微妙 | |
var list = []; | |
for(var i=0;i<this.map.length;i=i+1){ | |
for(var j=0;j<this.map[i].length;j=j+1){ | |
list.push(this.map[i][j]); | |
} | |
} | |
let finish = list.filter((item) => item != 0).length === 64 | |
let white = list.filter((item) => item === 1).length | |
let black = list.filter((item) => item === 2).length | |
if (finish) { | |
score.innerText = `白: ${white} 黒: ${black}` | |
logArea.innerText = `ゲーム終了 白: ${white} 黒: ${black}` | |
this.drawMap() | |
return true | |
} | |
score.innerText = `白: ${white} 黒: ${black}` | |
return false | |
}, | |
initializeBoard: function () { | |
// マス目を引く処理 | |
ctx.fillStyle = "green" | |
ctx.fillRect(0, 0, this.squareSize * fieldSize, this.squareSize * fieldSize) | |
let ss = this.squareSize | |
for (let i = 0; i < fieldSize; i++) { | |
// 縦に線を引く | |
ctx.beginPath() | |
ctx.moveTo(ss * i, 0) | |
ctx.lineTo(ss * i, ss * fieldSize) | |
// 横に線を引く | |
ctx.moveTo(0, ss * i) | |
ctx.lineTo(ss * fieldSize, ss * i) | |
ctx.stroke() | |
} | |
playerName.innerHTML = this.player.name + "の順番です" | |
this.preAreas = [] | |
this.drawMap() | |
}, | |
drawMap: function() { | |
// 配列を元にして、盤の石を描画する | |
for (let posY = 0; posY < fieldSize; posY++) { | |
for (let posX = 0; posX < fieldSize; posX++) { | |
this.drawStone(posX, posY, this.map[posY][posX]) | |
this.drawlightPlase(posX, posY) | |
} | |
} | |
}, | |
update: function () { | |
if(this.preAreas.length == 0) { | |
logArea.innerText = "置ける場所がありません" | |
this.nextPlayer() | |
return | |
} | |
// ゲームAI AIオブジェクトに任せるべき | |
if(this.player === Player2){ | |
canvas.onclick = null // クリックを向こうにする | |
this.aiSimulation() // AIの描画 | |
}else{ | |
canvas.onclick = onClickPutStone | |
} | |
}, | |
aiSimulation: function(){ | |
// 考え中の表示 | |
let text = "" | |
let intId = setInterval(() => { | |
text = text + "." | |
let pos = Math.floor(Math.random() * this.preAreas.length) | |
logArea.innerText = `AI 考え中 ${text} \n x: ${this.preAreas[pos].x} y: ${this.preAreas[pos].y}` | |
}, 250) | |
// AIが石を配置する(ランダムで置く位置を選ぶ) | |
setTimeout(() => { | |
clearInterval(intId) | |
let pos = Math.floor(Math.random() * this.preAreas.length) | |
let pre = this.preAreas[pos] | |
this.aiPutArea = {x: pre.x, y: pre.y} | |
logArea.innerText = `AI : x: ${pre.x} y: ${pre.y} に黒石を置きました` | |
playBoard.drawStone(pre.x, pre.y, playBoard.player.stone) | |
playBoard.putStone(pre.x, pre.y, playBoard.player.stone) | |
}, 2500) | |
}, | |
// プレイヤーを交代する | |
nextPlayer: function () { | |
this.player = this.player === Player1 ? Player2 : Player1 // 交代 | |
if(!this.gameOver()){ | |
// ゲームを続ける | |
this.initializeBoard() | |
this.update() | |
} | |
}, | |
// 座標を計算する | |
calcPosition: function (positon) { | |
return this.squareSize * positon + (this.squareSize / 2) | |
}, | |
// 石を描画する | |
drawStone: function (x, y, color) { | |
if (color === 0) return; // 何もしない | |
if(this.aiPutArea.x === x && this.aiPutArea.y === y){ | |
this.drawAiPutArea() | |
} | |
const colors = ["none", "white", "black"] | |
ctx.fillStyle = colors[color]; | |
ctx.beginPath(); | |
ctx.arc(this.calcPosition(x), this.calcPosition(y), this.stoneSize, 0, 2*Math.PI); | |
ctx.fill(); | |
}, | |
// 手番のプレーヤーの石が置ける箇所を目立つように明るい緑で描画する | |
drawlightPlase: function (x, y) { | |
if (!this.canPutStone(x, y, this.player.stone, "simulation")) return | |
// 位置を覚えておく | |
this.preAreas.push({x, y}) | |
// マスより少し小さくする | |
ctx.fillStyle = "#44AA44"; | |
ctx.fillRect(x * this.squareSize + 1, y * this.squareSize + 1, this.squareSize - 2, this.squareSize - 2) | |
}, | |
// 手番のプレーヤーの石が置ける箇所を目立つように明るい緑で描画する | |
drawAiPutArea: function () { | |
ctx.fillStyle = "#3a613b"; | |
ctx.fillRect(this.aiPutArea.x * this.squareSize + 1, this.aiPutArea.y * this.squareSize + 1, this.squareSize - 2, this.squareSize - 2) | |
this.aiPutArea = {x: -1, y: -1} | |
}, | |
// x: x座標, y: y座標, color: "white" or "black" | |
putStone: function (x, y, color) { | |
if (!this.canPutStone(x, y, color)) return | |
this.map[y][x] = color // 0:none, 1:white 2:black とする | |
this.nextPlayer() // 手番を交代する | |
}, | |
reverse: function(changeStones, mode){ | |
if (mode == "simulation") return | |
for (let i = 0; i < changeStones.length; i++) { | |
let cs = changeStones[i] | |
this.map[cs.y][cs.x] = this.player.stone | |
} | |
}, | |
// 石を置けるか調べる関数 mode: simulation の場合は実際にひっくり返す処理はスキップする | |
canPutStone: function (posX, posY, color, mode) { | |
if (this.map[posY][posX] != StoneColor.none) return false // 置けない | |
let flg = false // ひっくり返せるかのフラグ | |
for (let i = 0; i < movesDirection.length; i++) { | |
let changeStones = [] // ひっくり返す石を保持しておく | |
for (let loopNo=0,x=posX,y=posY; x<=7&&x>=0&&y<=7&&y>=0; loopNo++) { | |
x += movesDirection[i].x | |
y += movesDirection[i].y | |
if (x > 7 || y > 7 || x < 0 || y < 0) break // 配列外なので無視する | |
// 1回目のループかどうかで処理が分岐する | |
if (loopNo === 0) { | |
if (this.map[y][x] != this.player.reversi) break // 相手の石でない場合は無視する | |
changeStones.push({ x: x, y: y }) | |
continue | |
} else { | |
if (this.map[y][x] === StoneColor.none) break // 何もない場合はそこで探索をやめる | |
if (this.map[y][x] === this.player.reversi) { | |
changeStones.push({ x: x, y: y }) // 相手の石の場合はひっくり返す配列に格納して次のマスを調べる | |
continue | |
} | |
if (this.map[y][x] === this.player.stone) { | |
this.reverse(changeStones, mode) | |
flg = true | |
} | |
} | |
} | |
} | |
return flg // 石を置けるかを返す | |
} | |
} | |
// クリックした座標に石を置く | |
function onClickPutStone(event) { | |
let rect = event.target.getBoundingClientRect(); | |
let x = event.clientX - rect.left; | |
let y = event.clientY - rect.top; | |
x = x - x % 40 + 20 // キリが良い箇所に配置されるようにx座標を補正 | |
y = y - y % 40 + 20 // キリが良い箇所に配置されるようにy座標を補正 | |
let posX = (x - 20) / 40 // 0-7の整数に修正する | |
let posY = (y - 20) / 40 // 0-7の整数に修正する | |
playBoard.putStone(posX, posY, playBoard.player.stone); | |
} | |
// クリックイベントを登録する | |
canvas.onclick = onClickPutStone | |
button.onclick = () => playBoard.nextPlayer() | |
// ゲーム開始 | |
playBoard.initializeBoard() | |
playBoard.update() | |
</script> | |
</body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Document</title> | |
<style> | |
body { | |
text-align: center; | |
background-color: black; | |
} | |
#main { | |
text-align: center; | |
} | |
#main div { | |
color: white; | |
} | |
</style> | |
</head> | |
<body> | |
<canvas id="reversi" width="320" height="320"></canvas> | |
<div id="main"> | |
<div><button id="button">石を変更</button></div> | |
<div id="player"></div> | |
</div> | |
<script> | |
const playerName = document.getElementById("player") | |
const canvas = document.getElementById("reversi") | |
const button = document.getElementById("button") | |
const ctx = canvas.getContext("2d") | |
const fieldSize = 8 | |
const movesDirection = [ | |
{ x: 0, y: 1 }, // 上 | |
{ x: 0, y: -1 }, // 下 | |
{ x: 1, y: 0 }, // 右 | |
{ x: 1, y: 1 }, // 右下 | |
{ x: 1, y: -1 }, // 右上 | |
{ x: -1, y: 0 }, // 左 | |
{ x: -1, y: 1 }, // 左下 | |
{ x: -1, y: -1 } // 左上 | |
] | |
const StoneColor = { none: 0, white: 1, black: 2 } | |
const Player1 = { name: "プレーヤー白", stone: StoneColor.white, reversi: StoneColor.black } | |
const Player2 = { name: "プレーヤー黒", stone: StoneColor.black, reversi: StoneColor.white } | |
var playBoard = { | |
player: Player1, | |
squareSize: 40, // マスの大きさ | |
stoneSize: 16, // 石の大きさ | |
map: [ | |
[0, 0, 0, 0, 0, 0, 0, 0], | |
[0, 0, 0, 0, 0, 0, 0, 0], | |
[0, 0, 0, 0, 0, 0, 0, 0], | |
[0, 0, 0, 1, 2, 0, 0, 0], | |
[0, 0, 0, 2, 1, 0, 0, 0], | |
[0, 0, 0, 0, 0, 0, 0, 0], | |
[0, 0, 0, 0, 0, 0, 0, 0], | |
[0, 0, 0, 0, 0, 0, 0, 0] | |
], | |
initialize: function () { | |
// マス目を引く処理 | |
ctx.fillStyle = "green" | |
ctx.fillRect(0, 0, this.squareSize * fieldSize, this.squareSize * fieldSize) | |
let ss = this.squareSize // 別名をつけている | |
for (let i = 0; i < fieldSize; i++) { | |
// 縦に線を引く | |
ctx.beginPath() | |
ctx.moveTo(ss * i, 0) | |
ctx.lineTo(ss * i, ss * fieldSize) | |
// 横に線を引く | |
ctx.moveTo(0, ss * i) | |
ctx.lineTo(ss * fieldSize, ss * i) | |
ctx.stroke() | |
} | |
playerName.innerHTML = this.player.name + "の順番です" | |
this.update() | |
}, | |
update: function () { | |
// 盤面の配列を元にして、盤の石を全て描画する | |
for (let posY = 0; posY < fieldSize; posY++) { | |
for (let posX = 0; posX < fieldSize; posX++) { | |
this.drawStone(posX, posY, this.map[posY][posX]) | |
this.drawlightPlase(posX, posY) | |
} | |
} | |
}, | |
// プレイヤーを交代する | |
nextPlayer: function () { | |
this.player = this.player === Player1 ? Player2 : Player1 | |
this.initialize() | |
}, | |
// 座標を計算する | |
calcPosition: function (positon) { | |
return this.squareSize * positon + (this.squareSize / 2) | |
}, | |
// 石を描画する | |
drawStone: function (x, y, color) { | |
if (color === 0) return; // 何もしない | |
const colors = ["none", "white", "black"] | |
ctx.fillStyle = colors[color]; | |
ctx.beginPath(); | |
ctx.arc(this.calcPosition(x), this.calcPosition(y), this.stoneSize, 0, 2*Math.PI); | |
ctx.fill(); | |
}, | |
// 手番のプレーヤーの石が置ける箇所を目立つように明るい緑で描画する | |
drawlightPlase: function (x, y) { | |
if (!this.canPutStone(x, y, this.player.stone, "simulation")) return | |
// 罫線と被らないようにずらした上でマスよりも少し小さいサイズにする | |
ctx.fillStyle = "#44AA44"; | |
ctx.fillRect(x * this.squareSize + 1, y * this.squareSize + 1, this.squareSize - 2, this.squareSize - 2) | |
}, | |
// 石を置く関数 x: x座標, y: y座標, color: "white" or "black" | |
putStone: function (x, y, color) { | |
if (!this.canPutStone(x, y, color)) return // 石が置けないのでスキップ | |
this.map[y][x] = color // 0:none, 1:white 2:black とする | |
this.nextPlayer() // 手番を交代する | |
}, | |
reverse: function(changeStones, mode){ | |
if (mode == "simulation") return // シミュレーションの場合は実際には描画はしない | |
changeStones.forEach(cs => { | |
this.map[cs.y][cs.x] = this.player.stone // 座標のデータを書き換える | |
}); | |
}, | |
// 石を置けるか調べる関数 mode: "simulation" の場合は実際にひっくり返す処理はスキップする | |
canPutStone: function (posX, posY, color, mode) { | |
if (this.map[posY][posX] != StoneColor.none) return false // 置けない | |
let flg = false // ひっくり返せるかのフラグ | |
movesDirection.forEach(md => { | |
let changeStones = [] // ひっくり返す石を保持しておく | |
for (let loopNo=0,x=posX,y=posY; x<=7&&x>=0&&y<=7&&y>=0; loopNo++) { | |
x += md.x | |
y += md.y | |
if (x > 7 || y > 7 || x < 0 || y < 0) break // 配列外なので無視する | |
// 1回目のループかどうかで処理が分岐する | |
if (loopNo === 0) { | |
// 1回目 | |
if (this.map[y][x] != this.player.reversi) break // 相手の石でない場合は無視する | |
changeStones.push({ x: x, y: y }) | |
continue | |
} else { | |
// 2回目以降 | |
if (this.map[y][x] === StoneColor.none) break // 何もない場合はそこで探索をやめる | |
if (this.map[y][x] === this.player.reversi) { | |
changeStones.push({ x: x, y: y }) // 相手の石の場合はひっくり返す配列に格納して次のマスを調べる | |
continue | |
} | |
if (this.map[y][x] === this.player.stone) { | |
this.reverse(changeStones, mode) | |
flg = true | |
} | |
} | |
} | |
}); | |
return flg // 石を置けるかを返す | |
} | |
} | |
// クリックした座標に石を置く | |
function onClickPutStone(event) { | |
let rect = event.target.getBoundingClientRect(); | |
let x = event.clientX - rect.left; | |
let y = event.clientY - rect.top; | |
x = x - x % 40 + 20 // キリが良い箇所に配置されるようにx座標を補正 | |
y = y - y % 40 + 20 // キリが良い箇所に配置されるようにy座標を補正 | |
let posX = (x - 20) / 40 // 0-7の整数に修正する | |
let posY = (y - 20) / 40 // 0-7の整数に修正する | |
playBoard.putStone(posX, posY, playBoard.player.stone); | |
} | |
// クリックイベントを登録する | |
canvas.onclick = onClickPutStone | |
button.onclick = () => playBoard.nextPlayer() | |
// ゲーム開始 | |
playBoard.initialize() | |
</script> | |
</body> | |
</html> |
@t--takai
情報共有ありがとうございます!
Gistのファイルをブラウザで動作確認できるようになります。
これは知らなかったです、便利ですね!今度使ってみます!
😄 👍
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Firebaseで配信済でしたね。失礼しました 🙇♂️
https://reversi-hack.firebaseapp.com/