- βΒ Previous
- Game Loop and State Management
- NextΒ β
- Graphics and Ada's Type System
Get Latest Code
π Beginning of Lesson 4 β 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-04-work origin/lesson-03-work
β You're now ready to start Lesson 4 from your own progress.
π ±οΈ Option B β Start from Official Course Snapshot
Check out and push to your origin the starting point for Lesson 4 :
git remote add upstream https://github.com/GNAT-Academic-Program/snake-tutorial.git
git fetch upstream
git checkout -b lesson-04-work upstream/lesson-04-start
git push -u origin lesson-04-work
β You're now starting Lesson 4 from the clean official reference code.
Making Things Less Verbose
Let's reduce verbosity! Ada lets you control how explicit you want to be with package names. Instead of typing Noki. everywhere, we can create shorter aliases.
- Add a package rename in
snake_game.adb:- Just under
procedure Snake_Game is, add:
package N renames Noki; - Just under
- Replace all
Noki.withN.throughout your code:Noki.Log (Noki.Clear_Screen);becomesN.Log (N.Clear_Screen);- Update all other
Noki.Logcalls toN.Log
π Code Review - Complete snake_game.adb
Your complete snake_game.adb should look like this:
with Noki;
procedure Snake_Game is
package N renames Noki;
type Game_State_T is (Welcome, Play, Game_Over, Undefined);
Game_State : Game_State_T := Welcome;
begin
loop
N.Log (N.Clear_Screen);
case Game_State is
when Welcome =>
N.Log ("Welcome to Snakotron!");
Game_State := Play;
when Play =>
N.Log ("We are playing.");
Game_State := Game_Over;
when Game_Over =>
N.Log ("Game Over!");
Game_State := Undefined;
when others =>
N.Log ("Press Ctrl+C to abort program!");
end case;
delay 1.0;
end loop;
end Snake_Game;
β Expected result: Same behavior, less typing!
π€― Heads Up! In large codebases, explicit package names (Noki.Log) are often more readable than short aliases (N.Log) or dropping the namespace entirely (Log). They show exactly where code comes from. Balance brevity with clarity based on your project's needs.
Create an Input Task
Let's create a thread to handle keyboard input! This will run concurrently with our game loop, allowing real-time user interaction.
- Declare a task type in
noki.adsbeforeend Noki;:task type Input_Task_T; - Implement the task body in
noki.adbbeforeend Noki;:task body Input_Task_T is C : Character := 'X'; begin loop Ada.Text_IO.Get_Immediate (C); -- blocking Log ("Got a character: " & C'Image); end loop; end Input_Task_T;
π€― Heads Up! The task loops indefinitely, blocking until a character is available, then puts that char into C and loops again waiting for a new character. It logs the character using C'Image (Ada 2022 feature for String representation of any type).
- Use the input task in
snake_game.adb:- Add a task variable in the declarative section (the task starts looping immediately):
Input_Task : N.Input_Task_T;
π Code Review - Complete snake_game.adb
Your complete snake_game.adb should now look like this:
with Noki;
procedure Snake_Game is
package N renames Noki;
type Game_State_T is (Welcome, Play, Game_Over, Undefined);
Game_State : Game_State_T := Welcome;
Input_Task : N.Input_Task_T;
begin
loop
N.Log (N.Clear_Screen);
case Game_State is
when Welcome =>
N.Log ("Welcome to Snakotron!");
Game_State := Play;
when Play =>
N.Log ("We are playing.");
Game_State := Game_Over;
when Game_Over =>
N.Log ("Game Over!");
Game_State := Undefined;
when others =>
N.Log ("Press Ctrl+C to abort program!");
end case;
delay 1.0;
end loop;
end Snake_Game;
β Expected result: Your game now handles keyboard input concurrently! Press keys while the game runs to see "Got a character: ..." messages.
Handle Specific Input Commands
Let's filter input to handle only the commands we need! We'll support Enter/Space to select and Q/Esc to quit.
- Add command type to
noki.adsafterprocedure Log (S : String);:type Cmd_T is (Enter, Quit, Undefined); - Add protected object for thread-safe command sharing:
protected type Input_Cmd_T is procedure Set (Cmd : Cmd_T); function Get return Cmd_T; procedure Reset; private Local_Cmd : Cmd_T := Undefined; end Input_Cmd_T; Input_Cmd : Input_Cmd_T;
π€― Heads Up! Protected objects handle concurrent access automatically - the runtime queues access calls to prevent data corruption between threads.
π Code Review - Complete noki.ads
Your complete noki.ads should look like this:
package Noki is
CSI : constant String := Character'Val (16#1B#) & '[';
function Clear_Screen return String is (CSI & "2J" & CSI & "H");
procedure Log (S : String);
type Cmd_T is (Enter, Quit, Undefined);
protected type Input_Cmd_T is
procedure Set (Cmd : Cmd_T);
function Get return Cmd_T;
procedure Reset;
private
Local_Cmd : Cmd_T := Undefined;
end Input_Cmd_T;
Input_Cmd : Input_Cmd_T;
task type Input_Task_T;
end Noki;
- Implement protected object in
noki.adbbefore the task body:protected body Input_Cmd_T is procedure Set (Cmd : Cmd_T) is begin Local_Cmd := Cmd; end Set; function Get return Cmd_T is (Local_Cmd); procedure Reset is begin Local_Cmd := Undefined; end Reset; end Input_Cmd_T; - Update task body - replace the character logging with command setting:
-- Replace the Ada.Text_IO.Get_Immediate section with: Ada.Text_IO.Get_Immediate (C); case C is when 'q' | Character'Val (27) => Input_Cmd.Set (Quit); when Character'Val (10) | Character'Val (32) => Input_Cmd.Set (Enter); when others => Input_Cmd.Set (Undefined); end case;
π Code Review - Complete noki.adb
Your complete noki.adb should look like this:
with Ada.Text_IO;
package body Noki is
procedure Log (S : String) is
begin
Ada.Text_IO.Put (S);
Ada.Text_IO.New_Line;
end Log;
protected body Input_Cmd_T is
procedure Set (Cmd : Cmd_T) is
begin
Local_Cmd := Cmd;
end Set;
function Get return Cmd_T is (Local_Cmd);
procedure Reset is
begin
Local_Cmd := Undefined;
end Reset;
end Input_Cmd_T;
task body Input_Task_T is
C : Character;
begin
loop
Ada.Text_IO.Get_Immediate (C);
case C is
when 'q' | Character'Val (27) =>
Input_Cmd.Set (Quit);
when Character'Val (10) | Character'Val (32) =>
Input_Cmd.Set (Enter);
when others =>
Input_Cmd.Set (Undefined);
end case;
end loop;
end Input_Task_T;
end Noki;
- Update main game in
snake_game.adb:- Add
with GNAT.OS_Lib;at the top - Add
use N;after the package rename for operator visibility - Add exit condition:
exit when Input_Cmd.Get = Quit;afterloop - Update Welcome state to check for Enter command
- Update Game_Over state to handle replay functionality
- Add
GNAT.OS_Lib.OS_Exit (0);after the loop ends
- Add
Key changes to snake_game.adb:
-- Add at top:
with GNAT.OS_Lib;
-- After package rename:
use N;
-- Add after 'loop':
exit when Input_Cmd.Get = Quit;
-- Update Welcome state:
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;
-- Update Game_Over state:
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;
-- Add after 'end loop;':
GNAT.OS_Lib.OS_Exit (0);
π Code Review - Complete snake_game.adb
Your complete snake_game.adb should look like this:
with Noki;
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.");
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: Game responds to Enter/Space (advance/replay) and Q/Esc (quit) keys only. Players can replay after Game Over. Pressing Q/Esc properly terminates the program.
Save Your Progress
π― End of Lesson 4 β 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-04-work, run:git checkout -b lesson-04-work
Stage and commit your progress:
git add .
git commit -m "Lesson 4 progress"
Push to your fork:
git push origin lesson-04-work
β Expected result: Your fork now has a lesson-4-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
- Game Loop and State Management
- NextΒ β
- Graphics and Ada's Type System