SCXML 스도쿠
별도의 자바스크립트 파일을 SCXML과 함께 사용하는 방법을 보여줍니다.
예제 실행하기
에서 예제를 실행하려면 Qt Creator에서 Welcome 모드를 열고 Examples 에서 예제를 선택합니다. 자세한 내용은 예제 빌드 및 실행하기를 참조하세요.
스도쿠 기능
스도쿠에는 다음과 같은 기능이 포함되어 있습니다:
- 게임이 처음 시작되고 종료되면 스도쿠는
idle
상태가 됩니다. 이 상태에서 플레이어는 마지막 게임이 성공적으로 끝났는지 여부를 확인할 수 있습니다. 그런 다음 상태 머신은idle
상태의 두 하위 상태 중 하나인solved
또는unsolved
상태가 됩니다.idle
상태에서 플레이어는 풀고자 하는 스도쿠 그리드를 선택할 수도 있습니다. 그리드는 비활성화되고 사용자 상호작용은 무시됩니다. - 플레이어가 Start 버튼을 클릭하면 스도쿠는
playing
상태가 되고 보드에서 사용자 상호작용을 할 준비가 됩니다. - 게임이
playing
상태에 있고 플레이어가 Stop 버튼을 클릭하면 게임이 종료되고idle
상태의unsolved
하위 상태로 들어갑니다. 플레이어가 현재 퍼즐을 성공적으로 풀면 게임이 자동으로 종료되고 성공을 나타내는idle
상태의solved
하위 상태로 들어갑니다. - 보드는 9x9 그리드에 배치된 81개의 버튼으로 구성되어 있습니다. 초기값이 지정된 버튼은 게임 중에 비활성화된 상태로 유지됩니다. 플레이어는 처음에 비어 있는 버튼과만 상호작용할 수 있습니다. 버튼을 클릭할 때마다 값이 하나씩 증가합니다.
- 게임 중에는 플레이어의 편의를 위해 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가 자체적으로 업데이트되어야 함을 알리고자 할 때마다 발생합니다. 이 이벤트는 game
상태의 또 다른 대상 없는 전환에서 포착됩니다:
<transition event="update"> <send event="updateGUI"> <param name="currentState" expr="currentState"/> <param name="initState" expr="initState"/> </send> </transition>
C++ 코드에서 가로채고 있는 외부 이벤트 updateGUI
를 전송합니다. 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
이벤트를 발생시킵니다. 제대로 이동한 경우의 마지막 명령은 그리드의 변경 사항을 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()
함수는 기록에서 마지막 이동이 있는 경우 이를 제거하고 이 이동에서 가져온 좌표로 설명되는 셀의 현재 값을 감소시킵니다.
playing
상태는 플레이어가 Stop 버튼을 누를 때 GUI에서 보내는 stop
이벤트에 대한 준비 상태이기도 합니다. 이 경우 idle
상태를 활성화하기만 하면 됩니다.
또한 내부적으로 전송된 solved
이벤트를 가로채서 이 경우 solved
상태를 활성화합니다.
C++ 파트: GUI 구성하기
애플리케이션의 C++ 부분은 GUI를 구성하는 MainWindow
클래스로 구성되며, 이 클래스는 SCXML 부분과 결합됩니다. 이 클래스는 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
클래스는 sudoku.scxml
파일에서 Qt가 자동으로 생성한 상태 머신 클래스인 QScxmlStateMachine *m_machine
에 대한 포인터를 보유합니다. 또한 일부 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개의 버튼을 생성하고 버튼의 좌표를 전달하는 tap
이벤트를 상태 머신에 제출하는 람다 표현식에 버튼의 clicked
신호를 연결합니다.
그런 다음 그리드에 가로 및 세로선을 추가하여 버튼을 3x3 상자로 그룹화합니다.
connect(m_startButton, &QAbstractButton::clicked, this, [this]() { if (m_machine->isActive("playing")) m_machine->submitEvent("stop"); else m_machine->submitEvent("start"); });
Start / Stop 버튼을 만들고 클릭 신호를 머신이 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 클래스를 인스턴스화합니다. 스테이트 머신을 시작하고 메인 창을 표시한 다음 애플리케이션을 실행합니다.
© 2025 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.