Files
HolodoriCalc/src/lib/engine/combination-evaluation-pool.test.ts
T
dal4segno da40ec4238
Publish GitHub Pages output / publish (push) Successful in 34s
perf: 조합 탐색 정밀 계산과 Worker 처리 최적화
카드 조건과 빈도별 공통 계산을 재사용하고 준비·평가 Worker를 재사용합니다.
준비된 후보를 모두 정밀 평가하고 최종 결과를 한 번 표시합니다.
업데이트 문구를 한 항목으로 통합합니다.
2026-10-13 12:03:10 +09:00

53 lines
4.6 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { CombinationEvaluationPool } from './combination-evaluation-pool.ts';
import { CombinationWorkerSession } from './combination-worker-session.ts';
import type { EvaluationCandidate } from './combination-evaluation-pool.ts';
import type { ExactSongChart } from './types.ts';
const chart: ExactSongChart = {songId:'pool',difficulty:'expert',durationSeconds:1,scoreRatio:1,specialStartSeconds:[0,0,0,0,0],notes:[]};
const candidates = Array.from({length:16},(_,index)=>({overallPower:1000+index,perfectNoteScore:100,
cards:Array.from({length:5},(_,slot)=>({id:`${index}-${slot}`,name:'test',type:'pure',generation:'0',active:{scoreUpPercent:0,cooldownSeconds:1,durationSeconds:1,probabilityPercent:100},special:{scoreUpPercent:0,durationSeconds:1},allowedCooldownReductions:[0,4,8,12] as const}))
})) satisfies EvaluationCandidate[];
class FakeWorker {
onmessage: ((event:{data:any})=>void)|null=null;onerror:(()=>void)|null=null;terminations=0;
requests:any[]=[];automatic=true;
terminate(){this.terminations++;}
respond(request:any,operationId=request.operationId){
const rankings=request.candidates.map((c:EvaluationCandidate)=>({overallPower:c.overallPower,cards:c.cards,result:{order:c.cards.map(c=>c.id),theoreticalMaxScore:c.overallPower,expectedScore:c.overallPower-1}}));
this.onmessage?.({data:{type:'combination-result',operationId,rankings,indices:rankings.map((_:any,i:number)=>i)}});
}
postMessage(request:any){this.requests.push(request);if(this.automatic)queueMicrotask(()=>this.respond(request));}
}
test('one pool reuses Workers across complete precision passes',async()=>{
const session=new CombinationWorkerSession<Worker>();const id=session.start();const workers:FakeWorker[]=[];
const pool=new CombinationEvaluationPool(()=>{const w=new FakeWorker();workers.push(w);return w as unknown as Worker;},session,id,4);
const phases:string[]=[];const options={resultLimit:10,onPhase:(p:string)=>phases.push(p),onProgress:()=>{}};
const first=await pool.run(candidates,chart,options);assert.equal(first.length,10);assert.equal(first[0].overallPower,1015);
const full=await pool.run(candidates,chart,{...options});assert.deepEqual(first,full);assert.equal(workers.length,4);
assert.ok(workers.every(w=>w.requests.length>=2));assert.deepEqual(phases,['exact','exact']);
pool.close();pool.close();assert.ok(workers.every(w=>w.terminations===1));
});
test('stale operation replies are ignored and cancellation rejects the pending pass',async()=>{
const session=new CombinationWorkerSession<Worker>();const id=session.start();const worker=new FakeWorker();worker.automatic=false;
const pool=new CombinationEvaluationPool(()=>worker as unknown as Worker,session,id,1);
let resolved=false;const pending=pool.run(candidates,chart,{resultLimit:10,onPhase:()=>{},onProgress:()=>{}}).then(()=>{resolved=true;});
worker.respond(worker.requests[0],0);await Promise.resolve();assert.equal(resolved,false);
session.cancel();pool.close();await assert.rejects(pending,/취소/);assert.equal(worker.terminations,1);
worker.respond(worker.requests[0]);assert.equal(resolved,false);
});
test('Worker failures reject the pass and closing terminates every owned Worker',async()=>{
const session=new CombinationWorkerSession<Worker>();const id=session.start();const worker=new FakeWorker();worker.automatic=false;
const pool=new CombinationEvaluationPool(()=>worker as unknown as Worker,session,id,1);
const pending=pool.run(candidates,chart,{resultLimit:10,onPhase:()=>{},onProgress:()=>{}});
worker.onerror?.();await assert.rejects(pending,/Worker/);pool.close();assert.equal(worker.terminations,1);
});
test('every prepared candidate keeps all allowed cooldowns and receives precision evaluation',async()=>{
const session=new CombinationWorkerSession<Worker>();const id=session.start();const workers:FakeWorker[]=[];
const pool=new CombinationEvaluationPool(()=>{const worker=new FakeWorker();workers.push(worker);return worker as unknown as Worker;},session,id,4);
const result=await pool.run(candidates,chart,{resultLimit:10,onPhase:()=>{},onProgress:()=>{}});
assert.equal(pool.lastExactCount,candidates.length);assert.equal(result.length,10);
assert.equal(workers.reduce((sum,worker)=>sum+worker.requests.reduce((n,request)=>n+request.candidates.length,0),0),candidates.length);
assert.ok(workers.every(worker=>worker.requests.every(request=>!request.summaryOnly&&request.candidates.every((candidate:EvaluationCandidate)=>candidate.cards.every(card=>card.allowedCooldownReductions?.length===4)))));
pool.close();assert.ok(workers.every(worker=>worker.terminations===1));
});