Skip to content

Commit

Permalink
Merge branch 'main' into public-release
Browse files Browse the repository at this point in the history
TheApplePieGod committed Jan 16, 2025
2 parents 3e76ef0 + efa57cc commit 91aab60
Showing 7 changed files with 134 additions and 47 deletions.
16 changes: 14 additions & 2 deletions battlecode25/engine/game/constants.py
Original file line number Diff line number Diff line change
@@ -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

41 changes: 29 additions & 12 deletions battlecode25/engine/game/game.py
Original file line number Diff line number Diff line change
@@ -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],
9 changes: 7 additions & 2 deletions battlecode25/engine/game/map_info.py
Original file line number Diff line number Diff line change
@@ -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,11 +28,15 @@ 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 \
passable: {self.passable} \n \
is_wall: {self.wall} \n \
has_ruin: {self.ruin} \n \
paint: {self.paint} \n \
mark: {self.mark} \n"
mark: {self.mark} \n \
is_resource_pattern_center: {self.resource_pattern_center}\n"
25 changes: 11 additions & 14 deletions battlecode25/engine/game/robot.py
Original file line number Diff line number Diff line change
@@ -109,32 +109,29 @@ 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
self.has_tower_single_attacked = False
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
45 changes: 40 additions & 5 deletions battlecode25/engine/game/robot_controller.py
Original file line number Diff line number Diff line change
@@ -358,13 +358,15 @@ 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)
all_locs = self.game.get_all_locations_within_radius_squared(self.robot.loc, self.robot.type.action_radius_squared)
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()
29 changes: 17 additions & 12 deletions battlecode25/engine/game/unit_type.py
Original file line number Diff line number Diff line change
@@ -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
16 changes: 16 additions & 0 deletions battlecode25/stubs.py
Original file line number Diff line number Diff line change
@@ -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:

0 comments on commit 91aab60

Please sign in to comment.