File Coverage

blib/lib/Game/WordBrain.pm
Criterion Covered Total %
statement 144 144 100.0
branch 31 32 96.8
condition 6 7 85.7
subroutine 21 21 100.0
pod 6 6 100.0
total 208 210 99.0


line stmt bran cond sub pod time code
1             package Game::WordBrain;
2              
3 6     6   16855 use strict;
  6         6  
  6         135  
4 6     6   16 use warnings;
  6         6  
  6         102  
5              
6 6     6   348 use Game::WordBrain::Letter;
  6         7  
  6         93  
7 6     6   900 use Game::WordBrain::Word;
  6         10  
  6         98  
8 6     6   1748 use Game::WordBrain::Solution;
  6         10  
  6         109  
9 6     6   290 use Game::WordBrain::WordToFind;
  6         6  
  6         76  
10 6     6   1821 use Game::WordBrain::Prefix;
  6         13  
  6         190  
11 6     6   2072 use Game::WordBrain::Speller;
  6         11  
  6         141  
12              
13 6     6   3207 use Storable qw( dclone );
  6         19420  
  6         420  
14 6     6   50 use List::Util qw( reduce first );
  6         8  
  6         475  
15 6     6   3324 use List::MoreUtils qw( first_index );
  6         52092  
  6         31  
16              
17             our $VERSION = '0.2.2'; # 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 129     129 1 1194 my $class = shift;
148 129         173 my $args = shift;
149              
150 129 50       274 if( !exists $args->{solutions} ) {
151 129         216 $args->{solutions} = undef;
152             }
153              
154 129 100       226 if( !exists $args->{speller} ) {
155 7         46 $args->{speller} = Game::WordBrain::Speller->new();
156             }
157              
158 129 100       276 if( !exists $args->{prefix} ) {
159             my $largest_word_to_find = reduce {
160 3 100   3   35 $a->{num_letters} > $b->{num_letters} ? $a : $b
161 7         77 } @{ $args->{words_to_find} };
  7         130  
162              
163             $args->{prefix} = Game::WordBrain::Prefix->new({
164             max_prefix_length => $largest_word_to_find->{num_letters}
165 7         157 });
166             }
167              
168 129         417 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 123     123 1 861 my $self = shift;
199              
200 123         131 my $max_word_length = 0;
201 123         94 for my $word_to_find (@{ $self->{words_to_find} }) {
  123         215  
202 124 100       315 if( $max_word_length < $word_to_find->{num_letters} ) {
203 123         177 $max_word_length = $word_to_find->{num_letters};
204             }
205             }
206              
207 123         115 my @solutions;
208 123         91 for my $letter (@{ $self->{letters} }) {
  123         149  
209 594         1277 my $possible_words = $self->find_near_words({
210             letter => $letter,
211             max_word_length => $max_word_length,
212             });
213              
214 594         1041 my @actual_words;
215 594         402 for my $possible_word (@{ $possible_words }) {
  594         600  
216 6823 100       4116 if( grep { $_->{num_letters} == length ( $possible_word->word ) } @{ $self->{words_to_find} } ) {
  7825         10643  
  6823         6141  
217 1286 100       2213 if( $self->{speller}->is_valid_word( $possible_word ) ) {
218 195         234 push @actual_words, $possible_word;
219             }
220             }
221             }
222              
223              
224 594         2016 for my $word ( @actual_words ) {
225 195 100       131 if( scalar @{ $self->{words_to_find} } > 1 ) {
  195         422  
226 121         288 my $updated_game = $self->construct_game_without_word( $word );
227 121         223 my $updated_game_solutions = $updated_game->solve();
228              
229 121         109 for my $updated_game_solution (@{ $updated_game_solutions }) {
  121         1134  
230             push @solutions, Game::WordBrain::Solution->new({
231 73         86 words => [ $word, @{ $updated_game_solution->{words} } ],
  73         238  
232             });
233             }
234             }
235             else {
236 74         342 push @solutions, Game::WordBrain::Solution->new({
237             words => [ $word ],
238             });
239             }
240             }
241             }
242              
243 123         314 $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 122     122 1 487 my $self = shift;
261 122         118 my $found_word = shift;
262              
263 122         3326 my $words_to_find = dclone $self->{words_to_find};
264             my $index_of_found_word = first_index {
265 220     220   204 $_->{num_letters} == scalar @{ $found_word->{letters} }
  220         354  
266 122         423 } @{ $self->{words_to_find} };
  122         483  
