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:
@@ -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을 생략하지 말아 주세요.
|
||||
|
||||
@@ -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 경로에서 구성합니다.
|
||||
|
||||
@@ -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: '현재 구현을 설명하는 문서입니다. 코드와 검증 범위를 함께 확인해 주세요.',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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
|
||||
경계에 결과를 전달합니다.
|
||||
|
||||
- 장비 판매는 판매 대금 반영 → 판매 이벤트 → 슬롯 제거 순서입니다.
|
||||
|
||||
@@ -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 통과만으로 일반 장수 명령
|
||||
전체를 `확인` 또는 이식 완료로 표시하지 않는다.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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 command’s 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 command’s 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 (1–5% 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 95–105 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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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`
|
||||
@@ -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 20–50% 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 2–8 except 5) can auto-queue `che_임관` to the
|
||||
attacker nation with a random delay (0–12 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 (20–50%).
|
||||
- Scout message chance and NPC auto-join chance.
|
||||
- Randomized join turn delay (0–12).
|
||||
|
||||
## `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.
|
||||
@@ -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`
|
||||
@@ -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`
|
||||
@@ -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`.
|
||||
@@ -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는 약
|
||||
464–467 MiB였다. 따라서 이 실행에서 RSS 대부분을 savepoint payload
|
||||
자체가 설명하지는 않는다.
|
||||
|
||||
운영 통일 handler는 `isUnited`와 history를 memory에 반영한 뒤 inheritance,
|
||||
hall-of-fame, dynasty PostgreSQL 정산 세 개를 await하지 않고 시작한다.
|
||||
이 프로파일은 통일 판정까지의 engine memory를 측정하지만 그 비동기 정산의
|
||||
완료, 실패 복구, 메모리 또는 distributed atomicity는 검증하지 않는다.
|
||||
그 경로를 관찰하려면 격리 PostgreSQL fixture와 정산 완료 barrier 또는
|
||||
durable outbox가 별도로 필요하다.
|
||||
@@ -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`를 사용합니다.
|
||||
현재 코드 경로가 문서의 근거이며, 보고서의 완료 표현만으로 구현 상태를
|
||||
판정하지 않습니다.
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
@@ -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의 별도 파일 시스템 경로입니다.
|
||||
|
||||
@@ -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` 값이 같은지 확인해 주세요.
|
||||
|
||||
@@ -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.
|
||||
@@ -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의 재현 정보
|
||||
|
||||
@@ -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 한 곳에만 적용하지 않습니다.
|
||||
|
||||
@@ -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
@@ -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에 기록합니다.
|
||||
|
||||
@@ -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
@@ -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 set과 command 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와 오류 경로를 함께 갱신해 주세요.
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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
@@ -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
@@ -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를 이름 추정으로
|
||||
삭제하지 않습니다.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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)을 함께 사용합니다.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
@@ -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
@@ -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, 개인정보를 포함하지 않습니다.
|
||||
|
||||
@@ -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을 삭제하지 않습니다.
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user