575 lines
23 KiB
Python
575 lines
23 KiB
Python
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 <count> 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, 10)
|
|
|
|
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):
|
|
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 = 12 # number of tiles in x direction
|
|
y_dims = 6 # number of tiles in y direction
|
|
|
|
def __init__(self):
|
|
self.board = {}
|
|
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 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)
|
|
|