Add References to git

This commit is contained in:
2023-09-28 20:23:18 +08:00
parent 4d51eb339c
commit 50b5f8c2c1
276 changed files with 56093 additions and 2 deletions

63
info/Minesweeper2/.gitattributes vendored Normal file
View File

@ -0,0 +1,63 @@
###############################################################################
# Set default behavior to automatically normalize line endings.
###############################################################################
* text=auto
###############################################################################
# Set default behavior for command prompt diff.
#
# This is need for earlier builds of msysgit that does not have it on by
# default for csharp files.
# Note: This is only used by command line
###############################################################################
#*.cs diff=csharp
###############################################################################
# Set the merge driver for project and solution files
#
# Merging from the command prompt will add diff markers to the files if there
# are conflicts (Merging from VS is not affected by the settings below, in VS
# the diff markers are never inserted). Diff markers may cause the following
# file extensions to fail to load in VS. An alternative would be to treat
# these files as binary and thus will always conflict and require user
# intervention with every merge. To do so, just uncomment the entries below
###############################################################################
#*.sln merge=binary
#*.csproj merge=binary
#*.vbproj merge=binary
#*.vcxproj merge=binary
#*.vcproj merge=binary
#*.dbproj merge=binary
#*.fsproj merge=binary
#*.lsproj merge=binary
#*.wixproj merge=binary
#*.modelproj merge=binary
#*.sqlproj merge=binary
#*.wwaproj merge=binary
###############################################################################
# behavior for image files
#
# image files are treated as binary by default.
###############################################################################
#*.jpg binary
#*.png binary
#*.gif binary
###############################################################################
# diff behavior for common document formats
#
# Convert binary document formats to text before diffing them. This feature
# is only available from the command line. Turn it on by uncommenting the
# entries below.
###############################################################################
#*.doc diff=astextplain
#*.DOC diff=astextplain
#*.docx diff=astextplain
#*.DOCX diff=astextplain
#*.dot diff=astextplain
#*.DOT diff=astextplain
#*.pdf diff=astextplain
#*.PDF diff=astextplain
#*.rtf diff=astextplain
#*.RTF diff=astextplain

340
info/Minesweeper2/.gitignore vendored Normal file
View File

@ -0,0 +1,340 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUNIT
*.VisualState.xml
TestResult.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# JustCode is a .NET coding add-in
.JustCode
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- Backup*.rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# JetBrains Rider
.idea/
*.sln.iml
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# BeatPulse healthcheck temp database
healthchecksdb

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
</startup>
</configuration>

View File

@ -0,0 +1,84 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{B19C6960-EC27-4425-BFB5-E7AA65D69C28}</ProjectGuid>
<OutputType>Exe</OutputType>
<RootNamespace>Bulk_Runner</RootNamespace>
<AssemblyName>Bulk Runner</AssemblyName>
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<Deterministic>true</Deterministic>
<PublishUrl>publish\</PublishUrl>
<Install>true</Install>
<InstallFrom>Disk</InstallFrom>
<UpdateEnabled>false</UpdateEnabled>
<UpdateMode>Foreground</UpdateMode>
<UpdateInterval>7</UpdateInterval>
<UpdateIntervalUnits>Days</UpdateIntervalUnits>
<UpdatePeriodically>false</UpdatePeriodically>
<UpdateRequired>false</UpdateRequired>
<MapFileExtensions>true</MapFileExtensions>
<ApplicationRevision>0</ApplicationRevision>
<ApplicationVersion>1.0.0.%2a</ApplicationVersion>
<IsWebBootstrapper>false</IsWebBootstrapper>
<UseApplicationTrust>false</UseApplicationTrust>
<BootstrapperEnabled>true</BootstrapperEnabled>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<PlatformTarget>x64</PlatformTarget>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<Prefer32Bit>false</Prefer32Bit>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Numerics" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="BulkRunner.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<None Include="App.config" />
</ItemGroup>
<ItemGroup>
<BootstrapperPackage Include=".NETFramework,Version=v4.7.2">
<Visible>False</Visible>
<ProductName>Microsoft .NET Framework 4.7.2 %28x86 and x64%29</ProductName>
<Install>true</Install>
</BootstrapperPackage>
<BootstrapperPackage Include="Microsoft.Net.Framework.3.5.SP1">
<Visible>False</Visible>
<ProductName>.NET Framework 3.5 SP1</ProductName>
<Install>false</Install>
</BootstrapperPackage>
</ItemGroup>
<Import Project="..\MinesweeperGame\MinesweeperGame.projitems" Label="Shared" />
<Import Project="..\MinesweeperSolver\MinesweeperSolver.projitems" Label="Shared" />
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

View File

@ -0,0 +1,158 @@
using MinesweeperControl;
using MinesweeperSolver;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Numerics;
using static MinesweeperControl.GameDescription;
using static MinesweeperControl.MinesweeperGame;
namespace Bulk_Runner {
class BulkRunner {
private static readonly bool pauseOnWin = false;
private static readonly bool playUntilWin = false;
private static GameAction[] firstPlay;
private static int[] deathTable = new int[50];
static void Main(string[] args) {
GameDescription description = GameDescription.EXPERT_SAFE;
//GameDescription description = new GameDescription(30, 24, 225, GameType.Safe);
//GameDescription description = new GameDescription(100, 100, 1500, GameType.Zero);
//GameDescription description = new GameDescription(50, 50, 500, GameType.Safe);
//GameDescription description = new GameDescription(8, 8, 34, GameType.Safe);
if (description.gameType == GameType.Zero) {
firstPlay = new GameAction[] { new GameAction(3, 3, ActionType.Clear) };
} else {
firstPlay = new GameAction[] { new GameAction(0, 0, ActionType.Clear) };
}
int seedGen = new Random().Next();
//int seedGen = 1104105816;
Random rng = new Random(seedGen);
int run = 100000;
int steps = 100;
int won = 0;
int lost = 0;
int deaths = 0;
SolverMain.Initialise();
Write("using generation seed " + seedGen + " to run " + run + " games of minesweeper " + description.AsText());
Write("--- Press Enter to start ---");
Console.ReadLine();
long start = DateTime.Now.Ticks;
for (int i=0; i < run; i++) {
int seed = rng.Next();
MinesweeperGame game = new MinesweeperGame(description, seed, !playUntilWin);
//Write("Seed " + game.seed + " starting");
GameStatus status = AutoPlayRunner(game);
int died = game.GetDeaths();
if (died < deathTable.Length) {
deathTable[died]++;
} else {
deathTable[deathTable.Length - 1]++;
}
deaths = deaths + died;
if (status == GameStatus.Lost) {
lost++;
} else if (status == GameStatus.Won) {
won++;
if (pauseOnWin) {
Write("Seed " + game.seed + " won");
Write("--- Press Enter to continue ---");
Console.ReadLine();
}
} else {
Write("Seed " + game.seed + " : Unexpected game status from autoplay " + status);
}
if ((i + 1) % steps == 0) {
Write("Seed " + game.seed + " finished. Games won " + won + " out of " + (i + 1));
}
}
long duration = (DateTime.Now.Ticks - start ) / 10000;
Write("Games won " + won + ", lost " + lost + " out of " + run + " in " + duration + " milliseconds.");
double winRate = (won * 10000 / run) / 100d;
Write("Win rate " + winRate + "%");
Write("Deaths " + deaths + " average deaths per game " + (deaths * 1000 / run) / 1000d);
for (int i=0; i < deathTable.Length - 1; i++) {
Write("Died " + i + " times in " + deathTable[i] + " games");
}
Write("Died >=" + (deathTable.Length - 1) + " times in " + deathTable[deathTable.Length - 1] + " games");
Console.ReadLine();
}
// this method runs on a different thread. gameNumber can be changed by clicking chosing a new game from the UI thread.
private static GameStatus AutoPlayRunner(MinesweeperGame game) {
//long start = DateTime.Now.Ticks;
SolverActionHeader solverActions;
GameResult result = game.ProcessActions(firstPlay);
SolverInfo solverInfo = new SolverInfo(game.description);
while (result.status == GameStatus.InPlay) {
solverInfo.AddInformation(result); // let the solver know what has happened
solverActions = SolverMain.FindActions(solverInfo); // pass the solver info into the solver and see what it comes up with
if (solverActions.solverActions.Count == 0) {
Write("No actions returned by the solver!!");
break;
}
result = game.ProcessActions(solverActions.solverActions);
//Write("Game controller returned " + result.actionResults.Count + " results from this action");
//foreach (SolverAction action in solverActions) {
// Write("Playing " + action.AsText() + " probability " + action.safeProbability);
//}
}
//long end = DateTime.Now.Ticks;
//Write("game took " + (end - start) + " ticks");
return result.status;
}
private static void Write(String text) {
Console.WriteLine(text);
}
}
}

View File

@ -0,0 +1,36 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("Bulk Runner")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("Bulk Runner")]
[assembly: AssemblyCopyright("Copyright © 2019")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("b19c6960-ec27-4425-bfb5-e7aa65d69c28")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

View File

@ -0,0 +1,41 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.29123.89
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MinesweeperGui", "MinesweeperGui\MinesweeperGui.csproj", "{C981193D-B2EF-40D2-A0E1-99071CA2936F}"
EndProject
Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "MinesweeperGame", "MinesweeperGame\MinesweeperGame.shproj", "{DBCA9D17-B7A4-477E-A515-6E82B39840F1}"
EndProject
Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "MinesweeperSolver", "MinesweeperSolver\MinesweeperSolver.shproj", "{60591E9A-F066-4AF3-B1E3-B20B2B2B0B1D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bulk Runner", "Bulk Runner\Bulk Runner.csproj", "{B19C6960-EC27-4425-BFB5-E7AA65D69C28}"
EndProject
Global
GlobalSection(SharedMSBuildProjectFiles) = preSolution
MinesweeperSolver\MinesweeperSolver.projitems*{60591e9a-f066-4af3-b1e3-b20b2b2b0b1d}*SharedItemsImports = 13
MinesweeperGame\MinesweeperGame.projitems*{b19c6960-ec27-4425-bfb5-e7aa65d69c28}*SharedItemsImports = 4
MinesweeperSolver\MinesweeperSolver.projitems*{b19c6960-ec27-4425-bfb5-e7aa65d69c28}*SharedItemsImports = 4
MinesweeperGame\MinesweeperGame.projitems*{dbca9d17-b7a4-477e-a515-6e82b39840f1}*SharedItemsImports = 13
EndGlobalSection
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{C981193D-B2EF-40D2-A0E1-99071CA2936F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C981193D-B2EF-40D2-A0E1-99071CA2936F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C981193D-B2EF-40D2-A0E1-99071CA2936F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C981193D-B2EF-40D2-A0E1-99071CA2936F}.Release|Any CPU.Build.0 = Release|Any CPU
{B19C6960-EC27-4425-BFB5-E7AA65D69C28}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B19C6960-EC27-4425-BFB5-E7AA65D69C28}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B19C6960-EC27-4425-BFB5-E7AA65D69C28}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B19C6960-EC27-4425-BFB5-E7AA65D69C28}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {22747E34-ADCA-4C34-9FA8-EA30B962C694}
EndGlobalSection
EndGlobal

View File

@ -0,0 +1,39 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace MinesweeperControl {
public class GameDescription {
public enum GameType { Safe, Zero };
public static readonly GameDescription BEGINNER_SAFE = new GameDescription(8, 8, 10, GameType.Safe);
public static readonly GameDescription INTERMEDIATE_SAFE = new GameDescription(16, 16, 40, GameType.Safe);
public static readonly GameDescription EXPERT_SAFE = new GameDescription(30, 16, 99, GameType.Safe);
public static readonly GameDescription BEGINNER_ZERO = new GameDescription(9, 9, 10, GameType.Zero);
public static readonly GameDescription INTERMEDIATE_ZERO = new GameDescription(16, 16, 40, GameType.Zero);
public static readonly GameDescription EXPERT_ZERO = new GameDescription(30, 16, 99, GameType.Zero);
public readonly int width;
public readonly int height;
public readonly int mines;
public readonly GameType gameType;
public GameDescription(int width, int height, int mines, GameType gameType) {
this.width = width;
this.height = height;
this.mines = mines;
this.gameType = gameType;
}
public string AsText() {
return width + "x" + height + "x" + mines + " " + gameType + " start";
}
}
}

View File

@ -0,0 +1,506 @@
using System;
using System.Collections.Generic;
using static MinesweeperControl.GameDescription;
namespace MinesweeperControl {
/*
* This class describes a game of minesweeper
*/
public class MinesweeperGame {
public enum ActionType { Clear, Flag, Chord, Dead };
public enum GameStatus { NotStarted, InPlay, Won, Lost };
public enum ResultType { Cleared, Flagged, Hidden, Exploded, Mine, FlaggedWrong };
// describes the actions being perform in the gane
public class GameAction {
public readonly int x;
public readonly int y;
public readonly ActionType action;
public GameAction(int x, int y, ActionType action) {
this.x = x;
this.y = y;
this.action = action;
}
public String AsText() {
return "(" + x + "," + y + ") " + action;
}
}
// describes the result of the actions on the game
public class GameResult {
public readonly GameStatus status;
public readonly List<ActionResult> actionResults;
public GameResult(GameStatus status, List<ActionResult> actionResults) {
this.status = status;
this.actionResults = actionResults;
}
}
// describes the result of actions being perform in the gane
public class ActionResult {
public readonly int x;
public readonly int y;
public readonly ResultType resultType;
public readonly int value;
internal ActionResult(int x, int y, ResultType resultType) : this(x, y, resultType, 0) {
}
internal ActionResult(int x, int y, ResultType resultType, int value) {
this.x = x;
this.y = y;
this.resultType = resultType;
this.value = value;
}
}
// game variables
public readonly GameDescription description;
public readonly int seed; // seed to use to build the game
private readonly int[] adjacentOffset = new int[8];
private GameStatus gameStatus = GameStatus.NotStarted;
private int tilesLeft;
private MinesweeperTile[] tiles;
private int minesLeft;
private readonly bool hardcore;
private int deaths = 0;
public MinesweeperGame(GameDescription description, int seed) : this(description, seed, true) {
}
public MinesweeperGame(GameDescription description, int seed, bool hardcore) {
if (seed == 0) {
this.seed = new Random().Next(1, Int32.MaxValue);
} else {
this.seed = seed;
}
this.description = description;
this.hardcore = hardcore;
// create adjacent offsets
adjacentOffset[0] = -description.width - 1;
adjacentOffset[1] = -description.width;
adjacentOffset[2] = -description.width + 1;
adjacentOffset[3] = -1;
adjacentOffset[4] = 1;
adjacentOffset[5] = +description.width - 1;
adjacentOffset[6] = +description.width;
adjacentOffset[7] = +description.width + 1;
tilesLeft = this.description.height * this.description.width - this.description.mines;
minesLeft = description.mines;
// create all the tiles. the mines get placed at the first clear.
CreateTiles();
}
public int GetMinesLeft() {
return minesLeft;
}
public int GetDeaths() {
return deaths;
}
public GameStatus GetGameStatus() {
return this.gameStatus;
}
public GameResult ProcessActions<T>(IList<T> actions) where T : GameAction { // accept any Array of classes extending GameAction
//long start = DateTime.Now.Ticks;
List<ActionResult> actionResults = new List<ActionResult>();
foreach (GameAction action in actions) {
if (gameStatus != GameStatus.Lost && gameStatus != GameStatus.Won) { // only process actions while the game is in play
if (action.action == ActionType.Clear) {
actionResults.AddRange(ClearTile(action));
} else if (action.action == ActionType.Flag) {
actionResults.AddRange(FlagTile(action));
} else if (action.action == ActionType.Chord) {
actionResults.AddRange(ChordTile(action));
}
}
}
GameStatus finalStatus = gameStatus; // take the game status for the game
if (gameStatus == GameStatus.Lost) {
for (var i = 0; i < this.tiles.Length; i++) {
MinesweeperTile tile = this.tiles[i];
if (tile.IsMine() && !tile.IsFlagged() && !tile.GetExploded()) {
actionResults.Add(new ActionResult(tile.GetX(), tile.GetY(), ResultType.Mine)); // show remaining mines
}
if (!tile.IsMine() && tile.IsFlagged()) {
actionResults.Add(new ActionResult(tile.GetX(), tile.GetY(), ResultType.FlaggedWrong)); // show flags placed wrong
}
}
}
//Write("Game processing actions took " + (DateTime.Now.Ticks - start) + " ticks");
return new GameResult(finalStatus, actionResults);
}
private List<ActionResult> FlagTile(GameAction action) {
List<ActionResult> actionResults = new List<ActionResult>();
int index = GetIndex(action.x, action.y);
MinesweeperTile tile = this.tiles[index];
// if the tile is covered then toggle the flag and return it's new state
if (tile.IsCovered()) {
tile.ToggleFlagged();
if (tile.IsFlagged()) {
actionResults.Add(new ActionResult(action.x, action.y, ResultType.Flagged));
minesLeft--;
} else {
if (tile.GetExploded()) {
actionResults.Add(new ActionResult(action.x, action.y, ResultType.Exploded));
} else {
actionResults.Add(new ActionResult(action.x, action.y, ResultType.Hidden));
}
minesLeft++;
}
}
return actionResults;
}
// clicks the assigned tile and returns an object containing a list of tiles cleared
private List<ActionResult> ClearTile(GameAction action) {
List<ActionResult> actionResults = new List<ActionResult>();
int index = GetIndex(action.x, action.y);
// if this is the first click then create the tiles and place the mines
if (this.gameStatus == GameStatus.NotStarted) {
PlaceMines(index);
}
MinesweeperTile tile = this.tiles[index];
// are we clicking on a flag
if (tile.IsFlagged()) {
Write("Unable to Clear: Clicked on a Flag");
} else if (tile.GetExploded()) {
Write("Unable to Clear: Clicked on an exploded Mine");
} else if (tile.IsMine()) {
//Write("Gameover: Clicked on a mine");
deaths++;
if (hardcore) {
this.gameStatus = GameStatus.Lost;
}
tile.SetExploded(true);
actionResults.Add(new ActionResult(action.x, action.y, ResultType.Exploded));
} else {
if (tile.IsCovered()) { // make sure the tile is clickable
List<MinesweeperTile> tilesToReveal = new List<MinesweeperTile>();
tilesToReveal.Add(tile);
actionResults.AddRange(Reveal(tilesToReveal));
}
}
return actionResults;
}
// chord the assigned tile and returns an object containing a list of tiles cleared
private List<ActionResult> ChordTile(GameAction action) {
List<ActionResult> actionResults = new List<ActionResult>();
int index = GetIndex(action.x, action.y);
// if this is the first click then create the tiles and place the mines
if (this.gameStatus == GameStatus.NotStarted) {
PlaceMines(index);
}
MinesweeperTile tile = this.tiles[index];
int flagCount = 0;
int hiddenCount = 0;
foreach (int adjIndex in GetAdjacentIndex(index)) {
if (tiles[adjIndex].IsFlagged()) {
flagCount++;
} else if (tiles[adjIndex].IsCovered()) {
hiddenCount++;
}
}
// If the hidden count is zero then there is nothing to do
if (hiddenCount == 0) {
Write("Unable to Chord: Nothing to clear");
return actionResults;
}
// nothing to do if the tile is not yet surrounded by the correct number of flags
if (tile.GetValue() != flagCount) {
Write("Unable to Chord: value=" + tile.GetValue() + " flags=" + flagCount);
return actionResults;
}
// see if there are any unflagged bombs in the area to be chorded - this loses the game
var bombCount = 0;
foreach (int adjIndex in GetAdjacentIndex(index)) {
MinesweeperTile adjTile = tiles[adjIndex];
if (adjTile.IsMine() && !adjTile.IsFlagged()) {
adjTile.SetExploded(true);
actionResults.Add(new ActionResult(adjTile.GetX(), adjTile.GetY(), ResultType.Exploded)); // mark the tile as exploded
bombCount++;
}
}
// if we have triggered a bomb then return
if (bombCount != 0) {
deaths = deaths + bombCount;
if (hardcore) {
this.gameStatus = GameStatus.Lost;
}
return actionResults;
}
// seems okay, so do the chording
List<MinesweeperTile> tilesToReveal = new List<MinesweeperTile>();
// determine which tiles need revealing
foreach (int adjIndex in this.GetAdjacentIndex(index)) {
MinesweeperTile adjTile = tiles[adjIndex];
if (adjTile.IsCovered() && !adjTile.IsFlagged()) { // covered and not flagged
tilesToReveal.Add(adjTile);
}
}
actionResults.AddRange(Reveal(tilesToReveal));
return actionResults;
}
// takes a list of tiles and reveals them and expands any zeros
private List<ActionResult> Reveal(List<MinesweeperTile> firstTiles) {
List<ActionResult> actionResults = new List<ActionResult>();
var soFar = 0;
foreach (MinesweeperTile firstTile in firstTiles) {
firstTile.SetCovered(false);
}
int safety = 1000000;
while (soFar < firstTiles.Count) {
MinesweeperTile tile = firstTiles[soFar];
actionResults.Add(new ActionResult(tile.GetX(), tile.GetY(), ResultType.Cleared, tile.GetValue()));
this.tilesLeft--;
// if the value is zero then for each adjacent tile not yet revealed add it to the list
if (tile.GetValue() == 0) {
foreach (int adjIndex in GetAdjacentIndex(tile.GetIndex())) {
MinesweeperTile adjTile = this.tiles[adjIndex];
if (adjTile.IsCovered() && !adjTile.IsFlagged()) { // if not covered and not a flag
adjTile.SetCovered(false); // it will be uncovered in a bit
firstTiles.Add(adjTile);
}
}
}
soFar++;
if (safety-- < 0) {
Write("MinesweeperGame: Reveal Safety limit reached !!");
break;
}
}
// if there are no tiles left to find then set the remaining tiles to flagged and we've won
if (this.tilesLeft == 0) {
for (var i = 0; i < this.tiles.Length; i++) {
MinesweeperTile tile = this.tiles[i];
if (tile.IsMine() && !tile.IsFlagged()) {
minesLeft--;
tile.ToggleFlagged();
actionResults.Add(new ActionResult(tile.GetX(), tile.GetY(), ResultType.Flagged)); // auto set remaining flags
}
}
this.gameStatus = GameStatus.Won;
}
return actionResults;
}
// converts X and Y position to an Index
private int GetIndex(int x, int y) {
return y * this.description.width + x;
}
// create the tiles
private void CreateTiles() {
//long start = DateTime.Now.Ticks;
tiles = new MinesweeperTile[this.description.width * this.description.height];
// create the tiles and store non-excluded indices into a list
for (int y = 0; y < this.description.height; y++) {
for (int x = 0; x < this.description.width; x++) {
int i = GetIndex(x, y);
tiles[i] = new MinesweeperTile(i, x, y);
}
}
//Write("Ticks to create MinesweeperTiles " + (DateTime.Now.Ticks - start));
}
// builds all the tiles and assigns bombs to them
private void PlaceMines(int firstIndex) {
//long start = DateTime.Now.Ticks;
// hold the tiles to exclude from being a mine
HashSet<int> excludedIndices = new HashSet<int>();
excludedIndices.Add(firstIndex);
// for a zero start game all the adjacent tile can't be mines either
if (this.description.gameType == GameType.Zero) {
foreach (int adjIndex in GetAdjacentIndex(firstIndex)) {
excludedIndices.Add(adjIndex);
}
}
// create a list of all included indices
List<int> indices = new List<int>();
for (int y = 0; y < this.description.height; y++) {
for (int x = 0; x < this.description.width; x++) {
int i = GetIndex(x, y);
if (!excludedIndices.Contains(i)) {
indices.Add(i);
}
}
}
// shuffle the indices using a seed
indices.Shuffle(seed);
// allocate the mines and calculate the values
for (int i = 0; i < description.mines; i++) {
int index = indices[i];
MinesweeperTile tile = tiles[index];
//Utility.Write("Setting " + tile.AsText() + " to be a mine");
tile.SetMine(true); // this is set to be a mine
// set each affected tile to have an increased 'value'
foreach (int adjIndex in GetAdjacentIndex(tile.GetIndex())) {
tiles[adjIndex].IncrementValue();
}
}
this.gameStatus = GameStatus.InPlay;
//Write("Ticks to place mines on MinesweeperTiles " + (DateTime.Now.Ticks - start));
}
// returns all the indices adjacent to this index
private List<int> GetAdjacentIndex(int index) {
int col = index % description.width;
int row = index / description.width;
int first_row = Math.Max(0, row - 1);
int last_row = Math.Min(description.height - 1, row + 1);
int first_col = Math.Max(0, col - 1);
int last_col = Math.Min(description.width - 1, col + 1);
List<int> result = new List<int>();
for (int r = first_row; r <= last_row; r++) {
for (int c = first_col; c <= last_col; c++) {
int i = description.width * r + c;
if (i != index) {
result.Add(i);
}
}
}
return result;
}
private void Write(String text) {
Console.WriteLine(text);
}
}
public static class Shuffler {
// shuffle a given array
public static void Shuffle<T>(this IList<T> list, int seed) {
Random rng = new Random(seed);
int n = list.Count;
while (n > 1) {
n--;
int k = rng.Next(n + 1);
T value = list[k];
list[k] = list[n];
list[n] = value;
}
}
}
}

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
<HasSharedItems>true</HasSharedItems>
<SharedGUID>dbca9d17-b7a4-477e-a515-6e82b39840f1</SharedGUID>
</PropertyGroup>
<PropertyGroup Label="Configuration">
<Import_RootNamespace>MinesweeperControl</Import_RootNamespace>
</PropertyGroup>
<ItemGroup>
<Compile Include="$(MSBuildThisFileDirectory)GameDescription.cs" />
<Compile Include="$(MSBuildThisFileDirectory)MinesweeperGame.cs" />
<Compile Include="$(MSBuildThisFileDirectory)MinesweeperTile.cs" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Label="Globals">
<ProjectGuid>dbca9d17-b7a4-477e-a515-6e82b39840f1</ProjectGuid>
<MinimumVisualStudioVersion>14.0</MinimumVisualStudioVersion>
</PropertyGroup>
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.Common.Default.props" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.Common.props" />
<PropertyGroup />
<Import Project="MinesweeperGame.projitems" Label="Shared" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.CSharp.targets" />
</Project>

