|
| 1 | +# |
| 2 | +# Unsupported browser type! |
| 3 | +# The Garage officially supports Chrome 85+, Microsoft Edge 85+, Firefox 78+, Opera 71+ |
| 4 | +# The Garage DOES NOT support Safari |
| 5 | +# Your browser is: Safari 16.3 |
| 6 | +# |
| 7 | +# If you cannot switch to a different browser, consider downloading Rumblebot, our command line tool |
| 8 | +# https://rr-docs.readthedocs.io/en/latest/rumblebot.html |
| 9 | +# |
| 10 | + |
| 11 | + |
| 12 | +from rumblelib import * |
| 13 | +from typing import ( |
| 14 | + Dict, |
| 15 | + NamedTuple, |
| 16 | + Sequence, |
| 17 | + Hashable, |
| 18 | + List, |
| 19 | + Optional, |
| 20 | + Tuple, |
| 21 | + Set, |
| 22 | +) |
| 23 | +from collections import namedtuple |
| 24 | +import random |
| 25 | + |
| 26 | +############################################################ |
| 27 | +### To implement new strategies rewrite make_obj_plan()! ### |
| 28 | +############################################################ |
| 29 | + |
| 30 | + |
| 31 | +robot_actions: Dict[ |
| 32 | + str, Action |
| 33 | +] = {} # Global variable to store all actions in init_turn() and to be used in robot() |
| 34 | + |
| 35 | + |
| 36 | +shift_to_direction: Dict[Tuple, Direction] = { |
| 37 | + (1, 0): Direction.East, |
| 38 | + (-1, 0): Direction.West, |
| 39 | + (0, 1): Direction.South, |
| 40 | + (0, -1): Direction.North, |
| 41 | +} |
| 42 | + |
| 43 | + |
| 44 | +# A plan contains an .action for an .id |
| 45 | +# Plans are selected by *lowest* .score such that for each .target there is only one selection |
| 46 | +Plan = namedtuple( |
| 47 | + "Plan", |
| 48 | + [ |
| 49 | + "id", # str: id of object |
| 50 | + "target", # Hashable: only one target will be selected from all equal targets |
| 51 | + "score", # float: minimum score is highest priority |
| 52 | + "action", # Action: action to be executed]) |
| 53 | + ], |
| 54 | +) |
| 55 | + |
| 56 | + |
| 57 | +def make_info(state: State) -> Dict: |
| 58 | + """ |
| 59 | + Return anything you want to pre-calculate at the beginning of a turn |
| 60 | + """ |
| 61 | + return {} |
| 62 | + |
| 63 | + |
| 64 | +def make_obj_plan(state: State, info: Dict, obj: Obj) -> Sequence[Plan]: |
| 65 | + """ |
| 66 | + Should emit a list of plans for the current object |
| 67 | + """ |
| 68 | + result: List[Plan] = [] |
| 69 | + |
| 70 | + enemies = state.objs_by_team(state.other_team) |
| 71 | + |
| 72 | + if not enemies: |
| 73 | + return [] |
| 74 | + |
| 75 | + closest_enemies, distance = find_closest(obj, enemies, get_dist) |
| 76 | + closest_enemy = random.choice(closest_enemies) |
| 77 | + |
| 78 | + shifts = get_directions_to(closest_enemy.coords - obj.coords) |
| 79 | + |
| 80 | + for shift in shifts: |
| 81 | + assert shift != (0, 0) # should never happen |
| 82 | + |
| 83 | + next_pos = obj.coords + Coords(shift.x, shift.y) |
| 84 | + |
| 85 | + if is_spawn_turn(state.turn + 1) and not is_inside_nonspawn(next_pos): |
| 86 | + continue |
| 87 | + |
| 88 | + direction = shift_to_direction[shift] |
| 89 | + |
| 90 | + next_obj = state.obj_by_coords(next_pos) |
| 91 | + |
| 92 | + if next_obj is not None and next_obj.team == state.other_team: |
| 93 | + action = Action.attack(direction) |
| 94 | + else: |
| 95 | + action = Action.move(direction) |
| 96 | + |
| 97 | + plan = Plan( |
| 98 | + id=obj.id, |
| 99 | + target=next_pos, |
| 100 | + score=distance, |
| 101 | + action=action, |
| 102 | + ) |
| 103 | + result.append(plan) |
| 104 | + |
| 105 | + return result |
| 106 | + |
| 107 | + |
| 108 | +############################################################## |
| 109 | +### Following functions probably do not have to be changed ### |
| 110 | +############################################################## |
| 111 | + |
| 112 | +Shift = namedtuple("Shift", "x y") |
| 113 | + |
| 114 | + |
| 115 | +def init_turn(state: State): |
| 116 | + """ |
| 117 | + run once for each turn |
| 118 | + use this to initialize global variables to be used by all units |
| 119 | + """ |
| 120 | + global robot_actions |
| 121 | + |
| 122 | + try: |
| 123 | + info = make_info(state) |
| 124 | + plans = make_plans(state, info) |
| 125 | + selected_actions = select_plans(plans) |
| 126 | + robot_actions = selected_actions |
| 127 | + except Exception as exc: |
| 128 | + import traceback |
| 129 | + |
| 130 | + traceback.print_exc() |
| 131 | + |
| 132 | + |
| 133 | +def robot(state: State, unit: Obj) -> Optional[Action]: |
| 134 | + """ |
| 135 | + called for each bot and needs to return an action |
| 136 | + """ |
| 137 | + |
| 138 | + return robot_actions.get(unit.id) # bots without plans will return None and be idle |
| 139 | + |
| 140 | + |
| 141 | +def make_plans(state: State, info: Dict) -> List[Plan]: |
| 142 | + plans: List[Plan] = [] |
| 143 | + |
| 144 | + for obj in state.objs_by_team(state.our_team): |
| 145 | + plans.extend(make_obj_plan(state, info, obj)) |
| 146 | + |
| 147 | + return plans |
| 148 | + |
| 149 | + |
| 150 | +def select_plans(plans: List[Plan]) -> Dict[str, Action]: |
| 151 | + # Simple greedy selection by lowest score |
| 152 | + targets_used: Set[Hashable] = set() |
| 153 | + random.shuffle(plans) # to avoid systematic priority effects |
| 154 | + plans = sorted(plans, key=lambda x: x.score) |
| 155 | + |
| 156 | + result: Dict[str, Action] = {} |
| 157 | + |
| 158 | + for plan in plans: |
| 159 | + if plan.id in result or ( |
| 160 | + plan.target is not None and plan.target in targets_used |
| 161 | + ): |
| 162 | + continue |
| 163 | + |
| 164 | + result[plan.id] = plan.action |
| 165 | + targets_used.add(plan.target) |
| 166 | + |
| 167 | + return result |
| 168 | + |
| 169 | + |
| 170 | +def dist_from_center(coord: Coords): |
| 171 | + # "octagonal distance" |
| 172 | + dx = coord.x - 9 |
| 173 | + dy = coord.y - 9 |
| 174 | + return max(abs(dx), abs(dy), abs(dx) + abs(dy) - 4) |
| 175 | + |
| 176 | + |
| 177 | +def is_spawn_turn(turn: int) -> bool: |
| 178 | + return turn % 10 == 0 |
| 179 | + |
| 180 | + |
| 181 | +def is_inside_field(coord: Coords): |
| 182 | + return dist_from_center(coord) <= 8 |
| 183 | + |
| 184 | + |
| 185 | +def is_inside_nonspawn(coord: Coords): |
| 186 | + return dist_from_center(coord) < 8 |
| 187 | + |
| 188 | + |
| 189 | +def is_spawn_region(coord: Coords): |
| 190 | + return dist_from_center(coord) == 8 |
| 191 | + |
| 192 | + |
| 193 | +def get_dist(obj1: Obj, obj2: Obj) -> int: |
| 194 | + return obj1.coords.walking_distance_to(obj2.coords) |
| 195 | + |
| 196 | + |
| 197 | +def find_closest_idx(obj, others, dist_func) -> Tuple[List[int], Optional[int]]: |
| 198 | + """ |
| 199 | + Return indices of position from poses2 which is closest to pos1 |
| 200 | + Also return distance |
| 201 | + """ |
| 202 | + if not others: |
| 203 | + return [], None |
| 204 | + |
| 205 | + distances = [dist_func(obj, other) for other in others] |
| 206 | + |
| 207 | + shortest_distance = min(distances) |
| 208 | + |
| 209 | + indices = [ |
| 210 | + i for i, distance in enumerate(distances) if distance == shortest_distance |
| 211 | + ] |
| 212 | + |
| 213 | + return ( |
| 214 | + indices, |
| 215 | + shortest_distance, |
| 216 | + ) |
| 217 | + |
| 218 | + |
| 219 | +def find_closest(obj, others, dist_func): |
| 220 | + closest_indices, distance = find_closest_idx(obj, others, get_dist) |
| 221 | + return [others[i] for i in closest_indices], distance |
| 222 | + |
| 223 | + |
| 224 | +def get_directions_to(shift: Shift) -> Set[Shift]: |
| 225 | + """ |
| 226 | + Returns all directions that would bring you closer along shift |
| 227 | + """ |
| 228 | + if shift == (0, 0): |
| 229 | + return {Shift(0, 0)} |
| 230 | + |
| 231 | + result: Set[Shift] = set() |
| 232 | + |
| 233 | + if shift.x > 0: |
| 234 | + result.add(Shift(1, 0)) |
| 235 | + |
| 236 | + if shift.x < 0: |
| 237 | + result.add(Shift(-1, 0)) |
| 238 | + |
| 239 | + if shift.y > 0: |
| 240 | + result.add(Shift(0, 1)) |
| 241 | + |
| 242 | + if shift.y < 0: |
| 243 | + result.add(Shift(0, -1)) |
| 244 | + |
| 245 | + return result |
| 246 | + |
0 commit comments