スタイルシート フリーセル の説明
戻る


JavaScript を使わず,スタイルシートだけでフリーセルを作ってみます.
全くのお遊びです.これが何か実用の役に立つようなことは多分無いでしょう.スタイルシートは本来,動作を記述するものではありませんので,かなり力ずくで作っています.

画面
(これはキャプチャ画像です.これで遊ぶことはできません.)

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


使い方

「ゲーム開始」をクリックするとカードが並べられます.

最初は 7 枚または 6 枚のカードが 8 列に並んでいます.その部分を以下の説明では「列」といいます.
画面の左上と右上にカードを置ける場所があります.左上がフリー セル,右上がホーム セルです.
実物のカードでプレイする場合は,カードをインデックスが見えるようにずらした状態で重ねて場に置きますが,このおもちゃではカードを並べて表示しています.画面位置で下の方に表示されるカードが上に重なっているカードになります.

下記の操作で,列またはフリー セルにあるカードを選択して,それを列,フリー セルまたはホーム セルに移動します.
動かせるのは選択したカードだけです.ある列にある連続した複数のカードをそのままの順序で他の列に移動できる場合でも,1 枚ずつ移動する必要があります.
また,ホーム セルに移動できるカードがあっても自動的に移動することはありません.手動で移動してください.

移動するカードをクリックして選択します.
カードの選択
列にあるカードは,そのカードの上でなくても,列のどこをクリックしても選択できます.フリー セルにあるカードは,そのカードをクリックします.

移動先をクリックするとそこにカードが移動します.
列に移動するときは,列のどこをクリックしても移動できます.フリー セルまたはホーム セルに移動するときは,フリー セル/ホーム セルの領域のどこをクリックしても移動できます.
フリー セルのカードは左から順に詰めて置かれます.ホーム セルのカードはスート(スペード,クラブ など)によって決められた場所に置かれます.

移動できる場所にマウス ポインタを持っていくと,クリックできる範囲が赤色の枠で示されます.
列のクリックできる範囲
フリー セルのクリックできる範囲
ホーム セルのクリックできる範囲

選択したカードをもう一度クリックすると選択を取り消すことができます.
列にあるカードについては,列のどこをクリックしても取り消すことができます.フリー セルにあるカードについては,フリー セルの領域のどこをクリックしても取り消すことができます.

「やり直し」で最初の状態に戻せます.

クリアすると「もう一度」ボタンが表示されます.ボタンをクリックすると,またプレイできます.


問題の選択

問題はあらかじめ用意したパターンの中から自動で選びます.問題は 100 個用意しています.

次のように,カードの初期位置に 0 〜 51 の数値を対応させます.
カード位置の数値
問題のカードの位置に応じて,この数値をカードに割り当てます. たとえば,スペードの A に 21 を割り当てると,スペードの A は左から 6 列目の上から 3 段目に配置されます.
各問題の各カードについてその割り当てを定義します.
各カードの位置の数値を設定するカスタム プロパティ(52 個)を用意して,そのプロパティに問題のカードの位置を設定します.
スタイルシートのコードを短くするため,カスタム プロパティの“--”の後の名前は 1 文字にしています.このように‘A’〜‘Z’と‘a’〜‘z’の 52 文字を 52 枚のカードに対応させています.
--A--Mスペードの A 〜 K
--N--Zクラブの A 〜 K
--a--mハートの A 〜 K
--n--zダイヤの A 〜 K
設定は,たとえばこのようになります.
プロパティ カード 位置
--A:21スペードの A6 列目の 3 段目
--B:16スペードの 21 列目の 3 段目
--C:32スペードの 31 列目の 5 段目