View File

@ -0,0 +1,82 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace MinesweeperControl {
internal class MinesweeperTile {
private readonly int index;
private readonly int x;
private readonly int y;
private bool covered = true;
private int value = 0;
private bool flagged = false;
private bool exploded = false;
private bool mine;
internal MinesweeperTile(int index, int x, int y) {
this.index = index;
this.x = x;
this.y = y;
}
internal int GetIndex() {
return index;
}
internal int GetX() {
return x;
}
internal int GetY() {
return y;
}
internal bool IsCovered() {
return covered;
}
internal void SetCovered(bool value) {
covered = value;
}
internal int GetValue() {
return value;
}
internal void IncrementValue() {
this.value++;
}
internal bool IsFlagged() {
return flagged;
}
internal void ToggleFlagged() {
flagged = !flagged;
}
internal bool GetExploded() {
return exploded;
}
internal void SetExploded(bool value) {
exploded = value;
}
internal bool IsMine() {
return mine;
}
internal void SetMine(bool value) {
mine = value;
}
internal String AsText() {
return "(" + x + "," + y + ")";
}
}
}

View File

@ -0,0 +1,61 @@
<Application x:Class="MinesweeperGui.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:MinesweeperGui"
StartupUri="MainWindow.xaml">
<Application.Resources>
<Style TargetType="Button">
<!--Set to true to not get any properties from the themes.-->
<Setter Property="OverridesDefaultStyle" Value="True"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="border" Background="Red" BorderBrush="Black" BorderThickness="3" CornerRadius="5" >
<Grid Height="40" Width="120">
<Image x:Name="buttonImage" Source="resources/images/facingDown.png" Stretch="Fill" />
<ContentPresenter Margin="{TemplateBinding Padding}" HorizontalAlignment="Center" VerticalAlignment="Center" />
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="true">
<Setter TargetName="border" Property="BorderBrush" Value="Blue" />
</Trigger>
<Trigger Property="IsPressed" Value="true">
<Setter TargetName="buttonImage" Property="Source" Value="resources/images/0.png" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="RaisedButton" TargetType="Button">
<Setter Property="Background">
<Setter.Value>
<ImageBrush ImageSource="resources/images/facingDown.png"/>
</Setter.Value>
</Setter>
<Style.Triggers>
<!--
<Trigger Property="IsPressed" Value="True">
<Setter Property="Background">
<Setter.Value>
<ImageBrush ImageSource="resources/images/0.png"/>
</Setter.Value>
</Setter>
</Trigger>
-->
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Foreground" Value="Red" />
<Setter Property="Background">
<Setter.Value>
<ImageBrush ImageSource="resources/images/0.png"/>
</Setter.Value>
</Setter>
</Trigger>
</Style.Triggers>
</Style>
</Application.Resources>
</Application>

View File

@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
namespace MinesweeperGui
{
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
Utility.Write("In OnStartup");
base.OnStartup(e);
}
}
}

View File

@ -0,0 +1,52 @@
<Window x:Class="MinesweeperGui.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:MinesweeperGui"
mc:Ignorable="d"
Title="Minesweeper solver" Height="720" Width="1250">
<Canvas x:Name="mainWindow" Background="#FFC7BFBF" SizeChanged="RedrawWindow">
<Canvas MaxWidth="180" Margin="6,9,6,10" Width="180" Height="542" Canvas.Left="16" Canvas.Top="97">
<Button Content="Beginner" Canvas.Left="27" Canvas.Top="140" Click="BeginnerClick"/>
<Button Content="Intermediate" Canvas.Left="27" Canvas.Top="191" Click="IntermediateClick"/>
<Button Content="Expert" Canvas.Left="27" Canvas.Top="244" Click="ExpertClick"/>
<Button Content="Custom" Canvas.Left="27" Canvas.Top="400" Click="CustomClick"/>
<Label Content="Width:" Height="32" Canvas.Left="10" Canvas.Top="301" Width="55" RenderTransformOrigin="0.178,0.47"/>
<TextBox x:Name="CustomWidth" Height="22" Canvas.Left="70" Canvas.Top="302" Text="TextBox" TextWrapping="Wrap" Width="95"/>
<Label Content="Height:" Height="32" Canvas.Left="10" Canvas.Top="329" Width="55" RenderTransformOrigin="0.178,0.47"/>
<TextBox x:Name="CustomHeight" Height="22" Canvas.Left="70" Canvas.Top="334" Text="TextBox" TextWrapping="Wrap" Width="95" RenderTransformOrigin="0.491,0.502"/>
<Label Content="Mines:" Height="32" Canvas.Left="10" Canvas.Top="359" Width="55" RenderTransformOrigin="0.178,0.47"/>
<TextBox x:Name="CustomMines" Height="22" Canvas.Left="70" Canvas.Top="365" Text="TextBox" TextWrapping="Wrap" Width="95" RenderTransformOrigin="0.491,0.502"/>
<CheckBox x:Name="useSeed" Content="Use seed" Height="18" Width="134" Canvas.Left="36" Canvas.Top="10"/>
<TextBox x:Name="SeedTextBox" Height="22" Canvas.Left="27" Canvas.Top="33" TextWrapping="Wrap" Width="138"/>
<CheckBox x:Name="showHInts" Content="Show hints" Height="18" Width="160" Canvas.Left="10" Canvas.Top="454" Checked="SetSolverDetails" Unchecked="SetSolverDetails"/>
<CheckBox x:Name="autoPlay" Content="Auto play" Height="18" Width="160" Canvas.Left="10" Canvas.Top="477" Checked="SetSolverDetails" Unchecked="SetSolverDetails"/>
<CheckBox x:Name="acceptGuesses" Content="Accept guesses" Height="18" Width="160" Canvas.Left="10" Canvas.Top="500" Checked="SetSolverDetails" Unchecked="SetSolverDetails"/>
<RadioButton x:Name="safeStart" Content="Safe start" Height="16" Canvas.Left="44" Canvas.Top="66" Width="92" GroupName="startType" IsChecked="True"/>
<RadioButton x:Name="zeroStart" Content="Zero start" Height="20" Canvas.Left="44" Canvas.Top="89" Width="92" GroupName="startType"/>
<CheckBox x:Name="hardcore" Content="Hard core" Height="18" Width="110" Canvas.Left="43" Canvas.Top="114" IsChecked="True"/>
</Canvas>
<Canvas>
<Label Content="Minesweeper" Grid.ColumnSpan="2" FontSize="30" FontWeight="Bold" Height="50" Margin="10,10,10,0" Canvas.Top="-9" Width="205" Canvas.Left="-9"/>
<Border x:Name="MinesLeftHolder" BorderBrush="#7F000000" BorderThickness="4,4,0,0" Margin="10,58,754,0" Grid.Column="1" Width="104" Height="48" Canvas.Left="211" Canvas.Top="-42">
<Border BorderThickness="0,0,4,4" BorderBrush="#7FFFFFFF">
<Canvas x:Name="MinesLeft" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" />
</Border>
</Border>
<Label Content="Solver" FontSize="30" FontWeight="Bold" Height="50" Canvas.Top="35" Width="104" RenderTransformOrigin="0.519,0.521" Canvas.Left="42"/>
</Canvas>
<Border x:Name="BoardHolder" BorderBrush="#7F000000" BorderThickness="4,4,0,0" Height="562" Width="960" Canvas.Left="220" Canvas.Top="70">
<Border BorderThickness="0,0,4,4" BorderBrush="#7FFFFFFF">
<Canvas x:Name="gameCanvas" Background="#FF58F037" MouseDown="CanvasMouseDown" MouseMove="MouseMoveOnBoard" MouseLeave="MouseLeftBoard" MouseEnter="MouseEnteredBoard" MouseWheel="MouseWheelOnBoard"/>
</Border>
</Border>
<Border x:Name="MessageHolder" BorderThickness="2" BorderBrush="Black" MaxHeight="35" Height="35" Background="#FFC7BFBF" Canvas.Bottom="5" Canvas.Left="220" Width="1000">
<Label x:Name="MessageLine" Content="Message line" FontWeight="Bold" FontSize="14" MouseDown="ClickMessageLine"/>
</Border>
<ScrollBar x:Name="horizontalScrollbar" Maximum="8000" ViewportSize="2000" Orientation="Horizontal" Height="20" Width="1019" Canvas.Left="221" Canvas.Bottom="50" Scroll="HorizontalScroll" SmallChange="1"/>
<ScrollBar x:Name="verticalScrollbar" Maximum="8000" ViewportSize="2000" Orientation="Vertical" Height="500" Width="20" Canvas.Right="10" Canvas.Top="70" Scroll="VerticalScroll" SmallChange="1"/>
</Canvas>
</Window>

View File

