#!perl

# FRAGMENT id=shcompgen-hint command=tabrace

use 5.010001;
use strict;
use warnings;

use Time::HiRes qw(time);

our $AUTHORITY = 'cpan:PERLANCAR'; # AUTHORITY
our $DATE = '2021-09-21'; # DATE
our $DIST = 'Games-TabRace'; # DIST
our $VERSION = '0.010'; # VERSION

my %Opts = (
    'data_file' => "$ENV{HOME}/.tabrace.dat",
);
my $State = {
    state => 'uninit',
    num_completed_numbers => 0,
};

my $data_file_read;
sub read_data_file {
    return if $data_file_read;
    require JSON::MaybeXS;
    open my $fh, "<", $Opts{data_file} or return;
    local $/;
    my $content = <$fh>;
    $State = JSON::MaybeXS::decode_json($content);
    $data_file_read++;
}

sub write_data_file {
    require JSON::MaybeXS;
    open my $fh, ">", $Opts{data_file}
        or die "Can't write to data file '$Opts{data_file}': $!\n";
    print $fh JSON::MaybeXS::encode_json($State);
}

sub enter_high_scores {
    # check if the game can be entered in high score list
    $State->{high_scores} //= [];

    if (@{ $State->{high_scores} } < 5 ||
            $State->{score} > $State->{high_scores}[-1]{score}) {
        print "Congratulations, you made the high scores list!\n\n";
    }

    push @{ $State->{high_scores} }, {
        user  => $ENV{USER},
        score => $State->{score},
        time  => time(),
    };
    $State->{high_scores} = [
        sort { ($b->{score}//0) <=> ($a->{score}//0) }
            @{ $State->{high_scores} }
    ];
    splice @{ $State->{high_scores} }, 5
        if @{ $State->{high_scores} } > 5;
}

sub print_high_scores {
    require Text::Table::Sprintf;

    $State->{high_scores} //= [];

    say Text::Table::Sprintf::table(
        header_row => 1,
        rows => [
            ['Name', 'Score', 'Time'],
            map { [$_->{user}, $_->{score}, scalar(localtime $_->{time})] }
                @{ $State->{high_scores} },
        ],
    );
    print "\n";
}

sub gen_number {
    my $num_digits = int(3*rand() + 4);
    $State->{current_number} =
        int(10**($num_digits-1) + 9*10**($num_digits-1)*rand());
    $State->{start_time} = undef;
    $State->{end_time}   = undef;
}

if ($ENV{COMP_LINE} || $ENV{COMMAND_LINE}) {

    # inside tab completion

    my $shell;
    if ($ENV{COMP_SHELL}) {
        ($shell = $ENV{COMP_SHELL}) =~ s!.+/!!;
    } elsif ($ENV{COMMAND_LINE}) {
        $shell = 'tcsh';
    } else {
        $shell = 'bash';
    }

    my ($words, $cword);
    if ($ENV{COMP_LINE}) {
        require Complete::Bash;
        ($words,$cword) = @{ Complete::Bash::parse_cmdline(undef, undef, {truncate_current_word=>1}) };
        ($words,$cword) = @{ Complete::Bash::join_wordbreak_words($words, $cword) };
    } elsif ($ENV{COMMAND_LINE}) {
        require Complete::Tcsh;
        $shell //= 'tcsh';
        ($words, $cword) = @{ Complete::Tcsh::parse_cmdline() };
    }

    shift @$words; $cword--; # strip program name
    my $word = splice @$words, $cword, 1;

    read_data_file();

    require Complete::Util;
    my $compres;

    if ($State->{state} eq 'uninit') {
        $compres = {message=>'Please run tabrace first to init the game.'};
    } else {
        $State->{start_time} //= time();

        if ($State->{current_number} eq $word) {
            # number guessed correctly
            $compres = [$word];
            goto FORMAT;
        }

        if (index($State->{current_number}, $word) != 0) {
            # wrong typing, reset word
            $compres = [''];
            goto FORMAT;
        }

        $compres = Complete::Util::complete_array_elem(
            word  => $word,
            array => [map {
                "$word$_" .
                    (index($State->{current_number}, "$word$_") == 0 ?
                         "x" : "")
                } 0..9],
        );
    }

  FORMAT:
    if ($shell eq 'bash') {
        require Complete::Bash;
        print Complete::Bash::format_completion(
            $compres, {word=>$words->[$cword]});
    } elsif ($shell eq 'fish') {
        require Complete::Fish;
        print Complete::Bash::format_completion(
            $compres, {word=>$words->[$cword]});
    } elsif ($shell eq 'tcsh') {
        require Complete::Tcsh;
        print Complete::Tcsh::format_completion($compres);
    } elsif ($shell eq 'zsh') {
        require Complete::Zsh;
        print Complete::Zsh::format_completion($compres);
    } else {
        die "Unknown shell '$shell'";
    }

    write_data_file();
    exit 0;

} else {

    # outside tab completion

    require Getopt::Long;
    Getopt::Long::Configure(
        'no_ignore_case', 'bundling', 'auto_help', 'auto_version');
    Getopt::Long::GetOptions(
        "reset-game" => sub {
            read_data_file();
            $State->{state} = 'uninit';
            $State->{num_completed_numbers} = 0;
            $State->{score} = 0;
            write_data_file();
            exit 0;
        },
        "reset-high-scores" => sub {
            read_data_file();
            $State->{high_scores} = [];
            write_data_file();
            exit 0;
        },
        "high-scores" => sub {
            read_data_file();
            print_high_scores();
            exit 0;
        },
    );

    read_data_file();
    if ($State->{state} eq 'uninit') {
        require File::Which;
        unless (File::Which::which("tabrace")) {
            print <<'_';

tabrace doesn't seem to be in your PATH. For proper game play, please put
me in your PATH first.
_
            exit 1;
        }
        $State->{state} = 'play';
        gen_number();
        write_data_file();
        print <<'_';

Welcome to tabrace, a game played with tab completion. You must first enable tab
completion for your shell. Here's how to do it in bash:

    % complete -C tabrace tabrace

Or alternatively, install shcompgen from CPAN using 'cpanm -n App::shcompgen'.
For instructions on how to enable tab completion for other shells, see manpage.

The objective of the game is to type three numbers (each number has between 4
and 6 digits) as fast as possible, digit by digit. For each number, you type
"tabrace " then press Tab (Tab) to see digits between 0 to 9. There's an 'x'
next to the correct digit. Type the correct digit and press Tab (Tab) again to
see the next digit. After all digits are typed, press Enter to clock in your
time for the number. Your points will be determined from how fast you typed the
number. Do the same for the rest of the numbers to get the total score.

I'm ready to play! Are you? Type "tabrace" and press Tab (Tab) to start.

_
        exit 0;
    } elsif ($State->{state} eq 'play' && @ARGV) {
        require Lingua::EN::Numbers::Ordinate;

        $State->{num_completed_numbers}++;
        $State->{end_time} = time();
        $State->{start_time} //= $State->{end_time};
        my $dur = sprintf("%.3f", $State->{end_time}-$State->{start_time});
        my $nth = ($State->{num_completed_numbers} >= 3 ? "last" :
                       Lingua::EN::Numbers::Ordinate::ordinate($State->{num_completed_numbers}));
        if ($ARGV[0] eq $State->{current_number}) {
            print "You typed the $nth number in $dur sec(s).";
            my $good_time = 3.5+length($State->{current_number});
            my $bonus = 0;
            if ($dur <= $good_time) {
                $bonus = 50;
                print " Good job!";
            } elsif ($dur <= $good_time + 3) {
                $bonus = 10;
                print " Not bad, try better next time.";
            } else {
                print " Too slow, really try better next time.";
            }
            my $point = $bonus +
                int(100 + 500/($dur+1)*length($State->{current_number})/4);
            print " You get $point points.\n\n";
            $State->{score} += $point;
        } else {
            print "You typed the $nth number incorrectly, the correct number is $State->{current_number}.\n\n";
        }
        if ($State->{num_completed_numbers} >= 3) {
            $State->{score} //= 0;
            print "Game over. Your total score for this game: $State->{score}.\n\n";
            enter_high_scores();
            print_high_scores();
            print "To play another game, press 'tabrace <tab>' again.\n";
            $State->{num_completed_numbers} = 0;
            $State->{score} = 0;
            gen_number();
        } else {
            print "Ready for the next number? Type 'tabrace <tab>' again.\n";
            gen_number();
        }
        write_data_file();
    } else {
        print <<'_';

You are in a game play. Type "tabrace" and press Tab (Tab) to continue playing.
Or, if you want to reset the game, type "tabrace --reset-game" and press Enter.

_
        exit 0;

    }

} # outside tab completion

# ABSTRACT: Type numbers digit by digit, as fast as you can
# PODNAME: tabrace

__END__

=pod

=encoding UTF-8

=head1 NAME

tabrace - Type numbers digit by digit, as fast as you can

=head1 VERSION

This document describes version 0.010 of tabrace (from Perl distribution Games-TabRace), released on 2021-09-21.

=head1 SYNOPSIS

To start playing the game:

 % tabrace <enter>

To type a number (type digit marked by x):

 % tabrace <tab>
 0    1    2    3    4x   5    6    7    8    9
 % tabrace 4<tab>
 40    41    42    43    44   45    46    47x    48    49
 % tabrace 47<tab>
 470x    471    472    473    474   475    476    477    478    479
 % tabrace 470<tab>
 4700    4701    4702    4703    4704   4705    4706    4707    4708x    4709
 % tabrace 4708<tab>
 % tabrace 4708 _<enter>

A demo screencast:

=for html <img src="https://st.aticpan.org/source/PERLANCAR/Games-TabRace-0.010/share/images/screencast1.gif" />


=head1 DESCRIPTION

Welcome to tabrace, a game played with tab completion. You must first enable tab
completion for your shell. Here's how to do it in the various shells:

=over

=item * bash

 % complete -C tabrace tabrace

Or alternatively, install L<shcompgen> from CPAN using C<cpanm -n
App::shcompgen>.

=item * tcsh

 % complete tabrace 'p/*/`tabrace`/'

Or alternatively, install L<shcompgen> from CPAN using C<cpanm -n
App::shcompgen>.

=item * zsh

Put a file named C<_tabrace> containing the text below somewhere to your
C<fpath>:

 #compdef tabrace
 _tabrace() {
   si=$IFS
   compadd -- $(COMP_LINE=$BUFFER COMP_POINT=$CURSOR tabrace)
   IFS=$si
 }
 _tabrace "$@"

Or alternatively, install L<shcompgen> from CPAN using C<cpanm -n
App::shcompgen>.

=item * fish

 % complete -c tabrace -f -a '(begin; set -lx COMP_SHELL fish; set -lx COMP_LINE (commandline); set -lx COMP_POINT (commandline -C); tabrace; end)'

=back

The objective of the game is to type 3 numbers as fast as possible. For each
number, you type "tabrace " then press Tab (Tab) to see digits between 0 to 9.
There's an 'x' next to the correct digit. Type the correct digit and press Tab
(Tab) again to see the next digit. After all digits are typed, press Enter to
clock in your time for the number. Your points will be determined from how fast
you typed the number. Do the same for the rest of the numbers to get the total
score.

=head1 OPTIONS

=head2 --help

Display help and exit.

=head2 --version

Display version and exit.

=head2 --reset-game

Reset game.

=head2 --reset-high-scores

Reset high scores.

=head2 --high-scores

Show high scores and exit.

=head1 HOMEPAGE

Please visit the project's homepage at L<https://metacpan.org/release/Games-TabRace>.

=head1 SOURCE

Source repository is at L<https://github.com/perlancar/perl-Games-TabRace>.

=head1 SEE ALSO

This game serves as a demo of the L<Complete> module family, including
L<Complete::Bash>, L<Complete::Util>, and so on.

Other games played by tab completion: L<Games::TabNoun>.

=head1 AUTHOR

perlancar <perlancar@cpan.org>

=head1 CONTRIBUTING


To contribute, you can send patches by email/via RT, or send pull requests on
GitHub.

Most of the time, you don't need to build the distribution yourself. You can
simply modify the code, then test via:

 % prove -l

If you want to build the distribution (e.g. to try to install it locally on your
system), you can install L<Dist::Zilla>,
L<Dist::Zilla::PluginBundle::Author::PERLANCAR>, and sometimes one or two other
Dist::Zilla plugin and/or Pod::Weaver::Plugin. Any additional steps required
beyond that are considered a bug and can be reported to me.

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2021, 2016 by perlancar <perlancar@cpan.org>.

This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.

=head1 BUGS

Please report any bugs or feature requests on the bugtracker website L<https://rt.cpan.org/Public/Dist/Display.html?Name=Games-TabRace>

When submitting a bug or request, please include a test-file or a
patch to an existing test-file that illustrates the bug or desired
feature.

=cut
