this post was submitted on 16 Nov 2025
5 points (85.7% liked)

Advent Of Code

1199 readers
15 users here now

An unofficial home for the advent of code community on programming.dev! Other challenges are also welcome!

Advent of Code is an annual Advent calendar of small programming puzzles for a variety of skill sets and skill levels that can be solved in any programming language you like.

Everybody Codes is another collection of programming puzzles with seasonal events.

EC 2025

AoC 2025

Solution Threads

M T W T F S S
1 2 3 4 5 6 7
8 9 10 11 12

Visualisations Megathread

Rules/Guidelines

Relevant Communities

Relevant Links

Credits

Icon base by Lorc under CC BY 3.0 with modifications to add a gradient

console.log('Hello World')

founded 2 years ago
MODERATORS
 

Quest 10: Feast on the Board

  • Keep top level comments as only solutions, if you want to say something other than a solution put it in a new post. (replies to comments can be whatever)
  • You can send code in code blocks by using three backticks, the code, and then three backticks or use something such as https://topaz.github.io/paste/ if you prefer sending it through a URL

Link to participate: https://everybody.codes/

you are viewing a single comment's thread
view the rest of the comments
[โ€“] Pyro@programming.dev 1 points 4 weeks ago (1 children)

Python

Took me quite a while to figure out. Did part 3 using DFS initially, then optimized to use memoization.

# queue for BFS
from collections import deque

# possible moves for the dragon (knight-like moves)
DRAGON_MOVES = [(2, 1), (2, -1), (-2, 1), (-2, -1), (1, 2), (1, -2), (-1, 2), (-1, -2)]

# yields all possible moves for dragon at (i, j)
def get_next_moves(i, j, m, n):
    for dx, dy in DRAGON_MOVES:
        n_i, n_j = i + dx, j + dy
        if 0 <= n_i < m and 0 <= n_j < n:
            yield n_i, n_j

# BFS for given number of moves
def part1(data: str, moves: int = 4):
    board = [list(row) for row in data.splitlines()]
    m, n = len(board), len(board[0])

    # initialize queue to dragon position
    queue = deque()
    for i in range(m):
        for j in range(n):
            if board[i][j] == 'D':
                queue.append((i, j))

    eaten = 0
    visited = set()
    # perform BFS for the given number of moves
    # we do moves+1 because we only process a move after popping from queue
    for _ in range(moves+1):
        nq = len(queue)
        for _ in range(nq):
            i, j = queue.popleft()
            if (i, j) in visited:
                continue
            visited.add((i, j))

            # eat sheep if present
            if board[i][j] == 'S':
                eaten += 1

            # add unvisited next moves to queue
            for n_i, n_j in get_next_moves(i, j, m, n):
                if (n_i, n_j) in visited:
                    continue
                queue.append((n_i, n_j))

    return eaten

# <assert snipped>

def part2(data: str, rounds: int = 20):
    board = [list(row) for row in data.splitlines()]
    m, n = len(board), len(board[0])

    # initialize list to initial dragon location
    dragon_locs = []
    for i in range(m):
        for j in range(n):
            if board[i][j] == 'D':
                dragon_locs.append((i, j))

    eaten = 0
    # simulate for given rounds
    for _ in range(rounds):
        # move dragon
        # calculate all possible new positions for the dragon
        dragon_moved_to = set()
        for i, j in dragon_locs:
            for n_i, n_j in get_next_moves(i, j, m, n):
                dragon_moved_to.add((n_i, n_j))
        # update dragon locations and eat any sheep present
        dragon_locs = list(dragon_moved_to)
        for i, j in dragon_locs:
            if board[i][j] == 'S':
                eaten += 1
                board[i][j] = '.'  # sheep is eaten

        # move sheep
        # process from bottom to top to avoid overlaps
        for i in range(m-1, -1, -1):
            for j in range(n):
                if 'S' not in board[i][j]:
                    # no sheep here
                    continue
                # move sheep out of current position
                board[i][j] = board[i][j].replace('S', '')
                if i == m-1:
                    # sheep escapes
                    continue
                if board[i+1][j] == '#':
                    # sheep moves into hiding
                    board[i+1][j] = 'S#'
                    continue
                if (i+1, j) in dragon_moved_to:
                    # sheep runs into dragon
                    eaten += 1
                    continue
                # sheep moves down
                board[i+1][j] = 'S'
            
    return eaten