このような定義を 100 組作って,どれを使うかをアニメーションの機能で選択します.
100 段階のステップのアニメーションを行い,各段階のキーフレームでプロパティを設定します.
@keyframes random {
  0%, 100% {
    --A:21;--B:16;--C:32;--D:34;--E:11;--F:48;--G:31;--H:41;--I:10;--J:26;--K:20;--L:43;--M:17;
    --N:29;--O:37;--P:23;--Q:24;--R:25;--S:44;--T: 6;--U:46;--V:49;--W:47;--X: 3;--Y:13;--Z: 9;
    --a:22;--b:50;--c:15;--d:28;--e: 7;--f:51;--g: 5;--h:36;--i: 2;--j:35;--k:38;--l:27;--m:14;
    --n:12;--o: 1;--p:45;--q:30;--r: 4;--s:40;--t:39;--u:42;--v:18;--w:33;--x: 0;--y:19;--z: 8;
  }
  1% {
    --A:48;--B:35;--C:36;--D:21;--E:49;--F:28;--G:24;--H:20;--I:33;--J:37;--K:10;--L:12;--M:26;
    --N:30;--O: 5;--P: 3;--Q: 4;--R: 7;--S:19;--T:27;--U:18;--V:15;--W:34;--X:17;--Y: 1;--Z: 2;
    --a:32;--b:39;--c:40;--d:29;--e:44;--f:11;--g:42;--h:31;--i:38;--j:50;--k:51;--l:23;--m:46;
    --n:41;--o:14;--p:43;--q: 8;--r:22;--s:13;--t:47;--u:45;--v:25;--w:16;--x: 9;--y: 0;--z: 6;
  }
  2% {
    --A:25;--B:34;--C:39;--D:49;--E:50;--F: 4;--G:38;--H:13;--I:40;--J:18;--K:11;--L:12;--M:21;
    --N:28;--O:41;--P:35;--Q:43;--R:51;--S:32;--T:22;--U:48;--V:14;--W:16;--X:46;--Y: 2;--Z: 0;
    --a:44;--b:45;--c:42;--d:24;--e:47;--f:20;--g: 3;--h: 5;--i:36;--j: 6;--k: 9;--l:17;--m:31;
    --n:23;--o: 1;--p:30;--q: 7;--r:19;--s:29;--t: 8;--u:27;--v:10;--w:33;--x:26;--y:15;--z:37;
  }

        ・
        ・
        ・

  99% {
    --A:23;--B:29;--C:35;--D: 2;--E:51;--F:24;--G:18;--H:48;--I:50;--J: 9;--K: 1;--L:16;--M:33;
    --N:22;--O:40;--P:32;--Q:37;--R:10;--S:26;--T: 4;--U:43;--V:20;--W:36;--X:34;--Y: 3;--Z:49;
    --a:15;--b:41;--c:39;--d:12;--e:25;--f:21;--g:31;--h:19;--i:30;--j:17;--k: 6;--l: 8;--m:42;
    --n: 0;--o:47;--p:27;--q:28;--r:38;--s: 5;--t:44;--u:45;--v:11;--w: 7;--x:13;--y:46;--z:14;
  }
}


animation:random 1s steps(100, jump-start) infinite paused;
初期状態のとき(HTML をロードした直後),およびゲームをクリアしたときにアニメーションを動かし,ゲームを開始したらアニメーションを停止して,そのときのプロパティの値でカードを配置します.ゲームを開始するタイミングによって問題がランダムに選ばれます.

初期状態で動かしたアニメーションを停止する処理のためにチェック ボックスを使います.チェック ボックスは非表示にしておき,それにラベル(LABEL 要素)を関連付けて,「ゲーム開始」ボタンを作っています.
チェック ボックスの :not(:checked) 状態でアニメーションを実行するようにしておきます.ボタンをクリックするとチェック ボックスが :not(:checked) 状態でなくなるので,アニメーションが停止します.

ゲームをクリアしたときというのは,4 つのスートの K がすべてホーム セルに置かれたときなので,その条件でアニメーションを実行するようにしておきます.後述のように,「もう一度」をクリックすると K も含めてすべてのカードが列に戻るので,アニメーションが停止します.


カードのデータの持ち方

