ftp.nice.ch/pub/next/games/card/NEXTVegas3.0.src.tar.gz#/NEXTVegas/Blackjack/Blackjack.m

This is Blackjack.m in view mode; [Download] [Up]

#import "Blackjack.h"
#import "hints.h"

#define BJ_DRAWPAUSE 200000
#define BJ_LONGPAUSE 999999
#define BJ_MINSHUFFLEVALUE 20
#define BJ_NAMEFORDEFAULTS "NVBlackjack"

#define BJ_DONTCLEARTABLE 11

void BJ_RunGame(DPSTimedEntry teNumber, double now, void *userData)
{
	[Dealer() perform:sel_getUid((char *)userData)];
}


@implementation Blackjack

+ initialize
{
    static NXDefaultsVector ourDefaults = {
        {"numDecksTag", "8"},
		{"dealerPrefTag", "0"},
		{"doublePrefTag", "0"},
		{"dealerPeakTag", "0"},
		{"showHandTotals", "0"},
		{"reshuffleValue", "BJ_MINSHUFFLEVALUE"},
		{"betterHandAfterAces", "1"},
		{"clearTable", "BJ_DONTCLEARTABLE"},
		{"suggestMoves", "0"},
		{NULL, NULL}
    };
    
    NXRegisterDefaults(BJ_NAMEFORDEFAULTS, ourDefaults);

	return self;
}



- awakeFromNib
{
	int i;
	char path[MAXPATHLEN+1];
	 	
	// Set all the pointers for the first player area...
	players[0].hand[BJ_REGULAR_HAND].view = p1HandView;
	players[0].hand[BJ_REGULAR_HAND].handText = p1HandText;
	players[0].hand[BJ_REGULAR_HAND].totalText = p1TotalText;
	players[0].hand[BJ_SPLIT_HAND].view = p1SplitHandView;
	players[0].hand[BJ_SPLIT_HAND].handText = p1SplitHandText;
	players[0].hand[BJ_SPLIT_HAND].totalText = p1SplitTotalText;
	players[0].betView = p1BetView;
	players[0].insuranceView = p1InsuranceView;
	players[0].dBetText = [p1DefaultBetText setIntValue:0];
	players[0].PButton = area1PButton;
	players[0].RWButton = area1RemoveWin;
	players[0].dBetButton = area1DButton;
	
	// Set all the pointers for the second player area...
	players[1].hand[BJ_REGULAR_HAND].view = p2HandView;
	players[1].hand[BJ_REGULAR_HAND].handText = p2HandText;
	players[1].hand[BJ_REGULAR_HAND].totalText = p2TotalText;
	players[1].hand[BJ_SPLIT_HAND].view = p2SplitHandView;
	players[1].hand[BJ_SPLIT_HAND].handText = p2SplitHandText;
	players[1].hand[BJ_SPLIT_HAND].totalText = p2SplitTotalText;
	players[1].betView = p2BetView;
	players[1].insuranceView = p2InsuranceView;
	players[1].dBetText = [p2DefaultBetText setIntValue:0];
	players[1].PButton = area2PButton;
	players[1].RWButton = area2RemoveWin;
	players[1].dBetButton = area2DButton;
	
	// Set all the pointers for the third player area...
	players[2].hand[BJ_REGULAR_HAND].view = p3HandView;
	players[2].hand[BJ_REGULAR_HAND].handText = p3HandText;
	players[2].hand[BJ_REGULAR_HAND].totalText = p3TotalText;
	players[2].hand[BJ_SPLIT_HAND].view = p3SplitHandView;
	players[2].hand[BJ_SPLIT_HAND].handText = p3SplitHandText;
	players[2].hand[BJ_SPLIT_HAND].totalText = p3SplitTotalText;
	players[2].betView = p3BetView;
	players[2].insuranceView = p3InsuranceView;
	players[2].dBetText = [p3DefaultBetText setIntValue:0];
	players[2].PButton = area3PButton;
	players[2].RWButton = area3RemoveWin;
	players[2].dBetButton = area3DButton;
	
	// Set all the pointers for the fourth player area...
	players[3].hand[BJ_REGULAR_HAND].view = p4HandView;
	players[3].hand[BJ_REGULAR_HAND].handText = p4HandText;
	players[3].hand[BJ_REGULAR_HAND].totalText = p4TotalText;
	players[3].hand[BJ_SPLIT_HAND].view = p4SplitHandView;
	players[3].hand[BJ_SPLIT_HAND].handText = p4SplitHandText;
	players[3].hand[BJ_SPLIT_HAND].totalText = p4SplitTotalText;
	players[3].betView = p4BetView;
	players[3].insuranceView = p4InsuranceView;
	players[3].dBetText = [p4DefaultBetText setIntValue:0];
	players[3].PButton = area4PButton;
	players[3].RWButton = area4RemoveWin;
	players[3].dBetButton = area4DButton;
	
	// Set all the pointers for the dealer...
	players[BJ_DEALER].hand[BJ_REGULAR_HAND].view = dealerHandView;
	players[BJ_DEALER].hand[BJ_REGULAR_HAND].handText = dealerHandText;
	players[BJ_DEALER].hand[BJ_REGULAR_HAND].totalText = dealerTotalText;
	[players[BJ_DEALER].hand[BJ_REGULAR_HAND].handText setStringValue:""];
	players[BJ_DEALER].hand[BJ_REGULAR_HAND].pile = 
		[players[BJ_DEALER].hand[BJ_REGULAR_HAND].view cardPile];	
	
	// This may take a while, so lets inform the user
	[self say:"Pre-Drawing Cards..."];
	NXPing();
	[Card drawCardImages];
	
	// Initialize the betviews, and cardpiles...
	for(i=0; i<BJ_MAXPLAYERS; i++)
	{
		[[[[[[players[i].betView init] 
						        setDelegate:self] 
							    setTag:i] 
							    setViewType:BJ_BETVIEW]
								setSoundEnabled:YES]
								setNumRows:1];
		[[[[[[[[players[i].insuranceView init]
		                               setDelegate:self] 
		                               setTag:i+BJ_INSURANCEVIEW] 
								       setViewType:BJ_INSURANCEVIEW]
									   setSoundEnabled:YES]
									   setNumRows:1]
									   setPayoff:2 cost:1]
									   setEnabled:NO];
		
		[[[[players[i].hand[BJ_REGULAR_HAND].view setTag:i] 
		                                       setDelegate:playerPileDelegate] 
											   setDrawOutline:NO] 
											   setCoversOthers:YES];
		[[[[players[i].hand[BJ_SPLIT_HAND].view setTag:-i] 
		                                     setDelegate:playerPileDelegate]
											 setDrawOutline:NO] 
											 setCoversOthers:YES];
		players[i].hand[BJ_REGULAR_HAND].pile=[players[i].hand[BJ_REGULAR_HAND].view cardPile];
		players[i].hand[BJ_SPLIT_HAND].pile=[players[i].hand[BJ_SPLIT_HAND].view cardPile];
		[players[i].hand[BJ_REGULAR_HAND].view setOffset:0.0 :30.0];
		[players[i].hand[BJ_SPLIT_HAND].view setOffset:0.0 :30.0];
		[players[i].hand[BJ_REGULAR_HAND].view resetBacking:self];
		[players[i].hand[BJ_SPLIT_HAND].view resetBacking:self];
		
		
		// select cells in the PButtons...
		[self selectCellWithTag:i forPopUpButton:players[i].PButton];
		
	}
		
	// Initialize Dealer's card pile
	[[[players[BJ_DEALER].hand[BJ_REGULAR_HAND].view	setOffset:60.0 :0.0]
														setDrawOutline:NO]
														setCoversOthers:YES];
	// The deck will use the shoe
	[drawView setUseShoe:YES];
	[drawView resetBacking:self];
		
	// create sounds
	if ([bundle getPath:path forResource:"shuffle" ofType:"snd"]) 
	{
		shuffleSound = [[SoundEffect allocFromZone:[self zone]] initFromSection:path];
	}
	if ([bundle getPath:path forResource:"drawCard" ofType:"snd"]) 
	{
		drawSound = [[SoundEffect allocFromZone:[self zone]] initFromSection:path];
	}
	
	// If not preferences for number of decks, we'll use eight decks
	[self selectCellWithTag:8 forPopUpButton:deckPrefButton];
	
	// Get preferences
	numDecks = -1;
	[self revertPreferences:self];
	[self getPreferences:self];
	
	// Get ready for a new game
	[self initializeTable];
	[[self clearHands] enableBetViews:YES];
	
	[standButton setEnabled:NO];
	[hitButton setEnabled:NO];
	[hintButton setEnabled:NO];
	
	[self updateTable];
	
	return self;
}
 
