sim.tpz
// 결정론 입자 시뮬레이션. 시드로 PRNG를 초기화해 입자를 만들고, 고정 스텝 물리로 굴린 뒤 SVG로 그린다.
// Deterministic particle simulation. Seed a PRNG, create particles, run fixed-step physics, draw SVG.
// 같은 입력(시드 개수 스텝 중력)은 항상 바이트 동일한 SVG를 낸다. 이것이 리플레이 결정론이다.
// The same input (seed count steps gravity) always yields a byte-identical SVG. This is replay determinism.
// 식별자 용어집 / glossary:
// 입자=particle, 엑스/와이=x/y, 브이엑스/브이와이=vx/vy, 다음난수=next PRNG int, 단위난수=[0,1) float,
// 정수인가=isInt, 이스케이프=html-escape, 처리=process, 자취=trajectory, 반발=restitution.
// 안 하는 것: 실시간 시계 애니메이션, 시스템 난수, 네트워크, 입자간 충돌, sqrt 힘, canvas 픽셀버퍼(SVG만).
// Won't do: clock-driven animation, system RNG, network, particle collisions, sqrt forces, canvas pixels.
type 입자 = { 엑스: float, 와이: float, 브이엑스: float, 브이와이: float }
let 너비 = 360
let 높이 = 260
let 최대개수 = 100
let 최대스텝 = 1500
let 최대작업 = 15000
// LCG 의사난수. 시드 기반 결정론, 시스템 난수/시계 미사용.
// LCG PRNG: seeded and deterministic, no system randomness or clock.
function 다음난수(상태: int) -> int {
let 다음 = (1103515245 * 상태 + 12345) % 2147483648
if 다음 < 0 { 다음 + 2147483648 } else { 다음 }
}
// PRNG 상태를 [0,1) 실수로 바꾼다.
// Map a PRNG state to a [0,1) float.
function 단위난수(상태: int) -> float {
toFloat(상태) / 2147483648.0
}
// 정수 문자열인가(선행 - 허용).
// Is the string an integer (optional leading minus)?
function 정수인가(글자열: string) -> bool {
let 글자들 = 글자열.scalars()
if 글자들.length == 0 { return false }
let mut 시작 = 0
if 글자들[0] == "-" { 시작 = 1 }
if 글자들.length == 시작 { return false }
let mut 번호 = 시작
while 번호 < 글자들.length {
let 코 = 글자들[번호].codePointAt(0) ?? 0
if 코 < 48 || 코 > 57 { return false }
번호 = 번호 + 1
}
true
}
// HTML 이스케이프. 사용자 입력은 출력 전에 반드시 이스케이프한다.
// HTML escape: always escape user input before output.
function 이스케이프(원문: string) -> string {
let mut 결과 = ""
for 글 in 원문.scalars() {
if 글 == "&" { 결과 = 결과 + "&" }
else if 글 == "<" { 결과 = 결과 + "<" }
else if 글 == ">" { 결과 = 결과 + ">" }
else if 글 == "\"" { 결과 = 결과 + """ }
else { 결과 = 결과 + 글 }
}
결과
}
// 입력을 받아 시뮬레이션 결과 SVG(또는 오류)를 낸다.
// Take the input and produce the simulation SVG (or an error).
function 처리(입력: string) -> string {
let 토큰들 = filter(입력.trim().split(" "), 토 => 토 != "")
if 토큰들.length == 0 {
return "<div class=\"meta\">시드 개수 스텝을 입력하세요. 중력은 선택입니다. 예: 42 20 250 40</div>"
}
if 토큰들.length < 3 {
return "<div class=\"err\">시드, 개수, 스텝을 모두 입력하세요. 예: 42 20 250 40</div>"
}
if 토큰들.length > 4 {
return "<div class=\"err\">입력은 시드 개수 스텝과 선택 중력까지 네 개입니다. 예: 42 20 250 40</div>"
}
let mut 검번호 = 0
while 검번호 < 토큰들.length {
let 토 = 토큰들[검번호]
if !정수인가(토) {
return "<div class=\"err\">숫자만 입력하세요: {이스케이프(토)}</div>"
}
if 토.scalars().length > 12 {
return "<div class=\"err\">숫자가 너무 큽니다: {이스케이프(토)}</div>"
}
검번호 = 검번호 + 1
}
let 시드원 = toInt(토큰들[0]) ?? 0
let 개수 = toInt(토큰들[1]) ?? 0
let 스텝 = toInt(토큰들[2]) ?? 0
let mut 중력정수 = 40
if 토큰들.length >= 4 { 중력정수 = toInt(토큰들[3]) ?? 0 }
if 개수 < 1 || 개수 > 최대개수 {
return "<div class=\"err\">입자 개수는 1부터 {최대개수}까지여야 합니다.</div>"
}
if 스텝 < 1 || 스텝 > 최대스텝 {
return "<div class=\"err\">스텝은 1부터 {최대스텝}까지여야 합니다.</div>"
}
if 개수 * 스텝 > 최대작업 {
return "<div class=\"err\">개수와 스텝의 곱이 너무 큽니다. 최대 {최대작업}까지 가능합니다.</div>"
}
// 시드를 [0, 2^31)로 접어 LCG 곱이 넘치지 않게 한다.
// Fold the seed into [0, 2^31) so the LCG multiply cannot overflow.
let 시드절대 = ((시드원 % 2147483648) + 2147483648) % 2147483648
let mut 상태 = 시드절대 % 2147483648
상태 = 다음난수(상태)
상태 = 다음난수(상태)
let 중력 = toFloat(중력정수) / 100.0
let 반발 = 0.82
let 너비실수 = toFloat(너비)
let 높이실수 = toFloat(높이)
// 입자 초기화(시드에서 위치와 속도를 뽑는다).
// Initialize particles, drawing position and velocity from the seed.
let mut 입자들: Array<입자> = []
let mut 자취들: Array<string> = []
let mut 만든수 = 0
while 만든수 < 개수 {
상태 = 다음난수(상태)
let 시작엑스 = 단위난수(상태) * (너비실수 - 20.0) + 10.0
상태 = 다음난수(상태)
let 시작와이 = 단위난수(상태) * (높이실수 * 0.5) + 10.0
상태 = 다음난수(상태)
let 시작브이엑스 = (단위난수(상태) - 0.5) * 8.0
상태 = 다음난수(상태)
let 시작브이와이 = (단위난수(상태) - 0.5) * 4.0
입자들.push({ 엑스: 시작엑스, 와이: 시작와이, 브이엑스: 시작브이엑스, 브이와이: 시작브이와이 })
자취들.push("")
만든수 = 만든수 + 1
}
// 고정 스텝 물리: 중력 + 벽 반사. 일정 간격으로 자취를 표본한다.
// Fixed-step physics: gravity plus wall reflection. Sample the trajectory at a fixed interval.
let 표본간격 = if 스텝 / 30 < 1 { 1 } else { 스텝 / 30 }
let mut 스텝번호 = 0
while 스텝번호 < 스텝 {
let mut 입자번호 = 0
while 입자번호 < 개수 {
let 현재 = 입자들[입자번호]
let mut 새브이와이 = 현재.브이와이 + 중력
let mut 새브이엑스 = 현재.브이엑스
let mut 새엑스 = 현재.엑스 + 새브이엑스
let mut 새와이 = 현재.와이 + 새브이와이
if 새엑스 < 5.0 { 새엑스 = 5.0; 새브이엑스 = -새브이엑스 * 반발 }
else if 새엑스 > 너비실수 - 5.0 { 새엑스 = 너비실수 - 5.0; 새브이엑스 = -새브이엑스 * 반발 }
if 새와이 < 5.0 { 새와이 = 5.0; 새브이와이 = -새브이와이 * 반발 }
else if 새와이 > 높이실수 - 5.0 { 새와이 = 높이실수 - 5.0; 새브이와이 = -새브이와이 * 반발 }
입자들[입자번호] = 현재{ 엑스: 새엑스, 와이: 새와이, 브이엑스: 새브이엑스, 브이와이: 새브이와이 }
입자번호 = 입자번호 + 1
}
if 스텝번호 % 표본간격 == 0 {
let mut 표번호 = 0
while 표번호 < 개수 {
자취들[표번호] = 자취들[표번호] + " {입자들[표번호].엑스},{입자들[표번호].와이}"
표번호 = 표번호 + 1
}
}
스텝번호 = 스텝번호 + 1
}
// SVG로 그린다: 상자, 입자별 자취(옅게), 마지막 위치(원).
// Render SVG: the box, each particle's trajectory (faint), and its final position (circle).
let mut 본문 = "<rect x=\"0\" y=\"0\" width=\"{너비}\" height=\"{높이}\" fill=\"none\" stroke=\"#111\" />"
let mut 그릴번호 = 0
while 그릴번호 < 개수 {
본문 = 본문 + "<polyline points=\"{자취들[그릴번호]}\" fill=\"none\" stroke=\"#bbb\" stroke-width=\"1\" />"
본문 = 본문 + "<circle cx=\"{입자들[그릴번호].엑스}\" cy=\"{입자들[그릴번호].와이}\" r=\"3\" fill=\"#111\" />"
그릴번호 = 그릴번호 + 1
}
let 머리 = "<div class=\"meta\">시드 {시드원}, 입자 {개수}개, 스텝 {스텝}회. 같은 입력은 같은 그림을 냅니다.</div>"
머리 + "<svg viewBox=\"0 0 {너비} {높이}\" width=\"100%\" style=\"border:1px solid #e3e3e3;margin-top:12px\">" + 본문 + "</svg>"
}
print(처리(input()))