File Coverage

blib/lib/Games/Alak.pm
Criterion Covered Total %
statement 18 164 10.9
branch 0 98 0.0
condition 0 36 0.0
subroutine 6 15 40.0
pod 0 7 0.0
total 24 320 7.5


line stmt bran cond sub pod time code
1             #!/usr/bin/perl
2             require 5;
3             package Games::Alak;
4 3     3   14375 use strict;
  3         6  
  3         134  
5 3     3   16 use vars qw($Tree $Term $Max_depth $VERSION);
  3         7  
  3         299  
6             $VERSION = '0.19';
7             # BEGIN {$^W = 1}; # warnings on
8              
9 3     3   17 use constant BOARD_SIZE => 11;
  3         10  
  3         244  
10 3     3   15 use constant ENDGAME => 1000;
  3         6  
  3         341  
11             die( "Board_size " , BOARD_SIZE, " is too small!") if BOARD_SIZE < 9;
12              
13 3         662 use constant NEW_BOARD_STRING =>
14 3     3   14 ('xxxx' . ('.' x (BOARD_SIZE - 8)) . 'oooo');
  3         5  
15              
16             #--------------------------------------------------------------------------
17              
18             sub play {
19 0     0 0   my($x_best_move, @o_move_chosen, $from, $to);
20            
21 0           $Max_depth = 3; # must be an integer > 0
22 0           $Tree = _new_node(NEW_BOARD_STRING, 'x', -1,-10,0);
23            
24 3     3   3496 use Term::ReadLine;
  3         12091  
  3         11123  
25 0           $Term = Term::ReadLine->new('Alak');
26 0           my $out = $Term->OUT;
27 0 0         select($out) if $out;
28              
29 0           print "Lookahead set to $Max_depth. I am X, you are O.\n";
30 0           print "Enter h for help\n";
31            
32             Main_loop:
33 0           while(1) {
34 0           grow($Tree);
35 0           $x_best_move = optimal_move($Tree, 'x');
36              
37 0 0         die "No X move possible?!" unless $x_best_move;
38 0           $Tree = $x_best_move; # select that node
39              
40 0           printf "X moves from %s to %s, yielding %s\n",
41             1 + $Tree->{'last_move_from'},
42             1 + $Tree->{'last_move_to'},
43             $Tree->{'board'};
44            
45 0 0         if($Tree->{'endgame'}) {
46 0           print "Endgame. X wins after ",
47             $Tree->{'move_count'}, " moves.\n";
48 0           last;
49             }
50 0 0         grow($Tree) unless @{$Tree->{'successors'}};
  0            
51              
52 0           Get_move:
53             {
54 0           ($from, $to) = prompt_for_next_move();
55 0           --$from; # we index from 0, not 1
56 0           --$to;
57            
58             # check legality
59 0           @o_move_chosen =
60             grep $_->{'last_move_from'} eq $from &&
61             $_->{'last_move_to'} eq $to,
62 0   0       @{$Tree->{'successors'}};
63 0 0         if(@o_move_chosen > 1) {
    0          
64 0           die "PANIC!? ", $Tree->{'board'};
65             } elsif(@o_move_chosen == 0) {
66 0           print "Invalid move!\n";
67 0           redo Get_move;
68             } else {
69             # That move designates just one successor
70 0           $Tree = $o_move_chosen[0]; # select that node.
71             }
72             }
73              
74 0           printf "O moves from %s to %s, yielding %s\n",
75             1 + $Tree->{'last_move_from'},
76             1 + $Tree->{'last_move_to'},
77             $Tree->{'board'};
78              
79 0 0         if($Tree->{'endgame'}) {
80 0           print "Endgame. O wins after ",
81             $Tree->{'move_count'}, " moves.\n";
82 0           last;
83             }
84             }
85             }
86              
87             #--------------------------------------------------------------------------
88              
89             sub prompt_for_next_move { # prompting
90 0     0 0   my $line;
91 0           while(defined($line = $Term->readline('alak>'))) {
92 0           $line =~ s/^\s+//s;
93 0           $line =~ s/\s+$//s;
94 0 0         next unless length($line);
95 0           $Term->addhistory($line);
96              
97             # Knuckle-headed command parsing:
98              
99 0 0 0       if($line =~ m/^q/s) {
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
100 0           last; # quit
101             } elsif($line =~ m/^(\d+)\s*to\s*(\d+)$/s) {
102 0           return($1, $2);
103             } elsif($line =~ m/^g(?:row)?$/s) {
104 0           grow($Tree);
105 0           print "Tree grown.\n";
106             } elsif($line eq 'reset') {
107 0           $Tree = _new_node(NEW_BOARD_STRING, 'o', -1,-1,0,0);
108 0           grow($Tree);
109 0           print "Board reset to ", NEW_BOARD_STRING, "\nYour move.\n";
110             } elsif($line =~ m/^reset\s+([.ox]+)/s) {
111 0           my $board = $1;
112 0 0         if(length($board) != BOARD_SIZE) {
    0          
    0          
    0          
113 0           print "But a board has to be ", BOARD_SIZE, " wide, not ",
114             length($board), "\n";
115             } elsif($board !~ m/\./s) {
116 0           print "But there's no spaces on board $board\n";
117             } elsif(($board =~ tr/x//) < 2) {
118 0           print "But there's fewer than two x's in board $board\n";
119             } elsif(($board =~ tr/o//) < 2) {
120 0           print "But there's fewer than two o's in board $board\n";
121             } else {
122 0           $Tree = _new_node($board, 'o',-1,-1,0,0);
123 0           grow($Tree);
124 0           print "Board reset to $board\nYour move.\n";
125             }
126             } elsif($line =~ m/^d(?:ump)?\s*(\d+)?$/s) {
127 0 0         dump_tree($Tree, defined($1) ? ($1 + 1) : undef);
128             } elsif($line eq 'advise' or $line eq 'advice') {
129 0           my $m = optimal_move($Tree,'o');
130 0           printf "Try %d to %d.\n",
131             1 + $m->{'last_move_from'},
132             1 + $m->{'last_move_to'},
133             ;
134             } elsif($line =~ m/l(?:ookahead)?\s+([1-9]+)$/s) {
135 0           $Max_depth = $1;
136 0           print "Lookahead set to $1.\n";
137             } elsif($line =~ m/^h/s) {
138 0           print
139             "Commands:\n",
140             " q -- quit\n",
141             " dump N -- dump game tree to depth N.\n",
142             " lookahead N -- set tree-deepening depth to N.\n",
143             " advise -- have me suggest a move.\n",
144             " reset -- start anew.\n",
145             " reset xxx.o.x..oo -- start anew from the board specified.\n",
146             " N to N -- move piece from N to N.\n",
147             " h -- help (this message)\n",
148             "\n",
149             } else {
150 0           print "Unknown command. Enter h for help.\n";
151             }
152             }
153              
154             # Either we got undef back, or lasted out from 'q'
155 0           print "Quitting.\n";
156 0           exit;
157             }
158            
159             #--------------------------------------------------------------------------
160              
161             sub _new_node {
162             return
163             {
164 0     0     'board' => $_[0],
165             'whose_turn' => $_[1],
166             'last_move_from' => $_[2],
167             'last_move_to' => $_[3],
168             'last_move_payoff' => $_[4],
169             # payoff to x, that is.
170             'move_count' => $_[5],
171             'successors' => [],
172             };
173             }
174              
175             #--------------------------------------------------------------------------
176              
177             sub grow {
178 0     0 0   my $n = $_[0];
179 0   0       my $depth = $_[1] || 0;
180 0           figure_successors($n)
181             unless
182             $depth >= $Max_depth
183 0 0 0       or @{$n->{'successors'}}
      0        
184             or $n->{'endgame'};
185            
186 0 0         if(@{$n->{'successors'}}) {
  0            
187 0           my $a_payoff_sum = 0;
188 0           foreach my $s (@{$n->{'successors'}}) {
  0            
189 0           grow($s, $depth + 1); # RECURSE
190 0           $a_payoff_sum += $s->{'average_payoff'};
191             }
192 0           $n->{'average_payoff'} =
193 0           $a_payoff_sum / @{$n->{'successors'}};
194             } else {
195 0           $n->{'average_payoff'} = $n->{'last_move_payoff'};
196             }
197             }
198              
199             #--------------------------------------------------------------------------
200              
201             sub optimal_move {
202 0     0 0   my($board, $mover) = @_;
203             # given a board (node), return the successors that are the
204             # best for the mover.
205             # (in scalar context, randomly choose from
206             # the best ones, if there's a tie for first place)
207              
208 0           my @best_cases;
209 0           foreach my $c (@{$board->{'successors'}}) {
  0            
210 0           my $this_payoff = $c->{'average_payoff'};
211 0 0 0       if(!@best_cases # nothing seen yet
    0          
    0          
212             or $best_cases[0]{'average_payoff'} == $this_payoff
213             # tie for first place so far
214             ) {
215 0           push @best_cases, $c;
216             } elsif(
217             $mover eq 'x' # does 'best' mean HIGH payoff?
218             ? ($this_payoff > $best_cases[0]{'average_payoff'}) # max!
219             : ($this_payoff < $best_cases[0]{'average_payoff'}) # min!
220             ) {
221 0           @best_cases = ($c);
222             }
223             # otherwise what's there is not as good as what we've got
224             }
225 0 0         return $best_cases[0] if @best_cases == 1; # no tie
226 0 0         return $best_cases[rand @best_cases] if @best_cases;
227 0           return undef; # shouldn't ever happen!
228             }
229              
230              
231             #--------------------------------------------------------------------------
232              
233             {
234             my($depth, $census, $max_depth_seen, $show_to_depth);
235            
236             sub dump_tree {
237             # wrapper around _dump_recursor
238 0     0 0   my $starting_node;
239 0           ($starting_node, $show_to_depth) = @_;
240             # initialize things
241 0           $depth = $census = $max_depth_seen = 0;
242 0           _dump_recursor($starting_node);
243 0   0       printf "%d in tree of depth %d (branching factor %.2f)\n",
244             $census, $max_depth_seen,
245             $census ** (1/($max_depth_seen || 1)),
246             ;
247             }
248            
249             sub _dump_recursor {
250 0     0     my $n = $_[0];
251            
252 0           ++$census;
253 0 0         $max_depth_seen = $depth if $depth > $max_depth_seen;
254 0 0 0       printf " %s%s %s %2s to %2s %s %s %s\n",
    0          
    0          
255             ' : ' x $depth, # indenting
256             $n->{'board'},
257             $n->{'whose_turn'} eq 'o' ? 'x' : 'o',
258             # Count places on the board starting at 1:
259             1 + $n->{'last_move_from'},
260             1 + $n->{'last_move_to'},
261             omit_if_zero('Immediate score = ', $n->{'last_move_payoff'}),
262             omit_if_zero('Avg score = ', $n->{'average_payoff'}),
263             $n->{'endgame'} ? 'endgame' : '',
264             unless defined($show_to_depth) and $show_to_depth <= $depth;
265            
266 0 0         if(@{$n->{'successors'}}) {
  0            
267 0           ++$depth;
268 0           foreach my $s (@{$n->{'successors'}}) {
  0            
269 0           _dump_recursor($s);
270             }
271 0           --$depth;
272             }
273            
274 0           return;
275             }
276             }
277              
278             sub omit_if_zero {
279 0 0   0 0   return '' unless $_[1];
280 0           return join(' ', $_[0], substr($_[1],0,5));
281             }
282              
283             #--------------------------------------------------------------------------
284              
285             sub figure_successors { # ...of a given node
286 0 0   0 0   die "I need a board!" unless ref $_[0] eq 'HASH';
287 0           my $node = $_[0];
288 0 0 0       return if $node->{'endgame'}
      0        
289             or $node->{'board'} =~ tr/x// < 2
290             or $node->{'board'} =~ tr/o// < 2;
291              
292 0           my $board = $node->{'board'};
293 0           my $mover = $node->{'whose_turn'};
294 0           my $other;
295 0 0         if($mover eq 'x') { $other = 'o' }
  0 0          
296 0           elsif ($mover eq 'o') { $other = 'x' }
  0            
297             else {die "Mover \"$mover\" is neither x nor o!"; }
298              
299 0           my $successors = $node->{'successors'};
300 0 0         die "I already figured successors for this!?" if @$successors;
301              
302 0   0       my $this_move_count = 1 + ($node->{'move_count'} || 0);
303              
304 0           foreach(my $i = 0; $i < BOARD_SIZE; $i++) {
305 0 0         next unless substr($board,$i,1) eq $mover;
306              
307             # Find the first blanks to the left and
308             # to the right of the current piece
309 0           foreach my $to (
310             rindex($board,'.',$i-1),
311             index($board,'.',$i+1),
312             ) {
313 0 0         next if $to == -1;
314             # if no move possible in this direction
315              
316 0           my $new_board = $board;
317 0           substr($new_board, $i, 1) = '.'; # move from...
318 0           substr($new_board, $to,1) = $mover; # ...to
319              
320 0           my $payoff = 0;
321              
322             # Now see if a move from $i to $to deletes nonmover's pieces!
323             # Look for mover's piece and other pieces in the part of
324             # the board that's to my left, and my right.
325             # (This is the only really scary code in this program. Honest!)
326 0 0 0       $payoff += length $1 # look to the left
    0          
327             if $to > 1 and
328             $mover eq 'o'
329 0           ? substr($new_board,0,$to) =~ s/o(x+)$/'o' . ('.' x length $1)/se
330 0           : substr($new_board,0,$to) =~ s/x(o+)$/'x' . ('.' x length $1)/se;
331 0 0 0       $payoff += length $1 # look to the right
    0          
332             if $to < BOARD_SIZE - 2 and
333             $mover eq 'o'
334 0           ? substr($new_board,$to+1) =~ s/^(x+)o/('.' x length $1) . 'o'/se
335 0           : substr($new_board,$to+1) =~ s/^(o+)x/('.' x length $1) . 'x'/se;
336              
337             # Exaggerate payoff if $mover wins
338 0           my $is_endgame;
339 0 0         if( grep($_ eq $other, split '', $new_board) < 2 ) {
340 0           $payoff = ENDGAME;
341 0           $is_endgame = 1;
342             }
343 0 0         $payoff = 0 - $payoff if $mover eq 'o';
344             # harming X is a /negative/ payoff
345              
346 0           push @$successors,
347             _new_node(
348             $new_board,
349             $other, # it's other guy's turn now
350             $i,
351             $to,
352             $payoff,
353             $this_move_count,
354             );
355 0 0         $successors->[-1]{'endgame'} = 1 if $is_endgame;
356             # THAT node shouldn't have successors
357             }
358             }
359 0           return;
360             }
361              
362             #--------------------------------------------------------------------------
363             play() unless caller;
364             # if this module is what was run (instead of used), then start game
365              
366             #--------------------------------------------------------------------------
367             1;
368              
369             __END__