File Coverage

blib/lib/Karel/Robot/WithGrid.pm
Criterion Covered Total %
statement 126 128 98.4
branch 24 28 85.7
condition n/a
subroutine 38 38 100.0
pod 16 16 100.0
total 204 210 97.1


line stmt bran cond sub pod time code
1             package Karel::Robot::WithGrid;
2              
3             =head1 NAME
4              
5             Karel::Robot::WithGrid
6              
7             =head1 DESCRIPTION
8              
9             A robot with an associated grid. To create the robot, use
10              
11             my $robot = 'Karel::Robot'->new;
12             my $grid = 'Karel::Grid'->new(x => 10, y => 12);
13             $robot = $robot->set_grid($grid, 1, 1);
14              
15             =head1 METHODS
16              
17             =over 4
18              
19             =cut
20              
21 6     6   2813 use warnings;
  6         11  
  6         161  
22 6     6   43 use strict;
  6         7  
  6         142  
23 6     6   23 use parent 'Karel::Robot';
  6         8  
  6         41  
24 6     6   324 use Karel::Util qw{ positive_int };
  6         8  
  6         256  
25 6     6   23 use Carp;
  6         6  
  6         360  
26 6     6   22 use List::Util qw{ first };
  6         7  
  6         330  
27 6     6   2198 use Clone qw{ clone };
  6         11487  
  6         359  
28             use constant {
29 6         556 CONTINUE => 0,
30             FINISHED => 1,
31             FINISHED_DELAYED => 2,
32             QUIT => -1,
33 6     6   29 };
  6         9  
34 6     6   26 use Moo::Role;
  6         8  
  6         38  
35             requires qw{ set_grid knows };
36              
37             =item $robot->x, $robot->y
38              
39             my ($x, $y) = map $robot->$_, qw( x y );
40              
41             Coordinates of the robot in its grid.
42              
43             =cut
44              
45             has [qw[ x y ]] => ( is => 'rwp',
46             isa => \&positive_int,
47             );
48              
49             =item $robot->grid
50              
51             my $grid = $robot->grid;
52              
53             The associated C object.
54              
55             =cut
56              
57             my $grid_type = sub {
58             my ($grid) = @_;
59             eval { $grid->isa('Karel::Grid') } or croak "Invalid grid type\n";
60             };
61              
62              
63             has grid => ( is => 'rwp',
64             isa => $grid_type,
65             );
66              
67             =item $robot->set_grid($grid, $x, $y, $direction);
68              
69             Initialize the grid. Grid must be an object of the C
70             type, C<$x> and C<$y> are coordinates of the robot, C<$direction> is
71             one of C (defaults to C). Dies if the robot's place is
72             occupied by a wall.
73              
74             =cut
75              
76             around set_grid => sub {
77             my (undef, $self, $grid, $x, $y, $direction) = @_;
78             $self->_set_grid($grid);
79             $self->_set_x($x);
80             $self->_set_y($y);
81             $self->_set_direction($direction) if $direction;
82             };
83              
84             =item $robot->drop_mark
85              
86             Drop mark in the current location. Dies if there are already 9 marks.
87              
88             =cut
89              
90             sub drop_mark {
91 42     42 1 40 my ($self) = shift;
92 42         82 $self->grid->drop_mark($self->coords);
93 42         168 return 1
94             }
95              
96             =item $robot->pick_mark
97              
98             Picks up one mark from the current location. Dies if there's nothing
99             to pick.
100              
101             =cut
102              
103             sub pick_mark {
104 19     19 1 21 my ($self) = shift;
105 19         36 $self->grid->pick_mark($self->coords);
106 19         75 return 1
107             }
108              
109             =item $robot->direction
110              
111             my $direction = $robot->direction;
112              
113             Returns the robot's direction: one of C.
114              
115             =cut
116              
117             my $string_list = sub {
118             do {
119             my %strings = map { $_ => 1 } @_;
120             sub { $strings{+shift} or croak "Invalid string" }
121             }
122             };
123              
124             has direction => ( is => 'rwp',
125             isa => $string_list->(qw( N W S E )),
126             default => 'N',
127             );
128              
129             =item $robot->left
130              
131             Turn the robot to the left.
132              
133             =cut
134              
135             my @directions = qw( N W S E );
136             sub left {
137 63     63 1 596 my ($self) = @_;
138 63         98 my $dir = $self->direction;
139 63     136   238 my $idx = first { $directions[$_] eq $dir } 0 .. $#directions;
  136         142  
140 63         244 $self->_set_direction($directions[ ($idx + 1) % @directions ]);
141 63         390 return FINISHED
142             }
143              
144             =item $robot->coords
145              
146             Returns the robot's coordinates, i.e. C and C.
147              
148             =cut
149              
150             sub coords {
151 119     119 1 100 my ($self) = @_;
152 119         323 return ($self->x, $self->y)
153             }
154              
155             =item $robot->cover
156              
157             Returns the grid element at the robot's coordinates, i.e.
158              
159             $r->grid->at($r->coords)
160              
161             =cut
162              
163             sub cover {
164 18     18 1 1524 my ($self) = @_;
165 18         45 return $self->grid->at($self->coords)
166             }
167              
168             =item $robot->facing_coords
169              
170             Returns the coordinates of the grid element the robot is facing.
171              
172             =cut
173              
174             my %facing = ( N => [0, -1],
175             E => [1, 0],
176             S => [0, 1],
177             W => [-1, 0]
178             );
179              
180             sub facing_coords {
181 40     40 1 447 my ($self) = @_;
182 40         63 my $direction = $self->direction;
183 40         60 my @coords = $self->coords;
184 40         103 $coords[$_] += $facing{$direction}[$_] for 0, 1;
185             return @coords
186 40         96 }
187              
188             =item $robot->facing
189              
190             Returns the contents of the grid element the robot is facing.
191              
192             =cut
193              
194             sub facing {
195 27     27 1 3421 my ($self) = @_;
196 27         92 $self->grid->at($self->facing_coords)
197             }
198              
199              
200             has _stack => ( is => 'rwp',
201             predicate => 'is_running',
202             clearer => 'not_running',
203             isa => sub {
204             my $s = shift;
205             'ARRAY' eq ref $s or croak "Invalid stack";
206             ! grep 'ARRAY' ne ref $_, @$s
207             or croak "Invalid stack element";
208             }
209             );
210              
211             sub _pop_stack {
212 188     188   154 my $self = shift;
213 188         121 shift @{ $self->_stack };
  188         255  
214 188 100       194 $self->not_running unless @{ $self->_stack };
  188         1276  
215             }
216              
217             sub _push_stack {
218 169     169   154 my ($self, $commands) = @_;
219 169         114 unshift @{ $self->_stack }, [ clone($commands), 0 ];
  169         1360  
220             }
221              
222              
223 526     526   853 sub _stacked { shift->_stack->[0] }
224              
225             sub _stack_command {
226 108     108   95 my $self = shift;
227 108         80 my ($commands, $index) = @{ $self->_stacked };
  108         390  
228 108         131 return $commands->[$index]
229             }
230              
231             sub _stack_previous_commands {
232 49     49   136 shift->_stack->[1][0]
233             }
234              
235             sub _stack_previous_index {
236 49     49   304 shift->_stack->[1][1]
237             }
238              
239             sub _run {
240 29     29   2249 my ($self, $prog) = @_;
241 29         151 $self->_set__stack([ [$prog, 0] ]);
242             }
243              
244             =item $robot->run($command_name)
245              
246             Run the given command.
247              
248             =cut
249              
250             sub run {
251 14     14 1 2193 my ($self, $command) = @_;
252 14         108 my $parsed = $self->parser->parse("run $command");
253 13         76 $self->_run($$parsed);
254             }
255              
256             =item $robot->forward
257              
258             Moves the robot one cell forward in its direction.
259              
260             =cut
261              
262             sub forward {
263 11     11 1 12 my ($self) = @_;
264 11 50       17 croak "Can't walk through walls" if $self->facing =~ /w/i;
265 11         87 my ($x, $y) = $self->facing_coords;
266 11         27 $self->_set_x($x);
267 11         73 $self->_set_y($y);
268 11         77 return FINISHED
269             }
270              
271             =item $robot->repeat($count, $commands)
272              
273             Runs the C command: decreases the counter, and if it's
274             non-zero, pushes the body to the stack. Returns 0 (CONTINUE) when it
275             should stay in the stack, 1 (FINISHED) otherwise.
276              
277             =cut
278              
279             sub repeat {
280 139     139 1 137 my ($self, $count, $commands) = @_;
281 139 100       177 if ($count) {
282 108         205 $self->_stack_command->[1] = $count - 1;
283 108         152 $self->_push_stack($commands);
284 108         168 return CONTINUE
285              
286             } else {
287 31         55 return FINISHED
288             }
289             }
290              
291             =item $isnot_south = $robot->condition('!S')
292              
293             Solve the given condition. Supported parameters are:
294              
295             =over 4
296              
297             =item * N E S W
298              
299             Facing North, East, South, West
300              
301             =item * m
302              
303             Covering mark(s).
304              
305             =item * w
306              
307             Facing a wall.
308              
309             =item * !
310              
311             Negates the condition.
312              
313             =back
314              
315             Returns true or false, dies on invalid condition.
316              
317             =cut
318              
319             sub condition {
320 41     41 1 46 my ($self, $condition) = @_;
321 41         89 my $negation = $condition =~ s/!//;
322 41         30 my $result;
323              
324 41 100       126 if ($condition =~ /^[NESW]$/) {
    100          
    50          
325 18         57 $result = $self->direction eq $condition;
326              
327             } elsif ($condition eq 'w') {
328 12         47 $result = $self->facing =~ /w/i;
329              
330             } elsif ($condition eq 'm') {
331 11         18 $result = $self->cover =~ /^[1-9]$/;
332              
333             } else {
334 0         0 croak "Invalid condition '$condition'"
335             }
336              
337 41 100       182 $result = ! $result if $negation;
338 41         78 return $result
339             }
340              
341             =item $robot->If($condition, $commands, $else)
342              
343             If $condition is true, puts $commands to the stack, otherwise puts
344             $else to the stack. Returns 2 (FINISH_DELAYED) in the former case, 1
345             (FINISHED) in the latter one.
346              
347             =cut
348              
349             sub If {
350 22     22 1 27 my ($self, $condition, $commands, $else) = @_;
351 22 100       40 if ($self->condition($condition)) {
    100          
352 17         30 $self->_push_stack($commands);
353             } elsif ($else) {
354 2         6 $self->_push_stack($else);
355             } else {
356 3         6 return FINISHED
357             }
358 19         33 return FINISHED_DELAYED
359             }
360              
361             =item $robot->While($condition, $commands)
362              
363             Similar to C, but returns 0 (CONTINUE) if the condition is true,
364             i.e. it stays in the stack.
365              
366             =cut
367              
368             sub While {
369 19     19 1 22 my ($self, $condition, $commands) = @_;
370 19 100       41 if ($self->condition($condition)) {
371 12         19 $self->_push_stack($commands);
372 12         23 return CONTINUE
373              
374             } else {
375 7         14 return FINISHED
376             }
377             }
378              
379             =item $robot->call($command)
380              
381             Checks whether the robot knows the command, and if so, pushes its
382             definition to the stack. Dies otherwise. Returns 2 (FINISH_DELAYED).
383              
384             =cut
385              
386             sub call {
387 30     30 1 40 my ($self, $command_name) = @_;
388 30         93 my $commands = $self->knows($command_name);
389 30 50       58 if ($commands) {
390 30         68 $self->_push_stack($commands);
391             } else {
392 0         0 croak "Unknown command $command_name.";
393             }
394 30         50 return FINISHED_DELAYED
395             }
396              
397             =item $robot->stop
398              
399             Stops execution of the current program and clears the stack. Returns
400             -1 (QUIT).
401              
402             =cut
403              
404 3     3 1 14 sub stop { shift->not_running; QUIT }
  3         83  
405              
406             =item $robot->step
407              
408             Makes one step in the currently running program.
409              
410             =cut
411              
412             sub step {
413 390     390 1 1916 my ($self) = @_;
414 390 100       642 croak 'Not running!' unless $self->is_running;
415              
416 389         263 my ($commands, $index) = @{ $self->_stacked };
  389         443  
417              
418 389         307 my $command = $commands->[$index];
419             my $action = { f => 'forward',
420             l => 'left',
421             p => 'pick_mark',
422             d => 'drop_mark',
423             r => 'repeat',
424             i => 'If',
425             w => 'While',
426             q => 'stop',
427             c => 'call',
428 46     46   53 x => sub { FINISHED },
429 389         2042 }->{ $command->[0] };
430 389 50       988 croak "Unknown action " . $command->[0] unless $action;
431              
432 389         437 my $finished = $self->$action(@{ $command }[ 1 .. $#$command ]);
  389         876  
433             # warn "$command->[0], $finished.\n";
434             # use Data::Dump; warn Data::Dump::dump($self->_stack);
435              
436             { FINISHED, sub {
437 217 100   217   340 if (++$index > $#$commands) {
438 188         253 $self->_pop_stack;
439              
440             } else {
441 29         42 $self->_stacked->[1] = $index;
442             }
443             },
444 120     120   150 CONTINUE, sub { @_ = ($self); goto &step },
  120         174  
445             FINISHED_DELAYED, sub {
446 49     49   98 $self->_stack_previous_commands
447             ->[ $self->_stack_previous_index ][0] = 'x';
448             },
449       3     QUIT, sub { },
450 389         2167 }->{ $finished }->();
451             }
452              
453             =back
454              
455             =head1 RETURN VALUES
456              
457             There are three special return values corresponding to the stack handling:
458              
459             0 CONTINUE
460             1 FINISHED
461             2 FINISHED_DELAYED
462              
463             If a command returns C, the stack doesn't change. If it
464             returns C, the following command in the stack is executed.
465             If it returns C, new commands are put in the stack,
466             but once they're finished, the command behaves as if finished, too.
467              
468             =cut
469              
470             __PACKAGE__