diff --git a/src/search/ChessKnightNode.java b/src/search/ChessKnightNode.java new file mode 100755 index 0000000..55df7a7 --- /dev/null +++ b/src/search/ChessKnightNode.java @@ -0,0 +1,93 @@ +package search; + +import java.util.ArrayList; +import java.util.List; + +/** + * For a Kata on codewars.com. + * How many moves need a knight (Springer) to get from a given start to a given end on the chessboard, + * e.g. a3 -> b5, the knight needs 1 move; + * a1 -> f1, the knight needs 3 moves + */ +public class ChessKnightNode extends Node { + public ChessKnightNode(final KnightPair state) { + super(state); + } + + private ChessKnightNode(final KnightPair value, final Node parent, final int heuristicCosts) { + super(value, parent, heuristicCosts); + } + + @Override + public boolean isTargetReached(final Node target) { + return this.valueEquals(target); + } + + @Override + public List> generateSuccessors() { + final var successors = new ArrayList>(); + + for (final var dir : ChessDirections.values()) { + try { + final char newCol = (char) (super.value.col() + dir.getColChange()); + final char newRow = (char) (super.value.row() + dir.getRowChange()); + + final var newState = new KnightPair(newCol, newRow); + + final var successor = new ChessKnightNode(newState, this, super.heuristicCosts + 1); + + if (!successor.valueEquals(this) && !successor.valueEquals(super.getParent())) { + successors.add(successor); + } + } catch (final ChessKnightException ignored) { + } + } + + return successors; + } + + private boolean valueEquals(final Node node) { + return node != null && super.value.equals(node.value); + } + + @Override + public String toString() { + return "%c%c".formatted(super.value.col, super.value.row); + } + + protected record KnightPair(char col, char row) { + public KnightPair { + if (col < 'a' || col > 'h') throw new ChessKnightException("Row out of bounds at index %c".formatted(col)); + if (row < '1' || row > '8') throw new ChessKnightException("Column out of bounds at index %c".formatted(row)); + } + } + + protected enum ChessDirections { + TOP_LEFT(-1, 2), TOP_RIGHT(1, 2), + RIGHT_TOP(2, 1), RIGHT_BOTTOM(2, -1), + DOWN_RIGHT(1 ,-2), DOWN_LEFT(-1,-2), + LEFT_BOTTOM(-2,-1), LEFT_TOP(-2,1); + + public final int colChange; + public final int rowChange; + + ChessDirections( final int colChange, final int rowChange) { + this.colChange = colChange; + this.rowChange = rowChange; + } + + public int getColChange() { + return colChange; + } + + public int getRowChange() { + return rowChange; + } + } + + protected static class ChessKnightException extends IndexOutOfBoundsException { + public ChessKnightException(final String s) { + super(s); + } + } +} \ No newline at end of file diff --git a/test/search/ChessKnightNodeTest.java b/test/search/ChessKnightNodeTest.java new file mode 100755 index 0000000..99497be --- /dev/null +++ b/test/search/ChessKnightNodeTest.java @@ -0,0 +1,107 @@ +package search; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class ChessKnightNodeTest +{ + @Test + public void shouldReturnTrueWhenTargetReached() + { + final var state = new ChessKnightNode.KnightPair('a', '1'); + + final var node = new ChessKnightNode(state); + final var target = new ChessKnightNode(state); + + Assertions.assertTrue(node.isTargetReached(target)); + } + + @Test + public void shouldReturnFalseWhenTargetNotReached() + { + final var actualState = new ChessKnightNode.KnightPair('a', '1'); + + final var targetState = new ChessKnightNode.KnightPair('h', '8'); + + final var node = new ChessKnightNode(actualState); + final var target = new ChessKnightNode(targetState); + + Assertions.assertFalse(node.isTargetReached(target)); + } + + @Test + public void shouldReturnNonEmptyListOfSuccessors() + { + final var state = new ChessKnightNode.KnightPair('a', '1'); + + final var node = new ChessKnightNode(state); + final var successors = node.generateSuccessors(); + + Assertions.assertFalse(successors.isEmpty()); + } + + @Test + public void shouldReturnCorrectSuccessorsWithMaxPossibleSuccessors() + { + final var state = new ChessKnightNode.KnightPair('f', '3'); + + final var node = new ChessKnightNode(state); + final var successors = node.generateSuccessors(); + + Assertions.assertEquals(8, successors.size()); + + final var newState1 = new ChessKnightNode.KnightPair('e', '5'); + final var newState2 = new ChessKnightNode.KnightPair('g', '5'); + final var newState3 = new ChessKnightNode.KnightPair('h', '4'); + final var newState4 = new ChessKnightNode.KnightPair('h', '2'); + final var newState5 = new ChessKnightNode.KnightPair('g', '1'); + final var newState6 = new ChessKnightNode.KnightPair('e', '1'); + final var newState7 = new ChessKnightNode.KnightPair('d', '2'); + final var newState8 = new ChessKnightNode.KnightPair('d', '4'); + + + Assertions.assertEquals(newState1, successors.get(0).getValue()); + Assertions.assertEquals(newState2, successors.get(1).getValue()); + Assertions.assertEquals(newState3, successors.get(2).getValue()); + Assertions.assertEquals(newState4, successors.get(3).getValue()); + Assertions.assertEquals(newState5, successors.get(4).getValue()); + Assertions.assertEquals(newState6, successors.get(5).getValue()); + Assertions.assertEquals(newState7, successors.get(6).getValue()); + Assertions.assertEquals(newState8, successors.get(7).getValue()); + } + + @Test + public void shouldReturnCorrectSuccessorsOnEdge() + { + final var state = new ChessKnightNode.KnightPair('a', '1'); + + final var node = new ChessKnightNode(state); + final var successors = node.generateSuccessors(); + + Assertions.assertEquals(2, successors.size()); + + Assertions.assertThrows(ChessKnightNode.ChessKnightException.class, + () -> new ChessKnightNode.KnightPair((char) ('a' - 1), '3')); + + final var newState1 = new ChessKnightNode.KnightPair('b', '3'); + final var newState2 = new ChessKnightNode.KnightPair('c', '2'); + + Assertions.assertThrows(ChessKnightNode.ChessKnightException.class, + () -> new ChessKnightNode.KnightPair('c', '0')); + + Assertions.assertThrows(ChessKnightNode.ChessKnightException.class, + () -> new ChessKnightNode.KnightPair('c', (char) ('0' - 1))); + + Assertions.assertThrows(ChessKnightNode.ChessKnightException.class, + () -> new ChessKnightNode.KnightPair((char) ('a' - 1), (char) ('0' - 1))); + + Assertions.assertThrows(ChessKnightNode.ChessKnightException.class, + () -> new ChessKnightNode.KnightPair((char) ('a' - 2), '0')); + + Assertions.assertThrows(ChessKnightNode.ChessKnightException.class, + () -> new ChessKnightNode.KnightPair((char) ('a' - 2), '3')); + + Assertions.assertEquals(newState1, successors.get(0).getValue()); + Assertions.assertEquals(newState2, successors.get(1).getValue()); + } +} diff --git a/test/search/ChessKnightTest.java b/test/search/ChessKnightTest.java new file mode 100755 index 0000000..3601590 --- /dev/null +++ b/test/search/ChessKnightTest.java @@ -0,0 +1,51 @@ +package search; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import search.uninformed.breadthfirstsearch.BreadthFirstSearch; + +import java.util.List; + +import static search.SearchTestUtils.countNodes; +import static search.SearchTestUtils.printSolution; + +public class ChessKnightTest +{ + @Test + public void shouldReturnOne() + { + final var state = new ChessKnightNode.KnightPair('a', '3'); + + final var root = new ChessKnightNode(state); + + final var targetState = new ChessKnightNode.KnightPair('b', '5'); + final var expected = new ChessKnightNode(targetState); + + final var actual = new BreadthFirstSearch().breadthFirstSearch(List.of(root), expected); + + printSolution(actual); + var nodeCount = countNodes(actual); + var edgeCount = nodeCount - 1; + + Assertions.assertEquals(1, edgeCount); + } + + @Test + public void shouldReturnThree() + { + final var state = new ChessKnightNode.KnightPair('a', '1'); + + final var root = new ChessKnightNode(state); + + final var targetState = new ChessKnightNode.KnightPair('f', '1'); + final var expected = new ChessKnightNode(targetState); + + final var actual = new BreadthFirstSearch().breadthFirstSearch(List.of(root), expected); + + printSolution(actual); + var nodeCount = countNodes(actual); + var edgeCount = nodeCount - 1; + + Assertions.assertEquals(3, edgeCount); + } +} diff --git a/test/search/GameHelperTest.java b/test/search/GameHelperTest.java index 5e889a1..d4c5cf9 100755 --- a/test/search/GameHelperTest.java +++ b/test/search/GameHelperTest.java @@ -11,9 +11,9 @@ public class GameHelperTest void shouldReturnCorrectTargetWartales() { final int[][] state = { - {5, 8, 2}, - {1, 7, 3}, - {4, 0, 6} + {1, 8, 6}, + {3, 2, 7}, + {5, 4, 0} }; final var root = new EightPuzzleNode(state); @@ -44,7 +44,7 @@ public class GameHelperTest {true, true, true}, {true, true, true}, {true, true, true}, - }; + }; final var expected = new LabyrinthineNode(targetState); diff --git a/test/search/SearchTestUtils.java b/test/search/SearchTestUtils.java index a3c85a4..ca71fbd 100755 --- a/test/search/SearchTestUtils.java +++ b/test/search/SearchTestUtils.java @@ -17,4 +17,18 @@ public class SearchTestUtils System.out.println("START"); } + + public static int countNodes(final Node targetNode) + { + var node = targetNode; + + int nodeCount = 0; + while (node != null) + { + nodeCount++; + node = node.getParent(); + } + + return nodeCount; + } }