ftp.nice.ch/pub/next/games/strategic/TileSlide.NIHS.bs.tar.gz#/TileSlide/Source/TileControl.m

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

	// TileSlide v1.0
	// By Kevin Brain (ksbrain@zeus.UWaterloo.ca)
	//  with SoundGenerator class by Ali Ozer 
	// Released to the public domain September, 1991

#import "TileControl.h"

#import <appkit/nextstd.h>	//imports math.h,stdio.h,libc.h
							// (libc.h imports string.h)
#import	<appkit/Box.h>
#import	<appkit/Button.h>
#import	<appkit/Cell.h>
#import <appkit/Text.h>			// NXOrderStrings
#import <appkit/Matrix.h>		// cellAt:: method
#import <appkit/Panel.h>		// NXRunAlertPanel
#import <appkit/OpenPanel.h>	// for loading background
#import	<appkit/Application.h>	// NX_BASETHRESHOLD
#import	<appkit/FontManager.h>	// for changing size of button fonts
#import <appkit/publicWraps.h>	// for NXConvertWinNumToGlobal
#import <appkit/Listener.h>		// for NX_WORKSPACEREQUEST
#import <appkit/Speaker.h>		// for setSendPort
#import <defaults.h>			// for using defaults system

#import <next/machparam.h>		// for DELAY (playing ending tune)

	// The scheme for the "music" is to play 2 note chords, where the notes are
	// selected from arrays of chords.  The musical key changes every NOTESPERBAR notes.
	// The two arrays rowNotes and colNotes store the pitches.  (The notes of the 
	// rowNotes array are generally lower in pitch).  A note from each array is
	// played whenever a tile is moved.  The note from the rowNotes array is selected 
	// by the row number clicked, while the notes within a given key (row) of the 
	// colNotes array are played sequentially.
#define NUMBEROFBARS 8
#define NOTESPERBAR 4
#define COMPUTEBAR ((int)((numberOfMoves%(NUMBEROFBARS*NOTESPERBAR)) /NOTESPERBAR))
#define SEQUENTIALNOTES ((numberOfMoves%(NUMBEROFBARS*NOTESPERBAR)) %NOTESPERBAR)
#define ROWNOTE(k,n)   (MKKeyNumToFreq (rowNotes[(k%NUMBEROFBARS)][(n%5)]))
#define COLNOTE(k,n)   (MKKeyNumToFreq (colNotes[(k%NUMBEROFBARS)][(n%5)]))

@implementation TileControl

