package org.artisanlogiciel.games.maze; import java.util.ArrayList; import java.util.LinkedList; import java.util.Random; /** * Model of labyrinth storing only paths not walls. wall are regenerated later * on based on adjacent paths. each position (x,y) stores what move can be done * from here to go to deeper path. a node is tagged OPEN and contained in * openList if all moves from its position have not been resolved a node is * tagged CLOSED when fully processed **/ public class LabyModel implements WallsProvider { /** * WARNING don't change those values, they are used as it is for * optimisation */ private final static short FLAGLENGTH = 7; private final static short CLEAR = 0; // mandatory 0 since array creation is // initialized with 0. public final static short HORIZONTAL = 1; public final static short VERTICAL = 2; public final static short DIRECTION = 4; // could we get rid of that to // free one bit for other purpose // ? // can be both POSITIVE and NEGATIVE, it means that you can move in positive direction and in negative direction. public final static short POSITIVE = 8; public final static short NEGATIVE = 16; // completed public final static short LEFT = Brick.LEFT << FLAGLENGTH | DIRECTION | HORIZONTAL | NEGATIVE; public final static short DOWN = Brick.DOWN << FLAGLENGTH | DIRECTION | VERTICAL | POSITIVE; public final static short RIGHT = Brick.RIGHT << FLAGLENGTH | DIRECTION | HORIZONTAL | POSITIVE; public final static short UP = Brick.UP << FLAGLENGTH | DIRECTION | VERTICAL | NEGATIVE; // flag when a wall should be open to access this for entry public final static short ENTRY = Brick.ENTRY << FLAGLENGTH; // flag when a wall should be open to access this for exit private final static short GOAL = Brick.GOAL << FLAGLENGTH; // can it be both OPEN and CLOSED ? private final static short OPEN = 32; // can be reused once generation is // completed private final static short CLOSED = 64; // can be reused once generation is // flag when solution is on this path. public final static short SOLVED = 64 << FLAGLENGTH; // free flag private final static short FREE = 128 << FLAGLENGTH; // remains 2 free bits ( keep one out for sign ) // orders matters see getReverseDirection public static final short[] AllDirections = {LEFT, DOWN, RIGHT, UP}; private int width; private int height; // wall flags Brick.(LEFT,DOWN,RIGHT,UP,ENTRY,GOAL) + status flags for each position (x,y) private short[][] t; private int depth = 0; /** * will enforce maxdepth as maximum length of path in depth first. It means * that after every maxdepth moves there should be crossing paths. I think * that to shorter maxdepth is the harder the labyrinth is ... **/ private int maxdepth = 0; // longest path found private int deepest = 0; // each move is a linearwork step private int linearwork = 0; private Position deepestEnd = null; boolean maxreached = false; java.util.Random random; // list of positions not fully walked private final LinkedList openList = new LinkedList(); // list of entries and exits. private final LinkedList entryExits = new LinkedList(); private final Object coherentLock = new Object(); // before getting the lock // and after lock release // all is coherent ( ie // check() is ok ), MazeCreationListener listener = null; private LabyModel(int width, int height, int maxdepth, Random random) { this.width = width; this.height = height; this.maxdepth = maxdepth; this.random = random; // CLEAR == 0 and array is initialized with 0s t = new short[width][height]; } public LabyModel(MazeParams params) { this(params.getWidth(), params.getHeight(), params.getMaxDepth(), new java.util.Random(params.getSeed())); } /** * construct LabyModel from an InputStream, yet only "raw" is supported **/ public LabyModel(int width, int heigh, short [][] t) { random = null; this.width = width; this.height = height; this.t = t; } public LabyModel(LabyModel other ) { random = null; this.width = other.width; this.height = other.height; this.t = other.t; } public void setMazeListener(MazeCreationListener listener) { this.listener = listener; } // FIXME FULLY BREAK RESOLVING... public void noWalls(int x, int y) { if ((x >= 0) && (x < width) && (y >= 0) && (y < height)) { // t[x][y] |= DIRECTION | VERTICAL | POSITIVE | HORIZONTAL | NEGATIVE | LEFT | RIGHT | UP | DOWN; t[x][y] |= LEFT | RIGHT | UP | DOWN; } } /* set direction existing onee are lost */ // FIXME FULLY BREAK RESOLVING... public void setDirection(int x, int y, short path) { if ((x >= 0) && (x < width) && (y >= 0) && (y < height)) { t[x][y] = (short) (path | OPEN); } } /** * add a new direction, exiting ones are kept */ // FIXME FULLY BREAK RESOLVING... public void addDirection(int x, int y, short path) { if ((x >= 0) && (x < width) && (y >= 0) && (y < height)) { t[x][y] |= (short) (path | OPEN); } } // entry and exit can be outside the model boundaries a one x or one y out. public boolean addEntryOrExit(int x, int y) { entryExits.add(new Position(x, y)); if ((x > 0) && (x < width) && (y > 0) && (y < height)) { t[x][y] |= ENTRY; } return true; } public void debugOut() { for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { out4XY(x, y); } System.out.print('\n'); } } public void out4XY(int x, int y) { int direction = t[x][y]; if ((direction & OPEN) == OPEN) { System.out.print("?"); } else if ((direction & CLOSED) == CLOSED) { System.out.print("."); } else { System.out.print(" "); } // don't display information about short. direction &= ~OPEN; direction &= ~GOAL; direction &= ~CLOSED; switch (direction) { case LEFT: // left System.out.print("<-"); break; case DOWN: // down System.out.print("vv"); break; case RIGHT: // right System.out.print("->"); break; case UP: // up System.out.print("^^"); break; case HORIZONTAL: // - System.out.print("--"); break; case VERTICAL: // | System.out.print("||"); break; case CLEAR: System.out.print(" "); break; case CLOSED: System.out.print("00"); break; case RIGHT | LEFT: System.out.print("--"); break; case UP | DOWN: System.out.print("||"); break; case RIGHT | UP: System.out.print("^>"); break; case RIGHT | DOWN: System.out.print("v>"); break; case LEFT | DOWN: System.out.print(""); break; case LEFT | RIGHT | UP | DOWN: System.out.print("++"); break; case OPEN: System.out.print("??"); break; case GOAL: System.out.print("**"); break; default: System.out.print(".."); } if (((t[x][y] & RIGHT) == RIGHT) || ((x + 1 < width) && ((t[x + 1][y] & LEFT) == LEFT))) { System.out.print("-"); } else { System.out.print("H"); } } public String outHorizWall2XY(int x, int y) { String freeway = " "; if ((t[x][y] & SOLVED) == SOLVED) { freeway = "*"; } if ((x < 0) || (x > width)) { return " H"; } if ((y < 0) || (y >= height)) { return "HH"; } if (((t[x][y] & DOWN) == DOWN) || ((y + 1 < height) && ((t[x][y + 1] & UP) == UP))) { return freeway + "H"; } else { return "HH"; } } public String out2XY(int x, int y) { // can check for entry exits. if ((y < 0) || (y >= height) || (x < 0) || (x > width)) { return " H"; } String low = ""; int direction = t[x][y]; String freeway = " "; if ((t[x][y] & SOLVED) == SOLVED) { freeway = "*"; } // don't display information about short. direction &= ~OPEN; direction &= ~GOAL; direction &= ~CLOSED; direction &= ~SOLVED; switch (direction) { case LEFT: case DOWN: case RIGHT: case UP: low = freeway; break; case HORIZONTAL: // - low = " "; break; case VERTICAL: // | low = " "; break; case CLEAR: low = " "; break; case CLOSED: low = "0"; break; case RIGHT | LEFT: case UP | DOWN: case RIGHT | UP: case RIGHT | DOWN: case LEFT | DOWN: case LEFT | UP: case LEFT | RIGHT | UP: case LEFT | RIGHT | DOWN: case LEFT | DOWN | UP: case RIGHT | DOWN | UP: case LEFT | RIGHT | UP | DOWN: low = "."; break; case OPEN: low = "?"; break; case GOAL: low = "*"; break; default: low = "."; } for (Position exit : entryExits) { if ((exit.getX() == x + 1) && (exit.getY() == y)) { low = low + ">"; return low; } } if (((t[x][y] & RIGHT) == RIGHT) || ((x + 1 < width) && ((t[x + 1][y] & LEFT) == LEFT))) { low = low + freeway; } else { low = low + "H"; } return low; } public LabyMap toLabyMap() { synchronized (coherentLock) { Brick[][] brickMap = new Brick[width][height]; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { brickMap[x][y] = new Brick(out2XY(x, y), outHorizWall2XY(x, y), getWalls(x, y)); } } return new LabyMap(brickMap, new LinkedList(entryExits)); } } public int getWidth() { return this.width; } public int getHeight() { return this.height; } public LinkedList getOpenList() { return openList; } public void reset() { depth = 0; computeOpenList(); resetResolving(); } public void resetResolving() { // don't keep solved path for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { t[x][y] &= ~(SOLVED); } } } public void fullReset() { depth = 0; openList.clear(); maxreached = false; deepest = 0; deepestEnd = null; // clear open for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { // resetCell(x,y); t[x][y] &= ~(OPEN | SOLVED); } } } /** * check model coherency and fix if model needed a fix it means that * algorithm has a problem * * @return true if model is ok and no fix was applied. **/ public boolean check() { boolean check = true; synchronized (coherentLock) { // node in OPEN should be tagged OPEN and not CLOSED. for (Position p : openList) { int x = p.getX(); int y = p.getY(); if (isFlagSet(t[x][y], OPEN)) { check = false; t[x][y] |= OPEN; } if (isFlagSet(t[x][y], CLOSED)) { check = false; t[x][y] &= ~CLOSED; } } // should do reverse : every node tagged OPEN should be in open // list. // a little more cpu consuming... } return check; } // coherency : if CLOSED then not OPEN public void computeOpenList() { openList.clear(); for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { if (isFlagSet(t[x][y], CLOSED)) { t[x][y] &= ~OPEN; } if (isFlagSet(t[x][y], OPEN) || (t[x][y] == CLEAR)) { openList.add(new Position(x, y)); } } } } /** * This is core of generator. *

* generate a maze with an entry set at (x,y) with a maxdepth **/ public void generateWithEntry(int x, int y) { openList.add(new Position(x, y)); while (!openList.isEmpty()) { // this is where magic happens step(); synchronized (coherentLock) { linearwork++; if (linearwork % maxdepth == 0) { coherentLock.notifyAll(); if (listener != null) { listener.changed(null, null, this); } // should not continue in depth first... if (!openList.isEmpty()) { // insert head as next position to pick up. Position current = openList.removeFirst(); openList.add(current); } } } } if (deepestEnd != null) { t[deepestEnd.getX()][deepestEnd.getY()] |= GOAL; } } public int getDepth() { return depth; } public Position getDeepestEnd() { return deepestEnd; } public int getDeepestPath() { return deepest; } public boolean maxReached() { return maxreached; } /** * @param p Position * @return true if newly added , false if already open. **/ private boolean open(boolean first, Position p) { int x = p.getX(); int y = p.getY(); if ((t[x][y] & OPEN) != OPEN) { t[x][y] |= OPEN; openList.addLast(p); return true; } return false; } public final static boolean isFlagSet(short check, short flag) { return ((check & flag) == flag); } public final short getCell(Position p) { return t[p.getX()][p.getY()]; } public final void updateCell(Position p, short flags) { t[p.getX()][p.getY()] |= flags; } public final static short getReverseDirection(int index) { return AllDirections[(index + 2) % 4]; } /** * return direction to use to be closer to 'to' from 'from' */ public final static short getDirection(Position from, Position to) { short pointingdirection = DIRECTION; if (from.equals(to)) { return pointingdirection; } if (from.getX() < to.getX()) { pointingdirection |= RIGHT; } else if (from.getX() > to.getX()) { pointingdirection |= LEFT; } if (from.getY() < to.getY()) { pointingdirection |= DOWN; } else if (from.getY() > to.getY()) { pointingdirection |= UP; } return pointingdirection; } private final void closePosition(int x, int y) { t[x][y] &= ~OPEN; t[x][y] |= CLOSED; } /** * One step in Maze generation process * get last element in open list and explore one random direction with it. * * @returns whether process closed current node. **/ public boolean step() { boolean complete = false; Position current = null; synchronized (coherentLock) { if (!openList.isEmpty()) { // last : in depth before. current = openList.getLast(); } else { return true; } if (current != null) { int x = current.getX(); int y = current.getY(); // should find all free positions... ArrayList freeDirection = new ArrayList(); for (short direction : AllDirections) { int delta = 0; int newx = -1; int newy = -1; if ((direction & POSITIVE) == POSITIVE) { delta = 1; } else { delta = -1; } if ((direction & HORIZONTAL) == HORIZONTAL) { newx = x + delta; newy = y; } else { newy = y + delta; newx = x; } if ((newx >= 0) && (newy >= 0) && (newx < width) && (newy < height)) { if ((t[newx][newy]) == CLEAR) { freeDirection.add(Short.valueOf(direction)); } } } if (!freeDirection.isEmpty()) { // control random using our own pseudorandom short direction = 0; if (freeDirection.size() > 1) { direction = freeDirection.get(random.nextInt(freeDirection.size())); } else { direction = freeDirection.get(0); Position last = openList.removeLast(); if (last != current) { // GUARD, should not happen ( or multi-thread access // to model error ) System.err.println("INTERNAL ERROR 3"); return false; } } int delta = 0; int newx = -1; int newy = -1; if ((direction & POSITIVE) == POSITIVE) { delta = 1; } else { delta = -1; } if ((direction & HORIZONTAL) == HORIZONTAL) { newx = x + delta; newy = y; } else { newy = y + delta; newx = x; } Position target = new Position(newx, newy, current.getDepth() + 1); open(false, target); if ((t[x][y] & DIRECTION) == DIRECTION) { t[x][y] |= direction; } else { t[x][y] = direction; } // not a 'direction' ... is it necessary to check ? if (freeDirection.size() > 1) { // keep it open at the very same place, previous open // did add another node to inspect. return false; } // else this was the unique direction , object already // removed ( since if we try to remove it now it is not the // last ) // can proceed to close } else { // no free direction remaining => closing Position last = openList.removeLast(); if (last != current) { // GUARD, should not happen. System.err.println("INTERNAL ERROR 3"); return false; } } complete = true; closePosition(x, y); final int currentdepth = current.getDepth(); if (currentdepth > deepest) { deepest = currentdepth; deepestEnd = current; } } // current != null; } // synchronized return complete; } public short getWalls(int x, int y) { short walls = 0; for (short direction : AllDirections) { if (hasWallInDirection(x, y, direction)) { walls |= (direction >> FLAGLENGTH); } } return walls; } public short getPath(int x, int y) { short path = 0; if ((x < width) && (y < height)) { path = t[x][y]; } return path; } /** * is there a wall in that direction ? **/ public boolean hasWallInDirection(int x, int y, short direction) { int newx = 0; int newy = 0; int delta = 0; // best way to find reversedirection ? int reversedirection = 0; // is this direction on the path ? yes => no wall if ((t[x][y] & direction) == direction) { return false; } // is adjacent tile in direction pointing in reverse direction ? yes => // no wall if ((direction & POSITIVE) == POSITIVE) { delta = 1; } else { delta = -1; } if ((direction & HORIZONTAL) == HORIZONTAL) { newx = x + delta; newy = y; reversedirection = (direction == RIGHT) ? LEFT : RIGHT; } else { newy = y + delta; newx = x; reversedirection = (direction == UP) ? DOWN : UP; } if ((newx >= 0) && (newy >= 0) && (newx < width) && (newy < height)) { return !isFlagSet(t[newx][newy], (short) reversedirection); } else { // outside boundaries. // TODO CHECK exits. return true; } } /** * is there no wall in that direction ? **/ public boolean canMoveInDirection(int x, int y, short direction) { // tried to replace by but does not work ( can't go back ...). // return ! hasWallInDirection(x,y, (short) (direction << FLAGLENGTH)); return ((getWalls(x, y) & direction ) == 0); } }