The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#!/usr/bin/perl

use strict;
use warnings;

package Games::Solitaire::Verify::App::From_DBM_FC_Solver;

use base 'Games::Solitaire::Verify::Base';

use Games::Solitaire::Verify::VariantsMap;
use Games::Solitaire::Verify::Solution;
use Games::Solitaire::Verify::State;
use Games::Solitaire::Verify::Move;

use Getopt::Long qw(GetOptionsFromArray);

__PACKAGE__->mk_acc_ref(
    [
        qw(
            _filename
            _variant_params
        )
    ]
);

sub _init
{
    my ($self, $args) = @_;

    my $argv = $args->{'argv'};

    my $variant_map = Games::Solitaire::Verify::VariantsMap->new();

    my $variant_params = $variant_map->get_variant_by_id("freecell");

    GetOptionsFromArray(
        $argv,
        'g|game|variant=s' => sub {
            my (undef, $game) = @_;

            $variant_params = $variant_map->get_variant_by_id($game);

            if (!defined($variant_params))
            {
                die "Unknown variant '$game'!\n";
            }
        },
        'freecells-num=i' => sub {
            my (undef, $n) = @_;
            $variant_params->num_freecells($n);
        },
        'stacks-num=i' => sub {
            my (undef, $n) = @_;
            $variant_params->num_columns($n);
        },
        'decks-num=i' => sub {
            my (undef, $n) = @_;

            if (! ( ($n == 1) || ($n == 2) ) )
            {
                die "Decks should be 1 or 2.";
            }

            $variant_params->num_decks($n);
        },
        'sequences-are-built-by=s' => sub {
            my (undef, $val) = @_;

            my %seqs_build_by =
            (
                (map { $_ => $_ }
                    (qw(alt_color suit rank))
                ),
                "alternate_color" => "alt_color",
            );

            my $proc_val = $seqs_build_by{$val};

            if (! defined($proc_val))
            {
                die "Unknown sequences-are-built-by '$val'!";
            }

            $variant_params->seqs_build_by($proc_val);
        },
        'empty-stacks-filled-by=s' => sub {
            my (undef, $val) = @_;

            my %empty_stacks_filled_by_map =
            (map { $_ => 1 } (qw(kings any none)));

            if (! exists($empty_stacks_filled_by_map{$val}))
            {
                die "Unknown empty stacks filled by '$val'!";
            }

            $variant_params->empty_stacks_filled_by($val);
        },
        'sequence-move=s' => sub {
            my (undef, $val) = @_;

            my %seq_moves = (map { $_ => 1 } (qw(limited unlimited)));

            if (! exists ($seq_moves{$val}) )
            {
                die "Unknown sequence move '$val'!";
            }

            $variant_params->sequence_move($val);
        },
    )
        or die "Cannot process command line arguments";

    my $filename = shift(@$argv);

    if (!defined($filename))
    {
        $filename = "-";
    }

    $self->_variant_params($variant_params);
    $self->_filename($filename);

    return;
}

sub run
{
    my $self = shift;

    my $filename = $self->_filename();
    my $variant_params = $self->_variant_params();

    my $fh;

    if ($filename eq "-")
    {
        $fh = *STDIN;
    }
    else
    {
        open $fh, "<", $filename
            or die "Cannot open '$filename' - $!";
    }

    my $found = 0;

    LINES_PREFIX:
    while (my $line = <$fh>)
    {
        chomp($line);
        if ($line eq "Success!")
        {
            $found = 1;
            last LINES_PREFIX;
        }
    }

    if (!$found)
    {
        close($fh);
        die "State was not solved successfully.";
    }

    my $read_next_state = sub {
        my $line = <$fh>;
        chomp($line);
        if ($line eq "END")
        {
            return;
        }
        elsif ($line ne "--------")
        {
            die "Incorrect format.";
        }

        my $s = <$fh>;
        LINES:
        while ($line = <$fh>)
        {
            if ($line !~ /\S/)
            {
                last LINES;
            }
            $s .= $line;
        }
        $line = <$fh>;
        chomp($line);
        if ($line ne "==")
        {
            die "Cannot find '==' terminator";
        }

        return Games::Solitaire::Verify::State->new(
            {
                variant => "custom",
                variant_params => $self->_variant_params(),
                string => $s,
            },
        );
    };

    my $initial_state = $read_next_state->();

    my $running_state = $initial_state->clone();
    my @cols_indexes = (0 .. ($running_state->num_columns() - 1));
    my @fc_indexes = (0 .. ($running_state->num_freecells() - 1));

    print "-=-=-=-=-=-=-=-=-=-=-=-\n\n";

    my $out_running_state = sub {
        print $running_state->to_string();
        print "\n\n====================\n\n";
    };

    my $calc_foundation_to_put_card_on = sub {
        my $card = shift;

        DECKS_LOOP:
        for my $deck (0 .. $running_state->num_decks() - 1)
        {
            if ($running_state->get_foundation_value($card->suit(), $deck) ==
                $card->rank() - 1)
            {
                my $other_deck_idx;

                for $other_deck_idx (0 ..
                    (($running_state->num_decks() << 2) - 1)
                )
                {
                    if ($running_state->get_foundation_value(
                            $card->get_suits_seq->[$other_deck_idx % 4],
                            ($other_deck_idx >> 2),
                        ) < $card->rank() - 2 -
                        (($card->color_for_suit(
                            $card->get_suits_seq->[$other_deck_idx % 4]
                        ) eq $card->color()) ? 1 : 0)
                    )
                    {
                        next DECKS_LOOP;
                    }
                }
                return [$card->suit(), $deck];
            }
        }
        return;
    };

    $out_running_state->();
    MOVES:
    while (my $move_line = <$fh>)
    {
        chomp($move_line);

        if ($move_line eq "END")
        {
            last MOVES;
        }

        # I thought I needed them, but I did not eventually.
        #
        # my @rev_cols_indexes;
        # @rev_cols_indexes[@cols_indexes] = (0 .. $#cols_indexes);
        # my @rev_fc_indexes;
        # @rev_fc_indexes[@fc_indexes] = (0 .. $#fc_indexes);

        my ($src, $dest);
        my $dest_move;

        my @tentative_fc_indexes = @fc_indexes;
        my @tentative_cols_indexes = @cols_indexes;
        if (($src, $dest) = $move_line =~ m{\AColumn (\d+) -> Freecell (\d+)\z})
        {
            $dest_move = "Move a card from stack $tentative_cols_indexes[$src] to freecell $tentative_fc_indexes[$dest]";
        }
        elsif (($src, $dest) = $move_line =~ m{\AColumn (\d+) -> Column (\d+)\z})
        {
            $dest_move = "Move 1 cards from stack $tentative_cols_indexes[$src] to stack $tentative_cols_indexes[$dest]";
        }
        elsif (($src, $dest) = $move_line =~ m{\AFreecell (\d+) -> Column (\d+)\z})
        {
            $dest_move = "Move a card from freecell $tentative_fc_indexes[$src] to stack $tentative_cols_indexes[$dest]";
        }
        elsif (($src) = $move_line =~ m{\AColumn (\d+) -> Foundation \d+\z})
        {
            $dest_move = "Move a card from stack $tentative_cols_indexes[$src] to the foundations";
        }
        elsif (($src) = $move_line =~ m{\AFreecell (\d+) -> Foundation \d+\z})
        {
            $dest_move = "Move a card from freecell $tentative_fc_indexes[$src] to the foundations";
        }
        print "$dest_move\n\n";


        $running_state->verify_and_perform_move(
            Games::Solitaire::Verify::Move->new(
                {
                    fcs_string => $dest_move,
                    game => $running_state->_variant(),
                },
            )
        );
        $out_running_state->();

        # Now do the horne's prune.
        my $num_moved = 1; # Always iterate at least once.

        my $perform_prune_move = sub {
            my $prune_move = shift;

            $num_moved++;

            $running_state->verify_and_perform_move(
                Games::Solitaire::Verify::Move->new(
                    {
                        fcs_string => $prune_move,
                        game => $running_state->_variant(),
                    }
                )
            );
            print "$prune_move\n\n";
            $out_running_state->();
        };

        while ($num_moved)
        {
            $num_moved = 0;
            foreach my $idx (0 .. ($running_state->num_columns()-1) )
            {
                my $col = $running_state->get_column($idx);

                if ($col->len())
                {
                    my $card = $col->top();
                    my $f = $calc_foundation_to_put_card_on->($card);

                    if (defined($f))
                    {
                        $perform_prune_move->(
                            "Move a card from stack $idx to the foundations"
                        );
                    }
                }
            }

            foreach my $idx (0 .. ($running_state->num_freecells() - 1))
            {
                my $card = $running_state->get_freecell($idx);

                if (defined($card))
                {
                    my $f = $calc_foundation_to_put_card_on->($card);

                    if (defined($f))
                    {
                        $perform_prune_move->(
                            "Move a card from freecell $idx to the foundations"
                        );
                    }
                }
            }
        }

        my $new_state = $read_next_state->();

        # Calculate the new indexes.
        my @new_cols_indexes;
        my @new_fc_indexes;

        my %old_cols_map;
        my %old_fc_map;
        my %non_assigned_cols =
            (map { $_ => 1 } (0 .. $running_state->num_columns() - 1));

        my %non_assigned_fcs =
            (map { $_ => 1 } (0 .. $running_state->num_freecells() - 1));

        foreach my $idx (0 .. ($running_state->num_columns() - 1))
        {
            my $col = $running_state->get_column($idx);
            my $card = $col->len ? $col->pos(0)->to_string() : '';

            push @{$old_cols_map{$card}}, $idx;
        }

        foreach my $idx (0 .. ($running_state->num_columns() - 1))
        {
            my $col = $new_state->get_column($idx);
            my $card = $col->len ? $col->pos(0)->to_string() : '';
            # TODO: Fix edge cases.
            my $aref = $old_cols_map{$card};

            if ((!defined($aref)) or (! @$aref))
            {
                $aref = $old_cols_map{''};
            }
            my $i = shift(@$aref);

            $new_cols_indexes[$idx] = $i;
            if (defined($i))
            {
                delete($non_assigned_cols{$i});
            }
        }

        my @non_assigned_cols_list = sort { $a <=> $b } keys(%non_assigned_cols);
        foreach my $col_idx (@new_cols_indexes)
        {
            if (!defined($col_idx))
            {
                $col_idx = shift(@non_assigned_cols_list);
            }
        }

        foreach my $idx (0 .. ($running_state->num_freecells() - 1))
        {
            my $card_obj = $running_state->get_freecell($idx);
            my $card = defined($card_obj) ? $card_obj->to_string() : '';

            push @{$old_fc_map{$card}}, $idx;
        }

        foreach my $idx (0 .. ($running_state->num_freecells() - 1))
        {
            my $card_obj = $new_state->get_freecell($idx);
            my $card = defined($card_obj) ? $card_obj->to_string() : '';
            # TODO : Fix edge cases.

            my $aref = $old_fc_map{$card};

            if ((!defined($aref)) or (! @$aref))
            {
                $aref = $old_fc_map{''};
            }

            my $i = shift(@$aref);
            $new_fc_indexes[$idx] = $i;
            if (defined($i))
            {
                delete($non_assigned_fcs{$i});
            }
        }

        my @non_assigned_fcs_list = sort { $a <=> $b } keys(%non_assigned_fcs);

        foreach my $fc_idx (@new_fc_indexes)
        {
            if (!defined ($fc_idx))
            {
                $fc_idx = shift(@non_assigned_fcs_list);
            }
        }

        my $verify_state =
            Games::Solitaire::Verify::State->new(
                {
                    variant => 'custom',
                    variant_params => $self->_variant_params(),
                }
            );

        foreach my $idx (0 .. ($running_state->num_columns() - 1))
        {
            $verify_state->add_column(
                $running_state->get_column($new_cols_indexes[$idx])->clone()
            );
        }

        $verify_state->set_freecells(
            Games::Solitaire::Verify::Freecells->new(
                {
                    count => $running_state->num_freecells(),
                }
            )
        );

        foreach my $idx (0 .. ($running_state->num_freecells() - 1))
        {
            my $card_obj = $running_state->get_freecell($new_fc_indexes[$idx]);

            if (defined($card_obj))
            {
                $verify_state->set_freecell($idx, $card_obj->clone());
            }
        }

        $verify_state->set_foundations($running_state->_foundations->clone());

        {
            my $v_s = $verify_state->to_string();
            my $n_s = $new_state->to_string();
            if ($v_s ne $n_s)
            {
                die "States mismatch:\n<<\n$v_s\n>>\n vs:\n<<\n$n_s\n>>\n.";
            }
        }

        @cols_indexes = @new_cols_indexes;
        @fc_indexes = @new_fc_indexes;
    }

    print "This game is solveable.\n";

    close($fh);
}

package main;

Games::Solitaire::Verify::App::From_DBM_FC_Solver->new({ argv => [@ARGV] })->run();