등급 1~10 확장 + 라운드 주목도 하드 캡 재구현, 문서 체계 정비
서울 PC에서 Remote-SSH로 작업하던 중 연결이 끊겨 커밋 전 상태로 유실된 2026-09-15 결정사항(등급 1~4→1~10 확장, 5장 포커 핸드, 주목도 기반 페널티, 토큰 1종 통합, 상점/규약 재작성)을 평택 로컬 체크아웃에서 전체 재구현했다. 그 과정에서 라운드/회기 구조를 오해해서 잘못 반영했던 부분을 다시 고쳤다. **데이터** - cards.csv 43개 선택지, pollution_pool.csv 재매핑 — 등급 1·3 공백을 없애고 1~10 전 구간을 채움 (평균 등급 4.14, 8결재 기준 예상 주목도 33) - pollution_pool.csv에 등급 1·3 전용 오염 카드 8개 신규 추가 (축별 2개) **게임 루프** - 라운드/회기 이층 구조 도입 → 원래 의도(라운드마다 정산·의회를 거치는 것)와 어긋난 오해로 판명, 단일 라운드 구조로 환원 - 라운드 종료 조건을 "결재 8건 고정"에서 블랙잭식 하드 캡으로 교체 — 주목도가 라운드 한도(21 + 라운드당 +5)를 넘으면 합본 기회 없이 즉시 강제 종료(버스트), 한도 밑에서는 "정산하기"로 언제든 자율 종료 가능 - CouncilScreen: 징벌 규약 5개 초과 시 목록 접기/펼치기 (§9.2 UI 요건) **문서 체계** - CLAUDE.md: 흩어진 기획 문서 7종 참조를 코어시스템 기획서 단일 참조(§ 매핑 포함)로 교체, Phase 진행 표 폐지 후 §17 체크박스를 진행 상황 원본으로 지정 - CLAUDE.md에 "결정 노트" 규칙 신설 — T-decision.md 양식 + DEVLOG 위키링크 연동 - docs/DEVLOG.md 신규 작성 (구 버전은 old/2026.09.04/ 보관) - 결정 노트 2건 작성: 등급확장-주목도시스템, 라운드-주목도-하드캡-복원 - ProjectSS_코어시스템_기획서.md §2/§4.2-B/§17/§19/§20을 현재 결정에 맞게 갱신 Playwright로 등급 1·3 카드 실등장, 라운드 번호 증가, 주목도 초과 시 버스트→ 상점 강제 전환까지 실제 브라우저에서 확인. 콘솔 에러 0건. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
cde44c4f2c
commit
0a065068c1
@@ -1,16 +1,23 @@
|
||||
// 책상(Desk) 엔진 — ProjectSS_코어시스템_기획서.md §4.
|
||||
// 오염 카드는 덱에 들어가지 않는다. 드로우·셔플 없음, 슬롯 무제한. 여기서 다루는 desk는
|
||||
// 순수 배열 [{ id, axis, grade }] 이며, React state는 이 배열을 그대로 들고 있다가
|
||||
// 이 모듈의 함수로 파생값(축별 스택, 패널티)을 계산한다. React/훅 의존 없음 — 순수 함수만.
|
||||
// 오염 카드는 덱에 들어가지 않는다. 드로우·셔플 개념 없음, 슬롯 무제한. 여기서 다루는 desk는
|
||||
// 순수 배열 [{ id, axis, grade, name, flavor, tag }]이며, React state는 이 배열을 그대로 들고
|
||||
// 있다가 이 모듈의 함수로 파생값(주목도, 페널티)을 계산한다. React/훅 의존 없음 — 순수 함수만.
|
||||
//
|
||||
// 2026-09-15 결정 반영: 페널티 기준을 "카드 장수" → "등급 합계"로 전환하고 이를 주목도라 부른다.
|
||||
// 등급 편차가 1~10으로 커진 만큼, 장수만 세면 특급 기밀과 일반 문서가 똑같이 취급돼 압박이
|
||||
// 정직하지 않다. 등급 자체가 곧 가중치이므로 별도 가중치 테이블은 없다.
|
||||
//
|
||||
// 이월(§4.1): 합본에 사용되지 않은 카드는 소멸하지 않고 책상에 남는다. 별도 이월 시스템이
|
||||
// 없는 이유가 바로 이것 — "합본이 닿은 카드만 사라진다"는 규칙 하나로 자연히 성립한다.
|
||||
|
||||
import { AXES } from './symbols.js';
|
||||
import { AXES, AXIS_META, clampGrade } from './symbols.js';
|
||||
|
||||
let seq = 0;
|
||||
// meta: pollutionPool.js에서 뽑은 { name, flavor, tag } — 책상 카드가 실제 이름/사연을 갖게 한다.
|
||||
export const makeDeskCard = (axis, grade, meta = {}) => ({
|
||||
id: `desk-${Date.now().toString(36)}-${(seq++).toString(36)}`,
|
||||
axis,
|
||||
grade,
|
||||
grade: clampGrade(grade),
|
||||
name: meta.name || null,
|
||||
flavor: meta.flavor || null,
|
||||
tag: meta.tag || null,
|
||||
@@ -23,7 +30,7 @@ export function countByAxis(deskCards) {
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 축별 등급 분포. { axis_threat: {1: 2, 3: 1}, ... } — DeskBar의 축×등급 매트릭스 표시용. */
|
||||
/** 축별 등급 분포. { axis_threat: {7: 2, 3: 1}, ... } — DeskBar의 축×등급 표시용. */
|
||||
export function gradeBreakdownByAxis(deskCards) {
|
||||
const out = Object.fromEntries(AXES.map((a) => [a, {}]));
|
||||
deskCards.forEach((c) => {
|
||||
@@ -34,41 +41,50 @@ export function gradeBreakdownByAxis(deskCards) {
|
||||
|
||||
export const totalCount = (deskCards) => deskCards.length;
|
||||
|
||||
/* ── A. 게이지 상시 상승 (§4.2-A, 플레이테스트 피드백 반영 재조정) ──
|
||||
* 최초 구현은 "장수"만 셌는데, 1급 기밀이나 일반 문서나 똑같이 취급돼 위기감이 없었다.
|
||||
* 그래서 장수 대신 등급 가중 "오염 압력"(P)을 쓴다: 등급1=1, 등급2=2, 등급3=3, 등급4=5.
|
||||
* 구간도 초반 체감이 되도록 올렸다 — 정확한 수치는 가안, 플레이테스트로 계속 조정한다.
|
||||
*/
|
||||
const GRADE_WEIGHT = { 1: 1, 2: 2, 3: 3, 4: 5 };
|
||||
/* ── 주목도 (§4.2) — 페널티 산정의 단일 기준 ── */
|
||||
|
||||
/** 해당 축의 오염 압력(P) = Σ 카드별 등급 가중치. */
|
||||
export function axisPressure(deskCards, axis) {
|
||||
/** 해당 축의 주목도 = 그 축 카드들의 등급 합. */
|
||||
export function axisAttention(deskCards, axis) {
|
||||
return deskCards
|
||||
.filter((c) => c.axis === axis)
|
||||
.reduce((sum, c) => sum + (GRADE_WEIGHT[c.grade] || 0), 0);
|
||||
.reduce((sum, c) => sum + c.grade, 0);
|
||||
}
|
||||
|
||||
/** 책상 전체 주목도 = 모든 카드의 등급 합. */
|
||||
export function totalAttention(deskCards) {
|
||||
return deskCards.reduce((sum, c) => sum + c.grade, 0);
|
||||
}
|
||||
|
||||
export function attentionByAxis(deskCards) {
|
||||
const out = {};
|
||||
AXES.forEach((axis) => { out[axis] = axisAttention(deskCards, axis); });
|
||||
return out;
|
||||
}
|
||||
|
||||
/* ── A. 게이지 상시 상승 (§4.2-A) — 축별 주목도 기준 누진 ── */
|
||||
|
||||
const GAUGE_TIERS = [
|
||||
{ max: 0, rate: 0 },
|
||||
{ max: 2, rate: 1.0 },
|
||||
{ max: 6, rate: 2.0 },
|
||||
{ max: 12, rate: 3.5 },
|
||||
{ max: 20, rate: 6.0 },
|
||||
{ max: 30, rate: 9.0 },
|
||||
{ max: Infinity, rate: 13.0 },
|
||||
{ max: 15, rate: 0.5 },
|
||||
{ max: 30, rate: 1.5 },
|
||||
{ max: 40, rate: 3.0 },
|
||||
];
|
||||
const GAUGE_OVERRUN_FROM = 41;
|
||||
const GAUGE_OVERRUN_BASE = 5.0;
|
||||
|
||||
/** 오염 압력(P)에 따른 결재 1건당 게이지 상승률(%). */
|
||||
export function gaugeRiseRate(pressure) {
|
||||
const tier = GAUGE_TIERS.find((t) => pressure <= t.max);
|
||||
return tier ? tier.rate : 13.0;
|
||||
/** 축 주목도에 따른 결재 1건당 게이지 상승률(%). 41 이상은 초과분 10당 +1.0% 추가 누진. */
|
||||
export function gaugeRiseRate(attention) {
|
||||
const tier = GAUGE_TIERS.find((t) => attention <= t.max);
|
||||
if (tier) return tier.rate;
|
||||
const overrun = attention - GAUGE_OVERRUN_FROM;
|
||||
return GAUGE_OVERRUN_BASE + Math.floor(overrun / 10) * 1.0;
|
||||
}
|
||||
|
||||
/** 축별 현재 상승률(%) — DeskBar의 "▲+X%/결재" 표시에 그대로 쓴다.
|
||||
* extraFlat: 활성 징벌 규약(§9.2 유형C)이 얹는 가산분(council.js의 penaltyGaugeSurcharge). */
|
||||
export function gaugeRiseRateByAxis(deskCards, extraFlat = 0) {
|
||||
const out = {};
|
||||
AXES.forEach((axis) => { out[axis] = gaugeRiseRate(axisPressure(deskCards, axis)) + extraFlat; });
|
||||
AXES.forEach((axis) => { out[axis] = gaugeRiseRate(axisAttention(deskCards, axis)) + extraFlat; });
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -76,31 +92,48 @@ export function gaugeRiseRateByAxis(deskCards, extraFlat = 0) {
|
||||
export function applyPassiveGaugeRise(gauges, deskCards, extraFlat = 0) {
|
||||
const next = { ...gauges };
|
||||
AXES.forEach((axis) => {
|
||||
const gaugeKey = axisGaugeKey(axis);
|
||||
const rate = gaugeRiseRate(axisPressure(deskCards, axis)) + extraFlat;
|
||||
const gaugeKey = AXIS_META[axis].gauge;
|
||||
const rate = gaugeRiseRate(axisAttention(deskCards, axis)) + extraFlat;
|
||||
next[gaugeKey] = Math.min(100, (next[gaugeKey] || 0) + rate);
|
||||
});
|
||||
return next;
|
||||
}
|
||||
|
||||
// symbols.js의 AXIS_META를 다시 import하면 순환 우려가 없으므로 바로 가져와 쓴다.
|
||||
import { AXIS_META } from './symbols.js';
|
||||
function axisGaugeKey(axis) {
|
||||
return AXIS_META[axis].gauge;
|
||||
/* ── B. 라운드 종료 판단 (§4.2-B, 2026-09-16 결정 재수정) — 블랙잭식 하드 캡.
|
||||
* 라운드당 주목도 한도를 넘기면 그 즉시 라운드가 강제 종료된다(합본 기회 없이, 카드는 이월).
|
||||
* 한도 자체가 라운드가 갈수록 올라가 후반에는 더 크게 쌓아도 되는 대신 압박도 커진다.
|
||||
* 한도 밑에서는 플레이어가 아무 때나 자율적으로 라운드를 끝내고 정산으로 넘어갈 수 있다 —
|
||||
* "한 장 더 받을까 말까"의 긴장은 여기서 나온다. 결정 노트: 2026-09-16-라운드-주목도-하드캡-복원. */
|
||||
|
||||
const ROUND_CAP_BASE = 21; // 가안 — 최초 블랙잭 기준으로 복귀
|
||||
const ROUND_CAP_GROWTH = 5; // 라운드당 가산분, 가안
|
||||
|
||||
/** 라운드 번호에 따른 이번 라운드의 주목도 한도. */
|
||||
export function roundAttentionCap(round) {
|
||||
return ROUND_CAP_BASE + (round - 1) * ROUND_CAP_GROWTH;
|
||||
}
|
||||
|
||||
/* ── B. 토큰 지출 가산 (§4.2-B) — 총 적재 장수에 따라 모든 토큰 지출에 가산 ── */
|
||||
/** 한도 대비 총 주목도 구간: 'normal' | 'warning'(한도 5 이내로 근접) | 'bust'(한도 초과). */
|
||||
export function attentionZone(deskCards, cap) {
|
||||
const total = totalAttention(deskCards);
|
||||
if (total > cap) return 'bust';
|
||||
if (total >= cap - 5) return 'warning';
|
||||
return 'normal';
|
||||
}
|
||||
|
||||
/* ── C. 토큰 지출 가산 (§4.2-C) — 총 주목도 기준 ── */
|
||||
|
||||
const SURCHARGE_TIERS = [
|
||||
{ max: 7, surcharge: 0 },
|
||||
{ max: 14, surcharge: 1 },
|
||||
{ max: 21, surcharge: 2 },
|
||||
{ max: 20, surcharge: 0 },
|
||||
{ max: 40, surcharge: 1 },
|
||||
{ max: 60, surcharge: 2 },
|
||||
{ max: Infinity, surcharge: 4 },
|
||||
];
|
||||
|
||||
/** 총 적재 장수 기준 모든 토큰 지출에 붙는 가산액.
|
||||
/** 총 주목도 기준 모든 토큰 지출에 붙는 가산액.
|
||||
* extraFlat: 활성 징벌 규약(§9.2 유형C)이 얹는 가산분(council.js의 penaltyTokenSurcharge). */
|
||||
export function tokenSurcharge(deskCards, extraFlat = 0) {
|
||||
const total = totalCount(deskCards);
|
||||
const total = totalAttention(deskCards);
|
||||
const tier = SURCHARGE_TIERS.find((t) => total <= t.max);
|
||||
return (tier ? tier.surcharge : 4) + extraFlat;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user