Skip to content

feat: balance lab P0 (Datra.Lab, [Knob], <DatraLab />) - #17

Merged
penspanic merged 3 commits into
mainfrom
feature/lab-p0
Sep 30, 2026
Merged

penspanic merged 3 commits into
mainfrom
feature/lab-p0

Conversation

@penspanic

Copy link
Copy Markdown
Owner

게임 수치를 표로 읽지 않고 결과로 보며 맞추는 "밸런스 실험실"의 첫 조각(P0)입니다. 게임이 필드에 [Knob] 을 붙이고 시뮬레이터 클래스 하나를 쓰면, Datra 가 손잡이 판을 만들고 봇 수십 명을 같은 시드로 돌려 단계별 시간·진행·구매 순서를 그립니다. 끄는 동안 데이터 파일에는 아무것도 쓰지 않습니다.

Unity 와 무관한 일반 부분만 들어 있습니다. 머지는 하지 않았습니다.

들어 있는 것

Datra (코어)

  • Datra/Attributes/KnobAttribute.cs: [Knob], KnobLayer, KnobApply. 데이터 어셈블리가 Datra 만 참조해도 붙일 수 있게 [TableData] 옆에 두었습니다(.meta 포함). 코어의 다른 파일은 건드리지 않았습니다.

Datra.Lab (새 프로젝트, netstandard2.1, UI 없음)

  • 손잡이 스키마: DataContext 를 리플렉션으로 훑어 [Knob] 을 모읍니다. Row(한 행), EachRow(행마다 하나), Set / Scale / Offset, 코드로 선언하는 묶음 손잡이(IKnobSet, KnobBuilder).
  • 갈래 컨텍스트: ContextForker 가 OverlayRawDataProvider 위에 같은 타입의 컨텍스트를 새로 올립니다. 읽기는 캐시된 기준 파일로, 쓰기는 오버레이에만 갑니다. 시뮬레이터는 평소처럼 data.Floor 를 읽습니다.
  • 시뮬레이터 계약: ISimulator<TContext>, SimRun(Stage, Tick, Progress, Purchase, Mark, Explain, Outcome<T>, Policy, Param), SimRandom(SplitMix64).
  • 봇 실행기: 봇 N명을 병렬로, 봇마다 파생 시드 하나. 기록은 봇 순서로 돌아와 스레드 수와 무관하게 같은 결과가 납니다.
  • 결과 JSON: ResultAggregator → LabResult(단계별 중앙값·p10·p90·목표, 진행 곡선, 대표 봇의 구매, 체크, 표시, 이유 문장, 문제 목록).
  • 물리 캐시 읽기 계약: IOutcomeReader, InMemoryOutcomes, JsonlOutcomeReader. 읽기만 있습니다.
  • 시나리오(기준 + 바꾼 손잡이)와 스냅샷(FileSnapshotStore, {이름}.json).

호스팅

  • Datra.WebEditor.Server: AddDatraLab<TContext, TSimulator>(), MapDatraLab() → /api/datra/lab/schema, /run, /snapshots. 표 편집기가 저장·리로드하면 기준을 다시 읽습니다.
  • Datra.WebEditor: <DatraLab />. 안쪽 그림은 프레임워크 없는 JS 모듈(SVG)입니다(wwwroot/lab/). 스키마로 만든 손잡이 판, 단계별 시간 막대, 진행, 구매 타임라인. 손잡이를 움직이면 저장된 데이터의 결과가 회색으로 뒤에 겹칩니다. 비교 대상을 스냅샷으로 바꿀 수 있습니다. 모듈은 source 객체(schema(), run()…)만 보므로 나중에 HTML 한 장으로 내보낼 때 같은 코드를 씁니다. 화면 글자는 Locale="ko" 로 한국어가 됩니다.

샘플과 테스트

  • Datra.Lab.Sample: 장난감 인크리멘탈 게임. 층마다 운임, 요금함이 곧 지갑(사면 진행이 내려감), 내려갈 때 돈은 전부 두고 감, 계정 업그레이드와 층 장치. 물리 배치를 흉내 낸 결과 풀에서 뽑아 씁니다.
  • Datra.WebEditor.Sample: /lab 에서 Unity 없이 끝까지 돕니다.
  • Datra.Lab.Tests(49): 스키마 추출, 갈래 격리(파일 바이트 동일, 저장된 컨텍스트 불변, 갈래끼리 객체 공유 없음, 갈래를 저장해도 오버레이에만), 시드별 결정성(같은 시드 같은 결과, 스레드 1개와 8개가 같은 결과, 봇 하나만 다시 돌려도 같은 기록), 기록·집계, 스냅샷.
  • Datra.WebEditor.Tests(+13): 인메모리 서버로 엔드포인트 왕복.
  • docs/LAB.md: Datra 가 주는 것과 게임이 쓰는 것, 소비자 코드.

게임이 쓰는 코드

[Knob("Step value", Group = "Rewards", Unit = "coins/step", Min = 1, Max = 15, Step = 0.5)]
public float StepValue { get; set; } = 5f;

public sealed class GameSim : ISimulator<GameDataContext> { public void Play(GameDataContext data, SimRun run) { … } }

