Skip to main content

Practical Game Programming: 8. Game Development Projects

Practical Game Programming
8. Game Development Projects
  • Show the following:

    Annotations
    Resources
  • Adjust appearance:

    Font
    Font style
    Color Scheme
    Light
    Dark
    Annotation contrast
    Low
    High
    Margins
  • Search within:
    • My Notes + Comments
    • Notifications
    • Privacy
  • Project HomePractical Game Programming
  • Learn more about Manifold

Notes

table of contents
  1. Cover
  2. Acknowledgements
  3. 1. Introduction
    1. 1.1. Introduction to Video Games and Game Programming
    2. 1.2. History of Video Games
    3. 1.3. Types of Video Games
    4. 1.4. Road Map of Game Development
    5. 1.5. Set Up Game Development IDE
    6. 1.6. The Allegro 5 Game Library
      1. Exercises, Homework Questions, and Projects
  4. 2. Essentials of Game Programming with Allegro
    1. 2.1. Common Structure of a C/C++ Program with Allegro 5
    2. 2.2. Handling User Input
    3. 2.3. Collision Detection
    4. 2.4. Adding Sound Effects to Games
    5. 2.5. Timers and Game Timing
    6. 2.6. Using Files for Games
      1. Exercises, Homework Questions, and Projects
  5. 3. Using Graphics in Games
    1. 3.1. Bitmap Fundamentals
    2. 3.2. Bitmaps Creation and Use in Allegro 5
    3. 3.3. Utilizing Bitmaps in a Program
    4. 3.4. Scaling and Rotating Bitmaps
    5. 3.5. Displaying a Section of a Bitmap and the Concept of a Viewport
    6. 3.6. Advanced Graphics Techniques in Allegro 5
    7. 3.7. How Transformations Help with Resolution Independence
    8. 3.8. Antialiasing and Texture Smoothing
    9. 3.9. Shaders
    10. 3.10. Using Premultiplied Alpha in Allegro 5
      1. Exercises, Homework Questions, and Projects
  6. 4. Programming Sprites in Allegro 5
    1. 4.1. The Fundamentals
    2. 4.2. Additional Sprite Handling Routines
    3. 4.3. Allegro Blending Function and Sprites
    4. 4.4. Advanced Sprite Use in Allegro 5
      1. Exercises, Homework Questions, and Projects
  7. 5. Programming Backgrounds for Video Games
    1. 5.1. Types of Backgrounds
    2. 5.2. Creating and Using Static Backgrounds
    3. 5.3. Creating and Using Scrolling Backgrounds
    4. 5.4. Creating and Using Dynamic Backgrounds
    5. 5.5. Creating and Using 3D Backgrounds
      1. Exercises, Homework Questions, and Projects
  8. 6. Game Design and Development Fundamentals
    1. 6.1. Conceive an Amazing Game
    2. 6.2. Transforming a Billion-Dollar Idea into a Detailed Game Design
    3. 6.3. Implement the Game
      1. Exercises, Homework Questions, and Projects
  9. 7. Advanced Topics in Practical Game Programming
    1. 7.1. Multithreading
    2. 7.2. Custom Shaders
    3. 7.3. Networking for Multiplayer Games
    4. 7.4. AI Programming
    5. 7.5. Optimization Techniques
    6. 7.6. Custom GUI Systems
    7. 7.7. Advanced Audio
      1. Exercises, Homework Questions, and Projects
  10. 8. Game Development Projects
    1. 8.1. Action Game: Space Shooter
      1. Exercises, Homework Questions, and Projects
    2. 8.2. Platform Game: Tank War
      1. Exercises, Homework Questions, and Projects
    3. 8.3. Adventure Game
      1. Exercises, Homework Questions, and Projects
    4. 8.4. Arcade Game
      1. Exercises, Homework Questions, and Projects
    5. 8.5. Board Game
      1. Exercises, Homework Questions, and Projects
    6. 8.6. Emulator Game
      1. Exercises, Homework Questions, and Projects
    7. 8.7. Puzzle Game
      1. Exercises, Homework Questions, and Projects
    8. 8.8. Role-Playing Game
      1. Exercises, Homework Questions, and Projects
    9. 8.9. Sports Game
      1. Exercises, Homework Questions, and Projects
    10. 8.10. Strategy Game
      1. Exercises, Homework Questions, and Projects
    11. 8.11. Utilities Game
      1. Exercises, Homework Questions, and Projects
  11. References and Resources

Chapter 8.8 Game Development Projects

In previous chapters, we have acquired the fundamental knowledge and skills for game programming. In this chapter, we will design and implement some real games with Allegro in C or C++.

8.1. Action Game: Space Shooter

Among action games, space shooter games are a popular subgenre that involves controlling a spaceship and battling against waves of enemies, often in outer space settings. These games typically feature fast-paced gameplay, challenging levels, and a variety of power-ups and upgrades. Designing and implementing a space shooter game with Allegro in C++ is an exciting project.

8.1.1. Game Design

Game Concept

  • Genre: A 2D space shooter game where the player controls a spaceship, shoots enemies, and avoids obstacles.
  • Objective: The player controls a spaceship, shoots at incoming enemies, and tries to survive as long as possible.

Core Components

  • Player Ship: The player-controlled spaceship
    • Attributes: Position (x, y), speed, sprite
    • Movement: Controlled via keyboard input (left, right, up, down)
    • Shooting: Fires bullets when the player presses a key
  • Enemies: Enemy ships that move toward the player and can be destroyed
    • Attributes: Position (x, y), speed, sprite
    • Movement: Move downward toward the player
    • Spawning: New enemies spawn at the top of the screen at regular intervals
  • Bullets: Projectiles fired by the player and enemies
    • Attributes: Position (x, y), speed, sprite
    • Movement: Move upward (player bullets) or downward (enemy bullets)
  • Game State: Tracks the current state of the game (playing, game over, etc.)
    • Attributes: Current state (playing, game over), score, lives

Key Techniques and Features to Have

A 2D space shooter combines responsive controls, real-time entity management, and fast feedback into a tightly coupled gameplay loop. The following features define a clear and functional baseline for an engaging space shooter implemented with Allegro.

  1. 1. Player Spaceship Control and Movement

    The player ship should support smooth four-directional movement using keyboard input and remain clamped within screen boundaries. Input handling must be responsive to allow precise dodging and positioning.

  2. 2. Player Shooting Mechanics

    The player should be able to fire bullets upward using a cooldown-limited shooting system. Bullets must spawn relative to the ship’s sprite and travel at a fixed speed, with active bullets stored in a dynamic container.

  3. 3. Enemy Ship Behavior and Spawning

    Enemy ships should spawn from the top of the screen, move downward, and vary in speed or pattern to increase difficulty. Spawning may occur at regular intervals or in structured waves.

  4. 4. Collision Detection

    Collisions between player bullets and enemies, as well as between enemies and the player, should be detected using AABB checks. Entities involved in collisions must be removed or deactivated immediately.

  5. 5. Game State, Scoring, and Survival Rules

    The game should track core states such as playing and game over. Destroying enemies should increase the score, and optional lives or health systems may extend gameplay longevity.

  6. 6. Enemy Variations and Difficulty Scaling

    Difficulty should increase over time by introducing faster enemies, higher health values, or new movement and attack patterns. Scaling should remain gradual to preserve playability.

  7. 7. Power-Ups and Collectibles (Optional)

    Power-ups such as rapid fire, shields, spread shots, or speed boosts may be added. These should spawn occasionally and apply temporary effects to the player.

  8. 8. Visual and Gameplay Feedback

    Clear sprites for the player, enemies, and bullets should reinforce readability. Visual or audio feedback—such as flashes, explosions, or sounds—should confirm hits and successful actions.

  9. 9. Main Game Loop and Allegro Integration

    An Allegro timer (e.g., 60 fps) should drive all updates, including movement, spawning, collision checks, and rendering. Game logic and rendering should be separated into distinct update and render phases.

  10. 10. Asset Management and Cleanup

    All visual assets should be loaded during Initialization and released cleanly on shutdown. Allegro resources such as bitmaps, timers, event queues, and displays must be properly destroyed to ensure stability.

8.1.2. Implementation

Set Up Your Development Environment

Before writing any code, it’s important to ensure that your tools and libraries are properly installed and configured. The following checklist summarizes the required software and setup steps needed to build and run the project.

Create Project Structure

A clean and organized folder layout helps keep the project maintainable as it grows. The structure here shows how the game’s source files and assets should be arranged:

A parent folder SpaceShooter containing the file CMakeLists.txt and two subfolders, src and assets. The src subfolder contains the files main.cpp, spaceship.cpp, spaceship.h, enemy.cpp, enemy.h, bullet.cpp, bullet.h, game.cpp, and game.h. The assets subfolder contains the files spaceship.png, enemy.png, bullet.png, and background.png.

Write CMakeLists.txt for Ninja Build

To compile the project efficiently, you’ll use CMake with the Ninja generator. The following configuration file sets up the build process and links the Allegro libraries required by the game:

cmake_minimum_required(VERSION 3.10)

project(SpaceShooter)

set(CMAKE_CXX_STANDARD 17)

set(CMAKE_CXX_STANDARD_REQUIRED True)

add_executable(SpaceShooter src/main.cpp src/spaceship.cpp src/enemy.cpp src/bullet.cpp src/game.cpp)

find_package(PkgConfig REQUIRED)

pkg_check_modules(ALLEGRO5 REQUIRED allegro-5 allegro_main-5 allegro_image-5 allegro_font-5 allegro_ttf-5 allegro_primitives-5)

include_directories(${ALLEGRO5_INCLUDE_DIRS})

target_link_libraries(SpaceShooter ${ALLEGRO5_LIBRARIES})

Key Operations in the Game

A space shooter game runs through a continuous cycle of updates and rendering steps that keep gameplay responsive and engaging. These operations define how the player interacts with the game world, how enemies behave, and how the system maintains real-time action. The following list summarizes the essential operations executed during each frame of the game loop:

  1. 1. Process Player Input
    • • Read keyboard state to determine movement and shooting actions.
    • • Apply movement constraints to keep the ship within screen boundaries.
  2. 2. Update Player, Enemy, and Bullet States
    • • Move the player ship based on input.
    • • Advance bullets upward or downward depending on their type.
    • • Update enemy positions as they drift or spawn from the top of the screen.
  3. 3. Spawn New Enemies
    • • Introduce new enemy ships at timed intervals or in waves.
    • • Randomize spawn positions or speeds to increase difficulty.
  4. 4. Perform Collision Detection
    • • Check for collisions between bullets and enemies.
    • • Detect collisions between enemies and the player ship.
    • • Remove or deactivate any entities involved in a collision.
  5. 5. Manage Game State and Scoring
    • • Track whether the game is running, paused, or over.
    • • Update the score when enemies are destroyed.
    • • Handle player defeat conditions.
  6. 6. Clean Up Inactive Entities
    • • Remove bullets that leave the screen.
    • • Remove enemies that are destroyed or move off-screen.
  7. 7. Render the Frame
    • • Draw the background, player ship, enemies, bullets, and UI elements.
    • • Flip the display to present the updated frame to the player.

Structure of the Game Program

A 2D space shooter is composed of several interconnected systems that manage player control, enemy behavior, real-time action, and visual rendering. Organizing these systems clearly helps ensure responsive gameplay, predictable behavior, and ease of extension as new features such as scoring, power-ups, or additional enemy types are added. The following outline describes the typical structure of a space shooter game program:

  1. 1. Initialization
    • • Initialize Allegro subsystems (display, keyboard input, image, primitives, fonts).
    • • Create the main display window, event queue, and timer.
    • • Install input devices and prepare the game clock (e.g., 60 fps).
  2. 2. Resource Loading
    • • Load sprite assets for the player ship, enemies, bullets, and background.
    • • Load fonts and any sound assets used for feedback.
    • • Validate that all required resources are available before entering gameplay.
  3. 3. Entity Creation
    • • Create the player ship with its initial position and movement parameters.
    • • Initialize enemy containers and enemy spawn timers.
    • • Prepare data structures for bullets and other dynamic entities.
  4. 4. Input Handling
    • • Capture keyboard events or keyboard state each frame.
    • • Map input to player movement and shooting actions.
    • • Handle global inputs such as pause, restart, or window-close events.
  5. 5. Update Functions
    • • Update the player’s position based on input and apply screen boundaries.
    • • Advance bullets and enemy positions each frame.
    • • Spawn new enemies according to timers or wave rules.
    • • Detect and resolve collisions between bullets, enemies, and the player.
    • • Update score, lives, and game state as required.
  6. 6. Rendering Functions
    • • Clear the screen and draw the background.
    • • Render the player ship, enemies, and active bullets.
    • • Draw UI elements such as score, lives, or game-over messages.
    • • Flip the display to present the rendered frame.
  7. 7. Game Loop
    • • Wait for events from the event queue.
    • • On timer events, update the game state and render the scene.
    • • Maintain a consistent update rate to ensure smooth gameplay.
  8. 8. Shutdown
    • • Destroy timers, event queues, fonts, and the display.
    • • Release loaded bitmaps and other resources.
    • • Exit the program cleanly.

Coding the Game

The implementation is divided into several source files, each responsible for a specific part of the game’s functionality. The following sections walk through the purpose and contents of each file.

1. main.cpp

#include "game.h"

#include <allegro5/allegro_native_dialog.h>

int main() {

    Game game;

    if (!game.init()) {

        al_show_native_message_box(nullptr, "Init failed", "Init returned false",

                                   "Pausing 5 seconds so you can read prior errors.", nullptr, 0);

        al_rest(5.0);

        return 1;

    }

    al_show_native_message_box(nullptr, "Info", "Startup",

                               "Game initialized successfully!", nullptr, 0);

    game.run();

    al_show_native_message_box(nullptr, "Info", "Shutdown",

                               "Game exiting. Pausing 2 seconds.", nullptr, 0);

    al_rest(2.0);

    game.shutdown();

    return 0;

}

2. game.h

#ifndef GAME_H

#define GAME_H

#include <allegro5/allegro.h>

#include <allegro5/allegro_image.h>

#include <allegro5/allegro_primitives.h>

#include <vector>

#include <string>

#include "spaceship.h"

#include "enemy.h"

#include "bullet.h"

class Game {

public:

    Game();

    bool init();

    void run();

    void shutdown();

private:

    void update();

    void render();

    bool check_collision(ALLEGRO_BITMAP* bmp1, float ax, float ay,

                         ALLEGRO_BITMAP* bmp2, float bx, float by)

    // core Allegro

    ALLEGRO_DISPLAY* display;

    ALLEGRO_EVENT_QUEUE* event_queue;

    ALLEGRO_TIMER* timer;

    bool running;

    ALLEGRO_BITMAP* background;

    // gameplay

    Spaceship player;

    std::vector<Enemy> enemies;

    std::vector<Bullet> bullets;

    // input/game timing

    double last_shot_time;

    const double SHOT_COOLDOWN = 0.20; // seconds

    // asset paths

    std::string bullet_path;

};

#endif

3. game.cpp

#include "game.h"

#include <allegro5/allegro.h>

#include <allegro5/allegro_image.h>

#include <allegro5/allegro_primitives.h>

#include <allegro5/allegro_native_dialog.h>

#include <algorithm>

#include <string>

#include <cstdio>

// Build a full path like "<exe_dir>/<subdir>/<filename>"

static std::string exe_path_join(const char* subdir, const char* filename) {

    ALLEGRO_PATH* p = al_get_standard_path(ALLEGRO_EXENAME_PATH);

    if (subdir && *subdir) {

        al_append_path_component(p, subdir);

    }

    al_set_path_filename(p, filename);

    std::string out = al_path_cstr(p, ALLEGRO_NATIVE_PATH_SEP);

    al_destroy_path(p);

    return out;

}

Game::Game()

    : display(nullptr),

      event_queue(nullptr),

      timer(nullptr),

      running(false),

      background(nullptr),

      player(400.0f, 520.0f),

      last_shot_time(0.0) {}

bool Game::init() {

    std::fprintf(stderr, "[INIT] Allegro starting\n");

    if (!al_init()) {

        al_show_native_message_box(nullptr, "Error", "Allegro Init", "Failed to initialize Allegro!", nullptr, 0);

        return false;

    }

    if (!al_install_keyboard()) {

        al_show_native_message_box(nullptr, "Error", "Keyboard", "Failed to install keyboard!", nullptr, 0);

        return false;

    }

    if (!al_init_image_addon()) {

        al_show_native_message_box(nullptr, "Error", "Image Addon", "Failed to initialize image addon!", nullptr, 0);

        return false;

    }

    if (!al_init_primitives_addon()) {

        al_show_native_message_box(nullptr, "Error", "Primitives Addon", "Failed to initialize primitives addon!", nullptr, 0);

        return false;

    }

    display = al_create_display(800, 600);

    if (!display) {

        al_show_native_message_box(nullptr, "Error", "Display Creation", "Failed to create display!", nullptr, 0);

        return false;

    }

    //——Asset loads relative to the EXE directory——

    const std::string bgPath    = exe_path_join("assets", "background.png");

    const std::string shipPath  = exe_path_join("assets", "ship.png");

    const std::string enemyPath = exe_path_join("assets", "enemy.png");

    bullet_path                 = exe_path_join("assets", "bullet.png");

    background = al_load_bitmap(bgPath.c_str());

    if (!background) {

        al_show_native_message_box(display, "Asset error", "Load failed",

                                   ("Failed to load " + bgPath).c_str(), nullptr, 0);

        return false;

    }

    if (!player.load(shipPath.c_str())) {

        al_show_native_message_box(display, "Asset error", "Load failed",

                                   ("Failed to load " + shipPath).c_str(), nullptr, 0);

        return false;

    }

    // Starter enemies across the top

    enemies.clear();

    for (int i = 0; i < 5; ++i) {

        float x = 60.0f + i * 140.0f;

        float y = 20.0f;

        enemies.emplace_back(x, y, enemyPath.c_str());

    }

    // Event queue + timer

    event_queue = al_create_event_queue();

    if (!event_queue) {

        al_show_native_message_box(nullptr, "Error", "Event Queue", "Failed to create event queue!", nullptr, 0);

        return false;

    }

    timer = al_create_timer(1.0 / 60.0);

    if (!timer) {

        al_show_native_message_box(nullptr, "Error", "Timer", "Failed to create timer!", nullptr, 0);

        return false;

    }

    al_register_event_source(event_queue, al_get_display_event_source(display));

    al_register_event_source(event_queue, al_get_timer_event_source(timer));

    al_register_event_source(event_queue, al_get_keyboard_event_source());

    return true;

}

void Game::run() {

    std::fprintf(stderr, "[RUN] start\n");

    running = true;

    al_start_timer(timer);

    // Initial clear so we don't see junk before the first flip

    al_clear_to_color(al_map_rgb(0, 0, 0));

    al_flip_display();

    while (running) {

        ALLEGRO_EVENT ev;

        if (al_wait_for_event_timed(event_queue, &ev, 0.25)) {

            switch (ev.type) {

                case ALLEGRO_EVENT_TIMER:

                    std::fprintf(stderr, "[EV] TIMER\n");

                    update();

                    render();

                    break;

                case ALLEGRO_EVENT_DISPLAY_CLOSE:

                    std::fprintf(stderr, "[EV] DISPLAY_CLOSE\n");

                    running = false;

                    break;

                case ALLEGRO_EVENT_KEY_DOWN:

                    std::fprintf(stderr, "[EV] KEY_DOWN: %d\n", ev.keyboard.keycode);

                    if (ev.keyboard.keycode == ALLEGRO_KEY_ESCAPE) {

                        running = false;

                    }

                    break;

                default:

                    std::fprintf(stderr, "[EV] other: %u\n", ev.type);

                    break;

            }

        } else {

            // No event within timeout—render anyway to keep window responsive

            render();

        }

    }

    std::fprintf(stderr, "[RUN] end\n");

}

//————methods————

void Game::update() {

    player.update();

    ALLEGRO_KEYBOARD_STATE ks;

    al_get_keyboard_state(&ks);

    double now = al_get_time();

    if (al_key_down(&ks, ALLEGRO_KEY_SPACE) && (now—last_shot_time) >= SHOT_COOLDOWN) {

        player.shoot(bullets, bullet_path.c_str());

        last_shot_time = now;

    }

    for (auto& b : bullets) b.update();

    bullets.erase(std::remove_if(bullets.begin(), bullets.end(),

                                 [](const Bullet& b){ return !b.is_active(); }),

                  bullets.end());

    for (auto& e : enemies) e.update();

    for (auto& e : enemies) {

        if (!e.is_active() || !e.get_bitmap()) continue;

        for (auto& b : bullets) {

            if (!b.is_active() || !b.get_bitmap()) continue;

            if (check_collision(b.get_bitmap(), b.get_x(), b.get_y(),

                                e.get_bitmap(), e.get_x(), e.get_y())) {

                b.set_active(false);

                e.set_active(false);

                break;

            }

        }

    }

    if (player.get_bitmap()) {

        for (auto& e : enemies) {

            if (!e.is_active() || !e.get_bitmap()) continue;

            if (check_collision(player.get_bitmap(), player.get_x(), player.get_y(),

                                e.get_bitmap(), e.get_x(), e.get_y())) {

                std::fprintf(stderr, "[GAME] Player collided with enemy—ending\n");

                running = false;

                break;

            }

        }

    }

}

void Game::render() {

    if (background) al_draw_bitmap(background, 0, 0, 0);

    player.render();

    for (auto& e : enemies) e.render();

    for (auto& b : bullets) b.render();

    al_flip_display();

}

bool Game::check_collision(ALLEGRO_BITMAP* a, float ax, float ay,

                           ALLEGRO_BITMAP* b, float bx, float by) {

    if (!a || !b) return false;

    const float aw = static_cast<float>(al_get_bitmap_width(a));

    const float ah = static_cast<float>(al_get_bitmap_height(a));

    const float bw = static_cast<float>(al_get_bitmap_width(b));

    const float bh = static_cast<float>(al_get_bitmap_height(b));

    const float a_right  = ax + aw;

    const float a_bottom = ay + ah;

    const float b_right  = bx + bw;

    const float b_bottom = by + bh;

    return (ax < b_right) && (a_right > bx) && (ay < b_bottom) && (a_bottom > by);

}

void Game::shutdown() {

    if (timer) { al_stop_timer(timer); al_destroy_timer(timer); timer = nullptr; }

    if (event_queue) { al_destroy_event_queue(event_queue); event_queue = nullptr; }

    if (background) { al_destroy_bitmap(background); background = nullptr; }

    bullets.clear();

    enemies.clear();

    if (display) { al_destroy_display(display); display = nullptr; }

    // Optional: shutdown addons (safe to omit; Allegro cleans up on exit)

    // al_shutdown_primitives_addon();

    // al_shutdown_image_addon();

}

4. spaceship.h

#ifndef SPACESHIP_H

#define SPACESHIP_H

#include <allegro5/allegro.h>

#include <allegro5/allegro_image.h>

#include "bullet.h"

#include <vector>

class Spaceship {

public:

    Spaceship(float x, float y);

    ~Spaceship();

    // Non-copyable (owning raw pointer)

    Spaceship(const Spaceship&) = delete;

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

    // Movable

    Spaceship(Spaceship&& other) noexcept;

    Spaceship& operator=(Spaceship&& other) noexcept;

    bool load(const char* image_path);

    void update();

    void render();

    void shoot(std::vector<Bullet>& bullets, const char* bullet_image_path);

    float get_x() const { return x; }

    float get_y() const { return y; }

    ALLEGRO_BITMAP* get_bitmap() const { return image; }

private:

    float x, y;

    ALLEGRO_BITMAP* image;

};

#endif

5. spaceship.cpp

#include "spaceship.h"

#include <allegro5/allegro.h>

Spaceship::Spaceship(float x, float y) : x(x), y(y), image(nullptr) {}

Spaceship::~Spaceship() { if (image) { al_destroy_bitmap(image); image = nullptr; } }

// Move ctor

Spaceship::Spaceship(Spaceship&& other) noexcept

    : x(other.x), y(other.y), image(other.image)

{

    other.image = nullptr;

}

// Move assign

Spaceship& Spaceship::operator=(Spaceship&& other) noexcept {

    if (this != &other) {

        if (image) al_destroy_bitmap(image);

        x = other.x; y = other.y; image = other.image;

        other.image = nullptr;

    }

    return *this;

}

bool Spaceship::load(const char* image_path) {

    image = al_load_bitmap(image_path);

    return image != nullptr;

}

void Spaceship::update() {

    ALLEGRO_KEYBOARD_STATE state;

    al_get_keyboard_state(&state);

    if (al_key_down(&state, ALLEGRO_KEY_LEFT)) x—= 5;

    if (al_key_down(&state, ALLEGRO_KEY_RIGHT)) x += 5;

    if (al_key_down(&state, ALLEGRO_KEY_UP)) y—= 5;

    if (al_key_down(&state, ALLEGRO_KEY_DOWN)) y += 5;

    if (x < 0) x = 0;

    if (y < 0) y = 0;

    if (image) {

        int w = al_get_bitmap_width(image);

        int h = al_get_bitmap_height(image);

        if (x > 800—w) x = 800—w;

        if (y > 600—h) y = 600—h;

    }

}

void Spaceship::render() {

    if (image) al_draw_bitmap(image, x, y, 0);

}

void Spaceship::shoot(std::vector<Bullet>& bullets, const char* bullet_image_path) {

    if (!image) return;

    float bx = x + al_get_bitmap_width(image) / 2.0f—2.0f;

    float by = y—10.0f;

    if (bullet_image_path && *bullet_image_path) {

        bullets.emplace_back(bx, by, 0.0f,—8.0f, bullet_image_path);

    } else {

        bullets.emplace_back(bx, by, 0.0f,—8.0f);

    }

}

6. enemy.h

#ifndef ENEMY_H

#define ENEMY_H

#include <allegro5/allegro.h>

#include <allegro5/allegro_image.h>

class Enemy {

public:

    Enemy(float x, float y, const char* image_path);

    void update();

    void render();

    void set_active(bool active) { this->active = active; }

    bool is_active() const { return active; }

    float get_x() const { return x; }

    float get_y() const { return y; }

    ALLEGRO_BITMAP* get_bitmap() const { return image; }

private:

    float x, y;

    bool active;

    ALLEGRO_BITMAP* image;

};

#endif

7. enemy.cpp

#include "enemy.h"

#include <cstdlib>

Enemy::Enemy(float x, float y, const char* image_path)

    : x(x), y(y), active(true), image(nullptr)

{

    image = al_load_bitmap(image_path);

    if (!image) {

        active = false;

    }

}

Enemy::~Enemy() {

    if (image) { al_destroy_bitmap(image); image = nullptr; }

}

// Move ctor

Enemy::Enemy(Enemy&& other) noexcept

    : x(other.x), y(other.y), active(other.active), image(other.image)

{

    other.image = nullptr;

    other.active = false;

}

// Move assign

Enemy& Enemy::operator=(Enemy&& other) noexcept {

    if (this != &other) {

        if (image) al_destroy_bitmap(image);

        x = other.x; y = other.y; active = other.active; image = other.image;

        other.image = nullptr;

        other.active = false;

    }

    return *this;

}

void Enemy::update() {

    if (!active || !image) return;

    // simple downward drift

    y += 2.0f;

    if (y > 600) {

        int h = al_get_bitmap_height(image);

        y =—h;

        int w = al_get_bitmap_width(image);

        x = static_cast<float>(rand() % (800—w));

    }

}

void Enemy::render() {

    if (active && image) {

        al_draw_bitmap(image, x, y, 0);

    }

}

8. bullet.h

#ifndef BULLET_H

#define BULLET_H

#include <allegro5/allegro.h>

#include <allegro5/allegro_image.h>

class Bullet {

public:

    Bullet(float x, float y, float dx, float dy);

    Bullet(float x, float y, float dx, float dy, const char* ima

    ~Bullet();

    // Non-copyable (owning raw pointer)

    Bullet(const Bullet&) = delete;

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

    // Movable

    Bullet(Bullet&& other) noexcept;

    Bullet& operator=(Bullet&& other) noexcept;

    void update();

    void render();

    bool is_active() const { return active; }

    void set_active(bool a) { active = a; }

    float get_x() const { return x; }

    float get_y() const { return y; }

    ALLEGRO_BITMAP* get_bitmap() const { return image; }

private:

    void load_image_from(const char* image_path);

    float x, y;

    float dx, dy;

    bool active;

    ALLEGRO_BITMAP* image;

};

#endif

9. bullet.cpp

#include "bullet.h"

Bullet::Bullet(float x, float y, float dx, float dy)

    : x(x), y(y), dx(dx), dy(dy), active(true), image(nullptr)

{

    load_image_from("assets/bullet.png");

}

Bullet::Bullet(float x, float y, float dx, float dy, const char* image_path)

    : x(x), y(y), dx(dx), dy(dy), active(true), image(nullptr)

{

    load_image_from(image_path);

}

void Bullet::load_image_from(const char* image_path) {

    image = al_load_bitmap(image_path);

    if (!image) active = false;

}

Bullet::~Bullet() {

    if (image) { al_destroy_bitmap(image); image = nullptr; }

}

// Move ctor

Bullet::Bullet(Bullet&& other) noexcept

    : x(other.x), y(other.y), dx(other.dx), dy(other.dy),

      active(other.active), image(other.image)

{

    other.image = nullptr;

    other.active = false;

}

// Move assign

Bullet& Bullet::operator=(Bullet&& other) noexcept {

    if (this != &other) {

        if (image) al_destroy_bitmap(image);

        x = other.x; y = other.y; dx = other.dx; dy = other.dy;

        active = other.active; image = other.image;

        other.image = nullptr; other.active = false;

    }

    return *this;

}

void Bullet::update() {

    if (!active || !image) { active = false; return; }

    y += dy; x += dx;

    int w = al_get_bitmap_width(image);

    int h = al_get_bitmap_height(image);

    if (y <—h || y > 600 || x <—w || x > 800) active = false;

}

void Bullet::render() {

    if (active && image) al_draw_bitmap(image, x, y, 0);

}

Build the Project

After building the executable, you can launch the game directly from your development environment. The following steps explain how to run the program and verify that everything is working correctly:

  • • Open the Command Palette (⇧⌘P or Ctrl + Shift + P) and run CMake: Configure.
  • • Select the appropriate kit (e.g., GCC) and specify the generator as Ninja.
  • • Run CMake to build.

Run Your Game

Use the CMake: Run command or configure a launch configuration in the launch.json file. This is a basic framework to get you started.

Tasks for Completion and Improvement

With the core game implemented, there are many opportunities to expand its features and polish the experience. The following tasks suggest meaningful enhancements you can add to deepen gameplay and improve presentation.

  1. 1. Scoring and Health System: Display hit counts or health bars clearly on the screen and refine how damage is calculated.
  2. 2. Improved AI: Enhance enemy tank behavior with smarter movement, targeting, and obstacle avoidance.
  3. 3. Weapons and Shooting Mechanics: Introduce multiple weapon types or adjust firing rate, speed, and damage for variety.
  4. 4. Battlefield and Levels: Create different maps or obstacle layouts and increase difficulty through more complex environments.
  5. 5. Sound Effects: Add sound effects for firing, impacts, and victory events to improve immersion.

Exercises, Homework Questions, and Projects

Exercises

  1. 1. Action Game Characteristics

    Identify the defining characteristics of action games demonstrated by the space shooter. Why are responsiveness and fast feedback more critical here than narrative depth or complex AI?

  2. 2. Entity Lifecycle Management

    Explain the lifecycle of a bullet from creation to removal. Why is it important to deactivate or erase bullets once they leave the screen or collide with an enemy?

  3. 3. Collision Detection Basics

    Describe how axis-aligned bounding box (AABB) collision checks work. Why are they well suited to a 2D space shooter with rectangular sprites?

  4. 4. Game State Awareness

    Explain how the game distinguishes between different states such as “playing” and “game over.” Why is explicit game-state tracking important even in simple action games?

Homework Questions

  1. 1. Input Handling Strategies

    Compare polling the keyboard state with event-based input handling. What are the advantages of using continuous keyboard state checks for movement in an action game?

  2. 2. Difficulty and Player Skill

    Discuss how enemy spawn frequency, speed, and quantity influence difficulty. How can designers balance challenge so the game remains fun rather than frustrating?

  3. 3. Performance Considerations

