From f25371a379414bfa491d126edd57ebfcda2e81e1 Mon Sep 17 00:00:00 2001
From: Ryan Reed <ryanreed@noreply.gitea.ryanreed.net>
Date: Sat, 4 Dec 2021 19:12:43 -0500
Subject: [PATCH] Adding day04

I'm not happy with how this works. I should have built it much different. Maybe I'll refactor in the future..
Co-Authored-By: Ryan Reed <ryanreed@noreply.gitea.ryanreed.net>
Co-Committed-By: Ryan Reed <ryanreed@noreply.gitea.ryanreed.net>
---
 inputs/day04-example.txt       |  19 ++
 inputs/day04.txt               | 601 +++++++++++++++++++++++++++++++++
 puzzles/TEMPLATE_test_dayXX.py |   8 +-
 puzzles/day04.py               | 102 ++++++
 puzzles/test_day04.py          |  32 ++
 5 files changed, 758 insertions(+), 4 deletions(-)
 create mode 100644 inputs/day04-example.txt
 create mode 100644 inputs/day04.txt
 create mode 100644 puzzles/day04.py
 create mode 100644 puzzles/test_day04.py

diff --git a/inputs/day04-example.txt b/inputs/day04-example.txt
new file mode 100644
index 0000000..669a51d
--- /dev/null
+++ b/inputs/day04-example.txt
@@ -0,0 +1,19 @@
+7,4,9,5,11,17,23,2,0,14,21,24,10,16,13,6,15,25,12,22,18,20,8,19,3,26,1
+
+22 13 17 11  0
+ 8  2 23  4 24
+21  9 14 16  7
+ 6 10  3 18  5
+ 1 12 20 15 19
+
+ 3 15  0  2 22
+ 9 18 13 17  5
+19  8  7 25 23
+20 11 10 24  4
+14 21 16 12  6
+
+14 21 17 24  4
+10 16 15  9 19
+18  8 23 26 20
+22 11 13  6  5
+ 2  0 12  3  7
diff --git a/inputs/day04.txt b/inputs/day04.txt
new file mode 100644
index 0000000..c21b9b9
--- /dev/null
+++ b/inputs/day04.txt
@@ -0,0 +1,601 @@
+18,99,39,89,0,40,52,72,61,77,69,51,30,83,20,65,93,88,29,22,14,82,53,41,76,79,46,78,56,57,24,36,38,11,50,1,19,26,70,4,54,3,84,33,15,21,9,58,64,85,10,66,17,43,31,27,2,5,95,96,16,97,12,34,74,67,86,23,49,8,59,45,68,91,25,48,13,28,81,94,92,42,7,37,75,32,6,60,63,35,62,98,90,47,87,73,44,71,55,80
+
+12 79 33 36 97
+82 38 77 61 84
+55 74 21 65 39
+54 56 95 50  2
+45 68 31 94 14
+
+95 26 80 25 62
+79 91 70 76 61
+98 97 17 75 23
+71 30 21 52 29
+20 54 32 12 31
+
+35 31 86 36 52
+18 50 79 60 67
+98  3 51 46 25
+ 4 61 55 87 70
+94 39 68 27 14
+
+ 9 49 53 52  2
+65 92  4 73 67
+90 95 39 78 64
+17 27 13 60 51
+25 63 70 77 22
+
+33 34  6 58 77
+44 75 54 18 21
+15 25 79 12 70
+90 14 81 22 49
+72 38 59 78 16
+
+13 51 94 40 56
+ 6 99  9 64 91
+87 73 93 71 31
+39 42 28 24  7
+ 1 52 17 85 11
+
+95 23 15 29 12
+40 79  2 64 59
+94 93  3 25 10
+ 5 58  7 78 82
+62 73 74 16 56
+
+17 55 56 14 51
+70 94 66 21 37
+97 90 43 80 89
+50 12 52 46 85
+16 79 60 15 45
+
+18 59 37 25 41
+51 61 63  7 53
+ 3 21 31 76 72
+29 99 34  6 83
+55 45 93 75 15
+
+59 63  4 95 68
+18 47 43 15 26
+66  7 54 22  8
+50 75 91 20 98
+49 61 55 12  1
+
+96 74 37 12 66
+10 94  7 29 75
+20 48 26 67 41
+40 99  9 80 35
+72 87 82 76 85
+
+60 58 43 17 57
+25 33 85 91 27
+46 36 54 49 64
+86 53  4 63 95
+38 74 84 37 67
+
+66 90 73 98 51
+11 94 60 92 14
+82 58 17 63 52
+33  1 18 77 72
+12 76 20 23 10
+
+28 69 67  6 32
+ 4 17 77 52 33
+26 57 92 56 73
+30 91 50 82  2
+21 31 61 78 99
+
+98 63 26 81 75
+23 36  3 88 65
+69 87 33 20 43
+ 4 51 47 70 50
+10 91 74 77 66
+
+61 47 90 22 63
+26 15 41  8 73
+71 40 82 91 75
+39 95 56 84 55
+ 5 76 32 69 34
+
+78 77  1 57 39
+70 49 83 90 87
+97 74 26 67 19
+50 21  2 92 60
+82 65 62 31 33
+
+ 6 46 97 50  2
+48 23 34 24 93
+83  7 31 59 66
+44 18 21 47 77
+69 86 35 72 17
+
+78 68 69  4 26
+47 38 15 95 53
+66 46 49 79 59
+82 30 50  7  2
+17 16 34 64 51
+
+19 20 13 75 26
+63 28 14 93 50
+27 70 61 29 67
+12 89 71 33 98
+31 25 16 79 69
+
+95 23 83 86 16
+ 4 50 27 92 62
+42 29 75 88 49
+31 58 39 44 93
+80 19 15 97 96
+
+91 60  6 47 63
+51 90 36 11 72
+73 84 50 76 40
+49 75 82  9 35
+71  1  0 67 59
+
+48 56 80 24 86
+ 4 74 95 53 41
+52 58 36 19 64
+90 87 99 82 25
+35 13 22 26 21
+
+68 87 18 23 97
+69 25 40 76 24
+34 33 56 16 51
+49 77 88 75 26
+ 2 19 96 35 86
+
+29 26 56 49 10
+89 91 95  5 69
+52  1 99 34 20
+ 8 74 64 32 61
+25 72 60 22 97
+
+39 65  0 91 86
+ 9  7 21 62 89
+22 92 63 46 17
+61 66 37 59 52
+93 70  3 97 49
+
+29  3 66  4 43
+46 25  7 62 54
+71 26 70 75  5
+17 82 33 51 19
+65 40 91 53 15
+
+88 59 14 41 92
+83 74 73 65 12
+95 97 35 47 55
+62  2  3  9 63
+52 44 32 33 72
+
+82 12 77 25 60
+ 8 93 38 56 97
+59  5 70 33 40
+ 9 90  1 41 57
+68 73 26 91  6
+
+72 15 64 59 49
+26 12  9 97 80
+84 18 91 20 87
+69 34 14 65 40
+98 74 27  5 31
+
+55 99 23 12 76
+57 92 71 83 53
+19 24 39 97 73
+96 91 32 34 28
+ 0 72 42 65  1
+
+84 50 97 46 99
+90  1 95 64 22
+45 53 55 25 42
+30 70 52 88 72
+29 44 98 51  6
+
+70 74 85 78 45
+34 91 82 63 99
+17  3 50 53 87
+38 88 98  6 92
+39 30 10 18 67
+
+26 45 13 93 29
+18 51 66 22 17
+55 75 70  9  6
+ 3 74 30 42 91
+21 77 57 37 90
+
+17 94 74 85 60
+ 2 67 97 19 11
+20 32 58 18 56
+10 95 40 51 93
+66 50 15 65 52
+
+ 7 38 79 77 42
+15 34 13 80 93
+46 54 74 33 17
+ 1  4 83 68 35
+70 85 66 63 39
+
+22 68 41  2 58
+69 76 14 62 33
+64 39 46 28 59
+60 21 26 13 74
+12 54 99 82  0
+
+ 9  0 82  4 20
+88 76 18 55 44
+10 53 74 93 58
+49 50 16 42  2
+19 23 66 54 65
+
+96  9 42 30 67
+ 3 29 94  5 45
+65 62 25 59 17
+21 79 73 44 66
+38  6 86  7  4
+
+89 77 17 13 79
+51 10 28 18 36
+59 69 80 61 25
+ 0 21  9 91 30
+86 96  2 29 39
+
+53 11 27 20 82
+15 38 90 34 98
+69 28 16 14 99
+67 49 48 42 59
+51  2 61 23  3
+
+37 60 14 43 49
+66 24 54 96 58
+73 28 48  3 81
+93 35 97 46 52
+75 84 79  1 11
+
+48 75  8 88 85
+81 94  3 32 28
+63 49 78 86 90
+67 84  5  4 52
+57 89 29 96 71
+
+ 3 34 89 15 64
+47 31 57 72 26
+48 75 19  2 65
+42 45 97 54 71
+55 27 87 77 35
+
+74 50 53 19 96
+95 89 71 75 26
+ 8 11 20 43 38
+78 51 28 65 93
+64  9 84 31 23
+
+37 42 65 94 92
+69 35 40  1 71
+63 43 75 59 74
+97 90 67 91 85
+28 58 21 49 52
+
+20 62 44 96 74
+18 37 63 54 51
+39 90  1  4 84
+ 7 72 57 19 79
+99 66 43 38 36
+
+ 2 68 67 21 78
+81 86 63 13 69
+74 29 53 56 30
+16 72  7 54  1
+64 50 73 14 45
+
+21 47 88 28 32
+ 7 43 83 39 27
+95  9 98 45 99
+68 49 94 73 35
+10 36 13 25 37
+
+45  2 16 75  9
+66 46 20 91 87
+36 74 64 85 62
+51 39 93  3 57
+ 0 37 56 27 50
+
+ 9 21 37 96 89
+66  4 16  3 56
+58 32 97 76 47
+ 7 59 13  0 34
+69 68 95 61 51
+
+30 41 60 88 98
+33 94 57 79 40
+14 36 17 56 99
+10 37 62 54  0
+65 32 58 21 23
+
+89 47 65 40 18
+25 17 13  0 33
+49 57 26  3 76
+15 71 70 95 61
+67 94 80 77  7
+
+92 40 55 95  8
+88 62  4 43 13
+54  1 19 20 87
+11 97 23 27 80
+63 10 66 45  7
+
+46 76 10 61 26
+86 55 15 84 25
+12 80 82 83 54
+22 81 69 70 48
+53 89 30 44 20
+
+84 62 57 74 30
+96 18 24  9 29
+22  8 56 44  4
+71 85 16 55 73
+25 28 88 61 33
+
+60 26 72 28 89
+51 94 24 96 53
+39  0 90 12 22
+92 95 65 34 32
+63 36 48 58 52
+
+92  5 32 52 30
+ 6 93 73 74 10
+38 62 55 28 79
+88 60 27  7 83
+82 53 39 70 95
+
+84 35 58  4 19
+80 96 53 45  7
+31 52 14  8 63
+87 41 75 65 18
+ 3 72 79 91 86
+
+16 19 42 49  0
+34 18 26 70 98
+45 38 87 17 54
+84 51  7 24 66
+57 46  3 36 43
+
+36 13 44 81 48
+93 88 41 77 57
+ 6 64  2  3 46
+30 10  8 20 14
+26 76  7  1 90
+
+67 74 39 25 78
+ 4 96 62 70 30
+22 41 72 47 32
+87 27 77 51 86
+65 35 26 11 85
+
+94 42 49 91 12
+78 51  3 95 76
+ 0 72 13 30 25
+33 81 32 58 62
+67  5 88 26  1
+
+71 21 29 73 75
+39 77 63 66 36
+74 18 80 35 34
+26  5 53 33  6
+19  1 27 25  9
+
+72 53 57 24 30
+66 84 68 40 12
+52 48 34 21 26
+73  3 18 81 67
+82  7 17 36 56
+
+93 85 42 81 30
+47  4 48 82 96
+28 41 52  0 36
+97 14 35  9 33
+98 11 69 20 87
+
+46 67 85 96 34
+90 92 68 56 51
+16 61 29 28 27
+81 57 64 53 26
+ 6 49 43 41 65
+
+37 82 55 88 27
+43 79 25  5 50
+92 83 81 44 22
+75 74 39 16 41
+48 29 80 77 90
+
+57 25 10 61 40
+ 1 22 20 81 41
+32 83 43 54 97
+29 47 15 27  8
+79 28 63 77 99
+
+62 43 94 24 48
+45 93 14 34 11
+58 31 33 71 97
+10 59 76 30 26
+64 54 79 80 56
+
+34  8 49 13 12
+54 57  4 82 46
+84 32 59 55 40
+ 0 73 99 38 56
+16 61 36 17 78
+
+ 7 97 37 81 91
+ 3 10 83 48 90
+49 40 72  6  2
+17 39 94 82 66
+58 68 52 64 42
+
+30 93 40 75 74
+62 48 47  3 59
+57 10 73 79 86
+71 89 88  5 38
+13  6 56 80 81
+
+58 54 52 83 88
+12 84 87 28 46
+42  6 27 39  4
+47 72 93 64 66
+79 71 44 18  3
+
+60 72 89 35 17
+77 66 68 59  9
+ 2 43 18 14 33
+20 30 82 31 52
+81 64 56 45 27
+
+64  9 44 23 99
+40 75 55 29 86
+94 46 45 84 97
+92 25 30 13 10
+90 87 61 53 70
+
+ 6 49 41 40 37
+65 24 47 33 55
+51  4 72 16 85
+56  5 25 98 14
+32 26 50 17 34
+
+24 78 72 40 31
+ 3 12 33 87 71
+99 91  8 59 34
+50 90 63 45 29
+75 96  7 22 44
+
+81 94 61 41 10
+23  5 66 27 70
+62 82  4 74 52
+19 55 67 59 49
+48 77 35 33 45
+
+ 7 86 81 93 19
+90 57 64 45 36
+75 13  2 28 39
+46 10 83 80 47
+ 1 91 94 24 56
+
+20 18  0 49 44
+77 72 31 61 46
+66 95 73  8 76
+ 3 13 99 94 96
+59 97 43 85 86
+
+34 78 54 37  0
+12 85 86 93 70
+53 10 59  8 55
+63 20 29 36 32
+76 75 43 96 61
+
+62 55 26 17 74
+10 15 47 38 20
+41 66 59 58  9
+34 52 40 93 82
+45 43 21 11 24
+
+85 90 47 75 50
+51 12  8 54 11
+73 19  3 37 28
+25 24 84 67 87
+44 94  6 20 45
+
+36 99 81 96 75
+89 90 29 33 55
+40 25 10 32 92
+51 27 70  0 15
+76 50 44 22 24
+
+31 63 21 13 24
+42 87 53  5 38
+ 8 50 30 73 83
+95 91 15 41 52
+23 99 10 67 60
+
+88 49  2  4 85
+22 25  6 30 62
+34 53 74 96 64
+77 80 93 65 63
+37 27 26 68 56
+
+86 90 40 79  7
+57  2 34 91 35
+85 74 52 97 93
+63 32 38  1 12
+29 46 56 84 25
+
+38 26 22 49 24
+56 84  2 18 62
+ 3 90 17 73 37
+55 95 92 35 29
+ 7 51 42  5 39
+
+14  0 19 53 69
+ 2 22 65 78 31
+43 88 37 93 46
+12 30 83 26 29
+92 68 62  7 44
+
+38 65 61 93 36
+53 43 11 12 54
+97 99 48 24 21
+84 33  3 23 31
+56  4 87 90  7
+
+ 0 61 67 13 56
+24 69 94 85 73
+78 35 89 55 72
+21  4 90 80 83
+64 99  5 16 36
+
+55 72  7 83 35
+63 73 96 21 75
+ 8 79 59 62 37
+15 89 28 80 82
+34 61 97 23  0
+
+48 21 43 36 15
+97 65 91  6 83
+84 64 98 31 90
+72 58  5 70 92
+52 71 25 41 51
+
+14 32 70  8 73
+71 75 95 29 68
+22 27 65  7 78
+21 37 47 19  6
+60 83  2 80 98
+
+59 90 57 29 68
+54 31 13 61 28
+65 37 17 53  1
+25 38 20 46 69
+23 18 77 98 96
+
+20 38 79 65 16
+93 77 14 97 47
+34 42  8 18 54
+91 64  0 58 75
+25 67 37  6 56
+
+88 81  6  8 65
+30 51 95 12 97
+70 16 55 54 73
+75 64 93  1 52
+82 77 24 50 36
+
+41 59 17 76 95
+51 58 61 73 56
+ 1  5 53 20 21
+22 14 37 28  0
+43 35 26 96 32
+
+17 86 68 23 20
+41 88 75 58 64
+94 90  3  0 26
+30 51 13 95 10
+97 77 83 57 21
diff --git a/puzzles/TEMPLATE_test_dayXX.py b/puzzles/TEMPLATE_test_dayXX.py
index e6771c0..dc4ac8d 100644
--- a/puzzles/TEMPLATE_test_dayXX.py
+++ b/puzzles/TEMPLATE_test_dayXX.py
@@ -37,10 +37,10 @@ def test_example2(example_data2):
 
 
 @pytest.mark.skip(reason="Not implemented")