- appDidInit:sender
// responds as application's delegate
{
	NXRect contentViewFrame;
	NXSize theSize;
	char titleTemp[3];
	const char *tmpstr;
	int i,row,col,x,y;
	int tempIconPosition;
    unsigned int windowNum;
    id	speaker = [NXApp appSpeaker];
	
	static NXDefaultsVector TileSlideDefaults = {
		{"Picture", ""},
		{"Difficulty", "Normal"},
		{"UsePictures", "YES"},
		{"UseNumbers", "NO"},
		{"EasyBests", "999,999,999,999"},
		{"NormalBests", "999,999,999,999"},
		{"HardBests", "999,999,999,999"},
		{"BoardLocation","314 314"},
		{NULL}};

	NXRegisterDefaults([NXApp appName], TileSlideDefaults);

	tileSlideCV = [slideWindowOut contentView];
	infoPanelCV = [infoPanelOut contentView];
  /* set initial difficulty */
	tmpstr = NXGetDefaultValue ([NXApp appName], "Difficulty");
    if (sscanf(tmpstr, "%d", &NumTilesX) != 1) NumTilesX = 4;
	NumTilesY = NumTilesX;

  /* retrieve bests for four challenges in each difficulty */
	tmpstr = NXGetDefaultValue ([NXApp appName], "EasyBests");
    if (sscanf(tmpstr, "%d %d %d %d", &bests[0][0],&bests[0][1],&bests[0][2],&bests[0][3]) != 4)
		for (i=0;i<4;i++) bests[0][i] = MAXMOVES;
	tmpstr = NXGetDefaultValue ([NXApp appName], "NormalBests");
    if (sscanf(tmpstr, "%d %d %d %d", &bests[1][0],&bests[1][1],&bests[1][2],&bests[1][3]) != 4)
		for (i=0;i<4;i++) bests[1][i] = MAXMOVES;
	tmpstr = NXGetDefaultValue ([NXApp appName], "HardBests");
    if (sscanf(tmpstr, "%d %d %d %d", &bests[2][0],&bests[2][1],&bests[2][2],&bests[2][3]) != 4)
		for (i=0;i<4;i++) bests[2][i] = MAXMOVES;
	for (row=0;row < 3;row++)
		for (col=0; col < 4; col++)
			if (bests[row][col] < MAXMOVES)
				[[bestsMatrixOut cellAt:row:col] setIntValue:bests[row][col]];
			else
				[[bestsMatrixOut cellAt:row:col] setStringValue:"±"];

  /* initialize info panel animation variables */
	[infoPanelCV getFrame:&contentViewFrame];
	stripe1Positiony = 5;
	stripe2Positiony = (contentViewFrame.size.height-33);
	stripe1Positionx = 5;
	stripe2Positionx = (contentViewFrame.size.width-33);
	stripe1Movement = 5;
	stripe2Movement = -5;

	tmpstr = NXGetDefaultValue ([NXApp appName], "Picture");
	if ([self initAndCheckPicture:(char *)tmpstr] == NO) {
		pictureLoaded = NO;  
		usePictures = NO;
		useNumbers = YES;
		[useNumbersSwitch setEnabled:NO];
		[useNumbersSwitch setIntValue:1];
		[usePicturesSwitch setEnabled:NO];
		[usePicturesSwitch setIntValue:0];
		tempIconPosition = NX_TITLEONLY;
		}
	else {
	
		[picture getSize:(NXSize *)&theSize];
		tmpstr = NXGetDefaultValue ([NXApp appName], "UsePictures");

		if (NXOrderStrings((unsigned char *) "YES",(unsigned char *) tmpstr, YES, 4, NULL) == 0) {
			usePictures = YES;
			[usePicturesSwitch setIntValue:1];
			}
		else {	
			usePictures = NO;
			[usePicturesSwitch setIntValue:0];
			}

		if ((theSize.height >= NUMBERMINHEIGHT) && (theSize.width >= NUMBERMINWIDTH)
				&& ([usePicturesSwitch intValue] == 1))
			[useNumbersSwitch setEnabled:YES];
		else
			[useNumbersSwitch setEnabled:NO];

		tmpstr = NXGetDefaultValue ([NXApp appName], "UseNumbers");
		if ((NXOrderStrings((unsigned char *) "YES",(unsigned char *) tmpstr, YES, 4, NULL) == 0)
				&& (theSize.height >= NUMBERMINHEIGHT) && (theSize.width >= NUMBERMINWIDTH)) {
			useNumbers = YES;
			[useNumbersSwitch setIntValue:1];
			[usePicturesSwitch setEnabled:YES];
			if (usePictures == YES)
				tempIconPosition = NX_ICONOVERLAPS;
			else
				tempIconPosition = NX_TITLEONLY;
			}
		else {
			useNumbers = NO;
			[useNumbersSwitch setIntValue:0];
			[usePicturesSwitch setEnabled:NO];
			if (usePictures == YES)
				tempIconPosition = NX_ICONONLY;
			else
				tempIconPosition = NX_TITLEONLY;
			}
		tempIconPosition = NX_ICONONLY;
		}
	myFontManager = [FontManager new];
  /* create the box that holds the tiles */
	tileBox = [Box new];
	[tileBox setBorderType:NX_BEZEL];
	[tileBox setTitlePosition:NX_NOTITLE];
	[tileBox setOffsets:(NXCoord)0 :(NXCoord)0];
  /* create a box for erasing the old button position when a button is moved */
	clearBox = [Box new];
	[clearBox setBorderType:NX_NOBORDER];
	[clearBox setTitlePosition:NX_NOTITLE];
	[clearBox setOffsets:(NXCoord)0 :(NXCoord)0];

	for (i=0; i<(MAXSIZEX*MAXSIZEY); i++) {
		tile[i] = [Button new];
		[tile[i] setAutodisplay:NO];
		sprintf(titleTemp,"%d",i+1);
		[tile[i] setTitle:titleTemp];
		[tile[i] setIconPosition:tempIconPosition];
		}
		
  /* Create a matrix of invisible buttons that detect button presses */
  /* Tag numbers of the form (row * MAXSIZEX + col) tell the slideTile: */
  /* method where the button was pressed */ 
	for (row=0; row<MAXSIZEY; row++) 
		for (col=0; col<MAXSIZEX; col++) {
			tileInPosition[row][col].invisibleID = [Button new];
			[tileInPosition[row][col].invisibleID setTransparent:(BOOL)YES];
			[tileInPosition[row][col].invisibleID setTag:(row * MAXSIZEX + col)];
			[tileInPosition[row][col].invisibleID setTarget:self];
			[tileInPosition[row][col].invisibleID setAction:@selector(slideTile:)];
			}

  /* add the tile to the tileBox */
	for (row=0; row<NumTilesY; row++) 
		for (col=0; col<NumTilesX; col++) {
			[tileBox addSubview:tile[row * NumTilesX + col]];
			}
	[tile[NumTilesX*NumTilesY-1] removeFromSuperview];	// remove one for the space

  /* add the invisible tiles to the tileBox (added last so they are on top) */
	for (row=0; row<NumTilesY; row++) 
		for (col=0; col<NumTilesX; col++) {
			[tileBox addSubview:tileInPosition[row][col].invisibleID];
			}
	[tileSlideCV addSubview:tileBox];
	tmpstr = NXGetDefaultValue ([NXApp appName], "BoardLocation");
    if (sscanf(tmpstr, "%d %d", &x,&y) != 2) { x = 314; y = 314; }
	[slideWindowOut moveTo:(NXCoord)x :(NXCoord)y];	
	[self shuffle:mixPattern];
	[self randomMix:self];
	[self sizeSlideBoard];
	[slideWindowOut orderFront:self];

  /* register the tile slide window with the workspace (to accept pictures) */
  /* (see Controller.m from Acceptor demo app) */
    listener = [Listener new];
    [listener setDelegate:self];
    [listener usePrivatePort];
    [listener addPort];
    NXConvertWinNumToGlobal([slideWindowOut windowNum], &windowNum);
    [speaker setSendPort:NXPortFromName(NX_WORKSPACEREQUEST, NULL)];
    [speaker registerWindow:windowNum toPort:[listener listenPort]];
  /* enable the soundGenerator object (get the DSP) */
	if ([soundGenerator enable] == NO) {
		NXRunAlertPanel(NULL,"Can't open the DSP.", "That's life.", NULL, NULL);
		[soundSwitch setIntValue:0];
		}

	return self;
}

- appDidHide:app
// responds as application's delegate
{
	[soundGenerator disable];
	if ([infoPanelOut isVisible] == YES)	// if info panel is open, animation is happening
		DPSRemoveTimedEntry (infoTimer);	// STOP IT!
	return self;
}

- appDidUnhide:app
// responds as application's delegate
{
	void infoAnimatorTimeout();
	if ([soundGenerator enable] == NO) {
		NXRunAlertPanel(NULL,"Can't open the DSP.", "That's life.", NULL, NULL);
		[soundSwitch setIntValue:0];
		}
	if ([infoPanelOut isVisible] == YES)	// if info panel is open, restart animation
		infoTimer = DPSAddTimedEntry((double) INFOTIMERPERIOD, &infoAnimatorTimeout, self, NX_BASETHRESHOLD);
	return self;
}

