Skip to content

lynx1097/Terminal-Platformer

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 

History

9 Commits
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

Warning

This Game was originally implemented and compiled in Pure C++ (cmake/clang) and later was transcompiled to web assembly so it may not run perfectly as native in the browser version .

๐Ÿ’ฃ DEMOLITION RUN

A grid-based terminal roguelite โ€” written in C++, compiled to WebAssembly, and played in the browser.


โ–ถ  Play in Browser




๐Ÿ“– About

Demolition Run is a 15 ร— 15 grid roguelite rendered entirely in ANSI-escaped terminal art. Navigate the battlefield, collect ammo and potions, engage monsters in melee and ranged combat, and detonate weapon specials to clear the map โ€” all inside a terminal (or browser tab).

The game was developed from scratch in C++ for the Programming III course at the German International University (GIU), then compiled to WebAssembly via Emscripten and wired to a browser terminal using xterm.js โ€” no server, no plugins, no installation. Just a single HTML file and a .wasm binary.


๐Ÿ–ผ๏ธ Screenshots

Hero & Weapon Selection

Grid Combat & Exploration


๐ŸŽฎ Game Overview

Heroes

Hero Symbol Special ย X
Demolition Hero @ (cyan) Carries two weapons simultaneously and hot-swaps between them
Medic Hero @ (green) Single-use self-heal restoring +50 HP

Weapons

Weapon Bullets Damage Range Special ย G
Pistol 16 30 2 โ€”
Bazooka 4 150 8 Diagonal blast โ€” sweeps all cells in an X pattern across the full grid
Rifle 30 20 4 โ€”

Monsters

Monster Symbol Behaviour
Tank Monster โ–ฒ Armoured โ€” all incoming damage is first absorbed by a shield layer before HP is reduced
Ghost Monster โ—† Phases out every 3rd round โ€” becomes invisible on the map but still occupies its cell

Items

Item Symbol Effect
Potion โ™ฅ Restores 10 โ€“ 50 HP on pickup
Ammo โŠ• Adds 5 โ€“ 20 bullets to the current weapon

๐Ÿ•น๏ธ Controls

Action Input
Move W A S D
Enter firing mode F
Aim & fire F โ†’ W / A / S / D
Hero special ability X
Gun special ability G
Quit Q

Combat tip โ€” stepping into a monster's cell triggers melee. The hero deals weapon damage (reduced by any shield), while the monster retaliates for a flat 10 HP. Ranged fire costs one bullet and stops at the first monster hit in line.


๐Ÿ—๏ธ Object-Oriented Architecture

The entire game entity system is built on a single inheritance hierarchy rooted at Object. Every cell on the 15 ร— 15 grid stores an Object*, allowing heroes, monsters, items, and empty tiles to coexist in the same array and be queried polymorphically.

Object
โ”œโ”€โ”€ Item
โ”‚   โ”œโ”€โ”€ Potion
โ”‚   โ””โ”€โ”€ Ammo
โ””โ”€โ”€ Character
    โ”œโ”€โ”€ Hero
    โ”‚   โ”œโ”€โ”€ DemolitionHero
    โ”‚   โ””โ”€โ”€ MedicHero
    โ””โ”€โ”€ Monster
        โ”œโ”€โ”€ TankMonster
        โ””โ”€โ”€ GhostMonster

Gun  (independent hierarchy)
โ”œโ”€โ”€ Pistol
โ”œโ”€โ”€ Bazooka
โ””โ”€โ”€ Rifle

The Gun hierarchy is kept deliberately separate from Object โ€” guns are owned resources of heroes, not grid entities, so they live outside the map and are managed through raw pointer members on the Hero class.


๐Ÿง  C++ Skills Demonstrated

๐Ÿ“ฆ ย Inheritance & Polymorphism

The project features a multi-level inheritance chain across two independent hierarchies (Object and Gun). Virtual methods are declared at the base and selectively overridden at each level, allowing the game loop to drive all entities through base-class pointers without ever knowing the concrete type.

