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..513651d6
--- /dev/null
+++ b/88_3-D_Tic-Tac-Toe/csharp/Qubic.cs
@@ -0,0 +1,1192 @@
+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 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,
+ /// 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. Such rows may help set up trapping opportunities.
+ if (RowSums[row] == (POTENTIAL * 4) || RowSums[row] == MACHINE + (POTENTIAL * 3))
+ {
+ // Try moving to a plane edge in an advantageous row.
+ return MovePlaneEdge(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 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,
+ /// 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. 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 plane edge in an advantageous row.
+ return MovePlaneEdge(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 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.
+ /// Such a plane is one that the player could likely use to form traps.
+ ///
+ /// Original BASIC: 1830-2020
+ ///
+ /// 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,
+ /// 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 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 = MovePlaneEdge(row, POTENTIAL);
+ if (moveResult != MachineAction.None)
+ {
+ return moveResult;
+ }
+ }
+
+ // If no POTENTIAL plane edge found, look for an EMPTY one.
+ for (int row = (4 * plane) - 4; row < (4 * plane); row++)
+ {
+ var moveResult = MovePlaneEdge(row, EMPTY);
+ if (moveResult != MachineAction.None)
+ {
+ return moveResult;
+ }
+ }
+ }
+ }
+
+ // No satisfactory planes with open plane edges found.
+ ClearPotentialMoves();
+ return MachineAction.None;
+ }
+
+ ///
+ /// 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
+ ///
+ /// 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 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.
+ /// 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 plane edge piece in the row with the given spaceValue was
+ /// found,
+ /// None otherwise
+ ///
+ private MachineAction MovePlaneEdge(int row, double spaceValue)
+ {
+ // 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
+ // plane 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 plane edge pieces have their row indices marked. The pattern
+ // above shows that:
+ //
+ // 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):
+ //
+ // 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 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]
+ for (int space = a - 1; space <= 4 - a; space += 5 - (2 * a))
+ {
+ if (Board[RowsByPlane[row, space]] == spaceValue)
+ {
+ // Found a plane 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, },
+ };
+ }
+}