スタイルシート マインスイーパー の説明
戻る


JavaScript を使わず,スタイルシートだけでマインスイーパーを作ってみます.
全くのお遊びです.これが何か実用の役に立つようなことは多分無いでしょう.スタイルシートは本来,動作を記述するものではありませんので,かなり力ずくで作っています.
画面
(これはキャプチャ画像です.これで遊ぶことはできません.)
使い方と仕組みについて説明します.


使い方

セルの数は縦 9 個 × 横 9 個の 81 個で固定です.機雷の数は 8 個 〜 12 個の範囲で選べます.
盤面の上側に旗の絵のボタンがあります.セルをクリックしたときの処理をこのボタンで切り替えます.
ゲームを開始したときはセルを開けるモードになっています.その状態でセルをクリックするとクリックしたセルが開きます.
このボタンをクリックすると旗を付けるモードになります.ボタンの表示とマウス ポインタの形状がこのように変わります.
旗を付けるモード
この状態でセルをクリックするとクリックしたセルに旗が付きます.旗が付いているセルをクリックすると旗が外れます.
旗を付けるモードのときにボタンをクリックすると,セルを開けるモードに戻ります.


モードの切り替え

スタイルシートではマウスを右クリックしたときに何か処理を行うということができません.一般的なマインスイーパーでは右クリックでセルに旗を付けることができますが,それができないため,セルをクリックしたときにセルを開けるかセルに旗を付けるかをボタンで切り替えます.
どちらの処理を行うかの状態を保持するためにチェック ボックスを使います.チェック ボックスは非表示にしておき,それにラベル(LABEL 要素)を関連付けます.ラベルをクリックするとチェック ボックスの :checked 状態が変わるので,それに対応して処理のモードを切り替えます.


セルの構成

各セルは次の 3 つの要素で構成されます.
  • セルの背景

  • セルを開けるラジオ ボタンに対応するラベル

  • セルに旗を付けるチェック ボックスに対応するラベル
セルの背景は開けたセルを表示するための要素です.セルの周囲の機雷の数を示す数値はここに表示されます.
セルを開けるラジオ ボタンに対応するラベル(以下「セルを開けるラベル」という)とセルに旗を付けるチェック ボックスに対応するラベル(以下「旗を付けるラベル」という)については後述します.

ゲームを開始したときは,この 3 つの要素がこのような状態になっています.
セルの構成 1
セルの背景の前面にセルを開けるラベルがあり,旗を付けるラベルは非表示になっています.セルをクリックするとセルを開けるラベルがクリックされます.

旗を付けるモードにしたときは,このような状態になります.
セルの構成 2
旗を付けるラベルが表示されます.セルをクリックすると旗を付けるラベルがクリックされます.

開いているセルについては,このような状態になります.
セルを開けるモードのとき
セルの構成 3

旗を付けるモードのとき
セルの構成 4
セルの背景が前面になります.セルをクリックしても何も起こりません.


セルを開ける処理

各セルに対応して 81 個のラジオ ボタンを使います.ラジオ ボタンは非表示にしておき,各ラジオ ボタンに対してラベルを関連付けます.
ラベルをクリックすると対応するラジオ ボタンが :checked 状態になるので,セルの背景の Z オーダーを変えてセルの背景が前面になるようにします.


セルに旗を付ける処理

各セルに対応して 81 個のチェック ボックスを使います.チェック ボックスは非表示にしておき,各チェック ボックスに対してラベルを関連付けます.
ラベルをクリックすると対応するチェック ボックスの :checked 状態が変わるので,それに対応してラベルの旗の表示を付けたり外したりします.セルを開けるモードに戻すとセルを開けるラベルが前面になるので,セルを開けるラベルにも旗の表示を付けたり外したりします.
また,旗を付けたセルは開けることができないようにするため,旗を付けたセルについてはセルを開けるラベルに pointer‑events:none を設定して,クリックに反応しないようにします.


セルを自動で開ける処理

何れかのセルを開けたとき,その周囲のセルに機雷がひとつも無ければ(つまり開けたセルに数字が表示されていない場合),周囲のセルを自動的に開けます.その処理が結構厄介です.
たとえば,次のようなセル a とセル b の関係を考えます.
セルを自動で開ける処理 1
セル a を開けたとき周囲に機雷がひとつも無ければセル b も自動で開けます.
セルを自動で開ける処理 2
一方,セル b を開けたとき周囲に機雷がひとつも無ければセル a も自動で開けます.
セルを自動で開ける処理 3
隣り合っているセルの状態を互いに参照することになります.そのような処理をスタイルシートの計算式で書こうとすると参照が循環してしまいます.
その問題を解決するため,一方向の参照だけを行う 2 つの計算に処理を分けます.セルの並びに順序を決めて,その順序の正順の参照を行う計算と逆順の参照を行う計算を別に行います.
順序はどのように決めてもよいのですが,このプログラム(?)では左上から右下に向かう次のような順にしています.
セルの順序付け
次の図で真ん中にあるセルを開ける条件を周囲のセルの状態から計算する際,上記の順序ではセル a 〜 d が正順の参照,セル e 〜 h が逆順の参照になります.
セルの参照の方向
セル a 〜 d の状態からの計算とセル e 〜 h の状態からの計算を別々に行って,どちらかの結果がセルを開ける条件を満たしていたら,真ん中のセルを開けます.

