C

Qt Quick Ultralite chess Example

// Copyright (C) 2024 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial
#pragma once
#include <qul/property.h>
#include <qul/singleton.h>
#include <qul/timer.h>
#include <qul/signal.h>

/**
 * This is a very simple example demonstrating how to integrate a C++ Object
 * with Qt Quick Ultralite
 *
 * The example does not conform to the rules of chess, it has been simplified
 * on purpose
 *
 * The idea is just to expose a few properties and functions from a Singleton.
 * By inheriting from Qul::Singleton, we tell the qmlinterfacegenerator tool
 * to expose the type to QML. (the qul_target_generate_interfaces macro in CMakeLists.txt
 * will make sure that the tool is ran on this file)
 */
struct ChessModel : Qul::Singleton<ChessModel>
{
    /// This property is public and will be exposed to QML
    Qul::Property<bool> whiteTurn;
    Qul::Property<int> secondsSinceMove;
    Qul::Signal<void(int col, int row)> validMove;
    Qul::Signal<void()> invalidMove;

    /// Return the current column position of a specific piece
    /// (or -1 if it is not in the chess board).
    /// This function is public and can be called from the QML
    int col(int piece) { return position[piece].value().first; }

    /// Return the current row position of a specific piece
    /// (or -1 if it is not in the chess board).
    /// This function is public and can be called from the QML
    int row(int piece) { return position[piece].value().second; }

    /// Return true if it is allowed to drop the piece on thie case
    /// This function is public and can be called from the QML
    bool canDrop(int col, int row)
    {
        if (col < 0 || col > 7 || row < 0 || row > 7)
            return false;
        return squares[col][row].value().canDrop;
    }

    /// Set the current active piece
    /// (if piece is negative, there shall not be any active piece)
    /// This function is public and can be called from the QML
    void setActivePiece(int piece)
    {
        if (piece < 0) {
            for (int r = 0; r < 8; ++r)
                for (int c = 0; c < 8; ++c) {
                    SquareInfo g = squares[c][r].value();
                    if (g.canDrop) {
                        g.canDrop = false;
                        squares[c][r].setValue(g);
                    }
                }
            return;
        }
        if (whiteTurn.value() ? piece >= 16 : piece < 16)
            return; // piece of the wrong color

        Coordinates currentPos = position[piece].value();
        squares[currentPos.first][currentPos.second].setValue(SquareInfo(piece, true));

        switch (piece % 16) {
        case 0:
            // King
            setMove(piece, currentPos.first + 1, currentPos.second);
            setMove(piece, currentPos.first - 1, currentPos.second);
            setMove(piece, currentPos.first, currentPos.second + 1);
            setMove(piece, currentPos.first, currentPos.second - 1);
            setMove(piece, currentPos.first + 1, currentPos.second + 1);
            setMove(piece, currentPos.first - 1, currentPos.second - 1);
            setMove(piece, currentPos.first + 1, currentPos.second - 1);
            setMove(piece, currentPos.first - 1, currentPos.second + 1);
            break;
        case 1:
            // Queen
            for (int i = 1; setMove(piece, currentPos.first + i, currentPos.second); ++i)
                ;
            for (int i = 1; setMove(piece, currentPos.first - i, currentPos.second); ++i)
                ;
            for (int i = 1; setMove(piece, currentPos.first, currentPos.second + i); ++i)
                ;
            for (int i = 1; setMove(piece, currentPos.first, currentPos.second - i); ++i)
                ;
            for (int i = 1; setMove(piece, currentPos.first + i, currentPos.second + i); ++i)
                ;
            for (int i = 1; setMove(piece, currentPos.first - i, currentPos.second + i); ++i)
                ;
            for (int i = 1; setMove(piece, currentPos.first + i, currentPos.second - i); ++i)
                ;
            for (int i = 1; setMove(piece, currentPos.first - i, currentPos.second - i); ++i)
                ;
            break;
        case 2:
        case 3:
            // Rock
            for (int i = 1; setMove(piece, currentPos.first + i, currentPos.second); ++i)
                ;
            for (int i = 1; setMove(piece, currentPos.first - i, currentPos.second); ++i)
                ;
            for (int i = 1; setMove(piece, currentPos.first, currentPos.second + i); ++i)
                ;
            for (int i = 1; setMove(piece, currentPos.first, currentPos.second - i); ++i)
                ;
            break;
        case 4:
        case 5:
            // Bishop
            for (int i = 1; setMove(piece, currentPos.first + i, currentPos.second + i); ++i)
                ;
            for (int i = 1; setMove(piece, currentPos.first - i, currentPos.second + i); ++i)
                ;
            for (int i = 1; setMove(piece, currentPos.first + i, currentPos.second - i); ++i)
                ;
            for (int i = 1; setMove(piece, currentPos.first - i, currentPos.second - i); ++i)
                ;
            break;
        case 6:
        case 7:
            // Knight
            setMove(piece, currentPos.first + 1, currentPos.second + 2);
            setMove(piece, currentPos.first + 1, currentPos.second - 2);
            setMove(piece, currentPos.first - 1, currentPos.second + 2);
            setMove(piece, currentPos.first - 1, currentPos.second - 2);
            setMove(piece, currentPos.first + 2, currentPos.second + 1);
            setMove(piece, currentPos.first + 2, currentPos.second - 1);
            setMove(piece, currentPos.first - 2, currentPos.second + 1);
            setMove(piece, currentPos.first - 2, currentPos.second - 1);
            break;
        default: // Pawn
            if (whiteTurn.value()) {
                if (setMove(piece, currentPos.first, currentPos.second + 1, true) && currentPos.second == 1)
                    setMove(piece, currentPos.first, currentPos.second + 2, true);
                pawnTake(currentPos.first + 1, currentPos.second + 1);
                pawnTake(currentPos.first - 1, currentPos.second + 1);
            } else {
                if (setMove(piece, currentPos.first, currentPos.second - 1, true) && currentPos.second == 6)
                    setMove(piece, currentPos.first, currentPos.second - 2, true);
                pawnTake(currentPos.first + 1, currentPos.second - 1);
                pawnTake(currentPos.first - 1, currentPos.second - 1);
            }
        }
    }

