#!/usr/bin/env perl use strict; use warnings; # PODNAME: smt # ABSTRACT: command line interface to Games::SMTNocturne::Demons use Getopt::Long; use List::Util 1.27 'max', 'sum0'; use Games::SMTNocturne::Demons; =head1 SYNOPSIS smt [args...] Possible commands: demon demons_of_type all_demons fuse [--boss [,...] [--deathstones ] [--kagutsuchi ] fusions_for [--boss [,...] [--max_level ] min_level_for [--boss [,...] [--max_level ] party_fusion [--boss [,...] [--max_level ] [--deathstones ] [ ...] =head1 DESCRIPTION This program exposes the functionality from L on the command line, as well as adding some useful functionality on top of it. =head1 COMMANDS =head2 demon $ smt demon Pixie Displays the type, name, and base level for the given demon. =head2 demons_of_type $ smt demons_of_type Wilder Displays the type, name, and base level for all demons of a given type. =head2 all_demons $ smt all_demons [...] Displays the type, name, and base level for all demons. =head2 fuse $ smt fuse Zhen Lilim Displays the demon that would result from fusing C with C. =head2 fusions_for $ smt fusions_for 'Jack Frost' Fuse with resulting in Fuse with resulting in Fuse with resulting in Fuse with resulting in [...] Displays all possible ways to create C via fusion. =head2 min_level_for $ smt min_level_for 'Jack Frost' Level 4: Fuse with resulting in Calculates the "easiest" way to fuse a given demon, where "easiest" is defined as requiring the lowest leveled demons possible. =head2 party_fusion $ smt party_fusion Pixie 'Hua Po' Zhen : Fuse with resulting in : Fuse with resulting in Fuse with resulting in : Fuse with resulting in Fuse with resulting in : Fuse with resulting in Calculates all possible demons that can be fused from the given party, as well as an example path to take in order to create that demon. =cut sub _demon { Games::SMTNocturne::Demons::demon(@_) } sub _demons_of_type { Games::SMTNocturne::Demons::demons_of_type(@_) } sub _all_demons { Games::SMTNocturne::Demons::all_demons(@_) } sub _fuse { Games::SMTNocturne::Demons::fuse(@_) } sub _fusions_for { Games::SMTNocturne::Demons::fusions_for(@_) } my $command = shift @ARGV; my ($max_level, $deathstones, $kagutsuchi, $bosses); GetOptions( "max_level=i" => \$max_level, "deathstones=i" => \$deathstones, "kagutsuchi=i" => \$kagutsuchi, "boss=s@" => \$bosses, ) or die "couldn't parse options"; $bosses = [ split(/,/, join(',', @$bosses)) ] if $bosses; my $options = { max_level => $max_level, deathstone => $deathstones, kagutsuchi => $kagutsuchi, bosses => $bosses, }; if ($command !~ /^_/ && defined &$command) { { no strict 'refs'; &{ $command }(@ARGV) } } else { die "unknown command $command"; } sub demon { my ($demon) = @_; print _demon($demon), "\n"; } sub demons_of_type { my ($type) = @_; print join("\n", _demons_of_type($type)), "\n"; } sub all_demons { print join("\n", _all_demons), "\n"; } sub fuse { my ($demon1, $demon2, $sacrifice) = @_; print _fuse($demon1, $demon2, { sacrifice => $sacrifice, %$options }), "\n"; } sub fusions_for { my ($demon) = @_; print join("\n", _fusions_for($demon, $options)), "\n"; } sub min_level_for { my ($demon) = @_; my @fusions = _fusions_for($demon, $options); my $min_level = 99; my @min_level_fusions; for my $fusion (@fusions) { my $max_level = max(map { $_->level } $fusion->all_demons); if ($max_level < $min_level) { @min_level_fusions = ($fusion); $min_level = $max_level; } elsif ($max_level == $min_level) { push @min_level_fusions, $fusion; } } print "Level $min_level:\n"; print join("\n", @min_level_fusions), "\n"; } sub party_fusion { my @demons = @_; my $seen = _party_fusion_recursive_fuse( { map { $_ => _demon($_) } @demons }, { map { $_ => _demon($_) } @demons }, $deathstones, map { _demon($_) } @demons ); my @found = sort { $a->level <=> $b->level } map { _demon($_) } keys %$seen; for my $demon (@found) { my $path = $seen->{$demon->name}; if (ref($path) eq 'ARRAY') { print "$demon:\n"; my ($fusion, $current) = @$path; print " $_\n" for _linearize_tree($current, $fusion); } else { print "$demon\n"; } } } my $SEEN = {}; sub _party_fusion_recursive_fuse { my ($seen, $current, $deathstones, @demons) = @_; return $seen if $SEEN->{join(';', @demons, ($deathstones || ''))}++; if (@demons > 1) { for my $demon1 (@demons) { for my $demon2 (grep { $_ ne $demon1 } @demons) { if ($deathstones) { for my $phase (0..8) { _check_fusion( $seen, $current, \@demons, $demon1, $demon2, undef, $deathstones, $phase ); } } else { _check_fusion($seen, $current, \@demons, $demon1, $demon2); } for my $demon3 (grep { $_ ne $demon1 && $_ ne $demon2 } @demons) { _check_fusion( $seen, $current, \@demons, $demon1, $demon2, $demon3 ); } } } } return $seen; } sub _check_fusion { my ($seen, $current, $party, $demon1, $demon2, $sacrifice, $deathstones, $phase) = @_; my $fused = _fuse( $demon1, $demon2, { %$options, sacrifice => $sacrifice, deathstone => $deathstones, kagutsuchi => $phase, }, ); return unless $fused; return if defined $max_level && $fused->level > $max_level; my $new_current = { %$current }; my $new_fusion = Games::SMTNocturne::Demons::Fusion->new( $options, $demon1, $demon2, ($sacrifice ? ($sacrifice) : ()), ($fused->type eq 'Fiend' ? ('', [$phase]) : ()), ); if (my $old_fusion = $new_current->{$fused->name}) { my $old_max_level = _max_level($new_current, $old_fusion); my $new_max_level = _max_level($new_current, $new_fusion); if ($new_max_level < $old_max_level) { $new_current->{$fused->name} = $new_fusion; } else { my $old_demon_count = _demon_count($new_current, $old_fusion); my $new_demon_count = _demon_count($new_current, $new_fusion); if ($new_demon_count < $old_demon_count) { $new_current->{$fused->name} = $new_fusion; } } } else { $new_current->{$fused->name} = $new_fusion; } if (my $old = $seen->{$fused->name}) { my ($old_fusion, $old_current) = ref($old) eq 'ARRAY' ? @$old : ($old, undef); my $old_max_level = _max_level($old_current, $old_fusion); my $new_max_level = _max_level($new_current, $new_fusion); if ($new_max_level < $old_max_level) { $seen->{$fused->name} = [ $new_fusion, $new_current ]; } else { my $old_demon_count = _demon_count($old_current, $old_fusion); my $new_demon_count = _demon_count($new_current, $new_fusion); if ($new_demon_count < $old_demon_count) { $seen->{$fused->name} = [ $new_fusion, $new_current ]; } } } else { $seen->{$fused->name} = [ $new_fusion, $new_current ]; } my @new_party = ( $fused, grep { $_ ne $demon1 && $_ ne $demon2 && (!$sacrifice || $_ ne $sacrifice) } @$party ); _party_fusion_recursive_fuse( $seen, $new_current, ($fused->type eq 'Fiend' ? $deathstones - 1 : $deathstones), @new_party ); } sub _max_level { my ($seen, $fusion) = @_; return 0 if $fusion->isa('Games::SMTNocturne::Demons::Demon'); return 1 + max( map { _max_level($seen, $seen->{$_->name}) } $fusion->all_demons ); } sub _demon_count { my ($seen, $fusion) = @_; return 1 if $fusion->isa('Games::SMTNocturne::Demons::Demon'); my @demons = $fusion->all_demons; return @demons + sum0( map { _demon_count($seen, $seen->{$_->name}) } @demons ); } sub _linearize_tree { my ($seen, $fusion) = @_; return if $fusion->isa('Games::SMTNocturne::Demons::Demon'); return ( (map { _linearize_tree($seen, $seen->{$_->name}) } $fusion->all_demons), $fusion, ); }