File Coverage

blib/lib/Games/AlphaBeta.pm
Criterion Covered Total %
statement 74 76 97.3
branch 30 46 65.2
condition n/a
subroutine 9 9 100.0
pod 2 2 100.0
total 115 133 86.4


line stmt bran cond sub pod time code
1             package Games::AlphaBeta;
2 2     2   46102 use base qw(Games::Sequential);
  2         7  
  2         958  
3              
4 2     2   10 use Carp;
  2         5  
  2         109  
5 2     2   31 use 5.006001;
  2         6  
  2         59  
6              
7 2     2   10 use strict;
  2         4  
  2         49  
8 2     2   8 use warnings;
  2         11  
  2         1515  
9              
10              
11             our $VERSION = '0.4.7';
12              
13             =head1 NAME
14              
15             Games::AlphaBeta - game-tree search with object oriented interface
16              
17             =head1 SYNOPSIS
18              
19             package My::GamePos;
20             use base qw(Games::AlphaBeta::Position);
21              
22             # initialise starting position
23             sub _init { ... }
24              
25             # Methods required by Games::AlphaBeta
26             sub apply { ... }
27             sub endpos { ... } # optional
28             sub evaluate { ... }
29             sub findmoves { ... }
30              
31             # Draw a position in the game (optional)
32             sub draw { ... }
33              
34             package main;
35             my $pos = My::GamePos->new;
36             my $game = Games::AlphaBeta->new($pos);
37              
38             while ($game->abmove) {
39             print draw($game->peek_pos);
40             }
41              
42             =head1 DESCRIPTION
43              
44             Games::AlphaBeta provides a generic implementation of the
45             AlphaBeta game-tree search algorithm (also known as MiniMax
46             search with alpha beta pruning). This algorithm can be used to
47             find the best move at a particular position in any two-player,
48             zero-sum game with perfect information. Examples of such games
49             include Chess, Othello, Connect4, Go, Tic-Tac-Toe and many, many
50             other boardgames.
51              
52             Users must pass an object representing the initial state of the
53             game as the first argument to C. This object must provide
54             the following methods: C, C, C,
55             C and C. This is explained more
56             carefully in L which is a base class
57             you can use to implement your position object.
58              
59             =head1 INHERITED METHODS
60              
61             The following methods are inherited from L:
62              
63             =over
64              
65             =item new
66              
67             =item debug
68              
69             =item peek_pos
70              
71             =item peek_move
72              
73             =item move
74              
75             =item undo
76              
77             =back
78              
79             =head1 METHODS
80              
81             =over
82              
83             =item _init [@list]
84              
85             I
86              
87             Initialize an AlphaBeta object.
88              
89             =cut
90              
91             sub _init {
92 2     2   25 my $self = shift;
93 2         15 my %config = (
94             # Runtime variables
95             ply => 2, # default search depth
96             alpha => -100_000,
97             beta => 100_000,
98             );
99              
100 2         18 @$self{keys %config} = values %config;
101 2         23 $self->SUPER::_init(@_);
102              
103 2         18 my $pos = $self->peek_pos;
104 2 50       21 croak "no endpos() method defined" unless $pos->can("endpos");
105 2 50       10 croak "no evaluate() method defined" unless $pos->can("evaluate");
106 2 50       12 croak "no findmoves() method defined" unless $pos->can("findmoves");
107              
108 2         10 return $self;
109             }
110              
111              
112             =item ply [$value]
113              
114             Return current default search depth and, if invoked with an
115             argument, set to new value.
116              
117             =cut
118              
119             sub ply {
120 1     1 1 2 my $self = shift;
121 1         3 my $prev = $self->{ply};
122 1 50       4 $self->{ply} = shift if @_;
123 1         5 return $prev;
124             }
125              
126              
127             =item abmove [$ply]
128              
129             Perform the best move found after an AlphaBeta game-tree search
130             to depth $ply. If $ply is not specified, the default depth is
131             used (see C). The best move found is performed and a
132             reference to the resulting position is returned on success, and
133             undef is returned on failure.
134              
135             Note that this function can take a long time if $ply is high,
136             particularly if the game in question has many possible moves at
137             each position.
138              
139             If C is set, some basic debugging is printed as the
140             search progresses.
141              
142             =cut
143              
144             sub abmove {
145 64     64 1 82810 my $self = shift;
146 64         106 my $ply;
147              
148 64 100       183 if (@_) {
149 1         9 $ply = shift;
150 1 50       5 print "Explicit ply $ply overrides default ($self->{ply})\n" if $self->{debug};
151             }
152             else {
153 63         172 $ply = $self->{ply};
154             }
155              
156 64         87 my (@moves, $bestmove);
157 64         80 my $bestmove_valid = 0;
158 64         190 my $pos = $self->peek_pos;
159              
160 64 50       251 return if $pos->endpos;
161 64 100       218 return unless @moves = $pos->findmoves;
162              
163 63         161 my $alpha = $self->{alpha};
164 63         102 my $beta = $self->{beta};
165              
166 63 50       182 print "Searching to depth $ply\n" if $self->{debug};
167 63         168 $self->{found_end} = $self->{count} = 0;
168 63         102 for my $move (@moves) {
169 499         726 my ($npos, $sc);
170 499         2166 $npos = $pos->copy;
171 499 50       2088 $npos->apply($move) or croak "apply() failed";
172 499         1699 $sc = -$self->_alphabeta($npos, -$beta, -$alpha, $ply - 1);
173              
174 499 50       2092 print "ab val: $sc" if $self->{debug};
175 499 100       1203 if ($sc > $alpha) {
176 125 50       294 print " > $alpha new best move" if $self->{debug};
177 125         162 $bestmove_valid = 1;
178 125         174 $bestmove = $move;
179 125         159 $alpha = $sc;
180             }
181 499 50       6081 print "\n" if $self->{debug};
182             }
183 63 50       237 print "$self->{count} visited\n" if $self->{debug};
184              
185 63 50       121 return unless $bestmove_valid;
186 63         410 return $self->move($bestmove);
187             }
188              
189              
190             =item _alphabeta $pos $alpha $beta $ply
191              
192             I
193              
194             =cut
195              
196             sub _alphabeta {
197 2878     2878   4370 my ($self, $pos, $alpha, $beta, $ply) = @_;
198 2878         2909 my @moves;
199              
200             # Keep count of the number of positions we've seen
201 2878         4819 $self->{count}++;
202              
203             # When using iterative deepening we can optimise for the case
204             # when we find an end position at every branch (for example,
205             # near the end of the game)
206             #
207 2878 50       11076 if ($pos->endpos) {
    100          
208 0         0 $self->{found_end}++;
209 0         0 return $pos->evaluate;
210             }
211             elsif ($ply <= 0) {
212 2331         7049 return $pos->evaluate;
213             }
214              
215 547 100       2170 unless (@moves = $pos->findmoves) {
216 1         2 $self->{found_end}++;
217 1         4 return $pos->evaluate;
218             }
219              
220 546         1569 for my $move (@moves) {
221 2379         3035 my ($npos, $sc);
222 2379 50       9895 $npos = $pos->copy or croak "$pos->copy() failed";
223 2379 50       8764 $npos->apply($move) or croak "$pos->apply() failed";
224              
225 2379         8581 $sc = -$self->_alphabeta($npos, -$beta, -$alpha, $ply - 1);
226              
227 2379 100       6437 $alpha = $sc if $sc > $alpha;
228 2379 100       21643 last unless $alpha < $beta;
229             }
230              
231 546         3577 return $alpha;
232             }
233              
234              
235             1; # ensure using this module works
236             __END__