Chase
Description of the page content
Conversion of Chase to several programming languages.
This program has been converted to 13 programming languages.
Original
Origin of chase.bas:
The original file was found in a CP/M EBASIC disk image.
The original code was published in 1977 in "The Best of Creative Computing",
Volume 2, page 253:
https://www.atariarchives.org/bcc2/showpage.php?page=253
10 PRINT "THIS IS THE GAME OF CHASE"
60 PRINT "WANT INSTRUCTIONS";
70 INPUT C$
75 RANDOMIZE
80 IF LEFT$(C$,1)="N" THEN 200
90 IF LEFT$(C$,1)<>"Y" THEN 60
100 PRINT "YOU ARE '*' IN A HIGH VOLTAGE MAZE WITH 5"
110 PRINT "SECURITY MACHINES '+' TRYING TO DESTROY YOU"
120 PRINT "YOU MUST MANEUVER THE SECURITY MACHINES INTO"
130 PRINT "THE MAZE 'X' TO SURVIVE. GOOD LUCK !!!"
140 PRINT "MOVES ARE 7,8,9"
150 PRINT " 4,5,6"
160 PRINT " 1,2,3 0 TO END THE GAME"
170 PRINT
200 DIM A(10,20),E(21),F(21)
210 LET G=0
220 FOR B=1 TO 10
230 FOR C=1 TO 20
240 LET A(B,C)=0
250 IF B=1 THEN 285
260 IF B=10 THEN 285
270 IF C=1 THEN 285
280 IF C=20 THEN 285
283 GOTO 290
285 LET A(B,C)=1
290 NEXT C
300 NEXT B
310 FOR D=1 TO 21
320 LET B=INT(RND*8)+2
330 LET C=INT(RND*18)+2
340 IF A(B,C)<>0 THEN 320
350 LET A(B,C)=1
360 IF D<6 THEN 365
363 GOTO 370
365 LET A(B,C)=2
370 IF D=6 THEN 375
373 GOTO 380
375 LET A(B,C)=3
380 LET E(D)=B
390 LET F(D)=C
400 NEXT D
500 FOR B=1 TO 10
510 FOR C=1 TO 20
520 IF A(B,C)<>0 THEN 530
523 PRINT " ";
525 GOTO 560
530 IF A(B,C)<>1 THEN 540
533 PRINT "X";
535 GOTO 560
540 IF A(B,C)<>2 THEN 550
543 PRINT "+";
545 GOTO 560
550 IF A(B,C)<>3 THEN 560
555 PRINT "*";
560 NEXT C
570 PRINT
580 NEXT B
600 LET B=E(6)
610 LET C=F(6)
620 LET A(B,C)=0
630 INPUT Y
640 ON Y+1 GOTO 1400,680,680,680,690,800,690,660,660,660
660 LET B=B-1
670 GOTO 690
680 LET B=B+1
690 ON Y GOTO 700,800,720,700,800,720,700,800,720
700 LET C=C-1
710 GOTO 800
720 LET C=C+1
800 IF A(B,C)=1 THEN 1500
810 IF A(B,C)=2 THEN 1600
820 LET A(B,C)=3
830 LET E(6)=B
840 LET F(6)=C
850 FOR D=1 TO 5
860 IF A(E(D),F(D))<>2 THEN 960
870 LET A(E(D),F(D))=0
880 IF E(D)>=B THEN 890
883 LET E(D)=E(D)+1
885 GOTO 900
890 IF E(D)=B THEN 900
895 LET E(D)=E(D)-1
900 IF F(D)>=C THEN 910
903 LET F(D)=F(D)+1
905 GOTO 920
910 IF F(D)=C THEN 920
915 LET F(D)=F(D)-1
920 IF A(E(D),F(D))=3 THEN 1600
930 IF A(E(D),F(D))=0 THEN 940
933 LET G=G+1
935 GOTO 950
940 LET A(E(D),F(D))=2
950 IF G=5 THEN 1700
960 NEXT D
970 GOTO 500
1400 PRINT "SORRY TO SEE YOU QUIT"
1410 GOTO 1710
1500 PRINT "ZAP!!! YOU TOUCHED THE FENCE !!!!!"
1510 GOTO 1710
1600 PRINT "** YOU HAVE BEEN DESTROYED BY A LUCKY COMPUTER **"
1610 GOTO 1710
1700 PRINT "YOU ARE LUCKY **YOU DESTROYED ALL THE ENEMY**"
1710 PRINT "WANT TO PLAY AGAIN";
1720 INPUT C$
1730 IF LEFT$(C$,1)="Y" THEN 210
1735 IF LEFT$(C$,1)<>"N" THEN 1710
1740 PRINT "HOPE YOU DON'T FEEL FENCED IN."
1750 PRINT "TRY AGAIN SOMETIME"
2000 END
In C#
// Chase
// Original version in BASIC:
// Anonymous.
// Published in 1977 in "The Best of Creative Computing", Volume 2:
// https://www.atariarchives.org/bcc2/showpage.php?page=253
// This version in C#:
// Copyright (c) 2024, Marcos Cruz (programandala.net)
// SPDX-License-Identifier: Fair
//
// Written on 2025-01-01.
//
// Last modified 20251205T1534+0100.
using System;
using System.Linq;
class Chase
{
// Globals {{{1
// =============================================================
// Colors
const ConsoleColor DEFAULT_INK = ConsoleColor.White;
const ConsoleColor INPUT_INK = ConsoleColor.Green;
const ConsoleColor INSTRUCTIONS_INK = ConsoleColor.DarkYellow;
const ConsoleColor TITLE_INK = ConsoleColor.Red;
// Arena
const int ARENA_WIDTH = 20;
const int ARENA_HEIGHT = 10;
const int ARENA_LAST_X = ARENA_WIDTH - 1;
const int ARENA_LAST_Y = ARENA_HEIGHT - 1;
const int ARENA_ROW = 2;
static string[,] arena = new string[ARENA_HEIGHT, ARENA_WIDTH];
const string EMPTY = " ";
const string FENCE = "X";
const string MACHINE = "m";
const string HUMAN = "@";
const int FENCES = 15; // inner obstacles, not the border
// The end
enum End
{
NotYet,
Quit,
Electrified,
Killed,
Victory,
}
static End theEnd;
// Machines
const int MACHINES = 5;
const int MACHINES_DRAG = 2; // probability not moving: 0=0%, 1=50%, 2=66%, 3=75%, etc.
static int[] machineX = new int[MACHINES];
static int[] machineY = new int[MACHINES];
static bool[] operative = new bool[MACHINES];
static int destroyedMachines; // counter
// Human
static int humanX;
static int humanY;
// Random numbers generator
static Random rand = new Random();
// User input {{{1
// =============================================================
// Print the given prompt and wait until the user enters a string.
//
static string InputString(string prompt = "")
{
Console.ForegroundColor = INPUT_INK;
Console.Write(prompt);
string result = Console.ReadLine();
Console.ForegroundColor = DEFAULT_INK;
return result;
}
// Print the given prompt and wait until the user presses Enter.
//
static void PressEnter(string prompt)
{
InputString(prompt);
}
// Return `true` if the given string is "yes" or a synonym.
//
static bool IsYes(string s)
{
string[] validOptions = {"ok", "y", "yeah", "yes"};
return validOptions.Contains(s.ToLower());
}
// Return `true` if the given string is "no" or a synonym.
//
static bool IsNo(string s)
{
string[] validOptions = {"n", "no", "nope"};
return validOptions.Contains(s);
}
// Print the given prompt, wait until the user enters a valid yes/no string,
// and return `true` for "yes" or `false` for "no".
//
static bool Yes(string prompt)
{
while (true)
{
string answer = InputString(prompt);
if (IsYes(answer))
{
return true;
}
if (IsNo(answer))
{
return false;
}
}
}
// Title, credits and instructions {{{1
// =============================================================
const string TITLE = "Chase";
// Print the title at the current cursor position.
//
static void PrintTitle()
{
Console.ForegroundColor = TITLE_INK;
Console.WriteLine(TITLE);
Console.ForegroundColor = DEFAULT_INK;
}
static void PrintCredits()
{
PrintTitle();
Console.WriteLine("\nOriginal version in BASIC:");
Console.WriteLine(" Anonymous.");
Console.WriteLine(" Published in \"The Best of Creative Computing\", Volume 2, 1977.");
Console.WriteLine(" https://www.atariarchives.org/bcc2/showpage.php?page=253");
Console.WriteLine("\nThis version in C#:");
Console.WriteLine(" Copyright (c) 2024, Marcos Cruz (programandala.net)");
Console.WriteLine(" SPDX-License-Identifier: Fair");
}
// Print the game instructions and wait for a key.
//
static void PrintInstructions()
{
PrintTitle();
Console.ForegroundColor = INSTRUCTIONS_INK;
Console.WriteLine($"\nYou ({HUMAN}) are in a high voltage maze with {MACHINES}");
Console.WriteLine($"security machines ({MACHINE}) trying to kill you.");
Console.WriteLine($"You must maneuver them into the maze ({FENCE}) to survive.\n");
Console.WriteLine("Good luck!\n");
Console.WriteLine("The movement commands are the following:\n");
Console.WriteLine(" ↖ ↑ ↗");
Console.WriteLine(" NW N NE");
Console.WriteLine(" ← W E →");
Console.WriteLine(" SW S SE");
Console.WriteLine(" ↙ ↓ ↘");
Console.WriteLine("\nPlus 'Q' to end the game.");
Console.ForegroundColor = DEFAULT_INK;
}
// Arena {{{1
// =============================================================
// Display the arena at the top left corner of the screen.
//
static void PrintArena()
{
Console.SetCursorPosition(0, ARENA_ROW);
for (int y = 0; y <= ARENA_LAST_Y; y++)
{
for (int x = 0; x <= ARENA_LAST_X; x++)
{
Console.Write(arena[y, x]);
}
Console.WriteLine();
}
}
// If the given arena coordinates `y` and `x` are part of the border arena
// (i.e. its surrounding fence), return `true`, otherwise return `false`.
//
static bool IsBorder(int y, int x)
{
return (y == 0) || (x == 0) || (y == ARENA_LAST_Y) || (x == ARENA_LAST_X);
}
// Return a random integer in the given inclusive range.
//
static int RandomIntInInclusiveRange(int min, int max)
{
return rand.Next(max - min + 1) + min;
}
// Place the given string at a random empty position of the arena and return
// the coordinates.
//
static (int y, int x) Place(string s)
{
int y;
int x;
while (true)
{
y = RandomIntInInclusiveRange(1, ARENA_LAST_Y - 1);
x = RandomIntInInclusiveRange(1, ARENA_LAST_X - 1);
if (arena[y, x] == EMPTY)
{
break;
}
}
arena[y, x] = s;
return (y, x);
}
// Inhabit the arena with the machines, the inner fences and the human.
//
static void InhabitArena()
{
for (int m = 0; m < MACHINES; m++)
{
(machineY[m], machineX[m]) = Place(MACHINE);
operative[m] = true;
}
for (int _ = 0; _ < FENCES; _++) Place(FENCE);
(humanY, humanX) = Place(HUMAN);
}
// Clean the arena, i.e. empty it and add a surrounding fence.
//
static void CleanArena()
{
for (int y = 0; y <= ARENA_LAST_Y; y++)
{
for (int x = 0; x <= ARENA_LAST_X; x++)
{
arena[y, x] = IsBorder(y, x) ? FENCE : EMPTY;
}
}
}
// Game {{{1
// =============================================================
// Init the game.
//
static void InitGame()
{
CleanArena();
InhabitArena();
destroyedMachines = 0;
theEnd = End.NotYet;
}
// Move the given machine.
//
static void MoveMachine(int m)
{
int maybe;
arena[machineY[m], machineX[m]] = EMPTY;
maybe = rand.Next(2);
if (machineY[m] > humanY)
{
machineY[m] -= maybe;
}
else if (machineY[m] < humanY)
{
machineY[m] += maybe;
}
maybe = rand.Next(2);
if ((machineX[m] > humanX))
{
machineX[m] -= maybe;
}
else if ((machineX[m] < humanX))
{
machineX[m] += maybe;
}
if (arena[machineY[m], machineX[m]] == EMPTY)
{
arena[machineY[m], machineX[m]] = MACHINE;
}
else if (arena[machineY[m], machineX[m]] == FENCE)
{
operative[m] = false;
destroyedMachines += 1;
if (destroyedMachines == MACHINES)
{
theEnd = End.Victory;
}
}
else if (arena[machineY[m], machineX[m]] == HUMAN)
{
theEnd = End.Killed;
}
}
// Maybe move the given operative machine.
//
static void MaybeMoveMachine(int m)
{
if (rand.Next(MACHINES_DRAG) == 0)
{
MoveMachine(m);
}
}
// Move the operative machines.
//
static void MoveMachines()
{
for (int m = 0; m < MACHINES; m++)
{
if (operative[m])
{
MaybeMoveMachine(m);
}
}
}
// Erase the current line to the right from the position of the cursor.
//
static void EraseLineRight()
{
int y = Console.CursorTop;
int x = Console.CursorLeft;
string blank = new String(' ', Console.WindowWidth - x - 1);
Console.Write(blank);
Console.SetCursorPosition(x, y);
}
// Read a user command; update `theEnd` accordingly and return the direction
// increments.
//
static (int yInc, int xInc) GetMove()
{
int yInc = 0;
int xInc = 0;
Console.WriteLine();
EraseLineRight();
string command = InputString("Command: ").ToLower();
switch (command)
{
case "q" : theEnd = End.Quit; break;
case "sw" : yInc = +1; xInc = -1; break;
case "s" : yInc = +1; break;
case "se" : yInc = +1; xInc = +1; break;
case "w" : xInc = -1; break;
case "e" : xInc = +1; break;
case "nw" : yInc = -1; xInc = -1; break;
case "n" : yInc = -1; break;
case "ne" : yInc = -1; xInc = +1; break;
}
return (yInc, xInc);
}
static void Play()
{
int yInc;
int xInc;
while (true) { // game loop
Console.Clear();
PrintTitle();
InitGame();
while (true) { // action loop
PrintArena();
(yInc, xInc) = GetMove();
if (theEnd == End.NotYet)
{
if (yInc != 0 || xInc != 0)
{
arena[humanY, humanX] = EMPTY;
if (arena[humanY + yInc, humanX + xInc] == FENCE)
{
theEnd = End.Electrified;
}
else if (arena[humanY + yInc, humanX + xInc] == MACHINE)
{
theEnd = End.Killed;
}
else
{
arena[humanY, humanX] = EMPTY;
humanY = humanY + yInc;
humanX = humanX + xInc;
arena[humanY, humanX] = HUMAN;
PrintArena();
MoveMachines();
}
}
}
if (theEnd != End.NotYet)
{
break;
}
}// action loop;
switch (theEnd)
{
case End.Quit :
Console.WriteLine("\nSorry to see you quit.");
break;
case End.Electrified :
Console.WriteLine("\nZap! You touched the fence!");
break;
case End.Killed :
Console.WriteLine("\nYou have been killed by a lucky machine.");
break;
case End.Victory :
Console.WriteLine("\nYou are lucky, you destroyed all machines.");
break;
}
if (! Yes("\nDo you want to play again? "))
{
break;
}
}; // game loop
Console.WriteLine("\nHope you don't feel fenced in.");
Console.WriteLine("Try again sometime.");
}
// Main {{{1
// =============================================================
static void Main()
{
Console.ForegroundColor = DEFAULT_INK;
Console.Clear();
PrintCredits();
PressEnter("\nPress the Enter key to read the instructions. ");
Console.Clear();
PrintInstructions();
PressEnter("\nPress the Enter key to start. ");
Play();
}
}
In Chapel
// Chase
// Original version in BASIC:
// Anonymous.
// Published in 1977 in "The Best of Creative Computing", Volume 2.
// https://www.atariarchives.org/bcc2/showpage.php?page=253
// This version in Chapel:
// Copyright (c) 2025, Marcos Cruz (programandala.net)
// SPDX-License-Identifier: Fair
//
// Written on 2025-04-08.
// Last modified: 20250731T1954+0200.
// Modules {{{1
// =============================================================================
import IO;
import Random;
// Terminal {{{1
// =============================================================================
const BLACK = 0;
const RED = 1;
const GREEN = 2;
const YELLOW = 3;
const BLUE = 4;
const MAGENTA = 5;
const CYAN = 6;
const WHITE = 7;
const DEFAULT = 9;
const STYLE_OFF = 20;
const FOREGROUND = 30;
const BACKGROUND = 40;
const BRIGHT = 60;
const NORMAL_STYLE = 0;
proc moveCursorHome() {
write("\x1B[H");
}
proc setCursorPosition(line: int, col: int) {
writef("\x1B[%i;%iH", line, col);
}
proc hideCursor() {
write("\x1B[?25l");
}
proc showCursor() {
write("\x1B[?25h");
}
proc setStyle(style: int) {
writef("\x1B[%im", style);
}
proc resetAttributes() {
setStyle(NORMAL_STYLE);
}
proc eraseLineToEnd() {
write("\x1B[K");
}
proc eraseScreen() {
write("\x1B[2J");
}
proc clearScreen() {
eraseScreen();
resetAttributes();
moveCursorHome();
}
// Config {{{1
// =============================================================
const DEFAULT_INK = WHITE + FOREGROUND;
const INPUT_INK = BRIGHT + GREEN + FOREGROUND;
const INSTRUCTIONS_INK = YELLOW + FOREGROUND;
const TITLE_INK = BRIGHT + RED + FOREGROUND;
// Data {{{1
// =============================================================
// Arena
const ARENA_WIDTH = 20;
const ARENA_HEIGHT = 10;
const ARENA_LAST_X = ARENA_WIDTH - 1;
const ARENA_LAST_Y = ARENA_HEIGHT - 1;
const ARENA_ROW = 3;
const arenaDomain: domain(2) = {0 ..< ARENA_HEIGHT, 0 ..< ARENA_WIDTH};
var arena: [arenaDomain] string;
const EMPTY = " ";
const FENCE = "X";
const MACHINE = "m";
const HUMAN = "@";
const FENCES = 15; // inner obstacles, not the border
// The end
enum End {
NOT_YET,
QUIT,
ELECTRIFIED,
KILLED,
VICTORY
}
var theEnd: End = End.NOT_YET;
// Machines
const MACHINES = 5;
// probability not moving: 0=0%, 1=50%, 2=66%, 3=75%, etc.
const MACHINES_DRAG = 2;
var machineX: [0 ..< MACHINES] int;
var machineY: [0 ..< MACHINES] int;
var operative: [0 ..< MACHINES] bool;
var destroyedMachines: int = 0; // counter
// Human
var humanX: int = 0;
var humanY: int = 0;
// User input {{{1
// =============================================================
proc acceptString(prompt: string): string {
write(prompt);
IO.stdout.flush();
return IO.readLine().strip();
}
proc getString(prompt: string): string {
setStyle(INPUT_INK);
defer setStyle(DEFAULT_INK);
return acceptString(prompt);
}
proc pressEnter(prompt: string) {
acceptString(prompt);
}
proc isYes(s: string): bool {
select s.toLower() {
when "ok", "y", "yeah", "yes" do return true;
otherwise do return false;
}
}
proc isNo(s: string): bool {
select s.toLower() {
when "n", "no", "nope" do return true;
otherwise do return false;
}
}
// Print the given prompt, wait until the user enters a valid yes/no string,
// and return `true` for "yes" or `false` for "no".
//
proc yes(prompt: string): bool {
var result: bool;
while true {
var answer: string = getString(prompt);
if isYes(answer) {
result = true;
break;
}
if isNo(answer) {
result = false;
break;
}
}
return result;
}
// Title, credits and instructions {{{1
// =============================================================
const TITLE = "Chase";
proc printTitle() {
setStyle(TITLE_INK);
writef("%s\n", TITLE);
setStyle(DEFAULT_INK);
}
proc printCredits() {
printTitle();
writeln("\nOriginal version in BASIC:");
writeln(" Anonymous.");
writeln(' Published in "The Best of Creative Computing", Volume 2, 1977.');
writeln(" https://www.atariarchives.org/bcc2/showpage.php?page=253");
writeln("This version in Chapel:");
writeln(" Copyright (c) 2025, Marcos Cruz (programandala.net)");
writeln(" SPDX-License-Identifier: Fair");
}
proc printInstructions() {
printTitle();
setStyle(INSTRUCTIONS_INK);
writef("\nYou (%s) are in a high voltage maze with %i\n", HUMAN, MACHINES);
writef("security machines (%s) trying to kill you.\n", MACHINE);
writef("You must maneuver them into the maze (%s) to survive.\n\n", FENCE);
writeln("Good luck!\n");
writeln("The movement commands are the following:\n");
writeln(" ↖ ↑ ↗");
writeln(" NW N NE");
writeln(" ← W E →");
writeln(" SW S SE");
writeln(" ↙ ↓ ↘");
writeln("\nPlus 'Q' to end the game.");
setStyle(DEFAULT_INK);
}
// Arena {{{1
// =============================================================
proc printArena() {
setCursorPosition(ARENA_ROW, 1);
for y in 0 .. ARENA_LAST_Y {
for x in 0 .. ARENA_LAST_X {
write(arena[y, x]);
}
write("\n");
}
}
// If the given arena coordinates `y` and `x` are part of the border arena
// (i.e. its surrounding fence), return `true`, otherwise return `false`.
//
proc isBorder(y: int, x: int): bool {
return (y == 0) || (x == 0) || (y == ARENA_LAST_Y) || (x == ARENA_LAST_X);
}
var rsInt = new Random.randomStream(int);
proc randomIntInInclusiveRange(min: int, max: int): int {
return rsInt.next(min, min + max - 1);
}
// Place the given string at a random empty position of the arena and return
// the coordinates.
//
proc place(s: string): [] int {
var y: int = 0;
var x: int = 0;
while true {
y = randomIntInInclusiveRange(1, ARENA_LAST_Y - 1);
x = randomIntInInclusiveRange(1, ARENA_LAST_X - 1);
if arena[y, x] == EMPTY {
break;
}
}
arena[y, x] = s;
return [y, x];
}
// Inhabit the arena with the machines, the inner fences and the human.
//
proc inhabitArena() {
var coords: [0 .. 1] int;
for m in 0 ..< MACHINES {
coords = place(MACHINE);
machineY[m] = coords[0];
machineX[m] = coords[1];
operative[m] = true;
}
for 0 ..< FENCES {
place(FENCE);
}
coords = place(HUMAN);
humanY = coords[0];
humanX = coords[1];
}
// Clean the arena, i.e. empty it and add a surrounding fence.
//
proc cleanArena() {
for y in 0 ..< ARENA_HEIGHT {
for x in 0 ..< ARENA_WIDTH {
arena[y, x] = if isBorder(y, x) then FENCE else EMPTY;
}
}
}
// Game {{{1
// =============================================================
proc initGame() {
cleanArena();
inhabitArena();
destroyedMachines = 0;
theEnd = End.NOT_YET;
}
proc moveMachine(m: int) {
var maybe: int = 0;
arena[machineY[m], machineX[m]] = EMPTY;
maybe = rsInt.next(0, 2 - 1);
if machineY[m] > humanY {
machineY[m] -= maybe;
} else if machineY[m] < humanY {
machineY[m] += maybe;
}
maybe = rsInt.next(0, 2 - 1);
if machineX[m] > humanX {
machineX[m] -= maybe;
} else if machineX[m] < humanX {
machineX[m] += maybe;
}
select true {
when arena[machineY[m], machineX[m]] == EMPTY {
arena[machineY[m], machineX[m]] = MACHINE;
}
when arena[machineY[m], machineX[m]] == FENCE {
operative[m] = false;
destroyedMachines += 1;
if destroyedMachines == MACHINES {
theEnd = End.VICTORY;
}
}
when arena[machineY[m], machineX[m]] == HUMAN {
theEnd = End.KILLED;
}
}
}
proc maybeMoveMachine(m: int) {
if rsInt.next(0, MACHINES_DRAG - 1) == 0 {
moveMachine(m);
}
}
proc moveMachines() {
for m in 0 ..< MACHINES {
if operative[m] {
maybeMoveMachine(m);
}
}
}
// Read a user command; update `theEnd` accordingly and return the direction
// increments.
//
proc getMove(): [] int {
var yInc: int = 0;
var xInc: int = 0;
write("\n");
eraseLineToEnd();
var command: string = getString("Command: ").toLower();
select command {
when "q" {
theEnd = End.QUIT;
}
when "sw" {
yInc = 1;
xInc = -1;
}
when "s" {
yInc = 1;
}
when "se" {
yInc = 1;
xInc = 1;
}
when "w" {
xInc = -1;
}
when "e" {
xInc = 1;
}
when "nw" {
yInc = -1;
xInc = -1;
}
when "n" {
yInc = -1;
}
when "ne" {
yInc = -1;
xInc = 1;
}
}
return [yInc, xInc];
}
proc play() {
var yInc: int = 0;
var xInc: int = 0;
while true { // game loop
clearScreen();
printTitle();
initGame();
label actionLoop while true {
printArena();
var coords: [0 .. 1] int = getMove();
yInc = coords[0];
xInc = coords[1];
if theEnd == End.NOT_YET {
if yInc != 0 || xInc != 0 {
arena[humanY, humanX] = EMPTY;
if arena[humanY + yInc, humanX + xInc] == FENCE {
theEnd = End.ELECTRIFIED;
} else if arena[humanY + yInc, humanX + xInc] == MACHINE {
theEnd = End.KILLED;
} else {
arena[humanY, humanX] = EMPTY;
humanY = humanY + yInc;
humanX = humanX + xInc;
arena[humanY, humanX] = HUMAN;
printArena();
moveMachines();
}
}
}
select theEnd {
when End.QUIT {
writeln("\nSorry to see you quit.");
break actionLoop;
}
when End.ELECTRIFIED {
writeln("\nZap! You touched the fence!");
break actionLoop;
}
when End.KILLED {
writeln("\nYou have been killed by a lucky machine.");
break actionLoop;
}
when End.VICTORY {
writeln("\nYou are lucky, you destroyed all machines.");
break actionLoop;
}
}
} // action loop;
if !yes("\nDo you want to play again? ") {
break;
}
} // game loop
writeln("\nHope you don't feel fenced in.");
writeln("Try again sometime.");
}
// Main {{{1
// =============================================================
proc main() {
setStyle(DEFAULT_INK);
clearScreen();
printCredits();
pressEnter("\nPress the Enter key to read the instructions. ");
clearScreen();
printInstructions();
pressEnter("\nPress the Enter key to start. ");
play();
}
In Crystal
# Chase
# Original version in BASIC:
#
# Anonymous.
# Published in 1977 in "The Best of Creative Computing", Volume 2.
#
# https://www.atariarchives.org/bcc2/showpage.php?page=253
# This version in Crystal:
#
# Copyright (c) 2024, Marcos Cruz (programandala.net)
# SPDX-License-Identifier: Fair
#
# Written in 2024-11-27/29.
#
# Last modified: 20250511T2305+0200.
# Terminal {{{1
# ==============================================================================
# Screen colors
BLACK = 0
RED = 1
GREEN = 2
YELLOW = 3
BLUE = 4
MAGENTA = 5
CYAN = 6
WHITE = 7
DEFAULT = 9
# Screen attributes
NORMAL = 0
# Screen color offsets
FOREGROUND = +30
BRIGHT = +60
# Moves the cursor to the home position.
def home
print "\e[H"
end
# Clears the screen and moves the cursor to the home position.
def clear_screen
print "\e[2J"
home
end
def set_color(color : Int)
print "\e[#{color}m"
end
def set_attribute(attr : Int)
print "\e[0;#{attr}m"
end
# Sets the cursor position to the given coordinates (the top left position is 1, 1).
def set_cursor_position(line, column : Int)
print "\e[#{line};#{column}H"
end
def erase_line_right
print "\e[K"
end
def hide_cursor
print "\e[?25l"
end
def show_cursor
print "\e[?25h"
end
# Globals {{{1
# =============================================================
# Colors
DEFAULT_INK = FOREGROUND + WHITE
INPUT_INK = FOREGROUND + BRIGHT + GREEN
INSTRUCTIONS_INK = FOREGROUND + YELLOW
TITLE_INK = FOREGROUND + BRIGHT + RED
# Arena
ARENA_WIDTH = 20
ARENA_HEIGHT = 10
ARENA_LAST_X = ARENA_WIDTH - 1
ARENA_LAST_Y = ARENA_HEIGHT - 1
ARENA_ROW = 3
EMPTY = " "
FENCE = "X"
MACHINE = "m"
HUMAN = "@"
FENCES = 15 # inner obstacles, not the border
# The end
enum End
Not_Yet
Quit
Electrified
Killed
Victory
end
the_end : End
# Machines
# XXX TODO
# class Machine
# property x : Int32
# property y : Int32
# property operative : Bool
#
# def initialize
# @x = 0
# @y = 0
# @operative = true
# end
# end
MACHINES = 5
MACHINES_DRAG = 2 # probability not moving: 0=0%, 1=50%, 2=66%, 3=75%, etc.
class Global
class_property arena = Array(Array(String)).new(ARENA_HEIGHT) { Array(String).new(ARENA_WIDTH, "") }
class_property machine_x : Array(Int32) = Array.new(MACHINES, 0)
class_property machine_y : Array(Int32) = Array.new(MACHINES, 0)
class_property operative : Array(Bool) = Array.new(MACHINES, true)
class_property destroyed_machines = 0 # counter
class_property human_x = 0
class_property human_y = 0
end
# User input {{{1
# =============================================================
# Print the given prompt and wait until the user enters a String.
def input_string(prompt : String = "") : String
s = nil
set_color(INPUT_INK)
while s.is_a?(Nil)
print prompt
s = gets
end
set_color(DEFAULT_INK)
s
end
# Print the given prompt and wait until the user presses Enter.
def press_enter(prompt : String)
input_string(prompt)
end
# Return `true` if the given String is "yes" or a synonym.
def is_yes?(answer : String) : Bool
["ok", "yeah", "yes", "y"].any? { |yes| answer.downcase == yes }
end
# Return `true` if the given String is "no" or a synonym.
def is_no?(answer : String) : Bool
["no", "nope", "n"].any? { |no| answer.downcase == no }
end
# Print the given prompt, wait until the user enters a valid yes/no
# String, and return `true` for "yes" or `false` for "no".
def yes?(prompt : String) : Bool
while true
answer = input_string(prompt)
if is_yes?(answer)
return true
end
if is_no?(answer)
return false
end
end
end
# Title, credits and instructions {{{1
# =============================================================
TITLE = "Chase"
# Print the title at the current cursor position.
def print_title
set_color(TITLE_INK)
puts TITLE
set_color(DEFAULT_INK)
end
def print_credits
print_title
puts
puts "Original version in BASIC:"
puts " Anonymous."
puts " Published in \"The Best of Creative Computing\", Volume 2, 1977."
puts " https://www.atariarchives.org/bcc2/showpage.php?page=253"
puts
puts "This version in Crystal:"
puts " Copyright (c) 2024, Marcos Cruz (programandala.net)"
puts " SPDX-License-Identifier: Fair"
end
# Print the game instructions and wait for a key.
def print_instructions
print_title
set_color(INSTRUCTIONS_INK)
puts "\nYou (#{HUMAN}) are in a high voltage maze with #{MACHINES}"
puts "security machines (#{MACHINE}) trying to kill you."
puts "You must maneuver them into the maze (#{FENCE}) to survive."
puts
puts "Good luck!"
puts
puts "The movement commands are the following:"
puts
puts " ↖ ↑ ↗"
puts " NW N NE"
puts " ← W E →"
puts " SW S SE"
puts " ↙ ↓ ↘"
puts "\nPlus 'Q' to end the game."
set_color(DEFAULT_INK)
end
# Arena {{{1
# =============================================================
# Display the arena at the top left corner of the screen.
def print_arena
set_cursor_position(ARENA_ROW, 1)
(0..ARENA_LAST_Y).each do |y|
(0..ARENA_LAST_X).each do |x|
print Global.arena[y][x]
end
puts
end
end
# If the given arena coordinates `y` and `x` are part of the border arena (i.e.
# its surrounding fence), return `true`, otherwise return `false`.
def is_border?(y : Int, x : Int) : Bool
return (y == 0) || (x == 0) || (y == ARENA_LAST_Y) || (x == ARENA_LAST_X)
end
# Return a random integer in the given inclusive range.
def random_int_in_inclusive_range(min, max : Int) : Int
return rand(max - min + 1) + min
end
# Place the given String at a random empty position of the arena and return
# the coordinates.
def place(s : String) : Tuple
while true
y = random_int_in_inclusive_range(1, ARENA_LAST_Y - 1)
x = random_int_in_inclusive_range(1, ARENA_LAST_X - 1)
if Global.arena[y][x] == EMPTY
break
end
end
Global.arena[y][x] = s
{y, x}
end
# Inhabit the arena with the machines, the inner fences and the human.
def inhabit_arena
(0...MACHINES).each do |m|
Global.machine_y[m], Global.machine_x[m] = place(MACHINE)
Global.operative[m] = true
end
(0...FENCES).each do
place(FENCE)
end
Global.human_y, Global.human_x = place(HUMAN)
end
# Clean the arena, i.e. empty it and add a surrounding fence.
def clean_arena
(0..ARENA_LAST_Y).each do |y|
(0..ARENA_LAST_X).each do |x|
Global.arena[y][x] = is_border?(y, x) ? FENCE : EMPTY
end
end
end
# Game {{{1
# =============================================================
# Move the given machine.
def move_machine(m : Int)
maybe : Int32
Global.arena[Global.machine_y[m]][Global.machine_x[m]] = EMPTY
maybe = rand(2)
if Global.machine_y[m] > Global.human_y
Global.machine_y[m] -= maybe
elsif Global.machine_y[m] < Global.human_y
Global.machine_y[m] += maybe
end
maybe = rand(2)
if (Global.machine_x[m] > Global.human_x)
Global.machine_x[m] -= maybe
elsif (Global.machine_x[m] < Global.human_x)
Global.machine_x[m] += maybe
end
if Global.arena[Global.machine_y[m]][Global.machine_x[m]] == EMPTY
Global.arena[Global.machine_y[m]][Global.machine_x[m]] = MACHINE
elsif Global.arena[Global.machine_y[m]][Global.machine_x[m]] == FENCE
Global.operative[m] = false
Global.destroyed_machines += 1
if Global.destroyed_machines == MACHINES
the_end = End::Victory
end
elsif Global.arena[Global.machine_y[m]][Global.machine_x[m]] == HUMAN
the_end = End::Killed
end
end
# Maybe move the given operative machine.
def maybe_move_machine(m : Int)
if rand(MACHINES_DRAG) == 0
move_machine(m)
end
end
# Move the operative machines.
def move_machines
(0...MACHINES).each do |m|
if Global.operative[m]
maybe_move_machine(m)
end
end
end
# Read a user command; update `the_end` accordingly and return the direction
# increments.
def get_move : Tuple
y_inc = 0
x_inc = 0
puts
erase_line_right
command = input_string("Command: ").downcase
case command
when "q" ; the_end = End::Quit
when "sw"; y_inc = +1; x_inc = -1
when "s" ; y_inc = +1
when "se"; y_inc = +1; x_inc = +1
when "w" ; x_inc = -1
when "e" ; x_inc = +1
when "nw"; y_inc = -1; x_inc = -1
when "n" ; y_inc = -1
when "ne"; y_inc = -1; x_inc = +1
end
return y_inc, x_inc
end
def play
y_inc : Int32
x_inc : Int32
moving : Bool
while true # game loop
clear_screen
print_title
# init game
clean_arena
inhabit_arena
Global.destroyed_machines = 0
the_end = End::Not_Yet
while true # action loop
print_arena
y_inc, x_inc = get_move
if the_end == End::Not_Yet
if y_inc != 0 || x_inc != 0
Global.arena[Global.human_y][Global.human_x] = EMPTY
if Global.arena[Global.human_y + y_inc][Global.human_x + x_inc] == FENCE
the_end = End::Electrified
elsif Global.arena[Global.human_y + y_inc][Global.human_x + x_inc] == MACHINE
the_end = End::Killed
else
Global.arena[Global.human_y][Global.human_x] = EMPTY
Global.human_y = Global.human_y + y_inc
Global.human_x = Global.human_x + x_inc
Global.arena[Global.human_y][Global.human_x] = HUMAN
print_arena
move_machines
end
end
end
if the_end != End::Not_Yet
break
end
end # action loop
puts
case the_end
when End::Quit
puts "Sorry to see you quit."
when End::Electrified
puts "Zap! You touched the fence!"
when End::Killed
puts "You have been killed by a lucky machine."
when End::Victory
puts "You are lucky, you destroyed all machines."
end
if !yes?("\nDo you want to play again? ")
break
end
end # game loop
puts "\nHope you don't feel fenced in."
puts "Try again sometime."
end
# Main {{{1
# =============================================================
set_color(DEFAULT_INK)
clear_screen
print_credits
press_enter("\nPress the Enter key to read the instructions. ")
clear_screen
print_instructions
press_enter("\nPress the Enter key to start. ")
play
In D
// Chase
// Original version in BASIC:
// Anonymous.
// Published in 1977 in "The Best of Creative Computing", Volume 2.
// https://www.atariarchives.org/bcc2/showpage.php?page=253
// This version in D:
// Copyright (c) 2025, Marcos Cruz (programandala.net)
// SPDX-License-Identifier: Fair
//
// Written on 2025-03-23.
// Last modified: 20251220T0637+0100.
module chase;
// Terminal {{{1
// =============================================================================
enum BLACK = 0;
enum RED = 1;
enum GREEN = 2;
enum YELLOW = 3;
enum BLUE = 4;
enum MAGENTA = 5;
enum CYAN = 6;
enum WHITE = 7;
enum DEFAULT = 9;
enum STYLE_OFF = 20;
enum FOREGROUND = 30;
enum BACKGROUND = 40;
enum BRIGHT = 60;
enum NORMAL_STYLE = 0;
void moveCursorHome()
{
import std.stdio : write;
write("\x1B[H");
}
void setCursorPosition(int line, int col)
{
import std.stdio : writef;
writef("\x1B[%d;%dH", line, col);
}
void hideCursor()
{
import std.stdio : write;
write("\x1B[?25l");
}
void showCursor()
{
import std.stdio : write;
write("\x1B[?25h");
}
void setStyle(int style)
{
import std.stdio : writef;
writef("\x1B[%dm", style);
}
void resetAttributes()
{
setStyle(NORMAL_STYLE);
}
void eraseLineToEnd()
{
import std.stdio : write;
write("\x1B[K");
}
void eraseScreen()
{
import std.stdio : write;
write("\x1B[2J");
}
void clearScreen()
{
eraseScreen();
resetAttributes();
moveCursorHome();
}
// Config {{{1
// =============================================================
enum DEFAULT_INK = WHITE + FOREGROUND;
enum INPUT_INK = BRIGHT + GREEN + FOREGROUND;
enum INSTRUCTIONS_INK = YELLOW + FOREGROUND;
enum TITLE_INK = BRIGHT + RED + FOREGROUND;
// Data {{{1
// =============================================================
// Arena
enum ARENA_WIDTH = 20;
enum ARENA_HEIGHT = 10;
enum ARENA_LAST_X = ARENA_WIDTH - 1;
enum ARENA_LAST_Y = ARENA_HEIGHT - 1;
enum ARENA_ROW = 3;
//string[ARENA_HEIGHT][ARENA_WIDTH] arena;
string[ARENA_WIDTH][ARENA_HEIGHT] arena;
enum EMPTY = " ";
enum FENCE = "X";
enum MACHINE = "m";
enum HUMAN = "@";
enum FENCES = 15; // inner obstacles, not the border
// The end
enum End
{
NOT_YET,
QUIT,
ELECTRIFIED,
KILLED,
VICTORY
}
End theEnd = End.NOT_YET;
// Machines
enum MACHINES = 5;
enum MACHINES_DRAG = 2; // probability not moving: 0=0%, 1=50%, 2=66%, 3=75%, etc.
int[MACHINES] machineX;
int[MACHINES] machineY;
bool[MACHINES] operative;
int destroyedMachines = 0; // counter
// Human
int humanX = 0;
int humanY = 0;
// User input {{{1
// =============================================================
string acceptString(string prompt)
{
import std.stdio : readln;
import std.stdio : write;
import std.string : strip;
write(prompt);
return strip(readln());
}
string getString(string prompt)
{
setStyle(INPUT_INK);
string s = acceptString(prompt);
setStyle(DEFAULT_INK);
return s;
}
void pressEnter(string prompt)
{
acceptString(prompt);
}
bool isYes(string s)
{
import std.uni : toLower;
switch (toLower(s))
{
case "ok":
case "y":
case "yeah":
case "yes":
return true;
default:
return false;
}
}
bool isNo(string s)
{
import std.uni : toLower;
switch (toLower(s))
{
case "n":
case "no":
case "nope":
return true;
default:
return false;
}
}
// Print the given prompt, wait until the user enters a valid yes/no string,
// and return `true` for "yes" or `false` for "no".
bool yes(string prompt)
{
while (true)
{
string answer = getString(prompt);
if (isYes(answer))
{
return true;
}
if (isNo(answer))
{
return false;
}
}
}
// Title, credits and instructions {{{1
// =============================================================
enum TITLE = "Chase";
void printTitle()
{
import std.stdio : writef;
setStyle(TITLE_INK);
writef("%s\n", TITLE);
setStyle(DEFAULT_INK);
}
void printCredits()
{
import std.stdio : writeln;
printTitle();
writeln("\nOriginal version in BASIC:");
writeln(" Anonymous.");
writeln(" Published in \"The Best of Creative Computing\", Volume 2, 1977.");
writeln(" https://www.atariarchives.org/bcc2/showpage.php?page=253");
writeln("This version in D:");
writeln(" Copyright (c) 2025, Marcos Cruz (programandala.net)");
writeln(" SPDX-License-Identifier: Fair");
}
void printInstructions()
{
import std.stdio : writef;
import std.stdio : writeln;
printTitle();
setStyle(INSTRUCTIONS_INK);
writef("\nYou (%s) are in a high voltage maze with %d\n", HUMAN, MACHINES);
writef("security machines (%s) trying to kill you.\n", MACHINE);
writef("You must maneuver them into the maze (%s) to survive.\n\n", FENCE);
writeln("Good luck!\n");
writeln("The movement commands are the following:\n");
writeln(" ↖ ↑ ↗");
writeln(" NW N NE");
writeln(" ← W E →");
writeln(" SW S SE");
writeln(" ↙ ↓ ↘");
writeln("\nPlus 'Q' to end the game.");
setStyle(DEFAULT_INK);
}
// Arena {{{1
// =============================================================
void printArena()
{
import std.stdio : write;
setCursorPosition(ARENA_ROW, 1);
foreach (int y; 0 .. ARENA_LAST_Y + 1)
{
foreach (int x; 0 .. ARENA_LAST_X + 1)
{
write(arena[y][x]);
}
write("\n");
}
}
// If the given arena coordinates `y` and `x` are part of the border arena
// (i.e. its surrounding fence), return `true`, otherwise return `false`.
bool isBorder(int y, int x)
{
return (y == 0) || (x == 0) || (y == ARENA_LAST_Y) || (x == ARENA_LAST_X);
}
int randomIntInInclusiveRange(int min, int max)
{
import std.random : uniform;
return uniform(min, min + max);
}
// Place the given string at a random empty position of the arena and return
// the coordinates.
int[] place(string s)
{
int y = 0;
int x = 0;
while (true)
{
y = randomIntInInclusiveRange(1, ARENA_LAST_Y - 1);
x = randomIntInInclusiveRange(1, ARENA_LAST_X - 1);
if (arena[y][x] == EMPTY)
{
break;
}
}
arena[y][x] = s;
return [y, x];
}
// Inhabit the arena with the machines, the inner fences and the human.
void inhabitArena()
{
int[] coords;
foreach (int m; 0 .. MACHINES)
{
coords = place(MACHINE);
machineY[m] = coords[0];
machineX[m] = coords[1];
operative[m] = true;
}
foreach (int i; 0 .. FENCES)
{
place(FENCE);
}
coords = place(HUMAN);
humanY = coords[0];
humanX = coords[1];
}
// Clean the arena, i.e. empty it and add a surrounding fence.
void cleanArena()
{
foreach (int y; 0 .. ARENA_LAST_Y + 1)
{
foreach (int x; 0 .. ARENA_LAST_X + 1)
{
arena[y][x] = isBorder(y, x) ? FENCE : EMPTY;
}
}
}
// Game {{{1
// =============================================================
void initGame()
{
cleanArena();
inhabitArena();
destroyedMachines = 0;
theEnd = End.NOT_YET;
}
void moveMachine(int m)
{
import std.random : uniform;
int maybe = 0;
arena[machineY[m]][machineX[m]] = EMPTY;
maybe = uniform(0, 2);
if (machineY[m] > humanY)
{
machineY[m] -= maybe;
}
else if (machineY[m] < humanY)
{
machineY[m] += maybe;
}
maybe = uniform(0, 2);
if (machineX[m] > humanX)
{
machineX[m] -= maybe;
}
else if (machineX[m] < humanX)
{
machineX[m] += maybe;
}
if (arena[machineY[m]][machineX[m]] == EMPTY)
{
arena[machineY[m]][machineX[m]] = MACHINE;
}
else if (arena[machineY[m]][machineX[m]] == FENCE)
{
operative[m] = false;
destroyedMachines += 1;
if (destroyedMachines == MACHINES)
{
theEnd = End.VICTORY;
}
}
else if (arena[machineY[m]][machineX[m]] == HUMAN)
{
theEnd = End.KILLED;
}
}
void maybeMoveMachine(int m)
{
import std.random : uniform;
if (uniform(0, MACHINES_DRAG) == 0)
{
moveMachine(m);
}
}
void moveMachines()
{
foreach (int m; 0 .. MACHINES)
{
if (operative[m])
{
maybeMoveMachine(m);
}
}
}
// Read a user command; update `theEnd` accordingly and return the direction
// increments.
int[] getMove()
{
import std.stdio : write;
import std.uni : toLower;
int yInc = 0;
int xInc = 0;
write("\n");
eraseLineToEnd();
string command = toLower(getString("Command: "));
switch (command)
{
case "q":
theEnd = End.QUIT;
break;
case "sw":
yInc = 1;
xInc = -1;
break;
case "s":
yInc = 1;
break;
case "se":
yInc = 1;
xInc = 1;
break;
case "w":
xInc = -1;
break;
case "e":
xInc = 1;
break;
case "nw":
yInc = -1;
xInc = -1;
break;
case "n":
yInc = -1;
break;
case "ne":
yInc = -1;
xInc = 1;
break;
default:
break;
}
return [yInc, xInc];
}
void play()
{
import std.stdio : writeln;
int yInc = 0;
int xInc = 0;
while (true) { // game loop
clearScreen();
printTitle();
initGame();
actionLoop:
while (true) {
printArena();
int[] coords = getMove();
yInc = coords[0];
xInc = coords[1];
if (theEnd == End.NOT_YET)
{
if (yInc != 0 || xInc != 0)
{
arena[humanY][humanX] = EMPTY;
if (arena[humanY + yInc][humanX + xInc] == FENCE)
{
theEnd = End.ELECTRIFIED;
}
else if (arena[humanY + yInc][humanX + xInc] == MACHINE)
{
theEnd = End.KILLED;
}
else
{
arena[humanY][humanX] = EMPTY;
humanY = humanY + yInc;
humanX = humanX + xInc;
arena[humanY][humanX] = HUMAN;
printArena();
moveMachines();
}
}
}
final switch (theEnd)
{
case End.NOT_YET:
break;
case End.QUIT:
writeln("\nSorry to see you quit.");
break actionLoop;
case End.ELECTRIFIED:
writeln("\nZap! You touched the fence!");
break actionLoop;
case End.KILLED:
writeln("\nYou have been killed by a lucky machine.");
break actionLoop;
case End.VICTORY:
writeln("\nYou are lucky, you destroyed all machines.");
break actionLoop;
}
} // action loop;
if (!yes("\nDo you want to play again? "))
{
break;
}
} // game loop
writeln("\nHope you don't feel fenced in.");
writeln("Try again sometime.");
}
// Main {{{1
// =============================================================
void main()
{
setStyle(DEFAULT_INK);
clearScreen();
printCredits();
pressEnter("\nPress the Enter key to read the instructions. ");
clearScreen();
printInstructions();
pressEnter("\nPress the Enter key to start. ");
play();
}
In FreeBASIC
/'
Chase
Original version in BASIC:
Anonymous.
Published in 1977 in "The Best of Creative Computing", Volume 2.
https://www.atariarchives.org/bcc2/showpage.php?page=253
Version in Oberon-07:
Copyright (c) 2022, 2023, Marcos Cruz (programandala.net)
SPDX-License-Identifier: Fair
Version in Odin:
Copyright (c) 2023, 2025, Marcos Cruz (programandala.net)
SPDX-License-Identifier: Fair
This version in FreeBASIC:
Copyright (c) 2026, Marcos Cruz (programandala.net)
SPDX-License-Identifier: Fair
Written on 2026-09-03
.
Last modified 20260903T1700+0200.
'/
' Globals {{{1
' =============================================================
const bright => 8
' XXX TODO make an `enum`
const black => 0
const dark_grey => bright + black
const blue => 1
const bright_blue => bright + blue
const green => 2
const bright_green => bright + green
const cyan => 3
const bright_cyan => bright + cyan
const red => 4
const bright_red => bright + red
const pink => 5
const bright_pink => bright + pink
const yellow => 6
const bright_yellow => bright + yellow
const grey => 7
const white => bright + grey
const default_ink => white
const input_ink => bright_green
const instructions_ink => yellow
const title_ink => bright_red
const arena_width => 20
const arena_height => 10
const arena_last_x => arena_width - 1
const arena_last_y => arena_height - 1
const arena_row => 3
dim shared arena(arena_height, arena_width) as string
const empty => " "
const fence_symbol => "X"
const machine_symbol => "m"
const human_symbol => "@"
const fences => 15 ' inner obstacles, not the border
enum end_id explicit
not_yet
quit
electrified
killed
victory
end enum
dim shared the_end as end_id
const machines => 5
const machines_drag => 2 ' probability not moving: 0=0%, 1=50%, 2=66%, 3=75%, etc.
type machine_type
y as integer
x as integer
operative as boolean
end type
dim shared machine(machines) as machine_type
dim shared destroyed_machines as integer ' counter
dim shared human_x as integer
dim shared human_y as integer
' User input {{{1
' =============================================================
function get_string(prompt as string => "") as string
print prompt;
dim result as string
line input result
return result
end function
sub pause(prompt as string)
print prompt;
do
loop until inkey = chr(13)
end sub
function is_yes(s as string) as boolean
select case lcase(trim(s))
case "ok", "y", "yeah", "yes"
return true
case else
return false
end select
end function
function is_no(s as string) as boolean
select case lcase(trim(s))
case "n", "no", "nope"
return true
case else
return false
end select
end function
function yes(prompt as string) as boolean
do
var answer => get_string(prompt)
if is_yes(answer) then
return true
end if
if is_no(answer) then
return false
end if
loop
end function
' Title, credits and instructions {{{1
' =============================================================
const title => "Chase"
sub print_title()
color(title_ink)
print title
color(default_ink)
end sub
sub print_credits()
print_title()
print !"\nOriginal version in BASIC:"
print " Anonymous."
print !" Published in \"The Best of Creative Computing\" Volume 2 & 1977."
print " https://www.atariarchives.org/bcc2/showpage.php?page=253"
print "This version in FreeBASIC:"
print " Copyright (c) 2026, Marcos Cruz (programandala.net)"
print " SPDX-License-Identifier: Fair"
end sub
sub print_instructions()
print_title()
color(instructions_ink)
print !"\nYou (" & human_symbol & ") are in a high voltage maze with " & machines
print "security machines (" & machine_symbol & ") trying to kill you."
print "You must maneuver them into the maze (" & fence_symbol & !") to survive.\n"
print !"Good luck!\n"
print !"The movement commands are the following:\n"
print " ↖ ↑ ↗"
print " NW N NE"
print " ← W E →"
print " SW S SE"
print " ↙ ↓ ↘"
print !"\nPlus 'Q' to end the game."
color(default_ink)
end sub
' Arena {{{1
' =============================================================
sub print_arena()
locate(arena_row, 1)
for y as integer => 0 to arena_last_y
for x as integer => 0 to arena_last_x
print arena(y, x);
next
print
next
end sub
function is_border(y as integer, x as integer) as boolean
return (y = 0) orelse (x = 0) orelse (y = arena_last_y) orelse (x = arena_last_x)
end function
function random_integer_in_inclusive_range(min as integer, max as integer) as integer
return int(rnd * (max - min) + min)
end function
sub place overload (s as string, byref y as integer, byref x as integer)
do
y => random_integer_in_inclusive_range(1, arena_last_y - 1)
x => random_integer_in_inclusive_range(1, arena_last_x - 1)
if arena(y, x) = empty then
exit do
end if
loop
arena(y, x) => s
end sub
sub place overload (s as string)
dim dummy_y as integer
dim dummy_x as integer
place(s, dummy_y, dummy_x)
end sub
sub inhabit_arena()
for m as integer => 0 to machines - 1
place(machine_symbol, machine(m).y, machine(m).x)
machine(m).operative => true
next
for i as integer => 1 to fences
place(fence_symbol)
next
place(human_symbol, human_y, human_x)
end sub
sub clean_arena()
for y as integer => 0 to arena_last_y
for x as integer => 0 to arena_last_x
arena(y, x) => iif(is_border(y, x), fence_symbol, empty)
next
next
end sub
' Game {{{1
' =============================================================
sub init_game()
clean_arena()
inhabit_arena()
destroyed_machines => 0
the_end => end_id.not_yet
end sub
sub move_machine(m as integer)
dim maybe as integer
arena(machine(m).y, machine(m).x) => empty
maybe => random_integer_in_inclusive_range(0, 2)
if machine(m).y > human_y then
machine(m).y -= maybe
elseif machine(m).y < human_y then
machine(m).y += maybe
end if
maybe => random_integer_in_inclusive_range(0, 2)
if machine(m).x > human_x then
machine(m).x -= maybe
elseif machine(m).x < human_x then
machine(m).x += maybe
end if
if arena(machine(m).y, machine(m).x) = empty then
arena(machine(m).y, machine(m).x) => machine_symbol
elseif arena(machine(m).y, machine(m).x) = fence_symbol then
machine(m).operative => false
destroyed_machines += 1
if destroyed_machines = machines then
the_end => end_id.victory
end if
elseif arena(machine(m).y, machine(m).x) = human_symbol then
the_end => end_id.killed
end if
end sub
sub maybe_move_machine(m as integer)
if random_integer_in_inclusive_range(0, machines_drag) = 0 then
move_machine(m)
end if
end sub
sub move_machines()
for m as integer => 0 to machines - 1
if machine(m).operative then
maybe_move_machine(m)
end if
next
end sub
function screen_width() as integer
return loword(width)
end function
const first_column as integer => 1
sub erase_current_line_right()
print space(screen_width() - pos + first_column);
locate(csrlin, first_column)
end sub
sub set_move(byref y_inc as integer, byref x_inc as integer)
print
erase_current_line_right()
dim command_ as string => lcase(get_string("Command: "))
select case command_
case "q"
the_end => end_id.quit
case "sw"
y_inc => +1
x_inc => -1
case "s"
y_inc => +1
x_inc => 0
case "se"
y_inc => +1
x_inc => +1
case "w"
y_inc => 0
x_inc => -1
case "e"
y_inc => 0
x_inc => +1
case "nw"
y_inc => -1
x_inc => -1
case "n"
y_inc => -1
x_inc => 0
case "ne"
y_inc => -1
x_inc => +1
end select
end sub
sub play()
dim y_inc as integer
dim x_inc as integer
do ' game loop
cls
print_title()
init_game()
do ' action loop
print_arena()
set_move(y_inc, x_inc)
if the_end = end_id.not_yet then
if y_inc <> 0 or x_inc <> 0 then
arena(human_y, human_x) => empty
if arena(human_y + y_inc, human_x + x_inc) = fence_symbol then
the_end => end_id.electrified
elseif arena(human_y + y_inc, human_x + x_inc) = machine_symbol then
the_end => end_id.killed
else
arena(human_y, human_x) => empty
human_y => human_y + y_inc
human_x => human_x + x_inc
arena(human_y, human_x) => human_symbol
print_arena()
move_machines()
end if
end if
end if
if the_end <> end_id.not_yet then
exit do
end if
loop ' action loop
select case the_end
case end_id.quit
print !"\nSorry to see you quit."
case end_id.electrified
print !"\nZap! You touched the fence!"
case end_id.killed
print !"\nYou have been killed by a lucky machine."
case end_id.victory
print !"\nYou are lucky, you destroyed all machines."
end select
loop until not yes(!"\nDo you want to play again? ")
print !"\nHope you don't feel fenced in."
print "Try again sometime."
end sub
' Main {{{1
' =============================================================
randomize
color(default_ink)
cls
print_credits()
pause(!"\nPress the Enter key to read the instructions. ")
cls
print_instructions()
pause(!"\nPress the Enter key to start. ")
play()
' vim: filetype=freebasic
In Hare
// Chase
//
// Original version in BASIC:
// Anonymous.
// Published in 1977 in "The Best of Creative Computing", Volume 2.
// https://www.atariarchives.org/bcc2/showpage.php?page=253
//
// This version in Hare:
// Copyright (c) 2025, Marcos Cruz (programandala.net)
// SPDX-License-Identifier: Fair
//
// Written on 2025-02-17.
// Last modified: 20260213T1645+0100.
// Modules {{{1
// =============================================================================
use ascii;
use bufio;
use fmt;
use math::random;
use os;
use strings;
use time;
// Terminal {{{1
// =============================================================================
def BLACK = 0;
def RED = 1;
def GREEN = 2;
def YELLOW = 3;
def BLUE = 4;
def MAGENTA = 5;
def CYAN = 6;
def WHITE = 7;
def DEFAULT = 9;
def STYLE_OFF = 20;
def FOREGROUND = 30;
def BACKGROUND = 40;
def BRIGHT = 60;
def NORMAL_STYLE = 0;
fn move_cursor_home() void = {
fmt::print("\x1B[H")!;
};
fn set_cursor_position(line: int, column: int) void = {
fmt::printf("\x1B[{};{}H", line, column)!;
};
fn hide_cursor() void = {
fmt::print("\x1B[?25l")!;
};
fn show_cursor() void = {
fmt::print("\x1B[?25h")!;
};
fn set_style(style: int) void = {
fmt::printf("\x1B[{}m", style)!;
};
fn reset_attributes() void = {
set_style(NORMAL_STYLE);
};
fn erase_line_to_end() void = {
fmt::print("\x1B[K")!;
};
fn erase_screen() void = {
fmt::print("\x1B[2J")!;
};
fn clear_screen() void = {
erase_screen();
reset_attributes();
move_cursor_home();
};
// Config {{{1
// =============================================================
def DEFAULT_INK = FOREGROUND + WHITE;
def INPUT_INK = FOREGROUND + BRIGHT + GREEN;
def INSTRUCTIONS_INK = FOREGROUND + YELLOW;
def TITLE_INK = FOREGROUND + BRIGHT + RED;
// Data {{{1
// =============================================================
// Arena
def ARENA_WIDTH = 20;
def ARENA_HEIGHT = 10;
def ARENA_LAST_X = ARENA_WIDTH - 1;
def ARENA_LAST_Y = ARENA_HEIGHT - 1;
def ARENA_ROW = 3;
let arena: [ARENA_HEIGHT][ARENA_WIDTH]str = [[""...]...];
def EMPTY = " ";
def FENCE = "X";
def MACHINE = "m";
def HUMAN = "@";
def FENCES = 15; // inner obstacles, not the border
// The end
type end = enum {
NOT_YET,
QUIT,
ELECTRIFIED,
KILLED,
VICTORY,
};
let the_end: end = end::NOT_YET;
// Machines
def MACHINES = 5;
def MACHINES_DRAG = 2; // probability not moving: 0=0%, 1=50%, 2=66%, 3=75%, etc.
let machine_x: [MACHINES]int = [0...];
let machine_y: [MACHINES]int = [0...];
let operative: [MACHINES]bool = [false...];
let destroyed_machines: int = 0; // counter
// Human
let human_x: int = 0;
let human_y: int = 0;
// User input {{{1
// =============================================================
fn print_prompt(prompt: str = "") void = {
fmt::print(prompt)!;
bufio::flush(os::stdout)!;
};
fn accept_string(prompt: str = "") str = {
print_prompt(prompt);
const buffer = match (bufio::read_line(os::stdin)!) {
case let buffer: []u8 =>
yield buffer;
case =>
return "";
};
defer free(buffer);
return strings::dup(strings::fromutf8(buffer)!)!;
};
fn get_string(prompt: str = "") str = {
set_style(INPUT_INK);
defer set_style(DEFAULT_INK);
return accept_string(prompt);
};
fn press_enter(prompt: str) void = {
free(accept_string(prompt));
};
// Return `true` if the given string is "yes" or a synonym.
//
fn is_yes(s: str) bool = {
const lowercase_s = ascii::strlower(s)!;
defer free(lowercase_s);
return switch(lowercase_s) {
case "ok", "y", "yeah", "yes" =>
yield true;
case =>
yield false;
};
};
// Return `true` if the given string is "no" or a synonym.
//
fn is_no(s: str) bool = {
const lowercase_s = ascii::strlower(s)!;
defer free(lowercase_s);
return switch(lowercase_s) {
case "n", "no", "nope" =>
yield true;
case =>
yield false;
};
};
// Print the given prompt, wait until the user enters a valid yes/no string,
// and return `true` for "yes" or `false` for "no".
//
fn yes(prompt: str) bool = {
for (true) {
let answer = get_string(prompt);
defer free(answer);
if (is_yes(answer)) {
return true;
};
if (is_no(answer)) {
return false;
};
};
};
// Title, credits and instructions {{{1
// =============================================================
def TITLE = "Chase";
// Print the title at the current cursor position.
//
fn print_title() void = {
set_style(TITLE_INK);
fmt::println(TITLE)!;
set_style(DEFAULT_INK);
};
fn print_credits() void = {
print_title();
fmt::println("\nOriginal version in BASIC:")!;
fmt::println(" Anonymous.")!;
fmt::println(" Published in \"The Best of Creative Computing\", Volume 2, 1977.")!;
fmt::println(" https://www.atariarchives.org/bcc2/showpage.php?page=253")!;
fmt::println("This version in Hare:")!;
fmt::println(" Copyright (c) 2025, Marcos Cruz (programandala.net)")!;
fmt::println(" SPDX-License-Identifier: Fair")!;
};
// Print the game instructions and wait for a key.
//
fn print_instructions() void = {
print_title();
set_style(INSTRUCTIONS_INK);
defer set_style(DEFAULT_INK);
fmt::printfln("\nYou ({}) are in a high voltage maze with {}", HUMAN, MACHINES)!;
fmt::printfln("security machines ({}) trying to kill you.", MACHINE)!;
fmt::printfln("You must maneuver them into the maze ({}) to survive.\n", FENCE)!;
fmt::println("Good luck!\n")!;
fmt::println("The movement commands are the following:\n")!;
fmt::println(" ↖ ↑ ↗")!;
fmt::println(" NW N NE")!;
fmt::println(" ← W E →")!;
fmt::println(" SW S SE")!;
fmt::println(" ↙ ↓ ↘")!;
fmt::println("\nPlus 'Q' to end the game.")!;
};
// Random numbers {{{1
// =============================================================================
let rand: random::random = 0;
fn randomize() void = {
rand = random::init(time::now(time::clock::MONOTONIC).sec: u64);
};
fn random_int_in_inclusive_range(min: int, max: int) int = {
return random::u32n(&rand, (max - min):u32): int + min;
};
// Arena {{{1
// =============================================================
// Display the arena at the top left corner of the screen.
//
fn print_arena() void = {
set_cursor_position(ARENA_ROW, 1);
for (let y = 0; y <= ARENA_LAST_Y; y += 1) {
for (let x = 0; x <= ARENA_LAST_X; x += 1) {
fmt::print(arena[y][x])!;
};
fmt::println()!;
};
};
// If the given arena coordinates `y` and `x` are part of the border arena
// (i.e. its surrounding fence), return `true`, otherwise return `false`.
//
fn is_border(y: int, x: int) bool = {
return (y == 0) || (x == 0) || (y == ARENA_LAST_Y) || (x == ARENA_LAST_X);
};
// Place the given string at a random empty position of the arena and return
// the coordinates.
//
fn place(s: str) (int, int) = {
let y: int = 0;
let x: int = 0;
for (true) {
y = random_int_in_inclusive_range(1, ARENA_LAST_Y - 1);
x = random_int_in_inclusive_range(1, ARENA_LAST_X - 1);
if (arena[y][x] == EMPTY) {
break;
};
};
arena[y][x] = s;
return (y, x);
};
// Inhabit the arena with the machines, the inner fences and the human.
//
fn inhabit_arena() void = {
for (let m = 0; m < MACHINES; m += 1) {
let coords = place(MACHINE);
machine_y[m] = coords.0;
machine_x[m] = coords.1;
operative[m] = true;
};
for (let i = 0; i < FENCES; i += 1) {
place(FENCE);
};
let coords = place(HUMAN);
human_y = coords.0;
human_x = coords.1;
};
// Clean the arena, i.e. empty it and add a surrounding fence.
//
fn clean_arena() void = {
for (let y = 0; y <= ARENA_LAST_Y; y += 1) {
for (let x = 0; x <= ARENA_LAST_X; x += 1) {
arena[y][x] = if (is_border(y, x)) FENCE else EMPTY;
};
};
};
// Game {{{1
// =============================================================
// Init the game.
//
fn init_game() void = {
clean_arena();
inhabit_arena();
destroyed_machines = 0;
the_end = end::NOT_YET;
};
// Move the given machine.
//
fn move_machine(m: int) void = {
let maybe: int = 0;
arena[machine_y[m]][machine_x[m]] = EMPTY;
maybe = random::u32n(&rand, 2): int;
if (machine_y[m] > human_y) {
machine_y[m] -= maybe;
} else if (machine_y[m] < human_y) {
machine_y[m] += maybe;
};
maybe = random::u32n(&rand, 2): int;
if (machine_x[m] > human_x) {
machine_x[m] -= maybe;
} else if (machine_x[m] < human_x) {
machine_x[m] += maybe;
};
if (arena[machine_y[m]][machine_x[m]] == EMPTY) {
arena[machine_y[m]][machine_x[m]] = MACHINE;
} else if (arena[machine_y[m]][machine_x[m]] == FENCE) {
operative[m] = false;
destroyed_machines += 1;
if (destroyed_machines == MACHINES) {
the_end = end::VICTORY;
};
} else if (arena[machine_y[m]][machine_x[m]] == HUMAN) {
the_end = end::KILLED;
};
};
// Maybe move the given operative machine.
//
fn maybe_move_machine(m: int) void = {
if (random::u32n(&rand, MACHINES_DRAG) == 0) {
move_machine(m);
};
};
// Move the operative machines.
//
fn move_machines() void = {
for (let m = 0; m < MACHINES; m += 1) {
if (operative[m]) {
maybe_move_machine(m);
};
};
};
// Read a user command; update `the_end` accordingly and return the direction
// increments.
//
fn get_move() (int, int) = {
let y_inc: int = 0;
let x_inc: int = 0;
fmt::println()!;
erase_line_to_end();
let command = ascii::strlower(get_string("Command: "))!;
defer free(command);
switch (command) {
case "q" =>
the_end = end::QUIT;
case "sw" =>
y_inc = 1;
x_inc = -1;
case "s" =>
y_inc = 1;
case "se" =>
y_inc = 1;
x_inc = 1;
case "w" =>
x_inc = -1;
case "e" =>
x_inc = 1;
case "nw" =>
y_inc = -1;
x_inc = -1;
case "n" =>
y_inc = -1;
case "ne" =>
y_inc = -1;
x_inc = 1;
case =>
void;
};
return (y_inc, x_inc);
};
fn play() void = {
let y_inc: int = 0;
let x_inc: int = 0;
for (true) { // game loop
clear_screen();
print_title();
init_game();
for (true) { // action loop
print_arena();
let coords = get_move();
y_inc = coords.0;
x_inc = coords.1;
if (the_end == end::NOT_YET) {
if (y_inc != 0 || x_inc != 0) {
arena[human_y][human_x] = EMPTY;
if (arena[human_y + y_inc][human_x + x_inc] == FENCE) {
the_end = end::ELECTRIFIED;
} else if (arena[human_y + y_inc][human_x + x_inc] == MACHINE) {
the_end = end::KILLED;
} else {
arena[human_y][human_x] = EMPTY;
human_y = human_y + y_inc;
human_x = human_x + x_inc;
arena[human_y][human_x] = HUMAN;
print_arena();
move_machines();
};
};
};
if (the_end != end::NOT_YET) {
break;
};
}; // action loop;
switch (the_end) {
case end::QUIT =>
fmt::println("\nSorry to see you quit.")!;
case end::ELECTRIFIED =>
fmt::println("\nZap! You touched the fence!")!;
case end::KILLED =>
fmt::println("\nYou have been killed by a lucky machine.")!;
case end::VICTORY =>
fmt::println("\nYou are lucky, you destroyed all machines.")!;
case =>
void;
};
if (!yes("\nDo you want to play again? ")) {
break;
};
}; // game loop
fmt::println("\nHope you don't feel fenced in.")!;
fmt::println("Try again sometime.")!;
};
// Main {{{1
// =============================================================
export fn main() void = {
randomize();
set_style(DEFAULT_INK);
clear_screen();
print_credits();
press_enter("\nPress the Enter key to read the instructions. ");
clear_screen();
print_instructions();
press_enter("\nPress the Enter key to start. ");
play();
};
In Kotlin
/*
Chase
Original version in BASIC:
Anonymous.
Published in 1977 in "The Best of Creative Computing", Volume 2.
https://www.atariarchives.org/bcc2/showpage.php?page=253
This version in Kotlin:
Copyright (c) 2025, Marcos Cruz (programandala.net)
SPDX-License-Identifier: Fair
Written on 2025-05-18.
Last modified 20250519T0024+0200.
*/
import kotlin.random.Random
// Terminal {{{1
// =============================================================
const val BLACK = 0
const val RED = 1
const val GREEN = 2
const val YELLOW = 3
const val BLUE = 4
const val MAGENTA = 5
const val CYAN = 6
const val WHITE = 7
const val DEFAULT = 9
const val FOREGROUND = +30
const val BACKGROUND = +40
const val BRIGHT = +60
// Move the cursor to the top left position of the terminal.
fun moveCursorHome() {
print("\u001B[H")
}
// Clear the terminal and move the cursor to the top left position.
fun clearScreen() {
print("\u001B[2J")
moveCursorHome()
}
// Set the cursor position to the given coordinates (the top left position is 1, 1)
fun setCursorPosition(line: Int, column: Int) {
print("\u001B[${line};${column}H")
}
// Set the color.
fun setColor(color: Int) {
print("\u001B[${color}m")
}
// Erase from the current cursor position to the end of the current line.
fun eraseLineRight() {
print("\u001B[K")
}
// Globals {{{1
// =============================================================
// Colors
const val DEFAULT_INK = WHITE + FOREGROUND
const val INPUT_INK = BRIGHT + GREEN + FOREGROUND
const val INSTRUCTIONS_INK = YELLOW + FOREGROUND
const val TITLE_INK = BRIGHT + RED + FOREGROUND
// Arena
const val ARENA_WIDTH = 20
const val ARENA_HEIGHT = 10
const val ARENA_LAST_X = ARENA_WIDTH - 1
const val ARENA_LAST_Y = ARENA_HEIGHT - 1
const val ARENA_ROW = 3
var arena = Array(ARENA_HEIGHT) { Array(ARENA_WIDTH) { "" } }
const val EMPTY = " "
const val FENCE = "X"
const val MACHINE = "m"
const val HUMAN = "@"
const val FENCES = 15 // inner obstacles, not the border
// The end
enum class End {
Not_Yet,
Quit,
Electrified,
Killed,
Victory,
}
var theEnd: End = End.Not_Yet
// Machines
const val MACHINES = 5
const val MACHINES_DRAG = 2 // probability not moving: 0=0%, 1=50%, 2=66%, 3=75%, etc.
class Machine (
var x: Int = 0,
var y: Int = 0,
var operative: Boolean = true,
)
var machine: Array<Machine> = Array(MACHINES) { Machine() }
var destroyedMachines: Int = 0 // counter
// Human
var humanX: Int = 0
var humanY: Int = 0
// User input {{{1
// =============================================================
// Print the given prompt and wait until the user enters a string.
//
fun inputString(prompt: String = ""): String {
setColor(INPUT_INK)
print(prompt)
val result = readln()
setColor(DEFAULT_INK)
return result
}
// Print the given prompt and wait until the user presses Enter.
//
fun pressEnter(prompt: String) {
inputString(prompt)
}
// Return `true` if the given string is "yes" or a synonym.
//
fun isYes(s: String): Boolean {
return s.lowercase() in arrayOf("ok", "y", "yeah", "yes")
}
// Return `true` if the given string is "no" or a synonym.
//
fun isNo(s: String): Boolean {
return s.lowercase() in arrayOf("n", "no", "nope")
}
// Print the given prompt, wait until the user enters a valid yes/no string,
// and return `true` for "yes" or `false` for "no".
//
fun yes(prompt: String): Boolean {
while (true) {
val answer = inputString(prompt)
if (isYes(answer)) return true
if (isNo(answer)) return false
}
}
// Title, credits and instructions {{{1
// =============================================================
const val TITLE = "Chase"
// Print the title at the current cursor position.
//
fun printTitle() {
setColor(TITLE_INK)
println(TITLE)
setColor(DEFAULT_INK)
}
fun printCredits() {
printTitle()
println("\nOriginal version in BASIC:")
println(" Anonymous.")
println(" Published in \"The Best of Creative Computing\", Volume 2, 1977.")
println(" https://www.atariarchives.org/bcc2/showpage.php?page=253")
println("This version in Kotlin:")
println(" Copyright (c) 2025, Marcos Cruz (programandala.net)")
println(" SPDX-License-Identifier: Fair")
}
// Print the game instructions and wait for a key.
//
fun printInstructions() {
printTitle()
setColor(INSTRUCTIONS_INK)
println("\nYou ($HUMAN) are in a high voltage maze with $MACHINES")
println("security machines ($MACHINE) trying to kill you.")
println("You must maneuver them into the maze ($FENCE) to survive.\n")
println("Good luck!\n")
println("The movement commands are the following:\n")
println(" ↖ ↑ ↗")
println(" NW N NE")
println(" ← W E →")
println(" SW S SE")
println(" ↙ ↓ ↘")
println("\nPlus 'Q' to end the game.")
setColor(DEFAULT_INK)
}
// Arena {{{1
// =============================================================
// Display the arena at the top left corner of the screen.
//
fun printArena() {
setCursorPosition(ARENA_ROW, 1)
for (y in 0 .. ARENA_LAST_Y) {
for (x in 0 .. ARENA_LAST_X) {
print(arena[y][x])
}
println()
}
}
// If the given arena coordinates `y` and `x` are part of the border arena
// (i.e. its surrounding fence), return `true`, otherwise return `false`.
//
fun isBorder(y: Int, x: Int): Boolean {
return (y == 0) || (x == 0) || (y == ARENA_LAST_Y) || (x == ARENA_LAST_X)
}
// Place the given string at a random empty position of the arena and return
// the coordinates.
//
fun place(s: String): Pair<Int, Int> {
var y: Int = 0
var x: Int = 0
while (true) {
y = Random.nextInt(1, ARENA_LAST_Y)
x = Random.nextInt(1, ARENA_LAST_X)
if (arena[y][x] == EMPTY) break
}
arena[y][x] = s
return Pair(y, x)
}
// Inhabit the arena with the machines, the inner fences and the human.
//
fun inhabitArena() {
for (m in 0 .. MACHINES - 1) {
var (y, x) = place(MACHINE)
machine[m].y = y
machine[m].x = x
machine[m].operative = true
}
for (i in 0 .. FENCES - 1) {
place(FENCE)
}
var (y, x) = place(HUMAN)
humanY = y
humanX = x
}
// Clean the arena, i.e. empty it and add a surrounding fence.
//
fun cleanArena() {
for (y in 0 .. ARENA_LAST_Y) {
for (x in 0 .. ARENA_LAST_X) {
arena[y][x] = if (isBorder(y, x)) FENCE else EMPTY
}
}
}
// Game {{{1
// =============================================================
// Init the game.
//
fun initGame() {
cleanArena()
inhabitArena()
destroyedMachines = 0
theEnd = End.Not_Yet
}
// Move the given machine.
//
fun moveMachine(m: Int) {
var maybe: Int = 0
arena[machine[m].y][machine[m].x] = EMPTY
maybe = Random.nextInt(2) // 0 .. 1
if (machine[m].y > humanY) {
machine[m].y -= maybe
} else if (machine[m].y < humanY) {
machine[m].y += maybe
}
maybe = Random.nextInt(2) // 0 .. 1
if (machine[m].x > humanX) {
machine[m].x -= maybe
} else if (machine[m].x < humanX) {
machine[m].x += maybe
}
if (arena[machine[m].y][machine[m].x] == EMPTY) {
arena[machine[m].y][machine[m].x] = MACHINE
} else if (arena[machine[m].y][machine[m].x] == FENCE) {
machine[m].operative = false
destroyedMachines += 1
if (destroyedMachines == MACHINES) {
theEnd = End.Victory
}
} else if (arena[machine[m].y][machine[m].x] == HUMAN) {
theEnd = End.Killed
}
}
// Maybe move the given operative machine.
//
fun maybeMoveMachine(m: Int) {
if (Random.nextInt(0, MACHINES_DRAG) == 0) moveMachine(m)
}
// Move the operative machines.
//
fun moveMachines() {
for (m in 0 .. MACHINES - 1) {
if (machine[m].operative) {
maybeMoveMachine(m)
}
}
}
// Read a user command; update `theEnd` accordingly and return the direction
// increments.
//
fun getMove(): Pair<Int, Int> {
var yInc: Int = 0
var xInc: Int = 0
println()
eraseLineRight()
when (inputString("Command: ").lowercase()) {
"q" ->
theEnd = End.Quit
"sw" -> {
yInc = +1
xInc = -1
}
"s" ->
yInc = +1
"se" -> {
yInc = +1
xInc = +1
}
"w" ->
xInc = -1
"e" ->
xInc = +1
"nw" -> {
yInc = -1
xInc = -1
}
"n" ->
yInc = -1
"ne" -> {
yInc = -1
xInc = +1
}
}
return Pair(yInc, xInc)
}
fun play() {
while (true) { // game loop
clearScreen()
printTitle()
initGame()
while (true) { // action loop
printArena()
var (yInc, xInc) = getMove()
if (theEnd == End.Not_Yet) {
if (yInc != 0 || xInc != 0) {
arena[humanY][humanX] = EMPTY
if (arena[humanY + yInc][humanX + xInc] == FENCE) {
theEnd = End.Electrified
} else if (arena[humanY + yInc][humanX + xInc] == MACHINE) {
theEnd = End.Killed
} else {
arena[humanY][humanX] = EMPTY
humanY = humanY + yInc
humanX = humanX + xInc
arena[humanY][humanX] = HUMAN
printArena()
moveMachines()
}
}
}
if (theEnd != End.Not_Yet) break
}// action loop
when (theEnd) {
End.Quit ->
println("\nSorry to see you quit.")
End.Electrified ->
println("\nZap! You touched the fence!")
End.Killed ->
println("\nYou have been killed by a lucky machine.")
End.Victory ->
println("\nYou are lucky, you destroyed all machines.")
else ->
print("")
}
if (! yes("\nDo you want to play again? ") ) break
} // game loop
println("\nHope you don't feel fenced in.")
println("Try again sometime.")
}
// Main {{{1
// =============================================================
fun main() {
setColor(DEFAULT_INK)
clearScreen()
printCredits()
pressEnter("\nPress the Enter key to read the instructions. ")
clearScreen()
printInstructions()
pressEnter("\nPress the Enter key to start. ")
play()
}
In Nim
#[
Chase
Original version in BASIC:
Anonymous.
Published in 1977 in "The Best of Creative Computing", Volume 2.
https://www.atariarchives.org/bcc2/showpage.php?page=253
Version in Oberon-07:
Copyright (c) 2022, 2023, Marcos Cruz (programandala.net)
SPDX-License-Identifier: Fair
Version in Odin:
Copyright (c) 2023, Marcos Cruz (programandala.net)
SPDX-License-Identifier: Fair
This version in Nim:
Copyright (c) 2025, Marcos Cruz (programandala.net)
SPDX-License-Identifier: Fair
Written on 2025-01-22.
Last modified: 20250405T0304+0200.
]#
import std/strformat
import std/random
import std/strutils
import std/terminal
# Terminal {{{1
# =============================================================
proc cursorHome() =
setCursorPos(0, 0)
proc clearScreen() =
eraseScreen()
cursorHome()
proc eraseLineRight() =
write(stdout, "\e[K")
proc eraseDown() =
# Erases the screen from the current line down to the bottom of the screen.
#
# This is defined in std/terminal but deactivated, because it does not have
# equivalent in Windows.
write(stdout, "\e[J")
proc setCursorPosYX(y: int, x: int) =
setCursorPos(x, y)
const black = 0
const red = 1
const green = 2
const yellow = 3
const blue = 4
const magenta = 5
const cyan = 6
const white = 7
const default = 9
const foreground = +30 # color offset
const background = +40 # color offset
const bright = +60 # foreground and background offset
const normal = 0
const bold = 1 # bold text (or bright on terminals not supporting)
const dim = 2 # dim text
const italic = 3 # italic (or reverse on terminals not supporting)
const underlined = 4
const blinking = 5 # blinking/bold text
const rapidBlinking = 6 # rapid blinking/bold text (not widely supported)
const reversed = 7
const hidden = 8
const crossedout = 9 # strikethrough
const styleOff = +20 # style offset to turn it off
const notBold = 21 # or double underlined on some terminals
const notDim = 22
const notItalic = 23
const notUnderlined = 24
const notBlinking = 25
const notReversed = 27
const notHidden = 28
const notCrossedout = 29
proc ansiStyleCode(fg: int = default, bg: int = default, style: int = normal): string =
result = &"\e[0;{style};{fg + foreground};{bg + background}m"
proc setStyle(style: string) =
write(stdout, style)
proc setStyles(fg: int = default, bg: int = default, style: int = normal) =
setStyle(ansiStyleCode(fg, bg, style))
# Globals {{{1
# =============================================================
# Colors
const defaultInk = ansiStyleCode(white)
const inputInk = ansiStyleCode(bright + green)
const instructionsInk = ansiStyleCode(bright + yellow)
const titleInk = ansiStyleCode(bright + red)
# Arena
const arenaWidth = 20
const arenaHeight = 10
const arenaLastX = arenaWidth - 1
const arenaLastY = arenaHeight - 1
const arenaRow = 3
type Arena = array[arenaHeight, array[arenaWidth, string]]
var arena: Arena
const empty = " "
const fence = "X"
const machine = "m"
const human = "@"
const fences = 15 # inner obstacles, not the border
# The end
type End = enum
notYet,
surrender,
electrified,
killed,
victory,
var theEnd: End
# Machines
const machines = 5
const machinesDrag = 2 # probability not moving: 0=0%, 1=50%, 2=66%, 3=75%, etc.
var machineX: array[machines, int]
var machineY: array[machines, int]
var operative: array[machines, bool]
var destroyedMachines: int # counter
# Human
var humanX: int
var humanY: int
# User input {{{1
# =============================================================
# Print the given prompt and wait until the user enters a string.
#
proc inputString(prompt: string = ""): string =
setStyle(inputInk)
defer: setStyle(defaultInk)
write(stdout, prompt)
result = readLine(stdin) # XXX TODO orElse ""
# Print the given prompt and wait until the user presses Enter.
#
proc pressEnter(prompt: string) =
discard inputString(prompt)
# Return `true` if the given string is "yes" or a synonym.
#
proc isYes(s: string): bool =
return toLower(s) in ["ok", "y", "yeah", "yes"]
# Return `true` if the given string is "no" or a synonym.
#
proc isNo(s: string): bool =
return toLower(s) in ["n", "no", "nope"]
# Print the given prompt, wait until the user enters a valid yes/no string,
# and return `true` for "yes" or `false` for "no".
#
proc yes(prompt: string): bool =
while true:
var answer = inputString(prompt)
if isYes(answer):
return true
if isNo(answer):
return false
# Title, credits and instructions {{{1
# =============================================================
const title = "Chase"
# Print the title at the current cursor position.
#
proc printTitle() =
setStyle(titleInk)
writeLine(stdout, title)
setStyle(defaultInk)
proc printCredits() =
printTitle()
writeLine(stdout, "\nOriginal version in BASIC:")
writeLine(stdout, " Anonymous.")
writeLine(stdout, " Published in \"The Best of Creative Computing\", Volume 2, 1977.")
writeLine(stdout, " https://www.atariarchives.org/bcc2/showpage.php?page=253")
writeLine(stdout, "\nThis version in Nim:")
writeLine(stdout, " Copyright (c) 2025, Marcos Cruz (programandala.net)")
writeLine(stdout, " SPDX-License-Identifier: Fair")
# Print the game instructions and wait for a key.
#
proc printInstructions() =
printTitle()
setStyle(instructionsInk)
defer: setStyle(defaultInk)
writeLine(stdout, "\nYou (", human, ") are in a high voltage maze with ", machines, "")
writeLine(stdout, "security machines (", machine, ") trying to kill you.")
writeLine(stdout, "You must maneuver them into the maze (", fence, ") to survive.\n")
writeLine(stdout, "Good luck!\n")
writeLine(stdout, "The movement commands are the following:\n")
writeLine(stdout, " ↖ ↑ ↗")
writeLine(stdout, " NW N NE")
writeLine(stdout, " ← W E →")
writeLine(stdout, " SW S SE")
writeLine(stdout, " ↙ ↓ ↘")
writeLine(stdout, "\nPlus 'Q' to end the game.")
# Arena {{{1
# =============================================================
# Display the arena at the top left corner of the screen.
#
proc printArena() =
setCursorPosYX(arenaRow, 0)
for y in 0 .. arenaLastY:
for x in 0 .. arenaLastX:
write(stdout, arena[y][x])
writeLine(stdout, "")
# If the given arena coordinates `y` and `x` are part of the border arena
# (i.e. its surrounding fence), return `true`, otherwise return `false`.
#
proc isBorder(y, x: int): bool =
result = (y == 0) or (x == 0) or (y == arenaLastY) or (x == arenaLastX)
# Place the given string at a random empty position of the arena and return
# the coordinates.
#
proc place(s: string): (int, int) =
var y: int
var x: int
while true:
y = rand(1 .. arenaLastY - 1)
x = rand(1 .. arenaLastX - 1)
if arena[y][x] == empty:
break
arena[y][x] = s
result = (y, x)
# Inhabit the arena with the machines, the inner fences and the human.
#
proc inhabitArena() =
for m in 0 ..< machines:
(machineY[m], machineX[m]) = place(machine)
operative[m] = true
for _ in 0 ..< fences:
discard place(fence)
(humanY, humanX) = place(human)
# Clean the arena, i.e. empty it and add a surrounding fence.
#
proc cleanArena() =
for y in 0 .. arenaLastY:
for x in 0 .. arenaLastX:
arena[y][x] = if isBorder(y, x): fence else: empty
# Game {{{1
# =============================================================
# Init the game.
#
proc initGame() =
cleanArena()
inhabitArena()
destroyedMachines = 0
theEnd = notYet
# Move the given machine.
#
proc moveMachine(m: int) =
var maybe: int
arena[machineY[m]][machineX[m]] = empty
maybe = rand(0 .. 1)
if machineY[m] > humanY:
machineY[m] -= maybe
elif machineY[m] < humanY:
machineY[m] += maybe
maybe = rand(0 .. 1)
if machineX[m] > humanX:
machineX[m] -= maybe
elif machineX[m] < humanX:
machineX[m] += maybe
if arena[machineY[m]][machineX[m]] == empty:
arena[machineY[m]][machineX[m]] = machine
elif arena[machineY[m]][machineX[m]] == fence:
operative[m] = false
destroyedMachines += 1
if destroyedMachines == machines:
theEnd = victory
elif arena[machineY[m]][machineX[m]] == human:
theEnd = killed
# Maybe move the given operative machine.
#
proc maybeMoveMachine(m: int) =
if rand(machinesDrag) == 0:
moveMachine(m)
# Move the operative machines.
#
proc moveMachines() =
for m in 0 ..< machines:
if operative[m]:
maybeMoveMachine(m)
# Read a user command; update `theEnd` accordingly and return the direction
# increments.
#
proc getMove(): (int, int) =
var yInc = 0
var xInc = 0
writeLine(stdout, "")
eraseLineRight()
var command = toLower(inputString("Command: "))
case command:
of "q":
theEnd = surrender
of "sw":
yInc = +1
xInc = -1
of "s":
yInc = +1
of "se":
yInc = +1
xInc = +1
of "w":
xInc = -1
of "e":
xInc = +1
of "nw":
yInc = -1
xInc = -1
of "n":
yInc = -1
of "ne":
yInc = -1
xInc = +1
else:
discard
result = (yInc, xInc)
proc play() =
var yInc: int
var xInc: int
while true: # game loop
clearScreen()
printTitle()
initGame()
while true: # action loop
printArena()
(yInc, xInc) = getMove()
if theEnd == notYet:
if yInc != 0 or xInc != 0:
arena[humanY][humanX] = empty
if arena[humanY + yInc][humanX + xInc] == fence:
theEnd = electrified
elif arena[humanY + yInc][humanX + xInc] == machine:
theEnd = killed
else:
arena[humanY][humanX] = empty
humanY = humanY + yInc
humanX = humanX + xInc
arena[humanY][humanX] = human
printArena()
moveMachines()
if theEnd != notYet:
break
# action loop
case theEnd:
of surrender:
writeLine(stdout, "\nSorry to see you quit.")
of electrified:
writeLine(stdout, "\nZap! You touched the fence!")
of killed:
writeLine(stdout, "\nYou have been killed by a lucky machine.")
of victory:
writeLine(stdout, "\nYou are lucky, you destroyed all machines.")
else:
discard
if not yes("\nDo you want to play again? "):
break
# game loop
writeLine(stdout, "\nHope you don't feel fenced in.")
writeLine(stdout, "Try again sometime.")
# Main {{{1
# =============================================================
randomize()
setStyle(defaultInk)
clearScreen()
printCredits()
pressEnter("\nPress the Enter key to read the instructions. ")
clearScreen()
printInstructions()
pressEnter("\nPress the Enter key to start. ")
play()
In Oberon-07
(*
Chase
Original version in BASIC:
Anonymous. Published in 1977 in The Best of Creative Computing
Volumen 2, page 253:
https://www.atariarchives.org/bcc2/showpage.php?page=253
This version in Oberon-07:
Copyright (c) 2022, 2023, Marcos Cruz (programandala.net)
SPDX-License-Identifier: Fair
Last modified 20231215T1548+0100.
*)
MODULE chase;
IMPORT
Input0,
Out,
obqChar,
obqKey,
obqRandom,
obqScreen;
CONST
(* Map size: *)
xSize = 20;
lastX = xSize-1;
ySize = 10;
lastY = ySize-1;
(* Values of `theEnd` beside the default 0: *)
quit = 1;
electrified = 2;
killed = 3;
victory = 4;
machines = 5;
lastMachine = machines-1;
machinesDrag = 2; (* probability not moving: 0=0%, 1=50%, 2=66%, 3=75%, etc. *)
(* Map content: *)
empty = " ";
fence = "X";
machine = "m";
human = "@";
fences = 15; (* inner obstacles, not the border *)
VAR
map : ARRAY ySize,xSize OF CHAR;
machineX : ARRAY machines OF INTEGER;
machineY : ARRAY machines OF INTEGER;
operative : ARRAY machines OF BOOLEAN;
destroyedMachines : INTEGER; (* counter *)
humanX : INTEGER;
humanY : INTEGER;
theEnd : INTEGER;
moving : BOOLEAN;
xInc : INTEGER;
yInc : INTEGER;
PROCEDURE Instructions;
(*
Display the game instructions and wait for a key.
*)
BEGIN
obqScreen.Clear;
Out.String("You (");
Out.Char(human);
Out.String(") are in a high voltage maze with ");
Out.Int(machines,0);Out.Ln;
Out.String("security machines (");
Out.Char(machine); Out.String(") trying to kill you.");Out.Ln;
Out.String("You must maneuver them into the maze (");
Out.Char(fence);Out.Char(")");Out.Ln;
Out.String("to survive.");Out.Ln;
Out.Ln;
Out.String("Good luck!");Out.Ln;
Out.Ln;
Out.String("The movement keys are the following 8 digits:");Out.Ln;
Out.Ln;
obqScreen.Tab(10);Out.String(" \ ^ / ");Out.Ln;
obqScreen.Tab(10);Out.String(" \ | / ");Out.Ln;
obqScreen.Tab(10);Out.String(" 7 8 9 ");Out.Ln;
obqScreen.Tab(10);Out.String(" <--4 6-->");Out.Ln;
obqScreen.Tab(10);Out.String(" 1 2 3 ");Out.Ln;
obqScreen.Tab(10);Out.String(" / | \ ");Out.Ln;
obqScreen.Tab(10);Out.String(" / V \ ");Out.Ln;
Out.Ln;
Out.String("Plus '0' to end the game.");Out.Ln;
Out.Ln;
Out.String("Press any key to start.");
WHILE Input0.Available() = 0 DO
END;
END Instructions;
PROCEDURE Yes(question: ARRAY OF CHAR): BOOLEAN;
(*
Display the given string `question` followed by the valid answer
keys and wait for a valid keypress ("Y" or "N", case-insensitive).
If the key is "Y" return `TRUE`, otherwise return `FALSE`.
*)
VAR key: CHAR;
BEGIN
Out.String(question);
Out.String(" (Y/N)");
Out.Ln;
REPEAT
key := obqChar.Upper(obqKey.Wait());
UNTIL (key = "Y") OR (key = "N");
RETURN key = "Y"
END Yes;
PROCEDURE PrintMap;
(*
Display the map at the top left corner of the screen.
*)
VAR x,y: INTEGER;
BEGIN
obqScreen.Home;
FOR y := 0 TO lastY DO
FOR x := 0 TO lastX DO
Out.Char(map[y,x])
END;
Out.Ln;
END;
END PrintMap;
PROCEDURE IsBorder(y,x: INTEGER): BOOLEAN;
(*
If the given map coordinates `y` and `x` are part of the border map
(i.e. its surrounding fence), return `TRUE`, otherwise return
`FALSE`.
*)
BEGIN
RETURN (y = 0) OR (x = 0) OR (y = lastY) OR (x = lastX)
END IsBorder;
PROCEDURE Place(VAR y,x: INTEGER; c: CHAR);
(*
Place the given character `c` at a random empty position of the map
and update the given variables `y` and `x` with that position.
*)
VAR tmpY,tmpX: INTEGER;
BEGIN
REPEAT
tmpY := obqRandom.IntRange(1,lastY-1);
tmpX := obqRandom.IntRange(1,lastX-1);
UNTIL map[tmpY,tmpX] = empty;
y := tmpY;
x := tmpX;
map[y,x] := c
END Place;
PROCEDURE InhabitMap;
(*
Occupy the map with the machines, the inner fences and the human.
*)
VAR n,x,y: INTEGER;
BEGIN
FOR n := 0 TO lastMachine DO
Place(machineY[n],machineX[n],machine);
operative[n] := TRUE
END;
FOR n := 1 TO fences DO
Place(y,x,fence); (* XXX TODO *)
END;
Place(humanY,humanX,human);
END InhabitMap;
PROCEDURE MakeMap;
(*
Make an empty map with a surrounding fence.
*)
VAR x,y: INTEGER;
BEGIN
FOR y := 0 TO lastY DO
FOR x := 0 TO lastX DO
IF IsBorder(y,x) THEN
map[y,x] := fence
ELSE
map[y,x] := empty
END;
END;
END;
END MakeMap;
PROCEDURE Init;
(*
Init the game by making a new map and resetting the variables.
*)
BEGIN
MakeMap;
InhabitMap;
destroyedMachines := 0;
theEnd := 0;
END Init;
PROCEDURE MoveMachine(m: INTEGER);
(*
Move the given machine `m`.
*)
VAR
maybe: INTEGER;
BEGIN
map[machineY[m],machineX[m]] := empty;
maybe := obqRandom.IntRange(0,1);
IF machineY[m]>humanY THEN
DEC(machineY[m],maybe)
ELSIF machineY[m]<humanY THEN
INC(machineY[m],maybe)
END;
maybe := obqRandom.IntRange(0,1);
IF (machineX[m]>humanX) THEN
DEC(machineX[m],maybe)
ELSIF (machineX[m]<humanX) THEN
INC(machineX[m],maybe)
END;
IF map[machineY[m],machineX[m]] = empty THEN
map[machineY[m],machineX[m]] := machine
ELSIF map[machineY[m],machineX[m]] = fence THEN
operative[m] := FALSE;
INC(destroyedMachines);
IF destroyedMachines = machines THEN
theEnd := victory
END
ELSIF map[machineY[m],machineX[m]] = human THEN
theEnd := killed
END;
END MoveMachine;
PROCEDURE MoveMachines;
(*
Move all of the operative machines.
*)
VAR machine: INTEGER;
BEGIN
FOR machine := 0 TO lastMachine DO
IF operative[machine] & (obqRandom.IntMax(machinesDrag) = 0) THEN
MoveMachine(machine)
END;
END;
END MoveMachines;
PROCEDURE GetMove(VAR yInc,xInc: INTEGER): BOOLEAN;
(*
Read a keypress and update the given variables `yInc` and `xInc`
according to the direction, with values -1..1.
If the key is "0", update the `theEnd`.
Return `TRUE` if a movement key was pressed, otherwise `FALSE`.
*)
VAR key: CHAR;
BEGIN
yInc := 0;
xInc := 0;
Input0.Read(key);
CASE ORD(key) OF
48: theEnd := quit | (* "0" = quit *)
49: yInc := +1; xInc := -1 | (* "1" = SW *)
50: yInc := +1; | (* "2" = S *)
51: yInc := +1; xInc := +1 | (* "3" = SE *)
52: xInc := -1 | (* "4" = W *)
54: xInc := +1 | (* "6" = E *)
55: yInc := -1; xInc := -1 | (* "7" = NW *)
56: yInc := -1; | (* "8" = N *)
57: yInc := -1; xInc := +1 | (* "9" = NE *)
0..47,53,58..255: (* other = nothing *)
END;
RETURN (yInc # 0) OR (xInc # 0)
END GetMove;
BEGIN
obqRandom.Randomize;
obqScreen.Clear;
Out.String("CHASE");
Out.Ln;
IF Yes("Do you want instructions?") THEN
Instructions;
END;
REPEAT (* game loop *)
obqScreen.Clear;
Init;
REPEAT (* action loop *)
PrintMap;
moving := GetMove(yInc,xInc);
IF theEnd = 0 THEN
IF moving THEN
map[humanY,humanX] := empty;
IF map[humanY+yInc,humanX+xInc] = fence THEN
theEnd := electrified
ELSIF map[humanY+yInc,humanX+xInc] = machine THEN
theEnd := killed
ELSE
map[humanY,humanX] := empty;
humanY := humanY + yInc;
humanX := humanX + xInc;
map[humanY,humanX] := human;
PrintMap;
MoveMachines
END;
END;
END;
UNTIL theEnd # 0; (* action loop *)
CASE theEnd OF
quit:
Out.String("Sorry to see you quit.") |
electrified:
Out.String("Zap! You touched the fence!") |
killed:
Out.String("You have been killed by a lucky machine.") |
victory:
Out.String("You are lucky, you destroyed all machines.");
END;
Out.Ln;
UNTIL ~ Yes("Do you want to play again?"); (* game loop *)
Out.String("Hope you don't feel fenced in.");
Out.Ln;
Out.String("Try again sometime.");
Out.Ln;
END chase.
In Odin
/*
Chase
Original version in BASIC:
Anonymous.
Published in 1977 in "The Best of Creative Computing", Volume 2.
https://www.atariarchives.org/bcc2/showpage.php?page=253
Version in Oberon-07:
Copyright (c) 2022, 2023, Marcos Cruz (programandala.net)
SPDX-License-Identifier: Fair
This version in Odin:
Copyright (c) 2023, 2025, Marcos Cruz (programandala.net)
SPDX-License-Identifier: Fair
Written on 2023-12-15, 2025-02-27.
Last modified 20250227T1852+0100.
*/
package chase
import "../lib/anodino/src/basic"
import "../lib/anodino/src/read"
import "../lib/anodino/src/term"
import "core:fmt"
import "core:math/rand"
import "core:slice"
import "core:strings"
// Globals {{{1
// =============================================================
// Colors
DEFAULT_INK :: basic.WHITE
INPUT_INK :: basic.BRIGHT_GREEN
INSTRUCTIONS_INK :: basic.YELLOW
TITLE_INK :: basic.BRIGHT_RED
// Arena
ARENA_WIDTH :: 20
ARENA_HEIGHT :: 10
ARENA_LAST_X :: ARENA_WIDTH - 1
ARENA_LAST_Y :: ARENA_HEIGHT - 1
ARENA_ROW :: 3
arena : [ARENA_HEIGHT][ARENA_WIDTH]string
EMPTY :: " "
FENCE :: "X"
MACHINE :: "m"
HUMAN :: "@"
FENCES :: 15 // inner obstacles, not the border
// The end
End :: enum {
Not_Yet,
Quit,
Electrified,
Killed,
Victory,
}
the_end : End
// Machines
MACHINES :: 5
MACHINES_DRAG :: 2 // probability not moving: 0=0%, 1=50%, 2=66%, 3=75%, etc.
machine_x : [MACHINES]int
machine_y : [MACHINES]int
operative : [MACHINES]bool
destroyed_machines : int // counter
// Human
human_x : int
human_y : int
// User input {{{1
// =============================================================
// Print the given prompt and wait until the user enters a string.
//
input_string :: proc(prompt : string = "") -> (string, bool) {
basic.color(INPUT_INK)
defer basic.color(DEFAULT_INK)
fmt.print(prompt)
return read.a_string()
}
// Print the given prompt and wait until the user presses Enter.
//
press_enter :: proc(prompt : string) {
s, _ := input_string(prompt)
delete(s)
}
// Return `true` if the given string is "yes" or a synonym.
//
is_yes :: proc(s : string) -> bool {
lowercase_s := strings.to_lower(s)
defer delete(lowercase_s)
return slice.any_of([]string{"ok", "y", "yeah", "yes"}, lowercase_s)
}
// Return `true` if the given string is "no" or a synonym.
//
is_no :: proc(s : string) -> bool {
lowercase_s := strings.to_lower(s)
defer delete(lowercase_s)
return slice.any_of([]string{"n", "no", "nope"}, lowercase_s)
}
// Print the given prompt, wait until the user enters a valid yes/no string,
// and return `true` for "yes" or `false` for "no".
//
yes :: proc(prompt : string) -> bool {
for {
answer, _ := input_string(prompt)
defer delete(answer)
if is_yes(answer) do return true
if is_no(answer) do return false
}
}
// Title, credits and instructions {{{1
// =============================================================
TITLE :: "Chase"
// Print the title at the current cursor position.
//
print_title :: proc() {
basic.color(TITLE_INK)
fmt.println(TITLE)
basic.color(DEFAULT_INK)
}
print_credits :: proc() {
print_title()
fmt.println("\nOriginal version in BASIC:")
fmt.println(" Anonymous.")
fmt.println(" Published in \"The Best of Creative Computing\", Volume 2, 1977.")
fmt.println(" https://www.atariarchives.org/bcc2/showpage.php?page=253")
fmt.println("This version in Odin:")
fmt.println(" Copyright (c) 2023, Marcos Cruz (programandala.net)")
fmt.println(" SPDX-License-Identifier: Fair")
}
// Print the game instructions and wait for a key.
//
print_instructions :: proc() {
print_title()
basic.color(INSTRUCTIONS_INK)
defer basic.color(DEFAULT_INK)
fmt.printfln("\nYou (%v) are in a high voltage maze with %v", HUMAN, MACHINES)
fmt.printfln("security machines (%v) trying to kill you.", MACHINE)
fmt.printfln("You must maneuver them into the maze (%v) to survive.\n", FENCE)
fmt.println("Good luck!\n")
fmt.println("The movement commands are the following:\n")
fmt.println(" ↖ ↑ ↗")
fmt.println(" NW N NE")
fmt.println(" ← W E →")
fmt.println(" SW S SE")
fmt.println(" ↙ ↓ ↘")
fmt.println("\nPlus 'Q' to end the game.")
}
// Arena {{{1
// =============================================================
// Display the arena at the top left corner of the screen.
//
print_arena :: proc() {
term.set_cursor_position(ARENA_ROW, 1)
for y in 0 ..= ARENA_LAST_Y {
for x in 0 ..= ARENA_LAST_X {
fmt.print(arena[y][x])
}
fmt.println()
}
}
// If the given arena coordinates `y` and `x` are part of the border arena
// (i.e. its surrounding fence), return `true`, otherwise return `false`.
//
is_border :: proc(y, x: int) -> bool {
return (y == 0) || (x == 0) || (y == ARENA_LAST_Y) || (x == ARENA_LAST_X)
}
// Return a random integer in the given inclusive range.
//
random_int_in_inclusive_range :: proc(min, max : int) -> int {
return rand.int_max(max - min + 1) + min
}
// Place the given string at a random empty position of the arena and return
// the coordinates.
//
place :: proc(s: string) -> (y, x : int) {
for {
y = random_int_in_inclusive_range(1, ARENA_LAST_Y - 1)
x = random_int_in_inclusive_range(1, ARENA_LAST_X - 1)
if arena[y][x] == EMPTY do break
}
arena[y][x] = s
return y, x
}
// Inhabit the arena with the machines, the inner fences and the human.
//
inhabit_arena :: proc() {
for m in 0 ..< MACHINES {
machine_y[m], machine_x[m] = place(MACHINE)
operative[m] = true
}
for _ in 0 ..< FENCES do place(FENCE)
human_y, human_x = place(HUMAN)
}
// Clean the arena, i.e. empty it and add a surrounding fence.
//
clean_arena :: proc() {
for y in 0 ..= ARENA_LAST_Y {
for x in 0 ..= ARENA_LAST_X {
arena[y][x] = is_border(y, x) ? FENCE : EMPTY
}
}
}
// Game {{{1
// =============================================================
// Init the game.
//
init_game :: proc() {
clean_arena()
inhabit_arena()
destroyed_machines = 0
the_end = .Not_Yet
}
// Move the given machine.
//
move_machine :: proc(m: int) {
maybe : int
arena[machine_y[m]][machine_x[m]] = EMPTY
maybe = rand.int_max(2)
if machine_y[m] > human_y {
machine_y[m] -= maybe
} else if machine_y[m] < human_y {
machine_y[m] += maybe
}
maybe = rand.int_max(2)
if machine_x[m] > human_x {
machine_x[m] -= maybe
} else if machine_x[m] < human_x {
machine_x[m] += maybe
}
if arena[machine_y[m]][machine_x[m]] == EMPTY {
arena[machine_y[m]][machine_x[m]] = MACHINE
} else if arena[machine_y[m]][machine_x[m]] == FENCE {
operative[m] = false
destroyed_machines += 1
if destroyed_machines == MACHINES {
the_end = .Victory
}
} else if arena[machine_y[m]][machine_x[m]] == HUMAN {
the_end = .Killed
}
}
// Maybe move the given operative machine.
//
maybe_move_machine :: proc(m: int) {
if rand.int_max(MACHINES_DRAG) == 0 do move_machine(m)
}
// Move the operative machines.
//
move_machines :: proc() {
for m in 0 ..< MACHINES {
if operative[m] do maybe_move_machine(m)
}
}
// Read a user command; update `the_end` accordingly and return the direction
// increments.
//
get_move :: proc() -> (y_inc, x_inc : int) {
y_inc = 0
x_inc = 0
fmt.println()
term.erase_line_right()
raw_command, _ := input_string("Command: ")
defer delete(raw_command)
command := strings.to_lower(raw_command)
defer delete(command)
switch command {
case "q" : the_end = .Quit
case "sw" : y_inc = +1; x_inc = -1
case "s" : y_inc = +1;
case "se" : y_inc = +1; x_inc = +1
case "w" : x_inc = -1
case "e" : x_inc = +1
case "nw" : y_inc = -1; x_inc = -1
case "n" : y_inc = -1;
case "ne" : y_inc = -1; x_inc = +1
}
return y_inc, x_inc
}
play :: proc() {
y_inc, x_inc : int
for { // game loop
term.clear_screen()
print_title()
init_game()
for { // action loop
print_arena()
y_inc, x_inc = get_move()
if the_end == .Not_Yet {
if y_inc != 0 || x_inc != 0 {
arena[human_y][human_x] = EMPTY
if arena[human_y + y_inc][human_x + x_inc] == FENCE {
the_end = .Electrified
} else if arena[human_y + y_inc][human_x + x_inc] == MACHINE {
the_end = .Killed
} else {
arena[human_y][human_x] = EMPTY
human_y = human_y + y_inc
human_x = human_x + x_inc
arena[human_y][human_x] = HUMAN
print_arena()
move_machines()
}
}
}
if the_end != .Not_Yet do break
}// action loop
#partial switch the_end {
case .Quit :
fmt.println("\nSorry to see you quit.")
case .Electrified :
fmt.println("\nZap! You touched the fence!")
case .Killed :
fmt.println("\nYou have been killed by a lucky machine.")
case .Victory :
fmt.println("\nYou are lucky, you destroyed all machines.")
}
if ! yes("\nDo you want to play again? ") do break
} // game loop
fmt.println("\nHope you don't feel fenced in.")
fmt.println("Try again sometime.")
}
// Main {{{1
// =============================================================
main :: proc() {
basic.randomize()
basic.color(DEFAULT_INK)
term.clear_screen()
print_credits()
press_enter("\nPress the Enter key to read the instructions. ")
term.clear_screen()
print_instructions()
press_enter("\nPress the Enter key to start. ")
play()
}
In Pike
#!/usr/bin/env pike
// Chase
// Original version in BASIC:
// Anonymous.
// Published in 1977 in "The Best of Creative Computing", Volume 2.
// https://www.atariarchives.org/bcc2/showpage.php?page=253
// This version in Pike:
// Copyright (c) 2025, Marcos Cruz (programandala.net)
// SPDX-License-Identifier: Fair
//
// Written on 2025-03-11.
// Last modified: 20250731T1954+0200.
// Terminal {{{1
// =============================================================================
constant BLACK = 0;
constant RED = 1;
constant GREEN = 2;
constant YELLOW = 3;
constant BLUE = 4;
constant MAGENTA = 5;
constant CYAN = 6;
constant WHITE = 7;
constant DEFAULT = 9;
constant STYLE_OFF = 20;
constant FOREGROUND = 30;
constant BACKGROUND = 40;
constant BRIGHT = 60;
constant NORMAL_STYLE = 0;
void move_cursor_home() {
write("\x1B[H");
}
void set_cursor_position(int line, int col) {
write("\x1B[%d;%dH", line, col);
}
void hide_cursor() {
write("\x1B[?25l");
}
void show_cursor() {
write("\x1B[?25h");
}
void set_style(int style) {
write("\x1B[%dm", style);
}
void reset_attributes() {
set_style(NORMAL_STYLE);
}
void erase_line_to_end() {
write("\x1B[K");
}
void erase_screen() {
write("\x1B[2J");
}
void clear_screen() {
erase_screen();
reset_attributes();
move_cursor_home();
}
// Config {{{1
// =============================================================
constant DEFAULT_INK = WHITE + FOREGROUND;
constant INPUT_INK = BRIGHT + GREEN + FOREGROUND;
constant INSTRUCTIONS_INK = YELLOW + FOREGROUND;
constant TITLE_INK = BRIGHT + RED + FOREGROUND;
// Data {{{1
// =============================================================
// Arena
constant ARENA_WIDTH = 20;
constant ARENA_HEIGHT = 10;
constant ARENA_LAST_X = ARENA_WIDTH - 1;
constant ARENA_LAST_Y = ARENA_HEIGHT - 1;
constant ARENA_ROW = 3;
array(array(string)) arena = allocate(ARENA_HEIGHT, allocate(ARENA_WIDTH, ""));
constant EMPTY = " ";
constant FENCE = "X";
constant MACHINE = "m";
constant HUMAN = "@";
constant FENCES = 15; // inner obstacles, not the border
// The end
enum End {
NOT_YET,
QUIT,
ELECTRIFIED,
KILLED,
VICTORY
}
End the_end = NOT_YET;
// Machines
constant MACHINES = 5;
constant MACHINES_DRAG = 2; // probability not moving: 0=0%, 1=50%, 2=66%, 3=75%, etc.
array(int) machine_x = allocate(MACHINES);
array(int) machine_y = allocate(MACHINES);
array(bool) operative = allocate(MACHINES);
int destroyed_machines = 0; // counter
// Human
int human_x = 0;
int human_y = 0;
// User input {{{1
// =============================================================
string accept_string(string prompt) {
write(prompt);
return Stdio.stdin->gets();
}
string get_string(string prompt) {
set_style(INPUT_INK);
string s = accept_string(prompt);
set_style(DEFAULT_INK);
return s;
}
void press_enter(string prompt) {
accept_string(prompt);
}
bool is_yes(string s) {
switch(lower_case(s)) {
case "ok":
case "y":
case "yeah":
case "yes":
return true;
default:
return false;
}
}
bool is_no(string s) {
switch(lower_case(s)) {
case "n":
case "no":
case "nope":
return true;
default:
return false;
}
}
// Print the given prompt, wait until the user enters a valid yes/no string,
// and return `true` for "yes" or `false` for "no".
//
bool yes(string prompt) {
while (true) {
string answer = get_string(prompt);
if (is_yes(answer)) {
return true;
}
if (is_no(answer)) {
return false;
}
}
}
// Title, credits and instructions {{{1
// =============================================================
constant TITLE = "Chase";
void print_title() {
set_style(TITLE_INK);
write("%s\n", TITLE);
set_style(DEFAULT_INK);
}
void print_credits() {
print_title();
write("\nOriginal version in BASIC:\n");
write(" Anonymous.\n");
write(" Published in \"The Best of Creative Computing\", Volume 2, 1977.\n");
write(" https://www.atariarchives.org/bcc2/showpage.php?page=253\n");
write("This version in Pike:\n");
write(" Copyright (c) 2025, Marcos Cruz (programandala.net)\n");
write(" SPDX-License-Identifier: Fair\n");
}
void print_instructions() {
print_title();
set_style(INSTRUCTIONS_INK);
write("\nYou (%s) are in a high voltage maze with %d\n", HUMAN, MACHINES);
write("security machines (%s) trying to kill you.\n", MACHINE);
write("You must maneuver them into the maze (%s) to survive.\n\n", FENCE);
write("Good luck!\n\n");
write("The movement commands are the following:\n\n");
write(" ↖ ↑ ↗\n");
write(" NW N NE\n");
write(" ← W E →\n");
write(" SW S SE\n");
write(" ↙ ↓ ↘\n");
write("\nPlus 'Q' to end the game.\n");
set_style(DEFAULT_INK);
}
// Arena {{{1
// =============================================================
void print_arena() {
set_cursor_position(ARENA_ROW, 1);
for (int y = 0; y <= ARENA_LAST_Y; y += 1) {
for (int x = 0; x <= ARENA_LAST_X; x += 1) {
write(arena[y][x]);
}
write("\n");
}
}
// If the given arena coordinates `y` and `x` are part of the border arena
// (i.e. its surrounding fence), return `true`, otherwise return `false`.
//
bool is_border(int y, int x) {
return (y == 0) || (x == 0) || (y == ARENA_LAST_Y) || (x == ARENA_LAST_X);
}
int random_int_in_inclusive_range(int min, int max) {
return random(max - min) + min;
}
// Place the given string at a random empty position of the arena and return
// the coordinates.
//
array(int) place(string s) {
int y = 0;
int x = 0;
while (true) {
y = random_int_in_inclusive_range(1, ARENA_LAST_Y - 1);
x = random_int_in_inclusive_range(1, ARENA_LAST_X - 1);
if (arena[y][x] == EMPTY) {
break;
}
}
arena[y][x] = s;
return ({y, x});
}
// Inhabit the arena with the machines, the inner fences and the human.
//
void inhabit_arena() {
array(int) coords;
for (int m = 0; m < MACHINES; m += 1) {
coords = place(MACHINE);
machine_y[m] = coords[0];
machine_x[m] = coords[1];
operative[m] = true;
}
for (int i = 0; i < FENCES; i += 1) {
place(FENCE);
}
coords = place(HUMAN);
human_y = coords[0];
human_x = coords[1];
}
// Clean the arena, i.e. empty it and add a surrounding fence.
//
void clean_arena() {
for (int y = 0; y <= ARENA_LAST_Y; y += 1) {
for (int x = 0; x <= ARENA_LAST_X; x += 1) {
arena[y][x] = is_border(y, x) ? FENCE : EMPTY;
}
}
}
// Game {{{1
// =============================================================
void init_game() {
clean_arena();
inhabit_arena();
destroyed_machines = 0;
the_end = NOT_YET;
}
void move_machine(int m) {
int maybe = 0;
arena[machine_y[m]][machine_x[m]] = EMPTY;
maybe = random(2);
if (machine_y[m] > human_y) {
machine_y[m] -= maybe;
} else if (machine_y[m] < human_y) {
machine_y[m] += maybe;
}
maybe = random(2);
if (machine_x[m] > human_x) {
machine_x[m] -= maybe;
} else if (machine_x[m] < human_x) {
machine_x[m] += maybe;
}
if (arena[machine_y[m]][machine_x[m]] == EMPTY) {
arena[machine_y[m]][machine_x[m]] = MACHINE;
} else if (arena[machine_y[m]][machine_x[m]] == FENCE) {
operative[m] = false;
destroyed_machines += 1;
if (destroyed_machines == MACHINES) {
the_end = VICTORY;
}
} else if (arena[machine_y[m]][machine_x[m]] == HUMAN) {
the_end = KILLED;
}
}
void maybe_move_machine(int m) {
if (random(MACHINES_DRAG) == 0) {
move_machine(m);
}
}
void move_machines() {
for (int m = 0; m < MACHINES; m += 1) {
if (operative[m]) {
maybe_move_machine(m);
}
}
}
// Read a user command; update `the_end` accordingly and return the direction
// increments.
//
array(int) get_move() {
int y_inc = 0;
int x_inc = 0;
write("\n");
erase_line_to_end();
string command = lower_case(get_string("Command: "));
switch (command) {
case "q":
the_end = QUIT;
break;
case "sw":
y_inc = 1;
x_inc = -1;
break;
case "s":
y_inc = 1;
break;
case "se":
y_inc = 1;
x_inc = 1;
break;
case "w":
x_inc = -1;
break;
case "e":
x_inc = 1;
break;
case "nw":
y_inc = -1;
x_inc = -1;
break;
case "n":
y_inc = -1;
break;
case "ne":
y_inc = -1;
x_inc = 1;
}
return ({y_inc, x_inc});
}
void play() {
int y_inc = 0;
int x_inc = 0;
while (true) { // game loop
clear_screen();
print_title();
init_game();
while (true) { // action loop
print_arena();
array(int) coords = get_move();
y_inc = coords[0];
x_inc = coords[1];
if (the_end == NOT_YET) {
if (y_inc != 0 || x_inc != 0) {
arena[human_y][human_x] = EMPTY;
if (arena[human_y + y_inc][human_x + x_inc] == FENCE) {
the_end = ELECTRIFIED;
} else if (arena[human_y + y_inc][human_x + x_inc] == MACHINE) {
the_end = KILLED;
} else {
arena[human_y][human_x] = EMPTY;
human_y = human_y + y_inc;
human_x = human_x + x_inc;
arena[human_y][human_x] = HUMAN;
print_arena();
move_machines();
}
}
}
if (the_end != NOT_YET) {
break;
}
} // action loop;
switch (the_end) {
case QUIT:
write("\nSorry to see you quit.\n");
break;
case ELECTRIFIED:
write("\nZap! You touched the fence!\n");
break;
case KILLED:
write("\nYou have been killed by a lucky machine.\n");
break;
case VICTORY:
write("\nYou are lucky, you destroyed all machines.\n");
}
if (!yes("\nDo you want to play again? ")) {
break;
}
} // game loop
write("\nHope you don't feel fenced in.\n");
write("Try again sometime.\n");
}
// Main {{{1
// =============================================================
void main() {
set_style(DEFAULT_INK);
clear_screen();
print_credits();
press_enter("\nPress the Enter key to read the instructions. ");
clear_screen();
print_instructions();
press_enter("\nPress the Enter key to start. ");
play();
}
In V
/*
Chase
Original version in BASIC:
Anonymous.
Published in 1977 in "The Best of Creative Computing", Volume 2.
https://www.atariarchives.org/bcc2/showpage.php?page=253
Version in Oberon-07:
Copyright (c) 2022, 2023, Marcos Cruz (programandala.net)
SPDX-License-Identifier: Fair
Version in Odin:
Copyright (c) 2023, Marcos Cruz (programandala.net)
SPDX-License-Identifier: Fair
This version in V:
Copyright (c) 2025, Marcos Cruz (programandala.net)
SPDX-License-Identifier: Fair
Written on 2025-01-11.
Last modified 20250122T0344+0100.
*/
import os
import rand
import term
// Console {{{1
// =============================================================
const black = 0
const red = 1
const green = 2
const yellow = 3
const blue = 4
const magenta = 5
const cyan = 6
const white = 7
const default_color = 9
const foreground = 30
const background = 40
const bright = 60
// Set the given color.
//
fn set_color(color int) {
print('\x1B[0;${color}m')
}
// Erase from the current cursor position to the end of the current line.
//
fn erase_line_right() {
print('\x1B[K')
}
// Constants {{{1
// =============================================================
// Colors
const default_ink = foreground + default_color
const input_ink = foreground + bright + green
const instructions_ink = foreground + yellow
const title_ink = foreground + bright + red
// Arena
const arena_width = 20
const arena_height = 10
const arena_last_x = arena_width - 1
const arena_last_y = arena_height - 1
const arena_row = 3
const empty = ' '
const fence = 'X'
const machine = 'm'
const human = '@'
const fences = 15 // inner obstacles, not the border
// The end
enum End {
not_yet
quit
electrified
killed
victory
}
// Machines
const machines = 5
const machines_drag = 2 // probability not moving: 0=0%, 1=50%, 2=66%, 3=75%, etc.
// Context structure {{{1
// =============================================================
struct Machine {
mut:
x int
y int
operative bool
}
struct Human {
mut:
x int
y int
}
struct Context {
mut:
arena [arena_height][arena_width]string
machine [machines]Machine
human Human
destroyed_machines int
the_end End
}
// User input {{{1
// =============================================================
// Print the given prompt and wait until the user enters a string.
//
fn input_string(prompt string) string {
set_color(input_ink)
defer { set_color(default_ink) }
return os.input(prompt)
}
// Print the given prompt and wait until the user presses Enter.
//
fn press_enter(prompt string) {
input_string(prompt)
}
// Return `true` if the given string is "yes" or a synonym.
//
fn is_yes(s string) bool {
return ['ok', 'y', 'yeah', 'yes'].any(it == s.to_lower())
}
// Return `true` if the given string is "no" or a synonym.
//
fn is_no(s string) bool {
return ['n', 'no', 'nope'].any(it == s.to_lower())
}
// Print the given prompt, wait until the user enters a valid yes/no string,
// and return `true` for "yes" or `false` for "no".
//
fn yes(prompt string) bool {
mut result := false
for {
answer := input_string(prompt)
if is_yes(answer) {
result = true
break
}
if is_no(answer) {
result = false
break
}
}
return result
}
// Title, credits and instructions {{{1
// =============================================================
const title = 'Chase'
// Print the title at the current cursor position.
//
fn print_title() {
set_color(title_ink)
println(title)
set_color(default_ink)
}
fn print_credits() {
print_title()
println('\nOriginal version in BASIC:')
println(' Anonymous.')
println(' Published in "The Best of Creative Computing", Volume 2, 1977.')
println(' https://www.atariarchives.org/bcc2/showpage.php?page=253')
println('This version in V:')
println(' Copyright (c) 2025, Marcos Cruz (programandala.net)')
println(' SPDX-License-Identifier: Fair')
}
// Print the game instructions and wait for a key.
//
fn print_instructions() {
print_title()
set_color(instructions_ink)
defer { set_color(default_ink) }
println('\nYou (${human}) are in a high voltage maze with ${machines}')
println('security machines (${machine}) trying to kill you.')
println('You must maneuver them into the maze (${fence}) to survive.\n')
println('Good luck!\n')
println('The movement commands are the following:\n')
println(' ↖ ↑ ↗')
println(' NW N NE')
println(' ← W E →')
println(' SW S SE')
println(' ↙ ↓ ↘')
println("\nPlus 'Q' to end the game.")
}
// Arena {{{1
// =============================================================
// Display the arena at the top left corner of the screen.
//
fn print_arena(context Context) {
term.set_cursor_position(y: arena_row, x: 1)
for y in 0 .. arena_last_y + 1 {
for x in 0 .. arena_last_x + 1 {
print(context.arena[y][x])
}
println('')
}
}
// If the given arena coordinates `y` and `x` are part of the border arena
// (i.e. its surrounding fence), return `true`, otherwise return `false`.
//
fn is_border(y int, x int) bool {
return y == 0 || x == 0 || y == arena_last_y || x == arena_last_x
}
// Return a random integer in the given inclusive range.
//
fn random_int_in_inclusive_range(min int, max int) int {
return rand.intn(max - min + 1) or { 0 } + min
}
// Place the given string at a random empty position of the arena and return
// the coordinates.
//
fn place(s string, mut context Context) (int, int) {
mut x := 0
mut y := 0
for {
y = random_int_in_inclusive_range(1, arena_last_y - 1)
x = random_int_in_inclusive_range(1, arena_last_x - 1)
if context.arena[y][x] == empty {
break
}
}
context.arena[y][x] = s
return y, x
}
// Inhabit the arena with the machines, the inner fences and the human.
//
fn inhabit_arena(mut context Context) {
for m in 0 .. machines {
context.machine[m].y, context.machine[m].x = place(machine, mut context)
context.machine[m].operative = true
}
for _ in 0 .. fences {
place(fence, mut context)
}
context.human.y, context.human.x = place(human, mut context)
}
// Clean the arena, i.e. empty it and add a surrounding fence.
//
fn clean_arena(mut context Context) {
for y in 0 .. arena_last_y + 1 {
for x in 0 .. arena_last_x + 1 {
context.arena[y][x] = if is_border(y, x) { fence } else { empty }
}
}
}
// Game {{{1
// =============================================================
// Init the game.
//
fn init_game(mut context Context) {
clean_arena(mut context)
inhabit_arena(mut context)
context.destroyed_machines = 0
context.the_end = .not_yet
}
// Move the given machine.
//
fn move_machine(m int, mut context Context) {
context.arena[context.machine[m].y][context.machine[m].x] = empty
mut maybe := rand.intn(2) or { 0 }
if context.machine[m].y > context.human.y {
context.machine[m].y -= maybe
} else if context.machine[m].y < context.human.y {
context.machine[m].y += maybe
}
maybe = rand.intn(2) or { 0 }
if context.machine[m].x > context.human.x {
context.machine[m].x -= maybe
} else if context.machine[m].x < context.human.x {
context.machine[m].x += maybe
}
if context.arena[context.machine[m].y][context.machine[m].x] == empty {
context.arena[context.machine[m].y][context.machine[m].x] = machine
} else if context.arena[context.machine[m].y][context.machine[m].x] == fence {
context.machine[m].operative = false
context.destroyed_machines += 1
if context.destroyed_machines == machines {
context.the_end = .victory
}
} else if context.arena[context.machine[m].y][context.machine[m].x] == human {
context.the_end = .killed
}
}
// Maybe move the given operative machine.
//
fn maybe_move_machine(m int, mut context Context) {
if rand.intn(machines_drag) or { 0 } == 0 {
move_machine(m, mut context)
}
}
// Move the operative machines.
//
fn move_machines(mut context Context) {
for m in 0 .. machines {
if context.machine[m].operative {
maybe_move_machine(m, mut context)
}
}
}
// Read a user command; update `context.the_end` accordingly and return the direction
// increments.
//
fn get_move(mut context Context) (int, int) {
mut y_inc := 0
mut x_inc := 0
println('')
erase_line_right()
command := input_string('Command: ').to_lower()
match command {
'q' {
context.the_end = .quit
}
'sw' {
y_inc = 1
x_inc = -1
}
's' {
y_inc = 1
}
'se' {
y_inc = 1
x_inc = 1
}
'w' {
x_inc = -1
}
'e' {
x_inc = 1
}
'nw' {
y_inc = -1
x_inc = -1
}
'n' {
y_inc = -1
}
'ne' {
y_inc = -1
x_inc = 1
}
else {}
}
return y_inc, x_inc
}
fn play() {
mut context := Context{}
mut y_inc := 0
mut x_inc := 0
for { // game loop
term.clear()
print_title()
init_game(mut context)
for { // action loop
print_arena(context)
y_inc, x_inc = get_move(mut context)
if context.the_end == .not_yet {
if y_inc != 0 || x_inc != 0 {
context.arena[context.human.y][context.human.x] = empty
if context.arena[context.human.y + y_inc][context.human.x + x_inc] == fence {
context.the_end = .electrified
} else if context.arena[context.human.y + y_inc][context.human.x + x_inc] == machine {
context.the_end = .killed
} else {
context.arena[context.human.y][context.human.x] = empty
context.human.y = context.human.y + y_inc
context.human.x = context.human.x + x_inc
context.arena[context.human.y][context.human.x] = human
print_arena(context)
move_machines(mut context)
}
}
}
if context.the_end != .not_yet {
break
}
} // action loop
match context.the_end {
.quit {
println('\nSorry to see you quit.')
}
.electrified {
println('\nZap! You touched the fence!')
}
.killed {
println('\nYou have been killed by a lucky machine.')
}
.victory {
println('\nYou are lucky, you destroyed all machines.')
}
else {}
}
if !yes('\nDo you want to play again? ') {
break
}
} // game loop
println("\nHope you don't feel fenced in.")
println('Try again sometime.')
}
// Main {{{1
// =============================================================
fn main() {
set_color(default_ink)
term.clear()
print_credits()
press_enter('\nPress the Enter key to read the instructions. ')
term.clear()
print_instructions()
press_enter('\nPress the Enter key to start. ')
play()
}
In Vala
// Chase
// Original version in BASIC:
// Anonymous.
// Published in 1977 in "The Best of Creative Computing", Volume 2.
// https://www.atariarchives.org/bcc2/showpage.php?page=253
// This version in Vala:
// Copyright (c) 2026, Marcos Cruz (programandala.net)
// SPDX-License-Identifier: Fair
//
// Written in 2026-08-28/30.
// Last modified: 20260830T0041+0200.
// Terminal {{{1
// =============================================================================
const int BLACK = 0;
const int RED = 1;
const int GREEN = 2;
const int YELLOW = 3;
const int BLUE = 4;
const int MAGENTA = 5;
const int CYAN = 6;
const int WHITE = 7;
const int DEFAULT = 9;
const int STYLE_OFF = 20;
const int FOREGROUND = 30;
const int BACKGROUND = 40;
const int BRIGHT = 60;
const int NORMAL_STYLE = 0;
void move_cursor_home() {
stdout.printf("\x1B[H");
}
void set_cursor_position(int line, int col) {
stdout.printf("\x1B[%d;%dH", line, col);
}
void set_style(int style) {
stdout.printf("\x1B[%dm", style);
}
void reset_attributes() {
set_style(NORMAL_STYLE);
}
void erase_line_to_end() {
stdout.printf("\x1B[K");
}
void erase_screen() {
stdout.printf("\x1B[2J");
}
void clear_screen() {
erase_screen();
reset_attributes();
move_cursor_home();
}
const int DEFAULT_INK = WHITE + FOREGROUND;
const int INPUT_INK = BRIGHT + GREEN + FOREGROUND;
const int INSTRUCTIONS_INK = YELLOW + FOREGROUND;
const int TITLE_INK = BRIGHT + RED + FOREGROUND;
// Data {{{1
// =============================================================
const int ARENA_WIDTH = 20;
const int ARENA_HEIGHT = 10;
const int ARENA_LAST_X = ARENA_WIDTH - 1;
const int ARENA_LAST_Y = ARENA_HEIGHT - 1;
const int ARENA_ROW = 3;
char[,] arena;
const char EMPTY = ' ';
const char FENCE = 'X';
const char MACHINE = 'm';
const char HUMAN = '@';
const int FENCES = 15; // inner obstacles, not the border
enum End {
NOT_YET,
QUIT,
ELECTRIFIED,
KILLED,
VICTORY
}
End the_end = End.NOT_YET;
const int MACHINES = 5;
const int MACHINES_DRAG = 2; // probability not moving: 0=0%, 1=50%, 2=66%, 3=75%, etc.
struct Machine {
int x;
int y;
bool operative;
}
Machine[] machine;
int destroyed_machines = 0; // counter
int human_x = 0;
int human_y = 0;
// User input {{{1
// =============================================================
string accept_string(string prompt) {
stdout.printf(prompt);
return stdin.read_line().strip();
}
string get_string(string prompt) {
set_style(INPUT_INK);
string s = accept_string(prompt);
set_style(DEFAULT_INK);
return s;
}
void press_enter(string prompt) {
accept_string(prompt);
}
bool is_yes(string s) {
switch (s.down()) {
case "ok":
case "y":
case "yeah":
case "yes":
return true;
default:
return false;
}
}
bool is_no(string s) {
switch (s.down()) {
case "n":
case "no":
case "nope":
return true;
default:
return false;
}
}
// Print the given prompt, wait until the user enters a valid yes/no string,
// and return `true` for "yes" or `false` for "no".
bool yes(string prompt) {
while (true) {
string answer = get_string(prompt);
if (is_yes(answer)) {
return true;
}
if (is_no(answer)) {
return false;
}
}
}
// Title, credits and instructions {{{1
// =============================================================
const string TITLE = "Chase";
void print_title() {
set_style(TITLE_INK);
stdout.printf("%s\n", TITLE);
set_style(DEFAULT_INK);
}
void print_credits() {
print_title();
stdout.printf("\nOriginal version in BASIC:\n");
stdout.printf(" Anonymous.\n");
stdout.printf(" Published in \"The Best of Creative Computing\", Volume 2, 1977.\n");
stdout.printf(" https://www.atariarchives.org/bcc2/showpage.php?page=253\n");
stdout.printf("This version in Vala:\n");
stdout.printf(" Copyright (c) 2026, Marcos Cruz (programandala.net)\n");
stdout.printf(" SPDX-License-Identifier: Fair\n");
}
void print_instructions() {
print_title();
set_style(INSTRUCTIONS_INK);
stdout.printf("\nYou (%c) are in a high voltage maze with %d\n", HUMAN, MACHINES);
stdout.printf("security machines (%c) trying to kill you.\n", MACHINE);
stdout.printf("You must maneuver them into the maze (%c) to survive.\n\n", FENCE);
stdout.printf("Good luck!\n\n");
stdout.printf("The movement commands are the following:\n\n");
stdout.printf(" ↖ ↑ ↗\n");
stdout.printf(" NW N NE\n");
stdout.printf(" ← W E →\n");
stdout.printf(" SW S SE\n");
stdout.printf(" ↙ ↓ ↘\n");
stdout.printf("\nPlus 'Q' to end the game.\n");
set_style(DEFAULT_INK);
}
// Arena {{{1
// =============================================================
void print_arena() {
set_cursor_position(ARENA_ROW, 1);
for (int y = 0; y <= ARENA_LAST_Y; y++) {
for (int x = 0; x <= ARENA_LAST_X; x++) {
stdout.printf("%c", arena[y, x]);
}
stdout.printf("\n");
}
}
bool is_border(int y, int x) {
return (y == 0) || (x == 0) || (y == ARENA_LAST_Y) || (x == ARENA_LAST_X);
}
int random_int_in_inclusive_range(int min, int max) {
return GLib.Random.int_range(min, min + max);
}
// Place the given string at a random empty position of the arena and update the
// coordinates.
void place(char s, ref int y, ref int x) {
do {
y = random_int_in_inclusive_range(1, ARENA_LAST_Y - 1);
x = random_int_in_inclusive_range(1, ARENA_LAST_X - 1);
} while (arena[y, x] != EMPTY);
arena[y, x] = s;
}
void place_fences() {
int y = 0;
int x = 0;
for (int i = 0; i < FENCES; i++) {
place(FENCE, ref y, ref x);
}
}
void place_machines() {
for (int m = 0; m < MACHINES; m++) {
place(MACHINE, ref machine[m].y, ref machine[m].x);
machine[m].operative = true;
}
}
void inhabit_arena() {
place_machines();
place_fences();
place(HUMAN, ref human_y, ref human_x);
}
void clean_arena() {
for (int y = 0; y <= ARENA_LAST_Y; y++) {
for (int x = 0; x <= ARENA_LAST_X; x++) {
arena[y, x] = is_border(y, x) ? FENCE : EMPTY;
}
}
}
// Game {{{1
// =============================================================
void init_once() {
arena = new char[ARENA_HEIGHT, ARENA_WIDTH];
machine = new Machine[MACHINES];
}
void init_game() {
clean_arena();
inhabit_arena();
destroyed_machines = 0;
the_end = End.NOT_YET;
}
void move_machine(int m) {
int maybe = 0;
arena[machine[m].y, machine[m].x] = EMPTY;
maybe = GLib.Random.int_range(0, 2);
if (machine[m].y > human_y) {
machine[m].y -= maybe;
}
else if (machine[m].y < human_y) {
machine[m].y += maybe;
}
maybe = GLib.Random.int_range(0, 2);
if (machine[m].x > human_x) {
machine[m].x -= maybe;
}
else if (machine[m].x < human_x) {
machine[m].x += maybe;
}
if (arena[machine[m].y, machine[m].x] == EMPTY) {
arena[machine[m].y, machine[m].x] = MACHINE;
}
else if (arena[machine[m].y, machine[m].x] == FENCE) {
machine[m].operative = false;
destroyed_machines += 1;
if (destroyed_machines == MACHINES) {
the_end = End.VICTORY;
}
}
else if (arena[machine[m].y, machine[m].x] == HUMAN) {
the_end = End.KILLED;
}
}
void maybe_move_machine(int m) {
if (GLib.Random.int_range(0, MACHINES_DRAG) == 0) {
move_machine(m);
}
}
void move_machines() {
for (int m = 0; m < MACHINES; m++) {
if (machine[m].operative) {
maybe_move_machine(m);
}
}
}
// Read a user command; update `the_end` accordingly and set the direction
// increments.
void set_move(ref int y_inc, ref int x_inc) {
stdout.printf("\n");
erase_line_to_end();
string command = get_string("Command: ").down();
switch (command) {
case "q":
the_end = End.QUIT;
break;
case "sw":
y_inc = 1;
x_inc = -1;
break;
case "s":
y_inc = 1;
x_inc = 0;
break;
case "se":
y_inc = 1;
x_inc = 1;
break;
case "w":
y_inc = 0;
x_inc = -1;
break;
case "e":
y_inc = 0;
x_inc = 1;
break;
case "nw":
y_inc = -1;
x_inc = -1;
break;
case "n":
y_inc = -1;
x_inc = 0;
break;
case "ne":
y_inc = -1;
x_inc = 1;
break;
default:
y_inc = 0;
x_inc = 0;
break;
}
}
void play() {
init_once();
int y_inc = 0;
int x_inc = 0;
do {
clear_screen();
print_title();
init_game();
while (the_end == End.NOT_YET) {
print_arena();
set_move(ref y_inc, ref x_inc);
if (the_end == End.NOT_YET) {
if (y_inc != 0 || x_inc != 0) {
arena[human_y, human_x] = EMPTY;
if (arena[human_y + y_inc, human_x + x_inc] == FENCE) {
the_end = End.ELECTRIFIED;
}
else if (arena[human_y + y_inc, human_x + x_inc] == MACHINE) {
the_end = End.KILLED;
}
else {
arena[human_y, human_x] = EMPTY;
human_y = human_y + y_inc;
human_x = human_x + x_inc;
arena[human_y, human_x] = HUMAN;
print_arena();
move_machines();
}
}
}
switch (the_end) {
case End.NOT_YET:
break;
case End.QUIT:
stdout.printf("\nSorry to see you quit.\n");
break;
case End.ELECTRIFIED:
stdout.printf("\nZap! You touched the fence!\n");
break;
case End.KILLED:
stdout.printf("\nYou have been killed by a lucky machine.\n");
break;
case End.VICTORY:
stdout.printf("\nYou are lucky, you destroyed all machines.\n");
break;
}
} // action loop;
} while (yes("\nDo you want to play again? "));
stdout.printf("\nHope you don't feel fenced in.\n");
stdout.printf("Try again sometime.\n");
}
void main() {
set_style(DEFAULT_INK);
clear_screen();
print_credits();
press_enter("\nPress the Enter key to read the instructions. ");
clear_screen();
print_instructions();
press_enter("\nPress the Enter key to start. ");
play();
}
