Day 16 Part 1

Summary

Determine the amount of tiles the light beam enters.

  • Data a matrix.
  • Light starts from top-left and is moving right.
  • ‘\’ from the right: bounces light south.
  • ‘/’ from the right: bounces light north.
  • ‘-’ splits light east/west if entered north/south, else does nothing.
  • ‘|’ splits light north/south if entered east/weat, else does nothing.

Formatting

My idea to format the data is to create an interface for each tile, basically a dictionary that way it’s easier to manage what titles are energized or not.

1
2
3
4
5
6
7
interface tile {
  contents: string,
  energized: boolean,
  reflected: boolean,
  x: number,
  y: number,
}

Originally I tried to use energized to catch when a splitter has already been hit, so there’s no looping. This didn’t take into account splitters that were passed through and hence reflected.

To transform the initial data into this format, it’s a simple loop

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
export function get_data(data: string[]): tile[][]{
  let matrix_data: tile[][] = [];

  data.forEach((row, y) => {
    let row_data: tile[] = [];
    row.split('').forEach((cell, x) => {
      row_data.push({
        contents: cell,
        energized: false,
        reflected: false,
        x: x,
        y: y,
      });
    });
    matrix_data.push(row_data);
  });

  return matrix_data;
}

Splits

The main twist is the splitters. I’ll have to create a way to store where light splits and where it’s going, so that I can pick up and finish it later. I’m imagining I’ll start with the initial path and continue until it hits a wall and go back to any branches I didn’t take. Another simple interface will work well:

1
2
3
4
5
interface light_points {
  x: number,
  y: number,
  direction: string,
}

Movement

Instead of trying to replicate what I did in Day 15 Part 2 , I went the simpler looping method, instead of depending on x&y coordinates.

The main takeaways are:

  1. The functions return the tile hit or undefined if it hit a wall.
  2. All tiles are energized when they’re hit, even if it has no effect on the path.
  3. All loops plus or minus the original value by 1, this is so it skips the previous tile hit.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
const horizontal_actionable_contents = ['/', '\\', '|']
const vertical_actionable_contents = ['/', '\\', '-']

function go_east(data: tile[][], start_x: number, start_y: number): tile|undefined{
  for (let x = start_x + 1; x < data[start_y].length; x++){

    // energized tile
    data[start_y][x].energized = true;

    // check if action needs to be taken
    if (horizontal_actionable_contents.indexOf(data[start_y][x].contents) !== -1){
      return data[start_y][x];
    }

  };
  return undefined; // hit a wall
}

function go_west(data: tile[][], start_x: number, start_y: number): tile|undefined{
  // x, subtracting to 0 (inclusive)
  for (let x = start_x - 1; 0 <= x; x--){
    data[start_y][x].energized = true;
    if (horizontal_actionable_contents.indexOf(data[start_y][x].contents) !== -1){
      return data[start_y][x];
    }
  };
  return undefined; // hit a wall
}

function go_north(data: tile[][], start_x: number, start_y: number): tile|undefined{
  // y, subtracting to zero (inclusive)
  for (let y = start_y - 1; 0 <= y; y--){
    data[y][start_x].energized = true;
    if (vertical_actionable_contents.indexOf(data[y][start_x].contents) !== -1){
      return data[y][start_x];
    }
  };
  return undefined; // hit a wall
}

function go_south(data: tile[][], start_x: number, start_y: number): tile|undefined{
  // y, adding to length
  for (let y = start_y + 1; y < data.length; y++){
    data[y][start_x].energized = true;
    if (vertical_actionable_contents.indexOf(data[y][start_x].contents) !== -1){
      return data[y][start_x];
    }
  };
  return undefined; // hit a wall
}

Main loop

When I first started creating the loop, it used if statements to determine what function to call based on the returned tile. Once I realized ‘\’ and ‘/’ can reflect in all 4 ways, I chucked that idea and decided to use a map instead.

The format is ${direction_moving}_${mirror_hit}

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
interface call_dict {
  func: Function,
  direction: string,
}

let mirror_map = new Map<string, call_dict>();

mirror_map.set('E_\\', {func: go_south, direction: 'S'});
mirror_map.set('W_\\', {func: go_north, direction: 'N'});
mirror_map.set('N_\\', {func: go_west, direction: 'W'});
mirror_map.set('S_\\', {func: go_east, direction: 'E'});

mirror_map.set('E_/', {func: go_north, direction: 'N'});
mirror_map.set('W_/', {func: go_south, direction: 'S'});
mirror_map.set('N_/', {func: go_east, direction: 'E'});
mirror_map.set('S_/', {func: go_west, direction: 'W'});

I did not encode ‘-’ and ‘|’ in a similar fashion purely because they’re going to create light_point objects so an if is necessary either way.

I also updated the light_point interface to also share this same idea:

1
2
3
4
5
6
7
interface light_point {
  contents: string,
  x: number,
  y: number,
  func: Function,
  direction: string,
}

For the loop I always start small, looping 5-10 items at a time and making sure the basics work:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
let x: number = -1; // start **off** the board
let y: number = 0;
let direction: string = 'E'
let call: Function = go_east

for (let i = 0; i < 5; i++){
  let tile: tile|undefined = call(data, x, y);

  console.log(
    `(${x}, ${y}) ${direction}`
    + ` -> ${tile.x-x}, ${tile.y-y}`
    + ` => (${tile.x}, ${tile.y}) '${tile.contents}'`
  );

  if (['\\', '/'].indexOf(tile.contents) !== -1){
    const result: call_dict = mirror_map.get(`${direction}_${tile.contents}`)
    call = result['func'];
    direction = result['direction'];
  }

  // update coordinates
  x = tile.x;
  y = tile.y;
};

When I hit a splitter I stored the direction and coordinates in an array called lights and set the loop variables to initiate going the way not being stored.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
let lights: light_point[] = [];

for (let i = 0; i < 8; i++){
  // ...
  if (['\\', '/'].indexOf(tile.contents) !== -1){
    // ...
  }else if (tile.contents == '|' && !tile.reflected){
    lights.push({
      contents: tile.contents,
      x: tile.x,
      y: tile.y,
      func: go_north,
      direction: 'N',
    });
    call = go_south;
    direction = 'S';
    tile.reflected = true; // anti-loops
  }else if (tile.contents == '-' && !tile.reflected){
    lights.push({
      contents: tile.contents,
      x: tile.x,
      y: tile.y,
      func: go_east,
      direction: 'E',
    });
    call = go_west;
    direction = 'W';
    tile.reflected = true; // anti-loops
  }else{
    // presumed a `|` or `-` previously reflected
    tile = undefined;
  }

  if (tile !== undefined){
    // update coordinates
    x = tile.x;
    y = tile.y;
  }
};

Once I hit a wall I wrapped the entire if to make sure tile being undefined would never hit that section of code and pop’d an item out of the lights array to initiate for the next loop. Once lights has no items, I break out of the loop as there’s no other light beams to map out. At this point I removed the debugging for loop and replaced it with the final while

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
let lights: light_point[] = [];

while (true){
  // ...

  if (tile !== undefined){
    if (['\\', '/'].indexOf(tile.contents) !== -1){
      // ...
    }else if (tile.contents == '|' && !tile.reflected){
      // ...
    // ...
    }else{
      tile = undefined;
    }
  }

  if (tile !== undefined){
    // update coordinates
    x = tile.x;
    y = tile.y;
  }else{
    if (lights.length){
      // hit an edge, use a point from lights
      let light: light_point = lights.pop();

      // update using stored values
      direction = light.direction;
      call = light['func'];
      x = light.x;
      y = light.y;
    }else{
      // empty => all done
      break;
    }
  }
};

Sum

Once out of the main loop all that needs to be done is to calculate the sum of all the energized tiles. Because of the energized attribute in the tile interface, it’s as easy as looping and adding up the amount… or possibly even a filter to save a line or two :-)

1
2
3
4
5
6
let sum: number = 0;
data.forEach((row) => {
  sum += row.filter((tile) => tile.energized).length;
});

console.log(`Sum => ${sum}`);

Conclusion

I enjoyed this one!- It was a fun challenge and I liked the idea. I did get tripped up on there being loops with splitters and then properly dealing with the splitters.

As with most of these challenges, I do wish the example had a couple more edge cases. I find my solutions generally work with the example but they never work the first time on the input. Looking through verbose outputs for a needle in the haystack kind of sucks.. but, I guess that’s part of the challenge!

On to Part 2!

comments powered by Disqus