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.7
import core.state { 상태, 기본상태, 역직렬화, 직렬화, 품목표, 표갱신, 마지막밤, 알림번호, 단계토큰 }8
import data.market10
let 기본시드 = "DS-0001"11
let 사입원가 = 513
// 공용 도우미 시작 (줄값 셸값)14
// 여러 도구가 같은 원본을 씁니다. 여기서 고치지 않고 원본에서 고친 뒤 다시 생성합니다.16
// 같은 접두의 줄이 여럿이면 마지막 줄을 씁니다.17
function 줄값(입력: 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()22
}24
// "키=값,키=값" 에서 = 로 정확히 두 조각인 항만 읽고 키와 값의 앞뒤 공백을 지웁니다. 같은 키가 여럿이면 마지막 값을 씁니다.25
function 셸값(셸: string, 키: string) -> string {26
let 값들 = 셸.split(",").map(항 => 항.split("=")).filter(조각들 => 조각들.length == 2 && 조각들[0].trim() == 키)27
if 값들.length == 0 { "" } else { 값들[값들.length - 1][1].trim() }28
}30
// 공용 도우미 끝32
type 이벤트 = "init" | "resume" | "up" | "down" | "left" | "right" | "a" | "b" | "unknown"34
function 이벤트파싱(토큰: 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
}46
}48
// 정산 화면의 A 는 다음 밤으로 넘어갑니다. 현금, 평판, 가격은 이어 가고 재고는 하룻밤에 하나씩 상합니다.49
function 다음밤(직전: 상태) -> 상태 {50
if 직전.밤 >= 마지막밤 {51
상태 { ...직전, 단계: "끝", 알림: "장사끝" }52
} else {53
상태 {54
...직전,55
밤: 직전.밤 + 1,56
단계: "영업",57
재고: 품목표(market.품목목록().map(품 => (직전.재고.getOr(품, 0) - 1).atLeast(0))),58
손님차례: 0,59
알림: "다음밤",60
}61
}62
}64
// 현금이 모자라면 사지 않고 알림만 지웁니다.65
function 사입(직전: 상태, 품: market.품목) -> 상태 {66
if 직전.현금 >= 사입원가 {67
상태 {68
...직전,69
현금: 직전.현금 - 사입원가,70
재고: 표갱신(직전.재고, 품, 직전.재고.getOr(품, 0) + 1),71
알림: "사입",72
}73
} else {74
상태 { ...직전, 알림: "없음" }75
}76
}78
// 알뜰한 손님은 흥정해서 예산의 80% 까지만 냅니다.79
function 지불한도(손님값: market.손님) -> int {80
match 손님값.성향 {81
case "알뜰" => 손님값.예산 * 8 / 1082
case "급함" | "묶음" => 손님값.예산83
}84
}86
// 묶음을 좋아하는 손님은 좋은 거래에 평판을 하나 더 줍니다.87
function 평판보상(손님값: market.손님) -> int {88
match 손님값.성향 {89
case "묶음" => 290
case "알뜰" | "급함" => 191
}92
}94
function 응대(직전: 상태) -> 상태 {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 차례 = 직전.손님차례 + 1112
if 차례 >= 직전.손님수 {113
상태 { ...응대뒤, 손님차례: 차례, 단계: "정산", 알림: "정산" }114
} else {115
상태 { ...응대뒤, 손님차례: 차례 }116
}117
}119
// 영업 중에는 조작마다 알림을 새로 정하므로 아무것도 바꾸지 않는 조작도 알림을 지웁니다.120
function 영업(이벤트값: 이벤트, 직전: 상태) -> 상태 {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
}137
}139
// 끝난 판과 정산 화면은 A 말고는 아무 조작도 받지 않으며 알림도 그대로 둡니다.140
function 적용(이벤트값: 이벤트, 직전: 상태) -> 상태 {141
match 직전.단계 {142
case "끝" => 직전143
case "정산" => if 이벤트값 == "a" { 다음밤(직전) } else { 직전 }144
case "영업" => 영업(이벤트값, 직전)145
}146
}148
function 품목그리기(현재: 상태, i: int) -> string {149
let 품 = market.품목목록()[i]150
let 골랐음 = if i == 현재.선택 { 1 } else { 0 }151
"\nGOOD|{i}|{품}|{현재.재고.getOr(품, 0)}|{현재.가격.getOr(품, 0)}|{골랐음}"152
}154
function 그리기목록(현재: 상태) -> 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|{현재.밤}|{현재.현금}|{현재.평판}|{현재.손님차례}|{현재.손님수}|{현재.판매}|{현재.놓침}|{단계토큰(현재.단계)}|{알림번호(현재.알림)}"163
}165
function 처리(입력: 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:{그리기목록(새상태)}"175
}177
input() |> 처리 |> printSHA256 8756025c235254d30f6f7e08cfe4f96eeaeb265f810077d5468ee58dce97defa