← 제니앱

달시장

손님이 원하는 물건을 적당한 값에 팔아 밤새 가장 많이 버는 결정론 야시장 경영 게임입니다. 같은 시드면 늘 같은 손님 줄입니다.

NIGHT MARKET · DETERMINISTIC달시장

◀▶ 품목 · ▲▼ 가격 · 응대 · 사입. 같은 시드면 늘 같은 손님입니다.

// ===== 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] }
}