Skip to main content

Command Palette

Search for a command to run...

Game States, Game Over, and Restart Logic

Updated
6 min read
Game States, Game Over, and Restart Logic
D

An engineer with a curious mind and a love for systems that go beyond the surface.I enjoy exploring how things work under the hood — whether it's rendering pixels, routing audio, or handling low-level protocols. My work spans across game development, system programming, and occasionally web tech when the problem is worth solving. I’m driven by the desire to build things that are fast, efficient, and just a bit unconventional. This blog is my space to document experiments, challenges, and the weirdly satisfying bugs that teach you more than tutorials ever could.

Outside of tech, I enjoy observing how people use technology, and occasionally breaking things just to fix them better.

So far, our game only has one mode: running.

That’s fine for experiments, but real games don’t work like that.

A real game has states:

  • a start screen

  • a playing state

  • an ending state

In this part, we’ll introduce game states and finally handle:

  • game over conditions

  • restarting the game

  • clean transitions between screens

What Is a Game State?

A game state is just a variable that describes what the game is currently doing.

Instead of asking:

“Is the game running?”

We ask:

“Which part of the game are we in right now?”

Defining Game States

We’ll use an enum for clarity.

enum GameState {
  STATE_HOME,
  STATE_PLAYING,
  STATE_GAME_OVER
};

And a variable to track the current state:

GameState state = STATE_HOME;

This single variable will control:

  • what logic runs

  • what gets rendered

  • how input is handled

Updating the Main Loop Structure

Instead of one big loop doing everything, we’ll branch based on the state.

Inside the main loop:

switch (state) {
  case STATE_HOME:
    // show start screen
    break;

  case STATE_PLAYING:
    // normal game logic
    break;

  case STATE_GAME_OVER:
    // show game over screen
    break;
}

Same loop. Same timing.
Just better organization.

Home Screen (Start Screen)

The home screen is simple.

It should:

  • show the game title

  • explain controls

  • wait for player input

Example logic:

if (ch == '\n') {
  state = STATE_PLAYING;
}

No gameplay happens here.
Just input and rendering.

Detecting Game Over

Now for the important rule we postponed earlier.

If any enemy reaches the ground, the game is over.

Since the cannon sits at playerY, we treat that row as the ground.

During the playing state:

for (const auto &enemy : enemies) {
  if (enemy.y >= playerY) {
    state = STATE_GAME_OVER;
    break;
  }
}

Game Over Screen

The game over screen should:

  • show “Game Over”

  • display the final score

  • explain how to restart or quit

Example:

mvprintw(maxY / 2,     maxX / 2 - 5, "GAME OVER");
mvprintw(maxY / 2 + 1, maxX / 2 - 4, "Score: %d", score);
mvprintw(maxY / 2 + 3, maxX / 2 - 9, "Press R to restart");
mvprintw(maxY / 2 + 4, maxX / 2 - 8, "Press Q to quit");

No gameplay updates happen in this state.

Restarting the Game

Restarting is just resetting state.

When the player presses R:

bullets.clear();
enemies.clear();

playerX = maxX / 2;
score = 0;

spawnCounter = 0;
frameCount = 0;

state = STATE_PLAYING;

Same loop.
Fresh game.

Full Code

Important note for the reader:
Most of this code is familiar from previous parts. The new idea here is game states and how the game switches between them.

#include <ncurses.h>
#include <chrono>
#include <thread>
#include <vector>
#include <cstdlib>

// --------------------------------------------------
// Data structures
// --------------------------------------------------

struct Bullet {
  int x;
  int y;
};

struct Enemy {
  int x;
  int y;
};

// --------------------------------------------------
// Game states
// --------------------------------------------------

enum GameState {
  STATE_HOME,
  STATE_PLAYING,
  STATE_GAME_OVER
};

// --------------------------------------------------
// Constants
// --------------------------------------------------

const int FPS = 60;
const int FRAME_TIME = 1000 / FPS;

const int SPAWN_DELAY = 60;        // frames
const int INVERSE_ENEMY_SPEED = 10; // higher = slower enemies

// --------------------------------------------------
// Helper functions
// --------------------------------------------------

void setupNcurses() {
  initscr();
  cbreak();
  noecho();
  nodelay(stdscr, TRUE);
  keypad(stdscr, TRUE);
  curs_set(0);
}

void resetGame(
  int &playerX,
  int playerY,
  int maxX,
  std::vector<Bullet> &bullets,
  std::vector<Enemy> &enemies,
  int &score,
  int &spawnCounter,
  int &frameCount
) {
  playerX = maxX / 2;
  bullets.clear();
  enemies.clear();
  score = 0;
  spawnCounter = 0;
  frameCount = 0;
}

// --------------------------------------------------
// Main
// --------------------------------------------------

