Files
feuerwerk/src/core/graph/nodes/data/Compare.ts
T
adminandClaude Opus 5.5 9d6a0aebd3
game-maker / gate (push) Successful in 3m4s
game-maker / suite (push) Failing after 9m47s
Let Compare and Equals treat float noise as equal numbers
A graph that checks "time - late == 6" after On timer (#224) failed,
because the difference comes out as 5.999999999999972. Both values are
sums of measured frame times, and they round along different paths.
The only workaround was a Round node in front of every comparison.

- New helper nearlyEqual in values.ts. Two numbers count as equal when
  |a - b| <= 1e-9 * max(1, |a|, |b|). The tolerance grows with the
  size of the values, so it holds for hours of summed dt.
- Compare uses the rule for all six operators. "<" and ">" only hold
  when the values are not nearly equal, "<=" and ">=" also hold when
  they are. So "==" and ">" are never true at the same time.
- Equals uses the same rule when both inputs are real numbers. Objects
  still compare by identity, everything else as text. The node
  description says so.
- The tolerance is fixed, there is no parameter on the node. Physics
  values come from float32 in Rapier and are not covered on purpose.
- The log display is unchanged and still prints the raw number.

Tests:
- ./check.sh: 156 files, 2311 tests, CHECK EXIT 0 (before 2300).
- 11 new cases in node-catalog.test.ts, including the value from the
  log against 6, growth with magnitude, 6.001 != 6 and 1e-6 > 0.
- Counter-check exact compare: exactly the 8 noise cases fail.
- Counter-check "<"/">" without the exclusion: exactly 2 cases fail.
- No e2e change, the check sum stays at 1374.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 08:30:45 +02:00

39 lines
1.3 KiB
TypeScript

import type { NodeDef } from '../../NodeRegistry';
import { asNumber, nearlyEqual } from '../../values';
import { bool, num } from '../shared';
import { textOf } from '../../../model/Properties';
// All six share one notion of equal, so `==` and `>` are never true at once.
const OPERATORS: Record<string, (a: number, b: number) => boolean> = {
'==': (a, b) => nearlyEqual(a, b),
'!=': (a, b) => !nearlyEqual(a, b),
'<': (a, b) => a < b && !nearlyEqual(a, b),
'<=': (a, b) => a < b || nearlyEqual(a, b),
'>': (a, b) => a > b && !nearlyEqual(a, b),
'>=': (a, b) => a > b || nearlyEqual(a, b),
};
export const Compare: NodeDef = {
type: 'Compare',
label: 'Compare',
category: 'data',
description: 'Compares two numbers.',
inputs: [num('a'), num('b')],
outputs: [bool('result')],
params: [
{
key: 'op',
label: 'Operator',
kind: 'enum',
default: '<',
options: Object.keys(OPERATORS).map((op) => ({ value: op, label: op })),
},
],
run: (_ctx, inputs, params) => {
// The fallback matches the param's declared default, so a node with no setting behaves
// like a freshly created one.
const compare = OPERATORS[textOf(params.op)] ?? OPERATORS['<']!;
return { outputs: { result: compare(asNumber(inputs.a), asNumber(inputs.b)) } };
},
};