clone

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

A dispersed screen has a shorter ladder than a clustered one

Dami committed c08ce72
ORDERED at a coarse cell collapses into lines that nobody drew, and on a
1x1 at three buffer pixels a unit it does it plainly: faint rules
through the block, moving as the block moves.

The reason is what "dispersed" means. A clustered screen grows a dot as
its cell grows -- the ink stays in one place and gets bigger, which is
what a coarser press does to a halftone. A dispersed screen has no dot
to grow. Its ink is scattered single cells across the 8x8, so enlarging
the cell enlarges the *scatter*, and past a point it stops being a
texture and becomes a grid. At a light tone that grid is mostly empty,
its occupied cells fall into rows and columns, and the eye joins them.
The block ends up ruled.

So the ladder is a property of the shape and not only of the theme:
ORDERED ends at MEDIUM, which is cells one and two rather than two and
three. Every other ruled shape keeps all five. It is the same argument
as a block too small to carry a whole period, applied to the pattern
rather than to the thing it is printed on.

Capped in effectiveRuling rather than in the pools, because that is the
one gate every ruling passes through -- the seeded roll, a pinned look,
the console handle -- so there is no door left open for it to come back
through. And because a ladder is a prefix of the full one, fitRuling can
still step down from an allowed rung without knowing any of this.