各カードに対応して以下のようなデータを持ちます.
・ 列の位置
カードが何列目にあるかを表す数値を次のように設定します.
11 列目
22 列目
43 列目
84 列目
165 列目
326 列目
647 列目
1288 列目
つまり,ビット フラグで設定します.
1 〜 8 のような数値でなくビット フラグにしているのは,その方が各列の最後のカードのデータを調べる処理が簡単になるためです.
カードがフリー セル/ホーム セルにあるときは,このデータは使わないので 0 を設定します.
・ 初期の並び順
カードの初期配置では各列に 7 枚または 6 枚のカードが並びます.初期配置のまま移動していないカードについては,その並び順を 0 〜 6 または 5 で設定します.
移動したカードについては,列にあるときは 7 を設定します.フリー セル/ホーム セルにあるときは,このデータは使わないので 0 を設定します.
・ ランクに対応する数値
ランク(A,2,3,…)の A 〜 K に対して 13 〜 1 を設定します.
数値を 13 → 1 と降順にしているのは,移動して列に並べる順が K → A なので,並べたときに数値が昇順になるようにしているためです.
・ 色
カードの色を 0(黒)/1(赤)で設定します.
カード毎にひとつ,整数のカスタム プロパティを用意し,それを複数のビット フィールドに分割してこのデータを格納します.
ビット位置 データ
0
1 〜 4ランクに対応する数値
5 〜 7初期の並び順
8 〜 15列の位置

(ビット位置は下位から上位に向かって 0 → 15)
データの種類毎に別のプロパティを使わないのは,複数のプロパティを使うと各プロパティを紐付けるのが難しくなるためです,


カードの並べ方

カードを並べる場はグリッド レイアウトで作っています.カードがグリッドのアイテムです.また,後述のようにフリー セル/ホーム セルの背景もグリッドのアイテムにしています.

列にカードを移動すると,その列にすでにあるカードの次の位置に置かれます.その動作をさせるためにグリッド レイアウトの自動配置の機能を使っています.
grid-column で列を指定して grid-row は指定しないと,該当の列に順番にカードが並んで配置されます.並び順は order で指定した順になります.下記のように,列にあるカードについては,上述のカードのデータを order に設定しています.

カードが列,フリー セル,ホーム セルのどこにあるかによって,以下のようにグリッド上のカードの位置を設定します.

カードが列にある場合

カードのデータの「列の位置」に従って grid-column を設定し,grid-row は設定しません.また,カードのデータ全体を order に設定します.
同じ列にあるカードの「列の位置」はすべて同じなので,「列の位置」の部分は並び順に影響しません.「初期の並び順」以下の内容により並び順が決まります.
移動したカードの「初期の並び順」は 7 で,初期配置されたカードの「初期の並び順」より大きいので,移動したカードは初期配置のままのカードより後に並びます.移動したカードの「初期の並び順」はすべて同じなので,移動したカードの並び順は「ランクに対応する数値」で決まります.
初期配置のままのカードについては,ひとつの列の中では「初期の並び順」はすべて異なるので,「ランクに対応する数値」,「色」は並び順には影響しません.
移動したカードについては,ひとつの列の中では「ランクに対応する数値」はすべて異なる(1 ずつ増えていく)ので,「色」は並び順には影響しません.

列にあるカードの並びの例

order(=カードのデータ)の値は,たとえば上のハート 7 のカードの場合だと
32×28+0×25+7×2+1 = 8207
のように計算されます.
グリッドの 1 行目はフリー セルとホーム セルに使うので,列にあるカードは 2 行目から並べるようにします.
フリー セルとホーム セルにはカードの置き場所を示す背景を表示するので,それをグリッドのアイテムとして 1 行目の 1 〜 4 列目と 5 〜 8 列目に置くことで,列にあるカードが 1 行目に置かれないようにしています.

以上の結果として,まず初期配置から移動していないカードが 2 行目からその配置順に並び,その後に移動したカードがランク順(降順)に並ぶことになります.

