スタイルシート モンテカルロ の説明
戻る


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


使い方

「開始」をクリックすると,1 組のトランプのカードのうち 25 枚が並べられます.
残りの 27 枚は現実のゲームでは山札になり,カードを取り除いて空いた場所を詰めるときにそこからカードを追加します.このおもちゃでは画面には山札は表示されませんが,「詰める」をクリックすると残りのカードが順に画面に追加されます.

ランク(A,2,3,…)が同じで隣接している 2 枚のカードを選んで取り除いていきます.隣接するというのは,縦・横・斜めの何れかの方向に隣り合っているということです.
取り除くカードの一方をクリックします.クリックしたカードが選択されて,黄色で示されます.もう一方のカードをクリックすると,その 2 枚のカードが取り除かれます.
選択したカードをもう一度クリックすると,選択を取り消すことができます.

カードを取り除いた後「詰める」をクリックすると,空いている場所が詰められます.

カードをすべて取り除くことができたらゲーム クリアです.

カードをすべて取り除いたか,または,どのカードも取り除けない状態になったら「終了」をクリックしてください.最初の状態に戻ります.
その後「開始」をクリックすると,別のカードの並びでまたプレイできます.


カードを並べる処理

カードを並べる場はグリッド レイアウトで作っています.カードがグリッドのアイテムです.
カードをシャッフルするためにアニメーションの機能を使います.それぞれのカードについて,アイテムの order が変化するアニメーションを無限に繰り返すように設定します.
初期状態のとき(HTML をロードした直後),およびゲームを終了したときにアニメーションを動かし,ゲームを開始したらアニメーションを停止します.その時点の order の値でカードの並び順が決まります.
各カード毎にアニメーションのパラメータを変えてあるので,ボタンをクリックするタイミングによって並び順が変わります.

2024年に W3C の Working Draft で乱数に関する機能が提案されましたが,その機能が実装されても,このプログラム(?)のように何かの操作をする度に違う値を発生させるようなことはできないようです.

画面には最大 25 枚のカードが表示されますが,グリッドにはすべてのカードを配置します.グリッド要素の大きさはカード 25 枚分にして,overflow:hidden を設定してあふれた部分は表示されないようにします.
グリッドのカードの配置
カードを詰める処理を行うと,非表示の領域のカードが順に前に移動して表示されます.

一般に,ゲームを作るときに構成要素の配置などをランダムに決めたいことがよくありますが,それをスタイルシートだけで行おうとすると問題が起きます.
何らかの方法で乱数のようなものを発生させて,それに従って配置などを決めようとすると,発生させた数が重複した場合に困ります.JavaScript を使うのであれば,重複しない数が得られるまで乱数の発生を繰り返せばよいのですが,スタイルシートではそのようなことはできません.
しかし,このプログラムの場合はそのような問題はありません.order は重複していても構いませんし,連続している必要もありません.ですので,単にアニメーションで決めた値をそのまま order に指定すればよいことになります.

このプログラムではカードの位置関係で処理が変わることはありません.取り除けるのは隣接するカードだけという条件はありますが,後述のように,選択したカードの位置を基準にしてマスクを作ることでその条件を満たすことができますから,他のカードとの位置関係を判定したりする必要はありません.表示上カードの位置が変わってさえいればよいので,order を使って並び順を変えるだけで充分です.


カードの構成要素

1 枚のカードは HTML 上では,たとえばこのような構成になっています.
<DIV ID="csa" CLASS="card">A
  <LABEL CLASS="sel" FOR="sa"></LABEL>
  <LABEL CLASS="remca" FOR="sca"></LABEL>
  <LABEL CLASS="remha" FOR="sha"></LABEL>
  <LABEL CLASS="remda" FOR="sda"></LABEL>
</DIV>
カード要素(クラス名 card)の ::before と ::after の疑似要素も使っているので,実際の要素の構成はこのようになっています.
<DIV ID="csa" CLASS="card">A
  ::before
  <LABEL CLASS="sel" FOR="sa"></LABEL>
  <LABEL CLASS="remca" FOR="sca"></LABEL>
  <LABEL CLASS="remha" FOR="sha"></LABEL>
  <LABEL CLASS="remda" FOR="sda"></LABEL>
  ::after
