TL;DR

On with Advent of Code puzzle 5 from 2021: an overkill solution.

This challenge basically requires implementing a system to track Bingo! boards and figure out which of them is the winner.

When no immediate, clever solution comes to my mind (which is, letâ€™s say it, most of the times), I usually start implementing features that will probably be useful. I know, itâ€™s bottom-up and tends to require much more effort, but at least it gets me moving towards the end.

This year Iâ€™m also trying to do some more practice in Raku, which is another reason why I tend to err on the regular, boring stuff. Which has also the advantage of usually being also readable and maintenable, so regular boring stuff for the win!.

In this puzzle, I opted for implementing a full-fledged class that allows managing a board, with all bells and whistles and batteries included:

• tracking the contents of each cell, of course, indexed by row and column;
• do the same tracking by the value they contain inside;
• track the amount of marked cells by row;
• track the amount of marked cells by column;
• track a new value extracted and mark the related cell, if present;
• track the score of the board, which gets calculated as soon as a board has a complete row or column of marked cells;
• query the object to see if itâ€™s in a winning state;
• printing the board;
• dumping the contents;
• resetting the board to the original state;
• convenience method to sweep the whole board, both by rows and by columns.

Hereâ€™s the monster:

``````class Board {
has %!cell-for;
has @!cell-at;
has %!count-for;
has \$!score = Nil;

multi method BUILD (Str:D :\$desc) {
my \$ri = 0;
for \$desc.split(/\r?\n/) -> \$line {
%!count-for<rows>[\$ri] = 0;
my \$ci = 0;
for \$line.split(/\s+/) -> \$cell {
%!count-for<cols>[\$ci] //= 0;
next unless \$cell ~~ /\d/;
@!cell-at[\$ri][\$ci] = %!cell-for{\$cell} = [\$ri, \$ci, 0, \$cell];
++\$ci;
}
++\$ri;
}
}
method sweep-by-cols (&cb) {
my \$n-rows = @!cell-at.end;
my \$n-cols = @!cell-at[0].end;
for 0 .. \$n-cols -> \$ci {
for 0 .. \$n-rows -> \$ri {
&cb(@!cell-at[\$ri][\$ci]);
}
}
}
method sweep-by-rows (&cb) {
my \$n-rows = @!cell-at.end;
my \$n-cols = @!cell-at[0].end;
for 0 .. \$n-rows -> \$ri {
for 0 .. \$n-cols -> \$ci {
&cb(@!cell-at[\$ri][\$ci]);
}
}
}
method dump () {
@!cell-at.say;
%!cell-for.say;
%!count-for.say;
}
method print () {
my \$last;
my @line;
self.sweep-by-rows: -> \$cell {
if (\$last && \$last[0] < \$cell[0]) {
@line.join(' ').put;
@line = ();
}
@line.push: '%2d%s'.sprintf(\$cell[3], \$cell[2] ?? '*' !! ' ');
\$last = \$cell;
};
@line.join(' ').put;
}
method mark (\$value) {
return unless %!cell-for{\$value}:exists;
my \$cell = %!cell-for{\$value};
\$cell[2] = 1;
%!count-for<rows>[\$cell[0]]++;
%!count-for<cols>[\$cell[1]]++;
if ! defined \$!score {
if (@!cell-at.elems == %!count-for<cols>[\$cell[1]])
|| (@!cell-at[0].elems == %!count-for<rows>[\$cell[0]]) {
\$!score = 0;
self.sweep-by-rows: -> \$cell {
\$!score += \$cell[3] unless \$cell[2];
}
\$!score *= \$value;
}
}
return self;
}
method reset () {
for %!count-for.values -> \$seq {
for @\$seq -> \$item is rw {
\$item = 0;
}
}
for %!cell-for.values -> \$cell {
\$cell[2] = 0;
}
\$!score = Nil;
}
method won () { return defined \$!score }
method score () { return \$!score }
}
``````

There is indeed a custom builder, because weâ€™re taking the inputs from a file so it comes as text that must be parsed.

At the end of the day, only methods `mark`, `won` and `score` were actually needed (the latter two being more or less the same, as you can see from the implementation). Well, thereâ€™s also `reset`, of course, as well as using `sweep-by-rows` inside `mark` when we calculate the score.

All in all itâ€™s been a fun experience and the result seems tidy and to the point.

And, of course, incredibly overkill.

If youâ€™re curious to run the full code, you can find it here. In any caseâ€¦ stay safe!