スタイルシート 三目並べ の説明
戻る


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

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

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


使い方

「先手」/「後手」でプレイヤーが先手になるか後手になるかを選びます.
「弱い」/「強い」でコンピュータの強さを選びます.
「スタート」をクリックするとゲームを開始します.

先手が ○,後手が × です.「あなた」/「コンピュータ」でプレイヤーとコンピュータのどちらの手番かが示されます.手番の側が強調表示されます.
プレイヤーの手番のときに,取りたいマスをクリックして ○/× を付けます.

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


ゲーム開始

ゲーム開始時の処理を行うためにチェック ボックスを使います.チェック ボックスは非表示にしておき,それにラベル(LABEL 要素)を関連付けて,「ゲーム開始」ボタンを作っています.
チェック ボックスの :checked 状態で,後述の乱数を作る処理やプレイヤーの 1 手目を判定するチェック ボックスのラベルの表示,コンピュータが先手のときのコンピュータの 1 手目を決める処理などを行います.


プレイヤーが取ったマスの判定

プレイヤーが取ったマスをチェック ボックスで判定します.

9 個のマスに対応して 9 個のチェック ボックスを使います.チェック ボックスは非表示にしておき,各チェック ボックスに対してラベルを関連付けて,それをマスの位置に置いています.
そのような 9 個のチェック ボックスの組を 5 組用意します.
5 組というのは,プレイヤーがマスを取る回数が最大 5 回なので,そのそれぞれの手番で取った位置を保持するために 5 組必要ということです.
プレイヤーが後手の場合は,プレイヤーがマスを取る回数は最大 4 回なので,4 組だけを使います.

マスをクリックすると対応するチェック ボックスが :checked 状態になるので,次の組のラベルの表示(下記),コンピュータの次の手を決める処理などを行います.

各組のチェック ボックスに対応するラベルは最初は非表示にしておき,チェック ボックスのチェック状態に合わせて順番に表示します.
「スタート」をクリックする.
1 組目のラベルを表示する.
1 組目のラベルのどれかをクリックする.
1 組目のラベルを非表示にし,2 組目のラベルを表示する.
2 組目のラベルのどれかをクリックする.
2 組目のラベルを非表示にし,3 組目のラベルを表示する.


ただし,すでにマークがあるマスのラベルは Z オーダーの設定で背面に隠して,クリックできないようにします.


コンピュータの手の決め方

コンピュータが取るマスを以下のような方法で決めます.

各マスにこのように番号を付けます.
マスの番号
番号は単純に 1 〜 9 などでも構わないのですが,この方がマスの位置が多少判りやすくなるので,このようにしています.
この番号の数値に,取るマスの優先順位と乱数で重みを付けます.
重みづけした数値(最大 5 桁)
重みづけした数値
下 2 桁がマス番号です.その上の 2 桁が乱数で,一番上の 1 桁が優先順位を表す数値です.
max() 関数を使って,その中から最大のものを選びます.優先順位が最大の数値が選ばれます.優先順位が同じ数値が複数あるときは,乱数が一番大きいものが選ばれます.
乱数は,優先順位が同じマスが複数あるときに,いつも同じマスが選ばれてしまわないようにするために付加しています.
選ばれた数値のマス番号の部分を取り出して,そのマスを取る処理を行います.

優先順位はこのように付けます.

優先順位が一番高いのは,そのマスを取るとコンピュータが勝つマスです.
その次に優先順位が高いのは,プレイヤーがそのマスを取るとプレイヤーが勝つマスです.プレイヤーの勝ちを防ぐために,そのマスを優先的に取ります.

この 2 つについては,乱数は使っていません.
この条件を満たすマスが 2 つある場合はありますが,そのときはもうゲーム終了になるので,そこまでランダム化しなくてもよいかなと思います.
そのマスを取るとコンピュータが勝つマスが 2 つあれば,どちらを取ってもそこでゲーム終了ですし,プレイヤーがそのマスを取ると勝つマスが 2 つあるときは,どちらかを防いでも次の 1 手でゲーム終了です.

上記の他に,単純にマスの位置によって優先順位を付けます.
マスの位置による優先順位は上記の優先順位より低いので,上記に該当するマスがある場合は,そのマスが選ばれます.
マスの位置による優先順位はコンピュータの強弱によって変えます.

コンピュータが強い場合,上記の 2 つの次に優先順位が高いのは中央のマスです.次いで角の 4 つのマスです.
残りのマスは最低の優先順位になります.
コンピュータが弱い場合は,どの位置のマスも同じ優先順位にします.

このようなマスの選び方を基本にしますが,コンピュータが強い場合は,以下のように一部これとは違う選び方をしています.

コンピュータが先手の場合の 1 手目では,角と中央の 5 つのマスを同じ優先順位で選びます.

プレイヤーが先手の場合のコンピュータの 1 手目では,まだプレイヤーが 1 マス取っただけの状態なので,基本の選び方より少し簡単な計算方法にしています.

プレイヤーが先手で,コンピュータのマークが斜めに挟まれた場合,基本のマスの選び方では角を取ることになりますが,それだと負けるので,この場合は角でないマスを取ります.
斜めに挟まれた状態

もう一度プレイする

ゲーム終了後もう一度プレイする処理を以下のように作ります.

ゲーム開始のチェック ボックスとプレイヤーが取ったマスを判定するチェック ボックスをフォームに関連付けます.プレイヤーが取ったマスを判定するチェック ボックスはフォームの中に入れます.ゲーム開始のチェック ボックスはフォームの外に置きますが,FORM 属性でフォームに所属させます.
フォームにリセット ボタン(TYPE 属性 RESET の INPUT 要素)を付けます.リセット ボタンは非表示にしておき,それにラベルを関連付けて,「もう一度」ボタンを作っています.

ボタンをクリックするとフォームがリセットされて,チェック ボックスが初期の状態に戻るので,ゲーム開始前の状態に戻ります.

ゲーム開始のチェック ボックスをフォームの外に置いているのは,ゲーム開始で先手/後手の選択とコンピュータの強さの選択のスタイルを変えて,選択を変更できないようにするためです.そのためにはゲーム開始のチェック ボックスの方が HTML 上で先に置かれている必要があります.
先手/後手の選択とコンピュータの強さの選択は,もう一度プレイするときもそのままの状態を保持しておきたかったので,フォームの外に置きました.その都合でゲーム開始のチェック ボックスもフォームの外になりました.


乱数の作り方

上述の処理を行うために乱数が必要です.
2024年に W3C の Working Draft で乱数に関する機能が提案されましたが,このおもちゃを作った時点では,まだ対応しているブラウザは無いようです.
このおもちゃでは以下のような方法で乱数を作ります.

乱数は,優先順位が同じマスが複数あるときに,いつも同じマスが選ばれてしまわないようにするためのものです.選ばれ方がある程度ばらけてくれれば充分なので,品質のよい乱数である必要はありません.そのため,線形合同法という擬似乱数発生方法のうちの乗算合同法という簡便な方法を使っています.

擬似乱数のシードをランダムに選ぶため,アニメーションの機能を使います.
シードの値を表すカスタム プロパティを定義しておいて,その値が変化するアニメーションを無限に繰り返すように設定します.そして,「スタート」をクリックした時点でアニメーションを停止し,そのときのプロパティの値を使って乱数生成を行います.こうすれば,「スタート」をクリックするタイミングによってシードが変わります.


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

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

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


条件分岐

スタイルシートでは,プログラミング言語のように 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)
このような考え方で,条件分岐的な処理を力業で書いています.


戻る