From 3ac616fb5b908a675d73c0fe476611e92fab9fb3 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 8 Sep 2026 22:33:21 +0900 Subject: [PATCH] =?UTF-8?q?feat(B06):=20=EA=B5=AC=EC=A1=B0=EB=AC=BC=20?= =?UTF-8?q?=EC=9E=94=ED=86=A0=EB=A5=BC=20=EC=82=AC=ED=86=A0=EC=97=90=20?= =?UTF-8?q?=EB=8D=94=ED=95=98=EB=8A=94=20=ED=86=B5=EB=A1=9C=20=EC=8B=A0?= =?UTF-8?q?=EC=84=A4=20=E2=80=94=20=EA=B3=B5=EC=A0=9C=EC=99=80=20=EC=A7=9D?= =?UTF-8?q?=EC=9D=B4=20=EB=90=98=EB=8A=94=20=EC=9E=90=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 데스크탑 메인이 구조물 터파기·되메우기·잔토를 인계에 실었는데, **잔토 126.625㎥ 가 어디에도 안 실렸다**. 사토로 실어 내는 몫이라 유토곡선이 세야 겹치지 않는다. 구조물 잔토는 사토에 한 번만 더한다. B08 은 소요량(structure_spoil_m3, ㎥ 양수)을 내기만 하고, 더하는 자리는 유토곡선의 사토뿐이다. - ⚠ **잔량 하나하나를 늘린다** — 총량만 늘리면 사토는 늘고 운반이 안 는다(공제 때와 같은 자리). - ⚠ 나누는 법: 지금은 총량 하나만 오므로 **남은 사토 잔량 크기에 비례**해 나눈다. 한 곳에 몰면 운반거리가 틀린다. 측점별 값이 오면 이 함수만 고치면 된다. - ⚠ 자연방토·지반유형 안분은 안 건드린다 — 실어 내는 흙이고, 어느 지반에서 나온지 모른다. - `null`(아직 안 옴) ≠ `0`(없음), 받은 값과 **실제로 더한 값**을 따로 싣는다. 시험 5건(더해짐·잔량 동반 증가·자연방토 불변·토취 불변·공제와 함께 와도 한 번씩). Co-Authored-By: Claude Opus 5 (1M context) --- B06_Section/B06_Section_Server_Calc_Node.ts | 4 ++ .../B06_Section_Server_Calc_Prebuild.py | 10 +++- common_util/common_util_mass_haul_balance.ts | 49 ++++++++++++++++++- 3 files changed, 61 insertions(+), 2 deletions(-) diff --git a/B06_Section/B06_Section_Server_Calc_Node.ts b/B06_Section/B06_Section_Server_Calc_Node.ts index 1dc529e6..dbcd4d4b 100644 --- a/B06_Section/B06_Section_Server_Calc_Node.ts +++ b/B06_Section/B06_Section_Server_Calc_Node.ts @@ -39,6 +39,8 @@ interface ServerCalcInput { haul_equipment_limits?: Parameters[1]; /** 채집석 공제(㎥, 양수) — B08 이 낸다. `null`/없음은 「아직 안 옴」이다. */ collected_stone_deduction_m3?: number | null; + /** 구조물 터파기 잔토(㎥, 양수) — B08 이 낸다. 사토에 **더한다**. */ + structure_spoil_m3?: number | null; }; } @@ -56,6 +58,7 @@ if (input.haul_plan_for) { // 그리기 코드가 손대지 않고 그대로 받는다. 전부 숫자·문자열이라 JSON 으로 오간다. const plan = computeHaulPlan(input.haul_plan_for, input.context?.haul_equipment_limits, { collected_stone_deduction_m3: input.context?.collected_stone_deduction_m3 ?? null, + structure_spoil_m3: input.context?.structure_spoil_m3 ?? null, }); writeFileSync(outputPath, JSON.stringify({ haul_plan: plan ?? null })); process.exit(0); @@ -80,6 +83,7 @@ const result = conversion const plan = result ? computeHaulPlan(result, input.context?.haul_equipment_limits, { collected_stone_deduction_m3: input.context?.collected_stone_deduction_m3 ?? null, + structure_spoil_m3: input.context?.structure_spoil_m3 ?? null, }) : null; const massHaul = result diff --git a/B06_Section/B06_Section_Server_Calc_Prebuild.py b/B06_Section/B06_Section_Server_Calc_Prebuild.py index 8e43e6f1..727e7b56 100644 --- a/B06_Section/B06_Section_Server_Calc_Prebuild.py +++ b/B06_Section/B06_Section_Server_Calc_Prebuild.py @@ -63,7 +63,10 @@ _AREA_KEYS = ( ) -def _mass_haul_context(collected_stone_deduction_m3: float | None = None) -> dict[str, Any]: +def _mass_haul_context( + collected_stone_deduction_m3: float | None = None, + structure_spoil_m3: float | None = None, +) -> dict[str, Any]: """유토곡선 계산에 필요한 값 — 화면이 `sections/context`로 받는 것과 같은 상수다. ⚠ 채집석 공제(`collected_stone_deduction_m3`)만 상수가 아니라 **B08 이 내는 값**이다. @@ -85,6 +88,11 @@ def _mass_haul_context(collected_stone_deduction_m3: float | None = None) -> dic # ⚠ B08 이 아직 이 값을 내지 않는다(2026-09-09) — 그때까지 `None`(아직 안 옴)이다. # 값을 내기 시작하면 여기에 실어 주기만 하면 통로가 이어진다. "collected_stone_deduction_m3": collected_stone_deduction_m3, + # 구조물 터파기 잔토(㎥, 양수) — **사토에 더한다**(공제는 빼고 이것은 더한다). + # 구조물 잔토는 사토에 한 번만 더한다. + # B08 은 소요량(structure_spoil_m3, ㎥ 양수)을 내기만 하고, + # 더하는 자리는 유토곡선의 사토뿐이다. + "structure_spoil_m3": structure_spoil_m3, } diff --git a/common_util/common_util_mass_haul_balance.ts b/common_util/common_util_mass_haul_balance.ts index cce5d269..6676d4b9 100644 --- a/common_util/common_util_mass_haul_balance.ts +++ b/common_util/common_util_mass_haul_balance.ts @@ -199,6 +199,13 @@ export interface HaulPlan { collected_stone_deduction_m3: number | null; /** 실제로 사토에서 뺀 양(㎥). 사토가 모자라면 받은 값보다 작을 수 있다. */ collected_stone_deducted_m3: number; + /** + * 구조물 터파기가 남긴 잔토(㎥, 양수) — **사토에 더한다**. `null` 은 「아직 안 옴」이고 + * `0` 은 「없음」이다(공제와 같은 태도). + */ + structure_spoil_m3: number | null; + /** 실제로 사토에 더한 양(㎥). 받을 잔량이 없으면 받은 값보다 작을 수 있다. */ + structure_spoil_added_m3: number; /** 블록 안에서 옮기는 양(㎥). */ hauled_m3: number; /** 떨어진 구간끼리 장거리로 옮기는 양(㎥). */ @@ -367,10 +374,46 @@ function applyCollectedStoneDeduction(residuals: HaulResidual[], deduction: numb return deduction - Math.max(left, 0); } +/** + * 구조물 잔토 — **사토에 한 번만 더한다**(2026-09-09 네 창 합의). + * + * 구조물 잔토는 사토에 한 번만 더한다. + * B08 은 소요량(structure_spoil_m3, ㎥ 양수)을 내기만 하고, + * 더하는 자리는 유토곡선의 사토뿐이다. + * + * ⚠ **잔량 하나하나를 늘린다** — 총량만 늘리면 사토는 늘고 **운반이 안 는다**(공제 때와 같은 자리). + * ⚠ **나누는 법** — B08 이 지금은 **총량 하나**만 준다. 어느 측점에서 나온 잔토인지 모르므로 + * **남은 사토 잔량의 크기에 비례**해 나눈다. 한 곳에 몰면 운반거리가 틀리기 때문이고, + * 측점별 값이 오면 그때 그 자리에 얹을 것(그때는 이 함수만 고치면 된다). + * ⚠ **자연방토(`natural_m3`)는 안 늘린다** — 구조물 잔토는 실어 내는 흙이다. + * ⚠ **지반유형 안분(ea/rr/br)도 안 건드린다** — 어느 지반에서 나온 흙인지 모른다. + * 합계(`volume_m3`)와 갈래 합이 어긋나는 것은 그 사실을 드러내는 표시다. + * + * 돌려주는 값은 **실제로 더한 양(㎥)**. 받을 사토 잔량이 하나도 없으면 0 이다. + */ +function applyStructureSpoil(residuals: HaulResidual[], amount: number | null): number { + if (amount === null || !Number.isFinite(amount) || amount <= 0) return 0; + const spoils = residuals.filter((residual) => residual.kind === "spoil"); + const total = spoils.reduce((sum, residual) => sum + residual.volume_m3, 0); + if (!spoils.length || total <= EPSILON) return 0; + let added = 0; + spoils.forEach((residual, index) => { + // 마지막 잔량은 나머지를 그대로 받아 반올림 오차가 새지 않게 한다. + const share = + index === spoils.length - 1 ? amount - added : amount * (residual.volume_m3 / total); + residual.volume_m3 += share; + added += share; + }); + return added; +} + export function computeHaulPlan( result: MassHaulResult, limits: HaulEquipmentLimit[] | undefined, - options?: { collected_stone_deduction_m3?: number | null }, + options?: { + collected_stone_deduction_m3?: number | null; + structure_spoil_m3?: number | null; + }, ): HaulPlan | null { const points = result.points; if (points.length < 2) return null; @@ -551,6 +594,8 @@ export function computeHaulPlan( const deductionInput = options?.collected_stone_deduction_m3 ?? null; const deducted = applyCollectedStoneDeduction(settled, deductionInput); + const structureSpoilInput = options?.structure_spoil_m3 ?? null; + const structureSpoilAdded = applyStructureSpoil(settled, structureSpoilInput); const remaining = settled.filter((residual) => residual.volume_m3 > EPSILON); remaining.forEach((residual, index) => { residual.index = index + 1; @@ -577,6 +622,8 @@ export function computeHaulPlan( natural_spoil_m3: naturalSpoil, collected_stone_deduction_m3: deductionInput, collected_stone_deducted_m3: deducted, + structure_spoil_m3: structureSpoilInput, + structure_spoil_added_m3: structureSpoilAdded, hauled_m3: blocks.reduce((sum, block) => sum + block.volume_m3, 0), transferred_m3: transfers.reduce((sum, entry) => sum + entry.volume_m3, 0), fill_total_m3: fillTotal,