Javascript オセロニア 検証用リバーシ

![image](https://qiita-image-store.s3.amazonaws.com/0/102207/28542cca-53af-467b-2259-26d87155086f.png)
[動作イメージ](https://run.plnkr.co/plunks/QjB7SF/)/[ソースコード](https://embed.plnkr.co/QjB7SF/)

ついやってしまいました。オセロニア。ソシャゲーなんて、とか思っていましたがこれはハマります。

やっているうちに、「打った後の盤面の表示」「アンドゥ」ができるアプリがほしくなり、自作してみようと思いました。

#盤面の表示

```javascript:javascript
//盤面の初期情報(6 x 6)
var startBoard = [ 0,  0,  0,  0,  0,  0,
                   0,  0,  0,  0,  0,  0,
                   0,  0, -1, +1,  0,  0,
                   0,  0, +1, -1,  0,  0,
                   0,  0,  0,  0,  0,  0,
                   0,  0,  0,  0,  0,  0 ];

var width = Math.sqrt(startBoard.length);
```

画像はつかわず、cssのクラス名を書き換えることで表現しています。
マスの数も、手作業とはなりますが、8x8 10x10 などへ変更が可能なように設計しています。

```html:html
<tr>
    <td id='0_0'><div class='cell corner'></div></td>
    <td id='0_1'><div class='cell'></div></td>
    <td id='0_2'><div class='cell true'></div></td>
    <td id='0_3'><div class='cell false'></div></td>
    <td id='0_4'><div class='cell'></div></td>
    <td id='0_5'><div class='cell corner'></div></td>
</tr>
```

```css:css
.corner {
	background-color: rgba(255, 255, 200, 0.2);
}
.cell.true {
	border: solid 1px #888;
	border-radius: 32px;
	background-color: black;
	box-shadow: 1px 1px 1px rgba(0,0,0,0.4);
}
.cell.false {
	border: solid 1px #888;
	border-radius: 32px;
	background-color: white;
	box-shadow: 1px 1px 1px rgba(0,0,0,0.4);
}
```

#コマを置ける場所の取得
置ける場所を探すアルゴリズム

- **敵のコマを探す**
![image](https://qiita-image-store.s3.amazonaws.com/0/102207/c4ad282a-06ac-c8ce-ec00-7939225e4355.png)

- **敵のコマを見つけたら、周囲の空きマスを探す**
![image](https://qiita-image-store.s3.amazonaws.com/0/102207/eb343e63-3b9d-a3ad-964c-f0b2703244fe.png)

- **敵のコマの先に自分のコマがないなら次の空きマスへ**
![image](https://qiita-image-store.s3.amazonaws.com/0/102207/85aa254c-a28b-b99d-8492-0ccd272fa9fe.png)

- **自分のコマを見つけるまで進む**
![image](https://qiita-image-store.s3.amazonaws.com/0/102207/e2fd57d2-f61d-cd99-9a57-690ef6294d2e.png)

- **見つけたらその位置を配列に入れて次の空きマスへ**
- **全ての空きマスを見終えたら、次の敵のコマを探す**

ソースコードはこんな感じです。

```Javascript
//置けるセルの一覧を作成
function getPlaceables(fa){
	var ret = [];
	var cnts = {};
	for(var i=0; i<board.length; i++){
		// 敵のコマを探す
		if(board[i] == (-fa)){
			//周囲8マスをぐるりと見渡す
			for(var j=0; j<8; j++){
				var c = getForwordCode(i, j); //進んだ先のマスの番号
				if (c < 0 || c >= board.length) continue;
				// 周りの空きセルを探す
				if(board[c] == 0){
					var f = 7 - j; //空きマスの反対方向の向き
					var cur = c; //現在位置
					var cnt = 0; //敵のコマのカウンタ
					while(1){
						cur = getForwordCode(cur, f); //次へ進む
						if (cur < 0 || cur >= board.length || board[cur]==0) break;
						if (board[cur]==fa && cnt > 0){
							//敵の向こうに自分のコマがあったら場所と個数記録
							cnts[c] = (cnts[c] ? cnts[c] + cnt: cnt);
							ret[ret.length] = {code: c, count: cnts[c], weight: weight[c] };
							break;
						}
						cnt ++;
					}
				}
			}
		}
	}
	return ret;
}
```

ぐるぐるループで目が回ります＞＜

#CPUのAIっぽいもの
AIは学習させるものなど、賢こくする方法はいろいろあると思いますが、
今回はあまり面倒なことはせず、あらかじめマスに重みを付ける方法を採用しました。

```Javascript:AI用重みパターン
var weightOriginal = [
	1, 2, 0, 0, 2, 1,
	2, 3, 1, 1, 3, 2,
	0, 1, 0, 0, 1, 0,
	0, 1, 0, 0, 1, 0,
	2, 3, 1, 1, 3, 2,
	1, 2, 0, 0, 2, 1
];
```

*重み*は 0, 1, 2, 3 と数値が高くなるほど危険度が高いぞ！ということになり、
コマを置ける場所のリストをこの数値を元にソートするだけで、簡単にAIらしきものができます。

それだけでは、同じ重みの場所が複数存在してしまうので、置いた時に裏返せる枚数をつかってソートをかけます。
さらに、同じ重み・枚数があると、毎回同じ順番にソートされてしまうので、あらかじめランダムにシャッフルしておきます。

```javascript:ソースコード
//AI？(ただのソートです)
function compute(fa){
	if(placeable.length == 0) return -1;
	if(placeable.length > 1){
		//置けるセル情報をランダムに入れ替える(重みや裏返し枚数が同じ場合に変動させる)
		for(var i=0; i<placeable.length; i++){
			var r;
			while((r = ~~(Math.random() * placeable.length))==i);
			var tmp = placeable[i];
			placeable[i] = placeable[r];
			placeable[r] = tmp;
		}
		//置けるセル情報をソートする ( weight ASC, count DESC )
		placeable.sort(function(_oA, _oB) {
			return _oA.weight > _oB.weight ||
				(_oA.weight == _oB.weight && _oA.count < _oB.count) 
				? 1
				: (_oA.weight == _oB.weight && _oA.weight == _oB.weight && _oA.count == _oB.count) ? 0 : -1;
			}
		);
	}
	return placeable[0].code;
}
```

#安藤！アンドゥ！
アンドゥの機能は、盤面をまるっと保存して、盤面を復元させれば簡単ですが、それだと変動の無いマス情報が無駄に記憶されてしまうので、裏返されたコマの情報だけ保存することにしました。

- 置かれたり裏返されるコマのあったマスの変更前の状態を配列にして``undoBuffer``へpushします。
- アンドゥボタンが押されたら、``undoBuffer``から状態をpopして変更前の情報にもどします。

```javascript:コマを裏返す処理
function setAndReverse(_code, fa){
	$("td").removeClass("placeable");
	$("td").removeAttr("num");
	var tt = 0;
	if(_code >= 0) {
		var putted = false;
		for(var i=0; i<8; i++){
			var c = getForwordCode(_code, i);
			if (c < 0 || c >= board.length) continue;
			//敵のセルを探す
			if(board[c] == (-fa)){
				var cur = c; //現在位置
				var cells = [cur];
				while(1){
					cur = getForwordCode(cur, i);
					//敵の向こうになにもないなら記録を消してbreak
					if (cur < 0 || cur >= board.length || board[cur]==0){ cells = []; break; }
					if (board[cur]==fa) break;
					cells[cells.length] = cur;
				}
				//新たに置いたコマ
				if(!putted) {
					undoBuffer.push([{code: _code, piece: board[_code]}]);
					setPiece(_code, fa);
					putted = true;
				}
				//コマを裏返す
				for(let cell of cells){
					undoBuffer[undoBuffer.length-1].push({code: cell, piece: board[cell]});
					setTimeout(function(){
						setPiece(cell, fa);
					}, tt += 100);
				}
			}
		}
	}
	//裏返し終わった後にターン交代
	setTimeout(function(){
		$("#com").prop("disabled", false);
		nextTurn();
	}, tt + 100);
}
```

```javascript:アンドゥ
function undoBoard(){
	if(undoBuffer.length==0) return;
	var pieces = undoBuffer.pop();
	for(let p in pieces){
		//重みリストを初期の状態に
		weight[pieces[p].code] = weightOriginal[pieces[p].code];
		//コマを戻す
		setPiece(pieces[p].code, pieces[p].piece);
	}
	$("td").removeClass("placeable");
	$("td").removeAttr("num");
	nextTurn();
}
```

#おしまい
作る作業で時間がかかってしまい、肝心のオセロニアのログインボーナスを取り逃してしまいました＞＜