3
0
mirror of https://github.com/pragma-/pbot.git synced 2024-11-09 05:29:33 +01:00
pbot/Plugins/Battleship.pm
Pragmatic Software 976bfcb5e7 Refactor commands to begin with cmd_ and take a $context object
Move all command subroutines closer to top of source file
Do not send WHO to non-chanop channels
Minor misc bugfixes and improvements
2020-05-04 13:21:35 -07:00

1105 lines
44 KiB
Perl

# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
package Plugins::Battleship;
use parent 'Plugins::Plugin';
use warnings; use strict;
use feature 'unicode_strings';
use utf8;
use feature 'switch';
no if $] >= 5.018, warnings => "experimental::smartmatch";
use Time::Duration qw/concise duration/;
use Data::Dumper;
$Data::Dumper::Useqq = 1;
$Data::Dumper::Sortkeys = 1;
sub initialize {
my ($self, %conf) = @_;
$self->{pbot}->{commands}->register(sub { $self->cmd_battleship(@_) }, 'battleship', 0);
$self->{pbot}->{event_dispatcher}->register_handler('irc.part', sub { $self->on_departure(@_) });
$self->{pbot}->{event_dispatcher}->register_handler('irc.quit', sub { $self->on_departure(@_) });
$self->{pbot}->{event_dispatcher}->register_handler('irc.kick', sub { $self->on_kick(@_) });
$self->{channel} = $self->{pbot}->{registry}->get_value('battleship', 'channel') // '##battleship';
$self->{debug} = $self->{pbot}->{registry}->get_value('battleship', 'debug') // 0;
$self->{player_one_vert} = '|';
$self->{player_one_horiz} = '—';
$self->{player_two_vert} = 'I';
$self->{player_two_horiz} = '=';
$self->create_states;
}
sub unload {
my $self = shift;
$self->{pbot}->{commands}->unregister('battleship');
$self->{pbot}->{timer}->dequeue_event('battleship loop');
$self->{pbot}->{event_dispatcher}->remove_handler('irc.part');
$self->{pbot}->{event_dispatcher}->remove_handler('irc.quit');
$self->{pbot}->{event_dispatcher}->remove_handler('irc.kick');
}
sub on_kick {
my ($self, $event_type, $event) = @_;
my ($nick, $user, $host) = ($event->{event}->nick, $event->{event}->user, $event->{event}->host);
my ($victim, $reason) = ($event->{event}->to, $event->{event}->{args}[1]);
my $channel = $event->{event}->{args}[0];
return 0 if lc $channel ne $self->{channel};
$self->player_left($nick, $user, $host);
return 0;
}
sub on_departure {
my ($self, $event_type, $event) = @_;
my ($nick, $user, $host, $channel) = ($event->{event}->nick, $event->{event}->user, $event->{event}->host, $event->{event}->to);
my $type = uc $event->{event}->type;
return 0 if $type ne 'QUIT' and lc $channel ne $self->{channel};
$self->player_left($nick, $user, $host);
return 0;
}
my %color = (
white => "\x0300",
black => "\x0301",
blue => "\x0302",
green => "\x0303",
red => "\x0304",
maroon => "\x0305",
purple => "\x0306",
orange => "\x0307",
yellow => "\x0308",
lightgreen => "\x0309",
teal => "\x0310",
cyan => "\x0311",
lightblue => "\x0312",
magneta => "\x0313",
gray => "\x0314",
lightgray => "\x0315",
bold => "\x02",
italics => "\x1D",
underline => "\x1F",
reverse => "\x16",
reset => "\x0F",
);
sub cmd_battleship {
my ($self, $context) = @_;
$context->{arguments} =~ s/^\s+|\s+$//g;
my $usage = "Usage: battleship challenge|accept|bomb|board|score|quit|players|kick|abort; for more information about a command: battleship help <command>";
my ($command, $arguments) = split / /, $context->{arguments}, 2;
$command = lc $command;
my ($channel, $result);
given ($command) {
when ('help') {
given ($arguments) {
when ('help') { return "Seriously?"; }
default {
if (length $arguments) { return "Battleship help is coming soon."; }
else { return "Usage: battleship help <command>"; }
}
}
}
when ('leaderboard') { return "Coming soon."; }
when ('challenge') {
if ($self->{current_state} ne 'nogame') { return "There is already a game of Battleship underway."; }
if (not length $arguments) {
$self->{current_state} = 'accept';
$self->{state_data} = {players => [], counter => 0};
my $id = $self->{pbot}->{messagehistory}->{database}->get_message_account($context->{nick}, $context->{user}, $context->{host});
my $player = {id => $id, name => $context->{nick}, missedinputs => 0};
push @{$self->{state_data}->{players}}, $player;
$player = {id => -1, name => undef, missedinputs => 0};
push @{$self->{state_data}->{players}}, $player;
$self->{pbot}->{timer}->enqueue_event(sub {
$self->run_one_state;
}, 1, 'battleship loop', 1
);
return "/msg $self->{channel} $context->{nick} has made an open challenge! Use `accept` to accept their challenge.";
}
my $challengee = $self->{pbot}->{nicklist}->is_present($self->{channel}, $arguments);
if (not $challengee) { return "That nick is not present in this channel. Invite them to $self->{channel} and try again!"; }
$self->{current_state} = 'accept';
$self->{state_data} = {players => [], counter => 0};
my $id = $self->{pbot}->{messagehistory}->{database}->get_message_account($context->{nick}, $context->{user}, $context->{host});
my $player = {id => $id, name => $context->{nick}, missedinputs => 0};
push @{$self->{state_data}->{players}}, $player;
($id) = $self->{pbot}->{messagehistory}->{database}->find_message_account_by_nick($challengee);
$player = {id => $id, name => $challengee, missedinputs => 0};
push @{$self->{state_data}->{players}}, $player;
$self->{pbot}->{timer}->enqueue_event(sub {
$self->battleship_loop;
}, 1, 'battleship loop', 1
);
return "/msg $self->{channel} $context->{nick} has challenged $challengee to Battleship! Use `accept` to accept their challenge.";
}
when ('accept') {
if ($self->{current_state} ne 'accept') { return "/msg $context->{nick} This is not the time to use `accept`."; }
my $id = $self->{pbot}->{messagehistory}->{database}->get_message_account($context->{nick}, $context->{user}, $context->{host});
my $player = $self->{state_data}->{players}->[1];
# open challenge
if ($player->{id} == -1) {
$player->{id} = $id;
$player->{name} = $context->{nick};
}
if ($player->{id} == $id) {
$player->{accepted} = 1;
return "/msg $self->{channel} $context->{nick} has accepted $self->{state_data}->{players}->[0]->{name}'s challenge!";
} else {
return "/msg $context->{nick} You have not been challenged to a game of Battleship yet.";
}
}
when ($_ eq 'decline' or $_ eq 'quit' or $_ eq 'forfeit' or $_ eq 'concede') {
my $id = $self->{pbot}->{messagehistory}->{database}->get_message_account($context->{nick}, $context->{user}, $context->{host});
my $removed = 0;
for (my $i = 0; $i < @{$self->{state_data}->{players}}; $i++) {
if ($self->{state_data}->{players}->[$i]->{id} == $id) {
$self->{state_data}->{players}->[$i]->{removed} = 1;
$removed = 1;
}
}
if ($removed) {
if ($self->{state_data}->{current_player} >= @{$self->{state_data}->{players}}) { $self->{state_data}->{current_player} = @{$self->{state_data}->{players}} - 1 }
if (@{$self->{state_data}->{players}} == 2 and ($self->{state_data}->{players}->[1]->{id} == -1 || not $self->{state_data}->{players}->[1]->{accepted})) {
return "/msg $self->{channel} $context->{nick} declined the challenge.";
} else {
return "/msg $self->{channel} $context->{nick} has left the game!";
}
} else {
return "$context->{nick}: But you are not even playing the game.";
}
}
when ('abort') {
if (not $self->{pbot}->{users}->loggedin_admin($self->{channel}, $context->{hostmask})) {
return "$context->{nick}: Only admins may abort the game.";
}
$self->{current_state} = 'gameover';
return "/msg $self->{channel} $context->{nick}: The game has been aborted.";
}
when ('score') {
if (@{$self->{state_data}->{players}} == 2) {
$self->show_scoreboard;
return;
} else {
return "There is no game going on right now.";
}
}
when ('players') {
if ($self->{current_state} eq 'accept') { return "$self->{state_data}->{players}->[0]->{name} has challenged $self->{state_data}->{players}->[1]->{name}!"; }
elsif (@{$self->{state_data}->{players}} == 2) { return "$self->{state_data}->{players}->[0]->{name} is in battle with $self->{state_data}->{players}->[1]->{name}!"; }
else { return "There are no players playing right now. Start a game with `battleship challenge <nick>`!"; }
}
when ('kick') {
if (not $self->{pbot}->{users}->loggedin_admin($self->{channel}, $context->{hostmask})) {
return "$context->{nick}: Only admins may kick people from the game.";
}
if (not length $arguments) { return "Usage: battleship kick <nick>"; }
my $removed = 0;
for (my $i = 0; $i < @{$self->{state_data}->{players}}; $i++) {
if (lc $self->{state_data}->{players}->[$i]->{name} eq $arguments) {
$self->{state_data}->{players}->[$i]->{removed} = 1;
$removed = 1;
}
}
if ($removed) {
if ($self->{state_data}->{current_player} >= @{$self->{state_data}->{players}}) { $self->{state_data}->{current_player} = @{$self->{state_data}->{players}} - 1 }
return "/msg $self->{channel} $context->{nick}: $arguments has been kicked from the game.";
} else {
return "$context->{nick}: $arguments isn't even in the game.";
}
}
when ('bomb') {
if ($self->{debug}) { $self->{pbot}->{logger}->log("Battleship: bomb state: $self->{current_state}\n" . Dumper $self->{state_data}); }
if ($self->{current_state} ne 'playermove' and $self->{current_state} ne 'checkplayer') { return "$context->{nick}: It's not time to do that now."; }
my $id = $self->{pbot}->{messagehistory}->{database}->get_message_account($context->{nick}, $context->{user}, $context->{host});
my $player;
if ($self->{state_data}->{players}->[0]->{id} == $id) { $player = 0; }
elsif ($self->{state_data}->{players}->[1]->{id} == $id) { $player = 1; }
else { return "You are not playing in this game."; }
if (not length $arguments) {
if (delete $self->{state_data}->{players}->[$player]->{location}) { return "$context->{nick}: Attack location cleared."; }
else { return "$context->{nick}: Usage: bomb <location>"; }
}
if ($arguments !~ m/^[a-zA-Z][0-9]+$/) { return "$context->{nick}: Usage: battleship bomb <location>; <location> must be in the form of A15, B3, C9, etc."; }
$arguments = uc $arguments;
my ($x, $y);
($x) = $arguments =~ m/^(.)/;
($y) = $arguments =~ m/^.(.*)/;
$x = ord($x) - 65;
if ($x < 0 || $x > $self->{N_Y} || $y < 0 || $y > $self->{N_X}) { return "$context->{nick}: Target out of range, try again."; }
if ($self->{state_data}->{current_player} != $player) {
my $msg;
if (not exists $self->{state_data}->{players}->[$player]->{location}) { $msg = "$context->{nick}: You will attack $arguments when it is your turn."; }
else { $msg = "$context->{nick}: You will now attack $arguments instead of $self->{state_data}->{players}->[$player]->{location} when it is your turn."; }
$self->{state_data}->{players}->[$player]->{location} = $arguments;
return $msg;
}
if ($self->{player}->[$player]->{done}) { return "$context->{nick}: You have already attacked this turn."; }
if ($self->bomb($player, uc $arguments)) {
if ($self->{player}->[$player]->{won}) {
$self->{previous_state} = $self->{current_state};
$self->{current_state} = 'checkplayer';
$self->run_one_state;
} else {
$self->{player}->[$player]->{done} = 1;
$self->{player}->[!$player]->{done} = 0;
$self->{state_data}->{current_player} = !$player;
$self->{state_data}->{ticks} = 1;
$self->{state_data}->{first_tock} = 1;
$self->{state_data}->{counter} = 0;
}
}
}
when ($_ eq 'specboard' or $_ eq 'board') {
if ($self->{current_state} eq 'nogame' or $self->{current_state} eq 'accept' or $self->{current_state} eq 'genboard' or $self->{current_state} eq 'gameover') {
return "$context->{nick}: There is no board to show right now.";
}
if ($_ eq 'specboard') {
$self->show_battlefield(2);
return;
}
my $id = $self->{pbot}->{messagehistory}->{database}->get_message_account($context->{nick}, $context->{user}, $context->{host});
for (my $i = 0; $i < 2; $i++) {
if ($self->{state_data}->{players}->[$i]->{id} == $id) {
$self->send_message($self->{channel}, "$context->{nick} surveys the battlefield!");
$self->show_battlefield($i);
return;
}
}
$self->show_battlefield(2);
}
when ('fullboard') {
if (not $self->{pbot}->{users}->loggedin_admin($self->{channel}, $context->{hostmask})) {
return "$context->{nick}: Only admins may see the full board.";
}
if ($self->{current_state} eq 'nogame' or $self->{current_state} eq 'accept' or $self->{current_state} eq 'genboard' or $self->{current_state} eq 'gameover') {
return "$context->{nick}: There is no board to show right now.";
}
# show real board if admin is actually in the game ... no cheating!
my $id = $self->{pbot}->{messagehistory}->{database}->get_message_account($context->{nick}, $context->{user}, $context->{host});
for (my $i = 0; $i < 2; $i++) {
if ($self->{state_data}->{players}->[$i]->{id} == $id) {
$self->send_message($self->{channel}, "$context->{nick} surveys the battlefield!");
$self->show_battlefield($i);
return;
}
}
$self->show_battlefield(4, $context->{nick});
}
default { return $usage; }
}
return $result;
}
sub player_left {
my ($self, $nick, $user, $host) = @_;
my $id = $self->{pbot}->{messagehistory}->{database}->get_message_account($nick, $user, $host);
my $removed = 0;
for (my $i = 0; $i < @{$self->{state_data}->{players}}; $i++) {
if ($self->{state_data}->{players}->[$i]->{id} == $id) {
$self->{state_data}->{players}->[$i]->{removed} = 1;
$self->send_message($self->{channel}, "$nick has left the game!");
$removed = 1;
}
}
if ($removed) {
if ($self->{state_data}->{current_player} >= @{$self->{state_data}->{players}}) { $self->{state_data}->{current_player} = @{$self->{state_data}->{players}} - 1 }
return "/msg $self->{channel} $nick has left the game!";
}
}
sub send_message {
my ($self, $to, $text, $delay) = @_;
$delay = 0 if not defined $delay;
my $botnick = $self->{pbot}->{registry}->get_value('irc', 'botnick');
my $message = {
nick => $botnick, user => 'battleship', host => 'localhost', command => 'battleship text', checkflood => 1,
message => $text
};
$self->{pbot}->{interpreter}->add_message_to_output_queue($to, $message, $delay);
}
sub run_one_state {
my $self = shift;
# check for naughty or missing players
if ($self->{current_state} =~ /(?:move|accept)/) {
my $removed = 0;
for (my $i = 0; $i < @{$self->{state_data}->{players}}; $i++) {
if ($self->{state_data}->{players}->[$i]->{missedinputs} >= 3) {
$self->send_message(
$self->{channel},
"$color{red}$self->{state_data}->{players}->[$i]->{name} has missed too many prompts and has been ejected from the game!$color{reset}"
);
$self->{state_data}->{players}->[$i]->{removed} = 1;
$removed = 1;
}
}
if ($removed) {
if ($self->{state_data}->{current_player} >= @{$self->{state_data}->{players}}) { $self->{state_data}->{current_player} = @{$self->{state_data}->{players}} - 1 }
}
if ($self->{state_data}->{players}->[0]->{removed} or $self->{state_data}->{players}->[1]->{removed}) { $self->{current_state} = 'gameover'; }
}
my $state_data = $self->{state_data};
# this shouldn't happen
if (not defined $self->{current_state}) {
$self->{pbot}->{logger}->log("Battleship state broke.\n");
$self->{current_state} = 'nogame';
return;
}
# transistioned to a brand new state; prepare first tock
if ($self->{previous_state} ne $self->{current_state}) {
$state_data->{newstate} = 1;
$state_data->{ticks} = 1;
if (exists $state_data->{tick_drift}) {
$state_data->{ticks} += $state_data->{tick_drift};
delete $state_data->{tick_drift};
}
$state_data->{first_tock} = 1;
$state_data->{counter} = 0;
} else {
$state_data->{newstate} = 0;
}
# dump new state data for logging/debugging
if ($self->{debug} and $state_data->{newstate}) { $self->{pbot}->{logger}->log("Battleship: New state: $self->{current_state}\n" . Dumper $state_data); }
# run one state/tick
$state_data = $self->{states}{$self->{current_state}}{sub}($state_data);
if ($state_data->{tocked}) {
delete $state_data->{tocked};
delete $state_data->{first_tock};
$state_data->{ticks} = 0;
}
# transform to next state
$state_data->{previous_result} = $state_data->{result};
$self->{previous_state} = $self->{current_state};
$self->{current_state} = $self->{states}{$self->{current_state}}{trans}{$state_data->{result}};
$self->{state_data} = $state_data;
# next tick
$self->{state_data}->{ticks}++;
}
sub create_states {
my $self = shift;
$self->{pbot}->{logger}->log("Battleship: Creating game state machine\n");
$self->{previous_state} = '';
$self->{current_state} = 'nogame';
$self->{state_data} = {players => [], ticks => 0, newstate => 1};
$self->{state_data}->{current_player} = 0;
$self->{states}{'nogame'}{sub} = sub { $self->nogame(@_) };
$self->{states}{'nogame'}{trans}{challenge} = 'accept';
$self->{states}{'nogame'}{trans}{nogame} = 'nogame';
$self->{states}{'accept'}{sub} = sub { $self->accept(@_) };
$self->{states}{'accept'}{trans}{stop} = 'nogame';
$self->{states}{'accept'}{trans}{wait} = 'accept';
$self->{states}{'accept'}{trans}{accept} = 'genboard';
$self->{states}{'genboard'}{sub} = sub { $self->genboard(@_) };
$self->{states}{'genboard'}{trans}{next} = 'showboard';
$self->{states}{'showboard'}{sub} = sub { $self->showboard(@_) };
$self->{states}{'showboard'}{trans}{next} = 'playermove';
$self->{states}{'playermove'}{sub} = sub { $self->playermove(@_) };
$self->{states}{'playermove'}{trans}{wait} = 'playermove';
$self->{states}{'playermove'}{trans}{next} = 'checkplayer';
$self->{states}{'checkplayer'}{sub} = sub { $self->checkplayer(@_) };
$self->{states}{'checkplayer'}{trans}{sunk} = 'gameover';
$self->{states}{'checkplayer'}{trans}{next} = 'playermove';
$self->{states}{'gameover'}{sub} = sub { $self->gameover(@_) };
$self->{states}{'gameover'}{trans}{wait} = 'gameover';
$self->{states}{'gameover'}{trans}{next} = 'nogame';
}
# battleship stuff
sub init_game {
my ($self, $nick1, $nick2) = @_;
$self->{N_X} = 15;
$self->{N_Y} = 8;
$self->{SHIPS} = 6;
for (my $x = 0; $x < $self->{SHIPS}; $x++) { $self->{ship_length}->[$x] = 0; }
$self->{board} = [];
$self->{player} = [
{bombs => 0, hit => 0, miss => 0, sunk => 0, nick => $nick1, done => 0},
{bombs => 0, hit => 0, miss => 0, sunk => 0, nick => $nick2, done => 0}
];
$self->{turn} = 0;
$self->{horiz} = 0;
$self->generate_battlefield;
}
sub count_ship_sections {
my ($self, $player) = @_;
my ($x, $y, $sections);
$sections = 0;
for ($x = 0; $x < $self->{N_Y}; $x++) {
for ($y = 0; $y < $self->{N_X}; $y++) {
if ($player == 0) {
if ($self->{board}->[$x][$y] eq $self->{player_two_vert} || $self->{board}->[$x][$y] eq $self->{player_two_horiz}) { $sections++; }
} else {
if ($self->{board}->[$x][$y] eq $self->{player_one_vert} || $self->{board}->[$x][$y] eq $self->{player_one_horiz}) { $sections++; }
}
}
}
return $sections;
}
sub check_ship {
my ($self, $x, $y, $o, $d, $l) = @_;
my ($xd, $yd, $i);
if (!$o) {
if (!$d) {
$yd = -1;
if ($y - $l < 0) { return 0; }
} else {
$yd = 1;
if ($y + $l >= $self->{N_X}) { return 0; }
}
$xd = 0;
} else {
if (!$d) {
$xd = -1;
if ($x - $l < 0) { return 0; }
} else {
$xd = 1;
if ($x + $l >= $self->{N_Y}) { return 0; }
}
$yd = 0;
}
for (my $i = 0; $i < $l; $i++) {
if ($self->{board}->[$x += $o ? $xd : 0][$y += $o ? 0 : $yd] ne '~') { return 0; }
}
return 1;
}
sub number {
my ($self, $lower, $upper) = @_;
return int(rand($upper - $lower)) + $lower;
}
sub generate_ship {
my ($self, $player, $ship) = @_;
my ($x, $y, $o, $d, $i, $l);
my ($yd, $xd) = (0, 0);
my $fail = 0;
while (1) {
$x = $self->number(0, $self->{N_Y});
$y = $self->number(0, $self->{N_X});
$o = $self->number(1, 10) < 6;
$d = $self->number(1, 10) < 6;
if (not $self->{ship_length}->[$ship]) { $l = $self->number(3, 6); }
else { $l = $self->{ship_length}->[$ship]; }
$self->{pbot}->{logger}->log("generate ships player $player: ship $ship x,y: $x,$y o,d: $o,$d length: $l\n");
if ($self->check_ship($x, $y, $o, $d, $l)) {
if (!$o) {
if ($self->{horiz} < 2) { next; }
if (!$d) { $yd = -1; }
else { $yd = 1; }
$xd = 0;
} else {
$self->{horiz}++;
if (!$d) { $xd = -1; }
else { $xd = 1; }
$yd = 0;
}
for (my $i = 0; $i < $l; $i++) {
$self->{board}->[$x += $o ? $xd : 0][$y += $o ? 0 : $yd] =
$player ? ($o ? $self->{player_two_vert} : $self->{player_two_horiz}) : ($o ? $self->{player_one_vert} : $self->{player_one_horiz});
}
$self->{ship_length}->[$ship] = $l;
return 1;
}
if (++$fail >= 5000) {
$self->{pbot}->{logger}->log("Failed to generate ship\n");
$self->send_message($self->{channel}, "Failed to place a ship. I cannot continue. Game over.");
$self->{current_state} = 'nogame';
return 0;
}
}
}
sub generate_battlefield {
my ($self) = @_;
my ($x, $y);
for ($y = 0; $y < $self->{N_Y}; $y++) {
for ($x = 0; $x < $self->{N_X}; $x++) { $self->{board}->[$y][$x] = '~'; }
}
for ($x = 0; $x < $self->{SHIPS}; $x++) {
if (!$self->generate_ship(0, $x) || !$self->generate_ship(1, $x)) { return 0; }
}
return 1;
}
sub check_sunk {
my ($self, $x, $y, $player) = @_;
my ($i, $target);
$target = $self->{board}->[$x][$y];
given ($target) {
when ($_ eq $self->{player_two_vert} or $_ eq $self->{player_one_vert}) {
for ($i = $x + 1; $i < $self->{N_Y}; $i++) {
if (($self->{board}->[$i][$y] eq $self->{player_one_vert} && $player) || ($self->{board}->[$i][$y] eq $self->{player_two_vert} && !$player)) { return 0; }
if ($self->{board}->[$i][$y] eq '~' || $self->{board}->[$i][$y] eq '*' || $self->{board}->[$i][$y] eq 'o') { last; }
}
for ($i = $x - 1; $i >= 0; $i--) {
if (($self->{board}->[$i][$y] eq $self->{player_one_vert} && $player) || ($self->{board}->[$i][$y] eq $self->{player_two_vert} && !$player)) { return 0; }
if ($self->{board}->[$i][$y] eq '~' || $self->{board}->[$i][$y] eq '*' || $self->{board}->[$i][$y] eq 'o') { last; }
}
return 1;
}
when ($_ eq $self->{player_one_horiz} or $_ eq $self->{player_two_horiz}) {
for ($i = $y + 1; $i < $self->{N_X}; $i++) {
if (($self->{board}->[$x][$i] eq $self->{player_one_horiz} && $player) || ($self->{board}->[$x][$i] eq $self->{player_two_horiz} && !$player)) { return 0; }
if ($self->{board}->[$x][$i] eq '~' || $self->{board}->[$x][$i] eq '*' || $self->{board}->[$x][$i] eq 'o') { last; }
}
for ($i = $y - 1; $i >= 0; $i--) {
if (($self->{board}->[$x][$i] eq $self->{player_one_horiz} && $player) || ($self->{board}->[$x][$i] eq $self->{player_two_horiz} && !$player)) { return 0; }
if ($self->{board}->[$x][$i] eq '~' || $self->{board}->[$x][$i] eq '*' || $self->{board}->[$x][$i] eq 'o') { last; }
}
return 1;
}
}
}
sub bomb {
my ($self, $player, $location) = @_;
my ($x, $y, $hit, $sections, $sunk) = (0, 0, 0, 0, 0);
$location = uc $location;
($x) = $location =~ m/^(.)/;
($y) = $location =~ m/^.(.*)/;
$x = ord($x) - 65;
$self->{pbot}->{logger}->log("bomb player $player $x,$y $self->{board}->[$x][$y]\n");
if ($x < 0 || $x > $self->{N_Y} || $y < 0 || $y > $self->{N_X}) {
$self->send_message($self->{channel}, "Target out of range, try again.");
return 0;
}
$y--;
if (!$player) {
if ($self->{board}->[$x][$y] eq $self->{player_two_vert} || $self->{board}->[$x][$y] eq $self->{player_two_horiz}) { $hit = 1; }
} else {
if ($self->{board}->[$x][$y] eq $self->{player_one_vert} || $self->{board}->[$x][$y] eq $self->{player_one_horiz}) { $hit = 1; }
}
$sunk = $self->check_sunk($x, $y, $player);
if ($hit) {
if (!$player) { $self->{board}->[$x][$y] = '1'; }
else { $self->{board}->[$x][$y] = '2'; }
$self->{player}->[$player]->{hit}++;
} else {
if ($self->{board}->[$x][$y] eq '~') {
if (!$player) { $self->{board}->[$x][$y] = '*'; }
else { $self->{board}->[$x][$y] = 'o'; }
$self->{player}->[$player]->{miss}++;
}
}
my $nick1 = $self->{player}->[$player]->{nick};
my $nick2 = $self->{player}->[$player ? 0 : 1]->{nick};
my @attacks = (
"launches torpedoes at", "launches nukes at", "fires cannons at", "fires torpedoes at", "fires nukes at",
"launches tomahawk missiles at", "fires a gatling gun at", "launches ballistic missiles at"
);
my $attacked = $attacks[rand @attacks];
if ($hit) {
$self->send_message($self->{channel}, "$nick1 $attacked $nick2 at $location! $color{red}--- HIT! --- $color{reset}");
$self->{player}->[$player]->{destroyed}++;
if ($sunk) {
$self->{player}->[$player]->{sunk}++;
my $remaining = $self->count_ship_sections($player);
$self->send_message($self->{channel}, "$color{red}$nick1 has sunk ${nick2}'s ship! $remaining ship section" . ($remaining != 1 ? 's' : '') . " remaining!$color{reset}");
if ($remaining == 0) {
$self->send_message($self->{channel}, "$nick1 has WON the game of Battleship!");
$self->{player}->[$player]->{won} = 1;
}
}
} else {
$self->send_message($self->{channel}, "$nick1 $attacked $nick2 at $location! --- miss ---");
}
$self->{player}->[$player]->{bombs}++;
return 1;
}
sub show_scoreboard {
my ($self) = @_;
my $buf;
my $p1sections = $self->count_ship_sections(1);
my $p2sections = $self->count_ship_sections(0);
my $p1win = "";
my $p2win = "";
if ($p1sections > $p2sections) {
$p1win = "$color{bold}$color{lightgreen} * ";
$p2win = "$color{red} ";
} elsif ($p1sections < $p2sections) {
$p1win = "$color{red} ";
$p2win = "$color{bold}$color{lightgreen} * ";
}
my $length_a = length $self->{player}->[0]->{nick};
my $length_b = length $self->{player}->[1]->{nick};
my $longest = $length_a > $length_b ? $length_a : $length_b;
my $bombslen = ($self->{player}->[0]->{bombs} > 10 || $self->{player}->[1]->{bombs} > 10) ? 2 : 1;
my $hitlen = ($self->{player}->[0]->{hit} > 10 || $self->{player}->[1]->{hit} > 10) ? 2 : 1;
my $misslen = ($self->{player}->[0]->{miss} > 10 || $self->{player}->[1]->{miss} > 10) ? 2 : 1;
my $sunklen = ($self->{player}->[0]->{sunk} > 10 || $self->{player}->[1]->{sunk} > 10) ? 2 : 1;
my $intactlen = ($p1sections > 10 || $p2sections > 10) ? 2 : 1;
my $p1bombscolor = $self->{player}->[0]->{bombs} > $self->{player}->[1]->{bombs} ? $color{green} : $color{red};
my $p1hitcolor = $self->{player}->[0]->{hit} > $self->{player}->[1]->{hit} ? $color{green} : $color{red};
my $p1misscolor = $self->{player}->[0]->{miss} < $self->{player}->[1]->{miss} ? $color{green} : $color{red};
my $p1sunkcolor = $self->{player}->[0]->{sunk} > $self->{player}->[1]->{sunk} ? $color{green} : $color{red};
my $p1intactcolor = $p1sections > $p2sections ? $color{green} : $color{red};
my $p2bombscolor = $self->{player}->[0]->{bombs} < $self->{player}->[1]->{bombs} ? $color{green} : $color{red};
my $p2hitcolor = $self->{player}->[0]->{hit} < $self->{player}->[1]->{hit} ? $color{green} : $color{red};
my $p2misscolor = $self->{player}->[0]->{miss} > $self->{player}->[1]->{miss} ? $color{green} : $color{red};
my $p2sunkcolor = $self->{player}->[0]->{sunk} < $self->{player}->[1]->{sunk} ? $color{green} : $color{red};
my $p2intactcolor = $p1sections < $p2sections ? $color{green} : $color{red};
$buf = sprintf(
"$p1win%*s$color{reset}: bomb: $p1bombscolor%*d$color{reset}, hit: $p1hitcolor%*d$color{reset}, miss: $p1misscolor%*d$color{reset}, sunk: $p1sunkcolor%*d$color{reset}, sections left: $p1intactcolor%*d$color{reset}",
$longest, $self->{player}->[0]->{nick}, $bombslen, $self->{player}->[0]->{bombs},
$hitlen, $self->{player}->[0]->{hit}, $misslen, $self->{player}->[0]->{miss},
$sunklen, $self->{player}->[0]->{sunk}, $intactlen, $p1sections
);
$self->send_message($self->{channel}, $buf);
$buf = sprintf(
"$p2win%*s$color{reset}: bomb: $p2bombscolor%*d$color{reset}, hit: $p2hitcolor%*d$color{reset}, miss: $p2misscolor%*d$color{reset}, sunk: $p2sunkcolor%*d$color{reset}, sections left: $p2intactcolor%*d$color{reset}",
$longest, $self->{player}->[1]->{nick}, $bombslen, $self->{player}->[1]->{bombs},
$hitlen, $self->{player}->[1]->{hit}, $misslen, $self->{player}->[1]->{miss},
$sunklen, $self->{player}->[1]->{sunk}, $intactlen, $p2sections
);
$self->send_message($self->{channel}, $buf);
}
sub show_battlefield {
my ($self, $player, $nick) = @_;
my ($x, $y, $buf);
$self->{pbot}->{logger}->log("showing battlefield for player $player\n");
$buf = "$color{cyan},01 ";
for ($x = 1; $x < $self->{N_X} + 1; $x++) {
if ($x % 10 == 0) {
$buf .= "$color{yellow},01" if $self->{N_X} > 10;
$buf .= $x % 10;
$buf .= ' ';
$buf .= "$color{cyan},01" if $self->{N_X} > 10;
} else {
$buf .= $x % 10;
$buf .= ' ';
}
}
$buf .= "\n";
for ($y = 0; $y < $self->{N_Y}; $y++) {
$buf .= sprintf("$color{cyan},01%c ", 97 + $y);
for ($x = 0; $x < $self->{N_X}; $x++) {
if ($player == 0) {
if ($self->{board}->[$y][$x] eq $self->{player_two_vert} || $self->{board}->[$y][$x] eq $self->{player_two_horiz}) {
$buf .= "$color{blue},01~ ";
next;
} else {
if ($self->{board}->[$y][$x] eq '1' || $self->{board}->[$y][$x] eq '2') { $buf .= "$color{red},01"; }
elsif ($self->{board}->[$y][$x] eq 'o' || $self->{board}->[$y][$x] eq '*') { $buf .= "$color{cyan},01"; }
elsif ($self->{board}->[$y][$x] eq '~') {
$buf .= "$color{blue},01~ ";
next;
} else {
$buf .= "$color{white},01";
}
$buf .= "$self->{board}->[$y][$x] ";
$self->{pbot}->{logger}->log("$y, $x: $self->{board}->[$y][$x]\n");
}
} elsif ($player == 1) {
if ($self->{board}->[$y][$x] eq $self->{player_one_vert} || $self->{board}->[$y][$x] eq $self->{player_one_horiz}) {
$buf .= "$color{blue},01~ ";
next;
} else {
if ($self->{board}->[$y][$x] eq '1' || $self->{board}->[$y][$x] eq '2') { $buf .= "$color{red},01"; }
elsif ($self->{board}->[$y][$x] eq 'o' || $self->{board}->[$y][$x] eq '*') { $buf .= "$color{cyan},01"; }
elsif ($self->{board}->[$y][$x] eq '~') {
$buf .= "$color{blue},01~ ";
next;
} else {
$buf .= "$color{white},01";
}
$buf .= "$self->{board}->[$y][$x] ";
}
} elsif ($player == 2) {
if ( $self->{board}->[$y][$x] eq $self->{player_one_vert}
|| $self->{board}->[$y][$x] eq $self->{player_one_horiz}
|| $self->{board}->[$y][$x] eq $self->{player_two_vert}
|| $self->{board}->[$y][$x] eq $self->{player_two_horiz})
{
$buf .= "$color{blue},01~ ";
next;
} else {
if ($self->{board}->[$y][$x] eq '1' || $self->{board}->[$y][$x] eq '2') { $buf .= "$color{red},01"; }
elsif ($self->{board}->[$y][$x] eq 'o' || $self->{board}->[$y][$x] eq '*') { $buf .= "$color{cyan},01"; }
elsif ($self->{board}->[$y][$x] eq '~') {
$buf .= "$color{blue},01~ ";
next;
} else {
$buf .= "$color{white},01";
}
$buf .= "$self->{board}->[$y][$x] ";
}
} else {
if ($self->{board}->[$y][$x] eq '1' || $self->{board}->[$y][$x] eq '2') { $buf .= "$color{red},01"; }
elsif ($self->{board}->[$y][$x] eq 'o' || $self->{board}->[$y][$x] eq '*') { $buf .= "$color{cyan},01"; }
elsif ($self->{board}->[$y][$x] eq '~') {
$buf .= "$color{blue},01~ ";
next;
} else {
$buf .= "$color{white},01";
}
$buf .= "$self->{board}->[$y][$x] ";
}
}
$buf .= sprintf("$color{cyan},01%c", 97 + $y);
$buf .= "$color{reset}\n";
}
# bottom border
$buf .= "$color{cyan},01 ";
for ($x = 1; $x < $self->{N_X} + 1; $x++) {
if ($x % 10 == 0) {
$buf .= $color{yellow}, 01 if $self->{N_X} > 10;
$buf .= $x % 10;
$buf .= ' ';
$buf .= $color{cyan}, 01 if $self->{N_X} > 10;
} else {
$buf .= $x % 10;
$buf .= ' ';
}
}
$buf .= "\n";
my $player1 = $self->{player}->[0]->{nick};
my $player2 = $self->{player}->[1]->{nick};
if ($player == 0) {
$self->send_message(
$self->{player}->[$player]->{nick},
"Player One Legend: ships: [| -] ocean: [$color{blue},01~$color{reset}] $player1 miss: [$color{cyan},01*$color{reset}] $player2 miss: [$color{cyan},01o$color{reset}] $player1 hit: [$color{red},01"
. "1"
. "$color{reset}] $player2 hit: [$color{red},012$color{reset}]"
);
} elsif ($player == 1) {
$self->send_message(
$self->{player}->[$player]->{nick},
"Player Two Legend: ships: [I =] ocean: [$color{blue},01~$color{reset}] $player1 miss: [$color{cyan},01*$color{reset}] $player2 miss: [$color{cyan},01o$color{reset}] $player1 hit: [$color{red},01"
. "1"
. "$color{reset}] $player2 hit: [$color{red},012$color{reset}]"
);
} elsif ($player == 2) {
$self->send_message(
$self->{channel},
"Spectator Legend: ocean: [$color{blue},01~$color{reset}] $player1 miss: [$color{cyan},01*$color{reset}] $player2 miss: [$color{cyan},01o$color{reset}] $player1 hit: [$color{red},01"
. "1"
. "$color{reset}] $player2 hit: [$color{red},012$color{reset}]"
);
} elsif ($player == 3) {
$self->send_message(
$self->{channel},
"Final Board Legend: $player1 ships: [| -] $player2 ships: [I =] ocean: [$color{blue},01~$color{reset}] $player1 miss: [$color{cyan},01*$color{reset}] $player2 miss: [$color{cyan},01o$color{reset}] $player1 hit: [$color{red},01"
. "1"
. "$color{reset}] $player2 hit: [$color{red},012$color{reset}]"
);
} else {
$self->send_message(
$nick,
"Full Board Legend: $player1 ships: [| -] $player2 ships: [I =] ocean: [$color{blue},01~$color{reset}] $player1 miss: [$color{cyan},01*$color{reset}] $player2 miss: [$color{cyan},01o$color{reset}] $player1 hit: [$color{red},01"
. "1"
. "$color{reset}] $player2 hit: [$color{red},012$color{reset}]"
);
}
foreach my $line (split /\n/, $buf) {
if ($player == 0 || $player == 1) { $self->send_message($self->{player}->[$player]->{nick}, $line); }
elsif ($player == 2 || $player == 3) { $self->send_message($self->{channel}, $line); }
else { $self->send_message($nick, $line); }
}
}
# state subroutines
sub nogame {
my ($self, $state) = @_;
$state->{result} = 'nogame';
$self->{pbot}->{timer}->update_repeating('battleship loop', 0);
return $state;
}
sub accept {
my ($self, $state) = @_;
$state->{max_count} = 3;
if ($state->{players}->[1]->{accepted}) {
$state->{result} = 'accept';
return $state;
}
my $tock = 15;
if ($state->{ticks} % $tock == 0) {
$state->{tocked} = 1;
if (++$state->{counter} > $state->{max_count}) {
if ($state->{players}->[1]->{id} == -1) { $self->send_message($self->{channel}, "Nobody has accepted $state->{players}->[0]->{name}'s challenge."); }
else { $self->send_message($self->{channel}, "$state->{players}->[1]->{name} has failed to accept $state->{players}->[0]->{name}'s challenge."); }
$state->{result} = 'stop';
$state->{players} = [];
return $state;
}
if ($state->{players}->[1]->{id} == -1) {
$self->send_message($self->{channel}, "$state->{players}->[0]->{name} has made an open challenge! Use `accept` to accept their challenge.");
} else {
$self->send_message($self->{channel}, "$state->{players}->[1]->{name}: $state->{players}->[0]->{name} has challenged you! Use `accept` to accept their challenge.");
}
}
$state->{result} = 'wait';
return $state;
}
sub genboard {
my ($self, $state) = @_;
$self->init_game($state->{players}->[0]->{name}, $state->{players}->[1]->{name});
$state->{current_player} = 0;
$state->{max_count} = 3;
$state->{result} = 'next';
return $state;
}
sub showboard {
my ($self, $state) = @_;
$self->send_message($self->{channel}, "Showing battlefield to $self->{player}->[0]->{nick}...");
$self->show_battlefield(0);
$self->send_message($self->{channel}, "Showing battlefield to $self->{player}->[1]->{nick}...");
$self->show_battlefield(1);
$self->send_message($self->{channel}, "Fight! Anybody (players and spectators) can use `board` at any time to see the battlefield.");
$state->{result} = 'next';
return $state;
}
sub playermove {
my ($self, $state) = @_;
my $tock;
if ($state->{first_tock}) { $tock = 3; }
else { $tock = 15; }
if ($self->{player}->[$state->{current_player}]->{done}) {
$self->{pbot}->{logger}->log("playermove: player $state->{current_player} done, nexting\n");
$state->{result} = 'next';
return $state;
}
my $player = $state->{current_player};
my $location = delete $state->{players}->[$player]->{location};
if (defined $location) {
if ($self->bomb($player, uc $location)) {
$self->{player}->[$player]->{done} = 1;
$self->{player}->[!$player]->{done} = 0;
$self->{state_data}->{current_player} = !$player;
$state->{result} = 'next';
return $state;
}
}
if ($state->{ticks} % $tock == 0) {
$state->{tocked} = 1;
if (++$state->{counter} > $state->{max_count}) {
$state->{players}->[$state->{current_player}]->{missedinputs}++;
$self->send_message($self->{channel}, "$state->{players}->[$state->{current_player}]->{name} failed to launch an attack in time. They forfeit their turn!");
$self->{player}->[$state->{current_player}]->{done} = 1;
$self->{player}->[!$state->{current_player}]->{done} = 0;
$state->{current_player} = !$state->{current_player};
$state->{result} = 'next';
return $state;
}
my $red = $state->{counter} == $state->{max_count} ? $color{red} : '';
my $remaining = 15 * $state->{max_count};
$remaining -= 15 * ($state->{counter} - 1);
$remaining = "(" . (concise duration $remaining) . " remaining)";
$self->send_message($self->{channel}, "$state->{players}->[$state->{current_player}]->{name}: $red$remaining Launch an attack now via `bomb <location>`!$color{reset}");
}
$state->{result} = 'wait';
return $state;
}
sub checkplayer {
my ($self, $state) = @_;
if ($self->{player}->[0]->{won} or $self->{player}->[1]->{won}) { $state->{result} = 'sunk'; }
else { $state->{result} = 'next'; }
return $state;
}
sub gameover {
my ($self, $state) = @_;
if ($state->{ticks} % 5 == 0) {
if ($state->{players}->[1]->{id} != -1 && $state->{players}->[1]->{accepted}) {
$self->show_battlefield(3);
$self->show_scoreboard;
$self->send_message($self->{channel}, "Game over!");
}
$state->{players} = [];
$state->{counter} = 0;
$state->{result} = 'next';
} else {
$state->{result} = 'wait';
}
return $state;
}
1;