DEV Community

AyahZahra
AyahZahra

Posted on Originally published at brandmu.my.id

What I Learned from Reading an 839-Line Pascal Game Built Without a Game Engine

Whenever someone mentions writing a game from scratch, I often see recommendations to use a game engine. But for a small 2D grid game, skipping the engine can be the better choice.

Not because game engines are bad, but because they can introduce solutions to problems you don't have while hiding decisions you might want to make yourself.

To explore this idea, I read Moss Circuit, an 839-line Pascal game that renders directly through the Windows API's GDI.

No game engine. No asset pipeline. No game-loop abstraction.

Just Windows, Messages, SysUtils, Math, and Types in the uses clause.

Here's what I learned from reading all 839 lines.

1. Why Write a Game This Way?

The entire program lives in a single unit containing 41 procedures and functions.

Some of them handle palette colors:

RGBColor, ThemeColor, PageColor, PanelColor, BorderColor, TextColor,
MutedColor, AccentColor, SnakeColor, FoodColor, CanvasColor, GridColor
Enter fullscreen mode Exit fullscreen mode

Others handle drawing primitives:

DrawTextRect, FillRounded, FillCircle, DrawPetal
Enter fullscreen mode Exit fullscreen mode

The remaining code handles game logic and Win32 integration.

This organization is worth learning from, even if you never write Pascal. Separating color helpers, drawing primitives, game logic, and platform-specific code makes a small project easier to navigate without introducing a complicated architecture.

2. Board Geometry Is Recomputed on Resize

This is one of my favorite parts of the implementation.

Whenever the window is resized, CalculateBoard derives the board geometry from the client area's dimensions.

procedure CalculateBoard(ClientWidth, ClientHeight: Integer);
var
  Padding: Integer;
begin
  Padding := 48;
  BoardSize := Min(ClientWidth - Padding * 2, ClientHeight - 286);
  BoardSize := Max(240, BoardSize);
  BoardLeft := (ClientWidth - BoardSize) div 2;
  BoardTop := 190;
  CellSize := BoardSize div BoardCells;
  BoardSize := CellSize * BoardCells;
  BoardLeft := (ClientWidth - BoardSize) div 2;
end;
Enter fullscreen mode Exit fullscreen mode

There are four details here that are easy to miss.

  1. One constant defines the board dimensions. BoardCells = 24 means the grid is always 24 × 24 cells.

  2. Integer division can leave unused pixels. CellSize := BoardSize div BoardCells truncates the result. The next assignment, BoardSize := CellSize * BoardCells, snaps the board size to an exact multiple of the cell size.

  3. The minimum size prevents the board from shrinking indefinitely. Max(240, ...) establishes a lower bound of 240 pixels.

  4. The board is centered again after snapping. Recalculating BoardLeft ensures the final board remains centered rather than using the position calculated from the original dimensions.

That small sequence makes the board geometry predictable without needing a layout engine.

3. Difficulty Is Calculated from the Score

Many Snake clones use a lookup table to define difficulty levels. Moss Circuit uses a formula instead.

function CurrentSpeed: UINT;
begin
  Result := Max(MinimumSpeed, StartSpeed - Score * 3);
end;
Enter fullscreen mode Exit fullscreen mode

With StartSpeed = 145 and MinimumSpeed = 66, the timer interval starts at 145 milliseconds and decreases by 3 milliseconds for every point, until it reaches the minimum.

The interval reaches its 66 ms floor at a score of 27.

Applying the calculated speed to the timer takes just one line:

SetTimer(MainWindow, TimerId, CurrentSpeed, nil);
Enter fullscreen mode Exit fullscreen mode

The WM_TIMER message drives the game loop. There is no while true loop anywhere.

This is a good example of using the operating system's event mechanism instead of building a separate game-loop abstraction.

4. The Anti-Reverse Check Defines the Game Feel

