Chase

Description of the page content

Conversion of Chase to several programming languages.

Tags:

This program has been converted to 11 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 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()
}

Related pages

Basics off
Metaproject about the "Basics of…" projects.

External related links