STUDIO HAZE · jeniapp
Topaz source
Open by house policy. Topaz is our own language.
Topaz 5.20.2 · 63 files
apps/sim/sim.tpz
Topaz · 9206 B · Original file
1
// 입자 시뮬레이션. 시드로 의사난수를 초기화해 입자를 만들고 중력과 벽 반사로 굴린 뒤 자취와 마지막 위치를 그립니다.2
// 입력은 "시드 개수 스텝 중력" 정수 토큰이며 중력은 100분의 1 단위이고 생략하면 40 입니다. 출력은 요약 한 줄과 SVG 하나를 담은 HTML 조각입니다.3
// 같은 입력이면 인터프리터와 웹 빌드에서 바이트까지 같은 출력을 냅니다. 시계와 시스템 난수는 쓰지 않습니다.4
// 실시간 애니메이션, 입자끼리의 충돌, 캔버스 픽셀 그리기는 하지 않습니다.5
// Particle simulation: seeded particles under gravity with damped wall bounces, drawn as SVG.7
record 입자 { x: float, y: float, vx: float, vy: float }8
record 축운동 { 위치: float, 속도: float }9
record 궤적 { 점들: Array<string>, 끝: 입자 }10
record 조건 { 시드: int, 개수: int, 스텝: int, 중력: float }12
let 너비 = 36013
let 높이 = 26014
let 너비실수 = toFloat(너비)15
let 높이실수 = toFloat(높이)16
let 반발 = 0.8217
let 최대개수 = 10018
let 최대스텝 = 150019
let 최대작업 = 1500020
let 난수범위 = 214748364821
let 입력안내 = "<div class=\"meta\">시드 개수 스텝을 입력하세요. 중력은 선택입니다. 예: 42 20 250 40</div>"23
// 공용 도우미 시작 (이스케이프 숫자인가 모두숫자인가 오류알림 정수오류 정수인가 정수읽기 선형합동다음 결과풀기 오류바꾸기)24
// 여러 도구가 같은 원본을 씁니다. 여기서 고치지 않고 원본에서 고친 뒤 다시 생성합니다.26
// 사용자 입력을 HTML 에 넣기 전에 반드시 거칩니다.27
function 이스케이프(원문: string) -> string {28
원문.replace("&", "&").replace("<", "<").replace(">", ">").replace("\"", """)29
}31
function 숫자인가(글자: string) -> bool {32
let 코드 = 글자.codePointAt(0) ?? 033
코드 >= 48 && 코드 <= 5734
}36
// 빈 문자열은 참입니다. 부호와 소수점을 받지 않으려고 toInt 앞에서 씁니다.37
function 모두숫자인가(글: string) -> bool {38
글.scalars().reduce(true, (모두, 글자) => 모두 && 숫자인가(글자))39
}41
// 문구는 고정 문장이거나 이미 이스케이프한 값이어야 합니다.42
function 오류알림(문구: string) -> string {43
"<div class=\"err\">{문구}</div>"44
}46
// 정수 읽기가 실패한 까닭입니다. 보여 줄 문구는 앱이 정합니다.47
type 정수오류 = "숫자아님" | "너무큼"49
// 맨 앞의 빼기 부호 하나만 받습니다. 더하기 부호와 소수점은 받지 않습니다.50
function 정수인가(토큰: string) -> bool {51
let 글자들 = 토큰.scalars()52
let 시작 = if 토큰.startsWith("-") { 1 } else { 0 }53
글자들.length > 시작 && 모두숫자인가(글자들.slice(시작, 글자들.length).join(""))54
}56
// 부호까지 센 글자 수를 최대자릿수로 막아 toInt 와 뒤의 계산이 넘치지 않게 합니다.57
function 정수읽기(토큰: string, 최대자릿수: int) -> Result<int, 정수오류> {58
if !정수인가(토큰) { return Err("숫자아님") }59
if 토큰.scalars().length > 최대자릿수 { return Err("너무큼") }60
Ok(toInt(토큰) ?? 0)61
}63
// 곱수 1103515245, 증분 12345, 법 2^31 의 선형 합동입니다. 값이 2^31 미만이면 곱이 64비트를 넘지 않고, 음수는 [0, 2^31) 로 옮깁니다.64
function 선형합동다음(값: int) -> int {65
let 다음 = (1103515245 * 값 + 12345) % 214748364866
if 다음 < 0 { 다음 + 2147483648 } else { 다음 }67
}69
// 성공 값과 안내가 모두 바로 보여 줄 HTML 조각일 때 진입점에서 한 번 풉니다. 토파즈 과제 'Result 오류 쪽 변환과 풀기' 의 우회입니다.70
function 결과풀기(값: Result<string, string>) -> string {71
match 값 {72
case Ok(결과) => 결과73
case Err(안내) => 안내74
}75
}77
// 성공 값은 그대로 두고 오류만 문구로 바꿉니다. 토파즈 과제 'Result 오류 쪽 변환과 풀기' 의 우회입니다.78
function 오류바꾸기<T, E>(결과: Result<T, E>, 바꾸기: (E) -> string) -> Result<T, string> {79
match 결과 {80
case Ok(값) => Ok(값)81
case Err(오류) => Err(바꾸기(오류))82
}83
}85
// 공용 도우미 끝87
// 처음 두 값을 버리는 것까지 출력 계약입니다.88
function 난수열(시드: int, 길이: int) -> Array<float> {89
let mut 상태 = 선형합동다음(선형합동다음(((시드 % 난수범위) + 난수범위) % 난수범위))90
let mut 값들: Array<float> = []91
while 값들.length < 길이 {92
상태 = 선형합동다음(상태)93
값들.push(toFloat(상태) / toFloat(난수범위))94
}95
값들96
}98
function 입자만들기(난수들: Array<float>, 시작: int) -> 입자 {99
입자 {100
x: 난수들[시작] * (너비실수 - 20.0) + 10.0,101
y: 난수들[시작 + 1] * (높이실수 * 0.5) + 10.0,102
vx: (난수들[시작 + 2] - 0.5) * 8.0,103
vy: (난수들[시작 + 3] - 0.5) * 4.0,104
}105
}107
// 벽 안쪽 5 에서 멈추고 속도를 뒤집어 줄입니다.108
function 벽반사(위치: float, 속도: float, 벽: float) -> 축운동 {109
if 위치 < 5.0 {110
축운동 { 위치: 5.0, 속도: -속도 * 반발 }111
} else if 위치 > 벽 - 5.0 {112
축운동 { 위치: 벽 - 5.0, 속도: -속도 * 반발 }113
} else {114
축운동 { 위치: 위치, 속도: 속도 }115
}116
}118
// 속도를 먼저 바꾸고 위치에 더합니다. 이 덧셈 순서가 출력 소수를 정합니다.119
function 한걸음(현재: 입자, 중력: float) -> 입자 {120
let 가로 = 벽반사(현재.x + 현재.vx, 현재.vx, 너비실수)121
let vy = 현재.vy + 중력122
let 세로 = 벽반사(현재.y + vy, vy, 높이실수)123
입자 { x: 가로.위치, y: 세로.위치, vx: 가로.속도, vy: 세로.속도 }124
}126
// 입자끼리 영향을 주지 않으므로 입자마다 따로 굴려도 함께 굴린 결과와 같습니다.127
function 굴리기(처음: 입자, 스텝: int, 중력: float) -> 궤적 {128
// 스텝 수와 상관없이 자취 점을 30개 안팎으로 둡니다.129
let 표본간격 = (스텝 / 30).atLeast(1)130
let mut 현재 = 처음131
let mut 점들: Array<string> = []132
for 스텝번호 in 0..<스텝 {133
현재 = 한걸음(현재, 중력)134
if 스텝번호 % 표본간격 == 0 { 점들.push(" {현재.x},{현재.y}") }135
}136
궤적 { 점들: 점들, 끝: 현재 }137
}139
function 궤적그리기(자취: 궤적) -> string {140
let 선 = "<polyline points=\"{자취.점들.join("")}\" fill=\"none\" stroke=\"#bbb\" stroke-width=\"1\" />"141
"{선}<circle cx=\"{자취.끝.x}\" cy=\"{자취.끝.y}\" r=\"3\" fill=\"#111\" />"142
}144
function 정수문구(종류: 정수오류, 토큰: string) -> string {145
match 종류 {146
case "숫자아님" => 오류알림("숫자만 입력하세요: {이스케이프(토큰)}")147
case "너무큼" => 오류알림("숫자가 너무 큽니다: {이스케이프(토큰)}")148
}149
}151
// 부호까지 12글자로 막아 toInt 와 뒤의 곱셈이 넘치지 않게 합니다.152
function 입력정수읽기(토큰: string) -> Result<int, string> {153
오류바꾸기(정수읽기(토큰, 12), 종류 => 정수문구(종류, 토큰))154
}156
function 조건읽기(입력: string) -> Result<조건, string> {157
let 토큰들 = 입력.trim().split(" ").filter(토큰 => 토큰 != "")158
if 토큰들.length == 0 { return Err(입력안내) }159
if 토큰들.length < 3 { return Err(오류알림("시드, 개수, 스텝을 모두 입력하세요. 예: 42 20 250 40")) }160
if 토큰들.length > 4 {161
return Err(오류알림("입력은 시드 개수 스텝과 선택 중력까지 네 개입니다. 예: 42 20 250 40"))162
}163
let 시드 = 입력정수읽기(토큰들[0])?164
let 개수 = 입력정수읽기(토큰들[1])?165
let 스텝 = 입력정수읽기(토큰들[2])?166
let 중력값 = if 토큰들.length == 4 { 입력정수읽기(토큰들[3])? } else { 40 }167
if 개수 < 1 || 개수 > 최대개수 { return Err(오류알림("입자 개수는 1부터 {최대개수}까지여야 합니다.")) }168
if 스텝 < 1 || 스텝 > 최대스텝 { return Err(오류알림("스텝은 1부터 {최대스텝}까지여야 합니다.")) }169
if 개수 * 스텝 > 최대작업 {170
return Err(오류알림("개수와 스텝의 곱이 너무 큽니다. 최대 {최대작업}까지 가능합니다."))171
}172
Ok(조건 { 시드: 시드, 개수: 개수, 스텝: 스텝, 중력: toFloat(중력값) / 100.0 })173
}175
function 시뮬레이션(입력: string) -> Result<string, string> {176
let 설정 = 조건읽기(입력)?177
let 난수들 = 난수열(설정.시드, 설정.개수 * 4)178
let 그림들 = map(0..<설정.개수, i => 궤적그리기(굴리기(입자만들기(난수들, i * 4), 설정.스텝, 설정.중력)))179
let 상자 = "<rect x=\"0\" y=\"0\" width=\"{너비}\" height=\"{높이}\" fill=\"none\" stroke=\"#111\" />"180
let 요약 = "<div class=\"meta\">시드 {설정.시드}, 입자 {설정.개수}개, 스텝 {설정.스텝}회. 같은 입력은 같은 그림을 냅니다.</div>"181
let 틀 = "<svg viewBox=\"0 0 {너비} {높이}\" width=\"100%\" style=\"border:1px solid #e3e3e3;margin-top:12px\">"182
Ok("{요약}{틀}{상자}{그림들.join("")}</svg>")183
}185
input() |> 시뮬레이션 |> 결과풀기 |> printSHA256 a14bb93bd19390e28db1d1bb00aeecd4adab0bf68b5717bb63ba5ce7a63f938d