- init
// Initialize a few variables.  Some of these things could be changed while
// running if a user interface to them is added (perhaps a preferrences panel)
{
	[super init];
	movementSteps = 3;	// number of animation steps when moving tiles
	currentStep = 0;
	slidePeriod = .01; 	// seconds of delay between animation frames
	topMargin = 15;		// does not include window's titlebar
	bottomMargin = 53;	// between sizebar and bottom of tilebox
	leftMargin = 10;
	rightMargin = 15;
	interTileDistance = 2;
	buttonBorderWidth = 3;	// lines which make up border of tile buttons
	pictureType = TYPENONE;
	infoTimer = NULL;
	mixPattern = RANDOMFLAG;
	solved = NO;
	
	return self;
}

- free
{
	int i,row,col;
	[picture free];
	[tileBox free];
	[clearBox free];
	[myFontManager free];
	for (i=0; i<(MAXSIZEX*MAXSIZEY); i++) 
		[tile[i] free];
	for (row=0; row<MAXSIZEY; row++) 
		for (col=0; col<MAXSIZEX; col++) 
			[tileInPosition[row][col].invisibleID free];
	return [super free];
}

- openInfoPanel:sender
// show info panel and start info panel animation
{
	void infoAnimatorTimeout();
	[infoPanelOut orderFront:self];
	if (!infoTimer)
		infoTimer = DPSAddTimedEntry((double) INFOTIMERPERIOD, &infoAnimatorTimeout, self, NX_BASETHRESHOLD);
	return self;
}

- windowWillClose:sender
// respond as delegate for info panel
{
	DPSRemoveTimedEntry (infoTimer);
	infoTimer = NULL;

	return self;
}

- windowDidMove:sender;
// save new board position in defaults database
{
	NXRect theRect;
	char str[20];
	
	[slideWindowOut getFrame:(NXRect *)&theRect];
	sprintf (str, "%d %d",(int)theRect.origin.x,(int)theRect.origin.y);
	NXWriteDefault ([NXApp appName], "BoardLocation", str);
	return self;
}

void infoAnimatorTimeout (DPSTimedEntry timedEntry, double timeNow, void *data)
// dummy method to invoke changeInfoPanel method in response to info panel animation timeout
{
    [(id)data changeInfoPanel];
}

- (void)changeInfoPanel
// Move buttons on info panel to create animation!!
{
	NXRect contentViewFrame;
	
	[infoPanelCV getFrame:&contentViewFrame];
	[stripe1Blanker moveTo:(NXCoord)stripe1Positionx:(NXCoord)stripe1Positiony];
	if (stripe1Positiony+stripe1Movement < 5)
		stripe1Movement = -stripe1Movement;
	if (stripe1Positiony+stripe1Movement+33 > contentViewFrame.size.height)
		stripe1Movement = -stripe1Movement;
	stripe1Positiony += stripe1Movement;
	[stripe1 moveTo:(NXCoord)stripe1Positionx :(NXCoord)stripe1Positiony];
	[stripe1Blanker display];
	[stripe1 display];

	[stripe2Blanker moveTo:(NXCoord)stripe2Positionx:(NXCoord)stripe2Positiony];
	if (stripe2Positiony+stripe2Movement < 5)
		stripe2Movement = -stripe2Movement;
	if (stripe2Positiony+stripe2Movement+33 > contentViewFrame.size.height)
		stripe2Movement = -stripe2Movement;
	stripe2Positiony += stripe2Movement;
	[stripe2 moveTo:(NXCoord)stripe2Positionx :(NXCoord)stripe2Positiony];
	[stripe2Blanker display];
	[stripe2 display];
	return;
}

- infoPanelNotes:sender
// plays a random 2-note chord when a button on info panel is pressed
{
	int key;
	
	key = random() % NUMBEROFBARS;


	return self;
}

- openBackground:sender
// select background to load via open panel
{
	char *theFile,*theDirectory;
	const char *const *returnedFileList;
	char **fileTypeList;
	char *typeList[3];
	NXSize theSize;
	int length;
	
	typeList[0] = "tiff";
	typeList[1] = "eps";
	typeList[2] = 0;
	fileTypeList = typeList;
	
	myOpenPanel = [OpenPanel new];
	[myOpenPanel allowMultipleFiles:(BOOL)NO];
	if ([myOpenPanel runModalForTypes:(const char *const *)fileTypeList] == 0) 
		return self;	// CANCEL selected
	returnedFileList = [myOpenPanel filenames];
	if (returnedFileList == NULL) return self;
	theFile = (char *) *returnedFileList;
	theDirectory = (char *) [myOpenPanel directory];
	length = (strlen(theFile) + strlen(theDirectory)+1);
    if (filePathLength <= length) {
		if (filePath) {
			free(filePath);
			}
		filePath = (char *)malloc(length + 1);
		filePathLength = length;
		}

	strcpy(filePath, theDirectory);
	strcat(filePath, "/");
	strcat(filePath, theFile);

	if ([self initAndCheckPicture:(char *)filePath] == NO)
		return self;	// selected file is not loadable
	else {
		if (usePictures == NO){
			usePictures = YES;
			[usePicturesSwitch setIntValue:1];
			NXWriteDefault ([NXApp appName], "UsePictures", "YES");
			}
		[picture getSize:&theSize];
		[picture getSize:(NXSize *)&theSize];
		if ((theSize.height >= NUMBERMINHEIGHT) && (theSize.width >= NUMBERMINWIDTH)) {
			[useNumbersSwitch setEnabled:YES];
			if (useNumbers == YES)
				[usePicturesSwitch setEnabled:YES];
			}
		else {			// picture too small to allow using numbers
			useNumbers = NO;
			[useNumbersSwitch setEnabled:NO];
			[useNumbersSwitch setIntValue:0];
			[usePicturesSwitch setEnabled:NO];
			}
			[self sizeSlideBoard];
		}
	return self;
}

- togglePictures:sender
// Toggles pictures on and off. Receives action messages from the "Picture" switch.
{
	if ([sender intValue] == 1) {
		usePictures = YES;
		NXWriteDefault ([NXApp appName], "UsePictures", "YES");
		[useNumbersSwitch setEnabled:YES];
		}
	else {
		usePictures = NO;
		NXWriteDefault ([NXApp appName], "UsePictures", "NO");
		[useNumbersSwitch setEnabled:NO];
		};
	[self sizeSlideBoard];
		
	return self;
}

