Retract the 120-ply round cap and record the missing wiring step
The cap item existed so a three-round match would fit inside a 400-step scan. Phase 0a's truncation bootstrap removed that constraint -- scan length is a free parameter now -- and the same phase showed the cap is the thing that turns stalled games into losses: 20.6% of cap-hit games lost, 99.4% of all losses. A 120-ply cap would put *more* games into the wall and make "freeze the round while ahead" easier, so the round cap stays at 400 and max_steps_rate stays a watched metric. Also records what the plan never listed: ppo.py, gates.py and league.py all take a single-round State and still have to be wired to MatchState. That is the largest remaining piece of Phase 1. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XBQKgvBbxbheiTF1AVy1Sh
This commit is contained in:
@@ -172,20 +172,47 @@ scale을 낮춰서(예: 50 → 25) 제거한다.
|
||||
|
||||
### Phase 1 — 3라운드 env
|
||||
|
||||
- [ ] `State`에 `carry`(누적 점수차), `round_idx`, **라운드/매치 스텝 카운터**,
|
||||
**pre-sampled 덱 3세트 + 코인플립 비트 + 1라운드 선공 비트** 추가.
|
||||
- [ ] `step()`: 덱 소진 시 라운드 < 3이면 carry 갱신 → 다음 덱 슬라이스로 전환 →
|
||||
`round_idx += 1`, `done` 유지. 라운드 == 3이면 `done = True`.
|
||||
- [ ] **선공 규칙**: 1라운드 pre-sampled 비트. 2·3라운드는 `carry > 0` → 나, `< 0` → 상대,
|
||||
`== 0` → **pre-sampled 코인플립 비트**.
|
||||
- [ ] 라운드당 스텝 상한을 400 → **~120**으로 분리. **전역 `MAX_STEPS` 상수를 직접 수정하지
|
||||
말 것** — engine forced-done(`engine.py:224`), obs 정규화(`obs.py:75`), 모든 eval 스캔
|
||||
길이(`ppo.py:773`, `ppo.py:1049`, `gates.py`)에 물려 있다. 매치 레벨 스캔 길이를 따로 둔다.
|
||||
- [ ] **league 풀 재구축**: 기존 스냅샷 멤버는 obs 변경으로 전부 무효화된다 — Phase 1 산출물로 명시.
|
||||
- [ ] **테스트** (엔진 재작성에 테스트 없이 들어가면 3라운드 버그를 학습 곡선으로만 발견하게 된다):
|
||||
라운드당 총 턴 수 = 44 + 버림패 드로우 (property), `carry` = 라운드 점수 합 불변식,
|
||||
선공 규칙 3분기(`>`, `<`, `==`) 각각, 코인플립 통계(≈50/50), PBRS Φ가 라운드 경계에서
|
||||
연속(보드차 → carry 흡수), 미러 매치 대칭성.
|
||||
**구현: `src/lost_cities_jax/match.py` (커밋 `7cd299d`).** 단판 엔진을 고치지 않고 **감쌌다**
|
||||
— `engine.py`는 TS 클라이언트와의 차분 테스트가 지키는 규칙 오라클이고, 라운드 자체는
|
||||
매치가 생겨도 변하지 않는다. `MatchState`가 내부에 단판 `State`를 든다.
|
||||
|
||||
- [x] `MatchState`: `round`(단판 State), `deck_orders`(3×60), `coin_flips`(3,),
|
||||
`round_idx`, `carry`(2, 플레이어별 누적 점수), `done`.
|
||||
별도 매치 스텝 카운터는 두지 않았다 — 매치는 3라운드가 끝나면 종료하므로 종료 판정에
|
||||
불필요하고, 라운드 진행도는 내부 `State.step_count`(라운드마다 리셋)가 이미 준다.
|
||||
- [x] `match_step()`: 덱 소진 시 라운드 < 3이면 carry 갱신 → 다음 덱 슬라이스 → `round_idx += 1`,
|
||||
`done` 유지. 라운드 == 3이면 `done = True`. **보상은 3라운드 끝에서만.**
|
||||
- [x] **선공 규칙**: `carry[0] > carry[1]` → p0, `<` → p1, `==` → pre-sampled 코인플립.
|
||||
1라운드는 특수 케이스가 **필요 없다** — carry가 (0,0)이라 동점 분기가 자동으로 코인을
|
||||
뽑고, 그게 룰북의 "가장 나이 많은 사람"(= 임의)과 정확히 같다.
|
||||
- [x] **PRNG**: 딜 3벌 + 코인 3개를 `match_reset`에서 전부 미리 뽑아 상태에 저장.
|
||||
`match_step`은 결정론적이라 키를 rollout/eval/gates에 꿸 필요가 없고, **미러 매치가
|
||||
자리 라벨 교체만으로 성립**한다 (Phase 5 antithetic 페어링이 여기 의존).
|
||||
- [x] ~~라운드당 스텝 상한을 400 → ~120으로 분리~~ — **철회한다. 라운드 상한은 400을 유지.**
|
||||
|
||||
**이 항목의 원래 명분이 사라졌다.** 근거는 "3라운드(~360수)가 400스텝 스캔에 들어가게"
|
||||
였는데, **Phase 0a의 GAE 절단 부트스트랩이 스캔 길이 제약을 없앴다.** 잘린 매치는
|
||||
올바르게 부트스트랩되므로 스캔 길이는 이제 자유 파라미터다.
|
||||
|
||||
**그리고 상한을 낮추면 아티팩트가 악화된다.** Phase 0a 실측: 400수 벽에 부딪힌 판의
|
||||
20.6%가 패배로 뒤집혔고 **전체 패배의 99.4%가 거기서 발생**했다. 상한을 120으로 내리면
|
||||
**더 많은 판이 벽에 박고**, "앞선 쪽이 상한까지 끌어 라운드를 얼린다"는 착취가 훨씬
|
||||
쉬워진다. 셀프플레이에서는 라운드가 54수로 수렴해 120이든 400이든 안 걸리므로,
|
||||
**덜 걸리는 쪽을 두고 `max_steps_rate`를 감시**하는 것이 맞다.
|
||||
|
||||
- [ ] **학습 파이프라인 배선** (원래 계획서에서 누락된 항목): `ppo.py`의 rollout/eval,
|
||||
`gates.py`, `league.py`가 전부 단판 `State`를 받는다. `MatchState`를 받도록 배선한다.
|
||||
- 매치용 obs 래퍼: 우선 `observation(state.round, player)`로 carry-blind하게 연결
|
||||
(carry/round 피처는 Phase 3에서 추가).
|
||||
- `match_legal_action_mask`, `match_step`, `match_score`로 교체.
|
||||
- 스캔 길이는 자유 파라미터가 되었으므로 셀프플레이 매치 길이(~160수) 기준으로 잡되,
|
||||
잘려도 부트스트랩이 처리한다.
|
||||
- [ ] **league 풀 재구축**: 기존 스냅샷 멤버는 obs 변경으로 전부 무효화된다 — Phase 3 이후로
|
||||
미룬다 (obs가 확정되기 전에 풀을 다시 채우면 두 번 일한다).
|
||||
- [x] **테스트 12개** (`tests/lost_cities_jax/test_match.py`): 라운드당 덱 드로우 = 44 +
|
||||
버림패 드로우만큼 연장, `carry`가 각 라운드를 정확히 한 번씩 적립, 선공 규칙 3분기,
|
||||
1라운드 코인 공정성(≈50/50), 라운드 경계에서 누적 점수 불연속 없음(PBRS 전제),
|
||||
미러 매치 대칭성, 보상은 3라운드 끝에서만 지급.
|
||||
|
||||
### Phase 0b — 측정 자 (첫 장기 3라운드 학습 **전에** 착지)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user