diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml index db25fb7f0197e6d9c0ba94386084cc04e4198214..b7648c37bfb036c35a935ec778a8b0b35f9004d9 100644 --- a/.idea/inspectionProfiles/Project_Default.xml +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -9,5 +9,12 @@ </list> </option> </inspection_tool> + <inspection_tool class="PyUnresolvedReferencesInspection" enabled="true" level="WARNING" enabled_by_default="true"> + <option name="ignoredIdentifiers"> + <list> + <option value="tuple.*" /> + </list> + </option> + </inspection_tool> </profile> </component> \ No newline at end of file diff --git a/search/__pycache__/main.cpython-38.pyc b/search/__pycache__/main.cpython-38.pyc index 306b913ed92d9ff2c02f03382457c5518ae3a453..eb1ac70c63e7dcc6ece6b90117cd8b9bfe10b959 100644 Binary files a/search/__pycache__/main.cpython-38.pyc and b/search/__pycache__/main.cpython-38.pyc differ diff --git a/search/__pycache__/movement_logic.cpython-38.pyc b/search/__pycache__/movement_logic.cpython-38.pyc index fd26de4a44dce1583ac4a901083de447674a94d6..a3ebab1dd81cbec0f07ea7082efc74db478d0ed9 100644 Binary files a/search/__pycache__/movement_logic.cpython-38.pyc and b/search/__pycache__/movement_logic.cpython-38.pyc differ diff --git a/search/__pycache__/search_algo.cpython-38.pyc b/search/__pycache__/search_algo.cpython-38.pyc index 0b2c38ff301effe104872c92bc5895229634aac7..bd303834e681a6b55b086b2bfdc84b5f8add8763 100644 Binary files a/search/__pycache__/search_algo.cpython-38.pyc and b/search/__pycache__/search_algo.cpython-38.pyc differ diff --git a/search/__pycache__/search_algorithm.cpython-38.pyc b/search/__pycache__/search_algorithm.cpython-38.pyc index 59ce8da52be11782436d43c6965409b689989642..8b71a1b293e250952f20fa14b089c534806b984d 100644 Binary files a/search/__pycache__/search_algorithm.cpython-38.pyc and b/search/__pycache__/search_algorithm.cpython-38.pyc differ diff --git a/search/main.py b/search/main.py index adf21701aac675e73148276757a0817e3ff9a8a3..d4a4f710b4a8f4e3fe22130d6e60578bce8df120 100644 --- a/search/main.py +++ b/search/main.py @@ -5,15 +5,15 @@ Project Part A: Searching This script contains the entry point to the program (the code in `__main__.py` calls `main()`). Your solution starts here! """ - import sys import json -from search.search_algo import * + +from search.search_algorithm import * def main(): # define global variable - global upperDictPieces, lowerDictPieces, targetDict, setBlocks + global upperPiecesDict, lowerPiecesDict, targetsDict, setBlocks try: with open(sys.argv[1]) as file: data = json.load(file) @@ -22,27 +22,19 @@ def main(): sys.exit(1) parse_input(data) - # So basically it is heavily implied to treat the game as a state-based search problem. - # We are also told in question 3 of the design report to discuss the time and space - # requirements, and the connection with the branching factor and search tree depth. - # In question 2 we are told to comment on any heuristics we use. - # Considering all of the above, I propose that we use the heuristic of "tiles to closest target" - # This is greedy, but it's a lot faster than trying to path optimally. - # And for search algorithm choice let's try starting with depth-first search, depth limit 1, 2 or 3 - # Not sure which is best at the moment, looking ahead is good but looking too far ahead costs too much time - # We'll use a dictionary to track current targets - # ALGORITHM GOES HERE # Add starting targets - for piece in upperDictPieces: + for piece in upperPiecesDict: find_target(piece) + # keep moving until all the piece met its target - while targetDict: + while targetsDict: input() - upperDictPieces = update_state(upperDictPieces, lowerDictPieces, setBlocks, targetDict) - targetDict = check_if_piece_hit_target(upperDictPieces, lowerDictPieces, targetDict) + take_turn() + make_board() + print_board(board) + file.close() - print_board(make_board(lowerDictPieces,upperDictPieces,setBlocks)) def parse_input(data): """ @@ -56,7 +48,7 @@ def parse_input(data): # We can put the code to read the file NOT in the try/except statement because # if the file couldn't be read the process would end anyway - global upperDictPieces, lowerDictPieces, setBlocks + global upperPiecesDict, lowerPiecesDict, setBlocks, positionHistory initialPiecesUpper = data["upper"] initialPiecesLower = data["lower"] initialBlocks = data["block"] @@ -75,8 +67,8 @@ def parse_input(data): else: nums = nums + 1 keyWrite = "S" + str(nums) - upperDictPieces[keyWrite] = (piece[ROW], piece[COLUMN]) - visit_record[keyWrite] = {} + upperPiecesDict[keyWrite] = (piece[F_ROW], piece[F_COLUMN]) + positionHistory[keyWrite] = {} # parse the Lower player's token nump, numr, nums = 0, 0, 0 @@ -90,18 +82,11 @@ def parse_input(data): else: nums = nums + 1 keyWrite = "s" + str(nums) - lowerDictPieces[keyWrite] = (piece[ROW], piece[COLUMN]) + lowerPiecesDict[keyWrite] = (piece[F_ROW], piece[F_COLUMN]) # parse the block object for block in initialBlocks: - setBlocks.add((block[ROW], block[COLUMN])) - - - - - - - + setBlocks.add((block[F_ROW], block[F_COLUMN])) # Situation 1: If you plan the route ahead without knowing any other piece route, # there will be a crash at tile N as both R and S try to take a straight line diff --git a/search/movement_logic.py b/search/movement_logic.py index 16017e5facaeec9ab1a05fa9fef3001c859520b4..2abd2c718969a7a49aac41b8108cc407d1a5b12b 100644 --- a/search/movement_logic.py +++ b/search/movement_logic.py @@ -306,7 +306,8 @@ def distance_between(Upper_token, Lower_token): under section: double coordinates for doublewidth -> """ - dx = abs(Upper_token[0] - Lower_token[0]) - dy = abs(Upper_token[1] - Lower_token[1]) + + dx = abs(Upper_token[1] - Lower_token[1]) + dy = abs(Upper_token[0] - Lower_token[0]) result = dx + max(0, (dy - dx) / 2) return result diff --git a/search/search_algo.py b/search/search_algo.py index bc5632ae1fcca7c7dc69140f9594270a0a8a2e37..8452f74026b114ae24561b2172261ca136fb244b 100644 --- a/search/search_algo.py +++ b/search/search_algo.py @@ -30,6 +30,7 @@ visit_record = {} def make_board(lowerPieces, upperPieces, setBlocks): """ + done create a board of the current game -> can do a position look up. :param upperPieces: dictionary contain all the upper piece and its location :param lowerPieces: dictionary contain all the lower piece and its location @@ -50,6 +51,7 @@ def make_board(lowerPieces, upperPieces, setBlocks): def get_stronger_piece(piece_type): """ + done Stronger piece is base on the type, even if the piece are from a player. :param piece_type: the type of the piece that we are interested :return: the type of the piece that stronger than the input piece @@ -63,6 +65,7 @@ def get_stronger_piece(piece_type): def add_slide_action(upperDict, piece, board): """ + done add all the valid slide action to a list :param board: dictionary contain all piece and block :param upperDict: contain detail about upper piece @@ -94,6 +97,7 @@ def add_slide_action(upperDict, piece, board): def add_if_valid(position_list, piece, new_position, piece_position, board): """ + done check if the move is valid. :param position_list: list contain all added slide action from this turn :param piece: the interested upper piece @@ -123,6 +127,7 @@ def add_if_valid(position_list, piece, new_position, piece_position, board): def make_priority_list_of_action(upperDict, piece, targetDict, board): """ + done compile all possible action this piece can under go. sort them base on the result distance relative to the piece's target :param upperDict: use to read the position of the piece @@ -152,6 +157,7 @@ def make_priority_list_of_action(upperDict, piece, targetDict, board): def update_state(upperPieces, lowerPieces, setBlocks, targetDict): """ + done move the piece in a way that bring all piece closer to its target # currently only in away that is work for one piece. :param upperPieces: dictionary contain all the upper piece @@ -175,6 +181,7 @@ def update_state(upperPieces, lowerPieces, setBlocks, targetDict): def choose_optimal_combination(possible_action, upperPieces, board, targetDict): """ + done prioritise action lead a piece directly to its target. else choose a combination that does not cause collision between upper piece @@ -238,6 +245,12 @@ def choose_optimal_combination(possible_action, upperPieces, board, targetDict): def decrease_appeal(position, piece): + """ + done + :param position: + :param piece: + :return: + """ global visit_record if position not in visit_record[piece].keys(): @@ -249,6 +262,7 @@ def decrease_appeal(position, piece): def check_if_piece_hit_target(upperPieces, lowerPieces, targetDict): """ + done remove the target from the target dictionary if the upper piece is at its target location :param upperPiece: contain all upper piece :param targetDict: map upper piece to its lower target @@ -281,6 +295,7 @@ def check_if_piece_hit_target(upperPieces, lowerPieces, targetDict): def piece_collision(pieceA, pieceB) -> int: """ + done Our upper pieces are R, P and S, lower pieces are r, p and s We will convert both to lower case letters and check each case Would be nice to use comparator but can't have r>s>p>r using it @@ -317,6 +332,7 @@ def piece_collision(pieceA, pieceB) -> int: def add_swing_action(upperDict, piece, board): """ + done check for adjacent tile. if there are any adjacent tile to this add swing action from those tile :param upperDict: contain all the upper piece @@ -381,6 +397,7 @@ def add_swing_action(upperDict, piece, board): def find_target(piece_key): """ + done This function changes the value of the key given in the target dictionary to the closest enemy piece it can beat XUAN: by separated the upper to lower piece, we dont need to search for the whole dictionary every time we compare. @@ -420,6 +437,13 @@ def find_target(piece_key): def rank_by_appeal(x, targetDict, piece): + """ + done + :param x: + :param targetDict: + :param piece: + :return: + """ # distance_between(x, targetDict[piece]) # make_priority_list_of_action(upperDict, piece, targetDict, board) # the reduce would base on how close it is to its target and how many time we visit this. diff --git a/search/search_algorithm.py b/search/search_algorithm.py index 25a65da05d1e627d546f53093fbf0bf8755bcb41..666985893bd303aa583db1b51dfecbab13006cdf 100644 --- a/search/search_algorithm.py +++ b/search/search_algorithm.py @@ -1,115 +1,384 @@ """ -logic: - -> prioritise the action which take the token closer to its target +This module is hold all method relate to Search algorithm """ -from search.main import * +from search.movement_logic import * +from search.search_algo import piece_collision + +# Constant definition: from search.util import print_board +BLOCK = "[X]" +UPPER_ROCK = 'R' +UPPER_SCISSOR = 'S' +UPPER_PAPER = 'P' +LOWER_ROCK = 'r' +LOWER_PAPER = 'p' +LOWER_SCISSOR = 's' +TYPE = 0 +F_ROW = 1 +F_COLUMN = 2 +ROW = 0 +COLUMN = 1 +A_WIN = 1 +B_WIN = 2 +DRAW = 1 +MAX_DISTANCE = 10 +FIRST_ENTRY = 1 +RE_ENTRY = 1 +HIGHEST_RANK = 0 + +# Global variable: +# All the dictionary is made into global variable since all method is interact with them +upperPiecesDict = {} +lowerPiecesDict = {} +setBlocks = set() + +targetsDict = {} +# keep track all the lower player's piece <- to ensure no two Upper piece can target only one distinguish lower piece +targetedPiece = set() + +# keep track of how many time a upper piece visit a tile while aiming for a target. +# this help rank the action. +positionHistory = {} + board = {} +''' +METHOD +''' +def get_weaker_piece(piece: str) -> str: + """ + get the weaker piece type in low case + :param piece: key of the piece {e.g: 'r1'} + :return: a single character the follow the rule: P> R> S> P + """ + if piece[TYPE] in [LOWER_ROCK, UPPER_ROCK]: + return LOWER_SCISSOR + elif piece[TYPE] in [LOWER_PAPER, UPPER_PAPER]: + return LOWER_ROCK + return LOWER_PAPER + + +def get_stronger_piece(piece: str) -> str: + """ + get the stronger piece type in low case + :param piece: key of the piece {e.g: 'R1'} + :return: a single character that follow the rule: P> R> S> P + """ + if piece[TYPE] in [LOWER_ROCK, UPPER_ROCK]: + return LOWER_PAPER + elif piece[TYPE] in [LOWER_PAPER, UPPER_PAPER]: + return LOWER_SCISSOR + return LOWER_ROCK + + +def make_board(): + """ + add all the piece into a dictionary of board: + (1) -> help with illustrate the current board + (2) -> faster look up using a location. + """ + global board + + for piece in lowerPiecesDict: + board[lowerPiecesDict[piece]] = piece + for piece in upperPiecesDict: + board[upperPiecesDict[piece]] = piece + for block in setBlocks: + board[block] = BLOCK + + + + +def check_valid_action(piece: str, new_position: tuple) -> bool: + """ + check if the action is resolved successfully. + -> no piece should defeat any other piece unless the one whom it defeated is its Target. + -> cant move on top the Block + :param piece: we investigate + :param new_position: result after perform the action + :return: True if the action is successful. False if there are invalid move + """ + # check if if the action happen {if it fail the new_position will be same as piece's position} + if compare_tile(upperPiecesDict[piece], new_position): + return False + + # check if new_position is the piece's target + if piece in targetsDict.keys() and compare_tile(targetsDict[piece], new_position): + return True + + # check if the position result from an action is into a free tile. + if new_position in board.keys(): + if board[new_position] == BLOCK: + return False + if piece_collision(board[new_position], piece) != DRAW: + return False + + return True + + +def check_in(position: tuple, piece: str): + """ + log each time piece visit a tile. + -> if the piece comeplete its journey {arrived at its Target's location} + :param position: of the piece + :param piece: that we want to log + """ + global positionHistory + # first time visit this tile. + if position not in positionHistory[piece].keys(): + positionHistory[piece][position] = FIRST_ENTRY + else: + positionHistory[piece][position] += RE_ENTRY + + +def rank(position: tuple, piece: str) -> int: + """ + rank is base on how far of interested position to piece's target position + -> how ever position is will be rank lower if it is visit before + :param position: we want to rank + :param piece: rank base on the piece + :return: rank + """ + # base rank is the distance between current position to the piece's target + baseRank = distance_between(position, targetsDict[piece]) + # reduce_factor is base on how many time has this piece visit current tile. + reduceFactor = 0 + if piece in positionHistory.keys() and position in positionHistory[piece].keys(): + reduceFactor = positionHistory[piece][position] + + return baseRank + reduceFactor -def add_to_queue(token, state, targetDict): +def piece_collision(pieceA: str, pieceB: str) -> int: """ - -> check the adjacent tile for potential move. - -> add them into a priority queue - :param targetDict: - :param state: run - :param token: the one that we want to choose an action for - :return: the list sort base on how close it is to the token's target + Our upper pieces are R, P and S, lower pieces are r, p and s + We will convert both to lower case letters and check each case + Would be nice to use comparator but can't have r>s>p>r using it + + :param pieceA: type of the token in {'R','P','S','r','p','s'} + :param pieceB: type of the token in {'R','P','S','r','p','s'} + :return: A_WIN, B_WIN or DRAW """ - # add all the adjacent move to queue - appending_list = add_adjacent_action(token, state) + pieceA = pieceA[TYPE].lower() + pieceB = pieceB[TYPE].lower() + if pieceA is LOWER_ROCK: + if pieceB == LOWER_SCISSOR: + return A_WIN + elif pieceB == LOWER_PAPER: + return B_WIN - # sort the list base on the how close it is to target - appending_list.sort() - appending_list.sort(key=(lambda x: distance_between(x, targetDict[token]))) + elif pieceA == LOWER_SCISSOR: + if pieceB == LOWER_PAPER: + return A_WIN + elif pieceB == LOWER_ROCK: + return B_WIN + elif pieceA == LOWER_PAPER: + if pieceB == LOWER_ROCK: + return A_WIN + elif pieceB == LOWER_SCISSOR: + return B_WIN + return DRAW - return appending_list +def slide_action(piece: str) -> list: + """ + for each piece there will be 6 total position as result of slide action. + -> check each to ensure they are valid move + :param piece: will perform an slide action + :return: unsorted list of all the valid action + """ + action_list = [] + position = upperPiecesDict[piece] + for action in (slide_right(position), slide_left(position), slide_up_left(position), + slide_up_right(position), slide_down_left(position), slide_down_right(position)): + if check_valid_action(piece, action): + action_list.append(action) -def add_if_allowed(list, token, token_new_position, token_position): + return action_list + + +def swing_action(piece: str) -> list: """ - handle check if the tile has sit on top of a block. - or if it is sit on top on a token would beat it - :param list: the temporary list to append to while await to process - :param token: the adjcent tile of a token - :param token_new_position: - :return: the list so the local change can be pass to higher scope + for each adjacent piece there will be at most 3 swing move. + -> check those move to ensure they are valid + :param piece: will perform swing action + :return: unsorted list of all valid action """ - if compare_tile(token_new_position, token_position): - return list - if token_new_position == token_new_position not in board.keys() or \ - board[token_new_position] != "" or \ - board[token_new_position] != get_stronger_token(token): - list.append(token_new_position) - return list + # find all the adjacent piece + adjacent_pieces = [] + + position = upperPiecesDict[piece] + for tile in (slide_right(position), slide_left(position), slide_up_left(position), + slide_up_right(position), slide_down_left(position), slide_down_right(position)): + # check if the move is not out of board + if not compare_tile(position, tile): + # the tile can be any object except block + if tile in board and board[tile] != BLOCK: + adjacent_pieces.append(tile) + + # if there are at least one adjacent piece, add the position result from performing swing action from those piece. + action_list = [] -def add_adjacent_action(token, state): + for adjacent_piece in adjacent_pieces: + for action in (swing_to_tile_1(position, adjacent_piece), swing_to_tile_2(position, adjacent_piece), + swing_to_tile_3(position, adjacent_piece)): + if check_valid_action(piece, action): + action_list.append(action) + + return action_list + + +def find_target(piece: str): """ - create a abstraction to hide implementation detail on how to add adjacent move. - check if the any slide action is allowed. - :param token:token that we want to find a direction to - :return: the queue + Find a weaker piece to this piece which closest + -> only add if that potential target is not targeted by any other upper piece. + :param piece: which we want to map a target to """ - appending_list = [] + global targetsDict + # get the target + targetType = get_weaker_piece(piece) + + targetDistance = MAX_DISTANCE + target = "" - # check the right tile - appending_list = add_if_allowed(appending_list, token, slide_right(state[token]), state[token]) + for lowerPiece in lowerPiecesDict: + if lowerPiece[TYPE] == targetType and lowerPiece not in targetedPiece: - # check the left tile - appending_list = add_if_allowed(appending_list, token, slide_left(state[token]), state[token]) + distance = distance_between(upperPiecesDict[piece], lowerPiecesDict[lowerPiece]) - # check the up_left - appending_list = add_if_allowed(appending_list, token, slide_up_left(state[token]), state[token]) + if distance < targetDistance: + target = lowerPiece + targetDistance = distance + if targetDistance != MAX_DISTANCE: + targetsDict[piece] = lowerPiecesDict[target] + targetedPiece.add(target) - # check the up_uight - appending_list = add_if_allowed(appending_list, token, slide_up_right(state[token]), state[token]) - # check the down_left - appending_list = add_if_allowed(appending_list, token, slide_down_left(state[token]), state[token]) +def make_ranked_move_list(piece: str) -> list: + """ + add all the possible position after perform an action. + sort them according to their rank{low to high} + :param piece: will be perform the move + :return: sorted list of position + """ - # check the down_right - appending_list = add_if_allowed(appending_list, token, slide_down_right(state[token]), state[token]) + # add all the valid adjacent position result from perform a slide action + position_list = slide_action(piece) - return appending_list + # add all the valid position result from perform a swing action + position_list.extend(swing_action(piece)) + # rank the list base on how close it is to its target. The least visited position the higher rank it is. + if piece in targetsDict.keys(): + position_list.sort(key=(lambda x: rank(x, piece))) + return position_list -def get_stronger_token(token): - if token[FIRST_CHAR] == 'R': - return 'p' - if token[FIRST_CHAR] == 'S': - return 'r' - return 's' +def perform_optimal_combination(move_list: dict): + """ + perform an action from each of the piece sorted action list. + -> prioritise action that after complete bring piece to its target. + -> then choose an action from those piece with the least choice. + -> then choose action from the remained piece in a way that not cause collision + :param move_list: contain all the pottential moved for all piece + """ + # perform all the action that directly lead the piece to its target. + priority_list = [] -def update_state(state, setblocks, targetDict): + for piece in move_list: + if piece in targetsDict.keys() and compare_tile(move_list[piece][HIGHEST_RANK], targetsDict[piece]): - make_board(state, setblocks) + # perform swap and update board + del board[upperPiecesDict[piece]] + upperPiecesDict[piece] = targetsDict[piece] - print("\n\n") + board[targetsDict[piece]] = piece + else: + priority_list.append(piece) + + # sort the list base on number of action. + priority_list.sort(key=(lambda key: len(move_list[key]))) + + # perform an action that does not result in a collision. + for piece in priority_list: + index = HIGHEST_RANK + moved = False + + while not moved: + if index >= len(move_list[piece]): + break + + if move_list[piece][index] in board.keys() and\ + piece_collision(board[move_list[piece][index]], piece) != DRAW: + index += 1 + continue + + # perform the action + if upperPiecesDict[piece] in board.keys(): + del board[upperPiecesDict[piece]] + upperPiecesDict[piece] = move_list[piece][index] + + board[move_list[piece][index]] = piece + moved = True + + check_in(move_list[piece][index], piece) + + + +def take_turn(): + """ + each turn: map out the board in order to look up easily. + + choose a combination of move that is partially optimal {move all the piece <some time> closer to its target} + """ + make_board() print_board(board) - for token in state: - if token[0] in ('s', 'p', 'r'): - print("continue") - continue - potential_new_position = add_to_queue(token, state, targetDict) - state[token] = potential_new_position[0] - print(state[token]) - return state + possible_actions = {} + for piece in upperPiecesDict: + possible_actions[piece] = make_ranked_move_list(piece) -def make_board(state, setblocks): - global board - board = {} - for piece in state: + perform_optimal_combination(possible_actions) + update_target_dictionary() - board[state[piece]] = piece - for block in setblocks: - board[block] = '' +def update_target_dictionary(): + """ + removed all piece that hit its target. + attempt to assign it a new target. + :return: + """ + global targetsDict, lowerPiecesDict, positionHistory + removed = False + deleted = [] + for piece in targetsDict.keys(): + if compare_tile(upperPiecesDict[piece], targetsDict[piece]): + # remove the defeated piece + removed_piece = "" + for removed_p in lowerPiecesDict: + if compare_tile(lowerPiecesDict[removed_p], targetsDict[piece]): + removed_piece = removed_p + break + deleted.append(piece) + del lowerPiecesDict[removed_piece] + targetedPiece.remove(removed_piece) + # reset the log for visited node + positionHistory[piece] = {} + removed = True + for piece in deleted: + del targetsDict[piece] + # once the piece hit it target, it is free to find a new target. + # if there are more lower piece then which already targeted {some lower piece is not target} + # we can check if the available pieces can target those + if removed and len(lowerPiecesDict) > len(targetsDict): + for piece in upperPiecesDict: + # when a piece is remove then there will + if piece in targetsDict.keys(): + continue + find_target(piece) \ No newline at end of file diff --git a/search/test.py b/search/test.py index e715455e1f581633e7dfd05ab0b2b417abc6c03f..374b8ce5f34e531230b0d08821dae092721dd52b 100644 --- a/search/test.py +++ b/search/test.py @@ -1 +1,8 @@ -print('R' in ('R', 's')) +from search.movement_logic import distance_between + +print(distance_between((2, -2), (4, 0))) +print(distance_between((2, -1), (4, 0))) +print(distance_between((1, 0), (4, 0))) +print(distance_between((0, 0), (4, 0))) +print(distance_between((0, -1), (4, 0))) +print(distance_between((1, -2), (4, 0))) \ No newline at end of file