feat: 전투 시뮬레이터를 브라우저 Worker로 이관

서버가 권위 환경과 반복 seed를 준비하고 공용 logic 프로세서를 브라우저와 기존 서버 fallback이 함께 사용하도록 변경한다. production Chromium에서 고정 seed와 1000회 Node 결과 동등성을 검증한다.
This commit is contained in:
2026-08-17 01:45:41 +00:00
parent 63005c22fe
commit 161ec90bf8
23 changed files with 1258 additions and 854 deletions
@@ -0,0 +1,46 @@
# 전투 시뮬레이터 브라우저 실행 경계
## 요청과 권위 데이터
`BattleSimulatorView.vue`는 사용자가 편집한 장수·국가·도시 입력을
`battle.prepareSimulation`에 보냅니다. 이 API는 로그인만 요구하며 게임 장수
보유 여부나 input-event transaction에는 의존하지 않습니다. 서버는 현재
`WorldState`에서 다음 값을 읽어 실행 payload에 추가합니다.
- 전체 `UnitSetDefinition`
- 전투 상수와 성벽 병종/병과 ID
- 시나리오 시작 연도와 저장된 `scenarioEffect`
- 고정 seed가 없는 경우 각 반복에 사용할 UUID seed
클라이언트가 같은 이름의 `scenarioEffect`를 보내도 입력 schema가 제거하며,
저장된 효과가 유일한 기준입니다. 1000회 실행 seed를 서버에서 한 번 확정하므로
브라우저와 Node가 동일 payload를 재실행해 전체 결과를 정확히 비교할 수 있습니다.
## 실행 경로
준비된 payload는 module Web Worker에 전달됩니다. Worker는 메시지 handler를 먼저
등록한 뒤 `@sammo-ts/logic`을 lazy-load하고 `processBattleSimJob()`을 실행합니다.
전투 중 UI thread를 점유하지 않으며, 결과만 화면 상태로 돌려줍니다. Worker
bundle은 동적 trait/item module을 한 파일에 묶어 첫 실행의 다수 요청을 피합니다.
공용 processor와 DTO는 `packages/logic/src/battleSimulator/`가 소유합니다.
game-api의 processor/type 파일은 기존 import와 Redis worker 호환을 위한 re-export
경계입니다. 기존 `battle.simulate` → Redis queue → battle-sim worker 경로는 당장
삭제하지 않고 검증 및 호환 fallback으로 유지하지만 기본 화면에서는 호출하지
않습니다.
## RNG와 부작용
- 고정 seed가 있으면 기존 계약대로 한 번만 실행합니다.
- 고정 seed가 없으면 `payload.seeds[index]`를 반복 순서대로 소비합니다.
- 구형 Redis payload처럼 seed 배열이 없을 때만 실행 runtime의
`crypto.randomUUID()`를 fallback으로 사용합니다.
- 계산은 전달받은 plain object에서 새 도메인 객체를 만들며 DB, Redis, 턴 상태를
변경하지 않습니다.
## 검증
`battleSimulator.spec.ts`는 production bundle의 실제 Chromium module Worker를
실행합니다. 고정 seed와 서버 발급 seed 1000개 케이스에서 Node processor 결과와
브라우저 Worker의 전체 결과 객체를 비교하고, 기본 UI 경로가
`battle.simulate`/`battle.getSimulation`을 호출하지 않는지 확인합니다.