Hung-Yi's LogoHung-Yi’s Journal

Advent of Code 2022: Day 8

A TypeScript solution for the Advent of Code 2022, Day 8 puzzle: 2D arrays and coordinate gymnastics.

Today’s puzzle involves manipulating a two-dimensional grid of numbers, representing heights of trees in a rectangular forest.

The puzzle provides us with an example input, which looks like this.

30373
25512
65332
33549
35390

To begin, we need to parse this input into a data structure that we can work with.

// The forest of tree heights
const FOREST = puzzleInput
  .split('\n')
  .map(row => row
    .split('')
    .map(n => parseInt(n)))

This code splits the input string into individual lines, and then splits each line into an array of individual numbers. It then uses the parseInt() function to convert each string representation of a number into a numerical (tree) height.

Now, we need to determine the size of the forest.

// Some measurements of the forest size
const ROWS = FOREST.length
const LAST_ROW = ROWS - 1;
const COLS = FOREST[0].length;
const LAST_COL = COLS - 1;

This code calculates the number of rows and columns in the forest, as well as the index of the last row and column. These will come in handy later, when we need to check if we’ve reached the edge of the forest.

The next step in solving the puzzle is to determine the line of sight for each tree in the forest. To do this, we need to implement a function that can take a particular point in the forest (i.e. the coordinates of a tree), and a direction, and return a string of coordinates in that direction, to the edge of the forest.

type Direction = 'up' | 'down' | 'left' | 'right';

// Gets a string of coordinates in a given direction
// and from a particular point, to the edge
const lineOfSight = (
  row: number,
  col: number,
  direction: Direction
) => {
  switch (direction) {
    case 'up':
      return Array(row)
        .fill(row)
        .map((x, y) => [x-y-1, col]);
    case 'down':
      return Array(LAST_ROW - row)
        .fill(row)
        .map((x, y) => [x+y+1, col]);
    case 'left':
      return Array(col)
        .fill(col)
        .map((x, y) => [row, x-y-1]);
    case 'right':
      return Array(LAST_COL - col)
        .fill(col)
        .map((x, y) => [row, x+y+1]);
  }
}

We switch on the direction and calculate the number of steps that need to be taken to reach the edge of the forest. Then we use Array() and map() to generate a range of coordinates along the line of sight, which is returned.

As an example, if the size of the forest is 10 by 10 and we call lineOfSight() at the coordinates [5,5] with the direction “up”, we would get…

[[4, 5], [3, 5], [2, 5], [1, 5], [0, 5]]

…which is a string of coordinates from [5,5] to the top edge of the forest.

Once we are able to get the line of sight coordinates like this, we next need to determine whether the line of sight is blocked by a taller tree, and if so, how many trees are visible along the line of sight.

To do this, we can implement a function called look(), which takes a point in the forest, and a direction to move in as arguments, and returns two properties: blocked and distance.

// Look in a direction from a given point to see
// if it's blocked and how many trees you can see
const look = (
  row: number,
  col: number,
  direction: Direction
) => {
  const sight = lineOfSight(row, col, direction);
  const tree = FOREST[row][col];
  const blockedAt = sight.findIndex(([ii, jj]) => FOREST[ii][jj] >= tree);
  const blocked = blockedAt !== -1;
  const distance = blocked ? blockedAt + 1 : sight.length;
  return { blocked, distance };
}

The key things we do are:

  1. Use lineOfSight() to determine the string of tree coordinates in the specified direction to the edge of the forest.
  2. Fetch the height of the tree at the current point from FOREST, and use findIndex() to determine the index of the first tree along the line of sight that is taller than the current tree. If there is no such tree, the findIndex() method will return -1.
  3. Set the blocked property to true if findIndex() returns -1.
  4. Set the distance property to the number of trees up to the blocking tree including the blocking tree, if it is blocked, or the total number of trees along the line of sight, if it is not blocked.

The last bit of preparation is to determine the coordinates of every tree in the forest, which will make it easy to iterate over the trees.

// The coordinates of every tree in the forest
const EVERY_TREE = Array(ROWS)
  .fill(0)
  .flatMap((_, row) => Array(COLS)
    .fill(0)
    .map((_, col) => [row, col]));

