const data = require("fs").readFileSync("input.txt", { encoding: "utf-8" }).trim();
const groups = data.split(/\n{2,}/);
const tiles = {};
const edgeCandidates = {};
for (const tileLines of groups) {
const lines = tileLines.split(/\n/);
const head = lines.shift();
const id = /Tile (\d+):/.exec(head)[1];
tiles[id] = { lines };
tiles[id].possibleEdges = [];
tiles[id].possibleEdges[0] = lines[0];
tiles[id].possibleEdges[1] = lines.map((l) => l[0]).join("");
tiles[id].possibleEdges[2] = lines.map((l) => l[l.length - 1]).join("");
tiles[id].possibleEdges[3] = lines[lines.length - 1];
for (let i = 0; i < 4; i++) {
tiles[id].possibleEdges.push([...tiles[id].possibleEdges[i]].reverse().join(""));
}
for (const edge of tiles[id].possibleEdges) {
edgeCandidates[edge] = edgeCandidates[edge] || [];
edgeCandidates[edge].push(id);
}
tiles[id].id = id;
tiles[id].adjacent = [];
}
for (const [edge, ids] of Object.entries(edgeCandidates)) {
if (ids.length === 1) {
continue;
}
for (const id of ids) {
tiles[id].connections = tiles[id].connections || 0;
tiles[id].connections++;
}
tiles[ids[0]].adjacent.push({ t: tiles[ids[1]], e: edge });
tiles[ids[1]].adjacent.push({ t: tiles[ids[0]], e: edge });
}
const edgeTiles = Object.values(tiles)
.filter(({ id, connections }) => connections === 4)
.map(({ id }) => id);
function flipLines(lines) {
for (const [k, line] of lines.entries()) {
lines[k] = [...line].reverse().join("");
}
}
function rotateLines(lines) {
const oldLines = lines.map((l) => [...l]);
const newLines = Array.from({ length: oldLines.length }, () => Array.from({ length: oldLines.length }, () => ""));
for (let j = oldLines.length - 1; j >= 0; j--) {
for (let i = 0; i < oldLines.length; i++) {
newLines[j][i] = oldLines[i][oldLines.length - j - 1];
}
}
newLines.forEach((l, idx) => {
lines[idx] = l.join("");
});
}
const map = [];
const availableIds = new Set(Object.keys(tiles));
{
const flipIfEdge = [1, 3, 4, 6];
const rotateIfEdge = [0, 1, 1, 2, 0, 3, 3, 2];
map[0] = [edgeTiles[0]];
const connectingEdgeString = tiles[map[0][0]].adjacent[0].e;
const edgeStringNo = tiles[map[0][0]].possibleEdges.indexOf(connectingEdgeString);
if (flipIfEdge.includes(edgeStringNo)) {
flipLines(tiles[map[0][0]].lines);
}
for (let r = 0; r < rotateIfEdge[edgeStringNo] + 3; r++) {
rotateLines(tiles[map[0][0]].lines);
}
}
availableIds.delete(map[0][0]);
let px = 0;
let py = 0;
{
let isFirstLine = true;
do {
const rightEdgeString = tiles[map[py][px]].lines.map((l) => l[l.length - 1]).join("");
const adj = tiles[map[py][px]].adjacent.find(({ t: tileCandidate }) => {
if (!availableIds.has(tileCandidate.id)) return false;
for (let i = 0; i < 4; i++) {
const leftEdgeString = tileCandidate.lines.map((l) => l[0]).join("");
if (rightEdgeString === leftEdgeString) return true;
rotateLines(tileCandidate.lines);
}
flipLines(tileCandidate.lines);
for (let i = 0; i < 4; i++) {
const leftEdgeString = tileCandidate.lines.map((l) => l[0]).join("");
if (rightEdgeString === leftEdgeString) return true;
rotateLines(tileCandidate.lines);
}
});
availableIds.delete(adj.t.id);
map[py][px + 1] = adj.t.id;
if ((isFirstLine && !edgeTiles.includes(adj.t.id)) || map[py].length !== map[0].length) {
px++;
continue;
}
if (availableIds.size === 0) {
break;
}
{
let bottomEdgeString = tiles[map[py][0]].lines[tiles[map[py][0]].lines.length - 1];
let adj = tiles[map[py][0]].adjacent.find(({ t: tileCandidate }) => {
if (!availableIds.has(tileCandidate.id)) return false;
for (let i = 0; i < 4; i++) {
const topEdgeString = tileCandidate.lines[0];
if (bottomEdgeString === topEdgeString) return true;
rotateLines(tileCandidate.lines);
}
flipLines(tileCandidate.lines);
for (let i = 0; i < 4; i++) {
const topEdgeString = tileCandidate.lines[0];
if (bottomEdgeString === topEdgeString) return true;
rotateLines(tileCandidate.lines);
}
});
if (!adj && isFirstLine) {
map[0].forEach((id) => {
tiles[id].lines.reverse();
});
bottomEdgeString = tiles[map[py][0]].lines[tiles[map[py][0]].lines.length - 1];
adj = tiles[map[py][0]].adjacent.find(({ t: tileCandidate }) => {
if (!availableIds.has(tileCandidate.id)) return false;
for (let i = 0; i < 4; i++) {
const topEdgeString = tileCandidate.lines[0];
if (bottomEdgeString === topEdgeString) return true;
rotateLines(tileCandidate.lines);
}
flipLines(tileCandidate.lines);
for (let i = 0; i < 4; i++) {
const topEdgeString = tileCandidate.lines[0];
if (bottomEdgeString === topEdgeString) return true;
rotateLines(tileCandidate.lines);
}
});
}
availableIds.delete(adj.t.id);
map[py + 1] = [adj.t.id];
px = 0;
py++;
isFirstLine = false;
}
} while (true);
}
const image = [];
const tileSizeForImage = tiles[map[0][0]].lines.length - 2;
for (let i = 0; i < map.length; i++) {
for (let j = 0; j < map[0].length; j++) {
for (let k = 1; k <= tileSizeForImage; k++) {
const line = tiles[map[i][j]].lines[k].substr(1, tiles[map[i][j]].lines[k].length - 2);
image[i * tileSizeForImage + (k - 1)] = image[i * tileSizeForImage + (k - 1)] || "";
image[i * tileSizeForImage + (k - 1)] = image[i * tileSizeForImage + (k - 1)] + line;
}
}
}
const pattern = [
` # `, `# ## ## ###`, ` # # # # # # `, ];
const requiredHashesOffset = [];
for (let i = 0; i < pattern.length; i++) {
for (let j = 0; j < pattern[0].length; j++) {
if (pattern[i][j] === "#") {
requiredHashesOffset.push([i - 1, j]);
}
}
}
const imageSize = image.length;
function countPatterns() {
let res = 0;
for (let i = 1; i < imageSize - 1; i++) {
for (let j = 0; j < imageSize - pattern[0].length; j++) {
res += requiredHashesOffset.every((shift) => image[i + shift[0]][j + shift[1]] === "#");
}
}
return res;
}
let foundPatterns = 0;
search: do {
for (let i = 0; i < 4; i++) {
if ((foundPatterns = countPatterns())) break search;
rotateLines(image);
}
flipLines(image);
for (let i = 0; i < 4; i++) {
if ((foundPatterns = countPatterns())) break search;
rotateLines(image);
}
} while (false);
const hashesInPattern = [...pattern.join("")].filter((char) => char === "#").length;
const hashesInImage = [...image.join("")].filter((char) => char === "#").length;
console.log(hashesInImage - hashesInPattern * foundPatterns);