Skip to content

Instantly share code, notes, and snippets.

@sma
Last active September 1, 2025 09:35
Show Gist options
  • Save sma/ff7dd4ae2a3b136a315c537df5646368 to your computer and use it in GitHub Desktop.
Save sma/ff7dd4ae2a3b136a315c537df5646368 to your computer and use it in GitHub Desktop.
A focal interpreter written in Dart, including a step-by-step tutorial how to do this

Building a FOCAL Interpreter in Dart

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.

What's This All About?

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.

Explaination

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.

Plan

We'll build the interpreter bottom-up:

  1. Input Class - Pattern matching foundation
  2. Expression Parser - Math expressions with precedence
  3. Output System - Text and number formatting
  4. Minimal Executor - Basic command loop with TYPE
  5. User Input & Semicolons - ASK command and statement separators
  6. Single-line Programs - Practical calculator examples
  7. Variables - SET command for storage
  8. Program Structure - Multi-line program loading
  9. Control Flow - DO, GOTO, IF commands as needed
  10. Built-ins - FITR, FRAN functions
  11. Integration - Complete Hamurabi game

Each step builds on the previous and can be tested immediately.

Input Class

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 consumed
  • match() consumes matching patterns, skipping whitespace
  • matches() tests patterns without consuming
  • toString() 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"

Expression Parser

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

Output System

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

Minimal Executor

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

User Input & Semicolons

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.

Single-line Programs

Test with a practical example - convert Celsius to Fahrenheit:

line = Input('A "Celsius: "C; T "Fahrenheit: "C*9/5+32!');
execute();

Variables

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();

Program Structure

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

Control Flow Commands

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.

Built-in Functions

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';
}

Program Termination

Last but not least, add QUIT command to stop execution:

switch (line.next()) {
  // ... existing cases ...
  case 'Q':
    return;
  // ... rest of cases ...
}

Final Integration

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.

Summary

We built a complete FOCAL interpreter by:

  1. Creating a flexible input tokenizer
  2. Implementing expression parsing with operator precedence
  3. Building output formatting for text and numbers
  4. Adding interactive commands (ASK, SET, TYPE)
  5. Supporting multi-line programs with line numbers
  6. Implementing control flow (DO, GOTO, IF)
  7. 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.

Addendum

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 as xx.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 to output:

        } 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));
import 'dart:io';
import 'dart:math';
final lines = <String>[];
final stack = <(Input, int, bool)>[];
Input get line => stack.last.$1;
set line(Input value) {
lines
..clear()
..add('00.00 ${value.string}');
stack.clear();
_addLine(0);
}
final variables = <String, num>{};
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 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;
}
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 'D':
final (target, g) = _target();
_addLine(_index(target), g);
case 'G':
_setLine(_index(_target().$1));
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]));
case 'Q':
return;
case 'R':
if (stack.length == 1) throw 'no subroutine to return from';
stack.removeLast();
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';
}
}
}
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)}';
}
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() {
for (var result = _power(); ;) {
if (line.matches('^')) {
result = pow(result, _power());
} else {
return result;
}
}
}
num _power() {
if (line.matches('-')) {
return -_power();
} else if (line.matches('(')) {
final result = expr();
line.match(')') ?? (throw 'missing ) in $line');
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 in $line');
} else if (line.matches('FITR')) {
return expr().toInt();
} else if (line.matches('FRAN()')) {
return Random().nextDouble();
} else if (line.matches('FSQT')) {
return sqrt(expr());
}
throw 'syntax error: $line';
}
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('%')) {
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));
} else {
break;
}
}
}
num padding = 8.04;
void _addLine(int index, [bool g = false]) {
stack.add((Input(lines[index].substring(6)), index + 1, g));
}
void _setLine(int index) {
stack.removeLast();
_addLine(index);
}
(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';
}
final _digits = RegExp(r'\d+(\.\d+)?|\.\d+');
final _variables = RegExp(r'[A-EG-Z]');
import 'package:focal/focal.dart' as focal;
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
''';
void main() {
focal.run(hamurabi);
}
import 'package:focal/focal.dart' as focal;
const lander = '''
01.04 T "CONTROL CALLING LUNAR MODULE.MANUAL CONTROL IS NECESSARY"!
01.06 T "YOU MAY RESET FUEL RATE K EACH 10 SECS TO 0 OR ANY VALUE"!
01.08 T "BETWEEN 8&200 LBS/SEC.YOU'VE 16000 LBS FUEL.ESTIMATED"!
01.11 T "FREE FALL IMPACT TIME-120 SECS.CAPSULE WEIGHT-32500 LBS"!
01.20 T "FIRST RADAR CHECK COMING UP"!!!
01.30 T "COMMENCE LANDING PROCEDURE"!"TIME,SECS ALTITUDE,"
01.40 T "MILES+FEET VELOCITY,MPH FUEL,LBS FUEL RATE"!
02.05 S L=0;S A=120;S V=1;S M=33000;S N=16500;S G=.001;S Z=1.8
02.10 T " ",%3,L," ",FITR(A)," ",%4,5280*(A-FITR(A))
02.20 T %6.02," ",3600*V," ",%6.01,M-N," K=";A K;S T=10
02.70 T %7.02;I (K)2.72;I (200-K)2.72;I (K-8)2.71,3.1,3.1
02.71 I (K-0)2.72,3.1,2.72
02.72 T "NOT POSSIBLE";F X=1,51;T "."
02.73 T "K=";A K;G 2.7
03.10 I ((M-N)-.001)4.1;I (T-.001)2.1;S S=T
03.40 I ((N+S*K)-M)3.5,3.5;S S=(M-N)/K
03.50 D 9;I (I)7.1,7.1;I (V)3.8,3.8;I (J)8.1
03.80 D 6;G 3.1
04.10 T "FUEL OUT AT",L," SECS"!
04.40 S S=(-V+FSQT(V*V+2*A*G))/G;S V=V+G*S;S L=L+S
05.10 T "ON THE MOON AT",L," SECS"!;S W=3600*V
05.20 T "IMPACT VELOCITY OF",W," M.P.H."!,"FUEL LEFT:"
05.30 T M-N," LBS."!;I (-W+1)5.5,5.5
05.40 T "PERFECT LANDING !-(LUCKY)"!;G 5.9
05.50 I (-W+10)5.6,5.6;T "GOOD LANDING-(COULD BE BETTER)"!;G 5.90
05.60 I (-W+25)5.7,5.7;T "CONGRATULATIONS ON A POOR LANDING"!;G 5.9
05.70 I (-W+60)5.8,5.8;T "CRAFT DAMAGE.GOOD LUCK"!;G 5.9
05.80 T "SORRY,BUT THERE WERE NO SURVIVORS-YOU BLEW IT!"!"IN"
05.81 T "FACT YOU BLASTED A NEW LUNAR CRATER",W*.277777,"FT.DEEP.
05.90 T "CONTROL OUT";Q
06.10 S L=L+S;S T=T-S;S M=M-S*K;S A=I;S V=J
07.10 I (S-.005)5.1;S S=2*A/(V+FSQT(V*V+2*A*(G-Z*K/M)))
07.30 D 9;D 6;G 7.1
08.10 S W=(1-M*G/(Z*K))/2;S S=M*V/(Z*K*(W+FSQT(W*W+V/Z)))+.05;D 9
08.30 I (I)7.1,7.1;D 6;I (-J)3.1,3.1;I (V)3.1,3.1,8.1
09.10 S Q=S*K/M;S J=V+G*S+Z*(-Q-Q^2/2-Q^3/3-Q^4/4-Q^5/5)
09.40 S I=A-G*S*S/2-V*S+Z*S*(Q/2+Q^2/6+Q^3/12+Q^4/20+Q^5/30)
''';
void main() {
focal.run(lander);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment