1import gleam/list2import gleeunit/should3import klotski/ground4import klotski/shading.{5 Atkinson, BenDay, Billboard, Coarse, Comic, Crosshatch, Diagonal, Fine, Lines,6 Medium, Ordered, Poster, Solid,7}89/// Every screen is an 8x8 table over 0..255, and every one of them has to agree10/// at the two extremes — the whole scene outside the block sprites is composed11/// at 0 and 255, and a table that reached either end would put ink on bare paper12/// or take it off solid black.13pub fn every_screen_is_a_full_table_inside_the_range_test() {14 shading.patterns()15 |> list.all(fn(pattern) {16 let table = shading.table(pattern)17 list.length(table) == 6418 && list.all(table, fn(value) { value > 0 && value < 255 })19 })20 |> should.be_true21}2223/// A ruling means nothing to error diffusion — its grain *is* the pixel grid —24/// and nothing to a flat threshold, which has no grain at all. Saying so is more25/// honest than inventing a frequency for them and shipping five identical looks.26pub fn the_unruled_shapes_report_one_look_each_test() {27 shading.rulings_for(Atkinson) |> should.equal([Fine])28 shading.rulings_for(Solid) |> should.equal([Fine])29 shading.rulings_for(Ordered) |> list.length |> should.equal(5)3031 // A roll that lands on a frequency an unruled shape cannot print reports the32 // one it can, so the sprite cache key agrees with what is on screen.33 shading.effective_ruling(Atkinson, Billboard) |> should.equal(Fine)34 shading.effective_ruling(Ordered, Billboard) |> should.equal(Billboard)35}3637/// A screen's shape lives in its whole period: eight cells make one, and a block38/// spanning less than a period and a half does not show a pattern, it shows one39/// or two arbitrary bands of it.40pub fn a_screen_coarser_than_its_block_is_stepped_down_test() {41 // 96 buffer pixels is room for BILLBOARD's 48.42 shading.fit_ruling(Billboard, 96, 0) |> should.equal(Billboard)43 // 40 is not: 6 * 12 = 72 > 40, and neither is POSTER's 48, so COARSE.44 shading.fit_ruling(Billboard, 40, 0) |> should.equal(Coarse)45 shading.fit_ruling(Billboard, 10, 0) |> should.equal(Fine)46}4748/// The second requirement is register. The screen is ruled across the whole49/// picture rather than restarted at each block, so a cell that is not a whole50/// number of periods wide would put every piece on a different part of the51/// pattern from its neighbour. A screen that does not hold register is not a52/// coarser screen, it is a misprint.53pub fn a_screen_out_of_register_with_the_grid_is_stepped_down_test() {54 // At three buffer pixels per unit a 32-unit cell is 96 wide, and every55 // ruling's period divides it — so a phone gets the whole ladder.56 shading.fit_ruling(Billboard, 96, 96) |> should.equal(Billboard)57 // A 24-unit cell is 72, which only FINE and COARSE divide.58 shading.fit_ruling(Billboard, 72, 72) |> should.equal(Coarse)59 shading.fit_ruling(Medium, 72, 72) |> should.equal(Fine)60 // Passing no grid skips the check, which is what the previews want: an atlas61 // row is a picture of a board rather than a board, and nothing on it moves.62 shading.fit_ruling(Poster, 72, 0) |> should.equal(Poster)63}6465/// COMIC is the one that is not a threshold at all but a second pass: a tint66/// carries a second plate, so the block's ink is split between the piece colour67/// and the same hue at full strength on a rotated screen.68pub fn only_the_comic_screen_carries_a_second_plate_test() {69 shading.overprint_table(Comic)70 |> should.be_ok71 |> list.length72 |> should.equal(64)7374 [Atkinson, Ordered, BenDay, Lines, Diagonal, Crosshatch, Solid]75 |> list.all(fn(pattern) { shading.overprint_table(pattern) == Error(Nil) })76 |> should.be_true77}7879/// The tables have to be the proof of concept's to the number, or a board that80/// looked one way there looks another here. A shear either way approximates a81/// rotation on a grid this small — and unlike a real rotation it still tiles,82/// which is why the second plate is a rearrangement of the first's values rather83/// than a permutation of them.84pub fn the_tables_match_the_reference_test() {85 shading.table(Ordered)86 |> list.take(8)87 |> should.equal([2, 129, 34, 161, 10, 137, 42, 169])8889 shading.table(BenDay)90 |> list.take(8)91 |> should.equal([98, 42, 50, 106, 141, 189, 197, 149])9293 let assert Ok(rotated) = shading.overprint_table(Comic)94 rotated |> list.take(8) |> should.equal([98, 42, 50, 58, 181, 237, 253, 213])95 // Every level on the second plate is one the first plate prints: it is the96 // same screen at a different angle, not a different screen.97 rotated98 |> list.all(fn(value) { list.contains(shading.table(Comic), value) })99 |> should.be_true100}101102/// One axis only, so tone becomes the width of a rule. The eight levels ascend103/// outward from the middle rather than running top to bottom, so darkening104/// widens one band symmetrically instead of adding a stray row at the edge — and105/// keeps the band away from the tile seam, where a small block could miss it.106pub fn a_ruled_screen_varies_along_one_axis_only_test() {107 let table = shading.table(Lines)108 // Every row is flat.109 rows(table)110 |> list.all(fn(row) {111 case row {112 [first, ..rest] -> list.all(rest, fn(value) { value == first })113 [] -> True114 }115 })116 |> should.be_true117118 // And the middle of the tile is the first to take ink.119 let assert Ok(top) = list.first(table)120 let assert Ok(middle) = list.first(list.drop(table, 3 * 8))121 { middle < top } |> should.be_true122}123124/// A background is not a picture: the densest stencil here is one pixel in125/// eight, and the lattice that shipped first is one in sixteen. A ground that126/// competes with the board is a ground that has to be looked past.127pub fn every_ground_is_a_sparse_eight_by_eight_stencil_test() {128 ground.all()129 |> list.all(fn(backdrop) {130 let tile = ground.tile(backdrop)131 let ink = list.fold(tile, 0, fn(n, bit) { n + bit })132 list.length(tile) == 64133 && list.all(tile, fn(bit) { bit == 0 || bit == 1 })134 && ink <= 8135 })136 |> should.be_true137}138139/// A flat ground is a real member of the family, and it is the only one that140/// lets a pure-black theme actually reach pure black across the whole screen.141pub fn the_plain_ground_puts_nothing_down_test() {142 ground.tile(ground.Plain)143 |> list.all(fn(bit) { bit == 0 })144 |> should.be_true145}146147/// The table, as its eight rows.148fn rows(table: List(Int)) -> List(List(Int)) {149 case table {150 [] -> []151 _ -> [list.take(table, 8), ..rows(list.drop(table, 8))]152 }153}