diff --git a/battlecode25/engine/game/constants.py b/battlecode25/engine/game/constants.py index 4ecb9f6..00ae5b1 100644 --- a/battlecode25/engine/game/constants.py +++ b/battlecode25/engine/game/constants.py @@ -24,7 +24,7 @@ class GameConstants: GAME_MAX_NUMBER_OF_ROUNDS = 2000 - ROBOT_BYTECODE_LIMIT = 15000 + ROBOT_BYTECODE_LIMIT = 17500 TOWER_BYTECODE_LIMIT = 20000 @@ -138,6 +138,15 @@ class GameConstants: # The increase in extra damage for ally towers for upgrading a defense tower EXTRA_TOWER_DAMAGE_LEVEL_INCREASE = 2 + # The money cost of completing a resource pattern + COMPLETE_RESOURCE_PATTERN_COST = 200 + + # Resource patterns must exist for this many turns before they start producing resources + RESOURCE_PATTERN_ACTIVE_DELAY = 50 + + # A robot takes this much damage every time it ends a turn with 0 paint + NO_PAINT_DAMAGE = 20 + # ************************ # ****** COOLDOWNS ******* # ************************ @@ -155,7 +164,7 @@ class GameConstants: BUILD_ROBOT_COOLDOWN = 10 # The amount added to the action cooldown counter after attacking (as a mopper for the swing attack) - ATTACK_MOPPER_SWING_COOLDOWN = 40 + ATTACK_MOPPER_SWING_COOLDOWN = 20 # THe amount added to the action cooldown counter after transferring paint PAINT_TRANSFER_COOLDOWN = 10 @@ -170,6 +179,9 @@ class GameConstants: # The maximum squared radius a robot can send a message to MESSAGE_RADIUS_SQUARED = 20 + # The maximum squared radius that a tower can broadcast messages to + BROADCAST_RADIUS_SQUARED = 80 + # The maximum number of rounds a message will exist for MESSAGE_ROUND_DURATION = 5 diff --git a/battlecode25/engine/game/game.py b/battlecode25/engine/game/game.py index 6e3f5c2..06dbb1c 100644 --- a/battlecode25/engine/game/game.py +++ b/battlecode25/engine/game/game.py @@ -47,7 +47,8 @@ def __init__(self, code, initial_map: InitialMap, game_fb: GameFB, game_args): self.game_fb = game_fb self.pattern = self.create_patterns() self.resource_pattern_centers = [] - self.resouce_pattern_centers_by_loc = [Team.NEUTRAL] * total_area + self.resource_pattern_centers_by_loc = [Team.NEUTRAL] * total_area + self.resource_pattern_lifetimes = [0] * total_area self.code = code self.debug = game_args.debug self.running = True @@ -102,6 +103,9 @@ def has_ruin(self, loc: MapLocation): def has_wall(self, loc: MapLocation): return self.walls[loc.y * self.width + loc.x] + def has_resource_pattern_center(self, loc: MapLocation, team: Team): + return self.resource_pattern_centers_by_loc[loc.y * self.width + loc.x] == team + def get_paint_num(self, loc: MapLocation): return self.paint[loc.y * self.width + loc.x] @@ -139,7 +143,8 @@ def get_map_info(self, team, loc): case 2: mark_type = PaintType.ALLY_SECONDARY passable = not self.walls[idx] and not self.ruins[idx] - return MapInfo(loc, passable, self.walls[idx], paint_type, mark_type, self.ruins[idx]) + resource_pattern_center = self.resource_pattern_centers_by_loc[idx] == team + return MapInfo(loc, passable, self.walls[idx], paint_type, mark_type, self.ruins[idx], resource_pattern_center) def spawn_robot(self, type: UnitType, loc: MapLocation, team: Team, id=None): if id is None: @@ -245,11 +250,15 @@ def set_winner(self, team, domination_factor): self.domination_factor = domination_factor def update_resource_patterns(self): - for i, center in enumerate(self.resource_pattern_centers): - team = self.resouce_pattern_centers_by_loc[self.loc_to_index(center)] + for i, center in enumerate(self.resource_pattern_centers[:]): + idx = self.loc_to_index(center) + team = self.resource_pattern_centers_by_loc[idx] if not self.simple_check_pattern(center, Shape.RESOURCE, team): self.resource_pattern_centers.pop(i) - self.resouce_pattern_centers_by_loc[self.loc_to_index(center)] = Team.NEUTRAL + self.resource_pattern_centers_by_loc[idx] = Team.NEUTRAL + self.resource_pattern_lifetimes[idx] = 0 + else: + self.resource_pattern_lifetimes[idx] += 1 def serialize_team_info(self): coverage_a = math.floor(self.team_info.get_tiles_painted(Team.A) / self.area_without_walls * 1000) @@ -259,13 +268,19 @@ def serialize_team_info(self): def complete_resource_pattern(self, team, loc): idx = self.loc_to_index(loc) - if self.resouce_pattern_centers_by_loc[idx] != Team.NEUTRAL: + if self.resource_pattern_centers_by_loc[idx] != Team.NEUTRAL: return self.resource_pattern_centers.append(loc) - self.resouce_pattern_centers_by_loc[idx] = team + self.resource_pattern_centers_by_loc[idx] = team + self.resource_pattern_lifetimes[idx] = 0 def count_resource_patterns(self, team): - return [self.resouce_pattern_centers_by_loc[self.loc_to_index(loc)] == team for loc in self.resource_pattern_centers].count(True) + count = 0 + for loc in self.resource_pattern_centers: + idx = self.loc_to_index(loc) + if self.resource_pattern_centers_by_loc[idx] == team and self.resource_pattern_lifetimes[idx] >= GameConstants.RESOURCE_PATTERN_ACTIVE_DELAY: + count += 1 + return count def update_defense_towers(self, team, new_tower_type): if new_tower_type == UnitType.LEVEL_TWO_DEFENSE_TOWER or new_tower_type == UnitType.LEVEL_THREE_DEFENSE_TOWER: @@ -547,6 +562,8 @@ def create_methods(self, rc: RobotController): 'can_send_message': (rc.can_send_message, 50), 'send_message': (rc.send_message, 50), 'read_messages': (rc.read_messages, 10), + 'can_broadcast_message': (rc.can_broadcast_message, 5), + 'broadcast_message': (rc.broadcast_message, 50), 'can_transfer_paint': (rc.can_transfer_paint, 5), 'transfer_paint': (rc.transfer_paint, 5), 'can_upgrade_tower': (rc.can_upgrade_tower, 2), @@ -561,11 +578,11 @@ def create_methods(self, rc: RobotController): def create_patterns(self): resource_pattern = [ - [True, False, True, False, True], - [False, True, False, True, False], + [True, True, False, True, True], [True, False, False, False, True], - [False, True, False, True, False], - [True, False, True, False, True] + [False, False, True, False, False], + [True, False, False, False, True], + [True, True, False, True, True] ] money_tower_pattern = [ [False, True, True, True, False], diff --git a/battlecode25/engine/game/map_info.py b/battlecode25/engine/game/map_info.py index 389b10e..0aac9a6 100644 --- a/battlecode25/engine/game/map_info.py +++ b/battlecode25/engine/game/map_info.py @@ -2,13 +2,14 @@ from .paint_type import PaintType class MapInfo: - def __init__(self, loc: MapLocation, passable: bool, wall: bool, paint: PaintType, mark: PaintType, ruin: bool): + def __init__(self, loc: MapLocation, passable: bool, wall: bool, paint: PaintType, mark: PaintType, ruin: bool, resource_pattern_center: bool): self.loc = loc self.passable = passable self.wall = wall self.paint = paint self.mark = mark self.ruin = ruin + self.resource_pattern_center = resource_pattern_center def is_passable(self) -> bool: return self.passable @@ -27,6 +28,9 @@ def get_mark(self) -> PaintType: def get_map_location(self) -> MapLocation: return self.loc + + def is_resource_pattern_center(self) -> bool: + return self.resource_pattern_center def __str__(self): return f"Location: {self.loc} \n \ @@ -34,4 +38,5 @@ def __str__(self): is_wall: {self.wall} \n \ has_ruin: {self.ruin} \n \ paint: {self.paint} \n \ - mark: {self.mark} \n" \ No newline at end of file + mark: {self.mark} \n \ + is_resource_pattern_center: {self.resource_pattern_center}\n" \ No newline at end of file diff --git a/battlecode25/engine/game/robot.py b/battlecode25/engine/game/robot.py index 3feb01f..afb4ae5 100644 --- a/battlecode25/engine/game/robot.py +++ b/battlecode25/engine/game/robot.py @@ -109,19 +109,20 @@ def process_end_of_turn(self): if self.type.is_robot_type(): multiplier = GameConstants.MOPPER_PAINT_PENALTY_MULTIPLIER if self.type == UnitType.MOPPER else 1 + count = 0 + for adj_loc in self.game.get_all_locations_within_radius_squared(self.loc, 2): + adj_robot = self.game.get_robot(adj_loc) + if adj_robot and adj_robot != self and adj_robot.team == self.team: + count += 1 + if self.game.team_from_paint(paint_status) == self.team: - paint_penalty = 0 + paint_penalty = count elif self.game.team_from_paint(paint_status) == Team.NEUTRAL: paint_penalty = GameConstants.PENALTY_NEUTRAL_TERRITORY * multiplier + paint_penalty += count else: - paint_penalty = GameConstants.PENALTY_ENEMY_TERRITORY * multiplier - count = 0 - for adj_loc in self.game.get_all_locations_within_radius_squared(self.loc, 2): - adj_robot = self.game.get_robot(adj_loc) - if adj_robot and adj_robot != self and adj_robot.team == self.team: - count += 1 + paint_penalty = GameConstants.PENALTY_ENEMY_TERRITORY * multiplier paint_penalty += 2 * count - self.game.game_fb.add_indicator_string(f"Round {self.game.round}, Location {self.loc.__str__()}, Penalty {paint_penalty}") self.add_paint(-paint_penalty) self.has_tower_area_attacked = False @@ -129,12 +130,8 @@ def process_end_of_turn(self): self.message_buffer.next_round() self.sent_message_count = 0 - if self.paint == 0: - self.turns_without_paint += 1 - else: - self.turns_without_paint = 0 - if self.type.is_robot_type() and self.turns_without_paint >= GameConstants.MAX_TURNS_WITHOUT_PAINT: - self.game.destroy_robot(self.id) + if self.type.is_robot_type() and self.paint == 0: + self.add_health(-GameConstants.NO_PAINT_DAMAGE) self.game.game_fb.end_turn(self.id, self.health, self.paint, self.movement_cooldown, self.action_cooldown, self.bytecodes_used, self.loc) self.rounds_alive += 1 diff --git a/battlecode25/engine/game/robot_controller.py b/battlecode25/engine/game/robot_controller.py index c326674..e9a10d4 100644 --- a/battlecode25/engine/game/robot_controller.py +++ b/battlecode25/engine/game/robot_controller.py @@ -358,6 +358,7 @@ def attack(self, loc: MapLocation, use_secondary_color: bool=False) -> None: self.game.set_paint(loc, 0) else: # Tower + attacked = False if loc is None: self.robot.has_tower_area_attacked = True damage = self.robot.type.aoe_attack_strength + self.game.team_info.get_defense_damage_increase(self.robot.team) @@ -365,6 +366,7 @@ def attack(self, loc: MapLocation, use_secondary_color: bool=False) -> None: for new_loc in all_locs: target_robot = self.game.get_robot(new_loc) if target_robot and target_robot.team != self.robot.team: + attacked = True target_robot.add_health(-damage) self.game.game_fb.add_attack_action(target_robot.id) self.game.game_fb.add_damage_action(target_robot.id, damage) @@ -373,9 +375,12 @@ def attack(self, loc: MapLocation, use_secondary_color: bool=False) -> None: self.robot.has_tower_single_attacked = True target_robot = self.game.get_robot(loc) if target_robot and target_robot.team != self.robot.team: + attacked = True target_robot.add_health(-damage) self.game.game_fb.add_attack_action(target_robot.id) self.game.game_fb.add_damage_action(target_robot.id, damage) + if attacked: + self.game.team_info.add_coins(self.robot.team, self.robot.type.attack_money_bonus) def assert_can_mop_swing(self, dir: Direction) -> None: self.assert_not_none(dir) @@ -400,14 +405,14 @@ def mop_swing(self, dir: Direction) -> None: self.robot.add_action_cooldown(GameConstants.ATTACK_MOPPER_SWING_COOLDOWN) swing_offsets = { - Direction.NORTH: ((-1, 1), (0, 1), (1, 1)), - Direction.SOUTH: ((-1, -1), (0, -1), (1, -1)), - Direction.EAST: ((1, -1), (1, 0), (1, 1)), - Direction.WEST: ((-1, -1), (-1, 0), (-1, 1)) + Direction.NORTH: ((-1, 1), (0, 1), (1, 1), (-1, 2), (0, 2), (1, 2)), + Direction.SOUTH: ((-1, -1), (0, -1), (1, -1), (-1, -2), (0, -2), (1, -2)), + Direction.EAST: ((1, -1), (1, 0), (1, 1), (2, -1), (2, 0), (2, 1)), + Direction.WEST: ((-1, -1), (-1, 0), (-1, 1), (-2, -1), (-2, 0), (-2, 1)) } target_ids = [] - for i in range(3): + for i in range(6): offset = swing_offsets[dir][i] new_loc = MapLocation(self.robot.loc.x + offset[0], self.robot.loc.y + offset[1]) if not self.game.on_the_map(new_loc): @@ -421,6 +426,7 @@ def mop_swing(self, dir: Direction) -> None: else: target_ids.append(0) self.game.game_fb.add_mop_action(target_ids[0], target_ids[1], target_ids[2]) + self.game.game_fb.add_mop_action(target_ids[3], target_ids[4], target_ids[5]) def can_paint(self, loc: MapLocation) -> bool: self.assert_not_none(loc) @@ -561,6 +567,10 @@ def assert_can_complete_resource_pattern(self, loc: MapLocation) -> None: self.assert_is_robot_type(self.robot.type) self.assert_can_act_location(loc, GameConstants.RESOURCE_PATTERN_RADIUS_SQUARED) + if self.game.team_info.get_coins(self.robot.team) < GameConstants.COMPLETE_RESOURCE_PATTERN_COST: + raise RobotError(f"Not enough money to complete resource pattern") + if self.game.has_resource_pattern_center(loc, self.robot.team): + raise RobotError("Can't complete a resource pattern that has already been completed") if not self.game.is_valid_pattern_center(loc): raise RobotError(f"Cannot complete resource pattern at ({loc.x}, {loc.y}) because it is too close to the edge of the map") if not self.game.simple_check_pattern(loc, Shape.RESOURCE, self.robot.team): @@ -576,6 +586,7 @@ def can_complete_resource_pattern(self, loc: MapLocation) -> bool: def complete_resource_pattern(self, loc: MapLocation) -> None: self.assert_can_complete_resource_pattern(loc) self.game.complete_resource_pattern(self.robot.team, loc) + self.game.team_info.add_coins(self.robot.team, -GameConstants.COMPLETE_RESOURCE_PATTERN_COST) self.game.game_fb.add_complete_resource_pattern_action(loc) # BUILDING FUNCTIONS @@ -649,6 +660,30 @@ def send_message(self, loc: MapLocation, message_content: int) -> None: self.robot.sent_message_count += 1 self.game.game_fb.add_message_action(target.id, message_content) + def assert_can_broadcast_message(self): + if not self.robot.type.is_robot_type(): + raise RobotError("Only towers can broadcast messages") + if self.robot.sent_message_count >= GameConstants.MAX_MESSAGES_SENT_TOWER: + raise RobotError("Tower has already sent too many messages this round") + + def can_broadcast_message(self) -> bool: + try: + self.assert_can_broadcast_message() + return True + except RobotError as e: + return False + + def broadcast_message(self, message_content: int) -> None: + self.assert_can_broadcast_message() + message_content &= 0xFFFFFFFF + message = Message(message_content, self.robot.id, self.game.round) + all_locs = self.game.get_all_locations_within_radius_squared(self.robot.loc, GameConstants.BROADCAST_RADIUS_SQUARED) + for loc in all_locs: + robot = self.game.get_robot(loc) + if robot is not None and robot.type.is_tower_type() and robot.team == self.robot.team and robot != self.robot: + robot.message_buffer.add_message(message) + self.robot.sent_message_count += 1 + def read_messages(self, round=-1) -> List[Message]: if round == -1: return self.robot.message_buffer.get_all_messages() diff --git a/battlecode25/engine/game/unit_type.py b/battlecode25/engine/game/unit_type.py index 8f62c05..85e78b8 100644 --- a/battlecode25/engine/game/unit_type.py +++ b/battlecode25/engine/game/unit_type.py @@ -16,24 +16,25 @@ class RobotAttributes: aoe_attack_strength: int paint_per_turn: int money_per_turn: int + attack_money_bonus: int class UnitType(Enum): # Define enum members with RobotAttributes dataclass - SOLDIER = RobotAttributes(200, 250, 5, 250, -1, 200, 10, 9, 50, -1, 0, 0) - SPLASHER = RobotAttributes(300, 400, 50, 150, -1, 300, 50, 4, -1, 100, 0, 0) - MOPPER = RobotAttributes(100, 300, 0, 50, -1, 100, 30, 2, -1, -1, 0, 0) + SOLDIER = RobotAttributes(200, 250, 5, 250, -1, 200, 10, 9, 50, -1, 0, 0, 0) + SPLASHER = RobotAttributes(300, 400, 50, 150, -1, 300, 50, 4, -1, 100, 0, 0, 0) + MOPPER = RobotAttributes(100, 300, 0, 50, -1, 100, 30, 2, -1, -1, 0, 0, 0) - LEVEL_ONE_PAINT_TOWER = RobotAttributes(0, 1000, 0, 1000, 1, 1000, 10, 9, 20, 10, 5, 0) - LEVEL_TWO_PAINT_TOWER = RobotAttributes(0, 2500, 0, 1500, 2, 1000, 10, 9, 20, 10, 10, 0) - LEVEL_THREE_PAINT_TOWER = RobotAttributes(0, 5000, 0, 2000, 3, 1000, 10, 9, 20, 10, 15, 0) + LEVEL_ONE_PAINT_TOWER = RobotAttributes(0, 1000, 0, 1000, 1, 1000, 10, 9, 20, 10, 5, 0, 0) + LEVEL_TWO_PAINT_TOWER = RobotAttributes(0, 2500, 0, 1500, 2, 1000, 10, 9, 20, 10, 10, 0, 0) + LEVEL_THREE_PAINT_TOWER = RobotAttributes(0, 5000, 0, 2000, 3, 1000, 10, 9, 20, 10, 15, 0, 0) - LEVEL_ONE_MONEY_TOWER = RobotAttributes(0, 1000, 0, 1000, 1, 1000, 10, 9, 20, 10, 0, 20) - LEVEL_TWO_MONEY_TOWER = RobotAttributes(0, 2500, 0, 1500, 2, 1000, 10, 9, 20, 10, 0, 30) - LEVEL_THREE_MONEY_TOWER = RobotAttributes(0, 5000, 0, 2000, 3, 1000, 10, 9, 20, 10, 0, 40) + LEVEL_ONE_MONEY_TOWER = RobotAttributes(0, 1000, 0, 1000, 1, 1000, 10, 9, 20, 10, 0, 20, 0) + LEVEL_TWO_MONEY_TOWER = RobotAttributes(0, 2500, 0, 1500, 2, 1000, 10, 9, 20, 10, 0, 30, 0) + LEVEL_THREE_MONEY_TOWER = RobotAttributes(0, 5000, 0, 2000, 3, 1000, 10, 9, 20, 10, 0, 40, 0) - LEVEL_ONE_DEFENSE_TOWER = RobotAttributes(0, 1000, 0, 2000, 1, 1000, 10, 16, 40, 20, 0, 0) - LEVEL_TWO_DEFENSE_TOWER = RobotAttributes(0, 2500, 0, 2500, 2, 1000, 10, 16, 50, 25, 0, 0) - LEVEL_THREE_DEFENSE_TOWER = RobotAttributes(0, 5000, 0, 3000, 3, 1000, 10, 16, 60, 30, 0, 0) + LEVEL_ONE_DEFENSE_TOWER = RobotAttributes(0, 1000, 0, 2000, 1, 1000, 10, 16, 40, 20, 0, 0, 20) + LEVEL_TWO_DEFENSE_TOWER = RobotAttributes(0, 2500, 0, 2500, 2, 1000, 10, 16, 50, 25, 0, 0, 30) + LEVEL_THREE_DEFENSE_TOWER = RobotAttributes(0, 5000, 0, 3000, 3, 1000, 10, 16, 60, 30, 0, 0, 40) # Read-only property accessors for attributes @property @@ -83,6 +84,10 @@ def paint_per_turn(self): @property def money_per_turn(self): return self.value.money_per_turn + + @property + def attack_money_bonus(self): + return self.value.attack_money_bonus def can_attack(self): return self == UnitType.SOLDIER or self == UnitType.SPLASHER diff --git a/battlecode25/stubs.py b/battlecode25/stubs.py index ef59250..4263316 100644 --- a/battlecode25/stubs.py +++ b/battlecode25/stubs.py @@ -391,6 +391,22 @@ def read_messages(round=-1) -> List[Message]: """ pass +def can_broadcast_message(self) -> bool: + """ + Returns true if this tower can broadcast a message. You can broadcast a message if this robot is a tower and the tower has + not yet sent the maximum number of messages this round (broadcasting a message to other towers counts as one message sent, even + if multiple towers receive the message). + """ + pass + +def broadcast_message(self, message_content: int) -> None: + """ + Broadcasts a message to all friendly towers within the broadcasting radius. This works the same as sendMessage, but it can only + be performed by towers and sends the message to all friendly towers within range simultaneously. The towers need not be connected + by paint to receive the message. + """ + pass + # TRANSFER PAINT FUNCTIONS def can_transfer_paint(target_location: MapLocation, amount: int) -> bool: