mirror of
https://github.com/Such-Software/wownerogue.git
synced 2026-10-17 22:31:26 +02:00
A browser-based roguelike synchronized with crypto block timing. A run is a race against the next block: reach the exit before the block lands, or the house wins. The same codebase runs the solo dungeon, the social Tavern, and operator-selected PvP and race rulesets, with separate free and paid leaderboards. Self-hostable under the MIT License. See README.md to run it, docs/ARCHITECTURE.md for the design, and docs/DEPLOY.md to operate an instance.
82 lines
3.1 KiB
JavaScript
82 lines
3.1 KiB
JavaScript
/**
|
|
* Money / atomic-unit arithmetic tests (Phase 1.1).
|
|
*
|
|
* These specifically exercise the large-amount cases where the old float path
|
|
* (`base * multiplier`, `sum + Number(x)`) silently loses precision above 2^53.
|
|
*/
|
|
|
|
const money = require('../src/money/atomic');
|
|
|
|
describe('money.toBig / toSafe', () => {
|
|
test('parses string, number and bigint', () => {
|
|
expect(money.toBig('100000000000')).toBe(100000000000n);
|
|
expect(money.toBig(5)).toBe(5n);
|
|
expect(money.toBig(7n)).toBe(7n);
|
|
});
|
|
|
|
test('rejects non-integer numbers and junk strings', () => {
|
|
expect(() => money.toBig(1.5)).toThrow();
|
|
expect(() => money.toBig('1.5')).toThrow();
|
|
expect(() => money.toBig('abc')).toThrow();
|
|
expect(() => money.toBig(Infinity)).toThrow();
|
|
});
|
|
|
|
test('toSafe narrows to number when exact, keeps string when too large', () => {
|
|
expect(money.toSafe(200000000000n)).toBe(200000000000);
|
|
// 10^18 atomic > MAX_SAFE_INTEGER (~9.007e15) -> must stay a string, not a lossy number
|
|
expect(money.toSafe(1000000000000000000n)).toBe('1000000000000000000');
|
|
});
|
|
});
|
|
|
|
describe('money.mulByDecimal (exact, no float)', () => {
|
|
test('whole multipliers', () => {
|
|
expect(money.mulByDecimal(100000000000n, 2)).toBe(200000000000n);
|
|
expect(money.mulByDecimal(100000000000n, 3)).toBe(300000000000n);
|
|
});
|
|
|
|
test('fractional multipliers with half-up rounding', () => {
|
|
expect(money.mulByDecimal(100000000000n, 1.5)).toBe(150000000000n);
|
|
expect(money.mulByDecimal(50000000000n, 1.5)).toBe(75000000000n);
|
|
expect(money.mulByDecimal(3n, 1.5)).toBe(5n); // 4.5 -> 5 (half up)
|
|
expect(money.mulByDecimal(1n, 2.5)).toBe(3n); // 2.5 -> 3 (half up)
|
|
expect(money.mulByDecimal(0n, 3)).toBe(0n);
|
|
});
|
|
|
|
test('large amounts stay exact where float would drift', () => {
|
|
// 9,007,199,254,740,993 is 2^53 + 1: not representable as a float64.
|
|
const big = 9007199254740993n;
|
|
expect(money.mulByDecimal(big, 3)).toBe(27021597764222979n);
|
|
// Sanity: the naive float computation is wrong, proving why BigInt matters.
|
|
expect(Number(big) * 3).not.toBe(27021597764222979);
|
|
});
|
|
|
|
test('rejects invalid multipliers', () => {
|
|
expect(() => money.mulByDecimal(1n, -1)).toThrow();
|
|
expect(() => money.mulByDecimal(1n, 'x')).toThrow();
|
|
});
|
|
});
|
|
|
|
describe('money.sum / add (exact)', () => {
|
|
test('sums large atomic amounts without precision loss', () => {
|
|
const vals = ['9007199254740991', '9007199254740991', '9007199254740991'];
|
|
expect(money.sum(vals)).toBe(27021597764222973n); // exact
|
|
// The naive float reduce drifts away from the exact integer total.
|
|
const floatSum = vals.reduce((a, b) => a + Number(b), 0);
|
|
expect(BigInt(floatSum)).not.toBe(money.sum(vals));
|
|
});
|
|
|
|
test('add coerces mixed types', () => {
|
|
expect(money.add('100', 50)).toBe(150n);
|
|
expect(money.add(7n, '3')).toBe(10n);
|
|
});
|
|
});
|
|
|
|
describe('money.format', () => {
|
|
test('formats with currency decimals and trims trailing zeros', () => {
|
|
expect(money.format(150000000000n, 11)).toBe('1.5');
|
|
expect(money.format(100000000000n, 11)).toBe('1');
|
|
expect(money.format(1n, 11)).toBe('0.00000000001');
|
|
expect(money.format(0n, 11)).toBe('0');
|
|
});
|
|
});
|