Part 1: Find first bingo winner.

Given 5x5 bingo boards and a sequence of numbers, find the first winner. It’s simplified to only count straight horizontal/vertical bingos.

This is perfect for Numpy, as we can do all the updates and checks with simple expressions. Again, see Python Is Slow for why this is good.

aoc = __import__('aoc-common').aoc()
import numpy as np

text = aoc.input_text(4)

_numbers, _boards = text.split(maxsplit=1)
numbers = np.array(_numbers.split(','), 'i4')
boards = np.array(_boards.split(), 'i4').reshape(-1,5,5)

from enum import Enum

class Orientation(Enum):
  COLS = 2
  ROWS = 1

def mark_bingos(boards, dir):
  """Translate an array of boards into an array that is nonzero if the board has a
  bingo in the given direction.

  return (boards.sum(dir.value) == -boards.shape[dir.value]).sum(1)

def score_board(board, n):
  board[board == -1] = 0  # reset the temporary sentinel markers.
  return board.sum() * n

def find_winner(B, numbers):
  """Iteratively marks off the numbers and returns the first winner. Modifies the boards."""
  for n in numbers:
    B[B == n] = -1  # uses -1 because apparently 0 is a valid bingo number.
    if len(wins := mark_bingos(B, Orientation.COLS).nonzero()[0]):
      return score_board(B[wins[0]], n)
    if len(wins := mark_bingos(B, Orientation.ROWS).nonzero()[0]):
      return score_board(B[wins[0]], n)

score = find_winner(boards, numbers)

print(f'answer to part 1: {score}')

Note also that again the question of “orientation” or “direction” is somewhat awkward to generalize over in Python (as any imperative language) and invariably leads to pseudo-repetitive code (unless we resort to ugly hacks).

Part 2: Find the losing board.

def find_loser(B, numbers):
  """Iteratively marks off the numbers and returns the last winner. Modifies the boards."""
  for i,n in enumerate(numbers):
    if len(B) <= 1:
      return find_winner(B, numbers[i:])
    B[B == n] = -1

    nonwins = (mark_bingos(B, Orientation.COLS) == 0).nonzero()[0]
    B = B[nonwins]

    nonwins = (mark_bingos(B, Orientation.ROWS) == 0).nonzero()[0]
    B = B[nonwins]

score = find_loser(boards, numbers)

print(f'answer to part 2: {score}')