We use Array() and map() to generate a 2D array of coordinates for every point in the forest, and then use flatMap() to flatten the array into a single dimension.

Now that we have the coordinates of every tree in the forest, we can use them to survey the whole forest and determine the line of sight distances for each tree.

// Survey the whole forest by looking around
// at every tree coordinate
const SURVEY = EVERY_TREE
  .map(([row, col]) => ({
    up: look(row, col, 'up'),
    left: look(row, col, 'left'),
    right: look(row, col, 'right'),
    down: look(row, col, 'down'),
  }));

To solve part 1, we need to determine the number of trees that are visible from the edge of the forest. This can be done by iterating over SURVEY, and counting the number of trees that have at least one direction in which the line of sight is not blocked by a taller tree.

// Count the trees that are visible in some direction
const part1 = SURVEY
  .filter(({ up, left, right, down }) =>
    !up.blocked || !left.blocked || !right.blocked || !down.blocked
  ).length;

To solve part 2, we need to determine the score for each tree in the forest, and return the highest score. This can be done by iterating over SURVEY, and for each tree, calculating the product of its line of sight distances in each direction. We can use Math.max() to determine the highest product, which is the solution.

// Score the trees by the distance in every direction
// and find the highest score.
const part2 = Math.max(
  ...SURVEY.map(({ up, left, right, down }) =>
    up.distance * left.distance * right.distance * down.distance
  )
);

Final Solution

// The forest of tree heights
const FOREST = puzzleInput
  .split('\n')
  .map(row => row
    .split('')
    .map(n => parseInt(n)))

// Some measurements of the forest size
const ROWS = FOREST.length
const LAST_ROW = ROWS - 1;
const COLS = FOREST[0].length;
const LAST_COL = COLS - 1;

type Direction = 'up' | 'down' | 'left' | 'right';

// Gets a string of coordinates in a given direction
// and from a particular point, to the edge
const lineOfSight = (
  row: number,
  col: number,
  direction: Direction
) => {
  switch (direction) {
    case 'up':
      return Array(row)
        .fill(row)
        .map((x, y) => [x-y-1, col]);
    case 'down':
      return Array(LAST_ROW - row)
        .fill(row)
        .map((x, y) => [x+y+1, col]);
    case 'left':
      return Array(col)
        .fill(col)
        .map((x, y) => [row, x-y-1]);
    case 'right':
      return Array(LAST_COL - col)
        .fill(col)
        .map((x, y) => [row, x+y+1]);
  }
}

// Look in a direction from a given point to see
// if it's blocked and how many trees you can see
const look = (
  row: number,
  col: number,
  direction: Direction
) => {
  const sight = lineOfSight(row, col, direction);
  const tree = FOREST[row][col];
  const blockedAt = sight.findIndex(([ii, jj]) => FOREST[ii][jj] >= tree);
  const blocked = blockedAt !== -1;
  const distance = blocked ? blockedAt + 1 : sight.length;
  return { blocked, distance };
}

// The coordinates of every tree in the forest
const EVERY_TREE = Array(ROWS)
  .fill(0)
  .flatMap((_, row) => Array(COLS)
    .fill(0)
    .map((_, col) => [row, col]));

// Survey the whole forest by looking around
// at every tree coordinate
const SURVEY = EVERY_TREE
  .map(([row, col]) => ({
    up: look(row, col, 'up'),
    left: look(row, col, 'left'),
    right: look(row, col, 'right'),
    down: look(row, col, 'down'),
  }));

// Count the trees that are visible in some direction
const part1 = SURVEY
  .filter(({ up, left, right, down }) =>
    !up.blocked || !left.blocked || !right.blocked || !down.blocked
  ).length;

// Score the trees by the distance in every direction
// and find the highest score.
const part2 = Math.max(
  ...SURVEY.map(({ up, left, right, down }) =>
    up.distance * left.distance * right.distance * down.distance
  )
);

console.log("Part 1:", part1);
console.log("Part 2:", part2);
Part 1: 1543
Part 2: 595080