カードがフリー セルにある場合

grid-row に 1 を設定し,grid-column は設定しません.order には 0 を設定します.
カードはグリッドの 1 行目に横並びに並びます.
1 行目の 1 列目から 8 列目までは上述のフリー セルとホーム セルの背景が置かれているので,カードは 9 列目から並びます.それを left で位置をずらして,フリー セルの場所に表示しています.

カードがホーム セルにある場合

grid-row に 1,grid-column に 5 を設定します.order には 0 を設定します.
カードはグリッドの 1 行目の 5 列目に重ねて置かれます.
order がすべて同じで z-index も指定していないので,カードは HTML で記述された順に重なります.それをカードのスートに応じて left で位置をずらして,ホーム セルの場所に表示しています.


カード位置の保持

移動したカードについて,その位置を保持しておくためにラジオ ボタンを使っています.
各カードについて 8 個の列とフリー セル,ホーム セルに対応するラジオ ボタンをを使って,チェックされているラジオ ボタンでカードがある位置を表します.
ラジオ ボタンは非表示にしておき,各ラジオ ボタンに対してラベルを関連付けて対応する位置に置きます.
位置を保持するラジオ ボタンのラベル
ラベルをクリックすると対応するラジオ ボタンが :checked 状態になるので,後述の「カードの選択と移動」のように,該当の位置にカードを移動する処理を行います.
ゲーム開始時はどのラジオ ボタンもチェックされていないので,カードの位置は初期配置の位置になります.

列内の位置,フリー セルの領域やホーム セルの領域の中の位置は,上述の「カードの並べ方」により決まります.


カードの選択と移動

カードを選択する処理のために,各カードについてもうひとつラジオ ボタンを使います.
ラジオ ボタンは非表示にしておき,各ラジオ ボタンに対してラベルを関連付けて,それをカードに付けています.
カードをクリックすると対応するラジオ ボタンが :checked 状態になるので,以下のようにして,そのカードを移動できるようにします.

選択したカードを各列,フリー セル,ホーム セルに移動できるかどうかを判定します.
それぞれの場所に移動できる条件は以下のようになります.
【列に移動できる条件】

該当の列の最下行のカードが,移動しようとするカードよりランク(A<K)がひとつ大きく,色が異なる.
または該当の列にカードが 1 枚も無い.

【フリー セルに移動できる条件】

フリー セルに空きがある.

【ホーム セルに移動できる条件】

ホーム セルに,移動しようとするカードとスートが同じでランク(A<K)がひとつ小さいカードがすでにある.
フリー セルに移動できる条件とホーム セルに移動できる条件は,ラジオ ボタンのチェック状態で容易に判定できますが,列に移動できる条件を判定するためには,該当の列の最下行のカードのデータを調べる必要があります.
列が同じカードの中でデータ(= order)の数値が一番大きいものが最下行のカードなので,その列にあるカードのデータの最大値を求めます.

カードの位置を保持するラジオ ボタンに対応するラベルについて,移動できる位置に対応するものは前面になり,移動できない位置に対応するものは背景の背面になってクリックできなくなるように,Z オーダーをセットします.
前面にあるラベルをクリックすると,そのラベルに対応する位置が新しい位置になります.

カードの選択用のラジオ ボタンは位置を保持するためのラジオ ボタンと同じグループにしている(NAME 属性を同じにしている)ので,移動先をクリックすると選択用のラジオ ボタンはチェック状態でなくなり,選択の状態は自動的に解除されます.
これにより,選択→移動→選択→移動→ … という操作を連続して行えるようになります.

選択用のラジオ ボタンと位置を保持するラジオ ボタンを同じグループにすると,逆に,選択用のラジオ ボタンをチェックすると位置を保持するラジオ ボタンがチェック状態でなくなることになります.そのままでは位置が不定になってしまうので,移動先を指定するまでの間は後述の「カード選択中のカード位置の保持」の方法で位置を保持しています.


