From 6da06fba538c1b5620ee786b8864382f66b92881 Mon Sep 17 00:00:00 2001 From: Cofee Coder <41608260+CoffeeCoder1015@users.noreply.github.com> Date: Sun, 22 Mar 2026 20:34:47 -0700 Subject: [PATCH 01/31] k_learning init --- bots/k_learning/builderbot/__init__.py | 9 ++ bots/k_learning/core/__init__.py | 183 +++++++++++++++++++++++++ bots/k_learning/main.py | 31 +++++ bots/k_learning/turret/__init__.py | 0 4 files changed, 223 insertions(+) create mode 100644 bots/k_learning/builderbot/__init__.py create mode 100644 bots/k_learning/core/__init__.py create mode 100644 bots/k_learning/main.py create mode 100644 bots/k_learning/turret/__init__.py diff --git a/bots/k_learning/builderbot/__init__.py b/bots/k_learning/builderbot/__init__.py new file mode 100644 index 0000000..034ce2f --- /dev/null +++ b/bots/k_learning/builderbot/__init__.py @@ -0,0 +1,9 @@ +from cambc import Controller + + +class BuilderBot: + def __init__(self): + pass + + def run(self, ct: Controller): + pass \ No newline at end of file diff --git a/bots/k_learning/core/__init__.py b/bots/k_learning/core/__init__.py new file mode 100644 index 0000000..d197e58 --- /dev/null +++ b/bots/k_learning/core/__init__.py @@ -0,0 +1,183 @@ +import sys + +from collections import deque +from cambc import Controller, Direction, EntityType, Position + +DIRECTIONS = [ + Direction.NORTH, Direction.NORTHEAST, Direction.EAST, Direction.SOUTHEAST, + Direction.SOUTH, Direction.SOUTHWEST, Direction.WEST, Direction.NORTHWEST, + Direction.CENTRE +] + +class Core: + def __init__(self): + self.spawned = 0 + self.reinvestment_budget = 0 # Available titanium for spawning + self.last_titanium = 0 + + # Adaptive Growth (25-round Sliding Window) + self.growth_history = deque(maxlen=25) + self.sum_x = 0 + self.sum_x2 = 0 + self.moving_avg_growth = 0 + self.variance = 0 + + # Opposite direction index pairs for symmetry: (N,S), (E,W), (NE,SW), (SE,NW) + self.pairs = [(0, 4), (2, 6), (1, 5), (3, 7)] + + # Adaptive Expense Tracking + self.reserve_buffer = 120 # Start with enough to build one full Harvester pipeline + + # Resource Drain Wave (Aggressive Spawning Mode) + self.drain_wave_active = False + self.drain_wave_rounds_left = 0 + self.DRAIN_WAVE_DURATION = 20 # Spawn every round for 20 rounds + self.enemy_core_pos = None # Targeted for directional spawning + self.CORE_MARKER_MAGIC = 0xCAFE + + def run(self, ct: Controller): + res = ct.get_global_resources() + titanium = res[0] + rnd = ct.get_current_round() + + # 0.1 Adaptive Expense Tracking + # If the swarm bottomed out the bank entirely (< 3 Ti for a conveyor), + # they wanted to spend more but starved. Ratchet up the hold buffer incrementally! + if rnd > 1 and titanium < 3: + self.reserve_buffer += 50 + print(f"[Core] Swarm Starved! Ratcheting reserve buffer to {self.reserve_buffer} Ti", file=sys.stderr) + elif rnd > 1 and titanium > self.reserve_buffer + 100: + # Slow decay to find the lowest safe floor + self.reserve_buffer = max(120, self.reserve_buffer - 1) + + # 0. Growth tracking (25-round Sliding Window) + if rnd > 0: + delta = max(0, titanium - self.last_titanium) + + # Incremental update logic: subtract old, add new + if len(self.growth_history) == 25: + old_x = self.growth_history[0] + self.sum_x -= old_x + self.sum_x2 -= old_x**2 + + self.growth_history.append(delta) + self.sum_x += delta + self.sum_x2 += delta**2 + + count = len(self.growth_history) + self.moving_avg_growth = self.sum_x / count + self.variance = max(0, (self.sum_x2 / count) - (self.moving_avg_growth ** 2)) + + # 0.5. Budget Accrual (Flow Economy) + # Reinvest 80% of growth if above safety threshold + if self.moving_avg_growth >= 1.2: + self.reinvestment_budget += self.moving_avg_growth * 0.8 + + # 0.6 Dynamic reserve floor — always tethered to live harvester cost + harv_c_early = ct.get_harvester_cost()[0] + conv_c_early = ct.get_conveyor_cost()[0] + min_reserve = harv_c_early + 3 * conv_c_early + self.reserve_buffer = max(self.reserve_buffer, min_reserve) + + self.last_titanium = titanium + + # 0.7 Marker Detection (Drain Wave Activation) + if not self.drain_wave_active and ct.get_action_cooldown() == 0: + for m_id in ct.get_nearby_buildings(): + if ct.get_entity_type(m_id) == EntityType.MARKER: + val = ct.get_marker_value(m_id) + if (val >> 16) == self.CORE_MARKER_MAGIC: + # Extract core position from marker: magic(16) | x(8) | y(8) + target_x = (val >> 8) & 0xFF + target_y = val & 0xFF + self.enemy_core_pos = Position(target_x, target_y) + self.drain_wave_active = True + self.drain_wave_rounds_left = self.DRAIN_WAVE_DURATION + print(f"[CORE] DRAIN WAVE ACTIVATED! Enemy Core at {self.enemy_core_pos}. " + f"Aggressive spawning for {self.DRAIN_WAVE_DURATION} rounds.", file=sys.stderr) + break + + if ct.get_action_cooldown() == 0: + # DRAIN WAVE: Aggressive spawning — one bot every round + # Floor tied to dynamic reserve so we can still build harvesters + if self.drain_wave_active: + bot_c_dw = ct.get_builder_bot_cost()[0] + if self.drain_wave_rounds_left > 0 and titanium >= self.reserve_buffer + bot_c_dw: + print(f"[CORE] DRAIN WAVE SPAWN " + f"({self.drain_wave_rounds_left} left, Ti={titanium})", + file=sys.stderr) + self._spawn(ct) + self.drain_wave_rounds_left -= 1 + elif self.drain_wave_rounds_left <= 0: + print("[CORE] DRAIN WAVE ENDED.", file=sys.stderr) + self.drain_wave_active = False + return + + # 1. Opening Sequence (Max 4 bots initially) + if rnd < 60: + if self.spawned < 4 and titanium >= 20: + self._spawn(ct) + return + + # 2. Flow Reinvestment (Profit + Dynamic Margin Staircasing) + # Tether cost to live game stats: + bot_c = ct.get_builder_bot_cost()[0] + harv_c = ct.get_harvester_cost()[0] + conv_c = ct.get_conveyor_cost()[0] + current_deployment_cost = bot_c + harv_c + conv_c + + # Dynamic margin based on scale percent AND current wealth: + # Scale Penalty: -5% margin per +1.0 scale percent above 100.0%. + # Wealth Bonus: -10% margin per 500 Titanium in the bank. + # Clamps at -150% (-1.50) to maintain aggressive spending. + current_scale = getattr(ct, 'get_scale_percent', lambda: 100.0)() + scale_penalty = (current_scale - 100.0) * 0.05 + wealth_bonus = (titanium / 500) * 0.10 + + margin_pct = 0.10 - scale_penalty - wealth_bonus + margin_pct = max(-1.50, min(0.20, margin_pct)) + + target_budget = current_deployment_cost * (1.0 + margin_pct) + + # 2.5 Wealthy Overdrive + # If we are overflowing with cash (3x the discovered expense buffer), + # we just dump it into bots regardless of the profit debt ledger. + wealth_overdrive = titanium > self.reserve_buffer * 3 + + # Require titanium to be >= the discovered reserve buffer (plus the bot cost we are about to spend). + # This ensures we NEVER accidentally drain the bank and starve bots from purchasing harvesters! + if (self.reinvestment_budget >= target_budget or wealth_overdrive) and titanium >= self.reserve_buffer + bot_c: + print(f"[Core] Flow Spawn{' (OVERDRIVE)' if wealth_overdrive else ''}. " + f"Budget: {self.reinvestment_budget:.1f}/{target_budget:.1f}, " + f"Margin: {margin_pct*100:.1f}%, Scale: {current_scale:.1f}%", file=sys.stderr) + self._spawn(ct) + # We ALWAYS deduct the full deployment cost from our internal profit tracking. + # If target_budget is negative, we go heavily into debt, ensuring the + # "zig-zag" burst spending eventually stops to let profit catch back up! + self.reinvestment_budget -= current_deployment_cost + + def _spawn(self, ct: Controller): + # 1. Determine base preferred direction index + if self.enemy_core_pos is not None: + # Prioritize spawning TOWARD the enemy core + pref_d = ct.get_position().direction_to(self.enemy_core_pos) + # Find index in DIRECTIONS (CENTRE is at index 8) + try: + preferred_d_idx = DIRECTIONS.index(pref_d) + except ValueError: + preferred_d_idx = 0 + else: + pair_idx = (self.spawned // 2) % 4 + sub_idx = self.spawned % 2 + preferred_d_idx = self.pairs[pair_idx][sub_idx] + + # 2. Try preferred direction first, then scan all 9 (including CENTRE) + for offset in range(9): + d_idx = (preferred_d_idx + offset) % 9 + d = DIRECTIONS[d_idx] + spawn_pos = ct.get_position().add(d) + + if ct.can_spawn(spawn_pos): + ct.spawn_builder(spawn_pos) + self.spawned += 1 + return diff --git a/bots/k_learning/main.py b/bots/k_learning/main.py new file mode 100644 index 0000000..8684183 --- /dev/null +++ b/bots/k_learning/main.py @@ -0,0 +1,31 @@ +"""Starter bot - a simple example to demonstrate usage of the Controller API. + +Each unit gets its own Player instance; the engine calls run() once per round. +Use Controller.get_entity_type() to branch on what kind of unit you are. + +This bot: + - Core: spawns up to 3 builder bots on random adjacent tiles, places a marker + - Builder bot: builds a harvester on any adjacent ore tile, then moves in a + random direction (laying a road first so the tile is passable) +""" + + +from cambc import Controller, EntityType + +from builderbot import BuilderBot +from core import Core + +mapping = { + EntityType.CORE: Core, + EntityType.BUILDER_BOT: BuilderBot, +} + +class Player: + def __init__(self): + self.active:Core | BuilderBot = None + + def run(self, ct: Controller) -> None: + etype = ct.get_entity_type() + if self.active is None: + self.active = mapping[etype]() + self.active.run(ct) \ No newline at end of file diff --git a/bots/k_learning/turret/__init__.py b/bots/k_learning/turret/__init__.py new file mode 100644 index 0000000..e69de29 From ab0622cfc05ce46b000b02dd7e6ee22948b6e7fb Mon Sep 17 00:00:00 2001 From: Cofee Coder <41608260+CoffeeCoder1015@users.noreply.github.com> Date: Mon, 23 Mar 2026 11:44:22 -0700 Subject: [PATCH 02/31] base gradient + development movement --- bots/k_learning/builderbot/__init__.py | 86 ++++++++++++++++++++++-- bots/k_learning/builderbot/navigation.py | 0 2 files changed, 82 insertions(+), 4 deletions(-) create mode 100644 bots/k_learning/builderbot/navigation.py diff --git a/bots/k_learning/builderbot/__init__.py b/bots/k_learning/builderbot/__init__.py index 034ce2f..2e5107b 100644 --- a/bots/k_learning/builderbot/__init__.py +++ b/bots/k_learning/builderbot/__init__.py @@ -1,9 +1,87 @@ -from cambc import Controller +import random +from typing import Counter +from cambc import Controller, Direction, EntityType, Environment, Position, Team +DIR_VECTORS: dict[Direction, tuple[int, int]] = { + Direction.NORTH: (0, -1), + Direction.NORTHEAST: (1, -1), + Direction.EAST: (1, 0), + Direction.SOUTHEAST: (1, 1), + Direction.SOUTH: (0, 1), + Direction.SOUTHWEST: (-1, 1), + Direction.WEST: (-1, 0), + Direction.NORTHWEST: (-1, -1), +} + +DIRECTIONS = [d for d in Direction if d != Direction.CENTRE] class BuilderBot: def __init__(self): - pass - + self.debug = True + self.ally_core_pos = None + self.round_number = 0 + self.dimensions = None + + # Movement + self.potential_direction = None + self.last_perturb_round = -999 + + def in_bounds(self,pos: Position): + return 0 <= pos.x <= self.dimensions[0] and 0 <= pos.y <= self.dimensions[1] + def run(self, ct: Controller): - pass \ No newline at end of file + # const updates + self.round_number = ct.get_current_round() + if self.ally_core_pos is None: + self.ally_core_pos = ct.get_position(1 if ct.get_team() == Team.A else 2) + + if self.debug: + ct.draw_indicator_dot(self.ally_core_pos, 255, 0, 0) + if self.dimensions is None: + self.dimensions = ( ct.get_map_width(),ct.get_map_height ()) + + # Movement + current_pos = ct.get_position() + if self.potential_direction is None: + self.potential_direction = self.ally_core_pos.direction_to(current_pos) + + + + move_direction = self.potential_direction + move_dest = current_pos.add(self.potential_direction) + + # Spreading out / Potential logic + # Perturbation spreading + nearby_tiles = ct.get_nearby_tiles() + nearby_buildings = ct.get_nearby_buildings() + development_percentage = len(nearby_buildings) / max(1, len(nearby_tiles)) + + # Gemini: + if development_percentage >= 0.3: + k = max(1, int(15 - 14 * (development_percentage - 0.3) / 0.7)) + if self.round_number - self.last_perturb_round >= k: + self.last_perturb_round = self.round_number + current_dir_idx = DIRECTIONS.index(move_direction) + raw_preturb_directions = [ DIRECTIONS[(current_dir_idx + offset) % 8] for offset in (1, -1, 2, -2)] + preturb_directions = filter(lambda x: ct.can_move(x) or ct.can_build_road(current_pos.add(x)), raw_preturb_directions) + move_direction = random.choice(list( preturb_directions )) + move_dest = current_pos.add(move_direction) + + # Build road if cannot move onto tile + while not ct.can_move(move_direction): + if ct.can_build_road(move_dest): + ct.build_road(move_dest) + else: + move_direction,move_dest = self.do_a_bounce(ct, current_pos, move_direction) + self.potential_direction = move_direction + + ct.move(move_direction) + + def do_a_bounce(self, ct, current_pos, move_direction): + # bounce by picking direction that is to the sides or oppisite diagonals diagonals to current direction + current_dir_idx = DIRECTIONS.index(move_direction) + raw_bounce_directions = [ DIRECTIONS[(current_dir_idx + offset) % 8] for offset in (2, -2, 3, -3)] + bounce_directions = filter(lambda x: ct.can_move(x) or ct.can_build_road(current_pos.add(x)), raw_bounce_directions) + move_direction = random.choice(list(bounce_directions)) + move_dest = current_pos.add(move_direction) + return move_direction,move_dest diff --git a/bots/k_learning/builderbot/navigation.py b/bots/k_learning/builderbot/navigation.py new file mode 100644 index 0000000..e69de29 From 2314c44cbacef57e3713360ee70a63c6c1dd2fc3 Mon Sep 17 00:00:00 2001 From: Cofee Coder <41608260+CoffeeCoder1015@users.noreply.github.com> Date: Mon, 23 Mar 2026 14:11:57 -0700 Subject: [PATCH 03/31] finding symmetry --- bots/k_learning/builderbot/__init__.py | 97 +++++++++++++++++++++++++- 1 file changed, 95 insertions(+), 2 deletions(-) diff --git a/bots/k_learning/builderbot/__init__.py b/bots/k_learning/builderbot/__init__.py index 2e5107b..832ebac 100644 --- a/bots/k_learning/builderbot/__init__.py +++ b/bots/k_learning/builderbot/__init__.py @@ -1,6 +1,5 @@ import random -from typing import Counter -from cambc import Controller, Direction, EntityType, Environment, Position, Team +from cambc import Controller, Direction, Environment, Position, Team DIR_VECTORS: dict[Direction, tuple[int, int]] = { Direction.NORTH: (0, -1), @@ -25,6 +24,68 @@ def __init__(self): # Movement self.potential_direction = None self.last_perturb_round = -999 + + # Symmetry + self.map_lut = None + self.possible_symmetries = ["horizontal", "vertical", "rotational"] + + def update_symmetry(self, ct: Controller, nearby_tiles: list[Position], nearby_units: list[int]): + w, h = self.dimensions + cx, cy = self.ally_core_pos.x, self.ally_core_pos.y + + # 1. Deterministic Core-based Symmetry Confirmation + enemy_id = 2 if ct.get_team() == Team.A else 1 + if enemy_id in nearby_units: + enemy_pos = ct.get_position(enemy_id) + ex, ey = enemy_pos.x, enemy_pos.y + + confirmed_sym = None + if ex == w - 1 - cx and ey == cy: + confirmed_sym = "horizontal" + elif ex == cx and ey == h - 1 - cy: + confirmed_sym = "vertical" + elif ex == w - 1 - cx and ey == h - 1 - cy: + confirmed_sym = "rotational" + + if confirmed_sym and confirmed_sym in self.possible_symmetries: + self.possible_symmetries = [confirmed_sym] + return # Core found, symmetry is deterministic + + # 2. Environment-based POI Matching (if not yet deterministic) + if len(self.possible_symmetries) <= 1: + return + + for pos in nearby_tiles: + if self.map_lut[pos.y][pos.x] != 0: + continue + + env = ct.get_tile_env(pos) + if env == Environment.ORE_TITANIUM: + val = 2 + elif env == Environment.WALL: + val = 3 + elif env == Environment.ORE_AXIONITE: + val = 4 + else: + val = 1 + + if val > 1: + for sym in self.possible_symmetries: + sym_x, sym_y = pos.x, pos.y + if sym == "horizontal": + sym_x = w - 1 - pos.x + elif sym == "vertical": + sym_y = h - 1 - pos.y + elif sym == "rotational": + sym_x = w - 1 - pos.x + sym_y = h - 1 - pos.y + + if 0 <= sym_x < w and 0 <= sym_y < h: + sym_val = self.map_lut[sym_y][sym_x] + if sym_val != 0 and sym_val != val: + self.possible_symmetries.remove(sym) + + self.map_lut[pos.y][pos.x] = val def in_bounds(self,pos: Position): return 0 <= pos.x <= self.dimensions[0] and 0 <= pos.y <= self.dimensions[1] @@ -32,6 +93,7 @@ def in_bounds(self,pos: Position): def run(self, ct: Controller): # const updates self.round_number = ct.get_current_round() + if self.ally_core_pos is None: self.ally_core_pos = ct.get_position(1 if ct.get_team() == Team.A else 2) @@ -39,6 +101,20 @@ def run(self, ct: Controller): ct.draw_indicator_dot(self.ally_core_pos, 255, 0, 0) if self.dimensions is None: self.dimensions = ( ct.get_map_width(),ct.get_map_height ()) + self.map_lut = [ [0] * self.dimensions[0] for _ in range(self.dimensions[1]) ] + + # Early elimination based on core position overlap + w, h = self.dimensions + cx, cy = self.ally_core_pos.x, self.ally_core_pos.y + overlap_x = abs(2 * cx - (w - 1)) <= 2 + overlap_y = abs(2 * cy - (h - 1)) <= 2 + + if overlap_x and "horizontal" in self.possible_symmetries: + self.possible_symmetries.remove("horizontal") + if overlap_y and "vertical" in self.possible_symmetries: + self.possible_symmetries.remove("vertical") + if overlap_x and overlap_y and "rotational" in self.possible_symmetries: + self.possible_symmetries.remove("rotational") # Movement current_pos = ct.get_position() @@ -53,6 +129,23 @@ def run(self, ct: Controller): # Spreading out / Potential logic # Perturbation spreading nearby_tiles = ct.get_nearby_tiles() + nearby_units = ct.get_nearby_units() + + # get nearby environment and insert obstacles and ores into LUT + self.update_symmetry(ct, nearby_tiles, nearby_units) + + # draw possible enemy core positions + if self.debug: + w, h = self.dimensions + cx, cy = self.ally_core_pos.x, self.ally_core_pos.y + for sym in self.possible_symmetries: + if sym == "horizontal": + ct.draw_indicator_line(current_pos, Position(w - 1 - cx, cy), 255, 165, 0) + elif sym == "vertical": + ct.draw_indicator_line(current_pos, Position(cx, h - 1 - cy), 255, 165, 0) + elif sym == "rotational": + ct.draw_indicator_line(current_pos, Position(w - 1 - cx, h - 1 - cy), 255, 165, 0) + nearby_buildings = ct.get_nearby_buildings() development_percentage = len(nearby_buildings) / max(1, len(nearby_tiles)) From d80119b299d9394cb122280fa10b48c980a1d478 Mon Sep 17 00:00:00 2001 From: Cofee Coder <41608260+CoffeeCoder1015@users.noreply.github.com> Date: Mon, 23 Mar 2026 14:13:41 -0700 Subject: [PATCH 04/31] debug: must set line current pos to be after the move of the round --- bots/k_learning/builderbot/__init__.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/bots/k_learning/builderbot/__init__.py b/bots/k_learning/builderbot/__init__.py index 832ebac..f7980f9 100644 --- a/bots/k_learning/builderbot/__init__.py +++ b/bots/k_learning/builderbot/__init__.py @@ -134,18 +134,6 @@ def run(self, ct: Controller): # get nearby environment and insert obstacles and ores into LUT self.update_symmetry(ct, nearby_tiles, nearby_units) - # draw possible enemy core positions - if self.debug: - w, h = self.dimensions - cx, cy = self.ally_core_pos.x, self.ally_core_pos.y - for sym in self.possible_symmetries: - if sym == "horizontal": - ct.draw_indicator_line(current_pos, Position(w - 1 - cx, cy), 255, 165, 0) - elif sym == "vertical": - ct.draw_indicator_line(current_pos, Position(cx, h - 1 - cy), 255, 165, 0) - elif sym == "rotational": - ct.draw_indicator_line(current_pos, Position(w - 1 - cx, h - 1 - cy), 255, 165, 0) - nearby_buildings = ct.get_nearby_buildings() development_percentage = len(nearby_buildings) / max(1, len(nearby_tiles)) @@ -169,6 +157,19 @@ def run(self, ct: Controller): self.potential_direction = move_direction ct.move(move_direction) + + # draw possible enemy core positions + if self.debug: + w, h = self.dimensions + cx, cy = self.ally_core_pos.x, self.ally_core_pos.y + for sym in self.possible_symmetries: + if sym == "horizontal": + ct.draw_indicator_line(current_pos.add(move_direction), Position(w - 1 - cx, cy), 255, 165, 0) + elif sym == "vertical": + ct.draw_indicator_line(current_pos.add(move_direction), Position(cx, h - 1 - cy), 255, 165, 0) + elif sym == "rotational": + ct.draw_indicator_line(current_pos.add(move_direction), Position(w - 1 - cx, h - 1 - cy), 255, 165, 0) + def do_a_bounce(self, ct, current_pos, move_direction): # bounce by picking direction that is to the sides or oppisite diagonals diagonals to current direction From 4a53ec2859526853db9db436f9deeb52a51dec7a Mon Sep 17 00:00:00 2001 From: Cofee Coder <41608260+CoffeeCoder1015@users.noreply.github.com> Date: Mon, 23 Mar 2026 18:05:16 -0700 Subject: [PATCH 05/31] mod: moving symmetry into its own file --- bots/k_learning/builderbot/__init__.py | 92 ++------------------- bots/k_learning/builderbot/navigation.py | 100 +++++++++++++++++++++++ 2 files changed, 107 insertions(+), 85 deletions(-) diff --git a/bots/k_learning/builderbot/__init__.py b/bots/k_learning/builderbot/__init__.py index f7980f9..b57676b 100644 --- a/bots/k_learning/builderbot/__init__.py +++ b/bots/k_learning/builderbot/__init__.py @@ -1,5 +1,6 @@ import random -from cambc import Controller, Direction, Environment, Position, Team +from cambc import Controller, Direction, Position, Team +from .navigation import SymmetryAnalyzer DIR_VECTORS: dict[Direction, tuple[int, int]] = { Direction.NORTH: (0, -1), @@ -26,66 +27,8 @@ def __init__(self): self.last_perturb_round = -999 # Symmetry - self.map_lut = None - self.possible_symmetries = ["horizontal", "vertical", "rotational"] + self.symmetry_analyzer = None - def update_symmetry(self, ct: Controller, nearby_tiles: list[Position], nearby_units: list[int]): - w, h = self.dimensions - cx, cy = self.ally_core_pos.x, self.ally_core_pos.y - - # 1. Deterministic Core-based Symmetry Confirmation - enemy_id = 2 if ct.get_team() == Team.A else 1 - if enemy_id in nearby_units: - enemy_pos = ct.get_position(enemy_id) - ex, ey = enemy_pos.x, enemy_pos.y - - confirmed_sym = None - if ex == w - 1 - cx and ey == cy: - confirmed_sym = "horizontal" - elif ex == cx and ey == h - 1 - cy: - confirmed_sym = "vertical" - elif ex == w - 1 - cx and ey == h - 1 - cy: - confirmed_sym = "rotational" - - if confirmed_sym and confirmed_sym in self.possible_symmetries: - self.possible_symmetries = [confirmed_sym] - return # Core found, symmetry is deterministic - - # 2. Environment-based POI Matching (if not yet deterministic) - if len(self.possible_symmetries) <= 1: - return - - for pos in nearby_tiles: - if self.map_lut[pos.y][pos.x] != 0: - continue - - env = ct.get_tile_env(pos) - if env == Environment.ORE_TITANIUM: - val = 2 - elif env == Environment.WALL: - val = 3 - elif env == Environment.ORE_AXIONITE: - val = 4 - else: - val = 1 - - if val > 1: - for sym in self.possible_symmetries: - sym_x, sym_y = pos.x, pos.y - if sym == "horizontal": - sym_x = w - 1 - pos.x - elif sym == "vertical": - sym_y = h - 1 - pos.y - elif sym == "rotational": - sym_x = w - 1 - pos.x - sym_y = h - 1 - pos.y - - if 0 <= sym_x < w and 0 <= sym_y < h: - sym_val = self.map_lut[sym_y][sym_x] - if sym_val != 0 and sym_val != val: - self.possible_symmetries.remove(sym) - - self.map_lut[pos.y][pos.x] = val def in_bounds(self,pos: Position): return 0 <= pos.x <= self.dimensions[0] and 0 <= pos.y <= self.dimensions[1] @@ -101,20 +44,7 @@ def run(self, ct: Controller): ct.draw_indicator_dot(self.ally_core_pos, 255, 0, 0) if self.dimensions is None: self.dimensions = ( ct.get_map_width(),ct.get_map_height ()) - self.map_lut = [ [0] * self.dimensions[0] for _ in range(self.dimensions[1]) ] - - # Early elimination based on core position overlap - w, h = self.dimensions - cx, cy = self.ally_core_pos.x, self.ally_core_pos.y - overlap_x = abs(2 * cx - (w - 1)) <= 2 - overlap_y = abs(2 * cy - (h - 1)) <= 2 - - if overlap_x and "horizontal" in self.possible_symmetries: - self.possible_symmetries.remove("horizontal") - if overlap_y and "vertical" in self.possible_symmetries: - self.possible_symmetries.remove("vertical") - if overlap_x and overlap_y and "rotational" in self.possible_symmetries: - self.possible_symmetries.remove("rotational") + self.symmetry_analyzer = SymmetryAnalyzer(self.dimensions[0], self.dimensions[1], self.ally_core_pos) # Movement current_pos = ct.get_position() @@ -130,11 +60,11 @@ def run(self, ct: Controller): # Perturbation spreading nearby_tiles = ct.get_nearby_tiles() nearby_units = ct.get_nearby_units() + nearby_buildings = ct.get_nearby_buildings() # get nearby environment and insert obstacles and ores into LUT - self.update_symmetry(ct, nearby_tiles, nearby_units) + self.symmetry_analyzer.update_symmetry(ct, nearby_tiles, nearby_units) - nearby_buildings = ct.get_nearby_buildings() development_percentage = len(nearby_buildings) / max(1, len(nearby_tiles)) # Gemini: @@ -160,15 +90,7 @@ def run(self, ct: Controller): # draw possible enemy core positions if self.debug: - w, h = self.dimensions - cx, cy = self.ally_core_pos.x, self.ally_core_pos.y - for sym in self.possible_symmetries: - if sym == "horizontal": - ct.draw_indicator_line(current_pos.add(move_direction), Position(w - 1 - cx, cy), 255, 165, 0) - elif sym == "vertical": - ct.draw_indicator_line(current_pos.add(move_direction), Position(cx, h - 1 - cy), 255, 165, 0) - elif sym == "rotational": - ct.draw_indicator_line(current_pos.add(move_direction), Position(w - 1 - cx, h - 1 - cy), 255, 165, 0) + self.symmetry_analyzer.draw_debug(ct) def do_a_bounce(self, ct, current_pos, move_direction): diff --git a/bots/k_learning/builderbot/navigation.py b/bots/k_learning/builderbot/navigation.py index e69de29..9d45f0e 100644 --- a/bots/k_learning/builderbot/navigation.py +++ b/bots/k_learning/builderbot/navigation.py @@ -0,0 +1,100 @@ +from enum import Enum + +from cambc import Controller, Position, Team, Environment + +class LUT(Enum): + UNEXPLORED = 0 + EMPTY = 1 + ORE_TITANIUM = 2 + WALL = 3 + ORE_AXIONITE = 4 + +class SymmetryAnalyzer: + def __init__(self, w: int, h: int, ally_core_pos: Position): + self.w = w + self.h = h + self.ally_core_pos = ally_core_pos + self.map_lut = [[LUT.UNEXPLORED] * w for _ in range(h)] + self.possible_symmetries = ["horizontal", "vertical", "rotational"] + self.eliminate_core_overlap() + + def eliminate_core_overlap(self): + """Early elimination based on core position overlap.""" + cx, cy = self.ally_core_pos.x, self.ally_core_pos.y + overlap_x = abs(2 * cx - (self.w - 1)) <= 2 + overlap_y = abs(2 * cy - (self.h - 1)) <= 2 + + if overlap_x and "horizontal" in self.possible_symmetries: + self.possible_symmetries.remove("horizontal") + if overlap_y and "vertical" in self.possible_symmetries: + self.possible_symmetries.remove("vertical") + if overlap_x and overlap_y and "rotational" in self.possible_symmetries: + self.possible_symmetries.remove("rotational") + + def update_symmetry(self, ct: Controller, nearby_tiles: list[Position], nearby_units: list[int]): + cx, cy = self.ally_core_pos.x, self.ally_core_pos.y + + # 1. Deterministic Core-based Symmetry Confirmation + enemy_id = 2 if ct.get_team() == Team.A else 1 + if enemy_id in nearby_units: + enemy_pos = ct.get_position(enemy_id) + ex, ey = enemy_pos.x, enemy_pos.y + + confirmed_sym = None + if ex == self.w - 1 - cx and ey == cy: + confirmed_sym = "horizontal" + elif ex == cx and ey == self.h - 1 - cy: + confirmed_sym = "vertical" + elif ex == self.w - 1 - cx and ey == self.h - 1 - cy: + confirmed_sym = "rotational" + + if confirmed_sym and confirmed_sym in self.possible_symmetries: + self.possible_symmetries = [confirmed_sym] + return # Core found, symmetry is deterministic + + # 2. Environment-based POI Matching (if not yet deterministic) + if len(self.possible_symmetries) <= 1: + return + + for pos in nearby_tiles: + if self.map_lut[pos.y][pos.x] != LUT.UNEXPLORED: + continue + + env = ct.get_tile_env(pos) + if env == Environment.ORE_TITANIUM: + val = LUT.ORE_TITANIUM + elif env == Environment.WALL: + val = LUT.WALL + elif env == Environment.ORE_AXIONITE: + val = LUT.ORE_AXIONITE + else: + val = LUT.EMPTY + + if val.value > LUT.EMPTY.value: + for sym in self.possible_symmetries[:]: + sym_x, sym_y = pos.x, pos.y + if sym == "horizontal": + sym_x = self.w - 1 - pos.x + elif sym == "vertical": + sym_y = self.h - 1 - pos.y + elif sym == "rotational": + sym_x = self.w - 1 - pos.x + sym_y = self.h - 1 - pos.y + + if 0 <= sym_x < self.w and 0 <= sym_y < self.h: + sym_val = self.map_lut[sym_y][sym_x] + if sym_val != LUT.UNEXPLORED and sym_val != val: + self.possible_symmetries.remove(sym) + + self.map_lut[pos.y][pos.x] = val + + def draw_debug(self, ct: Controller): + current_pos = ct.get_position() + cx, cy = self.ally_core_pos.x, self.ally_core_pos.y + for sym in self.possible_symmetries: + if sym == "horizontal": + ct.draw_indicator_line(current_pos, Position(self.w - 1 - cx, cy), 255, 165, 0) + elif sym == "vertical": + ct.draw_indicator_line(current_pos, Position(cx, self.h - 1 - cy), 255, 165, 0) + elif sym == "rotational": + ct.draw_indicator_line(current_pos, Position(self.w - 1 - cx, self.h - 1 - cy), 255, 165, 0) \ No newline at end of file From bde76534242dab24b289301107cd6ede8999c004 Mon Sep 17 00:00:00 2001 From: Cofee Coder <41608260+CoffeeCoder1015@users.noreply.github.com> Date: Mon, 23 Mar 2026 18:59:15 -0700 Subject: [PATCH 06/31] Flattend LUT update in symmetry checker --- bots/k_learning/builderbot/navigation.py | 46 +++++++++++++----------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/bots/k_learning/builderbot/navigation.py b/bots/k_learning/builderbot/navigation.py index 9d45f0e..934f7cb 100644 --- a/bots/k_learning/builderbot/navigation.py +++ b/bots/k_learning/builderbot/navigation.py @@ -14,10 +14,13 @@ def __init__(self, w: int, h: int, ally_core_pos: Position): self.w = w self.h = h self.ally_core_pos = ally_core_pos - self.map_lut = [[LUT.UNEXPLORED] * w for _ in range(h)] + self.map_lut = [LUT.UNEXPLORED] * (w * h) self.possible_symmetries = ["horizontal", "vertical", "rotational"] self.eliminate_core_overlap() - + + def _get_idx(self, x: int, y: int) -> int: + return y * self.w + x + def eliminate_core_overlap(self): """Early elimination based on core position overlap.""" cx, cy = self.ally_core_pos.x, self.ally_core_pos.y @@ -57,7 +60,8 @@ def update_symmetry(self, ct: Controller, nearby_tiles: list[Position], nearby_u return for pos in nearby_tiles: - if self.map_lut[pos.y][pos.x] != LUT.UNEXPLORED: + idx = self._get_idx(pos.x, pos.y) + if self.map_lut[idx] != LUT.UNEXPLORED: continue env = ct.get_tile_env(pos) @@ -69,24 +73,24 @@ def update_symmetry(self, ct: Controller, nearby_tiles: list[Position], nearby_u val = LUT.ORE_AXIONITE else: val = LUT.EMPTY - - if val.value > LUT.EMPTY.value: - for sym in self.possible_symmetries[:]: - sym_x, sym_y = pos.x, pos.y - if sym == "horizontal": - sym_x = self.w - 1 - pos.x - elif sym == "vertical": - sym_y = self.h - 1 - pos.y - elif sym == "rotational": - sym_x = self.w - 1 - pos.x - sym_y = self.h - 1 - pos.y - - if 0 <= sym_x < self.w and 0 <= sym_y < self.h: - sym_val = self.map_lut[sym_y][sym_x] - if sym_val != LUT.UNEXPLORED and sym_val != val: - self.possible_symmetries.remove(sym) - - self.map_lut[pos.y][pos.x] = val + + for sym in self.possible_symmetries[:]: + sym_x, sym_y = pos.x, pos.y + if sym == "horizontal": + sym_x = self.w - 1 - pos.x + elif sym == "vertical": + sym_y = self.h - 1 - pos.y + elif sym == "rotational": + sym_x = self.w - 1 - pos.x + sym_y = self.h - 1 - pos.y + + if 0 <= sym_x < self.w and 0 <= sym_y < self.h: + sym_idx = self._get_idx(sym_x, sym_y) + sym_val = self.map_lut[sym_idx] + if sym_val != LUT.UNEXPLORED and sym_val != val: + self.possible_symmetries.remove(sym) + + self.map_lut[idx] = val def draw_debug(self, ct: Controller): current_pos = ct.get_position() From 39ef1aca46b05d1f82eaa3a427454585b104503d Mon Sep 17 00:00:00 2001 From: Cofee Coder <41608260+CoffeeCoder1015@users.noreply.github.com> Date: Fri, 27 Mar 2026 01:20:53 -0700 Subject: [PATCH 07/31] debug bot --- bots/do_nothing/main.py | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 bots/do_nothing/main.py diff --git a/bots/do_nothing/main.py b/bots/do_nothing/main.py new file mode 100644 index 0000000..36a9e58 --- /dev/null +++ b/bots/do_nothing/main.py @@ -0,0 +1,7 @@ +from cambc import Controller +class Player: + def __init__(self): + self.num_spawned = 0 # number of builder bots spawned so far (core) + + def run(self, ct: Controller) -> None: + return \ No newline at end of file From d66155dbedfb44bece0405d4eed30b6c4a6f9d95 Mon Sep 17 00:00:00 2001 From: Cofee Coder <41608260+CoffeeCoder1015@users.noreply.github.com> Date: Fri, 27 Mar 2026 13:50:49 -0700 Subject: [PATCH 08/31] tmp: single bot testing --- bots/k_learning/core/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bots/k_learning/core/__init__.py b/bots/k_learning/core/__init__.py index d197e58..8a6ac92 100644 --- a/bots/k_learning/core/__init__.py +++ b/bots/k_learning/core/__init__.py @@ -39,6 +39,8 @@ def run(self, ct: Controller): res = ct.get_global_resources() titanium = res[0] rnd = ct.get_current_round() + if rnd > 0: + return # 0.1 Adaptive Expense Tracking # If the swarm bottomed out the bank entirely (< 3 Ti for a conveyor), From b3641c924c0ba2cad4fd2b9e19231e280480b817 Mon Sep 17 00:00:00 2001 From: Cofee Coder <41608260+CoffeeCoder1015@users.noreply.github.com> Date: Fri, 27 Mar 2026 13:51:05 -0700 Subject: [PATCH 09/31] new quad based exploration --- bots/k_learning/builderbot/__init__.py | 48 ++----------- bots/k_learning/builderbot/navigation.py | 91 +++++++++++++++++++++++- 2 files changed, 95 insertions(+), 44 deletions(-) diff --git a/bots/k_learning/builderbot/__init__.py b/bots/k_learning/builderbot/__init__.py index b57676b..9055977 100644 --- a/bots/k_learning/builderbot/__init__.py +++ b/bots/k_learning/builderbot/__init__.py @@ -1,6 +1,6 @@ import random from cambc import Controller, Direction, Position, Team -from .navigation import SymmetryAnalyzer +from .navigation import Navigation, SymmetryAnalyzer DIR_VECTORS: dict[Direction, tuple[int, int]] = { Direction.NORTH: (0, -1), @@ -22,13 +22,11 @@ def __init__(self): self.round_number = 0 self.dimensions = None - # Movement - self.potential_direction = None - self.last_perturb_round = -999 - # Symmetry self.symmetry_analyzer = None + # Movement + self.nav = None def in_bounds(self,pos: Position): return 0 <= pos.x <= self.dimensions[0] and 0 <= pos.y <= self.dimensions[1] @@ -45,16 +43,11 @@ def run(self, ct: Controller): if self.dimensions is None: self.dimensions = ( ct.get_map_width(),ct.get_map_height ()) self.symmetry_analyzer = SymmetryAnalyzer(self.dimensions[0], self.dimensions[1], self.ally_core_pos) + self.nav = Navigation(self.dimensions[0],self.dimensions[1]) # Movement current_pos = ct.get_position() - if self.potential_direction is None: - self.potential_direction = self.ally_core_pos.direction_to(current_pos) - - - move_direction = self.potential_direction - move_dest = current_pos.add(self.potential_direction) # Spreading out / Potential logic # Perturbation spreading @@ -65,39 +58,10 @@ def run(self, ct: Controller): # get nearby environment and insert obstacles and ores into LUT self.symmetry_analyzer.update_symmetry(ct, nearby_tiles, nearby_units) - development_percentage = len(nearby_buildings) / max(1, len(nearby_tiles)) - - # Gemini: - if development_percentage >= 0.3: - k = max(1, int(15 - 14 * (development_percentage - 0.3) / 0.7)) - if self.round_number - self.last_perturb_round >= k: - self.last_perturb_round = self.round_number - current_dir_idx = DIRECTIONS.index(move_direction) - raw_preturb_directions = [ DIRECTIONS[(current_dir_idx + offset) % 8] for offset in (1, -1, 2, -2)] - preturb_directions = filter(lambda x: ct.can_move(x) or ct.can_build_road(current_pos.add(x)), raw_preturb_directions) - move_direction = random.choice(list( preturb_directions )) - move_dest = current_pos.add(move_direction) - - # Build road if cannot move onto tile - while not ct.can_move(move_direction): - if ct.can_build_road(move_dest): - ct.build_road(move_dest) - else: - move_direction,move_dest = self.do_a_bounce(ct, current_pos, move_direction) - self.potential_direction = move_direction + self.nav.update_info(ct,current_pos,nearby_tiles,self.symmetry_analyzer) + self.nav.explore(ct) - ct.move(move_direction) - # draw possible enemy core positions if self.debug: self.symmetry_analyzer.draw_debug(ct) - - def do_a_bounce(self, ct, current_pos, move_direction): - # bounce by picking direction that is to the sides or oppisite diagonals diagonals to current direction - current_dir_idx = DIRECTIONS.index(move_direction) - raw_bounce_directions = [ DIRECTIONS[(current_dir_idx + offset) % 8] for offset in (2, -2, 3, -3)] - bounce_directions = filter(lambda x: ct.can_move(x) or ct.can_build_road(current_pos.add(x)), raw_bounce_directions) - move_direction = random.choice(list(bounce_directions)) - move_dest = current_pos.add(move_direction) - return move_direction,move_dest diff --git a/bots/k_learning/builderbot/navigation.py b/bots/k_learning/builderbot/navigation.py index 934f7cb..bcc3383 100644 --- a/bots/k_learning/builderbot/navigation.py +++ b/bots/k_learning/builderbot/navigation.py @@ -1,6 +1,9 @@ from enum import Enum +import heapq +from math import e +from sys import stderr -from cambc import Controller, Position, Team, Environment +from cambc import Controller, Direction, EntityType, Position, Team, Environment class LUT(Enum): UNEXPLORED = 0 @@ -101,4 +104,88 @@ def draw_debug(self, ct: Controller): elif sym == "vertical": ct.draw_indicator_line(current_pos, Position(cx, self.h - 1 - cy), 255, 165, 0) elif sym == "rotational": - ct.draw_indicator_line(current_pos, Position(self.w - 1 - cx, self.h - 1 - cy), 255, 165, 0) \ No newline at end of file + ct.draw_indicator_line(current_pos, Position(self.w - 1 - cx, self.h - 1 - cy), 255, 165, 0) + + +CARDINALS = [ + Direction.NORTH, + Direction.SOUTH, + Direction.EAST, + Direction.WEST +] +DIRECTIONS = [d for d in Direction if d != Direction.CENTRE] + +class Navigation: + def __init__(self,w,h): + self.w = w + self.h = h + self.map_lut = [0] * (w * h) + self.quad_scaling = 5 + self.current_pos: Position | None = None + self.sym = None + self.pq = [] + + def _get_idx(self, x: int, y: int) -> int: + return y * self.w + x + + def in_bounds(self,x:int,y:int): + return 0 <= x < self.w and 0 <= y < self.h + + def update_info(self,ct:Controller,current_pos:Position,nearby_tiles:list[Position],sym:SymmetryAnalyzer): + self.current_pos = current_pos + self.sym = sym + + # Update LUT + for pos in nearby_tiles: + idx = self._get_idx(*pos) + self.map_lut[idx] = 1 + + def get_lut(self,x,y): + return self.map_lut[self._get_idx(x,y)] + + def move(self,ct:Controller,target_pos:Position): + m_dir = self.current_pos.direction_to(target_pos) + for _ in range(8): + next_pos = self.current_pos.add(m_dir) + if ct.can_move(m_dir) and self.get_lut(*next_pos) == 0: + ct.move(m_dir) + return + elif ct.can_build_road(next_pos): + ct.build_road(next_pos) + ct.move(m_dir) + return + else: + m_dir = m_dir.rotate_left() + + def get_neighbors(self): + quads = [ + (self.current_pos.x, self.current_pos.y - 5), + (self.current_pos.x + 5, self.current_pos.y - 5), + (self.current_pos.x + 5, self.current_pos.y), + (self.current_pos.x + 5, self.current_pos.y + 5), + (self.current_pos.x, self.current_pos.y + 5), + (self.current_pos.x - 5, self.current_pos.y + 5), + (self.current_pos.x - 5, self.current_pos.y), + (self.current_pos.x - 5, self.current_pos.y - 5), + ] + neighbors = [] + for q in quads: + if self.in_bounds(*q) and self.get_lut(*q) == 0: + neighbors.append((0,q)) + return neighbors + + def explore(self,ct:Controller): + for _,p in self.pq: + ct.draw_indicator_dot(Position(*p),255,0,0) + + if not self.pq: + self.pq = self.get_neighbors() + + top = self.pq[0][1] + if top == self.current_pos: + self.pq.pop(0) + self.pq.extend(self.get_neighbors()) + else: + self.move(ct,Position(*top)) + + \ No newline at end of file From 3566c030bd83a041ffec87dfb641b9d0bc3c1273 Mon Sep 17 00:00:00 2001 From: Cofee Coder <41608260+CoffeeCoder1015@users.noreply.github.com> Date: Fri, 27 Mar 2026 14:19:22 -0700 Subject: [PATCH 10/31] goofy movement algorithm --- bots/k_learning/builderbot/navigation.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/bots/k_learning/builderbot/navigation.py b/bots/k_learning/builderbot/navigation.py index bcc3383..ed8301c 100644 --- a/bots/k_learning/builderbot/navigation.py +++ b/bots/k_learning/builderbot/navigation.py @@ -149,13 +149,25 @@ def move(self,ct:Controller,target_pos:Position): next_pos = self.current_pos.add(m_dir) if ct.can_move(m_dir) and self.get_lut(*next_pos) == 0: ct.move(m_dir) - return + return True elif ct.can_build_road(next_pos): ct.build_road(next_pos) ct.move(m_dir) - return + return True else: m_dir = m_dir.rotate_left() + for _ in range(8): + next_pos = self.current_pos.add(m_dir) + if ct.can_move(m_dir): + ct.move(m_dir) + return True + elif ct.can_build_road(next_pos): + ct.build_road(next_pos) + ct.move(m_dir) + return True + else: + m_dir = m_dir.rotate_right() + return False def get_neighbors(self): quads = [ From 8f1f0e1b8ba80af0fba232e507bc6e65bba3b6a4 Mon Sep 17 00:00:00 2001 From: Cofee Coder <41608260+CoffeeCoder1015@users.noreply.github.com> Date: Fri, 27 Mar 2026 14:20:04 -0700 Subject: [PATCH 11/31] closest quadrant recalculation --- bots/k_learning/builderbot/navigation.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/bots/k_learning/builderbot/navigation.py b/bots/k_learning/builderbot/navigation.py index ed8301c..0cdf0a4 100644 --- a/bots/k_learning/builderbot/navigation.py +++ b/bots/k_learning/builderbot/navigation.py @@ -192,12 +192,20 @@ def explore(self,ct:Controller): if not self.pq: self.pq = self.get_neighbors() + top = self.pq[0][1] - if top == self.current_pos: + if top == self.current_pos or top == ct.get_position(): self.pq.pop(0) self.pq.extend(self.get_neighbors()) + # Update distances + new_pq = [] + for i in range(len(self.pq)): + _,pos = self.pq[i] + new_val = self.current_pos.distance_squared(Position(*pos)) + heapq.heappush(new_pq,(new_val,pos)) + self.pq = new_pq + elif self.sym.map_lut[self._get_idx(*top)] == LUT.WALL: + self.pq.pop(0) else: - self.move(ct,Position(*top)) - - \ No newline at end of file + self.move(ct,Position(*top)) \ No newline at end of file From 511f3124fec6efc446dd4ea6b4fda1e888d6e833 Mon Sep 17 00:00:00 2001 From: Cofee Coder <41608260+CoffeeCoder1015@users.noreply.github.com> Date: Fri, 27 Mar 2026 19:06:28 -0700 Subject: [PATCH 12/31] working map explorer --- bots/k_learning/builderbot/navigation.py | 108 +++++++++++++++++------ 1 file changed, 83 insertions(+), 25 deletions(-) diff --git a/bots/k_learning/builderbot/navigation.py b/bots/k_learning/builderbot/navigation.py index 0cdf0a4..ef9e18e 100644 --- a/bots/k_learning/builderbot/navigation.py +++ b/bots/k_learning/builderbot/navigation.py @@ -124,6 +124,12 @@ def __init__(self,w,h): self.current_pos: Position | None = None self.sym = None self.pq = [] + self.visited = set() + self.hugging_wall = 0 + self.start_pos = None + self.hit_dist = None + self.hit_pos = None + self.last_dir = None def _get_idx(self, x: int, y: int) -> int: return y * self.w + x @@ -139,37 +145,83 @@ def update_info(self,ct:Controller,current_pos:Position,nearby_tiles:list[Positi for pos in nearby_tiles: idx = self._get_idx(*pos) self.map_lut[idx] = 1 + self.map_lut[self._get_idx(current_pos.x,current_pos.y)] = 2 def get_lut(self,x,y): return self.map_lut[self._get_idx(x,y)] def move(self,ct:Controller,target_pos:Position): + self.current_pos = ct.get_position() m_dir = self.current_pos.direction_to(target_pos) - for _ in range(8): - next_pos = self.current_pos.add(m_dir) - if ct.can_move(m_dir) and self.get_lut(*next_pos) == 0: + next_pos = self.current_pos.add(m_dir) + if self.hugging_wall == 0: + self.start_pos = self.current_pos + if ct.can_move(m_dir): ct.move(m_dir) return True elif ct.can_build_road(next_pos): ct.build_road(next_pos) ct.move(m_dir) return True - else: - m_dir = m_dir.rotate_left() - for _ in range(8): - next_pos = self.current_pos.add(m_dir) + + self.hugging_wall = 1 + self.hit_dist = self.current_pos.distance_squared(target_pos) + self.hit_pos = self.current_pos + self.last_dir = m_dir + + + dx = target_pos.x - self.start_pos.x + dy = target_pos.y - self.start_pos.y + cx = self.current_pos.x - self.start_pos.x + cy = self.current_pos.y - self.start_pos.y + + on_mline = None + # Cross product should be ~0 for collinear points + cross = dx * cy - dy * cx + if abs(cross) > 1: # tolerance of 1 for grid cells + on_mline = False + + # Dot product to check current is between start and target + dot = cx * dx + cy * dy + len_sq = dx * dx + dy * dy + on_mline = 0 <= dot <= len_sq + + + current_dist = self.current_pos.distance_squared(target_pos) + if ( self.current_pos != self.hit_pos and current_dist < self.hit_dist and on_mline): + print("EXITING") if ct.can_move(m_dir): ct.move(m_dir) + self.hugging_wall = 0 return True elif ct.can_build_road(next_pos): ct.build_road(next_pos) ct.move(m_dir) + self.hugging_wall = 0 return True - else: - m_dir = m_dir.rotate_right() - return False + + for _ in range(9): + rs = self.last_dir.rotate_right() + right_pos = self.current_pos.add(rs) + right_env = ct.get_tile_env(right_pos) + right_build_id = ct.get_tile_building_id(right_pos) + right_build = ct.get_entity_type(right_build_id) if right_build_id else None + hugging_enemy_core = right_build==EntityType.CORE and ct.get_team(right_build_id) != ct.get_team() + next_pos = self.current_pos.add(self.last_dir) + if right_env == Environment.WALL or hugging_enemy_core : + if ct.can_move(self.last_dir): + ct.move(self.last_dir) + return True + elif ct.can_build_road(next_pos): + ct.build_road(next_pos) + ct.move(self.last_dir) + return True + self.last_dir = self.last_dir.rotate_left() + - def get_neighbors(self): + raise Exception("Failed to move") + + def get_neighbors(self,ct:Controller): quads = [ (self.current_pos.x, self.current_pos.y - 5), (self.current_pos.x + 5, self.current_pos.y - 5), @@ -182,8 +234,9 @@ def get_neighbors(self): ] neighbors = [] for q in quads: - if self.in_bounds(*q) and self.get_lut(*q) == 0: + if self.in_bounds(*q) and q not in self.visited: neighbors.append((0,q)) + self.visited.add(q) return neighbors def explore(self,ct:Controller): @@ -191,21 +244,26 @@ def explore(self,ct:Controller): ct.draw_indicator_dot(Position(*p),255,0,0) if not self.pq: - self.pq = self.get_neighbors() + self.pq = self.get_neighbors(ct) top = self.pq[0][1] - if top == self.current_pos or top == ct.get_position(): - self.pq.pop(0) - self.pq.extend(self.get_neighbors()) - # Update distances - new_pq = [] - for i in range(len(self.pq)): - _,pos = self.pq[i] - new_val = self.current_pos.distance_squared(Position(*pos)) - heapq.heappush(new_pq,(new_val,pos)) - self.pq = new_pq - elif self.sym.map_lut[self._get_idx(*top)] == LUT.WALL: + if (self.current_pos.x,self.current_pos.y) == top: self.pq.pop(0) + self.pq.extend(self.get_neighbors(ct)) else: - self.move(ct,Position(*top)) \ No newline at end of file + tpos = Position(*top) + dist = self.current_pos.distance_squared(tpos) + if dist <= ct.get_vision_radius_sq() and ct.get_tile_env(tpos) == Environment.WALL: + self.pq.pop(0) + + # Update distances + new_pq = [] + for i in range(len(self.pq)): + _,pos = self.pq[i] + new_val = self.current_pos.distance_squared(Position(*pos)) + heapq.heappush(new_pq,(new_val,pos)) + self.pq = new_pq + final_target = Position(*self.pq[0][1]) + ct.draw_indicator_line(self.current_pos,final_target,0,255,0) + self.move(ct,final_target) \ No newline at end of file From b4b20f89de22bb636f33eb8b72b18de58f0c7404 Mon Sep 17 00:00:00 2001 From: Cofee Coder <41608260+CoffeeCoder1015@users.noreply.github.com> Date: Sat, 28 Mar 2026 06:12:14 -0700 Subject: [PATCH 13/31] a* explorer --- bots/k_learning/builderbot/__init__.py | 2 +- bots/k_learning/builderbot/navigation.py | 168 +++++++++++------------ 2 files changed, 84 insertions(+), 86 deletions(-) diff --git a/bots/k_learning/builderbot/__init__.py b/bots/k_learning/builderbot/__init__.py index 9055977..63fc35f 100644 --- a/bots/k_learning/builderbot/__init__.py +++ b/bots/k_learning/builderbot/__init__.py @@ -58,7 +58,7 @@ def run(self, ct: Controller): # get nearby environment and insert obstacles and ores into LUT self.symmetry_analyzer.update_symmetry(ct, nearby_tiles, nearby_units) - self.nav.update_info(ct,current_pos,nearby_tiles,self.symmetry_analyzer) + self.nav.update_info(ct,current_pos,nearby_tiles,nearby_buildings) self.nav.explore(ct) # draw possible enemy core positions diff --git a/bots/k_learning/builderbot/navigation.py b/bots/k_learning/builderbot/navigation.py index ef9e18e..26d6644 100644 --- a/bots/k_learning/builderbot/navigation.py +++ b/bots/k_learning/builderbot/navigation.py @@ -1,6 +1,5 @@ -from enum import Enum +from enum import Enum, EnumType import heapq -from math import e from sys import stderr from cambc import Controller, Direction, EntityType, Position, Team, Environment @@ -119,17 +118,14 @@ class Navigation: def __init__(self,w,h): self.w = w self.h = h - self.map_lut = [0] * (w * h) + self.map_lut = [[0]*h for _ in range(w)] self.quad_scaling = 5 self.current_pos: Position | None = None self.sym = None self.pq = [] self.visited = set() - self.hugging_wall = 0 - self.start_pos = None - self.hit_dist = None - self.hit_pos = None - self.last_dir = None + + self.path_cahce= [] def _get_idx(self, x: int, y: int) -> int: return y * self.w + x @@ -137,89 +133,88 @@ def _get_idx(self, x: int, y: int) -> int: def in_bounds(self,x:int,y:int): return 0 <= x < self.w and 0 <= y < self.h - def update_info(self,ct:Controller,current_pos:Position,nearby_tiles:list[Position],sym:SymmetryAnalyzer): + def update_info(self,ct:Controller,current_pos:Position,nearby_tiles:list[Position],nearby_buildings:list[int]): self.current_pos = current_pos - self.sym = sym - # Update LUT for pos in nearby_tiles: - idx = self._get_idx(*pos) - self.map_lut[idx] = 1 - self.map_lut[self._get_idx(current_pos.x,current_pos.y)] = 2 - - def get_lut(self,x,y): - return self.map_lut[self._get_idx(x,y)] - - def move(self,ct:Controller,target_pos:Position): - self.current_pos = ct.get_position() - m_dir = self.current_pos.direction_to(target_pos) - next_pos = self.current_pos.add(m_dir) - if self.hugging_wall == 0: - self.start_pos = self.current_pos - if ct.can_move(m_dir): - ct.move(m_dir) - return True - elif ct.can_build_road(next_pos): - ct.build_road(next_pos) - ct.move(m_dir) - return True - - self.hugging_wall = 1 - self.hit_dist = self.current_pos.distance_squared(target_pos) - self.hit_pos = self.current_pos - self.last_dir = m_dir + x,y = pos + tile_env = ct.get_tile_env(pos) + match tile_env: + case Environment.EMPTY: + self.map_lut[x][y] = 1 + case Environment.ORE_TITANIUM: + self.map_lut[x][y] = 2 + case Environment.ORE_AXIONITE: + self.map_lut[x][y] = 3 + case Environment.WALL: + self.map_lut[x][y] = 4 + for building in nearby_buildings: + pos = ct.get_position(building) + x,y = pos + btype = ct.get_entity_type(building) + match btype: + case EntityType.CORE: + team = ct.get_team(building) == ct.get_team() + if team: + continue + self.map_lut[x][y] = 5 + for direction in DIRECTIONS: + x,y = pos.add(direction) + self.map_lut[x][y] = 5 + case EntityType.HARVESTER: + self.map_lut[x][y] = 6 - dx = target_pos.x - self.start_pos.x - dy = target_pos.y - self.start_pos.y - cx = self.current_pos.x - self.start_pos.x - cy = self.current_pos.y - self.start_pos.y + def a_star(self,target_pos:Position): + queue = [(0,(self.current_pos.x,self.current_pos.y))] + check = {(self.current_pos.x,self.current_pos.y):0} + full_path = {} + stopped_at = (target_pos.x,target_pos.y) + while queue: + top = heapq.heappop(queue) + reached_target = top[1] == stopped_at + goes_into_unknown = self.map_lut[top[1][0]][top[1][1]] == 0 + if reached_target or goes_into_unknown: + stopped_at = top[1] + break + elapsed_dist = check[top[1]] + for direction in DIRECTIONS: + deltas = direction.delta() + neighbor = ( top[1][0] + deltas[0],top[1][1] + deltas[1] ) + if not (0 <= neighbor[0] < self.w and 0 <= neighbor[1] < self.h ) or self.map_lut[neighbor[0]][neighbor[1]] >= 4: + continue + new_dist = 1 + elapsed_dist + rank = 1 + max(abs(neighbor[0]-target_pos.x),abs(neighbor[1]-target_pos.y)) + if neighbor not in check or new_dist < check[neighbor]: + check[neighbor] = new_dist + full_path[neighbor] = top[1] + heapq.heappush(queue,(rank,neighbor)) - on_mline = None - # Cross product should be ~0 for collinear points - cross = dx * cy - dy * cx - if abs(cross) > 1: # tolerance of 1 for grid cells - on_mline = False + path = [] + current = stopped_at + while current != (self.current_pos.x, self.current_pos.y): + path.insert(0,current) + current = full_path[current] + self.path_cahce = path - # Dot product to check current is between start and target - dot = cx * dx + cy * dy - len_sq = dx * dx + dy * dy - on_mline = 0 <= dot <= len_sq - - - current_dist = self.current_pos.distance_squared(target_pos) - if ( self.current_pos != self.hit_pos and current_dist < self.hit_dist and on_mline): - print("EXITING") - if ct.can_move(m_dir): - ct.move(m_dir) - self.hugging_wall = 0 - return True - elif ct.can_build_road(next_pos): - ct.build_road(next_pos) - ct.move(m_dir) - self.hugging_wall = 0 - return True - - for _ in range(9): - rs = self.last_dir.rotate_right() - right_pos = self.current_pos.add(rs) - right_env = ct.get_tile_env(right_pos) - right_build_id = ct.get_tile_building_id(right_pos) - right_build = ct.get_entity_type(right_build_id) if right_build_id else None - hugging_enemy_core = right_build==EntityType.CORE and ct.get_team(right_build_id) != ct.get_team() - next_pos = self.current_pos.add(self.last_dir) - if right_env == Environment.WALL or hugging_enemy_core : - if ct.can_move(self.last_dir): - ct.move(self.last_dir) - return True - elif ct.can_build_road(next_pos): - ct.build_road(next_pos) - ct.move(self.last_dir) - return True - self.last_dir = self.last_dir.rotate_left() - + def move(self,ct:Controller,target_pos:Position): + self.current_pos = ct.get_position() + if not self.path_cahce: + self.a_star(target_pos) + next_pos = Position(*self.path_cahce.pop(0)) + direction = self.current_pos.direction_to(next_pos) + if not ct.can_move(direction) or not ct.can_build_road(next_pos): + self.a_star(target_pos) - raise Exception("Failed to move") + next_pos = Position(*self.path_cahce.pop(0)) + direction = self.current_pos.direction_to(next_pos) + if ct.can_move(direction): + ct.move(direction) + return True + elif ct.can_build_road(next_pos): + ct.build_road(next_pos) + ct.move(direction) + return True def get_neighbors(self,ct:Controller): quads = [ @@ -254,8 +249,11 @@ def explore(self,ct:Controller): else: tpos = Position(*top) dist = self.current_pos.distance_squared(tpos) - if dist <= ct.get_vision_radius_sq() and ct.get_tile_env(tpos) == Environment.WALL: - self.pq.pop(0) + if dist <= ct.get_vision_radius_sq(): + building = ct.get_tile_building_id(tpos) + impassible = ct.get_entity_type(building) in (EntityType.CORE,EntityType.HARVESTER) if building else False + if ct.get_tile_env(tpos) == Environment.WALL or impassible: + self.pq.pop(0) # Update distances new_pq = [] From 3b2901bef5df98dadabbdf5dfcec2b2686ddc3a8 Mon Sep 17 00:00:00 2001 From: Cofee Coder <41608260+CoffeeCoder1015@users.noreply.github.com> Date: Sat, 28 Mar 2026 18:24:58 -0700 Subject: [PATCH 14/31] check flattened array update --- bots/k_learning/builderbot/navigation.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/bots/k_learning/builderbot/navigation.py b/bots/k_learning/builderbot/navigation.py index 26d6644..531234b 100644 --- a/bots/k_learning/builderbot/navigation.py +++ b/bots/k_learning/builderbot/navigation.py @@ -1,3 +1,4 @@ +import array from enum import Enum, EnumType import heapq from sys import stderr @@ -167,7 +168,10 @@ def update_info(self,ct:Controller,current_pos:Position,nearby_tiles:list[Positi def a_star(self,target_pos:Position): queue = [(0,(self.current_pos.x,self.current_pos.y))] - check = {(self.current_pos.x,self.current_pos.y):0} + + check = array.array('h',[-1]) * (self.w * self.h) + check[self._get_idx(self.current_pos.x,self.current_pos.y)] = 0 + full_path = {} stopped_at = (target_pos.x,target_pos.y) while queue: @@ -177,7 +181,7 @@ def a_star(self,target_pos:Position): if reached_target or goes_into_unknown: stopped_at = top[1] break - elapsed_dist = check[top[1]] + elapsed_dist = check[self._get_idx(*top[1])] for direction in DIRECTIONS: deltas = direction.delta() neighbor = ( top[1][0] + deltas[0],top[1][1] + deltas[1] ) @@ -185,8 +189,11 @@ def a_star(self,target_pos:Position): continue new_dist = 1 + elapsed_dist rank = 1 + max(abs(neighbor[0]-target_pos.x),abs(neighbor[1]-target_pos.y)) - if neighbor not in check or new_dist < check[neighbor]: - check[neighbor] = new_dist + + neighbor_idx = self._get_idx(*neighbor) + neighbor_dist = check[neighbor_idx] + if neighbor_dist == -1 or new_dist < neighbor_dist: + check[neighbor_idx] = new_dist full_path[neighbor] = top[1] heapq.heappush(queue,(rank,neighbor)) From 625e7f23af5bb1f4361e7cf61d073246f121c0cc Mon Sep 17 00:00:00 2001 From: Cofee Coder <41608260+CoffeeCoder1015@users.noreply.github.com> Date: Sat, 28 Mar 2026 20:20:36 -0700 Subject: [PATCH 15/31] bucket a*? --- bots/k_learning/builderbot/navigation.py | 40 +++++++++++++++++++----- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/bots/k_learning/builderbot/navigation.py b/bots/k_learning/builderbot/navigation.py index 531234b..5efd304 100644 --- a/bots/k_learning/builderbot/navigation.py +++ b/bots/k_learning/builderbot/navigation.py @@ -1,4 +1,5 @@ import array +from collections import deque from enum import Enum, EnumType import heapq from sys import stderr @@ -120,13 +121,17 @@ def __init__(self,w,h): self.w = w self.h = h self.map_lut = [[0]*h for _ in range(w)] - self.quad_scaling = 5 self.current_pos: Position | None = None - self.sym = None + self.pq = [] + self.visited = set() - self.path_cahce= [] + + self.bucket_n = self.w * self.h + self.min_ptr = 0 + self.buckets = [[] for _ in range(self.bucket_n)] + self.open_pos = set() def _get_idx(self, x: int, y: int) -> int: return y * self.w + x @@ -166,16 +171,35 @@ def update_info(self,ct:Controller,current_pos:Position,nearby_tiles:list[Positi case EntityType.HARVESTER: self.map_lut[x][y] = 6 + def reset_pq(self): + self.min_ptr = 0 + while self.open_pos: + top = self.open_pos.pop() + self.buckets[top] = [] + + def push_pq(self,rank,item): + self.open_pos.add(rank) + self.buckets[rank].append(item) + + def pop_pq(self): + while not self.buckets[self.min_ptr]: + self.min_ptr+=1 + top_item = self.buckets[self.min_ptr].pop() + if len(self.buckets[self.min_ptr]) == 0: + self.open_pos.remove(self.min_ptr) + return ( self.min_ptr,top_item ) + def a_star(self,target_pos:Position): - queue = [(0,(self.current_pos.x,self.current_pos.y))] + self.reset_pq() + self.push_pq(0,(self.current_pos.x,self.current_pos.y)) check = array.array('h',[-1]) * (self.w * self.h) check[self._get_idx(self.current_pos.x,self.current_pos.y)] = 0 full_path = {} stopped_at = (target_pos.x,target_pos.y) - while queue: - top = heapq.heappop(queue) + while self.open_pos: + top = self.pop_pq() reached_target = top[1] == stopped_at goes_into_unknown = self.map_lut[top[1][0]][top[1][1]] == 0 if reached_target or goes_into_unknown: @@ -188,14 +212,14 @@ def a_star(self,target_pos:Position): if not (0 <= neighbor[0] < self.w and 0 <= neighbor[1] < self.h ) or self.map_lut[neighbor[0]][neighbor[1]] >= 4: continue new_dist = 1 + elapsed_dist - rank = 1 + max(abs(neighbor[0]-target_pos.x),abs(neighbor[1]-target_pos.y)) + rank = new_dist + max(abs(neighbor[0]-target_pos.x),abs(neighbor[1]-target_pos.y)) neighbor_idx = self._get_idx(*neighbor) neighbor_dist = check[neighbor_idx] if neighbor_dist == -1 or new_dist < neighbor_dist: check[neighbor_idx] = new_dist full_path[neighbor] = top[1] - heapq.heappush(queue,(rank,neighbor)) + self.push_pq(rank,neighbor) path = [] current = stopped_at From 054fc2555ac5f48ac07af343fb90bcb201e40d94 Mon Sep 17 00:00:00 2001 From: Cofee Coder <41608260+CoffeeCoder1015@users.noreply.github.com> Date: Sun, 29 Mar 2026 02:01:42 -0700 Subject: [PATCH 16/31] fix: double pop bug --- bots/k_learning/builderbot/navigation.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/bots/k_learning/builderbot/navigation.py b/bots/k_learning/builderbot/navigation.py index 5efd304..2797303 100644 --- a/bots/k_learning/builderbot/navigation.py +++ b/bots/k_learning/builderbot/navigation.py @@ -234,11 +234,13 @@ def move(self,ct:Controller,target_pos:Position): self.a_star(target_pos) next_pos = Position(*self.path_cahce.pop(0)) direction = self.current_pos.direction_to(next_pos) - if not ct.can_move(direction) or not ct.can_build_road(next_pos): + can_move = ct.can_move(direction) or ct.can_build_road(next_pos) + if not can_move: self.a_star(target_pos) + # TODO: path_cache empty guard + next_pos = Position(*self.path_cahce.pop(0)) + direction = self.current_pos.direction_to(next_pos) - next_pos = Position(*self.path_cahce.pop(0)) - direction = self.current_pos.direction_to(next_pos) if ct.can_move(direction): ct.move(direction) return True From caf2ea1d7dd81c0a68d4a87286298f4754e57950 Mon Sep 17 00:00:00 2001 From: Cofee Coder <41608260+CoffeeCoder1015@users.noreply.github.com> Date: Sun, 29 Mar 2026 02:06:26 -0700 Subject: [PATCH 17/31] feat1: tracking inserted tems --- bots/k_learning/builderbot/navigation.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/bots/k_learning/builderbot/navigation.py b/bots/k_learning/builderbot/navigation.py index 2797303..1d11abb 100644 --- a/bots/k_learning/builderbot/navigation.py +++ b/bots/k_learning/builderbot/navigation.py @@ -130,6 +130,7 @@ def __init__(self,w,h): self.bucket_n = self.w * self.h self.min_ptr = 0 + self.inserted_items = 0 self.buckets = [[] for _ in range(self.bucket_n)] self.open_pos = set() @@ -173,6 +174,7 @@ def update_info(self,ct:Controller,current_pos:Position,nearby_tiles:list[Positi def reset_pq(self): self.min_ptr = 0 + self.inserted_items = 0 while self.open_pos: top = self.open_pos.pop() self.buckets[top] = [] @@ -180,6 +182,7 @@ def reset_pq(self): def push_pq(self,rank,item): self.open_pos.add(rank) self.buckets[rank].append(item) + self.inserted_items += 1 def pop_pq(self): while not self.buckets[self.min_ptr]: @@ -187,6 +190,7 @@ def pop_pq(self): top_item = self.buckets[self.min_ptr].pop() if len(self.buckets[self.min_ptr]) == 0: self.open_pos.remove(self.min_ptr) + self.inserted_items -= 1 return ( self.min_ptr,top_item ) def a_star(self,target_pos:Position): From e51cc9ac3d47269a7595ce9e0642934d268cda00 Mon Sep 17 00:00:00 2001 From: Cofee Coder <41608260+CoffeeCoder1015@users.noreply.github.com> Date: Sun, 29 Mar 2026 10:27:22 -0700 Subject: [PATCH 18/31] append and reverse --- bots/k_learning/builderbot/navigation.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bots/k_learning/builderbot/navigation.py b/bots/k_learning/builderbot/navigation.py index 1d11abb..237f834 100644 --- a/bots/k_learning/builderbot/navigation.py +++ b/bots/k_learning/builderbot/navigation.py @@ -228,8 +228,9 @@ def a_star(self,target_pos:Position): path = [] current = stopped_at while current != (self.current_pos.x, self.current_pos.y): - path.insert(0,current) + path.append(current) current = full_path[current] + path.reverse() self.path_cahce = path def move(self,ct:Controller,target_pos:Position): From 3e6c0682b81edfea00a730d8d9021479f9d0c109 Mon Sep 17 00:00:00 2001 From: Cofee Coder <41608260+CoffeeCoder1015@users.noreply.github.com> Date: Sun, 29 Mar 2026 12:59:19 -0700 Subject: [PATCH 19/31] reset on target switch and smarter impossible target detection --- bots/k_learning/builderbot/navigation.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/bots/k_learning/builderbot/navigation.py b/bots/k_learning/builderbot/navigation.py index 237f834..b2ba253 100644 --- a/bots/k_learning/builderbot/navigation.py +++ b/bots/k_learning/builderbot/navigation.py @@ -125,6 +125,7 @@ def __init__(self,w,h): self.pq = [] + self.last_target = None self.visited = set() self.path_cahce= [] @@ -235,7 +236,8 @@ def a_star(self,target_pos:Position): def move(self,ct:Controller,target_pos:Position): self.current_pos = ct.get_position() - if not self.path_cahce: + if not self.path_cahce or self.last_target != target_pos: + self.last_target = target_pos self.a_star(target_pos) next_pos = Position(*self.path_cahce.pop(0)) direction = self.current_pos.direction_to(next_pos) @@ -284,14 +286,8 @@ def explore(self,ct:Controller): if (self.current_pos.x,self.current_pos.y) == top: self.pq.pop(0) self.pq.extend(self.get_neighbors(ct)) - else: - tpos = Position(*top) - dist = self.current_pos.distance_squared(tpos) - if dist <= ct.get_vision_radius_sq(): - building = ct.get_tile_building_id(tpos) - impassible = ct.get_entity_type(building) in (EntityType.CORE,EntityType.HARVESTER) if building else False - if ct.get_tile_env(tpos) == Environment.WALL or impassible: - self.pq.pop(0) + elif self.map_lut[top[0]][top[1]] >= 4: # Impassible + self.pq.pop(0) # Update distances new_pq = [] From 492092c1b3728575cee47fce743d8e0e284c5200 Mon Sep 17 00:00:00 2001 From: Cofee Coder <41608260+CoffeeCoder1015@users.noreply.github.com> Date: Sun, 29 Mar 2026 13:02:16 -0700 Subject: [PATCH 20/31] reveresed path following --- bots/k_learning/builderbot/navigation.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/bots/k_learning/builderbot/navigation.py b/bots/k_learning/builderbot/navigation.py index b2ba253..b303c72 100644 --- a/bots/k_learning/builderbot/navigation.py +++ b/bots/k_learning/builderbot/navigation.py @@ -231,7 +231,6 @@ def a_star(self,target_pos:Position): while current != (self.current_pos.x, self.current_pos.y): path.append(current) current = full_path[current] - path.reverse() self.path_cahce = path def move(self,ct:Controller,target_pos:Position): @@ -239,13 +238,13 @@ def move(self,ct:Controller,target_pos:Position): if not self.path_cahce or self.last_target != target_pos: self.last_target = target_pos self.a_star(target_pos) - next_pos = Position(*self.path_cahce.pop(0)) + next_pos = Position(*self.path_cahce.pop()) direction = self.current_pos.direction_to(next_pos) can_move = ct.can_move(direction) or ct.can_build_road(next_pos) if not can_move: self.a_star(target_pos) # TODO: path_cache empty guard - next_pos = Position(*self.path_cahce.pop(0)) + next_pos = Position(*self.path_cahce.pop()) direction = self.current_pos.direction_to(next_pos) if ct.can_move(direction): From ea606ea60c6f0b2daf4b65346b928a1349c78b34 Mon Sep 17 00:00:00 2001 From: Cofee Coder <41608260+CoffeeCoder1015@users.noreply.github.com> Date: Sun, 29 Mar 2026 13:12:39 -0700 Subject: [PATCH 21/31] update rank on push --- bots/k_learning/builderbot/navigation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bots/k_learning/builderbot/navigation.py b/bots/k_learning/builderbot/navigation.py index b303c72..fbbb5f6 100644 --- a/bots/k_learning/builderbot/navigation.py +++ b/bots/k_learning/builderbot/navigation.py @@ -217,13 +217,13 @@ def a_star(self,target_pos:Position): if not (0 <= neighbor[0] < self.w and 0 <= neighbor[1] < self.h ) or self.map_lut[neighbor[0]][neighbor[1]] >= 4: continue new_dist = 1 + elapsed_dist - rank = new_dist + max(abs(neighbor[0]-target_pos.x),abs(neighbor[1]-target_pos.y)) neighbor_idx = self._get_idx(*neighbor) neighbor_dist = check[neighbor_idx] if neighbor_dist == -1 or new_dist < neighbor_dist: check[neighbor_idx] = new_dist full_path[neighbor] = top[1] + rank = new_dist + max(abs(neighbor[0]-target_pos.x),abs(neighbor[1]-target_pos.y)) self.push_pq(rank,neighbor) path = [] From f8ca21ee4225c97cf6e8ba829dd4082086890de1 Mon Sep 17 00:00:00 2001 From: Cofee Coder <41608260+CoffeeCoder1015@users.noreply.github.com> Date: Sun, 29 Mar 2026 17:30:59 -0700 Subject: [PATCH 22/31] tmp: profiler --- bots/k_learning/main.py | 17 ++++++++++++++++- profile_bot.bat | 3 +++ 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 profile_bot.bat diff --git a/bots/k_learning/main.py b/bots/k_learning/main.py index 8684183..1a2f338 100644 --- a/bots/k_learning/main.py +++ b/bots/k_learning/main.py @@ -10,6 +10,10 @@ """ +import os +if os.getenv("PROFILE"): + import cProfile + from cambc import Controller, EntityType from builderbot import BuilderBot @@ -23,9 +27,20 @@ class Player: def __init__(self): self.active:Core | BuilderBot = None + self.profiler = None def run(self, ct: Controller) -> None: etype = ct.get_entity_type() if self.active is None: self.active = mapping[etype]() - self.active.run(ct) \ No newline at end of file + if os.getenv("PROFILE"): + if ct.get_entity_type() == EntityType.BUILDER_BOT: + if not self.profiler: + self.profiler = cProfile.Profile() + self.profiler.enable() # just run it the whole time + self.active.run(ct) + if os.getenv("PROFILE"): + if ct.get_entity_type() == EntityType.BUILDER_BOT: + self.profiler.disable() + if ct.get_current_round() == 100: # dump at round 500 and stop + self.profiler.dump_stats("profile.prof") \ No newline at end of file diff --git a/profile_bot.bat b/profile_bot.bat new file mode 100644 index 0000000..1b44200 --- /dev/null +++ b/profile_bot.bat @@ -0,0 +1,3 @@ +@echo off +set PROFILE=1 +cambc run %* \ No newline at end of file From 7a840479f57944d7bc41529ee5cf1ed4d069ac75 Mon Sep 17 00:00:00 2001 From: Cofee Coder <41608260+CoffeeCoder1015@users.noreply.github.com> Date: Sun, 29 Mar 2026 17:40:24 -0700 Subject: [PATCH 23/31] deltas function is ahh --- bots/k_learning/builderbot/navigation.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bots/k_learning/builderbot/navigation.py b/bots/k_learning/builderbot/navigation.py index fbbb5f6..74ed358 100644 --- a/bots/k_learning/builderbot/navigation.py +++ b/bots/k_learning/builderbot/navigation.py @@ -116,6 +116,8 @@ def draw_debug(self, ct: Controller): ] DIRECTIONS = [d for d in Direction if d != Direction.CENTRE] +DELTAS = [ (0, -1), (1, -1), (1, 0), (1, 1), (0, 1), (-1, 1), (-1, 0), (-1, -1), ] + class Navigation: def __init__(self,w,h): self.w = w @@ -211,8 +213,7 @@ def a_star(self,target_pos:Position): stopped_at = top[1] break elapsed_dist = check[self._get_idx(*top[1])] - for direction in DIRECTIONS: - deltas = direction.delta() + for deltas in DELTAS: neighbor = ( top[1][0] + deltas[0],top[1][1] + deltas[1] ) if not (0 <= neighbor[0] < self.w and 0 <= neighbor[1] < self.h ) or self.map_lut[neighbor[0]][neighbor[1]] >= 4: continue From 307278aaa6a3a55c80f43f8a2143a007231b8b52 Mon Sep 17 00:00:00 2001 From: Cofee Coder <41608260+CoffeeCoder1015@users.noreply.github.com> Date: Sun, 29 Mar 2026 17:44:07 -0700 Subject: [PATCH 24/31] corretness changes and updating get_neighbors --- bots/k_learning/builderbot/navigation.py | 32 +++++++++++++----------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/bots/k_learning/builderbot/navigation.py b/bots/k_learning/builderbot/navigation.py index 74ed358..48aa703 100644 --- a/bots/k_learning/builderbot/navigation.py +++ b/bots/k_learning/builderbot/navigation.py @@ -200,7 +200,7 @@ def a_star(self,target_pos:Position): self.reset_pq() self.push_pq(0,(self.current_pos.x,self.current_pos.y)) - check = array.array('h',[-1]) * (self.w * self.h) + check = [-1] * (self.w * self.h) check[self._get_idx(self.current_pos.x,self.current_pos.y)] = 0 full_path = {} @@ -256,16 +256,19 @@ def move(self,ct:Controller,target_pos:Position): ct.move(direction) return True - def get_neighbors(self,ct:Controller): + def get_neighbors(self,root_pos=None): + start_pos = self.current_pos + if root_pos: + start_pos = root_pos quads = [ - (self.current_pos.x, self.current_pos.y - 5), - (self.current_pos.x + 5, self.current_pos.y - 5), - (self.current_pos.x + 5, self.current_pos.y), - (self.current_pos.x + 5, self.current_pos.y + 5), - (self.current_pos.x, self.current_pos.y + 5), - (self.current_pos.x - 5, self.current_pos.y + 5), - (self.current_pos.x - 5, self.current_pos.y), - (self.current_pos.x - 5, self.current_pos.y - 5), + (start_pos.x, start_pos.y - 5), + (start_pos.x + 5, start_pos.y - 5), + (start_pos.x + 5, start_pos.y), + (start_pos.x + 5, start_pos.y + 5), + (start_pos.x, start_pos.y + 5), + (start_pos.x - 5, start_pos.y + 5), + (start_pos.x - 5, start_pos.y), + (start_pos.x - 5, start_pos.y - 5), ] neighbors = [] for q in quads: @@ -279,13 +282,12 @@ def explore(self,ct:Controller): ct.draw_indicator_dot(Position(*p),255,0,0) if not self.pq: - self.pq = self.get_neighbors(ct) - - + self.pq = self.get_neighbors() + top = self.pq[0][1] if (self.current_pos.x,self.current_pos.y) == top: self.pq.pop(0) - self.pq.extend(self.get_neighbors(ct)) + self.pq.extend(self.get_neighbors()) elif self.map_lut[top[0]][top[1]] >= 4: # Impassible self.pq.pop(0) @@ -293,7 +295,7 @@ def explore(self,ct:Controller): new_pq = [] for i in range(len(self.pq)): _,pos = self.pq[i] - new_val = self.current_pos.distance_squared(Position(*pos)) + new_val = max(abs(self.current_pos.x-pos[0]),abs(self.current_pos.y-pos[1])) heapq.heappush(new_pq,(new_val,pos)) self.pq = new_pq final_target = Position(*self.pq[0][1]) From d10cb0b4c40bd94850a06fc1cd3f16ae70e1e84d Mon Sep 17 00:00:00 2001 From: Cofee Coder <41608260+CoffeeCoder1015@users.noreply.github.com> Date: Sun, 29 Mar 2026 17:44:46 -0700 Subject: [PATCH 25/31] definetly save min_ptr --- bots/k_learning/builderbot/navigation.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bots/k_learning/builderbot/navigation.py b/bots/k_learning/builderbot/navigation.py index 48aa703..a47527a 100644 --- a/bots/k_learning/builderbot/navigation.py +++ b/bots/k_learning/builderbot/navigation.py @@ -176,7 +176,7 @@ def update_info(self,ct:Controller,current_pos:Position,nearby_tiles:list[Positi self.map_lut[x][y] = 6 def reset_pq(self): - self.min_ptr = 0 + self.min_ptr = self.bucket_n self.inserted_items = 0 while self.open_pos: top = self.open_pos.pop() @@ -185,6 +185,7 @@ def reset_pq(self): def push_pq(self,rank,item): self.open_pos.add(rank) self.buckets[rank].append(item) + self.min_ptr = min(self.min_ptr,rank) self.inserted_items += 1 def pop_pq(self): From 17c9c669e664e8cee6f66ee6083d0ed6f0987c15 Mon Sep 17 00:00:00 2001 From: Cofee Coder <41608260+CoffeeCoder1015@users.noreply.github.com> Date: Sun, 29 Mar 2026 17:45:23 -0700 Subject: [PATCH 26/31] might be better than min cuz that shit is overloaded as fuck --- bots/k_learning/builderbot/navigation.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bots/k_learning/builderbot/navigation.py b/bots/k_learning/builderbot/navigation.py index a47527a..102cd34 100644 --- a/bots/k_learning/builderbot/navigation.py +++ b/bots/k_learning/builderbot/navigation.py @@ -185,7 +185,8 @@ def reset_pq(self): def push_pq(self,rank,item): self.open_pos.add(rank) self.buckets[rank].append(item) - self.min_ptr = min(self.min_ptr,rank) + if rank < self.min_ptr: + self.min_ptr = rank self.inserted_items += 1 def pop_pq(self): From f9a0379b11d0555e84e2c24a3d79d105e73c59d3 Mon Sep 17 00:00:00 2001 From: Cofee Coder <41608260+CoffeeCoder1015@users.noreply.github.com> Date: Sun, 29 Mar 2026 18:05:03 -0700 Subject: [PATCH 27/31] min max tracking --- bots/k_learning/builderbot/navigation.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/bots/k_learning/builderbot/navigation.py b/bots/k_learning/builderbot/navigation.py index 102cd34..a4fd41a 100644 --- a/bots/k_learning/builderbot/navigation.py +++ b/bots/k_learning/builderbot/navigation.py @@ -133,6 +133,7 @@ def __init__(self,w,h): self.bucket_n = self.w * self.h self.min_ptr = 0 + self.max_ptr = 0 self.inserted_items = 0 self.buckets = [[] for _ in range(self.bucket_n)] self.open_pos = set() @@ -177,6 +178,7 @@ def update_info(self,ct:Controller,current_pos:Position,nearby_tiles:list[Positi def reset_pq(self): self.min_ptr = self.bucket_n + self.max_ptr = 0 self.inserted_items = 0 while self.open_pos: top = self.open_pos.pop() @@ -185,8 +187,13 @@ def reset_pq(self): def push_pq(self,rank,item): self.open_pos.add(rank) self.buckets[rank].append(item) - if rank < self.min_ptr: + + if rank < self.min_ptr: # Pick min self.min_ptr = rank + + if rank > self.max_ptr: + self.max_ptr = rank + self.inserted_items += 1 def pop_pq(self): From f55fcb713c3bb07858376af5b6fbe16833b4fcc0 Mon Sep 17 00:00:00 2001 From: Cofee Coder <41608260+CoffeeCoder1015@users.noreply.github.com> Date: Sun, 29 Mar 2026 18:29:29 -0700 Subject: [PATCH 28/31] local spike timing --- bots/k_learning/main.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/bots/k_learning/main.py b/bots/k_learning/main.py index 1a2f338..e88a730 100644 --- a/bots/k_learning/main.py +++ b/bots/k_learning/main.py @@ -11,6 +11,7 @@ import os +import time if os.getenv("PROFILE"): import cProfile @@ -28,6 +29,7 @@ class Player: def __init__(self): self.active:Core | BuilderBot = None self.profiler = None + self.te = 0 def run(self, ct: Controller) -> None: etype = ct.get_entity_type() @@ -38,7 +40,11 @@ def run(self, ct: Controller) -> None: if not self.profiler: self.profiler = cProfile.Profile() self.profiler.enable() # just run it the whole time + start = time.time_ns() self.active.run(ct) + end = time.time_ns() + self.te = max(self.te,end-start) + print(self.te) if os.getenv("PROFILE"): if ct.get_entity_type() == EntityType.BUILDER_BOT: self.profiler.disable() From 2d1e3fd542ab60f90334cc2b790e3ff623e97ab2 Mon Sep 17 00:00:00 2001 From: Cofee Coder <41608260+CoffeeCoder1015@users.noreply.github.com> Date: Sun, 29 Mar 2026 18:34:27 -0700 Subject: [PATCH 29/31] codex's implementaiton of beam search --- bots/k_learning/builderbot/navigation.py | 70 +++++++++++++++++++++++- 1 file changed, 67 insertions(+), 3 deletions(-) diff --git a/bots/k_learning/builderbot/navigation.py b/bots/k_learning/builderbot/navigation.py index a4fd41a..e66edc1 100644 --- a/bots/k_learning/builderbot/navigation.py +++ b/bots/k_learning/builderbot/navigation.py @@ -131,6 +131,7 @@ def __init__(self,w,h): self.visited = set() self.path_cahce= [] + self.beam_width = 32 self.bucket_n = self.w * self.h self.min_ptr = 0 self.max_ptr = 0 @@ -243,17 +244,80 @@ def a_star(self,target_pos:Position): current = full_path[current] self.path_cahce = path + def beam_search(self, target_pos: Position): + start = (self.current_pos.x, self.current_pos.y) + check = [-1] * (self.w * self.h) + check[self._get_idx(*start)] = 0 + + full_path = {} + stopped_at = start + beam = [start] + + while beam: + next_layer = [] + next_seen = set() + + for node in beam: + reached_target = node == (target_pos.x, target_pos.y) + goes_into_unknown = self.map_lut[node[0]][node[1]] == 0 + if reached_target or goes_into_unknown: + stopped_at = node + beam = [] + next_layer = [] + break + + elapsed_dist = check[self._get_idx(*node)] + for deltas in DELTAS: + neighbor = (node[0] + deltas[0], node[1] + deltas[1]) + if not self.in_bounds(*neighbor) or self.map_lut[neighbor[0]][neighbor[1]] >= 4: + continue + + new_dist = elapsed_dist + 1 + neighbor_idx = self._get_idx(*neighbor) + neighbor_dist = check[neighbor_idx] + if neighbor_dist != -1 and new_dist >= neighbor_dist: + continue + + check[neighbor_idx] = new_dist + full_path[neighbor] = node + if neighbor not in next_seen: + next_seen.add(neighbor) + score = new_dist + max(abs(neighbor[0] - target_pos.x), abs(neighbor[1] - target_pos.y)) + heuristic = max(abs(neighbor[0] - target_pos.x), abs(neighbor[1] - target_pos.y)) + heapq.heappush(next_layer, (-score, -heuristic, neighbor)) + if len(next_layer) > self.beam_width: + heapq.heappop(next_layer) + + if not next_layer: + continue + + beam = [pos for _, _, pos in sorted(next_layer, reverse=True)] + stopped_at = beam[0] + + path = [] + current = stopped_at + while current != start and current in full_path: + path.append(current) + current = full_path[current] + self.path_cahce = path + def move(self,ct:Controller,target_pos:Position): self.current_pos = ct.get_position() if not self.path_cahce or self.last_target != target_pos: self.last_target = target_pos - self.a_star(target_pos) + # self.a_star(target_pos) + self.beam_search(target_pos) + if not self.path_cahce: + return False next_pos = Position(*self.path_cahce.pop()) direction = self.current_pos.direction_to(next_pos) can_move = ct.can_move(direction) or ct.can_build_road(next_pos) if not can_move: - self.a_star(target_pos) + # self.a_star(target_pos) + self.beam_search(target_pos) # TODO: path_cache empty guard + if not self.path_cahce: + return False next_pos = Position(*self.path_cahce.pop()) direction = self.current_pos.direction_to(next_pos) @@ -309,4 +373,4 @@ def explore(self,ct:Controller): self.pq = new_pq final_target = Position(*self.pq[0][1]) ct.draw_indicator_line(self.current_pos,final_target,0,255,0) - self.move(ct,final_target) \ No newline at end of file + self.move(ct,final_target) From b2c21e2649a331df8229106f31e41898de76f608 Mon Sep 17 00:00:00 2001 From: Cofee Coder <41608260+CoffeeCoder1015@users.noreply.github.com> Date: Sun, 29 Mar 2026 19:41:47 -0700 Subject: [PATCH 30/31] a* bounded frontier and a* cutoff --- bots/k_learning/builderbot/navigation.py | 261 ++++++++++++++++------- 1 file changed, 189 insertions(+), 72 deletions(-) diff --git a/bots/k_learning/builderbot/navigation.py b/bots/k_learning/builderbot/navigation.py index e66edc1..045d843 100644 --- a/bots/k_learning/builderbot/navigation.py +++ b/bots/k_learning/builderbot/navigation.py @@ -117,6 +117,9 @@ def draw_debug(self, ct: Controller): DIRECTIONS = [d for d in Direction if d != Direction.CENTRE] DELTAS = [ (0, -1), (1, -1), (1, 0), (1, 1), (0, 1), (-1, 1), (-1, 0), (-1, -1), ] +REVERSE_DIR_CODES = [(i + 4) % len(DELTAS) for i in range(len(DELTAS))] +DELTA_TO_DIRECTION = {direction.delta(): direction for direction in DIRECTIONS} +DIR_CODE_TO_DIRECTION = [DELTA_TO_DIRECTION[delta] for delta in DELTAS] class Navigation: def __init__(self,w,h): @@ -129,7 +132,6 @@ def __init__(self,w,h): self.last_target = None self.visited = set() - self.path_cahce= [] self.beam_width = 32 self.bucket_n = self.w * self.h @@ -138,6 +140,14 @@ def __init__(self,w,h): self.inserted_items = 0 self.buckets = [[] for _ in range(self.bucket_n)] self.open_pos = set() + self.parent_dir = array.array("b", [-1]) * self.bucket_n + self.parent_epoch = array.array("I", [0]) * self.bucket_n + self.next_dir = array.array("b", [-1]) * self.bucket_n + self.next_epoch = array.array("I", [0]) * self.bucket_n + self.search_epoch = 0 + self.route_epoch = 0 + self.max_a_star_expansions = 64 + self.max_a_star_frontier = 32 def _get_idx(self, x: int, y: int) -> int: return y * self.w + x @@ -145,6 +155,58 @@ def _get_idx(self, x: int, y: int) -> int: def in_bounds(self,x:int,y:int): return 0 <= x < self.w and 0 <= y < self.h + def _begin_search(self): + self.search_epoch += 1 + self.route_epoch = 0 + return self.search_epoch + + def _invalidate_route(self): + self.route_epoch = 0 + + def _step_idx(self, idx: int, dir_code: int) -> int: + x = idx % self.w + y = idx // self.w + dx, dy = DELTAS[dir_code] + return self._get_idx(x + dx, y + dy) + + def _rebuild_route(self, start_idx: int, stopped_idx: int, search_epoch: int): + if stopped_idx == start_idx: + self._invalidate_route() + return + + self.route_epoch = search_epoch + current_idx = stopped_idx + while current_idx != start_idx and self.parent_epoch[current_idx] == search_epoch: + reverse_dir = self.parent_dir[current_idx] + if reverse_dir < 0: + break + parent_idx = self._step_idx(current_idx, reverse_dir) + self.next_dir[parent_idx] = REVERSE_DIR_CODES[reverse_dir] + self.next_epoch[parent_idx] = self.route_epoch + current_idx = parent_idx + + if current_idx != start_idx: + self._invalidate_route() + + def _has_route_step(self, idx: int) -> bool: + return self.route_epoch != 0 and self.next_epoch[idx] == self.route_epoch and self.next_dir[idx] >= 0 + + def _heuristic(self, x: int, y: int, target_pos: Position) -> int: + return max(abs(x - target_pos.x), abs(y - target_pos.y)) + + def _pick_better_fallback( + self, + current_best: tuple[int, int, int, tuple[int, int]] | None, + node: tuple[int, int], + g_cost: int, + target_pos: Position, + ): + heuristic = self._heuristic(node[0], node[1], target_pos) + score = (heuristic, g_cost + heuristic, g_cost, node) + if current_best is None or score < current_best: + return score + return current_best + def update_info(self,ct:Controller,current_pos:Position,nearby_tiles:list[Position],nearby_buildings:list[int]): self.current_pos = current_pos # Update LUT @@ -205,15 +267,32 @@ def pop_pq(self): self.open_pos.remove(self.min_ptr) self.inserted_items -= 1 return ( self.min_ptr,top_item ) + + def pop_worst_pq(self): + while self.max_ptr > 0 and not self.buckets[self.max_ptr]: + self.max_ptr -= 1 + + if not self.buckets[self.max_ptr]: + return None + + top_item = self.buckets[self.max_ptr].pop() + if len(self.buckets[self.max_ptr]) == 0: + self.open_pos.remove(self.max_ptr) + while self.max_ptr > 0 and not self.buckets[self.max_ptr]: + self.max_ptr -= 1 + self.inserted_items -= 1 + return (self.max_ptr, top_item) def a_star(self,target_pos:Position): self.reset_pq() - self.push_pq(0,(self.current_pos.x,self.current_pos.y)) + start = (self.current_pos.x,self.current_pos.y) + start_idx = self._get_idx(*start) + search_epoch = self._begin_search() + self.push_pq(0,start) check = [-1] * (self.w * self.h) - check[self._get_idx(self.current_pos.x,self.current_pos.y)] = 0 + check[start_idx] = 0 - full_path = {} stopped_at = (target_pos.x,target_pos.y) while self.open_pos: top = self.pop_pq() @@ -223,7 +302,7 @@ def a_star(self,target_pos:Position): stopped_at = top[1] break elapsed_dist = check[self._get_idx(*top[1])] - for deltas in DELTAS: + for dir_code, deltas in enumerate(DELTAS): neighbor = ( top[1][0] + deltas[0],top[1][1] + deltas[1] ) if not (0 <= neighbor[0] < self.w and 0 <= neighbor[1] < self.h ) or self.map_lut[neighbor[0]][neighbor[1]] >= 4: continue @@ -233,93 +312,131 @@ def a_star(self,target_pos:Position): neighbor_dist = check[neighbor_idx] if neighbor_dist == -1 or new_dist < neighbor_dist: check[neighbor_idx] = new_dist - full_path[neighbor] = top[1] - rank = new_dist + max(abs(neighbor[0]-target_pos.x),abs(neighbor[1]-target_pos.y)) + self.parent_dir[neighbor_idx] = REVERSE_DIR_CODES[dir_code] + self.parent_epoch[neighbor_idx] = search_epoch + rank = new_dist + self._heuristic(neighbor[0], neighbor[1], target_pos) self.push_pq(rank,neighbor) + self._rebuild_route(start_idx, self._get_idx(*stopped_at), search_epoch) - path = [] - current = stopped_at - while current != (self.current_pos.x, self.current_pos.y): - path.append(current) - current = full_path[current] - self.path_cahce = path - - def beam_search(self, target_pos: Position): + def a_star_cutoff(self, target_pos: Position, max_expansions: int | None = None): + self.reset_pq() start = (self.current_pos.x, self.current_pos.y) + start_idx = self._get_idx(*start) + search_epoch = self._begin_search() check = [-1] * (self.w * self.h) - check[self._get_idx(*start)] = 0 + check[start_idx] = 0 + stopped_at = start + best_fallback = self._pick_better_fallback(None, start, 0, target_pos) + expansions = 0 + budget = self.max_a_star_expansions if max_expansions is None else max_expansions + self.push_pq(0, start) + + while self.open_pos and expansions < budget: + _, node = self.pop_pq() + expansions += 1 + + reached_target = node == (target_pos.x, target_pos.y) + goes_into_unknown = self.map_lut[node[0]][node[1]] == 0 + if reached_target or goes_into_unknown: + stopped_at = node + break + + elapsed_dist = check[self._get_idx(*node)] + best_fallback = self._pick_better_fallback(best_fallback, node, elapsed_dist, target_pos) + for dir_code, deltas in enumerate(DELTAS): + neighbor = (node[0] + deltas[0], node[1] + deltas[1]) + if not self.in_bounds(*neighbor) or self.map_lut[neighbor[0]][neighbor[1]] >= 4: + continue + + new_dist = elapsed_dist + 1 + neighbor_idx = self._get_idx(*neighbor) + neighbor_dist = check[neighbor_idx] + if neighbor_dist != -1 and new_dist >= neighbor_dist: + continue + + check[neighbor_idx] = new_dist + self.parent_dir[neighbor_idx] = REVERSE_DIR_CODES[dir_code] + self.parent_epoch[neighbor_idx] = search_epoch + rank = new_dist + self._heuristic(neighbor[0], neighbor[1], target_pos) + self.push_pq(rank, neighbor) + best_fallback = self._pick_better_fallback(best_fallback, neighbor, new_dist, target_pos) + else: + if best_fallback is not None: + stopped_at = best_fallback[3] - full_path = {} + self._rebuild_route(start_idx, self._get_idx(*stopped_at), search_epoch) + + def a_star_bounded_frontier(self, target_pos: Position, max_frontier_size: int | None = None): + self.reset_pq() + start = (self.current_pos.x, self.current_pos.y) + start_idx = self._get_idx(*start) + search_epoch = self._begin_search() + check = [-1] * (self.w * self.h) + check[start_idx] = 0 stopped_at = start - beam = [start] - - while beam: - next_layer = [] - next_seen = set() - - for node in beam: - reached_target = node == (target_pos.x, target_pos.y) - goes_into_unknown = self.map_lut[node[0]][node[1]] == 0 - if reached_target or goes_into_unknown: - stopped_at = node - beam = [] - next_layer = [] - break - - elapsed_dist = check[self._get_idx(*node)] - for deltas in DELTAS: - neighbor = (node[0] + deltas[0], node[1] + deltas[1]) - if not self.in_bounds(*neighbor) or self.map_lut[neighbor[0]][neighbor[1]] >= 4: - continue + best_fallback = self._pick_better_fallback(None, start, 0, target_pos) + frontier_limit = self.max_a_star_frontier if max_frontier_size is None else max_frontier_size + self.push_pq(0, start) - new_dist = elapsed_dist + 1 - neighbor_idx = self._get_idx(*neighbor) - neighbor_dist = check[neighbor_idx] - if neighbor_dist != -1 and new_dist >= neighbor_dist: - continue + while self.open_pos: + _, node = self.pop_pq() - check[neighbor_idx] = new_dist - full_path[neighbor] = node - if neighbor not in next_seen: - next_seen.add(neighbor) - score = new_dist + max(abs(neighbor[0] - target_pos.x), abs(neighbor[1] - target_pos.y)) - heuristic = max(abs(neighbor[0] - target_pos.x), abs(neighbor[1] - target_pos.y)) - heapq.heappush(next_layer, (-score, -heuristic, neighbor)) - if len(next_layer) > self.beam_width: - heapq.heappop(next_layer) - - if not next_layer: - continue + reached_target = node == (target_pos.x, target_pos.y) + goes_into_unknown = self.map_lut[node[0]][node[1]] == 0 + if reached_target or goes_into_unknown: + stopped_at = node + break - beam = [pos for _, _, pos in sorted(next_layer, reverse=True)] - stopped_at = beam[0] + elapsed_dist = check[self._get_idx(*node)] + best_fallback = self._pick_better_fallback(best_fallback, node, elapsed_dist, target_pos) + for dir_code, deltas in enumerate(DELTAS): + neighbor = (node[0] + deltas[0], node[1] + deltas[1]) + if not self.in_bounds(*neighbor) or self.map_lut[neighbor[0]][neighbor[1]] >= 4: + continue - path = [] - current = stopped_at - while current != start and current in full_path: - path.append(current) - current = full_path[current] - self.path_cahce = path + new_dist = elapsed_dist + 1 + neighbor_idx = self._get_idx(*neighbor) + neighbor_dist = check[neighbor_idx] + if neighbor_dist != -1 and new_dist >= neighbor_dist: + continue + + check[neighbor_idx] = new_dist + self.parent_dir[neighbor_idx] = REVERSE_DIR_CODES[dir_code] + self.parent_epoch[neighbor_idx] = search_epoch + rank = new_dist + self._heuristic(neighbor[0], neighbor[1], target_pos) + self.push_pq(rank, neighbor) + best_fallback = self._pick_better_fallback(best_fallback, neighbor, new_dist, target_pos) + + if self.inserted_items > frontier_limit: + self.pop_worst_pq() + else: + if best_fallback is not None: + stopped_at = best_fallback[3] + + self._rebuild_route(start_idx, self._get_idx(*stopped_at), search_epoch) def move(self,ct:Controller,target_pos:Position): self.current_pos = ct.get_position() - if not self.path_cahce or self.last_target != target_pos: + current_idx = self._get_idx(self.current_pos.x, self.current_pos.y) + if self.last_target != target_pos or not self._has_route_step(current_idx): self.last_target = target_pos + self.a_star_bounded_frontier(target_pos) # self.a_star(target_pos) - self.beam_search(target_pos) - if not self.path_cahce: + if not self._has_route_step(current_idx): return False - next_pos = Position(*self.path_cahce.pop()) - direction = self.current_pos.direction_to(next_pos) + + direction = DIR_CODE_TO_DIRECTION[self.next_dir[current_idx]] + next_pos = self.current_pos.add(direction) can_move = ct.can_move(direction) or ct.can_build_road(next_pos) if not can_move: + self._invalidate_route() + self.a_star_bounded_frontier(target_pos) # self.a_star(target_pos) - self.beam_search(target_pos) - # TODO: path_cache empty guard - if not self.path_cahce: + current_idx = self._get_idx(self.current_pos.x, self.current_pos.y) + if not self._has_route_step(current_idx): return False - next_pos = Position(*self.path_cahce.pop()) - direction = self.current_pos.direction_to(next_pos) + direction = DIR_CODE_TO_DIRECTION[self.next_dir[current_idx]] + next_pos = self.current_pos.add(direction) if ct.can_move(direction): ct.move(direction) From b00001085c35616a0fca3b6f2b425602e9ff040b Mon Sep 17 00:00:00 2001 From: Cofee Coder <41608260+CoffeeCoder1015@users.noreply.github.com> Date: Sun, 29 Mar 2026 20:57:08 -0700 Subject: [PATCH 31/31] combined limited a* --- bots/k_learning/builderbot/navigation.py | 70 ++++++------------------ 1 file changed, 17 insertions(+), 53 deletions(-) diff --git a/bots/k_learning/builderbot/navigation.py b/bots/k_learning/builderbot/navigation.py index 045d843..400770c 100644 --- a/bots/k_learning/builderbot/navigation.py +++ b/bots/k_learning/builderbot/navigation.py @@ -133,7 +133,6 @@ def __init__(self,w,h): self.last_target = None self.visited = set() - self.beam_width = 32 self.bucket_n = self.w * self.h self.min_ptr = 0 self.max_ptr = 0 @@ -318,7 +317,12 @@ def a_star(self,target_pos:Position): self.push_pq(rank,neighbor) self._rebuild_route(start_idx, self._get_idx(*stopped_at), search_epoch) - def a_star_cutoff(self, target_pos: Position, max_expansions: int | None = None): + def a_star_limited( + self, + target_pos: Position, + max_expansions: int | None = None, + max_frontier_size: int | None = None, + ): self.reset_pq() start = (self.current_pos.x, self.current_pos.y) start_idx = self._get_idx(*start) @@ -328,58 +332,13 @@ def a_star_cutoff(self, target_pos: Position, max_expansions: int | None = None) stopped_at = start best_fallback = self._pick_better_fallback(None, start, 0, target_pos) expansions = 0 - budget = self.max_a_star_expansions if max_expansions is None else max_expansions - self.push_pq(0, start) - - while self.open_pos and expansions < budget: - _, node = self.pop_pq() - expansions += 1 - - reached_target = node == (target_pos.x, target_pos.y) - goes_into_unknown = self.map_lut[node[0]][node[1]] == 0 - if reached_target or goes_into_unknown: - stopped_at = node - break - - elapsed_dist = check[self._get_idx(*node)] - best_fallback = self._pick_better_fallback(best_fallback, node, elapsed_dist, target_pos) - for dir_code, deltas in enumerate(DELTAS): - neighbor = (node[0] + deltas[0], node[1] + deltas[1]) - if not self.in_bounds(*neighbor) or self.map_lut[neighbor[0]][neighbor[1]] >= 4: - continue - - new_dist = elapsed_dist + 1 - neighbor_idx = self._get_idx(*neighbor) - neighbor_dist = check[neighbor_idx] - if neighbor_dist != -1 and new_dist >= neighbor_dist: - continue - - check[neighbor_idx] = new_dist - self.parent_dir[neighbor_idx] = REVERSE_DIR_CODES[dir_code] - self.parent_epoch[neighbor_idx] = search_epoch - rank = new_dist + self._heuristic(neighbor[0], neighbor[1], target_pos) - self.push_pq(rank, neighbor) - best_fallback = self._pick_better_fallback(best_fallback, neighbor, new_dist, target_pos) - else: - if best_fallback is not None: - stopped_at = best_fallback[3] - - self._rebuild_route(start_idx, self._get_idx(*stopped_at), search_epoch) - - def a_star_bounded_frontier(self, target_pos: Position, max_frontier_size: int | None = None): - self.reset_pq() - start = (self.current_pos.x, self.current_pos.y) - start_idx = self._get_idx(*start) - search_epoch = self._begin_search() - check = [-1] * (self.w * self.h) - check[start_idx] = 0 - stopped_at = start - best_fallback = self._pick_better_fallback(None, start, 0, target_pos) + expansion_budget = self.max_a_star_expansions if max_expansions is None else max_expansions frontier_limit = self.max_a_star_frontier if max_frontier_size is None else max_frontier_size self.push_pq(0, start) - while self.open_pos: + while self.open_pos and expansions < expansion_budget: _, node = self.pop_pq() + expansions += 1 reached_target = node == (target_pos.x, target_pos.y) goes_into_unknown = self.map_lut[node[0]][node[1]] == 0 @@ -406,7 +365,6 @@ def a_star_bounded_frontier(self, target_pos: Position, max_frontier_size: int | rank = new_dist + self._heuristic(neighbor[0], neighbor[1], target_pos) self.push_pq(rank, neighbor) best_fallback = self._pick_better_fallback(best_fallback, neighbor, new_dist, target_pos) - if self.inserted_items > frontier_limit: self.pop_worst_pq() else: @@ -415,12 +373,18 @@ def a_star_bounded_frontier(self, target_pos: Position, max_frontier_size: int | self._rebuild_route(start_idx, self._get_idx(*stopped_at), search_epoch) + def a_star_cutoff(self, target_pos: Position, max_expansions: int | None = None): + self.a_star_limited(target_pos, max_expansions=max_expansions) + + def a_star_bounded_frontier(self, target_pos: Position, max_frontier_size: int | None = None): + self.a_star_limited(target_pos, max_frontier_size=max_frontier_size) + def move(self,ct:Controller,target_pos:Position): self.current_pos = ct.get_position() current_idx = self._get_idx(self.current_pos.x, self.current_pos.y) if self.last_target != target_pos or not self._has_route_step(current_idx): self.last_target = target_pos - self.a_star_bounded_frontier(target_pos) + self.a_star_limited(target_pos) # self.a_star(target_pos) if not self._has_route_step(current_idx): return False @@ -430,7 +394,7 @@ def move(self,ct:Controller,target_pos:Position): can_move = ct.can_move(direction) or ct.can_build_road(next_pos) if not can_move: self._invalidate_route() - self.a_star_bounded_frontier(target_pos) + self.a_star_limited(target_pos) # self.a_star(target_pos) current_idx = self._get_idx(self.current_pos.x, self.current_pos.y) if not self._has_route_step(current_idx):