Advent of Code 2020 Day 12 - Solution + Tutorial (TypeScript)

published
All TypeScript Solutions for Advent of Code 2020

I'll try to update asap. Please bear in a mind that I'll probably lag one or multiple days behind.

Prerequisites

I assume you've put your puzzle input into an array called lines where each array item is a line of the input text file. It's up to you to either parse the text file or create an array by hand.

const lines = [
  "N2",
  "L180",
  "S5",
  "L90",
  "E3",];

Solution

Preface

Starting with Day 10, I'll just publish my solution for both parts without explaining every single step. Unfortunately, I can't continue providing full step-by-step tutorials for each day. The concepts used get more difficult day by day. So, I've concluded that it's better if I wrote separate blog posts about these concepts later.

Also, it's holiday season. This makes it much more difficult creating well-thought-out tutorials. However, I'll try to annotate my code examples a little. This way, you might understand what I've done.

I will now move on to sharing useful tips for web developers regularly. These should help you become a better developer. Also, the shared tips should help with solving problems like those we encounter in Advent of Code. Here's my first post: 14 Awesome JavaScript Array Tips You Should Know About

Puzzle

Just to make sure, you know what I'm talking about, take a look at today's puzzle:

Day 12: Rain Risk

Part 1

Today, we'll move a ship according to the instructions (our puzzle input). It's possible to move in every compass direction, to turn left or right, and to move forward. Moving forward means to move in the direction the ship is currently facing.

This puzzle can be solved in many ways. In my solution, I've created a Ship class that encapsulates the ship's behavior. Also, I've initialized a variable named compass to help with turning. It has the degrees as keys, and the compass direction as values.

A Ship object has a current position and degrees, that are telling us which direction the ship is facing. Depending on the move direction and value, the ship's position and degrees will change. This is implemented with a switch-statement. The position is thought as a coordinate system having an x and y value. For example, moving north increases the y value (position[1]), and moving west decreases the x value (position[0]). If we want to move forward, we check our current degrees, and our compass to determine the correct compass direction.

With that done, we just have to parse each line and use the Ship#move method. After that, retrieve the x and y value from the ship's position and return the sum of their absolute values. Done!

Here's the full solution:

const compass = {
  0: "N",
  90: "E",
  180: "S",
  270: "W",
};
class Ship {
  protected _degrees: keyof typeof compass;
  protected _position: [x: number, y: number];

  public constructor() {
    this._degrees = 90;
    this._position = [0, 0];
  }

  public move(direction: string, value: number) {
    switch (direction) {
      case "N":
        this._position[1] += value;
        break;
      case "E":
        this._position[0] += value;
        break;
      case "S":
        this._position[1] -= value;
        break;
      case "W":
        this._position[0] -= value;
        break;
      case "F":
        this.move(compass[this._degrees], value);
        break;
      case "L":
        this._degrees -= value;
        if (this._degrees < 0) this._degrees += 360;
        break;
      case "R":
        this._degrees += value;
        if (this._degrees >= 360) this._degrees -= 360;
        break;
    }
  }

  public get position(): [x: number, y: number] {
    return [...this._position];
  }
}
const ship = new Ship();

lines.forEach((line) => {
  const action = line.substr(0, 1);
  const value = parseInt(line.substr(1));
  ship.move(action, value);
});

const [x, y] = ship.position;
return Math.abs(x) + Math.abs(y);

Part 2

Now, part 2 changes the rules a bit. This time our ship orients itself to a waypoint placed relative to the ship. Therefore, we have to modify our Ship class. Again, I see the whole thing as a coordinate system. We don't need our compass variable from part 1 anymore, but we'll add a waypoint property to our Ship class. Thus, we can adjust the ship's position, and the waypoint position.

This time, moving in a compass direction means moving the waypoint. We do so like we moved the ship in part 1. However, moving forward means moving the ship to the waypoint multiple times. Therefore, we'll add multiples of the waypoint's coordinates to our ship's position. Also, turning left and right has changed a bit. We'll have to move our waypoint accordingly.

After implementing all of this, we can reuse some more code from part 1. The parsing of the lines and summing up the absolute values of the position stays the same. Then, we've solved the puzzle.

For completeness, here's the full solution:

class Ship {
  protected _position: [x: number, y: number];
  protected _waypoint: [x: number, y: number];

  public constructor() {
    this._position = [0, 0];
    this._waypoint = [10, 1];
  }

  public move(direction: string, value: number) {
    switch (direction) {
      case "N":
        this._waypoint[1] += value;
        break;
      case "E":
        this._waypoint[0] += value;
        break;
      case "S":
        this._waypoint[1] -= value;
        break;
      case "W":
        this._waypoint[0] -= value;
        break;
      case "F":
        this._position[0] += this._waypoint[0] * value;
        this._position[1] += this._waypoint[1] * value;
        break;
      case "L":
        switch (value) {
          case 90:
            this._waypoint.reverse();
            this._waypoint[0] *= -1;
            break;
          case 180:
            this._waypoint[0] *= -1;
            this._waypoint[1] *= -1;
            break;
          case 270:
            this._waypoint.reverse();
            this._waypoint[1] *= -1;
            break;
        }
        break;
      case "R":
        switch (value) {
          case 90:
            this._waypoint.reverse();
            this._waypoint[1] *= -1;
            break;
          case 180:
            this._waypoint[0] *= -1;
            this._waypoint[1] *= -1;
            break;
          case 270:
            this._waypoint.reverse();
            this._waypoint[0] *= -1;
            break;
        }
        break;
    }
  }

  public get position(): [x: number, y: number] {
    return [...this._position];
  }
}
const ship = new Ship();

lines.forEach((line) => {
  const action = line.substr(0, 1);
  const value = parseInt(line.substr(1));
  ship.move(action, value);
});

const [x, y] = ship.position;
return Math.abs(x) + Math.abs(y);

Conclusion

Today's puzzle somehow reminded me of the game Battleship (in Germany known as Schiffe versenken). Using the Ship class to encapsulate the behavior of the ship made the code much cleaner, in my opinion. All in all, the puzzle felt easier than the days before. Depending on your implementation for part 1 this may differ: For me, part 2 was solved very quickly, because I only had to do some small changes.

Thanks a lot for reading this post. Please consider sharing it with your friends and colleagues. See you tomorrow!

If you like my content and you want to see more, please follow me on Twitter!
Questions, feedback or just wanna chat? Come and join my Discord!
All TypeScript Solutions for Advent of Code 2020

I'll try to update asap. Please bear in a mind that I'll probably lag one or multiple days behind.