콘텐츠로 이동

게임북을 쓰면,증명은Lute가 합니다.

마리나가 라멘 카운터에서 고개를 든다.‘어머! 픽서 씨!’

  1. 담담하게 묻는다면 2번으로
  2. 다정하게 묻는다면 3번으로
시작하기
플레이그라운드

어떤 저장소에서든, 명령 하나로

언어

당신은 담담하게 묻는다. “마리나. 번호 좀.” 그녀가 번호를 적어 주고, 당신은 속으로 생각한다. 아무래도 오늘 저녁, 저 사람을 어딘가로 데려가게 된 모양이다.

장면은 대본처럼 읽힙니다. 1번 섹션은 바로 이 .lute 문서입니다. 대사, 선택지를 담은 <branch>, 그리고 당신의 선택을 되짚는 <match>. <match>는 첫 일치가 우선이며 <otherwise>가 필수라서 모든 분기는 total합니다 — 어느 쪽이든 번호는 얻고, 다정하게 물으면 그녀의 마음이 조금 누그러질 뿐입니다. {mono} 속마음 줄은 frontmatter pov로 지정한 시점 인물(여기서는 fixer)만 말할 수 있습니다.

scene.lute
---
kind: scene
character: marina
season: 1
episode: 2
title: The Hostess with a Name
pov: fixer
state:
scene.affect.marina: { type: int, default: 0 }
enums:
emotion: [neutral, surprised, delighted, shy, content, angry, sad]
framing: [close, tight]
defs:
fond: { type: bool, cel: "scene.affect.marina >= 1" }
---
## The Hostess with a Name
::camera{focus="marina" framing="close" duration="0.5"}
@marina{emotion="surprised"}: Oh!
@marina{emotion="delighted"}: Mr. Fixer!
@marina{emotion="delighted"}: From the ramen counter — you eat at Mechageki too, isn't it wonderful?
/* a choice of TONE — both paths get the number; the gentle ask warms her */
<branch id="number">
<choice id="blunt" text="Just ask, flatly">
@fixer: Marina. Your number.
</choice>
<choice id="soft" text="Ask gently">
@fixer: Marina — would you mind terribly if I had your number?
::set{scene.affect.marina += 1}
</choice>
</branch>
/* state-driven callback — first-match-wins; <otherwise> is mandatory */
<match subject="scene.choices.number">
<when test="@fond">
@fixer{mono}: I asked nicely, which I am electing not to examine.
</when>
<otherwise>
@fixer{mono}: I am, apparently, taking that woman somewhere this evening.
</otherwise>
</match>

<otherwise>를 빼면 체커는 장면을 거부하고, 어떤 선택지가 처리되지 않았는지 짚어 줍니다:

lute check scene.lute
scene.lute:36:1: error [E-NONEXHAUSTIVE] non-exhaustive `<match>`: `blunt`, `soft` are not covered and there is no `<otherwise>`
failed: scene.lute (1 error(s), 0 warning(s))

하나의 대상을 속성별 트랙(pos, opacity)으로 나누고, 독립적인 카메라 트랙과 나란히 배치합니다:

beat.lute
---
kind: scene
character: marina
season: 1
episode: 1
enums:
anchor: { members: [left, center, right], default: center }
action: { members: [slideInLeft, fadeInUp, fadeOut], exits: [fadeOut] }
framing: [close, tight]
cameraMove: [shake]
---
## Beat
<timeline duration="1.2">
<track subject="marina" property="pos">
::actor{character="marina" anchor="left" action="slideInLeft"}
</track>
<track subject="marina" property="opacity">
::actor{character="marina" action="fadeInUp"}
</track>
<track subject="camera">
::camera{focus="marina" framing="tight" duration="0.6"}
::camera{move="shake" duration="0.3" at="0.7"}
</track>
</timeline>

스토리 중심 RPG는 대사만이 아니라 세계 상태로 움직입니다. <hub>는 떠날 때까지 선택지를 다시 보여 주고, 선택지는 when 가드가 참일 때만 제시되며, 단서는 ::assert로 기록하는 관계형 팩트이고, 마지막 <match>는 사건이 얼마나 진전됐는지에 반응합니다. 체커는 플레이어가 보기 전에 모든 관계 시그니처, 가드, match 분기를 검증합니다.