- initializeTable
/*
	Re-sets the table for a new game.
	All players are set to BJ_PLAYING, and their betviews are set to 1 column
*/
{
	int i;
	
	for(i=0; i<BJ_MAXPLAYERS+1; i++)
	{
		players[i].hand[BJ_REGULAR_HAND].status = BJ_PLAYING;
				
		[[players[i].betView setNumCols:1] positionChips];
		[[players[i].insuranceView setNumCols:1] positionChips];
	}
		
	return self;
}

- newDeck:sender
/*
	Set the user values for the deck.  Face cards count as ten, and aces as 11.
*/
{
	int thecard, cardValue;
	
	drawPile = [drawView cardPile];
	[drawView setCardSize:CS_SMALL];
	
	[self newDeck:numDecks forView:drawView];
	
	// promote all aces to have value ACE_HIGH_VALUE, face cards to FACE_VALUE
	for(thecard = 0; thecard < [drawPile cardCount]; thecard++)
	{ 
		cardValue = [[drawPile cardAt:thecard] value];
		switch(cardValue)
		{
			case CS_ACE:	[[drawPile cardAt:thecard] setUserValue:BJ_ACE_HIGH_VALUE];
						 	break;
			case CS_TWO:	[[drawPile cardAt:thecard] setUserValue:BJ_TWO_VALUE]; 
							break;
			case CS_THREE:	[[drawPile cardAt:thecard] setUserValue:BJ_THREE_VALUE];
							break;
			case CS_FOUR:	[[drawPile cardAt:thecard] setUserValue:BJ_FOUR_VALUE];
							break;
			case CS_FIVE:	[[drawPile cardAt:thecard] setUserValue:BJ_FIVE_VALUE]; 
							break;
			case CS_SIX:	[[drawPile cardAt:thecard] setUserValue:BJ_SIX_VALUE]; 
							break;
			case CS_SEVEN:	[[drawPile cardAt:thecard] setUserValue:BJ_SEVEN_VALUE];
							break;
			case CS_EIGHT:	[[drawPile cardAt:thecard] setUserValue:BJ_EIGHT_VALUE];
							break;
			case CS_NINE:	[[drawPile cardAt:thecard] setUserValue:BJ_NINE_VALUE];
							break;
			case CS_TEN:	[[drawPile cardAt:thecard] setUserValue:BJ_TEN_VALUE];
							break;
			case CS_JACK:	[[drawPile cardAt:thecard] setUserValue:BJ_FACE_VALUE];
							break;
			case CS_QUEEN:	[[drawPile cardAt:thecard] setUserValue:BJ_FACE_VALUE];
							break;
			case CS_KING:	[[drawPile cardAt:thecard] setUserValue:BJ_FACE_VALUE];
							break;
		}
	}
	
	return self;
}



/***************************************************************************************
 *                                Game Flow Methods                                    *
 ***************************************************************************************/
- startGame
/*
	When the player hits the deal button, this method is called.  It starts the game
	and controls it up to the point where players are asked to hit or stand.
	
	We get players, clear the hands, disable betviews, get a new deck if necessary, 
	deal cards.  If dealer has ace showing we ask players if they want insurance, and
	if dealer has blackjack we end the game.  Otherwise the control passes to the 
	current player.
*/
{
	if(kludgeTE)
		DPSRemoveTimedEntry(kludgeTE);
	
	[dealButton setEnabled:NO];
	[self getPlayers];  // find out whose in the game (have bets)
	
	if([self anyBetsOutsideLimits]) return nil;
	
	dealerShouldFinishHand = NO;
	gameInProgress = YES;
	
	[self getPreferences:self];
	
	[[self clearHands] enableBetViews:NO];
	[self initializeTable];
	
	// Get new deck if this one's expired, or near so...
	if([drawPile cardCount] < [reshuffleText intValue])
	{
		[self newDeck:self];
	} 
	
	currentPlayer = -1;
	
	[self say:"Dealing Cards..."];
	NXPing();
	[self dealCards:2];  // Deal 2 cards to each player
	
	// see if we have an ace showing, get insurance...
	if([self checkForInsurance] == nil)
	{
		// We had blackjack, so game is over...
		gameInProgress = NO;
		[self endGame];
		return self;
	}	
	
	// If players want the dealer to always peak at the hole card, then do so...
	// End game if dealer has blackjack
	if([[players[BJ_DEALER].hand[BJ_REGULAR_HAND].pile cardAt:CS_TOP] userValue] == 	
			BJ_FACE_VALUE && peakAtHoleAlways)
	{ 
		[self say:"Dealer is peaking at hole card..."];
		NXPing();
		sleep(1);
		if(players[BJ_DEALER].hand[BJ_REGULAR_HAND].total == BJ_BLACKJACK)
		{
			// flip dealers hole card over
			[[players[BJ_DEALER].hand[BJ_REGULAR_HAND].pile cardAt:CS_BOTTOM] setFaceUp:YES];
			[players[BJ_DEALER].hand[BJ_REGULAR_HAND].view display];
			NXPing();
			[drawSound play:1.0 pan:0.0];
			usleep(BJ_DRAWPAUSE);
			
			[self say:"Dealer Has Blackjack!"];
			NXPing();
			[PBoss() playSound:NV_LOSESOUND];
			
			gameInProgress = NO;
			[self endGame];
			return self;
		}
	}
	currentPlayer = 0;
	currentHand = BJ_REGULAR_HAND;
	
	[hintButton setEnabled:YES];
	[hitButton setEnabled:YES];
	[standButton setEnabled:YES];
	
	[self updateTable];
	[self checkCurrentPlayer];
	
	// From this point on, its up to the players to take cards...  When play reaches
	// the dealer, he takes cards, and when he's done he calls endGame:
		
	return self;
}

- clearHands
/*
	Clear player's hands, including the dealer's
*/
{
	int p;
	
	for(p=0; p<BJ_MAXPLAYERS+1; p++)
	{
		[players[p].hand[BJ_REGULAR_HAND].pile freeCards];
		[players[p].hand[BJ_REGULAR_HAND].view display];
		[players[p].hand[BJ_SPLIT_HAND].pile freeCards];
		[players[p].hand[BJ_SPLIT_HAND].view display];
		
		players[p].hand[BJ_REGULAR_HAND].wasPaid = NO;
		players[p].hand[BJ_SPLIT_HAND].wasPaid = NO;
		
		players[p].hand[BJ_SPLIT_HAND].total = 0;
		players[p].hand[BJ_REGULAR_HAND].total = 0;
		
		players[p].hand[BJ_REGULAR_HAND].status = BJ_PLAYING;
		players[p].hand[BJ_SPLIT_HAND].status = BJ_PLAYING;
		
		[players[p].hand[BJ_REGULAR_HAND].handText setStringValue:""];
		[players[p].hand[BJ_SPLIT_HAND].handText setStringValue:""];
		
		[players[p].hand[BJ_REGULAR_HAND].totalText setStringValue:""];
		[players[p].hand[BJ_SPLIT_HAND].totalText setStringValue:""];
		
		players[p].numHands = 1;
	}
	
	return self;
}

- dealCards:(int)numCards
/*
	Deal a card to every player.
*/
{
	int c, p;
	
	dealingCards = YES;
	
	for(c=0; c<numCards; c++)
	{
		for(p=0; p<BJ_MAXPLAYERS+1; p++)
		{
			// in case of emergency...
			if([drawPile cardCount] == 0)
				[self newDeck:self];
			
			if(players[p].isPlaying)
			{
				// if player is dealer, and this is our first card, deal it face down
				if(p == BJ_DEALER && c==0)
					[self drawCardForPlayer:p forHand:BJ_REGULAR_HAND faceUp:NO];
				else
					[self drawCardForPlayer:p forHand:BJ_REGULAR_HAND faceUp:YES];
			}
		}
	}
	
	dealingCards = NO;
		
	return self;
}

- drawCardForPlayer:(int)playerNum forHand:(int)aHand faceUp:(BOOL)shouldBeUp
/*
	Player num is index of player in players.  aHand is which hand to draw a card
	for.  REGULAR_HAND = hand, SPLIT_HAND = splitHand.
*/
{
	id topCard = [drawPile cardAt:CS_TOP];
	
	[drawPile removeCard:topCard];
	[drawView display];
	[players[playerNum].hand[aHand].pile addCard:topCard];
	
	if(shouldBeUp)	
		[topCard setFaceUp:YES];
	else
		[topCard setFaceUp:NO];
		
	[players[playerNum].hand[aHand].view display];
	NXPing();  // attempt to synch sound with graphics...
	[drawSound play:1.0 pan:0.0];
	
	[self handTotalForPlayer:playerNum forHand:aHand];
	[self updateTable];
	NXPing();
	
	usleep(BJ_DRAWPAUSE);
	
	return self;
}

