University of Wales Trinity Saint David
ACCB5017 - Games Architecture and Mechanics

Almost Fruit Ninja

Overview

This lab builds Almost Fruit Ninja, a small SFML game centred on a single Game class, one part at a time. Starting from an empty Visual Studio project, the game grows from a bare window that spawns and despawns falling targets into a fully playable game: mouse-based slicing with a live score, a limited-use super move, a lives system with a game over condition, difficulty that scales with score, and finally keyboard-based colour slicing that replaces the mouse entirely. Each part edits the same project in place, so by the end the game contains everything built across every part.

Learning Objectives

By the end of this lab you will be able to:

Concept

Part 1 - Falling Targets and the Game Loop

A game that spawns an object every single frame quickly runs out of control, the screen fills instantly and the vector holding those objects grows without bound. What is actually needed is a steady trickle, one new target appears every so often, and only while the number currently on screen is below some ceiling. That calls for two pieces of state working together, a spawn timer that counts up every frame and a maximum count that the current number of live targets is compared against before a new one is allowed to appear.

setFramerateLimit(60) caps the window at roughly 60 frames per second, it does not guarantee that rate, so counting frames rather than measuring real elapsed time is a deliberately simplified, frame-based stand-in for a clock. It is close enough for a steady spawn cadence here, though it means spawning and movement both stay tied to however fast frames actually arrive rather than to real time. Each frame adds a fixed amount to the spawn timer, and once that timer reaches a threshold and the target count is still below the maximum, a new sf::RectangleShape is pushed into a std::vector at a random x position and the timer resets to zero. Every existing target then needs to move a little further down the window with move(), and any target whose position has passed the bottom edge needs to disappear. Removing an element from the middle of a std::vector while a loop is still walking through it is done with the index-controlled erase loop, calling erase() on the element's iterator and skipping the usual increment for that pass so the next element is not accidentally skipped.

Part 2 - Mouse Slicing and Score Display

A target falling down the window has a position measured in the same coordinate space the window draws everything in, but the mouse cursor's position, as SFML reports it, is measured in screen pixels relative to the window's corner. Those two coordinate spaces usually line up, but comparing them directly breaks the moment the window is resized, moved, or given a scrolled or zoomed view. The safe way to compare a mouse click against a target's bounds is to convert the raw pixel position into the window's own coordinate space first, with window.mapPixelToCoords().

Once the mouse position is in the right coordinate space, testing whether it lies over a specific target is a single call, target.getGlobalBounds().contains(mouseViewPosition), which returns true only when the point falls inside that target's rectangle. Combined with sf::Mouse::isButtonPressed(sf::Mouse::Button::Left), a target can be erased from the vector the instant the held mouse position overlaps it. Showing the running total on screen needs an sf::Text object, which in SFML 3 is constructed from a reference to the sf::Font it will render with, loaded ahead of time via font.openFromFile(). Because sf::Text stores a plain string rather than a number, updating the displayed score means building a fresh string every time the score changes, most simply with a std::stringstream and setString().

Part 3 - Super Move

A limited-use ability needs exactly one new piece of state, a counter that starts at some fixed number and only ever counts down. The action itself, clearing every target on screen, is a single call to std::vector::clear(), but the reward for using it depends on how many targets existed the instant before they were cleared, so that count has to be read and saved before clear() throws it away.

Making the ability limited means checking the counter before doing anything else, an early return right at the top of the function stops the whole action cold the instant the counter reaches zero, with no further code in that function able to run this call. The key press itself slots into the same updateEvents() loop already checking for sf::Event::KeyPressed, as one more if branch alongside the existing Escape check, this time matching sf::Keyboard::Key::Enter. Showing the remaining count on screen reuses the exact shape of work the score display already does, a second sf::Text object, constructed against the same font, refreshed through its own small helper function every time the underlying number it displays changes.

Part 4 - Lives and Game Over

Inside updateTargets(), a target can already leave the vector for two different reasons, the player sliced it, or it fell off the bottom of the window unsliced. Those two cases currently do exactly the same thing, erase the element and move on, but they need to mean something different to the player. Missing a target is a failure that should cost something, while slicing one is a success that already pays out through the score. The two erase sites sit in different parts of the same loop, so charging a cost only applies to the one that runs when a target crosses the bottom edge, the slicing branch stays untouched.

The cost itself is a lives counter that only ever decreases, following the same shape as the super move counter, a member variable, a paired sf::Text constructed against the shared font, and a small refresh function that rebuilds its string whenever the underlying number changes. What is different this time is what happens once the counter bottoms out. Rather than guarding an action with an early return the way triggerSuperMove() does, reaching zero lives needs to end the game outright, which in this program means calling window->close() from inside updateTargets() itself. Because running() just reports window->isOpen(), closing the window from anywhere in the code ends the main loop in main() the same way clicking the window's own close button already does.

Part 5 - Difficulty Scaling

Every target so far has fallen at the same fixed speed, written directly as a literal inside move(), and the maximum number allowed on screen has stayed fixed since initVariables() first set it. Making either of those change over time means turning the literal into a member variable that starts at the same value but can be reassigned later, once, from a completely different function, without touching the code that uses it.

Difficulty here is entirely driven by the existing score value, checked against a set of thresholds. A repeatable rule, like adding one to the maximum target count, needs its threshold to move forward every time it fires, otherwise the same crossing would trigger again on every single frame the score stays above it. That means storing the next threshold in its own member variable and advancing it by a fixed step each time the check passes, rather than testing against a hardcoded number directly. A rule that should only ever happen once, doubling the maximum target count, needs the opposite kind of guard, a bool flag checked alongside the threshold so the doubling cannot repeat once it has already happened. All of these checks read from score but change entirely different pieces of state, so they can be written as independent if statements inside one function, run once per frame.

Part 6 - Key-Based Slicing

A std::vector<sf::RectangleShape> works fine as long as every target only ever needs its shape, but slicing by colour means every target also needs to remember which colour it was given, and a plain sf::RectangleShape has nowhere to store that. The fix is a small struct bundling a shape together with an enum class value naming its colour, and changing the vector to hold that struct instead of a bare shape. Every place that used to read targets[i] as a shape directly now reads targets[i].shape, a small, mechanical change repeated everywhere the old type was used.

