SCXML 스도쿠
SCXML과 함께 별도의 자바스크립트 파일을 사용하는 방법을 보여줍니다.
예제 실행하기
다음 위치에서 예제를 실행할 수 있습니다:
- 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)’ 상태의 두 하위 상태 중 하나, 즉 ‘게임 시작(solved)’ 또는 ‘게임 종료(unsolved)’ 상태에 있게 됩니다. ‘게임 시작/종료(idle)’ 상태에서는 플레이어가 풀고 싶은 스도쿠 그리드를 선택할 수도 있습니다. 이 상태에서는 그리드가 비활성화되며 사용자의 상호작용은 무시됩니다. - 플레이어가 ‘ Start ’ 버튼을 클릭하면 스도쿠는 ‘
playing’ 상태로 전환되며, 보드에서 사용자 상호작용을 받을 준비가 됩니다. - 게임이 ‘
playing’ 상태에 있을 때 플레이어가 ‘ Stop ’ 버튼을 클릭하면 게임이 종료되고 ‘idle’ 상태의 하위 상태인 ‘unsolved’ 상태로 들어갑니다. 플레이어가 현재 퍼즐을 성공적으로 풀면 게임이 자동으로 종료되고, 성공을 나타내는 ‘idle’ 상태의 하위 상태인 ‘solved’ 상태로 들어갑니다. - 보드는 9x9 격자로 배열된 81개의 버튼으로 구성됩니다. 초기 값이 지정된 버튼은 게임 중 비활성화된 상태로 유지됩니다. 플레이어는 처음에 비어 있는 버튼과만 상호작용할 수 있습니다. 버튼을 클릭할 때마다 해당 버튼의 값이 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 | 현재 게임의 초기 상태를 저장합니다. 이는 초기 스도쿠 숫자가 들어 있는 9x9 셀로 구성된 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 버튼을 눌러 게임을 시작할 때마다 발생합니다. 사용자가 현재 게임을 완료한 후에는 이 이벤트를 전송하지 않으려 합니다. 마지막 게임 플레이에서 채워진 그리드를 계속 표시하고 싶기 때문입니다. 따라서 이 이벤트는 세 가지 서로 다른 컨텍스트에서 발생하며, game 상태의 대상 없는 전환 시 내부적으로 한 번 캡처됩니다:
<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> 요소 내에 지정된 추가 데이터가 포함됩니다. 우리는 두 개의 매개변수를 전달하며, 이 매개변수들은 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 부분에서 전송될 수 있는 두 가지 이벤트, 즉 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번 호출됩니다. 각 반복마다 그리드의 행, 열, 정사각형을 나타내는 세 개의 숫자 리스트를 생성하고, 이에 대해 isOK() 를 호출합니다. 27개의 리스트가 모두 정상일 때, 그리드는 올바르게 풀린 것이며 true 를 반환합니다.
SCXML 파일로 돌아가서, ` isSolved() `가 ` true`를 반환하는 경우, 내부적으로 ` solved ` 이벤트를 발생시킵니다. 이동이 정상적으로 이루어진 경우의 마지막 명령은 ` update ` 이벤트를 발생시키는 것입니다. 이는 그리드의 변경 사항을 GUI에 알릴 필요가 있기 때문입니다.
<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 ` 이벤트를 상태 머신에 제출합니다.
그 후, 버튼을 3x3 상자로 그룹화하기 위해 그리드에 수평선과 수직선을 추가합니다.
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 키 아래의 variant 맵에 정수(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.