element-call-Github/test/video-grid/BigGrid-test.ts
Robin Townsend 3ac98c8865 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.
2023-07-06 00:59:14 -04:00

491 lines
6.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
Copyright 2023 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import {
addItems,
column,
cycleTileSize,
fillGaps,
forEachCellInArea,
Grid,
resize,
row,
moveTile,
} from "../../src/video-grid/BigGrid";
import { TileDescriptor } from "../../src/video-grid/VideoGrid";
/**
* Builds a grid from a string specifying the contents of each cell as a letter.
*/
function mkGrid(spec: string): Grid {
const secondNewline = spec.indexOf("\n", 1);
const columns = secondNewline === -1 ? spec.length : secondNewline - 1;
const cells = spec.match(/[a-z ]/g) ?? ([] as string[]);
const areas = new Set(cells);
areas.delete(" "); // Space represents an empty cell, not an area
const grid: Grid = { columns, cells: new Array(cells.length) };
for (const area of areas) {
const start = cells.indexOf(area);
const end = cells.lastIndexOf(area);
const rows = row(end, grid) - row(start, grid) + 1;
const columns = column(end, grid) - column(start, grid) + 1;
forEachCellInArea(start, end, grid, (_c, i) => {
grid.cells[i] = {
item: { id: area } as unknown as TileDescriptor<unknown>,
origin: i === start,
rows,
columns,
};
});
}
return grid;
}
/**
* Turns a grid into a string showing the contents of each cell as a letter.
*/
function showGrid(g: Grid): string {
let result = "\n";
for (let i = 0; i < g.cells.length; i++) {
if (i > 0 && i % g.columns == 0) result += "\n";
result += g.cells[i]?.item.id ?? " ";
}
return result;
}
function testFillGaps(title: string, input: string, output: string): void {
test(`fillGaps ${title}`, () => {
expect(showGrid(fillGaps(mkGrid(input)))).toBe(output);
});
}
testFillGaps(
"does nothing on an empty grid",
`
`,
`
`
);
testFillGaps(
"does nothing if there are no gaps",
`
ab
cd
ef`,
`
ab
cd
ef`
);
testFillGaps(
"fills a gap",
`
a b
cde
f`,
`
cab
fde`
);
testFillGaps(
"fills multiple gaps",
`
a bc
defgh
ijkl
mno`,
`
aebch
difgl
mjnok`
);
testFillGaps(
"fills a big gap with 1×1 tiles",
`
abcd
e f
g h
ijkl`,
`
abcd
ehkf
glji`
);
testFillGaps(
"fills a big gap with a large tile",
`
aa
bc`,
`
aa
cb`
);
testFillGaps(
"prefers moving around large tiles",
`
a bc
ddde
dddf
ghij
k`,
`
abce
dddf
dddj
kghi`
);
testFillGaps(
"moves through large tiles if necessary",
`
a bc
dddd
efgh
i`,
`
afbc
dddd
iegh`
);
testFillGaps(
"keeps a large tile from hanging off the bottom",
`
abcd
efgh
ii
ii`,
`
abcd
iigh
iief`
);
testFillGaps(
"collapses large tiles trapped at the bottom",
`
abcd
e fg
hh
hh
ii
ii`,
`
abcd
hhfg
hhie`
);
testFillGaps(
"gives up on pushing large tiles upwards when not possible",
`
aa
aa
bccd
eccf
ghij`,
`
aadf
aaji
bcch
eccg`
);
function testCycleTileSize(
title: string,
tileId: string,
input: string,
output: string
): void {
test(`cycleTileSize ${title}`, () => {
const grid = mkGrid(input);
const tile = grid.cells.find((c) => c?.item.id === tileId)!.item;
expect(showGrid(cycleTileSize(grid, tile))).toBe(output);
});
}
testCycleTileSize(
"expands a tile to 2×2 in a 3 column layout",
"c",
`
abc
def
ghi`,
`
acc
dcc
gbe
ifh`
);
testCycleTileSize(
"expands a tile to 3×3 in a 4 column layout",
"g",
`
abcd
efgh`,
`
acdh
bggg
fggg
e`
);
testCycleTileSize(
"restores a tile to 1×1",
"b",
`
abbc
dbbe
fghi
jk`,
`
abhc
djge
fik`
);
testCycleTileSize(
"expands a tile even in a crowded grid",
"c",
`
abb
cbb
dde
ddf
ghi
klm`,
`
abb
gbb
dde
ddf
ccm
cch
lik`
);
testCycleTileSize(
"does nothing if the tile has no room to expand",
"c",
`
abb
cbb
dde
ddf`,
`
abb
cbb
dde
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(
title: string,
items: TileDescriptor<unknown>[],
input: string,
output: string
): void {
test(`addItems ${title}`, () => {
expect(showGrid(addItems(items, mkGrid(input)))).toBe(output);
});
}
testAddItems(
"appends 1×1 tiles",
["e", "f"].map((i) => ({ id: i } as unknown as TileDescriptor<unknown>)),
`
aab
aac
d`,
`
aab
aac
def`
);
testAddItems(
"places one tile near another on request",
[{ id: "g", placeNear: "b" } as unknown as TileDescriptor<unknown>],
`
abc
def`,
`
abc
g
def`
);
testAddItems(
"places items with a large base size",
[{ id: "g", largeBaseSize: true } as unknown as TileDescriptor<unknown>],
`
abc
def`,
`
abc
ggf
gge
d`
);
function testMoveTile(
title: string,
from: number,
to: number,
input: string,
output: string
): void {
test(`moveTile ${title}`, () => {
expect(showGrid(moveTile(mkGrid(input), from, to))).toBe(output);
});
}
testMoveTile(
"refuses to move a tile too far to the left",
1,
-1,
`
abc`,
`
abc`
);
testMoveTile(
"refuses to move a tile too far to the right",
1,
3,
`
abc`,
`
abc`
);
testMoveTile(
"moves a large tile to an unoccupied space",
3,
1,
`
a b
ccd
cce`,
`
acc
bcc
d e`
);
testMoveTile(
"refuses to move a large tile to an occupied space",
3,
1,
`
abb
ccd
cce`,
`
abb
ccd
cce`
);
function testResize(
title: string,
columns: number,
input: string,
output: string
): void {
test(`resize ${title}`, () => {
expect(showGrid(resize(mkGrid(input), columns))).toBe(output);
});
}
testResize(
"contracts the grid",
2,
`
abbb
cbbb
ddde
dddf
gh`,
`
af
bb
bb
dd
dd
ch
eg`
);
testResize(
"expands the grid",
4,
`
af
bb
bb
ch
dd
dd
eg`,
`
afcd
bbbg
bbbe
h`
);