- βΒ Previous
- Concurrent Input Handling
- NextΒ β
- Building a Terminal Renderer
Get Latest Code
π Beginning of Lesson 5 β 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
π °οΈ Option A β Resume from Your Fork (Recommended)
Check out your saved progress to a branch:
git checkout -b lesson-05-work origin/lesson-04-work
β You're now ready to start Lesson 5 from your own progress.
π ±οΈ Option B β Start from Official Course Snapshot
Check out and push to your origin the starting point for Lesson 5 :
git remote add upstream https://github.com/GNAT-Academic-Program/snake-tutorial.git
git fetch upstream
git checkout -b lesson-05-work upstream/lesson-05-start
git push -u origin lesson-05-work
β You're now starting Lesson 5 from the clean official reference code.
We Need Pixels
Let's create pixel abstractions for our terminal game! Our "pixel" combines foreground color, background color, character, and boldness - different from screen pixels but perfect for terminal graphics.
Understanding Ada's Type System
Before we build pixels, let's understand Ada's powerful type system:
Modular Types: type U8_T is mod 2 ** 8 creates a type that wraps around (0-255). When it overflows, it loops back to 0. Perfect for hardware registers and color values.
Records: Like structs in C - group related data together with named fields.
Subtypes: subtype Bold_T is Boolean creates a compatible alias. You can use Bold_T anywhere you'd use Boolean without conversion.
Strong Typing: Ada prevents mixing incompatible types. You can't accidentally add Miles_T and Kilometers_T - you need explicit conversion. This catches bugs at compile time.
- Define basic types in
noki.adsafterpackage Noki is:subtype WWChar_T is Wide_Wide_Character; -- Unicode characters (less verbose) type U8_T is mod 2 ** 8 with Size => 8; -- 0-255, wraps on overflow type Color_T is record -- RGB color structure R, G, B : U8_T; end record with Size => 32; subtype Bold_T is Boolean; -- Semantic clarity over raw Boolean - Create the Pixel record:
type Pixel_T is record FC : Color_T; -- Foreground Color BC : Color_T; -- Background Color C : WWChar_T; -- Character B : Bold_T := False; -- Bold (default False) end record; - Add colors for our game:
Deep_Navy : Color_T := (13, 2, 33); Hot_Tangerine : Color_T := (255, 122, 24); Acid_Lime : Color_T := (201, 255, 0); Neon_Pink : Color_T := (255, 102, 209);
Organizing Game-Specific Code
The previous definitions are game-agnostic and belong in our library noki.ads. Now let's create things specific to our snake game.
Why separate packages? We could put everything in our main binary, but that rapidly clutters the code. A better approach is creating a specific package for our game to keep things organized and comprehensible.
- Create a Snake package - make a new file
snake.adsnext tosnake_game.adb:with Noki; use Noki; package Snake is end Snake;
Inside snake.ads add the following:
- Define snake characters (showing different Unicode initializations):
Tongue : constant WWChar_T := '~'; -- ASCII literal Head : constant WWChar_T := WWChar_T'Val (16#25C0#); -- Unicode by hex value Torso : constant WWChar_T := 'β'; -- Direct Unicode paste - Create actual snake pixels:
Tongue_Pix : Pixel_T := (FC => Hot_Tangerine, BC => Deep_Navy, C => Tongue, B => True); Head_Pix : Pixel_T := (Acid_Lime, Deep_Navy, Head, True); Torso_Pix : Pixel_T := (Neon_Pink, Deep_Navy, Torso, True);
π Code Review - Complete Files
Your noki.ads should include these game-agnostic types after package Noki is:
subtype WWChar_T is Wide_Wide_Character;
type U8_T is mod 2 ** 8 with Size => 8;
type Color_T is record
R, G, B : U8_T;
end record with Size => 32;
subtype Bold_T is Boolean;
type Pixel_T is record
FC : Color_T; -- Foreground Color
BC : Color_T; -- Background Color
C : WWChar_T; -- Character
B : Bold_T := False; -- Bold
end record;
Deep_Navy : Color_T := (13, 2, 33);
Hot_Tangerine : Color_T := (255, 122, 24);
Acid_Lime : Color_T := (201, 255, 0);
Neon_Pink : Color_T := (255, 102, 209);
Your snake.ads should contain the snake-specific definitions:
with Noki; use Noki;
package Snake is
Tongue : constant WWChar_T := '~';
Head : constant WWChar_T := WWChar_T'Val (16#25C0#);
Torso : constant WWChar_T := 'β';
Tongue_Pix : Pixel_T := (FC => Hot_Tangerine,
BC => Deep_Navy,
C => Tongue,
B => True);
Head_Pix : Pixel_T := (Acid_Lime, Deep_Navy, Head, True);
Torso_Pix : Pixel_T := (Neon_Pink, Deep_Navy, Torso, True);
end Snake;
β Expected result: Clean separation between game library and snake-specific code!
Convert Pixels to ANSI Strings
Let's build a pixel rendering system! We need to convert our pixel model into ANSI escape codes that the terminal can display.
Understanding Terminal as a 2D Canvas
Our rendering target is a modern terminal emulator. With ANSI escape codes we can move the cursor and apply styles, so the terminal's 2D space acts like a pixel buffer - a rendered image.
Instead of scattering escape codes everywhere, we'll build semantic helpers to draw pixels.
1. Add Wide_Wide_String Support
Edit noki.ads and add this after the WWChar_T subtype:
subtype WWStr_T is Wide_Wide_String;
Why? In Ada, a String is an array of Character. We need the Wide_Wide (Unicode) version for our pixel rendering.
2. Create the Pixel-to-String Converter
We'll overload the + operator for clean, dense syntax. Add this signature to noki.ads:
function "+" (P : Pixel_T) return WWStr_T;
3. Start the Implementation
Edit noki.adb and add the function stub:
function "+" (P : Pixel_T) return WWStr_T is
begin
null;
end "+";
4. Add the Trim Helper Function
Key insight: When Ada converts numbers to strings, it systematically adds a space for the sign:
-1becomes"-1"(2 characters:['-', '1'])1becomes" 1"(2 characters:[' ', '1']), the space is for the implicit+
We need to trim this leading space. Add this helper function before the + function:
function Trim (S : String) return String is
(S (S'First + 1 .. S'Last));
Ada features used:
- Function expressions (skip
begin/endfor single expressions) - Array attributes:
'Firstand'Lastgive array bounds - Range notation:
..for slicing arrays
5. Build the Color Control Strings Step by Step
Remember: We already defined the CSI constant at the top of noki.ads.
Now let's build the ANSI control strings inside the + function. Add these constants between is and begin:
First, add the foreground color:
FG_Color : constant String := CSI & "38;2;" &
Trim (P.FC.R'Image) & ";" &
Trim (P.FC.G'Image) & ";" &
Trim (P.FC.B'Image) & "m";
For RGB values like R=44, G=192, B=255, this creates: "\e[38;2;44;192;255m"
π€― Heads Up! The ESC character (byte 27 or hex 16#1B#) has no glyph - that's why you see notations like \e or ^[ in documentation. These are just human-readable representations of the actual escape character.
Next, add the background color (same pattern, different code):
BG_Color : constant String := CSI & "48;2;" &
Trim (P.BC.R'Image) & ";" &
Trim (P.BC.G'Image) & ";" &
Trim (P.BC.B'Image) & "m";
Add the bold control:
BOLD : constant String := CSI & "1m";
Add the reset control:
RESET : constant String := CSI & "0m";
Combine our pixel format conditionally:
FORMAT : constant String := (if P.B then
FG_Color & BG_Color & BOLD
else
FG_Color & BG_Color);
6. Complete the Function
Now we're ready to implement the function body using our building blocks.
Understanding the type conversion: Our function returns WWStr_T (Unicode string), and our pixel character P.C is already WWChar_T (Unicode). However, our ANSI control strings (FORMAT, RESET) are regular String type, so we need explicit conversion to satisfy Ada's strong typing.
Add the conversion package at the top of noki.adb:
with Ada.Characters.Conversions;
Complete the function by replacing the null; line with:
return Ada.Characters.Conversions.To_Wide_Wide_String (FORMAT) &
P.C &
Ada.Characters.Conversions.To_Wide_Wide_String (RESET);
Key insight: We convert the String control codes to Wide_Wide_String, then concatenate with the Unicode character to create the complete formatted pixel string.
π‘ Bonus: Function Renaming
As you can see, Ada.Characters.Conversions.To_Wide_Wide_String is verbose to write repeatedly. Ada allows you to rename functions for convenience!
Add this function rename inside the + function (in the declarative section):
function WWS (S : String) return WWStr_T renames Ada.Characters.Conversions.To_Wide_Wide_String;
Then simplify your return statement to:
return WWS (FORMAT) & P.C & WWS (RESET);
Why this works: Function renames create an alias with the same signature, making your code cleaner while maintaining full type safety.
π Code Review - Complete + Function
Your complete implementation should look like this:
function Trim (S : String) return String is
(S (S'First + 1 .. S'Last));
function "+" (P : Pixel_T) return WWStr_T is
BOLD : constant String := CSI & "1m";
RESET : constant String := CSI & "0m";
FG_Color : constant String := CSI & "38;2;" &
Trim (P.FC.R'Image) & ";" &
Trim (P.FC.G'Image) & ";" &
Trim (P.FC.B'Image) & "m";
BG_Color : constant String := CSI & "48;2;" &
Trim (P.BC.R'Image) & ";" &
Trim (P.BC.G'Image) & ";" &
Trim (P.BC.B'Image) & "m";
FORMAT : constant String := (if P.B then
FG_Color & BG_Color & BOLD
else
FG_Color & BG_Color);
function WWS (S : String) return WWStr_T
renames Ada.Characters.Conversions.To_Wide_Wide_String;
begin
return WWS (FORMAT) & P.C & WWS (RESET);
end "+";
β
Expected result: You can now convert any Pixel_T to a formatted terminal unicode string using the + operator!
Draw Pixels to Terminal
Let's create a Draw procedure to output our pixel to the terminal! We'll use Unicode I/O to handle our Wide_Wide_String pixel data.
Understanding Unicode I/O in Ada
Since we're dealing with Unicode characters and Wide_Wide_String, we need different I/O methods than regular strings. Ada provides Ada.Wide_Wide_Text_IO for this purpose.
1. Declare the Draw Procedure
Edit noki.ads and add this procedure signature after the current Log definition:
procedure Draw (P : Pixel_T);
2. Add Unicode I/O Support
Edit noki.adb and add this with clause at the top:
with Ada.Wide_Wide_Text_IO;
Why? We need the standard I/O library for Unicode to output our Wide_Wide_String pixel data.
3. Implement the Draw Procedure
Add this implementation in noki.adb:
procedure Draw (P : Pixel_T) is
begin
Ada.Wide_Wide_Text_IO.Put (+P);
end Draw;
π€― Heads Up! Make sure the Draw implementation comes after the + function implementation, otherwise the compiler won't find the + operator when we use +P.
Key insight: We're calling our + function from the previous lesson to convert the Pixel_T to WWStr_T in one clean operation, then outputting it with Unicode I/O.
4. Display Snake Pixels in Game
Now let's show our snake during gameplay! Edit snake_game.adb and replace the Play state code:
when Play =>
Log ("We are playing.");
Draw (Tongue_Pix);
Draw (Head_Pix);
Draw (Torso_Pix);
Draw (Torso_Pix);
Draw (Torso_Pix);
Draw (Torso_Pix);
Game_State := Game_Over;
5. Import the Snake Package
Since we defined our game-specific pixels (Tongue_Pix, Head_Pix, Torso_Pix) in the Snake package, we need to import it in our main program.
Add this line at the top of snake_game.adb:
with Snake; use Snake;
π€― Heads Up! The use directive makes package resources directly accessible. Since we have package N renames Noki; use N;, we can call Log, Draw, etc. directly without the N. prefix. Same with with Snake; use Snake;.
π Code Review - Complete snake_game.adb
Your main should now look like this:
with Noki;
with Snake; use Snake;
with GNAT.OS_Lib;
procedure Snake_Game is
package N renames Noki;
use N;
Input_Task : Input_Task_T;
type Game_State_T is (Welcome, Play, Game_Over, Undefined);
Game_State : Game_State_T := Welcome;
begin
loop
exit when Input_Cmd.Get = Quit;
Log (Clear_Screen);
case Game_State is
when Welcome =>
Log ("Welcome to Snakotron!");
Log ("Press Enter/Space to play, Q/Esc to quit");
if Input_Cmd.Get = Enter then
Game_State := Play;
Input_Cmd.Reset;
end if;
when Play =>
Log ("We are playing.");
Draw (Tongue_Pix);
Draw (Head_Pix);
Draw (Torso_Pix);
Draw (Torso_Pix);
Draw (Torso_Pix);
Draw (Torso_Pix);
Game_State := Game_Over;
when Game_Over =>
Log ("Game Over!");
Log ("Press Enter/Space to replay");
if Input_Cmd.Get = Enter then
Game_State := Play;
Input_Cmd.Reset;
end if;
when others =>
null;
end case;
delay 1.0;
end loop;
GNAT.OS_Lib.OS_Exit (0);
end Snake_Game;
Build and run:
alr build
bin/snake_game
β Expected result: Your snake pixels now appear on screen with full color and Unicode characters support during the Play state!
Save Your Progress
π― End of Lesson 5 β 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-05-work, run:git checkout -b lesson-05-work
Stage and commit your progress:
git add .
git commit -m "Lesson 5 progress"
Push to your fork:
git push origin lesson-05-work
β Expected result: Your fork now has a lesson-5-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
- Concurrent Input Handling
- NextΒ β
- Building a Terminal Renderer