SCXML Sudoku
Demuestra el uso de un archivo javascript independiente con SCXML.
Ejecutar el ejemplo
Para ejecutar el ejemplo desde Qt Creatorabra el modo Welcome y seleccione el ejemplo de Examples. Para más información, consulte Qt Creator: Tutorial: Construir y ejecutar.
Características de Sudoku

Nuestro sudoku contiene las siguientes características:
- Inicialmente y cuando el juego termina, el sudoku entra en el estado
idle. En ese estado los jugadores pueden ver si su última partida terminó con éxito o no. A continuación, la máquina de estados se encuentra en uno de los dos estados hijos del estadoidle:solvedounsolved, respectivamente. En el estadoidlelos jugadores también pueden elegir la cuadrícula de sudoku que les gustaría resolver. La cuadrícula se desactiva y se ignora la interacción del usuario. - Después de que los jugadores hagan clic en el botón Start, el sudoku entra en el estado
playingy está listo para la interacción del usuario en el tablero. - Cuando el juego está en el estado
playingy los jugadores pulsan el botón Stop, el juego termina y entra en el estadounsolvedhijo del estadoidle. Si los jugadores han resuelto el puzzle actual con éxito, el juego termina automáticamente y entra en el estadosolvedhijo del estadoidleindicando el éxito. - El tablero consta de 81 botones, dispuestos en una cuadrícula de 9x9. Los botones con valores iniciales dados permanecen desactivados durante el juego. Los jugadores sólo pueden interactuar con botones inicialmente vacíos. Cada clic en el botón aumenta su valor en uno.
- Durante el juego, el botón Undo está disponible para comodidad de los jugadores.
Parte SCXML: Descripción Lógica Interna
El archivo sudoku.scxml describe la estructura interna de los estados en los que el juego sudoku puede estar, define las transiciones entre estados, y dispara las funciones de script apropiadas cuando las transiciones tienen lugar. También se comunica con la parte GUI enviando eventos y escuchando los próximos eventos y reaccionando a ellos.
Utilizamos el modelo de datos ECMAScript:
<scxml xmlns="http://www.w3.org/2005/07/scxml" version="1.0"
name="Sudoku" datamodel="ecmascript">Declaramos las siguientes variables:
<datamodel>
<data id="initState"/>
<data id="currentState" expr="[[]]"/>
<data id="undoStack"/>
</datamodel>| Variable | Descripción |
|---|---|
initState | Contiene el estado inicial del juego actual. Es un array bidimensional de celdas 9x9 que contienen los números iniciales del sudoku. El valor cero significa que la celda está inicialmente vacía. |
currentState | Mantiene el estado actual del juego que se está jugando. Es similar a la variable initState e inicialmente contiene el mismo contenido. Sin embargo, cuando los jugadores comienzan a introducir los números en las celdas vacías, esta variable se actualiza en consecuencia, mientras que la variable initState permanece sin cambios. |
undoStack | Contiene el historial de movimientos de los jugadores. Es una lista de las coordenadas de las celdas que fueron tocadas por última vez. Cada nueva modificación durante una partida añade un par de coordenadas x e y a esa lista. |
Las variables anteriores se comparten con las funciones de ayuda al script definidas en el archivo sudoku.js:
<script src="sudoku.js"/>
Llamamos a algunas de las funciones allí definidas cuando tomamos transiciones o en reacción a los eventos enviados por la GUI.
Todos los estados posibles antes mencionados se definen en un estado raíz game.
<state id="game">
<onentry>
<raise event="restart"/>
</onentry>
<state id="idle">
...
<state id="unsolved"/>
<state id="solved"/>
</state>
<state id="playing">
...
</state>
...
</state>Cuando se inicia el ejemplo del sudoku, la máquina de estados entra en el estado game y permanece en este estado hasta que la aplicación sale. Al entrar en este estado, lanzamos internamente el evento restart. Este evento también se activa cada vez que los jugadores cambian la cuadrícula de sudoku actual o cuando inician el juego pulsando el botón Start. No queremos enviarlo cuando han terminado el juego actual porque todavía queremos mostrar la cuadrícula llena del último juego. Por lo tanto, este evento está siendo planteado desde tres contextos diferentes y es capturado internamente una vez en una transición sin objetivo del estado game:
<transition event="restart">
<script>
restart();
</script>
<raise event="update"/>
</transition>Cuando capturamos el evento restart, llamamos a un método script de ayuda restart(), definido en el archivo sudoku.js y levantamos internamente un evento adicional update.
function restart() {
for (var i = 0; i < initState.length; i++)
currentState[i] = initState[i].slice();
undoStack = [];
}La función restart() asigna initState a la variable currentState y borra la variable undoStack.
El evento update se lanza internamente cada vez que queremos notificar al GUI que los contenidos de la rejilla han sido cambiados y que el GUI debe actualizarse de acuerdo a los valores pasados. Este evento es capturado en otra transición sin objetivo del estado game:
<transition event="update">
<send event="updateGUI">
<param name="currentState" expr="currentState"/>
<param name="initState" expr="initState"/>
</send>
</transition>Enviamos el evento externo updateGUI, que está siendo interceptado en el código C++. El evento updateGUI está equipado con datos adicionales, especificados dentro de los elementos <param>. Pasamos dos parámetros, que son accesibles externamente a través de los nombres currentState y initState. Los valores reales pasados para ellos equivalen a las variables currentState y initState del modelo de datos, respectivamente, que se especifican mediante los atributos 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>Cuando estamos en el estado idle, reaccionamos a dos eventos, que pueden ser enviados por la parte GUI: start y setup. Cuando recibimos el evento start, simplemente pasamos al estado playing. Cuando recibimos el evento setup, esperamos que la parte GUI nos haya enviado la nueva rejilla a resolver. Se espera que el nuevo estado inicial de la rejilla se pase a través del campo initState de _event.data. Asignamos el valor pasado a la variable initState definida en nuestro modelo de datos y reiniciamos el contenido de la rejilla.
<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>Cada vez que entramos en el estado playing, reiniciamos los contenidos de la rejilla ya que podríamos haber estado mostrando todavía los contenidos del juego anterior. En el estado playing reaccionamos a posibles eventos enviados desde la GUI: tap, undo, y stop.
El evento tap es enviado cuando los jugadores presionan una de las celdas de sudoku habilitadas. Se espera que este evento contenga datos adicionales especificando las coordenadas de la celda, que son pasadas a través de los campos x y y de _event.data. Primero, verificamos si las coordenadas pasadas son válidas invocando la función de script 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;
}Nos aseguramos de que las coordenadas no son negativas ni mayores que nuestra cuadrícula. Además, comprobamos si las coordenadas apuntan a una celda inicialmente vacía, ya que no podemos modificar las celdas dadas inicialmente por la descripción de la cuadrícula.
Cuando nos hemos asegurado de que las coordenadas pasadas son correctas, llamamos a la función de script 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]);
}Esta función incrementa el valor de la celda de la rejilla pasada y añade el nuevo movimiento al historial de la pila de deshacer.
Justo después de que la función calculateCurrentState() termine su ejecución, comprobamos si la cuadrícula ya está resuelta llamando a la función de script 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;
}La función isSolved() devuelve true si la cuadrícula está correctamente resuelta. Como necesitamos comprobar cada fila, cada columna y cada cuadrado de 3x3, definimos la función de ayuda isOK(). Esta función toma la lista de números y devuelve true si la lista pasada contiene números únicos y ningún número es igual a cero, lo que significa que no hay ninguna celda vacía. El bucle principal de isSolved() se invoca nueve veces. En cada iteración, construimos tres listas de números que representan una fila, una columna y un cuadrado de la cuadrícula y llamamos a isOK() para ellos. Cuando las 27 listas están bien, la cuadrícula se resuelve correctamente y devolvemos true.
Volviendo a nuestro fichero SCXML, en caso de que isSolved() devuelva true, lanzamos el evento solved internamente. La última instrucción en caso de un movimiento correcto es lanzar el evento update, ya que necesitamos notificar al GUI sobre el cambio de la grilla.
<state id="playing">
...
<transition event="undo">
<script>
undo();
</script>
<raise event="update"/>
</transition>
<transition event="stop" target="idle"/>
<transition event="solved" target="solved"/>
</state>Cuando estamos en el estado playing, también reaccionamos al evento undo enviado desde el GUI. En este caso, llamamos a la función de script undo() y notificamos al GUI sobre la necesidad de una actualización.
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;
}La función undo() elimina el último movimiento del historial, si lo hubo, y disminuye el valor actual de la celda descrita por las coordenadas tomadas de este movimiento.
El estado playing también está listo para el evento stop enviado por el GUI cuando los jugadores presionan el botón Stop. En este caso, simplemente activamos el estado idle.
Además, interceptamos el evento solved enviado internamente y activamos el estado solved en este caso.
Parte C++: Construyendo la GUI
La parte C++ de la aplicación consiste en una clase MainWindow que construye la GUI y la pega con la parte SCXML. La clase se declara en 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; };
La clase MainWindow contiene el puntero a QScxmlStateMachine *m_machine, que es la clase de máquina de estados generada automáticamente por Qt a partir del archivo sudoku.scxml. También contiene los punteros a algunos elementos GUI.
MainWindow::MainWindow(QScxmlStateMachine *machine, QWidget *parent) : QWidget(parent), m_machine(machine) {
El constructor de la clase MainWindow instantiza la parte GUI de la aplicación y almacena el puntero a la máquina de estados pasada. También inicializa la parte GUI y pega la parte GUI a la máquina de estado conectando sus interfaces de comunicación.
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); });
En primer lugar, creamos 81 botones y conectamos su señal clicked a una expresión lambda que envía el evento tap a la máquina de estados pasando las coordenadas del botón.
Después, añadimos algunas líneas horizontales y verticales a la rejilla para agrupar los botones en cajas de 3x3.
connect(m_startButton, &QAbstractButton::clicked, this, [this]() { if (m_machine->isActive("playing")) m_machine->submitEvent("stop"); else m_machine->submitEvent("start"); });
Creamos el botón Start / Stop y conectamos su señal de clic a una expresión lambda que envía el evento stop o start dependiendo de si la máquina está en el estado playing o no, respectivamente.
Creamos una etiqueta que informa de si la cuadrícula está resuelta o no, y un botón Undo, que envía el evento undo cada vez que se hace clic sobre él.
connect(m_undoButton, &QAbstractButton::clicked, this, [this]() { m_machine->submitEvent("undo"); });
A continuación creamos un combobox que se rellena con los nombres de las rejillas a resolver. Estas rejillas se leen del directorio :/data de los recursos compilados de la aplicación.
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);
Cada vez que los jugadores cambian la rejilla en el combobox, leemos el contenido de la rejilla almacenándolo en el mapa de variantes bajo la clave initValues como una lista de listas de variantes int y enviamos el evento setup a la máquina de estados pasando el contenido de la rejilla. Inicialmente, leemos la primera rejilla disponible de la lista y la pasamos directamente a la máquina de estado del sudoku como rejilla inicial.
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) { ... });
Más tarde, nos conectamos a las señales que se envían cada vez que la máquina entra o sale de los estados playing o solved, y actualizamos algunas partes de la GUI en consecuencia. También nos conectamos al evento updateGUI de la máquina de estados y actualizamos todos los valores de los botones de acuerdo con los estados de las celdas pasadas.
#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(); }
En la función main() del archivo main.cpp, instanciamos el objeto de aplicación app, la máquina de estados Sudoku y la clase GUI MainWindow. Iniciamos la máquina de estados, mostramos la ventana principal y ejecutamos la aplicación.
© 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.