- toggleNumbers:sender
// Toggles visibility of numbers on and off.
// Receives action messages from the "Numbers" switch.
{
	if ([sender intValue] == 1) {
		useNumbers = YES;
		NXWriteDefault ([NXApp appName], "UseNumbers", "YES");
		if (pictureLoaded == YES)
			[usePicturesSwitch setEnabled:YES];
		}
	else {
		useNumbers = NO;
		NXWriteDefault ([NXApp appName], "UseNumbers", "NO");
		[usePicturesSwitch setEnabled:NO];
		};
	[self sizeSlideBoard];
	
	return self;
}

- sizeSlideBoard
// Draws the tile board at the correct size, with the NumTilesX * NumTilesY tiles
// Determines the size from the current size of "picture" if the "Picture" switch
// is checked, and from the size of the window if not.
{
	NXRect tempRect;
	NXSize tempSize;
	NXPoint tempPoint;
	id tempImage,newFont;
	int	i,row,col,minSize;
	float tempFontSize;
	
	if (usePictures == YES) {
		[slideWindowOut disableFlushWindow];
		[picture getSize:&tempSize];
		sizex = (int)((tempSize.width/NumTilesX)-buttonBorderWidth);
		sizey = (int)((tempSize.height/NumTilesY)-buttonBorderWidth);
		tempSize.width = (NXCoord)sizex;
		tempSize.height = (NXCoord)sizey;
		tempRect.size = tempSize;
		if (sizex>sizey) minSize = sizey;
		else minSize = sizex;
		tempFontSize = (float)((minSize-5+buttonBorderWidth)/1.6);
		newFont = [myFontManager findFont:(const char *)"Helvetica" traits:
					(NXFontTraitMask)NX_UNBOLD weight:(int)0 size:tempFontSize];
		for (row=0; row<NumTilesY; row++) 
			for (col=0; col<NumTilesX; col++){
				tempPoint.x = (NXCoord)((sizex+buttonBorderWidth) * col);
				tileInPosition[row][col].xPosition = tempPoint.x;
				tempPoint.y = (NXCoord)((sizey+buttonBorderWidth) * (NumTilesY-row-1));
				tileInPosition[row][col].yPosition = tempPoint.y;
				tempRect.origin = tempPoint;
				tempImage = [[NXImage alloc] initFromImage:(NXImage *)picture rect:(const NXRect *)&tempRect];
				[tile[row*NumTilesX+col] setImage:tempImage];

				[tile[row*NumTilesX+col] sizeTo:(NXCoord)(sizex+buttonBorderWidth)
					:(NXCoord)(sizey+buttonBorderWidth)];
				[tile[tileInPosition[row][col].tileNumber] moveTo:
					(tileInPosition[row][col].xPosition) :(tileInPosition[row][col].yPosition)];
				[tileInPosition[row][col].invisibleID moveTo:
					(tileInPosition[row][col].xPosition) :(tileInPosition[row][col].yPosition)];
				[tileInPosition[row][col].invisibleID sizeTo:
					(NXCoord)(sizex+buttonBorderWidth) :(NXCoord)(sizey+buttonBorderWidth)];
				if (useNumbers == YES)
					[tile[row*NumTilesX+col] setIconPosition:NX_ICONOVERLAPS];
				else
					[tile[row*NumTilesX+col] setIconPosition:NX_ICONONLY];
				}
		[clearBox sizeTo:(NXCoord)(sizex+buttonBorderWidth) :(NXCoord)(sizey+buttonBorderWidth)];
		[tileBox setBorderType:NX_BEZEL]; 
		[tileBox sizeToFit];
		[tileBox getFrame:&tempRect];
		windowSize.width = (NXCoord)(tempRect.size.width + leftMargin + rightMargin);
		windowSize.height = (NXCoord)(tempRect.size.height + topMargin + bottomMargin);
		[slideWindowOut  sizeWindow:windowSize.width :windowSize.height];
		[tileBox moveTo:(NXCoord)leftMargin :(NXCoord)bottomMargin];

		// I don't think the display in the following loop should be necessary, but set font
		// doesn't seem to work unless you do a display after setting icon position
		if (useNumbers == YES) {
			for (i=0; i<(MAXSIZEX*MAXSIZEY); i++) {
				[tile[i] display]; 
				[tile[i] setFont:newFont];
				}
			}
			
		[slideWindowOut reenableFlushWindow];
		[slideWindowOut display];
		}

	else {
		[slideWindowOut disableFlushWindow];
		[tileSlideCV getFrame:&tempRect];
		sizex = (int) ((tempRect.size.width - leftMargin - rightMargin -((NumTilesX-1) * interTileDistance))/NumTilesX);
		sizey = (int) ((tempRect.size.height - topMargin - bottomMargin -((NumTilesY-1) * interTileDistance))/NumTilesY);
	
		for (row=0; row<NumTilesY; row++) 
			for (col=0; col<NumTilesX; col++){
				tileInPosition[row][col].xPosition =(NXCoord)((sizex+interTileDistance) * col);
				tileInPosition[row][col].yPosition =(NXCoord)((sizey+interTileDistance) * (NumTilesY-row-1));
				[tile[tileInPosition[row][col].tileNumber] moveTo:
					(tileInPosition[row][col].xPosition) :(tileInPosition[row][col].yPosition)];
				[tileInPosition[row][col].invisibleID moveTo:
					(tileInPosition[row][col].xPosition) :(tileInPosition[row][col].yPosition)];
				[tile[tileInPosition[row][col].tileNumber] sizeTo:(NXCoord)sizex :(NXCoord)sizey];
				[tileInPosition[row][col].invisibleID sizeTo:(NXCoord)sizex :(NXCoord)sizey];
				}
		if (sizex>sizey) minSize = sizey;
		else minSize = sizex;
		for (i=0; i<(MAXSIZEX*MAXSIZEY); i++) {
			[tile[i] setIconPosition:NX_TITLEONLY];
			[tile[i] display];
			[tile[i] setFont:[myFontManager findFont:(const char *)"Helvetica" traits:(NXFontTraitMask)NX_UNBOLD weight:(int)0 size:(float)((minSize-5)/1.6)]];
			}
	
		[clearBox sizeTo:(NXCoord)sizex :(NXCoord)sizey];
		[tileBox setBorderType:NX_BEZEL]; 
		[tileBox sizeToFit];
		[tileBox moveTo:(NXCoord)leftMargin :(NXCoord)bottomMargin];
		[slideWindowOut reenableFlushWindow];
		[slideWindowOut display];
		}
	
	return self;	
}

