#!/usr/bin/perl
#
# This file is part of Grue Hunter.
#
# Copyright (C) 2013 Jason Self <j@jxself.org>
#
# Grue Hunter gives you software freedom; you can copy, modify, convey,
# and/or redistribute it under the terms of the GNU Affero General Public
# License as published by the Free Software Foundation; either version 3 of 
# the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License 
# for more details.
#
# You should have received a copy of the GNU Affero General Public License 
# along with this program in a file called 'AGPLv3.txt'. If not, see
# http://www.gnu.org/licenses/agpl-3.0-standalone.html or write to the: Free
# Software Foundation, Inc., 51 Franklin St, Fifth Floor Boston, MA 
# 02110-1301, USA.

use strict;
use warnings;

my $instructions =  <<INSTRUCTIONS;
\033[2J
\033[0;0H
Grue Hunter version 1.0
Homepage: http://jxself.org/grue-hunter.shtml
Copyright (C) 2013 Jason Self <j\@jxself.org>

Grue Hunter gives you software freedom; you can copy, modify, convey,
and/or redistribute it under the terms of the GNU Affero General Public
License as published by the Free Software Foundation; either version 3 of
the License, or (at your option) any later version.

You should have received a copy of the GNU Affero General Public License
along with this program in a file called 'AGPLv3.txt'. If not, see
http://www.gnu.org/licenses/
%
\033[2J
\033[0;0H
In this game you are a brave hunter. You are going to enter an underground 
cave system in search of the Grue. If you can capture the Grue and get out 
alive you will win the game.
%
Once in the cave system you can move around or shoot an arrow using the 
commands 'move' and 'shoot' and any of the four compass directions. You can 
also shorten the commands to 'm' and 's' and the directions to 'n', 's', 'e', 
and 'w' for brevity. You may enter 'quit' or 'q' to end the game at any time.
%
But... There are other things in this cave system. Giant bats can pick you up 
and drop you elsewhere. There are also bottomless pits. If you fall into one 
of these you will never get out. There is also the Grue itself. Though not an 
aggressive creature, it will eat you if you come too close. There are also 
earthquakes that move things around (the bats, the bottomless pits, the Grue, 
and the exit.)
INSTRUCTIONS

my $space = <<SPACE;
The room here slopes downward. Old candles are scattered across the floor.
An old fountain sits in the middle of the room. It is covered in rubble and dirt. A toppled statue lies to the right.
The walls here are smooth. There is an old anvil here.
The walls contain claw marks and some sort of graffiti in a language you cannot read. A crack in the ceiling above the middle of the north wall allows a trickle of water to flow down to the floor. The water pools near the base of the wall, and a rivulet runs along the wall and out into the hall. The water smells fresh.
You push a heavy stone door open, revealing a room with pillars lining the wall. Several square holes have been cut into the walls.
Stalactites drip down from the ceiling in sharp points while flowstone makes strange shapes on the floor. Rusting spikes line the walls.
This room smells of rotting vegetation. Burning torches in iron sconces line the walls.
The scent of smoke fills the room. A small hole to your left attracts your attention; but it is too small to be of any concern.
A stream of oil flows from one side of the room to the other, in a small rut in the floor. There is a mysterious stain on the wall.
This is the center of a narrow passage that connects other rooms. This room is about average size; but is filled with a pungent aroma that is very nauseating.
There are cracks in the floor here. It is foggy, making it difficult to see.
Thick cobwebs fill the corners of this room and wisps of webbing hang from the ceiling, wavering in a wind you can't feel. You notice a light humming sound.
The floor is damp and looks quite slippery. You will need to be very careful here. A haze of smoke hangs in the air.
This is a very large room with a large boulder in the center of it. Part of the wall has collapsed into this room.
Several pieces of rotten fruit are scatted throughout the area. A hole has been dug into the wall. After looking inside you see that it doesn’t go anywhere.
This room is very dusty, and you see footprints set in the dust. There are spiders on the walls.
The passageway is very low; but if we crawl we can make it. After coming through the other side you see a stone stairway that goes up to the wall, where it abruptly ends.
As you enter the room part of the ceiling collapses but you jump out of the way in time to avoid getting hurt. Duck your head in here; as large rock stalactites hang from the ceiling.
This area is covered in rubble and dirt. There is a puddle and scraps of wood here.
The floor is covered with sand and pottery shards.
The walls have been engraved with geometric patterns. Fog gathers and swirls on the floor, concealing it.
There is a salty, wet smell to this area. A small hole in the ceiling lets light from outside through ... But you would never fit through it.
There are bones and several piece of rotting wood scattered here.
It is misty here. Several empty flasks are scattered throughout the area.
Rats run past you, in the direction you just came from. The floor here is covered in perfect hexagonal tiles.
There are strange looking yellow stones here. Water drips through the ceiling.
Your nostrils are overwhelmed by a sulphurous smell. The ceiling here is damp.
Grime and filth smear this room. This area is filled with stalactites and stalagmites.
This is a very dirty room; it was partially filled in by the last earthquake that hit. A slight breeze blows through this area.
A shallow pit sits in the center of the room. The walls here have been engraved with spiral patterns.
This cavern has a high, domed ceiling. There is a mold growing here.
A slight, damp breeze blows through this area. This rooms smells of mildew.
This area is very dusty. A dry well sits in the center of the room.
As you enter this room some small rocks fall from the ceiling. Several broken arrows are scatted about.
You are in a small passageway. The walls, ceiling and floor are composed of a smooth black stone. Illuminating your way is a silvery white glow coming from two long grooves midway up each wall that run the length of the passageway.
You hear an eerie sound. Jagged steel blades project from cracks in the walls.
Someone has scrawled the drawing of a door on the wall. A haze of dust hangs in the air.
The walls are covered in slime. There is broken glass on the ground.
There is a green slime on the floor. This room is very small; but I think we can make it through ok. 
There is a stale smell here. Your steps echo here.
This large room is illuminated by a low-burning fire pit. There is an iron sconce on the wall, but the torch has been removed.
A rivulet of water slowly trickles from a hole in the wall. A narrow pit covered by iron bars lies in the center of the room.
Several empty bottles lie on the floor. Water drips from the ceiling, landing on your head.
You are in a low depression in the center of a medium-sized room. A flurry of bats suddenly fly past you as you enter. Their screeching fills your ears.
Someone has left a lighted torch on the wall and it illuminates your passage. A liquid-filled pit sits in the center of the room and is so murky that you can't see its bottom. The room smells sour.
This area is only a few feet high, but crawling on your stomach you make it through. You notice centipedes crawling along the wall.
A rattling noise abruptly stops when you enter. Black mold grows on the walls and parts of the floor.
You are in a small room with rocks and debris scattered everywhere. Part of the here ceiling has collapsed.
A huge iron cage lies on its side in this room, and its gate rests open on the floor. A broken chain lies on the floor. This room has sour odor, like something rotten or fermented.
An iron chandelier hangs from the ceiling. A old wooden barrel lies on its side, mostly collapsed.
SPACE

# Constants
my $NORTH = 1;
my $SOUTH = 2;
my $EAST = 3;
my $WEST = 4;

# Update this constant to increase the number of rooms in the game.
my $MAX_ROOMS = 50;

# Update this constant if more room descriptions are added.
my $MAX_ROOM_DESCRIPTIONS = 50;

my $MAX_LINK_COUNT = 4;
my $EARTHQUAKE = 3;
my $EARTHQUAKE_CHANCE = 14;
my $EVEN_CHANCE = 2;
my $PIT_CHANCE = 32;

my $SET = 1;
my $RESET = 0;

my $CLOSED = -1;
my $GRUE = -2;
my $EXIT = -3;
my $BAT1 = -4;
my $BAT2 = -5;
my $PIT1 = -6;
my $PIT2 = -7;

# Game State
my $grue_location = $CLOSED;
my $exit_location = $CLOSED;
my $bat1_location = $CLOSED;
my $bat2_location = $CLOSED;
my $pit1_location = $CLOSED;
my $pit2_location = $CLOSED;
my $player_location = $CLOSED;
my $grue_bagged = 0;

# Game Space
my @rooms = ();

sub randomize_rooms {
	my @room_descriptions = split(/\n/, $space);
	my %random_hash = ();
	my $random_room = 0;
	my $random_room_description = 0;
	
	for (my $i = 0; $i < $MAX_ROOMS;) {
		$random_room = int(rand($MAX_ROOMS));
		$random_room_description = int(rand($MAX_ROOM_DESCRIPTIONS));
		
		if (!$random_hash{$random_room}) {
			$random_hash{$random_room} = 1;
			my %room = ();
			$room{'description'} = $room_descriptions[$random_room_description];
			$room{$NORTH} = $CLOSED;
			$room{$SOUTH} = $CLOSED;
			$room{$EAST} = $CLOSED;
			$room{$WEST} = $CLOSED;
			$room{'link_count'} = 0;
			$rooms[$i] = \%room;
			$i++;
		}
	}
}

sub connect_rooms {
	my $current_link_count = 0;
	my $random_link_count = 0;
	my $rooms_left_to_link = 0;
	my $room_direction_state = 0;
	my $next_room = 0;
	
	for (my $i = 0; $i < $MAX_ROOMS;) {
	
		# Take into consideration current link count and get random link count.
		$current_link_count = $rooms[$i] -> {'link_count'};
		if ($current_link_count < $MAX_LINK_COUNT) {
			$random_link_count = int(rand($MAX_LINK_COUNT - $current_link_count)) + 1;
			
			# Check that there are enough rooms left to link. If there are not
			# then substitute the random link count with the rooms left to link.
			$rooms_left_to_link = $MAX_ROOMS - $i;
			if ($random_link_count >= $rooms_left_to_link) {
				$random_link_count = $rooms_left_to_link;
			}
		}
		
		# If the room is fully linked then set the random link count to 0.
		else {
			$random_link_count = 0;
		}
		
		# For each room create links upto a random link count by going through
		# the vacant compass directions.
		for (my $j = 0; $j < $random_link_count; $j++) {
			for (my $k = 1; $k < 5; $k++) {
				if ($i < $MAX_ROOMS) {
					$room_direction_state = $rooms[$i] -> {$k};
					if ($room_direction_state == $CLOSED) {
						$next_room = $i + 1;
						if ($next_room < $MAX_ROOMS) {
							$rooms[$i] -> {$k} = $next_room;
							$rooms[$next_room] -> {opposite($k)} = $i;
						}
						$i = $next_room;
					}
				}
			}
		}
	}
}

sub place_directional_object {
	my $room_number = shift;
	my $object = shift;
	my $direction;
	
	# Get a random direction and set the object in a random room direction.
	$direction = int(rand(3)) + 1;
	$rooms[$room_number] -> {$direction} = $object;
	
	# Update the room's link count
	($rooms[$room_number] -> {'link_count'})++;

	if ($object == $GRUE) {
		$grue_location = $room_number;
	}
	elsif ($object == $EXIT) {
		$exit_location = $room_number;
	}
	elsif ($object == $PIT1) {
		$pit1_location = $room_number;
	}
	elsif ($object == $PIT2) {
		$pit2_location = $room_number;
	}
}

sub get_random_rooms {
	my $number_of_rooms = shift;
	my $random_num = 0;
	my %random_hash = ();
	my @room_numbers = ();

	for (my $i = 0; $i < $number_of_rooms;) {
		$random_num = int(rand($MAX_ROOMS));
		
		if (!$random_hash{$random_num}) {
			$random_hash{$random_num} = 1;
			$room_numbers[$i] = $random_num;
			$i++;
		}
	}
	return \@room_numbers;
}

sub set_state {
	my $set = shift;
	my $number_of_rooms;
	my $room_numbers;
	
	if ($grue_bagged) {
		$number_of_rooms = 5;
	}
	else {
		$number_of_rooms = 6;
	}
	
	$room_numbers = get_random_rooms($number_of_rooms);
	
	# Bats are non-directional objects and are placed in rooms. Since they are
	# non-directional they do not occupy a compass direction and do not add to the
	# room's link count. They are simply assigned to the Game State variables.
	$bat1_location = @$room_numbers[0];
	$bat2_location = @$room_numbers[1];
	
	# The Grue, Pits and Exit are directional objects, placed in rooms. They
	# are directional they occupy compass dirctions and are added to the room's
	# link count. In addition they are assigned to the Game State variables. The
	# Grue is only placed in a room again if it has not been bagged.
	place_directional_object(@$room_numbers[2], $PIT1);
	place_directional_object(@$room_numbers[3], $PIT2);
	place_directional_object(@$room_numbers[4], $EXIT);
	if (!$grue_bagged) {
		place_directional_object(@$room_numbers[5], $GRUE);
	}
	# If the game state is being set for the first time, then the player's
	# location is set to the same location as the Exit.
	if ($set) {
		$player_location = @$room_numbers[4];
	}
}

sub opposite {
	my $direction = shift;
	
	if ($direction == $NORTH) {
		return $SOUTH;
	}
	elsif ($direction == $SOUTH) {
		return $NORTH;
	}
	elsif ($direction == $EAST) {
		return $WEST;
	}
	elsif ($direction == $WEST) {
		return $EAST;
	}
	else {
		return 0;
	}
}

sub action_handler {
	my ($action, $sub_action) = @_;
	my $action_success = 0;
	
	if (defined $action) {
		$action = lc($action);
	
		# If the sub action (direction) is defined.
		if (defined $sub_action) {
			$sub_action = lc($sub_action);
			
			# If sub action (direction) is a valid direction.
			if (is_direction($sub_action)) {
				if ($sub_action eq 'q' || $action eq 'quit') {
					quit();
				}
				elsif ($action eq 'm' || $action eq 'move') {
					move_player($sub_action);
					$action_success = 1;
				}
				elsif ($action eq 's' || $action eq 'shoot') {
					shoot($sub_action);
					$action_success = 1; 
				}
			}
			
			# If the sub action (direction) is not a valid direction.
			else {
				print "Enter 'N', 'S', 'E' OR 'W'.\n";
				if ($action eq 'm' || $action eq 'move') {
					move_player();
				}
				elsif ($action eq 's' || $action eq 'shoot') {
					shoot();
				}
			}
			
			if ($action_success) {
				my $random_num = 0;
				$random_num = int(rand($EARTHQUAKE_CHANCE));
				
				if ($random_num == $EARTHQUAKE) {
					print "\n<<<EARTHQUAKE>>>\n\n";
					randomize_rooms();
					set_state($RESET);
					connect_rooms();
				}
				
				describe_location();
			}
		}
		# If the sub action (direction) is not defined.
		else {
			if ($action eq 'm' || $action eq 'move') {
				move_player();
			}
			elsif ($action eq 's' || $action eq 'shoot') {
				shoot();
			}
			elsif ($action eq 'q' || $action eq 'quit') {
				quit();
			}
		}
	}
}

sub display_text {
	$| = 1;
	my $text = shift;
	for my $word (split/%/, $text) {
		print $word."\n";
		print 'Press enter when ready to continue: ';
		<STDIN>;
	}
	print "\033[2J";
	print "\033[0;0H";
	print "\t\t\t\t".'***********************'."\n";
	print "\t\t\t\t".'***                 ***'."\n";
        print "\t\t\t\t".'***   GRUE HUNTER   ***'."\n";
	print "\t\t\t\t".'***                 ***'."\n";
	print "\t\t\t\t".'***********************'."\n\n";
}

sub read_input {
	my $action;
	my @actions;
	$action = <>;
	@actions = split(/\s+/, $action);
	return @actions;
}

sub is_direction {
	my $direction = shift;
	if (defined $direction) {
		return (
			$direction eq 'n' or
			$direction eq 'north' or
			$direction eq 's' or
			$direction eq 'south' or
			$direction eq 'e' or
			$direction eq 'east' or
			$direction eq 'w' or
			$direction eq 'west' or
			$direction eq 'q' or
			$direction eq 'quit'
		);
	}
	return '';
}

sub direction {
	my $direction = shift;
	
	if ($direction == $NORTH) {
		return 'north';
	}
	elsif ($direction == $SOUTH) {
		return 'south';
	}
	elsif ($direction == $EAST) {
		return 'east';
	}
	elsif ($direction == $WEST) {
		return 'west';
	}
	else {
		return '';
	}
}

sub move_player {
	my $sub_action = shift;
	my $direction;
	my $room_direction;
	
	if (defined $sub_action) {
		if ($sub_action eq 'n' || $sub_action eq 'north') {
			$direction = $NORTH;
		}
		elsif ($sub_action eq 's' || $sub_action eq 'south') {
			$direction = $SOUTH;
		}
		elsif ($sub_action eq 'e' || $sub_action eq 'east') {
			$direction = $EAST;
		}
		elsif ($sub_action eq 'w' || $sub_action eq 'west') {
			$direction = $WEST;
		}
		
		$room_direction = $rooms[$player_location] -> {$direction};
		
		# If the room that the player wants to move to is a room.
		if ($room_direction > $CLOSED) {
		
			# Check if the room has a bat.
			if ($room_direction == $bat1_location || $room_direction == $bat2_location) {
				print "Bats have you now.\n";
				print "They're lifting you up.\n";
				print "Ohhhh, where are we now?\n";
				
				my $number_of_rooms = 2;
				my $room_numbers = get_random_rooms($number_of_rooms);
				
				# Change player location (to where the bat dropped the player).
				$player_location = @$room_numbers[0];
				
				# Change location of offending bat.
				if ($room_direction == $bat1_location) {
					$bat1_location = @$room_numbers[1];
				}
				else {
					$bat2_location = @$room_numbers[1];
				}
			}
			
			# If free of bats, go ahead and move into the room.
			else {
				print "OK...\n";
				$player_location = $room_direction;
			}
		}
		elsif ($room_direction == $CLOSED) {
			print "You cannot go that way.\n";
		}
		elsif ($room_direction == $GRUE) {
			print "You bumped into the Grue.\n";
			print "He ate you before you could move.\n";
			quit();
		}
		elsif ($room_direction == $EXIT) {
			if ($grue_bagged) {
				print "You have reached the exit with your Grue.\n";
				print "Your supper will be filling tonight for sure!\n";
			}
			else {
				print "You have reached the exit without any Grue.\n";
				print "You are sure to starve.\n";
			}
			quit();
		}
		elsif ($room_direction < $PIT1) {
			my $pit_chance = int(rand($PIT_CHANCE));
			if ($pit_chance) {
				print "This room contains a bottomless pit. Part of the ground around it gave way when you entered and you almost fell in.\n";
				print "Be more careful next time!\n";
				$player_location = $room_direction;
			}
			else {
				print "You fell into a bottomless pit.\n";
				print "You fell a loooooong way...\n";
				quit();
			}
		}
	}
	else {
		my @actions;
		print "Move which way?\n";
		@actions = read_input();
		action_handler('m', $actions[0]);
	}
}

sub shoot {
	my $sub_action = shift;
	my $direction;
	my $room_direction;
	
	if (defined $sub_action) {
		if ($sub_action eq 'n' || $sub_action eq 'north') {
			$direction = $NORTH;
		}
		elsif ($sub_action eq 's' || $sub_action eq 'south') {
			$direction = $SOUTH;
		}
		elsif ($sub_action eq 'e' || $sub_action eq 'east') {
			$direction = $EAST;
		}
		elsif ($sub_action eq 'w' || $sub_action eq 'west') {
			$direction = $WEST;
		}
		
		$room_direction = $rooms[$player_location] -> {$direction};
		
		# If the arrow was shot in a direction without the Grue.
		if ($room_direction == $CLOSED) {
			print "Clunk!\n";
			print "The arrow bounced off the wall.\n";
		}
		# If the arrow was shot in a direction of the Grue.
		elsif ($room_direction == $GRUE) {
			my $shot = int(rand($EVEN_CHANCE));
			if ($shot) {
				print "You caught the Grue!\n";
				print "Now to find a way out...\n";
				$grue_bagged++;
				
				# Remove the Grue from the Room once bagged. Set the Grue
				# location to an undefined state (the Grue object -2).
				$rooms[$player_location] -> {$direction} = $CLOSED;
				($rooms[$player_location] -> {'link_count'})--;
				$grue_location = $GRUE;
			}
			else {
				print "The arrow missed the Grue.\n";
			}
		}
		else {
			print "The arrow missed the Grue.\n";
		}
	}
	else {
		my @actions;
		print "Shoot which way?\n";
		@actions = read_input();
		action_handler('s', $actions[0]);
	}
}

sub describe_location {
	my $room_direction;
	
	print $rooms[$player_location] -> {'description'} . "\n";
# print "Player Location: $player_location\n";
# print "Grue Location: $grue_location\n";
# print "Bat 1 Location: $bat1_location\n";
# print "Bat 2 Location: $bat2_location\n";
# print "Pit 1 Location: $pit1_location\n";
# print "Pit 2 Location: $pit2_location\n";
# print "Exit Location: $exit_location\n";
	
	print 'I see passages to the ';
	my @passages;
	for (my $i = 1; $i < 5; $i++) {
		$room_direction = $rooms[$player_location] -> {$i};
		if ($room_direction != $CLOSED) {
			push(@passages, direction($i));
		}
	}
	
	for (my $i = 0; $i < $#passages + 1; $i++) {
		if ($i == 0) {
			print $passages[$i];
		}
		elsif ($i < $#passages) {
			print ', ' . $passages[$i];
		}
		elsif ($i == $#passages) {
			print ' and ' . $passages[$i];
		}
	}
	print ". Choose wisely.\n";
	
	# If the Grue is in the player's current location.
	if ($player_location == $grue_location) {
		print "I smell the Grue!\n";
	}
	# If either pit is in the player's current location.
	elsif ($player_location == $pit1_location || $player_location == $pit2_location) {
		print "I feel a draft. A bottomless pit must be nearby. Be careful not to fall in.\n";
	}
	# If the Exit is in the player's current location, tell the player which
	# direction the Exit is located.
	elsif ($player_location == $exit_location) {
		for (my $i = 1; $i < 5; $i++) {
			$room_direction = $rooms[$player_location] -> {$i};
			if ($room_direction == $EXIT) {
				print 'The exit is to the ' . direction($i) . ".\n";
			}
		}
	}
	# Check the adjacent rooms for the Exit, Grue, Bats and Pits. Here
	# check whether the adjacent rooms are equivalent to the game state
	# variables for the Exit, Grue, Bats and Pits.
	else {
		for (my $i = 1; $i < 5; $i++) {
			$room_direction = $rooms[$player_location] -> {$i};
			if ($room_direction > $CLOSED) {
				if ($room_direction == $exit_location) {
					print "Exit nearby...\n";
				}
				elsif ($room_direction == $grue_location) {
					print "I smell the Grue!\n";
				}
				# If the room adjacent contains one of the two bats then warn the player.
				elsif ($room_direction == $bat1_location || $room_direction == $bat2_location) {
					print "I hear bats flying around in the distance. Be careful or they may grab you and carry you away.\n";
				}
				# If the room adjacent contains one of the two pits then warn the player.
				elsif ($room_direction == $pit1_location || $room_direction == $pit2_location) {
					print "I feel a draft. A bottomless pit must be nearby. Be careful not to fall in.\n";
				}
			}
		}
	}
}

sub quit {
	exit;
}

sub main {
	my @actions;
	
	display_text($instructions);
	randomize_rooms();
	set_state($SET);
	connect_rooms();
	describe_location();
	while (1) {
		print "Move or shoot?\n";
		@actions = read_input();
		action_handler(@actions);
	}
}

main();
