◀▶ 품목 · ▲▼ 가격 · 응대 · 사입. 같은 시드면 늘 같은 손님입니다.
// ===== main.tpz =====
// 달시장 — 결정론 야시장 경제. .tpz = 순수 스텝퍼(입력 → 다음 STATE + DRAW). 손님 줄은 시드로 결정 → 같은 밤. 레버: 가격(상하)·사입(B)·응대(A)·선택(좌우).
// moon-market: a deterministic night-market economy stepper. The customer queue is seeded, so the same seed is the same night. Levers: price, restock, serve, select.
import core.state
import data.market
let 기본시드 = "DS-0001"
// 닫힌 이벤트 태그. 원문 EVENT 토큰은 패킷 경계라 파싱 지점에서만 좁힌다.
// Closed event tags. The raw EVENT token is packet-boundary text and narrows only at parse time.
type 이벤트 = "init" | "resume" | "up" | "down" | "left" | "right" | "a" | "b" | "unknown"
function 이벤트파싱(토큰: string) -> 이벤트 {
match 토큰.trim() {
case "init" => "init"
case "resume" => "resume"
case "up" => "up"
case "down" => "down"
case "left" => "left"
case "right" => "right"
case "a" => "a"
case "b" => "b"
case _ => "unknown"
}
}
// 입력 패킷 파싱. str.slice = 게임 #1이 추가한 도그푸드 메서드를 여기서 사용. / packet parsing, using the str.slice added by game #1.
function 접두사뒤(전체: string, 접두사: string) -> string {
전체.slice(접두사.scalars().length, 전체.scalars().length)
}
function 줄값(입력: string, 접두: string) -> string {
let mut 값 = ""
for 줄 in 입력.split("\n") {
let 다듬 = 줄.trim()
if 다듬.startsWith(접두) { 값 = 접두사뒤(다듬, 접두) }
}
값.trim()
}
function 셸값(셸: string, 키: string) -> string {
for 항 in 셸.split(",") {
let kv = 항.split("=")
if kv.length >= 2 && kv[0] == 키 { return kv[1] }
}
""
}
// ── 한 손님/한 조작 적용 ── 좌우=선택, 상하=가격 ±1, B=사입(원가 5), A=응대(원하는 품목을 가격<=예산 & 재고>0이면 구매).
function 적용(이벤트값: 이벤트, 직전: state.상태) -> state.상태 {
let 품목 = market.품목목록()
if 직전.단계 == "끝" { return 직전 }
// 정산 화면에서 A = 다음 밤(현금·평판·가격은 이어가고, 재고는 하룻밤 상함 -1). 5밤이 끝나면 최종. / summary + A = next night (carry cash/rep/prices, spoil stock -1); after night 5, finale.
if 직전.단계 == "summary" {
if 이벤트값 != "a" { return 직전 }
if 직전.밤 >= 5 {
return 직전{ 단계: "끝", 메시지: 7 }
}
let mut 새재고: Map<string, int> = Map.new()
for 품 in 품목 { 새재고.insert(품, (직전.재고.getOr(품, 0) - 1).atLeast(0)) }
return 직전{ 밤: 직전.밤 + 1, 단계: "serve", 재고: 새재고, 큐인덱스: 0, 메시지: 8 }
}
let mut 선택 = 직전.선택
let mut 현금 = 직전.현금
let mut 평판 = 직전.평판
let mut 큐 = 직전.큐인덱스
let mut 판매 = 직전.판매
let mut 놓침 = 직전.놓침
let mut 단계: state.단계 = 직전.단계
let mut 메시지 = 0
// 재고·가격을 가변 복사해 수정(레코드 필드는 불변). 매 스텝 전체 복사 = Map.update가 있으면 사라질 군더더기(도그푸드).
let mut 재고: Map<string, int> = Map.new()
let mut 가격: Map<string, int> = Map.new()
for 품 in 품목 { 재고.insert(품, 직전.재고.getOr(품, 0)); 가격.insert(품, 직전.가격.getOr(품, 0)) }
match 이벤트값 {
case "left" => { 선택 = (선택 + 품목.length - 1) % 품목.length }
case "right" => { 선택 = (선택 + 1) % 품목.length }
case "up" => {
let 품 = 품목[선택]; 가격.insert(품, (가격.getOr(품, 0)) + 1); 메시지 = 1
}
case "down" => {
let 품 = 품목[선택]; 가격.insert(품, (가격.getOr(품, 0) - 1).atLeast(1)); 메시지 = 2
}
case "b" => {
let 품 = 품목[선택]; if 현금 >= 5 { 현금 = 현금 - 5; 재고.insert(품, (재고.getOr(품, 0)) + 1); 메시지 = 3 }
}
case "a" => {
if 큐 < 직전.손님수 {
let 손 = market.손님생성(market.시드숫자(직전.시드), 직전.밤, 큐)
let 원 = 손.원함
let 값 = 가격.getOr(원, 0)
let 수 = 재고.getOr(원, 0)
// 성향이 가격 판단을 바꾼다: 알뜰=예산의 80%까지만(흥정), 묶음=사면 평판 +2(좋은 거래를 좋아함), 급함=예산까지. / trait shapes the buy decision.
let 한도 = if 손.성향 == "알뜰" { 손.예산 * 8 / 10 } else { 손.예산 }
if 값 <= 한도 && 수 > 0 {
현금 = 현금 + 값; 재고.insert(원, 수 - 1); 판매 = 판매 + 1
평판 = 평판 + (if 손.성향 == "묶음" { 2 } else { 1 }); 메시지 = 4
} else {
놓침 = 놓침 + 1; 평판 = (평판 - 1).atLeast(0); 메시지 = 5
}
큐 = 큐 + 1
if 큐 >= 직전.손님수 { 단계 = "summary"; 메시지 = 6 }
}
}
case "init" => {}
case "resume" => {}
case "unknown" => {}
}
직전{
단계: 단계,
현금: 현금, 평판: 평판, 재고: 재고, 가격: 가격, 선택: 선택,
큐인덱스: 큐, 판매: 판매, 놓침: 놓침, 메시지: 메시지
}
}
function 불(b: bool) -> int { if b { 1 } else { 0 } }
export function 드로우리스트(s: state.상태) -> string {
let 품목 = market.품목목록()
let mut 품목줄 = ""
let mut i = 0
while i < 품목.length {
let 품 = 품목[i]
품목줄 = "{품목줄}\nGOOD|{i}|{품}|{s.재고.getOr(품, 0)}|{s.가격.getOr(품, 0)}|{불(i == s.선택)}"
i = i + 1
}
let 손님줄 = if s.단계 == "serve" && s.큐인덱스 < s.손님수 {
let 손 = market.손님생성(market.시드숫자(s.시드), s.밤, s.큐인덱스)
"\nCUST|{손.종류}|{손.원함}|{손.예산}|{손.성향}"
} else { "" }
"DD1{품목줄}{손님줄}\nHUD|{s.밤}|{s.현금}|{s.평판}|{s.큐인덱스}|{s.손님수}|{s.판매}|{s.놓침}|{s.단계}|{s.메시지}"
}
function 처리(입력: string) -> string {
let 이벤트값 = 이벤트파싱(줄값(입력, "EVENT:"))
let 셸 = 줄값(입력, "SHELL:")
let 셸시드 = 셸값(셸, "seed")
let 시드 = if 셸시드 != "" { 셸시드 } else { 기본시드 }
let 새상태 = match 이벤트값 {
case "init" => state.기본상태(시드)
case "resume" => state.역직렬화(줄값(입력, "STATE:"), 시드)
case "up" => 적용(이벤트값, state.역직렬화(줄값(입력, "STATE:"), 시드))
case "down" => 적용(이벤트값, state.역직렬화(줄값(입력, "STATE:"), 시드))
case "left" => 적용(이벤트값, state.역직렬화(줄값(입력, "STATE:"), 시드))
case "right" => 적용(이벤트값, state.역직렬화(줄값(입력, "STATE:"), 시드))
case "a" => 적용(이벤트값, state.역직렬화(줄값(입력, "STATE:"), 시드))
case "b" => 적용(이벤트값, state.역직렬화(줄값(입력, "STATE:"), 시드))
case "unknown" => 적용(이벤트값, state.역직렬화(줄값(입력, "STATE:"), 시드))
}
"DS1\nSTATE:{state.직렬화(새상태)}\nDRAW:{드로우리스트(새상태)}"
}
print(처리(input()))
// ===== core/state.tpz =====
// 달시장 STATE — 결정론 경제. 재고·가격은 Map<string,int>(품목→수량/값). 정규 상태는 레코드/맵, 스텝 경계에서만 파이프 문자열로 직렬화.
// moon-market STATE: canonical records/maps; serialized to a pipe string only at step boundaries. Stock/prices are Map<string,int> keyed by good.
import data.market
export type 단계 = "serve" | "summary" | "끝"
export type 상태 = {
시드: string, 밤: int, 단계: 단계,
현금: int, 평판: int,
재고: Map<string, int>, 가격: Map<string, int>,
선택: int, // 커서: 어느 품목을 고르는가 / selected good index
큐인덱스: int, 손님수: int, 판매: int, 놓침: int,
메시지: int
}
export function 기본상태(시드: string) -> 상태 {
let mut 재고: Map<string, int> = Map.new()
let mut 가격: Map<string, int> = Map.new()
for 품 in market.품목목록() { 재고.insert(품, 5); 가격.insert(품, 10) } // 시작: 각 5개, 가격 10원 / start: 5 each at 10
{
시드: 시드, 밤: 1, 단계: "serve",
현금: 20, 평판: 50, 재고: 재고, 가격: 가격, 선택: 0,
큐인덱스: 0, 손님수: 6, 판매: 0, 놓침: 0, 메시지: 0
}
}
// 고정 품목 순서로 Map 값을 직렬화/역직렬화(키 순회 대신 알려진 품목 목록 사용). / serialize a Map by the fixed goods order.
function 맵직렬(m: Map<string, int>) -> string {
let mut s = ""
for 품 in market.품목목록() {
let v = m.getOr(품, 0)
s = if s == "" { "{v}" } else { "{s};{v}" }
}
s
}
function 맵역직렬(부분: string) -> Map<string, int> {
let mut m: Map<string, int> = Map.new()
let 값들 = 부분.split(";")
let 품목 = market.품목목록()
let mut i = 0
while i < 품목.length {
let v = if i < 값들.length { toInt(값들[i]) ?? 0 } else { 0 }
m.insert(품목[i], v)
i = i + 1
}
m
}
function 단계파싱(토큰: string) -> 단계 {
match 토큰 {
case "serve" => "serve"
case "summary" => "summary"
case "끝" => "끝"
case _ => "serve"
}
}
export function 직렬화(s: 상태) -> string {
"DS1|{s.시드}|{s.밤}|{s.단계}|{s.현금}|{s.평판}|{맵직렬(s.재고)}|{맵직렬(s.가격)}|{s.선택}|{s.큐인덱스}|{s.손님수}|{s.판매}|{s.놓침}|{s.메시지}"
}
export function 역직렬화(문자열: string, 대체시드: string) -> 상태 {
let 부분 = 문자열.split("|")
if 부분.length < 14 { return 기본상태(대체시드) }
{
시드: 부분[1], 밤: toInt(부분[2]) ?? 1, 단계: 단계파싱(부분[3]),
현금: toInt(부분[4]) ?? 0, 평판: toInt(부분[5]) ?? 50,
재고: 맵역직렬(부분[6]), 가격: 맵역직렬(부분[7]),
선택: toInt(부분[8]) ?? 0, 큐인덱스: toInt(부분[9]) ?? 0, 손님수: toInt(부분[10]) ?? 6,
판매: toInt(부분[11]) ?? 0, 놓침: toInt(부분[12]) ?? 0, 메시지: toInt(부분[13]) ?? 0
}
}
// ===== data/market.tpz =====
// 달시장 데이터: 품목 · 결정론 난수(LCG, 산술만) · 시드 기반 손님 생성. 같은 시드 = 같은 밤(완전 결정론).
// moon-market data: goods, a deterministic LCG (arithmetic only), and seeded customer generation. Same seed = same night.
// 닫힌 시장 데이터 태그. 시드 문자열은 입력 경계라 string으로 둔다.
// Closed market data tags. Seed strings stay string at the input boundary.
export type 품목 = "떡" | "어묵" | "차"
export type 손님종류 = "학생" | "직장인" | "아주머니"
export type 성향 = "알뜰" | "급함" | "묶음"
export type 손님 = { 종류: 손님종류, 원함: 품목, 예산: int, 성향: 성향 }
let 품목들: Array<품목> = ["떡", "어묵", "차"]
export function 품목목록() -> Array<품목> { 품목들 }
let 종류들: Array<손님종류> = ["학생", "직장인", "아주머니"]
let 성향들: Array<성향> = ["알뜰", "급함", "묶음"]
// 시드 문자열 → 정수(스칼라 코드 합). / hash the seed string to an int.
export function 시드숫자(시드: string) -> int {
let mut h = 5381
let mut i = 0
let n = 시드.scalars().length
while i < n {
h = (h * 33 + (시드.codePointAt(i) ?? 0)) % 2147483647
i = i + 1
}
h
}
// 결정론 LCG: 다음 난수(0..2^31). 비트연산 없이 산술만. s<2^31 → s*1103515245 < 2.4e18 < i64 max(9.2e18), 안전. / deterministic LCG.
export function 난수다음(s: int) -> int { (s * 1103515245 + 12345) % 2147483648 }
// 시드·밤·인덱스로 결정론 손님 한 명. / a deterministic customer from (seed, night, index).
export function 손님생성(시드수: int, 밤: int, 인덱스: int) -> 손님 {
let mut r = 난수다음(시드수 + 밤 * 7919 + 인덱스 * 104729 + 1)
let 종 = 종류들[r % 3]
r = 난수다음(r)
let 원 = 품목들[r % 3]
r = 난수다음(r)
let 예산 = 8 + (r % 12)
r = 난수다음(r)
// 예전 인내 필드가 쓰던 난수 슬롯은 성향 순서를 바꾸지 않도록 그대로 소비한다.
// Keep consuming the old patience RNG slot so trait order remains byte-identical.
r = 난수다음(r)
{ 종류: 종, 원함: 원, 예산: 예산, 성향: 성향들[r % 3] }
}