← Previous
Building a Terminal Renderer

Get Latest Code

πŸš€ Beginning of Lesson 7 β€” 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-07-work origin/lesson-06-work

βœ… You're now ready to start Lesson 7 from your own progress.

πŸ…±οΈ Option B β€” Start from Official Course Snapshot

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

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

βœ… You're now starting Lesson 7 from the clean official reference code.

Building Transform Components

Let's build proper game entity machinery to model our snake! We'll create 2D position types and a transform component to track object locations in the game world.

Creating Position and Transform Types

  1. Add position types to noki.ads - we need to model a 2D point and a transform component:
    type Pt2_T is record
       X : Float := 0.0;
       Y : Float := 0.0;
    end record;
    
    type Transform_T is record
       Pt : Pt2_T := (0.0, 0.0);
    end record;
    

Why Float? While the terminal uses integer coordinates, we'll use floats for smooth movement calculations. We'll convert to integers when rendering.

What's a Transform? In game development, a transform component stores an entity's position (and often rotation and scale). Here we start simple with just position.

Creating the Snake Entity Type

  1. Define the Snake type - add this to the end of snake.ads:
    type Snake_T is record
       Transform : Transform_T;
       Texture   : Texture_T := [[Tongue_Pix, Head_Pix,  Empty_Pix],
                                 [Empty_Pix,  Torso_Pix, Empty_Pix],
                                 [Empty_Pix,  Torso_Pix, Torso_Pix]];
    end record;
    

Why combine them? This creates a proper game entity - the snake now has both visual representation (texture) and spatial data (transform). This is a fundamental game development pattern.

Updating the Player

  1. Replace the old player in snake_game.adb - remove this:
    Player : Texture_T := [[Tongue_Pix, Head_Pix,  Empty_Pix],
                           [Empty_Pix,  Torso_Pix, Empty_Pix],
                           [Empty_Pix,  Torso_Pix, Torso_Pix]];
    
  2. Add the new player - replace with:
    Player : Snake_T;
    

Default initialization: Notice we don't provide values - Ada uses the default values from the type definition. The transform starts at (0.0, 0.0) and the texture uses our predefined snake graphic.

  1. Update the render call - change this:
    Render (Player);
    
    To this:
    Render (Player.Texture);
    

Record access syntax: The . operator accesses record fields. Player.Texture gets the texture component from our Snake_T record.

Build and test:

alr build
bin/snake_game

βœ… Expected result: The game still works exactly the same, but now has a cleaner entity structure ready for position-based movement!

Implementing Position-Based Rendering

Now let's make the snake move! We'll update the renderer to draw textures at specific positions and animate the snake across the screen.

Staying in the Play State

  1. Keep the game loop running - edit snake_game.adb and remove this line:
    Game_State := Game_Over;
    

Why? We want to stay in the Play state indefinitely to see continuous animation. Use Ctrl-C, Esc, or Q to quit.

Adding Position to the Renderer

  1. Update the Render signature in noki.ads:
    procedure Render (T : Texture_T; P : Pt2_T);
    
  2. Update the implementation in noki.adb - change the procedure declaration to match:
    procedure Render (T : Texture_T; P : Pt2_T) is
    

Parameter order: We pass the texture first, then the position. This reads naturally as "render this texture at this position".

Converting Float Position to Integer Coordinates

  1. Add coordinate conversion - in the declarative region of Render (after the Move_Cursor function), add:
    Dx : Integer := Integer (P.X);
    Dy : Integer := Integer (P.Y);
    

