Game States, Game Over, and Restart Logic

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.



