library(tidyverse)
library(here)
path_data <- here("2021/inputs/04-input.txt")
input <- tibble(x = read_lines(path_data))
In this one we’re playing bingo with a giant squid, and we’re going to do it with dataframes! Let’s go…
We need to essentially play bingo, find the first board that wins, and then do some fancy calculations on the winning board. So, we need to first get the boards organized. This turns out to be the toughest part.
The input provides us with both the list of numbers drawn and the bingo boards, so we need to separate those.
First the numbers to draw: we can take the first row of input and split it up and convert into numbers. There: a nice vector of drawn numbers.
# get vector of the drawing numbers
draw_numbers <- input[[1, "x"]] %>% str_split(pattern = ",")
draw_numbers <- as.numeric(draw_numbers[[1]])
draw_numbers
## [1] 84 28 29 75 58 71 26 6 73 74 41 39 87 37 16 79 55 60 62 80 64 95 46 15 5 47 2 35 32 78 89
## [32] 90 96 33 4 69 42 30 54 85 65 83 44 63 20 17 66 81 67 77 36 68 82 93 10 25 9 34 24 72 91 88
## [63] 11 38 3 45 14 56 22 61 97 27 12 48 18 1 31 98 86 19 99 92 8 43 52 23 21 0 7 50 57 70 49
## [94] 13 51 40 76 94 53 59
Now we can tackle the boards. We will:
# let's convert the boards into vectors and define tests for each type
# just the board input section, no delimiters
raw <-
input %>%
slice(3:nrow(.)) %>%
filter(x != "")
# df of raw board strings
raw_boards <- raw %>%
# count boards
mutate(board = (row_number() - 1) %/% 5) %>%
# for each board
group_by(board) %>%
# make each number two chars
mutate(board_string = paste(x, collapse = " ")) %>%
mutate(board_string = str_replace_all(board_string, " ", " 0"),
board_string = str_replace_all(board_string, "^ ([0-9])", "\\1")) %>%
# unique board strings
select(-x) %>%
distinct()
raw_boards
## # A tibble: 100 × 2
## # Groups: board [100]
## board board_string
## <dbl> <chr>
## 1 0 31 93 46 11 30 02 45 40 69 33 82 21 37 99 86 57 16 34 94 85 60 49 28 14 65
## 2 1 96 02 20 41 24 29 15 27 83 48 07 93 99 82 26 03 91 66 35 85 62 78 67 04 22
## 3 2 10 87 50 84 40 78 05 17 59 44 38 88 15 46 32 08 72 74 90 23 64 93 49 39 20
## 4 3 25 41 32 30 39 06 66 38 95 05 31 13 56 67 34 69 18 64 44 96 75 14 88 97 40
## 5 4 39 62 50 10 68 18 07 95 72 82 83 23 19 70 71 11 64 30 08 03 06 81 27 34 99
## 6 5 40 52 66 20 49 93 74 16 35 29 97 88 06 98 81 62 55 99 47 12 83 76 57 75 22
## 7 6 52 76 43 86 99 58 26 61 36 42 11 69 65 03 49 33 07 71 08 25 50 82 32 16 64
## 8 7 45 38 88 96 08 22 17 05 60 66 87 12 61 59 02 00 37 18 15 98 07 62 23 56 92
## 9 8 20 07 12 26 69 81 63 89 57 19 18 44 61 64 53 47 27 08 30 00 60 99 28 06 96
## 10 9 70 50 63 56 26 55 97 65 05 96 72 68 29 91 61 34 00 14 28 04 45 53 78 80 47
## # … with 90 more rows
Now we can convert these strings into a dataframe of boards. We want a tidy representation of them, meaning:
We can get there from our raw strings:
# df of all boards
boards <- raw_boards %>%
# split up the board strings
separate(board_string, sep = " ", into = as.character(seq(1, 25))) %>%
# pivot, baby!
pivot_longer(cols = -board, names_to = "place", values_to = "num") %>%
# tough to parse with all the parens, but a lovely trick!
mutate(across(where(is.character), as.numeric)) %>%
# zero index number places
mutate(place = place - 1)
boards
## # A tibble: 2,500 × 3
## # Groups: board [100]
## board place num
## <dbl> <dbl> <dbl>
## 1 0 0 31
## 2 0 1 93
## 3 0 2 46
## 4 0 3 11
## 5 0 4 30
## 6 0 5 2
## 7 0 6 45
## 8 0 7 40
## 9 0 8 69
## 10 0 9 33
## # … with 2,490 more rows
We now have a dataframe of all boards, with every number and where they go. Sweeeeet. Let’s play bingo!
Hang on…
Here’s what our board layout looks like:
# what a board looks like, numbers are places
test_board <- matrix(0:24, nrow = 5, byrow = TRUE)
test_board
## [,1] [,2] [,3] [,4] [,5]
## [1,] 0 1 2 3 4
## [2,] 5 6 7 8 9
## [3,] 10 11 12 13 14
## [4,] 15 16 17 18 19
## [5,] 20 21 22 23 24
To check for a bingo, we need to track whether a number is marked and whether someone has won. First, we should add to our boards
dataframe to specify which row and column each number is a part of. Which means…math shows up!
From the test board above, we can use the place values to define what each row and column are:
And we will set all of the boards to have no marks. Our bingo game is now setup.
# setup bingo df
bingo <- boards %>%
mutate(row = place %/% 5,
col = place %% 5,
marked = FALSE)
bingo
## # A tibble: 2,500 × 6
## # Groups: board [100]
## board place num row col marked
## <dbl> <dbl> <dbl> <dbl> <dbl> <lgl>
## 1 0 0 31 0 0 FALSE
## 2 0 1 93 0 1 FALSE
## 3 0 2 46 0 2 FALSE
## 4 0 3 11 0 3 FALSE
## 5 0 4 30 0 4 FALSE
## 6 0 5 2 1 0 FALSE
## 7 0 6 45 1 1 FALSE
## 8 0 7 40 1 2 FALSE
## 9 0 8 69 1 3 FALSE
## 10 0 9 33 1 4 FALSE
## # … with 2,490 more rows
Now we need to take our bingo setup and play! But we will need a way to check if anyone has won. To do this, we can group by each board and count the number of marked values on each board, for all rows and all columns. If any total ever hits five, we know someone has won.
get_totals <- function(df) {
# total for the rows
rows <- df %>%
group_by(board, row) %>%
summarise(total = sum(marked)) %>%
ungroup() %>%
select(-row) %>%
mutate(type = "row")
# total for the columns
cols <- df %>%
group_by(board, col) %>%
summarise(total = sum(marked)) %>%
ungroup() %>%
select(-col) %>%
mutate(type = "col")
# combine
totals <- bind_rows(rows, cols) %>%
select(board, type, total)
return(totals)
}
To play the game, we can loop through a selection of numbers (our “basket” of numbers), updating our bingo dataframe each time. Once all numbers are drawn, we can find use the results to find the totals and see the status of the game.
play_bingo <- function(number_basket) {
# start fresh
curr_bingo <- bingo
# if you have the number, mark it, otherwise leave it
for (draw_num in number_basket) {
curr_bingo <- curr_bingo %>%
mutate(marked = ifelse(num == draw_num, TRUE, marked))
}
return(curr_bingo)
}
Phew, okay. Let’s finally play some bingo!
We will keep playing bingo with more and more numbers till someone wins (we see a five in our totals)
# an example of playing bingo, no one has won yet (no 5s under total)
play_bingo(draw_numbers[1:12]) %>%
get_totals() %>%
arrange(desc(total))
## # A tibble: 1,000 × 3
## board type total
## <dbl> <chr> <int>
## 1 59 row 4
## 2 69 row 4
## 3 16 row 3
## 4 21 row 3
## 5 35 row 3
## 6 42 row 3
## 7 64 row 3
## 8 36 col 3
## 9 68 col 3
## 10 82 col 3
## # … with 990 more rows
But as soon we draw the 23rd number…BINGO!
# congrats, you won!
play_bingo(draw_numbers[1:23]) %>%
get_totals() %>%
arrange(desc(total)) %>%
head(3)
## # A tibble: 3 × 3
## board type total
## <dbl> <chr> <int>
## 1 79 col 5
## 2 27 row 4
## 3 35 row 4
And it’s board 79! Let’s tally their score!
Let’s get the results of board 79 after they have won:
# winning board is 79
winning_board_1 <- play_bingo(draw_numbers[1:23]) %>%
filter(board == 79)
To get the score, we:
marked == FALSE
)# calculating score
# sum of all unmarked numbers
winning_unmarked_sum_1 <- winning_board_1 %>%
filter(!marked) %>%
summarise(total = sum(num)) %>%
pull(total)
# number that was just called
just_called_1 <- draw_numbers[23]
# final score
winning_unmarked_sum_1 * just_called_1
## [1] 29440
Well played! But what if we play the long game?
Now we want to know the board that is the last to win. Same idea, but we will add a filter to just look for the boards that haven’t won yet. As soon as we are left with just one board (in this case, ten rows of totals), we have the last player.
# an example of playing bingo, checking for players still in the game
# Notice the dimensions: we will fiddle till we only have ten rows (one board)
play_bingo(draw_numbers[1:60]) %>%
get_totals() %>%
arrange(desc(total)) %>%
group_by(board) %>%
filter(all(total < 5))
## # A tibble: 540 × 3
## # Groups: board [54]
## board type total
## <dbl> <chr> <int>
## 1 2 row 4
## 2 2 row 4
## 3 4 row 4
## 4 6 row 4
## 5 6 row 4
## 6 7 row 4
## 7 8 row 4
## 8 9 row 4
## 9 10 row 4
## 10 11 row 4
## # … with 530 more rows
And after the 83rd number is drawn, there is only one player remaining:
# earliest number drawn to return only one board
play_bingo(draw_numbers[1:83]) %>%
get_totals() %>%
arrange(desc(total)) %>%
group_by(board) %>%
filter(all(total < 5))
## # A tibble: 10 × 3
## # Groups: board [1]
## board type total
## <dbl> <chr> <int>
## 1 32 row 4
## 2 32 row 4
## 3 32 row 4
## 4 32 col 4
## 5 32 col 4
## 6 32 col 4
## 7 32 row 3
## 8 32 row 3
## 9 32 col 3
## 10 32 col 3
So board 32 is the last one still in the game. Let’s let them win already! With the 84th number they are still playing, but once we draw the 85th number, they finally get a bingo. So now we take their winning board:
# get 32's winning board
winning_board_2 <- play_bingo(draw_numbers[1:85]) %>%
filter(board == 32)
winning_board_2
## # A tibble: 25 × 6
## # Groups: board [1]
## board place num row col marked
## <dbl> <dbl> <dbl> <dbl> <dbl> <lgl>
## 1 32 0 52 0 0 TRUE
## 2 32 1 35 0 1 TRUE
## 3 32 2 43 0 2 TRUE
## 4 32 3 77 0 3 TRUE
## 5 32 4 79 0 4 TRUE
## 6 32 5 53 1 0 FALSE
## 7 32 6 56 1 1 TRUE
## 8 32 7 93 1 2 TRUE
## 9 32 8 92 1 3 TRUE
## 10 32 9 12 1 4 TRUE
## # … with 15 more rows
And figure out their score:
# calculate score
# sum of all unmarked numbers
winning_unmarked_sum_2 <- winning_board_2 %>%
filter(!marked) %>%
summarise(total = sum(num)) %>%
pull(total)
# number that was just called
just_called_2 <- draw_numbers[85]
# final score
winning_unmarked_sum_2 * just_called_2
## [1] 13884
And there we go! We have played bingo with a giant squid!
🦑
Phew! That was a lot. Hope you learned something!
How would you do it? What’s your shortcut? Please share!
Till next time!