-def test_part1(day02_data):
-    assert aoc.part1(day02_data) == ...
+def test_part1(dayXX_data):
+    assert aoc.part1(dayXX_data) == ...
 
 
 @pytest.mark.skip(reason="Not implemented")
-def test_part2(day02_data):
-    assert aoc.part2(day02_data) == ...
+def test_part2(dayXX_data):
+    assert aoc.part2(dayXX_data) == ...
diff --git a/puzzles/day04.py b/puzzles/day04.py
new file mode 100644
index 0000000..2bf4571
--- /dev/null
+++ b/puzzles/day04.py
@@ -0,0 +1,102 @@
+"""
+Advent of Code 2021 - Day 04
+
+Run with:
+    python puzzles/day04.py inputs/day04.txt
+"""
+
+import pathlib
+import sys
+from typing import Dict, List, Tuple
+
+import numpy as np
+
+
+class Bingo:
+    boards = None
+    draws = None
+
+    def __init__(self, inputs: str) -> None:
+        inputs = inputs.split("\n\n")
+
+        # Convert all values to int
+        self.draws = list(map(int, inputs.pop(0).split(",")))
+        self.boards = np.array(
+            [
+                [list(map(int, value.split())) for value in line.split("\n")]
+                for line in inputs
+            ],
+            dtype=object,
+        )
+
+    def solve(self, squid_rules: bool = False) -> int:
+        game = self.boards.copy()
+        winning_scores = []
+        for draw in self._draw():
+            game[game == draw] = np.nan
+
+            for index, board in enumerate(game):
+                if self._is_solved(board):
+                    winning_scores.append(self._calculate_score(board, draw))
+
+                    try:
+                        game = np.delete(game, index, axis=0)
+                    except IndexError:  # I don't understand why this is needed
+                                        # Need to rework the whole class
+                                        # Wrong way to go about this
+                        pass
+
+            if len(winning_scores) == np.size(self.boards, axis=0):
+                break
+
+        index = -1 if squid_rules else 0
+        return winning_scores[index]
+
+    def _draw(self) -> int:
+        for draw in self.draws:
+            yield draw
+
+    @staticmethod
+    def _calculate_score(solved_board, last_draw: int) -> int:
+        board_score = np.nansum(solved_board)
+        return board_score * last_draw
+
+    @staticmethod
+    def _is_solved(board) -> bool:
+        # Solved if any row or column is 0
+        cols = np.nansum(board, axis=0)
+        rows = np.nansum(board, axis=1)
+        return (np.count_nonzero(cols) != cols.size or np.count_nonzero(rows) != rows.size)
+
+
+def part1(inputs: str) -> int:
+    game = Bingo(inputs)
+    return game.solve()
+
+
+def part2(inputs: str) -> int:
+    game = Bingo(inputs)
+    return game.solve(squid_rules=True)
+
+
+def solve(path: str) -> Tuple[int, int]:
+    """Solve the puzzle"""
+    inputs = pathlib.Path(path).read_text().strip()
+    part1_result = part1(inputs)
+    part2_result = part2(inputs)
+
+    return part1_result, part2_result
+
+
+def main() -> None:
+    for path in sys.argv[1:]:
+        print(f"Input File: {path}")
+
+        part1_result, part2_result = solve(path)
+
+        print(f"Part 1 Result: {part1_result}")
+        print(f"Part 2 Result: {part2_result}")
+
+
+if __name__ == "__main__":
+    main()
diff --git a/puzzles/test_day04.py b/puzzles/test_day04.py
new file mode 100644
index 0000000..64e10d9
--- /dev/null
+++ b/puzzles/test_day04.py
@@ -0,0 +1,32 @@
+import pathlib
+import pytest
+import day04 as aoc
+
+INPUTS_DIR = f"{pathlib.Path(__file__).parent.parent}/inputs"
+
+@pytest.fixture
+def example_data():
+    input_path = f"{INPUTS_DIR}/day04-example.txt"
+    return pathlib.Path(input_path).read_text().strip()
+
+
+@pytest.fixture
+def day04_data():
+    input_path = f"{INPUTS_DIR}/day04.txt"
+    return pathlib.Path(input_path).read_text().strip()
+
+
+def test_example1(example_data):
+    assert aoc.part1(example_data) == 4512
+
+
+def test_example2(example_data):
+    assert aoc.part2(example_data) == 1924
+
+
+def test_part1(day04_data):
+    assert aoc.part1(day04_data) == 25023
+
+
+def test_part2(day04_data):
+    assert aoc.part2(day04_data) == 2634