- slideTile:sender
// Receives action messages from the invisible buttons over the tiles
// Starts sliding animation timer if sliding is to be done. 
{
	void slideTimeout ();
	int row,col;
	
	if (solved == YES) return self;
	if (sliding == YES) return self;
	row = (int) [sender tag]/MAXSIZEX;
	col = (int) [sender tag]-(row*MAXSIZEX);
	if ((rowOfSpace != row) && (colOfSpace != col)) return self;
	if ((rowOfSpace == row) && (colOfSpace == col)) return self;


	if (rowOfSpace == row) {
		slideRowOrCol = ROW;
		howManyToMove = colOfSpace - col;
		}
	if (colOfSpace == col) { 
		slideRowOrCol = COL;
		howManyToMove = rowOfSpace - row;
		}
	numberOfMoves++;
	[movesOut setIntValue:numberOfMoves];

	sliding = (BOOL)YES;
	fromRow = row;
	fromCol = col;
	slideTimer = DPSAddTimedEntry((double) slidePeriod, &slideTimeout, self, NX_BASETHRESHOLD);

    return self;
}

void slideTimeout (DPSTimedEntry timedEntry, double timeNow, void *data)
// Dummy method to invoke slideABit method in response to tile sliding timeout
{
    [(id)data slideABit];
}

- (void) slideABit
// Slide tiles one step; re-arrange position of tiles when done sliding.
// Check for puzzle solved, congratulate and re-shuffle if it is
{
	int	distance,i,direction;
	int row,col,difficulty;
	char str[100];
	
	currentStep++;
	if (howManyToMove < 0) 
		direction = (-1);
	else 
		direction = 1;

	if (currentStep == movementSteps) {		// done sliding; re-arrange position of tiles
		DPSRemoveTimedEntry (slideTimer);
		sliding = (BOOL)NO;
		if (slideRowOrCol == ROW) {
			for (i=(fromCol + howManyToMove);i!= fromCol;i=i-direction) {
				[tile[tileInPosition[fromRow][i-direction].tileNumber] addSubview:clearBox];
				[clearBox display];
				[clearBox removeFromSuperview];
				tileInPosition[fromRow][i].tileNumber = tileInPosition[fromRow][i-direction].tileNumber;
				[tile[tileInPosition[fromRow][i].tileNumber] moveTo:
					(tileInPosition[fromRow][i].xPosition) :(tileInPosition[fromRow][i].yPosition)];
				[tile[tileInPosition[fromRow][i].tileNumber] display];
				}
			}
		else {
			for (i=(fromRow + howManyToMove);i!= fromRow;i=i-direction) {
				[tile[tileInPosition[i-direction][fromCol].tileNumber] addSubview:clearBox];
				[clearBox display];
				[clearBox removeFromSuperview];
				tileInPosition[i][fromCol].tileNumber = tileInPosition[i-direction][fromCol].tileNumber;
				[tile[tileInPosition[i][fromCol].tileNumber] moveTo:
					(tileInPosition[i][fromCol].xPosition) :(tileInPosition[i][fromCol].yPosition)];
				[tile[tileInPosition[i][fromCol].tileNumber] display]; 
				}
			}
		tileInPosition[fromRow][fromCol].tileNumber = NumTilesX*NumTilesY-1;
		// check for puzzle solved
		solved = YES;
		for (row=0; row<NumTilesY; row++) 
			for (col=0; col<NumTilesX; col++)
				if (tileInPosition[row][col].tileNumber != (row * NumTilesX + col)) solved = NO;
		rowOfSpace = fromRow;
		colOfSpace = fromCol;
		if (solved == YES) {
			[tileInPosition[NumTilesY-1][NumTilesX-1].invisibleID removeFromSuperview];
			[tileBox addSubview:tile[NumTilesX*NumTilesY-1]];
			[tileBox addSubview:tileInPosition[NumTilesY-1][NumTilesX-1].invisibleID];
			[tile[NumTilesX*NumTilesY-1] moveTo:(tileInPosition[NumTilesY-1][NumTilesX-1].xPosition)
				:(tileInPosition [NumTilesY-1][NumTilesX-1].yPosition)];
			[tile[NumTilesX*NumTilesY-1] display];
			[self playFinishMusic];
			difficulty = NumTilesX-3;
			if ((mixPattern != RANDOMFLAG) && (numberOfMoves < bests[difficulty][mixPattern])) {
				bests[difficulty][mixPattern] = numberOfMoves;
				[[bestsMatrixOut cellAt:difficulty:mixPattern]
					setIntValue:bests[difficulty][mixPattern]];
				[[bestsMatrixOut cellAt:difficulty:mixPattern] setBackgroundGray:(float)NX_WHITE];
				[bestsPanelOut orderFront:self];
				NXRunAlertPanel("Congratulations!",
					"You have set a new 'best'!", "Yippee!", NULL, NULL);
				switch (difficulty) {
					case 0:
						sprintf (str, "%d %d %d %d\0", 
							bests[0][0], bests[0][1], bests[0][2], bests[0][3]);
						NXWriteDefault ([NXApp appName], "EasyBests", str);
						break;
					case 1:
						sprintf (str, "%d %d %d %d\0", 
							bests[1][0], bests[1][1], bests[1][2], bests[1][3]);
						NXWriteDefault ([NXApp appName], "NormalBests", str);
						break;
					case 2:
						sprintf (str, "%d %d %d %d\0", 
							bests[2][0], bests[2][1], bests[2][2], bests[2][3]);
						NXWriteDefault ([NXApp appName], "HardBests", str);
						break;
					}
				[[bestsMatrixOut cellAt:difficulty:mixPattern] setBackgroundGray:(float)NX_LTGRAY];
				}
			}
		currentStep = 0;
		}
	else
		if (slideRowOrCol == ROW) {		// not done sliding; move tiles one step
			distance = (int) (((float)currentStep/(float)movementSteps) * sizex * direction);
			for (i=(fromCol + howManyToMove-direction);i!= fromCol-direction;i=i-direction) {
				[tile[tileInPosition[fromRow][i].tileNumber] addSubview:clearBox];
				[clearBox display];
				[clearBox removeFromSuperview];
				[tile[tileInPosition[fromRow][i].tileNumber] moveTo:
					(NXCoord)(tileInPosition[fromRow][i].xPosition + distance) :(tileInPosition[fromRow][i].yPosition)];
				[tile[tileInPosition[fromRow][i].tileNumber] display]; 
				}
			}
		else {
			distance = (int) (((float)currentStep/(float)movementSteps)*sizey*direction * -1);
			for (i=(fromRow + howManyToMove-direction);i!= fromRow-direction;i=i-direction) {
				[tile[tileInPosition[i][fromCol].tileNumber] addSubview:clearBox];
				[clearBox display];
				[clearBox removeFromSuperview];
				[tile[tileInPosition[i][fromCol].tileNumber] moveTo:
					(tileInPosition[i][fromCol].xPosition) :(NXCoord)(tileInPosition[i][fromCol].yPosition + distance)];
				[tile[tileInPosition[i][fromCol].tileNumber] display]; 
				}
			}
	return;
} 

