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
-1
@@ -442,7 +442,7 @@ as official level data, and sim tests use them as a plain two-halves road.
|
||||
| `special` | `{"mire":{"intervalSec":1,"aspdPerStack":-0.05,"moveMulPerStack":-0.05,"maxStacks":10,"heavyWeight":3,…}}`, `{"infection":{"bb":{"damage":70,"atk":0.2,"attack_speed":20,"duration":300}}}`, `{"deepsea":{"skillId":"sktok_tidectrl_3","bb":{…}}}`, `{"smog":{"rule":"operatorsNotTargetableByEnemyRanged"}}`, `{"blower":{…}}` | special-terrain parameters |
|
||||
| `runes[]`, `globalBuffs[]` | `{"key":"map_tile_blackb_assign","bb":{…},"bbStr":{…}}` | level runes (act2 m03 reed rune has no reed tiles — leftover) |
|
||||
| `deployTiles` | `{"normal":{"melee":[[9,3],…],"rangedOnly":[[10,4],…],"changedByDevices":[…]},"bossLeft":{…},"bossRight":{…}}` | deployable tiles at match start: `melee` = LOW (effective) buildable ALL/MELEE (ranged may use them too) — and a 深水区 under an active 特制水上平台 (`waterPlatform`, "在水上建立可以部署任意单位的平台"; act1 m05 only — the prep's deploy map only: the sim does not model the canoes, so its Grid keeps those tiles NONE, automatic placements avoid them and the path tie-break counts them as non-blockable [ASSUMED, weight 0 this season]) —, `rangedOnly` = high ground / active platforms; tiles under active crates/mounds and the 深水区 excluded. `bossLeft` / `bossRight` (field coordinates, boss rows 1–5) are what a player may deploy on in a boss round's prep: board (r, c) = boss (r − 7, c) / the right half mirrored (r − 7, 20 − c) (server/match/board.js `field`, user playtest #5 item 7) |
|
||||
| `groundPaths` / `groundPathsWithDevices` | `{"12,10->9,2":[[12,10],[12,9],…,[12,3],[11,3],[10,3],[9,3],[9,2]]}` | the ground route of the sim's own `server/sim/grid.js` flow field (research 08 §3.4: 4-direction SPFA from the goal, crates cost 1000, Bresenham line-of-sight smoothing — official route lengths — kept unless the remake's (0.1.0's) road-over-floor preference route crosses strictly fewer non-blockable floor / gate / 深水区 tiles — the fewest among equal-length chains, smoothing that never cuts across floor its grid route does not walk; a corner touch does not count as a crossing — e.g. 战场#01's lower gate takes the col-8 road, not the col-9 floor lane, while 战场#04's keeps the official diagonal from row 9 into row 10; DESIGN §14 corrections, test/sim/pathing-official.test.js) as the tiles an enemy crosses, over the whole map, without / with the match-start devices (crates cost 1000, platforms / mounds blocking [ASSUMED]); the battle re-reads its flow fields live |
|
||||
| `groundPaths` / `groundPathsWithDevices` | `{"12,10->9,2":[[12,10],[12,9],…,[12,3],[11,3],[10,3],[9,3],[9,2]]}` | the ground route of the sim's own `server/sim/grid.js` flow field (research 08 §3.4: 4-direction SPFA from the goal, crates cost 1000, Bresenham line-of-sight smoothing — official route lengths — kept unless the remake's (0.1.0's) road-over-floor preference route crosses strictly fewer non-blockable floor / gate / 深水区 tiles and no more 深水区 (GitHub #375) — the fewest among equal-length chains, smoothing that never cuts across floor its grid route does not walk; a corner touch does not count as a crossing — e.g. 战场#01's lower gate takes the col-8 road, not the col-9 floor lane, while 战场#04's keeps the official diagonal from row 9 into row 10; DESIGN §14 corrections, test/sim/pathing-official.test.js) as the tiles an enemy crosses, over the whole map, without / with the match-start devices (crates cost 1000, platforms / mounds blocking [ASSUMED]); the battle re-reads its flow fields live |
|
||||
| `options`, `config` | `{"characterLimit":8,"moveMultiplier":0.5}`, `{…configBlackBoard strings…}` | |
|
||||
|
||||
Glyph legend (`rows`):
|
||||
|
||||
+3
-1
@@ -215,7 +215,9 @@ WALK legs pathfind on the stage grid inside the rect with the official flow fiel
|
||||
destination, crates cost 1000, then Bresenham line-of-sight smoothing — research 08 §3.4). The official route stays
|
||||
unless 0.1.0's road-over-floor preference route (the fewest non-blockable tiles among equal-length chains, a line of
|
||||
sight that never covers floor — diagonal-step corners included — its grid route does not walk; user playtest #2 item 2)
|
||||
crosses strictly fewer non-blockable tiles (floor / gate lanes); "crosses" = passes through the tile's interior, a
|
||||
crosses strictly fewer non-blockable tiles (floor / gate lanes) and no more 深水区 (GitHub #375: on 战场#08(下半) the
|
||||
patrolling 铳 / 卢西恩 waded through two water tiles to dodge one floor tile; the official upper road crosses one);
|
||||
"crosses" = passes through the tile's interior, a
|
||||
corner touch does not count (`grid.js` `segmentTiles`; community report D5 after 0.1.0: 战场#04's lower-gate enemies cut
|
||||
diagonally from row 9 into row 10 as officially), and on equal counts the official route stays.
|
||||
test/sim/pathing-official.test.js lists the 21 of 154 stage routes that still differ from the official ones (战场#01's
|
||||
|
||||
@@ -95,7 +95,7 @@ Piece = { uid, kind: 'chess'|'item'|'token', id, golden: bool, tier, items: [ {
|
||||
|
||||
**Corrections from research 08/09 (normative):**
|
||||
- Waves: official generator (research 08 §2): 3 of 6 types, each owns exactly 3 of the 15 round slots, SPECIAL the rest (shuffled per match); per round one type + one weighted special entry (half by round); each template SPAWN **action** becomes `clamp(roundHalfEven(n·P(t)/P(new)), 1, 5)` units over the same window; placeholder actions of the other movement class are skipped. Stage per match from the mode's list (标准 = 战场#01 only, §6.5). No custom balance multipliers (`data/tuning.json` keeps only titles); co-op boss pool = `bloodPoint[difficulty]` unscaled.
|
||||
- Ground pathing: official SPFA-style 4-direction search from the goal with crates as cost-1000 tiles, then straight-line smoothing (research 08 §3.4). act1 m02 crates #001–#004 inactive in 下半. Remake refinement (user playtest #2, narrowed by §21.15): route lengths and routes stay the official ones unless 0.1.0's road-over-floor preference route crosses strictly fewer non-blockable (floor / gate / 深水区) tiles — a corner touch is not a crossing; ties keep the official route. E.g. 战场#01's lower-gate enemies take the col-8 road, while 战场#04's keep the official diagonal from row 9 into row 10 (server/sim/grid.js header; test/sim/pathing-official.test.js, pathing-blockable.test.js).
|
||||
- Ground pathing: official SPFA-style 4-direction search from the goal with crates as cost-1000 tiles, then straight-line smoothing (research 08 §3.4). act1 m02 crates #001–#004 inactive in 下半. Remake refinement (user playtest #2, narrowed by §21.15): route lengths and routes stay the official ones unless 0.1.0's road-over-floor preference route crosses strictly fewer non-blockable (floor / gate / 深水区) tiles and no more 深水区 (GitHub #375) — a corner touch is not a crossing; ties keep the official route. E.g. 战场#01's lower-gate enemies take the col-8 road, while 战场#04's keep the official diagonal from row 9 into row 10 (server/sim/grid.js header; test/sim/pathing-official.test.js, pathing-blockable.test.js).
|
||||
- Observing: no watching while your own normal battle runs; after it ends (and in prep) tap a teammate → 前往查看 (local replica, fast-forwarded). ‹ › only inside 联防/FA fields; FA: own pair only; eliminated: free.
|
||||
- Prep interactions: tap a unit → underframe with 出售 +1 (and 撤退 on board); items/Arts → 销毁; shop and upgrade are two-tap (select → confirm; the remake's 机变 cards too, §18.2); strategy draft forbids a strategy already taken by a teammate (队友已选).
|
||||
- Emotes: 6 themes × 6 battle emotes (research 09 §4.2), image-only bubbles beside the sender avatar, 1 s cooldown, 3 s bubble; `g.emote {id}` with the official emoji id.
|
||||
|
||||
@@ -352,8 +352,9 @@ reported the 战场#01 lower-gate enemies walking up the col-9 floor lane, where
|
||||
`server/sim/grid.js` keeps every official route LENGTH and the official route itself unless the 2026-09-29
|
||||
road-over-floor preference route (equal-length ties to the chain with the fewest non-blockable floor / gate tiles,
|
||||
smoothing that never cuts across floor its grid route does not walk, the corner tiles of a diagonal step included)
|
||||
crosses strictly fewer non-blockable tiles; in that comparison a segment that only touches a floor tile's corner does
|
||||
not cross it. Used alone (0.1.0) the preference also bent official diagonals into L shapes — the D5 report after
|
||||
crosses strictly fewer non-blockable tiles and no more 深水区 (GitHub #375, 0.2.2: the 深水区 is non-blockable too, and
|
||||
on 战场#08(下半) the patrolling leaders had waded through two water tiles to dodge one floor tile); in that comparison a
|
||||
segment that only touches a floor tile's corner does not cross it. Used alone (0.1.0) the preference also bent official diagonals into L shapes — the D5 report after
|
||||
0.1.0: on 战场#04 the lower-gate enemies walked (9,10) → (9,8) → (10,8) instead of the official diagonal (9,10) →
|
||||
(10,7), which only brushes the corner of the floor (10,9). Lanes that still differ from the table above (21 of the 154
|
||||
stage × gate × field routes, test/sim/pathing-official.test.js):
|
||||
|
||||
+22
-11
@@ -26,8 +26,7 @@
|
||||
// 0.1.0: on 战场#04 活性源石 the lower-gate enemies must cross diagonally from row 9 into row 10 as officially, not
|
||||
// walk two tiles left of the gate and then straight up). A tile is non-blockable when it is walkable but not LOW
|
||||
// ground buildable for melee (floor / gate / goal / teleport / 深水区 tiles — `blockable()` false; no gate route of 战场#08
|
||||
// changed when its 深水区 became non-buildable, a few off-route smoothed steps of its boss field, rows 4–5, go around the
|
||||
// water instead of cutting across it). Each field holds two candidate
|
||||
// changed when its 深水区 became non-buildable). Each field holds two candidate
|
||||
// pointers per tile, both with the official `dist` (so the crate cost 1000 and every grid route length stay official):
|
||||
// * official: the plain algorithm above;
|
||||
// * preference (0.1.0's): the SPFA relaxes on (dist, pen) lexicographically, `pen` = non-blockable tiles on the chain
|
||||
@@ -36,7 +35,10 @@
|
||||
// only on the tile's own raw chain between its two ends, so it never slips past a road tile beside floor its grid
|
||||
// route does not walk.
|
||||
// `next[tile]` is the official pointer unless the preference pointer leads to a route CROSSING strictly fewer
|
||||
// non-blockable tiles (`cost`, counted to the destination in increasing `dist` order). A segment crosses the tiles
|
||||
// non-blockable tiles (`cost`, counted to the destination in increasing `dist` order) and no more 深水区 (`deep`): 深水区
|
||||
// is non-blockable itself, so without that test the preference waded into the pool to dodge floor (GitHub #375: on
|
||||
// 战场#08(下半) the patrolling 铳 / 卢西恩 took the lower road (3,9) → (2,3) through two water tiles, one floor tile fewer
|
||||
// than the official upper road with one). A segment crosses the tiles
|
||||
// whose interior it passes through (`crossTiles`, exact geometry); one it only touches at a corner point is not crossed
|
||||
// — the first version counted the corner tiles there too and so bent official diagonals into L shapes (D5). On equal
|
||||
// counts the official pointer stays, except where the preference one merely skips it (the official waypoint lies on
|
||||
@@ -166,9 +168,12 @@ export class Grid {
|
||||
}
|
||||
/** 1 = walkable terrain an operator cannot block on (floor, gate, goal, teleport…): the path tie-break penalty */
|
||||
this.unblockable = new Uint8Array(ROWS * COLS);
|
||||
/** 1 = walkable 深水区 (terrain 'deepsea': tile_deepsea / tile_deepwater): the tie-break never walks into more of it */
|
||||
this.deepWater = new Uint8Array(ROWS * COLS);
|
||||
for (let k = 0; k < ROWS * COLS; k++) {
|
||||
const t = this.tiles[k];
|
||||
this.unblockable[k] = t.pass === 'ALL' && !isBlockableTile(t) ? 1 : 0;
|
||||
this.deepWater[k] = t.pass === 'ALL' && t.terrain === 'deepsea' ? 1 : 0;
|
||||
}
|
||||
/** obstacle bits per tile (OB_BLOCK | OB_CRATE) */
|
||||
this.obstacle = new Uint8Array(ROWS * COLS);
|
||||
@@ -272,10 +277,10 @@ export class Grid {
|
||||
|
||||
_buildField(er, ec, allowDiagonal, ignore) {
|
||||
const N = ROWS * COLS;
|
||||
const f = { dest: -1, dist: null, parent: null, pen: null, next: null, official: null, cost: null, len: null, version: this.version, allowDiagonal, ignore };
|
||||
const f = { dest: -1, dist: null, parent: null, pen: null, next: null, official: null, cost: null, deep: null, len: null, version: this.version, allowDiagonal, ignore };
|
||||
if (!this.inBounds(er, ec)) {
|
||||
const none = new Int32Array(N).fill(-1);
|
||||
return Object.assign(f, { dist: none, parent: none, next: none, official: none, pen: new Int32Array(N), cost: new Int32Array(N) });
|
||||
return Object.assign(f, { dist: none, parent: none, next: none, official: none, pen: new Int32Array(N), cost: new Int32Array(N), deep: new Int32Array(N) });
|
||||
}
|
||||
const dest = er * COLS + ec;
|
||||
f.dest = dest;
|
||||
@@ -300,25 +305,30 @@ export class Grid {
|
||||
(n) => { for (let x = n, g = N; x >= 0 && g-- > 0; x = pref.parent[x]) onChain[x] = n; });
|
||||
// per tile, in increasing distance: the official pointer unless the preference one crosses strictly fewer
|
||||
// non-blockable tiles on the way to the destination — or as few while only skipping the official waypoint o, which
|
||||
// then lies on the straight line to it and leads there (the same walk, one waypoint fewer)
|
||||
// then lies on the straight line to it and leads there (the same walk, one waypoint fewer) — and no more 深水区
|
||||
// than the official one (GitHub #375)
|
||||
const order = [];
|
||||
for (let k = 0; k < N; k++) if (dist[k] >= 0) order.push(k);
|
||||
order.sort((a, b) => dist[a] - dist[b] || a - b);
|
||||
const through = (a, b) => { let n = -unb[a]; crossTiles(a, b, (r, c) => { n += unb[r * COLS + c]; }); return n; };
|
||||
const deepW = this.deepWater;
|
||||
const wet = (a, b) => { let n = -deepW[a]; crossTiles(a, b, (r, c) => { n += deepW[r * COLS + c]; }); return n; };
|
||||
const next = new Int32Array(N).fill(-1);
|
||||
const cost = new Int32Array(N);
|
||||
const deep = new Int32Array(N);
|
||||
for (const k of order) {
|
||||
const o = nextO[k], p = nextP[k];
|
||||
if (o < 0) continue; // the destination
|
||||
let use = o, best = through(k, o) + cost[o];
|
||||
let use = o, best = through(k, o) + cost[o], bestWet = wet(k, o) + deep[o];
|
||||
if (p >= 0 && p !== o) {
|
||||
const cp = through(k, p) + cost[p];
|
||||
if (cp < best || (cp === best && next[o] === p && onSegment(k, o, p))) { use = p; best = cp; }
|
||||
const cp = through(k, p) + cost[p], wp = wet(k, p) + deep[p];
|
||||
if (wp <= bestWet && (cp < best || (cp === best && next[o] === p && onSegment(k, o, p)))) { use = p; best = cp; bestWet = wp; }
|
||||
}
|
||||
next[k] = use;
|
||||
cost[k] = best;
|
||||
deep[k] = bestWet;
|
||||
}
|
||||
return Object.assign(f, { dist, parent: pref.parent, pen: pref.pen, next, official: nextO, cost });
|
||||
return Object.assign(f, { dist, parent: pref.parent, pen: pref.pen, next, official: nextO, cost, deep });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -434,8 +444,9 @@ export class Grid {
|
||||
* @property {Int32Array} pen non-blockable tiles on that raw chain (tile included, destination excluded)
|
||||
* @property {Int32Array} official smoothed parent of the pure official algorithm
|
||||
* @property {Int32Array} next the tile to walk straight toward (official, or the preference pointer when it crosses fewer
|
||||
* non-blockable tiles — see the module header)
|
||||
* non-blockable tiles and no more 深水区 — see the module header)
|
||||
* @property {Int32Array} cost non-blockable tiles the route of `next` crosses from the tile (excluded) to the destination
|
||||
* @property {Int32Array} deep 深水区 tiles the route of `next` crosses from the tile (excluded) to the destination
|
||||
* @property {Float64Array|null} len memoised smoothed length (fieldLength)
|
||||
*/
|
||||
|
||||
|
||||
+48
-48
@@ -142,75 +142,75 @@
|
||||
"killed": 8,
|
||||
"total": 8,
|
||||
"unspawned": 0,
|
||||
"bossHpLeft": 5311560,
|
||||
"rngDraws": 839,
|
||||
"bossHpLeft": 5301010,
|
||||
"rngDraws": 783,
|
||||
"errors": {"count":0,"kinds":[]},
|
||||
"players": {
|
||||
"p1": {"killed":8,"total":8,"perfect":true,"leaks":0,"leaksUncounted":0,"coins":0,"dmg":612448,"bossDmg":313008,"heal":16689,"deaths":0,"layerGains":{}},
|
||||
"p2": {"killed":0,"total":0,"perfect":true,"leaks":0,"leaksUncounted":0,"coins":0,"dmg":937710,"bossDmg":372608,"heal":6945,"deaths":2,"layerGains":{}}
|
||||
"p1": {"killed":8,"total":8,"perfect":true,"leaks":0,"leaksUncounted":0,"coins":0,"dmg":741043,"bossDmg":366117,"heal":19816,"deaths":1,"layerGains":{}},
|
||||
"p2": {"killed":0,"total":0,"perfect":true,"leaks":0,"leaksUncounted":0,"coins":0,"dmg":819707,"bossDmg":330056,"heal":4781,"deaths":3,"layerGains":{}}
|
||||
},
|
||||
"units": [
|
||||
["p1",1,"op","chess_char_5_19_b",2,"uniequip_002_mlynar",3907,1,9,0,183438,1132,0,53,3],
|
||||
["p1",2,"op","chess_char_4_01_b",2,"uniequip_002_rmixer",5482.35,1,11,0,3903,11319,305,35,1],
|
||||
["p1",3,"op","chess_char_5_14_b",1,"none",2174,1,7,0,30509,953,294,58,17],
|
||||
["p1",4,"op","chess_char_5_17_b",1,"uniequip_002_f12yin",2738,1,13,0,10026,0,0,66,1],
|
||||
["p1",5,"op","chess_char_5_15_b",1,"uniequip_002_thorn2",1170,1,1,0,79315,0,168,32,8],
|
||||
["p1",6,"op","chess_char_5_20_b",2,"uniequip_002_aglina",2418.5,1,3,0,78933,0,0,44,4],
|
||||
["p1",7,"op","chess_char_5_21_b",1,"uniequip_002_sntlla",2769.3,1,17,1,26033,0,0,43,2],
|
||||
["p1",8,"op","chess_char_5_22_b",1,"uniequip_002_nymph",1611,1,19,2,168011,1748,1748,55,18],
|
||||
["p1",9,"op","chess_char_5_23_b",1,"uniequip_002_reckpr",1610,1,5,0,0,0,14119,21,3],
|
||||
["p1",10,"op","chess_char_4_02_b",2,"uniequip_002_mostma",1695,1,15,0,32280,1383,55,35,2],
|
||||
["p2",11,"op","chess_char_4_11_b",1,"uniequip_002_cathy",836.62,1,12,0,0,3919,1500,0,2],
|
||||
["p2",12,"op","chess_char_4_04_b",1,"uniequip_002_ines",3545.85,1,51,2,253580,496,458,287,8],
|
||||
["p2",13,"op","chess_char_4_07_b",1,"uniequip_002_bpipe",2099.31,1,8,0,4500,1149,865,24,8],
|
||||
["p2",14,"op","chess_char_4_16_b",2,"uniequip_002_texas2",2671.35,1,52,1,112288,6746,1602,147,13],
|
||||
["p2",15,"op","chess_char_4_09_b",1,"uniequip_002_mizuki",2777.6,1,10,0,62072,0,0,54,3],
|
||||
["p2",16,"op","chess_char_4_06_b",1,"uniequip_002_kroos2",1561,1,2,1,303642,0,0,181,3],
|
||||
["p1",1,"op","chess_char_5_19_b",2,"uniequip_002_mlynar",3907,1,9,0,235262,2487,117,74,3],
|
||||
["p1",2,"op","chess_char_4_01_b",2,"uniequip_002_rmixer",5482.35,1,11,0,12166,13761,318,33,1],
|
||||
["p1",3,"op","chess_char_5_14_b",1,"none",2174,1,7,0,24813,953,148,16,12],
|
||||
["p1",4,"op","chess_char_5_17_b",1,"uniequip_002_f12yin",2738,1,13,0,5871,0,0,20,1],
|
||||
["p1",5,"op","chess_char_5_15_b",1,"uniequip_002_thorn2",1170,1,1,0,123408,0,5557,77,10],
|
||||
["p1",6,"op","chess_char_5_20_b",2,"uniequip_002_aglina",2418.5,1,3,0,81805,0,0,44,4],
|
||||
["p1",7,"op","chess_char_5_21_b",1,"uniequip_002_sntlla",2769.3,1,17,2,56882,1768,780,100,3],
|
||||
["p1",8,"op","chess_char_5_22_b",1,"uniequip_002_nymph",737.6,1,43,1,153989,3270,350,46,16],
|
||||
["p1",9,"op","chess_char_5_23_b",1,"uniequip_002_reckpr",1610,1,5,0,0,0,12546,19,3],
|
||||
["p1",10,"op","chess_char_4_02_b",2,"uniequip_002_mostma",1695,1,15,1,46847,0,0,34,2],
|
||||
["p2",11,"op","chess_char_4_11_b",1,"uniequip_002_cathy",2172.64,1,50,0,0,5191,1078,0,1],
|
||||
["p2",12,"op","chess_char_4_04_b",1,"uniequip_002_ines",3545.85,1,51,2,259014,0,0,279,8],
|
||||
["p2",13,"op","chess_char_4_07_b",1,"uniequip_002_bpipe",2383,1,52,0,3678,2726,343,18,7],
|
||||
["p2",14,"op","chess_char_4_16_b",2,"uniequip_002_texas2",2671.35,1,49,1,97836,2233,1258,153,10],
|
||||
["p2",15,"op","chess_char_4_09_b",1,"uniequip_002_mizuki",2777.6,1,10,0,82406,0,0,73,4],
|
||||
["p2",16,"op","chess_char_4_06_b",1,"uniequip_002_kroos2",1561,1,2,0,179138,0,0,231,3],
|
||||
["p2",17,"op","chess_char_4_10_b",1,"uniequip_002_aroma",1664,1,4,0,0,0,0,0,0],
|
||||
["p2",18,"op","chess_char_4_12_b",2,"uniequip_002_glady",1418.27,1,18,1,69028,3331,2517,58,3],
|
||||
["p2",19,"op","chess_char_4_13_b",1,"uniequip_002_gnosis",1989,1,40,0,44456,1991,2,63,18],
|
||||
["p2",20,"op","chess_char_4_14_b",1,"uniequip_002_lionhd",1626,1,6,0,88144,0,0,57,14],
|
||||
["p2",18,"op","chess_char_4_12_b",2,"uniequip_002_glady",1003.26,1,18,0,72377,3328,2099,61,4],
|
||||
["p2",19,"op","chess_char_4_13_b",1,"uniequip_002_gnosis",1989,1,40,0,62819,1991,2,81,22],
|
||||
["p2",20,"op","chess_char_4_14_b",1,"uniequip_002_lionhd",1626,1,6,1,62440,0,0,47,13],
|
||||
["p2",21,"token","token_10041_cathy_catsld",null,null,100,1,21,0,0,0,0,0,null],
|
||||
["p2",22,"token","token_10041_cathy_catsld",null,null,100,1,22,0,0,0,0,0,null]
|
||||
],
|
||||
"enemies": {"enemy_1005_yokai_2":[6,6,0,3179,62190],"enemy_1430_lrrook":[2,2,0,0,802449],"enemy_9017_achunt":[2,0,0,30987,688440]},
|
||||
"hooks": [1,50,6000,1312,1325,2789,2834,4850,11,26,131,125,21,35636,978,0,10,0,1,11,0,13,1],
|
||||
"events": {"atk":1342,"deploy":40,"die":26,"dmg":2826,"engage":17,"fx":602,"heal":43,"skill":256,"spawn":32,"status":489},
|
||||
"enemies": {"enemy_1005_yokai_2":[6,6,0,3956,62190],"enemy_1430_lrrook":[2,2,0,0,802449],"enemy_9017_achunt":[2,0,0,33752,698990]},
|
||||
"hooks": [1,50,6000,1371,1409,2907,2944,3285,13,26,127,123,21,38164,1047,0,10,0,1,13,0,12,1],
|
||||
"events": {"atk":1410,"deploy":40,"die":26,"dmg":2934,"engage":17,"fx":696,"heal":80,"skill":250,"spawn":32,"status":549},
|
||||
"fx": {
|
||||
"aoe": 38,
|
||||
"aoe": 33,
|
||||
"appear": 2,
|
||||
"blink": 16,
|
||||
"burst": 13,
|
||||
"blink": 14,
|
||||
"burst": 12,
|
||||
"catShield": 1,
|
||||
"charge": 8,
|
||||
"charge": 7,
|
||||
"counter": 21,
|
||||
"crit": 88,
|
||||
"dilemma": 3,
|
||||
"crit": 89,
|
||||
"dilemma": 4,
|
||||
"disappear": 2,
|
||||
"dodge": 1,
|
||||
"explosion": 14,
|
||||
"frostNova": 18,
|
||||
"explosion": 13,
|
||||
"frostNova": 22,
|
||||
"garrisonGrant": 1,
|
||||
"iceSpike": 39,
|
||||
"overclock": 2,
|
||||
"pull": 3,
|
||||
"iceSpike": 89,
|
||||
"overclock": 1,
|
||||
"pull": 4,
|
||||
"quadShot": 2,
|
||||
"ripple": 13,
|
||||
"ripple": 20,
|
||||
"sentry": 6,
|
||||
"sermon": 1,
|
||||
"shadowWeave": 11,
|
||||
"shield": 6,
|
||||
"soul": 5,
|
||||
"sp": 11,
|
||||
"shadowWeave": 10,
|
||||
"shield": 5,
|
||||
"soul": 4,
|
||||
"sp": 7,
|
||||
"stealth": 8,
|
||||
"swordRain": 96,
|
||||
"swordStorm": 13,
|
||||
"tornado": 3,
|
||||
"tornadoPulse": 15,
|
||||
"swordRain": 82,
|
||||
"swordStorm": 10,
|
||||
"tornado": 4,
|
||||
"tornadoPulse": 20,
|
||||
"weightless": 4,
|
||||
"zone": 138
|
||||
"zone": 197
|
||||
},
|
||||
"snaps": "7917cfe4 ec53e8ac 09d8b201 823345e1 5d4f2a95 9efdd7ee 56e0addd 78c7726c 4ee3695b 20aee5b5 ba3311c0 d4d127c9 0c500cac d842272b 4cbf4a87 2525db60 e8039c1e 209c57b0 af85310c 92ca4257"
|
||||
"snaps": "7917cfe4 719c3d1e 5fce9836 dcc6d1db c6372862 d28f9510 24fc36dd 51354537 24003a40 0c6277ff 0e828fa2 41a80b07 a7179da1 e40e2870 d6867511 cc2e7940 487d9e8e b4112909 c46a7eff 812e6963"
|
||||
},
|
||||
"boss-boss_2-solo": {
|
||||
"about": "假想敌:铳 solo pool 3000000: chess_char_4_17_b chess_char_4_18_b chess_char_4_23_b chess_char_4_19_b chess_char_4_22_b chess_char_4_20_b chess_char_4_21_b chess_char_4_24_b chess_char_4_25_b chess_char_4_26_b",
|
||||
|
||||
@@ -1404,7 +1404,7 @@
|
||||
"combat": "client",
|
||||
"boost": {"lp":400,"layers":300},
|
||||
"setup": {"stageId":"act2autochess_m04","bossId":"boss_5","hiddenBossId":"boss_9","factions":["FLY","ELEMENT","INVISIBLE"]},
|
||||
"end": {"victory":true,"roundsPassed":15,"hiddenReached":true,"hiddenCleared":true,"reason":"victory","teamLp":800,"bossPoolLeft":0,"virtualMs":121650},
|
||||
"end": {"victory":true,"roundsPassed":15,"hiddenReached":true,"hiddenCleared":true,"reason":"victory","teamLp":615,"bossPoolLeft":0,"virtualMs":121650},
|
||||
"rng": {"setup":25,"shop":590,"waves":0,"draft":61,"bots":22075,"meta":141},
|
||||
"errors": {"engine":0,"logged":0,"sim":0,"dispatcher":0},
|
||||
"rounds": [
|
||||
@@ -1480,12 +1480,12 @@
|
||||
"victory": true,
|
||||
"roundsPassed": 15,
|
||||
"eliminatedRound": null,
|
||||
"lp": 400,
|
||||
"lp": 308,
|
||||
"bandId": "band_sarkazb",
|
||||
"title": {"id":"comment_1","name":"卫戍之星","picId":"comment_icon_1","text":"对敌方领袖造成伤害最高(仅胜利时)"},
|
||||
"title": {"id":"comment_3","name":"坚若磐石","picId":"comment_icon_3","text":"目标生命值损失最少"},
|
||||
"trophies": 8,
|
||||
"reward": 425,
|
||||
"stats": {"activatedLayers":2267,"bossDamage":6992346,"dmgDealt":24545248,"gold":149,"itemsEquipped":23,"kills":290,"leaks":0,"lpLost":0,"merges":6,"perfectRounds":13,"refreshes":16},
|
||||
"stats": {"activatedLayers":2267,"bossDamage":5899848,"dmgDealt":17549821,"gold":149,"itemsEquipped":23,"kills":301,"leaks":0,"lpLost":0,"merges":6,"perfectRounds":13,"refreshes":16},
|
||||
"lineup": ["3_16_b@12,8[3_01_e_a+1_01_e_b]","4_09_b@11,5","3_11_b@11,8[3_02_e_a+6_06_e_a]","1_02_b@10,4[1_02_e_a+4_02_e_a]","5_14_a@10,8[3_03_e_a+1_05_e_a]","6_20_a@9,6","3_20_a@9,8[3_07_e_a+3_08_e_a]","6_05_a@9,9[2_04_e_a+5_02_e_b]"],
|
||||
"bonds": ["arcaneShip 0/316","deputShip 1/312","egirShip 0/300","emptyShip 0/300","indomShip 0/300","investShip 0/300","kazimierzShip 0/302","kjeragShip 1/343","lateranoShip 0/300","maniShip 0/300","miraShip 0/300","preciShip 1/343","raidShip 0/300","sargonShip 0/300","siracusaShip 0/300","skillfulShip 0/319","soloShip 1/310","steadShip 1/341","suntShip 1/310","swiftShip 1/308","victoriaShip 0/300","visiShip 0/300","yanShip 0/300"]
|
||||
},
|
||||
@@ -1494,12 +1494,12 @@
|
||||
"victory": true,
|
||||
"roundsPassed": 15,
|
||||
"eliminatedRound": null,
|
||||
"lp": 400,
|
||||
"lp": 307,
|
||||
"bandId": "band_doberm",
|
||||
"title": {"id":"comment_2","name":"不朽盟约","picId":"comment_icon_2","text":"激活的盟约层数总和最高"},
|
||||
"title": {"id":"comment_1","name":"卫戍之星","picId":"comment_icon_1","text":"对敌方领袖造成伤害最高(仅胜利时)"},
|
||||
"trophies": 8,
|
||||
"reward": 425,
|
||||
"stats": {"activatedLayers":2301,"bossDamage":6900614,"dmgDealt":23861530,"gold":165,"itemsEquipped":31,"kills":276,"leaks":0,"lpLost":0,"merges":5,"perfectRounds":13,"refreshes":38},
|
||||
"stats": {"activatedLayers":2301,"bossDamage":7980752,"dmgDealt":27393341,"gold":165,"itemsEquipped":31,"kills":276,"leaks":0,"lpLost":0,"merges":5,"perfectRounds":13,"refreshes":38},
|
||||
"lineup": ["2_10_a@11,5[2_03_e_b+3_09_e_a]","5_03_a@11,8[1_02_e_a+5_02_e_a]","1_06_a@10,3[1_05_e_a+3_02_e_a]","3_16_a@10,4[3_03_e_b+4_04_e_b]","5_06_a@10,8[3_09_e_b+3_09_e_b]","4_21_b@9,6[4_06_e_a+3_10_e_b]","5_02_a@9,8[4_09_e_a+3_06_e_b]","1_17_b@9,9[3_08_e_a+1_01_e_a]"],
|
||||
"bonds": ["arcaneShip 1/327","deputShip 1/318","egirShip 0/300","emptyShip 0/300","indomShip 1/308","investShip 0/300","kazimierzShip 0/300","kjeragShip 0/300","lateranoShip 0/300","maniShip 0/300","miraShip 0/300","preciShip 1/404","raidShip 0/300","sargonShip 0/300","siracusaShip 0/300","skillfulShip 1/308","soloShip 0/310","steadShip 0/323","suntShip 1/304","swiftShip 0/300","victoriaShip 1/332","visiShip 0/300","yanShip 0/305"]
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -5,8 +5,9 @@
|
||||
// preference field = 0.1.0's — equal-length ties to the chain with the fewest non-blockable tiles, a line of sight that
|
||||
// covers non-blockable tiles, diagonal-step corners included, only on the own raw chain — whose pointer replaces the
|
||||
// official one where its route crosses fewer non-blockable tiles, or as few while only skipping the official waypoint
|
||||
// (on the straight line to it, leading there); "crosses" = positive-length intersection with the tile, found here by
|
||||
// clipping the segment against each tile, independently of grid.js crossTiles):
|
||||
// (on the straight line to it, leading there), and no more 深水区 than the official route (GitHub #375); "crosses" =
|
||||
// positive-length intersection with the tile, found here by clipping the segment against each tile, independently of
|
||||
// grid.js crossTiles):
|
||||
// * the 8 active stages with their match-start devices: the smoothed chain from EVERY walkable tile of the normal,
|
||||
// 联防 and boss rects to every goal equals the full-map reference one (the sim's rect limit changes nothing);
|
||||
// * 400 random crate / block layouts: identical chains to the same algorithm limited to the rect;
|
||||
@@ -56,6 +57,8 @@ function official(sid, { crates: extraCrates = [], blocks: extraBlocks = [], rec
|
||||
if (t.tileKey === 'tile_deepsea') return true;
|
||||
return !(t.heightType === 'LOWLAND' && (t.buildableType === 'ALL' || t.buildableType === 'MELEE'));
|
||||
};
|
||||
/** 深水区 — the preference may not cross more of it than the official route (#375) */
|
||||
const deep = (r, c) => ['tile_deepsea', 'tile_deepwater'].includes(tile(r, c).tileKey);
|
||||
const bres = ([r0, c0], [r1, c1], clear) => {
|
||||
const dr = Math.abs(r1 - r0), dc = Math.abs(c1 - c0), sr = r1 > r0 ? 1 : -1, sc = c1 > c0 ? 1 : -1;
|
||||
let err = dc - dr, r = r0, c = c0;
|
||||
@@ -140,12 +143,15 @@ function official(sid, { crates: extraCrates = [], blocks: extraBlocks = [], rec
|
||||
const off = spfa(false);
|
||||
const dist = off.dist;
|
||||
const segNb = (a, b) => through(a, b).filter(([y, x]) => !(y === a[0] && x === a[1]) && nb(y, x)).length;
|
||||
const segWet = (a, b) => through(a, b).filter(([y, x]) => !(y === a[0] && x === a[1]) && deep(y, x)).length;
|
||||
let nxt = smooth(off, false);
|
||||
if (prefer) {
|
||||
// per tile in increasing distance: the preference pointer when its route crosses fewer non-blockable tiles (or as
|
||||
// few while it only skips the official waypoint: o strictly inside the segment a → p, and o's chosen pointer is p)
|
||||
// and no more 深水区 than the official pointer's route
|
||||
const nxtP = smooth(spfa(true), true);
|
||||
const cost = new Map([[D, 0]]);
|
||||
const wet = new Map([[D, 0]]);
|
||||
const chosen = new Map();
|
||||
const skips = (a, o, p) => {
|
||||
const t = through(a, p);
|
||||
@@ -155,9 +161,11 @@ function official(sid, { crates: extraCrates = [], blocks: extraBlocks = [], rec
|
||||
if (k === D) continue;
|
||||
const a = unkey(k), o = nxt.get(k), p = nxtP.get(k);
|
||||
const co = segNb(a, o) + cost.get(key(o)), cp = segNb(a, p) + cost.get(key(p));
|
||||
const useP = cp < co || (cp === co && key(o) !== key(p) && key(chosen.get(key(o)) ?? []) === key(p) && skips(a, o, p));
|
||||
const wo = segWet(a, o) + wet.get(key(o)), wp = segWet(a, p) + wet.get(key(p));
|
||||
const useP = wp <= wo && (cp < co || (cp === co && key(o) !== key(p) && key(chosen.get(key(o)) ?? []) === key(p) && skips(a, o, p)));
|
||||
chosen.set(k, useP ? p : o);
|
||||
cost.set(k, useP ? cp : co);
|
||||
wet.set(k, useP ? wp : wo);
|
||||
}
|
||||
nxt = chosen;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
// Official route shapes (community report after 0.1.0, D5: on 战场#04 活性源石 the lower-gate enemies walked two tiles
|
||||
// straight left of the red gate and then straight up, instead of crossing diagonally from the 4th row to the 3rd as in
|
||||
// the official game). grid.js keeps the official flow field (research 08 §3.1) unless the blockable-ground preference
|
||||
// route of user playtest #2 (0.1.0's) crosses strictly fewer non-blockable tiles (floor, gates) — a segment that only
|
||||
// route of user playtest #2 (0.1.0's) crosses strictly fewer non-blockable tiles (floor, gates) and no more 深水区
|
||||
// (GitHub #375) — a segment that only
|
||||
// brushes a floor tile's corner does not cross it. Audit: every stage (active or not) × gate × field against the pure
|
||||
// official algorithm; the routes that still differ are listed, each avoiding floor the official route walks over.
|
||||
import { test } from 'node:test';
|
||||
@@ -127,6 +128,43 @@ test('audit: every route of every stage (11, active or not) × gate × field is
|
||||
assert.equal(official, stages.length * legs.length - seen.size);
|
||||
});
|
||||
|
||||
// GitHub #375 (FrogThai: 「水图boss会从下面这条路往蓝门走,实际应该从上路走」): on 战场#08(下半) 涨潮控制 the patrolling leaders
|
||||
// (铳 — level_act1autochess_h07_02 route 6 —, 卢西恩 — act2autochess_h07_05 route 8: PATROL_MOVE (3,9) → (2,3) → (5,9))
|
||||
// took the lower road (3,9) → (2,3) through the two 深水区 tiles (3,6) / (4,6) — one floor tile fewer than the official
|
||||
// upper road along row 5, which crosses one water tile. The tie-break may not wade into more 深水区 than the official
|
||||
// route (grid.js), so these legs are official again.
|
||||
test('#375 战场#08(下半): the (3,9) → (2,3) patrol leg stays on the official upper road', REAL, () => {
|
||||
const sid = 'act2autochess_m04';
|
||||
const g = stageGrid(sid, GEO.BOSS_RECT), pure = stageGrid(sid, GEO.BOSS_RECT, true);
|
||||
for (const [s, e, wp] of [
|
||||
[[3, 9], [2, 3], '3,9 5,9 5,4 2,4 2,3'],
|
||||
[[3, 11], [2, 17], '3,11 5,11 5,16 2,16 2,17'],
|
||||
[[3, 10], [2, 3], '3,10 5,9 5,4 2,4 2,3'],
|
||||
[[4, 10], [3, 9], '4,10 3,9'],
|
||||
]) {
|
||||
assert.deepEqual(g.waypoints(s[0], s[1], e[0], e[1]), pure.waypoints(s[0], s[1], e[0], e[1]), `(${s}) → (${e})`);
|
||||
assert.equal(str(g.waypoints(s[0], s[1], e[0], e[1])), wp);
|
||||
}
|
||||
});
|
||||
|
||||
test('#375 real battle (act2 m04, 铳 on act1autochess_h07_02): the patrol walks row 5 to (2,3) and steps on one 深水区 only', REAL, () => {
|
||||
const h = makeBattle({ kind: 'boss', stageId: 'act2autochess_m04', waveTemplate: 'act1autochess_h07_02', units: [], autoFinish: false, timeLimit: 120,
|
||||
sharedBoss: { hp: 1e12, maxHp: 1e12, damage(pid, n) { this.hp -= n; } } });
|
||||
const rows = getDefaultSource().getStage('act2autochess_m04').raw.rows;
|
||||
const tiles = [];
|
||||
h.b.on('tick', () => {
|
||||
const e = h.b.enemies.find((u) => u.alive && u.defId === 'enemy_9017_achunt');
|
||||
if (!e) return;
|
||||
const k = `${Math.round(e.y)},${Math.round(e.x)}`;
|
||||
if (tiles[tiles.length - 1] !== k) tiles.push(k);
|
||||
});
|
||||
assert.ok(h.runUntil(() => tiles.includes('2,4'), 60), `reaches (2,4): ${tiles.join(' ')}`);
|
||||
const upTo = tiles.slice(0, tiles.indexOf('2,4') + 1);
|
||||
assert.deepEqual(upTo.slice(0, 4), ['4,10', '3,9', '4,9', '5,9'], `up to row 5 first: ${upTo.join(' ')}`);
|
||||
const water = upTo.filter((k) => { const [r, c] = k.split(',').map(Number); return rows[r][c] === 'd'; });
|
||||
assert.deepEqual(water, ['5,6'], `深水区 stepped on: ${water.join(' ')} (route ${upTo.join(' ')})`);
|
||||
});
|
||||
|
||||
test('D5: an operator on (9,9), (9,8) or (10,8) blocks the lower-gate enemy on the official diagonal', REAL, () => {
|
||||
const guard = chessRec({ id: 't_guard', profession: 'WARRIOR', stats: { atk: 0, blockCnt: 3, maxHp: 1e6 }, skill: null });
|
||||
const walker = enemyRec({ key: 'enemy_walker', hp: 1e6, speed: 1 });
|
||||
|
||||
Reference in new issue
Block a user