- checkForInsurance
/*
	Called when the dealer has an ACE showing.  We ask each playing player if
	the want insurance.  If they do, we automatically place 1/2 their wager on
	the insurance line for them.
*/
{
	int p, amount;
	
	if([[players[BJ_DEALER].hand[BJ_REGULAR_HAND].pile cardAt:CS_TOP] value] == CS_ACE)
	{
		[self say:"Ace Showing..."];

		for(p=0; p<BJ_MAXPLAYERS; p++)
		{
			if(players[p].isPlaying &&
				 players[p].hand[BJ_REGULAR_HAND].total != BJ_BLACKJACK)
			{
				switch(NXRunAlertPanel([NXApp appName], 
					"%s, the Dealer has an ACE showing.  Insurance?", 
					"No Thanks", "Sure", NULL, [players[p].player playerName]))
				{
					case NX_ALERTALTERNATE:	
						// make an insurance bet for them
						amount = [players[p].betView 
							amountAtRow:0 col:BJ_REGULAR_HAND]/2;
						[players[p].insuranceView bet:amount atRow:0 
																col:BJ_REGULAR_HAND];
						[players[p].player removeChip:amount];
						break;
					case NX_ALERTDEFAULT:	// they declined...
						break;
				}
			}
		}
		usleep(BJ_LONGPAUSE);
		
		if(players[BJ_DEALER].hand[BJ_REGULAR_HAND].total == BJ_BLACKJACK)
		{
			// flip dealers hole card over
			[[players[BJ_DEALER].hand[BJ_REGULAR_HAND].pile cardAt:CS_BOTTOM] setFaceUp:YES];
			[players[BJ_DEALER].hand[BJ_REGULAR_HAND].view display];
			NXPing();
			[drawSound play:1.0 pan:0.0];
			usleep(BJ_DRAWPAUSE);
			
			[PBoss() playSound:NV_LOSESOUND];
			[self say:"Dealer Has Blackjack!  Insurance pays 2 to 1!"];
			NXPing();
			
			usleep(BJ_LONGPAUSE);
		
			// Finish game.
			[self payInsuranceBets:YES];
			
			return nil;
		}
		else
		{
			[self say:"Dealer does not have Blackjack..."];
			NXPing();
			usleep(BJ_LONGPAUSE);
			[self payInsuranceBets:NO];
		}
	}
	
	return self;
}

- payInsuranceBets:(BOOL)shouldPay
/*
	Everyone with an insurance wager wins 2 for 1 if shouldPay is YES, else
	clears their bet.
*/
{
	int p;
	
	for(p=0; p<BJ_MAXPLAYERS; p++)
	{
		if(players[p].isPlaying)
		{
			if(shouldPay)
				[players[p].insuranceView payBets];
			else
				[players[p].insuranceView clearBets];
		}
	}
	
	return self;
}

- doDealersHand
/*
	Draw cards for dealer.  When finished, check bets and pay winners.  
	
	This is the last action of the game.
*/
{		
	BJHand *dealerHand = &players[BJ_DEALER].hand[BJ_REGULAR_HAND];
	
	gameInProgress = NO;

	[self updateTable];
	
	[self say:"Dealer's Hole Card..."];
	NXPing();
	
	// flip dealers hole card over
	[[dealerHand->pile cardAt:CS_BOTTOM] setFaceUp:YES];
	[dealerHand->view display];
	NXPing();
	[drawSound play:1.0 pan:0.0];
	usleep(BJ_DRAWPAUSE);

	[self updateTable];
		
	if(dealerShouldFinishHand)
	{
		[self say:"Dealer's Draws..."];
		NXPing();
	
		if(dealerHitSoft17)
		{
			while(dealerHand->total < 17 || 
				(dealerHand->total == 17 && [self handIsSoft:BJ_REGULAR_HAND forPlayer:BJ_DEALER]))
			{
				[self drawCardForPlayer:BJ_DEALER forHand:BJ_REGULAR_HAND faceUp:YES];
			}
		}
		else
		{
			while(dealerHand->total < 17)
			{
				[self drawCardForPlayer:BJ_DEALER forHand:BJ_REGULAR_HAND faceUp:YES];
			}
		}
	}
			
	return self;
}


- endGame
/*
	When the last player stands, then this method is called.  We do the dealers hand,
	and check the players hands.  Then we get ready for a new game.
*/
{ 
	int clearValue;
	
	[hintButton setEnabled:NO];
	[hitButton setEnabled:NO];
	[standButton setEnabled:NO];

	[self updateTable];
		
	if(gameInProgress)
		[self doDealersHand];

	[self say:"Checking player's hands..."];
	
	[[self setWinStatus] payOut];
	
	clearValue = [clearTableText intValue];
	if(clearValue != BJ_DONTCLEARTABLE)
	{
		[self say:"Clearing Cards From Table..."];
		
		if(clearValue)
			sleep(clearValue);
		
		[self clearHands];
		NXPing();
	}
	
	currentPlayer = 0;
	currentHand = BJ_REGULAR_HAND;
	
	[self updateTable];
	
	[self enableBetViews:YES];
	[self say:"New Game...  Place Your Bets!"];
	
	sleep(1);
	[self removeWinnings];
	[self placeDefaultBets];
	
	[self enableDealButton];
	
	return self;
}

- setWinStatus
/*
	Go through each players hand(s) and find out if they've won or lost.
	
	Sets the status field in players.hand to either WINNER, LOSER, PUSH, or BUSTED.
*/
{
	int p, h, playerTotal, dealerTotal;
		
	dealerTotal = players[BJ_DEALER].hand[BJ_REGULAR_HAND].total;
	
	for(p=0; p<BJ_MAXPLAYERS; p++)
	{
		if(players[p].isPlaying) 
		{
			for(h=0; h<players[p].numHands; h++)
			{
				if(players[p].hand[h].wasPaid == NO)
				{
					playerTotal = players[p].hand[h].total;
					
					// first check for Blackjack.
					if([players[p].hand[h].pile isBlackjack])
					{
						// If dealer has BJ also, then its a push
						if([players[BJ_DEALER].hand[BJ_REGULAR_HAND].pile isBlackjack])
						{
							players[p].hand[h].status = BJ_PUSH;
							[players[p].hand[h].handText setStringValue:"Push"];
						}
						else	// player has BJ!
						{
							[players[p].hand[h].handText setStringValue:"BJ!"];
							players[p].hand[h].status = BJ_BLACKJACK;
						}
					} 
					// Check for dealer's blackjack	
					else if([players[BJ_DEALER].hand[BJ_REGULAR_HAND].pile isBlackjack])
					{
						// If player has BJ also, then its a push
						if([players[p].hand[h].pile isBlackjack])
						{
							players[p].hand[h].status = BJ_PUSH;
							[players[p].hand[h].handText setStringValue:"Push"];
						}
						else	// player loses!
						{
							[players[p].hand[h].handText setStringValue:"Lose"];
							players[p].hand[h].status = BJ_LOSER;
						}
					}
					
					// Now check to see if player busted
					else if(playerTotal > BJ_BLACKJACK)
					{
						players[p].hand[h].status = BJ_BUSTED;
						[players[p].hand[h].handText setStringValue:"Bust"];
					}
					
					// If dealer busted, everyone who hasn't wins...
					else if(dealerTotal > BJ_BLACKJACK)
					{
						players[p].hand[h].status = BJ_WINNER;
						[players[p].hand[h].handText setStringValue:"Win!"];
					}
					
					// Player doesn't have blackjack, player & dealer haven't busted,
					// so now compare their hands
					
					// Is player's hand greater than dealers?
					else if(playerTotal > dealerTotal)
					{
						players[p].hand[h].status = BJ_WINNER;
						[players[p].hand[h].handText setStringValue:"Win!"];
					}
					
					// Is dealer's hand greater than player's?
					else if(dealerTotal > playerTotal)
					{
						players[p].hand[h].status = BJ_LOSER;
						[players[p].hand[h].handText setStringValue:"Lose"];
					}
					
					// Finally, it must be a push
					else if(dealerTotal == playerTotal)
					{
						players[p].hand[h].status = BJ_PUSH;
						[players[p].hand[h].handText setStringValue:"Push"];
					}
				}

			}
		}
	}
	 
	return self;
}

- payOut
/*
	For each player, calls payOutForPlayer: to pay a players bet.
*/
{
	int p;
	char buf[128];
	
	sleep(1);

	for(p=0; p<BJ_MAXPLAYERS; p++)
	{
		if(players[p].isPlaying)
		{
			sprintf(buf, "Checking %s's wager...", [players[p].player playerName]);
			[self say:buf];
			currentPlayer = p;
			
			[self payOutForPlayer:p];
			NXPing();
		}
	}
	
	[self say:""];
	
	return self;
}

