File Coverage

blib/lib/Game/WordBrain.pm
Criterion Covered Total %
statement 66 144 45.8
branch 11 32 34.3
condition 0 7 0.0
subroutine 15 21 71.4
pod 6 6 100.0
total 98 210 46.6


line stmt bran cond sub pod time code
1             package Game::WordBrain;
2              
3 5     5   18919 use strict;
  5         10  
  5         107  
4 5     5   14 use warnings;
  5         7  
  5         89  
5              
6 5     5   308 use Game::WordBrain::Letter;
  5         6  
  5         84  
7 5     5   882 use Game::WordBrain::Word;
  5         7  
  5         91  
8 5     5   1517 use Game::WordBrain::Solution;
  5         7  
  5         90  
9 5     5   300 use Game::WordBrain::WordToFind;
  5         6  
  5         69  
10 5     5   1517 use Game::WordBrain::Prefix;
  5         11  
  5         169  
11 5     5   2441 use Game::WordBrain::Speller;
  5         11  
  5         137  
12              
13 5     5   3380 use Storable qw( dclone );
  5         18849  
  5         393  
14 5     5   35 use List::Util qw( reduce first );
  5         7  
  5         437  
15 5     5   2971 use List::MoreUtils qw( first_index );
  5         48467  
  5         37  