    Why should inactive enemies and bullets be removed from their containers regularly? What problems might arise if these objects accumulate over time?

  4. 4. Visual Feedback and Player Perception

    Explain how immediate visual feedback (such as explosions or flashing sprites) improves player perception of responsiveness and fairness during gameplay.

Programming Projects

  1. 1. Scoring System (Core Project)

    Add a visible scoring system that increases the player’s score whenever an enemy is destroyed. Display the score prominently on the screen during gameplay.

  2. 2. Lives and Health System

    Extend the game so the player has multiple lives or a health bar. Deduct lives or health when colliding with enemies and trigger a game-over condition when it reaches zero.

  3. 3. Power-Ups and Collectibles

    Implement power-ups that occasionally drop from destroyed enemies, such as rapid fire, temporary shields, or spread shots. Ensure power-ups activate and expire correctly.

  4. 4. Multiple Enemy Types

    Introduce additional enemy types with different movement speeds, behaviors, or hit points. Use simple variation rules to increase gameplay depth.

  5. 5. Level Progression (Advanced)

    Create multiple levels or stages. Each level should feature increased difficulty through faster enemies, denser waves, or new enemy behaviors.

  6. 6. Sound Effects and Audio Feedback (Advanced)

    Add sound effects for shooting, enemy destruction, and player damage using Allegro’s audio add-ons. Balance volume levels to avoid overwhelming the player.

Capstone Project

Design and implement a fully polished space shooter game featuring multiple levels, score tracking, lives, power-ups, sound effects, and clear game-over and restart mechanics. Include a short design reflection describing how input handling, collision logic, and difficulty progression work together to create a compelling action-game experience.

8.2. Platform Game: Tank War

Tank war games are a popular genre that focuses on tank warfare, offering players the chance to control powerful armored vehicles in various combat scenarios.

Designing and implementing a tank war game with Allegro in C++ can be a fun and rewarding project. Here’s a step-by-step guide to help you get started.

8.2.1. Game Design

Game Concept

  • Genre: A simple 2D tank war game where players control tanks, navigate a battlefield, and try to destroy each other.
  • Objective: Players control tanks with smooth movement, rotation, and shooting mechanics. The game should implement real-time combat using projectiles, collision detection, and health tracking. Design a bounded battlefield with obstacles that influence movement, cover, and tactics and provide clear win and loss conditions through a hit-based scoring system. Reinforce core game programming concepts, including the game loop, event-driven input, timing, collision handling, and basic AI behaviors.

Core Components

  • Player Tank: The primary tank controlled by the player
    • Attributes: Position (x, y), rotation angle, movement speed, sprite, health (or hit count)
    • Movement: Controlled via keyboard input; the tank rotates left or right and moves forward or backward based on its facing direction
    • Shooting: Fires projectiles in the direction the tank is facing, subject to firing rate and projectile speed limits
  • Enemy Tank: An opposing tank controlled by scripted logic or simple AI behavior
    • Attributes: Position (x, y), rotation angle, movement speed, sprite, health (or hit count)
    • Movement: Moves based on AI rules such as patrolling, chasing the player, or repositioning for tactical advantage
    • Shooting: Fires projectiles toward the player or along its facing direction, using the same projectile system as the player tank
  • Bullets (Projectiles): Projectiles fired by both player and enemy tanks
    • Attributes: Position (x, y), velocity (dx, dy), speed
    • Movement: Travel in straight lines in the firing direction until they hit a tank, collide with an obstacle, or leave the playfield
    • Collision: Deactivate on impact with tanks or environmental obstacles
  • Battlefield and Obstacles: The bounded play area where tank combat takes place
    • Attributes: Screen bounds, obstacle positions, obstacle sizes
    • Function: Restricts tank movement to the playfield and provides cover or tactical chokepoints that influence combat and positioning
  • Collision System: Manages interactions between tanks, bullets, and obstacles
    • Detection Method: Axis-aligned bounding box (AABB) collision checks
    • Function: Prevents tanks from passing through obstacles, registers bullet hits, and enforces valid movement
  • Game State and Scoring: Tracks the overall progress and outcome of a round
    • Attributes: Current round state (playing, round won, paused), hit counters or health values, win condition thresholds
    • Win Condition: A round ends when a tank reaches a defined hit limit or health depletion, triggering a win or loss state

Key Techniques and Features to Have

The following features describe the core mechanics and engineering practices required to implement the tank war game. Each item corresponds directly to systems implemented in the Game, Tank, and Bullet classes.

  1. 1. Game Loop and Timing (60 fps)

    The game should use an event-driven loop driven by an ALLEGRO_TIMER at 60 fps. All gameplay logic should execute on timer events to ensure deterministic updates and stable rendering.

  2. 2. Input Handling and Control Mapping

    Keyboard input should be mapped cleanly to tank movement, rotation, and firing, with separate control schemes for each tank. Input events should set intent flags that are applied during the update step, and global inputs (quit, close) should halt the loop cleanly.

  3. 3. Rotation, Movement, and Orientation

    Tank orientation should be represented using radians and normalized to prevent drift. Movement should project forward and backward motion from the tank’s facing angle, accounting for sprite-specific forward offsets.

  4. 4. World Constraints and Clamping

    Tanks must be constrained to the playfield bounds. Invalid movement through obstacles should be prevented by reverting overlapping moves detected during updates.

  5. 5. Obstacles: Placement, Blocking, and Rendering

    Obstacles should be generated procedurally without overlapping tank spawn areas or each other. They must be stored centrally for collision checks and rendered clearly using filled shapes with outlines.

  6. 6. Projectiles (Bullets)

    Bullets should spawn just outside the firing tank’s bounding box, travel at constant speed, and be removed when off-screen or when colliding with obstacles. Visual representations may be simplified without affecting collision logic.

  7. 7. Collision Detection and Responses

    Collision checks should use simple point-in-rectangle or AABB tests. Bullet–tank and bullet–obstacle collisions should immediately remove the bullet, while tank–obstacle collisions prevent movement. More advanced collision models can be deferred.

  8. 8. Health, Scoring, and Round Flow

    Each tank should track hits taken, with scoring debounced per update to prevent multiple counts. A round ends when a tank reaches the hit limit, displays a winner message, pauses briefly, and then resets the battlefield.

  9. 9. UI and Player Feedback

    The UI should display hit counters and a winner banner using readable fonts and colour coding. Feedback should be immediate and easy to interpret during active play and round transitions.

  10. 10. Resources and Asset Pipeline

    Tank textures and other assets should be loaded once and destroyed properly to avoid leaks. Bitmap settings should favor smooth scaling, and build tools should automatically place assets alongside the executable.

  11. 11. Randomness and Reproducibility

    Random elements, such as obstacle placement, should be seeded for variation, with optional fixed seeds available for debugging or reproducible testing.

  12. 12. Code Organization and Extensibility

    Responsibilities should be clearly divided between Game, Tank, and Bullet classes. The design should include hooks for future extensions such as AI steering, external map loading, new weapons, and audio feedback.

  13. 13. Performance and Stability

    Per-frame operations should scale linearly with active bullets and obstacles. Allocations in hot paths should be avoided, and all Allegro resources should be validated and released cleanly on shutdown.

8.2.2. Implementation

Before implementing this and other games in this chapter, ensure you have Visual Studio Code, GCC, CMake, Ninja, and Allegro 5 installed and configured as described in chapter 1 and mentioned in 8.1.2.

Key Operations in the Game

A tank war game relies on a continuous sequence of operations that update the battlefield, process player actions, and maintain the flow of combat. These operations occur every frame and ensure that tanks move realistically, bullets behave predictably, and the game responds immediately to player input. The following list summarizes the essential runtime operations that drive the tank war gameplay experience:

  1. 1. Process Player Input
    • • Read Up/Down key events to determine tank movement, rotation, and firing.
    • • Update intent flags (e.g., forward, backward) and apply them during the update step.
  2. 2. Update Tank Movement and Orientation
    • • Move tanks forward or backward based on their current facing angle.
    • • Apply rotation increments to adjust tank direction.
    • • Normalize angles to avoid drift and maintain consistent orientation.
  3. 3. Apply World Constraints and Obstacle Collisions
    • • Clamp tank positions to remain within the 800 × 600 playfield.
    • • Prevent tanks from passing through obstacles by reverting invalid moves.
  4. 4. Update Bullets
    • • Advance bullets along their velocity vectors.
    • • Remove bullets that leave the screen or collide with obstacles.
  5. 5. Detect Collisions Between Bullets and Tanks
    • • Check whether any bullet intersects a tank’s axis-aligned bounding box.
    • • Mark hits and remove bullets that successfully strike a tank.
  6. 6. Update Scoring and Round Flow
    • • Increment hit counters for each tank when struck.
    • • Trigger win conditions when a tank accumulates five hits.
    • • Display a winner banner and pause the game before resetting the round.
  7. 7. Render the Frame
    • • Draw obstacles, tanks, bullets, and UI elements.
    • • Display hit counters and winner messages when appropriate.
    • • Flip the display to present the updated frame.

Structure of the Game Program

The tank war game is organized into several interconnected modules that work together to implement gameplay, rendering, and event handling. Each part of the program has a clear responsibility, making the code easier to maintain and extend. The following outline summarizes the major structural components of the game.

  1. 1. Initialization
    • • Initialize Allegro subsystems (keyboard, image, primitives, font, TTF).
    • • Create the display, event queue, and timer.
    • • Load tank sprites and prepare the battlefield.
  2. 2. Resource Loading
    • • Load tank textures, bullet graphics (if any), and fonts.
    • • Set bitmap flags for smooth scaling and rendering.
    • • Generate obstacles procedurally to populate the map.
  3. 3. Entity Creation
    • • Instantiate Tank objects for both players.
    • • Initialize bullet containers and obstacle lists.
    • • Set initial positions and angles for both tanks.
  4. 4. Input Handling
    • • Capture keyboard events to set movement and rotation flags.
    • • Trigger bullet firing when Ctrl keys are pressed.
    • • Handle Esc key press and display-close events to exit the game.
  5. 5. Update Functions
    • • Move tanks and apply collision checks.
    • • Update bullet positions and remove invalid bullets.
    • • Detect hits and update scoring logic.
    • • Manage round transitions and pause states.
  6. 6. Rendering Functions
    • • Draw obstacles, tanks, bullets, and UI text.
    • • Display winner banners and hit counters.
    • • Clear and flip the display each frame.
  7. 7. Game Loop
    • • Wait for events from the event queue.
    • • On timer events, call update() and render().
    • • Maintain a consistent 60 fps update rate.
  8. 8. Shutdown
    • • Destroy fonts, timers, event queues, and the display.
    • • Release any loaded resources.
    • • Cleanly exit the program.

Coding the Game

As shown in the program structure, code files for the game project can be organized in a tree, and the main.cpp is the root of the tree. The following is the outline of the main.cpp file for this game.

main.cpp

#include "game.h"

int main() {

    Game game;

    if (game.init()

        game.run();

    }

    game.shutdown()

    return 0;

}

game.h

#ifndef GAME_H

#define GAME_H

#include <allegro5/allegro.h>

#include <allegro5/allegro_image.h>

#include <allegro5/allegro_primitives.h>

#include <vector>

#include <string>

#include <allegro5/allegro_font.h>

#include "tank.h"

#include "bullet.h"

#include "common.h"

struct Obstacle { float x, y, w, h; };

class Game {

public:

    Game();

    bool init();

    void run();

    void shutdown();

private:

    void generateObstacles();

    bool tankOverlapsAnyObstacle(const Tank& t) const;

    bool bulletHitsAnyObstacle(float bx, float by) const;

    void renderObstacles() const;

    bool rectsOverlap(float ax,float ay,float aw,float ah, float bx,float by,float bw,flo

private:

    void update();

    void render();

    void fireBulletFrom(const Tank& t);

    bool check_collision(const Tank& tank, const Bullet& bullet);

    void clampTankToScreen(Tank& t);

    void resetRound();

    ALLEGRO_DISPLAY* display;

    ALLEGRO_EVENT_QUEUE* event_queue;

    ALLEGRO_TIMER* timer;

    bool running;

    Tank tank1; // right side (WASD + Right Ctrl), PNG faces LEFT (offset PI)

    Tank tank2; // left  side (Arrows + Left Ctrl), PNG faces RIGHT (offset 0)

    bool t1_forward=false, t1_backward=false;

    bool t2_forward=false, t2_backward=false;

    float moveSpeed = 3.0f;

    const float rotStep = PI / 4.0f;

    const float bulletSpeed = 8.0f;

    int score1 = 0; // hits on tank1

    int score2 = 0; // hits on tank2

    std::vector<Bullet> bullets;

    std::vector<Obstacle> obstacles;

    //——Round win & pause UI——

    static constexpr int WIN_SCORE = 5; // first tank to TAKE 5 hits loses

    ALLEGRO_FONT* uiFont = nullptr;

    std::string   winText;

    int           winTextFrames = 0;   // frames to show banner (300 == 5s @60FPS)

    bool          roundPaused   = false;

};

#endif // GAME_H

Game.cpp

#include<cmath>

#include "game.h"

#include "tank.h"

#include "bullet.h"

#include <algorithm>

#include <cstdlib>

#include <ctime>

#include <cstdio>

#include <allegro5/allegro_font.h>

#include <allegro5/allegro_ttf.h>

static constexpr int SCREEN_W = 800;

static constexpr int SCREEN_H = 600;

Game::Game()

