diff --git a/2022/README.md b/2022/README.md
index cf6a50a44bb447019ceb05294436558b3539d541..8ca785acb2b8f5c76d679b80545213234ce68f34 100644
--- a/2022/README.md
+++ b/2022/README.md
@@ -617,11 +617,46 @@ The subproblems are
 
 I'm feeling a dynamic programming solution is in order.
 
+Ugh. The search space is big. My initial approach allows valves to be re-opened. 
+Instead of keying to boolean, maybe use a bit vector.
+This will require remembering which bit position corresponds to which valve.
+Unfortunately, that'll require too much space/time to calculate the full solution space.
+
+Perhaps instead we focus on getting all the valves (with non-zero flow) open as soon as possible.
+The particular order is affected by how much flow each contributes.
+Once they're all open, we can kill the remaining time doing whatever.
+
+(not yet complete)
+
 ### Part 2
 
 ...
 
 ## Day 17
 
+- [The problem](https://adventofcode.com/2022/day/17)
+- [The solution](src/main/java/edu/unl/cse/bohn/year2022/Day17.java)
+
+### Part 1
+
+The subproblems are:
+- Simulate the movements of the rock
+- Detect collisions
+
+### Part 2
+
+Processing a trillion rocks will require some changes.
+Besides changing a few `int`s to `long`s, we'll need to keep our memory use under control.
+
+We can do this by "throwing away" a few rows from the bottom of the tunnel whenever the tunnel is blocked.
+
+No. That solves the memory problem but not the time problem.
+Even if we could process 4 billion rocks per second (which we can't) then it would take a little over 4 minutes to
+process 1 trillion rocks, and AOC solutions shouldn't take that long.
+
+...
+
+## Day 18
+
 (coming soon)
 
diff --git a/2022/src/main/java/edu/unl/cse/bohn/year2022/Day16.java b/2022/src/main/java/edu/unl/cse/bohn/year2022/Day16.java
index 572eb1d0dd302684786a53bbba6047ec8274fedf..033ace36d54023a140ea6bd408b7a8e924a0ba91 100644
--- a/2022/src/main/java/edu/unl/cse/bohn/year2022/Day16.java
+++ b/2022/src/main/java/edu/unl/cse/bohn/year2022/Day16.java
@@ -3,11 +3,13 @@ package edu.unl.cse.bohn.year2022;
 import edu.unl.cse.bohn.Puzzle;
 
 import java.util.*;
+import java.util.stream.Collectors;
 
-@SuppressWarnings("unused")
+@SuppressWarnings({"unused"})
 public class Day16 extends Puzzle {
 
-    public static final int TIME_REMAINING = 30;
+    public static final boolean verbose = true;
+    public static final int TIME_AVAILABLE = 30;
 
     public Day16(boolean isProductionReady) {
         super(isProductionReady);
@@ -26,9 +28,15 @@ public class Day16 extends Puzzle {
 
     @Override
     public long computePart1(List<String> data) {
-        Valve.createValveNetwork(data);
-        String startingLocation = data.get(0).substring(6, 8);
-        return Valve.getOptimalFlow(TIME_REMAINING, "AA", false);
+        Map<ValvePair, Long> shortestDistances = Valve.createValveNetwork(data);
+        Valve startingLocation = Valve.getValve(data.get(0).substring(6, 8));
+        Set<Valve> openValves = Valve.getValves().stream()
+                .filter(valve -> valve.getFlowRate() == 0)
+                .collect(Collectors.toSet());
+        Set<Valve> closedValves = Valve.getValves().stream()
+                .filter(valve -> valve.getFlowRate() > 0)
+                .collect(Collectors.toSet());
+        return findOptimalFlow(startingLocation, shortestDistances, TIME_AVAILABLE, closedValves, openValves);
     }
 
     @Override
@@ -36,24 +44,53 @@ public class Day16 extends Puzzle {
         return 0;
     }
 
+    private long findOptimalFlow(Valve startingLocation, Map<ValvePair, Long> shortestDistances, long timeRemaining,
+                                 Set<Valve> closedValves, Set<Valve> openValves) {
+        if (verbose) {
+            System.out.println("At " + startingLocation + " with " + timeRemaining + " minutes left. Open: "
+                    + openValves + " Closed: " + closedValves);
+        }
+        if (timeRemaining < 1) {
+            return 0L;
+        }
+        // with one minute remaining, even opening a valve would relieve no pressure.
+        // also, if all valves are open, might as well just kill time
+        long currentFlowRate = openValves.stream().mapToLong(Valve::getFlowRate).sum();
+        if (timeRemaining == 1 || closedValves.isEmpty()) {
+            return timeRemaining * currentFlowRate;
+        }
+        long maximumFlow = Long.MIN_VALUE;
+        // we know how long it takes to get from here to anywhere else, and
+        // the only locations worth visiting are those with open valves --
+        // which valve should we open next?
+        for (Valve valve : closedValves) {
+            Set<Valve> newClosedValves = new HashSet<>(closedValves);
+            newClosedValves.remove(valve);
+            Set<Valve> newOpenValves = new HashSet<>(openValves);
+            newOpenValves.add(valve);
+            // lose one minute to turn valve, and more time to travel to next location
+            long newTimeRemaining = timeRemaining - shortestDistances.get(new ValvePair(startingLocation, valve)) - 1;
+            if (newTimeRemaining > 0) {
+                long flowDuringTravel = (timeRemaining - newTimeRemaining) * currentFlowRate;
+                maximumFlow = Math.max(maximumFlow,
+                        findOptimalFlow(valve, shortestDistances, newTimeRemaining, newClosedValves, newOpenValves));
+            }
+        }
+        // would we have been better off walking in circles?
+        maximumFlow = Math.max(maximumFlow, timeRemaining * currentFlowRate);
+        return maximumFlow;
+    }
+
     private static class Valve {
-        private static Map<String, Valve> valves = null;
-        private static Map<Integer, Map<String, Map<Boolean, Integer>>> optimalFlows = null;
+        private static final Set<Valve> valves = new HashSet<>();
+        private static final Map<ValvePair, Long> shortestDistances = new HashMap<>();
 
-        @SuppressWarnings("FieldCanBeLocal")
         private final String name;
         private final int flowRate;
-        private final Set<String> adjacentValves;
-
-        private Valve(String name, int flowRate, String[] adjacentValves) {
-            this.name = name;
-            this.flowRate = flowRate;
-            this.adjacentValves = Set.of(adjacentValves);
-        }
+        private final Set<String> adjacentValveNames;
+        private Set<Valve> adjacentValves = null;
 
-        public static void createValveNetwork(List<String> descriptions) {
-            valves = new HashMap<>();
-            optimalFlows = new HashMap<>();
+        public static Map<ValvePair, Long> createValveNetwork(List<String> descriptions) {
             for (String description : descriptions) {
                 String name = description.substring(6, 8);
                 String[] descriptionHalves = description.split(";");
@@ -63,48 +100,75 @@ public class Day16 extends Puzzle {
                     neighborsString = neighborsString.substring(2);
                 }
                 String[] adjacentValves = neighborsString.split(", ");
-                valves.put(name, new Valve(name, flowRate, adjacentValves));
+                valves.add(new Valve(name, flowRate, adjacentValves));
             }
-        }
-
-        public static int getOptimalFlow(int timeRemaining, String valveName, boolean valveIsAlreadyOpen) {
-            if (optimalFlows == null) {
-                throw new IllegalStateException("Cannot calculate optimal flow without creating valve network first.");
-            }
-            int depthPreviouslyCalculated = optimalFlows.size();
-            if (timeRemaining < depthPreviouslyCalculated) {
-                return optimalFlows.get(timeRemaining).get(valveName).get(valveIsAlreadyOpen);
-            }
-            /* TODO: incorporate time to determine "eventual total pressure release" */
-            for (int t = depthPreviouslyCalculated; t <= timeRemaining; t++) {
-                HashMap<String, Map<Boolean, Integer>> depthOptima = new HashMap<>();
-                for (String valve : valves.keySet()) {
-                    HashMap<Boolean, Integer> valveOptima = new HashMap<>();
-                    if (t == 0) {
-                        valveOptima.put(true, 0);
-                        valveOptima.put(false, 0);
-                    } else {
-                        int depth = t - 1;
-                        // if the valve is already open then we must move
-                        int optimalFlow = valves.get(valve).adjacentValves.stream()
-                                .map(name -> optimalFlows.get(depth).get(name).get(true))
-                                .max(Integer::compareTo).orElseThrow();
-                        valveOptima.put(true, optimalFlow);
-                        // if the valve is still closed then...
-                        // option 1: move to an adjacent valve
-                        int optimalFlowByMoving = valves.get(valve).adjacentValves.stream()
-                                .map(name -> optimalFlows.get(depth).get(name).get(false))
-                                .max(Integer::compareTo).orElseThrow();
-                        // option 2: turn the current valve
-                        int optimalFlowByOpeningValve = valves.get(valve).flowRate
-                                + optimalFlows.get(depth).get(valve).get(true);
-                        valveOptima.put(false, Math.max(optimalFlowByMoving, optimalFlowByOpeningValve));
+            // initially assume it takes forever to get from anywhere to anywhere
+//            Valve.valves.forEach(from ->
+//                    valves.forEach(to ->
+//                            shortestDistances.put(new ValvePair(from, to), Long.MAX_VALUE)));
+            // oops, that had an overflow problem during addition
+            Valve.valves.forEach(from ->
+                    valves.forEach(to ->
+                            shortestDistances.put(new ValvePair(from, to), 2L * valves.size())));
+            // but we know some locations are just one minute apart
+            valves.forEach(from ->
+                    from.getAdjacentValves().forEach(to ->
+                            shortestDistances.put(new ValvePair(from, to), 1L)));
+            // just in case we want to go from where we are to the same location, why take a tunnel?
+            valves.forEach(valve -> shortestDistances.put(new ValvePair(valve, valve), 0L));
+            // find the shortest travel time from anywhere to anywhere
+            for (Valve valve : valves) {
+                for (Valve from : valves) {
+                    for (Valve to : valves) {
+                        ValvePair valvePair = new ValvePair(from, to);
+                        shortestDistances.put(valvePair, Math.min(shortestDistances.get(valvePair),
+                                shortestDistances.get(new ValvePair(from, valve))
+                                        + shortestDistances.get(new ValvePair(valve, to)))
+                        );
                     }
-                    depthOptima.put(valve, valveOptima);
                 }
-                optimalFlows.put(t, depthOptima);
             }
-            return optimalFlows.get(timeRemaining).get(valveName).get(valveIsAlreadyOpen);
+            return shortestDistances;
+        }
+
+        public static Set<Valve> getValves() {
+            return Collections.unmodifiableSet(valves);
+        }
+
+        public static Valve getValve(String name) {
+            return valves.stream().filter(valve -> valve.name.equals(name)).findAny().orElse(null);
+        }
+
+        private Valve(String name, int flowRate, String[] adjacentValves) {
+            this.name = name;
+            this.flowRate = flowRate;
+            this.adjacentValveNames = Set.of(adjacentValves);
+        }
+
+        public String getName() {
+            return name;
+        }
+
+        public int getFlowRate() {
+            return flowRate;
+        }
+
+        public Set<Valve> getAdjacentValves() {
+            if (adjacentValves == null) {
+                adjacentValves = adjacentValveNames.stream()
+                        .map(valveName ->
+                                valves.stream().filter(v -> v.name.equals(valveName)).findFirst().orElseThrow())
+                        .collect(Collectors.toSet());
+            }
+            return adjacentValves;
         }
+
+        @Override
+        public String toString() {
+            return name + "(" + flowRate + ")";
+        }
+    }
+
+    private record ValvePair(Valve from, Valve to) {
     }
 }
diff --git a/2022/src/main/java/edu/unl/cse/bohn/year2022/Day17.java b/2022/src/main/java/edu/unl/cse/bohn/year2022/Day17.java
new file mode 100644
index 0000000000000000000000000000000000000000..02e89dffc54bb674b709b12b8e5cb8647b71c51a
--- /dev/null
+++ b/2022/src/main/java/edu/unl/cse/bohn/year2022/Day17.java
@@ -0,0 +1,166 @@
+package edu.unl.cse.bohn.year2022;
+
+import edu.unl.cse.bohn.Puzzle;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+import java.util.stream.IntStream;
+
+@SuppressWarnings("unused")
+public class Day17 extends Puzzle {
+    public static final boolean veryVerbose = false;
+    public static final boolean verbose = false;
+
+    public Day17(boolean isProductionReady) {
+        super(isProductionReady);
+        sampleData = ">>><<><>><<<>><>>><<<>>><<<><<<>><>><<>>";
+    }
+
+    public static final List<Sprite> sprites = List.of(
+            new Sprite(List.of("####")),
+            new Sprite(List.of(".#.", "###", ".#.")),
+            new Sprite(List.of("###", "..#", "..#")),
+            new Sprite(List.of("#", "#", "#", "#")),
+            new Sprite(List.of("##", "##"))
+    );
+
+    public static final int NUMBER_OF_ROCKS_TO_DROP = 2022;
+
+    @Override
+    public long computePart1(List<String> data) {
+        List<Character> jets = data.get(0).chars().mapToObj(c -> (char) c).toList();
+        return dropRocks(jets, NUMBER_OF_ROCKS_TO_DROP);
+    }
+
+    @Override
+    public long computePart2(List<String> data) {
+        return 0;
+    }
+
+    @SuppressWarnings({"OverlyLongMethod", "SameParameterValue"})
+    private int dropRocks(List<Character> jets, int numberOfRocksToDrop) {
+        Iterator<Character> jetIterator = jets.iterator();
+        Iterator<Sprite> spriteIterator = sprites.iterator();
+        List<int[]> tunnel = new ArrayList<>(numberOfRocksToDrop * 3);
+        int[] floor = {1, 1, 1, 1, 1, 1, 1, 1, 1};
+        int[] walls = {1, 0, 0, 0, 0, 0, 0, 0, 1};
+        int height = 0;
+        final int horizontalOffset = 3;
+        final int verticalOffset = 4;
+        int x = 0, y = 0;
+        int[][] rock = null;
+        boolean rockIsAtRest = true;
+        int numberOfRocksAtRest = 0;
+        tunnel.add(floor);
+        while (numberOfRocksAtRest < numberOfRocksToDrop) {
+            if (rockIsAtRest) {
+                rockIsAtRest = false;
+                // get next rock
+                if (!spriteIterator.hasNext()) {
+                    spriteIterator = sprites.iterator();
+                }
+                rock = spriteIterator.next().rockOrNot;
+                // expand the tunnel as necessary
+                for (int i = tunnel.size(); i < height + rock.length + verticalOffset; i++) {
+                    tunnel.add(walls.clone());
+                }
+                // place the rock
+                x = horizontalOffset;
+                y = height + verticalOffset;
+                for (int i = 0; i < rock.length; i++) {
+                    System.arraycopy(rock[i], 0, tunnel.get(y + i), x, rock[i].length);
+                }
+                if (veryVerbose || verbose) {
+                    System.out.println(System.lineSeparator()
+                            + "-- " + numberOfRocksAtRest + " rocks at rest -- new rock dropped --");
+                    IntStream.iterate(tunnel.size() - 1, i -> i >= 0, i -> i - 1)
+                            .mapToObj(i -> Arrays.toString(tunnel.get(i))).forEach(System.out::println);
+                }
+            } else {
+                if (!jetIterator.hasNext()) {
+                    jetIterator = jets.iterator();
+                }
+                char jet = jetIterator.next();
+                // horizontal motion
+                eraseRock(tunnel, rock, x, y);
+                x += jet == '>' ? 1 : -1;
+                drawRock(tunnel, rock, x, y);
+                boolean foundCollision = detectCollision(tunnel, rock, y);
+                if (foundCollision) {
+                    eraseRock(tunnel, rock, x, y);
+                    x += jet == '>' ? -1 : 1;
+                    drawRock(tunnel, rock, x, y);
+                }
+                // vertical motion
+                eraseRock(tunnel, rock, x, y);
+                y--;
+                drawRock(tunnel, rock, x, y);
+                foundCollision = detectCollision(tunnel, rock, y);
+                if (foundCollision) {
+                    eraseRock(tunnel, rock, x, y);
+                    y++;
+                    drawRock(tunnel, rock, x, y);
+                    rockIsAtRest = true;
+                    numberOfRocksAtRest++;
+                    height = Math.max(height, y + rock.length - 1);
+                }
+                if (veryVerbose) {
+                    System.out.println(System.lineSeparator() + jet);
+                    IntStream.iterate(tunnel.size() - 1, i -> i >= 0, i -> i - 1)
+                            .mapToObj(i -> Arrays.toString(tunnel.get(i))).forEach(System.out::println);
+                }
+            }
+        }
+        if (veryVerbose || verbose) {
+            System.out.println(System.lineSeparator()
+                    + "-- " + numberOfRocksAtRest + " rocks at rest -- complete --");
+            IntStream.iterate(tunnel.size() - 1, i -> i >= 0, i -> i - 1)
+                    .mapToObj(i -> Arrays.toString(tunnel.get(i))).forEach(System.out::println);
+        }
+        return height;
+    }
+
+    private static boolean detectCollision(List<int[]> tunnel, int[][] rock, int y) {
+        boolean foundCollision = false;
+        for (int i = 0; i < rock.length; i++) {
+            int[] row = tunnel.get(y + i);
+            for (int positionValue : row) {
+                foundCollision = foundCollision || (positionValue == 2);
+            }
+        }
+        return foundCollision;
+    }
+
+    private static void drawRock(List<int[]> tunnel, int[][] rock, int x, int y) {
+        for (int i = 0; i < rock.length; i++) {
+            int[] row = tunnel.get(y + i);
+            for (int j = 0; j < rock[i].length; j++) {
+                row[x + j] += rock[i][j];
+            }
+        }
+    }
+
+    private static void eraseRock(List<int[]> tunnel, int[][] rock, int x, int y) {
+        for (int i = 0; i < rock.length; i++) {
+            int[] row = tunnel.get(y + i);
+            for (int j = 0; j < rock[i].length; j++) {
+                row[x + j] -= rock[i][j];
+            }
+        }
+    }
+
+    private static class Sprite {
+        public final int[][] rockOrNot;
+
+        public Sprite(List<String> description) {
+            rockOrNot = new int[description.size()][description.get(0).length()];
+            for (int i = 0; i < description.size(); i++) {
+                for (int j = 0; j < description.get(i).length(); j++) {
+                    rockOrNot[i][j] = description.get(i).charAt(j) == '#' ? 1 : 0;
+                }
+            }
+        }
+    }
+}