- playFinishMusic
// plays the run of notes when the puzzle is solved
{
	


	return self;
}

- selectSize:sender
// Receives action messages from the "Difficulty" menu cells
// Selects x and y size of tile board (currently the same) and invokes "shuffle:" 
{
	int	row,col;
    char str[3];

	for (row=0; row<NumTilesY; row++) 
		for (col=0; col<NumTilesX; col++){
			[tile[tileInPosition[row][col].tileNumber] removeFromSuperview];
			[tileInPosition[row][col].invisibleID removeFromSuperview];
			}
    [tileBox setBorderType:NX_NOBORDER];		// clear old board 
	[tileSlideCV display];

    // set number of tiles 
	NumTilesX = [[sender selectedCell] tag];
	NumTilesY = NumTilesX;

    sprintf (str, "%d\0", NumTilesX);
    NXWriteDefault ([NXApp appName], "Difficulty", str);

	for (row=0; row<NumTilesY; row++) 
		for (col=0; col<NumTilesX; col++) {
			tileInPosition[row][col].tileNumber = (row * NumTilesX)+ col;
			[tileBox addSubview:tile[tileInPosition[row][col].tileNumber]];
			}
	[tile[NumTilesX*NumTilesY-1] removeFromSuperview];
	for (row=0; row<NumTilesY; row++) 		// invisible buttons added last so they are on top!
		for (col=0; col<NumTilesX; col++) {
			[tileBox addSubview:tileInPosition[row][col].invisibleID];
			}
	[tile[NumTilesX*NumTilesY-1] removeFromSuperview];

	if (mixPattern != RANDOMFLAG) {
		[self shuffle:mixPattern];
		[self sizeSlideBoard];
		[slideWindowOut orderFront:self];
		}
	else {
		[self shuffle:mixPattern];
		[self randomMix:self];
		}
	return self;
}

- shuffle:(int)pattern
// shuffle the board (to a default shuffled pattern) and reset numberOfMoves
// Currently, this only handles 3*3, 4*4, and 5*5 boards
{
	static int scrambledSize5[NUMOFCHALLENGES][25]=	
		{{24,10,8,22,5,7,15,16,20,23,17,12,2,14,6,3,13,4,18,1,0,19,11,9,21},
		{24,10,14,13,22,9,21,1,3,11,4,8,16,23,18,2,6,20,15,17,0,7,19,5,12},
		{24,19,14,9,4,23,18,13,8,3,22,17,12,7,2,21,16,11,6,1,20,15,10,5,0},
		{24,23,22,21,20,19,18,17,16,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0}};
	static int scrambledSize4[NUMOFCHALLENGES][16]=
		{{15,14,3,13,0,9,11,4,10,7,5,2,8,1,6,12},
		{15,3,5,13,1,11,12,2,4,10,0,8,6,14,9,7},
		{15,11,7,3,14,10,6,2,13,9,5,1,12,8,4,0},
		{15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0}};
	static int scrambledSize3[NUMOFCHALLENGES][9]=
		{{8,0,4,2,5,3,6,1,7},
		{8,1,5,6,4,3,0,2,7},
		{8,5,2,6,4,1,7,3,0},
		{8,1,5,7,4,6,2,3,0}};
	int row,col,patternUsed;

	patternUsed = pattern % NUMOFCHALLENGES;
    switch (NumTilesX) {
		case 3:
			for (row=0;row<NumTilesY;row++)
				for (col=0;col<NumTilesX;col++)
					tileInPosition[row][col].tileNumber = 
						(int)scrambledSize3[patternUsed][col + (row *NumTilesX)];
			break;
		case 4:
			for (row=0;row<NumTilesY;row++)
				for (col=0;col<NumTilesX;col++)
					tileInPosition[row][col].tileNumber = 
						(int)scrambledSize4[patternUsed][col + (row *NumTilesX)];
			break;
		case 5:
			for (row=0;row<NumTilesY;row++)
				for (col=0;col<NumTilesX;col++)
					tileInPosition[row][col].tileNumber = 
						(int)scrambledSize5[patternUsed][col + (row *NumTilesX)];
			break;
		}
	rowOfSpace = 0;
	colOfSpace = 0;

	numberOfMoves = 0;
	[movesOut setIntValue:numberOfMoves];
	solved = NO;

	return self;
}