- payOutForPlayer:(int)playerNum
/*
	Pay the winners, clear the bets of the losers.  Players with PUSHed hands remain
	unchanged.
*/
{
	int h, stat, total=0, numchips;
	id betview;
	BOOL doSound;
	
	if(players[playerNum].isPlaying)
	{
		betview = players[playerNum].betView;
		doSound = YES;
		
		for(h=0; h<players[playerNum].numHands; h++)
		{
			
			if(players[playerNum].hand[h].wasPaid == NO)
			{
				stat = players[playerNum].hand[h].status;
				switch(stat)
				{
					case BJ_BUSTED:
						[[betview clearBetAtRow:0 col:h] display];
						break;
					case BJ_LOSER:
						[[betview clearBetAtRow:0 col:h] display];
						break;
					case BJ_WINNER:
						[betview payBetAtRow:0 col:h];
						break;
					case BJ_PUSH:
						doSound = NO;
						break;
					case BJ_BLACKJACK:
						// Special case...
						[betview setPayoff:3 cost:2 atRow:0 col:h];
						[betview payBetAtRow:0 col:h];
						[betview setPayoff:1 cost:1 atRow:0 col:h];
						break;
				}

				players[playerNum].hand[h].status = BJ_NOT_PLAYING;
				players[playerNum].hand[h].wasPaid = YES;
			}
			else if(players[playerNum].hand[h].wasPaid == YES)
				doSound = NO;
		}
		
		// Now collapse into one bet
		numchips = [[players[playerNum].insuranceView chipPileAtRow:0 col:0] numChips];
		numchips += [[betview chipPileAtRow:0 col:BJ_REGULAR_HAND] numChips];
		numchips += [[betview chipPileAtRow:0 col:BJ_SPLIT_HAND] numChips];
		
		total = [players[playerNum].insuranceView collectBets];
		total += [betview collectBets];
		
		if(doSound)
			[PBoss() playSound:NV_CHIPSOUND];
			
		[[betview setNumCols:1] positionChips];
		[[[betview chipPileAtRow:0 col:BJ_REGULAR_HAND] setAmount:total]
			 setNumChips:numchips]; 
		[betview display];
	}
	
	return self;
}



/***************************************************************************************
 *                            Target/Action Methods                                    *
 ***************************************************************************************/

- deal:sender
/*
	Sender is the dealButton.
*/
{
	if(gameInProgress)
		return nil;
		
	[dealButton setEnabled:NO];
	
	kludgeTE = DPSAddTimedEntry(.05, (DPSTimedEntryProc)BJ_RunGame, "startGame", NX_BASETHRESHOLD);
	
	return self;
}

- doubleDown:sender
{
	[doubleButton setEnabled:NO];
	
	kludgeTE = DPSAddTimedEntry(.05, (DPSTimedEntryProc)BJ_RunGame, "doubleDown", NX_BASETHRESHOLD);
	
	return self;
}

- doubleDown
/*
	Double players bet, give player one card, then make him stand.
*/
{
	int amount;
	int chand = currentHand, cplayer = currentPlayer;	
	
	if(kludgeTE)
		DPSRemoveTimedEntry(kludgeTE);
	
	amount = [players[currentPlayer].betView amountAtRow:0 col:currentHand];
	if([players[currentPlayer].player amountInBank] < amount)
	{
		[PBoss() playSound:NV_WARNSOUND];
		[self say:"You don't have enough money to double down right now!"];
		return nil;
	}
	
	[players[currentPlayer].player removeChip:amount];
	[PBoss() playSound:NV_CHIPSOUND];
	[[players[currentPlayer].betView chipPileAtRow:0 col:currentHand]
		addPile:[players[currentPlayer].betView chipPileAtRow:0 col:currentHand]];
	[players[currentPlayer].betView display];
	
	players[currentPlayer].hand[currentHand].canDouble = NO;
	
	usleep(BJ_DRAWPAUSE);
	
	[self hit];
	
	currentHand = chand;
	currentPlayer = cplayer;
	
	[self stand];
		
	return self;
}

- hit:sender
{
	[hitButton setEnabled:NO];
	
	kludgeTE = DPSAddTimedEntry(.05, (DPSTimedEntryProc)BJ_RunGame, "hit", NX_BASETHRESHOLD);
	
	return self;
}

- hit
/*
	Current player wants another card.  Give it to him, and then find his total.
	If he busted, either go on to his next hand or to the next player.
*/
{
	if(kludgeTE)
		DPSRemoveTimedEntry(kludgeTE);
	
	[self drawCardForPlayer:currentPlayer forHand:currentHand faceUp:YES];
	
	if(players[currentPlayer].hand[currentHand].total > BJ_BLACKJACK)
	{
		players[currentPlayer].hand[currentHand].status = BJ_BUSTED;
		[players[currentPlayer].hand[currentHand].handText setStringValue:"Bust"];
		
		usleep(BJ_DRAWPAUSE);
		
		// immediately remove bet
		[[players[currentPlayer].betView clearBetAtRow:0 col:currentHand] display];
		NXPing();
		[pitBoss playSound:NV_CHIPSOUND];

		players[currentPlayer].hand[currentHand].wasPaid = YES;
		
		[self incrementHand];
	}
	else if(players[currentPlayer].hand[currentHand].total == BJ_BLACKJACK)
	{
		[players[currentPlayer].hand[currentHand].handText setStringValue:""];
		dealerShouldFinishHand = YES;
	
		[self incrementHand];
	}
	
	[hitButton setEnabled:YES];
	
	[self updateTable];
	
	[self checkCurrentPlayer];
	
    return self;
}

- split:sender
{
	kludgeTE = DPSAddTimedEntry(.05, (DPSTimedEntryProc)BJ_RunGame, "split", NX_BASETHRESHOLD);
	
	return self;
}

- split
{
	id card;
	
	[splitButton setEnabled:NO];
	
	if(kludgeTE)
		DPSRemoveTimedEntry(kludgeTE);
	
	if([self playerWillSplit:currentPlayer])
	{
		// move card at top of regular hand over to split hand
		card = [players[currentPlayer].hand[BJ_REGULAR_HAND].pile cardAt:CS_TOP];
		[players[currentPlayer].hand[BJ_SPLIT_HAND].pile addCard:card];
		[players[currentPlayer].hand[BJ_REGULAR_HAND].pile removeCard:card];	
		
		[players[currentPlayer].hand[BJ_SPLIT_HAND].view display];
		[players[currentPlayer].hand[BJ_REGULAR_HAND].view display];	
			
		
		return [self playerDidSplit:currentPlayer];
	}
	
	return self;
}

- stand:sender
{
	[standButton setEnabled:NO];
	
	kludgeTE = DPSAddTimedEntry(.05, (DPSTimedEntryProc)BJ_RunGame, "stand", NX_BASETHRESHOLD);
	
	return self;
}

- stand
/*
	Player doesn't want anymore cards or doesn't want insurance.  Go on to next
	player, or the players next hand if he has one.
*/
{
	if(kludgeTE)
		DPSRemoveTimedEntry(kludgeTE);
	
    [players[currentPlayer].hand[currentHand].handText setStringValue:""];
	
	dealerShouldFinishHand = YES; 
			
	[standButton setEnabled:YES];
	
	[self incrementHand];
	
	[self checkCurrentPlayer];
	
	return self;
}

- surrender:sender
{
	[surrenderButton setEnabled:NO];
	
	kludgeTE = DPSAddTimedEntry(.05, (DPSTimedEntryProc)BJ_RunGame, "surrender", NX_BASETHRESHOLD);
	
	return self;
}

- surrender
{
	BJHand *thisHand;
	
	if(kludgeTE)
		DPSRemoveTimedEntry(kludgeTE);
	
	thisHand = &players[currentPlayer].hand[currentHand];
	
    [players[currentPlayer].hand[currentHand].handText setStringValue:""];
	
	[thisHand->pile empty];
	[thisHand->view display];
	[thisHand->totalText setStringValue:""];
	thisHand->total = 0;
	[players[currentPlayer].betView multiplyBetsAtCol:currentHand by:.5];
	NXPing();
	[PBoss() playSound:NV_CHIPSOUND];
	
	thisHand->wasPaid = YES;
	
	[self incrementHand];
	[self checkCurrentPlayer];
	
	return self;
}

- updatePButtons:sender
/*
	Fills the cells in the PButtons with the names of the currently opened players.
	Calls updatePlayerAreas to set the players the are selected in the PButtons.
*/
{
	int p, tag;
	id	cell, player, matrix;
	char buf[128];
	
	for(p=0; p<BJ_MAXPLAYERS; p++)
	{
		matrix = [[players[p].PButton target] itemList];
		
		for(tag=0; tag<BJ_MAXPLAYERS; tag++)
		{
			player = [PBoss() playerAt:tag];
			
			cell = [matrix findCellWithTag:tag];
			
			if(player)
				[cell setTitle:[player playerName]];
			else
			{
				sprintf(buf, "Player %d", tag+1);
				[cell setTitle:buf];
			}
		}

		[players[p].PButton setTitle:[[matrix selectedCell] title]];
	}
	
	[self updatePlayerAreas:sender];
	
	return self;
}

