You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
311 lines
8.4 KiB
JavaScript
311 lines
8.4 KiB
JavaScript
4 years ago
|
/**
|
||
|
* 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();
|