267              
268 122         255 splice @{ $words_to_find }, $index_of_found_word, 1;
  122         218  
269              
270 122         181 my @new_letters;
271 122         122 for my $letter (@{ $self->{letters} }) {
  122         268  
272 1098 100       636 if( grep { $_ == $letter } @{ $found_word->{letters} } ) {
  4608         5115  
  1098         1111  
273 512         530 next;
274             }
275              
276             my $num_letters_used_below = grep {
277             $_->{col} == $letter->{col}
278             && $_->{row} > $letter->{row}
279 586 100       402 } @{ $found_word->{letters} };
  2440         4775  
  586         612  
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 586         1709 });
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 122         727 });
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 36514     36514 1 25570 my $self = shift;
310 36514         21162 my $args = shift;
311              
312             return first {
313             $_->{row} == $args->{row}
314             && $_->{col} == $args->{col}
315 36514 100   161761   52004 } @{ $self->{letters} };
  161761         244976  
  36514         51185  
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 4564     4564 1 6962 my $self = shift;
333 4564         2896 my $args = shift;
334              
335 4564         2621 my @near_letters;
336 4564         4357 for my $row_offset ( -1, 0, 1 ) {
337 13692         10955 for my $col_offset ( -1, 0, 1 ) {
338 41076 100 100     69419 if( $row_offset == 0 && $col_offset == 0 ) {
339             ### Skipping Center Letter
340 4564         3225 next;
341             }
342              
343 36512         26651 my $near_row_number = $args->{row_number} + $row_offset;
344 36512         22715 my $near_col_number = $args->{col_number} + $col_offset;
345              
346 36512         55833 my $letter = $self->get_letter_at_position({
347             row => $near_row_number,
348             col => $near_col_number,
349             });
350              
351 36512 100       74215 if( !$letter ) {
352 23521         22865 next;
353             }
354              
355 12991 100       8776 if( grep { $_ == $letter } @{ $args->{used} } ) {
  34697         38279  
  12991         12931  
356             ### Skipping Already Used Letter
357 6098         6455 next;
358             }
359              
360 6893         8405 push @near_letters, $letter;
361             }
362             }
363              
364 4564         4791 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 598     598 1 3452 my $self = shift;
382 598         370 my $args = shift;
383              
384 598   50     1714 $args->{used} //= [ ];
385 598   100     787 $args->{max_word_length} //= scalar @{ $self->{letters} };
  4         18  
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 598         1460 });
393             }
394              
395             sub _find_near_words {
396 7467     7467   5355 my $self = shift;
397 7467         5353 my $args = shift;
398              
399 7467         4340 push @{ $args->{used} }, $args->{letter};
  7467         8295  
400              
401 7467 100       4665 if( scalar @{ $args->{word_root}->{letters} } >= $args->{max_word_length} ) {
  7467         12350  
402 942         2851 return [ ];
403             }
404              
405 6525 100       12060 if( !$self->{prefix}->is_start_of_word( $args->{word_root} ) ) {
406 1969         5470 return [ ];
407             }
408              
409 4556         4276 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 4556         11916 });
416              
417 4556         4102 for my $near_letter (@{ $near_letters }) {
  4556         4488  
418             my $new_word_root = Game::WordBrain::Word->new({
419 6869         5134 letters => [ @{ $args->{word_root}{letters} }, $near_letter ]
  6869         18031  
420             });
421              
422 6869         6579 push @words, $new_word_root;
423              
424 6869         161056 my $near_letter_used = dclone $args->{used};
425              
426             push @words, @{
427 6869         5718 $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 6869         16342 });
433             };
434             }
435              
436 4556         11197 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;