File Coverage

blib/lib/App/SweeperBot.pm
Criterion Covered Total %
statement 19 21 90.4
branch n/a
condition n/a
subroutine 7 7 100.0
pod n/a
total 26 28 92.8


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__