/** * Copyright © 2011,2013 Konstantin Livitski * * This file is part of n-Puzzle application. n-Puzzle application is free * software; you can redistribute it and/or modify it under the terms of the GNU * General Public License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * n-Puzzle application contains adaptations of artwork covered by the Creative * Commons Attribution-ShareAlike 3.0 Unported license. Please refer to the * NOTICE.md file at the root of this distribution or repository for licensing * terms that apply to that artwork. * * n-Puzzle application is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License along with * n-Puzzle application; if not, see the LICENSE/gpl.txt file of this distribution * or visit <http://www.gnu.org/licenses>. */ package name.livitski.games.puzzle.android.model; import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Random; import java.util.Set; import java.util.SortedMap; import java.util.StringTokenizer; import java.util.TreeMap; /** * Represents an n-Puzzle gaming board. */ public class Board { /** * Returns this board's {@link #Board(int) size}. */ public int getSize() { return layout.length; } /** * Returns the number of tiles on this board, including * the blank tile. */ public int getTileCount() { return tiles.length; } /** * Returns the number of tiles currently at their * {@link Tile#isOnTarget() target places} on this board. */ public int getScore() { int score = 0; for (Tile tile : tiles) if (tile.isOnTarget()) score++; return score; } /** * Returns the tile at a specific location. * @throws IllegalStateException if the board is empty * @throws IllegalArgumentException if the location is invalid */ public Tile getTileAt(int row, int col) { if (0 > row || layout.length <= row) throw new IllegalArgumentException("Invalid row index " + row); if (0 > col || layout.length <= col) throw new IllegalArgumentException("Invalid column index " + col); Tile tile = layout[row][col]; if (null == tile) throw new IllegalStateException("Cannot point at a tile: the board is empty"); return tile; } /** * Returns a string of tile numbers for all board positions. * The numbers are comma-separated. The sequence is left-to-right * for tiles in a row, then top to bottom for rows. * @return a string of tile numbers * @throws IllegalStateException if the board is empty */ public String getTileLayout() { StringBuilder buffer = new StringBuilder(3 * getTileCount()); final int size = getSize(); for (int i = 0; i < size; i++) for (int j = 0; j < size; j++) buffer.append(',').append(getTileAt(i, j).getNumber()); return 0 == buffer.length() ? "" : buffer.substring(1); } /** * Returns the move of a blank field that will swap it * with this tile according to the rules. Returns * <code>null</code> if this tile is not a neighbor * of the blank tile, or if it denotes the blank field. * @throws IllegalArgumentException if the tile * does not belong to this board's tile set */ public Move permittedMoveFor(final Tile tile) { final int number = tile.getNumber(); if (0 > number || tiles.length <= number || tile != tiles[number]) throw new IllegalArgumentException(tile.toString()); final int yoffset = tile.getRow() - blank.getRow(); final int xoffset = tile.getColumn() - blank.getColumn(); Move move = null; switch(xoffset) { case 0: // same column switch (yoffset) { case -1: // tile is above the blank move = Move.UP; break; case 1: // tile is below the blank move = Move.DOWN; break; } break; case 1: case -1: // horizontal moves are allowed within a single row if (0 == yoffset) move = 0 > xoffset ? Move.LEFT : Move.RIGHT; break; } return move; } /** * Moves a tile according to the rules. * @throws IllegalStateException if the board is empty * @throws IllegalArgumentException if requested move would push * the blank tile outside the board */ public void move(Move direction) { int row = blank.getRow(); int col = blank.getColumn(); if (0 > row || 0 > col) throw new IllegalStateException("Cannot make a move: the board is empty"); if (direction.isHorizontal()) { col += direction.getAmount(); if (0 > col || layout.length <= col) throw new IllegalArgumentException("Cannot make a " + direction + " to column " + col); } else if (direction.isVertical()) { row += direction.getAmount(); if (0 > row || layout.length <= row) throw new IllegalArgumentException("Cannot make a " + direction + " to row " + row); } else throw new UnsupportedOperationException("Unimplemented " + direction); Tile tile = layout[row][col]; swapTiles(tile, blank); noitfyMoveListeners(blank, tile); } public void addMoveListener(MoveListener listener) { listeners.add(listener); } public void addTileOnTargetListener(final TileOnTargetListener listener) { for (Tile tile : tiles) tile.addOnTargetListener(listener); } public void forEachTile(TileHandler handler) { for (Tile tile : tiles) handler.processTile(tile); } /** * Places tiles on the board in the reverse order. */ protected void placeTilesReverse() { int row, col; row = col = 0; final int size = getSize(); for (int i = getTileCount(); 0 < i--;) { Tile tile = tiles[i]; placeTile(tile, row, col); if (size <= ++col) { row++; col = 0; assert size > row; } } // For even board sizes, swap tiles 1 and 2 if (0 == layout.length % 2) // We don't allow board size of 0, so if the size is even, there will always be tiles 1 and 2 swapTiles(tiles[1], tiles[2]); } /** * Places tiles on the board in the solved puzzle order. */ protected void placeTilesOnTarget() { forEachTile(new TileHandler() { public void processTile(Tile tile) { placeTile(tile, tile.getTargetRow(), tile.getTargetColumn()); } }); } /** * Places tiles on the board according to a * {@link #getTileLayout() layout string). */ protected void placeTiles(String layout) { StringTokenizer numbers = new StringTokenizer(layout, ","); final int size = getSize(); Set<Integer> placedTileNos = new HashSet<Integer>(getTileCount(), 1f); for (int i = 0, j = 0; numbers.hasMoreTokens();) { int number = Integer.parseInt(numbers.nextToken().trim()); if (!placedTileNos.add(number)) throw new IllegalArgumentException("Tile " + number + " has already been placed"); placeTile(tiles[number], i, j); if (size <= ++j) { j = 0; ++i; } } if (placedTileNos.size() < getTileCount()) throw new IllegalArgumentException("Some tiles have not been placed, expected " + getTileCount() + " tile(s), placed " + placedTileNos.size()); } /** * Places tiles on the board in a random order. */ protected void placeTilesRandom() { // obtain a random arrangement of tiles Random random = new Random(); // assign each tile a different random key SortedMap<Integer, Tile> randomOrder = new TreeMap<Integer, Tile>(); for (int i = getTileCount(); 0 < i--;) { Integer key; do key = random.nextInt(); while (randomOrder.containsKey(key)); randomOrder.put(key, tiles[i]); } // place tiles on the board in order of their random keys final int size = getSize(); Iterator<Tile> sequence = randomOrder.values().iterator(); for (int i = 0; i < size; i++) for (int j = 0; j < size; j++) placeTile(sequence.next(), i, j); // check if this arrangement is solvable (Calabro, 2005) int distanceMod2 = (blank.getRow() + blank.getColumn()) % 2; if (computePermutationSign() != distanceMod2) // if not, swap two non-blank tiles swapTiles(tiles[1], tiles[2]); } /** * Determines whether current placement of tiles is even. * For the explanation of this algorithm, please refer to: * Chris Calabro, Solving the 15-Puzzle, June 14, 2005. * (retrieved Mar, 9 2011 from http://cseweb.ucsd.edu/~ccalabro/essays/15_puzzle.pdf) */ protected int computePermutationSign() { final int tileCount = getTileCount(); final int size = getSize(); // construct a permutation in a 1-based array int[] permutation = new int[tileCount + 1]; int i = 1; for (int row = 0; row < size; row++) for (int col = 0; col < size; col++) { int number = layout[row][col].getNumber(); if (0 == number) number = tileCount; permutation[i++] = number; } // run Calabro's algorithm over the permutation and return its result int s = 0; for (i = 1; tileCount >= i;) if (i != permutation[i]) { int temp = permutation[i]; permutation[i] = permutation[temp]; permutation[temp] = temp; s = 1 - s; } else i++; return s; } protected void noitfyMoveListeners(Tile from, Tile to) { for (MoveListener l : listeners) l.tileMoved(from, to); } protected void establishTarget(Tile tile) { int number = tile.getNumber(); if (0 == number) number = getTileCount(); number--; final int edgeSize = getSize(); tile.target(number / edgeSize, number % edgeSize); } /** * Constructs an empty board of specified size and the associated * set of tiles. Before starting a game, one must place tiles on the * board. * @param edgeSize the number of rows (and columns) on the board */ protected Board(int edgeSize) { if (0 >= edgeSize || Integer.MAX_VALUE <= (long)edgeSize * edgeSize) throw new IllegalArgumentException("edgeSize = " + edgeSize); final int tileCount = edgeSize * edgeSize; this.layout = new Tile[edgeSize][edgeSize]; this.tiles = new Tile[tileCount]; for (int i = 0; tileCount > i; i++) establishTarget(tiles[i] = new Tile(i)); this.blank = tiles[0]; } protected interface TileHandler { void processTile(Tile tile); } private void swapTiles(Tile tile1, Tile tile2) { int row1 = tile1.getRow(); int col1 = tile1.getColumn(); placeTile(tile1, tile2.getRow(), tile2.getColumn()); placeTile(tile2, row1, col1); } private void placeTile(Tile tile, int row, int col) { tile.place(row, col); layout[row][col] = tile; } private final Tile[][] layout; private final Tile[] tiles; private final Tile blank; private final List<MoveListener> listeners = new ArrayList<MoveListener>(); }