diff --git a/tictactoe.py b/tictactoe.py index 13b1ff4..2bc5175 100644 --- a/tictactoe.py +++ b/tictactoe.py @@ -2,6 +2,14 @@ import sys import math import random import itertools +from enum import Enum + + +class PlacementResult(Enum): + success = 0 + non_numeric_input = 1 + dimension_size_error = 2 + unavailable_placement = 3 class Player: @@ -122,7 +130,7 @@ class Board: self.__display_recur(list_of_list[i], d - 1) def __get_space(self, index_list): - if len(index_list) != self.__dimensions: + if len(index_list) != self.__dimensions: # this is safety that is helpful only in the building process print("\nCoordinates for a space were impossible.") return None else: @@ -142,18 +150,18 @@ class Board: for index in indexes: if not index.isnumeric(): - return 1 # This will be a key value to denote a failure of type + return PlacementResult.non_numeric_input if len(indexes) != self.__dimensions: - return 2 # This will be a key value to denote a failure of quantity + return PlacementResult.dimension_size_error if self.__get_space(indexes).place_token(player.get_token()): self.__round += 1 self.__winner = self.__is_winning_move(coordinate, player) player.save_move(coordinate) - return 0 # This will be a key value to denote ultimate success + return PlacementResult.success else: - return 3 # This will be a key value to denote a occupancy of space + return PlacementResult.unavailable_placement def __get_random_coordinate(self, start, stop): result = "" @@ -164,6 +172,7 @@ class Board: return result def get_center_coordinates(self): + # this will only be called for odd dimensions, it should probably have some more safety on it center = self.__dimensions / 2 return self.__get_random_coordinate(center, center + 1) @@ -178,6 +187,8 @@ class Board: freedom_set = self.__get_dimension_locks() intersecting_pathsets = self.__get_winning_paths(coord_list, freedom_set) + # we want to look through all of the found possible paths a placed token in a space could finish a continuous + # streak and see if it did for paths in intersecting_pathsets: for path in paths: solution_in_path = True @@ -189,17 +200,16 @@ class Board: return None def __get_dimension_locks(self): - # This gets all unique permutations of True-False for each dimension so 2^n elements. - # This is used as a matrix to know when to freeze a dimension to create cross sections of a n-space, - # it gets all possibilities. - # This is also used to pattern all the ways to manipulate a 2-dimensional element, increase/decrease it. + # This gets all unique permutations of True-False for each dimension so 2^n elements + # This is used to create a matrix of all the possible cross sections of a n-space + # This is also used to pattern all the ways to manipulate a 1-dimensional element, increase/decrease it final = list() initial = list() for i in range(self.__dimensions): initial.append(True) - for i in range(len(initial)): # Don't do +1 b/c +1 item will get all False + for i in range(len(initial)): # Don't do +1 b/c +1 item will get all False, which will mean the 0th dimension if i > 0: initial[i - 1] = False for j in list(itertools.permutations(initial)): @@ -212,37 +222,48 @@ class Board: def __get_winning_paths(self, coord_list, freedoms): path_set = list() - # This will get all possible paths that are n+1 long from the starting point, including wraparounds - for freedom in freedoms: + # This will get all possible paths that are n+1 long from the starting point + for freedom in freedoms: # Each freedom is a permutation of whether each dimension should change result = list() - for edit_pattern in freedoms: + for edit_pattern in freedoms: # Each edit_pattern is a permutation of how to change each dimension patterned_edit = list() - for i in range(self.__dimensions + 1): - temp = coord_list.copy() - for j in range(len(temp)): + for i in range(self.__dimensions + 1): # dim + 1 i is number of spaces for a continuous streak + temp = coord_list.copy() # we want a new memory location to edit our coord for a new coord + for j in range(len(temp)): # j is which dimension in our new coord we are manipulating if freedom[j]: if edit_pattern[j]: + # We want to increase or decrease such that we are always between 0 and the dimension. + # Doing this will create a wraparound problem though e.g. (1,0),(0,1),(2,2). We will + # abstain from including these soon. We also want to only shift in a dimension by a + # single space. temp[j] = (temp[j] + i + (self.__dimensions + 1)) % (self.__dimensions + 1) else: temp[j] = (temp[j] - i - (self.__dimensions + 1)) % (self.__dimensions + 1) patterned_edit.append(temp) + # we only want unique coordinate sets and we do not want wraparounds or other non-continuous paths if patterned_edit not in result and self.__is_path_continuous(patterned_edit, freedom): result.append(patterned_edit) + # we only want unique coordinate groups if result not in path_set: path_set.append(result) return path_set def __is_path_continuous(self, path, freedoms): # This will prune out impossible paths (paths that wrap around the space) - # Though this could be static, it doesn't feel right as static, so I haven't made it so. + # Though this could be static, it doesn't make sense as static, so I haven't made it so. slope_is_legit = True + # We want to look at each element in a list against every other element in that list, except itself so it is a + # for i for j where i != j kind of thing - this is more detail that necessary in a comment, the so it is a part for i in range(len(path)): found_a_slope_buddy = False for j in range(len(path)): if i != j: point_has_good_slope = True for k in range(len(path[i])): - if freedoms[k]: + if freedoms[k]: # we include freedom because a dimension may be "locked", which means we + # may be looking at a victory path in some dimension r (such that r < n) cross section, when + # this is the case we don't want to check if there is a change of 1 in those dimensions that + # are locked because there will be a change of 0 and the test will give us a false negative point_has_good_slope = point_has_good_slope and abs(path[i][k] - path[j][k]) == 1 found_a_slope_buddy = found_a_slope_buddy or point_has_good_slope slope_is_legit = slope_is_legit and found_a_slope_buddy @@ -261,87 +282,109 @@ class Board: result += ")\n" return result -# Setup Inputs -print("\nWelcome to multidimensional Tic-Tac-Toe!\n\nThe board will contain (n+1)^n spaces for n dimensions.\nFor even " - "dimensions starting with the 4th dimension, the center space can be setup to be unplayable.\nClaim n+1 " - "spaces in a continuous streak to win!\n") -difficulty_attempts = 1 -difficulty = input("How many dimensions of Tic Tac Toe would you like to attempt? ") -while not difficulty.isnumeric() and difficulty_attempts < 3: - difficulty_attempts += 1 - print("Remaining attempts: " + str(3 - difficulty_attempts) + " failure will result in termination.") - difficulty = input("We require more integers for dimension count. How many dimensions of tic tac toe would you " - "like to attempt? ") +def main(): + board = create_board_from_inputs() + play_tic_tac_toe(board) -if difficulty_attempts >= 3 and not difficulty.isnumeric(): - print("Let me know when you find your number pad. Please come again.") - sys.exit() -dimension = int(difficulty) -if dimension < 2: - print("Tic Tac Toe requires at least 2 dimensions and you chose " + difficulty + ", so we will use 2 dimensions.") - dimension = 2 -else: - print("You have requested a board of " + str(math.pow(dimension + 1, dimension)) + " spaces.") - if dimension > 5: - print("This is a very large board. It may take some time to create.") +def create_board_from_inputs(): + print( + "\nWelcome to multidimensional Tic-Tac-Toe!\n\nThe board will contain (n+1)^n spaces for n dimensions.\nFor " + "even dimensions starting with the 4th dimension, the center space can be setup to be unplayable.\nClaim n+1 " + "spaces in a continuous streak to win!\n") + difficulty_attempts = 1 + max_attempts = 3 + difficulty = input("How many dimensions of Tic Tac Toe would you like to attempt? ") -board = Board(dimension) -if dimension > 2 and dimension % 2 == 0: - middle_attempts = 1 - center_selectable = input("Do you want the center most space selectable (Y/N)? ") + while not difficulty.isnumeric() and difficulty_attempts < max_attempts: + difficulty_attempts += 1 + print("Remaining attempts: " + str(max_attempts - difficulty_attempts) + ", failure will result in termination.") + difficulty = input( + "We require more integers for dimension count. How many dimensions of tic tac toe would you " + "like to attempt? ") - while not (center_selectable == "Y" or center_selectable == "N") and middle_attempts < 3: - middle_attempts += 1 - print("Remaining attempts: " + str(3 - middle_attempts) + " failure will result in termination.") - center_selectable = input("Y/N type input is required. Do you want the center most space selectable (Y/N)? ") - - if not (center_selectable == "Y" or center_selectable == "N") and middle_attempts >= 3: - print("Let me know when you find your 'Y' and 'N' keys. Please come again.") + if difficulty_attempts >= max_attempts and not difficulty.isnumeric(): + print("Let me know when you find your number pad. Please come again.") sys.exit() - if center_selectable == "N": - # Player(-1) is a player for the board to claim the center location - board.place_token(board.get_center_coordinates(), Player(-1)) # This will always work #usefulcomments + dimension = int(difficulty) + if dimension < 2: + print( + "Tic Tac Toe requires at least 2 dimensions and you chose " + difficulty + ", so we will use 2 dimensions.") + dimension = 2 + else: + print("You have requested a board of " + str(int(math.pow(dimension + 1, dimension))) + " spaces.") + if dimension > 5: + print("This is a very large board. It may take some time to create.") -# Actually Playing the game -turn = 0 -while not board.is_full() and not board.has_winner(): - board.display() - player_to_play = turn % 2 - move_attempts = 1 - coordinate_move = input("\nPlayer " + str(player_to_play + 1) + " please input coordinates (.. ... .<3rd index>..) to place your '" - + board.players[player_to_play].get_token() + "' token: ") - move_result = board.place_token(coordinate_move, board.players[player_to_play]) - while move_result > 0 and move_attempts < 3: - if move_result == 1: - print("Input Error on Coordinate. There is a non-integer type between the '.' separators.") - if move_result == 2: - print("Input Error on Coordinate. The number of indexes in your coordinate does not match the dimension.") - if move_result == 3: - print("Input Error on Coordinate. That space is already claimed.") - print("Remaining attempts: " + str(3 - move_attempts) + " failure will result in random placement.") - coordinate_move = input("\nPlayer " + str(player_to_play + 1) + " please input coordinates (.. ... .<3rd index>..) to place your '" - + board.players[player_to_play].get_token() + "' token: ") - move_attempts += 1 + board = Board(dimension) + if dimension > 2 and dimension % 2 == 0: + middle_attempts = 1 + center_selectable = input("Do you want the center most space selectable (Y/N)? ") + + while not (center_selectable == "Y" or center_selectable == "N") and middle_attempts < max_attempts: + middle_attempts += 1 + print("Remaining attempts: " + str(max_attempts - middle_attempts) + ", failure will result in termination.") + center_selectable = input( + "Y/N type input is required. Do you want the center most space selectable (Y/N)? ") + + if not (center_selectable == "Y" or center_selectable == "N") and middle_attempts >= max_attempts: + print("Let me know when you find your 'Y' and 'N' keys. Please come again.") + sys.exit() + + if center_selectable == "N": + # Player(-1) is a player for the board to claim the center location + board.place_token(board.get_center_coordinates(), Player(-1)) # This is half why Player is not a subclass + return board + + +def play_tic_tac_toe(board): + turn = 0 + while not board.is_full() and not board.has_winner(): + board.display() + player_to_play = turn % 2 + move_attempts = 1 + max_attempts = 3 + coordinate_move = input( + "\nPlayer " + str(player_to_play + 1) + + " please input coordinates (.. ... .<3rd index>..) to place your '" + + board.players[player_to_play].get_token() + "' token: ") move_result = board.place_token(coordinate_move, board.players[player_to_play]) - if move_result > 0 and move_attempts >= 3: - move_result = board.place_random(board.players[player_to_play]) - while move_result > 0: - move_result = board.place_random(board.players[player_to_play]) + while move_result != PlacementResult.success and move_attempts < max_attempts: + if move_result == PlacementResult.non_numeric_input: + print("Input Error on Coordinate. There is a non-integer type between the '.' separators.") + if move_result == PlacementResult.dimension_size_error: + s = "Input Error on Coordinate. The number of indexes in your coordinate does not match the dimension." + print(s) + if move_result == PlacementResult.unavailable_placement: + print("Input Error on Coordinate. That space is already claimed.") + s = "Remaining attempts: " + str(max_attempts - move_attempts) + s += ", failure will result in random placement." + print(s) + s = "\nPlayer " + str(player_to_play + 1) + s += " please input coordinates (.. ... .<3rd index>..) to place " + s += "your '" + board.players[player_to_play].get_token() + "' token: " + coordinate_move = input(s) + move_attempts += 1 + move_result = board.place_token(coordinate_move, board.players[player_to_play]) - turn += 1 + if move_result != PlacementResult.success and move_attempts >= max_attempts: + while move_result != PlacementResult.success: + move_result = board.place_random(board.players[player_to_play]) + turn += 1 -# Reporting on End of Game -board.display() -if board.is_full(): - print("Board has filled and no victor has been found. Game Over. Thanks for playing, try again.") + # Reporting on End of Game + board.display() + if board.is_full(): + print("Board has filled and no victor has been found. Game Over. Thanks for playing, try again.") -if board.has_winner(): - print("\nVictory to Player " + str(board.players[board.get_winner()].get_id() + 1) + " - respect. The " - + board.players[board.get_winner()].get_token() + "'s win!\nThe winning path is:\n" + board.get_winning_path() - + "\nGame Over. Thanks for playing, play again.") + if board.has_winner(): + s = "\nVictory to Player " + str(board.players[board.get_winner()].get_id() + 1) + s += " - respect. The " + board.players[board.get_winner()].get_token() + s += "'s win!\nThe winning path contains:\n" + board.get_winning_path() + s += "\nGame Over. Thanks for playing, play again." + print(s) + +main()