スタイルシート 15 ゲーム の説明
戻る


JavaScript を使わず,スタイルシートだけで 15 ゲームを作ってみます.
全くのお遊びです.これが何か実用の役に立つようなことは多分無いでしょう.スタイルシートは本来,動作を記述するものではありませんので,かなり力ずくで作っています.
画面
(これはキャプチャ画像です.これで遊ぶことはできません.)
なお,「遊び部屋」にある他の 15 ゲームでは隣接する二つまたは三つの駒を一緒に動かせますが,このおもちゃでは動かせるのは空いているマスの隣の駒だけです.

仕組みについて説明します.


駒の位置の保持

このゲームでは,現在の駒の配置によって次に動かせる駒が決まります.そのような処理を行うには,各駒の位置を保持しておく必要があります.そのためにラジオ ボタンを使っています.
1番から 15番までの各駒に対して,16個のマス目に対応するラジオ ボタンを使います.
16 個のラジオ ボタン
1番の駒
2番の駒
3番の駒


15番の駒
チェックされているラジオ ボタンが,その駒があるマスを表します.
ただし,各ラジオ ボタンは先頭から順に 1番目のマス,2番目のマス,… というように,マスの位置と順番に対応している訳ではありません.ゲームを開始すると駒をシャッフルしますが,スタイルシートでラジオ ボタンのチェック状態を変更することはできませんので,ラジオ ボタンとマスの位置の対応を変えることでシャッフルをしています.
たとえば,ラジオ ボタンとマスの位置の対応がこのようになっている場合
1番目のラジオ ボタン5番目のマス
2番目のラジオ ボタン3番目のマス
3番目のラジオ ボタン9番目のマス


3番目のラジオ ボタンがチェックされていれば,その駒は現在 9番目のマスにあるということになります.


駒の移動

以下のような方法で,動かす駒をクリックしたときにラジオ ボタンのチェック状態が変更されるようにします.

動かせる駒は空いているマスの隣にあるもの(最大 4個)です.その駒の位置をクリックすると,駒の 16個のラジオ ボタンのうち,空いているマスの位置に対応するものがチェックされるようにします.
ラジオ ボタンは非表示にしておき,各ラジオ ボタンに対してラベル(LABEL タグ)を関連付けます.動かせる駒について,空いているマスに対応するラジオ ボタンに関連付けたラベルを駒の上に重ねます.
たとえば,駒A がマスa にあり,隣のマスb が空いているとします.
ラジオ ボタンのチェック状態変更 1
マスaマスb
駒A
マスb に対応するラジオ ボタンに関連付けたラベルをラベルb とすると,ラベルb をマスa の位置に置きます.
ラジオ ボタンのチェック状態変更 2
ラベルb をクリックすると,マスb に対応するラジオ ボタンがチェック状態になります.つまり,駒A がマスb に移動することになります.
ラジオ ボタンのチェック状態変更 3
マスaマスb
駒A
空いているマスに対応するもの以外のラベルや動かせない駒のラベルは,位置を画面外に設定してクリックできないようにします.
スタイルシートで HTML 要素をクリックできないようにするには pointer-events:none を指定するのが普通だと思いますが,pointer-events だと条件によって設定値を変える計算式で設定することができません.
Z オーダーを変えて背面に隠してもいいのですが,クリックできるラベルについては位置を設定しますので,位置と Z オーダーの両方で制御するより位置だけで制御する方が簡単です.


シャッフル

ゲームを開始すると駒をシャッフルします.駒をランダムな並びにするために乱数が必要ですが,スタイルシートには乱数を生成する関数はありませんので,自前で擬似乱数の処理を作ります.
駒をシャッフルするには,最初に駒を番号順に並べておいてから,ランダムに二つの駒を選んで入れ替えることを何回か繰り返す方法や,ランダムに駒を選んで順に並べていく方法などがありますが,何れもスタイルシートで行うには向きません.
ある処理を繰り返し実行するというようなことはできないので,やるとすれば繰り返す回数分同じ処理を書くような形になりますが,スタイルシートとしては重い処理になります.
また,ランダムに選んだ二つの駒が同じものだった場合のことも考慮する必要があります.確率は非常に低いですが,繰り返しのすべてで同じ駒が選ばれてしまう可能性もあり,その場合は全くシャッフルがされないことになります.同じ駒が選ばれた場合は,違う駒が選ばれるまでやり直すなどという処理は,スタイルシートではできません.
同様に,ランダムに駒を選んで順に並べていく方法でも,すでに選ばれている駒がまた選ばれた場合,まだ選ばれていない駒が選ばれるまでやり直すなどという処理はできません.

