#
#	$Id: Blackjack.pm,v 1.18 2004/12/30 02:48:21 kevin Exp $
#
#	Author: Kevin Walsh <kevin@cursor.biz>
#
#	Copyright (c) 2003-2004 Cursor Software Limited.
#	All rights reserved.
#
#	----------------------------------------------------------------------
#
#	Blackjack card game plug-in for the SLIMP3 server.
#
#	Press ADD to add a card to your hand or RIGHT to compare your hand
#	with the dealer's.
#
#	The idea of the game, as I understand it, is to get a hand that
#	is higher in value than the dealer's, without going over 21.  If
#	you go over 21 then you "bust" and the dealer wins.  If the dealer
#	busts then you win.
#
#	Aces are worth 1 or 11.  Numbered cards are worth their face value.
#	Jacks, Queens and Kings are all worth 10.  The card's suit is
#	meaningless.
#
#	If you get 21 with your first two cards then you have a Blackjack.
#	A Blackjack beats any hand the dealer may have, except for the
#	dealer's own blackjack.  If you both have a blackjack then there is
#	a tie and nobody wins.
#
#	The dealer will keep taking cards until the hand value is 17 or
#	more ("dealer_stand" preference).  If the dealer's hand value is
#	exactly 17, and one of the cards is an ace then the dealer will
#	continue to take cards until the value is greater than 17.
#
#	Please let me know if my understanding of the rules is incorrect.
#	Also, it'd be nice to be informed if my implementation of the
#	rules turns out to be incorrect. :-)
#
#	----------------------------------------------------------------------
#
#	This program is free software; you can redistribute it and/or modify
#	it under the terms of the GNU General Public License as published by
#	the Free Software Foundation; either version 2 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 General Public License for more details.
#
#	You should have received a copy of the GNU General Public License
#	along with this program; if not, write to the Free Software
#	Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
#	02111-1307 USA
#
package Plugins::Blackjack;
use Slim::Utils::Strings qw(string);
use strict;

my %prefs = (
    'decks' => {
	order => 0,
	default => 1,
	widget => {
	    options => {
		'1' => '1',
		'2' => '2',
		'3' => '3',
		'4' => '4',
		'5' => '5',
	    },
	},
    },
    'dealer_show' => {
	order => 1,
	default => 1,
	widget => {
	    options => {
		'1' => '1',
		'2' => '2',
	    },
	},
    },
    'dealer_stand' => {
	order => 2,
	default => 17,
	widget => {
	    validate => \&Slim::Web::Setup::validateInt,
	    validateArgs => [1,21,1,21],
	},
    },
);

use vars qw($VERSION);
$VERSION = substr(q$Revision: 1.18 $,10);

my %suit_char = (
    'H' => Slim::Hardware::VFD::symbol('heart'),
    'D' => Slim::Hardware::VFD::symbol('diamond'),
    'S' => Slim::Hardware::VFD::symbol('spade'),
    'C' => Slim::Hardware::VFD::symbol('club'),
);

my @cards = ( 'A', 2 .. 10, 'J', 'Q', 'K' );
my @suits = qw( H D S C );
my $limit = 21;
my %context;

my %functions = (
    'left' => sub {
	Slim::Buttons::Common::popModeRight(shift);
    },
    'right' => sub {
	my $client = shift;
	my $ref = $context{$client} || {};

	my @oldlines = Slim::Display::Display::curLines($client);

	if ($ref->{playing} && $ref->{player} <= $limit) {
	    dealer_turn($client);
	}
	else {
	    new_game($client);
	}

	#
	#	push the old lines off the left side of the display
	#
	Slim::Display::Animation::pushLeft(
	    $client,
	    @oldlines,
	    Slim::Display::Display::curLines($client),
	);
    },
    'add' => sub {
	my $client = shift;
	my $ref = $context{$client} || {};

	return undef unless ($ref->{playing} && $ref->{player} <= $limit);

	my $card = shift(@{$ref->{shoe}});
	push (@{$ref->{hand}},$card);
	$ref->{player} = hand_total(@{$ref->{hand}});

	my $msg = string('PLUGIN_BLACKJACK_NEWCARD');
	$card =~ s/{([HDSC])}/$suit_char{$1}/g;
	$msg =~ s/\${card}/$card/;

	Slim::Display::Animation::showBriefly($client,$msg);
    },
);

