From 5fcbe429a2ed7217294fecab4e99fc8157bb6dc8 Mon Sep 17 00:00:00 2001 From: mannito <40789152+mannito@users.noreply.github.com> Date: Sat, 7 Jul 2018 07:29:19 +0200 Subject: [PATCH] Add Connect4 game Plugin (#45) Connect4 game Plugin --- PBot/Plugins/Connect4.pm | 851 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 851 insertions(+) create mode 100644 PBot/Plugins/Connect4.pm diff --git a/PBot/Plugins/Connect4.pm b/PBot/Plugins/Connect4.pm new file mode 100644 index 00000000..1d157e89 --- /dev/null +++ b/PBot/Plugins/Connect4.pm @@ -0,0 +1,851 @@ +# 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 PBot::Plugins::Connect4; + +use warnings; +use strict; + +use feature 'switch'; +no if $] >= 5.018, warnings => "experimental::smartmatch"; + +use Carp (); +use Time::Duration qw/concise duration/; +use Data::Dumper; +$Data::Dumper::Useqq = 1; +$Data::Dumper::Sortkeys = 1; + +sub new { + Carp::croak("Options to " . __FILE__ . " should be key/value pairs, not hash reference") if ref $_[1] eq 'HASH'; + my ($class, %conf) = @_; + my $self = bless {}, $class; + $self->initialize(%conf); + return $self; +} + +sub initialize { + my ($self, %conf) = @_; + $self->{pbot} = delete $conf{pbot}; + + $self->{pbot}->{commands}->register(sub { $self->connect4_cmd(@_) }, 'connect4', 0); + + $self->{pbot}->{timer}->register(sub { $self->connect4_timer }, 1, 'connect4 timer'); + + $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} = '##connect4'; + $self->create_states; +} + +sub unload { + my $self = shift; + $self->{pbot}->{commands}->unregister('connect4'); + $self->{pbot}->{timer}->unregister('connect4 timer'); +} + +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 connect4_cmd { + my ($self, $from, $nick, $user, $host, $arguments) = @_; + $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 "; + + my $options; + my $command; + ($command, $arguments, $options) = split / /, $arguments, 3; + $command = lc $command; + + my ($channel, $result); + + given ($command) { + when ('help') { + given ($arguments) { + when ('help') { + return "Seriously?"; + } + + default { + if (length $arguments) { + return "connect4 has no such command '$arguments'. I can't help you with that."; + } else { + return "Usage: connect4 help "; + } + } + } + } + + when ('challenge') { + if ($self->{current_state} ne 'nogame') { + return "There is already a game of connect4 underway."; + } + + if (not length $arguments || $arguments =~ m/^[4-9]$/) { + $self->{current_state} = 'accept'; + $self->{state_data} = { players => [], counter => 0 }; + $self->{CONNECTIONS} = ((not length $arguments) ? 4 : $arguments); + + my $id = $self->{pbot}->{messagehistory}->{database}->get_message_account($nick, $user, $host); + my $player = { id => $id, name => $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} $nick has made an open challenge (connect-$self->{CONNECTIONS})! 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 }; + $self->{CONNECTIONS} = ((not length $options) ? 4 : $options); + + my $id = $self->{pbot}->{messagehistory}->{database}->get_message_account($nick, $user, $host); + my $player = { id => $id, name => $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} $nick has challenged $challengee to Connect4! Use `accept` to accept their challenge."; + } + + when ('accept') { + if ($self->{current_state} ne 'accept') { + return "/msg $nick This is not the time to use `accept`."; + } + + my $id = $self->{pbot}->{messagehistory}->{database}->get_message_account($nick, $user, $host); + my $player = $self->{state_data}->{players}->[1]; + + # open challenge + if ($player->{id} == -1) { + $player->{id} = $id; + $player->{name} = $nick; + } + + if ($player->{id} == $id) { + $player->{accepted} = 1; + return "/msg $self->{channel} $nick has accepted $self->{state_data}->{players}->[0]->{name}'s challenge!"; + } else { + return "/msg $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($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; + $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!"; + } else { + return "$nick: But you are not even playing the game."; + } + } + + when ('abort') { + if (not $self->{pbot}->{admins}->loggedin($self->{channel}, "$nick!$user\@$host")) { + return "$nick: Sorry, only admins may abort the game."; + } + + $self->{current_state} = 'gameover'; + return "/msg $self->{channel} $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 `!"; + } + } + + when ('kick') { + if (not $self->{pbot}->{admins}->loggedin($self->{channel}, "$nick!$user\@$host")) { + return "$nick: Sorry, only admins may kick people from the game."; + } + + if (not length $arguments) { + return "Usage: connect4 kick "; + } + + 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} $nick: $arguments has been kicked from the game."; + } else { + return "$nick: $arguments isn't even in the game."; + } + } + + when ('play') { + $self->{pbot}->{logger}->log("Connect4: play state: $self->{current_state}\n" . Dumper $self->{state_data}); + + if ($self->{current_state} ne 'playermove') { + return "$nick: It's not time to do that now."; + } + + my $id = $self->{pbot}->{messagehistory}->{database}->get_message_account($nick, $user, $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 "$nick: It is not your turn to attack!"; + } + + if ($self->{player}->[$player]->{done}) { + return "$nick: You have already played this turn."; + } + + if ($arguments !~ m/^[1-7]$/) { + return "$nick: Usage: connect4 play ; must be in the 1..7 range etc."; + } + + 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; + } + } + } + + 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 "$nick: There is no board to show right now."; + } + + if ($_ eq 'specboard') { + $self->show_board(2); + return; + } + + my $id = $self->{pbot}->{messagehistory}->{database}->get_message_account($nick, $user, $host); + for (my $i = 0; $i < 2; $i++) { + if ($self->{state_data}->{players}->[$i]->{id} == $id) { + $self->send_message($self->{channel}, "$nick surveys the board!"); + $self->show_board($i); + return; + } + } + $self->show_board(2); + } + + default { + return $usage; + } + } + + return $result; +} + +sub connect4_timer { + my $self = shift; + $self->run_one_state; +} + +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', command => 'connect4 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}"); + 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 ($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->{N_X} = 7; + $self->{N_Y} = 6; + $self->{chips} = 0; + $self->{draw} = 0; + + $self->{board} = []; + + $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 connected { + my ($self) = @_; + my ($i, $j, $row, $col, $prev) = (0, 0, 0, 0, 0); + my ($tis, $n) = (0, 0); + + for ($row = 0; $row < $self->{N_Y}; $row++) { + $n = 0; + $prev = ' '; + for ($i = $row, $j = $self->{N_X}-1; $i < $self->{N_Y} && $j >= 0; $i++, $j--) { + $tis = $self->{board}[$i][$j]; + + $n = (($tis eq $prev) && $prev ne ' ') ? $n+1 : 1; + + if ($tis eq ' ') { $n = 0; } + + if ($n == $self->{CONNECTIONS}) { + return 1; + } + + $prev = $tis; + } + } + + for ($col = $self->{N_X} - 1; $col >= 0; $col--) { + $n = 0; + $prev = ' '; + for ($i = 0, $j = $col; $i < $self->{N_Y} && $j >= 0; $i++, $j--) { + $tis = $self->{board}[$i][$j]; + + $n = (($tis eq $prev) && $prev ne ' ') ? $n+1 : 1; + + if ($tis eq ' ') { $n = 0; } + + if ($n == $self->{CONNECTIONS}) { + return 2; + } + + $prev = $tis; + } + } + + for ($row = 0; $row < $self->{N_Y}; $row++) { + $n = 0; + $prev = ' '; + for ($i = $row, $j = 0; $i < $self->{N_Y}; $i++, $j++) { + $tis = $self->{board}[$i][$j]; + + $n = (($tis eq $prev) && $prev ne ' ') ? $n+1 : 1; + + if ($tis eq ' ') { $n = 0; } + + if ($n == $self->{CONNECTIONS}) { + return 3; + } + + $prev = $tis; + } + } + + for ($col = 0; $col < $self->{N_X}; $col++) { + $n = 0; + $prev = ' '; + for ($i = 0, $j = $col; $i < $self->{N_Y} && $j < $self->{N_X}; $i++, $j++) { + $tis = $self->{board}[$i][$j]; + + $n = (($tis eq $prev) && $prev ne ' ') ? $n+1 : 1; + + if ($tis eq ' ') { $n = 0; } + + if ($n == $self->{CONNECTIONS}) { + return 4; + } + + $prev = $tis; + } + } + + for ($row = 0; $row < $self->{N_Y}; $row++) { + $n = 0; + $prev = ' '; + for ($col = 0; $col < $self->{N_X}; $col++) { + $tis = $self->{board}[$row][$col]; + + $n = (($tis eq $prev) && $prev ne ' ') ? $n+1 : 1; + + if ($tis eq ' ') { $n = 0; } + + if ($n == $self->{CONNECTIONS}) { + return 5; + } + + $prev = $tis; + } + } + + for ($col = 0; $col < $self->{N_X}; $col++) { + $n = 0; + $prev = ' '; + for ($row = $self->{N_Y} - 1; $row >= 0; $row--) { + $tis = $self->{board}[$row][$col]; + + $n = (($tis eq $prev) && $prev ne ' ') ? $n+1 : 1; + + if ($tis eq ' ') { $n = 0; } + + if ($n == $self->{CONNECTIONS}) { + return 6; + } + + $prev = $tis; + } + } + + 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] != ' ') { + $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, $player) = @_; + my ($x, $y, $buf, $chip, $c); + + $self->{pbot}->{logger}->log("showing board\n"); + + $buf = "$color{blue}"; + + for($x = 1; $x < $self->{N_X} + 1; $x++) { + if ($x % 10 == 0) { + $buf .= $color{yellow}; + $buf .= ' '; + $buf .= $x % 10; + $buf .= ' '; + $buf .= $color{blue}; + } 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]; + $c = $chip eq 'O' ? $color{red} : $color{cyan}; + $buf .= "[$c$chip$color{reset}]"; + } + $buf .= "\n"; + } + + foreach my $line (split /\n/, $buf) { + if ($player != 2) { + $self->send_message($self->{player}->[$player]->{nick}, $line); + } else { + $self->send_message($self->{channel}, $line); + } + } +} + +# state subroutines + +sub nogame { + my ($self, $state) = @_; + $state->{result} = 'nogame'; + 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(2); + $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 `!$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(2); + $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;