/**
* Copyright (C) 2010, LGPL
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* @author Matfyz, fyzmat@gmail.com
*/
package mines;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.List;
import mines.MinefieldContainer.AROUND_FIELDS_DISTANCE;
import mines.MinefieldContainer.CENTER_OPTION;
import mines.MinefieldContainer.Coord;
/**
* Class, which marks determinable mined and numbered fields. It uses optimized
* depth search algorithm (backjumping). A heuristic algorithm, which does
* the depth search only on limited area, is also implemented.
*/
public class MineSolver {
/**
* Class mainly for debugging purposes. It can log happening in the solver
* and can be turned on/off. The logger generates a log file as an overview
* and saves state of minefield during specified processes.
*/
private class Logger {
/** Directory path for debug prints. */
String mDirPath = null;
/** Output stream for the log file. */
PrintStream mLogFile = null;
/** Counter of saved log records. */
int mLogCounter = 0;
/** Flag for enabling the logger. */
boolean mEnabled = false;
/** Name of the log file. */
final static String LOG_FILENAME = "log.txt";
/** Prefix of filenames, which contain saved minefield. */
final static String MINEFIELD_DATA_PREFIX = "minefield_";
/**
* Initializes the logger.
* @param logDir Path to the directory with logger outputs.
*/
public Logger(String logDir){
mDirPath = logDir;
// create the log directory if it does not exist
File dir = new File(mDirPath);
if(!dir.exists() || !dir.isDirectory()) {
dir.mkdir();
}
try {
mLogFile = new PrintStream(mDirPath + "/" + LOG_FILENAME);
} catch (FileNotFoundException e) {
throw new MinesSolverInternalError(e.getMessage());
}
}
/**
* Enables or disables the logging.
* @param enabled True to enable the logging.
*/
public void setEnabled(boolean enabled) {
mEnabled = enabled;
}
/**
* Puts the message to the log file.
* @param message A message to be recorded.
*/
public void Log(String message) {
if(!mEnabled) {
return;
}
mLogFile.println("" + mLogCounter + ": " + message);
mLogCounter++;
}
/**
* Saves given minefield to file.
* @param filename Filename, where the minefield will be stored.
* @param data Saved minefield.
*/
void saveMinefieldDataToFile(String filename, MinefieldContainer<GUI_FIELD> data) {
try {
ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream(filename));
os.writeObject(data);
os.close();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* Log message and saves the given minefield to file. The number of message
* in log and the number in saved filename matches.
* @param message Message to be logged.
* @param data Minefield to be saved.
*/
public void Log(String message, MinefieldContainer<SOLVER_FIELD> data) {
if(!mEnabled) {
return;
}
// put the message to the log file
mLogFile.println("" + mLogCounter + "< " + message);
mLogFile.flush();
// save the minefield
String dataFilename = String.format("%06d", mLogCounter);
MinefieldContainer<GUI_FIELD> guiData = MinesUtils.convertSolverToGUIMinefield(mContent);
saveMinefieldDataToFile(mDirPath + "/" + MINEFIELD_DATA_PREFIX +
dataFilename + MINESOLVER_DATA_SUFFIX, guiData);
mLogCounter++;
}
}
/**
* Error to signalize, that the current state of minefield is in an inconsistent
* state, or that something logically bad has happened.
*/
public static class MinesSolverInternalError extends Error {
/**
* Initializes the exception
* @param message The error message
*/
public MinesSolverInternalError(String message) {
super(message);
}
}
/**
* Exception for stopping the algorithm. In the time consuming parts of algorithm
* there is a check, if somebody wants to stop the algorithm. In such
* case the algorithm is aborted by throwing this exception.
*/
public static class ComputationAbortedException extends Exception { }
/** Suffix for filenames, where are stored the logged minefields. */
public final static String MINESOLVER_DATA_SUFFIX = ".mdd";
/** Directory for output of solver activities. */
public final static String MINESOLVER_DATA_DIR = "./solver_logs";
/** The logger object. */
private Logger mLogger = new Logger(MINESOLVER_DATA_DIR);
/** The minefield data structure, used for this algorithm. It holds covered
fields, numbered fields, covered fields which are marked as numbered (clear)
and covered fields, which are marked as mined. Every field, which is marked
as mined, decreases value of uncovered numbered fields in its neighbourhood
by one from their real value. */
private MinefieldContainer<SOLVER_FIELD> mContent = null;
/** Flag, which is checked in time consuming parts to stop the computations. */
private boolean mAborted = false;
/**
* Enables or disables logging of the solver activities.
* @param enable True, to enable.
*/
public void enableLogging(boolean enable) {
mLogger.setEnabled(enable);
}
/**
* Aborts solver execution. The exection will be aborder as soon as the
* solver detects, that it should abort the computations.
*/
public void abort() {
mAborted = true;
}
/**
* Sets the minefield for the solver.
* @param content Minefield used as the solver data. It uses its own internal
* copy of given minefield.
*/
public void setContent(MinefieldContainer<SOLVER_FIELD> content) {
mContent = new MinefieldContainer<SOLVER_FIELD>(content);
// we need to decrease the value of numbered fields according to the
// mined fields
List<Coord> numberCoords = mContent.getAllFieldsIndexes(SOLVER_FIELD.NUMBER_FLAGS);
for (Coord numberCoord : numberCoords) {
updateNumberField(numberCoord);
}
}
/**
* Counts the number of mines in the neighbourhood of given numbered field
* and decrease the value of the number by that count.
* @param updatedCoord Coordinate of numbered field.
*/
void updateNumberField(Coord updatedCoord) {
// get the current value of the numbered field
SOLVER_FIELD numberValue = mContent.get(updatedCoord);
if(!numberValue.isFiltered(SOLVER_FIELD.NUMBER_FLAGS)) {
throw new MinesSolverInternalError("Unexpected field value " + numberValue);
}
// count the mines in the around
List<Coord> minedField = mContent.getAroundFields(updatedCoord, AROUND_FIELDS_DISTANCE.ONE,
SOLVER_FIELD.MINE_FLAG, CENTER_OPTION.EXCLUDE_CENTER);
int alreadyLayedMinesCount = minedField.size();
// change the value of the numbered field
SOLVER_FIELD updatedValue = SOLVER_FIELD.addToNumber(numberValue, -alreadyLayedMinesCount);
mContent.set(updatedCoord, updatedValue);
}
/**
* Checks, if a mine can be layed on the given field. This only checks, if there
* is a zero number in the neighbourhood of the given field.
* @param coord Coordinates to be checked for mine.
* @return True, if the mine can be layed in the given coordinates from the view
* of distance one.
*/
private boolean canPutMine(Coord coord) {
SOLVER_FIELD fieldValue = mContent.get(coord);
if(!fieldValue.equals(SOLVER_FIELD.COVERED)) {
throw new MinesSolverInternalError("Expected covered field at " + coord);
}
// check, if there is a zero field in the neighbourhood
List<Coord> numFields = mContent.getAroundFields(coord, SOLVER_FIELD.NUMBER_FLAGS);
for(Coord numField : numFields) {
SOLVER_FIELD numValue = mContent.get(numField);
if(numValue.equals(SOLVER_FIELD.ZERO)) {
// here is the zero, the mine cannot be layed here
return false;
}
}
return true;
}
/**
* Puts mine to given coordinates. Then it decreases the uncovered numbered
* fields in the neighbourhood by one.
* @param coord Coordinates to put the mine.
*/
private void putMine(Coord coord) {
if(!canPutMine(coord)) {
throw new MinesSolverInternalError("Cannot put mine at " + coord);
}
// put the mine
mContent.set(coord, SOLVER_FIELD.MINE);
// decrease the numbered fields
List<Coord> numFields = mContent.getAroundFields(coord, SOLVER_FIELD.NUMBER_FLAGS);
for(Coord numCoord : numFields)
{
SOLVER_FIELD numValue = mContent.get(numCoord);
numValue = SOLVER_FIELD.addToNumber(numValue, -1);
mContent.set(numCoord, numValue);
}
}
/**
* Picks a mine on given coordinates. Then it increases the value of numbered
* fields in the neighbourhood by one.
* @param coord Coordinates to pick up a mine.
*/
private void pickUpMine(Coord coord) {
SOLVER_FIELD fieldValue = mContent.get(coord);
if(!fieldValue.equals(SOLVER_FIELD.MINE)) {
throw new MinesSolverInternalError("Expected mine field at " + coord);
}
// pick up the mine
mContent.set(coord, SOLVER_FIELD.COVERED);
// increase the numbered fields by one
List<Coord> numFields = mContent.getAroundFields(coord, SOLVER_FIELD.NUMBER_FLAGS);
for(Coord numField : numFields) {
SOLVER_FIELD numValue = mContent.get(numField);
if(numValue.equals(SOLVER_FIELD.TWELVE)) {
// hint - possible inconsistence in minefield generator
throw new MinesSolverInternalError("Mine cannot be picked up at " + coord);
}
numValue = SOLVER_FIELD.addToNumber(numValue, +1);
mContent.set(numField, numValue);
}
}
/**
* Wrapper for return value of minefield consistency checking method {@link
* #isMinefieldConsistent isMinefieldConsistent}.
*/
private class SearchResultWrapper {
/** True, if the searched layout of mines was found. */
public boolean mIsConsistent = false;
/** True, if searched layout of mines was not found and there is a need to
remove additional mines in order to have a chance to find such layout. */
public boolean mCanBackjump = false;
/** Count of mines, which must be removed, minus one. Minus one because,
it is suitable for implementation in {@link #isMinefieldConsistent} method. */
public int mBackJumpLength = -1;
}
/**
* <P>Tries to find out, if the current minefield is consistent. Does the recursive
* depth first search with backjumping optimilization.</P>
*
* <P>Minefield consistency definition:
* A minefield is called consistent, if there is such layout of mines on the
* covered fields, that every uncovered numbered field has the same count of mines
* in its neighbourhood as its number. </P>
*
* <P>Testing, if a minefield is consistent is an NP complete problem. See
* <A href="http://web.mat.bham.ac.uk/R.W.Kaye/minesw/">this</A> for more information</P>
*
* <P>In this environment we also consider, that
* there can be some fixed mines on the minefield (layed by previous phases of
* this algorithm, by the user or by something else) and some uncovered numbered
* fields can be ignored (for reduction of search complexity).</P>
*
* <P>The depth first search without the backjumping:</P>
*
* <P>Algorithm tries to lay mines around every numbered field in the affected fields
* in such way, that every numbered field has count of mines in its neighbourhood
* same as its number.</P>
*
* <P>In each step of recursion we choose one nonzero field
* from the affected fields and try to put one mine in its covered neighbourhood (this
* would fail immedeately, when there would be some numbered field with more mines
* in its neighbourhood than its number). Then we use this same method for asking,
* if the minefield with the currently layed mine is consistent. Thus the recursion
* goes deeper.</P>
*
* <P>In each step of recursion we try to put the mine in the all fields in
* covered neighbourhood in such described way. This ensures, that all the
* possibilities are searched.</P>
*
* <P>Putting the mine means decreasing the value of numbered fields in the mine neighbourhood
* by one, so the minefield is consistent, if all the affected fields have zero value.</P>
*
* <P>The backjumping optimilization:</P>
*
* <P>The technique is generally described <a href="http://en.wikipedia.org/wiki/Backjumping">here</a>.</P>
*
* <P>This technique is used when we cannot put a mine to all the covered fields
* in neighbourhood of nonzero affected field, which is used in current
* level of recursion. That means from the view of current level of recursion, that
* the minefield is not consistent.</P>
*
* <P>In such situation we simulate, that the recursion is going up by removing
* the layed mines. After each removal of a mine we use some quick (polynomial)
* function to test, whether the minefield is still inconsistent. The depth
* first search will return up to the first level of recursion, where the test function
* has not proven minefield inconsistency.</P>
*
* <P>Statement: Described modified version of the depth first search algorithm
* does not miss any searched layout of mines.</P>
*
* <P>Proof: If the minefield with already layed (fixed) mines is inconsistent,
* then there is no searched layout of mines, which contains those fixed layed
* mines. Thus every other minefield, which would contain those fixed mines and
* some additional mines, would be inconsistent.</P>
*
* <P>The testing function for minefield inconsistency is {@link #checkOneField checkOneField}.</P>
*
* @param minedFields Coordinates of mines, which has already been layed during
* the consistency checking.
* @param affectedFields Uncovered numbered fields, which are taken into account
* for consistency checking. The rest of uncovered numbered fields is ignored.
* @return The value indicating consistency and, if there is a need to backjump.
* @throws mines.MineSolver.ComputationAbortedException If the search is aborted
* possibly for taking too much time.
*/
private SearchResultWrapper isMinefieldConsistent(List<Coord> minedFields, List<Coord> affectedFields)
throws ComputationAbortedException {
if(mAborted) {
mAborted = false;
throw new ComputationAbortedException();
}
// take the first non zero field
Coord nextNonzeroField = null;
for (Coord affected : affectedFields) {
SOLVER_FIELD fieldValue = mContent.get(affected);
if(fieldValue.isFiltered(SOLVER_FIELD.NON_ZERO_NUMBER_FLAG)) {
nextNonzeroField = affected;
break;
}
}
if (nextNonzeroField == null) {
// all the affected fields contains zeroes, so we have just found
// the searched layout of mines
SearchResultWrapper result = new SearchResultWrapper();
result.mIsConsistent = true;
result.mCanBackjump = false;
return result;
}
// we have a numbered nonzero field, we will try to put a mine somewhere
// into its neighbourhood
List<Coord> coveredFields = mContent.getAroundFields(nextNonzeroField, SOLVER_FIELD.COVERED_FLAG);
for(Coord coveredField : coveredFields) {
if(!canPutMine(coveredField)) {
// zero in the neighbourhood of covered field
// we cannot put a mine there
continue;
}
putMine(coveredField);
minedFields.add(coveredField);
mLogger.Log("Putting mine at " + coveredField, mContent);
SearchResultWrapper searchResult = isMinefieldConsistent(minedFields, affectedFields);
if(searchResult.mIsConsistent) {
// the searched layout was found somewhere in the deep
// lets get the minefield to the original state
pickUpMine(coveredField);
mLogger.Log("Is consistent - picking up at" + coveredField, mContent);
return searchResult;
}
// putting the mine here was a bad step
minedFields.remove(minedFields.size() - 1);
pickUpMine(coveredField);
mLogger.Log("Picking up at " + coveredField, mContent);
// hopefully it was determined in the deeper levels of recursion, that
// we does not need to continue searching here
if(!searchResult.mCanBackjump) {
continue;
}
// lets backjump
// mBackJumpLength == 0 means, that one mine needs to be picked up
// this mine has already been picked up, so for this case we will
// continue the cycle and leave recursion at the same level
if(searchResult.mBackJumpLength > 0) {
searchResult.mBackJumpLength--;
return searchResult;
}
}
// we have searched all the possibilities and we have not found the layout
// the minefield is in the inconsistent state, lets find where to backjump
int backjumpLength = countBackjumpLength(minedFields, nextNonzeroField);
SearchResultWrapper result = new SearchResultWrapper();
result.mIsConsistent = false;
result.mCanBackjump = true;
result.mBackJumpLength = backjumpLength;
return result;
}
/**
* Searches all the uncovered numbered fields fcr fields, which have equal
* count of covered fields in theirs neighbourhood as theirs number. The whole
* neighbourhood of all of these fields is marked as mined.
*
* @param affectedFields Uncovered numbered fields, which are taken into account
* for consistency checking. The rest of uncovered numbered fields is ignored.
* @return True, if at least one field has been marked as mined.
*/
boolean findApparentMinedFields(List<Coord> affectedFields) {
boolean change = false;
for(Coord affectedField : affectedFields) {
List<Coord> coveredAroundFields =
mContent.getAroundFields(affectedField, SOLVER_FIELD.COVERED_FLAG);
SOLVER_FIELD affectedValue = mContent.get(affectedField);
assert(affectedValue.isFiltered(SOLVER_FIELD.NUMBER_FLAGS));
// check, if the whole neighbourhood must be mined
if(affectedValue.convert() == coveredAroundFields.size()) {
for(Coord aroundField : coveredAroundFields) {
putMine(aroundField);
change = true;
}
mLogger.Log("Apparent mines around " + affectedField, mContent);
}
}
return change;
}
/**
* Searches for covered fields, which must be numbered. For each covered field
* in the neighbourhood of the affected fields it tries to prove, that the covered
* field is numbered. This proof has polynomial complexity and therefore it is not
* guarantied, that every determinable numbered field will be found. During this proof
* we put a mine to the tested covered field and test the minefield consistency.
* The consistency test runs only on the limited area of affected fields, so
* its time complexity is bounded by some constant.
*
* @param affectedFields Uncovered numbered fields, which are used for getting the
* area of searched covered fields.
* @return True, if at least one field has been marked as mined.
* @throws mines.MineSolver.ComputationAbortedException If the search is aborted
* possibly for taking too much time.
*/
boolean findApparentClearFields(List<Coord> affectedFields) throws ComputationAbortedException {
boolean change = false;
// take all the affected fields
for(Coord affectedField : affectedFields) {
// get the limited area of affected fields for taken affected fields
List<Coord> limitedAreaAff = mContent.getAroundFields(affectedField,
AROUND_FIELDS_DISTANCE.TWO, SOLVER_FIELD.NON_ZERO_NUMBER_FLAG, CENTER_OPTION.INCLUDE_CENTER);
List<Coord> coveredFields = mContent.getAroundFields(affectedField, SOLVER_FIELD.COVERED_FLAG);
// try to put a mine on on each covered field and check the consistency
for(Coord coveredField : coveredFields) {
List<Coord> minedFields = new ArrayList<Coord>();
if(!canPutMine(coveredField)) {
// there is an uncovered zero in the neighbourhood
mContent.set(coveredField, SOLVER_FIELD.CLEAR);
change = true;
mLogger.Log("Apparent clear at " + coveredField, mContent);
continue;
}
// put the mine and run the consistency check
putMine(coveredField);
minedFields.add(coveredField);
SearchResultWrapper result = isMinefieldConsistent(minedFields, limitedAreaAff);
minedFields.remove(coveredField);
pickUpMine(coveredField);
if(!result.mIsConsistent) {
mContent.set(coveredField, SOLVER_FIELD.CLEAR);
change = true;
mLogger.Log("Apparent clear at " + coveredField, mContent);
}
}
}
return change;
}
/**
* Tries to mark mined and numbered fields. It uses only heuristics,
* so it is not guarantied, that all of the determinable fields will be marked.
* This function has polynomial time complexity and it should be called
* before the NP-complete searching algorithms.
*
* @throws mines.MineSolver.ComputationAbortedException If the evaluation is aborted
* possibly for taking too much time.
*/
public void findApparentFields() throws ComputationAbortedException {
boolean change;
List<Coord> affectedFields = mContent.getAllFieldsIndexes(SOLVER_FIELD.NON_ZERO_NUMBER_FLAG);
do {
change = findApparentMinedFields(affectedFields);
change = findApparentClearFields(affectedFields);
} while(change);
}
/**
* Gets the count of mines, which must be picked up, when the minefield
* is inconsistent. Otherwise the minefield would be always inconsistent.
*
* @param minedFields Coordinates of layed mines in the order from the firstly
* layed mine to the latest layed mine from the view of depth first search algorithm.
* @param affectedField Uncovered numbered fields, which are taken into account
* for consistency checking. The rest of uncovered numbered fields is ignored.
* @return Number of mines, which must be picked up, minus one.
*/
private int countBackjumpLength(List<Coord> minedFields, Coord affectedField) {
int backjumpLength = 0;
int minesCount = minedFields.size();
int pickedUpMines = 0;
// we will pick up the mines one by one in the reverse order and
// test the minefield inconsistency each time
for(int i = 0; i < minesCount; i++) {
int lastIndex = (minesCount - 1) - i;
Coord minedCoord = minedFields.get(lastIndex);
pickUpMine(minedCoord);
pickedUpMines++;
if(checkOneField(affectedField)) {
// the minefield has a chance to be consistent now
// we will store the number of picked up mines
backjumpLength = pickedUpMines - 1;
break;
}
}
// now we have to put the removed mines back
for(int i = 0; i < pickedUpMines; i++)
{
int lastIndex = (minesCount - 1) - i;
Coord minedCoord = minedFields.get(lastIndex);
if(!canPutMine(minedCoord)) {
throw new MinesSolverInternalError("Unable to put back removed mines.");
}
putMine(minedCoord);
}
return backjumpLength;
}
/**
* Checks, if it is possible for given numbered field to put the appropriate
* count of mines around it. Does the exhaustive search, which takes at maximum
* ${m}\\choose{n}$ mine putting and picking up operations,
* where n is the count of mines and m is the count of covered fields.
*
* @param coord Coordinates of uncovered numbered field.
* @return True, if it is possible to put mines around given field.
*/
private boolean checkOneField(Coord coord) {
SOLVER_FIELD fieldNum = mContent.get(coord);
assert(fieldNum.isFiltered(SOLVER_FIELD.NUMBER_FLAGS));
List<Coord> coveredFields = mContent.getAroundFields(coord, SOLVER_FIELD.COVERED_FLAG);
boolean result = checkOneField(coord, coveredFields);
return result;
}
/**
* Checks, if it is possible for given numbered field to put the appropriate
* count of mines around it. Does the exhaustive search, which takes at maximum
* ${m}\\choose{n}$ mine putting and picking up operations,
* where n is the count of mines and m is the count of covered fields.
*
* @param coord Coordinates of uncovered numbered field.
* @param coveredFields List of covered field in the neighbourhood of
* the affected field.
* @return True, if it is possible to put mines around given field.
*/
private boolean checkOneField(Coord coord, List<Coord> coveredFields) {
//try to put mine on every covered field
for(Coord coveredField : coveredFields) {
if(canPutMine(coveredField)) {
putMine(coveredField);
// if the number is already zero, then we have just layed all
// the mines
SOLVER_FIELD numberValue = mContent.get(coord);
if(numberValue.equals(SOLVER_FIELD.ZERO)) {
pickUpMine(coveredField);
return true;
}
// lay next mine
if(checkOneField(coord))
{
// all mines has been layed
pickUpMine(coveredField);
return true;
}
// pick up the layed mine and try another covered field
pickUpMine(coveredField);
}
}
return false;
}
/**
* Checks, if there can be a mine on given covered field. It puts a mine
* on given field and checks the minefield consistency. If the minefield
* is consistent, the mine can be on given field.
*
* @param coord Coordinates of a covered field, which will be checked.
* @return True, a mine can be layed on the given field.
* @throws mines.MineSolver.ComputationAbortedException If the evaluation is aborted
* possibly for taking too much time.
*/
public boolean checkForMine(Coord coord) throws ComputationAbortedException {
SOLVER_FIELD fieldValue = mContent.get(coord);
if (!fieldValue.equals(SOLVER_FIELD.COVERED)) {
throw new IllegalArgumentException(coord.toString());
}
// a mine can be layed into the middle of covered fields
List<Coord> aroundNumericalFields = mContent.getAroundFields(coord,
AROUND_FIELDS_DISTANCE.ONE, SOLVER_FIELD.NUMBER_FLAGS, CENTER_OPTION.EXCLUDE_CENTER);
if(aroundNumericalFields.size() == 0) {
return true;
}
if(!canPutMine(coord)) {
// there is a zero in the neighbourhood of the tested field
return false;
}
// get the fields, which will be searched
List<Coord> affectedFields = MinesUtils.getAffectedFields(mContent, coord);
// we will start the searching
// we will put a mine on the tested field and run the consistency check
List<Coord> minedFields = new ArrayList<Coord>();
putMine(coord);
minedFields.add(coord);
SearchResultWrapper result = isMinefieldConsistent(minedFields, affectedFields);
pickUpMine(coord);
return result.mIsConsistent;
}
/**
* Checks, if the given field can be numbered field. It puts a clear mark
* on given field and checks the minefield consistency. If the minefield
* is consistent, the given field can be numbered.
*
* @param coord Coordinates of the field, which will be tested.
* @return True, if the given field can be numbered field.
* @throws mines.MineSolver.ComputationAbortedException If the evaluation is aborted
* possibly for taking too much time.
*/
public boolean checkForClear(Coord coord) throws ComputationAbortedException {
SOLVER_FIELD fieldValue = mContent.get(coord);
if (!fieldValue.equals(SOLVER_FIELD.COVERED)) {
throw new IllegalArgumentException(coord.toString());
}
if(!canPutMine(coord)) {
// the tested field even must be numbered
return true;
}
// we will start the searching
// we will put a clear mark on the tested field and run the consistency check
mContent.set(coord, SOLVER_FIELD.CLEAR);
List<Coord> affectedFields = MinesUtils.getAffectedFields(mContent, coord);
List<Coord> minedFields = new ArrayList<Coord>();
SearchResultWrapper result = isMinefieldConsistent(minedFields, affectedFields);
mContent.set(coord, SOLVER_FIELD.COVERED);
return result.mIsConsistent;
}
/**
* Return type for method {@link #solveOneField solveOneField}. It says, if there must
* be a mine on the specified coordinates or, if there must not be a mine or,
* that the solver cannot say anything about the field.
*
*/
public enum FIELD_SOLVE_RESULT {
UNKNOWN,
MUST_BE_MINE,
CANNOT_BE_MINE
}
/**
* This is used for asking the solver, if the specified field must be mined,
* must be numbered or it is not determinable.
*
* @param coord Covered field to check.
* @return Appropriate value of the {@link FIELD_SOLVE_RESULT enumeration}.
* @throws mines.MineSolver.ComputationAbortedException If the evaluation is aborted
* possibly for taking too much time.
*/
public FIELD_SOLVE_RESULT solveOneField(Coord coord) throws ComputationAbortedException {
SOLVER_FIELD fieldValue = mContent.get(coord);
mLogger.Log("Solving " + coord, mContent);
switch(fieldValue) {
case MINE:
return FIELD_SOLVE_RESULT.MUST_BE_MINE;
case CLEAR:
return FIELD_SOLVE_RESULT.CANNOT_BE_MINE;
case COVERED:
// this covered field has not been determined yet
// check, if a mine can be layed on it and if the field
// can be numbered
mLogger.Log("Checking for mine at " + coord, mContent);
if(!checkForMine(coord)) {
mLogger.Log("Found clear field.");
mContent.set(coord, SOLVER_FIELD.CLEAR);
return FIELD_SOLVE_RESULT.CANNOT_BE_MINE;
}
mLogger.Log("Checking for clear at " + coord, mContent);
if(!checkForClear(coord)) {
mLogger.Log("Found mined field.");
putMine(coord);
return FIELD_SOLVE_RESULT.MUST_BE_MINE;
}
return FIELD_SOLVE_RESULT.UNKNOWN;
default:
mLogger.Log("Unexpected field value " + fieldValue + " at " + coord, mContent);
throw new IllegalArgumentException("Unexpected field value " +
fieldValue + " at " + coord);
}
}
/**
* Uncovers the covered field by the given number.
*
* @param coveredCoord Coordinates of covered field to be revealed.
* @param numberValue Number value to be set instead of the covered field.
* This value does not depend on the count of determined surrounding mines.
*/
public void revealField(Coord coveredCoord, SOLVER_FIELD numberValue) {
SOLVER_FIELD currentValue = mContent.get(coveredCoord);
if(!currentValue.isFiltered(SOLVER_FIELD.NOT_REVEALED_FLAGS)) {
mLogger.Log("Reveal unexpected field " + currentValue + " at +" + coveredCoord);
throw new MinesSolverInternalError("Reveal unexpected field " + currentValue);
}
mLogger.Log("Reveal " + coveredCoord, mContent);
// set the value of the covered field and decrease it by the count
// of surrounding mines
mContent.set(coveredCoord, numberValue);
updateNumberField(coveredCoord);
}
/**
* For testing purposes only.
*/
public void testPutMine(Coord coord) {
putMine(coord);
}
/**
* For testing purposes only.
*/
public int testFindBadRecursionDeep(List<Coord> minedCoords, Coord numCoord) {
return countBackjumpLength(minedCoords, numCoord);
}
/**
* For testing purposes only.
*/
public boolean testCheckOneField(Coord numCoord) {
return checkOneField(numCoord);
}
}