- updatePlayerAreas:sender
/*
	Sets the players the are selected in the PButtons.
*/
{
	int p, tag;
	
	for(p=0; p<BJ_MAXPLAYERS; p++)
	{
		tag = [self tagOfSelectedCellInPopUpButton:players[p].PButton];
		players[p].player = [PBoss() playerAt:tag];
	}
	
	[self updateTable];
	
	return self;
	
}

/***************************************************************************************
 *                                 Utility Methods                                     *
 ***************************************************************************************/

- (BOOL)anyBetsOutsideLimits;
/*
	Make sure that all bets are at least the table min, and no greater than the table
	max for each player.  If a bad bet is encounted, warn player and return YES, else
	return NO.
*/
{
	int p, amount;
	
	for(p=0; p<BJ_MAXPLAYERS; p++)
	{
		if(players[p].isPlaying)
		{
			amount = [players[p].betView amountAtRow:0 col:BJ_REGULAR_HAND];
			if(amount < [self tableMin])
			{
				NXBeep();
				NXRunAlertPanel([NXApp appName], 
					"%s, your $%d wager at seat #%d is below the table minimum!", 
					"Okay", NULL, NULL, [players[p].player playerName], amount, p+1);
				return YES;
			}
			else if(amount > [self tableMax])
			{
				NXBeep();
				NXRunAlertPanel([NXApp appName], 
					"%s, your $%d wager at seat #%d is above the table maximum!", 
					"Okay", NULL, NULL, [players[p].player playerName], amount, p+1);
				return YES;
			}
		}
	}
	
	return NO;
}

- checkCurrentPlayer
/*
	Asks current player if they want to hit or stand, etc.  If the current player doesn't
	exist, or the current hand doesn't exist for the current player, we increment the 
	hand (or player) (ie go on to the next hand or player).
*/
{
	char buf[128], hint[128];
	
	// if this current player doesn't exist, go on to next player
	if(players[currentPlayer].isPlaying == NO)
	{
		[self incrementHand];
		
		return [self checkCurrentPlayer];
	}
	
	// if we've exhausted player list, do dealer's hand
	if(currentPlayer >= BJ_MAXPLAYERS) 
	{
		[self endGame];	
		return self;
	}
	
	// if this player's hand is not in play, check next hand
	if(players[currentPlayer].hand[currentHand].status != BJ_PLAYING ||
		players[currentPlayer].hand[currentHand].total >= BJ_BLACKJACK)
	{
		[self incrementHand];
		return [self checkCurrentPlayer];
	}
	
	if(friendlyDealer)
	{
		switch([self getHintForCurrent])
		{
			case BJ_HIT:	strcpy(hint, "I think you should HIT."); break;
			case BJ_DOUBLE:	strcpy(hint, "I think you should DOUBLE."); break;
			case BJ_SPLIT:	strcpy(hint, "I think you should SPLIT."); break;
			case BJ_STAND:	strcpy(hint, "I think you should STAND."); break;
			case BJ_SURRENDER:	strcpy(hint, "I think you should SURRENDER."); break;
			case BJ_NOHINT:	strcpy(hint, "It's all about you... I don't have a clue.");
				 break;
		}
	}
	else 
		strcpy(hint, "");
		
	sprintf(buf, "%s: Hit, %s%sor Stand?    %s", 
		[players[currentPlayer].player playerName],
		(players[currentPlayer].hand[currentHand].canDouble) ? "Double, " : "", 
		(players[currentPlayer].hand[currentHand].canSplit) ? "Split, " : "",
		hint);
	
	[players[currentPlayer].hand[currentHand].handText setStringValue:"Hit?"];

	[self say:buf];
	[pitBoss setCurrentPlayer:players[currentPlayer].player];
	[self updateTable];
	
	return self;
}

- enableBetViews:(BOOL)flag
/*
	Enables bet views if flag is yes, disables if flag is no.  Also enables/disables 
	the PButtons.
*/
{
	int p;
	
	for(p=0; p<BJ_MAXPLAYERS; p++)
	{
		[players[p].betView setEnabled:flag];
		[players[p].PButton setEnabled:flag];
	}
	
	return self;
}

- enableDealButton
/*
	Checks each player's betview to see if there's a bet.  If it finds a bet on any of
	the betviews, the deal button will be enabled.

	If it doesn't find any bets on the table, the deal button will be disabled.
*/
{
	int i;
	
	for(i=0; i<BJ_MAXPLAYERS; i++)
	{
		if([players[i].betView amountAtRow:0 col:BJ_REGULAR_HAND])
		{
			[dealButton setEnabled:YES];
			return self;
		}
	}
	
	[dealButton setEnabled:NO];
	
	return self;
}

- finishSessionAndClose
{
	int p;
	
	for(p=0; p<BJ_MAXPLAYERS+1; p++)
	{
		[players[p].betView discardTrackingRect];
		[players[p].insuranceView discardTrackingRect];
	}
	
	return self;
}

- (int)getPlayers
/*
	Scans each player, finds out who has a bet placed, and sets that player as playing.
	
	Returns the number of players playing.
*/
{
	int p, np=0;
	for(p=0; p<BJ_MAXPLAYERS; p++)
	{
		if(players[p].player)
		{
			if([players[p].betView amountAtRow:0 col:BJ_REGULAR_HAND] > 0)
			{
				players[p].isPlaying = YES;
				np++;
			}
			else
			{
				players[p].isPlaying = NO;
				[players[p].dBetButton setState:0];
			}
		}
		else
			players[p].isPlaying = NO;
	}
	
	players[BJ_DEALER].isPlaying = YES;
	
	return np;
}

- (BOOL)handIsSoft:(int)aHand forPlayer:(int)playerNum
/*
	Returns YES if the hand has an ACE that is being counted as 11 rather than 1, NO
	if not.
*/
{
	int c;
	BJHand *thisHand;
	
	thisHand = &players[playerNum].hand[aHand];
		
	for(c=0; c<[thisHand->pile cardCount]; c++)
	{
		if([[thisHand->pile cardAt:c] userValue] == BJ_ACE_HIGH_VALUE)
			return YES;
	}
	
	return NO;
}

- (int)handTotalForPlayer:(int)playerNum forHand:(int)aHand
/*
	Add up all the user values in players hand aHand.  If the player's total is
	greater than 21, and the player has an ace that was counted as an 11 value, then
	set the ace to have the value of one and then recursively call this method 
	again to get the total.
	
	Sets the total field in the players.hand structure.
*/
{
	int total=0, c, numCards, nextCardValue;
	BJHand *thisHand;
	
	thisHand = &players[playerNum].hand[aHand];

	numCards = [thisHand->pile cardCount];
	
	total = [thisHand->pile userTotal];
	
	if(total > BJ_BLACKJACK)
	{
		// find an ace, demote it to value of 1
		for(c=0; c<numCards; c++)
		{
			nextCardValue = [[thisHand->pile cardAt:c] userValue];
			if(nextCardValue == BJ_ACE_HIGH_VALUE)
			{
				[[thisHand->pile cardAt:c] setUserValue:BJ_ACE_LOW_VALUE];
				return [self handTotalForPlayer:playerNum forHand:aHand];
			}
		}
	}
	
	if(numCards == 2 && canAlwaysDouble)
		thisHand->canDouble = YES;
	else if(numCards == 2 && (total==10 || total==11))
		thisHand->canDouble = YES;
	else if(numCards == 2 && total==20 && [self handIsSoft:aHand forPlayer:playerNum])
	{
		thisHand->canDouble = YES;
	}
	else
		thisHand->canDouble = NO;

	if(numCards == 2 && aHand == BJ_REGULAR_HAND && 
		(([[thisHand->pile cardAt:CS_TOP] userValue] == 
							[[thisHand->pile cardAt:CS_BOTTOM] userValue]) ||
		([[thisHand->pile cardAt:CS_TOP] value] == 
							[[thisHand->pile cardAt:CS_BOTTOM] value])))
		thisHand->canSplit = YES;
	else
		thisHand->canSplit = NO;
		
	if(numCards == 2)
		thisHand->canSurrender = YES;
	else
		thisHand->canSurrender = NO;
	
	thisHand->total = total;
	
	return total;
}

- incrementHand
/*
	moves currentHand pointer to next hand.  If the current player has no next
	hand, then moves to next player's regular hand.  If there is not a next player,
	then moves to dealer.
*/
{
	currentHand++;
	
	if(currentHand >= players[currentPlayer].numHands)
	{
		currentHand = BJ_REGULAR_HAND;
		currentPlayer++;
	}
	
	// go to next player
	while(!players[currentPlayer].isPlaying && currentPlayer < BJ_MAXPLAYERS)
	{
		currentPlayer++;
		currentHand = BJ_REGULAR_HAND;
	}
	
	if(currentPlayer >= BJ_MAXPLAYERS)
	{
		currentPlayer = BJ_DEALER;
		currentHand = BJ_REGULAR_HAND;
	}
		
	return self;
}

- setDefaultBet:sender
{
	int amount = [sender intValue];
	
	if(amount < 0)
		amount = 0;
	else if(amount > [self tableMax])
		amount = [self tableMax];
	
	[sender setIntValue:amount];
	
	if(amount)
		[players[[sender tag]].dBetButton setState:1];
	else
		[players[[sender tag]].dBetButton setState:0];
	
	return self;
}

- placeDefaultBets
/*
	Checks each players DBetText to see if it has a value > 0, if so, it will make a
	bet for the player of that amount.
*/
{
	int p, amount, amountToBet;
	
	for(p=0; p<BJ_MAXPLAYERS; p++)
	{
		[self setDefaultBet:players[p].dBetText];
		
		if(players[p].player && [players[p].dBetButton state])
		{
			amountToBet = [players[p].dBetText intValue];
			amount = [players[p].betView amountAtRow:0 col:BJ_REGULAR_HAND];
			if(amount == 0 && amountToBet > 0 && 
				amountToBet < [players[p].player amountInBank])
			{
				[players[p].player removeChip:amountToBet];
				[players[p].betView bet:amountToBet atRow:0 col:BJ_REGULAR_HAND];
				NXPing();
			}
			else if(amount < [self tableMin] && amountToBet > 0 && 
				amountToBet < [players[p].player amountInBank])
			{
				amountToBet = amountToBet - amount;
				[players[p].player removeChip:amountToBet];
				[players[p].betView bet:amountToBet atRow:0 col:BJ_REGULAR_HAND];
				NXPing();
			}
		}
	}
	
	return self;
}

- removeWinnings
{
	int p, amount;
	
	for(p=0; p<BJ_MAXPLAYERS; p++)
	{
		amount = [players[p].betView amountAtRow:0 col:BJ_REGULAR_HAND];
		if([players[p].RWButton state] && amount)
		{
			[PBoss() playSound:NV_CHIPSOUND];
			[[players[p].betView clearBetAtRow:0 col:BJ_REGULAR_HAND] display];
			NXPing();
			[players[p].player addChip:amount];
		}
	}
	
	return self;
}

- updateTable
/*
	Using status, go over table and set buttons and things accordingly.
	
	Sets the buttons to reflect current possible actions for the current player, 
	highlights the current player's name, etc.
*/
{
	int p, h;
	
	// Compile a list of cards in buf for each player
	for(p=0; p<BJ_MAXPLAYERS; p++)
	{
		for(h=0; h<2; h++)
		{
			if(showTotal && players[p].hand[h].total>0)
				[players[p].hand[h].totalText setIntValue:players[p].hand[h].total];
			else
				[players[p].hand[h].totalText setStringValue:""];
		}
	}
	
	// Now do Dealer
	if(players[BJ_DEALER].hand[BJ_REGULAR_HAND].total > BJ_BLACKJACK)
	{
		// If dealer has busted...
		[dealerHandText setStringValue:"Busted!"];
	}
	else if(!gameInProgress && 
			[players[BJ_DEALER].hand[BJ_REGULAR_HAND].pile isBlackjack])
	{
		// If games not in progress and dealer has blackjack...
		[dealerHandText setStringValue:"Blackjack!"];
	}
	else
		[dealerHandText setStringValue:""];
	
	if(gameInProgress || players[BJ_DEALER].hand[BJ_REGULAR_HAND].total == 0)
		[dealerTotalText setStringValue:""];
	else if(showTotal)
	{
		char buf[128];
		
		sprintf(buf, 
			"Dealers Hand: %d", players[BJ_DEALER].hand[BJ_REGULAR_HAND].total);
		[dealerTotalText setStringValue:buf];
	}
	
	// set buttons...
	[doubleButton setEnabled:gameInProgress && !dealingCards &&
			players[currentPlayer].hand[currentHand].canDouble];
	[splitButton setEnabled:gameInProgress && !dealingCards &&
			players[currentPlayer].hand[currentHand].canSplit];
	[surrenderButton setEnabled:gameInProgress && !dealingCards &&
			players[currentPlayer].hand[currentHand].canSurrender];
	
	return self;
}

- view:aView wasLoadedOnTable:tableObject
/* 
	sent after the table was loaded on window, so we need to reset the backing on the
	card piles, and to set tracking rects for betviews
*/
{
	int p;
	
	// Fill cells of the PButtons with the names of the open players
	[self updatePButtons:self];

	for(p=0; p<BJ_MAXPLAYERS+1; p++)
	{
		[players[p].betView setTrackingRectForWindow:[pitBoss tableWindow]];
		[players[p].insuranceView setTrackingRectForWindow:[pitBoss tableWindow]];
	}
	
	[self say:"Welcome to Blackjack!  Place your bets!"];

	return self;
}

- (int)getHintForCurrent
{
	id currentPile = players[currentPlayer].hand[currentHand].pile;
	id dealerPile = players[BJ_DEALER].hand[BJ_REGULAR_HAND].pile;
	int handTotal = players[currentPlayer].hand[currentHand].total;
	int pindex=-1, dindex=-1, hint;
	int dealerUpCardValue = [[dealerPile cardAt:CS_TOP] userValue];
	
	switch(dealerUpCardValue)
	{
		case BJ_TWO_VALUE:		dindex = 0; break;
		case BJ_THREE_VALUE:	dindex = 1; break;
		case BJ_FOUR_VALUE:		dindex = 2; break;
		case BJ_FIVE_VALUE:		dindex = 3; break;
		case BJ_SIX_VALUE:		dindex = 4; break;
		case BJ_SEVEN_VALUE: 	dindex = 5; break;
		case BJ_EIGHT_VALUE:	dindex = 6; break;
		case BJ_NINE_VALUE:		dindex = 7; break;
		case BJ_TEN_VALUE:		dindex = 8; break;
		case BJ_ACE_HIGH_VALUE:	dindex = 9; break;
	}
	
	if([self handIsSoft:currentHand forPlayer:currentPlayer])
	{
		switch(handTotal)
		{
			case 20:	pindex = 0; break;
			case 19:	pindex = 1; break;
			case 18:	pindex = 2; break;
			case 17:	pindex = 3; break;
			case 16:	pindex = 4; break;
			case 15:	pindex = 5; break;
			case 14:	pindex = 6; break;
			case 13:	pindex = 7; break;
			case 12:	pindex = 8; break;
			default:	return BJ_NOHINT;
		}
		
		hint = BJ_Hint[pindex][dindex];
		if(hint == BJ_DOUBLE && !players[currentPlayer].hand[currentHand].canDouble) 
			hint = BJ_HIT;
		
		return hint;
	}
	else if([currentPile cardCount] == 2 &&
		[[currentPile cardAt:CS_TOP] userValue] == 
			[[currentPile cardAt:CS_BOTTOM] userValue])
	{
		switch(handTotal)
		{
			case 20:	pindex = 9; break;
			case 18:	pindex = 10; break;
			case 16:	pindex = 11; break;
			case 14:	pindex = 12; break;
			case 12:	pindex = 13; break;
			case 10:	pindex = 14; break;
			case 8:		pindex = 15; break;
			case 6:		pindex = 16; break;
			case 4:		pindex = 17; break;
			default:	return BJ_NOHINT;
		}
		hint = BJ_Hint[pindex][dindex];
		if(hint == BJ_DOUBLE && !players[currentPlayer].hand[currentHand].canDouble) 
			hint = BJ_HIT;
		
		return hint;
	}
	else if([currentPile cardCount] == 2 && handTotal >= 9 && handTotal <= 11)
	{
		switch(handTotal)
		{
			case 11:	pindex = 23; break;
			case 10:	pindex = 24; break;
			case 9:		pindex = 25; break;
			default:	return BJ_NOHINT;
		}
		hint = BJ_Hint[pindex][dindex];
		if(hint == BJ_DOUBLE && !players[currentPlayer].hand[currentHand].canDouble) 
			hint = BJ_HIT;
		
		return hint;
	}
	else if(players[currentPlayer].hand[currentHand].canSurrender &&
		![self handIsSoft:currentHand forPlayer:currentPlayer] &&
		(handTotal == 15 || handTotal == 16) &&
		(dealerUpCardValue == 7 || dealerUpCardValue == 8 || dealerUpCardValue == 9 ||
		 dealerUpCardValue == 10 || dealerUpCardValue == 11))
	{
		return BJ_SURRENDER;
	}
	else if(handTotal >= 12 && handTotal <= 16)
	{
		switch(handTotal)
		{
			case 16:	pindex = 18; break;
			case 15:	pindex = 19; break;
			case 14:	pindex = 20; break;
			case 13:	pindex = 21; break;
			case 12:	pindex = 22; break;
			default:	return BJ_NOHINT;
		}
		hint = BJ_Hint[pindex][dindex];
		if(hint == BJ_DOUBLE && !players[currentPlayer].hand[currentHand].canDouble) 
			hint = BJ_HIT;
		
		return hint;
	}
	else if(handTotal >= 17)
	{
		return BJ_STAND;
	}
	else if(handTotal <= 11)
	{
		return BJ_HIT;
	}
	
	return BJ_NOHINT;
}

- giveHintForCurrent:sender
{
	char buf[1024];
	int hint;
	
	if(!gameInProgress) return self;
	
	hint = [self getHintForCurrent];
	
	if(hint != BJ_NOHINT)
		sprintf(buf, "I think you should %s.", 
			(hint == BJ_HIT) ? "HIT" : (hint == BJ_STAND) ? "STAND" : (hint == BJ_SPLIT)
			? "SPLIT" : (hint == BJ_DOUBLE) ? "DOUBLE" : (hint == BJ_SURRENDER) ?
			"SURRENDER": "");
	else
		strcpy(buf, "I don't have a hint for you right now...");
		
	if(NXRunAlertPanel("Dealer Says...", buf, "Do It", "Don't Do It", NULL) == NX_ALERTDEFAULT)
	{
		switch(hint)
		{
			case BJ_HIT:		[self hit:self]; 		break;
			case BJ_SPLIT:		[self split:self]; 		break;
			case BJ_DOUBLE:		[self doubleDown:self]; break;
			case BJ_SURRENDER:	[self surrender:self]; 	break;
			case BJ_STAND:		[self stand:self];		break;
			default: break;
		}
	}
	
	return self;
}

/***************************************************************************************
 *                               Preferences/Defaults                                  *
 ***************************************************************************************/
- getPreferences:sender
/*
	Get all the preferences for this round.
*/
{
	id theMatrix;
	int oldNumDecks = numDecks;
	
	theMatrix = [[deckPrefButton target] itemList];
	numDecks = [[theMatrix selectedCell] tag]; 
	if(numDecks == 0) numDecks = 1;
	
	if(numDecks != oldNumDecks)
	{
		[self newDeck:self];
		if(reshuffleValue > numDecks*52)
			reshuffleValue = numDecks*52;
	}
	
	theMatrix = [[dealerHitPrefButton target] itemList];
	dealerHitSoft17 = (BOOL)[[theMatrix selectedCell] tag];
	if(dealerHitSoft17)
		[dealerHitSoft17Text setStringValue:"DEALER MUST HIT SOFT 17"];
	else
		[dealerHitSoft17Text setStringValue:"Dealer Must Draw To 16 And Stand On All 17s"];
	
	theMatrix = [[doublePrefButton target] itemList];
	canAlwaysDouble = (BOOL)[[theMatrix selectedCell] tag];
	
	showTotal = (BOOL)[showTotalButton state];
	
	theMatrix = [[peakAtHoleButton target] itemList];
	peakAtHoleAlways = (BOOL)[[theMatrix selectedCell] tag];
	
	if([reshuffleText intValue] > numDecks*52)
	{
		reshuffleValue = numDecks*52;
	}
	else if ([reshuffleText intValue] < BJ_MINSHUFFLEVALUE)
	{
		reshuffleValue = BJ_MINSHUFFLEVALUE;
	}
	else
	{
		reshuffleValue = [reshuffleText intValue];
	}
	[reshuffleText setIntValue:reshuffleValue];
	
	canBetterHandAfterSplittingAces = (BOOL)[betterHandButton state];
	
	if([clearTableText intValue] < 0)
		[clearTableText setIntValue:0];
	else if([clearTableText intValue] > BJ_DONTCLEARTABLE)
		[clearTableText setIntValue:BJ_DONTCLEARTABLE];
	
	friendlyDealer = (BOOL)[suggestMovesButton state];

	[self updateTable];
	
	[self preferencesChanged:self];
	
	return self;
}

- revertPreferences:sender
{
	[self selectCellWithTag:atoi(NXGetDefaultValue(BJ_NAMEFORDEFAULTS, "numDecksTag")) 
		forPopUpButton:deckPrefButton];
	
	[self selectCellWithTag:atoi(NXGetDefaultValue(BJ_NAMEFORDEFAULTS, "dealerPrefTag")) 
		forPopUpButton:dealerHitPrefButton];
	
	[self selectCellWithTag:atoi(NXGetDefaultValue(BJ_NAMEFORDEFAULTS, "doublePrefTag")) 
		forPopUpButton:doublePrefButton];
	
	[self selectCellWithTag:atoi(NXGetDefaultValue(BJ_NAMEFORDEFAULTS, "dealerPeakTag")) 
		forPopUpButton:peakAtHoleButton];
	
    [showTotalButton setState:
		atoi(NXGetDefaultValue(BJ_NAMEFORDEFAULTS, "showHandTotals"))];
	
	[reshuffleText setStringValue:
		NXGetDefaultValue(BJ_NAMEFORDEFAULTS,"reshuffleValue")];
		
	[betterHandButton setState:
		atoi(NXGetDefaultValue(BJ_NAMEFORDEFAULTS,"betterHandAfterAces"))];

	[clearTableText setIntValue:
		atoi(NXGetDefaultValue(BJ_NAMEFORDEFAULTS,"clearTable"))];

	[suggestMovesButton setState:
		atoi(NXGetDefaultValue(BJ_NAMEFORDEFAULTS,"suggestMoves"))];

	return self;
}

- setPreferences:sender
{
    static NXDefaultsVector newDefaults = {
        {"numDecksTag", "8"},
		{"dealerPrefTag", "0"},
		{"doublePrefTag", "0"},
		{"dealerPeakTag", "0"},
		{"showHandTotals", "0"},
		{"reshuffleValue", "BJ_MINSHUFFLEVALUE"},
		{"betterHandAfterAces", "1"},
		{"clearTable", "BJ_DONTCLEARTABLE"},
		{"suggestMoves", "0"},
		{NULL, NULL}
    };
    
    newDefaults[0].value = alloca(256);
    newDefaults[1].value = alloca(256);
    newDefaults[2].value = alloca(256);
    newDefaults[3].value = alloca(256);
	newDefaults[4].value = alloca(256);
    newDefaults[5].value = alloca(256);
    newDefaults[6].value = alloca(256);
    newDefaults[7].value = alloca(256);
    newDefaults[8].value = alloca(256);

	sprintf(newDefaults[0].value, "%d", 
		[[[[deckPrefButton target] itemList] selectedCell] tag]);
	sprintf(newDefaults[1].value, "%d", 
		[[[[dealerHitPrefButton target] itemList] selectedCell] tag]);
	sprintf(newDefaults[2].value, "%d", 
		[[[[doublePrefButton target] itemList] selectedCell] tag]);
	sprintf(newDefaults[3].value, "%d", 
		[[[[peakAtHoleButton target] itemList] selectedCell] tag]);
	sprintf(newDefaults[4].value, "%d", [showTotalButton state]);
	sprintf(newDefaults[5].value, "%d", [reshuffleText intValue]);
	sprintf(newDefaults[6].value, "%d", [betterHandButton state]);
	sprintf(newDefaults[7].value, "%d", [clearTableText intValue]);
	sprintf(newDefaults[8].value, "%d", [suggestMovesButton state]);

    NXWriteDefaults(BJ_NAMEFORDEFAULTS, newDefaults);
    
	return self;
}


/***************************************************************************************
 *                            Inspector And Rules Methods                              *
 ***************************************************************************************/
- inspector:sender
/*
	Should return the inspector for the game module.  The default method returns nil,
	indicating that there is no inspector available.
*/
{
	return [inspector contentView];
}

- (BOOL)hasRules
{
	return YES;
}


/***************************************************************************************
 *                                  Player Methods                                     *
 ***************************************************************************************/
- (int)collectAllBetsForPlayer:(int)playerNum
/*
	Collects all the bets a player may have on the table.  Checks each player area to
	see if that player "owns" it, then removes the bet there.  Returns the total amount
	that the player had on the table.
*/
{
	int i, p, total=0, tag;
	
	
	if(!gameInProgress)
	{
		for(p=0; p<BJ_MAXPLAYERS; p++)
		{
			tag = [self tagOfSelectedCellInPopUpButton:players[p].PButton];
			if(tag == playerNum)
			{
				// get amount for each hand and add to total
				for(i=0; i<players[p].numHands; i++)
				{
					total += [players[p].betView collectBetAtRow:0 col:i];
					[players[p].betView display];
				}
				
				total +=[players[p].insuranceView collectBetAtRow:0 col:BJ_REGULAR_HAND];
				[players[p].insuranceView display];
				
				[self enableDealButton];
			}
		}
	}
	else
	{
		[self say:"Can't remove chips while game is in progress!"];
		[PBoss() playSound:NV_WARNSOUND];
		NXPing();
	}
	
	return total;
}