The look count follows honestly rather than being restated: a theme
lists five rulings, ORDERED can print at two of them, so a theme is 1218
looks rather than 1344, counted through effectiveRuling so the number
cannot drift from what is actually on screen.
commit
c08ce727c23409cbc00d8bdcdc3882633d132054
parent
5e637c2b8589d3e0f132c6af3b2544500a504ce3
author
Dami <dami.etoile@gmail.com>
5 files changed, +97 -14
MREADME.md17+++++++++--
Mmockup/src/shading.js37+++++++++++++++++++++++-
Mmockup/src/theme.js9+++---
Mmockup/sw.js2+-
Mmockup/tools/check.mjs46+++++++++++++++++++++++++++---
M README.md +14 -3
@@ -60,6 +60,16 @@ pipeline to stop being one bit per region — but the same mechanism and the sam
6060 Diffusion and the flat threshold take no ruling: error diffusion's grain *is* the pixel grid, and6161 a flat threshold has no grain. They report one look each rather than five identical ones.6262 63+And one shape has a shorter ladder than the rest. A clustered screen grows a *dot* as its cell64+grows — the ink stays put and gets bigger, which is what a coarser press does. A dispersed one has65+no dot to grow: its ink is scattered single cells across the 8×8, so a bigger cell enlarges the66+*scatter*, and past a point it stops being a texture and becomes a grid. At a light tone that grid67+is mostly empty, its occupied cells line up in rows and columns, and the eye joins them — a 1×168+comes out ruled with faint lines nobody drew, which move as the block moves. So ORDERED stops at69+MEDIUM. That is the same argument as a block too small to carry a period, applied to the pattern70+instead of the thing it is printed on, and it is enforced in `effectiveRuling` rather than in the71+pools, so every door into a ruling goes through it and the look count stays honest.72+6373 **Black on white, with a menu bar.** The Macintosh was ink on paper, so this is too. Text lives6474 inside a window, never on the desktop, which is how a patterned ground and legible labels stop6575 being a trade. Blocks are flat with a hard offset drop shadow; an extruded 2.5D side was tried and
@@ -313,9 +323,10 @@ gold — and four *pools*: which backdrop stencils belong to it, which colour pa
313323 printed in, which screen shapes the blocks may take, and how coarse those screens may be. A board314324 draws one of each, seeded on its family and rung by the same pair that decides which board it is,315325 so a rung always looks the way it looked and a ladder walks through combinations nobody authored.316-Seven backdrops, six grounds and thirty-two block looks (six shapes at five rulings, plus the two317-that take none) is **1344 per theme** out of about twenty written-down numbers, which is the whole318-reason for doing it this way rather than drawing 1344 backdrops.326+Seven backdrops, six grounds and twenty-nine block looks (five shapes at five rulings, ORDERED at327+the two it can hold, plus the two that take no ruling at all) is **1218 per theme** out of about328+twenty written-down numbers, which is the whole reason for doing it this way rather than drawing329+1218 backdrops.319330 320331 The rolling is confined to the board — the card, the puzzle and the record. The atlas, the title321332 and the record-of-everything are chrome, and chrome that shifts under you every time you open it is
M mockup/src/shading.js +35 -2
@@ -178,7 +178,23 @@ export const RULING_NAMES = Object.keys(RULINGS);
178178 * ink between two plates. */179179 export const PATTERNS = {180180 atkinson: { label: 'ATKINSON', diffuse: true, ruled: false },181- ordered: { label: 'ORDERED', m: BAYER, ruled: true },181+ /* `top` is the coarsest rung this shape may be printed at, and ORDERED is the182+ * one that needs it.183+ *184+ * A clustered screen grows a *dot* as its cell grows: the ink stays in one185+ * place and gets bigger, which is what a coarser press does. A dispersed one186+ * has no dot to grow -- its ink is scattered single cells over the 8x8, so187+ * enlarging the cell enlarges the *scatter*, and past a certain size it stops188+ * being a texture and becomes a grid. At a light tone that grid is mostly189+ * empty, its occupied cells line up in rows and columns, and the eye joins190+ * them: a 1x1 at three buffer pixels a unit comes out ruled with faint lines191+ * that no one drew and that move as the block moves.192+ *193+ * So this shape's ladder ends at MEDIUM. It is not a smaller version of the194+ * coarse end, it is the point past which a dispersed screen stops being one --195+ * the same argument as the block that is too small to carry a period, applied196+ * to the pattern instead of the thing it is printed on. */197+ ordered: { label: 'ORDERED', m: BAYER, ruled: true, top: 'medium' },182198 benday: { label: 'BEN-DAY', m: CLUSTER, ruled: true },183199 lines: { label: 'LINES', m: LINES, ruled: true },184200 diagonal: { label: 'DIAGONAL', m: DIAGONAL, ruled: true },
@@ -189,9 +205,26 @@ export const PATTERNS = {
189205 export const PATTERN_NAMES = Object.keys(PATTERNS);190206 191207 /** The ruling a pattern will actually use: 'fine' for those that take none. */208+/** How many rungs of the ladder a shape can actually be printed at. */209+export const rulingsFor = pattern => {210+ const p = PATTERNS[pattern];211+ if (!p || !p.ruled) return ['fine'];212+ return RULING_NAMES.slice(0, RULING_NAMES.indexOf(p.top || RULING_NAMES[RULING_NAMES.length - 1]) + 1);213+};214+215+/* The ruling a pattern will actually use.216+ *217+ * 'fine' for the shapes that take none, and capped at the shape's own top for218+ * the one that has a top. Capping here rather than in the pools is what keeps219+ * every count honest: a theme lists five rulings, ORDERED can print at two of220+ * them, and the roll landing on BILLBOARD gives MEDIUM rather than a sixth name221+ * for a picture already on the list. Every ladder is a prefix of the full one,222+ * so anything that steps *down* from here stays inside it. */192223 export function effectiveRuling(pattern, ruling) {193224 const p = PATTERNS[pattern];194- return p && p.ruled && RULINGS[ruling] ? ruling : 'fine';225+ if (!p || !p.ruled || !RULINGS[ruling]) return 'fine';226+ const allowed = rulingsFor(pattern);227+ return allowed.includes(ruling) ? ruling : allowed[allowed.length - 1];195228 }196229 197230 /* A screen coarser than the thing it is printed on stops being a screen.
M mockup/src/theme.js +5 -4
M mockup/sw.js +1 -1
M mockup/tools/check.mjs +42 -4
@@ -7,7 +7,7 @@ import {
77 } from '../src/theme.js';88 import { GROUND_TILES } from '../src/ground.js';99 import {10- PATTERNS, RULINGS, RULING_NAMES, effectiveRuling, fitRuling, screenOf,10+ PATTERNS, RULINGS, RULING_NAMES, effectiveRuling, fitRuling, screenOf, rulingsFor,1111 } from '../src/shading.js';1212 import { walkTrace, traceStats } from '../src/walk.js';1313
@@ -145,6 +145,35 @@ ok(fitRuling('billboard', 9999) === 'billboard', 'a preview should not be held t
145145 console.log(`register: a 96px cell carries ${RULING_NAMES.filter(r => 96 % (RULINGS[r].cell * 8) === 0).length}` +146146 ` of ${RULING_NAMES.length} rulings, a 72px cell ${RULING_NAMES.filter(r => 72 % (RULINGS[r].cell * 8) === 0).length}`);147147 148+/* 3c-2. a shape is never printed past its own ladder.149+ *150+ * ORDERED is dispersed: its ink is scattered single cells rather than a dot151+ * that grows, so a coarse cell enlarges the scatter until the occupied cells152+ * line up and the eye joins them into rules nobody drew. It stops at MEDIUM,153+ * and the cap has to hold wherever a ruling can enter -- the seeded pools, a154+ * pinned look, the console handle -- or the artifact comes back through155+ * whichever door was left open. */156+for (const pat of Object.keys(PATTERNS)) {157+ const allowed = rulingsFor(pat);158+ for (const asked of RULING_NAMES) {159+ const got = effectiveRuling(pat, asked);160+ ok(allowed.includes(got), `${pat} asked for ${asked} and got ${got}, which is off its ladder`);161+ }162+ // Every ladder is a prefix of the full one, which is what lets fitRuling step163+ // down without checking: anything below an allowed rung is allowed.164+ ok(allowed.every((r, i) => r === RULING_NAMES[i]), `${pat}: ladder is not a prefix of the full one`);165+}166+ok(rulingsFor('ordered').join() === 'fine,medium', 'ORDERED should stop at MEDIUM');167+ok(effectiveRuling('ordered', 'billboard') === 'medium', 'ORDERED at BILLBOARD should come back as MEDIUM');168+ok(rulingsFor('benday').length === RULING_NAMES.length, 'a clustered screen keeps the whole ladder');169+for (const t of Object.values(THEMES)) {170+ const rolled = new Set(t.rulings.map(r => effectiveRuling('ordered', r)));171+ ok([...rolled].every(r => rulingsFor('ordered').includes(r)),172+ 'a theme pool can still roll ORDERED past its top');173+}174+console.log(`ladders: ${Object.keys(PATTERNS).filter(p => PATTERNS[p].ruled)175+ .map(p => `${p}:${rulingsFor(p).length}`).join(' ')}`);176+148177 /* 3d. AUTO resolves to a real pair.149178 *150179 * The light and dark ends have to exist, and they have to actually be light and
@@ -206,11 +235,15 @@ for (const fam of FAMILIES.slice(0, 3)) {
206235 }207236 }208237 // A ruling is only ever reported when the shape actually has a grain to209-// resize, so the combo never claims a coarse Atkinson.238+// resize, so the combo never claims a coarse Atkinson -- and never claims a239+// rung past the shape's own ladder either, which for ORDERED ends at MEDIUM.210240 for (const s of Object.keys(PATTERNS)) {211241 for (const d of Object.keys(RULINGS)) {212242 const got = effectiveRuling(s, d);213- ok(PATTERNS[s].ruled ? got === d : got === 'fine', `${s}@${d} normalised to ${got}`);243+ const allowed = rulingsFor(s);244+ const want = !PATTERNS[s].ruled ? 'fine'245+ : allowed.includes(d) ? d : allowed[allowed.length - 1];246+ ok(got === want, `${s}@${d} normalised to ${got}, expected ${want}`);214247 }215248 }216249 for (const id of Object.keys(THEMES)) {
@@ -292,7 +325,12 @@ for (const [id, t] of Object.entries(THEMES)) {
292325 // look however many densities the theme lists, and multiplying regardless293326 // would claim variety that is not on screen.294327 const looksPerTheme = t => {295- const blocks = t.shading.reduce((n, s) => n + (PATTERNS[s].ruled ? t.rulings.length : 1), 0);328+ // Counted through effectiveRuling, so a shape with a shorter ladder is329+ // counted at its own length: ORDERED stops at MEDIUM, and a pool that rolls330+ // BILLBOARD for it produces MEDIUM rather than a sixth name for a picture331+ // already on the list.332+ const blocks = t.shading.reduce(333+ (n, s) => n + new Set(t.rulings.map(r => effectiveRuling(s, r))).size, 0);296334 return t.backdrops.length * t.grounds.length * blocks;297335 };298336 const combos = Object.values(THEMES).map(looksPerTheme).reduce((a, b) => a + b, 0);