//-----------------------------------------------------------------------------
// SokoBoard.M
//
//	Object representing a single puzzle instance.
//
// Copyright (c), 1997, Paul McCarthy.  All rights reserved.
//-----------------------------------------------------------------------------
//-----------------------------------------------------------------------------
// $Id: SokoBoard.M,v 1.2 97/12/10 07:05:02 sunshine Exp $
// $Log:	SokoBoard.M,v $
//  Revision 1.2  97/12/10  07:05:02  sunshine
//  v10.1: Ported to OPENSTEP 4.1 for Mach, OPENSTEP 4.2 for Mach & Windows,
//  and Rhapsody Developer Release (RDR) for Mach and Windows (Yellow Box).
//  Fixed bug: +nextMaze: was neglecting to send -makeKeyWindow to maze.
//  Fixed bug: When opening a saved game with a deferred window, the NSSlider
//  did not get drawn.  Was incorrectly & unnecessarily sending it a -sizeToFit
//  message.  For some reason, on deferred windows, this has the side-effect of
//  setting the slider's frame size to (0,0).
//  Fixed bug: New & Open panels failed to restrict allowed types to .sokomaze
//  and .sokosave, respectively.  Problem was they were using the inherited
//  -setRequiredFileType: method which seems to be ineffectual in NSOpenPanel.
//  Now explicitly uses -runModalForDirectory:file:types:.
//  Fixed bug: Opening a saved game with no history resulted in a "corrupt
//  moves/pushes" error message.  Problem was that an earlier fscanf() format
//  string was eating the blank line which represented the empty history.
//  Fixed bug: Opening a saved game with no history resulted in malloc(0).
//  Fixed bug: Opening a saved game would report "corrupt recorded values"
//  error in this case: 1) solve and save game, 2) undo two or more moves,
//  3) make a move which is not in the history, meaning no auto-redo, 4) save
//  game, 5) close game, 6) re-open game.  The problem was that at step 3, the
//  history was truncated.  Yet the load method was asserting that the
//  "recorded" moves/pushes had to be less than the history-length; which,
//  after truncation, is an invalid assertion.
//  Respects new "auto-save when solved" preference flag.
//  
//  Revision 1.1  97/11/13  02:56:29  zarnuk
//  v9
//-----------------------------------------------------------------------------
#import "SokoBoard.h"
#import "SokoDefs.h"
#import	"SokoEncode.h"
#import	"SokoFiles.h"
#import	"SokoMatrix.h"
#import "SokoMouse.h"
#import "SokoPref.h"
#import "SokoScore.h"
extern "Objective-C" {
#import <AppKit/NSApplication.h>
#import <AppKit/NSBox.h>
#import <AppKit/NSButton.h>
#import <AppKit/NSButtonCell.h>
#import <AppKit/NSImage.h>
#import <AppKit/NSMenu.h>
#import <AppKit/NSNibLoading.h>
#import <AppKit/NSOpenPanel.h>
#import <AppKit/NSSlider.h>
#import <AppKit/NSTextField.h>
#import <Foundation/NSRunLoop.h>
#import <Foundation/NSScanner.h>
}

// Characters from input file.		// ASCII
#define	EMPTY_CHAR		' '	// 32	0010 0000
#define	WALL_CHAR		'#'	// 35	0010 0011
#define	CRATE_CHAR		'$'	// 36	0010 0100
#define	SAFE_CRATE_CHAR		'*'	// 42	0010 1010
#define	SAFE_EMPTY_CHAR		'.'	// 46	0010 1110
#define	PLAYER_CHAR		'@'	// 64	0100 0000
#define	SAFE_PLAYER_CHAR	'^'	// 94	0101 1110

// Internal representation.
#define SAFE_BIT	0x01
#define	PLAYER_BIT	0x02
#define	CRATE_BIT	0x04
#define	WALL_BIT	0x08
#define	ILLEGAL_BIT	0x10
#define	PRINTABLE_BIT	0x40	// Must be greater than any digit.

#define	EMPTY_CH	((char)(0))
#define	SAFE_EMPTY_CH	((char)(SAFE_BIT))
#define	PLAYER_CH	((char)(PLAYER_BIT))
#define	SAFE_PLAYER_CH	((char)(PLAYER_BIT|SAFE_BIT))
#define	CRATE_CH	((char)(CRATE_BIT))
#define	SAFE_CRATE_CH	((char)(CRATE_BIT|SAFE_BIT))
#define	WALL_CH		((char)(WALL_BIT))
#define	ILLEGAL_CH	((char)(ILLEGAL_BIT))

#define	IS_SAFE(x)	((x) & (SAFE_BIT))
#define	IS_EMPTY(x)	(((x) & (PLAYER_BIT|CRATE_BIT|WALL_BIT)) == 0)
#define	IS_PLAYER(x)	((x) & (PLAYER_BIT))
#define	IS_CRATE(x)	((x) & (CRATE_BIT))
#define	IS_ILLEGAL(x)	((x) & (ILLEGAL_BIT))

int const NUM_IMAGES = 9;

static NSString* IMAGE_NAME_FOR_CH[ NUM_IMAGES ] =
	{
	@"Empty",	// 0000
	@"EmptySafe",	// 0001
	@"Player",	// 0010
	@"PlayerSafe",	// 0011
	@"Crate",	// 0100
	@"CrateSafe",	// 0101
	0,		// 0110
	0,		// 0111
	@"Wall",	// 1000
	};

static NSImage* IMAGE_FOR_CH[ NUM_IMAGES ];

inline static NSImage* image_for_ch( int ch ) { return IMAGE_FOR_CH[ch]; }

static char translate( char input_ch )
    {
    char ch;
    switch (input_ch)
	{
	case EMPTY_CHAR:	ch = EMPTY_CH;		break;
	case WALL_CHAR:		ch = WALL_CH;		break;
	case CRATE_CHAR:	ch = CRATE_CH;		break;
	case SAFE_CRATE_CHAR:	ch = SAFE_CRATE_CH;	break;
	case SAFE_EMPTY_CHAR:	ch = SAFE_EMPTY_CH;	break;
	case PLAYER_CHAR:	ch = PLAYER_CH;		break;
	case SAFE_PLAYER_CHAR:	ch = SAFE_PLAYER_CH;	break;
	default:		ch = ILLEGAL_CH;	break;
	}
    return ch;
    }

#define	GOOD_ROW(r)	((0 <= (r)) && ((r) < NUMROWS))
#define	GOOD_COL(c)	((0 <= (c)) && ((c) < NUMCOLS))

struct Coord
    {
    int row;
    int col;
    };

enum Direction
    {
    DIR_UP,
    DIR_LEFT,
    DIR_RIGHT,
    DIR_DOWN
    };

#define	DIR_FIRST	DIR_UP
#define	DIR_LAST	DIR_DOWN
#define	DIR_MAX		((Direction)(DIR_LAST + 1))
#define GOOD_DIR(x)	(((unsigned int)(x)) <= ((unsigned int)DIR_LAST))

static int	DirDeltaRow[DIR_MAX]	= { -1,  0,  0,  1 };
static int	DirDeltaCol[DIR_MAX]	= {  0, -1,  1,  0 };
//static Coord	DirDeltaCoord[DIR_MAX]	= {{-1,0},{0,-1},{0,1},{1,0}};
static Direction DirOpposite[DIR_MAX]	= {DIR_DOWN,DIR_RIGHT,DIR_LEFT,DIR_UP};

#define	MOVE_UP_CH	'u'
#define	MOVE_LEFT_CH	'l'
#define	MOVE_RIGHT_CH	'r'
#define	MOVE_DOWN_CH	'd'
#define	PUSH_UP_CH	'U'
#define	PUSH_LEFT_CH	'L'
#define	PUSH_RIGHT_CH	'R'
#define	PUSH_DOWN_CH	'D'
#define	IS_PUSH(x)	(isupper(x))
#define	IS_MOVE(x)	(islower(x))

static char const MOVE_CHARS[] = "ulrd";
static char const PUSH_CHARS[] = "ULRD";
static char const HISTORY_CHARS[] = "ulrdULRD";

enum ReachableMark
    {
    RCH_UNKNOWN,
    RCH_UNREACHABLE,
    RCH_REACHABLE
    };

static NSMutableArray* openBoards = 0;
static NSString* const NEW_SCORE_NOTIFICATION = @"SokoNewScoreNotification";

int const CELL_HEIGHT	= 16;
int const CELL_WIDTH	= 16;

int const SAVE_FILE_VERSION = 1;


//-----------------------------------------------------------------------------
// ch_for_move
//-----------------------------------------------------------------------------
static char ch_for_move( Direction dir, BOOL isPush )
    {
    NSCParameterAssert( GOOD_DIR(dir) );
    return (isPush ? PUSH_CHARS[dir] : MOVE_CHARS[dir]);
    }


//-----------------------------------------------------------------------------
// dir_from_move
//-----------------------------------------------------------------------------
static Direction dir_from_move( char move )
    {
    Direction dir = DIR_UP;
    switch (move)
	{
	case MOVE_UP_CH:	dir = DIR_UP;		break;
	case MOVE_LEFT_CH:	dir = DIR_LEFT;		break;
	case MOVE_RIGHT_CH:	dir = DIR_RIGHT;	break;
	case MOVE_DOWN_CH:	dir = DIR_DOWN;		break;
	case PUSH_UP_CH:	dir = DIR_UP;		break;
	case PUSH_LEFT_CH:	dir = DIR_LEFT;		break;
	case PUSH_RIGHT_CH:	dir = DIR_RIGHT;	break;
	case PUSH_DOWN_CH:	dir = DIR_DOWN;		break;
	default:
	    NSLog( @"Internal error: dir_from_move('%c') bad move.\n", move );
	    exit(3);
	}
    return dir;
    }


//-----------------------------------------------------------------------------
// eat_newline
//-----------------------------------------------------------------------------
static BOOL eat_newline( FILE* stream )
    {
    int c;
    do { c = getc( stream ); } while (c != EOF && c != '\n');
    return (c == '\n');
    }


//=============================================================================
// IMPLEMENTATION
//=============================================================================
@implementation SokoBoard

- (NSString*) saveFileName	{ return saveFileName; }

//-----------------------------------------------------------------------------
// +loadImages
//-----------------------------------------------------------------------------
+ (void)loadImages
    {
    for (int i = 0; i < NUM_IMAGES; i++)
	{
	NSImage* image = 0;
	NSString* name = IMAGE_NAME_FOR_CH[i];
	if (name != 0)
	    {
	    NSString* path = [[NSBundle bundleForClass:self]
		pathForImageResource:name];
	    if (path != 0)
		image = [[NSImage alloc] initWithContentsOfFile:path];
	    }
	IMAGE_FOR_CH[i] = image;
	}
    }


//-----------------------------------------------------------------------------
// +initialize
//-----------------------------------------------------------------------------
+ (void)initialize
    {
    if (openBoards == 0)
	{
	openBoards = [[NSMutableArray alloc] init];
	[self loadImages];
	}
    return;
    }


//-----------------------------------------------------------------------------
// +forgetBoard:
//-----------------------------------------------------------------------------
+ (void)forgetBoard:(SokoBoard*)brd
    {
    [openBoards removeObject:brd];
    }


//-----------------------------------------------------------------------------
// +findBoardWithSaveFileName:
//-----------------------------------------------------------------------------
+ (SokoBoard*)findBoardWithSaveFileName:(NSString*)name
    {
    unsigned int i,lim;
    lim = [openBoards count];
    for (i = 0;  i < lim;  i++)
	if ([[[openBoards objectAtIndex:i] saveFileName] isEqualToString:name])
	    return [openBoards objectAtIndex:i];
    return 0;
    }


//-----------------------------------------------------------------------------
// -dealloc
//-----------------------------------------------------------------------------
- (void)dealloc
    {
    [[NSNotificationCenter defaultCenter] removeObserver:0 name:0 object:self];
    [saveFileName release];
    [mazeFileName release];
    [window close];
    [window release];
    if (history != 0)
	free( history );
    [super dealloc];
    }


//-----------------------------------------------------------------------------
// -performPrint:
//-----------------------------------------------------------------------------
- (void)performPrint:(id)sender
    {
    [window print:self];
    }


//-----------------------------------------------------------------------------
// -recordNewLevel
//-----------------------------------------------------------------------------
- (void)recordNewLevel
    {
    NSScanner* scan = [NSScanner scannerWithString:filenamePart(mazeFileName)];
    int level;
    if ([scan scanInt:&level])
	if (level >= [SokoPref getLevel])
	    [SokoPref setLevel:level+1];
    }


//-----------------------------------------------------------------------------
// -recordNewScore:
//-----------------------------------------------------------------------------
- (void)recordNewScore:(NSNotification*)notification
    {
    [SokoScore	solved:filenamePart(mazeFileName)
		moves:recordedMoves
		pushes:recordedPushes];
    [window setDocumentEdited:YES];
    if ([SokoPref doesAutoSave])
	[self save:self];
    }


//-----------------------------------------------------------------------------
// -isSolved
//-----------------------------------------------------------------------------
- (BOOL)isSolved
    {
    int row,col;
    for (row = 0;  row < NUMROWS;  row++)
	for (col = 0;  col < NUMCOLS;  col++)
	    if (board[row][col] == CRATE_CH)
	    	return NO;
    return YES;
    }


//-----------------------------------------------------------------------------
// -solvedCheck
//-----------------------------------------------------------------------------
- (void)solvedCheck
    {
    if ([self isSolved])
	{
	[solvedLabel setStringValue:@"SOLVED"];
	if ((recordedMoves == 0) ||
	    (numMoves < recordedMoves) ||
	    (numPushes < recordedPushes))
	    {
	    if (recordedMoves == 0)
		[self recordNewLevel];
	    recordedMoves = numMoves;
	    recordedPushes = numPushes;
	    [[NSNotificationQueue defaultQueue] enqueueNotification:
		[NSNotification notificationWithName:NEW_SCORE_NOTIFICATION
		object:self] postingStyle:NSPostWhenIdle];
	    }
	}
    else
	[solvedLabel setStringValue:@""];
    }


//-----------------------------------------------------------------------------
// -solvedCheckDst:DDst:
//-----------------------------------------------------------------------------
- (void)solvedCheckDst:(char)dst DDst:(char)ddst
    {
    if ((dst == CRATE_CH && ddst == SAFE_EMPTY_CH) ||
	(ddst == SAFE_CRATE_CH && dst == PLAYER_CH))
	[self solvedCheck];
    }


//-----------------------------------------------------------------------------
// -canMove:
//-----------------------------------------------------------------------------
- (BOOL)canMove:(Direction)dir
    {
    int row,col,ch;
    row = playerRow + DirDeltaRow[dir];
    col = playerCol + DirDeltaCol[dir];
    ch = board[row][col];
    if (IS_EMPTY(ch))
	return YES;
    else if (IS_CRATE(ch))
	{
	row += DirDeltaRow[dir];
	col += DirDeltaCol[dir];
	ch = board[row][col];
	return IS_EMPTY(ch);
	}
    return NO;
    }


//-----------------------------------------------------------------------------
// -updatePlaybackSlider
//-----------------------------------------------------------------------------
- (void)updatePlaybackSlider
    {
    [playbackSlider setMaxValue:history_len];
    [playbackSlider setIntValue:history_pos];
    [playbackSlider setEnabled:(history_len > 0)];
    }


//-----------------------------------------------------------------------------
// -updateControls
//-----------------------------------------------------------------------------
- (void)updateControls
    {
    BOOL up,left,right,down,up_left,up_right,down_left,down_right;
    up_left	= IS_EMPTY(board[playerRow-1][playerCol-1]);
    up		= IS_EMPTY(board[playerRow-1][playerCol  ]);
    up_right	= IS_EMPTY(board[playerRow-1][playerCol+1]);
    left	= IS_EMPTY(board[playerRow  ][playerCol-1]);
    right	= IS_EMPTY(board[playerRow  ][playerCol+1]);
    down_left	= IS_EMPTY(board[playerRow+1][playerCol-1]);
    down	= IS_EMPTY(board[playerRow+1][playerCol  ]);
    down_right	= IS_EMPTY(board[playerRow+1][playerCol+1]);
    
    [moveUpBtn setEnabled:[self canMove:DIR_UP]];
    [moveLeftBtn setEnabled:[self canMove:DIR_LEFT]];
    [moveRightBtn setEnabled:[self canMove:DIR_RIGHT]];
    [moveDownBtn setEnabled:[self canMove:DIR_DOWN]];
    [moveUpLeftBtn setEnabled:up_left && (up || left)];
    [moveUpRightBtn setEnabled:up_right && (up || right)];
    [moveDownLeftBtn setEnabled:down_left && (down || left)];
    [moveDownRightBtn setEnabled:down_right && (down || right)];
    [undoBtn setEnabled:(history_pos > 0)];
    [redoBtn setEnabled:(history_pos < history_len)];
    [playbackSlider setEnabled:(history_len > 0)];
    }


//-----------------------------------------------------------------------------
// -updateAfterMoving
//-----------------------------------------------------------------------------
- (void)updateAfterMoving
    {
    [numMovesFld setIntValue:numMoves];
    [numPushesFld setIntValue:numPushes];
    [self updatePlaybackSlider];
    [self updateControls];
    }

//-----------------------------------------------------------------------------
// -appendMoveToHistory:isPush:
//-----------------------------------------------------------------------------
- (void)appendMoveToHistory:(Direction)dir isPush:(BOOL)isPush
    {
    char move_ch;
    BOOL changed = YES;
    move_ch = ch_for_move( dir, isPush );
    if (history_pos < history_len)
	{
	if (history[ history_pos ] == move_ch)
	    {
	    history_pos++;
	    changed = NO;
	    }
	else
	    {
	    history[ history_pos++ ] = move_ch;
	    history_len = history_pos;
	    }
	}
    else
	{
	if (history_len >= history_max)
	    {
	    if (history_max != 0)
		history_max <<= 1;
	    else
		history_max = 1024;
	    history = (char*)realloc( history, history_max );
	    if (history == 0)
		{
		NSLog( @"realloc() failed: %s\n", strerror(errno) );
		exit(3);
		}
	    }
	history[ history_pos++ ] = move_ch;
	history_len = history_pos;
	}
    
    numMoves++;
    if (isPush)
	numPushes++;
    
    if (!animating)
	[self updateAfterMoving];

    if (changed && ![window isDocumentEdited])
	[window setDocumentEdited:YES];
    }


//-----------------------------------------------------------------------------
// -shrinkBoard
//-----------------------------------------------------------------------------
- (void) shrinkBoard
    {
    int ch, row, first_row, last_row, col, first_col, last_col, non_blank_row;
    
    first_row = ROWMAX;
    first_col = COLMAX;
    last_row = -1;
    last_col = -1;
    
    for (row = 0;  row < ROWMAX;  row++)
	{
	non_blank_row = 0;
	for (col = 0;  col < COLMAX;  col++)
	    {
	    ch = board[row][col];
	    if (ch != ' ')
		{
		if (col < first_col)
		    first_col = col;
		if (col > last_col)
		    last_col = col;
		non_blank_row = 1;
		}
	    }
	if (non_blank_row)
	    {
	    if (row < first_row)
		first_row = row;
	    if (row > last_row)
		last_row = row;
	    }
	}
    
    NUMROWS = (last_row - first_row) + 1;
    NUMCOLS = (last_col - first_col) + 1;

    if (first_row > 0)
	memmove( board, board + (first_row * COLMAX), NUMROWS * COLMAX );

    if (first_col > 0)
	for (row = 0;  row < NUMROWS;  row++)
	    memmove( board[row], board[row] + first_col, NUMCOLS );
    }


//-----------------------------------------------------------------------------
// -makeMatrix
//-----------------------------------------------------------------------------
- (void)makeMatrix
    {
    NSButtonCell* protoCell;
    float windowWidth;
    float windowHeight;
    NSRect boxFrame;
    NSRect matrixFrame;
    NSSize intercell = NSZeroSize;
    
    protoCell = [[NSButtonCell allocWithZone:[self zone]]
	initImageCell:[NSImage imageNamed:@"Wall"]];
    [protoCell setEnabled:YES];
    [protoCell setBordered:NO];
    [protoCell setHighlightsBy:NSNoCellMask];
    [protoCell setShowsStateBy:NSNoCellMask];
    [protoCell setImagePosition:NSImageOnly];
    [protoCell setContinuous:YES];

    boxFrame = [controlBox frame];

    windowWidth = boxFrame.size.width;

    matrixFrame.size.width  = NUMCOLS * CELL_WIDTH;
    matrixFrame.size.height = NUMROWS * CELL_HEIGHT;
    matrixFrame.origin.y = boxFrame.size.height + 8;
    if (matrixFrame.size.width >= windowWidth)
	{
	matrixFrame.origin.x = 0;
	windowWidth = matrixFrame.size.width;
	}
    else
	{
	matrixFrame.origin.x = floor((windowWidth - matrixFrame.size.width)/2);
	}
    matrixFrame.origin.x += 4;
    windowWidth += 8;

    windowHeight = matrixFrame.size.height + boxFrame.size.height + 12;

    [window setContentSize:NSMakeSize(windowWidth, windowHeight)];

    [controlBox setFrameOrigin:
	NSMakePoint(floor((windowWidth - boxFrame.size.width) / 2), 4)];

    cellMatrix = [[SokoMatrix allocWithZone:[self zone]]
	initWithFrame:matrixFrame
	mode:NSRadioModeMatrix
	prototype:protoCell
	numberOfRows:NUMROWS
	numberOfColumns:NUMCOLS];

    [cellMatrix setIntercellSpacing:intercell];
    [cellMatrix setTarget:self];
    [[window contentView] addSubview:cellMatrix];
    [cellMatrix setNextKeyView:moveUpLeftBtn];
    [animateSwitch setNextKeyView:cellMatrix];
    }


//-----------------------------------------------------------------------------
// -validateBoard:
//		Make sure all cells are valid.
//		Find the player on the board.
//		Mark all unreachable cells as wall cells.
//		Make sure all border cells are wall cells.
//-----------------------------------------------------------------------------
- (BOOL)validateBoard:(BOOL)needTranslate
    {
    NSString* errmsg = @"";
    int found = 0;
    int row,col,ch;
    BoardArray map;
    Coord coord;
    Coord stack[ ROWMAX * COLMAX ];
    int stack_top;
    
    for (row = 0;  row < NUMROWS;  row++)
	for (col = 0;  col < NUMCOLS;  col++)
	    {
	    if (needTranslate)
		ch = translate(board[row][col]);
	    else
		ch = board[row][col];
	    if (IS_ILLEGAL(ch))
		{
		errmsg = @"Unrecognized characters in maze.";
		goto ERR_EXIT;
		}
	    else
		{
		board[row][col] = ch;
		map[row][col] = (ch == WALL_CH ? RCH_UNREACHABLE : RCH_UNKNOWN);
		if (IS_PLAYER(board[row][col]))
		    {
		    if (!found)
			{
			found = 1;
			playerRow = row;
			playerCol = col;
			}
		    else
			{
			errmsg = @"More than one player in maze.";
			goto ERR_EXIT;
			}
		    }
		}
	    }
    
    if (!found)
	{
	errmsg = @"Cannot find player in maze.";
	goto ERR_EXIT;
	}
    
    stack_top = 0;
    map[ playerRow ][ playerCol ] = RCH_REACHABLE;	// "mark"
    coord.row = playerRow;
    coord.col = playerCol;
    stack[ stack_top++ ] = coord;			// "push"
    while (stack_top > 0)
	{
	coord = stack[ --stack_top ];			// "pop"
	for (int dir = (int)DIR_FIRST;  dir <= (int)DIR_LAST;  dir++)
	    {
	    row = coord.row + DirDeltaRow[dir];
	    col = coord.col + DirDeltaCol[dir];
	    if (GOOD_ROW(row) && GOOD_COL(col) && map[row][col] == RCH_UNKNOWN)
		{
		map[row][col] = RCH_REACHABLE;		// "mark"
		stack[ stack_top ].row = row;
		stack[ stack_top ].col = col;
		stack_top++;				// "push"
		}
	    }
	}
    
    for (row = 0;  row < NUMROWS;  row++)
	for (col = 0;  col < NUMCOLS;  col++)
	    if (map[row][col] != RCH_REACHABLE)
		{
		ch = board[row][col];
		if (ch == EMPTY_CH)
		    {
		    board[row][col] = WALL_CH;
		    }
		else if (ch != WALL_CH)
		    {
		    if (IS_CRATE(ch))
			errmsg = @"Maze contains unreachable crates.";
		    else if (IS_SAFE(ch))
			errmsg = @"Maze contains unreachable safe squares.";
		    else
			errmsg = @"Internal: reachable error.";
		    goto ERR_EXIT;
		    }
		}
    
    for (row = 0;  row < NUMROWS;  row++)
	if (board[row][0] != WALL_CH || board[row][NUMCOLS-1] != WALL_CH)
	    {
	    errmsg = @"Maze is not closed";
	    goto ERR_EXIT;
	    }
    
    for (col = 0;  col < NUMCOLS;  col++)
	if (board[0][col] != WALL_CH || board[NUMROWS-1][0] != WALL_CH)
	    {
	    errmsg = @"Maze is not closed";
	    goto ERR_EXIT;
	    }
    
    return YES;
    
    ERR_EXIT:
	NSRunAlertPanel( @"Corrupt Maze", errmsg, @"OK", 0, 0 );
	return NO;
    }