Turning an enum value into something the player can act on needs two small mapping functions, one from colour to an sf::Color for drawing, and one from a sf::Keyboard::Key back to the colour it represents. Because a key press is a single discrete event with no target under a cursor to check bounds against, slicing by key removes every target sharing that colour in one pass, using the same index-controlled erase loop already used elsewhere in this game to despawn targets that fall off the bottom of the window. Once slicing works this way, the mouse position tracking and the click-detection branch built earlier have no remaining purpose and should be deleted outright rather than left in place unused.

Tasks

Part 1 - Falling Targets and the Game Loop

A single Game class owns its own window and a capped pool of falling rectangle targets. This part declares the class, opens the window, spawns targets at a random x position gated by a timer and a maximum count, moves them down the screen every frame, and removes any target once it passes the bottom edge.

Task 1 - Creating the project and the Game class

Create a new Console App (C++) project the same way as in the Environment Setup lab, naming it AlmostFruitNinja, and set the solution platform to x64.

  1. The project template generates a source file named after the project. Rename it to main.cpp in Solution Explorer.
  2. In Solution Explorer, use Add > Class... to create a class named Game, generating a matching Game.h and Game.cpp.
  3. Delete the generated contents of Game.h and Game.cpp. The next tasks fill them back in.

Success condition: Solution Explorer shows an empty Game.h, an empty Game.cpp, and main.cpp still holding its stub main, and the project still builds.

Task 2 - Declaring the Game interface

Open Game.h and declare the class's public and private members. Only update(), render() and running() are ever called from outside the class, so everything else stays private.

  1. Include <SFML/Graphics.hpp> and <vector>.
  2. In the private section, declare a sf::RenderWindow* window and a sf::VideoMode videoMode.
  3. Declare a std::vector<sf::RectangleShape> targets member.
  4. Declare an unsigned int maxTargetCount, a float spawnTimer and a float spawnTimerMax.
  5. Declare private helper methods initVariables(), initWindow(), updateEvents(), sf::RectangleShape spawnTarget(), updateTargets() and renderTargets(), all returning void except spawnTarget().
  6. In the public section, declare a default constructor, a destructor, update(), render(), and const bool running() const.
Game.h
#pragma once

#include <SFML/Graphics.hpp>
#include <vector>

class Game
{
private:
    // TODO: sf::RenderWindow* window;
    // TODO: sf::VideoMode videoMode;

    // TODO: std::vector<sf::RectangleShape> targets;

    // TODO: unsigned int maxTargetCount;
    // TODO: float spawnTimer;
    // TODO: float spawnTimerMax;

    // TODO: initVariables(), initWindow(), updateEvents()
    // TODO: sf::RectangleShape spawnTarget();
    // TODO: updateTargets(), renderTargets()

public:
    // TODO: default constructor and destructor

    // TODO: update(), render()

    // TODO: const bool running() const;
};

Task 3 - Setting starting values in initVariables

Open Game.cpp, include "Game.h", and define Game::initVariables(). This sets every piece of state to a sensible starting point before the window even exists.

  1. Set window to nullptr.
  2. Set spawnTimerMax to 60.0f and spawnTimer to that same value, so a target is ready to spawn the moment the game starts.
  3. Set maxTargetCount to 5.
Game.cpp
#include "Game.h"

void Game::initVariables()
{
    // TODO: window = nullptr;

    // TODO: spawnTimerMax = 60.0f;
    // TODO: spawnTimer = spawnTimerMax;

    // TODO: maxTargetCount = 5;
}

Task 4 - Opening the window in initWindow

Below initVariables() in Game.cpp, define Game::initWindow().

  1. Set videoMode to sf::VideoMode({800u, 600u}).
  2. Allocate window with new sf::RenderWindow(videoMode, "Almost Fruit Ninja", sf::State::Windowed).
  3. Call window->setFramerateLimit(60).
Game.cpp
void Game::initWindow()
{
    // TODO: videoMode = sf::VideoMode({800u, 600u});

    // TODO: window = new sf::RenderWindow(videoMode, "Almost Fruit Ninja", sf::State::Windowed);
    // TODO: window->setFramerateLimit(60);
}

Task 5 - Polling window events and handling Escape

Below initWindow(), define Game::updateEvents(). SFML 3 returns each pending event as a std::optional<sf::Event>, checked with is<T>() and read with getIf<T>().

  1. Loop while (std::optional<sf::Event> event = window->pollEvent()).
  2. If event->is<sf::Event::Closed>(), call window->close().
  3. Otherwise, use const auto* keyPressed = event->getIf<sf::Event::KeyPressed>(). If keyPressed is not null and keyPressed->code == sf::Keyboard::Key::Escape, also call window->close().
Game.cpp
void Game::updateEvents()
{
    // TODO: while (std::optional<sf::Event> event = window->pollEvent())
    //   TODO: if event->is<sf::Event::Closed>(), close the window
    //   TODO: else if getIf<sf::Event::KeyPressed>() and code is Escape,
    //         close the window
}

Task 6 - Spawning a target at a random x position

Below updateEvents(), define Game::spawnTarget(). It builds and returns one new target, ready to be dropped into the window at the top edge.

  1. Create a local sf::RectangleShape target and set its size to sf::Vector2f(100.0f, 100.0f).
  2. Compute the widest valid x position as the window's width minus the target's own width, and set the target's position to a random x between 0 and that value, with y at 0.f. Use rand() and cast as needed.
  3. Set the target's fill colour to sf::Color::Red.
  4. Return the target.
Game.cpp
sf::RectangleShape Game::spawnTarget()
{
    sf::RectangleShape target;
    // TODO: target.setSize(sf::Vector2f(100.0f, 100.0f));

    // TODO: compute maxX = window's width - target's width
    // TODO: target.setPosition(sf::Vector2f(random x in [0, maxX], 0.f));

    // TODO: target.setFillColor(sf::Color::Red);

    return target;
}

Task 7 - Gating spawns with a timer and a maximum count

Below spawnTarget(), define Game::updateTargets(). Start with the spawning half of the function, the part that decides whether a new target is allowed to appear this frame.

  1. If targets.size() is less than maxTargetCount, check whether spawnTimer >= spawnTimerMax.
  2. If it is, push the result of spawnTarget() onto targets and reset spawnTimer to 0.0f.
  3. Otherwise, add 1.0f to spawnTimer.
Game.cpp
void Game::updateTargets()
{
    // TODO: if (targets.size() < maxTargetCount)
    //   TODO: if (spawnTimer >= spawnTimerMax)
    //     TODO: targets.push_back(spawnTarget());
    //     TODO: spawnTimer = 0.0f;
    //   TODO: else
    //     TODO: spawnTimer += 1.0f;
}

