15 of 27 modules failed `gleam format --check`, and the reason mattered: the packings in `families.gleam` were written as pictures of the board, and the formatter reflows a list of integers to fill the width. Conforming meant losing the one place where the source shows you the thing it describes. It does not reflow a list whose elements are themselves lists. So a packing is now a list of rows, each cell a one-letter constant — `e` empty, `s` single, `u` upright, `r` recumbent, `g` goal, which are the shape codes `domain` already encodes with, not a private numbering. Every cell is one character wide, so the columns line up; the rows survive the formatter because of their structure rather than in spite of it. `families.grid` flattens at the point of use, which is where everything downstream wanted it flat anyway. All nine packings were checked cell by cell against the previous revision and are identical, and each is exactly width times height cells. The rest is the formatter run over `src` and `test`. One real loss: it strips trailing zeros, so the weight column in `difficulty.gleam` reads 0.15, 0.4, 0.6, 0.1 and can no longer be aligned on the decimal point. Not worth the exemption, and there is no exemption to take.
- commit
- 619a0c3c7682e51c4f04586b984216d626ecba16
- parent
- 0c29377f001337d49c64f04831abb93387bd840a
- author
- Dami <dami.etoile@gmail.com>
| M | src/ | 150 | +++++++++++++++++------------- |
| M | src/ | 6 | +- |
| M | src/ | 9 | +- |
| M | src/ | 115 | ++++++++++++++--------- |
| M | src/ | 77 | ++++----------- |
| M | src/ | 46 | +++++---- |
| M | src/ | 83 | ++++++----------- |
| M | src/ | 3 | +- |
| M | src/ | 9 | +- |
| M | src/ | 17 | +-- |
| M | src/ | 26 | ++--- |
| M | src/ | 26 | ++---- |
| M | src/ | 9 | +- |
| M | test/ | 10 | +- |
| M | test/ | 5 | +- |
| M | test/ | 3 | +- |
@@ -356,7 +356,10 @@ fn update(model: Model, message: Message) -> #(Model, Effect(Message)) {
356356 PointerDown(x, y) -> pointer_down(model, x, y)357357 PointerMoved(x, y) -> pointer_moved(model, x, y)358358 PointerUp(x, y) -> pointer_up(model, x, y)359- PointerCancelled -> #(Model(..model, drag: None, scroll: None), effect.none())359+ PointerCancelled -> #(360+ Model(..model, drag: None, scroll: None),361+ effect.none(),362+ )360363 KeyPressed(key) -> key_pressed(model, key)361364 362365 FamilyOpened(index) -> open_family(model, families.at(index))@@ -364,7 +367,8 @@ fn update(model: Model, message: Message) -> #(Model, Effect(Message)) {
364367 Model(..model, sweep: Some(Sweep(family, count, total, phase))),365368 effect.none(),366369 )367- SurveyFinished(family, ecc, count) -> survey_finished(model, family, ecc, count)370+ SurveyFinished(family, ecc, count) ->371+ survey_finished(model, family, ecc, count)368372 PuzzleOffered(family, rung, candidates) ->369373 puzzle_offered(model, family, rung, candidates)370374 Measured(family, seq, distances) -> measured(model, family, seq, distances)@@ -427,7 +431,8 @@ fn update(model: Model, message: Message) -> #(Model, Effect(Message)) {
427431 DensityChosen(density) -> {428432 // Re-cuts every pixel the app owns: the backing stores are sized in buffer429433 // pixels and the sprite cache is baked at the old ones.430- let model = relayout(Model(..model, save: store.Save(..model.save, density:)))434+ let model =435+ relayout(Model(..model, save: store.Save(..model.save, density:)))431436 #(432437 model,433438 effect.batch([persist(model), render.clear_sprites(), repaint(model)]),@@ -475,10 +480,7 @@ fn tick(model: Model, now: Float) -> #(Model, Effect(Message)) {
475480 wipe: case model.wipe >=. 0.0 {476481 True -> {477482 let next = model.wipe +. dt *. 0.9478- case479- next480- >. int.to_float(int.max(model.layout.vw, model.layout.vh))481- {483+ case next >. int.to_float(int.max(model.layout.vw, model.layout.vh)) {482484 True -> -1.0483485 False -> next484486 }@@ -513,7 +515,10 @@ fn tick(model: Model, now: Float) -> #(Model, Effect(Message)) {
513515 card_step: case model.system.still {514516 True -> 4515517 False ->516- int.min(4, float.truncate(float.min(1.0, model.card_t /. 280.0) *. 5.0))518+ int.min(519+ 4,520+ float.truncate(float.min(1.0, model.card_t /. 280.0) *. 5.0),521+ )517522 },518523 )519524 @@ -606,7 +611,12 @@ fn advance_anim(model: Model, dt: Float) -> #(Model, Effect(Message)) {
606611 case a.then {607612 None -> #(Model(..model, anim: None), effect.none())608613 Some(p) ->609- perform_move(Model(..model, anim: None), p.piece, p.direction, p.cells)614+ perform_move(615+ Model(..model, anim: None),616+ p.piece,617+ p.direction,618+ p.cells,619+ )610620 }611621 }612622 }@@ -670,7 +680,7 @@ fn open_family(model: Model, family: Family) -> #(Model, Effect(Message)) {
670680 family.height,671681 family.goal_x,672682 family.goal_y,673- family.grid,683+ families.grid(family),674684 ),675685 paint(model),676686 ]),@@ -708,7 +718,8 @@ fn ask_for_puzzle(model: Model, family: Family, rung: Int) -> Effect(Message) {
708718 Some(sv) -> {709719 let want = progress.depth_for_rung(rung, sv.ecc)710720 let ceiling = want >= sv.ecc - 1711- let #(seed, _) = prng.next_int(prng.seeded_at(family.id, rung * 101), 1_000_000)721+ let #(seed, _) =722+ prng.next_int(prng.seeded_at(family.id, rung * 101), 1_000_000)712723 browser.request_puzzle(713724 family_id: family.id,714725 rung:,@@ -724,9 +735,9 @@ fn ask_for_puzzle(model: Model, family: Family, rung: Int) -> Effect(Message) {
724735 offset: prng.hash_string(family.id)725736 + rung726737 * case ceiling {727- True -> 5728- False -> 7729- },738+ True -> 5739+ False -> 7740+ },730741 seed:,731742 stray: difficulty.stray,732743 rollouts: difficulty.rollouts,@@ -798,7 +809,8 @@ fn puzzle_offered(
798809 case model.family.id == id && model.rung == rung && wants_card(model) {799810 False -> #(model, paint(model))800811 True -> {801- let model = Model(..model, puzzle: Some(puzzle), card_t: 0.0, card_step: 0)812+ let model =813+ Model(..model, puzzle: Some(puzzle), card_t: 0.0, card_step: 0)802814 go(model, Card)803815 }804816 }@@ -827,7 +839,11 @@ fn wants_card(model: Model) -> Bool {
827839 }828840 }829841 830-fn open_rung(model: Model, family: Family, rung: Int) -> #(Model, Effect(Message)) {842+fn open_rung(843+ model: Model,844+ family: Family,845+ rung: Int,846+) -> #(Model, Effect(Message)) {831847 let model = Model(..model, family:, rung:, puzzle: None)832848 #(model, ask_for_puzzle(model, family, rung))833849 }@@ -891,11 +907,14 @@ fn begin_play(model: Model) -> #(Model, Effect(Message)) {
891907 // last board is finished with, and a reload costs a player the wait and892908 // nothing else. Almost every time there is nothing waiting and this does893909 // nothing at all.894- #(model, effect.batch([895- browser.apply_update(model.family.id),896- page(model),897- paint(model),898- ]))910+ #(911+ model,912+ effect.batch([913+ browser.apply_update(model.family.id),914+ page(model),915+ paint(model),916+ ]),917+ )899918 }900919 }901920 }@@ -929,7 +948,8 @@ fn finish(model: Model) -> #(Model, Effect(Message)) {
929948 now: browser.now(),930949 today: browser.today(),931950 )932- let model = Model(..model, save:, screen: Won, finishing: None, walking: False)951+ let model =952+ Model(..model, save:, screen: Won, finishing: None, walking: False)933953 #(934954 model,935955 effect.batch([@@ -986,8 +1006,7 @@ fn perform_move(
9861006 browser.steps(moved, model.save.sound),9871007 browser.buzz([8], model.save.haptics),9881008 ])989- let measure =990- browser.request_distances(model.family.id, states, seq)1009+ let measure = browser.request_distances(model.family.id, states, seq)9911010 case solved {9921011 False -> #(model, effect.batch([sound, measure, paint(model)]))9931012 True -> {@@ -1099,7 +1118,10 @@ fn measured(
10991118 Model(11001119 ..model,11011120 awaiting:,1102- trace: list.append(model.trace, list.filter(distances, fn(d) { d >= 0 })),1121+ trace: list.append(1122+ model.trace,1123+ list.filter(distances, fn(d) { d >= 0 }),1124+ ),11031125 )11041126 #(model, effect.batch([keep_walking(model), paint(model)]))11051127 }@@ -1181,14 +1203,14 @@ fn keep_walking(model: Model) -> Effect(Message) {
11811203 fn piece_at(model: Model, x: Float, y: Float) -> Option(Piece) {11821204 let b = model.board11831205 let cell = b.cell1184- let cx = float.truncate({ x -. int.to_float(b.board.x) } /. int.to_float(cell))1206+ let cx =1207+ float.truncate({ x -. int.to_float(b.board.x) } /. int.to_float(cell))11851208 let cy =1186- float.truncate({ y -. int.to_float(b.board.y - model.rise) } /. int.to_float(cell))1209+ float.truncate(1210+ { y -. int.to_float(b.board.y - model.rise) } /. int.to_float(cell),1211+ )11871212 case1188- cx < 01189- || cy < 01190- || cx >= model.family.width1191- || cy >= model.family.height1213+ cx < 0 || cy < 0 || cx >= model.family.width || cy >= model.family.height11921214 {11931215 True -> None11941216 False ->@@ -1218,10 +1240,7 @@ fn pointer_down(model: Model, x: Float, y: Float) -> #(Model, Effect(Message)) {
12181240 case piece_at(model, x, y) {12191241 None -> #(model, effect.none())12201242 Some(piece) -> #(1221- Model(1222- ..model,1223- drag: Some(Drag(piece.id, x, y, None, 0, 0, 0)),1224- ),1243+ Model(..model, drag: Some(Drag(piece.id, x, y, None, 0, 0, 0))),12251244 paint(model),12261245 )12271246 }@@ -1230,14 +1249,22 @@ fn pointer_down(model: Model, x: Float, y: Float) -> #(Model, Effect(Message)) {
12301249 }12311250 }12321251 1233-fn pointer_moved(model: Model, x: Float, y: Float) -> #(Model, Effect(Message)) {1252+fn pointer_moved(1253+ model: Model,1254+ x: Float,1255+ y: Float,1256+) -> #(Model, Effect(Message)) {12341257 case model.scroll, model.drag {12351258 Some(#(origin, from, moved)), _ -> {12361259 let dy = y -. origin12371260 let model =12381261 Model(12391262 ..model,1240- scroll: Some(#(origin, from, float.max(moved, float.absolute_value(dy)))),1263+ scroll: Some(#(1264+ origin,1265+ from,1266+ float.max(moved, float.absolute_value(dy)),1267+ )),12411268 atlas_scroll: clamp_scroll(model, from + float.round(dy)),12421269 )12431270 #(model, paint(model))@@ -1247,7 +1274,9 @@ fn pointer_moved(model: Model, x: Float, y: Float) -> #(Model, Effect(Message))
12471274 let dy = y -. drag.origin_y12481275 case drag.axis {12491276 None ->1250- case float.absolute_value(dx) <. 3.0 && float.absolute_value(dy) <. 3.0 {1277+ case1278+ float.absolute_value(dx) <. 3.0 && float.absolute_value(dy) <. 3.01279+ {12511280 True -> #(model, effect.none())12521281 False -> begin_axis(model, drag, dx, dy)12531282 }@@ -1264,10 +1293,7 @@ fn pointer_moved(model: Model, x: Float, y: Float) -> #(Model, Effect(Message))
12641293 let offset =12651294 float.round(clamped /. int.to_float(shading.period))12661295 * shading.period1267- #(1268- Model(..model, drag: Some(Drag(..drag, offset:))),1269- paint(model),1270- )1296+ #(Model(..model, drag: Some(Drag(..drag, offset:))), paint(model))12711297 }12721298 }12731299 }@@ -1317,8 +1343,7 @@ fn begin_axis(
13171343 fn clamp_scroll(model: Model, y: Int) -> Int {13181344 let g = paint.atlas_geometry(model.layout, families.count())13191345 let over =1320- paint.atlas_content_height(g)1321- - { model.layout.vh - model.layout.bar_h - 6 }1346+ paint.atlas_content_height(g) - { model.layout.vh - model.layout.bar_h - 6 }13221347 case over <= 0 {13231348 True -> 013241349 False -> int.max(-over, int.min(0, y))@@ -1369,8 +1394,7 @@ fn end_drag(model: Model, drag: Drag) -> #(Model, Effect(Message)) {
13691394 _ -> #(model, paint(model))13701395 }13711396 Some(axis) -> {1372- let cells =1373- float.round(int.to_float(drag.offset) /. int.to_float(cell))1397+ let cells = float.round(int.to_float(drag.offset) /. int.to_float(cell))13741398 case cells {13751399 0 ->13761400 animate(model, drag.piece, axis, int.to_float(drag.offset), 0.0, None)@@ -1478,8 +1502,7 @@ fn key_direction(key: String) -> Option(Direction) {
14781502 /// interchangeable with something the same shape.14791503 fn arrow_move(model: Model, direction: Direction) -> #(Model, Effect(Message)) {14801504 let board = families.board(model.family)1481- let goal =1482- model.pieces |> list.find(fn(p) { p.shape == domain.Goal })1505+ let goal = model.pieces |> list.find(fn(p) { p.shape == domain.Goal })14831506 case goal {14841507 Error(_) -> #(model, effect.none())14851508 Ok(piece) ->@@ -1620,8 +1643,7 @@ fn ops_for(
16201643 case model.screen {16211644 Boot -> paint.boot(model.layout, families.at(0), screen, t)16221645 Sweeping -> {1623- let sweep =1624- model.sweep |> option.unwrap(Sweep(model.family.id, 0, 1, 1))1646+ let sweep = model.sweep |> option.unwrap(Sweep(model.family.id, 0, 1, 1))16251647 paint.survey(16261648 model.layout,16271649 model.family.label,@@ -1765,13 +1787,14 @@ fn atlas_rows(model: Model, t: Theme) -> List(paint.Row) {
17651787 Some(s) -> s.ecc17661788 None -> 017671789 }1768- let combo = theme.apply_pin(theme.combo_for(t, family.id, rung), model.save.shading)1790+ let combo =1791+ theme.apply_pin(theme.combo_for(t, family.id, rung), model.save.shading)17691792 paint.Row(17701793 family:,17711794 rung:,17721795 grid: case preview {17731796 Some(p) -> p.grid1774- None -> family.grid1797+ None -> families.grid(family)17751798 },17761799 surveyed: option.is_some(survey) && option.is_some(preview),17771800 optimal: case preview {@@ -1806,13 +1829,10 @@ fn view(model: Model) -> Element(Message) {
18061829 html.div(18071830 [18081831 attribute.id("stage"),1809- attribute.attribute(1810- "data-mode",1811- case l.mode == Split {1812- True -> "split"1813- False -> "stack"1814- },1815- ),1832+ attribute.attribute("data-mode", case l.mode == Split {1833+ True -> "split"1834+ False -> "stack"1835+ }),18161836 attribute.styles([18171837 #("left", px(int.to_float(l.off_x))),18181838 #("top", px(int.to_float(l.off_y))),@@ -1941,10 +1961,7 @@ fn chrome(model: Model) -> List(Element(Message)) {
19411961 zone(19421962 model,19431963 "gate",1944- int.min(1945- w.y + float.round(int.to_float(w.h) *. 0.85),1946- w.y + w.h - 24,1947- ),1964+ int.min(w.y + float.round(int.to_float(w.h) *. 0.85), w.y + w.h - 24),19481965 20,19491966 column,19501967 [@@ -2011,10 +2028,9 @@ fn menu_zone(model: Model) -> Element(Message) {
20112028 case on_atlas {20122029 True -> html.span([attribute.class("spacer")], [])20132030 False ->2014- html.button(2015- [attribute.class("ghost"), event.on_click(BackPressed)],2016- [html.text("BACK")],2017- )2031+ html.button([attribute.class("ghost"), event.on_click(BackPressed)], [2032+ html.text("BACK"),2033+ ])20182034 },20192035 html.span([attribute.class("spacer")], []),20202036 case on_atlas && model.install_available {@@ -318,11 +318,7 @@ pub fn request_puzzle(
318318 }319319 320320 @external(javascript, "./browser_ffi.mjs", "requestDistances")321-fn request_distances_ffi(322- family_id: String,323- states: List(Int),324- seq: Int,325-) -> Nil321+fn request_distances_ffi(family_id: String, states: List(Int), seq: Int) -> Nil326322 327323 /// The exact distance of every state a move passed through, in order, so the328324 /// trace the record screen draws is measured rather than inferred.@@ -56,15 +56,14 @@ pub fn score(
5656 retreats retreats: Int,5757 branch branch: Float,5858 ) -> Float {59- let retreat_rate =60- int.to_float(retreats) /. int.to_float(int.max(1, optimal))59+ let retreat_rate = int.to_float(retreats) /. int.to_float(int.max(1, optimal))6160 0.156261 *. log1p(int.to_float(optimal))63- +. 0.4062+ +. 0.46463 *. log1p(wander)65- +. 0.6064+ +. 0.66665 *. retreat_rate67- +. 0.1066+ +. 0.16867 *. log1p(branch)6968 }7069 @@ -23,6 +23,23 @@ import gleam/list
2323 import gleam/result2424 import klotski/domain.{type Board, Board}2525 26+/// A cell, one letter wide so that a packing below stays a picture of itself.27+/// The values are the shape codes `domain` encodes with, not a private28+/// numbering, so a grid can be read straight into `pieces_from_grid`.29+///30+/// One letter rather than a word because every cell has to be the same width or31+/// the rows stop lining up, and a packing that does not line up is a list of32+/// numbers again. `e` empty, `s` single, `u` upright, `r` recumbent, `g` goal.33+const e = 034+35+const s = 136+37+const u = 238+39+const r = 340+41+const g = 442+2643 pub type Family {2744 Family(2845 id: String,@@ -31,8 +48,11 @@ pub type Family {
3148 height: Int,3249 goal_x: Int,3350 goal_y: Int,34- /// The solved packing: 0 empty, 1 single, 2 upright, 3 recumbent, 4 goal.35- grid: List(Int),51+ /// The solved packing, a row at a time. Nested rather than flat because the52+ /// formatter keeps a list of lists one element to a line and reflows a list53+ /// of integers to fill the width — so the shape survives `gleam format`54+ /// here and did not before.55+ rows: List(List(Int)),3656 )3757 }3858 @@ -40,6 +60,13 @@ pub fn board(family: Family) -> Board {
4060 Board(family.width, family.height, family.goal_x, family.goal_y)4161 }4262 63+/// The packing flat, which is how everything downstream of here wants it: the64+/// worker, the encoder and `pieces_from_grid` all read a board in row-major65+/// order and none of them cares where the rows ended.66+pub fn grid(family: Family) -> List(Int) {67+ list.flatten(family.rows)68+}69+4370 /// Classic leads because it is what the word on the title screen promises. The4471 /// rest are ordered by ceiling, so the list past it is a ramp: 24 steps of4572 /// depth at the top, 178 at the bottom.@@ -48,62 +75,62 @@ pub fn all() -> List(Family) {
4875 // The traditional packing, and the only family whose own arrangement is a4976 // landmark in its own right: 116 unit steps from home.5077 Family("classic", "CLASSIC", 4, 5, 1, 3, [51- 2, 4, 4, 2,52- 2, 4, 4, 2,53- 2, 3, 3, 2,54- 2, 1, 1, 2,55- 1, 0, 0, 1,78+ [u, g, g, u],79+ [u, g, g, u],80+ [u, r, r, u],81+ [u, s, s, u],82+ [s, e, e, s],5683 ]),5784 Family("narrow", "NARROW", 3, 5, 0, 3, [58- 3, 3, 1,59- 1, 1, 1,60- 1, 1, 1,61- 4, 4, 0,62- 4, 4, 0,85+ [r, r, s],86+ [s, s, s],87+ [s, s, s],88+ [g, g, e],89+ [g, g, e],6390 ]),6491 Family("compact", "COMPACT", 4, 4, 1, 2, [65- 3, 3, 3, 3,66- 1, 1, 1, 1,67- 0, 4, 4, 0,68- 0, 4, 4, 0,92+ [r, r, r, r],93+ [s, s, s, s],94+ [e, g, g, e],95+ [e, g, g, e],6996 ]),7097 // Four cells of air: it gives, and keeps giving. A wide state space with a7198 // low ceiling, which is a different kind of easy from a small one.7299 Family("span", "SPAN", 5, 4, 1, 2, [73- 2, 2, 2, 2, 1,74- 2, 2, 2, 2, 1,75- 0, 4, 4, 0, 1,76- 0, 4, 4, 0, 1,100+ [u, u, u, u, s],101+ [u, u, u, u, s],102+ [e, g, g, e, s],103+ [e, g, g, e, s],77104 ]),78105 Family("wide", "WIDE", 4, 5, 1, 3, [79- 3, 3, 3, 3,80- 3, 3, 3, 3,81- 1, 1, 1, 1,82- 1, 4, 4, 1,83- 0, 4, 4, 0,106+ [r, r, r, r],107+ [r, r, r, r],108+ [s, s, s, s],109+ [s, g, g, s],110+ [e, g, g, e],84111 ]),85112 Family("yard", "YARD", 5, 4, 2, 2, [86- 2, 3, 3, 3, 3,87- 2, 3, 3, 3, 3,88- 1, 1, 4, 4, 0,89- 1, 1, 4, 4, 0,113+ [u, r, r, r, r],114+ [u, r, r, r, r],115+ [s, s, g, g, e],116+ [s, s, g, g, e],90117 ]),91118 // The same pieces as MIXED, leaving by the corner instead of the middle.92119 // One goal cell moved, and the distance field is a different shape — the93120 // cheapest way there is to get a genuinely new family.94121 Family("corner", "CORNER", 4, 5, 0, 3, [95- 3, 3, 3, 3,96- 2, 3, 3, 2,97- 2, 1, 1, 2,98- 4, 4, 1, 1,99- 4, 4, 0, 0,122+ [r, r, r, r],123+ [u, r, r, u],124+ [u, s, s, u],125+ [g, g, s, s],126+ [g, g, e, e],100127 ]),101128 Family("mixed", "MIXED", 4, 5, 1, 3, [102- 3, 3, 3, 3,103- 2, 3, 3, 2,104- 2, 1, 1, 2,105- 1, 4, 4, 1,106- 0, 4, 4, 0,129+ [r, r, r, r],130+ [u, r, r, u],131+ [u, s, s, u],132+ [s, g, g, s],133+ [e, g, g, e],107134 ]),108135 ]109136 }@@ -134,10 +161,10 @@ pub fn index_of(id: String) -> Int {
134161 135162 fn fallback() -> Family {136163 Family("classic", "CLASSIC", 4, 5, 1, 3, [137- 2, 4, 4, 2,138- 2, 4, 4, 2,139- 2, 3, 3, 2,140- 2, 1, 1, 2,141- 1, 0, 0, 1,164+ [u, g, g, u],165+ [u, g, g, u],166+ [u, r, r, u],167+ [u, s, s, u],168+ [s, e, e, s],142169 ])143170 }@@ -63,94 +63,59 @@ pub fn tile(backdrop: Backdrop) -> List(Int) {
6363 // The original, preserved exactly: rows every 4px, alternate rows offset6464 // by 2. One pixel in sixteen.6565 Lattice -> [66- 1, 0, 0, 0, 1, 0, 0, 0,67- 0, 0, 0, 0, 0, 0, 0, 0,68- 0, 0, 0, 0, 0, 0, 0, 0,69- 0, 0, 0, 0, 0, 0, 0, 0,70- 0, 0, 1, 0, 0, 0, 1, 0,71- 0, 0, 0, 0, 0, 0, 0, 0,72- 0, 0, 0, 0, 0, 0, 0, 0,73- 0, 0, 0, 0, 0, 0, 0, 0,66+ 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,67+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,68+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,7469 ]7570 7671 // Two dots in sixty-four. Almost nothing — which is the point on a dark7772 // theme, where the ground reads as a field of stars rather than a texture.7873 Sparse -> [79- 1, 0, 0, 0, 0, 0, 0, 0,80- 0, 0, 0, 0, 0, 0, 0, 0,81- 0, 0, 0, 0, 0, 0, 0, 0,82- 0, 0, 0, 0, 0, 0, 0, 0,83- 0, 0, 0, 0, 1, 0, 0, 0,84- 0, 0, 0, 0, 0, 0, 0, 0,85- 0, 0, 0, 0, 0, 0, 0, 0,86- 0, 0, 0, 0, 0, 0, 0, 0,74+ 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,75+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,76+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,8777 ]8878 8979 // A single diagonal thread. The only pattern with a direction in it, which9080 // is exactly why there is one and not two: crossing it with its mirror9181 // doubles the density and reads as a mesh rather than a weave.9282 Twill -> [93- 1, 0, 0, 0, 0, 0, 0, 0,94- 0, 0, 0, 0, 0, 0, 0, 1,95- 0, 0, 0, 0, 0, 0, 1, 0,96- 0, 0, 0, 0, 0, 1, 0, 0,97- 0, 0, 0, 0, 1, 0, 0, 0,98- 0, 0, 0, 1, 0, 0, 0, 0,99- 0, 0, 1, 0, 0, 0, 0, 0,100- 0, 1, 0, 0, 0, 0, 0, 0,83+ 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0,84+ 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0,85+ 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0,10186 ]10287 10388 // Dotted rules every 4px. A solid scanline was tried and it stripes the10489 // screen; broken in half it reads as ruled paper.10590 Rule -> [106- 1, 0, 1, 0, 1, 0, 1, 0,107- 0, 0, 0, 0, 0, 0, 0, 0,108- 0, 0, 0, 0, 0, 0, 0, 0,109- 0, 0, 0, 0, 0, 0, 0, 0,110- 1, 0, 1, 0, 1, 0, 1, 0,111- 0, 0, 0, 0, 0, 0, 0, 0,112- 0, 0, 0, 0, 0, 0, 0, 0,113- 0, 0, 0, 0, 0, 0, 0, 0,91+ 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,92+ 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,93+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,11494 ]11595 11696 // Graph paper: one dotted rule each way per tile.11797 Graph -> [118- 1, 0, 1, 0, 1, 0, 1, 0,119- 0, 0, 0, 0, 0, 0, 0, 0,120- 1, 0, 0, 0, 0, 0, 0, 0,121- 0, 0, 0, 0, 0, 0, 0, 0,122- 1, 0, 0, 0, 0, 0, 0, 0,123- 0, 0, 0, 0, 0, 0, 0, 0,124- 1, 0, 0, 0, 0, 0, 0, 0,125- 0, 0, 0, 0, 0, 0, 0, 0,98+ 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0,99+ 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0,100+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,126101 ]127102 128103 // Clustered dots — the newsprint ground, and the backdrop that belongs129104 // with the Ben-Day block screen. Clustered rather than dispersed is the130105 // whole difference between a halftone and a dither.131106 Benday -> [132- 0, 0, 0, 0, 0, 0, 0, 0,133- 0, 1, 1, 0, 0, 0, 0, 0,134- 0, 1, 1, 0, 0, 0, 0, 0,135- 0, 0, 0, 0, 0, 0, 0, 0,136- 0, 0, 0, 0, 0, 0, 0, 0,137- 0, 0, 0, 0, 0, 1, 1, 0,138- 0, 0, 0, 0, 0, 1, 1, 0,139- 0, 0, 0, 0, 0, 0, 0, 0,107+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0,108+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0,109+ 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0,140110 ]141111 142112 // No pattern at all. Not an omission — a flat ground is a real member of143113 // the family, and it is the only one that lets a pure-black theme actually144114 // reach pure black across the whole screen.145115 Plain -> [146- 0, 0, 0, 0, 0, 0, 0, 0,147- 0, 0, 0, 0, 0, 0, 0, 0,148- 0, 0, 0, 0, 0, 0, 0, 0,149- 0, 0, 0, 0, 0, 0, 0, 0,150- 0, 0, 0, 0, 0, 0, 0, 0,151- 0, 0, 0, 0, 0, 0, 0, 0,152- 0, 0, 0, 0, 0, 0, 0, 0,153- 0, 0, 0, 0, 0, 0, 0, 0,116+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,117+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,118+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,154119 ]155120 }156121 }@@ -150,8 +150,10 @@ pub fn plate_h(cell: Int, board_h: Int) -> Int {
150150 round(151151 int.to_float(cell * board_h)152152 +. int.to_float(plate_pad(cell) * 2)153- +. int.to_float(cell) *. 0.5154- +. int.to_float(cell) *. 1.05,153+ +. int.to_float(cell)154+ *. 0.5155+ +. int.to_float(cell)156+ *. 1.05,155157 )156158 }157159 @@ -253,10 +255,13 @@ fn metrics_for(mode: Mode, scale: Float, box: Box) -> Layout {
253255 let bar_h =254256 int.max(255257 18,256- round(int.to_float(vh) *. case split {257- True -> 0.07258- False -> 0.048259- }),258+ round(259+ int.to_float(vh)260+ *. case split {261+ True -> 0.07262+ False -> 0.048263+ },264+ ),260265 )261266 262267 // The head and the foot are fractions with floors under them. The fractions@@ -265,11 +270,10 @@ fn metrics_for(mode: Mode, scale: Float, box: Box) -> Layout {
265270 // posting the actions row off the bottom of the screen. The foot is the266271 // gauge, the readout, the buttons and the air between them, so it is stated267272 // as exactly that rather than as a fraction that happens to work out.268- let hud_gap =269- int.min(46, int.max(26, round(int.to_float(vh) *. 0.095)))273+ let hud_gap = int.min(46, int.max(26, round(int.to_float(vh) *. 0.095)))270274 let head_h = int.max(bar_h + 10, round(int.to_float(vh) *. 0.14))271275 let foot_h =272- int.max(hud_gap + 30 + 10 + 34 + 4, round(int.to_float(vh) *. 0.30))276+ int.max(hud_gap + 30 + 10 + 34 + 4, round(int.to_float(vh) *. 0.3))273277 274278 Layout(275279 mode:,@@ -482,7 +486,8 @@ pub fn board_layout(layout: Layout, family: Family) -> BoardLayout {
482486 True -> pw + pair_gap + panel_w483487 False -> pw484488 }485- let plate_x = round({ int.to_float(layout.vw) -. int.to_float(comp_w) } /. 2.0)489+ let plate_x =490+ round({ int.to_float(layout.vw) -. int.to_float(comp_w) } /. 2.0)486491 487492 // Stacked, the plate takes the head it is owed and then centres itself in488493 // whatever is left. On a tall phone that is the same 14% of the height it has@@ -497,13 +502,13 @@ pub fn board_layout(layout: Layout, family: Family) -> BoardLayout {
497502 layout.bar_h + 6,498503 round(499504 int.to_float(layout.bar_h)500- +. { int.to_float(layout.vh - layout.bar_h - ph) } /. 2.0,505+ +. { int.to_float(layout.vh - layout.bar_h - ph) }506+ /. 2.0,501507 ),502508 )503509 False ->504510 round(505- int.to_float(head)506- +. int.to_float(int.max(0, head_room - head)) /. 2.0,511+ int.to_float(head) +. int.to_float(int.max(0, head_room - head)) /. 2.0,507512 )508513 }509514 @@ -522,9 +527,7 @@ pub fn board_layout(layout: Layout, family: Family) -> BoardLayout {
522527 False -> 0523528 }524529 let group_y = case split {525- True ->526- plate_y527- + int.max(4, round(int.to_float(ph - group_h) /. 2.0))530+ True -> plate_y + int.max(4, round(int.to_float(ph - group_h) /. 2.0))528531 False -> 0529532 }530533 @@ -534,7 +537,13 @@ pub fn board_layout(layout: Layout, family: Family) -> BoardLayout {
534537 }535538 let panel = case split {536539 True ->537- Panel(x: plate_x + pw + pair_gap, y: plate_y, w: panel_w, h: ph, actions_y:)540+ Panel(541+ x: plate_x + pw + pair_gap,542+ y: plate_y,543+ w: panel_w,544+ h: ph,545+ actions_y:,546+ )538547 False -> {539548 let y = foot + round(int.to_float(layout.vh) *. 0.02)540549 Panel(@@ -556,8 +565,7 @@ pub fn board_layout(layout: Layout, family: Family) -> BoardLayout {
556565 let gauge_y = case split {557566 True -> group_y558567 False ->559- foot560- + int.min(22, int.max(12, round(int.to_float(layout.vh) *. 0.045)))568+ foot + int.min(22, int.max(12, round(int.to_float(layout.vh) *. 0.045)))561569 }562570 563571 BoardLayout(@@ -13,15 +13,13 @@
1313 import gleam/float1414 import gleam/int1515 import gleam/list16+import gleam/option1617 import gleam/result1718 import gleam/string1819 import klotski/domain1920 import klotski/families.{type Family}20-import klotski/layout.{21- type BoardLayout, type Layout, type Rect, Rect, Split,22-}21+import klotski/layout.{type BoardLayout, type Layout, type Rect, Rect, Split}2322 import klotski/progress.{type Level}24-import gleam/option2523 import klotski/scene.{2624 type BlockStyle, type Op, type Screen, Block, BlockStyle, Center, Clip,2725 DevLogo, Display, End, Fill, GoldLine, Label, Screen, Start, Text, Trophy,@@ -201,13 +199,9 @@ pub fn boot(l: Layout, hero: Family, screen: Screen, theme: Theme) -> List(Op) {
201199 // The specimen takes what is left under the rule rather than its own fraction202200 // of the window, which is the same trap the title fell into.203201 let top = rule_y + float.round(int.to_float(win.h) *. 0.06)204- let room =205- win.y + win.h - top - float.round(int.to_float(win.h) *. 0.22)202+ let room = win.y + win.h - top - float.round(int.to_float(win.h) *. 0.22)206203 let cell =207- int.max(208- 8,209- int.min({ win.w - 52 } / hero.width, room / hero.height - 2),210- )204+ int.max(8, int.min({ win.w - 52 } / hero.width, room / hero.height - 2))211205 let tp = int.max(3, float.round(int.to_float(cell) *. 0.16))212206 let bx = float.round(cx -. int.to_float(cell * hero.width) /. 2.0)213207 let by = top + tp@@ -256,7 +250,7 @@ pub fn boot(l: Layout, hero: Family, screen: Screen, theme: Theme) -> List(Op) {
256250 // on the goal block. It is the same object; it should not be a lookalike.257251 grid_blocks(258252 hero,259- hero.grid,253+ families.grid(hero),260254 bx,261255 by,262256 cell,@@ -285,7 +279,7 @@ pub fn survey(
285279 286280 let bar_w = float.round(u *. 0.5)287281 let bar_x = float.round(cx -. int.to_float(bar_w) /. 2.0)288- let bar_y = float.round(vh *. 0.70)282+ let bar_y = float.round(vh *. 0.7)289283 // The sweep still counts up under reduced motion, because that is a progress290284 // bar and hiding it would leave a still screen during a second and a half of291285 // work. It was never motion for its own sake.@@ -502,8 +496,7 @@ fn atlas_lines(g: AtlasGeometry, r: Rect) -> Lines {
502496 let air = int.to_float(r.h - 10) -. content503497 let lead = float.max(2.0, float.min(air /. 3.0, g.name_size *. 0.6))504498 let top =505- int.to_float(r.y)506- +. { int.to_float(r.h) -. content -. 3.0 *. lead } /. 2.0499+ int.to_float(r.y) +. { int.to_float(r.h) -. content -. 3.0 *. lead } /. 2.0507500 508501 let stat_top = top +. name_h +. lead509502 let marks_top = stat_top +. stat_h +. lead@@ -576,9 +569,7 @@ pub fn atlas(
576569 Label(577570 text: brand,578571 x: int.to_float(8 + logo_size + 6),579- y: int.to_float(580- l.bar_h - float.round(int.to_float(l.bar_h) *. 0.28),581- ),572+ y: int.to_float(l.bar_h - float.round(int.to_float(l.bar_h) *. 0.28)),582573 size: int.to_float(l.bar_h) *. 0.5,583574 face: Display,584575 track: int.to_float(l.bar_h) *. 0.08,@@ -601,10 +592,7 @@ fn atlas_row(
601592 let family = row.family602593 let inner = r.h - 12603594 let mini =604- int.max(605- 4,606- int.min(g.board_col / family.width, inner / family.height),607- )595+ int.max(4, int.min(g.board_col / family.width, inner / family.height))608596 // Centred in its fixed column, so boards of different shapes still line up.609597 let bx = r.x + 8 + { g.board_col - mini * family.width } / 2610598 let by = r.y + { r.h - mini * family.height } / 2@@ -637,7 +625,8 @@ fn atlas_row(
637625 text: "TAP TO BEGIN",638626 x: int.to_float(tx),639627 y: int.to_float(lines.meter)640- +. typeface.cap_height(g.foot_size, Text) /. 2.0,628+ +. typeface.cap_height(g.foot_size, Text)629+ /. 2.0,641630 size: g.foot_size,642631 face: Text,643632 track: u *. 0.004,@@ -656,19 +645,14 @@ fn atlas_row(
656645 // and a fixed seventy-two units had them fighting over the same space.657646 let foot_w = typeface.measure(foot, g.foot_size, Text, 0.0)658647 let bar_w =659- int.max(660- g.mark * 4,661- g.text_w - float.round(foot_w) - int.max(4, g.mark),662- )648+ int.max(g.mark * 4, g.text_w - float.round(foot_w) - int.max(4, g.mark))663649 let bar_y = lines.meter664650 let stop = g.tick665651 // How much of this family's depth you have walked. It fills as the rungs666652 // climb and stops when the ladder reaches the family's ceiling — which is667653 // the honest end of "deeper", not the end of the family.668654 let walked =669- clamp01(670- int.to_float(row.depth) /. int.to_float(int.max(1, row.ecc)),671- )655+ clamp01(int.to_float(row.depth) /. int.to_float(int.max(1, row.ecc)))672656 let head = int.max(2, g.mark - 1)673657 list.flatten([674658 [@@ -699,7 +683,8 @@ fn atlas_row(
699683 text: foot,700684 x: int.to_float(tx + bar_w + int.max(4, g.mark)),701685 y: int.to_float(bar_y)702- +. typeface.cap_height(g.foot_size, Text) /. 2.0,686+ +. typeface.cap_height(g.foot_size, Text)687+ /. 2.0,703688 size: g.foot_size,704689 face: Text,705690 track: 0.0,@@ -862,12 +847,7 @@ pub fn play(
862847 let by = b.board.y - rise863848 let tp = b.tray_pad864849 let tray =865- Rect(866- x: bx - tp,867- y: by - tp,868- w: b.board.w + tp * 2,869- h: b.board.h + tp * 2,870- )850+ Rect(x: bx - tp, y: by - tp, w: b.board.w + tp * 2, h: b.board.h + tp * 2)871851 872852 let placed =873853 list.map(blocks, fn(p) {@@ -919,9 +899,7 @@ pub fn play(
919899 text: family.label,920900 x: int.to_float(plate.x) +. int.to_float(plate.w) /. 2.0,921901 y: int.to_float(922- plate.y923- + plate.h924- - float.round(int.to_float(b.pad_bottom) *. 0.34),902+ plate.y + plate.h - float.round(int.to_float(b.pad_bottom) *. 0.34),925903 ),926904 size: int.to_float(cell) *. 0.44,927905 face: Display,@@ -972,8 +950,7 @@ fn gauge(
972950 let x = b.gauge.x973951 let k =974952 clamp01(975- 1.0976- -. int.to_float(distance) /. int.to_float(int.max(1, reference)),953+ 1.0 -. int.to_float(distance) /. int.to_float(int.max(1, reference)),977954 )978955 list.flatten([979956 [scene.rule(x, y, w, 1)],@@ -1039,10 +1016,7 @@ pub fn won(
10391016 let gy = solved_y + float.round(u *. 0.13)10401017 // Room for the baseline rule and a breath under it before the caption's caps.10411018 let gh =1042- int.max(1043- 24,1044- cap_y - float.round(cap_size) - float.round(u *. 0.075) - gy,1045- )1019+ int.max(24, cap_y - float.round(cap_size) - float.round(u *. 0.075) - gy)10461020 10471021 let n = int.max(1, list.length(trace))10481022 let start = int.max(1, trace |> list.first |> result.unwrap(1))@@ -1246,9 +1220,7 @@ pub fn stats(
12461220 Label(12471221 text: "RECORD",12481222 x: int.to_float(l.vw) /. 2.0,1249- y: int.to_float(1250- l.bar_h - float.round(int.to_float(l.bar_h) *. 0.28),1251- ),1223+ y: int.to_float(l.bar_h - float.round(int.to_float(l.bar_h) *. 0.28)),12521224 size: int.to_float(l.bar_h) *. 0.5,12531225 face: Display,12541226 track: int.to_float(l.bar_h) *. 0.08,@@ -1362,7 +1334,8 @@ fn lifetime_panel(
13621334 ) -> #(List(Op), Int) {13631335 let efficiency = case totals.steps {13641336 0 -> 01365- steps -> float.round(int.to_float(totals.optimal * 100) /. int.to_float(steps))1337+ steps ->1338+ float.round(int.to_float(totals.optimal * 100) /. int.to_float(steps))13661339 }13671340 let cells = [13681341 #(int.to_string(totals.solved), "SOLVED"),@@ -1432,7 +1405,7 @@ fn family_table(
14321405 let h = 16 + list.length(records) * row_h + 614331406 let columns = [14341407 x + 8,1435- x + float.round(int.to_float(w) *. 0.50),1408+ x + float.round(int.to_float(w) *. 0.5),14361409 x + float.round(int.to_float(w) *. 0.68),14371410 x + w - 8,14381411 ]@@ -1549,14 +1522,14 @@ fn recent_panel(
15491522 let gy = y + 1615501523 let gh = h - 2415511524 let peak =1552- int.max(1, list.fold(entries, 1, fn(best, e) { int.max(best, e.optimal) }))1525+ int.max(1526+ 1,1527+ list.fold(entries, 1, fn(best, e) { int.max(best, e.optimal) }),1528+ )15531529 // Bars keep their own width instead of stretching, so three solves read as15541530 // three solves rather than as one filled rectangle.15551531 let bw =1556- float.min(1557- int.to_float(gw) /. int.to_float(list.length(entries)),1558- 7.0,1559- )1532+ float.min(int.to_float(gw) /. int.to_float(list.length(entries)), 7.0)15601533 let daylight = case bw >. 3.0 {15611534 True -> 115621535 False -> 0@@ -108,8 +108,7 @@ pub fn pick(rng: Rng, items: List(a), fallback: a) -> #(a, Rng) {
108108 0 -> #(fallback, rng)109109 _ -> {110110 let #(index, rng) = next_int(rng, count)111- let item =112- items |> list.drop(index) |> list.first |> unwrap_or(fallback)111+ let item = items |> list.drop(index) |> list.first |> unwrap_or(fallback)113112 #(item, rng)114113 }115114 }@@ -37,8 +37,7 @@ pub fn depth_for_rung(rung: Int, ecc: Int) -> Int {
3737 case ecc > 0 {3838 False -> 03939 True -> {40- let base =41- int.max(5, int.min(14, float.round(int.to_float(ecc) *. 0.22)))40+ let base = int.max(5, int.min(14, float.round(int.to_float(ecc) *. 0.22)))4241 let step =4342 int.max(2, float.round(int.to_float(ecc - base) /. int.to_float(arc)))4443 int.min(ecc, base + step * { int.max(1, rung) - 1 })@@ -60,10 +59,8 @@ pub fn xp_for(
6059 steps steps: Int,6160 first_clear first_clear: Bool,6261 ) -> Int {63- let base =64- 4.0 +. int.to_float(optimal) *. { 0.6 +. score /. 6.0 }65- let ratio =66- int.to_float(optimal) /. int.to_float(int.max(1, steps))62+ let base = 4.0 +. int.to_float(optimal) *. { 0.6 +. score /. 6.0 }63+ let ratio = int.to_float(optimal) /. int.to_float(int.max(1, steps))6764 let efficiency = case float.square_root(clamp(ratio, 0.35, 1.0)) {6865 Ok(value) -> value6966 Error(_) -> 1.0@@ -302,7 +302,10 @@ pub fn aperture(
302302 // a unit past it: a frame that stops level with what it frames stops being a303303 // frame.304304 let high =305- int.max(2, int.min({ room - 3 } / 2, float.round(int.to_float(cell) *. 0.26)))305+ int.max(306+ 2,307+ int.min({ room - 3 } / 2, float.round(int.to_float(cell) *. 0.26)),308+ )306309 let head = int.min(high * 2 - 1, w - 6)307310 let deep = high + 2308311 @@ -333,15 +336,5 @@ pub fn label(
333336 face: Face,334337 align: Align,335338 ) -> Op {336- Label(337- text:,338- x:,339- y:,340- size:,341- face:,342- track: 0.0,343- align:,344- alpha: 1.0,345- max_w: 0.0,346- )339+ Label(text:, x:, y:, size:, face:, track: 0.0, align:, alpha: 1.0, max_w: 0.0)347340 }@@ -252,11 +252,7 @@ pub fn fit_ruling(ruling: Ruling, fill_px: Int, grid_px: Int) -> Ruling {
252252 /// outside the block sprites is composed entirely at those two values.253253 fn spread(raw: List(Int), levels: Int) -> List(Int) {254254 list.map(raw, fn(value) {255- float_round(256- { int.to_float(value) +. 0.5 }257- /. int.to_float(levels)258- *. 255.0,259- )255+ float_round({ int.to_float(value) +. 0.5 } /. int.to_float(levels) *. 255.0)260256 })261257 }262258 @@ -298,10 +294,10 @@ pub fn overprint_table(pattern: Pattern) -> Result(List(Int), Nil) {
298294 fn bayer() -> List(Int) {299295 spread(300296 [301- 0, 32, 8, 40, 2, 34, 10, 42, 48, 16, 56, 24, 50, 18, 58, 26,302- 12, 44, 4, 36, 14, 46, 6, 38, 60, 28, 52, 20, 62, 30, 54, 22,303- 3, 35, 11, 43, 1, 33, 9, 41, 51, 19, 59, 27, 49, 17, 57, 25,304- 15, 47, 7, 39, 13, 45, 5, 37, 63, 31, 55, 23, 61, 29, 53, 21,297+ 0, 32, 8, 40, 2, 34, 10, 42, 48, 16, 56, 24, 50, 18, 58, 26, 12, 44, 4, 36,298+ 14, 46, 6, 38, 60, 28, 52, 20, 62, 30, 54, 22, 3, 35, 11, 43, 1, 33, 9, 41,299+ 51, 19, 59, 27, 49, 17, 57, 25, 15, 47, 7, 39, 13, 45, 5, 37, 63, 31, 55,300+ 23, 61, 29, 53, 21,305301 ],306302 64,307303 )@@ -312,14 +308,10 @@ fn bayer() -> List(Int) {
312308 /// halftone screen is, and why this reads as printed rather than as a computer.313309 fn cluster_raw() -> List(Int) {314310 [315- 24, 10, 12, 26, 35, 47, 49, 37,316- 8, 0, 2, 14, 45, 59, 61, 51,317- 22, 6, 4, 16, 43, 57, 63, 53,318- 30, 20, 18, 28, 33, 41, 55, 39,319- 34, 46, 48, 38, 25, 11, 13, 27,320- 44, 58, 60, 50, 9, 1, 3, 15,321- 42, 56, 62, 52, 23, 7, 5, 17,322- 32, 40, 54, 36, 31, 21, 19, 29,311+ 24, 10, 12, 26, 35, 47, 49, 37, 8, 0, 2, 14, 45, 59, 61, 51, 22, 6, 4, 16,312+ 43, 57, 63, 53, 30, 20, 18, 28, 33, 41, 55, 39, 34, 46, 48, 38, 25, 11, 13,313+ 27, 44, 58, 60, 50, 9, 1, 3, 15, 42, 56, 62, 52, 23, 7, 5, 17, 32, 40, 54,314+ 36, 31, 21, 19, 29,323315 ]324316 }325317 @@ -176,8 +176,7 @@ pub fn record(
176176 let first_clear = !list.any(family.best, fn(entry) { entry.0 == rung })177177 let xp = progress.xp_for(optimal:, score:, steps:, first_clear:)178178 179- let previous_best =180- family.best |> list.key_find(rung) |> result.unwrap(steps)179+ let previous_best = family.best |> list.key_find(rung) |> result.unwrap(steps)181180 let best = case first_clear || steps < previous_best {182181 True -> list.key_set(family.best, rung, steps)183182 False -> family.best@@ -211,9 +210,9 @@ pub fn record(
211210 xp: totals.xp + xp,212211 clean: totals.clean213212 + case detours == 0 {214- True -> 1215- False -> 0216- },213+ True -> 1214+ False -> 0215+ },217216 detours: totals.detours + detours,218217 )219218 @@ -260,12 +259,7 @@ pub fn stamp_day(days: Days, today: String) -> Days {
260259 }261260 _ -> 1262261 }263- Days(264- first:,265- last: Some(today),266- streak:,267- best: int.max(days.best, streak),268- )262+ Days(first:, last: Some(today), streak:, best: int.max(days.best, streak))269263 }270264 }271265 }@@ -422,11 +416,7 @@ fn payload_decoder() -> Decoder(Save) {
422416 use pattern <- decode.optional_field("shading", "auto", decode.string)423417 use hints <- decode.optional_field("hints", "full", decode.string)424418 use density <- decode.optional_field("density", 3, decode.int)425- use fams <- decode.optional_field(426- "fams",427- [],428- decode.list(family_decoder()),429- )419+ use fams <- decode.optional_field("fams", [], decode.list(family_decoder()))430420 use totals <- decode.optional_field(431421 "totals",432422 default.totals,@@ -521,8 +511,8 @@ fn guard(save: Save) -> Save {
521511 Save(522512 ..save,523513 families: list.filter(save.families, fn(record) {524- list.any(families.all(), fn(family) { family.id == record.id })525- })514+ list.any(families.all(), fn(family) { family.id == record.id })515+ })526516 |> list.map(fn(record) {527517 FamilyRecord(..record, rung: int.max(1, record.rung))528518 }),@@ -328,11 +328,7 @@ pub fn combo_for(theme: Theme, family_id: String, rung: Int) -> Combo {
328328 let rng = prng.seeded_at(family_id <> "|" <> theme.id, rung * 8191)329329 let #(backdrop, rng) = prng.pick(rng, theme.backdrops, ground.Lattice)330330 let #(ground_pair, rng) =331- prng.pick(332- rng,333- theme.grounds,334- Ground("MINT", theme.paper, theme.ink),335- )331+ prng.pick(rng, theme.grounds, Ground("MINT", theme.paper, theme.ink))336332 let #(pattern, rng) = prng.pick(rng, theme.shading, shading.Atkinson)337333 // Drawn unconditionally so the stream stays in step whichever shape came up,338334 // then normalised: the shapes that take no ruling always report FINE rather@@ -369,8 +365,7 @@ pub fn apply_pin(combo: Combo, pinned: Result(Pattern, Nil)) -> Combo {
369365 /// switching theme is not merely the same choice recoloured.370366 pub fn logo_ink(theme: Theme, day: Int) -> Rgb {371367 let rng = prng.seeded_at("dev-logo|" <> theme.id, day * 2657)372- let #(colour, _) =373- prng.pick(rng, theme.pieces, Rgb(0xD1, 0x4A, 0x42))368+ let #(colour, _) = prng.pick(rng, theme.pieces, Rgb(0xD1, 0x4A, 0x42))374369 colour375370 }376371 @@ -5,7 +5,8 @@ import klotski/families
55 66 pub fn a_family_packing_parses_into_its_pieces_test() {77 let family = families.at(0)8- let pieces = domain.pieces_from_grid(families.board(family), family.grid)8+ let pieces =9+ domain.pieces_from_grid(families.board(family), families.grid(family))910 pieces |> list.length |> should.equal(10)1011 }1112 @@ -16,7 +17,8 @@ pub fn a_family_packing_parses_into_its_pieces_test() {
1617 pub fn every_packing_but_the_traditional_one_is_seated_test() {1718 families.all()1819 |> list.map(fn(family) {19- let pieces = domain.pieces_from_grid(families.board(family), family.grid)20+ let pieces =21+ domain.pieces_from_grid(families.board(family), families.grid(family))2022 #(family.id, domain.is_solved(families.board(family), pieces))2123 })2224 |> should.equal([@@ -34,7 +36,7 @@ pub fn every_packing_but_the_traditional_one_is_seated_test() {
3436 pub fn a_piece_moves_only_through_free_cells_test() {3537 let family = families.by_id("compact")3638 let board = families.board(family)37- let pieces = domain.pieces_from_grid(board, family.grid)39+ let pieces = domain.pieces_from_grid(board, families.grid(family))3840 let assert Ok(goal) = list.find(pieces, fn(p) { p.shape == Goal })3941 4042 // COMPACT is seated on the bottom row with a column of air either side of it,@@ -47,7 +49,7 @@ pub fn a_piece_moves_only_through_free_cells_test() {
4749 pub fn moving_and_reversing_restores_a_board_test() {4850 let family = families.at(0)4951 let board = families.board(family)50- let pieces = domain.pieces_from_grid(board, family.grid)52+ let pieces = domain.pieces_from_grid(board, families.grid(family))5153 // The classic packing's only air is the two cells under the middle singles.5254 let assert Ok(single) =5355 list.find(pieces, fn(p) { p.shape == Single && p.x == 1 && p.y == 3 })@@ -24,6 +24,9 @@ pub fn mulberry_matches_the_reference_test() {
2424 fn list_round(values: List(Float)) -> List(Int) {2525 case values {2626 [] -> []27- [value, ..rest] -> [float.round(value *. 1_000_000.0 -. 0.5), ..list_round(rest)]27+ [value, ..rest] -> [28+ float.round(value *. 1_000_000.0 -. 0.5),29+ ..list_round(rest)30+ ]2831 }2932 }@@ -41,7 +41,8 @@ pub fn a_corrupt_save_is_a_fresh_one_test() {
4141 /// A streak survives a gap of one day, because "yesterday" should still count4242 /// while today is still going. Two days is a break.4343 pub fn a_streak_survives_one_day_and_not_two_test() {44- let start = store.stamp_day(store.Days(option.None, option.None, 0, 0), "2026-08-05")44+ let start =45+ store.stamp_day(store.Days(option.None, option.None, 0, 0), "2026-08-05")4546 start.streak |> should.equal(1)4647 4748 let next = store.stamp_day(start, "2026-08-06")