Lesson  in  Build a Snake Game with Ada and ANSI Magic

Game Loop and State Management

Build a proper game loop with state management and terminal control. Create custom enumeration types, implement state machines with case statements, and use ANSI escape codes for dynamic terminal rendering. Transform your program into a real-time game foundation.

Get Latest Code

🚀 Beginning of Lesson 3 — Two Options

Each lesson starts with a new machine, you need to pull the latest code first, then either resume your own progress or start from the official snapshot.

Clone your fork (replace <YOUR_GITHUB_USERNAME>):

git clone https://github.com/<YOUR_GITHUB_USERNAME>/snake-tutorial.git
cd ~/sandbox/snake-tutorial

Check out your saved progress to a branch:

git checkout -b lesson-03-work origin/lesson-02-work

✅ You're now ready to start Lesson 3 from your own progress.

🅱️ Option B — Start from Official Course Snapshot

Check out and push to your origin the starting point for Lesson 3 :

git remote add upstream https://github.com/GNAT-Academic-Program/snake-tutorial.git
git fetch upstream
git checkout -b lesson-03-work upstream/lesson-03-start
git push -u origin lesson-03-work

✅ You're now starting Lesson 3 from the clean official reference code.

Add the Loop

Let's create a game loop! This will handle our different gameplay states and keep the game running.

  1. Edit snake_game.adb to add a loop around the existing code
  2. Add loop structure around the Noki.Log call:
    loop
       Noki.Log ("Welcome to my Snake Game!");
    end loop;
    
  3. Add a delay to slow down the loop so we can see the output:
    • On the line after Noki.Log ("Welcome to my Snake Game!") add:
    delay 1.0;
    

Your complete code inside the Snake_Game procedure should look like this:

loop
   Noki.Log ("Welcome to my Snake Game!");
   delay 1.0;
end loop;

📋 Code Review - Complete snake_game.adb

Your complete file should look like this:

with Noki;

procedure Snake_Game is
begin
   loop
      Noki.Log ("Welcome to my Snake Game!");
      delay 1.0;
   end loop;
end Snake_Game;

Build and run:

cd ~/sandbox/snake-tutorial/noki/snake_game
alr build
bin/snake_game

If something goes wrong: If you don't see Build finished successfully in ..., re-check the instructions carefully and start again from step 1.

Expected result: Welcome to my Snake Game! appearing every second over and over.

Important

🤯 Heads Up! This program will run forever! Use Ctrl+C in the terminal to stop it.

Create a Custom Game State Type

Let's create our first user-defined Ada type! We'll use an enumeration to manage different game states.

  1. Add the type definition to the declarative part of the Snake_Game procedure:
    • After procedure Snake_Game is and before begin, add:
    type Game_State_T is (Welcome, Play, Game_Over, Undefined);
    Game_State : Game_State_T := Welcome;
    
  2. Understanding the syntax:
    • First line creates a new enumeration type Game_State_T with four possible values
    • Second line declares a variable Game_State of type Game_State_T, initialized to Welcome
    • Ada syntax: variable_name : variable_type := initial_value
  3. Replace the simple log with state handling:
    • Remove the Noki.Log ("Welcome to my Snake Game!"); line
    • Add a case statement between loop and end loop:
    case Game_State is
       when Welcome =>
          Noki.Log ("Welcome to Snakotron!");
          Game_State := Play;
       when Play =>
          Noki.Log ("We are playing.");
          Game_State := Game_Over;
       when Game_Over =>
          Noki.Log ("Game Over!");
          Game_State := Undefined;
       when others =>
          Noki.Log ("Press Ctrl+C to abort program!");
    end case;
    

📋 Code Review - Complete snake_game.adb

Your complete snake_game.adb should look exactly like this:

with Noki;

procedure Snake_Game is
   type Game_State_T is (Welcome, Play, Game_Over, Undefined);
   Game_State : Game_State_T := Welcome;
begin
   loop
      case Game_State is
         when Welcome =>
            Noki.Log ("Welcome to Snakotron!");
            Game_State := Play;
         when Play =>
            Noki.Log ("We are playing.");
            Game_State := Game_Over;
         when Game_Over =>
            Noki.Log ("Game Over!");
            Game_State := Undefined;
         when others =>
            Noki.Log ("Press Ctrl+C to abort program!");
      end case;
      delay 1.0;
   end loop;
end Snake_Game;

Build and run:

alr build
bin/snake_game

If something goes wrong: Check that your case statement syntax matches exactly, including the when clauses and semicolons.

Expected result:

Build finished successfully in ... seconds.
Welcome to Snakotron!
We are playing.
Game Over!
Press Ctrl+C to abort program!
Press Ctrl+C to abort program!
...
Important

🤯 Heads Up! This program will run forever! Use Ctrl+C in the terminal to stop it.

Control the Game Rendering Using ANSI Escape Codes

Let's control our terminal rendering! Right now our output just appends to the terminal. For a proper game, we need to update what's displayed instead.

Quick ANSI background: ESC (ASCII 27, hex 0x1B) + [ = Control Sequence Introducer (CSI). This tells the terminal "what follows is a command, not text to print."

  1. Create a reusable CSI constant in noki.ads:
    • Before the procedure Log (S : String); line, add:
    CSI : constant String := Character'Val (16#1B#) & '[';
    
  2. Add a screen clearing function right after the CSI line:
    function Clear_Screen return String is (CSI & "2J" & CSI & "H");
    

    This builds the ANSI command: clear screen (2J) + move cursor to home (H).
  3. Use it in our game loop by opening snake_game.adb and adding this right after loop:
    Noki.Log (Noki.Clear_Screen);
    

📋 Code Review - Complete snake_game.adb

Your complete snake_game.adb should look like this:

with Noki;

procedure Snake_Game is
   type Game_State_T is (Welcome, Play, Game_Over, Undefined);
   Game_State : Game_State_T := Welcome;
begin
   loop
      Noki.Log (Noki.Clear_Screen);
      case Game_State is
         when Welcome =>
            Noki.Log ("Welcome to Snakotron!");
            Game_State := Play;
         when Play =>
            Noki.Log ("We are playing.");
            Game_State := Game_Over;
         when Game_Over =>
            Noki.Log ("Game Over!");
            Game_State := Undefined;
         when others =>
            Noki.Log ("Press Ctrl+C to abort program!");
      end case;
      delay 1.0;
   end loop;
end Snake_Game;

Build and run:

alr build
bin/snake_game

Expected result: Instead of scrolling text, you'll see only the current message updating in place as the game state advances.

Note

🤯 Heads Up! The terminal now clears and redraws each frame - this is the foundation of all terminal-based games!

Save Your Progress

🎯 End of Lesson 3 — Save Your Progress

To keep your work safe and accessible, push it to your fork.

Make sure you're at the root of the repo:

cd ~/sandbox/snake-tutorial

Make sure you're on your work branch:

git status
  • 🚨 If you're NOT on branch lesson-03-work, run:
    git checkout -b lesson-03-work
    

Stage and commit your progress:

git add .
git commit -m "Lesson 3 progress"

Push to your fork:

git push origin lesson-03-work

Expected result: Your fork now has a lesson-3-work branch with your code so far.