#
#	lines()
#	-------
#	Create and return the two-line display.
#
sub lines
{
    my $client = shift;
    my @lines;
    my $ref = $context{$client} || {};
    my $divider = Slim::Hardware::VFD::symbol('rightarrow');
    my $blackjack;

    #
    #	work out the value of the player's hand
    #
    my $value = $ref->{player};
    if ($value > $limit) {
	$value = string('PLUGIN_BLACKJACK_BUST');
    }
    elsif ($value == $limit && scalar(@{$ref->{hand}}) == 2) {
	$value = string('PLUGIN_BLACKJACK_BLACKJACK');
	$blackjack = 1;
    }

    #
    #	show the player's hand and its value
    #
    $lines[0] = (
	string('PLUGIN_BLACKJACK_PLAYER') .
	" $value: " .
	join(' ',@{$ref->{hand}})
    );

    if ($ref->{playing}) {
	#
	#	the user is still playing so set the second line's text
	#	depending upon whether the user's hand value is over
	#	the limit or not
	#
	if ($ref->{player} > $limit) {
	    $lines[1] = (
		string('PLUGIN_BLACKJACK_DEALER_WINS') .
		" $divider " .
		string('PLUGIN_BLACKJACK_HELP_NEWGAME')
	    );
	}
	else {
	    $lines[1] = dealer_hand($ref);

	    if ($blackjack) {
		$lines[1] .= string('PLUGIN_BLACKJACK_HELP_DEALER');
	    }
	    else {
		$lines[1] .= string('PLUGIN_BLACKJACK_HELP_PLAY');
	    }
	}
    }
    else {
	$lines[1] = dealer_hand($ref);

	if ($ref->{dealer} == $limit && scalar(@{$ref->{dealer_hand}}) == 2) {
	    #
	    #	the dealer has a blackjack hand so check whether he wins
	    #	or there is a tie
	    #
	    if ($ref->{blackjack}) {
		$lines[1] .= string('PLUGIN_BLACKJACK_TIE');
	    }
	    else {
		$lines[1] .= string('PLUGIN_BLACKJACK_DEALER_WINS');
	    }
	}
	elsif ($ref->{dealer} > $limit || $ref->{player} > $ref->{dealer} || $ref->{blackjack}) {
	    #
	    #	one of the following happened:  (1) the dealer bust
	    #	(2) the player's hand beat the dealer, or (3) the player
	    #	has a blackjack
	    #
	    $lines[1] .= string('PLUGIN_BLACKJACK_PLAYER_WINS');
	}
	else {
	    #
	    #	the player didn't beat the dealer, so the dealer wins
	    #	by default
	    #
	    $lines[1] .= string('PLUGIN_BLACKJACK_DEALER_WINS');
	}

	#
	#	the game is over so tell the user how to start a new one
	#
	$lines[1] .= (
	    " $divider " .
	    string('PLUGIN_BLACKJACK_HELP_NEWGAME')
	);
    }

    if (Slim::Utils::Prefs::clientGet($client,'doublesize')) {
	#
	#	use the top line in double-size mode if the player
	#	is still playing
	#
	$lines[1] = $lines[0] if $ref->{playing};
	$lines[1] =~ s/{([HDSC])}/$1/g;
    }
    else {
	#
	#	replace each suit with its corresponding custom character
	#
	foreach (@lines) {
	    s/{([HDSC])}/$suit_char{$1}/g;
	}
    }
    $lines[2] = $lines[3] = undef;
    @lines;
}

#
#	dealer_hand()
#	-------------
#	Work out the value of the dealer's hand and return a
#	displayable string.
#
sub dealer_hand
{
    my $ref = shift;
    my $value = $ref->{dealer};

    return '' unless $value;

    if ($value > $limit) {
	$value = string('PLUGIN_BLACKJACK_BUST');
    }
    elsif ($value == $limit && scalar(@{$ref->{dealer_hand}}) == 2) {
	$value = string('PLUGIN_BLACKJACK_BLACKJACK');
    }

    return(
	string('PLUGIN_BLACKJACK_DEALER') .
	" $value: " .
	join(' ',@{$ref->{dealer_hand}}) .
	' ' .
	Slim::Hardware::VFD::symbol('rightarrow') .
	' '
    );
}

#
#	new_game()
#	----------
#	Start a new game for the current client
#
sub new_game
{
    my $client = shift;
    my @shoe;

    load_preferences();

    #
    #	create a "shoe" of cards by merging the cards from a
    #	number of decks and shuffling them
    #
    for (1 .. $prefs{decks}->{current}) {
	foreach my $suit (@suits) {
	    push(@shoe,$_ . "{$suit}") for @cards;
	}
    }
    @shoe = sort { rand() <=> rand() } @shoe;

    #
    #	deal two cards from the shoe into the playeer's hand
    #
    my @hand = (shift(@shoe),shift(@shoe));

    #
    #	deal zero or more cards ("dealer_show" preference) into the
    #	dealer's hand
    #
    my @dealer_hand;
    push(@dealer_hand,shift(@shoe)) for (1 .. $prefs{dealer_show}->{current});

    #
    #	set up the player's context for this new game
    #
    $context{$client} = {
	shoe => \@shoe,
	hand => \@hand,
	dealer_hand => \@dealer_hand,
	player => scalar(hand_total(@hand)),
	dealer => scalar(hand_total(@dealer_hand)),
	playing => 1,
	blackjack => 0,
    };
}

#
#	hand_total()
#	------------
#	Work out the most advantageous value of the given hand.
#
#	Array context:  Returns the hand value and the number of aces
#	                in the hand.
#	Scalar context: Only returns the hand value.
#
sub hand_total
{
    my @hand = @_;
    my $total = 0;
    my $aces = 0;

    foreach (@hand) {
	s/^(\D|\d+).+/$1/;
	if ($_ eq 'A') {
	    $aces++;
	}
	elsif ($_ =~ /[JQK]/) {
	    $total += 10;
	}
	else {
	    $total += $_;
	}
    }
    $total += $aces;
    for (1 .. $aces) {
	last if $total + 10 > $limit;
	$total += 10;
    }
    return wantarray ? ($total,$aces) : $total;
}

#
#	dealer_turn()
#	-------------
#	The dealer's keeps taking cards until a hard-17 is reached
#	or breached.  The dealer will take another card if a soft-17
#	total is reached.
#
sub dealer_turn
{
    my $client = shift;
    my $ref = $context{$client} || {};
    my $soft;

    if ($ref->{player} == $limit && scalar(@{$ref->{hand}}) == 2) {
	$ref->{blackjack} = 1;
    }
    $ref->{playing} = 0;

    ($ref->{dealer},$soft) = hand_total(@{$ref->{dealer_hand}});
    return if $ref->{dealer} > $prefs{dealer_stand}->{current};
    return if ($ref->{dealer} == $prefs{dealer_stand}->{current} && !$soft);

    while (1) {
	push(@{$ref->{dealer_hand}},shift(@{$ref->{shoe}}));
	($ref->{dealer},$soft) = hand_total(@{$ref->{dealer_hand}});
	last if $ref->{dealer} > $prefs{dealer_stand}->{current};
	last if ($ref->{dealer} == $prefs{dealer_stand}->{current} && !$soft);
    }
}

#
#	load_custom_chars()
#	-------------------
#	Creates custom card suit symbols.
#
sub load_custom_chars
{
    Slim::Hardware::VFD::setCustomChar('heart',(
	0b00000,
	0b01010,
	0b11111,
	0b11111,
	0b01110,
	0b00100,
	0b00000,
	0,
    ));
    Slim::Hardware::VFD::setCustomChar('diamond',(
	0b00000,
	0b00100,
	0b01110,
	0b11111,
	0b01110,
	0b00100,
	0b00000,
	0,
    ));
    Slim::Hardware::VFD::setCustomChar('club',(
	0b00000,
	0b01110,
	0b01110,
	0b11111,
	0b11111,
	0b00100,
	0b00100,
	0,
    ));
    Slim::Hardware::VFD::setCustomChar('spade',(
	0b00000,
	0b00100,
	0b01110,
	0b11111,
	0b00100,
	0b01110,
	0b00000,
	0,
    ));
    undef;
}

#
#	load_preferences()
#	------------------
#	Load the current preferences or set defaults
#
sub load_preferences
{
    while (my ($key,$val) = each %prefs) {
	my $name = __PACKAGE__;
	$name =~ s/^.*:://;
	$key = 'plugin_' . lc($name) . "_$key";

	if (Slim::Utils::Prefs::isDefined($key)) {
	    $val->{current} = Slim::Utils::Prefs::get($key);
	}
	else {
	    $val->{current} = $val->{default};
	    Slim::Utils::Prefs::set($key,$val->{default});
	}
    }
}

sub setupGroup
{
    my $name = __PACKAGE__;
    $name =~ s/^.*:://;
    $name = 'plugin_' . lc($name);

    load_preferences();

    my @order;
    push(@order,"${name}_$_") for (sort {$prefs{$a}->{order} <=> $prefs{$b}->{order}} keys %prefs);

    my $version = ' (' . string("${name}_VERSION") . ')';
    $version =~ s/%s/$VERSION/;
    $version =~ s/\s+\)/)/;

    my %group = (
	PrefOrder => \@order,
	GroupHead => string("${name}_MODULE_NAME") . $version,
	GroupDesc => string("${name}_MODULE_DESCRIPTION"),
	GroupLine => 1,
	GroupSub => 1,
	Suppress_PrefSub => 1,
	Suppress_PrefLine => 1,
    );

    my %widgets;
    while (my ($key,$val) = each %prefs) {
	$widgets{"${name}_$key"} = $val->{widget};

	if ($val->{translate} && exists($val->{widget}->{options})) {
	    $_ = string("${name}_$_") for (values %{$val->{widget}->{options}});
	    delete $val->{translate};
	}
    }

    return (\%group,\%widgets);
}

sub setMode
{
    my $client = shift;

    load_custom_chars();
    new_game($client);

    $client->lines(\&lines);
    $client->update();
}

sub getFunctions
{
    \%functions;
}

sub getDisplayName
{
    my $name = 'PLUGIN_BLACKJACK_MODULE_NAME';

    $::VERSION =~ /^(\d+)/;
    return ($1 >= 6) ? $name : string($name);
}

sub addMenu
{
    'GAMES';
}

sub strings
{
    local $/ = undef;
    <DATA>;
}

1;

__DATA__

PLUGIN_BLACKJACK_MODULE_NAME
	EN	Blackjack Card Game

PLUGIN_BLACKJACK_MODULE_DESCRIPTION
	EN	Blackjack card game

PLUGIN_BLACKJACK_VERSION
	EN	Version %s

PLUGIN_BLACKJACK_PLAYER
	EN	Player

PLUGIN_BLACKJACK_DEALER
	EN	Dealer

PLUGIN_BLACKJACK_BUST
	EN	Bust

PLUGIN_BLACKJACK_DEALER_WINS
	EN	Dealer Wins

PLUGIN_BLACKJACK_PLAYER_WINS
	EN	Player Wins

PLUGIN_BLACKJACK_BLACKJACK
	EN	BLACKJACK

PLUGIN_BLACKJACK_TIE
	EN	Tie

PLUGIN_BLACKJACK_NEWCARD
	EN	Added ${card} to your hand

PLUGIN_BLACKJACK_HELP_NEWGAME
	EN	Press RIGHT to start a new game or LEFT to exit

PLUGIN_BLACKJACK_HELP_PLAY
	EN	Press ADD for another card, RIGHT to stand or LEFT to exit

PLUGIN_BLACKJACK_HELP_DEALER
	EN	Press RIGHT to allow the dealer to take its turn

SETUP_PLUGIN_BLACKJACK_DECKS
	EN	Number of decks of cards used to fill a shoe

SETUP_PLUGIN_BLACKJACK_DEALER_SHOW
	EN	The number of cards the dealer should take (and show you) before you get to play

SETUP_PLUGIN_BLACKJACK_DEALER_STAND
	EN	according to the rules, the dealer will keep taking cards until the hand total is >= hard 17.  You can change that value if you like