そのような問題を回避するため,線形合同法という擬似乱数の生成方法を使います.線形合同法で生成される乱数には比較的単純な周期性があります.それは乱数としては好ましくないことなのですが,逆にその性質を利用して,連続する数値を同じ数値が二度現れないようにランダムに並べることができます.その数値を各駒と空いているマスに対応させて,駒を重複無しにランダムに並べます.

線形合同法では,次のような漸化式で乱数列を生成します.
n+1 = (aX + c) mod m
a,c,m は定数で,mod は剰余演算を表します.X0 を乱数列の開始値として,そこから順次 X1,X2,… と乱数を求めていきます.

各パラメータを特定の規則に従って選ぶと,0 〜 m−1 の数値が重複せずにランダムに現れます.m = 16 とすれば,0 〜 15 の重複しない数値列を得られます.
たとえば a = 5,c = 1,m = 16 とし,X0 = 1 とすると,次のような乱数列が得られます.
1, 6,15,12,13, 2,11, 8, 9,14, 7, 4, 5,10, 3, 0
0 を変えると別の乱数列が得られます.ただし,X0 を変えればいくらでも違う乱数列が得られる訳ではなく,得られる乱数列のパターンは最大 16種類になります.
a や c を変えた場合も別の乱数列が得られます.a を特定の規則に従って変えると 4種類のパターンが得られます.c を特定の規則に従って変えると 8種類のパターンが得られます.
a,c,X0 の値の組み合わせで最大 512種類の乱数列を作ることができます.
4×8×16 = 512
ただし,その中には
0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13,14,15
という,数値順通りに並んでしまうもの(完成形に対応します)があるので,それを避けるために,その形が生成される X0 は除いてX0 を 15種類としています.そのため,生成される乱数列は 480種類になります.
4×8×15 = 480
このようにして 480種類の盤面を作りますが,まだこれだけでは不充分です.
ランダムに駒を並べると,50% の確率で完成できない配置になってしまいます.完成できない配置の場合は,どれか二つの駒同士を入れ替えると,完成できる配置になります.
そのため,一旦駒を並べた後,完成できる配置かどうかを調べ,完成できない配置の場合は最初の二つの駒を入れ替えます.


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

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


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

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

もうひとつは,計算結果の切り捨てのためです.
計算結果の小数部の切り捨てを行うために,整数のカスタム プロパティを定義して,計算結果を一度そのプロパティに設定することで切り捨てを行っています.
ただし,整数プロパティに小数値を設定した場合の丸めは四捨五入です.また,負の数で小数部が 0.5 の場合は負の無限大方向に丸められます(少なくとも Opera 71 では).切り捨てのために整数プロパティを使う場合は,その点を考慮して使用する必要があります.

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


条件分岐

スタイルシートでは,プログラミング言語のように if 文などで条件を判定して処理を分けるようなことができません.プロパティの設定はすべて式として書かなければなりません.また,直接数値の大小を比較するような機能もありません.
そのため,条件によって値を変えるところは,すべての条件の場合の値を計算しておいてから,そのうちのどれかが選ばれるような式を書くような形になります.C 言語の条件演算子(? :)や Visual Basic の If 演算子のようなものがあれば便利なのですが,今のところそのような機能は無いようです.

たとえば --x に,ある条件を満たす場合は --n1,満たさない場合は --n2 の値を設定するという場合,条件を満たす場合に 1,満たさない場合に 0 となるようなプロパティ --b を計算しておいて
--x:calc(var(--b) * var(--n1) + (1 - var(--b)) * var(--n2));
と書きます.
--b はプログラミング言語で言うブール変数に相当します.
--b の計算の仕方は,たとえば --v が整数を表すプロパティである場合,その値が 42 に等しいかどうかを表す --b はこのように計算できます.(小数の場合はもう少し面倒です.)
--w:calc(var(--v) - 42);
--b:calc(1 - min(var(--w) * var(--w), 1));
こんな風にも書けます.
--w:clamp(-1, var(--v) - 42, 1);
--b:calc(1 - var(--w) * var(--w));
abs() が実装されていれば,もう少し簡単に書けます.
--b:calc(1 - min(abs(var(--v) - 42), 1));
--b1--b2 を条件の真偽を表すプロパティ(1/0)とすると,論理演算は次のように計算できます.
論理積(AND)
  var(--b1) * var(--b2)
論理和(OR)
  min(var(--b1) + var(--b2), 1)
否定(NOT)
  1 - var(--b1)
このような考え方で,条件分岐的な処理を力業で書いています.


このおもちゃは Opera 71 以上用に作っていますが,Google Chrome 107 で動くことが確認できています.Google Chrome の他のバージョンでの動作は未確認です.
Firefox では,バージョン 105 の時点でまだ @property が未実装なので,動きません.


戻る