#!/usr/bin/perl
#
# Digits 1.0.2
#
# Copyright (C) 2013 Jason Self <j@jxself.org>
#
# This software's license gives you freedom; you can modify, propagate,
# and/or convey it under the terms of the GNU Affero General Public
# License as published by the Free Software Foundation (FSF); either 
# version 3 of the License, or (at your option) any later version 
# published by the FSF.
#
# 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;
use Getopt::Long;

# Constants
my $QUIT = -1;
my $TRUE = 1;
my $FALSE = 0;
my $MAX_DIGITS = 8;

# Game Setup
my $DIGITS = 3;
my $DISTINCT = $TRUE;
my $MAX_GUESSES = 0;

# Game State
my $guessed;
my $tries;
my @digits;

# Checks that the guess is a number and has the same number of digits as
# specified by $DIGITS.
sub is_correct_format {
	my $guess = shift;
	
	return ($guess =~ m/^\d{$DIGITS}$/);
}

# Checks that the guess is made up of distinct digits if $DISTINCT is 1.
sub is_distinct {
	my $array_ref = shift;
	my @guess_digits = @{$array_ref};
	
	for (my $i = 0; $i < $DIGITS; $i++) {
		for (my $j = $i+1; $j < $DIGITS; $j++) {
			if ($guess_digits[$i] eq $guess_digits[$j]) {
				return $FALSE || !$DISTINCT;
			}
		}
	}
	
	return $TRUE;
}

# This subroutine
# 1) checks that the digits entered are valid
# 2) checks the guess against the number to be guessed and prints hints.
sub check_digits {
	my $guess = shift;
	my $guessed = $TRUE;
	
	if (!defined($guess)) {
		return $FALSE;
	}
	elsif ($guess eq 'q' || $guess eq 'quit') {
		return $QUIT;
	}
	elsif (!is_correct_format($guess)) {
		print "Type $DIGITS digits please.\n";
		return $FALSE;
	}
	
	my @guess_digits = ();
	my $digit;
	
	for (my $i = 0; $i < $DIGITS; $i++) {
		$digit = chop($guess);
		unshift(@guess_digits, $digit);
	}
	
	if (!is_distinct(\@guess_digits)) {
		print "Type $DIGITS different digits please.\n";
		return $FALSE;
	}
	
	my $hint = '';
	for (my $i = 0; $i < $DIGITS; $i++) {
		if ($guess_digits[$i] eq $digits[$i]) {
			$hint = $hint . 'X';
		}
		else {
			$guessed = $FALSE;
			my $no_match = $TRUE;
			
			for (my $j = 0; $j < $DIGITS; $j++) {
				if ($guess_digits[$i] eq $digits[$j]) {
					$hint = $hint . 'O';
					$no_match = $FALSE;
					last;
				}
			}
			
			if ($no_match) {
				$hint = $hint . '-';
			}
		}
	}
	
	if ($hint) {
		print "For your guess of @guess_digits, I hint $hint.\n";
		$tries++;
		return $guessed;
	}
}

# Reads a line of user input.
sub read_input {
	my $input;
	
	$input = <STDIN>;
	
	if ($input) {
		chomp $input;
	}
	# Required for a graceful exit when user hits ^C.
	else {
		exit;
	}
	
	return $input;
}

# Contains various representations of 'Yes' used to parse user input.
sub affirmative {
	my $input = shift;
	
	if ($input eq 'y') {
		return $TRUE;
	}
	elsif ($input eq 'Y') {
		return $TRUE;
	}
	elsif ($input eq 'yes') {
		return $TRUE;
	}
	elsif ($input eq 'Yes') {
		return $TRUE;
	}
	elsif ($input eq 'YES') {
		return $TRUE;
	}
	else {
		return $FALSE;
	}
}

# Contains various representations of 'No' used to parse user input.
sub negative {
	my $input = shift;
	
	if ($input eq 'n') {
		return $TRUE;
	}
	elsif ($input eq 'N') {
		return $TRUE;
	}
	elsif ($input eq 'no') {
		return $TRUE;
	}
	elsif ($input eq 'No') {
		return $TRUE;
	}
	elsif ($input eq 'NO') {
		return $TRUE;
	}
	else {
		return $FALSE;
	}
}

# Asks player if they would like to play again.
sub play_again {
	my $input;
	my $recursion = shift;
	
	if ($recursion) {
		print "\nDo you want to play again? Please type a Y for yes or a N for No?\n";
	}
	else {
		print "\nDo you want to play again?\n";
	}
	
	$input = read_input();
	
	if (affirmative($input)) {
		set_game_state();
	}
	elsif (negative($input)) {
		exit;
	}
	else {
		play_again(1);
	}
}

# Clears screen and prints game title.
sub display_text {
	$| = 1;
	print "\033[2J";
	print "\033[0;0H";
    print "\t\t\t" . '*** DIGITS 1.0.1 ***' . "\n";
print "Homepage: http://jxself.org/digits.shtml\n\n";
print "Copyright (C) 2013 Jason Self <j\@jxself.org>\n\n";
print "Licensed under the GNU AGPL v3 or any later version.\n\n";

print "I will think of a number. The object of the game is to guess the number\n";
print "in as few tries as possible. Each time you guess I will provide a hint:\n\n";

print "For each digit in the correct position I will print an X\n";
print "For a correct digit with an incorrect position I will print an O\n";
print "For a totally incorrect digit I will print an -\n\n";

print "Let's begin. Here are some questions.\nYou can press return to accept the default setting.\n";

}

# Sets the following game state variables by reading command line arguments or 
# by prompting the player where required:
# 1) Length of combination, stored in $DIGITS
# 2) Whether of not the digits in the combination are distinct, stored in $DISTINCT
# 3) Number of guesses, stored in $MAX_GUESSES
# If the player accepts the defaults then the initial assignment of the above
# game state variable are used.
sub get_options {
	my $digits;
	my $distinct;
	my $guesses;

	GetOptions (
		'digits=i' => \$digits,
		'distinct=s' => \$distinct,
		'guesses=i' => \$guesses
	);
	
	if (!defined($digits)) {
		get_digits();
	}
	elsif (is_number($digits)) {
		$DIGITS = $digits;
	}
	else {
		get_digits();
	}
	
	if (!defined($distinct)) {
		get_distinct();
	}
	elsif (affirmative($distinct)) {
		$DISTINCT = $TRUE;
	}
	elsif (negative($distinct)) {
		$DISTINCT = $FALSE;
	}
	else {
		get_distinct();
	}
	
	if (!defined($guesses)) {
		get_max_guesses();
	}
	elsif (is_number($guesses)) {
		$MAX_GUESSES = $guesses;
	}
	else {
		get_max_guesses();
	}
	
	set_game_state();
}

# Checks if the input is a number.
sub is_number {
	my $input = shift;
	
	return ($input =~ m/^\d+$/);
}

# Prompts the player to get number of digits or accept default of 3.
sub get_digits {
	print "\nHow long should the combination be? [Default 3, Maximum 8]\n";
	my $input = read_input();
	
	if ($input) {
		if (is_number($input)) {
			if ($input > $MAX_DIGITS) {
				print "Please enter a number less than 8.\n";
				get_digits();
				return;
			}
			else {
				$DIGITS = $input;
			}
		}
		else {
			print "Please enter a number.\n";
			get_digits();
			return;
		}
	}
}

# Prompts the player about using distinct digits or not. Default is to use 
# distinct digits.
sub get_distinct {
	print "\nShould each digit in the combination be different from the others? [Default Yes]\n";
	my $input = read_input();
	
	if ($input) {
		if (affirmative($input)) {
			$DISTINCT = $TRUE;
		}
		elsif (negative($input)) {
			$DISTINCT = $FALSE;
		}
		else {
			print "Please enter a Y for yes or a N for No.\n";
			get_distinct();
			return;
		}
	}
}

# Prompts the player to get number of guesses or accept default of unlimited.
sub get_max_guesses {
	print "\nMaximum number of guesses? [Default unlimited]\n";
	my $input = read_input();
	
	if ($input) {
		if (is_number($input)) {
			$MAX_GUESSES = $input;
		}
		else {
			print "Please enter a number.\n";
			get_max_guesses();
			return;
		}
	}
}

# Sets game state for a new game.  Setting new game state includes
# 1) Setting whether or not the number has been guessed to $FALSE
# 2) Setting number of guesses/tries to 0
# 3) Setting the number to be guessed
sub set_game_state {
	my $digit;
	$guessed = $FALSE;
	$tries = 0;
	@digits = ();
	
	my $i = 0;
	while ($i < $DIGITS) {
		$digit = int(rand(9));
		
		if ($DISTINCT) {
			my $no_match = $TRUE;
			
			for (my $j = 0; $j < $#digits + 1; $j++) {
				if ($digits[$j] == $digit) {
					$no_match = $FALSE;
					last;
				}
			}
				
			if ($no_match) {
				push(@digits, $digit);
				$i++;
			}
		}
		else {
			push(@digits, $digit);
			$i++;
		}
		
	}
	print "\nOkay, I've got a number...\n";
}

# The main/starting program for the game.
sub main {
	my $guess;
	
	display_text();
	get_options();
	
	while ($TRUE) {
		print "\nWhat is your guess? Type 'q' or 'quit' to exit.\n";
		$guess = read_input();
		$guessed = check_digits($guess);
		
		if ($MAX_GUESSES) {
			if ($tries == $MAX_GUESSES) {
				if ($tries == 1) {
					print "\nYou have not guessed my number in 1 guess.\n";
				}
				else {
					print "\nYou have not guessed my number in $MAX_GUESSES guesses.\n";
				}
				
				play_again();
			}
		}
		
		if ($guessed == $QUIT) {
			print "\nThe number I had was @digits. Bye!\n";
			last;
		}
		elsif ($guessed) {
			if ($tries == 1) {
				print "Excellent! You guessed it in only 1 try!\n";
			}
			elsif ($tries < ($DIGITS * 2)) {
				print "Excellent! You guessed it in only $tries tries!\n";
			}
			else {
				print "You guessed it in $tries tries!\n";
			}
			
			play_again();
		}
	}
}

main();
