Skip to contents

Day 1: Sonar Sweep

Part 1

# Read in data
dat <- scan(here("inst", "2021", "day1.txt"))

# Count the number of times the depth increases from the previous measurement
sum(diff(dat) > 0)
## [1] 1832

0.012 sec.

Part 2

# Sum depths in each window
index <- 1
summed <- c()
while((index + 2) <= length(dat)) {
  summed <- c(summed, sum(dat[index:(index + 2)]))
  index <- index + 1
}

# Count the number of times the depth increases from the previous measurement
sum(diff(summed) > 0)
## [1] 1858

0.025 sec.

Day 2: Dive!

Part 1

#' Day 2: Dive!
#' @source <https://adventofcode.com/2021/day/2>
#' @name day2
#'
NULL

#' @rdname day2
#' @param path file path
#' @export
#'
read_day2 <- function(path) {
  path |>
    read.table() |>
    setNames(c("direction", "value"))
}

#' @rdname day2
#' @param dat dat
#' @export
dive <- function(dat) {
  summarise_dat <- dat |>
    dplyr::group_by(direction) |>
    dplyr::summarise(total = sum(value))

  horizontal_position <- summarise_dat$total[summarise_dat$direction == "forward"]
  depth <- summarise_dat$total[summarise_dat$direction == "down"] -
    summarise_dat$total[summarise_dat$direction == "up"]

  horizontal_position * depth
}
# Read in data
path <- here("inst", "2021", "day2.txt")
dat <- read_day2(path)

# Calculate the horizontal position and depth and multiply them together
dive(dat)
## [1] 1648020

0.031 sec.

Part 2

#' @rdname day2
#' @param dat dat
#' @export
#'
dive2 <- function(dat) {
  # Initialise variables
  aim <- 0
  horizontal_position <- 0
  depth <- 0

  # Track aim
  for (i in seq_len(nrow(dat))) {
    if (dat$direction[i] == "down") {
      aim <- aim  + dat$value[i]
    } else if (dat$direction[i] == "up") {
      aim <- aim  - dat$value[i]
    } else {
      horizontal_position <- horizontal_position + dat$value[i]
      depth <- depth + (aim * dat$value[i])
    }
  }

  horizontal_position * depth
}
dive2(dat)
## [1] 1759818555

0.016 sec.

Day 3: Binary Diagnostic

Part 1

#' Day 3: Binary Diagnostic
#' @source <https://adventofcode.com/2021/day/3>
#' @name day3
#'
NULL

#' @rdname day3
#' @param path file path
#' @export
#'
read_day3 <- function(path) {
  path |>
    scan(what = "character")
}

#' @rdname day3
#' @param dat dat
#' @export
#'
power_consumption <- function(dat) {
  # Number of bits in each binary number
  digits <- nchar(dat[1])

  # Calculate gamma and epsilon rates
  gamma_binary <- rep(NA, digits)
  epsilon_binary <- rep(NA, digits)

  for (i in seq_len(digits)) {
    # Extract the i[th] character of each string
    character <- vapply(dat, function(x) substr(x, i, i), character(1))
    # Find most common bit
    zeroes <- sum(character == "0")
    ones <- sum(character == "1")
    gamma_binary[i] <- dplyr::if_else(zeroes > ones, 0, 1)
    epsilon_binary[i] <- dplyr::if_else(zeroes < ones, 0, 1)
  }

  gamma_rate <- strtoi(paste(gamma_binary, collapse = ""), base = 2)
  epsilon_rate <- strtoi(paste(epsilon_binary, collapse = ""), base = 2)

  # Calculate power consumption
  gamma_rate * epsilon_rate
}
# Read in data
path <- here("inst", "2021", "day3.txt")
dat <- read_day3(path)

# Calculate power consumption
power_consumption(dat)
## [1] 2954600

0.046 sec.

Part 2

#' @rdname day3
#' @param dat dat
#' @param type type
#' @export
#'
rating <- function(dat, type) {
  # Number of bits in each binary number
  digits <- nchar(dat[1])

  # For each bit in a binary string
  for (i in seq_len(digits)) {
    # Extract the i[th] character of each string
    character <- vapply(dat, function(x) substr(x, i, i),
                        character(1))
    # Find zeroes and ones
    zeroes <- character == "0"
    ones <- character == "1"
    # Find most common bit
    if (sum(zeroes) > sum(ones)) {
      bit_criteria <- which(zeroes)
    } else if (sum(zeroes) < sum(ones)) {
      bit_criteria <- which(ones)
    } else {
      bit_criteria <- which(ones)
    }
    # Keep binary strings with most common bit in the ith position
    if (type == "oxygen") {
      dat <- dat[bit_criteria]
    } else if (type == "CO2") {
      dat <- dat[-bit_criteria]
    }
    # If there's only one binary string left, stop
    if (length(dat) == 1) break
  }
  # Return decimal
  strtoi(paste(dat, collapse = ""), base = 2)
}
# Calculate oxygen generator and CO2 scrubber ratings
oxygen_generator_rating <- rating(dat, "oxygen")
CO2_scrubber_rating <- rating(dat, "CO2")

# Calculate life support rating
oxygen_generator_rating * CO2_scrubber_rating
## [1] 1662846

0.033 sec.

Day 4: Giant Squid

Part 1

#' Day 4: Giant Squid
#' @source <https://adventofcode.com/2021/day/4>
#' @name day4
#'
NULL

#' @rdname day4
#' @param path file path
#' @export
#'
get_numbers <- function(path) {
  path |>
    scan(what = "character", n = 1) |>
    strsplit(",") |>
    unlist() |>
    as.numeric()
}

#' @rdname day4
#' @export
#'
get_boards <- function(path) {
  raw_boards <- read.table(path, skip = 1)
  number_of_boards <- nrow(raw_boards) / 5

  lapply(seq_len(number_of_boards), function(x) {
    start <- 1 + (5 * (x - 1))
    end <- start + 4
    raw_boards[start:end, ]
  })
}

#' @rdname day4
#' @param numbers numbers
#' @param boards boards
#' @export
#'
play_bingo <- function(numbers, boards) {
  # For each number in the bingo call
  for (this_number in numbers) {
    # Check each board
    for (i in seq_along(boards)) {
      # If the number is on the board, mark it off (as NA)
      boards[[i]] <- update_board(this_number, boards[[i]])
      # Check for win
      has_won <- check_for_win(boards[[i]])
      # If this board has won, calculate the winning score
      if (has_won)
        return(sum(boards[[i]], na.rm = TRUE) * this_number)
    }
  }
}

check_for_win <- function(board) {
  check_board <- is.na(board)
  row_win <- any(rowSums(check_board) == 5)
  column_win <- any(colSums(check_board) == 5)
  dplyr::if_else(row_win | column_win, TRUE, FALSE)
}

update_board <- function(number, board) {
  check_number <- board == number
  if (any(check_number, na.rm = TRUE)) {
    # Mark the number
    find_number <- which(check_number, arr.ind = TRUE)
    board[find_number] <- NA
  }
  board
}
# Read in data
path <- here("inst", "2021", "day4.txt")
numbers <- get_numbers(path)
boards <- get_boards(path)

# Play bingo
play_bingo(numbers, boards)
## [1] 31424

0.425 sec.

Part 2

#' @rdname day4
#' @export
#'
lose_bingo <- function(numbers, boards) {
  # Initialise objects
  results <- data.frame(number = numeric(), board = numeric(), score = numeric())
  remaining_boards <- seq_along(boards)

  # For each number in the bingo call
  for (this_number in numbers) {
    # Check each board
    for (i in remaining_boards) {
      # If the number is on the board, mark it off (as NA)
      boards[[i]] <- update_board(this_number, boards[[i]])
      # Check for win
      has_won <- check_for_win(boards[[i]])
      # If this board has won, calculate the winning score
      if (has_won) {
        score <- sum(boards[[i]], na.rm = TRUE) * this_number
        results <- rbind(results,
                         data.frame(number = this_number,
                                    board = i,
                                    score = score))
        remaining_boards <- remaining_boards[-which(remaining_boards == i)]
      }
    }
  }
  results
}
# Play bingo
losers <- lose_bingo(numbers, boards)
tail(losers)
##     number board score
## 95      43    70 14835
## 96      41    76 11767
## 97       9    15  4599
## 98       9    82  3726
## 99      84    89 36120
## 100     82    16 23042

1.113 sec.

Day 5: Hydrothermal Venture

Part 1

#' Day 5: Hydrothermal Venture
#' @source <https://adventofcode.com/2021/day/5>
#' @name day5
#'
NULL

#' @rdname day5
#' @param path file path
#' @export
#'
read_day5 <- function(path) {
  path |>
    read.table() |>
    dplyr::select(-V2) |>
    tidyr::separate(V1, c("x1", "y1")) |>
    tidyr::separate(V3, c("x2", "y2")) |>
    dplyr::mutate(dplyr::across(dplyr::everything(), as.numeric))
}

#' @rdname day5
#' @param coordinates coordinates
#' @export
#'
track_vents <- function(coordinates) {
  # Initialise object
  xlim <- max(c(coordinates$x1, coordinates$x2))
  ylim <- max(c(coordinates$y1, coordinates$y2))
  grid <- matrix(0, nrow = ylim, ncol = xlim)

  # Track horizontal and vertical lines
  for (i in seq_len(nrow(coordinates))) {
    this_line <- coordinates[i, ]

    if (this_line$x1 == this_line$x2) {
      # Horizontal line
      this_x <- this_line$x1
      y_values <- this_line$y1:this_line$y2
      # Add one to each point on the line
      for (y in y_values) grid[this_x, y] <- grid[this_x, y] + 1

    } else if (this_line$y1 == this_line$y2) {
      # Vertical line
      this_y <- this_line$y1
      x_values <- this_line$x1:this_line$x2
      # Add one to each point on the line
      for (x in x_values) grid[x, this_y] <- grid[x, this_y] + 1
    }
  }
  sum(grid >= 2)
}
path <- here("inst", "2021", "day5.txt")
coordinates <- read_day5(path)

# Find points where at least two lines overlap
track_vents(coordinates)
## [1] 6572

0.105 sec.

Part 2

#' @rdname day5
#' @export
#'
track_vents2 <- function(coordinates) {
  # Initialise object
  xlim <- max(c(coordinates$x1, coordinates$x2))
  ylim <- max(c(coordinates$y1, coordinates$y2))
  grid <- matrix(0, nrow = ylim, ncol = xlim)

  # Track horizontal, vertical lines, and diagonal lines
  for (i in seq_len(nrow(coordinates))) {
    this_line <- coordinates[i, ]

    if (this_line$x1 == this_line$x2) {
      # Horizontal line
      this_x <- this_line$x1
      y_values <- this_line$y1:this_line$y2
      # Add one to each point on the line
      for (y in y_values) grid[this_x, y] <- grid[this_x, y] + 1

    } else if (this_line$y1 == this_line$y2) {
      # Vertical line
      this_y <- this_line$y1
      x_values <- this_line$x1:this_line$x2
      # Add one to each point on the line
      for (x in x_values) grid[x, this_y] <- grid[x, this_y] + 1

    } else if(abs(this_line$x1 - this_line$x2) ==
              abs(this_line$y1 - this_line$y2)) {
      # Diagonal line
      x_values <- this_line$x1:this_line$x2
      y_values <- this_line$y1:this_line$y2
      # Add one to each point on the line
      for (i in seq_along(x_values))
        grid[x_values[i], y_values[i]] <- grid[x_values[i], y_values[i]] + 1
    }
  }
  sum(grid >= 2)
}
# Find points where at least two lines overlap
track_vents2(coordinates)
## [1] 21466

0.088 sec.

Day 6: Lanternfish

Part 1

#' Day 6: Lanternfish
#' @source <https://adventofcode.com/2021/day/6>
#' @name day6
#'
NULL

#' @rdname day6
#' @param path file path
#' @export
#'
read_day6 <- function(path) {
  path |>
    scan(what = "character", sep = ",") |>
    as.numeric()
}

#' @rdname day6
#' @param dat dat
#' @export
#'
simulate_lanternfish <- function(dat) {
  # Initialise variables
  days <- 80
  fish <- dat

  # Simulate lanternfish
  for (i in seq_len(days)) {
    # Find zeroes
    zeroes <- which(fish == 0)
    # Subtract 1 from all fish
    fish <- fish - 1
    # Reset zeroes to six
    fish[zeroes] <- 6
    # Add eights for each zero
    fish <- c(fish, rep(8, length(zeroes)))
  }
  length(fish)
}
# Read in data
path <- here("inst", "2021", "day6.txt")
dat <- read_day6(path)

# How many lanternfish would there be after 80 days?
simulate_lanternfish(dat)
## [1] 390923

0.066 sec.

Part 2

It takes too much memory to simulate individual fish in a vector, so generate a frequency table instead.

#' @rdname day6
#' @export
#'
simulate_lanternfish2 <- function(dat) {
  # Initialise variables
  days <- 256

  # Generate a frequency table
  fish_counts <- data.frame(age = dat) |>
    dplyr::count(age) |>                     # Count fish
    tidyr::complete(age = 0:8, fill = list(n = 0))  # Fill in the missing categories

  for (i in seq_len(days)) {
    # Number of zeroes
    n_zeroes <- fish_counts$n[fish_counts$age == 0]
    # Subtract 1 from all fish
    fish_counts$n[1:(nrow(fish_counts) - 1)] <- fish_counts$n[2:nrow(fish_counts)]
    # Reset zeroes to six
    age_6 <- which(fish_counts$age == 6)
    fish_counts$n[age_6] <- fish_counts$n[age_6] + n_zeroes
    # Add eights for each zero
    fish_counts$n[which(fish_counts$age == 8)] <- n_zeroes
  }
  sum(fish_counts$n) |>
    format(scientific = FALSE)
}
# How many lanternfish would there be after 256 days?
simulate_lanternfish2(dat)
## [1] "1749945484935"

0.075 sec.

Day 7: The Treachery of Whales

Part 1

#' Day 7: The Treachery of Whales
#' @source <https://adventofcode.com/2021/day/7>
#' @name day7
#'
NULL

#' @rdname day7
#' @param path file path
#' @export
#'
read_day7 <- function(path) {
  path |>
    scan(what = "character", sep = ",") |>
    as.numeric()
}

#' @rdname day7
#' @param dat dat
#' @export
#'
track_crabs <- function(dat) {
  # Initialise objects
  xlim <- max(dat)
  results <- data.frame(x = 1:xlim, total_fuel_cost = NA)

  # Determine the horizontal position that the crabs can align to using the least
  # fuel possible
  for (position in seq_len(xlim)) {
    fuel_cost <- 0
    for (crab in dat) {
      fuel_cost <- fuel_cost + abs(crab - position)
    }
    results$total_fuel_cost[position] <- sum(fuel_cost)
  }
  lowest <- which.min(results$total_fuel_cost)
  results$x[lowest]

  results$total_fuel_cost[lowest]
}
# Read in data
path <- here("inst", "2021", "day7.txt")
dat <- read_day7(path)

# How much fuel must they spend to align to that position?
track_crabs(dat)
## [1] 356992

0.282 sec.

Part 2

#' @rdname day7
#' @export
#'
track_crabs2 <- function(dat) {
  # Initialise objects
  xlim <- max(dat)
  results <- data.frame(x = 1:xlim, total_fuel_cost = NA)

  # Determine the horizontal position that the crabs can align to using the least
  # fuel possible
  for (position in seq_len(xlim)) {
    fuel_cost <- 0
    for (crab in dat) {
      distance <- abs(crab - position)
      fuel_cost <- fuel_cost + sum(0:distance)
    }
    results$total_fuel_cost[position] <- sum(fuel_cost)
  }

  lowest <- which.min(results$total_fuel_cost)
  results$x[lowest]

  results$total_fuel_cost[lowest]
}
# How much fuel must they spend to align to that position?
track_crabs2(dat)
## [1] 101268110

1.14 sec.

Part 1

#' Day 8: Seven Segment Search
#' @source <https://adventofcode.com/2021/day/8>
#' @name day8
#'
NULL

#' @rdname day8
#' @param path file path
#' @export
#'
read_day8 <- function(path) {
  path |>
    scan(what = "character", sep = "\n")
}

#' @rdname day8
#' @param dat dat
#' @export
#'
count_digits <- function(dat) {
  results <- c()
  for (i in seq_along(dat)) {
    tmp <- strsplit(dat[i], "\\| ")[[1]][2]
    digits <- strsplit(tmp, " ")[[1]]
    results[i] <- sum(nchar(digits) %in% c(2, 4, 3, 7))
  }
  sum(results)
}
# Read in data
path <- here("inst", "2021", "day8.txt")
dat <- read_day8(path)

# How many times do digits 1, 4, 7, or 8 (length 2, 4, 3, and 7) appear?
count_digits(dat)
## [1] 321

0.019 sec.

Part 2

#' @rdname day8
#' @param dat dat
#' @export
#'
decode_segments <- function(dat) {
  # Determine all of the wire/segment connections
  results <- c()
  for (i in seq_along(dat)) {
    segments <- strsplit(dat[i], " \\|")[[1]][1]
    segments <- strsplit(segments, " ")[[1]]

    # Known
    one <- find_digit(segments, 2)
    four <- find_digit(segments, 4)
    seven <- find_digit(segments, 3)
    eight <- find_digit(segments, 7)

    # Remaining segments
    remaining <- segments[-which(nchar(segments) %in% c(2, 4, 3, 7))]
    # six, nine, zero (6)
    length_six <- remaining[which(nchar(remaining) == 6)]
    six <- contains_subset(length_six, one, FALSE)
    nine <- contains_subset(length_six, four, TRUE)
    zero <- length_six[!(length_six %in% c(six, nine))]
    # two, three, five (5)
    length_five <- remaining[which(nchar(remaining) == 5)]
    five <- subset_of(length_five, six)
    length_five <- length_five[!length_five %in% five]
    three <- subset_of(length_five, nine)
    two <- length_five[!length_five %in% three]

    # Initialise dictionary
    dict <- data.frame(number = 0:9,
                       segments = c(zero, one, two, three, four,
                                    five, six, seven, eight, nine))

    # Decode the four-digit output values
    digits <- strsplit(dat[i], "\\| ")[[1]][2]
    digits <- strsplit(digits, " ")[[1]]
    results[i] <- decode(digits, dict)
  }
  sum(results)
}

#' Find string in `string_vector` of length `num_characters`
#'
find_digit <- function(string_vector, num_characters) {
  string_vector[which(nchar(string_vector) == num_characters)]
}

#' Split string into vector of letters
#'
deconstruct <- function(string) {
  strsplit(string, "")[[1]]
}

#' contains_subset
#'
#' Find (1) which string in `unknown_vector` contains all segments
#' (characters) in `known_string` or (2) which string is the only one that
#' doesn't
#' @param unknown_vector an unknown vector
#' @param known_string a known string
#' @param contains (optional) default is `TRUE`
#'
contains_subset <- function(unknown_vector, known_string, contains = TRUE) {
  results <- c()
  for (i in seq_along(unknown_vector)) {
    known_segments <- deconstruct(known_string)
    unknown_segments <- deconstruct(unknown_vector[i])
    if (contains) {
      results[i] <- all(known_segments %in% unknown_segments)
    } else {
      results[i] <- !all(known_segments %in% unknown_segments)
    }
  }
  unknown_vector[which(results)]
}

#' Find the string in `unknown_vector` whose segments (characters) are a
#' subset of `known_string`
#'
subset_of <- function(unknown_vector, known_string) {
  results <- c()
  for (i in seq_along(unknown_vector)) {
    known_segments <- deconstruct(known_string)
    unknown_segments <- deconstruct(unknown_vector[i])
    results[i] <- all(unknown_segments %in% known_segments)
  }
  unknown_vector[which(results)]
}

#' Decode each string (digit) in `unknown_vector`
#'
decode <- function(unknown_vector, dictionary) {
  code <- c()
  for (i in seq_along(unknown_vector)) {
    # Check against each dictionary entry
    entry <- c()
    for (j in seq_len(nrow(dictionary))) {
      length_match <- nchar(dictionary$segments[j]) == nchar(unknown_vector[i])
      characters_match <- all(deconstruct(dictionary$segments[j]) %in%
                                deconstruct(unknown_vector[i]))
      entry[j] <- length_match && characters_match
    }
    code[i] <- dictionary$number[which(entry)]
  }
  as.numeric(paste(code, collapse = ""))
}
# What do you get if you add up all of the output values?
decode_segments(dat)
## [1] 1028926

0.212 sec.

Day 9: Smoke Basin

Part 1

#' Day 9: Smoke Basin
#' @source <https://adventofcode.com/2021/day/9>
#' @name day9
#'
NULL

#' @rdname day9
#' @param path file path
#' @export
#'
read_day9 <- function(path) {
  path |>
    readLines() |>
    strsplit("") |>
    do.call(what = "rbind") |>
    apply(1, as.numeric)
}

#' @rdname day9
#' @param dat dat
#' @export
#'
low_points <- function(dat) {
  # Find all of the low points on the heightmap and calculate risk level
  lowest_points <- data.frame(x = numeric(), y = numeric(), risk = numeric())
  for (i in seq_len(nrow(dat))) {
    for (j in seq_len(ncol(dat))) {
      lowest_point <- compare_heights(dat, i, j)
      lowest_points <- rbind.data.frame(lowest_points, lowest_point)
    }
  }
  lowest_points
}

#' Find `height` of adjacent points and if it's lower than all
#' `adjacent_points`, return risk level
compare_heights <- function(dat, row, column) {
  height <- dat[row, column]
  # Extract adjacent heights
  adjacent_points <- c()
  if (row != 1)
    adjacent_points <- c(adjacent_points, dat[row - 1, column]) # above
  if (column != ncol(dat))
    adjacent_points <- c(adjacent_points, dat[row, column + 1]) # right
  if (row != nrow(dat))
    adjacent_points <- c(adjacent_points, dat[row + 1, column]) # below
  if (column != 1)
    adjacent_points <- c(adjacent_points, dat[row, column - 1]) # left
  # Compare height to adjacent heights
  if (all(height < adjacent_points))
    return(data.frame(x = row, y = column, risk = height + 1))
}
# Read in data
path <- here("inst", "2021", "day9.txt")
dat <- read_day9(path)

# What is the sum of the risk levels of all low points on your heightmap?
lowest_points <- low_points(dat)
sum(lowest_points$risk)
## [1] 603

0.677 sec.

Part 2

#' @rdname day9
#' @param lowest_points lowest_points
#' @export
#'
survey_basins <- function(dat, lowest_points) {
  # Find all of the low points on the heightmap and calculate basin size
  results <- c()
  for (i in seq_len(nrow(lowest_points))) {
    # cat("\r", i, "/", nrow(lowest_points))
    size <- survey(dat, lowest_points$x[i], lowest_points$y[i])
    results <- c(results, size)
  }
  prod(tail(sort(results), 3))
}

#' For a particular `lowest_point`, survey the basin and return it's size
survey <- function(dat, row, column) {
  # Height of lowest point being surveyed
  height <- dat[row, column]
  # Initialise basin dataframe
  basin <- data.frame(row = row, column = column, risk = height + 1)
  # Find adjacent points with depth less than 9
  neighbours <- check_adjacent(dat, row, column)
  # Add them to the basin
  tmp <- neighbours |>
    dplyr::mutate(risk = height + 1) |>
    dplyr::select(-height)
  basin <- rbind(basin, tmp)

  # Check neighbouring points
  continue <- nrow(neighbours) > 0
  while(continue) {
    more_neighbours <- check_adjacent(dat,
                                      neighbours$row[1],
                                      neighbours$column[1])
    # Remove this point from `neighbours`
    neighbours <- neighbours[-1, ]
    # Remove new neighbours that are already in the basin
    more_neighbours <- setdiff(dplyr::select(more_neighbours, -height),
                               dplyr::select(basin, -risk)) |>
      dplyr::left_join(more_neighbours, by = c("row", "column"))

    # If any new neighbours are remaining
    if (nrow(more_neighbours) > 0) {
      # Add them to the basin
      tmp <- more_neighbours |>
        dplyr::mutate(risk = height + 1) |>
        dplyr::select(-height)
      basin <- rbind(basin, tmp)
      # And add them to `neighbours` (for checking)
      neighbours <- rbind(neighbours, more_neighbours)
    }

    continue <- nrow(neighbours) > 0
  }
  # Return size of basin
  nrow(basin)
}

#' Return neighbouring points that have a height less than 9
check_adjacent <- function(dat, row, column) {
  # Find height of adjacent points
  adjacent_points <- data.frame(row = numeric(), column = numeric(),
                                height = numeric())

  if (row != 1)            # Above
    adjacent_points <- rbind(adjacent_points, check_height(dat, row - 1, column))
  if (column != ncol(dat)) # Right
    adjacent_points <- rbind(adjacent_points, check_height(dat, row, column + 1))
  if (row != nrow(dat))    # Below
    adjacent_points <- rbind(adjacent_points, check_height(dat, row + 1, column))
  if (column != 1)         # Left
    adjacent_points <- rbind(adjacent_points, check_height(dat, row, column - 1))

  # Return neibouring points that should be checked
  adjacent_points |>
    dplyr::filter(height != 9)
}

#' Check height is less than 9
check_height <- function(dat, row, column) {
  if (dat[row, column] < 9)
    return(data.frame(row = row, column = column, height = dat[row, column]))
}
# Multiply together the sizes of the three largest basins
survey_basins(dat, lowest_points)
## [1] 786780

57.328 sec.

Day 10: Syntax Scoring

Part 1

#' Day 10: Syntax Scoring
#' @source <https://adventofcode.com/2021/day/10>
#' @name day10
#'
NULL

#' @rdname day10
#' @param path file path
#' @export
#'
read_day10 <- function(path) {
  path |>
    readLines()
}

#' @rdname day10
#' @param dat dat
#' @export
#'
syntax_score <- function(dat) {
  total <- 0
  remove_lines <- c()

  for (i in seq_along(dat)){
    expecting <- c()
    points <- NA
    check_this <- strsplit(dat[i], "")[[1]]

    for (j in seq_along(check_this)) {
      if (check_this[j] == "("){
        expecting <- c(expecting, ")")

      } else if (check_this[j] == "[") {
        expecting <- c(expecting, "]")

      } else if (check_this[j] == "{") {
        expecting <- c(expecting, "}")

      } else if (check_this[j] == "<") {
        expecting <- c(expecting, ">")

      } else {
        # If `check_this` matches the last expected value, remove it
        if (check_this[j] == tail(expecting, 1)) {
          expecting <- expecting[-length(expecting)]

        } else if (is.na(points)) {
          # Otherwise add the score to `total`
          points <- corrupted_score(check_this[j])
          total <- total + points
          remove_lines <- c(remove_lines, i)
          break
        }
      }
    }
  }
  list(total = total,
       remove_lines = remove_lines)
}

corrupted_score <- function(character) {
  score_table <- data.frame(char = c(")", "]", "}", ">"),
                            val = c(3, 57, 1197, 25137))
  score_table$val[which(score_table$char == character)]
}
# Read in data
path <- here("inst", "2021", "day10.txt")
dat <- read_day10(path)

# Find the first illegal character in each corrupted line of the navigation
# subsystem. What is the total syntax error score for those errors?
tmp <- syntax_score(dat)
tmp$total
## [1] 296535

0.066 sec.

Part 2

#' @rdname day10
#' @export
#'
syntax_score2 <- function(dat, remove_lines) {
  # Remove corrupted lines
  incomplete_lines <- dat[-remove_lines]

  results <- c()
  for (i in seq_along(incomplete_lines)){
    expecting <- c()
    check_this <- strsplit(incomplete_lines[i], "")[[1]]
    points <- 0

    for (j in seq_along(check_this)) {
      if (check_this[j] == "("){
        expecting <- c(expecting, ")")

      } else if (check_this[j] == "[") {
        expecting <- c(expecting, "]")

      } else if (check_this[j] == "{") {
        expecting <- c(expecting, "}")

      } else if (check_this[j] == "<") {
        expecting <- c(expecting, ">")
      } else {
        # If `check_this` matches the last expected value, remove it
        if (check_this[j] == tail(expecting, 1)) {
          expecting <- expecting[-length(expecting)]
        }
      }
    }
    expecting <- rev(expecting)

    # Calculate score
    for (k in seq_along(expecting)) {
      points <- (points * 5) + incomplete_score(expecting[k])
    }
    results <- c(results, points)
  }
  index <- ceiling(length(results) / 2)
  value <- sort(results)[index]
  format(value, scientific = FALSE)
}

incomplete_score <- function(character) {
  score_table <- data.frame(char = c(")", "]", "}", ">"),
                            val = c(1, 2, 3, 4))
  score_table$val[which(score_table$char == character)]
}
# Find the completion string for each incomplete line, score the completion
# strings, and sort the scores. What is the middle score?
remove_lines <- tmp$remove_lines
syntax_score2(dat, remove_lines)
## [1] "4245130838"

0.148 sec.

Day 11: Dumbo Octopus

Part 1

#' Day 11: Dumbo Octopus
#' @source <https://adventofcode.com/2021/day/11>
#' @name day11
#'
NULL

#' @rdname day11
#' @param path file path
#' @export
#'
read_day11 <- function(path) {
  path |>
    scan(what = "character") |>
    strsplit("") |>
    do.call(what = rbind) |>
    apply(1, as.numeric)
}

#' @rdname day11
#' @param octopus octopus
#' @export
#'
simulate_octopus <- function(octopus) {
  count <- 0
  for (i in 1:100) {
    # cat("\r", i, "of 100")
    data <- timestep(octopus)
    octopus <- data$octopus
    count <- count + data$latest_flashes
  }
  count
}

timestep <- function(octopus) {
  # Increases the energy level of each octopus by 1
  octopus <- octopus + 1
  done <- matrix(nrow = 0, ncol = 2)

  # Octopi with energy levels > 9 flash and their energy levels reset to 0,
  # the energy level of all adjacent octopuses increases by 1. If this causes
  # an octopus to have an energy level > 9, it also flashes
  while (any(octopus > 9)) {
    flash <- which(octopus > 9, arr.ind = T)    # if energy is > 9
    done <- rbind(done, flash)                  # flash
    octopus[flash] <- 0                         # and reset energy level

    # For each flashing octopus, find neighbouring octopi, and increase their
    # energy levels by 1
    for (j in seq_len(nrow(flash))) {
      x <- unname(flash[j, "row"])
      y <- unname(flash[j, "col"])
      neighbours <- find_neighbours(x, y, flash, done, octopus)
      octopus[neighbours] <- octopus[neighbours] + 1
    }
  }

  # Latest flashes
  list(octopus = octopus, latest_flashes = nrow(done))
}

find_neighbours <- function(x, y, flashing_now, flashed_previously, octopus) {
  # Index neighbours
  tmp <- rbind(data.frame(row = x - 1, col = (y - 1):(y + 1)), # above
               data.frame(row = x, col = c(y - 1, y + 1)),     # sides
               data.frame(row = x + 1, col = (y - 1):(y + 1))) # below

  # Remove invalid coordinates
  xlim <- ncol(octopus) + 1
  ylim <- nrow(octopus) + 1
  tmp <- dplyr::filter(tmp, row != 0, col != 0, row != xlim, col != ylim)

  # Add to `neighbours`, remove duplicates, and remove those that are flashing
  # now or have flashed previously
  tmp |>
    unique() |>
    dplyr::anti_join(data.frame(flashing_now), by = c("row", "col")) |>
    dplyr::anti_join(data.frame(flashed_previously), by = c("row", "col")) |>
    as.matrix()
}
# Read in data
test <- here("inst", "2021", "day11-test.txt")
path <- here("inst", "2021", "day11.txt")

test_dat <- read_day11(test)
dat <- read_day11(path)

# Test simulation
assertthat::assert_that(simulate_octopus(test_dat) == 1656)
## [1] TRUE
# Given the starting energy levels of the dumbo octopuses in your cavern,
# simulate 100 steps. How many total flashes are there after 100 steps?
simulate_octopus(dat)
## [1] 1743

7.496 sec.

Part 2

#' @rdname day11
#' @export
#'
simulate_octopus2 <- function(octopus) {
  count <- 1
  # What is the first step during which all octopuses flash?
  while (sum(octopus) != 0) {
    # cat("\r", count)
    data <- timestep(octopus)
    octopus <- data$octopus
    count <- count + 1
  }
  count
}
# What is the first step during which all octopuses flash?
simulate_octopus2(dat)
## [1] 365

25.377 sec.

Day 12: Passage Pathing

Part 1

#' Day 12: Passage Pathing
#' @source <https://adventofcode.com/2021/day/12>
#' @name day12
#'
NULL

#' @rdname day12
#' @param path file path
#' @export
#'
read_day12 <- function(path) {
  dat <- path |>
    read.table(sep = "-")
  rbind(dat, dplyr::rename(dat, V1 = V2, V2 = V1)) |>
    dplyr::rename(from = V1,
                  to = V2) |>
    dplyr::filter(from != "end",
                  to != "start")
}

#' @rdname day12
#' @param dat data
#' @export
#'
cave_routes <- function(dat) {
  # Initialise routes
  completed_routes <- list()
  unfinished_routes <- dplyr::filter(dat, from == "start")
  unfinished_routes <- lapply(seq_len(nrow(unfinished_routes)),
                              function(x) unname(unlist(unfinished_routes[x,])))

  # Plot routes through cave system
  while (length(unfinished_routes) > 0) {
    this_route <- unfinished_routes[[1]]
    this_cave <- tail(this_route, 1)

    # Record next step(s) of the route
    next_steps <- dplyr::filter(dat, from == this_cave, to != "start")$to

    # If the exit has been found, move the route to `completed_routes`
    if ("end" %in% next_steps) {
      completed_routes <- c(completed_routes, list(c(this_route, "end")))
      next_steps <- setdiff(next_steps, "end")
    }

    # If the cave is small and has already been visited, ignore it
    small <- next_steps[vapply(next_steps, function(x)
      grepl("^[[:lower:]]+$", x), logical(1))]
    big <- setdiff(next_steps, small)
    next_steps <- c(setdiff(small, this_route), big)

    # Record next step(s) of the route
    add_these <- lapply(next_steps, function(x) c(this_route, x))

    # Record steps and tidy up
    unfinished_routes <- c(unfinished_routes, add_these)
    unfinished_routes <- unfinished_routes[-1]
  }
  length(completed_routes)
}
# Read in data
path <- here("inst", "2021", "day12.txt")
dat <- read_day12(path)

# How many paths through this cave system are there that visit small caves at
# most once?
cave_routes(dat)
## [1] 3887

10.912 sec.

Part 2

We have to use a different method for Part 2, since it takes too long to calculate the number of routes.

#' @rdname day12
#' @export
#'
cave_routes2 <- function(dat) {
  # Initialise routes
  unfinished <- dplyr::filter(dat, from == "start")
  complete <- data.frame()

  last_step <- "to"

  # Plot routes through cave system
  while (nrow(unfinished) != 0) {
    # Record next step(s) of the route
    unfinished <- dplyr::left_join(unfinished, dat,
                                   by = setNames("from", last_step))

    # Extract column name of the last step
    last_step <- tail(names(unfinished), 1)

    # For each unfinished route, count the number of small caves that were
    # visited more than once
    test <- apply(unfinished, 1, function(x) {
      duplicates <- x[duplicated(unlist(x))]
      islower <- grepl("^[[:lower:]]+$", duplicates)
      sum(islower)})
    # Remove routes where more than one small cave was visited more than once
    index <- which(test > 1)
    if (length(index) > 0)
      unfinished <- unfinished[-index, ]

    # Add completed routes to `complete`
    complete <- dplyr::bind_rows(complete,
                                 dplyr::filter(unfinished,
                                               get(last_step) == "end"))

    # Remove completed routes from `unfinished`
    unfinished <- dplyr::filter(unfinished, get(last_step) != "end")
  }
  nrow(complete)
}
# If we visit a single small cave twice, how many paths through this cave
# system are there?
cave_routes2(dat)
## [1] 104834

15.743 sec.

Day 13: Transparent Origami

Part 1

#' Day 13: Transparent Origami
#' @source <https://adventofcode.com/2021/day/13>
#' @name day13
#'
NULL

#' @rdname day13
#' @param path file path
#' @export
#'
read_day13 <- function(path) {
  read.table(path, sep = "-")
}

#' @rdname day13
#' @param dat dat
#' @export
#'
origami_instructions <- function(dat) {
  dplyr::filter(dat, grepl("^fold", V1)) |>
    dplyr::mutate(V1 = gsub("^.*([a-z]=[0-9]*)$", "\\1", V1)) |>
    tidyr::separate(V1, c("axis", "value")) |>
    dplyr::mutate(value = as.numeric(value) + 1)
}

#' @rdname day13
#' @export
#'
origami_paper <- function(dat) {
  dots <- dat |>
    dplyr::filter(!grepl("^fold", V1)) |>
    tidyr::separate(V1, c("y", "x"), convert = TRUE) |>
    dplyr::mutate(x = x + 1,
                  y = y + 1) |>
    dplyr::select(x, y)

  xlim <- max(dots$x)
  ylim <- max(dots$y)

  # Generate grid of dots
  paper <- matrix(FALSE, nrow = xlim, ncol = ylim)
  for (i in seq_len(nrow(dots)))
    paper[dots$x[i], dots$y[i]] <- TRUE

  paper
}

#' @rdname day13
#' @param grid grid
#' @param value value
#' @export
#'
fold_left <- function(grid, value) {
  left <- grid[, 1:(value - 1)]
  right <- grid[, (value + 1):ncol(grid)]
  pad <- matrix(0, nrow = nrow(grid), ncol = abs(ncol(left) - ncol(right)))
  if (ncol(right) < ncol(left)) {
    right <- cbind(right, pad)
  } else if (ncol(right) > ncol(left)) {
    left <- cbind(pad, left)
  }
  left | right[, ncol(right):1]
}
# Read in data
path <- here("inst", "2021", "day13.txt")
dat <- read_day13(path)

paper <- origami_paper(dat)
instructions <- origami_instructions(dat)

# How many dots are visible after completing just the first fold instruction on
# your transparent paper?
result <- fold_left(paper, instructions[1,]$value)
sum(result > 0)
## [1] 847

0.059 sec.

Part 2

#' @rdname day13
#' @param paper paper
#' @export
#'
simulate_origami <- function(paper, instructions) {
  # Finish folding the transparent paper according to the instructions. The
  # manual says the code is always eight capital letters.
  for (i in seq_len(nrow(instructions))) {
    if (instructions$axis[i] == "x") {
      paper <- fold_left(paper, instructions$value[i])

    } else if(instructions$axis[i] == "y") {
      paper <- fold_up(paper, instructions$value[i])
    }
  }
  coords <- which(paper, arr.ind = TRUE) |>
    as.data.frame() |>
    dplyr::mutate(value = 1,
                  row = max(row) - row)

  ggplot2::ggplot(coords, ggplot2::aes(x = col, y = row, fill = value)) +
    ggplot2::geom_tile(fill = "goldenrod2") +
    ggplot2::coord_fixed() +
    ggplot2::theme_void() +
    ggplot2::theme(legend.position = "none")
}

#' @description Make a horizontal fold
#' @rdname day13
#' @export
#'
fold_up <- function(grid, value) {
  upper <- grid[1:(value - 1), ]
  lower <- grid[(value + 1):nrow(grid), ]
  pad <- matrix(FALSE, nrow = abs(nrow(upper) - nrow(lower)), ncol = ncol(upper))
  if (nrow(upper) < nrow(lower)) {
    upper <- rbind(pad, upper)
  } else if (nrow(upper) > nrow(lower)) {
    lower <- rbind(lower, pad)
  }
  upper | lower[nrow(lower):1, ]
}
# What code do you use to activate the infrared thermal imaging camera system?
simulate_origami(paper, instructions)

0.19 sec.

Day 14: Extended Polymerization

Part 1

#' Day 14: Extended Polymerization
#' @source <https://adventofcode.com/2021/day/14>
#' @name day14
#'
NULL

#' @rdname day14
#' @param path file path
#' @export
#'
get_template <- function(path) {
  strsplit(readLines(path, n = 1), "") |>
    unlist()
}

#' @rdname day14
#' @export
#'
get_rules <- function(path) {
  tmp <- read.table(path, skip = 1)
  data.frame(first  = substr(tmp[, 1], 1, 1),
             last = substr(tmp[, 1], 2, 2),
             insert = tmp[, 3])
}

#' @rdname day14
#' @param template template
#' @param rules rules
#' @param n n
#' @export
#'
polymerization <- function(template, rules, n) {
  for (i in seq_len(n)) {
    tmp <- convert_template(template) |>
      dplyr::left_join(rules, by = c("first", "last"))
    template <- tmp |> dplyr::select(insert, last) |>
      t() |>
      as.vector() |>
      append(tmp$first[1], 0)
  }

  freq <- table(template)
  max(freq) - min(freq)
}

convert_template <- function(template) {
  data.frame(first = head(template, -1),
             last = tail(template, -1))
}
# Read in data
test <- here("inst", "2021", "day14-test.txt")
path <- here("inst", "2021", "day14.txt")

# Read in polymer template
test_template <- get_template(test)
template <- get_template(path)

# Read in pair insertion rules
test_rules <- get_rules(test)
rules <- get_rules(path)

# Run simulation
assertthat::assert_that(polymerization(test_template, test_rules, 10) == 1588)
## [1] TRUE

0.073 sec.

# What do you get if you take the quantity of the most common element and
# subtract the quantity of the least common element?
polymerization(template, rules, 10)
## [1] 2027

0.041 sec.

Part 2

The second part asks for 40 iterations, which would take far too long using the same method used in part 1 (keeping track of an ever-growing string). So instead, we count the frequency of each pair.

#' @rdname day14
#' @export
#'
polymerization2 <- function(template, rules, n) {
  first <- template[1]
  template <- convert_template(template) |>
    dplyr::mutate(count = 1)

  for (i in seq_len(n)) {
    tmp <- dplyr::left_join(template, rules, by = c("first", "last"))

    set1 <- tmp |> dplyr::select(-last) |> dplyr::rename(last = insert)
    set2 <- tmp |> dplyr::select(-first) |> dplyr::rename(first = insert)
    pairs <- rbind(set1, set2)

    counts <- pairs |>
      dplyr::group_by(first, last) |>
      dplyr::summarise(count = sum(count), .groups = "drop")

    template <- counts
  }

  # Final count
  total <- template |>
    dplyr::select(-first) |>
    dplyr::group_by(last) |>
    dplyr::summarise(count = sum(count)) |>
    dplyr::mutate(count = dplyr::case_when(last == first ~ count + 1,
                                           TRUE ~ count))

  max(total$count) - min(total$count)
}
# Apply 40 steps of pair insertion to the polymer template and find the
# most and least common elements in the result
assertthat::assert_that(
  polymerization2(test_template, test_rules, 40) == 2188189693529)
## [1] TRUE
# What do you get if you take the quantity of the most common element
# and subtract the quantity of the least common element?
polymerization2(template, rules, 40) |> 
  format(scientific = FALSE)
## [1] "2265039461737"

0.424 sec.

Day 15: Chiton

Day 16: Packet Decoder

Day 17: Trick Shot

Part 1

#' Day 17: Trick Shot
#' @source <https://adventofcode.com/2021/day/17>
#' @name day17
#'
NULL

#' @rdname day17
#' @param path file path
#' @export
#'
read_day17 <- function(path) {
  dat <- path |>
    readLines()

  # Extract target coordinates
  xrange <- gsub("^.*x=(.*)\\.\\.(.*),.*$", "\\1 \\2", dat) |>
    strsplit(" ") |>
    unlist() |>
    as.numeric()
  yrange <- gsub("^.*y=(.*)\\.\\.(.*)$", "\\1 \\2", dat) |>
    strsplit(" ") |>
    unlist() |>
    as.numeric()

  list(xrange = xrange, yrange = yrange)
}

#' @rdname day17
#' @param xrange xrange
#' @param yrange yrange
#' @export
#'
simulate_launch <- function(xrange, yrange) {
  start <- c(0, 0)
  velocity <- c(0, 1)
  n <- 1

  results <- data.frame()

  for (i in 1:120) {
    trial <- data.frame(trial = n,
                        step = 0,
                        x = start[1],
                        y = start[2],
                        vx = velocity[1],
                        vy = velocity[2])

    results <- rbind(results, launch(trial, n, xrange, yrange))

    n <- n + 1

    if (tail(results, 1)$x < xrange[1]) {
      velocity[1] <- velocity[1] + 1
    } else if (tail(results, 1)$x > xrange[2]) {
      velocity[1] <- velocity[1] - 1
    } else {
      velocity[2] <- velocity[2] + 1
    }
  }

  this_trial <- results |>
    dplyr::filter(hit == T) |>
    dplyr::filter(y == max(y)) |>
    dplyr::pull(trial) |>
    unique()

  # Initial y velocity
  results |>
    dplyr::filter(trial == this_trial,
                  step == 0) |>
    dplyr::pull(vy)

  # What is the highest y position it reaches on this trajectory?
  results |>
    dplyr::filter(trial == this_trial) |>
    dplyr::filter(y == max(y)) |>
    dplyr::pull(y) |>
    unique()
}

#' @description Move one step
#' @rdname day17
#' @param status last step
#'
move <- function(status) {
  x <- status$x + status$vx
  y <- status$y + status$vy

  vx <- status$vx
  vy <- status$vy
  vx <- dplyr::if_else(vx > 0, vx - 1,
                       dplyr::if_else(vx < 0, vx + 1, vx))
  vy <- vy - 1

  data.frame(x = x, y = y, vx = vx, vy = vy)
}

#' @description Launch probe until target is hit or overshot
#' @rdname day17
#'
launch <- function(df, trial, xrange, yrange) {
  continue <- TRUE
  step <- 1
  while (continue) {
    update <- move(tail(df, 1))
    df <- rbind(df, mutate(trial = trial, step = step, update))
    foundtarget <- dplyr::if_else(
      update$x >= xrange[1] & update$x <= xrange[2] &
        update$y >= yrange[1] & update$y <= yrange[2],
      TRUE, FALSE)
    overshot <- dplyr::if_else(update$x > xrange[2] | update$y < yrange[2],
                               TRUE, FALSE)
    continue <- dplyr::if_else(foundtarget | overshot, FALSE, TRUE)
    step <- step + 1
  }
  mutate(df, hit = foundtarget)
}
# Read in data
path <- here("inst", "2021", "day17.txt")

dat <- read_day17(path)
xrange <- dat$xrange
yrange <- dat$yrange

# Find the initial velocity that causes the probe to reach the highest y
# position and still eventually be within the target area after any step
simulate_launch(xrange, yrange)
## [1] 3160

18.235 sec.

Part 2

#' @rdname day17
#' @export
#'
find_possibilities <- function(xrange, yrange) {
  # Minimum x_velocity (when x_velocity = i, distance in x axis = cumsum(1:i))
  xv_min <- optimize(function(x) abs(max(cumsum(1:x)) - xrange[1]), 1:10) |>
    purrr::pluck("minimum") |> round()

  # Which velocities might hit the target?
  poss_xv <- possible_x(range = xrange,
                        min_v = xv_min,
                        max_v = 500)

  poss_yv <- possible_y(range = yrange,
                        min_v = -2500,
                        max_v = 500)

  expand.grid(poss_xv, poss_yv) |>
    dplyr::rename(xv = Var1, yv = Var2)
}

#' @rdname day17
#' @param velocities velocities
#' @export
#'
simulate_launch2 <- function(velocities, xrange, yrange) {
  lapply(seq_len(nrow(velocities)), function(i) {
    # Extract velocities
    xv <- velocities$xv[i]
    yv <- velocities$yv[i]

    # Calculate trajectory
    z <- max(abs(yrange))

    if (yv > 0) {
      y <- c(0, cumsum(seq(yv, 1, -1)))
      y <- c(y, rev(y), -cumsum((yv + 1):(yv * z)))
    } else if (yv < 0) {
      y <- c(0, cumsum(yv:(yv * z)))
    } else {
      y <- c(0, 0)
      subtract <- 1
      while (min(y) > yrange[1]) {
        y <- c(y, (tail(y, 1) - subtract))
        subtract <- subtract + 1
      }
    }

    x <- c(0, cumsum(seq(xv, 1, -1)))

    if (length(x) > length(y)) {
      y <- c(y, rep(tail(y, 1), length(x) - length(y)))
    } else {
      x <- c(x, rep(tail(x, 1), length(y) - length(x)))
    }

    # Paste coordinates together
    trajectory <- cbind.data.frame(x = x, y = y)

    # Check whether points have hit the target
    hit <- trajectory |>
      dplyr::filter(between(x, xrange[1], xrange[2]),
                    between(y, yrange[1], yrange[2]))

    if (nrow(hit) != 0) {
      out <- data.frame(xv = xv,
                        yv = yv,
                        y_max = max(trajectory$y))
    } else {
      out <- NULL
    }
    out
  }) |>
    do.call(what = rbind)
}

#' @description Test which velocities might hit the target
#' @rdname day17
#' @param range range of target
#' @param min_v minimum velocity to try
#' @param max_v maximum velocity to try
possible_x <- function(range, min_v, max_v) {
  output <- c()
  for (v in min_v:max_v) {
    # Calculate position of points
    points <- cumsum(seq(v, 1, -1))

    # Check if any points hit the target zone
    if (any(between(points, min(range), max(range))))
      output <- c(output, v)
  }
  output
}

#' @description Test which velocities might hit the target
#' @rdname day17
possible_y <- function(range, min_v, max_v) {
  output <- c()
  for (v in min_v:max_v) {
    z <- max(abs(range))

    # Calculate position of points below the x axis
    if (v > 0) {
      points <- -cumsum((v + 1):(v * z))
    } else if (v < 0) {
      points <- cumsum(seq(v, (v * z), -1))
    } else {
      points <- 0
      subtract <- 1
      while (min(points) > range[1]) {
        points <- c(points, (tail(points, 1) - subtract))
        subtract <- subtract + 1
      }
    }

    # Check if any points hit the target zone
    if (any(between(points, min(range), max(range))))
      output <- c(output, v)
  }
  output
}
velocities <- find_possibilities(xrange, yrange)
results <- simulate_launch2(velocities, xrange, yrange)

# Find the initial velocity that causes the probe to reach the highest y
# position and still eventually be within the target area after any step
results |>
  dplyr::filter(y_max == max(y_max))
##   xv yv y_max
## 1 24 79  3160
# What is the highest y position it reaches on this trajectory?
results |>
  dplyr::filter(y_max == max(y_max)) |>
  dplyr::pull(y_max) |>
  unique()
## [1] 3160
# How many distinct initial velocity values cause the probe to be within the
# target area after any step?
results |>
  dplyr::select(xv, yv) |>
  unique() |>
  nrow()
## [1] 1928

27.904 sec.

Day 18: Snailfish

Part 1

#' Day 18: Snailfish
#' @source <https://adventofcode.com/2021/day/18>
#' @name day18
#'
NULL

#' @rdname day18
#' @param path file path
#' @export
#'
read_day18 <- function(path) {
  path |>
    readLines()
}

#' @rdname day18
#' @param dat dat
#' @export
#'
snailfish_maths <- function(dat) {
  first <- dat[1]
  for (i in tail(seq_along(dat), -1)) {
    result <- add_snailfish(first, dat[i])
    first <- result
  }
  check_magnitude(result)
}

# Function to check magnitude of answer
check_magnitude <- function(answer) {
  while(length(answer) != 1) {
    count <- 1
    depth <- 1
    for (i in tail(seq_along(answer), -1)) {
      if (answer[i] == "[") {
        count <- count + 1

      } else if (answer[i] == "]") {
        count <- count - 1
      }
      depth[i] <- count
    }

    deepest <- which(depth == max(depth))

    index <- R.utils::seqToIntervals(deepest) |>
      data.frame() |>
      dplyr::pull(from) |>
      rev() + 1

    for (j in index) {
      first <- 3 * as.numeric(answer[j])
      second <- 2 * as.numeric(answer[j + 2])

      answer <- answer[-c(j:(j + 3))]
      answer[j - 1] <- first + second
    }
  }

  return(as.numeric(answer))
}

# Add snailfish numbers
add_snailfish <- function(first, second) {

  if (length(first) > 1)
    first <- paste(first, collapse = "")

  paste0("[", first, ",", second, "]") |>
    strsplit("") |>
    unlist() |>
    reduce_snailfish()
}

# Function to reduce a snailfish number
reduce_snailfish <- function(dat) {
  try_this <- dat
  continue <- TRUE
  it_split <- FALSE

  while (continue) {
    result <- explode(try_this)
    it_exploded <- !all(is.na(result))

    if (it_exploded) {
      try_this <- result
      it_split <- FALSE
    } else {
      result <- split(try_this)
      it_split <- !all(is.na(result))

      if (!it_exploded & !it_split) {
        return(try_this)
      } else {
        try_this <- result
      }
    }
  }
}

# Function to check for and carry out explosion
explode <- function(dat) {
  count <- 0
  i <- 1
  left <- NA
  right <- NA

  # Check for explosion
  while(i != length(dat)) {
    if (dat[i] == "[") {
      count <- count + 1

    } else if (grepl("[0-9]", dat[i])) {

      if (dat[i + 1] == "," & grepl("[0-9]", dat[i + 2]) & count > 4) {
        right <- head(grep("[0-9]", dat[(i + 3):length(dat)]), 1) + i + 2
        if (length(right) == 0) right <- NA
        left <- tail(grep("[0-9]", dat[1:(i - 1)]), 1)
        if (length(left) == 0) left <- NA
        break
      }

    } else if (dat[i] == "]") {
      count <- count - 1
    }
    i <- i + 1
  }

  # Explode
  if (is.na(left) & is.na(right)) {
    return(NA)

  } else if (is.na(left)) {
    tmp <- dat
    tmp[right] <- as.numeric(dat[right]) + as.numeric(dat[i + 2])
    tmp <- tmp[-c(i:(i + 3))]
    tmp[i - 1] <- 0
    return(tmp)

  } else if (is.na(right)) {
    tmp <- dat[-c(i:(i + 3))]
    tmp[i - 3] <- as.numeric(dat[left]) + as.numeric(dat[i])
    tmp[i - 1] <- 0
    return(tmp)

  } else {
    tmp <- dat
    tmp[left] <- as.numeric(dat[left]) + as.numeric(dat[i])
    tmp[right] <- as.numeric(dat[right]) + as.numeric(dat[i + 2])
    tmp <- tmp[-c(i:(i + 3))]
    tmp[i - 1] <- 0
    return(tmp)
  }
}

# Function to check for and carry out split
split <- function(dat) {
  # Check for split
  index <- NA
  for (i in seq_along(dat)) {
    if (grepl("[0-9]", dat[i]) && as.numeric(dat[i]) >= 10) {
      index <- i
      break
    }
  }

  # Split
  if (is.na(index)) {
    return(NA)

  } else {
    left <- dat[1:(index - 1)]
    right <- dat[(index + 1):length(dat)]
    tmp <- as.numeric(dat[index])
    tmp <- c("[", floor(tmp / 2), ",", ceiling(tmp / 2), "]")
    return(c(left, tmp, right))
  }
}
path <- here("inst", "2021", "day18.txt")
dat <- read_day18(path)

# Add up all of the snailfish numbers from the homework assignment in the
# order they appear. What is the magnitude of the final sum?
snailfish_maths(dat)
## [1] 4116

4.849 sec.

Part 2

#' @rdname day11
#' @param dat dat
#' @export
#'
snailfish_maths2 <- function(dat) {
  n <- length(dat)
  combinations <- expand.grid(1:n, 1:n) |>
    dplyr::filter(Var1 != Var2)
  results <- c()

  for (i in seq_len(nrow(combinations))) {
    # cat("\r", i, "of", nrow(combinations))
    first <- combinations$Var1[i]
    second <- combinations$Var2[i]
    results[i] <- add_snailfish(dat[first], dat[second]) |>
      check_magnitude()
  }

  max(results)
}
# What is the largest magnitude of any sum of two different snailfish numbers
# from the homework assignment?
snailfish_maths2(dat)
## [1] 4638

93.588 sec.

Day 19: Beacon Scanner

Part 1

#' Day 19: Beacon Scanner
#' @source <https://adventofcode.com/2021/day/19>
#' @name day19
#'
NULL

#' @rdname day19
#' @param path file path
#' @export
#'
read_day19 <- function(path) {
  dat <- path |>
    readLines() |>
    data.frame() |>
    setNames("data")

  start <- grep("scanner", dat$data)
  end <- c(tail(start - 1, -1), nrow(dat))

  lapply(seq_along(start), function(x) {
    tmp <- dat[start[x]:end[x], ]
    tmp <- tmp[-1]
    index <- which(tmp == "")
    if (length(index) != 0)
      tmp <- tmp[-index]
    out <- data.frame(data = tmp) |>
      tidyr::separate(col = data, into = c("d1", "d2", "d3"), sep = ",") |>
      dplyr::mutate_if(is.character, as.numeric) |>
      as.matrix()
    colnames(out) <- NULL
    out
  })
}

#' @rdname day19
#' @param scans scans
#' @export
#'
assemble_map <- function(scans) {
  transformations <- get_combinations(1:3)
  scanners <- data.frame(scanner = 0, x = 0, y = 0, z = 0)

  while(nrow(scanners) != length(scans)) {
    results <- list()

    # Compare each scanner output to that of scanner 0
    for (i in tail(seq_along(scans), -1)) {

      # cat("\r", i, "of", length(scans), "-",
      #     length(scans) - nrow(scanners), "left to find...")

      if ((i - 1) %in% scanners$scanner) next

      scanner_zero <- scans[[1]]
      scanner_i <- scans[[i]]

      for (j in seq_len(nrow(transformations))) {
        this_transformation <- transformations[j, ]

        # Transform `scanner_i` by `this_transformation`
        transformed_scanner <- transform_all(scanner_i, this_transformation)

        # Try subtracting each beacon coordinate in `scanner_zero` from each
        # beacon coordinate in `transformed_scanner` (transformation of
        # `scanner_i`)
        subtract <- lapply(seq_len(nrow(scanner_zero)), function(x)
          apply(transformed_scanner, 1, function(y) y - scanner_zero[x, ]) |>
            t()) |>
          do.call(what = rbind.data.frame) |>
          tidyr::unite(unscramble) |>
          dplyr::group_by(unscramble) |>
          dplyr::summarize(n = dplyr::n())

        # If 12 or more matches are found, record the scanner position
        if (max(subtract$n) >= 12) {
          this_coordinate <- subtract |>
            dplyr::filter(n == max(n)) |>
            tidyr::separate(unscramble, c("x", "y", "z"), sep = "_") |>
            dplyr::select(-n) |>
            data.frame() |>
            dplyr::mutate_if(is.character, as.numeric)

          colnames(this_coordinate) <- c("x", "y", "z")

          scanners <- rbind(scanners,
                            cbind(scanner = i - 1, this_coordinate))
          results[[i]] <- list(scanner1 = 0,
                               scanner2 = i - 1,
                               beacon = this_coordinate,
                               transformation = this_transformation)
          break
        }
      }
    }

    # Which beacons can see each other?
    relatives <- lapply(seq_along(results), function(x)
      if (is.null(results[[x]])) {
        NA
      } else {
        data.frame(from = results[[x]]$scanner1,
                   to = results[[x]]$scanner2)
      }
    ) |>
      do.call(what = rbind)

    # Append new beacon coordinates to scanner outputs
    for (k in seq_len(nrow(relatives))) {
      if (all(is.na(relatives[k,]))) next

      tmp <- results[[k]]
      from <- relatives$from[k] + 1
      to <- relatives$to[k] + 1

      # Transform `to` scanner coordinates relative to `from` scanner coordinates
      transformed_to <- transform_all(scans[[to]], tmp$transformation) |>
        apply(1, function(x) x - tmp$beacon) |>
        do.call(what = rbind)
      colnames(transformed_to) <- c("X1", "X2", "X3")

      # Add beacon coordinates to `from` scanner
      scans[[from]] <- transformed_to |>
        dplyr::anti_join(data.frame(scans[[from]]),
                         by = c("X1", "X2", "X3")) |>
        as.matrix() |>
        rbind(scans[[from]]) |>
        unique()
    }
  }

  list(scanners = scanners, scans = scans)

}

get_combinations <- function(x) {
  x |> spin_and_flip() |>
    reorient() |>
    spin_and_flip() |>
    reorient() |>
    spin_and_flip()
}

spin_and_flip <- function(x) {
  x |> spin() |> flip() |> spin()
}

# Spin around an axis
spin <- function(x) {
  vec <- if(is.vector(x)) x else tail(x, 1)
  for (i in 1:3) {
    vec <- c(vec[3], vec[2], -vec[1])
    x <- rbind(x, vec)
  }
  unname(x)
}

# Turn upside down
flip <- function(x) {
  vec <- if(is.vector(x)) x else tail(x, 1)
  vec <- c(-vec[1], -vec[2], vec[3])
  rbind(x, vec) |>
    unname()
}

# Take the next axis as up
reorient <- function(x) {
  vec <- if(is.vector(x)) x else tail(x, 1)
  vec <- c(vec[3], vec[1], vec[2])
  rbind(x, vec) |>
    unname()
}

transform_all <- function(scanner_output, transformation) {
  lapply(seq_len(nrow(scanner_output)), function(x)
    transform(scanner_output[x, ], transformation)) |>
    do.call(what = rbind)
}

transform <- function(coordinate, transformation) {
  index <- abs(transformation)
  sign <- vapply(transformation, function(x) if (x > 0) 1 else -1, numeric(1))
  reindex <- c(coordinate[index[1]], coordinate[index[2]], coordinate[index[3]])
  reindex * sign
}
path <- here("inst", "2021", "day19.txt")
scans <- read_day19(path)

# How many beacons are there?
tmp <- assemble_map(scans)
scans <- tmp$scans
scans[[1]] |>
    unique() |>
    nrow()
## [1] 330

161.047 sec.

Part 2

#' @rdname day11
#' @param scanners scanners
#' @export
#'
dist_scanners <- function(scanners) {
  index <- seq_len(nrow(scanners)) - 1
  combinations <- t(combn(index, 2))

  res <- lapply(seq_len(nrow(combinations)), function(x) {
    first <- combinations[x, 1]
    second <- combinations[x, 2]
    a <- scanners |>
      dplyr::filter(scanner == first) |>
      dplyr::select(-scanner) |> unlist()
    b <- scanners |>
      dplyr::filter(scanner == second) |>
      dplyr::select(-scanner) |> unlist()
    manhattan(a, b)
  }) |> unlist()
  max(res)
}

manhattan <- function(a, b) {
  abs(a - b) |>
    sum()
}
# What is the largest Manhattan distance between any two scanners?
scanners <- tmp$scanners
dist_scanners(scanners)
## [1] 9634

1.543 sec.

Day 20: Trench Map

Part 1

#' Day 20: Trench Map
#' @source <https://adventofcode.com/2021/day/20>
#' @name day20
#'
NULL

#' @rdname day20
#' @param path file path
#' @export
#'
get_algorithm <- function(path) {
  path |>
    readLines(n = 1) |>
    strsplit("") |>
    unlist()
}

#' @rdname day20
#' @export
#'
get_input <- function(path) {
  path |>
    scan(what = "character", skip = 2) |>
    vapply(function(x) as.data.frame(strsplit(x, "")[[1]]),
           data.frame(1)) |>
    do.call(what = rbind) |>
    unname()
}

#' @rdname day20
#' @param img img
#' @export
#'
view_image <- function(img) {
  expand.grid(seq_len(nrow(img)), seq_len(nrow(img))) |>
    setNames(c("row", "col")) |>
    dplyr::left_join(which(img == "#", arr.ind = TRUE) |>
                       cbind.data.frame(what = "hash"), by = c("row", "col")) |>
    dplyr::mutate(what = dplyr::case_when(is.na(what) ~ "dot",
                                          TRUE ~ what)) |>
    ggplot2::ggplot() + ggplot2::theme_void() +
    ggplot2::geom_tile(ggplot2::aes(x = row, y = col, fill = what)) +
    ggplot2::scale_fill_manual(values = setNames(c("black", "white"),
                                                 c("hash", "dot"))) +
    ggplot2::coord_fixed() +
    ggplot2::theme(legend.position = "none")
}

#' @rdname day20
#' @param input input
#' @param n n
#' @param algorithm algorithm
#' @export
#'
enhance_image <- function(input, n, algorithm) {
  img <- input
  odd <- TRUE
  first <- head(algorithm, 1)
  second <- tail(algorithm, 1)

  # When the first element of the algorithm doesn't match the last one, change
  # the composition of the image border with each iteration
  correction <- first != second

  for (i in seq_len(n)) {
    if (correction)
      pad <- ifelse(odd, second, first)
    img <- img |>
      scan_input(pad) |>
      output_pixel(algorithm) |>
      generate_image()

    odd <- !odd
  }
  img
}

scan_input <- function(input, pad) {
  with_buffer <- rbind(pad, pad, input, pad, pad)
  with_buffer <- cbind(pad, pad, with_buffer, pad, pad) |> unname()

  index <- 2:(ncol(with_buffer) - 1)
  input_coords <- lapply(index, function(x) cbind(x = x, y = index)) |>
    do.call(what = rbind.data.frame)

  lapply(seq_len(nrow(input_coords)), function(x) {
    index <- with_buffer |>
      get_pixels(unlist(input_coords[x, ])) |>
      pix2bin()
    data.frame(index = index,
               x = input_coords$x[x] - 1,
               y = input_coords$y[x] - 1)
  }) |>
    do.call(what = rbind)
}

output_pixel <- function(values, algorithm) {
  values |>
    dplyr::rowwise() |>
    dplyr::mutate(pixel = dplyr::nth(algorithm, index + 1)) |>
    dplyr::select(-index) |>
    data.frame()
}

generate_image <- function(values) {
  matrix(values$pixel, ncol = max(values$y), nrow = max(values$x),
         byrow = TRUE)
}

get_pixels <- function(input, coord) {
  rows <- (coord[1] - 1):(coord[1] + 1)
  cols <- (coord[2] - 1):(coord[2] + 1)
  input[rows, cols]
}

# Convert matrix of input pixels into a decimal number
pix2bin <- function(pixels) {
  tmp <- pixels |>
    t() |>
    matrix(nrow = 1)
  as.numeric(tmp == "#") |>
    paste(collapse = "") |>
    strtoi(base = 2)
}
# Start with the original input image and apply the image enhancement
# algorithm twice, being careful to account for the infinite size of the
# images
path <- here("inst", "2021", "day20.txt")

input <- get_input(path)
algorithm <- get_algorithm(path)

img <- enhance_image(input, 2, algorithm)
view_image(img)

# How many pixels are lit in the resulting image?
sum(img == "#")
## [1] 5663

7.835 sec.

Part 2

# Start again with the original input image and apply the image enhancement algorithm 50 times
img <- enhance_image(input, 50, algorithm)

# How many pixels are lit in the resulting image?
sum(img == "#")
## [1] 19638

405.446 sec.

Day 21: Dirac Dice

Part 1

#' Day 21: Dirac Dice
#' @source <https://adventofcode.com/2021/day/21>
#' @name day21
#'
NULL

#' @rdname day21
#' @param path file path
#' @export
#'
read_day21 <- function(path) {
  path |>
    readLines()
}

#' @rdname day21
#' @param dat dat
#' @export
#'
deterministic <- function(dat) {
  start <- dat |>
    vapply(function(x) gsub(".*([0-9])$", "\\1", x),
           character(1)) |>
    as.numeric()

  pos1 <- start[1]
  pos2 <- start[2]
  score1 <- 0
  score2 <- 0
  i <- 0

  while (all(c(score1, score2) < 1000)) {
    i <- i + 1

    if (i%%2 != 0) {
      pos1 <- pos1 + sum(((i * 3) - 2):(i * 3))
      pos1 <- dplyr::if_else(pos1 > 10, pos1%%10, pos1)
      pos1 <- dplyr::if_else(pos1 == 0, 10, pos1) # since pos1 = 100 returns 0
      score1 <- score1 + pos1

    } else {
      pos2 <- pos2 + sum(((i * 3) - 2):(i * 3))
      pos2 <- dplyr::if_else(pos2 > 10, pos2%%10, pos2)
      pos2 <- dplyr::if_else(pos2 == 0, 10, pos2)
      score2 <- score2 + pos2
    }
  }

  min(score1, score2) * i * 3
}
# Play a practice game using the deterministic 100-sided die
path <- here("inst", "2021", "day21.txt")
dat <- read_day21(path)

# The moment either player wins, what do you get if you multiply the score of
# the losing player by the number of times the die was rolled during the game?
deterministic(dat)
## [1] 598416

0.041 sec.

Day 22: Reactor Reboot

Part 1

#' Day 22: Reactor Reboot
#' @source <https://adventofcode.com/2021/day/22>
#' @name day22
#'
NULL

#' @rdname day22
#' @param path file path
#' @export
#'
read_day22 <- function(dat, limit) {
  regex <- paste0("(\\D*)\\sx=(-?\\d*)..(-?\\d*),y=(-?\\d*)..(-?\\d*),",
                  "z=(-?\\d*)..(-?\\d*)")
  out <- dat |> data.frame() |>
    setNames("x") |>
    tidyr::extract(x, c("on", "x1", "x2", "y1", "y2", "z1", "z2"),
                   regex) |>
    dplyr::mutate(on = dplyr::if_else(on == "on", TRUE, FALSE)) |>
    dplyr::mutate_if(is.character, as.numeric)

  if(missing(limit)) {
    limit <- max(max(out), abs(min(out)))
  }
  # Reset minimum to 1
  dplyr::mutate_if(out, is.numeric, \(z) z + limit + 1)
}

#' @rdname day22
#' @param dat dat
#' @export
#'
reboot <- function(dat) {
  reactor <- array(FALSE, dim = c(101, 101, 101))

  for (i in seq_len(nrow(dat))) {
    # Are any coordintes outside the range?
    test <- dat[i, ] |>
      unlist() |>
      tail(-1) |>
      {\(x) dplyr::between(x, 1, max(dim(reactor)))}() |>
      all()

    if (test) {
      reactor <- get_indices(dat[i, ], reactor)
    } else {
      next
    }
  }
  sum(reactor)
}

get_indices <- function(df, reactor) {
  reactor[df$x1:df$x2, df$y1:df$y2, df$z1:df$z2] <- df$on
  reactor
}
# Read in data
test <- here("inst", "2021", "day22-test.txt")
path <- here("inst", "2021", "day22.txt")

test_dat <- readLines(test) |> read_day22(50)
dat <- readLines(path) |> read_day22(50)

# Execute the reboot steps
assertthat::assert_that(reboot(test_dat) == 590784)
## [1] TRUE
# Considering only cubes in the region x=-50..50,y=-50..50,z=-50..50, how many
# cubes are on?
reboot(dat)
## [1] 644257

0.145 sec.

Day 23: Amphipod

Day 25: Sea Cucumber

Part 1

#' Day 25: Sea Cucumber
#' @source <https://adventofcode.com/2021/day/25>
#' @name day25
#'
NULL

#' @description Read in cucumber positions
#' @rdname day25
#' @param path file path
#' @export
#'
read_day25 <- function(path) {
  path |>
    readLines() |>
    strsplit("") |>
    do.call(what = rbind)
}

#' @rdname day25
#' @param dat dat
#' @export
#'
get_cucumbers <- function(dat) {
  east_cucumbers <- which(dat == ">", arr.ind = TRUE) |>
    data.frame() |>
    dplyr::mutate(type = "east")
  south_cucumbers <- which(dat == "v", arr.ind = TRUE) |>
    data.frame() |>
    dplyr::mutate(type = "south")
  rbind(east_cucumbers, south_cucumbers) |>
    dplyr::mutate_if(is.numeric, as.double)
}

#' @rdname day25
#' @param dat dat
#' @param cucumbers cucumbers
#' @export
#'
simulate_cucumbers <- function(dat, cucumbers) {
  # Initialise variables
  track_cucumbers <- sea_cucumbers$new(dat, cucumbers)
  continue <- TRUE
  i <- 0

  # Find somewhere safe to land your submarine

  while (continue) {
    i <- i + 1
    # cat("\r", i)
    track_cucumbers$move_east()
    track_cucumbers$move_south()
    continue <- track_cucumbers$continue_east | track_cucumbers$continue_south
  }
  i
}

sea_cucumbers <- R6::R6Class("cucumbers", list(
  dat = NULL,
  cucumbers = NULL,
  continue_east = NULL,
  continue_south = NULL,
  plot = NULL,

  initialize = function(dat, cucumbers) {
    self$dat <- dat
    self$cucumbers <- dplyr::mutate(cucumbers, id = dplyr::row_number())
    self$continue_east <- TRUE
    self$continue_south <- TRUE
    invisible(self)
  },

  print = function(...) {
    cat("\n")
    for (i in seq_len(nrow(self$dat))) {
      cat(self$dat[i, ], "\n")
    }
    invisible(self)
  },

  move_east = function() {
    dat <- self$dat
    cucumbers <- self$cucumbers
    boundary <- ncol(dat) + 1

    # Determine which cucumbers move
    tmp <- cucumbers |>
      dplyr::mutate(prev = col,
                    col = dplyr::case_when(type == "east" ~ col + 1,
                                           TRUE ~ col)) |>
      dplyr::mutate(col = dplyr::case_when(col == boundary ~ 1,
                                           TRUE ~ col))

    # These do
    moved <- tmp |>
      dplyr::filter(type == "east") |>
      dplyr::anti_join(cucumbers, by = c("row", "col"))

    # These don't
    stationary <- dplyr::anti_join(cucumbers,
                                   dplyr::select(moved, -prev), by = c("id"))

    # Move cucumbers east
    dat[as.matrix(dplyr::select(moved, row, col))] <- ">"

    # Place a dot in their previous position
    prev <- dplyr::select(moved, row, prev)
    dat[as.matrix(prev)] <- "."

    # Update the cucumber list
    self$cucumbers <- moved |>
      dplyr::select(-prev) |>
      rbind(stationary)

    # Update objects
    self$dat <- dat
    self$continue_east <- nrow(moved) > 0

    invisible(self)
  },

  move_south = function(display = FALSE) {
    dat <- self$dat
    cucumbers <- self$cucumbers
    boundary <- nrow(dat) + 1

    # Determine which cucumbers move
    tmp <- cucumbers |>
      dplyr::mutate(prev = row,
                    row = dplyr::case_when(type == "south" ~ row + 1,
                                           TRUE ~ row)) |>
      dplyr::mutate(row = dplyr::case_when(row == boundary ~ 1,
                                           TRUE ~ row))

    # These do
    moved <- tmp |>
      dplyr::filter(type == "south") |>
      dplyr::anti_join(cucumbers, by = c("row", "col"))

    # These don't
    stationary <- dplyr::anti_join(cucumbers,
                                   dplyr::select(moved, -prev), by = c("id"))

    # Move cucumbers south
    dat[as.matrix(dplyr::select(moved, row, col))] <- "v"

    # Place a dot in their previous position
    prev <- dplyr::select(moved, prev, col)
    dat[as.matrix(prev)] <- "."

    # Update the cucumber list
    self$cucumbers <- moved |>
      dplyr::select(-prev) |>
      rbind(stationary)

    # Update objects
    self$dat <- dat
    self$continue_south <-  nrow(moved) > 0

    # Print to console
    if (display) print(self)

    invisible(self)
  },

  gg_cucumbers = function() {
    rows <- seq_len(nrow(self$dat))
    cols <- seq_len(ncol(self$dat))
    fill <- c("#489FB5", "#EDE7E3", "#FFA62B")

    self$cucumbers |>
      dplyr::select(-id) |>
      tidyr::complete(row = rows, col = cols,
                      fill = list(type = "empty")) |>
      dplyr::mutate(text = dplyr::case_when(type == "south" ~ "v",
                                            type == "east" ~ ">",
                                            TRUE ~ "")) |>
      ggplot2::ggplot(ggplot2::aes(x = col, y = row, fill = type)) +
      ggplot2::theme_void() +  ggplot2::coord_fixed() +
      ggplot2::scale_y_reverse() +
      ggplot2::scale_fill_manual(values = fill) +
      ggplot2::geom_tile() +
      ggplot2::geom_text(ggplot2::aes(label = text)) +
      ggplot2::theme(legend.position = "none")
  }
))
# Read in data
path <- here("inst", "2021", "day25.txt")
dat <- read_day25(path)
cucumbers <- get_cucumbers(dat)

# What is the first step on which no sea cucumbers move?
simulate_cucumbers(dat, cucumbers)
## [1] 530

14.852 sec.

Test data

knitr::include_graphics("recording.gif")

Test dataset