slow.monster

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.
7import core.state
8import data.levels
10let 기본시드 = "BT-0001"
12// 공용 도우미 시작 (줄값 셸값)
13// 여러 도구가 같은 원본을 씁니다. 여기서 고치지 않고 원본에서 고친 뒤 다시 생성합니다.
15// 같은 접두의 줄이 여럿이면 마지막 줄을 씁니다.
16function 줄값(입력: 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()
23// "키=값,키=값" 에서 = 로 정확히 두 조각인 항만 읽고 키와 값의 앞뒤 공백을 지웁니다. 같은 키가 여럿이면 마지막 값을 씁니다.
24function 셸값(셸: string, 키: string) -> string {
25 let 값들 = 셸.split(",").map(항 => 항.split("=")).filter(조각들 => 조각들.length == 2 && 조각들[0].trim() == 키)
26 if 값들.length == 0 { "" } else { 값들[값들.length - 1][1].trim() }
29// 공용 도우미 끝
31enum 이벤트 { 처음, 이어하기, 칸고르기(string), 이동(levels.방향), 줍기, 버티기, 모름 }
32record 흔들림예고 { 방향: int, 남은턴: int }
34// go 뒤에 레벨 코드가 오면 그 칸을 고릅니다. 모르는 토큰은 아무것도 하지 않고 한 턴을 보냅니다.
35function 이벤트읽기(토큰: 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 }
52function 플레이어칸인가(현재: state.상태, 목표: levels.칸) -> bool {
53 현재.x == 목표.x && 현재.y == 목표.y
56// 바닥으로만 움직이고 막히면 돌아서기만 합니다.
57function 걷기(레벨: levels.레벨정보, 현재: state.상태, 방향값: levels.방향) -> state.상태 {
58 let x = 현재.x + levels.가로이동(방향값)
59 let y = 현재.y + levels.세로이동(방향값)
60 if levels.바닥인가(레벨, x, y) { 현재{ x: x, y: y, 방향: 방향값 } } else { 현재{ 방향: 방향값 } }
63// a 는 열쇠, 가방, 출구 순으로만 일이 됩니다.
64function 줍거나탈출(레벨: levels.레벨정보, 현재: state.상태) -> state.상태 {
65 if 플레이어칸인가(현재, 레벨.열쇠) && !현재.열쇠있음 {
66 현재{ 열쇠있음: true, 안내: "열쇠" }
67 } else if 플레이어칸인가(현재, 레벨.가방) && 현재.열쇠있음 && !현재.가방있음 {
68 현재{ 가방있음: true, 안내: "가방" }
69 } else if 플레이어칸인가(현재, 레벨.출구) && 현재.가방있음 {
70 현재{ 결말: "탈출", 안내: "승리" }
71 } else {
72 현재
73 }
76// 탈출한 턴은 흔들림과 경비 판정 없이 다음 칸 처음으로 가고, 마지막 칸이면 그대로 끝납니다.
77function 칸넘기기(현재: state.상태) -> state.상태 {
78 let 다음 = levels.다음레벨코드(현재.레벨코드)
79 if 다음 != "" { state.레벨시작상태(현재.시드, 다음, "다음칸") } else { 현재{ 턴: 현재.턴 + 1 } }
82// 흔들림은 행동 전 턴 번호로 정하므로 셸이 미리 알린 것과 같습니다.
83function 흔들기(레벨: levels.레벨정보, 현재: state.상태, 턴: int) -> state.상태 {
84 let 흔들 = levels.흔들림방향(레벨, 턴)
85 if 흔들 == 0 || 현재.버팀 { return 현재 }
86 let x = 현재.x + 흔들
87 if levels.바닥인가(레벨, x, 현재.y) { 현재{ x: x } } else { 현재 }
90// 경비와 같은 칸이면 버텨도 잡히고, 시야는 버티면 피합니다.
91function 발각인가(레벨: levels.레벨정보, 현재: state.상태) -> bool {
92 let 잡은경비들 = levels.경비점들(레벨, 현재.경비단계들).filter(점 =>
93 (점.x == 현재.x && 점.y == 현재.y)
94 || (!현재.버팀 && levels.시야칸들(레벨, 점).filter(시야칸 => 플레이어칸인가(현재, 시야칸)).length > 0)
95 )
96 잡은경비들.length > 0
99// 버팀과 안내는 그 턴에만 유효해 행동 전에 지웁니다.
100function 적용(이벤트값: 이벤트, 직전: 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 다음턴
120// 찾는 즉시 멈춰야 큰 턴 값에서도 그 뒤 턴 번호를 더하지 않습니다.
121function 다음흔들림(레벨: levels.레벨정보, 턴: int) -> 흔들림예고 {
122 for k in 0..<(레벨.흔들주기 + 1) {
123 let 방향 = levels.흔들림방향(레벨, 턴 + k)
124 if 방향 != 0 { return 흔들림예고 { 방향: 방향, 남은턴: k } }
125 }
126 흔들림예고 { 방향: 0, 남은턴: 0 }
129// 위험 칸과 다음 경비 위치는 한 턴 뒤의 순찰이라 셸이 다음 수를 미리 판정하게 합니다.
130function 그리기목록(현재: 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")
153function 처리(입력: 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:{그리기목록(새상태)}"
168input() |> 처리 |> print

SHA256 a4a457ebc67d945aafcb4fe985bd3c9660dd67a9c51dc7ddf5e850442e2b4139