diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ffeb9d9..007257b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,10 +10,18 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-java@v2 + - uses: actions/checkout@v3 + - uses: actions/setup-java@v3 with: distribution: 'temurin' - java-version: '17' + java-version: '19' check-latest: true + - uses: actions/cache@v3 + with: + path: ~/.m2 + key: m2-${{ runner.os }}-19-${{ hashFiles('**/pom.xml') }} + restore-keys: | + m2-${{ runner.os }}-19 + m2-${{ runner.os }} + m2 - run: mvn clean install diff --git a/.gitignore b/.gitignore index 5c56b6a..4d131ae 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,7 @@ .settings advent-of-code.iml target/ +src/test/resources/config.properties +src/test/resources/input +src/test/resources/2020 +.DS_Store diff --git a/.java-version b/.java-version index 98d9bcb..6ea9a3b 100644 --- a/.java-version +++ b/.java-version @@ -1 +1 @@ -17 +19.0 diff --git a/README.md b/README.md index 6947951..29e9c19 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,12 @@ -# Advent of Code 2020 (Java) +# Advent of Code 2022 (Java) + +This is the code I used to solve the +[Advent of Code](https://adventofcode.com/2022) puzzles. The main branch +contains the most recent year in which I participated. Other years have +their own branch. ## Other Editions -* 2020 Advent of Code ([Rust](https://github.com/l0s/advent-of-code-rust)) +* 2020 Advent of Code ([Java](https://github.com/l0s/advent-of-code-java/tree/2020) | [Rust](https://github.com/l0s/advent-of-code-rust/releases/tag/y2020)) +* 2021 Advent of Code ([Java](https://github.com/l0s/advent-of-code-java/releases/tag/2021)) +* 2022 Advent of Code ([Rust](https://github.com/l0s/advent-of-code-rust)) \ No newline at end of file diff --git a/pom.xml b/pom.xml index 22284a9..fc13d64 100644 --- a/pom.xml +++ b/pom.xml @@ -6,14 +6,37 @@ com.macasaet advent-of-code - 0.2020.0-SNAPSHOT + 0.2022.0-SNAPSHOT - Advent of Code 2020 + Advent of Code 2022 UTF-8 - 17 - 17 + 19 + 19 + + + org.junit.jupiter + junit-jupiter + 5.9.0 + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0-M5 + + + Day*.java + + + + + + diff --git a/src/test/java/com/macasaet/Day01.java b/src/test/java/com/macasaet/Day01.java index 678b200..d70e9ed 100644 --- a/src/test/java/com/macasaet/Day01.java +++ b/src/test/java/com/macasaet/Day01.java @@ -1,58 +1,76 @@ package com.macasaet; -import java.io.IOException; +import org.junit.jupiter.api.Test; + +import java.math.BigInteger; import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.Iterator; import java.util.List; -import java.util.Spliterator; import java.util.stream.StreamSupport; - +/** + * --- Day 1: Calorie Counting --- + */ public class Day01 { - public static void main(String[] args) throws IOException { - try( var spliterator = new LineSpliterator(Day01.class.getResourceAsStream("/day-1-input.txt")) ) { -// part1(spliterator); - part2(spliterator); - } + protected Iterator getInput() { + return StreamSupport + .stream(new LineSpliterator("day-01.txt"), + false) + .iterator(); } - protected static void part1(final Spliterator spliterator) { - final var items = StreamSupport.stream(spliterator, false) - .mapToInt(Integer::parseInt) - .filter(candidate -> candidate <= 2020) // filter out the obvious - .sorted() // sort to ensure the complement is to the left - .collect(ArrayList::new, List::add, List::addAll); - for( int i = items.size(); --i >= 0; ) { - final int x = items.get(i); - for( int j = i; --j >= 0; ) { // avoid retrying combinations - final int y = items.get(j); - if( x + y == 2020 ) { - System.out.println( "" + ( x * y ) ); - return; - } + protected List getElves() { + var calories = new ArrayList(); + final var elves = new ArrayList(); + for (final var i = getInput(); i.hasNext(); ) { + final var line = i.next(); + if (line.isBlank()) { + elves.add(new Elf(Collections.unmodifiableList(calories))); + calories = new ArrayList<>(); + } else { + calories.add(new BigInteger(line.strip())); } } + if (!calories.isEmpty()) { + elves.add(new Elf(Collections.unmodifiableList(calories))); + } + return Collections.unmodifiableList(elves); } - protected static void part2(final Spliterator spliterator) { - final var items = StreamSupport.stream(spliterator, false) - .mapToInt(Integer::parseInt) - .filter(candidate -> candidate <= 2020) // filter out the obvious - .sorted() // sort to ensure the complements are to the left - .collect(ArrayList::new, List::add, List::addAll); - for( int i = items.size(); --i >= 0; ) { - final int x = items.get(i); - for( int j = i; --j >= 0; ) { - final int y = items.get(j); - for( int k = j; --k >= 0; ) { - final int z = items.get(k); - if( x + y + z == 2020 ) { - System.out.println( "" + ( x * y * z ) ); - return; - } - } + @Test + public final void part1() { + final var elves = getElves(); + final var elf = elves.stream() + .max(Comparator.comparing(Elf::totalCaloriesCarried)) + .get(); - } + System.out.println("Part 1: " + elf.totalCaloriesCarried()); + } + + @Test + public final void part2() { + final var elves = getElves(); + final var list = elves.stream() + .sorted(Comparator.comparing(Elf::totalCaloriesCarried).reversed()) + .toList(); + + System.out.println("Part 2: " + (list.get(0).totalCaloriesCarried().add(list.get(1).totalCaloriesCarried()).add(list.get(2).totalCaloriesCarried()))); + } + + /** + * An elf who collects food for the reindeer. + * + * @param itemCalories The number of calories of each item carried by the elf + */ + public record Elf(List itemCalories) { + public BigInteger totalCaloriesCarried() { + return itemCalories().stream() + .reduce(BigInteger::add) + .get(); } } -} + +} \ No newline at end of file diff --git a/src/test/java/com/macasaet/Day02.java b/src/test/java/com/macasaet/Day02.java index 7bfb3d0..424b241 100644 --- a/src/test/java/com/macasaet/Day02.java +++ b/src/test/java/com/macasaet/Day02.java @@ -1,91 +1,175 @@ package com.macasaet; -import java.io.IOException; -import java.util.regex.Pattern; +import org.junit.jupiter.api.Test; + +import java.util.stream.Stream; import java.util.stream.StreamSupport; +/** + * --- Day 2: Rock Paper Scissors --- + * https://adventofcode.com/2022/day/2 + */ public class Day02 { - public static void main(String[] args) throws IOException { - try( var spliterator = new LineSpliterator( Day02.class.getResourceAsStream("/day-2-input.txt" ) ) ) { - final long count = StreamSupport.stream(spliterator, false) -// .map(SledEntry::build) // part 1 - .map(TobogganEntry::build) // part 2 - .filter(Entry::isValid).count(); - System.out.println("" + count); - } - + protected Stream getInput() { + return StreamSupport + .stream(new LineSpliterator("day-02.txt"), + false) + .map(line -> { + final var components = line.strip().split(" "); + return new Round(Shape.forChar(components[0].charAt(0)), + Shape.forChar(components[1].charAt(0)), + ResponseStrategy.forChar(components[1].charAt(0))); + }); } - public static abstract class Entry { - - // TODO consider a more robust pattern: - // https://stackoverflow.com/a/4731164/914887 - protected static final Pattern separator = Pattern.compile("\\s"); - protected static final Pattern rangeSeparator = Pattern.compile("-"); - protected final char c; // TODO support code points - protected final String password; + @Test + public final void part1() { + final var result = getInput().mapToInt(Round::naiveScore).sum(); - protected Entry(final char c, final String password) { - this.c = c; - this.password = password; - } + System.out.println("Part 1: " + result); + } - public abstract boolean isValid(); + @Test + public final void part2() { + final var result = getInput().mapToInt(Round::score).sum(); + System.out.println("Part 2: " + result); } - public static class SledEntry extends Entry { - private final long minIterations; - private final long maxIterations; - - public SledEntry( final char c, final String password, final long minIterations, final long maxIterations ) { - super(c, password); - this.minIterations = minIterations; - this.maxIterations = maxIterations; + /** + * A shape that a contestant can play in a round + */ + public enum Shape { + Rock { + public int score() { + return 1; + } + + public Shape beatenBy() { + return Paper; + } + + public Shape beats() { + return Scissors; + } + }, + Paper { + public int score() { + return 2; + } + + public Shape beatenBy() { + return Scissors; + } + + public Shape beats() { + return Rock; + } + }, + Scissors { + public int score() { + return 3; + } + + @Override + public Shape beatenBy() { + return Rock; + } + + public Shape beats() { + return Paper; + } + }; + + public static Shape forChar(final int c) { + return switch (c) { + case 'X': + case 'A': + yield Shape.Rock; + case 'Y': + case 'B': + yield Shape.Paper; + case 'Z': + case 'C': + yield Shape.Scissors; + default: + throw new IllegalArgumentException("Invalid shape: " + c); + }; } - public static Entry build(final String line) { - final var components = separator.split(line, 3); - final var range = components[0]; - final var rangeComponents = rangeSeparator.split(range, 2); - return new SledEntry(components[1].charAt(0), - components[2], - Long.parseLong(rangeComponents[ 0 ]), - Long.parseLong(rangeComponents[ 1 ] )); - } + /** + * @return the inherent value of this shape + */ + public abstract int score(); - public boolean isValid() { - final var count = password.chars() - .filter(candidate -> (char) candidate == this.c) - .count(); - return count >= minIterations && count <= maxIterations; - } - } + /** + * @return the shape that beats this one + */ + public abstract Shape beatenBy(); - public static class TobogganEntry extends Entry { - private final int firstPosition; - private final int secondPosition; + /** + * @return the shape this one beats + */ + public abstract Shape beats(); + } - public TobogganEntry( final char c, final String password, final int firstPosition, final int secondPosition ) { - super( c, password ); - this.firstPosition = firstPosition; - this.secondPosition = secondPosition; + /** + * An approach to responding to the shape played by the opponent + */ + public enum ResponseStrategy { + Lose { + public Shape respond(Shape opponent) { + return opponent.beats(); + } + }, + Draw { + public Shape respond(Shape opponent) { + return opponent; + } + }, + Win { + public Shape respond(Shape opponent) { + return opponent.beatenBy(); + } + }; + + public static ResponseStrategy forChar(final char c) { + return switch (c) { + case 'X' -> Lose; + case 'Y' -> Draw; + case 'Z' -> Win; + default -> throw new IllegalArgumentException("Invalid strategy: " + c); + }; } - public static Entry build( final String line ) { - final var components = separator.split( line, 3 ); - final var positionComponents = rangeSeparator.split( components[ 0 ], 2 ); - return new TobogganEntry( components[ 1 ].charAt( 0 ), - components[ 2 ], - Integer.parseInt( positionComponents[ 0 ] ), - Integer.parseInt( positionComponents[ 1 ] ) ); + public abstract Shape respond(final Shape opponent); + } + + /** + * A single round of the game + * + * @param opponent The shape played by the opponent + * @param player The shape chosen based on the original (incorrect) interpretation of the responseStrategy guide + * @param responseStrategy The responseStrategy for responding to the opponent according to the responseStrategy guide + */ + public record Round(Shape opponent, Shape player, ResponseStrategy responseStrategy) { + /** + * @return the score based on the simple (incorrect) interpretation of the strategy guide + */ + public int naiveScore() { + final var outcome = opponent() == player() ? 3 : opponent().beatenBy() == player() ? 6 : 0; + return outcome + player().score(); } - public boolean isValid() { - final var x = password.charAt(firstPosition - 1); - final var y = password.charAt(secondPosition - 1); - return x == c ^ y == c; + /** + * @return the score based on following the strategy guide + */ + public int score() { + final var response = responseStrategy().respond(opponent()); + final var outcome = opponent() == response ? 3 : opponent().beatenBy() == response ? 6 : 0; + return outcome + response.score(); } } + } \ No newline at end of file diff --git a/src/test/java/com/macasaet/Day03.java b/src/test/java/com/macasaet/Day03.java index 344fc9f..af6e98a 100644 --- a/src/test/java/com/macasaet/Day03.java +++ b/src/test/java/com/macasaet/Day03.java @@ -1,57 +1,117 @@ package com.macasaet; -import java.io.IOException; -import java.math.BigInteger; -import java.util.Arrays; -import java.util.stream.Collectors; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Stream; import java.util.stream.StreamSupport; +/** + * --- Day 3: Rucksack Reörganisation --- + * https://adventofcode.com/2022/day/3 + */ public class Day03 { - public static void main(String[] args) throws IOException { - try( var spliterator = new LineSpliterator( Day03.class.getResourceAsStream("/day-3-input.txt" ) ) ) { - final var rowList = StreamSupport.stream(spliterator, false) - .map(String::toCharArray) - .collect(Collectors.toUnmodifiableList()); - final var matrix = new char[rowList.size()][]; - for (int i = rowList.size(); --i >= 0; matrix[i] = rowList.get(i)); - - final var slopes = new int[][] { - new int[] { 1, 1 }, - new int[] { 3, 1 }, // uncomment all but this for "part 1" - new int[] { 5, 1 }, - new int[] { 7, 1 }, - new int[] { 1, 2 }, - }; - final var totalTrees = Arrays.stream(slopes) - .mapToLong(pair -> { - final int slopeRight = pair[0]; - final int slopeDown = pair[1]; - - long numTrees = 0; - int rowIndex = 0; - int columnIndex = 0; - - do { - final var row = matrix[rowIndex]; - final var cell = row[columnIndex]; - if (cell == '#') { - numTrees += 1l; - } - rowIndex += slopeDown; - columnIndex = columnIndex + slopeRight; - columnIndex = columnIndex % row.length; // "These aren't the only trees, though; due to - // something you read about once involving - // arboreal genetics and biome stability, the - // same pattern repeats to the right many times" - } while( rowIndex < matrix.length ); - return numTrees; - }).mapToObj(BigInteger::valueOf) // I wasn't sure how large these (multiplied) values could get, in retrospect, `long` would have been fine - .reduce(BigInteger::multiply) - .get(); - System.out.println("" + totalTrees.toString()); + protected static int priority(final char c) { + if (c >= 'a' && c <= 'z') { + return c - 'a' + 1; + } + return c - 'A' + 27; + } + + protected Stream getInput() { + return StreamSupport + .stream(new LineSpliterator("day-03.txt"), + false) + .map(Rucksack::parse); + } + + @Test + public final void part1() { + final var result = getInput().mapToInt(Rucksack::priority).sum(); + + System.out.println("Part 1: " + result); + } + + @Test + public final void part2() { + final var groups = new ArrayList>(); + var currentGroup = new ArrayList(3); + + for (final var i = getInput().iterator(); i.hasNext(); ) { + final var rucksack = i.next(); + if (currentGroup.size() == 3) { + groups.add(Collections.unmodifiableList(currentGroup)); + currentGroup = new ArrayList<>(3); + } + currentGroup.add(rucksack); + } + if (currentGroup.size() == 3) { + groups.add(Collections.unmodifiableList(currentGroup)); } + final var result = groups.stream().map(this::getBadge).mapToInt(Day03::priority).sum(); + System.out.println("Part 2: " + result); } + protected char getBadge(final List group) { + final var first = group.get(0); + for (final var item : first.allItems()) { + if (group.get(1).allItems().contains(item) && group.get(2).allItems().contains(item)) { + return item; + } + } + throw new IllegalStateException(); + } + + /** + * An Elf's container of supplies for a jungle journey. "Each rucksack has two large compartments. All items of a + * given type are meant to go into exactly one of the two compartments." + * + * @param firstCompartment All the items in one compartment + * @param secondCompartment All the items in one compartment + * @param allItems All the items + */ + public record Rucksack(Set firstCompartment, Set secondCompartment, Set allItems) { + + public static Rucksack parse(final String line) { + final var chars = line.toCharArray(); + if (chars.length % 2 != 0) { + throw new IllegalArgumentException(); + } + final var firstCompartment = new HashSet(chars.length / 2); + final var secondCompartment = new HashSet(chars.length / 2); + for (int i = 0; i < chars.length / 2; i++) { + firstCompartment.add(chars[i]); + } + for (int i = chars.length / 2; i < chars.length; i++) { + secondCompartment.add(chars[i]); + } + final var union = new HashSet(chars.length); + union.addAll(firstCompartment); + union.addAll(secondCompartment); + return new Rucksack(Collections.unmodifiableSet(firstCompartment), + Collections.unmodifiableSet(secondCompartment), + Collections.unmodifiableSet(union)); + } + + public int priority() { + final var intersection = new HashSet(); + for (final char c : firstCompartment) { + if (secondCompartment.contains(c)) { + intersection.add(c); + } + } + if (intersection.size() != 1) { + throw new IllegalStateException("There should only be one common item between compartments"); + } + return Day03.priority(intersection.iterator().next()); + } + + + } } \ No newline at end of file diff --git a/src/test/java/com/macasaet/Day04.java b/src/test/java/com/macasaet/Day04.java index 138675b..8b3d13d 100644 --- a/src/test/java/com/macasaet/Day04.java +++ b/src/test/java/com/macasaet/Day04.java @@ -1,83 +1,69 @@ package com.macasaet; -import java.io.IOException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Set; -import java.util.stream.Collectors; +import org.junit.jupiter.api.Test; + +import java.util.stream.Stream; import java.util.stream.StreamSupport; +/** + * --- Day 4: Camp Cleanup --- + * https://adventofcode.com/2022/day/4 + */ public class Day04 { - public static void main(String[] args) throws IOException { - try (var spliterator = new LineSpliterator( Day04.class.getResourceAsStream( "/day-4-input.txt" ) ) ) { - final var rawLines = StreamSupport.stream(spliterator, false) - .collect(Collectors.toUnmodifiableList()); - String current = ""; - // collect all the text blocks into entries (separated by empty lines) - final var entries = new ArrayList(); - for (final var line : rawLines) { - if (line.isBlank()) { - if (!current.isBlank()) { - entries.add(current); - current = ""; - } - } else { - current += " " + line; - } - } - if (!current.isBlank()) { - entries.add(current); - } - int numValid = 0; - for (final var entry : entries) { - final var pairs = entry.split("\\s"); - final var map = new HashMap(); - for (final var pair : pairs) { - if (pair.isBlank()) { - continue; - } - final var components = pair.split(":", 2); - map.put(components[0].trim(), components[1].trim()); - } - final var birthYearString = map.get("byr"); - final var issueYearString = map.get("iyr"); - final var expirationYearString = map.get("eyr"); - final var heightString = map.get("hgt"); - final var hairColour = map.get("hcl"); - final var eyeColour = map.get("ecl"); - final var validEyeColours = Set.of("amb", "blu", "brn", "gry", "grn", "hzl", "oth"); - final var passportId = map.get("pid"); - if (birthYearString == null) continue; - final int birthYear = Integer.parseInt(birthYearString); - if (birthYear < 1920 || birthYear > 2002) continue; - if (issueYearString == null) continue; - final int issueYear = Integer.parseInt(issueYearString); - if (issueYear < 2010 || issueYear > 2020) continue; - if (expirationYearString == null) continue; - final int expirationYear = Integer.parseInt(expirationYearString); - if (expirationYear < 2020 || expirationYear > 2030) continue; - if (heightString == null) continue; - if (heightString.endsWith("cm")) { - final int centimetres = Integer.parseInt(heightString.replace("cm", "")); - if (centimetres < 150 || centimetres > 193) continue; - } else if (heightString.endsWith("in")) { - final int inches = Integer.parseInt(heightString.replace("in", "")); - if (inches < 59 || inches > 76) continue; - } else { - continue; - } - if (hairColour == null) continue; - if (!hairColour.matches("#[0-9a-f]{6}")) continue; - if (eyeColour == null) continue; - if (!validEyeColours.contains(eyeColour)) continue; - if (passportId == null) continue; - if (!passportId.matches("[0-9]{9}")) continue; - numValid++; - } - System.out.println(numValid); + /** + * One crew member responsible for cleaning up the camp. They are responsible for a contiguous range of sections. + * + * @param sectionMin the lowest section ID for which this Elf is responsible (inclusive) + * @param sectionMax the highest section ID for which this Elf is responsible (inclusive). + */ + public record Elf(int sectionMin, int sectionMax) { + + public boolean fullyContains(final Elf other) { + return sectionMin() <= other.sectionMin() && sectionMax() >= other.sectionMax(); + } + + public static Elf parse(final String string) { + final var components = string.split("-"); + return new Elf(Integer.parseInt(components[0]), Integer.parseInt(components[1])); + } + } + + /** + * Two elves (of a larger crew) assigned to clean up the camp. + */ + public record Pair(Elf left, Elf right) { + public boolean oneFullyContainsTheOther() { + return left.fullyContains(right) || right.fullyContains(left); + } + public boolean hasOverlap() { + return (left.sectionMin() <= right.sectionMin() && left.sectionMax() >= right.sectionMin()) + || (right.sectionMin() <= left.sectionMin() && right.sectionMax() >= left.sectionMin()); } + public static Pair parse(final String line) { + final var components = line.split(","); + return new Pair(Elf.parse(components[0]), Elf.parse(components[1])); + } + } + protected Stream getInput() { + return StreamSupport + .stream(new LineSpliterator("day-04.txt"), + false) + .map(Pair::parse); + } + + @Test + public final void part1() { + final var result = getInput().filter(Pair::oneFullyContainsTheOther).count(); + + System.out.println("Part 1: " + result); + } + + @Test + public final void part2() { + final var result = getInput().filter(Pair::hasOverlap).count(); + System.out.println("Part 2: " + result); } } \ No newline at end of file diff --git a/src/test/java/com/macasaet/Day05.java b/src/test/java/com/macasaet/Day05.java index dcb1d6a..97c43c4 100644 --- a/src/test/java/com/macasaet/Day05.java +++ b/src/test/java/com/macasaet/Day05.java @@ -1,55 +1,138 @@ package com.macasaet; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; +import org.junit.jupiter.api.Test; + +import java.util.Deque; +import java.util.LinkedList; +import java.util.stream.Stream; import java.util.stream.StreamSupport; +/** + * --- Day 5: Supply Stacks --- + * https://adventofcode.com/2022/day/5 + */ public class Day05 { - public static void main(String[] args) throws IOException { - try (var spliterator = new LineSpliterator( Day05.class.getResourceAsStream( "/day-5-input.txt" ) ) ) { - final var seatIds = StreamSupport.stream(spliterator, false) - .mapToInt(id -> { - int maxRowExclusive = 128; - int row = 0; - for( int i = 0; i < 7; i++ ) { - final char partition = id.charAt(i); - if( partition == 'B' ) { - row = ( ( maxRowExclusive - row ) / 2 ) + row; - } else if( partition == 'F' ) { - maxRowExclusive = maxRowExclusive - ( ( maxRowExclusive - row ) / 2 ); - } else { - throw new IllegalArgumentException("Invalid row partition: " + partition); - } + public record CrateMover9000Instruction(int count, int from, int to) { + public static CrateMover9000Instruction parse(final String line) { + final var components = line.split(" "); + final var count = Integer.parseInt(components[1]); + final var from = Integer.parseInt(components[3]) - 1; + final var to = Integer.parseInt(components[5]) - 1; + return new CrateMover9000Instruction(count, from, to); + } + public void execute(final Deque[] columns) { + final Deque from = columns[from()]; + final Deque to = columns[to()]; + for(int i = count(); --i >= 0; ) { + to.push(from.pop()); + } + } + } + + public record CrateMover9001Instruction(int count, int from, int to) { + public static CrateMover9001Instruction parse(final String line) { + final var components = line.split(" "); + final var count = Integer.parseInt(components[1]); + final var from = Integer.parseInt(components[3]) - 1; + final var to = Integer.parseInt(components[5]) - 1; + return new CrateMover9001Instruction(count, from, to); + } + public void execute(final Deque[] columns) { + final Deque from = columns[from()]; + final Deque to = columns[to()]; + final var buffer = new LinkedList(); + for(int i = count(); --i >= 0; ) { + buffer.push(from.pop()); + } + while(!buffer.isEmpty()) { + to.push(buffer.pop()); + } + } + } + protected Stream getInput() { + return StreamSupport + .stream(new LineSpliterator("day-05.txt"), + false); + } + + @Test + public final void part1() { + int mode = 0; + final Deque[] columns = new Deque[9]; + for(int i = columns.length; --i >= 0; columns[i] = new LinkedList<>()); + for(final var line : getInput().toList()) { + if(line.isBlank()) { + mode = 1; + } + if( mode == 0 ) { + final var chars = line.toCharArray(); + int index = -1; + for(int i = 0; i < chars.length; i++) { + if(chars[i] == '[') { + index = i / 4; + } else if(index >= 0) { + columns[index].addLast(chars[i]); + index = -1; } - int column = 0; - int maxColumnExclusive = 8; - for( int i = 7; i < 10; i++ ) { - final char half = id.charAt(i); - if( half == 'R' ) { - column = ( ( maxColumnExclusive - column ) / 2 ) + column; - } else if( half == 'L' ) { - maxColumnExclusive = maxColumnExclusive - ( ( maxColumnExclusive - column ) / 2 ); - } else { - throw new IllegalArgumentException("Invalid column partition: " + half); - } + } + } else { + if(line.isBlank()) { + continue; + } + final var instruction = CrateMover9000Instruction.parse(line); + instruction.execute(columns); + } + } + final var builder = new StringBuilder(); + for(final var column : columns) { + if(!column.isEmpty()) { + builder.append(column.getFirst()); + } + } + final var result = builder.toString(); + + System.out.println("Part 1: " + result); + } + + @Test + public final void part2() { + int mode = 0; + final Deque[] columns = new Deque[9]; + for(int i = columns.length; --i >= 0; columns[i] = new LinkedList<>()); + for(final var line : getInput().toList()) { + if(line.isBlank()) { + mode = 1; + } + if( mode == 0 ) { + final var chars = line.toCharArray(); + int index = -1; + for(int i = 0; i < chars.length; i++) { + if(chars[i] == '[') { + index = i / 4; + } else if(index >= 0) { +// System.err.println("column[ " + index + " ].addLast( " + chars[ i ] + " )" ); + columns[index].addLast(chars[i]); + index = -1; } - final int seatId = row * 8 + column; - return seatId; - }) - .sorted() - .collect(ArrayList::new, List::add, List::addAll); - System.out.println("part 1: " + seatIds.get(seatIds.size() - 1)); - for( int i = seatIds.size(); --i >= 1; ) { - final int x = seatIds.get( i - 1 ); - final int y = seatIds.get( i ); - if( y - x > 1 ) { - System.out.println("part 2: " + ( x + 1 ) ); - return; } + } else { + if(line.isBlank()) { + continue; + } + final var instruction = CrateMover9001Instruction.parse(line); + instruction.execute(columns); } } + final var builder = new StringBuilder(); + for(final var column : columns) { + if(!column.isEmpty()) { + builder.append(column.getFirst()); + } + } + final var result = builder.toString(); + + System.out.println("Part 2: " + result); } diff --git a/src/test/java/com/macasaet/Day06.java b/src/test/java/com/macasaet/Day06.java index 91a46e6..769fb1b 100644 --- a/src/test/java/com/macasaet/Day06.java +++ b/src/test/java/com/macasaet/Day06.java @@ -1,48 +1,55 @@ package com.macasaet; -import java.io.IOException; -import java.util.ArrayList; +import org.junit.jupiter.api.Test; + import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; +import java.util.stream.Stream; import java.util.stream.StreamSupport; +/** + * --- Day 6: --- + * https://adventofcode.com/2022/day/6 + */ public class Day06 { - public static void main(final String[] args) throws IOException { - try (var spliterator = new LineSpliterator(Day06.class.getResourceAsStream("/day-6-input.txt"))) { - final var rawLines = StreamSupport.stream(spliterator, false) - .collect(Collectors.toUnmodifiableList()); - var currentGroup = new ArrayList(); - // collect all the text blocks into entries (separated by empty lines) - final var groups = new ArrayList>(); - for (final var line : rawLines) { - if (line.isBlank()) { - if (currentGroup != null && !currentGroup.isEmpty()) { - groups.add(currentGroup); - currentGroup = new ArrayList<>(); - } - } else { - currentGroup.add(line); - } + protected Stream getInput() { + return StreamSupport + .stream(new LineSpliterator("day-06.txt"), + false); + } + + @Test + public final void part1() { + final var input = getInput().findFirst().get(); + for(int i = 4; i < input.length(); i++) { + final var set = new HashSet(4); + for(int j = 0; j < 4; j++) { + set.add(input.charAt(i - 4 + j)); + } + if(set.size() >= 4) { + final var result = i; + System.out.println("Part 1: " + result); + return; + } + } + throw new IllegalStateException(); + } + + @Test + public final void part2() { + final var input = getInput().findFirst().get(); + for(int i = 14; i < input.length(); i++) { + final var set = new HashSet(14); + for(int j = 0; j < 14; j++) { + set.add(input.charAt(i - 14 + j)); } - if (currentGroup != null && !currentGroup.isEmpty()) { - groups.add(currentGroup); + if(set.size() >= 14) { + final var result = i; + System.out.println("Part 2: " + result); + return; } - final int sum = groups.stream().mapToInt(group -> { - final var uniqueCharacters = group.stream() - .flatMapToInt(String::chars) - .collect(HashSet::new, Set::add, Set::addAll); - return (int) uniqueCharacters.stream() - .filter(c -> (int) group.stream() - .map(String::chars) - .filter(chars -> chars.anyMatch(answer -> answer == c)) - .count() == group.size()) - .count(); - }).sum(); - System.out.println(sum); } + throw new IllegalStateException(); } } \ No newline at end of file diff --git a/src/test/java/com/macasaet/Day07.java b/src/test/java/com/macasaet/Day07.java index 00c3dc8..5aee1bc 100644 --- a/src/test/java/com/macasaet/Day07.java +++ b/src/test/java/com/macasaet/Day07.java @@ -1,75 +1,241 @@ package com.macasaet; -import java.io.IOException; -import java.util.Arrays; +import org.junit.jupiter.api.Test; + import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; +import java.util.Set; +import java.util.stream.Stream; import java.util.stream.StreamSupport; +/** + * --- Day 7: --- + * https://adventofcode.com/2022/day/7 + */ public class Day07 { - public static void main(final String[] args) throws IOException { - try (var spliterator = new LineSpliterator(Day07.class.getResourceAsStream("/day-7-input.txt"))) { - final var bag = "shiny gold"; - final var ruleMap = StreamSupport.stream(spliterator, false) - .map(Rule::fromSentence) - .collect(HashMap::new, - (map, rule) -> map.put(rule.containerColour, rule), - Map::putAll); - final var count = ruleMap.values() - .stream() - .filter(rule -> rule.canContain(bag, ruleMap)) - .count(); - System.out.println("part 1: " + count); - System.out.println("part 2: " + (ruleMap.get(bag).countContained(ruleMap) - 1)); + static class Session { + private final Directory root = new Directory("/", new HashMap<>()); + private Directory workingDirectory = root; + private final Map parentMap = new HashMap<>(); + } + + static abstract class File { + abstract int size(); + } + + static class Directory extends File { + private final String name; + private final Map files; + + public Directory(String name, Map files) { + this.name = name; + this.files = files; } + + int size() { + int result = 0; + for(final var file : files.values()) { + result += file.size(); + } + return result; + } + + public String toString() { + return "Directory{" + + "name='" + name + '\'' + + '}'; + } + + final Set findDirectoriesSmallerThan(final int maxSize) { + final var result = new HashSet(); + for(final var file : files.values()) { + try { + final var directory = (Directory)file; + result.addAll(directory.findDirectoriesSmallerThan(maxSize)); + } catch(final ClassCastException ignored) { + } + } + if(size() < maxSize) { // FIXME duplicated traversal + result.add(this); + } + return Collections.unmodifiableSet(result); + } + final Set findDirectoriesLargerThan(final int minSize) { + final var result = new HashSet(); + if(size() >= minSize) { + for (final var file : files.values()) { // FIXME duplicated traversal + try { + final var directory = (Directory) file; + result.addAll(directory.findDirectoriesLargerThan(minSize)); + } catch (final ClassCastException ignored) { + } + } + result.add(this); + } + return Collections.unmodifiableSet(result); + } } - protected static class Rule { - final String containerColour; - final Map containedCounts; + static class Leaf extends File { + private final String name; + private final int size; + + public Leaf(String name, int size) { + this.name = name; + this.size = size; + } + + int size() { + return size; + } + + @Override + public String toString() { + return "Leaf{" + + "name='" + name + '\'' + + ", size=" + size + + '}'; + } + } + + static abstract class Line { + static Line parse(final String line) { + if(line.startsWith("$")) { + return Command.parse(line); + } + return Output.parse(line); + } + abstract void execute(Session session); + } + + static abstract class Command extends Line { + static Command parse(final String line) { + if(line.startsWith("$ cd")) { + return ChangeDirectory.parse(line); + } + return ListContents.parse(line); + } + } - public Rule(final String containerColour, final Map containedCounts) { - // TODO validation - this.containerColour = containerColour; - this.containedCounts = containedCounts; + static class ListContents extends Command { + void execute(final Session session) { + } + static Command parse(final String ignored) { + return new ListContents(); } + } - public int countContained(final Map ruleMap) { - return 1 + containedCounts.entrySet().stream().mapToInt(entry -> { - final var containedColour = entry.getKey(); - final int multiplier = entry.getValue(); + static class ChangeDirectory extends Command { + private final String argument; - final var subRule = ruleMap.get(containedColour); - final int base = subRule.countContained(ruleMap); - return base * multiplier; - }).sum(); + public ChangeDirectory(String argument) { + this.argument = argument; } - public boolean canContain(final String colour, final Map ruleMap) { - if (containedCounts.containsKey(colour)) { - return true; + void execute(Session session) { + if("..".equals(argument)) { + final var parent = session.parentMap.get(session.workingDirectory); + if(parent == null) { + throw new IllegalArgumentException("Working directory has no parent: " + session.workingDirectory); + } + session.workingDirectory = parent; + } else if( "/".equals(argument)) { + session.workingDirectory = session.root; + } else { + final var target = (Directory) session.workingDirectory.files.get(argument); + if(target == null) { + throw new IllegalArgumentException("No directory named \"" + argument + "\" inside \"" + session.workingDirectory + "\""); + } + session.workingDirectory = target; } - return containedCounts.keySet() - .stream() - .map(ruleMap::get) - .anyMatch(rule -> rule != null && rule.canContain(colour, ruleMap)); } - public static Rule fromSentence(final String sentence) { - final var components = sentence.split(" bags contain ", 2); - if ("no other bags.".equalsIgnoreCase(components[1])) { - return new Rule(components[0], Collections.emptyMap()); + static ChangeDirectory parse(final String line) { + return new ChangeDirectory(line.split(" ")[2]); + } + } + + static abstract class Output extends Line { + static Output parse(final String line) { + if(line.startsWith("dir")) { + return DirectoryListing.parse(line); } - final var containedPhrases = components[1].split(", "); - final var containedCounts = Arrays.stream(containedPhrases) - .map(phrase -> phrase.replaceFirst(" bag.*$", "")) - .map(phrase -> phrase.split(" ", 2)) - .collect(HashMap::new, - (map, phraseComponents) -> map.put(phraseComponents[1], Integer.parseInt(phraseComponents[0])), - Map::putAll); - return new Rule(components[0], Collections.unmodifiableMap(containedCounts)); + return LeafListing.parse(line); } } + + static class DirectoryListing extends Output { + private final String name; + + public DirectoryListing(String name) { + this.name = name; + } + + void execute(Session session) { + final var directory = new Directory(name, new HashMap<>()); + session.parentMap.put(directory, session.workingDirectory); // TODO method on Session + session.workingDirectory.files.put(name, directory); + } + + static DirectoryListing parse(final String line) { + final var components = line.split(" "); + return new DirectoryListing(components[1]); + } + } + + static class LeafListing extends Output { + private final int size; + private final String name; + + public LeafListing(int size, String name) { + this.size = size; + this.name = name; + } + + void execute(Session session) { + session.workingDirectory.files.put(name, new Leaf(name, size)); + } + + static LeafListing parse(final String line) { + final var components = line.split(" "); + final var size = Integer.parseInt(components[0]); + final var name = components[1]; + return new LeafListing(size, name); + } + } + + protected Stream getInput() { + return StreamSupport + .stream(new LineSpliterator("day-07.txt"), + false) + .map(Line::parse); + } + + @Test + public final void part1() { + final var session = new Session(); + getInput().forEach(line -> line.execute(session)); + final var result = session.root.findDirectoriesSmallerThan(100_000).stream().mapToInt(Directory::size).sum(); + System.out.println("Part 1: " + result); + } + + @Test + public final void part2() { + final var session = new Session(); + getInput().forEach(line -> line.execute(session)); + final var consumed = session.root.size(); + final var unused = 70_000_000 - consumed; + final var required = 30_000_000 - unused; + final var result = session.root.findDirectoriesLargerThan(required) + .stream() + .min(Comparator.comparing(Directory::size)) + .get() + .size(); + System.out.println("Part 2: " + result); + } + } \ No newline at end of file diff --git a/src/test/java/com/macasaet/Day08.java b/src/test/java/com/macasaet/Day08.java index 290c2c2..98c498e 100644 --- a/src/test/java/com/macasaet/Day08.java +++ b/src/test/java/com/macasaet/Day08.java @@ -1,96 +1,161 @@ package com.macasaet; -import java.io.IOException; -import java.util.HashSet; +import org.junit.jupiter.api.Test; + import java.util.stream.Collectors; import java.util.stream.StreamSupport; +/** + * --- Day 8: Treetop Tree House --- + * https://adventofcode.com/2022/day/8 + */ public class Day08 { - public static void main(final String[] args) throws IOException { - try (var spliterator = new LineSpliterator(Day08.class.getResourceAsStream("/day-8-input.txt"))) { - final var instructions = - StreamSupport.stream(spliterator, false) - .map(Instruction::fromLine) - .collect(Collectors.toUnmodifiableList()); - - instructions: - // "exactly one instruction is corrupted" - try them all - for (int i = instructions.size(); --i >= 0; ) { - final var toReplace = instructions.get(i); - - if (toReplace.operation == Operation.acc) { - // "No acc instructions were harmed in the corruption of this boot code." - continue; - } - final var opposite = toReplace.operation == Operation.nop ? Operation.jmp : Operation.nop; - final var replacement = new Instruction(opposite, toReplace.argument); - - int total = 0; - int index = 0; - - final var visited = new HashSet(); - // "The program is supposed to terminate by attempting to execute an instruction immediately after the last instruction in the file." - while (index < instructions.size()) { - final var instruction = index == i ? replacement : instructions.get(index); - if (visited.contains(index)) { - // replacing the instruction causes an infinite loop - // try replacing a different one - // NB: Simply re-visiting this instruction is an infinite loop because the argument to each instruction is constant and never dependent on the value of "total" - continue instructions; + record Forest(int[][] grid) { + public int countVisible() { + int result = 0; + for(int i = grid().length; --i >= 0; ) { + final var row = grid()[i]; + for(int j = row.length; --j >= 0; ) { + if(isVisible(i, j)) { + result++; } - visited.add(index); - total = instruction.updateTotal(total); - index = instruction.updateIndex(index); } - System.out.println("part 2: " + total); - return; // "exactly one instruction is corrupted" } + return result; } - } - public enum Operation { - acc { - public int updateTotal(final int previousTotal, final int argument) { - return previousTotal + argument; + public int scenicScore(final int x, final int y) { + final int treeHeight = grid()[x][y]; + int northScore = 0; + for(int i = x; --i >= 0; ) { + final var height = grid()[i][y]; + northScore += 1; + if(height >= treeHeight) { + break; + } } - }, - nop, - jmp { - public int updateIndex(final int previousIndex, final int argument) { - return previousIndex + argument; + int southScore = 0; + for(int i = x + 1; i < grid().length; i++) { + final var height = grid()[i][y]; + southScore += 1; + if(height >= treeHeight) { + break; + } } - }; - - public int updateIndex(final int previousIndex, final int argument) { - return previousIndex + 1; + int westScore = 0; + for(int j = y; --j >= 0; ) { + final var height = grid()[x][j]; + westScore += 1; + if(height >= treeHeight) { + break; + } + } + int eastScore = 0; + for(int j = y + 1; j < grid()[x].length; j++) { + final var height = grid()[x][j]; + eastScore += 1; + if(height >= treeHeight) { + break; + } + } + return northScore * eastScore * southScore * westScore; } - public int updateTotal(final int previousTotal, final int argument) { - return previousTotal; + boolean isVisible(final int x, final int y) { + if(x == 0 || x == grid().length || y == 0 || y == grid()[x].length) { + // trees on the edge + return true; + } + final int treeHeight = grid()[x][y]; + if (!isObstructedFromTheNorth(x, y, treeHeight)) { + return true; + } + if (!isObstructedFromTheSouth(x, y, treeHeight)) { + return true; + } + if (!isObstructedFromTheWest(x, y, treeHeight)) { + return true; + } + if (!isObstructedFromTheEast(x, y, treeHeight)) { + return true; + } + return false; } - } - public static class Instruction { - private final Operation operation; - private final int argument; + private boolean isObstructedFromTheEast(int x, int y, int treeHeight) { + for(int j = grid()[x].length; --j > y; ) { + if(grid()[x][j] >= treeHeight) { + return true; + } + } + return false; + } - public Instruction(final Operation operation, final int argument) { - this.operation = operation; - this.argument = argument; + private boolean isObstructedFromTheWest(int x, int y, int treeHeight) { + for(int j = y; --j >= 0; ) { + if(grid()[x][j] >= treeHeight) { + return true; + } + } + return false; } - public static Instruction fromLine(final String line) { - final var components = line.split(" ", 2); - return new Instruction(Operation.valueOf(components[0]), Integer.parseInt(components[1])); + private boolean isObstructedFromTheSouth(int x, int y, int treeHeight) { + for(int i = grid().length; --i > x; ) { + if(grid()[i][y] >= treeHeight) { + return true; + } + } + return false; } - public int updateIndex(final int previousIndex) { - return operation.updateIndex(previousIndex, argument); + private boolean isObstructedFromTheNorth(int x, int y, int treeHeight) { + for(int i = x; --i >= 0; ) { + if(grid()[i][y] >= treeHeight) { + return true; + } + } + return false; } + } - public int updateTotal(final int previousTotal) { - return operation.updateTotal(previousTotal, argument); + protected Forest getInput() { + final var list = StreamSupport + .stream(new LineSpliterator("day-08.txt"), + false) + .map(line -> { + final var chars = line.toCharArray(); + final var row = new int[chars.length]; + for(int i = chars.length; --i >= 0; row[i] = chars[i] - '0'); + return row; + }) + .collect(Collectors.toList()); + final var grid = new int[list.size()][]; + for(int i = list.size(); --i >= 0; grid[i] = list.get(i)); + return new Forest(grid); + } + + @Test + public final void part1() { + final var forest = getInput(); + final var result = forest.countVisible(); + System.out.println("Part 1: " + result); + } + + @Test + public final void part2() { + final var forest = getInput(); + int result = Integer.MIN_VALUE; + for(int i = forest.grid().length; --i >= 0; ) { + for( int j = forest.grid.length; --j >= 0; ) { + final var score = forest.scenicScore(i, j); + if(score > result) { + result = score; + } + } } + System.out.println("Part 2: " + result); } + } \ No newline at end of file diff --git a/src/test/java/com/macasaet/Day09.java b/src/test/java/com/macasaet/Day09.java index ac86ab7..b6bcb00 100644 --- a/src/test/java/com/macasaet/Day09.java +++ b/src/test/java/com/macasaet/Day09.java @@ -1,57 +1,169 @@ package com.macasaet; -import java.io.IOException; -import java.util.stream.Collectors; +import org.junit.jupiter.api.Test; + +import java.util.Set; +import java.util.SortedMap; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.stream.Stream; import java.util.stream.StreamSupport; +/** + * --- Day 9: Rope Bridge --- + * https://adventofcode.com/2022/day/9 + */ public class Day09 { - public static void main(final String[] args) throws IOException { - try (var spliterator = new LineSpliterator(Day09.class.getResourceAsStream("/day-9-input.txt"))) { - final int preambleSize = 25; - final var list = StreamSupport.stream(spliterator, false) - .map(Long::parseLong) - .collect(Collectors.toUnmodifiableList()); - long invalid = Long.MIN_VALUE; - outer: for (int i = preambleSize; i < list.size(); i++) { - final var current = list.get(i); - final var valid = list - .subList(i - preambleSize, i) - .stream() - .filter(l -> l <= current) // no negative numbers in the input, so filter out anything larger than our target - .collect(Collectors.toUnmodifiableList()); - - for (int j = valid.size(); --j >= 1; ) { - final var x = valid.get(j); - for (int k = j; --k >= 0; ) { - final var y = valid.get(k); - if (x + y == current) { - continue outer; - } - } - } - invalid = current; - System.out.println("Part 1: " + invalid); - break; - } - outer: for (int i = list.size(); --i >= 1;) { - var total = list.get(i); - var min = total; - var max = total; - for (int j = i; --j >= 0;) { - final var current = list.get(j); - if( current < min ) min = current; - if( current > max ) max = current; - total += current; - if (total == invalid) { - final var result = min + max; - System.out.println("Part 2: " + result); - return; - } else if (total > invalid) { // I can only do this because there are no negative numbers - continue outer; - } + record Coordinate(int x, int y) { + + public int distance(final Coordinate other) { + return (int)Math.sqrt(Math.pow((double)x() - (double)other.x(), 2.0) + Math.pow((double)y() - (double)other.y(), 2.0)); + } + + public Coordinate step(final int xDistance, final int yDistance) { + return new Coordinate(x() + xDistance, y + yDistance); + } + public Coordinate stepTowards(final Coordinate leader) { + final var xDistance = Integer.compare(leader.x(), x()); + final var yDistance = Integer.compare(leader.y(), y()); + return step(xDistance, yDistance); + } + } + + public static class Rope { + + Coordinate[] knotCoordinates; + + final SortedMap> visited = new TreeMap<>(); + + public Rope(final int knots) { + knotCoordinates = new Coordinate[knots]; + for(int i = knots; --i >= 0; knotCoordinates[i] = new Coordinate(0, 0)); + visited.computeIfAbsent(0, (key) -> new TreeSet<>()).add(0); + } + + public int countVisited() { + int result = 0; + for( final var map : visited.values() ) { + result += map.size(); + } + return result; + } + + public void process(final Instruction instruction) { + final int xStep = instruction.direction().xStep(); + final int yStep = instruction.direction().yStep(); + + for(int i = instruction.distance(); --i >= 0; ) { + knotCoordinates[0] = knotCoordinates[0].step(xStep, yStep); + for(int j = 1; j < knotCoordinates.length; j++) { + moveKnot(j); } } } + + protected void moveKnot(int knotIndex) { + if(knotIndex <= 0) { + throw new IllegalArgumentException("Cannot move head"); + } + final var leader = knotCoordinates[knotIndex - 1]; + var follower = knotCoordinates[knotIndex]; + + if(leader.equals(follower)) { + return; + } else if (leader.distance(follower) <= 1) { + return; + } + + follower = follower.stepTowards(leader); + knotCoordinates[knotIndex] = follower; + + if(knotIndex == knotCoordinates.length - 1) { + visited.computeIfAbsent(follower.x(), (key) -> new TreeSet<>()).add(follower.y()); + } + } + } + + enum Direction { + Up { + int xStep() { + return -1; + } + int yStep() { + return 0; + } + }, + Down { + int xStep() { + return 1; + } + int yStep() { + return 0; + } + }, + Left { + int xStep() { + return 0; + } + int yStep() { + return -1; + } + }, + Right { + int xStep() { + return 0; + } + int yStep() { + return 1; + } + }; + + abstract int xStep(); + abstract int yStep(); + + static Direction parse(final String string) { + return switch(string.trim()) { + case "U" -> Up; + case "D" -> Down; + case "L" -> Left; + case "R" -> Right; + default -> throw new IllegalArgumentException("Invalid direction: " + string); + }; + } + } + + record Instruction(Direction direction, int distance) { + static Instruction parse(final String string) { + final var components = string.split(" "); + final var direction = Direction.parse(components[0]); + final var distance = Integer.parseInt(components[1]); + return new Instruction(direction, distance); + } + } + + protected Stream getInput() { + return StreamSupport + .stream(new LineSpliterator("day-09.txt"), + false) + .map(Instruction::parse); + } + + @Test + public final void part1() { + final var rope = new Rope(2); + getInput().forEach(rope::process); + final var result = rope.countVisited(); + System.out.println("Part 1: " + result); + } + + @Test + public final void part2() { + final var rope = new Rope(10); + getInput().forEach(rope::process); + final var result = rope.countVisited(); + System.out.println("Part 2: " + result); + } + } \ No newline at end of file diff --git a/src/test/java/com/macasaet/Day10.java b/src/test/java/com/macasaet/Day10.java index 66a5189..a289ed3 100644 --- a/src/test/java/com/macasaet/Day10.java +++ b/src/test/java/com/macasaet/Day10.java @@ -1,80 +1,153 @@ package com.macasaet; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.Collections; import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Stream; import java.util.stream.StreamSupport; +/** + * --- Day 10: Cathode-Ray Tube --- + * https://adventofcode.com/2022/day/10 + */ public class Day10 { - public static void main(final String[] args) throws IOException { - try (var spliterator = new LineSpliterator(Day10.class.getResourceAsStream("/day-10-input.txt"))) { - final var adapterJoltages = StreamSupport.stream(spliterator, false) - .map(Integer::parseInt) - .sorted() // ensure the next adapter in the chain is always to the right - .distinct() - .collect(Collectors.toUnmodifiableList()); - final var targetJoltage = adapterJoltages.get(adapterJoltages.size() - 1) + 3; - - int current = 0; - var distribution = new int[]{0, 0, 1}; // the last chain link has a difference of 3 - while (current < targetJoltage - 3) { - final var _current = current; - final var next = adapterJoltages - .stream() - .filter(candidate -> candidate - _current >= 1 && candidate - _current <= 3) - .findFirst() - .get(); - final var difference = next - current; - distribution[difference - 1]++; - current = next; + public enum Instruction { + noop { + public int cycles() { + return 0; + } + }, + addx { + public int cycles() { + return 2; } - System.out.println("Part 1: " + (distribution[0] * distribution[2])); - System.out.println("Part 2: " + count(adapterJoltages, 0, targetJoltage)); + }; + + public abstract int cycles(); + + public static Instruction parse(final String string) { + return Instruction.valueOf(string); } } - private static final Map cache = new HashMap<>(); + public record Operation(Instruction instruction, Integer argument) { + public List execute(int cycle, int register) { + return switch (instruction()) { + case noop -> Collections.singletonList(new CycleSnapshot(cycle + 1, register)); + case addx -> + List.of(new CycleSnapshot(cycle + 1, register), + new CycleSnapshot(cycle + 2, register + argument)); + }; + } - protected static int hash(final Collection adapters, final int from, final int to) { - int retval = 0; - retval = 31 * retval + from; - retval = 31 * retval + to; - retval = 31 * retval + adapters.hashCode(); - return retval; + public static Operation parse(final String line) { + final var components = line.split(" "); + final var instruction = Instruction.parse(components[0]); + Integer argument = null; + if(instruction == Instruction.addx) { + argument = Integer.parseInt(components[1]); + } + return new Operation(instruction, argument); + } } - protected static long count(final List adapters, final int from, final int to) { - final var hash = hash(adapters, from, to); - if (cache.containsKey(hash)) { - return cache.get(hash); + public record CycleSnapshot(int cycle, int register) { + public int signalStrength() { + return cycle() * register(); + } + } + + public static class State { + private int register = 1; + private int cycle = 1; + + public List getActivePixels() { + return Arrays.asList(register - 1, register, register + 1); } - if (adapters.isEmpty()) { - return from + 3 == to ? 1 : 0; - } else if (adapters.size() == 1) { - final int single = adapters.get(0); - return single == to - 3 ? 1 : 0; + public List execute(final Operation operation) { + final var result = operation.execute(cycle, register); + final var last = result.get(result.size() - 1); + cycle = last.cycle(); + register = last.register(); + return result; } + } + + public static class Display { + final char[][] pixels = new char[6][]; - long retval = 0; - for (int i = 3; --i >= 0; ) { - if (i >= adapters.size()) continue; - final int first = adapters.get(i); - final int difference = first - from; - if (difference >= 1 && difference <= 3) { - final var remaining = - i < adapters.size() - ? adapters.subList(i + 1, adapters.size()) - : new ArrayList(); - retval += count(remaining, first, to); + { + for(int i = pixels.length; --i >= 0; pixels[i] = new char[40]); + } + + public void update(final CycleSnapshot snapshot) { + final var pixelIndex = snapshot.cycle() - 1; + final var spritePositions = + Arrays.asList(snapshot.register() - 1, snapshot.register(), snapshot.register() + 1); + final int row = pixelIndex / 40; + final int column = pixelIndex % 40; + if(row >= pixels.length) { + return; + } + if(spritePositions.contains(column)) { + pixels[row][column] = '#'; + } else { + pixels[row][column] = '.'; } } - cache.put(hash, retval); - return retval; + public String toString() { + final var buffer = new StringBuilder(); + buffer.append(pixels[0], 0, 40); + buffer.append('\n'); + buffer.append(pixels[1], 0, 40); + buffer.append('\n'); + buffer.append(pixels[2], 0, 40); + buffer.append('\n'); + buffer.append(pixels[3], 0, 40); + buffer.append('\n'); + buffer.append(pixels[4], 0, 40); + buffer.append('\n'); + buffer.append(pixels[5], 0, 40); + return buffer.toString(); + } + } + + protected Stream getInput() { + return StreamSupport + .stream(new LineSpliterator("day-10.txt"), + false) + .map(Operation::parse); } + + @Test + public final void part1() { + final var interestingCycles = Arrays.asList(20, 60, 100, 140, 180, 220); + final var state = new State(); + final var accumulator = new AtomicInteger(0); + getInput().forEach(instruction -> { + final var sideEffects = state.execute(instruction); + for(final var sideEffect : sideEffects) { + if(interestingCycles.contains(sideEffect.cycle)) { + accumulator.addAndGet(sideEffect.signalStrength()); + } + } + }); + final var result = accumulator.get(); + System.out.println("Part 1: " + result); + } + + @Test + public final void part2() { + final var state = new State(); + final var display = new Display(); + display.update(new CycleSnapshot(1, 1)); + getInput().forEach(instruction -> state.execute(instruction).forEach(display::update)); + System.out.println("Part 2:\n" + display); + } + } \ No newline at end of file diff --git a/src/test/java/com/macasaet/Day11.java b/src/test/java/com/macasaet/Day11.java index a74e3e9..45db617 100644 --- a/src/test/java/com/macasaet/Day11.java +++ b/src/test/java/com/macasaet/Day11.java @@ -1,277 +1,189 @@ package com.macasaet; -import java.io.IOException; +import org.junit.jupiter.api.Test; + +import java.math.BigInteger; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BiFunction; +import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.StreamSupport; +/** + * --- Day 11: Monkey in the Middle --- + * https://adventofcode.com/2022/day/11 + */ public class Day11 { - public static void main(final String[] args) throws IOException { - try (var spliterator = new LineSpliterator(Day11.class.getResourceAsStream("/day-11-input.txt"))) { - final var list = StreamSupport.stream(spliterator, false).map(String::toCharArray).collect(Collectors.toList()); - final var original = list.toArray(new char[list.size()][]); - var current = copy(original); - while (true) { - final var transformed = transform(current); - if (areEqual(current, transformed)) { - break; - } - current = transformed; + public enum Operator implements BiFunction { + ADD { + public BigInteger apply(BigInteger x, BigInteger y) { + return x.add(y); } - System.out.println( "Part 1: " + countOccupiedSeats(current)); - current = copy(original); - while( true ) { - final var transformed = transform2(current); - if (areEqual(current, transformed)) { - break; - } - current = transformed; + }, + MULTIPLY { + public BigInteger apply(BigInteger x, BigInteger y) { + return x.multiply(y); } - System.out.println( "Part 2: " + countOccupiedSeats(current)); - } - } + }; - protected static char[][] copy(final char[][] original) { - final char[][] retval = new char[ original.length ][]; - for( int i = original.length; --i >= 0; ) { - final var row = original[ i ]; - final var copiedRow = new char[ row.length ]; - System.arraycopy(row, 0, copiedRow, 0, row.length); - retval[ i ] = copiedRow; + public static Operator parse(final String string) { + return switch(string) { + case "*" -> MULTIPLY; + case "+" -> ADD; + default -> throw new IllegalArgumentException("Invalid operator: " + string); + }; } - return retval; } - protected static char[][] transform2(final char[][] source) { - final char[][] retval = new char[source.length][]; - for (int i = retval.length; --i >= 0; retval[i] = new char[source[0].length]) ; - rows: for (int i = source.length; --i >= 0; ) { - final var row = source[i]; - columns: for (int j = row.length; --j >= 0; ) { - final var original = source[ i ][ j ]; - retval[ i ][ j ] = original; - if( original == '.' ) { - continue; - } else if( original == 'L' ) { - // North - for( int x = i; --x >= 0; ) { - final var visibleSeat = source[ x ][ j ]; - if( visibleSeat == 'L' ) break; - if( visibleSeat == '#' ) continue columns; - } - // North-West - for( int x = i, y = j; --x >= 0 && -- y >= 0; ) { - final var visibleSeat = source[ x ][ y ]; - if( visibleSeat == 'L' ) break; - if( visibleSeat == '#' ) continue columns; - } - // West - for( int y = j; --y >= 0; ) { - final var visibleSeat = source[ i ][ y ]; - if( visibleSeat == 'L' ) break; - if( visibleSeat == '#' ) continue columns; - } - // South-West - for( int x = i + 1, y = j - 1; x < source.length && y >= 0; x++, y-- ) { - final var visibleSeat = source[ x ][ y ]; - if( visibleSeat == 'L' ) break; - if( visibleSeat == '#' ) continue columns; - } - // South - for( int x = i + 1; x < source.length; x++ ) { - final var visibleSeat = source[ x ][ j ]; - if( visibleSeat == 'L' ) break; - if( visibleSeat == '#' ) continue columns; - } - // South-East - for( int x = i + 1, y = j + 1; x < source.length && y < row.length; x++, y++ ) { - final var visibleSeat = source[ x ][ y ]; - if( visibleSeat == 'L' ) break; - if( visibleSeat == '#' ) continue columns; - } - // East - for( int y = j + 1; y < row.length; y++ ) { - final var visibleSeat = source[ i ][ y ]; - if( visibleSeat == 'L' ) break; - if( visibleSeat == '#' ) continue columns; - } - // North-East - for( int x = i - 1, y = j + 1; x >= 0 && y < row.length; x--, y++ ) { - final var visibleSeat = source[ x ][ y ]; - if( visibleSeat == 'L' ) break; - if( visibleSeat == '#' ) continue columns; - } - retval[ i ][ j ] = '#'; - } else if( original == '#' ) { - int visibleNeighbours = 0; - // North - for( int x = i; --x >= 0 && visibleNeighbours < 5; ) { - final var visibleSeat = source[x][j]; - if( visibleSeat == '#' ) { - visibleNeighbours++; - break; - } else if( visibleSeat == 'L' ) { - break; - } - } - // North-West - for( int x = i, y = j; --x >= 0 && -- y >= 0 && visibleNeighbours < 5; ) { - final var visibleSeat = source[ x ][ y ]; - if( visibleSeat == '#' ) { - visibleNeighbours++; - break; - } else if( visibleSeat == 'L' ) { - break; - } - } - // West - for( int y = j; --y >= 0 && visibleNeighbours < 5; ) { - final var visibleSeat = source[i][y]; - if( visibleSeat == '#' ) { - visibleNeighbours++; - break; - } else if( visibleSeat == 'L' ) { - break; - } - } - // South-West - for( int x = i + 1, y = j - 1; x < source.length && y >= 0 && visibleNeighbours < 5; x++, y-- ) { - final var visibleSeat = source[x][y]; - if( visibleSeat == '#' ) { - visibleNeighbours++; - break; - } else if( visibleSeat == 'L' ) { - break; - } - } - // South - for( int x = i + 1; x < source.length && visibleNeighbours < 5; x++ ) { - final var visibleSeat = source[x][j]; - if( visibleSeat == '#' ) { - visibleNeighbours++; - break; - } else if( visibleSeat == 'L' ) { - break; - } - } - // South-East - for( int x = i + 1, y = j + 1; x < source.length && y < row.length && visibleNeighbours < 5; x++, y++ ) { - final var visibleSeat = source[x][y]; - if( visibleSeat == '#' ) { - visibleNeighbours++; - break; - } else if( visibleSeat == 'L' ) { - break; - } - } - // East - for( int y = j + 1; y < row.length && visibleNeighbours < 5; y++ ) { - final var visibleSeat = source[i][y]; - if( visibleSeat == '#' ) { - visibleNeighbours++; - break; - } else if( visibleSeat == 'L' ) { - break; - } - } - // North-East - for( int x = i - 1, y = j + 1; x >= 0 && y < row.length && visibleNeighbours < 5; x--, y++ ) { - final var visibleSeat = source[x][y]; - if( visibleSeat == '#' ) { - visibleNeighbours++; - break; - } else if( visibleSeat == 'L' ) { - break; - } - } - if( visibleNeighbours >= 5 ) { - retval[ i ][ j ] = 'L'; - } - } else { - throw new IllegalArgumentException("Unsupported char: " + original); - } + public record Operation(Operator operator, + Function lValueSupplier, + Function rValueSupplier) implements Function { - } + public BigInteger apply(final BigInteger oldValue) { + Objects.requireNonNull(oldValue); + final var lValue = lValueSupplier.apply(oldValue); + final var rValue = rValueSupplier.apply(oldValue); + return operator.apply(lValue, rValue); } - return retval; - } - protected static int countOccupiedSeats(final char[][] matrix) { - int retval = 0; - for( int i = matrix.length; --i >= 0; ) { - final var row = matrix[ i ]; - for( int j = row.length; --j >= 0; ) { - if( matrix[ i ][ j ] == '#' ) retval++; + public static Operation parse(String line) { + line = line.strip(); + if(!line.trim().startsWith("Operation:")) { + throw new IllegalArgumentException("Not an operation: " + line); } + final var components = line.split(" "); + final var lValueExpression = components[3]; + final Function lValueSupplier = "old".equalsIgnoreCase(lValueExpression) + ? old -> old + : ignored -> new BigInteger(lValueExpression); + final var operator = Operator.parse(components[4]); + final var rValueExpression = components[5]; + final Function rValueSupplier = "old".equalsIgnoreCase(rValueExpression) + ? old -> old + : ignored -> new BigInteger(rValueExpression); + return new Operation(operator, lValueSupplier, rValueSupplier); } - return retval; } - protected static void print(final char[][] matrix) { - for (int i = 0; i < matrix.length; i++) { - final var row = matrix[i]; - System.err.println(new String(row)); + /** + * An observation of how a single monkey behaves + * + * @param id the monkey's unique identifier + * @param items your worry level for each belonging currently held by this monkey + * @param operation a function describing how your worry level changes when the monkey inspects the item + * @param divisor used by the monkey to evaluate your worry level and decide what to do with the item + * @param targetIfTrue the ID of the monkey who will receive the item should the test pass + * @param targetIfFalse the ID of the monkey who will receive the item should the test fail + * @param itemsInspected the total number of times this monkey has inspected an item + */ + public record Monkey(int id, List items, Operation operation, BigInteger divisor, int targetIfTrue, int targetIfFalse, AtomicReference itemsInspected) { + public static Monkey parse(final String block) { + final var lines = block.split("\n"); + final var id = Integer.parseInt(lines[0].replaceAll("[^0-9]", "")); + final var startingItems = Arrays.stream(lines[1].strip() + .replaceAll("^Starting items: ", "") + .split(", ")) + .map(item -> new BigInteger(item)) + .collect(Collectors.toList()); // must be mutable + final var operation = Operation.parse(lines[2]); + final var divisor = new BigInteger(lines[3].replaceAll("[^0-9]", "")); + final var targetIfTrue = Integer.parseInt(lines[4].replaceAll("[^0-9]", "")); + final var targetIfFalse = Integer.parseInt(lines[5].replaceAll("[^0-9]", "")); + return new Monkey(id, startingItems, operation, divisor, targetIfTrue, targetIfFalse, new AtomicReference<>(BigInteger.ZERO)); } - } - protected static char[][] transform(final char[][] source) { - final char[][] retval = new char[source.length][]; - for (int i = retval.length; --i >= 0; retval[i] = new char[source[0].length]) ; - for (int i = source.length; --i >= 0; ) { - final var row = source[i]; - for (int j = row.length; --j >= 0; ) { - process(i, j, source, retval); - } + public BigInteger countItemsInspected() { + return itemsInspected.get(); } - return retval; - } - protected static boolean areEqual(final char[][] x, final char[][] y) { - for (int i = x.length; --i >= 0; ) { - final var row = x[i]; - for (int j = row.length; --j >= 0; ) { - if (x[i][j] != y[i][j]) { - return false; - } + public Throw inspectItem(BigInteger reliefFactor) { + // this assumes monkeys can throw items to themselves + if(items.isEmpty()) { + return null; } + var worryLevel = items().remove(0); + worryLevel = operation().apply(worryLevel); + worryLevel = worryLevel.divide(reliefFactor); + final var target = worryLevel.mod(divisor()).equals(BigInteger.ZERO) + ? targetIfTrue() + : targetIfFalse(); + itemsInspected().updateAndGet(old -> old.add(BigInteger.ONE)); + return new Throw(target, worryLevel); } - return true; - } - protected static void process(final int x, final int y, final char[][] source, final char[][] target) { - final var original = source[x][y]; - if (original == '.') { - target[x][y] = '.'; - return; - } else if (original == 'L') { - target[x][y] = !isOccupied(x - 1, y - 1, source) && !isOccupied(x - 1, y, source) - && !isOccupied(x - 1, y + 1, source) && !isOccupied(x, y - 1, source) && !isOccupied(x, y + 1, source) - && !isOccupied(x + 1, y - 1, source) && !isOccupied(x + 1, y, source) && !isOccupied(x + 1, y + 1, source) ? '#' : original; - } else if (original == '#') { - int occupiedNeighbors = 0; - occupiedNeighbors += isOccupied(x - 1, y - 1, source) ? 1 : 0; - occupiedNeighbors += isOccupied(x - 1, y, source) ? 1 : 0; - occupiedNeighbors += isOccupied(x - 1, y + 1, source) ? 1 : 0; - occupiedNeighbors += isOccupied(x, y - 1, source) ? 1 : 0; - occupiedNeighbors += isOccupied(x, y + 1, source) ? 1 : 0; - occupiedNeighbors += isOccupied(x + 1, y - 1, source) ? 1 : 0; - occupiedNeighbors += isOccupied(x + 1, y, source) ? 1 : 0; - occupiedNeighbors += isOccupied(x + 1, y + 1, source) ? 1 : 0; - target[x][y] = occupiedNeighbors >= 4 ? 'L' : original; - } else { - throw new IllegalArgumentException("Unsupported char: " + original); + public List inspectItems(Function worryUpdater) { + // this assumes monkeys cannot throw items to themselves + final var result = items().stream().map(worryLevel -> { + worryLevel = operation().apply(worryLevel); + worryLevel = worryUpdater.apply(worryLevel); + final var target = worryLevel.mod(divisor()).equals(BigInteger.ZERO) + ? targetIfTrue() + : targetIfFalse(); + return new Throw(target, worryLevel); + }).toList(); + itemsInspected().updateAndGet(old -> old.add(BigInteger.valueOf(result.size()))); + items().clear(); + return result; } + + } + + public record Throw(int target, BigInteger itemWorryLevel) { + } + + protected List getInput() { + final var input = StreamSupport + .stream(new LineSpliterator("day-11.txt"), + false).collect(Collectors.joining("\n")); + return Arrays.stream(input.split("\n\n")) + .map(Monkey::parse) + .toList(); } - protected static boolean isOccupied(final int x, final int y, final char[][] matrix) { - if (x < 0 || y < 0 || x >= matrix.length) { - return false; + @Test + public final void part1() { + final var monkeys = getInput(); + final Function worryUpdater = worryLevel -> worryLevel.divide(BigInteger.valueOf(3)); + for(int i = 20; --i >= 0; ) { + for(final var monkey : monkeys) { + for(final var toss : monkey.inspectItems(worryUpdater)) { + monkeys.get(toss.target()).items().add(toss.itemWorryLevel()); + } + } } - final char[] row = matrix[x]; - if (y >= row.length) { - return false; + final var result = monkeys.stream() + .map(Monkey::countItemsInspected) + .sorted(Comparator.reverseOrder()) + .limit(2) + .reduce(BigInteger::multiply) + .get(); + System.out.println("Part 1: " + result); + } + + @Test + public final void part2() { + final var monkeys = getInput(); + final var productOfDivisors = monkeys.stream().map(Monkey::divisor).reduce(BigInteger::multiply).get(); + final Function worryUpdater = worryLevel -> worryLevel.mod(productOfDivisors); + for(int i = 10_000; --i >= 0; ) { + for(final var monkey : monkeys) { + for(final var toss : monkey.inspectItems(worryUpdater)) { + monkeys.get(toss.target()).items().add(toss.itemWorryLevel()); + } + } } - return row[y] == '#'; + final var result = monkeys.stream() + .map(Monkey::countItemsInspected) + .sorted(Comparator.reverseOrder()) + .limit(2) + .reduce(BigInteger::multiply) + .get(); + System.out.println("Part 2: " + result); } } \ No newline at end of file diff --git a/src/test/java/com/macasaet/Day12.java b/src/test/java/com/macasaet/Day12.java index c494842..3257259 100644 --- a/src/test/java/com/macasaet/Day12.java +++ b/src/test/java/com/macasaet/Day12.java @@ -1,200 +1,164 @@ package com.macasaet; -import java.io.IOException; -import java.util.stream.Collectors; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Objects; +import java.util.PriorityQueue; import java.util.stream.StreamSupport; +/** + * --- Day 12: Hill Climbing Algorithm --- + * https://adventofcode.com/2022/day/12 + */ public class Day12 { - public static void main(final String[] args) throws IOException { - try (var spliterator = new LineSpliterator(Day12.class.getResourceAsStream("/day-12-input.txt"))) { - // 0, 360 % 360 = North - // 90 = East - // 180 = South - // 270 = West - int direction = 90; - int x = 0; // positive means East, negative means West - int y = 0; // positive means North, negative means South - - - final var actions = StreamSupport.stream(spliterator, false) - .map(Action::fromLine) - .collect(Collectors.toUnmodifiableList()); - for (final var action : actions) { - switch (action.actionCode) { - case 'N': - y += action.value; - break; - case 'S': - y -= action.value; - break; - case 'E': - x += action.value; - break; - case 'W': - x -= action.value; - break; - case 'L': - final int relativeDirection = direction - action.value; - direction = relativeDirection < 0 ? relativeDirection + 360 : relativeDirection; - break; - case 'R': - direction = (direction + action.value) % 360; - break; - case 'F': - switch (direction) { - case 0: - y += action.value; - break; - case 90: - x += action.value; - break; - case 180: - y -= action.value; - break; - case 270: - x -= action.value; - break; - default: - throw new IllegalStateException("Unhandled direction: " + direction); + record Coordinate(int x, int y) { + } + + public record HeightMap(int[][] grid, Coordinate start, Coordinate end) { + + public int lengthOfShortestPath() { + return this.lengthOfShortestPath(this.start); + } + + public int lengthOfShortestPath(final Coordinate startingPoint) { + final var cheapestCostToNode = new HashMap(); + cheapestCostToNode.put(startingPoint, 0); + final var estimatedCostToFinish = new HashMap(); + estimatedCostToFinish.put(startingPoint, estimateDistance(startingPoint, this.end)); + final var openSet = new PriorityQueue((x, y) -> { + final var xScore = estimatedCostToFinish.getOrDefault(x, Integer.MAX_VALUE); + final var yScore = estimatedCostToFinish.getOrDefault(y, Integer.MAX_VALUE); + return xScore.compareTo(yScore); + }); + openSet.add(startingPoint); + while (!openSet.isEmpty()) { + final var current = openSet.remove(); + if (current.equals(this.end)) { + return cheapestCostToNode.get(current); + } + for (final var neighbour : neighbours(current)) { + final var tentativeGScore = cheapestCostToNode.get(current) + 1; + if (tentativeGScore < cheapestCostToNode.getOrDefault(neighbour, Integer.MAX_VALUE)) { + cheapestCostToNode.put(neighbour, tentativeGScore); + estimatedCostToFinish.put(neighbour, tentativeGScore + estimateDistance(neighbour, this.end)); + if (!openSet.contains(neighbour)) { + openSet.add(neighbour); + } } - break; - default: - throw new IllegalArgumentException("Invalid action: " + action.actionCode); + } } + return Integer.MAX_VALUE; } - System.out.println("Part 1: " + (Math.abs(x) + Math.abs(y))); - - // waypoint coordinates - int wx = 10; // positive means North, negative means South - int wy = 1; // positive means East, negative means West - - // ship coordinates - int sx = 0; - int sy = 0; - - for (final var action : actions) { - int xDiff = wx - sx; - int yDiff = wy - sy; - switch (action.actionCode) { - case 'N': - wy += action.value; - break; - case 'S': - wy -= action.value; - break; - case 'E': - wx += action.value; - break; - case 'W': - wx -= action.value; - break; - case 'L': - for (int i = action.value / 90; --i >= 0; ) { - wx = sx - yDiff; - wy = sy + xDiff; - xDiff = wx - sx; - yDiff = wy - sy; - } - break; - case 'R': - for (int i = action.value / 90; --i >= 0; ) { - wx = sx + yDiff; - wy = sy - xDiff; - xDiff = wx - sx; - yDiff = wy - sy; + + public List getPotentialTrailHeads() { + final var list = new ArrayList(); + for(int i = this.grid().length; --i >= 0; ) { + final var row = this.grid()[i]; + for(int j = row.length; --j >= 0; ) { + if(row[j] == 0) { + list.add(new Coordinate(i, j)); } - break; - case 'F': - sx += xDiff * action.value; - sy += yDiff * action.value; - wx = sx + xDiff; - wy = sy + yDiff; - break; - default: - throw new IllegalArgumentException("Unsupported argument: " + action.actionCode); + } } + return Collections.unmodifiableList(list); + } + + int height(final Coordinate coordinate) { + return this.grid[coordinate.x()][coordinate.y()]; + } + + List neighbours(final Coordinate coordinate) { + final var list = new ArrayList(4); + if (coordinate.x() > 0) { + final var up = new Coordinate(coordinate.x() - 1, coordinate.y()); + if (height(coordinate) + 1 >= height(up)) { + list.add(up); + } + } + if (coordinate.x() < this.grid.length - 1) { + final var down = new Coordinate(coordinate.x() + 1, coordinate.y()); + if (height(coordinate) + 1 >= height(down)) { + list.add(down); + } + } + if (coordinate.y() > 0) { + final var left = new Coordinate(coordinate.x(), coordinate.y() - 1); + if (height(coordinate) + 1 >= height(left)) { + list.add(left); + } + } + final var row = this.grid[coordinate.x()]; + if (coordinate.y() < row.length - 1) { + final var right = new Coordinate(coordinate.x(), coordinate.y() + 1); + if (height(coordinate) + 1 >= height(right)) { + list.add(right); + } + } + return Collections.unmodifiableList(list); } - System.out.println("Part 2: " + (Math.abs(sx) + Math.abs(sy))); - } - } - protected static class Action { - final char actionCode; - final int value; + int estimateDistance(final Coordinate from, final Coordinate to) { + return (int) Math.sqrt(Math.pow(from.x() - to.x(), 2.0) + Math.pow(from.y() - to.y(), 2.0)); + } - public Action(final char actionCode, final int value) { - this.actionCode = actionCode; - this.value = value; } - public static Action fromLine(final String line) { - final var direction = line.charAt(0); - final var valueString = line.substring(1); - final int value = Integer.parseInt(valueString); - return new Action(direction, value); + protected HeightMap getInput() { + final var charGrid = StreamSupport.stream(new LineSpliterator("day-12.txt"), false).map(line -> { + final var list = new ArrayList(line.length()); + for (final var c : line.toCharArray()) { + list.add(c); + } + return list; + }).toList(); + Coordinate origin = null; + Coordinate destination = null; + int[][] grid = new int[charGrid.size()][]; + for(int i = charGrid.size(); --i >= 0; ) { + final var row = charGrid.get(i); + grid[i] = new int[row.size()]; + for(int j = row.size(); --j >= 0; ) { + final char c = row.get(j); + if(c == 'S') { + origin = new Coordinate(i, j); + grid[i][j] = 0; + } else if(c == 'E') { + destination = new Coordinate(i, j); + grid[i][j] = 'z' - 'a'; + } else { + grid[i][j] = c - 'a'; + } + } } + Objects.requireNonNull(origin); + Objects.requireNonNull(destination); + return new HeightMap(grid, origin, destination); + } -// public SimpleNavState navigate(final SimpleNavState current) { -// switch (actionCode) { -// case 'N': -// return new SimpleNavState(current.x, current.y + value, actionCode); -// case 'S': -// return new SimpleNavState(current.x, current.y - value, actionCode); -// case 'E': -// return new SimpleNavState(current.x + value, current.y, actionCode); -// case 'W': -// return new SimpleNavState(current.x - value, current.y, actionCode); -// case 'L': -// final int relativeDirection = current.direction - value; -// final int direction = relativeDirection < 0 ? relativeDirection + 360 : relativeDirection; -// return new SimpleNavState(current.x, current.y, direction); -// case 'R': -// return new SimpleNavState(current.x, current.y, (current.direction + value) % 360); -// case 'F': -// switch (current.direction) { -// case 0: -// return new SimpleNavState(current.x, current.y + value, current.direction); -// case 90: -// return new SimpleNavState(current.x + value, current.y, current.direction); -// case 180: -// return new SimpleNavState(current.x, current.y - value, current.direction); -// case 270: -// return new SimpleNavState(current.x - value, current.y, current.direction); -// default: -// throw new IllegalStateException("Unhandled direction: " + current.direction); -// } -// default: -// throw new IllegalArgumentException("Invalid action: " + current.direction); -// } -// } + @Test + public final void part1() { + final var map = getInput(); + final var result = map.lengthOfShortestPath(); + System.out.println("Part 1: " + result); + } + @Test + public final void part2() { + final var map = getInput(); + var result = Integer.MAX_VALUE; + for(final var candidate : map.getPotentialTrailHeads()) { + final var length = map.lengthOfShortestPath(candidate); + if(length < result) { + result = length; + } + } + + System.out.println("Part 2: " + result); } -// -// protected static class SimpleNavState { -// private final int x; -// private final int y; -// private final int direction; -// -// public SimpleNavState(int x, int y, int direction) { -// this.x = x; -// this.y = y; -// this.direction = direction; -// } -// } -// -// protected static class AdvancedNavState { -// private final int sx; -// private final int sy; -// private final int wx; -// -// public AdvancedNavState(int sx, int sy, int wx, int wy) { -// this.sx = sx; -// this.sy = sy; -// this.wx = wx; -// this.wy = wy; -// } -// -// private final int wy; -// } + } \ No newline at end of file diff --git a/src/test/java/com/macasaet/Day13.java b/src/test/java/com/macasaet/Day13.java index cfb7520..457145f 100644 --- a/src/test/java/com/macasaet/Day13.java +++ b/src/test/java/com/macasaet/Day13.java @@ -1,93 +1,161 @@ package com.macasaet; -import java.io.IOException; +import org.junit.jupiter.api.Test; + +import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; import java.util.List; import java.util.stream.Collectors; +import java.util.stream.Stream; import java.util.stream.StreamSupport; +/** + * --- Day 13: Distress Signal --- + * https://adventofcode.com/2022/day/13 + */ public class Day13 { - public static void main(final String[] args) throws IOException { - try (var spliterator = new LineSpliterator(Day13.class.getResourceAsStream("/day-13-input.txt"))) { - final var lines = StreamSupport.stream(spliterator, false).collect(Collectors.toUnmodifiableList()); - final int earliestDeparture = Integer.parseInt(lines.get(0)); - final var idsString = lines.get(1); - final var idStrings = idsString.split(","); - final var candidate = Arrays.stream(idStrings).filter(busId -> !"x".equalsIgnoreCase(busId)).map(Integer::parseInt).map(busId -> { - if (earliestDeparture % busId == 0) { - return new Candidate(busId, 0); + public record Pair(ListItem x, ListItem y) { + static Pair parse(final String lines) { + final var components = lines.split("\n"); + final var x = ListItem.parse(components[0]); + final var y = ListItem.parse(components[1]); + return new Pair(x, y); + } + + public boolean isInOrder() { + return x().compareTo(y()) < 0; + } + + public Stream stream() { + return Stream.of(x(), y()).sorted(); + } + } + + interface Item extends Comparable { + + int compareToList(ListItem other); + int compareToLiteral(Literal other); + + default int compareTo(Item other) { + if(other instanceof ListItem) { + return compareToList((ListItem)other); + } else { + if(!(other instanceof Literal)) { + throw new IllegalArgumentException("Unknown implementation"); } - final int d = earliestDeparture / busId; - return new Candidate(busId, ((d + 1) * busId) - earliestDeparture); - }).sorted().findFirst().get(); - final int result = candidate.busId * candidate.timeToWait; - System.out.println("Part 1: " + result); - - final List list = new ArrayList(idStrings.length); - for (int i = 0; i < idStrings.length; i++) { - final var idString = idStrings[i]; - if ("x".equalsIgnoreCase(idString)) continue; - final var busId = Long.parseLong(idString); - list.add(new Bus(busId, i)); + return compareToLiteral((Literal) other); } - long sum = 0; - long productOfModSpaces = 1; - for (int i = list.size(); --i >= 0; ) { - final var bus = list.get(i); - productOfModSpaces *= bus.busId; - long remainder = bus.busId - bus.index; - remainder = remainder % bus.busId; - long productOfOtherModSpaces = 1; - for (int j = list.size(); --j >= 0; ) { - if (j == i) continue; - final var otherBus = list.get(j); - productOfOtherModSpaces *= otherBus.busId; + } + } + + public record ListItem(List items) implements Item { + public static ListItem parse(final String string) { + final var stack = new ArrayDeque(); + StringBuilder numberBuffer = new StringBuilder(); + for(final char c : string.toCharArray()) { + if(c == '[') { + stack.push(new ListItem(new ArrayList<>())); + } else if(c == ']') { + if(!numberBuffer.isEmpty()) { + final var numberString = numberBuffer.toString(); + numberBuffer.delete(0, numberBuffer.length()); + final var number = Integer.parseInt(numberString); + stack.peek().items().add(new Literal(number)); + } + if(stack.size() > 1) { + final var completed = stack.pop(); + stack.peek().items().add(completed); + } + } else if(c == ',') { + if(!numberBuffer.isEmpty()) { + final var numberString = numberBuffer.toString(); + numberBuffer.delete(0, numberBuffer.length()); + final var number = Integer.parseInt(numberString); + stack.peek().items().add(new Literal(number)); + } + } else { + numberBuffer.append(c); } - final long inverse = findInverse(productOfOtherModSpaces, bus.busId); - sum += remainder * productOfOtherModSpaces * inverse; - } - while (true) { - final long smallerSolution = sum - productOfModSpaces; - if (smallerSolution < 0) break; - sum = smallerSolution; } - System.out.println("Part 2: " + sum); + return stack.pop(); } - } - protected static long findInverse(final long partialProduct, final long modSpace) { - for (long multiplier = 1; ; multiplier++) { - if ((partialProduct * multiplier) % modSpace == 1) { - return multiplier; + public int compareToList(ListItem other) { + final Iterator x = this.items().iterator(); + final Iterator y = other.items().iterator(); + while(x.hasNext() && y.hasNext()) { + final var xItem = x.next(); + final var yItem = y.next(); + final var comparison = xItem.compareTo(yItem); + if(comparison != 0) { + return comparison; + } } + if(y.hasNext()) { + return -1; + } else if(x.hasNext()) { + return 1; + } + return 0; } + + public int compareToLiteral(Literal other) { + return compareToList(other.asList()); + } + } - protected static class Bus { - final long busId; - final long index; + public record Literal(int item) implements Item { + public int compareToList(ListItem other) { + return asList().compareToList(other); + } - public Bus(final long busId, final long index) { - this.busId = busId; - this.index = index; + public int compareToLiteral(Literal other) { + return Integer.compare(item(), other.item()); } + public ListItem asList() { + return new ListItem(Collections.singletonList(this)); + } } - protected static class Candidate implements Comparable { - final int busId; - final int timeToWait; + protected List getInput() { + final var lines = StreamSupport.stream(new LineSpliterator("day-13.txt"), false) + .collect(Collectors.joining("\n")); + final var blocks = lines.split("\n\n"); + return Arrays.stream(blocks).map(block -> Pair.parse(block)).toList(); + } - public Candidate(final int busId, final int timeToWait) { - this.busId = busId; - this.timeToWait = timeToWait; + @Test + public final void part1() { + final var pairs = getInput(); + var result = 0; + for(int i = 0; i < pairs.size(); i++) { + final var pair = pairs.get(i); + if(pair.isInOrder()) { + result += i + 1; + } } + System.out.println("Part 1: " + result); + } - public int compareTo(Candidate other) { - return Integer.compare(timeToWait, other.timeToWait); - } + @Test + public final void part2() { + final var pairs = getInput(); + final var packets = pairs.stream().flatMap(Pair::stream).sorted().toList(); + final int leftSearchResult = Collections.binarySearch(packets, + new ListItem(Collections.singletonList(new ListItem(Collections.singletonList(new Literal(2)))))); + final int leftInsertionPoint = -(leftSearchResult + 1) + 1; + final int rightSearchResult = Collections.binarySearch(packets, + new ListItem(Collections.singletonList(new ListItem(Collections.singletonList(new Literal(6)))))); + final int rightInsertionPoint = -(rightSearchResult + 1) + 2; + final int result = leftInsertionPoint * rightInsertionPoint; + + System.out.println("Part 2: " + result); } } \ No newline at end of file diff --git a/src/test/java/com/macasaet/Day14.java b/src/test/java/com/macasaet/Day14.java index a86ae7f..7f7d777 100644 --- a/src/test/java/com/macasaet/Day14.java +++ b/src/test/java/com/macasaet/Day14.java @@ -1,127 +1,212 @@ package com.macasaet; -import java.io.IOException; -import java.math.BigInteger; -import java.util.ArrayList; +import org.junit.jupiter.api.Test; + import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; -import java.util.stream.Collectors; +import java.util.Map; import java.util.stream.StreamSupport; +/** + * --- Day 14: Regolith Reservoir --- + * https://adventofcode.com/2022/day/14 + */ public class Day14 { - public static void main(final String[] args) throws IOException { - try (var spliterator = new LineSpliterator(Day14.class.getResourceAsStream("/day-14-input.txt"))) { - final var memory = new HashMap(); - char[] mask = null; - final var lines = StreamSupport.stream(spliterator, false).collect(Collectors.toUnmodifiableList()); - for (final var line : lines) { - final var components = line.split(" = "); - final var stringValue = components[1].strip(); - if ("mask".equalsIgnoreCase(components[0].strip())) { - mask = stringValue.strip().toCharArray(); - } else { - final var address = new BigInteger(components[0].replaceAll("[^0-9]", "")); - var binaryString = Integer.toBinaryString(Integer.parseInt(stringValue)); - final int padSize = 36 - binaryString.length(); - final var pad = new char[padSize]; - for (int i = padSize; --i >= 0; pad[i] = '0') ; - binaryString = new String(pad) + binaryString; - - final var valueBinary = new char[36]; - for (int j = 36; --j >= 0; ) { - if (mask[j] == 'X') { - valueBinary[j] = binaryString.charAt(j); - } else { - valueBinary[j] = mask[j]; + public enum Cell { + ROCK, + SAND + } + + record Coordinate(int verticalDepth, int horizontalOffset) { + + public static Coordinate parse(final String string) { + final var components = string.split(","); + final var verticalDepth = Integer.parseInt(components[1]); + final var horizontalOffset = Integer.parseInt(components[0]); + return new Coordinate(verticalDepth, horizontalOffset); + } + } + + public record Cave(Map> grid, int maxDepth, int minHorizontalOffset, int maxHorizontalOffset) { + + public int pourSandIntoAbyss() { + int settledSand = 0; + while(true) { + var fallingSandCoordinate = new Coordinate(0, 500); + while(true) { + final var next = getNextCoordinate(fallingSandCoordinate, null); + if(next != null) { + fallingSandCoordinate = next; + if(fallingSandCoordinate.verticalDepth() >= maxDepth()) { + return settledSand; } + } else { + final var row = grid.computeIfAbsent(fallingSandCoordinate.verticalDepth(), key -> new HashMap<>()); + row.put(fallingSandCoordinate.horizontalOffset(), Cell.SAND); + settledSand++; + break; } - - final var result = toInt(valueBinary); - memory.put(address, result); } } - var sum = memory.values().stream().reduce(BigInteger::add).get(); - System.out.println("Part 1: " + sum); - - memory.clear(); - mask = null; - for (final var line : lines) { - final var components = line.split(" = "); - final var stringValue = components[1].strip(); - if ("mask".equalsIgnoreCase(components[0].strip())) { - mask = stringValue.toCharArray(); - } else { - final var addressDecimal = Integer.parseInt(components[0].strip().replaceAll("[^0-9]", "")); - var addressBinaryString = Integer.toBinaryString(addressDecimal); - final int padSize = 36 - addressBinaryString.length(); - final var addressSpec = new char[36]; - for (int i = 0; i < padSize; addressSpec[i++] = '0') ; - for (int i = 0; i < addressBinaryString.length(); i++) { - addressSpec[i + padSize] = addressBinaryString.charAt(i); + } + + public int fillAperture() { + int settledSand = 0; + while(true) { + var fallingSandCoordinate = new Coordinate(0, 500); + while(true) { + final var next = getNextCoordinate(fallingSandCoordinate, floorDepth()); + if(next != null) { + fallingSandCoordinate = next; + } else { + final var secondRow = grid().computeIfAbsent(1, key -> new HashMap<>()); + if(secondRow.containsKey(499) && secondRow.containsKey(500) && secondRow.containsKey(501)) { + return settledSand + 1; + } + final var row = grid.computeIfAbsent(fallingSandCoordinate.verticalDepth(), key -> new HashMap<>()); + row.put(fallingSandCoordinate.horizontalOffset(), Cell.SAND); + settledSand++; + break; } - for (int i = 36; --i >= 0; ) { - if (mask[i] == '1') { - addressSpec[i] = '1'; - } else if (mask[i] == 'X') { - addressSpec[i] = 'X'; + } + } + } + + int floorDepth() { + return maxDepth() + 2; + } + + Coordinate getNextCoordinate(final Coordinate start, Integer floorDepth) { + final var x = start.verticalDepth(); + final var y = start.horizontalOffset(); + if(floorDepth != null && x + 1 >= floorDepth) { + return null; + } + final var nextRow = grid().computeIfAbsent(x + 1, key -> new HashMap<>()); + if(!nextRow.containsKey(y)) { + return new Coordinate(x + 1, y); + } else if(!nextRow.containsKey(y - 1)) { + return new Coordinate(x + 1, y - 1); + } else if(!nextRow.containsKey(y + 1)) { + return new Coordinate(x + 1, y + 1); + } + return null; + } + + public static Cave parse(final Collection lines) { + int maxDepth = 0; + int maxHorizontalOffset = Integer.MIN_VALUE; + int minHorizontalOffset = Integer.MAX_VALUE; + + final var grid = new HashMap>(); + for(final var line : lines) { + final var rockPath = parseRockPaths(line); + var last = rockPath.get(0); + if(last.verticalDepth() > maxDepth) { + maxDepth = last.verticalDepth(); + } + if(last.horizontalOffset() < minHorizontalOffset) { + minHorizontalOffset = last.horizontalOffset(); + } + if(last.horizontalOffset() > maxHorizontalOffset) { + maxHorizontalOffset = last.horizontalOffset(); + } + for(int i = 1; i < rockPath.size(); i++) { + final var current = rockPath.get(i); + if(last.verticalDepth() == current.verticalDepth()) { + // horizontal line + int start; + int end; + if(last.horizontalOffset() < current.horizontalOffset()) { + start = last.horizontalOffset(); + end = current.horizontalOffset(); + } else { + start = current.horizontalOffset(); + end = last.horizontalOffset(); + } + final var row = grid.computeIfAbsent(last.verticalDepth(), key -> new HashMap<>()); + for(int y = start; y <= end; y++) { + row.put(y, Cell.ROCK); + } + } else { + if(last.horizontalOffset() != current.horizontalOffset()) { + throw new IllegalStateException("Line segments are not on the same vertical axis"); + } + // vertical line + int start; + int end; + if(last.verticalDepth() < current.verticalDepth()) { + start = last.verticalDepth(); + end = current.verticalDepth(); + } else { + start = current.verticalDepth(); + end = last.verticalDepth(); + } + for(int x = start; x <= end; x++) { + final var row = grid.computeIfAbsent(x, key -> new HashMap<>()); + row.put(last.horizontalOffset(), Cell.ROCK); } } - final var value = toInt(Integer.parseInt(components[1].strip())); - for (final var address : explode(addressSpec)) { - memory.put(address, value); + if(current.verticalDepth() > maxDepth) { + maxDepth = current.verticalDepth(); } + if(current.horizontalOffset() < minHorizontalOffset) { + minHorizontalOffset = current.horizontalOffset(); + } + if(current.horizontalOffset() > maxHorizontalOffset) { + maxHorizontalOffset = current.horizontalOffset(); + } + last = current; } } - sum = memory.values().stream().reduce(BigInteger::add).get(); - System.out.println("Part 2: " + sum); + return new Cave(grid, maxDepth, minHorizontalOffset, maxHorizontalOffset); } - } - - protected static BigInteger toInt(final int decimal) { - final var string = Integer.toBinaryString(decimal); - return toInt(string.toCharArray()); - } - protected static BigInteger toInt(final char[] chars) { - var retval = BigInteger.ZERO; - for (int i = chars.length; --i >= 0; ) { - final int power = chars.length - i - 1; - final var multiplier = chars[i] == '0' ? BigInteger.ZERO : BigInteger.ONE; - retval = retval.add(BigInteger.TWO.pow(power).multiply(multiplier)); + static List parseRockPaths(final String line) { + return Arrays.stream(line.split(" -> ")).map(Coordinate::parse).toList(); } - return retval; - } - protected static List explode(final char[] chars) { - final var floatingIndices = new ArrayList(); - for (int i = 36; --i >= 0; ) { - if (chars[i] == 'X') { - floatingIndices.add(i); + @Override + public String toString() { + final var buffer = new StringBuilder(); + for(int i = 0; i <= floorDepth(); i++) { + buffer.append(i).append(' '); + final var row = grid.getOrDefault(i, Collections.emptyMap()); + for(int j = minHorizontalOffset(); j <= maxHorizontalOffset(); j++) { + final var cell = row.get(j); + final char marker = cell == null ? ' ' : Cell.ROCK.equals(cell) ? '#' : 'o'; + buffer.append(marker); + } + buffer.append('\n'); } + return buffer.toString(); } - return explode(chars, floatingIndices); } - protected static List explode(final char[] chars, final List floatingIndices) { - if (floatingIndices.isEmpty()) { - return Collections.singletonList(toInt(chars)); - } - final var index = floatingIndices.get(0); + protected Cave getInput() { + final var lines = StreamSupport.stream(new LineSpliterator("day-14.txt"), false) + .toList(); + return Cave.parse(lines); + } - var list = new ArrayList(); + @Test + public final void part1() { + final var cave = getInput(); + final var result = cave.pourSandIntoAbyss(); - final var copy = Arrays.copyOf(chars, 36); - final var sub = floatingIndices.subList(1, floatingIndices.size()); + System.out.println("Part 1: " + result); + } - copy[index] = '0'; - list.addAll(explode(copy, sub)); - copy[index] = '1'; - list.addAll(explode(copy, sub)); + @Test + public final void part2() { + final var cave = getInput(); + final var result = cave.fillAperture(); - return Collections.unmodifiableList(list); + System.out.println("Part 2: " + result); } } \ No newline at end of file diff --git a/src/test/java/com/macasaet/Day15.java b/src/test/java/com/macasaet/Day15.java index 3de8788..2fea62c 100644 --- a/src/test/java/com/macasaet/Day15.java +++ b/src/test/java/com/macasaet/Day15.java @@ -1,49 +1,205 @@ package com.macasaet; -import java.io.IOException; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; -import java.util.LinkedList; import java.util.List; +import java.util.Map; +import java.util.function.IntPredicate; +import java.util.stream.StreamSupport; +/** + * --- Day 15: Beacon Exclusion Zone --- + * https://adventofcode.com/2022/day/15 + */ public class Day15 { - public static void main(final String[] args) throws IOException { - try (var spliterator = new LineSpliterator(Day15.class.getResourceAsStream("/day-15-test.txt"))) { -// final var lines = StreamSupport.stream(spliterator, false).collect(Collectors.toUnmodifiableList()); -// final var line = lines.get(0); -// final var numbers = Arrays.stream(line.split(",")).map(String::strip).map(Integer::parseInt).collect(Collectors.toUnmodifiableList()); -// final var numbers = new int[]{ 0, 3, 6 }; - final var numbers = new int[]{ 13,16,0,12,15,1 }; - - int last = -1; - final var oralHistory = new HashMap>(); - for( int i = 0; i < numbers.length; i++ ) { - final int number = numbers[ i ]; - oralHistory.computeIfAbsent(number, k -> new LinkedList<>()).add( i ); - last = number; - System.err.println( "Speak: " + number ); + record Coordinate(int x, int y) { + static Coordinate parse(final String string) { + final var components = string.split(", "); + return new Coordinate(Integer.parseInt(components[1].replaceAll("^y=", "")), + Integer.parseInt(components[0].replaceAll("^x=", ""))); + } + + Map getRow(final Map> grid) { + return grid.computeIfAbsent(x(), ignored -> new HashMap<>()); + } + + int distanceTo(final Coordinate other) { + return Math.abs(x() - other.x()) + Math.abs(y() - other.y()); + } + } + + public record Sensor(Coordinate location, Coordinate beaconLocation) { + public static Sensor parse(final String line) { + final var location = Coordinate.parse(line.replaceAll("^Sensor at ", "").replaceAll(": closest beacon is at .*$", "")); + final var beaconLocation = Coordinate.parse(line.replaceAll("^.*: closest beacon is at ", "")); + return new Sensor(location, beaconLocation); + } + + void setSensor(final Map> grid, IntPredicate includeRow, IntPredicate includeColumn) { + if(includeRow.test(location().x()) && includeColumn.test(location().y())) { + location().getRow(grid).put(location().y(), Item.SENSOR); + } + } + + void setBeacon(final Map> grid, IntPredicate includeRow, IntPredicate includeColumn) { + if(includeRow.test(beaconLocation().x()) && includeColumn.test(beaconLocation().y())) { + beaconLocation().getRow(grid).put(beaconLocation().y(), Item.BEACON); + } + } + + void setCoverageArea(final Map> grid, IntPredicate includeRow, IntPredicate includeColumn) { + final var distance = distanceToBeacon(); + final var x = location().x(); + final var y = location().y(); + + for(int i = 0; i <= distance; i++ ) { + if(!includeRow.test(x + i) && !includeRow.test(x - i)) { + continue; + } + final var lowerRow = includeRow.test(x + i) + ? grid.computeIfAbsent(x + i, ignored -> new HashMap<>()) + : new HashMap(); + final var upperRow = includeRow.test(x - i) + ? grid.computeIfAbsent(x - i, ignored -> new HashMap<>()) + : new HashMap(); + for(int j = 0; j <= distance - i; j++ ) { + if(includeColumn.test(y + j)) { + // SE + lowerRow.putIfAbsent(y + j, Item.COVERED); + // NE + upperRow.putIfAbsent(y + j, Item.COVERED); + } + if(includeColumn.test(y - j)) { + // SW + lowerRow.putIfAbsent(y - j, Item.COVERED); + + // NW + upperRow.putIfAbsent(y - j, Item.COVERED); + } + } + } + } + + int distanceToBeacon() { + return location().distanceTo(beaconLocation()); + } + } + + enum Item { + SENSOR, + BEACON, + COVERED + } + public record CaveMap(Map> grid, int minX, int maxX, int minY, int maxY) { + public int countCoveredCellsInRow(final int x) { + final var row = grid().getOrDefault(x, Collections.emptyMap()); + int result = 0; + for(int j = minY(); j <= maxY(); j++) { + final var cell = row.get(j); + if(cell != null && cell != Item.BEACON) { + result++; + } + } + return result; + } + + public static CaveMap fromSensors(final Iterable sensors, IntPredicate includeRow, IntPredicate includeColumn) { + int minX = Integer.MAX_VALUE; + int maxX = Integer.MIN_VALUE; + int minY = Integer.MAX_VALUE; + int maxY = Integer.MIN_VALUE; + final var grid = new HashMap>(); + for(final var sensor : sensors) { + minX = Math.min(minX, sensor.location().x() - sensor.distanceToBeacon()); + maxX = Math.max(maxX, sensor.location().x() + sensor.distanceToBeacon()); + minY = Math.min(minY, sensor.location().y() - sensor.distanceToBeacon()); + maxY = Math.max(maxY, sensor.location().y() + sensor.distanceToBeacon()); + + sensor.setCoverageArea(grid, includeRow, includeColumn); + sensor.setBeacon(grid, includeRow, includeColumn); + sensor.setSensor(grid, includeRow, includeColumn); } - for( int i = numbers.length; i < 30_000_000; i++ ) { - final var history = oralHistory.computeIfAbsent(last, k -> new LinkedList<>()); - if( history.isEmpty() ) { - throw new IllegalStateException("No history for: " + last ); - } else if( history.size() == 1 ) { // spoken only once before - final int numberToSpeak = 0; - System.err.println( "Turn " + ( i + 1 ) + ": Speak: " + numberToSpeak ); - oralHistory.getOrDefault( numberToSpeak, new LinkedList<>() ).add( i ); - last = numberToSpeak; - } else { // spoken 2+ times - final int lastMention = history.get(history.size() - 1); - final int penultimateMention = history.get(history.size() - 2); - final int numberToSpeak = lastMention - penultimateMention; - System.err.println( "Turn " + ( i + 1 ) + ": Speak: " + numberToSpeak ); - oralHistory.computeIfAbsent(numberToSpeak, k -> new LinkedList<>()).add( i ); - last = numberToSpeak; + return new CaveMap(grid, minX, maxX, minY, maxY); + } + + public String toString() { + final var builder = new StringBuilder(); + for(int i = minX(); i <= maxX(); i++) { + builder.append(i).append('\t'); + final var row = grid().getOrDefault(i, Collections.emptyMap()); + for(int j = minY(); j <= maxY(); j++) { + final var item = row.get(j); + final var marker = item == null + ? '.' + : item == Item.BEACON + ? 'B' + : item == Item.SENSOR + ? 'S' + : '#'; + builder.append(marker); + } + builder.append('\n'); + } + return builder.toString(); + } + } + + protected CaveMap getInput(final IntPredicate includeRow, IntPredicate includeColumn) { + final var sensors = getSensors(); + return CaveMap.fromSensors(sensors, includeRow, includeColumn); + } + + protected static List getSensors() { + return StreamSupport.stream(new LineSpliterator("day-15.txt"), false) + .map(Sensor::parse) + .toList(); + } + + @Test + public final void part1() { + final int rowOfInterest = 2_000_000; + final var map = getInput(row -> row == rowOfInterest, _column -> true); + final var result = map.countCoveredCellsInRow(rowOfInterest); + + System.out.println("Part 1: " + result); + } + + @Test + public final void part2() { + final int max = 4_000_000; + final var sensors = getSensors(); + for(final var sensor : sensors) { + final var x = sensor.location().x(); + final var y = sensor.location().y(); + final var reach = sensor.distanceToBeacon(); + // Find all the points just outside this sensor's reach + for(int horizontalOffset = 0; horizontalOffset <= reach + 1; horizontalOffset++) { + final var verticalOffset = reach + 1 - horizontalOffset; + Assertions.assertEquals(horizontalOffset + verticalOffset, reach + 1); + for(final var candidate : Arrays.asList(new Coordinate(x + verticalOffset, y + horizontalOffset), // SE + new Coordinate(x + verticalOffset, y - horizontalOffset), // SW + new Coordinate(x - verticalOffset, y + horizontalOffset), // NE + new Coordinate(x - verticalOffset, y - horizontalOffset))) { // NW + if(candidate.x() < 0 || candidate.y() < 0 || candidate.x() > max || candidate.y() > max) { + continue; + } + Assertions.assertTrue(candidate.distanceTo(sensor.location()) > sensor.distanceToBeacon()); + // Check if the point is also outside the reach of every other sensor + if(sensors.stream().allMatch(other -> candidate.distanceTo(other.location()) > other.distanceToBeacon())) { + final long result = (long)candidate.y() * 4_000_000l + (long)candidate.x(); + System.out.println("Part 2: " + result); + return; + } } } - System.out.println( "Part 1: " + last ); } + throw new IllegalStateException("No uncovered point found"); } } \ No newline at end of file diff --git a/src/test/java/com/macasaet/Day16.java b/src/test/java/com/macasaet/Day16.java deleted file mode 100644 index 01bd853..0000000 --- a/src/test/java/com/macasaet/Day16.java +++ /dev/null @@ -1,198 +0,0 @@ -package com.macasaet; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; -import java.util.stream.IntStream; -import java.util.stream.StreamSupport; - -public class Day16 { - - public static void main(final String[] args) throws IOException { - try (var spliterator = new LineSpliterator(Day16.class.getResourceAsStream("/day-16-input.txt"))) { - final var lines = StreamSupport.stream(spliterator, false).collect(Collectors.toUnmodifiableList()); - - final var fields = new HashSet(); - int section = 0; - Ticket myTicket = null; - final var nearbyTickets = new ArrayList(); - for (final var line : lines) { - if (line.isBlank()) { - section++; - continue; - } else if ("your ticket:".equalsIgnoreCase(line.strip()) || "nearby tickets:".equalsIgnoreCase(line.strip())) { - continue; - } - switch (section) { - case 0 -> { - final var sections = line.split(": "); - final var label = sections[0].strip().replaceAll(":$", ""); - final var rangesString = sections[1].strip(); - final var rangeStringArray = rangesString.split(" or "); - fields.add(new Field(label, - Arrays.stream(rangeStringArray) - .map(Range::fromString) - .sorted() - .collect(Collectors.toUnmodifiableList()))); - } - case 1 -> myTicket = new Ticket(Arrays.stream(line.split(",")) - .map(Integer::parseInt) - .collect(Collectors.toUnmodifiableList())); - case 2 -> nearbyTickets.add(new Ticket(Arrays.stream(line.split(",")) - .map(Integer::parseInt) - .collect(Collectors.toUnmodifiableList()))); - } - } - - final int sum = nearbyTickets - .stream() - .flatMapToInt(ticket -> ticket - .getInvalidNumbers(fields) - .stream() - .mapToInt(Integer::intValue)) - .sum(); - System.out.println("Part 1: " + sum); - - final var validTickets = nearbyTickets - .stream() - .filter(candidate -> candidate.isValid(fields)) - .collect(Collectors.toUnmodifiableSet()); - final var unmappedIndices = IntStream - .range(0, myTicket.numbers.size()) - .collect(HashSet::new, Set::add, Set::addAll); - final var fieldTable = new HashMap(); - final var unmappedFields = new HashSet<>(fields); - while (!unmappedFields.isEmpty()) { - final var indicesToRemove = new HashSet(); - for (final int index : unmappedIndices) { - final var candidates = new HashSet<>(unmappedFields); - final var toRemove = new HashSet(); - for (final var ticket : validTickets) { - final var number = ticket.numbers.get(index); - for (final var candidate : candidates) { - if (!candidate.contains(number)) { - toRemove.add(candidate); - } - } - candidates.removeAll(toRemove); - } - if (candidates.isEmpty()) { - throw new IllegalStateException("no candidates for index: " + index); - } else if (candidates.size() == 1) { - // map candidate to index - final var field = candidates.iterator().next(); - fieldTable.put(field.label, index); - unmappedFields.remove(field); - indicesToRemove.add(index); - } - } - unmappedIndices.removeAll(indicesToRemove); - } - final var numbers = myTicket.numbers; - final long product = fieldTable - .keySet() - .stream() - .filter(candidate -> candidate.startsWith("departure")) - .map(fieldTable::get) - .map(numbers::get) - .mapToLong(Long::valueOf) - .reduce((x, y) -> x * y).getAsLong(); - System.out.println("Part 2: " + product); - } - } - - protected static class Ticket { - private final List numbers; - - public Ticket(final List numbers) { - this.numbers = numbers; - } - - public boolean isValid(final Collection fields) { - return getInvalidNumbers(fields).isEmpty(); - } - - public List getInvalidNumbers(final Collection fields) { - final List list = new ArrayList<>(); - outer: - for (final var number : numbers) { - for (final var field : fields) { - if (field.contains(number)) { - continue outer; - } - } - list.add(number); - } - return Collections.unmodifiableList(list); - } - } - - protected static class Field { - private final String label; - private final List ranges; - - public Field(final String label, final List ranges) { - this.label = label; - this.ranges = ranges; - } - - public boolean contains(final int number) { - for (final var range : ranges) { - if (range.contains(number)) { - return true; - } - } - return false; - } - - public int hashCode() { - return label.hashCode(); - } - - public boolean equals(final Object object) { - if( this == object ) { - return true; - } else if( object == null ) { - return false; - } - try { - final Field other = ( Field )object; - return label.equals(other.label); - } catch( final ClassCastException cce ) { - return false; - } - } - } - - protected static class Range implements Comparable { - - private final int minInclusive; - private final int maxInclusive; - - public Range(final int minInclusive, final int maxInclusive) { - this.minInclusive = minInclusive; - this.maxInclusive = maxInclusive; - } - - public static Range fromString(final String string) { - final var ends = string.split("-", 2); - return new Range(Integer.parseInt(ends[0].strip()), Integer.parseInt(ends[1].strip())); - } - - public int compareTo(final Range other) { - return Integer.compare(minInclusive, other.minInclusive); - } - - public boolean contains(final int candidate) { - return candidate >= minInclusive && candidate <= maxInclusive; - } - - } -} \ No newline at end of file diff --git a/src/test/java/com/macasaet/Day17.java b/src/test/java/com/macasaet/Day17.java deleted file mode 100644 index cf0456c..0000000 --- a/src/test/java/com/macasaet/Day17.java +++ /dev/null @@ -1,150 +0,0 @@ -package com.macasaet; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.LinkedList; -import java.util.SortedMap; -import java.util.TreeMap; -import java.util.stream.Collectors; -import java.util.stream.StreamSupport; - -public class Day17 { - - public static void main(final String[] args) throws IOException { - try (var spliterator = new LineSpliterator(Day17.class.getResourceAsStream("/day-17-input.txt"))) { - final var grid = new Grid(); - - final var lines = StreamSupport.stream(spliterator, false).collect(Collectors.toUnmodifiableList()); - for( int x = 0; x < lines.size(); x++ ) { - final var line = lines.get(x); - for( int y = 0; y < line.length(); y++ ) { - final char state = line.charAt(y); - final int z = 0; - final int w = 0; - grid.setInitial(x, y, z, w, state); - } - } - - for( int i = 0; i < 6; i++ ) { - grid.cycle(); - } - System.out.println( "Part 2: " + grid.countActive() ); - } - } - - public static class Grid { - - private final SortedMap>>> map = new TreeMap<>(); - private int minX = 0, maxX = 0, minY = 0, maxY = 0, minZ = 0, maxZ = 0, minW = 0, maxW = 0; - - public void setInitial(final int x, final int y, final int z, final int w, final char state) { - final var dimX = map.computeIfAbsent(x, key -> new TreeMap<>()); - final var dimY = dimX.computeIfAbsent(y, key -> new TreeMap<>()); - final var dimZ = dimY.computeIfAbsent(z, key -> new TreeMap<>()); - dimZ.put(w, state); - minX = Math.min(minX, x); - maxX = Math.max(maxX, x); - minY = Math.min(minY, y); - maxY = Math.max(maxY, y); - minZ = Math.min(minZ, z); - maxZ = Math.max(maxZ, z); - minW = Math.min(minW, w); - maxW = Math.max(maxW, w); - } - - public Runnable defer(final int x, final int y, final int z, final int w) { - int activeNeighbours = 0; - for( final var neighbour : neighbours( x, y, z, w ) ) { - if( neighbour.getState() == '#' ) { - activeNeighbours++; - } - } - final var cell = new Cell(x, y, z, w); - if( cell.getState() == '#' ) { // active - return activeNeighbours == 2 || activeNeighbours == 3 ? () -> {} : () -> cell.setState('.'); - } else { // inactive - return activeNeighbours == 3 ? () -> cell.setState('#') : () -> {}; - } - } - - public void cycle() { - final var updateTasks = new LinkedList(); - for( int x = minX - 1; x <= maxX + 1; x++ ) { - for( int y = minY - 1; y <= maxY + 1; y++ ) { - for( int z = minZ - 1; z <= maxZ + 1; z++ ) { - for( int w = minW - 1; w <= maxW + 1; w++ ) { - updateTasks.add(defer(x, y, z, w)); - } - } - } - } - updateTasks.forEach(Runnable::run); - } - - public int countActive() { - return map.values() - .stream() - .flatMapToInt(yDim -> yDim.values() - .stream() - .flatMapToInt(zDim -> zDim.values() - .stream() - .flatMapToInt(wDim -> wDim.values() - .stream() - .mapToInt(state -> state == '#' ? 1 : 0 )))) - .sum(); - } - - protected Collection neighbours(final int x, final int y, final int z, int w) { - final var list = new ArrayList(80); - for( int i = x - 1; i <= x + 1; i++ ) { - for( int j = y - 1; j <= y + 1; j++ ) { - for( int k = z - 1; k <= z + 1; k++ ) { - for( int l = w - 1; l <= w + 1; l++ ) { - if (i == x && j == y && k == z && l == w) continue; - list.add(new Cell(i, j, k, l)); - } - } - } - } - if( list.size() != 80 ) { - throw new IllegalStateException("There should be 80 neighbours :-("); - } - return Collections.unmodifiableList(list); - } - - protected class Cell { - final int x, y, z, w; - - public Cell(final int x, final int y, final int z, int w) { - this.x = x; - this.y = y; - this.z = z; - this.w = w; - } - - public char getState() { - final var dimensionX = map.getOrDefault(x, Collections.emptySortedMap()); - final var dimensionY = dimensionX.getOrDefault(y, Collections.emptySortedMap()); - final var dimensionZ = dimensionY.getOrDefault(z, Collections.emptySortedMap()); - return dimensionZ.getOrDefault(w, '.'); - } - - public void setState(final char state) { - final var dimensionX = map.computeIfAbsent(x, key -> new TreeMap<>()); - final var dimensionY = dimensionX.computeIfAbsent(y, key -> new TreeMap<>()); - final var dimensionZ = dimensionY.computeIfAbsent(z, key -> new TreeMap<>()); - dimensionZ.put(w, state); - minX = Math.min(minX, x); - maxX = Math.max(maxX, x); - minY = Math.min(minY, y); - maxY = Math.max(maxY, y); - minZ = Math.min(minZ, z); - maxZ = Math.max(maxZ, z); - minW = Math.min(minW, w); - maxW = Math.max(maxW, w); - } - } - } -} \ No newline at end of file diff --git a/src/test/java/com/macasaet/Day18.java b/src/test/java/com/macasaet/Day18.java index 61b25aa..3c29e60 100644 --- a/src/test/java/com/macasaet/Day18.java +++ b/src/test/java/com/macasaet/Day18.java @@ -1,91 +1,175 @@ package com.macasaet; -import java.io.IOException; -import java.math.BigInteger; -import java.util.LinkedList; -import java.util.stream.Collectors; +import org.junit.jupiter.api.Test; + +import java.util.ArrayDeque; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; import java.util.stream.StreamSupport; +/** + * --- Day 18: Boiling Boulders --- + * https://adventofcode.com/2022/day/18 + */ public class Day18 { - public static void main(final String[] args) throws IOException { - try (var spliterator = new LineSpliterator(Day18.class.getResourceAsStream("/day-18-input.txt"))) { - final var lines = StreamSupport.stream(spliterator, false).collect(Collectors.toUnmodifiableList()); - BigInteger sum = BigInteger.ZERO; - for (final var line : lines) { - final var outputQueue = new LinkedList(); - final var operatorStack = new LinkedList(); - - String numberString = ""; - for (final char c : line.toCharArray()) { - if (Character.isDigit(c)) { - numberString += c; - } else { - if (!"".equals(numberString)) { - outputQueue.add(numberString); - numberString = ""; - } - switch (c) { - case '+', '*' -> { - Character topOperator = operatorStack.peek(); - while (topOperator != null && topOperator > c /*&& topOperator != '('*/) { - outputQueue.add("" + operatorStack.pop()); - topOperator = operatorStack.peek(); - } - operatorStack.push(c); - } - case '(' -> operatorStack.push(c); - case ')' -> { - Character topOperator = operatorStack.peek(); - while (topOperator != null && topOperator != '(') { - outputQueue.add("" + operatorStack.pop()); - topOperator = operatorStack.peek(); - } - if (topOperator == null /*|| topOperator != '('*/) { - throw new IllegalStateException("mis-matched parentheses :-("); - } - operatorStack.pop(); - } - case ' ' -> { - } - default -> throw new IllegalStateException("Unexpected value: " + c); - } - } + public static final int SCAN_DIMENSION = 32; + protected static Droplet getInput() { + final var cubeCoordinates = StreamSupport.stream(new LineSpliterator("day-18.txt"), false) + .map(Cube::parse) + .toList(); + return new Droplet(cubeCoordinates); + } + + @Test + public final void part1() { + final var droplet = getInput(); + final var result = droplet.surfaceArea(CubeType.Air); + + System.out.println("Part 1: " + result); + } + + @Test + public final void part2() { + final var droplet = getInput(); + droplet.immerse(); + final var result = droplet.surfaceArea(CubeType.Water); + + System.out.println("Part 2: " + result); + } + + public enum CubeType { + Air, + Lava, + Water, + } + + record Cube(int x, int y, int z) { + public static Cube parse(final String line) { + final var components = line.split(","); + return new Cube(Integer.parseInt(components[0]), Integer.parseInt(components[1]), Integer.parseInt(components[2])); + } + + public CubeType getType(final CubeType[][][] grid) { + return grid[x()][y()][z()]; + } + + public void setType(final CubeType[][][] grid, final CubeType type) { + grid[x()][y()][z()] = type; + } + } + + public static class Droplet { + + private final Collection cubes; + private final CubeType[][][] grid; + + public Droplet(final Collection cubes) { + final CubeType[][][] grid = new CubeType[SCAN_DIMENSION][][]; + for (int i = SCAN_DIMENSION; --i >= 0; ) { + grid[i] = new CubeType[SCAN_DIMENSION][]; + for (int j = grid[i].length; --j >= 0; ) { + grid[i][j] = new CubeType[SCAN_DIMENSION]; + for (int k = grid[i][j].length; --k >= 0; grid[i][j][k] = CubeType.Air); } - if (!numberString.isBlank()) { - outputQueue.add(numberString); - } - while (!operatorStack.isEmpty()) { - outputQueue.add("" + operatorStack.pop()); - } - final var stack = new LinkedList(); - for (final var item : outputQueue) { - try { - stack.push(new BigInteger(item)); - } catch (final NumberFormatException nfe) { - switch (item) { - case "+" -> { - final var rValue = stack.pop(); - final var lValue = stack.pop(); - final var result = lValue.add(rValue); - stack.push(result); - } - case "*" -> { - final var rValue = stack.pop(); - final var lValue = stack.pop(); - final var result = lValue.multiply(rValue); - stack.push(result); - } - } + } + for (final var cube : cubes) { + cube.setType(grid, CubeType.Lava); + } + this.grid = grid; + this.cubes = cubes; + } + + public int surfaceArea(final CubeType element) { + int result = 0; + for (final var cube : getCubes()) { + result += exposedFaces(cube, element); + } + return result; + } + + public void immerse() { + final var grid = getGrid(); + final var queue = new ArrayDeque(); + final var encountered = new HashSet(); + final var origin = new Cube(0, 0, 0); + encountered.add(origin); + queue.add(origin); + + while (!queue.isEmpty()) { + final var cube = queue.remove(); + for (final var neighbour : neighbours(cube).stream().filter(neighbour -> neighbour.getType(grid) == CubeType.Air).toList()) { + if (!encountered.contains(neighbour)) { + encountered.add(neighbour); + queue.add(neighbour); } } - if (stack.size() != 1) { - throw new IllegalStateException("Invalid stack: " + stack); - } - sum = sum.add(stack.get(0)); + cube.setType(grid, CubeType.Water); + } + } + + protected Collection neighbours(final Cube cube) { + final var result = new HashSet(); + final var x = cube.x(); + final var y = cube.y(); + final var z = cube.z(); + if (x > 0) { + result.add(new Cube(x - 1, y, z)); + } + if (x < SCAN_DIMENSION - 1) { + result.add(new Cube(x + 1, y, z)); + } + if (y > 0) { + result.add(new Cube(x, y - 1, z)); + } + if (y < SCAN_DIMENSION - 1) { + result.add(new Cube(x, y + 1, z)); + } + if (z > 0) { + result.add(new Cube(x, y, z - 1)); + } + if (z < SCAN_DIMENSION - 1) { + result.add(new Cube(x, y, z + 1)); } - System.out.println("Part 2: " + sum); + return Collections.unmodifiableSet(result); + } + + public int exposedFaces(final Cube cube, final CubeType element) { + final int x = cube.x(); + final int y = cube.y(); + final int z = cube.z(); + final var grid = getGrid(); + + int result = 0; + if (grid[x + 1][y][z] == element) { + result++; + } + if (x <= 0 || grid[x - 1][y][z] == element) { + result++; + } + if (grid[x][y + 1][z] == element) { + result++; + } + if (y == 0 || grid[x][y - 1][z] == element) { + result++; + } + if (grid[x][y][z + 1] == element) { + result++; + } + if (z == 0 || grid[x][y][z - 1] == element) { + result++; + } + return result; + } + + public Collection getCubes() { + return cubes; + } + + public CubeType[][][] getGrid() { + return grid; } } diff --git a/src/test/java/com/macasaet/Day19.java b/src/test/java/com/macasaet/Day19.java deleted file mode 100644 index 007972a..0000000 --- a/src/test/java/com/macasaet/Day19.java +++ /dev/null @@ -1,152 +0,0 @@ -package com.macasaet; - -import java.io.IOException; -import java.util.Arrays; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Spliterators; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; - -public class Day19 { - - public static void main(final String[] args) throws IOException { - try (var spliterator = new LineSpliterator(Day19.class.getResourceAsStream("/day-19-input.txt"))) { - final var lines = StreamSupport.stream(spliterator, false).collect(Collectors.toUnmodifiableList()); - - final var ruleMap = new HashMap(); - int sum = 0; - int mode = 0; - for (var line : lines) { - // comment out the remapping for Part 1 - if ("8: 42".equals(line.strip())) { - line = "8: 42 | 42 8"; - } else if ("11: 42 31".equals(line.strip())) { - line = "11: 42 31 | 42 11 31"; - } - if (line.isBlank()) { - mode++; - continue; - } - if (mode == 0) { - final var components = line.split(": ", 2); - final var ruleId = Integer.parseInt(components[0].strip()); - final var value = components[1].strip(); - if (value.matches("\"[a-z]\"")) { - final var c = value.charAt(1); - final var rule = new CharacterRule(ruleId, c); - ruleMap.put(ruleId, rule); - } else if (value.contains("|")) { - final var ruleSets = value.split(" \\| "); - final var set = Arrays.stream(ruleSets) - .map(ruleSet -> Arrays.stream(ruleSet.split(" ")) - .map(String::strip) - .map(Integer::parseInt) - .collect(Collectors.toUnmodifiableList())) - .collect(Collectors.toUnmodifiableSet()); - final var rule = new DisjunctionRule(ruleId, set); - ruleMap.put(ruleId, rule); - } else { - final var subRules = Arrays.stream(value.split(" ")) - .map(String::strip) - .map(Integer::parseInt) - .collect(Collectors.toUnmodifiableList()); - final var rule = new SequenceRule(ruleId, subRules); - ruleMap.put(ruleId, rule); - } - } else { - final var rule = ruleMap.get(0); - if (rule.matches(line.strip(), ruleMap)) { - sum++; - } - } - } - System.out.println("Result: " + sum); - } - } - - public abstract static class Rule { - protected final int id; - - protected Rule(final int id) { - this.id = id; - } - - public boolean matches(final String string, final Map map) { - return getMatchingSuffixes(Stream.of(string), map).distinct().anyMatch(String::isBlank); - } - - protected abstract Stream getMatchingSuffixes(Stream strings, Map map); - } - - public static class CharacterRule extends Rule { - - private final char c; - - public CharacterRule(final int id, final char c) { - super(id); - this.c = c; - } - - protected Stream getMatchingSuffixes(Stream strings, Map map) { - return strings.flatMap(string -> - string.startsWith("" + c) - ? Stream.of(string.substring(1)) - : Stream.empty()); - } - } - - public static class SequenceRule extends Rule { - private final List ruleIds; - - public SequenceRule(final int id, final List ruleIds) { - super(id); - this.ruleIds = ruleIds; - } - - protected Stream getMatchingSuffixes(Stream strings, Map map) { - return strings.flatMap(string -> { - var result = Stream.of(string); - for (final var ruleId : ruleIds) { // match all in sequence - final var rule = map.get(ruleId); - final var iterator = rule.getMatchingSuffixes(result, map).iterator(); - if (!iterator.hasNext()) { - result = Stream.empty(); - break; - } - result = StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, 0), false); - } - return result; - }).distinct(); - } - } - - public static class DisjunctionRule extends Rule { - - private final Collection> options; - - public DisjunctionRule(final int id, final Collection> options) { - super(id); - this.options = options; - } - - protected Stream getMatchingSuffixes(final Stream strings, final Map map) { - return strings.flatMap(string -> options.stream().flatMap(option -> { - var result = Stream.of(string); - for (final var ruleId : option) { // match all in sequence - final var rule = map.get(ruleId); - final var iterator = rule.getMatchingSuffixes(result, map).iterator(); - if (!iterator.hasNext()) { - result = Stream.empty(); - break; - } - result = StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, 0), false); - } - return result; - })).distinct(); - } - } -} \ No newline at end of file diff --git a/src/test/java/com/macasaet/Day20.java b/src/test/java/com/macasaet/Day20.java index de4daeb..4125a47 100644 --- a/src/test/java/com/macasaet/Day20.java +++ b/src/test/java/com/macasaet/Day20.java @@ -1,406 +1,106 @@ package com.macasaet; -import java.io.IOException; +import org.junit.jupiter.api.Test; + import java.math.BigInteger; +import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; -import java.util.Set; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.IntConsumer; -import java.util.stream.Collectors; import java.util.stream.Stream; import java.util.stream.StreamSupport; +/** + * --- Day 20: Grove Positioning System --- + * https://adventofcode.com/2022/day/20 + */ public class Day20 { - private static final String seaMonsterString = - " # \n" + - "# ## ## ###\n" + - " # # # # # # "; - private static final char[][] seaMonster; - - static { - final var seaMonsterLines = seaMonsterString.split("\n"); - seaMonster = new char[seaMonsterLines.length][]; - for (int i = seaMonsterLines.length; --i >= 0; seaMonster[i] = seaMonsterLines[i].toCharArray()) ; - } - - public static void main(final String[] args) throws IOException { - try (var spliterator = new LineSpliterator(Day20.class.getResourceAsStream("/day-20-input.txt"))) { - final var tiles = new LinkedList(); - final var lines = - StreamSupport.stream(spliterator, false) - .map(String::strip) - .collect(Collectors.toUnmodifiableList()); - int tileId = -1; - var rows = new ArrayList(); - for (final var line : lines) { - if (line.startsWith("Tile ")) { - tileId = Integer.parseInt(line.replaceAll("[^0-9]", "")); - rows = new ArrayList<>(); - } else if (line.isBlank()) { - final var tile = new Tile(tileId, rows.toArray(new char[rows.size()][])); - tiles.add(tile); - tileId = -1; - rows = new ArrayList<>(); - } else { - rows.add(line.toCharArray()); - } - } - if (tileId > 0) { - final var tile = new Tile(tileId, rows.toArray(new char[rows.size()][])); - tiles.add(tile); - } - final int size = (int) Math.sqrt(tiles.size()); // the image is square - - final var possibleArrangements = getValidArrangements(Collections.emptyList(), tiles, size); - if (possibleArrangements.isEmpty()) { - throw new IllegalStateException("No possible arrangements"); - } else { - // there are multiple possible arrangements, but they are just rotated and/or flipped versions of each other - // TODO is there a matrix transform I can put in hashCode/equals that will treat these as equivalent? - System.err.println(possibleArrangements.size() + " possible arrangements: " + possibleArrangements); - for (final var arrangement : possibleArrangements) { - final var topLeft = arrangement.get(0); - final var topRight = arrangement.get(size - 1); - final var bottomLeft = arrangement.get(arrangement.size() - size); - final var bottomRight = arrangement.get(arrangement.size() - 1); - final var result = - Stream.of(topLeft, topRight, bottomLeft, bottomRight) - .map(corner -> corner.id) - .map(Long::valueOf) - .map(BigInteger::valueOf) - .reduce(BigInteger::multiply) - .get(); - System.out.println("Part 1: " + result); - - final var orderedCroppedTiles = - arrangement.stream().map(Tile::removeBorders).collect(Collectors.toUnmodifiableList()); - final var combined = combine(orderedCroppedTiles); - - for (final var permutation : combined.getPermutations()) { - final var counter = new AtomicInteger(0); - highlightSeaMonsters(permutation, counter::set); - final var numSeaMonsters = counter.get(); - if (numSeaMonsters > 0) { - System.err.println(permutation + " has " + numSeaMonsters + " sea monsters"); - int sum = 0; - for (int i = permutation.edgeLength; --i >= 0; ) { - for (int j = permutation.edgeLength; --j >= 0; ) { - if (permutation.grid[i][j] == '#') { - sum++; - } - } - } - System.out.println("Part 2: " + sum); - } - } - } - } + public record Number(int originalIndex, int value, BigInteger decryptedValue) { + Number(int originalIndex, int value) { + this(originalIndex, value, BigInteger.valueOf(value).multiply(BigInteger.valueOf(811_589_153))); } } - public static void highlightSeaMonsters(final Tile tile, final IntConsumer counter) { - final int windowHeight = seaMonster.length; - final int windowWidth = seaMonster[0].length; - final int verticalWindows = tile.edgeLength - windowHeight; - final int horizontalWindows = tile.edgeLength - windowWidth; - - int sum = 0; - for (int i = 0; i < verticalWindows; i++) { - for (int j = 0; j < horizontalWindows; j++) { - if (contains(tile.grid, i, j)) { - sum++; - highlight(tile.grid, i, j); - } - } + protected static List getInput() { + final List numbers = StreamSupport.stream(new LineSpliterator("day-20.txt"), false) + .mapToInt(Integer::parseInt) + .collect(ArrayList::new, (x, y) -> x.add(y), (x, y) -> x.addAll(y)); + final var result = new ArrayList(numbers.size()); + for(int i = 0; i < numbers.size(); i++) { + result.add(new Number(i, numbers.get(i))); } - counter.accept(sum); + return Collections.unmodifiableList(result); } - protected static boolean contains(final char[][] image, final int verticalOffset, final int horizontalOffset) { - for (int i = verticalOffset; i < verticalOffset + seaMonster.length; i++) { // loop the height of the pattern - final var patternRow = seaMonster[i - verticalOffset]; - final var imageRow = image[i]; - for (int j = horizontalOffset; j < horizontalOffset + patternRow.length; j++) { // loop the width of the pattern - final var p = patternRow[j - horizontalOffset]; - // spaces can be anything - if (p == '#' && imageRow[j] != '#') { - // only the # need to match - return false; - } + @Test + public final void part1() { + final var numbers = getInput(); + final var indexMap = new HashMap(numbers.size()); + Number zero = null; + for(final var number : numbers) { + indexMap.put(number.originalIndex, number.value()); + if(number.value() == 0) { + zero = number; } } - return true; - } + final var workingSet = new ArrayList<>(numbers); - protected static void highlight(final char[][] image, final int verticalOffset, final int horizontalOffset) { - for (int i = verticalOffset; i < verticalOffset + seaMonster.length; i++) { - final var patternRow = seaMonster[i - verticalOffset]; - final var imageRow = image[i]; - for (int j = horizontalOffset; j < horizontalOffset + patternRow.length; j++) { - final var p = patternRow[j - horizontalOffset]; - if (p == ' ') continue; - if (p == '#') imageRow[j] = 'O'; + for(final var number : numbers) { + final var originalIndex = workingSet.indexOf(number); + workingSet.remove(originalIndex); + var newIndex = (originalIndex + number.value()) % (numbers.size() - 1); + if(newIndex < 0) { + newIndex += numbers.size() - 1; } + workingSet.add(newIndex, number); } - } - protected static Set> getValidArrangements(final List partialArrangement, - final List remainingTiles, - final int edgeLength) { - if (remainingTiles.isEmpty()) { - return Collections.singleton(partialArrangement); - } else if (partialArrangement.isEmpty()) { - // find the candidates for the top-left tile - final Set> set = new HashSet<>(); - for (int i = remainingTiles.size(); --i >= 0; ) { - final var candidate = remainingTiles.get(i); // candidate for first tile - // consider all possible orientations - for (final var orientation : candidate.getPermutations()) { -// System.err.println("Trying " + orientation + " in the top left with."); - final var partial = Collections.singletonList(orientation); - final var remaining = new ArrayList(remainingTiles.size() - 1); - remaining.addAll(remainingTiles.subList(0, i)); - remaining.addAll(remainingTiles.subList(i + 1, remainingTiles.size())); + final var x = workingSet.get((workingSet.indexOf(zero) + 1000) % workingSet.size()).value(); + final var y = workingSet.get((workingSet.indexOf(zero) + 2000) % workingSet.size()).value(); + final var z = workingSet.get((workingSet.indexOf(zero) + 3000) % workingSet.size()).value(); - final var validArrangements = - getValidArrangements(partial, Collections.unmodifiableList(remaining), edgeLength); - if (!validArrangements.isEmpty()) { - System.err.println("Found arrangement with " + orientation + " in the top left."); - } - set.addAll(validArrangements); - } - } - return Collections.unmodifiableSet(set); - } + final var result = (long)x + (long)y + (long)z; - final Set> set = new HashSet<>(); - for (int i = remainingTiles.size(); --i >= 0; ) { - final var candidate = remainingTiles.get(i); - final var oriented = fits(partialArrangement, candidate, edgeLength); - if (oriented != null) { -// System.err.println(oriented + " fits at index " + partialArrangement.size()); - final var permutation = new ArrayList<>(partialArrangement); - permutation.add(oriented); // this is a new valid partial arrangement (is it the only one?) - - final var remaining = new ArrayList(remainingTiles.size() - 1); - remaining.addAll(remainingTiles.subList(0, i)); - remaining.addAll(remainingTiles.subList(i + 1, remainingTiles.size())); - - final var validArrangements = - getValidArrangements(Collections.unmodifiableList(permutation), - Collections.unmodifiableList(remaining), edgeLength); - set.addAll(validArrangements); - } - } - return Collections.unmodifiableSet(set); - } - - protected static Tile getTileAbove(final List partialArrangement, final int index, final int edgeLength) { - if (index < edgeLength) { - return null; - } - return partialArrangement.get(index - edgeLength); - } - - protected static Tile getTileToLeft(final List partialArrangement, final int index, final int edgeLength) { - if (index % edgeLength == 0) { - return null; - } - return partialArrangement.get(index - 1); - } - - protected static Tile fits(final List partialArrangement, final Tile candidate, final int edgeLength) { - final int index = partialArrangement.size(); - final var tileAbove = getTileAbove(partialArrangement, index, edgeLength); - final var tileToLeft = getTileToLeft(partialArrangement, index, edgeLength); - for (final var orientation : candidate.getPermutations()) { - final var topFits = tileAbove == null || tileAbove.getBottomBorder().equals(orientation.getTopBorder()); - final var leftFits = tileToLeft == null || tileToLeft.getRightBorder().equals(orientation.getLeftBorder()); - if (topFits && leftFits) { - return orientation; - } - } - return null; - } - - protected static Tile combine(final List arrangement) { - final int tilesOnEdge = (int) Math.sqrt(arrangement.size()); - final var combinedLength = tilesOnEdge * arrangement.get(0).edgeLength; - char[][] combinedGrid = new char[combinedLength][]; - int maxId = Integer.MIN_VALUE; - int id = 0; - for (int index = arrangement.size(); --index >= 0; ) { - final var tile = arrangement.get(index); - maxId = Math.max(maxId, tile.id); - id = id * 31 + tile.id; - final int tileRow = index / tilesOnEdge; - final int tileColumn = index % tilesOnEdge; - final int rowOffset = tile.edgeLength * tileRow; - final int columnOffset = tile.edgeLength * tileColumn; - for (int row = tile.edgeLength; --row >= 0; ) { - if (combinedGrid[row + rowOffset] == null) { - combinedGrid[row + rowOffset] = new char[combinedLength]; - } - for (int column = tile.edgeLength; --column >= 0; ) { - combinedGrid[row + rowOffset][column + columnOffset] = tile.grid[row][column]; - } - } - } - return new Tile(id % maxId, combinedGrid); + System.out.println("Part 1: " + result); } - protected static class Tile { - private final int id; - private final char[][] grid; - private final int edgeLength; - private final String label; - - public Tile(final int id, final char[][] grid) { - this(id, grid, "original"); - } - - protected Tile(final int id, final char[][] grid, final String label) { - this.id = id; - this.grid = grid; - this.edgeLength = grid.length; - this.label = label; - } - - public String getTopBorder() { - return new String(grid[0]); - } - - public String getBottomBorder() { - return new String(grid[edgeLength - 1]); - } - - public String getLeftBorder() { - final char[] array = new char[edgeLength]; - for (int i = edgeLength; --i >= 0; array[i] = grid[i][0]) ; - return new String(array); - } - - public String getRightBorder() { - final char[] array = new char[edgeLength]; - for (int i = edgeLength; --i >= 0; array[i] = grid[i][edgeLength - 1]) ; - return new String(array); - } - - public Collection getPermutations() { - return List.of(this, flipHorizontal(), flipVertical(), rotate90(), rotate180(), rotate270(), - rotate90().flipHorizontal(), rotate90().flipVertical(), - rotate180().flipHorizontal(), rotate180().flipVertical(), - rotate270().flipHorizontal(), rotate270().flipVertical()); - } - - public Tile flipVertical() { - final var flipped = new char[edgeLength][]; - for (int row = edgeLength; --row >= 0; ) { - final var flippedRow = new char[edgeLength]; - for (int oldColumn = edgeLength; --oldColumn >= 0; ) { - final int newColumn = edgeLength - oldColumn - 1; - flippedRow[newColumn] = grid[row][oldColumn]; + @Test + public final void part2() { + final var numbers = getInput(); + final var indexMap = new HashMap(numbers.size()); + Number zero = null; + for(final var number : numbers) { + indexMap.put(number.originalIndex, number.value()); + if(number.value() == 0) { + zero = number; + } + } + final var workingSet = new ArrayList<>(numbers); + + for(int i = 10; --i >= 0; ) { + for (final var number : numbers) { + final var originalIndex = workingSet.indexOf(number); + workingSet.remove(originalIndex); + var newIndex = number.decryptedValue().add(BigInteger.valueOf(originalIndex)).mod(BigInteger.valueOf(numbers.size() - 1)).intValue(); + if (newIndex < 0) { + newIndex += numbers.size() - 1; } - flipped[row] = flippedRow; + workingSet.add(newIndex, number); } - return new Tile(id, flipped, label + ", flipped around vertical axis"); } - public Tile flipHorizontal() { - final var flipped = new char[edgeLength][]; - for (int i = edgeLength; --i >= 0; ) { - final int newRowId = edgeLength - i - 1; - final char[] row = new char[edgeLength]; - System.arraycopy(grid[i], 0, row, 0, edgeLength); - flipped[newRowId] = row; - } - return new Tile(id, flipped, label + ", flipped around horizontal axis"); - } + final var x = workingSet.get((workingSet.indexOf(zero) + 1000) % workingSet.size()).decryptedValue(); + final var y = workingSet.get((workingSet.indexOf(zero) + 2000) % workingSet.size()).decryptedValue(); + final var z = workingSet.get((workingSet.indexOf(zero) + 3000) % workingSet.size()).decryptedValue(); - public Tile transpose() { - final var transposed = new char[edgeLength][]; // should this be its own permutation? - for (int i = edgeLength; --i >= 0; ) { - transposed[i] = new char[edgeLength]; - for (int j = edgeLength; --j >= 0; ) { - transposed[i][j] = grid[j][i]; - } - } - return new Tile(id, transposed, label + ", transposed"); - } + final var result = x.add(y).add(z); - public Tile rotate90() { - final var transposed = transpose().grid; - final var reversedRows = new char[edgeLength][]; - for (int i = edgeLength; --i >= 0; ) { - final var newRow = new char[edgeLength]; - for (int j = edgeLength; --j >= 0; ) { - final int newColumn = edgeLength - j - 1; - newRow[newColumn] = transposed[i][j]; - } - reversedRows[i] = newRow; - } - return new Tile(id, reversedRows, label + ", rotated 90 degrees"); - } - - public Tile rotate180() { - return new Tile(id, rotate90().rotate90().grid, label + ", rotated 180 degrees"); - } - - public Tile rotate270() { - return new Tile(id, rotate180().rotate90().grid, label + ", rotated 270 degrees"); - } - - public Tile removeBorders() { - final int length = edgeLength - 2; - final var cropped = new char[length][]; - for (int i = edgeLength - 1; --i >= 1; ) { - final var row = new char[length]; - System.arraycopy(grid[i], 1, row, 0, length); - cropped[i - 1] = row; - } - return new Tile(id, cropped, label + " cropped"); - } - - public String toString() { - return "Tile{ " + id + ", " + label + ", top=" + getTopBorder() + " }"; - } - - public int hashCode() { - int retval = 0; - retval = 31 * retval + id; - for (int i = edgeLength; --i >= 0; ) { - for (int j = edgeLength; --j >= 0; ) { - retval = 31 * retval + grid[i][j]; - } - } - return retval; - } - - public boolean equals(final Object o) { - if (o == null) { - return false; - } else if (this == o) { - return true; - } - try { - final Tile other = (Tile) o; - boolean retval = id == other.id; - for (int i = edgeLength; --i >= 0 && retval; ) { - for (int j = edgeLength; --j >= 0 && retval; ) { - retval &= grid[i][j] == other.grid[i][j]; - } - } - return retval; - } catch (final ClassCastException cce) { - return false; - } - } + System.out.println("Part 2: " + result); } } \ No newline at end of file diff --git a/src/test/java/com/macasaet/Day21.java b/src/test/java/com/macasaet/Day21.java index db1a5c6..b2fbfc8 100644 --- a/src/test/java/com/macasaet/Day21.java +++ b/src/test/java/com/macasaet/Day21.java @@ -1,120 +1,323 @@ package com.macasaet; -import java.io.IOException; -import java.util.Comparator; +import org.junit.jupiter.api.Test; + +import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; -import java.util.Map.Entry; -import java.util.Set; -import java.util.stream.Collectors; +import java.util.Map; import java.util.stream.StreamSupport; +/** + * --- Day 21: Monkey Math --- + * https://adventofcode.com/2022/day/21 + */ public class Day21 { - public static void main(final String[] args) throws IOException { - try (var spliterator = new LineSpliterator(Day21.class.getResourceAsStream("/day-21-input.txt"))) { - - final var foods = StreamSupport.stream(spliterator, false) - .map(String::strip) - .map(Food::fromLine) - .collect(Collectors.toUnmodifiableSet()); - final var allergenToFood = new HashMap>(); - final var ingredientToFood = new HashMap>(); - for (final var food : foods) { - for (final var allergen : food.allergens) { - allergenToFood.computeIfAbsent(allergen, key -> new HashSet<>()).add(food); - } - for (final var ingredient : food.ingredients) { - ingredientToFood.computeIfAbsent(ingredient, key -> new HashSet<>()).add(food); - } - } + public record Monkey(String name, Job job) { + public long yell(final Map monkeys, final Map results) { + return job().yell(monkeys, results); + } - final var ingredientsThatContainNoAllergens = new HashSet<>(ingredientToFood.keySet()); - final var allergenToIngredient = new HashMap>(); - allergenToFood.entrySet().stream().forEach(entry -> { - final var allergen = entry.getKey(); - final var appearances = entry.getValue(); - Set commonIngredients = null; - for (final var food : appearances) { - if (commonIngredients == null) { - commonIngredients = new HashSet<>(food.ingredients); - } else { - commonIngredients.retainAll(food.ingredients); - } - } - System.err.println(allergen + " may be found in: " + commonIngredients); - allergenToIngredient.put(allergen, commonIngredients); - ingredientsThatContainNoAllergens.removeAll(commonIngredients); - }); - - int sum = 0; - for (final var food : foods) { - for (final var ingredient : ingredientsThatContainNoAllergens) { - if (food.ingredients.contains(ingredient)) { - sum++; - } - } - } - System.out.println("Part 1: " + sum); - - final var ingredientToAllergen = new HashMap(); - - final var dangerousIngredients = new HashSet<>(ingredientToFood.keySet()); - dangerousIngredients.removeAll(ingredientsThatContainNoAllergens); - while (!dangerousIngredients.isEmpty()) { - for (final var i = dangerousIngredients.iterator(); i.hasNext(); ) { - final var ingredient = i.next(); - boolean mappedIngredient = false; - for (final var j = allergenToIngredient.entrySet().iterator(); j.hasNext(); ) { - final var entry = j.next(); - final var allergen = entry.getKey(); - final var ingredients = entry.getValue(); - if (ingredients.size() == 1 && ingredients.contains(ingredient)) { - // this is the only ingredient known to contain this allergen - ingredientToAllergen.put(ingredient, allergen); - System.err.println("Mapping " + ingredient + " to " + allergen); - j.remove(); - mappedIngredient |= true; - break; + public Simplification simplify(final Map monkeys, final Map results) { + return job().simplify(monkeys, results); + } + + public static Monkey parse(final String line) { + final var components = line.split(": "); + final var name = components[0].trim(); + final var job = Job.parse(components[1].trim()); + return new Monkey(name, job); + } + } + + public interface Simplification { + Simplification simplify(); + } + + record Expression(Simplification x, Operation operation, Simplification y) implements Simplification { + + public Simplification simplify() { + final var simpleX = x().simplify(); + final var simpleY = y().simplify(); + if (!x().equals(simpleX) || !y().equals(simpleY)) { + return new Expression(simpleX, operation(), simpleY); + } else if (x() instanceof Value && y() instanceof Value) { + return new Value(operation().operate(((Value) x).value(), ((Value) y).value())); + } else if (operation() == Operation.IS_EQUAL) { + if (x() instanceof final Value constant && y() instanceof final Expression expression) { + // e.g. 5=2/x or 5=x/2 + final var inverse = expression.operation().inverse(); + if (expression.x() instanceof Value) { + // e.g. 5=2/x + if (expression.operation.isSymmetric()) { + // e.g. 2=5x -> 10=x + final var lValue = new Expression(constant, inverse, expression.x()).simplify(); + return new Expression(lValue, Operation.IS_EQUAL, expression.y()); + } else { + // e.g. 5=2/x -> 5x=2 + final var lValue = new Expression(constant, inverse, expression.y()); + return new Expression(lValue, Operation.IS_EQUAL, expression.x()); } + } else if (expression.y() instanceof Value) { + // e.g. 5=x/2 -> 5*2=x -> 10=x + final var lValue = new Expression(constant, inverse, expression.y()).simplify(); + return new Expression(lValue, Operation.IS_EQUAL, expression.x()); } - if (mappedIngredient) { - for (final var entry : allergenToIngredient.entrySet()) { - final var ingredients = entry.getValue(); - ingredients.remove(ingredient); + // cannot simplify further + return this; + } else if (x() instanceof final Expression expression && y() instanceof final Value constant) { + // e.g. 5/x=2 or x/5=2 + final var inverse = expression.operation().inverse(); + if (expression.x() instanceof Value) { + // e.g. 5/x=2 or x/5=2 + if (expression.operation().isSymmetric()) { + // e.g. 2x=5 -> x=5*2 -> x=10 + final var rValue = new Expression(constant, inverse, expression.x()).simplify(); + return new Expression(expression.y(), Operation.IS_EQUAL, rValue); + } else { + // e.g. 5/x=2 -> 5=2x + final var rValue = new Expression(constant, inverse, expression.y()); + return new Expression(expression.x(), Operation.IS_EQUAL, rValue); } - i.remove(); + } else if (expression.y() instanceof Value) { + // e.g. x/5=2 -> x=2*5 -> x=10 + final var rValue = new Expression(constant, inverse, expression.y()).simplify(); + return new Expression(expression.x(), Operation.IS_EQUAL, rValue); } + // cannot simplify further + return this; } } + return this; + } - final var result = - ingredientToAllergen - .entrySet() - .stream() - .sorted(Comparator.comparing(Entry::getValue)) - .map(Entry::getKey) - .collect(Collectors.joining(",")); - System.out.println("Part 2: " + result); + public String toString() { + return "(" + x() + ") " + operation() + " (" + y() + ")"; } } - public static class Food { - private final Set ingredients; - private final Set allergens; + record Value(long value) implements Simplification { + public String toString() { + return "" + value(); + } - public Food(final Set ingredients, final Set allergens) { - this.ingredients = ingredients; - this.allergens = allergens; + public Simplification simplify() { + return this; } + } - public static Food fromLine(final String line) { - final var components = line.split("\\(contains "); - final var ingredientsArray = components[0].strip().split(" "); - final var allergensString = components[1].strip().replaceAll("\\)", ""); - final var allergensArray = allergensString.split(", "); - return new Food(Set.of(ingredientsArray), Set.of(allergensArray)); + record Variable() implements Simplification { + public String toString() { + return "x"; } + public Simplification simplify() { + return this; + } } + + public interface Job { + long yell(final Map monkeys, final Map results); + + Simplification simplify(final Map monkeys, final Map results); + + static Job parse(final String string) { + final var components = string.trim().split(" "); + if (components.length == 1) { + return Yell.parse(string.trim()); + } + return Math.parse(components); + } + } + + public enum Operation { + Add { + public long operate(final long x, final long y) { + return x + y; + } + + public Operation inverse() { + return Subtract; + } + + public boolean isSymmetric() { + return true; + } + }, + Subtract { + public long operate(final long x, final long y) { + return x - y; + } + + public Operation inverse() { + return Add; + } + + public boolean isSymmetric() { + return false; + } + }, + Multiply { + public long operate(final long x, final long y) { + return x * y; + } + + public Operation inverse() { + return Divide; + } + + public boolean isSymmetric() { + return true; + } + }, + Divide { + public long operate(final long x, final long y) { + return x / y; + } + + public Operation inverse() { + return Multiply; + } + + public boolean isSymmetric() { + return false; + } + }, + IS_EQUAL { + public long operate(final long x, final long y) { + // what a horrible hack, who would do this? + return x == y ? 1L : 0L; + } + + public Operation inverse() { + return IS_EQUAL; + } + + public boolean isSymmetric() { + return true; + } + }; + + public abstract long operate(long x, long y); + + public abstract Operation inverse(); + + public abstract boolean isSymmetric(); + + public String toString() { + return switch (this) { + case Add -> "+"; + case Subtract -> "-"; + case Multiply -> "*"; + case Divide -> "/"; + case IS_EQUAL -> "="; + }; + } + + public static Operation parse(final String operator) { + return switch (operator.trim()) { + case "+" -> Add; + case "-" -> Subtract; + case "*" -> Multiply; + case "/" -> Divide; + default -> throw new IllegalArgumentException("Invalid operator: " + operator); + }; + } + } + + public record Unknown() implements Job { + public long yell(Map monkeys, Map results) { + throw new UnsupportedOperationException(); // Oof + } + + public Simplification simplify(Map monkeys, Map results) { + return new Variable(); + } + } + + record Yell(long number) implements Job { + public long yell(Map monkeys, Map results) { + return number; + } + + public Simplification simplify(Map monkeys, Map results) { + return new Value(number()); + } + + public static Yell parse(final String string) { + return new Yell(Integer.parseInt(string)); + } + } + + public record Math(String monkeyX, Operation operation, String monkeyY) implements Job { + + public long yell(Map monkeys, Map results) { + final var x = getVariable(monkeyX(), monkeys, results); + final var y = getVariable(monkeyY(), monkeys, results); + return operation().operate(x, y); + } + + public Simplification simplify(Map monkeys, Map results) { + final var x = monkeys.get(monkeyX()).simplify(monkeys, results); + final var y = monkeys.get(monkeyY()).simplify(monkeys, results); + if (x instanceof final Value xValue && y instanceof final Value yValue) { + return new Value(operation().operate(xValue.value(), yValue.value())); + } + return new Expression(x, operation(), y); + } + + long getVariable(final String monkeyName, final Map monkeys, final Map results) { + if (results.containsKey(monkeyName)) { + return results.get(monkeyName); + } + final var result = monkeys.get(monkeyName).yell(monkeys, results); + results.put(monkeyName, result); + return result; + } + + public static Math parse(final String[] components) { + final var monkeyX = components[0].trim(); + final var operation = Operation.parse(components[1].trim()); + final var monkeyY = components[2].trim(); + return new Math(monkeyX, operation, monkeyY); + } + } + + protected static Map getInput() { + final Map result = new HashMap<>(); + StreamSupport.stream(new LineSpliterator("day-21.txt"), false) + .map(Monkey::parse) + .forEach(monkey -> result.put(monkey.name(), monkey)); + return Collections.unmodifiableMap(result); + } + + @Test + public final void part1() { + final var monkeys = getInput(); + final var results = new HashMap(); + final var result = monkeys.get("root").yell(monkeys, results); + + System.out.println("Part 1: " + result); + } + + @Test + public final void part2() { + final var monkeys = new HashMap<>(getInput()); + final var results = new HashMap(); + final var oldRoot = monkeys.get("root"); + final var oldJob = (Math) oldRoot.job(); + monkeys.put("root", new Monkey("root", new Math(oldJob.monkeyX(), Operation.IS_EQUAL, oldJob.monkeyY()))); + monkeys.put("humn", new Monkey("humn", new Unknown())); + + var simplification = monkeys.get("root").simplify(monkeys, results); + while (true) { + final var candidate = simplification.simplify(); + if (candidate.equals(simplification)) { + break; + } + simplification = candidate; + } + System.out.println("Part 2: " + simplification); + } + } \ No newline at end of file diff --git a/src/test/java/com/macasaet/Day22.java b/src/test/java/com/macasaet/Day22.java deleted file mode 100644 index c6e1598..0000000 --- a/src/test/java/com/macasaet/Day22.java +++ /dev/null @@ -1,157 +0,0 @@ -package com.macasaet; - -import java.io.IOException; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Objects; -import java.util.stream.Collectors; -import java.util.stream.StreamSupport; - -public class Day22 { - - public static void main(final String[] args) throws IOException { - try (var spliterator = new LineSpliterator(Day22.class.getResourceAsStream("/day-22-input.txt"))) { - final var lines = StreamSupport.stream(spliterator, false) - .collect(Collectors.toUnmodifiableList()); - - final var player1cards = new LinkedList(); - final var player2cards = new LinkedList(); - var target = player1cards; - for (final var line : lines) { - if (line.isBlank()) { - target = player2cards; - } - if (line.matches("^[0-9]+$")) { - target.add(Integer.parseInt(line)); - } - } - var player1 = new Player(1, new LinkedList<>(player1cards)); - var player2 = new Player(2, new LinkedList<>(player2cards)); - - // Part 1 - while (player1.hasCards() && player2.hasCards()) { - final int p1 = player1.drawTop(); - final int p2 = player2.drawTop(); - if (p1 > p2) { - player1.addToBottom(p1); - player1.addToBottom(p2); - } else { - player2.addToBottom(p2); - player2.addToBottom(p1); - } - } - var winner = player1.hasCards() ? player1 : player2; - System.out.println("Part 1: " + winner.score()); - - // Part 2 - player1 = new Player(1, new LinkedList<>(player1cards)); - player2 = new Player(2, new LinkedList<>(player2cards)); - winner = playGame(player1, player2); - System.out.println("Part 2: " + winner.score()); - } - } - - protected static Player playGame(final Player player1, final Player player2) { - final var rounds = new HashSet<>(); - while (player1.hasCards() && player2.hasCards()) { - final var round = new Round(player1, player2); - if (rounds.contains(round)) { - return player1; - } - rounds.add(round); - final int p1 = player1.drawTop(); - final int p2 = player2.drawTop(); - if (player1.cardCountIsAtLeast(p1) && player2.cardCountIsAtLeast(p2)) { - final var winner = playGame(player1.clone(p1), player2.clone(p2)); - if (winner.id == player1.id) { - player1.addToBottom(p1); - player1.addToBottom(p2); - } else { - player2.addToBottom(p2); - player2.addToBottom(p1); - } - } else { - if (p1 > p2) { - player1.addToBottom(p1); - player1.addToBottom(p2); - } else { - player2.addToBottom(p2); - player2.addToBottom(p1); - } - } - } - return player1.hasCards() ? player1 : player2; - } - - protected static class Player { - final int id; - final List deck; - - public Player(final int id, final List deck) { - this.id = id; - this.deck = deck; - } - - public boolean hasCards() { - return !deck.isEmpty(); - } - - public boolean cardCountIsAtLeast(final int count) { - return deck.size() >= count; - } - - public int drawTop() { - return deck.remove(0); - } - - public void addToBottom(final int card) { - deck.add(card); - } - - public Player clone(final int cardCount) { - return new Player(id, new LinkedList<>(deck.subList(0, cardCount))); - } - - public int score() { - int retval = 0; - int multiplier = deck.size(); - for (final var card : deck) { - retval += card * (multiplier--); - } - return retval; - } - } - - protected static class Round { - private final List x; - private final List y; - - public Round(final List x, final List y) { - this.x = List.copyOf(x); - this.y = List.copyOf(y); - } - - public Round(final Player x, final Player y) { - this(x.deck, y.deck); - } - - public int hashCode() { - return Objects.hash(x, y); - } - - public boolean equals(final Object o) { - if (this == o) { - return true; - } else if (o == null) { - return false; - } - try { - final Round other = (Round) o; - return Objects.equals(x, other.x) && Objects.equals(y, other.y); - } catch (final ClassCastException cce) { - return false; - } - } - } -} \ No newline at end of file diff --git a/src/test/java/com/macasaet/Day23.java b/src/test/java/com/macasaet/Day23.java index aa1f327..b701a77 100644 --- a/src/test/java/com/macasaet/Day23.java +++ b/src/test/java/com/macasaet/Day23.java @@ -1,104 +1,644 @@ package com.macasaet; -import java.io.IOException; -import java.math.BigInteger; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; +/** + * --- Day 23: Unstable Diffusion --- + * https://adventofcode.com/2022/day/23 + */ public class Day23 { - public static void main(final String[] args) throws IOException { - final var labels = new ArrayList(); - try (var inputStream = Day23.class.getResourceAsStream("/day-23-input.txt")) { - for (final var c : new String(inputStream.readAllBytes()).strip().toCharArray()) { - labels.add(Integer.parseInt("" + c)); - } - } - - final var map = new Cup[1_000_0000 + 1]; - Cup first = null; - Cup current = null; - int min = Integer.MAX_VALUE; - int max = Integer.MIN_VALUE; - for (final int label : labels) { - final var cup = new Cup(label); - map[label] = cup; - if (first == null) { - first = cup; - } else { - current.next = cup; - } - current = cup; - - min = Math.min(min, label); - max = Math.max(max, label); - } - - for (int i = max + 1; i <= 1_000_000; i++) { // Part 2 - final var cup = new Cup(i); - map[i] = cup; - current.next = cup; - current = cup; - } - max = 1_000_000; - current.next = first; - current = current.next; - - for (int moveId = 1; moveId <= 10_000_000; moveId++) { // Part 1 looped only 100 times - final var a = current.take(); - final var b = current.take(); - final var c = current.take(); - - int destinationValue = current.label - 1; - while (destinationValue == a.label || destinationValue == b.label || destinationValue == c.label || destinationValue < min || destinationValue > max) { - destinationValue--; - if (destinationValue < min) { - destinationValue = max; + record Coordinate(int x, int y) { + public boolean hasElf(final Map> grid) { + return grid.getOrDefault(x(), Collections.emptyMap()).getOrDefault(y(), false); + } + + public Set neighbours() { + final var neighbours = new HashSet(8); + for (int i = x() - 1; i <= x() + 1; i++) { + for (int j = y() - 1; j <= y() + 1; j++) { + if (i == x() && j == y()) { + continue; + } + neighbours.add(new Coordinate(i, j)); } } + return Collections.unmodifiableSet(neighbours); + } + } + + enum Direction { + North { + public Set relativeCoordinates(Coordinate reference) { + return Set.of(new Coordinate(reference.x() - 1, reference.y() - 1), + adjacent(reference), + new Coordinate(reference.x() - 1, reference.y() + 1)); + } + + public Coordinate adjacent(Coordinate reference) { + return new Coordinate(reference.x() - 1, reference.y()); + } + }, + East { + public Set relativeCoordinates(Coordinate reference) { + return Set.of(new Coordinate(reference.x() - 1, reference.y() + 1), + adjacent(reference), + new Coordinate(reference.x() + 1, reference.y() + 1)); + } + + public Coordinate adjacent(Coordinate reference) { + return new Coordinate(reference.x(), reference.y() + 1); + } + }, + South { + public Set relativeCoordinates(Coordinate reference) { + return Set.of(new Coordinate(reference.x() + 1, reference.y() - 1), + adjacent(reference), + new Coordinate(reference.x() + 1, reference.y() + 1)); + } + + public Coordinate adjacent(Coordinate reference) { + return new Coordinate(reference.x() + 1, reference.y()); + } + }, + West { + public Set relativeCoordinates(Coordinate reference) { + return Set.of(new Coordinate(reference.x() - 1, reference.y() - 1), + adjacent(reference), + new Coordinate(reference.x() + 1, reference.y() - 1)); + } + + public Coordinate adjacent(Coordinate reference) { + return new Coordinate(reference.x(), reference.y() - 1); + } + }; - final var destination = map[destinationValue]; - c.linkBefore(destination.next); - b.linkBefore(c); - a.linkBefore(b); - destination.linkBefore(a); - - current = current.next; - } - final var reference = map[1]; -// String result = ""; -// var cursor = reference.next; -// for (int i = labels.size() - 1; --i >= 0; ) { -// result += cursor.label; -// cursor = cursor.next; -// } -// System.out.println("Part 1: " + result); - final BigInteger x = new BigInteger("" + map[reference.next.label].label); - final BigInteger y = new BigInteger("" + map[reference.next.next.label].label); - System.out.println("Part 2: " + x.multiply(y).toString()); + public abstract Set relativeCoordinates(Coordinate reference); + + public abstract Coordinate adjacent(Coordinate reference); } - protected static class Cup { - private final int label; - private Cup next; + public static class Crater { + + private final Map> grid; + private int minX; + private int maxX; + private int minY; + private int maxY; + private List movementPriority = + new ArrayList<>(Arrays.asList(Direction.North, Direction.South, Direction.West, Direction.East)); - public Cup(final int label) { - this.label = label; + public Crater(final Map> grid, int minX, int maxX, int minY, int maxY) { + this.grid = grid; + setMinX(minX); + setMinY(minY); + setMaxX(maxX); + setMaxY(maxY); } - public Cup take() { - final Cup retval = next; - next = retval.next; - retval.next = null; - return retval; + public int round() { + final var destinations = new HashMap>(); + final var moves = new HashMap(); + // first half of round: planning phase + for (int i = getMinX(); i <= getMaxX(); i++) { + final var row = getGrid().get(i); + for (int j = getMinY(); j <= getMaxY(); j++) { + final var from = new Coordinate(i, j); + if (row.getOrDefault(j, false)) { + // determine destination + final var hasNeighbour = from.neighbours() + .stream() + .anyMatch(c -> getGrid().getOrDefault(c.x(), Collections.emptyMap()) + .getOrDefault(c.y(), false)); + if (!hasNeighbour) { + // "If no other Elves are in one of those eight positions, the Elf does not do anything + // during this round." + continue; + } + for (final var direction : getMovementPriority()) { + final var clear = direction.relativeCoordinates(from) + .stream() + .noneMatch(neighbour -> neighbour.hasElf(getGrid())); + if (clear) { + final var to = direction.adjacent(from); + destinations.computeIfAbsent(to, _row -> new HashSet<>()).add(from); + moves.put(from, to); + break; + } + } + } + } + } + + // second half of round: movement phase + for (final var move : moves.entrySet()) { + final var from = move.getKey(); + final var to = move.getValue(); + // "each Elf moves to their proposed destination tile if they were the only Elf to propose moving to + // that position. If two or more Elves propose moving to the same position, none of those Elves move." + if (destinations.get(to).size() == 1) { + getGrid().computeIfAbsent(to.x(), _row -> new HashMap<>()).put(to.y(), true); + getGrid().get(from.x()).put(from.y(), false); + setMinX(Math.min(getMinX(), to.x())); + setMaxX(Math.max(getMaxX(), to.x())); + setMinY(Math.min(getMinY(), to.y())); + setMaxY(Math.max(getMaxY(), to.y())); + } + } + // prune edges + // minX + final var highestRow = getGrid().get(getMinX()); + if (highestRow.values().stream().noneMatch(hasElf -> hasElf)) { + getGrid().remove(getMinX()); + setMinX(getMinX() + 1); + } + // maxX + final var lowestRow = getGrid().get(getMaxX()); + if (lowestRow.values().stream().noneMatch(hasElf -> hasElf)) { + getGrid().remove(getMaxX()); + setMaxX(getMaxX() - 1); + } + // minY + if (getGrid().values().stream().map(row -> row.get(getMinY())).noneMatch(hasElf -> hasElf != null && hasElf)) { + for (final var row : getGrid().values()) { + row.remove(getMinY()); + } + setMinY(getMinY() + 1); + } + // maxY + if (getGrid().values().stream().map(row -> row.get(getMaxY())).noneMatch(hasElf -> hasElf != null && hasElf)) { + for (final var row : getGrid().values()) { + row.remove(getMaxY()); + } + setMaxY(getMaxY() + 1); + } + + // "Finally, at the end of the round, the first direction the Elves considered is moved to the end of the + // list of directions." + final var previousFirst = getMovementPriority().remove(0); + getMovementPriority().add(previousFirst); + return moves.size(); + } + + public int countEmptyGroundTiles() { + int result = 0; + for (int i = getMinX(); i <= getMaxX(); i++) { + final var row = getGrid().getOrDefault(i, Collections.emptyMap()); + for (int j = getMinY(); j <= getMaxY(); j++) { + if (!row.getOrDefault(j, false)) { + result++; + } + } + } + return result; } - public void linkBefore(final Cup cup) { - next = cup; + public static Crater fromString(final String block) { + final var lines = block.split("\n"); + final Map> grid = new HashMap<>(lines.length); + int minX = Integer.MAX_VALUE, minY = Integer.MAX_VALUE; + int maxX = Integer.MIN_VALUE, maxY = Integer.MIN_VALUE; + for (int i = lines.length; --i >= 0; ) { + final var line = lines[i]; + final var chars = line.toCharArray(); + final Map row = new HashMap<>(chars.length); + boolean rowHasElf = false; + for (int j = chars.length; --j >= 0; ) { + if (chars[j] == '#') { + minY = Math.min(minY, j); + maxY = Math.max(maxY, j); + row.put(j, true); + rowHasElf |= true; + } else { + row.put(j, false); + } + } + grid.put(i, row); + if (rowHasElf) { + minX = Math.min(minX, i); + maxX = Math.max(maxX, i); + } + } + return new Crater(grid, minX, maxX, minY, maxY); } public String toString() { - return "Cup{ label=" + label + ", next=" + (next != null ? next.label : null) + " }"; + final var builder = new StringBuilder(); + builder.append("X: [").append(getMinX()).append(", ").append(getMaxX()).append("]\n"); + builder.append("Y: [").append(getMinY()).append(", ").append(getMaxY()).append("]\n"); + builder.append("Movement priority: ").append(getMovementPriority()).append("\n"); + appendGrid(builder); + return builder.toString(); + } + + protected void appendGrid(final StringBuilder builder) { + for (int i = getMinX(); i <= getMaxX(); i++) { + final var row = getGrid().getOrDefault(i, Collections.emptyMap()); + for (int j = getMinY(); j <= getMaxY(); j++) { + final var c = row.getOrDefault(j, false) + ? '#' + : '.'; + builder.append(c); + } + builder.append('\n'); + } + } + + protected Map> getGrid() { + return grid; + } + + protected int getMinX() { + return minX; + } + + protected void setMinX(int minX) { + this.minX = minX; + } + + protected int getMaxX() { + return maxX; + } + + protected void setMaxX(int maxX) { + this.maxX = maxX; + } + + protected int getMinY() { + return minY; + } + + protected void setMinY(int minY) { + this.minY = minY; + } + + protected int getMaxY() { + return maxY; + } + + protected void setMaxY(int maxY) { + this.maxY = maxY; + } + + protected List getMovementPriority() { + return movementPriority; + } + + protected void setMovementPriority(List movementPriority) { + this.movementPriority = movementPriority; + } + } + + protected static Crater getInput() { + final var lines = StreamSupport.stream(new LineSpliterator("day-23.txt"), false) + .collect(Collectors.joining("\n")); + return Crater.fromString(lines); + } + + @Test + public final void part1() { + final var crater = getInput(); + for (int i = 10; --i >= 0; crater.round()) ; + final var result = crater.countEmptyGroundTiles(); + + System.out.println("Part 1: " + result); + } + + @Test + public final void testRound1() { + // given + final var block = """ + ## + #. + .. + ## + """; + final var crater = Crater.fromString(block); + + // when + crater.round(); + + // then + final var builder = new StringBuilder(); + crater.appendGrid(builder); + final var result = builder.toString(); + Assertions.assertEquals(""" + ## + .. + #. + .# + #. + """, result); + } + + @Test + public final void testRound2() { + // given + final var block = """ + ## + .. + #. + .# + #. + """; + final var crater = Crater.fromString(block); + crater.setMovementPriority(new ArrayList<>(Arrays.asList(Direction.South, Direction.West, Direction.East, Direction.North))); + + // when + crater.round(); + + // then + final var builder = new StringBuilder(); + crater.appendGrid(builder); + final var result = builder.toString(); + Assertions.assertEquals(""" + .##. + #... + ...# + .... + .#.. + """, result); + } + + @Test + public final void testRound3() { + // given + final var block = """ + .##. + #... + ...# + .... + .#.. + """; + final var crater = Crater.fromString(block); + crater.setMovementPriority(new ArrayList<>(Arrays.asList(Direction.West, Direction.East, Direction.North, Direction.South))); + + // when + crater.round(); + + // then + final var builder = new StringBuilder(); + crater.appendGrid(builder); + final var result = builder.toString(); + Assertions.assertEquals(""" + ..#.. + ....# + #.... + ....# + ..... + ..#.. + """, result); + } + + @Test + public final void testLargerRound1() { + // given + final var block = """ + .......#...... + .....###.#.... + ...#...#.#.... + ....#...##.... + ...#.###...... + ...##.#.##.... + ....#..#...... + """; + final var crater = Crater.fromString(block); + + // when + crater.round(); + + // then + final var builder = new StringBuilder(); + crater.appendGrid(builder); + final var result = builder.toString(); + Assertions.assertEquals(""" + .....#... + ...#...#. + .#..#.#.. + .....#..# + ..#.#.##. + #..#.#... + #.#.#.##. + ......... + ..#..#... + """, result); + } + + @Test + public final void testLargerRound2() { + // given + final var block = """ + .....#... + ...#...#. + .#..#.#.. + .....#..# + ..#.#.##. + #..#.#... + #.#.#.##. + ......... + ..#..#... + """; + final var crater = Crater.fromString(block); + crater.setMovementPriority(new ArrayList<>(Arrays.asList(Direction.South, Direction.West, Direction.East, Direction.North))); + + // when + crater.round(); + + // then + final var builder = new StringBuilder(); + crater.appendGrid(builder); + final var result = builder.toString(); + Assertions.assertEquals(""" + ......#.... + ...#.....#. + ..#..#.#... + ......#...# + ..#..#.#... + #...#.#.#.. + ........... + .#.#.#.##.. + ...#..#.... + """, result); + } + + @Test + public final void testLargerRound3() { + // given + final var block = """ + ......#.... + ...#.....#. + ..#..#.#... + ......#...# + ..#..#.#... + #...#.#.#.. + ........... + .#.#.#.##.. + ...#..#.... + """; + final var crater = Crater.fromString(block); + crater.setMovementPriority(new ArrayList<>(Arrays.asList(Direction.West, Direction.East, Direction.North, Direction.South))); + + // when + crater.round(); + + // then + final var builder = new StringBuilder(); + crater.appendGrid(builder); + final var result = builder.toString(); + Assertions.assertEquals(""" + ......#.... + ....#....#. + .#..#...#.. + ......#...# + ..#..#.#... + #..#.....#. + ......##... + .##.#....#. + ..#........ + ......#.... + """, result); + } + + @Test + public final void testLargerRound4() { + // given + final var block = """ + ......#.... + ....#....#. + .#..#...#.. + ......#...# + ..#..#.#... + #..#.....#. + ......##... + .##.#....#. + ..#........ + ......#.... + """; + final var crater = Crater.fromString(block); + crater.setMovementPriority(new ArrayList<>(Arrays.asList(Direction.East, Direction.North, Direction.South, Direction.West))); + + // when + crater.round(); + + // then + final var builder = new StringBuilder(); + crater.appendGrid(builder); + final var result = builder.toString(); + Assertions.assertEquals(""" + ......#.... + .....#....# + .#...##.... + ..#.....#.# + ........#.. + #...###..#. + .#......#.. + ...##....#. + ...#....... + ......#.... + """, result); + } + + @Test + public final void testLargerRound5() { + // given + final var block = """ + ......#.... + .....#....# + .#...##.... + ..#.....#.# + ........#.. + #...###..#. + .#......#.. + ...##....#. + ...#....... + ......#.... + """; + final var crater = Crater.fromString(block); + crater.setMovementPriority(new ArrayList<>(Arrays.asList(Direction.North, Direction.South, Direction.West, Direction.East))); + + // when + crater.round(); + + // then + final var builder = new StringBuilder(); + crater.appendGrid(builder); + final var result = builder.toString(); + Assertions.assertEquals(""" + ......#.... + ........... + .#..#.....# + ........#.. + .....##...# + #.#.####... + ..........# + ...##..#... + .#......... + .........#. + ...#..#.... + """, result); + } + + @Test + public final void testLargerRounds() { + // given + final var block = """ + .......#...... + .....###.#.... + ...#...#.#.... + ....#...##.... + ...#.###...... + ...##.#.##.... + ....#..#...... + """; + final var crater = Crater.fromString(block); + + // when + for (int i = 10; --i >= 0; crater.round()) ; + + // then + final var builder = new StringBuilder(); + crater.appendGrid(builder); + final var result = builder.toString(); + Assertions.assertEquals(""" + ......#..... + ..........#. + .#.#..#..... + .....#...... + ..#.....#..# + #......##... + ....##...... + .#........#. + ...#.#..#... + ............ + ...#..#..#.. + """, result); + Assertions.assertEquals(110, crater.countEmptyGroundTiles()); + } + + @Test + public final void part2() { + final var crater = getInput(); + int rounds = 0; + while (true) { + final var moves = crater.round(); + rounds++; + if (moves == 0) { + break; + } } + System.out.println("Part 2: " + rounds); } } \ No newline at end of file diff --git a/src/test/java/com/macasaet/Day24.java b/src/test/java/com/macasaet/Day24.java deleted file mode 100644 index 3d3282e..0000000 --- a/src/test/java/com/macasaet/Day24.java +++ /dev/null @@ -1,179 +0,0 @@ -package com.macasaet; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map.Entry; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; - -public class Day24 { - - public static void main(final String[] args) throws IOException { - final Address origin = new Address(0, 0, 0); - final var lobbyFloor = new HashMap(); - try (var spliterator = new LineSpliterator(Day22.class.getResourceAsStream("/day-24-input.txt"))) { - StreamSupport.stream(spliterator, false) - .map(String::toCharArray) - .map(array -> { - int i = 0; - var directions = new ArrayList(array.length); - while (i < array.length) { - if (i < array.length - 1) { // check if there are at least 2 chars available - final var twoLetterDirection = "" + array[i] + array[i + 1]; - // check if two chars is a valid direction - if ("se".equalsIgnoreCase(twoLetterDirection) - || "sw".equalsIgnoreCase(twoLetterDirection) - || "nw".equalsIgnoreCase(twoLetterDirection) - || "ne".equalsIgnoreCase(twoLetterDirection)) { - directions.add(Direction.forAbbreviation(twoLetterDirection)); - i = i + 2; - continue; - } - } - final var oneLetterDirection = "" + array[i]; - if ("e".equalsIgnoreCase(oneLetterDirection) || "w".equalsIgnoreCase(oneLetterDirection)) { - directions.add(Direction.forAbbreviation(oneLetterDirection)); - i = i + 1; - continue; - } - throw new IllegalArgumentException("Invalid direction: " + oneLetterDirection); - } - return Collections.unmodifiableList(directions); - }).map(directions -> { - Address cursor = origin; - for (final var direction : directions) { - cursor = direction.travel(cursor); - } - return cursor; - }).forEach(address -> lobbyFloor.merge(address, 1, (old, def) -> old + 1)); - } - final int blackTiles = lobbyFloor.values().stream().mapToInt(count -> count % 2).sum(); - System.out.println("Part 1: " + blackTiles); - - for (int i = 1; i <= 100; i++) { - final var tasks = lobbyFloor.entrySet().stream().flatMap(entry -> { - // get the mapped tile - // as well as unmapped (white) adjacent tiles - final var from = entry.getKey(); - return Stream.concat(Stream.of(entry), Arrays.stream(Direction.values()).flatMap(direction -> { - final var neighbour = direction.travel(from); - if (!lobbyFloor.containsKey(neighbour)) { - // neighbour has never been visited, create a virtual entry for them - return Stream.of(new Entry() { - public Address getKey() { - return neighbour; - } - - public Integer getValue() { - return 0; - } - - public Integer setValue(Integer value) { - throw new UnsupportedOperationException(); - } - }); - } - return Stream.empty(); - })); - }).map(entry -> { // NB: this might not be a real tile - final var address = entry.getKey(); - final int adjacentBlackTiles = Arrays.stream(Direction.values()) - .map(direction -> direction.travel(address)) - .mapToInt(neighbour -> { - final Integer neighbouringCount = lobbyFloor.get(neighbour); - return neighbouringCount != null && neighbouringCount % 2 == 1 ? 1 : 0; - }).sum(); - if (entry.getValue() % 2 == 1 && (adjacentBlackTiles == 0 || adjacentBlackTiles > 2)) { - // Any black tile with zero or more than 2 black tiles immediately adjacent to it is flipped to white. - return (Runnable) () -> lobbyFloor.put(address, 0); - } else if (entry.getValue() % 2 == 0 && adjacentBlackTiles == 2) { - // Any white tile with exactly 2 black tiles immediately adjacent to it is flipped to black. - return (Runnable) () -> lobbyFloor.put(address, 1); - } - return (Runnable) () -> { - }; - }).collect(Collectors.toUnmodifiableList()); - for (final var task : tasks) { - task.run(); - } - } - final int count = lobbyFloor.values().stream().mapToInt(value -> value % 2).sum(); - System.out.println("Part 2: " + count); - } - - public enum Direction { - EAST("e", 1, -1, 0), - SOUTH_EAST("se", 0, -1, 1), - SOUTH_WEST("sw", -1, 0, 1), - WEST("w", -1, 1, 0), - NORTH_WEST("nw", 0, 1, -1), - NORTH_EAST("ne", 1, 0, -1); - - private final String abbreviation; - private final int xOffset; - private final int yOffset; - private final int zOffset; - - Direction(final String abbreviation, final int xOffset, final int yOffset, final int zOffset) { - this.abbreviation = abbreviation; - this.xOffset = xOffset; - this.yOffset = yOffset; - this.zOffset = zOffset; - } - - public static Direction forAbbreviation(final String abbreviation) { - for (final var candidate : values()) { - if (candidate.abbreviation.equalsIgnoreCase(abbreviation)) { - return candidate; - } - } - throw new IllegalArgumentException("Invalid direction: " + abbreviation); - } - - public Address travel(final Address from) { - return new Address(from.x + xOffset, from.y + yOffset, from.z + zOffset); - } - - } - - public static class Address { - - private final int x; - private final int y; - private final int z; - - public Address(final int x, final int y, final int z) { - this.x = x; - this.y = y; - this.z = z; - } - - public int hashCode() { - int retval = 0; - retval = 31 * retval + x; - retval = 31 * retval + y; - retval = 31 * retval + z; - return retval; - } - - public boolean equals(final Object o) { - if (this == o) { - return true; - } else if (o == null) { - return false; - } - try { - final Address other = (Address) o; - return x == other.x && y == other.y && z == other.z; - } catch (final ClassCastException cce) { - return false; - } - } - - } - -} \ No newline at end of file diff --git a/src/test/java/com/macasaet/Day25.java b/src/test/java/com/macasaet/Day25.java deleted file mode 100644 index 66be1df..0000000 --- a/src/test/java/com/macasaet/Day25.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.macasaet; - -import java.io.IOException; -import java.util.stream.Collectors; -import java.util.stream.StreamSupport; - -public class Day25 { - - public static void main(final String[] args) throws IOException { - try (var spliterator = new LineSpliterator(Day22.class.getResourceAsStream("/day-25-input.txt"))) { - final var publicKeys = StreamSupport.stream(spliterator, false) - .map(Integer::parseInt) - .collect(Collectors.toUnmodifiableList()); - final int cardPublicKey = publicKeys.get(0); - final int doorPublicKey = publicKeys.get(1); - final long initialSubjectNumber = 7; - - int cardLoopSize = 0; - for (long value = 1; value != cardPublicKey; cardLoopSize++) { - value = transformOnce(value, initialSubjectNumber); - } - System.out.println("cardLoopSize: " + cardLoopSize); - - int doorLoopSize = 0; - for (long value = 1; value != doorPublicKey; doorLoopSize++) { - value = transformOnce(value, initialSubjectNumber); - } - System.out.println("doorLoopSize: " + doorLoopSize); - - final long e1 = transformCompletely(doorPublicKey, cardLoopSize); - final long e2 = transformCompletely(cardPublicKey, doorLoopSize); - System.out.println("e1: " + e1); - System.out.println("e2: " + e2); - } - } - - protected static long transformCompletely(final long subjectNumber, final int loopSize) { - long value = 1; - for (int i = loopSize; --i >= 0; value = transformOnce(value, subjectNumber)) ; - return value; - } - - protected static long transformOnce(final long value, final long subjectNumber) { - return (value * subjectNumber) % 20201227; - } - -} \ No newline at end of file diff --git a/src/test/java/com/macasaet/LineSpliterator.java b/src/test/java/com/macasaet/LineSpliterator.java index 5333f65..6e3c360 100644 --- a/src/test/java/com/macasaet/LineSpliterator.java +++ b/src/test/java/com/macasaet/LineSpliterator.java @@ -1,18 +1,26 @@ package com.macasaet; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.Reader; +import java.io.*; import java.util.Objects; +import java.util.Properties; import java.util.Spliterator; import java.util.function.Consumer; public class LineSpliterator implements Spliterator, AutoCloseable { + private static final String prefix; private final BufferedReader reader; + static { + final var properties = new Properties(); + try { + final var config = LineSpliterator.class.getResourceAsStream("/config.properties"); + if (config != null) properties.load(config); + } catch (final IOException ignored) { + } + prefix = properties.getProperty("prefix", "/sample"); + } + public LineSpliterator(final BufferedReader reader) { Objects.requireNonNull(reader); this.reader = reader; @@ -26,6 +34,10 @@ public LineSpliterator(final InputStream stream) { this(new InputStreamReader(stream)); } + public LineSpliterator(final String fileName) { + this(LineSpliterator.class.getResourceAsStream(prefix + "/" + fileName)); + } + public boolean tryAdvance(final Consumer action) { try { final var line = reader.readLine(); @@ -35,7 +47,6 @@ public boolean tryAdvance(final Consumer action) { } reader.close(); } catch (final IOException ioe) { - ioe.printStackTrace(); } return false; } diff --git a/src/test/resources/sample/day-01.txt b/src/test/resources/sample/day-01.txt new file mode 100644 index 0000000..444e241 --- /dev/null +++ b/src/test/resources/sample/day-01.txt @@ -0,0 +1,14 @@ +1000 +2000 +3000 + +4000 + +5000 +6000 + +7000 +8000 +9000 + +10000 \ No newline at end of file diff --git a/src/test/resources/sample/day-02.txt b/src/test/resources/sample/day-02.txt new file mode 100644 index 0000000..25097e8 --- /dev/null +++ b/src/test/resources/sample/day-02.txt @@ -0,0 +1,3 @@ +A Y +B X +C Z \ No newline at end of file diff --git a/src/test/resources/sample/day-03.txt b/src/test/resources/sample/day-03.txt new file mode 100644 index 0000000..9919ffa --- /dev/null +++ b/src/test/resources/sample/day-03.txt @@ -0,0 +1,6 @@ +vJrwpWtwJgWrhcsFMMfFFhFp +jqHRNqRjqzjGDLGLrsFMfFZSrLrFZsSL +PmmdzqPrVvPwwTWBwg +wMqvLMZHhHMvwLHjbvcjnnSBnvTQFn +ttgJtRGJQctTZtZT +CrZsJsPPZsGzwwsLwLmpwMDw \ No newline at end of file diff --git a/src/test/resources/sample/day-04.txt b/src/test/resources/sample/day-04.txt new file mode 100644 index 0000000..99a66c5 --- /dev/null +++ b/src/test/resources/sample/day-04.txt @@ -0,0 +1,6 @@ +2-4,6-8 +2-3,4-5 +5-7,7-9 +2-8,3-7 +6-6,4-6 +2-6,4-8 \ No newline at end of file diff --git a/src/test/resources/sample/day-05.txt b/src/test/resources/sample/day-05.txt new file mode 100644 index 0000000..e98aba4 --- /dev/null +++ b/src/test/resources/sample/day-05.txt @@ -0,0 +1,9 @@ + [D] +[N] [C] +[Z] [M] [P] + 1 2 3 + +move 1 from 2 to 1 +move 3 from 1 to 3 +move 2 from 2 to 1 +move 1 from 1 to 2 \ No newline at end of file diff --git a/src/test/resources/sample/day-06.txt b/src/test/resources/sample/day-06.txt new file mode 100644 index 0000000..5a2b0a7 --- /dev/null +++ b/src/test/resources/sample/day-06.txt @@ -0,0 +1 @@ +mjqjpqmgbljsphdztnvjfqwrcgsmlb \ No newline at end of file diff --git a/src/test/resources/sample/day-07.txt b/src/test/resources/sample/day-07.txt new file mode 100644 index 0000000..bcbb513 --- /dev/null +++ b/src/test/resources/sample/day-07.txt @@ -0,0 +1,23 @@ +$ cd / +$ ls +dir a +14848514 b.txt +8504156 c.dat +dir d +$ cd a +$ ls +dir e +29116 f +2557 g +62596 h.lst +$ cd e +$ ls +584 i +$ cd .. +$ cd .. +$ cd d +$ ls +4060174 j +8033020 d.log +5626152 d.ext +7214296 k \ No newline at end of file diff --git a/src/test/resources/sample/day-08.txt b/src/test/resources/sample/day-08.txt new file mode 100644 index 0000000..6557024 --- /dev/null +++ b/src/test/resources/sample/day-08.txt @@ -0,0 +1,5 @@ +30373 +25512 +65332 +33549 +35390 \ No newline at end of file diff --git a/src/test/resources/sample/day-09.txt b/src/test/resources/sample/day-09.txt new file mode 100644 index 0000000..cbea2b3 --- /dev/null +++ b/src/test/resources/sample/day-09.txt @@ -0,0 +1,8 @@ +R 4 +U 4 +L 3 +D 1 +R 4 +D 1 +L 5 +R 2 \ No newline at end of file diff --git a/src/test/resources/sample/day-10.txt b/src/test/resources/sample/day-10.txt new file mode 100644 index 0000000..94cd0a8 --- /dev/null +++ b/src/test/resources/sample/day-10.txt @@ -0,0 +1,146 @@ +addx 15 +addx -11 +addx 6 +addx -3 +addx 5 +addx -1 +addx -8 +addx 13 +addx 4 +noop +addx -1 +addx 5 +addx -1 +addx 5 +addx -1 +addx 5 +addx -1 +addx 5 +addx -1 +addx -35 +addx 1 +addx 24 +addx -19 +addx 1 +addx 16 +addx -11 +noop +noop +addx 21 +addx -15 +noop +noop +addx -3 +addx 9 +addx 1 +addx -3 +addx 8 +addx 1 +addx 5 +noop +noop +noop +noop +noop +addx -36 +noop +addx 1 +addx 7 +noop +noop +noop +addx 2 +addx 6 +noop +noop +noop +noop +noop +addx 1 +noop +noop +addx 7 +addx 1 +noop +addx -13 +addx 13 +addx 7 +noop +addx 1 +addx -33 +noop +noop +noop +addx 2 +noop +noop +noop +addx 8 +noop +addx -1 +addx 2 +addx 1 +noop +addx 17 +addx -9 +addx 1 +addx 1 +addx -3 +addx 11 +noop +noop +addx 1 +noop +addx 1 +noop +noop +addx -13 +addx -19 +addx 1 +addx 3 +addx 26 +addx -30 +addx 12 +addx -1 +addx 3 +addx 1 +noop +noop +noop +addx -9 +addx 18 +addx 1 +addx 2 +noop +noop +addx 9 +noop +noop +noop +addx -1 +addx 2 +addx -37 +addx 1 +addx 3 +noop +addx 15 +addx -21 +addx 22 +addx -6 +addx 1 +noop +addx 2 +addx 1 +noop +addx -10 +noop +noop +addx 20 +addx 1 +addx 2 +addx 2 +addx -6 +addx -11 +noop +noop +noop \ No newline at end of file diff --git a/src/test/resources/sample/day-11.txt b/src/test/resources/sample/day-11.txt new file mode 100644 index 0000000..c04eddb --- /dev/null +++ b/src/test/resources/sample/day-11.txt @@ -0,0 +1,27 @@ +Monkey 0: + Starting items: 79, 98 + Operation: new = old * 19 + Test: divisible by 23 + If true: throw to monkey 2 + If false: throw to monkey 3 + +Monkey 1: + Starting items: 54, 65, 75, 74 + Operation: new = old + 6 + Test: divisible by 19 + If true: throw to monkey 2 + If false: throw to monkey 0 + +Monkey 2: + Starting items: 79, 60, 97 + Operation: new = old * old + Test: divisible by 13 + If true: throw to monkey 1 + If false: throw to monkey 3 + +Monkey 3: + Starting items: 74 + Operation: new = old + 3 + Test: divisible by 17 + If true: throw to monkey 0 + If false: throw to monkey 1 \ No newline at end of file diff --git a/src/test/resources/sample/day-12.txt b/src/test/resources/sample/day-12.txt new file mode 100644 index 0000000..86e9cac --- /dev/null +++ b/src/test/resources/sample/day-12.txt @@ -0,0 +1,5 @@ +Sabqponm +abcryxxl +accszExk +acctuvwj +abdefghi diff --git a/src/test/resources/sample/day-13.txt b/src/test/resources/sample/day-13.txt new file mode 100644 index 0000000..27c8912 --- /dev/null +++ b/src/test/resources/sample/day-13.txt @@ -0,0 +1,23 @@ +[1,1,3,1,1] +[1,1,5,1,1] + +[[1],[2,3,4]] +[[1],4] + +[9] +[[8,7,6]] + +[[4,4],4,4] +[[4,4],4,4,4] + +[7,7,7,7] +[7,7,7] + +[] +[3] + +[[[]]] +[[]] + +[1,[2,[3,[4,[5,6,7]]]],8,9] +[1,[2,[3,[4,[5,6,0]]]],8,9] \ No newline at end of file diff --git a/src/test/resources/sample/day-14.txt b/src/test/resources/sample/day-14.txt new file mode 100644 index 0000000..1926028 --- /dev/null +++ b/src/test/resources/sample/day-14.txt @@ -0,0 +1,2 @@ +498,4 -> 498,6 -> 496,6 +503,4 -> 502,4 -> 502,9 -> 494,9 \ No newline at end of file diff --git a/src/test/resources/sample/day-15.txt b/src/test/resources/sample/day-15.txt new file mode 100644 index 0000000..652e631 --- /dev/null +++ b/src/test/resources/sample/day-15.txt @@ -0,0 +1,14 @@ +Sensor at x=2, y=18: closest beacon is at x=-2, y=15 +Sensor at x=9, y=16: closest beacon is at x=10, y=16 +Sensor at x=13, y=2: closest beacon is at x=15, y=3 +Sensor at x=12, y=14: closest beacon is at x=10, y=16 +Sensor at x=10, y=20: closest beacon is at x=10, y=16 +Sensor at x=14, y=17: closest beacon is at x=10, y=16 +Sensor at x=8, y=7: closest beacon is at x=2, y=10 +Sensor at x=2, y=0: closest beacon is at x=2, y=10 +Sensor at x=0, y=11: closest beacon is at x=2, y=10 +Sensor at x=20, y=14: closest beacon is at x=25, y=17 +Sensor at x=17, y=20: closest beacon is at x=21, y=22 +Sensor at x=16, y=7: closest beacon is at x=15, y=3 +Sensor at x=14, y=3: closest beacon is at x=15, y=3 +Sensor at x=20, y=1: closest beacon is at x=15, y=3 \ No newline at end of file diff --git a/src/test/resources/sample/day-18.txt b/src/test/resources/sample/day-18.txt new file mode 100644 index 0000000..d18bf98 --- /dev/null +++ b/src/test/resources/sample/day-18.txt @@ -0,0 +1,13 @@ +2,2,2 +1,2,2 +3,2,2 +2,1,2 +2,3,2 +2,2,1 +2,2,3 +2,2,4 +2,2,6 +1,2,5 +3,2,5 +2,1,5 +2,3,5 \ No newline at end of file diff --git a/src/test/resources/sample/day-20.txt b/src/test/resources/sample/day-20.txt new file mode 100644 index 0000000..5cbf3d9 --- /dev/null +++ b/src/test/resources/sample/day-20.txt @@ -0,0 +1,7 @@ +1 +2 +-3 +3 +-2 +0 +4 \ No newline at end of file diff --git a/src/test/resources/sample/day-21.txt b/src/test/resources/sample/day-21.txt new file mode 100644 index 0000000..7993b87 --- /dev/null +++ b/src/test/resources/sample/day-21.txt @@ -0,0 +1,15 @@ +root: pppw + sjmn +dbpl: 5 +cczh: sllz + lgvd +zczc: 2 +ptdq: humn - dvpt +dvpt: 3 +lfqf: 4 +humn: 5 +ljgn: 2 +sjmn: drzm * dbpl +sllz: 4 +pppw: cczh / lfqf +lgvd: ljgn * ptdq +drzm: hmdt - zczc +hmdt: 32 \ No newline at end of file diff --git a/src/test/resources/sample/day-23.txt b/src/test/resources/sample/day-23.txt new file mode 100644 index 0000000..7ac3ba9 --- /dev/null +++ b/src/test/resources/sample/day-23.txt @@ -0,0 +1,7 @@ +....#.. +..###.# +#...#.# +.#...## +#.###.. +##.#.## +.#..#.. \ No newline at end of file