Added Solution for 2021 day 21
This commit is contained in:
parent
912bb4b2d9
commit
4d4fd2de5a
5 changed files with 227 additions and 0 deletions
8
2021/day21_dirac_dice/Cargo.toml
Normal file
8
2021/day21_dirac_dice/Cargo.toml
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
[package]
|
||||||
|
name = "day21_dirac_dice"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
64
2021/day21_dirac_dice/challenge.txt
Normal file
64
2021/day21_dirac_dice/challenge.txt
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
There's not much to do as you slowly descend to the bottom of the ocean. The submarine computer challenges you to a nice game of *Dirac Dice*.
|
||||||
|
|
||||||
|
This game consists of a single [die](https://en.wikipedia.org/wiki/Dice), two [pawns](https://en.wikipedia.org/wiki/Glossary_of_board_games#piece), and a game board with a circular track containing ten spaces marked `1` through `10` clockwise. Each player's *starting space* is chosen randomly (your puzzle input). Player 1 goes first.
|
||||||
|
|
||||||
|
Players take turns moving. On each player's turn, the player rolls the die *three times* and adds up the results. Then, the player moves their pawn that many times *forward* around the track (that is, moving clockwise on spaces in order of increasing value, wrapping back around to `1` after `10`). So, if a player is on space `7` and they roll `2`, `2`, and `1`, they would move forward 5 times, to spaces `8`, `9`, `10`, `1`, and finally stopping on `2`.
|
||||||
|
|
||||||
|
After each player moves, they increase their *score* by the value of the space their pawn stopped on. Players' scores start at `0`. So, if the first player starts on space `7` and rolls a total of `5`, they would stop on space `2` and add `2` to their score (for a total score of `2`). The game immediately ends as a win for any player whose score reaches *at least `1000`*.
|
||||||
|
|
||||||
|
Since the first game is a practice game, the submarine opens a compartment labeled *deterministic dice* and a 100-sided die falls out. This die always rolls `1` first, then `2`, then `3`, and so on up to `100`, after which it starts over at `1` again. Play using this die.
|
||||||
|
|
||||||
|
For example, given these starting positions:
|
||||||
|
|
||||||
|
```
|
||||||
|
Player 1 starting position: 4
|
||||||
|
Player 2 starting position: 8
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
This is how the game would go:
|
||||||
|
|
||||||
|
* Player 1 rolls `1`+`2`+`3` and moves to space `10` for a total score of `10`.
|
||||||
|
* Player 2 rolls `4`+`5`+`6` and moves to space `3` for a total score of `3`.
|
||||||
|
* Player 1 rolls `7`+`8`+`9` and moves to space `4` for a total score of `14`.
|
||||||
|
* Player 2 rolls `10`+`11`+`12` and moves to space `6` for a total score of `9`.
|
||||||
|
* Player 1 rolls `13`+`14`+`15` and moves to space `6` for a total score of `20`.
|
||||||
|
* Player 2 rolls `16`+`17`+`18` and moves to space `7` for a total score of `16`.
|
||||||
|
* Player 1 rolls `19`+`20`+`21` and moves to space `6` for a total score of `26`.
|
||||||
|
* Player 2 rolls `22`+`23`+`24` and moves to space `6` for a total score of `22`.
|
||||||
|
|
||||||
|
...after many turns...
|
||||||
|
|
||||||
|
* Player 2 rolls `82`+`83`+`84` and moves to space `6` for a total score of `742`.
|
||||||
|
* Player 1 rolls `85`+`86`+`87` and moves to space `4` for a total score of `990`.
|
||||||
|
* Player 2 rolls `88`+`89`+`90` and moves to space `3` for a total score of `745`.
|
||||||
|
* Player 1 rolls `91`+`92`+`93` and moves to space `10` for a final score, `1000`.
|
||||||
|
|
||||||
|
Since player 1 has at least `1000` points, player 1 wins and the game ends. At this point, the losing player had `745` points and the die had been rolled a total of `993` times; `745 * 993 = *739785*`.
|
||||||
|
|
||||||
|
Play a practice game using the deterministic 100-sided die. 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?*
|
||||||
|
|
||||||
|
Your puzzle answer was `518418`.
|
||||||
|
|
||||||
|
\--- Part Two ---
|
||||||
|
----------
|
||||||
|
|
||||||
|
Now that you're warmed up, it's time to play the real game.
|
||||||
|
|
||||||
|
A second compartment opens, this time labeled *Dirac dice*. Out of it falls a single three-sided die.
|
||||||
|
|
||||||
|
As you experiment with the die, you feel a little strange. An informational brochure in the compartment explains that this is a *quantum die*: when you roll it, the universe *splits into multiple copies*, one copy for each possible outcome of the die. In this case, rolling the die always splits the universe into *three copies*: one where the outcome of the roll was `1`, one where it was `2`, and one where it was `3`.
|
||||||
|
|
||||||
|
The game is played the same as before, although to prevent things from getting too far out of hand, the game now ends when either player's score reaches at least `*21*`.
|
||||||
|
|
||||||
|
Using the same starting positions as in the example above, player 1 wins in `*444356092776315*` universes, while player 2 merely wins in `341960390180808` universes.
|
||||||
|
|
||||||
|
Using your given starting positions, determine every possible outcome. *Find the player that wins in more universes; in how many universes does that player win?*
|
||||||
|
|
||||||
|
Your puzzle answer was `116741133558209`.
|
||||||
|
|
||||||
|
Both parts of this puzzle are complete! They provide two gold stars: \*\*
|
||||||
|
|
||||||
|
At this point, you should [return to your Advent calendar](/2021) and try another puzzle.
|
||||||
|
|
||||||
|
If you still want to see it, you can [get your puzzle input](21/input).
|
151
2021/day21_dirac_dice/src/lib.rs
Normal file
151
2021/day21_dirac_dice/src/lib.rs
Normal file
|
@ -0,0 +1,151 @@
|
||||||
|
use core::fmt::Display;
|
||||||
|
use std::{num::ParseIntError, collections::BTreeMap};
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
|
pub enum ParseError {
|
||||||
|
ParseIntError(std::num::ParseIntError),
|
||||||
|
LineMalformed(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ParseIntError> for ParseError {
|
||||||
|
fn from(value: ParseIntError) -> Self {
|
||||||
|
Self::ParseIntError(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for ParseError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::ParseIntError(e) => write!(f, "Unable to parse into integer: {e}"),
|
||||||
|
Self::LineMalformed(v) => write!(f, "Line is malformed: {v}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const WINNING_SCORE_V1: usize = 1000;
|
||||||
|
const WINNING_SCORE_V2: usize = 21;
|
||||||
|
|
||||||
|
#[derive(Clone, PartialOrd, Ord, PartialEq, Eq)]
|
||||||
|
struct Player {
|
||||||
|
score: usize,
|
||||||
|
position: usize,
|
||||||
|
won: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&str> for Player {
|
||||||
|
type Error = ParseError;
|
||||||
|
|
||||||
|
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
||||||
|
let (_, pos) = value.rsplit_once(' ').ok_or(Self::Error::LineMalformed(value.to_string()))?;
|
||||||
|
Ok(Self {
|
||||||
|
position: pos.parse()?,
|
||||||
|
score: 0,
|
||||||
|
won: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Player {
|
||||||
|
fn play(&mut self, die: &mut DeterministicDie) -> bool {
|
||||||
|
self.roll(die.roll() + die.roll() + die.roll());
|
||||||
|
if self.score >= WINNING_SCORE_V1 {
|
||||||
|
self.won = true;
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn roll(&mut self, face: usize) {
|
||||||
|
self.position = (self.position + face - 1) % 10 + 1;
|
||||||
|
self.score += self.position;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct DeterministicDie {
|
||||||
|
roll_counter: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DeterministicDie {
|
||||||
|
fn roll(&mut self) -> usize {
|
||||||
|
self.roll_counter += 1;
|
||||||
|
(self.roll_counter - 1) % 100 + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run(input: &str) -> Result<(usize, usize), ParseError> {
|
||||||
|
let mut players: Vec<_> = input.lines().map(Player::try_from).collect::<Result<Vec<_>, _>>()?;
|
||||||
|
let second = max_wins(&players);
|
||||||
|
let mut die = DeterministicDie::default();
|
||||||
|
'outer: loop {
|
||||||
|
for player in players.iter_mut() {
|
||||||
|
if player.play(&mut die) {
|
||||||
|
break 'outer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let first = players.iter().find(|p| !p.won).map(|p| p.score).unwrap() * die.roll_counter;
|
||||||
|
Ok((first, second))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn max_wins(players: &[Player]) -> usize {
|
||||||
|
let mut wins = [0, 0];
|
||||||
|
let mut open_set = BTreeMap::from([(players.to_vec(), 1)]);
|
||||||
|
|
||||||
|
let roll_results = [
|
||||||
|
(3, 1),
|
||||||
|
(4, 3),
|
||||||
|
(5, 6),
|
||||||
|
(6, 7),
|
||||||
|
(7, 6),
|
||||||
|
(8, 3),
|
||||||
|
(9, 1),
|
||||||
|
];
|
||||||
|
|
||||||
|
while let Some(current) = open_set.pop_first() {
|
||||||
|
let players = current.0;
|
||||||
|
let count = current.1;
|
||||||
|
for d1 in roll_results {
|
||||||
|
let mut player_1 = players[0].clone();
|
||||||
|
player_1.roll(d1.0);
|
||||||
|
if player_1.score >= WINNING_SCORE_V2 {
|
||||||
|
wins[0] += d1.1 * count;
|
||||||
|
} else {
|
||||||
|
for d2 in roll_results {
|
||||||
|
let mut player_2 = players[1].clone();
|
||||||
|
player_2.roll(d2.0);
|
||||||
|
if player_2.score >= WINNING_SCORE_V2 {
|
||||||
|
wins[1] += d1.1 * d2.1 * count;
|
||||||
|
} else {
|
||||||
|
open_set.entry(vec![player_1.clone(), player_2]).and_modify(|ct| *ct += count * d1.1 * d2.1).or_insert(count * d1.1 * d2.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*wins.iter().max().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::fs::read_to_string;
|
||||||
|
|
||||||
|
fn read_file(name: &str) -> String {
|
||||||
|
read_to_string(name).expect(&format!("Unable to read file: {name}")[..]).trim().to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sample() {
|
||||||
|
let sample_input = read_file("tests/sample_input");
|
||||||
|
assert_eq!(run(&sample_input), Ok((739785, 444356092776315)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_challenge() {
|
||||||
|
let challenge_input = read_file("tests/challenge_input");
|
||||||
|
assert_eq!(run(&challenge_input), Ok((518418, 116741133558209)));
|
||||||
|
}
|
||||||
|
}
|
2
2021/day21_dirac_dice/tests/challenge_input
Normal file
2
2021/day21_dirac_dice/tests/challenge_input
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
Player 1 starting position: 8
|
||||||
|
Player 2 starting position: 1
|
2
2021/day21_dirac_dice/tests/sample_input
Normal file
2
2021/day21_dirac_dice/tests/sample_input
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
Player 1 starting position: 4
|
||||||
|
Player 2 starting position: 8
|
Loading…
Add table
Add a link
Reference in a new issue