clone

an offline sliding-block puzzle game in gleam and lustre, with an endless ladder of puzzles ranked by search

main
klot.ski / src / klotski / walk.gleam
walk.gleam 4.6K
1/// What a run was actually made of.2///3/// The trace is the distance to the goal after every step, starting with the4/// distance before any. A unit move changes that distance by exactly -1, 0 or5/// +1 — never more, because the distance field is a breadth-first one over6/// these very moves — so a finished run decomposes without any interpretation7/// at all: every step either closed the gap, opened it, or did nothing.8///9/// Two identities fall straight out, and they are what makes the numbers honest10/// rather than decorative:11///12/// descents = optimal + setbacks every step back is re-walked13/// steps = optimal + 2*setbacks + idle14///15/// so `wasted` is not a residual anybody has to trust. A setback costs two16/// steps and an idle move costs one, and those two numbers add up to the gap17/// between what a run took and what it needed, exactly, every time.18///19/// A `detour` is one excursion above the closest you had already been, from20/// leaving that mark to regaining it. It is not the same number as `setbacks`21/// and the difference is the whole reason to count both: two setbacks in a row22/// are one detour two deep, and on the chart they are one bump, not two. What a23/// person reads off the ridge is detours; what the arithmetic above accounts24/// for is setbacks.25import gleam/int26import gleam/list27import gleam/order2829pub type Stats {30 Stats(31 steps: Int,32 optimal: Int,33 setbacks: Int,34 idle: Int,35 descents: Int,36 detours: Int,37 wasted: Int,38 longest_run: Int,39 worst_detour: Int,40 first_setback: Int,41 )42}4344type Walking {45 Walking(46 setbacks: Int,47 idle: Int,48 descents: Int,49 detours: Int,50 run: Int,51 longest_run: Int,52 worst_detour: Int,53 first_setback: Int,54 best: Int,55 in_detour: Bool,56 index: Int,57 previous: Int,58 )59}6061pub fn trace_stats(trace: List(Int)) -> Stats {62 let optimal = trace |> list.first |> unwrap_or(0)63 case trace {64 [] -> empty()65 [_] -> Stats(..empty(), optimal:)66 [first, ..rest] -> {67 let state =68 list.fold(69 rest,70 Walking(71 setbacks: 0,72 idle: 0,73 descents: 0,74 detours: 0,75 run: 0,76 longest_run: 0,77 worst_detour: 0,78 first_setback: -1,79 best: first,80 in_detour: False,81 index: 0,82 previous: first,83 ),84 step,85 )86 Stats(87 steps: list.length(rest),88 optimal:,89 setbacks: state.setbacks,90 idle: state.idle,91 descents: state.descents,92 detours: state.detours,93 wasted: state.setbacks * 2 + state.idle,94 longest_run: state.longest_run,95 worst_detour: state.worst_detour,96 first_setback: state.first_setback,97 )98 }99 }100}101102fn step(state: Walking, distance: Int) -> Walking {103 let index = state.index + 1104 let delta = distance - state.previous105 let state = case int.compare(delta, 0) {106 order.Gt ->107 Walking(108 ..state,109 setbacks: state.setbacks + 1,110 run: 0,111 first_setback: case state.first_setback < 0 {112 True -> index113 False -> state.first_setback114 },115 )116 order.Lt -> {117 let run = state.run + 1118 Walking(119 ..state,120 descents: state.descents + 1,121 run:,122 longest_run: int.max(state.longest_run, run),123 )124 }125 order.Eq -> Walking(..state, idle: state.idle + 1, run: 0)126 }127128 // A detour is one excursion above the closest you had already been: it opens129 // the first step past that mark and closes when the mark is regained, so two130 // setbacks in a row are one detour two deep rather than two detours.131 let opened = distance > state.best && !state.in_detour132 let in_detour = case distance <= state.best {133 True -> False134 False -> state.in_detour || opened135 }136 // The mark moves before the depth is measured against it, so a step that sets137 // a new closest is zero deep rather than deep by however far it improved.138 let best = int.min(state.best, distance)139 Walking(140 ..state,141 detours: state.detours142 + case opened {143 True -> 1144 False -> 0145 },146 in_detour:,147 best:,148 worst_detour: int.max(state.worst_detour, distance - best),149 index:,150 previous: distance,151 )152}153154fn empty() -> Stats {155 Stats(156 steps: 0,157 optimal: 0,158 setbacks: 0,159 idle: 0,160 descents: 0,161 detours: 0,162 wasted: 0,163 longest_run: 0,164 worst_detour: 0,165 first_setback: -1,166 )167}168169fn unwrap_or(result: Result(a, b), fallback: a) -> a {170 case result {171 Ok(value) -> value172 Error(_) -> fallback173 }174}