# Downstream Compatibility Assessment: Proposed TileGridLoader Fixes ↔ Milestones M2 (Rendering) & M3 (Collision)

> **Agent**: `explorer_m1_fix_3` (`teamwork_preview_explorer`)  
> **Role**: Explorer / Synthesis (Downstream Compatibility & Remediation Analysis)  
> **Working Directory**: `c:\Projects\FreeExile\.agents\teamwork\explorer_m1_fix_3`  
> **Authoritative References**:  
> - User Request: `c:\Projects\FreeExile\.agents\teamwork\ORIGINAL_REQUEST.md` (`## 2026-10-01T19:19:13Z`)  
> - Project Charter: `c:\Projects\FreeExile\.agents\teamwork\orchestrator_11\PROJECT.md`  
> - Challenger Report: `c:\Projects\FreeExile\.agents\teamwork\challenger_m1_2\handoff.md`  

---

## 1. Observation

Direct empirical observations, codebase inspections, and benchmark executions:

### 1.1. Challenger Findings & Defect Reproduction
1. **Defect 1 (`getTileAt` `NaN` / Non-finite bug)**:
   - File & Lines: `client/webapp/js/engine/tile_grid_loader.js:105-115`
   - Unpatched implementation:
     ```javascript
     105:   getTileAt(tx, ty) {
     106:     const w = window.currentMapWidth || 0;
     107:     const h = window.currentMapHeight || 0;
     108:     const grid = window.currentMapGrid;
     109:     const ix = Math.floor(tx);
     110:     const iy = Math.floor(ty);
     111:     if (!grid || ix < 0 || ix >= w || iy < 0 || iy >= h) {
     112:       return 2; // TileType.WALL
     113:     }
     114:     return grid[iy * w + ix];
     115:   },
     ```
   - Reproduction via `node tests/unit/test_challenger_tile_grid_stress.js`:
     ```
     [FAIL] OOB Vulnerability: NaN coordinates return WALL (2): getTileAt(NaN, 0) returned undefined, expected 2 (WALL)
     [FAIL] OOB Vulnerability: undefined/null/string coordinates return WALL (2): getTileAt(undefined, 0) returned undefined, expected 2
     ```
   - Root mechanism: In JavaScript, `NaN < 0` is `false` and `NaN >= w` is `false`. Thus line 111 guard fails, causing `grid[NaN]` to execute and return `undefined`.

2. **Defect 2 (Top-level `window` assignment in Node.js)**:
   - File & Lines: `client/webapp/js/engine/tile_grid_loader.js:131-137`
   - Verbatim code:
     ```javascript
     131: window.TileGridLoader = TileGridLoader;
     132: window.getTileAt = TileGridLoader.getTileAt;
     133: window.setTileAt = TileGridLoader.setTileAt;
     134: 
     135: if (typeof module !== "undefined" && module.exports) {
     136:   module.exports = { TileGridLoader, BIOME_CODES };
     137: }
     ```
   - Tool execution: `node -e "require('./client/webapp/js/engine/tile_grid_loader.js')"`
   - Verbatim error: `ReferenceError: window is not defined at Object.<anonymous> (tile_grid_loader.js:131:1)`.

3. **Defect 3 (Missing upfront buffer length validation)**:
   - File & Lines: `client/webapp/js/engine/tile_grid_loader.js:41-68`
   - Reproduction via `test_challenger_tile_grid_stress.js`:
     ```
     [FAIL] Truncation edge case: width=0, height=0 with fake POI count: Silently accepted 16-byte buffer claiming 10 POIs when width/height=0 (pois parsed: 0)
     ```
   - When a buffer has `width=0, height=0` and claims 10 POIs without payload, line 65 `offset + gridLength > byteBuf.length` (`16 + 0 <= 16`) evaluates to `false`, silently returning a metadata object instead of `null`.

### 1.2. Existing Downstream Codebase & Baseline Metrics
1. **Pytest Cross-Language Compatibility Baseline**:
   - Command: `pytest tests/unit/test_challenger_m1_2_binary_compat.py -v`
   - Result: `15 passed in 28.49s` (100% pass across all 9 canonical wilderness zones from $60 \times 45$ to $120 \times 90$, 10,800/10,800 cell fidelity).
2. **Current Collision Engine (`client/webapp/js/engine/collision_engine.js:88-141`)**:
   - `isPositionBlocked(wx, wy, radius, isDodge)` currently only checks static `ZONE_BOUNDS`, hardcoded water distance, and `mapProps` footprints.
   - It does NOT yet integrate `window.getTileAt(tx, ty)` (M3 planned scope).