//-----------------------------------------------------------------------------
// -loadMaze
//-----------------------------------------------------------------------------
- (BOOL)loadMaze
    {
    FILE* fp;
    BOOL retval = NO;
    
    errno = 0;
    NSString* const filename = expand( mazeFileName );
    if ((fp = fopen( [filename fileSystemRepresentation], "r" )) == 0)
	NSRunAlertPanel( @"Sorry", @"Cannot open maze file: \"%@\" [%s]",
		@"OK", 0, 0, filename, strerror(errno));
    else
	{
	int x = 0;
	int y = 0;
	int c;
	BOOL broken = NO;

	memset( board, ' ', sizeof(board) );

	while ((c = getc(fp)) != EOF)
	    {
	    if (c == '\n')
		{
		x = 0;
		if (y++ >= ROWMAX)
		    {
		    NSRunAlertPanel( @"Too Tall", @"Maze has too many rows",
			@"OK", 0, 0 );
		    broken = YES;
		    break;
		    }
		}
	    else
		{
		board[y][x] = (char)c;
		if (++x >= COLMAX)
		    {
		    NSRunAlertPanel( @"Too Wide", @"Maze has too many columns",
			@"OK", 0, 0 );
		    broken = YES;
		    break;
		    }
		}
	    }

	if (ferror(fp) || !feof(fp))
	    {
	    NSRunAlertPanel( @"Read Error", @"Error reading the maze file: %s",
		@"OK", 0, 0, strerror(errno) );
	    broken = YES;
	    }

	fclose(fp);

	if (!broken)
	    {
	    [self shrinkBoard];
	    [self makeMatrix];
	    retval = [self validateBoard:YES];
	    }
	}
    
    return retval;
    }


//-----------------------------------------------------------------------------
// -setCellMatrix
//-----------------------------------------------------------------------------
- (void)setCellMatrix
    {
    int row,col,ch;
    id cell;
    for (row = 0;  row < NUMROWS;  row++)
	for (col = 0;  col < NUMCOLS;  col++)
	    {
	    ch = board[row][col];
	    if (ch != WALL_CH)
		{
		cell = [cellMatrix cellAtRow:row column:col];
		[cell setImage:image_for_ch(ch)];
		[cell setEnabled:YES];
		}
	    }
    [cellMatrix setContinuous:YES];
    }


//-----------------------------------------------------------------------------
// -cascade
//-----------------------------------------------------------------------------
- (void)cascade
    {
    static int initialized = 0;
    static float base_top = 0;
    static float base_left = 200;
    static int cascadeCounter = -1;
    float top,left;

    if (!initialized)
	{
	initialized = 1;
	NSSize sz = [[NSScreen mainScreen] frame].size;
	base_top = sz.height - 100;
	}

    cascadeCounter++;
    if (cascadeCounter == 10)
	cascadeCounter = 0;

    top = base_top - (cascadeCounter * 20);
    left = base_left + (cascadeCounter * 20);
    [window setFrameTopLeftPoint:NSMakePoint(left, top)];
    [window setInitialFirstResponder:cellMatrix];
    [window orderFront:self];
    }


//-----------------------------------------------------------------------------
// -commonInit
//-----------------------------------------------------------------------------
- (void)commonInit
    {
    [super init];
    history = 0;
    [NSBundle loadNibNamed:@"SokoBoard" owner:self];
    [window setMiniwindowImage:[NSImage imageNamed:@"SokoSave"]];
    [[NSNotificationCenter defaultCenter] addObserver:self
	selector:@selector(recordNewScore:) name:NEW_SCORE_NOTIFICATION
	object:self];
    }


//-----------------------------------------------------------------------------
// -initMazeName:saveName:
//-----------------------------------------------------------------------------
- (id)initMazeName:(NSString*)mazeName saveName:(NSString*)saveName
    {
    [self commonInit];
    mazeFileName = [mazeName retain];
    saveFileName = [saveName retain];
    [window setTitleWithRepresentedFilename:saveFileName];

    if ([self loadMaze])
	{
	[self setCellMatrix];
	[self updateControls];
	[self cascade];
	}
    else
	{
	[self autorelease];		// Error exit.
	self = 0;
	}
    return self;
    }


//-----------------------------------------------------------------------------
// -validateHistory
//-----------------------------------------------------------------------------
- (BOOL)validateHistory
    {
    for (int i = 0; i < history_len; i++)
	if (strchr( HISTORY_CHARS, history[i] ) == 0)
	    {
	    NSRunAlertPanel( @"Corrupt",
		@"Illegal character in save file: 0x%02X '%c'.", @"OK", 0, 0,
		history[i], history[i] );
	    return NO;
	    }
    return YES;
    }


//-----------------------------------------------------------------------------
// -loadSaveFile
//-----------------------------------------------------------------------------
- (BOOL)loadSaveFile
    {
    NSString* errmsg = 0;
    BOOL retval = NO;
    int version, rc;
    char mazebuff[ FILENAME_MAX ];
    
    FILE* fp = fopen( [expand(saveFileName) fileSystemRepresentation], "r" );
    if (fp == 0)
	errmsg = @"Can't open save file.";
    else if (fscanf( fp, "%d", &version ) != 1)
	errmsg = @"Can't read file version.";
    else if (version < 0 || SAVE_FILE_VERSION < version)
	errmsg = @"Invalid file version.";
    else
	{
	rc = fscanf( fp, "%s\n%d %d %d",
		mazebuff, &history_max, &history_len, &history_pos );
	if (rc == 4 &&
	    0 <= history_pos &&
	    history_pos <= history_len &&
	    history_len <= history_max)
	    {
	    mazeFileName = [[NSString allocWithZone:[self zone]]
		initWithCString:mazebuff];
	    if (history_max > 0 && (history = (char*)malloc(history_max)) == 0)
		errmsg = @"Cannot allocate memory for history.";
	    else if (eat_newline(fp) &&
		runLengthDecodeString( fp, history, history_len ) == 0)
		errmsg = @"Cannot decode history.";
	    else
		{
		rc = fscanf( fp, "%d %d", &recordedMoves, &recordedPushes );
		if (rc != 2)
		    errmsg = @"Error reading recorded values.";
		else if (recordedMoves < 0 || recordedPushes < 0)
		    errmsg = @"Corrupted recorded values.";
		else
		    {
		    int nm, np;
		    rc = fscanf( fp, "%d %d", &nm, &np );
		    if (rc != 2)
			errmsg = @"Error reading number of moves/pushes.";
		    else if (nm < 0 || nm > history_len ||
				np < 0 || np > history_len)
			errmsg = @"Corrupted number of moves/pushes.";
		    else
			{
			numMoves = nm;
			numPushes = np;
			int nr, nc;
			rc = fscanf( fp, "%d %d", &nr, &nc );
			if (rc != 2)
			    errmsg = @"Error reading maze dimensions.";
			else if (nr < 0 || ROWMAX <= nr ||
				    nc < 0 || COLMAX <= nc)
			    errmsg = @"Corrupted maze dimensions.";
			else if (!eat_newline(fp))
			    errmsg = @"Maze missing.";
			else
			    {
			    NUMROWS = nr;
			    NUMCOLS = nc;
			    for (int r = 0; r < nr; r++)
				{
				if (runLengthDecodeString(fp,board[r],nc) == 0)
				    {
				    errmsg = @"Corrupted maze.";
				    break;
				    }
				for (int c = 0; c < nc; c++)
				    board[r][c] &= ~PRINTABLE_BIT;
				}
			    if (errmsg == 0)
				{
				[self makeMatrix];
				retval = [self validateBoard:NO];
				}
			    }
			}
		    }

		if (retval && errmsg == 0)
		    retval = [self validateHistory];
		}
	    }
	else
	    errmsg = @"Error reading file.";
	}
    
    if (fp != 0)
	fclose(fp);
    
    if (errmsg != 0)
	NSRunAlertPanel( @"Error", errmsg, @"OK", 0, 0 );
    
    return retval;
    }


