完成品:Reversi!
ソース:source:Reversi!
この「Reversi!」は、フラッシュやWebGLなどの外部技術を使わず、標準的なHTML5とJavaScriptで構成されています。描画はHTML5のCanvasを使ってます。
Contents
canvasのレスポンシブ
canvasの描画は、canvas内の座標(px)をもとに要素の配置を行うため、cssのメディアクエリーやmax-widthなどを使ってサイズを変えたところで、中身がオーバーフローしてしまいます。
そこで考えたのが、「cssのtransform:scaleを使って拡大縮小する」「js側でブラウザのウィンドウサイズを取得し、それを基準に係数を求め、要素の座標すべてにその係数をかける」の二つです。
僕は後者で実装しました。いちいち面倒ですが、先にそっちで成功したので。
let ua = navigator.userAgent; // UA取得
// ユーザーエージェントで分岐
if(ua.indexOf("iPhone") > 0 || ua.indexOf("iPod") > 0 ||
ua.indexOf('Android') > 0 && ua.indexOf('Mobile') > 0){
// スマホの場合
// 横幅をウィンドウの幅に
cvs.width = window.innerWidth;
// 縦幅はウィンドウの幅の1.4倍に
cvs.height = window.innerWidth * 1.4;
// 係数
c = window.innerWidth / 400;
} else {
let body = document.body;
// タブレット、PCの場合
// 画面中央に揃えるスタイルを追加
body.style.top = "50%";
body.style.left = "50%";
body.style.transform = "translate(-50%,-50%)";
// 横400px縦560px固定
cvs.width = 400;
cvs.height = 560;
// 係数は1(標準)
c = 1;
}
ctx要素を追加するときは、サイズや座標に係数をかけます。
ctx.font = 36*c + "px Comfortaa";
ctx.fillStyle = "rgba(255, 255, 255, 1)";
ctx.fillText("PLAY!", 150*c, 418*c);
オセロのアルゴリズム
オセロのアルゴリズムといっても色々パターンがありそうですが、あえて自分で考えてみました。
周囲8マスに相手の駒があり、その延長線上に自分の駒があったらひっくり返す。このルールを意識し、総当たり方式で実装しました。
抜粋なので分かりにくいかも。(0<=x<=7)
// main.js抜粋
let map = [ // 9:壁,1:自分,2:相手
[9,9,9,9,9,9,9,9,9,9],
[9,0,0,0,0,0,0,0,0,9],
[9,0,0,0,0,0,0,0,0,9],
[9,0,0,0,0,0,0,0,0,9],
[9,0,0,0,2,1,0,0,0,9],
[9,0,0,0,1,2,0,0,0,9],
[9,0,0,0,0,0,0,0,0,9],
[9,0,0,0,0,0,0,0,0,9],
[9,0,0,0,0,0,0,0,0,9],
[9,9,9,9,9,9,9,9,9,9]
];
// 周囲8マスを調べる
for (let vx = -1; vx <= 1; vx++) {
for (let vy = -1; vy <= 1; vy++) {
// 周囲のマスに相手のコマがあるか
if (x + vy + 1][x + vx + 1] == 2) {
// その先
var mx = vx * 2;
var my = vy * 2;
// そもそもおける場所か
while (1) {
if (map[x + my + 1][x + mx + 1] == 2) {
mx += vx;
my += vy;
continue;
}
// その延長線上に自コマがあるか
if (map[y + my + 1][x + mx + 1] == 1) {
var rx = vx;
var ry = vy;
// 裏返す
while (1) {
if (map[y + ry + 1][x + rx + 1] == 2) {
map[y + ry + 1][x + rx + 1] = 1;
rx += vx;
ry += vy;
continue;
}
// そのマスも裏返す
map[y + 1][y + 1] = 1;
// コマ数計算
pieceCount();
turn = "cpu";
break;
}
}
break;
}
}
}
}
CPUの打ち手
静的な評価マップを用意しておいて、都度参照し使いました。雑な評価方法だと思ったのですが、そこそこの強さを発揮してくれました。
const eval_map_1 = [ // 評価マップ1
[1,1,1,1,1,1,1,1],
[1,2,2,2,2,2,2,1],
[1,2,3,3,3,3,2,1],
[1,2,3,0,0,3,2,1],
[1,2,3,0,0,3,2,1],
[1,2,3,3,3,3,2,1],
[1,2,2,2,2,2,2,1],
[1,1,1,1,1,1,1,1]
];
const eval_map_2 = [ // 評価マップ2
[8,2,7,5,5,7,2,8],
[2,1,3,3,3,3,1,2],
[7,3,6,4,4,6,3,7],
[5,3,4,0,0,4,3,5],
[5,3,4,0,0,4,3,5],
[7,3,6,4,4,6,3,7],
[2,1,3,3,3,3,1,2],
[8,2,7,5,5,7,2,8]
];
この二つの評価を使っています。
序盤:中央を優先的に、中盤:場所取りを優先、終盤:場所ととれるコマ数をもとに。
// main.js抜粋
// 可動域マップ初期化
mov_scope = [
[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,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,0,0],
[0,0,0,0,0,0,0,0]
];
// 全マス総当たり
for(let y = 1; y <= 8; y++) {
for(let x = 1; x <= 8; x++) {
if(map[y][x] == 0) {
// 周囲8マスを調べる
for (let vx = -1; vx <= 1; vx++) {
for (let vy = -1; vy <= 1; vy++) {
// 隣にplayerコマがあるか
if (map[y + vy][x + vx] == 1) {
var rev_cnt = 1; // 裏返しカウント
var mx = vx * 2;
var my = vy * 2;
// 進む
while (1) {
if (map[y + my][x + mx] == 1) {
mx += vx;
my += vy;
rev_cnt++;
continue;
}
// さらにその先にCUPコマがあれば
// 置けることが確定
if (map[y + my][x + mx] == 2) {
if(piece.count.white + piece.count.black < 20) {
mov_scope[y - 1][x - 1] = eval_map_1[y - 1][x - 1];
} else if(piece.count.white + piece.count.black < 50) {
mov_scope[y - 1][x - 1] = eval_map_2[y - 1][x - 1];
} else {
mov_scope[y - 1][x - 1] = eval_map_2[y - 1][x - 1] + rev_cnt;
}
}
break;
}
}
}
}
}
}
}
// 評価が高い順に置ける場所を厳選
let maxFlg = false;
let maxArr = [];
for(let m = 14; m >= 2; m--) {
for(let i = 0; i <= 7; i++) {
for(let j = 0; j <= 7; j++) {
if(mov_scope[j][i] == m) {
maxArr.push([i,j]);
maxFlg = true;
}
}
}
// その評価で置けるところがあったらbreak
if(maxFlg) {
break;
}
}
// どこも置けなかったらターンチェンジ
if(maxArr.length == 0) {
turn = "player";
return;
}
// 裏返す
let rdm = Math.floor(Math.random() * maxArr.length); // maxArrからランダムで一か所取得
// 周囲8コマ(+1はmapと添え字の差分)
for (let vx = -1; vx <= 1; vx++) {
for (let vy = -1; vy <= 1; vy++) {
if (map[maxArr[rdm][1] + 1 + vy][maxArr[rdm][0] + 1 + vx] == 1) {
var mx = vx * 2;
var my = vy * 2;
while (1) {
if (map[maxArr[rdm][1] + 1 + my][maxArr[rdm][0] + 1 + mx] == 1) {
mx += vx;
my += vy;
continue;
}
if (map[maxArr[rdm][1] + 1 + my][maxArr[rdm][0] + 1 + mx] == 2) {
var rx = vx;
var ry = vy;
while (1) {
if (map[maxArr[rdm][1] + 1 + ry][maxArr[rdm][0] + 1 + rx] == 1) {
map[maxArr[rdm][1] + 1 + ry][maxArr[rdm][0] + 1 + rx] = 2;
rx += vx;
ry += vy;
continue;
}
// 置いた場所にCPUコマ
map[maxArr[rdm][1] + 1][maxArr[rdm][0] + 1] = 2;
// コマカウント
pieceCount();
turn = "player";
break
}
}
break;
}
}
}
}
まとめ
パフォーマンス的に改善できる部分が多々ありそうですが、とりあえずこれで完成。
WebSocketでオンライン対戦とか、機械学習でモデルを作ってとか、やれたらいいですねぇ…。
The present writer kazz.