정해진 순찰을 피해 열쇠로 가방을 열어 훔치고 출구로 탈출합니다. 같은 시드면 늘 같은 열차입니다.
// ===== main.tpz =====
// 비밀열차 — 결정론 턴제 잠입. .tpz = 순수 스텝퍼(입력 패킷 → 다음 STATE + DRAW 리스트). JS 셸이 그린다. 한 이벤트 = 한 턴: 플레이어 행동 → 경비 한 칸 전진 → 발각 판정 → 놓침 판정.
// Secret-train: a deterministic turn-based stealth heist. Pure stepper. One event = one turn: player acts -> guards step -> detection -> left-behind check.
import core.state
import data.levels
let 기본시드 = "BT-0001"
// 닫힌 이벤트 태그. 원문 EVENT 토큰은 패킷 경계라 파싱 지점에서만 좁힌다.
// Closed event tags. The raw EVENT token is packet-boundary text and narrows only at parse time.
type 이벤트 = "init" | "resume" | "goL01" | "goL02" | "goL03" | "goL04" | "goL05" | "up" | "down" | "left" | "right" | "a" | "b" | "unknown"
function 이벤트파싱(토큰: string) -> 이벤트 {
match 토큰.trim() {
case "init" => "init"
case "resume" => "resume"
case "goL01" => "goL01"
case "goL02" => "goL02"
case "goL03" => "goL03"
case "goL04" => "goL04"
case "goL05" => "goL05"
case "up" => "up"
case "down" => "down"
case "left" => "left"
case "right" => "right"
case "a" => "a"
case "b" => "b"
case _ => "unknown"
}
}
// ── 입력 패킷 파싱 ── 접두사 길이만큼 잘라낸 나머지. str.slice(스칼라 인덱스)로 한 줄 — 이 메서드가 없어 예전엔 scalars()+배열 slice로 우회했었다(도그푸드로 추가). / prefix-strip via str.slice (added to Topaz after this app surfaced the gap).
function 접두사뒤(전체: string, 접두사: string) -> string {
전체.slice(접두사.scalars().length, 전체.scalars().length)
}
function 줄값(입력: string, 접두: string) -> string {
let mut 값 = ""
for 줄 in 입력.split("\n") {
let 다듬 = 줄.trim()
if 다듬.startsWith(접두) { 값 = 접두사뒤(다듬, 접두) }
}
값.trim()
}
function 셸값(셸: string, 키: string) -> string {
for 항 in 셸.split(",") {
let kv = 항.split("=")
if kv.length >= 2 && kv[0] == 키 { return kv[1] }
}
""
}
// 방향 델타. 0상1우2하3좌. / facing delta.
function 델타엑스(방향: int) -> int { if 방향 == 1 { 1 } else if 방향 == 3 { -1 } else { 0 } }
function 델타와이(방향: int) -> int { if 방향 == 2 { 1 } else if 방향 == 0 { -1 } else { 0 } }
// 경비 한 명의 현재 순찰점(phase로 유도). / a guard's patrol point for a phase.
function 경비점(레벨: levels.레벨, 경비인덱스: int, phase: int) -> levels.순찰점 {
let g = 레벨.경비들[경비인덱스]
let mut p = phase
if p < 0 { p = 0 }
if p >= g.순찰.length { p = p % g.순찰.length }
g.순찰[p]
}
// 시야: 경비 정면으로 직선 한 칸씩(시야거리까지), 벽·좌석에 막힘. 플레이어 칸에 닿으면 발각. / straight LOS ray; blocked by wall/seat; hits player -> spotted.
function 시야적중(레벨: levels.레벨, gx: int, gy: int, gd: int, px: int, py: int) -> bool {
let mut x = gx
let mut y = gy
let mut k = 0
while k < 레벨.시야거리 {
x = x + 델타엑스(gd)
y = y + 델타와이(gd)
let t = levels.타일에서(레벨, x, y)
if t == 1 || t == 2 { return false }
if x == px && y == py { return true }
k = k + 1
}
false
}
// 인접한 칸에 좌석이 있나(버티기 가능 지점). / is there a seat orthogonally adjacent (a brace point)?
function 인접좌석(레벨: levels.레벨, x: int, y: int) -> bool {
levels.타일에서(레벨, x + 1, y) == 2 || levels.타일에서(레벨, x - 1, y) == 2 ||
levels.타일에서(레벨, x, y + 1) == 2 || levels.타일에서(레벨, x, y - 1) == 2
}
// ── 한 턴 적용 ── 행동 → 흔들림(안 버티면 한 칸 밀림) → 경비 전진 → 발각(버티면 시야 면역) → 놓침. 모두 결정론.
function 적용(이벤트값: 이벤트, 직전: state.상태) -> state.상태 {
if 직전.끝 != 0 { return 직전 }
let 레벨 = levels.레벨얻기(직전.레벨아이디)
let mut 엑스 = 직전.엑스
let mut 와이 = 직전.와이
let mut 방향 = 직전.방향
let mut 열쇠 = 직전.열쇠있음
let mut 가방 = 직전.가방있음
let mut 숨음 = false
let mut 메시지 = 0
// 1) 플레이어 행동. 방향키=마주+이동(바닥만). a=열쇠/가방/탈출. b=버티기(인접 좌석이면 흔들림·시야 면역, 아니면 대기).
match 이벤트값 {
case "up" => {
방향 = 0
let nx = 엑스 + 델타엑스(방향)
let ny = 와이 + 델타와이(방향)
if levels.타일에서(레벨, nx, ny) == 0 { 엑스 = nx; 와이 = ny }
}
case "right" => {
방향 = 1
let nx = 엑스 + 델타엑스(방향)
let ny = 와이 + 델타와이(방향)
if levels.타일에서(레벨, nx, ny) == 0 { 엑스 = nx; 와이 = ny }
}
case "down" => {
방향 = 2
let nx = 엑스 + 델타엑스(방향)
let ny = 와이 + 델타와이(방향)
if levels.타일에서(레벨, nx, ny) == 0 { 엑스 = nx; 와이 = ny }
}
case "left" => {
방향 = 3
let nx = 엑스 + 델타엑스(방향)
let ny = 와이 + 델타와이(방향)
if levels.타일에서(레벨, nx, ny) == 0 { 엑스 = nx; 와이 = ny }
}
case "a" => {
if 엑스 == 레벨.열쇠엑스 && 와이 == 레벨.열쇠와이 && !열쇠 {
열쇠 = true; 메시지 = 1
} else if 엑스 == 레벨.가방엑스 && 와이 == 레벨.가방와이 && 열쇠 && !가방 {
가방 = true; 메시지 = 2
} else if 엑스 == 레벨.출구엑스 && 와이 == 레벨.출구와이 && 가방 {
// 탈출: 다음 칸이 있으면 그 칸 처음으로(통과), 마지막이면 전체 클리어(끝=1). / escape: advance to next car, or full win if last.
let 다음 = levels.다음레벨(직전.레벨아이디)
if 다음 != "" { return state.레벨시작상태(직전.시드, 다음, 7) }
return state.상태재구성(직전, 엑스, 와이, 방향, 열쇠, 가방, false, 1, 직전.경비단계들, 3, 직전.턴 + 1)
}
}
case "b" => {
if 인접좌석(레벨, 엑스, 와이) { 숨음 = true; 메시지 = 6 }
}
case "init" => {}
case "resume" => {}
case "goL01" => {}
case "goL02" => {}
case "goL03" => {}
case "goL04" => {}
case "goL05" => {}
case "unknown" => {}
}
// 2) 흔들림: 안 버텼으면 이번 턴 흔들림 방향(±x)으로 한 칸 밀린다(바닥/출구만, 벽·좌석은 막힘). 예측 가능 = 도구.
let 흔들 = levels.흔들림방향(레벨, 직전.턴)
if 흔들 != 0 && !숨음 {
let jx = 엑스 + 흔들
if levels.타일에서(레벨, jx, 와이) == 0 || (jx == 레벨.출구엑스 && 와이 == 레벨.출구와이) { 엑스 = jx }
}
// 3) 경비 한 칸 전진(phase+1, 순환).
let mut 새단계: Array<int> = []
let mut gi = 0
for ph in 직전.경비단계들 {
let len = 레벨.경비들[gi].순찰.length
새단계.push((ph + 1) % len)
gi = gi + 1
}
// 4) 발각: 경비가 플레이어 칸에 오면(충돌, 버텨도 잡힘) 또는 시야에 들어오면(버티면 면역).
let mut 잡힘 = false
let mut j = 0
for ph in 새단계 {
let pt = 경비점(레벨, j, ph)
if pt.엑스 == 엑스 && pt.와이 == 와이 { 잡힘 = true }
if !숨음 && 시야적중(레벨, pt.엑스, pt.와이, pt.방향, 엑스, 와이) { 잡힘 = true }
j = j + 1
}
if 잡힘 { return state.상태재구성(직전, 엑스, 와이, 방향, 열쇠, 가방, 숨음, 2, 새단계, 4, 직전.턴 + 1) }
// 5) 놓침(제한턴 도달).
let 새턴 = 직전.턴 + 1
if 새턴 >= 직전.제한턴 { return state.상태재구성(직전, 엑스, 와이, 방향, 열쇠, 가방, 숨음, 3, 새단계, 5, 새턴) }
state.상태재구성(직전, 엑스, 와이, 방향, 열쇠, 가방, 숨음, 0, 새단계, 메시지, 새턴)
}
// ── 드로우리스트 ── (JS 셸이 렌더. GRID 한 번 + 다음턴 위험칸 + 행위자 + 아이템 + HUD.)
function 위험줄들(레벨: levels.레벨, 단계들: Array<int>) -> string {
// 다음 경비 위치(phase+1)의 시야 칸 = 플레이어가 한 턴 쓰면 감시될 칸. 계획용. / next-phase LOS = cells watched if you spend a turn.
let mut s = ""
let mut j = 0
for ph in 단계들 {
let len = 레벨.경비들[j].순찰.length
let pt = 경비점(레벨, j, (ph + 1) % len)
let mut x = pt.엑스
let mut y = pt.와이
let mut k = 0
while k < 레벨.시야거리 {
x = x + 델타엑스(pt.방향)
y = y + 델타와이(pt.방향)
let t = levels.타일에서(레벨, x, y)
if t == 1 || t == 2 { k = 레벨.시야거리 } else { s = "{s}\nDANGER|{x}|{y}"; k = k + 1 }
}
j = j + 1
}
s
}
export function 드로우리스트(s: state.상태) -> string {
let 레벨 = levels.레벨얻기(s.레벨아이디)
let mut 격자문 = ""
for t in 레벨.격자 { 격자문 = "{격자문}{t}" }
let mut 경비줄 = ""
let mut j = 0
for ph in s.경비단계들 {
let pt = 경비점(레벨, j, ph)
경비줄 = "{경비줄}\nGUARD|{pt.엑스}|{pt.와이}|{pt.방향}"
j = j + 1
}
let 열쇠줄 = if !s.열쇠있음 { "\nITEM|key|{레벨.열쇠엑스}|{레벨.열쇠와이}" } else { "" }
let 가방줄 = if !s.가방있음 { "\nITEM|case|{레벨.가방엑스}|{레벨.가방와이}" } else { "" }
let 출구줄 = "\nITEM|exit|{레벨.출구엑스}|{레벨.출구와이}"
let 위험 = if s.끝 == 0 { 위험줄들(레벨, s.경비단계들) } else { "" }
// 다음 흔들림(현재 턴부터): 방향(±1·0) + 몇 턴 후 + 지금 버틸 수 있나. 플레이어가 흔들림을 읽고 계획하게. / next jolt (dir + turns-until) + can-brace-here.
let mut 흔들방 = 0
let mut 흔들후 = 0
let mut k = 0
while k < 레벨.흔들주기 + 1 {
let d = levels.흔들림방향(레벨, s.턴 + k)
if d != 0 { 흔들방 = d; 흔들후 = k; k = 레벨.흔들주기 + 1 } else { k = k + 1 }
}
let 버틸수 = if 인접좌석(레벨, s.엑스, s.와이) { 1 } else { 0 }
let 흔들줄 = "\nJOLT|{흔들방}|{흔들후}|{버틸수}"
// 다음 턴 경비 위치(phase+1) — 셸의 수(手) 미리보기가 충돌까지 판정하도록. / next-turn guard positions for the shell's move preview (collision).
let mut 다음경비줄 = ""
if s.끝 == 0 {
let mut k2 = 0
for ph in s.경비단계들 {
let len = 레벨.경비들[k2].순찰.length
let pt = 경비점(레벨, k2, (ph + 1) % len)
다음경비줄 = "{다음경비줄}\nNEXTPOS|{pt.엑스}|{pt.와이}"
k2 = k2 + 1
}
}
"DT1\nGRID|{레벨.너비}|{레벨.높이}|{격자문}\nPLAYER|{s.엑스}|{s.와이}|{s.방향}|{불2(s.가방있음)}|{불2(s.숨음)}{경비줄}{열쇠줄}{가방줄}{출구줄}{위험}{흔들줄}{다음경비줄}\nHUD|{s.턴}|{s.제한턴}|{불2(s.열쇠있음)}|{불2(s.가방있음)}|{s.끝}|{s.메시지}|{레벨.번호}"
}
function 불2(b: bool) -> int { if b { 1 } else { 0 } }
function 처리(입력: string) -> string {
let 이벤트값 = 이벤트파싱(줄값(입력, "EVENT:"))
let 셸 = 줄값(입력, "SHELL:")
let 셸시드 = 셸값(셸, "seed")
let 시드 = if 셸시드 != "" { 셸시드 } else { 기본시드 }
let 새상태 = match 이벤트값 {
case "init" => state.기본상태(시드)
case "resume" => state.역직렬화(줄값(입력, "STATE:"), 시드)
case "goL01" => state.레벨시작상태(시드, "L01", 0) // 칸 선택: 해당 칸 처음부터(기록 도전). / car select: start a chosen car.
case "goL02" => state.레벨시작상태(시드, "L02", 0)
case "goL03" => state.레벨시작상태(시드, "L03", 0)
case "goL04" => state.레벨시작상태(시드, "L04", 0)
case "goL05" => state.레벨시작상태(시드, "L05", 0)
case "up" => 적용(이벤트값, state.역직렬화(줄값(입력, "STATE:"), 시드))
case "down" => 적용(이벤트값, state.역직렬화(줄값(입력, "STATE:"), 시드))
case "left" => 적용(이벤트값, state.역직렬화(줄값(입력, "STATE:"), 시드))
case "right" => 적용(이벤트값, state.역직렬화(줄값(입력, "STATE:"), 시드))
case "a" => 적용(이벤트값, state.역직렬화(줄값(입력, "STATE:"), 시드))
case "b" => 적용(이벤트값, state.역직렬화(줄값(입력, "STATE:"), 시드))
case "unknown" => 적용(이벤트값, state.역직렬화(줄값(입력, "STATE:"), 시드))
}
"BT1\nSTATE:{state.직렬화(새상태)}\nDRAW:{드로우리스트(새상태)}"
}
print(처리(input()))
// ===== core/state.tpz =====
// 비밀열차 STATE — 결정론 턴제 잠입. 동적 사실만 담는다(레벨 정적 데이터는 data.levels). 경비는 위치 대신 순찰 단계(phase)만 저장 → 위치·방향은 patrol[phase]에서 유도(결정론·압축).
// Dynamic facts only; guards store just their patrol PHASE (pos/facing derived from the level's patrol[phase]). Pure function of (seed + inputs).
import data.levels
export type 상태 = {
시드: string, 레벨아이디: string,
턴: int, 제한턴: int,
엑스: int, 와이: int, 방향: int, // 방향 0상1우2하3좌 / facing
열쇠있음: bool, 가방있음: bool,
숨음: bool, // 이번 턴 좌석에 버텼는가 — 흔들림에 안 밀리고 시야에도 안 잡힌다(연출+판정). / braced this turn: not slid by a jolt, not seen by LOS.
끝: int, // 0 진행 / 1 탈출 / 2 발각 / 3 놓침 (play/escape/caught/leftBehind)
경비단계들: Array<int>, // 경비별 순찰 phase / per-guard patrol phase
메시지: int // 한글 셸 텍스트 코드 / message code for the shell
}
// 특정 레벨의 시작 상태(경비 시작 phase는 레벨별로 어긋나게). 레벨 진행 시 재사용. / fresh start state for any level (per-level guard start phases).
export function 레벨시작상태(시드: string, 레벨아이디: string, 메시지: int) -> 상태 {
let lv = levels.레벨얻기(레벨아이디)
{
시드: 시드, 레벨아이디: 레벨아이디, 턴: 0, 제한턴: lv.제한턴,
엑스: lv.시작엑스, 와이: lv.시작와이, 방향: 2,
열쇠있음: false, 가방있음: false, 숨음: false, 끝: 0, 경비단계들: lv.시작단계들, 메시지: 메시지
}
}
export function 기본상태(시드: string) -> 상태 { 레벨시작상태(시드, "L01", 0) }
// 바뀐 필드만 갈아끼운 새 상태(시드·레벨·제한턴 보존). 턴 로직이 한 번에 재구성한다. / rebuild with the changed turn fields; static fields preserved.
export function 상태재구성(직전: 상태, 엑스: int, 와이: int, 방향: int, 열쇠있음: bool, 가방있음: bool, 숨음: bool, 끝: int, 경비단계들: Array<int>, 메시지: int, 턴: int) -> 상태 {
직전{
턴: 턴, 엑스: 엑스, 와이: 와이, 방향: 방향,
열쇠있음: 열쇠있음, 가방있음: 가방있음, 숨음: 숨음, 끝: 끝,
경비단계들: 경비단계들, 메시지: 메시지
}
}
function 불(b: bool) -> int { if b { 1 } else { 0 } }
function 단계직렬(단계들: Array<int>) -> string {
let mut s = ""
for p in 단계들 { s = if s == "" { "{p}" } else { "{s};{p}" } }
if s == "" { "-" } else { s }
}
export function 직렬화(s: 상태) -> string {
"BT1|{s.시드}|{s.레벨아이디}|{s.턴}|{s.제한턴}|{s.엑스}|{s.와이}|{s.방향}|{불(s.열쇠있음)}|{불(s.가방있음)}|{s.끝}|{단계직렬(s.경비단계들)}|{s.메시지}|{불(s.숨음)}"
}
function 단계파싱(부분: string) -> Array<int> {
let mut r: Array<int> = []
if 부분 == "-" || 부분 == "" { return r }
for t in 부분.split(";") { r.push(toInt(t) ?? 0) }
r
}
export function 역직렬화(문자열: string, 대체시드: string) -> 상태 {
let 부분 = 문자열.split("|")
if 부분.length < 13 { return 기본상태(대체시드) }
let lv = levels.레벨얻기(부분[2])
{
시드: 부분[1], 레벨아이디: 부분[2],
턴: toInt(부분[3]) ?? 0, 제한턴: toInt(부분[4]) ?? lv.제한턴,
엑스: toInt(부분[5]) ?? lv.시작엑스, 와이: toInt(부분[6]) ?? lv.시작와이, 방향: toInt(부분[7]) ?? 2,
열쇠있음: (toInt(부분[8]) ?? 0) == 1, 가방있음: (toInt(부분[9]) ?? 0) == 1,
끝: toInt(부분[10]) ?? 0, 경비단계들: 단계파싱(부분[11]), 메시지: toInt(부분[12]) ?? 0,
숨음: 부분.length >= 14 && (toInt(부분[13]) ?? 0) == 1
}
}
// ===== data/levels.tpz =====
// 비밀열차 레벨 데이터(저작). 타일 '#'=벽, 'S'=좌석(둘 다 이동·시야 차단; 좌석에 붙으면 버티기 가능), 그 외=바닥. 흐름: 시작 → 열쇠 → 가방(열쇠 필요) → 출구.
// 칸막이가 길을 가르고, 경비가 통로를 왕복하며 길목을 비춘다. 제한턴이 빡빡해 흔들림을 타야 시간 안에 탈출(흔들림 필수). 레벨이 오를수록 건너야 할 통로가 늘어난다.
// Secret-train level data. Barriers force the path through guarded chokepoints; the tight turn limit makes riding the jolt necessary. Higher levels = more corridors to cross.
export type 순찰점 = { 엑스: int, 와이: int, 방향: int } // 방향 0=상 1=우 2=하 3=좌 / facing
export type 경비 = { 순찰: Array<순찰점> }
export type 레벨 = {
번호: int,
너비: int, 높이: int, 격자: Array<int>,
시작엑스: int, 시작와이: int, 열쇠엑스: int, 열쇠와이: int,
가방엑스: int, 가방와이: int, 출구엑스: int, 출구와이: int,
제한턴: int, 시야거리: int,
흔들주기: int, 동쪽턴: int, 서쪽턴: int, // 결정론 흔들림: 턴%주기 가 동쪽턴이면 동(+x), 서쪽턴이면 서(-x). 예측 가능 = 도구. / deterministic jolt schedule.
경비들: Array<경비>, 시작단계들: Array<int> // 경비별 시작 순찰 phase(레벨마다 어긋나게 배치) / per-guard initial patrol phase.
}
// 레벨 1: 칸막이 한 줄(3·9열 틈) + 통로(4행)를 경비 1명이 왕복. 최단 30턴 + 슬랙 1. / one barrier, one guarded corridor.
let 행들L01 = [
"#############",
"#...........#",
"#.S.S.S.S.S.#",
"###.#####.###",
"#...........#",
"#.S.S.S.S.S.#",
"#...........#",
"#...........#",
"#############"
]
// 레벨 2: 칸막이 두 줄 + 통로 두 줄(3행·6행)을 경비 2명이 왕복 — 건너야 할 길목이 둘. 최단 33턴 + 슬랙 1. / two barriers, two guarded corridors.
let 행들L02 = [
"#############",
"#...........#",
"###.#####.###",
"#...........#",
"#.S.S.S.S.S.#",
"###.#####.###",
"#...........#",
"#...........#",
"#############"
]
// 레벨 3: 칸막이 세 줄 + 통로 세 줄(3·5·7행)을 경비 3명이 왕복 — 위(열쇠)에서 아래(가방·출구)까지 셋을 모두 건너야 한다. 최단 37턴 + 슬랙 3. / three guarded corridors.
let 행들L03 = [
"#############",
"#...........#",
"###.#####.###",
"#...........#",
"###.#####.###",
"#...........#",
"###.#####.###",
"#...........#",
"#############"
]
// 레벨 4 (참신): 트인 객실 + 교차 시야 — 가로 경비 1 + 세로(열) 경비 1이 오르내리며 비춘다. 움직이는 빛줄기를 꿰뚫어라. / open room, crossing sightlines.
let 행들L04 = [
"#############",
"#...........#",
"#.S.S.S.S.S.#",
"#...........#",
"#.S.S.S.S.S.#",
"#...........#",
"#.S.S.S.S.S.#",
"#...........#",
"#############"
]
function 행파싱(행: string) -> Array<int> {
let mut r: Array<int> = []
for c in 행.scalars() {
if c == "#" { r.push(1) } else if c == "S" { r.push(2) } else { r.push(0) }
}
r
}
function 격자만들기(행들: Array<string>) -> Array<int> {
let mut 격자: Array<int> = []
for 행 in 행들 { for t in 행파싱(행) { 격자.push(t) } }
격자
}
// 한 행(통로)을 좌→우→좌로 왕복. 진행 방향을 보며 통로를 비춰 길목을 번갈아 감시한다. / a guard pacing a corridor row, facing its travel direction.
function 통로경비(와이: int) -> 경비 {
let mut 순: Array<순찰점> = []
let mut x = 1
while x <= 11 { 순.push({ 엑스: x, 와이: 와이, 방향: 1 }); x = x + 1 } // →
x = 10
while x >= 2 { 순.push({ 엑스: x, 와이: 와이, 방향: 3 }); x = x - 1 } // ←
{ 순찰: 순 }
}
// 통로의 일부 구간만 왕복(짧은 순찰) — 안전 횡단 창이 넓어져 1번 칸을 가르치는 관대한 레벨로. / a SHORT row patrol; wider safe windows for a gentle teaching level.
function 통로경비범위(와이: int, 시작: int, 끝: int) -> 경비 {
let mut 순: Array<순찰점> = []
let mut x = 시작
while x <= 끝 { 순.push({ 엑스: x, 와이: 와이, 방향: 1 }); x = x + 1 } // →
x = 끝 - 1
while x >= 시작 + 1 { 순.push({ 엑스: x, 와이: 와이, 방향: 3 }); x = x - 1 } // ←
{ 순찰: 순 }
}
// 한 열(세로)을 위→아래→위로 왕복. 시야가 세로로 행을 가로질러 비춘다(가로 경비와 교차). / a guard pacing a column up<->down, vertical LOS.
function 세로경비(엑스: int) -> 경비 {
let mut 순: Array<순찰점> = []
let mut y = 1
while y <= 7 { 순.push({ 엑스: 엑스, 와이: y, 방향: 2 }); y = y + 1 } // ↓
y = 6
while y >= 2 { 순.push({ 엑스: 엑스, 와이: y, 방향: 0 }); y = y - 1 } // ↑
{ 순찰: 순 }
}
export function 레벨얻기(레벨아이디: string) -> 레벨 {
if 레벨아이디 == "L05" {
{
번호: 5,
너비: 13, 높이: 9, 격자: 격자만들기(행들L04), // 같은 트인 객실 / same open room
시작엑스: 1, 시작와이: 1, 열쇠엑스: 11, 열쇠와이: 1,
가방엑스: 11, 가방와이: 7, 출구엑스: 1, 출구와이: 7,
제한턴: 32, 시야거리: 6, // 세로 빛줄기 둘 사이를 꿰뚫기. 최단 30 + 슬랙 2. / thread two vertical beams.
흔들주기: 5, 동쪽턴: 2, 서쪽턴: 4,
경비들: [세로경비(5), 세로경비(9)], 시작단계들: [0, 0]
}
} else if 레벨아이디 == "L04" {
{
번호: 4,
너비: 13, 높이: 9, 격자: 격자만들기(행들L04),
시작엑스: 1, 시작와이: 1, 열쇠엑스: 11, 열쇠와이: 1,
가방엑스: 11, 가방와이: 7, 출구엑스: 1, 출구와이: 7,
제한턴: 34, 시야거리: 6, // 트인 객실 + 교차 시야. ~68% 발각(5번보다 낮게 = 단조 증가 커브). 흔들림 필수. / open room; ~68% caught (below car5 = monotonic).
흔들주기: 5, 동쪽턴: 2, 서쪽턴: 4,
경비들: [통로경비(3), 세로경비(9)], 시작단계들: [0, 3]
}
} else if 레벨아이디 == "L03" {
{
번호: 3,
너비: 13, 높이: 9, 격자: 격자만들기(행들L03),
시작엑스: 1, 시작와이: 1, 열쇠엑스: 11, 열쇠와이: 1,
가방엑스: 11, 가방와이: 7, 출구엑스: 1, 출구와이: 7,
제한턴: 42, 시야거리: 6, // ~51% 발각(2번 34% ↗ 4번 68%로 완만히). / ~51% caught, between car2 and car4.
흔들주기: 5, 동쪽턴: 2, 서쪽턴: 4,
경비들: [통로경비(3), 통로경비(5), 통로경비(7)], 시작단계들: [0, 0, 7]
}
} else if 레벨아이디 == "L02" {
{
번호: 2,
너비: 13, 높이: 9, 격자: 격자만들기(행들L02),
시작엑스: 1, 시작와이: 1, 열쇠엑스: 11, 열쇠와이: 1,
가방엑스: 11, 가방와이: 7, 출구엑스: 1, 출구와이: 7,
제한턴: 38, 시야거리: 6, // 완만한 램프: 한 경비는 짧은 순찰 → 이탈 발각 ~34%(1번 10% ↗ 뒷칸으로 상승). / smooth ramp ~34% caught.
흔들주기: 7, 동쪽턴: 3, 서쪽턴: 6,
경비들: [통로경비(3), 통로경비범위(6, 3, 9)], 시작단계들: [6, 6]
}
} else {
{
번호: 1,
너비: 13, 높이: 9, 격자: 격자만들기(행들L01),
시작엑스: 1, 시작와이: 1, 열쇠엑스: 11, 열쇠와이: 1,
가방엑스: 11, 가방와이: 6, 출구엑스: 1, 출구와이: 6,
제한턴: 36, 시야거리: 6, // 가르치는 첫 칸: 넉넉한 슬랙 + 짧은 순찰 → 관대(이탈 10%만 발각). 흔들림은 소개만; 뒷 칸에서 필수화. / gentle teaching level.
흔들주기: 7, 동쪽턴: 3, 서쪽턴: 6, // 드문 흔들림(소개용) / infrequent jolt, just to introduce it
경비들: [통로경비범위(4, 5, 8)], 시작단계들: [0]
}
}
}
// 다음 레벨 아이디("" = 마지막, 전체 클리어). / next level id ("" = last, full win).
export function 다음레벨(레벨아이디: string) -> string {
if 레벨아이디 == "L01" { "L02" } else if 레벨아이디 == "L02" { "L03" } else if 레벨아이디 == "L03" { "L04" } else if 레벨아이디 == "L04" { "L05" } else { "" }
}
// 이 턴의 흔들림 방향: 1 동(+x) · -1 서(-x) · 0 없음. 결정론(턴의 순수 함수). / jolt direction this turn.
export function 흔들림방향(레벨: 레벨, 턴: int) -> int {
if 턴 <= 0 { return 0 }
if 턴 % 레벨.흔들주기 == 레벨.동쪽턴 { return 1 }
if 턴 % 레벨.흔들주기 == 레벨.서쪽턴 { return -1 }
0
}
// 타일 조회(범위 밖 = 벽). / tile lookup (out of bounds = wall).
export function 타일에서(레벨: 레벨, 엑스: int, 와이: int) -> int {
if 엑스 < 0 || 와이 < 0 || 엑스 >= 레벨.너비 || 와이 >= 레벨.높이 { return 1 }
레벨.격자[와이 * 레벨.너비 + 엑스]
}