3. **Current World Renderer (`client/webapp/js/engine/world_renderer.js:17-28`)**:
   - Renders tiles using hardcoded math formulas (`Math.abs(x - y) <= 1` for stone, `Math.hypot(...) < 3.2` for water) without using map grid data (M2 planned scope).
4. **Empirical Throughput of Sanitized `getTileAt`**:
   - In Node.js benchmark with `Number.isFinite` and positive bounds checks, 1,000,000 lookups completed in 14.5 ms (**68.91 million queries/sec**).

---

## 2. Logic Chain

```mermaid
flowchart TD
    subgraph M1_Proposed_Fixes
        F1["Fix 1: Number.isFinite + Strict Positive Bounds in getTileAt"]
        F2["Fix 2: Universal Global Binding root = typeof window !== 'undefined' ? window : ..."]
        F3["Fix 3: Upfront minExpectedLength Check in loadBinaryMap"]
        F4["Fix 4: Reactive Dirtying Hook inside setTileAt"]
    end

    subgraph M2_Rendering_Downstream
        R1["Frustum Culling & Edge Chunks"]
        R2["Atlas Sprite Indexing (ATLAS.getTileRect)"]
        R3["Chunk Dirty Flag Mutation (16x16 LRU Cache)"]
        R4["Headless Benchmark Script (map_render_benchmark.js)"]
    end

    subgraph M3_Collision_Downstream
        C1["isPositionBlocked(wx, wy, radius)"]
        C2["Entity Movement NaN / Zero-Div Immunity"]
        C3["Boss Gate Breach & Dynamic Floor Mutation"]
        C4["Headless Unit Tests (test_tile_collision.py)"]
    end

    F1 -->|"Prevents undefined -> 2 (WALL)"| R2
    F1 -->|"Always returns 2 on OOB chunks"| R1
    F1 -->|"Traps NaN positions as BLOCKED"| C2
    F1 -->|"Consistent tile code lookup"| C1
    
    F2 -->|"Clean require() without global.window"| R4
    F2 -->|"Zero-shim Node/Pytest subprocesses"| C4

    F3 -->|"Rejects 0x0 degenerate buffers"| R1
    F3 -->|"Forces loadFallbackGrid() on corrupt wire"| C1

    F4 -->|"Automatically calls TileMapRenderer.markChunkDirty"| R3
    F4 -->|"Seamless trigger from boss_gate_controller"| C3
```

### 2.1. Compatibility with Milestone M2 (Mobile-Optimized Tile Rendering)

1. **Frustum Culling & Out-of-Bounds Chunk Safety**:
   - *Observation 1.1 & 1.2*: M2 chunks are $16 \times 16$ tiles. Maps have dimensions such as $60 \times 45$ (`zone_tang_kiem_nhai`) or $76 \times 58$. A $60 \times 45$ grid divided into $16 \times 16$ chunks requires a $4 \times 3$ chunk matrix covering up to tile coordinates $(63, 47)$.
   - *Logic*: During pre-baking of edge chunk $(3, 2)$, the renderer queries tiles with $tx \in [60..63]$ or $ty \in [45..47]$.
   - *Impact*: With Fix 1, `getTileAt` returns numeric `2 (TileType.WALL)`. The chunk baker safely draws the perimeter wall texture. Without Fix 1, if any coordinate arithmetic evaluates to non-finite or `NaN`, `getTileAt` returning `undefined` causes `ATLAS.getTileRect(undefined)` to throw `TypeError: Cannot read properties of undefined (reading 'x')`, crashing the render loop.
   - *Verdict*: **Fully Compatible & Required for Stability**.

2. **Mobile RAM & Throughput Budget Compliance**:
   - *Observation 1.2*: Benchmark proves `getTileAt` executes at 68.91 million lookups/sec (14.5 ns/op).
   - *Logic*: ORIGINAL_REQUEST §R2 enforces draw call budgeting $\le (viewport_W / TILE_W + 5) \times (viewport_H / TILE_H + 5) \approx 80$ tiles on screen, or $16 \times 16 = 256$ queries per chunk bake. At 68.9M QPS, baking an entire chunk's tile queries consumes $< 3.8\,\mu\text{s}$.
   - *Logic*: `currentMapGrid` references `byteBuf.subarray(...)` zero-copy. For the maximum $120 \times 90$ zone, RAM is 10.58 KB. This leaves $> 99.8\%$ of the 8 MB mobile RAM budget for the OffscreenCanvas chunk pool.
   - *Verdict*: **Exceeds Performance & Memory Requirements**.

3. **Degenerate Buffer Protection**:
   - *Observation 1.1 (Defect 3)*: Without upfront length validation, a degenerate 16-byte buffer with `width=0, height=0` is accepted as valid.
   - *Logic*: If `TileMapRenderer.init()` receives `width=0, height=0`, chunk count calculations divide by zero (`0 / 16 = 0`), creating degenerate loop bounds or 0-dimension canvas allocations that freeze mobile Safari / Chrome.
   - *Impact*: Fix 3 returns `null`, prompting `canvas_renderer.js` to execute `TileGridLoader.loadFallbackGrid()`, ensuring M2 always receives valid dimensions ($60 \times 45$).
   - *Verdict*: **Critical Preventive Guard for M2**.

4. **Chunk Invalidation Reactive Synchronization**:
   - *Observation 1.2*: In M2, Feature 13 specifies "Re-render chunk only when internal tile state changes" via `TileMapRenderer.markChunkDirty(tx, ty)`.
   - *Logic*: When in-game events occur (e.g. Boss Gate breach mutating `BOSS_GATE` (10) $\to$ `FLOOR` (1)), `setTileAt(bx, by, 1)` updates the memory buffer.
   - *Recommendation*: By adding an optional dispatch in `setTileAt`:
     ```javascript
     if (root.TileMapRenderer && typeof root.TileMapRenderer.markChunkDirty === "function") {
       root.TileMapRenderer.markChunkDirty(ix, iy);
     }
     ```
     `setTileAt` automatically triggers chunk invalidation without requiring tight coupling or manual notification boilerplate from callers.
   - *Verdict*: **Highly Recommended Architectural Bridge**.

---

### 2.2. Compatibility with Milestone M3 (Tile Collision & Boss Gate Logic)

1. **Entity Physics `NaN` Immunity in `isPositionBlocked`**:
   - *Observation 1.1 (Defect 1)*: Unpatched `getTileAt(NaN, y)` returned `undefined`.
   - *Logic*: In M3, `isPositionBlocked(wx, wy, radius)` checks:
     ```javascript
     const tile = window.getTileAt(Math.floor(wx), Math.floor(wy));
     const isImpassable = (tile === 0 || tile === 2 || tile === 3 || tile === 9 || tile === 10 || tile === 19);
     ```
     If velocity normalization divides by zero (`vx / Math.hypot(0, 0)`), `wx` becomes `NaN`. Under unpatched code, `tile = undefined`. Since `undefined !== 2`, `isImpassable` evaluates to `false` (NOT BLOCKED)! The player's coordinate becomes `NaN`, vanishing from the map and causing camera glitch.
   - *Impact*: With Fix 1, `getTileAt(NaN, wy)` returns `2 (WALL)`. `isPositionBlocked` returns `true` (BLOCKED), instantly halting movement and trapping the entity safely at its previous valid position.
   - *Verdict*: **Eliminates Critical Physics Exploit / Bypass**.

2. **Sub-Tile Boundary & Radial Footprint Collision**:
   - *Observation 1.2*: Characters have collision radius $R \approx 0.35$. Bounding box checks sample four corners $(wx \pm R, wy \pm R)$.
   - *Logic*: When an entity approaches the map boundary ($wx = 0.2$), $wx - R = -0.15 \implies \lfloor -0.15 \rfloor = -1$. Fix 1 strictly returns `2 (WALL)` for negative tile coordinates.
   - *Verdict*: **Fully Harmonizes with Dual-Axis Wall Sliding (`resolveMovementWithSliding`)**.

3. **Dynamic Boss Gate Breach Synchronization**:
   - *Observation 1.1 & 1.2*: `test_in_place_boss_gate_mutation` proves `setTileAt(bx, by, 1)` mutates `BOSS_GATE` (10) $\to$ `FLOOR` (1) in place.
   - *Logic*: In M3, `boss_gate_controller.js` unlocks the arena upon reaching the kill quota by invoking `window.setTileAt(bossGate.x, bossGate.y, 1)`.
   - *Impact*: In the very next simulation tick, `isPositionBlocked` queries `getTileAt(bossGate.x, bossGate.y)`, which returns `1 (FLOOR)`. The barrier becomes walkable immediately without server re-fetch or memory reallocation.
   - *Verdict*: **100% Compatible with M3 Specification**.

