
A Snake game… in RPN?
MOGWAI is an RPN (Reverse Polish Notation) scripting engine for .NET, inspired by HP RPL from the HP 28S and HP 48 calculators. Postfix notation, a stack-based model, low-level primitives… none of that sounds like the natural environment for a video game.
And yet, here is MOGWAI Snake: a fully playable Snake game, complete with score tracking, high score, a real-time health bar that drains steadily, randomly placed traps, and a clean restart system — all inside a TUI terminal, written in pure MOGWAI script.
This project was never meant to become a serious game. It is first and foremost a stress test: can MOGWAI handle something as dynamic and event-driven as a real-time game? The answer is yes, and here is how.
What you see on screen
The status bar permanently displays:
- MOGWAI SNAKE in cyan
- SCORE and HIGH SCORE in blue and magenta
- HEALTH: a visual bar
▓▓▓▓▓▓░░░░that drains over time and refills when a prize is collected
The play field is framed with Unicode box-drawing characters (┌─┐│└┘). Traps (green squares ■) are randomly placed at startup, outside a safety zone around the center. The snake is drawn in cyan (█). Prizes are red digits (1 through 9) — their value determines both the score gained and how much the snake grows.
Architecture: 5 key mechanics
1. The $SCREEN buffer — single source of truth
The play field is modelled as a flat data array of size WIDTH × HEIGHT. Each cell encodes its state:
0 → free
1 → snake body
2 → border (forbidden zone)
8 → trap
to 'setScreenValue' with [x: .number y: .number v: .number] do
{
v &$SCREEN y $WIDTH * x + set
}
to 'getScreenValue' with [x: .number y: .number] do
{
&$SCREEN y $WIDTH * x + get
}
Collision detection is reduced to a single test: if the value at the head’s position is greater than 0, game over. Borders, snake body and traps are all handled by the same mechanism.
if ($SX $SY getScreenValue 0 >) then { gameOver }
Note the use of the & sigil to access the array by direct reference — avoiding an expensive copy on every access.
2. $HEADING — the direction queue for the tail
This is the central mechanic of any Snake game: the tail does not follow the head immediately, it retraces its path with a delay. The solution here is elegant: at each cycle, the current direction is appended to the list $HEADING. When it is time to move the tail, the first entry of the list is consumed.
# Store the current direction
$HEADING $CURDIR + -> '$HEADING'
# ...later, for the tail:
$HEADING first -> 'heading'
$HEADING butfirst -> '$HEADING'
The $HEADING list acts as a natural FIFO queue using MOGWAI’s first and butfirst primitives. The longer the snake, the longer the list — and the tail traces the exact path of the head.
3. $WAITING_LOOP — snake growth
When the snake eats a prize of value n, it must grow by n × 2 cells. The approach is minimalist: a counter $WAITING_LOOP is set to n × 2. As long as it remains above 0, the tail does not move — the snake grows naturally, with no additional data structure required.
# When a prize of value $PRIZE_VALUE is caught:
$PRIZE_VALUE 2 * -> '$WAITING_LOOP'
# At the end of each cycle:
'$WAITING_LOOP' --
if ($WAITING_LOOP 0 <=) then
{
1 -> '$WAITING_LOOP'
hideSnakeTail
# ... tail movement
}
4. Timer + flag — health without race conditions
The health bar drains every 2 seconds via a MOGWAI timer. But the timer does not modify $HEALTH_LEVEL directly — it simply sets a flag.
timer 'HEALTH_TIMER' every 2000 do
{
'HEALTH_CHANGE_FLAG' flag.set
}
The main game loop consumes that flag at each cycle:
if ('HEALTH_CHANGE_FLAG' flag.isSet) then
{
'HEALTH_CHANGE_FLAG' flag.clear
'$HEALTH_LEVEL' --
if ($HEALTH_LEVEL 1 <) then { gameOver }
}
This decoupling is good practice in any environment with asynchronous callbacks: the timer signals, the loop decides. State is never modified outside the main execution thread.
5. post { startGame } — clean restart
When the player presses Y at the GAME OVER prompt, the game restarts. The temptation would be to call startGame directly. But startGame contains the main game loop (forever do), which would create unbounded recursion.
The solution is post, which schedules the execution of startGame after the current call returns. The call stack stays clean, regardless of how many games are played.
if (" NEW GAME (Y/N) ? " waitForYesOrNoChoice) then
{
post { startGame }
}
Startup checks
The script verifies two prerequisites before launching the game. First, the host must have the TERMINAL skill — a new feature introduced in MOGWAI 8.11 that lets a host declare its capabilities:
if ('TERMINAL' hasSkill not) then
{
"Unable to run the game." ?
"The host must have the 'TERMINAL' skill" ?
mogwai.exit
}
Then, the runtime version is checked:
if (mogwai.info->version: "8.11" ver>= not) then
{
"Unable to run the game." ?
"The MOGWAI runtime must be at least version 8.11" ?
mogwai.exit
}
These two guards illustrate a core MOGWAI philosophy: a script should be able to validate its execution environment and fail cleanly if the required conditions are not met.
What this project demonstrates
MOGWAI Snake is not a serious game — it is a proof of concept. In just over 600 lines of RPN script, you get:
- Dynamic TUI rendering with precise character-level positioning
- A real-time game loop with non-blocking keyboard input
- An asynchronous timer system decoupled from the main logic
- Unified collision detection (body, borders, traps) on a single buffer
- A snake growth mechanism with no dedicated data structure
- A recursion-free restart system
All of this with no framework, no game library, no graphics engine — just the MOGWAI runtime and the TUI primitives of MOGWAI CLI.
Try MOGWAI
MOGWAI is open source (Apache 2.0), available on GitHub and NuGet.
- GitHub: github.com/Sydney680928/mogwai
- Project site: mogwai.eu.com
- NuGet:
dotnet add package MOGWAI
The snake.mog script is included in the repository examples. It requires MOGWAI CLI v8.11 or later with the TERMINAL skill enabled.