Push large tiles upwards back into the grid

This commit is contained in:
Robin Townsend 2023-06-17 15:21:38 -04:00
parent cabad628b4
commit 8b8d6fd0e0
3 changed files with 199 additions and 19 deletions

38
src/array-utils.ts Normal file
View file

@ -0,0 +1,38 @@
/*
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.
*/
/**
* Gets the index of the last element in the array to satsify the given
* predicate.
*/
// TODO: remove this once TypeScript recognizes the existence of
// Array.prototype.findLastIndex
export function findLastIndex<T>(
array: T[],
predicate: (item: T) => boolean
): number | null {
for (let i = array.length - 1; i >= 0; i--) {
if (predicate(array[i])) return i;
}
return null;
}
/**
* Counts the number of elements in an array that satsify the given predicate.
*/
export const count = <T>(array: T[], predicate: (item: T) => boolean): number =>
array.reduce((acc, item) => (predicate(item) ? acc + 1 : acc), 0);

View file

@ -17,6 +17,7 @@ limitations under the License.
import TinyQueue from "tinyqueue";
import { TileDescriptor } from "./TileDescriptor";
import { count, findLastIndex } from "../array-utils";
/**
* A 1×1 cell in a grid which belongs to a tile.
@ -105,17 +106,6 @@ export function getPaths(dest: number, g: Grid): (number | null)[] {
return edges as (number | null)[];
}
function findLastIndex<T>(
array: T[],
predicate: (item: T) => boolean
): number | null {
for (let i = array.length - 1; i >= 0; i--) {
if (predicate(array[i])) return i;
}
return null;
}
const findLast1By1Index = (g: Grid): number | null =>
findLastIndex(g.cells, (c) => c?.rows === 1 && c?.columns === 1);
@ -209,11 +199,117 @@ function getNextGap(g: Grid): number | null {
return null;
}
/**
* Gets the index of the origin of the tile to which the given cell belongs.
*/
function getOrigin(g: Grid, index: number): number {
const initialColumn = column(index, g);
for (
let i = index;
i >= 0;
i = column(i, g) === 0 ? i - g.columns + initialColumn : i - 1
) {
const cell = g.cells[i];
if (
cell !== undefined &&
cell.origin &&
inArea(index, i, areaEnd(i, cell.columns, cell.rows, g), g)
)
return i;
}
throw new Error("Tile is broken");
}
/**
* Moves the tile at index "from" over to index "to", displacing other tiles
* along the way.
* Precondition: the destination area must consist of only 1×1 tiles.
*/
function moveTile(g: Grid, from: number, to: number) {
const tile = g.cells[from]!;
const fromEnd = areaEnd(from, tile.columns, tile.rows, g);
const toEnd = areaEnd(to, tile.columns, tile.rows, g);
const displacedTiles: Cell[] = [];
forEachCellInArea(to, toEnd, g, (c, i) => {
if (c !== undefined && !inArea(i, from, fromEnd, g)) displacedTiles.push(c);
});
const movingCells: Cell[] = [];
forEachCellInArea(from, fromEnd, g, (c, i) => {
movingCells.push(c!);
g.cells[i] = undefined;
});
forEachCellInArea(
to,
toEnd,
g,
(_c, i) => (g.cells[i] = movingCells.shift())
);
forEachCellInArea(
from,
fromEnd,
g,
(_c, i) => (g.cells[i] ??= displacedTiles.shift())
);
}
/**
* Attempts to push a tile upwards by one row, displacing 1×1 tiles and shifting
* enlarged tiles around when necessary.
* @returns Whether the tile was actually pushed
*/
function pushTileUp(g: Grid, from: number): boolean {
const tile = g.cells[from]!;
// TODO: pushing large tiles sideways might be more successful in some
// situations
const cellsAboveAreDisplacable =
from - g.columns >= 0 &&
allCellsInArea(
from - g.columns,
from - g.columns + tile.columns - 1,
g,
(c, i) =>
c === undefined ||
(c.columns === 1 && c.rows === 1) ||
pushTileUp(g, getOrigin(g, i))
);
if (cellsAboveAreDisplacable) {
moveTile(g, from, from - g.columns);
return true;
} else {
return false;
}
}
/**
* Backfill any gaps in the grid.
*/
export function fillGaps(g: Grid): Grid {
const result: Grid = { ...g, cells: [...g.cells] };
// This will hopefully be the size of the grid after we're done here, assuming
// that we can pack the large tiles tightly enough
const idealLength = count(result.cells, (c) => c !== undefined);
// Step 1: Take any large tiles hanging off the bottom of the grid, and push
// them upwards
for (let i = result.cells.length - 1; i >= idealLength; i--) {
const cell = result.cells[i];
if (cell !== undefined && (cell.columns > 1 || cell.rows > 1)) {
const originIndex =
i - (cell.columns - 1) - result.columns * (cell.rows - 1);
// If it's not possible to pack the large tiles any tighter, give up
if (!pushTileUp(result, originIndex)) break;
}
}
// Step 2: Fill all 1×1 gaps
let gap = getNextGap(result);
if (gap !== null) {
@ -263,9 +359,6 @@ export function fillGaps(g: Grid): Grid {
} while (gap !== null);
}
// TODO: If there are any large tiles on the last row, shuffle them back
// upwards into a full row
// Shrink the array to remove trailing gaps
const finalLength =
(findLastIndex(result.cells, (c) => c !== undefined) ?? -1) + 1;
@ -381,13 +474,18 @@ export function cycleTileSize(tileId: string, g: Grid): Grid {
// Copy tiles from the original grid to the new one, with the new rows
// inserted at the target location
g.cells.forEach((c, src) => {
g.cells.forEach((c, from) => {
if (c?.origin && c.item.id !== tileId) {
const offset =
row(src, g) > toRow + candidateHeight - 1 ? g.columns * newRows : 0;
forEachCellInArea(src, areaEnd(src, c.columns, c.rows, g), g, (c, i) => {
gappyGrid.cells[i + offset] = c;
});
row(from, g) > toRow + candidateHeight - 1 ? g.columns * newRows : 0;
forEachCellInArea(
from,
areaEnd(from, c.columns, c.rows, g),
g,
(c, i) => {
gappyGrid.cells[i + offset] = c;
}
);
}
});

View file

@ -169,6 +169,50 @@ dddd
iegh`
);
testFillGaps(
"keeps a large tile from hanging off the bottom",
`
abcd
efgh
ii
ii`,
`
abcd
iigh
iief`
);
testFillGaps(
"pushes a chain of large tiles upwards",
`
abcd
e fg
hh
hh
ii
ii`,
`
hhcd
hhfg
aiib
eii`
);
testFillGaps(
"gives up on pushing large tiles upwards when not possible",
`
aabb
aabb
cc
cc`,
`
aabb
aabb
cc
cc`
);
function testCycleTileSize(
title: string,
tileId: string,