| line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
|
1
|
|
|
|
|
|
|
package App::SweeperBot; |
|
2
|
|
|
|
|
|
|
|
|
3
|
|
|
|
|
|
|
# minesweeper.pl |
|
4
|
|
|
|
|
|
|
# |
|
5
|
|
|
|
|
|
|
# Win32::Screenshot, Win32::GuiTest, and Image::Magick are needed for this |
|
6
|
|
|
|
|
|
|
# program. Use ActivePerl's PPM to install the first two: |
|
7
|
|
|
|
|
|
|
# ppm> install Win32-GuiTest |
|
8
|
|
|
|
|
|
|
# ppm> install http://theoryx5.uwinnipeg.ca/ppms/Win32-Screenshot.ppd |
|
9
|
|
|
|
|
|
|
# |
|
10
|
|
|
|
|
|
|
# The version of Image-Magick used by this code can be found at |
|
11
|
|
|
|
|
|
|
# http://www.bribes.org/perl/ppmdir.html . Different ImageMagick |
|
12
|
|
|
|
|
|
|
# distributions may result in different signature codes. |
|
13
|
|
|
|
|
|
|
# |
|
14
|
|
|
|
|
|
|
# 20050726, Matt Sparks (f0rked), http://f0rked.com |
|
15
|
|
|
|
|
|
|
|
|
16
|
|
|
|
|
|
|
=head1 NAME |
|
17
|
|
|
|
|
|
|
|
|
18
|
|
|
|
|
|
|
App::SweeperBot - Play windows minesweeper, automatically! |
|
19
|
|
|
|
|
|
|
|
|
20
|
|
|
|
|
|
|
=head1 SYNOPSIS |
|
21
|
|
|
|
|
|
|
|
|
22
|
|
|
|
|
|
|
C:\Path\To\Distribution> SweeperBot.exe |
|
23
|
|
|
|
|
|
|
|
|
24
|
|
|
|
|
|
|
=head1 DESCRIPTION |
|
25
|
|
|
|
|
|
|
|
|
26
|
|
|
|
|
|
|
This is alpha code, and released for testing and demonstration |
|
27
|
|
|
|
|
|
|
purposes only. It is still under active development. |
|
28
|
|
|
|
|
|
|
|
|
29
|
|
|
|
|
|
|
Using this code for playing minesweeper on a production basis is |
|
30
|
|
|
|
|
|
|
strongly discouraged. |
|
31
|
|
|
|
|
|
|
|
|
32
|
|
|
|
|
|
|
=head1 METHODS |
|
33
|
|
|
|
|
|
|
|
|
34
|
|
|
|
|
|
|
=cut |
|
35
|
|
|
|
|
|
|
|
|
36
|
1
|
|
|
1
|
|
22099
|
use strict; |
|
|
1
|
|
|
|
|
3
|
|
|
|
1
|
|
|
|
|
33
|
|
|
37
|
1
|
|
|
1
|
|
5
|
use warnings; |
|
|
1
|
|
|
|
|
3
|
|
|
|
1
|
|
|
|
|
25
|
|
|
38
|
1
|
|
|
1
|
|
5
|
use Carp; |
|
|
1
|
|
|
|
|
5
|
|
|
|
1
|
|
|
|
|
79
|
|
|
39
|
1
|
|
|
1
|
|
20946
|
use NEXT; |
|
|
1
|
|
|
|
|
16199
|
|
|
|
1
|
|
|
|
|
39
|
|
|
40
|
|
|
|
|
|
|
|
|
41
|
1
|
|
|
1
|
|
40
|
use 5.006; |
|
|
1
|
|
|
|
|
4
|
|
|
|
1
|
|
|
|
|
782
|
|
|
42
|
|
|
|
|
|
|
|
|
43
|
|
|
|
|
|
|
our $VERSION = '0.03'; |
|
44
|
|
|
|
|
|
|
|
|
45
|
1
|
|
|
1
|
|
13
|
use Scalar::Util qw(looks_like_number); |
|
|
1
|
|
|
|
|
4
|
|
|
|
1
|
|
|
|
|
147
|
|
|
46
|
1
|
|
|
1
|
|
2870
|
use Win32::Process qw(NORMAL_PRIORITY_CLASS); |
|
|
0
|
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
47
|
|
|
|
|
|
|
|
|
48
|
|
|
|
|
|
|
use constant DEBUG => 0; |
|
49
|
|
|
|
|
|
|
use constant VERBOSE => 0; |
|
50
|
|
|
|
|
|
|
use constant CHEAT => 1; |
|
51
|
|
|
|
|
|
|
use constant UBER_CHEAT => 0; |
|
52
|
|
|
|
|
|
|
|
|
53
|
|
|
|
|
|
|
use constant SMILEY_LENGTH => 26; |
|
54
|
|
|
|
|
|
|
|
|
55
|
|
|
|
|
|
|
# The minimum and maximum top dressings define the range in which |
|
56
|
|
|
|
|
|
|
# we'll look for a smiley, which we use to calibrate our board. Different |
|
57
|
|
|
|
|
|
|
# windows themes put them in different places. |
|
58
|
|
|
|
|
|
|
|
|
59
|
|
|
|
|
|
|
use constant MINIMUM_TOP_DRESSING => 56; |
|
60
|
|
|
|
|
|
|
use constant MAXIMUM_TOP_DRESSING => 75; |
|
61
|
|
|
|
|
|
|
|
|
62
|
|
|
|
|
|
|
my $Smiley_offset = 0; |
|
63
|
|
|
|
|
|
|
|
|
64
|
|
|
|
|
|
|
use constant CHEAT_SAFE => "d0737abfd3abdacfeb15d559e28c2f0b3662a7aa03ac5b7a58afc422110db75a"; # Old 58 |
|
65
|
|
|
|
|
|
|
# use constant CHEAT_SAFE => "ad95131bc0b799c0b1af477fb14fcf26a6a9f76079e48bf090acb7e8367bfd0e"; # Old 510 |
|
66
|
|
|
|
|
|
|
|
|
67
|
|
|
|
|
|
|
use constant CHEAT_UNSAFE => "374708fff7719dd5979ec875d56cd2286f6d3cf7ec317a3b25632aab28ec37bb"; # Old 58 |
|
68
|
|
|
|
|
|
|
# use constant CHEAT_UNSAFE => "e3820096cb82366b860b8a4e668453a7aaaf423af03bdf289fa308ea03a79332"; # Old 510 |
|
69
|
|
|
|
|
|
|
|
|
70
|
|
|
|
|
|
|
# alarm(180); # Nuke process after three minutes, in case of run-aways. |
|
71
|
|
|
|
|
|
|
|
|
72
|
|
|
|
|
|
|
use Win32::Screenshot; |
|
73
|
|
|
|
|
|
|
|
|
74
|
|
|
|
|
|
|
use Win32::GuiTest qw( |
|
75
|
|
|
|
|
|
|
FindWindowLike |
|
76
|
|
|
|
|
|
|
GetWindowRect |
|
77
|
|
|
|
|
|
|
SendMouse |
|
78
|
|
|
|
|
|
|
MouseMoveAbsPix |
|
79
|
|
|
|
|
|
|
SendKeys |
|
80
|
|
|
|
|
|
|
); |
|
81
|
|
|
|
|
|
|
|
|
82
|
|
|
|
|
|
|
# Square width and height. |
|
83
|
|
|
|
|
|
|
|
|
84
|
|
|
|
|
|
|
use constant SQUARE_W => 16; |
|
85
|
|
|
|
|
|
|
use constant SQUARE_H => 16; |
|
86
|
|
|
|
|
|
|
|
|
87
|
|
|
|
|
|
|
# Top-left square location (15,104) |
|
88
|
|
|
|
|
|
|
|
|
89
|
|
|
|
|
|
|
use constant SQUARE1X => 15; |
|
90
|
|
|
|
|
|
|
|
|
91
|
|
|
|
|
|
|
use constant MIN_SQUARE1Y => 96; |
|
92
|
|
|
|
|
|
|
use constant MAX_SQAURE1Y => 115; |
|
93
|
|
|
|
|
|
|
|
|
94
|
|
|
|
|
|
|
# How far left of the smiley to click to focus on the board. |
|
95
|
|
|
|
|
|
|
use constant FOCUS_X_OFFSET => 50; |
|
96
|
|
|
|
|
|
|
|
|
97
|
|
|
|
|
|
|
my $Square1Y; |
|
98
|
|
|
|
|
|
|
|
|
99
|
|
|
|
|
|
|
my %char_for = ( |
|
100
|
|
|
|
|
|
|
0 => 0, |
|
101
|
|
|
|
|
|
|
unpressed => ".", |
|
102
|
|
|
|
|
|
|
1 => 1, |
|
103
|
|
|
|
|
|
|
2 => 2, |
|
104
|
|
|
|
|
|
|
3 => 3, |
|
105
|
|
|
|
|
|
|
4 => 4, |
|
106
|
|
|
|
|
|
|
5 => 5, |
|
107
|
|
|
|
|
|
|
6 => 6, |
|
108
|
|
|
|
|
|
|
7 => 7, |
|
109
|
|
|
|
|
|
|
8 => 8, |
|
110
|
|
|
|
|
|
|
bomb => "x", |
|
111
|
|
|
|
|
|
|
bomb_hilight => "X", |
|
112
|
|
|
|
|
|
|
flag => "*", |
|
113
|
|
|
|
|
|
|
); |
|
114
|
|
|
|
|
|
|
|
|
115
|
|
|
|
|
|
|
# 1 => Won, -1 => Lost, 0 => Still playing |
|
116
|
|
|
|
|
|
|
|
|
117
|
|
|
|
|
|
|
my %smiley_type = ( |
|
118
|
|
|
|
|
|
|
'd28bcc05d38fd736f6715388a12cb0b96da9852432669671ee7866135f35bbb7' => 1, |
|
119
|
|
|
|
|
|
|
'efef2037072c56fb029da1dd2cd626282173d0e1b2be39eab3e955cd2bcdc856' => 1, |
|
120
|
|
|
|
|
|
|
'08938969d349a6677a17a65a57f2887a85d1a7187dcd6c20d238e279a5ec3c18' => -1, |
|
121
|
|
|
|
|
|
|
'7cf1797ad25730136aa67c0a039b0c596f1aed9de8720999145248c72df52d1b' => -1, |
|
122
|
|
|
|
|
|
|
'56f7c05869d42918830e80ad5bf841109d88e17b38fc069c3e5bf19623a88711' => 0, |
|
123
|
|
|
|
|
|
|
'0955e50dda3f850913392d4e654f9ef45df046f063a4b8faeff530609b37379f' => 0, |
|
124
|
|
|
|
|
|
|
); |
|
125
|
|
|
|
|
|
|
|
|
126
|
|
|
|
|
|
|
# old - Bribes distro ImageMagick |
|
127
|
|
|
|
|
|
|
# new - "Official" ImageMagick |
|
128
|
|
|
|
|
|
|
# NB: This code is primarily tested under the bribes distribution of |
|
129
|
|
|
|
|
|
|
# ImageMagick, because it plays nicely with PAR. YMMV with other |
|
130
|
|
|
|
|
|
|
# versions. |
|
131
|
|
|
|
|
|
|
|
|
132
|
|
|
|
|
|
|
my %contents_of_square = ( |
|
133
|
|
|
|
|
|
|
"0b6f3e019208789db304a8a8c8bd509dacf62050a962ae9a0385733d6b595427" => 0, # old |
|
134
|
|
|
|
|
|
|
"cd348e1e78e4032f472c5c065c99d8289dffff7041096aa8746e29794a032698" => 0, # new |
|
135
|
|
|
|
|
|
|
"35fc6aa19ab4b99bf7d4a750767ee329b773fb2709bec46204d0ffb0a2eae1e0" => "unpressed", # old |
|
136
|
|
|
|
|
|
|
"880113df76cbba6336d3d1c93b035e904dbce5663acb35f9494eb292bda0226c" => "unpressed", # new |
|
137
|
|
|
|
|
|
|
"7a66485db1fee47e7c33acff15df5b48feccbc0328ea6e68795e52ce43649e1a" => 1, # old |
|
138
|
|
|
|
|
|
|
"99a8c67265186adef6cb5d4d4b37fefc120f096fa9df6fe0b4f90d6843fcc1e1" => 1, # new |
|
139
|
|
|
|
|
|
|
"ab70100c9ac47c63edf679d838fbb10ca38a567a16132aaf42ed2fe159aa8605" => 2, # old |
|
140
|
|
|
|
|
|
|
"3bb6ebdba9eead463b427b9cc94881626275b9efc9dfd552e174a017c601d9c2" => 2, # new |
|
141
|
|
|
|
|
|
|
"799f98eb9f61f3e96def93145a6a065cf872e67647939a7e0f4c623f38f585c3" => 3, # old |
|
142
|
|
|
|
|
|
|
"bdb6e1609d57dfa5559860e9856919ba82c844043e6a294387d975bf55208133" => 3, # new |
|
143
|
|
|
|
|
|
|
"b5b29ae361a9acf85ac81abb440d5a3f7525fe80738a5770df90832d0367f7d6" => 4, # old |
|
144
|
|
|
|
|
|
|
"56c72e77e03691789f10960bd4f728af2eb7a57dd04c977e6b2ab19b349e1943" => 4, # new |
|
145
|
|
|
|
|
|
|
"bff653f26af9160d66965635c8306795ca2440cd1e4eebf0f315c7abd0242fc6" => 5, # old |
|
146
|
|
|
|
|
|
|
"2ce52acf436da1971ed234b8607d4928add74c5c02d8a012fce56477b52ba251" => 5, # new |
|
147
|
|
|
|
|
|
|
"931b3e6a380fd85ee808fd4ac788123a0873bb3c1c30ec1737cea8e624ff866a" => 6, # old |
|
148
|
|
|
|
|
|
|
"36dc562ae36f15c7d3917e101a998736b3dc1a457872fea40e1f4bc896c3725c" => 6, # new |
|
149
|
|
|
|
|
|
|
"e5531a6de436ac50d36096b9d1b17bad2c919923650ca48063119f9868eb3943" => 7, # old |
|
150
|
|
|
|
|
|
|
"2d95bf5bb506232fe283d18d3fac1ac331ddc8116c7dde83e02a3aaae7da47e6" => 7, # new |
|
151
|
|
|
|
|
|
|
"c18dd2d3747aa97a9f432993de175bd32f8e38a70a8c122c94c737f8909bc3ca" => 8, |
|
152
|
|
|
|
|
|
|
"ad10157084c576142c0b0e811ddf9f935c3aab5925831fe3bf9a2da226c0c6d9" => "bomb", |
|
153
|
|
|
|
|
|
|
"d748d75fb4fbff41cf54237a5e0fa919189a927f1776683f141a4e38feff06ab" => "bomb_hilight", |
|
154
|
|
|
|
|
|
|
"e4305b6c2c750ebf0869a465f5e4f7721107bf066872edbcacd15c399ae60bff" => "flag", # old |
|
155
|
|
|
|
|
|
|
"645d48aa778b2ac881a3921f3044a8ed96b8029915d9b300abbe91bef3427784" => "flag", # new |
|
156
|
|
|
|
|
|
|
); |
|
157
|
|
|
|
|
|
|
|
|
158
|
|
|
|
|
|
|
=head2 new |
|
159
|
|
|
|
|
|
|
|
|
160
|
|
|
|
|
|
|
my $sweperbot = App::SweeperBot->new; |
|
161
|
|
|
|
|
|
|
|
|
162
|
|
|
|
|
|
|
Creates a new C object. Does not use any |
|
163
|
|
|
|
|
|
|
arguments passed, but will send them verbatim to an C<_init> |
|
164
|
|
|
|
|
|
|
method if defined on a child class. |
|
165
|
|
|
|
|
|
|
|
|
166
|
|
|
|
|
|
|
=cut |
|
167
|
|
|
|
|
|
|
|
|
168
|
|
|
|
|
|
|
sub new { |
|
169
|
|
|
|
|
|
|
my ($class, @args) = @_; |
|
170
|
|
|
|
|
|
|
|
|
171
|
|
|
|
|
|
|
my $this = {}; |
|
172
|
|
|
|
|
|
|
|
|
173
|
|
|
|
|
|
|
bless($this, $class); |
|
174
|
|
|
|
|
|
|
|
|
175
|
|
|
|
|
|
|
$this->EVERY::LAST::_init(@args); |
|
176
|
|
|
|
|
|
|
|
|
177
|
|
|
|
|
|
|
return $this; |
|
178
|
|
|
|
|
|
|
} |
|
179
|
|
|
|
|
|
|
|
|
180
|
|
|
|
|
|
|
|
|
181
|
|
|
|
|
|
|
|
|
182
|
|
|
|
|
|
|
=head2 spawn_minesweeper |
|
183
|
|
|
|
|
|
|
|
|
184
|
|
|
|
|
|
|
$sweeperbot->spawn_minesweeper; |
|
185
|
|
|
|
|
|
|
|
|
186
|
|
|
|
|
|
|
Attempts to spawn a new minesweeper instance. Returns the |
|
187
|
|
|
|
|
|
|
C object on success, or throws an exception |
|
188
|
|
|
|
|
|
|
on error. |
|
189
|
|
|
|
|
|
|
|
|
190
|
|
|
|
|
|
|
=cut |
|
191
|
|
|
|
|
|
|
|
|
192
|
|
|
|
|
|
|
sub spawn_minesweeper { |
|
193
|
|
|
|
|
|
|
|
|
194
|
|
|
|
|
|
|
Win32::Process::Create( |
|
195
|
|
|
|
|
|
|
my $minesweeper, |
|
196
|
|
|
|
|
|
|
"$ENV{SystemRoot}\\system32\\winmine.exe", |
|
197
|
|
|
|
|
|
|
"", |
|
198
|
|
|
|
|
|
|
0, |
|
199
|
|
|
|
|
|
|
NORMAL_PRIORITY_CLASS, |
|
200
|
|
|
|
|
|
|
"." |
|
201
|
|
|
|
|
|
|
) or croak "Cannot spawn minesweeper! - ". |
|
202
|
|
|
|
|
|
|
Win32::FormatError(Win32::GetLastError()); |
|
203
|
|
|
|
|
|
|
|
|
204
|
|
|
|
|
|
|
return $minesweeper; |
|
205
|
|
|
|
|
|
|
|
|
206
|
|
|
|
|
|
|
} |
|
207
|
|
|
|
|
|
|
|
|
208
|
|
|
|
|
|
|
=head2 locate_minesweeper |
|
209
|
|
|
|
|
|
|
|
|
210
|
|
|
|
|
|
|
$sweeperbot->locate_minesweeper; |
|
211
|
|
|
|
|
|
|
|
|
212
|
|
|
|
|
|
|
Locates the first minesweeper window that can be found, brings |
|
213
|
|
|
|
|
|
|
it into focus, and sets relevant state so that it can be |
|
214
|
|
|
|
|
|
|
acessed later. Must be used before a game can be started |
|
215
|
|
|
|
|
|
|
or played. Should be used if the minesweeper window |
|
216
|
|
|
|
|
|
|
changes size or position. |
|
217
|
|
|
|
|
|
|
|
|
218
|
|
|
|
|
|
|
Returns the window ID on success. Throws an exception on |
|
219
|
|
|
|
|
|
|
failure. |
|
220
|
|
|
|
|
|
|
|
|
221
|
|
|
|
|
|
|
=cut |
|
222
|
|
|
|
|
|
|
|
|
223
|
|
|
|
|
|
|
sub locate_minesweeper { |
|
224
|
|
|
|
|
|
|
my ($this) = @_; |
|
225
|
|
|
|
|
|
|
|
|
226
|
|
|
|
|
|
|
our $id=(FindWindowLike(0, "^Minesweeper"))[0]; |
|
227
|
|
|
|
|
|
|
our($l,$t,$r,$b)=GetWindowRect($id); |
|
228
|
|
|
|
|
|
|
our($w,$h)=($r-$l,$b-$t); |
|
229
|
|
|
|
|
|
|
# our($reset_x,$reset_y)=($l+$w/2,$t+70); |
|
230
|
|
|
|
|
|
|
our($reset_x,$reset_y)=($l+$w/2,$t+81); |
|
231
|
|
|
|
|
|
|
|
|
232
|
|
|
|
|
|
|
# Figure out our total number of squares |
|
233
|
|
|
|
|
|
|
# "header" of window is 96px tall |
|
234
|
|
|
|
|
|
|
# left side: 15px, right side: 11px |
|
235
|
|
|
|
|
|
|
# bottom is 11px tall |
|
236
|
|
|
|
|
|
|
|
|
237
|
|
|
|
|
|
|
# TODO - These constants are bogus, and depend upon the windowing |
|
238
|
|
|
|
|
|
|
# style used. |
|
239
|
|
|
|
|
|
|
# our($squares_x,$squares_y)=(($w-15-11)/SQUARE_W,($h-96-11)/SQUARE_H); |
|
240
|
|
|
|
|
|
|
our($squares_x,$squares_y)=(($w-15-11)/SQUARE_W,($h-104-11)/SQUARE_H); |
|
241
|
|
|
|
|
|
|
|
|
242
|
|
|
|
|
|
|
# Round up squares_y. TODO: This is a kludge to deal with |
|
243
|
|
|
|
|
|
|
# different window decorations. |
|
244
|
|
|
|
|
|
|
$squares_y = int ($squares_y + 0.9); |
|
245
|
|
|
|
|
|
|
|
|
246
|
|
|
|
|
|
|
our $squares=$squares_x*$squares_y; |
|
247
|
|
|
|
|
|
|
|
|
248
|
|
|
|
|
|
|
# Display status information |
|
249
|
|
|
|
|
|
|
print "Width: $w, height: $h\n" if VERBOSE; |
|
250
|
|
|
|
|
|
|
print "$squares_x across, $squares_y down, $squares total\n" if VERBOSE; |
|
251
|
|
|
|
|
|
|
|
|
252
|
|
|
|
|
|
|
print "Focusing on the window\n" if VERBOSE; |
|
253
|
|
|
|
|
|
|
$this->focus(); |
|
254
|
|
|
|
|
|
|
|
|
255
|
|
|
|
|
|
|
return $id; |
|
256
|
|
|
|
|
|
|
} |
|
257
|
|
|
|
|
|
|
|
|
258
|
|
|
|
|
|
|
=head2 click |
|
259
|
|
|
|
|
|
|
|
|
260
|
|
|
|
|
|
|
$sweeperbot->click($x,$y,$button); |
|
261
|
|
|
|
|
|
|
|
|
262
|
|
|
|
|
|
|
Clicks on ($x,$y) as an I position on the screen. |
|
263
|
|
|
|
|
|
|
C<$button> is any button as understood by L, |
|
264
|
|
|
|
|
|
|
usually C<{LEFTCLICK}>, C<{MIDDLECLICK}> or C<{RIGHTCLICK}>. |
|
265
|
|
|
|
|
|
|
|
|
266
|
|
|
|
|
|
|
If not specified, C<$button> defaults to a left-click. |
|
267
|
|
|
|
|
|
|
|
|
268
|
|
|
|
|
|
|
Returns nothing. |
|
269
|
|
|
|
|
|
|
|
|
270
|
|
|
|
|
|
|
=cut |
|
271
|
|
|
|
|
|
|
|
|
272
|
|
|
|
|
|
|
# Click the left button of the mouse. |
|
273
|
|
|
|
|
|
|
# Arguments: x, y as ABSOLUTE positions on the screen |
|
274
|
|
|
|
|
|
|
sub click { |
|
275
|
|
|
|
|
|
|
my($this, $x,$y,$button)=@_; |
|
276
|
|
|
|
|
|
|
$button ||= "{LEFTCLICK}"; |
|
277
|
|
|
|
|
|
|
MouseMoveAbsPix($x,$y); |
|
278
|
|
|
|
|
|
|
print "Button: $button ($x,$y)\n" if DEBUG; |
|
279
|
|
|
|
|
|
|
SendMouse($button); |
|
280
|
|
|
|
|
|
|
return; |
|
281
|
|
|
|
|
|
|
} |
|
282
|
|
|
|
|
|
|
|
|
283
|
|
|
|
|
|
|
=head2 new_game |
|
284
|
|
|
|
|
|
|
|
|
285
|
|
|
|
|
|
|
$sweeperbot->new_game; |
|
286
|
|
|
|
|
|
|
|
|
287
|
|
|
|
|
|
|
Starts a new game of minesweeper. C must |
|
288
|
|
|
|
|
|
|
have been called previously for this to work. |
|
289
|
|
|
|
|
|
|
|
|
290
|
|
|
|
|
|
|
Does not return a value, nor does it check to see if a new game |
|
291
|
|
|
|
|
|
|
has been successfully started. |
|
292
|
|
|
|
|
|
|
|
|
293
|
|
|
|
|
|
|
=cut |
|
294
|
|
|
|
|
|
|
|
|
295
|
|
|
|
|
|
|
|
|
296
|
|
|
|
|
|
|
# TODO: Rather than using the reset variables, we should properly |
|
297
|
|
|
|
|
|
|
# calculate the location of our reset button. We have calibration |
|
298
|
|
|
|
|
|
|
# code elsewhere that essentially finds the smiley, we just have to |
|
299
|
|
|
|
|
|
|
# click on it. |
|
300
|
|
|
|
|
|
|
|
|
301
|
|
|
|
|
|
|
sub new_game { |
|
302
|
|
|
|
|
|
|
my ($this) = @_; |
|
303
|
|
|
|
|
|
|
our ($reset_x,$reset_y); |
|
304
|
|
|
|
|
|
|
$this->click($reset_x,$reset_y); |
|
305
|
|
|
|
|
|
|
return; |
|
306
|
|
|
|
|
|
|
} |
|
307
|
|
|
|
|
|
|
|
|
308
|
|
|
|
|
|
|
=head2 focus |
|
309
|
|
|
|
|
|
|
|
|
310
|
|
|
|
|
|
|
$sweeperbot->focus; |
|
311
|
|
|
|
|
|
|
|
|
312
|
|
|
|
|
|
|
Focuses on t he minesweeper window by clicking a little left of the |
|
313
|
|
|
|
|
|
|
smiley. Does not check for success. Returns nothing. |
|
314
|
|
|
|
|
|
|
|
|
315
|
|
|
|
|
|
|
=cut |
|
316
|
|
|
|
|
|
|
|
|
317
|
|
|
|
|
|
|
# Focus on the Minesweeper window by clicking a little to the left of the game |
|
318
|
|
|
|
|
|
|
# button. |
|
319
|
|
|
|
|
|
|
sub focus { |
|
320
|
|
|
|
|
|
|
my ($this) = @_; |
|
321
|
|
|
|
|
|
|
our ($reset_x, $reset_y); |
|
322
|
|
|
|
|
|
|
$this->click($reset_x - FOCUS_X_OFFSET ,$reset_y); |
|
323
|
|
|
|
|
|
|
return; |
|
324
|
|
|
|
|
|
|
} |
|
325
|
|
|
|
|
|
|
|
|
326
|
|
|
|
|
|
|
=head2 capture_square |
|
327
|
|
|
|
|
|
|
|
|
328
|
|
|
|
|
|
|
my $image = $sweeperbot->capture_square($x,$y); |
|
329
|
|
|
|
|
|
|
|
|
330
|
|
|
|
|
|
|
Captures the square ($x,$y) of the minesweeper board. (1,1) is |
|
331
|
|
|
|
|
|
|
the top-left of the grid. No checking is done to see if the square |
|
332
|
|
|
|
|
|
|
is actually on the board. Returns the image as an L |
|
333
|
|
|
|
|
|
|
object. |
|
334
|
|
|
|
|
|
|
|
|
335
|
|
|
|
|
|
|
=head3 Bugs in capture_square |
|
336
|
|
|
|
|
|
|
|
|
337
|
|
|
|
|
|
|
On failure to capture the image, this returns an empty |
|
338
|
|
|
|
|
|
|
L object. This is considered a bug; in the future |
|
339
|
|
|
|
|
|
|
C will throw an exception on error. |
|
340
|
|
|
|
|
|
|
|
|
341
|
|
|
|
|
|
|
C depends upon calibration routines that are |
|
342
|
|
|
|
|
|
|
currently implemented in the L method; calling it before |
|
343
|
|
|
|
|
|
|
the first call to L can result in incorrect or inconsistent |
|
344
|
|
|
|
|
|
|
results. In future releases C will automatically |
|
345
|
|
|
|
|
|
|
calibrate itself if required. |
|
346
|
|
|
|
|
|
|
|
|
347
|
|
|
|
|
|
|
=cut |
|
348
|
|
|
|
|
|
|
|
|
349
|
|
|
|
|
|
|
# TODO GuiTest doesn't check the Image::Magick return codes, it |
|
350
|
|
|
|
|
|
|
# just assumes everything works. We should consider writing our |
|
351
|
|
|
|
|
|
|
# own code that _does_ test, since these diagnostics are very |
|
352
|
|
|
|
|
|
|
# useful when things go wrong. |
|
353
|
|
|
|
|
|
|
|
|
354
|
|
|
|
|
|
|
sub capture_square { |
|
355
|
|
|
|
|
|
|
my($this, $sx,$sy)=@_; |
|
356
|
|
|
|
|
|
|
our($l,$t); |
|
357
|
|
|
|
|
|
|
my $image=CaptureRect( |
|
358
|
|
|
|
|
|
|
$l+SQUARE1X+($sx-1)*SQUARE_W, |
|
359
|
|
|
|
|
|
|
$t+$Square1Y+($sy-1)*SQUARE_H, |
|
360
|
|
|
|
|
|
|
SQUARE_W, |
|
361
|
|
|
|
|
|
|
SQUARE_H |
|
362
|
|
|
|
|
|
|
); |
|
363
|
|
|
|
|
|
|
return $image; |
|
364
|
|
|
|
|
|
|
} |
|
365
|
|
|
|
|
|
|
|
|
366
|
|
|
|
|
|
|
=head2 value |
|
367
|
|
|
|
|
|
|
|
|
368
|
|
|
|
|
|
|
my $value = $sweeperbot->value($x,$y); |
|
369
|
|
|
|
|
|
|
|
|
370
|
|
|
|
|
|
|
Returns the value in position ($x,$y) of the board, square |
|
371
|
|
|
|
|
|
|
(1,1) is considered the top-left of the grid. Possible values |
|
372
|
|
|
|
|
|
|
are given below: |
|
373
|
|
|
|
|
|
|
|
|
374
|
|
|
|
|
|
|
0-8 # Number of adjacent mines (0 = empty) |
|
375
|
|
|
|
|
|
|
bomb # A bomb (only when game lost) |
|
376
|
|
|
|
|
|
|
bomb_hilight # The bomb we hit (only when game lost) |
|
377
|
|
|
|
|
|
|
flag # A flag |
|
378
|
|
|
|
|
|
|
unpressed # An unpressed square |
|
379
|
|
|
|
|
|
|
|
|
380
|
|
|
|
|
|
|
Support of question-marks is not provided, but may be included |
|
381
|
|
|
|
|
|
|
in a future version. |
|
382
|
|
|
|
|
|
|
|
|
383
|
|
|
|
|
|
|
Throws an exception on failure. |
|
384
|
|
|
|
|
|
|
|
|
385
|
|
|
|
|
|
|
=cut |
|
386
|
|
|
|
|
|
|
|
|
387
|
|
|
|
|
|
|
sub value { |
|
388
|
|
|
|
|
|
|
my($this, $sx,$sy)=@_; |
|
389
|
|
|
|
|
|
|
|
|
390
|
|
|
|
|
|
|
if (not $Square1Y) { |
|
391
|
|
|
|
|
|
|
# We haven't calibrated our board yet. Let's see if we can |
|
392
|
|
|
|
|
|
|
# find a square we recognise. |
|
393
|
|
|
|
|
|
|
|
|
394
|
|
|
|
|
|
|
CALIBRATION: { |
|
395
|
|
|
|
|
|
|
for (my $i = MIN_SQUARE1Y; $i <= MAX_SQAURE1Y; $i++) { |
|
396
|
|
|
|
|
|
|
$Square1Y = $i; |
|
397
|
|
|
|
|
|
|
|
|
398
|
|
|
|
|
|
|
warn "Trying to calibrate board $i pixels down\n" if DEBUG; |
|
399
|
|
|
|
|
|
|
|
|
400
|
|
|
|
|
|
|
my $sig = $this->capture_square(1,1)->Get("signature"); |
|
401
|
|
|
|
|
|
|
|
|
402
|
|
|
|
|
|
|
# Known signature, break out of calibration loop. |
|
403
|
|
|
|
|
|
|
last CALIBRATION if ($contents_of_square{$sig}); |
|
404
|
|
|
|
|
|
|
} |
|
405
|
|
|
|
|
|
|
|
|
406
|
|
|
|
|
|
|
# If we're here, we couldn't calibrate |
|
407
|
|
|
|
|
|
|
die "Board calibration failed\n"; |
|
408
|
|
|
|
|
|
|
} |
|
409
|
|
|
|
|
|
|
} |
|
410
|
|
|
|
|
|
|
|
|
411
|
|
|
|
|
|
|
my $sig = $this-> capture_square($sx,$sy)->Get("signature"); |
|
412
|
|
|
|
|
|
|
|
|
413
|
|
|
|
|
|
|
my $result = $contents_of_square{$sig}; |
|
414
|
|
|
|
|
|
|
|
|
415
|
|
|
|
|
|
|
defined($result) or die "Square $sx,$sy contains a value I don't recognise\n\n$sig\n\n"; |
|
416
|
|
|
|
|
|
|
|
|
417
|
|
|
|
|
|
|
return $result; |
|
418
|
|
|
|
|
|
|
} |
|
419
|
|
|
|
|
|
|
|
|
420
|
|
|
|
|
|
|
=head2 press |
|
421
|
|
|
|
|
|
|
|
|
422
|
|
|
|
|
|
|
$sweeperbot->press($x,$y, $button) |
|
423
|
|
|
|
|
|
|
|
|
424
|
|
|
|
|
|
|
Clicks on the square with co-ordinates ($x,$y) using the mouse-button |
|
425
|
|
|
|
|
|
|
C<$button>, or left-click by default. Square (1,1) |
|
426
|
|
|
|
|
|
|
is the top-left square. Does not return a value. |
|
427
|
|
|
|
|
|
|
|
|
428
|
|
|
|
|
|
|
=cut |
|
429
|
|
|
|
|
|
|
|
|
430
|
|
|
|
|
|
|
sub press { |
|
431
|
|
|
|
|
|
|
my($this, $sx,$sy,$button)=@_; |
|
432
|
|
|
|
|
|
|
$button ||= "{LEFTCLICK}"; |
|
433
|
|
|
|
|
|
|
our($l,$t); |
|
434
|
|
|
|
|
|
|
$this->click( |
|
435
|
|
|
|
|
|
|
$l+SQUARE1X+($sx-1)*SQUARE_W+SQUARE_W/2, |
|
436
|
|
|
|
|
|
|
$t+$Square1Y+($sy-1)*SQUARE_H+SQUARE_W/2, |
|
437
|
|
|
|
|
|
|
$button |
|
438
|
|
|
|
|
|
|
); |
|
439
|
|
|
|
|
|
|
|
|
440
|
|
|
|
|
|
|
return; |
|
441
|
|
|
|
|
|
|
} |
|
442
|
|
|
|
|
|
|
|
|
443
|
|
|
|
|
|
|
=head2 stomp |
|
444
|
|
|
|
|
|
|
|
|
445
|
|
|
|
|
|
|
$sweeperbot->stomp($x,$y); |
|
446
|
|
|
|
|
|
|
|
|
447
|
|
|
|
|
|
|
Stomps (middle-clicks) on the square at ($x,$y), normally used to |
|
448
|
|
|
|
|
|
|
stand on all squares adjacent to the square specified. Square (1,1) |
|
449
|
|
|
|
|
|
|
is the top-left of the grid. Does not return a value. |
|
450
|
|
|
|
|
|
|
|
|
451
|
|
|
|
|
|
|
=cut |
|
452
|
|
|
|
|
|
|
|
|
453
|
|
|
|
|
|
|
# Stomp on a square (left+right click) |
|
454
|
|
|
|
|
|
|
sub stomp { |
|
455
|
|
|
|
|
|
|
my ($this, $x, $y) = @_; |
|
456
|
|
|
|
|
|
|
$this->press($x,$y,"{MIDDLECLICK}"); |
|
457
|
|
|
|
|
|
|
|
|
458
|
|
|
|
|
|
|
return; |
|
459
|
|
|
|
|
|
|
} |
|
460
|
|
|
|
|
|
|
|
|
461
|
|
|
|
|
|
|
=head2 flag_mines |
|
462
|
|
|
|
|
|
|
|
|
463
|
|
|
|
|
|
|
$sweeperbot->flag_mines($game_state, |
|
464
|
|
|
|
|
|
|
[2,3], [7,1], [8,3] |
|
465
|
|
|
|
|
|
|
); |
|
466
|
|
|
|
|
|
|
|
|
467
|
|
|
|
|
|
|
Takes a game state, and a list of location tuples (array-refs), |
|
468
|
|
|
|
|
|
|
and marks all of those locations with flags. |
|
469
|
|
|
|
|
|
|
|
|
470
|
|
|
|
|
|
|
The requirement to pass C<$game_state> may be removed in a |
|
471
|
|
|
|
|
|
|
future version. |
|
472
|
|
|
|
|
|
|
|
|
473
|
|
|
|
|
|
|
=cut |
|
474
|
|
|
|
|
|
|
|
|
475
|
|
|
|
|
|
|
sub flag_mines { |
|
476
|
|
|
|
|
|
|
my ($this, $game_state, @flag_these) = @_; |
|
477
|
|
|
|
|
|
|
|
|
478
|
|
|
|
|
|
|
foreach my $square (@flag_these) { |
|
479
|
|
|
|
|
|
|
my ($x,$y) = @$square; |
|
480
|
|
|
|
|
|
|
|
|
481
|
|
|
|
|
|
|
# Skip to the next square if we have record that this |
|
482
|
|
|
|
|
|
|
# has already been flagged (earlier this iteration). |
|
483
|
|
|
|
|
|
|
next if $game_state->[$x][$y] eq "flag"; |
|
484
|
|
|
|
|
|
|
|
|
485
|
|
|
|
|
|
|
$this->press($x,$y,"{RIGHTCLICK}"); |
|
486
|
|
|
|
|
|
|
$game_state->[$x][$y] = "flag"; |
|
487
|
|
|
|
|
|
|
} |
|
488
|
|
|
|
|
|
|
|
|
489
|
|
|
|
|
|
|
return; |
|
490
|
|
|
|
|
|
|
} |
|
491
|
|
|
|
|
|
|
|
|
492
|
|
|
|
|
|
|
=begin deprecated |
|
493
|
|
|
|
|
|
|
|
|
494
|
|
|
|
|
|
|
# This code is left here as a mathom, but isn't used anymore. |
|
495
|
|
|
|
|
|
|
# Generally we want to call flag_mines() to flag mines, or |
|
496
|
|
|
|
|
|
|
# stomp() to stomp on a square. |
|
497
|
|
|
|
|
|
|
|
|
498
|
|
|
|
|
|
|
sub mark_adjacent { |
|
499
|
|
|
|
|
|
|
my ($this, $x, $y) = @_; |
|
500
|
|
|
|
|
|
|
$this->press($x-1,$y-1,"{RIGHTCLICK}"); |
|
501
|
|
|
|
|
|
|
$this->press($x ,$y-1,"{RIGHTCLICK}"); |
|
502
|
|
|
|
|
|
|
$this->press($x+1,$y-1,"{RIGHTCLICK}"); |
|
503
|
|
|
|
|
|
|
|
|
504
|
|
|
|
|
|
|
$this->press($x-1,$y ,"{RIGHTCLICK}"); |
|
505
|
|
|
|
|
|
|
$this->press($x+1,$y ,"{RIGHTCLICK}"); |
|
506
|
|
|
|
|
|
|
|
|
507
|
|
|
|
|
|
|
$this->press($x-1,$y+1,"{RIGHTCLICK}"); |
|
508
|
|
|
|
|
|
|
$this->press($x ,$y+1,"{RIGHTCLICK}"); |
|
509
|
|
|
|
|
|
|
$this->press($x+1,$y+1,"{RIGHTCLICK}"); |
|
510
|
|
|
|
|
|
|
|
|
511
|
|
|
|
|
|
|
} |
|
512
|
|
|
|
|
|
|
|
|
513
|
|
|
|
|
|
|
=end deprecated |
|
514
|
|
|
|
|
|
|
|
|
515
|
|
|
|
|
|
|
=head2 game_over |
|
516
|
|
|
|
|
|
|
|
|
517
|
|
|
|
|
|
|
if (my $state = $sweeperbot->game_over) { |
|
518
|
|
|
|
|
|
|
print $state > 0 ? "We won!\n" : "We lost!\n"; |
|
519
|
|
|
|
|
|
|
} |
|
520
|
|
|
|
|
|
|
|
|
521
|
|
|
|
|
|
|
Checks to see if the game is over by looking at the minesweeper smiley. |
|
522
|
|
|
|
|
|
|
Returns C<1> for game over due to a win, C<-1> for game over due to |
|
523
|
|
|
|
|
|
|
a loss, and false if the game has not finished. |
|
524
|
|
|
|
|
|
|
|
|
525
|
|
|
|
|
|
|
=cut |
|
526
|
|
|
|
|
|
|
|
|
527
|
|
|
|
|
|
|
# Is the game over (we hit a mine)? |
|
528
|
|
|
|
|
|
|
# Returns -1 if game is over and we lost, 0 if not over, 1 if over and we won |
|
529
|
|
|
|
|
|
|
sub game_over { |
|
530
|
|
|
|
|
|
|
# Capture game button and determine its sig |
|
531
|
|
|
|
|
|
|
# Game button is always at (x,56). X-value must be determined by |
|
532
|
|
|
|
|
|
|
# calculation using formula: x=w/2-11 |
|
533
|
|
|
|
|
|
|
# Size is 26x26 |
|
534
|
|
|
|
|
|
|
our($l,$t,$w); |
|
535
|
|
|
|
|
|
|
|
|
536
|
|
|
|
|
|
|
# If we don't know where our smiley lives, then go find it. |
|
537
|
|
|
|
|
|
|
if (not $Smiley_offset) { |
|
538
|
|
|
|
|
|
|
for (my $i = MINIMUM_TOP_DRESSING; $i <= MAXIMUM_TOP_DRESSING; $i++) { |
|
539
|
|
|
|
|
|
|
|
|
540
|
|
|
|
|
|
|
$Smiley_offset = $i; |
|
541
|
|
|
|
|
|
|
|
|
542
|
|
|
|
|
|
|
warn "Searching $Smiley_offset pixels down for smiley\n" if DEBUG; |
|
543
|
|
|
|
|
|
|
|
|
544
|
|
|
|
|
|
|
my $smiley = CaptureRect( |
|
545
|
|
|
|
|
|
|
$l+$w/2 - 11, |
|
546
|
|
|
|
|
|
|
$Smiley_offset + $t, |
|
547
|
|
|
|
|
|
|
SMILEY_LENGTH, |
|
548
|
|
|
|
|
|
|
SMILEY_LENGTH, |
|
549
|
|
|
|
|
|
|
); |
|
550
|
|
|
|
|
|
|
|
|
551
|
|
|
|
|
|
|
my $sig = $smiley->Get('signature'); |
|
552
|
|
|
|
|
|
|
|
|
553
|
|
|
|
|
|
|
if (exists $smiley_type{$sig}) { |
|
554
|
|
|
|
|
|
|
return $smiley_type{$sig}; |
|
555
|
|
|
|
|
|
|
} |
|
556
|
|
|
|
|
|
|
} |
|
557
|
|
|
|
|
|
|
|
|
558
|
|
|
|
|
|
|
# Oh no! We couldn't find our smiley! |
|
559
|
|
|
|
|
|
|
|
|
560
|
|
|
|
|
|
|
die "Smiley not found on gameboard!\n"; |
|
561
|
|
|
|
|
|
|
} |
|
562
|
|
|
|
|
|
|
|
|
563
|
|
|
|
|
|
|
# my $smiley=CaptureRect($l+$w/2-11,$t+56,26,26); |
|
564
|
|
|
|
|
|
|
# my $smiley=CaptureRect($l+$w/2-11, $t+64, SMILEY_LENGTH, SMILEY_LENGTH); |
|
565
|
|
|
|
|
|
|
# my $smiley=CaptureRect($l+$w/2-11,$t+75,26,26); |
|
566
|
|
|
|
|
|
|
|
|
567
|
|
|
|
|
|
|
my $smiley = CaptureRect( |
|
568
|
|
|
|
|
|
|
$l+$w/2 - 11, |
|
569
|
|
|
|
|
|
|
$Smiley_offset + $t, |
|
570
|
|
|
|
|
|
|
SMILEY_LENGTH, |
|
571
|
|
|
|
|
|
|
SMILEY_LENGTH, |
|
572
|
|
|
|
|
|
|
); |
|
573
|
|
|
|
|
|
|
|
|
574
|
|
|
|
|
|
|
|
|
575
|
|
|
|
|
|
|
my $sig = $smiley->Get("signature"); |
|
576
|
|
|
|
|
|
|
|
|
577
|
|
|
|
|
|
|
if (exists $smiley_type{$sig}) { |
|
578
|
|
|
|
|
|
|
return $smiley_type{$sig}; |
|
579
|
|
|
|
|
|
|
} |
|
580
|
|
|
|
|
|
|
|
|
581
|
|
|
|
|
|
|
die "I don't know what the smiley means\n$sig\n"; |
|
582
|
|
|
|
|
|
|
|
|
583
|
|
|
|
|
|
|
} |
|
584
|
|
|
|
|
|
|
|
|
585
|
|
|
|
|
|
|
=head2 make_move |
|
586
|
|
|
|
|
|
|
|
|
587
|
|
|
|
|
|
|
|
|
588
|
|
|
|
|
|
|
$sweeperbot->make_move($game_state); |
|
589
|
|
|
|
|
|
|
|
|
590
|
|
|
|
|
|
|
Given a game state, determines the next move(s) that should be made, |
|
591
|
|
|
|
|
|
|
and makes them. By default this uses a very simple process: |
|
592
|
|
|
|
|
|
|
|
|
593
|
|
|
|
|
|
|
=over |
|
594
|
|
|
|
|
|
|
|
|
595
|
|
|
|
|
|
|
=item * |
|
596
|
|
|
|
|
|
|
|
|
597
|
|
|
|
|
|
|
If C is set, then cheat. |
|
598
|
|
|
|
|
|
|
|
|
599
|
|
|
|
|
|
|
=item * |
|
600
|
|
|
|
|
|
|
|
|
601
|
|
|
|
|
|
|
If we find a square where the number of adjacent mines matches the |
|
602
|
|
|
|
|
|
|
number on the square, L on it. |
|
603
|
|
|
|
|
|
|
|
|
604
|
|
|
|
|
|
|
=item * |
|
605
|
|
|
|
|
|
|
|
|
606
|
|
|
|
|
|
|
If the number of adjacent unpressed squares matches the number of |
|
607
|
|
|
|
|
|
|
unknown adjacent mines, then flag them as mines. |
|
608
|
|
|
|
|
|
|
|
|
609
|
|
|
|
|
|
|
=item * |
|
610
|
|
|
|
|
|
|
|
|
611
|
|
|
|
|
|
|
If all else fails, pick a square at random. If C is defined, |
|
612
|
|
|
|
|
|
|
and we would have picked a square with a mine, then pick another. |
|
613
|
|
|
|
|
|
|
|
|
614
|
|
|
|
|
|
|
=back |
|
615
|
|
|
|
|
|
|
|
|
616
|
|
|
|
|
|
|
If you want to inherit from this class to change the AI, overriding |
|
617
|
|
|
|
|
|
|
this method is the place to do it. |
|
618
|
|
|
|
|
|
|
|
|
619
|
|
|
|
|
|
|
=cut |
|
620
|
|
|
|
|
|
|
|
|
621
|
|
|
|
|
|
|
sub make_move { |
|
622
|
|
|
|
|
|
|
my ($this, $game_state) = @_; |
|
623
|
|
|
|
|
|
|
our ($squares_x, $squares_y); |
|
624
|
|
|
|
|
|
|
my $altered_board = 0; |
|
625
|
|
|
|
|
|
|
foreach my $y (1..$squares_y) { |
|
626
|
|
|
|
|
|
|
SQUARE: foreach my $x (1..$squares_x) { |
|
627
|
|
|
|
|
|
|
|
|
628
|
|
|
|
|
|
|
if (UBER_CHEAT) { |
|
629
|
|
|
|
|
|
|
if (cheat_is_square_safe([$x,$y])) { |
|
630
|
|
|
|
|
|
|
$this->press($x,$y); |
|
631
|
|
|
|
|
|
|
} |
|
632
|
|
|
|
|
|
|
else { |
|
633
|
|
|
|
|
|
|
$this->flag_mines($game_state,[$x,$y]); |
|
634
|
|
|
|
|
|
|
} |
|
635
|
|
|
|
|
|
|
$altered_board = 1; |
|
636
|
|
|
|
|
|
|
} |
|
637
|
|
|
|
|
|
|
|
|
638
|
|
|
|
|
|
|
# Empty squares are dull. |
|
639
|
|
|
|
|
|
|
next SQUARE if ($game_state->[$x][$y] eq 0); |
|
640
|
|
|
|
|
|
|
|
|
641
|
|
|
|
|
|
|
# Unpressed/flag squares don't give us any information. |
|
642
|
|
|
|
|
|
|
next SQUARE if (not looks_like_number($game_state->[$x][$y])); |
|
643
|
|
|
|
|
|
|
|
|
644
|
|
|
|
|
|
|
my @adjacent_unpressed = $this->adjacent_unpressed_for($game_state,$x,$y); |
|
645
|
|
|
|
|
|
|
# If there are no adjacent unpressed squares, then |
|
646
|
|
|
|
|
|
|
# this square is boring. |
|
647
|
|
|
|
|
|
|
next SQUARE if not @adjacent_unpressed; |
|
648
|
|
|
|
|
|
|
|
|
649
|
|
|
|
|
|
|
my $adjacent_mines = $this->adjacent_mines_for($game_state,$x,$y); |
|
650
|
|
|
|
|
|
|
|
|
651
|
|
|
|
|
|
|
# If the number of mines is equal to the number |
|
652
|
|
|
|
|
|
|
# on this square, then stomp on it. |
|
653
|
|
|
|
|
|
|
|
|
654
|
|
|
|
|
|
|
if ($adjacent_mines == $game_state->[$x][$y]) { |
|
655
|
|
|
|
|
|
|
print "Stomping on $x,$y\n" if DEBUG; |
|
656
|
|
|
|
|
|
|
$this->stomp($x,$y); |
|
657
|
|
|
|
|
|
|
$altered_board = 1; |
|
658
|
|
|
|
|
|
|
} |
|
659
|
|
|
|
|
|
|
|
|
660
|
|
|
|
|
|
|
# If the number of mines plus unpressed squares is |
|
661
|
|
|
|
|
|
|
# equal to the number on this square, then mark all |
|
662
|
|
|
|
|
|
|
# adjacent squares as having mines. |
|
663
|
|
|
|
|
|
|
if ($adjacent_mines + @adjacent_unpressed == $game_state->[$x][$y]) { |
|
664
|
|
|
|
|
|
|
print "Marking mines next to $x,$y\n" if DEBUG; |
|
665
|
|
|
|
|
|
|
$this->flag_mines($game_state,@adjacent_unpressed); |
|
666
|
|
|
|
|
|
|
$altered_board = 1; |
|
667
|
|
|
|
|
|
|
} |
|
668
|
|
|
|
|
|
|
|
|
669
|
|
|
|
|
|
|
} |
|
670
|
|
|
|
|
|
|
} |
|
671
|
|
|
|
|
|
|
if (not $altered_board) { |
|
672
|
|
|
|
|
|
|
# Drat! Can't find a good move. Pick a square at |
|
673
|
|
|
|
|
|
|
# random. |
|
674
|
|
|
|
|
|
|
|
|
675
|
|
|
|
|
|
|
my @unpressed = (); |
|
676
|
|
|
|
|
|
|
|
|
677
|
|
|
|
|
|
|
foreach my $x (1..$squares_x) { |
|
678
|
|
|
|
|
|
|
foreach my $y (1..$squares_y) { |
|
679
|
|
|
|
|
|
|
push(@unpressed,[$x,$y]) if $game_state->[$x][$y] eq "unpressed"; |
|
680
|
|
|
|
|
|
|
} |
|
681
|
|
|
|
|
|
|
} |
|
682
|
|
|
|
|
|
|
|
|
683
|
|
|
|
|
|
|
my $square = $unpressed[rand @unpressed]; |
|
684
|
|
|
|
|
|
|
|
|
685
|
|
|
|
|
|
|
if (CHEAT) { |
|
686
|
|
|
|
|
|
|
while (not $this->cheat_is_square_safe($square)) { |
|
687
|
|
|
|
|
|
|
$square = $unpressed[rand @unpressed]; |
|
688
|
|
|
|
|
|
|
} |
|
689
|
|
|
|
|
|
|
} |
|
690
|
|
|
|
|
|
|
|
|
691
|
|
|
|
|
|
|
print "Guessing square ",join(",",@$square),"\n" if DEBUG; |
|
692
|
|
|
|
|
|
|
$this->press(@$square); |
|
693
|
|
|
|
|
|
|
|
|
694
|
|
|
|
|
|
|
} |
|
695
|
|
|
|
|
|
|
return; |
|
696
|
|
|
|
|
|
|
} |
|
697
|
|
|
|
|
|
|
|
|
698
|
|
|
|
|
|
|
=head2 capture_game_state |
|
699
|
|
|
|
|
|
|
|
|
700
|
|
|
|
|
|
|
my $game_state = $sweeperbot->capture_game_state; |
|
701
|
|
|
|
|
|
|
|
|
702
|
|
|
|
|
|
|
Walks over the entire board, capturing the value in each location and |
|
703
|
|
|
|
|
|
|
adding it to an array-of-arrays (game-state) structure. The value |
|
704
|
|
|
|
|
|
|
in a particular square can be accessed with: |
|
705
|
|
|
|
|
|
|
|
|
706
|
|
|
|
|
|
|
$value = $game_state->[$x][$y]; |
|
707
|
|
|
|
|
|
|
|
|
708
|
|
|
|
|
|
|
Where (1,1) is considered the top-left of the game board. |
|
709
|
|
|
|
|
|
|
|
|
710
|
|
|
|
|
|
|
=cut |
|
711
|
|
|
|
|
|
|
|
|
712
|
|
|
|
|
|
|
sub capture_game_state { |
|
713
|
|
|
|
|
|
|
|
|
714
|
|
|
|
|
|
|
my ($this) = @_; |
|
715
|
|
|
|
|
|
|
|
|
716
|
|
|
|
|
|
|
my $game_state = []; |
|
717
|
|
|
|
|
|
|
our ($squares_x, $squares_y); |
|
718
|
|
|
|
|
|
|
|
|
719
|
|
|
|
|
|
|
for my $y (1..$squares_y) { |
|
720
|
|
|
|
|
|
|
for my $x (1..$squares_x) { |
|
721
|
|
|
|
|
|
|
my $square_value = $this->value($x,$y); |
|
722
|
|
|
|
|
|
|
$game_state->[$x][$y] = $square_value; |
|
723
|
|
|
|
|
|
|
print $char_for{$square_value} if DEBUG; |
|
724
|
|
|
|
|
|
|
} |
|
725
|
|
|
|
|
|
|
print "\n" if DEBUG; |
|
726
|
|
|
|
|
|
|
} |
|
727
|
|
|
|
|
|
|
print "---------------\n" if DEBUG; |
|
728
|
|
|
|
|
|
|
|
|
729
|
|
|
|
|
|
|
# To make things easier later on, we provide a one square "padding" |
|
730
|
|
|
|
|
|
|
# of virtual squares that are always empty. |
|
731
|
|
|
|
|
|
|
|
|
732
|
|
|
|
|
|
|
for my $x (0..$squares_x+1) { |
|
733
|
|
|
|
|
|
|
$game_state->[$x][0] = 0; |
|
734
|
|
|
|
|
|
|
$game_state->[$x][$squares_y+1] = 0; |
|
735
|
|
|
|
|
|
|
} |
|
736
|
|
|
|
|
|
|
|
|
737
|
|
|
|
|
|
|
for my $y (0..$squares_y+1) { |
|
738
|
|
|
|
|
|
|
$game_state->[0][$y] = 0; |
|
739
|
|
|
|
|
|
|
$game_state->[$squares_x+1][$y] = 0; |
|
740
|
|
|
|
|
|
|
} |
|
741
|
|
|
|
|
|
|
|
|
742
|
|
|
|
|
|
|
return $game_state; |
|
743
|
|
|
|
|
|
|
} |
|
744
|
|
|
|
|
|
|
|
|
745
|
|
|
|
|
|
|
=head2 adjacent_mines_for |
|
746
|
|
|
|
|
|
|
|
|
747
|
|
|
|
|
|
|
my $mines = $sweeperbot->adjacent_mines_for($game_state, $x, $y); |
|
748
|
|
|
|
|
|
|
|
|
749
|
|
|
|
|
|
|
Examines all the squares adjacent to ($x,$y) and returns an |
|
750
|
|
|
|
|
|
|
array-ref of tuples for those that have already been flagged |
|
751
|
|
|
|
|
|
|
as a mine. |
|
752
|
|
|
|
|
|
|
|
|
753
|
|
|
|
|
|
|
=cut |
|
754
|
|
|
|
|
|
|
|
|
755
|
|
|
|
|
|
|
sub adjacent_mines_for { |
|
756
|
|
|
|
|
|
|
my ($this, $game_state, $x, $y) = @_; |
|
757
|
|
|
|
|
|
|
return $this->mines_at($game_state, |
|
758
|
|
|
|
|
|
|
[$x-1, $y-1], [$x, $y-1], [$x+1, $y-1], |
|
759
|
|
|
|
|
|
|
[$x-1, $y ], [$x+1, $y ], |
|
760
|
|
|
|
|
|
|
[$x-1, $y+1], [$x, $y+1], [$x+1, $y+1], |
|
761
|
|
|
|
|
|
|
); |
|
762
|
|
|
|
|
|
|
} |
|
763
|
|
|
|
|
|
|
|
|
764
|
|
|
|
|
|
|
=head2 adjacent_unpressed_for |
|
765
|
|
|
|
|
|
|
|
|
766
|
|
|
|
|
|
|
my $squares = $sweeperbot->adjacent_unpressed_for($game_state, $x, $y); |
|
767
|
|
|
|
|
|
|
|
|
768
|
|
|
|
|
|
|
Examines all the squares adjacent to ($x,$y) and returns an array-ref |
|
769
|
|
|
|
|
|
|
of tuples for those that have not been pressed (and not flagged as a |
|
770
|
|
|
|
|
|
|
mine). |
|
771
|
|
|
|
|
|
|
|
|
772
|
|
|
|
|
|
|
=cut |
|
773
|
|
|
|
|
|
|
|
|
774
|
|
|
|
|
|
|
sub adjacent_unpressed_for { |
|
775
|
|
|
|
|
|
|
my ($this, $game_state, $x, $y) = @_; |
|
776
|
|
|
|
|
|
|
return $this->unpressed_list($game_state, |
|
777
|
|
|
|
|
|
|
[$x-1, $y-1], [$x, $y-1], [$x+1, $y-1], |
|
778
|
|
|
|
|
|
|
[$x-1, $y ], [$x+1, $y ], |
|
779
|
|
|
|
|
|
|
[$x-1, $y+1], [$x, $y+1], [$x+1, $y+1], |
|
780
|
|
|
|
|
|
|
); |
|
781
|
|
|
|
|
|
|
} |
|
782
|
|
|
|
|
|
|
|
|
783
|
|
|
|
|
|
|
=head2 mines_at |
|
784
|
|
|
|
|
|
|
|
|
785
|
|
|
|
|
|
|
my $mines = $sweeperbot->mines_at($game_state, @locations); |
|
786
|
|
|
|
|
|
|
|
|
787
|
|
|
|
|
|
|
Takes a game state and a list of locations, and returns an array-ref |
|
788
|
|
|
|
|
|
|
containing those locations from the list that have been flagged as |
|
789
|
|
|
|
|
|
|
a mine. |
|
790
|
|
|
|
|
|
|
|
|
791
|
|
|
|
|
|
|
=cut |
|
792
|
|
|
|
|
|
|
|
|
793
|
|
|
|
|
|
|
|
|
794
|
|
|
|
|
|
|
sub mines_at { |
|
795
|
|
|
|
|
|
|
my ($this, $game_state, @locations) = @_; |
|
796
|
|
|
|
|
|
|
|
|
797
|
|
|
|
|
|
|
my $mines = 0; |
|
798
|
|
|
|
|
|
|
|
|
799
|
|
|
|
|
|
|
foreach my $square (@locations) { |
|
800
|
|
|
|
|
|
|
if ($game_state->[ $square->[0] ][ $square->[1] ] eq "flag") { |
|
801
|
|
|
|
|
|
|
$mines++; |
|
802
|
|
|
|
|
|
|
} |
|
803
|
|
|
|
|
|
|
} |
|
804
|
|
|
|
|
|
|
return $mines; |
|
805
|
|
|
|
|
|
|
} |
|
806
|
|
|
|
|
|
|
|
|
807
|
|
|
|
|
|
|
=head2 unpressed_list |
|
808
|
|
|
|
|
|
|
|
|
809
|
|
|
|
|
|
|
my $unpressed = $this->unpressed-list($game_state, @locations); |
|
810
|
|
|
|
|
|
|
|
|
811
|
|
|
|
|
|
|
Identical to L above, but returns any locations that have |
|
812
|
|
|
|
|
|
|
not been pressed (and not flagged as a mine). |
|
813
|
|
|
|
|
|
|
|
|
814
|
|
|
|
|
|
|
=cut |
|
815
|
|
|
|
|
|
|
|
|
816
|
|
|
|
|
|
|
sub unpressed_list { |
|
817
|
|
|
|
|
|
|
my ($this, $game_state, @locations) = @_; |
|
818
|
|
|
|
|
|
|
|
|
819
|
|
|
|
|
|
|
my @unpressed = grep { ($game_state->[ $_->[0] ][ $_->[1] ] eq "unpressed") } @locations; |
|
820
|
|
|
|
|
|
|
|
|
821
|
|
|
|
|
|
|
return @unpressed; |
|
822
|
|
|
|
|
|
|
} |
|
823
|
|
|
|
|
|
|
|
|
824
|
|
|
|
|
|
|
=head2 enable_cheats |
|
825
|
|
|
|
|
|
|
|
|
826
|
|
|
|
|
|
|
$sweeperbot->enable_cheats; |
|
827
|
|
|
|
|
|
|
|
|
828
|
|
|
|
|
|
|
Sends the magic C cheat to minesweeper, which allows us to |
|
829
|
|
|
|
|
|
|
determine the contents of a square by examining the top-left pixel |
|
830
|
|
|
|
|
|
|
of the entire display. |
|
831
|
|
|
|
|
|
|
|
|
832
|
|
|
|
|
|
|
For this cheat to be used in the default AI, the C constant |
|
833
|
|
|
|
|
|
|
must be set to a true value in the C source. |
|
834
|
|
|
|
|
|
|
|
|
835
|
|
|
|
|
|
|
=cut |
|
836
|
|
|
|
|
|
|
|
|
837
|
|
|
|
|
|
|
sub enable_cheats { |
|
838
|
|
|
|
|
|
|
SendKeys("xyzzy{ENTER}+ "); |
|
839
|
|
|
|
|
|
|
|
|
840
|
|
|
|
|
|
|
return; |
|
841
|
|
|
|
|
|
|
} |
|
842
|
|
|
|
|
|
|
|
|
843
|
|
|
|
|
|
|
=head2 cheat_is_square_safe |
|
844
|
|
|
|
|
|
|
|
|
845
|
|
|
|
|
|
|
if ($sweeperbot->cheat_is_square_safe($x,$y) { |
|
846
|
|
|
|
|
|
|
print "($x,$y) looks safe!\n"; |
|
847
|
|
|
|
|
|
|
} else { |
|
848
|
|
|
|
|
|
|
print "($x,$y) has a mine underneath.\n"; |
|
849
|
|
|
|
|
|
|
} |
|
850
|
|
|
|
|
|
|
|
|
851
|
|
|
|
|
|
|
If cheats are enabled, returns true if the given square looks |
|
852
|
|
|
|
|
|
|
safe to step on, or false if it appears to contain a mine. |
|
853
|
|
|
|
|
|
|
|
|
854
|
|
|
|
|
|
|
Note that especially on fast, multi-core systems, it's possible |
|
855
|
|
|
|
|
|
|
for this to move the mouse and capture the required pixel before |
|
856
|
|
|
|
|
|
|
minesweeper has had a chance to update it. So if you cheat, |
|
857
|
|
|
|
|
|
|
you may sometimes be surprised. |
|
858
|
|
|
|
|
|
|
|
|
859
|
|
|
|
|
|
|
=cut |
|
860
|
|
|
|
|
|
|
|
|
861
|
|
|
|
|
|
|
sub cheat_is_square_safe { |
|
862
|
|
|
|
|
|
|
my ($this, $square) = @_; |
|
863
|
|
|
|
|
|
|
our($l,$t); |
|
864
|
|
|
|
|
|
|
|
|
865
|
|
|
|
|
|
|
MouseMoveAbsPix( |
|
866
|
|
|
|
|
|
|
$l+SQUARE1X+($square->[0]-1)*SQUARE_W+SQUARE_W/2, |
|
867
|
|
|
|
|
|
|
$t+$Square1Y+($square->[1]-1)*SQUARE_H+SQUARE_W/2, |
|
868
|
|
|
|
|
|
|
); |
|
869
|
|
|
|
|
|
|
|
|
870
|
|
|
|
|
|
|
# Capture our pixel. |
|
871
|
|
|
|
|
|
|
my $pixel = CaptureRect(0,0,1,1); |
|
872
|
|
|
|
|
|
|
|
|
873
|
|
|
|
|
|
|
my $signature = $pixel->Get("signature"); |
|
874
|
|
|
|
|
|
|
|
|
875
|
|
|
|
|
|
|
print "Square at @$square has sig of $signature\n" if DEBUG; |
|
876
|
|
|
|
|
|
|
|
|
877
|
|
|
|
|
|
|
if ($signature eq CHEAT_SAFE) { |
|
878
|
|
|
|
|
|
|
print "This square (@$square) looks safe\n" if DEBUG; |
|
879
|
|
|
|
|
|
|
return 1; |
|
880
|
|
|
|
|
|
|
} elsif ($signature eq CHEAT_UNSAFE) { |
|
881
|
|
|
|
|
|
|
print "This square (@$square) looks dangerous!\n" if DEBUG; |
|
882
|
|
|
|
|
|
|
return; |
|
883
|
|
|
|
|
|
|
} |
|
884
|
|
|
|
|
|
|
die "Square @$square has unknown cheat-signature\n$signature\n"; |
|
885
|
|
|
|
|
|
|
} |
|
886
|
|
|
|
|
|
|
|
|
887
|
|
|
|
|
|
|
__END__ |