const test = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'); const { loadMembers, loadModule, resolveFile } = require('./new-player-ab-test-support.cjs'); const { LEVEL_TIME_BONUS } = loadModule('assets/Script/module/Config/LevelTimeBonusConfig.ts'); const configFile = 'assets/Script/module/Config/GameConfig.ts'; const readLevel = level => JSON.parse(fs.readFileSync(resolveFile(`assets/custom/Json/level${level}.json`), 'utf8')); const plain = value => JSON.parse(JSON.stringify(value)); function config(info = {}) { const Config = loadMembers(configFile, ['applyLevelData', 'LEVEL_INFO_init', 'getLevelConfigPath', 'loadLevelResource', 'loadLevelFromBundle'], { LEVEL_TIME_BONUS, console: { log() {} }, cc, GameTool: cc.fx.GameTool, LevelResources: { load: (_id, _mainline, local) => local(), prefetch() {}, ensureAll: () => Promise.resolve() }, }); Config.GM_INFO = info; return Config; } // Exercise the actual loader so file numbers, help levels and endless levels use the same key. const requests = []; const cc = { v2: (x, y) => ({ x, y }), director: { getScene: () => null }, fx: { GameTool: { maxLevel: () => false, MAX_NORMAL_LEVEL: 2040 } }, assetManager: { loadBundle(_name, _options, done) { done(null, {}); }, loadAny(request, _options, done) { requests.push(request.path); done(null, { json: readLevel(Number(request.path.replace('Json/level', ''))) }); }, }, }; test('A, unassigned and other groups retain the original time for every configured level', () => { assert.equal(Object.keys(LEVEL_TIME_BONUS).length, 100); for (const info of [undefined, {}, { abTests: {} }, ...[0, 1, 3].map(layer_three => ({ abTests: { layer_three } }))]) { const Config = config(info); for (const level of Object.keys(LEVEL_TIME_BONUS)) { const data = readLevel(level); Config.applyLevelData(data, false, 0, Number(level)); assert.equal(Config.LEVEL_INFO[0].time, data.LEVEL_INFO[0].time); assert.equal(Config.LEVEL_INFO, data.LEVEL_INFO); } } }); test('B adds time once, preserves cached data and returns to original time when switched to A', () => { const Config = config({ abTests: { layer_three: 2 } }); for (const [level, bonus] of Object.entries(LEVEL_TIME_BONUS)) { assert.ok(Number.isInteger(bonus) && bonus > 0); const data = readLevel(level), before = structuredClone(data); Config.GM_INFO.abTests.layer_three = 2; for (let retry = 0; retry < 2; retry++) { Config.applyLevelData(data, false, 0, Number(level)); const expected = structuredClone(before.LEVEL_INFO); expected[0].time += bonus; assert.deepEqual(plain(Config.LEVEL_INFO), expected); assert.equal(Config.BLOCK_INFO, data.BLOCK_INFO); assert.equal(Config.WALL_INFO, data.WALL_INFO); assert.deepEqual(data, before); } Config.GM_INFO.abTests.layer_three = 1; Config.applyLevelData(data, false, 0, Number(level)); assert.equal(Config.LEVEL_INFO[0].time, before.LEVEL_INFO[0].time); } }); test('B leaves unlisted levels unchanged and uses the file number instead of a stale internal id', () => { const Config = config({ abTests: { layer_three: 2 } }); for (const level of [1, 113, 115, 1282]) { const data = readLevel(level); Config.applyLevelData(data, false, 0, level); assert.equal(Config.LEVEL_INFO, data.LEVEL_INFO); } const data = readLevel(222); assert.equal(data.LEVEL_INFO[0].id, '158'); Config.applyLevelData(data, false, 0, 222); assert.equal(Config.LEVEL_INFO[0].time, 70); assert.equal(Config.LEVEL_INFO[0].id, '158'); }); test('loader applies B bonuses to the requested mainline, help and endless level', async () => { for (const mode of ['mainline', 'help', 'endless']) { const Config = config({ level: mode === 'mainline' ? 221 : 0, otherLevel: mode === 'help' ? 222 : 0, randomLevel: 222, abTests: { layer_three: 2 } }); cc.fx.GameConfig = Config; Config.levelLoadRequest = Config.sceneLoadRequest = 0; Config.takeEntryDownloadRelease = () => null; Config.LEVEL_INFO_init(false, 0, mode === 'help', mode === 'endless'); for (let i = 0; i < 10; i++) await Promise.resolve(); assert.equal(requests.at(-1), 'Json/level222'); assert.equal(Config.LEVEL_INFO[0].time, 70, mode); } }); test('server A/B assignments select original and bonus times before entering the scene', () => { const Manager = loadMembers('assets/Script/GameManager.ts', ['setABTests'], { cc }); const manager = new Manager(); const Config = config(); cc.fx.GameConfig = Config; const seen = []; Config.enterGameSceneWhenReady = (time, preparation) => seen.push([Config.LEVEL_INFO[0].time, time, preparation]); const preparation = {}; for (const [group, expected] of [['A', 135], ['B', 165], [1, 135], [2, 165]]) { manager.setABTests({ layer_3: { group } }); Config.applyLevelData(readLevel(114), true, 0.5, 114, preparation); assert.deepEqual(seen.at(-1), [expected, 0.5, preparation]); } });