File Coverage

blib/lib/Karel/Robot/WithGrid.pm
Criterion Covered Total %
statement 140 142 98.5
branch 30 34 88.2
condition 3 5 60.0
subroutine 41 41 100.0
pod 17 17 100.0
total 231 239 96.6


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 7     7   4268 use warnings;
  7         11  
  7         245  
22 7     7   66 use strict;
  7         46  
  7         154  
23 7     7   28 use parent 'Karel::Robot';
  7         48  
  7         56  
24 7     7   498 use Karel::Util qw{ positive_int };
  7         9  
  7         417  
25 7     7   34 use Carp;
  7         9  
  7         508  
26 7     7   32 use List::Util qw{ first };
  7         8  
  7         434  
27 7     7   2949 use Clone qw{ clone };
  7         16105  
  7         514  
28             use constant {
29 7         702 CONTINUE => 0,
30             FINISHED => 1,
31             FINISHED_DELAYED => 2,
32             QUIT => -1,
33 7     7   43 };
  7         12  
34 7     7   30 use Moo::Role;
  7         18  
  7         44  
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 49 my ($self) = shift;
92 42         108 $self->grid->drop_mark($self->coords);
93 42         211 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 25 my ($self) = shift;
105 19         46 $self->grid->pick_mark($self->coords);
106 19         97 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 263     263 1 906 my ($self) = @_;
138 263         443 my $dir = $self->direction;
139 263     636   905 my $idx = first { $directions[$_] eq $dir } 0 .. $#directions;
  636         691  
140 263         1137 $self->_set_direction($directions[ ($idx + 1) % @directions ]);
141 263         1939 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 129     129 1 122 my ($self) = @_;
152 129         425 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 1869 my ($self) = @_;
165 18         56 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 50     50 1 560 my ($self) = @_;
182 50         80 my $direction = $self->direction;
183 50         97 my @coords = $self->coords;
184 50         158 $coords[$_] += $facing{$direction}[$_] for 0, 1;
185             return @coords
186 50         158 }
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 37     37 1 4247 my ($self) = @_;
196 37         102 $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 340     340   343 my $self = shift;
213 340         256 shift @{ $self->_stack };
  340         509  
214 340 100       421 $self->not_running unless @{ $self->_stack };
  340         2170  