int main() {
  setupNcurses();

  int maxY, maxX;
  getmaxyx(stdscr, maxY, maxX);

  // Player position
  int playerX = maxX / 2;
  int playerY = maxY - 2;

  // Game objects
  std::vector<Bullet> bullets;
  std::vector<Enemy> enemies;

  // Game state
  GameState state = STATE_HOME;
  bool running = true;

  // Game variables
  int score = 0;
  int spawnCounter = 0;
  int frameCount = 0;

  // --------------------------------------------------
  // Main loop
  // --------------------------------------------------

  while (running) {
    auto frameStart = std::chrono::high_resolution_clock::now();
    int ch = getch();

    clear();

    // ==================================================
    // HOME STATE
    // ==================================================
    if (state == STATE_HOME) {
      mvprintw(maxY / 2 - 1, maxX / 2 - 8, "TERMINAL INVADERS");
      mvprintw(maxY / 2 + 1, maxX / 2 - 10, "Press ENTER to start");
      mvprintw(maxY / 2 + 2, maxX / 2 - 7, "Press Q to quit");

      if (ch == '\n') {
        resetGame(playerX, playerY, maxX, bullets, enemies,
                  score, spawnCounter, frameCount);
        state = STATE_PLAYING;
      }

      if (ch == 'q' || ch == 'Q') {
        running = false;
      }
    }

    // ==================================================
    // PLAYING STATE
    // ==================================================
    else if (state == STATE_PLAYING) {
      // -------- Input --------
      switch (ch) {
        case 'q':
        case 'Q':
          running = false;
          break;

        case KEY_LEFT:
          playerX--;
          break;

        case KEY_RIGHT:
          playerX++;
          break;

        case ' ':
          bullets.push_back({ playerX, playerY - 1 });
          break;

        default:
          break;
      }

      // -------- Bounds --------
      if (playerX < 0) playerX = 0;
      if (playerX >= maxX) playerX = maxX - 1;

      // -------- Spawn enemies --------
      spawnCounter++;
      if (spawnCounter >= SPAWN_DELAY) {
        enemies.push_back({ rand() % maxX, 1 });
        spawnCounter = 0;
      }

      // -------- Update bullets --------
      for (auto &b : bullets) {
        b.y--;
      }

      for (size_t i = 0; i < bullets.size(); ) {
        if (bullets[i].y < 0) {
          bullets.erase(bullets.begin() + i);
        } else {
          i++;
        }
      }

      // -------- Update enemies (slower) --------
      frameCount++;
      if (frameCount % INVERSE_ENEMY_SPEED == 0) {
        for (auto &e : enemies) {
          e.y++;
        }
      }

      // -------- Bullet–Enemy collision --------
      for (size_t i = 0; i < bullets.size(); ) {
        bool hit = false;

        for (size_t j = 0; j < enemies.size(); ) {
          if (bullets[i].x == enemies[j].x &&
              bullets[i].y == enemies[j].y) {
            bullets.erase(bullets.begin() + i);
            enemies.erase(enemies.begin() + j);
            score++;
            hit = true;
            break;
          } else {
            j++;
          }
        }

        if (!hit) i++;
      }

      // -------- Game over condition --------
      for (const auto &e : enemies) {
        if (e.y >= playerY) {
          state = STATE_GAME_OVER;
          break;
        }
      }

      // -------- Render --------
      mvaddch(playerY, playerX, 'A');

      for (const auto &b : bullets) {
        mvaddch(b.y, b.x, '|');
      }

      for (const auto &e : enemies) {
        mvaddch(e.y, e.x, 'V');
      }

      mvprintw(0, 0, "Score: %d", score);
      mvprintw(1, 0, "LEFT/RIGHT move | SPACE shoot | Q quit");
    }

    // ==================================================
    // GAME OVER STATE
    // ==================================================
    else if (state == STATE_GAME_OVER) {
      mvprintw(maxY / 2,     maxX / 2 - 5, "GAME OVER");
      mvprintw(maxY / 2 + 1, maxX / 2 - 4, "Score: %d", score);
      mvprintw(maxY / 2 + 3, maxX / 2 - 9, "Press R to restart");
      mvprintw(maxY / 2 + 4, maxX / 2 - 8, "Press Q to quit");

      if (ch == 'r' || ch == 'R') {
        resetGame(playerX, playerY, maxX, bullets, enemies,
                  score, spawnCounter, frameCount);
        state = STATE_PLAYING;
      }

      if (ch == 'q' || ch == 'Q') {
        running = false;
      }
    }

    refresh();

    // -------- FPS cap --------
    auto frameEnd = std::chrono::high_resolution_clock::now();
    auto elapsed =
      std::chrono::duration_cast<std::chrono::milliseconds>(frameEnd - frameStart).count();

    if (elapsed < FRAME_TIME) {
      std::this_thread::sleep_for(
        std::chrono::milliseconds(FRAME_TIME - elapsed));
    }
  }

  endwin();
  return 0;
}

The game might feel unplayable right now, that’s intentional. I want you to tweak the values and adjust speeds yourself.

What We’ve Achieved

At this point, the game has:

  • a start

  • gameplay

  • a losing condition

  • a restart path

What’s Next

In the next part, we’ll focus on difficulty scaling.

We’ll make the game:

  • start easy

  • gradually become harder

  • reward skill and survival

That’s where balancing comes in.