/*
File: MBCGameInfo.mm
Contains: Managing information about the current game
Version: 1.0
Copyright: © 2003 by Apple Computer, Inc., all rights reserved.
File Ownership:
DRI: Matthias Neeracher x43683
Writers:
(MN) Matthias Neeracher
Change History (most recent first):
$Log: MBCGameInfo.mm,v $
Revision 1.13 2009/04/22 23:19:47 neerache
<rdar://problem/6815838> Chess crashes when editing Game Info from Game Log window
Revision 1.12 2008/10/24 22:45:45 neerache
<rdar://problem/5844722> Chess: black may illegally move first in new game
Revision 1.11 2008/08/19 20:24:28 neerache
<rdar://problem/6159904> 10A149: Chess Crashes using Foundation-706
Revision 1.10 2008/04/22 18:40:55 neerache
Merge late Leopard changes into trunk
Revision 1.9.2.1 2007/05/18 20:36:37 neerache
Properly hook up board to game info <rdar://problem/3852844>
Revision 1.9 2007/03/02 07:40:46 neerache
Revise document handling & saving <rdar://problems/3776337&4186113>
Revision 1.8 2007/01/17 05:40:45 neerache
Defer title updates <rdar://problem/3852824>
Revision 1.7 2007/01/16 08:28:45 neerache
Don't mess with nil strings
Revision 1.6 2006/05/19 21:09:32 neerache
Fix 64 bit compilation errors
Revision 1.5 2003/07/25 22:05:21 neerache
Dismiss edit window properly (RADAR 3343292)
Revision 1.4 2003/07/03 05:35:28 neerache
Add tooltips, tweak game info window
Revision 1.3 2003/06/12 07:27:10 neerache
Reorganize preferences window, Add simpler title style
Revision 1.2 2003/05/27 03:13:57 neerache
Rework game loading/saving code
Revision 1.1 2003/05/24 20:29:25 neerache
Add Game Info Window
*/
#import "MBCGameInfo.h"
#import "MBCController.h"
#import "MBCPlayer.h"
#include <sys/types.h>
#include <regex.h>
#include <algorithm>
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];
strlcpy(n, [fullName UTF8String], 100);
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]];
fRows = 0;
fBoard = [[MBCController controller] board];
}
- (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
{
[NSObject cancelPreviousPerformRequestsWithTarget:self];
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;
} 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 * human;
fWhiteName = [[fWhite stringValue] retain];
fBlackName = [[fBlack stringValue] retain];
if (fHuman == kWhiteSide)
human = fWhiteName;
else if (fHuman == kBlackSide)
human = fBlackName;
else
human = nil; // Both or neither are humans, no default to learn here
if (human) {
NSString * humanFirst;
NSString * humanLast;
[MBCGameInfo parseName:human
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 performSelector:@selector(updateTitle:) withObject:nil
afterDelay:0.01];
fTitleNeedsUpdate = false;
}
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];
if (!move)
return nil;
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: