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   2724 use warnings;
  6         9  
  6         169  
22 6     6   47 use strict;
  6         7  
  6         133  
23 6     6   23 use parent 'Karel::Robot';
  6         5  
  6         39  
24 6     6   323 use Karel::Util qw{ positive_int };
  6         7  
  6         226  
25 6     6   21 use Carp;
  6         6  
  6         301  
26 6     6   21 use List::Util qw{ first };
  6         8  
  6         280  
27 6     6   2051 use Clone qw{ clone };
  6         11263  
  6         347  
28             use constant {
29 6         444 CONTINUE => 0,
30             FINISHED => 1,
31             FINISHED_DELAYED => 2,
32             QUIT => -1,
33 6     6   31 };
  6         8  
34 6     6   23 use Moo::Role;
  6         5  
  6         27  
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 45 my ($self) = shift;
92 42         76 $self->grid->drop_mark($self->coords);
93 42         167 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 22 my ($self) = shift;
105 19         31 $self->grid->pick_mark($self->coords);
106 19         77 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 589 my ($self) = @_;
138 63         94 my $dir = $self->direction;
139 63     136   213 my $idx = first { $directions[$_] eq $dir } 0 .. $#directions;
  136         132  
140 63         235 $self->_set_direction($directions[ ($idx + 1) % @directions ]);
141 63         362 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 94 my ($self) = @_;
152 119         343 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 1388 my ($self) = @_;
165 18         42 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 472 my ($self) = @_;
182 40         69 my $direction = $self->direction;
183 40         54 my @coords = $self->coords;
184 40         95 $coords[$_] += $facing{$direction}[$_] for 0, 1;
185             return @coords
186 40         95 }
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 3299 my ($self) = @_;
196 27         87 $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   151 my $self = shift;
213 188         126 shift @{ $self->_stack };
  188         224  
214 188 100       171 $self->not_running unless @{ $self->_stack };
  188         1325  
215             }
216              
217             sub _push_stack {
218 169     169   136 my ($self, $commands) = @_;
219 169         122 unshift @{ $self->_stack }, [ clone($commands), 0 ];
  169         1316  
220             }
221              
222              
223 526     526   826 sub _stacked { shift->_stack->[0] }
224              
225             sub _stack_command {
226 108     108   95 my $self = shift;
227 108         78 my ($commands, $index) = @{ $self->_stacked };
  108         112  
228 108         129 return $commands->[$index]
229             }
230              
231             sub _stack_previous_commands {
232 49     49   126 shift->_stack->[1][0]
233             }
234              
235             sub _stack_previous_index {
236 49     49   299 shift->_stack->[1][1]
237             }
238              
239             sub _run {
240 29     29   2284 my ($self, $prog) = @_;
241 29         129 $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 2153 my ($self, $command) = @_;
252 14         99 my $parsed = $self->parser->parse("run $command");
253 13         55 $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 11 my ($self) = @_;
264 11 50       18 croak "Can't walk through walls" if $self->facing =~ /w/i;
265 11         73 my ($x, $y) = $self->facing_coords;
266 11         23 $self->_set_x($x);
267 11         69 $self->_set_y($y);
268 11         62 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 140 my ($self, $count, $commands) = @_;
281 139 100       178 if ($count) {
282 108         243 $self->_stack_command->[1] = $count - 1;
283 108         139 $self->_push_stack($commands);
284 108         172 return CONTINUE
285              
286             } else {
287 31         53 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 40 my ($self, $condition) = @_;
321 41         86 my $negation = $condition =~ s/!//;
322 41         33 my $result;
323              
324 41 100       126 if ($condition =~ /^[NESW]$/) {
    100          
    50          
325 18         51 $result = $self->direction eq $condition;
326              
327             } elsif ($condition eq 'w') {
328 12         28 $result = $self->facing =~ /w/i;
329              
330             } elsif ($condition eq 'm') {
331 11         19 $result = $self->cover =~ /^[1-9]$/;
332              
333             } else {
334 0         0 croak "Invalid condition '$condition'"
335             }
336              
337 41 100       180 $result = ! $result if $negation;
338 41         73 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 26 my ($self, $condition, $commands, $else) = @_;
351 22 100       39 if ($self->condition($condition)) {
    100          
352 17         27 $self->_push_stack($commands);
353             } elsif ($else) {
354 2         26 $self->_push_stack($else);
355             } else {
356 3         7 return FINISHED
357             }
358 19         35 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 27 my ($self, $condition, $commands) = @_;
370 19 100       38 if ($self->condition($condition)) {
371 12         21 $self->_push_stack($commands);
372 12         17 return CONTINUE
373              
374             } else {
375 7         12 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 39 my ($self, $command_name) = @_;
388 30         108 my $commands = $self->knows($command_name);
389 30 50       63 if ($commands) {
390 30         56 $self->_push_stack($commands);
391             } else {
392 0         0 croak "Unknown command $command_name.";
393             }
394 30         55 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 12 sub stop { shift->not_running; QUIT }
  3         75  
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 1723 my ($self) = @_;
414 390 100       610 croak 'Not running!' unless $self->is_running;
415              
416 389         265 my ($commands, $index) = @{ $self->_stacked };
  389         435  
417              
418 389         306 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   50 x => sub { FINISHED },
429 389         1737 }->{ $command->[0] };
430 389 50       954 croak "Unknown action " . $command->[0] unless $action;
431              
432 389         396 my $finished = $self->$action(@{ $command }[ 1 .. $#$command ]);
  389         834  
433             # warn "$command->[0], $finished.\n";
434             # use Data::Dump; warn Data::Dump::dump($self->_stack);
435              
436             { FINISHED, sub {
437 217 100   217   324 if (++$index > $#$commands) {
438 188         255 $self->_pop_stack;
439              
440             } else {
441 29         39 $self->_stacked->[1] = $index;
442             }
443             },
444 120     120   148 CONTINUE, sub { @_ = ($self); goto &step },
  120         171  
445             FINISHED_DELAYED, sub {
446 49     49   101 $self->_stack_previous_commands
447             ->[ $self->_stack_previous_index ][0] = 'x';
448             },
449       3     QUIT, sub { },
450 389         2124 }->{ $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__