← Previous
Game Loop and State Management

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

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.

  1. Add a package rename in snake_game.adb:
    • Just under procedure Snake_Game is, add:
    package N renames Noki;
    
  2. Replace all Noki. with N. throughout your code:
    • Noki.Log (Noki.Clear_Screen); becomes N.Log (N.Clear_Screen);
    • Update all other Noki.Log calls to N.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.

  1. Declare a task type in noki.ads before end Noki;:
    task type Input_Task_T;
    
  2. Implement the task body in noki.adb before end 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).

  1. 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.

  1. Add command type to noki.ads after procedure Log (S : String);:
    type Cmd_T is (Enter, Quit, Undefined);
    
  2. 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;
  1. Implement protected object in noki.adb before 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;
    
  2. 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;
  1. 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; after loop
    • 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

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