#!/usr/bin/env 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 from typing import Optional, Tuple, Union 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) -> None: # 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) -> Player: 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 move_3d(self, x, y, z, player) -> bool: m = 4 * (4 * z + y) + x return self.move(m, player) def move(self, m, player) -> bool: if self.board[m] > 1: return False if player == Player.MACHINE: self.board[m] = 40 else: self.board[m] = 8 return True def get_3d_position(self, m) -> Tuple[int, int, int]: x = m % 4 y = (m // 4) % 4 z = m // 16 return x, y, z def evaluate_lines(self) -> None: 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 strategy_mark_line(self, i) -> None: for j in range(4): m = self.lines[i][j] if self.board[m] == 0: self.board[m] = 1 def clear_strategy_marks(self) -> None: for i in range(64): if self.board[i] == 1: self.board[i] = 0 def mark_and_move(self, vlow, vhigh, vmove) -> Optional[Tuple[Move, int]]: """ 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.move_triple(i) self.strategy_mark_line(i) self.evaluate_lines() for i in range(76): value = self.lineValues[i] if value == 4 or value == vmove: return self.move_diagonals(i, 1) return None def machine_move(self) -> Union[None, Tuple[Move, int], Tuple[Move, int, int]]: """machine works out what move to play""" self.clear_strategy_marks() self.evaluate_lines() for value, event in [ (32, self.human_win), (120, self.machine_win), (24, self.block_human_win), ]: for i in range(76): if self.lineValues[i] == value: return event(i) m = self.mark_and_move(80, 88, 43) if m is not None: return m self.clear_strategy_marks() m = self.mark_and_move(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.move_diagonals(i, s) if m is not None: return m self.clear_strategy_marks() 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 human_win(self, i) -> Tuple[Move, int, int]: return (Move.HUMAN_WIN, -1, i) def machine_win(self, i) -> Optional[Tuple[Move, int, int]]: for j in range(4): m = self.lines[i][j] if self.board[m] == 0: return (Move.MACHINE_WIN, m, i) return None def block_human_win(self, i) -> Optional[Tuple[Move, int]]: for j in range(4): m = self.lines[i][j] if self.board[m] == 0: return (Move.NICE_TRY, m) return None def move_triple(self, i) -> Tuple[Move, int]: """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 move_diagonals(self, i, s) -> Optional[Tuple[Move, int]]: 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 move_code(self, board, m) -> str: x, y, z = board.get3DPosition(m) return f"{z + 1:d}{y + 1:d}{x + 1:d}" def show_win(self, board, i) -> None: for m in board.lines[i]: print(self.move_code(board, m)) def show_board(self, board) -> None: 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 human_move(self, board) -> bool: print() c = "1234" while True: h = input("Your move?\n") if h == "1": return False if h == "0": self.show_board(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) -> None: 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'.") skip_human = 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 skip_human and not self.human_move(board): break skip_human = False m = board.machine_move() assert m is not None if m[0] == Move.HUMAN_WIN: print("You win as follows,") self.show_win(board, m[2]) # type: ignore break elif m[0] == Move.MACHINE_WIN: print( "Machine moves to {}, and wins as follows".format( self.move_code(board, m[1]) ) ) self.show_win(board, m[2]) # type: ignore 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.move_code(board, m[1])) board.move(m[1], Player.MACHINE) self.show_board(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()