slow.monster

STUDIO HAZE · jeniapp

Topaz source

Open by house policy. Topaz is our own language.

Topaz 5.20.2 · 63 files

apps/moon-market/main.tpz

Topaz · 7734 B · Original file

1// 달시장. 시드로 정해진 손님 줄을 다섯 밤 동안 응대하는 야시장 경제를 한 스텝씩 진행합니다.
2// 입력은 DS1 패킷의 STATE, EVENT, SHELL 줄이고 출력은 다음 STATE 줄과 화면을 그릴 DRAW 목록입니다.
3// 같은 시드와 같은 조작이면 늘 같은 밤이 나옵니다. 시간과 외부 난수를 쓰지 않습니다.
4// 화면을 직접 그리거나 저장하지 않습니다. 저장 문자열은 페이지가 들고 있다가 다음 스텝에 돌려줍니다.
5// moon-market: a deterministic night-market economy stepper over seeded customer queues.
7import core.state { 상태, 기본상태, 역직렬화, 직렬화, 품목표, 표갱신, 마지막밤, 알림번호, 단계토큰 }
8import data.market
10let 기본시드 = "DS-0001"
11let 사입원가 = 5
13// 공용 도우미 시작 (줄값 셸값)
14// 여러 도구가 같은 원본을 씁니다. 여기서 고치지 않고 원본에서 고친 뒤 다시 생성합니다.
16// 같은 접두의 줄이 여럿이면 마지막 줄을 씁니다.
17function 줄값(입력: string, 접두: string) -> string {
18 let 맞는줄들 = 입력.split("\n").map(줄 => 줄.trim()).filter(줄 => 줄.startsWith(접두))
19 if 맞는줄들.length == 0 { return "" }
20 let 끝줄 = 맞는줄들[맞는줄들.length - 1]
21 끝줄.slice(접두.scalars().length, 끝줄.scalars().length).trim()
24// "키=값,키=값" 에서 = 로 정확히 두 조각인 항만 읽고 키와 값의 앞뒤 공백을 지웁니다. 같은 키가 여럿이면 마지막 값을 씁니다.
25function 셸값(셸: string, 키: string) -> string {
26 let 값들 = 셸.split(",").map(항 => 항.split("=")).filter(조각들 => 조각들.length == 2 && 조각들[0].trim() == 키)
27 if 값들.length == 0 { "" } else { 값들[값들.length - 1][1].trim() }
30// 공용 도우미 끝
32type 이벤트 = "init" | "resume" | "up" | "down" | "left" | "right" | "a" | "b" | "unknown"
34function 이벤트파싱(토큰: string) -> 이벤트 {
35 match 토큰 {
36 case "init" => "init"
37 case "resume" => "resume"
38 case "up" => "up"
39 case "down" => "down"
40 case "left" => "left"
41 case "right" => "right"
42 case "a" => "a"
43 case "b" => "b"
44 case _ => "unknown"
45 }
48// 정산 화면의 A 는 다음 밤으로 넘어갑니다. 현금, 평판, 가격은 이어 가고 재고는 하룻밤에 하나씩 상합니다.
49function 다음밤(직전: 상태) -> 상태 {
50 if 직전.밤 >= 마지막밤 {
51 상태 { ...직전, 단계: "끝", 알림: "장사끝" }
52 } else {
53 상태 {
54 ...직전,
55 밤: 직전.밤 + 1,
56 단계: "영업",
57 재고: 품목표(market.품목목록().map(품 => (직전.재고.getOr(품, 0) - 1).atLeast(0))),
58 손님차례: 0,
59 알림: "다음밤",
60 }
61 }
64// 현금이 모자라면 사지 않고 알림만 지웁니다.
65function 사입(직전: 상태, 품: market.품목) -> 상태 {
66 if 직전.현금 >= 사입원가 {
67 상태 {
68 ...직전,
69 현금: 직전.현금 - 사입원가,
70 재고: 표갱신(직전.재고, 품, 직전.재고.getOr(품, 0) + 1),
71 알림: "사입",
72 }
73 } else {
74 상태 { ...직전, 알림: "없음" }
75 }
78// 알뜰한 손님은 흥정해서 예산의 80% 까지만 냅니다.
79function 지불한도(손님값: market.손님) -> int {
80 match 손님값.성향 {
81 case "알뜰" => 손님값.예산 * 8 / 10
82 case "급함" | "묶음" => 손님값.예산
83 }
86// 묶음을 좋아하는 손님은 좋은 거래에 평판을 하나 더 줍니다.
87function 평판보상(손님값: market.손님) -> int {
88 match 손님값.성향 {
89 case "묶음" => 2
90 case "알뜰" | "급함" => 1
91 }
94function 응대(직전: 상태) -> 상태 {
95 if 직전.손님차례 >= 직전.손님수 { return 상태 { ...직전, 알림: "없음" } }
96 let 손님값 = market.손님생성(market.djb2해시(직전.시드), 직전.밤, 직전.손님차례)
97 let 판매가 = 직전.가격.getOr(손님값.원함, 0)
98 let 재고수 = 직전.재고.getOr(손님값.원함, 0)
99 let 응대뒤 = if 판매가 <= 지불한도(손님값) && 재고수 > 0 {
100 상태 {
101 ...직전,
102 현금: 직전.현금 + 판매가,
103 재고: 표갱신(직전.재고, 손님값.원함, 재고수 - 1),
104 판매: 직전.판매 + 1,
105 평판: 직전.평판 + 평판보상(손님값),
106 알림: "판매",
107 }
108 } else {
109 상태 { ...직전, 놓침: 직전.놓침 + 1, 평판: (직전.평판 - 1).atLeast(0), 알림: "놓침" }
110 }
111 let 차례 = 직전.손님차례 + 1
112 if 차례 >= 직전.손님수 {
113 상태 { ...응대뒤, 손님차례: 차례, 단계: "정산", 알림: "정산" }
114 } else {
115 상태 { ...응대뒤, 손님차례: 차례 }
116 }
119// 영업 중에는 조작마다 알림을 새로 정하므로 아무것도 바꾸지 않는 조작도 알림을 지웁니다.
120function 영업(이벤트값: 이벤트, 직전: 상태) -> 상태 {
121 let 품목들 = market.품목목록()
122 match 이벤트값 {
123 case "left" => 상태 { ...직전, 선택: (직전.선택 + 품목들.length - 1) % 품목들.length, 알림: "없음" }
124 case "right" => 상태 { ...직전, 선택: (직전.선택 + 1) % 품목들.length, 알림: "없음" }
125 case "up" => {
126 let 품 = 품목들[직전.선택]
127 상태 { ...직전, 가격: 표갱신(직전.가격, 품, 직전.가격.getOr(품, 0) + 1), 알림: "가격올림" }
128 }
129 case "down" => {
130 let 품 = 품목들[직전.선택]
131 상태 { ...직전, 가격: 표갱신(직전.가격, 품, (직전.가격.getOr(품, 0) - 1).atLeast(1)), 알림: "가격내림" }
132 }
133 case "b" => 사입(직전, 품목들[직전.선택])
134 case "a" => 응대(직전)
135 case "init" | "resume" | "unknown" => 상태 { ...직전, 알림: "없음" }
136 }
139// 끝난 판과 정산 화면은 A 말고는 아무 조작도 받지 않으며 알림도 그대로 둡니다.
140function 적용(이벤트값: 이벤트, 직전: 상태) -> 상태 {
141 match 직전.단계 {
142 case "끝" => 직전
143 case "정산" => if 이벤트값 == "a" { 다음밤(직전) } else { 직전 }
144 case "영업" => 영업(이벤트값, 직전)
145 }
148function 품목그리기(현재: 상태, i: int) -> string {
149 let 품 = market.품목목록()[i]
150 let 골랐음 = if i == 현재.선택 { 1 } else { 0 }
151 "\nGOOD|{i}|{품}|{현재.재고.getOr(품, 0)}|{현재.가격.getOr(품, 0)}|{골랐음}"
154function 그리기목록(현재: 상태) -> string {
155 let 품목줄 = map(0..<market.품목목록().length, i => 품목그리기(현재, i)).join("")
156 let 손님줄 = if 현재.단계 == "영업" && 현재.손님차례 < 현재.손님수 {
157 let 손님값 = market.손님생성(market.djb2해시(현재.시드), 현재.밤, 현재.손님차례)
158 "\nCUST|{손님값.종류}|{손님값.원함}|{손님값.예산}|{손님값.성향}"
159 } else {
160 ""
161 }
162 "DD1{품목줄}{손님줄}\nHUD|{현재.밤}|{현재.현금}|{현재.평판}|{현재.손님차례}|{현재.손님수}|{현재.판매}|{현재.놓침}|{단계토큰(현재.단계)}|{알림번호(현재.알림)}"
165function 처리(입력: string) -> string {
166 let 이벤트값 = 줄값(입력, "EVENT:") |> 이벤트파싱
167 let 설정시드 = 셸값(줄값(입력, "SHELL:"), "seed")
168 let 시드 = if 설정시드 != "" { 설정시드 } else { 기본시드 }
169 let 새상태 = match 이벤트값 {
170 case "init" => 기본상태(시드)
171 case "resume" => 역직렬화(줄값(입력, "STATE:"), 시드)
172 case "up" | "down" | "left" | "right" | "a" | "b" | "unknown" => 적용(이벤트값, 역직렬화(줄값(입력, "STATE:"), 시드))
173 }
174 "DS1\nSTATE:{직렬화(새상태)}\nDRAW:{그리기목록(새상태)}"
177input() |> 처리 |> print

SHA256 8756025c235254d30f6f7e08cfe4f96eeaeb265f810077d5468ee58dce97defa