4. **Terrain Speed Modifiers (PATH, DENSE_TERRAIN, MUD_POOL)**:
   - *Observation from `map_data_types.py:50-59`*: `PATH` (13) gives $+20\%$ speed (cost 0.8), `DENSE_TERRAIN` (14) gives $-30\%$ speed (cost 1.43), `MUD_POOL` (4) gives $-50\%$ speed (cost 2.0).
   - *Logic*: Movement speed calculation in `canvas_renderer.js` checks `getTileAt(player.wx, player.wy)`. With Fix 1, all valid coordinates return exact integer codes, and out-of-bounds/invalid inputs default safely to `2 (WALL)` with $1.0\times$ speed multiplier.
   - *Verdict*: **Directly Compatible**.

5. **Headless Unit Testing & Automation (Fix 2)**:
   - *Observation 1.1 (Defect 2)*: Top-level `window` assignment threw `ReferenceError` when imported in Node.js.
   - *Logic*: Milestone M3 requires `tests/unit/test_tile_collision.py` ($\ge 10$ test cases). Testing tile collision from Python or Node.js requires importing `tile_grid_loader.js` headlessly.
   - *Impact*: Fix 2 binds to `root = typeof window !== "undefined" ? window : (typeof globalThis !== "undefined" ? globalThis : global)`. Tests run with zero boilerplate shimming.
   - *Verdict*: **Essential for M3 Automated Verification**.

---

## 3. Caveats

1. **`test_challenger_tile_grid_stress.js` Test Suite Inversion**:
   - Suite 1 of `test_challenger_tile_grid_stress.js` was written by Challenger M1-2 specifically to assert that unpatched code threw `ReferenceError: window is not defined`.
   - When Fix 2 is applied, direct require in Node.js succeeds cleanly (exit code 0). Therefore, the test in `test_challenger_tile_grid_stress.js:34-49` must be updated to assert that `require()` succeeds without throwing, rather than asserting that it throws.
2. **Read-Only Scope Compliance**:
   - As an Explorer agent, no direct modifications have been made to `tile_grid_loader.js` in this turn. The concrete drop-in implementation has been specified below for immediate application by the implementation worker.

---

## 4. Conclusion & Actionable Fix Specification

**Overall Compatibility Verdict:** **APPROVED WITH ENHANCEMENTS**.  
The fixes proposed by Challenger M1-2 are not only 100% downstream-compatible with Milestone M2 (Rendering) and Milestone M3 (Collision), but are **essential preconditions** to prevent catastrophic render loop crashes (due to `undefined` sprite lookup) and collision bypasses (due to `NaN` coordinate penetration).

### Recommended Drop-In Implementation for `client/webapp/js/engine/tile_grid_loader.js`

The implementation worker should apply the following contiguous updates to `client/webapp/js/engine/tile_grid_loader.js`:

#### Patch A: Upfront Expected Length Validation (`tile_grid_loader.js:40-42`)
```javascript
<<<<
    const encounterCount = byteBuf[13];

    let offset = 16;
====
    const encounterCount = byteBuf[13];
    const gridLength = width * height;
    const minExpectedLength = 16 + poiCount * 3 + encounterCount * 5 + gridLength;
    if (byteBuf.length < minExpectedLength) {
      console.error("[TileGridLoader] Buffer truncated:", byteBuf.length, "needed:", minExpectedLength);
      return null;
    }

    let offset = 16;
>>>>
```

