STUDIO HAZE · jeniapp
Topaz source
Open by house policy. Topaz is our own language.
Topaz 5.20.2 · 63 files
apps/secret-train/main.tpz
Topaz · 9283 B · Original file
1
// 비밀열차의 한 턴을 계산합니다. 셸이 보낸 패킷 하나를 받아 다음 상태와 그릴 목록을 돌려줍니다.2
// 입력은 BT1 패킷의 STATE, EVENT, SHELL 줄이고, 출력은 BT1 머리, 저장 문자열을 담은 STATE 줄, DT1 그리기 목록을 담은 DRAW 줄입니다.3
// 한 이벤트가 한 턴이며 행동, 흔들림, 경비 전진, 발각, 놓침 순으로 판정합니다. 같은 패킷이면 인터프리터와 웹 빌드에서 바이트까지 같은 출력을 냅니다.4
// 화면 그리기, 기록 저장, 시연 재생은 하지 않습니다. 모두 페이지의 셸이 맡습니다.5
// Secret train: one deterministic stealth turn per packet, returning the next save state and a draw list.7
import core.state8
import data.levels10
let 기본시드 = "BT-0001"12
// 공용 도우미 시작 (줄값 셸값)13
// 여러 도구가 같은 원본을 씁니다. 여기서 고치지 않고 원본에서 고친 뒤 다시 생성합니다.15
// 같은 접두의 줄이 여럿이면 마지막 줄을 씁니다.16
function 줄값(입력: string, 접두: string) -> string {17
let 맞는줄들 = 입력.split("\n").map(줄 => 줄.trim()).filter(줄 => 줄.startsWith(접두))18
if 맞는줄들.length == 0 { return "" }19
let 끝줄 = 맞는줄들[맞는줄들.length - 1]20
끝줄.slice(접두.scalars().length, 끝줄.scalars().length).trim()21
}23
// "키=값,키=값" 에서 = 로 정확히 두 조각인 항만 읽고 키와 값의 앞뒤 공백을 지웁니다. 같은 키가 여럿이면 마지막 값을 씁니다.24
function 셸값(셸: string, 키: string) -> string {25
let 값들 = 셸.split(",").map(항 => 항.split("=")).filter(조각들 => 조각들.length == 2 && 조각들[0].trim() == 키)26
if 값들.length == 0 { "" } else { 값들[값들.length - 1][1].trim() }27
}29
// 공용 도우미 끝31
enum 이벤트 { 처음, 이어하기, 칸고르기(string), 이동(levels.방향), 줍기, 버티기, 모름 }32
record 흔들림예고 { 방향: int, 남은턴: int }34
// go 뒤에 레벨 코드가 오면 그 칸을 고릅니다. 모르는 토큰은 아무것도 하지 않고 한 턴을 보냅니다.35
function 이벤트읽기(토큰: string) -> 이벤트 {36
if 토큰.startsWith("go") && levels.레벨코드인가(토큰.slice(2, 토큰.scalars().length)) {37
return 이벤트.칸고르기(토큰.slice(2, 토큰.scalars().length))38
}39
match 토큰 {40
case "init" => 이벤트.처음41
case "resume" => 이벤트.이어하기42
case "up" => 이벤트.이동("위")43
case "down" => 이벤트.이동("아래")44
case "left" => 이벤트.이동("왼쪽")45
case "right" => 이벤트.이동("오른쪽")46
case "a" => 이벤트.줍기47
case "b" => 이벤트.버티기48
case _ => 이벤트.모름49
}50
}52
function 플레이어칸인가(현재: state.상태, 목표: levels.칸) -> bool {53
현재.x == 목표.x && 현재.y == 목표.y54
}56
// 바닥으로만 움직이고 막히면 돌아서기만 합니다.57
function 걷기(레벨: levels.레벨정보, 현재: state.상태, 방향값: levels.방향) -> state.상태 {58
let x = 현재.x + levels.가로이동(방향값)59
let y = 현재.y + levels.세로이동(방향값)60
if levels.바닥인가(레벨, x, y) { 현재{ x: x, y: y, 방향: 방향값 } } else { 현재{ 방향: 방향값 } }61
}63
// a 는 열쇠, 가방, 출구 순으로만 일이 됩니다.64
function 줍거나탈출(레벨: levels.레벨정보, 현재: state.상태) -> state.상태 {65
if 플레이어칸인가(현재, 레벨.열쇠) && !현재.열쇠있음 {66
현재{ 열쇠있음: true, 안내: "열쇠" }67
} else if 플레이어칸인가(현재, 레벨.가방) && 현재.열쇠있음 && !현재.가방있음 {68
현재{ 가방있음: true, 안내: "가방" }69
} else if 플레이어칸인가(현재, 레벨.출구) && 현재.가방있음 {70
현재{ 결말: "탈출", 안내: "승리" }71
} else {72
현재73
}74
}76
// 탈출한 턴은 흔들림과 경비 판정 없이 다음 칸 처음으로 가고, 마지막 칸이면 그대로 끝납니다.77
function 칸넘기기(현재: state.상태) -> state.상태 {78
let 다음 = levels.다음레벨코드(현재.레벨코드)79
if 다음 != "" { state.레벨시작상태(현재.시드, 다음, "다음칸") } else { 현재{ 턴: 현재.턴 + 1 } }80
}82
// 흔들림은 행동 전 턴 번호로 정하므로 셸이 미리 알린 것과 같습니다.83
function 흔들기(레벨: levels.레벨정보, 현재: state.상태, 턴: int) -> state.상태 {84
let 흔들 = levels.흔들림방향(레벨, 턴)85
if 흔들 == 0 || 현재.버팀 { return 현재 }86
let x = 현재.x + 흔들87
if levels.바닥인가(레벨, x, 현재.y) { 현재{ x: x } } else { 현재 }88
}90
// 경비와 같은 칸이면 버텨도 잡히고, 시야는 버티면 피합니다.91
function 발각인가(레벨: levels.레벨정보, 현재: state.상태) -> bool {92
let 잡은경비들 = levels.경비점들(레벨, 현재.경비단계들).filter(점 =>93
(점.x == 현재.x && 점.y == 현재.y)94
|| (!현재.버팀 && levels.시야칸들(레벨, 점).filter(시야칸 => 플레이어칸인가(현재, 시야칸)).length > 0)95
)96
잡은경비들.length > 097
}99
// 버팀과 안내는 그 턴에만 유효해 행동 전에 지웁니다.100
function 적용(이벤트값: 이벤트, 직전: state.상태) -> state.상태 {101
if 직전.결말 != "진행" { return 직전 }102
let 레벨 = levels.레벨얻기(직전.레벨코드)103
let 대기 = 직전{ 버팀: false, 안내: "없음" }104
let 행동후 = match 이벤트값 {105
case 이동(방향값) => 걷기(레벨, 대기, 방향값)106
case 줍기 => 줍거나탈출(레벨, 대기)107
case 버티기 => if levels.좌석옆인가(레벨, 대기.x, 대기.y) { 대기{ 버팀: true, 안내: "버팀" } } else { 대기 }108
case 처음 | 이어하기 | 칸고르기(_) | 모름 => 대기109
}110
if 행동후.결말 == "탈출" { return 칸넘기기(행동후) }111
let 다음턴 = 흔들기(레벨, 행동후, 직전.턴){112
경비단계들: levels.다음단계들(레벨, 직전.경비단계들),113
턴: 직전.턴 + 1,114
}115
if 발각인가(레벨, 다음턴) { return 다음턴{ 결말: "발각", 안내: "발각" } }116
if 다음턴.턴 >= 직전.제한턴 { return 다음턴{ 결말: "놓침", 안내: "놓침" } }117
다음턴118
}120
// 찾는 즉시 멈춰야 큰 턴 값에서도 그 뒤 턴 번호를 더하지 않습니다.121
function 다음흔들림(레벨: levels.레벨정보, 턴: int) -> 흔들림예고 {122
for k in 0..<(레벨.흔들주기 + 1) {123
let 방향 = levels.흔들림방향(레벨, 턴 + k)124
if 방향 != 0 { return 흔들림예고 { 방향: 방향, 남은턴: k } }125
}126
흔들림예고 { 방향: 0, 남은턴: 0 }127
}129
// 위험 칸과 다음 경비 위치는 한 턴 뒤의 순찰이라 셸이 다음 수를 미리 판정하게 합니다.130
function 그리기목록(현재: state.상태) -> string {131
let 레벨 = levels.레벨얻기(현재.레벨코드)132
let 다음경비점들 = if 현재.결말 == "진행" { levels.경비점들(레벨, levels.다음단계들(레벨, 현재.경비단계들)) } else { [] }133
let 흔들림 = 다음흔들림(레벨, 현재.턴)134
let 버틸수있음 = state.일또는영(levels.좌석옆인가(레벨, 현재.x, 현재.y))135
let 줄묶음들: Array<Array<string>> = [136
[137
"DT1",138
"GRID|{레벨.너비}|{레벨.높이}|{levels.격자문자열(레벨)}",139
"PLAYER|{현재.x}|{현재.y}|{levels.방향번호(현재.방향)}|{state.일또는영(현재.가방있음)}|{state.일또는영(현재.버팀)}",140
],141
levels.경비점들(레벨, 현재.경비단계들).map(점 => "GUARD|{점.x}|{점.y}|{levels.방향번호(점.방향)}"),142
if 현재.열쇠있음 { [] } else { ["ITEM|key|{레벨.열쇠.x}|{레벨.열쇠.y}"] },143
if 현재.가방있음 { [] } else { ["ITEM|case|{레벨.가방.x}|{레벨.가방.y}"] },144
["ITEM|exit|{레벨.출구.x}|{레벨.출구.y}"],145
[for 점 in 다음경비점들 for 시야칸 in levels.시야칸들(레벨, 점) => "DANGER|{시야칸.x}|{시야칸.y}"],146
["JOLT|{흔들림.방향}|{흔들림.남은턴}|{버틸수있음}"],147
다음경비점들.map(점 => "NEXTPOS|{점.x}|{점.y}"),148
["HUD|{현재.턴}|{현재.제한턴}|{state.일또는영(현재.열쇠있음)}|{state.일또는영(현재.가방있음)}|{state.결말번호(현재.결말)}|{state.안내번호(현재.안내)}|{레벨.번호}"],149
]150
[for 묶음 in 줄묶음들 for 줄 in 묶음 => 줄].join("\n")151
}153
function 처리(입력: string) -> string {154
let 이벤트값 = 줄값(입력, "EVENT:") |> 이벤트읽기155
let 셸시드 = 셸값(줄값(입력, "SHELL:"), "seed")156
let 시드 = if 셸시드 != "" { 셸시드 } else { 기본시드 }157
let 저장 = 줄값(입력, "STATE:")158
// 칸 선택은 기록 도전이라 그 칸 처음부터 시작합니다.159
let 새상태 = match 이벤트값 {160
case 처음 => state.기본상태(시드)161
case 이어하기 => state.역직렬화(저장, 시드)162
case 칸고르기(레벨코드) => state.레벨시작상태(시드, 레벨코드, "없음")163
case 이동(_) | 줍기 | 버티기 | 모름 => 적용(이벤트값, state.역직렬화(저장, 시드))164
}165
"BT1\nSTATE:{state.직렬화(새상태)}\nDRAW:{그리기목록(새상태)}"166
}168
input() |> 처리 |> printSHA256 a4a457ebc67d945aafcb4fe985bd3c9660dd67a9c51dc7ddf5e850442e2b4139