Sur cette page

SCXML Sudoku

Démontre l'utilisation d'un fichier javascript séparé avec SCXML.

Exécution de l'exemple

Pour exécuter l'exemple à partir de Qt Creatorouvrez le mode Welcome et sélectionnez l'exemple à partir de Examples. Pour plus d'informations, voir Qt Creator: Tutoriel : Construire et exécuter.

Caractéristiques du Sudoku

Capture d'écran de l'exemple Sudoku

Notre sudoku contient les fonctionnalités suivantes :

  • Au début et à la fin de la partie, le sudoku entre dans l'état idle. Dans cet état, les joueurs peuvent voir si leur dernière partie s'est terminée avec succès ou non. La machine à états se trouve alors dans l'un des deux états fils de l'état idle: solved ou unsolved, respectivement. Dans l'état idle, les joueurs peuvent également choisir la grille de sudoku qu'ils souhaitent résoudre. La grille est désactivée et l'interaction de l'utilisateur est ignorée.
  • Après que les joueurs ont cliqué sur le bouton Start, le sudoku entre dans l'état playing et est prêt pour l'interaction de l'utilisateur sur le plateau.
  • Lorsque le jeu est dans l'état playing et que les joueurs cliquent sur le bouton Stop, le jeu se termine et entre dans l'état unsolved enfant de l'état idle. Si les joueurs ont réussi à résoudre l'énigme en cours, le jeu se termine automatiquement et entre dans l'état enfant solved de l'état idle, indiquant ainsi le succès.
  • Le plateau de jeu se compose de 81 boutons, disposés dans une grille de 9x9. Les boutons dont les valeurs initiales sont données restent désactivés pendant le jeu. Les joueurs ne peuvent interagir qu'avec les boutons initialement vides. Chaque clic sur le bouton augmente sa valeur d'une unité.
  • Pendant le jeu, le bouton Undo est à la disposition des joueurs.

Partie SCXML : Description de la logique interne

Le fichier sudoku.scxml décrit la structure interne des états dans lesquels le jeu de sudoku peut se trouver, définit les transitions entre les états et déclenche les fonctions de script appropriées lorsque les transitions ont lieu. Il communique également avec la partie GUI en envoyant des événements, en écoutant les événements à venir et en y réagissant.

Nous utilisons le modèle de données ECMAScript :

<scxml xmlns="http://www.w3.org/2005/07/scxml" version="1.0"
       name="Sudoku" datamodel="ecmascript">

Nous déclarons les variables suivantes :

    <datamodel>
        <data id="initState"/>
        <data id="currentState" expr="[[]]"/>
        <data id="undoStack"/>
    </datamodel>
VariableDescription
initStateContient l'état initial du jeu en cours. Il s'agit d'un tableau bidimensionnel de cellules 9x9 contenant les numéros initiaux du sudoku. La valeur zéro signifie que la cellule est initialement vide.
currentStateContient l'état actuel du jeu en cours. Elle est similaire à la variable initState et contient initialement le même contenu. Cependant, lorsque les joueurs commencent à entrer des chiffres dans les cellules vides, cette variable est mise à jour en conséquence, tandis que la variable initState reste inchangée.
undoStackContient l'historique des mouvements des joueurs. Il s'agit d'une liste des coordonnées des cellules qui ont été touchées en dernier. Chaque nouvelle modification au cours d'une partie ajoute une paire de coordonnées x et y à cette liste.

Les variables ci-dessus sont partagées avec les fonctions d'aide au script définies dans le fichier sudoku.js:

    <script src="sudoku.js"/>

Nous appelons certaines des fonctions définies dans ce fichier lorsque nous prenons des transitions ou en réaction aux événements envoyés par l'interface graphique.

Tous les états possibles mentionnés précédemment sont définis dans un état racine game.

    <state id="game">
        <onentry>
            <raise event="restart"/>
        </onentry>
        <state id="idle">
            ...
            <state id="unsolved"/>
            <state id="solved"/>
        </state>
        <state id="playing">
            ...
        </state>
        ...
    </state>

