package mines;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import com.trolltech.qt.gui.QMessageBox;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.Stack;
import mines.MinefieldContainer.AROUND_FIELDS_DISTANCE;
import mines.MinefieldContainer.CENTER_OPTION;
import mines.MinefieldContainer.Coord;
/**
* Class of static methods usefull for operations with the minefield.
*/
public class MinesUtils {
/**
* <P>Container of the real and the viewed content of a minefield. Can be saved
* to a file.</P>
*
* <P>TODO: This container is able to be stored to a file because of the serialization
* mechanism. Currently the default serialization is used. This makes troubles, when
* some changes in class structures are made, so it is handy to make the
* serialization independent from class structure.</P>
*/
public static class MineSweeperData implements Serializable {
private static final long serialVersionUID = -2157772593193389032L;
/** Real content of the minefield data */
public MinefieldContainer<GUI_FIELD> mRealContent = null;
/** Viewed content of the minefield data */
public MinefieldContainer<GUI_FIELD> mViewedContent = null;
public MineSweeperData() {}
/**
* Copy constructor.
* @param copy Original mine sweeper data object.
*/
public MineSweeperData(MineSweeperData copy) {
mRealContent = new MinefieldContainer<GUI_FIELD>(copy.mRealContent);
mViewedContent = new MinefieldContainer<GUI_FIELD>(copy.mViewedContent);
}
}
/**
* Gets coordinates of all the fields from minefield with given size.
* @param size Size of minefield.
* @return List of coordinates of all the fields.
*/
public static List<Coord> getAllFieldsCoords(int size) {
List<Coord> result = new ArrayList<Coord>();
for(int i = 0; i < size; i++) {
int columns = size - i;
int row = i * 2;
// down
for (int j = 0; j < columns; j++) {
Coord coord = new Coord(row, j);
result.add(coord);
}
// up
for (int j = 0; j < columns - 1; j++) {
Coord coord = new Coord(row + 1, j);
result.add(coord);
}
}
return result;
}
/**
* Founds out, if the specified row has fields with up orientation or down
* orientation.
* @param row Row of minefield.
* @return True, if the row contains fields with the down orientation.
*/
public static boolean isDownButton(int row) {
return (row % 2 == 0);
}
/**
* <P>Counts the number of fields in a minefield with the given size.</P>
*
* <P>Statement: The triangle minefield of size $n$ has $n^2$ fields.</P>
*
* <P>Proof: We will divide rows into two groups - rows with the down orientation
* and with the up orientation. There are $2n-1$ rows - $n$ of them are down oriented
* and $n-1$ of them are up oriented. We will count the number of fields via
* the sums of fields contained in these groups of rows. The count of fields
* in down rows is $\sum_{i=0}^{n} i$ and the count of fields in up rows gives
* $\sum_{i=1}^{n-1} i$. This gives together $n^2$.</P>
*
* @param size Size of minefield
* @return Count of fields in specified minefield.
*/
public static int getFieldsCount(int size) {
return size * size;
}
/**
* Counts the number of flagged covered fields on the minefield.
* @param minefield Minefield, where the flags will be counted.
* @return Count of flags.
*/
public static int countFlaggedFields(MinefieldContainer<GUI_FIELD> minefield) {
List<Coord> indexes = minefield.getAllFieldsIndexes(GUI_FIELD.FLAG_FLAG);
return indexes.size();
}
/**
* Lays randomly given number of mines on the given minefield.
*
* @param minefield Minefield, where the mines will be layed.
* @param mines Count of mines to lay.
*/
public static void layMinesRandomly(MinefieldContainer<GUI_FIELD> minefield, int mines) {
int fieldsCount = MinesUtils.getFieldsCount(minefield.getSize());
if (mines > fieldsCount) {
throw new Error("Internal error, more mines than fields: " + mines);
}
// initialize the minefield
minefield.initialFill(GUI_FIELD.COVERED);
// we will take all the coordinates and remove the mined ones
Random random = new Random();
List<Coord> coveredFields = minefield.getAllFieldsIndexes(GUI_FIELD.ALL_FLAGS);
for(int i = 0; i < mines; i++) {
// take a random number from interval <0; count of covered fields - 1>
// and lay a mine according to it
int fieldOrder = (Math.abs(random.nextInt()) % coveredFields.size());
Coord minedCoord = coveredFields.get(fieldOrder);
assert(minefield.get(minedCoord).equals(GUI_FIELD.COVERED));
minefield.set(minedCoord, GUI_FIELD.MINE);
// removed the mined field
coveredFields.remove(fieldOrder);
}
}
/**
* For each non mined field it counts the count of mines in the neighbourhood
* of that field and sets the value of that field same as the count of the mines.
* @param minefield Minefield for updating the numbered fields.
*/
public static void updateFieldsNumbers(MinefieldContainer<GUI_FIELD> minefield) {
List<Coord> allFieldsCoords = getAllFieldsCoords(minefield.getSize());
for (Coord coord : allFieldsCoords) {
GUI_FIELD current_type = minefield.get(coord);
// ignore mined fields
if (current_type.equals(GUI_FIELD.MINE)) {
continue;
}
List<Coord> minedCoords = minefield.getAroundFields(coord.mX, coord.mY,
AROUND_FIELDS_DISTANCE.ONE, GUI_FIELD.MINE_FLAG);
int aroundMinesCount = minedCoords.size();
GUI_FIELD new_type = GUI_FIELD.getButtonTypeFromMinesCount(aroundMinesCount);
minefield.set(coord, new_type);
}
}
/**
* Saves given minefield to given file. Eventual exception is shown in a message box.
* @param data Minefield to save.
* @param filename Filename for storing the minefield.
*/
public static void saveMinefieldToFileSafely(MineSweeperData data, String filename) {
try {
ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream(filename));
os.writeObject(data);
os.close();
} catch (Exception e) {
e.printStackTrace();
QMessageBox errDialog = new QMessageBox(QMessageBox.Icon.Warning, "Unable to save minefield.", e.getMessage());
errDialog.exec();
}
}
/**
* Loads serialized object from file. Eventual exception is shown in a message box.
* @param <T> Class of the object.
* @param filename Filename to load from.
* @return The loaded object.
*/
public static <T> T loadObjectFromFileSafely(String filename) {
try {
ObjectInputStream is = new ObjectInputStream(new FileInputStream(filename));
@SuppressWarnings("unchecked")
T result = (T) is.readObject();
is.close();
return result;
} catch (Exception e) {
e.printStackTrace();
QMessageBox errDialog = new QMessageBox(QMessageBox.Icon.Warning, "Unable to load minefield.", e.getMessage());
errDialog.exec();
return null;
}
}
/** Conversion map between {@link mines.GUI_FIELD} and {@link mines.SOLVER_FIELD}. */
private static Map<GUI_FIELD, SOLVER_FIELD> mGUItoSolverDataConversionMap = loadGUItoSolverConversion();
/** Conversion map between {@link mines.SOLVER_FIELD} and {@link mines.GUI_FIELD}. */
private static Map<SOLVER_FIELD, GUI_FIELD> mSolvertoGUIDataConversionMap = loadSolvertoGUIConversion();
/**
* Fills the conversion map between gui and solver. There are some unconvertable
* values.
* @return The conversion map.
*/
private static Map<GUI_FIELD, SOLVER_FIELD> loadGUItoSolverConversion() {
Map<GUI_FIELD, SOLVER_FIELD> result = new HashMap<GUI_FIELD, SOLVER_FIELD>();
result.put(GUI_FIELD.ZERO, SOLVER_FIELD.ZERO);
result.put(GUI_FIELD.ONE, SOLVER_FIELD.ONE);
result.put(GUI_FIELD.TWO, SOLVER_FIELD.TWO);
result.put(GUI_FIELD.THREE, SOLVER_FIELD.THREE);
result.put(GUI_FIELD.FOUR, SOLVER_FIELD.FOUR);
result.put(GUI_FIELD.FIVE, SOLVER_FIELD.FIVE);
result.put(GUI_FIELD.SIX, SOLVER_FIELD.SIX);
result.put(GUI_FIELD.SEVEN, SOLVER_FIELD.SEVEN);
result.put(GUI_FIELD.EIGHT, SOLVER_FIELD.EIGHT);
result.put(GUI_FIELD.NINE, SOLVER_FIELD.NINE);
result.put(GUI_FIELD.TEN, SOLVER_FIELD.TEN);
result.put(GUI_FIELD.ELEVEN, SOLVER_FIELD.ELEVEN);
result.put(GUI_FIELD.TWELVE, SOLVER_FIELD.TWELVE);
result.put(GUI_FIELD.COVERED, SOLVER_FIELD.COVERED);
result.put(GUI_FIELD.MARKED_CLEAR, SOLVER_FIELD.CLEAR);
result.put(GUI_FIELD.MARKED_MINE, SOLVER_FIELD.MINE);
result.put(GUI_FIELD.FLAG, SOLVER_FIELD.COVERED);
return result;
}
/**
* Fills the conversion map between solver and gui.
* @return The conversion map.
*/
private static Map<SOLVER_FIELD, GUI_FIELD> loadSolvertoGUIConversion() {
Map<SOLVER_FIELD, GUI_FIELD> result = new HashMap<SOLVER_FIELD, GUI_FIELD>();
result.put(SOLVER_FIELD.ZERO, GUI_FIELD.ZERO);
result.put(SOLVER_FIELD.ONE, GUI_FIELD.ONE);
result.put(SOLVER_FIELD.TWO, GUI_FIELD.TWO);
result.put(SOLVER_FIELD.THREE, GUI_FIELD.THREE);
result.put(SOLVER_FIELD.FOUR, GUI_FIELD.FOUR);
result.put(SOLVER_FIELD.FIVE, GUI_FIELD.FIVE);
result.put(SOLVER_FIELD.SIX, GUI_FIELD.SIX);
result.put(SOLVER_FIELD.SEVEN, GUI_FIELD.SEVEN);
result.put(SOLVER_FIELD.EIGHT, GUI_FIELD.EIGHT);
result.put(SOLVER_FIELD.NINE, GUI_FIELD.NINE);
result.put(SOLVER_FIELD.TEN, GUI_FIELD.TEN);
result.put(SOLVER_FIELD.ELEVEN, GUI_FIELD.ELEVEN);
result.put(SOLVER_FIELD.TWELVE, GUI_FIELD.TWELVE);
result.put(SOLVER_FIELD.COVERED, GUI_FIELD.COVERED);
result.put(SOLVER_FIELD.CLEAR, GUI_FIELD.MARKED_CLEAR);
result.put(SOLVER_FIELD.MINE, GUI_FIELD.MARKED_MINE);
return result;
}
/**
* Converts the value of solver field to gui field.
* @param data Value to convert.
* @return Appropriate gui field value.
*/
public static GUI_FIELD convertSolverToGUIData(SOLVER_FIELD data) {
assert(mSolvertoGUIDataConversionMap.containsKey(data));
return mSolvertoGUIDataConversionMap.get(data);
}
/**
* Converts the value of gui field to solver field.
* @param data Value to convert.
* @return Appropriate solver field value.
*/
public static SOLVER_FIELD convertGUIToSolverData(GUI_FIELD data) {
assert(mGUItoSolverDataConversionMap.containsKey(data));
return mGUItoSolverDataConversionMap.get(data);
}
/**
* Converts all the gui minefield to the solver minefield.
* @param GUIMinefield GUI minefield to convert.
* @return Appropriate solver minefield.
*/
public static MinefieldContainer<SOLVER_FIELD> convertGUIToSolverMinefield(
MinefieldContainer<GUI_FIELD> GUIMinefield) {
int minefieldSize = GUIMinefield.getSize();
MinefieldContainer<SOLVER_FIELD> solverMinefield = new MinefieldContainer<SOLVER_FIELD>(minefieldSize);
List<Coord> allCoords = MinesUtils.getAllFieldsCoords(minefieldSize);
// set the solver fields according to the appropriate gui fields
for(Coord coord : allCoords) {
GUI_FIELD guiValue = GUIMinefield.get(coord);
SOLVER_FIELD solverValue = convertGUIToSolverData(guiValue);
solverMinefield.set(coord, solverValue);
}
return solverMinefield;
}
/**
* Converts all the solver minefield to the gui minefield.
* @param solverMinefield Solver minefield to convert.
* @return Appropriate gui minefield.
*/
public static MinefieldContainer<GUI_FIELD> convertSolverToGUIMinefield(
MinefieldContainer<SOLVER_FIELD> solverMinefield) {
int minefieldSize = solverMinefield.getSize();
MinefieldContainer<GUI_FIELD> GUIMinefield = new MinefieldContainer<GUI_FIELD>(minefieldSize);
List<Coord> allCoords = MinesUtils.getAllFieldsCoords(minefieldSize);
// set the gui fields according to the appropriate solver fields
for(Coord coord : allCoords) {
SOLVER_FIELD solverValue = solverMinefield.get(coord);
GUI_FIELD guiValue = MinesUtils.convertSolverToGUIData(solverValue);
GUIMinefield.set(coord, guiValue);
}
return GUIMinefield;
}
/**
* <P>Gets the indepent area of fields, which can affect, whether on the given covered
* field must or must not be a mine. Used for reducing solver search complexity.</P>
*
* <P>Observation: The continous area of covered fields with the minimum width of two
* fields separates uncovered numbered fields to areas, which does not affect each other.
* That means, that minefield consistency of one such area does not depend on
* values of numbers in all the other areas.</P>
*
* <P>This function get union of all such areas, which are at boundary (distance 2)
* of given covered field.</P>
*
* @param minefield Minefield used for getting the affected fields.
* @param coveredField Field, where it is required to check, if it can be mined
* of numbered.
* @return Coordinates of uncovered numbered fields, which can affected the
* mine consistency of the searched area.
*/
public static List<Coord> getAffectedFields(MinefieldContainer<SOLVER_FIELD> minefield,
Coord coveredField) {
List<Coord> result = new ArrayList<Coord>();
Set<Coord> affectedFieldsSet = new HashSet<Coord>();
Stack<Coord> areaSearchStack = new Stack<Coord>();
// wave algorithm; start with the covered field, but do not add it to the
// result; in each iteration there will be added uncovered numbered fields
// with distance two of the taken field to the result; no coordinates
// are added twice
areaSearchStack.push(coveredField);
while(!areaSearchStack.isEmpty()) {
// remove taken field from stack and add it to the set to prevent
// the duplicities
Coord areaField = areaSearchStack.pop();
affectedFieldsSet.add(areaField);
List<Coord> aroundFields = minefield.getAroundFields(areaField, AROUND_FIELDS_DISTANCE.TWO,
SOLVER_FIELD.NON_ZERO_NUMBER_FLAG, CENTER_OPTION.EXCLUDE_CENTER);
// add new uncovered numbered fields to the stack and the result
for(Coord aroundField : aroundFields) {
if(affectedFieldsSet.contains(aroundField)) {
// around field has already been added
continue;
}
affectedFieldsSet.add(aroundField);
areaSearchStack.push(aroundField);
result.add(aroundField);
}
}
return result;
}
/**
* Gets the continous area of zero fields and non-zero fields, which are in the
* neighbourhood of that area. Usefull when the user reveals the zero field.
* Works recursively.
*
* @param searchedCoord Coordinates of next expansion of area to be revealed.
* @param alreadyFoundFields Coordinates of fields, which has already been
* added in the searched area.
* @param viewedMinefield To prevent revealing the uncovered.
* @param realMinefield The real content of minefield.
*/
public static void getRevealArea(Coord searchedCoord, Set<Coord> alreadyFoundFields,
MinefieldContainer<GUI_FIELD> viewedMinefield,
MinefieldContainer<GUI_FIELD> realMinefield) {
// ignore not covered fields
GUI_FIELD viewedValue = viewedMinefield.get(searchedCoord);
if (!viewedValue.isFiltered(GUI_FIELD.COVERED_AND_MARKED_CLEAR_FLAGS)) {
return;
}
// add nonzero covered fields
GUI_FIELD type = realMinefield.get(searchedCoord.mX, searchedCoord.mY);
if (!type.equals(GUI_FIELD.ZERO)) {
alreadyFoundFields.add(searchedCoord);
return;
}
// add zero covered fields and all the covered around fields
alreadyFoundFields.add(searchedCoord);
List<Coord> aroundCoords = realMinefield.getAroundFields(searchedCoord.mX, searchedCoord.mY);
for (Coord coord : aroundCoords) {
if (!alreadyFoundFields.contains(coord)) {
getRevealArea(coord, alreadyFoundFields, viewedMinefield,
realMinefield);
}
}
}
}