// 의회 엔진 — ProjectSS_코어시스템_기획서.md §7(명분) · §9.2(규약) · §9.3(실적 감사). // React 의존 없는 순수 함수 + 카탈로그. 13인 위원회의 실제 우호도·표결(§9.4~9.6)은 // 이중첩자·탄핵 방어까지 포함된 4차 범위라 아직 없다 — 그 대신 규약 후보 3개 중 1개를 // 뽑는 절차는 "가중 무작위"로 흉내만 낸다. §9.1 "규약 결정과 실적 감사는 독립적"이라는 // 원칙만은 지킨다: 감사 결과가 어떤 규약이 뽑히는지에 영향을 주지 않는다. /* ── §9.3 실적 감사 ── */ // 요구치 곡선은 가안: 1R 25, 라운드당 +20. export const requiredScore = (round) => 25 + (round - 1) * 20; // 실적 점수 = Σ(족보 점수, 라운드 중 합본 누적) + 토큰 납입×5 + 명분 납입×20. export const settleScore = ({ combineScore, tokensPaid, justificationsPaid }) => combineScore + tokensPaid * 5 + justificationsPaid * 20; /* ── §9.2 규약 — 유형 A(족보표 변경) 우선, 유형 C(징벌)는 감사 실패 시 필요해서 최소 카탈로그만 ── */ export const PROTOCOL_TYPE_A = [ { id: 'protocol_flush_ban', name: '동일 부서 일괄 처리 금지 규약', effect: { kind: 'disable_hand', patternId: 'flush_axis' }, flavor: '동일 부서 건을 묶어 올리는 관행이 유착으로 지적되었습니다. 개별 상신할 것.', }, { id: 'protocol_straight_ban', name: '연번 상신 금지 규약', effect: { kind: 'disable_hand', patternId: 'straight' }, flavor: '등급을 순서대로 올리는 관행이 결재 회피 수단으로 악용되고 있습니다.', }, { id: 'protocol_straightflush_ban', name: '전결 처리 금지 규약', effect: { kind: 'disable_hand', patternId: 'straight_flush' }, flavor: '한 부서가 연번까지 독점 처리하는 것은 감사 사각지대를 만듭니다.', }, { id: 'protocol_fourkind_ban', name: '전면 개편 금지 규약', effect: { kind: 'disable_hand', patternId: 'four_kind' }, flavor: '동일 등급 4건 일괄 개편은 절차 생략 소지가 있어 당분간 금지합니다.', }, { id: 'protocol_min_grade', name: '기밀 등급 심사 강화 규약', effect: { kind: 'min_grade', minGrade: 2 }, flavor: '일반 문서 단독으로는 더 이상 합본 상신이 승인되지 않습니다.', }, ]; export const PROTOCOL_TYPE_C = [ { id: 'penalty_gauge_accel', name: '미결 서류 관리 규정', effect: { kind: 'gauge_surcharge', amount: 1.0 }, flavor: '미결 누적에 대한 문책성 규정 — 모든 축의 결재당 게이지 상승이 가산됩니다.', }, { id: 'penalty_token_surcharge', name: '긴급 재정 감사 대응', effect: { kind: 'token_surcharge', amount: 2 }, flavor: '감사 대응 인력 편성 비용이 모든 토큰 지출에 얹힙니다.', }, ]; const pick = (arr, n) => { const pool = [...arr]; const out = []; while (out.length < n && pool.length > 0) { out.push(pool.splice(Math.floor(Math.random() * pool.length), 1)[0]); } return out; }; /** 매 라운드 규약 후보 3개를 뽑는다 (유형 A만, §17 2차 범위). */ export const rollProtocolCandidates = () => pick(PROTOCOL_TYPE_A, 3); /** 후보 중 1개를 "표결 통과"로 확정한다 — 실제 위원회가 없어 가중 무작위로 대신한다. */ export const resolveProtocolVote = (candidates) => { if (candidates.length === 0) return null; return candidates[Math.floor(Math.random() * candidates.length)]; }; export const rollPenalty = () => pick(PROTOCOL_TYPE_C, 1)[0]; /* ── §7.2 명분 개입 — 규약 후보에 개입한다. 실제 13인 표결이 없어 "1표 뒤집기"(혼합 명분)는 * 문자 그대로 구현할 표가 없다. 대신 혼합 명분은 "후보 중 하나를 직접 지정 통과"로 대체했다 — * 표 조작의 최종 효과(원하는 규약을 통과시킨다)는 같지만, 이건 §9.4~9.6의 실제 위원회가 * 생기면 반드시 다시 설계해야 할 자리표시자다. */ export const JUSTIFICATION_INTERVENTIONS = { 절차: { label: '규약 이월 (이번 라운드 규약 보류)', needsTarget: false }, 여론: { label: '후보 1개 강제 탈락', needsTarget: true }, 위기: { label: '누적 징벌 규약 1개 조기 해제', needsTarget: 'penalty' }, 내부: { label: '이번 라운드 실적 요구치 하향 (-20%)', needsTarget: false }, 혼합: { label: '후보 1개 직접 통과 지정', needsTarget: true }, }; /** * 명분 개입을 규약 후보에 반영한다 (§7.2 규약 개입). * interventions: [{ type: '절차'|'여론'|'위기'|'내부'|'혼합', targetCandidateId?, targetPenaltyId? }] * @returns {{ candidates, forcedWinnerId, deferVote, requiredScoreMultiplier, penaltyIdToClear }} */ export function applyInterventions(candidates, interventions) { let pool = [...candidates]; let forcedWinnerId = null; let deferVote = false; let requiredScoreMultiplier = 1; let penaltyIdToClear = null; interventions.forEach((iv) => { if (iv.type === '절차') deferVote = true; if (iv.type === '여론' && iv.targetCandidateId) { pool = pool.filter((c) => c.id !== iv.targetCandidateId); } if (iv.type === '내부') requiredScoreMultiplier *= 0.8; if (iv.type === '위기' && iv.targetPenaltyId) penaltyIdToClear = iv.targetPenaltyId; // 혼합 명분: 실제 표 뒤집기가 아니라 "직접 통과 지정"으로 대체한 자리표시자 — 위 §7.2 주석 참고. if (iv.type === '혼합' && iv.targetCandidateId) forcedWinnerId = iv.targetCandidateId; }); return { candidates: pool, forcedWinnerId, deferVote, requiredScoreMultiplier, penaltyIdToClear }; } /* ── 합본 판정에 규약 효과를 얹는다 ── */ /** 활성 유형 A 규약(들)을 반영해 detectHands 결과를 거른다. */ export function filterHandsByProtocols(matches, selection, activeProtocols) { let allowed = matches; activeProtocols.forEach((p) => { if (p.effect.kind === 'disable_hand') { allowed = allowed.filter((id) => id !== p.effect.patternId); } if (p.effect.kind === 'min_grade') { const violatesMinGrade = selection.some((c) => c.grade < p.effect.minGrade); if (violatesMinGrade) allowed = []; } }); return allowed; } /** 활성 징벌(유형 C)이 게이지 상승률에 더하는 가산분 합계. */ export function penaltyGaugeSurcharge(penalties) { return penalties.reduce((sum, p) => sum + (p.effect.kind === 'gauge_surcharge' ? p.effect.amount : 0), 0); } /** 활성 징벌(유형 C)이 토큰 지출 가산에 더하는 합계. */ export function penaltyTokenSurcharge(penalties) { return penalties.reduce((sum, p) => sum + (p.effect.kind === 'token_surcharge' ? p.effect.amount : 0), 0); }