3
0
mirror of https://github.com/pragma-/pbot.git synced 2024-11-25 21:39:27 +01:00
pbot/lib/PBot/Plugin/Connect4.pm
Pragmatic Software 926d57990b Support named-parameters in command registration
- convert several plugins to use named-parameters
- misc clean-ups in unrelated files
2021-07-30 19:01:24 -07:00

823 lines
30 KiB
Perl

# File: Connect4.pm
#
# Purpose: Connect-4 game for IRC. Includes options to resize board
# dimensions and how many pieces to connect.
# SPDX-FileCopyrightText: 2021 Pragmatic Software <pragma78@gmail.com>
# SPDX-License-Identifier: MIT
package PBot::Plugin::Connect4;
use parent 'PBot::Plugin::Base';
use PBot::Imports;
use Time::Duration qw/concise duration/;
use Data::Dumper;
use List::Util qw[min max];
$Data::Dumper::Useqq = 1;
$Data::Dumper::Sortkeys = 1;
sub initialize {
my ($self, %conf) = @_;
$self->{pbot}->{commands}->add(
name => 'connect4',
help => 'Connect-4 board game',
subref => sub { $self->cmd_connect4(@_) },
);
$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('connect4', 'channel') // '##connect4';
$self->{debug} = $self->{pbot}->{registry}->get_value('connect4', 'debug') // 0;
$self->create_states;
}
sub unload {
my $self = shift;
$self->{pbot}->{commands}->remove('connect4');
$self->{pbot}->{event_dispatcher}->remove_handler('irc.part');
$self->{pbot}->{event_dispatcher}->remove_handler('irc.quit');
$self->{pbot}->{event_dispatcher}->remove_handler('irc.kick');
$self->{pbot}->{event_queue}->dequeue_event('connect4 loop');
}
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",
);
my $DEFAULT_NX = 7;
my $DEFAULT_NY = 6;
my $DEFAULT_CONNECTIONS = 4;
my $MAX_NX = 80;
my $MAX_NY = 12;
# challenge options: CONNS:ROWSxCOLS
sub parse_challenge {
my ($self, $options) = @_;
my ($conns, $xy, $nx, $ny);
"x" =~ /x/; # clear $1, $2 ...
if ($options !~ m/^(\d+)(:(\d+)x(\d+))?$/) { return "Invalid options '$options', use: <CONNS:ROWSxCOLS>"; }
$conns = $1;
$xy = $2;
$ny = $3;
$nx = $4;
$self->{N_X} = (not length $nx) ? $DEFAULT_NX : $nx;
$self->{N_Y} = (not length $ny) ? $DEFAULT_NY : $ny;
$self->{CONNECTIONS} = (not length $conns) ? $DEFAULT_CONNECTIONS : $conns;
# auto adjust board size for `challenge N'
if ((not length $xy) && ($self->{CONNECTIONS} >= $self->{N_X} || $self->{CONNECTIONS} >= $self->{N_Y})) {
$self->{N_X} = min($self->{CONNECTIONS} * 2 - 1, $MAX_NX);
$self->{N_Y} = min($self->{CONNECTIONS} * 2 - 2, $MAX_NY);
}
if ($self->{N_X} > $MAX_NX || $self->{N_Y} > $MAX_NY) {
return "Invalid board options '$self->{CONNECTIONS}:$self->{N_Y}x$self->{N_X}', " . "maximum board size is: ${MAX_NY}x${MAX_NX}.";
}
if ($self->{N_X} < $self->{CONNECTIONS} && $self->{N_Y} < $self->{CONNECTIONS}) {
return "Invalid board options '$self->{CONNECTIONS}:$self->{N_Y}x$self->{N_X}', " . "rows or columns must be >= than connections.";
}
return 0;
}
sub cmd_connect4 {
my ($self, $context) = @_;
my $err;
$context->{arguments} =~ s/^\s+|\s+$//g;
my $usage = "Usage: connect4 challenge|accept|play|board|quit|players|kick|abort; for more information about a command: connect4 help <command>";
my ($command, $arguments, $options) = split / /, $context->{arguments}, 3;
$command = lc $command;
given ($command) {
when ('help') {
given ($arguments) {
when ('help') { return "Seriously?"; }
when ('challenge') { return "challenge [nick] [connections[:ROWSxCOLS]] -- connections has to be <= than rows or columns (duh!)."; }
default {
if (length $arguments) { return "connect4 has no such command '$arguments'. I can't help you with that."; }
else { return "Usage: connect4 help <command>"; }
}
}
}
when ('challenge') {
if ($self->{current_state} ne 'nogame') { return "There is already a game of connect4 underway."; }
$self->{N_X} = $DEFAULT_NX;
$self->{N_Y} = $DEFAULT_NY;
$self->{CONNECTIONS} = $DEFAULT_CONNECTIONS;
if ((not length $arguments) || ($arguments =~ m/^\d+.*$/ && not($err = $self->parse_challenge($arguments)))) {
$self->{current_state} = 'accept';
$self->{state_data} = {players => [], counter => 0};
$self->{pbot}->{event_queue}->enqueue_event(
sub {
$self->run_one_state;
}, 1, 'connect4 loop', 1
);
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;
return "/msg $self->{channel} $context->{nick} has made an open challenge (Connect-$self->{CONNECTIONS} @ "
. "$self->{N_Y}x$self->{N_X} board)! Use `accept` to accept their challenge.";
}
if ($err) { return $err; }
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!"; }
if (length $options) {
if ($err = $self->parse_challenge($options)) { return $err; }
}
$self->{current_state} = 'accept';
$self->{state_data} = {players => [], counter => 0};
$self->{pbot}->{event_queue}->enqueue_event(
sub {
$self->run_one_state;
}, 1, 'connect4 loop', 1
);
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;
return "/msg $self->{channel} $context->{nick} has challenged $challengee to "
. "Connect-$self->{CONNECTIONS} @ $self->{N_Y}x$self->{N_X} board! 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 Connect4 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) {
splice @{$self->{state_data}->{players}}, $i--, 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} 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 ('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 playing with $self->{state_data}->{players}->[1]->{name}!"; }
else { return "There are no players playing right now. Start a game with `connect4 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: connect4 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) {
splice @{$self->{state_data}->{players}}, $i--, 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 ('play') {
if ($self->{debug}) { $self->{pbot}->{logger}->log("Connect4: play state: $self->{current_state}\n" . Dumper $self->{state_data}); }
if ($self->{current_state} ne 'playermove') { 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 ($self->{state_data}->{current_player} != $player) { return "$context->{nick}: It is not your turn to attack!"; }
if ($self->{player}->[$player]->{done}) { return "$context->{nick}: You have already played this turn."; }
if ($arguments !~ m/^\d+$/) { return "$context->{nick}: Usage: connect4 play <location>; <location> must be in the [1, $self->{N_X}] range."; }
if ($self->play($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;
}
}
return "";
}
when ('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.";
}
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 board!");
$self->show_board;
return "";
}
}
$self->show_board;
}
default { return $usage; }
}
return "";
}
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) {
splice @{$self->{state_data}->{players}}, $i--, 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 => 'connect4',
host => 'localhost',
hostmask => "$botnick!connect4\@localhost",
command => 'connect4',
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}"
);
splice @{$self->{state_data}->{players}}, $i--, 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 (not @{$self->{state_data}->{players}} == 2) {
$self->send_message($self->{channel}, "A player has left the game! The game is now over.");
$self->{current_state} = 'nogame';
}
}
my $state_data = $self->{state_data};
# this shouldn't happen
if (not defined $self->{current_state}) {
$self->{pbot}->{logger}->log("Connect4 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("Connect4: 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("Connect4: 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}{end} = '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';
}
# connect4 stuff
sub init_game {
my ($self, $nick1, $nick2) = @_;
$self->{chips} = 0;
$self->{draw} = 0;
$self->{board} = [];
$self->{winner_line} = [];
$self->{player} = [
{nick => $nick1, done => 0},
{nick => $nick2, done => 0}
];
$self->{turn} = 0;
$self->{horiz} = 0;
$self->generate_board;
}
sub generate_board {
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] = ' '; }
}
}
sub check_one {
my ($self, $y, $x, $prev) = @_;
my $chip = $self->{board}[$y][$x];
push @{$self->{winner_line}}, "$y $x";
if ($chip eq ' ' || $chip ne $prev) { $self->{winner_line} = ($chip eq ' ') ? [] : ["$y $x"]; }
return (scalar @{$self->{winner_line}} == $self->{CONNECTIONS}, $chip);
}
sub connected {
my ($self) = @_;
my ($i, $j, $row, $col, $prev) = (0, 0, 0, 0, 0);
my $rv;
for ($row = 0; $row < $self->{N_Y}; $row++) {
$prev = ' ';
$self->{winner_line} = [];
for ($i = $row, $j = $self->{N_X} - 1; $i < $self->{N_Y} && $j >= 0; $i++, $j--) {
($rv, $prev) = $self->check_one($i, $j, $prev);
if ($rv) { return 1; }
}
}
for ($col = $self->{N_X} - 1; $col >= 0; $col--) {
$prev = ' ';
$self->{winner_line} = [];
for ($i = 0, $j = $col; $i < $self->{N_Y} && $j >= 0; $i++, $j--) {
($rv, $prev) = $self->check_one($i, $j, $prev);
if ($rv) { return 2; }
}
}
for ($row = 0; $row < $self->{N_Y}; $row++) {
$prev = ' ';
$self->{winner_line} = [];
for ($i = $row, $j = 0; $i < $self->{N_Y}; $i++, $j++) {
($rv, $prev) = $self->check_one($i, $j, $prev);
if ($rv) { return 3; }
}
}
for ($col = 0; $col < $self->{N_X}; $col++) {
$prev = ' ';
$self->{winner_line} = [];
for ($i = 0, $j = $col; $i < $self->{N_Y} && $j < $self->{N_X}; $i++, $j++) {
($rv, $prev) = $self->check_one($i, $j, $prev);
if ($rv) { return 4; }
}
}
for ($row = 0; $row < $self->{N_Y}; $row++) {
$prev = ' ';
$self->{winner_line} = [];
for ($col = 0; $col < $self->{N_X}; $col++) {
($rv, $prev) = $self->check_one($row, $col, $prev);
if ($rv) { return 5; }
}
}
for ($col = 0; $col < $self->{N_X}; $col++) {
$prev = ' ';
$self->{winner_line} = [];
for ($row = $self->{N_Y} - 1; $row >= 0; $row--) {
($rv, $prev) = $self->check_one($row, $col, $prev);
if ($rv) { return 6; }
}
}
$self->{winner_line} = [];
return 0;
}
sub column_top {
my ($self, $x) = @_;
my $y;
for ($y = 0; $y < $self->{N_Y}; $y++) {
if ($self->{board}->[$y][$x] ne ' ') { return $y - 1; }
}
return -1; # shouldnt happen
}
sub play {
my ($self, $player, $location) = @_;
my ($draw, $c4, $x, $y);
$x = $location - 1;
$self->{pbot}->{logger}->log("play player $player: $x\n");
if ($x < 0 || $x >= $self->{N_X} || $self->{board}[0][$x] ne ' ') {
$self->send_message($self->{channel}, "Target illegal/out of range, try again.");
return 0;
}
$y = $self->column_top($x);
$self->{board}->[$y][$x] = $player ? 'O' : 'X';
$self->{chips}++;
$c4 = $self->connected;
$draw = $self->{chips} == $self->{N_X} * $self->{N_Y};
my $nick1 = $self->{player}->[$player]->{nick};
my $nick2 = $self->{player}->[$player ? 0 : 1]->{nick};
$self->send_message($self->{channel}, "$nick1 placed piece at column: $location");
if ($c4) {
$self->send_message($self->{channel}, "$nick1 connected $self->{CONNECTIONS} pieces! $color{red}--- VICTORY! --- $color{reset}");
$self->{player}->[$player]->{won} = 1;
} elsif ($draw) {
$self->send_message($self->{channel}, "$color{red}--- DRAW! --- $color{reset}");
$self->{draw} = 1;
}
return 1;
}
sub show_board {
my ($self) = @_;
my ($x, $y, $buf, $chip, $c);
$self->{pbot}->{logger}->log("showing board\n");
my $nick1 = $self->{player}->[0]->{nick};
my $nick2 = $self->{player}->[1]->{nick};
$buf = sprintf("%s: %s ", $nick1, "$color{yellow}X$color{reset}");
$buf .= sprintf("%s: %s\n", $nick2, "$color{red}O$color{reset}");
$buf .= "$color{bold}";
for ($x = 1; $x < $self->{N_X} + 1; $x++) {
if ($x % 10 == 0) {
$buf .= $color{yellow};
$buf .= ' ';
$buf .= $x % 10;
$buf .= ' ';
$buf .= $color{reset} . $color{bold};
} else {
$buf .= " " . $x % 10 . " ";
}
}
$buf .= "\n";
for ($y = 0; $y < $self->{N_Y}; $y++) {
for ($x = 0; $x < $self->{N_X}; $x++) {
$chip = $self->{board}->[$y][$x];
my $rc = "$y $x";
$c = $chip eq 'O' ? $color{red} : $color{yellow};
if (grep(/^$rc$/, @{$self->{winner_line}})) { $c .= $color{bold}; }
$buf .= $color{blue} . "[";
$buf .= $c . $chip . $color{reset};
$buf .= $color{blue} . "]";
}
$buf .= $color{reset};
$buf .= "\n";
}
foreach my $line (split /\n/, $buf) { $self->send_message($self->{channel}, $line); }
}
# state subroutines
sub nogame {
my ($self, $state) = @_;
$state->{result} = 'nogame';
$self->{pbot}->{event_queue}->update_repeating('connect4 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->{max_count} = 3;
$state->{result} = 'next';
return $state;
}
sub showboard {
my ($self, $state) = @_;
$self->send_message($self->{channel}, "Showing board ...");
$self->show_board;
$self->send_message($self->{channel}, "Fight! Anybody (players and spectators) can use `board` at any time to see latest version of the board!");
$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;
}
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 play 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 Play now via `play <location>`!$color{reset}");
}
$state->{result} = 'wait';
return $state;
}
sub checkplayer {
my ($self, $state) = @_;
if ($self->{player}->[$state->{current_player}]->{won} || $self->{draw}) { $state->{result} = 'end'; }
else { $state->{result} = 'next'; }
return $state;
}
sub gameover {
my ($self, $state) = @_;
my $buf;
if ($state->{ticks} % 2 == 0) {
$self->show_board;
$self->send_message($self->{channel}, $buf);
$self->send_message($self->{channel}, "Game over!");
$state->{players} = [];
$state->{counter} = 0;
$state->{result} = 'next';
} else {
$state->{result} = 'wait';
}
return $state;
}
1;