動機
暇なときになんとなくマインスイーパを作ってみたら楽しかったので、記録として。
(ゲーム終了判定などは割愛してます。。)
マインスイーパのルールと処理
遊び方はここに分かりやすく載っています。
http://dotpico.com/mine/ja/rule.php
左クリックの処理
- クリックすると、そのセルを開くことができる
- そのセルに爆弾が入っていたら、その時点でゲームオーバー
- そのセルに爆弾が入っていなければ、周囲に存在する爆弾の数が表示される
- そのセルに爆弾が入っておらず、周囲に1つも爆弾が存在しないとき、周囲のセルが自動的に開かれる
右クリックの処理
- 1回右クリックすると、そのセルに「フラグ」をつけることができる
- 2回右クリックすると、そのセルに「?」をつけることができる
- 3回右クリックすると、元に戻る
ダブルクリックの処理
- 開いたセルに書いてある数字と、その周囲の「フラグ」の数が一致しているとき、そのセルをダブルクリックすると、「周囲の開かれていないセル」を一度に開くことができる
実装プログラム
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<style>
td {
padding: 0px;
width: 40px;
height: 40px !important;
text-align: center;
border: 1px solid rgb(60, 60, 60);
}
/** 未開封のクラス */
.closed {
background-color: lightgray;
color: black;
cursor: pointer;
}
/** 開封して爆弾だったときのクラス */
.bombed {
background: red;
color: white;
cursor: auto;
}
/** 開封して爆弾ではなかったときのクラス */
.opened {
background: white;
color: black;
cursor: auto;
}
</style>
</head>
<body>
<div id="msg"></div>
<table>
<tbody id="target">
<!-- 中身はJavaScriptでつくる -->
</tbody>
</table>
<script src="main.js" type="text/javascript"></script>
</body>
</html>
'use strict';
//===================================
// マインスイーパ用のセルクラス
// セルはこのクラスのインスタンスとする
//===================================
class MSCell extends HTMLTableCellElement {
//-----------------------------------
// コンストラクタ
//-----------------------------------
constructor() {
super();
// イベント登録
this.addEventListener('click', this.clickFunc);
this.addEventListener('contextmenu', this.clickRightFunc);
this.addEventListener('dblclick', this.clickDblFunc);
}
//-----------------------------------
// マインスイーパ初期設定
// x座業、y座標、爆弾かどうかをパラメータにとる
//-----------------------------------
init(x, y, bombFlg) {
// 開封フラグ(未開封のときfalse/開封済みのときtrue)
this.openedFlg = false;
// x座標
this.x = x;
// y座標
this.y = y;
// 爆弾フラグ(爆弾のときtrue/爆弾でなければfalse)
this.bombFlg = bombFlg;
// 見た目のクラス
this.classList.add('closed')
}
//-----------------------------------
// 周辺セルを設定する
// 周辺セルと、周辺セルの合計爆弾数を設定する
//-----------------------------------
setArounds(arounds) {
// 周辺セル
this.arounds = arounds;
// 周辺セルの爆弾数
this.aroundBombCount = this.arounds.filter(around => around.bombFlg).length;
}
//-----------------------------------
// そのセルの中身を表示する
//-----------------------------------
show() {
if (this.bombFlg) {
// 爆弾のときは「爆」
this.textContent = '爆';
// 見た目の変更
this.classList.remove('closed');
this.classList.add('bombed');
} else {
// 爆弾ではないとき
if (this.aroundBombCount > 0) {
// 周辺の爆弾数が1個以上のときは数を表示
this.textContent = this.aroundBombCount;
}
// 見た目の変更
this.classList.remove('closed');
this.classList.add('opened');
}
}
//-----------------------------------
// セルを左クリックしたときの関数
//-----------------------------------
clickFunc() {
if (this.openedFlg) {
// 開封済みのときは何もしない
return;
}
if (this.textContent === '旗' || this.textContent === '?') {
// 「旗」や「?」がついてるときも何もしない
return;
}
// 開封済みにする
this.openedFlg = true;
// このセルを開く
this.show();
if (this.bombFlg) {
// このセルが爆弾のときはゲームオーバーなので全セルを開く
msCells.forEach(button => button.show());
} else {
// このセルが爆弾でないとき
if (this.aroundBombCount === 0) {
// 周囲に爆弾が無いときは周囲のセルを全部開く
this.arounds.forEach(around => around.clickFunc());
}
}
}
//-----------------------------------
// セルを右クリックしたときの関数
//-----------------------------------
clickRightFunc(e) {
// 右クリックメニュー禁止
e.preventDefault();
if (this.openedFlg) {
// 既に開かれている場合は何もしない
return;
}
if (this.textContent === '') {
// 旗を表示
this.textContent = '旗';
} else if (this.textContent === '旗') {
// ?を表示
this.textContent = '?';
} else if (this.textContent === '?') {
// 元に戻す
this.textContent = '';
}
}
//-----------------------------------
// セルをダブルクリックしたときの関数
//-----------------------------------
clickDblFunc() {
if (!this.openedFlg) {
// 既に開かれている場合は何もしない
return;
}
// 周囲の旗の数を取得
let flgCount = this.arounds.filter(around => around.textContent === '旗').length;
// 周囲の旗の数と、クリックしたセルに表示されている爆弾数が一致していれば
// 周囲のセルをすべて開く
if (this.aroundBombCount === flgCount) {
this.arounds.forEach(around => around.clickFunc());
}
}
}
//===================================
// カスタム要素の定義
//===================================
customElements.define('ms-td', MSCell, { extends: 'td' });
//===================================
// 全セルを格納しておく変数
//===================================
let msCells = [];
//===================================
// ゲーム初期化用関数
//===================================
let initGame = function (xSize, ySize) {
// ボタン配置
for (let y = 0; y < ySize; y++) {
let tr = document.createElement('tr');
for (let x = 0; x < xSize; x++) {
// セルを作る
let msCell = document.createElement('td', { is: 'ms-td' });
// セルの初期化
msCell.init(x, y, Math.random() * 100 < 10);
// セルをtrにいれておく
tr.appendChild(msCell);
// msCellsにも入れておく
msCells.push(msCell);
}
document.getElementById('target').appendChild(tr);
}
// aroundsの設定
msCells.forEach(msCell => {
// 周囲8マスを取得
let arounds = msCells.filter(otherCell => {
if (msCell === otherCell) {
return false;
}
let xArea = [msCell.x - 1, msCell.x, msCell.x + 1];
let yArea = [msCell.y - 1, msCell.y, msCell.y + 1];
if (xArea.indexOf(otherCell.x) >= 0) {
if (yArea.indexOf(otherCell.y) >= 0) {
return true;
}
}
return false;
});
// 周囲8マスをaroundsとして設定
msCell.setArounds(arounds);
});
}
//===================================
// ゲーム初期化
//===================================
initGame(15, 15);
実装のポイント
セルの作り方
JavaScriptのcreateElementでtableタグの中身をつくっていますが、
その中のtdタグは、独自に定義したカスタム要素「MsCell」です。(Msはマインスイーパの略)
MsCellはHTMLTableCellElementを継承しています。
そのため、見た目は普通のtdタグと同じですが、
マインスイーパに必要なopenedFlgなどのプロパティと、clickFuncなどの関数を持っています。
セルに対する動作
ポイントは二つです。
開いたセルに爆弾が入っていなければ、そのセルの周囲に存在する爆弾の数が表示される
つまり、セルはその周囲のセルの中身を知っている必要があります。
そこで、セルにaroundsというプロパティを持たせ、周囲の8セルを参照させておくことにしました。
開いたセルに爆弾が入っておらず、そのセルの周囲に1つも爆弾が存在しないとき、周囲のセルが自動的に開かれる
クリックされたセルの周囲の爆弾数で0であれば、そのセルのaroundsもすべて再帰的にクリックしていきます。
また、再帰処理を抜けるための条件として、openedFlg(未開封:false/開封済:true)を持たせています。