Lorsque l'exemple de sudoku est lancé, la machine à états entre dans l'état game et reste dans cet état jusqu'à ce que l'application se termine. Lors de l'entrée dans cet état, l'événement restart est déclenché en interne. Cet événement est également déclenché chaque fois que les joueurs modifient la grille de sudoku en cours ou lorsqu'ils démarrent le jeu en appuyant sur le bouton Start. Nous ne voulons pas déclencher cet événement lorsque les joueurs ont terminé la partie en cours, car nous voulons toujours afficher la grille remplie lors de la dernière partie. Cet événement est donc déclenché par trois contextes différents et est capturé en interne une fois dans une transition sans cible de l'état game:

        <transition event="restart">
            <script>
                restart();
            </script>
            <raise event="update"/>
        </transition>

Lorsque nous capturons l'événement restart, nous appelons une méthode de script d'aide restart(), définie dans le fichier sudoku.js et soulevons en interne un événement update supplémentaire.

function restart() {
    for (var i = 0; i < initState.length; i++)
        currentState[i] = initState[i].slice();
    undoStack = [];
}

La fonction restart() affecte la variable initState à la variable currentState et efface la variable undoStack.

L'événement update est déclenché en interne chaque fois que nous voulons informer l'interface graphique que le contenu de la grille a été modifié et que l'interface graphique doit se mettre à jour en fonction des valeurs transmises. Cet événement est pris en compte dans une autre transition sans cible de l'état game:

        <transition event="update">
            <send event="updateGUI">
                <param name="currentState" expr="currentState"/>
                <param name="initState" expr="initState"/>
            </send>
        </transition>

Nous envoyons l'événement externe updateGUI, qui est intercepté dans le code C++. L'événement updateGUI est doté de données supplémentaires, spécifiées dans les éléments <param>. Nous transmettons deux paramètres, qui sont accessibles de l'extérieur par les noms currentState et initState. Les valeurs réelles qui leur sont transmises correspondent respectivement aux variables currentState et initState du modèle de données, qui sont spécifiées par les attributs 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>

Lorsque nous sommes dans l'état idle, nous réagissons à deux événements, qui peuvent être envoyés par la partie GUI : start et setup. Lorsque nous recevons l'événement start, nous passons simplement à l'état playing. Lorsque nous recevons l'événement setup, nous nous attendons à ce que l'interface graphique nous ait envoyé la nouvelle grille à résoudre. Le nouvel état initial de la grille est censé être transmis par le champ initState de _event.data. Nous attribuons la valeur transmise à la variable initState définie dans notre modèle de données et redémarrons le contenu de la grille.

        <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>

Chaque fois que nous entrons dans l'état playing, nous réinitialisons le contenu de la grille, car nous aurions pu continuer à afficher le contenu du jeu précédent. Dans l'état playing, nous réagissons aux événements possibles envoyés par l'interface graphique : tap, undo, et stop.

L'événement tap est envoyé lorsque les joueurs appuient sur l'une des cellules de sudoku activées. Cet événement est censé contenir des données supplémentaires spécifiant les coordonnées de la cellule, qui sont transmises par les champs x et y de _event.data. Tout d'abord, nous vérifions si les coordonnées transmises sont valides en invoquant la fonction 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;
}

Nous nous assurons que les coordonnées ne sont ni négatives ni plus grandes que notre grille. De plus, nous vérifions si les coordonnées pointent vers une cellule initialement vide, puisque nous ne pouvons pas modifier les cellules initialement données par la description de la grille.

Lorsque nous nous sommes assurés que les coordonnées transmises sont correctes, nous appelons la fonction 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]);
}

Cette fonction incrémente la valeur de la cellule de la grille transmise et ajoute le nouveau mouvement à l'historique de la pile d'annulation.

Juste après la fin de l'exécution de la fonction calculateCurrentState(), nous vérifions si la grille est déjà résolue en appelant la fonction 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 fonction isSolved() renvoie true si la grille est correctement résolue. Comme nous devons vérifier chaque ligne, chaque colonne et chaque carré 3x3, nous définissons la fonction d'aide isOK(). Cette fonction prend la liste des nombres et renvoie true si la liste transmise contient des nombres uniques et qu'aucun nombre n'est égal à zéro, ce qui signifie qu'il n'y a pas de cellule vide. La boucle principale de isSolved() est invoquée neuf fois. À chaque itération, nous construisons trois listes de nombres représentant une ligne, une colonne et un carré de la grille et nous appelons isOK() pour ces listes. Lorsque les 27 listes sont correctes, la grille est résolue correctement et nous renvoyons true.

