From d05bdd13e7e9680cc945e02df1e73847100fefd0 Mon Sep 17 00:00:00 2001 From: Noah Pauls Date: Mon, 17 Jan 2022 18:06:22 -0800 Subject: [PATCH 1/2] completed csharp version --- 88_3-D_Tic-Tac-Toe/csharp/Program.cs | 10 + 88_3-D_Tic-Tac-Toe/csharp/Qubic.cs | 1178 ++++++++++++++++++++++++ 88_3-D_Tic-Tac-Toe/csharp/QubicData.cs | 559 +++++++++++ 3 files changed, 1747 insertions(+) create mode 100644 88_3-D_Tic-Tac-Toe/csharp/Program.cs create mode 100644 88_3-D_Tic-Tac-Toe/csharp/Qubic.cs create mode 100644 88_3-D_Tic-Tac-Toe/csharp/QubicData.cs diff --git a/88_3-D_Tic-Tac-Toe/csharp/Program.cs b/88_3-D_Tic-Tac-Toe/csharp/Program.cs new file mode 100644 index 00000000..12c6ca83 --- /dev/null +++ b/88_3-D_Tic-Tac-Toe/csharp/Program.cs @@ -0,0 +1,10 @@ +namespace ThreeDTicTacToe +{ + class Program + { + static void Main() + { + new Qubic().Run(); + } + } +} diff --git a/88_3-D_Tic-Tac-Toe/csharp/Qubic.cs b/88_3-D_Tic-Tac-Toe/csharp/Qubic.cs new file mode 100644 index 00000000..595db661 --- /dev/null +++ b/88_3-D_Tic-Tac-Toe/csharp/Qubic.cs @@ -0,0 +1,1178 @@ +using System.Text; + +namespace ThreeDTicTacToe +{ + /// + /// Qubic is a 3D Tic-Tac-Toe game played on a 4x4x4 cube. This code allows + /// a player to compete against a deterministic AI that is surprisingly + /// difficult to beat. + /// + internal class Qubic + { + // The Y variable in the original BASIC. + private static readonly int[] CornersAndCenters = QubicData.CornersAndCenters; + // The M variable in the original BASIC. + private static readonly int[,] RowsByPlane = QubicData.RowsByPlane; + + // Board spaces are filled in with numeric values. A space could be: + // + // - EMPTY: no one has moved here yet. + // - PLAYER: the player moved here. + // - MACHINE: the machine moved here. + // - POTENTIAL: the machine, in the middle of its move, + // might fill a space with a potential move marker, which + // prioritizes the space once it finally chooses where to move. + // + // The numeric values allow the program to determine what moves have + // been made in a row by summing the values in a row. In theory, the + // individual values could be any positive numbers that satisfy the + // following: + // + // - EMPTY = 0 + // - POTENTIAL * 4 < PLAYER + // - PLAYER * 4 < MACHINE + private const double PLAYER = 1.0; + private const double MACHINE = 5.0; + private const double POTENTIAL = 0.125; + private const double EMPTY = 0.0; + + // The X variable in the original BASIC. This is the Qubic board, + // flattened into a 1D array. + private readonly double[] Board = new double[64]; + + // The L variable in the original BASIC. There are 76 unique winning rows + // in the board, so each gets an entry in RowSums. A row sum can be used + // to check what moves have been made to that row in the board. + // + // Example: if RowSums[i] == PLAYER * 4, the player won with row i! + private readonly double[] RowSums = new double[76]; + + public Qubic() { } + + /// + /// Run the Qubic game. + /// + /// Show the title, prompt for instructions, then begin the game loop. + /// + public void Run() + { + Title(); + Instructions(); + Loop(); + } + + /*********************************************************************** + /* Terminal Text/Prompts + /**********************************************************************/ + #region TerminalText + + /// + /// Display title and attribution. + /// + /// Original BASIC: 50-120 + /// + private static void Title() + { + Console.WriteLine( + "\n" + + " QUBIC\n\n" + + " CREATIVE COMPUTING MORRISTOWN, NEW JERSEY\n\n\n" + ); + } + + /// + /// Prompt user for game instructions. + /// + /// Original BASIC: 210-313 + /// + private static void Instructions() + { + Console.Write("DO YOU WANT INSTRUCTIONS? "); + var yes = ReadYesNo(); + + if (yes) + { + Console.WriteLine( + "\n" + + "THE GAME IS TIC-TAC-TOE IN A 4 X 4 X 4 CUBE.\n" + + "EACH MOVE IS INDICATED BY A 3 DIGIT NUMBER, WITH EACH\n" + + "DIGIT BETWEEN 1 AND 4 INCLUSIVE. THE DIGITS INDICATE THE\n" + + "LEVEL, ROW, AND COLUMN, RESPECTIVELY, OF THE OCCUPIED\n" + + "PLACE.\n" + + "\n" + + "TO PRINT THE PLAYING BOARD, TYPE 0 (ZERO) AS YOUR MOVE.\n" + + "THE PROGRAM WILL PRINT THE BOARD WITH YOUR MOVES INDI-\n" + + "CATED WITH A (Y), THE MACHINE'S MOVES WITH AN (M), AND\n" + + "UNUSED SQUARES WITH A ( ). OUTPUT IS ON PAPER.\n" + + "\n" + + "TO STOP THE PROGRAM RUN, TYPE 1 AS YOUR MOVE.\n\n" + ); + } + } + + /// + /// Prompt player for whether they would like to move first, or allow + /// the machine to make the first move. + /// + /// Original BASIC: 440-490 + /// + /// true if the player wants to move first + private static bool PlayerMovePreference() + { + Console.Write("DO YOU WANT TO MOVE FIRST? "); + var result = ReadYesNo(); + Console.WriteLine(); + return result; + } + + /// + /// Run the Qubic program loop. + /// + private void Loop() + { + // The "retry" loop; ends if player quits or chooses not to retry + // after game ends. + while (true) + { + ClearBoard(); + var playerNext = PlayerMovePreference(); + + // The "game" loop; ends if player quits, player/machine wins, + // or game ends in draw. + while (true) + { + if (playerNext) + { + // Player makes a move. + var playerAction = PlayerMove(); + if (playerAction == PlayerAction.Move) + { + playerNext = !playerNext; + } + else + { + return; + } + } + else + { + // Check for wins, if any. + RefreshRowSums(); + if (CheckPlayerWin() || CheckMachineWin()) + { + break; + } + + // Machine makes a move. + var machineAction = MachineMove(); + if (machineAction == MachineAction.Move) + { + playerNext = !playerNext; + } + else if (machineAction == MachineAction.End) + { + break; + } + else + { + throw new Exception("unreachable; machine should always move or end game in game loop"); + } + } + } + + var retry = RetryPrompt(); + + if (!retry) + { + return; + } + } + } + + /// + /// Prompt the user to try another game. + /// + /// Original BASIC: 1490-1560 + /// + /// true if the user wants to play again + private static bool RetryPrompt() + { + Console.Write("DO YOU WANT TO TRY ANOTHER GAME? "); + return ReadYesNo(); + } + + /// + /// Read a yes/no from the terminal. This method accepts anything that + /// starts with N/n as no and Y/y as yes. + /// + /// true if the player answered yes + private static bool ReadYesNo() + { + while (true) + { + var response = Console.ReadLine() ?? " "; + if (response.ToLower().StartsWith("y")) + { + return true; + } + else if (response.ToLower().StartsWith("n")) + { + return false; + } + else + { + Console.Write("INCORRECT ANSWER. PLEASE TYPE 'YES' OR 'NO'. "); + } + } + } + + #endregion + + /*********************************************************************** + /* Player Move + /**********************************************************************/ + #region PlayerMove + + /// + /// Possible actions player has taken after ending their move. This + /// replaces the `GOTO` logic that allowed the player to jump out of + /// the game loop and quit. + /// + private enum PlayerAction + { + /// + /// The player ends the game prematurely. + /// + Quit, + /// + /// The player makes a move on the board. + /// + Move, + } + + /// + /// Make the player's move based on their input. + /// + /// Original BASIC: 500-620 + /// + /// Whether the player moved or quit the program. + private PlayerAction PlayerMove() + { + // Loop until a valid move is inputted. + while (true) + { + var move = ReadMove(); + if (move == 1) + { + return PlayerAction.Quit; + } + else if (move == 0) + { + ShowBoard(); + } + else + { + ClearPotentialMoves(); + if (TryCoordToIndex(move, out int moveIndex)) + { + if (Board[moveIndex] == EMPTY) + { + Board[moveIndex] = PLAYER; + return PlayerAction.Move; + } + else + { + Console.WriteLine("THAT SQUARE IS USED, TRY AGAIN."); + } + } + else + { + Console.WriteLine("INCORRECT MOVE, TRY AGAIN."); + } + } + } + } + + /// + /// Read a player move from the terminal. Move can be any integer. + /// + /// Original BASIC: 510-520 + /// + /// the move inputted + private static int ReadMove() + { + Console.Write("YOUR MOVE? "); + return ReadInteger(); + } + + /// + /// Read an integer from the terminal. + /// + /// Original BASIC: 520 + /// + /// Unlike the basic, this code will not accept any string that starts + /// with a number; only full number strings are allowed. + /// + /// the integer inputted + private static int ReadInteger() + { + while (true) + { + var response = Console.ReadLine() ?? " "; + + if (int.TryParse(response, out var move)) + { + return move; + + } + else + { + Console.Write("!NUMBER EXPECTED - RETRY INPUT LINE--? "); + } + } + } + + /// + /// Display the board to the player. Spaces taken by the player are + /// marked with "Y", while machine spaces are marked with "M". + /// + /// Original BASIC: 2550-2740 + /// + private void ShowBoard() + { + var s = new StringBuilder(new string('\n', 9)); + + for (int i = 1; i <= 4; i++) + { + for (int j = 1; j <= 4; j++) + { + s.Append(' ', 3 * (j + 1)); + for (int k = 1; k <= 4; k++) + { + int q = (16 * i) + (4 * j) + k - 21; + s.Append(Board[q] switch + { + EMPTY or POTENTIAL => "( ) ", + PLAYER => "(Y) ", + MACHINE => "(M) ", + _ => throw new Exception($"invalid space value {Board[q]}"), + }); + } + s.Append("\n\n"); + } + s.Append("\n\n"); + } + + Console.WriteLine(s.ToString()); + } + + #endregion + + /*********************************************************************** + /* Machine Move + /**********************************************************************/ + #region MachineMove + + /// + /// Check all rows for a player win. + /// + /// A row indicates a player win if its sum = PLAYER * 4. + /// + /// Original BASIC: 720-780 + /// + /// whether the player won in any row + private bool CheckPlayerWin() + { + for (int row = 0; row < 76; row++) + { + if (RowSums[row] == (PLAYER * 4)) + { + // Found player win! + Console.WriteLine("YOU WIN AS FOLLOWS"); + DisplayRow(row); + return true; + } + } + + // No player win found. + return false; + } + + /// + /// Check all rows for a row that the machine could move to to win + /// immediately. + /// + /// A row indicates a player could win immediately if it has three + /// machine moves already; that is, sum = MACHINE * 3. + /// + /// Original Basic: 790-920 + /// + /// + private bool CheckMachineWin() + { + for (int row = 0; row < 76; row++) + { + if (RowSums[row] == (MACHINE * 3)) + { + // Found a winning row! + for (int space = 0; space < 4; space++) + { + int move = RowsByPlane[row, space]; + if (Board[move] == EMPTY) + { + // Found empty space in winning row; move there. + Board[move] = MACHINE; + Console.WriteLine($"MACHINE MOVES TO {IndexToCoord(move)} , AND WINS AS FOLLOWS"); + DisplayRow(row); + return true; + } + } + } + } + + // No winning row available. + return false; + } + + /// + /// Display the coordinates of a winning row. + /// + /// index into RowsByPlane data + private void DisplayRow(int row) + { + for (int space = 0; space < 4; space++) + { + Console.Write($" {IndexToCoord(RowsByPlane[row, space])} "); + } + Console.WriteLine(); + } + + /// + /// Possible actions machine can take in a move. This helps replace the + /// complex GOTO logic from the original BASIC, which allowed the + /// program to jump from the machine's action to the end of the game. + /// + private enum MachineAction + { + /// + /// Machine did not take any action. + /// + None, + /// + /// Machine made a move. + /// + Move, + /// + /// Machine either won, conceded, or found a draw. + /// + End, + } + + /// + /// Machine decides where to move on the board, and ends the game if + /// appropriate. + /// + /// The machine's AI tries to take the following actions (in order): + /// + /// 1. If the player has a row that will get them the win on their + /// next turn, block that row. + /// 2. If the machine can trap the player (create two different rows + /// with three machine moves each that cannot be blocked by only a + /// single player move, create such a trap. + /// 3. If the player can create a similar trap for the machine on + /// their next move, block the space where that trap would be + /// created. + /// 4. Find a plane in the board that is well-populated by player + /// moves, and take a space in the first such plane. + /// 5. Find the first open corner or center and move there. + /// 6. Find the first open space and move there. + /// + /// If none of these actions are possible, then the board is entirely + /// full, and the game results in a draw. + /// + /// Original BASIC: start at 930 + /// + /// the action the machine took + private MachineAction MachineMove() + { + // The actions the machine attempts to take, in order. + var actions = new Func[] + { + BlockPlayer, + MakePlayerTrap, + BlockMachineTrap, + MoveByPlane, + MoveCornerOrCenter, + MoveAnyOpenSpace, + }; + + foreach (var action in actions) + { + // Try each action, moving to the next if nothing happens. + var actionResult = action(); + if (actionResult != MachineAction.None) + { + // Not in original BASIC: check for draw after each machine + // move. + if (CheckDraw()) + { + return DrawGame(); + } + return actionResult; + } + } + + // If we got here, all spaces are taken. Draw the game. + return DrawGame(); + } + + /// + /// Block a row with three spaces already taken by the player. + /// + /// Original BASIC: 930-1010 + /// + /// + /// Move if the machine blocked, + /// None otherwise + /// + private MachineAction BlockPlayer() + { + for (int row = 0; row < 76; row++) + { + if (RowSums[row] == (PLAYER * 3)) + { + // Found a row to block on! + for (int space = 0; space < 4; space++) + { + if (Board[RowsByPlane[row, space]] == EMPTY) + { + // Take the remaining empty space. + Board[RowsByPlane[row, space]] = MACHINE; + Console.WriteLine($"NICE TRY. MACHINE MOVES TO {IndexToCoord(RowsByPlane[row, space])}"); + return MachineAction.Move; + } + } + } + } + + // Didn't find a row to block on. + return MachineAction.None; + } + + /// + /// Create a trap for the player if possible. A trap can be created if + /// moving to a space on the board results in two different rows having + /// three MACHINE spaces, with the remaining space not shared between + /// the two rows. The player can only block one of these traps, so the + /// machine will win. + /// + /// If a player trap is not possible, but a row is found that is + /// particularly advantageous for the machine to move to, the machine + /// will try and move to a corner-edge in that row. + /// + /// Original BASIC: 1300-1480 + /// + /// + /// Move if a trap was created, + /// End if the machine conceded, + /// None otherwise + /// + private MachineAction MakePlayerTrap() + { + for (int row = 0; row < 76; row++) + { + // Refresh row sum, since new POTENTIALs might have changed it. + var rowSum = RefreshRowSum(row); + + // Machine has moved in this row twice, and player has not moved + // in this row. + if (rowSum >= (MACHINE * 2) && rowSum < (MACHINE * 2) + 1) + { + // Machine has no potential moves yet in this row. + if (rowSum == (MACHINE * 2)) + { + for (int space = 0; space < 4; space++) + { + // Empty space can potentially be used to create a + // trap. + if (Board[RowsByPlane[row, space]] == EMPTY) + { + Board[RowsByPlane[row, space]] = POTENTIAL; + } + } + } + // Machine has already found a potential move in this row, + // so a trap can be created with another row. + else + { + return MakeOrBlockTrap(row); + } + } + } + + // No player traps can be made. + RefreshRowSums(); + + for (int row = 0; row < 76; row++) + { + // A row may be particularly advantageous for the machine to + // move to at this point; this is the case if a row is entirely + // filled with POTENTIAL or has one MACHINE and others + // POTENTIAL. + if (RowSums[row] == (POTENTIAL * 4) || RowSums[row] == MACHINE + (POTENTIAL * 3)) + { + // Try moving to a corner-edge in an advantageous row. + return MoveCornerEdges(row, POTENTIAL); + } + } + + // No spaces found that are particularly advantageous to machine. + ClearPotentialMoves(); + return MachineAction.None; + } + + /// + /// Block a trap that the player could create for the machine on their + /// next turn. + /// + /// If there are no player traps to block, but a row is found that is + /// particularly advantageous for the player to move to, the machine + /// will try and move to a corner-edge in that row. + /// + /// Original BASIC: 1030-1190 + /// + /// + /// Move if a trap was created, + /// End if the machine conceded, + /// None otherwise + /// + private MachineAction BlockMachineTrap() + { + for (int i = 0; i < 76; i++) + { + // Refresh row sum, since new POTENTIALs might have changed it. + var rowSum = RefreshRowSum(i); + + // Player has moved in this row twice, and machine has not moved + // in this row. + if (rowSum >= (PLAYER * 2) && rowSum < (PLAYER * 2) + 1) + { + // Machine has no potential moves yet in this row. + if (rowSum == (PLAYER * 2)) + { + for (int j = 0; j < 4; j++) + { + if (Board[RowsByPlane[i, j]] == EMPTY) + { + Board[RowsByPlane[i, j]] = POTENTIAL; + } + } + } + // Machine has already found a potential move in this row, + // so a trap can be created with another row by the player. + // Move to block. + else + { + return MakeOrBlockTrap(i); + } + } + } + + // No player traps to block found. + RefreshRowSums(); + + for (int row = 0; row < 76; row++) + { + // A row may be particularly advantageous for the player to move + // to at this point, indicated by a row containing all POTENTIAL + // moves or one PLAYER and rest POTENTIAL. + if (RowSums[row] == (POTENTIAL * 4) || RowSums[row] == PLAYER + (POTENTIAL * 3)) + { + // Try moving to a corner-edge in an advantageous row. + return MoveCornerEdges(row, POTENTIAL); + } + } + + // No spaces found that are particularly advantageous to the player. + return MachineAction.None; + } + + /// + /// Either make a trap for the player or block a trap the player could + /// create on their next turn. + /// + /// Unclear how this method could possibly end with a concession; it + /// seems it can only be called if the row contains a potential move. + /// + /// Original BASIC: 2230-2350 + /// + /// the row containing the space to move to + /// + /// Move if the machine moved, + /// End if the machine conceded + /// + private MachineAction MakeOrBlockTrap(int row) + { + for (int space = 0; space < 4; space++) + { + if (Board[RowsByPlane[row, space]] == POTENTIAL) + { + Board[RowsByPlane[row, space]] = MACHINE; + + // Row sum indicates we're blocking a player trap. + if (RowSums[row] < MACHINE) + { + Console.Write("YOU FOX. JUST IN THE NICK OF TIME, "); + } + // Row sum indicates we're completing a machine trap. + else + { + Console.Write("LET'S SEE YOU GET OUT OF THIS: "); + } + + Console.WriteLine($"MACHINE MOVES TO {IndexToCoord(RowsByPlane[row, space])}"); + + return MachineAction.Move; + } + } + + // Unclear how this can be reached. + Console.WriteLine("MACHINE CONCEDES THIS GAME."); + return MachineAction.End; + } + + /// + /// Find a satisfactory plane on the board and move to one if that + /// plane's corner-edges. + /// + /// A plane on the board is satisfactory if it meets the following + /// conditions: + /// 1. Player has made exactly 4 moves on the plane. + /// 2. Machine has made either 0 or one moves on the plane. + /// The machine then attempts to move to a corner-edge in that plane, + /// first finding any potential moves from the previous action it took + /// and moving there, and moving to any open corner-edge if there are + /// no potential moves found. + /// + /// This action by the machine tries to prevent the player from having + /// exclusive control over any plane in the board. + /// + /// Original BASIC: 1830-2020 + /// + /// The BASIC code for this action is tightly bound with base code for + /// MoveCornerEdges. + /// + /// + /// Move if a move in a plane was found, + /// None otherwise + /// + private MachineAction MoveByPlane() + { + // For each plane in the cube... + for (int plane = 1; plane <= 18; plane++) + { + double planeSum = PlaneSum(plane); + + // Check that plane sum satisfies condition. + const double P4 = PLAYER * 4; + const double P4_M1 = (PLAYER * 4) + MACHINE; + if ( + (planeSum >= P4 && planeSum < P4 + 1) || + (planeSum >= P4_M1 && planeSum < P4_M1 + 1) + ) + { + // Try to move to corner edges in each row of plane + // First, check for corner edges marked as POTENTIAL. + for (int row = (4 * plane) - 4; row < (4 * plane); row++) + { + var moveResult = MoveCornerEdges(row, POTENTIAL); + if (moveResult != MachineAction.None) + { + return moveResult; + } + } + + // If no POTENTIAL corner-edge found, look for an EMPTY one. + for (int row = (4 * plane) - 4; row < (4 * plane); row++) + { + var moveResult = MoveCornerEdges(row, EMPTY); + if (moveResult != MachineAction.None) + { + return moveResult; + } + } + } + } + + // No good corner edges found by plane. + ClearPotentialMoves(); + return MachineAction.None; + } + + /// + /// Given a row, move to the first corner-edge of the cube that has the + /// given value. + /// + /// Corner edges are pieces of the cube that have two faces. The AI + /// prefers to move to these spaces before others, presumably + /// because they are powerful moves: a corner edge space is contained + /// in 3 rows on the cube. + /// + /// Original BASIC: 2360-2490 + /// + /// In the original BASIC, this code is pointed to from three different + /// locations by GOTOs (1440/50, or MakePlayerTrap; 1160/70, or + /// BlockMachineTrap; and 1990, or MoveByPlane). Interestingly, line + /// 2440 can only be reached if the code proceeds after a call from + /// 1990. In short, this means that the code flow is incredibly + /// difficult to understand; the block of code at 2360 acts like a + /// generalized subroutine, but contains bits of code that belong + /// to specific pieces of calling code. + /// + /// the row to try to move to + /// + /// what value the space to move to should have in Board + /// + /// + /// Move if a corner-edge piece in the row with the given spaceValue was + /// found, + /// None otherwise + /// + private MachineAction MoveCornerEdges(int row, double spaceValue) + { + // Given a row, we want to find the corner-edge pieces in that row. + // We know that each row is part of a plane, and that the first + // and last rows of the plane are on the plane edge, while the + // other two rows are in the middle. If we know whether a row is an + // edge or middle, we can determine which spaces in that row are + // corner-edges. + // + // Below is a birds-eye view of a plane in the cube, with rows + // oriented horizontally: + // + // row 0: ( ) (1) (2) ( ) + // row 1: (0) ( ) ( ) (3) + // row 2: (0) ( ) ( ) (3) + // row 3: ( ) (1) (2) ( ) + // + // The corner edge pieces have their row indices marked. The pattern + // above shows that: + // + // if row == 0 | 3, corner edge spaces = [1, 2] + // if row == 1 | 2, corner edge spaces = [0, 3] + + // The below condition replaces the following BASIC code (2370): + // + // I-(INT(I/4)*4)>1 + // + // which in C# would be: + // + // + // int a = i - (i / 4) * 4 <= 1) + // ? 1 + // : 2; + // + // In the above, i is the one-indexed row in RowsByPlane. + // + // This condition selects a different a value based on whether the + // given row is on the edge or middle of its plane. + int a = (row % 4) switch + { + 0 or 3 => 1, // row is on edge of plane + 1 or 2 => 2, // row is in middle of plane + _ => throw new Exception($"unreachable ({row % 4})"), + }; + + // Iterate through corner edge pieces of the row. + // + // if a = 1 (row is edge), iterate through [0, 3] + // if a = 2 (row is middle), iterate through [1, 2] + for (int space = a - 1; space <= 4 - a; space += 5 - (2 * a)) + { + if (Board[RowsByPlane[row, space]] == spaceValue) + { + // Found a corner-edge to take! + Board[RowsByPlane[row, space]] = MACHINE; + Console.WriteLine($"MACHINE TAKES {IndexToCoord(RowsByPlane[row, space])}"); + return MachineAction.Move; + } + } + + // No valid corner edge to take. + return MachineAction.None; + } + + /// + /// Find the first open corner or center in the board and move there. + /// + /// Original BASIC: 1200-1290 + /// + /// This is the only place where the Z variable from the BASIC code is + /// used; here it is implied in the for loop. + /// + /// + /// Move if an open corner/center was found and moved to, + /// None otherwise + /// + private MachineAction MoveCornerOrCenter() + { + foreach (int space in CornersAndCenters) + { + if (Board[space] == EMPTY) + { + Board[space] = MACHINE; + Console.WriteLine($"MACHINE MOVES TO {IndexToCoord(space)}"); + return MachineAction.Move; + } + } + + return MachineAction.None; + } + + /// + /// Find the first open space in the board and move there. + /// + /// Original BASIC: 1720-1800 + /// + /// + /// Move if an open space was found and moved to, + /// None otherwise + /// + private MachineAction MoveAnyOpenSpace() + { + for (int row = 0; row < 64; row++) + { + if (Board[row] == EMPTY) + { + Board[row] = MACHINE; + Console.WriteLine($"MACHINE LIKES {IndexToCoord(row)}"); + return MachineAction.Move; + } + } + return MachineAction.None; + } + + /// + /// Draw the game in the event that there are no open spaces. + /// + /// Original BASIC: 1810-1820 + /// + /// End + private MachineAction DrawGame() + { + Console.WriteLine("THIS GAME IS A DRAW."); + return MachineAction.End; + } + + #endregion + + /*********************************************************************** + /* Helpers + /**********************************************************************/ + #region Helpers + + /// + /// Attempt to transform a cube coordinate to an index into Board. + /// + /// A valid cube coordinate is a three-digit number, where each digit + /// of the number X satisfies 1 <= X <= 4. + /// + /// Examples: + /// 111 -> 0 + /// 444 -> 63 + /// 232 -> 35 + /// + /// If the coord provided is not valid, the transformation fails. + /// + /// The conversion from coordinate to index is essentially a conversion + /// between base 4 and base 10. + /// + /// Original BASIC: 525-580 + /// + /// This method fixes a bug in the original BASIC (525-526), which only + /// checked whether the given coord satisfied 111 <= coord <= 444. This + /// allows invalid coordinates such as 199 and 437, whose individual + /// digits are out of range. + /// + /// cube coordinate (e.g. "111", "342") + /// trasnformation output + /// + /// true if the transformation was successful, false otherwise + /// + private static bool TryCoordToIndex(int coord, out int index) + { + // parse individual digits, subtract 1 to get base 4 number + var hundreds = (coord / 100) - 1; + var tens = ((coord % 100) / 10) - 1; + var ones = (coord % 10) - 1; + + // bounds check for each digit + foreach (int digit in new int[] { hundreds, tens, ones }) + { + if (digit < 0 || digit > 3) + { + index = -1; + return false; + } + } + + // conversion from base 4 to base 10 + index = (16 * hundreds) + (4 * tens) + ones; + return true; + } + + /// + /// Transform a Board index into a valid cube coordinate. + /// + /// Examples: + /// 0 -> 111 + /// 63 -> 444 + /// 35 -> 232 + /// + /// The conversion from index to coordinate is essentially a conversion + /// between base 10 and base 4. + /// + /// Original BASIC: 1570-1610 + /// + /// Board index + /// the corresponding cube coordinate + private static int IndexToCoord(int index) + { + // check that index is valid + if (index < 0 || index > 63) + { + // runtime exception; all uses of this method are with + // indices provided by the program, so this should never fail + throw new Exception($"index {index} is out of range"); + } + + // convert to base 4, add 1 to get cube coordinate + var hundreds = (index / 16) + 1; + var tens = ((index % 16) / 4) + 1; + var ones = (index % 4) + 1; + + // concatenate digits + int coord = (hundreds * 100) + (tens * 10) + ones; + return coord; + } + + /// + /// Refresh the values in RowSums to account for any changes. + /// + /// Original BASIC: 1640-1710 + /// + private void RefreshRowSums() + { + for (var row = 0; row < 76; row++) + { + RefreshRowSum(row); + } + } + + /// + /// Refresh a row in RowSums to reflect changes. + /// + /// row in RowSums to refresh + /// row sum after refresh + private double RefreshRowSum(int row) + { + double rowSum = 0; + for (int space = 0; space < 4; space++) + { + rowSum += Board[RowsByPlane[row, space]]; + } + RowSums[row] = rowSum; + return rowSum; + } + + /// + /// Calculate the sum of spaces in one of the 18 cube planes in RowSums. + /// + /// Original BASIC: 1840-1890 + /// + /// the desired plane + /// sum of spaces in plane + private double PlaneSum(int plane) + { + double planeSum = 0; + for (int row = (4 * (plane - 1)); row < (4 * plane); row++) + { + for (int space = 0; space < 4; space++) + { + planeSum += Board[RowsByPlane[row, space]]; + } + } + return planeSum; + } + + /// + /// Check whether the board is in a draw state, that is all spaces are + /// full and neither the player nor the machine has won. + /// + /// The original BASIC contains a bug that if the player moves first, a + /// draw will go undetected. An example series of player inputs + /// resulting in such a draw (assuming player goes first): + /// + /// 114, 414, 144, 444, 122, 221, 112, 121, + /// 424, 332, 324, 421, 231, 232, 244, 311, + /// 333, 423, 331, 134, 241, 243, 143, 413, + /// 142, 212, 314, 341, 432, 412, 431, 442 + /// + /// whether the game is a draw + private bool CheckDraw() + { + for (var i = 0; i < 64; i++) + { + if (Board[i] != PLAYER && Board[i] != MACHINE) + { + return false; + } + } + + RefreshRowSums(); + + for (int row = 0; row < 76; row++) + { + var rowSum = RowSums[row]; + if (rowSum == PLAYER * 4 || rowSum == MACHINE * 4) + { + return false; + } + } + + + return true; + } + + /// + /// Reset POTENTIAL spaces in Board to EMPTY. + /// + /// Original BASIC: 2500-2540 + /// + private void ClearPotentialMoves() + { + for (var i = 0; i < 64; i++) + { + if (Board[i] == POTENTIAL) + { + Board[i] = EMPTY; + } + } + } + + /// + /// Reset all spaces in Board to EMPTY. + /// + /// Original BASIC: 400-420 + /// + private void ClearBoard() + { + for (var i = 0; i < 64; i++) + { + Board[i] = EMPTY; + } + } + + #endregion + } +} diff --git a/88_3-D_Tic-Tac-Toe/csharp/QubicData.cs b/88_3-D_Tic-Tac-Toe/csharp/QubicData.cs new file mode 100644 index 00000000..298ee933 --- /dev/null +++ b/88_3-D_Tic-Tac-Toe/csharp/QubicData.cs @@ -0,0 +1,559 @@ +namespace ThreeDTicTacToe +{ + /// + /// Data in this class was originally given by the following DATA section in + /// the BASIC program: + /// + /// 2030 DATA 1,49,52,4,13,61,64,16,22,39,23,38,26,42,27,43 + /// 2040 DATA 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20 + /// 2050 DATA 21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38 + /// 2060 DATA 39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56 + /// 2070 DATA 57,58,59,60,61,62,63,64 + /// 2080 DATA 1,17,33,49,5,21,37,53,9,25,41,57,13,29,45,61 + /// 2090 DATA 2,18,34,50,6,22,38,54,10,26,42,58,14,30,46,62 + /// 2100 DATA 3,19,35,51,7,23,39,55,11,27,43,59,15,31,47,63 + /// 2110 DATA 4,20,36,52,8,24,40,56,12,28,44,60,16,32,48,64 + /// 2120 DATA 1,5,9,13,17,21,25,29,33,37,41,45,49,53,57,61 + /// 2130 DATA 2,6,10,14,18,22,26,30,34,38,42,46,50,54,58,62 + /// 2140 DATA 3,7,11,15,19,23,27,31,35,39,43,47,51,55,59,63 + /// 2150 DATA 4,8,12,16,20,24,28,32,36,40,44,48,52,56,60,64 + /// 2160 DATA 1,6,11,16,17,22,27,32,33,38,43,48,49,54,59,64 + /// 2170 DATA 13,10,7,4,29,26,23,20,45,42,39,36,61,58,55,52 + /// 2180 DATA 1,21,41,61,2,22,42,62,3,23,43,63,4,24,44,64 + /// 2190 DATA 49,37,25,13,50,38,26,14,51,39,27,15,52,40,28,16 + /// 2200 DATA 1,18,35,52,5,22,39,56,9,26,43,60,13,30,47,64 + /// 2210 DATA 49,34,19,4,53,38,23,8,57,42,27,12,61,46,31,16 + /// 2220 DATA 1,22,43,64,16,27,38,49,4,23,42,61,13,26,39,52 + /// + /// In short, each number is an index into the board. The data in this class + /// is zero-indexed, as opposed to the original data which was one-indexed. + /// + internal static class QubicData + { + /// + /// The corners and centers of the Qubic board. They correspond to the + /// following coordinates: + /// + /// [ + /// 111, 411, 414, 114, 141, 441, 444, 144, + /// 222, 323, 223, 322, 232, 332, 233, 333 + /// ] + /// + public static readonly int[] CornersAndCenters = new int[16] + { + // (X) ( ) ( ) (X) + // ( ) ( ) ( ) ( ) + // ( ) ( ) ( ) ( ) + // (X) ( ) ( ) (X) + + // ( ) ( ) ( ) ( ) + // ( ) (X) (X) ( ) + // ( ) (X) (X) ( ) + // ( ) ( ) ( ) ( ) + + // ( ) ( ) ( ) ( ) + // ( ) (X) (X) ( ) + // ( ) (X) (X) ( ) + // ( ) ( ) ( ) ( ) + + // (X) ( ) ( ) (X) + // ( ) ( ) ( ) ( ) + // ( ) ( ) ( ) ( ) + // (X) ( ) ( ) (X) + + 0,48,51,3,12,60,63,15,21,38,22,37,25,41,26,42 + }; + + /// + /// A list of all "winning" rows in the Qubic board; that is, sets of + /// four spaces that, if filled entirely by the player (or machine), + /// would result in a win. + /// + /// Each group of four rows in the list corresponds to a plane in the + /// cube, and each plane is organized so that the first and last rows + /// are on the plane's edges, while the second and third rows are in + /// the middle of the plane. The only exception is the last group of + /// rows, which contains the corners and centers rather than a plane. + /// + /// The order of the rows in this list is key to how the Qubic AI + /// decides its next move. + /// + public static readonly int[,] RowsByPlane = new int[76, 4] + { + // (1) (1) (1) (1) + // (2) (2) (2) (2) + // (3) (3) (3) (3) + // (4) (4) (4) (4) + + // ( ) ( ) ( ) ( ) + // ( ) ( ) ( ) ( ) + // ( ) ( ) ( ) ( ) + // ( ) ( ) ( ) ( ) + + // ( ) ( ) ( ) ( ) + // ( ) ( ) ( ) ( ) + // ( ) ( ) ( ) ( ) + // ( ) ( ) ( ) ( ) + + // ( ) ( ) ( ) ( ) + // ( ) ( ) ( ) ( ) + // ( ) ( ) ( ) ( ) + // ( ) ( ) ( ) ( ) + + { 0, 1, 2, 3, }, + { 4, 5, 6, 7, }, + { 8, 9, 10,11, }, + { 12,13,14,15, }, + + // ( ) ( ) ( ) ( ) + // ( ) ( ) ( ) ( ) + // ( ) ( ) ( ) ( ) + // ( ) ( ) ( ) ( ) + + // (1) (1) (1) (1) + // (2) (2) (2) (2) + // (3) (3) (3) (3) + // (4) (4) (4) (4) + + // ( ) ( ) ( ) ( ) + // ( ) ( ) ( ) ( ) + // ( ) ( ) ( ) ( ) + // ( ) ( ) ( ) ( ) + + // ( ) ( ) ( ) ( ) + // ( ) ( ) ( ) ( ) + // ( ) ( ) ( ) ( ) + // ( ) ( ) ( ) ( ) + + { 16,17,18,19, }, + { 20,21,22,23, }, + { 24,25,26,27, }, + { 28,29,30,31, }, + + // ( ) ( ) ( ) ( ) + // ( ) ( ) ( ) ( ) + // ( ) ( ) ( ) ( ) + // ( ) ( ) ( ) ( ) + + // ( ) ( ) ( ) ( ) + // ( ) ( ) ( ) ( ) + // ( ) ( ) ( ) ( ) + // ( ) ( ) ( ) ( ) + + // (1) (1) (1) (1) + // (2) (2) (2) (2) + // (3) (3) (3) (3) + // (4) (4) (4) (4) + + // ( ) ( ) ( ) ( ) + // ( ) ( ) ( ) ( ) + // ( ) ( ) ( ) ( ) + // ( ) ( ) ( ) ( ) + + { 32,33,34,35, }, + { 36,37,38,39, }, + { 40,41,42,43, }, + { 44,45,46,47, }, + + // ( ) ( ) ( ) ( ) + // ( ) ( ) ( ) ( ) + // ( ) ( ) ( ) ( ) + // ( ) ( ) ( ) ( ) + + // ( ) ( ) ( ) ( ) + // ( ) ( ) ( ) ( ) + // ( ) ( ) ( ) ( ) + // ( ) ( ) ( ) ( ) + + // ( ) ( ) ( ) ( ) + // ( ) ( ) ( ) ( ) + // ( ) ( ) ( ) ( ) + // ( ) ( ) ( ) ( ) + + // (1) (1) (1) (1) + // (2) (2) (2) (2) + // (3) (3) (3) (3) + // (4) (4) (4) (4) + + { 48,49,50,51, }, + { 52,53,54,55, }, + { 56,57,58,59, }, + { 60,61,62,63, }, + + // (1) ( ) ( ) ( ) + // (2) ( ) ( ) ( ) + // (3) ( ) ( ) ( ) + // (4) ( ) ( ) ( ) + + // (1) ( ) ( ) ( ) + // (2) ( ) ( ) ( ) + // (3) ( ) ( ) ( ) + // (4) ( ) ( ) ( ) + + // (1) ( ) ( ) ( ) + // (2) ( ) ( ) ( ) + // (3) ( ) ( ) ( ) + // (4) ( ) ( ) ( ) + + // (1) ( ) ( ) ( ) + // (2) ( ) ( ) ( ) + // (3) ( ) ( ) ( ) + // (4) ( ) ( ) ( ) + + { 0, 16,32,48, }, + { 4, 20,36,52, }, + { 8, 24,40,56, }, + { 12,28,44,60, }, + + // ( ) (1) ( ) ( ) + // ( ) (2) ( ) ( ) + // ( ) (3) ( ) ( ) + // ( ) (4) ( ) ( ) + + // ( ) (1) ( ) ( ) + // ( ) (2) ( ) ( ) + // ( ) (3) ( ) ( ) + // ( ) (4) ( ) ( ) + + // ( ) (1) ( ) ( ) + // ( ) (2) ( ) ( ) + // ( ) (3) ( ) ( ) + // ( ) (4) ( ) ( ) + + // ( ) (1) ( ) ( ) + // ( ) (2) ( ) ( ) + // ( ) (3) ( ) ( ) + // ( ) (4) ( ) ( ) + + { 1, 17,33,49, }, + { 5, 21,37,53, }, + { 9, 25,41,57, }, + { 13,29,45,61, }, + + // ( ) ( ) (1) ( ) + // ( ) ( ) (2) ( ) + // ( ) ( ) (3) ( ) + // ( ) ( ) (4) ( ) + + // ( ) ( ) (1) ( ) + // ( ) ( ) (2) ( ) + // ( ) ( ) (3) ( ) + // ( ) ( ) (4) ( ) + + // ( ) ( ) (1) ( ) + // ( ) ( ) (2) ( ) + // ( ) ( ) (3) ( ) + // ( ) ( ) (4) ( ) + + // ( ) ( ) (1) ( ) + // ( ) ( ) (2) ( ) + // ( ) ( ) (3) ( ) + // ( ) ( ) (4) ( ) + + { 2, 18,34,50, }, + { 6, 22,38,54, }, + { 10,26,42,58, }, + { 14,30,46,62, }, + + // ( ) ( ) ( ) (1) + // ( ) ( ) ( ) (2) + // ( ) ( ) ( ) (3) + // ( ) ( ) ( ) (4) + + // ( ) ( ) ( ) (1) + // ( ) ( ) ( ) (2) + // ( ) ( ) ( ) (3) + // ( ) ( ) ( ) (4) + + // ( ) ( ) ( ) (1) + // ( ) ( ) ( ) (2) + // ( ) ( ) ( ) (3) + // ( ) ( ) ( ) (4) + + // ( ) ( ) ( ) (1) + // ( ) ( ) ( ) (2) + // ( ) ( ) ( ) (3) + // ( ) ( ) ( ) (4) + + { 3, 19,35,51, }, + { 7, 23,39,55, }, + { 11,27,43,59, }, + { 15,31,47,63, }, + + // (1) ( ) ( ) ( ) + // (1) ( ) ( ) ( ) + // (1) ( ) ( ) ( ) + // (1) ( ) ( ) ( ) + + // (2) ( ) ( ) ( ) + // (2) ( ) ( ) ( ) + // (2) ( ) ( ) ( ) + // (2) ( ) ( ) ( ) + + // (3) ( ) ( ) ( ) + // (3) ( ) ( ) ( ) + // (3) ( ) ( ) ( ) + // (3) ( ) ( ) ( ) + + // (4) ( ) ( ) ( ) + // (4) ( ) ( ) ( ) + // (4) ( ) ( ) ( ) + // (4) ( ) ( ) ( ) + + { 0, 4, 8, 12, }, + { 16,20,24,28, }, + { 32,36,40,44, }, + { 48,52,56,60, }, + + // ( ) (1) ( ) ( ) + // ( ) (1) ( ) ( ) + // ( ) (1) ( ) ( ) + // ( ) (1) ( ) ( ) + + // ( ) (2) ( ) ( ) + // ( ) (2) ( ) ( ) + // ( ) (2) ( ) ( ) + // ( ) (2) ( ) ( ) + + // ( ) (3) ( ) ( ) + // ( ) (3) ( ) ( ) + // ( ) (3) ( ) ( ) + // ( ) (3) ( ) ( ) + + // ( ) (4) ( ) ( ) + // ( ) (4) ( ) ( ) + // ( ) (4) ( ) ( ) + // ( ) (4) ( ) ( ) + + { 1, 5, 9, 13, }, + { 17,21,25,29, }, + { 33,37,41,45, }, + { 49,53,57,61, }, + + // ( ) ( ) (1) ( ) + // ( ) ( ) (1) ( ) + // ( ) ( ) (1) ( ) + // ( ) ( ) (1) ( ) + + // ( ) ( ) (2) ( ) + // ( ) ( ) (2) ( ) + // ( ) ( ) (2) ( ) + // ( ) ( ) (2) ( ) + + // ( ) ( ) (3) ( ) + // ( ) ( ) (3) ( ) + // ( ) ( ) (3) ( ) + // ( ) ( ) (3) ( ) + + // ( ) ( ) (4) ( ) + // ( ) ( ) (4) ( ) + // ( ) ( ) (4) ( ) + // ( ) ( ) (4) ( ) + + { 2, 6, 10,14, }, + { 18,22,26,30, }, + { 34,38,42,46, }, + { 50,54,58,62, }, + + // ( ) ( ) ( ) (1) + // ( ) ( ) ( ) (1) + // ( ) ( ) ( ) (1) + // ( ) ( ) ( ) (1) + + // ( ) ( ) ( ) (2) + // ( ) ( ) ( ) (2) + // ( ) ( ) ( ) (2) + // ( ) ( ) ( ) (2) + + // ( ) ( ) ( ) (3) + // ( ) ( ) ( ) (3) + // ( ) ( ) ( ) (3) + // ( ) ( ) ( ) (3) + + // ( ) ( ) ( ) (4) + // ( ) ( ) ( ) (4) + // ( ) ( ) ( ) (4) + // ( ) ( ) ( ) (4) + + { 3, 7, 11,15, }, + { 19,23,27,31, }, + { 35,39,43,47, }, + { 51,55,59,63, }, + + // (1) ( ) ( ) ( ) + // ( ) (1) ( ) ( ) + // ( ) ( ) (1) ( ) + // ( ) ( ) ( ) (1) + + // (2) ( ) ( ) ( ) + // ( ) (2) ( ) ( ) + // ( ) ( ) (2) ( ) + // ( ) ( ) ( ) (2) + + // (3) ( ) ( ) ( ) + // ( ) (3) ( ) ( ) + // ( ) ( ) (3) ( ) + // ( ) ( ) ( ) (3) + + // (4) ( ) ( ) ( ) + // ( ) (4) ( ) ( ) + // ( ) ( ) (4) ( ) + // ( ) ( ) ( ) (4) + + { 0, 5, 10,15, }, + { 16,21,26,31, }, + { 32,37,42,47, }, + { 48,53,58,63, }, + + // ( ) ( ) ( ) (1) + // ( ) ( ) (1) ( ) + // ( ) (1) ( ) ( ) + // (1) ( ) ( ) ( ) + + // ( ) ( ) ( ) (2) + // ( ) ( ) (2) ( ) + // ( ) (2) ( ) ( ) + // (2) ( ) ( ) ( ) + + // ( ) ( ) ( ) (3) + // ( ) ( ) (3) ( ) + // ( ) (3) ( ) ( ) + // (3) ( ) ( ) ( ) + + // ( ) ( ) ( ) (4) + // ( ) ( ) (4) ( ) + // ( ) (4) ( ) ( ) + // (4) ( ) ( ) ( ) + + { 12,9, 6, 3, }, + { 28,25,22,19, }, + { 44,41,38,35, }, + { 60,57,54,51, }, + + // (1) (2) (3) (4) + // ( ) ( ) ( ) ( ) + // ( ) ( ) ( ) ( ) + // ( ) ( ) ( ) ( ) + + // ( ) ( ) ( ) ( ) + // (1) (2) (3) (4) + // ( ) ( ) ( ) ( ) + // ( ) ( ) ( ) ( ) + + // ( ) ( ) ( ) ( ) + // ( ) ( ) ( ) ( ) + // (1) (2) (3) (4) + // ( ) ( ) ( ) ( ) + + // ( ) ( ) ( ) ( ) + // ( ) ( ) ( ) ( ) + // ( ) ( ) ( ) ( ) + // (1) (2) (3) (4) + + { 0, 20,40,60, }, + { 1, 21,41,61, }, + { 2, 22,42,62, }, + { 3, 23,43,63, }, + + // ( ) ( ) ( ) ( ) + // ( ) ( ) ( ) ( ) + // ( ) ( ) ( ) ( ) + // (1) (2) (3) (4) + + // ( ) ( ) ( ) ( ) + // ( ) ( ) ( ) ( ) + // (1) (2) (3) (4) + // ( ) ( ) ( ) ( ) + + // ( ) ( ) ( ) ( ) + // (1) (2) (3) (4) + // ( ) ( ) ( ) ( ) + // ( ) ( ) ( ) ( ) + + // (1) (2) (3) (4) + // ( ) ( ) ( ) ( ) + // ( ) ( ) ( ) ( ) + // ( ) ( ) ( ) ( ) + + { 48,36,24,12, }, + { 49,37,25,13, }, + { 50,38,26,14, }, + { 51,39,27,15, }, + + // (1) ( ) ( ) ( ) + // (2) ( ) ( ) ( ) + // (3) ( ) ( ) ( ) + // (4) ( ) ( ) ( ) + + // ( ) (1) ( ) ( ) + // ( ) (2) ( ) ( ) + // ( ) (3) ( ) ( ) + // ( ) (4) ( ) ( ) + + // ( ) ( ) (1) ( ) + // ( ) ( ) (2) ( ) + // ( ) ( ) (3) ( ) + // ( ) ( ) (4) ( ) + + // ( ) ( ) ( ) (1) + // ( ) ( ) ( ) (2) + // ( ) ( ) ( ) (3) + // ( ) ( ) ( ) (4) + + { 0, 17,34,51, }, + { 4, 21,38,55, }, + { 8, 25,42,59, }, + { 12,29,46,63, }, + + // ( ) ( ) ( ) (1) + // ( ) ( ) ( ) (2) + // ( ) ( ) ( ) (3) + // ( ) ( ) ( ) (4) + + // ( ) ( ) (1) ( ) + // ( ) ( ) (2) ( ) + // ( ) ( ) (3) ( ) + // ( ) ( ) (4) ( ) + + // ( ) (1) ( ) ( ) + // ( ) (2) ( ) ( ) + // ( ) (3) ( ) ( ) + // ( ) (4) ( ) ( ) + + // (1) ( ) ( ) ( ) + // (2) ( ) ( ) ( ) + // (3) ( ) ( ) ( ) + // (4) ( ) ( ) ( ) + + { 48,33,18,3, }, + { 52,37,22,7, }, + { 56,41,26,11, }, + { 60,45,30,15, }, + + // (1) ( ) ( ) (3) + // ( ) ( ) ( ) ( ) + // ( ) ( ) ( ) ( ) + // (4) ( ) ( ) (2) + + // ( ) ( ) ( ) ( ) + // ( ) (1) (3) ( ) + // ( ) (4) (2) ( ) + // ( ) ( ) ( ) ( ) + + // ( ) ( ) ( ) ( ) + // ( ) (2) (4) ( ) + // ( ) (3) (1) ( ) + // ( ) ( ) ( ) ( ) + + // (2) ( ) ( ) (4) + // ( ) ( ) ( ) ( ) + // ( ) ( ) ( ) ( ) + // (3) ( ) ( ) (1) + + { 0, 21,42,63, }, + { 15,26,37,48, }, + { 3, 22,41,60, }, + { 12,25,38,51, }, + }; + } +} From c85a1ba06eb5b0cca481840841bf06e8416978bd Mon Sep 17 00:00:00 2001 From: Noah Pauls Date: Mon, 17 Jan 2022 19:47:44 -0800 Subject: [PATCH 2/2] updated comments with better details --- 88_3-D_Tic-Tac-Toe/csharp/Qubic.cs | 104 ++++++++++++++++------------- 1 file changed, 59 insertions(+), 45 deletions(-) diff --git a/88_3-D_Tic-Tac-Toe/csharp/Qubic.cs b/88_3-D_Tic-Tac-Toe/csharp/Qubic.cs index 595db661..513651d6 100644 --- a/88_3-D_Tic-Tac-Toe/csharp/Qubic.cs +++ b/88_3-D_Tic-Tac-Toe/csharp/Qubic.cs @@ -568,9 +568,15 @@ namespace ThreeDTicTacToe /// /// If a player trap is not possible, but a row is found that is /// particularly advantageous for the machine to move to, the machine - /// will try and move to a corner-edge in that row. + /// will try and move to a plane edge in that row. /// /// Original BASIC: 1300-1480 + /// + /// Lines 1440/50 of the BASIC call 2360 (MovePlaneEdge). Because it + /// goes to this code only after it has found an open space marked as + /// potential, it cannot reach line 2440 of that code, as that is only + /// reached if an open space failed to be found in the row on which + /// that code was called. /// /// /// Move if a trap was created, @@ -618,11 +624,11 @@ namespace ThreeDTicTacToe // A row may be particularly advantageous for the machine to // move to at this point; this is the case if a row is entirely // filled with POTENTIAL or has one MACHINE and others - // POTENTIAL. + // POTENTIAL. Such rows may help set up trapping opportunities. if (RowSums[row] == (POTENTIAL * 4) || RowSums[row] == MACHINE + (POTENTIAL * 3)) { - // Try moving to a corner-edge in an advantageous row. - return MoveCornerEdges(row, POTENTIAL); + // Try moving to a plane edge in an advantageous row. + return MovePlaneEdge(row, POTENTIAL); } } @@ -637,9 +643,15 @@ namespace ThreeDTicTacToe /// /// If there are no player traps to block, but a row is found that is /// particularly advantageous for the player to move to, the machine - /// will try and move to a corner-edge in that row. + /// will try and move to a plane edge in that row. /// /// Original BASIC: 1030-1190 + /// + /// Lines 1160/1170 of the BASIC call 2360 (MovePlaneEdge). As with + /// MakePlayerTrap, because it goes to this code only after it has + /// found an open space marked as potential, it cannot reach line 2440 + /// of that code, as that is only reached if an open space failed to be + /// found in the row on which that code was called. /// /// /// Move if a trap was created, @@ -685,11 +697,12 @@ namespace ThreeDTicTacToe { // A row may be particularly advantageous for the player to move // to at this point, indicated by a row containing all POTENTIAL - // moves or one PLAYER and rest POTENTIAL. + // moves or one PLAYER and rest POTENTIAL. Such rows may aid in + // in the later creation of traps. if (RowSums[row] == (POTENTIAL * 4) || RowSums[row] == PLAYER + (POTENTIAL * 3)) { - // Try moving to a corner-edge in an advantageous row. - return MoveCornerEdges(row, POTENTIAL); + // Try moving to a plane edge in an advantageous row. + return MovePlaneEdge(row, POTENTIAL); } } @@ -743,24 +756,20 @@ namespace ThreeDTicTacToe /// /// Find a satisfactory plane on the board and move to one if that - /// plane's corner-edges. + /// plane's plane edges. /// /// A plane on the board is satisfactory if it meets the following /// conditions: /// 1. Player has made exactly 4 moves on the plane. /// 2. Machine has made either 0 or one moves on the plane. - /// The machine then attempts to move to a corner-edge in that plane, - /// first finding any potential moves from the previous action it took - /// and moving there, and moving to any open corner-edge if there are - /// no potential moves found. - /// - /// This action by the machine tries to prevent the player from having - /// exclusive control over any plane in the board. + /// Such a plane is one that the player could likely use to form traps. /// /// Original BASIC: 1830-2020 /// - /// The BASIC code for this action is tightly bound with base code for - /// MoveCornerEdges. + /// Line 1990 of the original basic calls 2370 (MovePlaneEdge). Only on + /// this call to MovePlaneEdge can line 2440 of that method be reached, + /// which surves to help this method iterate through the rows of a + /// plane. /// /// /// Move if a move in a plane was found, @@ -781,21 +790,21 @@ namespace ThreeDTicTacToe (planeSum >= P4_M1 && planeSum < P4_M1 + 1) ) { - // Try to move to corner edges in each row of plane - // First, check for corner edges marked as POTENTIAL. + // Try to move to plane edges in each row of plane + // First, check for plane edges marked as POTENTIAL. for (int row = (4 * plane) - 4; row < (4 * plane); row++) { - var moveResult = MoveCornerEdges(row, POTENTIAL); + var moveResult = MovePlaneEdge(row, POTENTIAL); if (moveResult != MachineAction.None) { return moveResult; } } - // If no POTENTIAL corner-edge found, look for an EMPTY one. + // If no POTENTIAL plane edge found, look for an EMPTY one. for (int row = (4 * plane) - 4; row < (4 * plane); row++) { - var moveResult = MoveCornerEdges(row, EMPTY); + var moveResult = MovePlaneEdge(row, EMPTY); if (moveResult != MachineAction.None) { return moveResult; @@ -804,48 +813,53 @@ namespace ThreeDTicTacToe } } - // No good corner edges found by plane. + // No satisfactory planes with open plane edges found. ClearPotentialMoves(); return MachineAction.None; } /// - /// Given a row, move to the first corner-edge of the cube that has the - /// given value. + /// Given a row, move to the first space in that row that: + /// 1. is a plane edge, and + /// 2. has the given value in Board /// - /// Corner edges are pieces of the cube that have two faces. The AI + /// Plane edges are any spaces on a plane with one face exposed. The AI /// prefers to move to these spaces before others, presumably - /// because they are powerful moves: a corner edge space is contained - /// in 3 rows on the cube. + /// because they are powerful moves: a plane edge is contained on 3-4 + /// winning rows of the cube. /// /// Original BASIC: 2360-2490 /// /// In the original BASIC, this code is pointed to from three different - /// locations by GOTOs (1440/50, or MakePlayerTrap; 1160/70, or - /// BlockMachineTrap; and 1990, or MoveByPlane). Interestingly, line - /// 2440 can only be reached if the code proceeds after a call from - /// 1990. In short, this means that the code flow is incredibly - /// difficult to understand; the block of code at 2360 acts like a - /// generalized subroutine, but contains bits of code that belong - /// to specific pieces of calling code. + /// locations by GOTOs: + /// - 1440/50, or MakePlayerTrap; + /// - 1160/70, or BlockMachineTrap; and + /// - 1990, or MoveByPlane. + /// At line 2440, this code jumps back to line 2000, which is in + /// MoveByPlane. This makes it appear as though calling MakePlayerTrap + /// or BlockPlayerTrap in the BASIC could jump into the middle of the + /// MoveByPlane method; were this to happen, not all of MoveByPlane's + /// variables would be defined! However, the program logic prevents + /// this from ever occurring; see each method's description for why + /// this is the case. /// /// the row to try to move to /// /// what value the space to move to should have in Board /// /// - /// Move if a corner-edge piece in the row with the given spaceValue was + /// Move if a plane edge piece in the row with the given spaceValue was /// found, /// None otherwise /// - private MachineAction MoveCornerEdges(int row, double spaceValue) + private MachineAction MovePlaneEdge(int row, double spaceValue) { - // Given a row, we want to find the corner-edge pieces in that row. + // Given a row, we want to find the plane edge pieces in that row. // We know that each row is part of a plane, and that the first // and last rows of the plane are on the plane edge, while the // other two rows are in the middle. If we know whether a row is an // edge or middle, we can determine which spaces in that row are - // corner-edges. + // plane edges. // // Below is a birds-eye view of a plane in the cube, with rows // oriented horizontally: @@ -855,11 +869,11 @@ namespace ThreeDTicTacToe // row 2: (0) ( ) ( ) (3) // row 3: ( ) (1) (2) ( ) // - // The corner edge pieces have their row indices marked. The pattern + // The plane edge pieces have their row indices marked. The pattern // above shows that: // - // if row == 0 | 3, corner edge spaces = [1, 2] - // if row == 1 | 2, corner edge spaces = [0, 3] + // if row == 0 | 3, plane edge spaces = [1, 2] + // if row == 1 | 2, plane edge spaces = [0, 3] // The below condition replaces the following BASIC code (2370): // @@ -883,7 +897,7 @@ namespace ThreeDTicTacToe _ => throw new Exception($"unreachable ({row % 4})"), }; - // Iterate through corner edge pieces of the row. + // Iterate through plane edge pieces of the row. // // if a = 1 (row is edge), iterate through [0, 3] // if a = 2 (row is middle), iterate through [1, 2] @@ -891,7 +905,7 @@ namespace ThreeDTicTacToe { if (Board[RowsByPlane[row, space]] == spaceValue) { - // Found a corner-edge to take! + // Found a plane edge to take! Board[RowsByPlane[row, space]] = MACHINE; Console.WriteLine($"MACHINE TAKES {IndexToCoord(RowsByPlane[row, space])}"); return MachineAction.Move;