// 비밀열차의 한 턴을 계산합니다. 셸이 보낸 패킷 하나를 받아 다음 상태와 그릴 목록을 돌려줍니다.
// 입력은 BT1 패킷의 STATE, EVENT, SHELL 줄이고, 출력은 BT1 머리, 저장 문자열을 담은 STATE 줄, DT1 그리기 목록을 담은 DRAW 줄입니다.
// 한 이벤트가 한 턴이며 행동, 흔들림, 경비 전진, 발각, 놓침 순으로 판정합니다. 같은 패킷이면 인터프리터와 웹 빌드에서 바이트까지 같은 출력을 냅니다.
// 화면 그리기, 기록 저장, 시연 재생은 하지 않습니다. 모두 페이지의 셸이 맡습니다.
// Secret train: one deterministic stealth turn per packet, returning the next save state and a draw list.

import core.state
import data.levels

let 기본시드 = "BT-0001"

// 공용 도우미 시작 (줄값 셸값)
// 여러 도구가 같은 원본을 씁니다. 여기서 고치지 않고 원본에서 고친 뒤 다시 생성합니다.

// 같은 접두의 줄이 여럿이면 마지막 줄을 씁니다.
function 줄값(입력: string, 접두: string) -> string {
  let 맞는줄들 = 입력.split("\n").map(줄 => 줄.trim()).filter(줄 => 줄.startsWith(접두))
  if 맞는줄들.length == 0 { return "" }
  let 끝줄 = 맞는줄들[맞는줄들.length - 1]
  끝줄.slice(접두.scalars().length, 끝줄.scalars().length).trim()
}

// "키=값,키=값" 에서 = 로 정확히 두 조각인 항만 읽고 키와 값의 앞뒤 공백을 지웁니다. 같은 키가 여럿이면 마지막 값을 씁니다.
function 셸값(셸: string, 키: string) -> string {
  let 값들 = 셸.split(",").map(항 => 항.split("=")).filter(조각들 => 조각들.length == 2 && 조각들[0].trim() == 키)
  if 값들.length == 0 { "" } else { 값들[값들.length - 1][1].trim() }
}

// 공용 도우미 끝

enum 이벤트 { 처음, 이어하기, 칸고르기(string), 이동(levels.방향), 줍기, 버티기, 모름 }
record 흔들림예고 { 방향: int, 남은턴: int }

// go 뒤에 레벨 코드가 오면 그 칸을 고릅니다. 모르는 토큰은 아무것도 하지 않고 한 턴을 보냅니다.
function 이벤트읽기(토큰: string) -> 이벤트 {
  if 토큰.startsWith("go") && levels.레벨코드인가(토큰.slice(2, 토큰.scalars().length)) {
    return 이벤트.칸고르기(토큰.slice(2, 토큰.scalars().length))
  }
  match 토큰 {
    case "init" => 이벤트.처음
    case "resume" => 이벤트.이어하기
    case "up" => 이벤트.이동("위")
    case "down" => 이벤트.이동("아래")
    case "left" => 이벤트.이동("왼쪽")
    case "right" => 이벤트.이동("오른쪽")
    case "a" => 이벤트.줍기
    case "b" => 이벤트.버티기
    case _ => 이벤트.모름
  }
}

function 플레이어칸인가(현재: state.상태, 목표: levels.칸) -> bool {
  현재.x == 목표.x && 현재.y == 목표.y
}

// 바닥으로만 움직이고 막히면 돌아서기만 합니다.
function 걷기(레벨: levels.레벨정보, 현재: state.상태, 방향값: levels.방향) -> state.상태 {
  let x = 현재.x + levels.가로이동(방향값)
  let y = 현재.y + levels.세로이동(방향값)
  if levels.바닥인가(레벨, x, y) { 현재{ x: x, y: y, 방향: 방향값 } } else { 현재{ 방향: 방향값 } }
}

// a 는 열쇠, 가방, 출구 순으로만 일이 됩니다.
function 줍거나탈출(레벨: levels.레벨정보, 현재: state.상태) -> state.상태 {
  if 플레이어칸인가(현재, 레벨.열쇠) && !현재.열쇠있음 {
    현재{ 열쇠있음: true, 안내: "열쇠" }
  } else if 플레이어칸인가(현재, 레벨.가방) && 현재.열쇠있음 && !현재.가방있음 {
    현재{ 가방있음: true, 안내: "가방" }
  } else if 플레이어칸인가(현재, 레벨.출구) && 현재.가방있음 {
    현재{ 결말: "탈출", 안내: "승리" }
  } else {
    현재
  }
}

// 탈출한 턴은 흔들림과 경비 판정 없이 다음 칸 처음으로 가고, 마지막 칸이면 그대로 끝납니다.
function 칸넘기기(현재: state.상태) -> state.상태 {
  let 다음 = levels.다음레벨코드(현재.레벨코드)
  if 다음 != "" { state.레벨시작상태(현재.시드, 다음, "다음칸") } else { 현재{ 턴: 현재.턴 + 1 } }
}

// 흔들림은 행동 전 턴 번호로 정하므로 셸이 미리 알린 것과 같습니다.
function 흔들기(레벨: levels.레벨정보, 현재: state.상태, 턴: int) -> state.상태 {
  let 흔들 = levels.흔들림방향(레벨, 턴)
  if 흔들 == 0 || 현재.버팀 { return 현재 }
  let x = 현재.x + 흔들
  if levels.바닥인가(레벨, x, 현재.y) { 현재{ x: x } } else { 현재 }
}