    /// Release the given piece in the given position.
    void release(int piece, int col, int row)
    {
        if (!canDrop(col, row)) {
            invalidMove();
            return;
        }
        Coordinates pos = position[piece].value();
        if (pos.first == col && pos.second == row)
            return;
        // remove the piece from its old position
        squares[pos.first][pos.second].setValue(SquareInfo());
        // set the new position of the piece
        position[piece].setValue(std::make_pair(col, row));
        // Take the old piece out of the board
        int old = squares[col][row].value().piece;
        if (old >= 0) {
            position[old].setValue(std::make_pair(-1, -1));
        }
        // set the piece in its new square
        squares[col][row].setValue(SquareInfo(piece));
        whiteTurn.setValue(!whiteTurn.value());
        secondsSinceMove.setValue(0);
        moveTimer.start();
        validMove(col, row);
    }

    ChessModel()
        : whiteTurn(true)
    {
        // Set the chessboard to the initial position
        for (int c = 0; c <= 1; ++c) {
            int r = c == 0 ? 0 : 7;
            int o = c == 0 ? 0 : 16;
            position[o + 0].setValue(std::make_pair(4, r));
            position[o + 1].setValue(std::make_pair(3, r));
            position[o + 2].setValue(std::make_pair(0, r));
            position[o + 3].setValue(std::make_pair(7, r));
            position[o + 4].setValue(std::make_pair(2, r));
            position[o + 5].setValue(std::make_pair(5, r));
            position[o + 6].setValue(std::make_pair(1, r));
            position[o + 7].setValue(std::make_pair(6, r));
            r = c == 0 ? 1 : 6;
            o += 8;
            for (int p = 0; p < 8; ++p) {
                position[o + p].setValue(std::make_pair(p, r));
            }
        }

        for (int i = 0; i < 16 * 2; ++i) {
            const Coordinates &pp = position[i].value();
            squares[pp.first][pp.second].setValue(SquareInfo(i));
        }

        secondsSinceMove.setValue(-1);
        // Set a timer to fire every seconds to update the secondsSinceMove value
        IncrementMoveTime incrementer;
        incrementer.model = this;
        moveTimer.onTimeout(incrementer);
        moveTimer.setInterval(1000);
    }

private:
    // These private properties are not directly accessible from QML.
    // But by calling value() or setValue() on them, we still create
    // bindings to them.

    struct SquareInfo
    {
        explicit SquareInfo(int piece_ = -1, bool canDrop_ = false)
            : piece(piece_)
            , canDrop(canDrop_)
        {}
        int piece;
        bool canDrop;

        bool operator==(const SquareInfo &other) const { return piece == other.piece && canDrop == other.canDrop; }
    };
    Qul::Property<SquareInfo> squares[8][8];

    typedef std::pair<int, int> Coordinates;
    Qul::Property<Coordinates> position[32];

    // Example of a timer and its slot (initialized in the constructor)
    Qul::Timer moveTimer;
    struct IncrementMoveTime
    {
        ChessModel *model;
        void operator()() { model->secondsSinceMove.setValue(model->secondsSinceMove.value() + 1); }
    };

    /// Helper function to set that the piece can be dropped on the given square
    // returns true if the square was empty
    bool setMove(int, int col, int row, bool pawn = false)
    {
        if (col < 0 || col > 7 || row < 0 || row > 7)
            return false;
        int old = squares[col][row].value().piece;
        if (old < 0) {
            squares[col][row].setValue(SquareInfo(old, true));
            return true;
        }
        if (pawn) // pawn can't take like normal piece
            return false;

        // One can only take a piece of the opposite color
        if (whiteTurn.value() ? old >= 16 : old < 16) {
            squares[col][row].setValue(SquareInfo(old, true));
        }
        return false;
    };

    /// Helper function to set that a pawn can be dropped in the given location if
    /// it takes another piece
    void pawnTake(int col, int row)
    {
        if (col < 0 || col > 7 || row < 0 || row > 7)
            return;
        int old = squares[col][row].value().piece;
        if (old < 0)
            return;
        if (whiteTurn.value() ? old >= 16 : old < 16) {
            squares[col][row].setValue(SquareInfo(old, true));
        }
    };
};