이 문서는 "아직 정해지지 않았고, 정해지지 않으면 구현이 갈라지는 것"만 모은다.
CLAUDE.md 의 마지막 작업 원칙 — 미결 항목은 임의로 결정하지 않는다 — 을 실행하기 위한 장부다.
각 항목은 선택지와 그 결과, 추천안까지 적는다. "결정이 필요하다"로 끝나는 항목은 이 문서에 둘 수 없다. 결정되면 해당 문서 본문으로 옮기고 여기서는 지운다.
우선순위는 무엇을 막고 있는가로 정한다.
| 구획 | 의미 |
|---|---|
| A | Phase 2(정수화·결정론) 착수를 막는다 |
| B | Phase 3(서버 미러) / Phase 4(멀티플레이) 착수를 막는다 |
| C | 밸런싱. 구현은 진행 가능하고 값만 나중에 채운다 |
근거 수치가 붙은 항목은
node tools/sandbox/harness.mjs실측이다. 자동자를 고치면 다시 재야 한다.
결정. 규칙 3 확률 게이트의 해시 입력에서 step 을 뺀다.
활강 가능 여부가 위치로 고정되어 "잘 안 미끄러지는 자리"가 사면을 붙잡으므로
slideChance 가 안식각을 실제로 조절한다.
반영: terrain.md §3.1·§4.2·§7.1 / constants.py SLIDE_GATE_STATIC = 1
Q8 |
0 | 32 | 48 | 96 | 128 | 192 | 256 |
|---|---|---|---|---|---|---|---|
| 안식각 | 44.2° | 41.2° | 40.3° | 37.1° | 35.8° | 30.9° | 26.6° |
채택값: SAND 256 → 26.6°, SOIL 48 → 40.3°, SCREE 0 → 44.2°.
세 재질이 구분되므로 terrain.md §2 의 지층 노출 전제가 성립한다.
값 자체는 밸런싱 대상으로 남지만 조절 방법은 확정이다.
harness.mjs --repose 가 SCREE > SOIL > SAND 순서를 상시 검증한다.
결정. 고른 방향이 막혀 있으면 이번 스텝은 움직이지 않는다. 반대쪽을 시도하지 않는다.
step 이 바뀌면 side 도 바뀌므로 다음 스텝에 반대쪽을 시도하게 된다.
근거: 문서 문자 그대로이고, 모래가 물처럼 흐르지 않고 사각사각한 감각이 나온다. 벡터 구현이 마스크 1세트로 끝나는 것도 이점이다. 유동이 절반이라 정착이 느려지지만 A1 의 정적 게이트가 그 손실을 상당 부분 상쇄한다.
반영: terrain.md §3.1. 비교용 토글은 샌드박스·프로토타입에 남아 있다.
문제. 문서 시작값 SUBSTEPS = 4, MAX_SETTLE_STEPS = 600 이 실측과 크게 어긋난다.
아래는 A1 정적 게이트 · A2 한 방향 확정 후 재측정한 값이다.
| 시나리오 | 정착 스텝 | 스텝당 | 3초에 필요한 SUBSTEPS |
그때 프레임당 비용 |
|---|---|---|---|---|
| 반경 40 (기본 무기) 1발 | 1,235 | 0.083 ms | 7 | 0.6 ms |
| 반경 80 (최대 무기) 1발 | 1,881 | 0.160 ms | 11 | 1.8 ms |
| 반경 40 × 2발, 간격 320 | 4,846 | 0.198 ms | 27 | 5.3 ms |
| 반경 40 × 6발 | 3,636 | 0.185 ms | 21 | 3.9 ms |
최악은 발수가 아니라 간격이다 (terrain.md §11.6). 크레이터가 흩어져 있으면 각각
장거리 수송을 해야 해서 비싸고, 촘촘하면 합쳐져 싸다. 비단조라서 발수로 산정할 수 없다.
선택지.
SUBSTEPS |
최악 케이스 재생 | 프레임당 | 위험 | |
|---|---|---|---|---|
| (a) | 27 | 3.0 초 | 5.3 ms | rendering.md §8 의 5 ms 예산을 넘는다 |
| (b) | 20 | 4.0 초 | 4.0 ms | 예산 안. 최악 케이스가 4초 |
| (c) | 11 | 7.3 초 | 2.2 ms | 최대 무기 1발 기준. 동시 폭발에서 너무 길다 |
추천 (b) SUBSTEPS = 20. 4초는 참을 수 있고 (a)는 렌더 예산을 넘긴다.
3초를 지키려면 (a)를 쓰되 terrain.md §5.2 의 시간 압축을 필수로 만들어야 한다.
MAX_SETTLE_STEPS 는 12,000 을 권한다 — 관측 최악 4,937 의 2.4배.
이건 예외 경로여야 한다. 정상 플레이에서 걸리면 지형이 불안정한 채로 확정되고
terrain.md §5.1 의 활성 행 불변식도 깨진다.
확정 전에 남은 것 — 간격 스윕. 2발을 간격 64~480 으로 훑어 진짜 최악을 찾아야 한다.
그 값이 SUBSTEPS 하한을 정한다.
문제. 로드맵은 "감각이 변하면 상수를 다시 맞춘다"만 적고 절차가 없다. 어떤 상수를 어떤 고정소수점 형식(Q8/Q12/Q16)으로 표현할지, 반올림 방향, 재튜닝 판정 기준이 전부 없다.
추천 (결정 요청). 아래를 docs/simulation.md §2 에 규칙으로 넣는다.
- 스케일은 2의 거듭제곱만 쓴다.
docs/simulation.md§2.2 의 나눗셈 규칙이 그래서 성립한다 - 확률·비율은 Q8(0~256), 삼각함수는 Q12, 항력 등 미세 계수는 Q16
- 실수 표기를 문서에서 없앤다.
0.75가 아니라192/256으로 적는다 (0.15 × 65536 = 9830.4처럼 정수가 아닌 값이 나오면 floor/round 선택이 갈려 구현이 갈라진다) - 확정 상수는 문서 표가 기준이고,
server/src/neodeol/constants.py가 그 기계 판독 사본이다. 사본이 어긋나면 문서를 고친 뒤 사본을 맞춘다 - 정수화 후 §11 실측을 다시 돌려 각도·정착 스텝이 5% 이내인지 확인한다. 벗어나면 상수를 재튜닝한다
결정 완료. docs/mapgen.md가 비트 단위 명세다.
hash32기반 128셀 간격 정수 값 노이즈와 시프트 보간- 고정 지층, 기반암 앵커 4개, 파괴 가능한 암반 아치 1개, 좌우 기반암 봉인
- 생성 직후
mapSeed로 정착하고 연결성 재검사 루프까지 완료 - 정착된 표면에서 2~6인 스폰을 균등 목표 ±72셀 탐색, 최소 간격 96셀로 선택
MAPGEN_VERSION=1,kind=mapgen골든은.grid.gz없이mapSeed만 기록
TS client/src/sim/mapgen.ts와 Python server/src/neodeol/sim/mapgen.py가 교차 검증된다.
결정 전 문제 기록
문제. lockstep 의 초기 상태인데 명세가 어디에도 없다.
game-design.md §10.1 은 원하는 요소(봉우리/계곡, 오버행·아치, 기반암 기둥, 모래 주머니)의 목적만,
netcode.md §5.2 는 matchInit 에 mapSeed 만 보낸다. mapSeed → 격자 매핑이 비트 단위로 같지 않으면
1턴차에 바로 체크섬 불일치가 나고, 리싱크로 덮으면 절대 규칙 4의 복구 경로가 상시 경로가 된다.
게다가 통상적인 노이즈 함수(펄린/심플렉스)는 float 구현이라 terrain.md §7.3 과 정면으로 부딪힌다.
결정해야 할 것.
- 난수원 —
terrain.md§7.1 의hash32를 재사용한 정수 값 노이즈(격자점 해시 + 시프트 보간). 추천: 재사용. 해시 구현이 하나로 유지되고 float 금지와 충돌이 없다. (tools/sandbox/lab.js의noiseAt/ridge가 이 방식의 작동 예다) - 층 구조 결정 순서와 각 층 두께 규칙
- 아치·오버행·모래 주머니 배치 규칙과 개수
- 좌우 경계 처리 —
terrain.md§1.1 대로 최외곽 열이 유출되므로BEDROCK/ROCK으로 막을지 - 스폰 지점 선정과 최소 간격 수치 (
game-design.md§10 의 "최소 간격 보장") - 생성 직후 정착 자동자를 1회 돌려 안정화할지 (추천: 돌린다. 안 돌리면 1턴차에 지형이 저절로 무너진다)
어디에 쓸 것인가: docs/terrain.md 신규 절 또는 별도 docs/mapgen.md.
생성 결과의 체크섬을 test_cross_sim 의 첫 검증 지점으로 삼는 것도 함께 정한다.
개정 (MAPGEN_VERSION 2). 균일 지층을 지질 구역으로 바꿨다. 맵을 좌우로 4~5구역으로 나누고 구역마다 다른 지층 두께·기반암 깊이를 준다. 순열 배치라 한 맵에 네 지질이 전부 나온다.
왜. 기준선은 960열이 같은 지층이라 실측 안식각 26.6°/40.3°/44.2° 가 전술적으로 아무
일도 하지 않았다 — 어디를 파도 같은 것이 나오니 서 있는 자리에 의미가 없었다.
CLAUDE.md 의 "무기를 늘리기 전에 지형을 완성한다"가 가리키는 자리다.
표면 형상은 안 건드린다. 지표 아래 두께만 바뀌므로 초기 정착 비용이 그대로다
(실측 최악 9스텝 / 상한 12000, 초기 연결성 변환 0셀). 파헤쳤을 때만 갈린다 —
같은 반경 30 크레이터가 정착 4082,076스텝, 이동 5,98832,244셀로 5배 차이가 난다.
딸려 온 결정. 자리마다 발밑이 달라지므로 슬롯 배정을 라운드마다 돌린다
(beginRound(…, rotation = roundNo - 1)). 실측 최대 격차 4,580‰ → 916‰.
지질을 평탄화해 공정하게 만드는 대안은 이 변경의 목적을 없애므로 택하지 않았다.
docs/mapgen.md §4·§9.1.
결정 (turnSeed).
turnSeed = hash32(mapSeed, turnNo, 0, 0)
서버 난수가 아니다. 그래서 리플레이가 mapSeed + 시간순 intent 만으로 재현되고
roadmap.md Phase 7 과 netcode.md §8 의 "매우 저렴하다"는 서술이 참이 된다.
turnResolve.turnSeed 는 표시·검증용 참고값으로 격하된다 — 수신 측이 직접 파생해
대조할 수 있으므로 서버가 몰래 다른 시드를 밀어넣을 수 없다.
반영: terrain.md §7.1 / netcode.md §5.2 / tools/prototype/ (applyTurnSeed)
테스트 골든 파일 스키마는 구현 완료. terrain-only, terrain-turns, shots, mapgen,
match kind가 있고 매 표본/턴의 체크섬을 기록한다. 실제 서비스 리플레이 저장 스키마는 Phase 4 이후 항목이다.
남은 것은 Phase 7 의 서비스 리플레이 저장 스키마 하나다 — 테스트 골든과 다른 물건이다.
저장할 때 규칙 신원을 무엇으로 둘지가 핵심인데, 지금은 답이 준비돼 있다:
ruleHash(양쪽이 각자 계산하는 규칙 지문, sim/rules)가 simVersion 보다 적합하다 —
simVersion 은 서버만 계산할 수 있어 재생기가 검증할 수 없다. docs/netcode.md §5.
헤더 후보: mapSeed · protocolVersion · ruleHash · 플레이어 신원 · 시간순 intent.
Phase 7 에서 확정한다 — 저장 대상이 아직 없다.
결정. 직접 플레이 피드백에 따라 라운드 고정값 대신 매 턴 결정론적 랜덤 워크를 쓴다.
docs/match.md §3:
roll = hash32(mapSeed ^ 0x5715, turnNo, previousWind, 0) % 20
delta = roll < 4이면 -1, roll >= 16이면 +1, 나머지는 0
wind = clamp(previousWind + delta, -WIND_MAX, +WIND_MAX)
2026-09-17 플레이 피드백에 따라 60% 유지, 20% -1, 20% +1로 완화한다.
첫 턴의 previousWind는 0이며 범위를 넘는 변화는 경계에 머무른다. ±3 돌풍을 없애고
최대 변화량을 1로 제한해 방향을 바꾸려면 반드시 무풍을 거치게 한다.
표시 범위는 WIND_MAX = 4를 유지하되 탄도에는 WIND_SCALE_Q8 = 192(75%)를 적용한다.
네트워크의 wind는 권위 상태이자 클라이언트가 같은 입력으로 재계산해 검증할 수 있는 값이다.
로컬 턴·측풍계 예보도 독립 복사본 대신 같은 deriveWind를 사용한다.
결정. intent에 moveDx(셀)와 useShield를 넣는다. 슬롯 오름차순 이동 후 발사하며,
연료 1개당 14셀, 오르막 최대 6셀, 차폐막은 명시 사용 후 첫 피격 1회를 막는다.
정확한 절차는 docs/match.md §1·§5다.
문제. game-design.md §8.1 은 미끄러진다고 확정 서술하지만,
simulation.md §6.1 의 재배치 절차에는 수평 이동이 없고 같은 절이
"탱크는 자동자에 참여하지 않는다"고 못 박는다. 판정 기준인 "안식각을 넘으면" 도 측정 수단이 없다
— 안식각은 slideChance 에서 창발하는 값이고 명시적 각도 상수가 없다.
선택지.
- (a) 미끄러짐을 폐기하고 낙하만 유지.
simulation.md§6.1 과 일치하고 규칙이 가장 단순.game-design.md§8.1 의 해당 줄을 삭제해야 한다 - (b) 유지하고
simulation.md§6.1 에 결정론적 절차를 추가 — 예: 재배치 시 탱크 좌우 지지 셀 높이차가SLIDE_STEP_CELLS(예: 2셀) 이상이면 낮은 쪽으로 1셀 이동, 지지가 나올 때까지 반복. 방향 동점은 §7.1 해시로, 반복은 상한 명시
추천 (a) — MVP 범위를 줄이고, 필요하면 Phase 6 에서 (b) 로 추가한다. 어느 쪽이든 각도가 아니라 셀 높이차로 판정해야 한다. 각도는 이 자동자에서 측정 불가능한 값이다.
결정. 지형은 매치 내내 유지한다. 탱크는 만피로 부활하고 현재 정착 지형에서
chooseSpawnCells()를 다시 실행해 스폰을 재배정한다. 경제·탄약·아이템·점수는 이월한다.
추가 확정. 턴 상한은 ROUND_TURN_CAP 을 플레이어 수로 나누어떨어지게 내림한다
(2인 40 · 3인 39 · 4인 40 · 5인 40 · 6인 36). 안 나뉘면 캡 라운드에서 앞 슬롯이 한 발을
더 쏘고 승자가 HP 로 정해지므로 슬롯 번호가 유불리가 된다. 라운드 선공 편차는 5라운드가
6인으로 안 나뉘어 남지만, 라운드 수를 인원마다 바꾸면 매치 길이가 들쭉날쭉해져 감수한다.
docs/match.md §5.15.
Phase 3 결정. 생존 ≤1이면 종료, 0명은 전멸 무승부, 40턴 상한은 최고 HP 단독 선두가 승리한다.
점수는 킬×100 + 피해×1 + 생존 50이며 5라운드 누적 최고점들을 공동 선두로 반환한다.
최종 서비스에서 공동 우승을 허용할지 별도 타이브레이커를 둘지만 남았다. docs/match.md §6.
문제. rendering.md §4.4 는 uAge 가 "렌더 전용이지만 클라이언트마다 동일"하다고 하는데,
netcode.md §4.2 의 fullState 에 age 격자가 없다. 재접속자·관전자는 과거 턴 이벤트를 못 받으므로
원리적으로 복원할 방법이 없다.
선택지. (1) fullState 에 age 격자 포함 (zstd 로 수 KB. 복구 경로 안이므로 절대 규칙 4 위반 아님)
· (2) sim state 에 넣고 체크섬에서 제외 · (3) 재접속자는 전 셀 AGE_MAX 근사를 허용하고
"클라이언트마다 동일" 문장을 삭제.
추천 (1). 시그니처 연출이 재접속자에게만 깨지는 것은 눈에 띈다.
함께 정할 것: uAge 노출 판정의 정확한 정의, 셀 이동 시 age 가 따라가는지, 초기값
(0 이면 갓 생성된 맵 전체가 FRESH 로 렌더되어 §1.1 과 정반대가 된다 → AGE_MAX 로 시작해야 한다).
결정. SIM_VERSION = hash(constants snapshot)을 유지하되 스냅샷에 수동
RULES_VERSION을 포함한다. 상수 밖의 로직 변경 시 올리고, 누락은 교차 골든 CI가 잡는다.
즉 런타임 접속 게이트와 실증 결정론 게이트를 함께 쓴다.
결정 전 검토 기록
문제. netcode.md §7.3 은 이렇게 정한다.
simVersion은client/src/sim/전체 +tables/trig.bin의 해시로 빌드 시 생성한다.
소스 코드 해시는 서버와 클라이언트 사이에서 절대 일치할 수 없다. 서버는 Python 이고
클라이언트는 TypeScript 라 파일이 아예 다르다. 서버는 client/src/sim/ 을 해시할 수도 없다.
§7.3 의 목적(같은 규칙으로 계산하는지 확인)이 현재 정의로는 달성되지 않는다.
한편 server/src/neodeol/constants.py 는 상수 집합의 해시로 sim_version 을 계산해
GET /version 으로 노출한다. 이건 양쪽이 같은 값을 계산할 수 있다 — 상수는 문서가 기준이고
두 구현이 같은 값을 담기 때문이다. 하지만 상수가 안 바뀌고 로직만 바뀐 경우를 못 잡는다
(예: 해시 확산 단계 추가, 활성 집합 정의 변경 — Phase 0 에서 실제로 둘 다 일어났다).
선택지.
| 정의 | 잡는 것 | 못 잡는 것 | |
|---|---|---|---|
| (a) | 상수 해시만 | 상수 변경 | 로직 변경 |
| (b) | 소스 해시 | 자기 쪽 코드 변경 | 상대편과 대조 불가 — 성립하지 않는다 |
| (c) | 상수 해시 + 수동 RULES_VERSION 정수 |
둘 다 | 개발자가 올리는 걸 잊은 경우 |
| (d) | 골든 리플레이 체크섬 | 둘 다, 실증적으로 | 리플레이를 돌려야 알 수 있어 접속 시점에 쓰기 어렵다 |
추천 (c). simVersion = hash(constants) + ":" + RULES_VERSION 형태로 두고,
sim/ 의 규칙 로직을 바꾸면 RULES_VERSION 을 손으로 올리는 것을 리뷰 체크리스트에 넣는다.
잊는 사고를 막는 안전망은 (d) 다 — 골든 리플레이가 CI 에서 매번 돌면 로직 변경이 반드시 잡힌다.
즉 (c) 는 런타임 게이트, (d) 는 CI 게이트로 역할이 다르다.
확정 후 netcode.md §7.3 의 문장과 constants.py 의 compute_sim_version() 을 함께 고친다.
Phase 4 기준선. 세부 필드는 netcode.md §5가 규범이다.
- 슬롯은 로비 입장 순서대로 가장 낮은 빈 번호를 서버가 배정하고, 매치 시작 뒤에는 재접속해도 유지한다.
- WebSocket 업그레이드에서
token,protocolVersion,simVersion,buildHash를 보낸다. deadlineMs는 Unix epoch 밀리초 절대시각이다.checksum은 지형 격자만, 플레이어 상태는players[]필드별로 대조한다.playbackDone(turnNo, checksum)으로 재생 완료를 알린다. 연결 중인 전원 ACK 또는 8초 상한 뒤 다음 턴이다.fullState.gridGzip은 gzip 압축한 518,400바이트 격자다. 브라우저 표준DecompressionStream("gzip")을 쓴다.events[]는 권위 결과를 설명하는 표현·진단 입력이다. 지형과 피해를 다시 계산하는 입력으로 쓰지 않는다.- 구매는 한 요청에 무기 또는 아이템 하나이며
buyResult로 전체 인벤토리와 골드를 다시 보낸다. - 개발 코드형 로비 토큰은 256비트 불투명 토큰이다. 프로덕션 로비 JWT로 바꿔도 룸 프로토콜 의미는 같다.
- 오류 코드는
netcode.md§5.5의 숫자로 분기하고msg문자열로 분기하지 않는다.
한 턴의 물리 재생이 8초를 넘을 수 있으므로 8초는 서버가 다음 턴을 미루는 상한이지 애니메이션
길이 상한이 아니다. 늦은 클라이언트는 turnResult 상태로 스냅한 뒤 다음 턴을 따라간다.
결정. docs/match.md §1의 MatchState와 Player가 Phase 3 sim 상태 스키마다.
격자 상태는 terrain.grid, 매치 상태는
mapSeed/roundNo/turnNo/roundTurn/activeSlot/wind/spawnCells/players/phase이며
플레이어·탄약·아이템 배열은 슬롯/무기 ID 오름차순이다. 체크섬 대상은 격자만 유지한다.
msgpack 와이어 필드 폭과 이벤트 인코딩은 B10의 Phase 4 항목이다.
결정 전 문제 기록
docs/simulation.md §1 은 step(state, inputs) -> new_state 만 적고 state 의 필드 목록·타입·
직렬화 순서가 어디에도 없다. terrain.md §5.2 는 "sim 상태는 격자 하나뿐"이라 하고,
netcode.md §4.2 의 fullState 는 플레이어 상태를 열거한다. 두 서술이 같은 것을 말하는지 다른지
분명하지 않다 — 격자는 sim 상태고 플레이어는 그 밖이라는 뜻인지, 아니면 state 가 둘 다인지.
추천. state = { grid, players[], turnNo, roundNo, windBySlotRound, rngSeed } 처럼
명시하고, 그중 체크섬 대상은 grid 뿐임을 별도로 적는다(terrain.md §7.2 와 일관).
직렬화 순서는 골든 리플레이 포맷(B2)과 같은 규칙을 쓴다.
결정. POWER_SCALE 448 → 624. 최대 파워 45° 사거리가 1,899 px = 맵 폭의 98.9% 가 되어
맵 전체가 교전 범위가 된다. 스폰은 맵의 1/6·5/6 (간격 1,280 px = 맵 폭의 2/3)에 앉히고,
명중에는 파워 약 82% 가 필요해 위·아래로 여유가 남는다.
함께 바뀐 것.
simulation.md§8.1 의 목표치가 "맵 폭의 50% ± 5%" → "100% ± 5%"roadmap.mdPhase 1 완료 조건의 "적당한 대각선이 화면 절반쯤" 서술을 폐기tools/ballistics-check.mjs의TARGET_PCT를 100 으로- 체공이 51 → 71 tick 으로 늘었다. 편차/사거리 비는
WIND_MAX/GRAVITY라 파워와 무관하므로WIND_MAX를 다시 잡을 필요는 없다 (실측 확인)
발견 경로. 프로토타입에서 탱크를 맵의 1/6·5/6 에 앉히니 간격 1,280 px > 사거리 916 px 로 두 탱크가 서로 닿지 않아 라운드가 영원히 끝나지 않았다. 추상적 우려가 아니라 실제 사실이었다.
라운드 턴 상한도 해결됨. 40턴에서 생존자 중 최고 HP 단독 선두가 승리하고 동률이면 무승부다.
결정. 재배치의 밀어올리기 임계를 매몰 판정과 같은 BURIAL_PERMILLE(800‰) 로 맞춘다.
즉 매몰로 판정되는 상태면 반드시 밀어올려지고, 다음 재배치에서 자동으로 풀린다.
추천 (a) 를 "위쪽 셀이 비면"이 아니라 "매몰 판정과 같은 임계로"로 구체화한 것이다.
왜 이 형태인가. 밀어올리기는 이미 있었는데 임계가 1000‰ 이었다. 매몰 판정은 800‰
이라 800~999‰ 에 안착하면 탈출 경로가 원리적으로 없었다 — 실측 875‰ 에서 재배치가
0 px 움직이고, 턴당 6 피해로 약 17턴 확정사였다. 위 추천의 근거였던 "묻히는 것이
즉사와 다름없으면 매몰을 별도 상태로 둔 의미가 없다"가 정확히 그 상태였다.
두 임계를 하나의 상수로 묶는 것이 핵심이다. 값이 갈라지면 그 사이 구간이 다시 생긴다.
관련: 같은 판에서 낙하 피해는 원인을 가리지 않는다로 확정했다. 연료로 걸어 내려간
낙하도 지형 붕괴로 떨어진 것과 같은 피해를 받는다 — 그 전에는 applyMove 가
reseatTank() 의 반환값을 버려서 600 px 절벽이 무피해였다. match.md §5.1.
결정. Phase 3.5 직접 플레이 결과 표준 규칙을 완전 순차 턴으로 바꾼다.
MatchState.activeSlot의 플레이어 한 명만 intent를 제출하고, 그 포탄의 폭발·정착·재배치가
완전히 끝난 뒤 다음 생존 슬롯으로 넘어간다. turnNo와 roundTurn은 발사 한 번마다 증가한다.
라운드 시작 슬롯은 (roundNo - 1) % playerCount로 순환한다. 한 발만 정착하므로 기존의
"6발 동시 폭발 정착 비용" 미결은 폐기한다. 다중 폭발은 분열탄처럼 한 발이 만든 자탄에만 남는다.
결정. 직접 플레이 요청에 따라 8번째 무기로 핵포탄을 추가한다. 초기 탄약은 0발이며
상점 가격은 4,800G다. 시작 자금 1,500G와 현재 보상 계수에서는 첫 두 라운드에 살 수 없고,
보통 3개 라운드의 수입을 모아 4라운드 전후에 한 발을 확보하는 후반 선택지다.
2026-09-17 한 발의 위력을 높여 달라는 피드백으로 최대 피해를 120 → 200, 피해 반경을
4096 → 8192 subpx(256 → 512px), 카빙 반경을 80 → 128 cells로 높인다.
카빙 원의 기하학적 면적은 기존의 2.56배이며 실제 제거량은 재질 저항·지형·기반암에 따라 다르다.
가격·초기 탄약·구매 및 발사당 한 발 소모는 유지한다. 자신과 아군도 같은 광역 피해를 받는다.
기반암·차폐막 규칙은 유지하고, 다른 무기나 바람·탄도는 바꾸지 않는다.
확장 반경은 파괴·질량 보존·붕괴 수렴 및 양 언어 골든으로 검증한다. MATCH_VERSION = 12.
연출은 6.2초 동안 불기둥·넓은 구름 머리·말려 올라가는 연무·바닥 먼지가 단계적으로 나타나는
핵 전용 버섯구름을 사용한다. 관측경은 구름 꼭대기까지 담되 다음 발사나 승패 판정을 지연하지 않는다.
라운드가 끝날 때만 구름을 가리지 않도록 보급·결과 모달을 착탄 2.8초 이후에 표시한다.
빠르게 넘기기·동작 줄이기에서는 이 모달 대기를 생략한다.
개정 (B22). 상한과 충전 시간은 B22 가 대체한다 — 0 → 1500, 초당 400파워이므로
최대 출력까지 3,750ms다. 아래 원문의 1000·1,800ms는 B22 이전의 값이다.
결정. 포트리스 계보의 손맛을 위해 Space를 누르는 동안 파워를 0 → 1000으로 올리고,
키를 떼는 순간 현재 정수 파워로 발사한다. 최대 충전 시간은 1,800ms이며 최대치에서는 키를
뗄 때까지 유지한다. 클릭/드래그 각도, 휠 파워, 숫자 입력은 빠른 조정과 접근성을 위한 보조 입력으로 남긴다.
충전 게이지는 사이드 패널이 아니라 전장 하단을 길게 가로지르는 HUD로 둔다. 게이지를 클릭하거나 드래그하면 5단위 목표 핀이 놓이고, 충전량이 핀에 닿으면 강조한다. 핀은 자동 발사점이 아니라 손으로 키를 떼는 타이밍을 잡는 시각 기준이다.
네트워크에는 누른 시각이나 중간 게이지를 보내지 않고 키를 뗀 순간 확정된 파워 정수만 intent에 싣는다. 따라서 충전 애니메이션은 클라이언트 표현이고 권위 시뮬레이션 입력 스키마는 바뀌지 않는다.
결정. 좌우 궤도 아래의 지표 높이를 표본화해 차체 경사를 정수 데시도로 계산한다. 경사는
±14.0°로 제한하며, 0.0~14.0°의 삼각표를 정수 오차가 가장 작은 값으로 검색해 Python과
TypeScript가 같은 값을 고른다.
조준 입력 angle10은 이제 차체 기준 상대각이다. 실제 월드 탄도각은
effectiveAngle10 = clamp(angle10 - tankTilt10, 0, 1800)이고, 포탑 중심과 포신 끝도 차체 경사로
회전한다. 반원형 각도계와 포신 그래픽은 같은 규칙을 표시한다.
렌더러는 차체가 갑자기 튀지 않도록 같은 목표 경사를 프레임 사이 보간할 수 있지만, 탄도 계산은
항상 정수 tankTilt10을 즉시 사용한다. 시각 보간값을 시뮬레이션에 되먹이지 않는다.
결정. 싱글플레이 강화 요청에 따라 기존 규칙 위에 작전 선택·조작 안내·브라우저 로컬 전적을 추가한다. 모든 작전은 처음부터 선택할 수 있고, 5라운드 누적 점수·무기·경제·물리 규칙은 그대로다. 작전은 입문(2인, 쉬움), 표준(3인, 보통), 도전(4인, 어려움)의 AI 매치 프리셋이다. 슬롯 0만 사람이며 사용자 설정 매치와 멀티플레이도 별도로 유지한다. 시드 85·149·221은 회귀 테스트용이며 실제 작전은 B20에 따라 매번 새 시드를 사용한다.
완료 횟수·승리·최고 점수만 로컬에 기록하며 진행 중 매치 저장이나 서버 전적처럼 표현하지 않는다. 동점 1위는 승리로 기록하지 않는다. 저장소 사용 불가 시 세션 내에서만 유지하고 이를 표시한다. 홈 이동은 현재 매치를 포기하는 명시적 확인을 거친다. 조작 안내는 정답 궤적을 제공하지 않는다. 싱글플레이의 모달 메뉴는 재생·AI 입력을 일시정지하며 복귀 시 표현 시계를 보정한다. 멀티는 정지하지 않는다.
화면은 황혼 협곡·풍화된 지층·계측기 같은 HUD를 유지하되, 개발용 사이드바 대신 하단 조작부와
상단 전황으로 정리한다. Canvas 감성 패스를 개선하는 범위이며 WebGL2 완료로 간주하지 않는다.
지형 격자·실제 탱크 위치·발사각을 렌더러에서 변경하지 않는다. 디버그 UI는 ?dev=1에서만 노출한다.
결정. 주변 UI보다 실제 전장·탱크의 디테일을 보강한다는 추가 요청에 따라 Canvas 표현을 개선한다. 재질별 퇴적 결·암반 절리·광물맥, 지표의 사광과 절벽의 접촉 음영, 원경의 바위 면을 렌더 전용으로 추가한다. 탱크는 궤도 링크·보기륜·분할 장갑·포방패·냉각 그릴·머즐 브레이크를 가진 동일한 자체 모델로 재작성한다. 엔티티·효과 레이어를 고해상도로 그리되 차체의 기존 시각 너비, 실제 위치·충돌 크기·포구 좌표는 유지한다. 고장 난 차체와 낮은 HP의 연기, 포신 후퇴를 시각화하되 게임 상태에 되먹이지 않는다. 전장 하단에 접을 수 있는 관측경을 둔다. 조준 중에는 활성 탱크를 4배 확대하고, 발사 후에는 현재 재생 중인 포탄을 추적한다. 분열탄은 살아 있는 탄들의 영역을 함께 잡고 자동으로 축소한다. 착탄 후에는 폭발·붕괴를 관측하다가 다음 조준으로 복귀한다. 최소 1.4초 동안 착탄 지점을 유지하되 새 발사가 시작되면 즉시 추적을 재개한다. 전체 전장 카메라·턴 진행은 건드리지 않는다. 미래 궤적·예상 착탄점을 카메라에 사용하지 않는다. 화면 밖으로 나간 탄도 현재 재생 위치만 보여준다.
재질 질감과 정적 원경은 맵 시드별로 캐시하고 매 프레임 생성하지 않는다. 변경 행 갱신을 유지하며
흙과 바위의 실루엣을 거짓으로 추가하거나 지형 격자를 장식 목적으로 수정하지 않는다.
sim/, 맵 생성·무기 수치, 네트워크 프로토콜은 변경하지 않는다. WebGL2 이행은 별도 단계다.
결정. 전장이 작고 평평하며 첫 배치가 고정된다는 요청에 따라 다음을 변경한다. PC 기본 뷰는 21:9로 넓히고, 하단 깊은 지층 일부를 잘라 보여주되 비율을 늘여 왜곡하지 않는다. 활성 포대가 아래로 떨어지면 세로 시야를 따라 내린다. 전체 16:9 지형 보기 버튼을 제공하며 모바일은 전체 지형을 유지한다. 좌표 입력은 실제 캔버스의 표시 영역을 기준으로 변환한다.
맵에는 큰 봉우리·보조 능선·골짜기를 정수 시드로 생성한다. 급경사는 암반을 노출하고
중간 경사는 얇은 퇴적층으로 덮어 초기 붕괴 비용을 제한한다. 완만한 곳은 기존 재질별 깊이를 유지한다.
스폰은 정착된 지면에서 최소 36셀 간격·차체 양끝 지면 편차 합 12셀 이내의 후보를 우선하며,
근접 군집·장거리 분산·전역 무작위 배치를 시드별로 선택한다. 슬롯 순서도 섞어 사람의 좌측 고정을 없앤다.
싱글 작전 진입마다 UI에서 32비트 새 시드를 추첨한다. 명시적 사용자 시드는 재현용으로 유지한다.
멀티는 서버가 선택한 mapSeed 하나로 모든 클라이언트가 같은 지형과 스폰을 재현한다.
MAPGEN_VERSION=3, MATCH_VERSION=9, RULES_VERSION=7로 올리고 교차 골든을 갱신한다.
결정. AI가 너무 정확하다는 플레이 피드백에 따라 입력 생성기의 난이도를 조정한다.
쉬움은 각도 ±10°·파워 ±120, 보통은 ±4°·±55, 어려움은 기존 범위 ±0.4°·±4를 유지한다.
각도와 파워는 독립된 시드 해시의 16비트 값으로 오차를 뽑는다. 발사 의도는 같은 입력에 항상 같다.
쉬움은 라운드 첫 2 × 전체 참가자 수턴, 보통은 첫 전체 참가자 수턴에 표준탄만 사용한다.
이후 특수탄 선택 확률은 쉬움 15%·보통 35%·어려움 50%이고, 쉬움은 성형탄 마무리를 선택하지 않는다.
라운드 턴을 전달하여 이후 라운드에도 준비 시간이 적용된다. 사용자 설정의 난이도 변경도 즉시 반영한다.
체력·피해·물리·재화 보정이나 의도적인 무적은 추가하지 않는다. AI는 시뮬레이션 밖의 입력 생성기이므로
이 조정만으로 규칙 버전·교차 골든을 바꾸지 않는다. 평지 표본의 명중률·오차·무기 선택을 회귀 검사한다.
결정. 끝과 끝의 랜덤 배치에서도 역풍 때문에 교전 자체가 불가능하지 않도록 파워 상한을
1000에서 1500으로 높인다. 기존 파워별 탄도·바람·중력은 유지하고 추가 출력 구간만 제공한다.
45° 평지 기준 최대 역풍(-4) 사거리는 당시 1266px → 2871px로 맵 폭 1920px보다 넉넉해졌다.
2026-09-17 바람 영향 75% 완화 후 최대 파워 역풍 사거리는 3230px다 (C4).
무풍 최대 사거리는 4307px다. 산에 가로막히는 문제는 여전히 각도 선택으로 해결해야 한다.
입력·키보드·충전 핀·AI 탐색·권위 서버 검증이 같은 상한을 사용하고 규칙 지문에도 포함한다.
충전 속도는 초당 400파워를 유지한다(최대 출력까지 3.75초). MATCH_VERSION=10, RULES_VERSION=8.
탄도 재생은 경로 길이와 상관없이 초당 48 시뮬레이션 틱으로 진행한다. 고정 60틱 재생이 너무 빠르다는
추가 피드백을 반영해 속도를 20% 낮춘 값이다(물리 적분은 기존 60Hz 유지). 기존 최소 2.4초/최대 9초
정규화를 없애 근거리 포탄을 느리게 늘이지 않는다. 고속 보기만 같은 틱 진행률의 8배다.
파워·중력에 따른 실제 속도 차이는 보존하며 경로 점을 재배치하지 않는다. 관측경도 같은 틱을 따른다.
풍향계는 움직이는 전장 캔버스가 아닌 고정 HUD에 놓아 세로 카메라 이동에도 현재 바람·돌풍이 보인다.
측풍계 아이템이 있을 때만 다음 턴 예보를 표시하는 규칙은 유지한다.
전장 컨테이너는 overflow: clip으로 자른다. hidden은 포커스 이동으로 내부 스크롤이 발생해
카메라와 무관하게 HUD까지 밀어 올릴 수 있으므로, 키보드 파워 핀 조작에서도 스크롤이 0인지 확인한다.
결정. 아래 열로 확정하고 server/src/neodeol/constants.py 의 WEAPON_TABLE 이
유일한 사본이다. sim/weapons.py 가 이 표를 읽고, TS 하드코딩이 어긋나면 골든 헤더
대조가 잡는다 (test_cross_sim.py::_assert_tables_match). 값은 여전히 밸런싱 대상이지만
표가 바뀌면 SIM_VERSION 과 규칙 지문이 따라온다 — 그 전에는 tuple 이라는 이유로
해시에서 통째로 빠져 있었고 밸런스를 바꿔도 핸드셰이크가 통과했다.
id · name · kind · maxDamage · blastRadius · carveCells · ammo0 · price
특수: splitCount · splitSpread · burrowCells · rollCells · depositCells · depositMat
RESIST_Q8 오버라이드 열은 넣지 않는다. 지금 8종 중 필요한 무기가 없고, 쓰지 않는
열을 스키마에 두면 구현만 복잡해진다. "굴착탄이 암반을 더 잘 뚫는" 같은 방향이 필요해지면
그때 열을 늘리고 MATCH_VERSION 을 올린다.
damageShift 는 표에 저장하지 않고 blastRadius 에서 파생한다 —
shiftOf() 가 2의 거듭제곱이 아니면 생성 시점에 던진다. 두 값을 따로 두면 어긋날 수 있다.
원래 논의
game-design.md §6.1 에 정성적 서술만 있고 수치가 없다. 값은 밸런싱 대상이지만
어떤 파라미터가 존재하는지는 지금 확정해야 한다 — simulation.md §5.1 과 terrain.md §8 이 이 값들을 전제한다.
필요한 열: maxDamage, blastRadius, RESIST_Q8 오버라이드 여부, 기본 탄약, 가격,
특수 파라미터(분열 수, 굴착 깊이, 전복 최대 거리, 적층 셀 수).
관련 결정: DAMAGE_SHIFT 가 상수표에 없다. simulation.md §5.1 의
damage = (maxDamage * (blastRadius - dist)) >> DAMAGE_SHIFT 는 dist=0 에서 최대 피해가
maxDamage 가 되려면 blastRadius == 2^DAMAGE_SHIFT 여야 한다.
추천: 무기 테이블에 blastRadius(2의 거듭제곱 강제)와 damageShift = log2(blastRadius) 를 함께 저장하고
테스트로 blastRadius == 1 << damageShift 를 검증한다.
전부 구현됐다. 추천안대로 갔고 배치 모델(terrain.md §8)도 지켰다.
| 요구 | 어디에 | 확정된 규칙 |
|---|---|---|
| 적층탄의 지형 추가 | terrain.deposit() |
EMPTY 만 채운다. 비-EMPTY 는 건너뛴다 — 재질을 덮어쓰면 지층 서사가 깨진다 |
| 탱크 자리에 쌓이면 | 별도 규칙 없음 | 탱크는 격자에 없으므로 흙이 그 자리를 채우고, 결과는 매몰 판정이 흡수한다 (match.md §5 step 9) |
| 전복탄·굴착탄의 이동 | weapons.resolve_shot() |
추천대로 해결 페이즈 안의 탄도 서브모드다. 이동 상한(rollCells·burrowCells)이 있고, 정지 지점이 폭발 지점이 된다. 모든 폭발 지점이 확정된 뒤 카빙을 한 배치로 적용한다 |
| 좌우 동률일 때의 전복 방향 | hash32(seed, x, y, k) |
난수는 이것 하나뿐이라 재생 가능하다 |
값은 constants.WEAPON_TABLE 이 유일한 사본이고 골든 헤더 대조가 TS 와 묶는다 (C1).
simulation.md §8 의 두 곳이 검산과 어긋난다. §8 을 정정했다.
GRAVITY = 12 subpx/tick²→ 실제12 ÷ 16 × 60² = 2700 px/s². 표의≈ 2.8 px/s²는SUBPX나눗셈을 두 번 적용한 값으로 약 964배 어긋났다POWER_SCALE = 320에서power=1000, 45° 발사 사거리는 491 px 다 (node tools/ballistics-check.mjs정수 적분 실측). 맵이 1920 px 이므로 화면 26% 다.roadmap.mdPhase 1 의 "적당한 대각선이 화면 절반쯤 날아간다"와 2배 어긋난다
✅ B12 에서 목표가 맵 폭의 100% ± 5% 로 확정되었다. POWER_SCALE = 624 → 1,900px = 98.9%.
아래 표는 옛 목표(50%)를 기준으로 계산한 것이며 참고용으로 남긴다.
| 사거리 | 체공 | 부작용 | |
|---|---|---|---|
POWER_SCALE 320 → 448 |
970 px (50.5%) | 36 → 51 tick | 작다 |
GRAVITY 12 → 6 |
995 px (51.8%) | 36 → 73 tick | 체공 2배 → 바람 영향 2배 |
추천: POWER_SCALE 조정. 체공 증가폭이 작아 WIND_MAX(C4)를 따로 다시 잡지 않아도 된다.
확정은 Phase 1 프로토타입에서 체감으로.
기존 100% 영향도에서 수평 편차/사거리 비는 약 w/g 다. 정수 적분 실측
(node tools/ballistics-check.mjs, POWER_SCALE=320, GRAVITY=12):
WIND_MAX |
45° (사거리 491px) | 80° (사거리 169px) |
|---|---|---|
| 6 (중력의 절반) | 246px = 50% | 482px = 285% |
| 4 | 164px = 33% | 322px = 190% |
| 2 | 81px = 17% | 161px = 95% |
| 1 | 41px = 8% | 80px = 47% |
WIND_MAX = 6 이면 고각 사격은 조준값이 아니라 바람이 착탄점을 결정한다.
표시 범위 WIND_MAX = 4는 유지한다. 2026-09-17 플레이 피드백으로 탄도 영향 계수
WIND_SCALE_Q8 = 192를 도입해 같은 바람에서도 수평 가속을 25% 줄인다.
최대 표시 바람 4의 실효 가속은 3 subpx/tick²이고, 45° 편차는 약 33%에서 25%가 된다.
약한 바람 ±1도 사라지지 않도록 누적 Q8 가속의 정수 차분을 적용하며 양방향 반올림은 대칭이다.
턴 변화는 B3의 60% 유지·40% ±1 정책을 따른다. 고각만 별도로 제한하는 클램프는 이번 변경에 포함하지 않는다.
단, 상수만으로는 고각이 해결되지 않는다. WIND_MAX = 1 이어도 80° 에서 47% 다.
바람은 체공의 제곱으로 누적되는데 고각은 체공이 길고 사거리가 짧기 때문이다.
편차 클램프(체공 틱에 비례하는 상한)를 넣을지 함께 결정한다. 안 넣으면 고각 사격이
"운"이 되고, 그건 simulation.md §5.1 이 제곱 감쇠를 거부한 것과 같은 이유로 피해야 한다.
game-design.md §5 는 0.5° 단위라 하고, netcode.md §5.3 검증은 angle10 ∈ [0,1800] 전체를 허용하며
simulation.md §3 의 trig 표도 0.1° 해상도를 전부 준비한다.
추천 (a). 0.1° 를 정식 단위로 승격하고 game-design.md §5 표를 수정.
키보드는 1회 5(=0.5°), Shift 1(=0.1°). 프로토콜·trig 표와 일관된다.
함께 명시할 것: 파워의 키보드 스텝, 드래그 최대 당김 거리(px) → 파워 1500 매핑 (상한은 B22).
game-design.md §3 은 5라운드인데 근거 문장이 "상점 3회"라 한다. 라운드 사이에만 열리면 4회다.
또 "시작 자금은 라운드 1 시작 시 지급"인데 상점이 라운드 사이에만 열리면
시작 자금을 쓸 기회가 라운드 1 종료 후에야 생겨 라운드 1은 표준탄만으로 치러진다.
추천 (a). 매치 시작 직후 라운드 1 전에 상점을 1회 열고(roundNo=0) 총 5회.
시작 자금이 즉시 의미를 가진다.
Phase 3.5 프로토타입 직접 플레이에서 동시 턴이 한 발의 긴장과 지형 변화의 인과를 없앤다는 피드백이 확인됐다. B14에서 완전 순차 턴을 표준 규칙으로 확정했다. 별도 동시 모드는 MVP에 넣지 않는다.
game-design.md §6.1 의 시그니처 무기가 요구한 기능이 전부 들어갔다. 구현 위치는 C2.
| 무기 | 필요했던 것 | 지금 |
|---|---|---|
| 적층탄 | deposit() — 셀을 추가하는 연산 |
terrain.deposit(). EMPTY 만 채운다 |
| 전복탄 · 굴착탄 | 착탄 후 지형을 따라 이동 | 해결 페이즈의 탄도 서브모드 (C2) |
| 분열탄 | 다중 발사체와 분열 규칙 | 정점에서 5발, 산개 splitSpread = 34. 좌우 대칭 고정 오프셋이라 난수가 없다 |
| 성형탄 | 피해 반경 ≠ 카빙 반경 | blastRadius 512 · carveCells 5 로 분리 |
| 측풍계 | 바람 정보를 자기만 정확히 안다 | 아래 결정대로 다음 턴 예보로 재정의 |
마지막 항목이 특히 문제다 — 모든 클라가 같은 입력으로 같은 계산을 하므로 "나만 정확한 바람 값을 안다"는 것이 원리적으로 불가능하다. 서버가 다른 값을 보내면 재생이 갈라진다. 결정: 현재 바람 값은 모두에게 숫자로 표시하고, 측풍계는 다음 턴 바람의 세기와 방향을 예보하는 UI 아이템으로 재정의한다. 권위 입력을 숨기지는 않고 대응 시간을 사는 편의 효과로 남긴다.
deposit() 명세는 terrain.md §8.1 에 있다.
결정. 낙하는 그 턴의 발사자, 매몰은 묻은 사람에게 귀속한다.
매몰을 발사자로 두지 않는 이유: 매몰은 과거 턴에 생긴 지속 상태라 매 턴 발사자로
갈아끼우면 남이 묻어놓은 적 쪽으로 아무 데나 쏘기만 해도 턴당 피해와 처치 크레딧을
가져간다. 유발자를 buriedBy 로 들고 있다가 풀릴 때 지운다.
스스로 걸어 내려간 낙하(연료 이동)는 귀속 대상이 없다 — 유발한 발사가 아직 없다.
사망 귀속은 마지막 타격을 넣은 쪽이다. docs/match.md §5.2.
함께 확정. 피해 정산은 실제로 깎인 HP 만큼이다. 계산상 전량을 주면 빈사 상태 적에게 큰 무기를 맞혔을 때 기여의 수십 배를 받는다 (남은 HP 3 에 핵포탄 → 960G).
원래 논의
game-design.md §7 의 보상은 직접 명중 데미지에만 붙어 있다. 그런데 이 게임이 정체성이라
선언한 경로 — 지형을 무너뜨려 상대를 떨어뜨리거나 묻는 것 — 에는 귀속 규칙이 없다.
지금 규칙대로면 지형으로 죽이는 플레이가 경제적으로 손해다.
추천. 낙하·매몰 피해를 "그 붕괴를 유발한 발사자"에게 귀속시킨다. 순차 턴에서는 한 발의 다중 자탄도 소유자가 모두 같으므로 귀속이 모호하지 않다. B7 의 점수 공식과 같이 정한다.
- 매몰 판정 임계값(80%)과 지속 피해량
- 낙하 피해 계수 (
FALL_DAMAGE_NUM,FALL_DAMAGE_SHIFT가 상수표에 없다) - 탱크끼리의 충돌 처리 — 현재는 서로 통과. 겹쳐 앉을 수 있는데 그대로 둘지
- 6인 개인전의 팀색·마커 (
rendering.md는 4종만 정의,game-design.md§3 은 6인 확정) - 탄도 잔상 유지 턴 수 (
game-design.md§5.1 은 직전 1발,rendering.md§1.5 는 복수 턴 누적) - 입자 수량 규칙 —
rendering.md§5 의 비율 1/20 과 절대 수량 ~2000 이 양립하지 않는다. 대형 붕괴는 스텝당 수만 셀이 움직인다. 절대 상한 + 링 버퍼 재사용으로 바꿔야 한다 weather()곡선, 원경 능선 생성 방식, 서체 3종 확정
Phase 0 에서 실측으로 답이 나와 본문으로 옮긴 것들.
| 항목 | 결론 | 위치 |
|---|---|---|
| 우선순위 수식이 서술과 반대 | 가중치를 규칙번호의 역순으로 (규칙1=3) | terrain.md §3.2 |
| "동점은 발생하지 않는다" | 성립하지 않는다. 방향 비트로 구조적 제거 | terrain.md §3.2 |
해시의 h & 1 |
FNV-1a 는 하위 비트로 확산되지 않는다. 최종 확산 단계 필수 | terrain.md §7.1 |
| 활성 집합 정의 | "움직인 셀"이 아니라 "움직일 수 있는 셀" | terrain.md §5.1 |
| 정착 판정 | 가동 셀 0개. STABLE_FRAMES 불필요 |
terrain.md §5.2 |
| 격자 경계 처리 | 조건 분기로. 인덱스 산술 금지 | terrain.md §1.1 |
slideChance 의 실수 표기 |
Q8 정수(0~256) | terrain.md §4.1 |
SUBSTEPS 의 지위 |
시뮬레이션 상수가 아니라 표현 상수 | terrain.md §3.4 |
| 연결성 검사 시점 | 모든 carve 후 무조건 1회 + 정착 후 1회 | terrain.md §6.1 |
| carve 판정식과 단위 | 셀 좌표·셀 반경, 제곱 비교, RESIST_Q8 표 |
terrain.md §8 |
| Phase 0 완료 조건의 "화면 1/4" | 실전에 없는 규모. 최대 무기 반경으로 판정 | roadmap.md Phase 0 |
| 개발환경·로컬 실행 문서 없음 | docs/development.md 신설 |
— |
2차 감사(교차 렌즈 3종 + 회귀 검사)에서 나온 것들.
| 항목 | 결론 | 위치 |
|---|---|---|
해시 확산에 산술 시프트 >> |
논리 시프트 >>> 여야 한다. h ≥ 2³¹ 인 절반의 셀에서 값이 갈라진다 |
terrain.md §7.1.1 |
| 좌표·해시의 시프트 규칙이 하나였다 | 산술(좌표) / 논리(해시)로 분리 | simulation.md §2.2 |
vy = -(v0*SIN) >> 12 |
단항 마이너스가 먼저 묶여 -221 이 된다. -((...)>>12) 로 괄호 필수 |
simulation.md §2.2, §4.1 |
§5.1 의 min..max 과대 포함 허용 |
철회. §5.2 강제 종료가 불변식을 깨서 두 구현이 갈라진다 | terrain.md §5.1 |
정착 재개 시 step 리셋 여부 |
리셋하지 않는다. 턴당 하나의 카운터 | terrain.md §7.1 |
| §3.1 의사코드에 확률 게이트가 없었다 | 게이트를 규칙 3 에만, 출처 재질 임계로, 폴백 없이 | terrain.md §3.1 |
| §6.1 "턴당 정확히 1회" vs "정착 후 1회 더" | 변환 0 까지의 루프로 통일 + 상한 | terrain.md §6.1 |
BLAST_RESIST_Q8[BEDROCK] = 0 |
0 은 무적이 아니라 "반경 0" — 중심 1셀이 지워진다. 키를 뺐다 | constants.py, 테스트 추가 |
simVersion = 소스 해시 |
두 언어 사이에서 성립 불가. 상수 해시로 교체 | netcode.md §7.3 |
| 하네스가 상시 실패 → 게이트 무력화 | SOIL 검사를 "미결" 출력으로 내렸다. 게이트 exit 0 복구 | harness.mjs |
| §11 실측표가 Q8 192/38 로 측정 | Q8 256 으로 재측정 + 측정 조건 명시 | terrain.md §11 |
SUBSTEPS 52 가 렌더 예산을 깼다 |
재측정 후 26 이면 3.7 ms 로 예산 안 | terrain.md §4.1, rendering.md §8.1 |
rendering.md §4.1 "변경 행" |
활성 행은 변경 행의 상위집합. 안전한 과대 근사임을 명시 | rendering.md §4.1 |