File Coverage

blib/lib/AI/Evolve/Befunge/Physics.pm
Criterion Covered Total %
statement 121 137 88.3
branch 61 64 95.3
condition n/a
subroutine 20 21 95.2
pod 9 9 100.0
total 211 231 91.3


line stmt bran cond sub pod time code
1             package AI::Evolve::Befunge::Physics;
2 4     4   9303 use strict;
  4         10  
  4         143  
3 4     4   22 use warnings;
  4         7  
  4         104  
4 4     4   24 use Carp;
  4         7  
  4         380  
5 4     4   23 use Perl6::Export::Attrs;
  4         8  
  4         35  
6 4     4   186 use UNIVERSAL::require;
  4         14  
  4         47  
7              
8 4     4   83 use AI::Evolve::Befunge::Util;
  4         7  
  4         29  
9 4     4   593 use aliased 'AI::Evolve::Befunge::Board' => 'Board';
  4         6  
  4         22  
10 4     4   585 use aliased 'AI::Evolve::Befunge::Critter' => 'Critter';
  4         44  
  4         14  
11 4     4   830 use aliased 'AI::Evolve::Befunge::Critter::Result' => 'Result';
  4         8  
  4         66  
12              
13 4     4   746 use base 'Class::Accessor::Fast';
  4         9  
  4         1160  