builder.Services.AddDatraLab<GameDataContext, GameSim>(lab => lab.SnapshotDirectory("lab/snapshots"));
app.MapDatraLab();
<DatraLab />

설계 스케치와 다르게 한 곳

  • SimRun.Progress(fraction) 을 더했습니다. 요금함이 지갑이면 "운임을 얼마나 채웠나"가 번 돈의 합으로 나오지 않아 시뮬레이터가 직접 알려야 합니다.
  • 시간 단위는 전부 초입니다(Stage(id, targetSeconds:)). 화면만 분으로 보입니다.
  • [Knob(EachRow = true)] 를 더했습니다. 층마다 운임처럼 행마다 손잡이가 필요할 때 속성을 행 수만큼 반복하지 않습니다.
  • 물리 손잡이는 목록에 보이지만 읽기 전용이고, 시나리오에 넣으면 400 입니다.
  • AddDatraLab 은 Datra.WebEditor.Server 에 두었습니다. 컴포넌트는 엔드포인트만 부르므로 RCL 은 Datra.Lab 을 참조하지 않습니다.
  • 봇의 손잡이 적용 순서를 고정했습니다: 코드로 선언한 손잡이 → Set → Scale → Offset.

이번에 없는 것

  • 물리 재계산 큐, 추정 표시
  • 플레이 기록 겹치기와 봇 맞추기, 되돌려 보기
  • 실행 중인 게임으로 밀어 넣기
  • HTML 내보내기, 발표 모드, 공유 링크
  • 분포·민감도·훑어보기 뷰, 시나리오 diff, 시나리오를 YAML 에 적용
  • 표 편집기에서 아직 저장하지 않은 편집은 기준에 들어가지 않습니다
  • Datra.Lab 은 Unity 패키지가 아닙니다(asmdef·meta 없음). 호스트 쪽에서만 씁니다

확인

  • dotnet build Datra.sln -c Release: 오류 0. 새 경고 없음.
  • dotnet test Datra.sln -c Release: Datra.Tests 772, Datra.Editor.Tests 158, Datra.WebEditor.Tests 50, Datra.Lab.Tests 49, Datra.Analyzers.Tests 7 모두 통과.
  • 샘플 서버를 띄워 /lab 을 브라우저로 확인했습니다: 콘솔 오류 없음, 손잡이를 움직이면 막대·진행·타임라인이 바뀌고 저장값이 겹쳐 보임, 스냅샷 저장과 비교, ?lang=ko, 기존 편집기 화면(/) 그대로.
  • dotnet pack 으로 Datra.Lab, Datra.WebEditor 패키지에 JS·CSS 가 들어가는 것을 확인했습니다.
  • Unity 쪽은 확인하지 않았습니다. 코어에 더한 것은 속성 파일 하나입니다.

🤖 Generated with Claude Code

penspanic and others added 3 commits September 30, 2026 19:41
Datra.Lab lets a game tune its numbers by their outcome instead of by
reading tables: mark fields as knobs, write one simulator class, and a
batch of seeded bots plays the game on a forked copy of the data.

- Datra/Attributes: [Knob] (label, group, unit, range, Row / EachRow,
  Set / Scale / Offset, Economy / Physics layer). It sits next to
  [TableData] so a data assembly needs nothing but Datra.
- Knob schema by reflection over a data context, plus knobs declared in
  code for values that span several fields.
- ContextForker / OverlayRawDataProvider: a fork is an ordinary TContext
  on an overlay provider. Reads fall through to a cached baseline, writes
  stay in the overlay, so nothing reaches the saved data.
- ISimulator<TContext> and SimRun: stages, ticks, progress, purchases,
  marks, explanations, stored physics outcomes, policy parameters.
- BotRunner: N bots in parallel, one derived seed each, records returned
  in bot order so the result does not depend on scheduling.
- ResultAggregator and LabResult: the JSON document the views draw from.
- IOutcomeReader (read side only), InMemoryOutcomes, JsonlOutcomeReader.
- Scenario (baseline + changed knobs), snapshots, FileSnapshotStore.

Datra.Lab.Sample is a toy incremental game (floors with a fare, a fare
box that is also the wallet, everything left behind on the way down)
with its simulator; the tests and the web sample share it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Datra.WebEditor.Server: AddDatraLab<TContext, TSimulator>() and
  MapDatraLab() (/api/datra/lab/schema, /run, /snapshots). The lab reads
  the baseline through the container's IRawDataProvider and refreshes it
  when the table editor saves or reloads.
- Datra.WebEditor: <DatraLab />, a host for framework-free JavaScript
  view modules (SVG): a knob panel built from the schema, time per
  stage, progression, purchase timeline, each with the comparison (saved
  data or a snapshot) drawn behind the scenario. The modules take a
  source object, so a standalone export can feed them later.
- Datra.WebEditor.Sample: /lab runs the toy game end to end.
- Endpoint round-trip tests on an in-memory server.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs/LAB.md: what Datra provides and what a game writes, the consumer
code, knob semantics, the simulator contract, endpoints, and what P0
leaves out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant