import random import sys random.seed(0) class Card: possible_moves = ['forward', 'forward x2', 'forward x3', 'backward', 'turn left', 'turn right', 'turn around'] card_counter = 0 def __init__(self): self.number = Card.card_counter Card.card_counter += 1 self.action = random.choice(Card.possible_moves) self.priority = random.randint(0, 100) def __str__(self): return "Card No. " + str(self.number) + " " + self.action + " " + str(self.priority) def __repr__(self): return self.action + " (" + str(self.priority) + ")" class CardDeck: def __init__(self, n=84): self.deck = {} # generate cards for i in range(0, n): self.deck[i] = Card() self.dealt = set() self.discard_pile = set() def draw_cards(self, n=1): available = set(self.deck.keys()).difference(self.dealt) # print("{} cards are available".format(len(available))) if len(available) < n: drawn = list(available) # give out remaining cards # print("drawing remaining {} cards".format(len(drawn))) self.dealt = self.dealt.union(drawn) # put the cards from the discard pile back into the game self.dealt = self.dealt - self.discard_pile self.discard_pile = set() # reset the discard pile # draw rest of cards available = set(self.deck.keys()).difference(self.dealt) # print("drawing another {} cards".format(n - len(drawn))) drawn += random.sample(available, n - len(drawn)) else: drawn = random.sample(available, n) # print("cards drawn: {}".format(drawn)) self.dealt = self.dealt.union(drawn) return [self.deck[i] for i in drawn] def return_cards(self, cards): self.discard_pile = self.discard_pile.union(set([c.number for c in cards])) pass class Robot: # dictionary mapping the current orientation and a turn command to the resulting orientation resulting_orientation = { '^': {'turn left': '<', 'turn right': '>', 'turn around': 'v'}, '>': {'turn left': '^', 'turn right': 'v', 'turn around': '<'}, 'v': {'turn left': '>', 'turn right': '<', 'turn around': '^'}, '<': {'turn left': 'v', 'turn right': '^', 'turn around': '>'}, } # dictionary mapping the current orientation and the target orientation to the necessary turn command necessary_turn = { '^': {'>': 'turn right', 'v': 'turn around', '<': 'turn left'}, '>': {'v': 'turn right', '<': 'turn around', '^': 'turn left'}, 'v': {'<': 'turn right', '^': 'turn around', '>': 'turn left'}, '<': {'^': 'turn right', '>': 'turn around', 'v': 'turn left'}, } # dictionary mapping an orientation to its opposite opposites = {'^': 'v', '>': '<', 'v': '^', '<': '>'} def __init__(self, x, y, orientation, marker_id, board): self.x = x self.y = y self.orientation = orientation self.marker_id = marker_id self.damage = 0 self.collected_flags = set() self.board = board # 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.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: tiles.append(current_tile) return tiles 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.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 else: if neighbor_tile.is_empty(): return True elif neighbor_tile.modifier == '#': # if there's a wall on the neighbor tile the robot cannot be pushed there return False else: # this means there's another robot on the neighbor tile -> check if it can be pushed away return neighbor_tile.occupant.is_pushable(direction) def has_opposite_orientation(self, direction): opposites = [('^', 'v'), ('>', '<'), ('v', '^'), ('<', '>')] return (self.orientation, direction) in opposites def get_turn_direction(self, target_orienation): return Robot.necessary_turn[self.orientation][target_orienation] def get_opposite_orientation(self): return Robot.opposites[self.orientation] def turn(self, type): # change the orientation of the robot self.orientation = Robot.resulting_orientation[self.orientation][type] return "{}, {}".format(type, self.marker_id) def move(self, type): # move the robot forward or backward # this involves tile = self.get_tile() if type == 'forward': target_tile = self.get_adjecent_tile(self.orientation) if target_tile.occupant is not None: print("error: target tile is not empty") sys.exit(1) tile.occupant = None # delete the robot from the current tile target_tile.occupant = self # place the robot in the next tile self.x = target_tile.x self.y = target_tile.y # return the move for sending to the controller return "forward, {}".format(self.marker_id) elif type == 'backward': opposite_orientation = self.get_opposite_orientation() target_tile = self.get_adjecent_tile(opposite_orientation) if target_tile.occupant is not None: print("error: target tile is not empty") sys.exit(1) tile.occupant = None # delete the robot from the current tile target_tile.occupant = self # place the robot in the next tile self.x = target_tile.x self.y = target_tile.y # return the move for sending to the controller return "backward, {}".format(self.marker_id) else: print("error: invalid move") sys.exit(1) def nop(self): # do nothing command return "nop, {}".format(self.marker_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 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 take_damage(self, count): self.damage = min(self.damage + count, 4) def heal_damage(self, count): self.damage = max(self.damage - count, 0) def pick_up_flag(self, flag): self.collected_flags.add(flag) def __str__(self): return str(self.marker_id) class Tile: # possible modifiers: # # : wall (robot is blocked from moving there) # [<, >, ^, v] : conveyors (robot is pushed to the next tile) # + : rotation in positive direction (robot is rotated ccw) # - : rotation in negative direction (robot is rotated cw) # p : pit (robot takes damage) # r : repair station (robot heals damage) # [a,b,c,d] : flag (robot scores) # # occupant: Robot that is standing on the tile def __init__(self, x, y, modifier=None): if modifier == ' ': self.modifier = None else: self.modifier = modifier self.occupant = None self.x = x self.y = y def get_neighbor_coordinates(self, direction): # get the coordinates of the neighboring tile in the given direction if direction == '^': return (self.x, self.y - 1) elif direction == '>': return (self.x + 1, self.y) elif direction == 'v': return (self.x, self.y + 1) elif direction == '<': return (self.x - 1, self.y) else: print("error: unknown direction") sys.exit(1) def is_empty(self): # check if the tile is non-empty and does not contain a wall return self.occupant is None and self.modifier != '#' def __str__(self): if self.is_empty(): if self.modifier is None: return ' ' else: return self.modifier else: if self.occupant is None: return self.modifier else: return str(self.occupant) def __repr__(self): return "({}, {}) occ: {} mod: {}".format(self.x, self.y, self.occupant, self.modifier) class Board: x_dims = 13 # number of tiles in x direction y_dims = 7 # number of tiles in y direction def __init__(self): self.board = self.read_board_from_file('board.txt') #self.read_board_from_file('board.txt') # for x in range(Board.x_dims + 2): # for y in range(Board.y_dims + 2): # if (x == 0) or (x == Board.x_dims + 1) or (y == 0) or (y == Board.y_dims + 1): # # place walls around the board # self.board[(x, y)] = Tile(x, y, '#') # elif y > 2 and y < 6 and x == 7: # self.board[(x, y)] = Tile(x, y, '#') # elif x == 1 and (y >= 1) and (y < 4): # 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[(5, 1)].modifier = '+' # self.board[(5, 4)].modifier = '-' # self.board[(2, 2)].modifier = 'p' # self.board[(3, 3)].modifier = 'r' # # # place flags near the corners of the board # self.board[(2,2)].modifier = 'a' # self.board[(Board.x_dims-1, 2)].modifier = 'b' # self.board[(Board.x_dims-1, Board.y_dims-1)].modifier = 'c' # self.board[(2, Board.y_dims-1)].modifier = 'd' # self.board[(2, 2)].modifier = '^' # self.board[(2, 1)].modifier = '<' self.robots = {} #self.robots[0] = Robot(1, 1, 'v', 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) #self.create_robot(1,1,'>', 7, 11) def read_board_from_file(self, file): fh = open('board.txt').readlines() Board.x_dims = len(fh[0].strip()) - 2 Board.y_dims = len(fh) - 2 board = {} for y, line in enumerate(fh): for x, tile_modifier in enumerate(line.strip()): board[(x,y)] = Tile(x, y, tile_modifier) return board def create_robot(self, x, y, orientation, player_id, marker_id): new_robot = Robot(x, y, orientation, marker_id, self.board) self.robots[player_id] = new_robot return new_robot def handle_push(self, direction, pushed_robot, forward=True, pushing_robot=None): cmd_list = [] # push robot out of the way if pushed_robot.orientation == direction: if forward: # the pushed robot can just drive forward cmd_list += self.handle_single_action('forward', pushed_robot) else: # the pushed robot can just drive backward cmd_list += self.handle_single_action('backward', pushed_robot) elif pushed_robot.has_opposite_orientation(direction): if forward: # the pushed robot can drive backward cmd_list += self.handle_single_action('backward', pushed_robot) else: # the pushed robot drives forward cmd_list += self.handle_single_action('forward', pushed_robot) else: # we first have to turn the pushed robot s.t. it faces in the same orientation as the # pushing robot turn_direction = pushed_robot.get_turn_direction(direction) cmd_list += self.handle_single_action(turn_direction, pushed_robot) if forward: # then the pushed robot drives one step forward cmd_list += self.handle_single_action('forward', pushed_robot) else: # if its pushed backward it instead drives on step backward cmd_list += self.handle_single_action('backward', pushed_robot) # afterwards we turn the robot back to the original orientation if turn_direction == 'turn left': turn_back_direction = 'turn right' elif turn_direction == 'turn right': turn_back_direction = 'turn left' else: print("error: invalid turn direction") sys.exit(1) cmd_list += self.handle_single_action(turn_back_direction, pushed_robot) if pushing_robot is not None: # now the tile should be empty so the pushing robot can move into the tile if forward: cmd_list.append(pushing_robot.move('forward')) else: cmd_list.append(pushing_robot.move('backward')) return cmd_list def handle_single_action(self, action, robot): cmd_list = [] if 'forward' in action: # driving forward if "x2" in action: move_count = 2 elif "x3" in action: move_count = 3 else: move_count = 1 accessed_tiles = robot.get_accessed_tiles(move_count) for tile in accessed_tiles: if tile is None: # this case should not happen print("error: unknown state occured") sys.exit(1) elif tile.is_empty(): # if the tile is empty we can just move there cmd_list.append(robot.move('forward')) elif tile.modifier == '#': # robot hits a wall -> stop the robot cmd_list.append(robot.nop()) return cmd_list elif any([(tile.x, tile.y) == (r.x, r.y) for r in self.robots.values()]): # robots hits a tile occupied by another robot pushed_robot = next(filter(lambda r: (tile.x, tile.y) == (r.x, r.y), self.robots.values())) if pushed_robot.is_pushable(robot.orientation): # check if robot is pushable in the given direction cmd_list += self.handle_push(direction=robot.orientation, pushed_robot=pushed_robot, forward=True, pushing_robot=robot) else: cmd_list.append(robot.nop()) return cmd_list else: # this case should not happen print("error: unknown state occured") sys.exit(1) elif action == 'backward': # basically do the same as with forward accessed_tiles = robot.get_accessed_tiles(1, forward=False) for tile in accessed_tiles: if tile is None: # this case should not happen print("error: unknown state occured") sys.exit(1) elif tile.is_empty(): # if the tile is empty we can just move there cmd_list.append(robot.move('backward')) elif tile.modifier == '#': # robot hits a wall -> stop the robot cmd_list.append(robot.nop()) return cmd_list elif any([(tile.x, tile.y) == (r.x, r.y) for r in self.robots.values()]): # robots hits a tile occupied by another robot pushed_robot = next(filter(lambda r: (tile.x, tile.y) == (r.x, r.y), self.robots.values())) if pushed_robot.is_pushable(Robot.opposites[robot.orientation]): # check if robot is pushable in the given direction cmd_list += self.handle_push(direction=robot.orientation, pushed_robot=pushed_robot, forward=False, pushing_robot=robot) else: cmd_list.append(robot.nop()) return cmd_list else: # this case should not happen print("error: unknown state occured") sys.exit(1) else: # this means we have a turn action cmd_list.append(robot.turn(action)) return cmd_list def handle_board_element(self, robot): cmd_list = [] tile = self.board[(robot.x, robot.y)] if tile.modifier is None: return cmd_list elif tile.modifier in ['^', '>', 'v', '<']: # board element pushes the robot to next tile if robot.is_pushable(tile.modifier): cmd_list += self.handle_push(direction=tile.modifier, pushed_robot=robot, forward=True) else: cmd_list.append(robot.nop()) elif tile.modifier == '+': cmd_list.append(robot.turn('turn left')) elif tile.modifier == '-': cmd_list.append(robot.turn('turn right')) elif tile.modifier == 'p': robot.take_damage(1) elif tile.modifier == 'r': robot.heal_damage(1) elif tile.modifier in 'abcd': robot.pick_up_flag(tile.modifier) return cmd_list def apply_actions(self, cards): cmd_list = [] # apply the actions to the board and generate a list of movement commands for i, phase in enumerate(cards): # process register phases print("processing phase {}".format(i+1)) # sort actions by priority sorted_actions = sorted(phase, key=lambda a: a[1].priority) for a in sorted_actions: robot_id = a[0] robot = self.robots[robot_id] action = a[1].action print("robot {} action {}".format(robot, action)) cmd_list += self.handle_single_action(action, robot) print(self) # apply the actions caused by board elements at the end of the phase cmd_list += self.apply_board_element_actions() print(self) 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): #output = '#' * (Board.x_dims + 2) + '\n' output = '' for y in range(Board.y_dims+2): for x in range(Board.x_dims+2): if any((r.x, r.y) == (x,y) for (r_id, r) in self.robots.items()): r = next(filter(lambda r: (r[1].x,r[1].y) == (x,y), self.robots.items())) output += str(r[0]) else: output += str(self.board[(x, y)]) output += '\n' #output += '#' * (Board.x_dims + 2) for r_id, r in self.robots.items(): output += "Robot {}: {}\n".format(r_id, r.orientation) return output if __name__ == "__main__": n = 5 deck = CardDeck(n=1000) player_1_cards = deck.draw_cards(200) #player_2_cards = deck.draw_cards(200) #player_3_cards = deck.draw_cards(200) #player_4_cards = deck.draw_cards(200) #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_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) cards_1 = [(0, c) for c in player_1_cards] #cards_2 = [(1, c) for c in player_2_cards] #cards_3 = [(2, c) for c in player_3_cards] #cards_4 = [(3, c) for c in player_4_cards] #chosen_cards = list(zip(cards_1, cards_2, cards_3, cards_4)) chosen_cards = list(zip(cards_1)) b = Board() print(b) b.apply_actions(chosen_cards)