カード選択中のカード位置の保持

カードを選択してから移動先を指定するまでの間カードの位置を保持しておくために transition の機能を流用します.
transition では,プロパティの設定値が変更されてから実際に値が変わり始めるまでのディレイを設定できます.値を保持したいプロパティに,このようにディレイを設定します.
transition:left 0s 10000s;
そうすると,プロパティの設定が変わってもディレイで指定した時間が経過するまでは元の値が保持されます.ディレイに充分大きな値を指定することで,カードを選択したときのプロパティの値を保持します.

値を保持するプロパティはカードのデータを格納するカスタム プロパティと,カードの要素のプロパティのうち以下のものです.
  • left
  • grid-row-start
  • grid-column-start
  • order
実際の処理では,このように transition-property だけを変化させています.
基本の設定
transition-property:none;
transition-duration:0s;
transition-delay:10000s;
↓ カードを選択
カードのデータを格納するカスタム プロパティについての設定
transition-property:--psa, --ps2, --ps3,
カードの要素のプロパティについての設定
transition-property:left, grid-row-start, grid-column-start, order;
カスタム プロパティに transition を適用するために,後述のように @ルールの @property でプロパティをアニメーション可能に設定しています.


もう一度/やり直し

ゲームをクリアした後もう一度プレイする処理と,ゲームを最初からやり直す処理を,以下のように作ります.

カードの位置を保持するラジオ ボタンをすべてフォームの中に入れ,フォームにリセット ボタン(TYPE 属性 RESET の INPUT 要素)を付けます.リセット ボタンは非表示にしておき,それにラベルを関連付けて,「もう一度」ボタンと「やり直し」ボタンを作っています.
ゲームのプレイ中は「やり直し」を表示しておき,クリアしたら「もう一度」を表示します.

どちらのボタンをクリックした場合も,フォームがリセットされてラジオ ボタンが初期の状態に戻るので,カードはすべて初期の配置になります.
「もう一度」の場合は,問題を選択するアニメーションが停止し,カードは新たに選択された問題の配置になります.
「やり直し」の場合は,新しい問題を選択するアニメーションは実行されていないので,カードは同じ問題の配置になります.つまり,同じ問題のやり直しになります.


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

このプログラム(?)では,いくつかのカスタム プロパティを @ルールの @property で定義しています.
@property の定義は,たとえばこのように書きます.
@property --a {
  syntax:'<integer>';
  inherits:true;
  initial-value:0;
}
上述したように,カスタム プロパティの値を保持するために transition を使っています.カスタム プロパティに transition を使うためにはプロパティのデータ型を定義する必要があります.
@propertysyntax<integer> を指定することで,そのプロパティに transition を使えるようにしています.


条件分岐

スタイルシートでは,プログラミング言語のように if 文などで条件を判定して処理を分けるようなことができません.プロパティの設定はすべて式として書かなければなりません.また,直接数値の大小を比較するような機能もありません.
2024年に W3C の Working Draft で条件によってプロパティの値を設定する機能(if() 関数)が提案されましたが,その機能が実装されても,このプログラムのように計算の結果によってプロパティの値を変えることは,どうもできなそうです.
そのため,条件によって値を変えるところは,すべての条件の場合の値を計算しておいてから,そのうちのどれかが選ばれるような式を書くような形になります.

たとえば --x に,ある条件を満たす場合は --n1,満たさない場合は --n2 の値を設定するという場合,条件を満たす場合に 1,満たさない場合に 0 となるようなプロパティ --b を計算しておいて
--x:calc(var(--b) * var(--n1) + (1 - var(--b)) * var(--n2));
と書きます.
--b はプログラミング言語で言うブール変数に相当します.
--b の計算の仕方は,たとえば --v が整数を表すプロパティである場合,その値が 42 に等しいかどうかを表す --b はこのように計算できます.(小数の場合はもう少し面倒です.)
--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)
このような考え方で,条件分岐的な処理を力業で書いています.


戻る