Make the expand and collapse interactions inverses of one another

For the most part, at least. If the edge cases where they differ still feel weird, I can iterate on this further.

The diff is unfortunately a bit impenetrable, because I had to change both the fillGaps and cycleTileSize core algorithms used by the big grid layout. But: the main change of significance is the addition of a function vacateArea, which clears out an area within the grid in a specific way that mirrors the motion performed by fillGaps.
This commit is contained in:
Robin Townsend 2023-07-06 00:43:17 -04:00
parent aab08d2bf1
commit 3ac98c8865
3 changed files with 543 additions and 321 deletions

View file

@ -22,10 +22,10 @@ limitations under the License.
// Array.prototype.findLastIndex // Array.prototype.findLastIndex
export function findLastIndex<T>( export function findLastIndex<T>(
array: T[], array: T[],
predicate: (item: T) => boolean predicate: (item: T, index: number) => boolean
): number | null { ): number | null {
for (let i = array.length - 1; i >= 0; i--) { for (let i = array.length - 1; i >= 0; i--) {
if (predicate(array[i])) return i; if (predicate(array[i], i)) return i;
} }
return null; return null;
@ -34,5 +34,11 @@ export function findLastIndex<T>(
/** /**
* Counts the number of elements in an array that satsify the given predicate. * Counts the number of elements in an array that satsify the given predicate.
*/ */
export const count = <T>(array: T[], predicate: (item: T) => boolean): number => export const count = <T>(
array.reduce((acc, item) => (predicate(item) ? acc + 1 : acc), 0); array: T[],
predicate: (item: T, index: number) => boolean
): number =>
array.reduce(
(acc, item, index) => (predicate(item, index) ? acc + 1 : acc),
0
);

File diff suppressed because it is too large Load diff

View file

@ -20,7 +20,7 @@ import {
cycleTileSize, cycleTileSize,
fillGaps, fillGaps,
forEachCellInArea, forEachCellInArea,
BigGridState, Grid,
resize, resize,
row, row,
moveTile, moveTile,
@ -30,13 +30,13 @@ import { TileDescriptor } from "../../src/video-grid/VideoGrid";
/** /**
* Builds a grid from a string specifying the contents of each cell as a letter. * Builds a grid from a string specifying the contents of each cell as a letter.
*/ */
function mkGrid(spec: string): BigGridState { function mkGrid(spec: string): Grid {
const secondNewline = spec.indexOf("\n", 1); const secondNewline = spec.indexOf("\n", 1);
const columns = secondNewline === -1 ? spec.length : secondNewline - 1; const columns = secondNewline === -1 ? spec.length : secondNewline - 1;
const cells = spec.match(/[a-z ]/g) ?? ([] as string[]); const cells = spec.match(/[a-z ]/g) ?? ([] as string[]);
const areas = new Set(cells); const areas = new Set(cells);
areas.delete(" "); // Space represents an empty cell, not an area areas.delete(" "); // Space represents an empty cell, not an area
const grid: BigGridState = { columns, cells: new Array(cells.length) }; const grid: Grid = { columns, cells: new Array(cells.length) };
for (const area of areas) { for (const area of areas) {
const start = cells.indexOf(area); const start = cells.indexOf(area);
@ -60,7 +60,7 @@ function mkGrid(spec: string): BigGridState {
/** /**
* Turns a grid into a string showing the contents of each cell as a letter. * Turns a grid into a string showing the contents of each cell as a letter.
*/ */
function showGrid(g: BigGridState): string { function showGrid(g: Grid): string {
let result = "\n"; let result = "\n";
for (let i = 0; i < g.cells.length; i++) { for (let i = 0; i < g.cells.length; i++) {
if (i > 0 && i % g.columns == 0) result += "\n"; if (i > 0 && i % g.columns == 0) result += "\n";
@ -116,11 +116,11 @@ mno`,
` `
aebch aebch
difgl difgl
monjk` mjnok`
); );
testFillGaps( testFillGaps(
"fills a big gap", "fills a big gap with 1×1 tiles",
` `
abcd abcd
e f e f
@ -128,19 +128,19 @@ g h
ijkl`, ijkl`,
` `
abcd abcd
elhf ehkf
gkji` glji`
); );
testFillGaps( testFillGaps(
"only moves 1×1 tiles", "fills a big gap with a large tile",
` `
aa aa
bc`, bc`,
` `
bc aa
aa` cb`
); );
testFillGaps( testFillGaps(
@ -186,7 +186,7 @@ iief`
); );
testFillGaps( testFillGaps(
"pushes a chain of large tiles upwards", "collapses large tiles trapped at the bottom",
` `
abcd abcd
e fg e fg
@ -195,24 +195,24 @@ hh
ii ii
ii`, ii`,
` `
hhcd abcd
hhfg hhfg
aiib hhie`
eii`
); );
testFillGaps( testFillGaps(
"gives up on pushing large tiles upwards when not possible", "gives up on pushing large tiles upwards when not possible",
` `
aabb aa
aabb aa
cc bccd
cc`, eccf
ghij`,
` `
aabb aadf
aabb aaji
cc bcch
cc` eccg`
); );
function testCycleTileSize( function testCycleTileSize(
@ -237,9 +237,9 @@ def
ghi`, ghi`,
` `
acc acc
bcc dcc
def gbe
ghi` ifh`
); );
testCycleTileSize( testCycleTileSize(
@ -249,10 +249,10 @@ testCycleTileSize(
abcd abcd
efgh`, efgh`,
` `
abcd acdh
eggg bggg
fggg fggg
h` e`
); );
testCycleTileSize( testCycleTileSize(
@ -264,9 +264,9 @@ dbbe
fghi fghi
jk`, jk`,
` `
akbc abhc
djhe djge
fig` fik`
); );
testCycleTileSize( testCycleTileSize(
@ -284,9 +284,9 @@ abb
gbb gbb
dde dde
ddf ddf
cci ccm
cch cch
klm` lik`
); );
testCycleTileSize( testCycleTileSize(
@ -304,6 +304,34 @@ dde
ddf` ddf`
); );
test("cycleTileSize is its own inverse", () => {
const input = `
abc
def
ghi
jk`;
const grid = mkGrid(input);
let gridAfter = grid;
const toggle = (tileId: string) => {
const tile = grid.cells.find((c) => c?.item.id === tileId)!.item;
gridAfter = cycleTileSize(gridAfter, tile);
};
// Toggle a series of tiles
toggle("j");
toggle("h");
toggle("a");
// Now do the same thing in reverse
toggle("a");
toggle("h");
toggle("j");
// The grid should be back to its original state
expect(showGrid(gridAfter)).toBe(input);
});
function testAddItems( function testAddItems(
title: string, title: string,
items: TileDescriptor<unknown>[], items: TileDescriptor<unknown>[],
@ -437,9 +465,9 @@ gh`,
af af
bb bb
bb bb
dd
dd
ch ch
dd
dd
eg` eg`
); );
@ -455,9 +483,8 @@ dd
dd dd
eg`, eg`,
` `
bbbc afcd
bbbf bbbg
addd bbbe
hddd h`
ge`
); );