Tutorial 94: Building a Puzzle Game

CNA — C++ XNA 4.0 reimplementation

Grid-based game board

const int COLS = 8, ROWS = 8, CELL = 64;
const int GEM_TYPES = 5;

struct Cell { int type = -1; };

struct Board {
    Cell cells[ROWS][COLS];

    void Fill() {
        for (int r = 0; r < ROWS; ++r)
            for (int c = 0; c < COLS; ++c)
                cells[r][c].type = rand() % GEM_TYPES;
    }

    Cell& At(int r, int c) { return cells[r][c]; }
};

Piece matching logic

struct Match { int row, col, len; bool horizontal; };

std::vector<Match> FindMatches(const Board& board) {
    std::vector<Match> matches;
    // Horizontal runs of 3+
    for (int r = 0; r < ROWS; ++r) {
        for (int c = 0; c < COLS - 2; ) {
            int t = board.cells[r][c].type;
            int len = 1;
            while (c + len < COLS && board.cells[r][c+len].type == t) ++len;
            if (len >= 3) matches.push_back({r, c, len, true});
            c += len;
        }
    }
    // Vertical runs of 3+
    for (int c = 0; c < COLS; ++c) {
        for (int r = 0; r < ROWS - 2; ) {
            int t = board.cells[r][c].type;
            int len = 1;
            while (r + len < ROWS && board.cells[r+len][c].type == t) ++len;
            if (len >= 3) matches.push_back({r, c, len, false});
            r += len;
        }
    }
    return matches;
}

Animations with Lerp

struct SwapAnim {
    int r1,c1, r2,c2;
    float t = 0.0f;     // 0..1 animation progress
    bool done() const { return t >= 1.0f; }

    // Returns animated position of cell (r1,c1)
    Vector2 PosA(float dt) {
        t = std::min(1.0f, t + dt * 5.0f);  // 0.2s swap
        Vector2 from(c1*CELL, r1*CELL), to(c2*CELL, r2*CELL);
        return Vector2::Lerp(from, to, t);
    }
    Vector2 PosB(float dt) {
        Vector2 from(c2*CELL, r2*CELL), to(c1*CELL, r1*CELL);
        return Vector2::Lerp(from, to, t);
    }
};

Undo/redo stack

struct Move { int r1,c1, r2,c2; };

class UndoStack {
public:
    void Push(Move m) { undos_.push_back(m); redos_.clear(); }
    bool CanUndo() const { return !undos_.empty(); }
    bool CanRedo() const { return !redos_.empty(); }
    Move PopUndo() { auto m = undos_.back(); undos_.pop_back(); redos_.push_back(m); return m; }
    Move PopRedo() { auto m = redos_.back(); redos_.pop_back(); undos_.push_back(m); return m; }
private:
    std::vector<Move> undos_, redos_;
};

Particle effects on match

struct Particle {
    Vector2 pos, vel;
    Color   color;
    float   life, maxLife;
    bool    alive() const { return life > 0; }
};

void SpawnMatchParticles(std::vector<Particle>& particles,
                         int row, int col, Color color) {
    for (int i = 0; i < 12; ++i) {
        float angle = (float)i / 12.0f * MathHelper::TwoPi;
        float speed = 50.0f + (rand()%100);
        particles.push_back({
            Vector2(col*CELL + CELL/2.0f, row*CELL + CELL/2.0f),
            Vector2(std::cos(angle)*speed, std::sin(angle)*speed),
            color, 0.6f, 0.6f
        });
    }
}

void UpdateParticles(std::vector<Particle>& ps, float dt) {
    for (auto& p : ps) {
        if (!p.alive()) continue;
        p.pos  += p.vel * dt;
        p.vel  *= std::pow(0.9f, dt * 60.0f);   // drag
        p.life -= dt;
    }
    ps.erase(std::remove_if(ps.begin(), ps.end(),
                             [](const Particle& p){ return !p.alive(); }),
             ps.end());
}

Match-3 grid + swap animation

#include "Microsoft/Xna/Framework/Game.hpp"
#include "Microsoft/Xna/Framework/Graphics/GraphicsDeviceManager.hpp"
#include "Microsoft/Xna/Framework/Graphics/SpriteBatch.hpp"
#include "Microsoft/Xna/Framework/Input/Keyboard.hpp"
#include "Microsoft/Xna/Framework/Input/Keys.hpp"
#include "Microsoft/Xna/Framework/Color.hpp"
#include "Microsoft/Xna/Framework/Rectangle.hpp"
#include "Microsoft/Xna/Framework/MathHelper.hpp"
#include <vector>
#include <memory>
#include <algorithm>
#include <cstdlib>
#include <cmath>

using namespace Microsoft::Xna::Framework;
using namespace Microsoft::Xna::Framework::Graphics;
using namespace Microsoft::Xna::Framework::Input;

class Match3Game final : public Game {
public:
    Match3Game() : graphics_(this) {
        graphics_.setPreferredBackBufferWidth(COLS*CELL + 200);
        graphics_.setPreferredBackBufferHeight(ROWS*CELL + 60);
    }

protected:
    void LoadContent() override {
        sb_    = std::make_unique<SpriteBatch>(getGraphicsDeviceProperty());
        pixel_ = std::make_unique<Texture2D>(getGraphicsDeviceProperty(), 1, 1);
        Color w = Color::White;
        pixel_->SetData(&w, 1);
        board_.Fill();
        selectedRow_ = selectedCol_ = -1;
    }

    void Update(const GameTime& gt) override {
        float dt = static_cast<float>(gt.ElapsedGameTime.TotalSeconds());
        auto ks  = Keyboard::GetState();

        if (ks.IsKeyDown(Keys::Escape)) Exit();

        // Cursor movement
        auto Move = [&](int dr, int dc) {
            if (prevKeys_.IsKeyDown(Keys::Up) || prevKeys_.IsKeyDown(Keys::Down) ||
                prevKeys_.IsKeyDown(Keys::Left) || prevKeys_.IsKeyDown(Keys::Right))
                return;
            if (selectedRow_ < 0) { selectedRow_ = 0; selectedCol_ = 0; return; }
            selectedRow_ = std::clamp(selectedRow_ + dr, 0, ROWS-1);
            selectedCol_ = std::clamp(selectedCol_ + dc, 0, COLS-1);
        };
        if (ks.IsKeyDown(Keys::Up))    Move(-1, 0);
        if (ks.IsKeyDown(Keys::Down))  Move( 1, 0);
        if (ks.IsKeyDown(Keys::Left))  Move( 0,-1);
        if (ks.IsKeyDown(Keys::Right)) Move( 0, 1);

        // Swap with Space
        if (ks.IsKeyDown(Keys::Space) && !prevKeys_.IsKeyDown(Keys::Space) && selectedRow_ >= 0) {
            if (pendingR_ < 0) { pendingR_ = selectedRow_; pendingC_ = selectedCol_; }
            else {
                // Attempt swap
                std::swap(board_.cells[pendingR_][pendingC_].type,
                          board_.cells[selectedRow_][selectedCol_].type);
                auto m = FindMatches(board_);
                if (m.empty()) std::swap(board_.cells[pendingR_][pendingC_].type,
                                         board_.cells[selectedRow_][selectedCol_].type);
                else { score_ += (int)m.size() * 30; }
                pendingR_ = pendingC_ = -1;
            }
        }
        prevKeys_ = ks;

        UpdateParticles(particles_, dt);
    }

    void Draw(const GameTime&) override {
        auto& gd = getGraphicsDeviceProperty();
        gd.Clear(Color(20, 20, 40));
        sb_->Begin();

        static const Color gemColors[GEM_TYPES] = {
            Color::Red, Color::Blue, Color::Green, Color::Yellow, Color(200,0,200)
        };

        for (int r = 0; r < ROWS; ++r)
            for (int c = 0; c < COLS; ++c) {
                int t = board_.cells[r][c].type;
                Rectangle dest(c*CELL+2, r*CELL+2, CELL-4, CELL-4);
                sb_->Draw(*pixel_, dest, t >= 0 ? gemColors[t] : Color(40,40,60));
                if (r == selectedRow_ && c == selectedCol_)
                    sb_->Draw(*pixel_, Rectangle(c*CELL, r*CELL, CELL, 2), Color::White);
            }

        // Particles
        for (auto& p : particles_) {
            if (!p.alive()) continue;
            float alpha = p.life / p.maxLife;
            Color c(p.color.R, p.color.G, p.color.B, (uint8_t)(255*alpha));
            sb_->Draw(*pixel_, Rectangle((int)p.pos.X-2,(int)p.pos.Y-2,4,4), c);
        }
        sb_->End();
        gd.Present();
    }

private:
    static const int COLS=8, ROWS=8, CELL=64, GEM_TYPES=5;

    struct Cell { int type=-1; };
    struct Board {
        Cell cells[8][8];
        void Fill() { for(auto& r:cells) for(auto& c:r) c.type=rand()%5; }
    };
    struct Particle {
        Vector2 pos,vel; Color color; float life,maxLife;
        bool alive() const { return life>0; }
    };

    std::vector<struct Match> FindMatches(const Board& b) {
        std::vector<struct Match> out;
        // (simplified: just return empty for skeleton)
        return out;
    }
    struct Match { int row,col,len; bool h; };

    void UpdateParticles(std::vector<Particle>& ps, float dt) {
        for(auto& p:ps){if(!p.alive())continue;p.pos+=p.vel*dt;p.life-=dt;}
        ps.erase(std::remove_if(ps.begin(),ps.end(),[](const Particle&p){return !p.alive();}),ps.end());
    }

    GraphicsDeviceManager        graphics_;
    std::unique_ptr<SpriteBatch> sb_;
    std::unique_ptr<Texture2D>   pixel_;
    Board board_;
    std::vector<Particle> particles_;
    KeyboardState prevKeys_;
    int selectedRow_=-1, selectedCol_=-1, pendingR_=-1, pendingC_=-1;
    int score_=0;
};

int main() { Match3Game game; game.Run(); }