Merge branch 'main' into test/full-suite-20260730

# Conflicts:
#	docs/architecture/turn-state-differential-testing.md
#	docs/command-log-checklist.md
#	docs/integration-tests.md
This commit is contained in:
2026-07-30 16:21:04 +00:00
56 changed files with 939 additions and 6363 deletions
+4 -3
View File
@@ -37,8 +37,9 @@ commit 또는 삭제를 하지 말아 주세요.
## 완료 상태를 해석하는 법
2026-07-27의 백엔드 누락 감사에서는 현재 `main`에서 재현 가능한 구체적
미구현·미병합 항목을 찾지 못했습니다. 이를 “이관 전체 완료”로 해석하지 말아 주세요.
이관 상태는 현재 `main`의 코드, Git ancestry, 실행 경로와 ref 비교로
판정해 주세요. 기능 목록이나 report의 완료 표현만으로 전체 이관이 끝났다고
판정하지 말아 주세요.
- 새 차등 fixture가 mismatch를 드러내면 다시 제품 결함으로 분류해 주세요.
- green unit test는 ref 호환, 실제 DB transaction, Chromium geometry 또는
@@ -160,7 +161,7 @@ API request
- 전투·명령·월간 action의 호출, state patch, 로그와 RNG 소비 순서를
리팩터링 편의로 바꾸지 말아 주세요.
게임 난수는 기존 `packages/common/src/util/LiteHashDRBG.ts`, `RNG.ts`,
게임 난수는 `packages/common/src/util/LiteHashDRBG.ts`, `RNG.ts`,
`RandUtil.ts` 흐름을 사용해 주세요. `Math.random()` 같은 임의 경로를 gameplay에
추가하지 말아 주세요. 후보가 하나뿐인 선택, 실패 분기, fallback에서도 ref가
소비하는 RNG call을 생략하지 말아 주세요.
+90 -182
View File
@@ -1,81 +1,65 @@
# 삼국지 모의전투 HiDCHe core2026
# 삼국지 모의전투 HiDCHe core2026
`core2026`삼국지 모의전투 HiDCHe(삼모/삼모전/힏체섭)의 PHP 서비스를
TypeScript로 호환 이관하는 pnpm 모노레포입니다. 기준 구현은 이 저장소 안의
`legacy/`가 아니라 작업공간의 `../ref/sam`이며, 제품 동작·저장 상태·화면은
그 기준과 실제 실행 결과를 대조합니다.
`core2026``../ref/sam`의 PHP 서비스를 TypeScript로 호환 이관하는 pnpm
모노레포입니다. 전투·턴·권한·저장 상태·API·화면 동작은 ref 구현과 실제 실행
결과를 기준으로 검증합니다.
## 현재 상태
## 저장소 구성
2026-07-27의 `main` 기준으로 gateway, game API/프론트엔드, turn daemon과
주요 게임 명령·월간 이벤트가 구현되어 있습니다. 최근 백엔드 누락 감사에서
재현 가능한 구체적 미구현 또는 미병합 항목은 남지 않았지만, 이것이 전체
호환성이나 운영 준비 완료를 뜻하지는 않습니다.
| 경로 | 책임 |
| ------------------------------ | -------------------------------------------------------------- |
| `app/gateway-frontend` | 가입, 로그인, 로비, 계정, 관리자 UI |
| `app/gateway-api` | 계정·세션, profile 정책, operation queue, PM2 orchestration |
| `app/game-frontend` | profile별 게임 SPA와 ref 호환 화면 |
| `app/game-api` | tRPC, SSE, 인증, 조회·입력 API, 비동기 worker |
| `app/game-engine` | turn daemon, AI, 월간 lifecycle, in-memory world와 DB flush |
| `packages/common` | 공통 타입, 직렬화, 인증 token, 결정적 RNG |
| `packages/logic` | 명령, constraint, 전투, trait·item·병종 action module |
| `packages/infra` | gateway/game Prisma schema, migration, PostgreSQL·Redis client |
| `packages/tools-scripts` | resource schema 생성과 검증 |
| `resources` | scenario, map, unit set, turn-command profile |
| `tools/integration-tests` | PostgreSQL·Redis 및 ref↔core 차등 검증 |
| `tools/frontend-legacy-parity` | Chromium 기반 화면·상호작용 비교 |
| `tools/legacy-db-migration` | 레거시 장기보존 데이터 이관 CLI |
| `tools/docs` | 플레이어 커맨드 문서 생성 |
현재 열린 경계는 주로 다음과 같습니다.
구조와 실행 흐름은 [아키텍처 개요](docs/architecture/overview.md), 파일별 변경
위치는 [개발자 핸드북](docs/developer/index.md)에서 확인해 주세요. ref entry
point와 core 구현의 대응 근거는 상위 작업공간의
`../docs/ref-core2026-mapping.md`에 있습니다.
- 아직 fixture가 없는 명령 실패값·조합과 live 출병 조합의 차등 범위 확대
- 실제 배포 profile에서 worker의 장시간 소비, 재시작, host/firewall 장애 검증
- 인증 fixture가 필요한 화면과 남은 페이지의 Chromium 룩앤필 비교
- 외부 Caddy의 `/gateway/`, `/che/`, `/hwe/` 경로에서 tRPC, SSE, 자산,
새로고침과 로그인 인계의 반복 검증
- 운영 DB 이관 전 backup/restore, maintenance mode와 dry-run count 재확인
## 런타임 경계
테스트가 통과했다는 사실만으로 PHP 기준과의 호환성이 증명되지는 않습니다.
기능별 근거와 남은 범위는 작업공간의 `../docs/ref-core2026-mapping.md`
`../report/`를 함께 확인해 주세요.
Gateway는 계정과 profile 운영을 소유합니다. `gateway-api`가 gateway
PostgreSQL과 Redis session을 사용하며, game session token을 발급합니다.
관리 operation은 `GatewayProfile``GatewayOperation`에 저장되고
orchestrator가 commit별 worktree와 PM2 process를 조정합니다.
## 구성
각 game profile은 별도 PostgreSQL schema를 사용합니다. `game-api`는 인증된
요청을 검증하고 직접 처리할 mutation 또는 daemon 입력을
`InputEvent`에 기록합니다. `game-engine`은 DB lease와 fencing token을 확보한
단일 실행자로서 world를 메모리에 적재하고, 명령·월간 이벤트·로그·예약 턴과
input-event 결과를 transaction으로 반영합니다. Redis pub/sub과 SSE는 알림
경로이며 gameplay commit의 기준 저장소가 아닙니다.
| 경로 | 역할 |
| ------------------------------ | ------------------------------------------------- |
| `app/gateway-frontend` | 가입·로그인, 로비, 계정과 관리자 운영 UI |
| `app/gateway-api` | 계정·세션·profile 정책과 PM2 운영 orchestration |
| `app/game-frontend` | 게임 SPA와 ref 룩앤필 호환 화면 |
| `app/game-api` | profile별 tRPC/SSE, 조회·입력 API와 비동기 worker |
| `app/game-engine` | 턴 scheduler/daemon, 월간 처리와 DB flush |
| `packages/common` | 공통 타입, 직렬화, 결정적 RNG와 유틸리티 |
| `packages/logic` | 전투·명령·월간 action과 typed action module 로직 |
| `packages/infra` | game/gateway Prisma schema, migration과 client |
| `packages/tools-scripts` | resource schema 생성·검증 도구 |
| `tools/integration-tests` | PostgreSQL/Redis 및 ref↔core 통합·차등 테스트 |
| `tools/frontend-legacy-parity` | 실제 Chromium 기반 화면·상호작용 비교 |
| `tools/legacy-db-migration` | 레거시 장기보존 데이터 CLI 이관 |
| `tools/docs` | 플레이어 커맨드 문서 자동 생성 |
| `resources/scenario` | 시나리오 본문과 조합 가능한 이벤트·규칙 확장 |
| `docs` | VitePress 핸드북과 런타임·테스트·운영 문서 |
런타임의 영속 입력은 PostgreSQL `input_event`가 담당합니다. game API가
요청을 기록하고 turn daemon이 claim한 뒤, 게임 상태·로그·예약 턴·결과와
event 완료를 transaction으로 commit합니다. Redis는 session, realtime fan-out,
battle simulation 등 해당 기능의 계약에만 사용하며 게임 mutation의 영속
성공 여부를 대신하지 않습니다. 자세한 흐름은
[`docs/architecture/runtime.md`](docs/architecture/runtime.md)와
[`docs/architecture/turn-daemon-lifecycle.md`](docs/architecture/turn-daemon-lifecycle.md)에
있습니다.
장수 특기·관직·병종·계승·아이템 효과는
[`docs/architecture/action-module-protocol.md`](docs/architecture/action-module-protocol.md)의
계산 hook, 우선순위 trigger, 닫힌 의미 이벤트 경계를 따릅니다. 제품용
action stack은 ref의 소유권 순서를 brand한 `loadActionModuleBundle()`에서만
조립합니다.
자세한 흐름은 [런타임 아키텍처](docs/architecture/runtime.md)와
[요청·턴·저장 흐름](docs/developer/request-turn-persistence.md)을 확인해
주세요.
## 도구 체인
- pnpm workspace와 Turbo
- TypeScript `6.0.2` 고정
- Node.js + Fastify + tRPC + zod
- Vue 3 + Pinia + Vue Router + Vite
- PostgreSQL + Prisma, Redis
- Vitest Playwright/Chromium
- VitePress 정적 HTML 문서 사이트
- pnpm `11.17.0`, Turbo
- TypeScript `6.0.2`
- Fastify, tRPC, zod
- Vue 3, Pinia, Vue Router, Vite
- PostgreSQL, Prisma, Redis
- Vitest, Playwright/Chromium
- VitePress
package manager 버전은 루트 `package.json``packageManager`를 따릅니다.
현재 값은 `pnpm@11.17.0`입니다. 저장소는 Node `engines`를 고정하지 않으므로
개발 호스트의 임의 버전을 README 계약으로 간주하지 말고, lockfile 설치와
전체 검증 결과로 호환성을 확인해 주세요.
Node.js 버전은 저장소에서 고정하지 않습니다. 의존성 설치와 검증에는
`package.json``pnpm-lock.yaml`을 함께 사용해 주세요.
## 로컬 시작
## 개발 환경
```sh
pnpm install --frozen-lockfile
@@ -84,53 +68,37 @@ pnpm --filter @sammo-ts/infra prisma:generate
CI=1 pnpm typecheck
```
`.env`는 Git에서 제외됩니다. `.env.example`의 placeholder를 실제 비밀값으로
바꾸되 secret을 커밋, 명령행, 로그, 스크린샷 또는 `VITE_*` 변수에 넣지
말아 주세요.
PostgreSQL과 Redis가 필요합니다. 전체 `sam_rebuild` 작업공간에서는
`../docker_compose_files/development/README.md`의 worktree별 격리 stack을
사용할 수 있습니다. standalone checkout이라면 `.env.example` 계약에 맞는
별도 PostgreSQL/Redis를 준비해 주세요.
작업공간 helper를 사용하는 기본 예시는 다음과 같습니다.
`.env`는 Git에서 제외됩니다. 비밀값은 명령행, 로그, screenshot, report,
`VITE_*` 변수에 넣지 말아 주세요. 상위 작업공간에서는
`../docker_compose_files/development/README.md`의 PostgreSQL·Redis stack을
worktree별로 준비할 수 있습니다.
```sh
cd ../docker_compose_files/development
./scripts/prepare-instance.sh main 15433 16379 ../../core2026
./scripts/compose.sh main up -d --wait
cd ../../core2026
pnpm --filter @sammo-ts/infra prisma:generate
pnpm test:integration
```
`prepare-instance.sh`는 ignored `.env``.env.ci`를 생성합니다. 통합 테스트는
schema를 truncate하거나 Redis를 비울 수 있으므로 다른 worktree나 개발
데이터와 DB/Redis instance를 공유하지 말아 주세요. volume 삭제는 명시적으로
데이터 폐기를 결정한 경우에만 수행해 주세요.
통합 테스트는 schema를 truncate하거나 Redis key를 정리할 수 있습니다. 다른
worktree나 개발 데이터와 같은 instance를 공유하지 말아 주세요.
## 자주 쓰는 명령
## 검증 명령
```sh
pnpm lint
pnpm test
CI=1 pnpm typecheck
pnpm test
pnpm build
pnpm dev
pnpm test:integration
```
`pnpm test`의 일부 PostgreSQL/Redis 테스트는 전용 환경 변수가 없으면
의도적으로 skip됩니다. 외부 서비스가 필요한 경계까지 실행하려면 격리된
instance를 준비한 뒤 다음을 사용해 주세요.
PostgreSQL·Redis 조건부 suite는 격리된 서비스를 준비한 뒤 실행합니다.
```sh
pnpm test:integration
pnpm test:integration:conditional
```
레거시 명령의 정적 계약과 실제 Chromium 화면 비교는 각각 다음 entry
point를 사용해 주세요.
ref 명령 계약과 실제 화면은 다음 명령으로 비교합니다.
```sh
pnpm check:legacy:general
@@ -138,113 +106,53 @@ pnpm check:legacy:nation
pnpm test:e2e:frontend-legacy
```
명령들은 ref checkout, fixture, 로그인 상태 또는 별도 서비스가 필요할
수 있습니다. 실행 조건과 coverage는
[`docs/integration-tests.md`](docs/integration-tests.md)
[`docs/frontend-legacy-parity.md`](docs/frontend-legacy-parity.md)를 따라 주세요.
명령의 fixture, 서비스, 인증 요구사항은
[테스트 정책](docs/testing-policy.md),
[통합 테스트](docs/integration-tests.md),
[프론트엔드 호환 검증](docs/frontend-legacy-parity.md)에 있습니다. skip된
테스트와 mock 검증은 실제 외부 서비스 검증으로 간주하지 않습니다.
프론트엔드나 개별 서비스만 실행할 때는 workspace filter를 사용해 주세요.
```sh
pnpm --filter @sammo-ts/gateway-frontend dev
pnpm --filter @sammo-ts/gateway-api dev
pnpm --filter @sammo-ts/game-frontend dev
pnpm --filter @sammo-ts/game-api dev
pnpm --filter @sammo-ts/game-engine dev
```
## 문서 사이트
개발자 아키텍처·파일 흐름·핵심 클래스와 플레이어 커맨드·시기별 가이드는
[`docs/index.md`](docs/index.md)에서 시작합니다. VitePress 개발 서버와 정적 HTML
빌드는 다음 명령으로 실행합니다.
## 문서
```sh
pnpm docs:generate
pnpm docs:dev
pnpm docs:build
pnpm docs:preview
```
`docs:dev``docs:build`는 먼저 `pnpm docs:generate`를 실행합니다. 이 단계는
현재 장수·국가 명령 등록부와 각 `commandSpec`에서
`docs/user/command-catalog.generated.md`를 다시 만듭니다. 생성 파일을 직접
수정하지 말고 명령 정의나 생성기를 고쳐 주세요. 정적 HTML은
`docs/.vitepress/dist`에 생성되며 Git에서 제외됩니다.
`docs:generate`는 등록된 장수·국가 command spec에서
`docs/user/command-catalog.generated.md`를 만듭니다. 생성 파일은 직접
수정하지 말아 주세요.
핸드북이 조사한 코드 기준은
[`docs/reference-baseline.md`](docs/reference-baseline.md)에 고정해 두었습니다.
기능 리팩터링 뒤에는 기준 commit부터 현재 commit까지의 diff를 대조하고 관련
페이지, 생성 목록과 기준선을 같은 변경에서 갱신해 주세요.
문서의 시작점은 다음과 같습니다.
## DB schema와 migration
- [core2026 핸드북](docs/index.md)
- [개발자 핸드북](docs/developer/index.md)
- [플레이어 가이드](docs/user/index.md)
- [아키텍처 개요](docs/architecture/overview.md)
- [런타임 아키텍처](docs/architecture/runtime.md)
- [차등 검증](docs/architecture/turn-state-differential-testing.md)
- [Caddy prefix 계약](docs/e2e-caddy-routing.md)
- [레거시 DB 이관](docs/legacy-db-migration.md)
Gateway는 기본적으로 PostgreSQL `public` schema를 사용하고, 게임은
`PROFILE`별 schema를 사용합니다.
## DB와 배포
Gateway schema는 `packages/infra/prisma/gateway.prisma`, game schema는
`packages/infra/prisma/game.prisma`가 정의합니다. migration은 각각
`gateway-migrations/``migrations/`에 있습니다.
```sh
pnpm --filter @sammo-ts/infra prisma:migrate:status:game
pnpm --filter @sammo-ts/infra prisma:migrate:deploy:game
pnpm --filter @sammo-ts/infra prisma:migrate:deploy:gateway
```
새 영속 필드는 Prisma schema와 migration만 추가해서 끝내지 말아 주세요. runtime
model/type, loader, transaction flush와 실제 PostgreSQL 검증까지 연결해 주세요.
기존 migration 파일이나 checksum을 고치지 말아 주세요.
레거시 장기보존 데이터 이관은 HTTP 기능이 아닌 CLI입니다. 기본 동작은
dry-run이며 실제 쓰기에는 `--apply`가 필요합니다.
```sh
pnpm migrate:legacy -- --help
```
현재 기수의 `general`, `city`, `nation`, queue, 시장, 메시지 등은 이관
범위가 아닙니다. 운영 적용 절차와 table별 범위는
[`docs/legacy-db-migration.md`](docs/legacy-db-migration.md)와
[`tools/legacy-db-migration/README.md`](tools/legacy-db-migration/README.md)를
따라 주세요.
활성 외부 prefix는 `/gateway/`, `/che/`, `/hwe/`입니다. 앱은 필요한
listener를 `0.0.0.0`에 bind하고 prefix를 보존한 frontend, tRPC, SSE,
direct-navigation URL을 사용합니다. `/image/*`는 외부 Caddy가 소유합니다.
## 배포 경로와 자산
현재 외부 계약의 활성 경로는 gateway `/gateway/`, game `/che/``/hwe/`니다.
프론트 build와 API는 같은 prefix의 tRPC/SSE/direct navigation 계약을
지켜야 합니다. `/image/*`는 외부 Caddy가 별도 파일 시스템에서 제공하므로 앱이
rewrite하거나 복제하지 말아 주세요.
`kwe`, `twe`, `nya`, `pya`, `pwe` 같은 profile 이름이 코드나 계획 문서에
존재하더라도 외부 route가 활성화됐다는 뜻은 아닙니다. 실제 포트와 build-time
환경 변수는 [`docs/e2e-caddy-routing.md`](docs/e2e-caddy-routing.md)를
현재 인프라와 다시 대조해 주세요.
루트의 `build:server` 스크립트는 현재 profile resource를 `dist/<profile>`
복사하기 위한 placeholder이며, API·daemon·frontend의 완전한 배포 bundle을
만들지 않습니다. 운영 build는 gateway operation/orchestrator와 profile별
worktree 흐름을 사용합니다.
## 호환성 원칙
- 전투 결과, 판정·반올림·정렬과 RNG 소비 순서를 최우선으로 보존합니다.
- actor와 archive owner는 인증 session에서 서버가 결정합니다. client가 보낸
general ID나 owner 값을 권한 근거로 신뢰하지 않습니다.
- gameplay 난수는 `packages/common/src/util/LiteHashDRBG.ts`,
`RNG.ts`, `RandUtil.ts`의 기존 흐름을 사용합니다.
- ref와 같은 viewport, Chromium, font, image, 로그인 fixture에서 geometry와
hover/focus/active/disabled 상태를 비교합니다.
- 동일한 CSS class 이름만으로 page별 layout을 합치지 않습니다. 공통 token과
shell의 기준은 [`docs/frontend-css-architecture.md`](docs/frontend-css-architecture.md)입니다.
- mock/local E2E 성공과 외부 Caddy·운영 데이터 검증을 구분합니다.
## 핵심 문서
- [core2026 핸드북](docs/index.md)
- [문서 기준 커밋](docs/reference-baseline.md)
- [개발자 핸드북](docs/developer/index.md)
- [플레이어 가이드](docs/user/index.md)
- [테스트 정책](docs/testing-policy.md)
- [테스트 suite 감사](docs/test-suite-audit.md)
- [통합 테스트](docs/integration-tests.md)
- [프론트엔드 ref 호환 검증](docs/frontend-legacy-parity.md)
- [Caddy prefix E2E](docs/e2e-caddy-routing.md)
- [레거시 DB 이관](docs/legacy-db-migration.md)
- [TypeScript 버전 정책](docs/architecture/typescript-version.md)
- [저장소 작업 지침](AGENTS.md)
`build:server`는 profile resource를 `dist/<profile>`에 복사하는 도구입니다.
완전한 API·daemon·frontend 배포 bundle은 gateway orchestrator의
commit-worktree build 경로에서 구성합니다.
+4 -3
View File
@@ -12,7 +12,7 @@ export default defineConfig({
nav: [
{ text: '개발자', link: '/developer/' },
{ text: '플레이어', link: '/user/' },
{ text: '기준 커밋', link: '/reference-baseline' },
{ text: '아키텍처', link: '/architecture/overview' },
],
sidebar: {
'/developer/': [
@@ -20,7 +20,8 @@ export default defineConfig({
text: '개발자 핸드북',
items: [
{ text: '시작하기', link: '/developer/' },
{ text: '시스템 아키텍처', link: '/developer/system-architecture' },
{ text: '아키텍처 개요', link: '/architecture/overview' },
{ text: '런타임 아키텍처', link: '/architecture/runtime' },
{ text: '요청·턴·저장 흐름', link: '/developer/request-turn-persistence' },
{ text: '도메인 로직과 핵심 클래스', link: '/developer/domain-and-classes' },
{ text: '파일 지도와 변경 절차', link: '/developer/code-map' },
@@ -55,7 +56,7 @@ export default defineConfig({
text: '마지막 변경',
},
footer: {
message: '현재 구현을 설명하는 문서입니다. 기준 커밋과 검증 범위를 함께 확인해 주세요.',
message: '현재 구현을 설명하는 문서입니다. 코드와 검증 범위를 함께 확인해 주세요.',
},
},
});
+3 -3
View File
@@ -1,7 +1,7 @@
# 장수 행동 모듈 프로토콜
`packages/logic/src/actionModules/`ref의 `iAction`을 그대로 복사한 범용
interface가 아니라, 실제 core2026 실행 경계만 타입으로 표현합니다. 계산
`packages/logic/src/actionModules/`core2026 실행 경계를 타입으로
표현합니다. 계산
hook, 우선순위 trigger, 의미 이벤트는 서로 다른 실행 계약입니다.
## 세 가지 실행 계약
@@ -61,7 +61,7 @@ source입니다. 현재 이벤트는 장비 구매·판매, 계략 성공, 도
## 저장과 RNG 경계
의미 이벤트는 producer가 가진 객체를 동기적으로 수정합니다. producer는
이벤트 전후의 ref mutation 순서를 그대로 유지한 뒤 기존 effect/flush
이벤트 전후의 ref mutation 순서를 유지한 뒤 effect/flush
경계에 결과를 전달합니다.
- 장비 판매는 판매 대금 반영 → 판매 이벤트 → 슬롯 제거 순서입니다.
-165
View File
@@ -1,165 +0,0 @@
# Game Frontend SPA Plan
이 문서는 `app/game-frontend`를 Vue 3 + Pinia + Vue Router 기반 SPA로 구축하기 위한
지속 사용 가능한 작업 플랜이다. 레거시 화면(`legacy/hwe`)과 문서(`docs/`)를 기준으로
화면 목록, 인증/권한 분기, 데이터 계약을 정리하고 단계별 구현 순서를 정의한다.
## Goals
- 레거시 화면과 정보 제공 범위를 보존하면서 SPA로 재구성한다.
- 인증 상태별 정보 공개 범위를 엄격히 분리한다.
- API 통신은 tRPC로 통일하고, 추후 SSE 실시간 업데이트 경로를 고려한다.
- UI/레이아웃은 레거시와 유사하게 유지하되, 정보 구조는 SPA에 맞게 재배치 가능.
## Reference Sources
- Legacy view entrypoints: `legacy/hwe/{b_,v_,a_,index}*.php`
- Legacy Vue/TS sources: `legacy/hwe/ts/`, `legacy/hwe/ts/components/`
- Docs: `docs/architecture/overview.md`, `docs/architecture/rewrite-plan.md`,
`docs/architecture/runtime.md`, `docs/architecture/legacy-engine*.md`
## User State Matrix
- Public (미로그인/장수 미생성): 공개 정보만 노출
- 레거시 기준: 10분 캐시 지도 + 동향(최소 정보)
- Authed (로그인 + 장수 생성): 대부분의 정보 접근 허용
- Admin/GM: 운영자 전용 화면 및 도구 (후순위)
## Legacy Screen Inventory (Route 후보)
정확한 데이터 흐름/권한은 각 PHP 엔트리포인트와 연관 TS 컴포넌트에서 확인한다.
- `legacy/hwe/index.php`
- `legacy/hwe/v_cachedMap.php`
- `legacy/hwe/v_join.php`
- `legacy/hwe/v_processing.php`
- `legacy/hwe/v_board.php`
- `legacy/hwe/v_history.php`
- `legacy/hwe/v_vote.php`
- `legacy/hwe/v_auction.php`
- `legacy/hwe/v_battleCenter.php`
- `legacy/hwe/v_chiefCenter.php`
- `legacy/hwe/v_globalDiplomacy.php`
- `legacy/hwe/v_inheritPoint.php`
- `legacy/hwe/v_NPCControl.php`
- `legacy/hwe/v_nationBetting.php`
- `legacy/hwe/v_nationGeneral.php`
- `legacy/hwe/v_nationStratFinan.php`
- `legacy/hwe/v_troop.php`
- `legacy/hwe/a_bestGeneral.php`
- `legacy/hwe/a_emperior.php`
- `legacy/hwe/a_emperior_detail.php`
- `legacy/hwe/a_genList.php`
- `legacy/hwe/a_hallOfFame.php`
- `legacy/hwe/a_kingdomList.php`
- `legacy/hwe/a_npcList.php`
- `legacy/hwe/a_traffic.php`
- `legacy/hwe/b_betting.php`
- `legacy/hwe/b_currentCity.php`
- `legacy/hwe/b_genList.php`
- `legacy/hwe/b_myBossInfo.php`
- `legacy/hwe/b_myCityInfo.php`
- `legacy/hwe/b_myGenInfo.php`
- `legacy/hwe/b_myKingdomInfo.php`
- `legacy/hwe/b_myPage.php`
- `legacy/hwe/b_tournament.php`
## Architecture Decisions (SPA)
- Vue 3 + `<script setup>` 기반 단일 라우터 구조
- Pinia로 세션/월드/장수/도시/알림 상태 관리
- API: tRPC client + zod 기반 타입 안전성 유지
- UI 데이터 구성은 client-driven을 기본으로 하되 숨겨야 할 정보는 서버에서 제거
- 최소 정보 공개용 public API는 서버 캐시(10분)와 함께 제공
- 한국인 사용자 대상이며, 다국어 지원은 고려하지 않음.
- 지도 레이아웃(도시명/좌표)은 시나리오 기준으로 고정되므로 프로필 단위 캐시 가능
## Implementation Phases
### Phase 0: Discovery & Mapping
- 레거시 화면별 데이터 소스, 권한 레벨, 갱신 주기 파악
- `legacy/hwe/ts`의 컴포넌트 재사용 가능성 평가
- 화면/기능을 다음 3단계로 분류: Public / Core / Advanced
- tRPC 엔드포인트 목록과 데이터 계약 초안 작성
### Phase 1: Frontend Skeleton
- `app/game-frontend`에 Vite + Vue 3 + TS 기본 설정
- Router/Pinia/일괄 에러 처리/로딩 UI 스켈레톤 구축
- 인증 상태 전환 흐름(로그인, 장수 생성)을 위한 상태 머신 정의
### Phase 2: API Client Integration
- tRPC client 플러그인 및 요청 기본 래퍼 구성
- 요청 상관관계 `requestId` 생성 규칙 정리 (`docs/architecture/runtime.md` 참고)
- Public/Authed 라우트별 데이터 로딩 전략 정립
### Phase 3: Public Views (로그인 전/장수 미생성)
- 10분 캐시 지도/동향 화면부터 이행
- 공개 가능한 데이터만 제공하는 전용 tRPC API 추가
- 로그인/회원가입/장수 생성 진입 화면 정리
### Phase 4: Core Auth Views
- 핵심 화면 우선: 내 장수/내 도시/내 국가/세계 지도/게시판
- 상태 저장소(Pinia)를 도메인별로 분리
- 레거시 화면과 데이터 항목 매칭 후 누락 항목 체크
### Phase 5: Advanced/Peripheral Views
- 전투/외교/경매/베팅/통계/명예전당/NPC 제어 등 확장 기능
- 실시간 업데이트 필요 기능에 SSE 적용 여부 결정
### Phase 6: Hardening
- 라우트 가드, 에러 복구, 캐시/재시도 정책 확정
- 테스트(스토어 단위 + 최소 E2E 경로) 추가
- 성능 점검(맵/리스트 가상화, 이미지/아이콘 정리)
## Current Progress
- Phase 1: `app/game-frontend` 기본 스캐폴딩(Vite/Vue3/Pinia/Router/Tailwind) 완료
- Phase 2: 게임/게이트웨이 tRPC 클라이언트 분리 구성 및 env 키 추가
- 인증 부트스트랩: 게이트웨이 세션 확인 → 게임 토큰 발급 → 장수 보유 여부 반영
- 라우트: Public/Login/Join/Main 기본 가드 및 분기 처리
- 메인 화면 스켈레톤: 지도/명령/장수/도시/국가/메시지 패널 + 반응형 레이아웃 + 실시간 토글 UI
- API 보강: 게임 API에 `general.me` 추가 (메인 화면 컨텍스트 제공)
- MapViewer 1차 이식: 지도 토글/툴팁/도시 마커/디테일 모드와 Pinia 상태 연결
- 지도 레이아웃 API: 시나리오 기반 도시명/좌표 제공 + MapViewer 연동 완료
- 지도 선택 연동: 클릭 시 선택 도시 패널/명령 패널에 연결
- 레거시 맵 렌더링 보강: 테마/계절 배경, 도로 레이어, 성/이벤트 아이콘, 상태색 로직 이식
- 지도 아이콘 베이스 경로: `VITE_GAME_ASSET_URL`로 레거시 이미지 경로 주입
- Join/빙의 UI 구현: 장수 생성/빙의 탭, NPC 목록 로딩, 생성/빙의 후 세션 상태 갱신 및 메인 이동
- 게임 API: `join.getConfig`, `join.createGeneral`, `join.listPossessCandidates`, `join.possessGeneral` 추가
- Public 화면 구현: 캐시 지도/중원 정세/세력 일람/제한 장수 일람 구성
- Public API: `public.getCachedMap`, `public.getWorldTrend`, `public.getNationList`, `public.getGeneralList` 추가
- Gateway → Game handoff: 게이트웨이에서 `gameToken` 발급 후 게임 프론트에서 1회 교환(access token)하는 흐름 추가
- 실시간 업데이트(SSE): 메인 화면 토글과 EventSource 연결 + Redis pub/sub 연동
- CommandSelectForm/MessagePanel 예약/전송 플로우 연결(예턴 배치/메시지 전송)
## Next Frontend Tasks
- 게이트웨이 로그인/프로필 선택 플로우 정리 (토큰 전달 방식, 자동 로그인, 쿠키 기반 전환 고려)
- 게이트웨이/게임 프론트 도메인 경로(`VITE_GAME_WEB_URL`) 확정 및 운영 배포 경로 문서화
- MapViewer 비주얼 보강: 레거시 테마/아이콘/맵 배경 스타일 상세 이식
- 레거시 이미지 서빙 위치 확정 및 SPA 배포 시 정적 경로 매핑
- 유산 포인트/추가 옵션(도시·특기·턴타임) 이식 및 서버 검증 규칙 합의
- 화면 라우트 매핑 표 및 데이터 계약 문서화
## Deliverables
- 화면 라우트 매핑 표(legacy -> SPA)
- 권한/데이터 공개 범위 명세 (Public vs Authed)
- tRPC API 스키마 초안 + 클라이언트 호출 규칙
- SPA 초기 스캐폴딩 + 핵심 화면 MVP
## Open Questions
- Public 상태 동향 범위는 캐싱된 지도, 중원 정세, 세력일람으로 제한한다. 장수일람은 실시간 제공하되 이름/NPC 여부/국가/기본 능력치만 노출한다. 그 외 장수 정보는 캐싱된 자료에 기반하며, 빈번한 접근 제한 우회를 막기 위해 캐싱 전략을 유지한다.
- UI 스타일은 ref의 렌더링 계약을 유지한다. CSS 구조화는 공통 token과
검증된 shell 단위로 수행하되, ref의 computed DOM 치수·typography·texture·
interaction과 충돌하면 페이지별 ref 규칙을 우선한다. 새로운 디자인으로의
현대화는 이 이관의 기본 후속 단계로 간주하지 않는다.
- 실시간 업데이트는 메인 화면에 한정한다. 대상: 지도, 명령 목록, 현재 도시 정보, 소속 국가 정보, 장수 스탯, 장수 동향, 개인 기록, 중원 정세, 메시지함. 메인 화면에는 \"실시간 동기화 켬/끔\" 토글이 필요하다.
@@ -1,779 +0,0 @@
# 일반 장수 명령 차등 테스트 설계
## 상태
- 문서 상태: 단계적 구현 중
- 비교 기준: `ref/sam``ng_compare` 브랜치
- 대상: 휴식과 `cr_건국`을 포함한 일반 장수 예약 명령 55개
- 범위: 명령 결과, RNG 소비, 로그, 예약 턴 lifecycle, DB 영속화
현재 ref MariaDB와 core memory를 잇는 공통 runner, 성공 경로 55개,
실행 중 확률 실패 9개, full constraint fallback 12개와 모략 확률 clamp
8개, 모략 결과값 경계 5개, 부상 경계 3개, alternative 5개와 pre-required
turn 중간 경계 6개, post-required cooldown 경계 3개가 구현됐다.
실패 9개는 내정 critical
`주민선정/정착장려/상업투자/기술연구/물자조달`과 모략
`화계/선동/파괴/탈취`이며 RNG 전체 trace, semantic state delta와 실패
로그 본문을 비교한다. 공통 제약 7개는 무소속, 방랑국, 타국 도시, 보급
단절, 금·쌀 부족과 민심 상한을 대표한다. 모략 제약 5개는 동일 도시,
중립 도시, 불가침국, 계략 비용 금·쌀 부족을 고정한다. 모두 휴식
fallback과 RNG/state delta를 비교한다. clamp 8개는
`화계/선동/파괴/탈취` 각각의 계산 확률 0과
0.5 경계에서 성공 판정 RNG의 무소비 또는 `nextBits(1)` 소비와 전체
state delta를 비교한다. 결과값 5개는 화계 농업·상업, 선동 치안·민심,
파괴 수비·성벽의 0 바닥과 탈취의 대상 국가 자원 상한·미보급 도시 최종
저장 상태를 비교한다. 부상 3개는 화계·선동·파괴의 대상 장수별 판정,
부상도 80 상한, 병력·훈련·사기 0.98 정수 저장과 대상 장수 로그를
비교한다. alternative 5개는 해산·랜덤임관·무작위건국·출병의 모든 대체
분기에서 최초 명령 RNG의 연속 소비와 최종 상태를 비교한다. pre-required
turn 6개는 전투태세 1/2/3턴, 내정·전투 특기 초기화 1턴, 은퇴 1턴의
`last_turn`과 진행 로그, RNG 무소비를 비교한다. cooldown 3개는 특기
초기화 완료 직후 `current + 60 - preReq`, 1턴 전 차단, 경계 월 허용을
ref `next_execute` KV와 core general meta의 공통 projection으로 비교한다.
존재하지 않는 대상 경계 4개는 증여·등용의 장수 ID와 첩보·이동의 도시 ID를
고정해 원 명령 미완료, 휴식 fallback, RNG 무소비와 semantic delta를
비교한다.
2026-07-26부터 canonical snapshot은 관찰 장수의 `rank_data`도 비교한다.
ref의 `RankColumn` 37종만 의미 행으로 정규화하며, ref에서 자연 `general`
column인 경험·공헌·숙련을 위해 core가 보유한 7개 mirror row는 비교에서
제외한다. 화계 fixture는 같은 초기 `firenum`에서 성공 명령 뒤 양쪽이
동일하게 1 증가하는지 확인한다. 은퇴 fixture는 37종 전부를 서로 다른
비영 값으로 채운 뒤 양쪽이 전부 0으로 만드는지 확인한다. 이 검증으로
일반 명령 snapshot에서 누락됐던 명장일람 누적치와 은퇴 후 메모리→DB
재저장 경로를 관찰한다.
자원 인자·보유량 경계 13개는 증여·헌납·군량매매의 100단위 반올림과
100..max clamp 9개, 헌납의 보유량보다 큰 요청·최소 쌀 미달 2개,
증여의 최소 쌀 보존·자기 자신 거부 2개를 비교한다.
필수 인자 객체 자체를 생략한 `증여`는 ref/core 모두
`인자가 올바르지 않습니다. 증여 실패.` 로그와 휴식 fallback, RNG 무소비,
동일 state delta를 확인했다. 같은 형태의 `이동`은 ref가
`CityConstBase::byID(null)` TypeError로 종료된다. 예약 API가 이 입력을
사전에 거부하므로 정상 제품 경로가 아니며, 손상된 저장 큐에서도 daemon을
중단하지 않고 실패 로그와 휴식으로 복구하는 core 동작을 의도적 안전 차이로
유지한다.
나머지 명령별 제약 실패·값 경계와 전체 core PostgreSQL 재조회가 완료
기준을 통과하기 전까지 55개 명령 전체의 동적 호환 상태를 `확인`으로
올리지 않는다.
## 결정 요약
일반 장수 명령은 하나의 canonical fixture로 다음 세 결과를 만든다.
1. ref PHP가 격리된 MariaDB에서 실제 예약 턴을 실행한 결과
2. core2026이 `InMemoryTurnWorld`에서 같은 예약 턴을 실행한 결과
3. 2의 dirty state를 격리된 PostgreSQL에 flush하고 다시 읽은 결과
두 비교를 모두 통과해야 한다.
```text
canonical fixture
├── ref fixture adapter ──> PHP full turn ──> MariaDB ──> ref projection
│ │
└── core fixture adapter ─> InMemory full turn ──────────┼─ exact semantic diff
│ │
└── DB hooks ─> PostgreSQL ─┘
```
- `ref projection == core memory projection`은 호환 로직을 검증한다.
- `core memory projection == core persisted projection`은 loader/flush를
검증한다.
- raw MariaDB dump와 raw PostgreSQL dump는 비교하지 않는다. 테이블 구조와
필드 소유권이 다르므로 의미 필드로 정규화한 JSON을 비교한다.
- 명령 결과와 RNG는 원칙적으로 exact 비교한다. 저장 레이아웃 차이만
projection에서 제거한다.
## 목표와 비목표
### 목표
- 동일한 게임 상태, 명령, 인자, 시각과 seed로 ref/core를 실행한다.
- 성공, 실행 중 실패, 제약 실패, alternative, 다중 턴과 전처리 경로를
구분한다.
- 장수·도시·국가·부대·외교·예약 큐·rank·로그 등 모든 관찰 가능한
side effect를 비교한다.
- RNG의 domain, 호출 순서, 연산, 인자와 결과를 비교한다.
- core 메모리 결과가 실제 PostgreSQL에 같은 의미로 저장되고 재시작 후
같은 상태로 로드되는지 검증한다.
- 명령이 예상 밖의 raw DB 필드를 바꾸면 projection 누락으로 실패시킨다.
- 각 명령의 테스트 근거와 아직 없는 경로를 기계적으로 집계한다.
### 비목표
- MariaDB와 PostgreSQL의 물리 schema, sequence, index 또는 내부 row ID를
동일하게 만드는 일
- 레거시 제품 브랜치 `devel`에 비교 endpoint를 추가하는 일
- 운영/개발 DB를 초기화하거나 기존 ref DB를 fixture 저장소로 사용하는 일
- 현재 구현을 정답으로 삼아 snapshot을 자동 승인하는 일
- UI와 API 요청 형식 검증을 이 suite 하나로 대체하는 일
## 테스트 계층
### 1. core logic regression
기존 `InMemoryTurnWorld`와 예약 턴 handler를 사용한다. 빠른 테스트이며
fixture의 core memory projection을 단언한다. DB 제약이나 직렬화는 보장하지
않는다.
### 2. ref ↔ core differential integration
Docker의 ref PHP CLI와 격리 MariaDB를 호출하므로 integration test로
분류한다. 두 엔진의 canonical 결과와 RNG trace를 비교한다.
### 3. core persistence integration
동일한 core 실행 결과를 `databaseHooks`로 격리 PostgreSQL에 flush하고
새 connection으로 다시 load한다. 메모리 projection과 재조회 projection을
비교한다.
### 4. 선택적 daemon system test
예약 API, command queue와 daemon lifecycle까지 필요한 대표 명령만 별도
system test로 둔다. 55개 수치 호환성 검증을 이 느린 계층에 모두 넣지 않는다.
## 제안 파일 구조
구현 시 다음 경계를 사용한다.
```text
core2026/
tools/integration-tests/
fixtures/general/
manifest.json
base/
scenario-2.json
che_화계/
success-basic.json
failure-probability.json
probability-clamp-max.json
injury-and-item.json
src/general-command/
fixtureSchema.ts
canonicalSchema.ts
compareCanonical.ts
referenceRunner.ts
referenceProjection.ts
coreMemoryRunner.ts
coreMemoryProjection.ts
coreDatabaseRunner.ts
coreDatabaseProjection.ts
changedPathAudit.ts
databaseSandbox.ts
tracingRng.ts
test/
generalCommandDifferential.test.ts
generalCommandPersistence.test.ts
generalCommandComparator.test.ts
ref/sam/ # ng_compare 전용
hwe/compare/
general_command_trace.php
GeneralCommandFixture.php
GeneralCommandProjection.php
ComparisonTracingRNG.php
docker_compose_files/
general-command-differential/
compose.yml
.env.example
README.md
scripts/
prepare-secrets.sh
initialize-templates.sh
run-fixture.sh
verify-isolation.sh
secrets/
mariadb_password.example
postgres_password.example
```
기존 `battleDifferential.test.ts`의 workspace 탐색, `docker compose exec`,
stdin JSON 전달과 tracing RNG 패턴을 재사용한다. 일반 명령용 코드는 전투
fixture와 섞지 않는다.
integration Vitest는 case DB 수명주기를 예측할 수 있도록
`fileParallelism: false`, 기본 `testTimeout: 120_000`을 유지한다. CI
sharding은 별도 Compose project와 worker prefix를 받은 프로세스 사이에서만
수행한다.
## 실행 격리
### 전용 Compose stack
`general-command-differential`은 개발, ref UI, input-event E2E와 수명주기가
다르므로 별도 Compose stack으로 둔다.
필수 service:
- `ref-db`: 고정 버전 MariaDB, 외부 port 미공개
- `ref-runner`: 기존 ref PHP image, CLI 명령만 허용
- `core-db`: 고정 버전 PostgreSQL, 프로젝트 전용 loopback port
- 선택 profile `system`: Redis와 core daemon runner
DB data directory는 suite 전용 `tmpfs`를 기본으로 한다. 테스트가 중단되어도
운영·개발 volume을 가리킬 수 없게 Compose project, container, network와
port 이름을 별도로 고정한다.
실제 비밀값은 Git에서 제외된 secret file로만 주입한다. ref의 생성된
`d_setting/DB.php` overlay는 `/run` 또는 `mktemp` 아래에 mode `0600`으로
만들어 container에 read-only mount하고 종료 시 삭제한다. JSON 결과,
명령행과 보고서에는 credential을 넣지 않는다.
### ref는 transaction rollback을 사용하지 않는다
레거시 `general`, `city`, `general_turn`, `general_record`, `rank_data`
핵심 테이블은 Aria engine이다. 따라서 transaction rollback은 fixture
격리를 보장하지 못한다.
ref 격리는 다음 순서로 수행한다.
1. suite 시작 시 schema와 비교 기준 scenario config로 immutable template
DB 두 개(root/HWE)를 만든다.
2. case마다 검증된 prefix
`sammo_gc_ref_<worker>_<caseHash>`의 DB를 새로 만든다.
3. template dump를 case DB에 복원하고 fixture override를 적용한다.
4. case DB를 가리키는 임시 `RootDB.php`/`DB.php` overlay로 PHP CLI를
한 번 실행한다.
5. canonical 결과를 읽은 뒤 case DB를 삭제한다.
6. cleanup 대상 이름이 허용 prefix와 정확히 일치하지 않으면 삭제를
거부한다.
이 흐름은 기존 `test-fast-forward-sandbox.sh`의 DB 복제, DB 이름 override,
원본 DB 불변 검사 패턴을 재사용한다. 기존 `sammo_ref_hwe`는 읽거나
복제 기준으로도 사용하지 않고, suite가 직접 만든 template만 사용한다.
### core DB 격리
suite 전용 PostgreSQL 안에 `public``che` schema 및 migration을 적용한
template database를 만든다. case마다 template에서 새 database를 만들고
case 종료 후 검증된 prefix에 한해 삭제한다.
각 case는 다음을 보장한다.
- 새 Prisma connection 사용
- 명시적인 profile/scenario
- fixture에 없는 이전 row가 없음
- flush 이후 connection을 닫고 새 connection으로 재조회
- dirty-state acknowledge는 DB commit 이후에만 수행
- 실패한 case도 case database만 정리
현재 `.env.ci`가 가리키는 개발 DB의 `public`/`che` schema를 truncate하는
기존 initialization test는 이 suite의 backend로 사용하지 않는다.
## 기준 scenario와 base state
fixture는 양쪽에 공통으로 존재하는 `scenario_2`의 rule, map과 unit set을
사용한다. 전체 scenario의 NPC와 국가를 그대로 seed하면 검색, 정렬과
무작위 후보가 fixture 밖 row에 영향을 받으므로 다음 base를 별도로 만든다.
- scenario config, `GameConst`, map과 unit set은 `scenario_2`에서 로드
- map의 모든 도시는 중립 기본 row로 생성
- 장수, 국가, 부대, 외교와 예약 턴은 fixture가 명시한 것만 생성
- game/root env는 명령 생성과 턴 실행에 필요한 key 전체를 명시
- 시간은 UTC ISO 문자열과 게임 연·월을 함께 고정
- 자동 턴은 기본적으로 끄고 fixture가 요구할 때만 켬
- 국가 예약 명령은 기본 휴식으로 고정
- actor만 due 상태로 두고 대상/보조 장수의 turn time은 실행 범위 밖으로 둠
- fixture의 test hidden seed를 임시 `UniqueConst.php` overlay와 core world
meta 양쪽에 같은 값으로 주입
base generator가 양쪽 입력을 따로 만들되, 기준 값은 하나의 canonical
base JSON에서 가져온다.
## Fixture 계약
fixture는 구현 내부 객체가 아니라 게임 의미를 기술한다. Zod schema와
JSON Schema를 함께 생성하고 resource validation에 포함한다.
개념 예시는 다음과 같다.
```json
{
"schemaVersion": 1,
"id": "che_화계/success-basic",
"scenario": "scenario_2",
"execution": {
"mode": "full-turn",
"year": 200,
"month": 1,
"turnTime": "0200-01-01T00:00:00.000Z",
"hiddenSeed": "general-differential-test-seed",
"actorGeneralId": 101,
"command": {
"key": "che_화계",
"args": { "destCityId": 2 }
},
"autorun": false
},
"world": {
"generals": [],
"cities": [],
"nations": [],
"troops": [],
"diplomacy": [],
"generalTurns": [],
"nationTurns": [],
"rank": [],
"events": [],
"rootUsers": []
},
"observe": {
"generalIds": [101, 201],
"cityIds": [1, 2],
"nationIds": [1, 2],
"metaKeys": ["intel_exp", "firenum", "killturn", "myset", "inherit_lived_month"],
"collections": ["logs", "generalTurns", "rank"]
},
"expect": {
"outcome": "success"
},
"evidence": {
"legacyFiles": ["hwe/sammo/Command/General/che_화계.php", "hwe/func.php"],
"contract": "화계 성공 기본 경로"
}
}
```
실제 fixture에는 생략 없이 모든 필수 entity field를 넣는다. `hiddenSeed`
테스트 전용 공개값이며 운영 seed를 복사하지 않는다.
### Fixture 불변식
- 양쪽에서 같은 numeric ID를 사용한다.
- 이름과 정렬 순서에 영향을 주는 문자열도 명시한다.
- `undefined`를 사용하지 않는다. 값 없음은 `null`, collection 없음은 `[]`,
object 없음은 `{}`로 표현한다.
- 자동 증가 DB ID는 fixture의 의미 식별자로 사용하지 않는다.
- 확률 결과를 임의 stub으로 강제하지 않는다. seed와 입력 상태로 원하는
분기를 만들고 RNG trace를 고정한다.
- fixture의 기대값은 core 실행 결과에서 생성하지 않는다. ref trace와
레거시 코드 근거를 함께 기록한다.
- fixture update는 별도 review 대상이며 snapshot 자동 갱신 명령을
제공하지 않는다.
## 실행 모드
### `full-turn`
기본 모드다. 양쪽의 실제 예약 턴 실행 순서를 거친다.
- lived-month 증가
- preprocess trigger, 부상 회복, 병력 군량
- block
- 필요 시 국가 명령
- 일반 명령 제약, term stack, cooldown, alternative
- 명령별 RNG
- queue shift
- killturn, myset, autorun limit
- next turn time
- retirement/deletion
- DB flush와 로그 확정
55개 호환 판정은 이 모드의 결과로 한다.
### `command-only`
수식과 RNG 분기를 좁게 진단하는 보조 모드다. 전체 호환 판정의 근거로
단독 사용하지 않는다. full-turn 실패가 preprocess/queue 문제인지 명령
resolver 문제인지 분리할 때 사용한다.
## ref runner 계약
`general_command_trace.php`는 다음 조건을 모두 만족해야 한다.
- `PHP_SAPI === 'cli'`
- 명시적인 `SAMMO_GENERAL_COMPARE=1` guard
- stdin의 fixture 한 개만 처리
- case 전용 DB 이름 외 연결 거부
- fixture seed 후 `TurnExecutionHelper`의 실제 경로 호출
- logger를 flush하고 DB 결과를 읽은 뒤 JSON 한 개 출력
- stdout에는 JSON만, 진단은 stderr
- 기존 명령 계산·정렬·RNG·DB mutation 순서를 바꾸지 않음
- 종료 전 원본 template/main DB가 변하지 않았음을 runner가 검사
출력은 engine-specific raw state와 trace를 담는다. canonical 변환은
`referenceProjection.ts`가 수행한다. PHP와 TS projection이 서로의
오류를 그대로 복제하지 않도록 한 구현을 공유하지 않는다.
현재 `TurnExecutionHelper`는 내부에서 RNG를 직접 생성하므로 `ng_compare`
최소 test seam이 필요하다. 기본값은 기존 `new RandUtil(new
LiteHashDRBG(seed))`를 그대로 사용하고, CLI guard가 활성화된 경우에만
같은 DRBG를 tracing proxy로 감싸는 factory를 주입한다. observer on/off에서
동일 fixture의 DB 결과가 같다는 계측 무영향 테스트를 ref에 둔다.
## core runner 계약
### memory runner
- fixture를 `TurnWorldSnapshot`, `TurnWorldState`,
`InMemoryReservedTurnStore`로 변환
- production `createReservedTurnHandler``InMemoryTurnProcessor` 사용
- fixture가 선언한 한 장수의 한 due turn만 실행
- 실행 전후 world, dirty state, queue, logs와 RNG trace 반환
`reservedTurnHandler`도 RNG를 내부 생성하므로 production default를 보존하는
선택적 `rngFactory(domain, seed)` test seam을 둔다. 옵션을 생략한 경로는
현재 구현과 byte-for-byte 같은 DRBG를 만들고, 테스트만 tracing wrapper를
반환한다. seed 구성 자체를 runner에서 다시 구현하지 않는다.
### database runner
- 같은 fixture를 case PostgreSQL에 seed
- production loader로 새 `InMemoryTurnWorld` 생성
- 같은 handler/processor 실행
- production `databaseHooks`로 commit
- 모든 connection을 닫고 새 loader/Prisma query로 결과 재조회
- memory projection과 persisted projection 비교
테스트 전용 runner가 `buildCityUpdate` 같은 private production helper를
복제해 직접 호출해서는 안 된다. 반드시 production hook 경계를 지나야
`City.state`/`City.meta.state`와 같은 투영 오류를 검출할 수 있다.
## RNG trace
RNG trace entry는 다음 형태다.
```json
{
"domain": "generalCommand",
"sequence": 3,
"operation": "nextInt",
"arguments": { "maxInclusive": 99 },
"result": 42
}
```
domain은 최소 다음을 구분한다.
- `preprocess`
- `nationCommand`
- `generalCommand`
- `uniqueLottery`
- 명령이 추가로 분리한 명시적 child domain
비교 규칙:
- entry 수 exact
- domain과 sequence exact
- primitive RNG operation exact
- arguments exact
- integer/byte/bit result exact
- float는 JSON number의 실제 값 exact
PHP/JavaScript의 동일 계산 결과가 표현 차이만 보이는 경우에도 RNG trace
허용치를 넓히지 않는다. 게임 상태 수치에 불가피한 부동소수점 차이가 있으면
필드별 compatibility rule을 근거와 함께 별도 등록한다.
## Canonical snapshot
결과 envelope:
```text
schemaVersion
fixtureId
engine ref | core-memory | core-db
execution
requestedCommand
resolvedCommand
outcome success | command-failure | constraint-denied | fallback | error
blockedReason
nextTurnTime
before
after
delta
rng
unmappedChanges
```
`before``after`는 다음 collection을 ID/복합 key로 정렬한다.
### 일반 상태
- world: year, month, tick/turn term, killturn 설정과 명령이 읽거나 바꾼 env
- generals: scalar stats, 소속, 관직, 자원, 병력, 부상, 장비, 특기, 성격,
능력 경험, turn time, lastTurn
- general meta: killturn, myset, autorun/cooldown, 계승, command별 변경 key
- item inventory: instance ID 자체보다 item key, slot, charges와 values
- rank: `(generalId, type, value)`
### 도시·국가·관계
- cities: 소속, state, 인구와 최대치, 내정치와 최대치, 수비·성벽, 보급,
전선, trust, trade, region, conflict
- nations: 수도, 군주, 규모, 자원, 기술, power, type과 명령 관련 meta
- troops: leader/id, nation, name와 membership에 의해 바뀐 장수 troopId
- diplomacy: 양방향 row를 `(srcNationId, destNationId)`로 정렬하고 state,
term, dead/showing과 의미 meta 비교
### side-effect collection
- generalTurns, nationTurns: logical key, action, args와 순서
- logs: scope, category, subtype, year, month, 대상 ID와 최종 formatting text
- messages
- events
- hall/archive rows
- inheritance point/log/result
- access-log 변경
- 생성·삭제된 entity ID
DB auto ID, `createdAt`, `updatedAt`, connection별 sequence와 물리 JSON key
순서는 제거한다. JSON object key는 정렬하지만 array 순서는 보존한다.
core memory log는 production `finalizeLogEntry`를 같은 고정 year/month/time
context로 통과시킨 뒤 persisted/ref log와 비교한다.
## 명령 seed 밖의 난수
사료 NPC 랜덤임관의 PHP 전역 `shuffle()`처럼 `RandUtil` 밖의 난수는
명령 seed만으로 재현할 수 없다. 이 값을 무시하거나 fixture에서 해당
분기를 제외하지 않는다.
비교 환경에서는 외부 비결정값을 명시적 input tape로 취급한다.
```json
{
"externalDecisions": [
{
"domain": "legacyGlobalShuffle",
"input": [201, 202, 203],
"output": [203, 201, 202]
}
]
}
```
- ref `ng_compare`는 CLI guard 아래에서만 해당 shuffle 호출을 작은 wrapper로
통과시키고 tape의 permutation을 사용한다.
- core runner도 같은 permutation을 입력으로 사용한다.
- input/output 원소가 정확한 permutation이 아니면 실패한다.
- guard가 꺼진 wrapper는 PHP builtin `shuffle()`을 그대로 한 번 호출한다.
- 계측 on/off의 deterministic 경로 무영향 테스트와, guard-off shuffle의
permutation property test를 별도로 둔다.
- canonical trace에는 external decision의 소비 순서도 포함한다.
따라서 이 경로의 호환 의미는 “같은 외부 shuffle 결과가 주어졌을 때 이후
후보 평가, `RandUtil` 소비와 선택 결과가 같다”이다. PHP 전역 RNG 자체를
core command seed와 같다고 주장하지 않는다.
## 변경 경로 감사
fixture가 명시한 필드만 비교하면 예상하지 못한 side effect를 놓칠 수 있다.
각 runner는 engine raw state의 before/after diff도 만든다.
- 알려진 raw path는 canonical mapping registry에 연결한다.
- 명령이 바꾼 raw path가 canonical path나 명시적 ignore rule에 연결되지
않으면 `unmappedChanges`에 넣고 실패한다.
- ignore rule은 timestamp, auto ID처럼 게임 의미가 없는 필드만 허용한다.
- ignore entry에는 engine, raw path pattern, 이유와 근거 파일을 기록한다.
- broad wildcard로 `aux`, `meta` 또는 전체 table을 무시하지 않는다.
이 gate는 새로운 aux/meta key나 누락된 persistence table을 조용히
통과시키지 않기 위한 것이다.
## 비교 규칙
기본은 deep exact equality다.
정규화 허용:
- snake_case ↔ camelCase
- `intel``intelligence`
- ref scalar/aux/rank ↔ core typed field/meta/rank row
- `None``null`인 장비 없음 표현
- DB가 부여한 ID와 timestamp 제거
- JSON object key 정렬
정규화 금지:
- 반올림, truncation 또는 clamp 결과 변경
- RNG 호출 추가/삭제/재정렬
- collection 정렬로 실제 처리 순서 은폐
- 로그 문구나 조사 차이를 임의로 제거
- 누락 row를 기본값으로 만들어 일치시킴
- 도시 `state``meta.state`처럼 소유권이 다른 필드를 같은 값으로 간주
필드별 허용 차이는 `compatibility-rules.json`에 다음을 반드시 기록한다.
- fixture 또는 command
- canonical path
- 허용 조건
- 레거시/core 근거 파일
- 사용자 상태와 이후 턴에 영향이 없는 이유
- 제거 예정 여부
## 화계 첫 acceptance matrix
설계 검증의 첫 명령은 `che_화계`로 한다. 최소 fixture:
| fixture | 보호할 계약 |
| ------------------------ | ------------------------------------------------------------------------------------- |
| `success-basic` | 비용, 성공, 농업·상업 피해, state 32, 경험·공헌·지력 경험·firenum, queue/LastTurn/log |
| `failure-probability` | 실패 RNG, 피해·부상·아이템 소비 없음, 실패 경험/공헌 범위 |
| `probability-clamp-zero` | 음수 계산 결과 0 clamp와 RNG 소비 |
| `probability-clamp-max` | 0.5 상한, 거리 나눗셈 순서 |
| `defence-population` | 대상국 장수만 포함, 최대 지력, 인원 log2, 보급·치안 보정 |
| `injury-and-item` | 장수별 부상 판정/상한 80, crew/train/atmos 0.98 절삭, 일회용 아이템 소비 |
| `damage-clamp` | 낮은 농업·상업에서 0 미만 방지 |
| `constraint-denied` | 중립/같은 도시/자원/보급/불가침 제약과 queue fallback |
`success-basic`과 확률 상한 fixture는 `City.meta.state``City.state`
혼동을 검출한다. 상한 fixture에서 실제로 이 결함을 발견해 화계 성공 시
물리 `City.state=32`를 저장하도록 수정했다. 같은 fixture 묶음에서 선동의
전체 city spread가 불필요한 front 재계산을 일으키는 문제와 MariaDB
`city.trust FLOAT` 재조회 정밀도 차이도 발견해 부분 patch와 6자리
유효숫자 저장 경계로 바로잡았다.
결과값 경계 fixture에서는 파괴의 전체 city spread도 선동과 같이
불필요한 front 재계산을 일으키는 문제를 발견해 수비·성벽·state 부분
patch로 좁혔다. 미보급 탈취는 레거시가 자원 감소 update 뒤 원본
`destCity.agri/comm`을 다시 저장하므로 관찰 가능한 최종 자원은 변하지
않는다. 버그로 보이지만 호환 계약으로 보존하고 `state=32`만 남긴다.
부상 경계 fixture에서는 화계의 `계략로 인해` 조사를 레거시
`계략으로 인해`로 수정하고, 선동·파괴에 누락된 대상 장수 부상 로그를
추가했다. 병력·훈련·사기의 0.98 감소는 ref 정수 DB 저장처럼 내림이 아닌
반올림을 적용한다.
## 55개 명령 coverage manifest
`manifest.json`은 PHP command inventory와 TS registry의 합집합을 기준으로
생성 검증한다. 각 명령에는 다음 case class가 필요하다.
- 실행 가능한 기본 경로
- full constraint 거부
- 확률 분기가 있으면 성공과 실패
- 값 clamp/상한/하한이 있으면 경계
- 다중 턴이면 stack 중간과 완료/레거시 reset
- alternative가 있으면 원 명령과 대체 명령
- 생성/삭제/소속 변경이 있으면 관련 collection
- 아이템/특기/국가/관직 보정이 있으면 최소 한 개
- 명령별 외부 side effect가 있으면 해당 collection
적용 불가능한 case class는 `notApplicable`과 레거시 근거를 기록한다.
fixture가 하나 있다는 이유만으로 명령을 covered로 세지 않는다.
inventory gate가 검증할 집계:
- ref command 수
- core command 수
- command별 필수 case class 충족
- fixture schema validation
- orphan fixture
- skip/todo/notApplicable 근거
- unmapped changed path 수
## 실패 출력과 artifact
실패 시 다음만 출력한다.
- fixture ID와 세 engine
- 첫 canonical mismatch path와 양쪽 값
- 전체 RNG trace에서 최초 divergence와 전후 제한된 window
- unmapped raw changed path
- 재현 명령
전체 DB dump, credential, 운영 hidden seed, 사용자 개인정보는 출력하거나
artifact로 저장하지 않는다. 필요하면 canonical JSON만 Git 제외된
`artifacts/general-command/<fixture>/`에 저장하고 민감 필드를 검사한다.
## 실행 명령 계약
구현 후 제공할 명령:
```bash
# stack 준비와 격리 검증
pnpm general-diff:prepare
pnpm general-diff:verify-isolation
# 화계 한 fixture
pnpm general-diff:test --fixture che_화계/success-basic
# 한 명령 전체
pnpm general-diff:test --command che_화계
# 55개 coverage 및 전체 differential/persistence
pnpm general-diff:check
```
명령은 기존 개발 DB를 발견하거나 전용 stack marker가 없으면 실행을
거부한다. `general-diff:check`는 skip이 있으면 실패한다. 진단용
`--allow-skip`은 CI와 완료 판정에서 금지한다.
## CI 단계
### PR fast gate
- fixture/schema/manifest validation
- comparator unit test
- 변경된 명령과 공통 실행기 영향 명령의 differential
- 해당 fixture의 core persistence
### compatibility gate
- 55개 manifest의 모든 필수 case
- ref/core RNG 및 canonical state exact diff
- core memory/persisted exact diff
- unmapped changes 0
- skip 0
명령별 case를 shard할 수 있지만 같은 case DB를 공유하지 않는다.
## Comparator 자체 검증
`generalCommandComparator.test.ts`는 실제 production 결과 없이도 다음
synthetic mismatch를 각각 검출해야 한다.
- 숫자 1 차이
- null과 누락
- array 순서
- RNG operation/result
- 로그 대상과 format text
- queue shift 방향
- 생성 대신 수정
- 삭제 누락
- `City.state``meta.state`
- 알려지지 않은 aux/meta/raw DB 변경
protected behavior를 고의로 perturb한 mutation audit를 fixture review에
포함한다. 단순히 현재 구현을 snapshot으로 저장하고 다시 읽는 테스트는
호환 근거로 인정하지 않는다.
## 구현 순서
1. canonical fixture/schema와 comparator unit test
2. 전용 Compose stack과 원본 DB 불변 isolation test
3. ref `general_command_trace.php``ng_compare`에 추가
4. core memory runner
5. core DB runner와 새 connection reload
6. 화계 acceptance matrix 완성 및 현재 city state 결함 수정
7. 계략 4종으로 공통 runner 검증
8. side-effect family별 대표 명령 확장
9. 55개 manifest 완성
10. project-generated Prisma client를 사용하는 전용 connection readiness
check 추가
11. `pnpm general-diff:check`를 compatibility gate로 등록
ref 계측 commit, core test/infra commit, 제품 버그 수정 commit은 분리한다.
## 완료 기준
다음이 모두 증명되어야 이 설계의 구현을 완료로 본다.
- 전용 stack이 기존 ref/dev DB를 바꾸지 않는 isolation test 통과
- ref runner가 CLI/test guard 밖에서 접근 불가
- 화계 8개 acceptance fixture 통과
- 화계 `state=32`가 core DB 재조회에도 유지됨
- 각 fixture의 ref/core RNG trace exact 일치
- 외부 비결정 경로는 input tape 소비와 이후 결과 exact 일치
- 55개 명령의 필수 manifest case 충족
- ref/core memory canonical diff 0
- core memory/persisted canonical diff 0
- unmapped changed path 0
- skip/todo 0
- comparator mutation audit 통과
- 관련 typecheck, lint, build와 integration test 통과
- `docs/ref-core2026-mapping.md`에서 동적 검증 근거와 미확인 항목 갱신
- 실행 명령, 기준 commit과 fixture 목록을 `report/`에 기록
이 조건 전에는 정적 제약·로그 검사와 smoke test 통과만으로 일반 장수 명령
전체를 `확인` 또는 이식 완료로 표시하지 않는다.
-136
View File
@@ -1,136 +0,0 @@
# Legacy Command Catalog
This file lists the command keys available under `legacy/hwe/sammo/Command/`.
Command keys match class names and file names; they are the values stored in
`general_turn.action` and `nation_turn.action`.
## Command Resolution
- General: `buildGeneralCommandClass($type)` in `legacy/hwe/func_converter.php`
- Nation: `buildNationCommandClass($type)` in `legacy/hwe/func_converter.php`
- `null` or empty `type` defaults to `휴식`
- Post-req turn cooldowns are stored in:
- `next_execute` KV (per general)
- `nation_env` KV (per nation)
## Execution Semantics (BaseCommand)
- Conditions are checked via constraint sets (permission/min/full).
- `getPreReqTurn()` is enforced by `LastTurn` term stacking.
- `getPostReqTurn()` is enforced by `next_execute` (cooldown).
- If `run()` returns `false`, `getAlternativeCommand()` may be used.
## Command Prefixes and Scenarios
Command keys are scenario-driven. The prefixes (e.g. `che_`, `cr_`, `event_`)
identify the rule set or scenario extension, and the active scenario chooses
which commands are available via `GameConst` overrides. See
`docs/architecture/legacy-scenarios.md` for details.
## General Commands (`legacy/hwe/sammo/Command/General/`)
| Key | Display name (actionName) |
| -------------------- | ------------------------- |
| `che_NPC능동` | `NPC능동` | Planned |
| `che_강행` | `강행` | Planned |
| `che_거병` | `거병` | Ported |
| `che_건국` | `건국` | Ported |
| `che_견문` | `견문` | Ported |
| `che_군량매매` | `군량매매` | Ported |
| `che_귀환` | `귀환` | Planned |
| `che_기술연구` | `기술 연구` | Ported |
| `che_내정특기초기화` | `내정 특기 초기화` | Ported |
| `che_농지개간` | `농지 개간` | Ported |
| `che_단련` | `단련` | Ported |
| `che_등용` | `등용` | Ported |
| `che_등용수락` | `등용 수락` | Planned |
| `che_랜덤임관` | `무작위 국가로 임관` | Planned |
| `che_모반시도` | `모반시도` | Planned |
| `che_모병` | `모병` | Ported |
| `che_무작위건국` | `무작위 도시 건국` | Planned |
| `che_물자조달` | `물자조달` | Ported |
| `che_방랑` | `방랑` | Ported |
| `che_사기진작` | `사기진작` | Ported |
| `che_상업투자` | `상업 투자` | Ported |
| `che_선동` | `선동` | Ported |
| `che_선양` | `선양` | Planned |
| `che_성벽보수` | `성벽 보수` | Ported |
| `che_소집해제` | `소집해제` | Ported |
| `che_수비강화` | `수비 강화` | Ported |
| `che_숙련전환` | `숙련전환` | Ported |
| `che_요양` | `요양` | Ported |
| `che_은퇴` | `은퇴` | Ported |
| `che_이동` | `이동` | Ported |
| `che_인재탐색` | `인재탐색` | Ported |
| `che_임관` | `임관` | Ported |
| `che_장비매매` | `장비매매` | Planned |
| `che_장수대상임관` | `장수를 따라 임관` | Planned |
| `che_전투태세` | `전투태세` | Planned |
| `che_전투특기초기화` | `전투 특기 초기화` | Ported |
| `che_접경귀환` | `접경귀환` | Planned |
| `che_정착장려` | `정착 장려` | Ported |
| `che_주민선정` | `주민 선정` | Ported |
| `che_증여` | `증여` | Planned |
| `che_집합` | `집합` | Ported |
| `che_징병` | `징병` | Ported |
| `che_첩보` | `첩보` | Ported |
| `che_출병` | `출병` | Ported |
| `che_치안강화` | `치안 강화` | Ported |
| `che_탈취` | `탈취` | Ported |
| `che_파괴` | `파괴` | Ported |
| `che_하야` | `하야` | Ported |
| `che_해산` | `해산` | Planned |
| `che_헌납` | `헌납` | Ported |
| `che_화계` | `화계` | Ported |
| `che_훈련` | `훈련` | Ported |
| `cr_건국` | `건국` | Planned |
| `cr_맹훈련` | `맹훈련` | Planned |
| `휴식` | `휴식` | Ported |
## Nation Commands (`legacy/hwe/sammo/Command/Nation/`)
| Key | Display name (actionName) |
| -------------------- | ------------------------- |
| `che_감축` | `감축` |
| `che_국기변경` | `국기변경` |
| `che_국호변경` | `국호변경` |
| `che_급습` | `급습` |
| `che_몰수` | `몰수` |
| `che_무작위수도이전` | `무작위 수도 이전` |
| `che_물자원조` | `원조` |
| `che_발령` | `발령` |
| `che_백성동원` | `백성동원` |
| `che_부대탈퇴지시` | `부대 탈퇴 지시` |
| `che_불가침수락` | `불가침 수락` |
| `che_불가침제의` | `불가침 제의` |
| `che_불가침파기수락` | `불가침 파기 수락` |
| `che_불가침파기제의` | `불가침 파기 제의` |
| `che_선전포고` | `선전포고` |
| `che_수몰` | `수몰` |
| `che_의병모집` | `의병모집` |
| `che_이호경식` | `이호경식` |
| `che_종전수락` | `종전 수락` |
| `che_종전제의` | `종전 제의` |
| `che_증축` | `증축` |
| `che_천도` | `천도` |
| `che_초토화` | `초토화` |
| `che_포상` | `포상` |
| `che_피장파장` | `피장파장` |
| `che_필사즉생` | `필사즉생` |
| `che_허보` | `허보` |
| `cr_인구이동` | `인구이동` |
| `event_극병연구` | `극병 연구` |
| `event_대검병연구` | `대검병 연구` |
| `event_무희연구` | `무희 연구` |
| `event_산저병연구` | `산저병 연구` |
| `event_상병연구` | `상병 연구` |
| `event_원융노병연구` | `원융노병 연구` |
| `event_음귀병연구` | `음귀병 연구` |
| `event_화륜차연구` | `화륜차 연구` |
| `event_화시병연구` | `화시병 연구` |
| `휴식` | `휴식` |
## Open Questions / Follow-ups
- Prefix semantics (`che_`, `cr_`, `event_`) are not documented here and may be
scenario- or mode-specific.
-209
View File
@@ -1,209 +0,0 @@
# Legacy General AI (GeneralAI)
This document summarizes how `legacy/hwe/sammo/GeneralAI.php` selects nation and
general commands, which data fields it relies on, and how policies shape NPC
behavior. It also outlines considerations for an in-memory rewrite of the AI
loop.
## Entry Points
- `chooseNationTurn(NationCommand $reservedCommand)`
- `chooseInstantNationTurn(NationCommand $reservedCommand)`
- `chooseGeneralTurn(GeneralCommand $reservedCommand)`
All three call `updateInstance()` first, which caches current state and derives
key AI decisions (diplomacy state, general type, policy setup).
## Deterministic RNG
`GeneralAI` uses `LiteHashDRBG` seeded with:
```
hiddenSeed + "GeneralAI" + year + month + generalID
```
This makes AI choices reproducible per turn. All random choices (`choice`,
`choiceUsingWeight`, `nextBool`) flow through this RNG.
## State Snapshot and Derived Fields
`updateInstance()` pulls and caches:
- `game_env` (via `KVStorage`): year, month, startyear, turnterm, develcost,
init_year/init_month, killturn, global NPC policy defaults.
- `city` (current city row, from `general` -> `city`).
- `nation` (nation row, or fallback for neutral).
- `nation_env` (KVStorage): npc policy overrides, prev income, last attackable.
- `nation['aux']` decoded JSON.
- `general` primary stats (leadership/strength/intel; both full and adjusted).
- `baseDevelCost` and `maxResourceActionAmount`.
- `dipState`, `attackable`, `warTargetNation`.
- `genType` (type flags derived from stats + policy thresholds).
Derived groupings are computed lazily:
- `categorizeNationCities()` => `nationCities`, `frontCities`, `supplyCities`,
`backupCities` with per-city `dev` and `important` scores.
- `categorizeNationGeneral()` => user/NPC buckets, war/civil buckets,
troop leaders, lost generals, chief list.
## Diplomacy State (`calcDiplomacyState`)
Diplomacy state controls war/peace behavior:
- `d평화`: no war declarations, no active war.
- `d선포`: declaration in progress, no active war.
- `d징병`: pre-war recruitment window.
- `d직전`: immediate pre-war.
- `d전쟁`: active war, or recently lost a front (grace period).
Key inputs:
- `diplomacy` rows (`state`, `term`) for current nation.
- `front` cities with `supply=1`.
- `year/month` relative to `startyear` (early-game war lockout).
`warTargetNation` tracks eligible enemies:
- `2` = currently at war.
- `1` = declaration in progress / pre-war.
- `0` is used as "any neighbor" fallback when no active target exists.
## General Type (`calcGenType`)
General type is a bitmask:
- `t무장` (strength-leaning)
- `t지장` (intelligence-leaning)
- `t통솔장` (leadership threshold for war-capable NPCs)
Logic:
- Strength vs intel decides initial bias.
- If the weaker stat is within 80%, a probabilistic hybrid type can be added.
- `t통솔장` is set when `leadership >= nationPolicy->minNPCWarLeadership`.
## Policy Inputs (Autorun)
`AutorunNationPolicy` and `AutorunGeneralPolicy` are built from:
- per-user autorun options (`env['autorun_user']['options']`)
- `nation_env` overrides (`npc_nation_policy`, `npc_general_policy`)
- global defaults (`env['npc_nation_policy']`, `env['npc_general_policy']`)
- live nation + env snapshot
Policies provide:
- `priority` action lists
- `can{Action}` toggles
- thresholds (min crew, safe population ratio, resource floors, etc.)
## Nation Turn Behavior (`chooseNationTurn`)
Nation-level choices run only for NPCs (`npc >= 2`) or for autorun users:
1. Apply quarterly promotions and tax/bill adjustments.
2. Honor reserved nation commands if valid.
3. Iterate policy `priority`, invoking matching `do{Action}`.
4. Fall back to neutral (empty) nation command if all fail.
### Major Action Groups
- **Troop movement**
- `do부대전방발령`, `do부대후방발령`, `do부대구출발령`
- user/NPC versions to move generals between front/back/supply cities
- checks `frontCities`, `supplyCities`, `last발령`, and war route.
- **Resource distribution**
- `do유저장포상`, `doNPC포상`, `doNPC몰수`
- uses resource floors (`reqNation*`, `reqNPC*`, `reqHuman*`)
- sorts by each general's gold/rice, excludes inactive (`killturn <= 5`)
targets, and preserves the legacy geometric-mean amounts and candidate
weights.
- **Diplomacy**
- `do불가침제의`: respond to assistance requests with NAP offer.
- `do선전포고`: probabilistic declaration when strong enough.
- **Capital relocation**
- `do천도`: moves capital based on population, dev, and connectivity.
## General Turn Behavior (`chooseGeneralTurn`)
General-level decisions are layered:
1. NPC message broadcast if `npcmsg` and RNG triggers.
2. Reserved command is honored if valid (unless `휴식`).
3. Immediate recovery if `injury > cureThreshold`.
4. Special cases:
- A nationless NPC troop leader shortens `killturn` and keeps its reserved
command; an affiliated type-5 leader refreshes `killturn` and uses `집합`.
- wandering lords decide on founding, one-edge movement toward a cached
target, or disbanding.
5. Iterate policy `priority`, invoking `do{Action}`.
6. Fallback to `do중립`.
### Major Action Groups
- **Domestic development**
- `do일반내정`, `do전쟁내정`, `do긴급내정`
- weighted by `city` dev rates and general type flags.
- **War preparation**
- `do징병`, `do전투준비`, `do출병`
- strict checks on crew, train, atmos, population and diplomacy state.
- **Mobility**
- `do전방워프`, `do후방워프`, `do내정워프`, `do귀환`
- uses `front/supply/backup` cities and population thresholds.
- **Resource handling**
- `do금쌀구매` (trade), `doNPC헌납` (donation).
- **Neutral behavior**
- `do중립` selects between `물자조달`, `인재탐색`, `견문`.
## Data Fields Accessed (Representative)
GeneralAI reads or writes:
- **Nation**: `nation`, `capital`, `gold`, `rice`, `tech`, `level`, `type`,
`chief_set`, `rate`, `bill`, `aux`.
- **City**: `city`, `nation`, `supply`, `front`, `pop`, `pop_max`, `trust`,
`agri/comm/secu/def/wall` and `*_max`, `trade`, `level`, `region`.
- **General**: `npc`, `officer_level`, `officer_city`, `killturn`, `injury`,
`troop`, `crew`, `train`, `atmos`, `defence_train`, `gold`, `rice`, `belong`,
`permission`, `npcmsg`, `dex1..dex5`, `armType`, `affinity`, `makelimit`.
- **KVStorage**: `npc_*_policy`, `prev_income_*`, `last_attackable`,
`last천도Trial`, `resp_assist*`.
## In-Memory Rewrite Considerations
To port the AI to an in-memory state model without behavior drift:
- **Snapshot-first**
- Build a per-turn `GameSnapshot` containing env, nation, cities, generals,
diplomacy, and nation_env. `GeneralAI` should read only from this snapshot.
- **Derived caches**
- Cache `DiplomacyState`, `CityBuckets`, `GeneralBuckets`, and `WarRoute`.
- Use lazy recalculation and invalidate only the affected region/city/general
after a command is applied.
- **Deterministic ordering**
- For candidate lists, sort by ID before weighted RNG to preserve parity.
- RNG seeding should keep the exact per-turn seed scheme to ensure replay.
- **Command evaluation**
- Keep `hasFullConditionMet()` semantics intact by providing the same
generalized context (`general`, `city`, `nation`, `dest*`).
- **Policy snapshots**
- Cache policy values per turn and avoid reading KVStorage per action.
- When policies change, treat it as an explicit state transition.
These guidelines mirror the current "derive once, then select via priority"
pattern and minimize resimulation deltas in the rewrite.
## Migrated decision-parity regression
`app/game-engine/test/generalAiLegacyDecisionParity.test.ts` records focused
final-command expectations extracted from `ref/sam` `ng_compare@fe9ae978`.
Its matrix varies diplomacy/war state, city development and population,
technology/year ceilings, general gold/rice and casualty ranks, stats and
affinity, reserved/special NPC state, nation treasury reserves, and command
availability. It also asserts RNG-sensitive candidate weights where consuming
the same random branch is part of the final decision.
This is compatibility evidence for the represented decision branches. The
long-running NPC scenario suites remain smoke tests and are not a substitute
for this branch-level matrix.
-106
View File
@@ -1,106 +0,0 @@
# Legacy Auctions and Betting
This document covers the auction and betting systems: how auctions open/close,
what resources they trade, and how betting payouts are calculated. References
include `legacy/hwe/sammo/Auction*.php`, `legacy/hwe/func_auction.php`, and
`legacy/hwe/sammo/Betting.php`.
## Auction Types and Entry Points
- `Auction` (base class): shared bid logic and close-date extensions.
- `AuctionBuyRice`: sell rice for gold.
- `AuctionSellRice`: sell gold for rice.
- `AuctionUniqueItem`: bids use inheritance points for unique items.
- `processAuction()` in `legacy/hwe/func_auction.php` closes finished auctions.
- `registerAuction()` creates neutral (NPC-hosted) buy/sell auctions.
## Auction Data Model
Primary tables:
- `ng_auction`: auction metadata (type, target, host, close date, detail).
- `ng_auction_bid`: per-bid rows for each auction.
The `AuctionInfo` and `AuctionInfoDetail` DTOs are serialized into
`ng_auction` columns for structured access.
## Core Auction Flow
1. **Open**
- `AuctionBasicResource::openResourceAuction()` validates amounts and
host resources, then inserts `ng_auction`.
- `AuctionUniqueItem::openItemAuction()` checks availability, reserves
unique items, and posts a global history notice.
2. **Bid**
- Bids are inserted into `ng_auction_bid` and can extend the close date.
- Reverse auctions (when `detail->isReverse`) sort by lowest bid.
3. **Close** (`tryFinish()` from the concrete auction)
- If no bids: `rollbackAuction()` returns resources to host and logs.
- With bids: `finishAuction()` transfers resources, logs, and sends messages.
4. **Processing**
- `processAuction()` scans auctions past `close_date` and finishes them.
## Unique Item Auctions
- Currency: inheritance points (`ResourceType::inheritancePoint`).
- Host identity is obfuscated using `genObfuscatedName()` and a deterministic
shuffled name pool stored in `game_env.obfuscatedNamePool`.
- Limits and availability are enforced against `GameConst::$allItems` and
existing ownership in `general` rows.
## Betting System
Betting is an independent KV-backed system:
- `Betting::openBetting()` stores `BettingInfo` in KV storage (`betting`).
- `Betting::bet()` inserts or updates `ng_betting` rows and charges either
gold or inheritance points.
- `Betting::giveReward()` distributes payouts to winners (exclusive or shared).
Event actions controlling lifecycle:
- `OpenNationBetting`: opens a nation-strength bet and registers a
`DESTROY_NATION` event to close it.
- `FinishNationBetting`: evaluates winners when only N nations remain.
## RNG Notes
- `Auction::genObfuscatedName()` uses `hiddenSeed + 'obfuscatedNamePool'` to
shuffle the name pool once and reuse it from `game_env`.
- `registerAuction()` relies on an injected RNG to randomize neutral auctions.
## Open Questions / Follow-ups
- `registerAuction()` is called from legacy `func_gamerule.php` after the
logical month changes. Its RNG is seeded before that change with the previous
year/month, then consumes one power roll per nation and the conditional
tournament roll before auction generation.
- Scenario-specific overrides of unique auction timing have not been found.
Core2026 currently applies the legacy constants against `tickSeconds`.
## core2026 compatibility implementation (2026-07-25)
| Contract | core2026 path | Status |
| -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
| Resource auction list/open/bid | `game-frontend/AuctionView.vue``game-api/router/auction``auctionOpen`/`auctionBid` engine commands | Confirmed by integration test |
| Resource reservation/refund/settlement | opener reserves host gold/rice; bidder reserves and refunds the previous top bid; finalizer transfers or returns resources | Confirmed by integration test |
| Unique list/detail privacy | API replaces host and bidder identity with deterministic aliases and never returns unique-auction general IDs | Confirmed by API implementation and typecheck |
| Unique open | engine validates configured quantity, slot ownership, one active host/item auction, and atomically inserts the host's first bid while deducting `inheritance_point.previous` | Confirmed by integration test |
| Unique bid | row-locked auction, minimum `max(1%, 10)`, conditional point deduction, previous-top-bid refund, slot/top-bid conflict checks | Confirmed by integration test |
| Unique finalization | requested extension, per-slot and total ownership-limit extension, host neutralization, item inventory update | Confirmed by integration test |
| Timer/worker | API updates the Redis timer after open/bid; existing scheduler claims due rows and sends durable `auctionFinalize` commands | Confirmed by integration test for timer seed and durable command finalization |
| Neutral merchant registration | `neutralRegistrar.ts` runs at the month boundary, reproduces the previous-month seed and preceding RNG consumption, and queues host `0` auctions in the same DB transaction as the month state | Confirmed by PHP differential, engine boundary, PostgreSQL idempotency, and full integration tests |
Unique auction mutations deliberately run in the turn-engine database
transaction. Point reads use row locks and deductions use conditional updates,
so simultaneous requests cannot spend the same previous-season inheritance
points twice. A failed open command creates neither an auction nor an orphaned
inheritance request marker.
Neutral registration intentionally counts every historical host-`0` resource
auction, including finished rows, because the legacy probability denominator
does not filter on `finished`. Average gold/rice includes only generals with
`npcState < 2` and is clamped to 1,000..20,000. The month marker and generated
auction rows are persisted together under a PostgreSQL advisory transaction
lock, preventing the same month from producing duplicate merchant auctions
during daemon retry.
-48
View File
@@ -1,48 +0,0 @@
# Legacy City Data and Helpers
This document describes how city data is defined and accessed in the legacy
engine. References include `legacy/hwe/sammo/CityConstBase.php`,
`legacy/hwe/sammo/CityInitialDetail.php`, and `legacy/hwe/sammo/CityHelper.php`.
## City Constants (CityConst)
`CityConstBase` defines default city data and lookup helpers:
- `CityConstBase::$initCity` includes:
- id, name, level, population, agri/comm/secu/def/wall,
region, coordinates, and path connections.
- Region and level maps convert labels to numeric codes.
- `CityConstBase::byID()` and `byName()` return `CityInitialDetail` objects.
During scenario build, the engine generates `d_setting/CityConst.php` from the
scenario map file, replacing or extending these defaults.
## CityInitialDetail
`CityInitialDetail` is a plain data object containing the initial
static attributes for each city:
- `id`, `name`, `level`, `population`
- `agriculture`, `commerce`, `security`, `defence`, `wall`
- `region`, `posX`, `posY`, `path` (adjacent city ids)
## CityHelper Cache
`CityHelper` caches live DB city rows for fast lookup:
- `getAllCities()` returns a map keyed by city id.
- `getAllNationCities($nationID)` returns all cities for a nation.
- `getCityByName()` resolves a name to a DB row (with a warning if missing).
Cache invalidation is manual via `CityHelper::flushCache()`.
## Path Connectivity
City connectivity (used for supply and movement) is defined by the
`path` list in `CityConst` and referenced by systems like
`UpdateCitySupply` to traverse same-nation city graphs.
## Open Questions / Follow-ups
- Scenario-specific map files (`legacy/hwe/scenario/map/*.php`) override city
data; document per-map deltas when needed.
@@ -1,60 +0,0 @@
# Legacy Game Constants and Unit Sets
This document summarizes the static configuration layers that define game
rules and unit sets. References include `legacy/hwe/sammo/GameConstBase.php`,
`legacy/hwe/sammo/GameUnitConstBase.php`, `legacy/hwe/sammo/GameUnitDetail.php`,
and `legacy/hwe/sammo/GameUnitConstraint/*`.
## GameConst (Rules)
`GameConstBase` defines the default rule set:
- Turn limits, stat caps, and progression thresholds.
- Resource floors (`basegold`, `baserice`) and maintenance coefficients.
- Command lists (`availableGeneralCommand`, `availableChiefCommand`).
- Availability lists for specials, items, and nation types.
- Scenario overrides apply via `Scenario::buildConf()`.
During scenario build, the engine generates `d_setting/GameConst.php` from
`GameConstBase` and scenario overrides (see
`docs/architecture/legacy-scenarios.md`).
## Unit Sets (GameUnitConst)
`GameUnitConstBase` defines the base unit list and their combat properties:
- Unit identifiers, arm types, and labels.
- Base attack/defence stats, speed, avoid, magic coefficients.
- Cost and rice consumption.
- Attack/defence coefficients vs. other arm types.
- Attached trigger lists (`initSkillTrigger`, `phaseSkillTrigger`).
- Requirements via `GameUnitConstraint` objects.
Scenario build emits `d_setting/GameUnitConst.php` based on the selected
`unitSet` (`scenario/unit/*.php`).
## GameUnitDetail
`GameUnitDetail` is the runtime representation of a crew type:
- Computes derived attack/defence using general stats and tech (`getTechAbil`).
- Calculates resource costs (`costWithTech`, `riceWithTech`).
- Applies attack/defence multipliers by opponent crew type.
- Can include an `iActionList` (crew-type-specific action modules).
Crew types are also `iAction` providers, enabling triggers and stat modifiers
through `General::getActionList()`.
## Unit Constraints
Unit availability is controlled by `GameUnitConstraint` implementations:
- `ReqTech`, `ReqCities`, `ReqRegions`, `ReqMinRelYear`, `Impossible`, etc.
- Each constraint exposes `test(...)` and `getInfo()` for UI display.
- `ReqNationAux` gates unit availability by `nation.aux` flags (e.g. research
unlocks like `can_화시병사용`), which are written by nation commands.
## Open Questions / Follow-ups
- Scenario-specific overrides may redefine available units or unit sets;
document those deltas per scenario when needed.
@@ -1,62 +0,0 @@
# Legacy Constraint System
This document describes the constraint framework used to validate command
preconditions. References include `legacy/hwe/sammo/Constraint/Constraint.php`
and `legacy/hwe/sammo/Constraint/ConstraintHelper.php`.
## Core Concepts
Constraints are reusable predicate classes that validate a commands inputs and
state. They are built per-command and executed in order:
- Each constraint extends `Constraint` and implements `test()`.
- `Constraint::testAll()` iterates a list of constraints and returns the first
failure `[constraintName, reason]` or `null` for success.
- `BaseCommand::hasFullConditionMet()` uses these to decide command validity.
## Required Input Flags
Constraints declare required inputs using bit flags:
- `REQ_GENERAL`, `REQ_CITY`, `REQ_NATION`
- `REQ_DEST_GENERAL`, `REQ_DEST_CITY`, `REQ_DEST_NATION`
- `REQ_ARG` with typed sub-flags:
- `REQ_STRING_ARG`, `REQ_INT_ARG`, `REQ_NUMERIC_ARG`, `REQ_BOOLEAN_ARG`,
`REQ_ARRAY_ARG`, `REQ_BACKED_ENUM_ARG`
`Constraint::checkInputValues()` enforces these expectations and throws if
inputs are missing or malformed.
Some constraints expect `general.aux` to be preloaded as `auxVar`. Command
evaluation calls `General::unpackAux()` before constraint checks to satisfy
these requirements.
## Constraint Helper DSL
`ConstraintHelper` provides factory-style helpers used in command definitions:
- Examples: `AllowWar()`, `NearCity($distance)`, `ReqGeneralGold($amount)`,
`NotOccupiedDestCity()`, `AllowDiplomacyStatus(...)`, etc.
- These helpers return `[ConstraintName, arg]` tuples consumed by
`Constraint::testAll()`.
## Common Constraint Classes
Constraints are organized by domain:
- **Diplomacy**: `AllowDiplomacyStatus`, `AllowDiplomacyBetweenStatus`.
- **Nation/City**: `OccupiedCity`, `NotCapital`, `RemainCityCapacity`.
- **General**: `ReqGeneralCrew`, `ReqGeneralGold`, `MustBeTroopLeader`.
- **Routing**: `HasRoute`, `HasRouteWithEnemy`.
Each constraint sets a failure reason string used by UI and logs.
## Rewrite References
Rewrite constraint contracts and runtime split details live in
`docs/architecture/rewrite-constraints.md`.
## Open Questions / Follow-ups
- Some constraints rely on `env` values (`turnterm`, `year`, etc.); document
each commands exact `env` payload when porting.
@@ -1,55 +0,0 @@
# Legacy Diplomacy and Messaging
This document summarizes the legacy message system and how diplomatic messages
trigger nation command flows. References include
`legacy/hwe/sammo/Message.php`, `legacy/hwe/sammo/DiplomaticMessage.php`,
`legacy/hwe/sammo/MessageTarget.php`, and `legacy/hwe/sammo/Target.php`.
## Message Types and Mailboxes
`Message` supports multiple mailbox types:
- **Public**: `MAILBOX_PUBLIC` (global announcements).
- **National**: `MAILBOX_NATIONAL + nationID` for diplomacy/nation mail.
- **Private**: mailbox id is the receiver general id.
Message types (`MessageType`) include `public`, `national`, `private`,
and `diplomacy`.
## Message Lifecycle
- `Message::send()` (not shown here) writes to the `message` table.
- `Message::getMessagesFromMailBox()` reads by mailbox/type with validity
filtering (`valid_until > now`).
- `Message::buildFromArray()` instantiates:
- `DiplomaticMessage` when type is diplomacy.
- `ScoutMessage` or `RaiseInvaderMessage` when `option.action` matches.
## DiplomaticMessage Flow
`DiplomaticMessage` wraps diplomacy requests and accepts/rejects them:
- Supported actions:
- `TYPE_NO_AGGRESSION` (불가침)
- `TYPE_CANCEL_NA` (불가침 파기)
- `TYPE_STOP_WAR` (종전)
- `agreeMessage()` validates:
- message is still valid
- receiver is a valid diplomacy officer
- On accept, builds and runs the matching nation command:
- `che_불가침수락`, `che_불가침파기수락`, `che_종전수락`
- Uses `NoRNG` to avoid randomness in acceptance.
Each acceptance updates `diplomacyDetail` with command briefs and writes logs.
## Message Targets
`MessageTarget` and `Target` provide lightweight identifiers:
- `MessageTarget` includes general/nation names, colors, and icons.
- Helper constructors (`buildQuick`, `buildFromGeneralObj`) resolve from DB.
## Open Questions / Follow-ups
- The full list of `MessageType` values and message table schema live outside
this file and can be documented alongside the UI mailbox APIs.
@@ -1,89 +0,0 @@
# Legacy Economy and Monthly Updates
This document summarizes the legacy economic pipeline: monthly income,
maintenance, population growth, city supply recalculation, and nation level
updates. References include `legacy/hwe/func_time_event.php`,
`legacy/hwe/sammo/Event/Action/*`, and `legacy/hwe/sammo/TurnExecutionHelper.php`.
## Entry Points
- `TurnExecutionHelper::executeAllCommand()`
- Calls `runEventHandler(PRE_MONTH)``preUpdateMonthly()``turnDate()`
`runEventHandler(MONTH)``postUpdateMonthly()`.
- Event actions under `legacy/hwe/sammo/Event/Action/` execute concrete
economic updates.
## Monthly Income (ProcessIncome)
`Event/Action/ProcessIncome` distributes **gold** and **rice** on schedule:
- Collects all nations and cities, groups generals per nation.
- Computes income using:
- `getGoldIncome()` / `getRiceIncome()` from `func_time_event.php`.
- `getWallIncome()` for rice from walls (same file).
- Applies nation tax rate (`rate_tmp`) and salary ratio (`bill`).
- Pays generals by `dedication` via `getBill()`.
- Updates `nation_env.prev_income_{gold|rice}` and logs to history.
Key dependencies:
- `GameConst::$basegold`, `GameConst::$baserice` for minimum reserves.
- `GameConst::$resourceActionAmountGuide` for action amounts.
- `iAction::onCalcNationalIncome()` for nation-type modifiers.
## Semiannual Maintenance (ProcessSemiAnnual)
`Event/Action/ProcessSemiAnnual` applies upkeep and growth:
- City stats decay by 1% (agri/comm/secu/def/wall).
- Population growth via `popIncrease()`:
- Uses tax rate (`rate_tmp`) and security.
- Nation type can adjust via `onCalcNationalIncome('pop', ...)`.
- General and nation resources decay (15% bands based on thresholds).
## War Income and Casualties (ProcessWarIncome)
`Event/Action/ProcessWarIncome`:
- Converts `city.dead` into national gold income (supply-based war income).
- Returns 20% of `dead` to population (`pop += dead * 0.2`), then clears `dead`.
## City Supply and Isolation (UpdateCitySupply)
`Event/Action/UpdateCitySupply`:
- BFS from each nation capital through `CityConst` paths to mark `supply = 1`.
- Unsupplied cities (`supply = 0`) receive 10% stat/pop loss monthly.
- Generals in unsupplied cities lose 5% crew/train/atmos.
- Cities with `trust < 30` become neutral (nation = 0), and officers are reset.
## Nation Level Updates (UpdateNationLevel)
`Event/Action/UpdateNationLevel`:
- Nation level increases based on number of level≥4 cities.
- On level-up:
- Grants gold/rice bonus.
- Logs global and national history.
- Initializes `nation_turn` rows for new chief levels.
- Runs a unique-item lottery (deterministic RNG tagged `nationLevelUp`).
## City Trade Rates (RandomizeCityTradeRate)
- Once per month, adjusts `city.trade` with deterministic RNG:
- Probability depends on city level.
- Trade rate ranges 95105 when triggered.
## RNG Notes
Economic events using RNG seed with `UniqueConst::$hiddenSeed` and a
feature-specific tag:
- `RandomizeCityTradeRate`: `hiddenSeed + 'randomizeCityTradeRate' + year + month`
- `UpdateNationLevel`: `hiddenSeed + 'nationLevelUp' + year + month + nationID`
## Open Questions / Follow-ups
- `preUpdateMonthly()`, `postUpdateMonthly()`, and `turnDate()` live in
`legacy/hwe/func_time_event.php` and `legacy/hwe/func.php`; their full
side effects should be expanded alongside this document.
-150
View File
@@ -1,150 +0,0 @@
# Legacy Event System
This document summarizes how the legacy event system executes scenario- and
turn-based events, and how static event hooks are wired into commands. Primary
references include `legacy/hwe/sammo/TurnExecutionHelper.php`,
`legacy/hwe/sammo/Event/*`, and `legacy/hwe/sammo/StaticEventHandler.php`.
## Entry Points
- `TurnExecutionHelper::runEventHandler(EventTarget $eventTarget)`
- Loads `event` table rows by target and priority, evaluates conditions, and
runs actions.
- `StaticEventHandler::handleEvent(...)`
- Invoked by many commands and API handlers to run per-action static hooks.
## Event Table Schema
`event` rows are stored in the legacy DB schema (`legacy/hwe/sql/schema.sql`):
- `id`: auto-increment primary key
- `target`: enum of `PRE_MONTH`, `MONTH`, `OCCUPY_CITY`, `DESTROY_NATION`, `UNITED`
- `priority`: higher first (default 1000)
- `condition`: JSON array (condition DSL)
- `action`: JSON array (action DSL)
Indexes: `(target, priority, id)` for dispatch ordering. Both `condition` and
`action` are JSON-validated by DB constraints.
## Event Table Dispatch
`runEventHandler()` drives the dynamic event pipeline:
1. Query `event` rows with `target = {PRE_MONTH|MONTH|OCCUPY_CITY|DESTROY_NATION|UNITED}`
(ordered by `priority DESC, id ASC`).
2. Decode `condition` and `action` JSON.
3. Build a `Event\EventHandler` with condition + action lists.
4. Execute `tryRunEvent($env)` where `$env` is `game_env` KV storage plus
`currentEventID`.
Events are used inside the monthly pipeline and in special moments like
city occupation (`EventTarget::OCCUPY_CITY`, called by some commands).
### Rewrite runtime status
`app/game-engine/src/turn/monthlyEventHandler.ts` now dispatches persisted
`event` rows for `pre_month` and `month`. It preserves the legacy
`priority DESC, id ASC` order, evaluates `Date`, `DateRelative`, `RemainNation`,
and the boolean logic operators, and records `DeleteEvent` through the normal
turn dirty-state transaction.
The calendar calls `pre_month` before changing the world date and `month`
after changing it. Action names are resolved through an explicit registry.
An unported action stops the turn with its action name and event id instead of
being silently ignored. The runtime registry currently covers
`ProcessIncome`, `NoticeToHistoryLog`, `NewYear`, and `ResetOfficerLock`;
the remaining legacy action catalog must be migrated before full event-action
parity can be claimed.
## Condition and Action DSL
`Event\Condition::build()` and `Event\Action::build()` decode JSON arrays into
class instances:
- **Condition**
- Supports logic combinators (`and`, `or`, `xor`, `not`) via
`Event\Condition\Logic`.
- Built-in condition types include:
- `Date`, `DateRelative`, `Interval`
- `RemainNation`
- `ConstBool`
- Conditions return `{ value, chain }` for tracing.
- **Action**
- Actions are classes under `Event/Action/` with `run(array $env)`.
- The dispatcher instantiates them from `action` arrays like
`['ProcessIncome', 'gold']`.
## Common Event Actions (Examples)
These are the action modules observed in the legacy tree:
- **Economy & upkeep**: `ProcessIncome`, `ProcessSemiAnnual`, `ProcessWarIncome`
- **World state**: `UpdateCitySupply`, `UpdateNationLevel`, `RandomizeCityTradeRate`
- **NPC/Invader flow**: `RaiseInvader`, `RaiseNPCNation`, `ProvideNPCTroopLeader`
- **Betting & unique items**: `OpenNationBetting`, `FinishNationBetting`,
`LostUniqueItem`, `MergeInheritPointRank`
- **Event lifecycle**: `DeleteEvent`, `NoticeToHistoryLog`
All action execution uses the event environment (`year`, `month`, `startyear`,
`turnterm`, etc.) coming from `game_env`.
## Static Events (Command Hooks)
Static events are hooks triggered directly by commands/APIs:
- `StaticEventHandler::handleEvent()` looks up handler names from
`GameConst::$staticEventHandlers[$eventType]`.
- Handlers live under `legacy/hwe/sammo/StaticEvent/` and implement
`BaseStaticEvent::run()`.
- These hooks are used to extend command behavior without modifying the
command code itself (e.g., troop join/exit side effects).
### Static Handler Map Sources
`GameConst::$staticEventHandlers` defaults to an empty array in
`legacy/hwe/sammo/GameConstBase.php`. Scenario JSON can override it:
- `legacy/hwe/scenario/scenario_911.json` (only observed override in repo)
- `sammo\\API\\Troop\\JoinTroop``event_부대탑승즉시이동`
- `sammo\\Command\\Nation\\che_발령``event_부대발령즉시집합`
Static handler names should map to classes in `legacy/hwe/sammo/StaticEvent/`
(class name matches handler key).
## RNG Notes
Dynamic event actions can use deterministic RNG by constructing
`LiteHashDRBG` with `UniqueConst::$hiddenSeed` and an event-specific tag.
Examples include `RandomizeCityTradeRate` and `UpdateNationLevel`.
core2026의 authoritative game-state 계산에서는 `Math.random()`을 사용하지
않는다. 레거시 PHP 전역 `shuffle()`을 사용하던 다음 경로도 입력별 독립
`LiteHashDRBG` substream으로 고정한다.
- `RaiseNPCNation`: `hiddenSeed, "RaiseNPCNation", year, month,
"emptyCities"`
- `RaiseInvader`: `hiddenSeed, "RaiseInvader", year, month,
"martialDex"`
- 빈 tournament pattern: `hiddenSeed, "monthly", previousYear,
previousMonth, "tournamentPattern"`
- scenario general 사망월: scenario title/start year, source group,
general id/name/death year와 `"deathMonth"`
독립 substream을 쓰는 이유는 레거시 전역 shuffle의 비결정성만 제거하고
이미 호환 검증된 action/monthly RNG의 후속 소비 위치는 바꾸지 않기
위해서다. 테스트는 해당 경로에서 `Math.random()`을 호출하면 즉시
실패한다. 인증 token, request/event correlation ID 같은 보안·운영 식별자의
`crypto` RNG와 사용자가 랜덤 능력치 버튼으로 만드는 클라이언트 입력은
이 게임-state 재현 계약과 구분한다.
root ESLint 설정도 `app/game-engine/src`와 `packages/logic/src`에서
`Math.random` property 사용을 오류로 처리하므로 새 authoritative 경로가
같은 결함을 다시 도입할 수 없다.
## Open Questions / Follow-ups
- `Event\Engine` is a stub with a TODO; it is not currently used in the main
turn pipeline.
- Verify whether any runtime code injects additional static handlers beyond
scenario JSON overrides.
@@ -1,135 +0,0 @@
# Legacy Engine Execution Flow
This document summarizes how the legacy engine advances turns and executes
commands. It focuses on the execution pipeline, RNG seeding, and state writes
based on `legacy/hwe/sammo/API/Global/ExecuteEngine.php`,
`legacy/hwe/sammo/TurnExecutionHelper.php`, and `legacy/hwe/sammo/Command/BaseCommand.php`.
## Entry Point: ExecuteEngine API
- Endpoint: `legacy/hwe/sammo/API/Global/ExecuteEngine.php`
- Session: no session required (`NO_SESSION`)
- Optional `serverID` guard via `UniqueConst::$serverID`
- Calls `TurnExecutionHelper::executeAllCommand($updated, $locked)`
- Returns `{ updated, locked, lastExecuted }`
## Global Turn Loop (`TurnExecutionHelper::executeAllCommand`)
1. **Time gate**
- If `now < game_env.turntime`, return without executing.
2. **Locking**
- `tryLock()` on `plock` row (`type='GAME'`).
- If locked or `isunited` in `2|3`, return (frozen state).
3. **Pre-turn maintenance**
- Cache game env, `checkDelay()`, `updateOnline()`, `CheckOverhead()`.
4. **Catch-up monthly loop**
- Compute `prevTurn = cutTurn(turntime, turnterm)` and `nextTurn = addTurn(prevTurn)`.
- While `nextTurn <= now`:
- `executeGeneralCommandUntil(nextTurn, limitActionTime, year, month)`
- `updateTraffic()`
- Monthly pipeline:
- `runEventHandler(PreMonth)`
- `preUpdateMonthly()`
- `turnDate(nextTurn)`
- `checkStatistic()` (only if `month == 1`)
- `runEventHandler(Month)`
- `postUpdateMonthly($monthlyRng)`
- Advance `prevTurn`, `nextTurn`, update `game_env.turntime`.
5. **Current partial month**
- `turnDate(prevTurn)`
- `executeGeneralCommandUntil(now, limitActionTime, year, month)`
6. **Post-turn maintenance**
- `processTournament()`
- `processAuction()`
- Reset cache, `unlock()`.
`limitActionTime` is derived from `max_execution_time` (roughly 2/3 of the
configured limit). If time runs out mid-loop, it returns early and keeps
`game_env.turntime` at the most recent executed value.
## Per-General Execution (`executeGeneralCommandUntil`)
For each general with `turntime < targetDate` (ordered by `turntime, no`):
1. **Load state**
- `General::createObjFromDB()` and `KVStorage` for `game_env`.
2. **Nation command (if officer_level >= 5)**
- Pull `nation_turn` row for `turn_idx = 0`.
- Build `NationCommand` with `LastTurn` stored in `nation_env`.
3. **AI/autorun**
- NPCs (`npc >= 2`) or players past `autorun_limit` use `GeneralAI`.
- AI can replace both nation and general commands.
4. **Preprocess triggers**
- RNG seed: `hiddenSeed + 'preprocess' + year + month + generalID`.
- `preprocessCommand()` merges action triggers + `che_부상경감`, `che_병력군량소모`.
5. **Blocked users**
- `processBlocked()` consumes `killturn` and logs if `block >= 2`.
6. **Execute nation command**
- RNG seed: `hiddenSeed + 'nationCommand' + year + month + generalID + commandKey`.
- `processNationCommand()` checks conditions, term stack, `run()`.
- If `run()` fails and `getAlternativeCommand()` exists, it retries.
- Updates `nation_env` with `LastTurn` result.
7. **Execute general command**
- Load from `general_turn` (`turn_idx = 0`), default to `휴식`.
- RNG seed: `hiddenSeed + 'generalCommand' + year + month + generalID + commandKey`.
- `processCommand()` checks conditions, term stack, `run()`.
- Updates `killturn` based on NPC status, autorun, or `휴식`.
8. **Queue maintenance & turntime**
- `pullNationCommand()` / `pullGeneralCommand()` advance queues.
- `updateTurnTime()` handles deletion/retirement and sets next `turntime`.
- Persist via `General::applyDB()`.
9. **LastTurn persistence**
- General `LastTurn` lives in `general.last_turn` JSON.
- Nation `LastTurn` is stored in `nation_env` under `turn_last_{officer_level}`.
- `LastTurn.term` only advances when the same command + arg repeats across turns.
## Command Semantics (BaseCommand)
- **Constraints**: permission/min/full conditions are evaluated via
`Constraint::testAll()` with `general/city/nation/dest*` context.
- **Pre-turn requirement**: `getPreReqTurn()` uses `LastTurn` to accumulate
term stack (`addTermStack()`), logging "수행중" until the term completes.
- **Post-turn requirement**: `getPostReqTurn()` uses `next_execute` storage
(general: `next_execute`, nation: `nation_env`) to block early execution.
- **Run flow**: `run($rng)` returns `true` when completed; `false` can chain to
`getAlternativeCommand()`.
## Turntime & Lifecycle (`updateTurnTime`)
- **Inactivity**: if `killturn <= 0`:
- NPC-owned characters can detach (owner -> NPC) if `deadyear` not reached.
- Otherwise the general is deleted (`kill()`).
- **Retirement**: if `age >= retirementYear` and not NPC, `CheckHall()` and
`rebirth()` are invoked.
- **Scheduling**: next `turntime` is `addTurn(current, turnterm)` with
optional `nextTurnTimeBase` override (aux var).
## killturn, block, autorun
- `killturn` acts as an inactivity counter; it is reset to `game_env.killturn`
when a non-rest player action completes, and decremented for NPCs, autorun,
rest commands, or block states.
- `block >= 2` forces `killturn` decrement and logs a block message.
- When `killturn <= 0`, the general is converted to NPC (owner removed) or
deleted depending on NPC type and `deadyear`.
- `autorun_limit` is stored in general `aux` and used to decide AI takeover.
## Deterministic RNG Seeds
RNG uses `LiteHashDRBG` with `UniqueConst::$hiddenSeed`:
- Preprocess: `('preprocess', year, month, generalID)`
- Nation command: `('nationCommand', year, month, generalID, commandKey)`
- General command: `('generalCommand', year, month, generalID, commandKey)`
- Monthly: `('monthly', year, month)`
These seeds make command outcomes and monthly updates reproducible for
validation.
## Open Questions / Follow-ups
- `preUpdateMonthly()`, `postUpdateMonthly()`, `turnDate()`, `checkStatistic()`
are defined outside this flow; see `legacy/hwe/func_time_event.php` for
economic updates and population calculations.
- `updateTraffic()`, `checkDelay()`, `updateOnline()` logic lives in
`legacy/hwe/func.php` and related utility files.
-103
View File
@@ -1,103 +0,0 @@
# Legacy General Model and Action Stack
This document captures how `General` aggregates modifiers, computes stats, and
exposes trigger hooks. The main reference is `legacy/hwe/sammo/General.php`.
## Construction and Core State
- `General` builds from raw DB rows (general, city, nation, rank, access log).
- `last_turn` is parsed into `LastTurn`, and a separate `resultTurn` is
maintained for updates.
- `penalty` JSON is decoded into `PenaltyKey -> value` map.
- `aux` JSON is lazily decoded and only written when aux values change.
- Rank data is tracked with:
- `rankVarRead` (current read view)
- `rankVarIncrease` (increment queue)
- `rankVarSet` (explicit set queue)
## Action Objects (`iAction`)
`General` composes action sources into a single list:
1. Nation type (`buildNationTypeClass`)
2. Officer level (`TriggerOfficerLevel`)
3. Domestic special (`buildGeneralSpecialDomesticClass`)
4. War special (`buildGeneralSpecialWarClass`)
5. Personality (`buildPersonalityClass`)
6. Crew type (`GameUnitConst::byID`)
7. Inheritance buff (`TriggerInheritBuff`)
8. Scenario effect (`buildScenarioEffectClass`)
9. Items: horse / weapon / book / item (`buildItemClass`)
`getActionList()` returns this merged list and is the core entry point for
modifiers and triggers.
## Stat Computation
`getStatValue()` applies layered modifiers:
- Base stat from raw value
- Injury multiplier
- Cross-stat adjust (strength/intel + 1/4 of the other stat)
- Clamp to `GameConst::$maxLevel`
- `onCalcStat()` from every action object
- Cached by `(statName, injury, action, adjust)` tuple
Convenience wrappers:
- `getLeadership()`, `getStrength()`, `getIntel()`
## Turn Reservation and LastTurn
- `getReservedTurn(turnIdx)` reads from `general_turn`.
- When no entry exists, `buildGeneralCommandClass(null)` creates `휴식`.
- `LastTurn` holds `{ command, arg, term, seq }` and is used by
`BaseCommand::addTermStack()` to advance multi-turn commands.
- `applyDB()` persists updated fields and writes `last_turn` if it changed.
## Aux and Penalty Behavior
- `aux` is loaded on demand (`unpackAux()`), and `setAuxVar()` writes through to
`general.aux` only when values changed.
- Setting an aux value to `null` deletes the key from JSON.
- Command constraints that require aux values trigger `General::unpackAux()`
before evaluation.
- `penalty` is used in both `General` logic (AI and permission checks) and
API flows (messages, nation tools); runtime expiration is not automatic.
## Trigger and Modifier Hooks
`General` delegates calculations and trigger lists through action objects:
- Economy/strategy: `onCalcDomestic`, `onCalcStrategic`,
`onCalcNationalIncome`
- Combat modifiers: `onCalcStat`, `onCalcOpposeStat`, `getWarPowerMultiplier`
- Arbitrary actions: `onArbitraryAction` (used in city conquest)
Turn-prep triggers:
- `getPreTurnExecuteTriggerList()` merges trigger lists from every action
object. `TurnExecutionHelper::preprocessCommand()` adds
`che_부상경감` + `che_병력군량소모` on top.
Battle triggers:
- `getBattleInitSkillTriggerList()` merges per-action battle-init triggers.
- `getBattlePhaseSkillTriggerList()` includes base phase triggers:
- `che_필살시도`, `che_필살발동`
- `che_회피시도`, `che_회피발동`
- `che_계략시도`, `che_계략발동`, `che_계략실패`
- plus any action-specific additions.
## Rank and Access Log Updates
- `increaseRankVar()` / `setRankVar()` queue updates until `applyDB()`.
- `applyDB()` updates `general` and `rank_data`, then flushes logs.
- `checkStatChange()` uses `*_exp` thresholds to increase/decrease stats and
logs level changes.
## Open Questions / Follow-ups
- `PenaltyKey` effects and how penalties are applied are outside this file.
- `GeneralBase` and `LazyVarAndAuxUpdater` may contain additional state
conventions for new code to mirror.
-78
View File
@@ -1,78 +0,0 @@
# Legacy Items, Traits, and Action Modules
This document summarizes the legacy action modules that supply modifiers and
triggers: items, specials, personalities, nation types, crew types, and
scenario effects. Primary references include `legacy/hwe/sammo/iAction.php`,
`legacy/hwe/sammo/BaseItem.php`, `legacy/hwe/sammo/BaseSpecial.php`,
and `legacy/hwe/sammo/Action*/` directories.
## Core Concept: `iAction`
All traits/items/effects implement `iAction` and are merged by
`General::getActionList()` (see `docs/architecture/legacy-engine-general.md`).
They can:
- Modify stats and command calculations (`onCalcStat`, `onCalcDomestic`, etc.).
- Provide triggers (`getPreTurnExecuteTriggerList`, battle init/phase triggers).
- Apply ad-hoc effects (`onArbitraryAction`).
`DefaultAction` provides no-op implementations for all hooks.
## Item System
- Base class: `BaseItem` implements `iAction` and exposes:
- `getCost()`, `isConsumable()`, `isBuyable()`, `getReqSecu()`.
- Stat items inherit `BaseStatItem` and apply a fixed stat bonus in
`onCalcStat()` based on class name tokens.
- Item instances live on the general (`General::getItem()`); items can also
insert battle triggers (see `ActionItem/`).
Items are configured via `GameConst::$allItems` and scenario overrides.
## Specials (Domestic / War)
- Base class: `BaseSpecial` (implements `iAction`).
- `ActionSpecialDomestic/*` modifies domestic outcomes
(`onCalcDomestic`), e.g., cost/success/score adjustments.
- `ActionSpecialWar/*` injects battle triggers or stat modifiers.
- Weighting and eligibility are managed by `SpecialityHelper`:
- Uses `selectWeight` + stat/arm constraints to choose specials.
- Deterministic RNG affects special selection for NPCs or resets.
## Personalities
- Stored in `ActionPersonality/*` and also implement `iAction`.
- Typically adjust core stats or apply small domestic bonuses.
## Nation Types
- `ActionNationType/*` extends `BaseNation`.
- Provide global modifiers to income and domestic outcomes.
- Injected per-nation via `buildNationTypeClass()`.
## Crew Types (Units as Actions)
- `ActionCrewType/*` defines per-crew `iAction` additions.
- Crew types can alter battle order, apply triggers, or modify war stats.
- For example, `che_성벽선제` forces city battle order to a high value.
## Scenario Effects
- `ActionScenarioEffect/*` provides scenario-wide modifiers
(e.g., attacker advantage, trigger overrides).
- Bound to `GameConst::$scenarioEffect` during scenario build.
## Interaction with Triggers
Action modules often construct trigger callers:
- Items and war specials can return `WarUnitTriggerCaller` with `BaseWarUnitTrigger`.
- Some item triggers use `raiseType` to allow coexistence with non-item versions.
See `docs/architecture/legacy-engine-triggers.md` for trigger execution rules.
## Open Questions / Follow-ups
- The precise selection rules for personalities and specials depend on
`SpecialityHelper` RNG thresholds and scenario overrides; capture those
when documenting specific rule packs.
@@ -1,36 +0,0 @@
# Legacy Logging and Versioning
This document summarizes logging utilities and version helpers. References
include `legacy/hwe/sammo/ActionLogger.php`, `legacy/hwe/sammo/UserLogger.php`,
and `legacy/hwe/sammo/VersionGitDynamic.php`.
## ActionLogger
`ActionLogger` aggregates logs for a general, nation, and global context:
- Buffers multiple log channels:
- General history/action/battle logs
- National history logs
- Global history/action logs
- Supports formatted prefixes (`PLAIN`, `YEAR_MONTH`, `NOTICE`, etc.).
- `flush()` emits buffered logs via helper functions
(`pushGeneralHistoryLog`, `pushGlobalHistoryLog`, etc.).
It is used throughout command execution, battle resolution, and event actions.
## UserLogger
`UserLogger` writes user-scoped logs to `user_record`:
- Used for inheritance point spending/awards and other user-level events.
- `push($text, $type)` buffers entries; `flush()` inserts into DB with
`{user_id, server_id, year, month, date, text}`.
## Version Helpers
`VersionGitDynamic` exposes runtime Git metadata:
- `getVersion()` calls `git describe --long --tags` and `git branch`.
- `getHash()` calls `git rev-parse HEAD`.
These are used for diagnostic display and do not affect gameplay logic.
-43
View File
@@ -1,43 +0,0 @@
# Legacy General Pools
This document summarizes the general pool system used for character selection
and event-driven recruitment. References include `legacy/hwe/sammo/AbsGeneralPool.php`,
`legacy/hwe/sammo/AbsFromUserPool.php`, and `legacy/hwe/sammo/GeneralPool/*`.
## Core Abstractions
- `AbsGeneralPool`
- Wraps a `Scenario\GeneralBuilder` and a raw `info` payload.
- Provides `getGeneralBuilder()`, `getInfo()`, and `occupyGeneralName()`.
- Abstract method: `pickGeneralFromPool(...)`.
- `AbsFromUserPool`
- Implements reservation logic backed by the `select_pool` table.
- Reserves candidates for a user (`owner`) until `reserved_until`.
- On selection, clears reservation and binds `general_id`.
## Pool Implementations
- `GeneralPool/RandomNameGeneral`
- Generates random names using `GameConst::$randGen*` lists.
- Checks duplicate counts and appends suffixes if needed.
- Inserts reservation rows into `select_pool` for user selection.
- `GeneralPool/SPoolUnderU30`
- Loads a curated pool from `GeneralPool/Pool/UnderS30.json`.
- Inserts rows into `select_pool` with pre-defined stats and specials.
## Data Model
`select_pool` table fields (observed usage):
- `unique_name` (pool key)
- `info` (JSON: stats, portrait, specials, dex, etc.)
- `owner` (reservation user id)
- `reserved_until` (reservation expiry)
- `general_id` (assigned after creation)
## RNG Notes
Pool selection uses injected `RandUtil` to choose candidates and generate
random names; deterministic seeding depends on caller context.
@@ -1,25 +0,0 @@
# Legacy Server Environment
This document captures the legacy server-level environment settings and
utilities. References include `legacy/hwe/sammo/ServerDefaultEnv.php`,
`legacy/hwe/sammo/ServerEnv.php`, and `legacy/hwe/sammo/ServerTool.php`.
## Server Environment
- `ServerDefaultEnv` holds server-wide constants (e.g.,
`maxGeneralsPerMinute`).
- `ServerEnv` extends `ServerDefaultEnv` and is overridden by
`d_setting/ServerEnv.php` when present.
## Turn Term Management
`ServerTool::changeServerTerm($turnterm)` adjusts the global turn length:
1. Validates that `turnterm` divides 120 minutes.
2. Acquires the game lock (`tryLock()`), unless `ignoreLock` is set.
3. Recomputes each general's `turntime` based on the new interval.
4. Updates `game_env.turnterm` and `game_env.starttime`.
5. Broadcasts a global history notice about the change.
This is a direct mutation of runtime schedule and should be treated as an
administrative operation.
-226
View File
@@ -1,226 +0,0 @@
# Legacy Trigger System (iAction + Trigger Callers)
This document explains how the legacy Trigger system is built on `iAction`
and `TriggerCaller`, and how it affects both command outcomes and battle flow.
Primary references include `legacy/hwe/sammo/iAction.php`,
`legacy/hwe/sammo/TriggerCaller.php`, `legacy/hwe/sammo/General.php`,
`legacy/hwe/process_war.php`, and `legacy/hwe/sammo/WarUnit.php`.
## Core Concepts
### `iAction`: modifier hooks + trigger provider
`iAction` is the unified interface for traits, specials, crew types, items,
scenario effects, and hidden buffs. It provides:
- Modifier hooks (`onCalcDomestic`, `onCalcStat`, `onCalcOpposeStat`,
`onCalcStrategic`, `onCalcNationalIncome`, `getWarPowerMultiplier`).
- Trigger hooks (`getPreTurnExecuteTriggerList`,
`getBattleInitSkillTriggerList`, `getBattlePhaseSkillTriggerList`).
- Ad-hoc hooks (`onArbitraryAction`) for non-turn side effects.
Action sources are merged into a single list in `General::getActionList()`:
1. Nation type
2. Officer level
3. Domestic special
4. War special
5. Personality
6. Crew type (`GameUnitDetail`)
7. Inheritance buff (`TriggerInheritBuff`)
8. Scenario effect
9. Items
The order above is the execution order for modifier hooks, and also the merge
order for trigger lists (later actions override duplicates with the same
unique trigger ID).
Crew types (`GameUnitDetail`) are special:
- They implement `iAction`.
- They expose `initSkillTrigger` and `phaseSkillTrigger` arrays defined in
scenario data (`buildWarUnitTriggerClass`).
- They can include an `iActionList` (crew-type-specific actions) which
themselves implement `iAction`.
### `ObjectTrigger` and `TriggerCaller`
Triggers are small, prioritized actions:
- `ObjectTrigger` defines priorities and a single `action()` method.
- `TriggerCaller` groups triggers by priority, merges lists, and `fire()`s
them in priority order.
Priority constants (lower runs earlier):
- `PRIORITY_BEGIN` = 10000
- `PRIORITY_PRE` = 20000
- `PRIORITY_BODY` = 30000
- `PRIORITY_POST` = 40000
- `PRIORITY_FINAL` = 50000
`TriggerCaller` uses a unique ID (`priority + class + object id`) for dedup.
`BaseWarUnitTrigger` extends this with `raiseType` to separate item-based
triggers from trait-based ones.
Specialized callers enforce type safety:
- `GeneralTriggerCaller` accepts `BaseGeneralTrigger`.
- `WarUnitTriggerCaller` accepts `BaseWarUnitTrigger`.
## Modifier Hooks (Command + Stat + War)
### `onCalcDomestic`
Used by general commands to adjust cost, success, failure, and score. Inputs:
- `turnType`: command key (`징병`, `조달`, `주민선정`, `정착장려`, 등).
- `varType`: `cost`, `rice`, `train`, `atmos`, `success`, `fail`, `score`.
- `aux`: extra context (ex: `armType` in `che_징병`).
Examples:
- `Command/General/che_징병.php` uses `cost`, `rice`, `train`, `atmos`.
- `Command/General/che_주민선정.php` uses `score`, `success`, `fail`.
- `GeneralTrigger/che_병력군량소모.php` uses `징집인구:score`.
### `onCalcStrategic`
Used by nation commands to adjust delays/limits:
- `varType` often `delay`, `globalDelay`, `strategic_cmd_limit`.
- Examples: `Command/Nation/che_급습.php`, `che_백성동원.php`, `che_수몰.php`.
### `onCalcNationalIncome`
Used by `Event/Action/ProcessSemiAnnual.php` to adjust population growth and
income ratios at the nation level. Nation types (e.g., 유가/법가/병가 계열)이
여기에서 보정을 걸어준다.
### `onCalcStat` / `onCalcOpposeStat`
Used for base stats and battle-derived parameters. Common `statName` keys:
- Base stats: `leadership`, `strength`, `intel` (General stat calc).
- Progression: `addDex`, `experience`, `dedication`.
- Battle timing: `initWarPhase` (phase count from `WarUnitGeneral`).
- Battle accuracy: `dex{armType}` (e.g., `dex2`).
- Train/atmos: `bonusTrain`, `bonusAtmos`.
- Combat odds: `warCriticalRatio`, `warAvoidRatio`.
- War magic: `warMagicTrialProb`, `warMagicSuccessProb`,
`warMagicSuccessDamage`, `warMagicFailDamage`.
- Damage range: `criticalDamageRange` (in `WarUnit::criticalDamage()`).
- Supply cost: `killRice` (war rice consumption).
- Battle order: `cityBattleOrder` (opponent modifies city order).
`aux` carries context such as `isAttacker`, `opposeType`, or magic name
(`반목`, `화계`, etc.).
## Pre-Turn General Triggers
`TurnExecutionHelper::preprocessCommand()` runs pre-turn triggers:
1. `General::getPreTurnExecuteTriggerList()` merges triggers from all actions.
2. Base triggers are appended:
- `GeneralTrigger/che_부상경감` (priority 10000 / BEGIN)
- `GeneralTrigger/che_병력군량소모` (priority 50000 / FINAL)
3. `TriggerCaller::fire()` executes them before the command runs.
General triggers use `General::activateSkill()` for logging and gating.
`TurnExecutionHelper::processCommand()` clears activated skills after the
command completes.
## Battle Triggers
Battle triggers are fired in two stages inside `process_war.php`:
### Battle-init triggers
- Fired once per engagement when a defender is first set (`phase == 0`).
- Constructed via `General::getBattleInitSkillTriggerList()`.
- Used for start-of-battle setup (ex: `che_부상무효` from `견고`).
### Phase triggers
Every phase:
1. `WarUnit::beginPhase()` clears activated skills and recomputes war power.
2. `General::getBattlePhaseSkillTriggerList()` builds the trigger list.
- Base triggers (always included):
- `che_필살시도`, `che_필살발동`
- `che_회피시도`, `che_회피발동`
- `che_계략시도`, `che_계략발동`, `che_계략실패`
3. Attacker/defender trigger lists are merged and fired.
4. Damage is calculated using `getWarPower()` (after trigger multipliers).
### Attempt → Execute pattern (PRE/POST)
Most battle skills split into two triggers:
- **Attempt (PRE)**: check conditions, set flags or env payload.
- **Execute (POST)**: read env/flags and apply damage or status.
Examples:
- `WarUnitTrigger/che_저격시도.php` (PRE) sets `저격발동자`, wound ranges,
then `che_저격발동.php` (POST) applies wounds and logs.
- `WarUnitTrigger/che_계략시도.php` (PRE) sets `magic` and success/failure,
then `che_계략발동.php` / `che_계략실패.php` (POST) applies multipliers.
- `WarUnitTrigger/che_필살시도.php``che_필살발동.php` adjusts war power.
### Battle env and stop flags
`BaseWarUnitTrigger::action()` supplies a mutable env:
- `e_attacker` / `e_defender`: per-side state map.
- `stopNextAction`: if true, later triggers are skipped.
Triggers can return `false` from `actionWar()` to set `stopNextAction`.
### Item-based triggers and consumption
`BaseWarUnitTrigger` uses a `raiseType` bitmask to tag item-based triggers:
- `TYPE_ITEM`: item-triggered, with `아이템사용` skill gating.
- `TYPE_CONSUMABLE_ITEM`: consumes and deletes the item.
- `TYPE_DEDUP_TYPE_BASE`: offset for dedup grouping.
`processConsumableItem()` handles the consumption flow and logging. Item
triggers typically pass `raiseType` so the same skill can coexist with
non-item versions (see `ActionItem/che_저격_매화수전.php`).
## Interaction Notes for Porting
- Preserve `General::getActionList()` order; modifier hooks stack in sequence.
- Apply `onCalcOpposeStat` using the opponent's action list after the
general's own `onCalcStat` adjustments.
- Keep priority-based execution (`PRIORITY_*`) and the PRE/POST split.
- Replicate trigger dedup semantics (`getUniqueID` + `raiseType`).
- Maintain `beginPhase()` clearing of activated skills; battle triggers assume
per-phase activation.
- Ensure RNG usage stays deterministic (`RandUtil` everywhere in triggers).
## Current core2026 boundary
core2026 does not expose the whole legacy `iAction` surface as one interface.
Modifier folds and semantic side effects live under `src/actionModules/`;
priority callers remain under `src/triggers/` and the war trigger modules.
This separation is required because `TriggerCaller` sorts by priority and
deduplicates unique IDs, while semantic events synchronously preserve module
order and producer-owned mutation/RNG boundaries.
The former copied `onArbitraryAction(actionName, phase, aux)` surface was
removed. Equipment purchase/sale, strategy success and city conquest now use
the closed, nominally branded protocol documented in
[장수 행동 모듈 프로토콜](./action-module-protocol.md). This is a core2026
typing boundary only; the ref call order and persisted effects remain the
compatibility contract.
## Related Files
- `legacy/hwe/sammo/iAction.php`
- `legacy/hwe/sammo/TriggerCaller.php`
- `legacy/hwe/sammo/ObjectTrigger.php`
- `legacy/hwe/sammo/General.php`
- `legacy/hwe/sammo/WarUnit.php`
- `legacy/hwe/process_war.php`
-199
View File
@@ -1,199 +0,0 @@
# Legacy Battle and War Resolution
This document summarizes the legacy battle pipeline centered on
`legacy/hwe/process_war.php`, `legacy/hwe/sammo/WarUnit.php`,
`legacy/hwe/sammo/WarUnitGeneral.php`, and `legacy/hwe/sammo/WarUnitCity.php`.
## Entry Point: `processWar`
Inputs:
- `warSeed` (string): battle RNG seed
- `attackerGeneral`: `General`
- `rawAttackerNation`: nation row (snapshot)
- `rawDefenderCity`: city row (snapshot)
Setup steps:
1. Initialize RNG with `warSeed` (`LiteHashDRBG`).
2. Load defender nation row (or default neutral nation if `nation = 0`).
3. Build `WarUnitGeneral` (attacker) and `WarUnitCity` (defender city).
4. Collect defender generals in the city, filter with `extractBattleOrder()`.
5. Optionally append city as defender if city order > 0.
6. Sort defenders by battle order, iterate via `getNextDefender()`.
7. Run `processWar_NG()`; update DB and nation/city stats.
## Battle Order (`extractBattleOrder`)
For defender generals:
- Must have crew > 0
- Must have rice > crew/100
- `train` and `atmos` must meet `defence_train`
- Battle order uses:
- `totalStat = (realStat + fullStat) / 2`
- `totalCrew = crew / 1_000_000 * (train * atmos) ^ 1.5`
- `totalStat + totalCrew / 100`
For cities:
- Uses attacker `onCalcOpposeStat('cityBattleOrder', -1)`.
## Battle Loop (`processWar_NG`)
1. Log start, include seed in battle logs.
2. If no defenders remain, set defender = city and switch to siege.
3. **Initial engagement**
- `setOppose()` for attacker/defender
- `addTrain(1)` for both
- Fire battle-init triggers
4. **Per phase**
- `beginPhase()` computes war power
- Fire battle-phase triggers
- `calcDamage()` on both sides
- Clamp damage if it exceeds HP ratios
- Apply damage, increase killed/dead counters
- Log phase results
5. **Continuation checks**
- `continueWar()` fails on no rice or HP <= 0
- On retreat/defeat: log, apply win/lose, try wound
- If defender removed, move to next defender (or city siege)
6. **Finish**
- `logBattleResult()` for last phase if needed
- `finishBattle()` for attacker/defender
- City conflict tracking and history logs
## Post-Battle Updates (`processWar`)
After `processWar_NG()`:
- Apply attacker DB updates
- Update nation rice (supply and siege rules)
- Distribute city `dead` counts (40% attacker city, 60% defender city)
- Increase nation tech based on killed/dead and nation size adjustments
- Update `diplomacy.dead` for both sides
- If city conquered: call `ConquerCity()`
## Conflict Tracking (`city.conflict`)
City conflict tracks which nations contributed to siege damage, used to
resolve post-war ownership when multiple attackers participate.
Source: `WarUnitCity::addConflict()` in `legacy/hwe/sammo/WarUnitCity.php`.
- `city.conflict` is a JSON map `{ nationID: deadContribution }`.
- Contribution amount is based on city `dead` (minimum 1).
- First/last hit bonus: if no conflict exists yet or city HP is 0, `dead` is
multiplied by 1.05 ("선타, 막타 보너스").
- Contributions are sorted descending via `arsort()` after updates.
- `addConflict()` returns `true` when a new nation enters the conflict, which
triggers the global "분쟁" log in `processWar()`.
- `getConquerNation()` returns the first key of the sorted map to select the
final owner.
- `DeleteConflict($nation)` removes a nation from all city conflicts, used on
nation deletion and on the `che_방랑` flow.
## City Conquest Resolution (`ConquerCity`)
`ConquerCity()` finalizes city ownership, handles nation collapse, and applies
post-siege side effects. It is deterministic with the conquest RNG seed noted
below.
### Common Flow
- Logs conquest to attacker (general/nation/global) and defender nation history.
- Runs `EventTarget::OCCUPY_CITY` handlers via `TurnExecutionHelper::runEventHandler()`.
- Calls `onArbitraryAction(..., 'ConquerCity')` for each defender general in
the city, then persists them.
### Nation Collapse Path
Triggered when the defender nation owns exactly one city (the captured city):
- Calls `deleteNation()` using the defender lord (officer level 12).
- All defender generals lose 2050% of gold and rice, -10% experience, and
-50% dedication, with action logs. Loss amounts are aggregated.
- Optionally issues scout messages to fleeing generals (when `join_mode` allows).
- NPC defenders (NPC type 28 except 5) can auto-queue `che_임관` to the
attacker nation with a random delay (012 turns), gated by
`GameConst::$joinRuinedNPCProp`.
- Attacker reward:
- Half of defender nation gold/rice above base (`GameConst::$basegold`,
`GameConst::$baserice`) plus half of the aggregated general losses.
- Credited to attacker nation and logged to all chiefs (officer level >= 5).
- Runs `EventTarget::DESTROY_NATION` handlers.
### Nation Survives Path
If the defender nation still has other cities:
- Demotes city officers (태수/군사/종사) to general:
- `officer_level = 1`, `officer_city = 0`.
- If the city was the capital:
- Picks a new capital via `findNextCapital()` (closest distance, highest pop).
- Logs an emergency relocation message to global and all nation generals.
- Sets `nation.capital` to new city and halves nation gold/rice.
- Marks new capital as supply city; moves chiefs to it.
- Applies 20% morale loss to all generals (`atmos *= 0.8`).
- Refreshes cached nation static info.
### Final Ownership + City Reset
`getConquerNation()` inspects `city.conflict` to decide final owner. If the
attacker loses arbitration, the city is transferred to the conflict winner
and logs are emitted for both nations.
City stats are reset after ownership is settled:
- `supply = 1`, `term = 0`, `conflict = {}`, `nation = conquerNation`,
`officer_set = 0`.
- `agri/comm/secu` multiplied by 0.7.
- `def/wall` reset:
- If `level > 3`: both set to `GameConst::$defaultCityWall`.
- Else: set to `def_max/2`, `wall_max/2`.
- Frontline status recalculated for all nearby nations (`SetNationFront()`).
### Deterministic RNG
Conquest RNG seed:
`hiddenSeed + 'ConquerCity' + year + month + attackerNationID + attackerID + cityID`.
Used for:
- Defender general loss ratios (2050%).
- Scout message chance and NPC auto-join chance.
- Randomized join turn delay (012).
## `WarUnitGeneral` Highlights
- Train/atmos bonuses depend on city level and attacker/defender role.
- War power:
- Base attack/defence from crew type + tech
- Adjusted by train/atmos, dex (`getDex()`), crew type coefficients
- Experience level scales war power and counter-scales opponent
- `General::getWarPowerMultiplier()` applies special multipliers
- Rice consumption on kills: proportional to damage, tech cost, unit rice
- Wound chance: 5% unless `부상무효` / `퇴각부상무효` triggered
- `finishBattle()` updates rank stats, rounds values, and checks stat changes
## `WarUnitCity` Highlights
- Uses `DummyGeneral` with `CREWTYPE_CASTLE`
- HP = `def * 10`
- Computed attack/defence = `(def + wall * 9) / 500 + 200`
- City train/atmos scales with elapsed years since `startYear`
- Siege state:
- Non-siege battle ends after one exchange
- Siege continues until HP <= 0
- `heavyDecreaseWealth()` halves `agri/comm/secu` on supply-based rout
- `addConflict()` records contribution in `city.conflict` JSON
## Deterministic RNG
- Main battle uses `warSeed` directly.
- City conquest uses `hiddenSeed + 'ConquerCity' + year + month + nationID + generalID + cityID`.
## Open Questions / Follow-ups
- No automatic decay for `city.conflict` is visible; confirm if any scheduled
cleanup exists outside explicit reset paths.
-59
View File
@@ -1,59 +0,0 @@
# Legacy Engine Map
The legacy engine lives in `legacy/hwe/sammo/` and follows domain-first
organization rather than endpoint-first routing.
## Core Domains
- `Command/`: turn actions (general/nation commands) and resolution rules
- `API/`: engine operations for UI and automation
- `Event/`, `StaticEvent/`: dynamic and scheduled event processing
- `General*`, `Nation*`, `WarUnit*`, `City*`: entities and combat/city state
- `Action*`, `Special*`: traits, personalities, special actions, scenario effects
- `Trigger*`, `*Trigger`: conditional logic and state transitions
- `Scenario/`, `Scenario.php`: scenario loading and rulesets
- `DTO/`, `VO/`, `Enums/`, `Constraint/`: shared types and validation
## Endpoint Patterns
- JSON APIs: `legacy/hwe/j_*.php`
- Vue multi-entry pages: `legacy/hwe/v_*.php`
- PHP + jQuery pages: `legacy/hwe/b_*.php` and handlers `legacy/hwe/c_*.php`
- Modern router: `legacy/hwe/api.php` dispatches into `legacy/hwe/API/`
## Frontend and Assets
- Vue/TypeScript UI: `legacy/hwe/ts/`
- Shared components: `legacy/hwe/ts/components/`
- Styles: `legacy/css/` and `legacy/hwe/scss/`
- Templates: `legacy/hwe/templates/`
## Trigger Composition for Generals (Outline)
- Trigger evaluation order and priority rules
- "attempt" then "execute" phases
- Common trigger categories (traits, specials, scenario effects)
- How triggers combine when multiple sources apply
## Detailed Notes
- Entity and schema overview: `docs/architecture/legacy-entities.md`
- Turn execution pipeline: `docs/architecture/legacy-engine-execution.md`
- General model and action stack: `docs/architecture/legacy-engine-general.md`
- General AI behavior and policy model: `docs/architecture/legacy-engine-ai.md`
- Trigger system (iAction + trigger callers): `docs/architecture/legacy-engine-triggers.md`
- Battle and war resolution: `docs/architecture/legacy-engine-war.md`
- Command catalog: `docs/architecture/legacy-commands.md`
- Scenario system and rule sets: `docs/architecture/legacy-scenarios.md`
- Inheritance points (유산 포인트): `docs/architecture/legacy-inherit-points.md`
- Event system (dynamic/static): `docs/architecture/legacy-engine-events.md`
- Economy and monthly updates: `docs/architecture/legacy-engine-economy.md`
- Auctions and betting: `docs/architecture/legacy-engine-auction.md`
- Items, specials, and traits: `docs/architecture/legacy-engine-items.md`
- City data and helpers: `docs/architecture/legacy-engine-city.md`
- Diplomacy and messaging: `docs/architecture/legacy-engine-diplomacy.md`
- Constraint system: `docs/architecture/legacy-engine-constraints.md`
- Game constants and unit sets: `docs/architecture/legacy-engine-constants.md`
- Server environment tools: `docs/architecture/legacy-engine-server-env.md`
- Logging and versioning: `docs/architecture/legacy-engine-logging.md`
- General pools: `docs/architecture/legacy-engine-pools.md`
-432
View File
@@ -1,432 +0,0 @@
# Legacy Entities Overview
This document summarizes the primary entities in the legacy engine, how they
interact, and where their state lives. The active runtime lives in
`legacy/hwe/sammo/` and uses the tables in `legacy/hwe/sql/schema.sql`.
## Data Sources
- Table name map: `legacy/hwe/sammo/Enums/TableName.php`
- Schema definitions: `legacy/hwe/sql/schema.sql`
## Coverage Notes
- The legacy runtime mixes active, archived, and transitional tables.
- Table relationships are mostly enforced in code; few explicit FK constraints.
- Some tables are retained for history or migration and may not be touched in
the current runtime path.
## Core Entities
### General (장수)
- Class: `legacy/hwe/sammo/General.php` (base: `legacy/hwe/sammo/GeneralBase.php`)
- Tables: `general`, `general_turn`, `rank_data`, `general_access_log`
- Interaction highlights:
- Command execution: `legacy/hwe/sammo/Command/*` and
`legacy/hwe/sammo/TurnExecutionHelper.php`
- Turn reservation: `legacy/hwe/sammo/LastTurn.php`
- Combat adapter: `legacy/hwe/sammo/WarUnitGeneral.php`
- Modifiers/triggers via `iAction` modules (items, specials, traits)
- Logging: `legacy/hwe/sammo/ActionLogger.php`
- DB fields (schema):
- `general`: no, owner, npcmsg, npc, npc_org, affinity, bornyear, deadyear,
newmsg, picture, imgsvr, name, owner_name, nation, city, troop, leadership,
leadership_exp, strength, strength_exp, intel, intel_exp, injury,
experience, dedication, dex1, dex2, dex3, dex4, dex5, officer_level,
officer_city, permission, gold, rice, crew, crewtype, train, atmos, weapon,
book, horse, item, turntime, recent_war, makelimit, killturn, block,
dedlevel, explevel, age, startage, belong, betray, personal, special,
specage, special2, specage2, defence_train, tnmt, myset, tournament,
newvote, last_turn, aux, penalty
- `general_turn`: id, general_id, turn_idx, action, arg, brief
- `general_access_log`: id, general_id, user_id, last_refresh, refresh,
refresh_total, refresh_score, refresh_score_total
- `rank_data`: id, nation_id, general_id, type, value
- JSON columns: `general.last_turn`, `general.aux`, `general.penalty`
### Nation (국가)
- Data rows: `nation`, `nation_env`, `nation_turn`
- Nation-type actions: `legacy/hwe/sammo/ActionNationType/*`,
base in `legacy/hwe/sammo/BaseNation.php`
- Interaction highlights:
- Nation commands: `legacy/hwe/sammo/Command/Nation/*`
- Diplomacy: `diplomacy`, `ng_diplomacy`, and
`legacy/hwe/sammo/DiplomaticMessage.php`
- Monthly economy updates: `legacy/hwe/sammo/Event/Action/*`
- DB fields (schema):
- `nation`: nation, name, color, capital, capset, gennum, gold, rice, bill,
rate, rate_tmp, secretlimit, chief_set, scout, war, strategic_cmd_limit,
surlimit, tech, power, spy, level, type, aux
- `nation_turn`: id, nation_id, officer_level, turn_idx, action, arg, brief
- `nation_env`: id, namespace, key, value
- JSON columns: `nation.spy`, `nation.aux`, `nation_turn.arg`,
`nation_env.value`
### City (도시)
- Data rows: `city`
- Static map data: `legacy/hwe/sammo/CityConstBase.php`,
`legacy/hwe/sammo/CityInitialDetail.php`
- Live cache: `legacy/hwe/sammo/CityHelper.php`
- Interaction highlights:
- Supply and isolation updates via `Event/Action/UpdateCitySupply`
- Conquest/war defender: `legacy/hwe/sammo/WarUnitCity.php`
- DB fields (schema):
- `city`: city, name, level, nation, supply, front, pop, pop_max, agri,
agri_max, comm, comm_max, secu, secu_max, trust, trade, dead, def, def_max,
wall, wall_max, officer_set, state, region, term, conflict
- JSON columns: `city.conflict`
### Troop (부대)
- Data rows: `troop`
- Interaction highlights:
- Join/leave flows in `legacy/hwe/sammo/API/Troop/*`
- Static event hooks from `legacy/hwe/sammo/StaticEvent/*`
- DB fields (schema):
- `troop`: troop_leader, nation, name
### Command / Turn (턴 예약과 실행)
- Turn snapshots: `legacy/hwe/sammo/LastTurn.php`
- General commands: `legacy/hwe/sammo/Command/General/*`
- Nation commands: `legacy/hwe/sammo/Command/Nation/*`
- Execution entry: `legacy/hwe/sammo/TurnExecutionHelper.php` and
`legacy/hwe/proc.php`
- DB fields (schema):
- `general_turn`: id, general_id, turn_idx, action, arg, brief
- `nation_turn`: id, nation_id, officer_level, turn_idx, action, arg, brief
### War Units (전투 단위)
- Base: `legacy/hwe/sammo/WarUnit.php`
- General combat: `legacy/hwe/sammo/WarUnitGeneral.php`
- City combat: `legacy/hwe/sammo/WarUnitCity.php`
- Interaction highlights:
- Battle flow and resolution: `legacy/hwe/process_war.php`
- Uses General/Nation/City snapshots for deterministic resolution
## Rule Modules and Modifiers
### iAction Modules
- Interface: `legacy/hwe/sammo/iAction.php`
- Modules:
- Items: `legacy/hwe/sammo/ActionItem/*`
- Domestic specials: `legacy/hwe/sammo/ActionSpecialDomestic/*`
- War specials: `legacy/hwe/sammo/ActionSpecialWar/*`
- Personalities: `legacy/hwe/sammo/ActionPersonality/*`
- Nation types: `legacy/hwe/sammo/ActionNationType/*`
- Crew types: `legacy/hwe/sammo/ActionCrewType/*`
- Scenario effects: `legacy/hwe/sammo/ActionScenarioEffect/*`
- Applied by `General::getActionList()` and used by triggers, stat calculation,
and combat phases.
### Trigger System
- Trigger callers: `legacy/hwe/sammo/TriggerCaller.php`,
`legacy/hwe/sammo/GeneralTriggerCaller.php`,
`legacy/hwe/sammo/WarUnitTriggerCaller.php`
- Trigger definitions: `legacy/hwe/sammo/GeneralTrigger/*`,
`legacy/hwe/sammo/WarUnitTrigger/*`
- Used in pre-turn processing and battle phases.
### Constraints
- Validation rules: `legacy/hwe/sammo/Constraint/*`
- Command prechecks and selection logic use these constraints.
## System Entities
### Events
- Dynamic events: `event` table, executed by `legacy/hwe/sammo/Event/*`
- Static hooks: `legacy/hwe/sammo/StaticEvent/*`,
wired via `GameConst::$staticEventHandlers`
- DB fields (schema):
- `event`: id, target, priority, condition, action
- JSON columns: `event.condition`, `event.action`
### Messaging and Diplomacy
- Messages: `legacy/hwe/sammo/Message.php`
- Targets: `legacy/hwe/sammo/MessageTarget.php`,
`legacy/hwe/sammo/Target.php`
- Diplomacy wrapper: `legacy/hwe/sammo/DiplomaticMessage.php`
- Table: `message`
- DB fields (schema):
- `message`: id, mailbox, type, src, dest, time, valid_until, message
- `diplomacy`: no, me, you, state, term, dead, showing
- `ng_diplomacy`: no, src_nation_id, dest_nation_id, prev_no, state,
text_brief, text_detail, date, src_signer, dest_signer, aux
- JSON columns: `message.message`, `ng_diplomacy.aux`
### Economy and World State
- Monthly updates: `legacy/hwe/sammo/Event/Action/ProcessIncome`,
`ProcessSemiAnnual`, `ProcessWarIncome`
- Trade and supply: `RandomizeCityTradeRate`, `UpdateCitySupply`,
`UpdateNationLevel`
- DB fields (schema):
- `nation_env`: id, namespace, key, value
- `statistic`: no, year, month, nation_count, nation_name, nation_hist,
gen_count, personal_hist, special_hist, power_hist, crewtype, etc, aux
- JSON columns: `statistic.aux`, `nation_env.value`
### Auctions, Betting, and Votes
- Auctions: `legacy/hwe/sammo/Auction.php` and `ng_auction*` tables
- Betting: `legacy/hwe/sammo/Betting.php` and `ng_betting`
- Votes and tournaments: `vote`, `vote_comment`, `tournament`
- DB fields (schema):
- `ng_auction`: id, type, finished, target, host_general_id, req_resource,
open_date, close_date, detail
- `ng_auction_bid`: no, auction_id, owner, general_id, amount, date, aux
- `ng_betting`: id, betting_id, general_id, user_id, betting_type, amount
- `vote`: id, vote_id, general_id, nation_id, selection
- `vote_comment`: id, vote_id, general_id, nation_id, general_name,
nation_name, text, date
- `tournament`: seq, no, npc, name, w, b, h, leadership, strength, intel, lvl,
grp, grp_no, win, draw, lose, gl, prmt
- JSON columns: `ng_auction.detail`, `ng_auction_bid.aux`, `ng_betting.betting_type`,
`vote.selection`
### Logs and Records
- Action logs: `legacy/hwe/sammo/ActionLogger.php`
- User logs: `legacy/hwe/sammo/UserLogger.php`
- Tables: `ng_history`, `world_history`, `general_record`, `user_record`, `hall`
- DB fields (schema):
- `ng_history`: no, server_id, year, month, map, global_history, global_action,
nations
- `world_history`: id, nation_id, year, month, text
- `general_record`: id, general_id, log_type, year, month, text
- `user_record`: id, user_id, server_id, log_type, year, month, date, text
- `hall`: id, server_id, season, scenario, general_no, type, value, owner, aux
- JSON columns: `ng_history.map`, `ng_history.global_history`,
`ng_history.global_action`, `ng_history.nations`, `hall.aux`
### Boards, Locks, and Storage
- Boards/comments: `board`, `comment`
- Locks: `plock`
- KV stores: `storage`, `nation_env`
- DB fields (schema):
- `board`: no, nation_no, is_secret, date, general_no, author, author_icon,
title, text
- `comment`: no, nation_no, is_secret, date, document_no, general_no, author,
text
- `plock`: no, type, plock, locktime
- `storage`: id, namespace, key, value
- JSON columns: `storage.value`
### Archive and Lifecycle Tables
- Archive/season data: `ng_games`, `ng_old_nations`, `ng_old_generals`,
`emperior`
- Scheduling: `reserved_open`
- Selection pools: `select_npc_token`, `select_pool`
- Inheritance: `inheritance_result`
- DB fields (schema):
- `ng_games`: id, server_id, date, winner_nation, map, season, scenario,
scenario_name, env
- `ng_old_nations`: id, server_id, nation, data, date
- `ng_old_generals`: id, server_id, general_no, owner, name, last_yearmonth,
turntime, data
- `emperior`: no, server_id, phase, nation_count, nation_name, nation_hist,
gen_count, personal_hist, special_hist, name, type, color, year, month,
power, gennum, citynum, pop, poprate, gold, rice, l12name, l12pic, l11name,
l11pic, l10name, l10pic, l9name, l9pic, l8name, l8pic, l7name, l7pic,
l6name, l6pic, l5name, l5pic, tiger, eagle, gen, history, aux
- `reserved_open`: id, options, date
- `select_npc_token`: id, owner, valid_until, pick_more_from, pick_result,
nonce
- `select_pool`: id, unique_name, owner, general_id, reserved_until, info
- `inheritance_result`: id, server_id, owner, general_id, year, month, value
- JSON columns: `ng_games.env`, `ng_old_nations.data`, `ng_old_generals.data`,
`emperior.history`, `emperior.aux`, `reserved_open.options`,
`select_npc_token.pick_result`, `inheritance_result.value`
## Aux and JSON Payloads
Legacy data uses many JSON fields with dynamic keys. Most are updated in code,
not enforced by DB constraints.
### General `aux`
- Storage layer: `legacy/hwe/sammo/LazyVarAndAuxUpdater.php`
- Access pattern: `getAuxVar()` / `setAuxVar()` on `General`
- Literal keys observed in code (not exhaustive):
- `armType`, `autorun_limit`, `inheritBuff`, `inheritRandomUnique`,
`inheritResetSpecialWar`, `inheritResetTurnTime`,
`inheritSpecificSpecialWar`, `last발령`, `max_domestic_critical`,
`movingTargetCityID`, `nextTurnTimeBase`, `pickYearMonth`,
`use_auto_nation_turn`, `use_treatment`
- Key sources:
- Turn execution timing: `legacy/hwe/sammo/TurnExecutionHelper.php`
- AI state and movement: `legacy/hwe/sammo/GeneralAI.php`
- Inheritance actions: `legacy/hwe/sammo/API/InheritAction/*`
- Domestic critical resets: `legacy/hwe/sammo/Command/General/*`
- Troop dispatch markers: `legacy/hwe/sammo/Command/Nation/che_발령.php`
- Item or trigger-specific keys are stored via constants (e.g.
`ActionItem/event_충차.php`, `WarUnitTrigger/event_충차아이템소모.php`) and are
not visible in literal scans.
### Nation `aux`
- Enum keys: `legacy/hwe/sammo/Enums/NationAuxKey.php`
- `can_국기변경`, `can_국호변경`, `did_특성초토화`, `can_무작위수도이전`,
`can_대검병사용`, `can_극병사용`, `can_화시병사용`, `can_원융노병사용`,
`can_산저병사용`, `can_상병사용`, `can_음귀병사용`, `can_무희사용`,
`can_화륜차사용`
- Used by constraints and nation commands, especially unit unlock logic.
### Other JSON Columns
- `city.conflict`: siege contribution map keyed by nation id.
- `message.message`: serialized message payload including targets/options.
- `event.condition` / `event.action`: JSON DSL for event dispatch.
- `storage.value` / `nation_env.value`: key-value JSON via `KVStorage`
(`legacy/src/sammo/KVStorage.php`), used for `game_env`, `nation_env`,
`betting`, `vote`, `inheritance_*`, and other namespaces.
- Large history blobs: `ng_history.*`, `ng_old_nations.data`,
`ng_old_generals.data`, `emperior.history`, `emperior.aux`.
### KVStorage `game_env` Namespace
`game_env` is stored in `storage` with `namespace = 'game_env'` and is the
primary server-wide runtime state store. Keys are read/written in core
functions and APIs (e.g. `legacy/hwe/func.php`,
`legacy/hwe/func_time_event.php`, `legacy/hwe/sammo/TurnExecutionHelper.php`).
Observed literal keys (from code reads/writes, non-exhaustive):
- Time and turn: `startyear`, `year`, `month`, `init_year`, `init_month`,
`starttime`, `opentime`, `turntime`, `turnterm`
- Scenario and season: `scenario`, `scenario_text`, `season`
- Join and creation gates: `join_mode`, `maxgeneral`, `block_general_create`,
`show_img_level`, `icon_path`, `extended_general`
- Global flags and limits: `isunited`, `fiction`, `autorun_user`, `killturn`,
`refresh`, `maxrefresh`, `refreshLimit`, `maxonline`, `recentTraffic`,
`online_user_cnt`, `online_nation`, `server_cnt`, `msg`
- NPC policy: `npcmode`, `npc_nation_policy`, `npc_general_policy`
- Tournament and voting: `tournament`, `phase`, `tnmt_type`, `tnmt_time`,
`tnmt_auto`, `tnmt_msg`, `tnmt_trig`, `tnmt_pattern`, `develcost`,
`last_tournament_betting_id`, `lastVote`
- Scout restrictions: `block_change_scout`
- Auctions: `obfuscatedNamePool`
Notes:
- Some keys are used as feature toggles (e.g. `block_change_scout`,
`block_general_create`) while others carry time/season state.
- Additional keys may exist via dynamic or computed names outside literal
scans, especially in admin or migration scripts.
## Table Usage Index (Partial)
This index focuses on primary runtime touch points and omits view-only pages.
- `general`: `legacy/hwe/sammo/General.php`, `legacy/hwe/sammo/GeneralAI.php`,
`legacy/hwe/sammo/TurnExecutionHelper.php`
- `general_turn`: `legacy/hwe/sammo/TurnExecutionHelper.php`,
`legacy/hwe/sammo/API/Command/GetReservedCommand.php`
- `general_access_log`: `legacy/hwe/sammo/General.php`,
`legacy/hwe/sammo/API/General/GetFrontInfo.php`
- `rank_data`: `legacy/hwe/sammo/General.php`, `legacy/hwe/sammo/WarUnitGeneral.php`
- `nation`: `legacy/hwe/sammo/Command/Nation/*`,
`legacy/hwe/sammo/Scenario/Nation.php`
- `nation_turn`: `legacy/hwe/sammo/TurnExecutionHelper.php`,
`legacy/hwe/sammo/API/NationCommand/GetReservedCommand.php`
- `nation_env`: `legacy/src/sammo/KVStorage.php`,
`legacy/hwe/sammo/Command/NationCommand.php`
- `city`: `legacy/hwe/sammo/CityHelper.php`,
`legacy/hwe/sammo/WarUnitCity.php`, `legacy/hwe/process_war.php`
- `troop`: `legacy/hwe/sammo/API/Troop/*`,
`legacy/hwe/sammo/StaticEvent/*`
- `diplomacy`, `ng_diplomacy`: `legacy/hwe/sammo/DiplomaticMessage.php`,
`legacy/hwe/sammo/API/Global/GetDiplomacy.php`
- `message`: `legacy/hwe/sammo/Message.php`,
`legacy/hwe/sammo/API/Message/*`
- `event`: `legacy/hwe/sammo/TurnExecutionHelper.php`,
`legacy/hwe/sammo/Event/*`
- `board`, `comment`: `legacy/hwe/v_board.php`,
`legacy/hwe/j_board_get_articles.php`
- `ng_auction`, `ng_auction_bid`: `legacy/hwe/sammo/Auction.php`,
`legacy/hwe/sammo/API/Auction/*`
- `ng_betting`: `legacy/hwe/sammo/Betting.php`,
`legacy/hwe/sammo/API/Betting/*`
- `vote`, `vote_comment`: `legacy/hwe/sammo/API/Vote/*`,
`legacy/hwe/v_vote.php`
- `ng_history`, `world_history`: `legacy/hwe/sammo/ActionLogger.php`,
`legacy/hwe/func_history.php`
- `general_record`: `legacy/hwe/sammo/ActionLogger.php`
- `user_record`: `legacy/hwe/sammo/UserLogger.php`
- `storage`: `legacy/src/sammo/KVStorage.php`,
`legacy/hwe/sammo/ResetHelper.php`
- `ng_games`, `ng_old_nations`, `ng_old_generals`, `emperior`:
`legacy/hwe/sammo/ResetHelper.php`, `legacy/hwe/a_emperior.php`
## Interaction Sketch
- Turn execution (`TurnExecutionHelper`):
- Reads `general_turn` / `nation_turn`
- Builds General/Nation/City context
- Applies constraints → runs command → triggers → logs
- Runs monthly events and world updates
- Battle (`process_war.php`):
- Builds `WarUnitGeneral` and `WarUnitCity`
- Resolves phases and damage with triggers
- Applies city conquest and nation collapse rules
- Diplomacy:
- `Message` delivers a diplomacy request
- `DiplomaticMessage` accepts/rejects
- Nation command executes and updates diplomacy tables
## Entity Relationships (Mermaid ERD)
Note: `message.src` / `message.dest` are polymorphic in practice (general or
nation depending on message type). The diagram below treats them as general
links for simplicity.
```mermaid
erDiagram
GENERAL ||--o{ GENERAL_TURN : reserves
GENERAL ||--o{ GENERAL_ACCESS_LOG : logs
GENERAL ||--o{ GENERAL_RECORD : records
GENERAL ||--o{ RANK_DATA : ranks
GENERAL ||--o{ MESSAGE : sends
GENERAL ||--o{ MESSAGE : receives
GENERAL ||--o{ VOTE : votes
GENERAL ||--o{ VOTE_COMMENT : comments
GENERAL ||--o{ NG_BETTING : bets
GENERAL ||--o{ NG_AUCTION : hosts
GENERAL ||--o{ NG_AUCTION_BID : bids
GENERAL ||--o{ TROOP : leads
NATION ||--o{ GENERAL : has
NATION ||--o{ NATION_TURN : schedules
NATION ||--o{ CITY : owns
NATION ||--o{ DIPLOMACY : relates
NATION ||--o{ NG_DIPLOMACY : documents
NATION ||--o{ BOARD : boards
NATION ||--o{ COMMENT : comments
NATION ||--o{ WORLD_HISTORY : history
CITY ||--o{ GENERAL : hosts
BOARD ||--o{ COMMENT : comments
NG_AUCTION ||--o{ NG_AUCTION_BID : bids
```
## Related Docs
- `docs/architecture/legacy-engine.md`
- `docs/architecture/legacy-engine-general.md`
- `docs/architecture/legacy-engine-war.md`
- `docs/architecture/legacy-engine-economy.md`
- `docs/architecture/legacy-engine-diplomacy.md`
- `docs/architecture/legacy-engine-events.md`
-161
View File
@@ -1,161 +0,0 @@
# Legacy Inheritance Points (유산 포인트)
This document describes the cross-season inheritance point system, how points
are accumulated, merged, and spent, and which gameplay effects they unlock.
References include `legacy/hwe/sammo/InheritancePointManager.php`,
`legacy/hwe/sammo/API/InheritAction/*`, `legacy/hwe/sammo/TriggerInheritBuff.php`,
`legacy/hwe/sammo/API/General/Join.php`, and `legacy/hwe/sql/schema.sql`.
## Overview
- Inheritance points are **user-level, cross-game currency**.
- Points are stored in KV storage under `inheritance_{userID}` and are **not
cleared on season reset** (`legacy/hwe/sql/reset.sql` does not drop
`storage`).
- The current spendable balance is stored in the `previous` key.
## Storage and Persistence
- KV storage: `storage` table with `namespace = inheritance_{userID}`.
- Snapshot table: `inheritance_result` records per-season snapshots
`{server_id, owner, general_id, year, month, value}`.
- Logs: `user_record` with `log_type = inheritPoint`.
`InheritancePointManager::applyInheritanceUser()` merges all stored point
entries into a single total and writes it back to `previous`.
## Point Types and Sources
The key list is defined in `InheritanceKey` and configured in
`InheritancePointManager`. Each key has a source and a coefficient.
Directly stored keys (`storeType = true`):
- `previous` (기존 보유): current spendable balance.
- `lived_month`: +1 per executed turn (`TurnExecutionHelper::executeGeneralCommandUntil`).
- `max_domestic_critical`: updated via `updateMaxDomesticCritical()` in
`legacy/hwe/func_gamerule.php`.
- `active_action`: incremented by many commands (e.g. `che_출병`, `che_거병`,
`che_건국`, various nation commands). Coefficient = 3.
- `unifier`: points from unification events (e.g. `che_건국` +250,
`check_united()` awards +2000 for high officers).<br>
- `tournament`: awarded in tournament routines (`legacy/hwe/func_tournament.php`).
Computed keys (`storeType = false` or derived):
- `combat`: from `rank_data.warnum` (war engagements) \* 5.
- `sabotage`: from `rank_data.firenum` (strategy success) \* 20.
- `dex`: sum of arm-type dex, with overflow reduction, \* 0.001.
- `betting`: `betwin * 10 * (betwingold / betgold)^2`.
- `max_belong`: max of `general.belong` and aux `max_belong`, \* 10.
Notes:
- NPCs (`npc >= 2`) do not earn points.
- If `game_env.isunited != 0`, only `previous` remains available; other keys
are suppressed for calculation/spending.
## Merge and Apply Lifecycle
The system uses a two-step merge:
1. **Merge** (`mergeTotalInheritancePoint`)
- Computes derived keys and stores them into `inheritance_{userID}`.
- Records a snapshot into `inheritance_result`.
2. **Apply** (`applyInheritanceUser`)
- Sums all keys into a total, logs to `user_record`, resets storage to only
`previous` (plus any rebirth-kept keys).
This merge/apply is triggered in these places:
- **End of season**: `check_united()` calls merge/apply for all player
generals when the game is unified.
- **Death**: `General::kill()` merges and applies for the owner and refunds
pending inherit actions (random unique, reserved special war).
- **Rebirth**: `General::rebirth()` merges and applies with reduced retention
(`rebirthStoreCoeff`).
## Spending and Effects
### Join-time bonuses (`API/General/Join.php`)
Players can spend points during character creation:
- Choose a war special (`inheritSpecial`)
- Select a starting city (`inheritCity`)
- Set a turn-time zone (`inheritTurntimeZone`)
- Add bonus stats (`inheritBonusStat`)
Costs are defined in `GameConst`:
- `inheritBornSpecialPoint`
- `inheritBornCityPoint`
- `inheritBornTurntimePoint`
- `inheritBornStatPoint`
### InheritAction APIs (`API/InheritAction/*`)
- `BuyHiddenBuff`: buys a hidden buff level (1..5) stored in aux
`inheritBuff` (cost curve `inheritBuffPoints`).
- `SetNextSpecialWar`: reserves the next war special (`inheritSpecificSpecialWar`).
- `ResetSpecialWar`: clears current war special (cost grows by
`inheritResetAttrPointBase`, Fibonacci-extended).
- `ResetTurnTime`: randomizes turn time offset for future turns
(`nextTurnTimeBase`).
- `ResetStat`: reassigns base stats; optional bonus stats cost
`inheritBornStatPoint`.
- `BuyRandomUnique`: flags `inheritRandomUnique` to guarantee a unique drop
once unique items become available (see below).
- `CheckOwner`: reveals another general's owner name (cost
`inheritCheckOwnerPoint`).
All of these are disabled once `game_env.isunited` is set.
### Unique items and auctions
- **Random unique**: `inheritRandomUnique` is consumed in
`tryUniqueItemLottery()` when unique items are available. If no items are
available, points are refunded.
- **Unique auctions**: `AuctionUniqueItem` uses `inheritPoint` as the bid
currency. Opening an auction requires `inheritItemUniqueMinPoint`.
### Betting rewards
When betting is configured with `reqInheritancePoint`, rewards are paid
directly into `previous` (`Betting::giveReward()`), and logged to
`user_record`.
## Hidden Buff System (inheritBuff)
The `inheritBuff` aux value is injected into `General` as a `TriggerInheritBuff`
action. It modifies:
- Combat chances: avoid/critical/strategy trial ratios
- Domestic success/fail probabilities
- Opponent combat chances (negative modifiers)
Buff levels are 0..5 and multiply coefficients by 0.01 per level.
## Rank Tracking
`MergeInheritPointRank` event aggregates all keys (except `previous`) into
`rank_data.inherit_earned_dyn`, and then updates:
- `inherit_earned` (sum of earned)
- `inherit_spent` (from `inherit_spent_dyn`)
This is used for leaderboard-style tracking.
## Related Constants
Key cost and limits (from `GameConstBase` / scenario overrides):
- `inheritBuffPoints` (level costs)
- `inheritSpecificSpecialPoint`
- `inheritResetAttrPointBase`
- `inheritCheckOwnerPoint`
- `inheritItemRandomPoint`
- `inheritItemUniqueMinPoint`
- `minMonthToAllowInheritItem`
Scenario files can override these via `const` in `scenario_*.json`.
-139
View File
@@ -1,139 +0,0 @@
# Legacy Scenario System
This document explains how scenarios are loaded and how they define the active
rule set, commands, and effects. Core references include
`legacy/hwe/sammo/Scenario.php`, `legacy/hwe/sammo/ResetHelper.php`, and
`legacy/hwe/sammo/GameConstBase.php`.
core2026 리소스는 레거시 JSON 결과를 유지하면서 공통 이벤트와 규칙을
`extends`로 합성할 수 있습니다. 새 시나리오 구성과 합성 순서는
[시나리오 리소스 합성](./scenario-composition.md)을 확인해 주세요.
## Scenario Loading Flow
1. Server reset/init calls `ResetHelper::buildScenario()`.
2. `Scenario` loads `scenario_{id}.json` and merges defaults (`default.json`).
3. `Scenario::buildConf()` generates runtime constants:
- `d_setting/GameConst.php` from `GameConstBase + scenario.const/map/stat`
- `d_setting/CityConst.php` from `scenario/map/{mapName}.php`
- `d_setting/GameUnitConst.php` from `scenario/unit/{unitSet}.php`
4. `Scenario::build()` inserts nations, generals, and events into DB and runs
`initialEvents` immediately.
`Scenario::getAllScenarios()` is used for listing scenarios without fully
building them (lazy init).
## Scenario JSON Structure (Observed)
Common top-level keys (see `legacy/hwe/scenario/frame.json` and actual
`scenario_*.json` files):
- `title`, `startYear`, `history`, `iconPath`
- `stat`: default stat totals and bounds
- `map`: `mapName`, `unitSet`, `scenarioEffect`
- `const`: overrides for `GameConst` (commands, items, limits, etc.)
- `nation`, `diplomacy`
- `general`, `general_ex`, `general_neutral`
- `events`, `initialEvents`
- `ignoreDefaultEvents` (skip `GameConst::$defaultInitialEvents/$defaultEvents`)
Notes:
- A few files still use `initialActions` or `defaultInitialEvents` keys. The
engine currently reads `initialEvents` only.
- `general` rows use the tuple format from `Scenario::generateGeneral()`:
`affinity, name, picture, nationName, city, leadership, strength, intel,
officerLevel, birth, death, ego, char, text`.
## General Icon Paths
Scenario general rows store an explicit path relative to `/image/icons`.
New shared portraits use `장수/<general-name>.<extension>`; scenario-specific
portraits keep their existing directory such as `걸그룹/` or `롤시나리오/`.
This makes active and delayed generals persist the same path without requiring
the legacy image service's runtime `list.json` lookup.
`resources/general-icons.json` records the canonical general name, current
path, retained numeric legacy aliases, and source image. The numeric files in
the image repository remain compatibility aliases for deployed legacy
versions. Run the following from a core2026 checkout whenever scenarios or
portrait assets change:
```sh
pnpm manage:general-icons --image-root /path/to/image --write
pnpm manage:general-icons --image-root /path/to/image
```
The first command synchronizes canonical aliases and scenario paths. The second
is a read-only drift and file-content check. Missing source images stay listed
under the catalog's `unresolved` field and resolve to `default.jpg` when they
were an explicit numeric reference.
## How Scenario Chooses Commands and Effects
Scenario config influences runtime rules via `GameConst` and `ScenarioEffect`:
- `const.availableGeneralCommand` / `const.availableChiefCommand` define the
commands that appear in UI and can be executed.
- `const.availableSpecialDomestic/War`, `const.availablePersonality`,
`const.allItems`, `const.availableNationType` control selectable traits/items.
- `map.scenarioEffect` or `const.scenarioEffect` sets
`GameConst::$scenarioEffect`, which is injected into each `General` as an
`iAction` (`General::getActionList()`).
- `const.availableInstantAction` merges into
`GameConst::$availableInstantAction`.
Because `GameConst` is generated from scenario data, a scenario can swap
available commands or replace the action pool entirely.
## Command Prefix Conventions
Prefixes are used to separate rule packs and assets:
- `che_`: default rule set (base commands, specials, items, nation types).
- `cr_`: alternate rule set used by specific scenarios (e.g. `scenario_910`).
- `event_`: scenario-specific extensions (research, extra unit sets, or special
effects).
Example: `scenario_910.json` uses `mapName=cr` and `unitSet=cr` and overrides
`availableGeneralCommand/availableChiefCommand` to include `cr_건국`,
`cr_맹훈련`, and `cr_인구이동` alongside `che_` commands.
## Scenario Environment Variants (Current Repo)
These are the map/unit/effect variants referenced by existing scenario files.
Defaults are `mapName=che` and `unitSet=che` when not specified.
Map sets (`scenario/map/*.php`):
- `che` (default)
- `miniche`, `miniche_b`, `miniche_clean`
- `cr`
- `chess`
- `pokemon_v1`
- `ludo_rathowm`
Unit sets (`scenario/unit/*.php`):
- `che` (default)
- `che_except_siege`
- `cr`
- `basic`
- `siegetank`
- `event_more_crewtype`
- `ludo_rathowm`
Scenario effects (`sammo/ActionScenarioEffect/*`):
- `event_StrongAttacker`
- `event_UnlimitedDefenceThresholdChange`
- `event_MoreEffect`
## Event Targets
Scenario events are stored in the `event` table and executed via
`TurnExecutionHelper::runEventHandler()` using `EventTarget` values:
`PRE_MONTH`, `MONTH`, `OCCUPY_CITY`, `DESTROY_NATION`, `UNITED`.
Most scenario JSON uses lowercase targets (e.g. `"month"`). The DB enum uses
uppercase values but is case-insensitive, so lowercase targets still match.
@@ -1,101 +0,0 @@
# NPC 통일 장기 실행 메모리 프로파일
## 목적
`npcNationUprisingUnification.test.ts`의 고정 시나리오를 실제 턴 처리기로
통일까지 실행하고, 장수 수백 명 규모에서 Node 프로세스와 계산 savepoint가
차지하는 메모리를 분리해 관찰한다. 이 프로파일은 성능 회귀 관찰용이며
운영 capacity 산정이나 레거시 결과 동등성의 단독 근거가 아니다.
## 시나리오 계약
- 시작: 181년 8월, NPC 300명, 도시 9개, 국가 없음
- 고정 입력: world id/seed `1`, 고정 장수 능력치와 배치, 고정 map/unit set
- 실제 경로: NPC 건국, 국가 AI 선전포고, 장수 AI 출병, 전투, 점령, 멸망,
월 경계와 통일 판정
- 수렴 보조: 매 월 국가 ID 1 소유 도시의 인구·방어·성벽·민심을 최대값으로
복원한다. 따라서 이것은 자연 분포 예측이 아니라 통일 도달을 보장하기 위한
synthetic stress fixture다.
- 통일 handler는 실제 조건인 “활성 국가 1개이며 모든 도시를 소유”와
`isUnited=2`/통일 history만 재현한다. 운영 handler의 PostgreSQL
inheritance/hall-of-fame/dynasty 정산은 실행하지 않는다.
- yearbook PostgreSQL upsert와 tournament Redis lifecycle도 실행하지 않는다.
이 외부 저장소들의 메모리는 아래 프로세스 수치에 포함되지 않는다.
연감 map/nation payload는 같은 월 경계 world 상태라면 결정적이고 hash가
같다. 토너먼트 월 판정도 `hiddenSeed + previous year/month`와 선행 국가
power RNG 소비 횟수가 같으면 결정적이다. 이 profile 조사 중 발견한 빈
`tournamentPattern``Math.random()` fallback은
`hiddenSeed, "monthly", previousYear, previousMonth, "tournamentPattern"`
전용 `LiteHashDRBG` shuffle로 교체했다. 이 독립 substream은 pattern을
결정적으로 만들면서 뒤따르는 중립 경매의 기존 monthly RNG 위치를 바꾸지
않는다.
## 실행
```bash
pnpm --filter @sammo-ts/game-engine profile:npc-unification-memory
```
runner는 `node --expose-gc`, Vitest thread worker 1개로 이 파일만 실행한다.
전체 JSON은 기본적으로
`app/game-engine/test-results/npc-unification-memory.json`에 기록되며
`test-results/`는 Git에서 제외된다. 경로는
`NPC_UNIFICATION_MEMORY_REPORT_PATH`로 바꿀 수 있다.
## 측정 정의
- `maxObservedHeapUsedBytes`: 월별 관찰과 명시적 GC 전후 savepoint sample에서
본 Node heap 최고치
- `maxObservedRssBytes`: 같은 관찰점의 프로세스 RSS 최고치
- `processResourceMaxRssBytes`: OS가 보고한 실행 전체 high-water RSS
- `participantSnapshotBytes`: world와 reserved-turn snapshot을 V8 serialize한
크기. 실제 live heap 크기가 아니라 비교 가능한 payload 크기다.
- `participantSnapshotHeapDeltaBytes.peakWhileRetained`: 명시적 GC 직후
snapshot을 잡아 둔 동안의 heap 증가 최고치
- `participantSnapshotHeapDeltaBytes.peakAfterRelease`: snapshot 참조를
해제하고 다시 GC한 뒤 baseline 대비 heap 차이 최고치
- `participantSnapshotCloneMs`: `captureState()` 두 개의 복제 시간
프로세스 수치는 Node, Vitest worker, 테스트 harness, engine state를 모두
포함한다. 반대로 PostgreSQL, Redis와 production daemon 주변 프로세스는
포함하지 않는다.
## 2026-07-28 관찰 결과
Node `v24.14.1`, Linux x64에서 독립 실행 두 번 모두 다음 게임 결과가
동일했다.
- 193년 1월 통일, 137개월 진행
- 5개국 건국, 선전포고 6회, 출병 2,136회
- 종료 시 장수 356명
- participant snapshot: 초기 139,363 bytes, 종료 449,359 bytes,
최고 452,686 bytes
- snapshot 유지 중 heap 증가 최고 2,399,640 bytes, 해제·GC 후 최고
1,132,104 bytes
프로세스 관찰값은 실행별로 다음과 같았다.
| 실행 | wall time | max heap used | max observed RSS | OS max RSS | clone 평균 / 최고 |
| --- | ---: | ---: | ---: | ---: | ---: |
| 1 | 19.87 s | 145,309,008 B | 481,943,552 B | 486,830,080 B | 3.18 / 4.33 ms |
| 2 | 19.86 s | 145,253,136 B | 488,198,144 B | 489,697,280 B | 3.24 / 3.91 ms |
게임 결과와 serialized snapshot 크기는 두 번 모두 정확히 같았다. RSS는
allocator/JIT/Vitest 영향으로 약 6.3 MB 차이가 났으므로 단일 숫자를 엔진
상태 크기로 해석하지 않는다.
## 해석과 남은 범위
이 fixture에서 계산 rollback용 participant snapshot payload의 최고치는
약 0.43 MiB이고, snapshot이 살아 있는 순간의 관찰 heap 증가는 약
2.29 MiB였다. 반면 전체 테스트 프로세스 high-water RSS는 약
464467 MiB였다. 따라서 이 실행에서 RSS 대부분을 savepoint payload
자체가 설명하지는 않는다.
운영 통일 handler는 `isUnited`와 history를 memory에 반영한 뒤 inheritance,
hall-of-fame, dynasty PostgreSQL 정산 세 개를 await하지 않고 시작한다.
이 프로파일은 통일 판정까지의 engine memory를 측정하지만 그 비동기 정산의
완료, 실패 복구, 메모리 또는 distributed atomicity는 검증하지 않는다.
그 경로를 관찰하려면 격리 PostgreSQL fixture와 정산 완료 barrier 또는
durable outbox가 별도로 필요하다.
+94 -46
View File
@@ -1,66 +1,114 @@
# Architecture Overview
# core2026 아키텍처
This repository contains both the active legacy PHP game and an in-progress
TypeScript rewrite. The legacy engine remains the source of truth while the
monorepo plan is prepared alongside it.
## 시스템 경계
## Project Naming
`core2026`은 계정·profile 운영을 담당하는 gateway와 profile별 게임 런타임을
분리합니다.
- Official name: 삼국지 모의전투 HiDCHe
- Common nicknames: 삼모, 삼모전, 힏체섭
- Short forms in code/docs: sammo, hidche
- TypeScript rewrite working name: sammo-ts
```text
browser
├─ gateway-frontend ── tRPC ── gateway-api
│ ├─ gateway PostgreSQL
│ ├─ Redis session
│ └─ GatewayOrchestrator ── worktree / PM2
└─ game-frontend ───── tRPC/SSE ─ game-api
├─ profile PostgreSQL
├─ Redis worker/fan-out
└─ input_event ── game-engine
├─ DB lease/fencing
├─ in-memory world
└─ transactional flush
```
## Layers
Gateway session과 game session token은 별도 수명을 가집니다. game API는
token의 user, profile, role, sanction을 검증하고, 장수·국가 소유권은
PostgreSQL 상태와 결합해 서버에서 결정합니다.
- Legacy runtime: PHP entry points under `legacy/` and `legacy/hwe/`
- Legacy engine core: domain logic under `legacy/hwe/sammo/`
- Legacy frontend: Vue/TypeScript under `legacy/hwe/ts/`
- Rewrite (in progress): pnpm workspace monorepo under `packages/` and `app/`
## 애플리케이션
## Current Implementation Notes
### Gateway
- `packages/infra` is live with Prisma/Postgres connectors used by game services.
- `app/game-engine` implements an in-memory turn daemon with DB flush hooks and Redis control transport.
- `app/game-api` exposes tRPC endpoints for reserved turns, messages, and battle sims, communicating with the daemon via Redis.
- Gateway/game frontends are still placeholders (not part of current runtime).
- `app/gateway-frontend`는 가입, 로그인, 로비, 계정, 관리자 화면을 제공합니다.
- `app/gateway-api/src/router.ts`는 인증·로비·계정 router를 조립합니다.
- `app/gateway-api/src/adminRouter.ts`는 관리자 인증 뒤 사용자·profile·operation
기능을 제공합니다.
- `app/gateway-api/src/orchestrator/`는 DB operation queue, commit별 worktree,
build, PM2 process와 예약 상태를 조정합니다.
## Legacy Data Migration Policy
### Game
- Data under `legacy/` is migration-only and not used by the rewrite at runtime.
- After DB migration completes, legacy data is no longer required.
- `app/game-frontend`는 profile base path에서 실행하는 Vue SPA입니다.
- `app/game-api/src/router.ts`는 auth, public, general, nation, turns, board,
message, auction, tournament, archive 등 도메인 router를 조립합니다.
- `app/game-api/src/realtime/`은 commit 이후 알림을 SSE로 전달합니다.
- `app/game-api/src/daemon/`, `battleSim/`, `auction/`, `tournament/`는 각 worker
transport와 scheduler를 소유합니다.
- `app/game-engine/src/turn/turnDaemon.ts`는 world loader, 명령 registry,
AI, 월간 action, persistence hook을 조립합니다.
- `TurnDaemonLifecycle`은 clock, schedule, pause/resume/run command와 한 번의
실행 transaction을 제어합니다.
## Data and State
## 공유 package
- PHP engine owns authoritative gameplay state today
- Scenario and unit pack data are loaded from `legacy/hwe/scenario/`
- Deterministic RNG is required for gameplay outcomes
- Build/runtime profiles are server+scenario pairs; scenario selection is required
because it drives unit sets and DB settings.
### `packages/common`
## Legacy Docs
프로세스 사이에 공유하는 타입, schema, 인증 token, 직렬화와 RNG가 있습니다.
Gameplay 난수는 `LiteHashDRBG`, `RNG`, `RandUtil` 경로를 사용합니다.
- Legacy entities and DB schema overview: `docs/architecture/legacy-entities.md`
- Legacy engine map: `docs/architecture/legacy-engine.md`
- Postgres schema proposal (rewrite): `docs/architecture/postgres-schema.md`
### `packages/logic`
## Cross-Cutting Policies
DB와 서버 process에 종속되지 않는 게임 규칙을 소유합니다.
- No ad-hoc randomness for gameplay; use deterministic RNG
- External JSON/data inputs must be validated with zod; name zod schemas with a `z` prefix.
- Keep domain logic independent of endpoints or UI
- Prefer clear Korean comments in core gameplay logic for maintainers
- Test strategy and layering: `docs/testing-policy.md`
- `actions/`: 장수·국가 command definition과 실행 결과
- `constraints/`: 예약·실행 조건과 state view
- `actionModules/`: trait·관직·병종·계승·item 효과 조립
- `war/`: 전투 unit, phase, trigger와 결과
- `scenario/`, `world/`: resource schema와 world 타입
- `items/`, `crewType/`, `inheritance/`, `diplomacy/`: 도메인 catalog
## Runtime Processing (Outline)
Command는 `GeneralActionDefinition` 또는 국가 command module로 args,
constraint, turn metadata, 결과와 로그를 선언합니다. 런타임 context와
persistence는 `app/game-engine`이 제공합니다.
This document links to detailed runtime behavior in `docs/architecture/runtime.md`.
Use that document for turn daemon scheduling, API request handling, and
persistence sequencing.
### `packages/infra`
The turn daemon lifecycle and control contract are documented in
`docs/architecture/turn-daemon-lifecycle.md`.
`prisma/gateway.prisma``prisma/game.prisma`가 영속 schema의 기준입니다.
`src/gatewayPrisma.ts`, `src/gamePrisma.ts`, `src/postgres.ts`, `src/redis.ts`
연결과 client 생성을 담당합니다.
## Documentation TODOs
## 데이터 소유권
- Pending follow-ups: `docs/architecture/todo.md`.
| 데이터 | 기준 저장소 | 주요 접근 경로 |
| ----------------------------- | ------------------------------------ | --------------------- |
| 사용자·계정·profile·operation | gateway PostgreSQL | gateway API |
| gateway session·flush signal | Redis | gateway API |
| world·장수·국가·도시·턴·로그 | profile PostgreSQL | game API, game engine |
| durable 입력과 실행 상태 | profile PostgreSQL `InputEvent` | game API, game engine |
| turn daemon 소유권 | profile PostgreSQL `TurnDaemonLease` | game engine |
| realtime 알림 | Redis와 SSE | game API |
| scenario·map·unit set | `resources/` | game engine, build |
| 이미지 | 외부 `/image/*` | Caddy |
게임 상태 mutation은 PostgreSQL transaction이 확정합니다. Redis 전달 성공을
DB commit의 대체 조건으로 사용하지 않습니다.
## 시나리오와 profile
`profile`은 규칙·자산 계열이고 `scenario`는 게임 초기 데이터입니다.
런타임 식별자는 `${profile}:${scenario}`입니다. Scenario JSON은
`resources/scenario`에서 합성되고 zod parser를 거쳐 seeder와 runtime에
전달됩니다. map, unit set, turn-command profile도 resource loader를 통해
선택됩니다.
## 구현을 찾는 순서
1. frontend route와 store에서 사용자 흐름을 찾습니다.
2. gateway/game tRPC router에서 input, auth, transaction을 찾습니다.
3. engine command registry와 handler에서 실행 순서를 찾습니다.
4. `packages/logic`에서 constraint, 계산, trigger를 찾습니다.
5. Prisma schema, loader, dirty state, flush hook에서 저장 경계를 찾습니다.
6. unit·integration·differential·Chromium fixture에서 보장 범위를 확인합니다.
ref 대응은 상위 작업공간의 `../docs/ref-core2026-mapping.md`를 사용합니다.
현재 코드 경로가 문서의 근거이며, 보고서의 완료 표현만으로 구현 상태를
판정하지 않습니다.
-306
View File
@@ -1,306 +0,0 @@
# Postgres Schema Proposal (sammo-ts)
This document proposes a PostgreSQL schema baseline for the TypeScript rewrite.
It keeps legacy semantics while enabling flexible growth through JSONB `meta`
fields. Use this as the starting point for Prisma models and migrations.
## Design Principles
- Prefer explicit columns for hot paths and indexed filters.
- Use JSONB `meta` for infrequently queried, extensible, or experimental data.
- Keep JSONB values as objects (not arrays or scalars).
- Use partial or expression indexes when a JSONB key becomes a hot filter.
- If multiple server profiles live in one database, add `server_id` to every
table and index it; otherwise, use one database per profile. Scenario
selection is still required at build/runtime, but does not change the DB
partitioning baseline.
## Meta Policy (Reset)
- The rewrite resets naming: legacy `aux` is now `meta`.
- `meta` is the default extension surface for each entity.
- Keys with explicit enum contracts (ex: `NationAuxKey`) should live in a
dedicated table or typed column, not inside `meta`.
- When a key transitions into a hot query/filter, promote it to a column or
`*_flags` table and keep the old value mirrored during migration.
## Core Tables (DDL Sketch)
```sql
-- users: account-level identity (game servers can share this table)
CREATE TABLE app_user (
id BIGSERIAL PRIMARY KEY,
login_id TEXT NOT NULL UNIQUE,
display_name TEXT NOT NULL,
password_hash TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
last_login_at TIMESTAMPTZ
);
-- world_state: per-server runtime state and config
CREATE TABLE world_state (
id SMALLSERIAL PRIMARY KEY,
scenario_code TEXT NOT NULL,
current_year SMALLINT NOT NULL,
current_month SMALLINT NOT NULL,
tick_seconds INTEGER NOT NULL,
config JSONB NOT NULL DEFAULT '{}'::jsonb,
meta JSONB NOT NULL DEFAULT '{}'::jsonb,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CHECK (jsonb_typeof(config) = 'object'),
CHECK (jsonb_typeof(meta) = 'object')
);
CREATE TABLE nation (
id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL,
color TEXT NOT NULL,
capital_city_id BIGINT,
gold INTEGER NOT NULL DEFAULT 0,
rice INTEGER NOT NULL DEFAULT 0,
bill SMALLINT NOT NULL DEFAULT 0,
rate SMALLINT NOT NULL DEFAULT 0,
rate_tmp SMALLINT NOT NULL DEFAULT 0,
secret_limit SMALLINT NOT NULL DEFAULT 3,
chief_general_id BIGINT NOT NULL DEFAULT 0,
scout_level SMALLINT NOT NULL DEFAULT 0,
war_state SMALLINT NOT NULL DEFAULT 0,
strategic_cmd_limit SMALLINT NOT NULL DEFAULT 36,
surrender_limit SMALLINT NOT NULL DEFAULT 72,
tech REAL NOT NULL DEFAULT 0,
power INTEGER NOT NULL DEFAULT 0,
level SMALLINT NOT NULL DEFAULT 0,
type_code TEXT NOT NULL DEFAULT 'che_중립',
spy JSONB NOT NULL DEFAULT '{}'::jsonb,
meta JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CHECK (jsonb_typeof(spy) = 'object'),
CHECK (jsonb_typeof(meta) = 'object')
);
CREATE TABLE city (
id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL,
level SMALLINT NOT NULL,
nation_id BIGINT NOT NULL DEFAULT 0,
supply_state SMALLINT NOT NULL DEFAULT 1,
front_state SMALLINT NOT NULL DEFAULT 0,
pop INTEGER NOT NULL,
pop_max INTEGER NOT NULL,
agri INTEGER NOT NULL,
agri_max INTEGER NOT NULL,
comm INTEGER NOT NULL,
comm_max INTEGER NOT NULL,
secu INTEGER NOT NULL,
secu_max INTEGER NOT NULL,
trust INTEGER NOT NULL DEFAULT 0,
trade INTEGER NOT NULL DEFAULT 100,
dead SMALLINT NOT NULL DEFAULT 0,
def INTEGER NOT NULL DEFAULT 0,
def_max INTEGER NOT NULL DEFAULT 0,
wall INTEGER NOT NULL DEFAULT 0,
wall_max INTEGER NOT NULL DEFAULT 0,
officer_set INTEGER NOT NULL DEFAULT 0,
state SMALLINT NOT NULL DEFAULT 0,
region SMALLINT NOT NULL DEFAULT 0,
term SMALLINT NOT NULL DEFAULT 0,
conflict JSONB NOT NULL DEFAULT '{}'::jsonb,
meta JSONB NOT NULL DEFAULT '{}'::jsonb,
CHECK (jsonb_typeof(conflict) = 'object'),
CHECK (jsonb_typeof(meta) = 'object')
);
CREATE TABLE troop (
id BIGSERIAL PRIMARY KEY,
leader_general_id BIGINT NOT NULL,
nation_id BIGINT NOT NULL DEFAULT 0,
name TEXT NOT NULL,
meta JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CHECK (jsonb_typeof(meta) = 'object')
);
CREATE TABLE general (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT,
name TEXT NOT NULL,
nation_id BIGINT NOT NULL DEFAULT 0,
city_id BIGINT NOT NULL DEFAULT 0,
troop_id BIGINT NOT NULL DEFAULT 0,
npc_state SMALLINT NOT NULL DEFAULT 0,
affinity SMALLINT NOT NULL DEFAULT 0,
born_year SMALLINT NOT NULL DEFAULT 180,
dead_year SMALLINT NOT NULL DEFAULT 300,
picture TEXT NOT NULL,
image_server SMALLINT NOT NULL DEFAULT 0,
leadership SMALLINT NOT NULL DEFAULT 50,
leadership_exp SMALLINT NOT NULL DEFAULT 0,
strength SMALLINT NOT NULL DEFAULT 50,
strength_exp SMALLINT NOT NULL DEFAULT 0,
intel SMALLINT NOT NULL DEFAULT 50,
intel_exp SMALLINT NOT NULL DEFAULT 0,
injury SMALLINT NOT NULL DEFAULT 0,
experience INTEGER NOT NULL DEFAULT 0,
dedication INTEGER NOT NULL DEFAULT 0,
officer_level SMALLINT NOT NULL DEFAULT 0,
officer_city INTEGER NOT NULL DEFAULT 0,
permission TEXT NOT NULL DEFAULT 'normal',
gold INTEGER NOT NULL DEFAULT 1000,
rice INTEGER NOT NULL DEFAULT 1000,
crew INTEGER NOT NULL DEFAULT 0,
crew_type SMALLINT NOT NULL DEFAULT 0,
train SMALLINT NOT NULL DEFAULT 0,
atmos SMALLINT NOT NULL DEFAULT 0,
weapon_code TEXT NOT NULL DEFAULT 'None',
book_code TEXT NOT NULL DEFAULT 'None',
horse_code TEXT NOT NULL DEFAULT 'None',
item_code TEXT NOT NULL DEFAULT 'None',
turn_time TIMESTAMPTZ NOT NULL,
recent_war_time TIMESTAMPTZ,
make_limit SMALLINT NOT NULL DEFAULT 0,
kill_turn SMALLINT,
block_state SMALLINT NOT NULL DEFAULT 0,
ded_level SMALLINT NOT NULL DEFAULT 0,
exp_level SMALLINT NOT NULL DEFAULT 0,
age SMALLINT NOT NULL DEFAULT 20,
start_age SMALLINT NOT NULL DEFAULT 20,
belong SMALLINT NOT NULL DEFAULT 1,
betray SMALLINT NOT NULL DEFAULT 0,
personal_code TEXT NOT NULL DEFAULT 'None',
special_code TEXT NOT NULL DEFAULT 'None',
spec_age SMALLINT NOT NULL DEFAULT 0,
special2_code TEXT NOT NULL DEFAULT 'None',
spec2_age SMALLINT NOT NULL DEFAULT 0,
defence_train SMALLINT NOT NULL DEFAULT 80,
tournament_state SMALLINT NOT NULL DEFAULT 0,
last_turn JSONB NOT NULL DEFAULT '{}'::jsonb,
meta JSONB NOT NULL DEFAULT '{}'::jsonb,
penalty JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CHECK (jsonb_typeof(last_turn) = 'object'),
CHECK (jsonb_typeof(meta) = 'object'),
CHECK (jsonb_typeof(penalty) = 'object')
);
CREATE TABLE general_turn (
id BIGSERIAL PRIMARY KEY,
general_id BIGINT NOT NULL REFERENCES general(id) ON DELETE CASCADE,
turn_idx SMALLINT NOT NULL,
action_code TEXT NOT NULL,
arg JSONB NOT NULL DEFAULT '{}'::jsonb,
brief TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (general_id, turn_idx),
CHECK (jsonb_typeof(arg) = 'object')
);
CREATE TABLE nation_turn (
id BIGSERIAL PRIMARY KEY,
nation_id BIGINT NOT NULL REFERENCES nation(id) ON DELETE CASCADE,
officer_level SMALLINT NOT NULL,
turn_idx SMALLINT NOT NULL,
action_code TEXT NOT NULL,
arg JSONB NOT NULL DEFAULT '{}'::jsonb,
brief TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (nation_id, officer_level, turn_idx),
CHECK (jsonb_typeof(arg) = 'object')
);
CREATE TABLE diplomacy (
id BIGSERIAL PRIMARY KEY,
src_nation_id BIGINT NOT NULL,
dest_nation_id BIGINT NOT NULL,
state_code TEXT NOT NULL,
term SMALLINT NOT NULL DEFAULT 0,
is_dead BOOLEAN NOT NULL DEFAULT false,
is_showing BOOLEAN NOT NULL DEFAULT true,
meta JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CHECK (jsonb_typeof(meta) = 'object')
);
CREATE TABLE message (
id BIGSERIAL PRIMARY KEY,
mailbox_code TEXT NOT NULL,
message_type TEXT NOT NULL,
src_id BIGINT,
dest_id BIGINT,
sent_at TIMESTAMPTZ NOT NULL DEFAULT now(),
valid_until TIMESTAMPTZ,
payload JSONB NOT NULL DEFAULT '{}'::jsonb,
meta JSONB NOT NULL DEFAULT '{}'::jsonb,
CHECK (jsonb_typeof(payload) = 'object'),
CHECK (jsonb_typeof(meta) = 'object')
);
CREATE TABLE event (
id BIGSERIAL PRIMARY KEY,
target_code TEXT NOT NULL,
priority SMALLINT NOT NULL DEFAULT 0,
condition JSONB NOT NULL DEFAULT '{}'::jsonb,
action JSONB NOT NULL DEFAULT '{}'::jsonb,
meta JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CHECK (jsonb_typeof(condition) = 'object'),
CHECK (jsonb_typeof(action) = 'object'),
CHECK (jsonb_typeof(meta) = 'object')
);
```
## Meta Indexing Examples
```sql
-- Generic meta GIN index (use jsonb_path_ops when keys are simple)
CREATE INDEX idx_general_meta_gin ON general USING GIN (meta jsonb_path_ops);
-- Hot key promotion: when a key becomes a frequent filter
CREATE INDEX idx_general_meta_trait ON general ((meta ->> 'trait_code'));
-- JSONB object safety (optional additional guardrails)
ALTER TABLE general ADD CONSTRAINT general_meta_object CHECK (jsonb_typeof(meta) = 'object');
```
## `NationAuxKey` Handling
Legacy `NationAuxKey` is an explicit enum contract. Do not store these keys in
`nation.meta`. Use a dedicated table or typed column instead.
```sql
CREATE TYPE nation_aux_key AS ENUM (
'can_국기변경',
'can_국호변경',
'did_특성초토화',
'can_무작위수도이전',
'can_대검병사용',
'can_극병사용',
'can_화시병사용',
'can_원융노병사용',
'can_산저병사용',
'can_상병사용',
'can_음귀병사용',
'can_무희사용',
'can_화륜차사용'
);
CREATE TABLE nation_flag (
nation_id BIGINT NOT NULL REFERENCES nation(id) ON DELETE CASCADE,
key nation_aux_key NOT NULL,
value JSONB NOT NULL DEFAULT 'true'::jsonb,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (nation_id, key)
);
```
## Migration Notes
- Legacy `aux` is renamed to `meta` in the rewrite; migrate values 1:1.
- `meta`, `last_turn`, `penalty`, `arg`, and message payloads map naturally to
JSONB. Keep them as objects to ease validations and reuse existing TS RNG
tooling without ad-hoc randomness.
- Favor deterministic keys in JSONB for reproducible action logs and turn
outcomes.
- If a JSONB key starts affecting query plans or UI filters, promote it to a
typed column and keep a backfill step in the migration.
-106
View File
@@ -1,106 +0,0 @@
# Rewrite Constraint Contract
This document defines a shared constraint contract for the rewrite so both the
turn daemon (in-memory) and API server (DB-backed) can evaluate constraints
consistently.
## Goals
- Single source of truth for constraint logic.
- Support full evaluation in daemon and precheck in API.
- Explicit data requirements for batching and caching.
- Deterministic reasons for deny vs unknown outcomes.
## Types (TypeScript sketch)
```ts
export type ConstraintResult =
| { kind: 'allow' }
| { kind: 'deny'; reason: string; code?: string }
| { kind: 'unknown'; missing: RequirementKey[] };
export type RequirementKey =
| { kind: 'general'; id: number }
| { kind: 'city'; id: number }
| { kind: 'nation'; id: number }
| { kind: 'destGeneral'; id: number }
| { kind: 'destCity'; id: number }
| { kind: 'destNation'; id: number }
| { kind: 'arg'; key: string }
| { kind: 'env'; key: string };
export interface ConstraintContext {
actorId: number;
cityId?: number;
nationId?: number;
destGeneralId?: number;
destCityId?: number;
destNationId?: number;
args: Record<string, unknown>;
env: Record<string, unknown>;
mode: 'full' | 'precheck';
}
export interface StateView {
has(req: RequirementKey): boolean;
get(req: RequirementKey): unknown | null;
}
export interface Constraint {
name: string;
requires(ctx: ConstraintContext): RequirementKey[];
test(ctx: ConstraintContext, view: StateView): ConstraintResult;
}
```
## Evaluation Flow
- `ConstraintPlanner` collects requirements across constraints.
- `StateView` loads those requirements (daemon: in-memory, API: DB).
- `test()` returns:
- `allow` if constraint passes.
- `deny` with a stable reason/code for UI.
- `unknown` if required data is missing and `mode === 'precheck'`.
```ts
function evaluateConstraints(constraints: Constraint[], ctx: ConstraintContext, view: StateView): ConstraintResult {
for (const constraint of constraints) {
const missing = constraint.requires(ctx).filter((req) => !view.has(req));
if (missing.length && ctx.mode === 'precheck') {
return { kind: 'unknown', missing };
}
const result = constraint.test(ctx, view);
if (result.kind !== 'allow') {
return result;
}
}
return { kind: 'allow' };
}
```
## StateView Selection Boundary
The split between in-memory and DB-backed evaluation happens outside the
constraint logic. A factory or loader chooses the `StateView` implementation
based on the execution environment:
- Turn daemon -> `InMemoryStateView` with a full in-memory snapshot.
- API server -> `DbStateView` (or `ProjectedStateView`) that fetches only the
required fields from the DB or precomputed projections.
This keeps constraints pure and deterministic, while the infrastructure layer
decides how to satisfy `requires()` in each runtime.
## Mapping from Legacy Flags
- `REQ_GENERAL` -> `{ kind: 'general', id: actorId }`
- `REQ_CITY` -> `{ kind: 'city', id: cityId }`
- `REQ_NATION` -> `{ kind: 'nation', id: nationId }`
- `REQ_DEST_*` -> respective `dest` key
- `REQ_ARG` -> `{ kind: 'arg', key: <argName> }`
- `env` dependencies (for example `turnterm`, `year`) -> `{ kind: 'env', key: 'turnterm' }`
## Data Projection Suggestion
- API prechecks can rely on a small read model (for example `general_summary`)
updated by the turn daemon; `StateView` selects the source per requirement.
-55
View File
@@ -1,55 +0,0 @@
# TypeScript Rewrite Plan
The rewrite targets a pnpm workspace-based monorepo with Node.js services and
Vue 3 frontends.
## Planned Layout
- `/packages/common`: shared utilities and type definitions (RNG/bytes/테스트 RNG 포함, no infra)
- `/packages/infra`: Prisma/Redis connectors and other runtime infra
- `/packages/logic`: pure game logic with DI and interfaces
- `/app/gateway-frontend`: gateway UI
- `/app/gateway-api`: gateway service
- `/app/game-frontend`: game UI
- `/app/game-api`: game backend per server+scenario profile
- `/app/game-engine`: turn daemon per server+scenario profile
- `/tools/build-scripts`: build and deployment scripts
## Current Implementation Notes
- `packages/infra` is live and used by game-api/game-engine services.
- `app/game-api` and `app/game-engine` have initial implementations (tRPC endpoints, turn daemon loop).
- Redis transport for turn daemon control is implemented and wired.
- Frontend apps remain placeholders while backend/runtime stabilizes.
## Runtime Stack (Planned)
- Backend: Node.js + Fastify
- API: tRPC + zod
- ORM: Prisma
- Frontend: Vue 3, Pinia, Vue Router, TailwindCSS, Vite
- Data: PostgreSQL, Redis sessions
- Testing: Vitest
## Frontend Direction
- Gateway and game apps are separate SPAs (`/app/gateway-frontend`, `/app/game-frontend`).
- UI visuals should stay close to the legacy look, but layout changes are allowed
as long as required information is preserved.
- Prefer client-driven rendering: fetch most data via API and let the client own
data shaping and presentation unless the data must be hidden.
## Constraint Evaluation Contract
The shared constraint contract (daemon vs API precheck split) is documented in
`docs/architecture/rewrite-constraints.md`.
## Legacy Data Migration
- Legacy data is for migration only; the rewrite runtime does not depend on it.
- Once DB migration is complete, legacy data can be retired.
## Profiles (Planned)
- Profiles are server+scenario pairs; scenario selection is required for build/runtime.
- Server IDs: `che`, `kwe`, `pwe`, `twe`, `nya`, `pya`
+91 -266
View File
@@ -1,297 +1,122 @@
# Runtime and Build Profiles
# 런타임 아키텍처
Build outputs should be emitted to `/dist/{profileName}` per profile to keep
deployments predictable. Profiles are server+scenario pairs, and scenario
selection is required because it drives unit sets and DB settings.
## 프로세스
## TypeScript Toolchain
| 프로세스 | 시작점 | 책임 |
| -------------------- | -------------------------------------------------------- | ------------------------------------------- |
| gateway API | `app/gateway-api/src/server.ts` | 계정, session, profile, admin operation |
| gateway orchestrator | `app/gateway-api/src/orchestrator/orchestratorServer.ts` | DB queue, build, PM2 reconciliation |
| game API | `app/game-api/src/server.ts` | profile tRPC, SSE, worker transport |
| turn daemon | `app/game-engine/src/turn/cli.ts` | schedule, command, 월간 lifecycle, DB flush |
| battle worker | `app/game-api/src/battleSim/worker.ts` | 격리된 전투 시뮬레이션 |
| auction worker | `app/game-api/src/auction/worker.ts` | 경매 timer와 낙찰 처리 |
| tournament worker | `app/game-api/src/tournament/worker.ts` | 대회 진행과 결과 처리 |
The rewrite workspace uses exactly TypeScript `6.0.2`. TypeScript 7 is not a
supported build or validation environment until the compiler/tooling APIs used
by this repository and the dependent toolchain are available and compatible.
Package-local TypeScript 5 or 7 overrides are not allowed in `app/*`,
`packages/*`, or `tools/*`. The legacy runtime under `legacy/` is maintained
separately and is outside this rule.
Gateway API와 game API는 기본적으로 `0.0.0.0`에 bind합니다. 실제 port와
prefix는 환경 변수와 배포 profile이 결정합니다.
The rationale, scope, and TypeScript 7 upgrade gate are defined in
[`typescript-version.md`](typescript-version.md).
## Gateway 실행
## Database Schemas (Gateway vs Game)
`resolveGatewayApiConfigFromEnv()`가 PostgreSQL schema, Redis prefix, session
TTL, game-token secret, OAuth, local-account 정책과 orchestrator 설정을
검증합니다.
Gateway uses a shared schema (default `public`) for login/profile state, while
each game profile runs against its own schema. This keeps gateway data stable
and allows profile-scoped game data resets.
Gateway API는 다음 저장 경계를 사용합니다.
- Gateway schema: `GATEWAY_DB_SCHEMA` (default `public`)
- Game schema: `PROFILE` value (e.g., `hwe`, `che`)
- Optional override for gateway DB URL: `GATEWAY_DATABASE_URL`
- `AppUser`, `SystemSetting`: 계정과 정책
- `GatewayProfile`: profile, scenario, port, 상태와 build 결과
- `GatewayOperation`: build/reset/open/close 등 실행 요청과 결과
- Redis: gateway session, OAuth 임시 상태, flush channel
## Suggested Build Pattern
Orchestrator는 `GatewayOperation`을 claim하고 source ref를 commit으로
해결합니다. `WorkspaceManager`가 commit별 worktree를 준비하고 build runner가
artifact를 만들며 `Pm2ProcessManager`가 profile process를 조정합니다.
재시작 시 DB 상태와 process 상태를 reconciliation합니다.
- Wrapper script under `tools/build-scripts`
- `pnpm build:server --profile che --scenario default`
- CI-friendly: `PROFILE=che SCENARIO=default pnpm build:server`
- Build tooling: use `tsdown` for backend/libs, and Vite for frontend apps.
## Game API 실행
## Deterministic RNG Policy
`resolveGameApiConfigFromEnv()``PROFILE`, `SCENARIO`,
`GAME_PROFILE_NAME`, API·SSE·upload 경로와 worker timeout을 결정합니다.
기본 profile name은 `${profile}:${scenario}`입니다.
- Gameplay randomness must be reproducible from a deterministic seed
- Prefer `legacy/hwe/ts/util/LiteHashDRBG.ts` and `legacy/hwe/ts/util/RNG.ts`
- Seed composition should include hidden base seed plus action context
Fastify server는 요청에서 game token을 검증해 `GameApiContext.auth`
넣습니다. Router는 public procedure와 인증 procedure를 구분하고, 변경
대상 장수·국가·archive owner를 session actor와 DB에서 해석합니다.
## Gateway Orchestration (Single Host Draft)
Mutation은 두 형태입니다.
Gateway API is the single source of truth for profile state and reconciles
PM2-managed processes on boot and on a short interval. The orchestrator runs
as a separate process (`GATEWAY_ROLE=orchestrator`). The DB owns the desired
state; PM2 is treated as the actuator. The runtime state is grouped so that
`game-api` + `turn-daemon` are either on together or off together.
- API transaction으로 끝나는 mutation은 `executeInputEvent()`
`target=API` event를 만들고 결과와 event 상태를 같은 transaction에서
commit합니다.
- turn world가 필요한 mutation은 daemon transport가 `target=DAEMON`
event를 만들고 turn daemon의 처리 대상으로 전달합니다.
### DB-Owned Profile State
같은 `requestId`의 완료·처리 중 event는 중복 수락하지 않습니다. 실패 event는
claim 가능한 상태에서 attempts를 증가시켜 재처리합니다.
Profiles are tracked by `profileName` (= `${profile}:${scenario}`).
Gateway loads the profile table on boot, then reconciles PM2 to match.
## Turn daemon 조립
- `예약됨` (RESERVED): preopen/open timestamps are set; processes off.
- `가오픈` (PREOPEN): build done; API+daemon on, daemon paused.
- `가동중` (RUNNING): both `game-api` and `turn-daemon` should be on.
- `정지됨` (STOPPED): processes off; may be resumed later.
- `정지(오류)` (PAUSED): fatal error; daemon paused but process remains on.
- `천하통일` (COMPLETED): game finished; API on, daemon paused.
- `비활성화` (DISABLED): excluded from orchestration; start forbidden.
`createTurnDaemonRuntime()`은 다음 순서로 런타임을 구성합니다.
### Boot Reconciliation
1. `DatabaseTurnDaemonLease`가 profile lease와 fencing token을 확보합니다.
2. `loadTurnWorldFromDatabase()`가 world, 장수, 국가, 도시, 외교, 예약 턴,
event와 resource snapshot을 읽습니다.
3. scenario, map, unit set과 command profile을 적재합니다.
4. action-module bundle, AI, command registry, 월간 event action을 조립합니다.
5. `EngineStateManager`가 in-memory mutation과 transaction flush를 묶습니다.
6. `TurnDaemonLifecycle`이 control queue와 schedule을 실행합니다.
1. Load profile rows from DB.
2. List PM2 processes and map `profileName -> running state`.
3. For each profile:
- If desired `RUNNING/PREOPEN/PAUSED/COMPLETED` and any process is missing, start.
- If desired `RESERVED/STOPPED/DISABLED` and any process is running, stop both.
4. Persist errors to DB for audit.
Lifecycle은 가장 빠른 장수 턴과 다음 tick 중 앞선 시각을 선택합니다.
pause gate, 수동 run, shutdown과 budget을 같은 loop에서 처리합니다.
### Internal Scheduler (Gateway Cron)
## 한 번의 실행과 저장
Gateway runs a lightweight cron loop (setInterval) that:
- When `RESERVED` and `preopenAt <= now`, queue a build for the reserved commit.
- When build succeeds, status becomes `PREOPEN` (daemon paused).
- When `openAt <= now`, status becomes `RUNNING` and daemon resumes.
- Optionally drains a build queue (see build workflow).
### Build Workflow (Admin)
- 관리자는 gateway의 `/admin/server-operations` 전용 화면에서 profile별
runtime, build, worktree, 오류와 작업 이력을 확인하고 시작·정지·초기화를
요청한다.
- 운영 요청은 `gateway_operation` 행으로 기록된다. 같은 profile에는
`QUEUED` 또는 `RUNNING` 작업이 하나만 존재할 수 있으며, 상태는
`QUEUED -> RUNNING -> SUCCEEDED|FAILED` 또는 실행 전 `CANCELLED`
전이된다. 실패·취소 작업은 같은 입력으로 재시도할 수 있다.
- `BRANCH` 소스는 요청 시 유효성을 검사하지만 branch 이름을 보존하고,
실행 직전에 원격을 다시 fetch하여 최신 commit을 확정한다. `COMMIT`
소스는 요청 시 full SHA로 정규화하여 이후 branch 이동과 무관하게
동일 commit을 사용한다. 실제 사용한 SHA는 작업의
`resolvedCommitSha`와 profile의 `buildCommitSha`에 기록한다.
- 초기화 작업은 profile 프로세스를 정지한 뒤 선택 commit worktree에서
build하고, 같은 worktree의 scenario/map/unitset으로 profile DB를 seed한
`PREOPEN` 또는 `RUNNING`으로 전환하여 API와 daemon을 함께 시작한다.
대기 중 작업은 취소할 수 있으나 DB 변경이 시작된 `RUNNING` 작업은
중간 취소하지 않는다.
- Admin triggers a build request for a profile.
- Gateway queues a build job with `(profileName, commitSha)` and prepares a
per-commit workspace (`/.worktrees/{commitSha}` by default).
- Workspace is backed by `git worktree` and is reused across builds for the same commit.
- Each workspace stores `lastUsedAt` in DB so cleanup can remove stale worktrees.
- Cleanup is invoked manually by admin API and removes worktrees unused for 6+ months.
- Build runs `pnpm install` when workspace is created, then executes
the common build, Prisma client generation, and infra/logic dependency builds before
`pnpm --filter @sammo-ts/game-api build` and
`pnpm --filter @sammo-ts/game-engine build`, then marks build success/failure.
- On success, status moves to `PREOPEN` for reserved builds or stays unchanged for manual builds.
- PM2 starts API/daemon from the profile row's `buildWorkspace`, so profiles
pinned to different commits execute the artifacts from their own detached
worktrees. A profile without `buildWorkspace` intentionally falls back to the
main workspace; this is the `hwe`/main compatibility path.
`GatewayProfile.meta.adminActions` polling is retained only for requests
created by the older admin page. New operations must use `gateway_operation`
so concurrency, audit history, cancellation, retry, source intent, and the
resolved commit remain first-class data.
## Current Implementation Status
- Turn daemon lifecycle + in-memory state live in `app/game-engine` with DB flush hooks.
- 모든 tRPC mutation은 PostgreSQL `input_event` 경계에서 request ID와 처리
상태를 기록한다.
- 월드 mutation과 daemon control은 PostgreSQL inbox로 전달된다. Redis는
더 이상 이 명령들의 영속 큐가 아니며 realtime/battle-sim 전송에만 남아
있다.
- API가 직접 변경하는 예약 턴·메시지 등의 DB 쓰기는 API input event 완료와
같은 transaction에서 commit된다.
- 엔진 명령은 도메인 DB 쓰기, in-memory world flush, 예약 턴, 로그, 결과
저장과 inbox 완료를 하나의 transaction으로 commit한다.
## Turn Daemon and API Server Behavior (Outline)
- Turn daemon responsibilities: scheduling, turn resolution, state persistence
- API server responsibilities: query/command intake, validation, response shaping
- Concurrency model between daemon and API server
- Durable command channel: PostgreSQL `input_event`
- Client updates: SSE between API server and frontend where appropriate
## Durable Input Event Split
PostgreSQL is the source of truth for accepted input. Redis pub/sub remains a
best-effort notification/fan-out path and must not decide whether a gameplay
mutation was committed.
### Recommended Split
- PostgreSQL `input_event`:
- API mutation acceptance, idempotency, attempts and audit state.
- API server -> daemon mutation/control payloads and durable results.
- `FOR UPDATE SKIP LOCKED` claim plus worker lease for concurrent consumers.
- engine result and gameplay state commit in the same transaction.
- Redis pub/sub:
- Daemon -> API server: low-stakes live update signals (run started/ended).
- API server -> frontend: SSE fan-out triggered by pub/sub updates.
- Do not use pub/sub for data that must be replayed or audited.
### Operational Notes
- 각 profile은 별도 game DB schema/connection을 사용한다.
- HTTP `Idempotency-Key`(없으면 Fastify request ID)와 tRPC path를 합친 값이
API input event key다.
- 처리 중 lease가 만료된 engine event만 `PENDING`으로 회수한다.
- 완료 event 보존/정리 기간과 `FAILED` 재처리 운영 정책은 별도로 정해야 한다.
Detailed lifecycle and control flow are defined in
`docs/architecture/turn-daemon-lifecycle.md`.
## Authentication and Session Management (Draft)
Login uses Kakao OAuth as the primary identity provider because it leverages
Korean real-name verification and helps prevent multi-account abuse. The system
also supports local ID/password login for users who cannot use Kakao.
### Login Options
- Kakao login button (OAuth flow via Gateway).
- Local login with ID/password (managed by Gateway).
- Passkey is a possible future option; define later if required.
- Auto-login should be supported when an active session exists.
### Session and SSO-Like Behavior
- Gateway handles login and owns primary sessions in Redis.
- Game servers may run different branches; treat Gateway as a central SSO
authority that issues session tokens for each server+scenario profile.
- API servers validate tokens against Redis and accept sessions issued by
Gateway without re-authentication.
- Session tokens should be scoped by server+scenario profile to avoid cross-server leaks.
### Operational Notes
- Prefer HTTP-only secure cookies for session tokens where possible.
- Provide a logout flow that revokes tokens in Redis.
- Track last-login and session metadata for audit and abuse detection.
## Engine Runtime Flow (Draft)
### Turn Daemon Loop
- The turn daemon runs as a single-threaded loop.
- The daemon engine uses in-memory state as the primary working set.
- The daemon waits on two conditions during the event loop.
- Query/command requests from the external API server.
- The scheduled start time of the next turn.
- External requests are processed until the next turn start time is reached.
- If no requests arrive, the daemon waits until the next turn start time.
- When the next turn start time arrives, the daemon starts turn processing
immediately even if requests remain queued.
- While the daemon is resolving a turn, the API server queues incoming requests.
현재 구현은 control 명령과 registry의 모든 world mutation을 같은 DB queue로
읽어 턴 사이에 처리한다.
### Daemon Control Contract (Draft)
API server commands are inserted into PostgreSQL `input_event`; the daemon
stores status/results on the same row. An in-process queue remains available
for tests and internal signals.
```ts
export type RunReason = 'schedule' | 'manual' | 'poke';
export type DaemonCommand =
| { type: 'run'; reason: RunReason; targetTime?: string; budget?: TurnRunBudget }
| { type: 'pause'; reason?: string }
| { type: 'resume'; reason?: string }
| { type: 'getStatus'; requestId: string };
export type DaemonEvent =
| { type: 'status'; requestId?: string; status: TurnDaemonStatus }
| { type: 'runStarted'; at: string; reason: RunReason }
| { type: 'runCompleted'; at: string; result: TurnRunResult }
| { type: 'runFailed'; at: string; error: string };
```text
lease/fencing 확인
-> input_event claim
-> in-memory command 또는 calendar action
-> world dirty state와 side effect 수집
-> EngineStateManager transaction
-> world/general/nation/city/turn/log flush
-> input_event result/status 갱신
-> checkpoint 갱신
-> commit
-> realtime 알림
```
### API Server Flow
Transaction 실패 시 in-memory snapshot을 복원하고 event는 재시도 가능한 실패
상태로 남깁니다. Lease 소유권을 잃은 daemon은 flush를 확정하지 않습니다.
- The API server validates commands and records every tRPC mutation in
PostgreSQL.
- After a request is processed, the API server returns the result to clients.
- Read-only queries may access the DBMS directly.
- The API server may use SSE to stream live updates to the frontend.
예약 턴은 revision/CAS와 lease를 사용합니다. API의 편집과 daemon의 실행이
경합해도 오래된 revision이 새 queue를 덮어쓰지 않게 합니다.
### Queue and Rate Limits
## 월간 경계
- Engine-owned requests are delivered through the PostgreSQL inbox.
- Per-user pending limits and event retention remain operational follow-up
work; idempotency and exclusive claim are implemented.
Calendar handler는 turn time이 월 경계를 지날 때 scenario event table의
action을 순서대로 실행합니다. Core event, 수입, 국가 등급, NPC, 이민족,
도시 공급, 전쟁 수입, 특기, 유니크·유산, 베팅, 통일·연감 처리는
`app/game-engine/src/turn/monthly*.ts`, `calendarHandlers.ts`,
`unificationHandler.ts`, `yearbookHandler.ts`에서 조립됩니다.
### In-Memory and DBMS Flush
Action 순서, RNG 소비와 persistence 순서는 ref 호환 계약입니다. 새 handler는
event catalog, in-memory state, dirty marking, flush와 reload 검증까지
연결합니다.
- The daemon processes actions against in-memory state by default.
- DBMS writes are flushed in bulk after turn processing completes.
- Frequently changing "next-turn intent" data is stored separately.
- The API server persists this data in the DBMS.
- The daemon loads only this data when the next turn begins.
## 장애 경계
## Turn Daemon vs API Query Priority (Outline)
- PostgreSQL은 gameplay mutation과 daemon 소유권의 기준입니다.
- Redis pub/sub, SSE와 flush notification은 commit 이후 best-effort입니다.
- Worker timeout은 요청 실패로 반환하며 DB commit 여부를 별도로 확인합니다.
- API·daemon process 재시작은 `InputEvent`, lease, checkpoint와 operation
상태에서 이어집니다.
- 운영 process와 외부 Caddy 상태는 local build·mock E2E로 증명되지 않습니다.
- Expected priority order under load
- Rules for preemption or deferral
- Handling of write-heavy operations during turn resolution
## Build와 배포
## In-Memory Processing and DBMS Flush (Outline)
Gateway operation은 source commit, worktree, build artifact와 process를
연결합니다. `tools/build-scripts/build-server.mjs`는 profile resource 복사만
담당합니다.
- When in-memory state is authoritative
- Flush checkpoints and transactional boundaries
- Recovery strategy after crash during flush
## Testing and Observability (Outline)
- Metrics and logs required to validate scheduling and flush behavior
- Suggested test scenarios for concurrency and consistency
## Game Logic Testing (Draft)
### Deterministic Inputs
- RNG seed composition (hidden server seed, turn info, general info).
- Scenario selection and scenario data.
- Trigger set inputs: nation, general, and city state.
- Game time and tick schedule.
### Recommended Unit Test Flow
- Prepare a deterministic test fixture (mock DB or in-memory state snapshot).
- Execute game logic unit tests with fixed inputs and seeds.
- Compare expected outputs against the pre-flush change set that would be
written to the DBMS.
### Notes
- Deterministic RNG makes output comparison stable and repeatable.
- Prefer snapshotting inputs/outputs so regressions are easy to track.
외부 공개 경로는 `/gateway/`, `/che/`, `/hwe/`입니다. frontend base,
tRPC, SSE, upload와 direct navigation은 해당 prefix를 유지합니다.
`/image/*`는 Caddy의 별도 파일 시스템 경로입니다.
+7 -7
View File
@@ -38,8 +38,8 @@
순서를 소유하도록 작성해 주세요. 같은 key를 여러 확장이 설정한다면
`extends`의 뒤쪽 확장이 우선하며, 시나리오 본문이 항상 최종 우선권을 가집니다.
`default.json`의 능력치·아이콘 기본값은 확장 합성이 끝난 뒤 기존과 같은
파서 단계에서 적용됩니다. `parseScenarioDefinition()` 이미 합성된 객체를
`default.json`의 능력치·아이콘 기본값은 확장 합성이 끝난 뒤
파서에서 적용됩니다. `parseScenarioDefinition()`은 합성된 객체를
정규화하는 함수이므로 파일을 직접 읽는 코드에서는 사용하지 말아 주세요.
실제 설치는 `loadScenarioDefinitionById()`, Git commit 미리보기는
`composeScenarioResource()`를 거쳐 같은 합성 규칙을 사용합니다.
@@ -55,8 +55,8 @@
| `extensions/initial-events/expanded.json` | 전 도시 교역 초기화 이벤트 |
| `extensions/items/buyable-war-special-uniques.json` | 구매 가능한 전특과 해당 유니크 아이템 풀 |
기존 시나리오 70개가 확장을 사용합니다. 특히 구매 가능한 전특/유니크 표를
가지던 10개 시나리오는 같은 item 확장 하나를 참조합니다.
시나리오 80개 중 70개가 확장을 사용합니다. 구매 가능한 전특·유니크 표를
사용하는 10개 시나리오는 같은 item 확장 참조합니다.
## 검증
@@ -70,6 +70,6 @@ pnpm --filter @sammo-ts/gateway-api test scenarioCatalog.test.ts
```
`validate:resources`는 하위 `extensions/**/*.json`도 재귀적으로 검사합니다.
`scenarioLoader.test.ts` 추적 중인 시나리오 80개를 실제 합성 로더로 모두
읽습니다. 기존 시나리오를 확장으로 이관할 때에는 변경 전 JSON과 합성 결과를
전수 구조 비교하여 이벤트 배열 순서와 `const` 값이 같은지 확인해 주세요.
`scenarioLoader.test.ts`는 시나리오 80개를 합성 로더로 모두 읽습니다. 합성
구조를 바꿀 때에는 기준 JSON과 결과를 전수 구조 비교하여 이벤트 배열 순서와
`const` 값이 같은지 확인해 주세요.
-152
View File
@@ -1,152 +0,0 @@
# Architecture TODOs
This list tracks optional extensions and follow-up items for documentation.
Move items into the main docs once they are finalized.
## Runtime and Operations
- [AI suggestion] Make profile turn execution a durable single-writer contract:
add a DB-backed lease with fencing epoch/world version, and reject stale
daemon commits after a restart or split-brain.
- [AI suggestion] Bound very large world flush batches and add a transactional
realtime outbox. Entity deltas, logs, reserved turns, checkpoint and input
results are atomic now, but realtime publication remains post-commit
best-effort.
- [AI suggestion] Define `input_event` retention, failed-event inspection/DLQ,
manual replay authorization and per-user pending limits. Durable request IDs,
idempotent results, ordering, exclusive claim and expired-lease recovery are
implemented.
- [AI suggestion] Add ownership authorization to all turn reservation
procedures (`auth user -> owned general -> current officer role -> nation`)
and add revision/optimistic concurrency to reservation queue updates.
- [AI suggestion] Implement full turn engine pipeline parity with legacy: time gate, distributed lock, catch-up monthly loop, partial progress persistence, and post-turn maintenance (traffic/statistics/auction/tournament).
- [AI suggestion] Implement command continuation/alternate flow (LastTurn/term stack/pre/post requirements/next_execute) and integrate into reserved turn processing.
- [AI suggestion] Integrate trigger preprocessing + attempt/execute phases into turn resolution (parity with legacy trigger composition).
- [AI suggestion] Add AI/autorun and inactivity (killturn/block) handling with NPC takeover/deletion policies.
- [AI suggestion] Complete the event action registry beyond the implemented
`pre_month`/`month` dispatcher (`ProcessIncome`, `NoticeToHistoryLog`,
`NewYear`, `ResetOfficerLock`). Add initial and non-month targets plus the
remaining NPC, invader, disaster, supply, nation-level, betting, and
inheritance actions with legacy-compatible deterministic RNG.
- [AI suggestion] Add monthly economy/city/nation updates and hook them into the turn calendar handler.
- [AI suggestion] Implement diplomacy/state transitions and monthly/command-based updates beyond read-only maps.
- [AI suggestion] Integrate war/battle pipeline into turn processing (troop movement/war resolution hooks, not just isolated sim jobs).
- [AI suggestion] Expand turn command catalog beyond the current subset (general/nation commands).
- [AI suggestion] Replace growing switch-based command routing/serialization with a registry (command schema + handler map + validator) to reduce manual case additions.
- [AI suggestion] Apply install settings (`join_mode`, `npcmode`, `show_img_level`, `tournament_trig`) to runtime rules/command constraints and UI behavior, beyond just storing them in world state.
- [AI suggestion] Implement full auction finalization logic in the daemon (resource transfers, unique item grants, and log writes) once the auction schema is stabilized.
## Frontend
- [AI suggestion] Build a legacy-compatible `classic` theme from semantic
design tokens and shared UI primitives, verify it with Chromium
screenshot/geometry/computed-style tests, then add a separately selectable
`modern` theme without changing page behavior.
- [AI suggestion] Provide one profile-aware public runtime configuration for
web/API/event/asset base URLs so `/gateway`, `/che`, and `/hwe` work for SPA
navigation, direct refresh, tRPC, SSE, and externally owned `/image/*`.
- [AI suggestion] Maintain a game frontend SPA implementation plan and keep it aligned with legacy screen inventory (`docs/architecture/game-frontend-spa-plan.md`).
- [AI suggestion] Define gateway login handoff + profile selection flow for the game frontend (token delivery, auto-login, cookie vs localStorage policy).
- [AI suggestion] Implement Public 화면: 캐싱된 지도/중원정세/세력일람 + 제한된 장수일람 API/뷰.
- [AI suggestion] Define main screen SSE contract + 실시간 동기화 토글 연동 (지도/명령/도시/국가/장수/메시지/동향/기록).
- [AI suggestion] Port remaining legacy main UI components into `app/game-frontend` (MessagePanel 이관 완료; 나머지 패널 추적).
- [AI suggestion] Provide map city name/position data for MapViewer (API or scenario export) and replace placeholder layout.
- [AI suggestion] Implement join/빙의 UI and post-creation refresh flow.
- [AI suggestion] Build and maintain a legacy-to-SPA route mapping table with data requirements.
- [AI suggestion] Implement command reservation UI (일반/국가 예턴) and connect `CommandSelectForm` to `turns.reserved` APIs with validation + queue preview.
- [AI suggestion] Wire `realtimeEnabled` to an SSE or polling channel and update main dashboard data buckets (map/lobby/messages/commands).
- [AI suggestion] Finalize static asset and web base URLs (`VITE_GAME_WEB_URL`, `VITE_GAME_ASSET_URL`) and document deployment mapping for legacy images.
- [AI suggestion] Expand Join UI to cover inherit options (특기/도시/턴타임/보너스 스탯) using `join.getConfig` and `join.createGeneral` inputs.
- [x] Extend MessagePanel to support private/diplomacy targets and surface sender/receiver metadata from message payloads.
- [AI suggestion] Port legacy TipTap-based editors (국가 방침/임관 권유) into game-frontend and reuse the new board image upload policy.
## Runtime and Operations (Lower Priority)
- Document current turn daemon scheduling details and preemption rules (based on TurnDaemonLifecycle).
- Document in-memory state lifecycle and DB flush checkpoints (current InMemoryTurnWorld + databaseHooks flow).
- Document existing status/health endpoint requirements for ops and the current daemon loop behavior.
- Document tick budget settings (wall time, max generals, catch-up cap) and partial progress persistence.
- Document admin controls (pause/resume/manual run) and how they interact with lock/state.
- [AI suggestion] Define gateway admin build/daemon control approach (direct orchestration vs supervisor like systemd/pm2), security model, audit logging, and safe rollback/stop/start workflows.
- [AI suggestion] Specify single-host gateway orchestration: boot reconciliation from DB (완료/예약/가동중/정지됨/비활성화), desired-state mapping, and pm2-managed process lifecycles for api/daemon.
- Turn daemon vs API server priority policy under load
- Recovery behavior after partial flush or crash
- Observability: metrics, logs, and alerts for turn processing
- [AI suggestion] Define a stable in-memory AI state contract (snapshot + delta invalidation rules) aligned with `GeneralAI` inputs.
## Game Logic and Testing
- [AI suggestion] Audit every test inherited at the 2026-07-25 baseline before
using it as correctness evidence. Record the protected contract and
independent oracle, replace truthiness/smoke-only assertions, add
negative/boundary/side-effect coverage, and verify that an intentional
behavior mutation makes the test fail.
- [AI suggestion] Add a PHP↔TypeScript battle differential harness that
compares the serialized seed, ordered RNG calls, trigger
attempt/execute trace, phase-by-phase numeric outcomes, and final
general/city/nation/troop numeric snapshot. Treat log wording as a permitted
difference when event meaning, displayed values, and visibility are kept.
- [AI suggestion] Restore the legacy command RNG seed domains
(`generalCommand`/`nationCommand` plus raw command class) before treating
fixed-seed command or battle results as compatible.
- [AI suggestion] Define an explicit ruleset trigger manifest and fail profile
startup when a unit/item/trait references an unregistered trigger. Compose
personality, domestic/war specialty, item, inheritance, nation, and unit
triggers in the live turn-daemon battle path.
- [AI suggestion] Extend unit-set init/phase trigger specs from `string[]` to
a typed string-or-`{ key, args }` union before importing a legacy ruleset
that uses parameterized `buildWarUnitTriggerClass` arguments. Preserve
argument order and include it in differential trigger traces.
- Input snapshot format (seed, scenario, trigger inputs, game time)
- Deterministic RNG test harness guidelines
- Output comparison rules (sorting, tolerances, diff granularity)
- Unit test vs simulation test split and responsibilities
- [AI suggestion] Add a test helper that runs commands through parseArgs + constraint evaluation with a deterministic RNG (e.g., LiteHashDRBG) to mirror legacy execution paths.
- [AI suggestion] Expand command tests to assert legacy side effects (exp/ded/stat-exp, gold/rice deltas, betray/inventory/troop cleanup) for key commands (e.g., 하야/강행/농지개간).
- [AI suggestion] Replace smoke/placeholder tests with concrete assertions for success/failure outcomes (e.g., 등용, NPC능동 invalid args, 출병 troop creation).
- [AI suggestion] Document che*출병 parity gaps vs legacy (city state/term=43, fallback to che*이동 when friendly, nation.war/AllowWar check, post-war static events/unique-item lottery, missing route data when map/diplomacy not provided).
- [AI suggestion] Verify that "이호경식" followed by "출병" is correctly blocked with the new reserved-turn overlay (diplomacy state sync).
- [AI suggestion] Migrate the legacy `storage` namespaces used to reserve
unequipped unique items. Survey, monthly nation-level, and general-command
lotteries now count equipped items plus active/finalizing unique auctions,
but core2026 has no equivalent persistent storage namespace to include yet.
## Trigger System
- Example trigger sets per scenario or rule pack
- [AI suggestion] Define item/equipment effect modeling for war triggers (equip vs carry, stacking, consumable/breakable) and hook into WarActionPipeline/trigger registry.
## Data and Profiles (Lower Priority)
- [AI suggestion] Resolve the shipped `ludo_rathowm` unit set's
`defaultCrewTypeId=1100` mismatch with its `217xxx` crew IDs, then enable
catalog validation for default and castle crew IDs across every profile.
- [AI suggestion] Split gateway orchestration into immutable `Release`,
versioned `Ruleset`, `ProfileInstance`, `Deployment`, and first-class
`AdminJob` records. Store artifact/API/resource digests and an auditable job
state machine instead of profile `meta.adminActions` arrays.
- [AI suggestion] Run the artifact built in each profile's selected worktree
(or preferably an immutable image digest), include required workers and
frontend/resources in the release manifest, and verify readiness before
switching traffic.
- [AI suggestion] Give each profile instance an immutable ID, dedicated DB
schema/URL, and public route. Do not derive DB isolation from the broad
server profile (`che`, `hwe`) when multiple scenarios/releases can coexist.
- [AI suggestion] Publish and negotiate gateway/game API contract and
capability versions so profiles running different releases can be accepted,
degraded, or blocked explicitly by the gateway.
- Profile selection workflow and deployment mapping
- "Next-turn intent" (예턴) data schema and lifecycle
## Legacy Engine Docs (Lower Priority)
- [AI suggestion] Expand monthly pipeline details (`preUpdateMonthly`, `postUpdateMonthly`, `turnDate`, `checkStatistic`) with concrete side effects and tables touched.
- [AI suggestion] Document `event` table schema and the static event handler map (`GameConst::$staticEventHandlers`) with command hook examples.
- [AI suggestion] Add per-command effect summaries (inputs, resource deltas, logs).
- [AI suggestion] Document per-command `Constraint` env payload keys and lifecycle.
- [AI suggestion] Document `ConquerCity()` resolution paths (nation collapse, officer handling, reward/penalty rules).
- [AI suggestion] Document auction scheduling (`registerAuction` call sites) and lifecycle timing rules.
- [AI suggestion] Document scenario-specific unit/map overrides and per-map city deltas.
- [AI suggestion] Document `MessageType` values and message table schema used by diplomacy/mailbox flows.
- [AI suggestion] Document `PenaltyKey` effects and the `GeneralBase` / `LazyVarAndAuxUpdater` state conventions.
- [AI suggestion] Document personality/special selection RNG thresholds and scenario overrides.
-338
View File
@@ -1,338 +0,0 @@
# Turn Daemon Lifecycle
This document defines the lifecycle of the turn daemon for the rewrite when a
single daemon instance is responsible for turn resolution and all triggers come
from the API server.
## Assumptions
- One daemon process per server+scenario profile.
- API server is the only ingress for user/admin requests.
- The daemon owns world-state mutations; the API server mutates reserved turns
and messages inside the common API input-event transaction.
## Current Implementation Status
- The daemon loop handles scheduled runs plus PostgreSQL inbox control commands
(run/pause/resume/shutdown/getStatus).
- API mutation requests (troopJoin, vacation, etc.) are drained and handled by the daemon loop between turns.
- `getStatus` and command results are stored durably on their input-event row.
- API server currently writes reserved turns and messages directly to the DB.
## Responsibilities
- Maintain authoritative in-memory state.
- Execute turn resolution when scheduled.
- Flush state changes and checkpoints to the DBMS.
- Expose status for ops and admin tooling.
## Trigger Sources
- Scheduled tick: next turn time is reached.
- API poke: user/admin request asks the daemon to run now.
- Admin control: pause/resume or manual catch-up.
## State Model
```
Idle -> Running -> Flushing -> Idle
\-> Paused (admin)
\-> Stopping (shutdown)
```
- `running` flag prevents re-entrant execution when multiple triggers arrive.
- `pendingReason` tracks why a run was requested (schedule, manual, poke).
### Engine-owned state boundary
`EngineStateManager` is the worker-local unit-of-work coordinator. It does not
replace the daemon process or the PostgreSQL inbox transport:
```text
game-api
-> PostgreSQL input_event
-> game-engine worker
-> EngineStateManager transaction
- InMemoryTurnWorld
- InMemoryReservedTurnStore
- runtime calculation caches
-> databaseHooks transaction/flush
```
Each participant provides a detached snapshot and a restore operation. The
manager captures all participants before a command handler or scheduled turn
calculation. A calculation exception restores the world entities, checkpoint,
dirty journals, pending logs/messages/events, reserved-turn queues and
calculation caches together. Successful calculations advance a monotonic
worker-local revision.
The runtime exposes `stateManager.inspect()` for deterministic diagnostics and
tests. Inspection results are detached copies; modifying one cannot mutate the
live world. Tests may also capture a reusable savepoint and restore it between
cases. A savepoint is manager-specific and cannot be restored into another
runtime.
This is intentionally an in-process boundary. PostgreSQL remains the durable
source of truth across worker restarts, and Redis remains best-effort realtime
fan-out. No engine snapshot is sent through the API transport.
## Main Loop Sketch
The daemon interleaves API request handling between scheduled turn executions.
API requests are drained until the next turn time is reached; once the turn
starts, incoming requests are queued and processed after the run.
The current implementation follows this interleaving model.
```ts
while (!stopping) {
const signal = await waitForNextSignal(nextTurnTime, wakeSignal);
if (signal.type === 'pause') {
paused = true;
}
if (signal.type === 'resume') {
paused = false;
}
if (paused || running) {
continue;
}
await drainApiRequestsUntil(nextTurnTime);
if (now() < nextTurnTime && signal.type !== 'run') {
continue;
}
running = true;
try {
await runUntil(now(), budget);
await flushChanges();
publishTurnEvents();
} finally {
running = false;
}
}
```
- `drainApiRequestsUntil()` processes queued user/admin requests and applies
them to in-memory state until the next scheduled turn time is reached.
- During `running`, new API requests are enqueued and handled after the run.
## Run Budget and Checkpoints
Replace PHP `max_execution_time` with explicit limits to allow partial progress:
- `budgetMs`: max wall-clock time per run.
- `maxGenerals`: max generals processed per run.
- `catchUpCap`: max turns (or months) processed in one run.
When a limit is hit, persist a checkpoint so the next run resumes safely.
Minimum checkpoint data:
- `game_env.turntime` (last fully processed general turn time)
- optional cursor (last processed general id) if you batch within a turntime
- last known year/month if monthly transitions are mid-flight
## API Server Interaction
- API server inserts mutations and pokes into the PostgreSQL inbox.
- Read-only queries can read from DBMS or a read model.
- During `running`, incoming requests are queued and processed after the run.
## Status and Admin Controls
Expose a status endpoint to replace `proc.php` output:
- `running`, `paused`, `lastRunAt`, `lastDurationMs`
- `lastTurnTime`, `nextTurnTime`
- `pendingReason`, `queueDepth`
Admin controls should toggle `paused`, trigger manual run, and request catch-up.
### Suggested Status Endpoint (Draft)
`GET /admin/turn-daemon/status`
```json
{
"profile": "che:default",
"state": "idle",
"running": false,
"paused": false,
"lastRunAt": "2026-01-01T12:00:00Z",
"lastDurationMs": 842,
"lastTurnTime": "2026-01-01 12:00:00",
"nextTurnTime": "2026-01-01 12:10:00",
"pendingReason": "schedule",
"queueDepth": 3,
"checkpoint": {
"turnTime": "2026-01-01 12:00:00",
"generalId": 1201,
"year": 12,
"month": 3
}
}
```
### Suggested Admin Endpoints (Draft)
`POST /admin/turn-daemon/run`
```json
{
"reason": "manual",
"targetTime": "2026-01-01 12:10:00",
"budgetMs": 2500,
"catchUpCap": 3
}
```
`POST /admin/turn-daemon/pause`
```json
{
"reason": "maintenance"
}
```
`POST /admin/turn-daemon/resume`
```json
{
"reason": "maintenance complete"
}
```
### Daemon Control Contract (Draft)
API server to daemon control payloads (PostgreSQL inbox or in-process test
channel):
```ts
export type RunReason = 'schedule' | 'manual' | 'poke';
export type DaemonCommand =
| { type: 'run'; reason: RunReason; targetTime?: string; budget?: TurnRunBudget }
| { type: 'pause'; reason?: string }
| { type: 'resume'; reason?: string }
| { type: 'getStatus'; requestId: string };
export type DaemonEvent =
| { type: 'status'; requestId?: string; status: TurnDaemonStatus }
| { type: 'runStarted'; at: string; reason: RunReason }
| { type: 'runCompleted'; at: string; result: TurnRunResult }
| { type: 'runFailed'; at: string; error: string };
```
## Recovery and Shutdown
- On startup, load `game_env.turntime` and catch up to `now`.
- Claim pending input events with `FOR UPDATE SKIP LOCKED`. A worker ID and
expiry timestamp prevent another live consumer from claiming the same event;
only expired processing leases return to `PENDING`.
- A handler exception or transaction failure pauses the daemon without
acknowledging the event. Restart/recovery reloads world state before retry.
- On shutdown, stop accepting new triggers, finish the current run, flush, then
exit cleanly.
## Input Event Commit Boundary
For engine-owned mutations the lifecycle opens one database-owned unit of work:
```text
claim input_event
-> execute command against in-memory world
-> specialized domain DB writes (auction/tournament)
-> persist world/reserved turns/logs
-> store command result and mark input_event SUCCEEDED
-> commit
-> acknowledge in-memory dirty sets
-> respond
```
If any step before commit fails, PostgreSQL rolls back and dirty sets are not
cleared. Realtime publication happens after commit and cannot roll gameplay
state back.
There are two distinct failure boundaries:
- If command/turn calculation throws, `EngineStateManager` restores the
pre-calculation in-memory snapshot, so a partially mutated handler is not
observable to the next calculation.
- If calculation succeeds but PostgreSQL persistence fails, the calculated
dirty state remains in memory and the daemon pauses, preserving the existing
restart/reload recovery contract. The input event is not acknowledged.
State-manager restore does not attempt to compensate arbitrary external side
effects. New engine components that own mutable in-memory state must register a
participant; new database writes must remain inside `databaseHooks` or the
command's supplied transaction client.
Known pre-existing exceptions are not covered by this in-memory transaction:
`yearbookHandler` performs an idempotent PostgreSQL upsert,
`tournamentAutoStart` writes Redis tournament keys, and `unificationHandler`
starts archive/inheritance writes outside `databaseHooks`. If a later calendar
handler throws, those external effects are not rolled back even though the
registered in-memory participants are restored. Therefore this boundary is not
a distributed transaction. Moving these three effects into the durable flush
outbox is separate follow-up work and is required before claiming atomic
rollback of an entire monthly boundary.
## Testing with a Controlled Clock
- Turn daemon tests should use a controllable `Clock` implementation (예: `ManualClock`) to
advance time deterministically without relying on wall-clock time.
- 기준 시간은 DB에 저장된 `game_env.turntime`을 우선으로 삼고, 테스트에서도 동일한 기준을
사용해 스케줄 계산과 체크포인트 동작을 검증한다.
- 상태 관련 단위 테스트는 `EngineStateManager.transaction()` 실패 전후의
inspection을 비교해 entity 값뿐 아니라 dirty journal과 pending output도
원복됐는지 확인한다.
- persistence failure test는 계산 실패 test와 분리한다. 전자는 dirty state
유지와 미acknowledge를, 후자는 완전한 in-memory restore를 검증한다.
## TypeScript Sketch (Draft)
```ts
export type TurnDaemonState = 'idle' | 'running' | 'flushing' | 'paused' | 'stopping';
export interface TurnRunBudget {
budgetMs: number;
maxGenerals: number;
catchUpCap: number;
}
export interface TurnCheckpoint {
turnTime: string;
generalId?: number;
year: number;
month: number;
}
export interface TurnRunResult {
lastTurnTime: string;
processedGenerals: number;
processedTurns: number;
durationMs: number;
partial: boolean;
checkpoint?: TurnCheckpoint;
}
export interface TurnDaemonStatus {
state: TurnDaemonState;
running: boolean;
paused: boolean;
lastRunAt?: string;
lastDurationMs?: number;
lastTurnTime?: string;
nextTurnTime?: string;
pendingReason?: RunReason;
queueDepth: number;
checkpoint?: TurnCheckpoint;
}
export interface TurnRunner {
getStatus(): Promise<TurnDaemonStatus>;
requestRun(reason: RunReason, targetTime?: string, budget?: TurnRunBudget): Promise<void>;
pause(reason?: string): Promise<void>;
resume(reason?: string): Promise<void>;
}
```
@@ -1,244 +1,129 @@
# Turn command state differential testing
# ref ↔ core2026 차등 검증
## Scope
## 목적
This harness compares observable state changes produced by:
차등 검증은 같은 입력과 seed에서 ref와 core2026의 판정, RNG 소비, 출력,
상태 mutation과 persistence 순서를 비교합니다. 명령 이름이나 최종 성공 여부만
같은 것은 호환성 근거가 아닙니다.
- general reserved commands;
- nation reserved commands;
- live `che_출병`, including persisted battle aftermath.
## 검증 계층
It complements the battle simulator differential suite. The simulator suite
compares phase order, RNG and battle numbers. This harness compares the command
boundary, logs, queues, diplomacy and final database state.
| 계층 | 위치 | 보장 범위 |
| ----------------- | ----------------------------------------------- | ------------------------------------------ |
| 정적 catalog 비교 | `tools/compare-command-*.mjs` | command key, constraint, log 계약 |
| logic unit | `packages/logic/test` | 계산, constraint, trigger, fixed-seed 결과 |
| engine unit | `app/game-engine/test` | handler 순서, in-memory state, lifecycle |
| DB integration | `tools/integration-tests`, app integration test | Prisma transaction, lease, queue, reload |
| ref 차등 | `tools/integration-tests/src/legacy*` | 동일 fixture의 ref/core 결과 |
| live Chromium | `tools/frontend-legacy-parity` | 실제 화면 geometry와 interaction |
## Execution flow
각 계층은 다른 계층을 대체하지 않습니다. Unit test는 PostgreSQL commit을
증명하지 않고, API mock은 인증 transport를 증명하지 않으며, screenshot
한 장은 hover·focus·error 흐름을 증명하지 않습니다.
```text
canonical case
├─ ref ng_compare command runner
│ ├─ clone root/HWE MariaDB
│ ├─ execute legacy GeneralCommand or NationCommand
│ └─ before/after snapshot + command RNG trace
└─ core2026 execution boundary
├─ construct the in-memory turn world from the prepared ref snapshot
├─ execute the real reserved-turn handler
└─ before/after snapshot + command RNG trace
## Fixture 계약
ref delta ─┐
├─ exact semantic path comparison
core delta ┘
```
Fixture에는 다음 값이 명시되어야 합니다.
The comparison uses entity IDs rather than physical row order. Numeric leaves
are compared as `after - before`, so a fixture may use different harmless
starting balances while still requiring the same effect. Insertions, deletions
and non-numeric changes retain explicit before/after values.
- ref 기준 commit과 core2026 기준 commit
- profile, scenario, game time과 turn term
- user, general, nation, city, diplomacy, troop과 예약 턴
- command key와 원시 args
- seed와 RNG 초기 상태
- 성공·실패 경로에 필요한 resource
- 비교 대상 state field, log, message와 side effect
Logs and messages are sequence-sensitive and are therefore paired by observed
order rather than database primary key. Their physical IDs can be ignored
without losing ordering checks. Legacy `*ID` command argument keys are
normalized to the core `*Id` spelling before command identity comparison.
ref MariaDB fixture는 전용 schema를 사용합니다. core PostgreSQL과 Redis도
worktree 전용 instance를 사용합니다. 다른 실행이 공유 RNG, queue, table,
Redis key를 변경할 수 있는 환경에서 결과를 비교하지 않습니다.
## Components
## 실행 경로
- Ref `ng_compare`
- `hwe/compare/turn_state_snapshot.php`: read-only canonical projection.
- `hwe/compare/turn_command_trace.php`: guarded CLI action runner.
- Workspace reference stack
- `scripts/run-turn-differential-case.sh`: clones both MariaDB databases,
injects temporary DB configuration, runs one case and deletes only
validated `sammo_td_*` databases.
- Core integration tools
- `canonical.ts`: shared snapshot and trace contracts.
- `databaseSnapshot.ts`: PostgreSQL projection.
- `coreCommandTrace.ts`: real in-memory reserved-turn execution and
canonical projection.
- `trace.ts`: before/execute/after capture boundary.
- `compare.ts`: exact snapshot and delta comparison.
- `turnCommandGeneralMatrix.integration.test.ts`: 54 successful general
command paths. Together with the live sortie fixture, this exercises a
completed success path for every one of the 55 general command keys.
The matrix includes the four-call `전투태세` path, lifecycle and
appointment commands, troop assembly, rebellion and all three founding
variants.
- `turnCommandNationMatrix.integration.test.ts`: all 35 reserved nation
command paths, including the generated-general state and 36-call RNG
trace of `의병모집`.
- `turnCommandCoreReference.integration.test.ts`: declaration and live
sortie fixtures.
The ref runner refuses mutation unless `TURN_DIFFERENTIAL_ENABLED=1` is present.
The wrapper injects it only into the disposable tool container. Direct
execution against the reference database is not a supported workflow.
Two committed cases exercise the guarded runner end to end:
- `nation-declaration.json`: chief nation command, directed diplomacy
`state/term` changes and national messages.
- `live-sortie-conquest.json`: real `che_출병 -> processWar`, city capture,
defeated-general neutralization and last-city nation collapse.
Each case may include a structured `setup` object for `world`, `nations`,
`cities`, `generals`, `troops` and `diplomacy`. The runner accepts only
explicitly mapped fields; it does not accept SQL. Founding fixtures may pin
`initYear`/`initMonth` and an ordered random-founding city candidate set so
both maps expose the same RNG domain. Setup and command mutation happen only
inside the cloned `sammo_td_*` databases.
## Case request
```json
{
"kind": "general",
"actorGeneralId": 101,
"action": "che_출병",
"args": { "destCityID": 12 },
"observe": {
"generalIds": [101, 201],
"cityIds": [11, 12],
"nationIds": [1, 2],
"logAfterId": 0,
"messageAfterId": 0
}
}
```
Legacy argument spelling is retained at the ref boundary (`destCityID`,
`destNationID`). The core execution request uses the core spelling
(`destCityId`, `destNationId`), while the trace's semantic entity IDs remain
the same.
Run a ref case:
정적 명령 계약:
```sh
cd docker_compose_files/reference
./scripts/run-turn-differential-case.sh \
fixtures/turn-differential/nation-declaration.json \
> /tmp/ref-trace.json
pnpm check:legacy:general
pnpm check:legacy:nation
```
When the integration tests need to exercise a feature-worktree copy of the
case wrapper, set `TURN_DIFFERENTIAL_CASE_SCRIPT` to that shell script. This is
intentionally distinct from `TURN_DIFFERENTIAL_RUNNER_SCRIPT`, which the shell
wrapper forwards to select the PHP comparison runner inside the reference
container.
기능 worktree의 case wrapper를 검증할 때는
`TURN_DIFFERENTIAL_CASE_SCRIPT`로 해당 shell script를 지정합니다.
이 값은 wrapper가 ref container 내부 PHP 계측기를 선택할 때 전달하는
`TURN_DIFFERENTIAL_RUNNER_SCRIPT`와 구분합니다.
Capture the core side by wrapping the real reserved-turn or daemon execution
with `captureCoreDatabaseTurnTrace()`. Save the returned JSON, then compare:
즉시 외교 3종의 core log는 명령 파일이 아니라 공통
`packages/logic/src/diplomacy/instantResponse.ts`에서 생성됩니다.
정적 log 비교의 이 예외는 명령별 allowlist로 제한하며,
`instantDiplomacyReference.integration.test.ts`에서 실제 ref/core 결과를
별도로 비교합니다.
기본 integration:
```sh
TURN_REFERENCE_TRACE=/tmp/ref-trace.json \
TURN_CORE_TRACE=/tmp/core-trace.json \
pnpm --filter @sammo-ts/integration-tests test:integration \
turnTraceFiles.integration.test.ts
pnpm test:integration
```
## Canonical coverage
PostgreSQL·Redis 조건부 경계:
Snapshots currently include:
```sh
pnpm test:integration:conditional
```
- world year/month, tick term, last turn time and unification state;
- selected general numeric state, location, nation, troop, equipment metadata,
last turn and lifecycle counters;
- selected city ownership and all domestic/defence values;
- selected nation resources, type, capital, technology, cached power/counts;
- directed diplomacy state, term and death flag;
- general and nation reserved queues;
- action/history/battle logs;
- legacy messages and log/message watermarks.
개별 suite와 test name은 package script 뒤에 전달합니다.
Core2026 has no physical legacy `message` table, so its message projection is
empty. Message-equivalent effects must currently be compared through core log
effects or a command-specific projection. The saved-trace comparison explicitly
ignores the `messages` subtree for this reason; this is a documented schema
boundary, not a claim that message delivery is equal.
```sh
pnpm --filter @sammo-ts/game-engine test inputEventAtomicity.test.ts
pnpm --filter @sammo-ts/integration-tests test:integration
```
For live `che_출병`, use both suites:
실제 script 인자는 해당 package의 `package.json`을 확인해 주세요.
1. `battleDifferential.test.ts` for internal war RNG/event/numeric parity.
2. Turn command traces for route choice RNG, costs, general/city/nation changes,
queues, logs, conquest and nation deletion.
## 비교 snapshot
## Test trust boundary
Canonical 비교값은 의미 단위로 정규화합니다.
Comparator unit tests prove path identity, order independence, numeric delta
handling and deletion detection. Adapter integration tests prove that both
actual databases can produce the canonical form. They do not by themselves
claim that all 55 general and 38 nation commands match.
- general: 위치, 소속, 자원, 병력, 능력치, 경험·공헌, penalty, aux, turn time
- nation: 자원, level, 수도, 외교, 정책, aux
- city: 소유권, 인구·내정·방어, conflict, supply와 인접 상태
- queue: command, args, term, next available, revision
- output: 개인·국가·역사 log, message, 공개 문구와 error
- execution: RNG trace, handler 순서, input event와 checkpoint
Compatibility is established per case only when:
DB auto ID, 생성 시각처럼 의미 없는 차이는 comparator에서 이름과 이유를
명시합니다. 의미 field를 ignore하거나 숫자 허용 범위를 넓혀 mismatch를
숨기지 않습니다.
1. both engines executed the requested action rather than a fallback;
2. RNG operations and results match;
3. the semantic delta comparison is empty;
4. live sortie also passes the battle trace comparison;
5. any ignored path is documented in the case evidence.
## RNG
As of 2026-07-26, 54 general matrix cases, 35 reserved nation matrix cases,
declaration and live sortie pass this boundary. Live sortie is the remaining
general command key and covers battle entry, conquest, defeated-general
neutralization and last-city nation collapse. Thus all 55 general command keys
have a completed success-path comparison. The three instant diplomatic nation
responses pass a separate reference trace and core resolver/API boundary.
Gameplay RNG는 `LiteHashDRBG`, `RNG`, `RandUtil` 경로를 추적합니다.
후보가 하나인 선택, 실패 분기, fallback도 ref가 소비한 호출 수와 순서를
보존합니다. 보조 무작위성이 필요하면 main stream과 분리된 seeded substream을
사용하고 main stream의 소비량이 변하지 않는지 확인합니다.
Nine general in-action failure cases now also pass the common differential:
five domestic critical failures and four sabotage failures. They compare the
full command RNG trace, semantic state delta and the exact action-log body.
Seven common full-constraint cases cover neutral status, wandering nation,
city ownership, supply, gold, rice and trust-cap rejection. Five sabotage
constraint cases add the occupied-city target, neutral target, non-aggression
status and insufficient sabotage gold or rice. Both engines replace the denied
command with rest, consume the same RNG and produce the same semantic delta.
Eight sabotage clamp cases cover probability zero and 0.5 for
fire attack, agitation, destruction and seizure. The zero boundary skips the
success RNG primitive, while exactly 0.5 consumes `nextBits(1)`; the complete
RNG trace and semantic delta match. These cases also fixed fire-attack city
state persistence and agitation front/trust persistence differences.
Five sabotage value-boundary cases cover zero floors for fire-attack,
agitation and destruction city values, the supplied-nation resource cap for
seizure, and the legacy final state of unsupplied seizure. They also remove
destruction's unintended front recalculation and preserve the legacy
unsupplied-seizure overwrite that leaves city resources unchanged.
Three sabotage injury-boundary cases cover per-defender rolls, the injury cap
of 80, integer persistence after multiplying crew/train/atmosphere by 0.98,
and the target-general injury log for fire attack, agitation and destruction.
They restore the legacy Korean particle in the log, add the two missing logs,
and round rather than floor the integer fields.
Five general alternative cases cover disband and random-appointment fallback
to talent search, both random-founding alternatives, and sortie fallback to
movement. The alternative command reuses the original requested-action RNG
instance and current consumption position. Six pre-required-turn cases cover
battle-preparation terms 1 through 3 plus the first intermediate turn of both
trait resets and retirement. They compare the exact intermediate `lastTurn`,
progress log, zero command-RNG consumption and semantic state delta.
Three post-required cooldown cases project the legacy `next_execute` KV and
core general meta into the same world-level cooldown record. They cover the
stored `current + 60 - preReq` value, rejection one turn before availability,
and successful execution exactly at the boundary.
Four missing-target cases cover nonexistent general IDs for gift and
employment plus nonexistent city IDs for spying and movement. Both engines
reject the requested command, execute rest without command RNG, and produce
the same semantic state delta.
Thirteen resource argument and balance cases cover 100-unit rounding and
minimum/maximum clamps for gift, donation, and rice trade; donation against
available and minimum resources; and gift reserve and self-target rejection.
They compare normalized last-turn arguments, RNG, fallback, and semantic state
deltas.
Eleven nation resource cases cover 100-unit rounding and minimum/maximum
clamps for award and seizure; award against available gold, the base-rice
reserve, and self-target rejection; and seizure against target holdings and
self-target rejection. They compare exact target resource deltas, RNG,
fallback, and semantic state deltas.
Requests that omit the required argument object remain unverified because the
reference runner did not terminate within the bounded comparison run.
This is not yet a claim that every command-specific
constraint, clamp and persistence boundary has been dynamically
compared.
전투는 최종 승패 외에 phase, 공격 순서, critical·회피·계략 판정, 피해,
부상, 병량, 도시·국가 후처리와 log까지 비교합니다.
The fixture runner also reports whether the requested legacy command reached
its completed execution path. For multi-turn commands this is derived from the
pre-execution `LastTurn`, because commands such as `전투태세` reset their result
term to `1` on the completion call. `등용수락` and the founding commands have
explicit state-based completion checks because their successful legacy result
turn is not a reliable completion marker.
## 실패 판정
Mismatch는 다음 정보를 함께 남깁니다.
- fixture와 command
- 첫 RNG divergence
- 첫 state divergence
- ref/core log
- transaction 또는 reload 결과
- 재현 명령
기준선 자체의 실패와 변경으로 생긴 회귀를 분리합니다. 환경 누락으로 skip된
suite는 통과로 집계하지 않습니다.
## 완료 조건
기능별 완료에는 다음 근거가 모두 필요합니다.
1. 정상, 권한 실패, 입력 실패와 경계값 fixture
2. 같은 seed의 RNG와 결과 비교
3. state, log, queue와 persistence 비교
4. 실제 PostgreSQL 경로 또는 명시된 미검증 경계
5. UI 기능이면 같은 Chromium 조건의 화면·상호작용 비교
6. 상위 `ref-core2026-mapping.md`와 날짜가 있는 report의 재현 정보
+24 -42
View File
@@ -1,55 +1,37 @@
# TypeScript Version Policy
# TypeScript 버전 정책
## Decision
## 버전
The `core2026` rewrite workspace standardizes on exactly TypeScript `6.0.2`.
TypeScript 7 must not be introduced into individual workspace packages while
this policy is active.
Workspace 전체는 TypeScript `6.0.2`를 정확한 버전으로 사용합니다.
`pnpm-workspace.yaml`의 override가 transitive resolution에도 같은 버전을
적용합니다.
This is a temporary compatibility decision. TypeScript 7 currently does not
provide all compiler/tooling APIs required by this repository and its dependent
tools, which has caused typecheck, lint, and comparison-tool failures. The
workspace will remain on TypeScript 6 until those APIs and the surrounding
tooling support are available.
## Scope
This policy applies to every pnpm workspace package under:
적용 범위는 다음과 같습니다.
- 루트 개발 도구
- `app/*`
- `packages/*`
- `tools/*`
- the root development toolchain
All direct `typescript` dependencies in that scope must use the exact version
`6.0.2`. Do not use a caret range, package-local TypeScript 5 fallback, a
TypeScript 7 override, or an alias to another TypeScript version. A single
version must be resolved in `pnpm-lock.yaml`; the root `pnpm-workspace.yaml`
override enforces the same version for transitive TypeScript peer resolution.
Package별 다른 TypeScript 버전, caret range, alias와 fallback compiler를
추가하지 않습니다. `pnpm-lock.yaml`에는 승인된 한 버전만 resolve되어야
합니다. Ref PHP 저장소 `../ref/sam`의 도구 체인은 이 workspace 정책에
포함되지 않습니다.
The PHP runtime and its historical frontend under `legacy/` are not pnpm
workspace packages and retain their existing toolchain. This exception must not
be used by rewrite packages.
공유 `tsconfig.base.json`은 TypeScript 6의 `baseUrl` deprecation 경고에
대해 `ignoreDeprecations: "6.0"`을 사용합니다. Package별 suppression을
추가하지 않습니다.
TypeScript 6 reports the existing `baseUrl` configuration as deprecated. The
shared `tsconfig.base.json` sets `ignoreDeprecations` to `6.0` so the current
path mapping continues to work during this compatibility period. Removing or
replacing `baseUrl` remains part of the TypeScript 7 upgrade work; packages must
not add their own suppression values.
## 버전 변경
## TypeScript 7 Upgrade Gate
Compiler major version은 workspace 전체를 한 변경으로 갱신합니다. 다음
항목을 모두 확인해 주세요.
Moving to TypeScript 7 requires a deliberate workspace-wide change. Upgrade
only after all of the following are true:
1. 저장소 script가 사용하는 compiler API
2. `typescript-eslint`, `vue-tsc`, tsdown, Vite와 Prisma 도구 지원
3. 모든 manifest와 lockfile의 단일 version resolution
4. `CI=1 pnpm typecheck`, `pnpm lint`, `pnpm build`, `pnpm test`
5. `pnpm check:legacy:general`, `pnpm check:legacy:nation`
6. 별도 compiler 설치나 alias 없이 실행되는 비교 도구
1. The compiler/tooling APIs used by repository scripts are available.
2. `typescript-eslint`, `vue-tsc`, `tsdown`, Vite integrations, and other direct
TypeScript consumers officially support the selected TypeScript 7 release.
3. `CI=1 pnpm typecheck`, `pnpm lint`, `pnpm build`, and relevant tests pass.
4. Legacy comparison tools, including `pnpm check:legacy:nation`, pass without
a fallback TypeScript installation or version alias.
5. Every workspace manifest and `pnpm-lock.yaml` is updated together so only
the approved TypeScript 7 version is resolved for the rewrite workspace.
Until this gate is satisfied, failures caused by running TypeScript 7 are not a
supported baseline and must not be worked around by mixing compiler versions.
검증되지 않은 compiler major version을 package 한 곳에만 적용하지 않습니다.
-30
View File
@@ -1,30 +0,0 @@
Compare command logs (mode: action, strict: off, keepDate: off, excludeGuards: on, excludeTarget: on, countSensitive: off)
PHP commands: 87
TS commands: 84
Matched commands: 87
Mismatched commands: 0
Missing in TS: 0
Missing in PHP: 0
Ignored mismatches: 17
Shared TS log implementations excluded from per-command extraction:
- Nation/che_불가침수락: logs are emitted by
`packages/logic/src/diplomacy/instantResponse.ts` and verified by
`instantDiplomacyReference.integration.test.ts`.
- Nation/che_불가침파기수락: logs are emitted by
`packages/logic/src/diplomacy/instantResponse.ts` and verified by
`instantDiplomacyReference.integration.test.ts`.
- Nation/che_종전수락: logs are emitted by
`packages/logic/src/diplomacy/instantResponse.ts` and verified by
`instantDiplomacyReference.integration.test.ts`.
# Command Log Checklist
Mode: action
Strict: off
Keep date: off
Exclude guards: on
Exclude target: on
Ignore file: tools/compare-command-logs.ignore.json
- [x] All command logs match.
+46 -63
View File
@@ -1,83 +1,66 @@
# 파일 지도와 변경 절차
# 파일 지도
## 최상위 지도
## 최상위
```text
core2026/
├─ app/
│ ├─ gateway-frontend/ 계정·로비·관리 UI
│ ├─ gateway-api/ 인증·profile·operation·orchestrator
│ ├─ game-frontend/ profile 게임 SPA
│ ├─ game-api/ tRPC/SSE와 mutation 수신
│ └─ game-engine/ turn daemon과 persistence orchestration
│ ├─ gateway-frontend/
│ ├─ gateway-api/
│ ├─ game-frontend/
│ ├─ game-api/
│ └─ game-engine/
├─ packages/
│ ├─ common/ 공통 타입·직렬화·RNG
│ ├─ logic/ 명령·constraint·전투·trigger·scenario
│ ├─ infra/ Prisma·PostgreSQL·Redis
│ └─ tools-scripts/ resource schema 도구
├─ resources/ scenario·map·unitset·명령 profile
│ ├─ common/
│ ├─ logic/
│ ├─ infra/
│ └─ tools-scripts/
├─ resources/
├─ tools/
│ ├─ integration-tests/ DB/Redis 및 ref 차등
│ ├─ frontend-legacy-parity/ 실제 Chromium 비교
│ ├─ legacy-db-migration/ 장기보존 데이터 CLI
│ ├─ build-scripts/ profile resource 복사 기반 build 도구
│ └─ docs/ 문서 생성 도구
└─ docs/ VitePress 소스와 상세 설계 문서
└─ docs/
```
## 기능에서 파일로
## 기능별 시작점
| 기능 | 시작점 | 핵심 하위 경계 |
| -------------- | --------------------------------------------- | ------------------------------------- |
| 로그인·session | `gateway-api/src/router.ts` | `auth/*`, `account/router.ts`, Redis |
| profile 운영 | `gateway-api/src/adminRouter.ts` | `orchestrator/*`, gateway Prisma |
| 게임 인증 | `game-api/src/context.ts`, `router/auth` | session actor, profile·sanction |
| 메인 턴 입력 | `game-frontend/src/stores/mainDashboard.ts` | `router/turns`, `turns/*` |
| command 실행 | `game-engine/src/turn/reservedTurnHandler.ts` | `packages/logic/src/actions/turn` |
| 월간 lifecycle | `game-engine/src/turn/turnDaemon.ts` | `monthly*Handler.ts`, scenario events |
| 전투 | `actions/turn/general/che_출병.ts` | `packages/logic/src/war` |
| DB load/flush | `game-engine/src/turn/worldLoader.ts` | `databaseHooks.ts`, `packages/infra` |
| 공개/국가 정보 | `game-api/src/router/public`, `router/nation` | DTO와 redaction |
| 화면 parity | `game-frontend/src/views`, `styles` | `tools/frontend-legacy-parity` |
| 기능 | 시작점 | 연결 경로 |
| ----------------- | ---------------------------------------------------------- | ------------------------------------------- |
| gateway 인증·로비 | `app/gateway-api/src/router.ts` | `auth/`, `account/`, gateway frontend |
| 관리자·profile | `app/gateway-api/src/adminRouter.ts` | `orchestrator/`, gateway Prisma |
| game server | `app/game-api/src/server.ts` | config, context, tRPC, SSE, worker |
| game router | `app/game-api/src/router.ts` | `router/*` |
| game actor | `app/game-api/src/trpc.ts` | auth token, procedure, router shared helper |
| 예약 턴 API | `app/game-api/src/router/turns` | `turns/reservedTurns.ts`, input event |
| turn daemon | `app/game-engine/src/turn/turnDaemon.ts` | lifecycle, loader, handler, flush |
| daemon lease | `app/game-engine/src/lifecycle/databaseTurnDaemonLease.ts` | `TurnDaemonLease` |
| world load·flush | `worldLoader.ts`, `databaseHooks.ts` | `EngineStateManager`, game Prisma |
| 장수·국가 명령 | `packages/logic/src/actions/turn` | constraint, command module, engine handler |
| 전투 | `packages/logic/src/war` | action module, crew type, item, trait |
| 월간 처리 | `app/game-engine/src/turn/monthly*.ts` | scenario event, world dirty state |
| frontend route | `app/*-frontend/src/router/index.ts` | view, store, tRPC client |
| schema | `packages/infra/prisma/*.prisma` | migration, client, loader |
| resource | `resources/` | scenario/map/unit-set loader |
## 변경 절차
## 변경 단위
### API 화면
### API 화면
router의 input, auth procedure, transaction과 response를 먼저 정한 뒤 frontend 호출부와 오류·loading 상태를
연결합니다. public prefix에서 direct navigation, asset, tRPC와 SSE URL을 확인합니다. UI를 바꾸면 실제
Chromium에서 ref와 geometry·computed style·interaction을 비교합니다.
Router의 input schema, procedure, actor 해석, transaction과 error를 먼저
확인합니다. Frontend의 loading, empty, success, error와 auth redirect를
연결합니다. Prefix에서 direct navigation, tRPC, SSE와 asset URL을 검증합니다.
### 도메인 로직
### 게임 규칙
ref entry point부터 SQL·로그까지 호출 순서를 찾고, `packages/logic` 계산과 engine context/persistence를 함께
수정합니다. unit test만으로 끝내지 않고 fixed seed, 전체 state, RNG trace, 실패 side effect와 가능한
ref 차등을 확인합니다.
ref entry point, SQL, RNG, log와 mutation 순서를 조사합니다. 순수 계산은
`packages/logic`, 실행 context와 persistence는 `app/game-engine`에 둡니다.
Fixed-seed unit, 실제 DB integration과 ref 차등 fixture를 함께 갱신합니다.
### DB
기존 migration을 고치지 않고 새 migration을 만듭니다. 빈 DB 전체 적용, 기존 DB 증분 적용, 재실행 no-op,
constraint/index와 runtime query, backup/restore 또는 rollback 경로를 확인합니다.
Prisma schema, 새 migration, generated client, runtime type, loader,
in-memory dirty marking, transaction flush와 reload를 연결합니다. 기존 migration
파일은 수정하지 않습니다.
## 문서 사이트
### 문서
```sh
pnpm install
pnpm docs:generate
pnpm docs:dev
pnpm docs:build
pnpm docs:preview
```
`docs:dev``docs:build`는 먼저 커맨드 목록을 생성합니다. 정적 결과는 `docs/.vitepress/dist`에 생기며
Git에 포함하지 않습니다. 문서만 바꿔도 Prettier, 생성 결과의 clean diff, VitePress build와 내부 링크를
검증해 주세요.
## 리팩터링 체크포인트
- [문서 기준선](../reference-baseline.md)과 현재 commit의 diff를 먼저 봅니다.
- 파일 이동만 했는지 소유권·transaction·호출 순서까지 바뀌었는지 구분합니다.
- public API, DB schema, action key와 resource format은 내부 파일명보다 강한 계약입니다.
- `rg`로 이 페이지의 이전 경로가 남았는지 확인합니다.
- command key를 바꿨다면 저장된 예약 턴과 profile resource의 migration/호환 경로가 필요합니다.
- 문서의 광범위한 “완료” 표현은 관련 integration·ref 차등·Chromium 증거가 있을 때만 사용합니다.
코드 구조는 `architecture/``developer/`, 사용자 동작은 `user/`, ref
대응은 상위 mapping, 작업별 증거는 날짜가 있는 report에 기록합니다.
+48 -92
View File
@@ -1,106 +1,62 @@
# 도메인 로직과 핵심 클래스
# 도메인과 조립 지점
## 핵심 entity
## World entity
`packages/logic/src/domain/entities.ts`가 HTTP나 Prisma row에 종속되지 않은 `General`, `City`, `Nation`,
`Troop`, diplomacy와 trigger 상태를 정의합니다. engine 전용 `TurnGeneral`, `TurnWorldState`,
`TurnEvent``app/game-engine/src/turn/types.ts`에서 실행 시간·예약 턴·월간 상태를 더합니다.
`packages/logic/src/domain/entities.ts``world/types.ts`가 장수, 국가, 도시,
부대, 외교와 trigger state의 런타임 타입을 정의합니다. Prisma row는
`app/game-engine/src/turn/worldLoader.ts`가 이 타입으로 변환합니다.
`InMemoryTurnWorld`가 조회와 mutation을 제공하고 `EngineStateManager`
transaction snapshot과 dirty state를 관리합니다.
| entity | 핵심 책임 |
| ------- | -------------------------------------------------------------------------- |
| General | 능력치, 경험·공헌, 소속·도시·부대, 병력·훈련·사기, 자원, 특기·아이템, meta |
| City | 소유 국가, 규모, 인구·농업·상업·치안, 수비·성벽, 보급·전선 상태 |
| Nation | 수도, 국고·군량, 등급·국가 타입, 기술과 국가 meta |
| Troop | 부대장·구성원과 부대 상태 |
| World | 현재 연·월, 최근 턴 시각, scenario config/meta와 전체 entity collection |
## Command
Prisma row를 곧바로 게임 규칙에 넘기지 않습니다. `worldLoader.ts`와 API의 row mapper가 DB 표현을 domain
표현으로 바꾸고, flush 계층이 반대 변환을 담당합니다.
장수 command는 `GeneralActionDefinition`으로 다음 계약을 가집니다.
## 명령 정의
- key와 사용자 표시 이름
- raw args parser
- 예약, 최소, 실행 constraint
- 실패 문구 formatter
- pre/post turn과 stack metadata
- state patch, log와 side effect를 담는 실행 결과
`GeneralActionDefinition`은 장수·국가 예약 명령이 공유하는 계약입니다.
국가 command는 `packages/logic/src/actions/turn/nation`의 module과
`app/game-engine/src/turn/reservedTurnCommands.ts`에서 같은 실행 context에
연결됩니다. `commandRegistry.ts`와 command profile이 profile별 가용 명령을
결정합니다.
- `key`, `name`: 저장 key와 화면 표시명
- `parseArgs`: 외부 입력을 실행 인자로 변환
- `buildPermissionConstraints`: 예약 입력 자체를 허용할지 판단
- `buildMinConstraints`: command table에서 현재 가능한지 사전 판단
- `buildConstraints`: 실행 시점의 전체 조건
- `getPreReqTurn`, `getPostReqTurn`: 연속 실행과 재사용 대기
- `resolve`: domain state와 effect를 계산
## Constraint
각 파일의 `commandSpec`은 category, 인자 필요 여부, schema와 definition factory를 등록합니다.
`GENERAL_TURN_COMMAND_KEYS`, `NATION_TURN_COMMAND_KEYS`가 전체 key 집합이며, `TurnCommandProfile`
profile별 subset을 선택합니다.
`packages/logic/src/constraints``ConstraintContext``StateView`를 받아
예약·실행 조건을 평가합니다. API가 client 입력을 검증하는 단계와 daemon
실행 직전 world 상태를 검사하는 단계는 분리합니다. 실패 결과는 ref 문구,
turn 소비와 side effect 계약을 보존합니다.
## Constraint 시스템
## Action module
`packages/logic/src/constraints`는 “무엇이 필요한가”와 “현재 view가 무엇을 알고 있는가”를 분리합니다.
`ConstraintContext`에는 actor, city, nation, args, env와 평가 mode가 있고 `StateView`가 entity와 대상
정보를 제공합니다.
`loadActionModuleBundle()`은 국가 특성, 관직, 내정 특기, 전투 특기, 성격,
병종, 계승, scenario slot, item 순서로 module을 조립합니다. 계산 fold,
priority trigger와 의미 event는 각각 다른 interface를 사용합니다.
[행동 모듈 프로토콜](../architecture/action-module-protocol.md)을 따라 주세요.
평가 결과는 다음 셋입니다.
## 주요 클래스
- `allow`: 현재 정보로 조건을 만족합니다.
- `deny`: 이유가 확정된 실패입니다.
- `unknown`: 대상 입력이나 추가 state가 없어 아직 판정할 수 없습니다.
| 클래스·함수 | 책임 |
| ------------------------- | -------------------------------------------------- |
| `GatewayOrchestrator` | profile operation과 process reconciliation |
| `createGameApiServer` | game transport, context, router와 worker lifecycle |
| `DatabaseTurnDaemonLease` | profile별 lease, heartbeat와 fencing |
| `TurnDaemonLifecycle` | schedule, pause/resume/run/shutdown loop |
| `InMemoryTurnWorld` | turn 실행 중 world state와 dirty tracking |
| `EngineStateManager` | snapshot, transaction flush와 rollback restore |
| `ReservedTurnHandler` | revision·lease 기반 예약 명령 claim과 실행 |
| `GeneralActionPipeline` | action module 계산·trigger·event 실행 |
| `WarEngine` | 전투 phase, RNG, 상태와 log 결과 |
API command table은 `unknown`의 missing requirement가 대상 입력뿐이면 `needsInput`, 그 밖이면
`unknown`으로 보여 줍니다. 예약 뒤 실제 실행에서는 전체 context로 다시 판단합니다.
## 명령 추가
## 핵심 클래스와 조립 지점
### TurnDaemonLifecycle
`app/game-engine/src/lifecycle/turnDaemonLifecycle.ts`에 있습니다. clock, control queue, hook과 run budget을
조정하며 pause/resume/manual/scheduled run의 상태 전이를 소유합니다.
### DatabaseTurnDaemonLease
`app/game-engine/src/lifecycle/databaseTurnDaemonLease.ts`에 있습니다. profile별 단일 active owner와 fencing을
관리합니다. daemon 계산이 맞아도 lease를 잃었다면 결과를 저장하면 안 됩니다.
### InMemoryTurnWorld
`app/game-engine/src/turn/inMemoryWorld.ts`에 있습니다. entity map, dirty/create/delete set, log, message,
event, checkpoint와 월 변경을 소유합니다. `peekDirtyState()`는 저장할 변경을 보여 주고 성공한 flush 뒤
정리됩니다.
### EngineStateManager
`app/game-engine/src/turn/engineStateManager.ts`에 있습니다. world와 예약 턴 store 같은 mutable participant를
등록하고 계산 단위의 capture/restore/transaction을 제공합니다. PostgreSQL transaction을 대신하지 않고
실패한 계산의 메모리 rollback을 담당합니다.
### InMemoryReservedTurnStore와 ReservedTurnHandler
`reservedTurnStore.ts`는 장수 30칸·국가 12칸 예약 queue를 메모리에 유지합니다.
`reservedTurnHandler.ts`는 명령 loading, constraint, action context, AI fallback, progress/cooldown,
효과·로그와 queue rotation을 연결합니다.
### GeneralActionPipeline과 trigger module
`packages/logic/src/actions/engine.ts`, `triggers/*`는 명령 본체 전후의 특기·아이템·국가 특성 효과를
일정한 우선순위로 적용합니다. 같은 module 목록이라도 실행 순서가 결과와 RNG 소비를 바꿀 수 있습니다.
### WarEngine
`packages/logic/src/war/engine.ts`가 전투 resolution을, `war/actions.ts`와 trigger module이 확장 효과를,
`war/aftermath.ts`가 피해·점령·외교·후속 state를 계산합니다. `che_출병.ts`가 map, unit set, diplomacy,
time, seed와 aftermath를 조립하는 실제 장수 명령 entry입니다.
### GatewayOrchestrator
`app/gateway-api/src/orchestrator/gatewayOrchestrator.ts`가 DB의 profile desired state를 process state에
맞춥니다. `workspaceManager.ts`, `buildRunner.ts`, `seedProfileDatabase.ts`, `pm2ProcessManager.ts`
commit worktree 준비부터 build, seed, start/stop을 나눕니다.
## 새 명령을 추가할 때
1. 가장 가까운 ref command의 constraint, 실행 순서, RNG, 로그와 DB side effect를 조사합니다.
2. `packages/logic/src/actions/turn/{general,nation}`에 definition과 `commandSpec`을 작성합니다.
3. 해당 `*_TURN_COMMAND_KEYS`와 필요한 `resources/turn-commands` profile에 key를 등록합니다.
4. 인자가 있으면 Zod schema와 `app/game-api/src/turns/commandInput.ts`의 화면 입력 field를 연결합니다.
5. engine action context가 대상 entity·map·unit set·시간·seed를 완전하게 공급하는지 확인합니다.
6. permission/min/full 실패, 성공, 연속 턴, cooldown과 persistence를 테스트합니다.
7. `pnpm docs:generate`로 플레이어 커맨드 목록을 갱신하고 ref 매핑 문서를 함께 수정합니다.
1. ref command의 예약·실행 constraint, args, RNG, log와 DB mutation을 찾습니다.
2. command definition과 필요한 domain helper를 추가합니다.
3. engine registry, profile resource와 frontend args UI를 연결합니다.
4. state patch와 dirty field가 flush·reload되는지 확인합니다.
5. 정상·실패·경계 fixed-seed test와 ref 차등 fixture를 추가합니다.
6. 생성 command catalog, 상위 mapping과 report를 갱신합니다.
+25 -24
View File
@@ -1,30 +1,31 @@
# 개발자 핸드북
이 핸드북은 새 기능을 어디에 넣을지뿐 아니라 요청이 어떤 경계를 지나 상태로 남는지 설명합니다. 먼저
[문서 기준선](../reference-baseline.md)을 확인하고, 변경 성격에 따라 다음 순서로 읽어 주세요.
## 읽기 순서
| 변경하려는 것 | 먼저 읽을 문서 | 주로 확인할 코드 |
| -------------------------- | -------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- |
| 화면·라우팅·조회 API | [시스템 아키텍처](./system-architecture.md) | `app/*-frontend`, `app/*-api` |
| 턴 입력·게임 상태 mutation | [요청·턴·저장 흐름](./request-turn-persistence.md) | `app/game-api`, `app/game-engine` |
| 명령·전투·월간 로직 | [도메인 로직과 핵심 클래스](./domain-and-classes.md), [장수 행동 모듈 프로토콜](../architecture/action-module-protocol.md) | `packages/logic`, `app/game-engine/src/turn` |
| 시나리오 이벤트·규칙 조합 | [시나리오 리소스 합성](../architecture/scenario-composition.md) | `resources/scenario`, `scenarioLoader.ts` |
| 새 파일 위치·검증 범위 | [파일 지도와 변경 절차](./code-map.md) | package manifest, test, docs |
| 작업 | 문서 | 코드 시작점 |
| --------------------- | --------------------------------------------------------------- | ----------------------------- |
| 전체 구조 | [아키텍처 개요](../architecture/overview.md) | `app/`, `packages/`, `tools/` |
| process·worker·daemon | [런타임 아키텍처](../architecture/runtime.md) | app server와 CLI |
| 파일 위치 | [파일 지도](./code-map.md) | router, handler, schema |
| 명령·전투·효과 | [도메인과 조립](./domain-and-classes.md) | `packages/logic` |
| mutation·flush | [요청·턴·저장](./request-turn-persistence.md) | game API, game engine |
| action module | [행동 모듈 프로토콜](../architecture/action-module-protocol.md) | `actionModules/` |
| ref 비교 | [차등 검증](../architecture/turn-state-differential-testing.md) | `tools/integration-tests` |
## 읽을 때 지켜야 할 경계
## 경계
- `packages/logic`의 순수 계산과 `app/game-engine`의 scheduling·persistence orchestration을 구분합니다.
- game API가 mutation을 받는 것과 engine이 world mutation을 확정하는 것은 다른 단계입니다.
- PostgreSQL `input_event`가 내구성 있는 작업 경계이며 Redis는 realtime fan-out과 일부 보조 worker
통신에 사용됩니다.
- 로그인한 사용자, 게임 장수, 국가 직책은 같은 개념이 아닙니다. actor와 소유권은 session에서
서버가 결정합니다.
- `resources/` scenario·map·unit set·turn-command profile이 런타임 구성을 바꿉니다. 기본 TypeScript
목록만 보고 실제 profile을 단정하지 않습니다.
- ref 호환 변경은 결과뿐 아니라 판정·정렬·반올림·RNG 소비·로그·저장 순서를 비교합니다.
- `packages/logic` 계산과 규칙을 소유합니다.
- `app/game-engine`은 clock, queue, AI, 월간 순서, transaction과 flush를
소유합니다.
- `app/game-api`는 transport, 인증, input validation과 request acceptance를
소유합니다.
- `packages/infra`의 Prisma schema와 migration은 영속 구조의 기준입니다.
- `resources` scenario, map, unit setcommand profile을 구성합니다.
- actor와 resource owner는 session과 DB에서 서버가 결정합니다.
- `InputEvent`와 PostgreSQL transaction이 gameplay mutation의 내구성
경계입니다.
- Redis와 SSE는 session·worker·fan-out 경로이며 world commit의 대체 저장소가
아닙니다.
## 기존 상세 문서와의 관계
이 핸드북은 탐색용 상위 지도입니다. 상세한 호환 근거와 테스트 절차는 `docs/architecture/*`,
`docs/integration-tests.md`, `docs/frontend-legacy-parity.md`에 유지합니다. 상위 작업공간의
`../docs/ref-core2026-mapping.md`는 ref entry point와 core 구현의 end-to-end 대응 인덱스입니다.
기능을 변경하면 상위 `../docs/ref-core2026-mapping.md`의 ref entry point,
호출 순서, 인증, DB mutation, RNG와 오류 경로를 함께 갱신해 주세요.
+71 -75
View File
@@ -1,99 +1,95 @@
# 요청·턴·저장 흐름
## 조회 요청
일반 query는 다음 경로를 따릅니다.
## 조회
```text
Vue view/store
-> tRPC client
-> game-api router
-> session actor + 입력 validation
-> Prisma query
-> 권한에 맞춘 DTO/redaction
-> 화면 상태
browser -> tRPC -> procedure auth -> Prisma/Redis read -> DTO/redaction -> browser
```
조회는 engine의 in-memory object를 직접 공유하지 않습니다. 따라서 daemon이 transaction을 commit하기 전의
중간 계산은 API query에 노출되지 않습니다.
Public, own, same-nation, foreign, NPC와 role별 응답은 router가 server-side
actor와 resource relation으로 결정합니다. Prisma row를 그대로 반환하지 않고
endpoint DTO에서 공개 field를 선택합니다.
## API가 직접 끝내는 mutation
예약 턴, 메시지처럼 API가 DB에서 완결할 수 있는 변경도 `input_event`를 사용합니다.
1. `Idempotency-Key`와 tRPC path로 요청 identity를 만듭니다.
2. `app/game-api/src/inputEventBoundary.ts`가 중복·처리 상태를 확인합니다.
3. 같은 PostgreSQL transaction에서 대상 row와 input event 결과를 저장합니다.
4. commit 뒤 응답하고 필요한 realtime 갱신을 알립니다.
동일 revision을 전제로 한 예약 턴 수정은 다른 탭이나 요청이 먼저 갱신했으면 충돌합니다. frontend는 최신
목록을 다시 불러와 사용자의 변경을 덮어쓰지 않게 해야 합니다.
## engine이 처리하는 mutation
## API transaction mutation
```text
game-api mutation
-> input_event PENDING
-> daemon claim (FOR UPDATE SKIP LOCKED)
-> lease/fencing 확인
-> EngineStateManager savepoint
-> command/turn/monthly handler가 InMemoryTurnWorld 변경
-> world + 예약 턴 + log + message + event 결과 flush
-> input_event COMPLETED를 같은 DB transaction으로 commit
-> commit 이후 realtime 신호
request
-> requestId와 input 검증
-> session actor·권한 검사
-> InputEvent(target=API, PROCESSING)
-> Prisma transaction
-> domain row mutation
-> InputEvent(SUCCEEDED)
-> commit
-> notification
```
계산이나 DB 쓰기가 실패하면 `EngineStateManager`가 등록된 in-memory participant를 savepoint로 되돌립니다.
DB transaction도 commit되지 않아 메모리와 DB의 부분 진행을 피합니다. lease를 잃은 worker는 stale 결과를
commit할 수 없어야 합니다.
`app/game-api/src/inputEventBoundary.ts``executeInputEvent()`가 이 경계를
제공합니다. 중복 request ID는 완료·처리 중 event를 다시 실행하지 않으며,
실패 event는 claim 조건을 만족할 때 attempts를 증가시킵니다.
## 한 tick의 처리
## Daemon mutation
`TurnDaemonLifecycle`은 clock과 schedule에서 다음 실행 시점을 구합니다. 턴을 시작하면
`InMemoryTurnProcessor``InMemoryTurnWorld``turnTime`, 그다음 `general.id` 순서로 실행 대상을
결정합니다. checkpoint는 재시작 시 이미 처리한 동일 시점의 장수를 건너뛰는 기준입니다.
```text
request
-> actor·input 검증
-> InputEvent(target=DAEMON)
-> daemon transport
-> lease owner claim
-> in-memory world mutation
-> EngineStateManager transaction flush
-> event 결과와 checkpoint commit
-> SSE/realtime
```
장수 한 명의 예약 명령은 대략 다음 순서입니다.
명령은 API 수락 시점과 daemon 실행 시점에 필요한 조건을 각각 검사합니다.
예약 뒤 world가 바뀔 수 있으므로 실행 constraint를 생략하지 않습니다.
1. `InMemoryReservedTurnStore`에서 첫 예약 명령을 읽습니다.
2. 명령 key를 `GeneralTurnCommandLoader` 또는 `NationTurnCommandLoader`로 불러옵니다.
3. `actionContextBuilder`가 대상 도시·국가·장수, map, unit set, 외교, 시간과 RNG를 구성합니다.
4. permission/min/full constraint를 목적에 맞게 평가합니다.
5. 선행 턴이 있으면 진행 상태를 쌓고, 완성된 시점에 `resolve()`를 실행합니다.
6. effect와 직접 변경을 world에 반영하고 로그·메시지·후속 턴 시간을 기록합니다.
7. 실행된 queue를 당기고 끝에 기본 `휴식`을 채웁니다.
## Tick
예약 시 통과와 실행 시 성공은 같지 않습니다. 그 사이 자원, 도시 소유, 외교, 직책이 바뀔 수 있으므로 full
constraint는 실행 순간 다시 평가됩니다.
`TurnDaemonLifecycle`은 다음 장수 turn time과 tick 경계 중 빠른 시각을
선택합니다. 한 run은 budget 안에서 due command를 처리하고 calendar 경계를
진행합니다.
## 월 변경 경계
1. lease와 fencing token을 확인합니다.
2. 예약 턴을 revision/lease로 claim합니다.
3. command args와 실행 constraint를 평가합니다.
4. action module과 command handler가 state patch, log, message를 만듭니다.
5. world에 patch를 적용하고 dirty entity를 기록합니다.
6. 월 경계를 지났으면 scenario event action을 정해진 순서로 실행합니다.
7. transaction에서 dirty state, turn queue, log, event와 checkpoint를 flush합니다.
`InMemoryTurnWorld.advanceMonth()`는 다음 순서를 보존합니다.
Transaction 실패 시 `EngineStateManager`가 in-memory snapshot을 복원합니다.
Lease를 잃은 process는 fencing 검사에서 commit하지 못합니다.
1. 다음 연·월을 계산합니다.
2. `beforeMonthChanged` handler를 등록 순서대로 실행합니다.
3. world의 현재 연·월을 바꿉니다.
4. `onMonthChanged` handler를 등록 순서대로 실행합니다.
5. 연도가 바뀌었으면 `onYearChanged`를 실행합니다.
## 저장 위치
`turnDaemon.ts``composeCalendarHandlers()` 순서에는 월간 event, 수입, 연감, PRE_MONTH 상태 정리,
국가 명령, 국가 통계, 외교, 전쟁 설정, 방랑, 국가 수, 통일, 토너먼트, 경매, 전선 상태가 포함됩니다.
이 순서는 ref의 관찰 가능한 결과와 RNG·persistence에 영향을 주므로 리팩터링 시 단순 정렬하지 않습니다.
| 상태 | 기준 |
| ----------------------------------- | ------------------------------------ |
| world, 장수, 국가, 도시, 외교, 부대 | game Prisma model |
| 예약 명령과 revision | `GeneralTurn*`, `NationTurn*` |
| 내구성 입력 | `InputEvent` |
| daemon 소유권 | `TurnDaemonLease` |
| checkpoint와 calendar meta | `WorldState` |
| 사용자 출력 | `LogEntry`, message·board 관련 model |
| fan-out | Redis/SSE |
## RNG 경계
Redis notification 실패는 이미 commit된 PostgreSQL mutation을 되돌리지
않습니다. 재연결 client는 DB 조회로 상태를 복구합니다.
게임 결과에 영향을 주는 난수는 `LiteHashDRBG``RandUtil`을 사용합니다. seed에는 hidden seed와
action/month/general 같은 context가 직렬화됩니다. main RNG의 소비 순서를 유지해야 하는 로직과 독립된
재현 가능 substream을 써야 하는 fallback을 구분합니다. authoritative path에 `Math.random()`을 넣지
않습니다.
## RNG
## 장애를 추적할 위치
RNG instance는 command와 월간 handler context로 전달합니다. 판정 순서,
후보 정렬과 소비 호출 수를 변경하지 않습니다. Main stream과 관계없는
fallback 무작위성은 seed가 있는 별도 substream을 사용합니다.
| 증상 | 우선 확인 |
| ----------------------------- | --------------------------------------------------------------------- |
| 같은 mutation이 두 번 보임 | idempotency key, `input_event` 상태·attempt |
| 요청은 성공했지만 화면이 늦음 | DB commit 결과, Redis/SSE fan-out |
| daemon이 처리하지 않음 | profile gate, pause 상태, lease owner, PENDING claim |
| 재시작 뒤 일부 턴 반복 | checkpoint와 general turn ordering |
| DB와 메모리가 다름 | `EngineStateManager`, `databaseHooks`, flush 대상 누락 |
| ref와 결과가 다름 | constraint 순서, action context, RNG trace, rounding, log/effect 순서 |
## 추적 지점
- request acceptance: `app/game-api/src/inputEventBoundary.ts`
- daemon transport: `app/game-api/src/daemon/`
- lifecycle: `app/game-engine/src/lifecycle/turnDaemonLifecycle.ts`
- runtime composition: `app/game-engine/src/turn/turnDaemon.ts`
- world: `inMemoryWorld.ts`, `worldLoader.ts`
- transaction: `engineStateManager.ts`, `databaseHooks.ts`
- queue: `reservedTurnStore.ts`, `reservedTurnHandler.ts`
- schema: `packages/infra/prisma/game.prisma`
-100
View File
@@ -1,100 +0,0 @@
# 시스템 아키텍처
## 런타임 구성
```text
브라우저
├─ /gateway/ ─ gateway-frontend ─ tRPC ─ gateway-api
│ ├─ PostgreSQL public schema
│ ├─ Redis session
│ └─ orchestrator ─ Git worktree / build / PM2
└─ /{profile}/ ─ game-frontend ─ tRPC/SSE ─ game-api
├─ profile별 PostgreSQL schema
├─ Redis realtime/battle worker
└─ input_event ─ game-engine
├─ in-memory world
└─ transactional flush
```
Gateway는 계정·session·profile lifecycle을, game 계층은 한 profile의 플레이와 턴 진행을 소유합니다.
`/gateway`, `/che`, `/hwe` 같은 외부 prefix와 `/image/*`의 Caddy 소유권은 애플리케이션 밖의 배포
계약입니다.
## 애플리케이션
### gateway-frontend
`app/gateway-frontend/src/main.ts`가 Vue 앱과 router를 시작합니다. 가입·로그인·계정·로비·관리자 화면은
`src/views`에 있고, profile 선택 뒤 game frontend로 이동합니다. 브라우저에 보이는 `VITE_*` 값은
공개 설정이며 secret이 아닙니다.
### gateway-api
`app/gateway-api/src/server.ts``router.ts`가 HTTP/tRPC 경계입니다.
- `auth/*`, `account/router.ts`: Kakao·로컬 계정, session 발급·폐기, 사용자 저장소
- `lobby/profileStatusService.ts`: 사용자에게 보여 줄 profile 상태
- `adminRouter.ts`, `adminAuth.ts`: 관리자 권한과 operation 입력
- `orchestrator/*`: 원하는 profile 상태를 Git worktree, build, seed, PM2 프로세스에 반영
Gateway DB는 기본 `public` schema를 사용합니다. game profile DB를 직접 게임 로직의 source of truth로
대체하지 않습니다.
### game-frontend
`app/game-frontend/src/main.ts`가 profile base path 아래 Vue SPA를 시작합니다. `src/views`가 공개 정보,
메인 턴 입력, 국가 운영, 경매·토너먼트·기록 화면을 나누고 `src/stores/mainDashboard.ts`가 메인 화면의
query, 예약 턴 revision, mutation과 realtime refresh를 조정합니다.
UI는 서버가 반환한 command table의 `available`, `blocked`, `needsInput`, `unknown` 상태를 사용합니다.
클라이언트가 장수 ID나 직책을 보냈다는 이유만으로 권한이 생기지 않습니다.
### game-api
`app/game-api/src/server.ts``router.ts`가 query/mutation을 공개합니다. router는 기능 단위로
`src/router/*`에 나뉩니다.
- 읽기: world, public, directory, ranking, yearbook, dynasty 등은 권한·redaction을 거쳐 DB에서 조회합니다.
- 플레이: turns, join, nation, troop, diplomacy, messages, auction, betting, tournament 등이 있습니다.
- mutation: `inputEventBoundary.ts`가 idempotency와 PostgreSQL 작업 경계를 만듭니다.
- realtime: SSE와 Redis 알림은 commit 이후 화면 갱신 신호입니다.
### game-engine
`app/game-engine/src/index.ts`가 daemon entry point이고 `src/turn/turnDaemon.ts`가 profile resource,
world snapshot, command registry, calendar handler, persistence hook과 lease를 조립합니다.
한 daemon owner가 `TurnDaemonLifecycle`을 통해 정해진 tick과 durable inbox를 처리합니다. 실제 장수·도시·국가
상태는 `InMemoryTurnWorld`에서 계산하고, 성공한 작업만 `databaseHooks.ts`를 통해 PostgreSQL에 flush합니다.
lease/fencing은 오래된 daemon owner의 commit을 막습니다.
## 공유 package
| package | 책임 | 넣지 말아야 할 것 |
| ------------------------ | ------------------------------------------------------------------------------- | ---------------------------------- |
| `packages/common` | 공통 type, 직렬화, `LiteHashDRBG`, `RandUtil`, session/sanction 유틸리티 | profile DB orchestration |
| `packages/logic` | entity, constraint, command, battle, trigger, scenario parsing 같은 도메인 계산 | HTTP·Vue·Prisma transaction 소유권 |
| `packages/infra` | Prisma client, PostgreSQL·Redis connector, log와 turn-engine DB adapter | 게임 규칙 결정 |
| `packages/tools-scripts` | resource schema 생성·검증 | 런타임 요청 처리 |
## 데이터와 구성
- `packages/infra/prisma/schema.gateway.prisma`: 계정·profile·operation 같은 gateway 모델
- `packages/infra/prisma/schema.game.prisma`: profile별 world·general·city·nation·turn·event·log 모델
- `resources/scenario`: 시작 연도, 상수, 월간 event 등 scenario 정의.
`extensions/`의 이벤트·규칙·아이템 팩은 `extends`로 순서 있게 합성합니다.
- `resources/map`, `resources/unitset`: 지형과 병종 정의
- `resources/turn-commands`: profile별 허용 명령 목록
새 persistence field는 schema와 새 migration만으로 끝나지 않습니다. domain type, loader, in-memory dirty
tracking, transaction flush, reload 검증까지 연결해야 합니다.
## 인증·권한 모델
Gateway session은 사용자 identity를, game token은 profile과 게임 역할을 전달합니다. game API는 session으로
내 장수를 조회한 뒤 그 장수의 국가·직책·sanction과 대상 resource의 관계를 판단합니다. 공개 endpoint도
비공개 국가 정보, 타 사용자 archive, 비밀 명령을 DTO에서 제거해야 합니다.
권한 변경을 검증할 때는 무인증, 일반 사용자, 본인, 같은 국가, 다른 국가, NPC, 직책 보유자, sanction
적용자를 필요한 범위에서 나눕니다. 거부된 mutation은 world와 queue에 side effect를 남기지 않아야 합니다.
+79 -115
View File
@@ -1,132 +1,96 @@
# dev-sam-e2e Caddy 라우팅 계약
# Caddy prefix 계약
`dev-sam-e2e.hided.net`은 URI prefix를 제거하지 않고 frontend/API upstream에
전달한다. 따라서 Caddy matcher, frontend build base, 브라우저 API URL,
backend listen path가 같은 prefix를 사용해야 한다.
## 활성 경로
## Caddy 설정
| 서비스 | 공개 prefix | frontend | API |
| ------- | ----------- | -------: | ------: |
| gateway | `/gateway/` | `15000` | `15001` |
| che | `/che/` | `15002` | `15003` |
| hwe | `/hwe/` | `15014` | `15015` |
Caddy path matcher의 `/gateway``/gateway/` `/gateway/api/trpc`
포함하지 않는다. 각 앱의 exact path는 trailing slash로 redirect하고 하위
경로는 wildcard matcher로 받아야 한다. API matcher는 frontend matcher보다
먼저 둔다.
Upstream host는 `172.30.1.54`입니다. `kwe`, `pwe`, `twe`, `nya`, `pya`
resource·profile 이름으로 사용할 수 있지만 활성 Caddy route가 아닙니다.
```caddyfile
redir /gateway /gateway/ 308
@gatewayApi path /gateway/api /gateway/api/*
handle @gatewayApi {
reverse_proxy http://172.30.1.54:15001 {
header_up X-Real-IP {remote_host}
header_down -Strict-Transport-Security
}
}
handle /gateway/* {
reverse_proxy http://172.30.1.54:15000 {
header_up X-Real-IP {remote_host}
header_down -Strict-Transport-Security
}
}
Caddy는 prefix를 보존해 upstream에 전달합니다. 앱은 root 배포를 가정하지
않고 frontend base, tRPC, SSE, upload와 direct navigation에 같은 prefix를
사용합니다. `/gateway`, `/che`, `/hwe`는 trailing slash 경로로 redirect합니다.
redir /che /che/ 308
@cheApi path /che/api /che/api/*
handle @cheApi {
reverse_proxy http://172.30.1.54:15003 {
header_up X-Real-IP {remote_host}
header_down -Strict-Transport-Security
}
}
handle /che/* {
reverse_proxy http://172.30.1.54:15002 {
header_up X-Real-IP {remote_host}
header_down -Strict-Transport-Security
}
}
## Backend
redir /kwe /kwe/ 308
@kweApi path /kwe/api /kwe/api/*
handle @kweApi {
reverse_proxy http://172.30.1.54:15005 {
header_up X-Real-IP {remote_host}
header_down -Strict-Transport-Security
}
}
handle /kwe/* {
reverse_proxy http://172.30.1.54:15004 {
header_up X-Real-IP {remote_host}
header_down -Strict-Transport-Security
}
}
Gateway API:
redir /twe /twe/ 308
@tweApi path /twe/api /twe/api/*
handle @tweApi {
reverse_proxy http://172.30.1.54:15007 {
header_up X-Real-IP {remote_host}
header_down -Strict-Transport-Security
}
}
handle /twe/* {
reverse_proxy http://172.30.1.54:15006 {
header_up X-Real-IP {remote_host}
header_down -Strict-Transport-Security
}
}
redir /hwe /hwe/ 308
@hweApi path /hwe/api /hwe/api/*
handle @hweApi {
reverse_proxy http://172.30.1.54:15015 {
header_up X-Real-IP {remote_host}
header_down -Strict-Transport-Security
}
}
handle /hwe/* {
reverse_proxy http://172.30.1.54:15014 {
header_up X-Real-IP {remote_host}
header_down -Strict-Transport-Security
}
}
```sh
GATEWAY_API_HOST=0.0.0.0
GATEWAY_API_PORT=15001
GATEWAY_TRPC_PATH=/gateway/api/trpc
```
기존 `/image/*` handler는 앱 route 밖에 그대로 둔다. 위 블록은 URI를
strip하지 않으므로 backend의 tRPC/SSE path도 public path 전체를 사용한다.
CHE:
## build/runtime 환경값
```sh
GAME_API_HOST=0.0.0.0
GAME_API_PORT=15003
GAME_TRPC_PATH=/che/api/trpc
GAME_API_EVENTS_PATH=/che/api/events
```
| 프로세스 | 포트 | 필수 경로 환경값 |
| --- | ---: | --- |
| gateway frontend | 15000 | `VITE_APP_BASE_PATH=/gateway/`, `VITE_GATEWAY_API_URL=/gateway/api/trpc`, `VITE_GAME_API_URL_TEMPLATE=/{profile}/api/trpc`, `VITE_GAME_WEB_URL_TEMPLATE=/{profile}/` |
| gateway API | 15001 | `GATEWAY_API_HOST=0.0.0.0`, `GATEWAY_API_PORT=15001`, `GATEWAY_TRPC_PATH=/gateway/api/trpc` |
| che frontend | 15002 | `VITE_APP_BASE_PATH=/che/`, `VITE_GATEWAY_API_URL=/gateway/api/trpc`, `VITE_GAME_API_URL=/che/api/trpc`, `VITE_GAME_SSE_URL=/che/api/events`, `VITE_GAME_ASSET_URL=/image`, `VITE_GAME_PROFILE=che` |
| che API | 15003 | `GAME_API_HOST=0.0.0.0`, `GAME_API_PORT=15003`, `GAME_TRPC_PATH=/che/api/trpc`, `GAME_API_EVENTS_PATH=/che/api/events`, `GAME_UPLOAD_PATH=/che/api/uploads`, `PROFILE=che` |
| kwe frontend | 15004 | `VITE_APP_BASE_PATH=/kwe/`, `VITE_GATEWAY_API_URL=/gateway/api/trpc`, `VITE_GAME_API_URL=/kwe/api/trpc`, `VITE_GAME_SSE_URL=/kwe/api/events`, `VITE_GAME_ASSET_URL=/image`, `VITE_GAME_PROFILE=kwe` |
| kwe API | 15005 | `GAME_API_HOST=0.0.0.0`, `GAME_API_PORT=15005`, `GAME_TRPC_PATH=/kwe/api/trpc`, `GAME_API_EVENTS_PATH=/kwe/api/events`, `GAME_UPLOAD_PATH=/kwe/api/uploads`, `PROFILE=kwe` |
| twe frontend | 15006 | `VITE_APP_BASE_PATH=/twe/`, `VITE_GATEWAY_API_URL=/gateway/api/trpc`, `VITE_GAME_API_URL=/twe/api/trpc`, `VITE_GAME_SSE_URL=/twe/api/events`, `VITE_GAME_ASSET_URL=/image`, `VITE_GAME_PROFILE=twe` |
| twe API | 15007 | `GAME_API_HOST=0.0.0.0`, `GAME_API_PORT=15007`, `GAME_TRPC_PATH=/twe/api/trpc`, `GAME_API_EVENTS_PATH=/twe/api/events`, `GAME_UPLOAD_PATH=/twe/api/uploads`, `PROFILE=twe` |
| hwe frontend | 15014 | `VITE_APP_BASE_PATH=/hwe/`, `VITE_GATEWAY_API_URL=/gateway/api/trpc`, `VITE_GAME_API_URL=/hwe/api/trpc`, `VITE_GAME_SSE_URL=/hwe/api/events`, `VITE_GAME_ASSET_URL=/image`, `VITE_GAME_PROFILE=hwe` |
| hwe API | 15015 | `GAME_API_HOST=0.0.0.0`, `GAME_API_PORT=15015`, `GAME_TRPC_PATH=/hwe/api/trpc`, `GAME_API_EVENTS_PATH=/hwe/api/events`, `GAME_UPLOAD_PATH=/hwe/api/uploads`, `PROFILE=hwe` |
HWE:
`VITE_*` 값은 frontend build-time 값이다. frontend 프로세스를 시작할 때만
설정해서는 이미 생성된 bundle이 바뀌지 않는다. Caddy가 prefix를 보존하므로
`handle_path`로 바꾸거나 upstream에서 다시 strip하면 위 backend path와
불일치한다. E2E에서 Vite preview를 사용할 때는 외부 Host 헤더
`dev-sam-e2e.hided.net`도 preview allowlist에 포함되어야 한다.
```sh
GAME_API_HOST=0.0.0.0
GAME_API_PORT=15015
GAME_TRPC_PATH=/hwe/api/trpc
GAME_API_EVENTS_PATH=/hwe/api/events
```
## 앱을 구동하지 않는 검증
## Frontend build
다음 항목을 독립적으로 확인한다.
Gateway:
1. Caddy config를 `caddy validate`/`caddy adapt`로 검증한다.
2. 각 upstream 대신 요청 URI와 listen port를 돌려주는 mock HTTP server를
붙여 public path가 올바른 port로 전달되고 prefix가 보존되는지 확인한다.
3. frontend를 임시 output directory에 build하고 `index.html`의 module/CSS
URL이 각각 `/gateway/`, `/che/`, `/kwe/`, `/twe/`, `/hwe/`로 시작하는지 확인한다.
4. bundle에서 외부 API/SSE URL과 `/image` asset base를 확인한다.
```sh
VITE_APP_BASE_PATH=/gateway \
VITE_GATEWAY_API_URL=/gateway/api/trpc \
VITE_GAME_API_URL_TEMPLATE='/{profile}/api/trpc' \
VITE_GAME_WEB_URL_TEMPLATE='/{profile}/' \
pnpm --filter @sammo-ts/gateway-frontend build
```
저장소의 `tools/e2e-routing-check/Caddyfile`은 18080에서 public Caddy
matcher를 재현하고, 19000번대 mock upstream이 선택된 service와 전달받은
URI를 응답한다. core2026 프로세스나 DB/Redis 없이 matcher 우선순위와
prefix 보존 여부를 반복 검증할 수 있다.
Game frontend는 profile별 값으로 build합니다.
이 검증은 proxy/build 계약을 확인하지만 실제 DB, Redis, 인증과 게임 동작을
검증하지는 않는다.
```sh
VITE_APP_BASE_PATH=/che \
VITE_GAME_API_URL=/che/api/trpc \
VITE_GAME_SSE_URL=/che/api/events \
pnpm --filter @sammo-ts/game-frontend build
```
HWE는 `/che``/hwe`로 바꿉니다. `VITE_*`는 browser bundle에 포함되는
공개값이므로 secret을 넣지 않습니다.
## 정적 자산
- Vite asset URL은 frontend base path를 포함합니다.
- Router direct navigation과 새로고침은 SPA fallback으로 frontend에
전달됩니다.
- API와 events matcher는 frontend fallback보다 먼저 적용합니다.
- `/image/*`는 Caddy가 별도 파일 시스템에서 제공합니다. 앱에서 rewrite,
proxy 또는 복제하지 않습니다.
## 검증
앱을 실행하지 않는 검사는 built HTML·JS의 base와 URL, listener config,
Caddy matcher를 정적으로 확인합니다. Mock upstream을 사용할 때는 exact
path와 wildcard path를 모두 검사합니다.
실제 E2E는 다음을 확인합니다.
1. `/gateway`, `/che`, `/hwe` redirect
2. 각 prefix root와 deep link의 200 응답
3. hashed asset과 `/image/*`
4. tRPC batch와 error response
5. SSE 연결과 reconnect URL
6. 로그인 후 gateway→game token 인계
7. 새로고침 뒤 session·route 복구
Local proxy·mock 성공은 외부 DNS, TLS, Caddy process, host firewall와
upstream 상태를 증명하지 않습니다.
+64 -135
View File
@@ -1,142 +1,51 @@
# PM2-Orchestrated E2E Test (Gateway Orchestrator)
# Gateway orchestrator E2E
This document describes how to run a full end-to-end test using the gateway
orchestrator (PM2) so that `game-api` and `game-engine` run as real processes
and communicate over Redis.
## 검증 대상
## Goal
Gateway orchestrator는 PostgreSQL의 profile·operation 상태를 기준으로
commit worktree, build와 PM2 process를 조정합니다. 검증은 다음 경계를
나눕니다.
Validate the full process boundary:
- `app/gateway-api/test/orchestratorPlan.test.ts`: profile 상태에서 필요한
process plan 계산
- `orchestratorOperations.test.ts`: operation claim, 상태 전이와 오류
- `adminOperations.test.ts`: 관리자 API와 operation 생성
- `workspaceManager.test.ts`: source commit과 worktree
- `app/gateway-frontend/e2e/server-operations.spec.ts`: 관리자 화면
- `app/gateway-frontend/e2e/hwe-lifecycle.spec.ts`: reset/build/seed/PM2와
두 사용자 게임 진입
- gateway orchestrator starts/stops `game-api` and `turn-daemon` via PM2
- `game-api` talks to the daemon over Redis streams
- daemon publishes realtime events over Redis pub/sub
- optional mutation commands flow from API to engine and back
Unit·mock test는 실제 Git, build, PM2, PostgreSQL, Redis와 브라우저를 모두
통과한 lifecycle을 증명하지 않습니다.
## Where to Implement
## HWE lifecycle
Use the existing integration test package and add a PM2-backed test.
`hwe-lifecycle.spec.ts`는 한 관리자와 독립된 두 사용자 browser context로
다음을 실행합니다.
- Test file: `tools/integration-tests/test/orchestrator.e2e.test.ts`
- Test helpers: `tools/integration-tests/src/orchestratorHarness.ts`
- Reuse existing integration setup in `docs/integration-tests.md`
1. 관리자가 gateway에 로그인합니다.
2. `hwe:2` profile을 선택하고 full commit SHA로 reset operation을 요청합니다.
3. operation 완료와 HWE open 상태를 기다립니다.
4. 두 사용자가 각각 로그인하고 HWE로 이동합니다.
5. 각 사용자가 장수를 생성하고 game dashboard에 진입합니다.
Core integration points:
비밀번호는 ignored secret directory의 파일에서 읽습니다.
- Orchestrator entrypoint: `app/gateway-api/src/orchestrator/orchestratorServer.ts`
- PM2 manager: `app/gateway-api/src/orchestrator/pm2ProcessManager.ts`
- Process names: `app/gateway-api/src/orchestrator/gatewayOrchestrator.ts:244`
- Redis stream keys: `app/game-api/src/daemon/streamKeys.ts`, `app/game-engine/src/lifecycle/redisCommandStream.ts`
- Turn daemon status endpoint: `app/game-api/src/router/turnDaemon/index.ts`
```text
SAMMO_LIFECYCLE_SECRET_ROOT/
├─ admin_password
├─ user_a_password
└─ user_b_password
```
## Prerequisites
Secret file은 mode `0600`을 사용하고 값은 명령행·log·screenshot에 넣지
않습니다. `SAMMO_LIFECYCLE_SOURCE_COMMIT`에는 전체 commit SHA를 전달합니다.
- PM2 is installed and available on PATH.
- `pnpm --filter @sammo-ts/game-api build`
- `pnpm --filter @sammo-ts/game-engine build`
- `pnpm --filter @sammo-ts/gateway-api build` (to run orchestrator in dist)
- Postgres and Redis are running (use a dedicated DB/schema and Redis DB).
## Build 계약
Minimum environment variables for the test:
Gateway frontend:
- `DATABASE_URL` or `POSTGRES_*` (both `game-api` and `game-engine` rely on this)
- `GATEWAY_DB_SCHEMA` (gateway profile DB schema)
- `REDIS_URL`
- `GAME_TOKEN_SECRET`
- `GATEWAY_REDIS_PREFIX` (optional but recommended to isolate keys)
- `GATEWAY_WORKSPACE_ROOT` (workspace root with built `dist/`)
- `GATEWAY_WORKTREE_ROOT` (worktree root for builds; can be temp)
- `GATEWAY_TRPC_PATH` (e.g. `/gateway/api/trpc`)
- `GAME_TRPC_PATH` (e.g. `/che/api/trpc`)
- `GAME_API_EVENTS_PATH` (e.g. `/che/api/events`)
For profile alignment:
- `PROFILE` and `SCENARIO` for game server processes
- `TURN_PROFILE_NAME` should match `${PROFILE}:${SCENARIO}`
## Procedure
1) **Reset state**
- Truncate the game schema and gateway schema.
- Flush Redis (or use a dedicated Redis DB index).
2) **Seed the game DB**
- Use `seedProfileDatabase` to seed a scenario and create an admin general.
- Use a fixed `INTEGRATION_WORLD_SEED` to keep deterministic RNG.
3) **Create/Update the gateway profile record**
- Upsert a profile with status `RUNNING` (or `PREOPEN`) and a known `apiPort`.
- Ensure `profileName` is `${profile}:${scenario}`.
4) **Start the orchestrator**
- Run `app/gateway-api/dist/index.js` with `GATEWAY_ROLE=orchestrator`.
- Or instantiate `GatewayOrchestrator` directly and call `start()`.
- Wait for PM2 to report the processes as `online`.
5) **Verify API <-> daemon**
- Exchange a gateway session carrying `superuser`, `admin`, or
`admin.profiles.manage[:<profile>]` for a game access token.
- Call `turnDaemon.status` with that administrator token and expect a
non-null status.
- Send a mutation command that expects a result (e.g., `troop.join`) and
verify `commandResult` is received.
6) **Verify realtime events**
- Trigger a run via `turnDaemon.run` with the administrator token.
- Subscribe to `sammo:${profileName}:realtime:events` and wait for
`turnCompleted`.
7) **Cleanup**
- Stop orchestrator, then `pm2 delete` the processes by name:
`sammo:${profileName}:game-api` and `sammo:${profileName}:turn-daemon`.
- Flush Redis and drop the test schemas.
## What to Check
Mandatory checks:
- PM2 reports both processes as online.
- Authenticated profile administration is required for every
`turnDaemon.run/pause/resume/status` call.
- `turnDaemon.status` responds within the timeout for the administrator token.
- A command with `commandResult` returns a success response.
- A `turnCompleted` realtime event is observed after a `run` command.
Optional checks:
- `worldState.lastTurnTime` advances after a run.
- A mutation command changes DB state (e.g., troop join/exit).
## Notes
- `turnDaemon.run` is fire-and-forget; rely on status or realtime events for
confirmation.
- PM2 is global; use unique `profileName` per test run to avoid collisions.
- Orchestrator starts binaries from `dist/` under `GATEWAY_WORKSPACE_ROOT`.
If build artifacts are missing, PM2 will start and immediately exit.
## HWE GUI lifecycle
`app/gateway-frontend/e2e/hwe-lifecycle.spec.ts` verifies the operational
browser flow with one administrator and two independent user contexts:
1. The administrator logs in, selects `hwe:2`, loads scenario 2 from a fixed
commit, requests a reset, and waits for that exact operation to succeed.
2. The administrator returns to the gateway main page and sees the open HWE
action.
3. Each user logs in with a separate Chromium context, sees the same open HWE
row, creates a general, and reaches the HWE main dashboard without an error.
The test uses an ignored secret directory. It reads `admin_password`,
`user_a_password`, and `user_b_password` from
`SAMMO_LIFECYCLE_SECRET_ROOT`; passwords must not be passed on the command
line. `SAMMO_LIFECYCLE_SOURCE_COMMIT` must be a full commit SHA.
Build the gateway frontend with the public prefix contract before starting its
preview server:
```bash
```sh
VITE_APP_BASE_PATH=/gateway \
VITE_GATEWAY_API_URL=/gateway/api/trpc \
VITE_GAME_API_URL_TEMPLATE='/{profile}/api/trpc' \
@@ -144,18 +53,38 @@ VITE_GAME_WEB_URL_TEMPLATE='/{profile}/' \
pnpm --filter @sammo-ts/gateway-frontend build
```
Build the HWE frontend with `VITE_APP_BASE_PATH=/hwe`,
`VITE_GAME_API_URL=/hwe/api/trpc`, and
`VITE_GAME_SSE_URL=/hwe/api/events`. Start the isolated gateway API,
orchestrator, both previews, Postgres, and Redis, then run:
HWE frontend:
```bash
```sh
VITE_APP_BASE_PATH=/hwe \
VITE_GAME_API_URL=/hwe/api/trpc \
VITE_GAME_SSE_URL=/hwe/api/events \
pnpm --filter @sammo-ts/game-frontend build
```
Lifecycle suite는 PostgreSQL, Redis, gateway API, orchestrator, PM2와 두
frontend preview를 격리된 포트·schema·Redis prefix로 준비한 뒤 실행합니다.
```sh
SAMMO_LIFECYCLE_SECRET_ROOT=/path/to/ignored/secrets \
SAMMO_LIFECYCLE_SOURCE_COMMIT="$(git rev-parse HEAD)" \
pnpm --filter @sammo-ts/gateway-frontend test:e2e:hwe-lifecycle
```
The Playwright web server is a local prefix-preserving proxy on port `15140`.
It mirrors the Caddy route contract while keeping all navigation on one
origin. Passing the test means the actual reset/build/seed/PM2 process path and
both user creation flows completed; it is not a mocked API test.
Playwright용 prefix proxy는 `app/gateway-frontend/e2e/prefix-proxy.mjs`이며
기본 port는 `15140`입니다. 외부 Caddy를 통과하지 않으므로 suite 성공과 외부
HTTPS·host·firewall 검증을 구분합니다.
## 확인 항목
- operation의 requested/running/succeeded 또는 failed 상태
- resolved commit과 build workspace
- `sammo:<profileName>:game-api`, `sammo:<profileName>:turn-daemon` process
- game API와 daemon의 profile name 일치
- 관리자 capability와 일반 사용자 거부
- 두 사용자의 서로 분리된 session·장수 소유권
- 실패 시 secret이 없는 operation error와 PM2 log
Cleanup은 이 suite가 만든 process, worktree, schema와 Redis prefix만
대상으로 합니다. 공유 PM2 process, DB, Redis나 worktree를 이름 추정으로
삭제하지 않습니다.
+2 -2
View File
@@ -35,8 +35,8 @@ loads the following layers:
`.layout-grid` must not be promoted from a scoped SFC merely because the same
spelling appears elsewhere.
Existing feature hooks may remain while a screen is migrated, but new shared
presentation must be selected through one of the explicit shell namespaces.
Feature hooks stay inside their owning component. Shared presentation uses one
of the explicit shell namespaces.
## Consolidation rule
+3 -3
View File
@@ -41,7 +41,7 @@ screenshots.
Run the focused simulator fixture with:
```sh
pnpm --filter game-frontend test:e2e:battle-simulator
pnpm --filter @sammo-ts/game-frontend test:e2e:battle-simulator
```
The optional reference check uses `REF_BATTLE_SIM_URL`, `REF_USER_ID`, and
@@ -85,8 +85,8 @@ storage, route guards, and image loading.
The global game baseline is black, white, Pretendard 14px. Legacy texture
helpers intentionally follow `common.orig.css`: `bg0` is walnut, `bg1` is
green, and `bg2` is blue. Shared `PanelCard` uses the same walnut body and green
header so screens that still use the common component no longer inherit the
discarded Galmuri/parchment visual system.
header. Screens using the common component therefore render the walnut/green
contract with Pretendard typography.
The survey fixture covers both the poll list and an open detail. Its result
rows are visible before the current general votes when the poll uses the
+22 -14
View File
@@ -3,8 +3,8 @@ layout: home
hero:
name: core2026 핸드북
text: 구현과 플레이를 한곳에서 설명합니다
tagline: 현재 코드의 아키텍처·실행 흐름·핵심 클래스와 커맨드·시기별 이용 방법을 연결한 문서입니다.
text: 코드와 게임 동작을 연결합니다
tagline: 런타임, 저장 경계, 호환 검증과 플레이 방법을 현재 소스 구조에 맞춰 설명합니다.
actions:
- theme: brand
text: 개발자 핸드북
@@ -14,19 +14,27 @@ hero:
link: /user/
features:
- title: 코드에서 실행까지
details: frontend, tRPC, input_event, daemon, in-memory world와 PostgreSQL flush의 실제 경계를 따라갑니다.
- title: 커맨드와 시기
details: 장수·국가 커맨드 전체 목록을 소스에서 생성하고, 언제 왜 실행 가능하거나 막히는지 설명합니다.
- title: 고정된 기준선
details: 문서가 조사한 Git 기준 커밋과 재검증 지점을 밝혀 이후 리팩터링의 출발점을 남깁니다.
- title: 시스템 구조
details: gateway, game API, turn daemon, package와 PostgreSQL·Redis의 책임을 설명합니다.
- title: 요청과 상태
details: session actor부터 input_event, in-memory world와 transaction flush까지 추적합니다.
- title: ref 호환
details: 명령·RNG·상태·로그와 Chromium 화면을 같은 fixture에서 비교합니다.
---
## 문서 성격
## 문서 안내
이 사이트는 `report/`의 작업 일지가 아니라 제품 저장소 안에서 계속 갱신하는 핸드북입니다.
개발자는 [시스템 아키텍처](./developer/system-architecture.md)부터, 플레이어는
[시간과 턴](./user/time-and-turns.md)부터 읽으면 됩니다.
개발자는 [개발자 핸드북](./developer/index.md)과
[아키텍처 개요](./architecture/overview.md)에서 시작해 주세요. 플레이어는
[시간과 턴](./user/time-and-turns.md)
[커맨드 목록](./user/command-catalog.generated.md)을 확인해 주세요.
문서의 사실관계는 [기준 커밋](./reference-baseline.md)의 코드에서 확인했습니다. 기능을 변경했다면 같은
작업에서 관련 페이지와 자동 생성 커맨드 목록을 함께 갱신해 주세요.
세부 문서는 다음 책임으로 나뉩니다.
- `architecture/`: 현재 runtime, action module, scenario와 차등 검증 계약
- `developer/`: 파일 위치, 도메인 조립, 요청·저장 흐름
- `user/`: 화면, 시간, 국가 기능과 생성된 command catalog
- 루트 문서: 통합 테스트, Chromium 비교, Caddy, DB 이관과 운영 절차
작업 이력은 상위 작업공간의 `report/`에 보존합니다. ref PHP와 core2026의
구체적 대응은 상위 `../docs/ref-core2026-mapping.md`를 사용합니다.
+64 -67
View File
@@ -1,83 +1,80 @@
# Integration Tests (Initialization Flow)
# 통합 테스트
This document describes the end-to-end integration test that exercises the
gateway, game-api, and game-engine initialization flow.
## 실행 명령
## Scope
The initialization integration test validates:
- database reset and schema readiness
- bootstrap admin creation
- demo user provisioning (10 accounts)
- scenario install for `che` with `scenario_2` and 1-minute ticks
- deterministic seeding (via env)
- auto admin general creation
- general creation for demo users with constrained city selection
- reserved turn submission (uprising, founding, appointment)
- running three turns and validating founding outcomes
The test runs real Postgres and Redis, and talks to the API servers via tRPC.
## Files
- `tools/integration-tests/test/initialization.test.ts`
- `tools/integration-tests/vitest.config.ts`
## Prerequisites
- Postgres and Redis are running and reachable.
- `.env.ci` is present at the repo root and contains:
- Postgres and Redis connection settings.
- `GAME_TOKEN_SECRET`, `KAKAO_REST_KEY`, `KAKAO_REDIRECT_URI`
- `GATEWAY_BOOTSTRAP_TOKEN`
- `GATEWAY_API_HOST`, `GATEWAY_API_PORT`, `GAME_API_HOST`, `GAME_API_PORT`
- `GATEWAY_TRPC_PATH`, `GAME_TRPC_PATH`, `GAME_API_EVENTS_PATH`
- `PROFILE=che`, `SCENARIO=2`
- `pnpm install` and `pnpm build` have already completed.
## Safety
The test truncates the `public` and `che` schemas and flushes Redis. Use a
dedicated CI/local database and Redis instance.
## Running
Use the root helper script:
외부 서비스 없이 실행 가능한 integration:
```sh
pnpm test:integration
```
Or run the package script directly:
전용 PostgreSQL·Redis를 사용하는 조건부 suite:
```sh
pnpm --filter @sammo-ts/integration-tests test:integration
pnpm test:integration:conditional
```
## Environment Flags
조건부 runner는 환경 변수 존재 여부만으로 안전성을 보장하지 않습니다.
대상 host, port, database, schema와 Redis prefix가 해당 worktree 전용인지
확인해 주세요.
- `INTEGRATION_WORLD_SEED`: injected into world meta as `hiddenSeed` for
deterministic RNG.
- `INTEGRATION_JOIN_ALLOW_CITY=true`: allows test-only city selection when
creating generals (still restricted to level 5/6 cities).
## 준비
These are loaded from `.env.ci` and can be overridden per run.
상위 작업공간의 개발 stack을 사용할 수 있습니다.
The HTTP lifecycle fixtures explicitly set
`localAccountGeneralCreationGraceDays` on their disposable profile before
issuing game sessions for locally provisioned users. This keeps the test on the
same profile-policy path as production instead of bypassing Kakao eligibility in
the game API. The PM2 orchestrator fixture also uses a temporary `PM2_HOME` and
deletes all five profile roles before stopping that dedicated daemon. Cleanup
verifies the temporary daemon PID and command line, waits for its exit, and only
then removes `PM2_HOME`, so it does not share the operator's global PM2 daemon
or retain processes and paths from an older worktree.
```sh
cd ../docker_compose_files/development
./scripts/prepare-instance.sh docs-check 15443 16389 ../../core2026
./scripts/compose.sh docs-check up -d --wait
## Notes
cd ../../core2026
pnpm install --frozen-lockfile
pnpm --filter @sammo-ts/infra prisma:generate
pnpm --filter @sammo-ts/common build
pnpm --filter @sammo-ts/logic build
pnpm test:integration:conditional
```
- `auth.bootstrapLocal` only works when no users exist; the test resets the DB
to satisfy this precondition.
- `profiles.installNow` seeds the scenario and auto-creates the admin general.
- The test runs turn processing via administrator-authorized
`turnDaemon.run` and validates founding rules at the third turn.
Instance 이름과 port는 다른 worktree와 겹치지 않게 정합니다. 생성된 `.env`,
`.env.ci`, log와 DB volume은 Git에 추가하지 않습니다.
HTTP lifecycle fixture는 disposable profile에
`localAccountGeneralCreationGraceDays`를 명시하고 실제 profile 정책을
통과합니다. PM2 orchestrator fixture는 전용 `PM2_HOME`을 사용하며, 다섯
runtime role을 삭제하고 PID와 명령행 및 daemon 종료를 확인한 뒤에만 임시
디렉터리를 정리합니다. 운영자의 전역 PM2 daemon과 경로를 공유하지 않습니다.
## 범위
통합 suite는 다음 경계를 포함합니다.
- gateway/game Prisma 연결과 schema
- Redis session, queue와 pub/sub
- scenario initialization과 profile schema
- `InputEvent` 원자성, 재시도와 중복 request
- turn daemon lease, heartbeat, fencing과 takeover
- 예약 턴 revision/CAS와 API/daemon 경합
- auth header, role, sanction과 owner별 HTTP transport
- ref/core command snapshot, RNG trace와 persistence
- auction, tournament와 worker transaction
실제 포함 suite는 `tools/run-conditional-integration.sh`, 각 package의
`package.json`, `*.integration.test.ts`를 기준으로 확인합니다.
## 안전 경계
- Test는 game schema table을 truncate할 수 있습니다.
- Redis key 또는 선택한 DB index를 정리할 수 있습니다.
- 운영·공유 DB URL을 사용하지 않습니다.
- Volume 삭제는 사용자가 데이터 폐기를 명시한 경우에만 수행합니다.
- Secret과 펼쳐진 `docker compose config` 출력은 report나 artifact에 넣지
않습니다.
## 결과 해석
환경이 없어 skip된 test는 실행되지 않은 것입니다. Mock connector test는 실제
PostgreSQL·Redis 경계를 증명하지 않습니다. Full suite 실패는 변경 worktree와
변경 없는 `main`에서 각각 재현해 회귀와 baseline을 구분합니다.
Ref 호환성 판정은 [차등 검증](architecture/turn-state-differential-testing.md),
UI는 [프론트엔드 호환 검증](frontend-legacy-parity.md)을 함께 사용합니다.
+5 -10
View File
@@ -10,8 +10,8 @@ uses a stable legacy key with `ON CONFLICT`, so an interrupted run is
repeatable.
The source of truth for eligibility is the checked ref schema, not every table
that happens to exist in an old dump. An extra root `config` table found in the
2026-07-27 dump is therefore retained only in the recovery dump.
that happens to exist in a dump. Tables outside that schema remain only in the
recovery dump.
### Gateway
@@ -29,11 +29,9 @@ that UUID, so references such as `ng_old_generals.owner` remain stable even
when an old account was deleted before the dump.
Kakao members retain `oauth_id`, email and metadata. Cutover sets
`kakao_verified_at` and `kakao_grace_started_at` to the migration time, starting
the current verification policy from the migration instead of treating an old
Kakao login as unverified. Five source Kakao rows have no OAuth ID; their
metadata is retained for audit, but an absent provider identifier is not
invented.
`kakao_verified_at` and `kakao_grace_started_at` to the migration time and starts
the verification grace period there. Source rows without an OAuth ID retain
their metadata, but the importer does not invent a provider identifier.
Legacy password hashes remain usable when gateway-api has
`GATEWAY_LEGACY_PASSWORD_GLOBAL_SALT`; a successful login upgrades the stored
@@ -115,6 +113,3 @@ browser.
8. Retain the MariaDB dumps as rollback evidence. Rollback restores the
pre-cutover PostgreSQL backup; it does not reverse individual importer
upserts.
The supplied dump inventory has data for `che`, `hwe` and `root`. The `kwe`
directory is empty, so there is no KWE dataset to import or validate.
-39
View File
@@ -1,39 +0,0 @@
# 문서 기준선
## 코드 기준 커밋
이 핸드북의 최초 전면 갱신은 다음 상태를 기준으로 조사했습니다.
| 항목 | 값 |
| ----------- | ------------------------------------------ |
| 저장소 | `devsam/core2026.git` |
| 브랜치 | `main` |
| 기준 커밋 | `1181f6f4e03cbed77b1c40b6b572585f6e395a2c` |
| 기준 일자 | 2026-07-28 |
| 레거시 위치 | 형제 저장소 `../ref/sam``devel` |
기준 커밋은 “이 버전이 완성됐다”는 선언이 아니라 문장과 소스 경로를 다시 대조할 출발점입니다. 이후
리팩터링에서 설명과 코드가 어긋나면 다음 순서로 갱신해 주세요.
1. 이 페이지의 기준 커밋과 현재 `main` 사이의 변경 파일을 확인합니다.
2. 해당 문서가 가리키는 entry point, 호출 순서, transaction과 오류 경로를 다시 추적합니다.
3. 커맨드 등록부를 바꿨다면 `pnpm docs:generate`로 생성 페이지의 차이를 확인합니다.
4. `pnpm docs:build`로 링크와 정적 HTML 생성을 검증합니다.
5. 기준 커밋과 기준 일자를 현재 조사한 commit으로 바꾸고, 검증하지 못한 범위를 명시합니다.
## 사실 수준
- 이 핸드북의 아키텍처·파일·클래스 설명은 기준 커밋의 정적 코드와 기존 검증 문서를 교차 확인한
결과입니다.
- `input_event` transaction, daemon lease, ref 차등, 실제 Chromium 같은 동작 증명은 각 테스트와
기존 상세 문서가 담당합니다. 이 핸드북 자체의 HTML 빌드가 제품 동작을 다시 증명하지는 않습니다.
- profile과 scenario가 명령 목록·상수·맵·병종을 바꿀 수 있으므로 플레이어 화면의 현재 가능 여부가
정적 표보다 우선입니다.
## 상세 근거 문서
- [레거시 이관 제약](./architecture/rewrite-constraints.md)
- [턴 daemon lifecycle](./architecture/turn-daemon-lifecycle.md)
- [PostgreSQL schema](./architecture/postgres-schema.md)
- [프론트엔드 CSS 구조](./frontend-css-architecture.md)
- [레거시 화면 비교](./frontend-legacy-parity.md)
-161
View File
@@ -1,161 +0,0 @@
# Test Suite Audit
## Baseline and method
- Audit date: 2026-07-25
- Code baseline: `main@46ae79dbe7a0fb64aff9bdcc76eadafd75e10c9e`
- Executed test sources: 67 TypeScript `*.test.ts` files
- Excluded from the source count: ignored `dist/` outputs and non-executable
fixtures/helpers
- Historical generated copies removed by this audit:
`packages/common/test/*.{js,js.map,d.ts,d.ts.map}` (12 files)
Each suite was read together with its production entry point. Assertions were
classified by what they can actually establish:
- `contract`: focused input/output, rejection, ordering, or side-effect
regression with concrete assertions.
- `compatibility`: expected values or traces are independently grounded in the
PHP reference implementation.
- `integration`: real database, Redis, process, or HTTP behavior; environment
gating must be reported as a skip rather than a pass.
- `smoke`: a broad scenario that proves only the stated liveness/invariant
bounds. It is useful, but is not compatibility evidence.
`kept` means the suite's current scope and assertions match its stated role.
`corrected` means this audit changed a misleading, non-deterministic, empty, or
environment-dependent check.
## Per-suite disposition
### `packages/common`
| Test source | Disposition | Layer | What it establishes |
| ------------------------ | ----------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `test/clock.test.ts` | kept | contract | Manual and stepped clock advancement plus invalid step rejection. |
| `test/rng.test.ts` | corrected | compatibility | PHP/Python SHA-512 vectors, byte/bit consumption, integer and weighted selection, explicit boundary outputs, and a genuinely expanded long seed. Removed three assertion-free checks. |
| `test/test-rngs.test.ts` | kept | contract | Exact output sequences for deterministic test RNG implementations. |
### `app/gateway-api`
| Test source | Disposition | Layer | What it establishes |
| ------------------------------- | ----------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `test/authFlow.test.ts` | kept | contract | OAuth-backed registration, gateway session issuance, encrypted game token issuance, and successful validation through the router. Token storage/Redis TTL remains integration scope. |
| `test/orchestratorPlan.test.ts` | kept | contract | Exact start/stop/no-op reconciliation decisions for every profile status represented by the planner. |
| `test/scenarioCatalog.test.ts` | kept | contract | Git ref resolution and preview loading from the checked-out repository. |
### `app/game-api`
| Test source | Disposition | Layer | What it establishes |
| --------------------------------------------- | ----------- | -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `test/battleSimProcessor.test.ts` | corrected | contract + compatibility support | Fixed-seed numeric battle summary, skill activation, exact defender order, and unknown crew handler rejection. Detailed PHP parity remains in `battleDifferential.test.ts`. |
| `test/battleSimRouter.test.ts` | kept | contract | Queued-to-completed transport state flow through the battle router. |
| `test/commandTable.test.ts` | kept | contract | Minimum constraints, rather than full command constraints, determine command-table availability. This is intentionally a focused positive regression. |
| `test/idempotentTransport.test.ts` | kept | contract | Stable ordered child request IDs derived from one API input event. |
| `test/inputEventBoundary.integration.test.ts` | kept | integration | PostgreSQL atomic commit/rollback, retry, concurrent duplicate rejection, and child payload conflict. Explicitly skipped without `INPUT_EVENT_DATABASE_URL`. |
| `test/realtimeSse.test.ts` | kept | contract | Exact SSE framing, multiline encoding, event parsing rejection, and profile channel namespace. |
| `test/reservedTurns.test.ts` | kept | contract | General/nation queue padding and shift direction. Database constraints are outside this in-memory fake. |
| `test/router.test.ts` | kept | contract | Turn-daemon dispatch and serialization of world/status/reserved-turn responses through tRPC callers. |
| `test/streamKeys.test.ts` | kept | contract | Exact Redis stream namespace contract. |
| `test/tournamentWorker.test.ts` | corrected | contract | Fixed-seed bracket winner and final energies, payout command data, auto-join filtering, NPC/dummy fill, and completion. Replaced winner-exists-only coverage for the fixed bracket. |
### `app/game-engine`
| Test source | Disposition | Layer | What it establishes |
| ----------------------------------------------- | ----------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `test/crewTypeExecution.test.ts` | kept | compatibility | Crew action router ordering and legacy NPC arm-type weighting. |
| `test/databaseCommandQueue.integration.test.ts` | kept | integration | PostgreSQL single claim across consumers, persisted result, and expired-versus-active lease recovery. Explicitly skipped without a DB URL. |
| `test/generalAiLegacyDecisionParity.test.ts` | added | compatibility | Ref-backed final NPC command matrix across diplomacy, war readiness, city development/population, technology ceilings, gold/rice and casualty ranks, stats/affinity, special NPC state, treasury reserves, and command availability. |
| `test/inputEventAtomicity.test.ts` | kept | contract | Dirty-state retention, mutation/commit/respond ordering, and pause/no-ack behavior on handler or commit failure. |
| `test/monthlyCatalogCoverage.test.ts` | added | compatibility | Exact 29-action ref catalog coverage, dependency-safe segment assignment, executable core evidence files, and explicit multi-month/no-op/error dispositions. Behavioral claims remain in the referenced PostgreSQL/ref fixtures. |
| `test/nationCollapseOnConquest.test.ts` | kept | smoke | Last-city conquest removes the nation and neutralizes its general. It is a focused engine scenario, not full battle parity. |
| `test/nationTurnCompatibility.test.ts` | kept | compatibility | Legacy-backed 12-turn research accumulation and completion, diplomacy-message emission, and exact monthly strategy/diplomacy limit decay through the engine harness. |
| `test/npcGeneralDomesticTurn.test.ts` | corrected | contract | Fixed seed chooses the security command, applies exactly `+50`, leaves other city values unchanged, and advances exactly one tick. |
| `test/npcNationGrowthScenario.test.ts` | corrected | smoke | Long-running NPC growth invariants and collapse guards. The implementation-specific early recruitment bound was removed because legacy AI forbids peace/declaration recruitment; remaining thresholds are smoke bounds. |
| `test/npcNationTechResearch.test.ts` | corrected | smoke | Long-running monotonic tech growth. Net per-tick general gold is not treated as recruitment price because nation awards can occur in the same tick. |
| `test/npcNationUprisingUnification.test.ts` | corrected | smoke | Runs from 181-08 through at most 260-01 and covers multi-nation founding, declaration, live sortie, conquest, monotonic nation-count decline, and the no-orphan-city invariant. Terminal unification is asserted when reached; the focused war scenario below guarantees the terminal path. |
| `test/npcNationWarDeclaration.test.ts` | corrected | smoke | Declaration, war transition, a battle-ready cohort, front deployment, conquest, unification, and dispatch occurrence without assuming every recruit is already fully trained. |
| `test/npcWarPrepTurns.test.ts` | kept | smoke | Training/morale actions occur in the named months and leave battle-ready generals. |
| `test/reservedTurnExecution.test.ts` | kept | smoke | Multi-command engine application, invalid-argument and constraint fallbacks, uprising/founding transitions, and named failure constraints. Its large workflow scope is recorded as smoke rather than a unit test. |
| `test/scenarioSeeder.test.ts` | kept | integration | Real schema row counts, diplomacy symmetry, and persisted install options. Reported as skipped when required tables are unavailable. |
| `test/turnDaemonLease.integration.test.ts` | kept | integration | PostgreSQL profile lease exclusivity, expiry takeover with epoch increment, stale-owner fencing rollback, and clean release handoff. Explicitly skipped without a DB URL. |
| `test/turnDaemonLifecycle.test.ts` | kept | contract | Queue-front versus scheduled-boundary selection and exact processor checkpoint arguments. |
| `test/turnOrder.test.ts` | kept | contract | Stable `turnTime`, then ID, ordering independent of insertion order. |
| `test/uniqueLotteryCommand.test.ts` | kept | contract | Fixed-seed eligible command awards the expected unique item and item log. |
| `test/voteReward.test.ts` | corrected | contract | Gold, exact unique item, persisted reward metadata, log, and idempotent second application. |
### `packages/logic`
| Test source | Disposition | Layer | What it establishes |
| -------------------------------------------------------- | ----------- | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `test/actions/instant/nationStopWar.test.ts` | kept | contract | Invalid diplomacy rejection and symmetric neutral diplomacy patches. |
| `test/actions/turn/executionHelper.test.ts` | kept | contract | Successful resolution, fallback loading/log accumulation, and fallback loop termination. |
| `test/actions/turn/nation.test.ts` | corrected | compatibility + contract | Declaration effects/logs, non-neighbor denial, capital/name/city effects, and exact legacy `필사즉생` gains (`+15`, train/morale 100, global delay 9). |
| `test/actions/turn/nationMissing.test.ts` | kept | compatibility + contract | Legacy-backed rejection, message, cooldown, assistance accumulator, and state/resource/meta effects for represented migrated nation commands. Exact full PHP snapshots remain outside this suite. |
| `test/crewType.test.ts` | kept | contract | Unit-set normalization and tech/region/city availability requirements. |
| `test/crewTypeExecution.test.ts` | kept | compatibility | Shipped catalog compilation, unknown handlers, crew action routing, legacy triggers, and numeric pick-score formula. |
| `test/diplomacy.test.ts` | kept | compatibility | Exact monthly declaration/war/non-aggression transitions, casualty extension, and reset behavior. |
| `test/dispatchWarAction.test.ts` | kept | contract | Dispatch emits battle logs and patches for defender general, city, and diplomacy casualty state. Numeric battle parity is delegated to the differential suite. |
| `test/itemInventory.test.ts` | kept | contract | Legacy-slot upgrade, frozen-snapshot reads, exact consumable charge depletion, final unequip, and persisted metadata round-trip for canonical item instances. |
| `test/message.test.ts` | kept | contract | Private sender/receiver copies, national/public single-copy rules, and sender action sanitization. |
| `test/scenarioParser.test.ts` | corrected | contract | Defaults and representative legacy-format rows from tracked `resources/scenario`; removed dependence on an untracked local `legacy/` directory. |
| `test/scenarios/blankStart.test.ts` | kept | smoke | Uprising-to-appointment-to-founding workflow and named founding rejections. |
| `test/scenarios/diplomacy.test.ts` | corrected | contract | Declaration is denied for deployment, then the identical command becomes allowed only after transition to WAR. Removed the previous false check that inspected an always-empty troop list while bypassing constraints. |
| `test/scenarios/domestic.test.ts` | corrected | contract | Representative domestic action state changes and capacity behavior, including the exact `+100` agriculture effect, through the scenario harness. |
| `test/scenarios/general/che_NPC능동.test.ts` | corrected | contract | NPC teleport, non-NPC resolver rejection, and structural argument parsing. Replaced an empty always-passing test. |
| `test/scenarios/general/che_강행.test.ts` | kept | contract | Exact move/cost/experience effects, far-city denial, and wandering subordinate movement. |
| `test/scenarios/general/che_귀환.test.ts` | kept | contract | Capital/officer-city return effects and named location constraints. |
| `test/scenarios/general/che_등용수락.test.ts` | kept | contract | Neutral acceptance, betrayal resource/penalty effects, and monarch/same-nation denials. |
| `test/scenarios/general/che_이동.test.ts` | kept | contract | Exact movement, gold, morale, experience, and leadership-experience effects. |
| `test/scenarios/general/migratedGeneralCommands.test.ts` | kept | contract | Focused state transitions for mutiny, abdication, gift, disband, founding, and duplicate equipment purchase. |
| `test/scenarios/general_commands_new.test.ts` | kept | smoke | Multi-command resolver workflow for procurement/donation/status and employment/sabotage families, including exact one-use strategy-item consumption. Other broad assertions remain workflow smoke only. |
| `test/scenarios/reservedTurnExecution.test.ts` | kept | smoke | Multiple generals consume queued commands over three ticks and produce representative city/general changes. |
| `test/scenarios/troops.test.ts` | corrected | contract | Draft, train, and morale workflow with exact crew, training, and morale results through three commands. |
| `test/specialActions.test.ts` | kept | contract | Trait loading plus exact domestic/stat/heal/battle modifiers. |
| `test/uniqueLottery.test.ts` | kept | contract | Fixed-seed item, eligible acquisition types, founding guarantee, and occupied-item counting. |
| `test/warAftermath.test.ts` | kept | contract | Exact tech/death deltas and conquest/collapse resource/general/city effects. |
| `test/warEngine.test.ts` | kept | contract | Critical trigger multiplier, no-rice defender rout, and exact multi-use/one-use battle-item charge persistence. Detailed phase/RNG parity is differential-test scope. |
| `test/world/distance.test.ts` | kept | contract | Exact shortest distances, range search, and unreachable city handling. |
| `test/worldBootstrap.test.ts` | kept | contract | Scenario/map conversion into exact snapshot and seed fields. |
### `tools/integration-tests`
| Test source | Disposition | Layer | What it establishes |
| --------------------------------------------------- | ----------- | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `test/auctionFlow.test.ts` | kept | integration | Real API/PostgreSQL/Redis/daemon resource and unique-auction bidding, extension, finalization, payout, and ownership constraints. |
| `test/battleDifferential.test.ts` | kept | compatibility | PHP reference metadata and attacker/defender comparisons for all 145 scenario items, plus event order, RNG results, phase values, multi-defender, siege, and no-defender branches. The two exhaustive item cases currently fail on 13 attacker and 2 defender items; this is active compatibility evidence, not a green regression. |
| `test/initialization.test.ts` | kept | integration | Real gateway/game APIs, database/Redis reset, scenario seed, users, joins, turns, and founding state. |
| `test/orchestrator.e2e.test.ts` | kept | integration | Real PM2 game API/daemon startup, command serving, SSE completion, and persisted world update. |
| `test/turnSnapshotComparator.test.ts` | kept | contract | Canonical entity ordering, exact changed paths, numeric before/after delta equivalence, and live-conquest nation-deletion mismatch detection. |
| `test/turnSnapshotCoreDatabase.integration.test.ts` | kept | integration | Real PostgreSQL projection plus before/execute/after capture around a transaction boundary. Explicitly skipped without a DB URL. |
| `test/turnSnapshotReference.integration.test.ts` | kept | integration | Read-only canonical projection from the isolated PHP/MariaDB reference service. Explicitly enabled with `TURN_DIFFERENTIAL_REFERENCE=1`. |
| `test/turnCommandReference.integration.test.ts` | kept | compatibility harness | Disposable cloned-MariaDB execution of real legacy nation declaration and live sortie through conquest and nation collapse. Explicitly enabled with `TURN_DIFFERENTIAL_REFERENCE=1`. |
| `test/turnTraceFiles.integration.test.ts` | kept | compatibility harness | Saved ref/core command identity, RNG sequence and semantic delta comparison. It is skipped until both independently executed trace files are supplied. |
## Shared test support
The following support files were also reviewed because failures in them can
invalidate many suites:
| Support source | Result |
| ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `packages/logic/test/testEnv.ts` | corrected: replaced `Math.random()` with a complete fixed-seed `RandUtil`; missing general IDs now fail setup instead of silently skipping a command. |
| `packages/logic/test/fixtures/minimalMap.ts` | kept: small connected/unreachable map fixture used by focused command tests. |
| `app/game-engine/test/fixtures/largeTestMap.ts` | kept: broad NPC smoke fixture; not a legacy map snapshot. |
| `app/game-engine/test/helpers/turnTestHarness.ts` | kept: engine smoke harness; assertions remain in individual suites. |
## Trust boundary after this audit
This audit establishes that every executable test has a stated, non-empty
purpose and that broad simulations are labeled as smoke rather than silently
treated as exact compatibility proof. It does not promote all product behavior
to legacy-compatible:
- battle compatibility is supported only by the differential fixtures and the
suites explicitly marked `compatibility`;
- broad NPC and multi-command scenarios guard liveness/invariants, while
`generalAiLegacyDecisionParity.test.ts` establishes exact final choices only
for its explicitly represented input branches;
- DB/Redis/PM2 claims require their integration suites to run rather than skip;
- changing an implementation and observing a green unit suite is still not a
substitute for a new PHP trace when compatibility-sensitive behavior changes.
+71 -89
View File
@@ -1,115 +1,97 @@
# Testing Policy (Draft)
# 테스트 정책
This document summarizes the testing strategy for the Sammo/HiDCHe repository.
The key is to separate "DB behavior emulation" from "state/logic/flow validation"
so each layer is tested with the right scope.
## 원칙
## Historical Test Trust Boundary (2026-07-25)
- 변경한 계약과 가장 가까운 test부터 실행하고 범위를 넓힙니다.
- deterministic clock, seed와 fixture를 사용합니다.
- ref 호환성은 결과, 순서, RNG, 저장 상태와 출력으로 검증합니다.
- 인증·권한은 session actor, 소유권, role, sanction과 redaction을 포함합니다.
- DB 코드는 실제 PostgreSQL transaction과 reload 경로를 검증합니다.
- UI는 실제 Chromium에서 geometry, computed style과 interaction을 비교합니다.
- skip, baseline failure와 환경 미검증을 pass와 분리합니다.
Tests already present at the 2026-07-25 review baseline
(`main@a05f46130e621ff30fcac330af33c9e7e169f440`) or inherited from an older
commit are **untrusted legacy tests until individually audited**. A large part
of this suite was AI-authored, and some tests appear optimized to make the
current implementation pass rather than to detect incorrect behavior.
## 기본 검증
Consequently:
```sh
CI=1 pnpm typecheck
pnpm lint
pnpm test
pnpm build
```
- A passing baseline test proves only that its current assertions passed. It
does not by itself prove legacy compatibility, domain correctness, complete
side effects, authorization, transaction safety, or production readiness.
- Do not raise a mapping or implementation status to "verified" using only an
unaudited baseline test.
- Do not preserve behavior merely because an unaudited test expects it. Resolve
conflicts against the legacy implementation, an approved compatibility
contract, or an independently captured trace.
- Keep existing tests for possible regression value, but mark each audited test
or suite with its evidence source and review date.
문서만 변경한 경우에도 Markdown link, Prettier, 생성 문서 일치와
`pnpm docs:build`를 확인합니다. 제품 코드 동작을 바꾸지 않은 문서 작업은
typecheck·unit·Chromium 검증을 실행한 것으로 설명하지 않습니다.
An audit must confirm that a test:
## 테스트 계층
1. names the behavior and failure mode it is intended to protect;
2. derives expected values independently from the implementation under test;
3. asserts game-impacting numeric state and all relevant persistence side
effects, rather than only truthiness, types, or absence of exceptions;
4. covers important rejection, boundary, ordering, and concurrency paths;
5. fails when the protected behavior is deliberately perturbed;
6. uses a real DB/Redis or a differential legacy fixture where infrastructure
semantics or compatibility are part of the contract.
### Unit
Until this audit is recorded, test counts and green status are inventory
information only, not evidence of correctness.
`packages/common`, `packages/logic`, 각 app의 `test/`가 순수 계산, parser,
constraint, lifecycle과 service를 검증합니다. Gameplay 계산에는 fixed seed와
정확한 state/log 기대값을 사용합니다.
The executable TypeScript suite was reviewed on 2026-07-25. Its per-suite
disposition, intended scope, and evidence limits are recorded in
[`test-suite-audit.md`](./test-suite-audit.md). A `kept` or `corrected`
disposition means the test has useful regression value at the recorded layer;
it does not promote smoke or contract coverage into legacy-compatibility
evidence. New suites and material scope changes must update that registry.
### Integration
## Core Principles
`tools/integration-tests``*.integration.test.ts`가 PostgreSQL, Redis,
Fastify transport, Prisma transaction, lease와 worker 경계를 검증합니다.
- Prefer Repository/DB Port interfaces over ORM mocks; split implementations:
- InMemory Repository (Fake)
- Real DB Repository (e.g., Prisma)
- Validate domain constraints in the logic layer where possible; DB constraints
act as a secondary safety net.
- Use deterministic seeds for all gameplay-impacting RNG.
- Run TypeScript validation with the workspace-standard TypeScript `6.0.2`.
TypeScript 5 or 7 results do not replace the required repository typecheck.
See `docs/architecture/typescript-version.md`.
```sh
pnpm test:integration
pnpm test:integration:conditional
```
## Test Layers
조건부 suite는 worktree별 PostgreSQL·Redis instance를 준비합니다. Test가
schema truncate와 Redis 정리를 수행하므로 공유 개발 instance를 사용하지
않습니다.
### 1) Constraint Tests
### Differential
Goal: ensure constraints (unique/foreign key/check) behave consistently in
both InMemory state and the DB.
```sh
pnpm check:legacy:general
pnpm check:legacy:nation
```
- Unit tests: validate domain constraints with InMemory repositories.
- Integration tests: verify the same violations fail in the real DB.
- Mock target: InMemory Repository (Fake).
- Use real DB only in integration tests.
실제 ref runner와 canonical snapshot 계약은
[차등 검증](architecture/turn-state-differential-testing.md)을 따릅니다.
### 2) Game Logic / Command Tests
### Browser
Goal: verify state input -> state output and that flush behaves as expected.
```sh
pnpm test:e2e:frontend-legacy
```
- Unit tests: run logic against InMemory state and assert outputs.
- Integration tests: execute the same command and confirm DB persistence.
- Mock target: InMemory Repository (Fake).
- "Send to DB" behavior is validated via real DB tests.
- Cross-engine compatibility compares a canonical semantic snapshot rather than
raw MariaDB/PostgreSQL dumps. General-turn commands use the three-way
ref DB ↔ core InMemory ↔ core PostgreSQL design in
[`architecture/general-command-differential-testing.md`](./architecture/general-command-differential-testing.md).
같은 Chromium, viewport, device scale, zoom, locale, font, image, 로그인과
test data를 사용합니다. Screenshot과 함께 `getBoundingClientRect()`
`getComputedStyle()`을 수집하고 hover, focus, pointer down/up,
checked/selected/disabled, dropdown, modal과 transition을 확인합니다.
### 3) Turn Flow Tests
## 권한 matrix
Goal: verify the end-to-end scheduler/turn processing flow.
API 변경은 최소한 다음 행위자를 구분합니다.
- Nature: integration/system tests.
- Stack: Real DB + (test) Redis.
- RNG uses fixed seeds for determinism.
- Keep a small set of smoke/regression scenarios due to cost.
- 무인증
- 인증됐지만 장수가 없는 사용자
- 자기 장수
- 같은 국가의 다른 장수
- 외국 장수
- NPC
- 직책·서비스 role별 사용자
- sanction 대상 사용자
### 4) Scenario Build Tests
거부 경로는 DB·queue·Redis side effect가 없어야 합니다. Public DTO는 숨겨야
할 field가 빠졌는지 확인하고, role 이름만 비교하지 말고 실제 capability와
resource relation을 검증합니다.
Goal: ensure scenario parsing and DB application are correct.
## DB와 migration
- Unit tests: parse/validate scenario files (map size, ID collisions, ranges).
- Integration tests: load scenarios into a test DB and verify key tables.
Schema 변경은 빈 DB 전체 migration, 기존 DB 증분 migration, 두 번째 deploy의
no-op, unique/FK/index, runtime query와 reload를 확인합니다. 기존 migration
파일과 checksum은 수정하지 않습니다. `prisma db push`는 격리 fixture의
일시적 schema 준비에만 사용합니다.
## Result Composition Guidelines
## 결과 기록
- Unit tests: fast, wide coverage.
- Integration tests: focus on real DB/Redis behavior.
- Smoke tests: minimal coverage of turn flow/build/scenario loading.
- Test-specific Rule Relaxations:
- To simplify mocking and complex state preparation, the use of `any` type is allowed in test files.
- Type casting tricks like `as unknown as YourType` are also permitted in test code for convenience.
- However, test code must still be covered by TypeScript type checking to ensure API consistency and prevent regressions.
## MockDB Conclusion
- "InMemory Repository (Fake)" is more practical than an ORM mock.
- DB-level behavior (constraints/transactions/locks) belongs to integration tests.
Report에는 실행 명령, commit, 서비스와 fixture, pass/fail/skip 수, baseline
failure, 미검증 경계와 artifact 위치를 기록합니다. Screenshot과 log에는
token, password, 개인정보를 포함하지 않습니다.
+22 -24
View File
@@ -1,40 +1,38 @@
# Game database migrations
# Game database migration
`game.prisma`의 운영·검증 DB는 이 디렉터리의 migration chain으로
준비한다. 새 환경을 `prisma db push`만으로 만들지 않다.
`game.prisma`의 운영·검증 database는 이 디렉터리의 migration chain으로
준비합니다. `prisma db push`는 정식 migration을 대신하지 않습니다.
## 적용
Git에서 제외된 환경 파일 또는 secret 주입으로 `DATABASE_URL`을 설정한 뒤
저장소 루트에서 실행한다.
Git에서 제외된 환경 파일 또는 secret 주입으로 `DATABASE_URL`을 설정합니다.
```bash
```sh
pnpm --filter @sammo-ts/infra prisma:migrate:deploy:game
pnpm --filter @sammo-ts/infra prisma:migrate:status:game
```
`20250101000000_init_game_schema`는 증분 migration 이력이 생기기 전에
`db push`로 제공되던 핵심 game table의 baseline이다. 기존 설치에는
이미 같은 object가 있으므로 type, table과 index 생성은 idempotent하게
작성했다. 새 migration은 이 baseline 또는 기존 migration 파일을
수정하지 말고 뒤에 추가한다.
`20250101000000_init_game_schema`가 game schema의 baseline입니다. Baseline과
적용된 migration 파일·checksum은 수정하지 않고 새 timestamp migration을
추가합니다.
## 빈 DB 검증
다른 개발 DB나 volume을 재사용하지 말고 전용 임시 PostgreSQL database를
만든다. 위 deploy 명령을 한 번 실행해 전체 migration이 성공하는지,
두 번째 실행이 `No pending migrations to apply`인지 확인한다. 최소한
다음도 확인한다.
전용 임시 PostgreSQL database에 deploy를 두 번 실행합니다. 첫 실행은 전체
chain을 적용하고 두 실행`No pending migrations to apply`여야 합니다.
최소 확인 항목은 다음과 같습니다.
- `_prisma_migrations`의 모든 행이 완료 상태
- `world_state`, `nation`, `city`, `general`, `message`, `troop`,
`general_turn`, `nation_turn`, `diplomacy`, `event`, `log_entry`,
`error_log` 존재
- `nation.chief_general_id` 존재
- `world_state`, `nation`, `city`, `general`, `message`, `troop`
- `general_turn`, `nation_turn`과 revision·lease field
- `input_event`, `turn_daemon_lease`
- `diplomacy`, `event`, `log_entry`, `error_log`
- auction, board, vote, yearbook, archive와 inheritance table
- `nation.chief_general_id`
- `city.trade` nullable, `city.trust` REAL
- `auction_bid.meta` JSONB, NOT NULL
- `traffic_period`, `traffic_period_general` 존재 및
`(world_state_id, year, month)`/`(period_id, general_id)` unique key
- `auction_bid.meta` JSONB NOT NULL
- `traffic_period`, `traffic_period_general`과 unique key
검증 뒤에는 이름을 확인한 임시 database와 role만 제거다. 공유 개발
database나 Compose volume을 삭제하지 않다.
검증이 끝나면 이름을 직접 확인한 임시 database와 role만 제거합니다. 공유
database나 Compose volume을 삭제하지 않습니다.
+3 -3
View File
@@ -38,9 +38,9 @@ After reviewing the JSON counts and excluded-table reasons, add
`GATEWAY_DATABASE_URL` or `GAME_DATABASE_URL` and repeat with `--apply`.
Kakao members retain their OAuth ID, email, and OAuth metadata.
`kakao_verified_at` and `kakao_grace_started_at` are set to the migration time,
which renews verification at cutover. Legacy password hashes and salts are
retained and upgraded to Argon2id after the first successful login when
`kakao_verified_at` and `kakao_grace_started_at` are set to the migration time.
Legacy password hashes and salts are retained and upgraded to Argon2id after
the first successful login when
`GATEWAY_LEGACY_PASSWORD_GLOBAL_SALT` is configured in gateway-api.
Only tables present in the checked ref schemas are eligible. Extra tables found