from __future__ import annotations import math from typing import Any import torch _ORIGINAL_GENERATE_MAPS: Any | None = None _MOTIF_SETTINGS: dict[str, float | int] = {} def configure_motif_mapgen(*, replace: bool = False, **kwargs: float | int) -> None: if replace: _MOTIF_SETTINGS.clear() _MOTIF_SETTINGS.update(kwargs) def _setting(cfg: Any, name: str, default: float | int) -> float | int: if name in _MOTIF_SETTINGS: return _MOTIF_SETTINGS[name] return getattr(cfg, name, default) def install_motif_mapgen() -> None: """Patch the fast Torch env to use compact rhythm/motif maps. This is intentionally isolated under experiments/ so the base runner stays usable. The patch affects only the current Python process. """ global _ORIGINAL_GENERATE_MAPS from env import FastFloatingBeaconEnv if _ORIGINAL_GENERATE_MAPS is None: _ORIGINAL_GENERATE_MAPS = FastFloatingBeaconEnv.generate_maps FastFloatingBeaconEnv.generate_maps = generate_motif_maps def restore_original_mapgen() -> None: global _ORIGINAL_GENERATE_MAPS if _ORIGINAL_GENERATE_MAPS is None: return from env import FastFloatingBeaconEnv FastFloatingBeaconEnv.generate_maps = _ORIGINAL_GENERATE_MAPS def generate_motif_maps(self: Any) -> None: route_len, p, n = self.route_len, self.p, self.n self.platforms.zero_() self.sizes.fill_(0.0) base_size = torch.tensor([self.platform_x * 1.05, self.platform_y * 1.06], device=self.device) min_scale = float(_setting(self.cfg, "map_platform_min_scale", 0.58)) max_scale = float(_setting(self.cfg, "map_platform_max_scale", 1.36)) tiny_fraction = float(_setting(self.cfg, "map_platform_tiny_fraction", 0.18)) large_fraction = float(_setting(self.cfg, "map_platform_large_fraction", 0.14)) aspect_jitter = float(_setting(self.cfg, "map_platform_aspect_jitter", 0.46)) scale = min_scale + (max_scale - min_scale) * self.rand(n, p) tiny = self.rand(n, p) < tiny_fraction large = self.rand(n, p) < large_fraction scale = torch.where(tiny, 0.46 + 0.18 * self.rand(n, p), scale) scale = torch.where(large, 1.22 + 0.24 * self.rand(n, p), scale) aspect = torch.exp((self.rand(n, p) - 0.5) * aspect_jitter).clamp(0.72, 1.38) self.sizes[:, :, 0] = (base_size[0] * scale * aspect).clamp(0.44, 1.70) self.sizes[:, :, 1] = (base_size[1] * scale / aspect).clamp(0.38, 1.48) # Keep spawn/goal readable while allowing hard tiny landings inside the route. self.sizes[:, 0] = torch.maximum(self.sizes[:, 0], base_size * 1.18) self.sizes[:, route_len - 1] = torch.maximum(self.sizes[:, route_len - 1], base_size * 1.08) route_tiny_fraction = float(_setting(self.cfg, "map_route_tiny_fraction", 0.0)) if route_len > 3 and route_tiny_fraction > 0.0: route_nodes = torch.arange(route_len, device=self.device)[None, :] eligible = (route_nodes > 0) & (route_nodes < route_len - 1) rhythm = ((route_nodes * 2 + self.env_i[:, None]) % 7) == 3 route_tiny = eligible & (rhythm | (self.rand(n, route_len) < route_tiny_fraction * 0.20)) tiny_min = max(0.34, float(_setting(self.cfg, "map_route_tiny_min_scale", 0.44))) tiny_max = max(tiny_min + 0.04, float(_setting(self.cfg, "map_route_tiny_max_scale", 0.68))) tiny_scale = tiny_min + (tiny_max - tiny_min) * self.rand(n, route_len) tiny_aspect = torch.exp((self.rand(n, route_len) - 0.5) * 0.34).clamp(0.78, 1.28) tiny_x = (base_size[0] * tiny_scale * tiny_aspect).clamp(0.38, 0.84) tiny_y = (base_size[1] * tiny_scale / tiny_aspect).clamp(0.34, 0.76) self.sizes[:, :route_len, 0] = torch.where(route_tiny, tiny_x, self.sizes[:, :route_len, 0]) self.sizes[:, :route_len, 1] = torch.where(route_tiny, tiny_y, self.sizes[:, :route_len, 1]) height_offset = max(0.0, float(getattr(self.cfg, "map_height_offset", 2.85))) vertical = max(0.35, float(getattr(self.cfg, "map_vertical_scale", 1.6))) max_h = max(height_offset + 0.7, float(getattr(self.cfg, "map_max_height", 7.2))) edge_gap = max(0.44, float(getattr(self.cfg, "map_min_edge_gap", 0.62))) edge_gap_min = max(edge_gap, float(_setting(self.cfg, "map_edge_gap_min", 0.86))) edge_gap_max = max(edge_gap_min + 0.08, float(_setting(self.cfg, "map_edge_gap_max", 1.42))) route_gap_scale = max(0.80, float(_setting(self.cfg, "map_route_gap_scale", 1.10))) step_len_base = max(1.56, self.platform_x + edge_gap_min) turn_scale = max(0.0, float(_setting(self.cfg, "map_scenic_turn_scale", 1.0))) vertical_step = max(0.12, float(_setting(self.cfg, "map_scenic_vertical_step", 0.44))) * vertical island_radius = max(1.18, float(_setting(self.cfg, "map_scenic_island_radius", 1.55))) cleanup_iters = int(getattr(self.cfg, "map_overlap_cleanup_iters", 16)) momentum_fraction = max(0.0, float(_setting(self.cfg, "map_momentum_jump_fraction", 0.0))) momentum_gap_bias = min(max(float(_setting(self.cfg, "map_momentum_gap_bias", 0.82)), 0.55), 0.96) redirect_period = max(0, int(_setting(self.cfg, "map_redirect_period", 0))) redirect_angle = max(0.0, float(_setting(self.cfg, "map_redirect_angle", 1.05))) goal_vertical_bias = max(0.0, float(_setting(self.cfg, "map_goal_vertical_bias", 0.0))) goal_vertical_bias_min = min(goal_vertical_bias, max(0.0, float(_setting(self.cfg, "map_goal_vertical_bias_min", 0.0)))) hairpin_period = max(0, int(_setting(self.cfg, "map_hairpin_period", 0))) hairpin_angle = max(0.0, float(_setting(self.cfg, "map_hairpin_angle", 0.0))) hairpin_depth = min(max(float(_setting(self.cfg, "map_hairpin_depth", 0.0)), 0.0), 1.5) unintuitive_depth = min(max(float(_setting(self.cfg, "map_unintuitive_depth", 0.0)), 0.0), 1.5) valley_period = max(0, int(_setting(self.cfg, "map_valley_period", 0))) valley_depth = min(max(float(_setting(self.cfg, "map_valley_depth", unintuitive_depth)), 0.0), 1.5) route_detour_period = max(0, int(_setting(self.cfg, "map_route_detour_period", 0))) route_detour_depth = min(max(float(_setting(self.cfg, "map_route_detour_depth", 0.0)), 0.0), 1.5) backtrack_period = max(0, int(_setting(self.cfg, "map_backtrack_period", 0))) backtrack_depth = min(max(float(_setting(self.cfg, "map_backtrack_depth", 0.0)), 0.0), 1.5) climb_direction = torch.where(self.rand(n) < 0.5, 1.0, -1.0) climb_total = climb_direction * (goal_vertical_bias_min + (goal_vertical_bias - goal_vertical_bias_min) * self.rand(n)) climb_per_step = climb_total / max(1, route_len - 1) route_xy = torch.zeros((n, route_len, 2), device=self.device) route_z = torch.zeros((n, route_len), device=self.device) xy = torch.zeros((n, 2), device=self.device) heading = self.rand(n) * math.tau phase = self.rand(n) * math.tau motif_seed = torch.randint(0, 7, (n,), device=self.device, generator=self.gen) motif_offset = self.env_i % 3 side_seed = torch.where((self.env_i % 2) == 0, 1.0, -1.0) z = torch.full((n,), height_offset + 0.28, device=self.device) + self.rand(n) * 0.30 z = (z + (-climb_total).clamp_min(0.0)).clamp(height_offset + 0.18, max_h - 0.20) route_z[:, 0] = z self.platforms[:, 0, 2] = z turn_tables = ( (0.14, 0.20, 0.14, 0.04), # crescent (0.62, 0.08, -0.44, -0.18), # dogleg out/back (0.04, -0.10, 0.12, -0.04), # vertical terrace (0.86, 0.24, -0.62, -0.30), # compact loopback shelf (0.38, -0.46, -0.28, 0.42), # S curve (0.18, 0.04, -0.12, 0.08), # ladder (0.50, -0.16, -0.38, 0.24), # fork/merge bend ) step_tables = ( (1.02, 1.02, 0.96, 1.00), (0.88, 1.02, 0.88, 1.04), (0.92, 0.88, 0.92, 1.00), (0.72, 0.86, 0.76, 0.98), (0.92, 0.90, 0.94, 1.02), (0.84, 0.82, 0.90, 0.98), (0.90, 0.86, 0.92, 1.04), ) dz_tables = ( (0.42, 0.35, -0.28, -0.24), (0.18, 0.12, 0.22, -0.18), (0.46, 0.38, 0.20, -0.24), (0.34, -0.18, 0.44, -0.20), (0.24, 0.34, -0.26, -0.12), (0.52, 0.38, 0.18, -0.16), (0.16, 0.36, -0.10, 0.12), ) for layer in range(1, route_len): slot = (layer - 1) % 4 block = (layer - 1) // 4 motif = (motif_seed + int(block) + motif_offset) % 7 kind = [motif == i for i in range(7)] side = torch.where(((self.env_i + layer) % 2) == 0, 1.0, -1.0) * side_seed turn_delta = torch.sin(phase + float(layer) * 0.73) * 0.04 step_mult = torch.ones(n, device=self.device) dz = torch.sin(phase + float(layer) * 1.13) * vertical_step * 0.10 for i in range(7): active = kind[i].float() turn_delta = turn_delta + active * side * float(turn_tables[i][slot]) step_mult = step_mult + active * (float(step_tables[i][slot]) - 1.0) dz = dz + active * float(dz_tables[i][slot]) * vertical_step dz = dz + climb_per_step if redirect_period > 0 and redirect_angle > 0.0: redirect_slot = (layer - 2) % redirect_period if redirect_slot == 0: turn_delta = turn_delta + side * redirect_angle step_mult = step_mult * 0.88 elif redirect_slot == 1: turn_delta = turn_delta - side * redirect_angle * 0.62 elif redirect_slot == 2: turn_delta = turn_delta - side * redirect_angle * 0.30 if hairpin_period > 0 and hairpin_angle > 0.0 and hairpin_depth > 0.0: hairpin_slot = (layer - 2) % hairpin_period fold_side = torch.where(((self.env_i // max(1, hairpin_period) + layer) % 2) == 0, 1.0, -1.0) * side_seed if hairpin_slot == 0: turn_delta = turn_delta + fold_side * hairpin_angle step_mult = step_mult * (0.84 - 0.08 * hairpin_depth) dz = dz + vertical_step * 0.14 * hairpin_depth elif hairpin_slot == 1: turn_delta = turn_delta + fold_side * hairpin_angle * 0.42 step_mult = step_mult * (0.76 - 0.06 * hairpin_depth) dz = dz - vertical_step * 0.10 * hairpin_depth elif hairpin_slot == 2: turn_delta = turn_delta - fold_side * hairpin_angle * 0.78 step_mult = step_mult * (0.86 - 0.05 * hairpin_depth) dz = dz + vertical_step * 0.18 * hairpin_depth elif hairpin_slot == 3: turn_delta = turn_delta - fold_side * hairpin_angle * 0.58 step_mult = step_mult * 0.92 dz = dz - vertical_step * 0.12 * hairpin_depth if valley_period > 0 and valley_depth > 0.0: valley_slot = (layer - 2) % valley_period detour_side = torch.where(((self.env_i + layer // max(1, valley_period)) % 2) == 0, 1.0, -1.0) * side_seed if valley_slot == 0: # Go visibly sideways/down before the climb; this creates a # clean but human-unintuitive "wrong way first" segment. turn_delta = turn_delta + detour_side * (0.42 + 0.22 * unintuitive_depth) step_mult = step_mult * (0.88 - 0.04 * valley_depth) dz = dz - vertical_step * (0.42 + 0.18 * valley_depth) elif valley_slot == 1: turn_delta = turn_delta + detour_side * (0.24 + 0.14 * unintuitive_depth) step_mult = step_mult * 0.84 dz = dz - vertical_step * (0.26 + 0.16 * valley_depth) elif valley_slot == 2: turn_delta = turn_delta - detour_side * 0.18 step_mult = step_mult * 0.78 dz = dz + vertical_step * (0.10 + 0.14 * valley_depth) elif valley_slot == 3: turn_delta = turn_delta - detour_side * (0.38 + 0.18 * unintuitive_depth) step_mult = step_mult * 0.88 dz = dz + vertical_step * (0.38 + 0.18 * valley_depth) elif valley_slot == 4: turn_delta = turn_delta - detour_side * (0.18 + 0.12 * unintuitive_depth) step_mult = step_mult * 0.94 dz = dz + vertical_step * (0.28 + 0.14 * valley_depth) if route_detour_period > 0 and route_detour_depth > 0.0: detour_slot = (layer - 2) % route_detour_period detour_side = torch.where(((self.env_i // max(1, route_detour_period) + layer) % 2) == 0, 1.0, -1.0) * side_seed if detour_slot == 0: # The real route intentionally refuses the obvious goalward # step. Trap extras fill that obvious slot instead. turn_delta = turn_delta + detour_side * (0.72 + 0.30 * route_detour_depth) step_mult = step_mult * (0.86 - 0.04 * route_detour_depth) dz = dz - vertical_step * (0.22 + 0.14 * route_detour_depth) elif detour_slot == 1: turn_delta = turn_delta + detour_side * (0.34 + 0.16 * route_detour_depth) step_mult = step_mult * 0.86 dz = dz + vertical_step * (0.08 + 0.08 * route_detour_depth) elif detour_slot == 2: turn_delta = turn_delta - detour_side * (0.64 + 0.22 * route_detour_depth) step_mult = step_mult * 0.90 dz = dz + vertical_step * (0.24 + 0.12 * route_detour_depth) if backtrack_period > 0 and backtrack_depth > 0.0: back_slot = (layer - 2) % backtrack_period back_side = torch.where(((self.env_i // max(1, backtrack_period) + layer) % 2) == 0, 1.0, -1.0) * side_seed if back_slot == 0: # A deliberate "wrong-way first" jump: reverse relative to # the previous travel direction, but offset to a new shelf. turn_delta = turn_delta + back_side * (2.02 + 0.28 * backtrack_depth) step_mult = step_mult * (0.88 + 0.06 * backtrack_depth) dz = dz - vertical_step * (0.16 + 0.10 * backtrack_depth) elif back_slot == 1: turn_delta = turn_delta + back_side * (0.68 + 0.16 * backtrack_depth) step_mult = step_mult * (0.92 + 0.04 * backtrack_depth) dz = dz + vertical_step * (0.10 + 0.10 * backtrack_depth) elif back_slot == 2: turn_delta = turn_delta - back_side * (1.54 + 0.20 * backtrack_depth) step_mult = step_mult * (0.94 + 0.03 * backtrack_depth) dz = dz + vertical_step * (0.26 + 0.12 * backtrack_depth) elif back_slot == 3: turn_delta = turn_delta - back_side * (0.54 + 0.18 * backtrack_depth) step_mult = step_mult * 0.88 dz = dz - vertical_step * 0.08 * backtrack_depth heading = heading + turn_delta * turn_scale direction = torch.stack((torch.cos(heading), torch.sin(heading)), dim=1) support = ( (direction.abs() * self.sizes[:, layer - 1]).sum(dim=1) * 0.5 + (direction.abs() * self.sizes[:, layer]).sum(dim=1) * 0.5 ) target_edge_gap = ( edge_gap_min + (edge_gap_max - edge_gap_min) * self.rand(n) ) * step_mult.clamp(0.72, 1.18) * route_gap_scale climb_trade = (climb_per_step.clamp_min(0.0) / 0.42).clamp(0.0, 0.42) target_edge_gap = target_edge_gap * (1.0 - climb_trade) desired_center = support + target_edge_gap landing_pad = self.sizes[:, layer].amin(dim=1) * 0.34 + self.sizes[:, layer - 1].amin(dim=1) * 0.16 + float(self.radius) * 0.75 horizontal_speed = float(self.max_speed) * float(self.sprint_speed_mult) * float(getattr(self.cfg, "map_reach_scale", 0.82)) needed_flight = ((desired_center - landing_pad + 0.44) / max(horizontal_speed, 1e-6)).clamp_min(0.05) max_dz_for_desired_gap = float(self.jump_velocity) * needed_flight - 0.5 * float(self.gravity) * needed_flight.square() dz = (dz + (self.rand(n) - 0.5) * vertical_step * 0.20).clamp(-0.72, 0.62) dz = torch.minimum(dz, max_dz_for_desired_gap - 0.06) next_z = (z + dz).clamp(height_offset + 0.18, max_h) dz_actual = next_z - z jump_v = float(self.jump_velocity) gravity = float(self.gravity) disc = jump_v * jump_v - 2.0 * gravity * dz_actual flight = (jump_v + torch.sqrt(disc.clamp_min(0.0))) / gravity max_center = float(self.max_speed) * float(self.sprint_speed_mult) * flight * float(getattr(self.cfg, "map_reach_scale", 0.82)) + landing_pad - 0.42 min_center = support + edge_gap_min * 0.82 safe_min = torch.minimum(min_center, max_center).clamp_min(support + edge_gap_min * 0.35) center_dist = torch.minimum(desired_center, max_center).clamp_min(safe_min) if momentum_fraction > 0.0: rhythm = ((self.env_i + layer * 3) % 8) == 2 momentum = rhythm | (self.rand(n) < momentum_fraction * 0.18) hard_center = safe_min + (max_center - safe_min).clamp_min(0.0) * momentum_gap_bias valid_hard = max_center > safe_min + 0.35 center_dist = torch.where(momentum & valid_hard, torch.maximum(center_dist, hard_center), center_dist) step = direction * center_dist[:, None] xy = xy + step z = next_z route_xy[:, layer] = xy route_z[:, layer] = z self.platforms[:, layer, :2] = xy self.platforms[:, layer, 2] = z if layer >= 2 and float(_setting(self.cfg, "map_repair_local_lookback", 0.0)) > 0.0: self.repair_route_spacing(layer, min_center=max(1.38, step_len_base * 0.72)) route_xy[:, layer] = self.platforms[:, layer, :2] self.platforms[:, :route_len, :2] = route_xy self.platforms[:, :route_len, 2] = route_z goal_xy_min = max(0.0, float(_setting(self.cfg, "map_goal_xy_min", 0.0))) goal_xy_per_jump_min = max(0.0, float(_setting(self.cfg, "map_goal_xy_per_jump_min", 0.0))) goal_xy_min = max(goal_xy_min, goal_xy_per_jump_min * float(max(1, route_len - 1))) if goal_xy_min > 0.0 and route_len > 2: start_xy = route_xy[:, 0] goal_vec = route_xy[:, -1] - start_xy goal_dist = torch.linalg.norm(goal_vec, dim=1) first_vec = route_xy[:, 1] - start_xy first_dir = first_vec / torch.linalg.norm(first_vec, dim=1, keepdim=True).clamp_min(1e-4) goal_dir = torch.where(goal_dist[:, None] > 1e-4, goal_vec / goal_dist[:, None].clamp_min(1e-4), first_dir) stretch = (goal_xy_min - goal_dist).clamp_min(0.0) ramp = torch.linspace(0.0, 1.0, route_len, device=self.device)[None, :, None] route_xy = route_xy + goal_dir[:, None, :] * stretch[:, None, None] * ramp self.platforms[:, :route_len, :2] = route_xy route_delta_xy = route_xy[:, 1:, :2] - route_xy[:, :-1, :2] route_turn_strength = torch.zeros((n, route_len), device=self.device) if route_len > 3: prev_vec = route_delta_xy[:, :-1] next_vec = route_delta_xy[:, 1:] prev_len = torch.linalg.norm(prev_vec, dim=2).clamp_min(1e-4) next_len = torch.linalg.norm(next_vec, dim=2).clamp_min(1e-4) prev_n = prev_vec / prev_len[:, :, None] next_n = next_vec / next_len[:, :, None] cross = prev_n[..., 0] * next_n[..., 1] - prev_n[..., 1] * next_n[..., 0] dot = (prev_n * next_n).sum(dim=2).clamp(-1.0, 1.0) corner = torch.atan2(cross.abs(), dot) route_turn_strength[:, 1:-1] = corner route_turn_strength = route_turn_strength.clamp_min(0.0) extra_reach_exclude: torch.Tensor | None = None if p > route_len: extras = p - route_len unlock_lever_mask: torch.Tensor | None = None unlock_target_i: torch.Tensor | None = None unlock_gate_count = 0 inner_layers = max(1, route_len - 2) offset_seed = torch.randint(0, inner_layers, (n, 1), device=self.device, generator=self.gen) extra_i = torch.arange(extras, device=self.device)[None, :] even_extra = (extra_i % 2) == 0 odd_extra = ~even_extra anchors = 1 + ((extra_i * 2 + offset_seed) % inner_layers) unlock_enabled = bool(getattr(self.cfg, "unlock_platforms", False)) and route_len > 5 if unlock_enabled: gate_count = min(max(1, int(getattr(self.cfg, "unlock_gate_count", 1))), extras) unlock_gate_count = gate_count lock_span = min(max(1, int(getattr(self.cfg, "unlock_lock_span", 2))), max(1, route_len - 4)) anchor_span = max(1, route_len - lock_span - 3) gate_ids = torch.arange(gate_count, device=self.device)[None, :] gate_anchors = 2 + torch.remainder(self.env_i[:, None] * (5 + 2 * gate_ids) + gate_ids * 3, anchor_span) anchors[:, :gate_count] = gate_anchors unlock_lever_mask = torch.zeros((n, extras), dtype=torch.bool, device=self.device) unlock_lever_mask[:, :gate_count] = True unlock_target_i = (anchors + 1).clamp_max(route_len - 2) prev_i = (anchors - 1).clamp_min(0) next_i = (anchors + 1).clamp_max(route_len - 1) far_i = (anchors + 2).clamp_max(route_len - 1) braid_span = max(2, int(_setting(self.cfg, "map_braid_bridge_span", 3))) braid_target_i = (anchors + braid_span).clamp_max(route_len - 1) def gather_xy(index: torch.Tensor) -> torch.Tensor: return route_xy.gather(1, index[:, :, None].expand(-1, -1, 2)) anchor_xy = gather_xy(anchors) prev_xy = gather_xy(prev_i) next_xy = gather_xy(next_i) far_xy = gather_xy(far_i) braid_target_xy = gather_xy(braid_target_i) tangent = next_xy - prev_xy tangent = tangent / torch.linalg.norm(tangent, dim=2, keepdim=True).clamp_min(1e-4) perp = torch.stack((-tangent[..., 1], tangent[..., 0]), dim=2) side = torch.where(((extra_i + self.env_i[:, None]) % 2) == 0, 1.0, -1.0) ring = island_radius * (1.12 + 0.48 * self.rand(n, extras)) ring = torch.where((extra_i % 3) == 0, ring + 0.28, ring) forward = (((extra_i % 3).float() - 1.0) * 0.28 * step_len_base)[:, :, None] island_xy = anchor_xy + perp * side[:, :, None] * ring[:, :, None] + tangent * forward bridge_xy = 0.50 * anchor_xy + 0.50 * far_xy + perp * side[:, :, None] * (1.06 + 0.26 * self.rand(n, extras))[:, :, None] terrace_xy = anchor_xy + tangent * (0.66 + 0.22 * self.rand(n, extras))[:, :, None] + perp * side[:, :, None] * (1.22 + 0.34 * self.rand(n, extras))[:, :, None] bridge_mask = ((extra_i + offset_seed) % 4) == 0 terrace_mask = ((extra_i + offset_seed) % 5) == 0 direct_decoy_fraction = max(0.0, float(_setting(self.cfg, "map_direct_decoy_fraction", 0.0))) direct_decoy = self.rand(n, extras) < direct_decoy_fraction cluster_extra_fraction = max(0.0, float(_setting(self.cfg, "map_cluster_extra_fraction", 0.0))) cluster_extra = self.rand(n, extras) < cluster_extra_fraction decoy_corridor_fraction = max(0.0, float(_setting(self.cfg, "map_decoy_corridor_fraction", 0.0))) decoy_corridor = self.rand(n, extras) < decoy_corridor_fraction loopback_anchor_fraction = max(0.0, min(float(_setting(self.cfg, "map_loopback_anchor_fraction", 0.0)), 1.0)) loopback_anchor_period = max(0, int(_setting(self.cfg, "map_loopback_anchor_period", 0))) loopback_turn_gate = max(0.0, float(_setting(self.cfg, "map_loopback_turn_gate", 0.42))) mid_anchor = (anchors > 1) & (anchors < route_len - 2) route_turn_anchor = route_turn_strength.gather(1, anchors) route_choke_anchor = mid_anchor & (route_turn_anchor > loopback_turn_gate) if loopback_anchor_period > 0: route_choke_anchor = route_choke_anchor & (((anchors - 1) % loopback_anchor_period) == 0) mid_even = mid_anchor & even_extra # Ensure at least a controlled number of loopback entry points, preferring # high turn-rate choke points but falling back to highest-turn mids if # the route is too straight. mid_even_count = mid_even.sum(dim=1).clamp_min(1) raw_target = (mid_even_count.to(self.device).to(torch.float32) * float(loopback_anchor_fraction)).floor().to(torch.int64) if route_len <= 5 or float(loopback_anchor_fraction) <= 0.0: target_loopbacks = torch.zeros((n,), device=self.device, dtype=torch.int64) else: target_loopbacks = raw_target.clamp_min(0).clamp_max(mid_even_count) k_max = int(target_loopbacks.max().item()) if int(extras) > 0 else 0 if k_max > 0: choke_score = route_turn_anchor + 0.05 * self.rand(n, extras) fallback_score = route_turn_anchor - 1.0 - 0.05 * self.rand(n, extras) candidate_score = torch.where(route_choke_anchor, choke_score, fallback_score) candidate_score = torch.where(mid_even, candidate_score, torch.full_like(candidate_score, -1.0e7)) top_vals, top_idx = torch.topk(candidate_score, k=k_max, dim=1) keep_by_rank = torch.arange(k_max, device=self.device)[None, :] < target_loopbacks[:, None] forced_loopback_entry = torch.zeros((n, extras), device=self.device, dtype=torch.bool) forced_loopback_entry = forced_loopback_entry.scatter_(1, top_idx, keep_by_rank) else: forced_loopback_entry = torch.zeros((n, extras), device=self.device, dtype=torch.bool) forced_loopback_entry = (anchors % 2 == 0) & forced_loopback_entry forced_loopback_deadend = torch.roll(forced_loopback_entry, shifts=1, dims=1) & odd_extra false_summit_fraction = max(0.0, float(_setting(self.cfg, "map_false_summit_fraction", 0.0))) late_anchor = anchors >= max(2, route_len - 4) goal_fan_slots = extra_i >= max(0, extras - max(2, extras // 3)) false_summit = (late_anchor | goal_fan_slots) & (self.rand(n, extras) < false_summit_fraction) fan_width = max(1, min(4, route_len - 1)) fan_target_i = (route_len - 1 - ((extra_i + (self.env_i[:, None] % fan_width)) % fan_width)).clamp_min(1) fan_target_xy = gather_xy(fan_target_i) false_branch_fraction = max(0.0, float(_setting(self.cfg, "map_false_branch_fraction", 0.0))) false_branch = mid_anchor & (self.rand(n, extras) < false_branch_fraction) false_finish_fraction = max(0.0, float(_setting(self.cfg, "map_false_finish_fraction", 0.0))) late_setup_anchor = (anchors >= max(2, route_len - 7)) & (anchors < route_len - 2) false_finish_entry = late_setup_anchor & even_extra & (self.rand(n, extras) < false_finish_fraction) false_finish_goal = torch.roll(false_finish_entry, shifts=1, dims=1) & odd_extra extra_reach_exclude = false_finish_goal braid_bridge_fraction = max(0.0, float(_setting(self.cfg, "map_braid_bridge_fraction", 0.0))) braid_source_ok = (anchors > 0) & (anchors < route_len - max(2, braid_span)) braid_start = braid_source_ok & even_extra & (self.rand(n, extras) < braid_bridge_fraction) braid_end = torch.roll(braid_start, shifts=1, dims=1) & odd_extra greedy_trap_fraction = max(0.0, float(_setting(self.cfg, "map_greedy_trap_fraction", 0.0))) trap_entry = mid_anchor & even_extra & ~braid_start & (self.rand(n, extras) < greedy_trap_fraction) trap_deadend = torch.roll(trap_entry, shifts=1, dims=1) & odd_extra false_branch = false_branch | trap_entry false_branch = false_branch | forced_loopback_entry trap_jump_fraction = max(0.0, float(_setting(self.cfg, "map_trap_jump_fraction", 0.0))) trap_jump = mid_anchor & ~braid_start & ~braid_end & (self.rand(n, extras) < trap_jump_fraction) trap_jump = trap_jump | false_finish_entry trap_high_fraction = max(0.0, min(float(_setting(self.cfg, "map_trap_jump_high_fraction", 0.0)), 1.0)) trap_high = trap_jump & (self.rand(n, extras) < trap_high_fraction) trap_low = trap_jump & ~trap_high extra_reach_exclude = extra_reach_exclude | trap_high | forced_loopback_entry | forced_loopback_deadend if unlock_lever_mask is not None: direct_decoy = direct_decoy & ~unlock_lever_mask cluster_extra = cluster_extra & ~unlock_lever_mask decoy_corridor = decoy_corridor & ~unlock_lever_mask false_summit = false_summit & ~unlock_lever_mask false_branch = false_branch & ~unlock_lever_mask false_finish_entry = false_finish_entry & ~unlock_lever_mask false_finish_goal = false_finish_goal & ~unlock_lever_mask extra_reach_exclude = false_finish_goal braid_start = braid_start & ~unlock_lever_mask trap_entry = trap_entry & ~unlock_lever_mask braid_end = torch.roll(braid_start, shifts=1, dims=1) & odd_extra & ~unlock_lever_mask trap_deadend = torch.roll(trap_entry, shifts=1, dims=1) & odd_extra & ~unlock_lever_mask trap_jump = trap_jump & ~unlock_lever_mask trap_high = trap_high & ~unlock_lever_mask trap_low = trap_jump & ~trap_high extra_reach_exclude = false_finish_goal | trap_high forced_loopback_entry = forced_loopback_entry & ~unlock_lever_mask forced_loopback_deadend = forced_loopback_deadend & ~unlock_lever_mask goal_xy = route_xy[:, -1:, :] goal_dir = goal_xy - anchor_xy goal_dir = goal_dir / torch.linalg.norm(goal_dir, dim=2, keepdim=True).clamp_min(1e-4) direct_xy = anchor_xy + goal_dir * (step_len_base * (1.20 + 0.75 * self.rand(n, extras)))[:, :, None] corridor_lane = ((extra_i + offset_seed) % 3).float() corridor_xy = ( anchor_xy + goal_dir * (step_len_base * (0.72 + 0.42 * corridor_lane + 0.18 * self.rand(n, extras)))[:, :, None] + perp * side[:, :, None] * (0.18 + 0.28 * self.rand(n, extras))[:, :, None] ) cluster_lane = ((extra_i + self.env_i[:, None]) % 5).float() cluster_xy = ( anchor_xy + perp * side[:, :, None] * (island_radius * (0.78 + 0.18 * cluster_lane + 0.28 * self.rand(n, extras)))[:, :, None] + tangent * ((cluster_lane - 2.0) * 0.28 * step_len_base)[:, :, None] ) extra_xy = torch.where(bridge_mask[:, :, None], bridge_xy, island_xy) extra_xy = torch.where(terrace_mask[:, :, None], terrace_xy, extra_xy) extra_xy = torch.where(cluster_extra[:, :, None], cluster_xy, extra_xy) extra_xy = torch.where(decoy_corridor[:, :, None], corridor_xy, extra_xy) extra_xy = torch.where(direct_decoy[:, :, None], direct_xy, extra_xy) false_summit_xy = ( 0.62 * fan_target_xy + 0.38 * anchor_xy + perp * side[:, :, None] * (0.48 + 0.24 * self.rand(n, extras))[:, :, None] ) false_branch_xy = ( anchor_xy + goal_dir * (step_len_base * (0.82 + 0.24 * self.rand(n, extras)))[:, :, None] + perp * side[:, :, None] * (0.36 + 0.22 * self.rand(n, extras))[:, :, None] ) trap_entry_xy = ( anchor_xy + goal_dir * (step_len_base * (0.92 + 0.18 * self.rand(n, extras)))[:, :, None] + perp * side[:, :, None] * (0.16 + 0.16 * self.rand(n, extras))[:, :, None] ) forced_loopback_entry_xy = ( anchor_xy + goal_dir * (step_len_base * (0.82 + 0.12 * self.rand(n, extras)))[:, :, None] + perp * side[:, :, None] * (0.20 + 0.24 * self.rand(n, extras))[:, :, None] ) trap_deadend_xy = ( torch.roll(trap_entry_xy, shifts=1, dims=1) + torch.roll(goal_dir, shifts=1, dims=1) * (step_len_base * (0.82 + 0.16 * self.rand(n, extras)))[:, :, None] + torch.roll(perp * side[:, :, None], shifts=1, dims=1) * (0.18 + 0.18 * self.rand(n, extras))[:, :, None] ) forced_loopback_deadend_xy = torch.roll(forced_loopback_entry_xy, shifts=1, dims=1) trap_disguise = min(max(float(_setting(self.cfg, "map_trap_jump_disguise", 0.0)), 0.0), 1.5) lure_forward = step_len_base * (0.82 + 0.34 * self.rand(n, extras)) lure_side = (0.05 + (0.16 + 0.26 * trap_disguise) * self.rand(n, extras)) high_forward = step_len_base * (0.58 + 0.24 * self.rand(n, extras)) high_side = (0.18 + (0.30 + 0.24 * trap_disguise) * self.rand(n, extras)) lateral_lure_xy = ( anchor_xy + tangent * (step_len_base * (0.36 + 0.26 * self.rand(n, extras)))[:, :, None] + perp * side[:, :, None] * (0.74 + 0.34 * self.rand(n, extras) + 0.22 * trap_disguise)[:, :, None] ) direct_lure_xy = ( anchor_xy + goal_dir * lure_forward[:, :, None] + perp * side[:, :, None] * lure_side[:, :, None] ) high_lure_xy = ( anchor_xy + goal_dir * high_forward[:, :, None] + perp * side[:, :, None] * high_side[:, :, None] ) disguised_trap = self.rand(n, extras) < min(0.70, 0.34 * trap_disguise) trap_jump_xy = torch.where(disguised_trap[:, :, None], lateral_lure_xy, direct_lure_xy) trap_jump_xy = torch.where(trap_high[:, :, None], high_lure_xy, trap_jump_xy) false_finish_entry_xy = ( anchor_xy + goal_dir * (step_len_base * (0.86 + 0.18 * self.rand(n, extras)))[:, :, None] + perp * side[:, :, None] * (0.12 + 0.16 * self.rand(n, extras))[:, :, None] ) finish_dir = torch.roll(goal_dir, shifts=1, dims=1) finish_perp = torch.roll(perp, shifts=1, dims=1) finish_side = torch.roll(side, shifts=1, dims=1) false_finish_goal_xy = ( goal_xy - finish_dir * (step_len_base * (0.84 + 0.18 * self.rand(n, extras)))[:, :, None] + finish_perp * finish_side[:, :, None] * (0.30 + 0.30 * self.rand(n, extras))[:, :, None] ) bridge_vec = braid_target_xy - anchor_xy bridge_dir = bridge_vec / torch.linalg.norm(bridge_vec, dim=2, keepdim=True).clamp_min(1e-4) bridge_perp = torch.stack((-bridge_dir[..., 1], bridge_dir[..., 0]), dim=2) bridge_side = torch.where(((self.env_i[:, None] + anchors) % 2) == 0, 1.0, -1.0) bridge_bulge = island_radius * (0.54 + 0.18 * self.rand(n, extras)) braid_start_xy = ( anchor_xy * (1.0 - 0.34) + braid_target_xy * 0.34 + bridge_perp * bridge_side[:, :, None] * bridge_bulge[:, :, None] ) braid_end_xy = ( torch.roll(anchor_xy, shifts=1, dims=1) * (1.0 - 0.68) + torch.roll(braid_target_xy, shifts=1, dims=1) * 0.68 + torch.roll(bridge_perp * bridge_side[:, :, None] * bridge_bulge[:, :, None] * 0.78, shifts=1, dims=1) ) extra_xy = torch.where(false_branch[:, :, None], false_branch_xy, extra_xy) extra_xy = torch.where(trap_entry[:, :, None], trap_entry_xy, extra_xy) extra_xy = torch.where(trap_deadend[:, :, None], trap_deadend_xy, extra_xy) extra_xy = torch.where(forced_loopback_entry[:, :, None], forced_loopback_entry_xy, extra_xy) extra_xy = torch.where(forced_loopback_deadend[:, :, None], forced_loopback_deadend_xy, extra_xy) extra_xy = torch.where(braid_start[:, :, None], braid_start_xy, extra_xy) extra_xy = torch.where(braid_end[:, :, None], braid_end_xy, extra_xy) extra_xy = torch.where(false_summit[:, :, None], false_summit_xy, extra_xy) extra_xy = torch.where(trap_jump[:, :, None], trap_jump_xy, extra_xy) extra_xy = torch.where(false_finish_entry[:, :, None], false_finish_entry_xy, extra_xy) extra_xy = torch.where(false_finish_goal[:, :, None], false_finish_goal_xy, extra_xy) extra_xy = clamp_xy_to_route_span( route_xy, extra_xy, min_frac=float(_setting(self.cfg, "map_extra_span_min_frac", 0.05)), max_frac=float(_setting(self.cfg, "map_extra_span_max_frac", 0.90)), end_margin=float(_setting(self.cfg, "map_extra_span_end_margin", 1.10)), ) if unlock_lever_mask is not None: unlock_dir = next_xy - anchor_xy unlock_dir = unlock_dir / torch.linalg.norm(unlock_dir, dim=2, keepdim=True).clamp_min(1e-4) unlock_perp = torch.stack((-unlock_dir[..., 1], unlock_dir[..., 0]), dim=2) unlock_side = torch.where(((self.env_i[:, None] + anchors) % 2) == 0, 1.0, -1.0) unlock_xy = ( anchor_xy + unlock_dir * (step_len_base * (0.40 + 0.14 * self.rand(n, extras)))[:, :, None] + unlock_perp * unlock_side[:, :, None] * (island_radius * (0.92 + 0.18 * self.rand(n, extras)))[:, :, None] ) extra_xy = torch.where(unlock_lever_mask[:, :, None], unlock_xy, extra_xy) self.platforms[:, route_len:, :2] = extra_xy anchor_z = route_z.gather(1, anchors) braid_target_z = route_z.gather(1, braid_target_i) fan_target_z = route_z.gather(1, fan_target_i) legal_z = anchor_z + (self.rand(n, extras) - 0.5) * vertical_step * 0.90 legal_z = torch.where(terrace_mask, anchor_z + vertical_step * (0.38 + 0.20 * self.rand(n, extras)), legal_z) cluster_z = anchor_z + (self.rand(n, extras) - 0.5) * vertical_step * (1.15 + 0.35 * hairpin_depth) legal_z = torch.where(cluster_extra, cluster_z, legal_z) branch_z = anchor_z + (self.rand(n, extras) - 0.35) * vertical_step * 0.34 legal_z = torch.where(false_branch, branch_z, legal_z) summit_legal_z = anchor_z * 0.38 + fan_target_z * 0.62 + (self.rand(n, extras) - 0.45) * vertical_step * 0.20 legal_z = torch.where(false_summit, summit_legal_z, legal_z) trap_deadend_z = torch.roll(branch_z, shifts=1, dims=1) + (self.rand(n, extras) - 0.45) * vertical_step * 0.28 legal_z = torch.where(trap_deadend, trap_deadend_z, legal_z) braid_start_z = anchor_z * (1.0 - 0.34) + braid_target_z * 0.34 - vertical_step * (0.10 + 0.10 * self.rand(n, extras)) braid_end_z = ( torch.roll(anchor_z, shifts=1, dims=1) * (1.0 - 0.68) + torch.roll(braid_target_z, shifts=1, dims=1) * 0.68 + vertical_step * (0.04 + 0.10 * self.rand(n, extras)) ) legal_z = torch.where(braid_start, braid_start_z, legal_z) legal_z = torch.where(braid_end, braid_end_z, legal_z) goal_z = route_z[:, -1:] false_finish_goal_z = goal_z - vertical_step * (0.08 + 0.10 * self.rand(n, extras)) legal_z = torch.where(false_finish_goal, false_finish_goal_z, legal_z) trap_drop_max = max(0.55, float(_setting(self.cfg, "map_trap_jump_drop", 2.2))) trap_drop_min = min(trap_drop_max - 0.04, max(0.12, float(_setting(self.cfg, "map_trap_jump_min_drop", 0.42)))) trap_edge_margin = max(0.0, float(_setting(self.cfg, "map_trap_jump_edge_margin", 0.10))) trap_deep_z = anchor_z - trap_drop_max trap_shallow_z = anchor_z - trap_drop_min source_size_for_trap = self.sizes[:, :route_len].gather(1, anchors[:, :, None].expand(-1, -1, 2)) trap_size = self.sizes[:, route_len:] anchor_pos_for_trap = torch.cat((anchor_xy, anchor_z[:, :, None]), dim=2) lo = trap_deep_z hi = trap_shallow_z for _ in range(7): mid = (lo + hi) * 0.5 trap_pos = torch.cat((trap_jump_xy, mid[:, :, None]), dim=2) return_margin = pair_reach_margin(self, trap_pos, anchor_pos_for_trap, trap_size, source_size_for_trap) too_recoverable = return_margin > -trap_edge_margin hi = torch.where(too_recoverable, mid, hi) lo = torch.where(too_recoverable, lo, mid) trap_jump_z = lo + (self.rand(n, extras) - 0.5) * 0.04 high_min_up = max(0.08, float(_setting(self.cfg, "map_trap_jump_high_min_up", 0.34))) high_max_up = max(high_min_up + 0.08, float(_setting(self.cfg, "map_trap_jump_high_max_up", 1.35))) high_lo = anchor_z + high_min_up high_hi = torch.minimum(anchor_z + high_max_up, torch.full_like(anchor_z, max_h)) for _ in range(8): mid = (high_lo + high_hi) * 0.5 trap_pos = torch.cat((trap_jump_xy, mid[:, :, None]), dim=2) approach_margin = pair_reach_margin(self, anchor_pos_for_trap, trap_pos, source_size_for_trap, trap_size) too_reachable = approach_margin > -trap_edge_margin high_lo = torch.where(too_reachable, mid, high_lo) high_hi = torch.where(too_reachable, high_hi, mid) trap_high_z = high_hi + (0.03 + 0.05 * self.rand(n, extras)) trap_high_z = torch.minimum(trap_high_z, torch.full_like(trap_high_z, max_h)) trap_jump_z = torch.where(trap_high, trap_high_z, trap_jump_z) legal_z = torch.where(trap_jump, trap_jump_z, legal_z) if unlock_lever_mask is not None: unlock_next_z = route_z.gather(1, (anchors + 1).clamp_max(route_len - 1)) unlock_z = anchor_z * 0.56 + unlock_next_z * 0.44 + (self.rand(n, extras) - 0.5) * vertical_step * 0.14 legal_z = torch.where(unlock_lever_mask, unlock_z, legal_z) self.sizes[:, route_len : route_len + unlock_gate_count] = torch.maximum( self.sizes[:, route_len : route_len + unlock_gate_count], base_size * 1.02, ) decoy = self.rand(n, extras) < float(getattr(self.cfg, "map_illegal_decoy_fraction", 0.0)) if unlock_lever_mask is not None: decoy = decoy & ~unlock_lever_mask # Semantic decoys must be real affordances. They can be wrong choices, # but not random impossible blocks that visibly do nothing. illegal_z = anchor_z + float(getattr(self.cfg, "map_illegal_decoy_min_up", 0.9)) + self.rand(n, extras) * ( float(getattr(self.cfg, "map_illegal_decoy_max_up", 1.45)) - float(getattr(self.cfg, "map_illegal_decoy_min_up", 0.9)) ) extra_z = torch.where(decoy, illegal_z, legal_z) normal_floor = torch.full_like(extra_z, height_offset + 0.18) trap_floor = torch.full_like(extra_z, float(self.lava_z) + 0.65) z_floor = torch.where(trap_low, trap_floor, normal_floor) self.platforms[:, route_len:, 2] = torch.minimum(torch.maximum(extra_z, z_floor), torch.full_like(extra_z, max_h)) source_pos = torch.cat( ( anchor_xy, anchor_z[:, :, None], ), dim=2, ) source_size = self.sizes[:, :route_len].gather(1, anchors[:, :, None].expand(-1, -1, 2)) rolled_extra_pos = torch.roll(self.platforms[:, route_len:], shifts=1, dims=1) rolled_extra_size = torch.roll(self.sizes[:, route_len:], shifts=1, dims=1) source_pos = torch.where(trap_deadend[:, :, None], rolled_extra_pos, source_pos) source_size = torch.where(trap_deadend[:, :, None], rolled_extra_size, source_size) source_pos = torch.where(forced_loopback_deadend[:, :, None], rolled_extra_pos, source_pos) source_size = torch.where(forced_loopback_deadend[:, :, None], rolled_extra_size, source_size) source_pos = torch.where(braid_end[:, :, None], rolled_extra_pos, source_pos) source_size = torch.where(braid_end[:, :, None], rolled_extra_size, source_size) self_source_pos = self.platforms[:, route_len:] self_source_size = self.sizes[:, route_len:] source_pos = torch.where(false_finish_goal[:, :, None], self_source_pos, source_pos) source_size = torch.where(false_finish_goal[:, :, None], self_source_size, source_size) enforce_extra_source_reach(self, source_pos, source_size, safety=0.10, passes=1, exclude_mask=trap_high) if unlock_lever_mask is not None and unlock_target_i is not None: enforce_masked_extra_to_route_reach(self, unlock_lever_mask, unlock_target_i, safety=0.08, passes=2) if extra_reach_exclude is not None: goal_index = torch.full_like(anchors, route_len - 1) enforce_masked_extra_to_route_reach(self, extra_reach_exclude, goal_index, safety=0.08, passes=2) enforce_masked_extra_to_route_reach(self, braid_end, torch.roll(braid_target_i, shifts=1, dims=1), safety=0.04, passes=1) enforce_masked_extra_to_route_reach(self, false_summit, fan_target_i, safety=0.04, passes=1) enforce_extra_source_reach(self, source_pos, source_size, safety=0.04, passes=1, exclude_mask=trap_high) self.separate_xy_overlaps(iterations=cleanup_iters) separate_route_overlaps(self, iterations=max(12, cleanup_iters // 8), clearance=0.10) separate_nonroute_overlaps(self, iterations=max(24, cleanup_iters // 2)) enforce_route_edge_gap(self, floor=max(0.0, float(_setting(self.cfg, "map_route_edge_gap_floor", 0.0))), passes=2) enforce_route_reach(self, safety=0.12, passes=2) relocate_route_colliding_extras(self, iterations=5, clearance=0.26) separate_nonroute_overlaps(self, iterations=max(32, cleanup_iters)) enforce_route_edge_gap(self, floor=max(0.0, float(_setting(self.cfg, "map_route_edge_gap_floor", 0.0))), passes=2) enforce_route_reach(self, safety=0.12, passes=2) relocate_route_colliding_extras(self, iterations=6, clearance=0.30) resolve_route_extra_grazes(self, iterations=8, clearance=0.06) scatter_colliding_extras(self, iterations=8, clearance=0.34) relocate_route_colliding_extras(self, iterations=4, clearance=0.34) resolve_route_extra_grazes(self, iterations=4, clearance=0.08) separate_route_overlaps(self, iterations=max(8, cleanup_iters // 16), clearance=0.08) enforce_route_edge_gap(self, floor=max(0.0, float(_setting(self.cfg, "map_route_edge_gap_floor", 0.0))), passes=2) enforce_route_reach(self, safety=0.12, passes=2) separate_route_overlaps(self, iterations=max(16, cleanup_iters // 8), clearance=0.08) enforce_route_edge_gap(self, floor=max(0.0, float(_setting(self.cfg, "map_route_edge_gap_floor", 0.0))), passes=1) enforce_route_reach(self, safety=0.08, passes=1) separate_route_overlaps(self, iterations=max(8, cleanup_iters // 12), clearance=0.04) enforce_route_reach(self, safety=0.12, passes=2) enforce_extra_any_route_reach(self, safety=0.02, passes=2, exclude_mask=extra_reach_exclude) if int(self.p) > int(self.route_len): self.platforms[:, self.route_len :, :2] = clamp_xy_to_route_span( self.platforms[:, : self.route_len, :2], self.platforms[:, self.route_len :, :2], min_frac=float(_setting(self.cfg, "map_extra_span_min_frac", 0.05)), max_frac=float(_setting(self.cfg, "map_extra_span_max_frac", 0.90)), end_margin=float(_setting(self.cfg, "map_extra_span_end_margin", 1.10)), ) resolve_route_extra_grazes(self, iterations=4, clearance=0.05) enforce_extra_any_route_reach(self, safety=0.00, passes=1, exclude_mask=extra_reach_exclude) if int(self.p) > int(self.route_len): self.platforms[:, self.route_len :, :2] = clamp_xy_to_route_span( self.platforms[:, : self.route_len, :2], self.platforms[:, self.route_len :, :2], min_frac=float(_setting(self.cfg, "map_extra_span_min_frac", 0.05)), max_frac=float(_setting(self.cfg, "map_extra_span_max_frac", 0.90)), end_margin=float(_setting(self.cfg, "map_extra_span_end_margin", 1.10)), ) center = self.platforms[:, :, :2].mean(dim=1, keepdim=True) self.platforms[:, :, :2] -= center def clamp_xy_to_route_span( route_xy: torch.Tensor, xy: torch.Tensor, *, min_frac: float = 0.05, max_frac: float = 0.90, end_margin: float = 1.10, ) -> torch.Tensor: if xy.numel() == 0 or route_xy.shape[1] < 2: return xy start = route_xy[:, 0:1] goal = route_xy[:, -1:] axis = goal - start span = torch.linalg.norm(axis, dim=2, keepdim=True).clamp_min(1e-4) axis = axis / span rel = xy - start proj = (rel * axis).sum(dim=2, keepdim=True) lower = span * float(min_frac) upper = torch.maximum(lower + 0.25, span * float(max_frac) - float(end_margin)) clamped = proj.clamp_min(lower).clamp_max(upper) return xy + axis * (clamped - proj) def overlap_count(platforms: torch.Tensor, sizes: torch.Tensor, *, margin: float = 0.0) -> int: p = int(platforms.shape[1]) xy = platforms[:, :, :2] delta = (xy[:, :, None, :] - xy[:, None, :, :]).abs() gap = (sizes[:, :, None, :] + sizes[:, None, :, :]) * 0.5 + float(margin) colliding = (delta[..., 0] < gap[..., 0]) & (delta[..., 1] < gap[..., 1]) eye = torch.eye(p, dtype=torch.bool, device=platforms.device)[None, :, :] return int(((colliding & ~eye).sum() // 2).detach().cpu()) def route_overlap_count(env: Any, *, margin: float = 0.0) -> int: return overlap_count(env.platforms[:, : env.route_len], env.sizes[:, : env.route_len], margin=margin) def extra_overlap_count(env: Any, *, margin: float = 0.0) -> int: route_len = int(env.route_len) if int(env.p) <= route_len: return 0 return overlap_count(env.platforms[:, route_len:], env.sizes[:, route_len:], margin=margin) def route_extra_overlap_count(env: Any, *, margin: float = 0.0) -> int: route_len, p = int(env.route_len), int(env.p) if p <= route_len: return 0 route_xy = env.platforms[:, :route_len, :2] extra_xy = env.platforms[:, route_len:, :2] route_sizes = env.sizes[:, :route_len] extra_sizes = env.sizes[:, route_len:] delta = (extra_xy[:, :, None, :] - route_xy[:, None, :, :]).abs() gap = (extra_sizes[:, :, None, :] + route_sizes[:, None, :, :]) * 0.5 + float(margin) colliding = (delta[..., 0] < gap[..., 0]) & (delta[..., 1] < gap[..., 1]) return int(colliding.sum().detach().cpu()) def pair_reach_margin(env: Any, src: torch.Tensor, dst: torch.Tensor, src_size: torch.Tensor, dst_size: torch.Tensor) -> torch.Tensor: delta = dst - src xy = torch.linalg.norm(delta[..., :2], dim=-1) dz = delta[..., 2] jump_v = float(env.jump_velocity) gravity = float(env.gravity) disc = jump_v * jump_v - 2.0 * gravity * dz feasible = disc >= 0.0 flight = (jump_v + torch.sqrt(disc.clamp_min(0.0))) / gravity reach = float(env.max_speed) * float(env.sprint_speed_mult) * flight * float(getattr(env.cfg, "map_reach_scale", 0.82)) landing_pad = dst_size.amin(dim=-1) * 0.35 + src_size.amin(dim=-1) * 0.18 + float(env.radius) * 0.8 allowed = torch.where(feasible, reach + landing_pad - 0.10, torch.full_like(reach, -1.0e6)) return allowed - xy def enforce_extra_source_reach( env: Any, source_pos: torch.Tensor, source_size: torch.Tensor, *, safety: float = 0.10, passes: int = 2, exclude_mask: torch.Tensor | None = None, ) -> None: route_len, p = int(env.route_len), int(env.p) extras = p - route_len if extras <= 0: return for _ in range(max(1, int(passes))): dst = env.platforms[:, route_len:] dst_size = env.sizes[:, route_len:] margin = pair_reach_margin(env, source_pos, dst, source_size, dst_size) too_far = margin < float(safety) if exclude_mask is not None: too_far = too_far & ~exclude_mask if not bool(too_far.any().detach().cpu()): return delta_xy = dst[:, :, :2] - source_pos[:, :, :2] dist = torch.linalg.norm(delta_xy, dim=2).clamp_min(1e-4) direction = delta_xy / dist[:, :, None] pull = (float(safety) - margin).clamp_min(0.0) env.platforms[:, route_len:, :2] = dst[:, :, :2] - direction * pull[:, :, None] * too_far[:, :, None].float() def enforce_extra_any_route_reach( env: Any, *, safety: float = 0.0, passes: int = 2, exclude_mask: torch.Tensor | None = None, ) -> None: route_len, p = int(env.route_len), int(env.p) extras = p - route_len if extras <= 0: return for _ in range(max(1, int(passes))): route_pos = env.platforms[:, :route_len] route_size = env.sizes[:, :route_len] extra_pos = env.platforms[:, route_len:] extra_size = env.sizes[:, route_len:] margins = pair_reach_margin( env, route_pos[:, :, None, :], extra_pos[:, None, :, :], route_size[:, :, None, :], extra_size[:, None, :, :], ) best_margin, best_route = margins.max(dim=1) too_far = best_margin < float(safety) if exclude_mask is not None: too_far = too_far & ~exclude_mask if not bool(too_far.any().detach().cpu()): return src = route_pos.gather(1, best_route[:, :, None].expand(-1, -1, 3)) delta_xy = extra_pos[:, :, :2] - src[:, :, :2] dist = torch.linalg.norm(delta_xy, dim=2).clamp_min(1e-4) direction = delta_xy / dist[:, :, None] pull = (float(safety) - best_margin).clamp_min(0.0) env.platforms[:, route_len:, :2] = extra_pos[:, :, :2] - direction * pull[:, :, None] * too_far[:, :, None].float() def enforce_masked_extra_to_route_reach( env: Any, mask: torch.Tensor, route_index: torch.Tensor, *, safety: float = 0.0, passes: int = 1, ) -> None: route_len, p = int(env.route_len), int(env.p) extras = p - route_len if extras <= 0: return route_index = route_index.clamp(0, route_len - 1) for _ in range(max(1, int(passes))): extra_pos = env.platforms[:, route_len:] extra_size = env.sizes[:, route_len:] dst = env.platforms[:, :route_len].gather(1, route_index[:, :, None].expand(-1, -1, 3)) dst_size = env.sizes[:, :route_len].gather(1, route_index[:, :, None].expand(-1, -1, 2)) margin = pair_reach_margin(env, extra_pos, dst, extra_size, dst_size) too_far = (margin < float(safety)) & mask if not bool(too_far.any().detach().cpu()): return delta_xy = extra_pos[:, :, :2] - dst[:, :, :2] dist = torch.linalg.norm(delta_xy, dim=2).clamp_min(1e-4) direction = delta_xy / dist[:, :, None] pull = (float(safety) - margin).clamp_min(0.0) env.platforms[:, route_len:, :2] = extra_pos[:, :, :2] - direction * pull[:, :, None] * too_far[:, :, None].float() def route_reach_margin(env: Any) -> torch.Tensor: route = env.platforms[:, : env.route_len] sizes = env.sizes[:, : env.route_len] delta = route[:, 1:] - route[:, :-1] xy = torch.linalg.norm(delta[:, :, :2], dim=2) dz = delta[:, :, 2] jump_v = float(env.jump_velocity) gravity = float(env.gravity) disc = jump_v * jump_v - 2.0 * gravity * dz feasible = disc >= 0.0 flight = (jump_v + torch.sqrt(disc.clamp_min(0.0))) / gravity reach = float(env.max_speed) * float(env.sprint_speed_mult) * flight * float(getattr(env.cfg, "map_reach_scale", 0.82)) landing_pad = sizes[:, 1:].amin(dim=2) * 0.35 + sizes[:, :-1].amin(dim=2) * 0.18 + float(env.radius) * 0.8 allowed = torch.where(feasible, reach + landing_pad - 0.10, torch.full_like(reach, -1.0e6)) return allowed - xy def all_platform_path_diagnostics(env: Any, *, reach_margin: float = 0.0) -> dict[str, float]: """No-route-label geometry audit over every platform. This is intentionally separate from the construction route. It treats every physically reachable platform transition as a graph edge, then measures the shortest start-to-goal path a perfect full-map planner could take. If this is much shorter than route_len - 1, the generated map is easier than its route count claims. """ n, p = int(env.n), int(env.p) route_jumps = max(1, int(env.route_len) - 1) if p <= 1: return { "all_path_goal_reachable_fraction": 0.0, "all_path_shortest_hops_mean": float(route_jumps + 1), "all_path_shortest_hops_min": float(route_jumps + 1), "all_path_shortest_hops_p50": float(route_jumps + 1), "all_path_shortcut_ratio_mean": float((route_jumps + 1) / route_jumps), "all_path_shortcut_le_half_fraction": 0.0, "all_path_shortcut_le_five_fraction": 0.0, "all_path_platform_reachable_fraction": 0.0, "all_platform_nearest_edge_gap_mean": 0.0, "all_platform_nearest_edge_gap_min": 0.0, } pos = env.platforms sizes = env.sizes margin = pair_reach_margin( env, pos[:, :, None, :], pos[:, None, :, :], sizes[:, :, None, :], sizes[:, None, :, :], ) eye = torch.eye(p, dtype=torch.bool, device=env.device)[None, :, :] edge = (margin > float(reach_margin)) & ~eye reached = torch.zeros((n, p), dtype=torch.bool, device=env.device) reached[:, 0] = True distance = torch.full((n, p), float(p + 1), device=env.device) distance[:, 0] = 0.0 for hop in range(1, p + 1): next_reached = (reached[:, :, None] & edge).any(dim=1) new = next_reached & ~reached distance = torch.where(new, torch.full_like(distance, float(hop)), distance) reached = reached | next_reached if bool(reached[:, int(env.route_len) - 1].all().detach().cpu()): break goal_hops = distance[:, int(env.route_len) - 1] goal_reachable = goal_hops <= float(p) ratio = goal_hops / float(route_jumps) half_threshold = max(1.0, float(route_jumps) * 0.50) delta_xy = pos[:, :, None, :2] - pos[:, None, :, :2] dist_xy = torch.linalg.norm(delta_xy, dim=3).clamp_min(1e-4) direction = delta_xy / dist_xy[:, :, :, None] support = ( (direction.abs() * sizes[:, :, None, :]).sum(dim=3) * 0.5 + (direction.abs() * sizes[:, None, :, :]).sum(dim=3) * 0.5 ) edge_gap = dist_xy - support edge_gap = torch.where(eye, torch.full_like(edge_gap, 1.0e6), edge_gap) nearest_gap = edge_gap.amin(dim=2) return { "all_path_goal_reachable_fraction": float(goal_reachable.float().mean().detach().cpu()), "all_path_shortest_hops_mean": float(goal_hops.mean().detach().cpu()), "all_path_shortest_hops_min": float(goal_hops.min().detach().cpu()), "all_path_shortest_hops_p50": float(torch.quantile(goal_hops, 0.50).detach().cpu()), "all_path_shortcut_ratio_mean": float(ratio.mean().detach().cpu()), "all_path_shortcut_le_half_fraction": float(((goal_hops <= half_threshold) & goal_reachable).float().mean().detach().cpu()), "all_path_shortcut_le_five_fraction": float(((goal_hops <= 5.0) & goal_reachable).float().mean().detach().cpu()), "all_path_platform_reachable_fraction": float(reached.float().mean().detach().cpu()), "all_platform_nearest_edge_gap_mean": float(nearest_gap.mean().detach().cpu()), "all_platform_nearest_edge_gap_min": float(nearest_gap.min().detach().cpu()), } def enforce_route_reach(env: Any, *, safety: float = 0.10, passes: int = 2) -> None: route_len = int(env.route_len) if route_len <= 1: return for _ in range(max(1, int(passes))): for layer in range(1, route_len): prev = env.platforms[:, layer - 1] cur = env.platforms[:, layer] dxy = cur[:, :2] - prev[:, :2] dist = torch.linalg.norm(dxy, dim=1).clamp_min(1e-4) direction = dxy / dist[:, None] dz = cur[:, 2] - prev[:, 2] jump_v = float(env.jump_velocity) gravity = float(env.gravity) disc = jump_v * jump_v - 2.0 * gravity * dz flight = (jump_v + torch.sqrt(disc.clamp_min(0.0))) / gravity sizes = env.sizes landing_pad = sizes[:, layer].amin(dim=1) * 0.35 + sizes[:, layer - 1].amin(dim=1) * 0.18 + float(env.radius) * 0.8 max_dist = ( float(env.max_speed) * float(env.sprint_speed_mult) * flight * float(getattr(env.cfg, "map_reach_scale", 0.82)) + landing_pad - 0.10 - float(safety) ) too_far = dist > max_dist if not bool(too_far.any().detach().cpu()): continue pull = direction * (dist - max_dist).clamp_min(0.0)[:, None] * too_far[:, None].float() env.platforms[:, layer:route_len, :2] -= pull[:, None, :] def enforce_route_edge_gap(env: Any, *, floor: float = 0.0, passes: int = 2) -> None: route_len = int(env.route_len) if route_len <= 1 or floor <= 0.0: return for _ in range(max(1, int(passes))): for layer in range(1, route_len): prev = env.platforms[:, layer - 1] cur = env.platforms[:, layer] dxy = cur[:, :2] - prev[:, :2] dist = torch.linalg.norm(dxy, dim=1).clamp_min(1e-4) direction = dxy / dist[:, None] support = ( (direction.abs() * env.sizes[:, layer - 1]).sum(dim=1) * 0.5 + (direction.abs() * env.sizes[:, layer]).sum(dim=1) * 0.5 ) edge_gap = dist - support deficit = (float(floor) - edge_gap).clamp_min(0.0) env.platforms[:, layer:route_len, :2] += direction[:, None, :] * deficit[:, None, None] def separate_nonroute_overlaps(env: Any, *, iterations: int = 24) -> None: p = int(env.p) if p <= 1: return margin = max(0.12, float(getattr(env.cfg, "map_braid_mesh_clearance", 0.16))) eye = torch.eye(p, dtype=torch.bool, device=env.device)[None, :, :] mobility = torch.ones((env.n, p), device=env.device) mobility[:, : int(env.route_len)] = 0.0 for _ in range(max(0, int(iterations))): xy = env.platforms[:, :, :2] delta = xy[:, :, None, :] - xy[:, None, :, :] abs_delta = delta.abs() gap_x = (env.sizes[:, :, None, 0] + env.sizes[:, None, :, 0]) * 0.5 + margin gap_y = (env.sizes[:, :, None, 1] + env.sizes[:, None, :, 1]) * 0.5 + margin overlap_x = gap_x - abs_delta[..., 0] overlap_y = gap_y - abs_delta[..., 1] colliding = (overlap_x > 0.0) & (overlap_y > 0.0) & ~eye use_x = overlap_x <= overlap_y sign_x = torch.where(delta[..., 0] >= 0.0, 1.0, -1.0) sign_y = torch.where(delta[..., 1] >= 0.0, 1.0, -1.0) pair_push = torch.zeros_like(delta) pair_push[..., 0] = torch.where(colliding & use_x, sign_x * overlap_x, torch.zeros_like(overlap_x)) pair_push[..., 1] = torch.where(colliding & ~use_x, sign_y * overlap_y, torch.zeros_like(overlap_y)) share = mobility[:, :, None] / (mobility[:, :, None] + mobility[:, None, :] + 1e-6) weighted = pair_push * share[..., None] active_pairs = colliding.float().sum(dim=2, keepdim=True).clamp_min(1.0) push = weighted.sum(dim=2) / active_pairs env.platforms[:, :, :2] = xy + (push * mobility[:, :, None]).clamp(-0.28, 0.28) def separate_route_overlaps(env: Any, *, iterations: int = 6, clearance: float = 0.06) -> None: route_len = int(env.route_len) if route_len <= 2: return ids = torch.arange(route_len, device=env.device) pair_index_gap = (ids[:, None] - ids[None, :]).abs() non_adjacent = pair_index_gap > 1 eye = torch.eye(route_len, dtype=torch.bool, device=env.device)[None, :, :] mobility = torch.linspace(0.15, 1.0, route_len, device=env.device)[None, :] mobility[:, 0] = 0.02 mobility[:, -1] = 0.35 for _ in range(max(1, int(iterations))): xy = env.platforms[:, :route_len, :2] sizes = env.sizes[:, :route_len] delta = xy[:, :, None, :] - xy[:, None, :, :] abs_delta = delta.abs() gap_x = (sizes[:, :, None, 0] + sizes[:, None, :, 0]) * 0.5 + float(clearance) gap_y = (sizes[:, :, None, 1] + sizes[:, None, :, 1]) * 0.5 + float(clearance) overlap_x = gap_x - abs_delta[..., 0] overlap_y = gap_y - abs_delta[..., 1] colliding = (overlap_x > 0.0) & (overlap_y > 0.0) & non_adjacent[None, :, :] & ~eye if not bool(colliding.any().detach().cpu()): return use_x = overlap_x <= overlap_y sign_x = torch.where(delta[..., 0] >= 0.0, 1.0, -1.0) sign_y = torch.where(delta[..., 1] >= 0.0, 1.0, -1.0) pair_push = torch.zeros_like(delta) pair_push[..., 0] = torch.where(colliding & use_x, sign_x * overlap_x, torch.zeros_like(overlap_x)) pair_push[..., 1] = torch.where(colliding & ~use_x, sign_y * overlap_y, torch.zeros_like(overlap_y)) share = mobility[:, :, None] / (mobility[:, :, None] + mobility[:, None, :] + 1e-6) active_pairs = colliding.float().sum(dim=2, keepdim=True).clamp_min(1.0) push = (pair_push * share[..., None]).sum(dim=2) / active_pairs env.platforms[:, :route_len, :2] = xy + (push * mobility[:, :, None]).clamp(-0.36, 0.36) def relocate_route_colliding_extras(env: Any, *, iterations: int = 4, clearance: float = 0.22) -> None: route_len, p = int(env.route_len), int(env.p) extras = p - route_len if extras <= 0: return route_ids = torch.arange(route_len, device=env.device) extra_ids = torch.arange(extras, device=env.device) for step in range(max(1, int(iterations))): route_xy = env.platforms[:, :route_len, :2] extra_xy = env.platforms[:, route_len:, :2] route_sizes = env.sizes[:, :route_len] extra_sizes = env.sizes[:, route_len:] delta = extra_xy[:, :, None, :] - route_xy[:, None, :, :] abs_delta = delta.abs() gap = (extra_sizes[:, :, None, :] + route_sizes[:, None, :, :]) * 0.5 + float(clearance) overlap_x = gap[..., 0] - abs_delta[..., 0] overlap_y = gap[..., 1] - abs_delta[..., 1] colliding = (overlap_x > 0.0) & (overlap_y > 0.0) score = torch.where(colliding, overlap_x.clamp_min(0.0) + overlap_y.clamp_min(0.0), torch.full_like(overlap_x, -1.0)) best_score, best_route = score.max(dim=2) needs_move = best_score > 0.0 if not bool(needs_move.any().detach().cpu()): return anchor_xy = route_xy.gather(1, best_route[:, :, None].expand(-1, -1, 2)) anchor_sizes = route_sizes.gather(1, best_route[:, :, None].expand(-1, -1, 2)) raw_dir = extra_xy - anchor_xy norm = torch.linalg.norm(raw_dir, dim=2, keepdim=True) angle = ( env.env_i[:, None].float() * 1.618 + extra_ids[None, :].float() * 2.399 + route_ids[best_route].float() * 0.71 + float(step) * 0.83 ) fallback = torch.stack((torch.cos(angle), torch.sin(angle)), dim=2) direction = torch.where(norm > 1e-3, raw_dir / norm.clamp_min(1e-3), fallback) support = (direction.abs() * anchor_sizes).sum(dim=2) * 0.5 + (direction.abs() * extra_sizes).sum(dim=2) * 0.5 target = support + float(clearance) + 0.22 + 0.08 * ((extra_ids[None, :].float() + float(step)) % 3.0) relocated = anchor_xy + direction * target[:, :, None] env.platforms[:, route_len:, :2] = torch.where(needs_move[:, :, None], relocated, extra_xy) def resolve_route_extra_grazes(env: Any, *, iterations: int = 4, clearance: float = 0.04) -> None: route_len, p = int(env.route_len), int(env.p) extras = p - route_len if extras <= 0: return for _ in range(max(1, int(iterations))): route_xy = env.platforms[:, :route_len, :2] extra_xy = env.platforms[:, route_len:, :2] route_sizes = env.sizes[:, :route_len] extra_sizes = env.sizes[:, route_len:] delta = extra_xy[:, :, None, :] - route_xy[:, None, :, :] abs_delta = delta.abs() gap = (extra_sizes[:, :, None, :] + route_sizes[:, None, :, :]) * 0.5 overlap_x = gap[..., 0] - abs_delta[..., 0] overlap_y = gap[..., 1] - abs_delta[..., 1] colliding = (overlap_x > 0.0) & (overlap_y > 0.0) score = torch.where(colliding, overlap_x.clamp_min(0.0) + overlap_y.clamp_min(0.0), torch.full_like(overlap_x, -1.0)) best_score, best_route = score.max(dim=2) needs_move = best_score > 0.0 if not bool(needs_move.any().detach().cpu()): return chosen_delta = delta.gather(2, best_route[:, :, None, None].expand(-1, -1, 1, 2)).squeeze(2) chosen_ox = overlap_x.gather(2, best_route[:, :, None]).squeeze(2).clamp_min(0.0) chosen_oy = overlap_y.gather(2, best_route[:, :, None]).squeeze(2).clamp_min(0.0) use_x = chosen_ox <= chosen_oy sign_x = torch.where(chosen_delta[:, :, 0] >= 0.0, 1.0, -1.0) sign_y = torch.where(chosen_delta[:, :, 1] >= 0.0, 1.0, -1.0) push = torch.zeros_like(extra_xy) push[:, :, 0] = torch.where(use_x, sign_x * (chosen_ox + float(clearance)), torch.zeros_like(chosen_ox)) push[:, :, 1] = torch.where(~use_x, sign_y * (chosen_oy + float(clearance)), torch.zeros_like(chosen_oy)) env.platforms[:, route_len:, :2] = extra_xy + push * needs_move[:, :, None].float() def scatter_colliding_extras(env: Any, *, iterations: int = 6, clearance: float = 0.28) -> None: route_len, p = int(env.route_len), int(env.p) extras = p - route_len if extras <= 0: return extra_ids = torch.arange(extras, device=env.device) for step in range(max(1, int(iterations))): route_xy = env.platforms[:, :route_len, :2] extra_xy = env.platforms[:, route_len:, :2] route_sizes = env.sizes[:, :route_len] extra_sizes = env.sizes[:, route_len:] route_delta = extra_xy[:, :, None, :] - route_xy[:, None, :, :] route_abs = route_delta.abs() route_gap = (extra_sizes[:, :, None, :] + route_sizes[:, None, :, :]) * 0.5 + float(clearance) route_overlap_x = route_gap[..., 0] - route_abs[..., 0] route_overlap_y = route_gap[..., 1] - route_abs[..., 1] route_colliding = (route_overlap_x > 0.0) & (route_overlap_y > 0.0) route_score = torch.where( route_colliding, route_overlap_x.clamp_min(0.0) + route_overlap_y.clamp_min(0.0), torch.full_like(route_overlap_x, -1.0), ) best_route_score, best_route = route_score.max(dim=2) extra_delta = extra_xy[:, :, None, :] - extra_xy[:, None, :, :] extra_abs = extra_delta.abs() extra_gap = (extra_sizes[:, :, None, :] + extra_sizes[:, None, :, :]) * 0.5 + float(clearance) extra_overlap_x = extra_gap[..., 0] - extra_abs[..., 0] extra_overlap_y = extra_gap[..., 1] - extra_abs[..., 1] eye = torch.eye(extras, dtype=torch.bool, device=env.device)[None, :, :] extra_colliding = (extra_overlap_x > 0.0) & (extra_overlap_y > 0.0) & ~eye needs_move = (best_route_score > 0.0) | extra_colliding.any(dim=2) if not bool(needs_move.any().detach().cpu()): return nearest_route = torch.linalg.norm(route_delta, dim=3).argmin(dim=2) anchor_idx = torch.where(best_route_score > 0.0, best_route, nearest_route) anchor_xy = route_xy.gather(1, anchor_idx[:, :, None].expand(-1, -1, 2)) anchor_sizes = route_sizes.gather(1, anchor_idx[:, :, None].expand(-1, -1, 2)) angle = ( env.env_i[:, None].float() * 1.61803398875 + extra_ids[None, :].float() * 2.39996322973 + anchor_idx.float() * 0.73 + float(step) * 0.91 ) direction = torch.stack((torch.cos(angle), torch.sin(angle)), dim=2) support = (direction.abs() * anchor_sizes).sum(dim=2) * 0.5 + (direction.abs() * extra_sizes).sum(dim=2) * 0.5 lane = ((extra_ids[None, :].float() + float(step)) % 5.0) * 0.18 target = support + float(clearance) + 0.92 + lane relocated = anchor_xy + direction * target[:, :, None] env.platforms[:, route_len:, :2] = torch.where(needs_move[:, :, None], relocated, extra_xy) def mapgen_summary(env: Any) -> dict[str, float | int]: route = env.platforms[:, : env.route_len] route_sizes = env.sizes[:, : env.route_len] route_delta_xy = route[:, 1:, :2] - route[:, :-1, :2] step_xy = torch.linalg.norm(route_delta_xy, dim=2) direction = route_delta_xy / step_xy[:, :, None].clamp_min(1e-4) edge_support = ( (direction.abs() * route_sizes[:, :-1]).sum(dim=2) * 0.5 + (direction.abs() * route_sizes[:, 1:]).sum(dim=2) * 0.5 ) route_edge_gap = step_xy - edge_support step_z = route[:, 1:, 2] - route[:, :-1, 2] route_path_xy = step_xy.sum(dim=1) height_span = env.platforms[:, :, 2].amax(dim=1) - env.platforms[:, :, 2].amin(dim=1) goal_xy = torch.linalg.norm(route[:, -1, :2] - route[:, 0, :2], dim=1) goal_xy_to_route_path = goal_xy / route_path_xy.clamp_min(1e-4) goal_z_delta = route[:, -1, 2] - route[:, 0, 2] turn_abs = torch.zeros((), device=env.device) if int(env.route_len) > 2: prev_dir = direction[:, :-1] next_dir = direction[:, 1:] dot = (prev_dir * next_dir).sum(dim=2).clamp(-1.0, 1.0) cross = prev_dir[..., 0] * next_dir[..., 1] - prev_dir[..., 1] * next_dir[..., 0] turn_abs = torch.atan2(cross.abs(), dot).mean() goal_vec = route[:, -1, :2] - route[:, 0, :2] goal_dir = goal_vec / torch.linalg.norm(goal_vec, dim=1, keepdim=True).clamp_min(1e-4) goal_dot = (direction * goal_dir[:, None, :]).sum(dim=2) away_goal_fraction = (goal_dot < -0.10).float().mean() side_goal_fraction = (goal_dot.abs() < 0.25).float().mean() down_step_fraction = (step_z < -0.22).float().mean() margin = route_reach_margin(env) extra_reachable = torch.ones((), device=env.device) extra_to_route_reachable = torch.ones((), device=env.device) route_extra_predecessor_fraction = torch.zeros((), device=env.device) late_route_extra_predecessor_fraction = torch.zeros((), device=env.device) route_extra_predecessor_mean = torch.zeros((), device=env.device) low_extra_fraction = torch.zeros((), device=env.device) extra_before_start_fraction = torch.zeros((), device=env.device) extra_after_goal_fraction = torch.zeros((), device=env.device) extra_between_start_goal_fraction = torch.zeros((), device=env.device) extra_axis_projection_mean = torch.zeros((), device=env.device) lever_per_map = torch.zeros((), device=env.device) locked_per_map = torch.zeros((), device=env.device) locked_initially_open = torch.ones((), device=env.device) if hasattr(env, "lever_mask") and hasattr(env, "locked_by"): lever_per_map = env.lever_mask.float().sum(dim=1).mean() locked = env.locked_by >= 0 locked_per_map = locked.float().sum(dim=1).mean() if bool(locked.any().detach().cpu()): locked_initially_open = env.platform_available[locked].float().mean() if int(env.p) > int(env.route_len): route_pos = env.platforms[:, : env.route_len] route_size = env.sizes[:, : env.route_len] extra_pos = env.platforms[:, env.route_len :] extra_size = env.sizes[:, env.route_len :] route_floor = route_pos[:, :, 2].amin(dim=1, keepdim=True) low_extra_fraction = (extra_pos[:, :, 2] < route_floor - 0.85).float().mean() axis_vec = route_pos[:, -1:, :2] - route_pos[:, 0:1, :2] axis_len = torch.linalg.norm(axis_vec, dim=2, keepdim=True).clamp_min(1e-4) axis = axis_vec / axis_len projection = ((extra_pos[:, :, :2] - route_pos[:, 0:1, :2]) * axis).sum(dim=2, keepdim=True) / axis_len extra_axis_projection_mean = projection.mean() extra_before_start_fraction = (projection < 0.0).float().mean() extra_after_goal_fraction = (projection > 1.0).float().mean() extra_between_start_goal_fraction = ((projection >= 0.0) & (projection <= 1.0)).float().mean() route_to_extra = pair_reach_margin( env, route_pos[:, :, None, :], extra_pos[:, None, :, :], route_size[:, :, None, :], extra_size[:, None, :, :], ) extra_to_route = pair_reach_margin( env, extra_pos[:, :, None, :], route_pos[:, None, :, :], extra_size[:, :, None, :], route_size[:, None, :, :], ) extra_reachable = (route_to_extra.amax(dim=1) > 0.0).float().mean() extra_to_route_reachable = (extra_to_route.amax(dim=2) > 0.0).float().mean() extra_pred = extra_to_route > 0.0 pred_counts = extra_pred.float().sum(dim=1) route_nodes = torch.arange(int(env.route_len), device=env.device)[None, :] inner_route = (route_nodes > 0) & (route_nodes < int(env.route_len) - 1) late_route = route_nodes >= max(1, int(env.route_len) - 4) route_extra_predecessor_fraction = ((pred_counts > 0.0) & inner_route).float().mean() late_route_extra_predecessor_fraction = ((pred_counts > 0.0) & late_route).float().mean() route_extra_predecessor_mean = pred_counts[:, 1:-1].mean() footprint_area = env.sizes[:, :, 0] * env.sizes[:, :, 1] return { "envs": int(env.n), "platforms": int(env.p), "overlaps": overlap_count(env.platforms, env.sizes, margin=0.0), "overlaps_2cm": overlap_count(env.platforms, env.sizes, margin=0.02), "route_overlaps": route_overlap_count(env, margin=0.0), "extra_overlaps": extra_overlap_count(env, margin=0.0), "route_extra_overlaps": route_extra_overlap_count(env, margin=0.0), "extra_from_route_reachable_fraction": float(extra_reachable.detach().cpu()), "extra_to_route_reachable_fraction": float(extra_to_route_reachable.detach().cpu()), "route_extra_predecessor_fraction": float(route_extra_predecessor_fraction.detach().cpu()), "late_route_extra_predecessor_fraction": float(late_route_extra_predecessor_fraction.detach().cpu()), "route_extra_predecessor_mean": float(route_extra_predecessor_mean.detach().cpu()), "low_extra_fraction": float(low_extra_fraction.detach().cpu()), "lever_per_map": float(lever_per_map.detach().cpu()), "locked_per_map": float(locked_per_map.detach().cpu()), "locked_initially_open_fraction": float(locked_initially_open.detach().cpu()), "route_min_reach_margin": float(margin.min().detach().cpu()), "route_mean_reach_margin": float(margin.mean().detach().cpu()), "route_edge_gap_mean": float(route_edge_gap.mean().detach().cpu()), "route_edge_gap_min": float(route_edge_gap.min().detach().cpu()), "route_edge_gap_max": float(route_edge_gap.max().detach().cpu()), "step_xy_mean": float(step_xy.mean().detach().cpu()), "step_xy_min": float(step_xy.min().detach().cpu()), "step_xy_max": float(step_xy.max().detach().cpu()), "route_path_xy_mean": float(route_path_xy.mean().detach().cpu()), "goal_xy_to_route_path_ratio_mean": float(goal_xy_to_route_path.mean().detach().cpu()), "step_z_min": float(step_z.min().detach().cpu()), "step_z_max": float(step_z.max().detach().cpu()), "platform_area_mean": float(footprint_area.mean().detach().cpu()), "platform_area_min": float(footprint_area.min().detach().cpu()), "platform_area_max": float(footprint_area.max().detach().cpu()), "tiny_platform_fraction": float((footprint_area < 0.55).float().mean().detach().cpu()), "height_span_mean": float(height_span.mean().detach().cpu()), "height_span_max": float(height_span.max().detach().cpu()), "goal_xy_mean": float(goal_xy.mean().detach().cpu()), "goal_xy_min": float(goal_xy.min().detach().cpu()), "extra_axis_projection_mean": float(extra_axis_projection_mean.detach().cpu()), "extra_before_start_fraction": float(extra_before_start_fraction.detach().cpu()), "extra_after_goal_fraction": float(extra_after_goal_fraction.detach().cpu()), "extra_between_start_goal_fraction": float(extra_between_start_goal_fraction.detach().cpu()), "route_turn_abs_mean": float(turn_abs.detach().cpu()), "route_away_goal_fraction": float(away_goal_fraction.detach().cpu()), "route_side_goal_fraction": float(side_goal_fraction.detach().cpu()), "route_down_step_fraction": float(down_step_fraction.detach().cpu()), "goal_z_delta_mean": float(goal_z_delta.mean().detach().cpu()), "goal_z_delta_abs_mean": float(goal_z_delta.abs().mean().detach().cpu()), "goal_z_delta_min": float(goal_z_delta.min().detach().cpu()), "goal_z_delta_max": float(goal_z_delta.max().detach().cpu()), **all_platform_path_diagnostics(env), }