    : display(nullptr), event_queue(nullptr), timer(nullptr), running(true) {}

bool Game::init() {

    if (!al_init()) return false;

    if (!al_install_keyboard()) return false;

    if (!al_init_image_addon()) return false;

    if (!al_init_primitives_addon()) return false;

    if (!al_init_font_addon()) return false;

    al_init_ttf_addon();

    if (!uiFont) uiFont = al_create_builtin_font();

    al_set_new_bitmap_flags(ALLEGRO_VIDEO_BITMAP | ALLEGRO_MIN_LINEAR | ALLEGRO_MAG_LINEAR);

    display = al_create_display(SCREEN_W, SCREEN_H);

    if (!display) return false;

    al_set_blender(ALLEGRO_ADD, ALLEGRO_ALPHA, ALLEGRO_INVERSE_ALPHA);

    event_queue = al_create_event_queue();

    timer = al_create_timer(1.0 / 60.0);

    if (!event_queue || !timer) return false;

    al_register_event_source(event_queue, al_get_keyboard_event_source());

    al_register_event_source(event_queue, al_get_display_event_source(display));

    al_register_event_source(event_queue, al_get_timer_event_source(timer));

    // Tank2: left side, PNG faces RIGHT at 0°—> offset 0.0

    tank2 = Tank(600, 400, "assets/tank2.png", 0.0f);

    // Tank1: right side, PNG faces LEFT at 0°—> offset PI

    tank1 = Tank(100, 100, "assets/tank1.png", PI);

    generateObstacles();

al_start_timer(timer);

    return true;

}

void Game::shutdown() {

    if (uiFont) { al_destroy_font(uiFont); uiFont = nullptr; }

    if (timer) { al_destroy_timer(timer); timer = nullptr; }

    if (event_queue) { al_destroy_event_queue(event_queue); event_queue = nullptr; }

    if (display) { al_destroy_display(display); display = nullptr; }

}

void Game::resetRound() {

    bullets.clear();

    tank2.setPosition(100, 100);

    tank2.setAngle(0);

    tank1.setPosition(600, 400);

    tank1.setAngle(0);

    generateObstacles();

}

void Game::run() {

    while (running) {

        ALLEGRO_EVENT ev;

        al_wait_for_event(event_queue, &ev);

        if (ev.type == ALLEGRO_EVENT_TIMER) {

            update();

            render();

        } else if (ev.type == ALLEGRO_EVENT_DISPLAY_CLOSE) {

            running = false;

        } else if (ev.type == ALLEGRO_EVENT_KEY_DOWN) {

            switch (ev.keyboard.keycode) {

                // Tank1 (right side)—WASD + Right Ctrl

                case ALLEGRO_KEY_W:     t1_forward  = true; break;

                case ALLEGRO_KEY_S:     t1_backward = true; break;

                case ALLEGRO_KEY_A:     tank1.rotateBy(-rotStep); break;

                case ALLEGRO_KEY_D:     tank1.rotateBy(+rotStep); break;

                case ALLEGRO_KEY_LCTRL: fireBulletFrom(tank1); break;

                // Tank2 (left side)—Arrows + Left Ctrl

                case ALLEGRO_KEY_UP:    t2_forward  = true; break;

                case ALLEGRO_KEY_DOWN:  t2_backward = true; break;

                case ALLEGRO_KEY_LEFT:  tank2.rotateBy(-rotStep); break;

                case ALLEGRO_KEY_RIGHT: tank2.rotateBy(+rotStep); break;

                case ALLEGRO_KEY_RCTRL: fireBulletFrom(tank2); break;

                case ALLEGRO_KEY_ESCAPE: running = false; break;

            }

        } else if (ev.type == ALLEGRO_EVENT_KEY_UP) {

            switch (ev.keyboard.keycode) {

                // Tank1

                case ALLEGRO_KEY_W: t1_forward  = false; break;

                case ALLEGRO_KEY_S: t1_backward = false; break;

                // Tank2

                case ALLEGRO_KEY_UP:   t2_forward  = false; break;

                case ALLEGRO_KEY_DOWN: t2_backward = false; break;

            }

        }

    }

}

void Game::update() {

    if (!roundPaused) {

        // Movement (blocked by obstacles)

        if (t1_forward)   { tank1.moveForward(+moveSpeed); if (tankOverlapsAnyObstacle(tank1)) { tank1.

        if (t1_backward)  { tank1.moveForward(-moveSpeed); if (tankOverlapsAnyObstacle(tank1)) { tank1.

        if (t2_forward)   { tank2.moveForward(+moveSpeed); if (tankOverlapsAnyObstacle(tank2)) { tank2.

        if (t2_backward)  { tank2.moveForward(-moveSpeed); if (tankOverlapsAnyObstacle(tank2)) { tank2.

        clampTankToScreen(tank1);

        clampTankToScreen(tank2);

        // Bullets

        for (auto& b : bullets) b.update();

        bullets.erase(std::remove_if(bullets.begin(), bullets.end(),

            [this](const Bullet& b){

                if (b.getX() <—10 || b.getX() > SCREEN_W + 10 || b.getY() <—10 || b.getY() > SCREEN_H

                    return true;

                if (bulletHitsAnyObstacle(b.getX(), b.getY()))

                    return true;

                return false;

            }),

            bullets.end());

        // Tank hit detection—accumulate hits on each tank

        /*

        bool hit1 = false, hit2 = false;

        for (auto& b : bullets) {

            if (check_collision(tank1, b)) hit1 = true;

            if (check_collision(tank2, b)) hit2 = true;

        }

        if (hit1) score1++;   // score1 == hits on tank1

        if (hit2) score2++;   // score2 == hits on tank2

*/

bool hit1 = false, hit2 = false;

std::vector<size_t> toErase;

toErase.reserve(bullets.size());

for (size_t i = 0; i < bullets.size(); ++i) {

    const Bullet& b = bullets[i];

    bool consumed = false;

    if (check_collision(tank1, b)) {  // bullet overlaps tank1

        hit1 = true;

        consumed = true;

    }

    if (check_collision(tank2, b)) {  // bullet overlaps tank2

        hit2 = true;

        consumed = true;

    }

    if (consumed) {

        toErase.push_back(i);

    }

}

// remove bullets that hit any tank (back-to-front to keep indices valid)

for (size_t k = 0; k < toErase.size(); ++k) {

    bullets.erase(bullets.begin() + (toErase[k]—k));

}

// Debounced scoring: add at most one hit per tank this update

if (hit1) ++score1;  // score1 = hits on tank1

if (hit2) ++score2;  // score2 = hits on tank2

        // Round end: first tank to take 5 hits loses

        if (score1 >= WIN_SCORE || score2 >= WIN_SCORE) {

            if (score1 >= WIN_SCORE) {

                winText = "Tank 2 wins the round!";

            } else {

                winText = "Tank 1 wins the round!";

            }

            winTextFrames = 180; // 5 seconds at 60 FPS

            roundPaused = true;

        }

    } else {

        // Pause countdown

        if (winTextFrames > 0) {

           —winTextFrames;

        } else {

            // Reset battlefield and hit counters for next round

            resetRound();

            score1 = 0;

            score2 = 0;

            roundPaused = false;

        }

    }

}

void Game::render() {

    al_clear_to_color(al_map_rgb(20, 30, 40));

    renderObstacles();

    tank1.render();

    tank2.render();

    for (auto& b : bullets) b.render();

    // Draw winner banner if active

    if (winTextFrames > 0 && uiFont) {

        ALLEGRO_COLOR red = al_map_rgb(255, 0, 0);

        float tw = (float)al_get_text_width(uiFont, winText.c_str());

        float x  = (SCREEN_W—tw) * 0.5f;

        float y  = 8.0f;

        al_draw_text(uiFont, red, x, y, 0, winText.c_str());

    }

    ALLEGRO_FONT* f = al_create_builtin_font();

    if (f) {

        al_draw_textf(f, al_map_rgb(255,255,255), 10, 10, 0, "Hits on Tank1: %d", score1);

        al_draw_textf(f, al_map_rgb(255,255,255), 10, 30, 0, "Hits on Tank2: %d", score2);

        al_destroy_font(f);

    }

    al_flip_display();

}

void Game::fireBulletFrom(const Tank& t) {

    if (roundPaused) return;

    const float cx = t.getX() + t.getWidth()  * 0.5f;

    const float cy = t.getY() + t.getHeight() * 0.5f;

/*

    const float theta = t.getAngle() + SPRITE_FORWARD_OFFSET + t.getForwardOffset();

    const float muzzleOffset = std::max(t.getWidth(), t.getHeight()) * 0.5f + 4.0f;

*/

const float theta = t.getAngle() + SPRITE_FORWARD_OFFSET + t.getForwardOffset();

// Use half-diagonal so the spawn is guaranteed outside the AABB for any angle

const float halfDiag = 0.5f * std::sqrt(

    static_cast<float>(t.getWidth()) * t.getWidth() +

    static_cast<float>(t.getHeight()) * t.getHeight()

);

const float muzzleOffset = halfDiag + 6.0f; // small safety margin

    const float sx = cx + std::cos(theta) * muzzleOffset;

    const float sy = cy + std::sin(theta) * muzzleOffset;

    const float dx = std::cos(theta) * bulletSpeed;

    const float dy = std::sin(theta) * bulletSpeed;

    bullets.emplace_back(sx, sy, dx, dy);

}

bool Game::check_collision(const Tank& tank, const Bullet& bullet) {

    const float bx = bullet.getX();

    const float by = bullet.getY();

    const float tx = tank.getX();

    const float ty = tank.getY();

    const float tw = tank.getWidth();

    const float th = tank.getHeight();

    return (bx >= tx && bx <= tx + tw && by >= ty && by <= ty + th);

}

void Game::clampTankToScreen(Tank& t) {

    float nx = t.getX();

    float ny = t.getY();

    if (nx < 0) nx = 0;

    if (ny < 0) ny = 0;

    if (nx > SCREEN_W—t.getWidth())  nx = SCREEN_W—t.getWidth();

    if (ny > SCREEN_H—t.getHeight()) ny = SCREEN_H—t.getHeight();

    t.setPosition(nx, ny);

}

bool Game::rectsOverlap(float ax,float ay,float aw,float ah, float bx,float by,float bw,float bh) const

    return (ax < bx + bw) && (ax + aw > bx) && (ay < by + bh) && (ay + ah > by);

}

bool Game::tankOverlapsAnyObstacle(const Tank& t) const {

    for (const auto& ob : obstacles) {

        if (rectsOverlap(t.getX(), t.getY(), t.getWidth(), t.getHeight(), ob.x, ob.y, ob.w, ob.h)) {

            return true;

        }

    }

    return false;

}

bool Game::bulletHitsAnyObstacle(float bx, float by) const {

    for (const auto& ob : obstacles) {

        if (bx >= ob.x && bx <= ob.x + ob.w && by >= ob.y && by <= ob.y + ob.h) {

            return true;

        }

    }

    return false;

}

void Game::renderObstacles() const {

    for (const auto& ob : obstacles) {

        al_draw_filled_rectangle(ob.x, ob.y, ob.x + ob.w, ob.y + ob.h, al_map_rgb(110,110,110));

        al_draw_rectangle(ob.x, ob.y, ob.x + ob.w, ob.y + ob.h, al_map_rgb(40,40,40), 1.0f);

    }

}

void Game::generateObstacles() {

    obstacles.clear();

    std::srand((unsigned)std::time(nullptr));

    const int count = 10;

    const float w = tank1.getWidth()  > 0 ? tank1.getWidth()  : 64.0f;

    const float h = tank1.getHeight() > 0 ? tank1.getHeight() : 64.0f;

    auto overlapsTankStarts = [&](float x, float y) {

        const float pad = 12.0f;

        if (rectsOverlap(x, y, w, h, tank1.getX()-pad, tank1.getY()-pad, tank1.getWidth()+2*pad, tank1.

        if (rectsOverlap(x, y, w, h, tank2.getX()-pad, tank2.getY()-pad, tank2.getWidth()+2*pad, tank2.

        return false;

    };

    for (int i=0; i<count; ++i) {

        int guard = 0;

        while (true) {

            ++guard;

            float x = (float)(std::rand() % (SCREEN_W—(int)w—20) + 10);

            float y = (float)(std::rand() % (SCREEN_H—(int)h—20) + 10);

            if (overlapsTankStarts(x, y)) { if (guard < 2000) continue; }

            bool ok = true;

            for (const auto& ob : obstacles) {

                if (rectsOverlap(x, y, w, h, ob.x, ob.y, ob.w, ob.h)) { ok = false; break; }

            }

            if (!ok) { if (guard < 2000) continue; }

            obstacles.push_back(Obstacle{x,y,w,h});

            break;

        }

    }

}

tank.h

#ifndef TANK_H

#define TANK_H

#include <allegro5/allegro.h>

#include <allegro5/allegro_image.h>

class Tank {

public:

    Tank();

    Tank(float x, float y, const char* image_path, float forwardOffsetRadians = 0.0f);

    ~Tank();

    Tank(const Tank&) = delete;

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

    Tank(Tank&& other) noexcept;

    Tank& operator=(Tank&& other) noexcept;

    void setPosition(float nx, float ny) { x = nx; y = ny; }

    void moveBy(float dx, float dy) { x += dx; y += dy; }

    void rotateBy(float radians);

    void setAngle(float radians);

    void update() {} // placeholder

    void render();

    void moveForward(float amount);

    float getX() const { return x; }

    float getY() const { return y; }

    int   getWidth()  const { return width; }

    int   getHeight() const { return height; }

    float getAngle()  const { return angle; }

    float getForwardOffset() const { return forwardOffset; }

private:

    void normalizeAngle();

    float x = 0.0f, y = 0.0f;

    ALLEGRO_BITMAP* image = nullptr;

    int width = 0, height = 0;

    float angle = 0.0f;

    float forwardOffset = 0.0f;

};

#endif // TANK_H

tank.cpp

#include "tank.h"

#include "common.h"

#include <cstdio>

#include <allegro5/allegro_primitives.h>

Tank::Tank() : x(0.0f), y(0.0f), image(nullptr), width(0), height(0), angle(0.0f), forwardOffset(0.0f) {}

Tank::Tank(float x, float y, const char* image_path, float forwardOffsetRadians)

    : x(x), y(y), image(nullptr), width(0), height(0), angle(0.0f), forwardOffset(forwardOffsetRadians) {

    image = al_load_bitmap(image_path);

    if (!image) {

        std::fprintf(stderr, "[Tank] Failed to load: %s\n", image_path);

    } else {

        width = al_get_bitmap_width(image);

        height = al_get_bitmap_height(image);

    }

}

Tank::~Tank() {

    if (image) {

        al_destroy_bitmap(image);

        image = nullptr;

    }

}

Tank::Tank(Tank&& other) noexcept {

    x = other.x; y = other.y;

    image = other.image; other.image = nullptr;

    width = other.width; height = other.height;

    angle = other.angle;

    forwardOffset = other.forwardOffset;

}

Tank& Tank::operator=(Tank&& other) noexcept {

    if (this != &other) {

        if (image) al_destroy_bitmap(image);

        x = other.x; y = other.y;

        image = other.image; other.image = nullptr;

        width = other.width; height = other.height;

        angle = other.angle;

        forwardOffset = other.forwardOffset;

    }

    return *this;

}

void Tank::rotateBy(float radians) {

    angle += radians;

    normalizeAngle();

}

void Tank::setAngle(float radians) {

    angle = radians;

    normalizeAngle();

}

void Tank::normalizeAngle() {

    if (angle >= PI) {

        angle = std::fmod(angle + PI, 2.0f * PI)—PI;

    } else if (angle <—PI) {

        angle = std::fmod(angle—PI, 2.0f * PI) + PI;

    }

}

void Tank::render() {

    if (!image) {

        al_draw_filled_rectangle(x, y, x + 40, y + 40, al_map_rgb(255, 0, 0));

        return;

    }

    const float cx = width * 0.5f;

    const float cy = height * 0.5f;

    al_draw_rotated_bitmap(image, cx, cy, x + cx, y + cy, angle, 0);

}

void Tank::moveForward(float amount) {

    const float theta = angle + SPRITE_FORWARD_OFFSET + forwardOffset;

    x += std::cos(theta) * amount;

    y += std::sin(theta) * amount;

}

bullet.h

#ifndef BULLET_H

#define BULLET_H

#include <allegro5/allegro.h>

#include <allegro5/allegro_primitives.h>

class Bullet {

public:

    Bullet(float x, float y, float dx, float dy);

    void update();

    void render();

    float getX() const { return x; }

    float getY() const { return y; }

private:

    float x, y, dx, dy;

};

#endif // BULLET_H

bullet.cpp

#include "bullet.h"

Bullet::Bullet(float x, float y, float dx, float dy)

    : x(x), y(y), dx(dx), dy(dy) {}

void Bullet::update() {

    x += dx;

    y += dy;

}

void Bullet::render() {

    al_draw_filled_circle(x, y, 2, al_map_rgb(255, 255, 255));

}

common.h

#ifndef COMMON_H

#define COMMON_H

#include <cmath>

static constexpr float PI = 3.14159265358979323846f;

static constexpr float SPRITE_FORWARD_OFFSET = 0.0f;

#endif // COMMON_H

Tasks for Completion and Improvement

  1. 1. Features: Add more features.
  2. 2. AI: Improve the AI.
  3. 3. Graphics: Enhance the graphics.

Exercises, Homework Questions, and Projects

Exercises

  1. 1. Tank Movement and Orientation

    Explain how representing tank orientation using radians enables smooth rotation and directional movement. Why is it important to normalize angles over time?

  2. 2. Event-Driven Game Loop

    Describe how the Allegro timer drives the game loop at 60 frames per second. Why should game logic updates be executed only in response to timer events?

  3. 3. Collision Handling Strategy

    Review the collision logic used for tanks, bullets, and obstacles. Why are axis-aligned bounding boxes (AABB) an appropriate choice for this game?

  4. 4. Round-Based Game Flow

    Explain how hit counters, win conditions, pause states, and round resets work together to define the flow of a single round of gameplay.

Homework Questions

  1. 1. Control Scheme Design

    Discuss the advantages and challenges of having two players share a keyboard. How do control mappings affect player comfort and fairness?

  2. 2. Projectile Management

    Why is it important to spawn bullets outside the tank’s bounding box? Describe what could go wrong if bullets begin inside their source tank.

  3. 3. Obstacle-Driven Gameplay

    Explain how obstacles influence player tactics and movement decisions. How do obstacles increase strategic depth in an otherwise simple combat arena?

  4. 4. Simple AI Limitations

    The enemy tank AI uses basic decision logic. What types of player behaviors might this AI fail to respond to effectively, and why?

Programming Projects

  1. 1. Enhanced Enemy AI (Core Project)

    Improve enemy tank behavior by implementing simple steering logic, such as seeking the player, maintaining distance, or avoiding obstacles dynamically.

  2. 2. Multiple Weapon Types

    Extend the shooting system to support additional weapon types, such as spread shots, slow but powerful shells, or ricocheting bullets. Balance their speed and damage appropriately.

  3. 3. Map Loading from Data Files

    Replace procedural obstacle generation with map loading from an external file format (e.g., JSON or CSV). Allow different battlefield layouts for variety.

  4. 4. Health Bars and Visual Feedback

    Add visible health bars or damage indicators to tanks. Provide visual feedback when a tank is hit, such as a brief flash or screen shake.

  5. 5. Sound Effects Integration (Advanced)

    Add sound effects for firing bullets, tank explosions, and round victories using Allegro’s audio subsystem.

  6. 6. Single-Player Mode vs. AI (Advanced)

    Modify the game to allow a single-player mode where the second tank is fully AI-controlled. Adjust difficulty by tuning AI speed, firing rate, or accuracy.

Capstone Project

Design and implement a polished game featuring multiple maps, improved AI behaviors, enhanced graphics, and audio feedback. Include configurable match rules (such as time-based rounds or score limits) and provide a menu system for mode selection. Submit a short design overview explaining how movement, combat, AI, and level design combine to create engaging gameplay.

8.3. Adventure Game

Adventure games are a beloved genre that focuses on storytelling, exploration, and puzzle-solving.

8.3.1. Game Design

Game Concept

  • Genre: Adventure game.
  • Objective: The player explores a narrative-driven world, interacts with characters and environments, solves puzzles, and progresses through a central story revealed through exploration, object interaction, and dialogue.

Core Components

  • Player Character: The primary character controlled by the player
    • Attributes: Position (x, y), movement rules, inventory capacity, interaction range
    • Movement: Controlled via keyboard or mouse input; movement may be grid-based, screen-to-screen, or free-form depending on world design
    • Interaction: Can examine objects, collect and use items, talk to NPCs, activate environmental triggers, and initiate puzzle actions
  • Nonplayer Characters (NPCs): Characters inhabiting the world that the player can interact with
    • Attributes: Position (x, y), dialogue state, quest flags, interaction availability
    • Function: Provide information, story context, quests, items, or puzzle hints
    • Interaction: Engaged through dialogue menus or context-sensitive actions; dialogue options may change based on game state or player choices
  • World and Locations: The explorable environments that make up the game world
    • Attributes: Room or region ID, layout geometry, connected exits, contained objects, and NPCs
    • Structure: Organized into rooms, zones, or regions such as towns, caves, corridors, forests, or abstract puzzle spaces
    • Navigation: Movement between locations occurs through doors, passages, portals, or scripted transitions
  • Objects and Items: Interactive elements that support exploration and puzzle-solving
    • Attributes: Item ID, name, description, usage rules, combinability
    • Interaction: Items may be collected, examined, combined with other items, or used on world elements
    • Purpose: Enable puzzle resolution, gate progress, or advance narrative and quests
  • Interaction System: Defines how the player engages with the world
    • Input: Context-sensitive keyboard or mouse actions
    • Supported Actions: Talking to NPCs, using or inspecting items, opening containers, pressing switches, and reading notes or signs
    • Feedback: UI prompts, dialogue boxes, or visual/audio cues indicate successful or failed interactions
  • Puzzle Structures: Logical challenges embedded in the game world
    • Types: Item-based puzzles, sequence puzzles, environmental navigation puzzles, pattern or code puzzles, and dialogue-based puzzles
    • Function: Block or enable access to areas, items, or story content
    • Design Principle: Puzzles are integrated into narrative context and world logic to ensure fairness and clarity
  • Quest and Story Progression System: Tracks narrative and gameplay progress
    • Attributes: Active quest list, quest stages, story flags, optional objectives
    • Function: Controls availability of locations, puzzles, dialogue options, and endings
    • Progression: Advances through player actions such as puzzle completion, dialogue choices, or item usage
  • UI and Menu Systems: Interfaces that communicate information and support interaction
    • Components: Dialogue boxes, inventory menu, quest log, pause menu, settings screen
    • Function: Present narrative text, objectives, item lists, and player choices clearly and unobtrusively
  • Art and Audio Assets: Visual and audio elements that reinforce atmosphere and storytelling
    • Visual Assets: Room backgrounds, character sprites, object icons, portraits
    • Audio Assets: Sound effects for interactions, ambient sounds, optional background music
    • Purpose: Enhance immersion, mood, and narrative tone

Key Techniques and Features to Have

Adventure games emphasize storytelling, exploration, and puzzle-solving over reflex-driven action. A well-designed adventure game depends on clear narrative structure, intuitive interaction, and consistent world logic. The following features form a strong foundational baseline:

  1. 1. Narrative Structure and Branching Storylines

    The game should present a clear story arc with main objectives and optional side content. Branching dialogue or player choices should influence progression, with optional multiple endings to enhance replay value.

  2. 2. World Navigation and Exploration Systems

    Player movement should follow a consistent navigation model (grid-based rooms, screen transitions, or scrolling maps). Visual cues, landmarks, and environmental detail should guide exploration and help players orient themselves.

  3. 3. Interaction Mechanics (Objects, NPCs, Environment)

    The game should support interaction with objects, NPCs, and the environment through proximity-based or input-driven actions. Typical interactions include talking, examining, using items, and triggering scripted events.

  4. 4. Puzzle and Challenge Design

    Puzzles should be integrated naturally into the world and narrative, such as item-based, sequence, environmental, or logic puzzles. Progression should be logical and reward observation and reasoning rather than guesswork.

  5. 5. Game State Tracking and Persistence

    The game should track key progress variables, including location, quest states, collected items, and completed events. Save and load functionality should preserve this state for long-term play.

  6. 6. Inventory and Item Management

    An inventory system should allow players to collect, inspect, combine, and use items. Item usage should directly support puzzle resolution and narrative advancement.

  7. 7. Room and Level Construction

    Rooms should be clearly structured with walls, doors, and interactive elements, and may be grouped into themed areas or zones. Optional collision boundaries should ensure logical movement within spaces.

  8. 8. User Interface and Feedback Systems

    The UI should clearly present dialogue, inventory contents, quest information, and interaction prompts. Visual or audio feedback should confirm successful actions or indicate failed attempts.

  9. 9. Event and Script Management

    A flexible event system should handle story triggers, room transitions, puzzle activations, NPC behavior, and cutscenes. This can begin with simple conditional logic and expand into more scalable scripting.

  10. 10. Art and Atmosphere

    Consistent visual themes, lighting, and audio should reinforce mood and narrative tone. Ambient effects and sound design can enhance immersion and emotional impact.

8.3.2. Implementation

Key Operations in the Game

Adventure games rely on a continuous cycle of narrative progression, player interaction, and world updates. Unlike action-oriented genres, the core operations here focus on exploration, dialogue, puzzle resolution, and state management. These operations run throughout the game loop and ensure that the world responds consistently to player actions. The following list summarizes the essential operations that drive an adventure game during runtime.

  1. 1. Process Player Input
    • • Read keyboard or mouse input to move the player, interact with objects, or navigate menus.
    • • Trigger context-sensitive actions such as talking, examining, or using items.
  2. 2. Update Player and NPC States
    • • Move the player character through rooms or environments.
    • • Update NPC behaviors, dialogue availability, or scripted events.
  3. 3. Handle Interactions and Events
    • • Detect when the player is near an interactive object or NPC.
    • • Trigger dialogue, item pickups, puzzle steps, or environmental changes.
    • • Activate story events based on quest flags or conditions.
  4. 4. Manage Inventory and Puzzle Logic
    • • Add or remove items from the inventory.
    • • Check item combinations or usage attempts.
    • • Update puzzle states when the player completes required steps.
  5. 5. Track Game State and Story Progression
    • • Maintain variables for quests, visited locations, solved puzzles, and NPC interactions.
    • • Unlock new dialogue options, rooms, or events based on progress.
  6. 6. Render the World and UI
    • • Draw rooms, characters, objects, and environmental details.
    • • Display dialogue boxes, inventory menus, quest logs, and prompts.
    • • Provide visual or audio feedback for successful or failed actions.
  7. 7. Room Transitions and Navigation
    • • Detect when the player enters a doorway or transition zone.
    • • Load the next room’s layout, objects, and NPCs.
    • • Update the player’s position accordingly.

Structure of the Game Program

An adventure game is composed of several interconnected systems that manage world layout, player interaction, narrative flow, and rendering. Organizing these systems clearly helps ensure that the game remains scalable as more rooms, puzzles, and story elements are added. The following outline describes the typical structure of an adventure game program.

  1. 1. Initialization
    • • Initialize Allegro subsystems (display, keyboard, image, font, primitives).
    • • Create the main window, event queue, and timer.
    • • Load fonts, images, and any initial room or character data.
  2. 2. Resource Loading
    • • Load room definitions, object data, and NPC information.
    • • Prepare textures, background art, and UI assets.
    • • Optional: Load dialogue scripts or quest data from external files.
  3. 3. Entity Creation
    • • Create the player character with initial position and attributes.
    • • Generate rooms, walls, doors, and interactive objects.
    • • Instantiate NPCs and assign their dialogue or behaviors.
  4. 4. Input Handling
    • • Capture keyboard or mouse events.
    • • Map input to movement, interaction, menu navigation, or dialogue choices.
    • • Handle Esc key press or window-close events to exit the game.
  5. 5. Update Functions
    • • Move the player character and apply room boundaries.
    • • Check for interactions with objects, NPCs, or puzzle elements.
    • • Update quest states, puzzle progress, and triggered events.
    • • Manage transitions between rooms or scenes.
  6. 6. Rendering Functions
    • • Draw rooms, walls, doors, and environmental details.
    • • Render the player character and NPCs.
    • • Display UI elements such as dialogue boxes, inventory menus, and prompts.
    • • Flip the display to present the updated frame.
  7. 7. Game Loop
    • • Wait for events from the event queue.
    • • On timer events, update the game state and render the scene.
    • • Maintain a consistent frame rate (e.g., 60 fps).
  8. 8. Shutdown
    • • Destroy fonts, timers, event queues, and the display.
    • • Release any loaded textures or resources.
    • • Cleanly exit the program.

Coding the Game

player_movement.cpp

#include <allegro5/allegro.h>

#include <allegro5/allegro_image.h>

#include <allegro5/allegro_font.h>

#include <allegro5/allegro_ttf.h>

#include <allegro5/allegro_primitives.h>

#include "player_movement.h"

#include "room.h"

#include <iostream>

const float FPS = 60;

const int SCREEN_W = 800;

const int SCREEN_H = 600;

const int PLAYER_SIZE = 32;

void run_game_loop(ALLEGRO_DISPLAY* display, ALLEGRO_FONT* font) {

    al_init_primitives_addon();

    al_install_keyboard();

    ALLEGRO_TIMER *timer = al_create_timer(1.0 / FPS);

    ALLEGRO_EVENT_QUEUE *event_queue = al_create_event_queue();

    al_register_event_source(event_queue, al_get_display_event_source(display));

    al_register_event_source(event_queue, al_get_timer_event_source(timer));

    al_register_event_source(event_queue, al_get_keyboard_event_source());

    bool running = true;

    bool redraw = true;

    float player_x = SCREEN_W / 2.0f—PLAYER_SIZE / 2.0f;

    float player_y = SCREEN_H / 2.0f—PLAYER_SIZE / 2.0f;

    float player_speed = 4.0f;

std::vector<Room> rooms = generateRooms(

    3,               // number of rooms

    SCREEN_W, SCREEN_H,

    110, 170,        // min/max room size

    6,               // wall thickness

    46               // door width

);

    al_start_timer(timer);

    while (running) {

        ALLEGRO_EVENT ev;

        al_wait_for_event(event_queue, &ev);

        if (ev.type == ALLEGRO_EVENT_TIMER) {

            redraw = true;

        } else if (ev.type == ALLEGRO_EVENT_DISPLAY_CLOSE) {

            running = false;

        } else if (ev.type == ALLEGRO_EVENT_KEY_DOWN) {

            switch (ev.keyboard.keycode) {

                case ALLEGRO_KEY_ESCAPE:

                    running = false;

                    break;

                case ALLEGRO_KEY_UP:

                    player_y—= player_speed;

                    break;

                case ALLEGRO_KEY_DOWN:

                    player_y += player_speed;

                    break;

                case ALLEGRO_KEY_LEFT:

                    player_x—= player_speed;

                    break;

                case ALLEGRO_KEY_RIGHT:

                    player_x += player_speed;

                    break;

            }

        }

        if (redraw && al_is_event_queue_empty(event_queue)) {

            redraw = false;

            al_clear_to_color(al_map_rgb(0, 0, 0));

            renderRooms(rooms, al_map_rgb(120,120,120), al_map_rgb(30,30,30));

            al_draw_filled_rectangle(player_x, player_y, player_x + PLAYER_SIZE, playe

            // Center and draw a white ☺ inside the square using the existing font

            const char* smile = u8"\u263A";  // ☺ (Unicode U+263A)

            int tw = al_get_text_width(font, smile);

            int th = al_get_font_line_height(font);

            float tx = player_x + (PLAYER_SIZE—tw) * 0.5f;

            float ty = player_y + (PLAYER_SIZE—th) * 0.5f;

            al_draw_text(font, al_map_rgb(255,255,255), tx, ty, 0, smile);

            al_draw_text(font, al_map_rgb(255, 255, 255), SCREEN_W / 2, 20, ALLEGRO_AL

            al_flip_display();

        }

    }

    al_destroy_timer(timer);

    al_destroy_event_queue(event_queue);

}

player_movement.h

#ifndef PLAYER_MOVEMENT_H

#define PLAYER_MOVEMENT_H

#include <allegro5/allegro.h>

#include <allegro5/allegro_font.h>

void run_game_loop(ALLEGRO_DISPLAY* display, ALLEGRO_FONT* font);

#endif

room.cpp

#include <allegro5/allegro.h>

#include <allegro5/allegro_primitives.h>

#include "room.h"

#include <cstdlib>

#include <ctime>

#include <algorithm>

// Axis-aligned rectangle overlap check

static bool rectsOverlap(float ax,float ay,float aw,float ah,

                         float bx,float by,float bw,float bh) {

    return (ax < bx + bw) && (ax + aw > bx) && (ay < by + bh) && (ay + ah > by);

}

std::vector<Room> generateRooms(

    int count,

    int screenW, int screenH,

    int minSize, int maxSize,

    int wallThickness, int doorWidth

) {

    std::srand((unsigned)std::time(nullptr));

    std::vector<Room> rooms;

    rooms.reserve(count);

    for (int i=0; i<count; ++i) {

        int guard = 0;

        while (true) {

            ++guard;

            float size = (float)(minSize + (std::rand() % std::max(1, maxSize-minSize+1)));

            // Keep some padding from the screen edges

            float x = (float)(10 + std::rand() % std::max(1, screenW—(int)size—20));

            float y = (float)(10 + std::rand() % std::max(1, screenH—(int)size—20));

            // Avoid overlapping prior rooms

            bool ok = true;

            for (const auto& r : rooms) {

                if (rectsOverlap(x, y, size, size, r.x, r.y, r.size, r.size)) { ok = false; break; }

            }

            if (!ok) {

                if (guard < 1500) continue;

            }

            int   side = std::rand() % 4;      // which wall gets the door

            float th   = (float)wallThickness;

            float span = size—2.0f*th;       // usable interior span where a door can be centered

            float dW   = (float)doorWidth;

            if (dW > span—8.0f) dW = std::max(12.0f, span—8.0f);

            // door center along the wall's interior span

            float doorCenter = th + dW*0.5f + (float)(std::rand() % std::max(1, (int)(span—dW)));

            rooms.push_back(Room{ x, y, size, th, side, dW, doorCenter });

            break;

        }

    }

    return rooms;

}

static void drawWallWithGap(float x1, float y1, float x2, float y2,   // wall outer span

                            float gx1, float gy1, float gx2, float gy2,// gap rect (door)

                            ALLEGRO_COLOR wallColor, ALLEGRO_COLOR edgeColor) {

    // Horizontal wall (y1==y2): draw two filled strips split around the gap

    if (y1 == y2) {

        float th = gy2—gy1;

        if (gx1 > x1) al_draw_filled_rectangle(x1, y1, gx1, y1 + th, wallColor);

        if (gx2 < x2) al_draw_filled_rectangle(gx2, y1, x2,  y1 + th, wallColor);

        if (gx1 > x1) al_draw_rectangle(x1, y1, gx1, y1 + th, edgeColor, 1.0f);

        if (gx2 < x2) al_draw_rectangle(gx2, y1, x2,  y1 + th, edgeColor, 1.0f);

    } else { // Vertical wall (x1==x2)

        float tw = gx2—gx1;

        if (gy1 > y1) al_draw_filled_rectangle(x1, y1, x1 + tw, gy1, wallColor);

        if (gy2 < y2) al_draw_filled_rectangle(x1, gy2, x1 + tw, y2,  wallColor);

        if (gy1 > y1) al_draw_rectangle(x1, y1, x1 + tw, gy1, edgeColor, 1.0f);

        if (gy2 < y2) al_draw_rectangle(x1, gy2, x1 + tw, y2,  edgeColor, 1.0f);

    }

}

void renderRooms(const std::vector<Room>& rooms,

                 ALLEGRO_COLOR wallColor,

                 ALLEGRO_COLOR edgeColor) {

    for (const auto& r : rooms) {

        const float x  = r.x;

        const float y  = r.y;

        const float s  = r.size;

        const float th = r.wall;

        // Door gap geometry per wall

        switch (r.doorSide) {

            case 0: { // top (horizontal)

                float gx1 = x + r.doorCenter—r.doorW * 0.5f;

                float gx2 = x + r.doorCenter + r.doorW * 0.5f;

                float gy1 = y, gy2 = y + th;

                drawWallWithGap(x, y, x + s, y, gx1, gy1, gx2, gy2, wallColor, edgeColor);

            } break;

            case 1: { // right (vertical)

                float gy1 = y + r.doorCenter—r.doorW * 0.5f;

                float gy2 = y + r.doorCenter + r.doorW * 0.5f;

                float gx1 = x + s—th, gx2 = x + s;

                drawWallWithGap(x + s—th, y, x + s—th, y + s, gx1, gy1, gx2, gy2, wallColor, edgeColor);

            } break;

            case 2: { // bottom (horizontal)

                float gx1 = x + r.doorCenter—r.doorW * 0.5f;

                float gx2 = x + r.doorCenter + r.doorW * 0.5f;

                float gy1 = y + s—th, gy2 = y + s;

                drawWallWithGap(x, y + s—th, x + s, y + s—th, gx1, gy1, gx2, gy2, wallColor, edgeColor);

            } break;

            case 3: { // left (vertical)

                float gy1 = y + r.doorCenter—r.doorW * 0.5f;

                float gy2 = y + r.doorCenter + r.doorW * 0.5f;

                float gx1 = x, gx2 = x + th;

                drawWallWithGap(x, y, x, y + s, gx1, gy1, gx2, gy2, wallColor, edgeColor);

            } break;

        }

        // Other three solid walls (no gap)

        if (r.doorSide != 0) { // top

            al_draw_filled_rectangle(x, y, x + s, y + th, wallColor);

            al_draw_rectangle(x, y, x + s, y + th, edgeColor, 1.0f);

        }

        if (r.doorSide != 1) { // right

            al_draw_filled_rectangle(x + s—th, y, x + s, y + s, wallColor);

            al_draw_rectangle(x + s—th, y, x + s, y + s, edgeColor, 1.0f);

        }

        if (r.doorSide != 2) { // bottom

            al_draw_filled_rectangle(x, y + s—th, x + s, y + s, wallColor);

            al_draw_rectangle(x, y + s—th, x + s, y + s, edgeColor, 1.0f);

        }

        if (r.doorSide != 3) { // left

            al_draw_filled_rectangle(x, y, x + th, y + s, wallColor);

            al_draw_rectangle(x, y, x + th, y + s, edgeColor, 1.0f);

        }

    }

}

room.h

#ifndef ROOM_H

#define ROOM_H

#include <vector>

#include <allegro5/allegro.h>

// A square room with a single doorway in one wall.

struct Room {

    float x;        // top-left of outer square

    float y;

    float size;     // outer square size (width == height)

    float wall;     // wall thickness

    int   doorSide; // 0=top, 1=right, 2=bottom, 3=left

    float doorW;    // door width

    float doorCenter; // along the selected wall, local coordinate

};

// Generate non-overlapping rooms (best effort) inside screen bounds

std::vector<Room> generateRooms(

    int count,

    int screenW, int screenH,

    int minSize = 100, int maxSize = 180,

    int wallThickness = 6,

    int doorWidth = 40

);

// Draw room borders with a gap for the door

void renderRooms(const std::vector<Room>& rooms, ALLEGRO_COLOR wallColor, ALLEGRO_COLOR edgeColor);

#endif // ROOM_H

Tasks for Completion and Improvement

  1. 1. Level Design: Design the layout of each level, including key locations, obstacles, and objectives. Add collisions to make room walls solid
  2. 2. Puzzles: Create puzzles that challenge the player and fit seamlessly into the game’s world.
  3. 3. UI Elements: Add code for holding the Shift key down as a function to make the player move continuously (fast movement).
  4. 4. Menus and HUD: Create intuitive menus and HUD elements that provide necessary information to the player.
  5. 5. Usability Testing: Test the UI to ensure it is user-friendly and accessible.
  6. 6. Bug Fixing: Identify and fix bugs through rigorous testing.
  7. 7. Balancing: Ensure the game is balanced and provides a fair challenge.
  8. 8. Polish: Add final touches to the game, such as visual effects, sound enhancements, and minor gameplay tweaks.
  9. 9. Final Testing: Perform a final round of testing to ensure the game is polished and free of major issues.

Exercises, Homework Questions, and Projects

Exercises

  1. 1. Adventure Game System Mapping

    Identify the major systems described in this chapter (player movement, rooms, NPCs, interaction, puzzles, inventory, quests, UI). Briefly explain how at least three of these systems interact to support exploration-driven gameplay.

  2. 2. Exploration vs. Action

    Compare adventure games with action-oriented games. Why do adventure games emphasize slower pacing, environmental detail, and player investigation rather than rapid reflexes?

  3. 3. Room-Based World Design

    Explain how dividing the world into rooms or locations simplifies navigation and puzzle design. What advantages does this provide over a single large continuous map for an adventure game?

  4. 4. Puzzle Integration

    Choose one type of puzzle described in the chapter (item-based, environmental, dialogue-based, or sequence puzzle). Explain how it can be integrated naturally into the game world and narrative.

Homework Questions

  1. 1. Narrative Structure and Player Agency

    Discuss how branching dialogue and player choice can influence storytelling in adventure games. How can designers balance player freedom with a coherent narrative?

  2. 2. Inventory-Based Puzzles

    Why are inventory puzzles a common feature in adventure games? What design principles ensure these puzzles feel logical rather than frustrating?

  3. 3. Game-State Tracking

    Adventure games rely heavily on tracking progress (visited rooms, solved puzzles, items collected). What problems might occur if game-state variables are poorly designed or inconsistently updated?

  4. 4. Feedback and Fairness

    Explain why clear feedback is essential in adventure games when a player attempts an incorrect action. How does feedback help prevent players from becoming stuck?

Programming Projects

  1. 1. Room Collision and Boundaries (Core Project)

    Extend the room system by adding collision detection so the player cannot walk through walls except at door locations. Ensure movement feels smooth and predictable.

  2. 2. Object Interaction System

    Implement interactable objects within rooms, such as doors, switches, notes, or keys. Allow the player to examine or use these objects with a dedicated interaction key.

  3. 3. Inventory and Item Usage

    Add an inventory system that allows players to collect items, view them in a menu, and use them to solve puzzles or unlock new areas.

  4. 4. Puzzle Implementation

    Create at least one complete puzzle that requires the player to explore rooms, gather information or items, and perform actions in a specific order to progress.

  5. 5. NPC Dialogue and Story Hooks (Advanced)

    Add NPCs that provide dialogue, hints, or story context. Implement branching dialogue options that change based on quest progress or prior interactions.

  6. 6. Quest and Objective Tracking (Advanced)

    Introduce a simple quest system that tracks objectives and updates as the player completes tasks or solves puzzles. Display the current objective in a quest log or HUD.

  7. 7. Save and Load System (Advanced)

    Implement a save/load feature that preserves the player’s location, inventory contents, puzzle states, and story progress across sessions.

Capstone Project

Design and implement a small but complete adventure game scenario containing multiple rooms, interactive objects, at least one NPC, and a short puzzle-driven quest chain. The project should demonstrate coherent integration of exploration, interaction, puzzle-solving, and narrative feedback. Submit a brief design summary explaining how the story, puzzles, and environment work together to guide the player.

8.4. Arcade Game

Arcade games are a genre that emphasizes fast-paced gameplay, simple controls, and increasing difficulty. They originated in the coin-operated arcade machines of the seventies and eighties but have since evolved to include a wide variety of games available on multiple platforms. Here are some key aspects of arcade games.

8.4.1. Game Design

Game Concept

  • Genre: 2D arcade space shooter.
  • Objective: The player controls a spaceship to survive for as long as possible by destroying incoming enemies, avoiding collisions, preserving a limited number of lives, and achieving the highest possible score before all lives are lost.
  • Gameplay Mechanics:
    • • Move the spaceship left and right.
    • • Shoot bullets to destroy enemies.
    • • Avoid colliding with enemies.

Core Components

  • Player Ship: The spaceship controlled by the player
    • Attributes: Position (x, y), movement speed, sprite, remaining lives
    • Movement: Controlled via keyboard input; the ship moves left and right along the bottom of the screen and is constrained within screen boundaries
    • Shooting: Fires bullets upward when the player presses the designated fire key
  • Enemies: Hostile entities that threaten the player
    • Attributes: Position (x, y), movement speed, size or sprite
    • Movement: Spawn near the top of the screen and move downward toward the player at a constant or gradually increasing speed
    • Behavior: Must be destroyed by player bullets or avoided to prevent collisions
  • Bullets: Projectiles fired by the player’s spaceship
    • Attributes: Position (x, y), speed, size
    • Movement: Travel upward from the player’s ship toward enemies
    • Collision: Deactivate when they strike an enemy or leave the screen
  • Scoring System: Tracks the player’s progress and performance
    • Attributes: Current score
    • Function: Increases the score when an enemy is successfully destroyed
  • Lives System: Represents the player’s remaining chances to stay in the game
    • Attributes: Remaining lives (e.g., three at the start)
    • Rules: A collision between the player ship and an enemy results in the loss of one life; the game ends when all lives are lost

Key Techniques and Features to Have

Arcade games depend on tight controls, fast feedback, and simple but addictive mechanics. The following features form a solid baseline for an arcade-style space shooter.

  1. 1. Responsive Player Controls

    Player movement should be smooth, immediate, and low latency. The ship must be constrained within screen boundaries to prevent off-screen movement, ensuring precise and reliable control.

  2. 2. Shooting and Projectile Management

    The player should fire bullets using a dedicated key. Bullets must spawn relative to the player’s position, update every frame, and be removed when they leave the screen to maintain performance.

  3. 3. Enemy Generation and Behavior

    Enemies should spawn near the top of the screen and move downward toward the player. Speed and behavior variations (such as faster movement or simple patterns) can be introduced to increase challenge. Off-screen or destroyed enemies must be cleaned up promptly.

  4. 4. Collision Detection

    Collision checks should detect bullet–enemy and player–enemy interactions using simple AABB logic. Colliding objects should be deactivated immediately to keep gameplay fast and unambiguous.

  5. 5. Scoring and Life System

    The game should increment the score when enemies are destroyed and track player lives. Collisions with enemies reduce lives, and the game ends when lives reach zero, optionally displaying a game-over message.

  6. 6. Visual Feedback and Simple Effects

    The player, enemies, and bullets should be rendered with clear shapes or sprites. Simple visual effects—such as flashes, colour changes, or small explosion cues—can enhance readability and feedback without increasing complexity.

  7. 7. Increasing Difficulty Over Time

    Difficulty should rise gradually through increased enemy spawn rates, higher speeds, or tougher enemy variants, sustaining tension and encouraging repeat play.

  8. 8. Game Loop Structure and Allegro Integration

    An Allegro timer (e.g., 60 fps) should drive consistent updates. Game logic and rendering must be separated, and input and window events should be handled through Allegro’s event queue.

  9. 9. User Interface Elements

    The interface should display the score and remaining lives clearly. When the game ends, a centered game-over message should be shown for clear feedback.

  10. 10. Simplicity, Speed, and Replayability

    The game should focus on a small set of core actions—moving, shooting, and dodging. Clear visuals, smooth motion, and short play sessions reinforce replayability, a hallmark of arcade games

8.4.2. Implementation

Just as for any game development project with Allegro 5 in C++, we will assume you have all the software tools and the Allegro 5 library installed on the computer platform you have chosen. If not, please go back to chapter 1 to complete the installation.

Key Operations in the Game

Arcade games depend on a fast, tightly controlled sequence of operations that repeat every frame to maintain smooth gameplay and immediate responsiveness. These operations ensure that the player’s actions, enemy behavior, collisions, and scoring all update in real time. The following list summarizes the essential runtime operations that drive the arcade-style space shooter:

  1. 1. Process Player Input
    • • Read keyboard input for left/right movement and shooting.
    • • Clamp the player’s position to screen boundaries to prevent off-screen movement.
  2. 2. Update Player, Bullet, and Enemy States
    • • Move the player based on input.
    • • Advance bullets upward and deactivate them when they leave the screen.
    • • Move enemies downward and remove them when they exit the screen.
  3. 3. Spawn New Enemies
    • • Periodically generate enemies at random horizontal positions.
    • • Adjust spawn frequency or speed to increase difficulty over time.
  4. 4. Perform Collision Detection
    • • Check bullet–enemy collisions and award points for successful hits.
    • • Check player–enemy collisions to determine life loss or trigger game-over conditions.
    • • Deactivate bullets and enemies immediately upon collision.
  5. 5. Manage Scoring and Lives
    • • Increase score when enemies are destroyed.
    • • Track remaining lives and reduce them when the player is hit.
    • • Activate a game-over state when lives reach zero.
  6. 6. Clean Up Inactive Entities
    • • Remove bullets and enemies that are no longer active.
    • • Prevent memory growth by regularly pruning inactive objects.
  7. 7. Render the Frame
    • • Draw the player, bullets, enemies, score, and lives.
    • • Display flashing effects, hit indicators, or “GAME OVER” banners when appropriate.
    • • Flip the display to present the updated frame to the player.

Structure of the Game Program

  1. 1. Initialization
    • • Initialize Allegro and its add-ons for images, primitives, fonts, and keyboard input.
    • • Create a display window, font, timer, and event queue.
  2. 2. Game Loop
    • • Handle events for updating game state, such as player movement, bullet firing, and enemy spawning.
    • • Update the positions of bullets and enemies, and check for collisions.
    • • Draw the player, bullets, enemies, and score/lives on the screen.
  3. 3. Player Movement
    • • Move the player left and right using the arrow keys.
    • • Fire bullets using the space bar.
  4. 4. Collision Detection
    • • Check for collisions between bullets and enemies, and update the score/lives accordingly.
  5. 5. Rendering
    • • Clear the screen and draw the game elements (player, bullets, enemies, score, and lives).

Coding the Game

Here’s the basic structure of the game:

// simple_shooter.cpp  (Lives, player collision, GAME OVER)

#include <allegro5/allegro.h>

#include <allegro5/allegro_image.h>

#include <allegro5/allegro_primitives.h>

#include <allegro5/allegro_font.h>

#include <allegro5/allegro_ttf.h>

#include <vector>

#include <cstdlib>

#include <ctime>   // for std::srand / std::time

const float FPS = 60;

const int SCREEN_W = 800;

const int SCREEN_H = 600;

const int PLAYER_SIZE = 32;

const int BULLET_SIZE = 8;

const int ENEMY_SIZE = 32;

struct Bullet {

    float x, y;

    bool active;

};

struct Enemy {

    float x, y;

    bool active;

};

int main() {

    al_init();

    al_init_image_addon();

    al_init_primitives_addon();

    al_init_font_addon();

    al_init_ttf_addon();

    al_install_keyboard();

    std::srand((unsigned)std::time(nullptr));

    ALLEGRO_DISPLAY *display = al_create_display(SCREEN_W, SCREEN_H);

    ALLEGRO_FONT *font = al_load_ttf_font("arial.ttf", 24, 0);

    if (!font) font = al_create_builtin_font();

    ALLEGRO_TIMER *timer = al_create_timer(1.0 / FPS);

    ALLEGRO_EVENT_QUEUE *event_queue = al_create_event_queue();

    al_register_event_source(event_queue, al_get_display_event_source(display));

    al_register_event_source(event_queue, al_get_timer_event_source(timer));

    al_register_event_source(event_queue, al_get_keyboard_event_source());

    bool running = true;

    bool redraw = true;

    float player_x = SCREEN_W / 2.0f—PLAYER_SIZE / 2.0f;

    float player_y = SCREEN_H—PLAYER_SIZE—10;

    float player_speed = 5.0f;

    int score = 0;

    // Flashing faces on enemies

    int  flashCounter = 0;

    bool flashPhase = false;   // false => ☺ (white), true => ☻ (black)

    // NEW: lives + game-over state

    int  lives = 3;                 // start with 3 lives

    bool gameOver = false;

    int  gameOverFrames = (int)(5 * FPS);  // ~5s pause before exit when game over

    std::vector<Bullet> bullets;

    std::vector<Enemy> enemies;

    al_start_timer(timer);

    while (running) {

        ALLEGRO_EVENT ev;

        al_wait_for_event(event_queue, &ev);

        if (ev.type == ALLEGRO_EVENT_TIMER) {

            if (!gameOver) {

                // Update bullets

                for (auto &bullet : bullets) {

                    if (bullet.active) {

                        bullet.y—= 8;

                        if (bullet.y < 0) {

                            bullet.active = false;

                        }

                    }

                }

                // Update enemies

                for (auto &enemy : enemies) {

                    if (enemy.active) {

                        enemy.y += 2;

                        if (enemy.y > SCREEN_H) {

                            enemy.active = false;

                        }

                    }

                }

                // Flash phase toggle ~4 times/sec (every 15 frames at 60 FPS)

                if (++flashCounter >= 15) {

                    flashCounter = 0;

                    flashPhase = !flashPhase;

                }

                // Bullet–enemy collisions

                for (auto &bullet : bullets) {

                    if (!bullet.active) continue;

                    for (auto &enemy : enemies) {

                        if (!enemy.active) continue;

                        if (bullet.x < enemy.x + ENEMY_SIZE &&

                            bullet.x + BULLET_SIZE > enemy.x &&

                            bullet.y < enemy.y + ENEMY_SIZE &&

                            bullet.y + BULLET_SIZE > enemy.y) {

                            bullet.active = false;

                            enemy.active = false;

                            score += 10;

                        }

                    }

                }

                // Spawn new enemies

                if (std::rand() % 50 == 0) {

                    enemies.push_back({static_cast<float>(std::rand() % (SCREEN_W—ENEMY_SIZE)), 0, true});

                }

                // NEW: Player–enemy collision (use triangle's AABB)

                bool playerHit = false;

                for (auto &enemy : enemies) {

                    if (!enemy.active) continue;

                    bool overlap =

                        player_x < enemy.x + ENEMY_SIZE &&

                        player_x + PLAYER_SIZE > enemy.x &&

                        player_y < enemy.y + ENEMY_SIZE &&

                        player_y + PLAYER_SIZE > enemy.y;

                    if (overlap) {

                        enemy.active = false;   // consume the enemy

                        playerHit = true;

                    }

                }

                if (playerHit) {

                   —lives;

                    if (lives <= 0) {

                        gameOver = true;

                        gameOverFrames = (int)(5 * FPS);

                    }

                }

            } else {

                // NEW: simple 5-second pause then exit

                if (—gameOverFrames <= 0) {

                    running = false;

                }

            }

            redraw = true;

        } else if (ev.type == ALLEGRO_EVENT_DISPLAY_CLOSE) {

            running = false;

        } else if (ev.type == ALLEGRO_EVENT_KEY_DOWN) {

            switch (ev.keyboard.keycode) {

                case ALLEGRO_KEY_LEFT:

                    player_x—= player_speed;

                    if (player_x < 0) player_x = 0;

                    break;

                case ALLEGRO_KEY_RIGHT:

                    player_x += player_speed;

                    if (player_x > SCREEN_W—PLAYER_SIZE) player_x = SCREEN_W—PLAYER_SIZE;

                    break;

                case ALLEGRO_KEY_SPACE:

                    bullets.push_back({player_x + PLAYER_SIZE / 2—BULLET_SIZE / 2, player_y, true});

                    break;

                case ALLEGRO_KEY_ESCAPE:

                    running = false;

                    break;

            }

        }

        if (redraw && al_is_event_queue_empty(event_queue)) {

            redraw = false;

            al_clear_to_color(al_map_rgb(0, 0, 0));

            // Draw player (upright triangle)

            al_draw_filled_triangle(

                player_x + PLAYER_SIZE * 0.5f, player_y,                 // top

                player_x,                         player_y + PLAYER_SIZE, // bottom-left

                player_x + PLAYER_SIZE,           player_y + PLAYER_SIZE, // bottom-right

                al_map_rgb(0, 255, 0)

            );

            // Draw bullets

            for (const auto &bullet : bullets) {

                if (bullet.active) {

                    al_draw_filled_rectangle(

                        bullet.x, bullet.y,

                        bullet.x + BULLET_SIZE, bullet.y + BULLET_SIZE,

                        al_map_rgb(255, 255, 255)

                    );

                }

            }

            // Draw enemies with flashing ☺ / ☻ overlay

            for (const auto &enemy : enemies) {

                if (!enemy.active) continue;

                // enemy block

                al_draw_filled_rectangle(

                    enemy.x, enemy.y,

                    enemy.x + ENEMY_SIZE, enemy.y + ENEMY_SIZE,

                    al_map_rgb(255, 0, 0)

                );

                // flashing face

                const char* whiteFace = u8"\u263A"; // ☺

                const char* blackFace = u8"\u263B"; // ☻

                const char* face = flashPhase ? blackFace : whiteFace;

                ALLEGRO_COLOR faceColor = flashPhase ? al_map_rgb(0, 0, 0) : al_map_rgb(255, 255, 255);

                int tw = al_get_text_width(font, face);

                int th = al_get_font_line_height(font);

                float tx = enemy.x + (ENEMY_SIZE—tw) * 0.5f;

                float ty = enemy.y + (ENEMY_SIZE—th) * 0.5f;

                al_draw_text(font, faceColor, tx, ty, 0, face);

            }

            // Score (top-left)

            al_draw_textf(font, al_map_rgb(255, 255, 255), 10, 10, 0, "Score: %d", score);

            // NEW: Lives (top-right, opposite the score)

            al_draw_textf(font, al_map_rgb(255, 255, 255),

                          SCREEN_W—10, 10, ALLEGRO_ALIGN_RIGHT,

                          "Lives: %d", lives);

            // NEW: GAME OVER banner

            if (gameOver) {

                const char* msg = "GAME OVER";

                int tw = al_get_text_width(font, msg);

                int th = al_get_font_line_height(font);

                float x = (SCREEN_W—tw) * 0.5f;

                float y = (SCREEN_H—th) * 0.5f;

                // optional dim panel behind text (works best with alpha-enabled target)

                al_draw_filled_rectangle(x—20, y—10, x + tw + 20, y + th + 10, al_map_rgba(0, 0, 0, 180));

                al_draw_text(font, al_map_rgb(255, 64, 64), x, y, 0, msg);

            }

            al_flip_display();

        }

    }

    al_destroy_font(font);

    al_destroy_timer(timer);

    al_destroy_event_queue(event_queue);

    al_destroy_display(display);

    return 0;

}

Tasks for Completion and Improvement

  1. 1. Level Design and Difficulty Progression
    • Enemy Waves: Introduce structured waves of enemies with increasing complexity.
    • Speed Scaling: Gradually increase enemy speed or spawn frequency over time.
    • Obstacle Patterns: Add falling obstacles or hazards that require precise dodging.
  2. 2. User Interface (UI) and User Experience (UX) Enhancements
    • HUD Improvements: Add visual indicators for score, lives, level, and power-ups.
    • Menus: Create a start menu, pause menu, and game-over screen with restart options.
    • Accessibility: Ensure clear feedback for hits, missed shots, and player damage.
  3. 3. Testing and Quality Assurance
    • Collision Testing: Ensure bullet–enemy and player–enemy collisions are precise and fair.
    • Spawn Testing: Test edge cases where too many enemies spawn simultaneously.
    • Performance: Monitor frame performance under heavy activity (many bullets, enemies).
  4. 4. Content Expansion and Gameplay Variety
    • Power-Ups: Add temporary boosts such as faster shooting, shields, bigger bullets.
    • Enemy Types: Introduce different enemy behaviors (zigzag, homing, large slow enemies).
    • Boss Fights: Add a special boss enemy every few waves for challenge and pacing.
  5. 5. Visual and Audio Polish
    • Particle Effects: Add small explosion primitives when enemies are destroyed.
    • Background Animation: Implement simple scrolling stars or parallax backgrounds.
    • Sound Effects: Add sounds for firing, explosions, collecting power-ups, and losing lives.
  6. 6. Finalization and Release Preparation
    • Bug Fixing: Ensure no soft-locks, infinite bullets, or overlapping UI issues occur.
    • Balancing: Adjust enemy hit/health points (HP), speed, and spawn rates for an enjoyable difficulty curve.
    • Final Testing: Run full game sessions to ensure smooth pacing and stable performance.

Exercises, Homework Questions, and Projects

Exercises

  1. 1. Arcade Design Principles

    Explain why arcade games prioritize simple controls and immediate visual feedback. How do these principles contribute to short, replayable play sessions?

  2. 2. Game Loop Structure

    Describe the role of the game loop in an arcade shooter. Why is it important to separate update logic (movement, collisions) from rendering logic?

  3. 3. Collision Detection Strategy

    Review the bullet–enemy and player–enemy collision checks used in the game. Why are axis-aligned bounding box (AABB) checks sufficient for this type of arcade game?

  4. 4. Lives and Game-Over Logic

    Explain how the life system controls game difficulty and session length. What happens to gameplay tension as the number of remaining lives decreases?

Homework Questions

  1. 1. Difficulty Progression

    Arcade games often become more difficult over time. Discuss at least three ways difficulty can be scaled in a space shooter without changing the basic controls.

  2. 2. Performance Considerations

    Why is it important to remove inactive bullets and enemies from memory each frame? What problems might arise if inactive objects are never cleaned up?

  3. 3. Visual Readability

    Arcade games typically use high-contrast colours and simple shapes. Why is visual clarity especially important when many enemies or projectiles are on screen?

  4. 4. Replay Value

    What features encourage players to replay arcade games repeatedly? Consider scoring systems, randomness, and escalating challenge in your answer.

Programming Projects

  1. 1. Enemy Waves and Patterns (Core Project)

    Replace random enemy spawning with structured waves. Each wave should increase in difficulty by introducing faster enemies, denser formations, or new movement patterns.

  2. 2. Difficulty Scaling System

    Implement gradual difficulty progression by increasing enemy speed, spawn frequency, or bullet speed as the player’s score increases.

  3. 3. Power-Ups and Bonuses

    Add power-ups that temporarily enhance the player, such as faster firing, shields, wider bullets, or bonus points. Ensure power-ups expire after a fixed duration.

  4. 4. Additional Enemy Types

    Introduce new enemy behaviors, such as zigzag movement and homing shots, or large, slow enemies that require multiple hits to destroy.

  5. 5. Score Multipliers and Combos (Advanced)

    Implement a combo or multiplier system that rewards players for destroying enemies consecutively without missing or taking damage.

  6. 6. Pause and Menu System (Advanced)

    Add a start menu, pause menu, and game-over menu. Allow the player to restart or exit cleanly without closing the program.

Capstone Project (Optional)

Design and implement a polished arcade space shooter that includes multiple enemy waves, difficulty scaling, power-ups, scoring depth, and visual and audio effects. The game should be fully self-contained, support repeated play sessions, and clearly demonstrate classic arcade design principles. Include a short write-up explaining how your design choices enhance challenge, clarity, and replayability.

8.5. Board Game

Board games are a wonderful way to bring people together, promote social interaction, and create lasting memories. Whether you’re playing with family, friends, or new acquaintances, the social benefits of board games are undeniable.

In this project, we’ll create a basic tic-tac-toe game, where two players take turns placing their marks (X or O) on a 3 × 3 grid.

8.5.1. Game Design

Game Concept

  • Genre: Board game.
  • Objective: Two players take turns placing their marks (X or O) on a 3 × 3 grid. The first player to align three marks horizontally, vertically, or diagonally wins the game. If all cells are filled without a winning alignment, the game ends in a draw.
  • Gameplay Mechanics:
    • • Players click on a cell to place their mark.
    • • The game checks for a win or draw after each move.

Core Components

  • Game Board (Grid): The play area where the game takes place
    • Attributes: Grid size (3 × 3), cell dimensions, cell states (empty, X, O)
    • Function: Defines valid positions for placing marks and serves as the basis for win and draw detection logic
  • Player Marks: The symbols placed by players during the game
    • Types: X and O
    • Attributes: Mark type, grid position (row, column)
    • Rules: Marks can only be placed in empty grid cells and cannot be changed once placed
  • Turn Management System: Controls which player may act at any given time
    • Attributes: Current player (Player X or Player O)
    • Function: Alternates turns after each valid move and prevents multiple actions in the same turn
    • Feedback: Optionally displays a visual or textual indicator showing whose turn it is
  • Win and Draw Detection System: Determines the outcome of the game
    • Checks: Three matching marks in any row, column, or diagonal
    • Draw Condition: All grid cells are filled and no winning alignment exists
    • Function: Ends the game when a win or draw condition is met and prevents further input

Key Techniques and Features to Have

Although Tic-Tac-Toe is a simple board game, a clean implementation in Allegro 5 still requires clear state management, precise input handling, and readable rendering. The following features define a solid and reliable baseline.

  1. 1. Grid Representation and Board-State Management

    The game board should be represented by a 3 × 3 data structure storing cell states (empty, X, or O). Helper functions should reset the board, query cell values, and enforce valid updates so marked cells cannot be overwritten.

  2. 2. Player Input Handling (Mouse Interaction)

    Mouse clicks should be detected through Allegro’s event system and translated from screen coordinates to grid positions. Moves must be validated, and turns should alternate automatically between Player X and Player O.

  3. 3. Turn Management and Visual Feedback

    The current player should be tracked explicitly and updated after each valid move. The interface should clearly indicate whose turn it is, with optional hover highlighting to improve usability.

  4. 4. Win and Draw Detection

    After each move, the game should check for three-in-a-row in rows, columns, or diagonals. A draw should be detected when the board is full with no winner, and further input should be disabled when the game ends.

  5. 5. Rendering the Board and Marks

    The grid should be drawn using Allegro primitives, with X and O marks rendered clearly using consistent padding, contrast, and line thickness to ensure readability at all resolutions.

  6. 6. End-of-Game Display

    Clear messages such as “Player X Wins,” “Player O Wins,” or “It’s a Draw” should be displayed and centered on the screen. Optional visual emphasis, such as dimming the background or highlighting the winning line, may be added.

  7. 7. Game Reset or Replay (Optional)

    The game may provide a simple reset mechanism, such as a key press or replay button, allowing players to start a new round without restarting the application.

  8. 8. Basic UI and User Experience Considerations

    All text should use readable fonts with adequate spacing. Optional enhancements such as hover effects or subtle animations can modernize the experience without adding complexity.

8.5.2. Implementation

Just as for any game development project with Allegro 5 in C++, we will assume you have all the software tools and the Allegro 5 library installed on the computer platform you have chosen. If not, please go back to chapter 1 to complete the installation.

Key Operations in the Game

Implementing a tic-tac-toe game in Allegro 5 requires several core operations that ensure smooth gameplay, accurate input handling, and correct game state evaluation. These operations form the functional backbone of the program:

  1. 1. Mouse Input and Cell Selection
    • • Detect mouse clicks using Allegro’s mouse event system.
    • • Convert screen coordinates (x, y) into grid indices (row, col) using the formula:
      • ◦ row = y / CELL_SIZE
      • ◦ col = x / CELL_SIZE
    • • Validate that the selected cell is within bounds and currently empty before placing a mark.
  2. 2. Mark Placement
    • • Place the current player’s mark (X or O) into the selected grid cell.
    • • Prevent overwriting by checking grid[row][col] == NONE before assignment.
    • • Switch the turn to the other player immediately after a valid placement.
  3. 3. Board Rendering
    • • Draw the 3 × 3 grid using al_draw_line() to create dividing lines.
    • • Draw all X and O marks based on the grid array:
      • ◦ X marks: drawn with two diagonal lines.
      • ◦ O marks: drawn with a circle centered in the cell.
    • • Ensure consistency using padding, stroke width, and centered alignment.
  4. 4. Win Detection
    • • After every move, check the grid for a winning pattern:
      • ◦ Any of the three rows
      • ◦ Any of the three columns
      • ◦ Either diagonal
    • • Return the winning player when a match of three identical marks is detected.
  5. 5. Draw Detection
    • • If the board is completely filled and no winner has been found, declare a draw.
    • • This operation ensures the game ends correctly even without a winning line.
  6. 6. Turn Management
    • • Alternate between PLAYER_X and PLAYER_O after each valid move.
    • • Maintain a variable (e.g., currentPlayer) for easy swapping.
    • • Optional: Display the active player on the screen to guide users.
  7. 7. Game State Rendering
    • • Update the display every loop by drawing:
      • ◦ The grid
      • ◦ Current marks
      • ◦ Text messages such as “Player X Wins!” or “It’s a Draw!”
    • • Use Allegro’s font features for clean, centered text output.
  8. 8. End-of-Game Handling
    • • Stop accepting clicks once a winner or draw has been declared.
    • • Show the final result clearly on-screen.
    • • Optional: Offer restart behavior via a button, key press, or automatic reset.

Structure of the Game Program

Before you start coding, keep in mind the following general structure of the game program:

  1. 1. Initialization
    • • Initialize Allegro and its add-ons for primitives, fonts, and mouse input.
    • • Create a display window, font, and event queue.
  2. 2. Game Loop
    • • Handle events for mouse clicks to place marks on the grid.
    • • Update the game state, including checking for a winner or draw.
    • • Draw the grid, marks, and game status (winner or draw).
  3. 3. Drawing Functions
    • • draw_grid(): Draws the 3 × 3 grid.
    • • draw_marks(): Draws the X and O marks on the grid.
  4. 4. Game Logic
    • • check_winner(): Checks if there is a winner.
    • • is_draw(): Checks if the game is a draw.
  5. 5. Shutdown
    • • Clean release of Allegro resources.

For gameplay, the first player selects a square, then the second player proceeds to do the same. Play proceeds until one player manages to complete three in a row for their respective character (X or O) or the game ends in a draw.

Coding the Game

code—board_game.cpp

#include <allegro5/allegro.h>

#include <allegro5/allegro_primitives.h>

#include <allegro5/allegro_font.h>

#include <allegro5/allegro_ttf.h>

#include <iostream>

const int SCREEN_W = 600;

const int SCREEN_H = 600;

const int GRID_SIZE = 3;

const int CELL_SIZE = SCREEN_W / GRID_SIZE;

const int PADDING = 12; // visual padding for X/O rendering

enum Player { NONE, PLAYER_X, PLAYER_O };

Player grid[GRID_SIZE][GRID_SIZE];

Player currentPlayer = PLAYER_X;

// Draw the 3 × 3 grid (skip the outer border lines)

void draw_grid() {

    for (int i = 1; i < GRID_SIZE; ++i) {

        al_draw_line(i * CELL_SIZE, 0, i * CELL_SIZE, SCREEN_H, al_map_rgb(255, 255, 255), 2);

        al_draw_line(0, i * CELL_SIZE, SCREEN_W, i * CELL_SIZE, al_map_rgb(255, 255, 255), 2);

    }

}

// Draw X and O marks that are already placed

void draw_marks() {

    for (int row = 0; row < GRID_SIZE; ++row) {

        for (int col = 0; col < GRID_SIZE; ++col) {

            if (grid[row][col] == PLAYER_X) {

                // Two diagonals to form an X

                al_draw_line(col * CELL_SIZE + PADDING,         row * CELL_SIZE + PADDING,

                             (col + 1) * CELL_SIZE—PADDING,  (row + 1) * CELL_SIZE—PADDING,

                             al_map_rgb(255, 0, 0), 4);

                al_draw_line((col + 1) * CELL_SIZE—PADDING,   row * CELL_SIZE + PADDING,

                             col * CELL_SIZE + PADDING,         (row + 1) * CELL_SIZE—PADDING,

                             al_map_rgb(255, 0, 0), 4);

            } else if (grid[row][col] == PLAYER_O) {

                // Circle centered in the cell

                al_draw_circle(col * CELL_SIZE + CELL_SIZE / 2.0f,

                               row * CELL_SIZE + CELL_SIZE / 2.0f,

                               CELL_SIZE / 2.0f—PADDING,

                               al_map_rgb(0, 0, 255), 4);

            }

        }

    }

}

// Return the winner, if any; otherwise NONE

Player check_winner() {

    // Rows and columns

    for (int i = 0; i < GRID_SIZE; ++i) {

        // Row i

        if (grid[i][0] != NONE &&

            grid[i][0] == grid[i][1] &&

            grid[i][1] == grid[i][2]) {

            return grid[i][0];

        }

        // Column i

        if (grid[0][i] != NONE &&

            grid[0][i] == grid[1][i] &&

            grid[1][i] == grid[2][i]) {

            return grid[0][i];

        }

    }

    // Diagonals

    if (grid[0][0] != NONE &&

        grid[0][0] == grid[1][1] &&

        grid[1][1] == grid[2][2]) {

        return grid[0][0];

    }

    if (grid[0][2] != NONE &&

        grid[0][2] == grid[1][1] &&

        grid[1][1] == grid[2][0]) {

        return grid[0][2];

    }

    return NONE;

}

bool is_draw() {

    for (int row = 0; row < GRID_SIZE; ++row) {

        for (int col = 0; col < GRID_SIZE; ++col) {

            if (grid[row][col] == NONE) return false;

        }

    }

    return true;

}

int main() {

    if (!al_init()) {

        std::cerr << "Failed to init Allegro\\n";

        return—1;

    }

    al_init_primitives_addon();

    al_init_font_addon();

    al_init_ttf_addon();

    al_install_mouse();

    // Explicitly clear board to NONE

    for (int r = 0; r < GRID_SIZE; ++r) {

        for (int c = 0; c < GRID_SIZE; ++c) grid[r][c] = NONE;

    }

    ALLEGRO_DISPLAY *display = al_create_display(SCREEN_W, SCREEN_H);

    if (!display) {

        std::cerr << "Failed to create display\\n";

        return—1;

    }

    ALLEGRO_FONT *font = al_load_ttf_font("arial.ttf", 28, 0);

    if (!font) {

        // Fall back to built-in font so the program still runs even if TTF load fails

        font = al_create_builtin_font();

    }

    ALLEGRO_EVENT_QUEUE *event_queue = al_create_event_queue();

    al_register_event_source(event_queue, al_get_display_event_source(display));

    al_register_event_source(event_queue, al_get_mouse_event_source());

    bool running = true;

    Player winner = NONE;

    while (running) {

        ALLEGRO_EVENT ev;

        al_wait_for_event(event_queue, &ev);

        if (ev.type == ALLEGRO_EVENT_DISPLAY_CLOSE) {

            running = false;

        } else if (ev.type == ALLEGRO_EVENT_MOUSE_BUTTON_DOWN) {

            int col = ev.mouse.x / CELL_SIZE;

            int row = ev.mouse.y / CELL_SIZE;

            if (row >= 0 && row < GRID_SIZE && col >= 0 && col < GRID_SIZE) {

                if (grid[row][col] == NONE && winner == NONE) {

                    grid[row][col] = currentPlayer;

                    currentPlayer = (currentPlayer == PLAYER_X) ? PLAYER_O : PLAYER_X; // toggle player

                    winner = check_winner();

                }

            }

        }

        al_clear_to_color(al_map_rgb(0, 0, 0));

        draw_grid();

        draw_marks();

        if (winner != NONE) {

            const char* msg = (winner == PLAYER_X) ? "Player X Wins!" : "Player O Wins!";

            al_draw_text(font, al_map_rgb(255, 255, 255), SCREEN_W / 2.0f, SCREEN_H / 2.0f, ALLEGRO_ALIGN_CENTER, msg);

        } else if (is_draw()) {

            al_draw_text(font, al_map_rgb(255, 255, 255), SCREEN_W / 2.0f, SCREEN_H / 2.0f, ALLEGRO_ALIGN_CENTER, "It's a Draw!");

        }

        al_flip_display();

    }

    if (font) al_destroy_font(font);

    if (event_queue) al_destroy_event_queue(event_queue);

    if (display) al_destroy_display(display);

    return 0;

}

Tasks for Completion and Improvement

  1. 1. Enhanced Game Logic and Features
    • Win Highlighting: Draw a line or highlight the three winning cells.
    • Undo/Redo: Allow players to revert moves for casual play or teaching purposes.
    • Replayability: Add a “Play Again” button or automatic board reset.
  2. 2. User Interface (UI) and User Experience (UX) Improvements
    • Mark Previews: Show a faint preview (ghost mark) when hovering over a cell.
    • Turn Display: Improve turn indication with stylized icons or animated text.
    • Themes and Skins: Add selectable board themes (modern, classic, neon, emoji-based).
  3. 3. Game Modes and Rule Variations
    • AI Opponent: Implement multiple AI difficulty levels (random, defensive, minimax).
    • Custom Board Sizes: Allow players to pick 3 × 3, 4 × 4, or 5 × 5 grids.
    • Timed Mode: Give each player a limited time per move.
  4. 4. Testing and Quality Assurance
    • Input Validation: Test for out-of-bounds clicks or rapid click inputs.
    • Draw Detection: Verify draw logic across all grid sizes if scaling is implemented.
    • Device Compatibility: Ensure proper rendering on different screen resolutions.
  5. 5. Polish and Presentation
    • Animations: Animate X/O placement for a smoother visual feel.
    • Sound Effects: Add subtle click sounds, win chimes, and draw notifications.
    • Game-Over Screen: Add smoother transition effects when declaring the winner.
  6. 6. Final Touches and Optional Extensions
    • Tournament Mode: Track wins across multiple rounds or best-of-three series.
    • Online Play: Add turn-based networking for remote two-player matches.
    • Statistics: Track player wins, losses, draws, and longest streaks.

Exercises, Homework Questions, and Projects

Exercises

  1. 1. Board-State Representation

    Explain how the 3 × 3 grid array represents the game board state. Why is it important to include an explicit NONE state in addition to PLAYER_X and PLAYER_O?

  2. 2. Mouse-Based Input Mapping

    Describe how mouse click coordinates are converted into grid indices. What assumptions does this conversion make about cell size and screen layout?

  3. 3. Turn Management Logic

    Explain how the program ensures that Player X and Player O alternate turns correctly. What could go wrong if turn switching were not centralized in one place?

  4. 4. Win Detection Strategy

    Review the check_winner() function. Why is it sufficient to check rows, columns, and diagonals explicitly for a 3 × 3 Tic-Tac-Toe grid?

Homework Questions

  1. 1. State-Driven Game Logic

    Discuss how Tic-Tac-Toe demonstrates the idea of a finite game state (playing → won → draw). How does freezing input after the game ends simplify game logic?

  2. 2. User Feedback and Clarity

    Why is immediate visual feedback (drawing marks instantly, showing win/draw messages) important for board games? What kinds of confusion could arise without it?

  3. 3. Scalability Considerations

    How would the logic for win detection need to change if the board size were increased from 3 × 3 to 4 × 4 or 5 × 5?

  4. 4. Human vs. AI Play

    What challenges arise when replacing one human player with an AI opponent? Why might Tic-Tac-Toe be a good introductory problem for implementing game AI?

Programming Projects

  1. 1. Win Highlighting (Core Project)

    Extend the game to visually highlight the three winning cells or draw a line through the winning combination once a player has won.

  2. 2. Replay and Reset Functionality

    Add a button or key binding (such as R) that resets the board and starts a new round without restarting the program.

  3. 3. Move Undo and Redo

    Implement undo and redo functionality to allow players to step backward and forward through their move history. This feature is especially useful for teaching and experimentation.

  4. 4. AI Opponent

    Replace one of the human players with an AI. Begin with a random-move AI, then implement more advanced strategies such as defensive play or the minimax algorithm.

  5. 5. Custom Board Sizes (Advanced)

    Extend the game to support different board sizes (for example, 4 × 4 or 5 × 5). Update win and draw detection logic accordingly.

  6. 6. Timed Mode (Advanced)

    Add an optional timer that limits how long each player has to make a move. If time expires, the turn is forfeited or the game ends.

Capstone Project

Design a polished Tic-Tac-Toe application that includes visual win highlighting, sound effects, selectable themes, multiple game modes (human vs. human and human vs. AI), and persistent statistics tracking wins, losses, draws, and streaks. Include a short design reflection explaining how each added feature improves clarity, replayability, or user experience.

8.6. Emulator Game

  • Emulators are software programs that replicate the hardware and instruction set of older computing or gaming systems, allowing programs originally written for those systems to run unmodified on modern hardware. In this project, we design and implement a CHIP-8 emulator using Allegro 5 in C++.
  • CHIP-8 is a simple interpreted virtual machine developed in the 1970s and commonly used to teach emulation concepts. Despite its simplicity, it demonstrates all the core ideas behind real emulators, including instruction decoding, memory management, input handling, timing, and graphics output. Many classic games—such as Pong, Breakout, and Space Invaders—were written for CHIP-8 and can be executed directly by the emulator.

8.6.1. Emulator Design

Emulator Concept

  • Genre: Retro system emulator / virtual machine.
  • Objective: Implement a functional CHIP-8 emulator capable of loading and running original CHIP-8 game ROMs, faithfully reproducing their behavior, graphics, and input on a modern system.
  • Emulated System Characteristics:
    • • Memory Size: 4 KB
    • • Registers: 16 general-purpose registers (V0–VF)
    • • Program Control: Program counter and stack
    • • Display: Monochrome 64 × 32 pixel framebuffer
    • • Input: Hexadecimal keypad with 16 keys
    • • Timers: Delay timer and sound timer, updated at 60 Hz

Core Components

  • CPU (Interpreter): The central processing unit of the emulator.
    • Attributes: Program counter (PC), index register (I), general-purpose registers (V0–VF), stack, stack pointer
    • Function: Fetches, decodes, and executes CHIP-8 instructions (opcodes) according to the original specification
    • Behavior: Each opcode manipulates registers, memory, timers, graphics output, or program control flow
  • Memory System: Represents the CHIP-8 addressable memory space
    • Attributes: 4 KB memory array, reserved interpreter region, program memory region starting at address 0x200
    • Function: Stores program instructions, font sprites, and runtime data used by the emulator
    • Use: ROM files are loaded into memory and executed directly without modification
  • Display System: Handles graphical output for CHIP-8 programs
    • Attributes: 64 × 32 monochrome framebuffer
    • Rendering Model: Pixels are drawn using XOR logic, allowing sprite erasure and collision detection
    • Collision Flag: A dedicated register flag is set when a sprite draw operation overwrites an existing pixel
  • Input System: Maps modern keyboard input to CHIP-8 controls
    • Attributes: 16-key hexadecimal keypad state
    • Function: Translates keyboard events into CHIP-8 key presses so that original games receive input as intended
    • Special Behavior: Supports blocking key-wait instructions used by some CHIP-8 programs
  • Timer System: Maintains consistent timing behavior across all programs
    • Attributes: Delay timer, sound timer
    • Update Rate: Both timers decrement at 60 Hz
    • Function: Controls instruction timing, animations, and sound behavior in accordance with the CHIP-8 specification

Key Techniques and Features to Have

Implementing a CHIP-8 emulator requires faithful reproduction of the original virtual machine’s behavior. Although simpler than full console emulators, CHIP-8 demonstrates all core emulator design concepts:

  1. 1. Instruction Fetch-Decode-Execute Cycle

    The emulator should repeatedly fetch 16-bit opcodes from memory using the program counter, decode them via bit masks and shifts, execute the corresponding instruction, and update the program counter correctly.

  2. 2. Timing and Synchronization

    CPU execution should run at a controlled rate, while delay and sound timers decrement at 60 Hz using an Allegro timer. CPU execution should be decoupled from rendering to ensure stable timing.

  3. 3. Rendering

    The 64 × 32 monochrome framebuffer should be scaled to a modern window size and drawn using Allegro primitives or bitmaps. The display should be redrawn only when a draw instruction modifies the framebuffer.

  4. 4. Input Handling

    Keyboard input should be mapped to the CHIP-8 hexadecimal keypad. The emulator must support blocking key-wait instructions required by some CHIP-8 programs.

  5. 5. ROM Loading

    External CHIP-8 ROM files should be loaded into memory at the correct start address (0x200), and execution should begin from that location without modification.

8.6.2. Implementation

Key Operations in the Emulator

  1. 1. Initialization
    • • Initialize Allegro, keyboard input, display, timer, and event queue.
    • • Allocate memory and registers.
    • • Load built-in font sprites into memory.
  2. 2. ROM Loading
    • • Read a CHIP-8 ROM from file.
    • • Copy it into memory starting at address 0x200.
  3. 3. Main Emulation Loop
    • • Fetch and execute one or more opcodes per tick.
    • • Update timers at a fixed rate.
    • • Handle input and redraw the display when required.
  4. 4. Opcode Execution
    • • Implement arithmetic, logic, branching, graphics, and input instructions.
    • • Maintain correctness with respect to the CHIP-8 specification.
  5. 5. Rendering
    • • Convert the 64 × 32 framebuffer into scaled pixels on the screen.
    • • Clear and redraw efficiently.

Structure of the Emulator Program

  1. 1. Initialization
    • • Allegro setup and system Initialization
    • • Emulator state allocation
  2. 2. Emulation Loop
    • • Opcode execution
    • • Timer updates
    • • Event handling
  3. 3. Graphics Rendering
    • • Draw scaled monochrome pixels
    • • Refresh display only when necessary
  4. 4. Shutdown
    • • Clean release of Allegro resources

Coding the Emulator

#include <allegro5/allegro.h>

#include <allegro5/allegro_primitives.h>

#include <iostream>

#include <fstream>

#include <cstring>

// ====================

// CHIP-8 CONSTANTS

// ====================

static const int MEM_SIZE   = 4096;

static const int VIDEO_W    = 64;

static const int VIDEO_H    = 32;

static const int SCALE      = 10;

static const int START_ADDR = 0x200;

// ====================

// CHIP-8 STATE

// ====================

uint8_t  memory[MEM_SIZE];

uint8_t  V[16];              // Registers V0–VF

uint16_t I;                  // Index register

uint16_t pc;                 // Program counter

uint16_t stack[16];

uint8_t  sp;

uint8_t  delay_timer;

uint8_t  sound_timer;

uint8_t  framebuffer[VIDEO_W * VIDEO_H];

bool     keypad[16];

// ====================

// STUDENT TODO FLAGS

// ====================

bool debugMode = false;

int  cycles_per_frame = 8;   // STUDENT TODO: make configurable

// ====================

// INITIALIZATION

// ====================

void reset() {

    memset(memory, 0, sizeof(memory));

    memset(V, 0, sizeof(V));

    memset(stack, 0, sizeof(stack));

    memset(framebuffer, 0, sizeof(framebuffer));

    memset(keypad, 0, sizeof(keypad));

    pc = START_ADDR;

    I  = 0;

    sp = 0;

    delay_timer = 0;

    sound_timer = 0;

    // TODO (Task 2): Load CHIP-8 fontset into memory

}

// ====================

// ROM LOADING

// ====================

bool loadROM(const char* filename) {

    std::ifstream file(filename, std::ios::binary);

    if (!file) return false;

    file.read((char*)&memory[START_ADDR], MEM_SIZE - START_ADDR);

    return true;

}

// ====================

// OPCODE EXECUTION

// ====================

void executeCycle() {

    uint16_t opcode = memory[pc] << 8 | memory[pc + 1];

    pc += 2;

    // TODO (Task 2):

    // Decode and execute CHIP-8 opcodes.

    // Implement at least:

    // - CLS (00E0)

    // - JP addr (1NNN)

    // - LD Vx, byte (6XNN)

    // - ADD Vx, byte (7XNN)

    // - DRW Vx, Vy, nibble (DXYN)

    if (debugMode) {

        std::cout << "PC=" << std::hex << pc

                  << " OP=" << opcode << std::dec << "\n";

    }

}

// ====================

// DEBUG DRAWING

// ====================

void drawDebugOverlay() {

    // TODO (Task 4):

    // Display register values, PC, I, timers.

    // Optional: display memory or stack contents.

}

// ====================

// MAIN PROGRAM

// ====================

int main(int argc, char** argv) {

    if (argc < 2) {

        std::cerr << "Usage: chip8 <rom>\n";

        return 1;

    }

    // Allegro setup

    al_init();

    al_init_primitives_addon();

    al_install_keyboard();

    ALLEGRO_DISPLAY* display =

        al_create_display(VIDEO_W * SCALE, VIDEO_H * SCALE);

    ALLEGRO_TIMER* timer = al_create_timer(1.0 / 60.0);

    ALLEGRO_EVENT_QUEUE* queue = al_create_event_queue();

    al_register_event_source(queue, al_get_display_event_source(display));

    al_register_event_source(queue, al_get_timer_event_source(timer));

    al_register_event_source(queue, al_get_keyboard_event_source());

    reset();

    if (!loadROM(argv[1])) {

        std::cerr << "Failed to load ROM\n";

        return 1;

    }

    al_start_timer(timer);

    bool running = true;

    while (running) {

        ALLEGRO_EVENT ev;

        al_wait_for_event(queue, &ev);

        if (ev.type == ALLEGRO_EVENT_TIMER) {

            // ====================

            // CPU EXECUTION

            // ====================

            for (int i = 0; i < cycles_per_frame; ++i) {

                executeCycle();

            }

            // ====================

            // TIMERS

            // ====================

            if (delay_timer > 0) delay_timer--;

            if (sound_timer > 0) sound_timer--;

            // TODO (Task 1):

            // Play sound while sound_timer > 0

            // ====================

            // RENDERING

            // ====================

            al_clear_to_color(al_map_rgb(0, 0, 0));

            for (int y = 0; y < VIDEO_H; ++y) {

                for (int x = 0; x < VIDEO_W; ++x) {

                    if (framebuffer[y * VIDEO_W + x]) {

                        al_draw_filled_rectangle(

                            x * SCALE, y * SCALE,

                            (x + 1) * SCALE, (y + 1) * SCALE,

                            al_map_rgb(255, 255, 255)

                        );

                    }

                }

            }

            if (debugMode) {

                drawDebugOverlay();

            }

            al_flip_display();

        }

        else if (ev.type == ALLEGRO_EVENT_KEY_DOWN) {

            // ====================

            // INPUT

            // ====================

            if (ev.keyboard.keycode == ALLEGRO_KEY_ESCAPE)

                running = false;

            // TODO (Task 3):

            // Increase/decrease cycles_per_frame

            // TODO (Task 4):

            // Toggle debug mode (e.g., F1)

        }

        else if (ev.type == ALLEGRO_EVENT_DISPLAY_CLOSE) {

            running = false;

        }

    }

    al_destroy_event_queue(queue);

    al_destroy_timer(timer);

    al_destroy_display(display);

    return 0;

}

Tasks for Completion and Improvement

  1. 1. Sound Support: Add audible feedback when the sound timer is active.
  2. 2. Instruction Accuracy: Verify emulator behavior against known CHIP-8 test ROMs.
  3. 3. Configurable Speed: Allow the user to adjust CPU execution speed.
  4. 4. Debug Mode: Add register and memory inspection tools for learning and testing.

Exercises, Homework Questions, and Projects

Exercises

  1. 1. Emulation vs. Game Recreation

    Explain the difference between writing an emulator and re-implementing a game. Why does running original CHIP-8 ROMs require accurate replication of the original system’s behavior?

  2. 2. Instruction Fetch-Decode-Execute Cycle

    Describe each stage of the fetch-decode-execute cycle used in the CHIP-8 emulator. Why is it important to increment or modify the program counter carefully during opcode execution?

  3. 3. Timers and Synchronization

    Explain the difference between the CPU execution rate and the 60 Hz delay and sound timers in CHIP-8. What problems can occur if these timers are updated incorrectly?

  4. 4. Framebuffer and XOR Drawing

    Explain why CHIP-8 uses XOR logic when drawing sprites to the framebuffer. How does this approach simplify collision detection?

Homework Questions

  1. 1. Opcode Accuracy and Undefined Behavior

    Why is opcode accuracy critical for emulator correctness? What types of bugs might appear if even a single opcode is implemented incorrectly?

  2. 2. Input-Mapping Challenges

    Discuss the challenges involved in mapping a modern keyboard to the CHIP-8 hexadecimal keypad. How can poor input mapping affect game playability?

  3. 3. Rendering Efficiency

    Why is it beneficial to redraw the display only when the display flag is set, rather than every CPU cycle?

  4. 4. Debugging Emulated Systems

    Explain how a debug mode that displays registers, memory, and the program counter can help diagnose emulator errors more effectively than logging alone.

Programming Projects

  1. 1. Sound Timer Support (Core Project)

    Implement sound output that plays whenever the sound timer is greater than zero. Use Allegro’s audio system to generate a short beep or tone, and ensure it stops precisely when the timer reaches zero.

  2. 2. Opcode Set Completion

    Extend the emulator by implementing all remaining CHIP-8 opcodes. Verify correctness by running multiple test ROMs and comparing behavior with known-good emulator results.

  3. 3. Configurable CPU Speed

    Add keyboard controls that allow the user to increase or decrease the number of CPU cycles executed per frame at runtime. Display the current speed on screen.

  4. 4. Debug Overlay Implementation

    Implement a toggleable debug overlay that displays register values (V0–VF), the index register, program counter, stack pointer, and timers. Optionally highlight memory regions accessed by the current opcode.

  5. 5. ROM Selector Interface (Advanced)

    Create a simple menu system that allows the user to select and load CHIP-8 ROMs without restarting the program.

  6. 6. Super-CHIP Extension (Advanced)

    Extend the emulator to support Super-CHIP instructions, including higher-resolution graphics and extended opcodes.

Capstone Project

Develop a polished CHIP-8 emulator application that supports accurate opcode execution, sound, configurable speed, debugging tools, and multiple ROM loading. Include documentation describing how the emulator implements the fetch-decode-execute cycle, timer synchronization, and input mapping. Demonstrate correctness by running at least three classic CHIP-8 games successfully.

8.7. Puzzle Game

Puzzle games are a diverse and engaging genre that challenge players’ problem-solving skills, logic, and creativity. They come in various forms and can be enjoyed by players of all ages.

For this project, we will be designing and implementing a simple puzzle game using Allegro 5 in C++: a basic sliding-puzzle game where the player needs to arrange tiles in the correct order.

8.7.1. Game Design

Game Concept

  • Genre: Puzzle game (sliding puzzle).
  • Objective: The player arranges shuffled tiles to form a complete image or sequence.
  • Gameplay Mechanics:
    • • The player uses the keyboard arrow keys to indicate which adjacent tile to move into the empty black space. For example, right arrow for the tile on the right side of the empty space.
    • • The empty space swaps positions accordingly.
    • • Gameplay is complete when the numbered tiles are in order numerically.

Core Components

  • Puzzle Grid: The structured layout that holds all tiles
    • Attributes: Grid dimensions (e.g., 4 × 4), cell size, grid coordinates
    • Function: Defines valid tile locations and constrains movement rules
  • Tiles: Individual movable puzzle pieces
    • Attributes: Current grid position, correct grid position, identifier (number or image fragment), sprite
    • Movement: Tiles may only move into the adjacent empty space
  • Empty Space: The unoccupied grid cell that enables movement
    • Attributes: Grid position (row, column)
    • Function: Allows tiles to slide; moves when a tile enters its position
  • Shuffling System: Initial randomization logic
    • Function: Rearranges tiles into a solvable starting configuration
    • Rule: Only valid sliding moves are used to preserve solvability
  • Puzzle Completion Detection: Determines when the puzzle is solved
    • Condition: All tiles and the empty space are in their correct positions
    • Effect: Ends interaction and displays a completion message

Key Techniques and Features to Have

Sliding-tile puzzles depend on clear grid logic, constrained movement, and precise rendering. The following features form a clean and reliable baseline for implementing a sliding puzzle using Allegro 5:

  1. 1. Grid Representation and Tile Management

    The puzzle board should be represented as a 2D grid where each cell contains either a tile or the empty space. Each tile should track its current position, correct position, and identifier and be drawn based on its grid coordinates.

  2. 2. Shuffling with Valid Puzzle States

    The initial board state should be randomized while remaining solvable. A common approach is to shuffle the puzzle by performing many legal moves of the empty space rather than random permutations.

  3. 3. Movement Logic and Sliding Mechanics

    Only tiles adjacent to the empty space should be allowed to move. A valid move swaps the tile and empty space positions and updates the grid consistently. Movement should feel immediate and responsive.

  4. 4. Rendering and Visual Feedback

    Tiles should be drawn at positions derived from grid coordinates and tile size. The empty cell should be visually distinct, and tiles should clearly display either numbers or image fragments for easy recognition.

  5. 5. Win Condition and Completion Detection

    After each move, the game should check whether all tiles (and the empty space) are in their correct positions. Upon completion, the game should display a clear success message and prevent further moves unless reset.

  6. 6. Input Handling Using Allegro

    Keyboard or mouse input should trigger tile movement and reset actions. Input should be ignored once the puzzle is solved, except for restart commands.

  7. 7. Game Loop Structure and Smooth Updates

    An Allegro timer (e.g., 60 fps) should drive consistent updates and rendering. Each frame should clear the screen, draw tiles and UI elements, and flip the display cleanly.

  8. 8. UI Elements and Player Guidance

    The interface should clearly present tile values or image fragments and may optionally include a move counter, timer, or brief instructions. Fonts and messages should remain readable and unobtrusive.

8.7.2. Implementation

Key Operations in the Game

  1. 1. Initialization
    • • Initialize the Allegro library and its components (image add-on, keyboard input).
    • • Create the game window with specified dimensions.
  2. 2. Tile Creation
    • • Each tile has properties like its current position (x, y), its correct position (correctX, correctY), and its image.
    • • Generate tiles and assign them their correct positions. Create a bitmap for each tile and fill it with a random colour.
  3. 3. Shuffling
    • • Shuffle the tiles randomly to create the initial puzzle state. Optional: Ensure the puzzle is solvable.
  4. 4. Rendering
    • • Draw each tile at its current position on the screen. This is done in the game loop to continuously update the display.
  5. 5. Input Handling
    • • Capture user input to move the tiles. For example, use arrow keys to move the empty space and slide adjacent tiles into it.
  6. 6. Game Logic
    • • Implement logic to move tiles into the empty space when the player presses a key.
    • • Check if the tiles are in the correct order to determine if the player has solved the puzzle.
  7. 7. Game Loop
    • • The game runs in a loop where it continuously checks for user input, updates the game state, and renders the tiles.

Structure of the Game Program

  1. 1. Initialization
    • • Initializes Allegro and its image add-on and installs the keyboard.
  2. 2. Tile Creation
    • • Creates a grid of tiles, each assigned a random colour.
  3. 3. Shuffling
    • • Shuffles the tiles to create the puzzle.
  4. 4. Drawing
    • • Draws the tiles on the screen.
  5. 5. Main Loop of the Game
  6. 6. Shutdown
    • • Clean release of Allegro resources

Coding the Game

puzzle_game.cpp

#include <allegro5/allegro.h>

#include <allegro5/allegro_image.h>

#include <allegro5/allegro_primitives.h>

#include <allegro5/allegro_font.h>

#include <allegro5/allegro_ttf.h>

#include <iostream>

#include <vector>

#include <algorithm>

#include <ctime>

#include <cstdlib>

#include <string>

//——Config——

const int GRID_SIZE   = 4;

const int TILE_SIZE   = 150;                 // 4 × 4 * 150 = 600px square

const int SCREEN_W    = GRID_SIZE * TILE_SIZE;

const int SCREEN_H    = GRID_SIZE * TILE_SIZE;

// A simple colored tile with a number label (1..15). Empty has no Tile.

struct Tile {

    int row, col;                // current grid location

    int correctRow, correctCol;  // goal location

    int label;                   // 1..15 (goal order index); not used for empty

    ALLEGRO_BITMAP* image;       // visual for the tile

};

std::vector<Tile> tiles;     // all tiles except the empty space

int emptyRow = GRID_SIZE—1;

int emptyCol = GRID_SIZE—1;

bool solved = false;

ALLEGRO_FONT* g_font_numbers = nullptr;

ALLEGRO_FONT* g_font_ui = nullptr;

static inline bool in_bounds(int r, int c) {

    return r >= 0 && r < GRID_SIZE && c >= 0 && c < GRID_SIZE;

}

// Find the index of the tile currently occupying (r,c); returns—1 if it's empty.

int find_tile_at(int r, int c) {

    for (size_t i = 0; i < tiles.size(); ++i) {

        if (tiles[i].row == r && tiles[i].col == c) return static_cast<int>(i);

    }

    return—1; // empty cell

}

// Initialization

bool init() {

    if (!al_init()) { std::cerr << "al_init failed\n"; return false; }

    if (!al_init_image_addon()) { std::cerr << "image addon init failed\n"; return false; }

    if (!al_install_keyboard()) { std::cerr << "keyboard install failed\n"; return false; }

    if (!al_init_primitives_addon()) { std::cerr << "primitives addon init failed\n"; return false; }

    al_init_font_addon();

    al_init_ttf_addon();

    std::srand(static_cast<unsigned>(std::time(nullptr)));

    return true;

}

void draw_number_centered(ALLEGRO_BITMAP* bmp, ALLEGRO_FONT* font, int n, ALLEGRO_COLOR color) {

    ALLEGRO_BITMAP* back = al_get_target_bitmap();

    al_set_target_bitmap(bmp);

    std::string s = std::to_string(n);

    int w = al_get_text_width(font, s.c_str());

    int h = al_get_font_line_height(font);

    int x = TILE_SIZE/2—w/2;

    int y = TILE_SIZE/2—h/2;

    al_draw_text(font, color, x, y, 0, s.c_str());

    al_set_target_bitmap(back);

}

// Tile Creation

void create_tiles() {

    tiles.clear();

    emptyRow = GRID_SIZE—1;

    emptyCol = GRID_SIZE—1;

    solved = false;

    // create 15 tiles in goal order, positioned at their correct locations

    int label = 1;

    for (int r = 0; r < GRID_SIZE; ++r) {

        for (int c = 0; c < GRID_SIZE; ++c) {

            if (r == emptyRow && c == emptyCol) continue; // skip empty 16th cell

            Tile t;

            t.row = r; t.col = c;

            t.correctRow = r; t.correctCol = c;

            t.label = label++;

            t.image = al_create_bitmap(TILE_SIZE, TILE_SIZE);

            if (!t.image) { std::cerr << "Failed to create tile bitmap\n"; continue; }

            ALLEGRO_BITMAP* back = al_get_target_bitmap();

            al_set_target_bitmap(t.image);

            // pleasant pseudo-random color (deterministic per label for consistency):

            std::srand(t.label * 2654435761u); // mix a bit

            ALLEGRO_COLOR fill = al_map_rgb(70 + std::rand()%160, 70 + std::rand()%160, 70 + std::rand()%160);

            std::srand(static_cast<unsigned>(std::time(nullptr))); // restore randomness for shuffle later

            al_clear_to_color(fill);

            // border

            al_draw_filled_rectangle(0, 0, TILE_SIZE, 6, al_map_rgb(0,0,0));

            al_draw_filled_rectangle(0, TILE_SIZE-6, TILE_SIZE, TILE_SIZE, al_map_rgb(0,0,0));

            al_draw_filled_rectangle(0, 0, 6, TILE_SIZE, al_map_rgb(0,0,0));

            al_draw_filled_rectangle(TILE_SIZE-6, 0, TILE_SIZE, TILE_SIZE, al_map_rgb(0,0,0));

            // number

            draw_number_centered(t.image, g_font_numbers, t.label, al_map_rgb(255,255,255));

            al_set_target_bitmap(back);

            tiles.push_back(t);

        }

    }

}

// Perform one legal move by sliding a neighbor into the empty

bool do_random_move() {

    int candidates[4][2] = {

        { emptyRow-1, emptyCol }, // tile above empty moves down

        { emptyRow+1, emptyCol }, // tile below empty moves up

        { emptyRow, emptyCol-1 }, // left of empty moves right

        { emptyRow, emptyCol+1 }  // right of empty moves left

    };

    std::vector<std::pair<int,int>> opts;

    for (auto& cand : candidates) {

        if (in_bounds(cand[0], cand[1])) opts.emplace_back(cand[0], cand[1]);

    }

    if (opts.empty()) return false;

    auto pick = opts[std::rand() % opts.size()];

    int idx = find_tile_at(pick.first, pick.second);

    if (idx >= 0) {

        tiles[idx].row = emptyRow;

        tiles[idx].col = emptyCol;

        emptyRow = pick.first;

        emptyCol = pick.second;

        return true;

    }

    return false;

}

// Shuffling

void shuffle_tiles(int moves = 400) {

    for (int i = 0; i < moves; ++i) {

        (void)do_random_move();

    }

}

// Check if every tile is back in its correct row/col and empty at bottom-right

bool is_solved() {

    if (!(emptyRow == GRID_SIZE-1 && emptyCol == GRID_SIZE-1)) return false;

    for (const auto& t : tiles) {

        if (t.row != t.correctRow || t.col != t.correctCol) return false;

    }

    return true;

}

// Drawing

void draw_tiles() {

    al_clear_to_color(al_map_rgb(30, 30, 30));

    for (const auto& t : tiles) {

        int x = t.col * TILE_SIZE;

        int y = t.row * TILE_SIZE;

        al_draw_bitmap(t.image, x, y, 0);

    }

    // empty cell outline

    int ex = emptyCol * TILE_SIZE;

    int ey = emptyRow * TILE_SIZE;

    al_draw_rectangle(ex+4, ey+4, ex + TILE_SIZE—4, ey + TILE_SIZE—4, al_map_rgb(255, 255, 255), 2);

    if (solved) {

        const char* msg = "Solved!";

        int w = al_get_text_width(g_font_ui, msg);

        int h = al_get_font_line_height(g_font_ui);

        int x = SCREEN_W/2—w/2;

        int y = SCREEN_H/2—h/2;

        al_draw_filled_rectangle(x-16, y-10, x+w+16, y+h+10, al_map_rgba(0,0,0,200));

        al_draw_text(g_font_ui, al_map_rgb(255, 255, 255), SCREEN_W/2, SCREEN_H/2, ALLEGRO_ALIGN_CENTER, msg);

    }

}

// Input handling: slide a tile into the empty cell

void handle_keypress(int keycode) {

    if (solved) return; // ignore input after solved (optional)

    int srcR = emptyRow, srcC = emptyCol;

    if (keycode == ALLEGRO_KEY_UP)    srcR = emptyRow + 1; // tile below empty moves up

    if (keycode == ALLEGRO_KEY_DOWN)  srcR = emptyRow—1; // tile above empty moves down

    if (keycode == ALLEGRO_KEY_LEFT)  srcC = emptyCol + 1; // tile right of empty moves left

    if (keycode == ALLEGRO_KEY_RIGHT) srcC = emptyCol—1; // tile left of empty moves right

    if (!in_bounds(srcR, srcC)) return;

    int idx = find_tile_at(srcR, srcC);

    if (idx < 0) return;

    tiles[idx].row = emptyRow;

    tiles[idx].col = emptyCol;

    emptyRow = srcR;

    emptyCol = srcC;

    solved = is_solved();

}

// Main loop

int main() {

    if (!init()) return—1;

    ALLEGRO_DISPLAY* display = al_create_display(SCREEN_W, SCREEN_H);

    if (!display) { std::cerr << "display creation failed\n"; return—1; }

    // Load fonts (numbers/UI). If TTF load fails, fall back to builtin.

    g_font_numbers = al_load_ttf_font("arial.ttf", 64, 0);

    if (!g_font_numbers) g_font_numbers = al_create_builtin_font();

    g_font_ui = al_load_ttf_font("arial.ttf", 64, 0);

    if (!g_font_ui) g_font_ui = al_create_builtin_font();

    ALLEGRO_EVENT_QUEUE* queue = al_create_event_queue();

    ALLEGRO_TIMER* timer = al_create_timer(1.0 / 60.0);

    if (!queue || !timer) {

        std::cerr << "event queue or timer creation failed\n";

        if (timer) al_destroy_timer(timer);

        if (queue) al_destroy_event_queue(queue);

        if (g_font_numbers) al_destroy_font(g_font_numbers);

        if (g_font_ui) al_destroy_font(g_font_ui);

        al_destroy_display(display);

        return—1;

    }

    al_register_event_source(queue, al_get_display_event_source(display));

    al_register_event_source(queue, al_get_keyboard_event_source());

    al_register_event_source(queue, al_get_timer_event_source(timer));

    create_tiles();

    shuffle_tiles();

    al_start_timer(timer);

    bool running = true;

    while (running) {

        ALLEGRO_EVENT ev;

        al_wait_for_event(queue, &ev);

        switch (ev.type) {

            case ALLEGRO_EVENT_DISPLAY_CLOSE:

                running = false;

                break;

            case ALLEGRO_EVENT_KEY_DOWN:

                if (ev.keyboard.keycode == ALLEGRO_KEY_R) {

                    // quick reset/shuffle

                    create_tiles();

                    shuffle_tiles();

                } else {

                    handle_keypress(ev.keyboard.keycode);

                }

                break;

            case ALLEGRO_EVENT_TIMER:

                draw_tiles();

                al_flip_display();

                break;

        }

    }

    // Finishing up

    for (auto& t : tiles) {

        if (t.image) al_destroy_bitmap(t.image);

    }

    if (g_font_numbers) al_destroy_font(g_font_numbers);

    if (g_font_ui) al_destroy_font(g_font_ui);

    al_destroy_timer(timer);

    al_destroy_event_queue(queue);

    al_destroy_display(display);

    return 0;

}

Tasks for Completion and Improvement

  1. 1. Scoring System: Add visual feedback for scoring, such as displaying the score on the screen in regard to the number of moves it took to solve the puzzle.
  2. 2. Sound Effects: Add sound effects when moving a tile.
  3. 3. Difficulty Levels: Introduce different difficulty levels by varying the number of tiles to move—for example, change the matrix size to 3 × 3, 4 × 5, and so on.

Exercises, Homework Questions, and Projects

Exercises

  1. 1. Grid Representation and Tile Location

    Explain how the two-dimensional grid representation simplifies both the movement logic and rendering of puzzle tiles. Why is it useful to separate a tile’s current position from its correct position?

  2. 2. Movement Constraints

    Describe why only tiles adjacent to the empty space are allowed to move. How does enforcing this rule guarantee valid puzzle interactions?

  3. 3. Solvability Considerations

    Explain why random permutations of tiles may result in unsolvable puzzles. How does shuffling the puzzle by performing many legal moves avoid this problem?

  4. 4. Win Condition Detection

    Review the is_solved() function. Why is it important to check both the tile positions and the location of the empty space?

Homework Questions

  1. 1. User Experience in Puzzle Games

    Discuss how clear visual feedback (such as tile borders, numbered labels, and empty-space outlines) affects the player’s ability to understand and solve the puzzle.

  2. 2. Keyboard vs. Mouse Control

    Compare keyboard-based tile movement with mouse-based interaction (clicking tiles to slide them). What are the advantages and disadvantages of each input method?

  3. 3. Difficulty and Cognitive Load

    How does increasing the grid size affect the puzzle’s difficulty? At what point might a larger grid become frustrating rather than challenging?

  4. 4. Puzzle Games and Replayability

    Why are simple puzzle games often still highly replayable? Discuss the role of randomness, difficulty scaling, and performance tracking (time or moves).

Programming Projects

  1. 1. Move Counter and Scoring System (Core Project)

    Add a move counter that increments each time a tile is slid. Display the number of moves on the screen and show the final move count when the puzzle is solved.

  2. 2. Sound Effects Integration

    Add a sound effect that plays whenever a tile moves. Optionally include a different sound for invalid moves or when the puzzle is completed.

  3. 3. Difficulty Levels

    Extend the game to support different grid sizes, such as 3 × 3, 4 × 4, or 5 × 5. Allow the player to select the difficulty level before starting or restarting the puzzle.

  4. 4. Timer-Based Challenge Mode (Advanced)

    Add a timer that tracks how long the player takes to solve the puzzle. Display the elapsed time and use it as an alternative scoring metric alongside the move counter.

  5. 5. Image-Based Puzzle (Advanced)

    Replace the coloured tiles with fragments of an image. Load a single image, divide it into tile-sized sections, and render each fragment on the corresponding tile.

  6. 6. Animated Tile Sliding (Optional)

    Add smooth animations so tiles slide into the empty space rather than snapping instantly. Interpolate tile positions over a short duration to improve visual polish.

Capstone Project (Optional)

Design a polished sliding-puzzle game that includes adjustable difficulty, scoring by moves and time, sound effects, and graphical enhancements. Include a menu screen for selecting puzzle size and restarting the game. Submit a short reflection describing how changes in grid size, feedback, and presentation influence the player’s problem-solving experience.

8.8. Role-Playing Game

Role-playing games (RPGs) are a genre where players assume the roles of characters in a fictional setting. Players take control of these characters and guide them through various quests, battles, and storylines. Creating a role-playing game (RPG) with Allegro 5 in C++ is an exciting project! RPGs are complex in terms of the number of elements that need to be considered; there are often multiple enemy types, weapons, locations, quests, treasures, and so forth. As such, here, we’ll cover only the outline of a possible basic design and some implementation steps for a simple RPG—a basic framework that you can expand upon.

8.8.1. Game Design

Game Concept

  • Genre: Role-playing game (RPG).
  • Objective: The player controls a character who explores the game world, interacts with NPCs (nonplayer characters), and battles enemies to complete quests and progress through the story.

Core Components

  • Player Character: The main controllable protagonist
    • Attributes: Position (x, y), HP, attack, defense, level, experience points (XP)
    • Movement: Controlled via keyboard input (e.g., WASD or arrow keys)
  • Enemies: Hostile entities that challenge the player
    • Attributes: Position, HP, combat stats, sprite
    • Behavior: Move and attack using simple AI patterns
  • Nonplayer Characters (NPCs): Interactive characters within the world
    • Attributes: Dialogue state, quest flags, position
    • Function: Provide information, quests, items, or services
  • Quests and Story System: Tracks narrative progress
    • Attributes: Quest states (inactive, active, completed), quest stages
    • Function: Unlocks content and guides player progression
  • Inventory System: Manages collected items
    • Attributes: Item slots, item IDs, quantities
    • Function: Supports consumables, equipment, and key items
  • Combat System: Handles battles with enemies
    • Type: Turn-based combat
    • Function: Resolves attacks, abilities, damage, and rewards

Key Techniques and Features to Have

Designing a role-playing game (RPG) requires coordinating exploration, interaction, combat, progression, and presentation into a cohesive system. The following features provide a clear, beginner-friendly foundation for an extensible RPG built with Allegro 5:

  1. 1. World Navigation and Collision

    The game world should use tile-based movement with clearly defined walkable and blocked areas. Collision layers should distinguish static terrain from interactive elements such as doors, NPCs, and triggers. The camera should follow the player and clamp to map boundaries.

  2. 2. Player Controller and Stats

    Player movement should be driven by keyboard input, with a dedicated interaction key. Core statistics such as HP, ATK, DEF, level, and XP should be tracked and updated throughout play, with hooks for future status effects.

  3. 3. NPCs, Dialogue, and Choices

    NPC interaction should trigger dialogue based on proximity or facing direction. Dialogue systems should support branching choices and simple conditions, and dialogue outcomes should connect to quests, items, or stat changes.

  4. 4. Quests and World Triggers

    A quest log should track quest states and progression stages. World triggers—such as entering areas, activating switches, or defeating enemies—should advance quests and control access to content based on progress or inventory.

  5. 5. Inventory, Items, and Equipment

    The inventory should support consumables, equipment, and key items. Consumables apply direct effects, while equipment optionally modifies player stats. Item usage should tie directly into puzzle-solving and progression.

  6. 6. Combat System (Turn-Based Baseline)

    Combat should use a clear turn order and a simple damage model (e.g., ATK − DEF). The system should support basic attacks, limited abilities, and defined win/loss outcomes, awarding XP, currency, and loot on victory.

  7. 7. Progression and Balance

    Player progression should follow a tunable XP curve with stat increases on level-up. Enemies should use drop tables for rewards, and difficulty should scale gradually through enemy stats and encounter design.

  8. 8. Rendering and UI

    Rendering should follow a consistent draw order: world, entities, overlays, then UI. The HUD should display key player information such as HP, level, XP, currency, and active objectives. Dialogue UI should be readable and choice-aware.

  9. 9. Audio and Feedback

    Sound effects and background music should reinforce actions, locations, and combat. Key events—attacks, healing, level-ups—should provide clear audio or visual feedback, using subtle effects for emphasis.

  10. 10. Data-Driven and Modular Structure

    Maps, items, enemies, dialogue, and quests should be externalized into data files for easy iteration. The codebase should be modular, separating systems such as Map, Combat, Dialogue, and Inventory, with basic save/load support for persistent play.

8.8.2. Implementation

Key Operations in the Game

The following operations constitute the core runtime flow of a simple Allegro-based RPG. They map directly to the systems listed above and can be implemented incrementally.

  1. 1. Input Collection and Intent Mapping
    • • Read Allegro keyboard events → set intent flags (move up/down/left/right, interact, open inventory, confirm/cancel).
    • • Debounce menu inputs and prevent repeats inside a single frame where needed.
  2. 2. Player Movement and Collision Resolution
    • • Compute candidate position from input; test against tile collision mask.
    • • Accept move if walkable; otherwise, slide or cancel.
    • • Update camera to follow the player and clamp to map edges.
  3. 3. Interaction Detection
    • • On interact key press, raycast/check the front tile for NPC, sign, door, or chest.
    • • If found, open the correct UI: dialogue box, treasure, or door transition.
  4. 4. Dialogue Execution
    • • Load the dialogue node, render text and choices, and accept selection input.
    • • Apply effects (start/advance quest, grant item, play SFX) when a node completes.
  5. 5. Quest State Transitions
    • • When a trigger fires (e.g., entering a room, defeating an enemy, acquiring an item), update the quest log.
    • • Refresh the on-screen objective text and enable/disable relevant world gates.
  6. 6. Inventory Operations
    • • Open inventory UI; navigate slots, use items, and update stats (e.g., heal HP).
    • • Apply item rules (consumable vs. equipment vs. key) and enforce stack counts.
  7. 7. Encounter Handling (Overworld → Battle)
    • • Detect battle start (random encounter tile, scripted boss trigger, or touching an enemy).
    • • Push a combat state and pause overworld updates until combat ends.
  8. 8. Combat Turn Cycle
    • • Establish turn order; in the player’s turn, accept action selection (attack, skill, item).
    • • Compute damage/heal with the current formula and update HP/MP.
    • • Check victory/defeat; on victory, award XP/loot and return to overworld.
  9. 9. Progression Updates
    • • Add XP; if threshold reached, level up, raise stats, and show a level-up banner/SFX.
    • • Optional: Unlock new abilities at specific levels.
  10. 10. Map Transitions and Scene Management
    • • On door/warp tiles, save the player’s exit point, switch the current map, and place the player at the entry point.
    • • Reload map layers, entities, and background music as needed.
  11. 11. Rendering Pipeline per Frame
    • • Clear → draw tile map (visible region) → draw entities → draw overlays (e.g., highlights) → draw HUD/Dialog/menus → flip display.
    • • Respect draw order for clarity (UI on top).
  12. 12. Audio Triggers
    • • Play loops for BGM; trigger one-shots for actions (confirm, attack, item use, level up).
    • • Fade or switch tracks on map/scene changes.
  13. 13. Saving and Loading
    • • Serialize player stats, position, inventory, quest flags, and map identifier to a save file.
    • • On load, restore state and assets, then resume at the saved location.
  14. 14. Performance and Housekeeping
    • • Avoid per-frame allocations in hot paths; reuse containers/buffers.
    • • Destroy Allegro resources (bitmaps, fonts, audio) on shutdown; guard against nulls.

Structure of the Game Program

  1. 1. Initialization
    • • Initializes Allegro and its add-ons (image, font, audio, primitives, etc.) and installs input devices.
  2. 2. Resource Loading
    • • Loads sprites, tilesets, fonts, audio, and other assets.
  3. 3. Entity Creation
    • • Creates the player, NPCs, and enemies with attributes and positions.
  4. 4. Map/Level System
    • • Loads tile-based maps, manages environment layout, collision layers, and triggers for interactions (doors, transitions, scripted events).
  5. 5. Drawing
    • • Renders the player, NPCs, enemies, environment, and UI elements on the screen.
  6. 6. Input Handling
    • • Captures keyboard (and optional mouse/controller) input for movement, menus, combat, and interactions.
  7. 7. Game Loop
    • • Runs the main cycle (input → update → draw) to drive the game.
  8. 8. Dialogue System
    • • Displays NPC conversations with branching text, choices, and conditional outcomes (e.g., starting quests, giving items).
  9. 9. Quest Tracking
    • • Maintains active and completed quests, quest stages, and conditions for advancement. Connects to dialogue and world triggers.
  10. 10. Inventory System
    • • Allows managing items, equipment, and consumables. Includes UI for viewing and using items.
  11. 11. Combat System
    • • Handles battle flow (turn-based or real time), damage formulas, health management, and special abilities.
  12. 12. Progression System
    • • Tracks experience points (XP), leveling up, and stat growth for the player and possibly NPC allies.

Coding the Game

// ================ 1) Initialization ================

struct App {

    ALLEGRO_DISPLAY* display = nullptr;

    ALLEGRO_EVENT_QUEUE* queue = nullptr;

    ALLEGRO_TIMER* timer = nullptr;

    bool running = true;

    bool init(int w=1280, int h=720, int fps=60) {

        if (!al_init()) return false;

        al_install_keyboard();

        al_init_image_addon();

        al_init_font_addon();

        al_init_ttf_addon();

        al_install_audio();

        al_init_primitives_addon();

        display = al_create_display(w, h);

        timer = al_create_timer(1.0 / fps);

        queue = al_create_event_queue();

        al_register_event_source(queue, al_get_display_event_source(display));

        al_register_event_source(queue, al_get_timer_event_source(timer));

        al_register_event_source(queue, al_get_keyboard_event_source());

        al_start_timer(timer);

        return display && timer && queue;

    }

    void shutdown() {

        if (queue) al_destroy_event_queue(queue);

        if (timer) al_destroy_timer(timer);

        if (display) al_destroy_display(display);

        al_uninstall_audio();

    }

};

// ================ 2) Resource Loading ================

struct Assets {

    std::unordered_map<std::string, ALLEGRO_BITMAP*> bitmaps;

    std::unordered_map<std::string, ALLEGRO_FONT*> fonts;

    // SFX/BGM maps omitted for brevity.

    ALLEGRO_BITMAP* bmp(const std::string& id) { return bitmaps[id]; }

    ALLEGRO_FONT* font(const std::string& id) { return fonts[id]; }

    bool load() {

        bitmaps["player"] = al_load_bitmap("assets/player.png");

        bitmaps["tiles"]  = al_load_bitmap("assets/tiles.png");

        fonts["ui"]       = al_load_ttf_font("assets/ui.ttf", 18, 0);

        // Check for nulls in real code.

        return true;

    }

    void unload() {

        for (auto& [k,v] : bitmaps) if (v) al_destroy_bitmap(v);

        for (auto& [k,v] : fonts) if (v) al_destroy_font(v);

    }

};

// ================ 3) Entity Creation ================

struct Stats { int hp=10, mp=0, atk=2, def=1, lvl=1, xp=0; };

struct Entity {

    float x=0, y=0;

    ALLEGRO_BITMAP* sprite=nullptr;

    Stats stats;

    bool solid=true;

};

struct WorldEntities {

    Entity player;

    std::vector<Entity> enemies;

    std::vector<Entity> npcs;

    void create(Assets& A) {

        player.sprite = A.bmp("player");

        player.x = 100; player.y = 100;

        enemies.push_back(Entity{300, 180, A.bmp("enemy_slime"), Stats{6,0,1,0,1,0}, true});

        // Add NPCs similarly . . .

    }

};

// =============== 4) Map / Level System ===============

struct TileMap {

    int w=0, h=0, tile=32;

    std::vector<int> tiles;           // layer 0 visual

    std::vector<uint8_t> solidMask;   // 1 = blocked

    ALLEGRO_BITMAP* tileset=nullptr;

    bool load(const std::string& path, Assets& A) {

        // Minimal: pretend a single small room.

        w=40; h=25; tile=32;

        tiles.assign(w*h, 1);          // tile id 1

        solidMask.assign(w*h, 0);      // all walkable for now

        tileset = A.bmp("tiles");

        return true;

    }

    bool blocked(float px, float py) const {

        int cx = int(px) / tile, cy = int(py) / tile;

        if (cx<0||cy<0||cx>=w||cy>=h) return true;

        return solidMask[cy*w+cx] != 0;

    }

    void draw(float camx, float camy) const {

        // Ultra-minimal: draw visible region (no culling shown).

        for (int y=0; y<h; ++y)

            for (int x=0; x<w; ++x) {

                int id = tiles[y*w+x];

                // Assume id maps to tileset cell (0,0). Replace with atlas logic later.

                al_draw_bitmap_region(tileset, 0,0, tile,tile, x*tile-camx, y*tile-camy, 0);

            }

    }

};

// ==================== 5) Drawing ====================

struct Renderer {

    void drawWorld(const TileMap& map, const WorldEntities& E, float camx, float camy) {

        map.draw(camx, camy);

        if (E.player.sprite)

            al_draw_bitmap(E.player.sprite, E.player.x—camx, E.player.y—camy, 0);

        for (auto& e : E.enemies)

            if (e.sprite) al_draw_bitmap(e.sprite, e.x—camx, e.y—camy, 0);

        // NPCs, particles, etc.

    }

    void drawUI(Assets& A, const std::string& msg) {

        al_draw_text(A.font("ui"), al_map_rgb(255,255,255), 16, 16, 0, msg.c_str());

    }

};

// ================= 6) Input Handling =================

struct Input {

    bool up=false, down=false, left=false, right=false, interact=false, inventory=false, quit=false;

    void handle(const ALLEGRO_EVENT& ev) {

        if (ev.type == ALLEGRO_EVENT_KEY_DOWN || ev.type == ALLEGRO_EVENT_KEY_UP) {

            bool downEv = (ev.type == ALLEGRO_EVENT_KEY_DOWN);

            switch (ev.keyboard.keycode) {

                case ALLEGRO_KEY_W: case ALLEGRO_KEY_UP:    up = downEv; break;

                case ALLEGRO_KEY_S: case ALLEGRO_KEY_DOWN:  down = downEv; break;

                case ALLEGRO_KEY_A: case ALLEGRO_KEY_LEFT:  left = downEv; break;

                case ALLEGRO_KEY_D: case ALLEGRO_KEY_RIGHT: right = downEv; break;

                case ALLEGRO_KEY_E:                          interact = downEv; break;

                case ALLEGRO_KEY_I:                          inventory = downEv; break;

                case ALLEGRO_KEY_ESCAPE:                     quit = downEv; break;

            }

        }

    }

};

// ============== 7) Game Loop (State-lite) =============

struct Game {

    App app;

    Assets assets;

    Renderer renderer;

    Input input;

    TileMap map;

    WorldEntities ents;

    float camx=0, camy=0;

    bool boot() {

        if (!app.init()) return false;

        assets.load();

        assets.bitmaps["enemy_slime"] = al_load_bitmap("assets/enemy_slime.png");

        map.load("assets/maps/start.json", assets);

        ents.create(assets);

        return true;

    }

    void update(double dt) {

        // Simple movement with collision

        float speed = 120.0f;

        float dx = (input.right—input.left) * speed * dt;

        float dy = (input.down—input.up  ) * speed * dt;

        float nx = ents.player.x + dx, ny = ents.player.y + dy;

        if (!map.blocked(nx, ents.player.y)) ents.player.x = nx;

        if (!map.blocked(ents.player.x, ny)) ents.player.y = ny;

        // Camera follows

        camx = ents.player.x—640; camy = ents.player.y—360;

    }

    void draw() {

        al_clear_to_color(al_map_rgb(0,0,0));

        renderer.drawWorld(map, ents, camx, camy);

        renderer.drawUI(assets, "HP: " + std::to_string(ents.player.stats.hp));

        al_flip_display();

    }

    void run() {

        while (app.running) {

            ALLEGRO_EVENT ev; al_wait_for_event(app.queue, &ev);

            if (ev.type == ALLEGRO_EVENT_DISPLAY_CLOSE) app.running = false;

            if (ev.type == ALLEGRO_EVENT_TIMER) { update(1.0/60.0); draw(); }

            if (ev.type == ALLEGRO_EVENT_KEY_DOWN || ev.type == ALLEGRO_EVENT_KEY_UP) {

                input.handle(ev);

                if (input.quit) app.running = false;

            }

        }

    }

};

// ================== 8) Dialogue System ================

struct DialogueChoice { std::string text; std::string nextId; };

struct DialogueNode {

    std::string id;

    std::string text;

    std::vector<DialogueChoice> choices; // empty => press to continue

    // Minimal: conditions/effects omitted; add callbacks later.

};

struct DialogueDB {

    std::unordered_map<std::string, DialogueNode> nodes;

    const DialogueNode* get(const std::string& id) const {

        auto it = nodes.find(id); return (it==nodes.end())? nullptr : &it->second;

    }

};

struct DialogueRunner {

    const DialogueDB* db=nullptr;

    const DialogueNode* cur=nullptr;

    bool active=false;

    void start(const DialogueDB* d, const std::string& id) { db=d; cur=d->get(id); active = (cur!=nullptr); }

    void choose(size_t idx) {

        if (!active || !cur) return;

        if (idx < cur->choices.size()) {

            cur = db->get(cur->choices[idx].nextId);

            active = (cur != nullptr);

        } else { active = false; }

    }

};

// ================== 9) Quest Tracking =================

enum class QuestState { Inactive, Active, Completed, Failed };

struct Quest {

    std::string id;

    int stage=0;            // 0..N

    QuestState state=QuestState::Inactive;

};

struct QuestLog {

    std::unordered_map<std::string, Quest> quests;

    void start(const std::string& id) { quests[id] = Quest{id,0,QuestState::Active}; }

    void advance(const std::string& id) { auto& q=quests[id]; if (q.state==QuestState::Active) ++q.stage; }

    void complete(const std::string& id) { quests[id].state = QuestState::Completed; }

    bool isActive(const std::string& id) const {

        auto it=quests.find(id); return it!=quests.end() && it->second.state==QuestState::Active;

    }

};

// =============== 10) Inventory System ===============

enum class ItemType { Consumable, Equipment, Key };

struct ItemDef {

    int id; std::string name; ItemType type; int power=0; // power = heal amount or atk bonus, etc.

};

struct ItemDB {

    std::unordered_map<int, ItemDef> defs;

    const ItemDef& get(int id) const { return defs.at(id); }

};

struct Inventory {

    struct Slot { int itemId=-1; int count=0; };

    std::vector<Slot> slots = std::vector<Slot>(24);

    bool add(int itemId, int n=1) {

        for (auto& s: slots) if (s.itemId==itemId) { s.count+=n; return true; }

        for (auto& s: slots) if (s.itemId==-1) { s.itemId=itemId; s.count=n; return true; }

        return false;

    }

    bool use(int idx, Entity& target, const ItemDB& DB) {

        if (idx<0 || idx>= (int)slots.size()) return false;

        auto& s = slots[idx]; if (s.itemId<0 || s.count<=0) return false;

        const ItemDef& def = DB.get(s.itemId);

        if (def.type == ItemType::Consumable) {

            target.stats.hp = std::min(target.stats.hp + def.power, 999);

            if (—s.count==0) s.itemId=-1;

            return true;

        }

        // Equipment/Key handling omitted in minimal sketch.

        return false;

    }

};

// ======== 11) Combat System (Turn-based minimal) ======

struct Combatant { Entity* ref=nullptr; bool enemy=false; };

struct Combat {

    std::vector<Combatant> order;

    size_t turnIndex=0;

    bool active=false;

    void start(Entity& player, std::vector<Entity>& enemies) {

        order.clear();

        order.push_back({&player,false});

        for (auto& e: enemies) order.push_back({&e,true});

        turnIndex = 0; active = true;

    }

    void playerAttack(int targetIdx) {

        if (!active) return;

        auto& you = *order[0].ref;

        auto& tgt = *order[targetIdx].ref;

        int dmg = std::max(1, you.stats.atk—tgt.stats.def);

        tgt.stats.hp—= dmg;

        if (tgt.stats.hp <= 0) {/* mark defeated */}

        endTurn();

    }

    void enemyTurnAI() {

        auto& foe = *order[turnIndex].ref;

        auto& you = *order[0].ref;

        int dmg = std::max(1, foe.stats.atk—you.stats.def);

        you.stats.hp—= dmg;

        endTurn();

    }

    void endTurn() {

        turnIndex = (turnIndex + 1) % order.size();

        // End combat on victory/defeat checks (omitted here).

    }

};

// ================ 12) Progression System =============

struct Progression {

    // Simple linear XP curve for demo.

    int xpToNext(int lvl) const { return 10 + lvl*10; }

    bool grantXP(Entity& e, int xp) {

        e.stats.xp += xp;

        bool leveled=false;

        while (e.stats.xp >= xpToNext(e.stats.lvl)) {

            e.stats.xp—= xpToNext(e.stats.lvl);

            ++e.stats.lvl; leveled=true;

            e.stats.hp += 3; e.stats.atk += 1; e.stats.def += 1;

        }

        return leveled;

    }

};

// =================== Wiring it together ===============

int main() {

    Game game;

    if (!game.boot()) return 1;

    // Build minimal data for Dialogue/Quest/Items and wire events as needed.

    DialogueDB ddb;

    ddb.nodes["hello"] = DialogueNode{

        "hello", "Hello, traveler!", { { "Who are you?", "about" }, { "Goodbye.", "" } }

    };

    ddb.nodes["about"] = DialogueNode{

        "about", "I'm the mayor. Please clear the slimes in the well.",

        { { "Accept quest", "" } }

    };

    DialogueRunner dlg;

    QuestLog quests;

    ItemDB itemdb; itemdb.defs[1] = ItemDef{1,"Potion", ItemType::Consumable, 10};

    Inventory inv; inv.add(1, 2);

    // Example "interact" flow (pseudo):

    // if (player presses E near mayor) dlg.start(&ddb, "hello");

    // if (player chooses "Accept quest") quests.start("clear_slimes");

    game.run();

    game.app.shutdown();

    return 0;

}

Tasks for Completion and Improvement

  1. 1. Combat Mechanics: Although a basic turn-based combat scaffold exists in the Combat struct, it currently lacks full resolution logic. Students should complete the combat system by defining win/loss conditions, handling defeated enemies, and integrating combat flow into the main game loop.
  2. 2. NPC Interactions: NPC entities are present, and a dialogue system exists, but interactions are not yet fully wired. Students should implement logic that allows the player to interact with nearby NPCs and initiate dialogue sequences.
  3. 3. Inventory Management: The Inventory and ItemDB systems support adding and using items, but there is no UI or interaction logic yet. Students should expand inventory management to allow browsing items, selecting them, and applying their effects during gameplay.
  4. 4. More Complex Game Logic: The current game loop handles movement, rendering, and simple state updates, but more advanced logic (such as state switching and conditional behavior) is needed for a full RPG experience.
  5. 5. Quest System: The QuestLog system is defined but not yet fully integrated. Students should connect NPC dialogue options to quest creation, advancement, and completion.
  6. 6. Leveling System: A simple progression system already exists in Progression, but it is not fully linked to combat outcomes. Students should ensure that defeating enemies grants experience and triggers level-ups when thresholds are reached.
  7. 7. Advanced AI: Enemy behavior is currently static and predictable. Students should extend enemy logic to make encounters more engaging and tactical.
  8. 8. Sound and Music: The engine already initializes Allegro’s audio system, but no sounds are played. Students should add audio feedback to enhance immersion

Exercises, Homework Questions, and Projects

Exercises

  1. 1. RPG System Identification

    List the major gameplay systems described in this section (movement, dialogue, quests, inventory, combat, progression, and UI). Briefly explain how at least three of these systems interact with one another during normal gameplay.

  2. 2. Tile-Based World Navigation

    Explain how tile-based maps simplify collision detection and movement logic. Why is it useful to separate visual tile layers from collision or trigger layers?

  3. 3. Dialogue and Choice Flow

    Examine the dialogue system structure. How do dialogue nodes and choices enable branching conversations, and why is this approach preferable to hard-coded dialogue strings?

  4. 4. Turn-Based Combat Reasoning

    Describe the basic turn order used in the combat scaffold. How does turn-based combat simplify decision-making and state management compared to real-time combat?

Homework Questions

  1. 1. Balancing Complexity in RPG Design

    RPGs often include many interconnected systems. What risks arise when too many features are introduced too early in development, and how can a staged or modular approach mitigate these risks?

  2. 2. Quest Design and Player Motivation

    Discuss how quests help guide player behavior and provide narrative structure. What types of quest objectives are best suited for beginner-level RPG implementations?

  3. 3. Inventory Management Trade-offs

    Compare simple inventory systems (fixed slot lists) with more complex systems (weight limits, equipment slots). What trade-offs do these designs present for both developers and players?

  4. 4. Progression and Player Engagement

    Explain how experience points and leveling systems contribute to long-term player engagement. What problems might arise if progression is too fast or too slow?

Programming Projects

  1. 1. Combat Mechanics Completion (Core Project)

    Complete the turn-based combat system by implementing victory and defeat conditions, removing defeated enemies, and returning control to the overworld after combat ends.

  2. 2. NPC Interaction and Dialogue Integration

    Fully integrate NPC interactions so that the player can initiate dialogue through proximity or facing direction. Connect dialogue outcomes to quest progression, item rewards, or other gameplay effects.

  3. 3. Inventory UI and Item Usage

    Design and implement a user interface for the inventory system. Allow the player to browse items, select consumables, and apply their effects during gameplay.

  4. 4. Quest System Implementation

    Extend the quest system so that quests can be started through dialogue, advanced through world triggers or combat events, and completed to grant rewards such as experience, items, or currency.

  5. 5. Leveling and Progression Enhancements

    Link combat outcomes to the progression system so that defeating enemies grants experience points. Display visual or audio feedback when the player levels up and unlocks stat improvements or abilities.

  6. 6. Enemy AI Improvements (Advanced)

    Improve enemy behavior by introducing patrol routes, pursuit logic, or simple decision-making (for example, retreating at low health or prioritizing certain player actions).

  7. 7. Audio and Feedback Integration (Advanced)

    Add sound effects for movement, combat actions, item usage, and level-ups, along with background music for different map areas or combat encounters.

Capstone Project (Optional)

Design and implement a small but complete RPG scenario featuring at least one town, one dungeon, multiple NPCs, and a short quest line with combat and rewards. The game should demonstrate coherent integration of movement, dialogue, quests, combat, inventory, and progression systems. Submit a brief design document explaining how these systems interact and how the player is guided through the experience.

8.9. Sports Game

Sports games are a popular genre that simulates the practice of sports. They can range from realistic simulations to more arcade-style games. As with the previous RPG games, other than a simple simulator, sports games have multiple elements that need to be addressed, along with the associated programming tasks that go with those. For example, the simulation of a team spot would require multiple NPCs on both your and the opposing team. The actions of those players need to be considered in terms of responses to gameplay. How does that affect the players’ actions, and to what depth and extent might the logic be implemented?

If one considers games like basketball, hockey, or soccer, we have some fairly common elements that can be applied to the game itself: We have a player who has an object—a basketball, a puck, or a soccer ball—and that object moves with the player in an attempt for that player to get that item into the opposing team’s goal. Conversely, you have the opposing team players, which are trying to stop your player from getting to that primary objective, so they are going to actively want to head for that player character on the screen, and then they’re going to take that object (ball or puck) and try to get it into the first player’s goal.

In the simplest implementation, we would make that a one-on-one game. For a game like basketball, we would have one NPC trying to block or take the ball away from the user who’s trying to shoot the ball into the hoop. For the game mechanic of shooting the ball into the hoop, we might implement a key press where the amount of time you hold it down is the force with which the ball is thrown toward the hoop. We can set it up control-wise so you don’t have to aim at the hoop—that could be automatic—but gameplay-wise, the idea is that as the player, depending on the distance you are from the hoop, you have to determine whether you have enough power to make it to the hoop.

Perhaps we add some other deflection penalty based on the angle to the hoop for increased difficulty. All this time, as well, we have an NPC that’s trying to take your ball or block it (if the NPC moves into the path of your ball during flight, a random percentage determines whether they block it). In terms of your throw, are you going to overshoot or undershoot? Then add to that the constraint of gravity.

In a game played on a flat plane, like hockey or soccer, you might use the same approach in regard to using key-press (or mouse-click) duration to affect power. However, in these games, it might be more apt to have the player need to control the angle of the shot to direct his ball or puck toward the goal in question.

Recall what we’ve covered in previous chapters in terms of AI for NPCs. Here, we might do something where the NPC goes toward the player with the ball (seeking) with the goal of trying to take the ball away from the player (where a collision transfers an object). The user player is trying to evade (triggering a flee) function to get away. Here, we need to consider the speed of the player and the NPC in terms of running/skating. If both the user player and the NPC are moving at the same speed, we’ve got a situation where each will never catch up to the other if they evade. So we might implement a mechanic whereby if you are running in a straight line, your speed gradually increases, but if you change direction, you go back to that base speed. We’d give that same factor to the NPC character as well. Then we’d have a situation where when you’re running after an object, you can overtake the object, and a direction change becomes strategic in getting away from a pursuer. As mentioned before, with a collision detection, your player transfers the ball/puck object on contact, so it’s a game of figuring out angles and velocities to keep your ball/puck and get into position to make your shot into the goal.

In game scenarios like these, you start to see where one can apply elements from other chapters:

  • • Animated sprites for characters that change perspective on movement or position
  • • AI for character movements and actions/reaction
  • • Background graphics for that nice background screen of a basketball court, soccer field, or an ice rink
  • • Sound effects when you shoot, when you get goals, or when you miss the shot
  • • Multithreading for AI

Taking it to the next level, you could have multiple NPCs on-screen: your team and the opponent. This will add multiple layers of logic, where your player NPCs would defend against the opposing team’s NPCs and so on. Such is the complexity of elements that you would need to put into even the simplest sports game.

8.9.1. Game Design

Game Concept

  • Genre: Sports game (basketball shooting).
  • Objective: The player controls a character that shoots a basketball into a hoop. The goal is to score as many points as possible within a time limit and avoid the opposing team’s NPCs.

Core Components

  • Player Character: The athlete controlled by the player
    • Attributes: Position (x, y), movement speed, sprite
    • Movement: Controlled via keyboard input
  • Basketball: The object used to score points
    • Attributes: Position, velocity, possession state
    • Physics: Affected by gravity and collisions
  • Opposing NPC: Defensive character
    • Attributes: Position, speed, sprite
    • Behavior: Seeks the player, attempts steals, or blocks shots
  • Hoop and Scoring Zone: The scoring target
    • Attributes: Position, collision bounds
    • Function: Registers a score when the ball passes through correctly
  • Score and Timer System: Tracks performance
    • Attributes: Current score, remaining time
    • Function: Ends the game when time expires

Key Techniques and Features to Have

A sports-style minigame combines responsive controls, lightweight physics, scoring logic, and AI pressure into a compact, fast-paced experience. Although smaller in scope than a full sports title, these systems must integrate cleanly to feel fluid and competitive. The following features define a solid, extensible baseline:

  1. 1. Responsive Player Control and Speed Ramp

    Player movement should be immediate and low-latency. An optional speed-ramp mechanic can increase maximum speed during sustained straight-line movement and reset on sharp turns, encouraging positioning and intentional movement.

  2. 2. Possession and Ball Carry Logic

    The ball should exist in clear possession states: player-owned, NPC-owned, or free. When carried, it follows a fixed offset from the owner. Possession transitions occur on shots, steals, blocks, or drops, keeping state management simple and consistent.

  3. 3. Hold-to-Shoot Power (and Optional Angle Assist)

    Shots should use a press-and-hold mechanic where charge time maps to initial velocity. Optional aim assistance (horizontal alignment or arc adjustment) can be added and scaled to tune difficulty.

  4. 4. Ball Physics and World Bounds

    Ball movement should use simplified projectile physics with gravity, mild drag, and damped bounces. The ball must remain within court boundaries and come to rest naturally after low-energy impacts.

  5. 5. Rim/Hoop Collision and Scoring Window

    Rim collision and scoring detection should be separated. The rim reflects the ball, while a dedicated scoring window registers successful shots only when the ball passes through in the correct direction.

  6. 6. Defensive NPC AI (Seek/Mark/Intercept)

    The NPC should follow a priority system: pressure the player when they have the ball, chase a loose ball, or defend the hoop lane. Shot interception attempts should use distance- and timing-based rules with tunable success rates.

  7. 7. Steal and Block Mechanics

    Steals can occur during close-range ball carrying, transferring possession on success. Blocks or catches may occur while the ball is airborne, governed by probability and short cooldowns to prevent repeated attempts.

  8. 8. HUD: Score, Timer, and Power Bar

    The HUD should display score, remaining time, and a shot-charge meter. Optional indicators such as streaks or accuracy can support challenge or practice modes while remaining unobtrusive.

  9. 9. Game States and Flow

    The game should transition cleanly between play, pause, and game-over states. Pausing freezes physics and AI, and game over should offer a quick restart to maintain pacing.

  10. 10. Feedback and Polish

    Sound effects and subtle visuals should reinforce shots, blocks, and scores. Optional effects include brief screen nudges, particles, or a debug overlay for hitboxes and scoring zones.

  11. 11. Difficulty and Tuning Hooks

    Difficulty should be adjustable through exposed parameters such as NPC speed, block chance, hoop window size, gravity, bounce damping, and round length, enabling rapid balancing.

  12. 12. Extensibility

    The design should support easy extension, including power-ups, enhanced shot meters, or multiplayer modes. Modular systems and clean state logic minimize refactoring as features expand.

8.9.2. Implementation

Key Operations in the Game

  1. 1. Input Sampling and Intent Flags
    • • Read Allegro keyboard events each frame; set flags for move left/right/up/down, shoot hold/release, pause, debug.
  2. 2. Player Movement Update
    • • Convert intent → desired velocity; apply accel/decel.
    • • Apply speed ramp when sustaining direction; reset on sharp turns.
    • • Integrate position and clamp to court.
  3. 3. Possession Handling
    • • If ball is carried, pin ball to owner offset.
    • • If free on ground, check pickup radius for player or NPC to claim.
    • • On shoot release, switch to Free + Airborne.
  4. 4. Shot Charge → Release
    • • While the space bar is held, accumulate charge up to a cap.
    • • On release, compute initial (vx, vy) from charge (and optional distance/angle assist), set airborne, play “shot” SFX, reset charge.
  5. 5. Ball Physics Integration
    • • Per tick: gravity → velocity; velocity → position; apply drag.
    • • Handle floor/wall collisions with damped reflections; stop when energy is low.
  6. 6. Rim/Hoop Collision and Scoring Check
    • • If ball intersects rim, reflect and play “block/clang” SFX.
    • • If ball passes through score window with forward velocity, increment score, resolve postscore state (e.g., hand to NPC or reset).
  7. 7. Steal/Block Attempts
    • • Steal: While player/NPC carries ball, if opponent enters contact radius, roll chance to transfer possession.
    • • Block/Catch: While airborne, if NPC near trajectory, roll chance to deflect or catch (ball becomes NPC owned).
  8. 8. NPC AI Update
    • • Determine priority target (player, free ball, hoop lane).
    • • Pursue target with accel/turning limits; apply the same speed-ramp rules used for the player.
    • • If NPC owns ball and is near its shooting spot, trigger NPC shot release logic.
  9. 9. Timer and Game State
    • • Decrement countdown; when ≤ 0, set gameOver.
    • • Handle pause/resume toggles; mute SFX and freeze motion while paused.
  10. 10. HUD and Power Meter Rendering
    • • Draw score and time remaining; render power bar proportional to charge while holding.
    • • Optional: Accuracy streaks or debug text.
  11. 11. Audio/FX Triggers
    • • Fire SFX for shot, score, block/clang.
    • • Optional: Small particle/flash at rim contact and score.
  12. 12. Debug Overlay (Optional)
    • • Toggle to draw hitboxes, score window, rim rect, AI target, and current physics values.
  13. 13. Housekeeping and Performance
    • • Avoid per-frame allocations in hot paths; reuse containers.
    • • Validate/destroy Allegro resources on shutdown; guard null pointers.

Structure of the Game Program

  1. 1. Initialization
  2. 2. Resource Loading
  3. 3. Court/Level and Camera (bounds, hoop position/zone)
  4. 4. Entities (Player, NPC, Ball, Hoop; possession)
  5. 5. Input (movement + hold-to-shoot power)
  6. 6. Physics (ball gravity/drag, wall/floor bounces)
  7. 7. Collision and Scoring (ball ↔ rim/hoop, ball ↔ world, player/NPC ↔ ball for steals/blocks)
  8. 8. NPC AI (seek/mark, intercept ball, speed-ramp mechanic)
  9. 9. Shooting Mechanic (charge → release → initial velocity + optional angle penalty)
  10. 10. Game Loop / State (Play, Pause, gameOver)
  11. 11. Score and Timer (HUD)
  12. 12. Audio and FX (sfx for shot/score/block; simple particle flash)
  13. 13. Debug Overlay (toggle: hitboxes, AI targets, power bar)

Coding the Game

// ================ 1) Initialization ================

struct App {

    ALLEGRO_DISPLAY* disp=nullptr; ALLEGRO_EVENT_QUEUE* q=nullptr; ALLEGRO_TIMER* t=nullptr;

    bool init(int W=1280,int H=720,int FPS=60){

        al_init(); al_install_keyboard(); al_install_audio();

        al_init_image_addon(); al_init_font_addon(); al_init_ttf_addon(); al_init_primitives_addon();

        disp=al_create_display(W,H); t=al_create_timer(1.0/FPS); q=al_create_event_queue();

        al_register_event_source(q, al_get_display_event_source(disp));

        al_register_event_source(q, al_get_timer_event_source(t));

        al_register_event_source(q, al_get_keyboard_event_source());

        al_start_timer(t); return disp&&q&&t;

    }

    void shutdown(){ if(q)al_destroy_event_queue(q); if(t)al_destroy_timer(t); if(disp)al_destroy_display(disp); }

};

// ================ 2) Resource Loading ================

struct Assets {

    std::unordered_map<std::string, ALLEGRO_BITMAP*> bmp;

    std::unordered_map<std::string, ALLEGRO_FONT*> font;

    std::unordered_map<std::string, ALLEGRO_SAMPLE*> sfx;

    bool load(){

        bmp["court"]=al_load_bitmap("assets/court.png");

        bmp["player"]=al_load_bitmap("assets/player.png");

        bmp["npc"]=al_load_bitmap("assets/npc.png");

        bmp["ball"]=al_load_bitmap("assets/ball.png");

        font["ui"]=al_load_ttf_font("assets/ui.ttf", 20, 0);

        sfx["shot"]=al_load_sample("assets/shot.ogg");

        sfx["score"]=al_load_sample("assets/score.ogg");

        sfx["block"]=al_load_sample("assets/block.ogg");

        return true; // check nulls in real code

    }

    ~Assets(){ for(auto&p:bmp) if(p.second) al_destroy_bitmap(p.second);

               for(auto&p:font) if(p.second) al_destroy_font(p.second);

               for(auto&p:sfx) if(p.second) al_destroy_sample(p.second); }

};

// ============== 3) Court/Level & Camera ===============

struct Court {

    int W=1280, H=720; // world size

    // Hoop modeled as a "score window" rectangle (simple):

    ALLEGRO_RECT scoreZone{1180, 220, 1220, 280}; // right side hoop window

    ALLEGRO_RECT rimRect {1160, 260, 1180, 275};  // simple rim for collisions

    bool inScoreZone(float x,float y) const {

        return x>=scoreZone.l && x<=scoreZone.r && y>=scoreZone.t && y<=scoreZone.b;

    }

    bool inWorld(float x,float y) const { return x>=0 && y>=0 && x<=W && y<=H; }

};

// ==================== helpers ====================

struct Vec { float x=0,y=0; };

inline Vec operator+(Vec a,Vec b){return {a.x+b.x,a.y+b.y};}

inline Vec operator-(Vec a,Vec b){return {a.x-b.x,a.y-b.y};}

inline Vec operator*(Vec a,float s){return {a.x*s,a.y*s};}

inline float dot(Vec a,Vec b){return a.x*b.x+a.y*b.y;}

inline float len(Vec a){return std::sqrt(dot(a,a));}

inline Vec norm(Vec a){float L=len(a); return L>0? a*(1.0f/L) : Vec{0,0};}

// ==================== 4) Entities ================

enum class Possession { Player, NPC, Free };

struct Ball {

    Vec pos{200, 500}, vel{0,0};

    float radius=12.0f; bool airborne=false;

    Possession owner = Possession::Player;

};

struct Actor {

    Vec pos{100,500}, vel{0,0};

    float baseSpeed=200; float burstGain=0; // speed-ramp mechanic

    bool moving=false;

    ALLEGRO_BITMAP* sprite=nullptr;

};

struct World {

    Court court;

    Actor player, npc;

    Ball ball;

    int score=0;

    float timeLeft=60.0f;

    bool gameOver=false;

};

// ==================== 5) Input ====================

struct Input {

    bool left=false,right=false,up=false,down=false;

    bool shootHold=false, shootJustReleased=false, debug=false, quit=false;

    void onEvent(const ALLEGRO_EVENT& ev){

        if(ev.type==ALLEGRO_EVENT_KEY_DOWN || ev.type==ALLEGRO_EVENT_KEY_UP){

            bool d = ev.type==ALLEGRO_EVENT_KEY_DOWN;

            switch(ev.keyboard.keycode){

                case ALLEGRO_KEY_A: case ALLEGRO_KEY_LEFT:  left=d; break;

                case ALLEGRO_KEY_D: case ALLEGRO_KEY_RIGHT: right=d; break;

                case ALLEGRO_KEY_W: case ALLEGRO_KEY_UP:    up=d; break;

                case ALLEGRO_KEY_S: case ALLEGRO_KEY_DOWN:  down=d; break;

                case ALLEGRO_KEY_SPACE: shootHold=d; shootJustReleased=!d; break;

                case ALLEGRO_KEY_F1: if(d) debug=!debug; break;

                case ALLEGRO_KEY_ESCAPE: if(d) quit=true; break;

            }

        }

    }

};

// ================ 6) Physics (ball) ==================

struct Physics {

    float gravity=900.0f; float drag=0.995f;

    void stepBall(Ball& b, Court& c, float dt){

        if(!b.airborne) return;

        b.vel.y += gravity*dt;

        b.pos = b.pos + b.vel*dt;

        b.vel = b.vel*drag;

        // floor bounce

        if (b.pos.y + b.radius > c.H) {

            b.pos.y = c.H—b.radius;

            b.vel.y *=—0.55f; // damped bounce

            if (std::fabs(b.vel.y) < 60) { b.airborne=false; b.vel={0,0}; }

        }

        // walls

        if (b.pos.x—b.radius < 0)  { b.pos.x = b.radius; b.vel.x *=—0.6f; }

        if (b.pos.x + b.radius > c.W){ b.pos.x = c.W—b.radius; b.vel.x *=—0.6f; }

    }

};

// =============== 7) Collision & Scoring ==============

bool intersectsCircleRect(Vec p,float r, ALLEGRO_RECT R){

    float cx = std::clamp(p.x, R.l, R.r);

    float cy = std::clamp(p.y, R.t, R.b);

    float dx = p.x-cx, dy=p.y-cy;

    return dx*dx + dy*dy <= r*r;

}

struct Collisions {

    // Rim knockback and score detection

    void handle(World& w, Assets& A){

        // score: ball passes through scoreZone while moving left->right

        if (w.ball.airborne && w.court.inScoreZone(w.ball.pos.x, w.ball.pos.y) && w.ball.vel.x>0){

            ++w.score; w.ball.airborne=false; w.ball.owner=Possession::NPC; // after score, hand to NPC?

            al_play_sample(A.sfx["score"], 1,0,1, ALLEGRO_PLAYMODE_ONCE, nullptr);

        }

        // rim collision → reflect

        if (intersectsCircleRect(w.ball.pos, w.ball.radius, w.court.rimRect)){

            w.ball.vel.x *=—0.6f; w.ball.vel.y *=—0.6f;

            al_play_sample(A.sfx["block"], 0.6,0,1, ALLEGRO_PLAYMODE_ONCE, nullptr);

        }

        // steal/block: if NPC intersects ball in flight → chance to catch

        float dist = len(w.npc.pos—w.ball.pos);

        if (w.ball.airborne && dist < 32.0f) {

            if ((rand()%100)<30) { // 30% block/catch

                w.ball.airborne=false; w.ball.owner=Possession::NPC; w.ball.vel={0,0};

                al_play_sample(A.sfx["block"], 1,0,1, ALLEGRO_PLAYMODE_ONCE, nullptr);

            }

        }

        // pickup when free on ground

        if (!w.ball.airborne && w.ball.owner==Possession::Free){

            if (len(w.player.pos—w.ball.pos)<28) w.ball.owner=Possession::Player;

            if (len(w.npc.pos  —w.ball.pos)<28) w.ball.owner=Possession::NPC;

        }

        // carry with owner

        if (!w.ball.airborne){

            if (w.ball.owner==Possession::Player) w.ball.pos = w.player.pos + Vec{10,-20};

            if (w.ball.owner==Possession::NPC)    w.ball.pos = w.npc.pos    + Vec{-10,-20};

        }

    }

};

// ========= 8) NPC AI (seek/mark + speed ramp) =========

struct AI {

    float accel=800.0f, decel=1200.0f, burstMax=180.0f, turnPenalty=160.0f;

    void update(World& w, float dt){

        // target = player if player has ball, else loose ball, else hoop area to defend

        Vec target = (w.ball.owner==Possession::Player) ? w.player.pos

                  : (w.ball.owner==Possession::Free)   ? w.ball.pos

                  : Vec{w.court.scoreZone.l, (w.court.scoreZone.t+w.court.scoreZone.b)/2};

        // pursue

        Vec dir = norm(target—w.npc.pos);

        Vec desired = dir * (w.npc.baseSpeed + w.npc.burstGain);

        // turn penalty: reduce burst if direction changes sharply

        if (dot(norm(w.npc.vel), dir) < 0.6f) w.npc.burstGain = 0; // sharp turn resets run-up

        // accelerate towards desired

        Vec dv = desired—w.npc.vel;

        Vec step = norm(dv) * std::min(len(dv), accel*dt);

        w.npc.vel = w.npc.vel + step;

        // straight-line ramp up

        if (len(step)>0.0f && dot(norm(w.npc.vel), dir) > 0.95f)

            w.npc.burstGain = std::min(burstMax, w.npc.burstGain + 60.0f*dt);

        // integrate & clamp to court

        w.npc.pos = w.npc.pos + w.npc.vel*dt;

        w.npc.pos.x = std::clamp(w.npc.pos.x, 20.0f, (float)w.court.W-20.0f);

        w.npc.pos.y = std::clamp(w.npc.pos.y, 20.0f, (float)w.court.H-20.0f);

        // NPC simple shot if it owns ball and near left hoop (mirror if needed)

        if (w.ball.owner==Possession::NPC && w.npc.pos.x > w.court.W*0.7f) {

            // quick lob toward hoop

            w.ball.owner=Possession::Free; w.ball.airborne=true;

            w.ball.vel = { 320.0f,—520.0f }; // tuned by trial

        }

    }

};

// =========== 9) Shooting Mechanic (player) ===========

struct Shooter {

    float charge=0, chargeMax=1.5f; // seconds held

    void tick(Input& in, float dt){ if(in.shootHold) charge = std::min(charge+dt, chargeMax); }

    void releaseIfAny(Input& in, World& w, Assets& A){

        if (!in.shootJustReleased) return;

        if (w.ball.owner!=Possession::Player) { in.shootJustReleased=false; charge=0; return; }

        float p = charge / chargeMax; // 0..1

        // Auto-aim assist on X, gravity handled by physics; add small angle penalty with distance

        float baseVX = 500.0f * (0.6f + 0.8f*p);

        float baseVY =—700.0f * (0.5f + 0.7f*p);

        // Optional difficulty: off-axis penalty increases vertical error with distance:

        float dx = (w.court.scoreZone.l—w.player.pos.x);

        float distFactor = std::clamp(dx / 900.0f, 0.f, 1.f);

        baseVY—= 120.0f * distFactor; // need more arc from far

        w.ball.owner=Possession::Free;

        w.ball.airborne=true;

        w.ball.vel = { baseVX, baseVY };

        al_play_sample(A.sfx["shot"], 0.9,0,1, ALLEGRO_PLAYMODE_ONCE, nullptr);

        charge=0; in.shootJustReleased=false;

    }

};

// =============== 10) Game Loop / State ===============

// ================= 11) Score & Timer =================

// ================== 12) Audio & FX ===================

// ===================== 13) Debug =====================

struct Game {

    App app; Assets assets; World world; Input input;

    Physics physics; Collisions coll; AI ai; Shooter shooter;

    bool debug=false;

    bool boot(){

        if(!app.init()) return false;

        assets.load();

        world.player.sprite = assets.bmp["player"];

        world.npc.sprite    = assets.bmp["npc"];

        return true;

    }

    void handleEvent(const ALLEGRO_EVENT& ev){

        if(ev.type==ALLEGRO_EVENT_DISPLAY_CLOSE) input.quit=true;

        if(ev.type==ALLEGRO_EVENT_KEY_DOWN || ev.type==ALLEGRO_EVENT_KEY_UP) input.onEvent(ev);

    }

    void update(float dt){

        if (input.quit) { world.gameOver=true; return; }

        if (world.gameOver) return;

        // Player move + speed-ramp like NPC

        Vec dir{ (float)input.right—(float)input.left, (float)input.down—(float)input.up };

        if (len(dir)>0){ dir=norm(dir); world.player.moving=true; }

        else { world.player.moving=false; }

        // ramp

        if (world.player.moving && dot(norm(world.player.vel), dir) > 0.95f)

            world.player.burstGain = std::min(180.0f, world.player.burstGain + 80.0f*dt);

        else if (!world.player.moving) world.player.burstGain = std::max(0.0f, world.player.burstGain—200.0f*dt);

        Vec desired = dir * (world.player.baseSpeed + world.player.burstGain);

        Vec dv = desired—world.player.vel;

        world.player.vel = world.player.vel + norm(dv) * std::min(len(dv), 1200.0f*dt);

        world.player.pos = world.player.pos + world.player.vel*dt;

        world.player.pos.x = std::clamp(world.player.pos.x, 20.0f, (float)world.court.W-20.0f);

        world.player.pos.y = std::clamp(world.player.pos.y, 20.0f, (float)world.court.H-20.0f);

        // AI + ball physics + collisions

        ai.update(world, dt);

        shooter.tick(input, dt);

        physics.stepBall(world.ball, world.court, dt);

        coll.handle(world, assets);

        shooter.releaseIfAny(input, world, assets);

        // Timer

        world.timeLeft—= dt;

        if (world.timeLeft <= 0) world.gameOver = true;

    }

    void draw(){

        al_clear_to_color(al_map_rgb(20,20,30));

        if (assets.bmp["court"])

            al_draw_bitmap(assets.bmp["court"], 0, 0, 0);

        // Draw actors

        if (world.player.sprite) al_draw_bitmap(world.player.sprite, world.player.pos.x-16, world.player.pos.y-32, 0);

        if (world.npc .sprite)  al_draw_bitmap(world.npc .sprite, world.npc .pos.x-16,  world.npc .pos.y-32,  0);

        if (assets.bmp["ball"]) al_draw_bitmap(assets.bmp["ball"], world.ball.pos.x-12, world.ball.pos.y-12, 0);

        // HUD

        std::string hud = "Score: " + std::to_string(world.score) + "   Time: " + std::to_string((int)std::ceil(world.timeLeft));

        al_draw_text(assets.font["ui"], al_map_rgb(255,255,255), 16, 16, 0, hud.c_str());

        // Power bar (shoot charge)

        float pw = 200.0f * (std::min(shooter.charge, shooter.chargeMax)/shooter.chargeMax);

        al_draw_filled_rectangle(16, 48, 16+pw, 64, al_map_rgb(80,220,120));

        // Debug

        if (input.debug){

            // score window & rim

            al_draw_rectangle(world.court.scoreZone.l, world.court.scoreZone.t,

                              world.court.scoreZone.r, world.court.scoreZone.b, al_map_rgb(255,255,0), 2);

            al_draw_rectangle(world.court.rimRect.l, world.court.rimRect.t,

                              world.court.rimRect.r, world.court.rimRect.b, al_map_rgb(255,0,0), 2);

        }

        if (world.gameOver){

            al_draw_text(assets.font["ui"], al_map_rgb(255,200,0), 640, 320, ALLEGRO_ALIGN_CENTER, "GAME OVER");

        }

        al_flip_display();

    }

    void run(){

        while(true){

            ALLEGRO_EVENT ev; al_wait_for_event(app.q, &ev);

            if (ev.type==ALLEGRO_EVENT_TIMER){ update(1.0f/60.0f); draw(); }

            else handleEvent(ev);

            if (input.quit) break;

        }

    }

};

int main(){

    Game g; if(!g.boot()) return 1;

    g.run(); g.app.shutdown(); return 0;

}

Tasks for Completion and Improvement

  1. 1. Shooting Mechanics: Implement the logic for shooting the basketball and checking if it goes into the hoop.
  2. 2. Scoring System: Add a scoring system to keep track of successful shots.
  3. 3. Animations: Add animations for shooting and scoring.
  4. 4. Sound Effects: Add sound effects for shooting and scoring.

Exercises, Homework Questions, and Projects

Exercises

  1. 1. Possession State Analysis

    Identify the different possession states of the ball (player-owned, NPC-owned, free). Explain how transitions between these states occur during gameplay and why it is important to explicitly manage possession.

  2. 2. Hold-to-Shoot Mechanics

    Explain how charging shot power using key-press duration affects gameplay difficulty. What advantages does this mechanic have over a single-press shooting model?

  3. 3. Ball Physics Understanding

    Describe the role of gravity, velocity, and damping in the basketball’s movement. How do these factors influence whether a shot overshoots, undershoots, or scores?

  4. 4. NPC Blocking Logic

    Analyze the NPC’s blocking and interception behavior. Why is it beneficial to use probability and cooldowns rather than deterministic blocking?

Homework Questions

  1. 1. Sports Game Design Trade-Offs

    Compare realism and fun in sports games. Why might an arcade-style basketball game intentionally simplify physics or automate aiming?

  2. 2. Speed-Ramp Mechanics

    Discuss how gradually increasing speed during sustained movement changes player strategy. How does this mechanic affect evasion, pursuit, and positioning?

  3. 3. Scoring Reliability

    Explain why separating rim collision detection from scoring window detection produces more reliable scoring behavior than treating the hoop as a single collision object.

  4. 4. AI Pressure and Player Experience

    How does continuous NPC pressure (stealing, blocking, intercepting) influence the pace and challenge of the game? What risks exist if NPC behavior is too aggressive or too passive?

Programming Projects

  1. 1. Shooting Mechanics Completion (Core Project)

    Fully implement the shooting logic, including charging, release, trajectory calculation, and successful scoring detection when the ball passes through the hoop’s scoring window.

  2. 2. Scoring System Enhancement

    Extend the scoring system to track points per shot, streak bonuses, or accuracy percentages. Display this information in the HUD and update it dynamically.

  3. 3. Animations and Visual Feedback

    Add animations for player movement, shooting, and successful baskets. Include simple visual effects such as flashes, particles, or camera nudges to emphasize key events.

  4. 4. Sound Effects Integration

    Incorporate sound effects for shooting, rim impacts, blocked shots, and scoring. Balance audio feedback to enhance immersion without overwhelming the player.

  5. 5. Improved NPC Behavior (Advanced)

    Enhance NPC AI by introducing varied defensive tactics, such as anticipating shots based on player distance or prioritizing blocking over stealing in certain situations.

  6. 6. Difficulty Modes (Advanced)

    Implement multiple difficulty settings by adjusting NPC speed, block probability, shot accuracy penalties, or game timer length.

Capstone Project

Design and implement a complete one-on-one basketball challenge with polished presentation. The game should include responsive controls, believable physics, competitive NPC behavior, clear scoring and timing rules, and audiovisual feedback. Submit a short design report explaining your choices for difficulty balancing, AI logic, and user interface clarity.

8.10. Strategy Game

Strategy games are a genre that emphasizes skillful thinking and planning to achieve victory. They come in various forms, each offering unique gameplay experiences, from terraforming a strange new world, to running a farm or factory, to creating armies to do battle. There is something for everyone in this genre.

Like the RPG game, there are multiple design elements that make the game complex. The player has multiple units and assets to control and manage; likewise for the NPC. The information needs to be presented to the player effectively, and the interface for the game needs to be intuitive (mostly) and smooth. Usually, this would be mouse driven, with menus and clickable icons or text elements. AI plays a role in not only the NPC playing against the human player but also the player’s units following commands or taking actions unattended.

Let’s consider a basic real-time strategy (RTS) combat game, where the player can control units to gather resources and build structures. In the simplest form, we will have a map with resources scattered around; these are required to build our combat units. The players each start off at random locations, and the game’s goal is to gather resources, build your combat units, and use those to take control of the whole map.

8.10.1. Game Design

Game Concept

  • Genre: Real-time strategy (RTS).
  • Objective: The player manages units and resources, constructs structures, and defeats an opposing force through strategic planning and real-time decision-making.

Core Components

  • Units: Controllable characters
    • Attributes: Position, health, attack power, state
    • Actions: Move, gather resources, attack, build
  • Resources: Materials used for production
    • Attributes: Type (wood, stone), amount, location
    • Function: Enable unit and structure creation
  • Structures: Buildings placed on the map
    • Attributes: Position, health, type
    • Function: Produce units or support the economy
  • Economy System: Manages resource flow
    • Function: Handles gathering, carrying, and depositing resources
  • Enemy AI: Controls opposing forces
    • Behavior: Gathers resources, builds units, attacks player assets
  • UI and Command System: Player interaction layer
    • Function: Selection, issuing commands, displaying information

Key Techniques and Features to Have

A real-time strategy (RTS) game integrates unit control, economic simulation, pathfinding, artificial intelligence, and a readable user interface into a continuous gameplay loop. Even in simplified form, these systems must work together smoothly to remain responsive, understandable, and extensible. The following features define a robust baseline RTS implementation using Allegro:

  1. 1. Mouse-Driven Selection and Commands

    Players should select units with single clicks or box selection, using modifier keys (e.g., Shift) to add or remove units. Right-click commands should be context-sensitive, automatically resolving to move, gather, attack, or build actions. Clear visual feedback (selection rings or marquees) is essential.

  2. 2. World–Screen Coordinate Mapping

    All interactions should rely on consistent conversions between screen space, camera space, and world or grid space. Coordinate mapping should be centralized and documented, especially when using a tile grid, to support selection, placement, and pathfinding reliably.

  3. 3. Pathfinding and Movement

    Unit movement can begin with simple steering and obstacle avoidance and later evolve into grid-based A* pathfinding with passability masks. Basic separation or formation logic should reduce unit clumping and improve group movement behavior.

  4. 4. Finite-State Units

    Units should use finite state machines to govern behavior such as idle, moving, gathering, attacking, building, or retreating. State transitions should depend on ranges, cooldowns, and pursuit limits to keep behavior predictable and controllable.

  5. 5. Core Economy Loop

    The economy should follow a clear gather-carry-deposit pipeline. Resource nodes should deplete over time, encouraging expansion. Multiple resource types and tunable costs enable balanced unit production, structure building, and upgrades.

  6. 6. Building and Placement System

    Structures should be placed using a ghost preview that indicates valid or invalid locations. Placement checks include collision and terrain passability. Structures should progress through construction stages, and production buildings should support rally points.

  7. 7. Combat Model

    Combat should follow a consistent damage model incorporating range, armor or defense, and attack type (projectile or hitscan). Units should prioritize targets logically and handle death through cleanup, optional effects, or refunds.

  8. 8. Opponent AI

    Enemy AI should follow a simple strategic loop: gathering resources, expanding infrastructure, training units, and attacking the player. AI difficulty can scale through resource rates, reaction timing, and unit production speed.

  9. 9. Fog of War (Recommended)

    Fog of war should track visibility per tile, distinguishing explored areas from currently visible ones. Unseen areas should be hidden or dimmed, with changes reflected on the minimap to enhance strategic planning.

  10. 10. UI/HUD

    The interface should clearly display resources, population limits, selected unit information, and build options. Tool tips should explain costs and stats. An optional minimap can support navigation, alerts, and situational awareness.

  11. 11. Game States and Save/Load

    The game should transition cleanly between play, pause, victory, and defeat states. Pausing must freeze simulation without losing state. Optional save/load functionality may snapshot world state, resources, and production queues.

  12. 12. Performance and Data-Driven Design

    To maintain performance, avoid per-frame allocations and reuse frequently created objects. Units, structures, and costs should be defined in external data files (e.g., JSON or CSV) to allow tuning and extension without recompilation.

8.10.2. Implementation

Key Operations in the Game

  1. 1. Event Intake and Intent Mapping
    • • Poll Allegro events (mouse, keyboard).
    • • Update intent flags (selecting, dragging, issuing right-click orders, build hotkeys, camera pan/zoom, pause).
  2. 2. Screen ↔ World Transform
    • • Convert cursor from screen → world using camera offset and zoom.
    • • Use world coords for hit-tests (units, resources, structures) and placement ghosts.
  3. 3. Selection Lifecycle
    • • On LMB down: Start marquee.
    • • On LMB up: Compute marquee AABB in world space; mark units inside as selected (respect team).
    • • Shift-click to add/remove single units.
  4. 4. Command Resolution (RMB)
    • • Determine context at clicked world point (terrain, resource node, enemy, friendly building).
    • • Issue corresponding order to all selected units: Move, Gather, Attack, Build/Repair, or Return.
    • • Stagger goals slightly (formation spread) to reduce overlap.
  5. 5. Per-Tick Unit FSM Update
    • • For each unit, process state (Idle/Move/Gather/Return/Attack/Build).
    • • Update cooldowns, acquisition, and retreat checks.
    • • Transition when goals are reached or conditions change (node empty, target lost).
  6. 6. Path/Movement Step
    • • If path exists, advance along waypoints; else request path if stuck or goal changed.
    • • Collision/passability checks against map mask; nudge or repath on obstruction.
  7. 7. Economy Tick
    • • Gather when in range of node (decrease node amount, increase carried).
    • • Return to depot; on arrival, deposit to player stockpile; resume gathering if node remains.
  8. 8. Build System Tick
    • • If placing: update ghost position and validity; on confirm, spawn construction entity with progress.
    • • On completion, transform to finished structure, play SFX, set rally point (if applicable).
  9. 9. Combat Tick
    • • Target acquisition (in range and visible).
    • • Attack when off cooldown (apply damage or spawn projectile).
    • • Handle death: Remove entity, award bounty/XP (if used), trigger alerts.
  10. 10. Opponent AI Step
    • • Simple planner: if resources ≥ threshold → build; if army size ≥ threshold → harass nearest player asset.
    • • Assign worker routines and defense reactions.
  11. 11. Camera and Minimap
    • • Update camera (edge-pan, WASD pan, mouse-wheel zoom).
    • • Draw or update minimap buffer; map clicks to camera moves.
  12. 12. Fog of War (If Enabled)
    • • Recompute visible tiles from unit vision each tick or every N frames.
    • • Mask terrain/units in render pass; update minimap overlay.
  13. 13. Render Pipeline (per Frame)
    • • Clear; draw terrain; draw resources/structures/units (with selection rings and team colours); draw projectiles/effects; then UI/HUD/minimap; flip.
    • • Draw placement ghost and build progress bars.
  14. 14. Audio and Alerts
    • • Trigger SFX on gather tick, build complete, attack, unit trained, under attack.
    • • Optional: On-screen pings and minimap flashes.
  15. 15. Win/Lose and Persistence
    • • Check victory conditions (e.g., enemy HQ destroyed) or defeat (no HQ + no builders).
    • • Save/load hooks for snapshotting world state.
  16. 16. Housekeeping and Performance
    • • Cull off-screen effects; reuse vectors/buffers; guard all Allegro resources.
    • • Frame budget awareness: Consider updating costly systems (pathfinding/FoW) on intervals.

Structure of the Game Program

  1. 1. Initialization
  2. 2. Resource Loading (sprites, fonts, SFX)
  3. 3. Map/Terrain and Camera (grid, passability, spawn points)
  4. 4. Entities (Units, Resources, Structures; teams/factions)
  5. 5. Selection and Commands (mouse box-select, right-click orders)
  6. 6. Pathfinding and Movement (grid A* later; stub steering now)
  7. 7. Economy (gather, carry, deposit, resource counts)
  8. 8. Build System (place ghost, validate, spawn structure)
  9. 9. Combat (targeting, attack cooldown, damage/death)
  10. 10. Enemy AI (simple “expand + harass”)
  11. 11. Game Loop / States (Play, Pause, gameOver)
  12. 12. UI / HUD (resources, selected panel; optional minimap)
  13. 13. Save/Load (Optional: stubbed)

Coding the Game

// ================= 1) Initialization =================

struct App {

    ALLEGRO_DISPLAY* disp=nullptr; ALLEGRO_EVENT_QUEUE* q=nullptr; ALLEGRO_TIMER* timer=nullptr;

    bool init(int W=1280,int H=720,int FPS=60){

        al_init(); al_install_keyboard(); al_install_mouse(); al_install_audio();

        al_init_image_addon(); al_init_font_addon(); al_init_ttf_addon(); al_init_primitives_addon();

        disp=al_create_display(W,H); timer=al_create_timer(1.0/FPS); q=al_create_event_queue();

        al_register_event_source(q, al_get_display_event_source(disp));

        al_register_event_source(q, al_get_timer_event_source(timer));

        al_register_event_source(q, al_get_keyboard_event_source());

        al_register_event_source(q, al_get_mouse_event_source());

        al_start_timer(timer); return disp&&q&&timer;

    }

    void shutdown(){ if(q)al_destroy_event_queue(q); if(timer)al_destroy_timer(timer); if(disp)al_destroy_display(disp); }

};

// ================ Common helpers =====================

struct Vec { float x=0,y=0; };

inline Vec operator+(Vec a,Vec b){return {a.x+b.x,a.y+b.y};}

inline Vec operator-(Vec a,Vec b){return {a.x-b.x,a.y-b.y};}

inline Vec operator*(Vec a,float s){return {a.x*s,a.y*s};}

inline float dot(Vec a,Vec b){return a.x*b.x+a.y*b.y;}

inline float len(Vec a){return std::sqrt(dot(a,a));}

inline Vec norm(Vec a){float L=len(a); return L>0? a*(1.0f/L):Vec{0,0};}

// ================= 2) Resource Loading ================

struct Assets {

    std::unordered_map<std::string, ALLEGRO_BITMAP*> bmp;

    std::unordered_map<std::string, ALLEGRO_FONT*>   font;

    std::unordered_map<std::string, ALLEGRO_SAMPLE*> sfx;

    bool load(){

        bmp["tiles"]=al_load_bitmap("assets/tiles.png");

        bmp["unit"]=al_load_bitmap("assets/unit.png");

        bmp["enemy"]=al_load_bitmap("assets/enemy.png");

        bmp["tree"]=al_load_bitmap("assets/tree.png");

        bmp["mine"]=al_load_bitmap("assets/mine.png");

        bmp["hq"]  =al_load_bitmap("assets/hq.png");

        bmp["barracks"]=al_load_bitmap("assets/barracks.png");

        font["ui"]=al_load_ttf_font("assets/ui.ttf", 18, 0);

        sfx["gather"]=al_load_sample("assets/gather.ogg");

        sfx["build"]=al_load_sample("assets/build.ogg");

        sfx["attack"]=al_load_sample("assets/attack.ogg");

        return true; // check nulls in real code

    }

    ~Assets(){ for(auto&p:bmp) if(p.second) al_destroy_bitmap(p.second);

               for(auto&p:font) if(p.second) al_destroy_font(p.second);

               for(auto&p:sfx)  if(p.second) al_destroy_sample(p.second); }

};

// ================= 3) Map/Terrain & Camera ===========

struct Map {

    int W=128, H=72, tile=10;              // world grid size; tile px size (camera scales to display)

    std::vector<uint8_t> pass;             // 0 free, 1 blocked

    bool in(int gx,int gy)const{ return gx>=0&&gy>=0&&gx<W&&gy<H; }

    bool blockedPx(int px,int py) const {

        int gx=px/tile, gy=py/tile; if(!in(gx,gy)) return true; return pass[gy*W+gx]!=0;

    }

    void generate(){

        pass.assign(W*H,0);

        // scatter some blocked tiles to simulate rocks/trees (very minimal)

        for(int i=0;i<200;i++){ int gx=rand()%W, gy=rand()%H; pass[gy*W+gx]=1; }

    }

    void draw(const Assets& A, float camx, float camy){

        // Simple flat color grid; replace with tileset draw if desired

        for(int y=0;y<H;y++) for(int x=0;x<W;x++){

            ALLEGRO_COLOR c = pass[y*W+x]? al_map_rgb(60,85,60):al_map_rgb(35,120,35);

            al_draw_filled_rectangle(x*tile-camx,y*tile-camy,(x+1)*tile-camx,(y+1)*tile-camy,c);

        }

    }

};

struct Camera { float x=0,y=0; void follow(Vec p,int sw,int sh,int tile){ x=p.x-sw/2; y=p.y-sh/2; if(x<0)x=0;if(y<0)y=0; }

// ================= 4) Entities ========================

enum class Team { Player, Enemy, Neutral };

enum class UnitState { Idle, Move, Gather, Return, Attack, Build };

enum class ResourceType { Wood, Stone };

struct ResourceNode {

    Vec pos; int amount=300; ResourceType type=ResourceType::Wood;

    ALLEGRO_BITMAP* sprite=nullptr;

};

struct Structure {

    Vec pos; Team team; int hp=500; std::string kind; // "HQ","Barracks","Depot"

    ALLEGRO_BITMAP* sprite=nullptr;

};

struct Unit {

    Vec pos, vel; Team team=Team::Player; int hp=100, atk=8; float range=22;

    UnitState st=UnitState::Idle; Vec goal; int carry=0; int carryMax=50; ResourceType carryType=ResourceType::Wood;

    Structure* homeDepot=nullptr; ResourceNode* targetNode=nullptr; Unit* targetEnemy=nullptr;

    ALLEGRO_BITMAP* sprite=nullptr; bool selected=false;

};

struct World {

    Map map; Camera cam;

    std::vector<Unit> units;

    std::vector<ResourceNode> nodes;

    std::vector<Structure> structures;

    int wood=100, stone=0;      // player resources

    int e_wood=100;             // enemy resources (toy)

    bool gameOver=false;

};

// ============== 5) Selection & Commands ===============

struct Input {

    bool quit=false; bool lmb=false, rmb=false; bool dragging=false;

    Vec mouse{0,0}, dragStart{0,0};

    void handle(const ALLEGRO_EVENT& ev){

        if(ev.type==ALLEGRO_EVENT_DISPLAY_CLOSE) quit=true;

        if(ev.type==ALLEGRO_EVENT_MOUSE_AXES){ mouse={ (float)ev.mouse.x,(float)ev.mouse.y }; }

        if(ev.type==ALLEGRO_EVENT_MOUSE_BUTTON_DOWN){

            if(ev.mouse.button==1){ lmb=true; dragging=true; dragStart=mouse; }

            if(ev.mouse.button==2){ rmb=true; }

        }

        if(ev.type==ALLEGRO_EVENT_MOUSE_BUTTON_UP){

            if(ev.mouse.button==1){ lmb=false; dragging=false; }

            if(ev.mouse.button==2){ rmb=false; }

        }

    }

};

struct Selection {

    void boxSelect(World& w, Vec a, Vec b, float camx,float camy){

        float l=std::min(a.x,b.x)+camx, r=std::max(a.x,b.x)+camx;

        float t=std::min(a.y,b.y)+camy, d=std::max(a.y,b.y)+camy;

        for(auto& u:w.units) if(u.team==Team::Player){

            float ux=u.pos.x, uy=u.pos.y;

            u.selected = (ux>=l && ux<=r && uy>=t && uy<=d);

        }

    }

    void clear(World& w){ for(auto& u:w.units) u.selected=false; }

};

// ============== 6) Pathfinding & Movement ============

// Minimal "greedy steering" towards goal with obstacle nudge.

// Swap with A* grid later.

struct Mover {

    float speed=90.0f;

    void step(Unit& u, const Map& m, float dt){

        Vec d = u.goal—u.pos; if(len(d)<4){ u.vel={0,0}; return; }

        Vec dir = norm(d);

        // Simple obstacle nudge

        Vec next = u.pos + dir*speed*dt;

        if (m.blockedPx((int)next.x,(int)u.pos.y)) next.x=u.pos.x;

        if (m.blockedPx((int)u.pos.x,(int)next.y)) next.y=u.pos.y;

        u.pos = next;

    }

};

// ============== 7) Economy (gather/deposit) ==========

struct Economy {

    void update(Unit& u, World& w, float dt){

        switch(u.st){

        case UnitState::Gather:

            if (!u.targetNode || u.targetNode->amount<=0){ u.st=UnitState::Idle; break; }

            if (len(u.pos—u.targetNode->pos) > 18) { u.goal = u.targetNode->pos; } // move closer

            else {

                // gather tick

                int take = std::min(5, u.targetNode->amount);

                u.targetNode->amount—= take; u.carry += take; u.carryType = u.targetNode->type;

                if (u.carry >= u.carryMax){ u.st=UnitState::Return; u.goal = u.homeDepot? u.homeDepot->pos : u.pos; }

            }

            break;

        case UnitState::Return:

            if (len(u.pos—u.goal) > 18) break;

            // deposit

            if (u.team==Team::Player){

                if (u.carryType==ResourceType::Wood) w.wood += u.carry; else w.stone += u.carry;

            }

            u.carry=0; u.st=UnitState::Gather; // resume node

            break;

        default: break;

        }

    }

};

// ============== 8) Build System ======================

struct Build {

    bool placing=false; std::string kind="Barracks"; Vec ghostPx;

    int costWood=150;

    bool canPlace(const World& w, Vec px){

        // crude footprint check (block tiles?)

        return !w.map.blockedPx((int)px.x, (int)px.y);

    }

    void confirm(World& w, const Assets& A){

        if(!placing) return;

        if (kind=="Barracks" && w.wood>=costWood && canPlace(w, ghostPx)){

            w.wood—= costWood;

            Structure s; s.kind="Barracks"; s.pos=ghostPx; s.team=Team::Player; s.sprite=A.bmp.at("barracks");

            w.structures.push_back(s);

        }

        placing=false;

    }

};

// ============== 9) Combat ============================

struct Combat {

    float attackCD=0.8f;

    void tick(Unit& u, World& w, float dt){

        static std::unordered_map<Unit*, float> cd;

        cd[&u] = std::max(0.0f, cd[&u]-dt);

        if (u.st==UnitState::Attack && u.targetEnemy){

            if (len(u.pos—u.targetEnemy->pos) > u.range) { u.goal = u.targetEnemy->pos; return; }

            if (cd[&u]<=0.0f){

                u.targetEnemy->hp—= u.atk; cd[&u]=attackCD;

                if (u.targetEnemy->hp<=0) u.targetEnemy->hp=0; // (remove later)

            }

        }

    }

};

// ============== 10) Enemy AI =========================

struct EnemyAI {

    void update(World& w, float dt){

        // Very minimal: enemy gathers from nearest node until enough wood, then spawns a unit at enemy HQ to harass.

        // Find enemy HQ

        Structure* ehq=nullptr;

        for(auto& s:w.structures) if(s.team==Team::Enemy && s.kind=="HQ"){ ehq=&s; break; }

        if(!ehq) return;

        // Periodically spawn a small raider if resources allow

        static float acc=0; acc+=dt;

        if (acc>5.0f && w.e_wood>=50){ acc=0; w.e_wood-=50;

            Unit e; e.team=Team::Enemy; e.pos=ehq->pos; e.sprite=nullptr; e.hp=80; e.atk=6;

            // set it to attack nearest player structure

            Structure* tgt=nullptr; float best=1e9;

            for(auto& s:w.structures) if(s.team==Team::Player){

                float d = len(s.pos—e.pos); if(d<best){best=d; tgt=&s;}

            }

            if (tgt){ e.st=UnitState::Attack; /* treat structure as a proxy: set goal near pos; in real code, allow unit->structure attack */ }

            w.units.push_back(e);

        }

    }

};

// ============== 11) Game Loop / State ===============

struct Game {

    App app; Assets A; World W; Input input; Selection sel; Mover mover; Economy eco; Combat combat; Build build; EnemyAI enemyAI;

    bool boot(){

        if(!app.init()) return false;

        A.load(); W.map.generate();

        // Seed player HQ, a worker, a resource node, and enemy HQ

        W.structures.push_back( Structure{ Vec{100,100}, Team::Player, 600, "HQ", A.bmp["hq"] } );

        W.units.push_back( Unit{ Vec{140,140}, {}, Team::Player, 100, 8, 22, UnitState::Idle, {}, 0, 60, ResourceType::Wood, &W.structures.back(), nullptr, nullptr, A.bmp["unit"] } );

        W.nodes.push_back( ResourceNode{ Vec{320,200}, 600, ResourceType::Wood, A.bmp["tree"] } );

        W.structures.push_back( Structure{ Vec{1000,500}, Team::Enemy, 600, "HQ", A.bmp["hq"] } );

        return true;

    }

    // Right-click command resolution: move / gather / attack / build place

    void issueRightClick(Vec worldPx){

        // If clicking a resource → order gather for selected workers

        ResourceNode* rn=nullptr; for(auto& n:W.nodes) if(len(n.pos-worldPx)<20) { rn=&n; break; }

        Unit* enemy=nullptr; for(auto& u:W.units) if(u.team==Team::Enemy && len(u.pos-worldPx)<20) { enemy=&u; break; }

        for(auto& u:W.units) if(u.selected && u.team==Team::Player){

            if (rn){ u.st=UnitState::Gather; u.targetNode=rn; u.goal=rn->pos; }

            else if (enemy){ u.st=UnitState::Attack; u.targetEnemy=enemy; u.goal=enemy->pos; }

            else { u.st=UnitState::Move; u.goal=worldPx; }

        }

    }

    void update(float dt){

        if (input.quit) { W.gameOver=true; return; }

        enemyAI.update(W, dt);

        // Selection drag

        if (!input.dragging && input.lmb){ /* no-op */ }

        if (!input.lmb && input.dragging==false) { /* release happened */ }

        // Move units & run state machines

        for(auto& u:W.units){

            switch(u.st){

                case UnitState::Move:    mover.step(u, W.map, dt); break;

                case UnitState::Gather:  eco.update(u, W, dt); mover.step(u, W.map, dt); break;

                case UnitState::Return:  eco.update(u, W, dt); mover.step(u, W.map, dt); break;

                case UnitState::Attack:  combat.tick(u, W, dt); mover.step(u, W.map, dt); break;

                default: break;

            }

        }

        // Cleanup dead units (very basic)

        W.units.erase(std::remove_if(W.units.begin(), W.units.end(), [](const Unit& u){ return u.hp<=0; }), W.units.end());

    }

    void draw(){

        al_clear_to_color(al_map_rgb(25,35,25));

        W.map.draw(A, W.cam.x, W.cam.y);

        // Draw nodes

        for(auto& n:W.nodes) al_draw_filled_circle(n.pos.x-W.cam.x, n.pos.y-W.cam.y, 6, al_map_rgb(60,160,60));

        // Draw structures

        for(auto& s:W.structures) al_draw_filled_rectangle(s.pos.x-12-W.cam.x, s.pos.y-12-W.cam.y, s.pos.x+12-W.cam.x, s.pos.y+12-W.cam.y, s.team==Team::Player? al_map_rgb(70,120,255):al_map_rgb(220,70,70));

        // Draw units

        for(auto& u:W.units){

            ALLEGRO_COLOR c = (u.team==Team::Player)? al_map_rgb(120,200,255):al_map_rgb(255,120,120);

            al_draw_filled_circle(u.pos.x-W.cam.x, u.pos.y-W.cam.y, 6, c);

            if(u.selected) al_draw_circle(u.pos.x-W.cam.x, u.pos.y-W.cam.y, 9, al_map_rgb(255,255,0), 2);

        }

        // HUD

        std::string hud = "Wood: "+std::to_string(W.wood)+"  Stone: "+std::to_string(W.stone);

        al_draw_text(A.font["ui"], al_map_rgb(255,255,255), 10, 10, 0, hud.c_str());

        al_flip_display();

    }

    // Basic event pump: selection & commands

    void run(){

        bool selecting=false; Vec selStart;

        while(true){

            ALLEGRO_EVENT ev; al_wait_for_event(app.q, &ev);

            if (ev.type==ALLEGRO_EVENT_TIMER){ update(1.0f/60.0f); draw(); continue; }

            input.handle(ev);

            if (input.quit) break;

            if (ev.type==ALLEGRO_EVENT_MOUSE_BUTTON_DOWN && ev.mouse.button==1){

                selecting=true; selStart={ (float)ev.mouse.x,(float)ev.mouse.y };

            }

            if (ev.type==ALLEGRO_EVENT_MOUSE_BUTTON_UP && ev.mouse.button==1){

                selecting=false;

                sel.boxSelect(W, selStart, input.mouse, W.cam.x, W.cam.y);

            }

            if (ev.type==ALLEGRO_EVENT_MOUSE_BUTTON_UP && ev.mouse.button==2){

                Vec worldPx = input.mouse + Vec{W.cam.x, W.cam.y};

                issueRightClick(worldPx);

            }

            // keyboard quick build (B to start placing; Enter to confirm)

            if (ev.type==ALLEGRO_EVENT_KEY_DOWN){

                if (ev.keyboard.keycode==ALLEGRO_KEY_B){ build.placing=true; build.kind="Barracks"; }

                if (ev.keyboard.keycode==ALLEGRO_KEY_ENTER){ build.confirm(W, A); }

            }

            if (build.placing && ev.type==ALLEGRO_EVENT_MOUSE_AXES){

                build.ghostPx = input.mouse + Vec{W.cam.x, W.cam.y};

            }

        }

    }

};

int main(){ Game g; if(!g.boot()) return 1; g.run(); g.app.shutdown(); return 0; }

Tasks for Completion and Improvement

  1. 1. Resource Gathering: Implement logic for units to gather resources and bring them back to structures.
  2. 2. Building Structures: Allow the player to build new structures using gathered resources.
  3. 3. Combat Mechanics: Implement combat between player units and enemy units.
  4. 4. AI Behavior: Improve enemy AI to make the game more challenging.
  5. 5. User Interface: Add a UI to display resources, unit health, and other game information

Exercises, Homework Questions, and Projects

Exercises

  1. 1. Understanding Selection and Commands

    Explain how box selection differs from single-click selection in an RTS. Why is it important to support both, and how does modifier-key selection (such as Shift-click) improve usability?

  2. 2. Coordinate Systems in RTS Games

    Describe the difference between screen coordinates and world coordinates. Why is accurate conversion between these coordinate spaces essential for unit selection, movement commands, and structure placement?

  3. 3. Finite-State Unit Behavior

    Review the UnitState enumeration. For each state listed (Idle, Move, Gather, Return, Attack, Build), briefly describe what conditions cause a unit to enter or leave that state.

  4. 4. Economy Flow Analysis

    Trace the full lifecycle of a resource-gathering unit from locating a resource node to depositing resources at a structure. Identify which parts of the code manage each step of this process.

Homework Questions

  1. 1. RTS Complexity vs. Turn-Based Games

    Compare the challenges of implementing an RTS with those of a turn-based strategy game. Which systems become more complex in real time, and why?

  2. 2. Pathfinding Trade-Offs

    Discuss the limitations of the current “greedy steering” movement approach. Under what conditions does this approach fail, and how does grid-based A* pathfinding address those shortcomings?

  3. 3. User Interface and Cognitive Load

    RTS games present a large amount of information to players. What UI elements are most critical for preventing player confusion, and how should information priority be determined?

  4. 4. AI Design Considerations

    Why is it often preferable for enemy AI in strategy games to be predictable but efficient rather than optimal? How does this affect player enjoyment and perceived difficulty?

Programming Projects

  1. 1. Resource Gathering System (Core Project)

    Complete and refine the resource-gathering logic so that units correctly locate resources, gather them over time, return them to deposits, and resume gathering if resources remain. Add visual feedback for carrying resources.

  2. 2. Structure Construction System

    Extend the building and placement system to support multiple structure types. Implement construction progress indicators and prevent overlapping or invalid placement more robustly.

  3. 3. Combat Mechanics Expansion

    Enhance combat by adding armor or damage mitigation, ranged versus melee attacks, and visual attack indicators. Ensure combat resolves consistently for both player and enemy units.

  4. 4. Improved Enemy AI

    Expand the enemy AI beyond basic harassment. Add decision-making logic that allows the AI to choose between economic expansion, defense, or aggression based on game state.

  5. 5. User Interface and HUD Enhancements

    Build a richer HUD that displays selected unit statistics, health bars, resource counts, and active production queues. Optionally add a minimap with click-to-pan functionality.

  6. 6. Fog of War Implementation (Advanced)

    Implement a fog-of-war system that tracks explored versus visible areas. Mask terrain and units outside current vision and update visibility dynamically as units move.

  7. 7. Save and Load System (Advanced)

    Create a save/load feature that serializes the game state, including map layout, resources, units, structures, and AI state. Restore the game accurately from saved data.

8.11. Utilities Game

Utilities games are a unique category of software that blends gaming elements with practical tools to enhance productivity, creativity, or system performance. These games often provide a fun and interactive way to accomplish tasks or improve your workflow.

For this project, we will create a basic focus timer game that helps users manage their work sessions and breaks. The game will have a timer that counts down, and the user can start, pause, and reset the timer.

8.11.1. Game Design

Game Concept

  • Genre: Utilities game (focus timer).
  • Objective: The user sets a timer for work sessions and breaks to improve productivity.

Core Components

  • Timer: The core functionality of the application
    • Attributes: Total time, remaining time, state (stopped, running, paused)
    • Function: Counts down accurately based on elapsed time
  • Control Buttons: User interaction elements
    • Types: Start, Pause, Reset, Skip
    • Function: Control timer state transitions
  • Display: Visual output of timer state
    • Attributes: Position, font, colour
    • Function: Shows remaining time in MM:SS format
  • Session Tracking System: Light gamification element
    • Attributes: Completed sessions, total focus time
    • Function: Encourages consistent use
  • Persistence System: Stores user data
    • Content: Durations, preferences, session statistics
    • Purpose: Maintains continuity across application sessions

Key Techniques and Features to Have

Designing a focus timer as a utility-style game combines accurate timekeeping, light gamification, and a clean, reliable user interface. While mechanically simple, correctness and robustness are essential for user trust. The following features define a solid, extensible baseline for a focus timer implemented with Allegro:

  1. 1. Accurate Timekeeping and Drift Control

    The timer should compute remaining time using absolute elapsed time (via al_get_time()), not frame counts, to prevent drift. Pause and resume behavior should be handled by tracking cumulative paused duration so the countdown always reflects real elapsed time.

  2. 2. Clear State Machine

    Timer behavior should be governed by an explicit state machine with states such as stopped, running, paused, and completed. Optional Pomodoro cycling (work and break phases) can be layered on top. Clear state transitions simplify logic and reduce errors.

  3. 3. Configurable Durations and Presets

    Users should be able to customize work and break durations and save common presets: for example, 25/5 (25 minutes of work with a 5-minute break) or 50/10. Quick-set buttons for common intervals enable fast session starts with minimal interaction.

  4. 4. Responsive UI Components

    The interface should offer responsive controls for starting, pausing, resetting, and skipping sessions, with visual feedback for hover and press states. Keyboard shortcuts should mirror common actions. The display should include a clear MM:SS readout and a visible progress indicator.

  5. 5. Nonintrusive Notifications

    Session completion should be indicated through subtle audio or visual cues that do not disrupt focus. Users should control volume and muting. Optional desktop notifications may be used when the app is not in focus.

  6. 6. Session Tracking and Light Gamification

    The timer may track completed sessions, total focus time, and usage streaks. Optional achievements or badges can motivate continued use without distracting from the tool’s primary purpose.

  7. 7. Persistence of Settings and Statistics

    User preferences (durations, theme, audio) and usage statistics should persist across runs. Storing this data in a small external configuration file ensures continuity and reliability.

  8. 8. Themes and Accessibility

    The application should support light and dark themes and colour-blind-friendly palettes. Accessibility features such as large text, keyboard-only navigation, and visible focus indicators broaden usability.

  9. 9. Robust Handling of Edge Cases

    The timer should remain accurate if the window loses focus, the system sleeps, or rendering pauses. Remaining time must never underflow, and transitions into the completed state should be clear and deterministic.

8.11.2. Implementation

Key Operations in the Game

  1. 1. Event Intake (Mouse/Keyboard) and Intent Mapping
    • • Poll Allegro events each frame.
    • • Map input to actions: Start/Pause, Reset, Skip, Preset Selection, Theme Toggle, Mute.
    • • Support keyboard shortcuts (e.g., space bar = toggle run/pause, R = reset, M = mute).
  2. 2. Timebase Update (Drift-Resistant Countdown)
    • • Maintain sessionStartTime, pausedAccumulated, and lastTick.
    • • Per timer tick: elapsed = al_get_time() − sessionStartTime − pausedAccumulated; remaining = totalTime − elapsed;
    • • When pausing, capture pauseStart; when resuming, add to pausedAccumulated.
  3. 3. State Transitions
    • • Stopped → Running: Initialize timestamps. (Optional: Play start cue.)
    • • Running → Paused: Freeze countdown by recording pauseStart.
    • • Paused → Running: Adjust pausedAccumulated.
    • • Running → Completed: When remaining ≤ 0, clamp to 0, set Completed, trigger SFX/flash. (Optional: Advance to the next phase [break/work] if Pomodoro mode is enabled.)
  4. 4. Button Hit-Testing and UI Feedback
    • • On mouse down, test cursor against button rectangles; set pressed state.
    • • On mouse up, if still inside the same button, fire the action; update hover/pressed visuals.
  5. 5. Time Formatting and Progress Visualization
    • • Convert seconds to MM:SS (zero-padded).
    • • Compute progress ratio p = (totalTime − remaining)/totalTime for a bar or ring.
    • • Animate subtle easing on the progress indicator for polish.
  6. 6. Rendering Pass (per Frame)
    • • Clear background (theme colour).
    • • Draw time read-out, progress bar/ring, and buttons with current states.
    • • Draw status text (Running/Paused/Break/Completed). (Optional: Draw session counters.)
  7. 7. Audio/Notification Triggers
    • • On start, pause, resume, complete, and play appropriate SFX if enabled.
    • • Optional: On complete, display an overlay or desktop notification.
  8. 8. Persistence (Load/Save)
    • • On start-up, load preferences (durations, theme, audio), and stats (sessions, total minutes).
    • • On completion/reset/settings change, save back to disk.
  9. 9. Edge-Case Handling
    • • If the app regains focus or the system clock jumps, recompute remaining from absolute time.
    • • Ensure timer cannot underflow; clamp and transition once.
  10. 10. Optional:Pomodoro Cycle Manager
    • • Maintain a small state machine: Work → Break → Work → . . . → Long Break after N cycles.
    • • Auto-start the next phase (configurable) or wait for a Start click.

Structure of the Game Program

  1. 1. Initialization
    • • Initializes Allegro and its add-ons and installs keyboard and mouse input.
  2. 2. Definition of Timer
    • • A structure to keep track of the total time, remaining time, and running state.
  3. 3. Definition of Buttons
    • • Structures to define the start, pause, and reset buttons.
  4. 4. Definition of Drawing Functions
    • • Functions to draw the timer and buttons on the screen.
  5. 5. Definition of Input Handling
    • • Captures mouse input to start, pause, and reset the timer.
  6. 6. Game Loop
    • • Runs the main game loop to update and render the game.
  7. 7. Shutdown
    • • Clean release of Allegro resourcesFinishing

Coding the Game

focus_timer.cpp

#include <allegro5/allegro.h>

#include <allegro5/allegro_font.h>

#include <allegro5/allegro_ttf.h>

#include <allegro5/allegro_primitives.h>

#include <iostream>

#include <cstdio>

const int SCREEN_WIDTH  = 800;

const int SCREEN_HEIGHT = 600;

struct Timer {

    float totalTime;

    float remainingTime;

    bool running;

};

struct Button {

    float x, y, width, height;

    const char* label;

};

Timer timer = {1500.0f, 1500.0f, false}; // 25 minutes

Button startButton = {100, 500, 100, 50, "Start"};

Button pauseButton = {250, 500, 100, 50, "Pause"};

Button resetButton = {400, 500, 100, 50, "Reset"};

bool initAllegro() {

    if (!al_init()) return false;

    al_init_font_addon();

    al_init_ttf_addon();

    if (!al_install_keyboard()) return false;

    if (!al_install_mouse()) return false;

    if (!al_init_primitives_addon()) return false; // needed for draw_filled_rectangle

    return true;

}

bool isButtonClicked(const Button& button, int mx, int my) {

    return mx >= button.x && mx <= button.x + button.width &&

           my >= button.y && my <= button.y + button.height;

}

void drawButton(const Button& button, ALLEGRO_FONT* font) {

    // button body + border

    al_draw_filled_rectangle(button.x, button.y,

                             button.x + button.width, button.y + button.height,

                             al_map_rgb(200, 200, 200));

    al_draw_rectangle(button.x, button.y,

                      button.x + button.width, button.y + button.height,

                      al_map_rgb(0, 0, 0), 2.0f);

    // center label

    int th = al_get_font_line_height(font);

    float cx = button.x + button.width  / 2.0f;

    float cy = button.y + button.height / 2.0f—th / 2.0f;

    al_draw_text(font, al_map_rgb(0, 0, 0), cx, cy, ALLEGRO_ALIGN_CENTER, button.label);

}

void updateTimer(Timer& t, float dt) {

    if (t.running && t.remainingTime > 0.0f) {

        t.remainingTime—= dt;

        if (t.remainingTime <= 0.0f) {

            t.remainingTime = 0.0f;

            t.running = false;

            // (Optional) play a sound or flash the screen here.

        }

    }

}

void drawTimer(const Timer& t, ALLEGRO_FONT* font) {

    int total = (t.remainingTime > 0.0f) ? static_cast<int>(t.remainingTime) : 0;

    int minutes = total / 60;

    int seconds = total % 60;

    char timeStr[6];

    std::snprintf(timeStr, sizeof(timeStr), "%02d:%02d", minutes, seconds);

    al_draw_text(font, al_map_rgb(0, 0, 0), SCREEN_WIDTH / 2.0f, SCREEN_HEIGHT / 2.0f—al_get_font_line_height(font)/2.0f, ALLEGRO_ALIGN_CENTER, timeStr);

}

int main() {

    if (!initAllegro()) {

        std::cerr << "Failed to initialize Allegro.\n";

        return—1;

    }

    ALLEGRO_DISPLAY* display = al_create_display(SCREEN_WIDTH, SCREEN_HEIGHT);

    if (!display) { std::cerr << "Display creation failed.\n"; return—1; }

    ALLEGRO_EVENT_QUEUE* queue = al_create_event_queue();

    ALLEGRO_TIMER* ticker = al_create_timer(1.0 / 60.0);

    if (!queue || !ticker) {

        std::cerr << "Queue/timer creation failed.\n";

        if (ticker) al_destroy_timer(ticker);

        if (queue) al_destroy_event_queue(queue);

        al_destroy_display(display);

        return—1;

    }

    ALLEGRO_FONT* font = al_load_ttf_font("arial.ttf", 32, 0);

    if (!font) { // graceful fallback

        font = al_create_builtin_font();

        if (!font) { std::cerr << "Font creation failed.\n"; return—1; }

    }

    al_register_event_source(queue, al_get_display_event_source(display));

    al_register_event_source(queue, al_get_timer_event_source(ticker));

    al_register_event_source(queue, al_get_mouse_event_source());

    al_start_timer(ticker);

    bool running = true;

    while (running) {

        ALLEGRO_EVENT ev;

        al_wait_for_event(queue, &ev);

        if (ev.type == ALLEGRO_EVENT_DISPLAY_CLOSE) {

            running = false;

        } else if (ev.type == ALLEGRO_EVENT_TIMER) {

            updateTimer(timer, 1.0f / 60.0f);

            al_clear_to_color(al_map_rgb(255, 255, 255));

            drawTimer(timer, font);

            drawButton(startButton, font);

            drawButton(pauseButton, font);

            drawButton(resetButton, font);

            al_flip_display();

        } else if (ev.type == ALLEGRO_EVENT_MOUSE_BUTTON_DOWN) {

            if (isButtonClicked(startButton, ev.mouse.x, ev.mouse.y)) {

                timer.running = true;

            } else if (isButtonClicked(pauseButton, ev.mouse.x, ev.mouse.y)) {

                timer.running = false;

            } else if (isButtonClicked(resetButton, ev.mouse.x, ev.mouse.y)) {

                timer.remainingTime = timer.totalTime;

                timer.running = false;

            }

        }

    }

    if (font) al_destroy_font(font);

    if (ticker) al_destroy_timer(ticker);

    if (queue) al_destroy_event_queue(queue);

    if (display) al_destroy_display(display);

    return 0;

}

Tasks for Completion and Improvement

  1. 1. Customizable Timer: Allow the user to set custom work and break durations.
  2. 2. Sound Alerts: Add sound effects to notify the user when the timer ends.
  3. 3. Visual Themes: Add different visual themes to make the game more appealing, load a background image, or modify the font and sizes used for the buttons and time display.
  4. 4. Statistics: Track and display statistics such as total work time and number of sessions completed.

Exercises, Homework Questions, and Projects

Exercises

  1. 1. Understanding Timekeeping Accuracy

    Explain why computing the remaining time using al_get_time() is more reliable than decrementing a counter each frame. Describe a scenario in which frame-based updates could cause noticeable timer drift.

  2. 2. Timer State Transitions

    Draw a state diagram showing the transitions between the stopped, running, paused, and completed states of the timer. Indicate which user inputs or conditions cause each transition.

  3. 3. UI Interaction Analysis

    Review the button-handling code. Describe how hit-testing works and explain why separating input detection (isButtonClicked) from rendering (drawButton) is beneficial.

Homework Questions

  1. 1. Design Robustness

    Consider what happens if the application window loses focus or the system temporarily freezes. How would you modify the current implementation to ensure the timer remains accurate when the application resumes?

  2. 2. State Machines in Utility Games

    Compare the timer’s state machine to those used in traditional games (such as menus or combat systems). What similarities and differences do you observe?

  3. 3. Persistence and User Trust

    Why is saving user preferences and statistics important in a utilities game? What expectations might users have if this data were lost between sessions?

Programming Projects

  1. 1. Customizable Timer Durations (Core Project)

    Extend the timer so users can define custom work and break durations. Add UI elements (buttons or keyboard shortcuts) to select common presets (e.g., 15, 25, 45 minutes) and apply them at runtime.

  2. 2. Sound Alerts and Notifications

    Integrate Allegro’s audio subsystem to play a sound when the timer reaches zero. Add a mute toggle and allow users to control alert volume. Optionally, implement a brief visual flash effect when a session is completed.

  3. 3. Session Statistics and History Tracking

    Track additional statistics such as total accumulated work time, number of completed sessions, and daily streaks. Display these statistics in a simple summary panel or overlay.

  4. 4. Persistent Configuration File (Advanced)

    Store user preferences and statistics in an external configuration file (such as JSON or INI format). Load this file at start-up and save changes when the application exits or settings are modified.

  5. 5. Accessibility Enhancements (Optional)

    Add accessibility features such as a large-text mode, keyboard-only navigation, and high-contrast colour options. Explain how each modification improves usability.

Annotate

Next Chapter
References and Resources
PreviousNext
This work is licensed under a Creative Commons License (CC BY-NC-SA 4.0), except where otherwise noted. This license allows users to copy and redistribute the material in any medium or format and to remix, transform, and build upon the material as long as the original source is properly credited, the work is not used for commercial purposes, and the new creation is licensed under the same terms.
Powered by Manifold Scholarship. Learn more at
Opens in new tab or windowmanifoldapp.org