16              
17             our $VERSION = '0.2.1'; # VERSION
18             # ABSTRACT: Solver for Mag Interactive's WordBrain Mobile Game
19              
20             =head1 NAME
21              
22             Game::WordBrain - Solver for the Mobile App "WordBrain"
23              
24             =head1 SYNOPSIS
25              
26             # Create a new Game::WordBrain
27             my @letters;
28             push @letters, Game::WordBrain::Letter->new({ letter => 't', row => 0, col => 0 });
29             push @letters, Game::WordBrain::Letter->new({ letter => 'a', row => 0, col => 1 });
30             push @letters, Game::WordBrain::Letter->new({ letter => 'l', row => 1, col => 0 });
31             push @letters, Game::WordBrain::Letter->new({ letter => 'k', row => 1, col => 1 });
32              
33             my $words_to_find = [ Game::WordBrain::WordToFind->... ];
34             my $speller = Game::WordBrain::Speller->...;
35             my $prefix = Game::WordBrain::Prefix->...;
36              
37             my $game = Game::WordBrain->new({
38             letters => \@letters,
39             words_to_find => $words_to_find,
40             speller => $speller, # optional
41             prefix => $prefix, # optional
42             });
43              
44             # Solve a Game
45             $game->solve();
46             print "Number of Solutions Found: " . ( scalar @{ $game->{solutions} } ) . "\n";
47              
48             # Construct a game without a word
49             my $already_used_word = Game::WordBrain::Word->...;
50             my $sub_game = $game->construct_game_without_word( $already_used_word );
51              
52             # Get letter at position
53             my $letter = $game->get_letter_at_position({
54             row => 2,
55             col => 3,
56             });
57              
58              
59             # Find Near letters
60             my $near_letters = $game->find_near_letters({
61             used => [ Game::WordBrain::Letter->... ],
62             row_number => 1,
63             col_number => 1,
64             });
65              
66              
67             # Find Near Words
68             my $near_words = $game->find_near_words({
69             letter => WordBrain::Letter->...,
70             used => [ ], # Optional
71             max_word_length => 5, # Optional
72             });
73              
74             =head1 DESCRIPTION
75              
76             Game::WordBrain is a solver created to generation potential solutions for L's WordBrain. WordBrain is available for:
77              
78             =over 4
79              
80             =item L
81              
82             =item L
83              
84             =back
85              
86             This module is currently functional for I games ( 4x4 and less ) but it requires B time to process larger ones. Feel free to propose improvements at the L for this repo!
87              
88             If you are new to WordBrain or simply want a jumpstart on how this module works and it's limitations (and evolution) please see:
89              
90             =over 4
91              
92             =item L
93              
94             =item L
95              
96             =back
97              
98             =head1 ATTRIBUTES
99              
100             =head2 B
101              
102             ArrayRef of Ls that comprise the game field
103              
104             =head2 B
105              
106             ArrayRef of Ls that indicate the number of words to find as well as the length of each word.
107              
108             =head2 speller
109              
110             An instance of L that is used to spell check potential words.
111              
112             If this is not provided it will be automagically built. You generally do not need to provide this but if you wish to use something other than the provided wordlist creating your own L and providing it in the call to new would be how to accomplish that.
113              
114             =head2 prefix
115              
116             An instance of L used to speed up game play.
117              
118             If not provided, the max word_to_find will be detected and used to construct it. You generally do not need to provide this but if you wish to use something other than the provided wordlist creating your own L and providing it in the call to new would be how to accomplish that.
119              
120             =head2 solutions
121              
122             Generated after a call to ->solve has been made. This is an ArrayRef of Ls.
123              
124             =head1 METHODS
125              
126             =head2 new
127              
128             my $letters = [ Game::WordBrain::Letter->... ];
129             my $words_to_find = [ Game::WordBrain::WordToFind->... ];
130             my $speller = Game::WordBrain::Speller->...;
131             my $prefix = Game::WordBrain::Prefix->...;
132              
133             my $game = Game::WordBrain->new({
134             letters => $letters,
135             words_to_find => $words_to_find,
136             speller => $speller, # optional
137             prefix => $prefix, # optional
138             });
139              
140             Given an ArrayRef of Ls, an ArrayRef of L, and optionally an instance of L and L constructs and returns a new WordBrain game.
141              
142             B While it is also possible to pass solutions => [ Game::WordBrain::Solution->...], there is really no reason for a consumer to do so.
143              
144             =cut
145              
146             sub new {
147 3     3 1 85 my $class = shift;
148 3         5 my $args = shift;
149              
150 3 50       10 if( !exists $args->{solutions} ) {
151 3         7 $args->{solutions} = undef;
152             }
153              
154 3 100       9 if( !exists $args->{speller} ) {
155 2         15 $args->{speller} = Game::WordBrain::Speller->new();
156             }
157              
158 3 100       15 if( !exists $args->{prefix} ) {
159             my $largest_word_to_find = reduce {
160 2 100   2   30 $a->{num_letters} > $b->{num_letters} ? $a : $b
161 2         16 } @{ $args->{words_to_find} };
  2         42  
162              
163             $args->{prefix} = Game::WordBrain::Prefix->new({
164             max_prefix_length => $largest_word_to_find->{num_letters}
165 2         35 });
166             }
167              
168 3         31 return bless $args, $class;
169             }
170              
171             =head2 solve
172              
173             my @letters;
174             push @letters, WordBrain::Letter->new({ letter => 't', row => 0, col => 0 });
175             push @letters, WordBrain::Letter->new({ letter => 'a', row => 0, col => 1 });
176             push @letters, WordBrain::Letter->new({ letter => 'l', row => 1, col => 0 });
177             push @letters, WordBrain::Letter->new({ letter => 'k', row => 1, col => 1 });
178              
179             my @words_to_find;
180             push @words_to_find, WordBrain::WordToFind->new({ num_letters => 4 });
181              
182             my $game = Game::WordBrain->new({
183             letters => \@letters,
184             words_to_find => \@words_to_find,
185             });
186              
187             $game->solve();
188              
189             print "Number of Solutions Found: " . ( scalar @{ $game->{solutions} } ) . "\n";
190              
191             The solve method is the real meat of L. When called on a fully formed game this method will enumerate potential solutions and set the $game->{solutions} attribute.
192              
193             B Depending on the size of the game grid, this method can take a very long time to run.
194              
195             =cut
196              
197             sub solve {
198 0     0 1 0 my $self = shift;
199              
200 0         0 my $max_word_length = 0;
201 0         0 for my $word_to_find (@{ $self->{words_to_find} }) {
  0         0  
202 0 0       0 if( $max_word_length < $word_to_find->{num_letters} ) {
203 0         0 $max_word_length = $word_to_find->{num_letters};
204             }
205             }
206              
207 0         0 my @solutions;
208 0         0 for my $letter (@{ $self->{letters} }) {
  0         0  
209 0         0 my $possible_words = $self->find_near_words({
210             letter => $letter,
211             max_word_length => $max_word_length,
212             });
213              
214 0         0 my @actual_words;
215 0         0 for my $possible_word (@{ $possible_words }) {
  0         0  
216 0 0       0 if( grep { $_->{num_letters} == length ( $possible_word->word ) } @{ $self->{words_to_find} } ) {
  0         0  
  0         0  
217 0 0       0 if( $self->{speller}->is_valid_word( $possible_word ) ) {
218 0         0 push @actual_words, $possible_word;
219             }
220             }
221             }
222              
223              
224 0         0 for my $word ( @actual_words ) {
225 0 0       0 if( scalar @{ $self->{words_to_find} } > 1 ) {
  0         0  
226 0         0 my $updated_game = $self->construct_game_without_word( $word );
227 0         0 my $updated_game_solutions = $updated_game->solve();
228              
229 0         0 for my $updated_game_solution (@{ $updated_game_solutions }) {
  0         0  
230             push @solutions, Game::WordBrain::Solution->new({
231 0         0 words => [ $word, @{ $updated_game_solution->{words} } ],
  0         0  
232             });
233             }
234             }
235             else {
236 0         0 push @solutions, Game::WordBrain::Solution->new({
237             words => [ $word ],
238             });
239             }
240             }
241             }
242              
243 0         0 $self->{solutions} = \@solutions;
244             }
245              
246             =head2 construct_game_without_word
247              
248             my $word = Game::WordBrain::Word->...;
249             my $game = Game::WordBrain->...;
250              
251             my $sub_game = $game->construct_game_without_word( $word );
252              
253             In WordBrain, once a word is matched the letters for it are removed from the playing field, causing all other letters to shift down (think of it like gravity pulling the letters straight down). This method exists to simplify the process of generating a new instance of a L from an existing instance minus the found word.
254              
255             There really isn't a reason for a consumer to call this method directly, rather it is used by the solve method during solution enumeration.
256              
257             =cut
258              
259             sub construct_game_without_word {
260 1     1 1 325 my $self = shift;
261 1         2 my $found_word = shift;
262              
263 1         106 my $words_to_find = dclone $self->{words_to_find};
264             my $index_of_found_word = first_index {
265 2     2   3 $_->{num_letters} == scalar @{ $found_word->{letters} }
  2         22  
266 1         4 } @{ $self->{words_to_find} };
  1         10  
267              
268 1         3 splice @{ $words_to_find }, $index_of_found_word, 1;
  1         3  
269              
270 1         6 my @new_letters;
271 1         2 for my $letter (@{ $self->{letters} }) {
  1         2  
272 9 100       11 if( grep { $_ == $letter } @{ $found_word->{letters} } ) {
  36         60  
  9         10  
273 4         4 next;
274             }
275              
276             my $num_letters_used_below = grep {
277             $_->{col} == $letter->{col}
278             && $_->{row} > $letter->{row}
279 5 100       5 } @{ $found_word->{letters} };
  20         41  
  5         5  
280              
281             push @new_letters, Game::WordBrain::Letter->new({
282             letter => $letter->{letter},
283             row => $letter->{row} + $num_letters_used_below,
284             col => $letter->{col},
285 5         19 });
286             }
287              
288             return Game::WordBrain->new({
289             letters => \@new_letters,
290             words_to_find => $words_to_find,
291             speller => $self->{speller},
292             prefix => $self->{prefix},
293 1         7 });
294             }
295              
296             =head2 get_letter_at_position
297              
298             my $game = Game::WordBrain->...
299             my $letter = $game->get_letter_at_position({
300             row => 2,
301             col => 3,
302             });
303              
304             Simple convenience method to retrieve the instance of L at a given row and col.
305              
306             =cut
307              
308             sub get_letter_at_position {
309 0     0 1   my $self = shift;
310 0           my $args = shift;
311              
312             return first {
313             $_->{row} == $args->{row}
314             && $_->{col} == $args->{col}
315 0 0   0     } @{ $self->{letters} };
  0            
  0            
316             }
317              
318             =head2 find_near_letters
319              
320             my $game = Game::WordBrain->...
321             my $near_letters = $game->find_near_letters({
322             used => [ Game::WordBrain::Letter->... ],
323             row_number => 1,
324             col_number => 1,
325             });
326              
327             Given an ArrayRef of already used (for other words) Ls, and the row and col number of a position, returns an ArrayRef of Ls that are "near" the specified position. By "near" we mean a letter that is touching the specified position in one of the 8 cardinal directions and has not already been used.
328              
329             =cut
330              
331             sub find_near_letters {
332 0     0 1   my $self = shift;
333 0           my $args = shift;
334              
335 0           my @near_letters;
336 0           for my $row_offset ( -1, 0, 1 ) {
337 0           for my $col_offset ( -1, 0, 1 ) {
338 0 0 0       if( $row_offset == 0 && $col_offset == 0 ) {
339             ### Skipping Center Letter
340 0           next;
341             }
342              
343 0           my $near_row_number = $args->{row_number} + $row_offset;
344 0           my $near_col_number = $args->{col_number} + $col_offset;
345              
346 0           my $letter = $self->get_letter_at_position({
347             row => $near_row_number,
348             col => $near_col_number,
349             });
350              
351 0 0         if( !$letter ) {
352 0           next;
353             }
354              
355 0 0         if( grep { $_ == $letter } @{ $args->{used} } ) {
  0            
  0            
356             ### Skipping Already Used Letter
357 0           next;
358             }
359              
360 0           push @near_letters, $letter;
361             }
362             }
363              
364 0           return \@near_letters;
365             }
366              
367             =head2 find_near_words
368              
369             my $game = Game::WordBrain->...;
370             my $near_words = $game->find_near_words({
371             letter => WordBrain::Letter->...,
372             used => [ ], # Optional
373             max_word_length => 5, # Optional
374             });
375              
376             Similiar to find_near_letters, but returns an ArrayRef of Ls that can be constructed from the given L, ArrayRef of used Ls and the max_word_length that should be searched for ( this should be the max L->{num_letters} ).
377              
378             =cut
379              
380             sub find_near_words {
381 0     0 1   my $self = shift;
382 0           my $args = shift;
383              
384 0   0       $args->{used} //= [ ];
385 0   0       $args->{max_word_length} //= scalar @{ $self->{letters} };
  0            
386              
387             return $self->_find_near_words({
388             word_root => Game::WordBrain::Word->new({ letters => [ $args->{letter} ] }),
389             letter => $args->{letter},
390             used => $args->{used},
391             max_word_length => $args->{max_word_length},
392 0           });
393             }
394              
395             sub _find_near_words {
396 0     0     my $self = shift;
397 0           my $args = shift;
398              
399 0           push @{ $args->{used} }, $args->{letter};
  0            
400              
401 0 0         if( scalar @{ $args->{word_root}->{letters} } >= $args->{max_word_length} ) {
  0            
402 0           return [ ];
403             }
404              
405 0 0         if( !$self->{prefix}->is_start_of_word( $args->{word_root} ) ) {
406 0           return [ ];
407             }
408              
409 0           my @words;
410             my $near_letters = $self->find_near_letters({
411             used => $args->{used},
412             game => $args->{game},
413             row_number => $args->{letter}{row},
414             col_number => $args->{letter}{col},
415 0           });
416              
417 0           for my $near_letter (@{ $near_letters }) {
  0            
418             my $new_word_root = Game::WordBrain::Word->new({
419 0           letters => [ @{ $args->{word_root}{letters} }, $near_letter ]
  0            
420             });
421              
422 0           push @words, $new_word_root;
423              
424 0           my $near_letter_used = dclone $args->{used};
425              
426             push @words, @{
427 0           $self->_find_near_words({
428             word_root => $new_word_root,
429             letter => $near_letter,
430             used => $near_letter_used,
431             max_word_length => $args->{max_word_length},
432 0           });
433             };
434             }
435              
436 0           return \@words;
437             }
438              
439             =head1 AUTHORS
440              
441             Robert Stone, C<< >>
442              
443             =head1 CONTRIBUTORS
444              
445             Special thanks to the following individuals who submitted bug reports, performance ideas, and/or pull requests.
446              
447             =over 4
448              
449             =item Todd Rinaldo
450              
451             =item Mohammad S Anwar C< mohammad.anwar@yahoo.com >
452              
453             =back
454              
455             =head1 ACKNOWLEDGMENTS
456              
457             Special thanks to L for funding the development of this module and providing test resources.
458              
459             Further thanks to L for providing input and ideas for improvement.
460              
461             =head1 COPYRIGHT & LICENSE
462              
463             Copyright 2016 Robert Stone
464              
465             This program is free software; you can redistribute it and/or modify it under the terms of either: the GNU Lesser General Public License as published by the Free Software Foundation; or any compatible license.
466              
467             See http://dev.perl.org/licenses/ for more information.
468              
469             =cut
470              
471             1;