@ -0,0 +1,955 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Media;
using MinesweeperControl;
using MinesweeperSolver;
using static MinesweeperControl.GameDescription;
using static MinesweeperControl.MinesweeperGame;
namespace MinesweeperGui {
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
private readonly bool verbose = true; // determines whether diagnostic info gets written to the console
private readonly ImageSource Hidden;
private readonly ImageSource[] MineValue = new ImageSource[9];
private readonly ImageSource Flagged;
private readonly ImageSource FlaggedWrong;
private readonly ImageSource Mine;
private readonly ImageSource MineWrong;
private readonly ImageSource[] LedValue = new ImageSource[10];
private int tileSize = 32;
private GameDescription gameDescription = GameDescription.EXPERT_SAFE;
// details about the solver
private SolverInfo solverInfo;
private SolverActionHeader solverActions = new SolverActionHeader();
// graphical components
private Image hintImage; // overlay of hints from the solver
private Image totalImage; // composite image of imageDrawings
private DrawingGroup imageDrawings; // contains all the tiles
private ImageDrawing[,] images; // all the tiles
private readonly Image[] digits = new Image[8];
// tool tip elements
private Popup gameToolTip = new Popup();
private readonly TextBox popupText = new TextBox();
// details about the visible board
private int topX = 0;
private int topY = 0;
private int boardWidth = 0;
private int boardHeight = 0;
private ActionResult[,] tiles;
private MinesweeperGame game;
private int minesLeftSize = 6;
private bool useSolver = false;
private bool autoRunning = false;
private bool useGuesses = false;
private bool logicalLock = false;
private int gameNumber = 1;
private int autoPlayMinDelay = 250;
private SolverAction jumpTo;
public object MinesweeperMain { get; private set; }
public MainWindow() {
Utility.Write("At MainWindow constructor");
InitializeComponent();
// load the images we need
Hidden = Utility.BuildImageSource("facingDown.png", tileSize);
Flagged = Utility.BuildImageSource("flagged.png", tileSize);
FlaggedWrong = Utility.BuildImageSource("flaggedWrong.png", tileSize);
Mine = Utility.BuildImageSource("mine.png", tileSize);
MineWrong = Utility.BuildImageSource("exploded.png", tileSize);
for (int i = 0; i < 9; i++) {
MineValue[i] = Utility.BuildImageSource(i + ".png", tileSize);
}
for (int i = 0; i < 10; i++) {
LedValue[i] = Utility.BuildImageSource("led" + i + ".png", 24, 40);
}
CustomWidth.Text = "100";
CustomHeight.Text = "100";
CustomMines.Text = "2000";
for (int i = 0; i < digits.Length; i++) {
digits[i] = new Image() {
Source = LedValue[0]
};
Canvas.SetTop(digits[i], 0);
Canvas.SetLeft(digits[i], i * LedValue[0].Width);
MinesLeft.Children.Add(digits[i]);
}
// complete creating the popup
popupText.Text = "";
popupText.Background = new SolidColorBrush( Color.FromArgb(128, 227, 227, 227));
popupText.Foreground = Brushes.Black;
popupText.FontSize = 20;
popupText.FontWeight = FontWeights.Bold;
gameToolTip.Child = popupText;
gameToolTip.AllowsTransparency = true;
gameToolTip.Placement = PlacementMode.Relative;
gameToolTip.PlacementTarget = gameCanvas;
gameToolTip.IsOpen = true;
gameCanvas.Children.Add(gameToolTip);
// start up a game
InitializeTiles1();
Utility.Write("Graphics rendering tier = " + (RenderCapability.Tier >> 16));
SolverMain.Initialise();
}
private void AutoPlay() {
Thread autoplay = new Thread(AutoPlayRunner);
autoplay.Start();
}
// this method runs on a different thread. gameNumber is changed by clicking chosing a new game from the UI thread.
private void AutoPlayRunner() {
Dispatcher.Invoke(() => gameCanvas.Cursor = Cursors.Wait);
logicalLock = true;
int runningGameNumber = gameNumber; // remember the game number we are solving
//Utility.Write("AutoRunning thread starting");
solverActions = SolverMain.FindActions(solverInfo); // pass the solver info into the solver and see what it comes up with
Dispatcher.Invoke(() => RenderHints());
while (IsAutoPlayValid() && gameNumber == runningGameNumber) {
long start = DateTime.Now.Ticks;
GameResult result = game.ProcessActions(solverActions.solverActions);
//Utility.Write("Game controller returned " + result.actionResults.Count + " results from this action");
long end1 = DateTime.Now.Ticks;
// do the rendering while we are stil playing the same game
if (gameNumber == runningGameNumber) {
gameCanvas.Dispatcher.Invoke(() => RenderResults(result));
} else {
break;
}
//Utility.Write("After RenderResults took " + (DateTime.Now.Ticks - start) + " ticks");
solverInfo.AddInformation(result); // let the solver know what has happened
// nothing more to do if we have won or lost
if (result.status == GameStatus.Lost || result.status == GameStatus.Won) {
Dispatcher.Invoke(() => DisplayFinalOutcome(result));
break;
}
solverActions = SolverMain.FindActions(solverInfo); // pass the solver info into the solver and see what it comes up with
//foreach (SolverAction action in solverActions) {
// Utility.Write("Playing " + action.AsText() + " probability " + action.safeProbability);
//}
//long end2 = DateTime.Now.Ticks;
// do the rendering while we are stil playing the same game
if (gameNumber == runningGameNumber) {
Dispatcher.Invoke(() => RenderHints());
} else {
break;
}
//Utility.Write("After RenderHints took " + (DateTime.Now.Ticks - start) + " ticks");
Utility.Write("Tiles remaining " + solverInfo.GetTilesLeft());
long end = DateTime.Now.Ticks;
int milliseconds = (int) ((end - start) / 10000);
int wait = Math.Max(0, autoPlayMinDelay - milliseconds);
//Utility.Write("Autoplay processing took " + milliseconds + " milliseconds (" + (end - start) + " ticks)");
//Console.WriteLine("Autoplay processing took " + milliseconds + " milliseconds");
if (!IsAutoPlayValid()) {
break;
}
Thread.Sleep(wait); // wait until all the time is used up
}
logicalLock = false;
Dispatcher.Invoke(() => gameCanvas.Cursor = Cursors.Arrow);
//Utility.Write("AutoRunning thread ending");
}
// create the non-graphical parts of the game
private void InitializeTiles1() {
gameNumber++;
logicalLock = false;
minesLeftSize = Math.Max(3, (int) Math.Log10(gameDescription.mines) + 1);
MinesLeftHolder.Width = 24 * minesLeftSize + 8;
for (int i= 0; i < digits.Length; i++) {
if (i < minesLeftSize) {
digits[i].Visibility = Visibility.Visible;
} else {
digits[i].Visibility = Visibility.Hidden;
}
}
int seed = 0;
if (useSeed.IsChecked == true) {
try {
seed = int.Parse(SeedTextBox.Text);
} catch (Exception) {
}
}
//game = new MinesweeperGame(gameDescription, seed);
game = new MinesweeperGame(gameDescription, seed, (hardcore.IsChecked == true));
solverInfo = new SolverInfo(gameDescription, verbose);
RenderMinesLeft();
long start = DateTime.Now.Ticks;
tiles = new ActionResult[gameDescription.width, gameDescription.height];
solverActions = new SolverActionHeader(); // remove any hints from the previous game
Utility.Write("Ticks to build screen " + (DateTime.Now.Ticks - start));
}
// build the graphical part of the board
private void BuildBoard() {
//long start = DateTime.Now.Ticks;
images = new ImageDrawing[boardWidth, boardHeight];
// remove existing tiles from the canvas
gameCanvas.Children.Clear(); // clear game board
imageDrawings = new DrawingGroup();
for (int y = 0; y < boardHeight; y++) {
for (int x = 0; x < boardWidth; x++) {
ActionResult tile = tiles[topX + x, topY + y];
ImageSource imageSource;
if (tile == null) {
imageSource = Hidden;
} else if (tile.resultType == ResultType.Cleared) {
imageSource = MineValue[tile.value];
} else if (tile.resultType == ResultType.Flagged) {
imageSource = Flagged;
} else if (tile.resultType == ResultType.Hidden) {
imageSource = Hidden;
} else if (tile.resultType == ResultType.Exploded) {
imageSource = MineWrong;
} else if (tile.resultType == ResultType.FlaggedWrong) {
imageSource = FlaggedWrong;
} else if (tile.resultType == ResultType.Mine) {
imageSource = Mine;
} else {
imageSource = Hidden;
}
ImageDrawing image = new ImageDrawing() {
ImageSource = imageSource,
Rect = new Rect(x * tileSize, y * tileSize, tileSize, tileSize)
};
imageDrawings.Children.Add(image);
images[x, y] = image;
}
}
totalImage = new Image() {
Source = new DrawingImage(imageDrawings)
};
Canvas.SetLeft(totalImage, 0);
Canvas.SetTop(totalImage, 0);
gameCanvas.Children.Add(totalImage);
// create an empty hints image which we'll use later
hintImage = new Image();
Canvas.SetLeft(hintImage, 0);
Canvas.SetTop(hintImage, 0);
gameCanvas.Children.Add(hintImage);
//Utility.Write("Ticks to build screen " + (DateTime.Now.Ticks - start));
}
private void DoAction(int x, int y, ActionType action) {
//int index = gameDescription.width * y + x;
GameResult result = game.ProcessActions(new GameAction[] { new GameAction(x, y, action) });
Utility.Write("Game controller returned " + result.actionResults.Count + " results from this action");
RenderResults(result); // draw the new board
solverInfo.AddInformation(result); // let the solver know what has happened
if (result.status == GameStatus.Lost) {
MessageLine.Content = "The game is lost!";
} else if (result.status == GameStatus.Won) {
if (game.GetDeaths() != 0) {
MessageLine.Content = "The game is won with " + game.GetDeaths() + " deaths";
} else {
MessageLine.Content = "The game is won";
}
} else {
// if we are showing hints or auto playing then get the hints
if (useSolver || autoRunning) {
AutoPlay(); // let the solver playout the moves
}
}
}
private void DisplayFinalOutcome(GameResult result) {
if (result.status == GameStatus.Lost) {
MessageLine.Content = "The game is lost!";
} else if (result.status == GameStatus.Won) {
if (this.game.GetDeaths() != 0) {
MessageLine.Content = "The game is won with " + game.GetDeaths() + " deaths";
} else {
MessageLine.Content = "The game is won";
}
}
}
private bool IsAutoPlayValid() {
if (!autoRunning) {
//Utility.Write("Autoplay not valid - AutoPlay is not checked");
return false;
}
// if we have no actions then we can't autoplay
if (solverActions.solverActions.Count == 0) {
//Utility.Write("Autoplay not valid - no actions found by the solver");
return false;
}
// if we are accepting guesses then everything goes
if (useGuesses) {
//Utility.Write("Autoplay valid - accepting guesses");
return true;
}
if ((solverActions.solverActions[0].safeProbability > 0 && solverActions.solverActions[0].safeProbability < 1)) {
//Utility.Write("Autoplay not valid - no certain actions found by the solver and we aren't accepting guesses");
return false;
}
//Utility.Write("Autoplay valid - certain actions found by the solver");
return true;
}
// draw the outputs from the Game onto the visible board
private void RenderResults(GameResult result) {
//long start = DateTime.Now.Ticks;
SeedTextBox.Text = game.seed.ToString();
hintImage.Visibility = Visibility.Hidden; // hide the previous hints
foreach (ActionResult ar in result.actionResults) {
// remember what happen here last
tiles[ar.x, ar.y] = ar;
int boardX = ar.x - topX;
int boardY = ar.y - topY;
// if the action is taking place in the part of the board we are looking at
if (boardX >= 0 && boardX < boardWidth && boardY >= 0 && boardY < boardHeight) {
ImageDrawing image = images[boardX, boardY];
if (ar.resultType == ResultType.Cleared) {
image.ImageSource = MineValue[ar.value];
} else if (ar.resultType == ResultType.Flagged) {
image.ImageSource = Flagged;
} else if (ar.resultType == ResultType.Hidden) {
image.ImageSource = Hidden;
} else if (ar.resultType == ResultType.Exploded) {
image.ImageSource = MineWrong;
} else if (ar.resultType == ResultType.FlaggedWrong) {
image.ImageSource = FlaggedWrong;
} else if (ar.resultType == ResultType.Mine) {
image.ImageSource = Mine;
}
}
}
RenderMinesLeft();
//Utility.Write("Ticks to render results " + (DateTime.Now.Ticks - start));
}
// draw the outputs from the Solver onto the visible board
private void RenderHints() {
//long start = DateTime.Now.Ticks;
DrawingGroup hintDrawings = new DrawingGroup();
int offSetLeft = tileSize * (boardWidth + 1);
int offSetTop = tileSize * (boardHeight + 1);
jumpTo = null;
if (this.game.GetGameStatus() == GameStatus.InPlay) {
if (solverActions.solverActions.Count == 0) {
MessageLine.Content = "The solver has no suggestions!";
} else if (solverActions.solverActions.Count > 1) {
MessageLine.Content = "The solver has found " + solverActions.solverActions.Count + " moves.";
} else if (solverActions.solverActions[0].safeProbability > 0 && solverActions.solverActions[0].safeProbability < 1) {
MessageLine.Content = "The solver suggests guessing " + solverActions.solverActions[0].AsText();
jumpTo = solverActions.solverActions[0];
} else {
MessageLine.Content = "The solver has found 1 move.";
}
}
foreach (SolverAction ar in solverActions.solverActions) {
int boardX = ar.x - topX;
int boardY = ar.y - topY;
// if the action is taking place in the part of the board we are looking at
if (boardX >= 0 && boardX < boardWidth && boardY >= 0 && boardY < boardHeight) {
int x = boardX * tileSize;
int y = boardY * tileSize;
if (x < offSetLeft) {
offSetLeft = x;
}
if (y < offSetTop) {
offSetTop = y;
}
Color fill;
if (ar.action == ActionType.Clear) {
if (ar.safeProbability == 1) {
fill = Color.FromArgb(128, 0, 255, 0); // green if safe
} else if (ar.isDead) {
fill = Color.FromArgb(128, 0, 0, 0); // black if excluded
} else {
fill = Color.FromArgb(128, 255, 165, 0); // orange if not safe
}
} else if (ar.action == ActionType.Flag) {
fill = Color.FromArgb(128, 255, 0, 0); // red if a mine
}
GeometryDrawing square =
new GeometryDrawing(
new SolidColorBrush(fill),
new Pen(Brushes.Black, 0),
new RectangleGeometry(new Rect(x, y, tileSize, tileSize))
);
hintDrawings.Children.Add(square);
}
}
// render dead tiles
foreach (SolverAction ar in solverActions.deadActions) {
int boardX = ar.x - topX;
int boardY = ar.y - topY;
// if the action is taking place in the part of the board we are looking at
if (boardX >= 0 && boardX < boardWidth && boardY >= 0 && boardY < boardHeight) {
int x = boardX * tileSize;
int y = boardY * tileSize;
if (x < offSetLeft) {
offSetLeft = x;
}
if (y < offSetTop) {
offSetTop = y;
}
Color fill = Color.FromArgb(128, 0, 0, 0); // black if dead
GeometryDrawing square =
new GeometryDrawing(
new SolidColorBrush(fill),
new Pen(Brushes.Black, 0),
new RectangleGeometry(new Rect(x, y, tileSize, tileSize))
);
hintDrawings.Children.Add(square);
}
}
// replace the old composite hints image with the new
hintImage.Source = new DrawingImage(hintDrawings);
// and place it in the write position
Canvas.SetLeft(hintImage, offSetLeft);
Canvas.SetTop(hintImage, offSetTop);
hintImage.Visibility = Visibility.Visible;
//Utility.Write("Ticks to render hints " + (DateTime.Now.Ticks - start));
}
private void RebuildBoard(int topX, int topY, int boardWidth, int boardHeight) {
this.topX = topX;
this.topY = topY;
this.boardWidth = boardWidth;
this.boardHeight = boardHeight;
// make sure the top position allows enough space to fill the board
if (this.topX + this.boardWidth > gameDescription.width) {
this.topX = gameDescription.width - this.boardWidth;
}
if (this.topY + this.boardHeight > gameDescription.height) {
this.topY = gameDescription.height - this.boardHeight;
}
if (boardWidth > 0 && boardHeight > 0) {
BuildBoard();
RenderHints();
}
}
private void RenderMinesLeft() {
int i = minesLeftSize - 1;
int mines = game.GetMinesLeft();
while (i >= 0) {
int d0 = mines % 10;
mines = (mines - d0) / 10;
digits[i].Source = LedValue[d0];
i--;
}
}
private void CanvasMouseDown(object sender, MouseButtonEventArgs e) {
if (logicalLock) { // this means the solver is playing the game and the user input is ignored
return;
}
int x = (int) e.GetPosition(gameCanvas).X;
int y = (int) e.GetPosition(gameCanvas).Y;
//Utility.Write("Mouse clicked at X=" + x + ", Y=" + y);
int col = x / tileSize;
int row = y / tileSize;
if (col < 0 || col > boardWidth || row < 0 || row > boardHeight) {
Utility.Write("Clicked outside of game boundary col=" + col + ", row=" + row);
return;
}
//Utility.Write("col=" + col + ", row=" + row);
ActionType actionType;
ActionResult tile = tiles[col + topX, row + topY];
if (e.ChangedButton == MouseButton.Left) {
if (tile == null || tile.resultType == ResultType.Hidden) {
actionType = ActionType.Clear;
} else if (tile.resultType == ResultType.Cleared) {
actionType = ActionType.Chord;
} else {
return;
}
} else if (e.ChangedButton == MouseButton.Right) {
actionType = ActionType.Flag;
} else {
return;
}
DoAction(col + topX, row + topY, actionType);
}
private void NewGame() {
InitializeTiles1();
// reposition the display to the top left
//topX = 0;
//topY = 0;
// move the scroll bars back to the start
horizontalScrollbar.Value = 0;
verticalScrollbar.Value = 0;
// work out if scroll bars are need and how big the thumb is
//boardWidth = DoWidth(mainWindow.ActualWidth);
//boardHeight = DoHeight(mainWindow.ActualHeight);
RebuildBoard(0, 0, DoWidth(mainWindow.ActualWidth), DoHeight(mainWindow.ActualHeight));
}
private void BeginnerClick(object sender, RoutedEventArgs e) {
if (zeroStart.IsChecked == true) {
gameDescription = GameDescription.BEGINNER_ZERO;
} else {
gameDescription = GameDescription.BEGINNER_SAFE;
}
NewGame();
}
private void IntermediateClick(object sender, RoutedEventArgs e) {
if (zeroStart.IsChecked == true) {
gameDescription = GameDescription.INTERMEDIATE_ZERO;
} else {
gameDescription = GameDescription.INTERMEDIATE_SAFE;
}
NewGame();
}
private void ExpertClick(object sender, RoutedEventArgs e) {
if (zeroStart.IsChecked == true) {
gameDescription = GameDescription.EXPERT_ZERO;
} else {
gameDescription = GameDescription.EXPERT_SAFE;
}
NewGame();
}
private void CustomClick(object sender, RoutedEventArgs e) {
int gameWidth;
int gameHeight;
int gameMines;
try {
gameWidth = int.Parse(CustomWidth.Text);
} catch (System.Exception) {
gameWidth = 30;
CustomWidth.Text = gameWidth.ToString();
}
try {
gameHeight = int.Parse(CustomHeight.Text);
} catch (System.Exception) {
gameHeight = 16;
CustomHeight.Text = gameHeight.ToString();
}
try {
gameMines = int.Parse(CustomMines.Text);
} catch (System.Exception) {
gameMines = gameWidth * gameHeight / 5;
CustomMines.Text = gameMines.ToString();
}
if (zeroStart.IsChecked == true) {
gameDescription = new GameDescription(gameWidth, gameHeight, gameMines, GameType.Zero);
} else {
gameDescription = new GameDescription(gameWidth, gameHeight, gameMines, GameType.Safe);
}
NewGame();
}
// redraws the controls based on the new width and returns the number of columns we are now able to show
private int DoWidth(double width) {
MessageHolder.Width = Math.Max(0, width - 260);
double boardMax = gameDescription.width * tileSize + 8;
double actualWidth = Math.Max(8, Math.Min(boardMax, width - 260));
int showTilesWidth = (int)Math.Floor((actualWidth - 8) / tileSize);
double boardWidthPixels = showTilesWidth * tileSize + 8; // board is in Whole tiles
BoardHolder.Width = boardWidthPixels;
if (showTilesWidth == gameDescription.width) {
horizontalScrollbar.Visibility = Visibility.Hidden;
} else {
horizontalScrollbar.Visibility = Visibility.Visible;
}
horizontalScrollbar.Width = boardWidthPixels;
horizontalScrollbar.Maximum = (gameDescription.width - showTilesWidth);
horizontalScrollbar.ViewportSize = showTilesWidth;
horizontalScrollbar.LargeChange = showTilesWidth; // scroll bar large scroll is a whole screen
return showTilesWidth;
}
// redraws the controls based on the new height and returns the number of columns we are now able to show
private int DoHeight(double height) {
double boardMax = gameDescription.height * tileSize + 8;
double actualHeight = Math.Max(8, Math.Min(boardMax, height - 140));
int showTilesHeight = (int) Math.Floor((actualHeight - 8) / tileSize);
double boardHeightPixels = showTilesHeight * tileSize + 8; // board is in Whole tiles
BoardHolder.Height = boardHeightPixels;
if (showTilesHeight == gameDescription.height) {
verticalScrollbar.Visibility = Visibility.Hidden;
} else {
verticalScrollbar.Visibility = Visibility.Visible;
}
verticalScrollbar.Height = boardHeightPixels;
verticalScrollbar.Maximum = (gameDescription.height - showTilesHeight);
verticalScrollbar.ViewportSize = showTilesHeight;
verticalScrollbar.LargeChange = showTilesHeight; // scroll bar large scroll is a whole screen
return showTilesHeight;
}
private void RedrawWindow(object sender, SizeChangedEventArgs e) {
//Utility.Write("New width:" + e.NewSize.Width + " height:" + e.NewSize.Height);
int newBoardWidth = boardWidth;
int newBoardHeight = boardHeight;
if (e.WidthChanged == true) {
newBoardWidth = DoWidth(e.NewSize.Width);
}
if (e.HeightChanged == true) {
newBoardHeight = DoHeight(e.NewSize.Height);
}
if (newBoardWidth != this.boardWidth || newBoardHeight != this.boardHeight) {
RebuildBoard(topX, topY, newBoardWidth, newBoardHeight);
}
}
private void VerticalScroll(object sender, System.Windows.Controls.Primitives.ScrollEventArgs e) {
//Utility.Write("Vertical scroll : " + e.NewValue);
int newTopY = (int) Math.Floor(e.NewValue + 0.5d);
if (newTopY != topY) {
RebuildBoard(topX, newTopY, boardWidth, boardHeight);
}
}
private void HorizontalScroll(object sender, System.Windows.Controls.Primitives.ScrollEventArgs e) {
//Utility.Write("Horizontal scroll : " + e.NewValue);
int newTopX = (int)Math.Floor(e.NewValue + 0.5d);
if (newTopX != topX) {
RebuildBoard(newTopX, topY, boardWidth, boardHeight);
}
}
private void MouseMoveOnBoard(object sender, MouseEventArgs e) {
int x = (int)e.GetPosition(gameCanvas).X;
int y = (int)e.GetPosition(gameCanvas).Y;
int col = x / tileSize;
int row = y / tileSize;
if (col < 0 || col > boardWidth || row < 0 || row > boardHeight) {
Utility.Write("Mouse outside of game boundary col=" + col + ", row=" + row);
return;
}
int realCol = col + topX;
int realRow = row + topY;
double prob = solverInfo.GetProbability(realCol, realRow);
string probText;
if (prob == -1) {
probText = "";
} else if (prob == 0) {
probText = "Mine";
} else if (prob == 1) {
probText = "Clear";
} else {
//probText = String.Format("0.00", prob) + " Safe";
probText = (prob * 100).ToString("N2") + "% Safe";
}
popupText.Text = "(" + realCol + "," + realRow + ") " + probText;
gameToolTip.HorizontalOffset = x;
gameToolTip.VerticalOffset = y + 10;
}
private void MouseLeftBoard(object sender, MouseEventArgs e) {
gameToolTip.IsOpen = false;
}
private void MouseEnteredBoard(object sender, MouseEventArgs e) {
gameToolTip.IsOpen = true;
}
private void ClickMessageLine(object sender, MouseButtonEventArgs e) {
if (jumpTo == null) {
return;
}
// set the scroll bars
verticalScrollbar.Value = jumpTo.y - this.boardHeight / 2;
horizontalScrollbar.Value = jumpTo.x - this.boardWidth / 2;
// now see where the scrollbars are
int newTopY = (int)Math.Floor(verticalScrollbar.Value + 0.5d);
int newTopX = (int)Math.Floor(horizontalScrollbar.Value + 0.5d);
// redraw board if we need to
if (newTopY != topY || newTopX != topX) {
RebuildBoard(newTopX, newTopY, boardWidth, boardHeight);
}
}
private void MouseWheelOnBoard(object sender, MouseWheelEventArgs e) {
// if shift is pressed scroll left and right
if ((Keyboard.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift) {
horizontalScrollbar.Value -= e.Delta / 40;
int newTopX = (int)Math.Floor(horizontalScrollbar.Value + 0.5d);
if (newTopX != topX) {
RebuildBoard(newTopX, topY, boardWidth, boardHeight);
}
return;
}
if ((Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control) {
return;
}
verticalScrollbar.Value -= e.Delta / 40;
int newTopY = (int)Math.Floor(verticalScrollbar.Value + 0.5d);
if (newTopY != topY) {
RebuildBoard(topX, newTopY, boardWidth, boardHeight);
}
}
private void SetSolverDetails(object sender, RoutedEventArgs e) {
useSolver = (showHInts.IsChecked == true);
autoRunning = (autoPlay.IsChecked == true);
useGuesses = (acceptGuesses.IsChecked == true);
//Console.WriteLine(useSolver + " " + autoRunning + " " + useGuesses);
}
}
}

View File

@ -0,0 +1,93 @@
<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.0</TargetFramework>
<UseWPF>true</UseWPF>
<StartupObject>MinesweeperGui.App</StartupObject>
<ApplicationIcon />
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<PlatformTarget>x64</PlatformTarget>
<Optimize>false</Optimize>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<PlatformTarget>x64</PlatformTarget>
</PropertyGroup>
<ItemGroup>
<None Remove="resources\images\0.png" />
<None Remove="resources\images\1.png" />
<None Remove="resources\images\2.png" />
<None Remove="resources\images\3.png" />
<None Remove="resources\images\4.png" />
<None Remove="resources\images\5.png" />
<None Remove="resources\images\6.png" />
<None Remove="resources\images\7.png" />
<None Remove="resources\images\8.png" />
<None Remove="resources\images\bombTransparent.png" />
<None Remove="resources\images\exploded.png" />
<None Remove="resources\images\facingDown.png" />
<None Remove="resources\images\flagged.png" />
<None Remove="resources\images\flaggedWrong.png" />
<None Remove="resources\images\led0.png" />
<None Remove="resources\images\led1.png" />
<None Remove="resources\images\led2.png" />
<None Remove="resources\images\led3.png" />
<None Remove="resources\images\led4.png" />
<None Remove="resources\images\led5.png" />
<None Remove="resources\images\led6.png" />
<None Remove="resources\images\led7.png" />
<None Remove="resources\images\led8.png" />
<None Remove="resources\images\led9.png" />
<None Remove="resources\images\led_blank.png" />
<None Remove="resources\images\mine.png" />
<None Remove="resources\images\raised.png" />
</ItemGroup>
<ItemGroup>
<Resource Include="resources\images\0.png" />
<Resource Include="resources\images\1.png" />
<Resource Include="resources\images\2.png" />
<Resource Include="resources\images\3.png" />
<Resource Include="resources\images\4.png" />
<Resource Include="resources\images\5.png" />
<Resource Include="resources\images\6.png" />
<Resource Include="resources\images\7.png" />
<Resource Include="resources\images\8.png" />
<Resource Include="resources\images\bombTransparent.png" />
<Resource Include="resources\images\exploded.png" />
<Resource Include="resources\images\facingDown.png" />
<Resource Include="resources\images\flagged.png" />
<Resource Include="resources\images\flaggedWrong.png" />
<Resource Include="resources\images\led0.png" />
<Resource Include="resources\images\led1.png" />
<Resource Include="resources\images\led2.png" />
<Resource Include="resources\images\led3.png" />
<Resource Include="resources\images\led4.png" />
<Resource Include="resources\images\led5.png" />
<Resource Include="resources\images\led6.png" />
<Resource Include="resources\images\led7.png" />
<Resource Include="resources\images\led8.png" />
<Resource Include="resources\images\led9.png" />
<Resource Include="resources\images\led_blank.png" />
<Resource Include="resources\images\mine.png" />
<Resource Include="resources\images\raised.png" />
</ItemGroup>
<ItemGroup>
<None Update="App.xaml">
<Generator>MSBuild:Compile</Generator>
</None>
<None Update="MainWindow.xaml">
<Generator>MSBuild:Compile</Generator>
</None>
</ItemGroup>
<Import Project="..\MinesweeperGame\MinesweeperGame.projitems" Label="Shared" />
<Import Project="..\MinesweeperSolver\MinesweeperSolver.projitems" Label="Shared" />
</Project>

View File

@ -0,0 +1,49 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Text;
using System.Windows.Media;
using System.Windows.Media.Imaging;
namespace MinesweeperGui
{
/*
* This class holds some utility functions
*/
public static class Utility {
public static ImageSource BuildImageSource(string filename, int tileSize) {
return BuildImageSource(filename, tileSize, tileSize);
}
public static ImageSource BuildImageSource(string filename, int tileWidth, int tileHeight) {
Uri uri = new Uri("pack://application:,,,/resources/images/" + filename);
// Create source.
BitmapImage bi = new BitmapImage();
// BitmapImage.UriSource must be in a BeginInit/EndInit block.
bi.BeginInit();
bi.UriSource = uri;
bi.DecodePixelHeight = tileHeight;
bi.DecodePixelWidth = tileWidth;
bi.EndInit();
bi.Freeze();
return bi;
}
public static void Write(string text) {
//System.Diagnostics.Debug.Print(text);
Console.WriteLine(text);
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 697 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 871 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 916 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 977 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 973 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 879 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 990 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 948 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 991 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 749 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 929 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 327 B

View File

@ -0,0 +1,606 @@
using System;
using System.CodeDom;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
namespace MinesweeperSolver {
/**
* Performs a brute force search on the provided squares using the iterator
*
*/
public class Cruncher {
public const sbyte BOMB = -10;
readonly private SolverInfo information;
private readonly WitnessWebIterator iterator;
readonly private List<SolverTile> tiles;
readonly private List<BoxWitness> witnesses;
readonly private sbyte[] currentFlagsTiles;
readonly private sbyte[] currentFlagsWitnesses;
private int candidates = 0; // number of samples which satisfy the current board state
private readonly BruteForceAnalysis bfa;
public Cruncher(SolverInfo information, WitnessWebIterator iterator, List<BoxWitness> witnesses, BruteForceAnalysis bfa) {
this.information = information;
this.iterator = iterator; // the iterator
this.tiles = iterator.getTiles(); // the tiles the iterator is iterating over
this.witnesses = witnesses; // the dependent witnesses (class BoxWitness) which need to be checked to see if they are satisfied
this.bfa = bfa;
// determine how many found mines are currently next to each tile
this.currentFlagsTiles = new sbyte[this.tiles.Count];
for (int i = 0; i < this.tiles.Count; i++) {
this.currentFlagsTiles[i] = (sbyte) this.information.AdjacentTileInfo(this.tiles[i]).mines;
}
// determine how many found mines are currently next to each witness
this.currentFlagsWitnesses = new sbyte[this.witnesses.Count];
for (int i = 0; i < this.witnesses.Count; i++) {
this.currentFlagsWitnesses[i] = (sbyte) this.information.AdjacentTileInfo(this.witnesses[i].GetTile()).mines;
}
}
public static BruteForceAnalysis PerformBruteForce(SolverInfo information, WitnessWebIterator[] iterators, List<BoxWitness> witnesses) {
BruteForceAnalysis bfa = new BruteForceAnalysis(information, iterators[0].getTiles(), SolverMain.MAX_BFDA_SOLUTIONS, null);
Cruncher[] crunchers = new Cruncher[iterators.Length];
Task[] tasks = new Task[iterators.Length];
for (int i=0; i < iterators.Length; i++) {
crunchers[i] = new Cruncher(information, iterators[i], witnesses, bfa);
Cruncher cruncher = crunchers[i];
tasks[i] = Task.Factory.StartNew(() => { cruncher.Crunch(); });
}
Task.WaitAll(tasks);
int solutions = 0;
int iterations = 0;
for (int i = 0; i < iterators.Length; i++) {
solutions = solutions + crunchers[i].GetSolutionsFound();
iterations = iterations + iterators[i].GetIterations();
}
information.Write("Solutions found by brute force " + solutions + " after " + iterations + " iterations");
return bfa;
}
public void Crunch() {
int[] sample = this.iterator.GetSample();
while (sample != null) {
if (this.CheckSample(sample)) {
candidates++;
}
sample = this.iterator.GetSample();
}
}
public List<SolverTile> getTiles() {
return iterator.getTiles();
}
// this checks whether the positions of the mines are a valid candidate solution
private bool CheckSample(int[] sample) {
// get the tiles which are mines in this sample
SolverTile[] mine = new SolverTile[sample.Length];
for (int i = 0; i < sample.Length; i++) {
mine[i] = this.tiles[sample[i]];
}
for (int i = 0; i < this.witnesses.Count; i++) {
int flags1 = this.currentFlagsWitnesses[i];
int flags2 = 0;
// count how many candidate mines are next to this witness
for (int j = 0; j < mine.Length; j++) {
if (mine[j].IsAdjacent(this.witnesses[i].GetTile())) {
flags2++;
}
}
int flags3 = this.witnesses[i].GetTile().GetValue(); // number of flags indicated on the tile
if (flags3 != flags1 + flags2) {
//Console.WriteLine("Failed");
return false;
}
}
//if it is a good solution then calculate the distribution if required
//Console.WriteLine("Solution found");
sbyte[] solution = new sbyte[this.tiles.Count];
for (int i = 0; i < this.tiles.Count; i++) {
bool isMine = false;
for (int j = 0; j < sample.Length; j++) {
if (i == sample[j]) {
isMine = true;
break;
}
}
// if we are a mine then it doesn't matter how many mines surround us
if (!isMine) {
sbyte flags2 = this.currentFlagsTiles[i];
// count how many candidate mines are next to this square
for (int j = 0; j < mine.Length; j++) {
if (mine[j].IsAdjacent(this.tiles[i])) {
flags2++;
}
}
solution[i] = flags2;
} else {
solution[i] = BOMB;
}
}
bfa.AddSolution(solution);
/*
string output = "";
for (int i = 0; i < mine.length; i++) {
output = output + mine[i].asText();
}
console.log(output);
*/
return true;
}
public BruteForceAnalysis GetBruteForceAnalysis() {
return this.bfa;
}
public int GetSolutionsFound() {
return this.candidates;
}
}
// create an iterator which is like a set of rotating wheels
public class WitnessWebIterator {
private int[] sample;
private SequentialIterator[] cogs;
private int[] squareOffset;
private int[] mineOffset;
private List<SolverTile> tiles;
private int iterationsDone = 0;
readonly private int top;
readonly private int bottom;
private bool done = false;
/*
// if rotation is -1 then this does all the possible iterations
// if rotation is not - 1 then this locks the first 'cog' in that position and iterates the remaining cogs. This allows parallel processing based on the position of the first 'cog'
public WitnessWebIterator(ProbabilityEngine pe, List<SolverTile> allCoveredTiles, int rotation) {
this.tiles = new List<SolverTile>(); // list of tiles being iterated over
this.cogs = new SequentialIterator[pe.GetIndependentWitnesses().Count + 1]; // array of cogs
this.squareOffset = new int[pe.GetIndependentWitnesses().Count + 1]; // int array
this.mineOffset = new int[pe.GetIndependentWitnesses().Count + 1]; // int array
this.iterationsDone = 0;
this.done = false;
//this.probabilityEngine = pe;
// if we are setting the position of the top cog then it can't ever change
if (rotation == -1) {
this.bottom = 0;
} else {
this.bottom = 1;
}
List<SolverTile> loc = new List<SolverTile>(); // array of locations
List<BoxWitness> indWitnesses = pe.GetIndependentWitnesses();
int cogi = 0;
int indSquares = 0;
int indMines = 0;
// create an array of locations in the order of independent witnesses
foreach (BoxWitness w in indWitnesses) {
this.squareOffset[cogi] = indSquares;
this.mineOffset[cogi] = indMines;
this.cogs[cogi] = new SequentialIterator(w.GetMinesToFind(), w.GetAdjacentTiles().Count);
cogi++;
indSquares = indSquares + w.GetAdjacentTiles().Count;
indMines = indMines + w.GetMinesToFind();
loc.AddRange(w.GetAdjacentTiles());
}
//System.out.println("Mines left = " + (mines - indMines));
//System.out.println("Squrs left = " + (web.getSquares().length - indSquares));
// the last cog has the remaining squares and mines
//add the rest of the locations
for (int i = 0; i < allCoveredTiles.Count; i++) {
SolverTile l = allCoveredTiles[i];
bool skip = false;
for (int j = 0; j < loc.Count; j++) {
SolverTile m = loc[j];
if (l.IsEqual(m)) {
skip = true;
break;
}
}
if (!skip) {
loc.Add(l);
}
}
this.tiles = loc;
SolverInfo information = pe.GetSolverInfo();
int minesLeft = information.GetMinesLeft() - information.GetExcludedMineCount();
int tilesLeft = information.GetTilesLeft() - information.GetExcludedTiles().Count;
information.Write("Mines left " + minesLeft);
information.Write("Independent Mines " + indMines);
information.Write("Tiles left " + tilesLeft);
information.Write("Independent tiles " + indSquares);
// if there are more mines left then squares then no solution is possible
// if there are not enough mines to satisfy the minimum we know are needed
if (minesLeft - indMines > tilesLeft - indSquares
|| indMines > minesLeft) {
this.done = true;
this.top = 0;
Console.WriteLine("Nothing to do in this iterator");
return;
}
// if there are no mines left then no need for a cog
if (minesLeft > indMines) {
this.squareOffset[cogi] = indSquares;
this.mineOffset[cogi] = indMines;
this.cogs[cogi] = new SequentialIterator(minesLeft - indMines, tilesLeft - indSquares);
this.top = cogi;
} else {
top = cogi - 1;
}
//this.top = this.cogs.Length - 1;
this.sample = new int[minesLeft]; // make the sample array the size of the number of mines
// if we are locking and rotating the top cog then do it
if (rotation != -1) {
for (var i = 0; i < rotation; i++) {
this.cogs[0].GetNextSample();
}
}
// now set up the initial sample position
for (int i = 0; i < this.top; i++) {
int[] s = this.cogs[i].GetNextSample();
for (int j = 0; j < s.Length; j++) {
this.sample[this.mineOffset[i] + j] = this.squareOffset[i] + s[j];
}
}
}
*/
// if rotation is -1 then this does all the possible iterations
// if rotation is not - 1 then this locks the first 'cog' in that position and iterates the remaining cogs. This allows parallel processing based on the position of the first 'cog'
public WitnessWebIterator(SolverInfo information, List<BoxWitness> independentWitnesses, List<BoxWitness> depdendentWitnesses
, List<SolverTile> allCoveredTiles, int minesLeft, int tilesLeft, int rotation) {
this.tiles = new List<SolverTile>(); // list of tiles being iterated over
int cogs;
if (independentWitnesses == null) {
cogs = 1;
} else {
cogs = independentWitnesses.Count + 1;
}
this.cogs = new SequentialIterator[cogs]; // array of cogs
this.squareOffset = new int[cogs]; // int array
this.mineOffset = new int[cogs]; // int array
this.iterationsDone = 0;
this.done = false;
//this.probabilityEngine = pe;
// if we are setting the position of the top cog then it can't ever change
if (rotation == -1) {
this.bottom = 0;
} else {
this.bottom = 1;
}
List<SolverTile> loc = new List<SolverTile>(); // array of locations
int cogi = 0;
int indSquares = 0;
int indMines = 0;
// create an array of locations in the order of independent witnesses
if (independentWitnesses != null) {
foreach (BoxWitness w in independentWitnesses) {
this.squareOffset[cogi] = indSquares;
this.mineOffset[cogi] = indMines;
this.cogs[cogi] = new SequentialIterator(w.GetMinesToFind(), w.GetAdjacentTiles().Count);
cogi++;
indSquares = indSquares + w.GetAdjacentTiles().Count;
indMines = indMines + w.GetMinesToFind();
loc.AddRange(w.GetAdjacentTiles());
}
}
//System.out.println("Mines left = " + (mines - indMines));
//System.out.println("Squrs left = " + (web.getSquares().length - indSquares));
// the last cog has the remaining squares and mines
//add the rest of the locations
for (int i = 0; i < allCoveredTiles.Count; i++) {
SolverTile l = allCoveredTiles[i];
bool skip = false;
for (int j = 0; j < loc.Count; j++) {
SolverTile m = loc[j];
if (l.IsEqual(m)) {
skip = true;
break;
}
}
if (!skip) {
loc.Add(l);
}
}
this.tiles = loc;
information.Write("Mines left " + minesLeft);
information.Write("Independent Mines " + indMines);
information.Write("Tiles left " + tilesLeft);
information.Write("Independent tiles " + indSquares);
// if there are more mines left then squares then no solution is possible
// if there are not enough mines to satisfy the minimum we know are needed
if (minesLeft - indMines > tilesLeft - indSquares
|| indMines > minesLeft) {
this.done = true;
this.top = 0;
Console.WriteLine("Nothing to do in this iterator");
return;
}
// if there are no mines left then no need for a cog
if (minesLeft > indMines) {
this.squareOffset[cogi] = indSquares;
this.mineOffset[cogi] = indMines;
this.cogs[cogi] = new SequentialIterator(minesLeft - indMines, tilesLeft - indSquares);
this.top = cogi;
} else {
top = cogi - 1;
}
//this.top = this.cogs.Length - 1;
this.sample = new int[minesLeft]; // make the sample array the size of the number of mines
// if we are locking and rotating the top cog then do it
if (rotation != -1) {
for (var i = 0; i < rotation; i++) {
this.cogs[0].GetNextSample();
}
}
// now set up the initial sample position
for (int i = 0; i < this.top; i++) {
int[] s = this.cogs[i].GetNextSample();
for (int j = 0; j < s.Length; j++) {
this.sample[this.mineOffset[i] + j] = this.squareOffset[i] + s[j];
}
}
}
public int[] GetSample() {
if (this.done) {
Console.WriteLine("**** attempting to iterator when already completed ****");
return null;
}
int index = this.top;
int[] s = this.cogs[index].GetNextSample();
while (s == null && index != this.bottom) {
index--;
s = this.cogs[index].GetNextSample();
}
if (index == this.bottom && s == null) {
this.done = true;
return null;
}
for (int j = 0; j < s.Length; j++) {
this.sample[this.mineOffset[index] + j] = this.squareOffset[index] + s[j];
}
index++;
while (index <= this.top) {
this.cogs[index] = new SequentialIterator(this.cogs[index].GetNumberBalls(), this.cogs[index].GetNumberHoles());
s = this.cogs[index].GetNextSample();
for (int j = 0; j < s.Length; j++) {
this.sample[this.mineOffset[index] + j] = this.squareOffset[index] + s[j];
}
index++;
}
/*
String output = "";
for (int j = 0; j < sample.Length; j++) {
output = output + this.sample[j] + " ";
}
Console.WriteLine(output);
*/
this.iterationsDone++;
return this.sample;
}
public List<SolverTile> getTiles() {
return this.tiles;
}
public int GetIterations() {
return iterationsDone;
}
/*
// if the location is a Independent witness then we know it will always
// have exactly the correct amount of mines around it since that is what
// this iterator does
public bool WitnessAlwaysSatisfied(SolverTile location) {
for (var i = 0; i < this.probabilityEngine.independentWitness.length; i++) {
if (this.probabilityEngine.independentWitness[i].equals(location)) {
return true;
}
}
return false;
}
*/
}
public class SequentialIterator {
readonly private int[] sample;
readonly private int numberHoles;
readonly private int numberBalls;
private bool more;
private int index;
// a sequential iterator that puts n-balls in m-holes once in each possible way
public SequentialIterator(int n, int m) {
this.numberHoles = m;
this.numberBalls = n;
this.sample = new int[n];
this.more = true;
this.index = n - 1;
for (int i = 0; i < n; i++) {
this.sample[i] = i;
}
// reduce the iterator by 1, since the first getSample() will increase it
// by 1 again
this.sample[this.index]--;
//Console.WriteLine("Sequential Iterator has " + this.numberBalls + " mines and " + this.numberHoles + " squares");
}
public int[] GetNextSample() {
if (!this.more) {
Console.WriteLine("**** Trying to iterate after the end ****");
return null;
}
this.index = this.numberBalls - 1;
// add on one to the iterator
this.sample[this.index]++;
// if we have rolled off the end then move backwards until we can fit
// the next iteration
while (this.sample[this.index] >= this.numberHoles - this.numberBalls + 1 + this.index) {
if (this.index == 0) {
this.more = false;
return null;
} else {
this.index--;
this.sample[this.index]++;
}
}
// roll forward
while (this.index != this.numberBalls - 1) {
this.index++;
this.sample[this.index] = this.sample[this.index - 1] + 1;
}
return this.sample;
}
public int GetNumberBalls() {
return this.numberBalls;
}
public int GetNumberHoles() {
return this.numberHoles;
}
}
}

View File

@ -0,0 +1,837 @@
using System;
using System.Collections.Generic;
using System.Text;
using static MinesweeperControl.MinesweeperGame;
namespace MinesweeperSolver {
public class BruteForceAnalysis {
// used to hold all the solutions left in the game
public class SolutionTable {
private readonly object locker = new object();
private readonly BruteForceAnalysis bfa;
private readonly sbyte[][] solutions;
private int size = 0;
public SolutionTable(BruteForceAnalysis bfa, int maxSize) {
this.bfa = bfa;
solutions = new sbyte[maxSize][];
}
public void AddSolution(sbyte[] solution) {
lock(locker) {
solutions[size] = solution;
size++;
}
}
public int GetSize() {
return size;
}
public sbyte[] Get(int index) {
return solutions[index];
}
public void SortSolutions(int start, int end, int index) {
Array.Sort(solutions, start, end - start, bfa.sorters[index]);
}
}
/**
* This sorts solutions by the value of a position
*/
public class SortSolutions : IComparer<sbyte[]> {
private readonly int sortIndex;
public SortSolutions(int index) {
sortIndex = index;
}
public int Compare(sbyte[] o1, sbyte[] o2) {
return o1[sortIndex] - o2[sortIndex];
}
}
/**
* A key to uniquely identify a position
*/
public class Position {
private readonly byte[] position;
private int hash;
public Position(int size) {
position = new byte[size];
for (int i = 0; i < position.Length; i++) {
position[i] = 15;
}
}
public Position(Position p, int index, int value) {
// copy and update to reflect the new position
position = new byte[p.position.Length];
Array.Copy(p.position, position, p.position.Length);
position[index] = (byte)(value + 50);
}
// copied from String hash
public override int GetHashCode() {
int h = hash;
if (h == 0 && position.Length > 0) {
for (int i = 0; i < position.Length; i++) {
h = 31 * h + position[i];
}
hash = h;
}
return h;
}
public override bool Equals(Object o) {
if (o is Position) {
for (int i = 0; i < position.Length; i++) {
if (this.position[i] != ((Position)o).position[i]) {
return false;
}
}
return true;
} else {
return false;
}
}
}
/**
* Positions on the board which can still reveal information about the game.
*/
public class LivingLocation : IComparable<LivingLocation> {
//private int winningLines = 0;
public bool pruned = false;
public readonly short index;
public int mineCount = 0; // number of remaining solutions which have a mine in this position
public int maxSolutions = 0; // the maximum number of solutions that can be remaining after clicking here
public int zeroSolutions = 0; // the number of solutions that have a '0' value here
public sbyte maxValue = -1;
public sbyte minValue = -1;
public byte count; // number of possible values at this location
public Node[] children;
private readonly BruteForceAnalysis bfa;
public LivingLocation(BruteForceAnalysis bfa, short index) {
this.index = index;
this.bfa = bfa;
}
/**
* Determine the Nodes which are created if we play this move. Up to 9 positions where this locations reveals a value [0-8].
* @param location
* @return
*/
public void BuildChildNodes(Node parent) {
// sort the solutions by possible values
bfa.allSolutions.SortSolutions(parent.startLocation, parent.endLocation, this.index);
int index = parent.startLocation;
// skip over the mines
while (index < parent.endLocation && bfa.allSolutions.Get(index)[this.index] == Cruncher.BOMB) {
index++;
}
Node[] work = new Node[9];
for (int i = this.minValue; i < this.maxValue + 1; i++) {
// if the node is in the cache then use it
Position pos = new Position(parent.position, this.index, i);
if (!bfa.cache.TryGetValue(pos, out Node temp1)) { // if value not in cache
Node temp = new Node(bfa, pos);
temp.startLocation = index;
// find all solutions for this values at this location
while (index < parent.endLocation && bfa.allSolutions.Get(index)[this.index] == i) {
index++;
}
temp.endLocation = index;
work[i] = temp;
} else { // value in cache
//System.out.println("In cache " + temp.position.key + " " + temp1.position.key);
//if (!temp.equals(temp1)) {
// System.out.println("Cache not equal!!");
//}
//temp1.fromCache = true;
work[i] = temp1;
bfa.cacheHit++;
bfa.cacheWinningLines = bfa.cacheWinningLines + temp1.winningLines;
// skip past these details in the array
while (index < parent.endLocation && bfa.allSolutions.Get(index)[this.index] <= i) {
index++;
}
}
}
if (index != parent.endLocation) {
Console.WriteLine("Didn't read all the elements in the array; index = " + index + " end = " + parent.endLocation);
}
for (int i = this.minValue; i <= this.maxValue; i++) {
if (work[i].GetSolutionSize() > 0) {
//if (!work[i].fromCache) {
// work[i].determineLivingLocations(this.livingLocations, living.index);
//}
} else {
work[i] = null; // if no solutions then don't hold on to the details
}
}
this.children = work;
}
public int CompareTo(LivingLocation o) {
// return location most likely to be clear - this has to be first, the logic depends upon it
int test = this.mineCount - o.mineCount;
if (test != 0) {
return test;
}
// then the location most likely to have a zero
test = o.zeroSolutions - this.zeroSolutions;
if (test != 0) {
return test;
}
// then by most number of different possible values
test = o.count - this.count;
if (test != 0) {
return test;
}
// then by the maxSolutions - ascending
return this.maxSolutions - o.maxSolutions;
}
}
/**
* A representation of a possible state of the game
*/
public class Node {
public Position position; // representation of the position we are analysing / have reached
public int winningLines = 0; // this is the number of winning lines below this position in the tree
public int work = 0; // this is a measure of how much work was needed to calculate WinningLines value
private bool fromCache = false; // indicates whether this position came from the cache
public int startLocation; // the first solution in the solution array that applies to this position
public int endLocation; // the last + 1 solution in the solution array that applies to this position
public List<LivingLocation> livingLocations; // these are the locations which need to be analysed
public LivingLocation bestLiving; // after analysis this is the location that represents best play
private readonly BruteForceAnalysis bfa;
public Node(BruteForceAnalysis bfa, int size) {
position = new Position(size);
this.bfa = bfa;
}
public Node(BruteForceAnalysis bfa, Position position) {
this.position = position;
this.bfa = bfa;
}
public List<LivingLocation> GetLivingLocations() {
return livingLocations;
}
public int GetSolutionSize() {
return endLocation - startLocation;
}
/**
* Get the probability of winning the game from the position this node represents (winningLines / solution size)
* @return
*/
public double GetProbability() {
return ((double)winningLines) / GetSolutionSize();
//return BigDecimal.valueOf(winningLines).divide(BigDecimal.valueOf(getSolutionSize()), Solver.DP, RoundingMode.HALF_UP);
}
/**
* Calculate the number of winning lines if this move is played at this position
* Used at top of the game tree
*/
public int GetWinningLines(LivingLocation move) {
//if we can never exceed the cutoff then no point continuing
if (SolverMain.PRUNE_BF_ANALYSIS && this.GetSolutionSize() - move.mineCount <= this.winningLines) {
move.pruned = true;
return 0;
}
int winningLines = GetWinningLines(1, move, this.winningLines);
if (winningLines > this.winningLines) {
this.winningLines = winningLines;
}
return winningLines;
}
/**
* Calculate the number of winning lines if this move is played at this position
* Used when exploring the game tree
*/
public int GetWinningLines(int depth, LivingLocation move, int cutoff) {
int result = 0;
bfa.processCount++;
if (bfa.processCount > SolverMain.BRUTE_FORCE_ANALYSIS_MAX_NODES) {
return 0;
}
int notMines = this.GetSolutionSize() - move.mineCount;
move.BuildChildNodes(this);
foreach (Node child in move.children) {
if (child == null) {
continue; // continue the loop but ignore this entry
}
int maxWinningLines = result + notMines;
// if the max possible winning lines is less than the current cutoff then no point doing the analysis
if (SolverMain.PRUNE_BF_ANALYSIS && maxWinningLines <= cutoff) {
move.pruned = true;
return 0;
}
if (child.fromCache) { // nothing more to do, since we did it before
this.work++;
} else {
child.DetermineLivingLocations(this.livingLocations, move.index);
this.work++;
if (child.GetLivingLocations().Count == 0) { // no further information ==> all solution indistinguishable ==> 1 winning line
child.winningLines = 1;
} else { // not cached and not terminal node, so we need to do the recursion
foreach (LivingLocation childMove in child.GetLivingLocations()) {
// if the number of safe solutions <= the best winning lines then we can't do any better, so skip the rest
if (child.GetSolutionSize() - childMove.mineCount <= child.winningLines) {
break;
}
// now calculate the winning lines for each of these children
int winningLines = child.GetWinningLines(depth + 1, childMove, child.winningLines);
if (child.winningLines < winningLines || (child.bestLiving != null && child.winningLines == winningLines && child.bestLiving.mineCount < childMove.mineCount)) {
child.winningLines = winningLines;
child.bestLiving = childMove;
}
// if there are no mines then this is a 100% safe move, so skip any further analysis since it can't be any better
if (childMove.mineCount == 0) {
break;
}
}
// no need to hold onto the living location once we have determined the best of them
child.livingLocations = null;
//if (depth > solver.preferences.BRUTE_FORCE_ANALYSIS_TREE_DEPTH) { // stop holding the tree beyond this depth
// child.bestLiving = null;
//}
// add the child to the cache if it didn't come from there and it is carrying sufficient winning lines
if (child.work > 30) {
child.work = 0;
child.fromCache = true;
bfa.cacheSize++;
bfa.cache.Add(child.position, child);
} else {
this.work = this.work + child.work;
}
}
}
if (depth > SolverMain.BRUTE_FORCE_ANALYSIS_TREE_DEPTH) { // stop holding the tree beyond this depth
child.bestLiving = null;
}
// store the aggregate winning lines
result = result + child.winningLines;
notMines = notMines - child.GetSolutionSize(); // reduce the number of not mines
}
return result;
}
/**
* this generates a list of Location that are still alive, (i.e. have more than one possible value) from a list of previously living locations
* Index is the move which has just been played (in terms of the off-set to the position[] array)
*/
public void DetermineLivingLocations(List<LivingLocation> liveLocs, int index) {
List<LivingLocation> living = new List<LivingLocation>(liveLocs.Count);
foreach (LivingLocation live in liveLocs) {
if (live.index == index) { // if this is the same move we just played then no need to analyse it - definitely now non-living.
continue;
}
int value;
int[] valueCount = bfa.ResetValues();
int mines = 0;
int maxSolutions = 0;
byte count = 0;
sbyte minValue = 0;
sbyte maxValue = 0;
for (int j = startLocation; j < endLocation; j++) {
value = bfa.allSolutions.Get(j)[live.index];
if (value != Cruncher.BOMB) {
//values[value] = true;
valueCount[value]++;
} else {
mines++;
}
}
// find the new minimum value and maximum value for this location (can't be wider than the previous min and max)
for (sbyte j = live.minValue; j <= live.maxValue; j++) {
if (valueCount[j] > 0) {
if (count == 0) {
minValue = j;
}
maxValue = j;
count++;
if (maxSolutions < valueCount[j]) {
maxSolutions = valueCount[j];
}
}
}
if (count > 1) {
LivingLocation alive = new LivingLocation(bfa, live.index);
alive.mineCount = mines;
alive.count = count;
alive.minValue = minValue;
alive.maxValue = maxValue;
alive.maxSolutions = maxSolutions;
alive.zeroSolutions = valueCount[0];
living.Add(alive);
}
}
living.Sort();
//Collections.sort(living);
this.livingLocations = living;
}
public override int GetHashCode() {
return position.GetHashCode();
}
public override bool Equals(Object o) {
if (o is Node) {
return position.Equals(((Node)o).position);
} else {
return false;
}
}
}
// start of main class
private static readonly String INDENT = "................................................................................";
//private static readonly BigDecimal ONE_HUNDRED = BigDecimal.valueOf(100);
public int processCount = 0;
private readonly SolverInfo solver;
private readonly int maxSolutionSize;
//private Node top;
private readonly List<SolverTile> locations; // the positions being analysed
private readonly List<SolverTile> startLocations; // the positions which will be considered for the first move
private readonly SolutionTable allSolutions;
//private readonly String scope;
private Node currentNode;
private SolverTile expectedMove;
private readonly SortSolutions[] sorters;
private int cacheHit = 0;
public int cacheSize = 0;
private int cacheWinningLines = 0;
private bool allDead = false; // this is true if all the locations are dead
private bool tooMany = false;
private bool completed = false;
// some work areas to prevent having to instantiate many 1000's of copies of them
//private final boolean[] values = new boolean[9];
private readonly int[] valueCount = new int[9];
private Dictionary<Position, Node> cache = new Dictionary<Position, Node>(5000);
public BruteForceAnalysis(SolverInfo solver, List<SolverTile> locations, int size, List<SolverTile> startLocations) {
this.solver = solver;
this.locations = locations;
this.maxSolutionSize = size;
//this.top = new Node();
sorters = new SortSolutions[locations.Count];
for (int i = 0; i < sorters.Length; i++) {
sorters[i] = new SortSolutions(i);
}
this.allSolutions = new SolutionTable(this, size);
this.startLocations = startLocations;
}
public void AddSolution(sbyte[] solution) {
if (solution.Length != locations.Count) {
throw new Exception("Solution does not have the correct number of locations");
}
if (allSolutions.GetSize() >= maxSolutionSize) {
tooMany = true;
return;
}
/*
String text = "";
for (int i=0; i < solution.length; i++) {
text = text + solution[i] + " ";
}
solver.display(text);
*/
allSolutions.AddSolution(solution);
}
public void process() {
long start = DateTime.Now.Ticks;
solver.Write("----- Brute Force Deep Analysis starting ----");
solver.Write(allSolutions.GetSize() + " solutions in BruteForceAnalysis");
// create the top node
Node top = buildTopNode(allSolutions);
if (top.GetLivingLocations().Count == 0) {
allDead = true;
}
int best = 0;
foreach (LivingLocation move in top.GetLivingLocations()) {
// check that the move is in the startLocation list
if (startLocations != null) {
bool found = false;
foreach (SolverTile l in startLocations) {
if (locations[move.index].Equals(l)) {
found = true;
break;
}
}
if (!found) { // if not then skip this move
solver.Write(move.index + " " + locations[move.index].AsText() + " is not a starting location");
continue;
}
}
int winningLines = top.GetWinningLines(move); // calculate the number of winning lines if this move is played
if (best < winningLines || (top.bestLiving != null && best == winningLines && top.bestLiving.mineCount < move.mineCount)) {
best = winningLines;
top.bestLiving = move;
}
double singleProb = (allSolutions.GetSize() - move.mineCount) / allSolutions.GetSize();
if (move.pruned) {
solver.Write(move.index + " " + locations[move.index].AsText() + " is living with " + move.count + " possible values and probability " + singleProb + ", this location was pruned");
} else {
solver.Write(move.index + " " + locations[move.index].AsText() + " is living with " + move.count + " possible values and probability " + singleProb + ", winning lines " + winningLines);
}
}
top.winningLines = best;
currentNode = top;
if (processCount < SolverMain.BRUTE_FORCE_ANALYSIS_MAX_NODES) {
this.completed = true;
//if (solver.isShowProbabilityTree()) {
// solver.newLine("--------- Probability Tree dump start ---------");
// showTree(0, 0, top);
// solver.newLine("---------- Probability Tree dump end ----------");
//}
}
// clear down the cache
cache.Clear();
long end = DateTime.Now.Ticks;
solver.Write("Total nodes in cache = " + cacheSize + ", total cache hits = " + cacheHit + ", total winning lines saved = " + this.cacheWinningLines);
solver.Write("process took " + (end - start) + " milliseconds and explored " + processCount + " nodes");
solver.Write("----- Brute Force Deep Analysis finished ----");
}
/**
* Builds a top of tree node based on the solutions provided
*/
private Node buildTopNode(SolutionTable solutionTable) {
Node result = new Node(this, locations.Count);
result.startLocation = 0;
result.endLocation = solutionTable.GetSize();
List<LivingLocation> living = new List<LivingLocation>();
for (short i = 0; i < locations.Count; i++) {
int value;
int[] valueCount = ResetValues();
int mines = 0;
int maxSolutions = 0;
byte count = 0;
sbyte minValue = 0;
sbyte maxValue = 0;
for (int j = 0; j < result.GetSolutionSize(); j++) {
if (solutionTable.Get(j)[i] != Cruncher.BOMB) {
value = solutionTable.Get(j)[i];
//values[value] = true;
valueCount[value]++;
} else {
mines++;
}
}
for (sbyte j = 0; j < valueCount.Length; j++) {
if (valueCount[j] > 0) {
if (count == 0) {
minValue = j;
}
maxValue = j;
count++;
if (maxSolutions < valueCount[j]) {
maxSolutions = valueCount[j];
}
}
}
if (count > 1) {
LivingLocation alive = new LivingLocation(this, i);
alive.mineCount = mines;
alive.count = count;
alive.minValue = minValue;
alive.maxValue = maxValue;
alive.maxSolutions = maxSolutions;
alive.zeroSolutions = valueCount[0];
living.Add(alive);
} else {
solver.Write(locations[i].AsText() + " is dead with value " + minValue);
}
}
living.Sort();
//Collections.sort(living);
result.livingLocations = living;
return result;
}
public int[] ResetValues() {
for (int i = 0; i < valueCount.Length; i++) {
valueCount[i] = 0;
}
return valueCount;
}
public int GetSolutionCount() {
return allSolutions.GetSize();
}
public int GetNodeCount() {
return processCount;
}
public SolverAction GetNextMove() {
LivingLocation bestLiving = getBestLocation(currentNode);
if (bestLiving == null) {
return null;
}
SolverTile loc = this.locations[bestLiving.index];
//solver.display("first best move is " + loc.display());
double prob = 1 - bestLiving.mineCount / currentNode.GetSolutionSize();
while (!loc.IsHidden()) {
int value = loc.GetValue();
currentNode = bestLiving.children[value];
bestLiving = getBestLocation(currentNode);
if (bestLiving == null) {
return null;
}
prob = 1 - ((double) bestLiving.mineCount) / currentNode.GetSolutionSize();
loc = this.locations[bestLiving.index];
}
solver.Write("mines = " + bestLiving.mineCount + " solutions = " + currentNode.GetSolutionSize());
for (int i = 0; i < bestLiving.children.Length; i++) {
if (bestLiving.children[i] == null) {
//solver.display("Value of " + i + " is not possible");
continue; //ignore this node but continue the loop
}
String probText;
if (bestLiving.children[i].bestLiving == null) {
probText = (100 / (bestLiving.children[i].GetSolutionSize())) + "%";
} else {
probText = bestLiving.children[i].GetProbability() * 100 + "%";
}
solver.Write("Value of " + i + " leaves " + bestLiving.children[i].GetSolutionSize() + " solutions and winning probability " + probText + " (work size " + bestLiving.children[i].work + ")");
}
//String text = " (solve " + (currentNode.GetProbability() * 100) + "%)";
SolverAction action = new SolverAction(loc, ActionType.Clear, 0.5);
expectedMove = loc;
return action;
}
private LivingLocation getBestLocation(Node node) {
return node.bestLiving;
}
private void ShowTree(int depth, int value, Node node) {
String condition;
if (depth == 0) {
condition = node.GetSolutionSize() + " solutions remain";
} else {
condition = "When '" + value + "' ==> " + node.GetSolutionSize() + " solutions remain";
}
if (node.bestLiving == null) {
String line1 = INDENT.Substring(0, depth * 3) + condition + " Solve chance " + node.GetProbability() * 100 + "%";
Console.WriteLine(line1);
return;
}
SolverTile loc = this.locations[node.bestLiving.index];
double prob = 1 - node.bestLiving.mineCount / node.GetSolutionSize();
String line = INDENT.Substring(0, depth * 3) + condition + " play " + loc.AsText() + " Survival chance " + prob * 100 + "%, Solve chance " + node.GetProbability() * 100 + "%";
Console.WriteLine(line);
//for (Node nextNode: node.bestLiving.children) {
for (int val = 0; val < node.bestLiving.children.Length; val++) {
Node nextNode = node.bestLiving.children[val];
if (nextNode != null) {
ShowTree(depth + 1, val, nextNode);
}
}
}
public bool IsComplete() {
return this.completed;
}
public SolverTile GetExpectedMove() {
return expectedMove;
}
//private String percentage(double prob) {
// return Action.FORMAT_2DP.format(prob.multiply(ONE_HUNDRED));
//}
public bool GetAllDead() {
return allDead;
}
}
}

View File

@ -0,0 +1,45 @@
using System;
using System.Numerics;
namespace MinesweeperSolver {
public class Combination {
public static BigInteger Calculate(int mines, int squares) {
long start = DateTime.Now.Ticks;
BigInteger top = 1;
BigInteger bot = 1;
var range = Math.Min(mines, squares - mines);
// calculate the combination.
for (int i = 0; i < range; i++) {
top = top * (squares - i);
bot = bot * (i + 1);
}
BigInteger result = top / bot;
//SolverMain.Write(squares + " pick " + mines + " in " + result + " ways");
//SolverMain.Write("Combination duration " + (DateTime.Now.Ticks - start) + " ticks");
return result;
}
private static readonly BigInteger[] power10n = { BigInteger.One, new BigInteger(10), new BigInteger(100), new BigInteger(1000), new BigInteger(10000), new BigInteger(100000), new BigInteger(1000000) };
private static readonly int[] power10 = { 1, 10, 100, 1000, 10000, 100000, 1000000 };
public static double DivideBigIntegerToDouble(BigInteger numerator, BigInteger denominator, int dp) {
var work = numerator * power10n[dp] / denominator;
var result = (double) work / power10[dp];
return result;
}
}
}

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
<HasSharedItems>true</HasSharedItems>
<SharedGUID>60591e9a-f066-4af3-b1e3-b20b2b2b0b1d</SharedGUID>
</PropertyGroup>
<PropertyGroup Label="Configuration">
<Import_RootNamespace>MinesweeperSolver</Import_RootNamespace>
</PropertyGroup>
<ItemGroup>
<Compile Include="$(MSBuildThisFileDirectory)Binomial.cs" />
<Compile Include="$(MSBuildThisFileDirectory)BruteForce.cs" />
<Compile Include="$(MSBuildThisFileDirectory)BruteForceAnalysis.cs" />
<Compile Include="$(MSBuildThisFileDirectory)Combination.cs" />
<Compile Include="$(MSBuildThisFileDirectory)PrimeSieve.cs" />
<Compile Include="$(MSBuildThisFileDirectory)SolutionCounter.cs" />
<Compile Include="$(MSBuildThisFileDirectory)ProbabilityEngine.cs" />
<Compile Include="$(MSBuildThisFileDirectory)SolverAction.cs" />
<Compile Include="$(MSBuildThisFileDirectory)SolverActionHeader.cs" />
<Compile Include="$(MSBuildThisFileDirectory)SolverInfo.cs" />
<Compile Include="$(MSBuildThisFileDirectory)SolverMain.cs" />
<Compile Include="$(MSBuildThisFileDirectory)SolverTile.cs" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Label="Globals">
<ProjectGuid>60591e9a-f066-4af3-b1e3-b20b2b2b0b1d</ProjectGuid>
<MinimumVisualStudioVersion>14.0</MinimumVisualStudioVersion>
</PropertyGroup>
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.Common.Default.props" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.Common.props" />
<PropertyGroup />
<Import Project="MinesweeperSolver.projitems" Label="Shared" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\CodeSharing\Microsoft.CodeSharing.CSharp.targets" />
</Project>

View File

@ -0,0 +1,129 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
namespace MinesweeperSolver {
public class PrimeSieve {
// iterator for prime numbers
public class Primes : IEnumerable<int>, IEnumerator<int> {
private int index = 0;
private readonly int stop;
private int nextPrime;
private readonly bool[] composite;
public int Current => Next();
object IEnumerator.Current => Next();
public Primes(bool[] composite, int start, int stop) {
this.index = start;
this.stop = stop;
this.composite = composite;
this.nextPrime = findNext();
}
public int Next() {
int result = nextPrime;
nextPrime = findNext();
return result;
}
private int findNext() {
int next = -1;
while (index <= stop && next == -1) {
if (!composite[index]) {
next = index;
}
index++;
}
return next;
}
public void Dispose() {
}
public bool MoveNext() {
return (nextPrime != -1);
}
public void Reset() {
throw new NotImplementedException();
}
public IEnumerator<int> GetEnumerator() {
return this;
}
IEnumerator IEnumerable.GetEnumerator() {
return this;
}
}
private readonly bool[] composite;
private readonly int max;
public PrimeSieve(int n) {
if (n < 2) {
max = 2;
} else {
max = n;
}
composite = new bool[max + 1];
int rootN = (int)Math.Floor(Math.Sqrt(n));
for (int i = 2; i < rootN; i++) {
// if this is a prime number (not composite) then sieve the array
if (!composite[i]) {
int index = i + i;
while (index <= max) {
composite[index] = true;
index = index + i;
}
}
}
}
public bool IsPrime(int n) {
if (n <= 1 || n > max) {
throw new Exception("Test value " + n + " is out of range 2 - " + max);
}
return !composite [n];
}
public IEnumerable<int> getPrimesIterable(int start, int stop) {
if (start > stop) {
throw new Exception("start " + start + " must be <= to stop " + stop);
}
if (start <= 1 || start > max) {
throw new Exception("Start value " + start + " is out of range 2 - " + max);
}
if (stop <= 1 || stop > max) {
throw new Exception("Stop value " + stop + " is out of range 2 - " + max);
}
return new Primes(composite, start, stop);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,859 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using static MinesweeperControl.MinesweeperGame;
namespace MinesweeperSolver {
public class SolutionCounter {
private readonly int[][] SMALL_COMBINATIONS = new int[][] { new int[] { 1 }, new int[] { 1, 1 }, new int[] { 1, 2, 1 }, new int[] { 1, 3, 3, 1 }, new int[] { 1, 4, 6, 4, 1 }, new int[] { 1, 5, 10, 10, 5, 1 }, new int[] { 1, 6, 15, 20, 15, 6, 1 }, new int[] { 1, 7, 21, 35, 35, 21, 7, 1 }, new int[] { 1, 8, 28, 56, 70, 56, 28, 8, 1 } };
private readonly List<SolverTile> witnessed;
private readonly List<BoxWitness> prunedWitnesses = new List<BoxWitness>(); // a subset of allWitnesses with equivalent witnesses removed
private readonly List<Box> boxes = new List<Box>();
private readonly List<BoxWitness> boxWitnesses = new List<BoxWitness>();
private bool[] mask;
private readonly Dictionary<SolverTile, Box> boxLookup = new Dictionary<SolverTile, Box>(); // a lookup which finds which box a tile belongs to (an efficiency enhancement)
//private readonly List<DeadCandidate> deadCandidates = new List<DeadCandidate>();
private List<ProbabilityLine> workingProbs = new List<ProbabilityLine>();
private readonly List<ProbabilityLine> heldProbs = new List<ProbabilityLine>();
private readonly List<EdgeStore> edgeStore = new List<EdgeStore>(); // stores independent edge analysis until we know we have to merge them
private readonly int minesLeft;
private readonly int tilesLeft;
private readonly int tilesOffEdge;
private readonly int minTotalMines;
private readonly int maxTotalMines;
private int recursions;
// used to find the range of mine counts which offer the 2.5% - 97.5% weighted average
private int edgeMinesMin;
private int edgeMinesMax;
private int edgeMinesMinLeft;
private int edgeMinesMaxLeft;
private int mineCountUpperCutoff;
private int mineCountLowerCutoff;
private bool truncatedProbs = false; // this gets set when the number of held probabilies exceeds the permitted threshold
private BigInteger finalSolutionCount = 0;
private BigInteger solutionCountMultiplier = 1;
private int clearCount = 0;
private readonly SolverInfo information;
public SolutionCounter(SolverInfo information, List<SolverTile> allWitnesses, List<SolverTile> allWitnessed, int squaresLeft, int minesLeft) {
this.information = information;
this.witnessed = allWitnessed;
// constraints in the game
this.minesLeft = minesLeft;
this.tilesLeft = squaresLeft;
this.tilesOffEdge = squaresLeft - allWitnessed.Count; // squares left off the edge and unrevealed
this.minTotalMines = minesLeft - this.tilesOffEdge; //we can't use so few mines that we can't fit the remainder elsewhere on the board
this.maxTotalMines = minesLeft;
this.mineCountUpperCutoff = minesLeft;
this.mineCountLowerCutoff = minTotalMines;
information.Write("Tiles off edge " + tilesOffEdge);
//this.boxProb = []; // the probabilities end up here
// generate a BoxWitness for each witness tile and also create a list of pruned witnesses for the brute force search
int pruned = 0;
foreach (SolverTile wit in allWitnesses) {
BoxWitness boxWit = new BoxWitness(information, wit);
// if the witness is a duplicate then don't store it
bool duplicate = false;
foreach (BoxWitness w in this.boxWitnesses) {
if (w.Equivalent(boxWit)) {
//if (boardState.getWitnessValue(w) - boardState.countAdjacentConfirmedFlags(w) != boardState.getWitnessValue(wit) - boardState.countAdjacentConfirmedFlags(wit)) {
// boardState.display(w.display() + " and " + wit.display() + " share unrevealed squares but have different mine totals!");
// validWeb = false;
//}
duplicate = true;
break;
}
}
if (!duplicate) {
this.prunedWitnesses.Add(boxWit);
} else {
pruned++;
}
this.boxWitnesses.Add(boxWit); // all witnesses are needed for the probability engine
}
information.Write("Pruned " + pruned + " witnesses as duplicates");
information.Write("There are " + this.boxWitnesses.Count + " Box witnesses");
// allocate each of the witnessed squares to a box
int uid = 0;
foreach (SolverTile tile in this.witnessed) {
// for each adjacent tile see if it is a witness
int count = 0;
//foreach (SolverTile adjTile in information.GetAdjacentTiles(tile)) {
// if (information.GetWitnesses().Contains(adjTile)) {
// count++;
// }
//}
// count how many adjacent witnesses the tile has
foreach (SolverTile tile1 in allWitnesses) {
if (tile.IsAdjacent(tile1)) {
count++;
}
}
// see if the witnessed tile fits any existing boxes
bool found = false;
foreach (Box box in this.boxes) {
if (box.Fits(tile, count)) {
box.Add(tile);
boxLookup.Add(tile, box); // add this to the lookup
found = true;
break;
}
}
// if not found create a new box and store it
if (!found) {
Box box = new Box(this.boxWitnesses, tile, uid++);
this.boxes.Add(box);
boxLookup.Add(tile, box); // add this to the lookup
}
}
// calculate the min and max mines for each box
foreach (Box box in this.boxes) {
box.Calculate(this.minesLeft);
//console.log("Box " + box.tiles[0].asText() + " has min mines = " + box.minMines + " and max mines = " + box.maxMines);
}
// Report how many boxes each witness is adjacent to
foreach (BoxWitness boxWit in this.boxWitnesses) {
information.Write("Witness " + boxWit.GetTile().AsText() + " is adjacent to " + boxWit.GetBoxes().Count + " boxes and has " + boxWit.GetMinesToFind() + " mines to find");
}
}
// calculate a probability for each un-revealed tile on the board
public void Process() {
this.mask = new bool[this.boxes.Count];
// create an initial solution of no mines anywhere
ProbabilityLine held = new ProbabilityLine(this.boxes.Count);
held.SetSolutionCount(1);
this.heldProbs.Add(held);
// add an empty probability line to get us started
this.workingProbs.Add(new ProbabilityLine(this.boxes.Count));
NextWitness nextWitness = FindFirstWitness();
while (nextWitness != null) {
// mark the new boxes as processed - which they will be soon
foreach (Box box in nextWitness.GetNewBoxes()) {
this.mask[box.GetUID()] = true;
}
this.workingProbs = MergeProbabilities(nextWitness);
nextWitness = FindNextWitness(nextWitness);
}
CalculateBoxProbabilities();
}
// take the next witness details and merge them into the currently held details
private List<ProbabilityLine> MergeProbabilities(NextWitness nw) {
List<ProbabilityLine> newProbs = new List<ProbabilityLine>();
foreach (ProbabilityLine pl in this.workingProbs) {
int missingMines = nw.GetBoxWitness().GetMinesToFind() - (int)CountPlacedMines(pl, nw);
if (missingMines < 0) {
//console.log("Missing mines < 0 ==> ignoring line");
// too many mines placed around this witness previously, so this probability can't be valid
} else if (missingMines == 0) {
//console.log("Missing mines = 0 ==> keeping line as is");
newProbs.Add(pl); // witness already exactly satisfied, so nothing to do
} else if (nw.GetNewBoxes().Count == 0) {
//console.log("new boxes = 0 ==> ignoring line since nowhere for mines to go");
// nowhere to put the new mines, so this probability can't be valid
} else {
List<ProbabilityLine> result = DistributeMissingMines(pl, nw, missingMines, 0);
newProbs.AddRange(result);
}
}
//if (newProbs.length == 0) {
// console.log("Returning no lines from merge probability !!");
//}
return newProbs;
}
// counts the number of mines already placed
private BigInteger CountPlacedMines(ProbabilityLine pl, NextWitness nw) {
BigInteger result = 0;
foreach (Box b in nw.GetOldBoxes()) {
result = result + pl.GetMineBoxCount(b.GetUID());
}
return result;
}
// this is used to recursively place the missing Mines into the available boxes for the probability line
private List<ProbabilityLine> DistributeMissingMines(ProbabilityLine pl, NextWitness nw, int missingMines, int index) {
//console.log("Distributing " + missingMines + " missing mines to box " + nw.newBoxes[index].uid);
this.recursions++;
if (this.recursions % 100000 == 0) {
information.Write("Solution Counter recursision = " + recursions);
}
List<ProbabilityLine> result = new List<ProbabilityLine>();
// if there is only one box left to put the missing mines we have reach the end of this branch of recursion
if (nw.GetNewBoxes().Count - index == 1) {
// if there are too many for this box then the probability can't be valid
if (nw.GetNewBoxes()[index].GetMaxMines() < missingMines) {
//console.log("Abandon (1)");
return result;
}
// if there are too few for this box then the probability can't be valid
if (nw.GetNewBoxes()[index].GetMinMines() > missingMines) {
//console.log("Abandon (2)");
return result;
}
// if there are too many for this game then the probability can't be valid
if (pl.GetMineCount() + missingMines > this.maxTotalMines) {
//console.log("Abandon (3)");
return result;
}
// otherwise place the mines in the probability line
pl.SetMineBoxCount(nw.GetNewBoxes()[index].GetUID(), new BigInteger(missingMines));
pl.SetMineCount(pl.GetMineCount() + missingMines);
result.Add(pl);
//console.log("Distribute missing mines line after " + pl.mineBoxCount);
return result;
}
// this is the recursion
int maxToPlace = Math.Min(nw.GetNewBoxes()[index].GetMaxMines(), missingMines);
for (int i = nw.GetNewBoxes()[index].GetMinMines(); i <= maxToPlace; i++) {
ProbabilityLine npl = ExtendProbabilityLine(pl, nw.GetNewBoxes()[index], i);
List<ProbabilityLine> r1 = DistributeMissingMines(npl, nw, missingMines - i, index + 1);
result.AddRange(r1);
}
return result;
}
// create a new probability line by taking the old and adding the mines to the new Box
private ProbabilityLine ExtendProbabilityLine(ProbabilityLine pl, Box newBox, int mines) {
//console.log("Extended probability line: Adding " + mines + " mines to box " + newBox.uid);
//console.log("Extended probability line before" + pl.mineBoxCount);
ProbabilityLine result = new ProbabilityLine(this.boxes.Count);
result.SetMineCount(pl.GetMineCount() + mines);
result.CopyMineBoxCount(pl);
result.SetMineBoxCount(newBox.GetUID(), new BigInteger(mines));
//console.log("Extended probability line after " + result.mineBoxCount);
return result;
}
// here we store the information we have found about this independent edge for later use
private void StoreProbabilities() {
List<ProbabilityLine> crunched = CrunchByMineCount(this.workingProbs);
//Console.WriteLine("Edge has " + crunched.Count + " probability lines after consolidation");
edgeStore.Add(new EdgeStore(crunched, this.mask)); // store the existing data
workingProbs.Clear(); // and get a new list for the next independent edge
this.workingProbs.Add(new ProbabilityLine(this.boxes.Count)); // add a new starter probability line
this.mask = new bool[boxes.Count];
}
// this combines newly generated probabilities with ones we have already stored from other independent sets of witnesses
private void CombineProbabilities() {
List<ProbabilityLine> result = new List<ProbabilityLine>();
if (this.workingProbs.Count == 0) {
information.Write("working probabilites list is empty!!");
return;
}
// see if we can find a common divisor
BigInteger hcd = workingProbs[0].GetSolutionCount();
foreach (ProbabilityLine pl in workingProbs) {
hcd = BigInteger.GreatestCommonDivisor(hcd, pl.GetSolutionCount());
}
foreach (ProbabilityLine pl in heldProbs) {
hcd = BigInteger.GreatestCommonDivisor(hcd, pl.GetSolutionCount());
}
information.Write("Greatest Common Divisor is " + hcd);
solutionCountMultiplier = solutionCountMultiplier * hcd;
int mineCountMin = workingProbs[0].GetMineCount() + heldProbs[0].GetMineCount();
// shrink the window
edgeMinesMinLeft = edgeMinesMinLeft - workingProbs[0].GetMineCount();
edgeMinesMaxLeft = edgeMinesMaxLeft - workingProbs[workingProbs.Count - 1].GetMineCount();
foreach (ProbabilityLine pl in workingProbs) {
BigInteger plSolCount = pl.GetSolutionCount() / hcd;
foreach (ProbabilityLine epl in heldProbs) {
// if the mine count can never reach the lower cuttoff then ignore it
if (pl.GetMineCount() + epl.GetMineCount() + edgeMinesMaxLeft < this.mineCountLowerCutoff) {
continue;
}
// if the mine count will always be pushed beyonf the upper cuttoff then ignore it
if (pl.GetMineCount() + epl.GetMineCount() + edgeMinesMinLeft > this.mineCountUpperCutoff) {
continue;
}
ProbabilityLine newpl = new ProbabilityLine(this.boxes.Count);
newpl.SetMineCount(pl.GetMineCount() + epl.GetMineCount());
BigInteger eplSolCount = epl.GetSolutionCount() / hcd;
newpl.SetSolutionCount(pl.GetSolutionCount() * eplSolCount);
for (int k = 0; k < this.boxes.Count; k++) {
BigInteger w1 = pl.GetMineBoxCount(k) * eplSolCount;
BigInteger w2 = epl.GetMineBoxCount(k) * plSolCount;
newpl.SetMineBoxCount(k, w1 + w2);
}
result.Add(newpl);
}
}
//Console.WriteLine("Solution multiplier is " + solutionCountMultiplier);
this.heldProbs.Clear();
// if result is empty this is an impossible position
if (result.Count == 0) {
Console.WriteLine("Impossible position encountered");
return;
}
if (result.Count == 1) {
heldProbs.AddRange(result);
return;
}
// sort into mine order
result.Sort();
// and combine them into a single probability line for each mine count
int mc = result[0].GetMineCount();
ProbabilityLine npl = new ProbabilityLine(this.boxes.Count);
npl.SetMineCount(mc);
int startMC = mc;
foreach (ProbabilityLine pl in result) {
if (pl.GetMineCount() != mc) {
this.heldProbs.Add(npl);
mc = pl.GetMineCount();
npl = new ProbabilityLine(this.boxes.Count);
npl.SetMineCount(mc);
}
npl.SetSolutionCount(npl.GetSolutionCount() + pl.GetSolutionCount());
for (int j = 0; j < this.boxes.Count; j++) {
npl.SetMineBoxCount(j, npl.GetMineBoxCount(j) + pl.GetMineBoxCount(j));
}
}
this.heldProbs.Add(npl);
}
private List<ProbabilityLine> CrunchByMineCount(List<ProbabilityLine> target) {
//if (target.Count == 0) {
// return target;
//}
// sort the solutions by number of mines
target.Sort();
List<ProbabilityLine> result = new List<ProbabilityLine>();
int mc = target[0].GetMineCount();
ProbabilityLine npl = new ProbabilityLine(this.boxes.Count);
npl.SetMineCount(mc);
foreach (ProbabilityLine pl in target) {
if (pl.GetMineCount() != mc) {
result.Add(npl);
mc = pl.GetMineCount();
npl = new ProbabilityLine(this.boxes.Count);
npl.SetMineCount(mc);
}
MergeLineProbabilities(npl, pl);
}
//if (npl.GetMineCount() >= minTotalMines) {
result.Add(npl);
//}
//information.Write("Probability line has " + npl.GetMineCount() + " mines");
return result;
}
// calculate how many ways this solution can be generated and roll them into one
private void MergeLineProbabilities(ProbabilityLine npl, ProbabilityLine pl) {
BigInteger solutions = 1;
for (int i = 0; i < this.boxes.Count; i++) {
solutions = solutions * (BigInteger)SMALL_COMBINATIONS[this.boxes[i].GetTiles().Count][(int)pl.GetMineBoxCount(i)];
}
npl.SetSolutionCount(npl.GetSolutionCount() + solutions);
for (int i = 0; i < this.boxes.Count; i++) {
if (this.mask[i]) { // if this box has been involved in this solution - if we don't do this the hash gets corrupted by boxes = 0 mines because they weren't part of this edge
npl.SetMineBoxCount(i, npl.GetMineBoxCount(i) + pl.GetMineBoxCount(i) * solutions);
}
}
}
// return any witness which hasn't been processed
private NextWitness FindFirstWitness() {
BoxWitness excluded = null;
foreach (BoxWitness boxWit in this.boxWitnesses) {
if (!boxWit.IsProcessed()) {
return new NextWitness(boxWit);
} else if (!boxWit.IsProcessed()) {
excluded = boxWit;
}
}
if (excluded != null) {
return new NextWitness(excluded);
}
return null;
}
// look for the next witness to process
private NextWitness FindNextWitness(NextWitness prevWitness) {
// flag the last set of details as processed
prevWitness.GetBoxWitness().SetProcessed(true);
foreach (Box newBox in prevWitness.GetNewBoxes()) {
newBox.SetProcessed(true);
}
int bestTodo = 99999;
BoxWitness bestWitness = null;
// and find a witness which is on the boundary of what has already been processed
foreach (Box b in this.boxes) {
if (b.IsProcessed()) {
foreach (BoxWitness w in b.GetBoxWitnesses()) {
if (!w.IsProcessed()) {
int todo = 0;
foreach (Box b1 in w.GetBoxes()) {
if (!b1.IsProcessed()) {
todo++;
}
}
if (todo == 0) { // prioritise the witnesses which have the least boxes left to process
return new NextWitness(w);
} else if (todo < bestTodo) {
bestTodo = todo;
bestWitness = w;
}
}
}
}
}
if (bestWitness != null) {
return new NextWitness(bestWitness);
} else {
information.Write("Ending independent edge");
}
//independentGroups++;
// since we have calculated all the mines in an independent set of witnesses we can crunch them down and store them for later
// store the details for this edge
StoreProbabilities();
// get an unprocessed witness
NextWitness nw = FindFirstWitness();
if (nw != null) {
information.Write("Starting a new independent edge");
}
// If there is nothing else to process then either do the local clears or calculate the probabilities
if (nw == null) {
edgeStore.Sort(EdgeStore.SortByLineCount);
AnalyseAllEdges();
long start = DateTime.Now.Ticks;
foreach (EdgeStore edgeDetails in edgeStore) {
workingProbs = edgeDetails.data;
CombineProbabilities();
}
information.Write("Combined all edges in " + (DateTime.Now.Ticks - start) + " ticks");
}
// return the next witness to process
return nw;
}
// take a look at what edges we have and show some information - trying to get some ideas on how we can get faster good guesses
private void AnalyseAllEdges() {
//Console.WriteLine("Number of tiles off the edge " + tilesOffEdge);
//Console.WriteLine("Number of mines to find " + minesLeft);
this.edgeMinesMin = 0;
this.edgeMinesMax = 0;
foreach (EdgeStore edge in edgeStore) {
int edgeMinMines = edge.data[0].GetMineCount();
int edgeMaxMines = edge.data[edge.data.Count - 1].GetMineCount();
edgeMinesMin = edgeMinesMin + edgeMinMines;
edgeMinesMax = edgeMinesMax + edgeMaxMines;
}
information.Write("Min mines on all edges " + edgeMinesMin + ", max " + edgeMinesMax);
this.edgeMinesMaxLeft = this.edgeMinesMax; // these values are used in the merge logic to reduce the number of lines need to keep
this.edgeMinesMinLeft = this.edgeMinesMin;
this.mineCountLowerCutoff = this.edgeMinesMin;
this.mineCountUpperCutoff = Math.Min(this.edgeMinesMax, this.minesLeft); // can't have more mines than are left
// comment this out when doing large board analysis
return;
// the code below reduces the range of mine count values to just be the 'significant' range
List<ProbabilityLine> store = new List<ProbabilityLine>();
List<ProbabilityLine> store1 = new List<ProbabilityLine>();
ProbabilityLine init = new ProbabilityLine(0);
init.SetSolutionCount(1);
store.Add(init);
// combine all the edges to determine the relative weights of the mine count
foreach (EdgeStore edgeDetails in edgeStore) {
foreach (ProbabilityLine pl in edgeDetails.data) {
BigInteger plSolCount = pl.GetSolutionCount();
foreach (ProbabilityLine epl in store) {
if (pl.GetMineCount() + epl.GetMineCount() <= this.maxTotalMines) {
ProbabilityLine newpl = new ProbabilityLine(0);
newpl.SetMineCount(pl.GetMineCount() + epl.GetMineCount());
BigInteger eplSolCount = epl.GetSolutionCount();
newpl.SetSolutionCount(pl.GetSolutionCount() * eplSolCount);
store1.Add(newpl);
}
}
}
store.Clear();
// sort into mine order
store1.Sort();
int mc = store1[0].GetMineCount();
ProbabilityLine npl = new ProbabilityLine(0);
npl.SetMineCount(mc);
foreach (ProbabilityLine pl in store1) {
if (pl.GetMineCount() != mc) {
store.Add(npl);
mc = pl.GetMineCount();
npl = new ProbabilityLine(0);
npl.SetMineCount(mc);
}
npl.SetSolutionCount(npl.GetSolutionCount() + pl.GetSolutionCount());
}
store.Add(npl);
store1.Clear();
}
BigInteger total = 0;
int mineValues = 0;
foreach (ProbabilityLine pl in store) {
if (pl.GetMineCount() >= this.minTotalMines) { // if the mine count for this solution is less than the minimum it can't be valid
BigInteger mult = SolverMain.Calculate(this.minesLeft - pl.GetMineCount(), this.tilesOffEdge); //# of ways the rest of the board can be formed
total = total + mult * pl.GetSolutionCount();
mineValues++;
}
}
//this.mineCountLowerCutoff = this.edgeMinesMin;
//this.mineCountUpperCutoff = Math.Min(this.edgeMinesMax, this.minesLeft); // can't have more mines than are left
BigInteger soFar = 0;
foreach (ProbabilityLine pl in store) {
if (pl.GetMineCount() >= this.minTotalMines) { // if the mine count for this solution is less than the minimum it can't be valid
BigInteger mult = SolverMain.Calculate(this.minesLeft - pl.GetMineCount(), this.tilesOffEdge); //# of ways the rest of the board can be formed
soFar = soFar + mult * pl.GetSolutionCount();
double perc = Combination.DivideBigIntegerToDouble(soFar, total, 6) * 100;
//Console.WriteLine("Mine count " + pl.GetMineCount() + " has solution count " + pl.GetSolutionCount() + " multiplier " + mult + " running % " + perc);
//Console.WriteLine("Mine count " + pl.GetMineCount() + " has solution count " + pl.GetSolutionCount() + " has running % " + perc);
if (mineValues > 30 && perc < 2.5) {
this.mineCountLowerCutoff = pl.GetMineCount();
}
if (mineValues > 30 && perc > 97.5) {
this.mineCountUpperCutoff = pl.GetMineCount();
break;
}
}
}
information.Write("Significant range " + this.mineCountLowerCutoff + " - " + this.mineCountUpperCutoff);
//this.edgeMinesMaxLeft = this.edgeMinesMax;
//this.edgeMinesMinLeft = this.edgeMinesMin;
return;
// below here are experimental ideas on getting a good guess on very large boards
int midRangeAllMines = (this.mineCountLowerCutoff + this.mineCountUpperCutoff) / 2;
BigInteger[] tally = new BigInteger[boxes.Count];
double[] probability = new double[boxes.Count];
foreach (EdgeStore edgeDetails in edgeStore) {
int sizeRangeEdgeMines = (edgeDetails.data[edgeDetails.data.Count - 1].GetMineCount() - edgeDetails.data[0].GetMineCount()) / 2;
int start = (this.mineCountLowerCutoff - edgeDetails.data[0].GetMineCount() + this.mineCountUpperCutoff - edgeDetails.data[edgeDetails.data.Count - 1].GetMineCount()) / 2;
//int start = midRangeAllMines - sizeRangeEdgeMines;
//BigInteger mult = Combination.Calculate(this.minesLeft - start, this.tilesOffEdge);
BigInteger totalTally = 0;
foreach (ProbabilityLine pl in edgeDetails.data) {
BigInteger mult = Combination.Calculate(this.minesLeft - start - pl.GetMineCount(), this.tilesOffEdge);
totalTally += mult * pl.GetSolutionCount();
for (int i = 0; i < boxes.Count; i++) {
if (edgeDetails.mask[i]) {
BigInteger work = pl.GetMineBoxCount(i) * mult;
tally[i] += work;
}
}
//mult = mult * (this.tilesOffEdge - start) / (start + 1);
//start++;
}
for (int i = 0; i < boxes.Count; i++) {
if (edgeDetails.mask[i]) {
probability[i] = Combination.DivideBigIntegerToDouble(tally[i], totalTally, 6) / boxes[i].GetTiles().Count;
}
}
int minIndex = -1;
for (int i = 0; i < boxes.Count; i++) {
if (edgeDetails.mask[i]) {
if (minIndex == -1 || probability[i] < probability[minIndex]) {
minIndex = i;
}
}
}
if (minIndex != -1) {
information.Write("Best guess is " + boxes[minIndex].GetTiles()[0].AsText() + " with " + (1 - probability[minIndex]));
} else {
information.Write("No Guess found");
}
}
}
// here we expand the localised solution to one across the whole board and
// sum them together to create a definitive probability for each box
private void CalculateBoxProbabilities() {
//long start = DateTime.Now.Ticks;
if (truncatedProbs) {
Console.WriteLine("probability line combining was truncated");
}
information.Write("Solution count multiplier is " + this.solutionCountMultiplier);
BigInteger[] tally = new BigInteger[this.boxes.Count];
// total game tally
BigInteger totalTally = 0;
// outside a box tally
BigInteger outsideTally = 0;
//console.log("There are " + this.heldProbs.length + " different mine counts on the edge");
bool[] emptyBox = new bool[boxes.Count];
for (int i = 0; i < emptyBox.Length; i++) {
emptyBox[i] = true;
}
int linesProcessed = 0;
// calculate how many mines
foreach (ProbabilityLine pl in this.heldProbs) {
//console.log("Mine count is " + pl.mineCount + " with solution count " + pl.solutionCount + " mineBoxCount = " + pl.mineBoxCount);
if (pl.GetMineCount() >= this.minTotalMines) { // if the mine count for this solution is less than the minimum it can't be valid
linesProcessed++;
//console.log("Mines left " + this.minesLeft + " mines on PL " + pl.mineCount + " squares left = " + this.squaresLeft);
BigInteger mult = SolverMain.Calculate(this.minesLeft - pl.GetMineCount(), this.tilesOffEdge); //# of ways the rest of the board can be formed
information.Write("Mines in solution " + pl.GetMineCount() + " solution count " + pl.GetSolutionCount() + " multiplier " + mult);
outsideTally = outsideTally + mult * new BigInteger(this.minesLeft - pl.GetMineCount()) * (pl.GetSolutionCount());
// this is all the possible ways the mines can be placed across the whole game
totalTally = totalTally + mult * (pl.GetSolutionCount());
for (int i = 0; i < emptyBox.Length; i++) {
if (pl.GetMineBoxCount(i) != 0) {
emptyBox[i] = false;
}
}
}
}
// determine how many clear squares there are
if (totalTally > 0) {
for (int i = 0; i < emptyBox.Length; i++) {
if (emptyBox[i]) {
clearCount = clearCount + boxes[i].GetTiles().Count;
}
}
}
this.finalSolutionCount = totalTally * solutionCountMultiplier;
information.Write("Game has " + this.finalSolutionCount + " candidate solutions");
}
public BigInteger GetSolutionCount() {
return this.finalSolutionCount;
}
public int getClearCount() {
return this.clearCount;
}
public int GetSolutionCountMagnitude() {
return (int)Math.Floor(BigInteger.Log10(this.finalSolutionCount));
}
public SolverInfo GetSolverInfo() {
return this.information;
}
}
}

View File

@ -0,0 +1,23 @@
using System;
using static MinesweeperControl.MinesweeperGame;
namespace MinesweeperSolver {
public class SolverAction : GameAction, IComparable<SolverAction> {
public readonly double safeProbability;
public readonly bool isDead;
//public readonly bool isExcluded;
public SolverAction(SolverTile tile, ActionType action, double safeprob) : base(tile.x, tile.y, action) {
this.safeProbability = safeprob;
this.isDead = tile.IsDead();
//this.isExcluded = tile.IsExcluded();
}
public int CompareTo(SolverAction other) {
return safeProbability.CompareTo(other.safeProbability);
}
}
}

View File

@ -0,0 +1,22 @@
using System.Collections.Generic;
namespace MinesweeperSolver {
class SolverActionHeader {
public readonly IList<SolverAction> solverActions;
public readonly IList<SolverAction> deadActions;
public SolverActionHeader() {
List<SolverAction> empty = new List<SolverAction>();
this.solverActions = empty.AsReadOnly();
this.deadActions = empty.AsReadOnly();
}
public SolverActionHeader(List<SolverAction> solverActions, List<SolverAction> deadActions) {
this.solverActions = solverActions.AsReadOnly();
this.deadActions = deadActions.AsReadOnly();
}
}
}

View File

@ -0,0 +1,377 @@
using MinesweeperControl;
using System;
using System.Collections.Generic;
using static MinesweeperControl.MinesweeperGame;
namespace MinesweeperSolver {
public class SolverInfo {
private static readonly bool EXCLUDE_ON = false;
// contains information about what surrounds a tile
public class AdjacentInfo {
public readonly int mines;
public readonly int hidden;
public readonly int excluded;
public AdjacentInfo(int mines, int hidden, int excluded) {
this.mines = mines;
this.hidden = hidden;
this.excluded = excluded;
}
}
private readonly int key = new Random().Next();
public readonly GameDescription description;
public readonly bool verbose;
private GameStatus gameStatus = GameStatus.NotStarted;
private readonly SolverTile[,] tiles;
private int tilesLeft;
private ProbabilityEngine probabilityEngine = null; // this is the probability engine from the last solver run
private BruteForceAnalysis bfa = null; // this is the brute force analysis from the last solver run
private readonly HashSet<SolverTile> livingWitnesses = new HashSet<SolverTile>(20); // this is a set of tiles which are clear and have hidden neighbours.
private readonly HashSet<SolverTile> knownMines; // this is a set of tiles which are known to be mines.
private readonly HashSet<SolverTile> pendingClears = new HashSet<SolverTile>(); // this is a set of tiles which are clear and haven't yet been clicked.
private readonly HashSet<SolverTile> deadTiles = new HashSet<SolverTile>(); // this is a set of tiles which are known to be dead.
//private readonly HashSet<SolverTile> excludedWitnesses = new HashSet<SolverTile>(); // this is a set of witnesses which no longer influence the solving of the game.
//private readonly HashSet<SolverTile> excludedTiles = new HashSet<SolverTile>(); // this is a set of tiles which no longer influence the solving of the game.
//private int excludedMinesCount = 0; // number of mines which are with the exluded tiles area
private readonly List<SolverTile> newClears = new List<SolverTile>();
public SolverInfo(GameDescription description) : this(description, false) {
}
public SolverInfo(GameDescription description, bool verbose) {
this.description = description;
this.verbose = verbose;
tiles = new SolverTile[description.width, description.height];
// populate the grid with solver tiles showing what we know so far ... which is nothing
for (int x = 0; x < description.width; x++) {
for (int y = 0; y < description.height; y++) {
tiles[x, y] = new SolverTile(key, x, y);
}
}
tilesLeft = description.height * description.width;
knownMines = new HashSet<SolverTile>(description.mines);
}
public void AddInformation(GameResult information) {
//long start = DateTime.Now.Ticks;
gameStatus = information.status;
this.probabilityEngine = null; // previous probability engine invalidated
newClears.Clear();
// the game results tell us when a tile is cleared, flagged or unflagged
foreach (ActionResult ar in information.actionResults) {
if (ar.resultType == ResultType.Cleared) {
tilesLeft--;
tiles[ar.x, ar.y].SetValue(ar.value);
SolverTile tile = tiles[ar.x, ar.y];
newClears.Add(tile);
if (tile.IsDead()) {
deadTiles.Remove(tile);
}
//if (tile.IsExcluded()) {
// excludedTiles.Remove(tile);
//}
//pendingClears.Remove(tile);
} else if (ar.resultType == ResultType.Flagged) {
tiles[ar.x, ar.y].SetFlagged(true);
} else if (ar.resultType == ResultType.Hidden) {
tiles[ar.x, ar.y].SetFlagged(false);
} else if (ar.resultType == ResultType.Exploded) {
SolverTile tile = tiles[ar.x, ar.y];
tile.SetFlagged(false);
MineFound(tile);
this.bfa = null; // can't walk the bfa tree if we've trodden on a mine
}
}
// find and mark tiles as exhausted
//int removed = 0;
//if (newClears.Count > 0) {
foreach (SolverTile tile in livingWitnesses) {
if (AdjacentTileInfo(tile).hidden == 0) {
tile.SetExhausted();
}
}
// remove all exhausted tiles from the list of witnesses
livingWitnesses.RemoveWhere(x => x.IsExhausted());
//}
// add new witnesses which aren't exhausted
foreach (SolverTile newTile in newClears) {
AdjacentInfo adjInfo = AdjacentTileInfo(newTile);
if (adjInfo.hidden != 0) {
if (adjInfo.excluded != adjInfo.hidden) {
livingWitnesses.Add(newTile);
} else {
//excludedWitnesses.Add(newTile);
}
}
}
//Write("There are " + livingWitnesses.Count + " living witnesses (" + removed + " deleted)");
//Write("Adding Information to Solver took " + (DateTime.Now.Ticks - start) + " ticks");
}
public int GetMinesLeft() {
return description.mines - knownMines.Count; // mines left to find = mines in game - mines found
}
public int GetTilesLeft() {
return this.tilesLeft;
}
// sets the tile as a mine, stores it for future use and returns true if it is currently flagged
public bool MineFound(SolverTile tile) {
// if this is already known to be mine then nothing to do
if (tile.IsMine()) {
return tile.IsFlagged();
}
tilesLeft--;
tile.SetAsMine(key);
knownMines.Add(tile);
if (tile.IsDead()) {
deadTiles.Remove(tile); // remove the tile if it was on the dead list
}
//if (tile.IsExcluded()) {
// excludedTiles.Remove(tile); // remove the tile if it was excluded
// excludedMinesCount--;
//}
return tile.IsFlagged();
}
public void SetTileToDead(SolverTile tile) {
if (tile.IsMine()) {
Write("ERROR: Trying to set a mine tile to dead " + tile.AsText());
return;
}
tile.SetDead(key); // mark it
deadTiles.Add(tile); // and add to the set
}
public HashSet<SolverTile> GetDeadTiles() {
return deadTiles;
}
/*
public void ExcludeMines(int n) {
if (!EXCLUDE_ON) {
return;
}
this.excludedMinesCount = this.excludedMinesCount + n;
}
public int GetExcludedMineCount() {
return this.excludedMinesCount;
}
public void ExcludeTile(SolverTile tile) {
if (!EXCLUDE_ON) {
return;
}
tile.SetExcluded();
excludedTiles.Add(tile);
}
public HashSet<SolverTile> GetExcludedTiles() {
return this.excludedTiles;
}
/// <summary>
/// Move a tile from the list of witnesses to the list of excluded witnesses
/// </summary>
public void ExcludeWitness(SolverTile tile) {
if (!EXCLUDE_ON) {
return;
}
tile.SetExcluded();
if (livingWitnesses.Remove(tile)) {
excludedWitnesses.Add(tile);
}
}
public HashSet<SolverTile> GetExcludedWitnesses() {
return this.excludedWitnesses;
}
*/
public void SetBruteForceAnalysis(BruteForceAnalysis bfa) {
this.bfa = bfa;
}
public BruteForceAnalysis GetBruteForceAnalysis() {
return this.bfa;
}
public void SetProbabilityEngine(ProbabilityEngine pe) {
this.probabilityEngine = pe;
}
// returns the probability the tile is safe if known. Or -1 otherwise.
public double GetProbability(int x, int y) {
// if we are out of bounds then nothing to say
if (x < 0 || x >= description.width || y < 0 || y >= description.height) {
return -1;
}
SolverTile tile = tiles[x, y];
// an unflagged mine
if (tile.IsMine() && !tile.IsFlagged()) {
return 0;
}
// otherwise if revealed then nothing to say
if (!tile.IsHidden()) {
return -1;
}
//if (tile.IsExcluded()) {
// return -1;
//}
if (probabilityEngine != null) {
return probabilityEngine.GetProbability(tile);
} else if (pendingClears.Contains(tile)) {
return 1;
} else {
return -1;
}
}
//public bool IsTileDead(SolverTile tile) {
// return deadTiles.Contains(tile);
//}
// allow the gui to see if a tile is dead
//public bool IsTileDead(int x, int y) {
// return IsTileDead(tiles[x, y]);
//}
// Adds the tile to a list of known clears for future use
public void ClearFound(SolverTile tile) {
pendingClears.Add(tile);
}
// returns all the indices adjacent to this index
public List<SolverTile> GetAdjacentTiles(SolverTile tile) {
// have we already calculated the adjacent tiles
List<SolverTile> adjTiles = tile.GetAdjacentTiles();
if (adjTiles != null) {
return adjTiles;
}
adjTiles = new List<SolverTile>();
int first_row = Math.Max(0, tile.y - 1);
int last_row = Math.Min(description.height - 1, tile.y + 1);
int first_col = Math.Max(0, tile.x - 1);
int last_col = Math.Min(description.width - 1, tile.x + 1);
for (int r = first_row; r <= last_row; r++) {
for (int c = first_col; c <= last_col; c++) {
if (r != tile.y || c != tile.x) {
adjTiles.Add(tiles[c, r]);
}
}
}
// remember them for next time
tile.SetAdjacentTiles(adjTiles);
return adjTiles;
}
public AdjacentInfo AdjacentTileInfo(SolverTile tile) {
int hidden = 0;
int mines = 0;
int excluded = 0;
foreach (SolverTile adjTile in GetAdjacentTiles(tile)) {
if (adjTile.IsMine()) {
mines++;
} else if (adjTile.IsHidden() ) {
hidden++;
//if (adjTile.IsExcluded()) {
// excluded++;
//}
}
}
return new AdjacentInfo(mines, hidden, excluded);
}
public GameStatus GetGameStatus() {
return gameStatus;
}
public SolverTile GetTile(int x, int y) {
return tiles[x, y];
}
public HashSet<SolverTile> GetWitnesses() {
return livingWitnesses;
}
public HashSet<SolverTile> GetKnownMines() {
return knownMines;
}
public void Write(string text) {
if (verbose) {
Console.WriteLine(text);
}
}
}
}

View File

@ -0,0 +1,614 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using System.Text;
using static MinesweeperControl.MinesweeperGame;
using static MinesweeperSolver.SolverInfo;
namespace MinesweeperSolver {
class SolverMain {
public static readonly BigInteger PARALLEL_MINIMUM = new BigInteger(10000);
public static readonly BigInteger MAX_BRUTE_FORCE_ITERATIONS = new BigInteger(10000000);
public const int BRUTE_FORCE_ANALYSIS_TREE_DEPTH = 20;
public const int MAX_BFDA_SOLUTIONS = 400;
public const int BRUTE_FORCE_ANALYSIS_MAX_NODES = 150000;
public const bool PRUNE_BF_ANALYSIS = true;
private static readonly Binomial binomial = new Binomial(500000, 500);
public static SolverActionHeader FindActions(SolverInfo information) {
long time1 = DateTime.Now.Ticks;
List<SolverAction> actions ;
if (information.GetGameStatus() == GameStatus.Lost) {
information.Write("Game has been lost already - no valid moves");
return new SolverActionHeader();
} else if (information.GetGameStatus() == GameStatus.Won) {
information.Write("Game has been won already - no valid moves");
return new SolverActionHeader();
} else if (information.GetGameStatus() == GameStatus.NotStarted) {
information.Write("Game has not yet started - currently unable to provide help");
return new SolverActionHeader();
}
// are we walking down a brute force deep analysis tree?
BruteForceAnalysis lastBfa = information.GetBruteForceAnalysis();
if (lastBfa != null) { // yes
SolverTile expectedMove = lastBfa.GetExpectedMove();
if (expectedMove != null && expectedMove.IsHidden()) { // the expected move wasn't played !
information.Write("The expected Brute Force Analysis move " + expectedMove.AsText() + " wasn't played");
information.SetBruteForceAnalysis(null);
} else {
SolverAction move = lastBfa.GetNextMove();
if (move != null) {
information.Write("Next Brute Force Deep Analysis move is " + move.AsText());
actions = new List<SolverAction>(1);
actions.Add(move);
return BuildActionHeader(information, actions);
}
}
}
actions = FindTrivialActions(information);
long time2 = DateTime.Now.Ticks;
information.Write("Finding Trivial Actions took " + (time2 - time1) + " ticks");
// if we have some actions from the trivial search use them
if (actions.Count > 0) {
return BuildActionHeader(information, actions);
}
if (information.GetTilesLeft() == information.GetDeadTiles().Count) {
information.Write("All tiles remaining are dead");
// when all the tiles are dead return the first one (they are all equally good or bad)
foreach (SolverTile guess in information.GetDeadTiles()) {
actions.Add(new SolverAction(guess, ActionType.Clear, 0.5)); // not all 0.5 safe though !!
return BuildActionHeader(information, actions);
}
}
// get all the witnessed tiles
List<SolverTile> witnesses = new List<SolverTile>(information.GetWitnesses());
HashSet<SolverTile> witnessedSet = new HashSet<SolverTile>();
foreach (SolverTile witness in witnesses) {
foreach(SolverTile adjTile in information.GetAdjacentTiles(witness)) {
if (adjTile.IsHidden()) {
witnessedSet.Add(adjTile);
}
}
}
List<SolverTile> witnessed = new List<SolverTile>(witnessedSet);
int livingTilesLeft = information.GetTilesLeft();
int livingMinesLeft = information.GetMinesLeft();
int offEdgeTilesLeft = information.GetTilesLeft() - witnessed.Count;
//information.Write("Excluded tiles " + information.GetExcludedTiles().Count + " out of " + information.GetTilesLeft());
//information.Write("Excluded witnesses " + information.GetExcludedWitnesses().Count);
//information.Write("Excluded mines " + information.GetExcludedMineCount() + " out of " + information.GetMinesLeft());
// if there are no living mines but some living tiles then the living tiles can be cleared
if (livingMinesLeft == 0 && livingTilesLeft > 0) {
information.Write("There are no living mines left - all living tiles must be clearable");
for (int x = 0; x < information.description.width; x++) {
for (int y = 0; y < information.description.height; y++) {
SolverTile tile = information.GetTile(x, y);
if (tile.IsHidden() && !tile.IsMine()) {
actions.Add(new SolverAction(tile, ActionType.Clear, 1));
}
}
}
return BuildActionHeader(information, actions);
}
SolutionCounter solutionCounter = new SolutionCounter(information, witnesses, witnessed, livingTilesLeft, livingMinesLeft);
solutionCounter.Process();
information.Write("Solution counter says " + solutionCounter.GetSolutionCount() + " solutions and " + solutionCounter.getClearCount() + " clears");
//ProbabilityEngine pe = new ProbabilityEngine(information, witnesses, witnessed, information.GetTilesLeft(), information.GetMinesLeft());
ProbabilityEngine pe = new ProbabilityEngine(information, witnesses, witnessed, livingTilesLeft, livingMinesLeft);
pe.Process();
long time3 = DateTime.Now.Ticks;
information.Write("Probability Engine took " + (time3 - time2) + " ticks");
// have we found any local clears which we can use
List<SolverTile> localClears = pe.GetLocalClears();
if (localClears.Count > 0) {
foreach (SolverTile tile in localClears) { // place each local clear into an action
actions.Add(new SolverAction(tile, ActionType.Clear, 1));
}
information.Write("The probability engine has found " + localClears.Count + " safe Local Clears");
// add any mines to it
List<SolverTile> minesFound = pe.GetMinesFound();
foreach (SolverTile tile in minesFound) { // place each mine found into an action
information.MineFound(tile);
actions.Add(new SolverAction(tile, ActionType.Flag, 0));
}
information.Write("The probability engine has found " + minesFound.Count + " mines");
return BuildActionHeader(information, actions);
}
if (pe.GetBestEdgeProbability() == 1) {
actions = pe.GetBestCandidates(1);
information.Write("The probability engine has found " + actions.Count + " safe Clears");
// add any mines to it
List<SolverTile> minesFound = pe.GetMinesFound();
foreach (SolverTile tile in minesFound) { // place each mine found into an action
information.MineFound(tile);
actions.Add(new SolverAction(tile, ActionType.Flag, 0));
}
information.Write("The probability engine has found " + minesFound.Count + " mines");
return BuildActionHeader(information, actions);
}
// dead edge (all tiles on the edge are dead and there is only one mine count value)
if (pe.GetDeadEdge().Count != 0) {
SolverTile tile = pe.GetDeadEdge()[0];
information.Write("Probability engine has found a dead area, guessing at " + tile.AsText());
double probability = Combination.DivideBigIntegerToDouble(BigInteger.One, pe.GetDeadEdgeSolutionCount(), 6);
actions.Add(new SolverAction(tile, ActionType.Clear, probability));
return BuildActionHeader(information, actions);
}
// Isolated edges found (all adjacent tiles are also on the edge and there is only one mine count value)
if (pe.GetOutcome() == ProbabilityEngine.Outcome.ISOLATED_EDGE) {
information.Write("Probability engine has found an isolated area");
Cruncher cruncher = pe.getIsolatedEdgeCruncher();
// determine all possible solutions
cruncher.Crunch();
// determine best way to solver them
BruteForceAnalysis bfa = cruncher.GetBruteForceAnalysis();
bfa.process();
// if after trying to process the data we can't complete then abandon it
if (!bfa.IsComplete()) {
information.Write("Abandoned the Brute Force Analysis after " + bfa.GetNodeCount() + " steps");
bfa = null;
} else { // otherwise try and get the best long term move
information.Write("Built probability tree from " + bfa.GetSolutionCount() + " solutions in " + bfa.GetNodeCount() + " steps");
SolverAction move = bfa.GetNextMove();
if (move != null) {
information.SetBruteForceAnalysis(bfa); // save the details so we can walk the tree
information.Write("Brute Force Analysis: " + move.AsText());
actions.Add(move);
return BuildActionHeader(information, actions);
} else if (bfa.GetAllDead()) {
SolverTile tile = cruncher.getTiles()[0];
information.Write("Brute Force Analysis has decided all tiles are dead on the Isolated Edge, guessing at " + tile.AsText());
double probability = Combination.DivideBigIntegerToDouble(BigInteger.One, bfa.GetSolutionCount(), 6);
actions.Add(new SolverAction(tile, ActionType.Clear, probability));
return BuildActionHeader(information, actions);
} else {
information.Write("Brute Force Analysis: no move found!");
}
}
}
// after this point we know the probability engine didn't return any certain clears. But there are still some special cases when everything off edge is either clear or a mine
// If there are tiles off the edge and they are definitely safe then clear them all, or mines then flag them
if (offEdgeTilesLeft > 0 && (pe.GetOffEdgeProbability() == 1 || pe.GetOffEdgeProbability() == 0)) {
information.Write("Looking for the certain moves off the edge found by the probability engine");
bool clear;
if (pe.GetOffEdgeProbability() == 1) {
information.Write("All off edge tiles are clear");
clear = true;
} else {
information.Write("All off edge tiles are mines");
clear = false;
}
for (int x=0; x < information.description.width; x++) {
for (int y = 0; y < information.description.height; y++) {
SolverTile tile = information.GetTile(x, y);
if (tile.IsHidden() && !witnessedSet.Contains(tile)) {
if (clear) {
information.Write(tile.AsText() + " is clear");
actions.Add(new SolverAction(tile, ActionType.Clear, 1));
} else {
information.Write(tile.AsText() + " is mine");
information.MineFound(tile);
actions.Add(new SolverAction(tile, ActionType.Flag, 0));
}
}
}
}
if (actions.Count > 0) {
// add any mines to it
List<SolverTile> minesFound = pe.GetMinesFound();
foreach (SolverTile tile in minesFound) { // place each mine found into an action
information.MineFound(tile);
actions.Add(new SolverAction(tile, ActionType.Flag, 0));
}
information.Write("The probability engine has found " + minesFound.Count + " mines");
return BuildActionHeader(information, actions);
} else {
Console.WriteLine("No Actions found!");
}
}
// these are guesses
List<SolverAction> guesses = pe.GetBestCandidates(1);
// we know the Probability Engine completed so hold onto the information for the gui
information.SetProbabilityEngine(pe);
// if there aren't many possible solutions then do a brute force search
if (pe.GetSolutionCount() <= MAX_BFDA_SOLUTIONS) {
//if (minesFound.Count > 0) {
// information.Write("Not doing a brute force analysis because we found some mines using the probability engine");
// return BuildActionHeader(information, actions);
//}
// find a set of independent witnesses we can use as the base of the iteration
pe.GenerateIndependentWitnesses();
BigInteger expectedIterations = pe.GetIndependentIterations() * SolverMain.Calculate(livingMinesLeft - pe.GetIndependentMines(), livingTilesLeft - pe.GetIndependentTiles());
information.Write("Expected Brute Force iterations " + expectedIterations);
// do the brute force if there are not too many iterations
if (expectedIterations < MAX_BRUTE_FORCE_ITERATIONS) {
List<SolverTile> allCoveredTiles = new List<SolverTile>();
for (int x = 0; x < information.description.width; x++) {
for (int y = 0; y < information.description.height; y++) {
SolverTile tile = information.GetTile(x, y);
if (tile.IsHidden() && !tile.IsMine()) {
allCoveredTiles.Add(tile);
}
}
}
WitnessWebIterator[] iterators = BuildParallelIterators(information, pe, allCoveredTiles, expectedIterations);
BruteForceAnalysis bfa = Cruncher.PerformBruteForce(information, iterators, pe.GetDependentWitnesses());
bfa.process();
// if after trying to process the data we can't complete then abandon it
if (!bfa.IsComplete()) {
information.Write("Abandoned the Brute Force Analysis after " + bfa.GetNodeCount() + " steps");
bfa = null;
} else { // otherwise try and get the best long term move
information.Write("Built probability tree from " + bfa.GetSolutionCount() + " solutions in " + bfa.GetNodeCount() + " steps");
SolverAction move = bfa.GetNextMove();
if (move != null) {
information.SetBruteForceAnalysis(bfa); // save the details so we can walk the tree
information.Write("Brute Force Analysis: " + move.AsText());
actions.Add(move);
return BuildActionHeader(information, actions);
} else {
information.Write("Brute Force Analysis: no move found!");
}
}
} else {
information.Write("Too many iterations, Brute Force not atempted");
}
}
if (guesses.Count == 0) { // find an off edge guess
if (offEdgeTilesLeft > 0) {
information.Write("getting an off edge guess");
SolverTile tile = OffEdgeGuess(information, witnessedSet);
SolverAction action = new SolverAction(tile, ActionType.Clear, pe.GetOffEdgeProbability());
information.Write(action.AsText());
actions.Add(action);
} else {
if (information.GetDeadTiles().Count > 0) {
information.Write("Finding a dead tile to guess");
SolverTile tile = null;
foreach (SolverTile deadTile in information.GetDeadTiles()) { // get the first dead tile
tile = deadTile;
break;
}
SolverAction action = new SolverAction(tile, ActionType.Clear, 0.5); // probability may not be 0.5
actions.Add(action);
}
}
} else if (guesses.Count > 1) { // if we have more than 1 guess then do some tie break logic
information.Write("Doing a tie break for " + guesses.Count + " actions");
actions = DoTieBreak(guesses);
} else {
actions = guesses;
}
return BuildActionHeader(information, actions);
}
private static SolverActionHeader BuildActionHeader(SolverInfo information, List<SolverAction> actions) {
HashSet<SolverTile> dead = information.GetDeadTiles();
// add any dead tiles to the list of actions
List<SolverAction> deadList = new List<SolverAction>();
foreach (SolverTile dt in dead) {
deadList.Add(new SolverAction(dt, ActionType.Dead, 1));
}
return new SolverActionHeader(actions, deadList);
}
private static List<SolverAction> FindTrivialActions(SolverInfo information) {
List<SolverAction> actions = new List<SolverAction>();
// provide actions for known mines which aren't flagged
foreach (SolverTile tile in information.GetKnownMines()) {
if (!tile.IsFlagged()) {
actions.Add(new SolverAction(tile, ActionType.Flag, 0)); // request it is flagged
}
}
actions.AddRange(ScanForTrivialActions(information, information.GetWitnesses()));
//actions.AddRange(ScanForTrivialActions(information, information.GetExcludedWitnesses()));
information.Write("Found " + actions.Count + " trivial actions");
return actions;
}
private static List<SolverAction> ScanForTrivialActions(SolverInfo information, HashSet<SolverTile> set) {
List<SolverAction> actions = new List<SolverAction>(); ;
HashSet<SolverTile> alreadyProcessed = new HashSet<SolverTile>();
foreach (SolverTile tile in set) {
AdjacentInfo adjInfo = information.AdjacentTileInfo(tile);
if (tile.GetValue() == adjInfo.mines) { // if we have the correct number of mines then the remaining hidden tiles can be cleared
foreach (SolverTile adjTile in information.GetAdjacentTiles(tile)) {
if (!alreadyProcessed.Contains(adjTile) && adjTile.IsHidden() && !adjTile.IsMine()) {
alreadyProcessed.Add(adjTile); // avoid marking as cleared more than once
//Utility.Write(adjTile.AsText() + " can be cleared");
actions.Add(new SolverAction(adjTile, ActionType.Clear, 1));
}
}
}
if (tile.GetValue() == adjInfo.mines + adjInfo.hidden) { // If Hidden + Mines we already know about = tile value then the rest must be mines
foreach (SolverTile adjTile in information.GetAdjacentTiles(tile)) {
if (!alreadyProcessed.Contains(adjTile) && adjTile.IsHidden() && !adjTile.IsMine()) {
alreadyProcessed.Add(adjTile); // avoid marking as a mine more than once
//Utility.Write(adjTile.AsText() + " is a mine");
if (!information.MineFound(adjTile)) {
actions.Add(new SolverAction(adjTile, ActionType.Flag, 0)); // and request it is flagged
}
}
}
}
// 2 mines to find and only 3 tiles to put them
if (tile.GetValue() - adjInfo.mines == 2 && adjInfo.hidden == 3) {
foreach (SolverTile adjTile in information.GetAdjacentTiles(tile)) {
if (!adjTile.IsHidden() && !adjTile.IsMine() && !adjTile.IsExhausted()) {
AdjacentInfo tileAdjInfo = information.AdjacentTileInfo(adjTile);
if (adjTile.GetValue() - tileAdjInfo.mines == 1) { // an adjacent tile only needs to find one mine
int notAdjacentCount = 0;
SolverTile notAdjTile = null;
foreach (SolverTile adjTile2 in information.GetAdjacentTiles(tile)) {
if (adjTile2.IsHidden() && !adjTile2.IsAdjacent(adjTile)) {
notAdjacentCount++;
notAdjTile = adjTile2;
}
}
if (notAdjacentCount == 1 && !alreadyProcessed.Contains(notAdjTile)) { // we share all but one tile, so that one tile must contain the extra mine
alreadyProcessed.Add(notAdjTile); // avoid marking as a mine more than once
if (!information.MineFound(notAdjTile)) {
actions.Add(new SolverAction(notAdjTile, ActionType.Flag, 0)); // and request it is flagged
break; // restrict to finding one mine at a time (the action of marking the mine throws any further analysis out)
}
}
}
}
}
}
// Subtraction method
//continue; // skip this bit
foreach (SolverTile adjTile in information.GetAdjacentTiles(tile)) {
if (!adjTile.IsHidden() && !adjTile.IsMine() && !adjTile.IsExhausted()) {
AdjacentInfo tileAdjInfo = information.AdjacentTileInfo(adjTile);
if (adjTile.GetValue() - tileAdjInfo.mines == tile.GetValue() - adjInfo.mines) { // if the adjacent tile is revealed and shares the same number of mines to find
// If all the adjacent tiles adjacent tiles are also adjacent to the original tile
bool allAdjacent = true;
foreach (SolverTile adjTile2 in information.GetAdjacentTiles(adjTile)) {
if (adjTile2.IsHidden() && !adjTile2.IsAdjacent(tile)) {
allAdjacent = false;
break;
}
}
if (allAdjacent) {
//information.Write(tile.AsText() + " can be subtracted by " + adjTile.AsText());
foreach (SolverTile adjTile2 in information.GetAdjacentTiles(tile)) {
if (adjTile2.IsHidden() && !adjTile2.IsAdjacent(adjTile) && !alreadyProcessed.Contains(adjTile2)) {
alreadyProcessed.Add(adjTile2); // avoid marking as a mine more than once
actions.Add(new SolverAction(adjTile2, ActionType.Clear, 1));
}
}
}
}
}
}
}
return actions;
}
private static SolverTile OffEdgeGuess(SolverInfo information, HashSet<SolverTile> witnessedSet) {
// see if the corners are available
SolverTile bestGuess = information.GetTile(0, 0);
if (bestGuess.IsHidden() && !witnessedSet.Contains(bestGuess)) {
return bestGuess;
}
bestGuess = information.GetTile(0, information.description.height - 1);
if (bestGuess.IsHidden() && !witnessedSet.Contains(bestGuess)) {
return bestGuess;
}
bestGuess = information.GetTile(information.description.width - 1, 0);
if (bestGuess.IsHidden() && !witnessedSet.Contains(bestGuess)) {
return bestGuess;
}
bestGuess = information.GetTile(information.description.width - 1, information.description.height - 1);
if (bestGuess.IsHidden() && !witnessedSet.Contains(bestGuess)) {
return bestGuess;
}
bestGuess = null;
int bestGuessCount = 9;
for (int x = 0; x < information.description.width; x++) {
for (int y = 0; y < information.description.height; y++) {
SolverTile tile = information.GetTile(x, y);
if (tile.IsHidden() && !witnessedSet.Contains(tile)) {
AdjacentInfo adjInfo = information.AdjacentTileInfo(tile);
if (adjInfo.hidden < bestGuessCount) {
bestGuess = tile;
bestGuessCount = adjInfo.hidden;
}
}
}
}
return bestGuess;
}
// do tie break logic
private static List<SolverAction> DoTieBreak(List<SolverAction> actions) {
List<SolverAction> result = new List<SolverAction>();
// find and return a not dead tile
foreach (SolverAction sa in actions) {
if (!sa.isDead) {
result.Add(sa);
return result;
}
}
// otherwise return the first one
result.Add(actions[0]);
return result;
}
// break a witness web search into a number of non-overlapping iterators
private static WitnessWebIterator[] BuildParallelIterators(SolverInfo information, ProbabilityEngine pe, List<SolverTile> allCovered, BigInteger expectedIterations) {
information.Write("Building parallel iterators");
//information.Write("Non independent iterations = " + pe.GetNonIndependentIterations(mines));
int minesLeft = information.GetMinesLeft();
//the number of witnesses available
int totalWitnesses = pe.GetIndependentWitnesses().Count + pe.GetDependentWitnesses().Count;
// if there is only one cog then we can't lock it,so send back a single iterator
if (pe.GetIndependentWitnesses().Count == 1 && pe.GetIndependentMines() >= minesLeft || expectedIterations.CompareTo(PARALLEL_MINIMUM) < 0 || totalWitnesses == 0) {
information.Write("Only a single iterator will be used");
WitnessWebIterator[] one = new WitnessWebIterator[1];
one[0] = new WitnessWebIterator(information, pe.GetIndependentWitnesses(), pe.GetDependentWitnesses(), allCovered, minesLeft, information.GetTilesLeft(), -1);
return one;
}
int witMines = pe.GetIndependentWitnesses()[0].GetMinesToFind();
int squares = pe.GetIndependentWitnesses()[0].GetAdjacentTiles().Count;
BigInteger bigIterations = Calculate(witMines, squares);
int iter = (int) bigIterations;
information.Write("The first cog has " + iter + " iterations, so parallel processing is possible");
WitnessWebIterator[] result = new WitnessWebIterator[iter];
for (int i = 0; i < iter; i++) {
// create a iterator with a lock first got at position i
result[i] = new WitnessWebIterator(information, pe.GetIndependentWitnesses(), pe.GetDependentWitnesses(), allCovered, minesLeft, information.GetTilesLeft(), i);
}
return result;
}
public static BigInteger Calculate(int mines, int squares) {
return binomial.Generate(mines, squares);
}
public static void Initialise() {
}
}
}

View File

@ -0,0 +1,189 @@
using System;
using System.Collections.Generic;
using System.Text;
using static MinesweeperSolver.SolverInfo;
namespace MinesweeperSolver {
public class SolverTile {
public readonly int x;
public readonly int y;
private readonly long key; // the key identifies which SolverInfo created these tiles and locks down some methods.
// details about me
private bool isMine = false;
private bool isFlagged = false;
private bool isHidden = true;
private int value = 0;
private bool isDead = false; // a dead tile is one which provides no information if clicked. Should be avoided unless all tiles are dead.
private bool exhausted = false; // an exhausted tile is where it is cleared and all its neighbours are either cleared or a mine
//private bool newGrowth = true; // a tile is new growth the first time it is revealed. Once it has been analysed by the Probability Engine it stops being new growth.
//private bool excluded = false; // an excluded tile is one which no longer needs to be included in the propability engine since it no longer affects other tiles
private List<SolverTile> adjacentTiles = null;
public SolverTile(long key, int x, int y) {
this.key = key;
this.x = x;
this.y = y;
}
public bool IsMine() {
return isMine;
}
// this should only be called from the SolverInfo class
public void SetAsMine(long key) {
if (key != this.key) {
throw new Exception("Unauthorised access to the SetAsMine method");
}
this.isMine = true;
this.isHidden = false; // no longer hidden we know it is a mine
}
public bool IsFlagged() {
return this.isFlagged;
}
public void SetFlagged(bool flagged) {
isFlagged = flagged;
}
public bool IsHidden() {
return this.isHidden;
}
public void SetValue(int value) {
this.isHidden = false;
this.value = value;
}
public int GetValue() {
return this.value;
}
public bool IsExhausted() {
return this.exhausted;
}
public void SetExhausted() {
this.exhausted = true;
}
// this should only be called from from the SolverInfo class
public void SetDead(long key) {
if (key != this.key) {
throw new Exception("Unauthorised access to the SetDead method");
}
this.isDead = true;
}
//public bool IsNewGrowth() {
// return newGrowth;
//}
//public void SetExamined() {
// this.newGrowth = false;
//}
public bool IsDead() {
return isDead;
}
public List<SolverTile> GetAdjacentTiles() {
return this.adjacentTiles;
}
public void SetAdjacentTiles(List<SolverTile> adjTiles) {
this.adjacentTiles = adjTiles;
}
/*
public void SetExcluded() {
this.excluded = true;
}
public bool IsExcluded() {
return this.excluded;
}
*/
public bool IsEqual(SolverTile tile) {
if (this.x == tile.x && this.y == tile.y) {
return true;
} else {
return false;
}
}
// returns true if the tile provided is adjacent to this tile
public bool IsAdjacent(SolverTile tile) {
var dx = Math.Abs(this.x - tile.x);
if (dx > 1) {
return false;
}
var dy = Math.Abs(this.y - tile.y);
if (dy > 1) {
return false;
}
if (dx == 0 && dy == 0) {
return false;
} else {
return true;
}
/*
// adjacent and not equal
if (dx < 2 && dy < 2 && !(dx == 0 && dy == 0)) {
return true;
} else {
return false;
}
*/
}
//<summary> return the number of hidden tiles shared by these tiles
public int NumberOfSharedHiddenTiles(SolverTile tile) {
// if the locations are too far apart they can't share any of the same squares
if (Math.Abs(tile.x - this.x) > 2 || Math.Abs(tile.y - this.y) > 2) {
return 0;
}
int count = 0;
foreach (SolverTile tile1 in this.GetAdjacentTiles()) {
if (!tile1.IsHidden()) {
continue;
}
foreach (SolverTile tile2 in tile.GetAdjacentTiles()) {
if (!tile2.IsHidden()) {
continue;
}
if (tile1.IsEqual(tile2)) { // if they share a tile then return true
count ++;
}
}
}
// no shared tile found
return count;
}
public string AsText() {
return "(" + x + "," + y + ")";
}
}
}

View File

@ -0,0 +1,177 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Numerics;
using System.Text;
namespace MinesweeperSolver {
public class Binomial {
private static readonly double LOG10 = Math.Log(10);
private readonly int max;
private readonly PrimeSieve ps;
private readonly BigInteger[,] binomialLookup;
private readonly int lookupLimit;
public Binomial(int max, int lookup) {
this.max = max;
ps = new PrimeSieve(this.max);
if (lookup < 10) {
lookup = 10;
}
this.lookupLimit = lookup;
int lookup2 = lookup / 2;
binomialLookup = new BigInteger[lookup + 1, lookup2 + 1];
for (int total = 1; total <= lookup; total++) {
for (int choose = 0; choose <= total / 2; choose++) {
//try {
binomialLookup[total, choose] = Generate(choose, total);
//Console.WriteLine("Binomial " + total + " choose " + choose + " is " + binomialLookup[total, choose]);
//} catch (Exception e) {
// throw e;
//}
}
}
}
public BigInteger Generate(int k, int n) {
if (n == 0 && k == 0) {
return BigInteger.One;
}
if (n < 1) {
throw new Exception("Binomial: 1 <= n required, but n was " + n);
}
if (0 > k || k > n) {
throw new Exception("Binomial: 0 <= k and k <= n required, but n was " + n + " and k was " + k);
}
int choose = Math.Min(k, n - k);
if (n <= lookupLimit && binomialLookup[n, choose] != 0) { // it is zero when it hasn't been built yet
return binomialLookup[n, choose];
} else if (choose < 125) {
return Combination(choose, n);
} else if (n <= max) {
return CombinationLarge(choose, n);
} else {
return CombinationApprox(choose, n);
}
}
private static BigInteger Combination(int mines, int squares) {
BigInteger top = BigInteger.One;
BigInteger bot = BigInteger.One;
int range = Math.Min(mines, squares - mines);
// calculate the combination.
for (int i = 0; i < range; i++) {
top = top * new BigInteger(squares - i);
bot = bot * new BigInteger(i + 1);
}
BigInteger result = top / bot;
return result;
}
private BigInteger CombinationLarge(int k, int n) {
if ((k == 0) || (k == n)) return BigInteger.One;
int n2 = n / 2;
if (k > n2) {
k = n - k;
}
int nk = n - k;
int rootN = (int)Math.Floor(Math.Sqrt(n));
BigInteger result = BigInteger.One;
foreach (int prime in ps.getPrimesIterable(2, n)) {
if (prime > nk) {
result = result * new BigInteger(prime);
continue;
}
if (prime > n2) {
continue;
}
if (prime > rootN) {
if (n % prime < k % prime) {
result = result * new BigInteger(prime);
}
continue;
}
int r = 0, N = n, K = k, p = 1;
while (N > 0) {
r = (N % prime) < (K % prime + r) ? 1 : 0;
if (r == 1) {
p *= prime;
}
N /= prime;
K /= prime;
}
if (p > 1) {
result = result * new BigInteger(p);
}
}
return result;
}
// use the stirling approximation for factorials to create an approximate combinatorial
public BigInteger CombinationApprox(int k, int n) {
double logComb = (LogFactorialApprox(n) - LogFactorialApprox(k) - LogFactorialApprox(n - k));
int power = (int) Math.Floor(logComb);
int dp = Math.Min(6, power);
//double value = Math.Exp((logComb - power + dp) * LOG10);
//BigInteger result = new BigInteger(sigDigit) * BigInteger.Pow(new BigInteger(10), power - dp);
// find the significant digits
int sigDigits = (int) Math.Round(Math.Pow(10, logComb - power + dp));
String scientific = "" + sigDigits + "E" + (power - dp);
BigInteger result = BigInteger.Parse(scientific, NumberStyles.AllowExponent);
return result;
}
// returns an approximation for Log(n!)
private double LogFactorialApprox(int n) {
return n * Math.Log10(n) + 0.5d * Math.Log10(2 * Math.PI * n);
}
}
}