# <assert snipped>

# DP with memoization
def part3(data: str):
    board_lines = data.splitlines()
    m, n = len(board_lines), len(board_lines[0])
    
    hideouts = set()            # positions of hideouts
    initial_sheep = [None] * n  # initial positions of sheep in each column
    dragon_loc = None           # initial position of dragon
    
    # iterate through board to find hideouts, sheep, and dragon
    for i in range(m):
        for j in range(n):
            char = board_lines[i][j]
            if char == '#':
                hideouts.add((i, j))
            elif char == 'D':
                dragon_loc = (i, j)
            elif char == 'S':
                initial_sheep[j] = i
    
    # convert to tuple for immutability and hashability
    initial_sheep = tuple(initial_sheep)

    # optional: calculate lost positions for each column
    # lost_positions[j] is the row index where if the sheep reaches, can escape or stay hidden until escape (game over)
    # used to quickly prune unwinnable states
    lost_positions = [m] * n
    for j in range(n):
        for i in range(m-1, -1, -1):
            if (i, j) not in hideouts:
                break
            lost_positions[j] = i

    # memoization of state to number of paths
    # state is a tuple of (player, dragon_loc, sheep_positions)
    memo = {}

    # do sheeps' turn
    def solve_sheep(dragon_loc: tuple[int, int], sheep_positions: tuple[int|None, ...]) -> int:
        # check for memoized result
        state = ('S', dragon_loc, sheep_positions)
        if state in memo:
            return memo[state]
        
        total_paths = 0
        # used to check if any sheep can move
        # this helps determine if the dragon gets another turn
        can_move = False
        
        for j, i in enumerate(sheep_positions):
            if i is None:
                # no sheep in this column
                continue
            
            next_i = i + 1
            
            # Check collision with dragon
            if (next_i, j) == dragon_loc and (next_i, j) not in hideouts:
                # smart sheep avoids dragon
                continue
            
            # Check escape (entering safe zone)
            if next_i == lost_positions[j]:
                # this is a valid move, but its game over so we skip exploring further
                can_move = True
                continue
            
            # this sheep will move down
            can_move = True
            # create new state with sheep moved down
            new_sheep = list(sheep_positions)
            new_sheep[j] = next_i

            # continue solving for dragon's turn
            total_paths += solve_dragon(dragon_loc, tuple(new_sheep))
        
        # if no sheep could move, dragon gets another turn
        if not can_move:
            total_paths += solve_dragon(dragon_loc, sheep_positions)
        
        # memoize result before returning
        memo[state] = total_paths
        return total_paths

    # do dragon's turn
    def solve_dragon(dragon_loc, sheep_positions):
        # check for memoized result
        state = ('D', dragon_loc, sheep_positions)
        if state in memo:
            return memo[state]
        
        total_paths = 0
        i, j = dragon_loc
        
        # try all possible dragon moves
        for n_i, n_j in get_next_moves(i, j, m, n):
            # get where the sheep is in the column the dragon is moving to
            sheep_loc_in_col = sheep_positions[n_j]
            
            # if sheep is in the same row as the dragon and not in hideout, eat it
            if sheep_loc_in_col == n_i and (n_i, n_j) not in hideouts:
                sheep_count = sum(1 for x in sheep_positions if x is not None)
                if sheep_count == 1:
                    # all sheep eaten, end of path
                    total_paths += 1
                else:
                    # create new state with sheep eaten
                    new_sheep = list(sheep_positions)
                    new_sheep[n_j] = None
                    # continue solving for sheep's turn
                    total_paths += solve_sheep((n_i, n_j), tuple(new_sheep))
            else:
                # Empty, hideout, or sheep hidden in hideout
                # continue solving for sheep's turn with dragon moved
                total_paths += solve_sheep((n_i, n_j), sheep_positions)

        # memoize result before returning
        memo[state] = total_paths
        return total_paths

    # start solving with sheep's turn
    return solve_sheep(dragon_loc, initial_sheep)

# <asserts snipped>
[โ€“] hades@programming.dev 2 points 4 weeks ago

good job! yeah it doesn't matter what you do if you need to scan the entire state space :)