Task 8 - Moving targets down and despawning them off-screen

Still inside Game::updateTargets(), add a second loop after the spawning logic that advances every existing target and removes any that have fallen past the bottom of the window.

  1. Write a loop for (size_t i = 0; i < targets.size();) so the index only advances when nothing is erased this pass.
  2. Inside the loop, call targets[i].move(sf::Vector2f(0.f, 2.f)).
  3. If targets[i].getPosition().y is greater than videoMode.size.y, erase that element with targets.erase(targets.begin() + i) and continue so i is not incremented.
  4. Otherwise, increment i.
Game.cpp
void Game::updateTargets()
{
    if (targets.size() < maxTargetCount)
    {
        if (spawnTimer >= spawnTimerMax)
        {
            targets.push_back(spawnTarget());
            spawnTimer = 0.0f;
        }
        else
        {
            spawnTimer += 1.0f;
        }
    }

    // TODO: for (size_t i = 0; i < targets.size();)
    //   TODO: targets[i].move(sf::Vector2f(0.f, 2.f));
    //   TODO: if (targets[i].getPosition().y > videoMode.size.y)
    //     TODO: targets.erase(targets.begin() + i);
    //     TODO: continue;
    //   TODO: else
    //     TODO: ++i;
}

Task 9 - Rendering targets and wiring the constructor and destructor

Below updateTargets(), define Game::renderTargets(), the constructor, the destructor, update(), render() and running().

  1. In renderTargets(), loop over targets with a range-based for (auto& target : targets) and call window->draw(target) for each.
  2. In the constructor Game::Game(), call initVariables() then initWindow().
  3. In the destructor Game::~Game(), call delete window.
  4. In update(), call updateEvents() then updateTargets().
  5. In render(), call window->clear(), then renderTargets(), then window->display().
  6. Define const bool Game::running() const to return window->isOpen().
Game.cpp
void Game::renderTargets()
{
    // TODO: for (auto& target : targets) window->draw(target);
}

Game::Game()
{
    // TODO: initVariables();
    // TODO: initWindow();
}

Game::~Game()
{
    // TODO: delete window;
}

void Game::update()
{
    // TODO: updateEvents();
    // TODO: updateTargets();
}

void Game::render()
{
    // TODO: window->clear();
    // TODO: renderTargets();
    // TODO: window->display();
}

const bool Game::running() const
{
    // TODO: return window->isOpen();
    return false;
}

Task 10 - Launching the game from main

Open main.cpp.

  1. Include "Game.h" and <cstdlib> and <ctime>.
  2. In main, construct a Game game;, then call std::srand(static_cast<unsigned int>(time(NULL))) so spawnTarget()'s random positions differ between runs.
  3. Write while (game.running()) { game.update(); game.render(); }, then return 0.
  4. Build and run the program.

Success condition: a window titled Almost Fruit Ninja opens, and red rectangles keep appearing near the top at random x positions, drifting down, and disappearing once they reach the bottom, never more than five on screen at once.

main.cpp
#include "Game.h"
#include <cstdlib>
#include <ctime>

int main()
{
    // TODO: construct a Game, seed std::rand with the current time,
    //       then loop update()/render() while game.running()
    return 0;
}

Part 2 - Mouse Slicing and Score Display

This part adds slicing and a score. Holding the left mouse button over a target removes it using the window's mapped view coordinates, and a running score is displayed on screen through an sf::Text object rebuilt every time a target is sliced.

Task 11 - Declaring the mouse and score members

Open Game.h and add the members this part needs: somewhere to store the mouse's position, a running score, and the font and text objects that will display it.

  1. Add <sstream> to the includes.
  2. In the private section, declare a sf::Vector2i mouseWindowPosition and a sf::Vector2f mouseViewPosition.
  3. Declare a private void updateMousePosition().
  4. Declare an unsigned int score, a sf::Font font, and a sf::Text scoreText.
  5. Declare a private void refreshScoreText() that will rebuild scoreText's string whenever score changes.

sf::Text in SFML 3 has no default constructor, it must be constructed with a reference to the font it renders with. The moment scoreText exists as a member, Game's constructor has to construct it too, or the project stops compiling. Open Game.cpp and change the constructor's definition to Game::Game() : scoreText(font).

Success condition: the project still builds, with scoreText constructed against font but not yet positioned, sized, or drawn.

Game.h
#pragma once

#include <SFML/Graphics.hpp>
#include <vector>
#include <sstream>

class Game
{
private:
    sf::RenderWindow* window;
    sf::VideoMode videoMode;

    std::vector<sf::RectangleShape> targets;

    unsigned int maxTargetCount;
    float spawnTimer;
    float spawnTimerMax;

    // TODO: sf::Vector2i mouseWindowPosition;
    // TODO: sf::Vector2f mouseViewPosition;
    // TODO: void updateMousePosition();

    // TODO: unsigned int score;
    // TODO: sf::Font font;
    // TODO: sf::Text scoreText;
    // TODO: void refreshScoreText();

    void initVariables();
    void initWindow();
    void updateEvents();

    sf::RectangleShape spawnTarget();
    void updateTargets();
    void renderTargets();

public:
    Game();
    ~Game();

    void update();
    void render();

    const bool running() const;
};
Game.cpp
Game::Game()
    // TODO: : scoreText(font)
{
    initVariables();
    initWindow();
}

Task 12 - Loading the font and starting the score at zero

Open Game.cpp and extend Game::initVariables().

  1. Add <iostream> to the includes, needed for std::cerr.
  2. Set score to 0.
  3. Call font.openFromFile("C:/Windows/Fonts/arial.ttf"). If it returns false, that means the font failed to load, print an error to std::cerr in that case so a missing font is easy to notice.
Game.cpp
#include <iostream>

void Game::initVariables()
{
    window = nullptr;

    spawnTimerMax = 60.0f;
    spawnTimer = spawnTimerMax;

    maxTargetCount = 5;

    // TODO: score = 0;
    // TODO: if (!font.openFromFile("C:/Windows/Fonts/arial.ttf"))
    //   TODO: print an error to std::cerr
}

Task 13 - Tracking the mouse in the window’s view

Below updateEvents() in Game.cpp, define Game::updateMousePosition().

  1. Set mouseWindowPosition to sf::Mouse::getPosition(*window).
  2. Set mouseViewPosition to window->mapPixelToCoords(mouseWindowPosition).
Game.cpp
void Game::updateMousePosition()
{
    // TODO: mouseWindowPosition = sf::Mouse::getPosition(*window);
    // TODO: mouseViewPosition = window->mapPixelToCoords(mouseWindowPosition);
}

Task 14 - Calling updateMousePosition every frame

In Game::update(), call updateMousePosition() after updateEvents() and before updateTargets(), so the mouse position used for slicing this frame is always current.

Game.cpp
void Game::update()
{
    updateEvents();
    // TODO: updateMousePosition();
    updateTargets();
}

Task 15 - Detecting and removing a sliced target

In Game::updateTargets(), inside the loop that moves and despawns targets, add a check before the off-screen test that removes a target the player has clicked on.

  1. Inside the loop, before the off-screen check, test sf::Mouse::isButtonPressed(sf::Mouse::Button::Left).
  2. If it is pressed, test whether targets[i].getGlobalBounds().contains(mouseViewPosition).
  3. If both are true, erase that target with targets.erase(targets.begin() + i), increment score, and continue so i is not incremented.

Success condition: the project still builds. Clicking and holding over a falling target now removes it, though the score is not shown on screen yet.

Game.cpp
void Game::updateTargets()
{
    if (targets.size() < maxTargetCount)
    {
        if (spawnTimer >= spawnTimerMax)
        {
            targets.push_back(spawnTarget());
            spawnTimer = 0.0f;
        }
        else
        {
            spawnTimer += 1.0f;
        }
    }

    for (size_t i = 0; i < targets.size();)
    {
        targets[i].move(sf::Vector2f(0.f, 2.f));

        // TODO: if (sf::Mouse::isButtonPressed(sf::Mouse::Button::Left))
        //   TODO: if (targets[i].getGlobalBounds().contains(mouseViewPosition))
        //     TODO: targets.erase(targets.begin() + i);
        //     TODO: score++;
        //     TODO: continue;

        if (targets[i].getPosition().y > static_cast<float>(videoMode.size.y))
        {
            targets.erase(targets.begin() + i);
            continue;
        }

        ++i;
    }
}

Task 16 - Refreshing the score text

Below renderTargets() in Game.cpp, define Game::refreshScoreText().

  1. Build a std::stringstream, stream "Score: " and then score into it.
  2. Call scoreText.setString() with the stream's str().

Then call refreshScoreText() from updateTargets() right after score++, so the displayed text updates the instant a target is sliced.

Game.cpp
void Game::refreshScoreText()
{
    // TODO: std::stringstream scoreStream;
    // TODO: scoreStream << "Score: " << score;
    // TODO: scoreText.setString(scoreStream.str());
}

Task 17 - Positioning and displaying the score text

scoreText is already constructed against font from the previous task. This task gives it a position, a size, a colour, and starts drawing it.

  1. Inside the constructor's body, after initWindow(), set scoreText's position to sf::Vector2f(10.f, 10.f), its character size to 24, and its fill colour to sf::Color::White.
  2. Call refreshScoreText() so it starts showing Score: 0.
  3. In Game::render(), draw scoreText after renderTargets() and before window->display().

Success condition: a window titled Almost Fruit Ninja opens with Score: 0 shown in the top-left corner. Clicking and holding over a falling target removes it and the score climbs by one for each one sliced.

Game.cpp
Game::Game()
    : scoreText(font)
{
    initVariables();
    initWindow();

    // TODO: scoreText.setPosition(sf::Vector2f(10.f, 10.f));
    // TODO: scoreText.setCharacterSize(24);
    // TODO: scoreText.setFillColor(sf::Color::White);
    // TODO: refreshScoreText();
}

void Game::render()
{
    window->clear();
    renderTargets();
    // TODO: window->draw(scoreText);
    window->display();
}

Part 3 - Super Move

This part adds a limited-use super move. Pressing Enter clears every target currently on screen and awards one point per target cleared, guarded by an early return once a fixed number of uses runs out, with a second line of on-screen text showing how many uses remain.

Task 18 - Declaring the super move state

Open Game.h and add the members the super move needs.

  1. In the private section, declare an unsigned int superMovesRemaining.
  2. Declare a sf::Text superMoveText.
  3. Declare a private void triggerSuperMove() and a private void refreshSuperMoveText().

Like scoreText, superMoveText needs constructing with a reference to its font the moment it exists as a member. Open Game.cpp and extend the constructor's member initializer list to Game::Game() : scoreText(font), superMoveText(font).

Success condition: the project still builds, with superMoveText constructed against font but not yet positioned, sized, or drawn.

Game.h
// TODO: unsigned int superMovesRemaining;
// TODO: sf::Text superMoveText;
// TODO: void triggerSuperMove();
// TODO: void refreshSuperMoveText();
Game.cpp
Game::Game()
    : scoreText(font) // TODO: , superMoveText(font)
{
    initVariables();
    initWindow();

    scoreText.setPosition(sf::Vector2f(10.f, 10.f));
    scoreText.setCharacterSize(24);
    scoreText.setFillColor(sf::Color::White);
    refreshScoreText();
}

Task 19 - Initializing the super move count

Open Game.cpp and extend Game::initVariables() by setting superMovesRemaining to 3.

Game.cpp
void Game::initVariables()
{
    window = nullptr;

    spawnTimerMax = 60.0f;
    spawnTimer = spawnTimerMax;

    maxTargetCount = 5;

    score = 0;
    if (!font.openFromFile("C:/Windows/Fonts/arial.ttf"))
    {
        std::cerr << "Error loading font\n";
    }

    // TODO: superMovesRemaining = 3;
}

Task 20 - Handling the Enter key

In Game::updateEvents(), extend the existing keyPressed check with a second branch for the super move.

  1. Alongside the existing check for sf::Keyboard::Key::Escape, add an else if branch checking keyPressed->code == sf::Keyboard::Key::Enter.
  2. In that branch, call triggerSuperMove().
Game.cpp
void Game::updateEvents()
{
    while (std::optional<sf::Event> event = window->pollEvent())
    {
        if (event->is<sf::Event::Closed>())
        {
            window->close();
        }
        else if (const auto* keyPressed = event->getIf<sf::Event::KeyPressed>())
        {
            if (keyPressed->code == sf::Keyboard::Key::Escape)
            {
                window->close();
            }
            // TODO: else if (keyPressed->code == sf::Keyboard::Key::Enter)
            //   TODO: triggerSuperMove();
        }
    }
}

