feedback7 R22-FIELDS: the path tie-break never wades into more 深水区 than the official route — on 战场#08(下半) the patrolling 铳 / 卢西恩 take the official upper road from (3,9) to (2,3) again (GitHub #375); golden: boss-boss_2-pair, coop2-ABYSS-10-boosted
GitHub #375 (FrogThai): 「水图boss会从下面这条路往蓝门走,实际应该从上路走」. The walking leaders of 战场#08(下半) 涨潮控制 (act2autochess_m04) are 铳 (level_act1autochess_h07_02 route 6; the Hidden Core 铳 on act1autochess_h08_02's patrol routes) and 卢西恩 (act2autochess_h07_05 route 8): PATROL_MOVE (3,9) → (2,3) → (5,9). Measured (notes repro-375.mjs): ours 「3,9 2,9 2,6 4,6 4,4 2,4 2,3」 — the lower road through the 深水区 (3,6) / (4,6) — against the official SPFA 「3,9 5,9 5,4 2,4 2,3」 along row 5 with one 深水区 (5,6); the mirrored (3,11) → (2,17) split the same way. Cause: grid.js _buildField takes the blockable-ground preference pointer (user playtest #2) when its route crosses strictly fewer non-blockable tiles; 深水区 is non-blockable itself (no deployment), so the lower road counted 2 against the upper road's 3 (two floor tiles and one water tile) and won. Official pathfinding is the plain 4-neighbour SPFA (research 08 §3.1). Now the preference pointer is taken only when its route also crosses no more 深水区 than the official pointer's (a per-tile `deep` count beside `cost`, start excluded). Rule from the Grok triage (scratch-i1b/375.patch), re-implemented with a precomputed Grid.deepWater mask (terrain 'deepsea': tile_deepsea and tile_deepwater). Scope (notes dump-fields.mjs / check-official.mjs, every stage × normal / 联防 / boss rect × every walkable destination): 48 fields change, only on the two water maps (act2 m04 boss field; the inactive act1 m05), 149 pointers, every one back to the official pointer. The 21 accepted DEVIATIONS (战场#01 / #02 floor, #07, the inactive #05's 7 water-avoiding routes) are unchanged, as are all 154 audited gate routes and data/stages.json's groundPaths (tools/build-data.mjs --offline into a scratch dir: every data file identical). Other boss-field preference routes of m04 that cross fewer 深水区 than the official one stay. Tests: test/sim/pathing-official.test.js — two new #375 tests (the four patrol legs equal the pure official waypoints; in a real battle the 铳 climbs to row 5 and steps on the 深水区 (5,6) only), both failing on the parent; test/sim/pathing-crosscheck.test.js — the independent reference port gets the same 深水区 rule (it flagged both the patrol leg and a random-crate normal-field route of act2 m04 until then); test/sim/pathing-blockable.test.js — the WALK-leg audit compares with the equal-length routes that cross no more 深水区 than ours (the 14 accepted arena exceptions unchanged). Grid-related suites 777/777. Golden: - fields boss-boss_2-pair (铳 pair on act2autochess_m04, act1autochess_h07_02): the two 铳 patrol the upper roads; leader left 5311560 → 5301010, p1 damage up / p2 down, deaths 0 / 2 → 1 / 3; still 8 / 8 kills. - matches coop2-ABYSS-10-boosted (act2 m04; Final Assault 卢西恩, Hidden Core 铳): still cleared, team LP 800 → 615. The bots' operators stand on the lower rows (the same tiles as before); with the leaders on the upper road the Final Assault lasts 335 game s instead of 247 and the Hidden Core 638 instead of 190, both past the 150 real-second mark: the overtime drain takes 17 + 168 LP (none before), no leak.
This commit is contained in:
1 parent
f14b3a062f
commit
8d896e40ea
10 files changed
+167
-86
No files matched your search
@@ -1,7 +1,8 @@
|
||||
// Blockable-ground preference of the ground pathing (grid.js header; user playtest #2: on 战场#01 the lower-gate enemies
|
||||
// walked up the col-9 floor lane, where no operator can stand, instead of the col-8 road). The flow field keeps the
|
||||
// official route unless the preference route — the fewest non-blockable tiles among equal-length chains, smoothing
|
||||
// that never cuts across floor its grid route does not walk — crosses fewer non-blockable tiles. "Crosses" = the
|
||||
// that never cuts across floor its grid route does not walk — crosses fewer non-blockable tiles and no more 深水区
|
||||
// (GitHub #375). "Crosses" = the
|
||||
// segment passes through the tile's interior (grid.js segmentTiles; brushing a corner is not crossing — the D5 report
|
||||
// after 0.1.0, test/sim/pathing-official.test.js). Audit: every active stage × gate × field (normal, 联防 both halves,
|
||||
// boss both halves incl. the solo `_s` templates) — the non-blockable tiles an enemy still crosses are listed and
|
||||
@@ -69,27 +70,36 @@ function nbCrossed(g, s, e) {
|
||||
|
||||
/**
|
||||
* Fewest non-blockable tiles (start and goal excluded) over all 4-connected crate-free routes s → e of length ≤ the
|
||||
* shortest + `slack`; { best, min } (min = the minimum over those lengths).
|
||||
* shortest + `slack` that cross at most `maxWet` 深水区 tiles (grid.js: the tie-break never walks into more 深水区 than
|
||||
* the official route — GitHub #375); { best, min } (min = the minimum over those lengths).
|
||||
*/
|
||||
function fewestNb(g, s, e, slack = 2) {
|
||||
function fewestNb(g, s, e, slack = 2, maxWet = Infinity) {
|
||||
const f = g.flowField(e[0], e[1]);
|
||||
const best = f.dist[s[0] * COLS + s[1]];
|
||||
if (best < 0 || best >= 1000) return null;
|
||||
const ek = e[0] * COLS + e[1];
|
||||
const cost = (k) => (k === ek ? 0 : g.unblockable[k]);
|
||||
let cur = new Map([[s[0] * COLS + s[1], 0]]);
|
||||
const wetOf = Number.isFinite(maxWet) ? (k) => (k === ek ? 0 : g.deepWater[k]) : () => 0;
|
||||
// tile → (深水区 so far → fewest non-blockable so far)
|
||||
let cur = new Map([[s[0] * COLS + s[1], new Map([[0, 0]])]]);
|
||||
let min = Infinity;
|
||||
for (let L = 0; L <= best + slack; L++) {
|
||||
if (cur.has(ek)) min = Math.min(min, cur.get(ek));
|
||||
if (cur.has(ek)) for (const p of cur.get(ek).values()) min = Math.min(min, p);
|
||||
const nx = new Map();
|
||||
for (const [k, p] of cur) {
|
||||
for (const [k, byWet] of cur) {
|
||||
if (k === ek) continue;
|
||||
const r = (k / COLS) | 0, c = k % COLS;
|
||||
for (const [dr, dc] of [[1, 0], [0, 1], [-1, 0], [0, -1]]) {
|
||||
const nr = r + dr, nc = c + dc;
|
||||
if (!g.walkable(nr, nc) || g.isCrate(nr, nc)) continue;
|
||||
const nk = nr * COLS + nc, np = p + cost(nk);
|
||||
if (!nx.has(nk) || nx.get(nk) > np) nx.set(nk, np);
|
||||
const nk = nr * COLS + nc;
|
||||
let m = nx.get(nk);
|
||||
for (const [w, p] of byWet) {
|
||||
const nw = w + wetOf(nk), np = p + cost(nk);
|
||||
if (nw > maxWet) continue;
|
||||
if (!m) nx.set(nk, m = new Map());
|
||||
if (!m.has(nw) || m.get(nw) > np) m.set(nw, np);
|
||||
}
|
||||
}
|
||||
}
|
||||
cur = nx;
|
||||
@@ -97,6 +107,14 @@ function fewestNb(g, s, e, slack = 2) {
|
||||
return { best, min };
|
||||
}
|
||||
|
||||
/** 深水区 tiles crossed from s to e (start and goal excluded). */
|
||||
function wetCrossed(g, s, e) {
|
||||
const wp = g.waypoints(s[0], s[1], e[0], e[1]);
|
||||
const seen = new Set();
|
||||
for (const [r, c] of crossed(wp)) if (!(r === s[0] && c === s[1]) && !(r === e[0] && c === e[1]) && g.deepWater[r * COLS + c]) seen.add(`${r},${c}`);
|
||||
return seen.size;
|
||||
}
|
||||
|
||||
/** Tiles an enemy of `route` visits (rounded position per tick) and whether `u` blocked it. */
|
||||
function walk(h) {
|
||||
const seen = [];
|
||||
@@ -293,12 +311,15 @@ test('audit: every WALK leg of every wave template (normal / 联防 / boss / hid
|
||||
if (!grids.has(key)) grids.set(key, stageGrid(sid, rect));
|
||||
const g = grids.get(key);
|
||||
if (!g.walkable(from[0], from[1]) || !g.walkable(to[0], to[1])) continue;
|
||||
const eq = fewestNb(g, from, to, 0);
|
||||
if (!eq) continue;
|
||||
if (!fewestNb(g, from, to, 0)) continue;
|
||||
const got = nbCrossed(g, from, to);
|
||||
// compared with the routes that wade through no more 深水区 than ours (#375: 战场#08(下半)'s patrol legs to (2,3) /
|
||||
// (2,17) keep the official upper road, not the lower one through two water tiles that crosses one floor tile less)
|
||||
const wet = wetCrossed(g, from, to);
|
||||
const eq = fewestNb(g, from, to, 0, wet);
|
||||
const what = `${sid} ${id} (${from}) → (${to}) crosses ${got.join(' ')}`;
|
||||
assert.ok(got.length <= eq.min, `${what}, an equal-length route only ${eq.min}`);
|
||||
const near = fewestNb(g, from, to, 2);
|
||||
const near = fewestNb(g, from, to, 2, wet);
|
||||
if (near.min < got.length) {
|
||||
assert.ok(rect === GEO.BOSS_RECT && got.every(arena), `${what}: a route of length ≤ ${near.best}+2 crosses only ${near.min}`);
|
||||
accepted.push(what);
|
||||
|
||||
Reference in new issue
Block a user