mirror of
https://github.com/pragma-/pbot.git
synced 2024-12-27 13:12:42 +01:00
Plugins/Battleship: fix some bugs
This commit is contained in:
parent
e12b269fdb
commit
074ddce530
@ -1,10 +1,10 @@
|
||||
# File: Battleship.pm
|
||||
#
|
||||
# Purpose: Simplified version of the Battleship board game. In this variant,
|
||||
# there is only one game grid/board and every player's ships share it.
|
||||
# This adds an element of strategy: everybody knows where their own ships
|
||||
# are located and they know that ships cannot overlap, ergo they know where
|
||||
# NOT to aim. This helps to speed games up by removing some randomness.
|
||||
# there is one game grid/board and every player's ships share it without
|
||||
# overlapping. This adds nn element of strategy: everybody knows where their
|
||||
# own ships are located, ergo they know where NOT to aim. This helps to speed
|
||||
# games up by removing some randomness.
|
||||
#
|
||||
# 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
|
||||
@ -18,7 +18,8 @@
|
||||
# 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
|
||||
# the shot to a random tile.
|
||||
# the shot to a random tile. Much of the IOCCC silliness has been removed so that
|
||||
# I can maintain this code without going insane.
|
||||
|
||||
# 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
|
||||
@ -74,10 +75,16 @@ sub initialize {
|
||||
# player limit per game
|
||||
$self->{MAX_PLAYERS} = 5;
|
||||
|
||||
# types of board tiles
|
||||
$self->{TYPE_OCEAN} = 0;
|
||||
$self->{TYPE_WHIRLPOOL} = 1;
|
||||
$self->{TYPE_SHIP} = 2;
|
||||
|
||||
# battleship tile symbols
|
||||
$self->{TILE_HIT} = ['1' .. $self->{MAX_PLAYERS}];
|
||||
$self->{TILE_OCEAN} = "$color{blue}~";
|
||||
$self->{TILE_MISS} = "$color{cyan}o";
|
||||
$self->{TILE_HIT} = ['1' .. $self->{MAX_PLAYERS}];
|
||||
$self->{TILE_OCEAN} = "$color{blue}~";
|
||||
$self->{TILE_MISS} = "$color{cyan}o";
|
||||
$self->{TILE_WHIRLPOOL} = "$color{cyan}@";
|
||||
|
||||
# personal ship tiles shown on player board
|
||||
$self->{TILE_SHIP_VERT} = "$color{white}|";
|
||||
@ -86,29 +93,18 @@ sub initialize {
|
||||
# all player ship tiles shown on final/full board
|
||||
$self->{TILE_SHIP} = ['A' .. chr ord('A') + $self->{MAX_PLAYERS} - 1];
|
||||
|
||||
# initially hidden by ocean, revealed when shot.
|
||||
# when shot, sends particle to random coordinate
|
||||
# and damages any ship there, including player's
|
||||
$self->{TILE_WHIRLPOOL} = "$color{cyan}@";
|
||||
|
||||
# default board dimensions
|
||||
$self->{BOARD_X} = 14;
|
||||
$self->{BOARD_X} = 12;
|
||||
$self->{BOARD_Y} = 8;
|
||||
|
||||
# number of ships per player
|
||||
$self->{SHIP_COUNT} = 8;
|
||||
$self->{SHIP_COUNT} = 6;
|
||||
|
||||
# modifiers for show_battlefield()
|
||||
$self->{BOARD_SPECTATOR} = -1;
|
||||
$self->{BOARD_FINAL} = -2;
|
||||
$self->{BOARD_FULL} = -3;
|
||||
|
||||
# types of board tiles
|
||||
$self->{TYPE_OCEAN} = 0;
|
||||
$self->{TYPE_SHIP} = 1;
|
||||
$self->{TYPE_MISS} = 2;
|
||||
$self->{TYPE_WHIRLPOOL} = 3;
|
||||
|
||||
# ship orientation
|
||||
$self->{ORIENT_VERT} = 0;
|
||||
$self->{ORIENT_HORIZ} = 1;
|
||||
@ -177,7 +173,7 @@ sub cmd_battleship {
|
||||
return "There is already a game of Battleship underway.";
|
||||
}
|
||||
|
||||
# set game to the `accept` state to begin accepting challenges
|
||||
# set game to the `challenge` state to begin accepting challenge
|
||||
$self->set_state('challenge');
|
||||
|
||||
# add player 0, the challenger, to the game
|
||||
@ -371,13 +367,13 @@ sub cmd_battleship {
|
||||
|
||||
my $msg;
|
||||
if (not exists $player->{location}) {
|
||||
$msg = "/msg $channel $nick aims at $arguments.";
|
||||
$msg = "/msg $channel $nick aims somewhere.";
|
||||
}
|
||||
elsif (lc $player->{location} eq lc $arguments) {
|
||||
return '';
|
||||
}
|
||||
else {
|
||||
$msg = "/msg $channel $nick aims at $arguments instead of $player->{location}.";
|
||||
$msg = "/msg $channel $nick aims somewhere else.";
|
||||
}
|
||||
$player->{location} = $arguments;
|
||||
return $msg;
|
||||
@ -399,6 +395,10 @@ sub cmd_battleship {
|
||||
# show player's personal board if playing
|
||||
for (my $i = 0; $i < @{$self->{state_data}->{players}}; $i++) {
|
||||
if ($self->{state_data}->{players}->[$i]->{id} == $id) {
|
||||
if ($self->{state_data}->{players}->[$i]->{removed}) {
|
||||
return "$nick: You have been removed from this game. Try again next game.";
|
||||
}
|
||||
|
||||
$self->send_message($channel, "$nick surveys the battlefield!");
|
||||
$self->show_battlefield($i);
|
||||
return '';
|
||||
@ -473,13 +473,15 @@ sub new_player {
|
||||
return {
|
||||
id => $id,
|
||||
name => $nick,
|
||||
index => 0,
|
||||
ready => 0,
|
||||
health => 0,
|
||||
ships => 0,
|
||||
shots => 0,
|
||||
hit => 0,
|
||||
miss => 0,
|
||||
sunk => 0,
|
||||
index => 0,
|
||||
lost => 0,
|
||||
missedinputs => 0,
|
||||
};
|
||||
}
|
||||
@ -507,13 +509,12 @@ sub end_game_loop {
|
||||
my ($self) = @_;
|
||||
# remove `battleship loop` event
|
||||
|
||||
# repeating events get re-added to queue after event completes, so we
|
||||
# must set repeating to 0 to ensure the event gets removed
|
||||
# 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.
|
||||
$self->{pbot}->{event_queue}->update_repeating('battleship loop', 0);
|
||||
|
||||
# dequeue_event cannot remove repeating events if dequeue_event is called
|
||||
# from within a repeating event since the event infrastructure will just
|
||||
# re-add it afterwards (in other words, don't delete the above line)
|
||||
# dequeue event.
|
||||
$self->{pbot}->{event_queue}->dequeue_event('battleship loop', 0);
|
||||
}
|
||||
|
||||
@ -541,6 +542,9 @@ sub init_game {
|
||||
# field and helps speed games up)
|
||||
$self->{board} = [];
|
||||
|
||||
# reset winner flag
|
||||
$self->{got_winner} = 0;
|
||||
|
||||
# place ships and ocean tiles
|
||||
return $self->generate_battlefield;
|
||||
}
|
||||
@ -578,7 +582,7 @@ sub check_ship_placement {
|
||||
sub place_ship {
|
||||
my ($self, $player_id, $player_index, $ship) = @_;
|
||||
|
||||
my ($x, $y, $o, $d, $i, $l);
|
||||
my ($x, $y, $o, $i, $l);
|
||||
my ($yd, $xd) = (0, 0);
|
||||
|
||||
my $fail = 0;
|
||||
@ -602,7 +606,7 @@ sub place_ship {
|
||||
}
|
||||
|
||||
if ($self->{debug}) {
|
||||
$self->{pbot}->{logger}->log("attempt to place ship for player $player_index: ship $ship x,y: $x,$y o,d: $o,$d length: $l\n");
|
||||
$self->{pbot}->{logger}->log("attempt to place ship for player $player_index: ship $ship x,y: $x,$y o: $o length: $l\n");
|
||||
}
|
||||
|
||||
if ($self->check_ship_placement($x, $y, $o, $l)) {
|
||||
@ -650,6 +654,7 @@ sub place_ship {
|
||||
|
||||
$self->{ship_length}->[$ship] = $l;
|
||||
$self->{state_data}->{players}->[$player_index]->{health} += $l;
|
||||
$self->{state_data}->{players}->[$player_index]->{ships} += 1;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@ -725,13 +730,7 @@ sub check_sunk {
|
||||
my $top = $y - $tile->{index};
|
||||
my $bottom = $y + ($tile->{length} - ($tile->{index} + 1));
|
||||
|
||||
for (my $i = $y; $i >= $top; $i--) {
|
||||
if (not $self->{board}->[$x][$i]->{hit_by}) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
for (my $i = $y + 1; $i <= $bottom; $i++) {
|
||||
for (my $i = $bottom; $i >= $top; $i--) {
|
||||
if (not $self->{board}->[$x][$i]->{hit_by}) {
|
||||
return 0;
|
||||
}
|
||||
@ -742,13 +741,7 @@ sub check_sunk {
|
||||
my $left = $x - $tile->{index};
|
||||
my $right = $x + ($tile->{length} - ($tile->{index} + 1));
|
||||
|
||||
for (my $i = $x; $i >= $left; $i--) {
|
||||
if (not $self->{board}->[$i][$y]->{hit_by}) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
for (my $i = $x + 1; $i <= $right; $i++) {
|
||||
for (my $i = $right; $i >= $left; $i--) {
|
||||
if (not $self->{board}->[$i][$y]->{hit_by}) {
|
||||
return 0;
|
||||
}
|
||||
@ -796,7 +789,7 @@ sub check_hit {
|
||||
|
||||
# keep trying until we don't hit another whirlpool
|
||||
while (1) {
|
||||
$self->send_message($self->{channel}, "$player->{name} $attack $location! $color{cyan}--- SPLASH! ---$color{reset} ");
|
||||
$self->send_message($self->{channel}, "$player->{name} $attack $location! $color{cyan}--- SPLASH! ---$color{reset}");
|
||||
|
||||
$x = $self->number(0, $self->{N_X});
|
||||
$y = $self->number(0, $self->{N_Y});
|
||||
@ -821,10 +814,23 @@ sub check_hit {
|
||||
|
||||
# hit a ship, damage self or enemy alike
|
||||
if ($self->{board}->[$x][$y]->{type} == $self->{TYPE_SHIP}) {
|
||||
return 1;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
# did not hit whirlpool or ship
|
||||
# no hit
|
||||
return 0;
|
||||
}
|
||||
|
||||
@ -868,16 +874,17 @@ sub perform_attack {
|
||||
$self->{board}->[$x][$y]->{tile} = $color{red} . $self->{TILE_HIT}->[$player->{index}];
|
||||
$self->{board}->[$x][$y]->{hit_by} = $player->{id};
|
||||
|
||||
# deduct hit points from player
|
||||
$player->{health} -= 1;
|
||||
my $victim = $self->{state_data}->{players}->[$self->{board}->[$x][$y]->{player_index}];
|
||||
|
||||
# deduct hit points from victim
|
||||
$victim->{health} -= 1;
|
||||
|
||||
# check if ship has sunk (reveal what kind and whose ship it is)
|
||||
if ($self->check_sunk($x, $y)) {
|
||||
$player->{sunk}++;
|
||||
$victim->{ships}--;
|
||||
|
||||
my $length = $self->{board}->[$x][$y]->{length};
|
||||
my $remaining = $player->{health};
|
||||
my $victim = $self->{state_data}->{players}->[$self->{board}->[$x][$y]->{player_index}]->{name};
|
||||
|
||||
my %ship_names = (
|
||||
5 => 'battleship',
|
||||
@ -886,11 +893,31 @@ sub perform_attack {
|
||||
2 => 'patrol boat',
|
||||
);
|
||||
|
||||
$self->send_message($self->{channel}, "$color{red}$player->{name} has sunk ${victim}'s $ship_names{$length}! $victim has $remaining ship section" . ($remaining != 1 ? 's' : '') . " remaining!$color{reset}");
|
||||
my $ships_left = $victim->{ships};
|
||||
my $sections_left = $victim->{health};
|
||||
|
||||
if ($remaining == 0) {
|
||||
$self->send_message($self->{channel}, "$player->{name} has won the game of Battleship!");
|
||||
$player->{won} = 1;
|
||||
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 {
|
||||
$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}");
|
||||
$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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@ -930,14 +957,17 @@ sub list_players {
|
||||
sub show_scoreboard {
|
||||
my ($self) = @_;
|
||||
|
||||
foreach my $player (sort { $a->{health} <=> $b->{health} } @{$self->{state_data}->{players}}) {
|
||||
my $buf = sprintf("%-15s shots: %2d, hit: %2d, miss: %2d, acc: %3d%%, sunk: %2d, sections left: %2d",
|
||||
foreach my $player (sort { $b->{health} <=> $a->{health} } @{$self->{state_data}->{players}}) {
|
||||
next if $player->{removed};
|
||||
|
||||
my $buf = sprintf("%-15s shots: %2d, hit: %2d, miss: %2d, acc: %3d%%, sunk: %2d, ships left: %d, sections left: %2d",
|
||||
"$player->{name}:",
|
||||
$player->{shots},
|
||||
$player->{hit},
|
||||
$player->{miss},
|
||||
int (($player->{hit} / ($player->{shots} ? $player->{shots} : 1)) * 100),
|
||||
$player->{sunk},
|
||||
$player->{ships},
|
||||
$player->{health},
|
||||
);
|
||||
|
||||
@ -966,7 +996,7 @@ sub show_battlefield {
|
||||
|
||||
# render legend
|
||||
if ($player) {
|
||||
$output .= "Legend: 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";
|
||||
$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";
|
||||
}
|
||||
elsif ($player_index == $self->{BOARD_FULL} or $player_index == $self->{BOARD_FINAL}) {
|
||||
my $ships;
|
||||
@ -1112,30 +1142,29 @@ sub run_one_state {
|
||||
my ($self) = @_;
|
||||
|
||||
# check for naughty or missing players
|
||||
foreach my $player (@{$self->{state_data}->{players}}) {
|
||||
next if $player->{removed};
|
||||
my $players = 0;
|
||||
|
||||
foreach my $player (@{$self->{state_data}->{players}}) {
|
||||
next if $player->{removed} or $player->{lost};
|
||||
|
||||
# remove player if they have missed 3 inputs
|
||||
if ($player->{missedinputs} >= 3) {
|
||||
# remove player if they have missed 3 inputs
|
||||
$self->send_message(
|
||||
$self->{channel},
|
||||
"$color{red}$player->{name} has missed too many moves and has been ejected from the game!$color{reset}"
|
||||
);
|
||||
|
||||
$player->{removed} = 1;
|
||||
next;
|
||||
}
|
||||
|
||||
# count players still in the game
|
||||
$players++;
|
||||
}
|
||||
|
||||
# ensure there's at least 2 players still playing
|
||||
# ensure there are at least 2 players still playing
|
||||
if ($self->{current_state} eq 'move' or $self->{current_state} eq 'attack') {
|
||||
my $players = 0;
|
||||
|
||||
foreach my $player (@{$self->{state_data}->{players}}) {
|
||||
next if $player->{removed};
|
||||
++$players;
|
||||
}
|
||||
|
||||
if ($players < 2) {
|
||||
if ($players < 2 and not $self->{got_winner}) {
|
||||
$self->send_message($self->{channel}, "Not enough players left in the game. Aborting...");
|
||||
$self->set_state('gameover');
|
||||
}
|
||||
@ -1429,8 +1458,8 @@ sub state_attack {
|
||||
# launch attack
|
||||
$self->perform_attack($state, $player);
|
||||
|
||||
# transitiion to gameover if someone won
|
||||
$trans = 'gotwinner' if $player->{won};
|
||||
# transition to gameover if someone won
|
||||
$trans = 'gotwinner' if $self->{got_winner};
|
||||
}
|
||||
|
||||
$state->{trans} = $trans;
|
||||
|
Loading…
Reference in New Issue
Block a user