// 경비와 같은 칸이면 버텨도 잡히고, 시야는 버티면 피합니다.
function 발각인가(레벨: levels.레벨정보, 현재: state.상태) -> bool {
  let 잡은경비들 = levels.경비점들(레벨, 현재.경비단계들).filter(점 =>
    (점.x == 현재.x && 점.y == 현재.y)
      || (!현재.버팀 && levels.시야칸들(레벨, 점).filter(시야칸 => 플레이어칸인가(현재, 시야칸)).length > 0)
  )
  잡은경비들.length > 0
}

// 버팀과 안내는 그 턴에만 유효해 행동 전에 지웁니다.
function 적용(이벤트값: 이벤트, 직전: state.상태) -> state.상태 {
  if 직전.결말 != "진행" { return 직전 }
  let 레벨 = levels.레벨얻기(직전.레벨코드)
  let 대기 = 직전{ 버팀: false, 안내: "없음" }
  let 행동후 = match 이벤트값 {
    case 이동(방향값) => 걷기(레벨, 대기, 방향값)
    case 줍기 => 줍거나탈출(레벨, 대기)
    case 버티기 => if levels.좌석옆인가(레벨, 대기.x, 대기.y) { 대기{ 버팀: true, 안내: "버팀" } } else { 대기 }
    case 처음 | 이어하기 | 칸고르기(_) | 모름 => 대기
  }
  if 행동후.결말 == "탈출" { return 칸넘기기(행동후) }
  let 다음턴 = 흔들기(레벨, 행동후, 직전.턴){
    경비단계들: levels.다음단계들(레벨, 직전.경비단계들),
    턴: 직전.턴 + 1,
  }
  if 발각인가(레벨, 다음턴) { return 다음턴{ 결말: "발각", 안내: "발각" } }
  if 다음턴.턴 >= 직전.제한턴 { return 다음턴{ 결말: "놓침", 안내: "놓침" } }
  다음턴
}

// 찾는 즉시 멈춰야 큰 턴 값에서도 그 뒤 턴 번호를 더하지 않습니다.
function 다음흔들림(레벨: levels.레벨정보, 턴: int) -> 흔들림예고 {
  for k in 0..<(레벨.흔들주기 + 1) {
    let 방향 = levels.흔들림방향(레벨, 턴 + k)
    if 방향 != 0 { return 흔들림예고 { 방향: 방향, 남은턴: k } }
  }
  흔들림예고 { 방향: 0, 남은턴: 0 }
}

// 위험 칸과 다음 경비 위치는 한 턴 뒤의 순찰이라 셸이 다음 수를 미리 판정하게 합니다.
function 그리기목록(현재: state.상태) -> string {
  let 레벨 = levels.레벨얻기(현재.레벨코드)
  let 다음경비점들 = if 현재.결말 == "진행" { levels.경비점들(레벨, levels.다음단계들(레벨, 현재.경비단계들)) } else { [] }
  let 흔들림 = 다음흔들림(레벨, 현재.턴)
  let 버틸수있음 = state.일또는영(levels.좌석옆인가(레벨, 현재.x, 현재.y))
  let 줄묶음들: Array<Array<string>> = [
    [
      "DT1",
      "GRID|{레벨.너비}|{레벨.높이}|{levels.격자문자열(레벨)}",
      "PLAYER|{현재.x}|{현재.y}|{levels.방향번호(현재.방향)}|{state.일또는영(현재.가방있음)}|{state.일또는영(현재.버팀)}",
    ],
    levels.경비점들(레벨, 현재.경비단계들).map(점 => "GUARD|{점.x}|{점.y}|{levels.방향번호(점.방향)}"),
    if 현재.열쇠있음 { [] } else { ["ITEM|key|{레벨.열쇠.x}|{레벨.열쇠.y}"] },
    if 현재.가방있음 { [] } else { ["ITEM|case|{레벨.가방.x}|{레벨.가방.y}"] },
    ["ITEM|exit|{레벨.출구.x}|{레벨.출구.y}"],
    [for 점 in 다음경비점들 for 시야칸 in levels.시야칸들(레벨, 점) => "DANGER|{시야칸.x}|{시야칸.y}"],
    ["JOLT|{흔들림.방향}|{흔들림.남은턴}|{버틸수있음}"],
    다음경비점들.map(점 => "NEXTPOS|{점.x}|{점.y}"),
    ["HUD|{현재.턴}|{현재.제한턴}|{state.일또는영(현재.열쇠있음)}|{state.일또는영(현재.가방있음)}|{state.결말번호(현재.결말)}|{state.안내번호(현재.안내)}|{레벨.번호}"],
  ]
  [for 묶음 in 줄묶음들 for 줄 in 묶음 => 줄].join("\n")
}

function 처리(입력: string) -> string {
  let 이벤트값 = 줄값(입력, "EVENT:") |> 이벤트읽기
  let 셸시드 = 셸값(줄값(입력, "SHELL:"), "seed")
  let 시드 = if 셸시드 != "" { 셸시드 } else { 기본시드 }
  let 저장 = 줄값(입력, "STATE:")
  // 칸 선택은 기록 도전이라 그 칸 처음부터 시작합니다.
  let 새상태 = match 이벤트값 {
    case 처음 => state.기본상태(시드)
    case 이어하기 => state.역직렬화(저장, 시드)
    case 칸고르기(레벨코드) => state.레벨시작상태(시드, 레벨코드, "없음")
    case 이동(_) | 줍기 | 버티기 | 모름 => 적용(이벤트값, state.역직렬화(저장, 시드))
  }
  "BT1\nSTATE:{state.직렬화(새상태)}\nDRAW:{그리기목록(새상태)}"
}

input() |> 처리 |> print