215             }
216              
217             sub _push_stack {
218 312     312   328 my ($self, $commands, $current) = @_;
219 312 100       703 $current = $self->_stack->[1][-1]
220             unless $current;
221 312         253 unshift @{ $self->_stack }, [ clone($commands), 0, $current ];
  312         5073  
222             }
223              
224              
225 1212     1212   2486 sub _stacked { shift->_stack->[0] }
226              
227             sub _stack_command {
228 178     178   177 my $self = shift;
229 178         155 my ($commands, $index) = @{ $self->_stacked };
  178         226  
230 178         271 return $commands->[$index]
231             }
232              
233             sub _stack_previous_commands {
234 122     122   322 shift->_stack->[1][0]
235             }
236              
237             sub _stack_previous_index {
238 122     122   680 shift->_stack->[1][1]
239             }
240              
241             sub _stack_delay_finish {
242 122     122   148 my ($self) = @_;
243 122         237 $self->_stack_previous_commands
244             ->[ $self->_stack_previous_index ][0] = 'x';
245             }
246              
247             =item $robot->current
248              
249             For debugging Karel programs: returns the source of the currently
250             executed command, current position in the source and the length of the
251             command.
252              
253             =cut
254              
255             sub current {
256 373     373 1 173250 my ($self) = @_;
257 373   66 443   1514 my $command = (first { 'x' ne $_->[0][0][0] } @{ $self->_stack })
  443         1318  
  373         1320  
258             // $self->_stacked;
259 373         1054 my $current = $command->[-1];
260 373         370 my ($from, $length) = @{ $command->[0][ $command->[-2] ][-1] };
  373         770  
261 373   50     1072 my $known = $self->knowledge->{ $current // q() };
262 373 100       694 my $src = ref $current ? $current->[0] : $known->[1];
263 373         952 return $src, $from, $length
264             }
265              
266             sub _run {
267 30     30   2844 my ($self, $prog, $current) = @_;
268 30         185 $self->_set__stack([ [ $prog, 0, $current ] ]);
269             }
270              
271             =item $robot->run($command_name)
272              
273             Run the given command.
274              
275             =cut
276              
277             sub run {
278 15     15 1 3824 my ($self, $command) = @_;
279 15         178 my $parsed = $self->parser->parse("run $command");
280 14         109 $self->_run($$parsed, [$command]);
281             }
282              
283             =item $robot->forward
284              
285             Moves the robot one cell forward in its direction.
286              
287             =cut
288              
289             sub forward {
290 11     11 1 14 my ($self) = @_;
291 11 50       22 croak "Can't walk through walls" if $self->facing =~ /w/i;
292 11         91 my ($x, $y) = $self->facing_coords;
293 11         29 $self->_set_x($x);
294 11         87 $self->_set_y($y);
295 11         74 return FINISHED
296             }
297              
298             =item $robot->repeat($count, $commands)
299              
300             Runs the C command: decreases the counter, and if it's
301             non-zero, pushes the body to the stack. Returns 0 (CONTINUE) when it
302             should stay in the stack, 1 (FINISHED) otherwise.
303              
304             =cut
305              
306             sub repeat {
307 240     240 1 282 my ($self, $count, $commands) = @_;
308 240 100       409 if ($count) {
309 178         365 $self->_stack_command->[1] = $count - 1;
310 178         279 $self->_push_stack($commands);#, $self->_stack->[1][-1]);
311 178         367 return CONTINUE
312              
313             } else {
314 62         127 return FINISHED
315             }
316             }
317              
318             =item $isnot_south = $robot->condition('!S')
319              
320             Solve the given condition. Supported parameters are:
321              
322             =over 4
323              
324             =item * N E S W
325              
326             Facing North, East, South, West
327              
328             =item * m
329              
330             Covering mark(s).
331              
332             =item * w
333              
334             Facing a wall.
335              
336             =item * !
337              
338             Negates the condition.
339              
340             =back
341              
342             Returns true or false, dies on invalid condition.
343              
344             =cut
345              
346             sub condition {
347 51     51 1 62 my ($self, $condition) = @_;
348 51         131 my $negation = $condition =~ s/!//;
349 51         58 my $result;
350              
351 51 100       200 if ($condition =~ /^[NESW]$/) {
    100          
    50          
352 18         44 $result = $self->direction eq $condition;
353              
354             } elsif ($condition eq 'w') {
355 22         62 $result = $self->facing =~ /w/i;
356              
357             } elsif ($condition eq 'm') {
358 11         38 $result = $self->cover =~ /^[1-9]$/;
359              
360             } else {
361 0         0 croak "Invalid condition '$condition'"
362             }
363              
364 51 100       343 $result = ! $result if $negation;
365 51         127 return $result
366             }
367              
368             =item $robot->If($condition, $commands, $else)
369              
370             If $condition is true, puts $commands to the stack, otherwise puts
371             $else to the stack. Returns 2 (FINISH_DELAYED) in the former case, 1
372             (FINISHED) in the latter one.
373              
374             =cut
375              
376             sub If {
377 32     32 1 50 my ($self, $condition, $commands, $else) = @_;
378 32 100       80 if ($self->condition($condition)) {
    100          
379 27         56 $self->_push_stack($commands);
380             } elsif ($else) {
381 4         12 $self->_push_stack($else);
382             } else {
383 1         2 return FINISHED
384             }
385 31         62 return FINISHED_DELAYED
386             }
387              
388             =item $robot->While($condition, $commands)
389              
390             Similar to C, but returns 0 (CONTINUE) if the condition is true,
391             i.e. it stays in the stack.
392              
393             =cut
394              
395             sub While {
396 19     19 1 40 my ($self, $condition, $commands) = @_;
397 19 100       65 if ($self->condition($condition)) {
398 12         25 $self->_push_stack($commands);
399 12         27 return CONTINUE
400              
401             } else {
402 7         15 return FINISHED
403             }
404             }
405              
406             =item $robot->call($command)
407              
408             Checks whether the robot knows the command, and if so, pushes its
409             definition to the stack. Dies otherwise. Returns 2 (FINISH_DELAYED).
410              
411             =cut
412              
413             sub call {
414 91     91 1 124 my ($self, $command_name) = @_;
415 91         268 my $commands = $self->knows($command_name);
416 91 50       170 if ($commands) {
417 91         165 $self->_push_stack($commands, $command_name);
418             } else {
419 0         0 croak "Unknown command $command_name.";
420             }
421 91         193 return FINISHED_DELAYED
422             }
423              
424             =item $robot->stop
425              
426             Stops execution of the current program and clears the stack. Returns
427             -1 (QUIT).
428              
429             =cut
430              
431 3     3 1 15 sub stop { shift->not_running; QUIT }
  3         90  
432              
433             =item $robot->step
434              
435             Makes one step in the currently running program.
436              
437             =cut
438              
439             sub step {
440 845     845 1 4251 my ($self) = @_;
441 845 100       1680 croak 'Not running!' unless $self->is_running;
442              
443 844         741 my ($commands, $index) = @{ $self->_stacked };
  844         1177  
444              
445 844         762 my $command;
446 844 100       1330 $command = defined $index ? $commands->[$index] : ['x'];
447             my $action = { f => 'forward',
448             l => 'left',
449             p => 'pick_mark',
450             d => 'drop_mark',
451             r => 'repeat',
452             i => 'If',
453             w => 'While',
454             q => 'stop',
455             c => 'call',
456 129     129   209 x => sub { FINISHED },
457 844         5354 }->{ $command->[0] };
458 844 50       2794 croak "Unknown action " . $command->[0] unless $action;
459              
460 844         1206 my $finished = $self->$action(@{ $command }[ 1 .. $#$command ]);
  844         2308  
461             # warn "$command->[0], $finished.\n";
462             # use Data::Dump; warn Data::Dump::dump($self->_stack);
463              
464             { FINISHED, sub {
465 529 100   529   1051 if (++$index > $#$commands) {
466 340         606 $self->_pop_stack;
467              
468             } else {
469 189         331 $self->_stacked->[1] = $index;
470             }
471             },
472 190     190   324 CONTINUE, sub { @_ = ($self); goto &step },
  190         358  
473 122     122   290 FINISHED_DELAYED, sub { $self->_stack_delay_finish },
474       3     QUIT, sub { },
475 844         5948 }->{$finished}->();
476             }
477              
478             =back
479              
480             =head1 RETURN VALUES
481              
482             There are three special return values corresponding to the stack handling:
483              
484             0 CONTINUE
485             1 FINISHED
486             2 FINISHED_DELAYED
487              
488             If a command returns C, the stack doesn't change. If it
489             returns C, the following command in the stack is executed.
490             If it returns C, new commands are put in the stack,
491             but once they're finished, the command behaves as if finished, too.
492              
493             =cut
494              
495             __PACKAGE__