- challengeMix:sender
// select next pattern, call shuffle, change window title, and redraw
// target of the 'Challenge Mix' menu item
{
	char		buffer[30],bufferNum[5];

	[tile[NumTilesX*NumTilesY-1] removeFromSuperview];
	solved = NO;
	if (mixPattern == RANDOMFLAG)
		mixPattern = NUMOFCHALLENGES;	// wraps around when used
	mixPattern ++;
	if (mixPattern >= NUMOFCHALLENGES) mixPattern = 0;

	[self shuffle:mixPattern];
	strcpy(buffer,"Tile Slide - Challenge#");
	sprintf(bufferNum," %d", (mixPattern+1));
	strcat(buffer,bufferNum);
	[slideWindowOut setTitle:(const char *)buffer];
	[self sizeSlideBoard];
	[slideWindowOut orderFront:self];

	return self;
}

- randomMix:sender
// make 100 random slides, change window title, and redraw
// target of the 'Random Mix' menu item
{
	int	i,dir,tempRow,tempCol;
	int tempTileNum;
	static int moves[4][2] = {{0,1},{1,0},{0,-1},{-1,0}};
	char		buffer[30];
	
	[tile[NumTilesX*NumTilesY-1] removeFromSuperview];
	solved = NO;
	mixPattern = RANDOMFLAG;
	for (i=0;i<100;i++) {
		dir = random() % 4;
		while ((rowOfSpace+moves[dir][1] >= NumTilesY) || (rowOfSpace+moves[dir][1] < 0) ||
			(colOfSpace+moves[dir][0] >= NumTilesX)	|| (colOfSpace+moves[dir][0] < 0))
			dir = random() % 4;
		tempRow = rowOfSpace+ moves[dir][1];
		tempCol = colOfSpace+moves[dir][0];
		tempTileNum = tileInPosition[tempRow][tempCol].tileNumber;
		tileInPosition[tempRow][tempCol].tileNumber = tileInPosition[rowOfSpace][colOfSpace].tileNumber;
		tileInPosition[rowOfSpace][colOfSpace].tileNumber = tempTileNum;
		rowOfSpace = tempRow;
		colOfSpace = tempCol;
		}
	strcpy(buffer,"Tile Slide - Random");
	[slideWindowOut setTitle:(const char *)buffer];
	numberOfMoves = 0;
	[movesOut setIntValue:numberOfMoves];
	[self sizeSlideBoard];
	[slideWindowOut orderFront:self];
	return self;
}

- (int)iconEntered:(int)windowNum at:(double)x :(double)y
    iconWindow:(int)iconWindowNum iconX:(double)iconX iconY:(double)iconY
    iconWidth:(double)iconWidth iconHeight:(double)iconHeight
    pathList:(char *)pathList
// respond as delegate for main window
{
    char	*stringPosition;
    int		length;

  /* save the file's path for later */
    length = strlen(pathList);
    if (filePathLength <= length) {
		if (filePath) {
			free(filePath);
			}
		filePath = (char *)malloc(length + 1);
		filePathLength = length;
		}
    strcpy(filePath, pathList);
    stringPosition = filePath;
    
  /* the number of tabs + 1 equals the number of files dragged in */
    files = 1;
	while (stringPosition = index(stringPosition, '\t')) {
		files++;
		stringPosition++;
		}
	
	return 0;	
}

- (int)iconReleasedAt:(double)x :(double)y ok:(int *)flag
// respond as delegate for main window
{
	NXSize theSize;
	
	if (files!=1) {
		*flag = 0;	// tell Workspace to animate the ICON back to its source window
		return 0;
		}
	
	if ([self initAndCheckPicture:(char *)filePath] == NO) {
		*flag = 0;	// do not accept the icon
		}
	else {
		*flag = 1;	// accept the icon
		if (usePictures == NO){
			usePictures = YES;
			[usePicturesSwitch setIntValue:1];
			NXWriteDefault ([NXApp appName], "UsePictures", "YES");
			}
		[picture getSize:&theSize];
		[picture getSize:(NXSize *)&theSize];
		if ((theSize.height >= NUMBERMINHEIGHT) && (theSize.width >= NUMBERMINWIDTH)) {
			[useNumbersSwitch setEnabled:YES];
			if (useNumbers == YES)
				[usePicturesSwitch setEnabled:YES];
			}
		else 
		{							// picture too small to allow using numbers
			useNumbers = NO;
			[useNumbersSwitch setEnabled:NO];
			[useNumbersSwitch setIntValue:0];
			[usePicturesSwitch setEnabled:NO];
			}
			[self sizeSlideBoard];
		}
	return 0;		
}

