diff --git a/README.md b/README.md index 754e87d..9c9a919 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ To use the ```wfc4j``` library in your Java project, you can include the library eu.irzinfante wfc4j - 0.1.1 + 0.2.0 ``` @@ -24,6 +24,9 @@ Here are some examples that demonstrate the usage of the different API of the li - 1-dimensional: - [Euclidean (linear grid)](examples/1-dimensional-euclidean.md) - [Toroidal (cyclic grid)](examples/1-dimensional-toroidal.md) +- 2-dimensional: + - [Euclidean (planar grid)](examples/2-dimensional-euclidean.md) + - [Toroidal (toroidal grid)](examples/2-dimensional-toroidal.md) ## Contributing diff --git a/assets/1-dimensional-euclidean/result1.png b/assets/1-dimensional-euclidean/result1.png new file mode 100644 index 0000000..dde78e7 Binary files /dev/null and b/assets/1-dimensional-euclidean/result1.png differ diff --git a/assets/1-dimensional-euclidean/result2.png b/assets/1-dimensional-euclidean/result2.png new file mode 100644 index 0000000..2cb16ae Binary files /dev/null and b/assets/1-dimensional-euclidean/result2.png differ diff --git a/assets/1-dimensional-euclidean/result3.png b/assets/1-dimensional-euclidean/result3.png new file mode 100644 index 0000000..4fe9810 Binary files /dev/null and b/assets/1-dimensional-euclidean/result3.png differ diff --git a/assets/1-dimensional-toroidal/result1.png b/assets/1-dimensional-toroidal/result1.png new file mode 100644 index 0000000..becfe6e Binary files /dev/null and b/assets/1-dimensional-toroidal/result1.png differ diff --git a/assets/1-dimensional-toroidal/result2.png b/assets/1-dimensional-toroidal/result2.png new file mode 100644 index 0000000..01f33f7 Binary files /dev/null and b/assets/1-dimensional-toroidal/result2.png differ diff --git a/assets/2-dimensional-euclidean/NE.png b/assets/2-dimensional-euclidean/NE.png new file mode 100644 index 0000000..959fb98 Binary files /dev/null and b/assets/2-dimensional-euclidean/NE.png differ diff --git a/assets/2-dimensional-euclidean/NW.png b/assets/2-dimensional-euclidean/NW.png new file mode 100644 index 0000000..19dfc65 Binary files /dev/null and b/assets/2-dimensional-euclidean/NW.png differ diff --git a/assets/2-dimensional-euclidean/SE.png b/assets/2-dimensional-euclidean/SE.png new file mode 100644 index 0000000..9a657fb Binary files /dev/null and b/assets/2-dimensional-euclidean/SE.png differ diff --git a/assets/2-dimensional-euclidean/SW.png b/assets/2-dimensional-euclidean/SW.png new file mode 100644 index 0000000..a7a36f5 Binary files /dev/null and b/assets/2-dimensional-euclidean/SW.png differ diff --git a/assets/2-dimensional-euclidean/result1.png b/assets/2-dimensional-euclidean/result1.png new file mode 100644 index 0000000..6fdd536 Binary files /dev/null and b/assets/2-dimensional-euclidean/result1.png differ diff --git a/assets/2-dimensional-euclidean/result2.png b/assets/2-dimensional-euclidean/result2.png new file mode 100644 index 0000000..b2ee95f Binary files /dev/null and b/assets/2-dimensional-euclidean/result2.png differ diff --git a/assets/2-dimensional-toroidal/BTBT.png b/assets/2-dimensional-toroidal/BTBT.png new file mode 100644 index 0000000..3067789 Binary files /dev/null and b/assets/2-dimensional-toroidal/BTBT.png differ diff --git a/assets/2-dimensional-toroidal/BTLB.png b/assets/2-dimensional-toroidal/BTLB.png new file mode 100644 index 0000000..994384c Binary files /dev/null and b/assets/2-dimensional-toroidal/BTLB.png differ diff --git a/assets/2-dimensional-toroidal/BTLR.png b/assets/2-dimensional-toroidal/BTLR.png new file mode 100644 index 0000000..780e37e Binary files /dev/null and b/assets/2-dimensional-toroidal/BTLR.png differ diff --git a/assets/2-dimensional-toroidal/BTLT.png b/assets/2-dimensional-toroidal/BTLT.png new file mode 100644 index 0000000..26b67d2 Binary files /dev/null and b/assets/2-dimensional-toroidal/BTLT.png differ diff --git a/assets/2-dimensional-toroidal/BTRB.png b/assets/2-dimensional-toroidal/BTRB.png new file mode 100644 index 0000000..4a6b5ca Binary files /dev/null and b/assets/2-dimensional-toroidal/BTRB.png differ diff --git a/assets/2-dimensional-toroidal/BTRT.png b/assets/2-dimensional-toroidal/BTRT.png new file mode 100644 index 0000000..6377e66 Binary files /dev/null and b/assets/2-dimensional-toroidal/BTRT.png differ diff --git a/assets/2-dimensional-toroidal/BT__.png b/assets/2-dimensional-toroidal/BT__.png new file mode 100644 index 0000000..f18315c Binary files /dev/null and b/assets/2-dimensional-toroidal/BT__.png differ diff --git a/assets/2-dimensional-toroidal/LBBT.png b/assets/2-dimensional-toroidal/LBBT.png new file mode 100644 index 0000000..5753493 Binary files /dev/null and b/assets/2-dimensional-toroidal/LBBT.png differ diff --git a/assets/2-dimensional-toroidal/LBLB.png b/assets/2-dimensional-toroidal/LBLB.png new file mode 100644 index 0000000..b4aef8e Binary files /dev/null and b/assets/2-dimensional-toroidal/LBLB.png differ diff --git a/assets/2-dimensional-toroidal/LBLR.png b/assets/2-dimensional-toroidal/LBLR.png new file mode 100644 index 0000000..4b1bbf7 Binary files /dev/null and b/assets/2-dimensional-toroidal/LBLR.png differ diff --git a/assets/2-dimensional-toroidal/LBLT.png b/assets/2-dimensional-toroidal/LBLT.png new file mode 100644 index 0000000..a703d8a Binary files /dev/null and b/assets/2-dimensional-toroidal/LBLT.png differ diff --git a/assets/2-dimensional-toroidal/LBRB.png b/assets/2-dimensional-toroidal/LBRB.png new file mode 100644 index 0000000..077b7ee Binary files /dev/null and b/assets/2-dimensional-toroidal/LBRB.png differ diff --git a/assets/2-dimensional-toroidal/LBRT.png b/assets/2-dimensional-toroidal/LBRT.png new file mode 100644 index 0000000..b7a9c99 Binary files /dev/null and b/assets/2-dimensional-toroidal/LBRT.png differ diff --git a/assets/2-dimensional-toroidal/LB__.png b/assets/2-dimensional-toroidal/LB__.png new file mode 100644 index 0000000..3220e98 Binary files /dev/null and b/assets/2-dimensional-toroidal/LB__.png differ diff --git a/assets/2-dimensional-toroidal/LRBT.png b/assets/2-dimensional-toroidal/LRBT.png new file mode 100644 index 0000000..3170d10 Binary files /dev/null and b/assets/2-dimensional-toroidal/LRBT.png differ diff --git a/assets/2-dimensional-toroidal/LRLB.png b/assets/2-dimensional-toroidal/LRLB.png new file mode 100644 index 0000000..b9cb5e9 Binary files /dev/null and b/assets/2-dimensional-toroidal/LRLB.png differ diff --git a/assets/2-dimensional-toroidal/LRLR.png b/assets/2-dimensional-toroidal/LRLR.png new file mode 100644 index 0000000..ffb4096 Binary files /dev/null and b/assets/2-dimensional-toroidal/LRLR.png differ diff --git a/assets/2-dimensional-toroidal/LRLT.png b/assets/2-dimensional-toroidal/LRLT.png new file mode 100644 index 0000000..737bb75 Binary files /dev/null and b/assets/2-dimensional-toroidal/LRLT.png differ diff --git a/assets/2-dimensional-toroidal/LRRB.png b/assets/2-dimensional-toroidal/LRRB.png new file mode 100644 index 0000000..190ca73 Binary files /dev/null and b/assets/2-dimensional-toroidal/LRRB.png differ diff --git a/assets/2-dimensional-toroidal/LRRT.png b/assets/2-dimensional-toroidal/LRRT.png new file mode 100644 index 0000000..b653042 Binary files /dev/null and b/assets/2-dimensional-toroidal/LRRT.png differ diff --git a/assets/2-dimensional-toroidal/LR__.png b/assets/2-dimensional-toroidal/LR__.png new file mode 100644 index 0000000..85231c5 Binary files /dev/null and b/assets/2-dimensional-toroidal/LR__.png differ diff --git a/assets/2-dimensional-toroidal/LTBT.png b/assets/2-dimensional-toroidal/LTBT.png new file mode 100644 index 0000000..f8e9e4f Binary files /dev/null and b/assets/2-dimensional-toroidal/LTBT.png differ diff --git a/assets/2-dimensional-toroidal/LTLB.png b/assets/2-dimensional-toroidal/LTLB.png new file mode 100644 index 0000000..b14f201 Binary files /dev/null and b/assets/2-dimensional-toroidal/LTLB.png differ diff --git a/assets/2-dimensional-toroidal/LTLR.png b/assets/2-dimensional-toroidal/LTLR.png new file mode 100644 index 0000000..a40c58d Binary files /dev/null and b/assets/2-dimensional-toroidal/LTLR.png differ diff --git a/assets/2-dimensional-toroidal/LTLT.png b/assets/2-dimensional-toroidal/LTLT.png new file mode 100644 index 0000000..ef50bc3 Binary files /dev/null and b/assets/2-dimensional-toroidal/LTLT.png differ diff --git a/assets/2-dimensional-toroidal/LTRB.png b/assets/2-dimensional-toroidal/LTRB.png new file mode 100644 index 0000000..a80412b Binary files /dev/null and b/assets/2-dimensional-toroidal/LTRB.png differ diff --git a/assets/2-dimensional-toroidal/LTRT.png b/assets/2-dimensional-toroidal/LTRT.png new file mode 100644 index 0000000..72e70de Binary files /dev/null and b/assets/2-dimensional-toroidal/LTRT.png differ diff --git a/assets/2-dimensional-toroidal/LT__.png b/assets/2-dimensional-toroidal/LT__.png new file mode 100644 index 0000000..c7f367a Binary files /dev/null and b/assets/2-dimensional-toroidal/LT__.png differ diff --git a/assets/2-dimensional-toroidal/RBBT.png b/assets/2-dimensional-toroidal/RBBT.png new file mode 100644 index 0000000..6a884b5 Binary files /dev/null and b/assets/2-dimensional-toroidal/RBBT.png differ diff --git a/assets/2-dimensional-toroidal/RBLB.png b/assets/2-dimensional-toroidal/RBLB.png new file mode 100644 index 0000000..c5a544a Binary files /dev/null and b/assets/2-dimensional-toroidal/RBLB.png differ diff --git a/assets/2-dimensional-toroidal/RBLR.png b/assets/2-dimensional-toroidal/RBLR.png new file mode 100644 index 0000000..e9d7ae6 Binary files /dev/null and b/assets/2-dimensional-toroidal/RBLR.png differ diff --git a/assets/2-dimensional-toroidal/RBLT.png b/assets/2-dimensional-toroidal/RBLT.png new file mode 100644 index 0000000..0e641a6 Binary files /dev/null and b/assets/2-dimensional-toroidal/RBLT.png differ diff --git a/assets/2-dimensional-toroidal/RBRB.png b/assets/2-dimensional-toroidal/RBRB.png new file mode 100644 index 0000000..80b7937 Binary files /dev/null and b/assets/2-dimensional-toroidal/RBRB.png differ diff --git a/assets/2-dimensional-toroidal/RBRT.png b/assets/2-dimensional-toroidal/RBRT.png new file mode 100644 index 0000000..f2c86f7 Binary files /dev/null and b/assets/2-dimensional-toroidal/RBRT.png differ diff --git a/assets/2-dimensional-toroidal/RB__.png b/assets/2-dimensional-toroidal/RB__.png new file mode 100644 index 0000000..051ddd1 Binary files /dev/null and b/assets/2-dimensional-toroidal/RB__.png differ diff --git a/assets/2-dimensional-toroidal/RTBT.png b/assets/2-dimensional-toroidal/RTBT.png new file mode 100644 index 0000000..011b95b Binary files /dev/null and b/assets/2-dimensional-toroidal/RTBT.png differ diff --git a/assets/2-dimensional-toroidal/RTLB.png b/assets/2-dimensional-toroidal/RTLB.png new file mode 100644 index 0000000..536afb6 Binary files /dev/null and b/assets/2-dimensional-toroidal/RTLB.png differ diff --git a/assets/2-dimensional-toroidal/RTLR.png b/assets/2-dimensional-toroidal/RTLR.png new file mode 100644 index 0000000..babcb17 Binary files /dev/null and b/assets/2-dimensional-toroidal/RTLR.png differ diff --git a/assets/2-dimensional-toroidal/RTLT.png b/assets/2-dimensional-toroidal/RTLT.png new file mode 100644 index 0000000..44a4dc6 Binary files /dev/null and b/assets/2-dimensional-toroidal/RTLT.png differ diff --git a/assets/2-dimensional-toroidal/RTRB.png b/assets/2-dimensional-toroidal/RTRB.png new file mode 100644 index 0000000..e4ad363 Binary files /dev/null and b/assets/2-dimensional-toroidal/RTRB.png differ diff --git a/assets/2-dimensional-toroidal/RTRT.png b/assets/2-dimensional-toroidal/RTRT.png new file mode 100644 index 0000000..b5553cc Binary files /dev/null and b/assets/2-dimensional-toroidal/RTRT.png differ diff --git a/assets/2-dimensional-toroidal/RT__.png b/assets/2-dimensional-toroidal/RT__.png new file mode 100644 index 0000000..6231b41 Binary files /dev/null and b/assets/2-dimensional-toroidal/RT__.png differ diff --git a/assets/2-dimensional-toroidal/__BT.png b/assets/2-dimensional-toroidal/__BT.png new file mode 100644 index 0000000..4ae3602 Binary files /dev/null and b/assets/2-dimensional-toroidal/__BT.png differ diff --git a/assets/2-dimensional-toroidal/__LB.png b/assets/2-dimensional-toroidal/__LB.png new file mode 100644 index 0000000..43ed373 Binary files /dev/null and b/assets/2-dimensional-toroidal/__LB.png differ diff --git a/assets/2-dimensional-toroidal/__LR.png b/assets/2-dimensional-toroidal/__LR.png new file mode 100644 index 0000000..85b51a2 Binary files /dev/null and b/assets/2-dimensional-toroidal/__LR.png differ diff --git a/assets/2-dimensional-toroidal/__LT.png b/assets/2-dimensional-toroidal/__LT.png new file mode 100644 index 0000000..9f72035 Binary files /dev/null and b/assets/2-dimensional-toroidal/__LT.png differ diff --git a/assets/2-dimensional-toroidal/__RB.png b/assets/2-dimensional-toroidal/__RB.png new file mode 100644 index 0000000..efbf8d2 Binary files /dev/null and b/assets/2-dimensional-toroidal/__RB.png differ diff --git a/assets/2-dimensional-toroidal/__RT.png b/assets/2-dimensional-toroidal/__RT.png new file mode 100644 index 0000000..1655df1 Binary files /dev/null and b/assets/2-dimensional-toroidal/__RT.png differ diff --git a/assets/2-dimensional-toroidal/result.png b/assets/2-dimensional-toroidal/result.png new file mode 100644 index 0000000..f755294 Binary files /dev/null and b/assets/2-dimensional-toroidal/result.png differ diff --git a/examples/1-dimensional-euclidean.md b/examples/1-dimensional-euclidean.md index a31d60b..ba41ba0 100644 --- a/examples/1-dimensional-euclidean.md +++ b/examples/1-dimensional-euclidean.md @@ -83,7 +83,7 @@ We will perform 3 executions of the `generate` function in order: System.out.println(WFCUtils.WFC1DToString(result)); ``` - + - In the second one we will impose some constraint on the left end: @@ -98,7 +98,7 @@ We will perform 3 executions of the `generate` function in order: System.out.println(WFCUtils.WFC1DToString(result)); ``` - + - In the third one we will impose constraints in both ends: @@ -111,6 +111,4 @@ We will perform 3 executions of the `generate` function in order: System.out.println(WFCUtils.WFC1DToString(result)); ``` - - -Following this examples, and designing other tiles and adjacencies, one can play with wfc4j library to generate very diverse braids (topology). For more information about braids you can follow this [link (Braid - Wolfram MathWorld)](https://mathworld.wolfram.com/Braid.html) and DYOR. + diff --git a/examples/1-dimensional-toroidal.md b/examples/1-dimensional-toroidal.md index 99a9ca4..6fe8734 100644 --- a/examples/1-dimensional-toroidal.md +++ b/examples/1-dimensional-toroidal.md @@ -75,7 +75,7 @@ We will perform 2 executions of the `generate` function in order: System.out.println(WFCUtils.WFC1DToString(result)); ``` - + - In the second one we will impose some constraint on the fifth cell: @@ -90,6 +90,4 @@ We will perform 2 executions of the `generate` function in order: System.out.println(WFCUtils.WFC1DToString(result)); ``` - - -Following this examples, and designing other tiles and adjacencies, one can play with wfc4j library to generate very diverse types of rings, bands, bracelets, etc. + diff --git a/examples/2-dimensional-euclidean.md b/examples/2-dimensional-euclidean.md new file mode 100644 index 0000000..7acaeea --- /dev/null +++ b/examples/2-dimensional-euclidean.md @@ -0,0 +1,98 @@ +# 2-dimensional euclidean grid example + +Let's see how to use the wfc4j library to generate a planar pattern. These are the tiles we have available: + +| NE | NW | SE | SW | +|:---:|:---:|:---:|:---:| +||||| + +```java +import java.util.HashSet; +import eu.irzinfante.wfc4j.model.Tile; + +Tile NE = new Tile<>("NE"), NW = new Tile<>("NW"), SE = new Tile<>("SE"), SW = new Tile<>("SW"); + +var tileSet = new HashSet>(); +tileSet.add(NE); tileSet.add(NW); tileSet.add(SE); tileSet.add(SW); +``` + +So, the logical adjacencies between tiles are the following: + + +| Tile | Left adjacent tiles | Right adjacent tiles | Bottom adjacent tiles | Top adjacent tiles | +|:----:|:---------------------:|:----------------------:|:----------------------:|:----------------------:| +|| $~~~~$ | $~~~~$ | $~~~~$ | $~~~~$ | +|| $~~~~$ | $~~~~$ | $~~~~$ | $~~~~$ | +|| $~~~~$ | $~~~~$ | $~~~~$ | $~~~~$ | +|| $~~~~$ | $~~~~$ | $~~~~$ | $~~~~$ | + +```java +import eu.irzinfante.wfc4j.model.TileMap2D; +import eu.irzinfante.wfc4j.enums.Side2D; + +var adjacentNWSW = new HashSet>(); +adjacentNWSW.add(NW); adjacentNWSW.add(SW); + +var adjacentSESW = new HashSet>(); +adjacentSESW.add(SE); adjacentSESW.add(SW); + +var adjacentNESE = new HashSet>(); +adjacentNESE.add(NE); adjacentNESE.add(SE); + +var adjacentNENW = new HashSet>(); +adjacentNENW.add(NE); adjacentNENW.add(NW); + +var tileMap = new TileMap2D<>(tileSet); + +tileMap.setAdjacents(NE, Side2D.Left, adjacentNWSW); tileMap.setAdjacents(NE, Side2D.Right, adjacentNWSW); +tileMap.setAdjacents(NE, Side2D.Bottom, adjacentSESW); tileMap.setAdjacents(NE, Side2D.Top, adjacentSESW); + +tileMap.setAdjacents(NW, Side2D.Left, adjacentNESE); tileMap.setAdjacents(NW, Side2D.Right, adjacentNESE); +tileMap.setAdjacents(NW, Side2D.Bottom, adjacentSESW); tileMap.setAdjacents(NW, Side2D.Top, adjacentSESW); + +tileMap.setAdjacents(SE, Side2D.Left, adjacentNWSW); tileMap.setAdjacents(SE, Side2D.Right, adjacentNWSW); +tileMap.setAdjacents(SE, Side2D.Bottom, adjacentNENW); tileMap.setAdjacents(SE, Side2D.Top, adjacentNENW); + +tileMap.setAdjacents(SW, Side2D.Left, adjacentNESE); tileMap.setAdjacents(SW, Side2D.Right, adjacentNESE); +tileMap.setAdjacents(SW, Side2D.Bottom, adjacentNENW); tileMap.setAdjacents(SW, Side2D.Top, adjacentNENW); +``` + +Now we can instantiate the API class with an 8 by 6 grid: + +```java +import eu.irzinfante.wfc4j.api.WaveFunctionCollapseEuclidean2D; + +int gridSizeX = 8, gridSizeY = 6; +var WFC = new WaveFunctionCollapseEuclidean2D(tileMap, gridSizeX, gridSizeY, 148576907989080L); +``` + +We will perform 2 executions of the `generate` function in order: + +- For the first one we won't impose any restriction: + + ```java + import eu.irzinfante.wfc4j.util.WFCUtils; + + WFC.clear(); + var result = WFC.generate(); + + System.out.println(WFCUtils.WFC2DToString(result)); + ``` + + + +- For the second one we will impose some constraint on the center of the grid: + + ```java + import java.util.Arrays; + import eu.irzinfante.wfc4j.model.Cell2D; + + WFC.clear(); + WFC.setCellConstraint(new Cell2D<>(new HashSet<>(Arrays.asList(SE)), 4, 3)); + WFC.setCellConstraint(new Cell2D<>(new HashSet<>(Arrays.asList(NW)), 5, 4)); + result = WFC.generate(); + + System.out.println(WFCUtils.WFC2DToString(result)); + ``` + + diff --git a/examples/2-dimensional-toroidal.md b/examples/2-dimensional-toroidal.md new file mode 100644 index 0000000..e796318 --- /dev/null +++ b/examples/2-dimensional-toroidal.md @@ -0,0 +1,124 @@ +# 2-dimensional toroidal grid example + +In this example we will see how to use the wfc4j library to generate a 2D toroidal pattern with programmatically constructed tiles. Each tile will be the combination of two components. + +- First component: + +| LR__ | LB__ | LT__ | RB__ | RT__ | BT__ | +|:----:|:----:|:----:|:----:|:----:|:----:| +||||||| + +- Second component: + +| __LR | __LB | __LT | __RB | __RT | __BT | +|:----:|:----:|:----:|:----:|:----:|:----:| +||||||| + +And therefore the adjacency of the tiles will be based on how the two components of each tile fit together with the components of the adjacent tiles. In our example this is computed like this: + +```java +import eu.irzinfante.wfc4j.enums.Side2D; +import eu.irzinfante.wfc4j.exceptions.DimensionException; +import eu.irzinfante.wfc4j.exceptions.TileException; +import eu.irzinfante.wfc4j.model.Tile; +import eu.irzinfante.wfc4j.model.TileMap2D; + +final String LR = "LR", LB = "LB", LT = "LT", RB = "RB", RT = "RT", BT = "BT"; +final var components = new String[] {LR, LB, LT, RB, RT, BT}; + +var tileSet = new HashSet>(); +for(var first : components) { + for(var second : components) { + tileSet.add(new Tile<>(first + second)); + } +} + +var tileMap = new TileMap2D<>(tileSet); + +for(var tile : tileSet) { + var tileFirstLeft = tile.getValue().substring(0, 2).contains("L"); + var tileSecondLeft = tile.getValue().substring(2, 4).contains("L"); + var leftAdjacents = new HashSet>(); + + var tileFirstRight = tile.getValue().substring(0, 2).contains("R"); + var tileSecondRight = tile.getValue().substring(2, 4).contains("R"); + var rightAdjacents = new HashSet>(); + + var tileFirstBottom = tile.getValue().substring(0, 2).contains("B"); + var tileSecondBottom = tile.getValue().substring(2, 4).contains("B"); + var bottomAdjacents = new HashSet>(); + + var tileFirstTop = tile.getValue().substring(0, 2).contains("T"); + var tileSecondTop = tile.getValue().substring(2, 4).contains("T"); + var topAdjacents = new HashSet>(); + + for(var adjacent : tileSet) { + var adjacentFirstLeft = adjacent.getValue().substring(0, 2).contains("L"); + var adjacentSecondLeft = adjacent.getValue().substring(2, 4).contains("L"); + if( (tileFirstRight && adjacentFirstLeft && tileSecondRight && adjacentSecondLeft) || + (tileFirstRight && adjacentFirstLeft && !tileSecondRight && !adjacentSecondLeft) || + (!tileFirstRight && !adjacentFirstLeft && tileSecondRight && adjacentSecondLeft) || + (!tileFirstRight && !adjacentFirstLeft && !tileSecondRight && !adjacentSecondLeft) + ) { + rightAdjacents.add(adjacent); + } + + var adjacentFirstRight = adjacent.getValue().substring(0, 2).contains("R"); + var adjacentSecondRight = adjacent.getValue().substring(2, 4).contains("R"); + if( (tileFirstLeft && adjacentFirstRight && tileSecondLeft && adjacentSecondRight) || + (tileFirstLeft && adjacentFirstRight && !tileSecondLeft && !adjacentSecondRight) || + (!tileFirstLeft && !adjacentFirstRight && tileSecondLeft && adjacentSecondRight) || + (!tileFirstLeft && !adjacentFirstRight && !tileSecondLeft && !adjacentSecondRight) + ) { + leftAdjacents.add(adjacent); + } + + var adjacentFirstTop = adjacent.getValue().substring(0, 2).contains("T"); + var adjacentSecondTop = adjacent.getValue().substring(2, 4).contains("T"); + if( (tileFirstBottom && adjacentFirstTop && tileSecondBottom && adjacentSecondTop) || + (tileFirstBottom && adjacentFirstTop && !tileSecondBottom && !adjacentSecondTop) || + (!tileFirstBottom && !adjacentFirstTop && tileSecondBottom && adjacentSecondTop) || + (!tileFirstBottom && !adjacentFirstTop && !tileSecondBottom && !adjacentSecondTop) + ) { + bottomAdjacents.add(adjacent); + } + + var adjacentFirstBottom = adjacent.getValue().substring(0, 2).contains("B"); + var adjacentSecondBottom = adjacent.getValue().substring(2, 4).contains("B"); + if( (tileFirstTop && adjacentFirstBottom && tileSecondTop && adjacentSecondBottom) || + (tileFirstTop && adjacentFirstBottom && !tileSecondTop && !adjacentSecondBottom) || + (!tileFirstTop && !adjacentFirstBottom && tileSecondTop && adjacentSecondBottom) || + (!tileFirstTop && !adjacentFirstBottom && !tileSecondTop && !adjacentSecondBottom) + ) { + topAdjacents.add(adjacent); + } + } + + tileMap.setAdjacents(tile, Side2D.Left, leftAdjacents); + tileMap.setAdjacents(tile, Side2D.Right, rightAdjacents); + tileMap.setAdjacents(tile, Side2D.Bottom, bottomAdjacents); + tileMap.setAdjacents(tile, Side2D.Top, topAdjacents); +} +``` + +> Please, take your time on understanding the code above. + +Now we can instantiate the API class with an 6 by 6 grid: + +```java +import eu.irzinfante.wfc4j.api.WaveFunctionCollapseToroidal2D; + +int gridSizeX = 6, gridSizeY = 6; +var WFC = new WaveFunctionCollapseToroidal2D(tileMap, gridSizeX, gridSizeY, 103478937644546L); +``` + +Finally we will perform the excution of the `generate` function: + +```java +import eu.irzinfante.wfc4j.util.WFCUtils; + +var result = WFC.generate(); +System.out.println(WFCUtils.WFC2DToString(result)); +``` + + diff --git a/pom.xml b/pom.xml index 69e3064..b66ad78 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 eu.irzinfante wfc4j - 0.1.1 + 0.2.0 jar diff --git a/src/main/java/eu/irzinfante/wfc4j/api/WaveFunctionCollapseEuclidean2D.java b/src/main/java/eu/irzinfante/wfc4j/api/WaveFunctionCollapseEuclidean2D.java new file mode 100644 index 0000000..39adc63 --- /dev/null +++ b/src/main/java/eu/irzinfante/wfc4j/api/WaveFunctionCollapseEuclidean2D.java @@ -0,0 +1,120 @@ +/** + * Library to use the Wave Function Collapse strategy for procedural generation + * Copyright (C) 2023 Iker Ruiz de Infante Gonzalez + * + * This program 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. + * + * This program 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 this program. If not, see . + */ + +package eu.irzinfante.wfc4j.api; + +import eu.irzinfante.wfc4j.core.AbstractWaveFunctionCollapse2D; +import eu.irzinfante.wfc4j.exceptions.DimensionException; +import eu.irzinfante.wfc4j.exceptions.TileException; +import eu.irzinfante.wfc4j.model.Cell2D; +import eu.irzinfante.wfc4j.model.TileMap2D; + +/** + * @author irzinfante iker@irzinfante.eu + * @since 0.2.0 + */ +public class WaveFunctionCollapseEuclidean2D extends AbstractWaveFunctionCollapse2D { + + /** + * Creates a 2-dimensional euclidean grid on which to apply the WFC algorithm with the specified tilemap + * + * @param tileMap The to use for the WFC algorithm + * @param gridSizeX The size of the grid in the X axis (2-dimensional) + * @param gridSizeY The size of the grid in the Y axis (2-dimensional) + * @throws TileException If tileMap is null + * @throws DimensionException If gridSizeX or gridSizeY is less than one + * + * @since 0.2.0 + */ + public WaveFunctionCollapseEuclidean2D(TileMap2D tileMap, int gridSizeX, int gridSizeY) throws TileException, DimensionException { + super(tileMap, gridSizeX, gridSizeY); + } + + /** + * Creates a 2-dimensional euclidean grid on which to apply the WFC algorithm with the specified tilemap, + * with the possibility to control the random results providing a seed + * + * @param tileMap The to use for the WFC algorithm + * @param gridSizeX The size of the grid in the X axis (2-dimensional) + * @param gridSizeY The size of the grid in the Y axis (2-dimensional) + * @param seed Seed for random functions + * @throws TileException If tileMap is null + * @throws DimensionException If gridSizeX or gridSizeY is less than one + * + * @since 0.2.0 + */ + public WaveFunctionCollapseEuclidean2D(TileMap2D tileMap, int gridSizeX, int gridSizeY, long seed) throws TileException, DimensionException { + super(tileMap, gridSizeX, gridSizeY, seed); + } + + @Override + protected boolean canUpdateLeftCell(int x, int y) { + return x - 1 >= 1 && this.getLeftCell(x, y).getTile() == null; + } + + @Override + protected Cell2D getLeftCell(int x, int y) { + if(x == 1) { + return null; + } else { + return this.getCell(x - 1, y); + } + } + + @Override + protected boolean canUpdateRightCell(int x, int y) { + return x + 1 <= this.grid[y - 1].length && this.getRightCell(x, y).getTile() == null; + } + + @Override + protected Cell2D getRightCell(int x, int y) { + if(x == this.grid[y - 1].length) { + return null; + } else { + return this.getCell(x + 1, y); + } + } + + @Override + protected boolean canUpdateBottomCell(int x, int y) { + return y + 1 <= this.grid.length && this.getBottomCell(x, y).getTile() == null; + } + + @Override + protected Cell2D getBottomCell(int x, int y) { + if(y == this.grid.length) { + return null; + } else { + return this.getCell(x, y + 1); + } + } + + @Override + protected boolean canUpdateTopCell(int x, int y) { + return y - 1 >= 1 && this.getTopCell(x, y).getTile() == null; + } + + @Override + protected Cell2D getTopCell(int x, int y) { + if(y == 1) { + return null; + } else { + return this.getCell(x, y - 1); + } + } +} \ No newline at end of file diff --git a/src/main/java/eu/irzinfante/wfc4j/api/WaveFunctionCollapseToroidal2D.java b/src/main/java/eu/irzinfante/wfc4j/api/WaveFunctionCollapseToroidal2D.java new file mode 100644 index 0000000..87846cb --- /dev/null +++ b/src/main/java/eu/irzinfante/wfc4j/api/WaveFunctionCollapseToroidal2D.java @@ -0,0 +1,120 @@ +/** + * Library to use the Wave Function Collapse strategy for procedural generation + * Copyright (C) 2023 Iker Ruiz de Infante Gonzalez + * + * This program 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. + * + * This program 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 this program. If not, see . + */ + +package eu.irzinfante.wfc4j.api; + +import eu.irzinfante.wfc4j.core.AbstractWaveFunctionCollapse2D; +import eu.irzinfante.wfc4j.exceptions.DimensionException; +import eu.irzinfante.wfc4j.exceptions.TileException; +import eu.irzinfante.wfc4j.model.Cell2D; +import eu.irzinfante.wfc4j.model.TileMap2D; + +/** + * @author irzinfante iker@irzinfante.eu + * @since 0.2.0 + */ +public class WaveFunctionCollapseToroidal2D extends AbstractWaveFunctionCollapse2D { + + /** + * Creates a 2-dimensional toroidal grid (borders are stitched) on which to apply the WFC algorithm with the specified tilemap + * + * @param tileMap The to use for the WFC algorithm + * @param gridSizeX The size of the grid in the X axis (2-dimensional) + * @param gridSizeY The size of the grid in the Y axis (2-dimensional) + * @throws TileException If tileMap is null + * @throws DimensionException If gridSizeX or gridSizeY is less than one + * + * @since 0.2.0 + */ + public WaveFunctionCollapseToroidal2D(TileMap2D tileMap, int gridSizeX, int gridSizeY) throws TileException, DimensionException { + super(tileMap, gridSizeX, gridSizeY); + } + + /** + * Creates a 2-dimensional toroidal grid (borders are stitched) on which to apply the WFC algorithm with the specified tilemap, + * with the possibility to control the random results providing a seed + * + * @param tileMap The to use for the WFC algorithm + * @param gridSizeX The size of the grid in the X axis (2-dimensional) + * @param gridSizeY The size of the grid in the Y axis (2-dimensional) + * @param seed Seed for random functions + * @throws TileException If tileMap is null + * @throws DimensionException If gridSizeX or gridSizeY is less than one + * + * @since 0.2.0 + */ + public WaveFunctionCollapseToroidal2D(TileMap2D tileMap, int gridSizeX, int gridSizeY, long seed) throws TileException, DimensionException { + super(tileMap, gridSizeX, gridSizeY, seed); + } + + @Override + protected boolean canUpdateLeftCell(int x, int y) { + return this.getLeftCell(x, y).getTile() == null; + } + + @Override + protected Cell2D getLeftCell(int x, int y) { + if(x == 1) { + return this.getCell(this.grid[y - 1].length, y); + } else { + return this.getCell(x - 1, y); + } + } + + @Override + protected boolean canUpdateRightCell(int x, int y) { + return this.getRightCell(x, y).getTile() == null; + } + + @Override + protected Cell2D getRightCell(int x, int y) { + if(x == this.grid[y - 1].length) { + return this.getCell(1, y); + } else { + return this.getCell(x + 1, y); + } + } + + @Override + protected boolean canUpdateBottomCell(int x, int y) { + return this.getBottomCell(x, y).getTile() == null; + } + + @Override + protected Cell2D getBottomCell(int x, int y) { + if(y == this.grid.length) { + return this.getCell(x, 1); + } else { + return this.getCell(x, y + 1); + } + } + + @Override + protected boolean canUpdateTopCell(int x, int y) { + return this.getTopCell(x, y).getTile() == null; + } + + @Override + protected Cell2D getTopCell(int x, int y) { + if(y == 1) { + return this.getCell(x, this.grid.length); + } else { + return this.getCell(x, y - 1); + } + } +} \ No newline at end of file diff --git a/src/main/java/eu/irzinfante/wfc4j/core/AbstractWaveFunctionCollapse2D.java b/src/main/java/eu/irzinfante/wfc4j/core/AbstractWaveFunctionCollapse2D.java new file mode 100644 index 0000000..3c92b7a --- /dev/null +++ b/src/main/java/eu/irzinfante/wfc4j/core/AbstractWaveFunctionCollapse2D.java @@ -0,0 +1,283 @@ +/** + * Library to use the Wave Function Collapse strategy for procedural generation + * Copyright (C) 2023 Iker Ruiz de Infante Gonzalez + * + * This program 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. + * + * This program 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 this program. If not, see . + */ + +package eu.irzinfante.wfc4j.core; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Random; +import java.util.Set; + +import eu.irzinfante.wfc4j.enums.Side2D; +import eu.irzinfante.wfc4j.exceptions.DimensionException; +import eu.irzinfante.wfc4j.exceptions.TileException; +import eu.irzinfante.wfc4j.model.Cell2D; +import eu.irzinfante.wfc4j.model.Tile; +import eu.irzinfante.wfc4j.model.TileMap2D; + +/** + * @author irzinfante iker@irzinfante.eu + * @since 0.2.0 + */ +abstract public class AbstractWaveFunctionCollapse2D { + + protected TileMap2D tileMap; + protected Cell2D[][] grid; + protected Set> collapsableCells; + + protected Random random; + protected long deepth; + + protected AbstractWaveFunctionCollapse2D(TileMap2D tileMap, int gridSizeX, int gridSizeY) throws TileException, DimensionException { + + if(tileMap == null) { + throw new TileException("TileMap cannot be null"); + } else if(gridSizeX < 1 || gridSizeY < 1) { + throw new DimensionException("Invalid grid size"); + } + + this.tileMap = tileMap; + this.grid = (Cell2D[][]) new Cell2D[gridSizeY][gridSizeX]; + this.collapsableCells = new HashSet<>(); + + this.random = new Random(); + this.deepth = 0L; + + this.clear(); + } + + protected AbstractWaveFunctionCollapse2D(TileMap2D tileMap, int gridSizeX, int gridSizeY, long seed) throws TileException, DimensionException { + this(tileMap, gridSizeX, gridSizeY); + this.random = new Random(seed); + } + + /** + * Clears the cells of the entire grid + * + * @since 0.2.0 + */ + public void clear() { + for(int y = 1; y <= this.grid.length; y++) { + for(int x = 1; x <= this.grid[y - 1].length; x++) { + this.grid[y - 1][x - 1] = new Cell2D<>(this.tileMap.getTileSet(), x, y); + } + } + } + + /** + * Sets a cell on the grid if: the cell is not null, the cell has no value assigned + * and the entropy of the cell is not empty + * + * @param cell The cell to be set in the grid + * @throws DimensionException If the cell coordinates are out of the range of the grid + * + * @since 0.2.0 + */ + public void setCellConstraint(Cell2D cell) throws DimensionException { + + if(cell != null && cell.getTile() == null && !cell.getEntropy().isEmpty()) { + + var x = cell.getX(); + var y = cell.getY(); + if(y < 1 || y > this.grid.length) { + throw new DimensionException("Invalid cell y coordinate"); + } else if(x < 1 || x > this.grid[y - 1].length) { + throw new DimensionException("Invalid cell x coordinate"); + } + + this.grid[y - 1][x - 1] = cell; + this.collapsableCells.add(cell); + } + } + + /** + * Runs the WFC algorithm to populate the tile values for the cells of the grid + * + * @return A copy of the grid populated with tiles + * @throws TileException If exception occurs at the time of getting the adjacent tiles for some cell's value + * + * @since 0.2.0 + */ + public Cell2D[][] generate() throws TileException { + return this.generate(Long.MAX_VALUE); + } + + /** + * Runs the WFC algorithm to populate the tile values for the cells of the grid until the specified recursion level + * + * @param maxDeepth Maximum level of recursion + * @return A copy of the grid populated with tiles + * @throws TileException If exception occurs at the time of getting the adjacent tiles for some cell's value + * + * @since 0.2.0 + */ + public Cell2D[][] generate(long maxDeepth) throws TileException { + + Cell2D cell; + if(this.collapsableCells.isEmpty()) { + var y = this.random.nextInt(this.grid.length) + 1; + var x = this.random.nextInt(this.grid[y - 1].length) + 1; + cell = this.getCell(x, y); + } else { + cell = this.collapsableCellsToSortedList().get(0); + this.collapsableCells.remove(cell); + } + + if(this.collapseCell(cell, maxDeepth)) { + return Arrays.copyOf(this.grid, this.grid.length); + } else { + return null; + } + } + + protected Cell2D getCell(int x, int y) { + return this.grid[y - 1][x - 1]; + } + + protected List> collapsableCellsToSortedList() { + var collapsableCellsList = new ArrayList<>(this.collapsableCells); + Collections.shuffle(collapsableCellsList, this.random); + Collections.sort(collapsableCellsList); + return collapsableCellsList; + } + + protected boolean collapseCell(Cell2D cell, long maxDeepth) throws TileException { + this.deepth++; + + if(this.deepth <= maxDeepth) { + var entropy = cell.getEntropy(); + if(!entropy.isEmpty()) { + var options = new ArrayList<>(entropy); + Collections.shuffle(options, this.random); + + cell.setEntropy(new HashSet>()); + for(var tile : options) { + cell.setTile(tile); + + if(this.updateAdjacentCellsEntropy(cell)) { + + if(this.collapsableCells.isEmpty()) { + this.deepth--; + return true; + } + + for(var collapsableCell : this.collapsableCellsToSortedList()) { + this.collapsableCells.remove(collapsableCell); + if(this.collapseCell(collapsableCell, maxDeepth)) { + this.deepth--; + return true; + } + this.collapsableCells.add(collapsableCell); + } + } + } + + cell.setEntropy(entropy); + cell.setTile(null); + } + + this.deepth--; + return false; + } else { + this.deepth--; + return true; + } + } + + protected boolean updateAdjacentCellsEntropy(Cell2D currentCell) throws TileException { + var x = currentCell.getX(); + var y = currentCell.getY(); + + var valid = true; + + var newEntropyLeft = new HashSet>(); + if(valid && this.canUpdateLeftCell(x, y)) { + newEntropyLeft.addAll(this.getLeftCell(x, y).getReducedEntropy(this.tileMap.getAdjacents(currentCell.getTile(), Side2D.Left))); + if(newEntropyLeft.isEmpty()) { + valid = false; + } + } + + var newEntropyRight = new HashSet>(); + if(valid && this.canUpdateRightCell(x, y)) { + newEntropyRight.addAll(this.getRightCell(x, y).getReducedEntropy(this.tileMap.getAdjacents(currentCell.getTile(), Side2D.Right))); + if(newEntropyRight.isEmpty()) { + valid = false; + } + } + + var newEntropyBottom = new HashSet>(); + if(valid && this.canUpdateBottomCell(x, y)) { + newEntropyBottom.addAll(this.getBottomCell(x, y).getReducedEntropy(this.tileMap.getAdjacents(currentCell.getTile(), Side2D.Bottom))); + if(newEntropyBottom.isEmpty()) { + valid = false; + } + } + + var newEntropyTop = new HashSet>(); + if(valid && this.canUpdateTopCell(x, y)) { + newEntropyTop.addAll(this.getTopCell(x, y).getReducedEntropy(this.tileMap.getAdjacents(currentCell.getTile(), Side2D.Top))); + if(newEntropyTop.isEmpty()) { + valid = false; + } + } + + if(valid) { + if(this.canUpdateLeftCell(x, y)) { + this.getLeftCell(x, y).setEntropy(newEntropyLeft); + this.collapsableCells.add(this.getLeftCell(x, y)); + } + + if(this.canUpdateRightCell(x, y)) { + this.getRightCell(x, y).setEntropy(newEntropyRight); + this.collapsableCells.add(this.getRightCell(x, y)); + } + + if(this.canUpdateBottomCell(x, y)) { + this.getBottomCell(x, y).setEntropy(newEntropyBottom); + this.collapsableCells.add(this.getBottomCell(x, y)); + } + + if(this.canUpdateTopCell(x, y)) { + this.getTopCell(x, y).setEntropy(newEntropyTop); + this.collapsableCells.add(this.getTopCell(x, y)); + } + } + + return valid; + } + + abstract protected boolean canUpdateLeftCell(int x, int y); + + abstract protected Cell2D getLeftCell(int x, int y); + + abstract protected boolean canUpdateRightCell(int x, int y); + + abstract protected Cell2D getRightCell(int x, int y); + + abstract protected boolean canUpdateBottomCell(int x, int y); + + abstract protected Cell2D getBottomCell(int x, int y); + + abstract protected boolean canUpdateTopCell(int x, int y); + + abstract protected Cell2D getTopCell(int x, int y); +} \ No newline at end of file diff --git a/src/main/java/eu/irzinfante/wfc4j/enums/Side2D.java b/src/main/java/eu/irzinfante/wfc4j/enums/Side2D.java new file mode 100644 index 0000000..b01f43f --- /dev/null +++ b/src/main/java/eu/irzinfante/wfc4j/enums/Side2D.java @@ -0,0 +1,40 @@ +/** + * Library to use the Wave Function Collapse strategy for procedural generation + * Copyright (C) 2023 Iker Ruiz de Infante Gonzalez + * + * This program 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. + * + * This program 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 this program. If not, see . + */ + +package eu.irzinfante.wfc4j.enums; + +/** + * @author irzinfante iker@irzinfante.eu + * @since 0.2.0 + */ +public enum Side2D { + Left(0), + Right(1), + Bottom(2), + Top(3); + + private final int value; + + private Side2D(int value) { + this.value = value; + } + + public int getValue() { + return value; + } +} \ No newline at end of file diff --git a/src/main/java/eu/irzinfante/wfc4j/model/Cell2D.java b/src/main/java/eu/irzinfante/wfc4j/model/Cell2D.java new file mode 100644 index 0000000..22eb580 --- /dev/null +++ b/src/main/java/eu/irzinfante/wfc4j/model/Cell2D.java @@ -0,0 +1,94 @@ +/** + * Library to use the Wave Function Collapse strategy for procedural generation + * Copyright (C) 2023 Iker Ruiz de Infante Gonzalez + * + * This program 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. + * + * This program 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 this program. If not, see . + */ + +package eu.irzinfante.wfc4j.model; + +import java.util.Set; + +import java.util.HashSet; + +/** + * @author irzinfante iker@irzinfante.eu + * @since 0.2.0 + */ +public class Cell2D implements Comparable> { + + private Set> entropy; + private Tile tile; + private int x; + private int y; + + /** + * Creates a 2-dimensional cell with the given entropy and coordinates + * + * @param entropy The default entropy (all possible tile values) for the cell + * @param x The X component of the cell's coordinates in a 2-dimensional grid + * @param y The Y component of the cell's coordinates in a 2-dimensional grid + * + * @since 0.2.0 + */ + public Cell2D(Set> entropy, int x, int y) { + this.entropy = entropy; + this.tile = null; + this.x = x; + this.y = y; + } + + /** + * Returns the set intersection between the cell's current entropy and the intersectant set of tiles + * + * @param intersectant The set of tiles to be intersected with the cell's current entropy + * @return The set intersection between the current cell entropy and the intersectant set + * + * @since 0.2.0 + */ + public Set> getReducedEntropy(Set> intersectant) { + var reducedEntropy = new HashSet<>(this.entropy); + reducedEntropy.retainAll(intersectant); + return reducedEntropy; + } + + public Set> getEntropy() { + return this.entropy; + } + + public void setEntropy(Set> entropy) { + this.entropy = entropy; + } + + public Tile getTile() { + return this.tile; + } + + public void setTile(Tile tile) { + this.tile = tile; + } + + public int getX() { + return this.x; + } + + public int getY() { + return this.y; + } + + @Override + public int compareTo(Cell2D other) { + return this.entropy.size() - other.entropy.size(); + } +} \ No newline at end of file diff --git a/src/main/java/eu/irzinfante/wfc4j/model/TileMap2D.java b/src/main/java/eu/irzinfante/wfc4j/model/TileMap2D.java new file mode 100644 index 0000000..96b1fd3 --- /dev/null +++ b/src/main/java/eu/irzinfante/wfc4j/model/TileMap2D.java @@ -0,0 +1,125 @@ +/** + * Library to use the Wave Function Collapse strategy for procedural generation + * Copyright (C) 2023 Iker Ruiz de Infante Gonzalez + * + * This program 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. + * + * This program 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 this program. If not, see . + */ + +package eu.irzinfante.wfc4j.model; + +import java.util.Set; +import java.util.HashSet; +import java.util.Map; +import java.util.HashMap; + +import eu.irzinfante.wfc4j.enums.Side2D; +import eu.irzinfante.wfc4j.exceptions.TileException; + +/** + * @author irzinfante iker@irzinfante.eu + * @since 0.2.0 + */ +public class TileMap2D { + + private Set> tileSet; + private Map, Set>[]> adjacents; + + /** + * Creates a 2-dimensional tilemap with the given tileset + * + * @param tileSet The set of tiles (i.e. tileset) for the tilemap + * @throws TileException If tileSet is empty + * + * @since 0.2.0 + */ + public TileMap2D(Set> tileSet) throws TileException { + + if(tileSet.isEmpty()) { + throw new TileException("Set of tiles must be non-empty"); + } + + this.tileSet = tileSet; + this.adjacents = new HashMap<>(); + + tileSet.forEach(tile -> { + this.adjacents.put(tile, (Set>[]) new HashSet[4]); + }); + } + + /** + * Get the possible adjacent tiles to a specific side of a given tile + * + * @param tile The given tile for which to get the possible adjacent tiles + * @param side The side of the given tile from which to get the possible adjacent tiles + * @return Set of tiles that can be adjacent to the provided tile from the selected side + * @throws TileException If the given tile is not in present in the tilemap's tileset + * + * @since 0.2.0 + */ + public Set> getAdjacents(Tile tile, Side2D side) throws TileException { + + if(!this.tileSet.contains(tile)) { + throw new TileException("Tile must be present in TileMap"); + } + + return this.adjacents.get(tile)[side.getValue()]; + } + + /** + * Set the possible adjacent tiles to a specific side of a given tile + * + * @param tile The given tile for which to set the possible adjacent tiles + * @param side The side of the given tile for which to set the possible adjacent tiles + * @param adjacents The set of tiles to be set as the possible adjacent tiles for the given tile + * @throws TileException If the given tile or any of the potential adjacent tiles are not in present in the tilemap's tileset + * + * @since 0.2.0 + */ + public void setAdjacents(Tile tile, Side2D side, Set> adjacents) throws TileException { + + if(!this.tileSet.contains(tile)) { + throw new TileException("Tile must be present in TileMap"); + } else if(!this.tileSet.containsAll(adjacents)) { + throw new TileException("All adjacent tiles must be present in TileMap"); + } + + this.adjacents.get(tile)[side.getValue()] = adjacents; + } + + /** + * Add a single tile to the possible adjacent tiles to a specific side of a given tile + * + * @param tile The given tile for which to add the possible adjacent tile + * @param side The side of the given tile for which to add the possible adjacent tile + * @param adjacent A single tile to be added to the possible adjacent tiles for the given tile + * @return true if the adjacent tile to be added was not already contained in the set of adjacent sets + * @throws TileException If the given tile or the potential adjacent tile to be added are not in present in the tilemap's tileset + * + * @since 0.2.0 + */ + public boolean addAdjacent(Tile tile, Side2D side, Tile adjacent) throws TileException { + + if(!this.tileSet.contains(tile)) { + throw new TileException("Tile must be present in TileMap"); + } else if(!this.tileSet.contains(adjacent)) { + throw new TileException("Adjacent tile must be present in TileMap"); + } + + return this.adjacents.get(tile)[side.getValue()].add(adjacent); + } + + public Set> getTileSet() { + return this.tileSet; + } +} \ No newline at end of file diff --git a/src/main/java/eu/irzinfante/wfc4j/util/WFCUtils.java b/src/main/java/eu/irzinfante/wfc4j/util/WFCUtils.java index 298c45c..3312ed2 100644 --- a/src/main/java/eu/irzinfante/wfc4j/util/WFCUtils.java +++ b/src/main/java/eu/irzinfante/wfc4j/util/WFCUtils.java @@ -1,9 +1,11 @@ package eu.irzinfante.wfc4j.util; import eu.irzinfante.wfc4j.model.Cell1D; +import eu.irzinfante.wfc4j.model.Cell2D; /** * @author irzinfante iker@irzinfante.eu + * @version 0.2.0 * @since 0.1.0 */ public final class WFCUtils { @@ -40,4 +42,45 @@ public static String WFC1DToString(Cell1D[] grid) { return topBorder.toString() + "\n" + cellsRepr + "\n" + bottomBorder.toString(); } + + /** + * Gets the String representation of a 2-dimensional grid + * + * @param grid 2-dimensional grid to be represented as a String + * @return String representation of grid + * @since 0.2.0 + */ + public static String WFC2DToString(Cell2D[][] grid) { + var gridBuilder = new StringBuilder(); + + var bottomBorderBuilder = new StringBuilder("└"); + for(int y = 0; y < grid.length; y++) { + + var topBorderBuilder = new StringBuilder(y == 0 ? "┌" : "├"); + var rowBuilder = new StringBuilder("│"); + + for(int x = 0; x < grid[y].length; x++) { + var cellTile = grid[y][x].getTile(); + var value = cellTile == null ? new String(" ") : cellTile.getValue().toString(); + + topBorderBuilder.append("─".repeat(value.length())); + rowBuilder.append(value).append("│"); + + if (y == 0) { + topBorderBuilder.append(x == grid[y].length - 1 ? "┐" : "┬"); + } else { + topBorderBuilder.append(x == grid[y].length - 1 ? "┤" : "┼"); + if (y == grid.length - 1) { + bottomBorderBuilder.append("─".repeat(value.length())).append(x == grid[y].length - 1 ? "┘" : "┴"); + } + } + } + + gridBuilder.append(topBorderBuilder).append("\n"); + gridBuilder.append(rowBuilder).append("\n"); + } + gridBuilder.append(bottomBorderBuilder); + + return gridBuilder.toString(); + } } \ No newline at end of file diff --git a/src/test/java/eu/irzinfante/wfc4j/test/TestWFCEuclidean2D.java b/src/test/java/eu/irzinfante/wfc4j/test/TestWFCEuclidean2D.java new file mode 100644 index 0000000..828fe7f --- /dev/null +++ b/src/test/java/eu/irzinfante/wfc4j/test/TestWFCEuclidean2D.java @@ -0,0 +1,80 @@ +package eu.irzinfante.wfc4j.test; + +import static org.junit.Assert.assertEquals; + +import java.util.Arrays; +import java.util.HashSet; + +import org.junit.Test; + +import eu.irzinfante.wfc4j.api.WaveFunctionCollapseEuclidean2D; +import eu.irzinfante.wfc4j.enums.Side2D; +import eu.irzinfante.wfc4j.exceptions.DimensionException; +import eu.irzinfante.wfc4j.exceptions.TileException; +import eu.irzinfante.wfc4j.model.Cell2D; +import eu.irzinfante.wfc4j.model.Tile; +import eu.irzinfante.wfc4j.model.TileMap2D; +import eu.irzinfante.wfc4j.util.WFCUtils; + +public class TestWFCEuclidean2D { + + @Test + public void testStringTile() throws TileException, DimensionException { + + /* + * Tile | Left | Right | Bottom | Top | + * NE | NW, SW | NW, SW | SE, SW | SE, SW | + * NW | NE, SE | NE, SE | SE, SW | SE, SW | + * SE | NW, SW | NW, SW | NE, NW | NE, NW | + * SW | NE, SE | NE, SE | NE, NW | NE, NW | + */ + + Tile NE = new Tile<>("NE"), NW = new Tile<>("NW"), SE = new Tile<>("SE"), SW = new Tile<>("SW"); + + var tileSet = new HashSet>(); + tileSet.add(NE); tileSet.add(NW); tileSet.add(SE); tileSet.add(SW); + + var adjacentNWSW = new HashSet>(); + adjacentNWSW.add(NW); adjacentNWSW.add(SW); + + var adjacentSESW = new HashSet>(); + adjacentSESW.add(SE); adjacentSESW.add(SW); + + var adjacentNESE = new HashSet>(); + adjacentNESE.add(NE); adjacentNESE.add(SE); + + var adjacentNENW = new HashSet>(); + adjacentNENW.add(NE); adjacentNENW.add(NW); + + var tileMap = new TileMap2D<>(tileSet); + + tileMap.setAdjacents(NE, Side2D.Left, adjacentNWSW); tileMap.setAdjacents(NE, Side2D.Right, adjacentNWSW); + tileMap.setAdjacents(NE, Side2D.Bottom, adjacentSESW); tileMap.setAdjacents(NE, Side2D.Top, adjacentSESW); + + tileMap.setAdjacents(NW, Side2D.Left, adjacentNESE); tileMap.setAdjacents(NW, Side2D.Right, adjacentNESE); + tileMap.setAdjacents(NW, Side2D.Bottom, adjacentSESW); tileMap.setAdjacents(NW, Side2D.Top, adjacentSESW); + + tileMap.setAdjacents(SE, Side2D.Left, adjacentNWSW); tileMap.setAdjacents(SE, Side2D.Right, adjacentNWSW); + tileMap.setAdjacents(SE, Side2D.Bottom, adjacentNENW); tileMap.setAdjacents(SE, Side2D.Top, adjacentNENW); + + tileMap.setAdjacents(SW, Side2D.Left, adjacentNESE); tileMap.setAdjacents(SW, Side2D.Right, adjacentNESE); + tileMap.setAdjacents(SW, Side2D.Bottom, adjacentNENW); tileMap.setAdjacents(SW, Side2D.Top, adjacentNENW); + + int gridSizeX = 8, gridSizeY = 6; + var WFC = new WaveFunctionCollapseEuclidean2D(tileMap, gridSizeX, gridSizeY); + + WFC.clear(); + var result = WFC.generate(); + + System.out.println(WFCUtils.WFC2DToString(result)); + + WFC.clear(); + WFC.setCellConstraint(new Cell2D<>(new HashSet<>(Arrays.asList(SE)), 4, 3)); + WFC.setCellConstraint(new Cell2D<>(new HashSet<>(Arrays.asList(NW)), 5, 4)); + result = WFC.generate(); + + System.out.println(WFCUtils.WFC2DToString(result)); + assertEquals("Unexpected tile", result[2][4].getTile(), SW); + assertEquals("Unexpected tile", result[3][3].getTile(), NE); + } +} \ No newline at end of file diff --git a/src/test/java/eu/irzinfante/wfc4j/test/TestWFCToroidal2D.java b/src/test/java/eu/irzinfante/wfc4j/test/TestWFCToroidal2D.java new file mode 100644 index 0000000..b388ab8 --- /dev/null +++ b/src/test/java/eu/irzinfante/wfc4j/test/TestWFCToroidal2D.java @@ -0,0 +1,103 @@ +package eu.irzinfante.wfc4j.test; + +import java.util.HashSet; + +import org.junit.Test; + +import eu.irzinfante.wfc4j.api.WaveFunctionCollapseToroidal2D; +import eu.irzinfante.wfc4j.enums.Side2D; +import eu.irzinfante.wfc4j.exceptions.DimensionException; +import eu.irzinfante.wfc4j.exceptions.TileException; +import eu.irzinfante.wfc4j.model.Tile; +import eu.irzinfante.wfc4j.model.TileMap2D; +import eu.irzinfante.wfc4j.util.WFCUtils; + +public class TestWFCToroidal2D { + + @Test + public void testStringTile() throws TileException, DimensionException { + + final String LR = "LR", LB = "LB", LT = "LT", RB = "RB", RT = "RT", BT = "BT"; + final var components = new String[] {LR, LB, LT, RB, RT, BT}; + + var tileSet = new HashSet>(); + for(var first : components) { + for(var second : components) { + tileSet.add(new Tile<>(first + second)); + } + } + + var tileMap = new TileMap2D<>(tileSet); + + for(var tile : tileSet) { + var tileFirstLeft = tile.getValue().substring(0, 2).contains("L"); + var tileSecondLeft = tile.getValue().substring(2, 4).contains("L"); + var leftAdjacents = new HashSet>(); + + var tileFirstRight = tile.getValue().substring(0, 2).contains("R"); + var tileSecondRight = tile.getValue().substring(2, 4).contains("R"); + var rightAdjacents = new HashSet>(); + + var tileFirstBottom = tile.getValue().substring(0, 2).contains("B"); + var tileSecondBottom = tile.getValue().substring(2, 4).contains("B"); + var bottomAdjacents = new HashSet>(); + + var tileFirstTop = tile.getValue().substring(0, 2).contains("T"); + var tileSecondTop = tile.getValue().substring(2, 4).contains("T"); + var topAdjacents = new HashSet>(); + + for(var adjacent : tileSet) { + var adjacentFirstLeft = adjacent.getValue().substring(0, 2).contains("L"); + var adjacentSecondLeft = adjacent.getValue().substring(2, 4).contains("L"); + if( (tileFirstRight && adjacentFirstLeft && tileSecondRight && adjacentSecondLeft) || + (tileFirstRight && adjacentFirstLeft && !tileSecondRight && !adjacentSecondLeft) || + (!tileFirstRight && !adjacentFirstLeft && tileSecondRight && adjacentSecondLeft) || + (!tileFirstRight && !adjacentFirstLeft && !tileSecondRight && !adjacentSecondLeft) + ) { + rightAdjacents.add(adjacent); + } + + var adjacentFirstRight = adjacent.getValue().substring(0, 2).contains("R"); + var adjacentSecondRight = adjacent.getValue().substring(2, 4).contains("R"); + if( (tileFirstLeft && adjacentFirstRight && tileSecondLeft && adjacentSecondRight) || + (tileFirstLeft && adjacentFirstRight && !tileSecondLeft && !adjacentSecondRight) || + (!tileFirstLeft && !adjacentFirstRight && tileSecondLeft && adjacentSecondRight) || + (!tileFirstLeft && !adjacentFirstRight && !tileSecondLeft && !adjacentSecondRight) + ) { + leftAdjacents.add(adjacent); + } + + var adjacentFirstTop = adjacent.getValue().substring(0, 2).contains("T"); + var adjacentSecondTop = adjacent.getValue().substring(2, 4).contains("T"); + if( (tileFirstBottom && adjacentFirstTop && tileSecondBottom && adjacentSecondTop) || + (tileFirstBottom && adjacentFirstTop && !tileSecondBottom && !adjacentSecondTop) || + (!tileFirstBottom && !adjacentFirstTop && tileSecondBottom && adjacentSecondTop) || + (!tileFirstBottom && !adjacentFirstTop && !tileSecondBottom && !adjacentSecondTop) + ) { + bottomAdjacents.add(adjacent); + } + + var adjacentFirstBottom = adjacent.getValue().substring(0, 2).contains("B"); + var adjacentSecondBottom = adjacent.getValue().substring(2, 4).contains("B"); + if( (tileFirstTop && adjacentFirstBottom && tileSecondTop && adjacentSecondBottom) || + (tileFirstTop && adjacentFirstBottom && !tileSecondTop && !adjacentSecondBottom) || + (!tileFirstTop && !adjacentFirstBottom && tileSecondTop && adjacentSecondBottom) || + (!tileFirstTop && !adjacentFirstBottom && !tileSecondTop && !adjacentSecondBottom) + ) { + topAdjacents.add(adjacent); + } + } + + tileMap.setAdjacents(tile, Side2D.Left, leftAdjacents); + tileMap.setAdjacents(tile, Side2D.Right, rightAdjacents); + tileMap.setAdjacents(tile, Side2D.Bottom, bottomAdjacents); + tileMap.setAdjacents(tile, Side2D.Top, topAdjacents); + } + + int gridSizeX = 6, gridSizeY = 6; + var WFC = new WaveFunctionCollapseToroidal2D(tileMap, gridSizeX, gridSizeY); + + var result = WFC.generate(); + System.out.println(WFCUtils.WFC2DToString(result)); + } +} \ No newline at end of file