feedback7: the battle sim uses engine-independent math — Chrome, Firefox, Safari and the server agree bit for bit (PR #373 by @Cloudnyco)

- server/sim/detmath.js: hypot (sqrt of the squares, rescaled outside 2^±500), powi (integer powers in double-double,
  one rounding), sin / cos (fdlibm 5.3) and atan2 (fdlibm / FreeBSD msun) built only from correctly rounded IEEE
  operations; the sim's Math.hypot / sin / cos / atan2 / pow calls go through it (65 files); eslint.config.js forbids
  the approximated Math functions under server/sim and test/sim/detmath.test.js scans for them.
- sin / cos / atan2 are the algorithms V8 runs in Node, so the server's results stay; only hypot differs from V8's.
  Golden: 2 moved — diy-027 (a hypot distance, damage +80) and coop3-ABYSS-5 (a later card pick from round 9, same end).
- Applied unchanged after review (Grok static review: MERGE; the diff read in full: no network, no new dependency).
- Tests: broad node --test 5155 / 5155 with the new file tracked (detmath 7 / 7, package 10 / 10); lint 0 errors,
  typecheck, check:imports.
This commit is contained in:
Cloudnyco authored and sganggs committed 2026-10-08 18:35:55 +08:00
1 parent 2580152a3a
commit ae22335cfe
65 files changed
+616 -141

No files matched your search

+5 -4
View File
@@ -92,6 +92,7 @@ import { genericKit } from './generic.js';
import { bardRegen } from '../professions.js';
import { normDir, localOrder } from '../dir.js';
import { SKILL_SUMMON_START_DEPLOY } from '../../../shared/constants.js';
import { hypot } from '../detmath.js';
const num = (v, d = 0) => (typeof v === 'number' && Number.isFinite(v) ? v : (typeof v === 'string' && v.trim() !== '' && Number.isFinite(+v) ? +v : d));
const GRID_3X3 = Object.freeze([[1, -1], [1, 0], [1, 1], [0, -1], [0, 0], [0, 1], [-1, -1], [-1, 0], [-1, 1]]);
@@ -451,7 +452,7 @@ export function findSummonTile(battle, owner, placement = 'melee', opts = {}) {
let cover = 0;
if (placement === 'enemy' || placement === 'melee') cover = countNear(enemies, r, c, 1);
else if (placement === 'ally') cover = countNear(injured, r, c, 1);
const d = placement === 'ally' ? Math.hypot(c - owner.x, r - owner.y) : Math.hypot(c - focus.x, r - focus.y);
const d = placement === 'ally' ? hypot(c - owner.x, r - owner.y) : hypot(c - focus.x, r - focus.y);
// last: the tile's offset in the owner's facing-RIGHT frame (for a RIGHT owner = the tile key order), so equal
// candidates resolve the same way whichever direction the owner faces
const [lr, lc] = localOrder(r - owner.tileR, c - owner.tileC, owner.dir);
@@ -1236,7 +1237,7 @@ function yanyouKit(bb, raw) {
const keys = [];
const r0 = Math.floor(u.y - radius), r1 = Math.ceil(u.y + radius), c0 = Math.floor(u.x - radius), c1 = Math.ceil(u.x + radius);
for (let r = Math.max(0, r0); r <= Math.min(ROWS - 1, r1); r++) {
for (let c = Math.max(0, c0); c <= Math.min(COLS - 1, c1); c++) if (Math.hypot(c - u.x, r - u.y) <= radius + 1e-9) keys.push(r * COLS + c);
for (let c = Math.max(0, c0); c <= Math.min(COLS - 1, c1); c++) if (hypot(c - u.x, r - u.y) <= radius + 1e-9) keys.push(r * COLS + c);
}
u.rangeKeys = keys;
u.rangeKeySet = new Set(keys);
@@ -1299,7 +1300,7 @@ function yanyouKit(bb, raw) {
const tgt = lockOf(unit) ?? topAggroEnemy(battle, unit);
if (tgt) {
const dx = tgt.x - unit.x, dy = tgt.y + num(unit.mem.hoverDy, 0) - unit.y;
const d = Math.hypot(dx, dy);
const d = hypot(dx, dy);
if (d > YANYOU_STOP + 1e-6) {
const step = Math.min(d - YANYOU_STOP, speed * dt);
const R = battle.rect;
@@ -1351,7 +1352,7 @@ function airTile(battle, playerId, taken) {
if (taken.has(r * COLS + c) || held.has(r * COLS + c) || !tileFree(battle, r, c)) continue;
const t = battle.grid.tile(r, c);
const cls = t.build === 'NONE' && t.pass !== 'ALL' ? 0 : t.build === 'NONE' ? 1 : 2;
const s = [cls, Math.hypot(r - cr, c - cc), r * COLS + c];
const s = [cls, hypot(r - cr, c - cc), r * COLS + c];
let less = !bs;
if (!less) for (let i = 0; i < 3; i++) { if (s[i] < bs[i] - 1e-9) { less = true; break; } if (s[i] > bs[i] + 1e-9) break; }
if (less) { best = [r, c]; bs = s; }