SCXML 数独
SCXML を使用した、別ファイルの JavaScript の活用例を示します。
例の動作確認
以下の場所からサンプルを実行できます:
- Qt Creator
Welcome モードを開き、Examples からサンプルを選択します。詳細については、Qt Creator の「チュートリアル:ビルドと実行」を参照してください。
- Qt Extension for Visual Studio Code
Command Palette から `Qt: Open Qt examples ` コマンドを実行し、リストからサンプルを選択します。詳細については、Qt Extension for Visual Studio Code の「チュートリアル:ビルドと実行」を参照してください。```
数独の機能

当サイトの数独には、以下の機能が含まれています:
- ゲーム開始時および終了時、数独は「
idle」状態に入ります。この状態では、プレイヤーは前回のゲームが正常に終了したかどうかを確認できます。その後、ステートマシンは「idle」状態の2つの子状態のいずれか、すなわち「solved」または「unsolved」に移行します。「idle」状態では、プレイヤーは解きたい数独のグリッドを選択することもできます。 この状態では、数独のグリッドは使用不可となっており、ユーザーによる操作は無視されます。 - プレイヤーが「Start 」ボタンをクリックすると、数独は「
playing」状態になり、ボード上でのユーザーの操作を受け付ける準備が整います。 - ゲームが「
playing」状態にあるときに、プレイヤーが「Stop 」ボタンをクリックすると、ゲームは終了し、「idle」状態の子状態である「unsolved」状態に移行します。プレイヤーが現在のパズルを正しく解いた場合、ゲームは自動的に終了し、成功を示す「idle」状態の子状態である「solved」状態に移行します。 - ボードは9×9のグリッド上に配置された81個のボタンで構成されています。初期値が設定されているボタンは、ゲーム中は無効なままです。プレイヤーは、初期状態が空のボタンのみ操作できます。ボタンを1回クリックするごとに、その値が1ずつ増加します。
- ゲーム中は、プレイヤーの利便性を考慮して「Undo 」ボタンが利用可能です。
SCXML パート:内部ロジックの記述
sudoku.scxmlファイルは、数独ゲームが取り得る状態の内部構造を記述し、状態間の遷移を定義するとともに、遷移が発生した際に適切なスクリプト関数を呼び出します。また、イベントを送信したり、発生予定のイベントを監視してそれに対応したりすることで、GUI 部分と通信を行います。
ECMAScriptのデータモデルを使用しています:
<scxml xmlns="http://www.w3.org/2005/07/scxml" version="1.0"
name="Sudoku" datamodel="ecmascript">以下の変数を宣言します:
<datamodel>
<data id="initState"/>
<data id="currentState" expr="[[]]"/>
<data id="undoStack"/>
</datamodel>| 変数 | 説明 |
|---|---|
initState | 現在のゲームの初期状態を保持します。これは、数独の初期数字が格納された9×9のセルからなる2次元配列です。値が0の場合は、そのセルが初期状態で空であることを意味します。 |
currentState | 進行中のゲームの現在の状態を保持します。initState 変数と類似しており、初期状態では同じ内容を含んでいます。ただし、プレイヤーが空のマスに数字を入力し始めると、この変数はそれに応じて更新されますが、initState 変数は変更されません。 |
undoStack | プレイヤーの操作履歴を保持します。これは、最後にタッチされたセルの座標のリストです。ゲーム中の新しい変更が行われるたびに、x座標とy座標のペアがそのリストに追加されます。 |
上記の変数は、sudoku.js ファイルで定義されているスクリプトヘルパー関数と共有されています:
<script src="sudoku.js"/>遷移を行う際や、GUIから送信されたイベントに応答する際、そこで定義されている関数のいくつかを呼び出します。
前述のすべての状態は、ルート状態であるgame に定義されています。
<state id="game">
<onentry>
<raise event="restart"/>
</onentry>
<state id="idle">
...
<state id="unsolved"/>
<state id="solved"/>
</state>
<state id="playing">
...
</state>
...
</state>数独のサンプルが起動されると、ステートマシンはgame 状態に入り、アプリケーションが終了するまでこの状態にとどまります。この状態に入ると、内部でrestart イベントが発生します。このイベントは、プレイヤーが現在の数独のグリッドを変更したときや、Start ボタンを押してゲームを開始したときにも発生します。 現在のゲームが終了した時点では、前回のゲームプレイで埋まった盤面を表示し続けたいので、このイベントを送信したくありません。そのため、このイベントは 3 つの異なるコンテキストから発生し、game 状態のターゲットのない遷移において、内部で 1 回キャプチャされます:
<transition event="restart">
<script>
restart();
</script>
<raise event="update"/>
</transition>restart イベントをキャッチすると、sudoku.js ファイルで定義されているヘルパースクリプトメソッドrestart() を呼び出し、内部で追加のupdate イベントを発生させます。
function restart() {
for (var i = 0; i < initState.length; i++)
currentState[i] = initState[i].slice();
undoStack = [];
}restart() 関数は、initState をcurrentState 変数に代入し、undoStack 変数をクリアします。
update イベントは、グリッドの内容が変更されたことをGUIに通知し、渡された値に基づいてGUIが自身を更新する必要がある場合に、内部で発生します。このイベントは、game 状態の別のターゲットレス遷移で捕捉されます:
<transition event="update">
<send event="updateGUI">
<param name="currentState" expr="currentState"/>
<param name="initState" expr="initState"/>
</send>
</transition>外部イベント `updateGUI` を送信すると、C++ コード内でこれがインターセプトされます。`updateGUI ` イベントには、`<param> ` 要素内で指定された追加データが添付されています。2つのパラメータが渡され、これらは外部から `currentState ` および `initState ` という名前でアクセス可能です。これらに渡される実際の値は、それぞれデータモデルの `currentState ` および `initState ` 変数に等しく、これらは `expr ` 属性によって指定されています。
<state id="idle">
<transition event="start" target="playing"/>
<transition event="setup" target="unsolved">
<assign location="initState" expr="_event.data.initState"/>
<raise event="restart"/>
</transition>
<state id="unsolved"/>
<state id="solved"/>
</state>idle 状態にある場合、GUI側から送信される可能性のある2つのイベント(start およびsetup )に応答します。start イベントを受信した際は、単にplaying 状態へ遷移します。setup イベントを受信した際は、GUI側から新たなグリッドが送信されたものとみなします。 グリッドの新しい初期状態は、_event.data のinitState フィールドを通じて渡されることが想定されています。渡された値をデータモデルで定義されたinitState 変数に代入し、グリッドの内容を再起動します。
<state id="playing">
<onentry>
<raise event="restart"/>
</onentry>
<transition event="tap">
<if cond="isValidPosition()">
<script>
calculateCurrentState();
</script>
<if cond="isSolved()">
<raise event="solved"/>
</if>
<raise event="update"/>
</if>
</transition>
...
</state>playing 状態に入るたびに、前回のゲームプレイのコンテンツがまだ表示されている可能性があるため、グリッドのコンテンツをリセットします。playing 状態では、GUIから送信される可能性のあるイベント(tap 、undo 、stop )に対応します。
tap イベントは、プレイヤーが有効な数独のマスのいずれかを押したときに送信されます。このイベントには、マスの座標を指定する追加データが含まれていることが想定されており、そのデータは_event.data のx およびy フィールドを通じて渡されます。まず、isValidPosition() スクリプト関数を呼び出して、渡された座標が有効かどうかを確認します:
function isValidPosition() {
var x = _event.data.x;
var y = _event.data.y;
if (x < 0 || x >= initState.length)
return false;
if (y < 0 || y >= initState.length)
return false;
if (initState[x][y] !== 0)
return false;
return true;
}座標が負の値でもなく、グリッドの範囲を超えないことを確認します。さらに、グリッド記述で最初に指定されたセルは変更できないため、座標が初期状態では空のセルを指していないかどうかも確認します。
渡された座標が正しいことを確認したら、calculateCurrentState() スクリプト関数を呼び出します:
function calculateCurrentState() {
if (isValidPosition() === false)
return;
var x = _event.data.x;
var y = _event.data.y;
var currentValue = currentState[x][y];
if (currentValue === initState.length)
currentValue = 0;
else
currentValue += 1;
currentState[x][y] = currentValue;
undoStack.push([x, y]);
}この関数は、渡されたグリッドのセルの値をインクリメントし、新しい移動を元に戻すスタックの履歴に追加します。
calculateCurrentState() 関数の実行が完了した直後、isSolved() スクリプト関数を呼び出して、グリッドがすでに解かれているかどうかを確認します:
function isOK(numbers) {
var temp = [];
for (var i = 0; i < numbers.length; i++) {
var currentValue = numbers[i];
if (currentValue === 0)
return false;
if (temp.indexOf(currentValue) >= 0)
return false;
temp.push(currentValue);
}
return true;
}
function isSolved() {
for (var i = 0; i < currentState.length; i++) {
if (!isOK(currentState[i]))
return false;
var column = [];
var square = [];
for (var j = 0; j < currentState[i].length; j++) {
column.push(currentState[j][i]);
square.push(currentState[Math.floor(i / 3) * 3 + Math.floor(j / 3)]
[i % 3 * 3 + j % 3]);
}
if (!isOK(column))
return false;
if (!isOK(square))
return false;
}
return true;
}isSolved() 関数は、グリッドが正しく解かれている場合、true を返します。各行、各列、および各3x3の正方形を個別にチェックする必要があるため、isOK() ヘルパー関数を定義します。この関数は数値のリストを受け取り、渡されたリストに含まれる数値がすべて一意であり、かつ0に等しい数値がない場合(つまり、空のセルがない場合)、true を返します。isSolved() のメインループは9回呼び出されます。各反復処理で、グリッドの行、列、および3×3の正方形を表す3つの数値リストを構築し、それらに対してisOK() を呼び出します。27個すべてのリストがOKの場合、グリッドは正しく解かれており、true を返します。
SCXMLファイルの話に戻ると、isSolved() がtrue を返した場合、内部でsolved イベントを発生させます。移動が正常に行われた場合の最後の処理は、グリッドの変更をGUIに通知する必要があるため、update イベントを発生させることです。
<state id="playing">
...
<transition event="undo">
<script>
undo();
</script>
<raise event="update"/>
</transition>
<transition event="stop" target="idle"/>
<transition event="solved" target="solved"/>
</state>playing の状態にあるときは、GUIから送信されるundo イベントにも反応します。この場合、undo() スクリプト関数を呼び出し、更新が必要であることをGUIに通知します。
function undo() {
if (!undoStack.length)
return;
var lastMove = undoStack.pop();
var x = lastMove[0];
var y = lastMove[1];
var currentValue = currentState[x][y];
if (currentValue === 0)
currentValue = initState.length;
else
currentValue -= 1;
currentState[x][y] = currentValue;
}undo() 関数は、履歴に最後の移動があった場合、それを削除し、その移動から取得した座標で指定されるセルの現在の値を1減らします。
playing 状態は、プレイヤーが「Stop 」ボタンを押した際にGUIから送信されるstop イベントにも対応しています。この場合、単にidle 状態をアクティブにします。
さらに、内部から送信されるsolved イベントをインターセプトし、その場合はsolved 状態をアクティブにします。
C++ パート:GUI の構築
アプリケーションのC++部分は、GUIを構築し、それをSCXML部分と連携させるMainWindow クラスで構成されています。このクラスはmainwindow.hで宣言されています。
class MainWindow : public QWidget
{
Q_OBJECT
public:
explicit MainWindow(QScxmlStateMachine *machine, QWidget *parent = nullptr);
private:
QScxmlStateMachine *m_machine = nullptr;
QList<QList<QToolButton *>> m_buttons;
QToolButton *m_startButton = nullptr;
QToolButton *m_undoButton = nullptr;
QLabel *m_label = nullptr;
QComboBox *m_chooser = nullptr;
};MainWindow クラスは、QScxmlStateMachine *m_machine へのポインタを保持しています。 は、sudoku.scxml ファイルからQtによって自動的に生成されたステートマシンクラスです。また、このクラスはいくつかのGUI要素へのポインタも保持しています。
MainWindow::MainWindow(QScxmlStateMachine *machine, QWidget *parent) :
QWidget(parent),
m_machine(machine)
{MainWindow クラスのコンストラクタは、アプリケーションのGUI部分をインスタンス化し、渡されたステートマシンへのポインタを格納します。また、GUI部分を初期化し、それぞれの通信インターフェースを接続することで、GUI部分とステートマシンを連携させます。
connect(button, &QToolButton::clicked, this, [this, i, j]() {
QVariantMap data;
data.insert(u"x"_s, i);
data.insert(u"y"_s, j);
m_machine->submitEvent("tap", data);
});まず、81個のボタンを作成し、それらのclicked シグナルを、ボタンの座標を引数としてtap イベントをステートマシンに送信するラムダ式に接続します。
その後、ボタンを3×3のボックスにグループ化するために、グリッドに水平線と垂直線をいくつか追加します。
connect(m_startButton, &QAbstractButton::clicked, this, [this]() {
if (m_machine->isActive("playing"))
m_machine->submitEvent("stop");
else
m_machine->submitEvent("start");
});Start / Stop ボタンを作成し、そのclickedシグナルをラムダ式に接続します。このラムダ式は、ステートマシンがplaying 状態にあるかどうかに応じて、それぞれstop イベントまたはstart イベントを送信します。
グリッドが解かれたかどうかを示すラベルと、「Undo 」ボタンを作成します。このボタンは、クリックされるたびにundo イベントを送信します。
connect(m_undoButton, &QAbstractButton::clicked, this, [this]() {
m_machine->submitEvent("undo");
});次に、解くべきグリッド名が格納されたコンボボックスを作成します。これらのグリッドは、アプリケーションのコンパイル済みリソース内の:/data ディレクトリから読み込まれます。
connect(m_chooser, &QComboBox::currentIndexChanged, this, [this](int index) {
const QString sudokuFile = m_chooser->itemData(index).toString();
const QVariantMap initValues = readSudoku(sudokuFile);
m_machine->submitEvent("setup", initValues);
});
const QVariantMap initValues = readSudoku(
m_chooser->itemData(0).toString());
m_machine->setInitialValues(initValues);プレイヤーがコンボボックスの数独盤を変更するたびに、その数独盤の内容を読み取り、initValues キーの下のバリアントマップに、int型バリアントのリストのリストとして保存します。そして、数独盤の内容を引き数として渡して、setup イベントをステートマシンに送信します。 最初に、リストから利用可能な最初のグリッドを読み取り、それを初期グリッドとして数独ステートマシンに直接渡します。
m_machine->connectToState("playing", [this] (bool playing) {
...
});
m_machine->connectToState("solved", [this](bool solved) {
if (solved)
m_label->setText(tr("SOLVED !!!"));
else
m_label->setText(tr("unsolved"));
});
m_machine->connectToEvent("updateGUI", [this](const QScxmlEvent &event) {
...
});その後、ステートマシンが「playing 」または「solved 」状態に入ったり、そこから抜け出したりするたびに送信されるシグナルに接続し、それに応じてGUIの一部を更新します。また、ステートマシンの「updateGUI 」イベントにも接続し、渡されたセルの状態に応じてすべてのボタンの値を更新します。
#include "mainwindow.h"
#include "sudoku.h"
#include <QtWidgets/qapplication.h>
int main(int argc, char **argv)
{
QApplication app(argc, argv);
Sudoku machine;
MainWindow mainWindow(&machine);
machine.start();
mainWindow.show();
return app.exec();
}main.cpp ファイル内のmain() 関数では、app アプリケーションオブジェクト、Sudoku ステートマシン、およびMainWindow GUIクラスをインスタンス化します。ステートマシンを起動し、メインウィンドウを表示して、アプリケーションを実行します。
© 2026 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.