독자 수준별 구조 문서와 게이머 입문·전술 안내를 재정비한다
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
# 파일 지도
|
||||
|
||||
먼저 증상이 보이는 화면이나 동작 하나를 고르고, 아래 시작점에서 호출을 따라가세요.
|
||||
폴더 이름만으로 저장 책임이나 권한을 추측하지 않습니다. 읽는 순서는
|
||||
[기초 안내](./first-steps.md)와 [경험자 안내](./system-walkthrough.md)에 있습니다.
|
||||
|
||||
## 최상위
|
||||
|
||||
```text
|
||||
@@ -9,7 +13,8 @@ core2026/
|
||||
│ ├─ gateway-api/
|
||||
│ ├─ game-frontend/
|
||||
│ ├─ game-api/
|
||||
│ └─ game-engine/
|
||||
│ ├─ game-engine/
|
||||
│ └─ release-controller/
|
||||
├─ packages/
|
||||
│ ├─ common/
|
||||
│ ├─ logic/
|
||||
|
||||
@@ -1,16 +1,33 @@
|
||||
# 도메인과 조립 지점
|
||||
|
||||
**도메인**은 장수·도시·전투처럼 이 게임이 다루는 개념과 규칙입니다. 이 문서는
|
||||
객체 지향 문법을 설명하기보다 “규칙을 누가 계산하고 결과를 누가 적용하는가”를
|
||||
따라갑니다. [기초 안내](./first-steps.md)의 모병 예시를 떠올리면 좋습니다.
|
||||
|
||||
```text
|
||||
저장된 장수·도시 → loader → 메모리 세계
|
||||
예약한 명령 → 입력 해석 → 조건 검사 → 규칙 계산 → 변경 결과
|
||||
변경 결과 → 메모리 세계에 적용 → DB 저장 또는 실패 시 복원
|
||||
```
|
||||
|
||||
## World entity
|
||||
|
||||
`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를 관리합니다.
|
||||
메모리 snapshot과 실패 복원을 관리합니다. 실제 DB transaction은
|
||||
`databaseHooks.ts`의 책임입니다. **dirty state**는 마지막 저장 이후 바뀌어 다시
|
||||
저장해야 하는 상태를 뜻합니다.
|
||||
|
||||
## Command
|
||||
|
||||
장수 command는 `GeneralActionDefinition`으로 다음 계약을 가집니다.
|
||||
명령(command)은 “모병” 같은 행동 한 종류입니다. 입력(args)은 병종·수량처럼
|
||||
그 행동에 필요한 값입니다. **constraint**는 실행에 필요한 조건이고,
|
||||
**state patch**는 병력·자원 등 바꿀 값의 묶음입니다. 실행 결과에는 patch뿐 아니라
|
||||
플레이어에게 보일 로그와 후속 효과도 포함됩니다.
|
||||
|
||||
장수 command는 definition·command spec·resolver를 통해 다음 계약을 연결합니다.
|
||||
|
||||
- key와 사용자 표시 이름
|
||||
- raw args parser
|
||||
@@ -38,25 +55,26 @@ turn 소비와 side effect 계약을 보존합니다.
|
||||
priority trigger와 의미 event는 각각 다른 interface를 사용합니다.
|
||||
[행동 모듈 프로토콜](../architecture/action-module-protocol.md)을 따라 주세요.
|
||||
|
||||
## 주요 클래스
|
||||
## 주요 클래스와 함수
|
||||
|
||||
| 클래스·함수 | 책임 |
|
||||
| ------------------------- | -------------------------------------------------- |
|
||||
| `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 결과 |
|
||||
| 클래스·함수 | 책임 |
|
||||
| ----------------------------- | -------------------------------------------------- |
|
||||
| `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, 실패 시 restore |
|
||||
| `createReservedTurnHandler()` | revision·lease 기반 예약 명령 claim과 실행 |
|
||||
| `GeneralActionPipeline` | action module 계산·trigger·event 실행 |
|
||||
| `resolveWarBattle()` | 전투 phase, RNG, 상태와 log 결과 |
|
||||
|
||||
## 명령 추가
|
||||
|
||||
1. ref command의 예약·실행 constraint, args, RNG, log와 DB mutation을 찾습니다.
|
||||
1. 계승 명령이면 Ref의 예약·실행 조건, 입력, RNG, 로그와 DB 변경을 찾습니다.
|
||||
Core 전용 명령이면 요구사항과 의도한 결과를 먼저 정합니다.
|
||||
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를 추가합니다.
|
||||
5. 정상·실패·경계 fixed-seed test를 추가하고, 계승 계약은 Ref 차등 fixture로 확인합니다.
|
||||
6. 생성 command catalog, 상위 mapping과 report를 갱신합니다.
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
# 코드가 처음인 사람을 위한 구조 안내
|
||||
|
||||
함수, 변수, 조건문은 알지만 웹 서버나 큰 프로젝트는 처음인 독자를 위한 글입니다.
|
||||
이 문서를 읽고 나면 화면을 바꿀 파일, 게임 규칙을 바꿀 파일, 결과를 저장할 파일을
|
||||
구분할 수 있습니다. 설치 전에 읽어도 됩니다.
|
||||
|
||||
## 먼저 게임을 한 문장으로 이해하기
|
||||
|
||||
플레이어는 장수 한 명의 행동을 미리 예약합니다. 서버는 장수의 차례가 되면
|
||||
행동을 실행하고, 자원·도시·전투 결과를 저장합니다. 여러 플레이어와 컴퓨터 장수가
|
||||
같은 세계에 참여하므로, 브라우저를 닫아도 세계는 계속 진행될 수 있습니다.
|
||||
게임 자체가 낯설다면 [플레이어 첫 안내](../user/index.md)를 먼저 읽으세요.
|
||||
|
||||
## 화면과 서버는 다른 프로그램입니다
|
||||
|
||||
**프론트엔드**는 브라우저에서 실행되어 화면을 그리고 버튼 입력을 받는 프로그램입니다.
|
||||
**백엔드**는 서버에서 실행되어 로그인과 권한을 확인하고 데이터를 읽거나 바꾸는
|
||||
프로그램입니다. 버튼을 눌렀다고 브라우저가 직접 병력을 늘리는 것은 아닙니다.
|
||||
브라우저는 서버에 요청하고, 서버가 허용한 결과를 받아 표시합니다.
|
||||
|
||||
**API**는 두 프로그램이 요청과 응답을 주고받는 약속입니다. 이 프로젝트는
|
||||
TypeScript 타입으로 호출 형태를 연결하는 **tRPC**를 사용합니다. 타입이 맞더라도
|
||||
사용자 권한과 실제 자원은 서버가 다시 검사해야 합니다.
|
||||
|
||||
예를 들어 “모병을 예약”하는 과정은 다음과 같습니다.
|
||||
|
||||
```text
|
||||
브라우저: 병종과 수량을 고른다
|
||||
↓ 저장 요청
|
||||
게임 API: 로그인과 입력을 검사하고 예약 변경을 처리한다
|
||||
↓ 예약 목록에 반영
|
||||
턴 데몬: 내 차례가 되면 모병 조건을 다시 검사한다
|
||||
↓ 실행 결과
|
||||
데이터베이스: 병력, 자원, 로그와 남은 예약을 저장한다
|
||||
↓ 알림을 받고 다시 조회
|
||||
브라우저: 실제 결과를 보여 준다
|
||||
```
|
||||
|
||||
예약 저장과 모병 실행은 서로 다른 일입니다. 예약 이후 돈을 쓰거나 도시를
|
||||
잃었다면 실행에 실패할 수 있습니다. API를 읽을 때도 “요청을 받았다”와
|
||||
“게임 행동이 끝났다”를 구분해야 합니다.
|
||||
|
||||
## 다섯 곳을 먼저 익히세요
|
||||
|
||||
| 위치 | 역할 | 찾아볼 때 |
|
||||
| ------------------- | ------------------------------------- | -------------------------------------------- |
|
||||
| `app/game-frontend` | Vue 화면과 사용자 입력 | 예턴 입력창이나 도시 화면을 이해하고 싶을 때 |
|
||||
| `app/game-api` | 로그인·권한 검사, 조회·변경 요청 접수 | 누가 어떤 정보를 볼 수 있는지 궁금할 때 |
|
||||
| `app/game-engine` | 시간에 맞춘 실행, 세계 상태와 저장 | 예약한 행동이 언제 실행되는지 궁금할 때 |
|
||||
| `packages/logic` | 모병·내정·전투 등 게임 규칙 | 비용이나 성공 조건을 찾을 때 |
|
||||
| `packages/infra` | 데이터베이스 구조와 연결 | 무엇을 영구히 저장하는지 찾을 때 |
|
||||
|
||||
`app`은 실행하는 프로그램, `packages`는 여러 프로그램이 함께 쓰는 코드 묶음입니다.
|
||||
한 저장소에 여러 프로그램과 묶음이 있는 구성을 **모노레포**라고 합니다.
|
||||
`pnpm`은 이 묶음들의 설치와 명령 실행을 관리합니다.
|
||||
|
||||
`resources`에는 지도, 병종, 시나리오 설정이 있습니다. 규칙 코드가 같아도 선택한
|
||||
설정이 다르면 다른 게임이 됩니다. `tools`에는 검사·변환 같은 개발 도구가,
|
||||
`docs`에는 지금 읽고 있는 문서가 있습니다.
|
||||
|
||||
## 로비와 게임을 나누는 이유
|
||||
|
||||
**Gateway**는 회원 계정과 로비를 담당합니다. 하나의 계정으로 여러 게임 서버에
|
||||
들어갈 수 있지만, 각 게임의 장수·국가·진행 시점은 다릅니다. 운영 대상 게임을
|
||||
코드에서는 **프로필(profile)**이라고 부릅니다. 프로필은 개인 프로필 사진과 다른
|
||||
뜻입니다. **시나리오**는 그 게임에 적용할 시작 조건·지도·규칙 묶음입니다.
|
||||
|
||||
Gateway의 화면과 API는 `app/gateway-frontend`, `app/gateway-api`에 있습니다.
|
||||
게임 업데이트와 실행 관리도 필요하므로 운영용 프로그램이 추가로 있습니다.
|
||||
처음부터 이들을 모두 읽을 필요는 없습니다. [전체 구조](../architecture/overview.md)를
|
||||
읽을 때 게임 진행 경로와 운영 경로를 나눠 보세요.
|
||||
|
||||
## 데이터는 어디에 남나요?
|
||||
|
||||
**메모리**는 실행 중인 프로그램이 빠르게 사용하는 작업 공간입니다. 프로그램을
|
||||
다시 시작하면 사라질 수 있습니다. **PostgreSQL**은 세계 상태와 기록을 영구히
|
||||
보관하는 데이터베이스입니다. **Prisma**는 코드에서 데이터베이스를 다루기 위한
|
||||
도구이며, `packages/infra/prisma/game.prisma`는 게임 데이터 구조를 정의합니다.
|
||||
|
||||
턴 데몬은 세계를 메모리에 읽어 와 계산합니다. 계산이 끝나면 바뀐 값과 로그를
|
||||
**트랜잭션(transaction)**으로 묶어 저장합니다. 모병으로 돈만 줄고 병력은 늘지
|
||||
않는 식의 반쪽 저장을 막기 위한 경계입니다. 저장에 실패하면 메모리 상태도
|
||||
되돌려야 다음 행동이 잘못된 세계에서 실행되지 않습니다.
|
||||
|
||||
**Redis**는 세션이나 작업 전달·알림 등에 쓰는 별도의 저장·통신 도구입니다.
|
||||
게임 진행이 확정됐는지는 PostgreSQL 저장 결과를 기준으로 판단합니다.
|
||||
**SSE**는 서버가 열린 브라우저로 변경 알림을 보내는 연결입니다. 알림이 늦었다고
|
||||
곧바로 행동이 실패한 것은 아니므로 최신 데이터를 다시 조회합니다.
|
||||
|
||||
## 파일 하나에서 시작하는 읽기 연습
|
||||
|
||||
1. `packages/logic/src/actions/turn/general/che_모병.ts`를 엽니다.
|
||||
입력값, 조건, 결과를 나눠 봅니다. 지금은 모든 계산을 외우지 않아도 됩니다.
|
||||
2. `app/game-engine/src/turn/reservedTurnHandler.ts`를 읽으며 예약 행동이
|
||||
실행되는 곳을 찾습니다. 규칙 파일과 실행 담당 파일이 다른 이유를 확인합니다.
|
||||
3. `app/game-api/src/router/turns/`에서 예약을 바꾸는 요청을 찾습니다.
|
||||
예약과 실행 사이에 시간이 흐른다는 점을 다시 확인합니다.
|
||||
4. [파일 지도](./code-map.md)로 화면과 저장 담당 위치를 이어 봅니다.
|
||||
|
||||
파일명 뒤 `.ts`는 TypeScript 코드, `.vue`는 Vue 화면 구성요소, `.json`은 설정
|
||||
데이터, `.prisma`는 데이터베이스 구조, `.test.ts`는 검증 코드입니다.
|
||||
`import`는 다른 파일의 기능을 가져오는 선언입니다. 함수를 읽다가 모르는 이름을
|
||||
만나면 선언 위치로 이동해 입력과 반환값부터 확인하세요.
|
||||
|
||||
다음은 [개발자 핸드북](./index.md)의 경험자 경로입니다. 실행 환경을 준비할 때는
|
||||
저장소 `README.md`의 개발 환경과 [테스트 정책](../testing-policy.md)을 따릅니다.
|
||||
@@ -4,9 +4,11 @@ Baseline: `main@b91dcbcaaac5acd4c7349cd3ed0996c547f58756`
|
||||
|
||||
Branch: `test/game-clock-reconciliation-20260903`
|
||||
|
||||
This plan is the status source for the long-running user test branch. A checked
|
||||
item means code and focused automated evidence exist on this branch; it does not
|
||||
mean deployment or production validation.
|
||||
이 문서는 위 기준선·branch에서 진행한 구현 계획의 기록입니다. 체크 표시는 당시
|
||||
코드와 집중 검증이 있었다는 뜻이며, 현재 main·배포·운영 검증 상태를 나타내지 않습니다.
|
||||
현재 동작은 [게임 시계](../architecture/game-clock.md)와
|
||||
[재정렬 계약](../architecture/game-clock-reconciliation.md), 장애 처리는
|
||||
[복구 안내](./game-clock-recovery.md)를 읽으세요.
|
||||
|
||||
## Milestone 1 - authority and inventory
|
||||
|
||||
|
||||
@@ -1,61 +1,61 @@
|
||||
# Game clock reconciliation recovery
|
||||
# 게임 시계 재정렬 실패 복구
|
||||
|
||||
This runbook is intentionally fail-closed. Do not force a profile to `RUNNING`
|
||||
or delete an outbox row merely because its process is alive.
|
||||
이 문서는 운영 경험이 있는 개발자를 위한 절차입니다. 개념부터 읽으려면
|
||||
[게임 시계](../architecture/game-clock.md), 계산·저장 계약은
|
||||
[시계 재정렬](../architecture/game-clock-reconciliation.md)을 참고하세요.
|
||||
프로세스가 살아 있다는 이유만으로 `RUNNING`을 강제하거나 outbox 행을 지우지 않습니다.
|
||||
|
||||
## Observe
|
||||
## 먼저 관찰하기
|
||||
|
||||
Read only the target game schema. Record `world_state.clock_phase`,
|
||||
`clock_revision`, `deadline_generation`, the latest `clock_suspension`, all its
|
||||
participant checksums, and the matching `clock_projection_outbox`. Compare that
|
||||
target revision with `sammo:{profile}:clock:active-revision`. Never print DB or
|
||||
Redis credentials.
|
||||
대상 게임 schema에서 `world_state.clock_phase`, `clock_revision`,
|
||||
`deadline_generation`, 최신 `clock_suspension`, 참여자 checksum과 대응하는
|
||||
`clock_projection_outbox`를 읽습니다. Redis의
|
||||
`sammo:{profile}:clock:active-revision`과 목표 revision을 비교합니다.
|
||||
DB·Redis 접속 비밀은 출력하지 않습니다.
|
||||
|
||||
## Status meaning
|
||||
**outbox**는 DB에서 확정했지만 다른 저장소에 전달해야 할 일을 보관한 원장입니다.
|
||||
**checksum**은 재정렬 대상이 예상한 상태인지 비교하기 위한 요약값입니다.
|
||||
DB와 Redis를 하나의 트랜잭션으로 묶었다고 가정하지 말고 두 단계의 상태를 봅니다.
|
||||
|
||||
- `SUSPENDED`: the cut is durable; no alignment DB transaction has committed.
|
||||
- `RECONCILING` with `PENDING`/`FAILED` outbox: DB schedules moved, Redis is not
|
||||
authoritative yet, and gameplay must remain stopped.
|
||||
- `RECONCILING` with `APPLIED` outbox: verify Redis active revision and all
|
||||
participant checksums before finalizing.
|
||||
- `RUNNING`: DB revision, deadline generation, and Redis active revision must
|
||||
agree. A mismatch is an incident and workers must not dequeue.
|
||||
## 상태를 해석하기
|
||||
|
||||
## Retry
|
||||
| 상태 | 의미와 확인할 일 |
|
||||
| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
|
||||
| `SUSPENDED` | 중단 지점은 저장됐지만 일정 정렬 DB transaction은 아직 확정되지 않음 |
|
||||
| `RECONCILING`, outbox `PENDING`·`FAILED` | DB 일정은 옮겼지만 Redis 전환이 끝나지 않음. 게임 진행을 열지 않음 |
|
||||
| `RECONCILING`, outbox `APPLIED` | Redis 활성 revision과 모든 참여자 checksum을 확인하고 최종 확정 |
|
||||
| `RUNNING` | DB revision·deadline generation·Redis 활성 revision이 일치해야 함. 불일치하면 worker가 작업을 꺼내서는 안 됨 |
|
||||
|
||||
Retry the same suspension ID and target revision through the clock-operation
|
||||
service. The service must re-read participant checksums and either return the
|
||||
already-applied result or resume the pending outbox. Never create a replacement
|
||||
revision to hide a failed target revision.
|
||||
## 같은 작업을 재시도하기
|
||||
|
||||
For `UNIFICATION_WAIT`, never rerun invader creation as a separate repair.
|
||||
The input event, aligned schedules, optional rate, deterministic invader IDs,
|
||||
reserved turns, and outbox committed together. A committed command with a
|
||||
`RECONCILING` world therefore needs only the same outbox retry. If the command
|
||||
transaction rolled back, the original prompt and source revision remain and
|
||||
the same response can be retried without changing IDs or RNG results.
|
||||
시계 작업 서비스를 통해 **같은 suspension ID와 목표 revision**으로 재시도합니다.
|
||||
서비스는 checksum을 다시 확인하고, 이미 적용됐다면 그 결과를 사용하거나 남은
|
||||
outbox 처리를 이어갑니다. 실패를 숨기려고 새 revision을 만들지 않습니다.
|
||||
|
||||
When an outbox row is `FAILED`, the profile must remain `RECONCILING`. A retry
|
||||
is safe in both crash locations:
|
||||
`UNIFICATION_WAIT`에서는 이민족 생성을 별도 복구로 다시 실행하지 않습니다.
|
||||
입력 이벤트, 정렬된 일정, 선택적 간격 변경, 결정적인 이민족 ID, 예약 턴과 outbox가
|
||||
함께 확정됩니다. 명령이 확정됐고 세계가 `RECONCILING`이라면 같은 outbox만 이어
|
||||
처리합니다. transaction이 rollback됐다면 원래 질문·source revision이 남으므로
|
||||
ID나 RNG 결과를 바꾸지 않고 같은 응답을 재시도할 수 있습니다.
|
||||
|
||||
- before Redis commit, the Lua operation reapplies from the source revision;
|
||||
- after Redis commit but before DB finalization, the Lua operation returns the
|
||||
already-active target result and DB finalization resumes without shifting any
|
||||
tournament deadline twice.
|
||||
실패 위치에 따른 처리는 다음과 같습니다.
|
||||
|
||||
`lastError` naming a legacy tournament deadline means the tournament lacks its
|
||||
tick dual-write. Do not force the active revision; migrate or prove the
|
||||
tournament inactive, then retry the same outbox.
|
||||
- Redis 반영 전: Lua 작업이 원래 revision에서 적용합니다.
|
||||
- Redis 반영 후·DB 최종 확정 전: Lua가 이미 활성화한 결과를 돌려주고 DB 확정을
|
||||
이어갑니다. 토너먼트 기한을 두 번 이동하지 않습니다.
|
||||
|
||||
## Rollback
|
||||
`lastError`가 구형 토너먼트 기한을 지목하면 tick 기록이 갖춰졌는지 확인합니다.
|
||||
활성 revision을 강제하지 말고 migration이나 비활성 상태의 근거를 확인한 뒤 같은
|
||||
outbox를 재시도합니다.
|
||||
|
||||
There is no blind inverse update. Before enabling exact reconciliation in an
|
||||
environment, keep the normal database backup required for schema migrations.
|
||||
If participant verification shows an unexpected mutation, stop the profile,
|
||||
retain the ledger/outbox evidence, and restore the whole game schema from that
|
||||
backup. Redis projections are then rebuilt from the restored DB revision.
|
||||
## 되돌리기와 검증 범위
|
||||
|
||||
The conditional clock suite exercises `SUSPENDED`, `RECONCILING/PENDING`,
|
||||
`RECONCILING/FAILED` before and after Redis commit, recovered `APPLIED`, and
|
||||
final `RUNNING`. The admin status endpoint exposes the phase, revision,
|
||||
participant checksums, and outbox error needed to choose the matching step.
|
||||
모든 값을 반대로 이동하는 일괄 역연산은 복구 절차가 아닙니다. 운영 변경 전
|
||||
정상 백업을 준비하고, 예상하지 못한 mutation이 확인되면 대상 profile을 멈춰
|
||||
원장·outbox 증거를 보존합니다. 백업 복원이 필요하면 해당 시점 이후 데이터 손실
|
||||
범위를 평가하고 승인된 운영 복구 절차로 game schema를 복원합니다. Redis 투영은
|
||||
복원한 DB revision에서 다시 구성합니다.
|
||||
|
||||
조건부 시계 통합 suite는 `SUSPENDED`, Redis 적용 전·후의 실패, `APPLIED` 복구와
|
||||
최종 `RUNNING`을 검사합니다. 실행에 필요한 DB·Redis가 없어 skip된 결과는 복구
|
||||
검증 성공이 아닙니다. [테스트 정책](../testing-policy.md)과 해당 suite 설정을 확인하세요.
|
||||
|
||||
+13
-3
@@ -2,9 +2,16 @@
|
||||
|
||||
## 읽기 순서
|
||||
|
||||
신규 관리자 플레이 감사의 확정 범위, 수집·조회 비용과 후속 구현 체크리스트는
|
||||
[프로필별 플레이 감사 설계](../design/play-audit.md)에 있습니다. 해당 기능은
|
||||
아직 미구현이며 아래의 현재 구조 설명과 상태를 구분합니다.
|
||||
프로그래밍 기초만 안다면 [코드가 처음인 사람을 위한 구조 안내](./first-steps.md)에서
|
||||
시작하세요. 화면·서버·저장을 익힌 뒤 아키텍처 개요와 파일 지도로 넘어갑니다.
|
||||
|
||||
개발 경험이 있다면 [경험자를 위한 시스템 읽기](./system-walkthrough.md) →
|
||||
[아키텍처 개요](../architecture/overview.md) → [요청·턴·저장](./request-turn-persistence.md)
|
||||
순서가 좋습니다. 이후 아래 표에서 맡은 기능을 고르세요. 런타임 문서는 배포·worker까지
|
||||
포함하는 상세 참고서입니다.
|
||||
|
||||
관리자 감사는 [운영 안내](../play-audit-operations.md)에서 현재 기능을,
|
||||
[설계](../design/play-audit.md)에서 요구사항과 배경을 확인합니다.
|
||||
|
||||
| 작업 | 문서 | 코드 시작점 |
|
||||
| --------------------- | --------------------------------------------------------------- | ------------------------------ |
|
||||
@@ -18,6 +25,9 @@
|
||||
| action module | [행동 모듈 프로토콜](../architecture/action-module-protocol.md) | `actionModules/` |
|
||||
| ref 비교 | [차등 검증](../architecture/turn-state-differential-testing.md) | `tools/integration-tests` |
|
||||
|
||||
[구조 문서 찾아보기](./reference-map.md)에는 시계·재시도·실시간 갱신·측정·운영 자료를
|
||||
질문별로 모았습니다. 과거 계획과 현재 계약의 구분도 여기서 확인할 수 있습니다.
|
||||
|
||||
## 경계
|
||||
|
||||
- `packages/logic`은 계산과 규칙을 소유합니다.
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
# 구조 문서 찾아보기
|
||||
|
||||
[기초 안내](./first-steps.md)와 [경험자 안내](./system-walkthrough.md)가 학습 경로를,
|
||||
아래 문서들은 세부 계약을 제공합니다. 목록·벤치마크·과거 계획을 처음부터 모두
|
||||
읽기보다 현재 질문에 해당하는 문서를 고르세요.
|
||||
|
||||
## 현재 구조와 구현 계약
|
||||
|
||||
| 질문 | 문서 |
|
||||
| --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| 무엇이 어디서 실행되나요? | [개요](../architecture/overview.md), [런타임](../architecture/runtime.md) |
|
||||
| 어느 파일부터 읽나요? | [파일 지도](./code-map.md), [패키지 경계](../architecture/package-boundaries.md) |
|
||||
| 한 행동이 어떻게 계산되나요? | [도메인과 조립](./domain-and-classes.md), [행동 모듈](../architecture/action-module-protocol.md) |
|
||||
| 언제 저장되고 실패는 어떻게 처리하나요? | [요청·턴·저장](./request-turn-persistence.md), [API 재시도](../architecture/api-input-event-replay.md) |
|
||||
| 시간 정지·재개는 어떻게 되나요? | [게임 시계](../architecture/game-clock.md), [시간 도메인 목록](../architecture/time-domains.md), [재정렬 계약](../architecture/game-clock-reconciliation.md) |
|
||||
| 설정을 여러 파일에서 조합하나요? | [시나리오 합성](../architecture/scenario-composition.md) |
|
||||
| 화면에 변경이 어떻게 전달되나요? | [실시간 변경 원장](../architecture/realtime-change-journal.md) |
|
||||
| 전투 시뮬레이터가 어디서 계산하나요? | [브라우저 Worker](../architecture/battle-simulator-browser-worker.md) |
|
||||
| Ref와 무엇을 비교하나요? | [차등 검증](../architecture/turn-state-differential-testing.md) |
|
||||
| TypeScript 버전이 왜 둘인가요? | [도구 체인 정책](../architecture/typescript-version.md) |
|
||||
|
||||
## 변경 범위를 빠짐없이 확인하는 목록
|
||||
|
||||
[엔진 호출 procedure 목록](../architecture/game-api-daemon-procedure-inventory.md)과
|
||||
[직접 변경·journal 목록](../architecture/game-api-direct-mutation-journal-inventory.md)은
|
||||
읽기 교재보다 변경 누락을 찾는 검토 자료입니다. 표의 조사 날짜·기준선과 현재
|
||||
router·검사 코드를 함께 확인하세요. 새 API가 늘어도 과거 조사 숫자가 자동으로
|
||||
현재 개수가 되지는 않습니다.
|
||||
|
||||
시계 참여자 JSON·mutation evidence TSV는 기계가 사용하는 자료입니다. 형식과
|
||||
검사 계약을 유지하며 해당 기능을 바꿀 때 갱신합니다.
|
||||
|
||||
## 측정·계획·운영 자료
|
||||
|
||||
| 자료 | 해석 범위 |
|
||||
| ----------------------------------------------------------------------------------- | ----------------------------------------------------------- |
|
||||
| [NPC 메모리 측정](../architecture/npc-lifecycle-memory-profile.md) | 지정 fixture의 메모리·생성/사망 부하. 운영 DB 처리량과 다름 |
|
||||
| [NPC 천통 시간 측정](../architecture/npc-unification-timing-benchmark.md) | 인메모리 계산 시간. 실제 배포 성능 보장이 아님 |
|
||||
| [시계 구현 계획 기록](./game-clock-reconciliation-plan.md) | 당시 branch의 milestone. 현재 운영 상태의 기준이 아님 |
|
||||
| [시계 복구](./game-clock-recovery.md) | 상태·원장 확인과 같은 작업 재시도 |
|
||||
| [릴리스 운영](../release-operations.md) | 실제 배포 작업과 준비 확인 |
|
||||
| [관리자 콘솔](../admin-console.md), [플레이 감사 운영](../play-audit-operations.md) | 관리자 권한과 현재 조사 기능 |
|
||||
| [테스트 정책](../testing-policy.md) | 실행 준비·검증 종류·skip의 해석 |
|
||||
| [프론트엔드 CSS 구조](../frontend-css-architecture.md) | 화면 스타일의 소유권과 배치 계약 |
|
||||
|
||||
기능 설계의 목표, 코드에 있는 구현, 테스트로 확인한 범위, 실제 배포 상태는 서로
|
||||
다릅니다. 문서의 “완료”는 함께 적힌 날짜와 검증 범위로 해석하세요.
|
||||
@@ -1,5 +1,10 @@
|
||||
# 요청·턴·저장 흐름
|
||||
|
||||
이 문서는 데이터베이스와 트랜잭션을 아는 독자를 위한 실행 흐름입니다. 처음이라면
|
||||
[기초 구조 안내](./first-steps.md)를 먼저 읽으세요. **mutation**은 상태 변경,
|
||||
**flush**는 메모리에서 바뀐 내용을 저장소에 반영하는 작업입니다.
|
||||
조회·즉시 변경·예약 수정·시간에 따른 턴 실행을 구분해서 읽습니다.
|
||||
|
||||
## 조회
|
||||
|
||||
```text
|
||||
@@ -13,31 +18,35 @@ endpoint DTO에서 공개 field를 선택합니다.
|
||||
## API transaction mutation
|
||||
|
||||
```text
|
||||
request
|
||||
-> requestId와 input 검증
|
||||
-> session actor·권한 검사
|
||||
-> InputEvent(target=API, PROCESSING)
|
||||
-> Prisma transaction
|
||||
-> domain row mutation
|
||||
-> InputEvent(SUCCEEDED)
|
||||
-> commit
|
||||
-> notification
|
||||
request → requestId·입력·actor·권한 검증
|
||||
→ PostgreSQL transaction
|
||||
→ API InputEvent 생성 또는 기존 row 잠금
|
||||
→ identity 확인 → PROCESSING(attempts + 1)
|
||||
→ savepoint 이후 업무 변경
|
||||
→ SUCCEEDED + 실제 result → commit
|
||||
→ notification
|
||||
```
|
||||
|
||||
`app/game-api/src/inputEventBoundary.ts`의 `executeInputEvent()`가 이 경계를
|
||||
제공합니다. 중복 request ID는 완료·처리 중 event를 다시 실행하지 않으며,
|
||||
실패 event는 claim 조건을 만족할 때 attempts를 증가시킵니다.
|
||||
제공합니다. identity는 event type, actor, payload digest를 포함합니다. 같은 요청의
|
||||
완료 결과는 재사용하고, 다른 내용으로 같은 ID를 사용하면 충돌로 거부합니다.
|
||||
`PENDING`·`FAILED`는 identity가 맞으면 재시도할 수 있고, `PROCESSING`은 임의로
|
||||
다시 선점하지 않습니다.
|
||||
|
||||
업무 오류는 savepoint까지 되돌린 뒤 실패 상태를 저장합니다. DB transaction
|
||||
자체가 실패하면 그 안의 변경은 rollback됩니다. 상세 상태 표와 HTTP 응답 계약은
|
||||
[API 입력 재시도](../architecture/api-input-event-replay.md)를 따릅니다.
|
||||
|
||||
## Daemon mutation
|
||||
|
||||
```text
|
||||
request
|
||||
-> actor·input 검증
|
||||
-> InputEvent(target=DAEMON)
|
||||
-> InputEvent(target=ENGINE)
|
||||
-> daemon transport
|
||||
-> lease owner claim
|
||||
-> in-memory world mutation
|
||||
-> EngineStateManager transaction flush
|
||||
-> EngineStateManager의 메모리 복원 경계 안에서 DB transaction flush
|
||||
-> PostgreSQL event 결과 commit과 in-memory world checkpoint 확정
|
||||
-> SSE/realtime
|
||||
```
|
||||
@@ -60,7 +69,9 @@ request
|
||||
7. PostgreSQL transaction에서 dirty state, turn queue, log와 event를 flush하고,
|
||||
같은 `EngineStateManager` 경계에서 world checkpoint를 확정합니다.
|
||||
|
||||
Transaction 실패 시 `EngineStateManager`가 in-memory snapshot을 복원합니다.
|
||||
`databaseHooks.ts`가 PostgreSQL transaction을 담당하고, 이를 감싼
|
||||
`EngineStateManager`가 실패 시 메모리 snapshot을 복원합니다. 이 관리자는 DB를
|
||||
직접 알지 못합니다. DB rollback과 메모리 복원을 함께 유지해야 합니다.
|
||||
Lease를 잃은 process는 fencing 검사에서 commit하지 못합니다.
|
||||
`InMemoryTurnStateStore`는 checkpoint를 별도로 복제하지 않고 rollback 대상인
|
||||
`InMemoryTurnWorld`에서 읽습니다. 따라서 flush 실패 뒤 다음 run도 복원된
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
# 경험자를 위한 시스템 읽기
|
||||
|
||||
웹 개발이나 컴퓨터공학 배경이 있고, 이 저장소의 책임과 상태 전이를 빠르게
|
||||
이해하려는 독자를 위한 안내입니다. 웹 용어가 낯설다면 [기초 안내](./first-steps.md)를
|
||||
먼저 읽으세요.
|
||||
|
||||
## 세 가지 흐름으로 나누기
|
||||
|
||||
| 흐름 | 시작과 끝 | 핵심 질문 |
|
||||
| ----------- | -------------------------------------------------------- | --------------------------------------------------------- |
|
||||
| 사용자 요청 | 브라우저 → API → 저장 또는 엔진 입력 → 응답 | actor, 입력 검증, 중복 요청, 공개 범위는 누가 결정하는가? |
|
||||
| 게임 진행 | 스케줄 → 장수 턴·월간 처리 → 저장 → 알림 | 실행 순서, 난수 소비, 실패 후 복원이 보존되는가? |
|
||||
| 운영 | 관리자 요청 → 작업 원장 → 빌드·프로세스 전환 → 준비 확인 | 게임 상태와 배포 상태를 혼동하지 않는가? |
|
||||
|
||||
HTTP 요청 하나가 게임의 한 턴인 것은 아닙니다. 조회, 예약 수정, 즉시 동작,
|
||||
시간에 따라 실행하는 턴은 서로 다른 수명주기를 가집니다. [전체 구조](../architecture/overview.md)에서
|
||||
구성요소를 본 뒤 [요청·턴·저장](./request-turn-persistence.md)에서 경계를 읽으세요.
|
||||
|
||||
## 조회: 인증한 사람과 보여 줄 대상을 분리합니다
|
||||
|
||||
`app/game-api/src/server.ts`, `trpc.ts`, `router.ts`에서 transport와 인증 context,
|
||||
procedure 조립을 따라갑니다. Gateway 계정과 게임 장수는 같은 식별자가 아닙니다.
|
||||
클라이언트가 보낸 장수 번호만으로 소유권을 결정하지 않습니다.
|
||||
|
||||
구체적인 예는 `app/game-api/src/router/world/index.ts`의 `getCurrentCity`입니다.
|
||||
자신의 위치, 같은 국가 장수의 위치, 소유 도시, 첩보와 인접 여부를 조합해
|
||||
정보 공개 수준을 정합니다. 응답에 없는 비밀 값을 프론트엔드에서 가리는 방식이
|
||||
아닙니다. 데이터 전달 객체(DTO)는 저장 행 전체와 다를 수 있습니다.
|
||||
|
||||
## 변경: 입력 원장과 결과의 원자성을 읽습니다
|
||||
|
||||
`InputEvent`는 받아들인 변경 요청과 처리 상태를 기록하는 PostgreSQL 모델입니다.
|
||||
실제 `InputEventTarget` 값은 `API`와 `ENGINE`입니다. 엔진을 설명할 때 쓰는
|
||||
“daemon”은 프로세스 역할이며 DB enum 값이 아닙니다.
|
||||
|
||||
API 쪽 경계는 `app/game-api/src/inputEventBoundary.ts`입니다. request ID만
|
||||
같다고 모든 요청을 동일시하지 않습니다. actor, event type, payload identity와
|
||||
상태를 함께 읽고 재시도·충돌·완료 결과 재사용을 구분합니다. 엔진 쪽 입력은
|
||||
`app/game-api/src/daemon/`에서 따라갑니다.
|
||||
|
||||
예약 변경이 저장된 뒤 실제 턴 실행 조건은 다시 달라질 수 있습니다. request
|
||||
acceptance 검증과 command execution 검증을 하나로 합치지 않습니다. API에서
|
||||
완료된 변경과 엔진이 메모리에 보유한 세계 사이의 동기화도 확인해야 합니다.
|
||||
|
||||
## 엔진: 단일 소유자라도 장애 경계는 필요합니다
|
||||
|
||||
`createTurnDaemonRuntime()`의 조립부터 읽으면 loader, registry, AI, 월간 handler,
|
||||
저장 hook이 연결됩니다. 계산 중 상태는 `InMemoryTurnWorld`, 저장과 실패 복원은
|
||||
`EngineStateManager`, 실행 순서는 `TurnDaemonLifecycle`에서 추적합니다.
|
||||
|
||||
**Lease**는 일정 기간 엔진 소유권을 인정하는 임대입니다. **Fencing token**은
|
||||
소유권 세대를 구분하여, 멈췄다가 돌아온 과거 프로세스가 새 소유자의 상태를
|
||||
덮어쓰지 못하게 합니다. “프로세스가 한 개일 것”이라는 운영 가정만으로 대체할 수
|
||||
없습니다. DB 저장 실패에는 메모리 rollback도 필요합니다.
|
||||
|
||||
난수도 입력 상태의 일부입니다. seed가 같아도 분기·정렬·호출 횟수가 바뀌면
|
||||
이후 결과가 달라집니다. 도메인 계산은 [행동 모듈](../architecture/action-module-protocol.md),
|
||||
비교 범위는 [차등 검증](../architecture/turn-state-differential-testing.md)을 읽으세요.
|
||||
|
||||
## 시간을 하나의 Date로 생각하지 않습니다
|
||||
|
||||
게임 진행 시각과 인증 만료·lease 같은 현실 시각은 목적이 다릅니다. 일시정지나
|
||||
턴 간격 변경이 있다고 로그인 만료와 소유권 heartbeat를 같은 방식으로 옮길 수는
|
||||
없습니다. [시간 도메인](../architecture/time-domains.md), [게임 시계](../architecture/game-clock.md),
|
||||
[복구 절차](./game-clock-recovery.md) 순으로 읽으면 계산과 운영 경계가 이어집니다.
|
||||
|
||||
## 모듈과 프로세스도 다릅니다
|
||||
|
||||
`packages/logic`은 게임 계산을 소유하며 DB·파일·네트워크 I/O를 직접 하지 않습니다.
|
||||
필요한 외부 동작을 interface인 port로 표현하고, app/infra 쪽 adapter를 주입합니다.
|
||||
이 구분은 단위 검증과 런타임 저장 경계를 분리하기 위한 것입니다.
|
||||
|
||||
`game-api`가 `game-engine`의 일부 loader를 import한다고 API 안에서 턴 데몬을
|
||||
실행한다는 뜻은 아닙니다. 공개 subpath와 프로세스 entrypoint를 구분하세요.
|
||||
프론트엔드의 backend router 타입 참조도 서버 실행 코드를 브라우저에 넣는 것과
|
||||
다릅니다. [패키지 경계](../architecture/package-boundaries.md)는 이 규칙의 기준입니다.
|
||||
|
||||
## 다음 조사 위치를 선택하기
|
||||
|
||||
- 화면 상태·재접속 문제: frontend store → 조회 응답 → [실시간 변경 원장](../architecture/realtime-change-journal.md)
|
||||
- 새 게임 규칙: [도메인 조립](./domain-and-classes.md) → 명령·constraint → 엔진 결과 적용·flush
|
||||
- 설정에 따른 차이: [시나리오 합성](../architecture/scenario-composition.md) → 선택한 resource → 실제 loader
|
||||
- 계정·배포 문제: [런타임](../architecture/runtime.md) → [릴리스 운영](../release-operations.md)
|
||||
- 관리자 조사 기록: [플레이 감사 운영](../play-audit-operations.md) → 수집 정책·snapshot·조회
|
||||
|
||||
코드 검증은 [테스트 정책](../testing-policy.md)을 따릅니다. 순수 계산 테스트,
|
||||
실제 DB 저장 검증, 브라우저 검증은 서로 다른 질문에 답합니다. Ref는 계승 규칙의
|
||||
비교 기준이며, 별도로 결정된 Core 기능까지 예전 구현으로 되돌리는 기준은 아닙니다.
|
||||
Reference in New Issue
Block a user