14             __PACKAGE__->mk_accessors( qw{ name board_size commands token decorate generations } );
15              
16             # FIXME: this module needs some extra codepaths to handle non-boardgame Physics
17             # engines.
18              
19             =head1 NAME
20              
21             AI::Evolve::Befunge::Physics - Physics engine base class
22              
23             =head1 SYNOPSIS
24              
25             For a rules plugin (game or application):
26              
27             register_physics(
28             name => "ttt",
29             token => ord('T'),
30             decorate => 0.
31             board_size => Language::Befunge::Vector->new(3, 3),
32             commands => { M => \&AI::Evolve::Befunge::Physics::op_board_make_move },
33             );
34              
35             For everyone else:
36              
37             $ttt = Physics->new('ttt');
38             my $score = $ttt->double_match($blueprint1, $blueprint2);
39              
40              
41             =head1 DESCRIPTION
42              
43             This module serves a double purpose.
44              
45             First, it serves as a plugin repository for Physics engines. It
46             allows physics engines to register themselves, and it allows callers
47             to fetch entries from the database (indexed by the name of the Physics
48             engine).
49              
50             Second, it serves as a base class for Physics engines. It creates
51             class instances to represent a Physics engine, and given a blueprint
52             or two, allows callers to run creatures in a universe which follow the
53             rules of that Physics engine.
54              
55              
56             =head1 STANDALONE FUNCTIONS
57              
58             =head2 register_physics
59              
60             register_physics(
61             name => "ttt",
62             token => ord('T'),
63             decorate => 0.
64             board_size => Language::Befunge::Vector->new(3, 3),
65             commands => { M => \&AI::Evolve::Befunge::Physics::op_board_make_move },
66             );
67              
68             Create a new physics plugin, and register it with the Physics plugin
69             database. The "name" passed here can be used later on in ->new()
70             (see below) to fetch an instance of that physics plugin.
71              
72             The arguments are:
73              
74             name: The name of the Physics module. Used by Physics->new
75             to fetch the right plugin.
76             token: A unique numeric token representing this Physics
77             plugin. It is possible that a Critter could evolve
78             that can function usefully in more than one universe;
79             this token is pushed onto its initial stack in order
80             to encourage this.
81             decorate: Used by graphical frontends. If non-zero, the
82             graphical frontend will use special icons to indicate
83             spaces where a player may move.
84             commands: A hash of op callback functions, indexed on the
85             Befunge character that should call them.
86             board_size: A Vector denoting the size of a game's board. This
87             field is optional; non-game plugins should leave it
88             unspecified.
89              
90              
91             =cut
92              
93             { my %rules;
94              
95             sub register_physics :Export(:DEFAULT) {
96 8     8 1 2706 my %args = @_;
97 8 100       68 croak("no name given") unless exists $args{name};
98 7 100       50 croak("Physics plugin '".$args{name}."' already registered!\n") if exists($rules{$args{name}});
99 6         3761 $rules{$args{name}} = \%args;
100 4     4   26 }
  4         8  
  4         79  
101              
102              
103             =head2 find_physics
104              
105             my $physics = find_physics($name);
106              
107             Find a physics plugin in the database. Note that this is for internal
108             use; external users should use ->new(), below.
109              
110             =cut
111              
112             sub find_physics {
113 23     23 1 10297 my $name = shift;
114 23 100       92 return undef unless exists $rules{$name};
115 21         116 return $rules{$name};
116             }
117              
118             }
119              
120              
121             =head1 CONSTRUCTOR
122              
123             =head2 new
124              
125             my $physics = Physics->new('ttt');
126              
127             Fetch a class instance for the given physics engine. The argument
128             passed should be the name of a physics engine... for instance, 'ttt'
129             or 'othello'. The physics plugin should be in a namespace under
130             'AI::Evolve::Befunge::Physics'... the module will be loaded if
131             necessary.
132              
133             =cut
134              
135             sub new {
136 8     8 1 2644 my ($package, $physics) = @_;
137 8         20 my $usage = 'Usage: Physics->new($physicsname);';
138 8 100       43 croak($usage) unless defined($package);
139 7 100       40 croak($usage) unless defined($physics);
140 6         22 my $module = 'AI::Evolve::Befunge::Physics::' . $physics;
141 6         111 $module->require;
142 6         86 my $rv = find_physics($physics);
143 6 100       63 croak("no such physics module found") unless defined $rv;
144 5         35 $rv = {%$rv}; # copy of the original object
145 5         58 return bless($rv, $module);
146             }
147              
148              
149             =head1 METHODS
150              
151             Once you have obtained a class instance by calling ->new(), you may
152             call the following methods on that instance.
153              
154             =head2 run_board_game
155              
156             my $score = $physics->run_board_game([$critter1,$critter2],$board);
157              
158             Run the two critters repeatedly, so that they can make moves in a
159             board game.
160              
161             A score value is returned. If a number greater than 0 is returned,
162             the critter wins. If a number less than 0 is returned, the critter
163             loses.
164              
165             The score value is derived in one of several ways... first priority
166             is to bias toward a creature which won the game, second is to bias
167             toward a creature who did not die (when the other did), third,
168             the physics plugin is asked to score the creatures based on the moves
169             they made, and finally, a choice is made based on the number of
170             resources each critter consumed.
171              
172              
173             =cut
174              
175             sub run_board_game {
176 12     12 1 8796 my ($self, $aref, $board) = @_;
177 12         20 my $usage = 'Usage: $physics->run_board_game([$critter1, $critter2], $board)';
178 12 100       74 croak($usage) unless ref($self) =~ /^AI::Evolve::Befunge::Physics/;
179 11 100       45 croak($usage) unless ref($board) eq 'AI::Evolve::Befunge::Board';
180 10         16 my ($critter1, $critter2) = @$aref;
181 10 100       46 croak($usage) unless ref($critter1) eq 'AI::Evolve::Befunge::Critter';
182 9 100       27 croak($usage) unless ref($critter2) eq 'AI::Evolve::Befunge::Critter';
183 8 100       29 croak($usage) if @$aref != 2;
184 7 100       24 croak($usage) if @_ > 3;
185 6         7 my $moves = 1;
186 6         23 $self->setup_board($board);
187 6         43 my @orig_players = ({critter => $critter1, stats => {pass => 0}},
188             {critter => $critter2, stats => {pass => 0}});
189 6         15 my @players = @orig_players;
190             # create a dummy Result object, just in case the loop never gets to player2
191             # (because player1 died on the first move).
192 6         27 $players[1]{rv} = Result->new(name => $critter2->blueprint->name, tokens => $critter2->tokens);
193 6         24 while(!$self->over($board)) {
194 13         156 my $rv = $players[0]{rv} = $players[0]{critter}->move($board, $players[0]{stats});
195 13         96 my $move = $rv->choice();
196 13 100       82 undef $move unless ref($move) eq 'Language::Befunge::Vector';
197 13 100       31 if(!defined($move)) {
198 5 100       25 if($self->can_pass($board,$players[0]{critter}->color())) {
199 1         15 $players[0]{rv}->moves($moves);
200             } else {
201 4 100       50 if($rv->died) {
202 2         20 verbose("player ", $players[0]{critter}->color(), " died.\n");
203             } else {
204 2         14 $rv->died(0.5);
205 2         14 $rv->fate('conceded');
206 2         15 verbose("player ", $players[0]{critter}->color(), " conceded.\n");
207             }
208 4         11 last;
209             }
210             }
211 9         18 $moves++;
212 9 100       43 $self->make_move($board, $players[0]{critter}->color(), $move) if defined $move;
213             # swap players
214 9         22 my $player = shift @players;
215 9         32 push(@players,$player);
216             }
217              
218 6 100       31 $board->output() unless get_quiet();
219              
220             # tally up the results to feed to compare(), below.
221 6         24 my ($rv1 , $rv2 ) = ($orig_players[0]{rv} , $orig_players[1]{rv} );
222 6         19 my ($stats1, $stats2) = ($orig_players[0]{stats}, $orig_players[1]{stats});
223 6         22 $rv1->stats($stats1);
224 6         65 $rv2->stats($stats2);
225 6 50       41 $rv1->won(1) if $self->won($board) == $critter1->color();
226 6 100       77 $rv2->won(1) if $self->won($board) == $critter2->color();
227 6         84 $rv1->score($self->score($board, $critter1->color, $moves));
228 6         66 $rv2->score($self->score($board, $critter2->color, $moves));
229 6         75 return $self->compare($rv1, $rv2);
230             }
231              
232              
233             =head2 compare
234              
235             $rv = $physics->compare($rv1, $rv2);
236              
237             Given two return values (as loaded up by the L
238             method, above), return a comparison value for the critters they belong
239             to. This is essentially a "$critter1 <=> $critter2" comparison; a
240             return value below 0 indicates that critter1 is the lesser of the two
241             critters, and a return value above 0 indicates that critter1 is the
242             greater of the two critters. The following criteria will be used for
243             comparison, in decreasing order of precedence:
244              
245             =over 4
246              
247             =item The one that won
248              
249             =item The one that didn't die
250              
251             =item The one that scored higher
252              
253             =item The one that made more moves
254              
255             =item The one that had more tokens afterwards
256              
257             =item The one with a greater (asciibetical) name
258              
259             =back
260              
261             =cut
262              
263             # This is essentially a big $critter1 <=> $critter2 comparator.
264             sub compare {
265 12     12 1 22 my ($self, $rv1, $rv2) = @_;
266 12         15 my $rv;
267 12         35 $rv = ($rv1->won() <=> $rv2->won() )*32; # prefer more winning
268 12 100       115 $rv = ($rv1->score() <=> $rv2->score() )*16 # or prefer more scoring
269             unless $rv;
270 12 100       93 $rv = ($rv1->moves() <=> $rv2->moves() )*8 # or prefer more moves
271             unless $rv;
272 12 100       72 $rv = ($rv1->tokens() <=> $rv2->tokens())*4 # or prefer more tokens
273             unless $rv;
274 12 100       68 $rv = ($rv2->died() <=> $rv1->died() )*2 # or prefer less dying
275             unless $rv;
276 12 100       44 $rv = ($rv2->name() cmp $rv1->name() )*1 # or prefer quieter names
277             unless $rv;
278 12         120 return $rv;
279             }
280              
281              
282             =head2 setup_and_run_board_game
283              
284             my $score = $physics->setup_and_run_board_game($bp1, $bp2);
285              
286             Creates Critter objects from the given Blueprint objects, creates a
287             game board (with board_size as determined from the physics plugin),
288             and calls run_board_game, above.
289              
290             =cut
291              
292             sub setup_and_run_board_game {
293 3     3 1 86 my ($self, $config, $bp1, $bp2) = @_;
294 3         6 my $usage = '...->setup_and_run($config, $blueprint1, $blueprint2)';
295 3 100       27 croak($usage) unless ref($config) eq 'AI::Evolve::Befunge::Util::Config';
296 2 100       15 croak($usage) unless ref($bp1) eq 'AI::Evolve::Befunge::Blueprint';
297 1 50       13 croak($usage) unless ref($bp2) eq 'AI::Evolve::Befunge::Blueprint';
298 0         0 my @extra_args;
299 0         0 push(@extra_args, Config => $config);
300 0         0 push(@extra_args, Physics => $self);
301 0         0 push(@extra_args, Commands => $$self{commands});
302 0         0 push(@extra_args, BoardSize => $self->board_size);
303 0         0 my $board = Board->new(Size => $self->board_size);
304 0         0 my $critter1 = Critter->new(Blueprint => $bp1, Color => 1, @extra_args);
305 0         0 my $critter2 = Critter->new(Blueprint => $bp2, Color => 2, @extra_args);
306              
307 0         0 return $self->run_board_game([$critter1,$critter2], $board);
308             }
309              
310              
311             =head2 double_match
312              
313             my $relative_score = $physics->double_match($bp1, $bp2);
314              
315             Runs two board games; one with bp1 starting first, and again with
316             bp2 starting first. The second result is subtracted from the first,
317             and the result is returned. This represents a qualitative comparison
318             between the two creatures. This can be used as a return value for
319             mergesort or qsort.
320              
321             =cut
322              
323             sub double_match :Export(:DEFAULT) {
324 3     3 1 72 my ($self, $config, $bp1, $bp2) = @_;
325 3         6 my $usage = '...->double_match($config, $blueprint1, $blueprint2)';
326 3 100       17 croak($usage) unless ref($config) eq 'AI::Evolve::Befunge::Util::Config';
327 2 100       15 croak($usage) unless ref($bp1) eq 'AI::Evolve::Befunge::Blueprint';
328 1 50       11 croak($usage) unless ref($bp2) eq 'AI::Evolve::Befunge::Blueprint';
329 0         0 my ($data1, $data2);
330 0         0 $data1 = $self->setup_and_run_board_game($config,$bp1,$bp2);
331 0         0 $data2 = $self->setup_and_run_board_game($config,$bp2,$bp1);
332 0         0 return ($data1 - $data2) <=> 0;
333 4     4   7079 }
  4         6  
  4         17  
334              
335              
336             =head1 COMMAND CALLBACKS
337              
338             These functions are intended for use as Befunge opcode handlers, and
339             are used by the Physics plugin modules.
340              
341             =head2 op_make_board_move
342              
343             01M
344              
345             Pops a vector (of the appropriate dimensions for the given board, not
346             necessarily the same as the codesize) from the stack, and attempts
347             to make that "move". This is for Physics engines which represent
348             board games.
349              
350             =cut
351              
352             sub op_make_board_move {
353 146     146 1 7523 my ($interp)= @_;
354 146         240 my $critter = $$interp{_ai_critter};
355 146         193 my $board = $$interp{_ai_board};
356 146         204 my $color = $$critter{color};
357 146         233 my $physics = $$critter{physics};
358 146         492 my $vec = v($interp->get_curip->spop_mult($board->size->get_dims()));
359 146 100       504 return Language::Befunge::Ops::dir_reverse(@_)
360             unless $physics->valid_move($board, $color, $vec);
361 10         76 $$critter{move} = $vec;
362             }
363              
364             =head2 op_query_tokens
365              
366             T
367              
368             Query the number of remaining tokens.
369              
370             =cut
371              
372             sub op_query_tokens {
373 0     0 1   my ($interp)= @_;
374 0           my $critter = $$interp{_ai_critter};
375 0           $interp->get_curip->spush($critter->tokens);
376             }
377              
378             1;