- (BOOL)initAndCheckPicture:(char *)pictureName
// Checks out the dragged in icon.  Makes sure it is tiff or eps 
// and resizes if too big or small.  Returns YES if it loads a picture.
{
	NXStream *input;
	id newPicture;
	NXSize theSize,tempSize;
	NXRect tempRect;
	int tempPictureType;
	char *rightmostPeriod;

	if (pictureName == NULL) return NO;
	rightmostPeriod = strrchr(pictureName, '.');
	if (rightmostPeriod == NULL) return NO;
	tempPictureType = TYPENONE;
	if (NXOrderStrings((unsigned char *) ".tiff",(unsigned char *) rightmostPeriod, YES, 5, NULL) == 0) 
		tempPictureType = TYPETIFF;
	if (NXOrderStrings((unsigned char *) ".eps",(unsigned char *) rightmostPeriod, YES, 4, NULL) == 0)
		tempPictureType = TYPEEPS;
	if (tempPictureType == TYPENONE) return NO;		// unsuccessful load

	input = NXMapFile(pictureName, NX_READONLY);
	if (input == NULL) return NO;		// unsuccessful load

	newPicture = [[NXImage alloc] initFromStream:input];
	[newPicture getSize:(NXSize *)&theSize];

  /* if a tiff picture is too small, it is rejected */
  /* (We could just resize it, as we do for eps pictures, */
  /*   but I don't like the look of resized tiffs anyways.) */ 
	if (((theSize.height < IMAGEMINHEIGHT) || (theSize.width < IMAGEMINWIDTH))
			&& (tempPictureType == TYPETIFF))
		{
		[newPicture free];
		NXCloseMemory(input,NX_FREEBUFFER);
		return NO;		// unsuccessful load
		}

  /* we now know we have a valid picture, so get rid of the old one */
	[picture free];
	picture = newPicture;

  /* if a tiff picture is too large, use only the bottom left section */
	if (((theSize.height > IMAGEMAXHEIGHT) || (theSize.width > IMAGEMAXWIDTH))
			&& (tempPictureType == TYPETIFF))
		{
		if (theSize.width > IMAGEMAXWIDTH)
			tempRect.size.width = IMAGEMAXWIDTH;
		else
			tempRect.size.width = theSize.width;
		if (theSize.height > IMAGEMAXHEIGHT)
			tempRect.size.height = IMAGEMAXHEIGHT;
		else
			tempRect.size.height = theSize.height;
		tempRect.origin.x = 0;
		tempRect.origin.y = 0;
		picture = [[NXImage alloc] initFromImage:(NXImage *)newPicture rect:(const NXRect *)&tempRect];
		}
	
  /* if an eps picture is too small, we resize it */
	if (((theSize.height < IMAGEMINHEIGHT) || (theSize.width < IMAGEMINWIDTH))
			&& (tempPictureType == TYPEEPS))
		{
		if (theSize.width < IMAGEMINWIDTH)
			tempSize.width = IMAGEMINWIDTH;
		else
			tempSize.width = theSize.width;
		if (theSize.height < IMAGEMINHEIGHT)
			tempSize.height = IMAGEMINHEIGHT;
		else
			tempSize.height = theSize.height;
		[newPicture setSize:&tempSize];
		}

  /* if an eps picture is too large, we resize it */
	if (((theSize.height > IMAGEMAXHEIGHT) || (theSize.width > IMAGEMAXWIDTH))
			&& (tempPictureType == TYPEEPS))
		{
		if (theSize.width > IMAGEMAXWIDTH)
			tempSize.width = IMAGEMAXWIDTH;
		else
			tempSize.width = theSize.width;
		if (theSize.height > IMAGEMAXHEIGHT)
			tempSize.height = IMAGEMAXHEIGHT;
		else
			tempSize.height = theSize.height;
		[newPicture setSize:&tempSize];
		}

  /* set eps pictures to be scalable */
	if (tempPictureType == TYPEEPS) {
		[picture setScalable:(BOOL)YES]; 
		[picture setDataRetained:(BOOL)YES]; 
		} 

	pictureType = tempPictureType;
	pictureLoaded = YES;
	NXCloseMemory(input,NX_FREEBUFFER);
	NXWriteDefault ([NXApp appName], "Picture", filePath);
	return YES;
}

- windowWillResize:sender toSize:(NXSize *)frameSize
// Acts as delegate for main window confining window resizing
{
	if ( useNumbers == YES) 
		{
		if (frameSize->width < (NXCoord)(NUMBERMINWIDTH+leftMargin+rightMargin+2))
			frameSize->width = (NXCoord)(NUMBERMINWIDTH+leftMargin+rightMargin+2);
		if (frameSize->height < (NXCoord)(NUMBERMINHEIGHT+23+9+topMargin+bottomMargin))
			frameSize->height = (NXCoord)(NUMBERMINHEIGHT+23+9+topMargin+bottomMargin);
		}
	if ( usePictures == YES) 
		if (pictureType == TYPEEPS) {
			if (frameSize->width < (NXCoord)(IMAGEMINWIDTH+leftMargin+rightMargin+2))
				frameSize->width = (NXCoord)(IMAGEMINWIDTH+leftMargin+rightMargin+2);
			if (frameSize->height < (NXCoord)(IMAGEMINHEIGHT+23+9+topMargin+bottomMargin))
				frameSize->height = (NXCoord)(IMAGEMINHEIGHT+23+9+topMargin+bottomMargin);
			if (frameSize->width > (NXCoord)(IMAGEMAXWIDTH+leftMargin+rightMargin+2))
				frameSize->width = (NXCoord)(IMAGEMAXWIDTH+leftMargin+rightMargin+2);
			if (frameSize->height > (NXCoord)(IMAGEMAXHEIGHT+23+9+topMargin+bottomMargin))
				frameSize->height = (NXCoord)(IMAGEMAXHEIGHT+23+9+topMargin+bottomMargin);
			}
		else {		// I don't like the look of resized tiffs, so don't allow resizing
			frameSize->width = (windowSize.width+2);
			frameSize->height = (windowSize.height+23+9);
			}
	return self;
}

- windowDidResize:sender;
// Acts as delegate for main window. Enable or disable usePicturesSwitch and
// useNumbersSwitch according to new size.  Resize picture and redraw.
{
	NXRect theRect;
	NXSize theSize;
	
	[tileSlideCV getFrame:(NXRect *)&theRect];
	theSize.width = theRect.size.width - leftMargin - rightMargin;
	theSize.height = theRect.size.height - topMargin - bottomMargin;

	if ((theSize.height >= IMAGEMINHEIGHT) && (theSize.width >= IMAGEMINWIDTH) 
			&& (theSize.height <= IMAGEMAXHEIGHT) && (theSize.width <= IMAGEMAXWIDTH) 
			&& (pictureLoaded == YES) && (useNumbers == TRUE))
		[usePicturesSwitch setEnabled:YES];
	else 
		[usePicturesSwitch setEnabled:NO];

	if ((theSize.height >= NUMBERMINHEIGHT) && (theSize.width >= NUMBERMINWIDTH)
			&& (usePictures == TRUE))
		[useNumbersSwitch setEnabled:YES];
	else 
		[useNumbersSwitch setEnabled:NO];

	if ( usePictures == NO) {
		if (pictureType == TYPEEPS)
			[picture setSize:&theSize];
		[self sizeSlideBoard];
		}
	else 
		if (pictureType == TYPEEPS) {
			[picture setSize:&theSize];
			[self sizeSlideBoard];
			}
	
	return self;
}

@end

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