Java tutorial
/* * Copyright 2014 Jeffrey Bosboom. * This file is part of lynebot. * * lynebot is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * lynebot is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with lynebot. If not, see <http://www.gnu.org/licenses/>. */ package com.jeffreybosboom.lyne; import static com.google.common.base.Preconditions.checkArgument; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Sets; import java.util.Arrays; import java.util.Set; import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; /** * * @author Jeffrey Bosboom <jbosboom@csail.mit.edu> * @since 8/8/2014 */ public final class Puzzle { private static final int[][] NEIGHBORHOOD = { { -1, -1 }, { -1, 0 }, { -1, 1 }, { 0, -1 }, { 0, 1 }, { 1, -1 }, { 1, 0 }, { 1, 1 }, }; private final Node[][] nodes; private final ImmutableMap<Node, ImmutableSet<Node>> neighbors; //TODO: consider replacing with edgeSets.keySet() -- not sure of performance //impact, as edges() is hammered by CrossingEdgesRule private final ImmutableSet<Pair<Node, Node>> edges; private final ImmutableMap<Pair<Node, Node>, ImmutableSet<Node.Kind>> edgeSets; public Puzzle(Node[][] nodes) { assert Arrays.stream(nodes).mapToInt(x -> x.length).distinct().count() == 1 : "array not rectangular"; this.nodes = nodes; ImmutableMap.Builder<Node, ImmutableSet<Node>> neighborsBuilder = ImmutableMap.builder(); nodes().forEachOrdered(n -> { neighborsBuilder.put(n, ImmutableSet.copyOf(Arrays.stream(NEIGHBORHOOD) .map(p -> new int[] { n.row() + p[0], n.col() + p[1] }) .filter(p -> 0 <= p[0] && p[0] < nodes.length).filter(p -> 0 <= p[1] && p[1] < nodes[0].length) .map(p -> nodes[p[0]][p[1]]).filter(x -> x != null).iterator())); }); this.neighbors = neighborsBuilder.build(); this.edges = ImmutableSet.copyOf( nodes().filter(n -> n != null).flatMap(a -> neighbors(a).map(b -> Pair.sorted(a, b))).iterator()); //Only include colors if nodes of that color are present. ImmutableSet<Node.Kind> maximalEdgeSet = ImmutableSet.<Node.Kind>builder() .addAll(Arrays.stream(nodes).flatMap(Arrays::stream).filter(x -> x != null).map(Node::kind) .filter(Node.Kind::isColored).iterator()) .add(Node.Kind.NONE).build(); ImmutableMap.Builder<Pair<Node, Node>, ImmutableSet<Node.Kind>> edgeSetsBuilder = ImmutableMap.builder(); edges().forEachOrdered(e -> edgeSetsBuilder.put(e, maximalEdgeSet)); this.edgeSets = edgeSetsBuilder.build(); } private Puzzle(Puzzle puzzle, ImmutableMap<Pair<Node, Node>, ImmutableSet<Node.Kind>> edgeSets) { this.nodes = puzzle.nodes; this.edges = puzzle.edges; this.neighbors = puzzle.neighbors; this.edgeSets = edgeSets; } private Puzzle withEdgeSet(Pair<Node, Node> edge, ImmutableSet<Node.Kind> newEdgeSet) { assert neighbors(edge.first).anyMatch(Predicate.isEqual(edge.second)); assert edge.first.compareTo(edge.second) < 0; assert edgeSets.containsKey(edge) : "not an edge: " + edge; assert !newEdgeSet.contains(Node.Kind.OCTAGON); assert !newEdgeSet.isEmpty(); //TODO: no-change and ContradictionException checks could be moved here //from set and remove. ImmutableMap.Builder<Pair<Node, Node>, ImmutableSet<Node.Kind>> edgeSetBuilder = ImmutableMap.builder(); edgeSets.entrySet().stream().filter(e -> !e.getKey().equals(edge)).forEachOrdered(edgeSetBuilder::put); edgeSetBuilder.put(edge, newEdgeSet); return new Puzzle(this, edgeSetBuilder.build()); } /** * Creates a Puzzle by parsing a String. This method is the inverse of * toString(). */ public static Puzzle fromString(String str) { String[] rows = str.split("\n"); checkArgument(Arrays.stream(rows).mapToInt(s -> s.length()).distinct().count() == 1, "not rectangular"); Node[][] nodes = new Node[rows.length][rows[0].length()]; for (int row = 0; row < rows.length; ++row) for (int col = 0; col < rows[0].length(); ++col) { char c = rows[row].charAt(col); if (Character.isDigit(c)) nodes[row][col] = Node.octagon(row, col, Character.digit(c, 10)); else { Node.Kind kind = Arrays.stream(Node.Kind.values()) .filter(k -> k.toString().startsWith("" + Character.toUpperCase(c))).findFirst().get(); nodes[row][col] = Character.isUpperCase(c) ? Node.terminal(row, col, kind) : Node.nonterminal(row, col, kind); } } return new Puzzle(nodes); } public Node at(int row, int col) { return nodes[row][col]; } public Stream<Node> nodes() { return Arrays.stream(nodes).flatMap(Arrays::stream).filter(x -> x != null); } /** * Returns the pair of terminals for each color present in the puzzle. * These are usually not edges! * @return pairs of terminals */ public Stream<Pair<Node, Node>> terminals() { return nodes().filter(Node::isTerminal).collect(Collectors.groupingBy(Node::kind)).values().stream() .map(l -> { assert l.size() == 2 : l; return Pair.sorted(l.get(0), l.get(1)); }); } public Stream<Node> neighbors(Node n) { return neighbors.get(n).stream(); } /** * Returns each edge (pair of adjacent nodes) exactly once. The returned * pairs are in canonical order. * @return a stream of edges in this puzzle */ public Stream<Pair<Node, Node>> edges() { return edges.stream(); } public ImmutableSet<Node.Kind> possibilities(Node a, Node b) { Pair<Node, Node> p = Pair.sorted(a, b); return edgeSets.get(p); } /** * Returns a Puzzle with the given possibility removed from the edge between * the given nodes. If the possibility is already not possible, this Puzzle * is returned. If this removes the last possibility for this edge, * a ContradictionException is thrown. */ public Puzzle remove(Node a, Node b, Node.Kind possibility) { Pair<Node, Node> p = Pair.sorted(a, b); ImmutableSet<Node.Kind> possibilities = possibilities(a, b); if (!possibilities.contains(possibility)) return this; if (possibilities.size() == 1) throw new ContradictionException(); ImmutableSet<Node.Kind> newSet = ImmutableSet .copyOf(possibilities.stream().filter(x -> x != possibility).iterator()); return withEdgeSet(p, newSet); } public Puzzle restrict(Node a, Node b, Set<Node.Kind> possibilities) { return set(a, b, Sets.intersection(possibilities, possibilities(a, b))); } /** * Returns a Puzzle with the given possibility being the only one in the * edge between the given nodes. If this possibility is already the only * possible, this Puzzle is returned. If this possibility is not possible, * a ContradictionException is thrown. */ public Puzzle set(Node a, Node b, Node.Kind possibility) { return set(a, b, ImmutableSet.of(possibility)); } private Puzzle set(Node a, Node b, Set<Node.Kind> possibilities) { //private because rules actually want to call restrict instead Pair<Node, Node> p = Pair.sorted(a, b); ImmutableSet<Node.Kind> currentPossibilities = possibilities(a, b); if (possibilities.isEmpty() || !currentPossibilities.containsAll(possibilities)) throw new ContradictionException(); if (currentPossibilities.equals(possibilities)) return this; return withEdgeSet(p, ImmutableSet.copyOf(possibilities)); } @Override public String toString() { //TODO: concise way to print edge sets? StringBuilder sb = new StringBuilder(); for (Node[] r : nodes) { for (Node n : r) sb.append(n == null ? " " : n.toString()); sb.append("\n"); } return sb.toString().trim(); } }