Task 21 - Guarding against an empty counter

Below updateMousePosition() in Game.cpp, define Game::triggerSuperMove(), starting with the guard clause that stops it doing anything once every use is spent.

  1. At the top of the function, if superMovesRemaining == 0, return immediately.
  2. Otherwise, decrement superMovesRemaining.
Game.cpp
void Game::triggerSuperMove()
{
    // TODO: if (superMovesRemaining == 0) return;

    // TODO: superMovesRemaining--;
}

Task 22 - Clearing every target and awarding points

Still inside Game::triggerSuperMove(), after decrementing superMovesRemaining, clear the board and pay out for it.

  1. Read targets.size() into a local variable before clearing anything, since clear() would otherwise throw that count away.
  2. Call targets.clear().
  3. Add the saved count onto score, then call refreshScoreText() so the change is visible immediately.

Success condition: pressing Enter clears every target on screen and adds one point per target cleared to the score display, but only while at least one use remains, and remaining uses are not shown yet.

Game.cpp
void Game::triggerSuperMove()
{
    if (superMovesRemaining == 0) return;

    superMovesRemaining--;

    // TODO: unsigned int clearedCount = static_cast<unsigned int>(targets.size());
    // TODO: targets.clear();

    // TODO: score += clearedCount;
    // TODO: refreshScoreText();
}

Task 23 - Refreshing the super move text

Below refreshScoreText() in Game.cpp, define Game::refreshSuperMoveText(), following the same shape as refreshScoreText().

  1. Build a std::stringstream, stream "Super Moves: " and then superMovesRemaining into it.
  2. Call superMoveText.setString() with the stream's str().

Then call refreshSuperMoveText() at the end of triggerSuperMove(), after the guard clause has already returned for an empty counter.

Game.cpp
void Game::refreshSuperMoveText()
{
    // TODO: std::stringstream superMoveStream;
    // TODO: superMoveStream << "Super Moves: " << superMovesRemaining;
    // TODO: superMoveText.setString(superMoveStream.str());
}

void Game::triggerSuperMove()
{
    if (superMovesRemaining == 0) return;

    superMovesRemaining--;

    unsigned int clearedCount = static_cast<unsigned int>(targets.size());
    targets.clear();

    score += clearedCount;
    refreshScoreText();

    // TODO: refreshSuperMoveText();
}

Task 24 - Positioning and displaying the super move text

superMoveText is already constructed against font from the previous task. This task gives it a position, a size, a colour, and starts drawing it.

  1. Inside the constructor's body, after the existing scoreText setup, set superMoveText's position to sf::Vector2f(10.f, 40.f), its character size to 24, and its fill colour to sf::Color::White.
  2. Call refreshSuperMoveText() so it starts showing Super Moves: 3.
  3. In Game::render(), draw superMoveText alongside scoreText.

Success condition: a second line under the score shows Super Moves: 3. Pressing Enter clears every target on screen, adds a point per target cleared, and counts down to Super Moves: 0, after which pressing Enter again has no effect.

Game.cpp
Game::Game()
    : scoreText(font), superMoveText(font)
{
    initVariables();
    initWindow();

    scoreText.setPosition(sf::Vector2f(10.f, 10.f));
    scoreText.setCharacterSize(24);
    scoreText.setFillColor(sf::Color::White);
    refreshScoreText();

    // TODO: superMoveText.setPosition(sf::Vector2f(10.f, 40.f));
    // TODO: superMoveText.setCharacterSize(24);
    // TODO: superMoveText.setFillColor(sf::Color::White);
    // TODO: refreshSuperMoveText();
}

void Game::render()
{
    window->clear();
    renderTargets();
    window->draw(scoreText);
    // TODO: window->draw(superMoveText);
    window->display();
}

Part 4 - Lives and Game Over

This part adds a fail condition. The player starts with a fixed number of lives, shown in a third line of on-screen text, loses one each time a target reaches the bottom unsliced, and the window closes on its own once lives reach zero.

Task 25 - Declaring lives state

Open Game.h and add the members lives needs.

  1. In the private section, declare an unsigned int lives.
  2. Declare a sf::Text livesText.
  3. Declare a private void refreshLivesText().

Like the other two text objects, livesText needs constructing with a reference to its font the moment it exists as a member. Open Game.cpp and extend the constructor's member initializer list to Game::Game() : scoreText(font), superMoveText(font), livesText(font).

Success condition: the project still builds, with livesText constructed against font but not yet positioned, sized, or drawn.

Game.h
// TODO: unsigned int lives;
// TODO: sf::Text livesText;
// TODO: void refreshLivesText();
Game.cpp
Game::Game()
    : scoreText(font), superMoveText(font) // TODO: , livesText(font)
{
    // ...existing constructor body from earlier tasks...
}

Task 26 - Initializing lives

Open Game.cpp and extend Game::initVariables() by setting lives to 3.

Game.cpp
void Game::initVariables()
{
    // ...existing initialization from earlier tasks...

    // TODO: lives = 3;
}

Task 27 - Losing a life when a target reaches the bottom

In Game::updateTargets(), find the second erase site, the one that removes a target once its y position passes videoMode.size.y. This is the branch that means the player missed one, so it is the only branch that should cost a life.

  1. Before erasing the target in that branch, decrement lives, but only when it is greater than zero. lives is unsigned, so decrementing it once it is already zero would wrap it around to a huge number instead of going negative.
  2. Do not change the earlier erase site inside the mouse-click branch, slicing a target must not cost a life.
Game.cpp
void Game::updateTargets()
{
    // ...spawn logic from earlier tasks...

    for (size_t i = 0; i < targets.size();)
    {
        targets[i].move(sf::Vector2f(0.f, 2.f));

        if (sf::Mouse::isButtonPressed(sf::Mouse::Button::Left))
        {
            if (targets[i].getGlobalBounds().contains(mouseViewPosition))
            {
                targets.erase(targets.begin() + i);
                score++;
                refreshScoreText();
                continue;
            }
        }

        if (targets[i].getPosition().y > static_cast<float>(videoMode.size.y))
        {
            // TODO: if (lives > 0) lives--;
            targets.erase(targets.begin() + i);
            continue;
        }

        ++i;
    }
}

Task 28 - Displaying remaining lives

Below refreshSuperMoveText() in Game.cpp, define Game::refreshLivesText(), following the same shape as the other two refresh functions.

  1. Build a std::stringstream, stream "Lives: " and then lives into it.
  2. Call livesText.setString() with the stream's str().

Then call refreshLivesText() in updateTargets(), right after decrementing lives in the branch from the previous task.

livesText is already constructed against font from the previous task. This task gives it a position, a size, a colour, and starts drawing it.

  1. Inside the constructor's body, after the superMoveText setup, set livesText's position to sf::Vector2f(10.f, 70.f), its character size to 24, and its fill colour to sf::Color::White, then call refreshLivesText().
  2. In Game::render(), draw livesText alongside the other two text objects.

Success condition: a third line under the super move counter shows Lives: 3, and it counts down by one each time a target reaches the bottom of the window unsliced.

Game.cpp
void Game::refreshLivesText()
{
    // TODO: std::stringstream livesStream;
    // TODO: livesStream << "Lives: " << lives;
    // TODO: livesText.setString(livesStream.str());
}

Game::Game()
    : scoreText(font), superMoveText(font), livesText(font)
{
    // ...existing constructor body from earlier tasks...

    // TODO: livesText.setPosition(sf::Vector2f(10.f, 70.f));
    // TODO: livesText.setCharacterSize(24);
    // TODO: livesText.setFillColor(sf::Color::White);
    // TODO: refreshLivesText();
}

void Game::render()
{
    window->clear();
    renderTargets();
    window->draw(scoreText);
    window->draw(superMoveText);
    // TODO: window->draw(livesText);
    window->display();
}

Task 29 - Ending the game at zero lives

Still in Game::updateTargets(), add the check that ends the game right where lives is decremented, not after the whole loop. Two targets can reach the bottom in the same frame, and checking only once after the loop would let the second miss run past zero unnoticed.

  1. Immediately after decrementing lives, test whether it is now 0.
  2. If it is, call window->close().

Success condition: letting three targets reach the bottom of the window without slicing any of them closes the window and ends the program, the same way clicking the close button does.

Game.cpp
void Game::updateTargets()
{
    // ...spawn logic from earlier tasks...

    for (size_t i = 0; i < targets.size();)
    {
        targets[i].move(sf::Vector2f(0.f, 2.f));

        if (sf::Mouse::isButtonPressed(sf::Mouse::Button::Left))
        {
            if (targets[i].getGlobalBounds().contains(mouseViewPosition))
            {
                targets.erase(targets.begin() + i);
                score++;
                refreshScoreText();
                continue;
            }
        }

        if (targets[i].getPosition().y > static_cast<float>(videoMode.size.y))
        {
            if (lives > 0)
            {
                lives--;
            }

            // TODO: if (lives == 0) window->close();

            targets.erase(targets.begin() + i);
            continue;
        }

        ++i;
    }
}

Part 5 - Difficulty Scaling

This part makes the game get harder the longer a run lasts. Crossing score thresholds repeatedly raises the maximum target count and fall speed, and a separate flag-guarded rule doubles the maximum target count exactly once at a higher score.

Task 30 - Replacing the fixed fall speed with a member variable

Open Game.h and declare a float fallSpeed in the private section, alongside maxTargetCount.

Open Game.cpp and make two changes.

  1. In initVariables(), set fallSpeed to 2.0f, the same value the literal used before.
  2. In updateTargets(), change targets[i].move(sf::Vector2f(0.f, 2.f)) to use fallSpeed in place of the literal 2.f.
Game.h
// TODO: float fallSpeed;
Game.cpp
void Game::initVariables()
{
    // ...existing initialization from earlier tasks...

    // TODO: fallSpeed = 2.0f;
}

void Game::updateTargets()
{
    // ...spawn logic from earlier tasks...

    for (size_t i = 0; i < targets.size();)
    {
        // TODO: targets[i].move(sf::Vector2f(0.f, fallSpeed));

        // ...slice, miss and despawn logic from earlier tasks...
    }
}

Task 31 - Declaring difficulty thresholds

Open Game.h and declare the thresholds each scaling rule needs.

  1. Declare an unsigned int nextCountThreshold for the repeatable maximum-count increase.
  2. Declare an unsigned int nextSpeedThreshold for the repeatable speed increase.
  3. Declare a bool hasDoubled for the one-time doubling rule.
  4. Declare a private void updateDifficulty().
Game.h
// TODO: unsigned int nextCountThreshold;
// TODO: unsigned int nextSpeedThreshold;
// TODO: bool hasDoubled;
// TODO: void updateDifficulty();

Task 32 - Initializing the thresholds

Extend Game::initVariables().

  1. Set nextCountThreshold to 5.
  2. Set nextSpeedThreshold to 10.
  3. Set hasDoubled to false.
Game.cpp
void Game::initVariables()
{
    // ...existing initialization from earlier tasks...

    // TODO: nextCountThreshold = 5;
    // TODO: nextSpeedThreshold = 10;
    // TODO: hasDoubled = false;
}

Task 33 - Increasing the maximum target count as score rises

Below renderTargets() in Game.cpp, define Game::updateDifficulty(), starting with the repeatable count-increase rule.

  1. If score >= nextCountThreshold, increment maxTargetCount and add 5 onto nextCountThreshold so the same crossing cannot fire again until score rises another five points.
Game.cpp
void Game::updateDifficulty()
{
    // TODO: if (score >= nextCountThreshold)
    //   TODO: maxTargetCount++;
    //   TODO: nextCountThreshold += 5;
}

Task 34 - Increasing fall speed as score rises

Still in Game::updateDifficulty(), add the repeatable speed-increase rule, following the same shape as the previous task.

  1. If score >= nextSpeedThreshold, add 0.5f onto fallSpeed and add 10 onto nextSpeedThreshold.
Game.cpp
void Game::updateDifficulty()
{
    if (score >= nextCountThreshold)
    {
        maxTargetCount++;
        nextCountThreshold += 5;
    }

    // TODO: if (score >= nextSpeedThreshold)
    //   TODO: fallSpeed += 0.5f;
    //   TODO: nextSpeedThreshold += 10;
}

Task 35 - Doubling the target count once

Still in Game::updateDifficulty(), add the one-time doubling rule. Unlike the previous two rules, this one must never fire more than once, so it needs a flag rather than an advancing threshold.

  1. If !hasDoubled && score >= 20, multiply maxTargetCount by 2 and set hasDoubled to true.
Game.cpp
void Game::updateDifficulty()
{
    if (score >= nextCountThreshold)
    {
        maxTargetCount++;
        nextCountThreshold += 5;
    }

    if (score >= nextSpeedThreshold)
    {
        fallSpeed += 0.5f;
        nextSpeedThreshold += 10;
    }

    // TODO: if (!hasDoubled && score >= 20)
    //   TODO: maxTargetCount *= 2;
    //   TODO: hasDoubled = true;
}

Task 36 - Calling updateDifficulty every frame

In Game::update(), call updateDifficulty() after updateTargets(), so every scaling rule is checked once per frame against whatever the score became this frame.

Game.cpp
void Game::update()
{
    updateEvents();
    updateMousePosition();
    updateTargets();
    // TODO: updateDifficulty();
}

Task 37 - Building and observing the scaling

Build and run the program, then play a full round.

  1. Slice targets until the score passes 5. Observe that up to six targets can now be on screen at once, one more than the starting maximum of five.
  2. Keep going past a score of 10. Observe that targets now fall visibly faster than they did at the start.
  3. Keep going past a score of 20. Observe that the maximum target count jumps sharply, roughly doubling, and does not jump again if the score keeps climbing past further multiples of five.

Success condition: the game visibly speeds up and grows busier the longer a run continues, with the count and speed increases repeating at their own fixed intervals and the doubling happening exactly once.

Part 6 - Key-Based Slicing

This part replaces mouse-click slicing with colour-based keyboard input. Every target spawns as one of four colours through a small Target struct, and pressing the key matching a colour slices every target of that colour at once, with the now-unused mouse-slicing code deleted.

Task 38 - Giving each target a colour

Open Game.h. Targets need to remember a colour now, which a plain sf::RectangleShape cannot store, so replace it with a small struct.

  1. Above the Game class, declare an enum class TargetColor { Red, Green, Blue, Yellow };.
  2. Also above the Game class, declare a struct Target { sf::RectangleShape shape; TargetColor color; };.
  3. Change the targets member from std::vector<sf::RectangleShape> to std::vector<Target>.
  4. Change spawnTarget()'s return type from sf::RectangleShape to Target.

Success condition: the project does not build yet, every place that used targets[i] as a shape directly now needs to change to targets[i].shape. The next tasks fix those one at a time.

Game.h
#pragma once

#include <SFML/Graphics.hpp>
#include <vector>
#include <sstream>

// TODO: enum class TargetColor { Red, Green, Blue, Yellow };

// TODO: struct Target { sf::RectangleShape shape; TargetColor color; };

class Game
{
private:
    sf::RenderWindow* window;
    sf::VideoMode videoMode;

    // TODO: std::vector<Target> targets;

    unsigned int maxTargetCount;
    float spawnTimer;
    float spawnTimerMax;
    float fallSpeed;

    unsigned int nextCountThreshold;
    unsigned int nextSpeedThreshold;
    bool hasDoubled;
    void updateDifficulty();

    unsigned int score;
    sf::Font font;
    sf::Text scoreText;
    void refreshScoreText();

    unsigned int superMovesRemaining;
    sf::Text superMoveText;
    void triggerSuperMove();
    void refreshSuperMoveText();

    unsigned int lives;
    sf::Text livesText;
    void refreshLivesText();

    void initVariables();
    void initWindow();
    void updateEvents();

    // TODO: Target spawnTarget();
    void updateTargets();
    void renderTargets();

public:
    Game();
    ~Game();

    void update();
    void render();

    const bool running() const;
};

Task 39 - Mapping a colour to a display colour and a key

Open Game.h and declare two private helper methods that translate between TargetColor and the two things it needs to become, something drawable and something the player can press.

  1. Declare a private sf::Color colorToSFColor(TargetColor color).
  2. Declare a private bool keyMatchesColor(sf::Keyboard::Key key, TargetColor color).

Success condition: the project still does not build, these are only declared so far.

Game.h
// TODO: sf::Color colorToSFColor(TargetColor color);
// TODO: bool keyMatchesColor(sf::Keyboard::Key key, TargetColor color);

Task 40 - Defining the colour and key mapping functions

Open Game.cpp and define both mapping functions, using a switch on color in each.

  1. In colorToSFColor(), return sf::Color::Red for TargetColor::Red, sf::Color::Green for TargetColor::Green, sf::Color::Blue for TargetColor::Blue, and sf::Color::Yellow for TargetColor::Yellow.
  2. In keyMatchesColor(), return whether key equals sf::Keyboard::Key::R for Red, G for Green, B for Blue, and Y for Yellow.

Success condition: the project still does not build, spawnTarget() and updateTargets() still reference the old shape-only type.

Game.cpp
sf::Color Game::colorToSFColor(TargetColor color)
{
    // TODO: switch (color)
    //   TODO: case TargetColor::Red: return sf::Color::Red;
    //   TODO: case TargetColor::Green: return sf::Color::Green;
    //   TODO: case TargetColor::Blue: return sf::Color::Blue;
    //   TODO: case TargetColor::Yellow: return sf::Color::Yellow;
    return sf::Color::White;
}

bool Game::keyMatchesColor(sf::Keyboard::Key key, TargetColor color)
{
    // TODO: switch (color)
    //   TODO: case TargetColor::Red: return key == sf::Keyboard::Key::R;
    //   TODO: case TargetColor::Green: return key == sf::Keyboard::Key::G;
    //   TODO: case TargetColor::Blue: return key == sf::Keyboard::Key::B;
    //   TODO: case TargetColor::Yellow: return key == sf::Keyboard::Key::Y;
    return false;
}

Task 41 - Assigning a random colour when a target spawns

In Game.cpp, update spawnTarget() to build and return a Target instead of a bare shape.

  1. Rename the local variable's type from sf::RectangleShape to Target, keeping the name target.
  2. Change every line that used to call a method directly on target to call it on target.shape instead.
  3. Pick a random TargetColor by generating rand() % 4 and casting the result to TargetColor, storing it in target.color.
  4. Set the shape's fill colour with colorToSFColor(target.color) instead of the fixed sf::Color::Red.

Success condition: the project still does not build, updateTargets() and renderTargets() still treat each element as a bare shape.

Game.cpp
Target Game::spawnTarget()
{
    Target target;
    target.shape.setSize(sf::Vector2f(100.0f, 100.0f));

    float maxX = static_cast<float>(window->getSize().x) - target.shape.getSize().x;
    target.shape.setPosition(sf::Vector2f(static_cast<float>(rand() % static_cast<int>(maxX)), 0.f));

    // TODO: target.color = static_cast<TargetColor>(rand() % 4);
    // TODO: target.shape.setFillColor(colorToSFColor(target.color));

    return target;
}