//-----------------------------------------------------------------------------
// -initSaveName:
//-----------------------------------------------------------------------------
- (id)initSaveName:(NSString*)saveName
    {
    [self commonInit];
    saveFileName = [collapse(saveName) retain];
    [window setTitleWithRepresentedFilename:saveFileName];
    
    if ([self loadSaveFile])
	{    
	[self setCellMatrix];
	[self updateAfterMoving];
	[self solvedCheck];
	[self cascade];
	return self;		// Success exit.
	}

    [self autorelease];		// Error exit.
    return 0;
    }


//-----------------------------------------------------------------------------
// +newMaze:
//-----------------------------------------------------------------------------
+ (SokoBoard*)newMaze:(NSString*)maze
    {
    SokoBoard* p;
    NSString* n = saveFileNameForMazeName( maze );
    if ((p = [self findBoardWithSaveFileName:n]) == 0)
	{
	p = [[self alloc] initMazeName:maze saveName:n];
	if (p != 0)
	    [openBoards addObject:p];
	}
    else
	[p orderFront:self];
    return p;
    }


//-----------------------------------------------------------------------------
// +oldMaze:
//-----------------------------------------------------------------------------
+ (SokoBoard*)oldMaze:(NSString*)saveName;
    {
    SokoBoard* p;
    if ((p = [self findBoardWithSaveFileName:saveName]) == 0)
	{
	p = [[self alloc] initSaveName:saveName];
	if (p != 0)
	    [openBoards addObject:p];
	}
    else
	[p orderFront:self];
    return p;
    }


//-----------------------------------------------------------------------------
// +openFile:makeKeyWindow:
//-----------------------------------------------------------------------------
+ (SokoBoard*)openFile:(NSString*)filename makeKeyWindow:(BOOL)makeKey
    {
    SokoBoard* b = 0;
    if (![filename isEqualToString:@""])
	{
	NSString* const ext = [filename pathExtension];
	if ([ext isEqualToString:getMazeExtension()])
	    b = [self newMaze:filename];
	else if ([ext isEqualToString:getSaveExtension()])
	    b = [self oldMaze:filename];
	}
    if (b != 0)
	[b makeKeyWindow];
    return b;
    }


//-----------------------------------------------------------------------------
// +openFile:
//-----------------------------------------------------------------------------
+ (BOOL)openFile:(NSString*)filename
    {
    return ([self openFile:filename makeKeyWindow:YES] != 0);
    }


//-----------------------------------------------------------------------------
// +chooseMazeNew:saved:
//-----------------------------------------------------------------------------
+ (SokoBoard*)chooseMazeNew:(BOOL)useNew saved:(BOOL)useSaved
    {
    static NSOpenPanel* openPanel = 0;
    if (openPanel == 0)
	{
	openPanel = [[NSOpenPanel openPanel] retain];
	[openPanel setAllowsMultipleSelection:YES];
	}

    NSParameterAssert( useNew || useSaved );
    [openPanel setTitle:(useSaved ? @"Open Maze" : @"New Maze")];

    NSString* dir = expand( useNew ? getMazeDirectory() : getSaveDirectory() );
    NSString* file =
	(useSaved ? @"" : filenamePart(mazeNameForLevel([SokoPref getLevel])));

    NSMutableArray* types = [NSMutableArray array];
    if (useNew)   [types addObject:getMazeExtension()];
    if (useSaved) [types addObject:getSaveExtension()];

    SokoBoard* last_board = 0;
    if ([openPanel runModalForDirectory:dir file:file types:types]==NSOKButton)
	{
	NSArray* paths = [openPanel filenames];
	NSEnumerator* enumerator = [paths objectEnumerator];
	NSString* path;
	while ((path = [enumerator nextObject]) != 0)
	    {
	    SokoBoard* b = [self openFile:path makeKeyWindow:NO];
	    if (b != 0)
		last_board = b;
	    }
	[last_board makeKeyWindow];
	}
    return last_board;
    }


//-----------------------------------------------------------------------------
// +tryNewMaze:
//-----------------------------------------------------------------------------
+ (SokoBoard*)tryNewMaze:(NSString*)path
    {
    SokoBoard* b = 0;
    if ([[NSFileManager defaultManager] fileExistsAtPath:path])
	b = [self newMaze:path];
    return b;
    }


//-----------------------------------------------------------------------------
// +nextMaze:
//-----------------------------------------------------------------------------
+ (BOOL)nextMaze:(BOOL)required
    {
    SokoBoard* b = [self tryNewMaze:mazeNameForLevel([SokoPref getLevel])];
    if (b == 0 && required)
	{
	b = [self tryNewMaze:mazeNameForLevel(1)];
	if (b == 0)
	    b = [self chooseMazeNew:YES saved:YES];
	}

    if (b != 0)
	[b makeKeyWindow];

    return (b != 0);
    }


//-----------------------------------------------------------------------------
// +new:
//-----------------------------------------------------------------------------
+ (void)new:(id)sender
    {
    [self chooseMazeNew:YES saved:NO];
    }


//-----------------------------------------------------------------------------
// +open:
//-----------------------------------------------------------------------------
+ (void)open:(id)sender
    {
    [self chooseMazeNew:NO saved:YES];
    }


//-----------------------------------------------------------------------------
// -saveToFile:
//-----------------------------------------------------------------------------
- (BOOL)saveToFile:(NSString*)filename
    {
    FILE* fp;
    NSString* const path = expand(filename);
    NSString* const dir = directoryPart(path);
    if (!mkdirs( dir ))
	NSRunAlertPanel(@"Sorry", @"Cannot create save directory: \"%@\"",
		@"OK", 0, 0, dir );
    else if ((fp = fopen( [path fileSystemRepresentation], "w" )) == 0)
	NSRunAlertPanel(@"Sorry", @"Cannot create save file: \"%@\" [%s]",
		@"OK", 0, 0, path, strerror(errno) );
    else
	{
	fprintf( fp, "%d\n", SAVE_FILE_VERSION );
	fprintf( fp, "%s\n", [collapse(mazeFileName) cString] );
	fprintf( fp, "%d %d %d\n", history_max, history_len, history_pos );
	runLengthEncodeString( fp, history, history_len );
	fprintf( fp, "%d %d\n", recordedMoves, recordedPushes );
	fprintf( fp, "%d %d\n", numMoves, numPushes );
	fprintf( fp, "%d %d\n", NUMROWS, NUMCOLS );
	for (int r = 0; r < NUMROWS; r++)
	    {
	    int c;
	    for (c = 0; c < NUMCOLS; c++)
		board[r][c] |= PRINTABLE_BIT;	// Make it printable.
	    runLengthEncodeString( fp, board[r], NUMCOLS );
	    for (c = 0; c < NUMCOLS; c++)
		board[r][c] &= ~PRINTABLE_BIT;	// Make it unprintable.
	    }

	fclose( fp );
	[window setDocumentEdited:NO];
	return YES;
	}
    return NO;
    }


//-----------------------------------------------------------------------------
// -save:
//-----------------------------------------------------------------------------
- (void)save:(id)sender
    {
    if ([window isDocumentEdited])
	[self saveToFile:saveFileName]; 
    }


