├── .gitignore ├── README.md ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── gui └── main │ ├── java │ └── net │ │ └── jmecn │ │ └── map │ │ └── gui │ │ ├── Canvas.java │ │ └── UI.java │ └── resources │ └── net │ └── jmecn │ └── map │ └── gui │ ├── UI.properties │ ├── UI_zh_CN.properties │ ├── create.png │ ├── img.png │ └── txt.png ├── settings.gradle └── src ├── main ├── java │ └── net │ │ └── jmecn │ │ └── map │ │ ├── Direction.java │ │ ├── Map2D.java │ │ ├── Point.java │ │ ├── Rect.java │ │ ├── Tile.java │ │ ├── creator │ │ ├── Building.java │ │ ├── CaveCellauto.java │ │ ├── CaveDLA.java │ │ ├── CaveSanto.java │ │ ├── CityLeafVenation.java │ │ ├── DungeonHauberk.java │ │ ├── DungeonNickgravelyn.java │ │ ├── DungeonTyrant.java │ │ ├── DungeonYan.java │ │ ├── ForestHauberk.java │ │ ├── Islands.java │ │ ├── MapCreator.java │ │ ├── Maze.java │ │ └── MazeWilson.java │ │ └── pathfinding │ │ ├── Node.java │ │ └── PathFinding.java └── resources │ └── net │ └── jmecn │ └── map │ └── creator │ ├── Name.properties │ └── Name_zh_CN.properties └── test └── java └── net └── jmecn └── map └── Map2DTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | /.gradle/ 2 | /bin/ 3 | /build/ 4 | .project 5 | .classpath 6 | /.settings/ 7 | .class 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Referrences: 2 | 3 | 1. Random Dungeon Design 4 | http://www.brainycode.com/downloads/RandomDungeonGenerator.pdf 5 | http://www.roguebasin.com/index.php?title=Java_Example_of_Dungeon-Building_Algorithm 6 | 7 | 2. 房间和迷宫:一个地牢生成算法 8 | http://indienova.com/indie-game-development/rooms-and-mazes-a-procedural-dungeon-generator/ 9 | 10 | 3. 随机生成 Tile Based 地图之——洞穴 11 | http://indienova.com/indie-game-development/procedural-content-generation-tile-based-random-cave-map/ 12 | 13 | 4. 细胞自动机(Cellular automata) 14 | http://cell-auto.com/ 15 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * This build file was auto generated by running the Gradle 'init' task 3 | * by 'yanmaoyuan' at '2016-12-07 20:14' with Gradle 3.2.1 4 | * 5 | * This generated file contains a sample Java project to get you started. 6 | * For more details take a look at the Java Quickstart chapter in the Gradle 7 | * user guide available at https://docs.gradle.org/3.2.1/userguide/tutorial_java_projects.html 8 | */ 9 | 10 | apply plugin: 'java' 11 | 12 | group = 'net.jmecn' 13 | version = '1.0' 14 | sourceCompatibility = 1.6 15 | targetCompatibility = 1.6 16 | [compileJava, compileTestJava, javadoc]*.options*.encoding = 'UTF-8' 17 | 18 | sourceSets { 19 | main { 20 | java { 21 | srcDir 'src/main/java' 22 | srcDir 'gui/main/java' 23 | } 24 | 25 | resources { 26 | srcDir 'src/main/resources' 27 | srcDir 'gui/main/resources' 28 | } 29 | } 30 | 31 | test { 32 | java { 33 | srcDir 'src/test/java' 34 | } 35 | } 36 | } 37 | 38 | 39 | repositories { 40 | jcenter() 41 | } 42 | 43 | dependencies { 44 | 45 | } 46 | 47 | jar { 48 | manifest { 49 | attributes 'Main-Class' : 'net.jmecn.map.gui.UI' 50 | } 51 | 52 | // this will add all the dependent jars into the generated-jar package 53 | from { 54 | configurations.compile.collect { 55 | it.isDirectory() ? it : zipTree(it) 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jmecn/MapGenerator/ed8b5a90487dc72bb0d602cbf1ee260311de2745/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Dec 09 22:41:39 CST 2016 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-3.2.1-bin.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn ( ) { 37 | echo "$*" 38 | } 39 | 40 | die ( ) { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save ( ) { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /gui/main/java/net/jmecn/map/gui/Canvas.java: -------------------------------------------------------------------------------- 1 | package net.jmecn.map.gui; 2 | 3 | import java.awt.Color; 4 | import java.awt.Dimension; 5 | import java.awt.Graphics; 6 | import java.awt.event.MouseAdapter; 7 | import java.awt.event.MouseEvent; 8 | import java.awt.image.BufferedImage; 9 | 10 | import javax.swing.JPanel; 11 | 12 | import net.jmecn.map.Map2D; 13 | import net.jmecn.map.Tile; 14 | 15 | /** 16 | * The renderer 17 | * @author yan 18 | * 19 | */ 20 | public class Canvas extends JPanel { 21 | private BufferedImage image; 22 | 23 | private int SIZE = 12; 24 | 25 | private Color wallColor = new Color(96, 96, 96); 26 | private Color floorColor = new Color(255, 242, 182); 27 | 28 | public Canvas() { 29 | this(12); 30 | } 31 | 32 | public Canvas(int px) { 33 | setPixel(px); 34 | 35 | this.addMouseListener(listener); 36 | this.addMouseMotionListener(listener); 37 | } 38 | 39 | MouseAdapter listener = new MouseAdapter() { 40 | private void changeMap( int x, int y, int type) { 41 | if (map == null) { 42 | return; 43 | } 44 | 45 | x -= 5; 46 | y -= 5; 47 | x /= SIZE; 48 | y /= SIZE; 49 | 50 | if (x >= 0 && y >= 0 && x < col && y < row) { 51 | if (map[y][x] != type) { 52 | map[y][x] = type; 53 | drawImage(); 54 | updateUI(); 55 | } 56 | } 57 | } 58 | 59 | private boolean lPressed = false; 60 | private boolean rPressed = false; 61 | 62 | public void mousePressed(MouseEvent e) { 63 | switch (e.getButton()) { 64 | case MouseEvent.BUTTON1 : // left 65 | lPressed = true; 66 | changeMap(e.getX(), e.getY(), Tile.Wall); 67 | break; 68 | case MouseEvent.BUTTON3 : // right 69 | rPressed = true; 70 | changeMap(e.getX(), e.getY(), Tile.Floor); 71 | break; 72 | } 73 | } 74 | public void mouseReleased(MouseEvent e) { 75 | switch (e.getButton()) { 76 | case MouseEvent.BUTTON1 : // left 77 | lPressed = false; 78 | break; 79 | case MouseEvent.BUTTON3 : // right 80 | rPressed = false; 81 | break; 82 | } 83 | } 84 | public void mouseDragged(MouseEvent e) { 85 | 86 | if (lPressed) { 87 | changeMap(e.getX(), e.getY(), Tile.Wall); 88 | } else if (rPressed) { 89 | changeMap(e.getX(), e.getY(), Tile.Floor); 90 | } 91 | } 92 | }; 93 | 94 | public void setPixel(int px) { 95 | if (px < 7) px = 7; 96 | SIZE = px; 97 | } 98 | private static final long serialVersionUID = -3767183299577116646L; 99 | 100 | int[][] map; 101 | int col = 0; 102 | int row = 0; 103 | 104 | int width = 0; 105 | int height = 0; 106 | 107 | public void setMap(Map2D map) { 108 | this.map = map.getMap(); 109 | this.row = map.getHeight(); 110 | this.col = map.getWidth(); 111 | 112 | width = col * SIZE + 9; 113 | height = row * SIZE + 9; 114 | 115 | image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); 116 | 117 | drawImage(); 118 | this.setPreferredSize(new Dimension(width, height)); 119 | } 120 | 121 | private void drawImage() { 122 | image.flush(); 123 | 124 | Graphics g = image.getGraphics(); 125 | 126 | g.setColor(Color.white); 127 | g.fillRect(0, 0, width, height); 128 | 129 | for (int y = 0; y < row; y++) { 130 | g.setColor(wallColor); 131 | for (int x = 0; x < col; x++) { 132 | switch (map[y][x]) { 133 | case Tile.Unused: { 134 | // skip it 135 | break; 136 | } 137 | case Tile.Floor: { 138 | drawDirtFloor(g, x, y); 139 | break; 140 | } 141 | case Tile.Wall: { 142 | drawWall(g, x, y); 143 | break; 144 | } 145 | case Tile.Stone: { 146 | drawStoneWall(g, x, y); 147 | break; 148 | } 149 | case Tile.Corridor: { 150 | drawCorridor(g, x, y); 151 | break; 152 | } 153 | case Tile.Door: { 154 | drawDoor(g, x, y); 155 | break; 156 | } 157 | case Tile.UpStairs: { 158 | drawUpStairs(g, x, y); 159 | break; 160 | } 161 | case Tile.DownStairs: { 162 | drawDownStairs(g, x, y); 163 | break; 164 | } 165 | case Tile.Water: { 166 | drawWater(g, x, y); 167 | break; 168 | } 169 | case Tile.Grass: { 170 | drawGrass(g, x, y); 171 | break; 172 | } 173 | case Tile.Tree: { 174 | drawTree(g, x, y); 175 | break; 176 | } 177 | } 178 | } 179 | } 180 | } 181 | 182 | private void drawDirtFloor(Graphics g, int posX, int posY) { 183 | drawBox(g, posX, posY, floorColor); 184 | } 185 | 186 | private void drawWall(Graphics g, int posX, int posY) { 187 | drawBox(g, posX, posY, wallColor); 188 | } 189 | 190 | private void drawStoneWall(Graphics g, int posX, int posY) { 191 | int x = 5 + posX * SIZE; 192 | int y = 5 + posY * SIZE; 193 | 194 | g.setColor(new Color(0xEE, 0xEE, 0xFF)); 195 | g.fillRect(x + 1, y + 1 , SIZE - 2, SIZE - 2); 196 | 197 | g.setColor(Color.BLACK); 198 | 199 | g.drawLine(x + 1, y, x + SIZE - 2, y); 200 | g.drawLine(x + 1, y + SIZE - 1, x + SIZE - 2, y + SIZE - 1); 201 | g.drawLine(x, y + 1, x, y + SIZE - 2); 202 | g.drawLine(x + SIZE - 1, y + 1, x + SIZE - 1, y + SIZE - 2); 203 | 204 | g.drawLine(x + 2, y + SIZE - 3, x + SIZE - 3, y + SIZE - 3); 205 | g.drawLine(x + SIZE - 3, y + 2, x + SIZE - 3, y + SIZE - 3); 206 | } 207 | 208 | private void drawDoor(Graphics g, int posX, int posY) { 209 | int x = 5 + posX * SIZE; 210 | int y = 5 + posY * SIZE; 211 | 212 | g.setColor(Color.orange); 213 | g.fillRect(x + 1, y + 1 , SIZE - 2, SIZE - 2); 214 | g.setColor(Color.darkGray); 215 | g.drawRect(x, y, SIZE - 1, SIZE - 1); 216 | g.drawLine(x+2, y+3, x+2, y+4); 217 | 218 | } 219 | 220 | private void drawCorridor(Graphics g, int posX, int posY) { 221 | int x = 5 + posX * SIZE; 222 | int y = 5 + posY * SIZE; 223 | 224 | g.setColor(Color.GRAY); 225 | g.drawRect(x+1, y+1, SIZE-2, SIZE-2); 226 | } 227 | 228 | private void drawUpStairs(Graphics g, int posX, int posY) { 229 | int x = 5 + posX * SIZE; 230 | int y = 5 + posY * SIZE; 231 | 232 | g.setColor(Color.green); 233 | g.fillRect(x + 1, y + 1 , SIZE - 2, SIZE - 2); 234 | } 235 | 236 | private void drawDownStairs(Graphics g, int posX, int posY) { 237 | int x = 5 + posX * SIZE; 238 | int y = 5 + posY * SIZE; 239 | 240 | g.setColor(Color.RED); 241 | g.fillRect(x + 1, y + 1 , SIZE - 2, SIZE - 2); 242 | } 243 | 244 | private void drawWater(Graphics g, int posX, int posY) { 245 | drawBox(g, posX, posY, Color.BLUE); 246 | } 247 | 248 | private void drawGrass(Graphics g, int posX, int posY) { 249 | drawBox(g, posX, posY, new Color(0, 126, 0)); 250 | } 251 | 252 | private void drawTree(Graphics g, int posX, int posY) { 253 | int x = 5 + posX * SIZE; 254 | int y = 5 + posY * SIZE; 255 | 256 | int size = SIZE; 257 | if (SIZE % 2 == 0) { 258 | size--; 259 | } 260 | 261 | g.setColor(Color.GREEN); 262 | g.fillOval(x, y , size, SIZE/2+2); 263 | g.setColor(Color.DARK_GRAY); 264 | g.fillRect(x+size/2-1, y+SIZE/2+1, 3, SIZE/2); 265 | } 266 | 267 | private void drawBox(Graphics g, int posX, int posY, Color color) { 268 | int x = 5 + posX * SIZE; 269 | int y = 5 + posY * SIZE; 270 | 271 | g.setColor(color); 272 | g.fillRect(x, y , SIZE, SIZE); 273 | } 274 | 275 | @Override 276 | public void paint(Graphics g) { 277 | super.paint(g); 278 | g.drawImage(image, 0, 0, null); 279 | } 280 | 281 | @Override 282 | public void update(Graphics g) { 283 | paint(g); 284 | } 285 | 286 | public BufferedImage getImage() { 287 | return image; 288 | } 289 | } 290 | -------------------------------------------------------------------------------- /gui/main/java/net/jmecn/map/gui/UI.java: -------------------------------------------------------------------------------- 1 | package net.jmecn.map.gui; 2 | 3 | import java.awt.BorderLayout; 4 | import java.awt.Container; 5 | import java.awt.Dimension; 6 | import java.awt.FlowLayout; 7 | import java.awt.GridBagConstraints; 8 | import java.awt.GridBagLayout; 9 | import java.awt.Image; 10 | import java.awt.event.ActionEvent; 11 | import java.awt.event.ActionListener; 12 | import java.awt.image.BufferedImage; 13 | import java.io.File; 14 | import java.io.FileOutputStream; 15 | import java.io.IOException; 16 | import java.io.InputStream; 17 | import java.io.PrintStream; 18 | import java.security.MessageDigest; 19 | import java.text.MessageFormat; 20 | import java.util.ArrayList; 21 | import java.util.List; 22 | import java.util.ResourceBundle; 23 | 24 | import javax.imageio.ImageIO; 25 | import javax.swing.BorderFactory; 26 | import javax.swing.ImageIcon; 27 | import javax.swing.JButton; 28 | import javax.swing.JCheckBox; 29 | import javax.swing.JComboBox; 30 | import javax.swing.JFrame; 31 | import javax.swing.JLabel; 32 | import javax.swing.JMenu; 33 | import javax.swing.JMenuBar; 34 | import javax.swing.JMenuItem; 35 | import javax.swing.JPanel; 36 | import javax.swing.JScrollPane; 37 | import javax.swing.JSlider; 38 | import javax.swing.JTextField; 39 | import javax.swing.JToolBar; 40 | import javax.swing.UIManager; 41 | import javax.swing.event.ChangeEvent; 42 | import javax.swing.event.ChangeListener; 43 | 44 | import net.jmecn.map.creator.Building; 45 | import net.jmecn.map.creator.CaveCellauto; 46 | import net.jmecn.map.creator.DungeonHauberk; 47 | import net.jmecn.map.creator.DungeonNickgravelyn; 48 | import net.jmecn.map.creator.DungeonYan; 49 | import net.jmecn.map.creator.ForestHauberk; 50 | import net.jmecn.map.creator.MapCreator; 51 | import net.jmecn.map.creator.Maze; 52 | import net.jmecn.map.creator.MazeWilson; 53 | 54 | /** 55 | * The main UI for MapCreator 56 | * @author yanmaoyuan 57 | * 58 | */ 59 | public class UI extends JFrame implements ActionListener, ChangeListener { 60 | /** 61 | * 62 | */ 63 | private static final long serialVersionUID = -6014389155626784508L; 64 | 65 | ResourceBundle res = ResourceBundle.getBundle("net.jmecn.map.gui.UI"); 66 | 67 | // MapCreater base 68 | private List mapCreators; 69 | private MapCreator creator; 70 | private int width; 71 | private int height; 72 | private long seed; 73 | private boolean isRand; 74 | 75 | private int pixel; 76 | private Canvas canvas; 77 | 78 | // Components 79 | private JComboBox creatorList; 80 | 81 | private JLabel labelWidth; 82 | private JSlider sliderWidth; 83 | 84 | private JLabel labelHeight; 85 | private JSlider sliderHeight; 86 | 87 | private JLabel labelPixel; 88 | private JSlider sliderPixel; 89 | 90 | private JCheckBox checkIsRand; 91 | private JTextField seedText; 92 | 93 | private JButton btnCreate; 94 | 95 | public UI() { 96 | try { 97 | width = Integer.parseInt(res.getString("creator.width")); 98 | height = Integer.parseInt(res.getString("creator.height")); 99 | seed = md5(res.getString("creator.seed")); 100 | isRand = Boolean.valueOf(res.getString("creator.isRand")); 101 | pixel = Integer.parseInt(res.getString("canvas.pixel")); 102 | } catch (Exception e) { 103 | width = 40; 104 | height = 30; 105 | seed = md5("yan"); 106 | isRand = false; 107 | pixel = 12; 108 | } 109 | 110 | initMapCreators(); 111 | 112 | // use the first one 113 | this.creator = mapCreators.get(0); 114 | 115 | this.canvas = new Canvas(pixel); 116 | 117 | this.setTitle(res.getString("ui.title")); 118 | this.setSize(1024, 768); 119 | this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 120 | this.setJMenuBar(getJMenuBar()); 121 | this.setContentPane(getContentPanel()); 122 | 123 | this.updateMap(); 124 | 125 | this.setVisible(true); 126 | 127 | } 128 | 129 | private ImageIcon getScaledImage(String filename, int pixel) { 130 | ImageIcon icon = null; 131 | InputStream in = UI.class.getResourceAsStream(filename); 132 | try { 133 | BufferedImage image = ImageIO.read(in); 134 | Image img = image.getScaledInstance(pixel, pixel, BufferedImage.SCALE_FAST); 135 | icon = new ImageIcon(img); 136 | } catch (IOException e) { 137 | e.printStackTrace(); 138 | } 139 | 140 | return icon; 141 | } 142 | 143 | private void initMapCreators() { 144 | 145 | mapCreators = new ArrayList(); 146 | mapCreators.add(new ForestHauberk(width, height)); 147 | mapCreators.add(new CaveCellauto(width, height)); 148 | mapCreators.add(new DungeonNickgravelyn(width, height)); 149 | mapCreators.add(new DungeonYan(width, height)); 150 | mapCreators.add(new DungeonHauberk(width, height)); 151 | mapCreators.add(new Maze(width, height)); 152 | mapCreators.add(new MazeWilson(width, height)); 153 | mapCreators.add(new Building(width, height)); 154 | 155 | } 156 | 157 | private JPanel getContentPanel() { 158 | JPanel panel = new JPanel(new BorderLayout()); 159 | 160 | // the renderer 161 | JScrollPane pane = new JScrollPane(); 162 | pane.setViewportView(canvas); 163 | panel.add(pane, BorderLayout.CENTER); 164 | panel.add(getOperationPane(), BorderLayout.EAST); 165 | 166 | // the toolbar 167 | panel.add(getJToolBar(), BorderLayout.NORTH); 168 | 169 | return panel; 170 | } 171 | 172 | /** 173 | * update map 174 | */ 175 | private void updateMap() { 176 | creator.resize(width, height); 177 | creator.setSeed(seed); 178 | creator.setUseSeed(!isRand); 179 | creator.initialze(); 180 | creator.create(); 181 | 182 | updateCanvas(); 183 | } 184 | 185 | /** 186 | * Update canvas, redraw the map. 187 | */ 188 | private void updateCanvas() { 189 | canvas.setPixel(pixel); 190 | canvas.setMap(creator.getMap()); 191 | 192 | canvas.updateUI(); 193 | } 194 | 195 | /** 196 | * Save current BufferedImage to a png file. 197 | */ 198 | private void exportPng() { 199 | try { 200 | String name = creator.getClass().getSimpleName(); 201 | long time = System.currentTimeMillis(); 202 | ImageIO.write(canvas.getImage(), "png", new File(name + "_" + time + ".png")); 203 | } catch (IOException e) { 204 | e.printStackTrace(); 205 | } 206 | } 207 | 208 | /** 209 | * Save current Map2D to a txt file. 210 | */ 211 | private void exportTxt() { 212 | try { 213 | String name = creator.getClass().getSimpleName(); 214 | long time = System.currentTimeMillis(); 215 | 216 | PrintStream out = new PrintStream(new FileOutputStream(name + "_" + time + ".txt")); 217 | creator.getMap().printMapChars(out); 218 | creator.getMap().printMapArray(out); 219 | out.close(); 220 | } catch (IOException e) { 221 | e.printStackTrace(); 222 | } 223 | } 224 | 225 | public JMenuBar getJMenuBar() { 226 | JMenuBar bar = new JMenuBar(); 227 | 228 | JMenu fMenu = new JMenu(res.getString("menu.file")); 229 | bar.add(fMenu); 230 | 231 | JMenuItem export = new JMenuItem(res.getString("menu.exportPng")); 232 | export.addActionListener(new ActionListener() { 233 | public void actionPerformed(ActionEvent e) { 234 | exportPng(); 235 | } 236 | }); 237 | 238 | export.setIcon(getScaledImage("img.png", 16)); 239 | fMenu.add(export); 240 | 241 | export = new JMenuItem(res.getString("menu.exportTxt")); 242 | export.addActionListener(new ActionListener() { 243 | public void actionPerformed(ActionEvent e) { 244 | exportTxt(); 245 | } 246 | }); 247 | export.setIcon(getScaledImage("txt.png", 16)); 248 | fMenu.add(export); 249 | 250 | return bar; 251 | } 252 | 253 | public JToolBar getJToolBar() { 254 | JToolBar toolBar = new JToolBar(); 255 | toolBar.setFloatable(false); 256 | toolBar.setOrientation(JToolBar.HORIZONTAL); 257 | toolBar.setAlignmentY(5); 258 | toolBar.setLayout(new FlowLayout(FlowLayout.LEFT)); 259 | 260 | JButton btnCreate = new JButton(res.getString("btn.create")); 261 | btnCreate.addActionListener(new ActionListener() { 262 | public void actionPerformed(ActionEvent e) { 263 | if (!isRand) { 264 | String seeds = seedText.getText(); 265 | seed = md5(seeds); 266 | } 267 | updateMap(); 268 | } 269 | }); 270 | btnCreate.setIcon(getScaledImage("create.png", 32)); 271 | 272 | JButton btnExportPng = new JButton(res.getString("menu.exportPng")); 273 | btnExportPng.addActionListener(new ActionListener() { 274 | public void actionPerformed(ActionEvent e) { 275 | exportPng(); 276 | } 277 | }); 278 | btnExportPng.setIcon(getScaledImage("img.png", 32)); 279 | 280 | JButton btnExportTxt = new JButton(res.getString("menu.exportTxt")); 281 | btnExportTxt.addActionListener(new ActionListener() { 282 | public void actionPerformed(ActionEvent e) { 283 | exportTxt(); 284 | } 285 | }); 286 | btnExportTxt.setIcon(getScaledImage("txt.png", 32)); 287 | 288 | toolBar.add(btnCreate); 289 | toolBar.add(btnExportPng); 290 | toolBar.add(btnExportTxt); 291 | 292 | return toolBar; 293 | } 294 | 295 | private long md5(String seeds) { 296 | long value = seed; 297 | try { 298 | MessageDigest md = MessageDigest.getInstance("MD5"); 299 | md.update(seeds.getBytes("UTF-8")); 300 | 301 | byte byteData[] = md.digest(); 302 | 303 | // convert the byte to hex format method 2 304 | StringBuffer hexString = new StringBuffer(); 305 | hexString.append("0x"); 306 | for (int i = 0; i < 7; i++) { 307 | String hex = Integer.toHexString(0xff & byteData[i]); 308 | if (hex.length() == 1) 309 | hexString.append('0'); 310 | hexString.append(hex); 311 | } 312 | value = Long.decode(hexString.toString()); 313 | } catch (Exception ex) { 314 | ex.printStackTrace(); 315 | } 316 | 317 | return value; 318 | } 319 | 320 | /** 321 | * This is the operation panel where you can change map width/height. 322 | * @return 323 | */ 324 | private Container getOperationPane() { 325 | GridBagLayout gridBag =new GridBagLayout(); 326 | JPanel container = new JPanel(gridBag); 327 | 328 | GridBagConstraints gbc = new GridBagConstraints(); 329 | gbc.gridx = 0; 330 | gbc.gridy = 0; 331 | gbc.fill = GridBagConstraints.HORIZONTAL; 332 | gbc.anchor = GridBagConstraints.WEST; 333 | container.add(getMapPanel(), gbc); 334 | 335 | gbc = new GridBagConstraints(); 336 | gbc.gridx = 0; 337 | gbc.gridy = GridBagConstraints.RELATIVE; 338 | gbc.anchor = GridBagConstraints.WEST; 339 | container.add(getCanvasPanel(), gbc); 340 | 341 | gbc = new GridBagConstraints(); 342 | gbc.gridx = 0; 343 | gbc.gridy = GridBagConstraints.RELATIVE; 344 | gbc.anchor = GridBagConstraints.WEST; 345 | 346 | container.add(getSeedPanel(), gbc); 347 | 348 | gbc = new GridBagConstraints(); 349 | gbc.gridx = 0; 350 | gbc.gridy = GridBagConstraints.RELATIVE; 351 | gbc.anchor = GridBagConstraints.WEST; 352 | 353 | btnCreate = new JButton(res.getString("btn.create")); 354 | btnCreate.addActionListener(this); 355 | btnCreate.setIcon(getScaledImage("create.png", 16)); 356 | 357 | container.add(btnCreate, gbc); 358 | 359 | return container; 360 | } 361 | 362 | /** 363 | * In this panel, you choose the map creator to build a random map, use sliders to change map's width and height. 364 | * @return 365 | */ 366 | private Container getMapPanel() { 367 | JPanel container = new JPanel (new FlowLayout(FlowLayout.LEFT)); 368 | container.setBorder(BorderFactory.createTitledBorder(res.getString("panel.map"))); 369 | container.setPreferredSize(new Dimension(220, 240)); 370 | container.setMinimumSize(new Dimension(220, 240)); 371 | 372 | creatorList = new JComboBox(); 373 | for(int i=0; i= 0 && x < width; 79 | } 80 | 81 | public boolean isYInBounds(int y) { 82 | return y >= 0 && y < height; 83 | } 84 | 85 | public boolean isAreaUnused(int xStart, int yStart, int xEnd, int yEnd) { 86 | assert (isXInBounds(xStart) && isXInBounds(xEnd)); 87 | assert (isXInBounds(yStart) && isXInBounds(yEnd)); 88 | 89 | assert (xStart <= xEnd); 90 | assert (yStart <= yEnd); 91 | 92 | for (int y = yStart; y != yEnd + 1; ++y) 93 | for (int x = xStart; x != xEnd + 1; ++x) 94 | if (map[y][x] != Tile.Unused) 95 | return false; 96 | 97 | return true; 98 | } 99 | 100 | public boolean isAdjacent(int x, int y, int tile) { 101 | assert (isXInBounds(x - 1) && isXInBounds(x + 1)); 102 | assert (isXInBounds(y - 1) && isXInBounds(y + 1)); 103 | 104 | return map[y][x - 1] == tile || map[y][x + 1] == tile || map[y - 1][x] == tile || map[y + 1][x] == tile; 105 | } 106 | 107 | public int[][] getMap() { 108 | return map; 109 | } 110 | 111 | public boolean contains(int x, int y) { 112 | return (x >= 0 && y >= 0 && x < width && y < height); 113 | } 114 | 115 | public void printMapArray() { 116 | printMapArray(System.out); 117 | } 118 | 119 | /** 120 | * used to print the map on the screen 121 | * 122 | * @param os 123 | */ 124 | public void printMapArray(OutputStream os) { 125 | PrintStream out = new PrintStream(os); 126 | out.println("int width = " + width + ";"); 127 | out.println("int height = " + height + ";"); 128 | out.println("int[][] map = {"); 129 | for (int y = 0; y < height; y++) { 130 | out.print("\t{"); 131 | for (int x = 0; x < width; x++) { 132 | out.print(map[y][x]); 133 | if (x != width - 1) { 134 | out.print(","); 135 | } 136 | } 137 | out.print("}"); 138 | if (y != height - 1) { 139 | out.print(","); 140 | } 141 | out.println(); 142 | } 143 | out.println("};"); 144 | } 145 | 146 | public void printMapChars() { 147 | printMapChars(System.out); 148 | } 149 | 150 | public void printMapChars(OutputStream os) { 151 | PrintStream out = new PrintStream(os); 152 | out.println("/* preview"); 153 | for (int y = 0; y < height; y++) { 154 | for (int x = 0; x < width; x++) { 155 | out.print(Tile.getChar(map[y][x])); 156 | } 157 | out.println(); 158 | } 159 | out.println("*/"); 160 | } 161 | 162 | public static void main(String[] args) { 163 | Map2D map = new Map2D(40, 20); 164 | map.printMapArray(); 165 | } 166 | 167 | } 168 | -------------------------------------------------------------------------------- /src/main/java/net/jmecn/map/Point.java: -------------------------------------------------------------------------------- 1 | package net.jmecn.map; 2 | 3 | public class Point { 4 | public int x; 5 | public int y; 6 | 7 | public Point() { 8 | x = y = 0; 9 | } 10 | 11 | public Point(int x, int y) { 12 | this.x = x; 13 | this.y = y; 14 | } 15 | 16 | public Point(int x, int y, int tile) { 17 | this.x = x; 18 | this.y = y; 19 | } 20 | 21 | public int getX() { 22 | return x; 23 | } 24 | 25 | public int getY() { 26 | return y; 27 | } 28 | 29 | public boolean equals(Object o) { 30 | if (o == null) 31 | return false; 32 | 33 | if (o instanceof Point) { 34 | Point p = (Point) o; 35 | return (p.x == x && p.y == y); 36 | } 37 | 38 | return false; 39 | } 40 | 41 | public int hashCode() { 42 | final int prime = 31; 43 | int result = 1; 44 | result = prime * result + x; 45 | result = prime * result + y; 46 | return result; 47 | } 48 | 49 | @Override 50 | public String toString() { 51 | return "Point(" + x + ", " + y + ")"; 52 | } 53 | } -------------------------------------------------------------------------------- /src/main/java/net/jmecn/map/Rect.java: -------------------------------------------------------------------------------- 1 | package net.jmecn.map; 2 | 3 | public class Rect { 4 | public int x, y, width, height; 5 | 6 | public Rect() { 7 | x = y = width = height = 0; 8 | } 9 | 10 | public Rect(int x, int y, int width, int height) { 11 | this.x = x; 12 | this.y = y; 13 | this.width = width; 14 | this.height = height; 15 | } 16 | 17 | private boolean valueInRange(int value, int min, int max) { 18 | return (value <= max) && (value >= min); 19 | } 20 | 21 | public boolean contains(Point p) { 22 | return valueInRange(p.x, x, x + width) && valueInRange(p.y, y, y + height); 23 | } 24 | 25 | public boolean overlap(Rect B) { 26 | boolean xOverlap = valueInRange(x, B.x, B.x + B.width) || valueInRange(B.x, x, x + width); 27 | 28 | boolean yOverlap = valueInRange(y, B.y, B.y + B.height) || valueInRange(B.y, y, y + height); 29 | 30 | return xOverlap && yOverlap; 31 | } 32 | 33 | public int centerX(){ 34 | return x + (width / 2); 35 | } 36 | 37 | public int centerY(){ 38 | return y + (height / 2); 39 | } 40 | } -------------------------------------------------------------------------------- /src/main/java/net/jmecn/map/Tile.java: -------------------------------------------------------------------------------- 1 | package net.jmecn.map; 2 | 3 | final public class Tile { 4 | 5 | private Tile() { 6 | } 7 | 8 | public final static int Unused = -1; 9 | public final static int Floor = 0; 10 | public final static int Wall = 1; 11 | public final static int Stone = 2; 12 | public final static int Corridor = 3; 13 | public final static int Door = 4; 14 | public final static int UpStairs = 5; 15 | public final static int DownStairs = 6; 16 | public final static int Chest = 7; 17 | // terrain 18 | public final static int Water = 8; 19 | public final static int Grass = 9; 20 | public final static int Dirt = 10; 21 | public final static int Moss = 11; 22 | public final static int Tree = 12; 23 | 24 | public final static char getChar(int value) { 25 | switch (value) { 26 | case Unused: 27 | return ' '; 28 | case Wall: 29 | return '#'; 30 | case Floor: 31 | return '.'; 32 | case Stone: 33 | return 'S'; 34 | case Corridor: 35 | return '.'; 36 | case Door: 37 | return '+'; 38 | case UpStairs: 39 | return '<'; 40 | case DownStairs: 41 | return '>'; 42 | case Chest: 43 | return 'C'; 44 | case Water: 45 | return 'w'; 46 | case Grass: 47 | return '`'; 48 | case Dirt: 49 | return 'o'; 50 | case Moss: 51 | return '~'; 52 | case Tree: 53 | return 'T'; 54 | default: 55 | throw new RuntimeException("Unknown tile value=" + value); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/net/jmecn/map/creator/Building.java: -------------------------------------------------------------------------------- 1 | package net.jmecn.map.creator; 2 | 3 | import static net.jmecn.map.Tile.*; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | 8 | import net.jmecn.map.Point; 9 | 10 | /** 11 | * Basic BSP Dungeon generation 12 | * 13 | * http://www.roguebasin.com/index.php?title=Basic_BSP_Dungeon_generation 14 | * 15 | * @author yanmaoyuan 16 | * 17 | */ 18 | public class Building extends MapCreator { 19 | 20 | private int iterations; 21 | final static private int MIN = 3; 22 | 23 | public Building(int width, int height) { 24 | super("creator.building", width, height); 25 | this.iterations = 10; 26 | } 27 | 28 | public void setIterations(int iterations) { 29 | this.iterations = iterations; 30 | } 31 | 32 | @Override 33 | public void initialze() { 34 | map.fill(Floor); 35 | } 36 | 37 | @Override 38 | public void create() { 39 | Room root = new Room(0, 0, width, height); 40 | split(root, 0); 41 | buildWalls(root); 42 | buildDoors(root); 43 | map.buildBoundary(Wall); 44 | } 45 | 46 | private void buildDoors(Room room) { 47 | if (room.getLeft() != null && room.getRight() != null) { 48 | List points = new ArrayList(); 49 | if (room.isSplitVert()) { 50 | int x = room.getSplitX(); 51 | for (int y = room.getY(); y < room.getY() + room.getHeight(); y++) { 52 | if (map.get(x + 1, y) == Floor && map.get(x - 1, y) == Floor && y != 0 && y != height - 1) { 53 | points.add(new Point(x, y)); 54 | } 55 | } 56 | } else { 57 | int y = room.getSplitY(); 58 | for (int x = room.getX(); x < room.getX() + room.getWidth(); x++) { 59 | if (map.get(x, y + 1) == Floor && map.get(x, y - 1) == Floor && x != 0 && x != width - 1) { 60 | points.add(new Point(x, y)); 61 | } 62 | } 63 | } 64 | 65 | Point selection = points.get(nextInt(points.size())); 66 | map.set(selection.x, selection.y, Door); 67 | 68 | buildDoors(room.getLeft()); 69 | buildDoors(room.getRight()); 70 | } 71 | } 72 | 73 | private void buildWalls(Room room) { 74 | if (room.getLeft() != null && room.getRight() != null) { 75 | if (room.isSplitVert()) { 76 | int x = room.getSplitX(); 77 | for (int y = room.getY(); y < room.getY() + room.getHeight(); y++) { 78 | map.set(x, y, Wall); 79 | } 80 | } else { 81 | int y = room.getSplitY(); 82 | for (int x = room.getX(); x < room.getX() + room.getWidth(); x++) { 83 | map.set(x, y, Wall); 84 | } 85 | } 86 | 87 | buildWalls(room.getLeft()); 88 | buildWalls(room.getRight()); 89 | } 90 | } 91 | 92 | private void split(Room room, int iteration) { 93 | int limit = 2 * MIN + 1; 94 | if (iteration < iterations && (room.getWidth() > limit || room.getHeight() > limit)) { 95 | boolean splitVert = false; 96 | if (room.getWidth() <= limit) { 97 | splitVert = false; 98 | } else if (room.getHeight() <= limit) { 99 | splitVert = true; 100 | } else { 101 | splitVert = nextBoolean(); 102 | } 103 | 104 | Room leftOrTop; 105 | Room rightOrBottom; 106 | int splitPoint; 107 | if (splitVert) { 108 | splitPoint = MIN + room.getX() + nextInt(room.getWidth() - limit); 109 | leftOrTop = new Room(room.getX(), room.getY(), splitPoint - room.getX(), room.getHeight()); 110 | rightOrBottom = new Room(splitPoint, room.getY(), room.getX() + room.getWidth() - splitPoint, 111 | room.getHeight()); 112 | room.setSplitX(splitPoint); 113 | room.setSplitY(room.getHeight()); 114 | } else { 115 | splitPoint = MIN + room.getY() + nextInt(room.getHeight() - limit); 116 | leftOrTop = new Room(room.getX(), room.getY(), room.getWidth(), splitPoint - room.getY()); 117 | rightOrBottom = new Room(room.getX(), splitPoint, room.getWidth(), 118 | room.getY() + room.getHeight() - splitPoint); 119 | room.setSplitX(room.getWidth()); 120 | room.setSplitY(splitPoint); 121 | } 122 | 123 | room.setSplitVert(splitVert); 124 | room.setLeft(leftOrTop); 125 | room.setRight(rightOrBottom); 126 | 127 | iteration++; 128 | split(leftOrTop, iteration); 129 | split(rightOrBottom, iteration); 130 | } 131 | } 132 | 133 | private static class Room { 134 | int x, y, width, height, splitX, splitY; 135 | boolean splitVert; 136 | Room left, right; 137 | 138 | public Room(int x, int y, int width, int height) { 139 | this.x = x; 140 | this.y = y; 141 | this.width = width; 142 | this.height = height; 143 | splitVert = false; 144 | left = null; 145 | right = null; 146 | splitX = -1; 147 | splitY = -1; 148 | } 149 | 150 | public void setSplitVert(boolean splitVert) { 151 | this.splitVert = splitVert; 152 | } 153 | 154 | public int getX() { 155 | return x; 156 | } 157 | 158 | public int getY() { 159 | return y; 160 | } 161 | 162 | public int getWidth() { 163 | return width; 164 | } 165 | 166 | public int getHeight() { 167 | return height; 168 | } 169 | 170 | public int getSplitX() { 171 | return splitX; 172 | } 173 | 174 | public int getSplitY() { 175 | return splitY; 176 | } 177 | 178 | public boolean isSplitVert() { 179 | return splitVert; 180 | } 181 | 182 | public Room getLeft() { 183 | return left; 184 | } 185 | 186 | public Room getRight() { 187 | return right; 188 | } 189 | 190 | public void setRight(Room right) { 191 | this.right = right; 192 | } 193 | 194 | public void setLeft(Room left) { 195 | this.left = left; 196 | } 197 | 198 | public void setSplitY(int splitY) { 199 | this.splitY = splitY; 200 | } 201 | 202 | public void setSplitX(int splitX) { 203 | this.splitX = splitX; 204 | } 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /src/main/java/net/jmecn/map/creator/CaveCellauto.java: -------------------------------------------------------------------------------- 1 | package net.jmecn.map.creator; 2 | 3 | import static net.jmecn.map.Tile.*; 4 | 5 | /** 6 | * https://gamedevelopment.tutsplus.com/tutorials/generate-random-cave-levels-using-cellular-automata--gamedev-9664 7 | * 8 | * http://pixelenvy.ca/wa/ca_cave.html 9 | * 10 | * @author yanmaoyuan 11 | * 12 | */ 13 | public class CaveCellauto extends MapCreator { 14 | 15 | private int fillprob = 40; 16 | final static private int r1Cutoff = 5; 17 | final static private int r2Cutoff = 2; 18 | 19 | public CaveCellauto(int width, int height) { 20 | super("creator.cave.cellauto", width, height); 21 | } 22 | 23 | protected int randPick() { 24 | if (rand.nextInt(100) < fillprob) { 25 | return Wall; 26 | } else { 27 | return Floor; 28 | } 29 | } 30 | 31 | @Override 32 | public void initialze() { 33 | for (int y = 0; y < height; y++) { 34 | for (int x = 0; x < width; x++) { 35 | if (x == 0 || y == 0 || x == width - 1 || y == height - 1) { 36 | map.set(x, y, Wall); 37 | } else { 38 | map.set(x, y, randPick()); 39 | } 40 | } 41 | } 42 | 43 | } 44 | 45 | @Override 46 | public void create() { 47 | do { 48 | initialze(); 49 | for (int i = 0; i < 7; i++) { 50 | generation(i); 51 | } 52 | } while(!(floodFill())); 53 | 54 | // fill disconnected cave with wall 55 | for (int y = 1; y < height-1; y++) { 56 | for (int x = 1; x < width-1; x++) { 57 | if (map.get(x, y) == Unused) { 58 | map.set(x, y, Floor); 59 | } else if (map.get(x, y) == Floor) { 60 | map.set(x, y, Wall); 61 | } 62 | } 63 | } 64 | } 65 | 66 | private int sum = 0; 67 | 68 | private boolean floodFill() { 69 | // get a random start point 70 | int x, y; 71 | do { 72 | x = nextInt(width); 73 | y = nextInt(height); 74 | } while (map.get(x, y) != Floor); 75 | 76 | // 77 | sum = 0; 78 | floodFill8(x, y, Unused, Floor); 79 | 80 | // at least 45% place are floor 81 | double percent = 100.0 * sum/(width * height); 82 | return percent >= 45.0; 83 | } 84 | 85 | private void floodFill8(int x, int y, int newTile, int oldTile) { 86 | 87 | // skip boundary 88 | if (x < 0 || x > width - 1 || y < 0 || y > height - 1 || map.get(x, y) != oldTile || map.get(x, y) == newTile) { 89 | return; 90 | } 91 | map.set(x, y, newTile); 92 | sum++; 93 | 94 | floodFill8(x - 1, y - 1, newTile, oldTile); 95 | floodFill8(x - 1, y, newTile, oldTile); 96 | floodFill8(x - 1, y + 1, newTile, oldTile); 97 | floodFill8(x, y - 1, newTile, oldTile); 98 | floodFill8(x, y + 1, newTile, oldTile); 99 | floodFill8(x + 1, y - 1, newTile, oldTile); 100 | floodFill8(x + 1, y, newTile, oldTile); 101 | floodFill8(x + 1, y + 1, newTile, oldTile); 102 | 103 | } 104 | 105 | protected void generation(int gen) { 106 | int[][] tmp = copy(); 107 | for (int y = 1; y < height - 1; y++) { 108 | for (int x = 1; x < width - 1; x++) { 109 | 110 | int m = 0; 111 | for (int offsety = -1; offsety <= 1; offsety++) { 112 | for (int offsetx = -1; offsetx <= 1; offsetx++) { 113 | if (map.get(offsetx + x, offsety + y) == Wall) { 114 | m++; 115 | } 116 | } 117 | } 118 | 119 | int n = 0; 120 | for (int offsety = -2; offsety <= 2; offsety++) { 121 | for (int offsetx = -2; offsetx <= 2; offsetx++) { 122 | if (Math.abs(offsetx) == 2 && Math.abs(offsety) == 2) { 123 | continue; 124 | } 125 | 126 | if (map.get(offsetx + x, offsety + y) == Wall) { 127 | n++; 128 | } 129 | } 130 | } 131 | 132 | if (gen < 4) { 133 | if (m >= r1Cutoff || n <= r2Cutoff) { 134 | tmp[y][x] = Wall; 135 | } else { 136 | tmp[y][x] = Floor; 137 | } 138 | } else { 139 | if (m >= r1Cutoff) { 140 | tmp[y][x] = Wall; 141 | } else { 142 | tmp[y][x] = Floor; 143 | } 144 | } 145 | 146 | } 147 | } 148 | 149 | for (int y = 0; y < height; y++) { 150 | for (int x = 0; x < width; x++) { 151 | map.set(x, y, tmp[y][x]); 152 | } 153 | } 154 | } 155 | 156 | public void setFillprob(int fillprob) { 157 | this.fillprob = fillprob; 158 | } 159 | 160 | public static void main(String[] args) { 161 | CaveCellauto cave = new CaveCellauto(30, 30); 162 | cave.setSeed(1654987414656544l); 163 | cave.setUseSeed(true); 164 | cave.initialze(); 165 | cave.create(); 166 | cave.getMap().printMapChars(); 167 | cave.getMap().printMapArray(); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/main/java/net/jmecn/map/creator/CaveDLA.java: -------------------------------------------------------------------------------- 1 | package net.jmecn.map.creator; 2 | 3 | import static net.jmecn.map.Tile.*; 4 | 5 | /** 6 | * Diffusion-limited aggregation 7 | * 8 | * http://www.roguebasin.com/index.php?title=Diffusion-limited_aggregation 9 | * 10 | * @author yanmaoyuan 11 | * 12 | */ 13 | public class CaveDLA extends MapCreator { 14 | 15 | public CaveDLA(int width, int height) { 16 | super("creator.cave.dla", width, height); 17 | } 18 | 19 | @Override 20 | public void initialze() { 21 | map.fill(Wall); 22 | } 23 | 24 | @Override 25 | public void create() { 26 | int y = 0, x = 0; 27 | int builderSpawned = 0; 28 | int builderMoveDirection = 0; 29 | int allocatedBlocks = 0; // variable used to track the percentage of the map filled 30 | int rootX = width / 2, rootY = height / 2; // this is where the growth starts from. Currently center of map 31 | int stepped = 0; // this is how long corridors can be 32 | boolean orthogonalAllowed = false; // Orthogonal movement allowed? If not, it carves a wider cooridor on diagonal 33 | 34 | int[][] map = this.map.getMap(); 35 | 36 | /* The Diffusion Limited Aggregation Loop */ 37 | while (allocatedBlocks < ((width * height) / 8)) { // quit when an eighth of the map is filled 38 | if (builderSpawned != 1) { 39 | // Spawn at random position 40 | x = nextInt(width - 2) + 1; 41 | y = nextInt(height - 2) + 1; 42 | // See if builder is ontop of root 43 | if (Math.abs(rootX - y) <= 0 && Math.abs(rootY - x) <= 0) { 44 | // builder was spawned too close to root, clear that floor 45 | // and respawn 46 | if (map[y][x] != Floor) { 47 | map[y][x] = Floor; 48 | allocatedBlocks++; 49 | } // end if 50 | } else { 51 | builderSpawned = 1; 52 | builderMoveDirection = nextInt(8); 53 | stepped = 0; 54 | } // end if 55 | } else { 56 | // builder already spawned and knows it's direction, move 57 | // builder 58 | /* North */ if (builderMoveDirection == 0 && x > 0) { 59 | x--; 60 | stepped++; 61 | /* East */ } else if (builderMoveDirection == 1 && y < height - 1) { 62 | y++; 63 | stepped++; 64 | /* South */ } else if (builderMoveDirection == 2 && x < width - 1) { 65 | x++; 66 | stepped++; 67 | /* West */ } else if (builderMoveDirection == 3 && y > 0) { 68 | y++; 69 | stepped++; 70 | /* Northeast */ } else if (builderMoveDirection == 4 && y < height - 1 && x > 0) { 71 | x--; 72 | y++; 73 | stepped++; 74 | /* Southeast */ } else if (builderMoveDirection == 5 && y < height - 1 && x < width - 1) { 75 | x++; 76 | y++; 77 | stepped++; 78 | /* Southwest */ } else if (builderMoveDirection == 6 && y > 0 && x < width - 1) { 79 | x++; 80 | y--; 81 | stepped++; 82 | /* Northwest */ } else if (builderMoveDirection == 7 && y > 0 && x > 0) { 83 | x--; 84 | y--; 85 | stepped++; 86 | } 87 | /* ensure that the builder is touching an existing spot */ 88 | if (y < height - 1 && x < width - 1 && y > 0 && x > 0 && stepped <= 5) { 89 | /* East */ if (map[y + 1][x] == Floor) { 90 | if (map[y][x] != Floor) { 91 | map[y][x] = Floor; 92 | allocatedBlocks++; 93 | } 94 | /* West */ } else if (map[y - 1][x] == Floor) { 95 | if (map[y][x] != Floor) { 96 | map[y][x] = Floor; 97 | allocatedBlocks++; 98 | } 99 | /* South */ } else if (map[y][x + 1] == Floor) { 100 | if (map[y][x] != Floor) { 101 | map[y][x] = Floor; 102 | allocatedBlocks++; 103 | } 104 | /* North */ } else if (map[y][x - 1] == Floor) { 105 | if (map[y][x] != Floor) { 106 | map[y][x] = Floor; 107 | allocatedBlocks++; 108 | } 109 | /* Northeast */ } else if (map[y + 1][x - 1] == Floor) { 110 | if (map[y][x] != Floor) { 111 | map[y][x] = Floor; 112 | allocatedBlocks++; 113 | if (!orthogonalAllowed) { 114 | map[y + 1][x] = Floor; 115 | allocatedBlocks++; 116 | } 117 | } 118 | /* Southeast */ } else if (map[y + 1][x + 1] == Floor) { 119 | if (map[y][x] != Floor) { 120 | map[y][x] = Floor; 121 | allocatedBlocks++; 122 | if (!orthogonalAllowed) { 123 | map[y + 1][x] = Floor; 124 | allocatedBlocks++; 125 | } 126 | } 127 | /* Southwest */ } else if (map[y - 1][x + 1] == Floor) { 128 | if (map[y][x] != Floor) { 129 | map[y][x] = Floor; 130 | allocatedBlocks++; 131 | if (!orthogonalAllowed) { 132 | map[y - 1][x] = Floor; 133 | allocatedBlocks++; 134 | } 135 | } 136 | /* Northwest */ } else if (map[y - 1][x - 1] == Floor) { 137 | if (map[y][x] != Floor) { 138 | map[y][x] = Floor; 139 | allocatedBlocks++; 140 | if (!orthogonalAllowed) { 141 | map[y - 1][x] = Floor; 142 | allocatedBlocks++; 143 | } 144 | } 145 | } 146 | } else { 147 | builderSpawned = 0; 148 | } 149 | } // end if 150 | } // end while 151 | } 152 | 153 | } 154 | -------------------------------------------------------------------------------- /src/main/java/net/jmecn/map/creator/CaveSanto.java: -------------------------------------------------------------------------------- 1 | package net.jmecn.map.creator; 2 | 3 | import static net.jmecn.map.Tile.*; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | 8 | import net.jmecn.map.Point; 9 | 10 | /** 11 | * https://github.com/scarino/DungeonGenerator/blob/master/src/main/java/com/scarino/dungeongenerator/CaveGenerator.java 12 | * 13 | * 14 | * @author yanmaoyuan 15 | * 16 | */ 17 | public class CaveSanto extends MapCreator { 18 | 19 | private int seedCount; 20 | private int iterations; 21 | 22 | public CaveSanto(int width, int height) { 23 | super("creator.cave.santo", width, height); 24 | seedCount = 5; 25 | iterations = 300; 26 | } 27 | 28 | public CaveSanto(String name, int width, int height) { 29 | super(name, width, height); 30 | } 31 | 32 | @Override 33 | public void initialze() { 34 | map.fill(Wall); 35 | } 36 | 37 | @Override 38 | public void create() { 39 | List seeds = generateSeeds(); 40 | List dungeonList = new ArrayList(iterations+seedCount); 41 | List potentialTiles = new ArrayList(iterations+seedCount); 42 | 43 | for(Point pos : seeds){ 44 | dungeonList.add(pos); 45 | potentialTiles.addAll(getNeighbours(pos)); 46 | } 47 | 48 | int count = 0; 49 | while(count < iterations){ 50 | int next = nextInt(potentialTiles.size()); 51 | Point pos = potentialTiles.remove(next); 52 | if(!posExists(pos.getX(), pos.getY(), dungeonList) && !edgePos(pos)) { 53 | dungeonList.add(pos); 54 | potentialTiles.addAll(getNeighbours(pos)); 55 | count++; 56 | } 57 | } 58 | 59 | connectSeeds(seeds, dungeonList); 60 | copyListToArray(dungeonList); 61 | } 62 | 63 | private boolean edgePos(Point pos){ 64 | return pos.getY() == 0 || pos.getY() == this.height - 1 || 65 | pos.getX() == 0 || pos.getX() == this.width - 1; 66 | 67 | } 68 | 69 | private List getNeighbours(Point pos){ 70 | List neighbours = new ArrayList(); 71 | for (int offsetY = -1; offsetY <= 1; offsetY++) { 72 | for (int offsetX = -1; offsetX <= 1; offsetX++) { 73 | if (offsetX == 0 && offsetY == 0) 74 | continue; 75 | neighbours.add(new Point(pos.x + offsetX, pos.y + offsetY)); 76 | } 77 | } 78 | return neighbours; 79 | } 80 | 81 | private void connectSeeds(List seeds, List dungeonList){ 82 | Point first = seeds.remove(0); 83 | Point next; 84 | while(seeds.size() > 0){ 85 | next = getClosestSeed(first, seeds); 86 | addConnections(first, next, dungeonList); 87 | first = next; 88 | } 89 | } 90 | 91 | private void addConnections (Point first, Point second, List dungeonList){ 92 | int firstX = first.getX(); 93 | int secondX = second.getX(); 94 | int firstY = first.getY(); 95 | int secondY = second.getY(); 96 | 97 | while(firstX != secondX){ 98 | if(firstX < secondX){ 99 | firstX++; 100 | } 101 | else{ 102 | firstX--; 103 | } 104 | 105 | dungeonList.add(new Point(firstX, firstY)); 106 | } 107 | 108 | while(firstY != secondY){ 109 | if(firstY < secondY){ 110 | firstY++; 111 | } 112 | else{ 113 | firstY--; 114 | } 115 | 116 | dungeonList.add(new Point(firstX, firstY)); 117 | } 118 | } 119 | 120 | private Point getClosestSeed(Point seed, List seeds){ 121 | int curPos = -1; 122 | int curDis = Integer.MAX_VALUE; 123 | 124 | Point current; 125 | int nextDis; 126 | for(int i = 0; i < seeds.size(); i++){ 127 | current = seeds.get(i); 128 | nextDis = distance(seed, current); 129 | if(nextDis < curDis){ 130 | curPos = i; 131 | curDis = nextDis; 132 | } 133 | } 134 | 135 | return seeds.remove(curPos); 136 | } 137 | 138 | private int distance(Point seedOne, Point seedTwo){ 139 | int xDis = seedOne.getX() - seedTwo.getX(); 140 | xDis *= xDis; 141 | 142 | int yDis = seedOne.getY() - seedTwo.getY(); 143 | yDis *= yDis; 144 | 145 | return (int)Math.sqrt(xDis + yDis); 146 | } 147 | 148 | private void copyListToArray(List dungeonList){ 149 | for(Point pos : dungeonList){ 150 | map.set(pos.x, pos.y, Floor); 151 | } 152 | } 153 | 154 | private List generateSeeds(){ 155 | List seeds = new ArrayList(seedCount); 156 | 157 | int count = 0; 158 | while(count < seedCount){ 159 | int x = 1 + nextInt(width-2); 160 | int y = 1 + nextInt(height-2); 161 | 162 | if(!posExists(x, y, seeds)){ 163 | seeds.add(new Point(x, y)); 164 | count++; 165 | } 166 | } 167 | 168 | return seeds; 169 | } 170 | 171 | public boolean posExists(int x, int y, List positions){ 172 | 173 | for(Point pos : positions){ 174 | if(x == pos.getX() && y == pos.getY()){ 175 | return true; 176 | } 177 | } 178 | 179 | return false; 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /src/main/java/net/jmecn/map/creator/CityLeafVenation.java: -------------------------------------------------------------------------------- 1 | package net.jmecn.map.creator; 2 | 3 | import static net.jmecn.map.Tile.*; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | import java.util.logging.Logger; 8 | 9 | /** 10 | * An Implementation of City Generation by Leaf Venation 11 | * 12 | * The source was wrote on Python : 13 | * http://www.roguebasin.com/index.php?title=An_Implementation_of_City_Generation_by_Leaf_Venation 14 | * 15 | * @author yanmaoyuan 16 | * 17 | */ 18 | public class CityLeafVenation extends MapCreator { 19 | 20 | static Logger logger = Logger.getLogger(CityLeafVenation.class.getName()); 21 | 22 | private final static double RADIUS_INCREASE = 10.0; 23 | private final static double NEW_SITE_DENSITY = 0.0002; 24 | private final static double AUXIN_BIRTH_THRESHOLD = 2.0; 25 | private final static double VEIN_BIRTH_THRESHOLD = 2.0; 26 | private final static double VEIN_DEATH_THRESHOLD = 1.0; 27 | private final static double NODE_DISTANCE = 1.0; 28 | 29 | private final static int ITERATIONS = 20; 30 | 31 | // DATA 32 | private List auxinSites; 33 | private List veinSites; 34 | private int usedVeinIDs = 1; 35 | private int usedAuxinIDs = 0; 36 | private double radius = 10.0; 37 | 38 | public CityLeafVenation(int width, int height) { 39 | super("creator.city.leafvenation", width, height); 40 | 41 | auxinSites = new ArrayList(); 42 | veinSites = new ArrayList(); 43 | } 44 | 45 | @Override 46 | public void initialze() { 47 | usedVeinIDs = 0; 48 | usedAuxinIDs = 0; 49 | radius = 10.0; 50 | 51 | // int start_id = new_vein_id(); 52 | auxinSites.clear(); 53 | veinSites.clear(); 54 | veinSites.add(new Vein(usedVeinIDs++)); 55 | } 56 | 57 | @Override 58 | public void create() { 59 | for (int i = 0; i < ITERATIONS; i++) { 60 | iteration(); 61 | } 62 | 63 | draw(); 64 | } 65 | 66 | private void iteration() { 67 | modifyShape(); 68 | generateNewAuxin(); 69 | List new_veins = generateNewVein(); 70 | killVeins(new_veins); 71 | } 72 | 73 | private void modifyShape() { 74 | radius += RADIUS_INCREASE; 75 | } 76 | 77 | private Coords pickSite() { 78 | double r = rand.nextDouble() * radius; 79 | double theta = rand.nextDouble() * 2 * Math.PI - Math.PI; 80 | double x = Math.sqrt(r) * Math.cos(theta); 81 | double y = Math.sqrt(r) * Math.sin(theta); 82 | return new Coords(x, y); 83 | } 84 | 85 | private int newSites() { 86 | return (int) (NEW_SITE_DENSITY * Math.PI * this.radius * this.radius); 87 | } 88 | 89 | /** 90 | * Generate new auxin nodes 91 | */ 92 | private void generateNewAuxin() { 93 | int n = newSites(); 94 | for (int i = 0; i < n; i++) { 95 | Coords site = pickSite(); 96 | 97 | // Test each auxin node to see if we are too close 98 | boolean failed = false; 99 | for (int id = 0; id < usedAuxinIDs; id++) { 100 | Auxin auxin = auxinSites.get(id); 101 | if (site.distance(auxin.coords) < AUXIN_BIRTH_THRESHOLD) { 102 | failed = true; 103 | break; 104 | } 105 | } 106 | if (failed) { 107 | continue; 108 | } 109 | // Test each vein node to see if we are too close 110 | for (int id = 0; id < usedVeinIDs; id++) { 111 | Vein vein = veinSites.get(id); 112 | if (site.distance(vein.coords) < VEIN_BIRTH_THRESHOLD) { 113 | failed = true; 114 | break; 115 | } 116 | } 117 | if (failed) { 118 | continue; 119 | } 120 | // Create new auxin site 121 | auxinSites.add(new Auxin(usedAuxinIDs++, site)); 122 | } 123 | } 124 | 125 | /** 126 | * Create new veins 127 | * @return 128 | */ 129 | private List generateNewVein() { 130 | List newVeins = new ArrayList(); 131 | int veinLen = veinSites.size(); 132 | for (int veinId = 0; veinId linkedAuxin = new ArrayList(); 137 | for (int auxinId = 0; auxinId filter = new ArrayList(); 169 | for (int i = 0; i < vein.tags.size(); i++) { 170 | int tag = vein.tags.get(i); 171 | if (linkedAuxin.contains(tag)) { 172 | filter.add(tag); 173 | } 174 | } 175 | vein.tags.clear(); 176 | vein.tags.addAll(filter); 177 | 178 | // Calculate the new co-ords 179 | if (linkedAuxin.size() == 0) { 180 | continue; 181 | } 182 | double sum_x = 0; 183 | double sum_y = 0; 184 | int total = linkedAuxin.size(); 185 | for (int j = 0; j < total; j++) { 186 | int auxin_id = linkedAuxin.get(j); 187 | Auxin auxin = auxinSites.get(auxin_id); 188 | double dx = auxin.coords.x - vein.coords.x; 189 | double dy = auxin.coords.y - vein.coords.y; 190 | double scale = NODE_DISTANCE / Math.sqrt(dx * dx + dy * dy); 191 | sum_x += dx * scale; 192 | sum_y += dy * scale; 193 | } 194 | 195 | double avgX = sum_x / total + vein.coords.x; 196 | double avgY = sum_y / total + vein.coords.y; 197 | Coords new_coords = new Coords(avgX, avgY); 198 | // Create the new vein node 199 | newVeins.add(new Vein(usedVeinIDs++, new_coords, veinId, vein.tags)); 200 | vein.tags.clear(); 201 | } 202 | 203 | veinSites.addAll(newVeins); 204 | return newVeins; 205 | } 206 | 207 | private void killVeins(List new_veins) { 208 | int auxin_sites_len = auxinSites.size(); 209 | int vein_sites_len = veinSites.size(); 210 | int new_veins_len = new_veins.size(); 211 | for (int auxin_id = 0; auxin_id < auxin_sites_len; auxin_id++) { 212 | Auxin auxin = auxinSites.get(auxin_id); 213 | // Find overly close auxin nodes 214 | if (!auxin.tag) { 215 | for (int vein_id = 0; vein_id < vein_sites_len; vein_id++) { 216 | Vein vein = veinSites.get(vein_id); 217 | if (auxin.coords.distance(vein.coords) < VEIN_DEATH_THRESHOLD) { 218 | // Kill the node 219 | int newID = usedVeinIDs++; 220 | this.veinSites.add(new Vein(newID, auxin.coords)); 221 | auxin.vein = newID; 222 | auxin.tag = true; 223 | // affected_veins = filter(lambda ID: ID in auxin['LINKED'], this.vein_sites); 224 | List affected_veins = new ArrayList(); 225 | for (int id = 0; id < vein_sites_len; id++) { 226 | if (auxin.linked.contains(id)) { 227 | affected_veins.add(id); 228 | } 229 | } 230 | 231 | for (int new_vein_id = 0; new_vein_id < new_veins_len; new_vein_id++) { 232 | Vein new_vein = veinSites.get(new_vein_id); 233 | if ((affected_veins.size() + new_vein.root.size()) > 0) { 234 | new_vein.tags.add(auxin_id); 235 | } 236 | } 237 | break; 238 | } 239 | } 240 | } 241 | // Process dead auxin nodes 242 | if (auxin.tag) { 243 | for (int vein_id = 0; vein_id < vein_sites_len; vein_id++) { 244 | Vein vein = veinSites.get(vein_id); 245 | if (!vein.tags.contains(auxin_id)) 246 | continue; 247 | if (auxin.coords.distance(vein.coords) < VEIN_DEATH_THRESHOLD) { 248 | vein.tags.remove(auxin_id); 249 | veinSites.get(auxin.vein).root.add(vein_id); 250 | veinSites.get(auxin.vein).tags.addAll(vein.tags); 251 | vein.tags.clear(); 252 | } 253 | } 254 | } 255 | // Clear for next cycle 256 | auxin.linked.clear(); 257 | } 258 | } 259 | 260 | class Coords { 261 | public double x; 262 | public double y; 263 | 264 | public Coords() { 265 | x = y = 0; 266 | } 267 | 268 | public Coords(double x, double y) { 269 | this.x = x; 270 | this.y = y; 271 | } 272 | 273 | public double distance(Coords p) { 274 | double dx = this.x - p.x; 275 | double dy = this.y - p.y; 276 | return Math.sqrt(dx * dx + dy * dy); 277 | } 278 | 279 | public double distanceSquared(Coords p) { 280 | double dx = this.x - p.x; 281 | double dy = this.y - p.y; 282 | return dx * dx + dy * dy; 283 | } 284 | } 285 | 286 | class Auxin { 287 | protected int id; 288 | protected Coords coords; 289 | protected int vein; 290 | protected boolean tag; 291 | protected List linked; 292 | 293 | public Auxin() { 294 | this.id = 0; 295 | this.coords = new Coords(); 296 | this.vein = -1; 297 | this.tag = false; 298 | this.linked = new ArrayList(); 299 | } 300 | 301 | public Auxin(int id, Coords coords) { 302 | this.id = id; 303 | this.coords = coords; 304 | this.vein = -1; 305 | this.tag = false; 306 | this.linked = new ArrayList(); 307 | } 308 | 309 | public Auxin(int id, Coords coords, boolean tag, List linked) { 310 | this.id = id; 311 | this.coords = coords; 312 | this.vein = -1; 313 | this.tag = tag; 314 | this.linked = linked; 315 | } 316 | 317 | public double distanceSquared(Vein v) { 318 | return this.coords.distanceSquared(v.coords); 319 | } 320 | } 321 | 322 | class Vein { 323 | protected int id; 324 | protected Coords coords; 325 | protected List root; 326 | protected List tags; 327 | 328 | public Vein() { 329 | id = 0; 330 | coords = new Coords(0, 0); 331 | root = new ArrayList(); 332 | tags = new ArrayList(); 333 | } 334 | 335 | public Vein(int id) { 336 | this.id = id; 337 | coords = new Coords(0, 0); 338 | root = new ArrayList(); 339 | tags = new ArrayList(); 340 | } 341 | 342 | public Vein(int id, Coords coords) { 343 | this.id = id; 344 | this.coords = coords; 345 | this.root = new ArrayList(); 346 | this.tags = new ArrayList(); 347 | } 348 | 349 | public Vein(int id, Coords coords, int root, List tags) { 350 | this.id = id; 351 | this.coords = coords; 352 | this.root = new ArrayList(); 353 | this.root.add(root); 354 | this.tags = new ArrayList(); 355 | if (tags != null) { 356 | tags.addAll(tags); 357 | } 358 | } 359 | public double distanceSquared(Vein v) { 360 | return this.coords.distanceSquared(v.coords); 361 | } 362 | } 363 | 364 | /** 365 | * Draw the city 366 | */ 367 | private void draw() { 368 | int centreX = (int) (width * 0.5f + 0.5f); 369 | int centreY = (int) (height * 0.5f + 0.5f); 370 | 371 | // find the farest coords to calculate scale 372 | Coords centre = new Coords(0, 0); 373 | double maxLength = 1; 374 | for (Vein vein : veinSites) { 375 | Coords coords = vein.coords; 376 | double dis = coords.distance(centre); 377 | if (dis > maxLength) { 378 | maxLength = dis; 379 | } 380 | } 381 | double scale = Math.min(centreX-2, centreY-2) / maxLength; 382 | 383 | map.fill(Floor); 384 | map.buildBoundary(Stone); 385 | 386 | for (Vein vein : veinSites) { 387 | Coords coords = vein.coords; 388 | int x = (int) (centreX + coords.x * scale); 389 | int y = (int) (centreY + coords.y * scale); 390 | for (int root_id : vein.root) { 391 | Coords root_coords = veinSites.get(root_id).coords; 392 | int root_x = (int) (centreX + root_coords.x * scale); 393 | int root_y = (int) (centreY + root_coords.y * scale); 394 | drawLine(root_x, root_y, x, y, Corridor); 395 | } 396 | } 397 | } 398 | 399 | private void drawLine(int x1, int y1, int x2, int y2, int tile) { 400 | int a = Math.abs(x2 - x1); 401 | int b = Math.abs(y2 - y1); 402 | 403 | if (a >= b) { 404 | int xStep = 1; 405 | if (x2 < x1) xStep = -1; 406 | float yStep = (float) (y2- y1) / a; 407 | // use xStep 408 | int x = x1; 409 | int y = y1; 410 | for (int i = 0; i <= a; i++) { 411 | x = x1 + xStep * i; 412 | y = y1 +(int)(yStep * i); 413 | map.set(x, y, tile); 414 | } 415 | 416 | } else { 417 | // use yStep 418 | int yStep = 1; 419 | if (y2 < y1) yStep = -1; 420 | float xStep = (float) (x2 - x1) / b; 421 | // use xStep 422 | int x = x1; 423 | int y = y1; 424 | for (int i = 0; i <= b; i++) { 425 | x = x1 + (int) (xStep * i); 426 | y = y1 + yStep * i; 427 | map.set(x, y, tile); 428 | } 429 | } 430 | } 431 | 432 | public static void main(String[] args) { 433 | CityLeafVenation leaf = new CityLeafVenation(40, 40); 434 | leaf.initialze(); 435 | leaf.create(); 436 | leaf.getMap().printMapChars(); 437 | } 438 | } 439 | -------------------------------------------------------------------------------- /src/main/java/net/jmecn/map/creator/DungeonHauberk.java: -------------------------------------------------------------------------------- 1 | package net.jmecn.map.creator; 2 | 3 | import static net.jmecn.map.Tile.*; 4 | 5 | import java.util.ArrayList; 6 | import java.util.LinkedList; 7 | import java.util.List; 8 | 9 | import net.jmecn.map.Point; 10 | import net.jmecn.map.Rect; 11 | 12 | /// The random dungeon generator. 13 | /// 14 | /// Starting with a stage of solid walls, it works like so: 15 | /// 16 | /// 1. Place a number of randomly sized and positioned rooms. If a room 17 | /// overlaps an existing room, it is discarded. Any remaining rooms are 18 | /// carved out. 19 | /// 2. Any remaining solid areas are filled in with mazes. The maze generator 20 | /// will grow and fill in even odd-shaped areas, but will not touch any 21 | /// rooms. 22 | /// 3. The result of the previous two steps is a series of unconnected rooms 23 | /// and mazes. We walk the stage and find every tile that can be a 24 | /// "connector". This is a solid tile that is adjacent to two unconnected 25 | /// regions. 26 | /// 4. We randomly choose connectors and open them or place a door there until 27 | /// all of the unconnected regions have been joined. There is also a slight 28 | /// chance to carve a connector between two already-joined regions, so that 29 | /// the dungeon isn't single connected. 30 | /// 5. The mazes will have a lot of dead ends. Finally, we remove those by 31 | /// repeatedly filling in any open tile that's closed on three sides. When 32 | /// this is done, every corridor in a maze actually leads somewhere. 33 | /// 34 | /// The end result of this is a multiply-connected dungeon with rooms and lots 35 | /// of winding corridors. 36 | /// 37 | /// @author https://github.com/munificent 38 | /** 39 | * 40 | * project: https://github.com/munificent/hauberk 41 | * article: http://journal.stuffwithstuff.com/2014/12/21/rooms-and-mazes/ demo 42 | * for this article: https://github.com/munificent/rooms-and-mazes 43 | * source: 44 | * https://github.com/munificent/hauberk/blob/master/lib/src/content/dungeon.dart 45 | * 46 | * 47 | * Another algorithm used in TinyKeep is similar to this one. 48 | * 49 | * http://tinykeep.com/dungen/ 50 | * 51 | * https://www.reddit.com/r/gamedev/comments/1dlwc4/procedural_dungeon_generation_algorithm_explained/ 52 | * 53 | * https://github.com/adonaac/blog/issues/7 54 | * 55 | * http://www.gamasutra.com/blogs/AAdonaac/20150903/252889/Procedural_Dungeon_Generation_Algorithm.php 56 | * 57 | * Delaunay Triangulation + Graph 58 | * 59 | * Read more: 60 | * 61 | * https://en.wikipedia.org/wiki/Delaunay_triangulation 62 | * http://paulbourke.net/papers/triangulate/ 63 | * http://www-cs-students.stanford.edu/~amitp/game-programming/polygon-map-generation/ 64 | * https://en.wikipedia.org/wiki/Delaunay_tessellation_field_estimator 65 | * 66 | * 67 | * @author yanmaoyuan 68 | * 69 | */ 70 | public class DungeonHauberk extends MapCreator { 71 | 72 | static Point[] Direction = { new Point(1, 0), new Point(0, 1), new Point(-1, 0), new Point(0, -1) }; 73 | int numRoomTries = 200; 74 | 75 | /// The inverse chance of adding a connector between two regions that have 76 | /// already been joined. Increasing this leads to more loosely connected 77 | /// dungeons. 78 | int extraConnectorChance = 20;// =>20; 79 | 80 | /// Increasing this allows rooms to be larger. 81 | int roomExtraSize;// =>0; 82 | 83 | int windingPercent = 45;// =>0; 84 | 85 | private List rooms = new ArrayList(); 86 | 87 | /// For each open position in the dungeon, the index of the connected region 88 | /// that that position is a part of. 89 | private int[][] regions; 90 | 91 | /// The index of the current region being carved. 92 | private int currentRegion = -1; 93 | 94 | public DungeonHauberk(int width, int height) { 95 | super("creator.dungeon.hauberk", width, height); 96 | } 97 | 98 | @Override 99 | public void initialze() { 100 | if (width % 2 == 0 || height % 2 == 0) { 101 | // throw new RuntimeException("The map must be odd-sized."); 102 | return; 103 | } 104 | 105 | // init map 106 | map.fill(Wall); 107 | 108 | // init regions 109 | currentRegion = Unused; 110 | regions = new int[height][width]; 111 | for (int y = 0; y < height; y++) { 112 | for (int x = 0; x < width; x++) { 113 | regions[y][x] = Unused; 114 | } 115 | } 116 | 117 | // clear rooms 118 | rooms.clear(); 119 | } 120 | 121 | @Override 122 | public void create() { 123 | if (width % 2 == 0 || height % 2 == 0) { 124 | // throw new RuntimeException("The map must be odd-sized."); 125 | return; 126 | } 127 | 128 | addRooms(); 129 | 130 | addMazes(); 131 | 132 | connectRegions(); 133 | 134 | removeDeadEnds(); 135 | 136 | } 137 | 138 | /** 139 | * Places rooms ignoring the existing maze corridors. 140 | */ 141 | private void addRooms() { 142 | for (int i = 0; i < numRoomTries; i++) { 143 | // Pick a random room size. The funny math here does two things: 144 | // - It makes sure rooms are odd-sized to line up with maze. 145 | // - It avoids creating rooms that are too rectangular: too tall and 146 | // narrow or too wide and flat. 147 | // TODO: This isn't very flexible or tunable. Do something better 148 | // here. 149 | int size = nextInt(1, 3 + roomExtraSize) * 2 + 1; 150 | int rectangularity = nextInt(0, 1 + size / 2) * 2; 151 | int width = size; 152 | int height = size; 153 | if (nextInt(2) == 0) { 154 | width += rectangularity; 155 | } else { 156 | height += rectangularity; 157 | } 158 | 159 | int x = nextInt((this.width - width) / 2) * 2 + 1; 160 | int y = nextInt((this.height - height) / 2) * 2 + 1; 161 | 162 | Rect room = new Rect(x, y, width, height); 163 | 164 | boolean overlaps = false; 165 | for (Rect other : rooms) { 166 | if (room.overlap(other)) { 167 | overlaps = true; 168 | break; 169 | } 170 | } 171 | 172 | if (overlaps) { 173 | continue; 174 | } 175 | 176 | rooms.add(room); 177 | 178 | startRegion(); 179 | for (int yy = y; yy < y + height; yy++) { 180 | for (int xx = x; xx < x + width; xx++) { 181 | carve(xx, yy); 182 | } 183 | } 184 | } 185 | } 186 | 187 | /** 188 | * Fill in all of the empty space with mazes. 189 | */ 190 | private void addMazes() { 191 | // Fill in all of the empty space with mazes. 192 | for (int y = 1; y < height; y += 2) { 193 | for (int x = 1; x < width; x += 2) { 194 | Point pos = new Point(x, y); 195 | if (map.get(x, y) != Wall) 196 | continue; 197 | growMaze(pos); 198 | } 199 | } 200 | } 201 | 202 | /** 203 | * Implementation of the "growing tree" algorithm from here: 204 | * http://www.astrolog.org/labyrnth/algrithm.htm. 205 | * 206 | * This is a general algorithm, capable of creating Mazes of different 207 | * textures. It requires storage up to the size of the Maze. Each time you 208 | * carve a cell, add that cell to a list. Proceed by picking a cell from the 209 | * list, and carving into an unmade cell next to it. If there are no unmade 210 | * cells next to the current cell, remove the current cell from the list. 211 | * The Maze is done when the list becomes empty. The interesting part that 212 | * allows many possible textures is how you pick a cell from the list. For 213 | * example, if you always pick the most recent cell added to it, this 214 | * algorithm turns into the recursive backtracker. If you always pick cells 215 | * at random, this will behave similarly but not exactly to Prim's 216 | * algorithm. If you always pick the oldest cells added to the list, this 217 | * will create Mazes with about as low a "river" factor as possible, even 218 | * lower than Prim's algorithm. If you usually pick the most recent cell, 219 | * but occasionally pick a random cell, the Maze will have a high "river" 220 | * factor but a short direct solution. If you randomly pick among the most 221 | * recent cells, the Maze will have a low "river" factor but a long windy 222 | * solution. 223 | * 224 | * @param start 225 | */ 226 | private void growMaze(Point start) { 227 | LinkedList cells = new LinkedList(); 228 | Point lastDir = null; 229 | 230 | startRegion(); 231 | carve(start.x, start.y); 232 | 233 | cells.add(start); 234 | while (!cells.isEmpty()) { 235 | Point cell = cells.getLast(); 236 | 237 | // See which adjacent cells are open. 238 | ArrayList unmadeCells = new ArrayList(); 239 | for (Point dir : Direction) { 240 | if (canCarve(cell, dir)) 241 | unmadeCells.add(dir); 242 | } 243 | 244 | if (!unmadeCells.isEmpty()) { 245 | // Based on how "windy" passages are, try to prefer carving in 246 | // the same direction. 247 | Point dir; 248 | if (unmadeCells.contains(lastDir) && nextInt(100) > windingPercent) { 249 | dir = lastDir; 250 | } else { 251 | int index = nextInt(unmadeCells.size()); 252 | dir = unmadeCells.get(index); 253 | } 254 | 255 | // carve(cell + dir) 256 | carve(cell.x + dir.x, cell.y + dir.y); 257 | // carve(cell + dir*2) 258 | carve(cell.x + dir.x * 2, cell.y + dir.y * 2); 259 | 260 | cells.add(new Point(cell.x + dir.x * 2, cell.y + dir.y * 2)); 261 | lastDir = dir; 262 | } else { 263 | // No adjacent uncarved cells. 264 | cells.removeLast(); 265 | 266 | // This path has ended. 267 | lastDir = null; 268 | } 269 | } 270 | } 271 | 272 | class Graph { 273 | Edge[][] matrix; 274 | Graph(int vCnt) { 275 | matrix = new Edge[vCnt][vCnt]; 276 | } 277 | 278 | Edge get(int x, int y) { 279 | return matrix[x][y]; 280 | } 281 | 282 | void union(int x, int y, Point p) { 283 | if (matrix[x][y] == null) { 284 | Edge e = new Edge(); 285 | matrix[x][y] = matrix[y][x] = e; 286 | 287 | if (x > y) { 288 | e.a = y; 289 | e.b = x; 290 | } else { 291 | e.a = x; 292 | e.b = y; 293 | } 294 | } 295 | matrix[x][y].add(p); 296 | } 297 | 298 | } 299 | 300 | class Edge { 301 | int a, b; 302 | ArrayList points; 303 | 304 | Edge() { 305 | points = new ArrayList(); 306 | } 307 | 308 | void add(Point p) { 309 | points.add(p); 310 | } 311 | 312 | Point rand() { 313 | int index = nextInt(points.size()); 314 | return points.get(index); 315 | } 316 | 317 | } 318 | 319 | private void connectRegions() { 320 | 321 | // Find all of the tiles that can connect two (or more) regions. 322 | Graph graph = new Graph(currentRegion+1); 323 | for (int y = 1; y < height - 1; y++) { 324 | for (int x = 1; x < width - 1; x++) { 325 | // Can't already be part of a region. 326 | if (map.get(x, y) != Wall) 327 | continue; 328 | 329 | int a = -1; 330 | int b = -1; 331 | for (Point dir : Direction) { 332 | int region = regions[y+dir.y][x+dir.x]; 333 | if (region != Unused) { 334 | 335 | if (a == -1) { 336 | a = region; 337 | } else { 338 | if (b == -1 && a != region) { 339 | b = region; 340 | } 341 | } 342 | } 343 | } 344 | 345 | if (a != -1 && b != -1) { 346 | graph.union(a, b, new Point(x, y)); 347 | } 348 | } 349 | } 350 | 351 | // Keep track of which regions have been merged. This maps an original 352 | // region index to the one it has been merged to. 353 | List openRegions = new ArrayList(); 354 | for (int i = 0; i <= currentRegion; i++) { 355 | openRegions.add(i); 356 | } 357 | 358 | int len = openRegions.size(); 359 | int root = openRegions.get(nextInt(len)); 360 | openRegions.remove(new Integer(root)); 361 | 362 | List tree = new ArrayList(); 363 | tree.add(root); 364 | 365 | // Keep connecting regions until we're down to one. 366 | while (openRegions.size() > 0) { 367 | 368 | len = tree.size(); 369 | root = tree.get(nextInt(len)); 370 | 371 | List edges = new ArrayList(); 372 | len = openRegions.size(); 373 | for(int i=0; i 0) 388 | // edges.add(e); 389 | // } 390 | // } 391 | 392 | if (edges.size() == 0) { 393 | continue; 394 | } 395 | else { 396 | len = edges.size(); 397 | Edge e = edges.get(nextInt(len)); 398 | 399 | int b = e.b; 400 | if (b == root) b = e.a; 401 | 402 | 403 | if (openRegions.remove(new Integer(b))) { 404 | tree.add(b); 405 | // } else { 406 | // if (nextInt(extraConnectorChance) != 0) { 407 | // continue; 408 | // } 409 | } 410 | 411 | List points = e.points; 412 | // Carve the connection. 413 | len = points.size(); 414 | Point p = points.get(nextInt(len)); 415 | addJunction(p); 416 | points.clear(); 417 | } 418 | 419 | } 420 | } 421 | 422 | private void addJunction(Point pos) { 423 | map.set(pos.x, pos.y, Door); 424 | } 425 | 426 | private void removeDeadEnds() { 427 | boolean done = false; 428 | 429 | while (!done) { 430 | done = true; 431 | 432 | for (int y = 1; y < height - 1; y++) { 433 | for (int x = 1; x < width - 1; x++) { 434 | if (map.get(x, y) == Wall) 435 | continue; 436 | 437 | // If it only has one exit, it's a dead end. 438 | int exits = 0; 439 | for (Point dir : Direction) { 440 | if (map.get(x + dir.x, y + dir.y) != Wall) 441 | exits++; 442 | } 443 | 444 | if (exits != 1) 445 | continue; 446 | 447 | done = false; 448 | map.set(x, y, Wall); 449 | } 450 | } 451 | } 452 | } 453 | 454 | /** 455 | * Gets whether or not an opening can be carved from the given starting 456 | * [Cell] at [pos] to the adjacent Cell facing [direction]. Returns `true` 457 | * if the starting Cell is in bounds and the destination Cell is filled (or 458 | * out of bounds). 459 | * 460 | * @param pos 461 | * @param direction 462 | * @return 463 | */ 464 | private boolean canCarve(Point pos, Point direction) { 465 | // Must end in bounds. 466 | int x = pos.x + direction.x * 3; 467 | int y = pos.y + direction.y * 3; 468 | if (!map.contains(x, y)) 469 | return false; 470 | 471 | // Destination must not be open. 472 | x = pos.x + direction.x * 2; 473 | y = pos.y + direction.y * 2; 474 | return map.get(x, y) == Wall; 475 | } 476 | 477 | private void startRegion() { 478 | currentRegion++; 479 | } 480 | 481 | private void carve(int x, int y) { 482 | map.set(x, y, Floor); 483 | regions[y][x] = currentRegion; 484 | } 485 | 486 | } 487 | -------------------------------------------------------------------------------- /src/main/java/net/jmecn/map/creator/DungeonNickgravelyn.java: -------------------------------------------------------------------------------- 1 | package net.jmecn.map.creator; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | import net.jmecn.map.Point; 7 | import net.jmecn.map.Rect; 8 | 9 | import static net.jmecn.map.Direction.*; 10 | import static net.jmecn.map.Tile.*; 11 | 12 | /** 13 | * A dungeon algorithm in JavaScript 14 | * 15 | * http://nickgravelyn.github.io/dungeon/ 16 | * 17 | * @author yanmaoyuan 18 | * 19 | */ 20 | public class DungeonNickgravelyn extends MapCreator { 21 | 22 | private int minRoomSize; 23 | private int maxRoomSize; 24 | private int maxNumRooms; 25 | private int maxRoomArea; 26 | private boolean addStairsUp; 27 | private boolean addStairsDown; 28 | private List rooms; 29 | private List[][] roomGrid; 30 | 31 | @SuppressWarnings("unchecked") 32 | public DungeonNickgravelyn(int width, int height) { 33 | super("creator.dungeon.nickgravelyn", width, height); 34 | 35 | this.minRoomSize = 5; 36 | this.maxRoomSize = 15; 37 | this.maxNumRooms = 50; 38 | this.maxRoomArea = 150; 39 | 40 | this.addStairsUp = true; 41 | this.addStairsDown = true; 42 | 43 | this.rooms = new ArrayList(); 44 | this.roomGrid = new ArrayList[height][width]; 45 | } 46 | 47 | @Override 48 | public void initialze() { 49 | map.fill(Unused); 50 | } 51 | 52 | @SuppressWarnings("unchecked") 53 | @Override 54 | public void create() { 55 | // clear 56 | this.rooms = new ArrayList(); 57 | this.roomGrid = new ArrayList[height][width]; 58 | for(int y=0; y(); 61 | } 62 | } 63 | 64 | // seed the map with a starting randomly sized room in the center of the 65 | // map 66 | Room room = this.createRandomRoom(); 67 | room.x = (int) (Math.floor(width / 2) - Math.floor(room.width / 2)); 68 | room.y = (int) (Math.floor(height / 2) - Math.floor(room.height / 2)); 69 | this.addRoom(room); 70 | 71 | // continue generating rooms until we hit our cap or have hit our 72 | // maximum iterations (generally 73 | // due to not being able to fit any more rooms in the map) 74 | int iter = maxNumRooms * 5; 75 | while ((maxNumRooms <= 0 || rooms.size() < maxNumRooms) && iter-- > 0) { 76 | generateRoom(); 77 | } 78 | 79 | // now we want to randomly add doors between some of the rooms and other 80 | // rooms they touch 81 | for (int i = 0; i < this.rooms.size(); i++) { 82 | // find all rooms that we could connect with this one 83 | List targets = getPotentiallyTouchingRooms(rooms.get(i)); 84 | for (int j = 0; j < targets.size(); j++) { 85 | // make sure the rooms aren't already connected with a door 86 | if (!areRoomsConnected(rooms.get(i), targets.get(j))) { 87 | // 20% chance we add a door connecting the rooms 88 | if (Math.random() < 0.2) { 89 | addDoor(findNewDoorLocation(rooms.get(i), targets.get(j))); 90 | } 91 | } 92 | } 93 | } 94 | 95 | // add stairs if desired 96 | if (addStairsDown) { 97 | addStairs(DownStairs); 98 | } 99 | if (addStairsUp) { 100 | addStairs(UpStairs); 101 | } 102 | 103 | 104 | for(int i=0; i= x2 + w2 - 1 || y1 + h1 <= y2 + 1 || y1 >= y2 + h2 - 1) { 130 | return false; 131 | } 132 | 133 | return true; 134 | } 135 | 136 | private boolean canFitRoom(Room room) { 137 | // make sure the room fits inside the dungeon 138 | if (room.x < 0 || room.x + room.width > this.width - 1) { 139 | return false; 140 | } 141 | if (room.y < 0 || room.y + room.height > this.height - 1) { 142 | return false; 143 | } 144 | 145 | // make sure this room doesn't intersect any existing rooms 146 | for (int i = 0; i < this.rooms.size(); i++) { 147 | Room r = this.rooms.get(i); 148 | if (this.roomIntersect(room, r)) { 149 | return false; 150 | } 151 | } 152 | 153 | return true; 154 | } 155 | 156 | private List touchingRooms; 157 | private List getPotentiallyTouchingRooms(Room room) { 158 | touchingRooms = new ArrayList(); 159 | 160 | // iterate the north and south walls, looking for other rooms in those 161 | // tile locations 162 | for (int x = room.x + 1; x < room.x + room.width - 1; x++) { 163 | checkRoomList(x, room.y, room); 164 | checkRoomList(x, room.y + room.height - 1, room); 165 | } 166 | 167 | // iterate the west and east walls, looking for other rooms in those 168 | // tile locations 169 | for (int y = room.y + 1; y < room.y + room.height - 1; y++) { 170 | checkRoomList(room.x, y, room); 171 | checkRoomList(room.x + room.width - 1, y, room); 172 | } 173 | 174 | return touchingRooms; 175 | } 176 | 177 | // function that checks the list of rooms at a point in our grid for any 178 | // potential touching rooms 179 | private void checkRoomList(int x, int y, Room room) { 180 | List r = roomGrid[y][x]; 181 | for (int i = 0; i < r.size(); i++) { 182 | // make sure this room isn't the one we're searching around and that 183 | // it isn't already in the list 184 | if (r.get(i) != room && touchingRooms.indexOf(r.get(i)) < 0) { 185 | // make sure this isn't a corner of the room (doors can't go 186 | // into corners) 187 | int lx = x - r.get(i).x; 188 | int ly = y - r.get(i).y; 189 | if ((lx > 0 && lx < r.get(i).width - 1) || (ly > 0 && ly < r.get(i).height - 1)) { 190 | touchingRooms.add(r.get(i)); 191 | } 192 | } 193 | } 194 | }; 195 | 196 | private Point findNewDoorLocation(Room room1, Room room2) { 197 | Point doorPos = new Point(-1, -1); 198 | 199 | // figure out the direction from room1 to room2 200 | int dir = UnknownDir; 201 | 202 | // north 203 | if (room1.y == room2.y - room1.height + 1) { 204 | dir = North; 205 | } 206 | // west 207 | else if (room1.x == room2.x - room1.width + 1) { 208 | dir = West; 209 | } 210 | // east 211 | else if (room1.x == room2.x + room2.width - 1) { 212 | dir = East; 213 | } 214 | // south 215 | else if (room1.y == room2.y + room2.height - 1) { 216 | dir = South; 217 | } 218 | 219 | // use the direction to find an appropriate door location 220 | switch (dir) { 221 | // north 222 | case North: 223 | doorPos.x = nextInt(Math.max(room2.x, room1.x) + 1, 224 | Math.min(room2.x + room2.width, room1.x + room1.width) - 1); 225 | doorPos.y = room2.y; 226 | break; 227 | // west 228 | case West: 229 | doorPos.x = room2.x; 230 | doorPos.y = nextInt(Math.max(room2.y, room1.y) + 1, 231 | Math.min(room2.y + room2.height, room1.y + room1.height) - 1); 232 | break; 233 | // east 234 | case East: 235 | doorPos.x = room1.x; 236 | doorPos.y = nextInt(Math.max(room2.y, room1.y) + 1, 237 | Math.min(room2.y + room2.height, room1.y + room1.height) - 1); 238 | break; 239 | // south 240 | case South: 241 | doorPos.x = nextInt(Math.max(room2.x, room1.x) + 1, 242 | Math.min(room2.x + room2.width, room1.x + room1.width) - 1); 243 | doorPos.y = room1.y; 244 | break; 245 | } 246 | 247 | return doorPos; 248 | } 249 | 250 | private int random(List list) { 251 | return nextInt(list.size()); 252 | } 253 | 254 | private Result findRoomAttachment(Room room) { 255 | // pick a room, any room 256 | Room r = rooms.get(random(rooms)); 257 | 258 | Point pos = new Point(); 259 | 260 | // randomly position this room on one of the sides of the random room 261 | switch (nextInt(0, 4)) { 262 | // north 263 | case North: 264 | pos.x = nextInt(r.x - room.width + 3, r.x + r.width - 2); 265 | pos.y = r.y - room.height + 1; 266 | break; 267 | // west 268 | case West: 269 | pos.x = r.x - room.width + 1; 270 | pos.y = nextInt(r.y - room.height + 3, r.y + r.height - 2); 271 | break; 272 | // east 273 | case East: 274 | pos.x = r.x + r.width - 1; 275 | pos.y = nextInt(r.y - room.height + 3, r.y + r.height - 2); 276 | break; 277 | // south 278 | case South: 279 | pos.x = nextInt(r.x - room.width + 3, r.x + r.width - 2); 280 | pos.y = r.y + r.height - 1; 281 | break; 282 | } 283 | 284 | Result result = new Result(); 285 | result.position = pos; 286 | result.target = r; 287 | // return the position for this new room and the target room 288 | return result; 289 | } 290 | class Result { 291 | Room target; 292 | Point position; 293 | } 294 | 295 | boolean addRoom(Room room) { 296 | // if the room won't fit, we don't add it 297 | if (!canFitRoom(room)) { 298 | return false; 299 | } 300 | 301 | // add it to our main rooms list 302 | this.rooms.add(room); 303 | 304 | // update all tiles to indicate that this room is sitting on them. this 305 | // grid is used 306 | // when placing doors so all rooms in a space can be updated at the same 307 | // time. 308 | for (int y = room.y; y < room.y + room.height; y++) { 309 | for (int x = room.x; x < room.x + room.width; x++) { 310 | List list = this.roomGrid[y][x]; 311 | list.add(room); 312 | this.roomGrid[y][x] = list; 313 | } 314 | } 315 | 316 | return true; 317 | }; 318 | 319 | void addDoor(Point doorPos) { 320 | // get all the rooms at the location of the door 321 | List rooms = this.roomGrid[doorPos.y][doorPos.x]; 322 | for (int i = 0; i < rooms.size(); i++) { 323 | Room r = rooms.get(i); 324 | 325 | // convert the door position from world space to room space 326 | int x = doorPos.x - r.x; 327 | int y = doorPos.y - r.y; 328 | 329 | // set the tile to be a door 330 | r.tiles[y][x] = Door; 331 | } 332 | }; 333 | 334 | Room createRandomRoom() { 335 | int width = 0; 336 | int height = 0; 337 | int area = 0; 338 | 339 | // find an acceptable width and height using our min/max sizes while 340 | // keeping under 341 | // the maximum area 342 | do { 343 | width = nextInt(this.minRoomSize, this.maxRoomSize); 344 | height = nextInt(this.minRoomSize, this.maxRoomSize); 345 | area = width * height; 346 | } while (this.maxRoomArea > 0 && area > this.maxRoomArea); 347 | 348 | // create the room 349 | return new Room(width, height); 350 | } 351 | 352 | private void generateRoom() { 353 | // create the randomly sized room 354 | Room room = createRandomRoom(); 355 | 356 | // only allow 150 tries at placing the room 357 | int iter = 150; 358 | while (iter-- > 0) { 359 | // attempt to find another room to attach this one to 360 | Result result = this.findRoomAttachment(room); 361 | 362 | // update the position of this room 363 | room.x = result.position.x; 364 | room.y = result.position.y; 365 | 366 | // try to add it. if successful, add the door between the rooms and 367 | // break the loop 368 | if (addRoom(room)) { 369 | addDoor(findNewDoorLocation(room, result.target)); 370 | break; 371 | } 372 | } 373 | } 374 | 375 | private void addStairs(int type) { 376 | Room room = null; 377 | 378 | // keep picking random rooms until we find one that has only one door and doesn't already have stairs in it 379 | do { room = rooms.get(random(rooms)); } 380 | while (room.getDoorLocations().size() > 1 || room.hasStairs()); 381 | 382 | // build a list of all locations in the room that qualify for stairs 383 | List candidates = new ArrayList(); 384 | for (int y = 1; y < room.height - 2; y++) { 385 | for (int x = 1; x < room.width - 2; x++) { 386 | // only put stairs on the floor 387 | if (room.tiles[y][x] != Floor) { continue; } 388 | 389 | // make sure this floor isn't right next to a door 390 | if (room.tiles[y - 1][x] == Door || 391 | room.tiles[y + 1][x] == Door || 392 | room.tiles[y][x - 1] == Door || 393 | room.tiles[y][x + 1] == Door) { continue; } 394 | 395 | // add it to the candidate list 396 | candidates.add(new Point(x, y)); 397 | } 398 | } 399 | 400 | // pick a random candidate location and make it the stairs 401 | Point loc = candidates.get(random(candidates)); 402 | room.tiles[loc.y][loc.x] = type; 403 | }; 404 | 405 | private boolean areRoomsConnected(Room room1, Room room2) { 406 | // iterate the doors in room1 and see if any are also a door in room2 407 | List doors = room1.getDoorLocations(); 408 | for (int i = 0; i < doors.size(); i++) { 409 | Point d = doors.get(i); 410 | 411 | // move the door into "world space" using room1's position 412 | d.x += room1.x; 413 | d.y += room1.y; 414 | 415 | // move the door into room2 space by subtracting room2's position 416 | d.x -= room2.x; 417 | d.y -= room2.y; 418 | 419 | // make sure the position is valid for room2's tiles array 420 | if (d.x < 0 || d.x > room2.width - 1 || d.y < 0 || d.y > room2.height - 1) { 421 | continue; 422 | } 423 | 424 | // see if the tile is a door; if so this is a door from room1 to 425 | // room2 so the rooms are connected 426 | if (room2.tiles[d.y][d.x] == Door) { 427 | return true; 428 | } 429 | } 430 | 431 | return false; 432 | } 433 | 434 | private static class Room extends Rect { 435 | int[][] tiles; 436 | 437 | Room(int width, int height) { 438 | super(0, 0, width, height); 439 | tiles = new int[height][width]; 440 | // surround the room with walls, and fill the rest with floors. 441 | for (int y = 0; y < height; y++) { 442 | for (int x = 0; x < width; x++) { 443 | if (y == 0 || y == height - 1 || x == 0 || x == width - 1) { 444 | tiles[y][x] = Wall; 445 | } else { 446 | tiles[y][x] = Floor; 447 | } 448 | } 449 | } 450 | } 451 | 452 | /** 453 | * find out if we have any stair tiles in the room 454 | * 455 | * @return 456 | */ 457 | boolean hasStairs() { 458 | for (int y = 0; y < height; y++) { 459 | for (int x = 0; x < width; x++) { 460 | if (this.tiles[y][x] == DownStairs || this.tiles[y][x] == UpStairs) { 461 | return true; 462 | } 463 | } 464 | } 465 | return false; 466 | }; 467 | 468 | List getDoorLocations() { 469 | List doors = new ArrayList(); 470 | 471 | // find all the doors and add their positions to the list 472 | for (int y = 0; y < height; y++) { 473 | for (int x = 0; x < width; x++) { 474 | if (this.tiles[y][x] == Door) { 475 | doors.add(new Point(x, y)); 476 | } 477 | } 478 | } 479 | 480 | return doors; 481 | } 482 | } 483 | 484 | } 485 | -------------------------------------------------------------------------------- /src/main/java/net/jmecn/map/creator/DungeonTyrant.java: -------------------------------------------------------------------------------- 1 | package net.jmecn.map.creator; 2 | 3 | import static net.jmecn.map.Tile.*; 4 | 5 | import java.util.logging.Level; 6 | import java.util.logging.Logger; 7 | 8 | import static net.jmecn.map.Direction.*; 9 | 10 | /** 11 | * Tyrant's dungeon generate algorithm 12 | * 13 | * https://sourceforge.net/projects/tyrant/ 14 | * 15 | * @author yanmaoyuan 16 | * 17 | */ 18 | public class DungeonTyrant extends MapCreator { 19 | 20 | static Logger logger = Logger.getLogger(DungeonTyrant.class.getName()); 21 | 22 | final static int ChanceRoom = 50; // corridorChance = 100 - roomChance 23 | static final int minRoomSize = 3; 24 | static final int maxRoomSize = 6; 25 | 26 | static final int minCorridorLength = 3; 27 | static final int maxCorridorLength = 6; 28 | 29 | private int maxFeatures = 100; 30 | 31 | public DungeonTyrant(int width, int height) { 32 | super("creator.dungeon.tyrant", width, height); 33 | } 34 | 35 | @Override 36 | public void initialze() { 37 | map.fill(Unused); 38 | } 39 | 40 | @Override 41 | public void create() { 42 | // Make one room in the middle to start things off. 43 | int randomDir = nextInt(4); 44 | makeRoom(width / 2, height / 2, 16, 12, randomDir); 45 | 46 | for (int features = 1; features != maxFeatures; ++features) { 47 | if (!makeFeature()) { 48 | break; 49 | } 50 | } 51 | 52 | if (!makeStairs(UpStairs)) 53 | logger.log(Level.WARNING, "Unable to place up stairs."); 54 | 55 | if (!makeStairs(DownStairs)) 56 | logger.log(Level.WARNING, "Unable to place down stairs."); 57 | } 58 | 59 | public void setMaxFeatures(int maxFeatures) { 60 | this.maxFeatures = maxFeatures; 61 | } 62 | 63 | boolean makeCorridor(int x, int y, int maxLength, int direction) { 64 | assert (x >= 0 && x < width); 65 | assert (y >= 0 && y < height); 66 | 67 | assert (maxLength > 0 && maxLength <= Math.max(width, height)); 68 | 69 | int length = nextInt(2, maxLength); 70 | 71 | int xStart = x; 72 | int yStart = y; 73 | 74 | int xEnd = x; 75 | int yEnd = y; 76 | 77 | if (direction == North) 78 | yStart = y - length; 79 | else if (direction == East) 80 | xEnd = x + length; 81 | else if (direction == South) 82 | yEnd = y + length; 83 | else if (direction == West) 84 | xStart = x - length; 85 | 86 | if (!map.isXInBounds(xStart) || !map.isXInBounds(xEnd) || !map.isYInBounds(yStart) || !map.isYInBounds(yEnd)) 87 | return false; 88 | 89 | if (!map.isAreaUnused(xStart, yStart, xEnd, yEnd)) 90 | return false; 91 | 92 | map.setCells(xStart, yStart, xEnd, yEnd, Corridor); 93 | 94 | return true; 95 | } 96 | 97 | boolean makeRoom(int x, int y, int xMaxLength, int yMaxLength, int direction) { 98 | // Minimum room size of 4x4 tiles (2x2 for walking on, the rest is 99 | // walls) 100 | int xLength = nextInt(4, xMaxLength); 101 | int yLength = nextInt(4, yMaxLength); 102 | 103 | int xStart = x; 104 | int yStart = y; 105 | 106 | int xEnd = x; 107 | int yEnd = y; 108 | 109 | if (direction == North) { 110 | yStart = y - yLength; 111 | xStart = x - xLength / 2; 112 | xEnd = x + (xLength + 1) / 2; 113 | } else if (direction == East) { 114 | yStart = y - yLength / 2; 115 | yEnd = y + (yLength + 1) / 2; 116 | xEnd = x + xLength; 117 | } else if (direction == South) { 118 | yEnd = y + yLength; 119 | xStart = x - xLength / 2; 120 | xEnd = x + (xLength + 1) / 2; 121 | } else if (direction == West) { 122 | yStart = y - yLength / 2; 123 | yEnd = y + (yLength + 1) / 2; 124 | xStart = x - xLength; 125 | } 126 | 127 | if (!map.isXInBounds(xStart) || !map.isXInBounds(xEnd) || !map.isYInBounds(yStart) || !map.isYInBounds(yEnd)) 128 | return false; 129 | 130 | if (!map.isAreaUnused(xStart, yStart, xEnd, yEnd)) 131 | return false; 132 | 133 | map.setCells(xStart, yStart, xEnd, yEnd, Wall); 134 | map.setCells(xStart + 1, yStart + 1, xEnd - 1, yEnd - 1, Floor); 135 | 136 | return true; 137 | } 138 | 139 | boolean makeFeature(int x, int y, int xmod, int ymod, int direction) { 140 | // Choose what to build 141 | int chance = nextInt(0, 100); 142 | 143 | if (chance <= ChanceRoom) { 144 | if (makeRoom(x + xmod, y + ymod, 8, 6, direction)) { 145 | map.set(x, y, Door); 146 | 147 | // Remove wall next to the door. 148 | map.set(x + xmod, y + ymod, Floor); 149 | 150 | return true; 151 | } 152 | 153 | return false; 154 | } else { 155 | if (makeCorridor(x + xmod, y + ymod, 6, direction)) { 156 | map.set(x, y, Door); 157 | return true; 158 | } 159 | 160 | return false; 161 | } 162 | } 163 | 164 | boolean makeFeature() { 165 | int maxTries = 1000; 166 | 167 | for (int tries = 0; tries != maxTries; ++tries) { 168 | // Pick a random wall or corridor tile. 169 | // Make sure it has no adjacent doors (looks weird to have doors 170 | // next to each other). 171 | // Find a direction from which it's reachable. 172 | // Attempt to make a feature (room or corridor) starting at this 173 | // point. 174 | 175 | int x = nextInt(1, width - 2); 176 | int y = nextInt(1, height - 2); 177 | 178 | if (map.get(x, y) != Wall && map.get(x, y) != Corridor) 179 | continue; 180 | 181 | if (map.isAdjacent(x, y, Door)) 182 | continue; 183 | 184 | if (map.get(x, y + 1) == Floor || map.get(x, y + 1) == Corridor) { 185 | if (makeFeature(x, y, 0, -1, North)) 186 | return true; 187 | } else if (map.get(x - 1, y) == Floor || map.get(x - 1, y) == Corridor) { 188 | if (makeFeature(x, y, 1, 0, East)) 189 | return true; 190 | } else if (map.get(x, y - 1) == Floor || map.get(x, y - 1) == Corridor) { 191 | if (makeFeature(x, y, 0, 1, South)) 192 | return true; 193 | } else if (map.get(x + 1, y) == Floor || map.get(x + 1, y) == Corridor) { 194 | if (makeFeature(x, y, -1, 0, West)) 195 | return true; 196 | } 197 | } 198 | 199 | return false; 200 | } 201 | 202 | boolean makeStairs(int tile) { 203 | int tries = 0; 204 | int maxTries = 10000; 205 | 206 | for (; tries != maxTries; ++tries) { 207 | int x = nextInt(1, width - 2); 208 | int y = nextInt(1, height - 2); 209 | 210 | int t = map.get(x, y); 211 | if (!map.isAdjacent(x, y, Floor) || t == Wall || t == Door || map.isAdjacent(x, y, Door)) 212 | continue; 213 | 214 | map.set(x, y, tile); 215 | 216 | return true; 217 | } 218 | 219 | return false; 220 | } 221 | 222 | public static void main(String[] args) { 223 | DungeonTyrant dungeon = new DungeonTyrant(79, 24); 224 | dungeon.initialze(); 225 | dungeon.create(); 226 | dungeon.getMap().printMapChars(); 227 | dungeon.getMap().printMapArray(); 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /src/main/java/net/jmecn/map/creator/DungeonYan.java: -------------------------------------------------------------------------------- 1 | package net.jmecn.map.creator; 2 | 3 | import static net.jmecn.map.Direction.*; 4 | import static net.jmecn.map.Tile.*; 5 | 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | 9 | import net.jmecn.map.Point; 10 | import net.jmecn.map.Rect; 11 | 12 | /** 13 | * This is my own dungeon algorithm. 14 | * 15 | * @author yanmaoyuan 16 | * 17 | */ 18 | public class DungeonYan extends MapCreator { 19 | 20 | private List rooms; 21 | 22 | public DungeonYan(int width, int height) { 23 | super("creator.dungeon.yan", width, height); 24 | rooms = new ArrayList(); 25 | } 26 | 27 | @Override 28 | public void initialze() { 29 | map.fill(Unused); 30 | } 31 | 32 | @Override 33 | public void create() { 34 | rooms.clear(); 35 | Room room; 36 | 37 | int height = nextInt(5) + 9; 38 | int width = nextInt(5) + 6; 39 | 40 | // the Main room 41 | room = new Room(width, height); 42 | room.x = (this.width - width) / 2; 43 | room.y = (this.height - height) / 2; 44 | room.tiles[height/2][width/2] = UpStairs; 45 | room.type = Type.ROOM; 46 | room.print(); 47 | rooms.add(room); 48 | 49 | // generate rooms and corridors 50 | for (int i = 0; i < 500; i++) { 51 | room = randomRoom(); 52 | 53 | if (room.type == Type.CORRIDOR) { 54 | // 25% chance to generate a new corridor 55 | if (nextInt(4) == 0) { 56 | generateCorridor(room); 57 | } else { 58 | generateRoom(room); 59 | } 60 | } else { 61 | generateCorridor(room); 62 | } 63 | } 64 | 65 | // generate downStairs 66 | 67 | // travel the whole tree from the root node, find the deepest room and set a down stairs there. 68 | deepest = -1; 69 | best = null; 70 | dfs(rooms.get(0), 0); 71 | 72 | if (best != null) { 73 | map.set(best.x + best.width/2, best.y + best.height / 2, DownStairs); 74 | } else { 75 | System.out.println("Why didn't find any room ???"); 76 | 77 | // random choose a room 78 | do { 79 | room = randomRoom(); 80 | } while(room.type == Type.CORRIDOR || room.hasStairs()); 81 | map.set(room.x + room.width/2, room.y + room.height / 2, DownStairs); 82 | } 83 | } 84 | 85 | private int deepest = 0; 86 | private Room best = null; 87 | private void dfs(Room root, int depth) { 88 | if (root.children.size() > 0) { 89 | for(Room r : root.children) { 90 | dfs(r, depth+1); 91 | } 92 | } 93 | 94 | if(root.type == Type.ROOM) { 95 | if (depth > deepest) { 96 | deepest = depth; 97 | best = root; 98 | } 99 | } 100 | } 101 | 102 | /** 103 | * get a random room. 104 | * 105 | * @return 106 | */ 107 | private Room randomRoom() { 108 | int roomIndex = nextInt(rooms.size()); 109 | return rooms.get(roomIndex); 110 | } 111 | 112 | private void generateRoom(Room room) { 113 | // Size rnd(3~5)*2+1 114 | int halfW = nextInt(2) + 3; 115 | int halfH = nextInt(2) + 3; 116 | int width = halfW * 2 + 1; 117 | int height = halfH * 2 + 1; 118 | 119 | Room newRoom = new Room(width, height); 120 | newRoom.type = Type.ROOM; 121 | 122 | // new room; 123 | int mid; 124 | 125 | Point door = null; 126 | 127 | int dir = nextInt(DirectionCount); 128 | switch (dir) { 129 | case North: 130 | // NORTH 131 | mid = nextInt(room.width-2) + 1; 132 | 133 | newRoom.x = room.x + mid - halfW; 134 | newRoom.y = room.y - height + 1; 135 | door = new Point(halfW, height-1); 136 | break; 137 | case South: 138 | // SOUTH 139 | mid = nextInt(room.width-2) + 1 + room.x; 140 | 141 | newRoom.x = mid - halfW; 142 | newRoom.y = room.y + room.height - 1; 143 | door = new Point(halfW, 0); 144 | break; 145 | case East: 146 | // EAST 147 | mid = nextInt(room.height-2) + 1; 148 | 149 | newRoom.x = room.x + room.width - 1; 150 | newRoom.y = room.y + mid - halfH; 151 | door = new Point(0, halfH); 152 | break; 153 | case West: 154 | // WEST 155 | mid = nextInt(room.height-2) + 1 + room.y; 156 | 157 | newRoom.x = room.x - width + 1; 158 | newRoom.y = mid - halfH; 159 | door = new Point(width-1, halfH); 160 | break; 161 | default: 162 | // Nothing 163 | } 164 | 165 | if (checkOn(newRoom, dir)) { 166 | room.children.add(newRoom); 167 | 168 | rooms.add(newRoom); 169 | newRoom.addDoor(door); 170 | newRoom.print(); 171 | } 172 | } 173 | 174 | private void generateCorridor(Room room) { 175 | 176 | // in case that the dungeon become too much corridors 177 | if (room.doors.size() > 2) { 178 | return; 179 | } 180 | 181 | int mid, height, width; 182 | 183 | Room newRoom = null; 184 | Point door = null; 185 | Point junctionDoor = null; 186 | 187 | int dir = nextInt(DirectionCount); 188 | switch (dir) { 189 | case North: 190 | if (room.width <= 3) { 191 | mid = 1; 192 | } else { 193 | mid = nextInt(room.width - 2) + 1; 194 | } 195 | height = nextInt(4) + 5; 196 | 197 | newRoom = new Room(3, height); 198 | newRoom.type = Type.CORRIDOR; 199 | newRoom.x = room.x + mid - 1; 200 | newRoom.y = room.y - height + 1; 201 | 202 | door = new Point(1, height-1); 203 | junctionDoor = new Point(mid, 0); 204 | break; 205 | case South: 206 | if (room.width <= 3) { 207 | mid = 1; 208 | } else { 209 | mid = nextInt(room.width - 2) + 1; 210 | } 211 | height = nextInt(4) + 5; 212 | 213 | newRoom = new Room(3, height); 214 | newRoom.type = Type.CORRIDOR; 215 | newRoom.x = room.x + mid - 1; 216 | newRoom.y = room.y + room.height - 1; 217 | 218 | door = new Point(1, 0); 219 | junctionDoor = new Point(1, room.height-1); 220 | break; 221 | case East: 222 | if (room.height <= 3) { 223 | mid = 1; 224 | } else { 225 | mid = nextInt(room.height - 2) + 1; 226 | } 227 | width = nextInt(4) + 5; 228 | 229 | newRoom = new Room(width, 3); 230 | newRoom.type = Type.CORRIDOR; 231 | newRoom.x = room.x + room.width - 1; 232 | newRoom.y = room.y + mid - 1; 233 | door = new Point(0, 1); 234 | junctionDoor = new Point(room.width-1, mid); 235 | break; 236 | case West: 237 | if (room.height <= 3) { 238 | mid = 1; 239 | } else { 240 | mid = nextInt(room.height - 2) + 1; 241 | } 242 | width = nextInt(4) + 5; 243 | 244 | newRoom = new Room(width, 3); 245 | newRoom.type = Type.CORRIDOR; 246 | newRoom.x = room.x - width + 1; 247 | newRoom.y = room.y + mid - 1; 248 | 249 | door = new Point(width-1, 1); 250 | junctionDoor = new Point(0, mid); 251 | break; 252 | default: 253 | break; 254 | } 255 | 256 | if (checkOn(newRoom, dir)) { 257 | room.addDoor(junctionDoor); 258 | room.children.add(newRoom); 259 | 260 | rooms.add(newRoom); 261 | newRoom.addDoor(door); 262 | newRoom.print(); 263 | } 264 | } 265 | 266 | private boolean checkOn(Room room, int dir) { 267 | if (room.x < 0 || room.y < 0 || (room.x + room.width) >= this.width || (room.y + room.height) >= this.height) 268 | return false; 269 | 270 | int xstart = 0, ystart = 0; 271 | int xend = 0, yend = 0; 272 | switch (dir) { 273 | case North: 274 | yend = 1; 275 | break; 276 | case South: 277 | ystart = 1; 278 | break; 279 | case East: 280 | xstart = 1; 281 | break; 282 | case West: 283 | xend = 1; 284 | break; 285 | } 286 | for (int y = ystart; y < room.height-yend; y++) { 287 | for (int x = xstart; x < room.width-xend; x++) { 288 | if (!(map.get(room.x+x, room.y+y) == Unused)) 289 | return false; 290 | } 291 | } 292 | return true; 293 | } 294 | 295 | enum Type { 296 | ROOM, CORRIDOR; 297 | } 298 | 299 | private class Room extends Rect { 300 | Type type; 301 | 302 | // tiles 303 | int[][] tiles; 304 | 305 | // doors 306 | List doors; 307 | 308 | // sub rooms 309 | List children; 310 | 311 | Room(int width, int height) { 312 | super(0, 0, width, height); 313 | 314 | tiles = new int[height][width]; 315 | 316 | doors = new ArrayList(); 317 | children = new ArrayList(); 318 | 319 | // surround the room with walls, and fill the rest with floors. 320 | for (int y = 0; y < height; y++) { 321 | for (int x = 0; x < width; x++) { 322 | if (y == 0 || y == height-1 || x == 0 || x == width-1) { 323 | tiles[y][x] = Wall; 324 | } else { 325 | tiles[y][x] = Floor; 326 | } 327 | } 328 | } 329 | } 330 | 331 | void addDoor(Point door) { 332 | if (door != null) { 333 | doors.add(door); 334 | tiles[door.y][door.x] = Door; 335 | } 336 | } 337 | 338 | /** 339 | * find out if we have any stair tiles in the room 340 | * 341 | * @return 342 | */ 343 | boolean hasStairs() { 344 | for (int y = 0; y < height; y++) { 345 | for (int x = 0; x < width; x++) { 346 | if (this.tiles[y][x] == DownStairs || this.tiles[y][x] == UpStairs) { 347 | return true; 348 | } 349 | } 350 | } 351 | return false; 352 | }; 353 | 354 | void print() { 355 | for (int y = 0; y < height; y++) { 356 | for (int x = 0; x < width; x++) { 357 | map.set(this.x + x, this.y + y, tiles[y][x]); 358 | } 359 | } 360 | } 361 | } 362 | } 363 | -------------------------------------------------------------------------------- /src/main/java/net/jmecn/map/creator/ForestHauberk.java: -------------------------------------------------------------------------------- 1 | package net.jmecn.map.creator; 2 | 3 | import static net.jmecn.map.Tile.*; 4 | 5 | import java.util.ArrayList; 6 | import java.util.LinkedList; 7 | import java.util.List; 8 | 9 | import net.jmecn.map.Point; 10 | 11 | /** 12 | * 13 | * https://github.com/munificent/hauberk/blob/master/lib/src/content/forest.dart 14 | * 15 | * @author yanmaoyuan 16 | * 17 | */ 18 | public class ForestHauberk extends MapCreator { 19 | 20 | /// A forest is a collection of grassy meadows surrounded by trees and 21 | /// connected by passages. 22 | private int numMeadows = 10; 23 | 24 | /// The number of iterations of Lloyd's algorithm to run on the points. 25 | /// 26 | /// Fewer results in clumpier, less evenly spaced points. More results in 27 | /// more evenly spaced but can eventually look too regular. 28 | private int voronoiIterations = 10; 29 | 30 | public ForestHauberk(int width, int height) { 31 | super("creator.forest.hauberk", width, height); 32 | } 33 | 34 | public void setNumMeadows(int numMeadows) { 35 | if (numMeadows < 10) numMeadows = 10; 36 | this.numMeadows = numMeadows; 37 | } 38 | 39 | public void setVoronoiIterations(int voronoiIterations) { 40 | if (voronoiIterations < 5) voronoiIterations = 5; 41 | this.voronoiIterations = voronoiIterations; 42 | } 43 | 44 | @Override 45 | public void initialze() { 46 | map.fill(Wall); 47 | 48 | } 49 | 50 | @Override 51 | public void create() { 52 | // Randomly position the meadows. 53 | LinkedList meadows = new LinkedList(); 54 | for (int i = 0; i < numMeadows; i++) { 55 | int x = nextInt(width); 56 | int y = nextInt(height); 57 | meadows.add(new Point(x, y)); 58 | } 59 | 60 | // Space them out more evenly by moving each point to the centroid of 61 | // its cell in the Voronoi diagram of the points. In other words, for each 62 | // point, we find the (approximate) region of the stage where that point 63 | // is the closest one. Then we move the point to the center of that 64 | // region. 65 | // http://en.wikipedia.org/wiki/Lloyd%27s_algorithm 66 | for (int i = 0; i < voronoiIterations; i++) { 67 | // For each cell in the stage, determine which point it's nearest to. 68 | List> regions = new ArrayList>(numMeadows); 69 | for (int j = 0; j < numMeadows; j++) { 70 | regions.add(new ArrayList()); 71 | } 72 | 73 | for (int y = 0; y < height; y++) { 74 | for (int x = 0; x < width; x++) { 75 | int nearest = -1; 76 | int nearestDistanceSquared = 99999999; 77 | for (int j = 0; j < numMeadows; j++) { 78 | Point p = meadows.get(j); 79 | int dx = p.x - x; 80 | int dy = p.y - y; 81 | int lengthSquared = dx * dx + dy * dy; 82 | if (lengthSquared < nearestDistanceSquared) { 83 | nearestDistanceSquared = lengthSquared; 84 | nearest = j; 85 | } 86 | } 87 | 88 | regions.get(nearest).add(new Point(x, y)); 89 | } 90 | } 91 | 92 | // Now move each point to the centroid of its region. The centroid 93 | // is just the average of all of the cells in the region. 94 | for (int j = 0; j < numMeadows; j++) { 95 | List region = regions.get(j); 96 | int len = region.size(); 97 | 98 | if (len == 0) 99 | continue; 100 | int a = 0; 101 | int b = 0; 102 | for (int k = 0; k < len; k++) { 103 | Point p = region.get(k); 104 | a += p.x; 105 | b += p.y; 106 | } 107 | meadows.get(j).x = a / len; 108 | meadows.get(j).y = b / len; 109 | } 110 | } 111 | 112 | // Connect all of the points together. 113 | // Use Prim's algorithm to generate a minimum spanning tree. 114 | List connected = new ArrayList(numMeadows); 115 | 116 | // the root node 117 | connected.add(meadows.removeLast()); 118 | 119 | while (!meadows.isEmpty()) { 120 | Point bestFrom = null; 121 | Point bestTo = null; 122 | int bestDistance = -1; 123 | 124 | int cLen = connected.size(); 125 | int mLen = meadows.size(); 126 | for(int p=0; p bestDistance) { 174 | bestFrom = from; 175 | bestTo = to; 176 | bestDistance = distance; 177 | } 178 | } 179 | } 180 | map.set(bestFrom.x, bestFrom.y, UpStairs); 181 | map.set(bestTo.x, bestTo.y, DownStairs); 182 | 183 | // make wall 184 | erode(10000, Floor, Wall); 185 | 186 | // Plant trees 187 | for (int y = 0; y < height; y++) { 188 | for (int x = 0; x < width; x++) { 189 | if (map.get(x, y) == Wall) { 190 | map.set(x, y, Tree); 191 | } 192 | } 193 | } 194 | } 195 | 196 | /// Randomly turns some [wall] tiles into [floor] and vice versa. 197 | private void erode(int iterations, int floor, int wall) { 198 | for (int i = 0; i < iterations; i++) { 199 | // TODO: This way this works is super inefficient. Would be better to 200 | // keep track of the floor tiles near open ones and choose from them. 201 | // pos 202 | int x = nextInt(1, width - 1); 203 | int y = nextInt(1, height - 1); 204 | 205 | int here = map.get(x, y); 206 | if (here != wall) 207 | continue; 208 | 209 | // Keep track of how many floors we're adjacent too. We will only 210 | // erode 211 | // if we are directly next to a floor. 212 | int floors = 0; 213 | if (map.get(x - 1, y) == floor) 214 | floors++; 215 | if (map.get(x + 1, y) == floor) 216 | floors++; 217 | if (map.get(x, y - 1) == floor) 218 | floors++; 219 | if (map.get(x, y + 1) == floor) 220 | floors++; 221 | 222 | // Prefer to erode tiles near more floor tiles so the erosion isn't 223 | // too 224 | // spiky. 225 | if (floors < 2) 226 | continue; 227 | if (nextInt(9 - floors) == 0) 228 | map.set(x, y, floor); 229 | } 230 | } 231 | 232 | private void carvePath(Point from, Point to) { 233 | 234 | int a = Math.abs(to.x - from.x); 235 | int b = Math.abs(to.y - from.y); 236 | 237 | if (a >= b) { 238 | int xStep = 1; 239 | if (to.x < from.x) 240 | xStep = -1; 241 | 242 | float yStep = (float) (to.y - from.y) / a; 243 | // use xStep 244 | int x = from.x; 245 | int y = from.y; 246 | for (int i = 0; i <= a; i++) { 247 | x = from.x + xStep * i; 248 | y = from.y +(int)(yStep * i); 249 | // Make slightly wider passages. 250 | map.set(x, y, Floor); 251 | map.set(x + 1, y, Floor); 252 | map.set(x, y + 1, Floor); 253 | } 254 | 255 | } else { 256 | // use yStep 257 | int yStep = 1; 258 | if (to.y < from.y) 259 | yStep = -1; 260 | 261 | float xStep = (float) (to.x - from.x) / b; 262 | // use xStep 263 | int x = from.x; 264 | int y = from.y; 265 | for (int i = 0; i <= b; i++) { 266 | x = from.x + (int) (xStep * i); 267 | y = from.y + yStep * i; 268 | // Make slightly wider passages. 269 | map.set(x, y, Floor); 270 | map.set(x + 1, y, Floor); 271 | map.set(x, y + 1, Floor); 272 | } 273 | } 274 | } 275 | 276 | private void carveCircle(Point center, int radius) { 277 | // bounds 278 | int left = Math.max(1, center.x - radius); 279 | int top = Math.max(1, center.y - radius); 280 | int right = Math.min(center.x + radius, width - 2); 281 | int bottom = Math.min(center.y + radius, height - 2); 282 | 283 | for (int y = top; y <= bottom; y++) { 284 | for (int x = left; x <= right; x++) { 285 | int dx = x - center.x; 286 | int dy = y - center.y; 287 | if (Math.sqrt(dx * dx + dy * dy) <= radius) 288 | map.set(x, y, Floor); 289 | } 290 | } 291 | } 292 | } 293 | -------------------------------------------------------------------------------- /src/main/java/net/jmecn/map/creator/Islands.java: -------------------------------------------------------------------------------- 1 | package net.jmecn.map.creator; 2 | 3 | import static net.jmecn.map.Tile.*; 4 | 5 | /** 6 | * https://gamedevelopment.tutsplus.com/tutorials/generate-random-cave-levels-using-cellular-automata--gamedev-9664 7 | * 8 | * http://pixelenvy.ca/wa/ca_cave.html 9 | * 10 | * @author yanmaoyuan 11 | * 12 | */ 13 | public class Islands extends MapCreator { 14 | 15 | private int fillprob = 45; 16 | final static private int r1Cutoff = 5; 17 | 18 | public Islands(int width, int height) { 19 | super("creator.islands.cellauto", width, height); 20 | } 21 | 22 | protected int randPick() { 23 | if (rand.nextInt(100) < fillprob) { 24 | return Floor; 25 | } else { 26 | return Water; 27 | } 28 | } 29 | 30 | @Override 31 | public void initialze() { 32 | for (int y = 0; y < height; y++) { 33 | for (int x = 0; x < width; x++) { 34 | if (x == 0 || y == 0 || x == width - 1 || y == height - 1) { 35 | map.set(x, y, Floor); 36 | } else { 37 | map.set(x, y, randPick()); 38 | } 39 | } 40 | } 41 | 42 | } 43 | 44 | @Override 45 | public void create() { 46 | do { 47 | initialze(); 48 | for (int i = 0; i < 5; i++) { 49 | generation(i); 50 | } 51 | } while(!(floodFill())); 52 | 53 | // fill disconnected cave with wall 54 | for (int y = 1; y < height-1; y++) { 55 | for (int x = 1; x < width-1; x++) { 56 | if (map.get(x, y) == Floor) { 57 | map.set(x, y, Water); 58 | } else if (map.get(x, y) == Stone) { 59 | map.set(x, y, Floor); 60 | } 61 | } 62 | } 63 | } 64 | 65 | private int sum = 0; 66 | 67 | private boolean floodFill() { 68 | // get a random start point 69 | int x, y; 70 | do { 71 | x = nextInt(width); 72 | y = nextInt(height); 73 | } while (map.get(x, y) != Floor); 74 | 75 | // 76 | sum = 0; 77 | floodFill8(x, y, Stone, Floor); 78 | 79 | // at least 50% place are floor 80 | double percent = 100.0 * sum/(width * height); 81 | return percent >= 50.0; 82 | } 83 | 84 | private void floodFill8(int x, int y, int newTile, int oldTile) { 85 | 86 | // skip boundary 87 | if (x < 0 || x > width - 1 || y < 0 || y > height - 1 || map.get(x, y) != oldTile || map.get(x, y) == newTile) { 88 | return; 89 | } 90 | map.set(x, y, newTile); 91 | sum++; 92 | 93 | floodFill8(x - 1, y - 1, newTile, oldTile); 94 | floodFill8(x - 1, y, newTile, oldTile); 95 | floodFill8(x - 1, y + 1, newTile, oldTile); 96 | floodFill8(x, y - 1, newTile, oldTile); 97 | floodFill8(x, y + 1, newTile, oldTile); 98 | floodFill8(x + 1, y - 1, newTile, oldTile); 99 | floodFill8(x + 1, y, newTile, oldTile); 100 | floodFill8(x + 1, y + 1, newTile, oldTile); 101 | 102 | } 103 | 104 | protected void generation(int gen) { 105 | int[][] tmp = copy(); 106 | for (int y = 1; y < height - 1; y++) { 107 | for (int x = 1; x < width - 1; x++) { 108 | 109 | int m = 0; 110 | for (int offsety = -1; offsety <= 1; offsety++) { 111 | for (int offsetx = -1; offsetx <= 1; offsetx++) { 112 | if (map.get(offsetx + x, offsety + y) == Water) { 113 | m++; 114 | } 115 | } 116 | } 117 | 118 | if (m >= r1Cutoff) { 119 | tmp[y][x] = Floor; 120 | } else { 121 | tmp[y][x] = Water; 122 | } 123 | 124 | } 125 | } 126 | 127 | for (int y = 0; y < height; y++) { 128 | for (int x = 0; x < width; x++) { 129 | map.set(x, y, tmp[y][x]); 130 | } 131 | } 132 | } 133 | 134 | public void setFillprob(int fillprob) { 135 | this.fillprob = fillprob; 136 | } 137 | 138 | public static void main(String[] args) { 139 | Islands cave = new Islands(50, 50); 140 | cave.setSeed(1654987414656544l); 141 | cave.setUseSeed(false); 142 | cave.initialze(); 143 | cave.create(); 144 | cave.getMap().printMapChars(); 145 | cave.getMap().printMapArray(); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/main/java/net/jmecn/map/creator/MapCreator.java: -------------------------------------------------------------------------------- 1 | package net.jmecn.map.creator; 2 | 3 | import java.util.Arrays; 4 | import java.util.Random; 5 | import java.util.ResourceBundle; 6 | 7 | import net.jmecn.map.Map2D; 8 | 9 | /** 10 | * Base class for a tile map creator. 11 | * 12 | * @author yanmaoyuan 13 | * 14 | */ 15 | public abstract class MapCreator { 16 | 17 | protected String name;// Algorithm name 18 | protected Map2D map; 19 | protected int width; 20 | protected int height; 21 | 22 | protected static Random rand = new Random(); 23 | private long seed = 0; 24 | private boolean useSeed = false; 25 | 26 | public MapCreator(int width, int height) { 27 | this.name = "Unknown algorithm#"+rand.nextInt(); 28 | this.map = new Map2D(width, height); 29 | this.width = width; 30 | this.height = height; 31 | } 32 | 33 | public MapCreator(String name, int width, int height) { 34 | setName(name); 35 | this.map = new Map2D(width, height); 36 | this.width = width; 37 | this.height = height; 38 | } 39 | 40 | public void resize(int width, int height) { 41 | this.map = new Map2D(width, height); 42 | this.width = width; 43 | this.height = height; 44 | initRand(); 45 | initialze(); 46 | } 47 | 48 | public void setUseSeed(boolean useSeed) { 49 | this.useSeed = useSeed; 50 | initRand(); 51 | } 52 | 53 | public void setSeed(long seed) { 54 | this.seed = seed; 55 | } 56 | 57 | public void initRand() { 58 | initRand(useSeed); 59 | } 60 | 61 | public void initRand(boolean useSeed) { 62 | if (useSeed) { 63 | rand = new Random(seed); 64 | } else { 65 | rand = new Random(System.currentTimeMillis()); 66 | } 67 | } 68 | 69 | protected boolean nextBoolean() { 70 | return rand.nextBoolean(); 71 | } 72 | 73 | protected int nextInt() { 74 | return rand.nextInt(); 75 | } 76 | 77 | protected int nextInt(final int range) { 78 | return rand.nextInt(range); 79 | } 80 | 81 | protected int nextInt(final int min, final int max) { 82 | return rand.nextInt(max - min) + min; 83 | } 84 | 85 | protected int[][] copy() { 86 | int[][] data = map.getMap(); 87 | int w = map.getWidth(); 88 | int h = map.getHeight(); 89 | 90 | int[][] tmp = new int[h][]; 91 | 92 | for (int y = 0; y < h; y++) { 93 | tmp[y] = Arrays.copyOf(data[y], w); 94 | } 95 | 96 | return tmp; 97 | } 98 | 99 | public String getName() { 100 | return name; 101 | } 102 | 103 | public void setName(String name) { 104 | ResourceBundle res = ResourceBundle.getBundle("net.jmecn.map.creator.Name"); 105 | if (res != null) { 106 | try { 107 | this.name = res.getString(name); 108 | } catch (Exception e) { 109 | // no need to do anything 110 | this.name = name; 111 | } 112 | } else { 113 | this.name = name; 114 | } 115 | } 116 | 117 | public Map2D getMap() { 118 | return map; 119 | } 120 | 121 | public abstract void initialze(); 122 | 123 | public abstract void create(); 124 | 125 | @Override 126 | public String toString() { 127 | return name; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/main/java/net/jmecn/map/creator/Maze.java: -------------------------------------------------------------------------------- 1 | package net.jmecn.map.creator; 2 | 3 | import static net.jmecn.map.Direction.*; 4 | 5 | import java.util.logging.Level; 6 | import java.util.logging.Logger; 7 | 8 | import net.jmecn.map.Direction; 9 | import net.jmecn.map.Map2D; 10 | import net.jmecn.map.Tile; 11 | 12 | /** 13 | * About maze you should read this article: 14 | * http://www.astrolog.org/labyrnth/algrithm.htm 15 | * @author yanmaoyuan 16 | * 17 | */ 18 | public class Maze extends MapCreator { 19 | 20 | static Logger logger = Logger.getLogger(Maze.class.getName()); 21 | 22 | protected class Point { 23 | int x; 24 | int y; 25 | 26 | Point() { 27 | x = y = 0; 28 | } 29 | 30 | Point(int x, int y) { 31 | this.x = x; 32 | this.y = y; 33 | } 34 | 35 | int index() { 36 | return x + y * cellCols; 37 | } 38 | } 39 | 40 | protected class Cell { 41 | boolean[] door = new boolean[] { false, false, false, false }; 42 | } 43 | 44 | private Cell[][] maze; 45 | private int[] cells; 46 | private int cellCols; 47 | private int cellRows; 48 | private int cellCnt; 49 | 50 | private static int ROAD_SIZE = 2; 51 | 52 | public Maze(int width, int height) { 53 | super("creator.maze", width * ROAD_SIZE + 1, height * ROAD_SIZE + 1); 54 | this.cellCols = width; 55 | this.cellRows = height; 56 | } 57 | 58 | @Override 59 | public void resize(int width, int height) { 60 | this.map = new Map2D(width * ROAD_SIZE + 1, height * ROAD_SIZE + 1); 61 | this.width = cellCols * ROAD_SIZE + 1; 62 | this.height = cellRows * ROAD_SIZE + 1; 63 | this.cellCols = width; 64 | this.cellRows = height; 65 | initRand(); 66 | initialze(); 67 | } 68 | 69 | /** 70 | * set road size 71 | * 72 | * @param size 73 | */ 74 | public void setRoadSize(final int size) { 75 | ROAD_SIZE = size + 1; 76 | } 77 | 78 | @Override 79 | public void initialze() { 80 | this.maze = new Cell[cellRows][cellCols]; 81 | for (int y = 0; y < cellRows; y++) { 82 | for (int x = 0; x < cellCols; x++) { 83 | maze[y][x] = new Cell(); 84 | } 85 | } 86 | maze[0][0].door[West] = true; 87 | maze[cellRows - 1][cellCols - 1].door[East] = true; 88 | 89 | this.cellCnt = cellCols * cellRows; 90 | this.cells = new int[cellCnt]; 91 | for (int i = 0; i < cellCnt; i++) { 92 | cells[i] = -1; 93 | } 94 | } 95 | 96 | @Override 97 | public void create() { 98 | buildMaze(); 99 | buildTile(); 100 | } 101 | 102 | public void buildMaze() { 103 | int dir = 0; 104 | 105 | Point c1 = new Point(); 106 | Point c2 = null; 107 | 108 | while (true) { 109 | c1.x = nextInt(cellCols); 110 | c1.y = nextInt(cellRows); 111 | c2 = null; 112 | 113 | dir = nextInt(DirectionCount); 114 | switch (dir) { 115 | case North: 116 | if (c1.y > 0) 117 | c2 = new Point(c1.x, c1.y - 1); 118 | break; 119 | case South: 120 | if (c1.y < cellRows - 1) 121 | c2 = new Point(c1.x, c1.y + 1); 122 | break; 123 | case East: 124 | if (c1.x < cellCols - 1) 125 | c2 = new Point(c1.x + 1, c1.y); 126 | break; 127 | case West: 128 | if (c1.x > 0) 129 | c2 = new Point(c1.x - 1, c1.y); 130 | break; 131 | default: 132 | c2 = null; 133 | logger.log(Level.WARNING, "Unknown direction:" + dir); 134 | break; 135 | } 136 | 137 | if (c2 == null) 138 | continue; 139 | if (isConnect(c1.index(), c2.index())) 140 | continue; 141 | else { 142 | unionCells(c1.index(), c2.index()); 143 | maze[c1.y][c1.x].door[dir] = true; 144 | maze[c2.y][c2.x].door[Direction.negative(dir)] = true; 145 | } 146 | if (isConnect(0, cellCnt - 1) && allConnect()) 147 | break; 148 | 149 | } 150 | } 151 | 152 | private boolean isConnect(int c1, int c2) { 153 | while (cells[c1] >= 0) 154 | c1 = cells[c1]; 155 | while (cells[c2] >= 0) 156 | c2 = cells[c2]; 157 | if (c1 == c2) 158 | return true; 159 | else 160 | return false; 161 | } 162 | 163 | private boolean allConnect() { 164 | int i, count_root = 0; 165 | for (i = 0; i < cellRows * cellCols; i++) { 166 | if (cells[i] < 0) 167 | count_root++; 168 | } 169 | if (1 == count_root) 170 | return true; 171 | else 172 | return false; 173 | } 174 | 175 | /** 176 | * if the two adjacent rooms are not connect, remove the wall between them(or fix a door) 177 | * @param c1 178 | * @param c2 179 | */ 180 | private void unionCells(int c1, int c2) { 181 | while (cells[c1] >= 0) 182 | c1 = cells[c1]; 183 | while (cells[c2] >= 0) 184 | c2 = cells[c2]; 185 | 186 | // the depth of the tree with c2 is deepper than Tc1, Tc1 attach to Tc2 187 | if (cells[c1] > cells[c2]) { 188 | cells[c1] = c2; 189 | } else { 190 | if (cells[c1] == cells[c2]) 191 | cells[c1]--; 192 | cells[c2] = c1; 193 | } 194 | } 195 | 196 | public void buildTile() { 197 | for (int col = 0; col < cellCols; col++) { 198 | for (int row = 0; row < cellRows; row++) { 199 | Cell cell = maze[row][col]; 200 | int x = col * ROAD_SIZE; 201 | int y = row * ROAD_SIZE; 202 | if (!cell.door[North]) { 203 | makeWall(x, y, x + ROAD_SIZE, y); 204 | } 205 | if (!cell.door[East]) { 206 | makeWall(x + ROAD_SIZE, y, x + ROAD_SIZE, y + ROAD_SIZE); 207 | } 208 | if (!cell.door[South]) { 209 | makeWall(x, y + ROAD_SIZE, x + ROAD_SIZE, y + ROAD_SIZE); 210 | } 211 | if (!cell.door[West]) { 212 | makeWall(x, y, x, y + ROAD_SIZE); 213 | } 214 | } 215 | } 216 | 217 | } 218 | 219 | private void makeWall(int x1, int y1, int x2, int y2) { 220 | for (int y = y1; y <= y2; y++) { 221 | for (int x = x1; x <= x2; x++) { 222 | map.set(x, y, Tile.Wall); 223 | } 224 | } 225 | } 226 | 227 | public static void main(String[] args) { 228 | Maze maze = new Maze(30, 14); 229 | maze.setRoadSize(1); 230 | maze.setSeed(1654987414656544l); 231 | maze.setUseSeed(true); 232 | maze.initialze(); 233 | maze.create(); 234 | maze.getMap().printMapChars(); 235 | maze.getMap().printMapArray(); 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /src/main/java/net/jmecn/map/creator/MazeWilson.java: -------------------------------------------------------------------------------- 1 | package net.jmecn.map.creator; 2 | 3 | import static net.jmecn.map.Direction.*; 4 | import static net.jmecn.map.Tile.*; 5 | 6 | import java.util.ArrayList; 7 | import java.util.logging.Logger; 8 | 9 | import net.jmecn.map.Point; 10 | 11 | /** 12 | * Maze creator using Wilson's algorithm 13 | * 14 | * @author yanmaoyuan 15 | * 16 | */ 17 | public class MazeWilson extends MapCreator { 18 | 19 | class Cell extends Point { 20 | int tile; 21 | Cell(int x, int y) { 22 | super(x, y); 23 | tile = Unused; 24 | } 25 | Cell(int x, int y, int tile) { 26 | super(x, y); 27 | this.tile = tile; 28 | } 29 | } 30 | 31 | static Logger logger = Logger.getLogger(MazeWilson.class.getName()); 32 | 33 | public MazeWilson(int width, int height) { 34 | super("creator.maze.wilson", width, height); 35 | } 36 | 37 | @Override 38 | public void initialze() { 39 | map.fill(Wall); 40 | } 41 | 42 | @Override 43 | public void create() { 44 | // first spot on maze. walker seeks established maze tiles 45 | map.set(1, height - 2, Floor); 46 | 47 | // the first start point. 48 | Cell nextStart = new Cell(3, height - 2); 49 | while (nextStart != null) { 50 | erasedLoopWalk(nextStart.x, nextStart.y); 51 | nextStart = findNextStartCell(nextStart); 52 | } 53 | } 54 | 55 | private void erasedLoopWalk(int x, int y) { 56 | ArrayList trail = new ArrayList(); 57 | ArrayList moveLog = new ArrayList(); // should always 58 | 59 | trail.add(new Cell(x, y, Stone)); 60 | map.set(x, y, Stone); 61 | 62 | moveLog.add(UnknownDir);// placeholder that hopefully won't break anything 63 | while (true) { 64 | // decide candidates based on borders and previous move 65 | ArrayList candidates = new ArrayList(); 66 | int prev = moveLog.get(moveLog.size() - 1); 67 | if (x - 2 > 0 && prev != East) 68 | candidates.add(West); 69 | if (x + 2 < width - 1 && prev != West) 70 | candidates.add(East); 71 | if (y - 2 > 0 && prev != South) 72 | candidates.add(North); 73 | if (y + 2 < height - 1 && prev != North) 74 | candidates.add(South); 75 | 76 | int move = candidates.get(rand.nextInt(candidates.size())); 77 | 78 | for (int i = 0; i < 2; i++) { // do twice so that each move moves 2 spaces 79 | // choose a direction and walk a step 80 | if (move == West) 81 | x--; 82 | if (move == East) 83 | x++; 84 | if (move == North) 85 | y--; 86 | if (move == South) 87 | y++; 88 | 89 | trail.add(new Cell(x, y, Stone));// add the new piece of the trail 90 | map.set(x, y, Stone);// change its tile for collision purposes 91 | 92 | // look for loops and erase back if found 93 | boolean looped = false; 94 | if (map.get(x + 1, y) == Stone && move != West) { 95 | x++; 96 | looped = true; 97 | } else if (map.get(x - 1, y) == Stone && move != East) { 98 | x--; 99 | looped = true; 100 | } else if (map.get(x, y + 1) == Stone && move != North) { 101 | y++; 102 | looped = true; 103 | } else if (map.get(x, y - 1) == Stone && move != South) { 104 | y--; 105 | looped = true; 106 | } else if (map.get(x - 1, y - 1) == Stone 107 | && map.get(x - 1, y - 1) != trail.get(trail.size() - 3).tile) { 108 | x--; 109 | y--; 110 | looped = true; 111 | } else if (map.get(x + 1, y - 1) == Stone 112 | && map.get(x + 1, y - 1) != trail.get(trail.size() - 3).tile) { 113 | x++; 114 | y--; 115 | looped = true; 116 | } else if (map.get(x - 1, y + 1) == Stone 117 | && map.get(x - 1, y + 1) != trail.get(trail.size() - 3).tile) { 118 | x--; 119 | y++; 120 | looped = true; 121 | } else if (map.get(x + 1, y + 1) == Stone 122 | && map.get(x + 1, y + 1) != trail.get(trail.size() - 3).tile) { 123 | x++; 124 | y++; 125 | looped = true; 126 | } 127 | 128 | if (looped) { 129 | int retrace = trail.indexOf(new Cell(x, y)); 130 | 131 | for (int j = retrace + 1; j < trail.size(); j++) { 132 | Cell p = trail.get(j); 133 | map.set(p.x, p.y, Wall); 134 | } 135 | 136 | trail.subList(retrace + 1, trail.size()).clear(); 137 | moveLog.subList(retrace / 2 + 1, moveLog.size()).clear(); 138 | 139 | break; // breaks the for loop to avoid more collision detection 140 | } 141 | 142 | // look for connections and commit if found 143 | if (i == 0 && (map.get(x - 1, y) == Floor || map.get(x + 1, y) == Floor 144 | || map.get(x, y - 1) == Floor || map.get(x, y + 1) == Floor)) { 145 | for (Cell p : trail) { 146 | map.set(p.x, p.y, Floor); 147 | } 148 | return; 149 | } 150 | 151 | } // end for 152 | 153 | moveLog.add(move); 154 | } 155 | } 156 | 157 | private Cell findNextStartCell(Cell lastStart) { 158 | // search bottom to top, left to right, for a black cell with two 159 | // adjacent black cells 160 | 161 | boolean firstLoop = true; 162 | for (int y = height - 2; y >= 1; y--) { 163 | for (int x = 1; x <= width - 2; x++) { 164 | if (firstLoop) { 165 | x = lastStart.x; 166 | y = lastStart.y; 167 | firstLoop = false; 168 | } 169 | 170 | int adjacentBlackCounter = 0; 171 | 172 | for (int offsetY = -1; offsetY <= 1; offsetY++) { 173 | for (int offsetX = -1; offsetX <= 1; offsetX++) { 174 | if (offsetX == 0 && offsetY == 0) 175 | continue; 176 | if (map.get(x + offsetX, y + offsetY) == Wall) { 177 | adjacentBlackCounter++; 178 | } 179 | } 180 | } 181 | 182 | if (adjacentBlackCounter == 8) { 183 | return new Cell(x, y); 184 | } 185 | } 186 | } 187 | return null; 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/main/java/net/jmecn/map/pathfinding/Node.java: -------------------------------------------------------------------------------- 1 | package net.jmecn.map.pathfinding; 2 | 3 | import java.util.LinkedList; 4 | 5 | import net.jmecn.map.Point; 6 | 7 | public class Node implements Comparable { 8 | public Point _Pos; // position of the node 9 | public int sourcePoint; 10 | public int destiPoint; 11 | // public Node _parentnode; //the parent node 12 | public Node _parentnode; 13 | 14 | int col; 15 | int row; 16 | 17 | // initialize the NOde 18 | public Node(Point _Pos, int col, int row) { 19 | this._Pos = _Pos; 20 | this.col = col; 21 | this.row = row; 22 | } 23 | 24 | // get the cost of the Path 25 | public int GetCost(Node node) { 26 | int m = node._Pos.x - _Pos.x; 27 | int n = node._Pos.y - _Pos.y; 28 | return (int) Math.sqrt(m * m + n * n); 29 | } 30 | 31 | // check if the node is the destination point 32 | public boolean equals(Object node) { 33 | if (_Pos.x == ((Node) node)._Pos.x && _Pos.y == ((Node) node)._Pos.y) { 34 | return true; 35 | } 36 | return false; 37 | } 38 | 39 | // get the minist cost 40 | public int compareTo(Node node) { 41 | int a1 = sourcePoint + destiPoint; 42 | int a2 = ((Node) node).sourcePoint + ((Node) node).destiPoint; 43 | if (a1 < a2) { 44 | return -1; 45 | } else if (a1 == a2) { 46 | return 0; 47 | } else 48 | return 1; 49 | 50 | } 51 | 52 | public LinkedList getLimit() { 53 | LinkedList limit = new LinkedList(); 54 | int x = _Pos.x; 55 | int y = _Pos.y; 56 | if (y > 0) { 57 | limit.add(new Node(new Point(x, y - 1), col, row)); // up 58 | } 59 | if (y < row) { 60 | limit.add(new Node(new Point(x, y + 1), col, row)); // down 61 | } 62 | if (x > 0) { 63 | limit.add(new Node(new Point(x - 1, y), col, row)); // left 64 | } 65 | if (x < col) { 66 | limit.add(new Node(new Point(x + 1, y), col, row)); // right 67 | } 68 | return limit; 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/net/jmecn/map/pathfinding/PathFinding.java: -------------------------------------------------------------------------------- 1 | package net.jmecn.map.pathfinding; 2 | 3 | import java.util.LinkedList; 4 | import java.util.List; 5 | 6 | import net.jmecn.map.Point; 7 | 8 | public class PathFinding { 9 | 10 | private OpenedList openedList; 11 | private LinkedList closedList; 12 | private int[][] _map; 13 | private int[] _limit; 14 | 15 | private int col; 16 | private int row; 17 | 18 | public PathFinding(int[][] map, int[] limit) { 19 | _map = map; 20 | _limit = limit; 21 | 22 | col = map[0].length - 1; 23 | row = map.length - 1; 24 | 25 | openedList = new OpenedList(); 26 | closedList = new LinkedList(); 27 | 28 | } 29 | 30 | public List searchPath(Point startPos, Point destiPos) { 31 | Node startNode = new Node(startPos, col, row); 32 | Node destiNode = new Node(destiPos, col, row); 33 | startNode.sourcePoint = 0; 34 | startNode.destiPoint = startNode.GetCost(destiNode); 35 | startNode._parentnode = null; 36 | openedList.add(startNode); 37 | while (!openedList.isEmpty()) { 38 | // remove the initialized component 39 | Node firstNode = (Node) openedList.removeFirst(); 40 | // check the equality 41 | if (firstNode.equals(destiNode)) { 42 | // 43 | return makePath(firstNode); 44 | } else { 45 | // 46 | // add to the closedList 47 | closedList.add(firstNode); 48 | // get the mobile area of firstNode 49 | LinkedList _limit = firstNode.getLimit(); 50 | // visit 51 | for (int i = 0; i < _limit.size(); i++) { 52 | // get the adjacent node 53 | Node neighborNode = (Node) _limit.get(i); 54 | // 55 | boolean isOpen = openedList.contains(neighborNode); 56 | // check if it can work 57 | boolean isClosed = closedList.contains(neighborNode); 58 | // 59 | boolean isHit = isHit(neighborNode._Pos.x, neighborNode._Pos.y); 60 | // all of them are negative 61 | if (!isOpen && !isClosed && !isHit) { 62 | // set the costFromStart 63 | neighborNode.sourcePoint = firstNode.sourcePoint + 1; 64 | // set the costToObject 65 | neighborNode.destiPoint = neighborNode.GetCost(destiNode); 66 | // change the neighborNode's parent nodes 67 | neighborNode._parentnode = firstNode; 68 | // add to level 69 | openedList.add(neighborNode); 70 | } 71 | } 72 | } 73 | 74 | } 75 | // clear the data 76 | openedList.clear(); 77 | closedList.clear(); 78 | // 79 | return null; 80 | } 81 | 82 | private LinkedList makePath(Node node) { 83 | LinkedList path = new LinkedList(); 84 | while (node._parentnode != null) { 85 | path.addFirst(node); 86 | node = node._parentnode; 87 | } 88 | path.addFirst(node); 89 | return path; 90 | } 91 | 92 | private boolean isHit(int x, int y) { 93 | for (int i = 0; i < _limit.length; i++) { 94 | if (_map[y][x] == _limit[i]) { 95 | return true; 96 | } 97 | } 98 | return false; 99 | } 100 | 101 | private class OpenedList extends LinkedList { 102 | private static final long serialVersionUID = 1L; 103 | 104 | public boolean add(Node node) { 105 | for (int i = 0; i < size(); i++) { 106 | if (node.compareTo(get(i)) <= 0) { 107 | add(i, node); 108 | return false; 109 | } 110 | } 111 | addLast(node); 112 | 113 | return true; 114 | } 115 | } 116 | } -------------------------------------------------------------------------------- /src/main/resources/net/jmecn/map/creator/Name.properties: -------------------------------------------------------------------------------- 1 | # Outdoor 2 | creator.islands.cellauto=Islands(Cell Automata) 3 | creator.forest.hauberk=Forest(Lloyd's Algorithm) 4 | creator.city.leafvenation=City(Leaf Venation) 5 | 6 | # Cave 7 | creator.cave.cellauto=Cave(Cell Automata) 8 | creator.cave.dla=Diffusion-limited aggregation 9 | 10 | # Dungeon 11 | creator.dungeon.tyrant=Tyrant's Dungeon 12 | creator.dungeon.nickgravelyn=Nickgravelyn's dungeon 13 | creator.dungeon.yan=Yan's Dungeon 14 | creator.dungeon.hauberk=Hauberk's Dungeon 15 | 16 | # Building 17 | creator.building=Building(B-Tree) 18 | 19 | # Maze 20 | creator.maze=Maze(Connected graph) 21 | creator.maze.wilson=Wilson's Maze 22 | 23 | # Tiles 24 | tile.unused=Nothing. 25 | tile.dirtfloor=DirtFloor 26 | tile.dirtwall=DirtWall 27 | tile.stonewall=StoneWall 28 | tile.corridor=Corridor 29 | tile.door=Door 30 | tile.upstairs=UpStairs 31 | tile.downstairs=DownStairs 32 | tile.chest=Chest 33 | tile.desc=You see a {0} at location({1}, {2}). -------------------------------------------------------------------------------- /src/main/resources/net/jmecn/map/creator/Name_zh_CN.properties: -------------------------------------------------------------------------------- 1 | # Outdoor 2 | creator.islands.cellauto=\u5C9B\u5C7F(\u7EC6\u80DE\u81EA\u52A8\u673A) 3 | creator.forest.hauberk=\u68EE\u6797(Lloyd\u7B97\u6CD5) 4 | creator.city.leafvenation=\u57CE\u5E02(Leaf Venation) 5 | 6 | # Cave 7 | creator.cave.cellauto=\u6D1E\u7A74(\u7EC6\u80DE\u81EA\u52A8\u673A) 8 | creator.cave.dla=Diffusion-limited aggregation 9 | 10 | # Dungeon 11 | creator.dungeon.yan=Yan's\u5730\u7262 12 | creator.dungeon.tyrant=Tyrant\u5730\u7262 13 | creator.dungeon.nickgravelyn=Nickgravelyn\u5730\u7262 14 | creator.dungeon.hauberk=Hauberk\u5730\u7262 15 | 16 | # Building 17 | creator.building=\u5EFA\u7B51\u7269(\u4E8C\u53C9\u6811) 18 | 19 | # Maze 20 | creator.maze=\u8FF7\u5BAB(\u8FDE\u901A\u56FE) 21 | creator.maze.wilson=Wilson\u8FF7\u5BAB 22 | 23 | # Tiles 24 | tile.unused=Nothing. 25 | tile.dirtfloor=DirtFloor 26 | tile.dirtwall=DirtWall 27 | tile.stonewall=StoneWall 28 | tile.corridor=Corridor 29 | tile.door=Door 30 | tile.upstairs=UpStairs 31 | tile.downstairs=DownStairs 32 | tile.chest=Chest 33 | tile.desc=You see a {0} at location({1}, {2}). -------------------------------------------------------------------------------- /src/test/java/net/jmecn/map/Map2DTest.java: -------------------------------------------------------------------------------- 1 | package net.jmecn.map; 2 | 3 | public class Map2DTest { 4 | public static void main(String[] args) { 5 | Map2D map = new Map2D(40, 20); 6 | map.printMapArray(); 7 | } 8 | } 9 | --------------------------------------------------------------------------------