Skip to main content
4 of 5
edited tags
Jamal
  • 34.9k
  • 13
  • 133
  • 237

Simon Says / Four tiles game, with C++ and SDL

My take on the Prebash Challenge, "Simon Says" game, in less than 300 lines of C++ (plus the header file), using SDL for window management and drawing.

simon_says.hpp:

#ifndef CR_SIMON_SAYS_HPP
#define CR_SIMON_SAYS_HPP

#include "SDL.h"
#include <vector>
#include <random>

// --------------------------------------------------------

// "cr" stands for Code-Review, not Coding-Rampage ;)
namespace cr
{

class SimonSaysGame
{
public:

    SimonSaysGame() = default;
    ~SimonSaysGame();

    SimonSaysGame(const SimonSaysGame &) = delete;
    SimonSaysGame & operator = (const SimonSaysGame &) = delete;

    bool initSDL();
    void runGameLoop();

private:

    // IDs of the four colored squares of "Simon Says".
    enum class ColorIndex
    {
        Green, Red, Yellow, Blue,

        // Number of enum entries / invalid dummy:
        Count, Invalid = Count
    };

    // Internal helpers:
    void checkAppEvents();
    void checkFailCondition();
    void renderGameBoard();
    void newRandomColors();
    void drawColoredSquare(ColorIndex colorIdx, bool halfTone);

    // Random colors drawn for the current turn.
    std::vector<ColorIndex> colorsThisTurn;

    // User input this turn.
    std::vector<ColorIndex> userInputColors;
    ColorIndex lastUserColor = ColorIndex::Invalid;

    // Number of colors the player will have to remember next time.
    // Start of with an easy 2 colors.
    unsigned int numColorsNextTurn = 2;

    // Number of colors we still draw before ending
    // the display part of the current turn.
    unsigned int numColorsLeftToDisplay = 0;

    // Pseudo-random engine:
    std::mt19937 randGenerator{ std::random_device()() };
    std::uniform_int_distribution<int> randDist{ 0, static_cast<int>(ColorIndex::Count) - 1 };

    // Misc helper data:
    SDL_Window   * window   = nullptr;
    SDL_Renderer * renderer = nullptr;
    bool           lostGame = false;
    bool           isDone   = false;
};

} // namespace cr {}

#endif // CR_SIMON_SAYS_HPP

simon_says.cpp:

#include "simon_says.hpp"
#include <array>
#include <cassert>
#include <iostream>

// --------------------------------------------------------

namespace cr
{

namespace {

// Dimensions of our game window:
constexpr int WinWidth  = 400;
constexpr int WinHeight = 400;

// Small border (in pixels) on each side of a square:
constexpr int RectOffset = 4;

// The four colored rectangles:
const std::array<SDL_Rect, 4> rects = {{
    { RectOffset,                  RectOffset,                   (WinWidth / 2) - (RectOffset * 2), (WinHeight / 2) - (RectOffset * 2) },
    { (WinWidth / 2) + RectOffset, RectOffset,                   (WinWidth / 2) - (RectOffset * 2), (WinHeight / 2) - (RectOffset * 2) },
    { RectOffset,                  (WinHeight / 2) + RectOffset, (WinWidth / 2) - (RectOffset * 2), (WinHeight / 2) - (RectOffset * 2) },
    { (WinWidth / 2) + RectOffset, (WinHeight / 2) + RectOffset, (WinWidth / 2) - (RectOffset * 2), (WinHeight / 2) - (RectOffset * 2) }
}};

// And their RGB colors:
const std::array<unsigned char[3], 4> colors = {{
    { 0,   255, 0   }, // green
    { 255, 0,   0   }, // red
    { 255, 255, 0   }, // yellow
    { 0,   0,   255 }  // blue
}};

// Test if the (x,y) point is inside the SDL_Rect.
inline bool rectContainsPoint(const SDL_Rect & rect, const int x, const int y)
{
    if (x < rect.x) { return false; }
    if (y < rect.y) { return false; }
    if (x > (rect.x + rect.w)) { return false; }
    if (y > (rect.y + rect.h)) { return false; }
    return true;
}

} // namespace {}

SimonSaysGame::~SimonSaysGame()
{
    if (renderer != nullptr)
    {
        SDL_DestroyRenderer(renderer);
    }

    if (window != nullptr)
    {
        SDL_DestroyWindow(window);
    }

    SDL_Quit();
}

bool SimonSaysGame::initSDL()
{
    if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS) != 0)
    {
        std::cerr << "Failed to initialize SDL! " << SDL_GetError() << std::endl;
        return false;
    }

    if ((window = SDL_CreateWindow(nullptr, 0, 0, WinWidth, WinHeight, SDL_WINDOW_OPENGL)) == nullptr)
    {
        std::cerr << "Could not initialize Window! " << SDL_GetError() << std::endl;
        return false;
    }

    if ((renderer = SDL_CreateRenderer(window, -1, 0)) == nullptr)
    {
        std::cerr << "Could not create Renderer! " << SDL_GetError() << std::endl;
        return false;
    }

    newRandomColors();
    return true;
}

void SimonSaysGame::checkAppEvents()
{
    SDL_Event event;
    while (SDL_PollEvent(&event))
    {
        if (event.type == SDL_QUIT)
        {
            isDone = true;
        }
        else if (event.type == SDL_MOUSEBUTTONDOWN)
        {
            for (std::size_t i = 0; i < rects.size(); ++i)
            {
                if (!rectContainsPoint(rects[i], event.motion.x, event.motion.y))
                {
                    continue;
                }

                lastUserColor = static_cast<ColorIndex>(i);
                userInputColors.push_back(lastUserColor);
                std::cout << "Clicked square #" << i << "\n";
            }
        }
    }
}

void SimonSaysGame::checkFailCondition()
{
    if (lastUserColor == ColorIndex::Invalid)
    {
        return; // Didn't start playing yet.
    }

    if (!std::equal(std::begin(userInputColors), std::end(userInputColors), colorsThisTurn.rbegin()))
    {
        lostGame = true;
        std::cout << "You loose!\n";
    }
}

void SimonSaysGame::newRandomColors()
{
    colorsThisTurn.clear();
    userInputColors.clear();
    lastUserColor = ColorIndex::Invalid;

    std::cout << "Generating #" << numColorsNextTurn << " colors this turn.\n";

    for (unsigned int i = 0; i < numColorsNextTurn; ++i)
    {
        colorsThisTurn.push_back(static_cast<ColorIndex>(randDist(randGenerator)));
    }

    numColorsLeftToDisplay = static_cast<unsigned int>(colorsThisTurn.size());

    // One more color next turn.
    ++numColorsNextTurn;
}

void SimonSaysGame::renderGameBoard()
{
    if (lostGame)
    {
        // Draw a blood red screen and stall.
        // User will have to reset the app to play again :(
        SDL_SetRenderDrawColor(renderer, 180, 0, 0, 255);
        SDL_RenderClear(renderer);
        SDL_RenderPresent(renderer);
        return;
    }

    SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
    SDL_RenderClear(renderer);

    ColorIndex colorIdx;

    // Draw the random color set of current turn once:
    if (numColorsLeftToDisplay != 0)
    {
        colorIdx = colorsThisTurn[--numColorsLeftToDisplay];
    }
    else
    {
        // Time to draw the user input.
        colorIdx = lastUserColor;
    }

    if (colorIdx != ColorIndex::Invalid)
    {
        drawColoredSquare(colorIdx, false);
    }

    // We need to draw the selected colored square (if any) with
    // full brightness. The other three with a half-tone/faded color.
    for (int i = 0; i < static_cast<int>(ColorIndex::Count); ++i)
    {
        if (static_cast<ColorIndex>(i) != colorIdx)
        {
            drawColoredSquare(static_cast<ColorIndex>(i), true);
        }
    }

    SDL_RenderPresent(renderer);
}

void SimonSaysGame::drawColoredSquare(const ColorIndex colorIdx, const bool halfTone)
{
    const std::size_t index = static_cast<std::size_t>(colorIdx);
    assert(index < rects.size() && index < colors.size());

    const unsigned char colorScale = (halfTone ? 4 : 1);
    SDL_SetRenderDrawColor(renderer,
        colors[index][0] / colorScale,
        colors[index][1] / colorScale,
        colors[index][2] / colorScale, 255);

    SDL_RenderFillRect(renderer, &rects[index]);
}

void SimonSaysGame::runGameLoop()
{
    while (!isDone)
    {
        checkAppEvents();
        renderGameBoard();
        checkFailCondition();

        if (userInputColors.size() == colorsThisTurn.size() && !lostGame)
        {
            // Shuffle for a new turn.
            newRandomColors();
        }

        // We handle the "speed" of the game with a simple delay.
        // A rather crude way, but works...
        SDL_Delay(700);
    }
}

} // namespace cr {}

// --------------------------------------------------------

int main(int /* argc */, const char ** /* argv */)
{
    cr::SimonSaysGame game;

    if (!game.initSDL())
    {
        return -1;
    }

    game.runGameLoop();
}

And this is how it looks:

enter image description here

All reviews and comments are welcome.

glampert
  • 17.2k
  • 4
  • 30
  • 89