#### Patch B: Robust `getTileAt` & Reactive `setTileAt` (`tile_grid_loader.js:105-138`)
```javascript
<<<<
  getTileAt(tx, ty) {
    const w = window.currentMapWidth || 0;
    const h = window.currentMapHeight || 0;
    const grid = window.currentMapGrid;
    const ix = Math.floor(tx);
    const iy = Math.floor(ty);
    if (!grid || ix < 0 || ix >= w || iy < 0 || iy >= h) {
      return 2; // TileType.WALL
    }
    return grid[iy * w + ix];
  },

  setTileAt(tx, ty, tileCode) {
    const w = window.currentMapWidth || 0;
    const h = window.currentMapHeight || 0;
    const grid = window.currentMapGrid;
    const ix = Math.floor(tx);
    const iy = Math.floor(ty);
    if (grid && ix >= 0 && ix < w && iy >= 0 && iy < h) {
      grid[iy * w + ix] = tileCode;
      return true;
    }
    return false;
  }
};

window.TileGridLoader = TileGridLoader;
window.getTileAt = TileGridLoader.getTileAt;
window.setTileAt = TileGridLoader.setTileAt;

if (typeof module !== "undefined" && module.exports) {
  module.exports = { TileGridLoader, BIOME_CODES };
}
====
  getTileAt(tx, ty) {
    const root = typeof window !== "undefined" ? window : (typeof globalThis !== "undefined" ? globalThis : global);
    const w = root.currentMapWidth || 0;
    const h = root.currentMapHeight || 0;
    const grid = root.currentMapGrid;
    if (!Number.isFinite(tx) || !Number.isFinite(ty)) {
      return 2; // TileType.WALL
    }
    const ix = Math.floor(tx);
    const iy = Math.floor(ty);
    if (!grid || !(ix >= 0 && ix < w && iy >= 0 && iy < h)) {
      return 2; // TileType.WALL
    }
    return grid[iy * w + ix];
  },

  setTileAt(tx, ty, tileCode) {
    const root = typeof window !== "undefined" ? window : (typeof globalThis !== "undefined" ? globalThis : global);
    const w = root.currentMapWidth || 0;
    const h = root.currentMapHeight || 0;
    const grid = root.currentMapGrid;
    if (!Number.isFinite(tx) || !Number.isFinite(ty)) {
      return false;
    }
    const ix = Math.floor(tx);
    const iy = Math.floor(ty);
    if (grid && ix >= 0 && ix < w && iy >= 0 && iy < h) {
      grid[iy * w + ix] = tileCode;
      if (root.TileMapRenderer && typeof root.TileMapRenderer.markChunkDirty === "function") {
        root.TileMapRenderer.markChunkDirty(ix, iy);
      }
      return true;
    }
    return false;
  }
};

const root = typeof window !== "undefined" ? window : (typeof globalThis !== "undefined" ? globalThis : global);
root.TileGridLoader = TileGridLoader;
root.getTileAt = TileGridLoader.getTileAt;
root.setTileAt = TileGridLoader.setTileAt;

if (typeof module !== "undefined" && module.exports) {
  module.exports = { TileGridLoader, BIOME_CODES };
}
>>>>
```

---

## 5. Verification Method

To independently verify downstream compatibility and certify the fixes:

1. **Verify Out-of-Bounds & `NaN` Sanitization**:
   ```bash
   node -e "const { TileGridLoader } = require('./client/webapp/js/engine/tile_grid_loader.js'); TileGridLoader.loadFallbackGrid(10, 10, 1); console.log('NaN:', TileGridLoader.getTileAt(NaN, 0), 'Undef:', TileGridLoader.getTileAt(undefined, 0), 'Null:', TileGridLoader.getTileAt(null, 0), 'OOB:', TileGridLoader.getTileAt(-1, 0));"
   ```
   *Expected Output*: `NaN: 2 Undef: 2 Null: 2 OOB: 2` (All return TileType.WALL).

2. **Verify Node.js Un-shimmed Import (Fix 2)**:
   ```bash
   node -e "require('./client/webapp/js/engine/tile_grid_loader.js'); console.log('Import successful');"
   ```
   *Expected Output*: `Import successful` (Exit code 0).

3. **Verify Cross-Language Binary Compatibility Pytest Suite**:
   ```bash
   pytest tests/unit/test_challenger_m1_2_binary_compat.py -v
   ```
   *Expected Output*: `15 passed` (Exit code 0).

4. **Verify Query Throughput Performance**:
   ```bash
   node -e "const { TileGridLoader } = require('./client/webapp/js/engine/tile_grid_loader.js'); TileGridLoader.loadFallbackGrid(120, 90, 1); const t0 = process.hrtime.bigint(); for(let i=0; i<1000000; i++) TileGridLoader.getTileAt(i%120, (i/120|0)%90); const ms = Number(process.hrtime.bigint()-t0)/1e6; console.log('Throughput:', (1e6/(ms/1000)/1e6).toFixed(2), 'MQPS');"
   ```
   *Expected Output*: Throughput $\ge 50$ million queries/sec.

5. **Code Hygiene & Hard Cap Verification**:
   ```bash
   python tools/lint/check_code_and_doc_hygiene.py --strict
   ```
   *Expected Output*: `tile_grid_loader.js` $\approx 150$ lines ($\le 350$ lines soft cap).