Why convert? Our Pt2_T uses floats for smooth calculations, but terminal coordinates must be integers. The Integer() conversion truncates the decimal part.

  1. Apply the position offset - change this line:
    Ada.Text_IO.Put (Move_Cursor (Y + 1, Positive'First));
    
    To this:
    Ada.Text_IO.Put (Move_Cursor (Y + 1 + Dy, Positive'First + Dx));
    

Offset logic: Dx and Dy shift the texture from its base coordinates. Y + 1 + Dy means: base row + 1-based indexing + vertical offset.

Testing Position-Based Rendering

  1. Update the render call in snake_game.adb:
    Render (Player.Texture, Player.Transform.Pt);
    

New signature: Now we're passing both the texture and its position from the transform component.

Animating the Snake

  1. Add diagonal movement - update the Play case in snake_game.adb:
    when Play =>
       Player.Transform.Pt.X := @ + 1.0;
       Player.Transform.Pt.Y := @ + 1.0;
       Render (Player.Texture, Player.Transform.Pt);
    

The @ symbol: This is Ada's "target name" feature - it refers to the current value of the assignment target. @ + 1.0 means "take the current value and add 1.0 to it". It's shorthand for Player.Transform.Pt.X := Player.Transform.Pt.X + 1.0.

Movement pattern: Adding 1.0 to both X and Y each frame creates diagonal movement down and to the right.

Hiding the Terminal Cursor

  1. Add cursor hiding function - in noki.ads, add this new ANSI sequence:
    function Hide_Cursor return String is 
       (CSI & "?25l");
    

Why hide the cursor? Hiding the terminal cursor creates a cleaner visual experience.

ANSI sequence ?25l: This is the standard ANSI escape code to make the cursor invisible. We'll restore it later when the program exits.

  1. Hide cursor before game starts - in snake_game.adb, just before entering the game loop, add:
    Ada.Text_IO.Put (Hide_Cursor);
    

Timing matters: We hide the cursor once at the start, not in every frame. This is more efficient and prevents flickering.

Build and run:

alr build
bin/snake_game

βœ… Expected result: The snake moves diagonally across the screen with no visible cursor! The animation is clean and smooth, and you can quit with 'q' or Escape.

Improving Terminal Rendering

Let's optimize our terminal rendering to remove flickering and improve visual quality. We'll fix screen clearing, add proper IO flushing, and ensure graceful program termination.

Adding Text IO Support

  1. Import Ada.Text_IO - at the top of snake_game.adb, add this with clause:
    with Ada.Text_IO;
    

Why needed? We're about to use Ada.Text_IO.Put and Ada.Text_IO.Flush directly for better terminal control. This import makes those procedures available.

Fixing Screen Clear

  1. Replace Log with direct output - in snake_game.adb, at the beginning of the game loop, change this:
    noki.Log (Clear_Screen);
    
    To this:
    Ada.Text_IO.Put (Clear_Screen);
    

Why this matters: Log adds a newline after each call, which interferes with the ANSI escape sequence. Using Ada.Text_IO.Put directly emits a pure clear screen command without extra characters.

ANSI escape codes: Clear_Screen is an ANSI escape sequence that terminal emulators interpret as a command. Extra newlines can break these commands or cause visual artifacts.

Adding IO Flush

  1. Flush the output buffer - at the end of the game loop in snake_game.adb, just before the delay 0.1; line, add:
    Ada.Text_IO.Flush;
    

What is flushing? Output in Ada (and most languages) is buffered for performance. Flush forces all pending output to be written immediately to the terminal.

Why flush? Without flushing, rendered frames might stay in the buffer and appear out of sync. Flushing ensures smooth, consistent frame updates and eliminates visual lag.

Implementing Graceful Exit

  1. Remove the forced exit - edit snake_game.adb and remove these lines:
    with GNAT.OS_Lib;
    
    and
    GNAT.OS_Lib.OS_Exit (0);
    

Why remove it? OS_Exit terminates immediately without cleanup, leaving the terminal in a bad state. We want graceful shutdown instead.

  1. Update the input task exit - edit noki.adb and modify the quit key handling:
    when 'q' | Character'Val (27) => 
       Input_Cmd.Set (Quit);
       exit;
    

Character'Val (27): This is the Escape key's ASCII code. When either 'q' or Escape is pressed, we set the quit command and exit the task loop cleanly.

  1. Add cursor restoration - in noki.ads, add the show cursor sequence:
    function Show_Cursor return String is 
       (CSI & "?25h");
    

Symmetry: This is the opposite of Hide_Cursor. The ANSI sequence ?25h restores cursor visibility.

  1. Clean up on exit - in snake_game.adb, just after the game loop exits, add:
    Ada.Text_IO.Put (Show_Cursor);
    Ada.Text_IO.Put (Clear_Screen);
    Ada.Text_IO.Flush;
    

Why these three calls?

  • Show_Cursor restores the cursor so the terminal works normally after the game
  • Clear_Screen removes game artifacts from the terminal
  • Flush ensures these commands execute immediately before program termination

Build and run:

alr build
bin/snake_game

βœ… Expected result: Smooth animation with no flickering! The snake moves cleanly across the screen, and when you quit (press 'q' or Escape), the terminal returns to its normal state with the cursor visible and screen cleared.

Save Your Progress

🎯 End of Lesson 7 β€” 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-07-work, run:
    git checkout -b lesson-07-work
    

Stage and commit your progress:

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

Push to your fork:

git push origin lesson-07-work

βœ… Expected result: Your fork now has a lesson-7-work branch with your code so far.

Level up your Server Side game β€” Join 15,000 engineers who receive insightful learning materials straight to their inbox

← Previous
Building a Terminal Renderer