A surprisingly large part of making Snake feel right comes down to handling direction changes correctly.

Here's the relevant procedure:

procedure RequestDirection(NewX, NewY: Integer);
begin
  if State = gsReady then
    BeginGame;

  if (State <> gsPlaying) or TurnQueued then
    Exit;

  if (NewX = -DirectionX) and (NewY = -DirectionY) then
    Exit;

  NextDirectionX := NewX;
  NextDirectionY := NewY;
  TurnQueued := True;
end;
Enter fullscreen mode Exit fullscreen mode

Three things happen here.

  • Reverse input is rejected. If the snake is moving right, pressing left cannot immediately reverse its direction.
  • Only one turn is queued per tick. Without TurnQueued, a player could press right and then up within a few milliseconds, potentially causing an unintended turn before the next movement update.
  • The first direction input starts the game. There's no need to press a separate start button.

The queue pattern is useful beyond Snake. Any grid-based game that updates movement in discrete ticks can encounter similar input-handling bugs.

The important idea is to separate the direction requested by the player from the direction currently being applied by the game simulation.

5. Preferences Persist to %APPDATA%

Moss Circuit stores its settings in a directory under the user's application data folder.

DataDirectory := GetEnvironmentVariable('APPDATA');

if DataDirectory = '' then
  DataDirectory := ExtractFilePath(ParamStr(0));

DataDirectory :=
  IncludeTrailingPathDelimiter(DataDirectory) + 'MossCircuit';

ForceDirectories(DataDirectory);
Enter fullscreen mode Exit fullscreen mode

The theme preference and high score are stored under %APPDATA%\MossCircuit\ on a typical Windows installation.

If APPDATA is unavailable, the program falls back to the executable's directory.

The file-writing code also handles I/O errors without immediately terminating the game:

procedure SaveSetting(const FileName, Value: string);
begin
  AssignFile(FileHandle, DataFile(FileName));
  {$I-}
  Rewrite(FileHandle);

  if IOResult = 0 then
  begin
    WriteLn(FileHandle, Value);
    CloseFile(FileHandle);
  end;

  {$I+}
end;
Enter fullscreen mode Exit fullscreen mode

The {$I-} directive disables automatic I/O error checking, allowing the program to inspect the result through IOResult.

If opening the file fails, the game avoids a crash caused by that operation. The trade-off is that the setting may not be saved.

There is one important nuance: this fallback does not make the game fully cross-platform. The code still depends on Windows-specific APIs, and the executable directory may not be writable.

6. A Simple but Effective Theming System

The game supports two color themes, and palette values are routed through ThemeColor rather than being hardcoded throughout the drawing code.

Changing the theme flag updates the colors used by the palette helpers.

This is a lightweight theming system: one flag and a set of color functions.

There's no need for a CSS-like system, separate theme files, or a complex inheritance hierarchy for a game this small.

The broader lesson is that architecture should match the scale of the project. A small game can benefit from clear separation of concerns without needing a full framework.

7. What the Game Doesn't Have

Moss Circuit has no audio, particle effects, save slots, multiplayer, or level editor.

For an 839-line game, those omissions make sense. Every additional feature adds implementation and maintenance costs.

The gsComplete state and CompleteGarden procedure also suggest that the game has a defined completion state, with a flower that grows as the board fills.

Rather than adding features just because other games have them, the implementation focuses on the core experience.

Who Should Read This Code?

If you want to understand what a complete small game can look like without the overhead of a game engine, Moss Circuit is worth exploring.

The WinAPI-specific parts have a learning curve, but the game logic is relatively straightforward. If you're already familiar with GDI, you'll recognize many of the patterns quickly.

Even if you normally use a game engine, reading a project like this can help you understand what the engine is doing behind the scenes.

Source code: Moss Circuit on GitHub

For the Indonesian version, including the code map and screenshots, see my blog:

Read the full article on brandmu.my.id

Top comments (0)