//-----------------------------------------------------------------------------
// +saveAll:
//-----------------------------------------------------------------------------
+ (void)saveAll:(id)sender
    {
    [openBoards makeObjectsPerformSelector:@selector(save:) withObject:sender];
    }


//-----------------------------------------------------------------------------
// openMazes
//-----------------------------------------------------------------------------
+ (unsigned int)openMazes
    {
    return [openBoards count];
    }


//-----------------------------------------------------------------------------
// -saveAs:
//-----------------------------------------------------------------------------
- (void)saveAs:(id)sender
    {
    static NSSavePanel* savePanel = 0;
    if (savePanel == 0)
	{
	savePanel = [[NSSavePanel savePanel] retain];
	[savePanel setTitle:@"Save Maze"];
	[savePanel setRequiredFileType:getSaveExtension()];
	}

    if ([savePanel runModalForDirectory:expand(getSaveDirectory())
	file:filenamePart(saveFileName)] == NSOKButton)
	{
	NSString* filename = [savePanel filename];
	if ([self saveToFile:filename])
	    {
	    [saveFileName release];
	    saveFileName = [filename retain];
	    [window setTitleWithRepresentedFilename:filename];
	    }
	}
    }


//-----------------------------------------------------------------------------
// Window Control
//-----------------------------------------------------------------------------
- (BOOL)windowShouldClose:(id)sender
    {
    [SokoBoard forgetBoard:self];
    [self autorelease];
    return YES;
    }

- (void)orderFront:(id)sender
    {
    [window makeFirstResponder:cellMatrix];
    [window orderFront:sender];
    }

- (void)makeKeyWindow { [window makeKeyWindow]; }


//-----------------------------------------------------------------------------
// -undoMove
//-----------------------------------------------------------------------------
- (void)undoMove
    {
    int src_row,src_col, dst_row,dst_col, ddst_row,ddst_col;
    int ch,src_ch,dst_ch,ddst_ch;
    id  src_cell,dst_cell,ddst_cell;
    int move_ch;
    int is_push;
    int dir;
    
    NSParameterAssert( history_pos <= history_len );
    NSParameterAssert( history_pos > 0 );
    move_ch = history[ --history_pos ];
    is_push = IS_PUSH(move_ch);
    dir = dir_from_move(move_ch);
    
    dst_row  = playerRow;
    dst_col  = playerCol;
    NSParameterAssert( GOOD_ROW(dst_row) && GOOD_COL(dst_col) );
    dst_ch   = board[dst_row][dst_col];
    dst_cell = [cellMatrix cellAtRow:dst_row column:dst_col];
    NSParameterAssert( IS_PLAYER(dst_ch) );
    
    src_row  = dst_row - DirDeltaRow[ dir ];
    src_col  = dst_col - DirDeltaCol[ dir ];
    NSParameterAssert( GOOD_ROW(src_row) && GOOD_COL(src_col) );
    src_ch   = board[src_row][src_col];
    src_cell = [cellMatrix cellAtRow:src_row column:src_col];
    NSParameterAssert( IS_EMPTY(src_ch) );
    
    ddst_row  = dst_row + DirDeltaRow[ dir ];
    ddst_col  = dst_col + DirDeltaCol[ dir ];
    ddst_ch   = WALL_CH;
    ddst_cell = 0;
    if (is_push)
	{
	NSParameterAssert( GOOD_ROW(ddst_row) && GOOD_COL(ddst_col) );
	ddst_ch   = board[ddst_row][ddst_col];
	ddst_cell = [cellMatrix cellAtRow:ddst_row column:ddst_col];
	NSParameterAssert( IS_CRATE(ddst_ch) );
	}
    
    ch = IS_SAFE(src_ch) ? SAFE_PLAYER_CH : PLAYER_CH;
    board[src_row][src_col] = ch;
    [src_cell setImage:image_for_ch(ch)];
    
    if (is_push)
	{
	ch = IS_SAFE(dst_ch) ? SAFE_CRATE_CH : CRATE_CH;
	board[dst_row][dst_col] = ch;
	[dst_cell setImage:image_for_ch(ch)];
	
	ch = IS_SAFE(ddst_ch) ? SAFE_EMPTY_CH : EMPTY_CH;
	board[ddst_row][ddst_col] = ch;
	[ddst_cell setImage:image_for_ch(ch)];
	}
    else
	{
	ch = IS_SAFE(dst_ch) ? SAFE_EMPTY_CH : EMPTY_CH;
	board[dst_row][dst_col] = ch;
	[dst_cell setImage:image_for_ch(ch)];
	}
    
    playerRow = src_row;
    playerCol = src_col;
    
    numMoves--;
    if (is_push)
	numPushes--;
    
    if (!animating)
	[self updateAfterMoving];

    if (is_push)
	[self solvedCheckDst: dst_ch DDst: ddst_ch];
    }


//-----------------------------------------------------------------------------
// -redoMove
//-----------------------------------------------------------------------------
- (void) redoMove
    {
    int src_row,src_col, dst_row,dst_col, ddst_row,ddst_col;
    int ch,src_ch,dst_ch,ddst_ch;
    id  src_cell,dst_cell,ddst_cell;
    int move_ch;
    int is_push;
    int dir;
    
    NSParameterAssert( history_pos >= 0 );
    NSParameterAssert( history_pos < history_len );
    move_ch = history[ history_pos++ ];
    is_push = IS_PUSH(move_ch);
    dir = dir_from_move(move_ch);
    
    src_row  = playerRow;
    src_col  = playerCol;
    NSParameterAssert( GOOD_ROW(src_row) && GOOD_COL(src_col) );
    src_ch   = board[src_row][src_col];
    src_cell = [cellMatrix cellAtRow:src_row column:src_col];
    NSParameterAssert( IS_PLAYER(src_ch) );
    
    dst_row  = src_row + DirDeltaRow[ dir ];
    dst_col  = src_col + DirDeltaCol[ dir ];
    NSParameterAssert( GOOD_ROW(dst_row) && GOOD_COL(dst_col) );
    dst_ch   = board[dst_row][dst_col];
    dst_cell = [cellMatrix cellAtRow:dst_row column:dst_col];
    NSParameterAssert( (is_push && IS_CRATE(dst_ch)) ||
			(!is_push && IS_EMPTY(dst_ch)) );
    
    ddst_row  = dst_row + DirDeltaRow[ dir ];
    ddst_col  = dst_col + DirDeltaCol[ dir ];
    ddst_ch   = WALL_CH;
    ddst_cell = 0;
    if (is_push)
	{
	NSParameterAssert( GOOD_ROW(ddst_row) && GOOD_COL(ddst_col) );
	ddst_ch   = board[ddst_row][ddst_col];
	ddst_cell = [cellMatrix cellAtRow:ddst_row column:ddst_col];
	NSParameterAssert( IS_EMPTY(ddst_ch) );
	}
    
    ch = IS_SAFE(src_ch) ? SAFE_EMPTY_CH : EMPTY_CH;
    board[src_row][src_col] = ch;
    [src_cell setImage:image_for_ch(ch)];
    
    ch = IS_SAFE(dst_ch) ? SAFE_PLAYER_CH : PLAYER_CH;
    board[dst_row][dst_col] = ch;
    [dst_cell setImage:image_for_ch(ch)];
    
    if (is_push)
	{
	ch = IS_SAFE(ddst_ch) ? SAFE_CRATE_CH : CRATE_CH;
	board[ddst_row][ddst_col] = ch;
	[ddst_cell setImage:image_for_ch(ch)];
	}
    
    playerRow = dst_row;
    playerCol = dst_col;
    
    numMoves++;
    if (is_push)
	numPushes++;
    
    if (!animating)
	[self updateAfterMoving];
        
    if (is_push)
	[self solvedCheckDst: dst_ch DDst: ddst_ch];
    }


- (void)redoPressed:(id)sender
    { if (history_pos < history_len) [self redoMove]; }

- (void)undoPressed:(id)sender
    { if (history_pos > 0) [self undoMove]; }


