text: #!/usr/bin/python3
  
  # Ported from the BASIC source for 3D Tic Tac Toe
  # in BASIC Computer Games, by David H. Ahl
  # The code originated from Dartmouth College
  
  from enum import Enum
  
  
  class Move(Enum):
      """Game status and types of machine move"""
  
      HUMAN_WIN = 0
      MACHINE_WIN = 1
      DRAW = 2
      MOVES = 3
      LIKES = 4
      TAKES = 5
      GET_OUT = 6
      YOU_FOX = 7
      NICE_TRY = 8
      CONCEDES = 9
  
  
  class Player(Enum):
      EMPTY = 0
      HUMAN = 1
      MACHINE = 2
  
  
  class TicTacToe3D:
      """The game logic for 3D Tic Tac Toe and the machine opponent"""
  
      def __init__(self):
  
          # 4x4x4 board keeps track of which player occupies each place
          # and used by machine to work out its strategy
          self.board = [0] * 64
  
          # starting move
          self.corners = [0, 48, 51, 3, 12, 60, 63, 15, 21, 38, 22, 37, 25, 41, 26, 42]
  
          # lines to check for end game
          self.lines = [
              [0, 1, 2, 3],
              [4, 5, 6, 7],
              [8, 9, 10, 11],
              [12, 13, 14, 15],
              [16, 17, 18, 19],
              [20, 21, 22, 23],
              [24, 25, 26, 27],
              [28, 29, 30, 31],
              [32, 33, 34, 35],
              [36, 37, 38, 39],
              [40, 41, 42, 43],
              [44, 45, 46, 47],
              [48, 49, 50, 51],
              [52, 53, 54, 55],
              [56, 57, 58, 59],
              [60, 61, 62, 63],
              [0, 16, 32, 48],
              [4, 20, 36, 52],
              [8, 24, 40, 56],
              [12, 28, 44, 60],
              [1, 17, 33, 49],
              [5, 21, 37, 53],
              [9, 25, 41, 57],
              [13, 29, 45, 61],
              [2, 18, 34, 50],
              [6, 22, 38, 54],
              [10, 26, 42, 58],
              [14, 30, 46, 62],
              [3, 19, 35, 51],
              [7, 23, 39, 55],
              [11, 27, 43, 59],
              [15, 31, 47, 63],
              [0, 4, 8, 12],
              [16, 20, 24, 28],
              [32, 36, 40, 44],
              [48, 52, 56, 60],
              [1, 5, 9, 13],
              [17, 21, 25, 29],
              [33, 37, 41, 45],
              [49, 53, 57, 61],
              [2, 6, 10, 14],
              [18, 22, 26, 30],
              [34, 38, 42, 46],
              [50, 54, 58, 62],
              [3, 7, 11, 15],
              [19, 23, 27, 31],
              [35, 39, 43, 47],
              [51, 55, 59, 63],
              [0, 5, 10, 15],
              [16, 21, 26, 31],
              [32, 37, 42, 47],
              [48, 53, 58, 63],
              [12, 9, 6, 3],
              [28, 25, 22, 19],
              [44, 41, 38, 35],
              [60, 57, 54, 51],
              [0, 20, 40, 60],
              [1, 21, 41, 61],
              [2, 22, 42, 62],
              [3, 23, 43, 63],
              [48, 36, 24, 12],
              [49, 37, 25, 13],
              [50, 38, 26, 14],
              [51, 39, 27, 15],
              [0, 17, 34, 51],
              [4, 21, 38, 55],
              [8, 25, 42, 59],
              [12, 29, 46, 63],
              [48, 33, 18, 3],
              [52, 37, 22, 7],
              [56, 41, 26, 11],
              [60, 45, 30, 15],
              [0, 21, 42, 63],
              [15, 26, 37, 48],
              [3, 22, 41, 60],
              [12, 25, 38, 51],
          ]
  
      def get(self, x, y, z):
          m = self.board[4 * (4 * z + y) + x]
          if m == 40:
              return Player.MACHINE
          elif m == 8:
              return Player.HUMAN
          else:
              return Player.EMPTY
  
      def move3D(self, x, y, z, player):
          m = 4 * (4 * z + y) + x
          return self.move(m, player)
  
      def move(self, m, player):
          if self.board[m] > 1:
              return False
  
          if player == Player.MACHINE:
              self.board[m] = 40
          else:
              self.board[m] = 8
          return True
  
      def get3DPosition(self, m):
          x = m % 4
          y = (m // 4) % 4
          z = m // 16
          return x, y, z
  
      def evaluateLines(self):
          self.lineValues = [0] * 76
          for j in range(76):
              value = 0
              for k in range(4):
                  value += self.board[self.lines[j][k]]
              self.lineValues[j] = value
  
      def strategyMarkLine(self, i):
          for j in range(4):
              m = self.lines[i][j]
              if self.board[m] == 0:
                  self.board[m] = 1
  
      def clearStrategyMarks(self):
          for i in range(64):
              if self.board[i] == 1:
                  self.board[i] = 0
  
      def markAndMove(self, vlow, vhigh, vmove):
          """
          mark lines that can potentially win the game for the human
          or the machine and choose best place to play
          """
          for i in range(76):
              value = 0
              for j in range(4):
                  value += self.board[self.lines[i][j]]
              self.lineValues[i] = value
              if vlow <= value < vhigh:
                  if value > vlow:
                      return self.moveTriple(i)
                  self.strategyMarkLine(i)
          self.evaluateLines()
  
          for i in range(76):
              value = self.lineValues[i]
              if value == 4 or value == vmove:
                  return self.moveDiagonals(i, 1)
          return None
  
      def machineMove(self):
          """machine works out what move to play"""
          self.clearStrategyMarks()
  
          self.evaluateLines()
          for value, event in [
              (32, self.humanWin),
              (120, self.machineWin),
              (24, self.blockHumanWin),
          ]:
              for i in range(76):
                  if self.lineValues[i] == value:
                      return event(i)
  
          m = self.markAndMove(80, 88, 43)
          if m is not None:
              return m
  
          self.clearStrategyMarks()
  
          m = self.markAndMove(16, 24, 11)
          if m is not None:
              return m
  
          for k in range(18):
              value = 0
              for i in range(4 * k, 4 * k + 4):
                  for j in range(4):
                      value += self.board[self.lines[i][j]]
              if (32 <= value < 40) or (72 <= value < 80):
                  for s in [1, 0]:
                      for i in range(4 * k, 4 * k + 4):
                          m = self.moveDiagonals(i, s)
                          if m is not None:
                              return m
  
          self.clearStrategyMarks()
  
          for y in self.corners:
              if self.board[y] == 0:
                  return (Move.MOVES, y)
  
          for i in range(64):
              if self.board[i] == 0:
                  return (Move.LIKES, i)
  
          return (Move.DRAW, -1)
  
      def humanWin(self, i):
          return (Move.HUMAN_WIN, -1, i)
  
      def machineWin(self, i):
          for j in range(4):
              m = self.lines[i][j]
              if self.board[m] == 0:
                  return (Move.MACHINE_WIN, m, i)
          return None
  
      def blockHumanWin(self, i):
          for j in range(4):
              m = self.lines[i][j]
              if self.board[m] == 0:
                  return (Move.NICE_TRY, m)
          return None
  
      def moveTriple(self, i):
          """make two lines-of-3 or prevent human from doing this"""
          for j in range(4):
              m = self.lines[i][j]
              if self.board[m] == 1:
                  if self.lineValues[i] < 40:
                      return (Move.YOU_FOX, m)
                  else:
                      return (Move.GET_OUT, m)
          return (Move.CONCEDES, -1)
  
      # choose move in corners or center boxes of square 4x4
      def moveDiagonals(self, i, s):
          if 0 < (i % 4) < 3:
              jrange = [1, 2]
          else:
              jrange = [0, 3]
          for j in jrange:
              m = self.lines[i][j]
              if self.board[m] == s:
                  return (Move.TAKES, m)
          return None
  
  
  class Qubit:
      def moveCode(self, board, m):
          x, y, z = board.get3DPosition(m)
          return f"{z + 1:d}{y + 1:d}{x + 1:d}"
  
      def showWin(self, board, i):
          for m in board.lines[i]:
              print(self.moveCode(board, m))
  
      def showBoard(self, board):
          c = " YM"
          for z in range(4):
              for y in range(4):
                  print("   " * y, end="")
                  for x in range(4):
                      p = board.get(x, y, z)
                      print(f"({c[p.value]})      ", end="")
                  print("\n")
              print("\n")
  
      def humanMove(self, board):
          print("")
          c = "1234"
          while True:
              h = input("Your move?\n")
              if h == "1":
                  return False
              if h == "0":
                  self.showBoard(board)
                  continue
              if (len(h) == 3) and (h[0] in c) and (h[1] in c) and (h[2] in c):
                  x = c.find(h[2])
                  y = c.find(h[1])
                  z = c.find(h[0])
                  if board.move3D(x, y, z, Player.HUMAN):
                      break
  
                  print("That square is used. Try again.")
              else:
                  print("Incorrect move. Retype it--")
  
          return True
  
      def play(self):
          print("Qubic\n")
          print("Create Computing Morristown, New Jersey\n\n\n")
          while True:
              c = input("Do you want instructions?\n")
              if len(c) >= 1 and (c[0] in "ynYN"):
                  break
              print("Incorrect answer. Please type 'yes' or 'no.")
  
          c = c.lower()
          if c[0] == "y":
              print("The game is Tic-Tac-Toe in a 4 x 4 x 4 cube.")
              print("Each move is indicated by a 3 digit number, with each")
              print("digit between 1 and 4 inclusive.  The digits indicate the")
              print("level, row, and column, respectively, of the occupied")
              print("place.\n")
  
              print("To print the playing board, type 0 (zero) as your move.")
              print("The program will print the board with your moves indicated")
              print("with a (Y), the machine's moves with an (M), and")
              print("unused squares with a ( ).\n")
  
              print("To stop the program run, type 1 as your move.\n\n")
  
          play_again = True
          while play_again:
              board = TicTacToe3D()
  
              while True:
                  s = input("Do you want to move first?\n")
                  if len(s) >= 1 and (s[0] in "ynYN"):
                      break
                  print("Incorrect answer. Please type 'yes' or 'no'.")
  
              skipHuman = s[0] in "nN"
  
              move_text = [
                  "Machine moves to",
                  "Machine likes",
                  "Machine takes",
                  "Let's see you get out of this:  Machine moves to",
                  "You fox.  Just in the nick of time, machine moves to",
                  "Nice try. Machine moves to",
              ]
  
              while True:
                  if not skipHuman and not self.humanMove(board):
                      break
                  skipHuman = False
  
                  m = board.machineMove()
                  if m[0] == Move.HUMAN_WIN:
                      print("You win as follows,")
                      self.showWin(board, m[2])
                      break
                  elif m[0] == Move.MACHINE_WIN:
                      print(
                          "Machine moves to {}, and wins as follows".format(
                              self.moveCode(board, m[1])
                          )
                      )
                      self.showWin(board, m[2])
                      break
                  elif m[0] == Move.DRAW:
                      print("The game is a draw.")
                      break
                  elif m[0] == Move.CONCEDES:
                      print("Machine concedes this game.")
                      break
                  else:
                      print(move_text[m[0].value - Move.MOVES.value])
                      print(self.moveCode(board, m[1]))
                      board.move(m[1], Player.MACHINE)
  
                  self.showBoard(board)
  
              print(" ")
              while True:
                  x = input("Do you want to try another game\n")
                  if len(x) >= 1 and x[0] in "ynYN":
                      break
                  print("Incorrect answer. Please Type 'yes' or 'no'.")
  
              play_again = x[0] in "yY"
  
  
  if __name__ == "__main__":
      game = Qubit()
      game.play()
  