- playerDidClose:aPlayer
/*
 *  Sent after a player has closed and is no longer part of the game.  No messages
 *  should be sent to aPlayer.  This method should just do some internal cleanup
 *  if necessary.
 */
{
	[self updatePButtons:self];

	[self updateTable];
	
	return self;
}

- playerDidJoin:player
{
	[self updatePButtons:self];
		
	[self updateTable];

	return self;
}

- playerDidSplit:(int)playerNum
{
	int prevAmount;
	BOOL splitAces;
	
	prevAmount = [players[playerNum].betView amountAtRow:0 col:BJ_REGULAR_HAND];
	splitAces = 
	    ([[players[playerNum].hand[currentHand].pile cardAt:CS_TOP] value] == CS_ACE);
	
	// make new bet for split hand...
	[[players[playerNum].betView setNumCols:2] positionChips];
	
	[PBoss() playSound:NV_CHIPSOUND];
	[[players[playerNum].betView chipPileAtRow:0 col:BJ_SPLIT_HAND]
		addPile:[players[playerNum].betView chipPileAtRow:0 col:BJ_REGULAR_HAND]];
	[players[playerNum].betView display];
	
	[players[playerNum].player removeChip:prevAmount];
	
	[self drawCardForPlayer:playerNum forHand:BJ_REGULAR_HAND faceUp:YES];
	[self drawCardForPlayer:playerNum forHand:BJ_SPLIT_HAND faceUp:YES];
	
	if(splitAces)
	{
		[[players[playerNum].hand[BJ_REGULAR_HAND].pile cardAt:CS_BOTTOM] setUserValue:BJ_ACE_HIGH_VALUE];
		[[players[playerNum].hand[BJ_SPLIT_HAND].pile cardAt:CS_BOTTOM] setUserValue:BJ_ACE_HIGH_VALUE];
	}
	
	[self handTotalForPlayer:playerNum forHand:BJ_REGULAR_HAND];
	[self handTotalForPlayer:playerNum forHand:BJ_SPLIT_HAND];
	players[playerNum].numHands = 2;
		
	players[playerNum].hand[BJ_REGULAR_HAND].canSplit = NO;
	players[playerNum].hand[BJ_SPLIT_HAND].canSplit = NO;
	
	[self updateTable];
	
	[self checkCurrentPlayer];

	if(splitAces && canBetterHandAfterSplittingAces == NO) 
	// don't allow them to take any more cards
	{
		currentHand = BJ_SPLIT_HAND;
		[[self incrementHand] checkCurrentPlayer];
	}
	
	
	return self;
}

- (BOOL)playerWillClose:aPlayer
{
	
	
	return YES;
}

- (BOOL)playerWillJoin:sender
/*
	Don't allow more than BJ_MAXPLAYERS to play at once, or join when game is already
	in progress.
*/
{
	return YES;
}

- (BOOL)playerWillSplit:(int)playerNum
/*
	Make sure that player has enough money in his bank to cover this...
*/
{
	if([players[playerNum].betView amountAtRow:0 col:BJ_REGULAR_HAND] > [players[playerNum].player amountInBank])
	{
		[PBoss() playSound:NV_WARNSOUND];
		[self say:"You don't have enough money to split right now!"];
		return NO;
	}
	else
		return YES;
}

/***************************************************************************************
 *                                   Debug Methods                                     *
 ***************************************************************************************/

- runDebug
{
	char path[MAXPATHLEN+1];
	
	
	if(!dbWindow)
	{
		if ([bundle getPath:path forResource:"debug" ofType:"nib"]) 
		{
			[NXApp loadNibFile:path 
						 owner:self 
					 withNames:NO 
					  fromZone:[self zone]];
		}
		else
		{
			fprintf(stderr, "Couldn't load debug nib file!\n");
			return nil;
		}
	}
	
	[dbPnumText selectText:self];
	[dbWindow makeKeyAndOrderFront:self];
	
	return self;
}

- dbReplaceCard:sender
{
	int pnum, cnum, hnum;
	id  oldCard, topCard;
	
	pnum = [dbPnumText intValue];
	if(pnum < 0 || pnum > BJ_MAXPLAYERS) return nil;
	
	hnum = [dbHnumText intValue];
	if(hnum < 0 || hnum > players[pnum].numHands) return nil;
	
	cnum = [dbCnumText intValue];
	if(cnum < 0 || cnum > [players[pnum].hand[hnum].pile cardCount]-1) return nil;
	
	oldCard = [players[pnum].hand[hnum].pile cardAt:cnum];
	topCard = [drawPile cardAt:CS_TOP];
		
	[drawPile removeCard:topCard];
	[drawView display];
	[players[pnum].hand[hnum].pile insertCard:topCard at:cnum];
	
	[players[pnum].hand[hnum].pile removeCard:oldCard];
	
	[topCard setFaceUp:YES];
		
	[players[pnum].hand[hnum].view display];
	NXPing();  // attempt to synch sound with graphics...
	[drawSound play:1.0 pan:0.0];
	
	[self handTotalForPlayer:pnum forHand:hnum];
	[self updateTable];
	NXPing();
		
	return self;
}

@end

/***************************************************************************************
 *                              BetView delegate Methods                               *
 ***************************************************************************************/
@implementation Blackjack(BetViewDelegate)
- mouseEnteredView:sender
{
	return self;
}

- mouseExitedView:sender
{
	return self;
}

- player:aPlayer willBet:(int)betType onView:sender
{
	int amount, prevAmount, tag;
	id bettingPlayer;
	char buf[128];
	
	tag = [sender tag];
	
	bettingPlayer = players[tag].player;
	
	if(bettingPlayer != nil)
	{
		amount = [bettingPlayer selectedChip];
		
		prevAmount = [sender amountAtRow:0 col:BJ_REGULAR_HAND];

		switch(betType)
		{
			case NV_BET:
				if(amount > [bettingPlayer amountInBank]) 
				{
					NXBeep();
					[self say:"You can't wager more than you have!"];
					return nil;
				}
				if(prevAmount+amount > [self tableMax])
				{
					NXBeep();
					[self say:"That bet would put you over the table max!"];
					return nil;
				}
				if([bettingPlayer removeChip:amount])
				{
					[sender bet:amount atRow:0 col:BJ_REGULAR_HAND];
					sprintf(buf, "Dealer accepts %s's $%d %s.", 
						[bettingPlayer playerName], prevAmount+amount, 
						([sender viewType] == BJ_BETVIEW) ? "wager" : "insurance wager");
					[self say:buf];
				}
				break;
			
			case NV_REMOVEBET:
				if(prevAmount == 0) return nil;
				if(amount > prevAmount) 
				{
					amount = prevAmount;
				}

				[sender removeBet:amount atRow:0 col:BJ_REGULAR_HAND];
				[bettingPlayer addChip:amount];
				if(prevAmount-amount > 0)
				{
					sprintf(buf, "Dealer accepts %s's $%d %s.", 
						[bettingPlayer playerName], prevAmount-amount, 
						([sender viewType] == BJ_BETVIEW) ? "wager" : "insurance wager");
					[self say:buf];
				}
				else
					[self say:""];
				break;
				
			case NV_REMOVEALL:
				if(prevAmount == 0) return nil;
				
				amount = prevAmount;
				
				[sender removeBet:amount atRow:0 col:BJ_REGULAR_HAND];
				[bettingPlayer addChip:amount];
				if(prevAmount-amount > 0)
				{
					sprintf(buf, "Dealer accepts %s's $%d %s.", 
						[bettingPlayer playerName], prevAmount-amount, 
						([sender viewType] == BJ_BETVIEW) ? "wager" : "insurance wager");
					[self say:buf];
				}
				else
					[self say:""];
				break;
			
			default:
				NXBeep();
				[self say:"That action is not allowed in this game..."];
				break;
		}
	}
	
	[self enableDealButton];
	
	return self;
}

- (BOOL)playerWillDragChipAtRow:(int)row andCol:(int)col fromView:sender
{
	return YES;
}

- playerDraggedChipAtRow:(int)row andCol:(int)col fromView:sender
{
	[self enableDealButton];
	
	return self;
}

- (BOOL)playerWillDropChipAtRow:(int *)row andCol:(int *)col InView:sender;
{	
	// Allow a player to drop a chip on a betview only if there is a player 
	// playing at that position
	if(players[[sender tag]].player)
		return YES;
		
	return NO;
}

- playerDroppedChipAtRow:(int)row andCol:(int)col inView:sender
{
	[self enableDealButton];
	
	return self;
}

@end

These are the contents of the former NiCE NeXT User Group NeXTSTEP/OpenStep software archive, currently hosted by Netfuture.ch.