// Gun base โ€” empty default body acts as a soft interface
virtual void useSpecial(Hero& h, Object* grid[15][15]) {}

// Bazooka override โ€” full diagonal grid sweep
void useSpecial(Hero& h, Object* grid[15][15]) override {
    // sweeps northeast, northwest, southwest, southeast
}

getInfo() is declared virtual on Hero and overridden by both DemolitionHero and MedicHero to print hero-type-specific state โ€” weapon slots, healing amount โ€” through the same base pointer:

friend ostream& operator<<(ostream& os, Hero& h) {
    return h.getInfo(os); // dispatches to the correct subclass at runtime
}
๐Ÿ”’ ย Encapsulation

All internal state is kept private or protected, exposed only through narrow getter/setter interfaces. No external code ever touches a field directly:

class Gun {
private:
    string name;
    int bullets, damage, range;
public:
    int  getBullets()  const { return bullets; }
    void setBullets(int b)   { bullets = b;    }
    int  getDamage()   const { return damage;  }
    int  getRange()    const { return range;   }
};

TankMonster encapsulates its shield value behind getShield() / setShield(), so the combat logic in Hero::operator- can reduce the shield without ever accessing the raw field:

int absorbed = min(dmg, tank.getShield());
tank.setShield(tank.getShield() - absorbed);
dmg -= absorbed;
๐Ÿ”ง ย Constructors & Member Initializer Lists

Every class initializes its members through constructor chaining via initializer lists, delegating up the hierarchy cleanly rather than assigning in the body:

// Ammo โ†’ Item โ†’ Object: full chain via initializer lists
Ammo(int b) : Item('A'), bullets(b) {}

// GhostMonster โ†’ Monster โ†’ Character โ†’ Object
GhostMonster(char t, int x, int y, int hp)
    : Monster(t, x, y, hp), isVisible(true) {}

// DemolitionHero โ†’ Hero โ†’ Character โ†’ Object
DemolitionHero(char t, int x, int y, int hp)
    : Hero(t, x, y, hp), weapon2(nullptr) {}

This guarantees that base sub-objects are fully constructed before the derived constructor body runs โ€” a critical correctness property when base classes hold virtual dispatch tables.

๐Ÿ’ฅ ย Destructors & Resource Cleanup

Every class that owns heap-allocated resources defines an explicit virtual destructor to ensure the correct cleanup chain fires when an object is deleted through a base pointer.

Hero frees both weapon slots, with a guard to prevent double-deletion when current and weapon point to the same object:

virtual ~Hero() {
    if (weapon != nullptr) delete weapon;
    if (current != nullptr && current != weapon) delete current;
}

DemolitionHero extends this to also release the second weapon slot it exclusively owns:

~DemolitionHero() {
    if (weapon2 != nullptr) delete weapon2;
}

Game's destructor performs the most delicate teardown. Because the hero pointer lives both as a standalone member and as a raw pointer inside the grid, its cell is explicitly nulled before the grid sweep to prevent a double-free:

~Game() {
    if (hero != nullptr)
        grid[hero->getPositionX()][hero->getPositionY()] = nullptr;
    delete hero;
    for (int i = 0; i < rows; i++)
        for (int j = 0; j < cols; j++)
            delete grid[i][j];  // safe โ€” hero's cell is already nullptr
}
๐Ÿ–Š๏ธ ย Raw Pointers & Dynamic Memory

The entire map is a raw Object* 2D array โ€” no wrappers, no containers:

Object* grid[15][15];

Every entity on the map is heap-allocated. When the hero moves, an item is picked up, or a monster dies, the old object is explicitly freed and replaced:

delete grid[rx][ry];
grid[rx][ry] = new TankMonster('T', 0, 0, hp, hp / 2);

Runtime type identification uses the lightweight getType() character tag, followed by a C-style downcast to the concrete type โ€” a direct, zero-overhead approach appropriate for a performance-critical game loop:

if (grid[x][y]->getType() == 'T') {
    TankMonster& tank = (TankMonster&)(*grid[x][y]);
    tank.setShield(tank.getShield() - absorbed);
}

Pointer ownership is explicit and enforced by convention: the Game object owns the grid, Hero owns its guns, and DemolitionHero owns the additional weapon slot. There is no shared ownership โ€” each pointer has exactly one owner responsible for its deletion.

๐Ÿ‘ฅ ย Friend Functions & Operator Overloading

Non-member operator<< overloads are declared friend to read private fields and produce formatted output without breaking encapsulation:

// Prints: Name, Range, Bullets, Damage
friend ostream& operator<<(ostream& os, Gun& g);

The three gameplay interaction operators are also friend free functions, keeping the call-site syntax expressive and readable:

*hero + potion;   // HP restored         โ†’ operator+(Hero&, const Potion&)
*hero + ammo;     // Bullets refilled    โ†’ operator+(Hero&, const Ammo&)
*hero - monster;  // Melee combat round  โ†’ operator-(Hero&, Monster&)

The subtraction operator encapsulates the full melee resolution โ€” hero damage, shield absorption, and monster health reduction โ€” in a single, intention-revealing expression.


๐ŸŒ Web Deployment Pipeline

Bringing a raw C++ terminal game to the browser without rewriting any game logic required three composable layers:

  C++ Source (.h / .cpp)
        โ”‚
        โ”‚  emcc (Emscripten compiler)
        โ–ผ
  game.wasm  +  game.js  (Emscripten module loader)
        โ”‚
        โ”‚  xterm.js  (ANSI terminal emulator in <canvas>)
        โ”‚  ccall()   (JS โ†’ WASM keystroke bridge)
        โ–ผ
  index.html  โ”€โ”€โ”€โ”€โ”€โ–ถ  Browser  (zero install, zero server)

1 ยท Emscripten โ†’ WebAssembly

Emscripten compiles the C++ source to a .wasm binary paired with a game.js bootstrapper. Crucially, Emscripten's in-browser virtual filesystem and TTY layer intercept stdout/stderr at the byte level โ€” meaning every cout statement in C++ routes directly to the browser terminal, character by character, with no code changes.

2 ยท xterm.js โ€” ANSI Terminal in the Browser

The game's TUI is built purely with ANSI escape sequences: 24-bit RGB color, cursor positioning, alternate screen mode, and Unicode box-drawing characters. xterm.js is the only browser terminal emulator that faithfully renders all of these in a <canvas> element, producing output visually identical to a native terminal.

3 ยท Input Bridge โ€” ccall()

A custom C function sendInputToGame is exported from the WASM module and exposed to JavaScript. xterm.js captures every keypress and forwards it over Emscripten's ccall() bridge, giving the game single-character raw input โ€” exactly what the native getch() call would deliver:

term.onData(function(data) {
    Module.ccall('sendInputToGame', 'null', ['number'], [data.charCodeAt(0)]);
});

4 ยท UTF-8 Safe Output

The raw byte stream from C++ is decoded with a streaming TextDecoder before being written to the terminal. Using { stream: true } ensures that multi-byte UTF-8 sequences spanning multiple put_char calls โ€” such as the Unicode box-drawing and symbol characters used throughout the UI โ€” are decoded correctly and never corrupted:

const utf8Decoder = new TextDecoder('utf-8');

// Inside Emscripten's stdout hook:
term.write(utf8Decoder.decode(new Uint8Array([charData]), { stream: true }));

๐Ÿ“š Course Context

Programming III (C++) โ€” German International University (GIU)
Final course project demonstrating applied object-oriented design, manual memory management, and polymorphic program architecture in C++.


Built with terminal art, raw pointers, and a healthy fear of memory leaks.

About

A C++ Terminal game

Resources

Stars

0 stars

Watchers

0 watching

Forks

Contributors