しかし,まだこれだけでは充分ではありません.正順の条件から計算してセルが開いた結果,そのセルから逆順に隣接するセルが開く条件を満たす場合があります.また,その逆の場合もあります.そのため,一度計算で求めた結果を基にして,さらに同様の計算を行うということを繰り返す必要があります.
問題は,それを何回繰り返せばよいのかが判らないことです.普通のプログラムのように,新しく開くセルが無くなるまで処理を繰り返すということはできないので,必要な最大の回数,処理を繰り返すようにしておく必要があります.
しかし何回繰り返せばよいのでしょうか? 最大でもこの回数繰り返したら,どのような配置パターンの場合でも必ず条件を満たすセルがすべて開くという回数は数学的に求まるのでしょうか?
また,前の計算の結果を参照して次の計算を行うということを繰り返すと,繰り返す度に参照が深くなっていきます.繰り返しの回数があまり多いと,正常に動作しなくなる恐れがあります.

このプログラムでは以下のように,一方向の参照の計算を交互に,全部で 5 回繰り返すようにしました.
(1) 正順の参照で計算する.

(2) 手順 (1) の計算結果を基に逆順の参照で計算する.

(3) 手順 (2) の計算結果を基に正順の参照で計算する.

(4) 手順 (3) の計算結果を基に逆順の参照で計算する.

(5) 手順 (4) の計算結果を基に正順の参照で計算する.
これで,正順の参照から始まる計算は 正順→逆順→正順→逆順→正順 の 2 往復半,逆順の参照から始まる計算でも 逆順→正順→逆順→正順 の 2 往復の計算が行われます.
この方法で大体うまく行くようです.テストでかなりの回数プレイしてみましたが,今のところ本来は開くべきセルが開かなかったことはありません.

もし開かなかったセルがあるとどうなるかというと,それは自動で開かないだけで,そのセルをクリックすれば開けられますから,開かなかったセルがあった場合は手動で開けてください.


機雷の配置

機雷をランダムに配置するために乱数が必要ですが,今のところスタイルシートには乱数を生成する関数はありません.
2024年に W3C の Working Draft で乱数に関する機能が提案されましたが,このおもちゃを作った時点では,まだ対応しているブラウザは無いようです.
また,単に乱数で位置を決めるだけだと同じ位置が重複して選ばれる場合があります.同じ位置が選ばれた場合は違う位置が選ばれるまでやり直すなどという処理は,スタイルシートではできません.

そこで,線形合同法という擬似乱数の生成方法を使って位置を決めます.線形合同法で生成される乱数には比較的単純な周期性があり,生成のパラメータを特定の規則に従って選ぶと,0 〜 80 の数値を重複せずにランダムに並べることができます.その数値を各セルの位置に対応させ,生成される数値の最初から機雷の数(8 〜 12)の分を使って機雷の位置を決めます.
この方法ではすべての配置のパターンを生成することはできませんが,これでも充分多いパターンを生成できます.


擬似乱数のパラメータの決定方法

上述のように,擬似乱数を使って機雷の位置を決めるのですが,乱数生成の各パラメータをランダムに選ばなければなりません.そのために,アニメーションの機能を使います.
各パラメータに対応するカスタム プロパティを定義しておいて,パラメータの規則に従ってその値が変化するアニメーションを無限に繰り返すように設定します.そして,何れかのセルを開けた時点でアニメーションを停止し,そのときのプロパティの値を使って乱数生成を行います.こうすれば,セルを開けるタイミングによって異なるパラメータが使われます.


残りの機雷数の表示

残りの機雷数の表示には CSS カウンタを使っています.CSS カウンタの一般的な使い方は,counter-reset/counter-set で値を設定して counter-increment で値を増減させるというものだと思いますが,このプログラムでは counter-reset で値を設定しているだけです.
機雷の数から旗の数を引いたものが残りの機雷数です.付けた旗が合っているかどうかは無関係です.
その数を counter-reset でカウンタに設定して,counter() を使って表示しています.つまり,CSS カウンタは単に数値を文字列に変換する機能として使っています.


カスタム プロパティの定義

このプログラムでは,多くのカスタム プロパティを @ルールの @property で定義しています.
@property の定義は,たとえばこのように書きます.
@property --a {
  syntax:'<integer>';
  inherits:true;
  initial-value:0;
}
上述のように,乱数生成のパラメータを決めるためにカスタム プロパティのアニメーションを使っています.カスタム プロパティに計算値によるアニメーションを使うためには,プロパティのデータ型を定義する必要があります.
@propertysyntax<integer> を指定することで,そのプロパティを計算値によるアニメーション可能にしています.

その他に,処理の効率化のために多くのプロパティを @property で定義しています.
値が整数であることが決まっているプロパティは整数型と定義しておくと,どうも処理の効率がよくなるようです.
このプログラムでは,スタイルシートとしてはかなり複雑な計算を行いますので,少しでも処理効率がよくなるよう,値が整数に限定されるプロパティは @property で定義しています.


戻る