study.lute
---
kind: scene
character: detective
season: 1
episode: 1
pov: detective
entities:
clue: { members: [cufflink, ledger] }
suspect: { members: [mireille] }
relations:
found: { args: [clue], tier: run }
accused: { args: [suspect], tier: run }
state:
run.caseProgress: { type: int, default: 0 }
---
## The Study
@narrator: The lamp still burned. The ledger had already given you one name.
<hub id="investigate">
<choice id="desk" text="Search the writing desk" once>
@detective: A cufflink, wedged behind the drawer.
::assert{ found(cufflink) }
</choice>
<choice id="confront" text="Confront Mireille with the ledger" when="holds('found', ['ledger'])">
@mireille: ...You've read it, then. Fine.
::assert{ accused(mireille) }
::set{run.caseProgress += 1}
</choice>
<choice id="leave" text="Step back into the hall" exit>
@detective: Enough for now.
</choice>
</hub>
<match subject="run.caseProgress">
<when is="1..">
@detective{mono}: A name, a motive. The case has a spine now.
</when>
<otherwise>
@detective{mono}: Still only shadows. Keep looking.
</otherwise>
</match>
단서 발견
found(cufflink)가 런 팩트 저장소에 기록되고, holds('found', ['ledger'])가 이미 참이므로 confront 선택지가 열립니다.
퀘스트 진행
run.caseProgress가 올라가고, 마지막 <match>는 “A name, a motive. The case has a spine now.”로 넘어갑니다.
튜링 완전이 아닌, total
모든 시나리오는 반드시 종료됩니다 — 무한 루프도, 무한 재귀도 없습니다. ::end는 도달한 엔딩의 이름을 남기며, 같은 직선 본문에서 그 뒤에 오는 코드는 죽은 코드로 보고됩니다(W-CODE-AFTER-END).
CEL + Datalog 팩트
가드는 평범한 CEL 표현식입니다. Datalog 팩트 계층으로 더 높은 차원의 사실을 도출하고 장면 전체에서 질의할 수 있습니다.
타입이 지정된 플러그인 기능
디렉티브와 프로바이더는 타입이 지정된 매니페스트로 선언되므로, 체커는 각 플러그인이 무엇을 방출하고 소비하는지 정확히 압니다 — 옵션 타입이 틀리거나 코어가 소유한 이름을 쓰는 매니페스트는 거부합니다.
엔진이 무엇을 받는지 보려면 3번으로

엔진

당신은 다정하게 묻는다. “마리나 — 혹시 번호를 알려 줄 수 있을까요?” scene.affect.marina가 0에서 1이 되고, 콜백은 첫 번째 분기를 탄다. 정중하게 물었다. 그 이유는 굳이 따져 보지 않기로 한다.

Lute는 게임을 실행하지 않습니다

섹션 제목: “Lute는 게임을 실행하지 않습니다”

Lute는 저작 시점의 절반입니다. 문법, 체커, 컴파일러. Lute가 아는 모든 것은 버전이 명시된 JSON IR과 CEL이 되고, 엔진은 문서화된 런타임 계약을 통해 그것을 불러옵니다 — 엔진은 Lute 파서나 컴파일러를 내장하지 않습니다.

누가 무엇을 하는가
writer
│ writes
▼
.lute source ──▶ Lute [ parse · check · analyze · trace · compile · LSP ]
│ emits
▼
versioned JSON IR + CEL
│ loaded by
▼
engine adapter [ command loop · CEL eval · facts · quest lifecycle · bridges ]
│ drives
▼
UI · animation · save · gameplay
Lute (저작 + 툴체인)엔진 (런타임)
문법 + 타입 검사렌더링 + 입력
선택 / <match> 제어 데이터명령 실행
퀘스트 선언퀘스트 진행
데이터로서의 팩트 + Datalog 규칙팩트 저장소 + 규칙 평가
타임라인 데이터애니메이션 재생
브리지 시그니처 검사미니게임 / QTE 실행
연결성 분석장면 진입 + 저장

1번 섹션의 선택지는, 각 선택지가 넘어갈 위치를 적어 둔 레코드로 컴파일됩니다. lute compile scene.lute의 출력에서 그 명령 하나만 잘라 낸 것입니다:

