2023-02-14 07:35:50 +08:00
|
|
|
|
/*
|
|
|
|
|
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 {
|
2023-06-18 12:47:37 +08:00
|
|
|
|
addItems,
|
2023-02-14 07:35:50 +08:00
|
|
|
|
column,
|
|
|
|
|
cycleTileSize,
|
|
|
|
|
fillGaps,
|
|
|
|
|
forEachCellInArea,
|
2023-07-06 12:43:17 +08:00
|
|
|
|
Grid,
|
2023-06-18 09:33:54 +08:00
|
|
|
|
resize,
|
2023-02-14 07:35:50 +08:00
|
|
|
|
row,
|
2023-06-28 00:19:06 +08:00
|
|
|
|
moveTile,
|
|
|
|
|
} from "../../src/video-grid/BigGrid";
|
2023-06-10 05:22:34 +08:00
|
|
|
|
import { TileDescriptor } from "../../src/video-grid/VideoGrid";
|
2023-02-14 07:35:50 +08:00
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Builds a grid from a string specifying the contents of each cell as a letter.
|
|
|
|
|
*/
|
2023-07-06 12:43:17 +08:00
|
|
|
|
function mkGrid(spec: string): Grid {
|
2023-02-14 07:35:50 +08:00
|
|
|
|
const secondNewline = spec.indexOf("\n", 1);
|
|
|
|
|
const columns = secondNewline === -1 ? spec.length : secondNewline - 1;
|
2023-06-10 05:22:34 +08:00
|
|
|
|
const cells = spec.match(/[a-z ]/g) ?? ([] as string[]);
|
2023-02-14 07:35:50 +08:00
|
|
|
|
const areas = new Set(cells);
|
|
|
|
|
areas.delete(" "); // Space represents an empty cell, not an area
|
2023-07-06 12:43:17 +08:00
|
|
|
|
const grid: Grid = { columns, cells: new Array(cells.length) };
|
2023-02-14 07:35:50 +08:00
|
|
|
|
|
|
|
|
|
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] = {
|
2023-06-10 05:22:34 +08:00
|
|
|
|
item: { id: area } as unknown as TileDescriptor<unknown>,
|
2023-02-14 07:35:50 +08:00
|
|
|
|
origin: i === start,
|
|
|
|
|
rows,
|
|
|
|
|
columns,
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return grid;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Turns a grid into a string showing the contents of each cell as a letter.
|
|
|
|
|
*/
|
2023-07-06 12:43:17 +08:00
|
|
|
|
function showGrid(g: Grid): string {
|
2023-02-14 07:35:50 +08:00
|
|
|
|
let result = "\n";
|
2023-06-28 00:19:06 +08:00
|
|
|
|
for (let i = 0; i < g.cells.length; i++) {
|
2023-02-14 07:35:50 +08:00
|
|
|
|
if (i > 0 && i % g.columns == 0) result += "\n";
|
2023-06-28 00:19:06 +08:00
|
|
|
|
result += g.cells[i]?.item.id ?? " ";
|
|
|
|
|
}
|
2023-02-14 07:35:50 +08:00
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function testFillGaps(title: string, input: string, output: string): void {
|
|
|
|
|
test(`fillGaps ${title}`, () => {
|
|
|
|
|
expect(showGrid(fillGaps(mkGrid(input)))).toBe(output);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-14 11:24:04 +08:00
|
|
|
|
testFillGaps(
|
|
|
|
|
"does nothing on an empty grid",
|
|
|
|
|
`
|
|
|
|
|
`,
|
|
|
|
|
`
|
|
|
|
|
`
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
testFillGaps(
|
|
|
|
|
"does nothing if there are no gaps",
|
|
|
|
|
`
|
|
|
|
|
ab
|
|
|
|
|
cd
|
|
|
|
|
ef`,
|
|
|
|
|
`
|
|
|
|
|
ab
|
|
|
|
|
cd
|
|
|
|
|
ef`
|
|
|
|
|
);
|
|
|
|
|
|
2023-02-14 07:35:50 +08:00
|
|
|
|
testFillGaps(
|
|
|
|
|
"fills a gap",
|
|
|
|
|
`
|
|
|
|
|
a b
|
|
|
|
|
cde
|
|
|
|
|
f`,
|
|
|
|
|
`
|
|
|
|
|
cab
|
|
|
|
|
fde`
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
testFillGaps(
|
|
|
|
|
"fills multiple gaps",
|
|
|
|
|
`
|
|
|
|
|
a bc
|
|
|
|
|
defgh
|
|
|
|
|
ijkl
|
|
|
|
|
mno`,
|
|
|
|
|
`
|
|
|
|
|
aebch
|
|
|
|
|
difgl
|
2023-07-06 12:43:17 +08:00
|
|
|
|
mjnok`
|
2023-02-14 07:35:50 +08:00
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
testFillGaps(
|
2023-07-06 12:43:17 +08:00
|
|
|
|
"fills a big gap with 1×1 tiles",
|
2023-02-14 07:35:50 +08:00
|
|
|
|
`
|
|
|
|
|
abcd
|
|
|
|
|
e f
|
|
|
|
|
g h
|
|
|
|
|
ijkl`,
|
|
|
|
|
`
|
|
|
|
|
abcd
|
2023-07-06 12:43:17 +08:00
|
|
|
|
ehkf
|
|
|
|
|
glji`
|
2023-02-14 07:35:50 +08:00
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
testFillGaps(
|
2023-07-06 12:43:17 +08:00
|
|
|
|
"fills a big gap with a large tile",
|
2023-02-14 07:35:50 +08:00
|
|
|
|
`
|
|
|
|
|
|
|
|
|
|
aa
|
|
|
|
|
bc`,
|
|
|
|
|
`
|
2023-07-06 12:43:17 +08:00
|
|
|
|
aa
|
|
|
|
|
cb`
|
2023-02-14 07:35:50 +08:00
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
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`
|
|
|
|
|
);
|
|
|
|
|
|
2023-06-18 03:21:38 +08:00
|
|
|
|
testFillGaps(
|
|
|
|
|
"keeps a large tile from hanging off the bottom",
|
|
|
|
|
`
|
|
|
|
|
abcd
|
|
|
|
|
efgh
|
|
|
|
|
|
|
|
|
|
ii
|
|
|
|
|
ii`,
|
|
|
|
|
`
|
|
|
|
|
abcd
|
|
|
|
|
iigh
|
|
|
|
|
iief`
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
testFillGaps(
|
2023-07-06 12:43:17 +08:00
|
|
|
|
"collapses large tiles trapped at the bottom",
|
2023-06-18 03:21:38 +08:00
|
|
|
|
`
|
|
|
|
|
abcd
|
|
|
|
|
e fg
|
|
|
|
|
hh
|
|
|
|
|
hh
|
|
|
|
|
ii
|
|
|
|
|
ii`,
|
|
|
|
|
`
|
2023-07-06 12:43:17 +08:00
|
|
|
|
abcd
|
2023-06-18 03:21:38 +08:00
|
|
|
|
hhfg
|
2023-07-06 12:43:17 +08:00
|
|
|
|
hhie`
|
2023-06-18 03:21:38 +08:00
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
testFillGaps(
|
|
|
|
|
"gives up on pushing large tiles upwards when not possible",
|
|
|
|
|
`
|
2023-07-06 12:43:17 +08:00
|
|
|
|
aa
|
|
|
|
|
aa
|
|
|
|
|
bccd
|
|
|
|
|
eccf
|
|
|
|
|
ghij`,
|
2023-06-18 03:21:38 +08:00
|
|
|
|
`
|
2023-07-06 12:43:17 +08:00
|
|
|
|
aadf
|
|
|
|
|
aaji
|
|
|
|
|
bcch
|
|
|
|
|
eccg`
|
2023-06-18 03:21:38 +08:00
|
|
|
|
);
|
|
|
|
|
|
2023-02-14 07:35:50 +08:00
|
|
|
|
function testCycleTileSize(
|
|
|
|
|
title: string,
|
|
|
|
|
tileId: string,
|
|
|
|
|
input: string,
|
|
|
|
|
output: string
|
|
|
|
|
): void {
|
|
|
|
|
test(`cycleTileSize ${title}`, () => {
|
2023-06-28 00:19:06 +08:00
|
|
|
|
const grid = mkGrid(input);
|
|
|
|
|
const tile = grid.cells.find((c) => c?.item.id === tileId)!.item;
|
|
|
|
|
expect(showGrid(cycleTileSize(grid, tile))).toBe(output);
|
2023-02-14 07:35:50 +08:00
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
testCycleTileSize(
|
|
|
|
|
"expands a tile to 2×2 in a 3 column layout",
|
|
|
|
|
"c",
|
|
|
|
|
`
|
|
|
|
|
abc
|
|
|
|
|
def
|
|
|
|
|
ghi`,
|
|
|
|
|
`
|
|
|
|
|
acc
|
2023-07-06 12:43:17 +08:00
|
|
|
|
dcc
|
|
|
|
|
gbe
|
|
|
|
|
ifh`
|
2023-02-14 07:35:50 +08:00
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
testCycleTileSize(
|
|
|
|
|
"expands a tile to 3×3 in a 4 column layout",
|
|
|
|
|
"g",
|
|
|
|
|
`
|
|
|
|
|
abcd
|
|
|
|
|
efgh`,
|
|
|
|
|
`
|
2023-07-06 12:43:17 +08:00
|
|
|
|
acdh
|
|
|
|
|
bggg
|
2023-02-14 07:35:50 +08:00
|
|
|
|
fggg
|
2023-07-06 12:43:17 +08:00
|
|
|
|
e`
|
2023-02-14 07:35:50 +08:00
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
testCycleTileSize(
|
|
|
|
|
"restores a tile to 1×1",
|
|
|
|
|
"b",
|
|
|
|
|
`
|
|
|
|
|
abbc
|
|
|
|
|
dbbe
|
|
|
|
|
fghi
|
|
|
|
|
jk`,
|
|
|
|
|
`
|
2023-07-06 12:43:17 +08:00
|
|
|
|
abhc
|
|
|
|
|
djge
|
|
|
|
|
fik`
|
2023-02-14 07:35:50 +08:00
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
testCycleTileSize(
|
|
|
|
|
"expands a tile even in a crowded grid",
|
|
|
|
|
"c",
|
|
|
|
|
`
|
|
|
|
|
abb
|
|
|
|
|
cbb
|
|
|
|
|
dde
|
|
|
|
|
ddf
|
|
|
|
|
ghi
|
|
|
|
|
klm`,
|
|
|
|
|
`
|
|
|
|
|
abb
|
|
|
|
|
gbb
|
|
|
|
|
dde
|
|
|
|
|
ddf
|
2023-07-06 12:43:17 +08:00
|
|
|
|
ccm
|
2023-02-14 07:35:50 +08:00
|
|
|
|
cch
|
2023-07-06 12:43:17 +08:00
|
|
|
|
lik`
|
2023-02-14 07:35:50 +08:00
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
testCycleTileSize(
|
|
|
|
|
"does nothing if the tile has no room to expand",
|
|
|
|
|
"c",
|
|
|
|
|
`
|
|
|
|
|
abb
|
|
|
|
|
cbb
|
|
|
|
|
dde
|
|
|
|
|
ddf`,
|
|
|
|
|
`
|
|
|
|
|
abb
|
|
|
|
|
cbb
|
|
|
|
|
dde
|
|
|
|
|
ddf`
|
|
|
|
|
);
|
2023-02-14 11:24:04 +08:00
|
|
|
|
|
2023-07-06 12:43:17 +08:00
|
|
|
|
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);
|
|
|
|
|
});
|
|
|
|
|
|
2023-06-18 12:47:37 +08:00
|
|
|
|
function testAddItems(
|
|
|
|
|
title: string,
|
2023-06-18 13:13:45 +08:00
|
|
|
|
items: TileDescriptor<unknown>[],
|
2023-06-18 12:47:37 +08:00
|
|
|
|
input: string,
|
|
|
|
|
output: string
|
|
|
|
|
): void {
|
|
|
|
|
test(`addItems ${title}`, () => {
|
|
|
|
|
expect(showGrid(addItems(items, mkGrid(input)))).toBe(output);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
testAddItems(
|
|
|
|
|
"appends 1×1 tiles",
|
2023-06-18 13:13:45 +08:00
|
|
|
|
["e", "f"].map((i) => ({ id: i } as unknown as TileDescriptor<unknown>)),
|
2023-06-18 12:47:37 +08:00
|
|
|
|
`
|
2023-02-14 11:24:04 +08:00
|
|
|
|
aab
|
|
|
|
|
aac
|
2023-06-18 12:47:37 +08:00
|
|
|
|
d`,
|
|
|
|
|
`
|
2023-02-14 11:24:04 +08:00
|
|
|
|
aab
|
|
|
|
|
aac
|
2023-06-18 12:47:37 +08:00
|
|
|
|
def`
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
testAddItems(
|
|
|
|
|
"places one tile near another on request",
|
2023-06-18 13:13:45 +08:00
|
|
|
|
[{ id: "g", placeNear: "b" } as unknown as TileDescriptor<unknown>],
|
2023-06-18 12:47:37 +08:00
|
|
|
|
`
|
|
|
|
|
abc
|
|
|
|
|
def`,
|
|
|
|
|
`
|
|
|
|
|
abc
|
2023-06-28 00:19:06 +08:00
|
|
|
|
g
|
|
|
|
|
def`
|
2023-06-18 12:47:37 +08:00
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
testAddItems(
|
|
|
|
|
"places items with a large base size",
|
2023-06-18 13:13:45 +08:00
|
|
|
|
[{ id: "g", largeBaseSize: true } as unknown as TileDescriptor<unknown>],
|
2023-06-18 12:47:37 +08:00
|
|
|
|
`
|
|
|
|
|
abc
|
|
|
|
|
def`,
|
|
|
|
|
`
|
|
|
|
|
abc
|
|
|
|
|
ggf
|
|
|
|
|
gge
|
|
|
|
|
d`
|
|
|
|
|
);
|
2023-06-18 05:28:54 +08:00
|
|
|
|
|
2023-06-28 00:19:06 +08:00
|
|
|
|
function testMoveTile(
|
2023-06-18 05:28:54 +08:00
|
|
|
|
title: string,
|
|
|
|
|
from: number,
|
|
|
|
|
to: number,
|
|
|
|
|
input: string,
|
|
|
|
|
output: string
|
|
|
|
|
): void {
|
2023-06-28 00:19:06 +08:00
|
|
|
|
test(`moveTile ${title}`, () => {
|
|
|
|
|
expect(showGrid(moveTile(mkGrid(input), from, to))).toBe(output);
|
2023-06-18 05:28:54 +08:00
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2023-06-28 00:19:06 +08:00
|
|
|
|
testMoveTile(
|
2023-06-18 05:28:54 +08:00
|
|
|
|
"refuses to move a tile too far to the left",
|
|
|
|
|
1,
|
|
|
|
|
-1,
|
|
|
|
|
`
|
|
|
|
|
abc`,
|
|
|
|
|
`
|
|
|
|
|
abc`
|
|
|
|
|
);
|
|
|
|
|
|
2023-06-28 00:19:06 +08:00
|
|
|
|
testMoveTile(
|
2023-06-18 05:28:54 +08:00
|
|
|
|
"refuses to move a tile too far to the right",
|
|
|
|
|
1,
|
|
|
|
|
3,
|
|
|
|
|
`
|
|
|
|
|
abc`,
|
|
|
|
|
`
|
|
|
|
|
abc`
|
|
|
|
|
);
|
|
|
|
|
|
2023-06-28 00:19:06 +08:00
|
|
|
|
testMoveTile(
|
2023-06-18 05:28:54 +08:00
|
|
|
|
"moves a large tile to an unoccupied space",
|
|
|
|
|
3,
|
|
|
|
|
1,
|
|
|
|
|
`
|
|
|
|
|
a b
|
|
|
|
|
ccd
|
|
|
|
|
cce`,
|
|
|
|
|
`
|
|
|
|
|
acc
|
|
|
|
|
bcc
|
|
|
|
|
d e`
|
|
|
|
|
);
|
|
|
|
|
|
2023-06-28 00:19:06 +08:00
|
|
|
|
testMoveTile(
|
2023-06-18 05:28:54 +08:00
|
|
|
|
"refuses to move a large tile to an occupied space",
|
|
|
|
|
3,
|
|
|
|
|
1,
|
|
|
|
|
`
|
|
|
|
|
abb
|
|
|
|
|
ccd
|
|
|
|
|
cce`,
|
|
|
|
|
`
|
|
|
|
|
abb
|
|
|
|
|
ccd
|
|
|
|
|
cce`
|
|
|
|
|
);
|
2023-06-18 09:33:54 +08:00
|
|
|
|
|
|
|
|
|
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
|
2023-07-06 12:43:17 +08:00
|
|
|
|
ch
|
2023-06-18 09:33:54 +08:00
|
|
|
|
eg`
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
testResize(
|
|
|
|
|
"expands the grid",
|
|
|
|
|
4,
|
|
|
|
|
`
|
|
|
|
|
af
|
|
|
|
|
bb
|
|
|
|
|
bb
|
|
|
|
|
ch
|
|
|
|
|
dd
|
|
|
|
|
dd
|
|
|
|
|
eg`,
|
|
|
|
|
`
|
2023-07-06 12:43:17 +08:00
|
|
|
|
afcd
|
|
|
|
|
bbbg
|
|
|
|
|
bbbe
|
|
|
|
|
h`
|
2023-06-18 09:33:54 +08:00
|
|
|
|
);
|