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__ |