mirror of
https://github.com/Such-Software/wownerogue.git
synced 2026-10-22 08:38:33 +02:00
Phase 0.1 — Underpayment confirmation bug (CRITICAL): Payment confirmation gated on `status.confirmed`, which checkPaymentStatus sets for ANY incoming tx regardless of amount, so sending a single atomic unit granted a full game/credits. Now gate on `status.complete` (received >= expected). Underpayers are warned once and the payment stays pending so a top-up can complete it. Persist `received_amount` for single-game and credits confirmations for reconciliation. Adds an underpayment regression test. Phase 0.2 — Provably-fair was decorative (CRITICAL): The committed seed had zero effect on the dungeon (created seededRNG was unused; generation used Math.random + a globally-seeded ROT.RNG). Thread a per-game seeded RNG derived from the committed seed through dungeon generation (reseed the Digger via seedToInt + replace all Math.random) and monster movement (seededShuffle). Add DungeonGenerator.regenerateFromSeed and layoutFingerprint; /api/verify now regenerates the dungeon from the seed and returns a fingerprint a player can independently reproduce. Adds a determinism test suite. Full suite: 11 suites / 113 tests passing. Plan and progress tracked in docs/REMEDIATION_PLAN.md.
89 lines
3.4 KiB
JavaScript
89 lines
3.4 KiB
JavaScript
/**
|
|
* Provably-fair determinism tests (Phase 0.2).
|
|
*
|
|
* The committed seed must deterministically generate the dungeon and drive monster
|
|
* behaviour, so that:
|
|
* - the same seed always reproduces the exact same dungeon (verifiability), and
|
|
* - the server cannot craft a different/unfair dungeon after seeing the seed.
|
|
*/
|
|
|
|
const DungeonGenerator = require('../src/game/dungeon');
|
|
const Monster = require('../src/game/monster');
|
|
const { createSeededRNG, hashSeed, verifyGame } = require('../src/game/provablyFair');
|
|
|
|
const SEED_A = 'a'.repeat(64);
|
|
const SEED_B = 'b'.repeat(64);
|
|
|
|
describe('Provably fair: deterministic dungeon generation', () => {
|
|
test('same seed regenerates an identical dungeon layout', () => {
|
|
const d1 = DungeonGenerator.regenerateFromSeed(SEED_A, 'WOW');
|
|
const d2 = DungeonGenerator.regenerateFromSeed(SEED_A, 'WOW');
|
|
|
|
expect(DungeonGenerator.layoutFingerprint(d1))
|
|
.toBe(DungeonGenerator.layoutFingerprint(d2));
|
|
expect(d1.entrance).toEqual(d2.entrance);
|
|
expect(d1.exit).toEqual(d2.exit);
|
|
expect(d1.treasure).toEqual(d2.treasure);
|
|
expect(d1.map).toEqual(d2.map);
|
|
});
|
|
|
|
test('regeneration is stable even when interleaved with another seed (no global RNG bleed)', () => {
|
|
const a1 = DungeonGenerator.layoutFingerprint(DungeonGenerator.regenerateFromSeed(SEED_A, 'WOW'));
|
|
// Generate a different seed's dungeon in between — must not affect SEED_A's output.
|
|
DungeonGenerator.regenerateFromSeed(SEED_B, 'WOW');
|
|
const a2 = DungeonGenerator.layoutFingerprint(DungeonGenerator.regenerateFromSeed(SEED_A, 'WOW'));
|
|
expect(a1).toBe(a2);
|
|
});
|
|
|
|
test('different seeds produce different layouts', () => {
|
|
const fpA = DungeonGenerator.layoutFingerprint(DungeonGenerator.regenerateFromSeed(SEED_A, 'WOW'));
|
|
const fpB = DungeonGenerator.layoutFingerprint(DungeonGenerator.regenerateFromSeed(SEED_B, 'WOW'));
|
|
expect(fpA).not.toBe(fpB);
|
|
});
|
|
|
|
test('verifyGame confirms the seed/commitment relationship', () => {
|
|
const commitment = hashSeed(SEED_A);
|
|
expect(verifyGame(SEED_A, commitment).valid).toBe(true);
|
|
expect(verifyGame(SEED_B, commitment).valid).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('Provably fair: deterministic monster movement', () => {
|
|
// A small open room so the monster always has somewhere to wander.
|
|
function buildDungeon() {
|
|
const map = Array.from({ length: 7 }, () => Array(7).fill("'1"));
|
|
for (let i = 0; i < 7; i++) { map[0][i] = '#'; map[6][i] = '#'; map[i][0] = '#'; map[i][6] = '#'; }
|
|
return { map };
|
|
}
|
|
|
|
test('wandering is reproducible from the same seeded RNG sequence', () => {
|
|
const dungeon = buildDungeon();
|
|
const run = () => {
|
|
const rng = createSeededRNG(SEED_A);
|
|
const m = new Monster(3, 3, { visionRange: 5 });
|
|
const path = [];
|
|
for (let i = 0; i < 20; i++) {
|
|
m.moveTowardPlayer(null, dungeon, rng); // no player => wander using rng
|
|
path.push([m.x, m.y]);
|
|
}
|
|
return path;
|
|
};
|
|
expect(run()).toEqual(run());
|
|
});
|
|
|
|
test('different seeds yield different wander paths', () => {
|
|
const dungeon = buildDungeon();
|
|
const walk = (seed) => {
|
|
const rng = createSeededRNG(seed);
|
|
const m = new Monster(3, 3, { visionRange: 5 });
|
|
const path = [];
|
|
for (let i = 0; i < 20; i++) {
|
|
m.moveTowardPlayer(null, dungeon, rng);
|
|
path.push([m.x, m.y]);
|
|
}
|
|
return JSON.stringify(path);
|
|
};
|
|
expect(walk(SEED_A)).not.toBe(walk(SEED_B));
|
|
});
|
|
});
|