Task 42 - Updating movement, despawn and rendering for the new type

In Game.cpp, fix updateTargets() and renderTargets() to work with Target instead of a bare shape. For this task, leave the mouse-click slicing branch in place, it will be removed in a later task.

  1. In updateTargets(), change targets[i].move(...) to targets[i].shape.move(...).
  2. Change targets[i].getGlobalBounds() to targets[i].shape.getGlobalBounds(), and targets[i].getPosition() to targets[i].shape.getPosition().
  3. In renderTargets(), change the range-based loop's variable type so that window->draw(target) becomes window->draw(target.shape).

Success condition: the project builds again, and the game runs exactly as it did in Part 5 (Difficulty Scaling), mouse-click slicing still works, and every target now spawns in one of four colours instead of always red.

Game.cpp
void Game::updateTargets()
{
    // ...spawn logic from earlier tasks...

    for (size_t i = 0; i < targets.size();)
    {
        // TODO: targets[i].shape.move(sf::Vector2f(0.f, fallSpeed));

        if (sf::Mouse::isButtonPressed(sf::Mouse::Button::Left))
        {
            // TODO: if (targets[i].shape.getGlobalBounds().contains(mouseViewPosition))
            {
                targets.erase(targets.begin() + i);
                score++;
                refreshScoreText();
                continue;
            }
        }

        // TODO: if (targets[i].shape.getPosition().y > static_cast<float>(videoMode.size.y))
        {
            lives--;
            refreshLivesText();
            targets.erase(targets.begin() + i);
            continue;
        }

        ++i;
    }

    if (lives == 0)
    {
        window->close();
    }
}

void Game::renderTargets()
{
    for (auto& target : targets)
    {
        // TODO: window->draw(target.shape);
    }
}

Task 43 - Slicing every target of one colour at once

Below renderTargets() in Game.cpp, define a new private method void sliceTargetsOfColor(TargetColor color), declared in Game.h alongside updateTargets().

  1. In Game.h, declare void sliceTargetsOfColor(TargetColor color); in the private section.
  2. In Game.cpp, write a loop over targets using the same index-controlled erase loop as updateTargets().
  3. If a target's color equals the passed-in color, erase it and increment score. Otherwise, advance the index.
  4. After the loop, call refreshScoreText() once, so a key press that slices several targets at once only rebuilds the score string a single time.
Game.cpp
void Game::sliceTargetsOfColor(TargetColor color)
{
    // TODO: for (size_t i = 0; i < targets.size();)
    //   TODO: if (targets[i].color == color)
    //     TODO: targets.erase(targets.begin() + i);
    //     TODO: score++;
    //   TODO: else
    //     TODO: ++i;

    // TODO: refreshScoreText();
}

Task 44 - Wiring key presses to slicing

In Game::updateEvents(), extend the keyPressed branch with four new checks, one per colour, each calling sliceTargetsOfColor() through keyMatchesColor().

  1. Alongside the existing Escape and Enter checks, add four else if branches, one for each of TargetColor::Red, Green, Blue and Yellow, each testing keyMatchesColor(keyPressed->code, TargetColor::___).
  2. In each branch, call sliceTargetsOfColor() with the matching colour.

Success condition: pressing R, G, B or Y now slices every target of the matching colour on screen, awarding a point per target sliced, alongside the still-working mouse-click slicing.

Game.cpp
void Game::updateEvents()
{
    while (std::optional<sf::Event> event = window->pollEvent())
    {
        if (event->is<sf::Event::Closed>())
        {
            window->close();
        }
        else if (const auto* keyPressed = event->getIf<sf::Event::KeyPressed>())
        {
            if (keyPressed->code == sf::Keyboard::Key::Escape)
            {
                window->close();
            }
            else if (keyPressed->code == sf::Keyboard::Key::Enter)
            {
                triggerSuperMove();
            }
            // TODO: else if (keyMatchesColor(keyPressed->code, TargetColor::Red))
            //   TODO: sliceTargetsOfColor(TargetColor::Red);
            // TODO: else if (keyMatchesColor(keyPressed->code, TargetColor::Green))
            //   TODO: sliceTargetsOfColor(TargetColor::Green);
            // TODO: else if (keyMatchesColor(keyPressed->code, TargetColor::Blue))
            //   TODO: sliceTargetsOfColor(TargetColor::Blue);
            // TODO: else if (keyMatchesColor(keyPressed->code, TargetColor::Yellow))
            //   TODO: sliceTargetsOfColor(TargetColor::Yellow);
        }
    }
}

Task 45 - Removing the mouse-based slicing code

Mouse-click slicing has no remaining purpose now that colour-matched keys handle it. Leaving it in place would mean two different pieces of code can both remove the same target for two different reasons, so it comes out rather than staying dead and unused.

  1. In Game.h, delete the sf::Vector2i mouseWindowPosition, sf::Vector2f mouseViewPosition and void updateMousePosition() declarations.
  2. In Game.cpp, delete the Game::updateMousePosition() function entirely.
  3. In Game::update(), delete the call to updateMousePosition().
  4. In Game::updateTargets(), delete the if (sf::Mouse::isButtonPressed(...)) branch and its contents, leaving only the off-screen despawn check inside the loop.

Success condition: the project builds with no leftover references to the mouse. Clicking on a target no longer does anything, the only way to slice one is to press the key matching its colour.

Game.cpp
void Game::updateTargets()
{
    // ...spawn logic from earlier tasks...

    for (size_t i = 0; i < targets.size();)
    {
        targets[i].shape.move(sf::Vector2f(0.f, fallSpeed));

        // TODO: delete the mouse-click slicing branch entirely

        if (targets[i].shape.getPosition().y > static_cast<float>(videoMode.size.y))
        {
            lives--;
            refreshLivesText();
            targets.erase(targets.begin() + i);
            continue;
        }

        ++i;
    }

    if (lives == 0)
    {
        window->close();
    }
}

Task 46 - Building and playing the finished game

Build and run the program.

Success condition: targets fall in one of four colours, red, green, blue and yellow. Pressing R, G, B or Y slices every target of the matching colour currently on screen at once and increases the score. The mouse takes no part in slicing. Enter still triggers the super move, missing a target still costs a life, and the difficulty still scales with score exactly as it did in Part 5 (Difficulty Scaling).

Solution

Try the tasks above before revealing the solution.