The problem today is a bit of a breather. (Puzzles are easier on weekdays.) Still, I fell right into their trap! But it’s alright. In fact, I really like this puzzle, because I had tons of fun toying with its visualization!
Part 1
Show challenge - day 6, part 1
The sea floor is getting steeper. Maybe the sleigh keys got carried this way?
A massive school of glowing lanternfish swims past. They must spawn quickly to reach such large numbers - maybe exponentially quickly? You should model their growth rate to be sure.
Although you know nothing about this specific species of lanternfish, you make some guesses about their attributes. Surely, each lanternfish creates a new lanternfish once every 7 days.
However, this process isn’t necessarily synchronized between every lanternfish - one lanternfish might have 2 days left until it creates another lanternfish, while another might have 4. So, you can model each fish as a single number that represents the number of days until it creates a new lanternfish.
Furthermore, you reason, a new lanternfish would surely need slightly longer before it’s capable of producing more lanternfish: two more days for its first cycle.
So, suppose you have a lanternfish with an internal timer value of 3
:
- After one day, its internal timer would become
2
. - After another day, its internal timer would become
1
. - After another day, its internal timer would become
0
. - After another day, its internal timer would reset to
6
, and it would create a new lanternfish with an internal timer of8
. - After another day, the first lanternfish would have an internal timer of
5
, and the second lanternfish would have an internal timer of7
.
A lanternfish that creates a new fish resets its timer to 6
, not 7
(because 0
is included as a valid timer value).
The new lanternfish starts with an internal timer of 8
and does not start counting down until the next day.
Realizing what you’re trying to do, the submarine automatically produces a list of the ages of several hundred nearby lanternfish (your puzzle input). For example, suppose you were given the following list:
3,4,3,1,2
This list means that the first fish has an internal timer of 3
, the second fish has an internal timer of 4
, and so on until the fifth fish, which has an internal timer of 2
.
Simulating these fish over several days would proceed as follows:
Initial state: 3,4,3,1,2
After 1 day: 2,3,2,0,1
After 2 days: 1,2,1,6,0,8
After 3 days: 0,1,0,5,6,7,8
After 4 days: 6,0,6,4,5,6,7,8,8
After 5 days: 5,6,5,3,4,5,6,7,7,8
After 6 days: 4,5,4,2,3,4,5,6,6,7
After 7 days: 3,4,3,1,2,3,4,5,5,6
After 8 days: 2,3,2,0,1,2,3,4,4,5
After 9 days: 1,2,1,6,0,1,2,3,3,4,8
After 10 days: 0,1,0,5,6,0,1,2,2,3,7,8
After 11 days: 6,0,6,4,5,6,0,1,1,2,6,7,8,8,8
After 12 days: 5,6,5,3,4,5,6,0,0,1,5,6,7,7,7,8,8
After 13 days: 4,5,4,2,3,4,5,6,6,0,4,5,6,6,6,7,7,8,8
After 14 days: 3,4,3,1,2,3,4,5,5,6,3,4,5,5,5,6,6,7,7,8
After 15 days: 2,3,2,0,1,2,3,4,4,5,2,3,4,4,4,5,5,6,6,7
After 16 days: 1,2,1,6,0,1,2,3,3,4,1,2,3,3,3,4,4,5,5,6,8
After 17 days: 0,1,0,5,6,0,1,2,2,3,0,1,2,2,2,3,3,4,4,5,7,8
After 18 days: 6,0,6,4,5,6,0,1,1,2,6,0,1,1,1,2,2,3,3,4,6,7,8,8,8,8
Each day, a 0
becomes a 6
and adds a new 8
to the end of the list, while each other number decreases by 1
if it was present at the start of the day.
In this example, after 18 days, there are a total of 26
fish. After 80
days, there would be a total of 5934
.
Find a way to simulate lanternfish. How many lanternfish would there be after 80 days?
Here’s my provided input. It’s quite long so don’t try to scroll through:
Show input - day 6
1,4,1,1,1,1,5,1,1,5,1,4,2,5,1,2,3,1,1,1,1,5,4,2,1,1,3,1,1,1,1,1,1,1,2,1,1,1,1,1,5,1,1,1,1,1,1,1,1,1,4,1,1,1,1,5,1,4,1,1,4,1,1,1,1,4,1,1,5,5,1,1,1,4,1,1,1,1,1,3,2,1,1,1,1,1,2,3,1,1,2,1,1,1,3,1,1,1,2,1,2,1,1,2,1,1,3,1,1,1,3,3,5,1,4,1,1,5,1,1,4,1,5,3,3,5,1,1,1,4,1,1,1,1,1,1,5,5,1,1,4,1,2,1,1,1,1,2,2,2,1,1,2,2,4,1,1,1,1,3,1,2,3,4,1,1,1,4,4,1,1,1,1,1,1,1,4,2,5,2,1,1,4,1,1,5,1,1,5,1,5,5,1,3,5,1,1,5,1,1,2,2,1,1,1,1,1,1,1,4,3,1,1,4,1,4,1,1,1,1,4,1,4,4,4,3,1,1,3,2,1,1,1,1,1,1,1,4,1,3,1,1,1,1,1,1,1,5,2,4,2,1,4,4,1,5,1,1,3,1,3,1,1,1,1,1,4,2,3,2,1,1,2,1,5,2,1,1,4,1,4,1,1,1,4,4,1,1,1,1,1,1,4,1,1,1,2,1,1,2
Here’s the solution to part 1:
mutable struct LanternFishes
timer::Vector{Int}
end
function spawn_fish!(f::LanternFishes, n)
- append!(f.timer, repeat([8], n))
end
function day_forward!(fishes::LanternFishes)
fishes.timer .-= 1
spawn_fish!(fishes, sum(fishes.timer .< 0))
fishes.timer[fishes.timer .< 0] .= 6
end
fishes = parse.(Int, split(strip(input), ",")) |> LanternFishes
for day in 1:80
day_forward!(fishes)
end
length(fishes.timer) |> print
Part 2
Show challenge - day 6, part 2
Suppose the lanternfish live forever and have unlimited food and space. Would they take over the entire ocean?
After 256 days in the example above, there would be a total of 26984457539
lanternfish!
How many lanternfish would there be after 256 days?
On the surface, part 2 is just part 1 with extra for
loops.
But when my program took ages to run, and I realized I’ve taken the wrong approach.
The number of fishes grows exponentially, so my computer just cannot process them in a sensible time.
This is where you have to create a counting list, which tells us how many fishes are there for each timer count.
mutable struct LanternFishes
timer::Vector{Int64}
end
function day_forward!(f::LanternFishes)
n_new = f.timer[1]
f.timer = [f.timer[2:9]; n_new]
f.timer[7] += n_new
end
fishes = parse.(Int, split(strip(input), ","))
fishes = 0:8 .|> (x -> count(==(x), fishes)) |> LanternFishes
for day in 1:256
day_forward!(fishes)
end
sum(fishes.timer) |> print
Bonus: Boids simulation
I’ve been looking for good oppotunity for visualization, and today’s puzzle brought it to me. These kinds of exponential growth organism is perfectly suitable for a Boids simulation! So I took the chance and went for it. Here’s the results:
The code is below.
It’s a bit long and I’m not going through it, but you can tweak around.
The developers for Agents.jl
did a really good job.
using Agents
using LinearAlgebra
mutable struct Fish <: AbstractAgent
id::Int # |-> required for all agents
pos::NTuple{2,Float64} # |
vel::NTuple{2,Float64} # -> required for moving agents in ContinousSpeed
speed::Float64 # -> how far agents travel by vel per steps
cohere_factor::Float64 # -> maintaining the average position of neighbors
separation::Float64 # -> minimum distance from its neighbors
separate_factor::Float64 # -> iportance of maintaining ^separation
match_factor::Float64 # -> importance of matching the avg trajectory of neighbors
visual_distance::Float64 # -> distance agents can see
timer::Float64 # -> timer for spawn
end
function initialize_model(;
n_fishs = 300,
speed = 0.75,
cohere_factor = 0.25,
separation = 4.0,
separate_factor = 1,
match_factor = 0.125,
visual_distance = 5.0,
extent = (160, 90),
spacing = visual_distance / 1.5,
timers = input
)
space2d = ContinuousSpace(extent, spacing)
model = ABM(Fish, space2d, scheduler = Schedulers.randomly)
for i in 1:n_fishs
vel = Tuple(rand(model.rng, 2) * 2 .- 1)
timer = timers[i]
add_agent!(
model,
vel,
speed,
cohere_factor,
separation,
separate_factor,
match_factor,
visual_distance,
timer,
)
end
return model
end
function agent_step!(fish, model)
# Obtain the ids of neighbors within the fish's visual distance
neighbor_ids = nearby_ids(fish, model, fish.visual_distance)
N = 0
match = separate = cohere = (0.0, 0.0)
# Calculate behaviour properties based on neighbors
for id in neighbor_ids
N += 1
neighbor = model[id].pos
heading = neighbor .- fish.pos
# `cohere` computes the average position of neighboring fishs
cohere = cohere .+ heading
if edistance(fish.pos, neighbor, model) < fish.separation
# `separate` repels the fish away from neighboring fishs
separate = separate .- heading
end
# `match` computes the average trajectory of neighboring fishs
match = match .+ model[id].vel
end
N = max(N, 1)
# Normalise results based on model input and neighbor count
cohere = cohere ./ N .* fish.cohere_factor
separate = separate ./ N .* fish.separate_factor
match = match ./ N .* fish.match_factor
# Compute velocity based on rules defined above
fish.vel = (fish.vel .+ cohere .+ separate .+ match) ./ 2
fish.vel = fish.vel ./ norm(fish.vel)
# Keep fishes in bounds
if fish.pos[1] >= 155
fish.vel = fish.vel .- (0.5, 0.0)
end
if fish.pos[1] <= 5
fish.vel = fish.vel .+ (0.5, 0.0)
end
if fish.pos[2] >= 85
fish.vel = fish.vel .- (0.0, 0.5)
end
if fish.pos[2] <= 5
fish.vel = fish.vel .+ (0.0, 0.5)
end
# Move fish according to new velocity and speed
# Fishes move slow when they're newborn
move_agent!(fish, model, fish.timer <= 6 ? fish.speed : (8-fish.timer)*fish.speed/6)
# Decrease timer and spawn new fish
if fish.timer <= 0.0
fish.timer = 6.0
add_agent!(
fish.pos,
model,
Tuple(rand(model.rng, 2) * 2 .- 1),
fish.speed,
fish.cohere_factor,
fish.separation,
fish.separate_factor,
fish.match_factor,
fish.visual_distance,
8.0,
)
end
fish.timer -= 0.1
end
## Plotting the flock
using InteractiveDynamics
using CairoMakie
using ColorSchemes
using Colors
const fish_polygon = Polygon(Point2f0[(-0.5, -0.5), (1, 0), (-0.5, 0.5)])
function fish_marker(b::Fish)
φ = atan(b.vel[2], b.vel[1]) #+ π/2 + π
scale(rotate2D(fish_polygon, φ),
(b.timer <= 6 ? 1 : (8-b.timer)/6)*1 + 1)
end
color_vector = cgrad(:rainbow, 45, categorical=true, rev=true) .|> hex
color_vector = ["#" * c[3:end] for c in color_vector]
agent_color(f) = color_vector[abs(Int(round(f.timer * 5))) + 1]
input = "3,4,3,1,2"
input = parse.(Float64, split(strip(input), ","))
model = initialize_model(n_fishs=length(input),
speed=1, visual_distance=5.0, separation=4.0,
spacing= 10/3,
match_factor=0.1, cohere_factor=0.05, separate_factor=0.25)
abm_video(
"./Desktop/flocking.mp4", model, agent_step!;
am = fish_marker, ac = agent_color,
framerate = 24, frames = 800,
resolution = (1920, 1080),
title = "Lantern Fishes")