/** * TODO: optimize for reduced complexity * TODO: optimize for cleaner code */ const fs = require("fs"); function main() { const input = fs.readFileSync("input", "utf-8").trim(); const tiles = input.split("\n\n").map((tile) => new Tile(tile)); const tilesMap = Object.fromEntries(tiles.map((tile) => [tile.id, tile])); const tileSet = new Set(tiles); // console.log(tiles); // console.log(tileSet); const borderMap = {}; for (const tile of tiles) { for (const border of Object.values(tile.borders)) { const key = canonical(border); if (typeof borderMap[key] === "undefined") { borderMap[key] = []; } borderMap[key].push(tile.id); if (borderMap[key].length > 2) { console.warn( "Assumption does not hold: One edge is present in more than two tiles" ); } } } // console.log(borderMap); const edgeTiles = new Set(); const cornerTiles = new Set(); const neighbors = {}; for (const ids of Object.values(borderMap)) { if (ids.length === 1) { const id = ids[0]; if (edgeTiles.has(id)) { edgeTiles.delete(id); cornerTiles.add(id); } else { edgeTiles.add(id); } } else { const [tileA, tileB] = ids; if (typeof neighbors[tileA] === "undefined") neighbors[tileA] = []; if (typeof neighbors[tileB] === "undefined") neighbors[tileB] = []; neighbors[tileA].push(tileB); neighbors[tileB].push(tileA); } } console.log(neighbors); let topLeft = tilesMap[Array.from(cornerTiles)[0]]; console.log("TopLeft:", topLeft.id); const isEdge = (border) => borderMap[canonical(border)].length === 1; console.log(topLeft.pprint()); const reprs = []; for (const t of allOrientations(topLeft)) { reprs.push(t.pprint().split("\n")); } for (let i = 0; i < reprs[0].length; i++) { for (let j = 0; j < reprs.length; j++) { process.stdout.write(reprs[j][i]); process.stdout.write(" "); } process.stdout.write("\n"); } while (isEdge(topLeft.borders.e) || isEdge(topLeft.borders.s)) { console.log("rotating corner tile"); topLeft.rot90(); } // console.log(topLeft) tileSet.delete(topLeft); let curRow = [topLeft]; const orderedTiles = []; const tileBuffer = Array.from(tileSet); let lastTile = topLeft; let loopDetection = 0; while (tileBuffer.length > 0) { // console.log("Tiles in buffer:", tileBuffer.length); let cur = tileBuffer.shift(); if (isEdge(lastTile.borders.e)) { // console.log("Looking for southener of", topLeft.id, cur.id); for (cur of allOrientations(cur)) { if (reverse(topLeft.borders.s) === cur.borders.n) { orderedTiles.push(curRow); curRow = []; lastTile = cur; topLeft = cur; curRow.push(cur); break; } } } else { // console.log("Looking for eastern of", lastTile.id, cur.id); for (cur of allOrientations(cur)) { if (lastTile.borders.e === reverse(cur.borders.w)) { lastTile = cur; curRow.push(cur); break; } } } if (cur !== lastTile) { tileBuffer.push(cur); } else { // console.log("Tile found! Its", cur.id); loopDetection = 0; } // sleep(50); if (loopDetection > tileBuffer.length) { console.log("loop detected"); break; } loopDetection++; } orderedTiles.push(curRow); console.log("Found map arrangement"); console.log(orderedTiles); let map = ""; for (const row of orderedTiles) { for (let i = 0; i < row[0].content.length; i++) { for (let j = 0; j < row.length; j++) { map += row[j].content[i].join(""); // map += " "; } map += "\n"; } // map += "\n"; } const hashCount = map.match(/#/g).length; console.log("hashCount", hashCount); // console.log(map); map = map.split("\n").map((row) => row.split("")); let sumSeamonsters; for (let i = 0; i < 8; i++) { if (i === 4) { console.log("flip map"); map = flipH(map); } else { console.log("rotate map"); map = rotSquare(map); } sumSeamonsters = findSeamonster(map); console.log("Seamonsters found:", sumSeamonsters); if (sumSeamonsters) { break; } } const seamonsterHashes = 15 * sumSeamonsters; console.log(hashCount - seamonsterHashes); } const reverse = (str) => str.split("").reverse().join(""); const isCanonical = (edge) => edge < reverse(edge); const canonical = (edge) => (isCanonical(edge) ? edge : reverse(edge)); const trimEnds = (arr) => arr.slice(1, arr.length - 1); function rotSquare(mat) { const result = []; const length = mat.length; for (let i = 0; i < length; i++) { const row = []; for (let j = 0; j < length; j++) { row.push(mat[length - 1 - j][i]); } result.push(row); } return result; } const flipH = (mat) => mat.map((row) => row.reverse()); const flipV = (mat) => mat.reverse(); function* allOrientations(tile) { tile.rot90(); yield tile; tile.rot90(); yield tile; tile.rot90(); yield tile; tile.rot90(); yield tile; tile.flipH(); yield tile; tile.rot90(); yield tile; tile.rot90(); yield tile; tile.rot90(); yield tile; } function findSeamonster(map) { map = map.map((row) => row.join("")); const sm1 = " # "; const sm2 = "# ## ## ###"; const sm3 = " # # # # # # "; const pattern1 = new RegExp(`(?=${sm1.replaceAll(" ", ".")})`, "g"); const pattern2 = new RegExp(`(?=${sm2.replaceAll(" ", ".")})`, "g"); const pattern3 = new RegExp(`(?=${sm3.replaceAll(" ", ".")})`, "g"); let sum = 0; for (let lineIdx = 0; lineIdx < map.length - 3; lineIdx++) { const [line1, line2, line3] = map.slice(lineIdx, lineIdx + 3); let solutions1 = new Set(); let solutions2 = new Set(); let solutions3 = new Set(); // console.log("Processing line", lineIdx, line1); for (const match of line1.matchAll(pattern1)) { solutions1.add(match.index); } // console.log(solutions1); for (const match of line2.matchAll(pattern2)) { if (solutions1.has(match.index)) solutions2.add(match.index); } // console.log(solutions2); for (const match of line3.matchAll(pattern3)) { if (solutions2.has(match.index)) solutions3.add(match.index); } // console.log(solutions3); if (solutions3.size) { for (const idx of solutions3) { console.log(`Seamonster found at [${lineIdx}, ${idx}]`); } } sum += solutions3.size; } return sum; } const rot90Map = { n: "w", w: "s", s: "e", e: "n" }; const flipHMap = { w: "e", e: "w" }; const flipVMap = { n: "s", s: "n" }; function applyMap(map, obj, doReverse = false) { const result = {}; for (const [target, source] of Object.entries(map)) { if (obj[source] === undefined) { console.log(source, obj); throw Error("aa"); } result[target] = doReverse ? reverse(obj[source]) : obj[source]; } return { ...obj, ...result }; } class Tile { constructor(rawInput) { const lines = rawInput.split("\n"); this.id = lines.shift().match("[0-9]+")[0]; this.borders = { n: lines[0], e: lines.map((line) => line[line.length - 1]).join(""), s: reverse(lines[lines.length - 1]), w: reverse(lines.map((line) => line[0]).join("")), }; this.content = trimEnds(lines) .map(trimEnds) .map((line) => line.split("")); } rot90(times = 1) { while (times--) { this.borders = applyMap(rot90Map, this.borders); this.content = rotSquare(this.content); } } flipH() { this.borders = applyMap(flipHMap, this.borders, true); this.borders.n = reverse(this.borders.n); this.borders.s = reverse(this.borders.s); this.content.map((row) => row.reverse()); } flipV() { this.borders = applyMap(flipVMap, this.borders, true); this.borders.w = reverse(this.borders.w); this.borders.e = reverse(this.borders.e); this.content.reverse(); } pprint() { let str = ""; str += " " + this.borders.n + " \n"; for (let i = 0; i < this.borders.w.length; i++) { str += reverse(this.borders.w)[i]; if (i === 0 || i === this.borders.w.length - 1) { str += " ".repeat(this.borders.w.length); } else { str += " " + this.content[i - 1].join("") + " "; } str += this.borders.e[i]; str += "\n"; } str += " " + reverse(this.borders.s) + " "; return str; } } main();