</DIV>
ただし,後述のようにダイヤのカードについては ::after は使いません.

カード要素の各子要素の用途はこのようなものです.
要 素用 途
::before 疑似要素カードを取り除く処理のとき,選択したカードに隣接する領域の外側をマスクしてクリックできないようにする
クラス名 sel の要素カードを選択する処理のためのチェック ボックスのラベル
(以下「カード選択ラベル」という)
クラス名 rem○○ の要素カードを取り除く処理のためのチェック ボックスのラベル
(以下「カード除去ラベル」という)
::after 疑似要素カードを取り除く処理のディレイの間,画面をマスクしてクリックできないようにする

カード選択ラベル,カード除去ラベルについては後述します.

カードを取り除く処理のマスクは,ペアのうちの一方のカードの ::after を使います,
マスクに使うカードのスート(スペード,クラブ など)をこのように決めます.ダイヤのカードはマスクに使わないので,ダイヤのカードには ::after はありません.
ペアの組み合わせマスクに使うスート
スペードとクラブスペード
スペードとハートスペード
スペードとダイヤスペード
クラブとハートクラブ
クラブとダイヤクラブ
ハートとダイヤハート
カード選択ラベルとカード除去ラベルの要素は,Z オーダーを操作して有効/無効を切り替えます.無効にする要素はカード要素の背面になるように Z オーダーを設定して,カードをクリックしても反応しないようにします.


カードを選択する処理

カードを選択する処理のために,各カードに対応して 52 個のチェック ボックスを使います.チェック ボックスは非表示にしておき,各チェック ボックスに対してラベル(上述の「カード選択ラベル」)を関連付けて,それをカードに付けています.
このラベルは,他のカードが選択されていないとき有効にします.

選択されているカードが無い状態のとき,このラベルが有効になっているカードをクリックするとそのラベルがクリックされ,対応するチェック ボックスが :checked 状態になるので,そのカードとペアになるカードのカード除去ラベルを有効にします.
また,クリックしたもの以外のカードのカード選択ラベルを無効にします.つまり,カードを選択したら他のカードが選択できないようにします.
クリックしたカードのカード選択ラベルは有効のままにします.選択されているカードをクリックしたときは,そのカードのカード選択ラベルが再度クリックされてチェック ボックスのチェック状態が解除されるので,選択が取り消されます.

ペアになるカードであっても,選択したカードに隣接しているもの以外のものとでは取り除くことができないようにします.そのために,隣接するカードの場所以外の部分をマスク用の要素で覆います.
マスク用の要素は選択したカードの ::before 疑似要素を使います.マスク要素全体の位置と大きさは画面全体を覆うようにし,選択したカードに隣接するカードの領域に穴が開くように clip-path を設定します.(実際にはグリッド要素でクリップされるので,マスクされる領域はグリッド要素の範囲内に限定されます.)

たとえば,このようなカード配置で中央のハートの 10 を選択した場合,右上のクラブの 10 と左下のスペードの 10 がペアになります.
カード選択の例
以下の画像で青色で示した部分がマスクの領域です.
カード選択時のマスクの例
クラブの 10 はカード除去ラベルは有効な状態になっていますが,マスクで覆われているのでクリックは無効になります.


カードを取り除く処理

カードを取り除く処理のためにチェック ボックスを使います.
チェック ボックスは取り除けるカードの組み合わせの数分用意します.たとえば,「A」のカードを取る組み合わせは,次のように全部で 6 通りあります.
  • スペードの A と クラブの A
  • スペードの A と ハートの A
  • スペードの A と ダイヤの A
  • クラブの A と ハートの A
  • クラブの A と ダイヤの A
  • ハートの A と ダイヤの A
他のランクについても同様に 6 通りずつありますから,組み合わせの数は全部で 13 × 6 = 78 通りになります.その組み合わせに対応して 78 個のチェック ボックスを使います.
チェック ボックスは非表示にしておき,各チェック ボックスに対してラベル(上述の「カード除去ラベル」)を関連付けて,それをカードに付けています.
このラベルは,ペアになるカードが選択されているとき有効にします.

各チェック ボックスにはラベルが 2 つ関連付けられます.たとえば,スペードの A とクラブの A を取り除くチェック ボックスのラベルは,スペードの A とクラブの A の両方に付けます.
スペードの A をクリックして選択 → クラブの A をクリックして取り除く
という手順で操作した場合は,クラブの A に付けたラベルが使われます.
クラブの A をクリックして選択 → スペードの A をクリックして取り除く
という手順で操作した場合は,スペードの A に付けたラベルが使われます.

このラベルが有効になっているカード,つまりペアとなるカードが選択されているカードをクリックするとそのラベルがクリックされ,対応するチェック ボックスが :checked 状態になるので,対象のカードを非表示にします.また,カードを詰める処理のための準備をします.
カードを非表示にするために visibility:hidden を設定します.加えて,カードを詰める処理のために,位置が非表示の領域になるよう grid-row と grid-column を設定します.(カードを詰める処理については後述)

カードを非表示にしてもカードを選択するためのチェック ボックスはチェック状態のままですが,カードが選択された状態のままだと残りのカードの処理に支障があるため,カードの選択状態を参照するために使っているカスタム プロパティはクリアして,取り除いたカードは選択されていないものとして扱われるようにしています.

取り除くカードをクリックしたとき,そのカードを黄色で示して,少しディレイを置いてからカードを非表示にするようにしています.そのために visibility に transition でディレイを設定します.また,grid-row と grid-column には transition で大きなディレイを設定します.
具体的にはこのような transition を設定します.
transition:visibility 0s 0.3s,
           grid-row 0s 10000s allow-discrete, grid-column 0s 10000s allow-discrete;
カードは 0.3 秒後に非表示になりますが,grid-row と grid-column の設定はまだ表示に反映されないので,カード要素はまだ元の場所にあります.そのため,カードがあった場所は表示上は空いている状態になります.

非表示になるまでの間に他のカードや「詰める」がクリックされないよう,ディレイの間はマスク用の要素で画面全体を覆って,クリックが無効になるようにしています.
マスク用の要素はペアのカードのうちの一方の ::after 疑似要素を使います.
カードが非表示になると ::after 要素も非表示になるので,マスクは解除されます.


カードを詰める処理

カードを詰める処理のためにボタン(TYPE 属性 BUTTON の INPUT 要素)を使います.ボタンは非表示にしておき,ボタンに対してラベルを関連付けて,「詰める」ボタンを作っています.
「詰める」を押下するとボタンが :active 状態になるので,カードを取り除く処理で設定した transition を取り消します.
grid-row と grid-column のディレイが無くなるので設定が表示に反映され,取り除いたカードが非表示の領域に移動します.取り除いたカードがあった場所には,その後にあるカードが順に移動します.そして,取り除いたカードの枚数分,非表示の領域にあったカードが表示される領域に移動します.
カードを詰める前の状態

↓

カードを詰めた後の状態


ゲーム開始

ゲーム開始時の処理を行うためにチェック ボックスを使います.チェック ボックスは非表示にしておき,それにラベルを関連付けて,「開始」ボタンを作っています.
ボタンをクリックするとラベルがクリックされ,対応するラジオ ボタンが :checked 状態になるので,カードの並び順を決めるアニメーションを停止し,カードや「詰める」ボタンなどを表示します.


ゲーム終了

上で説明したチェック ボックスをすべてフォームの中に入れ,フォームにリセット ボタン(TYPE 属性 RESET の INPUT 要素)を付けます.リセット ボタンは非表示にしておき,それにラベルを関連付けて,「終了」ボタンを作っています.
ボタンをクリックすると,フォームがリセットされてチェック ボックスが初期の状態に戻るので,カードは初期状態になります.
ゲーム開始のチェック ボックスも未チェックの状態に戻るので,カードの並び順を決めるアニメーションも再開します.


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

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


戻る