Pour revenir à notre fichier SCXML, si isSolved() renvoie true, nous déclenchons l'événement solved en interne. La dernière instruction dans le cas d'un déplacement correct est de déclencher l'événement update, puisque nous devons informer l'interface graphique de la modification de la grille.

        <state id="playing">
            ...
            <transition event="undo">
                <script>
                    undo();
                </script>
                <raise event="update"/>
            </transition>
            <transition event="stop" target="idle"/>
            <transition event="solved" target="solved"/>
        </state>

Lorsque nous sommes dans l'état playing, nous réagissons également à l'événement undo envoyé par l'interface graphique. Dans ce cas, nous appelons la fonction de script undo() et informons l'interface graphique de la nécessité d'une mise à jour.

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 fonction undo() supprime le dernier déplacement de l'historique, s'il y en a eu un, et décrémente la valeur actuelle de la cellule décrite par les coordonnées de ce déplacement.

L'état playing est également prêt pour l'événement stop envoyé par l'interface graphique lorsque les joueurs appuient sur le bouton Stop. Dans ce cas, nous activons simplement l'état idle.

En outre, nous interceptons l'événement solved envoyé en interne et activons l'état solved dans ce cas.

Partie C++ : Construction de l'interface graphique

La partie C++ de l'application consiste en une classe MainWindow qui construit l'interface graphique et la relie à la partie SCXML. La classe est déclarée dans 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 classe MainWindow contient le pointeur vers la classe QScxmlStateMachine *m_machine, qui est la classe de machine à états générée automatiquement par Qt Machine à partir du fichier sudoku.scxml. Elle contient également les pointeurs vers certains éléments de l'interface graphique.

MainWindow::MainWindow(QScxmlStateMachine *machine, QWidget *parent) :
    QWidget(parent),
    m_machine(machine)
{

Le constructeur de la classe MainWindow instancie la partie GUI de l'application et stocke le pointeur vers la machine à états passée. Il initialise également la partie GUI et colle la partie GUI à la machine d'état en connectant leurs interfaces de communication ensemble.

            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);
            });

Tout d'abord, nous créons 81 boutons et connectons leur signal clicked à une expression lambda qui soumet l'événement tap à la machine à états en transmettant les coordonnées du bouton.

Ensuite, nous ajoutons des lignes horizontales et verticales à la grille afin de regrouper les boutons dans des boîtes de 3x3.

    connect(m_startButton, &QAbstractButton::clicked, this, [this]() {
        if (m_machine->isActive("playing"))
            m_machine->submitEvent("stop");
        else
            m_machine->submitEvent("start");
    });

Nous créons le bouton Start / Stop et connectons son signal cliqué à une expression lambda qui soumet l'événement stop ou start selon que la machine est dans l'état playing ou non, respectivement.

Nous créons une étiquette indiquant si la grille est résolue ou non, ainsi qu'un bouton Undo, qui soumet l'événement undo chaque fois qu'il est cliqué.

    connect(m_undoButton, &QAbstractButton::clicked, this, [this]() {
        m_machine->submitEvent("undo");
    });

Nous créons ensuite une boîte combobox qui est remplie avec les noms des grilles à résoudre. Ces grilles sont lues à partir du répertoire :/data des ressources compilées de l'application.

    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);

Chaque fois que les joueurs changent de grille dans la combobox, nous lisons le contenu de la grille en le stockant dans la carte des variantes sous la clé initValues sous la forme d'une liste de listes de variantes int et nous soumettons l'événement setup à la machine à états en lui transmettant le contenu de la grille. Au départ, nous lisons la première grille disponible dans la liste et la transmettons directement à la machine à états sudoku en tant que grille initiale.

    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) {
        ...
    });

Plus tard, nous nous connectons aux signaux envoyés chaque fois que la machine entre ou quitte les états playing ou solved, et nous mettons à jour certaines parties de l'interface graphique en conséquence. Nous nous connectons également à l'événement updateGUI de la machine à états et mettons à jour les valeurs de tous les boutons en fonction de l'état des cellules transmises.

#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();
}

Dans la fonction main() du fichier main.cpp, nous instancions l'objet d'application app, la machine à états Sudoku et la classe d'interface graphique MainWindow. Nous démarrons la machine à états, affichons la fenêtre principale et exécutons l'application.

Exemple de projet @ code.qt.io

© 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.