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.
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:
- The functions return the tile hit or
undefined
if it hit a wall.
- All tiles are energized when they’re hit, even if it has no effect on the
path.
- 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!