//-----------------------------------------------------------------------------
// -moveDir:
//-----------------------------------------------------------------------------
- (BOOL)moveDir:(Direction)dir
    {
    int ch,dst_ch,ddst_ch, row,col, dst_row,dst_col, ddst_row, ddst_col;
    int dr,dc;
    id cell,dst_cell,ddst_cell;
    BOOL retval = YES;
    BOOL is_push = NO;
    
    row = playerRow;
    col = playerCol;
    dr = DirDeltaRow[dir];
    dc = DirDeltaCol[dir];
    dst_row = row + dr;
    dst_col = col + dc;
    ddst_row = dst_row + dr;
    ddst_col = dst_col + dc;
    
    NSParameterAssert( GOOD_ROW(row) && GOOD_COL(col) );
    ch = board[row][col];
    cell = [cellMatrix cellAtRow:row column:col];
    
    NSParameterAssert( GOOD_ROW(dst_row) && GOOD_COL(dst_col) );
    dst_ch = board[dst_row][dst_col];
    dst_cell = [cellMatrix cellAtRow:dst_row column:dst_col];
    
    if (GOOD_ROW(ddst_row) && GOOD_COL(ddst_col))
	{
	ddst_ch = board[ddst_row][ddst_col];
	ddst_cell = [cellMatrix cellAtRow:ddst_row column:ddst_col];
	}
    else
	{
	ddst_ch = WALL_CH;
	ddst_cell = 0;
	}
    
    if (IS_EMPTY(dst_ch))	// It's a move (not a push).
	{
	if (history_pos > 0	// Is it the opposite of the last move?
	&&  history[ history_pos - 1 ] == ch_for_move( DirOpposite[dir], NO ))
	    {
	    [self undoMove];	// Yes, implicitly undo the last move.
	    retval = YES;
	    }
	else 
	    {
	    if (ch == SAFE_PLAYER_CH)
		{
		board[row][col] = SAFE_EMPTY_CH;
		[cell setImage:image_for_ch(SAFE_EMPTY_CH)];
		}
	    else
		{
		NSParameterAssert( ch == PLAYER_CH );
		board[row][col] = EMPTY_CH;
		[cell setImage:image_for_ch(EMPTY_CH)];
		}
	    if (dst_ch == SAFE_EMPTY_CH)
		{
		board[dst_row][dst_col] = SAFE_PLAYER_CH;
		[dst_cell setImage:image_for_ch(SAFE_PLAYER_CH)];
		}
	    else
		{
		NSParameterAssert( dst_ch == EMPTY_CH );
		board[dst_row][dst_col] = PLAYER_CH;
		[dst_cell setImage:image_for_ch(PLAYER_CH)];
		}
	    playerRow = dst_row;
	    playerCol = dst_col;
	    [self appendMoveToHistory:dir isPush:NO];
	    }
	}
    else if (IS_CRATE(dst_ch) && IS_EMPTY(ddst_ch))
	{
	is_push = YES;
	if (ch == SAFE_PLAYER_CH)
	    {
	    board[row][col] = SAFE_EMPTY_CH;
	    [cell setImage:image_for_ch(SAFE_EMPTY_CH)];
	    }
	else
	    {
	    NSParameterAssert( ch == PLAYER_CH );
	    board[row][col] = EMPTY_CH;
	    [cell setImage:image_for_ch(EMPTY_CH)];
	    }
	if (dst_ch == SAFE_CRATE_CH)
	    {
	    board[dst_row][dst_col] = SAFE_PLAYER_CH;
	    [dst_cell setImage:image_for_ch(SAFE_PLAYER_CH)];
	    }
	else
	    {
	    NSParameterAssert( dst_ch == CRATE_CH );
	    board[dst_row][dst_col] = PLAYER_CH;
	    [dst_cell setImage:image_for_ch(PLAYER_CH)];
	    }
	if (ddst_ch == SAFE_EMPTY_CH)
	    {
	    board[ddst_row][ddst_col] = SAFE_CRATE_CH;
	    [ddst_cell setImage:image_for_ch(SAFE_CRATE_CH)];
	    }
	else
	    {
	    NSParameterAssert( ddst_ch == EMPTY_CH );
	    board[ddst_row][ddst_col] = CRATE_CH;
	    [ddst_cell setImage:image_for_ch(CRATE_CH)];
	    }
	playerRow = dst_row;
	playerCol = dst_col;
	[self appendMoveToHistory:dir isPush:YES];
	[self solvedCheckDst:dst_ch DDst:ddst_ch];
	}
    else
	{
	retval = NO;		// Illegal move.
	}
    
    return retval;
    }


- (void)moveLeftPressed:(id)sender	{ [self moveDir:DIR_LEFT ]; }
- (void)moveRightPressed:(id)sender	{ [self moveDir:DIR_RIGHT]; }
- (void)moveUpPressed:(id)sender	{ [self moveDir:DIR_UP   ]; }
- (void)moveDownPressed:(id)sender	{ [self moveDir:DIR_DOWN ]; }


//-----------------------------------------------------------------------------
// -doDiag::
//-----------------------------------------------------------------------------
- (BOOL)doDiag:(Direction)a :(Direction)b
    {
    int r = playerRow + DirDeltaRow[a];
    int c = playerCol + DirDeltaCol[a];
    int ch = board[r][c];
    if (IS_EMPTY(ch))
	{
	r += DirDeltaRow[b];
	c += DirDeltaCol[b];
	ch = board[r][c];
	if (IS_EMPTY(ch))
	    {
	    [self moveDir:a];
	    [self moveDir:b];
	    return YES;
	    }
	}
    return NO;
    }


- (void)moveDiag:(Direction)a :(Direction)b
    {
    if (![self doDiag:a:b])
	[self doDiag:b:a];
    }

- (void)moveUpLeftPressed:(id)sender
	{ [self moveDiag:DIR_UP :DIR_LEFT]; }
- (void)moveUpRightPressed:(id)sender
	{ [self moveDiag:DIR_UP :DIR_RIGHT]; }
- (void)moveDownLeftPressed:(id)sender
	{ [self moveDiag:DIR_DOWN :DIR_LEFT]; }
- (void)moveDownRightPressed:(id)sender
	{ [self moveDiag:DIR_DOWN :DIR_RIGHT]; }


//-----------------------------------------------------------------------------
// -keyDown:
//-----------------------------------------------------------------------------
- (void)keyDown:(NSEvent*)ev 
    {
    unichar const c = [[ev characters] characterAtIndex:0];
    switch (c)
	{
	case NSLeftArrowFunctionKey:	[self moveDir:DIR_LEFT  ]; break;
	case NSUpArrowFunctionKey:	[self moveDir:DIR_UP    ]; break;
	case NSRightArrowFunctionKey:	[self moveDir:DIR_RIGHT ]; break;
	case NSDownArrowFunctionKey:	[self moveDir:DIR_DOWN  ]; break;
	default: [[cellMatrix nextResponder] keyDown:ev];	   break;
	}
    }


//-----------------------------------------------------------------------------
// -refreshMatrix
//-----------------------------------------------------------------------------
- (void)refreshMatrix
    {
    [cellMatrix displayIfNeeded];
    [window flushWindowIfNeeded];
    }


//-----------------------------------------------------------------------------
// -inspectPlaybackSlider:
//-----------------------------------------------------------------------------
- (void)inspectPlaybackSlider:(id)sender
    {
    animating = YES;
    BOOL animate = [animateSwitch state];

    int new_pos = [playbackSlider intValue];
    if (history_pos > new_pos)
	{
	while (history_pos > new_pos)
	    {
	    [self undoMove];
	    if (animate)
		[self refreshMatrix];
	    }
	}
    else if (history_pos < new_pos)
	{
	while (history_pos < new_pos)
	    {
	    [self redoMove];
	    if (animate)
		[self refreshMatrix];
	    }
	}
    animating = NO;
    [self updateAfterMoving];
    }


//-----------------------------------------------------------------------------
// -shortestPathTo::
//-----------------------------------------------------------------------------
- (void)shortestPathTo:(int)dst_row :(int)dst_col
    {
    int row,col,r,c, distance;
    int map[ ROWMAX ][ COLMAX ];
    Coord coord,temp_coord;
    Coord curr_stack[ ROWMAX * COLMAX ];
    Coord next_stack[ ROWMAX * COLMAX ];
    Coord* p_curr_stack;
    Coord* p_next_stack;
    Coord* p_temp_stack;
    int curr_stack_top;
    int next_stack_top;
    int temp_stack_top;
    
    NSParameterAssert( GOOD_ROW(dst_row) && GOOD_COL(dst_col) );
    NSParameterAssert( dst_row != playerRow || dst_col != playerCol );
    NSParameterAssert( IS_EMPTY(board[dst_row][dst_col]) );
    
    for (row = 0;  row < NUMROWS;  row++)
	for (col = 0;  col < NUMCOLS;  col++)
	    map[row][col] = (IS_EMPTY(board[row][col]) ? INT_MAX-1 : INT_MAX);
    
    next_stack_top = 0;
    p_next_stack = next_stack;
    curr_stack_top = 0;
    p_curr_stack = curr_stack;
    
    distance = 0;
    map[ playerRow ][ playerCol ] = distance;		// "mark"
    coord.row = playerRow;
    coord.col = playerCol;
    p_next_stack[ next_stack_top++ ] = coord;		// "push"
    while (next_stack_top > 0)
	{
	++distance;
	p_temp_stack = p_next_stack;			// switch stacks.
	p_next_stack = p_curr_stack;
	p_curr_stack = p_temp_stack;
	temp_stack_top = next_stack_top;
	next_stack_top = curr_stack_top;
	curr_stack_top = temp_stack_top;
	while (curr_stack_top > 0)
	    {
	    temp_coord = p_curr_stack[ --curr_stack_top ]; // "pop"
	    for (int dir = (int)DIR_FIRST;  dir <= (int)DIR_LAST;  dir++)
		{
		row = temp_coord.row + DirDeltaRow[ dir ];
		col = temp_coord.col + DirDeltaCol[ dir ];
		if (map[row][col] == INT_MAX-1)
		    {
		    if (row == dst_row && col == dst_col)
			goto FOUND_DST;
		    map[row][col] = distance;		// "mark"
		    coord.row = row;
		    coord.col = col;
		    p_next_stack[ next_stack_top++ ] = coord; // "push"
		    }
		}
	    }
	}
    
    return;	// *** RETURN ***	Destination is not reachable.
    
FOUND_DST:
    
    map[dst_row][dst_col] = distance;
    row = dst_row;
    col = dst_col;
    curr_stack_top = 0;
    while (distance > 0)
	{
	distance--;
	for (int dir = (int)DIR_FIRST;  dir <= (int)DIR_LAST;  dir++)
	    {
	    r = row + DirDeltaRow[dir];
	    c = col + DirDeltaCol[dir];
	    if (map[r][c] == distance)
		{
		curr_stack[ curr_stack_top++ ].row = (int) DirOpposite[ dir ];
		row = r;
		col = c;
		break;
		}
	    }
	}
    
    animating = YES;
    while (curr_stack_top > 0)
	{
	Direction const dir = Direction( curr_stack[ --curr_stack_top ].row );
	NSParameterAssert( [self canMove:dir] );
	[self moveDir:dir];
	[self refreshMatrix];
	}
    [self updateAfterMoving];
    animating = NO;
    }


//-----------------------------------------------------------------------------
// -trackCratePressDir:row:col:
//-----------------------------------------------------------------------------
- (void)trackCratePressDir:(Direction)dir row:(int)row col:(int)col
    {
    BOOL did_move = NO;
    int const delta_x = DirDeltaCol[ dir ] * CELL_WIDTH;
    int const delta_y = DirDeltaRow[ dir ] * CELL_HEIGHT;
    NSEvent* event;

    animating = YES;

    [NSEvent startPeriodicEventsAfterDelay:0.2 withPeriod:0.05];
    [cellMatrix lockFocus]; // Required by PSadjustcursor() (in SokoMoveMouse).
    
    do	{
	if ([self moveDir:dir])
	    {
	    SokoMoveMouse( delta_x, delta_y );
	    did_move = YES;
	    }
	event = [NSApp nextEventMatchingMask:(NSLeftMouseUpMask|NSPeriodicMask)
		untilDate:[NSDate distantFuture]
		inMode:NSEventTrackingRunLoopMode dequeue:YES];
	}
    while ([event type] == NSPeriodic);

    [cellMatrix unlockFocus];
    [NSEvent stopPeriodicEvents];
    
    animating = NO;
    
    if (did_move)
	[self updateAfterMoving];
    }


//-----------------------------------------------------------------------------
// -cratePressedAt::
//-----------------------------------------------------------------------------
- (void)cratePressedAt:(int)row :(int)col
    {
    int const dr = row - playerRow;
    int const dc = col - playerCol;

    if (-1 <= dr && dr <= 1 && -1 <= dc && dc <= 1)
	{
	Direction dir;
	if (dr == 0 && dc == -1)	dir = DIR_LEFT;
	else if (dr == 0 && dc == 1)	dir = DIR_RIGHT;
	else if (dc == 0 && dr == -1)	dir = DIR_UP;
	else				dir = DIR_DOWN;
	[self trackCratePressDir:dir row:row col:col];
	}
    }


//-----------------------------------------------------------------------------
// -mouseDown:
//-----------------------------------------------------------------------------
- (void)mouseDown:(NSEvent*)event 
    {
    NSPoint p = [cellMatrix convertPoint:[event locationInWindow] fromView:0];
    int const col = ((int)p.x) / CELL_WIDTH;
    int const row = ((int)p.y) / CELL_HEIGHT;
    char ch = board[row][col];
    if (IS_EMPTY(ch))
	[self shortestPathTo:row:col];
    else if (IS_CRATE(ch))
	[self cratePressedAt:row:col];
    }


//-----------------------------------------------------------------------------
// -pushCrateTo::
//-----------------------------------------------------------------------------
- (void)pushCrateTo:(int)row :(int)col
    {
    Direction dir;
    int dr,dc;
    char ch = board[row][col];	NSParameterAssert( IS_EMPTY(ch) );

    if (row < playerRow)	dr = -1;
    else if (row > playerRow)	dr = 1;
    else			dr = 0;

    if (col < playerCol)	dc = -1;
    else if (col > playerCol)	dc = 1;
    else			dc = 0;

    if (dr == 0 && dc == -1)		dir = DIR_LEFT;
    else if (dr == 0 && dc == 1)	dir = DIR_RIGHT;
    else if (dc == 0 && dr == -1)	dir = DIR_UP;
    else if (dc == 0 && dr == 1)	dir = DIR_DOWN;
    else return;		// **** EXIT **** 	Not a straight line.
    
    int num_crates = 0;
    int num_cells = 0;
    int r = playerRow;
    int c = playerCol;
    r += dr;
    c += dc;
    while (r != row || c != col)
	{
	ch = board[r][c];
	if (IS_CRATE(ch))
	    num_crates++;
	else if (ch == WALL_CH)
	    return;		// **** EXIT **** Not clear path.
	num_cells++;
	r += dr;
	c += dc;
	}
    
    if (num_crates <= 1 && num_cells > 0)
	{
	animating = YES;
	while (num_cells-- > 0)
	    {
	    [self moveDir:dir];
	    [self refreshMatrix];
	    }
	animating = NO;
	[self updateAfterMoving];
	}
    }


//-----------------------------------------------------------------------------
// -rightMouseDown:
//-----------------------------------------------------------------------------
- (void)rightMouseDown:(NSEvent*)event 
    {
    NSPoint p = [cellMatrix convertPoint:[event locationInWindow] fromView:0];
    int const col = ((int)p.x) / CELL_WIDTH;
    int const row = ((int)p.y) / CELL_HEIGHT;
    char const ch = board[row][col];
    if (IS_EMPTY(ch))
	[self pushCrateTo:row:col];
    }

- (void)undo:(id)sender	{ [self undoPressed:sender]; }
- (void)redo:(id)sender	{ [self redoPressed:sender]; }


//-----------------------------------------------------------------------------
// -validateMenuItem:
//-----------------------------------------------------------------------------
- (BOOL)validateMenuItem:(NSMenuItem*)item
    {
    SEL const action = [item action];
    if (action == @selector(undo:))
	return (history_pos > 0);
    else if (action == @selector(redo:))
	return (history_pos < history_len);
    else if (action == @selector(save:))
	return [window isDocumentEdited];
    return YES;
    }

@end
