Changeset 428

Show
Ignore:
Timestamp:
04/24/08 02:27:22 (4 months ago)
Author:
eevee
Message:

Replaced backtrace with a breeding chain app that performs about eight million times better.

Location:
veekun/trunk
Files:
12 added
1 removed
5 modified

Legend:

Unmodified
Added
Removed
  • veekun/trunk/lib/Vee.pm

    r426 r428  
    5151=cut 
    5252 
     53 
    5354# have to get site config before setup so controller init can play with options 
    5455my $siteopt_source = 'site_options.yml'; 
  • veekun/trunk/lib/Vee/Controller/Dex/Utils.pm

    r406 r428  
    33use strict; 
    44use warnings; 
    5 use base 'Catalyst::Controller'; 
     5use base 'Catalyst::Controller::HTML::FormFu'; 
    66 
    77use List::MoreUtils qw/first_index last_index/; 
     
    99use Vee::Form; 
    1010 
    11 __PACKAGE__->config->{namespace} = 'dex'; 
     11__PACKAGE__->config(namespace => 'dex'); 
    1212 
    1313=head1 NAME 
     
    133133} 
    134134 
    135 =head2 backtrace 
    136  
    137 Calculates parents for an egg move. 
    138  
    139 =cut 
    140  
    141 sub backtrace : Chained('pokemon_chain') : Args(1) { 
    142     my ($self, $c) = @_; 
    143     my $s = $c->stash; 
    144      
    145     my $gen = 'dp'; # TODO: :( 
    146  
    147     my $pokemon_name = $c->req->captures->[0]; 
    148     my $move_name    = $c->req->args    ->[0]; 
    149  
    150     # For some reason, Catalyst::DispatchType::Chained doesn't url-decode either 
    151     # args or captures, so do it here; this is safe to do since valid move and 
    152     # Pokemon names will never ever contain percent signs..  I hope. 
    153     $pokemon_name =~ s/%([0-9A-Fa-f]{2})/chr(hex($1))/eg; 
    154     $move_name    =~ s/%([0-9A-Fa-f]{2})/chr(hex($1))/eg; 
    155      
    156     my $pokemon = $c->model('DBIC::Pokemon')->single({ name => $pokemon_name }, { prefetch => 'breeds' }); 
    157     my $move    = $c->model('DBIC::Moves')  ->single({ name => $move_name    }); 
    158  
    159     $c->vee_stop('No such Pokémon ', $pokemon_name, '.') 
    160         if not $pokemon; 
    161     $c->vee_stop('No such move ', $move_name, '.') 
    162         if not $move; 
    163  
    164     $s->{page_title} = $pokemon->name . ' - ' . $move->name . ' parents'; 
    165     $s->{crumbs}     = [ 
    166         '<a href="' . $c->uri('Dex') . '">Pok&eacute;dex</a>', 
    167         '<a href="' . $c->uri('Dex', 'pokemon_list') . '">Pok&eacute;mon</a>', 
    168         '<a href="' . $c->uri('Dex', 'pokemon', lc $pokemon->name) . '">' . $pokemon->name . '</a>', 
    169         'Breeding chains', 
    170         '<a href="' . $c->uri('Dex', 'moves', lc $move->name) . '">' . $move->name . '</a>', 
    171     ]; 
    172  
    173     # ensure this move is actually inheritable 
    174     # TODO: should I error if the move is learnable normally? 
    175     my $inheritable_ct = $c->model('DBIC::PokemonMoves')->count({ 
    176         pokemon_id => $pokemon->id, 
    177         move_id    => $move->id, 
    178         method     => [qw/ egg machine /], 
    179         -nest      => \ "FIND_IN_SET('$gen', versions)", 
    180     }); 
    181     $c->vee_stop('', $pokemon->name, " can't inherit ", $move->name, '.') unless $inheritable_ct; 
    182      
    183     my $gender_restriction; 
    184     if ($pokemon->gender_rate == 255) { 
    185         # must also be genderless, i.e. bred with Ditto 
    186         $gender_restriction = 255; 
    187     } else { 
    188         # cannot be single-gender or genderless 
    189         $gender_restriction = { -not_in => [ 0, 254, 255 ] }; 
    190     } 
    191  
    192     ### grab the methods by which any Pokemon learn the move 
    193  
    194     my $methods_rs = $c->model('DBIC::PokemonMoves')->search({ 
    195         move_id => $move->id, 
    196         method  => [qw[ level egg machine ]], 
    197         -nest   => \ "FIND_IN_SET('$gen', versions)", 
    198         'pokemon.gender_rate' => $gender_restriction, 
    199     }, { 
    200         join => 'pokemon', 
    201     }); 
    202     my %learn_methods; 
    203     while (my $row = $methods_rs->next) { 
    204         my $method = ucfirst $row->method; 
    205         if ($row->method eq 'level') { 
    206             $method .= ' ' . $row->level; 
    207         } elsif ($row->method eq 'machine') { 
    208             $method = 'TM/HM'; 
    209         } 
    210         push @{ $learn_methods{ $row->pokemon_id } }, $method; 
    211     } 
    212  
    213     ### do a SEPARATE query to grab all the possible Pokemon at once 
    214     # (DBIC seems to get confused if I prefetch { pokemon => breeds } above) 
    215     # XXX: is this my fault or theirs? 
    216  
    217     my $learners_rs = $c->model('DBIC::Pokemon')->search({ 
    218         id => [ keys %learn_methods ], 
    219     }, { 
    220         prefetch => 'breeds', 
    221         order_by => 'me.id ASC', 
    222     }); 
    223  
    224     my (%pokemon); 
    225     while (my $row = $learners_rs->next) { 
    226         $pokemon{ $row->id } = $row; 
    227     } 
    228  
    229     my %breeding_tree = ( $pokemon->id => {} ); 
    230  
    231     my %seen = ( $pokemon->id => 0 );   # hash of ( poke_id => 1, ... ) 
    232     my $cur_level = 0;                  # current tree level 
    233     my @this_level = %breeding_tree;    # array of ( poke_id, hashref, ... ) on this level 
    234  
    235     ### build the breeding-chain tree 
    236     # What we need is a tree of ancestors that can learn the move and pass it 
    237     # down; it should look something like this: 
    238     # pokemon => { 
    239     #     a => { 
    240     #         b => {}, 
    241     #         c => {}, 
    242     #         d => { 
    243     #             e => {}, 
    244     #         }, 
    245     #     }, 
    246     #     f => { 
    247     #         g => {}, 
    248     #         d => [ref to former d], 
    249     #     }, 
    250     # } 
    251     # The rules are basically that: 
    252     # 1. Every Pokemon is a key in a hash 
    253     # 2. Each corresponding value is a similar hash, containing a list of 
    254     #    Pokemon that are compatible 
    255     # 3. No infinite looping! 
    256  
    257     do { 
    258         $cur_level++; 
    259         my %branches; 
    260  
    261         while (my $pokemon_id = shift @this_level) { 
    262             my $this_poke = $pokemon{$pokemon_id}; 
    263             my $hashref = shift @this_level; 
    264  
    265             for my $pokemon_id (keys %pokemon) { 
    266                 next if not $this_poke->can_breed_with( $pokemon{$pokemon_id} ); 
    267  
    268                 if (exists $seen{ $pokemon_id }) { 
    269                     # don't put anything if this Pokemon has been seen higher 
    270                     # in the chain; needless descending is bad 
    271                     $hashref->{ $pokemon_id } = undef 
    272                         unless $seen{ $pokemon_id } < $cur_level; 
    273                 } else { 
    274                     $hashref->{$pokemon_id} = ( $branches{$pokemon_id} ||= {} ); 
    275                     $seen{$pokemon_id} = $cur_level; 
    276                 } 
    277             } 
    278         } 
    279  
    280         @this_level = %branches; 
    281     } while ($cur_level < 10 and @this_level); 
    282  
    283     ### find the optimal path 
    284      
    285     # since there should only be a small finite number of Pokemon, we can just 
    286     # iterate over every possibility; flatten into arrays, and take whichever 
    287     # is shortest 
    288  
    289     # TODO: merge this with the actual building of the tree, and toss useless 
    290     # branches as we go; right now this sub MODIFIES THE TREE, pruning any 
    291     # branches it finds that don't end with a level; this makes the dupe 
    292     # removal up top a little silly! 
    293     my $flattener; $flattener = sub { 
    294         my ($branch) = @_; 
    295         my @subchains; 
    296  
    297         for my $key (keys %$branch) { 
    298             if (grep /level/i, @{ $learn_methods{$key} }) { 
    299                 # consider this Pokemon an endpoint if it learns the move 
    300                 # naturally; TMs don't count since they are of limited quantity 
    301                 push @subchains, [ $key ]; 
    302                 $branch->{$key} &&= {}; 
    303                 next; 
    304             } 
    305  
    306             # ...otherwise, only add more chains if this branch has any valid 
    307             # subchains of its own 
    308             my @child_chains = $flattener->( $branch->{$key} ); 
    309             if (@child_chains) { 
    310                 push @subchains, map [ $key, @$_ ], @child_chains; 
    311             } else { 
    312                 delete $branch->{$key}; 
    313             } 
    314         } 
    315  
    316         return @subchains; 
    317     }; 
    318  
    319     # only take the shortest chains; it's possible there could be several! 
    320     my @flat_chains  = sort { $#$a <=> $#$b } $flattener->( \%breeding_tree ); 
    321     my @optimal_path = grep { $#$_ == $#{ $flat_chains[0] } } @flat_chains; 
    322     @$_ = reverse @$_ for @optimal_path; 
    323      
    324     ### cram everything important into the stash 
    325     $s->{breeding_tree} = \%breeding_tree; 
    326     $s->{pokemon}       = \%pokemon; 
    327     $s->{learn_methods} = \%learn_methods; 
    328     $s->{optimal}       = \@optimal_path; 
    329  
    330     $s->{template} = 'dex/utils/backtrace.tt'; 
    331      
    332 #    $c->res->body(join "<br/>", map { qq'<a href="/dex/pokemon/$PokemonNames[$_]">$PokemonNames[$_]</a>' } @other_learners); 
    333      
    334 #    TODO: 
     135#    OLD TODO: 
    335136#        ! CHECK to make sure this is a move that the Pokemon can learn via egg *or* TM 
    336137#        - female is obvious and is the species of the target child 
     
    348149#        - make a /dex/pokemon/eevee/backtrace that lists all traceable moves? 
    349150 
     151=head2 breeding_chains 
     152 
     153Finds a family tree that can be bred through to obtain a given Pokemon and a 
     154given set of egg or TM moves. 
     155 
     156=cut 
     157 
     158# NOT A METHOD 
     159# Helper sub for constructiny the tree below, out of nodes that look like: 
     160#   { 
     161#       groups   => [ $code1, $code2 ], 
     162#       pokemon  => \@pokemon, 
     163#       children => \@nodes, 
     164#   } 
     165# The second parameter is a hash of code1;code2 => $node, used for node 
     166# lookup and modified as we go to ensure nodes are inserted only once. 
     167# This sub acts breadth-first, NOT the usual recursive depth-first.  This is 
     168# to ensure that groups are placed as high in the tree as possible. 
     169sub _build_egg_groups_tree { 
     170    my ($root_node, $egg_node_map_ref) = @_; 
     171    my @nodes_by_depth = ($root_node); 
     172 
     173    # Run over a list of nodes in depth order, building it as we add more 
     174    # children 
     175    my $i = 0; 
     176    while ($i <= $#nodes_by_depth) { 
     177        my $node = $nodes_by_depth[$i]; 
     178 
     179        my %my_groups_map = map { $_ => 1 } @{ $node->{groups} }; 
     180 
     181        while ( my ($code, $other_node) = each %{$egg_node_map_ref} ) { 
     182            next if not grep { $my_groups_map{$_} } 
     183                @{ $other_node->{groups} }; 
     184 
     185            push @{ $node->{children} }, $other_node; 
     186            push @nodes_by_depth, $other_node; 
     187 
     188            delete $egg_node_map_ref->{$code}; 
     189        } 
     190 
     191        $i++; 
     192    } 
     193 
     194    # Run back through the list in reverse order, removing egg-only learners 
     195    # from leaf nodes; going in reverse order guarantees we remove such nodes 
     196    # before looking at their parents 
     197    for my $node (reverse @nodes_by_depth) { 
     198        # Remove any of my children who have no useful Pokemon 
     199        @{ $node->{children} } = grep { 
     200            @{ $_->{pokemon} } 
     201        } @{ $node->{children} }; 
     202 
     203        # Remove any of my own Pokemon that can only learn the move via egg, 
     204        # but only if there's no children for them to get the move from 
     205        if (not @{ $node->{children} }) { 
     206            @{ $node->{pokemon} } = grep { 
     207                grep { $_->method ne 'egg' } $_->pokemon_moves 
     208            } @{ $node->{pokemon} }; 
     209        } 
     210    } 
     211 
     212    return; 
     213} 
     214 
     215# Recursively flattens a tree into a list of nodes with an indent added to 
     216# each node.  Named generically because it only cares that the nodes are 
     217# hashrefs and list their children with an arrayref keyed under 'children'. 
     218sub _flatten_tree { 
     219    my ($node, $indent) = @_; 
     220    $indent ||= 0; 
     221 
     222    $node->{indent} = $indent; 
     223    my @flattened_tree = ($node); 
     224 
     225    for my $child_node (@{ $node->{children} }) { 
     226        push @flattened_tree, _flatten_tree($child_node, $indent + 1); 
     227    } 
     228 
     229    return @flattened_tree; 
     230} 
     231 
     232sub breeding_chains 
     233    : Path('calculators/breeding_chains') 
     234    : Args(0) 
     235    : FormConfig('dex/utils/breeding_chains.yml') 
     236{ 
     237    my ($self, $c) = @_; 
     238    my $s    = $c->stash; 
     239    my $form = $s->{form}; 
     240    $s->{template} = 'dex/utils/breeding_chains.tt'; 
     241    $s->{page_title} = 'Breeding Chains'; 
     242 
     243    if (not $form->submitted or $form->has_errors) { 
     244        return; 
     245    } 
     246 
     247    # XXX do the retrievals in Inflator code 
     248    my $pokemon = $c->model('DBIC::Pokemon')->find($form->param_value('pokemon')); 
     249    warn join '|', $form->param_list('move'); 
     250    my @moves   = map { $c->model('DBIC::Moves')->find($_) } $form->param_list('move'); 
     251 
     252    # XXX 
     253    my $version = 'dp'; 
     254 
     255    my @learners = $c->model('DBIC::Pokemon')->search( { 
     256        -or => [ { 
     257            'pokemon_moves.move_id' => $moves[0]->id, 
     258            'pokemon_moves.method'  => [qw[ level egg machine ]], 
     259            -nest   => \ "FIND_IN_SET('dp', pokemon_moves.versions)", 
     260#            'pokemon.gender_rate' => $gender_restriction, 
     261        }, { 
     262            # Smeargle can, technically, learn anything 
     263            name => 'Smeargle', 
     264        } ], 
     265    }, { 
     266        prefetch => ['breeds', 'pokemon_moves'], 
     267    } ); 
     268 
     269    my @egg_group_nodes;  # see above for node def 
     270    my %egg_code_map;     # code1;code2 => $node 
     271    for my $learner_pokemon (@learners) { 
     272        my @groups = sort $learner_pokemon->breeding_groups; 
     273        my $code = join ';', @groups; 
     274 
     275        # Keep a map of codes to make them quick to find 
     276        if (not exists $egg_code_map{$code}) { 
     277            push @egg_group_nodes, { 
     278                groups   => \@groups, 
     279                pokemon  => [], 
     280                children => [], 
     281            }; 
     282            $egg_code_map{$code} = $egg_group_nodes[-1]; 
     283        } 
     284 
     285        push @{ $egg_code_map{$code}{pokemon} }, $learner_pokemon; 
     286    } 
     287 
     288    # Create a fake root node for just the Pokemon of interest and build the 
     289    # tree iteratively from there 
     290    my %egg_groups_tree = ( 
     291        groups   => [ $pokemon->breeding_groups ], 
     292        pokemon  => [ $pokemon ], 
     293        children => [], 
     294    ); 
     295    _build_egg_groups_tree(\%egg_groups_tree, \%egg_code_map); 
     296 
     297    # Flatten the tree into a list with 'indent' keys added; far easier to do 
     298    # this now than to shoehorn it into TT 
     299    my @flattened_tree = _flatten_tree(\%egg_groups_tree); 
     300 
     301    $s->{ flattened_tree } = \@flattened_tree; 
     302    $s->{ version        } = $version; 
     303    $s->{ moves          } = \@moves; 
     304 
     305    # XXX error form validation better :(? 
    350306} 
    351307 
  • veekun/trunk/lib/Vee/Schema/Pokemon.pm

    r406 r428  
    130130    # Compatible if at least one egg group appears twice 
    131131    return grep { $_ > 1 } values %breeds; 
     132} 
     133 
     134=head2 move_method_string($move, $version) 
     135 
     136Returns a fairly compact string listing how this Pokemon can learn the provided 
     137move in the provided version. 
     138 
     139Currently limited to the most standard methods: egg, machine, and level. 
     140 
     141=cut 
     142 
     143sub move_method_string { 
     144    my ($self, $move, $version) = @_; 
     145 
     146    my @pokemon_moves = grep { 
     147        $_->move_id == $move->id 
     148        and $_->versions =~ /$version/ 
     149    } $self->pokemon_moves; 
     150 
     151    my @methods = qw( level machine egg sketch ); 
     152    my %method_strings = map { $_ => [] } @methods; 
     153 
     154    for my $pokemon_move (@pokemon_moves) { 
     155        my $string; 
     156        if ($pokemon_move->method eq 'egg') { 
     157            $string = 'Egg'; 
     158        } 
     159        elsif ($pokemon_move->method eq 'machine') { 
     160            $string = 'TM/HM'; 
     161        } 
     162        elsif ($pokemon_move->method eq 'level') { 
     163            $string = 'Lv ' . $pokemon_move->level; 
     164        } 
     165        else { 
     166            $string = $pokemon_move->method; 
     167        } 
     168 
     169        push @{ $method_strings{ $pokemon_move->method } }, $string; 
     170    } 
     171 
     172    # Smeargle gets a special exception~ 
     173    if ($self->name eq 'Smeargle') { 
     174        $method_strings{sketch} = [ 'Sketch' ]; 
     175    } 
     176 
     177    return join '; ', map { @{$_} } @method_strings{@methods}; 
    132178} 
    133179 
  • veekun/trunk/templates/dex/page/pokemon.tt

    r417 r428  
    393393[%         move_id = move.move_id %] 
    394394<tr class="color[% color %]"> 
    395  <td><a href="[% dex_uri('pokemon', this.name) %]/backtrace/[% MoveData.$move_id.name | lower %]"><img src="/dex-images/tree.png" alt="Chains" title="Breeding chains"/></a></td> 
     395 <td><a href="[% c.uri('Dex::Utils', 'breeding_chains', { pokemon => this.name, move => MoveData.$move_id.name }) %]"><img src="/dex-images/tree.png" alt="Chains" title="Breeding chains"/></a></td> 
    396396[%         IF generation == 0 %] <td class="level"> </td>[% IF move_columns_inv.y %] <td class="level"> </td>[% END %][% END %] 
    397397[%         IF generation <= 1 %] 
     
    462462[%         move_id = move.move_id %] 
    463463<tr class="color[% color %]"> 
    464  <td>[% IF move.versions.dp %]<a href="[% dex_uri('pokemon', this.name) %]/backtrace/[% MoveData.$move_id.name | lower %]"><img src="/dex-images/tree.png" alt="Chains" title="Breeding chains"/></a>[% END %]</td> 
     464 <td><a href="[% c.uri('Dex::Utils', 'breeding_chains', { pokemon => this.name, move => MoveData.$move_id.name }) %]"><img src="/dex-images/tree.png" alt="Chains" title="Breeding chains"/></a></td> 
    465465[%         FOR ver_col IN header_icons %] 
    466466[%             ver = base_versions.$ver_col || ver_col %] 
  • veekun/trunk/vee.yml

    r421 r428  
    3737                - SET CHARACTER SET utf8 
    3838 
     39Controller::HTML::FormFu: 
     40    config_file_path: forms