MBCGameInfo.mm   [plain text]


/*
	File:		MBCGameInfo.mm
	Contains:	Managing information about the current game
	Copyright:	© 2002-2003 Apple Computer, Inc. All rights reserved.

	IMPORTANT: This Apple software is supplied to you by Apple Computer,
	Inc.  ("Apple") in consideration of your agreement to the following
	terms, and your use, installation, modification or redistribution of
	this Apple software constitutes acceptance of these terms.  If you do
	not agree with these terms, please do not use, install, modify or
	redistribute this Apple software.
	
	In consideration of your agreement to abide by the following terms,
	and subject to these terms, Apple grants you a personal, non-exclusive
	license, under Apple's copyrights in this original Apple software (the
	"Apple Software"), to use, reproduce, modify and redistribute the
	Apple Software, with or without modifications, in source and/or binary
	forms; provided that if you redistribute the Apple Software in its
	entirety and without modifications, you must retain this notice and
	the following text and disclaimers in all such redistributions of the
	Apple Software.  Neither the name, trademarks, service marks or logos
	of Apple Computer, Inc. may be used to endorse or promote products
	derived from the Apple Software without specific prior written
	permission from Apple.  Except as expressly stated in this notice, no
	other rights or licenses, express or implied, are granted by Apple
	herein, including but not limited to any patent rights that may be
	infringed by your derivative works or by other works in which the
	Apple Software may be incorporated.
	
	The Apple Software is provided by Apple on an "AS IS" basis.  APPLE
	MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION
	THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND
	FITNESS FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS
	USE AND OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS.
	
	IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT,
	INCIDENTAL OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
	PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
	PROFITS; OR BUSINESS INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE,
	REPRODUCTION, MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE,
	HOWEVER CAUSED AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING
	NEGLIGENCE), STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN
	ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

#import "MBCGameInfo.h"
#import "MBCController.h"
#import "MBCPlayer.h"

#include <sys/types.h>
#include <regex.h>
#include <algorithm>

using std::min;

static NSTextTab * MakeTab(NSTextTabType type, float location)
{
	return [[[NSTextTab alloc] initWithType:type location:location] 
			   autorelease];
}

@implementation MBCGameInfo

NSString * kMBCGameCity			= @"MBCGameCity";
NSString * kMBCGameCountry		= @"MBCGameCountry";
NSString * kMBCHumanFirst		= @"MBCHumanFirst";
NSString * kMBCHumanLast		= @"MBCHumanLast";
NSString * kMBCGameEvent		= @"MBCGameEvent";
NSString * kMBCShowMoveInTitle 	= @"MBCShowMoveInTitle";

+ (void) parseName:(NSString *)fullName intoFirst:(NSString **)firstName
			  last:(NSString **)lastName
{
	// 
	// Get name as UTF8. If the name is longer than 99 bytes, the last
	// character might be bad if it's non-ASCII. What sane person would
	// have such a long name anyway?
	//
	char	   n[100];
	NSData * d = [fullName dataUsingEncoding:NSUTF8StringEncoding];
	[d getBytes:n length:99];
	n[min(99u, [d length])] = 0;
	
	char * first 	= n+strspn(n, " \t"); 	// Beginning of first name
	char * last;
	char * nb1  	= NULL;			 		// Beginning of last word
	char * ne1  	= NULL;			 		// End of last word
	char * nb2  	= NULL;			 		// Beginning of last but one word
	char * ne2  	= NULL;			 		// End of last but two word
	char * ne3  	= NULL;            		// End of last but three word

	nb1	  = first;
	ne1   = nb1+strcspn(nb1, " \t");

	for (char * n; (n = ne1+strspn(ne1, " \t")) && *n; ) {
		ne3	= ne2; 
		nb2 = nb1;
		ne2 = ne1;
		nb1	= n;
		ne1 = nb1+strcspn(nb1, " \t");
	}

	if (ne3 && *nb2 >= 'a' && *nb2 <= 'z') { 
		//
		// Name has at least 3 words and last but one is 
		// lowercase, as in Ludwig van Beethoven
		//
		last = nb2;
		*ne3 = 0;
	} else if (ne2) {
		//
		// Name has at least two words. If 3 or more, last but one is 
		// uppercase as in John Wayne Miller
		//
		last = nb1;
		*ne2 = 0;
	} else {		// Name is single word
		last = ne1;
		*ne1 = 0;
	}
	*firstName	= [NSString stringWithUTF8String:first];
	*lastName	= [NSString stringWithUTF8String:last];
}

+ (void)initialize
{
	//
	// Parse the user name into last and first name
	//
	NSString * humanFirst;
	NSString * humanLast;

	[MBCGameInfo parseName:NSFullUserName() 
				 intoFirst:&humanFirst last:&humanLast];

	// 
	// Get the city we might be in. This technique may not necessarily
	// work in future revisions of Mac OS X, I suppose.
	//
	// PGN wants IOC codes for countries, which we're too lazy to convert.
	//
	NSArray *			cityInfo = 
		(NSArray *)CFPreferencesCopyValue((CFStringRef)
					@"com.apple.TimeZonePref.Last_Selected_City",
					kCFPreferencesAnyApplication, kCFPreferencesAnyUser,
					kCFPreferencesCurrentHost);
	NSString *			city 	= cityInfo ? [cityInfo objectAtIndex:7] : @"?";
	NSString *			country	= cityInfo ? [cityInfo objectAtIndex:8] : @"?";
	
	//
	// PGN wants ISO Latin 1, so we fall back to the non-Localized Name 
	// for non-Latin places.
	//
	if (![city canBeConvertedToEncoding:NSISOLatin1StringEncoding])
		city	= [cityInfo objectAtIndex:5];
	if (![country canBeConvertedToEncoding:NSISOLatin1StringEncoding])
		country	= [cityInfo objectAtIndex:6];

	if (cityInfo)
		[cityInfo release];

	NSString * event = 
		[NSLocalizedString(@"casual_game", @"casual game") retain];

	NSDictionary * defaults = 
		[NSDictionary 
			dictionaryWithObjectsAndKeys:
				humanFirst, kMBCHumanFirst,
				 humanLast, kMBCHumanLast,
			          city, kMBCGameCity,
   			       country, kMBCGameCountry,
			         event, kMBCGameEvent,
			nil];
	[[NSUserDefaults standardUserDefaults] registerDefaults: defaults];
}


- (id) init
{
	[[NSNotificationCenter defaultCenter] 
		addObserver:self
		selector:@selector(updateMoves:)
		name:MBCEndMoveNotification
		object:nil];
	[[NSNotificationCenter defaultCenter] 
		addObserver:self
		selector:@selector(takeback:)
		name:MBCTakebackNotification
		object:nil];
	[[NSNotificationCenter defaultCenter] 
		addObserver:self
		selector:@selector(updateMoves:)
		name:MBCIllegalMoveNotification
		object:nil];
	[[NSNotificationCenter defaultCenter] 
		addObserver:self
		selector:@selector(gameEnd:)
		name:MBCGameEndNotification
		object:nil];

	fOutcome	= nil;
	fWhiteName	= nil;
	fBlackName	= nil;
	fSetInfo	= false;

	return self;
}

- (void)awakeFromNib
{
	NSUserDefaults *defaults 	= [NSUserDefaults standardUserDefaults];

	[fShowMoveInTitle setIntValue:[defaults boolForKey:kMBCShowMoveInTitle]];
}

- (void) updateMoves:(NSNotification *)notification
{
	[fMoveList reloadData];
	[fMoveList setNeedsDisplay:YES];
	fTitleNeedsUpdate = true;

	int rows = [self numberOfRowsInTableView:fMoveList]; 
	if (rows != fRows) {
		fRows = rows;
		[fMoveList selectRow:rows-1 byExtendingSelection:NO];
		[fMoveList scrollRowToVisible:rows-1];
	}
}

- (void) takeback:(NSNotification *)notification
{
	[fOutcome release];
	fOutcome 	= nil;
	fResult		= [@"*" retain];
	[self updateMoves:notification];
}

- (void) gameEnd:(NSNotification *)notification
{
	MBCMove *    move 	= reinterpret_cast<MBCMove *>([notification object]);

	switch (move->fCommand) {
	case kCmdWhiteWins:
		fResult		= @"1-0";
		fOutcome	= NSLocalizedString(@"white_win_msg", @"White wins");
		break;
	case kCmdBlackWins:
		fResult		= @"0-1";
		fOutcome 	= NSLocalizedString(@"black_win_msg", @"Black wins");
		break;
	case kCmdDraw:
		fResult		= @"1/2-1/2";
		fOutcome 	= NSLocalizedString(@"draw_msg", @"Draw");
		break;
	default:
		return;
	}
	[fResult retain];
	[fOutcome retain];
	[self updateTitle:nil];
}

- (NSDictionary *)getInfo
{
	NSUserDefaults *defaults 	= [NSUserDefaults standardUserDefaults];

	return [NSDictionary dictionaryWithObjectsAndKeys:
							 fWhiteName, @"White",
						 fBlackName, @"Black",
						 fStartDate, @"StartDate",
						 fStartTime, @"StartTime",
						 fResult, 	 @"Result",
						 [defaults stringForKey:kMBCGameCity], 	  @"City",
						 [defaults stringForKey:kMBCGameCountry], @"Country",
						 [defaults stringForKey:kMBCGameEvent],   @"Event",
						 nil];
}

- (void)setInfo:(NSDictionary *)dict
{
	NSUserDefaults *defaults 	= [NSUserDefaults standardUserDefaults];
	NSString * 		human		=
		[NSString stringWithFormat:@"%@ %@",
				  [defaults stringForKey:kMBCHumanFirst],
				  [defaults stringForKey:kMBCHumanLast]];
	NSString * 		engine 		= @"Computer";

	fSetInfo	= true;
	[fWhiteName release];
	[fBlackName release];
	if (NSString * white = [dict objectForKey:@"White"])
		fWhiteName 	= [white retain];
	else if ([[dict objectForKey:@"WhiteType"] isEqual:kMBCHumanPlayer])
		fWhiteName 	= [human retain];
	else
		fWhiteName 	= [engine retain];
	if (NSString * black = [dict objectForKey:@"Black"])
		fBlackName 	= [black retain];
	else if ([[dict objectForKey:@"BlackType"] isEqual:kMBCHumanPlayer])
		fBlackName 	= [human retain];
	else
		fBlackName 	= [engine retain];
	fStartDate = [[dict objectForKey:@"StartDate"] retain];
	fStartTime = [[dict objectForKey:@"StartTime"] retain];
	fResult	   = [[dict objectForKey:@"Result"] retain];
	if (NSString * city = [dict objectForKey:@"City"])
		[defaults setObject:city forKey:kMBCGameCity];
	if (NSString * country = [dict objectForKey:@"Country"])
		[defaults setObject:country forKey:kMBCGameCountry];
	if (NSString * event = [dict objectForKey:@"Event"])
		[defaults setObject:event forKey:kMBCGameEvent];
		
	[fOutcome release];
	fOutcome = nil;
	if ([fResult isEqual:@"1-0"])
		fOutcome	= NSLocalizedString(@"white_win_msg", @"White wins");
	if ([fResult isEqual:@"0-1"])
		fOutcome 	= NSLocalizedString(@"black_win_msg", @"Black wins");
	else if ([fResult isEqual:@"1/2-1/2"]) 
		fOutcome 	= NSLocalizedString(@"draw_msg", @"Draw");
	[fOutcome retain];
}

- (void) startGame:(MBCVariant)variant playing:(MBCSide)sideToPlay
{
	if (!fSetInfo) {
		NSUserDefaults *defaults 	= [NSUserDefaults standardUserDefaults];
		NSString * 		human		=
			[NSString stringWithFormat:@"%@ %@",
					  [defaults stringForKey:kMBCHumanFirst],
					  [defaults stringForKey:kMBCHumanLast]];
		NSString * 		engine 		= @"Computer";

		[fWhiteName release];
		[fBlackName release];
		switch (fHuman = fSideToPlay = sideToPlay) {
		case kWhiteSide:
			fWhiteName 	= [human retain];
			fBlackName 	= [engine retain];
			break;
		case kBlackSide:
			fWhiteName 	= [engine retain];
			fBlackName 	= [human retain];
			break;
		case kBothSides:
			fWhiteName 	= [human retain];
			fBlackName 	= [human retain];
			fHuman	   	= kWhiteSide;
			break;
		case kNeitherSide:
			fWhiteName 	= [engine retain];
			fBlackName 	= [engine retain];
			break;
		}
		NSDate * now	= [NSDate date];
		fStartDate		= [[now descriptionWithCalendarFormat:@"%Y.%m.%d"
								timeZone:nil locale:nil] retain];
		fStartTime		= [[now descriptionWithCalendarFormat:@"%H:%M:%S"
								timeZone:nil locale:nil] retain];
		fResult			= [@"*" retain];
		[fOutcome release];
		fOutcome = nil;

		fRows	= 0;
		fBoard	= [[MBCController controller] board];
	} else
		fSetInfo	= false;

	[self updateMoves:nil];
	[self updateTitle:nil];
}

- (IBAction) editInfo:(id)sender
{
	NSUserDefaults * 	defaults 	= [NSUserDefaults standardUserDefaults];
	[fWhite setStringValue:fWhiteName];
	[fBlack setStringValue:fBlackName];
	[fCity setStringValue:[defaults stringForKey:kMBCGameCity]];
	[fCountry setStringValue:[defaults stringForKey:kMBCGameCountry]];
	[fEvent setStringValue:[defaults stringForKey:kMBCGameEvent]];

	switch (fSideToPlay) {
	case kWhiteSide:
		[fWhite setEditable:YES];
		[fBlack setSelectable:NO];
		break;
	case kBlackSide:
		[fWhite setSelectable:NO];
		[fBlack setEditable:YES];
		break;
	case kBothSides:
		[fWhite setEditable:YES];
		[fBlack setEditable:YES];
		break;
	case kNeitherSide:
		[fWhite setSelectable:NO];
		[fBlack setSelectable:NO];
		break;
	}

	[NSApp beginSheet:fEditSheet
		   modalForWindow:fInfoWindow
		   modalDelegate:nil
		   didEndSelector:nil
		   contextInfo:nil];
    [NSApp runModalForWindow:fEditSheet];
	[NSApp endSheet:fEditSheet];
	[fEditSheet orderOut:self];
	[fInfoWindow makeKeyAndOrderFront:self];
}

- (IBAction) cancelInfo:(id)sender
{
	[NSApp stopModal];
}

- (IBAction) updateInfo:(id)sender
{
	NSUserDefaults * 	defaults 	= [NSUserDefaults standardUserDefaults];
	NSString *			humanFirst;
	NSString *			humanLast;

	fWhiteName = [[fWhite stringValue] retain];
	fBlackName = [[fBlack stringValue] retain];
	if (fHuman == kWhiteSide) {
		[MBCGameInfo parseName:fWhiteName 
					 intoFirst:&humanFirst last:&humanLast];
	} else if (fHuman == kBlackSide) {
		[MBCGameInfo parseName:fBlackName
					 intoFirst:&humanFirst last:&humanLast];
	}
	[defaults setObject:humanFirst forKey:kMBCHumanFirst];
	[defaults setObject:humanLast forKey:kMBCHumanLast];
	[defaults setObject:[fCity stringValue] forKey:kMBCGameCity];
	[defaults setObject:[fCountry stringValue] forKey:kMBCGameCountry];
	[defaults setObject:[fEvent stringValue] forKey:kMBCGameEvent];
	[self updateTitle:nil];
	[NSApp stopModal];
}

- (int)numberOfRowsInTableView:(NSTableView *)aTableView
{
	return ([fBoard numMoves]+1) / 2;
}

- (id)tableView:(NSTableView *)v objectValueForTableColumn:(NSTableColumn *)col row:(int)row
{
	//
	// Defer title update until table gets redrawn
	//
	if (fTitleNeedsUpdate) 
		[self updateTitle:nil];

	NSString * 		ident 	= [col identifier];
	if ([ident isEqual:@"Move"])
		return [NSString stringWithFormat:@"%d.", row+1];
	
	NSString * move = [[fBoard move: row*2+[ident isEqual:@"Black"]] 
						  localizedText:YES];
	NSMutableParagraphStyle * style = 
		[[[NSParagraphStyle defaultParagraphStyle] mutableCopy] autorelease];
	float tab   = [col width] / 14.0f;
	[style setTabStops:
			   [NSArray arrayWithObjects:
							[move characterAtIndex:1]=='\t' 
						? MakeTab(NSRightTabStopType, 1.0f) 
						: MakeTab(NSRightTabStopType, 5.0f*tab),
						MakeTab(NSCenterTabStopType, 6.0f*tab),
						MakeTab(NSLeftTabStopType, 7.0f*tab),
						nil]];
	return [[[NSAttributedString alloc]
				initWithString:move attributes:
					[NSDictionary dictionaryWithObject:style
								  forKey:NSParagraphStyleAttributeName]]
			   autorelease];
}

- (IBAction) updateTitle:(id)sender
{
	NSString * 		move;
	int		 		numMoves = [fBoard numMoves];		

	if (sender) {
		NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];

		[defaults setBool:
					  [fShowMoveInTitle intValue] forKey:kMBCShowMoveInTitle];
	}
	
	if (numMoves && [fShowMoveInTitle intValue])
		move = 	[NSString stringWithFormat:@"%d. %@%@",
						  (numMoves+1)/2, numMoves&1 ? @"":@"... ",
						  [[fBoard lastMove] localizedText:NO]];
	else if (!numMoves)
		move =	NSLocalizedString(@"new_msg", @"New Game");
	else if (numMoves & 1)
		move = 	NSLocalizedString(@"black_move_msg", @"Black to move");
	else
		move = 	NSLocalizedString(@"white_move_msg", @"White to move");

	NSString * title =
		[NSString stringWithFormat:@"%@ - %@   (%@)", 
				  fWhiteName, fBlackName, move];
	if (fOutcome)
		title = [NSString stringWithFormat:@"%@   %@", title, fOutcome];
	[fMainWindow setTitle:title];
	unichar emdash = 0x2014;
	[fMatchup setStringValue:
				  [NSString stringWithFormat:@"%@\n%@\n%@", 
							fWhiteName, 
							[NSString stringWithCharacters:&emdash length:1],
							fBlackName]];
	fTitleNeedsUpdate = false;
}

- (NSSize)windowWillResize:(NSWindow *)sender toSize:(NSSize)proposedFrameSize
{
	[fMoveList setNeedsDisplay:YES];

	return proposedFrameSize;
}

- (NSString *)pgnHeader
{
	NSUserDefaults *defaults 	= [NSUserDefaults standardUserDefaults];
	NSString * 		wf;
	NSString *		wl;
	NSString *		bf;
	NSString * 		bl;
	[MBCGameInfo parseName:fWhiteName intoFirst:&wf last:&wl];
	[MBCGameInfo parseName:fBlackName intoFirst:&bf last:&bl];
	NSString *		humanw	=
		[NSString stringWithFormat:@"%@, %@", wl, wf];
	NSString *		humanb	=
		[NSString stringWithFormat:@"%@, %@", bl, bf];
	NSString * 		engine 		= 
		[NSString stringWithFormat:@"Apple Chess %@",
				  [[NSBundle mainBundle] 
					  objectForInfoDictionaryKey:@"CFBundleVersion"]];
	NSString * white;
	NSString * black;
	switch (fSideToPlay) {
	case kWhiteSide:
		white 	= humanw;
		black 	= engine;
		break;
	case kBlackSide:
		white 	= engine;
		black	= humanb;
		break;
	case kBothSides:
		white	= humanw;
		black	= humanb;
		
		break;
	case kNeitherSide:
		white	= engine;
		black	= engine;
		break;
	}

	//
	// PGN uses a standard format that is NOT localized
	//
	NSString * format = 
		[NSString stringWithCString:
				  "[Event \"%@\"]\n"
				  "[Site \"%@, %@\"]\n"
				  "[Date \"%@\"]\n"
				  "[Round \"-\"]\n"
				  "[White \"%@\"]\n"
				  "[Black \"%@\"]\n"
				  "[Result \"%@\"]\n"
				  "[Time \"%@\"]\n"];
	return [NSString stringWithFormat:format,
					 [defaults stringForKey:kMBCGameEvent],
					 [defaults stringForKey:kMBCGameCity],
					 [defaults stringForKey:kMBCGameCountry],
					 fStartDate, white, black, fResult, fStartTime];
}

- (NSString *)pgnResult
{
	return fResult;
}

@end

// Local Variables:
// mode:ObjC
// End: