2021-06-29 23:48:55 +02:00
# File: Battleship.pm
#
2021-07-03 00:58:05 +02:00
# Purpose: Simplified version of the Battleship board game. In this variant,
2021-07-03 20:01:37 +02:00
# there is one game grid/board and every player's ships share it without
2021-07-03 20:31:01 +02:00
# overlapping. This adds an element of strategy: everybody knows where their
2021-07-03 20:01:37 +02:00
# own ships are located, ergo they know where NOT to aim. This helps to speed
# games up by removing some randomness.
2021-06-29 23:48:55 +02:00
#
# Note: This code was written circa 1993 for a DikuMUD fork. It was originally
# written in C, as I was teaching the language to myself in my early teens. Two
# decades or so later, I transliterated this code from C to Perl for PBot. Much
# of the "ugly" C-style design of this code has been preserved for personal
# historical reasons -- I was inspired by the IOCCC and I attempted to be clever
# with nested conditional operators and other silliness. Please be gentle if you
# read this code. :)
2021-07-03 00:58:05 +02:00
#
# Update: Much of this code has now been refactored to support more than two
# players on a single board. The board grows in size for each additional player,
# to accomodate their ships. Whirlpools have also been added. They are initially
# hidden by the ocean. When shot, they reveal themselves on the map and deflect
2021-07-03 20:01:37 +02:00
# the shot to a random tile. Much of the IOCCC silliness has been removed so that
# I can maintain this code without going insane.
2021-06-29 23:48:55 +02:00
2021-07-11 00:00:22 +02:00
# SPDX-FileCopyrightText: 2021 Pragmatic Software <pragma78@gmail.com>
# SPDX-License-Identifier: MIT
2018-07-01 12:07:44 +02:00
2021-07-14 04:45:56 +02:00
package PBot::Plugin::Battleship ;
use parent 'PBot::Plugin::Base' ;
2018-07-01 12:07:44 +02:00
2021-06-19 06:23:34 +02:00
use PBot::Imports ;
2018-07-01 12:07:44 +02:00
2021-06-29 23:48:55 +02:00
use Time::Duration ;
2018-07-02 04:43:27 +02:00
use Data::Dumper ;
2018-07-01 12:07:44 +02:00
2021-07-03 00:58:05 +02:00
# some colors for IRC messages
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" ,
) ;
2018-07-01 12:07:44 +02:00
sub initialize {
2020-02-15 23:38:32 +01:00
my ( $ self , % conf ) = @ _ ;
2018-07-01 12:07:44 +02:00
2021-06-29 23:48:55 +02:00
# register `battleship` bot command
2021-07-31 04:01:24 +02:00
$ self - > { pbot } - > { commands } - > add (
name = > 'battleship' ,
help = > 'Battleship board game, simplified for IRC' ,
subref = > sub { $ self - > cmd_battleship ( @ _ ) } ,
) ;
2018-07-01 12:07:44 +02:00
2021-06-29 23:48:55 +02:00
# set the channel where to send game messages
2020-02-15 23:38:32 +01:00
$ self - > { channel } = $ self - > { pbot } - > { registry } - > get_value ( 'battleship' , 'channel' ) // '##battleship' ;
2021-06-29 23:48:55 +02:00
2021-07-03 00:58:05 +02:00
# debugging flag
2021-07-03 01:46:50 +02:00
$ self - > { debug } = $ self - > { pbot } - > { registry } - > get_value ( 'battleship' , 'debug' ) // 0 ;
2021-07-03 00:58:05 +02:00
# player limit per game
$ self - > { MAX_PLAYERS } = 5 ;
2021-07-03 20:19:09 +02:00
# max missed moves before player is ejected from game
$ self - > { MAX_MISSED_MOVES } = 5 ;
2021-07-03 20:01:37 +02:00
# types of board tiles
$ self - > { TYPE_OCEAN } = 0 ;
$ self - > { TYPE_WHIRLPOOL } = 1 ;
$ self - > { TYPE_SHIP } = 2 ;
2021-07-03 00:58:05 +02:00
# battleship tile symbols
2021-07-03 20:01:37 +02:00
$ self - > { TILE_HIT } = [ '1' .. $ self - > { MAX_PLAYERS } ] ;
$ self - > { TILE_OCEAN } = "$color{blue}~" ;
$ self - > { TILE_MISS } = "$color{cyan}o" ;
$ self - > { TILE_WHIRLPOOL } = "$color{cyan}@" ;
2021-07-03 00:58:05 +02:00
# personal ship tiles shown on player board
$ self - > { TILE_SHIP_VERT } = "$color{white}|" ;
$ self - > { TILE_SHIP_HORIZ } = "$color{white}—" ;
# all player ship tiles shown on final/full board
$ self - > { TILE_SHIP } = [ 'A' .. chr ord ( 'A' ) + $ self - > { MAX_PLAYERS } - 1 ] ;
2020-01-22 06:03:47 +01:00
2021-07-03 00:58:05 +02:00
# default board dimensions
2021-07-03 20:01:37 +02:00
$ self - > { BOARD_X } = 12 ;
2021-07-03 00:58:05 +02:00
$ self - > { BOARD_Y } = 8 ;
# number of ships per player
2021-07-03 20:01:37 +02:00
$ self - > { SHIP_COUNT } = 6 ;
2021-07-03 00:58:05 +02:00
# modifiers for show_battlefield()
$ self - > { BOARD_SPECTATOR } = - 1 ;
$ self - > { BOARD_FINAL } = - 2 ;
$ self - > { BOARD_FULL } = - 3 ;
# ship orientation
$ self - > { ORIENT_VERT } = 0 ;
$ self - > { ORIENT_HORIZ } = 1 ;
2020-01-22 06:03:47 +01:00
2021-07-08 18:30:32 +02:00
# paused state (0 is unpaused)
$ self - > { PAUSED_BY_PLAYER } = 1 ;
$ self - > { PAUSED_FOR_OUTPUT_QUEUE } = 2 ;
2021-06-29 23:48:55 +02:00
# create game state machine
2020-02-15 23:38:32 +01:00
$ self - > create_states ;
2021-07-04 08:13:36 +02:00
# receive notification when all messages in IRC output queue have been sent
$ self - > { pbot } - > { event_dispatcher } - > register_handler (
2021-07-05 18:38:37 +02:00
'pbot.output_queue_empty' , sub { $ self - > on_output_queue_empty ( @ _ ) }
2021-07-04 08:13:36 +02:00
) ;
2018-07-01 12:07:44 +02:00
}
sub unload {
2021-06-29 23:48:55 +02:00
my ( $ self ) = @ _ ;
2018-07-01 12:07:44 +02:00
2021-06-29 23:48:55 +02:00
# unregister `battleship` bot command
2021-07-31 04:01:24 +02:00
$ self - > { pbot } - > { commands } - > remove ( 'battleship' ) ;
2018-07-01 12:07:44 +02:00
2021-06-29 23:48:55 +02:00
# remove battleship loop event from event queue
2021-07-03 00:58:05 +02:00
$ self - > end_game_loop ;
2021-07-05 18:38:37 +02:00
# remove event handler
$ self - > { pbot } - > { event_dispatcher } - > remove_handler ( 'pbot.output_queue_empty' ) ;
2018-07-01 12:07:44 +02:00
}
2021-07-04 08:13:36 +02:00
# the game is paused at the beginning when sending the player boards to all
# the players and then resumed when the output queue has depleted. this prevents
# game events from queuing up while the board messages are being slowly
# trickled out to the ircd to avoid filling up its message queue (and getting
# disconnected with 'excess flood'). this event handler resumes the game once
# the boards have finished transmitting, unless the game was manually paused
# by a player.
2021-07-05 18:38:37 +02:00
sub on_output_queue_empty {
2021-07-04 08:13:36 +02:00
my ( $ self ) = @ _ ; # we don't care about the other event arguments
2021-07-08 18:30:32 +02:00
# if we're paused waiting for the output queue, go ahead and unpause
if ( $ self - > { state_data } - > { paused } == $ self - > { PAUSED_FOR_OUTPUT_QUEUE } ) {
2021-07-04 08:13:36 +02:00
$ self - > { state_data } - > { paused } = 0 ;
}
2021-07-04 08:18:02 +02:00
2021-07-04 08:13:36 +02:00
return 0 ;
}
2021-06-29 23:48:55 +02:00
# `battleship` bot command
2020-05-04 22:21:35 +02:00
sub cmd_battleship {
my ( $ self , $ context ) = @ _ ;
2018-07-01 12:07:44 +02:00
2021-07-04 08:13:36 +02:00
my $ usage = "Usage: battleship challenge|accept|decline|ready|unready|bomb|board|score|players|pause|quit|kick|abort; see also: battleship help <command>" ;
2021-06-29 23:48:55 +02:00
# strip leading and trailing whitespace
$ context - > { arguments } =~ s/^\s+|\s+$//g ;
2018-07-01 12:07:44 +02:00
2020-05-04 22:21:35 +02:00
my ( $ command , $ arguments ) = split / / , $ context - > { arguments } , 2 ;
2021-06-29 23:48:55 +02:00
$ command // = '' ;
2020-02-15 23:38:32 +01:00
$ command = lc $ command ;
2018-07-01 12:07:44 +02:00
2021-07-03 00:58:05 +02:00
$ arguments // = '' ;
$ arguments = lc $ arguments ;
2021-06-29 23:48:55 +02:00
# shorter aliases
my ( $ nick , $ user , $ host , $ hostmask , $ channel ) = (
$ context - > { nick } ,
$ context - > { user } ,
$ context - > { host } ,
$ context - > { hostmask } ,
$ self - > { channel } ,
) ;
2018-07-01 12:07:44 +02:00
2020-02-15 23:38:32 +01:00
given ( $ command ) {
2021-06-29 23:48:55 +02:00
# help doesn't do much yet
2018-07-01 12:07:44 +02:00
when ( 'help' ) {
2020-02-15 23:38:32 +01:00
given ( $ arguments ) {
2021-06-29 23:48:55 +02:00
when ( 'help' ) {
return "Seriously?" ;
}
2020-02-15 23:38:32 +01:00
default {
2021-06-29 23:48:55 +02:00
if ( length $ arguments ) {
return "Battleship help is coming soon." ;
} else {
return "Usage: battleship help <command>" ;
}
2020-02-15 23:38:32 +01:00
}
}
2018-07-01 12:07:44 +02:00
}
2021-06-29 23:48:55 +02:00
# issue a challenge to begin a game
2020-02-15 23:38:32 +01:00
when ( 'challenge' ) {
2021-06-29 23:48:55 +02:00
if ( $ self - > { current_state } ne 'nogame' ) {
return "There is already a game of Battleship underway." ;
}
2018-07-01 12:07:44 +02:00
2021-07-03 20:01:37 +02:00
# set game to the `challenge` state to begin accepting challenge
2021-07-03 00:58:05 +02:00
$ self - > set_state ( 'challenge' ) ;
2021-06-29 23:48:55 +02:00
2021-07-03 00:58:05 +02:00
# add player 0, the challenger, to the game
my $ id = $ self - > get_player_id ( $ nick , $ user , $ host ) ;
2021-06-29 23:48:55 +02:00
2021-07-03 00:58:05 +02:00
my $ player = $ self - > new_player ( $ id , $ nick ) ;
2018-07-01 12:07:44 +02:00
2021-07-03 00:58:05 +02:00
# clear out player data
$ self - > { state_data } - > { players } = [] ;
2018-07-02 03:46:58 +02:00
2021-07-03 00:58:05 +02:00
# add player 0
push @ { $ self - > { state_data } - > { players } } , $ player ;
2021-06-29 23:48:55 +02:00
2021-07-03 00:58:05 +02:00
# start the battleship game loop
$ self - > begin_game_loop ;
2020-03-08 04:27:15 +01:00
2021-07-03 00:58:05 +02:00
return "/msg $channel $nick has issued a Battleship challenge! Use `accept` to accept their challenge." ;
}
2020-03-08 04:27:15 +01:00
2021-07-03 00:58:05 +02:00
# accept a challenge
when ( [ 'accept' , 'join' ] ) {
if ( $ self - > { current_state } ne 'challenge' ) {
return "This is not the time to use `$command`." ;
2021-06-29 23:48:55 +02:00
}
2021-07-03 00:58:05 +02:00
if ( @ { $ self - > { state_data } - > { players } } >= $ self - > { MAX_PLAYERS } ) {
return "/msg $channel $nick: The player limit has been reached. Try again next game." ;
2020-02-15 23:38:32 +01:00
}
2018-07-02 03:46:58 +02:00
2021-07-03 00:58:05 +02:00
my $ id = $ self - > get_player_id ( $ nick , $ user , $ host ) ;
2018-07-01 12:07:44 +02:00
2021-07-03 00:58:05 +02:00
# check that player hasn't already accepted/joined
if ( grep { $ _ - > { id } == $ id } @ { $ self - > { state_data } - > { players } } ) {
return "$nick: You have already joined this Battleship game." ;
}
2018-07-01 12:07:44 +02:00
2021-07-03 00:58:05 +02:00
# add another player
my $ player = $ self - > new_player ( $ id , $ nick ) ;
2021-06-29 23:48:55 +02:00
2021-07-03 00:58:05 +02:00
$ player - > { index } = @ { $ self - > { state_data } - > { players } } ;
2021-06-29 23:48:55 +02:00
2020-02-15 23:38:32 +01:00
push @ { $ self - > { state_data } - > { players } } , $ player ;
2018-07-01 12:07:44 +02:00
2021-07-03 00:58:05 +02:00
return "/msg $channel $nick has joined the game. Use `ready` to ready-up." ;
2020-02-15 23:38:32 +01:00
}
2018-07-01 12:07:44 +02:00
2021-07-03 00:58:05 +02:00
# ready/unready
when ( [ 'ready' , 'unready' ] ) {
if ( $ self - > { current_state } ne 'challenge' ) {
return "This is not the time to use `$command`." ;
2021-06-29 23:48:55 +02:00
}
2018-07-01 12:07:44 +02:00
2021-07-03 00:58:05 +02:00
my $ id = $ self - > get_player_id ( $ nick , $ user , $ host ) ;
2018-07-01 12:07:44 +02:00
2021-07-03 00:58:05 +02:00
my ( $ player ) = grep { $ _ - > { id } == $ id } @ { $ self - > { state_data } - > { players } } ;
if ( not defined $ player ) {
return "$nick: You have not joined this game of Battleship. Use `accept` to join the game." ;
2020-02-15 23:38:32 +01:00
}
2018-07-01 12:07:44 +02:00
2021-07-03 00:58:05 +02:00
if ( $ command eq 'ready' ) {
$ player - > { ready } = 1 ;
return "/msg $channel $nick is ready!" ;
2020-02-15 23:38:32 +01:00
} else {
2021-07-03 00:58:05 +02:00
$ player - > { ready } = 0 ;
return "/msg $channel $nick is no longer ready." ;
2020-02-15 23:38:32 +01:00
}
2018-07-01 12:07:44 +02:00
}
2021-06-29 23:48:55 +02:00
# decline a challenge or forfeit/concede a game
when ( [ 'decline' , 'quit' , 'forfeit' , 'concede' ] ) {
2021-07-03 00:58:05 +02:00
my $ id = $ self - > get_player_id ( $ nick , $ user , $ host ) ;
2020-02-15 23:38:32 +01:00
for ( my $ i = 0 ; $ i < @ { $ self - > { state_data } - > { players } } ; $ i + + ) {
if ( $ self - > { state_data } - > { players } - > [ $ i ] - > { id } == $ id ) {
2021-07-03 00:58:05 +02:00
if ( $ self - > { current_state } eq 'challenge' ) {
# remove from player list now since this is only the accept
# stage and a game hasn't yet begun
splice @ { $ self - > { state_data } - > { players } } , $ i , 1 ;
} else {
# there is an on-going game, just mark them as removed
$ self - > { state_data } - > { players } - > [ $ i ] - > { removed } = 1 ;
}
2020-02-15 23:38:32 +01:00
2021-06-29 23:48:55 +02:00
return "/msg $channel $nick has left the game!" ;
2020-02-15 23:38:32 +01:00
}
2021-06-29 23:48:55 +02:00
}
2021-07-03 00:58:05 +02:00
return "There is nothing to $command." ;
2018-07-01 12:07:44 +02:00
}
2018-07-05 23:46:53 +02:00
2020-02-15 23:38:32 +01:00
when ( 'abort' ) {
2021-06-29 23:48:55 +02:00
if ( not $ self - > { pbot } - > { users } - > loggedin_admin ( $ channel , $ hostmask ) ) {
return "$nick: Only admins may abort the game." ;
2020-05-04 22:21:35 +02:00
}
2018-07-01 12:07:44 +02:00
2021-07-03 00:58:05 +02:00
if ( $ self - > { current_state } eq 'nogame' ) {
return "/msg $channel $nick: There is no ongoing game to abort." ;
}
# jump directly to the `gameover` state to
# show the final board and reset the game
2021-06-29 23:48:55 +02:00
$ self - > set_state ( 'gameover' ) ;
return "/msg $channel $nick: The game has been aborted." ;
2020-02-15 23:38:32 +01:00
}
2018-07-01 12:07:44 +02:00
2021-07-04 08:13:36 +02:00
when ( [ 'pause' , 'unpause' ] ) {
if ( $ command eq 'pause' ) {
2021-07-08 18:30:32 +02:00
$ self - > { state_data } - > { paused } = $ self - > { PAUSED_BY_PLAYER } ;
2021-07-04 08:13:36 +02:00
} else {
$ self - > { state_data } - > { paused } = 0 ;
}
return "/msg $channel $nick has " . ( $ self - > { state_data } - > { paused } ? 'paused' : 'unpaused' ) . " the game!" ;
}
2020-02-15 23:38:32 +01:00
when ( 'score' ) {
2021-07-03 00:58:05 +02:00
if ( $ self - > { current_state } ne 'move' and $ self - > { current_state } ne 'attack' ) {
return "There is no Battleship score to show right now." ;
2020-02-15 23:38:32 +01:00
}
2021-07-03 00:58:05 +02:00
$ self - > show_scoreboard ;
return '' ;
2020-02-15 23:38:32 +01:00
}
2018-07-01 12:07:44 +02:00
2020-02-15 23:38:32 +01:00
when ( 'players' ) {
2021-07-03 00:58:05 +02:00
if ( not @ { $ self - > { state_data } - > { players } } ) {
return "There are no players playing Battleship right now. Start a game with the `challenge` command!" ;
2021-06-29 23:48:55 +02:00
}
2021-07-03 00:58:05 +02:00
$ self - > list_players ;
return '' ;
2020-02-15 23:38:32 +01:00
}
2018-07-01 12:07:44 +02:00
2020-02-15 23:38:32 +01:00
when ( 'kick' ) {
2021-06-29 23:48:55 +02:00
if ( not $ self - > { pbot } - > { users } - > loggedin_admin ( $ channel , $ hostmask ) ) {
2021-07-03 00:58:05 +02:00
return "$nick: Only admins may kick players from the game." ;
2020-05-04 22:21:35 +02:00
}
2018-07-01 12:07:44 +02:00
2021-06-29 23:48:55 +02:00
if ( not length $ arguments ) {
return "Usage: battleship kick <nick>" ;
}
2018-07-01 12:07:44 +02:00
2021-07-03 00:58:05 +02:00
# get the id associated with this nick, in case the current player has changed nick while playing
my ( $ id ) = $ self - > { pbot } - > { messagehistory } - > { database } - > find_message_account_by_nick ( $ arguments ) ;
if ( not defined $ id ) {
return "I don't know anybody named $arguments." ;
}
$ id = $ self - > { pbot } - > { messagehistory } - > { database } - > get_ancestor_id ( $ id ) ;
2018-07-01 12:07:44 +02:00
2020-02-15 23:38:32 +01:00
for ( my $ i = 0 ; $ i < @ { $ self - > { state_data } - > { players } } ; $ i + + ) {
2021-07-03 00:58:05 +02:00
if ( lc $ self - > { state_data } - > { players } - > [ $ i ] - > { id } == $ id ) {
2020-02-15 23:38:32 +01:00
$ self - > { state_data } - > { players } - > [ $ i ] - > { removed } = 1 ;
2021-07-03 00:58:05 +02:00
return "/msg $channel $nick: $arguments has been kicked from the game." ;
2020-02-15 23:38:32 +01:00
}
}
2018-07-01 12:07:44 +02:00
2021-07-03 00:58:05 +02:00
return "$nick: $arguments isn't even in the game." ;
2018-07-01 12:07:44 +02:00
}
2020-02-15 23:38:32 +01:00
when ( 'bomb' ) {
2021-07-03 00:58:05 +02:00
if ( $ self - > { current_state } ne 'move' and $ self - > { current_state } ne 'attack' ) {
2021-06-29 23:48:55 +02:00
return "$nick: It's not time to do that now." ;
}
2018-07-01 12:07:44 +02:00
2021-07-03 00:58:05 +02:00
my $ id = $ self - > get_player_id ( $ nick , $ user , $ host ) ;
2018-07-03 08:33:22 +02:00
2021-07-03 00:58:05 +02:00
my ( $ player ) = grep { $ _ - > { id } == $ id } @ { $ self - > { state_data } - > { players } } ;
2018-07-03 08:33:22 +02:00
2021-07-03 00:58:05 +02:00
if ( not defined $ player ) {
2021-06-29 23:48:55 +02:00
return "You are not playing in this game." ;
}
2018-07-04 05:03:49 +02:00
2021-06-29 23:48:55 +02:00
# no arguments provided
2020-02-15 23:38:32 +01:00
if ( not length $ arguments ) {
2021-07-03 00:58:05 +02:00
if ( delete $ player - > { location } ) {
2021-06-29 23:48:55 +02:00
return "$nick: Attack location cleared." ;
} else {
return "$nick: Usage: bomb <location>" ;
}
2020-02-15 23:38:32 +01:00
}
2018-07-03 08:33:22 +02:00
2021-06-29 23:48:55 +02:00
# validate arguments
2020-02-15 23:38:32 +01:00
$ arguments = uc $ arguments ;
2018-07-03 08:33:22 +02:00
2021-06-29 23:48:55 +02:00
if ( $ arguments !~ m/^[A-Z][0-9]+$/ ) {
return "$nick: Usage: bomb <location>; <location> must be in the form of A15, B3, C9, etc." ;
}
# ensure arguments are within range of battlefield
my ( $ x , $ y ) = $ arguments =~ m/^(.)(.*)/ ;
2018-07-03 08:33:22 +02:00
2020-02-15 23:38:32 +01:00
$ x = ord ( $ x ) - 65 ;
2018-07-01 12:07:44 +02:00
2021-06-29 23:48:55 +02:00
if ( $ x < 0 || $ x > $ self - > { N_Y } || $ y < 0 || $ y > $ self - > { N_X } ) {
return "$nick: Target out of range, try again." ;
}
2018-07-01 12:07:44 +02:00
2021-07-03 00:58:05 +02:00
my $ msg ;
if ( not exists $ player - > { location } ) {
2021-07-03 20:01:37 +02:00
$ msg = "/msg $channel $nick aims somewhere." ;
2020-02-15 23:38:32 +01:00
}
2021-07-03 00:58:05 +02:00
elsif ( lc $ player - > { location } eq lc $ arguments ) {
return '' ;
2021-06-29 23:48:55 +02:00
}
2021-07-03 00:58:05 +02:00
else {
2021-07-03 20:01:37 +02:00
$ msg = "/msg $channel $nick aims somewhere else." ;
2020-02-15 23:38:32 +01:00
}
2021-07-03 00:58:05 +02:00
$ player - > { location } = $ arguments ;
return $ msg ;
2020-02-15 23:38:32 +01:00
}
2019-06-26 18:34:19 +02:00
2021-06-29 23:48:55 +02:00
when ( [ 'specboard' , 'board' ] ) {
2021-07-03 00:58:05 +02:00
if ( grep { $ _ eq $ self - > { current_state } } qw/nogame challenge genboard gameover/ ) {
2021-06-29 23:48:55 +02:00
return "$nick: There is no board to show right now." ;
2020-02-15 23:38:32 +01:00
}
2021-06-29 23:48:55 +02:00
# specifically show spectator board, even if invoked by a player
2020-02-15 23:38:32 +01:00
if ( $ _ eq 'specboard' ) {
2021-07-03 00:58:05 +02:00
$ self - > show_battlefield ( $ self - > { BOARD_SPECTATOR } ) ;
2021-06-29 23:48:55 +02:00
return '' ;
2020-02-15 23:38:32 +01:00
}
2021-07-03 00:58:05 +02:00
my $ id = $ self - > get_player_id ( $ nick , $ user , $ host ) ;
2021-06-29 23:48:55 +02:00
2021-07-03 00:58:05 +02:00
# show player's personal board if playing
for ( my $ i = 0 ; $ i < @ { $ self - > { state_data } - > { players } } ; $ i + + ) {
2020-02-15 23:38:32 +01:00
if ( $ self - > { state_data } - > { players } - > [ $ i ] - > { id } == $ id ) {
2021-07-03 20:01:37 +02:00
if ( $ self - > { state_data } - > { players } - > [ $ i ] - > { removed } ) {
return "$nick: You have been removed from this game. Try again next game." ;
}
2021-06-29 23:48:55 +02:00
$ self - > send_message ( $ channel , "$nick surveys the battlefield!" ) ;
2020-02-15 23:38:32 +01:00
$ self - > show_battlefield ( $ i ) ;
2021-06-29 23:48:55 +02:00
return '' ;
2020-02-15 23:38:32 +01:00
}
}
2021-06-29 23:48:55 +02:00
# otherwise show spectator board
2021-07-03 00:58:05 +02:00
$ self - > show_battlefield ( $ self - > { BOARD_SPECTATOR } ) ;
return '' ;
2020-02-15 23:38:32 +01:00
}
2018-07-01 12:07:44 +02:00
2021-07-03 00:58:05 +02:00
# this command shows the entire battlefield
2020-02-15 23:38:32 +01:00
when ( 'fullboard' ) {
2021-06-29 23:48:55 +02:00
if ( not $ self - > { pbot } - > { users } - > loggedin_admin ( $ channel , $ hostmask ) ) {
return "$nick: Only admins may see the full board." ;
2020-05-04 22:21:35 +02:00
}
2020-02-15 23:38:32 +01:00
2021-07-03 00:58:05 +02:00
if ( grep { $ _ eq $ self - > { current_state } } qw/nogame challenge genboard gameover/ ) {
2021-06-29 23:48:55 +02:00
return "$nick: There is no board to show right now." ;
2020-02-15 23:38:32 +01:00
}
2021-07-03 00:58:05 +02:00
# show real board if admin is in the game ... no cheating!
my $ id = $ self - > get_player_id ( $ nick , $ user , $ host ) ;
for ( my $ i = 0 ; $ i < @ { $ self - > { state_data } - > { players } } ; $ i + + ) {
2020-02-15 23:38:32 +01:00
if ( $ self - > { state_data } - > { players } - > [ $ i ] - > { id } == $ id ) {
2021-06-29 23:48:55 +02:00
$ self - > send_message ( $ channel , "$nick surveys the battlefield!" ) ;
2020-02-15 23:38:32 +01:00
$ self - > show_battlefield ( $ i ) ;
2021-06-29 23:48:55 +02:00
return '' ;
2020-02-15 23:38:32 +01:00
}
}
2018-07-01 12:07:44 +02:00
2021-06-29 23:48:55 +02:00
# show full board
2021-07-03 00:58:05 +02:00
$ self - > show_battlefield ( $ self - > { BOARD_FULL } , $ nick ) ;
2020-02-15 23:38:32 +01:00
}
2018-07-01 12:07:44 +02:00
2021-06-29 23:48:55 +02:00
default { return $ usage ; }
2018-07-01 12:07:44 +02:00
}
}
2021-06-29 23:48:55 +02:00
# add a message to PBot output queue, optionally with a delay
2018-07-01 12:07:44 +02:00
sub send_message {
2020-02-15 23:38:32 +01:00
my ( $ self , $ to , $ text , $ delay ) = @ _ ;
2021-06-23 20:45:53 +02:00
2021-06-29 23:48:55 +02:00
$ delay // = 0 ;
2021-06-23 20:45:53 +02:00
2020-02-15 23:38:32 +01:00
my $ botnick = $ self - > { pbot } - > { registry } - > get_value ( 'irc' , 'botnick' ) ;
2021-06-24 01:09:39 +02:00
2020-02-15 23:38:32 +01:00
my $ message = {
2021-06-21 00:10:16 +02:00
nick = > $ botnick ,
user = > 'battleship' ,
host = > 'localhost' ,
hostmask = > "$botnick!battleship\@localhost" ,
command = > 'battleship' ,
checkflood = > 1 ,
message = > $ text
2020-02-15 23:38:32 +01:00
} ;
2021-06-24 01:09:39 +02:00
2020-02-15 23:38:32 +01:00
$ self - > { pbot } - > { interpreter } - > add_message_to_output_queue ( $ to , $ message , $ delay ) ;
2018-07-01 12:07:44 +02:00
}
2021-07-03 00:58:05 +02:00
# get unambiguous internal id for player hostmask
sub get_player_id {
my ( $ self , $ nick , $ user , $ host ) = @ _ ;
my $ id = $ self - > { pbot } - > { messagehistory } - > { database } - > get_message_account ( $ nick , $ user , $ host ) ;
return $ self - > { pbot } - > { messagehistory } - > { database } - > get_ancestor_id ( $ id ) ;
}
2018-07-01 12:07:44 +02:00
2021-07-03 00:58:05 +02:00
# create a new player hash
sub new_player {
my ( $ self , $ id , $ nick ) = @ _ ;
return {
id = > $ id ,
name = > $ nick ,
2021-07-03 20:01:37 +02:00
index = > 0 ,
2021-07-03 00:58:05 +02:00
ready = > 0 ,
health = > 0 ,
2021-07-03 20:01:37 +02:00
ships = > 0 ,
2021-07-03 00:58:05 +02:00
shots = > 0 ,
hit = > 0 ,
miss = > 0 ,
sunk = > 0 ,
2021-07-03 20:01:37 +02:00
lost = > 0 ,
2021-07-03 00:58:05 +02:00
missedinputs = > 0 ,
} ;
}
2018-07-01 12:07:44 +02:00
2021-07-03 00:58:05 +02:00
# get a random number interval [lower, upper)
sub number {
my ( $ self , $ lower , $ upper ) = @ _ ;
return int rand ( $ upper - $ lower ) + $ lower ;
}
2018-07-01 12:07:44 +02:00
# battleship stuff
2021-07-03 00:58:05 +02:00
sub begin_game_loop {
my ( $ self ) = @ _ ;
# add `battleship loop` event repeating at 1s interval
$ self - > { pbot } - > { event_queue } - > enqueue_event (
sub {
$ self - > run_one_state ;
} ,
1 , 'battleship loop' , 1
) ;
}
2018-07-01 12:07:44 +02:00
2021-07-03 00:58:05 +02:00
sub end_game_loop {
my ( $ self ) = @ _ ;
# remove `battleship loop` event
2018-07-03 08:33:22 +02:00
2021-07-03 20:01:37 +02:00
# repeating events get added back to event queue if we attempt to
# dequeue_event() from within the event itself. we turn repeating
# off to ensure the event gets removed when it completes.
2021-07-03 00:58:05 +02:00
$ self - > { pbot } - > { event_queue } - > update_repeating ( 'battleship loop' , 0 ) ;
2018-07-01 12:07:44 +02:00
2021-07-03 20:01:37 +02:00
# dequeue event.
2021-07-03 00:58:05 +02:00
$ self - > { pbot } - > { event_queue } - > dequeue_event ( 'battleship loop' , 0 ) ;
}
2018-07-01 12:07:44 +02:00
2021-07-03 00:58:05 +02:00
sub init_game {
my ( $ self , $ state ) = @ _ ;
2018-07-01 12:07:44 +02:00
2021-07-03 00:58:05 +02:00
# default board dimensions
$ self - > { N_X } = $ self - > { BOARD_X } ;
$ self - > { N_Y } = $ self - > { BOARD_Y } ;
2018-07-01 12:07:44 +02:00
2021-07-03 00:58:05 +02:00
# increase board width by player count
$ self - > { N_X } += @ { $ state - > { players } } * 2 ;
2020-02-15 23:38:32 +01:00
2021-07-03 00:58:05 +02:00
# default count of ships per player
$ self - > { SHIPS } = $ self - > { SHIP_COUNT } ;
2020-02-15 23:38:32 +01:00
2021-07-03 00:58:05 +02:00
# initialize ship length fields
for ( my $ ship = 0 ; $ ship < $ self - > { SHIPS } ; $ ship + + ) {
$ self - > { ship_length } - > [ $ ship ] = 0 ;
2018-07-01 12:07:44 +02:00
}
2021-07-03 00:58:05 +02:00
# the battleship board/grid. all ships share the same grid to add an
# element of strategy (namely, ships cannot overlap thus you know
# where your enemy ships are NOT located, which narrows the battle
# field and helps speed games up)
$ self - > { board } = [] ;
2021-07-03 20:01:37 +02:00
# reset winner flag
$ self - > { got_winner } = 0 ;
2021-07-03 00:58:05 +02:00
# place ships and ocean tiles
return $ self - > generate_battlefield ;
2018-07-01 12:07:44 +02:00
}
2021-07-03 00:58:05 +02:00
# ensures a ship can be placed at this location (all desired tiles are ocean)
sub check_ship_placement {
my ( $ self , $ x , $ y , $ o , $ l ) = @ _ ;
2021-06-29 23:48:55 +02:00
2020-02-15 23:38:32 +01:00
my ( $ xd , $ yd , $ i ) ;
2018-07-01 12:07:44 +02:00
2021-07-03 00:58:05 +02:00
if ( $ o == $ self - > { ORIENT_VERT } ) {
if ( $ y + $ l >= $ self - > { N_Y } ) {
return 0 ;
2020-02-15 23:38:32 +01:00
}
$ xd = 0 ;
2021-07-03 00:58:05 +02:00
$ yd = 1 ;
2018-07-01 12:07:44 +02:00
} else {
2021-07-03 00:58:05 +02:00
if ( $ x + $ l >= $ self - > { N_X } ) {
return 0 ;
2020-02-15 23:38:32 +01:00
}
2021-07-03 00:58:05 +02:00
$ xd = 1 ;
2020-02-15 23:38:32 +01:00
$ yd = 0 ;
2018-07-01 12:07:44 +02:00
}
2020-02-15 23:38:32 +01:00
for ( my $ i = 0 ; $ i < $ l ; $ i + + ) {
2021-07-03 00:58:05 +02:00
if ( $ self - > { board } - > [ $ x += $ o == $ self - > { ORIENT_HORIZ } ? $ xd : 0 ] [ $ y += $ o == $ self - > { ORIENT_HORIZ } ? 0 : $ yd ] - > { type } != $ self - > { TYPE_OCEAN } ) {
2021-06-29 23:48:55 +02:00
return 0 ;
}
2018-07-01 12:07:44 +02:00
}
2020-02-15 23:38:32 +01:00
return 1 ;
2018-07-01 12:07:44 +02:00
}
2021-07-03 00:58:05 +02:00
# attempt to place a ship on the battlefield
sub place_ship {
my ( $ self , $ player_id , $ player_index , $ ship ) = @ _ ;
2021-06-29 23:48:55 +02:00
2021-07-03 20:01:37 +02:00
my ( $ x , $ y , $ o , $ i , $ l ) ;
2020-02-15 23:38:32 +01:00
my ( $ yd , $ xd ) = ( 0 , 0 ) ;
2021-07-05 05:35:01 +02:00
for ( my $ attempt = 0 ; $ attempt < 1000 ; $ attempt + + ) {
2021-07-03 00:58:05 +02:00
$ x = $ self - > number ( 0 , $ self - > { N_X } ) ;
$ y = $ self - > number ( 0 , $ self - > { N_Y } ) ;
2020-02-15 23:38:32 +01:00
$ o = $ self - > number ( 1 , 10 ) < 6 ;
2021-06-29 23:48:55 +02:00
if ( $ self - > { ship_length } - > [ $ ship ] ) {
2021-07-03 00:58:05 +02:00
# reuse saved length so all players have equal sized ships.
# perfectly balanced as all things must be.
2021-06-29 23:48:55 +02:00
$ l = $ self - > { ship_length } - > [ $ ship ] ;
} else {
2021-07-03 00:58:05 +02:00
# generate a random length ship
# TODO: perhaps use a fixed array of guaranteed ship lengths?
# i think random is more exciting because you never know what
# kinds of ships are going to be out there.
$ l = $ self - > number ( 2 , 6 ) ;
2021-06-29 23:48:55 +02:00
}
2020-02-15 23:38:32 +01:00
2021-06-29 23:48:55 +02:00
if ( $ self - > { debug } ) {
2021-07-03 20:01:37 +02:00
$ self - > { pbot } - > { logger } - > log ( "attempt to place ship for player $player_index: ship $ship x,y: $x,$y o: $o length: $l\n" ) ;
2021-06-29 23:48:55 +02:00
}
2020-02-15 23:38:32 +01:00
2021-07-03 00:58:05 +02:00
if ( $ self - > check_ship_placement ( $ x , $ y , $ o , $ l ) ) {
2020-02-15 23:38:32 +01:00
if ( ! $ o ) {
2021-07-03 00:58:05 +02:00
$ self - > { vert } + + ;
2021-06-29 23:48:55 +02:00
2021-07-03 00:58:05 +02:00
if ( $ self - > { horiz } < $ self - > { SHIPS } / 2 ) {
# generate a battlefield with half vertical and half horizontal ships
# perfectly balanced as all things must be.
next ;
2021-06-29 23:48:55 +02:00
}
2021-07-03 00:58:05 +02:00
$ yd = 1 ;
2020-02-15 23:38:32 +01:00
$ xd = 0 ;
} else {
$ self - > { horiz } + + ;
2021-06-29 23:48:55 +02:00
2021-07-03 00:58:05 +02:00
if ( $ self - > { vert } < $ self - > { SHIPS } / 2 ) {
# generate a battlefield with half vertical and half horizontal ships
# perfectly balanced as all things must be.
next ;
2021-06-29 23:48:55 +02:00
}
2021-07-03 00:58:05 +02:00
$ xd = 1 ;
2020-02-15 23:38:32 +01:00
$ yd = 0 ;
}
for ( my $ i = 0 ; $ i < $ l ; $ i + + ) {
2021-07-03 00:58:05 +02:00
my $ tile_data = {
type = > $ self - > { TYPE_SHIP } ,
player_id = > $ player_id ,
player_index = > $ player_index ,
orientation = > $ o ,
length = > $ l ,
index = > $ i ,
hit_by = > 0 ,
} ;
$ self - > { board } - > [ $ x += $ o == $ self - > { ORIENT_HORIZ } ? $ xd : 0 ] [ $ y += $ o == $ self - > { ORIENT_HORIZ } ? 0 : $ yd ] = $ tile_data ;
2020-02-15 23:38:32 +01:00
}
$ self - > { ship_length } - > [ $ ship ] = $ l ;
2021-07-03 00:58:05 +02:00
$ self - > { state_data } - > { players } - > [ $ player_index ] - > { health } += $ l ;
2021-07-03 20:01:37 +02:00
$ self - > { state_data } - > { players } - > [ $ player_index ] - > { ships } += 1 ;
2020-02-15 23:38:32 +01:00
return 1 ;
2018-07-01 12:07:44 +02:00
}
2021-07-03 00:58:05 +02:00
}
2018-07-01 12:07:44 +02:00
2021-07-03 00:58:05 +02:00
return 0 ;
}
sub place_whirlpool {
my ( $ self ) = @ _ ;
2021-07-05 05:35:01 +02:00
for ( my $ attempt = 0 ; $ attempt < 1000 ; $ attempt + + ) {
2021-07-03 00:58:05 +02:00
my $ x = $ self - > number ( 0 , $ self - > { N_X } ) ;
my $ y = $ self - > number ( 0 , $ self - > { N_Y } ) ;
# skip non-ocean tiles
if ( $ self - > { board } - > [ $ x ] [ $ y ] - > { type } != $ self - > { TYPE_OCEAN } ) {
next ;
2020-02-15 23:38:32 +01:00
}
2021-07-03 00:58:05 +02:00
# replace ocean tile with whirlpool
$ self - > { board } - > [ $ x ] [ $ y ] - > { type } = $ self - > { TYPE_WHIRLPOOL } ;
$ self - > { board } - > [ $ x ] [ $ y ] - > { tile } = $ self - > { TILE_OCEAN } ; # whirlpools hidden initially, until shot
return 1 ;
2018-07-01 12:07:44 +02:00
}
2021-07-03 00:58:05 +02:00
$ self - > { pbot } - > { logger } - > log ( "Failed to place whirlpool.\n" ) ;
return 0 ;
2018-07-01 12:07:44 +02:00
}
sub generate_battlefield {
2020-02-15 23:38:32 +01:00
my ( $ self ) = @ _ ;
2018-07-01 12:07:44 +02:00
2021-07-03 00:58:05 +02:00
# fill board with ocean
for ( my $ x = 0 ; $ x < $ self - > { N_X } ; $ x + + ) {
for ( my $ y = 0 ; $ y < $ self - > { N_Y } ; $ y + + ) {
$ self - > { board } - > [ $ x ] [ $ y ] = {
type = > $ self - > { TYPE_OCEAN } ,
tile = > $ self - > { TILE_OCEAN } ,
} ;
2021-06-29 23:48:55 +02:00
}
2018-07-01 12:07:44 +02:00
}
2021-07-03 00:58:05 +02:00
# place ships
for ( my $ player_index = 0 ; $ player_index < @ { $ self - > { state_data } - > { players } } ; $ player_index + + ) {
# counts how many horizontal/vertical ships have been placed so far
$ self - > { horiz } = 0 ;
$ self - > { vert } = 0 ;
for ( my $ ship = 0 ; $ ship < $ self - > { SHIPS } ; $ ship + + ) {
if ( ! $ self - > place_ship ( $ self - > { state_data } - > { players } - > [ $ player_index ] - > { id } , $ player_index , $ ship ) ) {
return 0 ;
}
}
}
# place whirlpools (2 whirlpools per player)
for ( my $ whirlpool = 0 ; $ whirlpool < @ { $ self - > { state_data } - > { players } } * 2 ; $ whirlpool + + ) {
if ( ! $ self - > place_whirlpool ) {
2021-06-29 23:48:55 +02:00
return 0 ;
}
2018-07-03 08:33:22 +02:00
}
2021-06-29 23:48:55 +02:00
2020-02-15 23:38:32 +01:00
return 1 ;
2018-07-01 12:07:44 +02:00
}
2021-07-03 00:58:05 +02:00
# we hit a ship; check if the ship has sunk
2018-07-01 12:07:44 +02:00
sub check_sunk {
2021-07-03 00:58:05 +02:00
my ( $ self , $ x , $ y ) = @ _ ;
2018-07-01 12:07:44 +02:00
2021-07-03 00:58:05 +02:00
# alias to the tile we hit
my $ tile = $ self - > { board } - > [ $ x ] [ $ y ] ;
2018-07-01 12:07:44 +02:00
2021-07-03 00:58:05 +02:00
if ( $ tile - > { orientation } == $ self - > { ORIENT_VERT } ) {
my $ top = $ y - $ tile - > { index } ;
my $ bottom = $ y + ( $ tile - > { length } - ( $ tile - > { index } + 1 ) ) ;
2020-02-15 23:38:32 +01:00
2021-07-03 20:01:37 +02:00
for ( my $ i = $ bottom ; $ i >= $ top ; $ i - - ) {
2021-07-03 00:58:05 +02:00
if ( not $ self - > { board } - > [ $ x ] [ $ i ] - > { hit_by } ) {
return 0 ;
2020-02-15 23:38:32 +01:00
}
2018-07-01 12:07:44 +02:00
}
2021-07-03 00:58:05 +02:00
return 1 ;
2018-07-01 12:07:44 +02:00
} else {
2021-07-03 00:58:05 +02:00
my $ left = $ x - $ tile - > { index } ;
my $ right = $ x + ( $ tile - > { length } - ( $ tile - > { index } + 1 ) ) ;
2020-02-15 23:38:32 +01:00
2021-07-03 20:01:37 +02:00
for ( my $ i = $ right ; $ i >= $ left ; $ i - - ) {
2021-07-03 00:58:05 +02:00
if ( not $ self - > { board } - > [ $ i ] [ $ y ] - > { hit_by } ) {
return 0 ;
2021-06-29 23:48:55 +02:00
}
2020-02-15 23:38:32 +01:00
}
2021-07-03 00:58:05 +02:00
return 1 ;
2018-07-01 12:07:44 +02:00
}
2021-07-03 00:58:05 +02:00
}
2018-07-01 12:07:44 +02:00
2021-07-03 00:58:05 +02:00
sub get_attack_text {
my ( $ self ) = @ _ ;
2018-07-01 12:07:44 +02:00
2020-02-15 23:38:32 +01:00
my @ attacks = (
2021-06-29 23:48:55 +02:00
"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" ,
2020-02-15 23:38:32 +01:00
) ;
2018-07-01 12:07:44 +02:00
2021-07-03 00:58:05 +02:00
return $ attacks [ rand @ attacks ] ;
}
# checks if we hit whirlpool, ocean, ship, etc
2021-07-05 05:35:01 +02:00
# reveals struck whirlpools
2021-07-03 00:58:05 +02:00
sub check_hit {
my ( $ self , $ state , $ player , $ location_data ) = @ _ ;
my ( $ x , $ y , $ location ) = (
$ location_data - > { x } ,
$ location_data - > { y } ,
$ location_data - > { location } ,
) ;
# check if we hit a whirlpool. if so, reveal whirlpool on the
# battlefield and deflect the shot
if ( $ self - > { board } - > [ $ x ] [ $ y ] - > { type } == $ self - > { TYPE_WHIRLPOOL } ) {
# reveal this whirlpool
$ self - > { board } - > [ $ x ] [ $ y ] - > { tile } = $ self - > { TILE_WHIRLPOOL } ;
my $ attack = $ self - > get_attack_text ;
# keep trying until we don't hit another whirlpool
while ( 1 ) {
2021-07-03 20:01:37 +02:00
$ self - > send_message ( $ self - > { channel } , "$player->{name} $attack $location! $color{cyan}--- SPLASH! ---$color{reset}" ) ;
2021-07-03 00:58:05 +02:00
$ x = $ self - > number ( 0 , $ self - > { N_X } ) ;
$ y = $ self - > number ( 0 , $ self - > { N_Y } ) ;
$ location = ( 'A' .. 'Z' ) [ $ y ] . ( $ x + 1 ) ;
$ self - > send_message ( $ self - > { channel } , "$player->{name} hit a whirlpool! It deflects their attack to $location!" ) ;
if ( $ self - > { board } - > [ $ x ] [ $ y ] - > { type } == $ self - > { TYPE_WHIRLPOOL } ) {
# hit another whirlpool
next ;
}
# update new location for caller
$ location_data - > { x } = $ x ;
$ location_data - > { y } = $ y ;
$ location_data - > { location } = $ location ;
last ;
}
}
# hit a ship, damage self or enemy alike
if ( $ self - > { board } - > [ $ x ] [ $ y ] - > { type } == $ self - > { TYPE_SHIP } ) {
2021-07-03 20:01:37 +02:00
my $ player_index = $ self - > { board } - > [ $ x ] [ $ y ] - > { player_index } ;
if ( $ state - > { players } - > [ $ player_index ] - > { removed } ) {
# removed players no longer exist
return 0 ;
}
if ( $ self - > { board } - > [ $ x ] [ $ y ] - > { hit_by } ) {
# this piece has already been struck
return 0 ;
} else {
# a hit! a very palpable hit.
return 1 ;
}
2021-07-03 00:58:05 +02:00
}
2021-07-03 20:01:37 +02:00
# no hit
2021-07-03 00:58:05 +02:00
return 0 ;
}
sub perform_attack {
my ( $ self , $ state , $ player ) = @ _ ;
$ player - > { shots } + + ;
# random attack verb
my $ attack = $ self - > get_attack_text ;
# attack location
my $ location = delete $ player - > { location } ;
# convert attack location to board coordinates
my ( $ y , $ x ) = $ location =~ /^(.)(.*)/ ;
$ y = ord ( $ y ) - 65 ;
$ x - - ;
# set location data reference so check_hit can update values
my $ location_data = {
x = > $ x ,
y = > $ y ,
location = > $ location ,
} ;
# launch a shot and see if it hit a ship (handles hitting whirlpools, ocean, etc)
2021-07-05 05:35:01 +02:00
my $ hit_ship = $ self - > check_hit ( $ state , $ player , $ location_data ) ;
2021-07-03 00:58:05 +02:00
2021-07-05 05:35:01 +02:00
# location_data can be updated by whirlpools, etc
$ x = $ location_data - > { x } ;
$ y = $ location_data - > { y } ;
$ location = $ location_data - > { location } ;
2021-06-29 23:48:55 +02:00
2021-07-05 05:35:01 +02:00
if ( $ hit_ship ) {
# player hit a ship!
2021-07-03 00:58:05 +02:00
$ self - > send_message ( $ self - > { channel } , "$player->{name} $attack $location! $color{red}--- HIT! --- $color{reset}" ) ;
2021-06-29 23:48:55 +02:00
2021-07-03 00:58:05 +02:00
$ player - > { hit } + + ;
2018-07-01 12:07:44 +02:00
2021-07-03 00:58:05 +02:00
# place hit marker
$ self - > { board } - > [ $ x ] [ $ y ] - > { tile } = $ color { red } . $ self - > { TILE_HIT } - > [ $ player - > { index } ] ;
$ self - > { board } - > [ $ x ] [ $ y ] - > { hit_by } = $ player - > { id } ;
2021-07-03 20:01:37 +02:00
my $ victim = $ self - > { state_data } - > { players } - > [ $ self - > { board } - > [ $ x ] [ $ y ] - > { player_index } ] ;
# deduct hit points from victim
$ victim - > { health } -= 1 ;
2021-07-03 00:58:05 +02:00
# check if ship has sunk (reveal what kind and whose ship it is)
if ( $ self - > check_sunk ( $ x , $ y ) ) {
$ player - > { sunk } + + ;
2021-07-03 20:01:37 +02:00
$ victim - > { ships } - - ;
2021-07-03 00:58:05 +02:00
2021-07-04 08:13:36 +02:00
my $ length = $ self - > { board } - > [ $ x ] [ $ y ] - > { length } ;
2021-07-03 00:58:05 +02:00
my % ship_names = (
5 = > 'battleship' ,
4 = > 'destroyer' ,
3 = > 'submarine' ,
2 = > 'patrol boat' ,
) ;
2021-07-03 20:01:37 +02:00
my $ ships_left = $ victim - > { ships } ;
my $ sections_left = $ victim - > { health } ;
my $ ships = 'ship' . ( $ ships_left != 1 ? 's' : '' ) ;
my $ sections = 'section' . ( $ sections_left != 1 ? 's' : '' ) ;
if ( $ sections_left > 0 ) {
$ self - > send_message ( $ self - > { channel } , "$color{red}$player->{name} has sunk $victim->{name}'s $ship_names{$length}! $victim->{name} has $ships_left $ships and $sections_left $sections remaining!$color{reset}" ) ;
} else {
2021-07-03 20:19:09 +02:00
$ self - > send_message ( $ self - > { channel } , "$color{red}$player->{name} has sunk $victim->{name}'s final $ship_names{$length}! $victim->{name} is out of the game!$color{reset}" ) ;
2021-07-03 20:01:37 +02:00
$ victim - > { lost } = 1 ;
# check if there is only one player still standing
my $ still_alive = 0 ;
my $ winner ;
foreach my $ p ( @ { $ state - > { players } } ) {
next if $ p - > { removed } or $ p - > { lost } ;
$ still_alive + + ;
$ winner = $ p ;
}
2018-07-01 12:07:44 +02:00
2021-07-03 20:01:37 +02:00
if ( $ still_alive == 1 ) {
$ self - > send_message ( $ self - > { channel } , "$color{yellow}$winner->{name} has won the game of Battleship!$color{reset}" ) ;
$ self - > { got_winner } = 1 ;
}
2020-02-15 23:38:32 +01:00
}
}
} else {
2021-07-03 00:58:05 +02:00
# player missed
$ self - > send_message ( $ self - > { channel } , "$player->{name} $attack $location! --- miss ---" ) ;
$ player - > { miss } + + ;
2021-07-05 05:35:01 +02:00
# place miss marker
2021-07-03 00:58:05 +02:00
if ( $ self - > { board } - > [ $ x ] [ $ y ] - > { type } == $ self - > { TYPE_OCEAN } ) {
$ self - > { board } - > [ $ x ] [ $ y ] - > { tile } = $ self - > { TILE_MISS } ;
$ self - > { board } - > [ $ x ] [ $ y ] - > { missed_by } = $ player - > { id } ;
}
}
2018-07-01 12:07:44 +02:00
}
2021-07-03 00:58:05 +02:00
sub list_players {
2020-02-15 23:38:32 +01:00
my ( $ self ) = @ _ ;
2018-07-01 12:07:44 +02:00
2021-07-03 00:58:05 +02:00
my @ players ;
2019-06-26 18:34:19 +02:00
2021-07-03 00:58:05 +02:00
foreach my $ player ( @ { $ self - > { state_data } - > { players } } ) {
push @ players , $ player - > { name } . ( $ player - > { ready } ? '' : " $color{red}(not ready)$color{reset}" ) ;
2018-07-02 02:01:18 +02:00
}
2021-07-03 00:58:05 +02:00
if ( @ players ) {
$ self - > send_message ( $ self - > { channel } , "Current players: " . ( join ', ' , @ players ) . ". Use `ready` when you are." ) ;
}
}
2021-06-29 23:48:55 +02:00
2021-07-03 00:58:05 +02:00
sub show_scoreboard {
my ( $ self ) = @ _ ;
2021-06-29 23:48:55 +02:00
2021-07-03 20:01:37 +02:00
foreach my $ player ( sort { $ b - > { health } <=> $ a - > { health } } @ { $ self - > { state_data } - > { players } } ) {
next if $ player - > { removed } ;
2021-07-03 20:31:01 +02:00
my $ buf = sprintf ( "%-10s shots: %2d, hit: %2d, miss: %2d, acc: %3d%%, sunk: %2d, ships left: %d, sections left: %2d" ,
2021-07-03 00:58:05 +02:00
"$player->{name}:" ,
$ player - > { shots } ,
$ player - > { hit } ,
$ player - > { miss } ,
int ( ( $ player - > { hit } / ( $ player - > { shots } ? $ player - > { shots } : 1 ) ) * 100 ) ,
$ player - > { sunk } ,
2021-07-03 20:01:37 +02:00
$ player - > { ships } ,
2021-07-03 00:58:05 +02:00
$ player - > { health } ,
) ;
2021-06-29 23:48:55 +02:00
2021-07-03 00:58:05 +02:00
$ self - > send_message ( $ self - > { channel } , $ buf ) ;
}
2020-02-15 23:38:32 +01:00
}
2018-07-01 12:07:44 +02:00
2020-02-15 23:38:32 +01:00
sub show_battlefield {
2021-07-03 00:58:05 +02:00
my ( $ self , $ player_index , $ nick ) = @ _ ;
$ self - > { pbot } - > { logger } - > log ( "Showing battlefield for player $player_index\n" ) ;
my $ player ;
if ( $ player_index >= 0 ) {
$ player = $ self - > { state_data } - > { players } - > [ $ player_index ] ;
}
2020-02-15 23:38:32 +01:00
2021-07-03 00:58:05 +02:00
my $ output ;
2020-02-15 23:38:32 +01:00
2021-07-03 00:58:05 +02:00
# player hit markers, for legend
my $ hits ;
foreach my $ p ( @ { $ self - > { state_data } - > { players } } ) {
$ hits . = "$p->{name} hit: $color{red}" . ( $ p - > { index } + 1 ) . "$color{reset} " ;
}
# render legend
if ( $ player ) {
2021-07-03 20:01:37 +02:00
$ output . = "Legend: Your ships: $self->{TILE_SHIP_VERT} $self->{TILE_SHIP_HORIZ}$color{reset} ${hits}ocean: $self->{TILE_OCEAN}$color{reset} miss: $self->{TILE_MISS}$color{reset} whirlpool: $self->{TILE_WHIRLPOOL}$color{reset}\n" ;
2021-07-03 00:58:05 +02:00
}
elsif ( $ player_index == $ self - > { BOARD_FULL } or $ player_index == $ self - > { BOARD_FINAL } ) {
my $ ships ;
foreach my $ p ( @ { $ self - > { state_data } - > { players } } ) {
$ ships . = "$p->{name}: $self->{TILE_SHIP}->[$p->{index}] " ;
}
$ output . = "Legend: ${ships}${hits}ocean: $self->{TILE_OCEAN}$color{reset} miss: $self->{TILE_MISS}$color{reset} whirlpool: $self->{TILE_WHIRLPOOL}$color{reset}\n" ;
}
else {
# spectator
$ output . = "Legend: ${hits}ocean: $self->{TILE_OCEAN}$color{reset} miss: $self->{TILE_MISS}$color{reset} whirlpool: $self->{TILE_WHIRLPOOL}$color{reset}\n" ;
}
# render top column coordinates
$ output . = "$color{cyan},01 " ;
2020-02-15 23:38:32 +01:00
2021-06-29 23:48:55 +02:00
for ( my $ x = 1 ; $ x < $ self - > { N_X } + 1 ; $ x + + ) {
2020-02-15 23:38:32 +01:00
if ( $ x % 10 == 0 ) {
2021-07-03 00:58:05 +02:00
$ output . = "$color{yellow},01" if $ self - > { N_X } > 10 ;
$ output . = $ x % 10 ;
$ output . = ' ' ;
$ output . = "$color{cyan},01" if $ self - > { N_X } > 10 ;
2018-07-01 12:07:44 +02:00
} else {
2021-07-03 00:58:05 +02:00
$ output . = $ x % 10 ;
$ output . = ' ' ;
2018-07-01 12:07:44 +02:00
}
2020-02-15 23:38:32 +01:00
}
2021-07-03 00:58:05 +02:00
$ output . = "\n" ;
2020-02-15 23:38:32 +01:00
2021-07-03 00:58:05 +02:00
# render battlefield row by row
2021-06-29 23:48:55 +02:00
for ( my $ y = 0 ; $ y < $ self - > { N_Y } ; $ y + + ) {
2021-07-03 00:58:05 +02:00
# left row coordinates
$ output . = sprintf ( "$color{cyan},01%c " , 97 + $ y ) ;
2021-06-29 23:48:55 +02:00
2021-07-03 00:58:05 +02:00
# render a row of the board column by column
2021-06-29 23:48:55 +02:00
for ( my $ x = 0 ; $ x < $ self - > { N_X } ; $ x + + ) {
2021-07-03 00:58:05 +02:00
my $ tile = $ self - > { board } - > [ $ x ] [ $ y ] ;
# render ocean/whirlpool, miss, but not hits or ships yet
if ( $ tile - > { type } != $ self - > { TYPE_SHIP } ) {
# reveal whirlpools on full/final boards
if ( $ player_index == $ self - > { BOARD_FULL } || $ player_index == $ self - > { BOARD_FINAL } ) {
if ( $ tile - > { type } == $ self - > { TYPE_WHIRLPOOL } ) {
$ output . = $ self - > { TILE_WHIRLPOOL } . ' ' ;
2020-02-15 23:38:32 +01:00
} else {
2021-07-03 00:58:05 +02:00
# render normal tile (ocean, miss)
$ output . = $ tile - > { tile } . ' ' ;
2020-02-15 23:38:32 +01:00
}
} else {
2021-07-03 00:58:05 +02:00
# render normal tile (ocean, revealed/hidden whirlpools, miss)
$ output . = $ tile - > { tile } . ' ' ;
2020-02-15 23:38:32 +01:00
}
2021-07-03 00:58:05 +02:00
next ;
}
2021-06-29 23:48:55 +02:00
2021-07-03 00:58:05 +02:00
# render hits
if ( $ tile - > { hit_by } ) {
$ output . = $ tile - > { tile } . ' ' ;
next ;
}
# render ships
# render player's view
if ( $ player ) {
# not player's ship
if ( $ tile - > { player_id } != $ player - > { id } ) {
# ship not found yet, show ocean
$ output . = $ self - > { TILE_OCEAN } . ' ' ;
2020-02-15 23:38:32 +01:00
next ;
2021-07-03 00:58:05 +02:00
}
if ( $ tile - > { orientation } == $ self - > { ORIENT_VERT } ) {
# vertical ship
$ output . = $ self - > { TILE_SHIP_VERT } ;
2020-02-15 23:38:32 +01:00
} else {
2021-07-03 00:58:05 +02:00
# horizontal ship
$ output . = $ self - > { TILE_SHIP_HORIZ } ;
2020-02-15 23:38:32 +01:00
}
2021-06-29 23:48:55 +02:00
2021-07-03 00:58:05 +02:00
$ output . = ' ' ;
next ;
2020-02-15 23:38:32 +01:00
}
2021-07-03 00:58:05 +02:00
# otherwise render spectator, full or final board
# spectators are not allowed to see ships unless hit
if ( $ player_index == $ self - > { BOARD_SPECTATOR } ) {
# ship not found yet, show ocean
$ output . = $ self - > { TILE_OCEAN } . ' ' ;
next ;
}
# full or final board, show all ships
$ output . = $ color { white } . $ self - > { TILE_SHIP } - > [ $ tile - > { player_index } ] . ' ' ;
2018-07-01 12:07:44 +02:00
}
2021-06-29 23:48:55 +02:00
2021-07-03 00:58:05 +02:00
# right row coordinates
$ output . = sprintf ( "$color{cyan},01%c" , 97 + $ y ) ;
$ output . = "$color{reset}\n" ;
2020-02-15 23:38:32 +01:00
}
2021-07-03 00:58:05 +02:00
# bottom column coordinates
$ output . = "$color{cyan},01 " ;
2020-02-15 23:38:32 +01:00
2021-06-29 23:48:55 +02:00
for ( my $ x = 1 ; $ x < $ self - > { N_X } + 1 ; $ x + + ) {
2020-02-15 23:38:32 +01:00
if ( $ x % 10 == 0 ) {
2021-07-03 00:58:05 +02:00
$ output . = $ color { yellow } , 01 if $ self - > { N_X } > 10 ;
$ output . = $ x % 10 ;
$ output . = ' ' ;
$ output . = $ color { cyan } , 01 if $ self - > { N_X } > 10 ;
2018-07-01 12:07:44 +02:00
} else {
2021-07-03 00:58:05 +02:00
$ output . = $ x % 10 ;
$ output . = ' ' ;
2018-07-01 12:07:44 +02:00
}
}
2020-02-15 23:38:32 +01:00
2021-07-03 00:58:05 +02:00
$ output . = "\n" ;
2020-02-15 23:38:32 +01:00
2021-07-03 00:58:05 +02:00
# send output, one message per line
foreach my $ line ( split /\n/ , $ output ) {
if ( $ player ) {
# player
$ self - > send_message ( $ player - > { name } , $ line ) ;
2021-06-29 23:48:55 +02:00
}
2021-07-03 00:58:05 +02:00
elsif ( $ player_index == $ self - > { BOARD_FULL } ) {
# full
$ self - > send_message ( $ nick , $ line ) ;
2021-06-29 23:48:55 +02:00
}
else {
2021-07-03 00:58:05 +02:00
# spectator, final
$ self - > send_message ( $ self - > { channel } , $ line ) ;
2021-06-29 23:48:55 +02:00
}
2018-07-01 12:07:44 +02:00
}
}
2021-06-29 23:48:55 +02:00
# game state machine stuff
# do one loop of the game engine
sub run_one_state {
my ( $ self ) = @ _ ;
2021-07-04 08:13:36 +02:00
# don't run a game loop if we're paused
if ( $ self - > { state_data } - > { paused } ) {
return ;
}
2021-06-29 23:48:55 +02:00
# check for naughty or missing players
2021-07-03 20:01:37 +02:00
my $ players = 0 ;
2021-07-03 00:58:05 +02:00
foreach my $ player ( @ { $ self - > { state_data } - > { players } } ) {
2021-07-03 20:01:37 +02:00
next if $ player - > { removed } or $ player - > { lost } ;
2021-07-03 00:58:05 +02:00
2021-07-03 20:01:37 +02:00
# remove player if they have missed 3 inputs
2021-07-03 20:19:09 +02:00
if ( $ player - > { missedinputs } >= $ self - > { MAX_MISSED_MOVES } ) {
2021-06-29 23:48:55 +02:00
$ self - > send_message (
$ self - > { channel } ,
2021-07-03 00:58:05 +02:00
"$color{red}$player->{name} has missed too many moves and has been ejected from the game!$color{reset}"
2021-06-29 23:48:55 +02:00
) ;
2021-07-03 00:58:05 +02:00
$ player - > { removed } = 1 ;
2021-07-03 20:01:37 +02:00
next ;
2021-06-29 23:48:55 +02:00
}
2021-07-03 20:01:37 +02:00
# count players still in the game
$ players + + ;
2021-07-03 00:58:05 +02:00
}
2021-07-03 20:01:37 +02:00
# ensure there are at least 2 players still playing
2021-07-03 00:58:05 +02:00
if ( $ self - > { current_state } eq 'move' or $ self - > { current_state } eq 'attack' ) {
2021-07-03 20:01:37 +02:00
if ( $ players < 2 and not $ self - > { got_winner } ) {
2021-07-03 00:58:05 +02:00
$ self - > send_message ( $ self - > { channel } , "Not enough players left in the game. Aborting..." ) ;
2021-06-29 23:48:55 +02:00
$ self - > set_state ( 'gameover' ) ;
}
}
# transitioned to a brand new state; prepare first tock
if ( $ self - > { previous_state } ne $ self - > { current_state } ) {
$ self - > { state_data } - > { ticks } = 1 ;
$ self - > { state_data } - > { first_tock } = 1 ;
$ self - > { state_data } - > { tocks } = 0 ;
# dump new state data for logging/debugging
if ( $ self - > { debug } ) {
$ Data:: Dumper:: Useqq = 1 ;
$ Data:: Dumper:: Sortkeys = 1 ;
$ self - > { pbot } - > { logger } - > log ( "Battleship: New state: $self->{current_state}\n" . Dumper $ self - > { state_data } ) ;
}
}
# run one state/tick
$ self - > { states } - > { $ self - > { current_state } } - > { sub } - > ( $ self - > { state_data } ) ;
# transition to next state
$ self - > { previous_state } = $ self - > { current_state } ;
$ self - > { current_state } = $ self - > { states } - > { $ self - > { current_state } } - > { trans } - > { $ self - > { state_data } - > { trans } } ;
# reset tick-tock once we've tocked
if ( $ self - > { state_data } - > { tocked } ) {
$ self - > { state_data } - > { tocked } = 0 ;
$ self - > { state_data } - > { ticks } = 0 ;
$ self - > { state_data } - > { first_tock } = 0 ;
}
# next tick
$ self - > { state_data } - > { ticks } + + ;
}
# skip directly to a state
sub set_state {
my ( $ self , $ newstate ) = @ _ ;
$ self - > { previous_state } = $ self - > { current_state } ;
$ self - > { current_state } = $ newstate ;
$ self - > { state_data } - > { ticks } = 0 ;
}
# set up game state machine
sub create_states {
my ( $ self ) = @ _ ;
$ self - > { pbot } - > { logger } - > log ( "Battleship: Creating game state machine\n" ) ;
# initialize default state
$ self - > { previous_state } = '' ;
$ self - > { current_state } = 'nogame' ;
# initialize state data
$ self - > { state_data } = {
2021-07-04 08:13:36 +02:00
players = > [] , # array of player data
ticks = > 0 , # number of ticks elapsed
paused = > 0 , # is the game paused?
2021-06-29 23:48:55 +02:00
} ;
$ self - > { states } = {
nogame = > {
2021-07-03 00:58:05 +02:00
sub => sub { $ self - > state_nogame ( @ _ ) } ,
2021-06-29 23:48:55 +02:00
trans = > {
2021-07-03 00:58:05 +02:00
challenge = > 'challenge' ,
2021-06-29 23:48:55 +02:00
nogame = > 'nogame' ,
}
} ,
2021-07-03 00:58:05 +02:00
challenge = > {
sub => sub { $ self - > state_challenge ( @ _ ) } ,
2021-06-29 23:48:55 +02:00
trans = > {
stop = > 'nogame' ,
2021-07-03 00:58:05 +02:00
wait = > 'challenge' ,
ready = > 'genboard' ,
2021-06-29 23:48:55 +02:00
}
} ,
genboard = > {
2021-07-03 00:58:05 +02:00
sub => sub { $ self - > state_genboard ( @ _ ) } ,
2021-06-29 23:48:55 +02:00
trans = > {
2021-07-03 00:58:05 +02:00
fail = > 'nogame' ,
2021-06-29 23:48:55 +02:00
next = > 'showboard' ,
}
} ,
showboard = > {
2021-07-03 00:58:05 +02:00
sub => sub { $ self - > state_showboard ( @ _ ) } ,
2021-06-29 23:48:55 +02:00
trans = > {
2021-07-03 00:58:05 +02:00
next = > 'move' ,
2021-06-29 23:48:55 +02:00
}
} ,
2021-07-03 00:58:05 +02:00
move = > {
sub => sub { $ self - > state_move ( @ _ ) } ,
2021-06-29 23:48:55 +02:00
trans = > {
2021-07-03 00:58:05 +02:00
wait = > 'move' ,
next = > 'attack' ,
2021-06-29 23:48:55 +02:00
}
} ,
2021-07-03 00:58:05 +02:00
attack = > {
sub => sub { $ self - > state_attack ( @ _ ) } ,
2021-06-29 23:48:55 +02:00
trans = > {
gotwinner = > 'gameover' ,
2021-07-03 00:58:05 +02:00
next = > 'move' ,
2021-06-29 23:48:55 +02:00
}
} ,
gameover = > {
2021-07-03 00:58:05 +02:00
sub => sub { $ self - > state_gameover ( @ _ ) } ,
2021-06-29 23:48:55 +02:00
trans = > {
next = > 'nogame' ,
}
} ,
} ;
}
# game states
2018-07-01 12:07:44 +02:00
2021-07-03 00:58:05 +02:00
sub state_nogame {
2020-02-15 23:38:32 +01:00
my ( $ self , $ state ) = @ _ ;
2021-07-03 00:58:05 +02:00
$ self - > end_game_loop ;
2021-06-29 23:48:55 +02:00
$ state - > { trans } = 'nogame' ;
2018-07-01 12:07:44 +02:00
}
2021-07-03 00:58:05 +02:00
sub state_challenge {
2020-02-15 23:38:32 +01:00
my ( $ self , $ state ) = @ _ ;
2018-07-01 12:07:44 +02:00
2021-07-03 00:58:05 +02:00
# max number of times to perform tock action
$ state - > { tock_limit } = 5 ;
2018-07-01 12:07:44 +02:00
2021-07-03 00:58:05 +02:00
# tock every 60 ticks
my $ tock = 60 ;
2018-07-01 12:07:44 +02:00
2021-07-03 00:58:05 +02:00
# every tick we check if all players have readied yet
my $ ready = 0 ;
foreach my $ player ( @ { $ state - > { players } } ) {
$ ready + + if $ player - > { ready } ;
}
2020-02-15 23:38:32 +01:00
2021-07-03 00:58:05 +02:00
# is it time for a tock?
2020-02-15 23:38:32 +01:00
if ( $ state - > { ticks } % $ tock == 0 ) {
2021-07-03 00:58:05 +02:00
$ state - > { tocked } = 1 ; # we've tocked
2020-02-15 23:38:32 +01:00
2021-07-03 00:58:05 +02:00
# reached maximum number of tocks
2021-06-29 23:48:55 +02:00
if ( + + $ state - > { tocks } > $ state - > { tock_limit } ) {
2021-07-03 00:58:05 +02:00
$ self - > send_message ( $ self - > { channel } , "Not all players have readied in time. The game has been aborted." ) ;
2021-06-29 23:48:55 +02:00
$ state - > { trans } = 'stop' ;
2020-02-15 23:38:32 +01:00
$ state - > { players } = [] ;
2021-06-29 23:48:55 +02:00
return ;
2020-02-15 23:38:32 +01:00
}
2021-07-03 00:58:05 +02:00
my $ max = $ self - > { MAX_PLAYERS } ;
my $ avail = $ max - @ { $ self - > { state_data } - > { players } } ;
my $ slots = 'slot' . ( $ avail == 1 ? '' : 's' ) ;
$ self - > send_message ( $ self - > { channel } , "There is a game of Battleship available! Use `accept` to enter the fray ($avail/$max $slots open)." ) ;
$ self - > list_players ;
if ( $ ready == 1 && @ { $ self - > { state_data } - > { players } } == 1 ) {
$ self - > send_message ( $ self - > { channel } , "Cannot begin game with one player." ) ;
2020-02-15 23:38:32 +01:00
}
2018-07-02 03:46:58 +02:00
}
2018-07-01 12:07:44 +02:00
2021-07-03 00:58:05 +02:00
if ( $ ready >= 2 && $ ready == @ { $ state - > { players } } ) {
# all players ready (min 2 players to start)
$ self - > send_message ( $ self - > { channel } , "All players ready!" ) ;
$ state - > { trans } = 'ready' ;
} else {
# wait another tick
$ state - > { trans } = 'wait' ;
}
2018-07-01 12:07:44 +02:00
}
2021-07-03 00:58:05 +02:00
sub state_genboard {
2020-02-15 23:38:32 +01:00
my ( $ self , $ state ) = @ _ ;
2021-07-03 00:58:05 +02:00
if ( ! $ self - > init_game ( $ state ) ) {
$ self - > { pbot } - > { logger } - > log ( "Failed to generate battlefield\n" ) ;
$ self - > send_message ( $ self - > { channel } , "Failed to generate a suitable battlefield. Please try again." ) ;
$ state - > { trans } = 'fail' ;
} else {
$ state - > { tock_limit } = 3 ;
$ state - > { trans } = 'next' ;
}
2018-07-01 12:07:44 +02:00
}
2021-07-03 00:58:05 +02:00
sub state_showboard {
2020-02-15 23:38:32 +01:00
my ( $ self , $ state ) = @ _ ;
2021-07-03 00:58:05 +02:00
2021-07-04 08:13:36 +02:00
# pause the game to send the boards to all the players.
# this is due to output pacing; the messages are trickled out slowly
# to avoid overflowing the ircd's receive queue. we do not want the
# game state to advance while the messages are being sent out. the
2021-07-05 18:38:37 +02:00
# game will resume when the `pbot.output_queue_empty` notification
2021-07-04 08:13:36 +02:00
# is received.
2021-07-08 18:30:32 +02:00
$ state - > { paused } = $ self - > { PAUSED_FOR_OUTPUT_QUEUE } ;
2021-07-04 08:13:36 +02:00
2021-07-03 00:58:05 +02:00
for ( my $ player = 0 ; $ player < @ { $ state - > { players } } ; $ player + + ) {
$ self - > send_message ( $ self - > { channel } , "Showing battlefield to $state->{players}->[$player]->{name}..." ) ;
$ self - > show_battlefield ( $ player ) ;
}
2020-02-15 23:38:32 +01:00
$ self - > send_message ( $ self - > { channel } , "Fight! Anybody (players and spectators) can use `board` at any time to see the battlefield." ) ;
2021-06-29 23:48:55 +02:00
$ state - > { trans } = 'next' ;
2018-07-01 12:07:44 +02:00
}
2021-07-03 00:58:05 +02:00
sub state_move {
2020-02-15 23:38:32 +01:00
my ( $ self , $ state ) = @ _ ;
2018-07-01 12:07:44 +02:00
2021-07-03 00:58:05 +02:00
# allow 5 tocks before players have missed their move
$ state - > { tock_limit } = 5 ;
# tock every 15 ticks
2021-06-29 23:48:55 +02:00
my $ tock = 15 ;
2021-07-03 00:58:05 +02:00
# tock sooner if this is the first
2021-06-29 23:48:55 +02:00
if ( $ state - > { first_tock } ) {
2021-07-03 00:58:05 +02:00
$ tock = 2 ;
2021-06-29 23:48:55 +02:00
}
2018-07-01 12:07:44 +02:00
2021-07-03 00:58:05 +02:00
# every tick, check if all players have moved
my $ moved = 0 ;
my $ players = 0 ;
2020-02-15 23:38:32 +01:00
2021-07-03 00:58:05 +02:00
foreach my $ player ( @ { $ state - > { players } } ) {
2021-07-03 20:19:09 +02:00
next if $ player - > { removed } or $ player - > { lost } ;
$ moved + + if $ player - > { location } ;
$ players + + ;
2021-07-03 00:58:05 +02:00
}
2020-02-15 23:38:32 +01:00
2021-07-03 00:58:05 +02:00
if ( $ moved == $ players ) {
# all players have moved
$ state - > { trans } = 'next' ;
return ;
2018-07-02 05:39:55 +02:00
}
2021-07-03 00:58:05 +02:00
# tock!
2020-02-15 23:38:32 +01:00
if ( $ state - > { ticks } % $ tock == 0 ) {
$ state - > { tocked } = 1 ;
2021-06-29 23:48:55 +02:00
if ( + + $ state - > { tocks } > $ state - > { tock_limit } ) {
2021-07-03 00:58:05 +02:00
# tock limit reached, flag all players who haven't moved
my @ missed ;
foreach my $ player ( @ { $ state - > { players } } ) {
2021-07-03 20:19:09 +02:00
next if $ player - > { removed } or $ player - > { lost } ;
2021-07-03 00:58:05 +02:00
if ( not $ player - > { location } ) {
$ player - > { missedinputs } + + ;
push @ missed , $ player - > { name } ;
}
}
my $ msg = join ', ' , @ missed ;
$ msg . = " failed to launch an attack in time. They forfeit their turn!" ;
2021-06-29 23:48:55 +02:00
2021-07-03 00:58:05 +02:00
$ self - > send_message ( $ self - > { channel } , $ msg ) ;
$ state - > { trans } = 'next' ;
2021-06-29 23:48:55 +02:00
return ;
2020-02-15 23:38:32 +01:00
}
2018-07-02 05:39:55 +02:00
2021-07-03 00:58:05 +02:00
# notify all players who haven't moved yet
my @ pending ;
foreach my $ player ( @ { $ state - > { players } } ) {
2021-07-03 20:19:09 +02:00
next if $ player - > { removed } or $ player - > { lost } ;
2021-07-03 00:58:05 +02:00
if ( not $ player - > { location } ) {
push @ pending , $ player - > { name } ;
}
}
my $ players = join ', ' , @ pending ;
2018-07-02 05:39:55 +02:00
2021-07-03 00:58:05 +02:00
my $ warning = $ state - > { tocks } == $ state - > { tock_limit } ? $ color { red } : '' ;
2020-02-15 23:38:32 +01:00
2021-07-03 00:58:05 +02:00
my $ remaining = 15 * $ state - > { tock_limit } ;
$ remaining -= 15 * ( $ state - > { tocks } - 1 ) ;
$ remaining = "(" . ( concise duration $ remaining ) . " remaining)" ;
$ self - > send_message ( $ self - > { channel } , "$players: $warning$remaining Launch an attack now via `bomb <location>`!$color{reset}" ) ;
2020-02-15 23:38:32 +01:00
}
2018-07-02 05:39:55 +02:00
2021-06-29 23:48:55 +02:00
$ state - > { trans } = 'wait' ;
2018-07-01 12:07:44 +02:00
}
2021-07-03 00:58:05 +02:00
sub state_attack {
2020-02-15 23:38:32 +01:00
my ( $ self , $ state ) = @ _ ;
2018-07-02 05:39:55 +02:00
2021-07-03 00:58:05 +02:00
my $ trans = 'next' ;
foreach my $ player ( @ { $ state - > { players } } ) {
# skip removed players
next if $ player - > { removed } ;
# skip players who haven't moved
next if not $ player - > { location } ;
# launch attack
$ self - > perform_attack ( $ state , $ player ) ;
2021-07-03 20:01:37 +02:00
# transition to gameover if someone won
$ trans = 'gotwinner' if $ self - > { got_winner } ;
2021-06-29 23:48:55 +02:00
}
2021-07-03 00:58:05 +02:00
$ state - > { trans } = $ trans ;
2018-07-01 12:07:44 +02:00
}
2021-07-03 00:58:05 +02:00
sub state_gameover {
2020-02-15 23:38:32 +01:00
my ( $ self , $ state ) = @ _ ;
2021-06-29 23:48:55 +02:00
2021-07-03 00:58:05 +02:00
if ( @ { $ state - > { players } } >= 2 ) {
$ self - > show_battlefield ( $ self - > { BOARD_FINAL } ) ;
$ self - > show_scoreboard ;
$ self - > send_message ( $ self - > { channel } , "Game over!" ) ;
2018-07-03 08:33:22 +02:00
}
2021-07-03 00:58:05 +02:00
$ state - > { players } = [] ;
$ state - > { trans } = 'next' ;
2018-07-01 12:07:44 +02:00
}
1 ;