Xchange

Descrition del contenete del págine

Conversion de Xchange a pluri lingues de programation.

Etiquettes:

Ti-ci programa esset convertet a 11 lingues de programation.

Originale

Origin of xchange.bas:

"The A to Z Book of Computer Games", by Thomas C. McIntire, 1979.

- https://archive.org/details/The_A_to_Z_Book_of_Computer_Games/page/n269/mode/2up
- https://github.com/chaosotter/basic-games

5 CLS: COLOR 12
10 REM "XCHANGE"
15 PRINT TAB(36); "Xchange": PRINT: PRINT
20 REM
25 RANDOMIZE TIMER
30 GOSUB 9000
40 LET M1$ = "Invalid character."
50 LET M2$ = "Illegal move."
60 LET M3$ = "You're the winner!"
70 LET A$ = "*"
80 DIM X$(9), Y$(9)
90 COLOR 10: PRINT "Do you want instructions (Y/N)";
100 INPUT Q$: COLOR 15
110 IF LEFT$(Q$, 1) = "N" OR LEFT$(Q$, 1) = "n" THEN 160
120 IF LEFT$(Q$, 1) = "Y" OR LEFT$(Q$, 1) = "y" THEN 150
130 PRINT "Huh?"
140 GOTO 90
150 GOSUB 1000
160 COLOR 10: PRINT "Number of players (1 or 2)";
170 INPUT P: COLOR 15
180 IF P = 1 THEN 210
190 IF P = 2 THEN 210
200 GOTO 160
210 GOSUB 2000
220 GOSUB 3000
225 IF Q$ = "X" OR Q$ = "x" THEN 280
230 IF P <> 2 THEN 250
240 GOSUB 4000
250 GOSUB 5000
260 GOSUB 6000
270 IF I <> 8 THEN 220
280 PRINT
290 COLOR 10: PRINT "Another game (Y/N)";
300 INPUT Q$: COLOR 15
310 IF LEFT$(Q$, 1) = "Y" OR LEFT$(Q$, 1) = "y" THEN 160
320 PRINT: PRINT "So long..."
330 END
1000 COLOR 14: PRINT
1010 PRINT "One or two may play.  If two, you take turns.  A grid looks like this:"
1020 COLOR 11: PRINT
1030 PRINT "    F G D"
1040 PRINT "    A H *"
1045 PRINT "    E B C"
1050 COLOR 14: PRINT
1055 PRINT "But it should look like this:"
1060 COLOR 11: PRINT
1065 PRINT "    A B C"
1070 PRINT "    D E F"
1080 PRINT "    G H *"
1090 COLOR 14: PRINT
1100 PRINT "You may exchange any one letter with the '*', but only one that's adjacent:"
1110 PRINT "above, below, left, or right.  Not all puzzles are possible, and you may enter"
1115 PRINT "'X' to give up."
1120 PRINT
1150 PRINT "Here we go..."
1155 PRINT: COLOR 15
1160 RETURN
1999 REM  "SCRAMBLER"
2000 DATA A,B,C,D,E,F,G,H,*
2010 FOR I = 1 TO 9
2020 READ X$(I)
2030 NEXT I
2040 RESTORE
2050 FOR I = 1 TO 9
2060 LET R = INT(10 * RND(1))
2070 IF R < 1 THEN 2060
2080 LET X$ = X$(I)
2090 LET X$(I) = X$(R)
2100 LET X$(R) = X$
2110 NEXT I
2120 FOR I = 1 TO 9
2130 LET Y$(I) = X$(I)
2140 NEXT I
2150 GOSUB 5000
2160 RETURN
2999 REM "PLAYER-X MOVES"
3000 COLOR 10: PRINT "Move";
3010 INPUT Q$: COLOR 15
3012 IF Q$ >= "a" AND Q$ <= "z" THEN Q$ = CHR$(ASC(Q$) - 32)
3015 IF Q$ = "X" OR Q$ = "x" THEN 3200
3020 IF Q$ <> A$ THEN 3050
3030 PRINT M2$
3040 GOTO 3000
3050 FOR I = 1 TO 9
3060 IF Q$ = X$(I) THEN 3100
3070 NEXT I
3080 PRINT M1$
3090 GOTO 3000
3100 FOR J = 1 TO 9
3110 IF X$(J) = A$ THEN 3130
3120 NEXT J
3130 IF I + 1 = J THEN 3180
3140 IF I + 3 = J THEN 3180
3150 IF I - 1 = J THEN 3180
3160 IF I - 3 = J THEN 3180
3170 GOTO 3030
3180 LET X$(J) = X$(I)
3190 LET X$(I) = A$
3200 RETURN
3999 REM "PLAYER-Y MOVES"
4000 COLOR 10: PRINT TAB(16); "Move";
4010 INPUT Q$: COLOR 15
4012 IF Q$ >= "a" AND Q$ <= "z" THEN Q$ = CHR$(ASC(Q$) - 32)
4020 IF Q$ <> A$ THEN 4050
4030 PRINT M2$
4040 GOTO 4000
4050 FOR I = 1 TO 9
4060 IF Q$ = Y$(I) THEN 4100
4070 NEXT I
4080 PRINT TAB(16); M1$
4090 GOTO 4000
4100 FOR J = 1 TO 9
4110 IF Y$(J) = A$ THEN 4130
4120 NEXT J
4130 IF I + 1 = J THEN 4180
4140 IF I + 3 = J THEN 4180
4150 IF I - 1 = J THEN 4180
4160 IF I - 3 = J THEN 4180
4170 GOTO 4030
4180 LET Y$(J) = Y$(I)
4190 LET Y$(I) = A$
4200 RETURN
4999 REM  "PRINT GRIDS"
5000 PRINT: COLOR 11
5010 FOR I = 1 TO 9 STEP 3
5020 PRINT "   ";: FOR J = I TO I + 2
5030 PRINT " "; X$(J);
5040 NEXT J
5050 IF P = 2 THEN 5100
5060 PRINT
5070 NEXT I
5080 PRINT
5090 COLOR 15: RETURN
5100 PRINT TAB(16);: FOR J = I TO I + 2
5110 PRINT " "; Y$(J);
5120 NEXT J
5130 GOTO 5060
5999 REM  "WINNER CHECK"
6000 FOR I = 1 TO 8
6010 READ X$
6020 IF X$(I) <> X$ THEN 6070
6030 NEXT I
6040 PRINT M3$: I = 8
6050 RESTORE
6060 RETURN
6070 IF P <> 2 THEN 6050
6080 FOR I = 1 TO 8
6090 READ Y$
6100 IF Y$(I) <> Y$ THEN 6050
6110 NEXT I
6120 PRINT TAB(16); M3$: I = 8
6130 GOTO 6050
9000 REM  "RANDOM NUMBER ROUTINE"
9010 LET Z = RND(1)
9020 RETURN


In C#

// Xchange

// Original version in BASIC:
//  By Thomas C. McIntire, 1979.
//  Published in "The A to Z Book of Computer Games", 1979.
//  - https://archive.org/details/The_A_to_Z_Book_of_Computer_Games/page/n269/mode/2up
//  - https://github.com/chaosotter/basic-games

// This improved remake in C#:
//  Copyright (c) 2024, Marcos Cruz (programandala.net)
//  SPDX-License-Identifier: Fair
//
// Written in 2024-12-29/2025-01-02.
//
// Last modified: 20251205T1550+0100.

using System;
using System.Linq;

class xchange
{
    // Globals {{{1
    // =============================================================

    const ConsoleColor BOARD_INK = ConsoleColor.Cyan;
    const ConsoleColor DEFAULT_INK = ConsoleColor.White;
    const ConsoleColor INPUT_INK = ConsoleColor.Green;
    const ConsoleColor INSTRUCTIONS_INK = ConsoleColor.DarkYellow;
    const ConsoleColor TITLE_INK = ConsoleColor.Red;

    const string BLANK = "*";

    const int GRID_HEIGHT = 3; // cell rows
    const int GRID_WIDTH = 3; // cell columns

    const int CELLS = GRID_WIDTH * GRID_HEIGHT;

    static string[] pristineGrid = new string[CELLS];

    const int GRIDS_ROW = 3; // screen row where the grids are printed
    const int GRIDS_COLUMN = 5; // screen column where the left grid is printed
    const int CELLS_GAP = 2; // distance between the grid cells, in screen rows or columns
    const int GRIDS_GAP = 16; // screen columns between equivalent cells of the grids

    const int MAX_PLAYERS = 4;

    static string[][] grid = new string[MAX_PLAYERS][];

    static bool[] isPlaying = new bool[MAX_PLAYERS];

    static int players;

    const string QUIT_COMMAND = "X";

    // User input {{{1
    // =============================================================

    // Print the given prompt and wait until the user enters an integer.
    //
    static int InputInt(string prompt = "")
    {
        Console.ForegroundColor = INPUT_INK;

        int number;

        while (true)
        {
            Console.Write(prompt);
            try
            {
                number = (int) Int64.Parse(Console.ReadLine());
                break;
            }
            catch
            {
                Console.WriteLine("Integer expected.");
            }
        }
        Console.ForegroundColor = DEFAULT_INK;
        return number;
    }

    // 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, instructions and credits {{{1
    // =============================================================

    // Print the title at the current cursor position.
    //
    static void PrintTitle()
    {
        Console.ForegroundColor = TITLE_INK;
        Console.WriteLine("Xchange");
        Console.ForegroundColor = DEFAULT_INK;
    }

    // Print the credits at the current cursor position.
    //
    static void PrintCredits()
    {
        PrintTitle();
        Console.WriteLine("\nOriginal version in BASIC:");
        Console.WriteLine("    Written by Thomas C. McIntire, 1979.");
        Console.WriteLine("    Published in \"The A to Z Book of Computer Games\", 1979.");
        Console.WriteLine("    https://archive.org/details/The_A_to_Z_Book_of_Computer_Games/page/n269/mode/2up");
        Console.WriteLine("    https://github.com/chaosotter/basic-games");
        Console.WriteLine("\nThis improved remake in C#:");
        Console.WriteLine("    Copyright (c) 2024, Marcos Cruz (programandala.net)");
        Console.WriteLine("    SPDX-License-Identifier: Fair");
    }

    // Print the instructions at the current cursor position.
    //
    static void PrintInstructions()
    {
        PrintTitle();
        Console.ForegroundColor = INSTRUCTIONS_INK;
        Console.WriteLine("\nOne or two may play.  If two, you take turns.  A grid looks like this:\n");
        Console.ForegroundColor = BOARD_INK;
        Console.WriteLine("    F G D");
        Console.Write($"    A H {BLANK}\n");
        Console.WriteLine("    E B C\n");
        Console.ForegroundColor = INSTRUCTIONS_INK;
        Console.WriteLine("But it should look like this:\n");
        Console.ForegroundColor = BOARD_INK;
        Console.WriteLine("    A B C");
        Console.WriteLine("    D E F");
        Console.Write($"    G H {BLANK}\n\n");
        Console.ForegroundColor = INSTRUCTIONS_INK;
        Console.Write($"You may exchange any one letter with the '{BLANK}', but only one that's adjacent:\n");
        Console.WriteLine("above, below, left, or right.  Not all puzzles are possible, and you may enter");
        Console.Write($"'{QUIT_COMMAND}' to give up.\n\n");
        Console.WriteLine("Here we go...");
        Console.ForegroundColor = DEFAULT_INK;
    }

    // Grids {{{1
    // =============================================================

    // Print the given player's grid title.
    //
    static void PrintGridTitle(int player)
    {
        Console.SetCursorPosition(GRIDS_COLUMN + (player * GRIDS_GAP), GRIDS_ROW);
        Console.Write($"Player {player + 1}");
    }

    // Return the cursor position of the given player's grid cell.
    //
    static (int row, int column) CellPosition(int player, int cell)
    {
        int gridRow = cell / GRID_HEIGHT;
        int gridColumn = cell % GRID_WIDTH;
        int titleMargin = players > 1 ? 2 : 0;
        return (GRIDS_ROW + titleMargin + gridRow, GRIDS_COLUMN + (gridColumn * CELLS_GAP) + (player * GRIDS_GAP));
    }

    // Return the cursor position of the given player's grid prompt.
    //
    static (int row, int column) GridPromptPosition(int player)
    {
        int gridRow = CELLS / GRID_HEIGHT;
        int gridColumn = CELLS % GRID_WIDTH;
        int titleMargin = players > 1 ? 2 : 0;
        return (GRIDS_ROW + titleMargin + gridRow + 1, GRIDS_COLUMN + (gridColumn * CELLS_GAP) + (player * GRIDS_GAP));
    }

    // Print the given player's grid, in the given or default color.
    //
    static void PrintGrid(int player, ConsoleColor color = BOARD_INK)
    {
        if (players > 1)
        {
            PrintGridTitle(player);
        }
        Console.ForegroundColor = color;
        for (int cell = 0; cell < CELLS; cell++)
        {
            (int y, int x) = CellPosition(player, cell);
            Console.SetCursorPosition(x, y);
            Console.Write(grid[player][cell]);
        }
        Console.ForegroundColor = DEFAULT_INK;
    }

    // Print the current players' grids.
    //
    static void PrintGrids()
    {
        for (int player = 0; player < players; player++)
        {
            if (isPlaying[player])
            {
                PrintGrid(player);
            }
        }
        Console.WriteLine();
        // EraseScreenDown(); // XXX TODO
    }

    // Scramble the grid of the given player.
    //
    static void ScrambleGrid(int player)
    {
        Random rand = new Random();
        for (int cell = 0; cell < CELLS; cell++)
        {
            int randomCell = rand.Next(CELLS);
            // Exchange the contents of the current cell with that of the random one.
            string tmp = grid[player][cell];
            grid[player][cell] = grid[player][randomCell];
            grid[player][randomCell] = tmp;
        }
    }

    // Init the grids.
    //
    static void InitGrids()
    {
        grid[0] = (string[]) pristineGrid.Clone();
        ScrambleGrid(0);
        for (int player = 0 + 1; player < players; player++)
        {
            grid[player] = (string[]) grid[0].Clone();
        }
    }

    // Messages {{{1
    // =============================================================

    // Return a message prefix for the given player.
    //
    static string PlayerPrefix(int player)
    {
        return players > 1 ? $"Player {player + 1}: " : "";
    }

    // Return the cursor position of the given player's messages, adding the given
    // row increment, which defaults to zero.
    //
    static (int row, int column) MessagePosition(int player, int rowInc = 0)
    {
        (int promptRow, _) = GridPromptPosition(player);
        return (promptRow + 2 + rowInc, 1);
    }

    // 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);
    }

    // Print the given message about the given player, adding the given row
    // increment, which defaults to zero, to the default cursor coordinates.
    //
    static void PrintMessage(string message, int player, int rowInc = 0)
    {
        (int y, int x) = MessagePosition(player, rowInc);
        Console.SetCursorPosition(x, y);
        Console.Write($"{PlayerPrefix(player)}{message}");
        EraseLineRight();
        Console.WriteLine();
    }

    // Erase the last message about the given player.
    //
    static void EraseMessage(int player)
    {
        (int y, int x) = MessagePosition(player);
        Console.SetCursorPosition(x, y);
        EraseLineRight();
    }

    // Game loop {{{1
    // =============================================================

    // Return a message with the players range.
    //
    static string PlayersRangeMessage()
    {
        return (MAX_PLAYERS == 2) ? "1 or 2" : $"from 1 to {MAX_PLAYERS}";
    }

    // Return the number of players, asking the user if needed.
    //
    static int NumberOfPlayers()
    {
        int players = 0;
        PrintTitle();
        Console.WriteLine();
        while (players < 1 || players > MAX_PLAYERS)
        {
            players = InputInt($"Number of players ({PlayersRangeMessage()}): ");
        }
        return players;
    }

    // Is the given cell the first one on a grid row?
    //
    static bool isFirstCellOfA_gridRow(int cell)
    {
        return cell % GRID_WIDTH == 0;
    }

    // Is the given cell the last one on a grid row?
    //
    static bool isLastCellOfA_gridRow(int cell)
    {
        return (cell + 1) % GRID_WIDTH == 0;
    }

    // Are the given cells adjacent?
    //
    static bool AreCellsAdjacent(int cell_1, int cell_2)
    {
        if (cell_2 == cell_1 + 1 && ! isFirstCellOfA_gridRow(cell_2))
            return true;
        else if (cell_2 == cell_1 + GRID_WIDTH)
            return true;
        else if (cell_2 == cell_1 - 1  && ! isLastCellOfA_gridRow(cell_2))
            return true;
        else if (cell_2 == cell_1 - GRID_WIDTH)
            return true;
        else
            return false;
    }

    // Is the given player's character cell a valid move, i.e. is it adjacent to
    // the blank cell? If so, return the blank cell of the grid and `true`
    // otherwise return a fake cell and `false`.
    //
    static (int blankCell, bool ok) IsLegalMove(int player, int charCell)
    {
        const int NOWHERE = -1;

        for (int cell = 0; cell < CELLS; cell++)
        {
            if (grid[player][cell] == BLANK)
            {
                if (AreCellsAdjacent(charCell, cell))
                {
                    return (cell, true);
                }
                break;
            }
        }

        PrintMessage($"Illegal move \"{grid[player][charCell]}\".", player);
        return (NOWHERE, false);
    }

    // Is the given player's command valid, i.e. a grid character? If so, return
    // its position in the grid and `true`; otherwise print an error message and
    // return a fake position and `false`.
    //
    static (int position, bool ok) IsValidCommand(int player, string command)
    {
        // XXX FIXME

        const int NOWHERE = -1;
        if (command != BLANK)
        {
            for (int i = 0; i < CELLS; i++)
            {
                if (grid[player][i] == command)
                {
                    return (i, true);
                }
            }
        }
        PrintMessage($"Invalid character \"{command}\".", player);
        return (NOWHERE, false);
    }

    // Forget the given player, who quitted.
    //
    static void ForgetPlayer(int player)
    {
        isPlaying[player] = false;
        PrintGrid(player, DEFAULT_INK);
    }

    // Play the turn of the given player.
    //
    static void PlayTurn(int player)
    {
        int blankPosition;
        int characterPosition = 0;

        if (isPlaying[player])
        {
            while (true)
            {
                string command;
                bool ok = false;
                while (! ok)
                {
                    (int y, int x) = GridPromptPosition(player);
                    Console.SetCursorPosition(x, y);
                    EraseLineRight();
                    Console.SetCursorPosition(x, y);
                    command = InputString("Move: ").Trim().ToUpper();
                    if (command == QUIT_COMMAND)
                    {
                        ForgetPlayer(player);
                        return;
                    }
                    (characterPosition, ok) = IsValidCommand(player, command);
                }
                (blankPosition, ok) = IsLegalMove(player, characterPosition);
                if (ok)
                {
                    break;
                }
            }
            EraseMessage(player);
            grid[player][blankPosition] = grid[player][characterPosition];
            grid[player][characterPosition] = BLANK;
        }
    }

    // Play the turns of all players.
    //
    static void PlayTurns()
    {
        for (int player = 0; player < players; player++) PlayTurn(player);
    }

    // Is someone playing?
    //
    static bool IsSomeonePlaying()
    {
        for (int player = 0; player < players; player++)
        {
            if (isPlaying[player])
            {
                return true;
            }
        }
        return false;
    }

    // Has someone won? If so, print a message for every winner and return `true`
    // otherwise just return `false`.
    //
    static bool HasSomeoneWon()
    {
        int winners = 0;

        for (int player = 0; player < players; player++)
        {
            if (isPlaying[player])
            {
                if (grid[player].SequenceEqual(pristineGrid))
                {
                    winners += 1;
                    if (winners > 0)
                    {
                        string too = winners > 1 ? ", too" : "";
                        PrintMessage
                        (
                            $"You're the winner{too}!",
                            player,
                            winners - 1
                        );
                    }
                }
            }
        }

        return winners > 0;
    }

    // Init the game.
    //
    static void InitGame()
    {
        Console.Clear();
        players = NumberOfPlayers();
        for (int player = 0; player < players; player++)
        {
            isPlaying[player] = true;
        }
        Console.Clear();
        PrintTitle();
        InitGrids();
        PrintGrids();
    }

    // Play the game.
    //
    static void Play()
    {
        InitGame();
        while (IsSomeonePlaying())
        {
            PlayTurns();
            PrintGrids();
            if (HasSomeoneWon())
            {
                break;
            }
        }
    }

    // Main {{{1
    // =============================================================

    // Init the program, i.e. just once before the first game.
    //
    static void InitOnce()
    {
        // Init the pristine grid.
        const int FIRST_CHAR_CODE = (int) 'A';
        for (int i = 0; i < CELLS - 1; i++)
        {
            pristineGrid[i] = ((char) (FIRST_CHAR_CODE + i)).ToString();
        }
        pristineGrid[CELLS - 1] = BLANK;

        // Init the grid
        for (int i = 0; i < MAX_PLAYERS; i++)
        {
            grid[i] = new string[CELLS];
            grid[i] = pristineGrid;
        }
    }

    // Return `true` if the player does not want to play another game; otherwise
    // return `false`.
    //
    static bool Enough()
    {
        (int y, int x) = (GridPromptPosition(player : 0));
        Console.SetCursorPosition(x, y);
        return ! Yes("Another game? ");
    }

    static void Main()
    {
        InitOnce();

        Console.Clear();
        PrintCredits();

        PressEnter("\nPress the Enter key to read the instructions. ");
        Console.Clear();
        PrintInstructions();

        PressEnter("\nPress the Enter key to start. ");
        while (true)
        {
            Play();
            if (Enough())
            {
                break;
            }
        }
        Console.WriteLine("So long…");
    }
}

In Chapel

// Xchange

// Original version in BASIC:
//  Written by Thomas C. McIntire, 1979.
//  Published in "The A to Z Book of Computer Games", 1979.
//  https://archive.org/details/The_A_to_Z_Book_of_Computer_Games/page/n269/mode/2up
//  https://github.com/chaosotter/basic-games

// This improved remake in Chapel:
//  Copyright (c) 2025, 2026, Marcos Cruz (programandala.net)
//  SPDX-License-Identifier: Fair
//
// Written on 2025-04-09 and in 2026-02-07/08.
//
// Last modified: 20260208T2107+0100.

// Modules {{{1
// =============================================================================

import Random;
use IO;
use IO.FormattedIO; // format

// 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, column: int) {
    writef("\x1B[%i;%iH", line, column);
}

proc setCursorCoord(coord: (int, int)) {
    setCursorPosition(coord[0], coord[1]);
}

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 eraseScreenToEnd() {
    write("\x1B[J");
}

proc eraseScreen() {
    write("\x1B[2J");
}

proc clearScreen() {
    eraseScreen();
    resetAttributes();
    moveCursorHome();
}

// Data {{{1
// =============================================================

const BOARD_INK = FOREGROUND + BRIGHT + CYAN;
const DEFAULT_INK = FOREGROUND + WHITE;
const INPUT_INK = FOREGROUND + BRIGHT + GREEN;
const INSTRUCTIONS_INK = FOREGROUND + YELLOW;
const TITLE_INK = FOREGROUND + BRIGHT + RED;

const BLANK = "*";

const GRID_HEIGHT = 3; // cell rows
const GRID_WIDTH = 3; // cell columns

const CELLS = GRID_WIDTH * GRID_HEIGHT;
const gridRange = 0 ..< CELLS;

var pristineGrid: [gridRange] string;

const GRIDS_ROW = 3; // screen row where the grids are printed
const GRIDS_COLUMN = 5; // screen column where the left grid is printed
const CELLS_GAP = 2; // distance between the grid cells, in screen rows or columns
const GRIDS_GAP = 16; // screen columns between equivalent cells of the grids

const FIRST_PLAYER = 0;
const MAX_PLAYERS = 4;
const playerRange = FIRST_PLAYER ..< MAX_PLAYERS;

var grid: [playerRange, gridRange] string;

var isPlaying: [0 ..< MAX_PLAYERS] bool;

var players: int = 0;

const QUIT_COMMAND = "X";

// used with coord arrays instead of 0 and 1, for clarity
const Y = 0;
const X = 1;

// User input {{{1
// =============================================================

proc acceptString(prompt: string): string {
    write(prompt);
    stdout.flush();
    return readLine().strip();
}

// Print the given prompt, accept a string from the user. If the typed string
// is a valid integer return it; otherwise return 0.
//
proc getInteger(prompt: string = ""): int {
    var result: int;
    var s: string = acceptString(prompt);
    try {
        result = (s): int;
    }
    catch exc {
        result = 0;
    }
    return result;
}

proc getString(prompt: string = ""): string {
    setStyle(INPUT_INK);
    var s: string = acceptString(prompt);
    setStyle(DEFAULT_INK);
    return s;
}

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, instructions and credits {{{1
// =============================================================

proc printTitle() {
    setStyle(TITLE_INK);
    writeln("Xchange");
    setStyle(DEFAULT_INK);
}

proc printCredits() {
    printTitle();
    writeln("\nOriginal version in BASIC:");
    writeln("    Written by Thomas C. McIntire, 1979.");
    writeln("    Published in \"The A to Z Book of Computer Games\", 1979.");
    writeln("    https://archive.org/details/The_A_to_Z_Book_of_Computer_Games/page/n269/mode/2up");
    writeln("    https://github.com/chaosotter/basic-games");
    writeln("This improved remake in Chapel:");
    writeln("    Copyright (c) 2025, 2026, Marcos Cruz (programandala.net)");
    writeln("    SPDX-License-Identifier: Fair");
}

proc printInstructions() {
    printTitle();
    setStyle(INSTRUCTIONS_INK);
    writeln("\nOne or two may play.  If two, you take turns.  A grid looks like this:\n");
    setStyle(BOARD_INK);
    writeln("    F G D");
    writef("    A H %s\n", BLANK);
    writeln("    E B C\n");
    setStyle(INSTRUCTIONS_INK);
    writeln("But it should look like this:\n");
    setStyle(BOARD_INK);
    writeln("    A B C");
    writeln("    D E F");
    writef("    G H %s\n\n", BLANK);
    setStyle(INSTRUCTIONS_INK);
    writef("You may exchange any one letter with the '%s', but only one that's adjacent:\n", BLANK);
    writeln("above, below, left, or right.  Not all puzzles are possible, and you may enter");
    writef("'%s' to give up.\n\n", QUIT_COMMAND);
    writeln("Here we go...");
    setStyle(DEFAULT_INK);
}

// Grids {{{1
// =============================================================

// Print the given player's grid title.
//
proc printGridTitle(player: int) {
    setCursorPosition(GRIDS_ROW, GRIDS_COLUMN + (player * GRIDS_GAP));
    writef("Player %i", player + 1);
}

// Return the cursor position of the given player's grid cell.
//
proc cellPosition(player: int, cell: int): (int, int) {
    var gridRow: int = cell / GRID_HEIGHT;
    var gridColumn: int = cell % GRID_WIDTH;
    var titleMargin: int = if players > 1 then 2 else 0;
    var row: int = GRIDS_ROW + titleMargin + gridRow;
    var column: int = GRIDS_COLUMN +
        (gridColumn * CELLS_GAP) +
        (player * GRIDS_GAP);
    return (row, column);
}

// Return the cursor position of the given player's grid prompt.
//
proc gridPromptPositionOf(player: int): (int, int) {
    var coord: (int, int) = cellPosition(player, CELLS);
    var row: int = coord[0];
    var column: int = coord[1];
    return (row + 1, column);
}

// Print the given player's grid, in the given or default color.
//
proc printGrid(player: int, color: int = BOARD_INK) {
    if players > 1 {
        printGridTitle(player);
    }
    setStyle(color);
    for cell in gridRange {
        setCursorCoord(cellPosition(player, cell));
        write(grid[player, cell]);
    }
    setStyle(DEFAULT_INK);
}

// Print the current players' grids.
//
proc printGrids() {
    for player in 0 ..< players {
        if isPlaying[player] {
            printGrid(player);
        }
    }
    writeln("");
    eraseScreenToEnd();
}

// Init the grids.
//
proc initGrids() {
    Random.shuffle(pristineGrid);
    for player in 0 ..< players {
        for cell in gridRange {
            grid[player, cell] = pristineGrid[cell];
        }
    }
}

// Messages {{{1
// =============================================================

// Return a message prefix for the given player.
//
proc playerPrefix(player: int): string {
    return if players > 1 then "Player %i: ".format(player + 1) else "";
}

// Return the cursor position of the given player's messages, adding the given
// row increment, which defaults to zero.
//
proc messagePosition(player: int, rowInc: int = 0 ): (int, int) {
    var promptCoord: (int, int) = gridPromptPositionOf(player);
    return (promptCoord[Y] + 2 + rowInc, 1);
}

// Print the given message about the given player, adding the given row
// increment, which defaults to zero, to the default cursor coordinates.
//
proc printMessage(message: string, player: int, rowInc: int = 0) {
    setCursorCoord(messagePosition(player, rowInc));
    writef("%s%s", playerPrefix(player), message);
    eraseLineToEnd();
    writeln("");
}

// Erase the last message about the given player.
//
proc eraseMessage(player: int) {
    setCursorCoord(messagePosition(player));
    eraseLineToEnd();
}

// Game loop {{{1
// =============================================================

// Return a message with the players range.
//
proc playersRangeMessage(): string {
    if MAX_PLAYERS == 2 {
        return "1 or 2";
    } else {
        return "from 1 to %i".format(MAX_PLAYERS);
    }
}

// Return the number of players, asking the user if needed.
//
proc numberOfPlayers(): int {
    var players: int = 0;
    printTitle();
    writeln("");
    if MAX_PLAYERS == 1 {
        players = 1;
    } else {
        while players < 1 || players > MAX_PLAYERS {
            var prompt: string = "Number of players (%s): ".format(playersRangeMessage());
            players = getInteger(prompt);
        }
    }
    return players;
}

// Is the given cell the first one on a grid row?
//
proc isFirstCellOfGridRow(cell: int): bool {
    return cell % GRID_WIDTH == 0;
}

// Is the given cell the last one on a grid row?
//
proc isLastCellOfGridRow(cell: int): bool {
    return (cell + 1) % GRID_WIDTH == 0;
}

// Are the given cells adjacent?
//
proc areCellsAdjacent(cell1: int, cell2: int): bool {
    return (cell2 == cell1 + 1 && !isFirstCellOfGridRow(cell2)) ||
        (cell2 == cell1 + GRID_WIDTH) ||
        (cell2 == cell1 - 1 && !isLastCellOfGridRow(cell2)) ||
        (cell2 == cell1 - GRID_WIDTH);
}

// If the given player's character cell is a valid move, i.e. it is adjacent to
// the blank cell, return the blank cell; otherwise return -1.
//
proc positionToCell(player: int, charCell: int): int {
    for cell in gridRange {
        if grid[player, cell] == BLANK {
            if areCellsAdjacent(charCell, cell) {
                return cell;
            } else {
                break;
            }
        }
    }
    printMessage("Illegal move \"%s\".".format(grid[player, charCell]), player);
    return -1;
}

// If the given player's command is valid, i.e. a grid character, return its
// position; otherwise return -1.
//
proc commandToPosition(player: int, command: string): int {
    if command != BLANK {
        for position in gridRange {
            if command == grid[player, position] {
                return position;
            }
        }
    }
    printMessage("Invalid character \"%s\".".format(command), player);
    return -1;
}

// Forget the given player, who quitted.
//
proc forgetPlayer(player: int) {
    isPlaying[player] = false;
    printGrid(player, DEFAULT_INK);
}

// Play the turn of the given player.
//
proc playTurn(player: int) {
    var blankPosition: int = 0;
    var characterPosition: int = 0;

    if isPlaying[player] {
        while true {
            while true {
                var coord: (int, int) = gridPromptPositionOf(player);
                setCursorCoord(coord);
                eraseLineToEnd();
                setCursorCoord(coord);
                var command: string = getString("Move: ").toUpper();
                if command == QUIT_COMMAND {
                    forgetPlayer(player);
                    return;
                }
                var position: int = commandToPosition(player, command);
                if position >= 0 {
                    characterPosition = position;
                    break;
                }
            }
            var position: int = positionToCell(player, characterPosition);
            if position >= 0 {
                blankPosition = position;
                break;
            }
        }
        eraseMessage(player);
        grid[player, blankPosition] = grid[player, characterPosition];
        grid[player, characterPosition] = BLANK;
    }
}

// Play the turns of all players.
//
proc playTurns() {
    for player in 0 ..< players {
        playTurn(player);
    }
}

// Is someone playing?
//
proc isSomeonePlaying(): bool {
    for player in 0 ..< players {
        if isPlaying[player] {
            return true;
        }
    }
    return false;
}

proc hasAnEmptyGrid(player: int): bool {
    for i in gridRange {
        if grid[player, i] != "" {
            return false;
        }
    }
    return true;
}

// Has someone won? If so, print a message for every winner and return `true`
// otherwise just return `false`.
//
proc hasSomeoneWon(): bool {
    var winners: int = 0;
    for player in 0 ..< players {
        if isPlaying[player] {
            if hasAnEmptyGrid(player) {
                winners += 1;
                if winners > 0 {
                    printMessage(
                        "You're the winner%s!".format(if winners > 1 then ", too" else ""),
                        player,
                        winners - 1);
                }
            }
        }
    }
    return winners > 0;
}

// Init the game.
//
proc initGame() {
    clearScreen();
    players = numberOfPlayers();
    for player in 0 ..< players {
        isPlaying[player] = true;
    }
    clearScreen();
    printTitle();
    initGrids();
    printGrids();
}

// Play the game.
//
proc play() {
    initGame();
    while isSomeonePlaying() {
        playTurns();
        printGrids();
        if hasSomeoneWon() {
            break;
        }
    }
}

// Main {{{1
// =============================================================

proc initOnce() {

    // Init the pristine grid.
    const FIRST_CHAR_CODE = 'A'.toCodepoint();
    for cell in gridRange {
        pristineGrid[cell] = codepointToString((FIRST_CHAR_CODE + cell): int(32));
    }
    pristineGrid[CELLS - 1] = BLANK;
}

// Return `true` if the player does not want to play another game; otherwise
// return `false`.
//
proc enough(): bool {
    setCursorCoord(gridPromptPositionOf(FIRST_PLAYER));
    return !yes("Another game? ");
}

proc main() {
    initOnce();

    clearScreen();
    printCredits();
    pressEnter("\nPress the Enter key to read the instructions. ");

    clearScreen();
    printInstructions();
    pressEnter("\nPress the Enter key to start. ");

    while true {
        play();
        if enough() {
            break;
        }
    }
    writeln("So long…");
}

In Crystal

# Xchange

# Original version in BASIC:
#   Written by Thomas C. McIntire, 1979.
#   Published in "The A to Z Book of Computer Games", 1979.
#   https://archive.org/details/The_A_to_Z_Book_of_Computer_Games/page/n269/mode/2up
#   https://github.com/chaosotter/basic-games

# This improved remake in Crystal:
#   Copyright (c) 2024, 2025, Marcos Cruz (programandala.net)
#   SPDX-License-Identifier: Fair
#
# Written in 2024-12, 2025-04.
#
# Last modified: 20250421T0020+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

# Clears the screen and moves the cursor to the home position.
def clear_screen
  print "\e[2J\e[H"
end

def set_color(color : Int32)
  print "\e[#{color}m"
end

# Sets the cursor position to the given coordinates (the top left position is 1, 1).
def set_cursor_position(line, column : Int32)
  print "\e[#{line};#{column}H"
end

def set_cursor_coord(coord : Tuple(Int32, Int32))
  line, column = coord
  set_cursor_position(line, column)
end

# Erases the screen from the current line down to the bottom of the screen.
def erase_screen_down
  print "\e[J"
end

def erase_line_right
  print "\e[K"
end

# Globals {{{1
# =============================================================

BOARD_INK        = BRIGHT + CYAN + FOREGROUND
DEFAULT_INK      = WHITE + FOREGROUND
INPUT_INK        = BRIGHT + GREEN + FOREGROUND
INSTRUCTIONS_INK = YELLOW + FOREGROUND
TITLE_INK        = BRIGHT + RED + FOREGROUND

BLANK = "*"

GRID_HEIGHT = 3 # cell rows
GRID_WIDTH  = 3 # cell columns

CELLS = GRID_WIDTH * GRID_HEIGHT

GRIDS_ROW    =  3 # screen row where the grids are printed
GRIDS_COLUMN =  5 # screen column where the left grid is printed
CELLS_GAP    =  2 # distance between the grid cells, in screen rows or columns
GRIDS_GAP    = 16 # screen columns between equivalent cells of the grids

MAX_PLAYERS = 4

QUIT_COMMAND = "X"

class Global
  class_property grid = Array(Array(String)).new(MAX_PLAYERS, Array(String).new(CELLS, ""))
  class_property is_playing = Array(Bool).new(MAX_PLAYERS, false)
  class_property players : Int32 = 0
  class_property pristine_grid = Array(String).new(CELLS)
end

# User input {{{1
# =============================================================

# Prints the given prompt, waits until the user enters a integer and returns it.
# If the input is not a valid integer, returns zero instead.
def input_int(prompt = "") : Int32
  while true
    print prompt
    begin
      return gets.not_nil!.to_i
      break
    rescue
      return 0
    end
  end
end

# Prints the given prompt and waits until the user enters a string.
#
def input_string(prompt = "") : String
  s = nil
  set_color(INPUT_INK)
  while s.is_a?(Nil)
    print prompt
    s = gets
  end
  set_color(DEFAULT_INK)
  return s
end

# Prints the given prompt and waits until the user presses Enter.
#
def press_enter(prompt : String)
  input_string(prompt)
end

# Returns `true` if the given string is "yes" or a synonym.
#
def is_yes?(s : String) : Bool
  return s.downcase.in?(["ok", "y", "yeah", "yes"])
end

# Returns `true` if the given string is "no" or a synonym.
#
def is_no?(s : String) : Bool
  return s.downcase.in?(["n", "no", "nope"])
end

# Prints the given prompt, waits until the user enters a valid yes/no string,
# and returns `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, instructions and credits {{{1
# =============================================================

# Prints the title at the current cursor position.
#
def print_title
  set_color(TITLE_INK)
  print "Xchange\n"
  set_color(DEFAULT_INK)
end

# Prints the credits at the current cursor position.
#
def print_credits
  print_title
  print "\nOriginal version in BASIC:\n"
  print "    Written by Thomas C. McIntire, 1979.\n"
  print "    Published in \"The A to Z Book of Computer Games\", 1979.\n"
  print "    https://archive.org/details/The_A_to_Z_Book_of_Computer_Games/page/n269/mode/2up\n"
  print "    https://github.com/chaosotter/basic-games\n"
  print "This improved remake in Crystal:\n"
  print "    Copyright (c) 2024, 2025, Marcos Cruz (programandala.net)\n"
  print "    SPDX-License-Identifier: Fair\n"
end

# Prints the instructions at the current cursor position.
#
def print_instructions
  print_title
  set_color(INSTRUCTIONS_INK)
  print "\nOne or two may play.  If two, you take turns.  A grid looks like this:\n\n"
  set_color(BOARD_INK)
  print "    F G D\n"
  print "    A H #{BLANK}\n"
  print "    E B C\n\n"
  set_color(INSTRUCTIONS_INK)
  print "But it should look like this:\n\n"
  set_color(BOARD_INK)
  print "    A B C\n"
  print "    D E F\n"
  print "    G H #{BLANK}\n\n"
  set_color(INSTRUCTIONS_INK)
  print "You may exchange any one letter with the '#{BLANK}', but only one that's adjacent:\n"
  print "above, below, left, or right.  Not all puzzles are possible, and you may enter\n"
  print "'#{QUIT_COMMAND}' to give up.\n\n"
  print "Here we go...\n"
  set_color(DEFAULT_INK)
end

# Grids {{{1
# =============================================================

# Prints the given player's grid title.
#
def print_grid_title(player : Int32)
  set_cursor_position(GRIDS_ROW, GRIDS_COLUMN + (player * GRIDS_GAP))
  print "Player #{player + 1}"
end

# Returns the cursor position of the given player's grid cell.
#
def cell_position(player, cell : Int32) : Tuple(Int32, Int32)
  grid_row : Int32 = cell // GRID_HEIGHT
  grid_column : Int32 = cell % GRID_WIDTH
  grid_columns : Int32 = GRID_HEIGHT * (CELLS_GAP + 1)
  title_margin = Global.players > 1 ? 2 : 0
  row = GRIDS_ROW + title_margin + grid_row
  column = GRIDS_COLUMN + (grid_column * CELLS_GAP) + (player * GRIDS_GAP)
  return row, column
end

# Returns the cursor position of the given player's grid prompt.
#
def grid_prompt_position(player : Int32) : Tuple(Int32, Int32)
  grid_row : Int32 = CELLS // GRID_HEIGHT
  grid_column : Int32 = CELLS % GRID_WIDTH
  grid_columns : Int32 = GRID_HEIGHT * (CELLS_GAP + 1)
  title_margin = Global.players > 1 ? 2 : 0
  rows = GRIDS_ROW + title_margin + grid_row + 1
  cols = GRIDS_COLUMN + (grid_column * CELLS_GAP) + (player * GRIDS_GAP)
  return rows, cols
end

# Prints the given player's grid, in the given or default color.
#
def print_grid(player : Int32, color : Int32 = BOARD_INK)
  if Global.players > 1
    print_grid_title(player)
  end
  set_color(color)
  (0...CELLS).each do |cell|
    set_cursor_coord(cell_position(player, cell))
    print Global.grid[player][cell]
  end
  set_color(DEFAULT_INK)
end

# Prints the current players' grids.
#
def print_grids
  (0...Global.players).each do |player|
    if Global.is_playing[player]
      print_grid(player)
    end
  end
  puts
  erase_screen_down
end

# Scrambles the grid of the given player.
#
def scramble_grid(player : Int32)
  (0...CELLS).each do |cell|
    random_cell = rand(CELLS)
    # Exchange the contents of the current cell with that of the random one.
    Global.grid[player][cell], Global.grid[player][random_cell] =
      Global.grid[player][random_cell], Global.grid[player][cell]
  end
end

# Inits the grids.
#
def init_grids
  Global.grid[0] = Global.pristine_grid.dup
  scramble_grid(0)
  (1...Global.players).each do |player|
    Global.grid[player] = Global.grid[0].dup
  end
end

# Messages {{{1
# =============================================================

# Returns a message prefix for the given player.
#
def player_prefix(player : Int32) : String
  return Global.players > 1 ? "Player #{player + 1}: " : ""
end

# Returns the cursor position of the given player's messages, adding the given
# row increment, which defaults to zero.
#
def message_position(player : Int32, row_inc : Int32 = 0) : Tuple(Int32, Int32)
  prompt_row, _ = grid_prompt_position(player)
  return prompt_row + 2 + row_inc, 1
end

# Prints the given message about the given player, adding the given row
# increment, which defaults to zero, to the default cursor coordinates.
#
def print_message(message : String, player : Int32, row_inc : Int32 = 0)
  set_cursor_coord(message_position(player, row_inc))
  print "#{player_prefix(player)}#{message}"
  erase_line_right
  puts
end

# Erases the last message about the given player.
#
def erase_message(player : Int32)
  set_cursor_coord(message_position(player))
  erase_line_right
end

# Game loop {{{1
# =============================================================

# Returns a message with the players range.
#
def players_range_message : String
  if MAX_PLAYERS == 2
    return "1 or 2"
  else
    return "from 1 to #{MAX_PLAYERS}"
  end
end

# Returns the number of players, asking the user if needed.
#
def number_of_players : Int32
  Global.players = 0
  print_title
  puts
  if MAX_PLAYERS == 1
    Global.players = 1
  else
    while Global.players < 1 || Global.players > MAX_PLAYERS
      Global.players = input_int("Number of players (#{players_range_message}): ")
    end
  end
  return Global.players
end

# Is the given cell the first one on a grid row?
#
def is_first_cell_of_a_grid_row?(cell : Int32) : Bool
  return (cell % GRID_WIDTH) == 0
end

# Is the given cell the last one on a grid row?
#
def is_last_cell_of_a_grid_row?(cell : Int32) : Bool
  return ((cell + 1) % GRID_WIDTH) == 0
end

# Are the given cells adjacent?
#
def are_cells_adjacent?(cell_1, cell_2 : Int32) : Bool
  case
  when cell_2 == cell_1 + 1 && !is_first_cell_of_a_grid_row?(cell_2)
    return true
  when cell_2 == cell_1 + GRID_WIDTH
    return true
  when cell_2 == cell_1 - 1 && !is_last_cell_of_a_grid_row?(cell_2)
    return true
  when cell_2 == cell_1 - GRID_WIDTH
    return true
  end
  return false
end

NOWHERE = -1

# If the given player's character cell is a valid move, i.e. it is adjacent to
# the blank cell, returns the blank cell; otherwise returns `nil`.
#
def position_to_cell(player, char_cell : Int32) : (Int32 | Nil)
  (0...CELLS).each do |cell|
    if Global.grid[player][cell] == BLANK
      if are_cells_adjacent?(char_cell, cell)
        return cell
      else
        break
      end
    end
  end
  print_message("Illegal move \"#{Global.grid[player][char_cell]}\".", player)
  return nil
end

# If the given player's command is valid, i.e. a grid character, return its
# position; otherwise return `nil`.
#
def command_to_position(player : Int32, command : String) : (Int32 | Nil)
  if command != BLANK
    (0...CELLS).each do |position|
      if Global.grid[player][position] == command
        return position
      end
    end
  end
  print_message("Invalid character \"#{command}\".", player)
  return nil
end

# Forgets the given player, who quitted.
#
def forget_player(player : Int32)
  Global.is_playing[player] = false
  print_grid(player, DEFAULT_INK)
end

# Plays the turn of the given player.
#
def play_turn(player : Int32)
  blank_position : (Int32 | Nil)
  character_position : (Int32 | Nil)

  if Global.is_playing[player]
    while true
      while true
        coord = grid_prompt_position(player)
        set_cursor_coord(coord)
        erase_line_right
        set_cursor_coord(coord)
        command = input_string("Move: ").rstrip(' ').upcase
        if command == QUIT_COMMAND
          forget_player(player)
          return
        end
        character_position = command_to_position(player, command)
        if character_position.is_a?(Int32)
          break
        end
      end
      blank_position = position_to_cell(player, character_position.as(Int32))
      if blank_position.is_a?(Int32)
        break
      end
    end

    erase_message(player)
    Global.grid[player][blank_position] = Global.grid[player][character_position.as(Int)]
    Global.grid[player][character_position.as(Int)] = BLANK
  end
end

# Plays the turns of all players.
#
def play_turns
  (0...Global.players).each do |player|
    play_turn(player)
  end
end

# Is someone playing?
#
def is_someone_playing : Bool
  (0...Global.players).each do |player|
    if Global.is_playing[player]
      return true
    end
  end
  return false
end

# Has someone won? If so, print a message for every winner and return `true`;
# otherwise just return `false`.
#
def has_someone_won? : Bool
  winners = 0
  (0...Global.players).each do |player|
    if Global.is_playing[player]
      if Global.grid[player] == Global.pristine_grid
        winners += 1
        if winners > 0
          print_message(
            "You're the winner#{winners > 1 ? ", too" : ""}!",
            player,
            row_inc = winners - 1)
        end
      end
    end
  end
  return winners > 0
end

# Inits the game.
#
def init_game
  clear_screen
  Global.players = number_of_players
  (0...Global.players).each do |player|
    Global.is_playing[player] = true
  end
  clear_screen
  print_title
  init_grids
  print_grids
end

# Plays the game.
#
def play
  init_game
  while is_someone_playing
    play_turns
    print_grids
    if has_someone_won?
      break
    end
  end
end

# Main {{{1
# =============================================================

FIRST_CHAR_CODE = 'A'.ord

# Inits the program, i.e. just once before the first game.
#
def init_once
  (0...CELLS).each do |cell|
    Global.pristine_grid << (FIRST_CHAR_CODE + cell).chr.to_s
  end
  Global.pristine_grid[-1] = BLANK
end

# Returns `true` if the player does not want to play another game; otherwise
# returns `false`.
#
def enough : Bool
  row, col = grid_prompt_position(player = 0)
  set_cursor_position(row, col)
  return !yes?("Another game? ")
end

init_once

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. ")
while true
  play
  if enough
    break
  end
end
print "So long…\n"

In D

// Xchange

// Original version in BASIC:
//     Written by Thomas C. McIntire, 1979.
//     Published in "The A to Z Book of Computer Games", 1979.
//     https://archive.org/details/The_A_to_Z_Book_of_Computer_Games/page/n269/mode/2up
//     https://github.com/chaosotter/basic-games

// This improved remake in D:
//     Copyright (c) 2025, Marcos Cruz (programandala.net)
//     SPDX-License-Identifier: Fair
//
// Written in 2025-03.
//
// Last modified: 20251220T0654+0100.

module xchange;

// 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 column)
{
    import std.stdio : writef;

    writef("\x1B[%d;%dH", line, column);
}

void setCursorCoord(int[] coord)
{
    setCursorPosition(coord[0], coord[1]);
}

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 eraseScreenToEnd()
{
    import std.stdio : write;

    write("\x1B[J");
}

void eraseScreen()
{
    import std.stdio : write;

    write("\x1B[2J");
}

void clearScreen()
{
    eraseScreen();
    resetAttributes();
    moveCursorHome();
}

// Data {{{1
// =============================================================

enum BOARD_INK = FOREGROUND + BRIGHT + CYAN;
enum DEFAULT_INK = FOREGROUND + WHITE;
enum INPUT_INK = FOREGROUND + BRIGHT + GREEN;
enum INSTRUCTIONS_INK = FOREGROUND + YELLOW;
enum TITLE_INK = FOREGROUND + BRIGHT + RED;

enum BLANK = "*";

enum GRID_HEIGHT = 3; // cell rows
enum GRID_WIDTH = 3; // cell columns

enum CELLS = GRID_WIDTH * GRID_HEIGHT;

string[CELLS] pristineGrid;

enum GRIDS_ROW = 3; // screen row where the grids are printed
enum GRIDS_COLUMN = 5; // screen column where the left grid is printed
enum CELLS_GAP = 2; // distance between the grid cells, in screen rows or columns
enum GRIDS_GAP = 16; // screen columns between equivalent cells of the grids

enum FIRST_PLAYER = 0;
enum MAX_PLAYERS = 4;

string[][] grid = new string[][](MAX_PLAYERS, CELLS);

bool[MAX_PLAYERS] isPlaying;

int players = 0;

enum QUIT_COMMAND = "X";

enum axis { Y, X }; // to use with coord arrays instead of 0 and 1, for clarity

// User input {{{1
// =============================================================

string acceptString(string prompt)
{
    import std.stdio : readln;
    import std.stdio : write;
    import std.string : strip;

    write(prompt);
    return strip(readln());
}

// Print the given prompt, accept a string from the user. If the typed string
// is a valid integer return it; otherwise return 0.

int getInteger(string prompt = "")
{
    import std.conv : to;

    int result;
    string s = acceptString(prompt);
    try
    {
        result = to!int(s);
    }
    catch (Exception exc)
    {
        result = 0;
    }
    return result;
}

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, instructions and credits {{{1
// =============================================================

void printTitle()
{
    import std.stdio : writeln;

    setStyle(TITLE_INK);
    writeln("Xchange");
    setStyle(DEFAULT_INK);
}

void printCredits()
{
    import std.stdio : writeln;

    printTitle();
    writeln("\nOriginal version in BASIC:");
    writeln("    Written by Thomas C. McIntire, 1979.");
    writeln("    Published in \"The A to Z Book of Computer Games\", 1979.");
    writeln("    https://archive.org/details/The_A_to_Z_Book_of_Computer_Games/page/n269/mode/2up");
    writeln("    https://github.com/chaosotter/basic-games");
    writeln("This improved remake in D:");
    writeln("    Copyright (c) 2025, Marcos Cruz (programandala.net)");
    writeln("    SPDX-License-Identifier: Fair");
}

void printInstructions()
{
    import std.stdio : writefln;
    import std.stdio : writeln;

    printTitle();
    setStyle(INSTRUCTIONS_INK);
    writeln("\nOne or two may play.  If two, you take turns.  A grid looks like this:\n");
    setStyle(BOARD_INK);
    writeln("    F G D");
    writefln("    A H %s", BLANK);
    writeln("    E B C\n");
    setStyle(INSTRUCTIONS_INK);
    writeln("But it should look like this:\n");
    setStyle(BOARD_INK);
    writeln("    A B C");
    writeln("    D E F");
    writefln("    G H %s\n", BLANK);
    setStyle(INSTRUCTIONS_INK);
    writefln("You may exchange any one letter with the '%s', but only one that's adjacent:", BLANK);
    writeln("above, below, left, or right.  Not all puzzles are possible, and you may enter");
    writefln("'%s' to give up.\n", QUIT_COMMAND);
    writeln("Here we go...");
    setStyle(DEFAULT_INK);
}

// Grids {{{1
// =============================================================

// Print the given player's grid title.

void printGridTitle(int player)
{
    import std.stdio : writef;

    setCursorPosition(GRIDS_ROW, GRIDS_COLUMN + (player * GRIDS_GAP));
    writef("Player %d", player + 1);
}

// Return the cursor position of the given player's grid cell.

int[] cellPosition(int player, int cell)
{
    int gridRow = cell / GRID_HEIGHT;
    int gridColumn = cell % GRID_WIDTH;
    int titleMargin = players > 1 ? 2 : 0;
    int row = GRIDS_ROW + titleMargin + gridRow;
    int column = GRIDS_COLUMN +
        (gridColumn * CELLS_GAP) +
        (player * GRIDS_GAP);
    return [row, column];
}

// Return the cursor position of the given player's grid prompt.

int[] gridPromptPositionOf(int player)
{
    int[] coord = cellPosition(player, CELLS);
    int row = coord[0];
    int column = coord[1];
    return [row + 1, column];
}

// Print the given player's grid, in the given or default color.

void printGrid(int player, int color = BOARD_INK)
{
    import std.stdio : write;

    if (players > 1)
    {
        printGridTitle(player);
    }
    setStyle(color);
    foreach (int cell; 0 .. CELLS)
    {
        setCursorCoord(cellPosition(player, cell));
        write(grid[player][cell]);
    }
    setStyle(DEFAULT_INK);
}

// Print the current players' grids.

void printGrids()
{
    import std.stdio : writeln;

    foreach (int player; 0 .. players)
    {
        if (isPlaying[player])
        {
            printGrid(player);
        }
    }
    writeln();
    eraseScreenToEnd();
}

// Init the grids.

void initGrids()
{
    import std.random : randomShuffle;

    grid[0] = pristineGrid.dup[0 .. $].randomShuffle();
    foreach (int player; 1 .. players)
    {
        grid[player] = grid[0].dup;
    }
}

// Messages {{{1
// =============================================================

// Return a message prefix for the given player.

string playerPrefix(int player)
{
    import std.format : format;

    return players > 1 ? format("Player %d: ", player + 1) : "";
}

// Return the cursor position of the given player's messages, adding the given
// row increment, which defaults to zero.

int[] messagePosition(int player, int rowInc = 0 )
{
    int[] promptCoord = gridPromptPositionOf(player);
    return [promptCoord[axis.Y] + 2 + rowInc, 1];
}

// Print the given message about the given player, adding the given row
// increment, which defaults to zero, to the default cursor coordinates.

void printMessage(string message, int player, int rowInc = 0)
{
    import std.stdio : writef;
    import std.stdio : writeln;

    setCursorCoord(messagePosition(player, rowInc));
    writef("%s%s", playerPrefix(player), message);
    eraseLineToEnd();
    writeln();
}

// Erase the last message about the given player.

void eraseMessage(int player)
{
    setCursorCoord(messagePosition(player));
    eraseLineToEnd();
}

// Game loop {{{1
// =============================================================

// Return a message with the players range.

string playersRangeMessage()
{
    import std.format : format;

    if (MAX_PLAYERS == 2)
    {
        return "1 or 2";
    }
    else
    {
        return format("from 1 to %d", MAX_PLAYERS);
    }
}

// Return the number of players, asking the user if needed.

int numberOfPlayers()
{
    import std.format : format;
    import std.stdio : writeln;

    int players = 0;
    printTitle();
    writeln();
    if (MAX_PLAYERS == 1)
    {
        players = 1;
    }
    else
    {
        while (players < 1 || players > MAX_PLAYERS)
        {
            string prompt = format("Number of players (%s): ", playersRangeMessage());
            players = getInteger(prompt);
        }
    }
    return players;
}

// Is the given cell the first one on a grid row?

bool isFirstCellOfGridRow(int cell)
{
    return cell % GRID_WIDTH == 0;
}

// Is the given cell the last one on a grid row?

bool isLastCellOfGridRow(int cell)
{
    return (cell + 1) % GRID_WIDTH == 0;
}

// Are the given cells adjacent?

bool areCellsAdjacent(int cell1, int cell2)
{
    return (cell2 == cell1 + 1 && !isFirstCellOfGridRow(cell2)) ||
        (cell2 == cell1 + GRID_WIDTH) ||
        (cell2 == cell1 - 1 && !isLastCellOfGridRow(cell2)) ||
        (cell2 == cell1 - GRID_WIDTH);
}

// If the given player's character cell is a valid move, i.e. it is adjacent to
// the blank cell, return the blank cell; otherwise return -1.

int positionToCell(int player, int charCell)
{
    import std.format : format;

    foreach (int cell; 0 .. CELLS)
    {
        if (grid[player][cell] == BLANK)
        {
            if (areCellsAdjacent(charCell, cell))
            {
                return cell;
            }
            else
            {
                break;
            }
        }
    }
    printMessage(format("Illegal move \"%s\".", grid[player][charCell]), player);
    return -1;
}

// If the given player's command is valid, i.e. a grid character, return its
// position; otherwise return -1.

int commandToPosition(int player, string command)
{
    import std.format : format;

    if (command != BLANK)
    {
        foreach (int position;  0 .. CELLS)
        {
            if (command == grid[player][position])
            {
                return position;
            }
        }
    }
    printMessage(format("Invalid character \"%s\".", command), player);
    return -1;
}

// Forget the given player, who quitted.

void forgetPlayer(int player)
{
    isPlaying[player] = false;
    printGrid(player, DEFAULT_INK);
}

// Play the turn of the given player.

void playTurn(int player)
{
    import std.uni : toUpper;

    int blankPosition = 0;
    int characterPosition = 0;

    if (isPlaying[player])
    {
        while (true)
        {
            while (true)
            {
                int[] coord = gridPromptPositionOf(player);
                setCursorCoord(coord);
                eraseLineToEnd();
                setCursorCoord(coord);
                string command = toUpper(getString("Move: "));
                if (command == QUIT_COMMAND)
                {
                    forgetPlayer(player);
                    return;
                }
                int position = commandToPosition(player, command);
                if (position >= 0)
                {
                    characterPosition = position;
                    break;
                }
            }
            int position = positionToCell(player, characterPosition);
            if (position >= 0)
            {
                blankPosition = position;
                break;
            }
        }
        eraseMessage(player);
        grid[player][blankPosition] = grid[player][characterPosition];
        grid[player][characterPosition] = BLANK;
    }
}

// Play the turns of all players.

void playTurns()
{
    foreach (player; 0 .. players)
    {
        playTurn(player);
    }
}

// Is someone playing?

bool isSomeonePlaying()
{
    foreach (int player; 0 .. players)
    {
        if (isPlaying[player])
        {
            return true;
        }
    }
    return false;
}

bool hasAnEmptyGrid(int player)
{
    foreach (int i; 0 .. CELLS)
    {
        if (grid[player][i] != "")
        {
            return false;
        }
    }
    return true;
}

// Has someone won? If so, print a message for every winner and return `true`
// otherwise just return `false`.

bool hasSomeoneWon()
{
    import std.format : format;

    int winners = 0;
    foreach (int player; 0 .. players)
    {
        if (isPlaying[player])
        {
            if (hasAnEmptyGrid(player))
            {
                winners += 1;
                if (winners > 0)
                {
                    printMessage(
                        format("You're the winner%s!", (winners > 1) ? ", too" : ""),
                        player,
                        winners - 1);
                }
            }
        }
    }
    return winners > 0;
}

// Init the game.

void initGame()
{
    clearScreen();
    players = numberOfPlayers();
    foreach (int player; 0 .. players)
    {
        isPlaying[player] = true;
    }
    clearScreen();
    printTitle();
    initGrids();
    printGrids();
}

// Play the game.

void play()
{
    initGame();
    while (isSomeonePlaying())
    {
        playTurns();
        printGrids();
        if (hasSomeoneWon())
        {
            break;
        }
    }
}

// Main {{{1
// =============================================================

void initOnce()
{
    import std.conv : to;
    import std.format : format;

    // Init the pristine grid.
    enum FIRST_CHAR_CODE = 'A';
    foreach (int cell; 0 .. CELLS - 1)
    {
        pristineGrid[cell] = format("%s", to!char(FIRST_CHAR_CODE + cell));
    }
    pristineGrid[CELLS - 1] = BLANK;
}

// Return `true` if the player does not want to play another game; otherwise
// return `false`.

bool enough()
{
    setCursorCoord(gridPromptPositionOf(FIRST_PLAYER));
    return !yes("Another game? ");
}

void main()
{
    import std.stdio : writeln;

    initOnce();

    clearScreen();
    printCredits();
    pressEnter("\nPress the Enter key to read the instructions. ");

    clearScreen();
    printInstructions();
    pressEnter("\nPress the Enter key to start. ");

    while (true)
    {
        play();
        if (enough())
        {
            break;
        }
    }
    writeln("So long…");
}

In Go

/*
Xchange

Original version in BASIC:
    Written by Thomas C. McIntire, 1979.
    Published in "The A to Z Book of Computer Games", 1979.
    https://archive.org/details/The_A_to_Z_Book_of_Computer_Games/page/n269/mode/2up
    https://github.com/chaosotter/basic-games

This improved remake in Go:
    Copyright (c) 2025, Marcos Cruz (programandala.net)
    SPDX-License-Identifier: Fair

Written on 2025-01-12.

Last modified: 20250112T0212+0100.
*/

package main

import "fmt"
import "math/rand"
import "strconv"
import "strings"
import "time"

// Console {{{1
// =============================================================

const blackColor = 0
const redColor = 1
const greenColor = 2
const yellowColor = 3
const blueColor = 4
const magentaColor = 5
const cyanColor = 6
const whiteColor = 7
const defaultColor = 9

const normalAttributes = 0

const foreground = +30
const background = +40
const bright = +60

func clearScreen() {
    fmt.Print("\x1B[0;0H\x1B[2J")
}

func moveCursorHome() {
    fmt.Print("\x1B[H")
}

func eraseLineRight() {
    fmt.Print("\x1B[K")
}

func eraseScreenDown() {
    fmt.Print("\x1B[J")
}

func hideCursor() {
    fmt.Print("\x1B[?25l")
}

func showCursor() {
    fmt.Print("\x1B[?25h")
}

func setColor(color int) {
    fmt.Printf("\x1B[%vm", color)
}

func setCursorPosition(line, column int) {
    fmt.Printf("\x1B[%v;%vH", line, column)
}

// Globals {{{1
// =============================================================

const boardInk int = foreground + bright + cyanColor
const defaultInk int = foreground + whiteColor
const inputInk int = foreground + bright + greenColor
const instructionsInk int = foreground + yellowColor
const titleInk int = foreground + bright + redColor

const blank string = "*"

const gridHeight int = 3 // cell rows
const gridWidth int = 3  // cell columns

const cells int = gridWidth * gridHeight

var pristineGrid = [cells]string{}

const gridsRow int = 3    // screen row where the grids are printed
const gridsColumn int = 5 // screen column where the left grid is printed
const cellsGap int = 2    // distance between the grid cells, in screen rows or columns
const gridsGap int = 16   // screen columns between equivalent cells of the grids

const maxPlayers int = 4

var grid = [maxPlayers][cells]string{}

var isPlaying = [maxPlayers]bool{}

var players int

const quitCommand string = "X"

// User input {{{1
// =============================================================

// Print the given prompt and wait until the user enters a string.
func inputString(prompt string) string {

    setColor(inputInk)
    defer setColor(defaultInk)
    fmt.Print(prompt)
    var s = ""
    fmt.Scanf("%s", &s)
    return s

}

// Print the given prompt and wait until the user enters an integer.
func inputInt(prompt string) int {

    var number int64
    var err error
    for {
        number, err = strconv.ParseInt(inputString(prompt), 10, 0)
        if err == nil {
            break
        } else {
            fmt.Println("Integer expected.")
        }
    }
    return int(number)

}

// Print the given prompt and wait until the user presses Enter.
func pressEnter(prompt string) {

    inputString(prompt)

}

// Return `true` if the given string is "yes" or a synonym.
func isYes(s string) bool {

    switch strings.ToLower(strings.TrimSpace(s)) {
    case "ok", "yeah", "yes", "y":
        return true
    default:
        return false
    }

}

// Return `true` if the given string is "no" or a synonym.
func isNo(s string) bool {

    switch strings.ToLower(strings.TrimSpace(s)) {
    case "n", "no", "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".
func yes(prompt string) bool {

    for {
        var answer = inputString(prompt)
        if isYes(answer) {
            return true
        }
        if isNo(answer) {
            return false
        }
    }

}

// Title, instructions and credits {{{1
// =============================================================

// Print the title at the current cursor position.
func printTitle() {

    setColor(titleInk)
    fmt.Println("Xchange")
    setColor(defaultInk)

}

// Print the credits at the current cursor position.
func printCredits() {

    printTitle()
    fmt.Println("\nOriginal version in BASIC:")
    fmt.Println("    Written by Thomas C. McIntire, 1979.")
    fmt.Println("    Published in \"The A to Z Book of Computer Games\", 1979.")
    fmt.Println("    https://archive.org/details/The_A_to_Z_Book_of_Computer_Games/page/n269/mode/2up")
    fmt.Println("    https://github.com/chaosotter/basic-games")
    fmt.Println("This improved remake in Go:")
    fmt.Println("    Copyright (c) 2025, Marcos Cruz (programandala.net)")
    fmt.Println("    SPDX-License-Identifier: Fair")

}

// Print the instructions at the current cursor position.
func printInstructions() {

    printTitle()
    setColor(instructionsInk)
    defer setColor(defaultInk)
    fmt.Println("\nOne or two may play.  If two, you take turns.  A grid looks like this:\n")
    setColor(boardInk)
    fmt.Println("    F G D")
    fmt.Printf("    A H %s\n", blank)
    fmt.Println("    E B C\n")
    setColor(instructionsInk)
    fmt.Println("But it should look like this:\n")
    setColor(boardInk)
    fmt.Println("    A B C")
    fmt.Println("    D E F")
    fmt.Printf("    G H %s\n\n", blank)
    setColor(instructionsInk)
    fmt.Printf("You may exchange any one letter with the '%s', but only one that's adjacent:\n", blank)
    fmt.Println("above, below, left, or right.  Not all puzzles are possible, and you may enter")
    fmt.Printf("'%s' to give up.\n\n", quitCommand)
    fmt.Println("Here we go...")

}

// Grids {{{1
// =============================================================

// Print the given player's grid title.
func printGridTitle(player int) {

    setCursorPosition(gridsRow, gridsColumn+(player*gridsGap))
    fmt.Printf("Player %v", player+1)

}

// Return the cursor position of the given player's grid cell.
func cellPosition(player, cell int) (row, column int) {

    var gridRow int = cell / gridHeight
    var gridColumn int = cell % gridWidth
    var titleMargin int = 0
    if players > 1 {
        titleMargin = 2
    }
    return gridsRow + titleMargin + gridRow,
        gridsColumn + (gridColumn * cellsGap) + (player * gridsGap)

}

// Return the cursor position of the given player's grid prompt.
func gridPromptPosition(player int) (row, column int) {

    var gridRow int = cells / gridHeight
    var gridColumn int = cells % gridWidth
    var titleMargin = 0
    if players > 1 {
        titleMargin = 2
    }
    return gridsRow + titleMargin + gridRow + 1,
        gridsColumn + (gridColumn * cellsGap) + (player * gridsGap)

}

// Print the given player's grid, in the given or default color.
func printGrid(player int, color int) {

    if players > 1 {
        printGridTitle(player)
    }
    setColor(color)
    defer setColor(defaultInk)
    for cell := 0; cell < cells; cell++ {
        setCursorPosition(cellPosition(player, cell))
        fmt.Print(grid[player][cell])
    }

}

// Print the current players' grids.
func printGrids() {

    for player := 0; player < players; player++ {
        if isPlaying[player] {
            printGrid(player, boardInk)
        }
    }
    fmt.Println()
    eraseScreenDown()

}

// Scramble the grid of the given player.
func scrambleGrid(player int) {

    for cell := 0; cell < cells; cell++ {
        var randomCell = rand.Intn(cells)
        // Exchange the contents of the current cell with that of the random one.
        grid[player][cell], grid[player][randomCell] =
            grid[player][randomCell], grid[player][cell]
    }

}

// Init the grids.
func initGrids() {

    grid[0] = pristineGrid
    scrambleGrid(0)
    for player := 0 + 1; player < players; player++ {
        grid[player] = grid[0]
    }

}

// Messages {{{1
// =============================================================

// Return a message prefix for the given player.
func playerPrefix(player int) string {

    if players > 1 {
        return fmt.Sprintf("Player %i: ", player+1)
    } else {
        return ""
    }

}

// Return the cursor position of the given player's messages, adding the given
// row increment, which defaults to zero.
func messagePosition(player int, rowInc int) (row, column int) {

    promptRow, _ := gridPromptPosition(player)
    return promptRow + 2 + rowInc, 1

}

// Print the given message about the given player, adding the given row
// increment, which defaults to zero, to the default cursor coordinates.
func printMessage(message string, player int, rowInc int) {

    setCursorPosition(messagePosition(player, rowInc))
    fmt.Printf("%s%s", playerPrefix(player), message)
    eraseLineRight()
    fmt.Println()

}

// Erase the last message about the given player.
func eraseMessage(player int) {

    setCursorPosition(messagePosition(player, 0))
    eraseLineRight()

}

// Game loop {{{1
// =============================================================

// Return a message with the players range.
func playersRangeMessage() string {

    if maxPlayers == 2 {
        return "1 or 2"
    } else {
        return fmt.Sprintf("from 1 to %v", maxPlayers)
    }

}

// Return the number of players, asking the user if needed.
func numberOfPlayers() int {

    var players = 0
    printTitle()
    fmt.Println()
    if maxPlayers == 1 {
        players = 1
    } else {
        for players < 1 || players > maxPlayers {
            players = inputInt(fmt.Sprintf("Number of players (%s): ", playersRangeMessage()))
        }
    }
    return players

}

// Is the given cell the first one on a grid row?
func isFirstCellOfA_gridRow(cell int) bool {

    return cell%gridWidth == 0

}

// Is the given cell the last one on a grid row?
func isLastCellOfA_gridRow(cell int) bool {

    return (cell+1)%gridWidth == 0

}

// Are the given cells adjacent?
func areCellsAdjacent(cell_1, cell_2 int) bool {

    switch {
    case cell_2 == cell_1+1 && !isFirstCellOfA_gridRow(cell_2):
        return true
    case cell_2 == cell_1+gridWidth:
        return true
    case cell_2 == cell_1-1 && !isLastCellOfA_gridRow(cell_2):
        return true
    case cell_2 == cell_1-gridWidth:
        return true
    }
    return false

}

// Is the given player's character cell a valid move, i.E. is it adjacent to
// the blank cell? If so, return the blank cell of the grid and `true`;
// otherwise return a fake cell and `false`.
func isLegalMove(player, charCell int) (blankCell int, ok bool) {

    const nowhere = -1
    for blankCell, cellContent := range grid[player] {
        if cellContent == blank {
            if areCellsAdjacent(charCell, blankCell) {
                return blankCell, true
            }
            break
        }
    }
    printMessage(fmt.Sprintf("Illegal move \"%s\".", grid[player][charCell]), player, 0)
    return nowhere, false

}

// Is the given player's command valid, i.E. a grid character? If so, return
// its position in the grid and `true`; otherwise print an error message and
// return a fake position and `false`.
func isValidCommand(player int, command string) (position int, ok bool) {

    const nowhere = -1
    if command != blank {
        for position, cellContent := range grid[player] {
            if cellContent == command {
                return position, true
            }
        }
    }
    printMessage(fmt.Sprintf("Invalid character \"%s\".", command), player, 0)
    return nowhere, false

}

// Forget the given player, who quitted.
func forgetPlayer(player int) {

    isPlaying[player] = false
    printGrid(player, defaultInk)

}

// Play the turn of the given player.
func playTurn(player int) {

    var blankPosition int
    var characterPosition int

    if isPlaying[player] {

        for {
            var ok bool
            var command string
            for ok = false; !ok; characterPosition, ok = isValidCommand(player, command) {
                row, column := gridPromptPosition(player)
                setCursorPosition(row, column)
                eraseLineRight()
                setCursorPosition(row, column)
                command = strings.ToUpper(strings.TrimSpace(inputString("Move: ")))
                if command == quitCommand {
                    forgetPlayer(player)
                    return
                }
            }
            blankPosition, ok = isLegalMove(player, characterPosition)
            if ok {
                break
            }
        }
        eraseMessage(player)
        grid[player][blankPosition] = grid[player][characterPosition]
        grid[player][characterPosition] = blank

    }

}

// Play the turns of all players.
func playTurns() {

    for player := 0; player < players; player++ {
        playTurn(player)
    }

}

// Is someone playing?
func isSomeonePlaying() bool {

    for player := 0; player < players; player++ {
        if isPlaying[player] {
            return true
        }
    }
    return false

}

// Has someone won? If so, print a message for every winner and return `true`;
// otherwise just return `false`.
func hasSomeoneWon() bool {

    var winners = 0
    for player := 0; player < players; player++ {
        if isPlaying[player] {
            if grid[player] == pristineGrid {
                winners += 1
                if winners > 0 {
                    var winner = "winner"
                    if winners > 1 {
                        winner = "winner, too"
                    }
                    printMessage(
                        fmt.Sprintf("You're the %s!", winner),
                        player,
                        winners-1)
                }
            }
        }
    }
    return winners > 0

}

// Init the game.
func initGame() {

    clearScreen()
    players = numberOfPlayers()
    for player := 0; player < players; player++ {
        isPlaying[player] = true
    }
    clearScreen()
    printTitle()
    initGrids()
    printGrids()

}

// Play the game.
func play() {

    initGame()
    for isSomeonePlaying() {
        playTurns()
        printGrids()
        if hasSomeoneWon() {
            break
        }
    }

}

// Main {{{1
// =============================================================

// Init the program, i.E. just once before the first game.
func initOnce() {

    rand.Seed(time.Now().UTC().UnixNano())

    // Init the pristine grid.
    const firstCharCode = int('A')
    for cell := 0; cell < cells-1; cell++ {
        pristineGrid[cell] = string(rune(firstCharCode + cell))
    }
    pristineGrid[cells-1] = blank

}

// Return `true` if the player does not want to play another game; otherwise
// return `false`.
func enough() bool {

    setCursorPosition(gridPromptPosition(0))
    return !yes("Another game? ")

}

func main() {

    initOnce()

    clearScreen()
    printCredits()

    pressEnter("\nPress the Enter key to read the instructions. ")
    clearScreen()
    printInstructions()

    pressEnter("\nPress the Enter key to start. ")
    for {
        play()
        if enough() {
            break
        }
    }
    fmt.Println("So long…")

}

In Hare

// Xchange
//
// Original version in BASIC:
//      Written by Thomas C. McIntire, 1979.
//      Published in "The A to Z Book of Computer Games", 1979.
//      https://archive.org/details/The_A_to_Z_Book_of_Computer_Games/page/n269/mode/2up
//      https://github.com/chaosotter/basic-games
//
// This improved remake in Hare:
//      Copyright (c) 2025, Marcos Cruz (programandala.net)
//      SPDX-License-Identifier: Fair
//
// Written in 2025-02-17/18.
//
// Last modified: 20260213T1645+0100.

// Modules {{{1
// =============================================================================

use ascii;
use bufio;
use fmt;
use math::random;
use os;
use strconv;
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 set_cursor_coord(coord: (int, int)) void = {
        const (line, column) = coord;
        set_cursor_position(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_to_end() void = {
        fmt::print("\x1B[J")!;
};

fn erase_screen() void = {
        fmt::print("\x1B[2J")!;
};

fn clear_screen() void = {
        erase_screen();
        reset_attributes();
        move_cursor_home();
};

// Data {{{1
// =============================================================

def BOARD_INK = FOREGROUND + BRIGHT + CYAN;
def DEFAULT_INK = FOREGROUND + WHITE;
def INPUT_INK = FOREGROUND + BRIGHT + GREEN;
def INSTRUCTIONS_INK = FOREGROUND + YELLOW;
def TITLE_INK = FOREGROUND + BRIGHT + RED;

def BLANK = "*";

def GRID_HEIGHT = 3; // cell rows
def GRID_WIDTH = 3; // cell columns

def CELLS = GRID_WIDTH * GRID_HEIGHT;

const pristine_grid: [CELLS]str = [""...];

def GRIDS_ROW = 3; // screen row where the grids are printed
def GRIDS_COLUMN = 5; // screen column where the left grid is printed
def CELLS_GAP = 2; // distance between the grid cells, in screen rows or columns
def GRIDS_GAP = 16; // screen columns between equivalent cells of the grids

def FIRST_PLAYER = 0;
def MAX_PLAYERS = 4;

let grid: [MAX_PLAYERS][CELLS]str = [[""...]...];

let is_playing: [MAX_PLAYERS]bool = [false...];

let players: int = 0;

def QUIT_COMMAND = "X";

// 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 accept_integer() (int | ...strconv::error) = {
        const s = accept_string();
        defer free(s);
        return strconv::stoi(s);
};

fn prompted_integer(prompt: str) (int | ...strconv::error) = {
        fmt::print(prompt)!;
        bufio::flush(os::stdout)!;
        return accept_integer();
};

fn prompted_integer_or_0(prompt: str) int = {
        match (prompted_integer(prompt)) {
        case let i: int =>
                return i;
        case =>
                return 0;
        };
};

fn get_integer(prompt: str = "") int = {
        set_style(INPUT_INK);
        defer set_style(DEFAULT_INK);
        return prompted_integer_or_0(prompt);
};

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);
                if (is_yes(answer)) {
                        return true;
                };
                if (is_no(answer)) {
                        return false;
                };
        };
};

// Title, instructions and credits {{{1
// =============================================================

fn print_title() void = {
        set_style(TITLE_INK);
        fmt::println("Xchange")!;
        set_style(DEFAULT_INK);
};

fn print_credits() void = {
        print_title();
        fmt::println("\nOriginal version in BASIC:")!;
        fmt::println("    Written by Thomas C. McIntire, 1979.")!;
        fmt::println("    Published in \"The A to Z Book of Computer Games\", 1979.")!;
        fmt::println("    https://archive.org/details/The_A_to_Z_Book_of_Computer_Games/page/n269/mode/2up")!;
        fmt::println("    https://github.com/chaosotter/basic-games")!;
        fmt::println("This improved remake in Hare:")!;
        fmt::println("    Copyright (c) 2025, Marcos Cruz (programandala.net)")!;
        fmt::println("    SPDX-License-Identifier: Fair")!;
};

fn print_instructions() void = {
        print_title();
        set_style(INSTRUCTIONS_INK);
        defer set_style(DEFAULT_INK);
        fmt::println("\nOne or two may play.  If two, you take turns.  A grid looks like this:\n")!;
        set_style(BOARD_INK);
        fmt::println("    F G D")!;
        fmt::printfln("    A H {}", BLANK)!;
        fmt::println("    E B C\n")!;
        set_style(INSTRUCTIONS_INK);
        fmt::println("But it should look like this:\n")!;
        set_style(BOARD_INK);
        fmt::println("    A B C")!;
        fmt::println("    D E F")!;
        fmt::printfln("    G H {}\n", BLANK)!;
        set_style(INSTRUCTIONS_INK);
        fmt::printfln("You may exchange any one letter with the '{}', but only one that's adjacent:", BLANK)!;
        fmt::println("above, below, left, or right.  Not all puzzles are possible, and you may enter")!;
        fmt::printfln("'{}' to give up.\n", QUIT_COMMAND)!;
        fmt::println("Here we go...")!;
};

// Grids {{{1
// =============================================================

// Print the given player's grid title.
//
fn print_grid_title(player: int) void = {
        set_cursor_position(GRIDS_ROW, GRIDS_COLUMN + (player * GRIDS_GAP));
        fmt::printf("Player {}", player + 1)!;
};

// Return the cursor position of the given player's grid cell.
//
fn cell_position(player: int, cell: int) (int, int) = {
        const grid_row: int = cell / GRID_HEIGHT;
        const grid_column: int = cell % GRID_WIDTH;
        const title_margin: int = if (players > 1) 2 else 0;
        const row: int = GRIDS_ROW + title_margin + grid_row;
        const column: int = GRIDS_COLUMN +
                (grid_column * CELLS_GAP) +
                (player * GRIDS_GAP);
        return (row, column);
};

// Return the cursor position of the given player's grid prompt.
//
fn grid_prompt_position_of(player: int) (int, int) = {
        const coord = cell_position(player, CELLS);
        const row = coord.0;
        const column = coord.1;
        return (row + 1, column);
};

// Print the given player's grid, in the given or default color.
//
fn print_grid(player: int, color: int = BOARD_INK) void = {
        if (players > 1) {
                print_grid_title(player);
        };
        set_style(color);
        defer set_style(DEFAULT_INK);
        for (let cell = 0; cell < CELLS; cell += 1) {
                set_cursor_coord(cell_position(player, cell));
                fmt::print(grid[player][cell])!;
        };
};

// Print the current players' grids.
//
fn print_grids() void = {
        for (let player = 0; player < players; player += 1) {
                if (is_playing[player]) {
                        print_grid(player);
                };
        };
        fmt::println()!;
        erase_screen_to_end();
};

// Scramble the grid of the given player.
//
fn scramble_grid(player: int) void = {
        for (let cell = 0; cell < CELLS; cell += 1) {
                const random_cell = random::u64n(&rand, CELLS);
                // Exchange the contents of the current cell with that of the random one.
                const tmp = grid[player][cell];
                grid[player][cell] = grid[player][random_cell];
                grid[player][random_cell] = tmp;
        };
};

// Init the grids.
//
fn init_grids() void = {
        grid[0] = pristine_grid;
        scramble_grid(0);
        for (let player = 1; player < players; player += 1) {
                grid[player] = grid[0];
        };
};

// Messages {{{1
// =============================================================

// Return a message prefix for the given player.
//
fn player_prefix(player: int) str = {
        return if (players > 1) fmt::asprintf("Player {}: ", player + 1)! else "";
};

// Return the cursor position of the given player's messages, adding the given
// row increment, which defaults to zero.
//
fn message_position(player: int, row_inc: int = 0) (int, int) = {
        let (prompt_row, _) = grid_prompt_position_of(player);
        return (prompt_row + 2 + row_inc, 1);
};

// Print the given message about the given player, adding the given row
// increment, which defaults to zero, to the default cursor coordinates.
//
fn print_message(message: str, player: int, row_inc: int = 0) void = {
        set_cursor_coord(message_position(player, row_inc));
        fmt::printf("{}{}", player_prefix(player), message)!;
        erase_line_to_end();
        fmt::println()!;
};

// Erase the last message about the given player.
//
fn erase_message(player: int) void = {
        set_cursor_coord(message_position(player));
        erase_line_to_end();
};

// Game loop {{{1
// =============================================================

// Return a message with the players range.
//
fn players_range_message() str = {
        if (MAX_PLAYERS == 2) {
                return "1 or 2";
        } else {
                return fmt::asprintf("from 1 to {}", MAX_PLAYERS)!;
        };
};

// Return the number of players, asking the user if needed.
//
fn number_of_players() int = {
        let players = 0;
        print_title();
        fmt::println()!;
        if (MAX_PLAYERS == 1) {
                players = 1;
        } else {
                for (players < 1 || players > MAX_PLAYERS) {
                        const prompt = fmt::asprintf("Number of players ({}): ", players_range_message())!;
                        defer free(prompt);
                        players = get_integer(prompt);
                };
        };
        return players;
};

// Is the given cell the first one on a grid row?
//
fn is_first_cell_of_a_grid_row(cell: int) bool = {
        return cell % GRID_WIDTH == 0;
};

// Is the given cell the last one on a grid row?
//
fn is_last_cell_of_a_grid_row(cell: int) bool = {
        return (cell + 1) % GRID_WIDTH == 0;
};

// Are the given cells adjacent?
//
fn are_cells_adjacent(cell_1: int, cell_2: int) bool = {
        return (cell_2 == cell_1 + 1 && !is_first_cell_of_a_grid_row(cell_2)) ||
                (cell_2 == cell_1 + GRID_WIDTH) ||
                (cell_2 == cell_1 - 1 && !is_last_cell_of_a_grid_row(cell_2)) ||
                (cell_2 == cell_1 - GRID_WIDTH);
};

type invalid = !void;

// If the given player's character cell is a valid move, i.e. it is adjacent to
// the blank cell, return the blank cell; otherwise return the `invalid` type
// error.
//
fn position_to_cell(player: int, char_cell: int) (int | invalid) = {
        for (let cell = 0; cell < CELLS; cell += 1) {
                if (grid[player][cell] == BLANK) {
                        if (are_cells_adjacent(char_cell, cell)) {
                                return cell;
                        } else {
                                break;
                        };
                };
        };
        print_message(fmt::asprintf("Illegal move \"{}\".", grid[player][char_cell])!, player);
        return invalid;
};

// If the given player's command is valid, i.e. a grid character, return its
// position; otherwise return the `invalid` error type.
//
fn command_to_position(player: int, command: str) (int | invalid) = {
        if (command != BLANK) {
                for (let position = 0; position < CELLS; position += 1) {
                        if (command == grid[player][position]) {
                                return position;
                        };
                };
        };
        print_message(fmt::asprintf("Invalid character \"{}\".", command)!, player);
        return invalid;
};

// Forget the given player, who quitted.
//
fn forget_player(player: int) void = {
        is_playing[player] = false;
        print_grid(player, DEFAULT_INK);
};

// Play the turn of the given player.
//
fn play_turn(player: int) void = {
        let blank_position: int = 0;
        let character_position: int = 0;

        if (is_playing[player]) {

                for (true) {
                        for (true) {
                                const coord = grid_prompt_position_of(player);
                                set_cursor_coord(coord);
                                erase_line_to_end();
                                set_cursor_coord(coord);
                                const command = ascii::strupper(strings::trim(get_string("Move: ")))!;
                                defer free(command);
                                if (command == QUIT_COMMAND) {
                                        forget_player(player);
                                        return;
                                };
                                match (command_to_position(player, command)) {
                                case let position: int =>
                                        character_position = position;
                                        break;
                                case =>
                                        void;
                                };
                        };
                        match (position_to_cell(player, character_position)) {
                        case let position: int =>
                                blank_position = position;
                                break;
                        case =>
                                void;
                        };
                };
                erase_message(player);
                grid[player][blank_position] = grid[player][character_position];
                grid[player][character_position] = BLANK;

        };
};

// Play the turns of all players.
//
fn play_turns() void = {
        for (let player = 0; player < players; player += 1) {
                play_turn(player);
        };
};

// Is someone playing?
//
fn is_someone_playing() bool = {
        for (let player = 0; player < players; player += 1) {
                if (is_playing[player]) {
                        return true;
                };
        };
        return false;
};

fn has_an_empty_grid(player: int) bool = {
        for (let i = 0; i < CELLS; i += 1) {
                if (grid[player][i] != "") {
                        return false;
                };
        };
        return true;
};

// Has someone won? If so, print a message for every winner and return `true`
// otherwise just return `false`.
//
fn has_someone_won() bool = {
        let winners = 0;
        for (let player = 0; player < players; player += 1) {
                if (is_playing[player]) {
                        if (has_an_empty_grid(player)) {
                                winners += 1;
                                if (winners > 0) {
                                        print_message(
                                                fmt::asprintf("You're the winner{}!", if (winners > 1) ", too" else "")!,
                                                player,
                                                winners - 1);
                                };
                        };
                };
        };
        return winners > 0;
};

// Init the game.
//
fn init_game() void = {
        clear_screen();
        players = number_of_players();
        for (let player = 0; player < players; player += 1) {
                is_playing[player] = true;
        };
        clear_screen();
        print_title();
        init_grids();
        print_grids();
};

// Play the game.
//
fn play() void = {
        init_game();
        for (is_someone_playing()) {
                play_turns();
                print_grids();
                if (has_someone_won()) {
                        break;
                };
        };
};

// Main {{{1
// =============================================================

let rand: random::random = 0;

fn randomize() void = {
        rand = random::init(time::now(time::clock::MONOTONIC).sec: u64);
};

// Init the program, i.e. just once before the first game.
//
fn init_once() void = {
        randomize();

        // Init the pristine grid.
        def FIRST_CHAR_CODE = 'A': int;
        for (let cell = 0; cell < CELLS - 1; cell += 1) {
                pristine_grid[cell] = strings::fromrunes([(FIRST_CHAR_CODE + cell): rune])!;
        };
        pristine_grid[CELLS - 1] = BLANK;
};

// Return `true` if the player does not want to play another game; otherwise
// return `false`.
//
fn enough() bool = {
        set_cursor_coord(grid_prompt_position_of(FIRST_PLAYER));
        return !yes("Another game? ");
};

export fn main() void = {
        init_once();

        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. ");

        for (true) {
                play();
                if (enough()) {
                        break;
                };
        };
        fmt::println("So long…")!;
};

In Kotlin

/*
Xchange

Original version in BASIC:
    Written by Thomas C. McIntire, 1979.
    Published in "The A to Z Book of Computer Games", 1979.
    https://archive.org/details/The_A_to_Z_Book_of_Computer_Games/page/n269/mode/2up
    https://github.com/chaosotter/basic-games

This improved remake in Kotlin:
    Copyright (c) 2025, Marcos Cruz (programandala.net)
    SPDX-License-Identifier: Fair

Written on 2025-05-18, 2025-08-06.

Last modified: 20260205T1353+0100.
*/

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")
}

// Erase the screen from the current line down to the bottom of the screen.
//
fun eraseScreenDown() {
    print("\u001B[J")
}

// Globals {{{1
// =============================================================

const val BOARD_INK = BRIGHT + CYAN + FOREGROUND
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

const val BLANK = "*"

const val GRID_HEIGHT = 3 // cell rows
const val GRID_WIDTH = 3 // cell columns

const val CELLS = GRID_WIDTH * GRID_HEIGHT

var pristineGrid = Array(CELLS) { "" }

const val GRIDS_ROW = 3 // screen row where the grids are printed
const val GRIDS_COLUMN = 5 // screen column where the left grid is printed
const val CELLS_GAP = 2 // distance between the grid cells, in screen rows or columns
const val GRIDS_GAP = 16 // screen columns between equivalent cells of the grids

const val MAX_PLAYERS = 4

var grid = Array(MAX_PLAYERS) { Array(CELLS) { "" } }

var isPlaying = BooleanArray(MAX_PLAYERS)

var players: Int = 0

const val QUIT_COMMAND = "X"

// User input {{{1
// =============================================================

// Print the given prompt and wait until the user enters an integer.
//
fun inputInt(prompt: String = ""): Int {
    setColor(INPUT_INK)
    var number: Int = 0
    while (true) {
        try {
            print(prompt)
            number = readln().toInt()
            break
        }
        catch (e: Exception) { }
    }
    setColor(DEFAULT_INK)
    return number
}

// Print the given prompt and wait until the user enters a string.
//
fun inputString(prompt: String = ""): String {
    setColor(INPUT_INK)
    print(prompt)
    val result: String = 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, instructions and credits {{{1
// =============================================================

// Print the title at the current cursor position.
//
fun printTitle() {
    setColor(TITLE_INK)
    println("Xchange")
    setColor(DEFAULT_INK)
}

// Print the credits at the current cursor position.
//
fun printCredits() {
    printTitle()
    println("\nOriginal version in BASIC:")
    println("    Written by Thomas C. McIntire, 1979.")
    println("    Published in \"The A to Z Book of Computer Games\", 1979.")
    println("    https://archive.org/details/The_A_to_Z_BookOf_Computer_Games/page/n269/mode/2up")
    println("    https://github.com/chaosotter/basic-games")
    println("This improved remake in Kotlin:")
    println("    Copyright (c) 2025, Marcos Cruz (programandala.net)")
    println("    SPDX-License-Identifier: Fair")
}

// Print the instructions at the current cursor position.
//
fun printInstructions() {
    printTitle()
    setColor(INSTRUCTIONS_INK)
    println("\nOne or two may play.  If two, you take turns.  A grid looks like this:\n")
    setColor(BOARD_INK)
    println("    F G D")
    println("    A H $BLANK")
    println("    E B C\n")
    setColor(INSTRUCTIONS_INK)
    println("But it should look like this:\n")
    setColor(BOARD_INK)
    println("    A B C")
    println("    D E F")
    println("    G H $BLANK\n")
    setColor(INSTRUCTIONS_INK)
    println("You may exchange any one letter with the '$BLANK', but only one that's adjacent:")
    println("above, below, left, or right.  Not all puzzles are possible, and you may enter")
    println("'$QUIT_COMMAND' to give up.\n")
    println("Here we go...")
    setColor(DEFAULT_INK)
}

// Grids {{{1
// =============================================================

// Print the given player's grid title.
//
fun printGridTitle(player: Int) {
    setCursorPosition(GRIDS_ROW, GRIDS_COLUMN + (player * GRIDS_GAP))
    print("Player ${player + 1 }")
}

// Return the cursor position of the given player's grid cell.
//
fun cellPosition(player: Int, cell: Int): Pair<Int, Int> {
    val gridRow: Int = cell / GRID_HEIGHT
    val gridColumn: Int = cell % GRID_WIDTH
    val titleMargin = if (players > 1) 2 else 0
    return Pair(
        GRIDS_ROW + titleMargin + gridRow,
        GRIDS_COLUMN + (gridColumn * CELLS_GAP) + (player * GRIDS_GAP)
    )
}

// Return the cursor position of the given player's grid prompt.
//
fun gridPromptPosition(player: Int): Pair<Int, Int> {
    val gridRow: Int = CELLS / GRID_HEIGHT
    val gridColumn: Int = CELLS % GRID_WIDTH
    val titleMargin = if (players > 1) 2 else 0
    return Pair(
        GRIDS_ROW + titleMargin + gridRow + 1,
        GRIDS_COLUMN + (gridColumn * CELLS_GAP) + (player * GRIDS_GAP)
    )
}

// Print the given player's grid, in the given or default color.
//
fun printGrid(player: Int, color: Int = BOARD_INK) {
    if (players > 1) printGridTitle(player)
    setColor(color)
    for (cell in 0 .. CELLS - 1) {
        val (row, column) = cellPosition(player, cell)
        setCursorPosition(row, column)
        print(grid[player][cell])
    }
    setColor(DEFAULT_INK)
}

// Print the current players' grids.
//
fun printGrids() {
    for (player in 0 .. players - 1) {
        if (isPlaying[player]) printGrid(player)
    }
    println()
    eraseScreenDown()
}

// Scramble the grid of the given player.
//
fun scrambleGrid(player: Int) {
    for (cell in 0 .. CELLS - 1) {
        val randomCell = Random.nextInt(CELLS)
        // Exchange the contents of the current cell with that of the random one.
        grid[player][cell] = grid[player][randomCell].also {
            grid[player][randomCell] = grid[player][cell]
        }
    }
}

fun initGrids() {
    grid[0] = pristineGrid.copyOf()
    scrambleGrid(0)
    for (player in 0 + 1 .. players - 1) {
        grid[player] = grid[0].copyOf()
    }
}

// Messages {{{1
// =============================================================

// Return a message prefix for the given player.
//
fun playerPrefix(player: Int): String {
    return if (players > 1) "Player " + player + 1 + ": " else ""
}

// Return the cursor position of the given player's messages, adding the given
// row increment, which defaults to zero.
//
fun messagePosition(player: Int, rowInc: Int = 0): Pair<Int, Int> {
    val (promptRow, prompColumn) = gridPromptPosition(player)
    return Pair(promptRow + 2 + rowInc, 1)
}

// Print the given message about the given player, adding the given row
// increment, which defaults to zero, to the default cursor coordinates.
//
fun printMessage(message: String, player: Int, rowInc: Int  = 0) {
    val (row, column) = messagePosition(player, rowInc)
    setCursorPosition(row, column)
    print("${playerPrefix(player)}${message}")
    eraseLineRight()
    println()
}

// Erase the last message about the given player.
//
fun eraseMessage(player: Int) {
    val (row, column) = messagePosition(player)
    setCursorPosition(row, column)
    eraseLineRight()
}

// Game loop {{{1
// =============================================================

// Return a message with the players range.
//
fun playersRangeMessage(): String {
    if (MAX_PLAYERS == 2) {
        return "1 or 2"
    } else {
        return "from 1 to " + MAX_PLAYERS
    }
}

// Return the number of players, asking the user if needed.
//
fun numberOfPlayers(): Int {
    var players: Int = 0
    printTitle()
    println()
    if (MAX_PLAYERS == 1) {
        players = 1
    } else {
        while (!(players in 1 .. MAX_PLAYERS)) {
            players = inputInt("Number of players (" + playersRangeMessage() + "): ")
        }
    }
    return players
}

fun isFirstCellOfGridRow(cell: Int): Boolean {
    return cell % GRID_WIDTH == 0
}

fun isLastCellOfGridRow(cell: Int): Boolean {
    return (cell + 1) % GRID_WIDTH == 0
}

fun areCellsAdjacent(cell_1: Int, cell_2: Int): Boolean {
    when {
        cell_2 == cell_1 + 1 && !isFirstCellOfGridRow(cell_2) ->
            return true
        cell_2 == cell_1 + GRID_WIDTH ->
            return true
        cell_2 == cell_1 - 1 && !isLastCellOfGridRow(cell_2) ->
            return true
        cell_2 == cell_1 - GRID_WIDTH ->
            return true
    }
    return false
}

const val NOWHERE = -1

// If the given player's character cell is a valid move, i.e. is it adjacent to
// the blank cell, return the blank cell position of the grid; otherwise return
// `null`.
//
fun targetPosition(player: Int, charCell: Int): Int? {
    val blankCell = grid[player].indexOf(BLANK)
    if (blankCell != NOWHERE && areCellsAdjacent(charCell, blankCell)) {
        return blankCell
    } else {
        printMessage("Illegal move \"" + grid[player][charCell] + "\".", player)
        return null
    }
}

// If the given player's command is valid, i.e. a grid character, return its
// position in the grid; otherwise print an error message and return `null`.
//
fun commandToCellPosition(player: Int, command: String): Int? {
    val position = grid[player].indexOf(command)
    if (position != NOWHERE) {
        return position
    } else {
        printMessage("Invalid character \"" + command + "\".", player)
        return null
    }
}

fun forgetPlayer(player: Int) {
    isPlaying[player] = false
    printGrid(player, DEFAULT_INK)
}

fun playTurn(player: Int) {

    var blankPosition: Int? = null
    var characterPosition: Int? = null

    if (isPlaying[player]) {

        while (true) {
            while (true) {
                val (row, column) = gridPromptPosition(player)
                setCursorPosition(row, column)
                eraseLineRight()
                setCursorPosition(row, column)
                val command: String = inputString("Move: ").trim().uppercase()
                if (command == QUIT_COMMAND) {
                    forgetPlayer(player)
                    return
                }
                characterPosition = commandToCellPosition(player, command)
                if (characterPosition is Int) {
                    break
                }
            }
            blankPosition = targetPosition(player, characterPosition)
            if (blankPosition is Int) {
                break
            }
        }
        eraseMessage(player)
        grid[player][blankPosition] = grid[player][characterPosition]
        grid[player][characterPosition] = BLANK

    }

}

fun playTurns() {
    for (player in 0 .. players - 1) playTurn(player)
}

fun isSomeonePlaying(): Boolean {
    for (player in 0 .. players - 1) {
        if (isPlaying[player]) return true
    }
    return false
}

// Has someone won? If so, print a message for every winner and return `true`;
// otherwise just return `false`.
//
fun hasSomeoneWon(): Boolean {
    var winners = 0
    for (player in 0 .. players - 1) {
        if (isPlaying[player]) {
            if (grid[player] contentEquals pristineGrid) {
                winners += 1
                if (winners > 0) {
                    printMessage(
                        "You're the winner" + (if (winners > 1) ", too" else "") + "!",
                        player,
                        rowInc = winners - 1)
                }
            }
        }
    }
    return winners > 0
}

fun initGame() {
    clearScreen()
    players = numberOfPlayers()
    for (player in 0 .. players - 1) {
        isPlaying[player] = true
    }
    clearScreen()
    printTitle()
    initGrids()
    printGrids()
}

fun play() {
    initGame()
    while (isSomeonePlaying()) {
        playTurns()
        printGrids()
        if (hasSomeoneWon()) break
    }
}

// Main {{{1
// =============================================================

// Init the program, i.e. just once before the first game.
//
fun initOnce() {
    // Init the pristine grid.
    val FIRST_CHAR_CODE = 'A'.code
    for (cell in 0 .. CELLS - 2) {
        pristineGrid[cell] = (FIRST_CHAR_CODE + cell).toChar().toString()
    }
    pristineGrid[CELLS - 1] = BLANK
}

// Return `true` if the player does not want to play another game; otherwise
// return `false`.
//
fun enough(): Boolean {
    val (row, column) = gridPromptPosition(player = 0)
    setCursorPosition(row, column)
    return !yes("Another game? ")
}

fun main() {
    initOnce()
    clearScreen()
    printCredits()
    pressEnter("\nPress the Enter key to read the instructions. ")
    clearScreen()
    printInstructions()
    pressEnter("\nPress the Enter key to start. ")
    while (true) {
        play()
        if (enough()) break
    }
    println("So long…")
}

In Nim

#[
Xchange

Original version in BASIC:
  Written by Thomas C. McIntire, 1979.
  Published in "The A to Z Book of Computer Games", 1979.
  https://archive.org/details/The_A_to_Z_Book_of_Computer_Games/page/n269/mode/2up
  https://github.com/chaosotter/basic-games

This improved remake in Nim:
  Copyright (c) 2025, Marcos Cruz (programandala.net)
  SPDX-License-Identifier: Fair

Written in 2025-01-21/22.

Last modified: 20250405T0304+0200.
]#

import std/strformat
import std/random
import std/strutils
import std/terminal
import std/unicode

# 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")

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
# =============================================================

const boardStyle = ansiStyleCode(cyan + bright)
const defaultStyle = ansiStyleCode(default, default, normal)
const inputStyle = ansiStyleCode(green + bright)
const instructionsStyle = ansiStyleCode(yellow)
const titleStyle = ansiStyleCode(red)

const blank = "*"

const gridHeight = 3 # cell rows
const gridWidth = 3 # cell columns

const cells = gridWidth * gridHeight

var pristineGrid: array[cells, string]

const gridsRow = 3 # screen row where the grids are printed
const gridsColumn = 5 # screen column where the left grid is printed
const cellsGap = 2 # distance between the grid cells, in screen rows or columns
const gridsGap = 16 # screen columns between equivalent cells of the grids

const maxPlayers = 4

var grid: array[maxPlayers, array[cells, string]]

var isPlaying: array[maxPlayers, bool]

var players: int

const quitCommand = "X"

# User input {{{1
# =============================================================

# Print the given prompt and return the integer entered by the
# user; if the user input is not a valid integer, return a zero
# instead.
#
proc inputInt(prompt: string): int =
  setStyle(inputStyle)
  defer: setStyle(defaultStyle)
  while true:
    try:
      write(stdout, prompt)
      result = parseInt(readLine(stdin))
      break
    except ValueError:
      result = 0

# Print the given prompt and wait until the user enters a string.
#
proc inputString(prompt: string = ""): string =
  setStyle(inputStyle)
  defer: setStyle(defaultStyle)
  write(stdout, prompt)
  result = readLine(stdin)

# 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:
    write(stdout, prompt)
    var answer = readLine(stdin)
    if isYes(answer):
      return true
    if isNo(answer):
      return false

# Title, instructions and credits {{{1
# =============================================================

# Print the title at the current cursor position.
#
proc printTitle() =
  setStyle(titleStyle)
  writeLine(stdout, "Xchange")
  setStyle(defaultStyle)

# Print the credits at the current cursor position.
#
proc printCredits() =
  printTitle()
  writeLine(stdout, "\nOriginal version in BASIC:")
  writeLine(stdout, "    Written by Thomas C. McIntire, 1979.")
  writeLine(stdout, "    Published in \"The A to Z Book of Computer Games\", 1979.")
  writeLine(stdout, "    https://archive.org/details/The_A_to_Z_Book_of_Computer_Games/page/n269/mode/2up")
  writeLine(stdout, "    https://github.com/chaosotter/basic-games")
  writeLine(stdout, "This improved remake in Nim:")
  writeLine(stdout, "    Copyright (c) 2025, Marcos Cruz (programandala.net)")
  writeLine(stdout, "    SPDX-License-Identifier: Fair")

# Print the instructions at the current cursor position.
#
proc printInstructions() =
  printTitle()
  setStyle(instructionsStyle)
  defer: setStyle(defaultStyle)
  writeLine(stdout, "\nOne or two may play.  If two, you take turns.  A grid looks like this:\n")
  setStyle(boardStyle)
  writeLine(stdout, "    F G D")
  writeLine(stdout, "    A H ", blank)
  writeLine(stdout, "    E B C\n")
  setStyle(instructionsStyle)
  writeLine(stdout, "But it should look like this:\n")
  setStyle(boardStyle)
  writeLine(stdout, "    A B C")
  writeLine(stdout, "    D E F")
  writeLine(stdout, "    G H ", blank, "\n")
  setStyle(instructionsStyle)
  writeLine(stdout, "You may exchange any one letter with the '", blank, "', but only one that's adjacent:")
  writeLine(stdout, "above, below, left, or right.  Not all puzzles are possible, and you may enter")
  writeLine(stdout, "'", quitCommand, "' to give up.\n")
  writeLine(stdout, "Here we go...")

# Grids {{{1
# =============================================================

# Print the given player's grid title.
#
proc printGridTitle(player: int) =
  setCursorPos(gridsColumn + (player * gridsGap), gridsRow)
  write(stdout, "Player ", player + 1)

# Return the cursor position of the given player's grid cell.
#
proc cellPosition(player, cell: int): (int, int) =
  let gridRow = int(cell / gridHeight)
  let gridColumn: int = cell mod gridWidth
  let titleMargin = if players > 1: 2 else: 0
  let row = gridsRow + titleMargin + gridRow
  let column = gridsColumn + (gridColumn * cellsGap) + (player * gridsGap)
  result = (column, row)

# Return the cursor position of the given player's grid prompt.
#
proc gridPromptPosition(player: int): (int, int) =
  let gridRow = int(cells / gridHeight)
  let gridColumn = cells mod gridWidth
  let titleMargin = if players > 1: 2 else: 0
  let row = gridsRow + titleMargin + gridRow + 1
  let column = gridsColumn + (gridColumn * cellsGap) + (player * gridsGap)
  result = (column, row)

# Print the given player's grid, in the given or default color.
#
proc printGrid(player: int, color = boardStyle) =
  if players > 1:
    printGridTitle(player)
  setStyle(color)
  defer: setStyle(defaultStyle)
  for cell in 0 ..< cells:
    var (column, row) = cellPosition(player, cell)
    setCursorPos(column, row)
    write(stdout, grid[player][cell])

# Print the current players' grids.
#
proc printGrids() =
  for player in 0 ..< players:
    if isPlaying[player]:
      printGrid(player)
  writeLine(stdout, "")
  eraseDown()

# Scramble the grid of the given player.
#
proc scrambleGrid(player: int) =
  for cell in 0 ..< cells:
    var randomCell = rand(cells - 1)
    var tempCell = grid[player][cell]
    grid[player][cell] = grid[player][randomCell]
    grid[player][randomCell] = tempCell

# Init the grids.
#
proc initGrids() =
  grid[0] = pristineGrid
  scrambleGrid(0)
  for player in 0 + 1 ..< players:
    grid[player] = grid[0]

# Messages {{{1
# =============================================================

# Return a message prefix for the given player.
#
proc playerPrefix(player: int): string =
  result = if players > 1: fmt"Player {player + 1}: " else: ""

# Return the cursor position of the given player's messages, adding the given
# row increment, which defaults to zero.
#
proc messagePosition(player: int, rowInc = 0): (int, int) =
  var (_, promptRow) = gridPromptPosition(player)
  result = (0, promptRow + 2 + rowInc)

# Print the given message about the given player, adding the given row
# increment, which defaults to zero, to the default cursor coordinates.
#
proc printMessage(message: string, player: int, rowInc = 0) =
  var (column, row) = messagePosition(player, rowInc)
  setCursorPos(column, row)
  write(stdout, playerPrefix(player), message)
  eraseLineRight()
  writeLine(stdout, "")

# Erase the last message about the given player.
#
proc eraseMessage(player: int) =
  var (column, row) = messagePosition(player)
  setCursorPos(column, row)
  eraseLineRight()

# Game loop {{{1
# =============================================================

# Return a message with the players range.
#
proc playersRangeMessage(): string =
  if maxPlayers == 2:
    return "1 or 2"
  else:
    return fmt"from 1 to {maxPlayers}"

# Return the number of players, asking the user if needed.
#
proc numberOfPlayers(): int =
  var players = 0
  printTitle()
  writeLine(stdout, "")
  if maxPlayers == 1:
    players = 1
  else:
    while players < 1 or players > maxPlayers:
      players = inputInt(fmt"Number of players ({playersRangeMessage()}): ")
  result = players

# Is the given cell the first one on a grid row?
#
proc isFirstCellOfAGridRow(cell: int): bool =
  result = cell mod gridWidth == 0

# Is the given cell the last one on a grid row?
#
proc isLastCellOfAGridRow(cell: int): bool =
  result = (cell + 1) mod gridWidth == 0

# Are the given cells adjacent?
#
proc areCellsAdjacent(cell_1, cell_2: int): bool =
  if cell_2 == cell_1 + 1 and not isFirstCellOfAGridRow(cell_2):
    return true
  elif cell_2 == cell_1 + gridWidth:
    return true
  elif cell_2 == cell_1 - 1  and not isLastCellOfAGridRow(cell_2):
    return true
  elif cell_2 == cell_1 - gridWidth:
    return true
  result = false

# Is the given player's character cell a valid move, i.e. is it adjacent to
# the blank cell? If so, return the blank cell of the grid and `true`;
# otherwise return a fake cell and `false`.
#
proc isLegalMove(player, charCell: int): (int, bool) =
  const nowhere = -1
  for blankCell, cellContent in grid[player]:
    if cellContent == blank:
      if areCellsAdjacent(charCell, blankCell):
        return (blankCell, true)
      break
  printMessage(&"Illegal move \"{grid[player][charCell]}\".", player)
  result = (nowhere, false)

# Is the given player's command valid, i.e. a grid character? If so, return
# its position in the grid and `true`; otherwise print an error message and
# return a fake position and `false`.
#
proc isValidCommand(player: int, command: string): (int, bool) =
  const nowhere = -1
  if command != blank:
    for position, cellContent in grid[player]:
      if cellContent == command:
        return (position, true)
  printMessage(&"Invalid character \"{command}\".", player)
  result = (nowhere, false)

# Forget the given player, who quitted.
#
proc forgetPlayer(player: int) =
  isPlaying[player] = false
  printGrid(player, defaultStyle)

# Play the turn of the given player.
#
proc playTurn(player: int) =
  var blankPosition: int
  var characterPosition: int

  if isPlaying[player]:

    while true:
      var command: string
      var ok = false
      while not ok:
        var (column, row) = gridPromptPosition(player)
        setCursorPos(column, row)
        eraseLineRight()
        setCursorPos(column, row)
        command = toUpper(strip(inputString("Move: ")))
        if command == quitCommand:
          forgetPlayer(player)
          return
        (characterPosition, ok) = isValidCommand(player, command)
      (blankPosition, ok) = isLegalMove(player, characterPosition)
      if ok:
        break
    eraseMessage(player)
    grid[player][blankPosition] = grid[player][characterPosition]
    grid[player][characterPosition] = blank

# Play the turns of all players.
#
proc playTurns() =
  for player in 0 ..< players:
    playTurn(player)

# Is someone playing?
#
proc isSomeonePlaying(): bool =
  for player in 0 ..< players:
    if isPlaying[player]:
      return true
  result = false

# Has someone won? If so, print a message for every winner and return `true`;
# otherwise just return `false`.
#
proc hasSomeoneWon(): bool =
  var winners = 0
  for player in 0 ..< players:
    if isPlaying[player]:
      if grid[player] == pristineGrid:
        winners += 1
        if winners > 0:
          var too = if winners > 1: ", too" else: ""
          printMessage(
            fmt"You're the winner{too}!",
            player,
            rowInc = winners - 1)
  result = winners > 0

# Init the game.
#
proc initGame() =
  clearScreen()
  players = numberOfPlayers()
  for player in 0 ..< players:
    isPlaying[player] = true
  clearScreen()
  printTitle()
  initGrids()
  printGrids()

# Play the game.
#
proc play() =
  initGame()
  while isSomeonePlaying():
    playTurns()
    printGrids()
    if hasSomeoneWon():
      break

# Main {{{1
# =============================================================

# Init the program, i.e. just once before the first game.
#
proc initOnce() =
  randomize()

 # Init the pristine grid.
  const firstCharCode = int('A')
  for cell in 0 ..< cells - 1:
    pristineGrid[cell] = $char(firstCharCode + cell)
  pristineGrid[cells - 1] = blank

# Return `true` if the player does not want to play another game; otherwise
# return `false`.
#
proc enough(): bool =
  var (column, row) = gridPromptPosition(player = 0)
  setCursorPos(column, row)
  result = not yes("Another game? ")

initOnce()
clearScreen()
printCredits()
pressEnter("\nPress the Enter key to read the instructions. ")
clearScreen()
printInstructions()
pressEnter("\nPress the Enter key to start. ")
while true:
  play()
  if enough():
    break
writeLine(stdout, "So long…")

In Odin

/*
Xchange

Original version in BASIC:
    Written by Thomas C. McIntire, 1979.
    Published in "The A to Z Book of Computer Games", 1979.
    https://archive.org/details/The_A_to_Z_Book_of_Computer_Games/page/n269/mode/2up
    https://github.com/chaosotter/basic-games

This improved remake in Odin:
    Copyright (c) 2023, 2024, 2025, Marcos Cruz (programandala.net)
    SPDX-License-Identifier: Fair

Written in 2023-12, 2024-12, 2025-02.

Last modified: 20250518T1741+0200.
*/

package xchange

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"
import "core:unicode/utf8"

// Globals {{{1
// =============================================================

BOARD_INK        :: basic.BRIGHT_CYAN
DEFAULT_INK      :: basic.WHITE
INPUT_INK        :: basic.BRIGHT_GREEN
INSTRUCTIONS_INK :: basic.YELLOW
TITLE_INK        :: basic.BRIGHT_RED

BLANK :: "*"

GRID_HEIGHT :: 3 // cell rows
GRID_WIDTH  :: 3 // cell columns

CELLS :: GRID_WIDTH * GRID_HEIGHT

pristine_grid := [CELLS]string{}

GRIDS_ROW :: 3 // screen row where the grids are printed
GRIDS_COLUMN :: 5 // screen column where the left grid is printed
CELLS_GAP :: 2 // distance between the grid cells, in screen rows or columns
GRIDS_GAP :: 16 // screen columns between equivalent cells of the grids

MAX_PLAYERS :: 4

grid := [MAX_PLAYERS][CELLS]string{}

is_playing := [MAX_PLAYERS]bool{}

players : int

QUIT_COMMAND :: "X"

// User input {{{1
// =============================================================

// Print the given prompt and wait until the user enters an integer.
//
input_int :: proc(prompt : string = "") -> int {

    basic.color(INPUT_INK)
    defer basic.color(DEFAULT_INK)
    fmt.print(prompt)
    return read.an_int_or_0()

}

// Print the given prompt and wait until the user enters a string.
//
input_string :: proc(prompt : string = "") -> string {

    basic.color(INPUT_INK)
    defer basic.color(DEFAULT_INK)
    fmt.print(prompt)
    return read.a_string() or_else ""

}

// Print the given prompt and wait until the user presses Enter.
//
press_enter :: proc(prompt : string) {

    input_string(prompt)

}

// 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)
        if is_yes(answer) do return true
        if is_no(answer) do return false
    }

}

// Title, instructions and credits {{{1
// =============================================================

// Print the title at the current cursor position.
//
print_title :: proc() {

    basic.color(TITLE_INK)
    fmt.println("Xchange")
    basic.color(DEFAULT_INK)

}

// Print the credits at the current cursor position.
//
print_credits :: proc() {

    print_title()
    fmt.println("\nOriginal version in BASIC:")
    fmt.println("    Written by Thomas C. McIntire, 1979.")
    fmt.println("    Published in \"The A to Z Book of Computer Games\", 1979.")
    fmt.println("    https://archive.org/details/The_A_to_Z_Book_of_Computer_Games/page/n269/mode/2up")
    fmt.println("    https://github.com/chaosotter/basic-games")
    fmt.println("This improved remake in Odin:")
    fmt.println("    Copyright (c) 2023, 2024, 2025, Marcos Cruz (programandala.net)")
    fmt.println("    SPDX-License-Identifier: Fair")

}

// Print the instructions at the current cursor position.
//
print_instructions :: proc() {

    print_title()
    basic.color(INSTRUCTIONS_INK)
    defer basic.color(DEFAULT_INK)
    fmt.println("\nOne or two may play.  If two, you take turns.  A grid looks like this:\n")
    basic.color(BOARD_INK)
    fmt.println("    F G D")
    fmt.printfln("    A H %s", BLANK)
    fmt.println("    E B C\n")
    basic.color(INSTRUCTIONS_INK)
    fmt.println("But it should look like this:\n")
    basic.color(BOARD_INK)
    fmt.println("    A B C")
    fmt.println("    D E F")
    fmt.printfln("    G H %s\n", BLANK)
    basic.color(INSTRUCTIONS_INK)
    fmt.printfln("You may exchange any one letter with the '%s', but only one that's adjacent:", BLANK)
    fmt.println("above, below, left, or right.  Not all puzzles are possible, and you may enter")
    fmt.printfln("'%s' to give up.\n", QUIT_COMMAND)
    fmt.println("Here we go...")

}

// Grids {{{1
// =============================================================

// Print the given player's grid title.
//
print_grid_title :: proc(player : int) {

    term.set_cursor_position(GRIDS_ROW, GRIDS_COLUMN + (player * GRIDS_GAP))
    fmt.printf("Player %v", player + 1)

}

// Return the cursor position of the given player's grid cell.
//
cell_position :: proc(player, cell : int) -> (row, column : int) {

    grid_row     : int = cell / GRID_HEIGHT
    grid_column  : int = cell % GRID_WIDTH
    title_margin := players > 1 ? 2 : 0
    return \
        GRIDS_ROW + title_margin + grid_row,
        GRIDS_COLUMN + (grid_column * CELLS_GAP) + (player * GRIDS_GAP)

}

// Return the cursor position of the given player's grid prompt.
//
grid_prompt_position :: proc(player : int) -> (row, column : int) {

    grid_row     : int = CELLS / GRID_HEIGHT
    grid_column  : int = CELLS % GRID_WIDTH
    title_margin := players > 1 ? 2 : 0
    return \
        GRIDS_ROW + title_margin + grid_row + 1,
        GRIDS_COLUMN + (grid_column * CELLS_GAP) + (player * GRIDS_GAP)

}

// Print the given player's grid, in the given or default color.
//
print_grid :: proc(player : int, color := BOARD_INK) {

    if players > 1 do print_grid_title(player)
    basic.color(color)
    defer basic.color(DEFAULT_INK)
    for cell in 0 ..< CELLS {
        term.set_cursor_position(cell_position(player, cell))
        fmt.print(grid[player][cell])
    }

}

// Print the current players' grids.
//
print_grids :: proc() {

    for player in 0 ..< players {
        if is_playing[player] do print_grid(player)
    }
    fmt.println()
    term.erase_screen_down()

}

// Scramble the grid of the given player.
//
scramble_grid :: proc(player : int) {

    for cell in 0 ..< CELLS {
        random_cell := rand.int_max(CELLS)
        // Exchange the contents of the current cell with that of the random one.
        grid[player][cell], grid[player][random_cell] =
            grid[player][random_cell], grid[player][cell]
    }

}

// Init the grids.
//
init_grids :: proc() {

    grid[0] = pristine_grid
    scramble_grid(0)
    for player in 0 + 1 ..< players {
        grid[player] = grid[0]
    }

}

// Messages {{{1
// =============================================================

// Return a message prefix for the given player.
//
player_prefix :: proc(player : int) -> string {

    return players > 1 ? fmt.tprintf("Player %i: ", player + 1) : ""

}

// Return the cursor position of the given player's messages, adding the given
// row increment, which defaults to zero.
//
message_position :: proc(player : int, row_inc := 0) -> (row, column : int) {

    prompt_row, _ := grid_prompt_position(player)
    return prompt_row + 2 + row_inc, 1

}

// Print the given message about the given player, adding the given row
// increment, which defaults to zero, to the default cursor coordinates.
//
print_message :: proc(message : string, player : int, row_inc := 0) {

    term.set_cursor_position(message_position(player, row_inc))
    fmt.printf("%s%s", player_prefix(player), message)
    term.erase_line_right()
    fmt.println()

}

// Erase the last message about the given player.
//
erase_message :: proc(player : int) {

    term.set_cursor_position(message_position(player))
    term.erase_line_right()

}

// Game loop {{{1
// =============================================================

// Return a message with the players range.
//
players_range_message :: proc() -> string {

    if MAX_PLAYERS == 2 {
        return "1 or 2"
    } else {
        return fmt.tprintf("from 1 to %v", MAX_PLAYERS)
    }

}

// Return the number of players, asking the user if needed.
//
number_of_players :: proc() -> int {

    players := 0
    print_title()
    fmt.println()
    if MAX_PLAYERS == 1 {
        players = 1
    } else {
        for players < 1 || players > MAX_PLAYERS {
            players = input_int(fmt.tprintf("Number of players (%s): ", players_range_message()))
        }
    }
    return players

}

// Is the given cell the first one on a grid row?
//
is_first_cell_of_a_grid_row :: proc(cell : int) -> bool {

    return cell % GRID_WIDTH == 0

}

// Is the given cell the last one on a grid row?
//
is_last_cell_of_a_grid_row :: proc(cell : int) -> bool {

    return (cell + 1) % GRID_WIDTH == 0

}

// Are the given cells adjacent?
//
are_cells_adjacent :: proc(cell_1, cell_2 : int) -> bool {

    switch {
        case cell_2 == cell_1 + 1 && ! is_first_cell_of_a_grid_row(cell_2) :
            return true
        case cell_2 == cell_1 + GRID_WIDTH :
            return true
        case cell_2 == cell_1 - 1 && ! is_last_cell_of_a_grid_row(cell_2) :
            return true
        case cell_2 == cell_1 - GRID_WIDTH :
            return true
    }
    return false

}

// Is the given player's character cell a valid move, i.e. is it adjacent to
// the blank cell? If so, return the blank cell of the grid and `true`;
// otherwise return a fake cell and `false`.
//
is_legal_move :: proc(player, char_cell : int) -> (blank_cell : int, ok : bool) {

    NOWHERE :: -1
    for cell_content, cell_number in grid[player] {
        if cell_content == BLANK {
            if are_cells_adjacent(char_cell, cell_number) {
                return cell_number, true
            } else {
                break
            }
        }
    }
    print_message(fmt.tprintf("Illegal move \"%s\".", grid[player][char_cell]), player)
    return NOWHERE, false

}

// Is the given player's command valid, i.e. a grid character? If so, return
// its position in the grid and `true`; otherwise print an error message and
// return a fake position and `false`.
//
is_valid_command :: proc(player : int, command : string) -> (position : int, ok : bool) {

    NOWHERE :: -1
    if command != BLANK {
        for cell_content, position in grid[player] {
            if cell_content == command do return position, true
        }
    }
    print_message(fmt.tprintf("Invalid character \"%s\".", command), player)
    return NOWHERE, false

}

// Forget the given player, who quitted.
//
forget_player :: proc(player : int) {

    is_playing[player] = false
    print_grid(player, DEFAULT_INK)

}

// Play the turn of the given player.
//
play_turn :: proc(player : int) {

    blank_position : int
    character_position : int

    if is_playing[player] {

        for {
            ok : bool
            command : string
            for ok = false ; ! ok; character_position, ok = is_valid_command(player, command) {
                row, column := grid_prompt_position(player)
                term.set_cursor_position(row, column)
                term.erase_line_right()
                term.set_cursor_position(row, column)
                raw_command := input_string("Move: ")
                defer delete(raw_command)
                command = strings.to_upper(strings.trim_space(raw_command))
                defer delete(command)
                if command == QUIT_COMMAND {
                    forget_player(player)
                    return
                }
            }
            blank_position, ok = is_legal_move(player, character_position)
            if ok do break
        }
        erase_message(player)
        grid[player][blank_position] = grid[player][character_position]
        grid[player][character_position] = BLANK

    }

}

// Play the turns of all players.
//
play_turns :: proc() {

    for player in 0 ..< players do play_turn(player)

}

// Is someone playing?
//
is_someone_playing :: proc() -> bool {

    for player in 0 ..< players {
        if is_playing[player] do return true
    }
    return false

}

// Has someone won? If so, print a message for every winner and return `true`;
// otherwise just return `false`.
//
has_someone_won :: proc() -> bool {

    winners := 0
    for player in 0 ..< players {
        if is_playing[player] {
            if grid[player] == pristine_grid {
                winners += 1
                if winners > 0 {
                    print_message(
                        fmt.tprintf("You're the winner%s!", winners > 1 ? ", too" : ""),
                        player,
                        row_inc = winners - 1)
                }
            }
        }
    }
    return winners > 0

}

// Init the game.
//
init_game :: proc() {

    term.clear_screen()
    players = number_of_players()
    for player in 0 ..< players {
        is_playing[player] = true
    }
    term.clear_screen()
    print_title()
    init_grids()
    print_grids()

}

// Play the game.
//
play :: proc() {

    init_game()
    for is_someone_playing() {
        play_turns()
        print_grids()
        if has_someone_won() do break
    }

}

// Main {{{1
// =============================================================

// Init the program, i.e. just once before the first game.
//
init_once :: proc() {

    basic.randomize()

    // Init the pristine grid.
    FIRST_CHAR_CODE :: int('A')
    for cell in 0 ..< CELLS - 1 {
        pristine_grid[cell] = utf8.runes_to_string([]rune{rune(FIRST_CHAR_CODE + cell)})
    }
    pristine_grid[CELLS - 1] = BLANK

}

// Return `true` if the player does not want to play another game; otherwise
// return `false`.
//
enough :: proc() -> bool {

    term.set_cursor_position(grid_prompt_position(player = 0))
    return ! yes("Another game? ")

}

main :: proc() {

    init_once()

    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. ")
    for {
        play()
        if enough() do break
    }
    fmt.println("So long…")

}

In Pike

#!/usr/bin/env pike

// Xchange

// Original version in BASIC:
//  Written by Thomas C. McIntire, 1979.
//  Published in "The A to Z Book of Computer Games", 1979.
//  https://archive.org/details/The_A_to_Z_Book_of_Computer_Games/page/n269/mode/2up
//  https://github.com/chaosotter/basic-games

// This improved remake in Pike:
//  Copyright (c) 2025, Marcos Cruz (programandala.net)
//  SPDX-License-Identifier: Fair
//
// Written on 2025-03-12.
//
// 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 column) {
    write("\x1B[%d;%dH", line, column);
}

void set_cursor_coord(array(int) coord) {
    set_cursor_position(coord[0], coord[1]);
}

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_to_end() {
    write("\x1B[J");
}

void erase_screen() {
    write("\x1B[2J");
}

void clear_screen() {
    erase_screen();
    reset_attributes();
    move_cursor_home();
}

// Data {{{1
// =============================================================

constant BOARD_INK = FOREGROUND + BRIGHT + CYAN;
constant DEFAULT_INK = FOREGROUND + WHITE;
constant INPUT_INK = FOREGROUND + BRIGHT + GREEN;
constant INSTRUCTIONS_INK = FOREGROUND + YELLOW;
constant TITLE_INK = FOREGROUND + BRIGHT + RED;

constant BLANK = "*";

constant GRID_HEIGHT = 3; // cell rows
constant GRID_WIDTH = 3; // cell columns

constant CELLS = GRID_WIDTH * GRID_HEIGHT;

array(string) pristine_grid = allocate(CELLS, "");

constant GRIDS_ROW = 3; // screen row where the grids are printed
constant GRIDS_COLUMN = 5; // screen column where the left grid is printed
constant CELLS_GAP = 2; // distance between the grid cells, in screen rows or columns
constant GRIDS_GAP = 16; // screen columns between equivalent cells of the grids

constant FIRST_PLAYER = 0;
constant MAX_PLAYERS = 4;

array(array(string)) grid = allocate(MAX_PLAYERS, allocate(CELLS, ""));

array(bool) is_playing = allocate(MAX_PLAYERS, false);

int players = 0;

constant QUIT_COMMAND = "X";

enum axis { Y, X }; // to use with coord arrays instead of 0 and 1, for clarity

// User input {{{1
// =============================================================

string accept_string(string prompt) {
    write(prompt);
    return Stdio.stdin->gets();
}

int accept_integer_or_0(string prompt) {
    // NB The casting accepts any valid integer at the start of the string
    // and ignores the rest;  if no valid integer is found at the start of
    // the string, zero is returned.
    return (int) accept_string(prompt);
}

int get_integer(string|void prompt) {
    prompt = prompt || "";
    set_style(INPUT_INK);
    int n = accept_integer_or_0(prompt);
    set_style(DEFAULT_INK);
    return n;
}

string get_string(string|void prompt) {
    prompt = 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, instructions and credits {{{1
// =============================================================

void print_title() {
    set_style(TITLE_INK);
    write("Xchange\n");
    set_style(DEFAULT_INK);
}

void print_credits() {
    print_title();
    write("\nOriginal version in BASIC:\n");
    write("    Written by Thomas C. McIntire, 1979.\n");
    write("    Published in \"The A to Z Book of Computer Games\", 1979.\n");
    write("    https://archive.org/details/The_A_to_Z_Book_of_Computer_Games/page/n269/mode/2up\n");
    write("    https://github.com/chaosotter/basic-games\n");
    write("This improved remake 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("\nOne or two may play.  If two, you take turns.  A grid looks like this:\n\n");
    set_style(BOARD_INK);
    write("    F G D\n");
    write("    A H %s\n", BLANK);
    write("    E B C\n\n");
    set_style(INSTRUCTIONS_INK);
    write("But it should look like this:\n\n");
    set_style(BOARD_INK);
    write("    A B C\n");
    write("    D E F\n");
    write("    G H %s\n\n", BLANK);
    set_style(INSTRUCTIONS_INK);
    write("You may exchange any one letter with the '%s', but only one that's adjacent:\n", BLANK);
    write("above, below, left, or right.  Not all puzzles are possible, and you may enter\n");
    write("'%s' to give up.\n\n", QUIT_COMMAND);
    write("Here we go...\n");
    set_style(DEFAULT_INK);
}

// Grids {{{1
// =============================================================

// Print the given player's grid title.
//
void print_grid_title(int player) {
    set_cursor_position(GRIDS_ROW, GRIDS_COLUMN + (player * GRIDS_GAP));
    write("Player %d", player + 1);
}

// Return the cursor position of the given player's grid cell.
//
array(int) cell_position(int player, int cell) {
    int grid_row = cell / GRID_HEIGHT;
    int grid_column = cell % GRID_WIDTH;
    int title_margin = players > 1 ? 2 : 0;
    int row = GRIDS_ROW + title_margin + grid_row;
    int column = GRIDS_COLUMN +
        (grid_column * CELLS_GAP) +
        (player * GRIDS_GAP);
    return ({ row, column });
}

// Return the cursor position of the given player's grid prompt.
//
array(int) grid_prompt_position_of(int player) {
    array(int) coord = cell_position(player, CELLS);
    int row = coord[0];
    int column = coord[1];
    return ({ row + 1, column });
}

// Print the given player's grid, in the given or default color.
//
void print_grid(int player, int|void color) {
    color = color || BOARD_INK;
    if (players > 1) {
        print_grid_title(player);
    }
    set_style(color);
    for (int cell = 0; cell < CELLS; cell += 1) {
        set_cursor_coord(cell_position(player, cell));
        write(grid[player][cell]);
    }
    set_style(DEFAULT_INK);
}

// Print the current players' grids.
//
void print_grids() {
    for (int player = 0; player < players; player += 1) {
        if (is_playing[player]) {
            print_grid(player);
        }
    }
    write("\n");
    erase_screen_to_end();
}

// Scramble the grid of the given player.
//
void scramble_grid(int player) {
    for (int cell = 0; cell < CELLS; cell += 1) {
        int random_cell = random(CELLS);
        // Exchange the contents of the current cell with that of the random one.
        string  tmp = grid[player][cell];
        grid[player][cell] = grid[player][random_cell];
        grid[player][random_cell] = tmp;
    }
}

// Init the grids.
//
void init_grids() {
    grid[0] = copy_value(pristine_grid);
    scramble_grid(0);
    for (int player = 1; player < players; player += 1) {
        grid[player] = copy_value(grid[0]);
    }
}

// Messages {{{1
// =============================================================

// Return a message prefix for the given player.
//
string player_prefix(int player) {
    return players > 1 ? sprintf("Player %d: ", player + 1) : "";
}

// Return the cursor position of the given player's messages, adding the given
// row increment, which defaults to zero.
//
array(int) message_position(int player, int|void row_inc) {
    row_inc = row_inc || 0;
    array(int) prompt_coord = grid_prompt_position_of(player);
    return ({ prompt_coord[Y] + 2 + row_inc, 1 });
}

// Print the given message about the given player, adding the given row
// increment, which defaults to zero, to the default cursor coordinates.
//
void print_message(string message, int player, int|void row_inc) {
    row_inc = row_inc || 0;
    set_cursor_coord(message_position(player, row_inc));
    write("%s%s", player_prefix(player), message);
    erase_line_to_end();
    write("\n");
}

// Erase the last message about the given player.
//
void erase_message(int player) {
    set_cursor_coord(message_position(player));
    erase_line_to_end();
}

// Game loop {{{1
// =============================================================

// Return a message with the players range.
//
string players_range_message() {
    if (MAX_PLAYERS == 2) {
        return "1 or 2";
    } else {
        return sprintf("from 1 to %d", MAX_PLAYERS);
    }
}

// Return the number of players, asking the user if needed.
//
int number_of_players() {
    int players = 0;
    print_title();
    write("\n");
    if (MAX_PLAYERS == 1) {
        players = 1;
    } else {
        while (players < 1 || players > MAX_PLAYERS) {
            string prompt = sprintf("Number of players (%s): ", players_range_message());
            players = get_integer(prompt);
        }
    }
    return players;
}

// Is the given cell the first one on a grid row?
//
bool is_first_cell_of_a_grid_row(int cell) {
    return cell % GRID_WIDTH == 0;
}

// Is the given cell the last one on a grid row?
//
bool is_last_cell_of_a_grid_row(int cell) {
    return (cell + 1) % GRID_WIDTH == 0;
}

// Are the given cells adjacent?
//
bool are_cells_adjacent(int cell_1, int cell_2) {
    return (cell_2 == cell_1 + 1 && !is_first_cell_of_a_grid_row(cell_2)) ||
        (cell_2 == cell_1 + GRID_WIDTH) ||
        (cell_2 == cell_1 - 1 && !is_last_cell_of_a_grid_row(cell_2)) ||
        (cell_2 == cell_1 - GRID_WIDTH);
}

// If the given player's character cell is a valid move, i.e. it is adjacent to
// the blank cell, return the blank cell; otherwise return an empty string.
//
int|string position_to_cell(int player, int char_cell) {
    for (int cell = 0; cell < CELLS; cell += 1) {
        if (grid[player][cell] == BLANK) {
            if (are_cells_adjacent(char_cell, cell)) {
                return cell;
            } else {
                break;
            }
        }
    }
    print_message(sprintf("Illegal move \"%s\".", grid[player][char_cell]), player);
    return "";
}

// If the given player's command is valid, i.e. a grid character, return its
// position; otherwise return an empty string.
//
int|string command_to_position(int player, string command) {
    if (command != BLANK) {
        for (int position = 0; position < CELLS; position += 1) {
            if (command == grid[player][position]) {
                return position;
            }
        }
    }
    print_message(sprintf("Invalid character \"%s\".", command), player);
    return "";
}

// Forget the given player, who quitted.
//
void forget_player(int player) {
    is_playing[player] = false;
    print_grid(player, DEFAULT_INK);
}

// Play the turn of the given player.
//
void play_turn(int player) {
    int blank_position = 0;
    int character_position = 0;

    if (is_playing[player]) {
        while (true) {
            while (true) {
                array(int) coord = grid_prompt_position_of(player);
                set_cursor_coord(coord);
                erase_line_to_end();
                set_cursor_coord(coord);
                string command = upper_case(String.trim(get_string("Move: ")));
                if (command == QUIT_COMMAND) {
                    forget_player(player);
                    return;
                }
                int|string position = command_to_position(player, command);
                if (intp(position)) {
                    character_position = position;
                    break;
                }
            }
            int|string position = position_to_cell(player, character_position);
            if (intp(position)) {
                blank_position = position;
                break;
            }
        }
        erase_message(player);
        grid[player][blank_position] = grid[player][character_position];
        grid[player][character_position] = BLANK;
    }
}

// Play the turns of all players.
//
void play_turns() {
    for (int player = 0; player < players; player += 1) {
        play_turn(player);
    }
}

// Is someone playing?
//
bool is_someone_playing() {
    for (int player = 0; player < players; player += 1) {
        if (is_playing[player]) {
            return true;
        }
    }
    return false;
}

bool has_an_empty_grid(int player) {
    for (int i = 0; i < CELLS; i += 1) {
        if (grid[player][i] != "") {
            return false;
        }
    }
    return true;
}

// Has someone won? If so, print a message for every winner and return `true`
// otherwise just return `false`.
//
bool has_someone_won() {
    int winners = 0;
    for (int player = 0; player < players; player += 1) {
        if (is_playing[player]) {
            if (has_an_empty_grid(player)) {
                winners += 1;
                if (winners > 0) {
                    print_message(
                        sprintf("You're the winner%s!", (winners > 1) ? ", too" : ""),
                        player,
                        winners - 1);
                }
            }
        }
    }
    return winners > 0;
}

// Init the game.
//
void init_game() {
    clear_screen();
    players = number_of_players();
    for (int player = 0; player < players; player += 1) {
        is_playing[player] = true;
    }
    clear_screen();
    print_title();
    init_grids();
    print_grids();
}

// Play the game.
//
void play() {
    init_game();
    while (is_someone_playing()) {
        play_turns();
        print_grids();
        if (has_someone_won()) {
            break;
        }
    }
}

// Main {{{1
// =============================================================

void init_once() {

    // Init the pristine grid.
    constant FIRST_CHAR_CODE = 'A';
    for (int cell = 0; cell < CELLS - 1; cell += 1) {
        pristine_grid[cell] = sprintf("%c", FIRST_CHAR_CODE + cell);
    }
    pristine_grid[CELLS - 1] = BLANK;
}

// Return `true` if the player does not want to play another game; otherwise
// return `false`.
//
bool enough() {
    set_cursor_coord(grid_prompt_position_of(FIRST_PLAYER));
    return !yes("Another game? ");
}

void main() {
    init_once();

    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. ");

    while (true) {
        play();
        if (enough()) {
            break;
        }
    }
    write("So long…\n");
}

In V

/*
Xchange

Original version in BASIC:
    Written by Thomas C. McIntire, 1979.
    Published in "The A to Z Book of Computer Games", 1979.
    https://archive.org/details/The_A_to_Z_Book_of_Computer_Games/page/n269/mode/2up
    https://github.com/chaosotter/basic-games

This improved remake in V:
    Copyright (c) 2025, Marcos Cruz (programandala.net)
    SPDX-License-Identifier: Fair

Written in 2025-01-11/12.

Last modified: 20250714T1333+0200.
*/
import os
import rand
import strconv
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

fn set_color(color int) {
    print('\x1B[0;${color}m')
}

// Constants {{{1
// =============================================================

const board_ink = foreground + bright + cyan
const default_ink = foreground + default_color
const input_ink = foreground + bright + green
const instructions_ink = foreground + yellow
const title_ink = foreground + bright + red

const blank = '*'

const grid_height = 3 // cell rows
const grid_width = 3 // cell columns

const cells = grid_width * grid_height

const grids_row = 3 // screen row where the grids are printed
const grids_column = 5 // screen column where the left grid is printed
const cells_gap = 2 // distance between the grid cells, in screen rows or columns
const grids_gap = 16 // screen columns between equivalent cells of the grids

const max_players = 4

const quit_command = 'X'

const nowhere = -1

const first_char_code = int(`A`)

// State structure {{{1
// =============================================================

// The `State` structure the game state, i.e. the global data.
//
struct State {
mut:
    pristine_grid [cells]string
    grid          [max_players][cells]string
    is_playing    [max_players]bool
    players       int
}

// 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 enters an integer.
//
fn input_int(prompt string) int {
    mut n := 0
    for {
        n = strconv.atoi(input_string(prompt)) or { continue }
        break
    }
    return n
}

// 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, instructions and credits {{{1
// =============================================================

// Print the title at the current cursor position.
//
fn print_title() {
    set_color(title_ink)
    println('Xchange')
    set_color(default_ink)
}

// Print the credits at the current cursor position.
//
fn print_credits() {
    print_title()
    println('\nOriginal version in BASIC:')
    println('    Written by Thomas C. McIntire, 1979.')
    println('    Published in "The A to Z Book of Computer Games", 1979.')
    println('    https://archive.org/details/The_A_to_Z_Book_of_Computer_Games/page/n269/mode/2up')
    println('    https://github.com/chaosotter/basic-games')
    println('This improved remake in V:')
    println('    Copyright (c) 2025, Marcos Cruz (programandala.net)')
    println('    SPDX-License-Identifier: Fair')
}

// Print the instructions at the current cursor position.
//
fn print_instructions() {
    print_title()
    set_color(instructions_ink)
    defer { set_color(default_ink) }
    println('\nOne or two may play.  If two, you take turns.  A grid looks like this:\n')
    set_color(board_ink)
    println('    F G D')
    println('    A H ${blank}')
    println('    E B C\n')
    set_color(instructions_ink)
    println('But it should look like this:\n')
    set_color(board_ink)
    println('    A B C')
    println('    D E F')
    println('    G H ${blank}\n')
    set_color(instructions_ink)
    println("You may exchange any one letter with the '${blank}', but only one that's adjacent:")
    println('above, below, left, or right.  Not all puzzles are possible, and you may enter')
    println("'${quit_command}' to give up.\n")
    println('Here we go…')
}

// Grids {{{1
// =============================================================

// Print the given player's grid title.
//
fn print_grid_title(player int) {
    term.set_cursor_position(y: grids_row, x: grids_column + (player * grids_gap))
    print('Player ${player + 1}')
}

// Return the cursor position of the given player's grid cell.
//
fn cell_position(player int, cell int, state State) term.Coord {
    grid_row := cell / grid_height
    grid_column := cell % grid_width
    title_margin := if state.players > 1 { 2 } else { 0 }
    return term.Coord{
        y: grids_row + title_margin + grid_row
        x: grids_column + (grid_column * cells_gap) + (player * grids_gap)
    }
}

// Return the cursor position of the given player's grid prompt.
//
fn grid_prompt_position(player int, state State) term.Coord {
    grid_row := cells / grid_height
    grid_column := cells % grid_width
    title_margin := if state.players > 1 { 2 } else { 0 }
    return term.Coord{
        y: grids_row + title_margin + grid_row + 1
        x: grids_column + (grid_column * cells_gap) + (player * grids_gap)
    }
}

// Print the given player's grid, in the given or default color.
//
fn print_grid(player int, color int, state State) {
    if state.players > 1 {
        print_grid_title(player)
    }
    set_color(color)
    defer { set_color(default_ink) }
    for cell in 0 .. cells {
        term.set_cursor_position(cell_position(player, cell, state))
        print(state.grid[player][cell])
    }
}

// Print the current players' grids.
//
fn print_grids(state State) {
    for player in 0 .. state.players {
        if state.is_playing[player] {
            print_grid(player, board_ink, state)
        }
    }
    println('')
    term.erase_toend()
}

// Scramble the grid of the given player.
//
fn scramble_grid(player int, mut state State) {
    for cell in 0 .. cells {
        random_cell := rand.intn(cells) or { 0 }
        // Exchange the contents of the current cell with that of the random one.
        state.grid[player][cell], state.grid[player][random_cell] = state.grid[player][random_cell], state.grid[player][cell]
    }
}

// Init the grids.
//
fn init_grids(mut state State) {
    state.grid[0] = state.pristine_grid
    scramble_grid(0, mut state)
    for player in 0 + 1 .. state.players {
        state.grid[player] = state.grid[0]
    }
}

// Messages {{{1
// =============================================================

// Return a message prefix for the given player.
//
fn player_prefix(player int, state State) string {
    return if state.players > 1 {
        unsafe { strconv.v_sprintf('Player ${player + 1}: ') }
    } else {
        ''
    }
}

// Return the cursor position of the given player's messages, adding the given
// row increment, which defaults to zero.
//
fn message_position(player int, row_inc int, state State) term.Coord {
    prompt_row := grid_prompt_position(player, state).y
    return term.Coord{
        y: prompt_row + 2 + row_inc
        x: 1
    }
}

// Print the given message about the given player, adding the given row
// increment, which defaults to zero, to the default cursor coordinates.
//
fn print_message(message string, player int, row_inc int, state State) {
    term.set_cursor_position(message_position(player, row_inc, state))
    print('${player_prefix(player, state)}${message}')
    term.erase_line_toend()
    println('')
}

// Erase the last message about the given player.
//
fn erase_message(player int, state State) {
    term.set_cursor_position(message_position(player, 0, state))
    term.erase_line_toend()
}

// Game loop {{{1
// =============================================================

// Return a message with the players range.
//
fn players_range_message() string {
    if max_players == 2 {
        return '1 or 2'
    } else {
        return unsafe { strconv.v_sprintf('from 1 to ${max_players}') }
    }
}

// Return the number of players, asking the user if needed.
//
fn number_of_players() int {
    mut players := 0
    print_title()
    println('')
    if max_players == 1 {
        players = 1
    } else {
        for players < 1 || players > max_players {
            players = input_int(unsafe { strconv.v_sprintf('Number of players (${players_range_message()}): ') })
        }
    }
    return players
}

// Is the given cell the first one on a grid row?
//
fn is_first_cell_of_a_grid_row(cell int) bool {
    return cell % grid_width == 0
}

// Is the given cell the last one on a grid row?
//
fn is_last_cell_of_a_grid_row(cell int) bool {
    return (cell + 1) % grid_width == 0
}

// Are the given cells adjacent?
//
fn are_cells_adjacent(cell_1 int, cell_2 int) bool {
    match true {
        cell_2 == cell_1 + 1 && !is_first_cell_of_a_grid_row(cell_2) {
            return true
        }
        cell_2 == cell_1 + grid_width {
            return true
        }
        cell_2 == cell_1 - 1 && !is_last_cell_of_a_grid_row(cell_2) {
            return true
        }
        cell_2 == cell_1 - grid_width {
            return true
        }
        else {}
    }
    return false
}

// Is the given player's character cell a valid move, i.e. is it adjacent to
// the blank cell? If so, return the blank cell of the grid and `true`;
// otherwise return a fake cell and `false`.
//
fn is_legal_move(player int, char_cell int, state State) (int, bool) {
    for blank_cell, cell_content in state.grid[player] {
        if cell_content == blank {
            if are_cells_adjacent(char_cell, blank_cell) {
                return blank_cell, true
            }
            break
        }
    }
    print_message(unsafe { strconv.v_sprintf('Illegal move "${state.grid[player][char_cell]}".') },
        player, 0, state)
    return nowhere, false
}

// Is the given player's command valid, i.e. a grid character? If so, return
// its position in the grid and `true`; otherwise print an error message and
// return a fake position and `false`.
//
fn is_valid_command(player int, command string, state State) (int, bool) {
    if command != blank {
        for position, cell_content in state.grid[player] {
            if cell_content == command {
                return position, true
            }
        }
    }
    print_message(unsafe { strconv.v_sprintf('Invalid character "${command}".') }, player,
        0, state)
    return nowhere, false
}

// Forget the given player, who quitted.
//
fn forget_player(player int, mut state State) {
    state.is_playing[player] = false
    print_grid(player, default_ink, state)
}

// Play the turn of the given player.
//
fn play_turn(player int, mut state State) {
    mut blank_position := 0
    mut character_position := 0

    if state.is_playing[player] {
        for {
            mut ok := false
            mut command := ''
            for ok = false; !ok; character_position, ok = is_valid_command(player, command,
                state) {
                coord := grid_prompt_position(player, state)
                row, column := coord.y, coord.x
                term.set_cursor_position(y: row, x: column)
                term.erase_line_toend()
                term.set_cursor_position(y: row, x: column)
                command = input_string('Move: ').trim_space().to_upper()
                if command == quit_command {
                    forget_player(player, mut state)
                    return
                }
            }
            blank_position, ok = is_legal_move(player, character_position, state)
            if ok {
                break
            }
        }
        erase_message(player, state)
        state.grid[player][blank_position] = state.grid[player][character_position]
        state.grid[player][character_position] = blank
    }
}

// Play the turns of all players.
//
fn play_turns(mut state State) {
    for player in 0 .. state.players {
        play_turn(player, mut state)
    }
}

// Is someone playing?
//
fn is_someone_playing(state State) bool {
    for player in 0 .. state.players {
        if state.is_playing[player] {
            return true
        }
    }
    return false
}

// Has someone won? If so, print a message for every winner and return `true`;
// otherwise just return `false`.
//
fn has_someone_won(state State) bool {
    mut winners := 0
    for player in 0 .. state.players {
        if state.is_playing[player] {
            if state.grid[player] == state.pristine_grid {
                winners += 1
                if winners > 0 {
                    winner := 'winner' + if winners > 1 { ', too' } else { '' }
                    print_message(unsafe { strconv.v_sprintf("You're the ${winner}!") },
                        player, winners - 1, state)
                }
            }
        }
    }
    return winners > 0
}

// Init the game.
//
fn init_game(mut state State) {
    term.clear()
    state.players = number_of_players()
    for player in 0 .. state.players {
        state.is_playing[player] = true
    }
    term.clear()
    print_title()
    init_grids(mut state)
    print_grids(state)
}

// Play the game.
//
fn play(mut state State) {
    init_game(mut state)
    for is_someone_playing(state) {
        play_turns(mut state)
        print_grids(state)
        if has_someone_won(state) {
            break
        }
    }
}

// Main {{{1
// =============================================================

// Init the program, i.e. just once before the first game.
//
fn init_once(mut state State) {
    // Init the pristine grid.
    for cell in 0 .. cells - 1 {
        state.pristine_grid[cell] = rune(first_char_code + cell).str()
    }
    state.pristine_grid[cells - 1] = blank
}

// Return `true` if the player does not want to play another game; otherwise
// return `false`.
//
fn enough(state State) bool {
    term.set_cursor_position(grid_prompt_position(0, state))
    return !yes('Another game? ')
}

fn main() {
    mut state := State{}
    init_once(mut state)

    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. ')
    for {
        play(mut state)
        if enough(state) {
            break
        }
    }
    println('So long…')
}

Págines relatet

Basics off
Metaprojecte pri li projectes «Basics of…».

Extern ligamentes relatet