Skip to content

Instantly share code, notes, and snippets.

@xhackjp1
Last active December 27, 2020 12:56
Show Gist options
  • Save xhackjp1/951904ec9d6cdf256637f3c213f26b9e to your computer and use it in GitHub Desktop.
Save xhackjp1/951904ec9d6cdf256637f3c213f26b9e to your computer and use it in GitHub Desktop.
簡易ゲームAIを実装
<!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>
<!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>
@xhackjp1
Copy link
Author

@t--takai

情報共有ありがとうございます!

Gistのファイルをブラウザで動作確認できるようになります。

これは知らなかったです、便利ですね!今度使ってみます!

@omega-takai
Copy link

😄 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment