DEV Community

Cover image for Advent of Code 2020 Solution Megathread - Day 22: Crab Combat
Ryan Palo
Ryan Palo

Posted on

Advent of Code 2020 Solution Megathread - Day 22: Crab Combat

We're getting close. I can comfortably say that it's Christmas Eve Eve Eve. Three Eves. We Three Eves. Actually, not a bad band name. Sort of a holiday-themed Destiny's Child vibe. Anyways, on to the puzzle.

The Puzzle

In today’s puzzle, we have gotten so bored on our journey that we've inexplicably taught a crab how to play cards. We're playing Combat (which I've played as War). Whoever has the highest card each round takes both cards into the bottom of their deck and play proceeds until someone has all the cards. Good luck!

The Leaderboards

As always, this is the spot where I’ll plug any leaderboard codes shared from the community.

Ryan's Leaderboard: 224198-25048a19 
Enter fullscreen mode Exit fullscreen mode

If you want to generate your own leaderboard and signal boost it a little bit, send it to me either in a DEV message or in a comment on one of these posts and I'll add it to the list above.

Yesterday’s Languages

Updated 07:10AM 12/22/2020 PST.

Language Count
JavaScript 2
Rust 1
Python 1
Haskell 1

Merry Coding!

Top comments (7)

Collapse
 
neilgall profile image
Neil Gall

I got there in the end on this one but I have to admit I don't know why. I was storing the complete game state (both players) in a HashSet for recording the history. This passed the tests but gave the wrong answer for the final score. After reading another thread online (the first time I've looked this year!) someone suggested you only need to store the first player's state as this is enough to uniquely identity a game position. That feels like an optimisation, not a logic change but I did it anyway and now it produces the correct answer. I feel bad for writing code I don't understand!

use log::debug; use std::collections::{HashSet, HashMap, VecDeque}; use parser::*; // -- model type Card = i64; type Score = i64; type PlayerID = usize; type Player = VecDeque<Card>; type GameState = Vec<Player>; type GameMemos = HashMap<Player, PlayerID>; #[derive(Debug, Copy, Clone, PartialEq)] enum Rules { Normal, Recursive } #[derive(Debug, PartialEq)] struct Game { players: GameState, history: HashSet<Player>, prefix: String, round: usize } impl Game { fn new(players: GameState) -> Self { Game { players, history: HashSet::new(), prefix: "".to_string(), round: 0 } } fn make_sub_game(&self, drawn: &Vec<(PlayerID, Card)>) -> Game { let mut game = Game::new( drawn.iter().map(|(player, card)| self.players[*player].iter().take(*card as usize).copied().collect() ).collect() ); game.prefix = format!("{} ", self.prefix); game } fn cards(&self, player: PlayerID) -> Vec<Card> { self.players[player].iter().copied().collect() } fn should_recurse(&self, cards: &Vec<(PlayerID, Card)>) -> bool { cards.iter().all(|(player, card)| self.players[*player].len() >= *card as usize) } fn play_round(&mut self, rules: Rules, memos: &mut GameMemos) { self.round += 1; debug!("{}round {}", self.prefix, self.round); debug!("{}player 1 {:?}", self.prefix, self.players[0]); debug!("{}player 2 {:?}", self.prefix, self.players[1]); let repeats_previous_round = self.history.contains(&self.players[0]); self.history.insert(self.players[0].clone()); let mut top_cards: Vec<(PlayerID, Card)> = self.players.iter_mut().filter_map(|p| p.pop_front()).enumerate().collect(); let mut winner = None; if rules == Rules::Recursive { if repeats_previous_round { winner = Some(0); } else if self.should_recurse(&top_cards) { match memos.get(&self.players[0]) { Some(w) => { winner = Some(*w); } None => { let mut sub_game = self.make_sub_game(&top_cards); sub_game.play_until_over(Rules::Recursive, memos); let w = sub_game.winner(); winner = Some(w); memos.insert(self.players[0].clone(), w); } } } } if winner == None { // normal rules winner = top_cards.iter().max_by_key(|(_, card)| card).map(|(player, _)| *player); } let winner = winner.unwrap(); top_cards.sort_by_key(|(player, _)| *player != winner); for (_, card) in top_cards { self.players[winner].push_back(card); } } fn over(&self) -> bool { self.players.iter().any(|p| p.is_empty()) } fn play_until_over(&mut self, rules: Rules, memos: &mut GameMemos) { while !self.over() { self.play_round(rules, memos); } } fn winner(&self) -> PlayerID { self.players.iter().enumerate().find(|(_, cards)| !cards.is_empty()).unwrap().0 } fn winning_score(&self) -> Score { let winner: &Player = self.players.iter().find(|p| !p.is_empty()).unwrap(); winner.iter().rev().enumerate().fold( 0, |score, (index, card)| score + card * ((index+1) as Score) ) } } // -- parser fn parse_input(input: &str) -> ParseResult<Game> { let player_tag = integer.between(match_literal("Player "), match_literal(":")); let cards = one_or_more(whitespace_wrap(integer)).map(|cards| cards.into_iter().collect()); let player = right(player_tag, cards); let game = one_or_more(player).map(Game::new); game.parse(input) } // -- problems fn part1(game: &mut Game) -> Score { game.play_until_over(Rules::Normal, &mut GameMemos::new()); game.winning_score() } fn part2(game: &mut Game) -> Score { game.play_until_over(Rules::Recursive, &mut GameMemos::new()); game.winning_score() } fn main() { env_logger::init(); let input = std::fs::read_to_string("./input.txt").unwrap(); let mut game = parse_input(&input).unwrap().1; println!("part 1 {}", part1(&mut game)); let mut game = parse_input(&input).unwrap().1; println!("part 2 {}", part2(&mut game)) } #[cfg(test)] mod tests { use super::*; fn test_game() -> Game { Game::new(vec![ vec![9, 2, 6, 3, 1].into_iter().collect(), vec![5, 8, 4, 7, 10].into_iter().collect() ]) } #[test] fn test_parser() { let game = parse_input( "Player 1: 9 2 6 3 1 Player 2: 5 8 4 7 10"); assert_eq!(game, Ok(("", test_game()))); } #[test] fn test_play_round() { let mut game = test_game(); game.play_round(Rules::Normal, &mut GameMemos::new()); assert_eq!(game.cards(0), vec![2, 6, 3, 1, 9, 5]); assert_eq!(game.cards(1), vec![8, 4, 7, 10]); game.play_round(Rules::Normal, &mut GameMemos::new()); assert_eq!(game.cards(0), vec![6, 3, 1, 9, 5]); assert_eq!(game.cards(1), vec![4, 7, 10, 8, 2]); game.play_round(Rules::Normal, &mut GameMemos::new()); assert_eq!(game.cards(0), vec![3, 1, 9, 5, 6, 4]); assert_eq!(game.cards(1), vec![7, 10, 8, 2]); } #[test] fn test_game_over() { let mut game = test_game(); game.play_until_over(Rules::Normal, &mut GameMemos::new()); assert_eq!(game.round, 29); } #[test] fn test_score() { let mut game = test_game(); game.play_until_over(Rules::Normal, &mut GameMemos::new()); assert_eq!(game.winning_score(), 306); } #[test] fn test_infinite_recursion_rule_works() { let mut game = Game::new(vec![ vec![43, 19].into_iter().collect(), vec![2, 29, 14].into_iter().collect() ]); game.play_until_over(Rules::Recursive, &mut GameMemos::new()); } #[test] fn test_play_recursive() { let mut game = test_game(); game.play_until_over(Rules::Recursive, &mut GameMemos::new()); assert_eq!(game.winning_score(), 291); assert_eq!(game.round, 17); assert_eq!(game.winner(), 1); } } 
Enter fullscreen mode Exit fullscreen mode
Collapse
 
bgaster profile image
Benedict Gaster • Edited

Ah today Haskell was a great choice, had fun and got it done really quick, which was lucky as spent a large amount of the day baking ginger bread houses for the kids to cover in icing and sweets :-)

parse :: String -> ([Int], [Int]) parse is = let xs = splitWhen (=="Player 2:") (lines is) players = (filter (/="") (tail (head xs)), head (tail xs)) in bimap (map read) (map read) players task1 :: ([Int], [Int]) -> Int task1 ([], p2) = sum $ zipWith (*) p2 [length p2, length p2-1..1] task1 (p1, []) = sum $ zipWith (*) p1 [length p1, length p1-1..1] task1 players = task1 (round players) where round (t:ts, t':ts') | t > t' = (ts ++ [t,t'], ts') | otherwise = (ts, ts' ++ [t',t]) task2 = result . game S.empty where result ([], p2) = sum $ zipWith (*) p2 [length p2, length p2-1..1] result (p1, []) = sum $ zipWith (*) p1 [length p1, length p1-1..1] game _ (xs, []) = (xs, []) game _ ([], ys) = ([], ys) game s (xs, ys) | (xs, ys) `S.member` s = (xs, []) | otherwise = round (S.insert (xs, ys) s) xs ys round s (x:xs) (y:ys) | (x > length xs || y > length ys) && x < y = game s (xs, ys ++ [y, x]) | (x > length xs || y > length ys) && x > y = game s (xs ++ [x, y], ys) | otherwise = case game S.empty (take x xs, take y ys) of (_, []) -> game s (xs ++ [x, y], ys) ([], _) -> game s (xs, ys ++ [y, x]) main = do is <- readFile "day22_input" <&> parse print (task1 is) print (task2 is) 
Enter fullscreen mode Exit fullscreen mode
Collapse
 
rpalo profile image
Ryan Palo

This one seemed pretty straightforward, so I knocked it out in Python in an effort to get caught up. I forgot about the "no repeats" rule and stared at my terminal recursing infinitely for a while. Also, deque was a great idea, but I didn't realize you couldn't slice a deque in Python, which made copying them partially a bit awkward.

"""Day 22: Crab Combat Play a crab in Combat (War) and calculate the score of the winner. """ from collections import deque from copy import deepcopy def parse(filename): """Parses the input file into a list of players numbers. Input file has the format: Player 1: 1 2 3 Player 2: 4 5 6 Supports arbitrary numbers of players. """ players = [deque()] current = 0 with open(filename, "r") as f: for line in f: line = line.strip() if line.isnumeric(): players[current].append(int(line)) continue if not line: current += 1 players.append(deque()) return players def part1(players): """Simulate a game of war and then calculate the score of the winner. """ while all(player for player in players): plays = [player.popleft() for player in players] winner = max((value, ind) for (ind, value) in enumerate(plays)) players[winner[1]].extend(sorted(plays, reverse=True)) return max(calculate_score(player) for player in players) def recursive_war(p1, p2): """Recursive war follows these rules: 1. If a particular configuration has been seen before in a game, Player 1 automatically wins. 2. Otherwise, if either player's number is greater than the number of cards they have, the player with the highest number wins. 3. Otherwise, the winner is the winner of a sub-game, where players play with the 1-nth cards in their deck where n is the value of their 0th card. Subgames do not affect parent games' decks (i.e. they are played with copies). Returns the calculated score of the winner of the root game's deck. Not designed for arbitrary #'s of players. Only two. """ seen = set() while p1 and p2: if str(p1) + str(p2) in seen: return 0, calculate_score(p1) else: seen.add(str(p1) + str(p2)) v1, v2 = p1.popleft(), p2.popleft() if v1 <= len(p1) and v2 <= len(p2): winner, _score = recursive_war(deque(list(p1)[:v1]), deque(list(p2)[:v2])) else: winner = 0 if v1 > v2 else 1 if winner == 0: p1.extend([v1, v2]) else: p2.extend([v2, v1]) if p1: return 0, calculate_score(p1) else: return 1, calculate_score(p2) def calculate_score(player): """Calculate the "score" of a player's deck. Their bottom card times 1 plus their second bottom card times 2 plus ... """ return sum(val * ind for ind, val in enumerate(reversed(player), start=1)) def part2(players): """Calculate the score of a winner of recursive War.""" winner, score = recursive_war(*players) return score if __name__ == "__main__": players = parse("data/day22_test.txt") assert part1(deepcopy(players)) == 306 assert part2(players) == 291 print("All tests passed!") players = parse("data/day22.txt") print("Part 1: ", part1(deepcopy(players))) print("Part 2: ", part2(players)) 
Enter fullscreen mode Exit fullscreen mode
Collapse
 
mustafahaddara profile image
Mustafa Haddara • Edited

I got mine done but it runs really slowly (~50s)...I'm pretty sure I did something dumb here. There's a lot of loops and I suspect I accidentally went O(n2) or possibly even O(n3)

import fs = require('fs'); const solve = (lines: string[]): number => { let i = 1; const p1 = []; const p2 = []; while (lines[i] !== '') { p1.push(parseInt(lines[i])); i++; } i++; // skip the blank line i++; // skip the header while (lines[i] !== '') { p2.push(parseInt(lines[i])); i++; } const winning_deck: number[] = conflict(p1, p2); return winning_deck.map((card, idx) => card * (winning_deck.length - idx)).reduce((total, value) => total + value, 0); }; const testInput = ['Player 1:', '9', '2', '6', '3', '1', '', 'Player 2:', '5', '8', '4', '7', '10', '']; // export const testInput = ['Player 1:', '43', '19', '', 'Player 2:', '2', '29', '14', '']; const conflict = (p1: number[], p2: number[], game_num = 1): number[] => { const turns = []; while (p1.length !== 0 && p2.length !== 0) { // recursive check if (turn_happened_before(turns, p1, p2)) { return p1; } turns.push([[...p1], [...p2]]); const p1card = p1.shift(); // removes const p2card = p2.shift(); // removes if (p1card <= p1.length && p2card <= p2.length) { // recurse to determine winner const subp1 = p1.slice(0, p1card); const subp2 = p2.slice(0, p2card); conflict(subp1, subp2, game_num + 1); // subp1 and subp2 get mutated if (subp1.length === 0) { // p2 won p2.push(p2card); p2.push(p1card); } else { // p1 won p1.push(p1card); p1.push(p2card); } } else if (p1card > p2card) { p1.push(p1card); p1.push(p2card); } else { p2.push(p2card); p2.push(p1card); } } if (p1.length === 0) return p2; else return p1; }; const turn_happened_before = (turns: [number[], number[]][], p1: number[], p2: number[]): boolean => { return turns.some((turn) => arr_eq(turn[0], p1) && arr_eq(turn[1], p2)); }; const arr_eq = (a1: number[], a2: number[]): boolean => a1.every((val, idx) => val === a2[idx]); const main = () => { const lines = getLinesFromFile(); console.log(solve(lines)); }; const getLinesFromFile = () => { const fname = `inputs/day22.txt`; return fs.readFileSync(fname, 'utf8').split('\n'); }; main(); 
Enter fullscreen mode Exit fullscreen mode
Collapse
 
meseta profile image
Yuan Gao • Edited

A quick bit of python, didn't want to spend too long on it

data = open("input.txt").read().splitlines() player1 = list(map(int, data[1:26])) player2 = list(map(int, data[28:54])) def game(depth, player1, player2): records = {} while player1 and player2: key = str(player1)+str(player2) if key in records: return 0 records[key] = None card1 = player1.pop(0) card2 = player2.pop(0) if len(player1) >= card1 and len(player2) >= card2: winner = game(depth+1, player1[:card1], player2[:card2]) else: winner = card2 > card1 if winner: player2.extend([card2, card1]) else: player1.extend([card1, card2]) if depth == 0: return (player2 if winner else player1) return winner windeck = game(0, player1, player2) print("sum", sum((a+1) * b for a, b in enumerate(reversed(windeck)))) 
Enter fullscreen mode Exit fullscreen mode
Collapse
 
brunoams profile image
Bruno AMS

In .NET 6 (1s)

var players = File.ReadAllText("input.txt") .Split(":").Where(p => !p.StartsWith("Player")) .Select((p, i) => (Player: i + 1, Deck: new Queue<int>(p.Split('\n').Where(n => char.IsNumber(n, 0)).Select(q => int.Parse(q))))) .ToArray(); int Play((int Player, Queue<int> Deck)[] players, bool enableSubgame = false) { HashSet<string> previousRounds = new(); while (players.Count(p => p.Deck.Any()) > 1) { if (!previousRounds.Add(string.Join("@", players.Select(p => $"{string.Join("", p.Deck)}")))) return 1; var roundPlayerCards = players.Select(p => (p.Player, Card: p.Deck.Dequeue())).ToArray(); var (roundWinningPlayer, winningDeck) = players.First(p => p.Player == roundPlayerCards.MaxBy(p => p.Card).Player); var haveSubgame = roundPlayerCards.All(cp => players.Any(c => c.Player == cp.Player && c.Deck.Count >= cp.Card)); if (haveSubgame && enableSubgame) { var subgame = players.Select(p => (p.Player, Deck: new Queue<int>(p.Deck.Take(roundPlayerCards.First(c => c.Player == p.Player).Card)))).ToArray(); roundWinningPlayer = Play(subgame, enableSubgame); (_, winningDeck) = players.First(p => p.Player == roundWinningPlayer); } foreach (var (_, card) in roundPlayerCards.OrderByDescending(p => p.Player == roundWinningPlayer)) winningDeck.Enqueue(card); } return players.First(p => p.Deck.Any()).Player; } var playerWinning = Play(players, enableSubgame: true); var score = players.First(p => p.Player == playerWinning).Deck.Reverse().Select((v, i) => v * (i + 1)).Sum(); 
Enter fullscreen mode Exit fullscreen mode
Collapse
 
thibpat profile image
Thibaut Patel

My JavaScript video walkthrough: