diff --git a/roborally.py b/roborally.py index 9fbb51a..adf699b 100644 --- a/roborally.py +++ b/roborally.py @@ -89,16 +89,25 @@ class Robot: # mark the tile on the board as occupied self.board[(x,y)].occupant = self + def get_tile(self): + # return the tile the robot is standing on + return self.board[(self.x, self.y)] + + def get_adjecent_tile(self, direction): + # get the tile adjecent to the robot in the given direction + current_tile = self.get_tile() + return self.board[current_tile.get_neighbor_coordinates(direction)] def get_accessed_tiles(self, count, forward=True): # create a list of all tiles the robot would enter if it drives steps forward tiles = [] - current_tile = self.board[(self.x, self.y)] + current_tile = self.get_tile() for i in range(1, count + 1): if forward: current_tile = self.board.get(current_tile.get_neighbor_coordinates(self.orientation)) else: current_tile = self.board.get(current_tile.get_neighbor_coordinates(Robot.opposites[self.orientation])) + if current_tile is None: return tiles else: @@ -108,7 +117,7 @@ class Robot: def is_pushable(self, direction): # check if the robot can be pushed in the given direction # this is the case if there is a non-blocking tile next to the robot or if there is another robot that is pushable - robot_tile = self.board[(self.x, self.y)] + robot_tile = self.get_tile() neighbor_tile = self.board.get(robot_tile.get_neighbor_coordinates(direction)) if neighbor_tile is None: # neighbor tile could not be found -> robot would be pushed out of the board return False @@ -140,9 +149,9 @@ class Robot: def move(self, type): # move the robot forward or backward # this involves - tile = self.board[(self.x, self.y)] + tile = self.get_tile() if type == 'forward': - target_tile = self.board[tile.get_neighbor_coordinates(self.orientation)] + target_tile = self.get_adjecent_tile(self.orientation) if target_tile.occupant is not None: print("error: target tile is not empty") @@ -157,7 +166,7 @@ class Robot: return "{}, forward".format(self.id) elif type == 'backward': opposite_orientation = self.get_opposite_orientation() - target_tile = self.board[tile.get_neighbor_coordinates(opposite_orientation)] + target_tile = self.get_adjecent_tile(opposite_orientation) if target_tile.occupant is not None: print("error: target tile is not empty") @@ -178,6 +187,19 @@ class Robot: # do nothing command return "{}, nop".format(self.id) + def board_element_processable(self): + # check if we can directly process the board element for the tile the current robot is located on + tile = self.get_tile() + + if tile.modifier is None: + return True + elif tile.modifier in ['^', '>', 'v', '<']: + direction = tile.modifier + neighbor_tile = self.get_adjecent_tile(direction) + return neighbor_tile.occupant is None # if the adjacent tile the robot will be pushed into is empty + # we can execute the push + return True + def __str__(self): return str(self.id) @@ -242,12 +264,23 @@ class Board: self.board[(x, y)] = Tile(x, y, 'v') elif y == 4: self.board[(x, y)] = Tile(x, y, '>') + elif y == 1 and (x >= 2) and (x < 5): + self.board[(x, y)] = Tile(x, y, '>') + elif y == 1 and (x >= 6) and (x <= 8): + self.board[(x, y)] = Tile(x, y, '<') else: self.board[(x,y)] = Tile(x,y) + self.board[(1, 1)].modifier = 'v' + self.board[(1, 2)].modifier = '>' + self.board[(2, 2)].modifier = '^' + self.board[(2, 1)].modifier = '<' + self.robots = {} - self.robots[0] = Robot(3, 1, '>', 0, self.board) - self.robots[1] = Robot(2, 1, 'v', 1, self.board) + self.robots[0] = Robot(1, 1, '>', 0, self.board) + self.robots[1] = Robot(1, 2, 'v', 1, self.board) + self.robots[2] = Robot(2, 1, '>', 2, self.board) + self.robots[3] = Robot(2, 2, 'v', 3, self.board) def handle_push(self, direction, pushed_robot, forward=True, pushing_robot=None): cmd_list = [] @@ -392,14 +425,57 @@ class Board: cmd_list += self.handle_single_action(action, robot) - # apply the actions caused by board elements at the end of the phase - for robot_id in self.robots: - robot = self.robots[robot_id] - cmd_list += self.handle_board_element(robot) - - print(cmd_list) print(self) - pass + + # apply the actions caused by board elements at the end of the phase + self.apply_board_element_actions() + + return cmd_list + + def apply_board_element_actions(self): + cmd_list = [] + remaining_robots = set(self.robots.values()) + processed_robots = set() + + # first we compute all tiles the robots would enter as a result of board game elements + target_tiles = {} # get target tiles for each robot + for r in remaining_robots: + tile = r.get_tile() + if tile.modifier in ['^', '>', 'v', '<']: # tile would push the robot around + direction = tile.modifier + target_tiles[r] = r.get_adjecent_tile(direction) # save tile the robot would be pushed to + + # now we check if there are any conflicts + conflicting_tiles = set([x for x in target_tiles.values() if list(target_tiles.values()).count(x) > 1]) + if len(conflicting_tiles) > 0: # check if any robots would be pushed to the same tile + # there is a conflict -> skip the board element execution and mark those robots as processed + conflicting_robots = set(filter(lambda r: target_tiles[r] in conflicting_tiles, target_tiles.keys())) + processed_robots = processed_robots.union(conflicting_robots) + + # Now we process the board game elements for the robots which have no conflicts. + # We have to pay attention to the order of the execution in order to avoid robots pushing other robots + # during this phase. + # This is done in a loop because we don't know yet which robot goes first. For instance, it may happen that + # multiple robots are queued on a conveyor belt. Then we first have to move the robot which is furthest down the + # line, then the second one and so on + # By doing this in a loop we can automatically determine the correct order by checking for each robot if it can + # move and then processing the robot such that the next robot can move + while len(processed_robots) < len(self.robots): + # update remaining robots to process + remaining_robots = set(self.robots.values()) - processed_robots + + # check which robots can be moved around + processable_robots = list(filter(lambda r: r.board_element_processable(), remaining_robots)) + + if len(processable_robots) > 0: + # handle the board game elements for robots that can move + for current_robot in processable_robots: + cmd_list += self.handle_board_element(current_robot) + processed_robots.add(current_robot) + else: + # this happens if there is a deadlock that cannot be resolved (e.g. caused by a cyclical conveyor belt) + break + return cmd_list def __str__(self): @@ -421,7 +497,7 @@ if __name__ == "__main__": deck = CardDeck() - player_1_cards = random.sample(list(filter(lambda c: 'backward' in c.action, deck.deck.values())), n) + player_1_cards = random.sample(list(filter(lambda c: 'turn around' in c.action, deck.deck.values())), n) player_2_cards = random.sample(list(filter(lambda c: 'turn around' in c.action, deck.deck.values())), n) #player_1_cards = deck.draw_cards(40) #player_2_cards = deck.draw_cards(40) @@ -429,7 +505,15 @@ if __name__ == "__main__": cards_1 = [(0, c) for c in player_1_cards] cards_2 = [(1, c) for c in player_2_cards] - chosen_cards = list(zip(cards_1, cards_2)) + player_3_cards = random.sample(list(filter(lambda c: 'turn around' in c.action, deck.deck.values())), n) + player_4_cards = random.sample(list(filter(lambda c: 'turn around' in c.action, deck.deck.values())), n) + #player_1_cards = deck.draw_cards(40) + #player_2_cards = deck.draw_cards(40) + + cards_3 = [(0, c) for c in player_3_cards] + cards_4 = [(1, c) for c in player_4_cards] + + chosen_cards = list(zip(cards_1, cards_2, cards_3, cards_4)) b = Board() print(b)