| スタイルシート ピラミッド の説明 |
|
|
|
JavaScript を使わず,スタイルシートだけでピラミッド ゲームを作ってみます. 全くのお遊びです.これが何か実用の役に立つようなことは多分無いでしょう.スタイルシートは本来,動作を記述するものではありませんので,かなり力ずくで作っています. 使い方と仕組みについて説明します. 使い方 ピラミッドのルールには色々なバリエーションがあるようです.このおもちゃでは以下のようなルールにしています. 「開始」をクリックすると,1 組のトランプのカードのうち 28 枚がピラミッド状に並べられ,残りの 24 枚が山札として左下の場所に裏向きに置かれます.ピラミッドの最下段のカードは表向きで,他の段のカードは裏向きに置かれます. ペアになる 2 枚のカードを選んで取り除いていきます.ペアになるカードはランクの合計が 13 になるカードです.ランクは,2 〜 10 のカードはその数値,エースは 1,ジャックは 11,クイーンは 12,キングは 13 です. キングは 1 枚だけで取り除けます. キング以外のカードを取り除く場合,取り除くカードの一方をクリックします.クリックしたカードが選択されて,黄色で示されます.もう一方のカードをクリックすると,その 2 枚のカードが取り除かれます. キングはクリックするとすぐに取り除かれます. 選択したカードをもう一度クリックすると,選択を取り消すことができます. 山札をクリックすると,山札の一番上のカードが表向きになって山札の隣に置かれます.山札をクリックする度に山札の一番上のカードが表向きになり,隣にあったカードは順次,右下の捨て札の場所に積まれます. 山札からめくったカードもピラミッドのカードとペアにして取り除くことができます.また,捨て札の一番上のカードもピラミッドのカードとペアにすることができます. ただし,山札または捨て札を選択した状態では,山札をめくることはできません.選択を取り消してからめくってください. ピラミッドの裏向きのカードの上に重なっている 2 枚のカードが取り除かれると,下にあったカードが表向きになります.新たに表向きになったカードも同様に取り除いていきます. 山札のカードをすべてめくるとリディールのボタンが表示されます. ボタンをクリックすると,めくった山札と捨て札のカードがすべて山札に戻り,再度一番上のカードからめくっていくことができます. リディールの回数に制限はありません.何回でもリディールできます. ただし,山札または捨て札を選択した状態では,リディールはできません.選択を取り消してからリディールしてください. ピラミッドのカードをすべて取り除くことができたらゲーム クリアです. ピラミッドのカードをすべて取り除いたか,または,どのカードも取り除けない状態になったら「終了」をクリックしてください.最初の状態に戻ります. その後「開始」をクリックすると,別のカードの並びでまたプレイできます. カードを並べる処理 カードはランダムに並べるのですが,それをスタイルシートで行うのは簡単ではありません.52 枚のカードを重複せずに並べなければならないので,単純に乱数で順番を決めることはできません. 「遊び部屋」にある他のスタイルシートだけで作ったおもちゃでは,ゲームの駒などの配置をランダムに決めるために線形合同法という擬似乱数発生方法を使っているものもあります. 線形合同法を使うと,数値 N が一定の条件を満たす場合は,0 から N - 1 までのN個の数値を重複しないように 1 回ずつ発生させることができるのですが,N が 52 の場合はその条件を満たさないので,このおもちゃではその方法は使えません. そこで,あらかじめランダムに並べた順番のデータを複数作っておき,その中から使うものをランダムに選ぶという方法にしました.データは 100 組用意しました. 各カードの位置を表すカスタム プロパティ(52 個)を用意して,そのプロパティに選んだデータの値を設定します. プロパティ名は,スペードのエースは --sa,スペードの 2 は --s2 などとしています.カードの位置は次のような数値で表します. 設定は,たとえばこのようになります. この数値にしたがってカードを配置します. また,ゲーム開始時には,この数値をそのままカードの z-index に設定しています.ピラミッドのカードは,下の段のカードほど Z オーダーが上になるので,下の段のカードが上の段のカードの上に重なります.山札のカードは,数値が 70 のカードが一番下で 93 のカードが一番上になります.捨て札では,99 からこの数値を引いた値を z-index に設定します.Z オーダーが山札と逆になるので,先にめくったカードの方が下になります.このような定義を 100 組作って,どれを使うかをアニメーションの機能で選択します. 100 段階のステップのアニメーションを行い,各段階のキーフレームでプロパティを設定します. 初期状態のとき(HTML をロードした直後),およびゲームを終了したときにアニメーションを動かし,ゲームを開始したらアニメーションを停止して,そのときのプロパティの値でカードを配置します.ゲームを開始するタイミングによってカードの配置がランダムに選ばれます. カードの構成要素 1 枚のカードは HTML 上では,たとえばこのような構成になっています. [スペードのエースのカード]カード要素(クラス名 card)の[スペードの 7のカード] ::before と ::after の疑似要素も使っているので,実際の要素の構成はこのようになっています.[スペードのエースのカード]カード要素の各子要素の用途はこのようなものです.[スペードの 7 のカード]
山札めくりラベル,カード選択ラベル,カード除去ラベルについては後述します. カードを取り除く処理のマスクは,ペアのうちのランクが小さい方のカードの ::before を使うので,7 〜 クイーンのカードについては ::before は使いません.キングのカードはクリックするとすぐ取り除かれるため,カード選択ラベルはありません. 山札めくりラベル,カード選択ラベル,カード除去ラベルの各要素は, left プロパティを操作して有効/無効を切り替えます.カード要素に overflow:hidden; を設定しておき,無効にする要素はカード要素の表示領域の外になるように left を設定して,カードをクリックしても反応しないようにします.カードの裏面の表示の要素も, left を操作して表示/非表示を切り替えます.裏向きのカードでは裏面の表示の要素が表の面の表示の要素より前面にあるので,裏面が見えます.カードを表向きにするときは,裏面の表示の要素がカード要素の表示領域の外になるように left を設定して隠し,表の面が見えるようにします.カード選択ラベルとカード除去ラベルは,カードが裏向きのときはカードの裏面の表示の背面になるので,カード要素の表示領域外でなくても無効になります. 山札めくりラベルは,カードの裏面の表示の前面になるように Z オーダーを設定しているので,カード要素の表示領域外でなければ有効になります. 山札をめくる処理 山札をめくる処理のために,各カードに対応して 52 個のラジオ ボタンを使います.ラジオ ボタンは非表示にしておき,各ラジオ ボタンに対してラベル(LABEL 要素,上述の「山札めくりラベル」)を関連付けて,それをカードに付けています. このラベルは,以下の条件をすべて満たすとき有効にします.
山札/捨て札が選択されている場合を除き,山札の場所をクリックすると一番上にあるカードの山札めくりラベルがクリックされ,対応するラジオ ボタンが :checked 状態になるので,対応するカードの位置をそのプロパティに設定します.山札のカードは位置の数値が小さいものが下になるように重ねているので,直近にめくったカードより位置の数値が大きいカードは,すでにめくったものということになります.ですので,各カードの位置と上記の記録した位置を比較して,カードの位置が記録した位置以上ならカードを表向きにします. また,記録した位置と同じ位置のカード(直近にめくったカード)は山札の場所の隣に置き,それより大きい位置のカード(すでにめくっていたカード)は捨て札の場所に置きます. 捨て札に移動したカードは,位置の数値が大きいものが下になるように Z オーダーを設定しているので,先にめくったカードが下になるように重なっていきます. 表向きにしたカードについては,キング以外はカード選択ラベル,キングはカード除去ラベルを有効にします. ただし,すでに他のカードが選択されている場合は,カード選択ラベルは無効にし,選択されているカードとペアになる場合のみ,カード除去ラベルを有効にします. リディールの処理のために,もうひとつラジオ ボタンを使います. ラジオ ボタンは非表示にしておき,ラジオ ボタンに対してラベルを関連付けて,リディール ボタンを作っています. リディール用のラジオ ボタンは山札をめくる処理のラジオ ボタンと同じグループにしている(NAME 属性を同じにしている)ので,リディール ボタンをクリックすると山札をめくるためのラジオ ボタンはチェック状態でなくなり,山札を 1 枚もめくっていない状態になります.(すでに取り除いたカードは非表示になっていますが) カードを選択する処理 カードを選択する処理のために,各カードに対応して 52 個のチェック ボックスを使います.チェック ボックスは非表示にしておき,各チェック ボックスに対してラベル(上述の「カード選択ラベル」)を関連付けて,それをカードに付けています. このラベルは,以下の条件をすべて満たすとき有効にします.
:checked 状態になるので,そのカードとペアになるカードで表向きのもののカード除去ラベルを有効にします.また,クリックしたもの以外のカードのカード選択ラベルを無効にします.つまり,カードを選択したら他のカードが選択できないようにします. クリックしたカードのカード選択ラベルは有効のままにします.選択されているカードをクリックしたときは,そのカードのカード選択ラベルが再度クリックされてチェック ボックスのチェック状態が解除されるので,選択が取り消されます. カードを取り除く処理 カードを取り除く処理のためにチェック ボックスを使います. チェック ボックスは取り除けるカードの組み合わせの数分用意します.たとえば,スペードのエースのカードを取り除く組み合わせは,次のように 4 通りあります.
そのようにして,スペードの 6 〜 ダイヤの 6 を取り除く組み合わせまでを対象にします. 7 以降のカードはと言うと,たとえばスペードの 7 を取り除く組み合わせは
これにキングを単独で取り除く処理も含めると,組み合わせの数は全部で 4 × 4 × 6 + 4 = 100 通りになります.その組み合わせに対応して 100 個のチェック ボックスを使います. チェック ボックスは非表示にしておき,各チェック ボックスに対してラベル(上述の「カード除去ラベル」)を関連付けて,それをカードに付けています. このラベルは,以下の条件をすべて満たすとき有効にします.
スペードのエースをクリックして選択 → スペードのクイーンをクリックして取り除くという手順で操作した場合は,スペードのクイーンに付けたラベルが使われます. スペードのクイーンをクリックして選択 → スペードのエースをクリックして取り除くという手順で操作した場合は,スペードのエースに付けたラベルが使われます. このラベルが有効になっているカード,つまりペアとなるカードが選択されているカードかキングをクリックするとそのラベルがクリックされ,対応するチェック ボックスが :checked 状態になるので,対象のカードを非表示にしたり,後述の取り除かれたカードを記録するプロパティを設定するなどの処理を行います.カードを非表示にしてもカードを選択するためのチェック ボックスはチェック状態のままですが,カードが選択された状態のままだと残りのカードの処理に支障があるため,カードの選択状態を参照するために使っているカスタム プロパティはクリアして,取り除いたカードは選択されていないものとして扱われるようにしています. 取り除くカードをクリックしたとき,そのカードを黄色で示して,少しディレイを置いてからカードを非表示にするようにしています.ディレイの間に他のカードをクリックされると問題が起きるので,ディレイの間はマスク用の要素で画面全体を覆って,クリックが無効になるようにしています. マスク用の要素は,キング以外はペアの 2 枚のカードのうちのランクが小さい方のカードの ::before 疑似要素を使います.キングはキングの ::before 疑似要素を使います.カードが非表示になると ::before 要素も非表示になるので,マスクは解除されます.ピラミッドのカードをめくる処理 ピラミッドのカードは,上に重なっているカードが取り除かれたら表向きにします.そのためには,どのカードが取り除かれたかを記録しておく必要があります. 取り除かれたカードの位置を表すために整数のカスタム プロパティを 2 つ使い,カードの位置をビット フラグで設定します.(2 つ使う理由は後述) ピラミッドのカードは 28 枚なので,ビット フラグは 28 ビット必要です. 仕様では整数のプロパティに設定できる数値の範囲は規定されていませんが,一般的には少なくとも 32 ビットの数値は設定できると思います.ですので,28 ビットの数値は設定できそうなのですが,実際には calc() などで計算した結果を設定すると(少なくとも Opera 122 では)正確な値で設定されません.28 ビットの符号無しの数値の最大値は 268435455 です.それを
のようにプロパティに設定して,ブラウザの開発者用ツールでその値を確認すると,その場合は正確にその値が設定されています.ところが
のように設定した場合は 2.68435e+08 という値が設定されています.計算の結果を設定する場合は,結果が整数のプロパティに設定できる範囲の整数であっても,その値が一定の大きさを超えると浮動小数点数になってしまうようです. そのため,28 ビットのデータを 2 つのプロパティに分けて格納し,正確に設定できる数値の範囲を超えないようにしています. ピラミッドの各カードについて,その上に重なっている 2 枚のカードが両方とも取り除かれたかどうかを上記のビット フラグで調べ,取り除かれている場合はそのカードを表向きにします. たとえば,6 段目の一番左のカードについては,7 段目の 1 番目と 2 番目のカードに対応するビットであるプロパティ 2 のビット位置 6,7 のビットが共に 1 のとき表向きにします. 表向きにしたカードについては,他のカードが選択されていなければ,キング以外はカード選択ラベル,キングはカード除去ラベルを有効にします. 他のカードが選択されているときは,そのカードとペアになる場合はカード除去ラベルを有効にします. ゲーム開始 ゲーム開始時の処理を行うためにチェック ボックスを使います.チェック ボックスは非表示にしておき,それにラベルを関連付けて,「開始」ボタンを作っています. ボタンをクリックするとラベルがクリックされ,対応するラジオ ボタンが :checked 状態になるので,カードの配置を決めるアニメーションを停止し,カードとリディール ボタンを表示します.(リディール ボタンは山札の背面にあるので見えませんが)ゲーム終了 上で説明したラジオ ボタン/チェック ボックスをすべてフォームの中に入れ,フォームにリセット ボタン(TYPE 属性 RESET の INPUT 要素)を付けます.リセット ボタンは非表示にしておき,それにラベルを関連付けて,「終了」ボタンを作っています. ボタンをクリックすると,フォームがリセットされてラジオ ボタン/チェック ボックスが初期の状態に戻るので,カードは初期状態になります. ゲーム開始のチェック ボックスも未チェックの状態に戻るので,カードの配置を決めるアニメーションも再開します. カスタム プロパティの定義 このプログラム(?)では,いくつかのカスタム プロパティを @ルールの @property で定義しています.@property の定義は,たとえばこのように書きます.上述したように,取り除かれたカードの位置を表すために整数のカスタム プロパティを使っています. また,山札をめくったときのカードの移動をアニメーション表示するために,カスタム プロパティに transition を使っています.カスタム プロパティに transition を使うためには,プロパティのデータ型を定義する必要があります. そのため, @property で syntax に <integer> を指定しています.条件分岐 スタイルシートでは,プログラミング言語のように if 文などで条件を判定して処理を分けるようなことができません.プロパティの設定はすべて式として書かなければなりません.また,直接数値の大小を比較するような機能もありません. 2024年に W3C の Working Draft で条件によってプロパティの値を設定する機能( if() 関数)が提案されましたが,その機能が実装されても,このプログラムのように計算の結果によってプロパティの値を変えることは,どうもできなそうです.そのため,条件によって値を変えるところは,すべての条件の場合の値を計算しておいてから,そのうちのどれかが選ばれるような式を書くような形になります. たとえば --x に,ある条件を満たす場合は --n1,満たさない場合は --n2 の値を設定するという場合,条件を満たす場合に 1,満たさない場合に 0 となるようなプロパティ --b を計算しておいてと書きます. --b はプログラミング言語で言うブール変数に相当します.--b の計算の仕方は,たとえば --v が整数を表すプロパティである場合,その値が 42 に等しいかどうかを表す --b はこのように計算できます.(小数の場合はもう少し面倒です.)
--b1,--b2 を条件の真偽を表すプロパティ(1/0)とすると,論理演算は次のように計算できます.論理積(AND) 論理和(OR) 否定(NOT)このような考え方で,条件分岐的な処理を力業で書いています. | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|