Skip to content

Commit c2a36db

Browse files
committed
Day 21 in Raku
1 parent d58f0b3 commit c2a36db

File tree

5 files changed

+144
-0
lines changed

5 files changed

+144
-0
lines changed

2021/day21/day21.raku

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
#!/usr/bin/env raku
2+
# Copyright 2021 Google LLC
3+
#
4+
# Use of this source code is governed by an MIT-style
5+
# license that can be found in the LICENSE file or at
6+
# https://opensource.org/licenses/MIT.
7+
#
8+
# https://adventofcode.com/2021/day/21
9+
use v6.d;
10+
use fatal;
11+
12+
grammar InputFormat {
13+
rule TOP { <line>+ }
14+
token ws { <!ww>\h* }
15+
token num { \d+ }
16+
rule line { ^^ Player <num> starting position\: <num> $$ \n }
17+
}
18+
19+
class Actions {
20+
method TOP($/) { make $<line>».made }
21+
method num($/) { make $/.Int }
22+
method line($/) { make {player => $<num>[0].made, start => $<num>[1].made } }
23+
}
24+
25+
# Two players are on a loop with spaces 1 through 1. Take turn rollig a die three
26+
# times, add the sum to their position, add the resulting position to their score.
27+
class Solver {
28+
has Str $.input is required;
29+
has $.parsed = InputFormat.parse($!input, :actions(Actions.new)) || die 'Parse failed';
30+
has @.lines = $!parsed<line.made;
31+
}
32+
33+
# Deterministic die rolls 1 through 100 in a cycle. Result is number of rolls
34+
# times the score of the losing player once someone reaches 1000.
35+
class Part1 is Solver {
36+
method solve( --> Str(Cool)) {
37+
my @players = @.lines.map({(player => $_<player>, pos => $_<start>, score => 0).Hash});
38+
my @rolls = 1..100;
39+
my $turn = 0;
40+
my $i = 1;
41+
my $rollcount = 0;
42+
while @players.map(*<score>).all < 1000 {
43+
my $roll = ((++$rollcount - 1) % 100 + 1 for ^3).sum;
44+
my $pos = (@players[$turn]<pos> - 1 + $roll) % 10 + 1;
45+
@players[$turn]<score> += $pos;
46+
@players[$turn]<pos> = $pos;
47+
$turn = ($turn + 1) % 2;
48+
}
49+
# say @players;
50+
$rollcount * @players.map(*<score>).min
51+
}
52+
}
53+
54+
# Dirac dice produce a superposition of 1, 2, and 3 each roll.
55+
# Result is the number of times the most-winning player wins.
56+
class Part2 is Solver {
57+
has %.memo;
58+
59+
method win-counts($pos, $score, $p1-turn) {
60+
return 1+0i if $score.re21;
61+
return 0+1i if $score.im21;
62+
my $key = "$pos#$score#$p1-turn";
63+
return %!memo{$key} if %!memo{$key}:exists;
64+
my $wins = 0+0i;
65+
for 1..3 X 1..3 X 1..3 -> $roll {
66+
my $p;
67+
my $s = $score;
68+
if $p1-turn {
69+
$p = Complex.new(($pos.re + $roll.sum - 1) % 10 + 1, $pos.im);
70+
$s += $p.re;
71+
} else {
72+
$p = Complex.new($pos.re, (($pos.im + $roll.sum - 1) % 10 + 1));
73+
$s += i*$p.im;
74+
}
75+
$wins += self.win-counts($p, $s, !$p1-turn);
76+
}
77+
%!memo{$key} = $wins
78+
}
79+
80+
method solve( --> Str(Cool)) {
81+
my $pos = Complex.new(@.lines[0]<start>, @.lines[1]<start>);
82+
my $wins = self.win-counts($pos, 0+0i, True);
83+
# say "Win counts: $wins";
84+
# say "Memoization cached {+%!memo} trees, saving {%!memo.values.map({.re + .im}).sum} wins";
85+
max($wins.re, $wins.im)
86+
}
87+
}
88+
89+
class RunContext {
90+
has $.input-file;
91+
has $.input;
92+
has %.expected is rw;
93+
has @.passed;
94+
95+
method run-part(Solver:U $part) {
96+
my $num = $part.^name.comb(/\d+/).head;
97+
my $expected = $.expected«$num» // '';
98+
say "Running Day21 part $num on $!input-file expecting '$expected'";
99+
my $start = now;
100+
my $solver = $part.new(:$!input);
101+
my $result = $solver.solve();
102+
my $end = now;
103+
put $result;
104+
"Part $num took %.3fms\n".printf(($end - $start) * 1000);
105+
@!passed.push($result eq 'TODO' || $expected && $expected eq $result);
106+
if $expected {
107+
if $expected eq $result {
108+
say "\c[CHECK MARK] PASS with expected value '$result'";
109+
} else {
110+
say "\c[CROSS MARK] FAIL expected '$expected' but got '$result'";
111+
}
112+
}
113+
}
114+
}
115+
116+
sub MAIN(*@input-files) {
117+
my $exit = all();
118+
for @input-files -> $input-file {
119+
if $input-file.IO.slurp -> $input {
120+
my $context = RunContext.new(:$input, :$input-file);
121+
if (my $expected-file = $input-file.IO.extension('expected')).f {
122+
for $expected-file.lines {
123+
$context.expected«$0» = $1.trim if m/part (\d+) \: \s* (.*)/;
124+
}
125+
}
126+
$context.run-part(Part1);
127+
say '';
128+
$context.run-part(Part2);
129+
$exit &= all($context.passed);
130+
} else {
131+
say "EMPTY INPUT FILE: $input-file";
132+
}
133+
say '=' x 40;
134+
}
135+
exit $exit ?? 0 !! 1;
136+
}

2021/day21/input.actual.expected

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
part1: 916083
2+
part2: 49982165861983

2021/day21/input.actual.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Player 1 starting position: 10
2+
Player 2 starting position: 2

2021/day21/input.example.expected

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
part1: 739785
2+
part2: 444356092776315

2021/day21/input.example.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Player 1 starting position: 4
2+
Player 2 starting position: 8

0 commit comments

Comments
 (0)