By Eric Burden | December 23, 2020
In the spirit of the holidays (and programming), I’ll be posting my solutions to the Advent of Code 2020 puzzles here, at least one day after they’re posted (no spoilers!). I’ll be implementing the solutions in R because, well, that’s what I like! What I won’t be doing is posting any of the actual answers, just the reasoning behind them.
Also, as a general convention, whenever the puzzle has downloadable input, I’m
saving it in a file named input.txt
.
Day 22 - Crab Combat
Find the problem description HERE.
Part One - Know When to Hold’em
Let’s play cards with a crustacean! Win or lose, this crab is a triumph. It
doesn’t even have thumbs! I guess we’ll need to shuffle the deck… The game
itself involves simulating a modified game of War where the high card (number)
always wins and the winner gets both cards. I added a pretty-print (pprint()
)
function to help with debugging.
# Setup ------------------------------------------------------------------------
test_input <- c(
"Player 1:",
"9", "2", "6", "3", "1",
"",
"Player 2:",
"5", "8", "4", "7", "10"
)
real_input <- readLines('input.txt')
library(dequer)
# Functions --------------------------------------------------------------------
# Helper function, given a vector of input lines returns an environment
# containing both player's (player1 and player2) decks and a starting number
# for the number of rounds the game has progressed
parse_input <- function(input) {
hands <- list(
player1 = queue(), # A queue for handling player 1's deck
player2 = queue(), # A queue for handling player 2's deck
round = 1 # Start with round == 1
)
# Starting from scratch, for every line in the input
current_player <- ''
for (line in input) {
if (line == '') { next } # Skip blank lines
# If the line indicates a player, set `current_player` to a string
# indicating that player's name. Otherwise, add the number on the line
# to `current_player`'s deck.
if (grepl('Player', line)) {
current_player <- tolower(gsub('[ :]', '', line))
} else {
pushback(hands[[current_player]], as.numeric(line))
}
}
list2env(hands, new.env()) # Return an environment
}
# Purely for debugging purposes, prints important bits from the game state
# to the console
pprint <- function(game_state) {
cat('\n\n')
cat('-- Round ', game_state$round, ' --', '\n')
cat(
'Player 1 deck: ',
game_state$p1_card, ', ',
paste(unlist(as.list(game_state$player1)), collapse = ', '),
'\n',
sep = ''
)
cat(
'Player 2 deck: ',
game_state$p2_card, ', ',
paste(unlist(as.list(game_state$player2)), collapse = ', '),
'\n',
sep = ''
)
cat('Player 1 plays:', game_state$p1_card, '\n')
cat('Player 2 plays:', game_state$p2_card, '\n')
cat('Winner: ', game_state$winner, '\n')
}
# Given an environment `env` representing the game state, updates the game
# state by simulating a round of play and returns the game state.
next_state <- function(env) {
# Both players draw. It's not strictly necessary to include the player's
# cards or the name of the player who won the round in the environment,
# but it helps with debugging
env$p1_card <- pop(env$player1)
env$p2_card <- pop(env$player2)
# pprint(env) # Prints important bits of the game state to console
# Identify the winner and add the cards to the winner's deck
env$winner <- if (env$p1_card > env$p2_card) { 'player1' } else { 'player2' }
if (env$winner == 'player1') {
pushback(env$player1, env$p1_card)
pushback(env$player1, env$p2_card)
} else {
pushback(env$player2, env$p2_card)
pushback(env$player2, env$p1_card)
}
env$round <- env$round + 1 # Advance the round count
# If a player's deck is empty, the other player won the game. Add the winning
# player's current deck to the environment `env`
if (length(env$player1) == 0) { env$winner_deck <- unlist(as.list(env$player2)) }
if (length(env$player2) == 0) { env$winner_deck <- unlist(as.list(env$player1)) }
env # Return the modified environment
}
# Processing -------------------------------------------------------------------
game_state <- parse_input(real_input) # Parse the input
# Until a player wins, advance the game state to the next state
while (is.null(game_state$winner_deck)) { game_state <- next_state(game_state) }
cards_in_winner_deck <- length(game_state$winner_deck)
answer1 <- sum(game_state$winner_deck * cards_in_winner_deck:1)
And…the crab won. (Depending on your individual input.)
Part Two - Know When to Fold’em
According to the puzzle text, we lost round one to the crab. So, just like a petulant grade-schooler, it’s time to change the rules! Specifically, we’re introducing recursive sub-games to the mix and breaking all infinite loops in out favor. Guess the crab wasn’t paying attention when it agreed to that rule.
# Setup ------------------------------------------------------------------------
source('exercise_1.R')
test_input <- c(
"Player 1:",
"9", "2", "6", "3", "1",
"",
"Player 2:",
"5", "8", "4", "7", "10"
)
infinite_input <- c(
"Player 1:",
"43", "19",
"",
"Player 2:",
"2", "29", "14"
)
real_input <- readLines('input.txt')
library(dequer)
library(digest) # For the `digest()` function
# Functions --------------------------------------------------------------------
# Update the `next_state()` function to support the rules of `Recursive Combat`
next_state <- function(env) {
# The `current_state` is a hash of the two player's hands
current_state <- digest(list(as.list(env$player1), as.list(env$player2)))
# The first time through, `env$past_states` will be NULL
# If `current_state` is in the list of recorded states for this game,
# Player 1 wins the game! This prevents infinite loops. Note, recursive
# games start with the `past_states` of their parent game.
if (current_state %in% env$past_states) {
env$winner <- 'player1'
env$winning_hand <- unlist(as.list(env$player1))
return(env)
}
# Otherwise, add the current state to the list of past states
env$past_states <- c(env$past_states, current_state)
# Both players draw
env$p1_card <- pop(env$player1)
env$p2_card <- pop(env$player2)
# If either player draws a number larger than the number of cards in
# their hand, play as normal
if (env$p1_card > length(env$player1) | env$p2_card > length(env$player2)) {
env$winner <- if (env$p1_card > env$p2_card) { 'player1' } else { 'player2' }
} else { # Otherwise, recursion!
# Create a new environment and initialize it with the necessary data,
# including the top `n` cards from each players deck where `n` is the
# value of that players card and the `past_states` of the parent state
sub_game <- new.env()
sub_game$player1 <- as.queue(as.list(env$player1)[1:env$p1_card])
sub_game$player2 <- as.queue(as.list(env$player2)[1:env$p2_card])
sub_game$past_states <- env$past_states
sub_game$round <- 1
# Play out the sub game
while (is.null(sub_game$winning_hand)) { sub_game <- next_state(sub_game) }
env$winner <- sub_game$winner
}
# pprint(env)
# Add the cards to the winner's deck
if (env$winner == 'player1') {
pushback(env$player1, env$p1_card)
pushback(env$player1, env$p2_card)
} else {
pushback(env$player2, env$p2_card)
pushback(env$player2, env$p1_card)
}
# If a player's deck is empty, the other player won the game. Add the winning
# player's current deck to the environment `env`
env$round <- env$round + 1
if (length(env$player1) == 0) { env$winning_hand <- unlist(as.list(env$player2)) }
if (length(env$player2) == 0) { env$winning_hand <- unlist(as.list(env$player1)) }
env # Return the modified environment
}
# Processing -------------------------------------------------------------------
game_state <- parse_input(real_input)
while (is.null(game_state$winning_hand)) {
game_state <- next_state(game_state)
if (game_state$round %% 25 == 0) { pprint(game_state) }
}
cards_in_winning_hand <- length(game_state$winning_hand)
answer2 <- sum(game_state$winning_hand * cards_in_winning_hand:1)
I guess that’ll show that crab. Unless you lost again…
Important to note that, to successfully get a hash of the game state, the
player1
and player2
stacks (representing hands) need to be converted to
lists, otherwise you’re just hashing two pointers (that don’t change) over
and over again.
Wrap-Up
This one was a bit fiddly (hence the pprint()
function). There always seemed
to be one small detail I missed from the instructions. Like passing the list of
prior hands to the recursive sub-games, for example. I initially thought each
sub-game was supposed to start with it’s own set of past_states
. I also
initially missed the fact that sub-game decks only consisted of the first n
cards from that deck, where n
was the number each player played in the round
that initiated the sub-game. Once I re-read the instructions enough times (and
made a couple of educated guesses), it all came out. The feeling of success
was palpable!
If you found a different solution (or spotted a mistake in one of mine), please drop me a line!