Skip to main content
replaced http://codereview.stackexchange.com/ with https://codereview.stackexchange.com/
Source Link

In an earlier questionearlier question, I showed my code for calculating the mine probability for each and every field in a Minesweeper board. But it doesn't stop there. In that very same Minesweeper Analyze project, there is also an algorithm for calculating the probability of each and every number for each and every field.

In an earlier question, I showed my code for calculating the mine probability for each and every field in a Minesweeper board. But it doesn't stop there. In that very same Minesweeper Analyze project, there is also an algorithm for calculating the probability of each and every number for each and every field.

In an earlier question, I showed my code for calculating the mine probability for each and every field in a Minesweeper board. But it doesn't stop there. In that very same Minesweeper Analyze project, there is also an algorithm for calculating the probability of each and every number for each and every field.

edited tags
Link
Simon Forsberg
  • 59.4k
  • 9
  • 156
  • 309
Tweeted twitter.com/#!/StackCodeReview/status/509307702041120768
Source Link
Simon Forsberg
  • 59.4k
  • 9
  • 156
  • 309

Advanced and Detailed Minesweeper Probabilities

In an earlier question, I showed my code for calculating the mine probability for each and every field in a Minesweeper board. But it doesn't stop there. In that very same Minesweeper Analyze project, there is also an algorithm for calculating the probability of each and every number for each and every field.

#Motivation

In the multiplayer version of Minesweeper called Minesweeper Flags, you have to be careful not to reveal too much information to your opponent. This is often what separates a good player from a bad inexperienced player. If there is a 80% chance of being a mine, but a 20% chance of revealing an open field (which could potentially reveal 10 obvious mines to your opponent), would you take the risk? Calculation of Expected Value (which is an entirely different topic in itself) tells us that it is probably a bad idea.

#Description

Consider this 8x7 Minesweeper board with a total of 6 mines:

8x7 Minesweeper board with the 1-3 sequence in the middle

When grouping the fields into FieldGroups (see previous question for a definition of field group), we find that there is:

  • 3 fields only connect to the 1 (b)
  • 4 fields connected to both 1 and 3 (c)
  • 3 fields only connected to the 3 (d)
  • 44 fields connected to neither (a)

The same map organized into field groups

When performing a mine-probability analyze on this board, we find that there are two Solution groups:

  • 4c = 1, 44a = 3, 3b = 0, 3d = 2, 158928 combinations (4 fields in group c should have one mine, 44 fields in group a should have 3 mines, etc.)
  • 4c = 0, 44a = 2, 3b = 1, 3d = 3, 2838 combinations

Now, what is the probability for the number '2' directly above the '1'? Let's call the field above the 1 as x. It would be possible to calculate that by adding a FieldRule to the existing board for the neighbors of x (2a + 3d + 1b = 2), and a rule for that field not being a mine (x = 0). But if we were to do this for each and every field on the board, it would take quite a lot of time. Not to mention that we have to do it for each and every number (0-8).

So, we can group the fields into probability-groups by which field groups they are neighboring to:

Field Groups     Probability Groups
  aaaaaaaa           abbbbbba
  aaaaaaaa           bcdefghb
  aabccdaa           bijklmnb
  aab13daa           bop13qrb
  aabccdaa           bijklmnb
  aaaaaaaa           bcdefghb
  aaaaaaaa           abbbbbba

As per the above 'diagram', the probability group k is where our field x belongs, it is neighboring the Field Groups 3*a, 2*b, 1*c. This information can be stored in the GroupValues structure that was introduced in the previous question, it is essentially a Map<FieldGroup, Integer>. To improve the lookup speed of it, it's hashCode value is stored once it is calculated for the first time, for better performance.

We can see that there are actually two k's on the probability groups map, so we gain some performance by grouping them together as they will have the same detailed probabilities. There are also 22 b's on the map so there's a lot of performance we gain there by having them share probabilities. Note that all fields in probability group b is neighboring 5 of FieldGroup a

#Calculating the detailed probabilities for a Probability Group

Let's focus on this solution for now: 4c = 1, 44a = 3, 3b = 0, 3d = 2, 158928 combinations

We know that the x field (in probability group k) has these neighbors: 3a, 2b, 1*c.

First we deal with 1c (because that's what the Eclipse debugger chose). Either that specific neighbor has a mine, or it does not. Let's say that it does not have a mine. Then there are 2 combinations, as there are two c fields away from field x, and one of them must have a mine.

Secondly, 3a. The a group has 44 fields in total, and in this solution should have 3 mines. This field is neighbor to 3 of those fields. Let's say that 1 of those neighbors are a mine, then there's (according to hypergeometric distribution) \$\binom{3}{1}*\binom{44 - 3}{3 - 1} = 2460\$ combinations.

Then, the neighbors 2b. We know from our solution though that there are no mines in b, so that's an easy one: One combination.

We're not neighbor with any d's so there's 3 combinations for them (3 fields, 2 mines).

So, for no mines in c, one mine in a and no mines in b, there are a total of one mines, and \$2 * 2460 * 1 * 3 = 14760\$ combinations. As there is currently no found mine around the x field we add 14760 to the combinations for our field x to be a 1.

This is done for all possible variations of how many mines is in each group. Once all the neighbor-groups and solutions has been processed, it is divided by the total number of combinations for the entire map to get the actual probabilities.

In the end, we get this double[] for the number probabilities for our field x: [0.4004549781783564, 0.2998343285980985, 0.05172285894440117, 0.0023552538852416455, 1.854530618300508E-5, 0.0, 0.0, 0.0, 0.0]

That is, 40% risk of being open field, 29.9% chance for a 1, 5.1% for a 2, etc...

#Speed reflection

There's one iteration over probability groups, one for solutions, and one for the neighboring FieldGroups, and then one recursive loop for a specific assignment. So it's something like \$O(probabilityGroups * solutions * K^{neighboringGroups})\$ Additionally, there's also some calculation of the actual combinations involved, using the binomial coefficient and hypergeometric distribution.

This causes huge performance issues when having many solutions, many field groups, and/or many probability groups - such as in The Super board of death, causing such boards to be left practically unsolved as it would take too much time to solve them. Luckily though, such situations rarely happen. When a bot is using this algorithm to play the bot simplifies the analyze required when taking mines. And in other occasions where this algorithm is used, it is mostly used to analyze games where the players are somewhat smart so the game rarely becomes too complex. And besides... this algorithm is faster than all other algorithms/approaches I know of.

###Typical values:

  • Field Groups tend to be between 10 - 50, in some rare cases up to 100.
  • Solution Groups tend to be between 2 - 50, in some games where two bad players are playing each other, it can easily go up to 1500 (and beyond).
  • Probability Groups tend to be around 50, in some cases up to 100.

This algorithm timeouts when these values get too big.

As games are played on a 16x16 board (at the moment at least, might support bigger in the future), the absolute maximum for Field Groups and Probability Groups is 256. For Solution Groups, there is no known maximum as games with a large amount of solution groups take too long to analyze even with 'simple' analyze (mine probabilities only).

###Class Summary (7 files)

  • DetailAnalyze: Entry-point, containing one static method to perform the analyze
  • DetailedResults: interface for accessing the results
  • DetailedResultsImpl: Implementation of the above interface
  • FieldProxy: Container for the detailed probabilities for a single field
  • NeighborFind: Interface to perform checking for neighbors and determining the current known mines around a field
  • ProbabilityKnowledge: The public interface of the FieldProxy class
  • ProxyProvider: Interface used while creating the results to access the data of other fields

#Dependencies

AnalyzeResult, Combinatorics, FieldGroup, GroupValues, RuntimeTimeoutException, Solution from the other parts of my Minesweeper Analyze project

#Code

The code is using Java 6, and can be found in my Minesweeper Analyze github repository

First of all, modified version of GroupValues

public class GroupValues<T> extends HashMap<FieldGroup<T>, Integer> {
    private static final long   serialVersionUID    = -107328884258597555L;
    private int bufferedHash = 0;
    
    public GroupValues(GroupValues<T> values) {
        super(values);
    }

    public GroupValues() {
        super();
    }

    @Override
    public int hashCode() {
        if (bufferedHash != 0) {
            return this.bufferedHash;
        }
        
        int result = super.hashCode();
        this.bufferedHash = result;
        return result;
    }
    
    public int calculateHash() {
        this.bufferedHash = 0;
        return this.hashCode();
    }

}

Then, the rest of the code:

DetailAnalyze.java: (62 lines, 2139 bytes)

/**
 * Creator of {@link DetailedResults} given an {@link AnalyzeResult} and a {@link NeighborFind} strategy
 * 
 * @author Simon Forsberg
 */
public class DetailAnalyze {
    public static <T> DetailedResults<T> solveDetailed(AnalyzeResult<T> analyze, NeighborFind<T> neighborStrategy) {
        // Initialize FieldProxies
        final Map<T, FieldProxy<T>> proxies = new HashMap<T, FieldProxy<T>>();
        for (FieldGroup<T> group : analyze.getGroups()) {
            for (T field : group) {
                FieldProxy<T> proxy = new FieldProxy<T>(group, field);
                proxies.put(field, proxy);
            }
        }
        
        // Setup proxy provider
        ProxyProvider<T> provider = new ProxyProvider<T>() {
            @Override
            public FieldProxy<T> getProxyFor(T field) {
                return proxies.get(field);
            }
        };
        
        // Setup neighbors for proxies
        for (FieldProxy<T> fieldProxy : proxies.values()) {
            fieldProxy.fixNeighbors(neighborStrategy, provider);
        }
        
        double totalCombinations = analyze.getTotal();
        Map<GroupValues<T>, FieldProxy<T>> bufferedValues = new HashMap<GroupValues<T>, FieldProxy<T>>();
        for (FieldProxy<T> proxy : proxies.values()) {
            // Check if it is possible to re-use a previous value
            FieldProxy<T> bufferedValue = bufferedValues.get(proxy.getNeighbors());
            if (bufferedValue != null && bufferedValue.getFieldGroup() == proxy.getFieldGroup()) {
                proxy.copyFromOther(bufferedValue, totalCombinations);
                continue;
            }
            
            // Setup the probabilities for this field proxy
            for (Solution<T> solution : analyze.getSolutionIteration()) {
                proxy.addSolution(solution);
            }
            proxy.finalCalculation(totalCombinations);
            bufferedValues.put(proxy.getNeighbors(), proxy);
        }
        
        int proxyCount = bufferedValues.size();
        
        return new DetailedResultsImpl<T>(analyze, proxies, proxyCount);
    }
}

DetailedResults.java: (46 lines, 1162 bytes)

/**
 * Interface for retreiving more detailed probabilities, for example 'What is the probability for a 4 on field x?'
 * 
 * @author Simon Forsberg
 *
 * @param <T> The field type
 */
public interface DetailedResults<T> {

    Collection<ProbabilityKnowledge<T>> getProxies();

    /**
     * Get the number of unique proxies that was required for the calculation. As some can be re-used, this will always be lesser than or equal to <code>getProxyMap().size()</code>
     * 
     * @return The number of unique proxies
     */
    int getProxyCount();

    /**
     * Get a specific proxy for a field
     * 
     * @param field
     * @return
     */
    ProbabilityKnowledge<T> getProxyFor(T field);

    /**
     * Get the underlying analyze that these detailed results was based on
     * 
     * @return {@link AnalyzeResult} object that is the source of this analyze
     */
    AnalyzeResult<T> getAnalyze();

    /**
     * @return The map of all probability datas
     */
    Map<T, ProbabilityKnowledge<T>> getProxyMap();

}

DetailedResultsImpl.java: (46 lines, 1211 bytes)

public class DetailedResultsImpl<T> implements DetailedResults<T> {

    private final AnalyzeResult<T> analyze;
    private final Map<T, ProbabilityKnowledge<T>> proxies;
    private final int proxyCount;

    public DetailedResultsImpl(AnalyzeResult<T> analyze, Map<T, FieldProxy<T>> proxies, int proxyCount) {
        this.analyze = analyze;
        this.proxies = Collections.unmodifiableMap(new HashMap<T, ProbabilityKnowledge<T>>(proxies));
        this.proxyCount = proxyCount;
    }
    
    @Override
    public Collection<ProbabilityKnowledge<T>> getProxies() {
        return Collections.unmodifiableCollection(proxies.values());
    }
    
    @Override
    public int getProxyCount() {
        return proxyCount;
    }

    @Override
    public ProbabilityKnowledge<T> getProxyFor(T field) {
        return proxies.get(field);
    }
    
    @Override
    public AnalyzeResult<T> getAnalyze() {
        return analyze;
    }
    
    @Override
    public Map<T, ProbabilityKnowledge<T>> getProxyMap() {
        return Collections.unmodifiableMap(proxies);
    }
}

FieldProxy.java: (182 lines, 5711 bytes)

public class FieldProxy<T> implements ProbabilityKnowledge<T> {
    
    private static int minK(int N, int K, int n) {
        // If all fields in group are neighbors to this field then all mines must be neighbors to this field as well
        return (N == K) ? n : 0;
    }
    
    private double[] detailedCombinations;
    private double[] detailedProbabilities;
    private final T field;
    private int found;
    private final FieldGroup<T> group;
    private final GroupValues<T> neighbors;
    
    public FieldProxy(FieldGroup<T> group, T field) {
        this.field = field;
        this.neighbors = new GroupValues<T>();
        this.group = group;
        this.found = 0;
    }
    
    void addSolution(Solution<T> solution) {
        recursiveRemove(solution.copyWithoutNCRData(), 1, 0);
    }
    
    /**
     * This field has the same values as another field, copy the values.
     * 
     * @param copyFrom {@link FieldProxy} to copy from
     * @param analyzeTotal Total number of combinations
     */
    void copyFromOther(FieldProxy<T> copyFrom, double analyzeTotal) {
        for (int i = 0; i < this.detailedCombinations.length - this.found; i++) {
            if (copyFrom.detailedCombinations.length <= i + copyFrom.found) {
                break;
            }
            this.detailedCombinations[i + this.found] = copyFrom.detailedCombinations[i + copyFrom.found];
        }
        
        this.finalCalculation(analyzeTotal);
    }
    
    /**
     * Calculate final probabilities from the combinations information
     * 
     * @param analyzeTotal Total number of combinations
     */
    void finalCalculation(double analyzeTotal) {
        this.detailedProbabilities = new double[this.detailedCombinations.length];
        for (int i = 0; i < this.detailedProbabilities.length; i++) {
            this.detailedProbabilities[i] = this.detailedCombinations[i] / analyzeTotal;
        }
    }
    
    /**
     * Setup the neighbors for this field
     * 
     * @param neighborStrategy {@link NeighborFind} strategy
     * @param proxyProvider Interface to get the related proxies
     */
    void fixNeighbors(NeighborFind<T> neighborStrategy, ProxyProvider<T> proxyProvider) {
        Collection<T> realNeighbors = neighborStrategy.getNeighborsFor(field);
        this.detailedCombinations = new double[realNeighbors.size() + 1];
        for (T neighbor : realNeighbors) {
            if (neighborStrategy.isFoundAndisMine(neighbor)) {
                this.found++;
                continue; // A found mine is not, and should not be, in a fieldproxy
            }
            
            FieldProxy<T> proxy = proxyProvider.getProxyFor(neighbor);
            if (proxy == null) {
                continue;
            }
            
            FieldGroup<T> neighborGroup = proxy.group;
            if (neighborGroup != null) {
                // Ignore zero-probability neighborGroups
                if (neighborGroup.getProbability() == 0) {
                    continue;
                }
                
                // Increase the number of neighbors
                Integer currentNeighborAmount = neighbors.get(neighborGroup);
                if (currentNeighborAmount == null) {
                    neighbors.put(neighborGroup, 1);
                }
                else neighbors.put(neighborGroup, currentNeighborAmount + 1);
            }
        }
    }
    
    @Override
    public T getField() {
        return this.field;
    }
    
    @Override
    public FieldGroup<T> getFieldGroup() {
        return this.group;
    }
    
    @Override
    public int getFound() {
        return this.found;
    }
    
    @Override
    public double getMineProbability() {
        return this.group.getProbability();
    }

    @Override
    public GroupValues<T> getNeighbors() {
        return this.neighbors;
    }

    @Override
    public double[] getProbabilities() {
        return this.detailedProbabilities;
    }

    private void recursiveRemove(Solution<T> solution, double combinations, int mines) {
        if (Thread.interrupted()) {
            throw new RuntimeTimeoutException();
        }
        
        // Check if there are more field groups with values
        GroupValues<T> remaining = solution.getSetGroupValues();
        if (remaining.isEmpty()) {
            this.detailedCombinations[mines + this.found] += combinations;
            return;
        }
        
        // Get the first assignment
        Entry<FieldGroup<T>, Integer> fieldGroupAssignment = remaining.entrySet().iterator().next();
        FieldGroup<T> group = fieldGroupAssignment.getKey();
        remaining.remove(group);
        solution = Solution.createSolution(remaining);
        
        // Setup values for the hypergeometric distribution calculation. See http://en.wikipedia.org/wiki/Hypergeometric_distribution
        int N = group.size();
        int n = fieldGroupAssignment.getValue();
        Integer K = this.neighbors.get(group);
        if (this.group == group) {
            N--; // Always exclude self becuase you can't be neighbor to yourself
        }
        
        if (K == null) {
            // This field does not have any neighbors to that group.
            recursiveRemove(solution, combinations * Combinatorics.nCr(N, n), mines);
            return;
        }
        
        // Calculate the values and then calculate for the next group
        int maxLoop = Math.min(K, n);
        for (int k = minK(N, K, n); k <= maxLoop; k++) {
            double thisCombinations = Combinatorics.NNKK(N, n, K, k);
            recursiveRemove(solution, combinations * thisCombinations, mines + k);
        }
    }
    
    @Override
    public String toString() {
        return "Proxy(" + this.field.toString() + ")"
                + "\n neighbors: " + this.neighbors.toString()
                + "\n group: " + this.group.toString()
                + "\n Mine prob " + this.group.getProbability() + " Numbers: " + Arrays.toString(this.detailedProbabilities);
    }
}

NeighborFind.java: (30 lines, 718 bytes)

/**
 * Interface strategy for performing a {@link DetailAnalyze}
 * 
 * @author Simon Forsberg
 *
 * @param <T> The field type
 */
public interface NeighborFind<T> {
    /**
     * Retrieve the neighbors for a specific field.
     * 
     * @param field Field to retrieve the neighbors for
     * 
     * @return A {@link Collection} of the neighbors that the specified field has
     */
    Collection<T> getNeighborsFor(T field);
    
    /**
     * Determine if a field is a found mine or not
     * 
     * @param field Field to check
     * 
     * @return True if the field is a found mine, false otherwise
     */
    boolean isFoundAndisMine(T field);
}

ProbabilityKnowledge.java: (39 lines, 1031 bytes)

public interface ProbabilityKnowledge<T> {

    /**
     * @return The field that this object has stored the probabilities for
     */
    T getField();
    
    /**
     * @return The {@link FieldGroup} for the field returned by {@link #getField()}
     */
    FieldGroup<T> getFieldGroup();
    
    /**
     * @return How many mines has already been found for this field
     */
    int getFound();
    
    /**
     * @return The mine probability for the {@link FieldGroup} returned by {@link #getFieldGroup()}
     */
    double getMineProbability();
    
    /**
     * @return {@link GroupValues} object for what neighbors the field returned by {@link #getField()} has
     */
    GroupValues<T> getNeighbors();
    
    /**
     * @return The array of the probabilities for what number this field has. The sum of this array + the value of {@link #getMineProbability()} will be 1.
     */
    double[] getProbabilities();
    
}

ProxyProvider.java: (7 lines, 132 bytes)

public interface ProxyProvider<T> {

    FieldProxy<T> getProxyFor(T field);
    
}

#Usage / Test

Available on Github: https://github.com/Zomis/Minesweeper-Analyze/blob/master/src/test/java/net/zomis/minesweeper/analyze/DetailAnalyzeTest.java

To see results of analyze in action, follow these steps:

A popup element will show the detailed probabilities for that field.

#Questions

In order of importance:

  1. Does another implementation of this exist? Are there any existing libraries out there?
  2. Any general comments welcome about this code and/or this approach
  3. Can it be made even faster? (except for some optimizations, I doubt it myself, but I would really love it if it could be improved significantly)