mirror of
https://github.com/Such-Software/wownerogue.git
synced 2026-10-17 14:21:25 +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.
116 lines
4.8 KiB
JavaScript
116 lines
4.8 KiB
JavaScript
const crypto = require('crypto');
|
|
const DungeonGenerator = require('../src/game/dungeon');
|
|
const Game = require('../src/game/game');
|
|
const { attachDungeonVerification } = require('../src/game/fairnessVerifier');
|
|
|
|
const LEGACY_OVERLAP_SEED = '5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9';
|
|
const LEGACY_OVERLAP_FINGERPRINT = '9fd69a09c5f7a233b03538142994dd932afb4f2d6cfa19baab7041d5303edb50';
|
|
|
|
function objectiveKeys(dungeon) {
|
|
return [dungeon.entrance, dungeon.exit, dungeon.treasure].map(point => point.join(','));
|
|
}
|
|
|
|
describe('versioned playable dungeon objectives', () => {
|
|
const saved = {};
|
|
|
|
beforeAll(() => {
|
|
for (const key of ['DIFFICULTY_PRESET', 'DUNGEON_LEVELS', 'GAME_MODE', 'PAYMENTS_ENABLED']) {
|
|
saved[key] = process.env[key];
|
|
}
|
|
process.env.DIFFICULTY_PRESET = 'easy';
|
|
process.env.DUNGEON_LEVELS = '1';
|
|
process.env.GAME_MODE = 'FREE';
|
|
process.env.PAYMENTS_ENABLED = 'false';
|
|
});
|
|
|
|
afterAll(() => {
|
|
for (const [key, value] of Object.entries(saved)) {
|
|
if (value === undefined) delete process.env[key];
|
|
else process.env[key] = value;
|
|
}
|
|
});
|
|
|
|
test('v2 deterministically retries a known v1 entrance/exit overlap', () => {
|
|
const v1 = DungeonGenerator.regenerateFromSeed(LEGACY_OVERLAP_SEED, 'XMR', {
|
|
width: 30,
|
|
height: 15,
|
|
generatorVersion: 'dungeon-generator-v1'
|
|
});
|
|
const v2 = DungeonGenerator.regenerateFromSeed(LEGACY_OVERLAP_SEED, 'XMR', {
|
|
width: 30,
|
|
height: 15,
|
|
generatorVersion: 'dungeon-generator-v2'
|
|
});
|
|
|
|
expect(v1.entrance).toEqual(v1.exit);
|
|
expect([v1.entrance, v1.exit, v1.treasure]).toEqual([[17, 8], [17, 8], [16, 8]]);
|
|
expect(DungeonGenerator.layoutFingerprint(v1)).toBe(LEGACY_OVERLAP_FINGERPRINT);
|
|
expect(new Set(objectiveKeys(v1)).size).toBeLessThan(3);
|
|
expect(new Set(objectiveKeys(v2)).size).toBe(3);
|
|
expect(DungeonGenerator.hasDistinctReachableObjectives(v2, {
|
|
primaryFloor: "'1",
|
|
secondaryFloor: "'2"
|
|
})).toBe(true);
|
|
const repeated = DungeonGenerator.regenerateFromSeed(LEGACY_OVERLAP_SEED, 'XMR', {
|
|
width: 30,
|
|
height: 15,
|
|
generatorVersion: 'dungeon-generator-v2'
|
|
});
|
|
expect(DungeonGenerator.layoutFingerprint(repeated)).toBe(DungeonGenerator.layoutFingerprint(v2));
|
|
});
|
|
|
|
test('easy-profile v2 samples always have distinct reachable objectives', () => {
|
|
for (let i = 0; i < 100; i += 1) {
|
|
const seed = crypto.createHash('sha256').update(`objective-sample-${i}`).digest('hex');
|
|
const dungeon = DungeonGenerator.regenerateFromSeed(seed, 'XMR', {
|
|
width: 30,
|
|
height: 15,
|
|
generatorVersion: DungeonGenerator.GENERATOR_VERSION
|
|
});
|
|
expect(new Set(objectiveKeys(dungeon)).size).toBe(3);
|
|
expect(DungeonGenerator.hasDistinctReachableObjectives(dungeon, {
|
|
primaryFloor: "'1",
|
|
secondaryFloor: "'2"
|
|
})).toBe(true);
|
|
}
|
|
});
|
|
|
|
test('historical v1 fingerprints remain verifiable after v2 becomes current', () => {
|
|
const legacy = DungeonGenerator.regenerateFromSeed(LEGACY_OVERLAP_SEED, 'XMR', {
|
|
width: 30,
|
|
height: 15,
|
|
generatorVersion: 'dungeon-generator-v1'
|
|
});
|
|
expect(DungeonGenerator.layoutFingerprint(legacy)).toBe(LEGACY_OVERLAP_FINGERPRINT);
|
|
const result = attachDungeonVerification({ valid: true }, {
|
|
effectiveSeed: LEGACY_OVERLAP_SEED,
|
|
proofContext: {
|
|
cryptoType: 'XMR',
|
|
maxDepth: 1,
|
|
generatorVersion: 'dungeon-generator-v1',
|
|
generationOptions: { width: 30, height: 15 }
|
|
},
|
|
expectedFingerprint: LEGACY_OVERLAP_FINGERPRINT,
|
|
generatorVersion: 'dungeon-generator-v1'
|
|
});
|
|
|
|
expect(result.valid).toBe(true);
|
|
expect(result.generatorVersion).toBe('dungeon-generator-v1');
|
|
expect(result.layoutMatches).toBe(true);
|
|
});
|
|
|
|
test('public game options cannot select a historical generator', () => {
|
|
const game = Game.createStandardGame('public-socket', { id: 'public-socket' }, {
|
|
cryptoType: 'XMR',
|
|
width: 30,
|
|
height: 15,
|
|
generatorVersion: 'dungeon-generator-v1'
|
|
});
|
|
|
|
expect(game.generatorVersion).toBe(DungeonGenerator.GENERATOR_VERSION);
|
|
expect(game.gameConfig).not.toHaveProperty('generatorVersion');
|
|
expect(game.gameProof.generatorVersion).toBe(DungeonGenerator.GENERATOR_VERSION);
|
|
expect(game.gameProof.context.generatorVersion).toBe(DungeonGenerator.GENERATOR_VERSION);
|
|
});
|
|
});
|