The Sumerian Game is one of the oldest computer games, created in 1964 as an education game. Its source code is unfortunately lost, but a simplified version was ported in 1969 to FOCAL for the PDP8 under the name King of Sumeria and then to BASIC around 1971 and eventually published as Hamurabi in David Ahl's 101 BASIC Computer Games book.
This tutorial shows how to build a FOCAL interpreter from scratch.
Here's the original* Hamurabi program we'll run:
const hamurabi = '''
01.10 S P=95;S S=2800;S H=3000;S E=200;S Y=3;S A=1000;S I=5;S Q=1
02.10 S D=0
02.20 D 6;T !!!"LAST YEAR"!D," STARVED,
02.25 T !I," ARRIVED,";S P=P+I;I (-Q)2.3
02.27 S P=FITR(P/2);T !"**PLAGUE**"!
02.30 T !"POPULATION IS"P,!!"THE CITY OWNS
02.35 T A," ACRES."!!;I (H-1)2.5;T "WE HARVESTED
02.40 D 3.2
02.50 T !" RATS ATE "E," BUSHELS, YOU NOW HAVE
02.60 T !S," BUSHELS IN STORE."!
03.10 D 6; D 8;S Y=C+17;T "LAND IS TRADING AT
03.20 T Y," BUSHELS PER ACRE;";S C=1
03.30 D 4.3;A " BUY?"!Q;I (Q)7.2,3.8
03.40 I (Y*Q-S)3.9,3.6;D 4.6;G 3.3
03.50 D 4.5;G 3.3
03.60 D 3.9:G 4.8
03.70 S A=A+Q;S S=Y*Q;S C=0
03.80 A !"TO SELL?"!Q;I (Q)7.2,3.9;S Q=-Q;I (A+Q)3.5
03.90 S A=A+Q;S S=S-Y*Q;S C=0
04.10 T !"BUSHELS TO USE
04.11 A " AS FOOD?"!Q;I (Q)7.7;I (Q-S)4.2,4.7;D 4.6;G 4.1
04.20 S S=S-Q;S C=1
04.30 A !"HOW MANY ACRES OF LAND DO YOU WISH TO
04.35 A !"PLANT WITH SEED? "D
04.40 I (D)7.2;I (A-D)4.45;I (FITR(D/2)-S-1)4.65;D 4.6;G 4.3
04.45 D 4.5;G 4.3
04.50 D 7;T A," ACRES."!
04.60 D 7;D 2.6
04.65 I (D-10*P-1)5.1;D 7;T P," PEOPLE."!;G 4.3
04.70 D 4.2
04.80 D 6;T "YOU HAVE NO GRAIN LEFT AS SEED !!";S D=0
05.10 S S=S-FITR(D/2);D 8;S Y=C;S H=D*Y
05.20 D 8;S E=0;I (FITR(C/2)-C/2)5.3;S E=FITR(S/C)
05.30 S S=S-E+H;D 8;S I=FITR(C*(20*A+S)/P/100+1);S C=FITR(Q/20)
05.40 S Q=FITR(10*FRAN());I (P-C)2.1;S D=P-C;S P=C;G 2.2
06.10 T !!"HAMURABI: "%5
07.10 I (C)7.2;S C=C-1;D 6;T "BUT YOU HAVE ONLY";R
07.20 D 6;T !"GOODBYE!"!!;Q
08.10 S C=FITR(5*FRAN())+1
''';
* I replaced the last !
with ;
in 04.80
, removed the superflous FABS
and ABS
calls in 05.40
and 08.10
, especially as ABS
seems to be missing an F
.
FOCAL programs have line numbers split into group numbers (01-31) and step numbers within that group (01-99). Within a line, commands are separated with ;
and start with a single letter.
- ASK prompts for a numeric input and stores it into a variable.
- DO jumps to a subroutine, automatically returning at the end of that group or line.
- GOTO jumps to another line. In both cases, the step number part can be omitted and if that value is less than 10, it is multiplied by 10.
- IF jumps conditionally if a numeric value is less than zero, zero, or greater than zero.
- QUIT stops the execution of the program.
- SET sets a variable to some numeric value. There are 26 variables A to Z that can store numbers.
- TYPE outputs text and numeric values. A
!
stands for a newline, a,
concatenates expressions, and%
sets the padding for numeric values.
We'll build the interpreter bottom-up:
- Input Class - Pattern matching foundation
- Expression Parser - Math expressions with precedence
- Output System - Text and number formatting
- Minimal Executor - Basic command loop with TYPE
- User Input & Semicolons - ASK command and statement separators
- Single-line Programs - Practical calculator examples
- Variables - SET command for storage
- Program Structure - Multi-line program loading
- Control Flow - DO, GOTO, IF commands as needed
- Built-ins - FITR, FRAN functions
- Integration - Complete Hamurabi game
Each step builds on the previous and can be tested immediately.
The Input
class handles pattern matching and tokenization:
class Input {
Input(this.string);
final String string;
var index = 0;
bool get isEmpty => index == string.length;
String get peek => isEmpty ? '' : string[index];
String next() => isEmpty ? '' : string[index++];
String? match(Pattern pattern) {
while (peek == ' ' || peek == '\t') {
next();
}
final match = pattern.matchAsPrefix(string, index);
if (match != null) {
index = match.end;
return match[0];
}
return null;
}
bool matches(Pattern pattern) => match(pattern) != null;
@override
String toString() => '${string.substring(0, index)} →${string.substring(index)}';
}
Key methods:
isEmpty
checks if all input is consumedmatch()
consumes matching patterns, skipping whitespacematches()
tests patterns without consumingtoString()
shows current position with arrow
Test it:
final input = Input(' 123 + 456');
print(input.match(RegExp(r'\d+'))); // "123"
print(input.matches('+')); // true
print(input); // " 123 + →456"
FOCAL expressions support +
, -
, *
, /
, parentheses, numbers, and variables. We use recursive descent parsing:
final _digits = RegExp(r'\d+');
final _variables = RegExp(r'[A-EG-Z]');
final variables = <String, num>{};
var line = Input(''); // will be changed later
num expr() {
for (var value = _term(); ;) {
if (line.matches('+')) {
value += _term();
} else if (line.matches('-')) {
value -= _term();
} else {
return value;
}
}
}
num _term() {
for (var value = _factor(); ;) {
if (line.matches('*')) {
value *= _factor();
} else if (line.matches('/')) {
value /= _factor();
} else {
return value;
}
}
}
num _factor() {
if (line.matches('-')) {
return -_factor();
} else if (line.matches('(')) {
final result = expr();
line.match(')') ?? (throw 'missing )');
return result;
} else if (line.match(_digits) case final number?) {
return num.tryParse(number) ?? (throw 'invalid number: $number');
} else if (line.match(_variables) case final name?) {
return variables[name] ?? (throw 'unset variable: $name');
}
throw 'syntax error: $line';
}
Note: FOCAL uses F for function prefix, so variable F is reserved.
Test expressions:
line = Input('2 + 3 * 4');
print(expr()); // 14
variables['A'] = 10;
line = Input('A * 2 + -5');
print(expr()); // 15
line = Input('(2 + 3) * 4');
print(expr()); // 20
The output()
function handles FOCAL's text formatting:
!
= newline,
= concatenates expressions"text"
= literal strings%N
= number padding (parsed but ignored)- expressions = numeric output (padded to 5 characters)
import 'dart:io';
void output(bool withExpr) {
while (!line.isEmpty && !line.matches(';')) {
if (line.matches('!')) {
stdout.write('\n');
} else if (line.matches(',')) {
} else if (line.matches('"')) {
for (;;) {
if (line.isEmpty) return;
final ch = line.next();
if (ch == '"') break;
stdout.write(ch);
}
} else if (line.matches('%')) {
expr(); // padding value (ignored)
} else if (withExpr) {
stdout.write('${expr()}'.padLeft(5));
} else {
break;
}
}
}
Test output:
line = Input('"Hello"!');
output(false); // Hello\n
variables['X'] = 42;
line = Input('"Result:"X');
output(true); // Result: 42
Basic command loop that handles TYPE commands:
void execute() {
for (;;) {
if (line.isEmpty) break;
if (line.matches(';')) continue;
switch (line.next()) {
case 'T':
output(true);
default:
throw 'syntax error: $line';
}
}
}
Test TYPE command:
line = Input('T "2 + 3 ="2+3!!');
execute(); // 2 + 3 = 5\n
Add ASK command for reading numbers:
import 'dart:io';
void execute() {
for (;;) {
if (line.isEmpty) break;
if (line.matches(';')) continue;
switch (line.next()) {
case 'A':
output(false);
final name = line.match(_variables);
if (name == null) break;
final input = stdin.readLineSync();
if (input == null) return;
variables[name] = num.parse(input);
case 'T':
output(true);
default:
throw 'syntax error: $line';
}
}
}
Now you can run interactive single-line programs.
Test with a practical example - convert Celsius to Fahrenheit:
line = Input('A "Celsius: "C; T "Fahrenheit: "C*9/5+32!');
execute();
Add SET command for storing intermediate results:
void execute() {
for (;;) {
if (line.isEmpty) break;
if (line.matches(';')) continue;
switch (line.next()) {
case 'A':
output(false);
final name = line.match(_variables);
if (name == null) break;
final input = stdin.readLineSync();
if (input == null) return;
variables[name] = num.parse(input);
case 'S':
final name = line.match(_variables) ?? (throw 'missing variable');
line.match('=') ?? (throw 'missing =');
variables[name] = expr();
case 'T':
output(true);
default:
throw 'syntax error: $line';
}
}
}
Test with intermediate calculations:
line = Input('A "Base: "B; A "Height: "H; S A=B*H/2; T "Area: "A!');
execute();
The run()
function parses multi-line programs and manages execution:
final lines = <String>[];
final stack = <(Input, int, bool)>[];
Input get line => stack.last.$1; // final definition
void run(String input) {
final validLine = RegExp(r'^\d\d\.\d\d ').hasMatch;
lines
..clear()
..addAll(input.split('\n').where(validLine));
stack.clear();
variables.clear();
_addLine(0);
execute();
}
void _addLine(int index, [bool g = false]) =>
stack.add((Input(lines[index].substring(6)), index + 1, g));
The stack
manages execution context - each entry contains the current line's Input
parser, the index of the next line to execute, and a flag whether DO
shall execute a group or a single line. The global line
reference always points to the topmost stack entry. The _addLine()
helper strips the line number prefix and pushes a new execution context together with the index of the next line and the optional "group" flag.
Enhanced execute()
for multi-line programs:
void execute() {
for (;;) {
if (line.isEmpty) {
final (_, next, g) = stack.removeLast();
if (next == lines.length) return;
_addLine(next, g);
continue;
}
if (line.matches(';')) continue;
// ... existing switch cases ...
}
}
When a line finishes, execution continues with the next sequential line.
Test with a simple multi-line program:
const program = '''
01.10 S A=5
01.20 S B=3
01.30 T "Sum: "A+B!
''';
run(program); // Sum: 8
Add commands as needed when running Hamurabi. Start with DO (subroutines):
void execute() {
for (;;) {
if (line.isEmpty) {
final (_, next, g) = stack.removeLast();
if (stack.isEmpty) {
if (next == lines.length) return;
_addLine(next, g);
} else {
if (g && next < lines.length && lines[next].substring(0, 2) == lines[next - 1].substring(0, 2)) {
_addLine(next, g);
}
}
continue;
}
// ... existing code ...
switch (line.next()) {
// ... existing cases ...
case 'D':
final (target, g) = _target();
_addLine(_index(target), g);
// ... rest of cases ...
}
}
}
(String, bool) _target() {
var t = line.match(RegExp(r'\d\d?(\.\d\d?)?')) ?? (throw 'missing jump target');
final parts = t.split('.');
if (parts.length == 1) return (parts.single.padLeft(2, '0'), true);
return ('${parts[0].padLeft(2, '0')}.${parts[1].padRight(2, '0')}', false);
}
int _index(String target) {
for (var i = 0; i < lines.length; i++) {
if (lines[i].startsWith(target)) return i;
}
throw 'no such line: $target';
}
If we're in a subroutine (stack depth > 1), we return to the caller if the end of the current group was reached or the end of the current line.
DO calls a subroutine by pushing the target line onto the stack. The _target()
function normalizes line numbers (e.g., "6" becomes "06", "4.3" becomes "04.30") for consistent prefix matching. It also returns whether a group call or a single line call shall be done.
The _index()
function then searches for the matching line number in our program.
Here is a test:
run('''
01.10 D 2;D 2.2
02.10 T"Hello
02.20 T"World
'''); // prints HelloWorldWorldHelloWorld
Add RETURN:
case 'R':
if (stack.length == 1) throw 'no subroutine to return from';
stack.removeLast();
Here is a test:
run('''
01.10 D 2;D 2.2
02.10 T"Hello;R
02.20 T"World
'''); // prints HelloWorldHello before crashing
Add GOTO (unconditional jump):
case 'G':
_setLine(_index(_target().$1));
void _setLine(int index) {
stack.removeLast();
_addLine(index);
}
GOTO simply jumps to another line using the existing target resolution and line indexing functions.
Here is the classic endless print loop:
run('01.10 T!"Hello, World!";G 1');
Add IF (conditional jumps):
case 'I':
final value = expr();
final targets = [_target().$1];
while (line.matches(',')) {
targets.add(_target().$1);
}
final index = value.compareTo(0) + 1;
if (index < targets.length) _setLine(_index(targets[index]));
IF evaluates an expression and conditionally jumps based on its sign. It takes 1-3 arguments: the line to jump to if negative, optionally if zero, and optionally if positive. For example, I (X-5)10,20,30
jumps to line 10 if X<5, line 20 if X=5, or line 30 if X>5. If fewer targets are provided or the condition doesn't match any target, execution continues normally.
Add FITR (truncate) and FRAN (random) functions to _factor()
because they are used by the Hamurabi code:
num _factor() {
// ... existing code ...
} else if (line.matches('FITR')) {
return expr().toInt();
} else if (line.matches('FRAN()')) {
return Random().nextDouble();
}
throw 'syntax error: $line';
}
Last but not least, add QUIT command to stop execution:
switch (line.next()) {
// ... existing cases ...
case 'Q':
return;
// ... rest of cases ...
}
Your complete interpreter can now run the Hamurabi game:
void main() {
focal.run(hamurabi);
}
Hopefully, the interpreter correctly handles all FOCAL commands and preserves the original behavior of the game.
We built a complete FOCAL interpreter by:
- Creating a flexible input tokenizer
- Implementing expression parsing with operator precedence
- Building output formatting for text and numbers
- Adding interactive commands (ASK, SET, TYPE)
- Supporting multi-line programs with line numbers
- Implementing control flow (DO, GOTO, IF)
- Adding built-in mathematical functions
The bottom-up approach let us test each component independently before integration, making debugging easier and ensuring correctness at each step.
Let's also run the lunar lander program.
-
S G=.001
is not valid because_factor
can only deal with integers so far. We have to change_digits
:final _digits = RegExp(r'\d+(\.\d+)?|\.\d+');
-
F X=1,51
is an unknown command. According to the manual, this a FOR loop which is used to type 51x.
Because that line is only reached on invalid input, we can ignore this. -
Q^2
is not supported. Let's rename_factor
to_power
and add a new_factor
implementation:num _factor() { for (var result = _power(); ;) { if (line.matches('^')) { result = pow(result, _power()); } else { return result; } } } num _power() { if (line.matches('-')) { return -_power(); // rest of the old _factor
-
The
FSQT
function is missing. It can be added to_factor
:} else if (line.matches('FSQT')) { return sqrt(expr());
-
I also need to support
%4.02
meaning that a number should be formatted asxx.yy
. Also, FOCAL will prefix each number with=
for whatever reason. I need to add this or the tabular display breaks. So let's add this tooutput
:} else if (line.matches('%')) { padding = expr(); } else if (withExpr) { final p = padding.toInt(); final f = ((padding - p) * 100).round(); stdout.write('= '); stdout.write(expr().toStringAsFixed(f).padLeft(p + f.sign));