├── LICENSE.md
├── README.md
├── demo-example.png
├── index.html
├── pattern-match-2d.js
├── src
├── dfa.ts
├── display.ts
├── grid.ts
├── idmap.ts
├── iset.ts
├── main.ts
├── matcher.ts
├── misc.ts
├── partition.ts
├── pattern.ts
├── sample.ts
└── symmetry.ts
└── tsconfig.json
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2022 Andrew Kay
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
10 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # pattern-match-2d.js
2 |
3 | An algorithm for fast 2D pattern-matching with wildcards, with a [demo](https://kaya3.github.io/pattern-match-2d/) app inspired by [MarkovJunior](https://github.com/mxgmn/MarkovJunior) (by Maxim Gumin).
4 |
5 | 
6 |
7 | The algorithm uses a [DFA](https://en.wikipedia.org/wiki/Deterministic_finite_automaton) to match individual rows of patterns in the rows of the grid, then another DFA to match the whole patterns by recognising vertical sequences of row matches. Scanning a rectangular area of the grid takes O((*w* + *a*)(*h* + *b*) + *m*) time where *w*, *h* are the width and height of the area, *a*, *b* are the maximum width and height of any pattern, and *m* is the the number of matches which were either made or broken in the scanned area.
8 |
--------------------------------------------------------------------------------
/demo-example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kaya3/pattern-match-2d/ef470d2684ec84bde0e0f105d132c1eefe45f3d2/demo-example.png
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 |
11 |
12 |
13 | pattern-match-2d.js demo, inspired by MarkovJunior by Maxim Gumin.
14 |
15 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/pattern-match-2d.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | /**
3 | * Data structure representing a partition of the natural numbers from 0 to n - 1,
4 | * for use in the `DFA.minimise` algorithm. The main operations are `refine` and
5 | * `pollUnprocessed`.
6 | *
7 | * https://en.wikipedia.org/wiki/Partition_refinement#Data_structure
8 | */
9 | class Partition {
10 | /**
11 | * The numbers from 0 to n - 1, ordered so that each subset in the partition
12 | * is a contiguous range.
13 | *
14 | * Invariant: `arr` is a permutation of the numbers from 0 to n - 1
15 | */
16 | arr;
17 | /**
18 | * Maps the numbers from 0 to n - 1 to their indices in `arr`.
19 | *
20 | * Invariant: `arr[i] === x` if and only if `indices[x] === i`
21 | */
22 | indices;
23 | /**
24 | * The boundaries in `arr` for each subset in the partition.
25 | *
26 | * Invariant: `subsets[i].index === i`
27 | * Invariant: `subsets[i].start < subsets[i].end`
28 | * Invariant: `subsets[i].start === 0` or there is a unique `j` such that `subsets[i].start === subsets[j].end`
29 | * Invariant: `subsets[i].end === n` or there is a unique `j` such that `subsets[i].end === subsets[j].start`
30 | */
31 | subsets = [];
32 | /**
33 | * The subsets which have yet to be processed by the `DFA.minimise` algorithm,
34 | * plus possibly some empty subsets which do not need to be processed.
35 | *
36 | * Invariant: if `subset.isUnprocessed` then `unprocessed.includes(subset)`
37 | * Invariant: if `unprocessed.includes(subset)` and not `subset.isUnprocessed`, then `subset.start === subset.end`
38 | */
39 | unprocessed = [];
40 | /**
41 | * Maps each number from 0 to n - 1 to the subset it is a member of.
42 | *
43 | * Invariant: `map[x].start <= indices[x] && indices[x] < map[x].end`
44 | */
45 | map;
46 | /**
47 | * Constructs a new instance representing a partition of the numbers from
48 | * 0 to n - 1. The partition initially contains only a single subset (the
49 | * whole range).
50 | */
51 | constructor(n) {
52 | this.arr = makeArray(n, i => i);
53 | this.indices = makeArray(n, i => i);
54 | const initialSubset = this.makeSubset(0, n, true);
55 | this.map = emptyArray(n, initialSubset);
56 | }
57 | /**
58 | * Returns the number of subsets in this partition.
59 | */
60 | countSubsets() {
61 | return this.subsets.length;
62 | }
63 | makeSubset(start, end, isUnprocessed) {
64 | const { subsets } = this;
65 | const subset = {
66 | index: subsets.length,
67 | start,
68 | end,
69 | isUnprocessed,
70 | sibling: undefined,
71 | };
72 | subsets.push(subset);
73 | if (isUnprocessed) {
74 | this.unprocessed.push(subset);
75 | }
76 | return subset;
77 | }
78 | deleteSubset(subset) {
79 | // sanity check
80 | if (subset.start !== subset.end) {
81 | throw new Error();
82 | }
83 | const { index } = subset;
84 | const removed = this.subsets.pop();
85 | if (removed.index !== index) {
86 | this.subsets[removed.index = index] = removed;
87 | }
88 | subset.isUnprocessed = false;
89 | }
90 | /**
91 | * Returns a subset which needs to be processed, and marks it as processed.
92 | * The elements are in no particular order.
93 | *
94 | * If no subsets remain to be processed, `undefined` is returned.
95 | */
96 | pollUnprocessed() {
97 | const { unprocessed } = this;
98 | while (unprocessed.length > 0) {
99 | const subset = unprocessed.pop();
100 | // have to check `isUnprocessed` because deleted subsets may still be in the stack
101 | if (subset.isUnprocessed) {
102 | subset.isUnprocessed = false;
103 | return this.arr.slice(subset.start, subset.end);
104 | }
105 | }
106 | return undefined;
107 | }
108 | /**
109 | * Returns a representative element from the subset in the partition which
110 | * contains the number `x`.
111 | */
112 | getRepresentative(x) {
113 | return this.arr[this.map[x].start];
114 | }
115 | /**
116 | * Calls the provided callback function with a representative element
117 | * from each subset in the partition.
118 | */
119 | forEachRepresentative(f) {
120 | const { arr } = this;
121 | for (const subset of this.subsets) {
122 | f(arr[subset.start]);
123 | }
124 | }
125 | /**
126 | * Refines this partition by splitting any subsets which partly intersect
127 | * with the given set. If an unprocessed subset is split, both parts are
128 | * marked unprocessed; otherwise, the smaller part is marked.
129 | */
130 | refine(set) {
131 | const { unprocessed, map } = this;
132 | const splits = [];
133 | ISet.forEach(set, x => {
134 | const subset = map[x];
135 | if (subset.sibling === undefined) {
136 | splits.push(subset);
137 | subset.sibling = this.makeSubset(subset.end, subset.end, subset.isUnprocessed);
138 | }
139 | this.moveToSibling(x, subset);
140 | });
141 | for (const subset of splits) {
142 | if (subset.start === subset.end) {
143 | this.deleteSubset(subset);
144 | }
145 | else if (!subset.isUnprocessed) {
146 | const sibling = subset.sibling;
147 | const min = subset.end - subset.start <= sibling.end - sibling.start ? subset : sibling;
148 | min.isUnprocessed = true;
149 | unprocessed.push(min);
150 | }
151 | subset.sibling = undefined;
152 | }
153 | }
154 | /**
155 | * Moves the element x from `subset` to `subset.sibling`, in O(1) time. The
156 | * sibling appears immediately afterwards in `arr`, so `x` is swapped with
157 | * the last member of `subset` and then the boundary is adjusted.
158 | */
159 | moveToSibling(x, subset) {
160 | const { arr, map, indices } = this;
161 | const sibling = subset.sibling;
162 | const i = indices[x];
163 | const j = subset.end = --sibling.start;
164 | const y = arr[j];
165 | arr[i] = y;
166 | indices[y] = i;
167 | arr[j] = x;
168 | indices[x] = j;
169 | map[x] = sibling;
170 | }
171 | }
172 | ///
173 | var Regex;
174 | (function (Regex) {
175 | function letters(letterIDs) {
176 | return { kind: 0 /* Kind.LETTERS */, letterIDs };
177 | }
178 | Regex.letters = letters;
179 | function wildcard() {
180 | return { kind: 1 /* Kind.WILDCARD */ };
181 | }
182 | Regex.wildcard = wildcard;
183 | function concat(children) {
184 | return { kind: 2 /* Kind.CONCAT */, children };
185 | }
186 | Regex.concat = concat;
187 | function union(children) {
188 | return { kind: 3 /* Kind.UNION */, children };
189 | }
190 | Regex.union = union;
191 | function kleeneStar(child) {
192 | return { kind: 4 /* Kind.KLEENESTAR */, child };
193 | }
194 | Regex.kleeneStar = kleeneStar;
195 | function accept(accept) {
196 | return { kind: 5 /* Kind.ACCEPT */, accept };
197 | }
198 | Regex.accept = accept;
199 | function compile(alphabetSize, acceptCount, regex) {
200 | return new NFA(alphabetSize, acceptCount, regex).toDFA().minimise();
201 | }
202 | Regex.compile = compile;
203 | })(Regex || (Regex = {}));
204 | class NFA {
205 | alphabetSize;
206 | acceptCount;
207 | nodes = [];
208 | startID;
209 | constructor(alphabetSize, acceptCount, regex) {
210 | this.alphabetSize = alphabetSize;
211 | this.acceptCount = acceptCount;
212 | this.startID = this.makeFromRegex(regex, this.makeNode([]));
213 | //console.log(`NFA with ${this.nodes.length} nodes on alphabet of size ${alphabetSize}`);
214 | }
215 | makeNode(epsilons, letters = [], nextID = -1) {
216 | const { nodes } = this;
217 | const id = nodes.length;
218 | nodes.push({ epsilons, letters, nextID, acceptSet: [] });
219 | return id;
220 | }
221 | makeFromRegex(regex, outID) {
222 | // https://en.wikipedia.org/wiki/Thompson's_construction
223 | switch (regex.kind) {
224 | case 0 /* Regex.Kind.LETTERS */: {
225 | return this.makeNode([], regex.letterIDs, outID);
226 | }
227 | case 1 /* Regex.Kind.WILDCARD */: {
228 | return this.makeNode([], makeArray(this.alphabetSize, i => i), outID);
229 | }
230 | case 2 /* Regex.Kind.CONCAT */: {
231 | const { children } = regex;
232 | for (let i = children.length - 1; i >= 0; --i) {
233 | outID = this.makeFromRegex(children[i], outID);
234 | }
235 | return outID;
236 | }
237 | case 3 /* Regex.Kind.UNION */: {
238 | const epsilons = regex.children.map(child => this.makeFromRegex(child, this.makeNode([outID])));
239 | return this.makeNode(epsilons);
240 | }
241 | case 4 /* Regex.Kind.KLEENESTAR */: {
242 | const childOutID = this.makeNode([outID]);
243 | const childInID = this.makeFromRegex(regex.child, childOutID);
244 | this.nodes[childOutID].epsilons.push(childInID);
245 | return this.makeNode([childInID, outID]);
246 | }
247 | case 5 /* Regex.Kind.ACCEPT */: {
248 | const node = this.nodes[outID];
249 | node.acceptSet.push(regex.accept);
250 | return outID;
251 | }
252 | }
253 | }
254 | toDFA() {
255 | // https://en.wikipedia.org/wiki/Powerset_construction
256 | const { alphabetSize, nodes } = this;
257 | // need to use a primitive key which will be compared by value; bigint is faster than sorting and joining as a string
258 | const nfaStates = IDMap.withKey(ISet.toBigInt);
259 | const dfaNodes = [];
260 | function getNodeID(nfaState) {
261 | // epsilon closure, by depth-first search
262 | // use ISet instead of Set or bigint for the state, for performance
263 | const stack = ISet.toArray(nfaState);
264 | while (stack.length > 0) {
265 | const nfaNodeID = stack.pop();
266 | for (const eps of nodes[nfaNodeID].epsilons) {
267 | if (!ISet.has(nfaState, eps)) {
268 | ISet.add(nfaState, eps);
269 | stack.push(eps);
270 | }
271 | }
272 | }
273 | return nfaStates.getOrCreateID(nfaState);
274 | }
275 | const startID = getNodeID(ISet.of(nodes.length, [this.startID]));
276 | // sanity check
277 | if (startID !== 0) {
278 | throw new Error();
279 | }
280 | const acceptSetMap = IDMap.withKey(ISet.arrayToBigInt);
281 | // this loop iterates over `nfaStates`, while adding to it via `getNodeID`
282 | for (let nfaStateID = 0; nfaStateID < nfaStates.size(); ++nfaStateID) {
283 | const transitionStates = makeArray(alphabetSize, () => ISet.empty(nodes.length));
284 | const acceptIDs = [];
285 | ISet.forEach(nfaStates.getByID(nfaStateID), nfaNodeID => {
286 | const nfaNode = nodes[nfaNodeID];
287 | for (const letterID of nfaNode.letters) {
288 | ISet.add(transitionStates[letterID], nfaNode.nextID);
289 | }
290 | acceptIDs.push(...nfaNode.acceptSet);
291 | });
292 | dfaNodes.push({
293 | transitions: transitionStates.map(getNodeID),
294 | acceptSetID: acceptSetMap.getOrCreateID(acceptIDs),
295 | acceptIDs,
296 | });
297 | }
298 | return new DFA(alphabetSize, this.acceptCount, acceptSetMap, dfaNodes);
299 | }
300 | }
301 | class DFA {
302 | alphabetSize;
303 | acceptCount;
304 | acceptSetMap;
305 | nodes;
306 | constructor(alphabetSize, acceptCount, acceptSetMap, nodes) {
307 | this.alphabetSize = alphabetSize;
308 | this.acceptCount = acceptCount;
309 | this.acceptSetMap = acceptSetMap;
310 | this.nodes = nodes;
311 | //console.log(`DFA with ${nodes.length} nodes on alphabet of size ${alphabetSize}, ${acceptCount} accepts and ${acceptSetMap.size()} accept sets`);
312 | }
313 | /**
314 | * Returns the number of distinct states of this DFA.
315 | */
316 | size() {
317 | return this.nodes.length;
318 | }
319 | go(state, letterID) {
320 | const { nodes, alphabetSize } = this;
321 | if (state >= 0 && state < nodes.length && letterID >= 0 && letterID < alphabetSize) {
322 | return nodes[state].transitions[letterID];
323 | }
324 | else {
325 | throw new Error();
326 | }
327 | }
328 | getAcceptIDs(state) {
329 | return this.nodes[state].acceptIDs;
330 | }
331 | getAcceptSetID(state) {
332 | return this.nodes[state].acceptSetID;
333 | }
334 | /**
335 | * Returns an array mapping each acceptID to the set of node IDs which accept it.
336 | */
337 | computeAcceptingStates() {
338 | const { nodes, acceptCount } = this;
339 | const n = nodes.length;
340 | const table = makeArray(acceptCount, () => ISet.empty(n));
341 | for (let id = 0; id < n; ++id) {
342 | for (const acceptID of nodes[id].acceptIDs) {
343 | ISet.add(table[acceptID], id);
344 | }
345 | }
346 | return table;
347 | }
348 | /**
349 | * Returns an equivalent DFA with the minimum possible number of states.
350 | */
351 | minimise() {
352 | // https://en.wikipedia.org/wiki/DFA_minimization#Hopcroft's_algorithm
353 | const { alphabetSize, nodes } = this;
354 | const n = nodes.length;
355 | const inverseTransitions = makeArray(alphabetSize * n, () => ISet.empty(n));
356 | for (let id = 0; id < n; ++id) {
357 | const { transitions } = nodes[id];
358 | for (let c = 0; c < alphabetSize; ++c) {
359 | ISet.add(inverseTransitions[c * n + transitions[c]], id);
360 | }
361 | }
362 | const partition = new Partition(n);
363 | for (const d of this.computeAcceptingStates()) {
364 | partition.refine(d);
365 | }
366 | while (true) {
367 | const a = partition.pollUnprocessed();
368 | if (a === undefined) {
369 | break;
370 | }
371 | for (let c = 0; c < alphabetSize; ++c) {
372 | const x = ISet.empty(n);
373 | for (const id of a) {
374 | ISet.addAll(x, inverseTransitions[c * n + id]);
375 | }
376 | partition.refine(x);
377 | // shortcut if the DFA cannot be minimised
378 | if (partition.countSubsets() === n) {
379 | return this;
380 | }
381 | }
382 | }
383 | const reps = IDMap.withKey(id => partition.getRepresentative(id));
384 | // ensure id(rep(0)) === 0, so that 0 is still the starting state
385 | reps.getOrCreateID(0);
386 | partition.forEachRepresentative(x => reps.getOrCreateID(x));
387 | const repNodes = reps.map(rep => {
388 | const { transitions, acceptSetID, acceptIDs } = this.nodes[rep];
389 | return {
390 | transitions: transitions.map(nodeID => reps.getID(nodeID)),
391 | acceptSetID,
392 | acceptIDs,
393 | };
394 | });
395 | return new DFA(alphabetSize, this.acceptCount, this.acceptSetMap, repNodes);
396 | }
397 | }
398 | // https://lospec.com/palette-list/pico-8
399 | const PICO8_PALETTE = {
400 | B: '#000000',
401 | I: '#1D2B53',
402 | P: '#7E2553',
403 | E: '#008751',
404 | N: '#AB5236',
405 | D: '#5F574F',
406 | A: '#C2C3C7',
407 | W: '#FFF1E8',
408 | R: '#FF004D',
409 | O: '#FFA300',
410 | Y: '#FFEC27',
411 | G: '#00E436',
412 | U: '#29ADFF',
413 | S: '#83769C',
414 | K: '#FF77A8',
415 | F: '#FFCCAA',
416 | };
417 | function displayGrid(grid, scale = 8) {
418 | const canvasElem = document.createElement('canvas');
419 | canvasElem.width = grid.width * scale;
420 | canvasElem.height = grid.height * scale;
421 | document.body.appendChild(canvasElem);
422 | const ctx = canvasElem.getContext('2d');
423 | ctx.fillStyle = PICO8_PALETTE[grid.alphabet.getByID(0)];
424 | ctx.fillRect(0, 0, grid.width * scale, grid.height * scale);
425 | grid.listen((minX, minY, maxX, maxY) => {
426 | for (let y = minY; y < maxY; ++y) {
427 | for (let x = minX; x < maxX; ++x) {
428 | ctx.fillStyle = PICO8_PALETTE[grid.get(x, y)] ?? 'black';
429 | ctx.fillRect(x * scale, y * scale, scale, scale);
430 | }
431 | }
432 | });
433 | }
434 | /**
435 | * Builds a pair of DFAs which can be used to match 2D patterns. The `rowDFA`
436 | * recognises pattern rows, and the `colDFA` recognises sequences of pattern
437 | * rows matched by the `rowDFA`.
438 | *
439 | * The DFAs recognise the patterns in reverse order, for convenience so that
440 | * matches are reported where the patterns start rather than where they end.
441 | */
442 | class PatternMatcher {
443 | alphabet;
444 | /**
445 | * The number of patterns recognised by this matcher.
446 | */
447 | numPatterns;
448 | /**
449 | * The DFA which recognises rows of patterns.
450 | */
451 | rowDFA;
452 | /**
453 | * The DFA which recognises sequences of matches from `rowDFA`.
454 | */
455 | colDFA;
456 | acceptSetMapSize;
457 | acceptSetDiffs;
458 | constructor(
459 | /**
460 | * The alphabet of symbols which can appear in patterns recognised by this matcher.
461 | */
462 | alphabet, patterns) {
463 | this.alphabet = alphabet;
464 | const numPatterns = this.numPatterns = patterns.size();
465 | const rowPatterns = IDMap.ofWithKey(patterns.map(p => p.rows()).flat(), Pattern.key);
466 | const rowRegex = Regex.concat([
467 | Regex.kleeneStar(Regex.wildcard()),
468 | Regex.union(rowPatterns.map((row, rowID) => Regex.concat([
469 | Regex.concat(row.rasterData.map(c => c < 0 ? Regex.wildcard() : Regex.letters([c])).reverse()),
470 | Regex.accept(rowID),
471 | ]))),
472 | ]);
473 | this.rowDFA = Regex.compile(alphabet.size(), rowPatterns.size(), rowRegex);
474 | const acceptingSets = makeArray(rowPatterns.size(), () => []);
475 | this.rowDFA.acceptSetMap.forEach((xs, id) => {
476 | for (const x of xs) {
477 | acceptingSets[x].push(id);
478 | }
479 | });
480 | const colRegex = Regex.concat([
481 | Regex.kleeneStar(Regex.wildcard()),
482 | Regex.union(patterns.map((pattern, patternID) => Regex.concat([
483 | Regex.concat(pattern.rows().map(row => {
484 | const rowID = rowPatterns.getID(row);
485 | return Regex.letters(acceptingSets[rowID]);
486 | }).reverse()),
487 | Regex.accept(patternID),
488 | ]))),
489 | ]);
490 | this.colDFA = Regex.compile(this.rowDFA.acceptSetMap.size(), numPatterns, colRegex);
491 | // precompute set differences, so that new/broken matches can be iterated in O(1) time per match
492 | const { acceptSetMap } = this.colDFA;
493 | this.acceptSetMapSize = acceptSetMap.size();
494 | const diffs = this.acceptSetDiffs = [];
495 | acceptSetMap.forEach(q => {
496 | const qSet = ISet.of(numPatterns, q);
497 | acceptSetMap.forEach(p => {
498 | const arr = p.filter(x => !ISet.has(qSet, x));
499 | diffs.push(arr);
500 | });
501 | });
502 | }
503 | getAcceptSetDiff(pState, qState) {
504 | const { colDFA, acceptSetMapSize: k } = this;
505 | const pID = colDFA.getAcceptSetID(pState), qID = colDFA.getAcceptSetID(qState);
506 | return this.acceptSetDiffs[pID + k * qID];
507 | }
508 | makeState(width, height) {
509 | return new MatcherState(this, width, height);
510 | }
511 | }
512 | class MatcherState {
513 | matcher;
514 | grid;
515 | /**
516 | * Maps each `grid.index(x, y)` to the row-DFA state at (x, y).
517 | */
518 | rowStates;
519 | /**
520 | * Maps each `grid.index(x, y)` to the column-DFA state at (x, y).
521 | */
522 | colStates;
523 | /**
524 | * Maps each pattern ID to the set of indices `grid.index(x, y)` where that pattern is matched at (x, y).
525 | *
526 | * Invariant: `matchIndices[p].has(i)` if and only if `matcher.colDFA` accepts `p` at state `colStates[i]`
527 | */
528 | matchIndices;
529 | constructor(matcher, width, height) {
530 | this.matcher = matcher;
531 | const n = width * height;
532 | this.rowStates = makeUintArray(n, matcher.rowDFA.size());
533 | this.colStates = makeUintArray(n, matcher.colDFA.size());
534 | this.matchIndices = makeArray(matcher.numPatterns, () => new SampleableSet(n));
535 | const grid = this.grid = new Grid(matcher.alphabet, width, height);
536 | grid.listen(this.recompute.bind(this));
537 | this.recompute(0, 0, width, height);
538 | }
539 | /**
540 | * Returns the number of times the given pattern matches this grid, in O(1) time.
541 | */
542 | countMatches(patternID) {
543 | return this.matchIndices[patternID].size();
544 | }
545 | /**
546 | * Returns the coordinates of a random match of the given pattern, in O(1) time,
547 | * or `undefined` if there are no matches.
548 | */
549 | getRandomMatch(patternID) {
550 | const index = this.matchIndices[patternID].sample();
551 | if (index !== undefined) {
552 | const { width } = this.grid;
553 | return {
554 | x: index % width,
555 | y: Math.floor(index / width),
556 | };
557 | }
558 | else {
559 | return undefined;
560 | }
561 | }
562 | /**
563 | * Updates the state to account for changes in the rectangular area from
564 | * startX/Y (inclusive) to endX/Y (exclusive).
565 | */
566 | recompute(startX, startY, endX, endY) {
567 | const { matcher, grid, rowStates, colStates, matchIndices } = this;
568 | const { rowDFA, colDFA } = matcher;
569 | const { width, height } = grid;
570 | // the pattern matching is done in reverse, for convenience so that
571 | // matches are accepted where the patterns start rather than where they end
572 | // recompute rowStates
573 | let minChangedX = startX;
574 | for (let y = startY; y < endY; ++y) {
575 | let state = endX === width ? 0 : rowStates[grid.index(endX, y)];
576 | for (let x = endX - 1; x >= 0; --x) {
577 | // O(1) time per iteration
578 | const index = grid.index(x, y);
579 | state = rowDFA.go(state, grid.grid[index]);
580 | if (state !== rowStates[index]) {
581 | rowStates[index] = state;
582 | minChangedX = Math.min(minChangedX, x);
583 | }
584 | else if (x < startX) {
585 | break;
586 | }
587 | }
588 | }
589 | // recompute colStates
590 | for (let x = minChangedX; x < endX; ++x) {
591 | let state = endY === height ? 0 : colStates[grid.index(x, endY)];
592 | for (let y = endY - 1; y >= 0; --y) {
593 | // O(m + 1) time per iteration, where m is the number of new + broken matches
594 | const index = grid.index(x, y);
595 | const acceptSetID = rowDFA.getAcceptSetID(rowStates[index]);
596 | state = colDFA.go(state, acceptSetID);
597 | const oldState = colStates[index];
598 | if (state !== oldState) {
599 | colStates[index] = state;
600 | // remove broken matches
601 | for (const acceptID of matcher.getAcceptSetDiff(oldState, state)) {
602 | matchIndices[acceptID].delete(index);
603 | }
604 | // add new matches
605 | for (const acceptID of matcher.getAcceptSetDiff(state, oldState)) {
606 | matchIndices[acceptID].add(index);
607 | }
608 | }
609 | else if (y < startY) {
610 | break;
611 | }
612 | }
613 | }
614 | }
615 | }
616 | ///
617 | class Grid {
618 | alphabet;
619 | width;
620 | height;
621 | /**
622 | * Maps each `index(x, y)` to the ID of the symbol at (x, y).
623 | */
624 | grid;
625 | /**
626 | * Array of listeners which will be notified after any area of the grid has changed.
627 | */
628 | onChange = [];
629 | constructor(alphabet, width, height) {
630 | this.alphabet = alphabet;
631 | this.width = width;
632 | this.height = height;
633 | this.grid = makeUintArray(width * height, alphabet.size());
634 | }
635 | index(x, y) {
636 | if (x < 0 || x >= this.width || y < 0 || y >= this.height) {
637 | throw new Error(`Out of bounds: ${x},${y}`);
638 | }
639 | return x + y * this.width;
640 | }
641 | get(x, y) {
642 | const c = this.grid[this.index(x, y)];
643 | return this.alphabet.getByID(c);
644 | }
645 | set(x, y, value) {
646 | this.grid[this.index(x, y)] = this.alphabet.getID(value);
647 | this.notify(x, y, x + 1, y + 1);
648 | }
649 | /**
650 | * Writes a pattern into the grid, starting at the coordinates (x, y).
651 | */
652 | setPattern(x, y, pattern) {
653 | const { grid } = this;
654 | const { vectorData, minX, minY, maxX, maxY } = pattern;
655 | for (let i = 0; i < vectorData.length; i += 3) {
656 | const dx = vectorData[i];
657 | const dy = vectorData[i + 1];
658 | const c = vectorData[i + 2];
659 | grid[this.index(x + dx, y + dy)] = c;
660 | }
661 | this.notify(x + minX, y + minY, x + maxX, y + maxY);
662 | }
663 | /**
664 | * Registers a callback function, which will be called whenever the grid's
665 | * contents change.
666 | */
667 | listen(f) {
668 | this.onChange.push(f);
669 | }
670 | /**
671 | * Notifies listeners of changes in the rectangular area from startX/Y
672 | * (inclusive) to endX/Y (exclusive).
673 | */
674 | notify(startX, startY, endX, endY) {
675 | for (const f of this.onChange) {
676 | f(startX, startY, endX, endY);
677 | }
678 | }
679 | }
680 | /**
681 | * Assigns unique, incremental IDs to a set of values.
682 | */
683 | class IDMap {
684 | keyFunc;
685 | static IDENTITY = (x) => x;
686 | static empty() {
687 | return new IDMap(IDMap.IDENTITY);
688 | }
689 | static withKey(keyFunc) {
690 | return new IDMap(keyFunc);
691 | }
692 | /**
693 | * Creates a new IDMap with the distinct elements from `iterable`, with IDs
694 | * in order of first occurrence.
695 | */
696 | static of(iterable) {
697 | return IDMap.ofWithKey(iterable, IDMap.IDENTITY);
698 | }
699 | static ofWithKey(iterable, keyFunc) {
700 | const map = new IDMap(keyFunc);
701 | for (const x of iterable) {
702 | map.getOrCreateID(x);
703 | }
704 | return map;
705 | }
706 | /**
707 | * Returns a new array of the distinct elements from `iterable`, in order
708 | * of first occurrence.
709 | */
710 | static distinct(iterable) {
711 | return IDMap.of(iterable).arr;
712 | }
713 | /**
714 | * Returns a new array of the elements from `iterable`, deduplicated using
715 | * the given key function, in order of first occurrence. If multiple values
716 | * have the same key, only the first is included.
717 | */
718 | static distinctByKey(iterable, keyFunc) {
719 | return IDMap.ofWithKey(iterable, keyFunc).arr;
720 | }
721 | /**
722 | * The distinct elements in this map, in insertion order.
723 | */
724 | arr = [];
725 | /**
726 | * Maps elements to their indices in `arr`.
727 | *
728 | * Invariant: `ids.get(keyFunc(x)) === i` if and only if `arr[i] === x`
729 | */
730 | ids = new Map();
731 | constructor(keyFunc) {
732 | this.keyFunc = keyFunc;
733 | }
734 | /**
735 | * Returns the number of elements in the map.
736 | */
737 | size() {
738 | return this.arr.length;
739 | }
740 | /**
741 | * Adds an element to the map if it is not already present, and returns the
742 | * element's ID, in O(1) time.
743 | */
744 | getOrCreateID(x) {
745 | const key = this.keyFunc(x);
746 | let id = this.ids.get(key);
747 | if (id === undefined) {
748 | id = this.arr.length;
749 | this.arr.push(x);
750 | this.ids.set(key, id);
751 | }
752 | return id;
753 | }
754 | /**
755 | * Indicates whether the given element is associated with an ID, in O(1)
756 | * time.
757 | */
758 | has(x) {
759 | return this.ids.has(this.keyFunc(x));
760 | }
761 | /**
762 | * Returns the ID of the given element, in O(1) time. An error is thrown if
763 | * the element is not associated with an ID.
764 | */
765 | getID(x) {
766 | const id = this.ids.get(this.keyFunc(x));
767 | if (id === undefined) {
768 | throw new Error();
769 | }
770 | return id;
771 | }
772 | /**
773 | * Returns the ID of the given element, or -1 if the given element is not
774 | * associated with an ID, in O(1) time.
775 | */
776 | getIDOrDefault(x) {
777 | return this.ids.get(this.keyFunc(x)) ?? -1;
778 | }
779 | /**
780 | * Returns the element associated with the given ID, in O(1) time. An error
781 | * is thrown if there is no element with the given ID.
782 | */
783 | getByID(id) {
784 | if (id < 0 || id >= this.arr.length) {
785 | throw new Error();
786 | }
787 | return this.arr[id];
788 | }
789 | forEach(f) {
790 | this.arr.forEach(f);
791 | }
792 | map(f) {
793 | return this.arr.map(f);
794 | }
795 | }
796 | /**
797 | * Helper functions for using a typed array as a set of natural numbers.
798 | *
799 | * Aggregate operations `addAll`, `toArray` and `forEach` are O(N), where N is
800 | * the domain size; therefore they must not be used in the pattern matching loop.
801 | */
802 | var ISet;
803 | (function (ISet) {
804 | /**
805 | * Creates an empty set, which can contain numbers `0 <= x < domainSize`.
806 | */
807 | function empty(domainSize) {
808 | return new Uint32Array(((domainSize - 1) >> 5) + 1);
809 | }
810 | ISet.empty = empty;
811 | /**
812 | * Creates a set containing the whole domain `0 <= x < domainSize`.
813 | */
814 | function full(domainSize) {
815 | const set = empty(domainSize);
816 | set.fill(-1);
817 | if ((domainSize & 31) !== 0) {
818 | set[set.length - 1] = (1 << (domainSize & 31)) - 1;
819 | }
820 | return set;
821 | }
822 | ISet.full = full;
823 | /**
824 | * Creates a set from an iterable of natural numbers, all of which must be
825 | * less than `domainSize`.
826 | */
827 | function of(domainSize, xs) {
828 | const set = empty(domainSize);
829 | for (const x of xs) {
830 | add(set, x);
831 | }
832 | return set;
833 | }
834 | ISet.of = of;
835 | /**
836 | * Indicates whether `set` contains the element `x`, in O(1) time.
837 | */
838 | function has(set, x) {
839 | return (set[x >> 5] & (1 << (x & 31))) !== 0;
840 | }
841 | ISet.has = has;
842 | /**
843 | * Returns the size of the set, in O(N) time.
844 | */
845 | function size(set) {
846 | let count = 0;
847 | for (let x of set) {
848 | while (x !== 0) {
849 | x &= x - 1;
850 | ++count;
851 | }
852 | }
853 | return count;
854 | }
855 | ISet.size = size;
856 | /**
857 | * Adds the element `x` to the set if it not already present, in O(1) time.
858 | */
859 | function add(set, x) {
860 | set[x >> 5] |= 1 << (x & 31);
861 | }
862 | ISet.add = add;
863 | /**
864 | * Adds all the members of the set `b` to the set `a`, in O(N) time.
865 | */
866 | function addAll(a, b) {
867 | if (a.length < b.length) {
868 | throw new Error();
869 | }
870 | for (let i = 0; i < b.length; ++i) {
871 | a[i] |= b[i];
872 | }
873 | }
874 | ISet.addAll = addAll;
875 | /**
876 | * Converts a set from an array to a `bigint`, in O(N^2) time.
877 | *
878 | * Using a primitive type is convenient for Map keys; `number` would only
879 | * work for sets with domain sizes of at most 32, and strings are slower.
880 | */
881 | function arrayToBigInt(xs) {
882 | let domainSize = 0;
883 | for (const x of xs) {
884 | domainSize = Math.max(domainSize, x + 1);
885 | }
886 | return domainSize > 0 ? toBigInt(of(domainSize, xs)) : 0n;
887 | }
888 | ISet.arrayToBigInt = arrayToBigInt;
889 | /**
890 | * Converts a set to a `bigint`, in O(N^2) time.
891 | *
892 | * Using a primitive type is convenient for Map keys; `number` would only
893 | * work for sets with domain sizes of at most 32, and strings are slower.
894 | */
895 | function toBigInt(set) {
896 | let r = 0n;
897 | for (let i = set.length - 1; i >= 0; --i) {
898 | r <<= 32n;
899 | r |= BigInt(set[i]);
900 | }
901 | return r;
902 | }
903 | ISet.toBigInt = toBigInt;
904 | /**
905 | * Returns a new array of the natural numbers in the given set, not
906 | * necessarily in order.
907 | */
908 | function toArray(set) {
909 | const arr = [];
910 | forEach(set, x => arr.push(x));
911 | return arr;
912 | }
913 | ISet.toArray = toArray;
914 | /**
915 | * Calls the function `f` for each element of the set, not necessarily in
916 | * order.
917 | */
918 | function forEach(set, f) {
919 | for (let i = 0; i < set.length; ++i) {
920 | const x = i << 5;
921 | let setPart = set[i];
922 | while (setPart !== 0) {
923 | // position of the highest 1 bit
924 | const dx = 31 - Math.clz32(setPart);
925 | // 'x ^ dx' is equivalent to `x + dx` here
926 | f(x ^ dx);
927 | // clear this bit
928 | setPart ^= 1 << dx;
929 | }
930 | }
931 | }
932 | ISet.forEach = forEach;
933 | })(ISet || (ISet = {}));
934 | ///
935 | ///
936 | function runDemo(size = 2) {
937 | const GRID_SIZE = (1 << 7) * size;
938 | const SPEED = 16 * size * size;
939 | const LAKE_SEEDS = 4;
940 | const LAKE_SIZE = (1 << 12) * size * size;
941 | const LAND_SEEDS = 32;
942 | const alphabet = IDMap.of('BWREI');
943 | const rules = [
944 | // make a few lakes by random growth
945 | rule('B', 'I', LAKE_SEEDS),
946 | rule('IB', '*I', LAKE_SIZE - LAKE_SEEDS),
947 | // make some land by a self-avoiding random walk with backtracking
948 | rule('B', 'R', LAND_SEEDS),
949 | rule('RBB', 'WWR'),
950 | rule('RWW', 'EER'),
951 | rule('R', 'E'),
952 | // erode narrow sections of land
953 | rule('BBWBB', '**B**'),
954 | // replace the solid lakes with isolated pixels
955 | rule('II', 'BB', LAKE_SIZE / 2),
956 | // fill unused space with a water texture
957 | rule('BB*/BBB/*B*', '***/*I*/***'),
958 | rule('*I*/IBI/*I*', '***/*I*/***'),
959 | // delete water pixels at random, for an animated effect
960 | rule('I', 'B'),
961 | ];
962 | function rule(patternIn, patternOut, limit) {
963 | return { patternIn, patternOut, limit };
964 | }
965 | const patternsIn = IDMap.withKey(Pattern.key);
966 | const patternsOut = IDMap.withKey(Pattern.key);
967 | const compiledRules = rules.map(spec => {
968 | const rewrites = Symmetry.generate(Pattern.of(alphabet, spec.patternIn), Pattern.of(alphabet, spec.patternOut)).map(([p, q]) => [
969 | patternsIn.getOrCreateID(p),
970 | patternsOut.getOrCreateID(q),
971 | ]);
972 | return { rewrites, limit: spec.limit };
973 | });
974 | function applyRule(state, rule) {
975 | if (rule.limit !== undefined && rule.limit <= 0) {
976 | return false;
977 | }
978 | const { rewrites } = rule;
979 | const counts = rewrites.map(pair => state.countMatches(pair[0]));
980 | const totalCount = counts.reduce((a, b) => a + b, 0);
981 | if (totalCount === 0) {
982 | return false;
983 | }
984 | let r = rng(totalCount);
985 | for (let i = 0; i < counts.length; ++i) {
986 | r -= counts[i];
987 | if (r < 0) {
988 | const [pID, qID] = rewrites[i];
989 | const pos = state.getRandomMatch(pID);
990 | state.grid.setPattern(pos.x, pos.y, patternsOut.getByID(qID));
991 | if (rule.limit !== undefined) {
992 | --rule.limit;
993 | }
994 | return true;
995 | }
996 | }
997 | throw new Error();
998 | }
999 | function step(state, rules, k) {
1000 | let changed = false;
1001 | for (let i = 0; i < k; ++i) {
1002 | changed = rules.some(r => applyRule(state, r));
1003 | if (!changed) {
1004 | break;
1005 | }
1006 | }
1007 | return changed;
1008 | }
1009 | const state = new PatternMatcher(alphabet, patternsIn).makeState(GRID_SIZE, GRID_SIZE);
1010 | const scale = Math.max(1, Math.floor(window.innerHeight / state.grid.height));
1011 | displayGrid(state.grid, scale);
1012 | function frameHandler() {
1013 | if (step(state, compiledRules, SPEED)) {
1014 | requestAnimationFrame(frameHandler);
1015 | }
1016 | }
1017 | requestAnimationFrame(frameHandler);
1018 | }
1019 | /**
1020 | * Creates an empty array of length `n`, which can hold unsigned integers less
1021 | * than `domainSize` (exclusive). The array is initially filled with zeroes.
1022 | */
1023 | function makeUintArray(n, domainSize) {
1024 | if (domainSize <= (1 << 8)) {
1025 | return new Uint8Array(n);
1026 | }
1027 | else if (domainSize <= (1 << 16)) {
1028 | return new Uint16Array(n);
1029 | }
1030 | else {
1031 | return new Uint32Array(n);
1032 | }
1033 | }
1034 | /**
1035 | * Creates an empty array of length `n`, filled with the given value.
1036 | */
1037 | function emptyArray(n, value) {
1038 | return makeArray(n, () => value);
1039 | }
1040 | /**
1041 | * Creates an array of length `n`, initialised using the given callback function.
1042 | */
1043 | function makeArray(n, f) {
1044 | // equivalent to `Array(n).map((_, i) => f(i))`, but guarantees an array without holes, which may be more performant to use
1045 | const arr = [];
1046 | for (let i = 0; i < n; ++i) {
1047 | arr.push(f(i));
1048 | }
1049 | return arr;
1050 | }
1051 | /**
1052 | * Returns a random integer from 0 to n - 1.
1053 | */
1054 | function rng(n) {
1055 | return Math.floor(Math.random() * n);
1056 | }
1057 | /**
1058 | * A small rectangular pattern which can be matched in a grid, or written to it.
1059 | * Patterns may contain wildcards, which match any symbol and do not write
1060 | * anything to the grid.
1061 | */
1062 | class Pattern {
1063 | width;
1064 | height;
1065 | rasterData;
1066 | /**
1067 | * Creates a pattern from a string.
1068 | *
1069 | * The pattern is specified by a string with rows separated by `/`; wildcards
1070 | * `*` in the pattern match any symbol and do not write anything to the grid.
1071 | */
1072 | static of(alphabet, pattern) {
1073 | const rows = pattern.split('/');
1074 | const width = rows[0].length;
1075 | const height = rows.length;
1076 | if (rows.some(row => row.length !== width)) {
1077 | throw new Error(pattern);
1078 | }
1079 | function symbolToID(c) {
1080 | return c === '*' ? -1 : alphabet.getID(c);
1081 | }
1082 | const rasterData = rows.flatMap(row => [...row].map(symbolToID));
1083 | return new Pattern(width, height, rasterData);
1084 | }
1085 | /**
1086 | * Rotates a pattern clockwise by 90 degrees.
1087 | */
1088 | static rotate(pattern) {
1089 | const { width, height, rasterData } = pattern;
1090 | const newData = [];
1091 | for (let x = 0; x < width; ++x) {
1092 | for (let y = height - 1; y >= 0; --y) {
1093 | newData.push(rasterData[x + width * y]);
1094 | }
1095 | }
1096 | return new Pattern(height, width, newData);
1097 | }
1098 | /**
1099 | * Reflects a pattern from top to bottom.
1100 | */
1101 | static reflect(pattern) {
1102 | const { width, height, rasterData } = pattern;
1103 | const newData = [];
1104 | for (let y = height - 1; y >= 0; --y) {
1105 | for (let x = 0; x < width; ++x) {
1106 | newData.push(rasterData[x + width * y]);
1107 | }
1108 | }
1109 | return new Pattern(width, height, newData);
1110 | }
1111 | /**
1112 | * Returns a string representation of a pattern, for use as a Map key.
1113 | */
1114 | static key(pattern) {
1115 | return pattern._key ??= `${pattern.width}:${pattern.height}:${pattern.rasterData.join(',')}`;
1116 | }
1117 | /**
1118 | * The cached key; see `Pattern.key`.
1119 | */
1120 | _key = undefined;
1121 | /**
1122 | * A flat array of (x, y, c) triples for each occurrence of a non-wildcard
1123 | * symbol `c` at a position (x, y) in this pattern.
1124 | */
1125 | vectorData;
1126 | minX;
1127 | minY;
1128 | maxX;
1129 | maxY;
1130 | constructor(
1131 | /**
1132 | * The width of the pattern.
1133 | */
1134 | width,
1135 | /**
1136 | * The height of the pattern.
1137 | */
1138 | height,
1139 | /**
1140 | * The cells of the pattern. A value of -1 indicates a wildcard.
1141 | */
1142 | rasterData) {
1143 | this.width = width;
1144 | this.height = height;
1145 | this.rasterData = rasterData;
1146 | let minX = width, minY = height, maxX = 0, maxY = 0;
1147 | const vectorData = this.vectorData = [];
1148 | for (let y = 0; y < height; ++y) {
1149 | for (let x = 0; x < width; ++x) {
1150 | const c = rasterData[x + width * y];
1151 | if (c >= 0) {
1152 | vectorData.push(x, y, c);
1153 | minX = Math.min(minX, x);
1154 | minY = Math.min(minY, y);
1155 | maxX = Math.max(maxX, x + 1);
1156 | maxY = Math.max(maxY, y + 1);
1157 | }
1158 | }
1159 | }
1160 | this.minX = Math.min(minX, maxX);
1161 | this.minY = Math.min(minY, maxY);
1162 | this.maxX = maxX;
1163 | this.maxY = maxY;
1164 | }
1165 | /**
1166 | * Returns the rows of this pattern as an array of (width * 1) patterns.
1167 | */
1168 | rows() {
1169 | const { width, height, rasterData } = this;
1170 | const out = [];
1171 | for (let y = 0; y < height; ++y) {
1172 | const row = rasterData.slice(y * width, (y + 1) * width);
1173 | out.push(new Pattern(width, 1, row));
1174 | }
1175 | return out;
1176 | }
1177 | }
1178 | /**
1179 | * A mutable set which can be randomly sampled in O(1) time.
1180 | */
1181 | class SampleableSet {
1182 | constructor(domainSize) { }
1183 | /**
1184 | * An unordered array of the set's members.
1185 | */
1186 | arr = [];
1187 | /**
1188 | * Maps the set's members to their indices in `arr`.
1189 | *
1190 | * Invariant: `arr[i] === x` if and only if `indices.get(x) === i`
1191 | */
1192 | indices = new Map();
1193 | /**
1194 | * Returns the number of elements in the set.
1195 | */
1196 | size() {
1197 | return this.arr.length;
1198 | }
1199 | /**
1200 | * Indicates whether the given value is a member of the set, in O(1) time.
1201 | */
1202 | has(x) {
1203 | return this.indices.has(x);
1204 | }
1205 | /**
1206 | * Adds an element to the set, if it is not already present, in O(1) time.
1207 | */
1208 | add(x) {
1209 | const { arr, indices } = this;
1210 | if (!indices.has(x)) {
1211 | indices.set(x, arr.length);
1212 | arr.push(x);
1213 | }
1214 | }
1215 | /**
1216 | * Deletes an element from the set, if it is present, in O(1) time.
1217 | */
1218 | delete(x) {
1219 | const { arr, indices } = this;
1220 | const i = indices.get(x);
1221 | if (i !== undefined) {
1222 | const j = arr.length - 1;
1223 | if (i !== j) {
1224 | const y = arr[j];
1225 | arr[i] = y;
1226 | indices.set(y, i);
1227 | }
1228 | arr.pop();
1229 | indices.delete(x);
1230 | }
1231 | }
1232 | /**
1233 | * Returns a random element from the set in O(1) time, or `undefined` if
1234 | * the set is empty.
1235 | */
1236 | sample() {
1237 | const { arr } = this;
1238 | return arr.length > 0 ? arr[rng(arr.length)] : undefined;
1239 | }
1240 | }
1241 | ///
1242 | var Symmetry;
1243 | (function (Symmetry) {
1244 | const GENERATING_SET = [Pattern.rotate, Pattern.reflect];
1245 | function generate(patternIn, patternOut, symmetries = GENERATING_SET) {
1246 | // depth-first search
1247 | const stack = [[patternIn, patternOut]];
1248 | const entries = new Map();
1249 | // TODO: key should include patternOut
1250 | entries.set(Pattern.key(patternIn), [patternIn, patternOut]);
1251 | while (stack.length > 0) {
1252 | const [p, q] = stack.pop();
1253 | for (const f of symmetries) {
1254 | const pSym = f(p);
1255 | const key = Pattern.key(pSym);
1256 | if (!entries.has(key)) {
1257 | const pair = [pSym, f(q)];
1258 | entries.set(key, pair);
1259 | stack.push(pair);
1260 | }
1261 | }
1262 | }
1263 | return [...entries.values()];
1264 | }
1265 | Symmetry.generate = generate;
1266 | })(Symmetry || (Symmetry = {}));
1267 |
--------------------------------------------------------------------------------
/src/dfa.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | namespace Regex {
4 | export const enum Kind {
5 | LETTERS,
6 | WILDCARD,
7 | CONCAT,
8 | UNION,
9 | KLEENESTAR,
10 | ACCEPT,
11 | }
12 |
13 | export type Node = Readonly<
14 | | {kind: Kind.LETTERS, letterIDs: readonly number[]}
15 | | {kind: Kind.WILDCARD}
16 | | {kind: Kind.CONCAT, children: readonly Node[]}
17 | | {kind: Kind.UNION, children: readonly Node[]}
18 | | {kind: Kind.KLEENESTAR, child: Node}
19 | | {kind: Kind.ACCEPT, accept: number}
20 | >
21 |
22 | export function letters(letterIDs: readonly number[]): Node {
23 | return {kind: Kind.LETTERS, letterIDs};
24 | }
25 | export function wildcard(): Node {
26 | return {kind: Kind.WILDCARD};
27 | }
28 | export function concat(children: Node[]): Node {
29 | return {kind: Kind.CONCAT, children};
30 | }
31 | export function union(children: Node[]): Node {
32 | return {kind: Kind.UNION, children};
33 | }
34 | export function kleeneStar(child: Node): Node {
35 | return {kind: Kind.KLEENESTAR, child};
36 | }
37 | export function accept(accept: number): Node {
38 | return {kind: Kind.ACCEPT, accept};
39 | }
40 |
41 | export function compile(alphabetSize: number, acceptCount: number, regex: Node): DFA {
42 | return new NFA(alphabetSize, acceptCount, regex).toDFA().minimise();
43 | }
44 | }
45 |
46 | type NFANode = Readonly<{
47 | epsilons: number[],
48 | letters: readonly number[],
49 | nextID: number,
50 | acceptSet: number[],
51 | }>
52 |
53 | class NFA {
54 | private readonly nodes: NFANode[] = [];
55 | private readonly startID: number;
56 | public constructor(
57 | private readonly alphabetSize: number,
58 | private readonly acceptCount: number,
59 | regex: Regex.Node,
60 | ) {
61 | this.startID = this.makeFromRegex(regex, this.makeNode([]));
62 | //console.log(`NFA with ${this.nodes.length} nodes on alphabet of size ${alphabetSize}`);
63 | }
64 |
65 | private makeNode(epsilons: number[]): number;
66 | private makeNode(epsilons: number[], letters: readonly number[], nextID: number): number;
67 | private makeNode(epsilons: number[], letters: readonly number[] = [], nextID: number = -1): number {
68 | const {nodes} = this;
69 | const id = nodes.length;
70 | nodes.push({epsilons, letters, nextID, acceptSet: []});
71 | return id;
72 | }
73 |
74 | private makeFromRegex(regex: Regex.Node, outID: number): number {
75 | // https://en.wikipedia.org/wiki/Thompson's_construction
76 | switch(regex.kind) {
77 | case Regex.Kind.LETTERS: {
78 | return this.makeNode([], regex.letterIDs, outID);
79 | }
80 | case Regex.Kind.WILDCARD: {
81 | return this.makeNode([], makeArray(this.alphabetSize, i => i), outID);
82 | }
83 | case Regex.Kind.CONCAT: {
84 | const {children} = regex;
85 | for(let i = children.length - 1; i >= 0; --i) {
86 | outID = this.makeFromRegex(children[i], outID);
87 | }
88 | return outID;
89 | }
90 | case Regex.Kind.UNION: {
91 | const epsilons = regex.children.map(child => this.makeFromRegex(child, this.makeNode([outID])));
92 | return this.makeNode(epsilons);
93 | }
94 | case Regex.Kind.KLEENESTAR: {
95 | const childOutID = this.makeNode([outID]);
96 | const childInID = this.makeFromRegex(regex.child, childOutID);
97 | this.nodes[childOutID].epsilons.push(childInID);
98 | return this.makeNode([childInID, outID]);
99 | }
100 | case Regex.Kind.ACCEPT: {
101 | const node = this.nodes[outID];
102 | node.acceptSet.push(regex.accept);
103 | return outID;
104 | }
105 | }
106 | }
107 |
108 | public toDFA(): DFA {
109 | // https://en.wikipedia.org/wiki/Powerset_construction
110 |
111 | const {alphabetSize, nodes} = this;
112 | // need to use a primitive key which will be compared by value; bigint is faster than sorting and joining as a string
113 | const nfaStates: IDMap = IDMap.withKey(ISet.toBigInt);
114 | const dfaNodes: DFANode[] = [];
115 |
116 | function getNodeID(nfaState: MutableISet): number {
117 | // epsilon closure, by depth-first search
118 | // use ISet instead of Set or bigint for the state, for performance
119 | const stack = ISet.toArray(nfaState);
120 | while(stack.length > 0) {
121 | const nfaNodeID = stack.pop()!;
122 | for(const eps of nodes[nfaNodeID].epsilons) {
123 | if(!ISet.has(nfaState, eps)) {
124 | ISet.add(nfaState, eps);
125 | stack.push(eps);
126 | }
127 | }
128 | }
129 |
130 | return nfaStates.getOrCreateID(nfaState);
131 | }
132 |
133 | const startID = getNodeID(ISet.of(nodes.length, [this.startID]));
134 | // sanity check
135 | if(startID !== 0) { throw new Error(); }
136 |
137 | const acceptSetMap: IDMap = IDMap.withKey(ISet.arrayToBigInt);
138 |
139 | // this loop iterates over `nfaStates`, while adding to it via `getNodeID`
140 | for(let nfaStateID = 0; nfaStateID < nfaStates.size(); ++nfaStateID) {
141 | const transitionStates = makeArray(alphabetSize, () => ISet.empty(nodes.length));
142 | const acceptIDs: number[] = [];
143 | ISet.forEach(nfaStates.getByID(nfaStateID), nfaNodeID => {
144 | const nfaNode = nodes[nfaNodeID];
145 | for(const letterID of nfaNode.letters) {
146 | ISet.add(transitionStates[letterID], nfaNode.nextID);
147 | }
148 | acceptIDs.push(...nfaNode.acceptSet);
149 | });
150 |
151 | dfaNodes.push({
152 | transitions: transitionStates.map(getNodeID),
153 | acceptSetID: acceptSetMap.getOrCreateID(acceptIDs),
154 | acceptIDs,
155 | });
156 | }
157 |
158 | return new DFA(alphabetSize, this.acceptCount, acceptSetMap, dfaNodes);
159 | }
160 | }
161 |
162 | type DFANode = Readonly<{
163 | transitions: readonly number[],
164 | acceptSetID: number,
165 | acceptIDs: readonly number[],
166 | }>
167 |
168 | class DFA {
169 | public constructor(
170 | private readonly alphabetSize: number,
171 | public readonly acceptCount: number,
172 | public readonly acceptSetMap: IDMap,
173 | private readonly nodes: readonly DFANode[],
174 | ) {
175 | //console.log(`DFA with ${nodes.length} nodes on alphabet of size ${alphabetSize}, ${acceptCount} accepts and ${acceptSetMap.size()} accept sets`);
176 | }
177 |
178 | /**
179 | * Returns the number of distinct states of this DFA.
180 | */
181 | public size(): number {
182 | return this.nodes.length;
183 | }
184 |
185 | public go(state: number, letterID: number): number {
186 | const {nodes, alphabetSize} = this;
187 | if(state >= 0 && state < nodes.length && letterID >= 0 && letterID < alphabetSize) {
188 | return nodes[state].transitions[letterID];
189 | } else {
190 | throw new Error();
191 | }
192 | }
193 |
194 | public getAcceptIDs(state: number): readonly number[] {
195 | return this.nodes[state].acceptIDs;
196 | }
197 |
198 | public getAcceptSetID(state: number): number {
199 | return this.nodes[state].acceptSetID;
200 | }
201 |
202 | /**
203 | * Returns an array mapping each acceptID to the set of node IDs which accept it.
204 | */
205 | private computeAcceptingStates(): Iterable {
206 | const {nodes, acceptCount} = this;
207 | const n = nodes.length;
208 | const table: MutableISet[] = makeArray(acceptCount, () => ISet.empty(n));
209 | for(let id = 0; id < n; ++id) {
210 | for(const acceptID of nodes[id].acceptIDs) {
211 | ISet.add(table[acceptID], id);
212 | }
213 | }
214 | return table;
215 | }
216 |
217 | /**
218 | * Returns an equivalent DFA with the minimum possible number of states.
219 | */
220 | public minimise(): DFA {
221 | // https://en.wikipedia.org/wiki/DFA_minimization#Hopcroft's_algorithm
222 |
223 | const {alphabetSize, nodes} = this;
224 |
225 | const n = nodes.length;
226 | const inverseTransitions = makeArray(alphabetSize * n, () => ISet.empty(n));
227 | for(let id = 0; id < n; ++id) {
228 | const {transitions} = nodes[id];
229 | for(let c = 0; c < alphabetSize; ++c) {
230 | ISet.add(inverseTransitions[c * n + transitions[c]], id);
231 | }
232 | }
233 |
234 | const partition = new Partition(n);
235 | for(const d of this.computeAcceptingStates()) { partition.refine(d); }
236 |
237 | while(true) {
238 | const a = partition.pollUnprocessed();
239 | if(a === undefined) { break; }
240 |
241 | for(let c = 0; c < alphabetSize; ++c) {
242 | const x = ISet.empty(n);
243 | for(const id of a) {
244 | ISet.addAll(x, inverseTransitions[c * n + id]);
245 | }
246 | partition.refine(x);
247 |
248 | // shortcut if the DFA cannot be minimised
249 | if(partition.countSubsets() === n) { return this; }
250 | }
251 | }
252 |
253 | const reps: IDMap = IDMap.withKey(id => partition.getRepresentative(id));
254 | // ensure id(rep(0)) === 0, so that 0 is still the starting state
255 | reps.getOrCreateID(0);
256 | partition.forEachRepresentative(x => reps.getOrCreateID(x));
257 |
258 | const repNodes: DFANode[] = reps.map(rep => {
259 | const {transitions, acceptSetID, acceptIDs} = this.nodes[rep];
260 | return {
261 | transitions: transitions.map(nodeID => reps.getID(nodeID)),
262 | acceptSetID,
263 | acceptIDs,
264 | };
265 | });
266 | return new DFA(alphabetSize, this.acceptCount, this.acceptSetMap, repNodes);
267 | }
268 | }
269 |
--------------------------------------------------------------------------------
/src/display.ts:
--------------------------------------------------------------------------------
1 | // https://lospec.com/palette-list/pico-8
2 | const PICO8_PALETTE: IRecord = {
3 | B: '#000000',
4 | I: '#1D2B53',
5 | P: '#7E2553',
6 | E: '#008751',
7 | N: '#AB5236',
8 | D: '#5F574F',
9 | A: '#C2C3C7',
10 | W: '#FFF1E8',
11 | R: '#FF004D',
12 | O: '#FFA300',
13 | Y: '#FFEC27',
14 | G: '#00E436',
15 | U: '#29ADFF',
16 | S: '#83769C',
17 | K: '#FF77A8',
18 | F: '#FFCCAA',
19 | } as const;
20 |
21 | function displayGrid(grid: Grid, scale: number = 8) {
22 | const canvasElem = document.createElement('canvas');
23 | canvasElem.width = grid.width * scale;
24 | canvasElem.height = grid.height * scale;
25 | document.body.appendChild(canvasElem);
26 | const ctx = canvasElem.getContext('2d')!;
27 | ctx.fillStyle = PICO8_PALETTE[grid.alphabet.getByID(0)];
28 | ctx.fillRect(0, 0, grid.width * scale, grid.height * scale);
29 |
30 | grid.listen((minX, minY, maxX, maxY) => {
31 | for(let y = minY; y < maxY; ++y) {
32 | for(let x = minX; x < maxX; ++x) {
33 | ctx.fillStyle = PICO8_PALETTE[grid.get(x, y)] ?? 'black';
34 | ctx.fillRect(x * scale, y * scale, scale, scale);
35 | }
36 | }
37 | });
38 | }
39 |
--------------------------------------------------------------------------------
/src/grid.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | type GridChangeListener = (minX: number, minY: number, maxX: number, maxY: number) => void
4 |
5 | class Grid {
6 | /**
7 | * Maps each `index(x, y)` to the ID of the symbol at (x, y).
8 | */
9 | public readonly grid: UintArray;
10 |
11 | /**
12 | * Array of listeners which will be notified after any area of the grid has changed.
13 | */
14 | private readonly onChange: GridChangeListener[] = [];
15 |
16 | public constructor(
17 | public readonly alphabet: IDMap,
18 | public readonly width: number,
19 | public readonly height: number,
20 | ) {
21 | this.grid = makeUintArray(width * height, alphabet.size());
22 | }
23 |
24 | public index(x: number, y: number): number {
25 | if(x < 0 || x >= this.width || y < 0 || y >= this.height) {
26 | throw new Error(`Out of bounds: ${x},${y}`);
27 | }
28 | return x + y * this.width;
29 | }
30 |
31 | public get(x: number, y: number): string {
32 | const c = this.grid[this.index(x, y)];
33 | return this.alphabet.getByID(c);
34 | }
35 | public set(x: number, y: number, value: string): void {
36 | this.grid[this.index(x, y)] = this.alphabet.getID(value);
37 | this.notify(x, y, x + 1, y + 1);
38 | }
39 |
40 | /**
41 | * Writes a pattern into the grid, starting at the coordinates (x, y).
42 | */
43 | public setPattern(x: number, y: number, pattern: Pattern): void {
44 | const {grid} = this;
45 | const {vectorData, minX, minY, maxX, maxY} = pattern;
46 |
47 | for(let i = 0; i < vectorData.length; i += 3) {
48 | const dx = vectorData[i];
49 | const dy = vectorData[i + 1];
50 | const c = vectorData[i + 2];
51 | grid[this.index(x + dx, y + dy)] = c;
52 | }
53 |
54 | this.notify(x + minX, y + minY, x + maxX, y + maxY);
55 | }
56 |
57 | /**
58 | * Registers a callback function, which will be called whenever the grid's
59 | * contents change.
60 | */
61 | public listen(f: GridChangeListener): void {
62 | this.onChange.push(f);
63 | }
64 |
65 | /**
66 | * Notifies listeners of changes in the rectangular area from startX/Y
67 | * (inclusive) to endX/Y (exclusive).
68 | */
69 | private notify(startX: number, startY: number, endX: number, endY: number): void {
70 | for(const f of this.onChange) {
71 | f(startX, startY, endX, endY);
72 | }
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/idmap.ts:
--------------------------------------------------------------------------------
1 | type PrimitiveKey = string | number | bigint
2 |
3 | /**
4 | * Assigns unique, incremental IDs to a set of values.
5 | */
6 | class IDMap {
7 | private static readonly IDENTITY = (x: T): T => x;
8 |
9 | public static empty(): IDMap {
10 | return new IDMap(IDMap.IDENTITY);
11 | }
12 |
13 | public static withKey(keyFunc: (x: T) => PrimitiveKey): IDMap {
14 | return new IDMap(keyFunc);
15 | }
16 |
17 | /**
18 | * Creates a new IDMap with the distinct elements from `iterable`, with IDs
19 | * in order of first occurrence.
20 | */
21 | public static of(iterable: Iterable): IDMap {
22 | return IDMap.ofWithKey(iterable, IDMap.IDENTITY);
23 | }
24 |
25 | public static ofWithKey(iterable: Iterable, keyFunc: (x: T) => PrimitiveKey): IDMap {
26 | const map = new IDMap(keyFunc);
27 | for(const x of iterable) { map.getOrCreateID(x); }
28 | return map;
29 | }
30 |
31 | /**
32 | * Returns a new array of the distinct elements from `iterable`, in order
33 | * of first occurrence.
34 | */
35 | public static distinct(iterable: Iterable): T[] {
36 | return IDMap.of(iterable).arr;
37 | }
38 |
39 | /**
40 | * Returns a new array of the elements from `iterable`, deduplicated using
41 | * the given key function, in order of first occurrence. If multiple values
42 | * have the same key, only the first is included.
43 | */
44 | public static distinctByKey(iterable: Iterable, keyFunc: (x: T) => PrimitiveKey): T[] {
45 | return IDMap.ofWithKey(iterable, keyFunc).arr;
46 | }
47 |
48 | /**
49 | * The distinct elements in this map, in insertion order.
50 | */
51 | private readonly arr: T[] = [];
52 |
53 | /**
54 | * Maps elements to their indices in `arr`.
55 | *
56 | * Invariant: `ids.get(keyFunc(x)) === i` if and only if `arr[i] === x`
57 | */
58 | private readonly ids = new Map();
59 |
60 | private constructor(private readonly keyFunc: (x: T) => PrimitiveKey) {}
61 |
62 | /**
63 | * Returns the number of elements in the map.
64 | */
65 | public size(): number {
66 | return this.arr.length;
67 | }
68 |
69 | /**
70 | * Adds an element to the map if it is not already present, and returns the
71 | * element's ID, in O(1) time.
72 | */
73 | public getOrCreateID(x: T): number {
74 | const key = this.keyFunc(x);
75 | let id = this.ids.get(key);
76 | if(id === undefined) {
77 | id = this.arr.length;
78 | this.arr.push(x);
79 | this.ids.set(key, id);
80 | }
81 | return id;
82 | }
83 |
84 | /**
85 | * Indicates whether the given element is associated with an ID, in O(1)
86 | * time.
87 | */
88 | public has(x: T): boolean {
89 | return this.ids.has(this.keyFunc(x));
90 | }
91 |
92 | /**
93 | * Returns the ID of the given element, in O(1) time. An error is thrown if
94 | * the element is not associated with an ID.
95 | */
96 | public getID(x: T): number {
97 | const id = this.ids.get(this.keyFunc(x));
98 | if(id === undefined) { throw new Error(); }
99 | return id;
100 | }
101 |
102 | /**
103 | * Returns the ID of the given element, or -1 if the given element is not
104 | * associated with an ID, in O(1) time.
105 | */
106 | public getIDOrDefault(x: T): number {
107 | return this.ids.get(this.keyFunc(x)) ?? -1;
108 | }
109 |
110 | /**
111 | * Returns the element associated with the given ID, in O(1) time. An error
112 | * is thrown if there is no element with the given ID.
113 | */
114 | public getByID(id: number): T {
115 | if(id < 0 || id >= this.arr.length) { throw new Error(); }
116 | return this.arr[id];
117 | }
118 |
119 | public forEach(f: (x: T, id: number) => void): void {
120 | this.arr.forEach(f);
121 | }
122 | public map(f: (x: T, id: number) => S): S[] {
123 | return this.arr.map(f);
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/src/iset.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * A set of natural numbers, represented using the bits of a typed array.
3 | */
4 | type ISet = Uint32Array
5 |
6 | /**
7 | * A mutable set of natural numbers, represented using the bits of a typed array.
8 | */
9 | type MutableISet = Uint32Array & {__mutable: true}
10 |
11 | /**
12 | * Helper functions for using a typed array as a set of natural numbers.
13 | *
14 | * Aggregate operations `addAll`, `toArray` and `forEach` are O(N), where N is
15 | * the domain size; therefore they must not be used in the pattern matching loop.
16 | */
17 | namespace ISet {
18 | /**
19 | * Creates an empty set, which can contain numbers `0 <= x < domainSize`.
20 | */
21 | export function empty(domainSize: number): MutableISet {
22 | return new Uint32Array(((domainSize - 1) >> 5) + 1) as MutableISet;
23 | }
24 |
25 | /**
26 | * Creates a set containing the whole domain `0 <= x < domainSize`.
27 | */
28 | export function full(domainSize: number): MutableISet {
29 | const set = empty(domainSize);
30 | set.fill(-1);
31 | if((domainSize & 31) !== 0) {
32 | set[set.length - 1] = (1 << (domainSize & 31)) - 1;
33 | }
34 | return set;
35 | }
36 |
37 | /**
38 | * Creates a set from an iterable of natural numbers, all of which must be
39 | * less than `domainSize`.
40 | */
41 | export function of(domainSize: number, xs: Iterable): MutableISet {
42 | const set = empty(domainSize);
43 | for(const x of xs) { add(set, x); }
44 | return set;
45 | }
46 |
47 | /**
48 | * Indicates whether `set` contains the element `x`, in O(1) time.
49 | */
50 | export function has(set: ISet, x: number): boolean {
51 | return (set[x >> 5] & (1 << (x & 31))) !== 0;
52 | }
53 |
54 | /**
55 | * Returns the size of the set, in O(N) time.
56 | */
57 | export function size(set: ISet): number {
58 | let count = 0;
59 | for(let x of set) {
60 | while(x !== 0) {
61 | x &= x - 1;
62 | ++count;
63 | }
64 | }
65 | return count;
66 | }
67 |
68 | /**
69 | * Adds the element `x` to the set if it not already present, in O(1) time.
70 | */
71 | export function add(set: MutableISet, x: number): void {
72 | set[x >> 5] |= 1 << (x & 31);
73 | }
74 |
75 | /**
76 | * Adds all the members of the set `b` to the set `a`, in O(N) time.
77 | */
78 | export function addAll(a: MutableISet, b: ISet): void {
79 | if(a.length < b.length) { throw new Error(); }
80 | for(let i = 0; i < b.length; ++i) {
81 | a[i] |= b[i];
82 | }
83 | }
84 |
85 | /**
86 | * Converts a set from an array to a `bigint`, in O(N^2) time.
87 | *
88 | * Using a primitive type is convenient for Map keys; `number` would only
89 | * work for sets with domain sizes of at most 32, and strings are slower.
90 | */
91 | export function arrayToBigInt(xs: readonly number[]): bigint {
92 | let domainSize = 0;
93 | for(const x of xs) { domainSize = Math.max(domainSize, x + 1); }
94 | return domainSize > 0 ? toBigInt(of(domainSize, xs)) : 0n;
95 | }
96 |
97 | /**
98 | * Converts a set to a `bigint`, in O(N^2) time.
99 | *
100 | * Using a primitive type is convenient for Map keys; `number` would only
101 | * work for sets with domain sizes of at most 32, and strings are slower.
102 | */
103 | export function toBigInt(set: ISet): bigint {
104 | let r = 0n;
105 | for(let i = set.length - 1; i >= 0; --i) {
106 | r <<= 32n;
107 | r |= BigInt(set[i]);
108 | }
109 | return r;
110 | }
111 |
112 | /**
113 | * Returns a new array of the natural numbers in the given set, not
114 | * necessarily in order.
115 | */
116 | export function toArray(set: ISet): number[] {
117 | const arr: number[] = [];
118 | forEach(set, x => arr.push(x));
119 | return arr;
120 | }
121 |
122 | /**
123 | * Calls the function `f` for each element of the set, not necessarily in
124 | * order.
125 | */
126 | export function forEach(set: ISet, f: (x: number) => void): void {
127 | for(let i = 0; i < set.length; ++i) {
128 | const x = i << 5;
129 | let setPart = set[i];
130 | while(setPart !== 0) {
131 | // position of the highest 1 bit
132 | const dx = 31 - Math.clz32(setPart);
133 | // 'x ^ dx' is equivalent to `x + dx` here
134 | f(x ^ dx);
135 | // clear this bit
136 | setPart ^= 1 << dx;
137 | }
138 | }
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | function runDemo(size: number = 2): void {
5 | const GRID_SIZE = (1 << 7) * size;
6 | const SPEED = 16 * size * size;
7 | const LAKE_SEEDS = 4;
8 | const LAKE_SIZE = (1 << 12) * size * size;
9 | const LAND_SEEDS = 32;
10 |
11 | const alphabet = IDMap.of('BWREI');
12 | const rules = [
13 | // make a few lakes by random growth
14 | rule('B', 'I', LAKE_SEEDS),
15 | rule('IB', '*I', LAKE_SIZE - LAKE_SEEDS),
16 |
17 | // make some land by a self-avoiding random walk with backtracking
18 | rule('B', 'R', LAND_SEEDS),
19 | rule('RBB', 'WWR'),
20 | rule('RWW', 'EER'),
21 | rule('R', 'E'),
22 |
23 | // erode narrow sections of land
24 | rule('BBWBB', '**B**'),
25 |
26 | // replace the solid lakes with isolated pixels
27 | rule('II', 'BB', LAKE_SIZE / 2),
28 |
29 | // fill unused space with a water texture
30 | rule('BB*/BBB/*B*', '***/*I*/***'),
31 | rule('*I*/IBI/*I*', '***/*I*/***'),
32 |
33 | // delete water pixels at random, for an animated effect
34 | rule('I', 'B'),
35 | ];
36 |
37 | type RuleSpec = {
38 | patternIn: string,
39 | patternOut: string,
40 | limit: number | undefined,
41 | }
42 | function rule(patternIn: string, patternOut: string, limit?: number): RuleSpec {
43 | return {patternIn, patternOut, limit};
44 | }
45 |
46 | type Rule = {
47 | readonly rewrites: readonly [number, number][],
48 | limit: number | undefined,
49 | }
50 | const patternsIn = IDMap.withKey(Pattern.key);
51 | const patternsOut = IDMap.withKey(Pattern.key);
52 | const compiledRules = rules.map(spec => {
53 | const rewrites: [number, number][] = Symmetry.generate(
54 | Pattern.of(alphabet, spec.patternIn),
55 | Pattern.of(alphabet, spec.patternOut),
56 | ).map(([p, q]) => [
57 | patternsIn.getOrCreateID(p),
58 | patternsOut.getOrCreateID(q),
59 | ]);
60 | return {rewrites, limit: spec.limit};
61 | });
62 |
63 | function applyRule(state: MatcherState, rule: Rule): boolean {
64 | if(rule.limit !== undefined && rule.limit <= 0) { return false; }
65 |
66 | const {rewrites} = rule;
67 | const counts = rewrites.map(pair => state.countMatches(pair[0]));
68 | const totalCount = counts.reduce((a, b) => a + b, 0);
69 |
70 | if(totalCount === 0) { return false; }
71 |
72 | let r = rng(totalCount);
73 | for(let i = 0; i < counts.length; ++i) {
74 | r -= counts[i];
75 | if(r < 0) {
76 | const [pID, qID] = rewrites[i];
77 | const pos = state.getRandomMatch(pID)!;
78 | state.grid.setPattern(pos.x, pos.y, patternsOut.getByID(qID));
79 | if(rule.limit !== undefined) { --rule.limit; }
80 | return true;
81 | }
82 | }
83 | throw new Error();
84 | }
85 | function step(state: MatcherState, rules: readonly Rule[], k: number): boolean {
86 | let changed = false;
87 | for(let i = 0; i < k; ++i) {
88 | changed = rules.some(r => applyRule(state, r));
89 | if(!changed) { break; }
90 | }
91 | return changed;
92 | }
93 |
94 | const state = new PatternMatcher(alphabet, patternsIn).makeState(GRID_SIZE, GRID_SIZE);
95 |
96 | const scale = Math.max(1, Math.floor(window.innerHeight / state.grid.height));
97 | displayGrid(state.grid, scale);
98 |
99 | function frameHandler(): void {
100 | if(step(state, compiledRules, SPEED)) {
101 | requestAnimationFrame(frameHandler);
102 | }
103 | }
104 | requestAnimationFrame(frameHandler);
105 | }
106 |
--------------------------------------------------------------------------------
/src/matcher.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Builds a pair of DFAs which can be used to match 2D patterns. The `rowDFA`
3 | * recognises pattern rows, and the `colDFA` recognises sequences of pattern
4 | * rows matched by the `rowDFA`.
5 | *
6 | * The DFAs recognise the patterns in reverse order, for convenience so that
7 | * matches are reported where the patterns start rather than where they end.
8 | */
9 | class PatternMatcher {
10 | /**
11 | * The number of patterns recognised by this matcher.
12 | */
13 | public readonly numPatterns: number;
14 |
15 | /**
16 | * The DFA which recognises rows of patterns.
17 | */
18 | public readonly rowDFA: DFA;
19 |
20 | /**
21 | * The DFA which recognises sequences of matches from `rowDFA`.
22 | */
23 | public readonly colDFA: DFA;
24 |
25 | private readonly acceptSetMapSize: number;
26 | private readonly acceptSetDiffs: readonly (readonly number[])[];
27 |
28 | public constructor(
29 | /**
30 | * The alphabet of symbols which can appear in patterns recognised by this matcher.
31 | */
32 | public readonly alphabet: IDMap,
33 | patterns: IDMap,
34 | ) {
35 | const numPatterns = this.numPatterns = patterns.size();
36 |
37 | const rowPatterns = IDMap.ofWithKey(patterns.map(p => p.rows()).flat(), Pattern.key);
38 | const rowRegex = Regex.concat([
39 | Regex.kleeneStar(Regex.wildcard()),
40 | Regex.union(
41 | rowPatterns.map((row, rowID) => Regex.concat([
42 | Regex.concat(row.rasterData.map(c => c < 0 ? Regex.wildcard() : Regex.letters([c])).reverse()),
43 | Regex.accept(rowID),
44 | ]))
45 | ),
46 | ]);
47 | this.rowDFA = Regex.compile(alphabet.size(), rowPatterns.size(), rowRegex);
48 |
49 | const acceptingSets: number[][] = makeArray(rowPatterns.size(), () => []);
50 | this.rowDFA.acceptSetMap.forEach((xs, id) => {
51 | for(const x of xs) {
52 | acceptingSets[x].push(id);
53 | }
54 | });
55 |
56 | const colRegex = Regex.concat([
57 | Regex.kleeneStar(Regex.wildcard()),
58 | Regex.union(
59 | patterns.map((pattern, patternID) => Regex.concat([
60 | Regex.concat(pattern.rows().map(row => {
61 | const rowID = rowPatterns.getID(row);
62 | return Regex.letters(acceptingSets[rowID]);
63 | }).reverse()),
64 | Regex.accept(patternID),
65 | ]))
66 | ),
67 | ]);
68 | this.colDFA = Regex.compile(this.rowDFA.acceptSetMap.size(), numPatterns, colRegex);
69 |
70 | // precompute set differences, so that new/broken matches can be iterated in O(1) time per match
71 | const {acceptSetMap} = this.colDFA;
72 | this.acceptSetMapSize = acceptSetMap.size();
73 | const diffs: (readonly number[])[] = this.acceptSetDiffs = [];
74 | acceptSetMap.forEach(q => {
75 | const qSet = ISet.of(numPatterns, q);
76 | acceptSetMap.forEach(p => {
77 | const arr = p.filter(x => !ISet.has(qSet, x));
78 | diffs.push(arr);
79 | });
80 | });
81 | }
82 |
83 | public getAcceptSetDiff(pState: number, qState: number): readonly number[] {
84 | const {colDFA, acceptSetMapSize: k} = this;
85 | const pID = colDFA.getAcceptSetID(pState), qID = colDFA.getAcceptSetID(qState);
86 | return this.acceptSetDiffs[pID + k * qID];
87 | }
88 |
89 | public makeState(width: number, height: number): MatcherState {
90 | return new MatcherState(this, width, height);
91 | }
92 | }
93 |
94 | class MatcherState {
95 | public readonly grid: Grid;
96 |
97 | /**
98 | * Maps each `grid.index(x, y)` to the row-DFA state at (x, y).
99 | */
100 | private readonly rowStates: UintArray;
101 | /**
102 | * Maps each `grid.index(x, y)` to the column-DFA state at (x, y).
103 | */
104 | private readonly colStates: UintArray;
105 |
106 | /**
107 | * Maps each pattern ID to the set of indices `grid.index(x, y)` where that pattern is matched at (x, y).
108 | *
109 | * Invariant: `matchIndices[p].has(i)` if and only if `matcher.colDFA` accepts `p` at state `colStates[i]`
110 | */
111 | private readonly matchIndices: SampleableSet[];
112 |
113 | public constructor(
114 | public readonly matcher: PatternMatcher,
115 | width: number,
116 | height: number,
117 | ) {
118 | const n = width * height;
119 | this.rowStates = makeUintArray(n, matcher.rowDFA.size());
120 | this.colStates = makeUintArray(n, matcher.colDFA.size());
121 | this.matchIndices = makeArray(matcher.numPatterns, () => new SampleableSet(n));
122 |
123 | const grid = this.grid = new Grid(matcher.alphabet, width, height);
124 | grid.listen(this.recompute.bind(this));
125 | this.recompute(0, 0, width, height);
126 | }
127 |
128 | /**
129 | * Returns the number of times the given pattern matches this grid, in O(1) time.
130 | */
131 | public countMatches(patternID: number): number {
132 | return this.matchIndices[patternID].size();
133 | }
134 |
135 | /**
136 | * Returns the coordinates of a random match of the given pattern, in O(1) time,
137 | * or `undefined` if there are no matches.
138 | */
139 | public getRandomMatch(patternID: number): {x: number, y: number} | undefined {
140 | const index = this.matchIndices[patternID].sample();
141 | if(index !== undefined) {
142 | const {width} = this.grid;
143 | return {
144 | x: index % width,
145 | y: Math.floor(index / width),
146 | };
147 | } else {
148 | return undefined;
149 | }
150 | }
151 |
152 | /**
153 | * Updates the state to account for changes in the rectangular area from
154 | * startX/Y (inclusive) to endX/Y (exclusive).
155 | */
156 | private recompute(startX: number, startY: number, endX: number, endY: number): void {
157 | const {matcher, grid, rowStates, colStates, matchIndices} = this;
158 | const {rowDFA, colDFA} = matcher;
159 | const {width, height} = grid;
160 |
161 | // the pattern matching is done in reverse, for convenience so that
162 | // matches are accepted where the patterns start rather than where they end
163 |
164 | // recompute rowStates
165 | let minChangedX = startX;
166 | for(let y = startY; y < endY; ++y) {
167 | let state = endX === width ? 0 : rowStates[grid.index(endX, y)];
168 | for(let x = endX - 1; x >= 0; --x) {
169 | // O(1) time per iteration
170 |
171 | const index = grid.index(x, y);
172 | state = rowDFA.go(state, grid.grid[index]);
173 | if(state !== rowStates[index]) {
174 | rowStates[index] = state;
175 | minChangedX = Math.min(minChangedX, x);
176 | } else if(x < startX) {
177 | break;
178 | }
179 | }
180 | }
181 |
182 | // recompute colStates
183 | for(let x = minChangedX; x < endX; ++x) {
184 | let state = endY === height ? 0 : colStates[grid.index(x, endY)];
185 | for(let y = endY - 1; y >= 0; --y) {
186 | // O(m + 1) time per iteration, where m is the number of new + broken matches
187 |
188 | const index = grid.index(x, y);
189 | const acceptSetID = rowDFA.getAcceptSetID(rowStates[index]);
190 | state = colDFA.go(state, acceptSetID);
191 | const oldState = colStates[index];
192 | if(state !== oldState) {
193 | colStates[index] = state;
194 |
195 | // remove broken matches
196 | for(const acceptID of matcher.getAcceptSetDiff(oldState, state)) {
197 | matchIndices[acceptID].delete(index);
198 | }
199 | // add new matches
200 | for(const acceptID of matcher.getAcceptSetDiff(state, oldState)) {
201 | matchIndices[acceptID].add(index);
202 | }
203 | } else if(y < startY) {
204 | break;
205 | }
206 | }
207 | }
208 | }
209 | }
210 |
--------------------------------------------------------------------------------
/src/misc.ts:
--------------------------------------------------------------------------------
1 | type IRecord = {readonly [J in K]: V}
2 |
3 | type UintArray = Uint8Array | Uint16Array | Uint32Array
4 |
5 | /**
6 | * Creates an empty array of length `n`, which can hold unsigned integers less
7 | * than `domainSize` (exclusive). The array is initially filled with zeroes.
8 | */
9 | function makeUintArray(n: number, domainSize: number): UintArray {
10 | if(domainSize <= (1 << 8)) {
11 | return new Uint8Array(n);
12 | } else if(domainSize <= (1 << 16)) {
13 | return new Uint16Array(n);
14 | } else {
15 | return new Uint32Array(n);
16 | }
17 | }
18 |
19 | /**
20 | * Creates an empty array of length `n`, filled with the given value.
21 | */
22 | function emptyArray(n: number, value: T): T[] {
23 | return makeArray(n, () => value);
24 | }
25 |
26 | /**
27 | * Creates an array of length `n`, initialised using the given callback function.
28 | */
29 | function makeArray(n: number, f: (i: number) => T): T[] {
30 | // equivalent to `Array(n).map((_, i) => f(i))`, but guarantees an array without holes, which may be more performant to use
31 | const arr: T[] = [];
32 | for(let i = 0; i < n; ++i) { arr.push(f(i)); }
33 | return arr;
34 | }
35 |
36 | /**
37 | * Returns a random integer from 0 to n - 1.
38 | */
39 | function rng(n: number): number {
40 | return Math.floor(Math.random() * n);
41 | }
42 |
--------------------------------------------------------------------------------
/src/partition.ts:
--------------------------------------------------------------------------------
1 | type PartitionSubset = {
2 | /**
3 | * The index at which this subset occurs in the `Partition.subsets` array.
4 | * Used to delete subsets from that array in O(1) time.
5 | */
6 | index: number,
7 | /**
8 | * The start of the index range in `Partition.arr` for this subset (inclusive).
9 | */
10 | start: number,
11 | /**
12 | * The end of the index range in `Partition.arr` for this subset (exclusive).
13 | */
14 | end: number,
15 | /**
16 | * Indicates whether this subset needs to be processed by the `DFA.minimise`
17 | * algorithm.
18 | */
19 | isUnprocessed: boolean,
20 | /**
21 | * In the `Partition.refine` algorithm when this set is being split, the
22 | * sibling is the set which occurs immediately after this set in `arr`.
23 | */
24 | sibling: PartitionSubset | undefined,
25 | }
26 |
27 | /**
28 | * Data structure representing a partition of the natural numbers from 0 to n - 1,
29 | * for use in the `DFA.minimise` algorithm. The main operations are `refine` and
30 | * `pollUnprocessed`.
31 | *
32 | * https://en.wikipedia.org/wiki/Partition_refinement#Data_structure
33 | */
34 | class Partition {
35 | /**
36 | * The numbers from 0 to n - 1, ordered so that each subset in the partition
37 | * is a contiguous range.
38 | *
39 | * Invariant: `arr` is a permutation of the numbers from 0 to n - 1
40 | */
41 | private readonly arr: number[];
42 |
43 | /**
44 | * Maps the numbers from 0 to n - 1 to their indices in `arr`.
45 | *
46 | * Invariant: `arr[i] === x` if and only if `indices[x] === i`
47 | */
48 | private readonly indices: number[];
49 |
50 | /**
51 | * The boundaries in `arr` for each subset in the partition.
52 | *
53 | * Invariant: `subsets[i].index === i`
54 | * Invariant: `subsets[i].start < subsets[i].end`
55 | * Invariant: `subsets[i].start === 0` or there is a unique `j` such that `subsets[i].start === subsets[j].end`
56 | * Invariant: `subsets[i].end === n` or there is a unique `j` such that `subsets[i].end === subsets[j].start`
57 | */
58 | private readonly subsets: PartitionSubset[] = [];
59 |
60 | /**
61 | * The subsets which have yet to be processed by the `DFA.minimise` algorithm,
62 | * plus possibly some empty subsets which do not need to be processed.
63 | *
64 | * Invariant: if `subset.isUnprocessed` then `unprocessed.includes(subset)`
65 | * Invariant: if `unprocessed.includes(subset)` and not `subset.isUnprocessed`, then `subset.start === subset.end`
66 | */
67 | private readonly unprocessed: PartitionSubset[] = [];
68 |
69 | /**
70 | * Maps each number from 0 to n - 1 to the subset it is a member of.
71 | *
72 | * Invariant: `map[x].start <= indices[x] && indices[x] < map[x].end`
73 | */
74 | private readonly map: PartitionSubset[];
75 |
76 | /**
77 | * Constructs a new instance representing a partition of the numbers from
78 | * 0 to n - 1. The partition initially contains only a single subset (the
79 | * whole range).
80 | */
81 | public constructor(n: number) {
82 | this.arr = makeArray(n, i => i);
83 | this.indices = makeArray(n, i => i);
84 |
85 | const initialSubset = this.makeSubset(0, n, true);
86 | this.map = emptyArray(n, initialSubset);
87 | }
88 |
89 | /**
90 | * Returns the number of subsets in this partition.
91 | */
92 | public countSubsets(): number {
93 | return this.subsets.length;
94 | }
95 |
96 | private makeSubset(start: number, end: number, isUnprocessed: boolean): PartitionSubset {
97 | const {subsets} = this;
98 | const subset: PartitionSubset = {
99 | index: subsets.length,
100 | start,
101 | end,
102 | isUnprocessed,
103 | sibling: undefined,
104 | };
105 | subsets.push(subset);
106 | if(isUnprocessed) { this.unprocessed.push(subset); }
107 | return subset;
108 | }
109 |
110 | private deleteSubset(subset: PartitionSubset): void {
111 | // sanity check
112 | if(subset.start !== subset.end) { throw new Error(); }
113 |
114 | const {index} = subset;
115 | const removed = this.subsets.pop()!;
116 | if(removed.index !== index) {
117 | this.subsets[removed.index = index] = removed;
118 | }
119 | subset.isUnprocessed = false;
120 | }
121 |
122 | /**
123 | * Returns a subset which needs to be processed, and marks it as processed.
124 | * The elements are in no particular order.
125 | *
126 | * If no subsets remain to be processed, `undefined` is returned.
127 | */
128 | public pollUnprocessed(): readonly number[] | undefined {
129 | const {unprocessed} = this;
130 | while(unprocessed.length > 0) {
131 | const subset = unprocessed.pop()!;
132 | // have to check `isUnprocessed` because deleted subsets may still be in the stack
133 | if(subset.isUnprocessed) {
134 | subset.isUnprocessed = false;
135 | return this.arr.slice(subset.start, subset.end);
136 | }
137 | }
138 | return undefined;
139 | }
140 |
141 | /**
142 | * Returns a representative element from the subset in the partition which
143 | * contains the number `x`.
144 | */
145 | public getRepresentative(x: number): number {
146 | return this.arr[this.map[x].start];
147 | }
148 |
149 | /**
150 | * Calls the provided callback function with a representative element
151 | * from each subset in the partition.
152 | */
153 | public forEachRepresentative(f: (x: number) => void): void {
154 | const {arr} = this;
155 | for(const subset of this.subsets) {
156 | f(arr[subset.start]);
157 | }
158 | }
159 |
160 | /**
161 | * Refines this partition by splitting any subsets which partly intersect
162 | * with the given set. If an unprocessed subset is split, both parts are
163 | * marked unprocessed; otherwise, the smaller part is marked.
164 | */
165 | public refine(set: ISet): void {
166 | const {unprocessed, map} = this;
167 | const splits: PartitionSubset[] = [];
168 | ISet.forEach(set, x => {
169 | const subset = map[x];
170 | if(subset.sibling === undefined) {
171 | splits.push(subset);
172 | subset.sibling = this.makeSubset(subset.end, subset.end, subset.isUnprocessed);
173 | }
174 | this.moveToSibling(x, subset);
175 | });
176 |
177 | for(const subset of splits) {
178 | if(subset.start === subset.end) {
179 | this.deleteSubset(subset);
180 | } else if(!subset.isUnprocessed) {
181 | const sibling = subset.sibling!;
182 | const min = subset.end - subset.start <= sibling.end - sibling.start ? subset : sibling;
183 | min.isUnprocessed = true;
184 | unprocessed.push(min);
185 | }
186 | subset.sibling = undefined;
187 | }
188 | }
189 |
190 | /**
191 | * Moves the element x from `subset` to `subset.sibling`, in O(1) time. The
192 | * sibling appears immediately afterwards in `arr`, so `x` is swapped with
193 | * the last member of `subset` and then the boundary is adjusted.
194 | */
195 | private moveToSibling(x: number, subset: PartitionSubset): void {
196 | const {arr, map, indices} = this;
197 | const sibling = subset.sibling!;
198 |
199 | const i = indices[x];
200 | const j = subset.end = --sibling.start;
201 |
202 | const y = arr[j];
203 | arr[i] = y; indices[y] = i;
204 | arr[j] = x; indices[x] = j;
205 |
206 | map[x] = sibling;
207 | }
208 | }
209 |
--------------------------------------------------------------------------------
/src/pattern.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * A small rectangular pattern which can be matched in a grid, or written to it.
3 | * Patterns may contain wildcards, which match any symbol and do not write
4 | * anything to the grid.
5 | */
6 | class Pattern {
7 | /**
8 | * Creates a pattern from a string.
9 | *
10 | * The pattern is specified by a string with rows separated by `/`; wildcards
11 | * `*` in the pattern match any symbol and do not write anything to the grid.
12 | */
13 | public static of(alphabet: IDMap, pattern: string): Pattern {
14 | const rows = pattern.split('/');
15 | const width = rows[0].length;
16 | const height = rows.length;
17 |
18 | if(rows.some(row => row.length !== width)) { throw new Error(pattern); }
19 |
20 | function symbolToID(c: string): number {
21 | return c === '*' ? -1 : alphabet.getID(c);
22 | }
23 | const rasterData = rows.flatMap(row => [...row].map(symbolToID));
24 | return new Pattern(width, height, rasterData);
25 | }
26 |
27 | /**
28 | * Rotates a pattern clockwise by 90 degrees.
29 | */
30 | public static rotate(pattern: Pattern): Pattern {
31 | const {width, height, rasterData} = pattern;
32 | const newData: number[] = [];
33 | for(let x = 0; x < width; ++x) {
34 | for(let y = height - 1; y >= 0; --y) {
35 | newData.push(rasterData[x + width * y]);
36 | }
37 | }
38 | return new Pattern(height, width, newData);
39 | }
40 |
41 | /**
42 | * Reflects a pattern from top to bottom.
43 | */
44 | public static reflect(pattern: Pattern): Pattern {
45 | const {width, height, rasterData} = pattern;
46 | const newData: number[] = [];
47 | for(let y = height - 1; y >= 0; --y) {
48 | for(let x = 0; x < width; ++x) {
49 | newData.push(rasterData[x + width * y]);
50 | }
51 | }
52 | return new Pattern(width, height, newData);
53 | }
54 |
55 | /**
56 | * Returns a string representation of a pattern, for use as a Map key.
57 | */
58 | public static key(pattern: Pattern): string {
59 | return pattern._key ??= `${pattern.width}:${pattern.height}:${pattern.rasterData.join(',')}`;
60 | }
61 |
62 | /**
63 | * The cached key; see `Pattern.key`.
64 | */
65 | private _key: string | undefined = undefined;
66 |
67 | /**
68 | * A flat array of (x, y, c) triples for each occurrence of a non-wildcard
69 | * symbol `c` at a position (x, y) in this pattern.
70 | */
71 | public readonly vectorData: readonly number[];
72 |
73 | public readonly minX: number;
74 | public readonly minY: number;
75 | public readonly maxX: number;
76 | public readonly maxY: number;
77 |
78 | private constructor(
79 | /**
80 | * The width of the pattern.
81 | */
82 | public readonly width: number,
83 | /**
84 | * The height of the pattern.
85 | */
86 | public readonly height: number,
87 | /**
88 | * The cells of the pattern. A value of -1 indicates a wildcard.
89 | */
90 | public readonly rasterData: readonly number[],
91 | ) {
92 | let minX = width, minY = height, maxX = 0, maxY = 0;
93 | const vectorData: number[] = this.vectorData = [];
94 |
95 | for(let y = 0; y < height; ++y) {
96 | for(let x = 0; x < width; ++x) {
97 | const c = rasterData[x + width * y];
98 | if(c >= 0) {
99 | vectorData.push(x, y, c);
100 | minX = Math.min(minX, x);
101 | minY = Math.min(minY, y);
102 | maxX = Math.max(maxX, x + 1);
103 | maxY = Math.max(maxY, y + 1);
104 | }
105 | }
106 | }
107 |
108 | this.minX = Math.min(minX, maxX);
109 | this.minY = Math.min(minY, maxY);
110 | this.maxX = maxX;
111 | this.maxY = maxY;
112 | }
113 |
114 | /**
115 | * Returns the rows of this pattern as an array of (width * 1) patterns.
116 | */
117 | public rows(): Pattern[] {
118 | const {width, height, rasterData} = this;
119 | const out: Pattern[] = []
120 | for(let y = 0; y < height; ++y) {
121 | const row = rasterData.slice(y * width, (y + 1) * width);
122 | out.push(new Pattern(width, 1, row));
123 | }
124 | return out;
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/src/sample.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * A mutable set which can be randomly sampled in O(1) time.
3 | */
4 | class SampleableSet {
5 | public constructor(domainSize: number) {}
6 |
7 | /**
8 | * An unordered array of the set's members.
9 | */
10 | private readonly arr: T[] = [];
11 |
12 | /**
13 | * Maps the set's members to their indices in `arr`.
14 | *
15 | * Invariant: `arr[i] === x` if and only if `indices.get(x) === i`
16 | */
17 | private readonly indices = new Map();
18 |
19 | /**
20 | * Returns the number of elements in the set.
21 | */
22 | public size(): number {
23 | return this.arr.length;
24 | }
25 |
26 | /**
27 | * Indicates whether the given value is a member of the set, in O(1) time.
28 | */
29 | public has(x: T): boolean {
30 | return this.indices.has(x);
31 | }
32 |
33 | /**
34 | * Adds an element to the set, if it is not already present, in O(1) time.
35 | */
36 | public add(x: T): void {
37 | const {arr, indices} = this;
38 | if(!indices.has(x)) {
39 | indices.set(x, arr.length);
40 | arr.push(x);
41 | }
42 | }
43 |
44 | /**
45 | * Deletes an element from the set, if it is present, in O(1) time.
46 | */
47 | public delete(x: T): void {
48 | const {arr, indices} = this;
49 | const i = indices.get(x);
50 | if(i !== undefined) {
51 | const j = arr.length - 1;
52 | if(i !== j) {
53 | const y = arr[j];
54 | arr[i] = y;
55 | indices.set(y, i);
56 | }
57 | arr.pop();
58 | indices.delete(x);
59 | }
60 | }
61 |
62 | /**
63 | * Returns a random element from the set in O(1) time, or `undefined` if
64 | * the set is empty.
65 | */
66 | public sample(): T | undefined {
67 | const {arr} = this;
68 | return arr.length > 0 ? arr[rng(arr.length)] : undefined;
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/symmetry.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | namespace Symmetry {
4 | type SymmetryFunction = (p: Pattern) => Pattern
5 |
6 | const GENERATING_SET: readonly SymmetryFunction[] = [Pattern.rotate, Pattern.reflect];
7 |
8 | export function generate(patternIn: Pattern, patternOut: Pattern, symmetries: readonly SymmetryFunction[] = GENERATING_SET): [Pattern, Pattern][] {
9 | // depth-first search
10 | const stack: [Pattern, Pattern][] = [[patternIn, patternOut]];
11 | const entries = new Map();
12 | // TODO: key should include patternOut
13 | entries.set(Pattern.key(patternIn), [patternIn, patternOut]);
14 | while(stack.length > 0) {
15 | const [p, q] = stack.pop()!;
16 | for(const f of symmetries) {
17 | const pSym = f(p);
18 | const key = Pattern.key(pSym);
19 | if(!entries.has(key)) {
20 | const pair: [Pattern, Pattern] = [pSym, f(q)];
21 | entries.set(key, pair);
22 | stack.push(pair);
23 | }
24 | }
25 | }
26 | return [...entries.values()];
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "rootDir": "src/",
4 | "outFile": "pattern-match-2d.js",
5 | "lib": ["ESNext", "DOM"],
6 | "target": "ESNext",
7 | "strict": true,
8 | },
9 | }
10 |
--------------------------------------------------------------------------------