Files
ocaml-aoc/bin/day2406.ml

81 lines
3.2 KiB
OCaml

(** [find_start strs] returns the location [(x, y)] of the starting position. *)
let find_start map =
match Aoc.Grid.idx_from_opt map 0 '^' with
| Some i -> Aoc.Grid.pos_of_idx map i
| None -> failwith "find_start"
(** [read_file fname] reads the input map from [fname]. It returns a
[(map, pos, vel)] tuple, consisting of the obsticle map, initial position,
and initial velocity. *)
let read_file fname =
let map = Aoc.Grid.of_file fname in
let pos = find_start map in
(map, pos, (0, -1))
(** [is_block map pos] returns [true] iff the location [pos] is a blockages in
[map]. *)
let is_block map pos =
if Aoc.Grid.pos_is_valid map pos then Aoc.Grid.get_by_pos map pos = '#'
else false
(** [insert_block map pos] inserts a blockage at [pos] into [map] and returns
the new map. *)
let insert_block map pos = Aoc.Grid.update_pos map pos '#'
(** [move map (pos, vel)] moves [pos] one step forward on the [map]. [vel] gives
the movement vector. If the movement will cause an obstacle to be hit then
[vel] is rotated right by 90 degrees and we move in that direction. Returns
the updated [(pos, vel)] pair. *)
let rec move map ((x, y), (dx, dy)) =
let x', y' = (x + dx, y + dy) in
if is_block map (x', y') then move map ((x, y), (-dy, dx))
else ((x', y'), (dx, dy))
(** [walk_map map (pos, vel)] walks around [map] starting at [pos] moving in the
direction [vel]. It returns a list of all positions visited before falling
off one of the sides. *)
let walk_map map (pos, vel) =
let rec impl acc (pos, vel) =
if Aoc.Grid.pos_is_valid map pos then
impl (pos :: acc) (move map (pos, vel))
else acc
in
impl [] (pos, vel)
(** [has_cycles map (pos, vel)] returns true if walking around [map] starting at
[pos] going in [vel] direction will end up in a never ending cycle.*)
let has_cycles map start =
(* We detect a cycle by walking two 'agents' around the map from the same
starting position. Agent 1 moves 1 step at a time, agent 2 moves 2. If
the agents ever end up on the same square facing the same direction we have
a cycle. This works even if the cycle doesn't start immediately. *)
let rec impl agent1 ((pos', _) as agent2) =
(* Only need to check pos' for validity because if pos is not valid then
pos' must also be invalid, and have been invalid before this. *)
if not (Aoc.Grid.pos_is_valid map pos') then false
else if agent1 = agent2 then true
else impl (move map agent1) (move map (move map agent2))
in
(* Start Agent 2 a step ahead of Agent 1 so we don't fail at the start
position. *)
impl start (move map start)
(** [walk_block map (pos, vel) bpos] adds a block to the map [map] at [bpos] and
then sees if walking the map starting with [(pos, vel)] has a cycle. *)
let walk_block map (pos, vel) bpos =
if bpos = pos then false
else
let map' = insert_block map bpos in
has_cycles map' (pos, vel)
let part1 (map, pos, vel) =
walk_map map (pos, vel) |> List.sort_uniq Aoc.IntPair.compare |> List.length
let part2 (map, pos, vel) =
walk_map map (pos, vel)
|> List.sort_uniq Aoc.IntPair.compare
|> List.filter (walk_block map (pos, vel))
|> List.length
let _ = Aoc.main read_file [ (string_of_int, part1); (string_of_int, part2) ]