/* 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, 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[], 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)), ` aab aac d`, ` aab aac def` ); testAddItems( "places one tile near another on request", [{ id: "g", placeNear: "b" } as unknown as TileDescriptor], ` abc def`, ` abc g def` ); testAddItems( "places items with a large base size", [{ id: "g", largeBaseSize: true } as unknown as TileDescriptor], ` 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` );