lute compile scene.lute — 선택 레코드
{
"kind": "choice",
"family": "control",
"position": "001-0500",
"branchId": "number",
"selectionKey": "scene.choices.number",
"options": [
{
"id": "blunt",
"text": "Just ask, flatly",
"lineId": "marina.s01ep02.number.blunt",
"target": "001-0600"
},
{
"id": "soft",
"text": "Ask gently",
"lineId": "marina.s01ep02.number.soft",
"target": "001-0800"
}
],
"converge": "001-1100"
}

공식 런타임(@lute-lang/runtime, 순수 스텝 함수)을 내장하거나, 적합성 코퍼스에 맞춰 IR을 직접 해석하세요. 플레이어(@lute-lang/player, @lute-lang/player-web)는 이야기를 그리고, 세계는 게임이 가집니다. 전체 분담은 런타임 계약 페이지에 있습니다.

누가 플레이하기 전에 돌려 보려면 4번으로

도구

누가 1번 섹션을 플레이하기 전에, 먼저 걸어 보세요. lute trace는 결정적이고 모의 데이터로 구동되는 경로 하나 — 상태, 팩트, 선택, 이벤트 — 를 실행하고, 모든 결정과 그 이유를 보고합니다. 모든 경로가 아니라 그 한 경로를 걷습니다.

lute trace scene.lute --choice number=soft
trace: scene.lute (seeds: 0 paths, 0 facts; 1 selection)
## The Hostess with a Name
::camera{focus="marina" framing="close" duration=0.5}
@marina{emotion="surprised"} Oh!
@marina{emotion="delighted"} Mr. Fixer!
@marina{emotion="delighted"} From the ramen counter — you eat at Mechageki too, isn't it wonderful?
<branch number> eligible: blunt, soft -> soft
@fixer Marina — would you mind terribly if I had your number?
::set scene.affect.marina = 1
<match scene.choices.number> -> arm 1 (@fond)
@fixer{mono} I asked nicely, which I am electing not to examine.
trace complete: 2 decisions; choices 1/2 (number), arms 1/2 (scene.choices.number @36:1)
lute check
문서를 정적으로 검증합니다 — 타입, 가드, 관계 시그니처, match의 total 여부 — 그리고 거부할 때는 무엇을 고치면 되는지 알려 줍니다.
lute trace
당신이 준 모의 데이터로 결정적인 경로 하나를 미리 보여 주므로, 누군가 플레이하기 전에 시나리오가 펼쳐지는 모습을 지켜볼 수 있습니다.
lute check --project
선언한 after: 경로 — visited, completed, active — 위에서 도달 가능성을 계산해 Reachable / Unreachable / Unknown을 돌려줍니다. 관계형 관문은 거짓 확신이 아니라 사람이 검토할 경계로 드러납니다.
lute context
에이전트를 위해 설계된 프로젝트의 구조화된 뷰를 방출합니다 — 이 언어는 모델을 거스르지 않고, 모델과 함께 작성되도록 설계되었습니다.
언어 서버
진단, 호버, 자동완성을 에디터로 전달하므로, 실수는 런타임이 아니라 입력하는 순간에 드러납니다.
나머지 책을 읽으려면 5번으로

문서

레퍼런스의 모든 페이지에는 이 페이지처럼 번호가 붙어 있습니다. 첫 장면부터 시작하거나, 필요한 장을 바로 펼치세요.

  1. 1 시작하기 7 쪽
  2. 2 치트시트 1쪽
  3. 3 진단 코드 1쪽
  4. 4 예약된 이름 1쪽
  5. 5 가이드 3 쪽
  6. 6 언어 14 쪽
  7. 7 상태와 로직 4 쪽
  8. 8 연결성 3 쪽
  9. 9 툴링 11 쪽
  10. 10 플레이어 5 쪽
  11. 11 플러그인 시스템 4 쪽
  12. 12 예제 2 쪽
  13. 13 스펙 2 쪽

설치 없이 지금 바로 써 보고 싶다면 플레이그라운드로 가세요. 실시간 진단, 컴파일된 JSON IR, 모의 데이터 트레이스가 모두 브라우저에서 돌아갑니다.

시작하기
플레이그라운드