The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.
package Game::Battleship::Player;
$VERSION = '0.0502';
use strict;
use warnings;
use Carp;
use Game::Battleship::Craft;
use Game::Battleship::Grid;

sub new {
    my ($proto, %args) = @_;
    my $class = ref ($proto) || $proto;

    my $self = {
        id    => $args{id}    || undef,
        name  => $args{name}  || undef,
        score => $args{score} || 0,
        life  => $args{life}  || 0,
        fleet => $args{fleet} || [
            Game::Battleship::Craft->new(
                name   => 'aircraft carrier',
                points => 5,
            ),
            Game::Battleship::Craft->new(
                name   => 'battleship',
                points => 4,
            ),
            Game::Battleship::Craft->new(
                name   => 'cruiser',
                points => 3,
            ),
            Game::Battleship::Craft->new(
                name   => 'submarine',
                points => 3,
            ),
            Game::Battleship::Craft->new(
                name   => 'destroyer',
                points => 2,
            ),
        ],
    };

    bless $self, $class;
    $self->_init(\%args);
    return $self;
}

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

    # Initialize a grid and place the player's fleet, if one is
    # provided.
    $self->{grid} = Game::Battleship::Grid->new(
        fleet => $self->{fleet},
        dimensions => $args->{dimensions},
    );

    # Compute the life points for this player.
    $self->{life} += $_->{points} for @{ $self->{fleet} };
}

sub name { return shift->{name} }

# The enemy must be a G::B::Player object.
sub grid {
    my ($self, $enemy) = @_;
    return $enemy
        ? join "\n",
            map { "@$_" } @{ $self->{$enemy->{name}}{matrix} }
        : join "\n",
            map { "@$_" } @{ $self->{grid}{matrix} };
}

# The enemy must be a G::B::Player object.
sub strike {
    my ($self, $enemy, $x, $y) = @_;

    croak "No opponent to strike.\n" unless $enemy;
    croak "No coordinate at which to strike.\n"
        unless defined $x && defined $y;

    if ($enemy->{life} > 0) {
        # Initialize the enemy grid map if we need to.
        $self->{$enemy->{name}} = Game::Battleship::Grid->new
            unless exists $self->{$enemy->{name}};

        my $enemy_pos = \$enemy->{grid}{matrix}[$x][$y];
        my $map_pos   = \$self->{$enemy->{name}}{matrix}[$x][$y];

        if ($$map_pos ne '.') {
            warn "Duplicate strike on $enemy->{name} by $self->{name} at $x, $y.\n";
            return -1;
        }
        elsif ($enemy->_is_a_hit($x, $y)) { # Set the enemy grid map coordinate char to 'hit'.
            $$map_pos = 'x';

            # What craft was hit?
            my $craft = $self->craft(id => $$enemy_pos);

            warn "$self->{name} hit $enemy->{name}'s $craft->{name}!\n";

            # How much is left on this craft?
            my $remainder = $craft->hit;

            # Tally the hit in the craft object, itself and emit a happy
            # warning if it was totally destroyed.
            warn "$self->{name} sunk $enemy->{name}'s $craft->{name}!\n"
                unless $remainder;

            # Indicate the hit on the enemy grid by lowercasing the craft
            # id.
            $$enemy_pos = lc $$enemy_pos;

            # Increment the player's score.
            $self->{score}++;

            # Decrement the opponent's life.
            warn "$enemy->{name} is out of the game.\n"
                if --$enemy->{life} <= 0;

            return 1;
        }
        else {
            # Set the enemy grid map coordinate char to 'miss'.
            warn "$self->{name} missed $enemy->{name} at $x, $y.\n";
            $$map_pos = 'o';
            return 0;
        }
    }
    else {
        warn "$enemy->{name} is already out of the game. Strike another opponent.\n";
        return -1;
    }
}

sub _is_a_hit {
    my ($self, $x, $y) = @_;
    return $self->{grid}{matrix}[$x][$y] ne '.'
        ? 1 : 0;
}

sub craft {
    my ($self, $key, $val) = @_;

    # If the key is not defined, assume it's supposed to be the id.
    unless (defined $val) {
        $val = $key;
        $key = 'id';
    }

    my $craft;

    for (@{ $self->{fleet} }) {
        if ($val eq $_->{$key}) {
            $craft = $_;
            last;
        }
    }

    return $craft;
}

1;

__END__

=head1 NAME

Game::Battleship::Player - A Battleship player class

=head1 SYNOPSIS

  use Game::Battleship::Player;
  my $aaron = Game::Battleship::Player->new(name => 'Aaron');
  my $gene  = Game::Battleship::Player->new(name => 'Gene');
  print 'Player 1: ', $aaron->name, "\n",
        'Player 2: ', $gene->name,  "\n";
  $aaron->strike($gene, 0, 0);
  # Repeat and get a duplicate strike warning.
  my $strike = $aaron->strike($gene, 0, 0);
  print $aaron->grid($gene), "\nThat was a " .
    ( $strike == 1 ? 'hit!'
    : $strike == 0 ? 'miss.'
                   : 'duplicate?' ), "\n";
  my $craft_obj = $aaron->craft($id);

=head1 DESCRIPTION

A C<Game::Battleship::Player> object represents a Battleship player
complete with fleet and game surface.

=head1 PUBLIC METHODS

=over 4

=item B<new> %ARGUMENTS

  $player => Game::Battleship::Player->new(
      name  => 'Aaron',
      fleet => \@fleet,
      dimensions => [$x, $y],
  );

=over 4

=item * name => $STRING

An optional attribute provided to give the player a name.

If not provided, the string "player_1" or "player_2" is used.

=item * fleet => [$CRAFT_1, $CRAFT_2, ... $CRAFT_N]

Array reference of C<Game::Battleship::Craft> objects.

If not explicitly provided, the standard fleet (with 5 ships) is
created by default.

=item * dimensions => [$WIDTH, $HEIGHT]

Array reference with the player's grid height and width values.

If the grid dimensions are not explicitly specified, the standard
ten by ten grid is used.

=back

=item B<grid>

  $grid = $player->grid();
  $grid = $player->grid($enemy);

Return the playing grid as a "flush-left" text matrix like this:

  . . . . . . . . . .
  . . . . . . . . . .
  . . . . . . . . . .
  . . . S S S . . . .
  . . . . . . C . . .
  . . . . . A C . . .
  . D . . . A C . . B
  . D . . . A . . . B
  . . . . . A . . . B
  . . . . . A . . . B

Eventually, this method will respect the game type and return an
appropriate representation, such as a PNG file or XML, etc.

=item B<strike> $PLAYER, @COORDINATE

  $strike = $player->strike($enemy, $x, $y);

Strike the enemy at the given coordinate and return a numeric value
to indicate success or failure.

The player to strike must be given as a C<Game::Battleship::Player>
object and the coordinate must be given as a numeric pair.

On success, an "x" is placed on the striking player's "opponent map
grid" (a C<Game::Battleship::Grid> object attribute named for the
opponent) at the given coordinate, the opponent's "craft grid" is
updated by lower-casing the C<Game::Battleship::Craft> object C<id>
at the given coordinate, the opponent C<Game::Battleship::Craft>
object C<hits> attribute is incremented, the striking player's
C<score> attribute is incremented, and a one (i.e. true) is returned.

If an enemy craft is completely destroyed, a happy warning is emitted.

On failure, an "o" is placed on the striking player's "opponent map
grid" at the given coordinate and a zero (i.e. false) is returned.

If a player calls for a strike at a coordinate that was already
struck, a warning is emitted and a negative one (-1) is returned.

=item B<craft> $KEY [$VALUE]

  $craft = $player->craft($id);
  $craft = $player->craft(id => $id);
  $craft = $player->craft(name => $name);

Return the player's C<Game::Battleship::Craft> object that matches
the given argument(s).

If the last argument is not provided the first argument is assumed to
be the C<id> attribute.

=back

=head1 PRIVATE METHODS

=over 4

=item B<_is_a_hit> @COORDINATE

Return true or false if another player's strike is successful.  That
is, return a one if there is a craft at the given coordinate and zero
otherwise.

=back

=head1 TO DO

Include a weapon argument in the C<strike> method.

Make the C<grid> method honor the game type and return something
appropriate.

=head1 SEE ALSO

L<Game::Battleship>,
L<Game::Battleship::Craft>,
L<Game::Battleship::Grid>

=head1 AUTHOR

Gene Boggs E<lt>gene@cpan.orgE<gt>

=head1 COPYRIGHT AND LICENSE

See L<Game::Battleship>.

=cut