slow.monster

STUDIO HAZE · jeniapp

Topaz source

Open by house policy. Topaz is our own language.

Topaz 5.20.2 · 63 files

apps/slow-monster/main.tpz

Topaz · 20125 B · Original file

1// slow.monster 의 진입점입니다. 입력 패킷 하나를 읽어 순수 코어로 다음 상태를 만들고 그 상태와 그리기 목록을 출력합니다.
2// 입력은 SM1, STATE:<직전 STATE 또는 빈칸>, EVENT:<init|resume|up|down|left|right|a|b|start|select|auto>, SHELL:<seed=,demo=,intent=,...> 네 줄이고 출력은 SM1, STATE:<새 STATE>, DRAW:<그리기 목록> 세 줄입니다.
3// 같은 패킷이면 늘 같은 출력입니다. 시드는 init 에서만 SHELL 을 따르고 그 뒤로는 STATE 안의 월드시드를 씁니다. demo 와 intent 도 입력의 일부라 켠 쪽과 끈 쪽이 각각 결정론입니다.
4// 무작위 조우는 없고 마주 본 엔티티에 A 를 눌러야 전투나 휴식이 일어납니다. 읽지 못한 STATE 는 기본 상태로 닫습니다.
5// slow.monster entry: one input packet in, the next STATE and its draw list out.
7import core.state
8import core.drawlist { 드로우리스트 }
9import core.creature
10import core.battle
11import core.encounter { 야생크리처 }
12import core.genome { 야생계통시드, 진화준비, 진화임계, 드리프트가두기 }
13import core.clash
14import core.evolution
15import data.maps { 맵타일들, 타일에서, 워프에서, 워프, 타일_물, 타일_나무 }
16import data.species
17import data.entities
18import core.auto { 자동결정 }
19import core.story { 파생비트, 켜진비트인가, 비트켜기, 비트끄기, 최하위비트, 비트수 }
20import core.util { 범위가두기 }
22let 기본시드 = "SM-SEED-001"
24// 공용 도우미 시작 (줄값 셸값)
25// 여러 도구가 같은 원본을 씁니다. 여기서 고치지 않고 원본에서 고친 뒤 다시 생성합니다.
27// 같은 접두의 줄이 여럿이면 마지막 줄을 씁니다.
28function 줄값(입력: string, 접두: string) -> string {
29 let 맞는줄들 = 입력.split("\n").map(줄 => 줄.trim()).filter(줄 => 줄.startsWith(접두))
30 if 맞는줄들.length == 0 { return "" }
31 let 끝줄 = 맞는줄들[맞는줄들.length - 1]
32 끝줄.slice(접두.scalars().length, 끝줄.scalars().length).trim()
35// "키=값,키=값" 에서 = 로 정확히 두 조각인 항만 읽고 키와 값의 앞뒤 공백을 지웁니다. 같은 키가 여럿이면 마지막 값을 씁니다.
36function 셸값(셸: string, 키: string) -> string {
37 let 값들 = 셸.split(",").map(항 => 항.split("=")).filter(조각들 => 조각들.length == 2 && 조각들[0].trim() == 키)
38 if 값들.length == 0 { "" } else { 값들[값들.length - 1][1].trim() }
41// 공용 도우미 끝
43type 이벤트 = "init" | "resume" | "up" | "down" | "left" | "right" | "a" | "b" | "start" | "select" | "auto" | "unknown"
45function 이벤트파싱(토큰: string) -> 이벤트 {
46 match 토큰.trim() {
47 case "init" => "init"
48 case "resume" => "resume"
49 case "up" => "up"
50 case "down" => "down"
51 case "left" => "left"
52 case "right" => "right"
53 case "a" => "a"
54 case "b" => "b"
55 case "start" => "start"
56 case "select" => "select"
57 case "auto" => "auto"
58 case _ => "unknown"
59 }
62function 진행입력인가(이벤트값: 이벤트) -> bool {
63 match 이벤트값 {
64 case "a" | "b" => true
65 case "init" | "resume" | "up" | "down" | "left" | "right" | "start" | "select" | "auto" | "unknown" => false
66 }
69function 파티슬롯교체(파티: Array<creature.전투원>, 자리: int, 새것: creature.전투원) -> Array<creature.전투원> {
70 map(0..<파티.length, i => if i == 자리 { 새것 } else { 파티[i] })
73// 물, 나무, 엔티티 칸은 막혀 제자리에서 방향만 바꿉니다. 옮긴 칸에 워프가 있으면 목적 맵의 목적 칸으로 갑니다.
74function 이동(현재: state.상태, 방향: state.방향) -> state.상태 {
75 let 돌아선 = 현재.플레이어{ 방향: 방향 }
76 let 새x = state.마주x(돌아선)
77 let 새y = state.마주y(돌아선)
78 let 타일 = 타일에서(맵타일들(현재.맵아이디), 새x, 새y)
79 let 막힘 = 타일 == 타일_물 || 타일 == 타일_나무 || entities.활성엔티티칸인가(현재.맵아이디, 새x, 새y, 현재.처치된것들)
80 let 목적x = if 막힘 { 현재.플레이어.x } else { 새x }
81 let 목적y = if 막힘 { 현재.플레이어.y } else { 새y }
82 let 도착 = match 워프에서(현재.맵아이디, 목적x, 목적y) {
83 case Some(대상) => { 맵아이디: 대상.목적맵, x: 대상.목적x, y: 대상.목적y }
84 case None => { 맵아이디: 현재.맵아이디, x: 목적x, y: 목적y }
85 }
86 현재{
87 스텝: 현재.스텝 + 1,
88 플레이어: { x: 도착.x, y: 도착.y, 방향: 방향 },
89 맵아이디: 도착.맵아이디,
90 장면: "field",
91 야생: state.빈야생(),
92 }
95// 야생이면 그 종과 레벨로 압력전을 열고, 인물이면 쉬어 파티 전체를 채웁니다.
96function 상호작용(현재: state.상태) -> state.상태 {
97 let 대상 = entities.활성엔티티에서(현재.맵아이디, state.마주x(현재.플레이어), state.마주y(현재.플레이어), 현재.처치된것들)
98 match 대상.종류 {
99 case 야생(종아이디, 레벨) => {
100 let 적종 = species.종찾기(종아이디)
101 현재{
102 장면: "battle",
103 야생: 야생크리처 { 종아이디: 적종.아이디, 이름: 적종.이름, 레벨: 레벨, 체력: 적종.기본체력, 스프라이트: 적종.스프라이트, 속성: 적종.속성 },
104 배틀: clash.압력전시작(종아이디, 레벨),
105 }
106 }
107 case 인물(_) => state.파티회복(현재)
108 case 없음 => 현재
109 }
112// 전투 중에는 플레이어가 움직이지 않으므로 마주 본 칸이 여전히 그 야생입니다.
113function 처치적용(현재: state.상태) -> state.상태 {
114 let 대상 = entities.활성엔티티에서(현재.맵아이디, state.마주x(현재.플레이어), state.마주y(현재.플레이어), 현재.처치된것들)
115 if entities.야생인가(대상) { 현재{ 처치된것들: state.처치추가(현재.처치된것들, 대상.아이디) } } else { 현재 }
118// SELECT 버튼과 Shift 키는 같은 select 이벤트를 보냅니다.
119function 필드적용(이벤트값: 이벤트, 직전: state.상태) -> state.상태 {
120 match 이벤트값 {
121 case "up" => 이동(직전, "위")
122 case "down" => 이동(직전, "아래")
123 case "left" => 이동(직전, "왼쪽")
124 case "right" => 이동(직전, "오른쪽")
125 case "a" => 상호작용(직전)
126 case "start" => 직전{ 장면: "party", 파티커서: 0 }
127 case "select" => 직전{ 장면: "dex" }
128 case "init" | "resume" | "b" | "auto" | "unknown" => 직전
129 }
132// 복원한 커서는 범위를 벗어날 수 있어 한쪽 끝만 넘어갈 때 반대쪽으로 돕니다.
133function 이전커서(커서: int, 길이: int) -> int {
134 if 커서 - 1 < 0 { 길이 - 1 } else { 커서 - 1 }
137function 다음커서(커서: int, 길이: int) -> int {
138 if 커서 + 1 >= 길이 { 0 } else { 커서 + 1 }
141// A 는 커서의 동료를 초점으로 삼아 이제 그 한 마리가 배틀하고 진화합니다.
142function 파티적용(이벤트값: 이벤트, 직전: state.상태) -> state.상태 {
143 let 길이 = 직전.파티.length
144 match 이벤트값 {
145 case "b" | "start" => 직전{ 장면: "field", 파티커서: 0 }
146 case "up" => if 길이 <= 0 { 직전 } else { 직전{ 장면: "party", 파티커서: 이전커서(직전.파티커서, 길이) } }
147 case "down" => if 길이 <= 0 { 직전 } else { 직전{ 장면: "party", 파티커서: 다음커서(직전.파티커서, 길이) } }
148 case "a" => if 길이 <= 0 { 직전 } else { 직전{ 초점: 범위가두기(직전.파티커서, 0, 길이 - 1) } }
149 case "init" | "resume" | "left" | "right" | "select" | "auto" | "unknown" => 직전
150 }
153// 커서는 발견 순서로 돌고 닫았다 열어도 같은 노드를 가리킵니다.
154function 도감적용(이벤트값: 이벤트, 직전: state.상태) -> state.상태 {
155 let 길이 = 직전.계통노드들.length
156 match 이벤트값 {
157 case "b" | "select" => 직전{ 장면: "field" }
158 case "left" | "up" => if 길이 <= 0 { 직전 } else { 직전{ 장면: "dex", 도감커서: 이전커서(직전.도감커서, 길이) } }
159 case "right" | "down" => if 길이 <= 0 { 직전 } else { 직전{ 장면: "dex", 도감커서: 다음커서(직전.도감커서, 길이) } }
160 case "init" | "resume" | "a" | "start" | "auto" | "unknown" => 직전
161 }
164function 배틀갱신(직전: state.상태, 새배틀: battle.배틀상태, 새활성: creature.전투원) -> state.상태 {
165 직전{ 장면: "battle", 파티: 파티슬롯교체(직전.파티, 직전.초점, 새활성), 배틀: 새배틀 }
168// 패배하면 체력을 채우되 계통시드, 드리프트, 진화 형태는 지우지 않습니다.
169function 배틀종료(직전: state.상태, 새활성: creature.전투원, 패배: bool) -> state.상태 {
170 let 남길전투원 = if 패배 { creature.회복(새활성) } else { 새활성 }
171 직전{ 장면: "field", 야생: state.빈야생(), 파티: 파티슬롯교체(직전.파티, 직전.초점, 남길전투원), 배틀: battle.빈배틀() }
174// 같은 시드로 같은 스텝, 칸, 종에서 잡으면 같은 계통시드라 같은 게놈입니다.
175function 포획성공처리(직전: state.상태, 새볼: int, 새포획카운터: int) -> state.상태 {
176 let 계통시드 = 야생계통시드(직전.월드시드, 직전.스텝, 직전.플레이어.x, 직전.플레이어.y, 직전.야생.종아이디)
177 let 잡은것 = creature.종에서전투원(직전.야생.종아이디, 직전.야생.레벨, 계통시드)
178 let 추가 = state.파티추가(직전.파티, 직전.보관함, 잡은것)
179 직전{
180 장면: "field",
181 야생: state.빈야생(),
182 스포어볼: 새볼,
183 포획카운터: 새포획카운터,
184 파티: 추가.파티,
185 보관함: 추가.보관함,
186 계통노드들: state.노드추가(직전.계통노드들, state.시작노드(잡은것, 직전.스텝)),
187 배틀: battle.빈배틀(),
188 }
191// 후보는 저장하지 않고 부모 스냅샷에서 다시 유도합니다. 부모고유번호는 고정한 뒤 자식 노드에 찍혀 계통수의 가지를 잇습니다.
192function 진화장면진입(직전: state.상태, 활성: creature.전투원) -> state.상태 {
193 // 진화 상태 전체에 타입을 달면 인터프리터와 웹 런타임이 게놈 타입의 모듈을 찾지 못해 단계만 좁혀 둡니다. 토파즈 과제 '구조 타입 주석 속 다른 모듈 레코드' 의 우회입니다.
194 let 고르기: state.진화단계 = "choose"
195 let 새진화 = {
196 단계: 고르기,
197 부모형태: if 활성.형태이름 != "" { 활성.형태이름 } else { species.종찾기(활성.종아이디).이름 },
198 부모게놈: 활성.게놈,
199 부모계통시드: 활성.계통시드,
200 부모진화카운터: 활성.진화카운터,
201 부모세대: 활성.세대,
202 부모고유번호: evolution.계통고유번호(활성.계통시드),
203 선택커서: 0,
204 잠긴분기: 0,
205 결과형태: "",
206 결과게놈: 활성.게놈,
207 }
208 직전{ 장면: "evolve", 야생: state.빈야생(), 진화: 새진화, 배틀: battle.빈배틀() }
211// 메뉴와 고정이 같은 함수로 후보를 보므로 늘 같은 후보입니다.
212function 진화후보들(직전: state.상태) -> Array<evolution.후보> {
213 let 진 = 직전.진화
214 evolution.후보들유도(진.부모게놈, evolution.계통고유번호(진.부모계통시드), 진.부모계통시드, 진.부모진화카운터)
217// 계통은 패배에서 배웁니다. 드리프트를 얻고 체력을 채우며, 준비되면 그 자리에서 진화해 더 강해져 다시 붙습니다. 야생은 맵에 남습니다.
218// 그래서 약한 옛 저장도 죽음의 반복 대신 적응으로 빠져나옵니다.
219function 배틀패배처리(직전: state.상태, 활성: creature.전투원) -> state.상태 {
220 let 학습 = creature.드리프트설정(활성, 드리프트가두기(활성.드리프트 + 진화임계))
221 if 진화준비(학습.드리프트) { 진화장면진입(배틀갱신(직전, battle.빈배틀(), 학습), 학습) } else { 배틀종료(직전, 학습, true) }
224// 진화장면진입은 키운 전투원이 이미 초점 자리에 들어간 상태를 받아야 합니다.
225function 배틀결과단계적용(이벤트값: 이벤트, 직전: state.상태, 활성: creature.전투원, 데모: bool) -> state.상태 {
226 if !진행입력인가(이벤트값) { return 직전 }
227 match 직전.배틀.결과 {
228 case "caught" => 처치적용(포획성공처리(직전, 직전.스포어볼 - 1, 직전.포획카운터 + 1))
229 case "win" => {
230 let 성장 = battle.경험적용(활성, creature.획득경험(직전.야생.레벨), 데모)
231 let 키운 = 성장.전투원
232 if 성장.오른레벨 > 0 && 진화준비(키운.드리프트) {
233 처치적용(진화장면진입(배틀갱신(직전, battle.빈배틀(), 키운), 키운))
234 } else {
235 처치적용(배틀종료(직전, 키운, false))
236 }
237 }
238 case "lose" | "fled" | "" => 배틀패배처리(직전, 활성)
239 }
242// 숲의 뿌리가 몇 그루 생길 때까지 파티 세 마리 미만이면 포획을 원하고, 그 뒤로는 쓰러뜨려 초점을 키웁니다.
243function 배틀비트단계적용(이벤트값: 이벤트, 직전: state.상태, 활성: creature.전투원) -> state.상태 {
244 if !진행입력인가(이벤트값) { return 직전 }
245 let 결과 = clash.비트진행(직전.월드시드, 직전.배틀, 활성, 직전.야생.종아이디, 직전.야생.레벨, 직전.스포어볼, 직전.파티.length < 3)
246 배틀갱신(직전, 결과.배틀, 결과.플레이어)
249// 옛 메뉴 전투의 menu, move, message 단계도 압력전 비트로 이어집니다.
250function 배틀적용(이벤트값: 이벤트, 직전: state.상태, 데모: bool) -> state.상태 {
251 let 활성 = state.초점전투원(직전)
252 match 직전.배틀.단계 {
253 case "result" => 배틀결과단계적용(이벤트값, 직전, 활성, 데모)
254 case "clash" | "menu" | "move" | "message" => 배틀비트단계적용(이벤트값, 직전, 활성)
255 }
258function 진화커서설정(직전: state.상태, 커서: int) -> state.상태 {
259 직전{ 장면: "evolve", 진화: 직전.진화{ 선택커서: 커서 } }
262// 고정은 되돌릴 수 없습니다. 진화 하위 상태를 morph 로 바꿔 한 프레임 동안 페이지가 모핑 장면을 재생하게 합니다.
263function 진화고정(직전: state.상태) -> state.상태 {
264 let 진 = 직전.진화
265 let 분기 = 범위가두기(진.선택커서, 0, 진화후보들(직전).length - 1)
266 let 결과 = evolution.분기진화(state.초점전투원(직전), 분기, 진.부모세대, 진.부모진화카운터)
267 let 모핑: state.진화단계 = "morph"
268 let 새진화 = 진{
269 단계: 모핑,
270 선택커서: 분기,
271 잠긴분기: 결과.분기,
272 결과형태: 결과.형태이름,
273 결과게놈: 결과.전투원.게놈,
274 }
275 직전{
276 장면: "evolve",
277 야생: state.빈야생(),
278 파티: 파티슬롯교체(직전.파티, 직전.초점, 결과.전투원),
279 계통노드들: state.노드추가(직전.계통노드들, evolution.노드빌드(결과, 진.부모고유번호, 직전.스텝)),
280 진화: 새진화,
281 배틀: battle.빈배틀(),
282 }
285function 진화종료(직전: state.상태) -> state.상태 {
286 직전{ 장면: "field", 야생: state.빈야생(), 진화: state.빈진화(), 배틀: battle.빈배틀() }
289// 모핑 프레임 뒤에는 어떤 키든 진화를 끝냅니다. none 은 생기지 않지만 생기면 필드로 돌아갑니다.
290function 진화적용(이벤트값: 이벤트, 직전: state.상태) -> state.상태 {
291 let 진 = 직전.진화
292 match 진.단계 {
293 case "morph" | "none" => 진화종료(직전)
294 case "choose" => {
295 let 수 = 진화후보들(직전).length
296 match 이벤트값 {
297 case "left" | "up" => 진화커서설정(직전, 이전커서(진.선택커서, 수))
298 case "right" | "down" => 진화커서설정(직전, 다음커서(진.선택커서, 수))
299 case "a" => 진화고정(직전)
300 case "init" | "resume" | "b" | "start" | "select" | "auto" | "unknown" => 직전
301 }
302 }
303 }
306// 도달했지만 아직 보지 않은 이정표를 본것과 대기에 켭니다. 본것은 큐에 넣을 때 켜서 저장과 재유도에서 겹치지 않습니다.
307// 필드에서만 이야기 카드로 넘어가고 전투, 진화, 도감 중에는 큐에만 쌓았다가 필드로 돌아올 때 띄웁니다.
308function 스토리갱신(현재: state.상태) -> state.상태 {
309 let 도달 = 파생비트(현재)
310 let 새비트들 = filter(0..<비트수, k => 켜진비트인가(도달, k) && !켜진비트인가(현재.스토리.본것, k))
311 let 새본것 = 새비트들.reduce(현재.스토리.본것, (비트셋, k) => 비트켜기(비트셋, k))
312 let 새대기 = 새비트들.reduce(현재.스토리.대기, (비트셋, k) => 비트켜기(비트셋, k))
313 let 새장면: state.장면 = if 현재.장면 == "field" && 새대기 != 0 { "story" } else { 현재.장면 }
314 현재{ 장면: 새장면, 스토리: { 본것: state.비트가두기(새본것), 대기: state.비트가두기(새대기) } }
317// 지금 카드를 큐에서 빼고 남은 카드가 없으면 필드로 갑니다.
318function 스토리진행(직전: state.상태) -> state.상태 {
319 let 비트 = 최하위비트(직전.스토리.대기)
320 let 새대기 = if 비트 >= 0 { 비트끄기(직전.스토리.대기, 비트) } else { 직전.스토리.대기 }
321 let 새장면: state.장면 = if 새대기 != 0 { "story" } else { "field" }
322 직전{ 장면: 새장면, 스토리: { 본것: 직전.스토리.본것, 대기: state.비트가두기(새대기) } }
325// auto 는 코어가 정한 보통 입력 하나를 같은 엔진으로 적용합니다. 정한 입력이 없으면 수동 갈림길처럼 멈춥니다.
326function 적용(이벤트값: 이벤트, 직전: state.상태, 데모: bool, 의도: int) -> state.상태 {
327 if 이벤트값 == "auto" {
328 let 자동입력 = 이벤트파싱(자동결정(직전, 의도))
329 if 자동입력 == "unknown" { return 직전 }
330 return 적용(자동입력, 직전, 데모, 의도)
331 }
332 match 직전.장면 {
333 case "story" => 스토리진행(직전)
334 case "battle" => 배틀적용(이벤트값, 직전, 데모)
335 case "party" => 파티적용(이벤트값, 직전)
336 case "evolve" => 진화적용(이벤트값, 직전)
337 case "dex" => 도감적용(이벤트값, 직전)
338 case "field" => 필드적용(이벤트값, 직전)
339 }
342function 역직렬화또는기본(문자열: string, 시드: string) -> state.상태 {
343 match state.역직렬화(문자열) {
344 case Ok(상태값) => 상태값
345 case Err(_) => state.기본상태(시드)
346 }
349// 의도 0 은 진화 갈림길에서 멈춰 플레이어가 고르고 1 은 첫 가지, 2 는 무작위 가지입니다.
350// 모든 경로를 스토리갱신으로 감싸 init 의 첫 조우 비트가 오프닝 카드로 열립니다. resume 은 어떤 장면이든 행동 없이 그대로 다시 그립니다.
351function 처리(입력: string) -> string {
352 let 이벤트값 = 이벤트파싱(줄값(입력, "EVENT:"))
353 let 셸 = 줄값(입력, "SHELL:")
354 let 셸시드 = 셸값(셸, "seed")
355 let 시드 = if 셸시드 != "" { 셸시드 } else { 기본시드 }
356 let 데모 = 셸값(셸, "demo") == "1"
357 let 의도 = match 셸값(셸, "intent") {
358 case "1" => 1
359 case "2" => 2
360 case _ => 0
361 }
362 let 상태문자열 = 줄값(입력, "STATE:")
363 let 새상태 = match 이벤트값 {
364 case "init" => 스토리갱신(state.기본상태(시드))
365 case "resume" => 스토리갱신(역직렬화또는기본(상태문자열, 시드))
366 case "up" | "down" | "left" | "right" | "a" | "b" | "start" | "select" | "auto" | "unknown" => 스토리갱신(적용(이벤트값, 역직렬화또는기본(상태문자열, 시드), 데모, 의도))
367 }
368 "SM1\nSTATE:{state.직렬화(새상태)}\nDRAW:{드로우리스트(새상태)}"
371input() |> 처리 |> print

SHA256 34510305cabd580dfa6781f680f3f05e38a4b811de312eca37af2f4113280566