Racket [Scheme] / Cocoa Glue – Part IV

As promised, here’s my current unit tests for my Racket / cocoa glue code. It’s just tests for my conversion code.

#lang racket

(require rackunit
         "cocoa-glue.rkt"
         ffi/unsafe
         ffi/unsafe/objc)

(test-case 
 "string->NSString and back"
 (let ([strings (list "1" "two" "◻" "◻◾" "◻five◾")])
   (for ((s strings))
     (let ([converted-string (string->NSString s)])
       (check-not-equal? s converted-string)
       (check-equal? s (NSString->string converted-string))))))

(test-case 
 "list->NSArray"
 (let* ([strings (list "a" "b" "c")]
        [foreign-strings (map string->NSString strings)]
        [ns-array (list->NSArray foreign-strings)])
   (check-equal? (NSArray-length ns-array)
                 (length strings))
   (for ((i (in-naturals))
         (string strings))
     (check-equal? 
      string
      (NSString->string 
       (NSArray-ref ns-array i))))))

(test-case 
 "list->NSArray->list"
 (let ([strings (list "1" "two" "◻◾◻")])
   (check-equal? 
    strings
    (map NSString->string
         (NSArray->list 
          (list->NSArray
           (map string->NSString strings)))))))

(define-simple-check (check-dict/NSDictionary-equiv? dict NSDict)
  (check-equal? (tell-int32 NSDict count) 
                (dict-count dict))
    (for (((k dict-value) (in-dict dict)))
      (let ([NSDict-value (tell NSDict objectForKey: (string->NSString k))])
        (check-equal? 
         (NSString->string dict-value)
         (NSString->string NSDict-value)))))

(let* ([keys (list "a" "b" "c")]
       [string-values (list "1" "2" "3")]
       [foreign-values (map string->NSString string-values)]
       [alist (map cons keys foreign-values)])
  (test-case 
   "dictionary->NSDictionary"
   (check-dict/NSDictionary-equiv? 
    alist
    (dictionary->NSDictionary alist)))
  (test-case 
   "dictionary->NSMutableDictionary"
   (check-dict/NSDictionary-equiv? 
    alist
    (dictionary->NSMutableDictionary alist))))

Racket [Scheme] / Cocoa Glue – Part III

For the third installment of my glue code between Cocoa and Racket, here is some data type conversion code. It’s all in Racket, using the Objective-C FFI. It includes conversions for strings to NSString and back, as well as converting between lists and NSArray and Racket dictionaries to NSDictionary and NSMutableDictionary. Also included are some helper functions for accessing NSArrays.

Next installment will be my unit tests for this code, so get excited.

cocoa-glue-data-types.rkt:

#lang racket

(provide (all-defined-out))

(require ffi/unsafe
         ffi/unsafe/objc
         ffi/cvector)

(import-class NSString)
(import-class NSArray)
(import-class NSDictionary)
(import-class NSMutableDictionary)

(define-syntax-rule (define-typed-tell fn-id type-id)
  (define-syntax-rule (fn-id obj msg (... ...))
    (tell #:type type-id obj msg (... ...))))

(define-typed-tell tell-int32  _int32)
(define-typed-tell tell-string _string)

(define (objC-tagged p)
  (unless (cpointer-has-tag? p 'id)
    (cpointer-push-tag! p 'id))
  p)

(define (string->NSString str)
  (tell NSString stringWithUTF8String: #:type _string str))

(define (NSString->string ns-string)
  (tell-string ns-string UTF8String))

(define (cvector->NSArray vec)
  (tell NSArray 
        arrayWithObjects: #:type _pointer (cvector-ptr vec)
        count: #:type _int32 (cvector-length vec)))

(define (list->NSArray lst #:type (type _pointer))
  (let ([vec (list->cvector lst type)])
    (cvector->NSArray vec)))

(define (NSArray-length ar)
  (tell-int32 ar count))

(define (NSArray-ref ar n)
  (tell ar objectAtIndex: #:type _int32 n))

(define (NSArray->list array)
  (for/list ((n (in-range (NSArray-length array))))
    (NSArray-ref array n)))

(define (dict-keys dict)
  (dict-map dict (lambda (k v) k)))

(define (dict-values dict)
  (dict-map dict (lambda (k v) v)))

(define (dictionary->NSDictionary dict)
  (tell NSDictionary
        dictionaryWithObjects: (list->NSArray (dict-values dict))
        forKeys: (list->NSArray
                  (map string->NSString (dict-keys dict)))))

(define (dictionary->NSMutableDictionary d)
  (let ([mutable (tell NSMutableDictionary dictionaryWithCapacity: #:type _int32 (dict-count d))])
    (for (((key value) (in-dict d)))
      (tell mutable 
            setObject: value
            forKey: (string->NSString key)))
    mutable))

Racket [Scheme] / Cocoa Glue – Part II

Part two of my enthralling posts about working with Cocoa from Racket.

Below is more of the code from the C / Objective-C library. You will notice that I use a couple of macros to deal with the NSAutoReleasePools. It’s potentially too clever for it’s own good. This code deals with opening video files for reading, writing, adding frames and exporting to h.264. It also includes a function for getting the contents of the current opengl texture as an NSImage.

#define POOL_START() NSAutoreleasePool *GENSYM00 = [[NSAutoreleasePool alloc] init]
#define POOL_DRAIN() [GENSYM00 drain]
#define WITH_POOL(type, f) { POOL_START(); type v = f; POOL_DRAIN(); return v;}
#define WITH_POOL_VOID(f)  { POOL_START(); f; POOL_DRAIN(); }

extern NSImage * glue_get_viewport_NSImage()
{
   GLint viewPort[4];
   glGetIntegerv( GL_VIEWPORT, viewPort );
   
   int width   = viewPort[2];
   int height  = viewPort[3];
   
   NSBitmapImageRep *rep = [[[NSBitmapImageRep alloc]
                             initWithBitmapDataPlanes:nil
                             pixelsWide: width
                             pixelsHigh: height
                             bitsPerSample:8
                             samplesPerPixel:4
                             hasAlpha:YES
                             isPlanar:NO
                             colorSpaceName:NSCalibratedRGBColorSpace
                             bytesPerRow:0
                             bitsPerPixel:0] autorelease];
   
   glReadBuffer( GL_FRONT );
	glReadPixels( 0, 0, width, height, GL_RGBA, GL_UNSIGNED_INT_8_8_8_8_REV, [rep bitmapData] );   
   NSImage * image = glue_NSBitmapImageRep_to_NSImage( rep );
   [image setFlipped:YES];
   // need this to actually flip image?
   [image lockFocusOnRepresentation:rep]; 
   [image unlockFocus];
   return image;
}

extern void * glue_quicktime_movie_open_write( const char * outputFile )
{
   WITH_POOL( QTMovie*, [[QTMovie alloc] 
                         initToWritableFile: [NSString stringWithUTF8String: outputFile]
                         error: NULL] );
}

extern void * glue_quicktime_movie_open_read( const char * inputFile )
{
   WITH_POOL( QTMovie*, ([[QTMovie alloc] 
                         initWithAttributes: [NSDictionary dictionaryWithObjectsAndKeys:
                                              [NSString stringWithUTF8String: inputFile], 
                                              QTMovieFileNameAttribute,
                                              NO, QTMovieOpenAsyncOKAttribute,
                                              nil]
                         error: nil]) );
}

extern void glue_quicktime_movie_set_current_time( QTMovie * movie, long timeValue, long timeScale )
{
   [movie setCurrentTime: QTMakeTime( timeValue, timeScale)];
}

extern NSImage * glue_quicktime_movie_get_current_frame( QTMovie * movie )
{
   return [movie currentFrameImage];
}

extern long glue_quicktime_movie_get_duration( QTMovie * movie )
{
   QTTime t = [movie duration];
   return (1000 * t.timeValue) / t.timeScale;
}

extern void glue_quicktime_movie_write( QTMovie * movie )
{
   [movie updateMovieFile];
}

extern void glue_quicktime_movie_add_frame( QTMovie * movie, NSImage * img,
                                           int length, int lengthScale )
{
   [movie addImage: img 
       forDuration: QTMakeTime(length, lengthScale)
    withAttributes: [NSDictionary dictionaryWithObject: @"tiff"
                                                forKey: QTAddImageCodecType]];
}

extern void glue_quicktime_movie_add_frame_current( QTMovie * movie, int length, int lengthScale )
{
   WITH_POOL_VOID( glue_quicktime_movie_add_frame( movie, 
                                                  glue_get_viewport_NSImage(),
                                                  length, lengthScale ) );
}

extern void glue_quicktime_movie_export_mp4_264( QTMovie * movie, const char * filePath )
{
   WITH_POOL_VOID ( ([movie writeToFile: [NSString stringWithUTF8String: filePath]
                         withAttributes: [NSDictionary dictionaryWithObjectsAndKeys:
                                          [NSNumber numberWithBool:YES], QTMovieExport,
                                          [NSNumber numberWithLong:'M4VH'], QTMovieExportType, nil]] ) );
}

Here’s the pertinent pieces of the FFI interface for Racket:

(define-glue glue-quicktime-movie-open-write : _string -> _pointer)
(define-glue glue-quicktime-movie-open-read : _string -> _pointer)
(define-glue glue-quicktime-movie-get-duration : _pointer -> _int32)
(define-glue glue-quicktime-movie-set-current-time : _pointer _int32 _int32 -> _void)
(define-glue glue-quicktime-movie-get-current-frame : _pointer -> _pointer)
(define-glue glue-quicktime-movie-add-frame : _pointer _pointer _int32 _int32 -> _void)
(define-glue glue-quicktime-movie-write : _pointer -> _void)
(define-glue glue-quicktime-movie-add-frame-current : _pointer _int32 _int32 -> _void)
(define-glue glue-quicktime-movie-export-mp4-264 : _pointer _string -> _void)

Racket [Scheme] / Cocoa Glue

I’ve been working on some video apps in Racket on my Mac, but I want to use some of the OSX features like Core Image Filters and QTKit. Racket has really good foreign function interface to both C and Objective-C, but a few things fall through the cracks. For example, it does not seem possible to pass a structure on the stack from Racket into Objective-C, which is required for a number of the image and video related APIs. Some other things are just easier to write in Objective-C. So I’ve been working on a library that makes some of these things easier.

My library consists of three pieces. An Objective-C library with a C interface, a Racket FFI interface to that library, and some Racket wrappers around Objective-C to make the interface simpler. Here’s some pieces.

Below is the Objective-C code for the C Interface for some image conversion routines. The most useful piece of this is the conversion routines from CIImage to NSImage and back, which are useful when using the Core Image Filters.

extern int glue_image_width( NSImage * img )
{
   return [img size].width;
}

extern int glue_image_height( NSImage * img )
{
   return [img size].height;
}

extern GLuint glue_NSBitmapImageRep_to_texture( NSBitmapImageRep * rep )
{  
   GLuint targetTexture;
   
   glGenTextures( 1, &targetTexture );
   int width            = [rep pixelsWide];
   int height           = [rep pixelsHigh];
   GLenum format        = [rep hasAlpha] ? GL_RGBA : GL_RGB;
   int numPixelsInRow   = [rep bytesPerRow] / ([rep bitsPerPixel] >> 3);
   
   GLenum target = GL_TEXTURE_RECTANGLE_EXT;
   glBindTexture( target, targetTexture );
   
   glPixelStorei(GL_UNPACK_ROW_LENGTH, numPixelsInRow); 
   
   glTexImage2D( target, 0, GL_RGBA, width, height, 0, format, GL_UNSIGNED_BYTE, [rep bitmapData] );
   glTexParameterf( target, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
   glTexParameterf( target, GL_TEXTURE_MAG_FILTER, GL_LINEAR );
   
   glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
   return targetTexture;
}

extern NSBitmapImageRep * glue_NSImage_to_NSBitmapImageRep( NSImage * img )
{
   return [NSBitmapImageRep imageRepWithData: [img TIFFRepresentation]];
}

extern CIImage * glue_NSImage_to_CIImage( NSImage * ns )
{
   NSBitmapImageRep * rep = glue_NSImage_to_NSBitmapImageRep( ns );

   return [[[CIImage alloc]
            initWithBitmapImageRep: rep] autorelease];
}

extern NSImage * glue_CIImage_to_NSImage( CIImage * ci )
{
   int width   = [ci extent].size.width;
   int height  = [ci extent].size.height;

   NSImage * img = [[[NSImage alloc] initWithSize: NSMakeSize(width,height)] autorelease];
   [img lockFocus];
   [[[NSGraphicsContext currentContext] CIContext]
    drawImage: ci 
    atPoint: CGPointMake( 0,0 )
    fromRect: [ci extent]];
   [img unlockFocus];
   return img;
}

The Racket FFI definitions for the above functions:

(provide (all-defined-out))

(require ffi/unsafe)
(require ffi/unsafe/define)
 
(define glue-lib (ffi-lib "./cocoa-glue/build/Debug/cocoa-glue"))

(define-syntax define-glue
  (syntax-rules (:)
    [(_ name : type ...)
     (define name
       (get-ffi-obj
        (regexp-replaces 'name '((#rx"-" "_")))
        glue-lib (_fun type ...)))]))

(define-glue glue-image-width : _pointer -> _int32)
(define-glue glue-image-height : _pointer -> _int32)
(define-glue glue-CIImage-to-NSImage : _pointer -> _pointer)
(define-glue glue-NSImage-to-CIImage : _pointer -> _pointer)
(define-glue glue-NSBitmapImageRep-to-texture : _pointer -> _uint32)
(define-glue glue-NSImage-to-NSBitmapImageRep : _pointer -> _pointer)

Tron Bot Nuts & Bolts – Full Source

Below is the full sourcecode to my tron bot. The final tournament is currently under way.

My bot uses a fairly basic minimax function with alpha beta cut offs. When both bots are competing for the same space, the evaluation function tries to control more space by being closer to more squares than the opponent. When the 2 bots cannot reach each other, it looks for longest reachable path. When someone runs into a wall, the win() or lose() functions return the value, which are very extreme values offset slightly by the depth at which they are reached.

The search uses iterative deepening to utilize as much time as possible. After the first depth is searched, subsequent searches poll frequently as to not run out of time. The polling functions tries to adjust its frequency to try poll once every 10-50 milliseconds. The search times out at 950 milliseconds.

Here is the source. It may differ slightly from the actual code at the last submission.

// MyTronBot.cc
//
// This is the code file that you will modify in order to create your entry.

#include "Map.h"
#include <string>
#include <string.h>
#include <vector>
#include <queue>
#include <map>
#include <stdlib.h>

#include <setjmp.h>
#include <sys/time.h>
#include <time.h>

#define WIN 		 1000000
#define LOSE		-1000000
#define DRAW		0

typedef enum { LP_RETURN_ONE, LP_RETURN_FLOODFILL } longestPathStrategy;

int g_time_limit = 950;

// forward declaration...
class visitedMap;

jmp_buf g_jmp;
timeval g_start_timeval;
int g_poll = 0;

int best_move_index_full_search = 0;

visitedMap * g_scratch_map_me = NULL;
visitedMap * g_scratch_map_op = NULL;
visitedMap * g_scratch_map = NULL;

int max(int a, int b)	{ return ( a > b ) ? a : b; }
int min(int a, int b)	{ return ( a < b ) ? a : b; }

int g_move_count = 0;

struct move { int dx; int dy; const char * direction; } g_moves[] =
{
	{ 1, 0, "EAST" },
	{ 0, 1, "SOUTH" },
	{ 0,-1, "NORTH" },
	{-1, 0, "WEST" },
};

struct queue_item { int x; int y; int d; } foo_queue[50*50*4];
int q_top = 0;
int q_bot = 0;

void queue_clear()
{
	q_top = 0;
	q_bot = 0;
}

void queue_add( int x, int y, int d )
{
	foo_queue[q_bot].x = x;
	foo_queue[q_bot].y = y;
	foo_queue[q_bot].d = d;
	q_bot++;
}

int queue_take( int * x, int * y, int * d )
{
	*x = foo_queue[q_top].x;
	*y = foo_queue[q_top].y;
	*d = foo_queue[q_top].d;
	q_top++;
}

int queue_emptyp()
{
	return ( q_top == q_bot );
}

int g_prev_elapsed = 0;

int get_elapsed()
{
	struct timeval tv_now;
	gettimeofday( &tv_now, NULL );
	int d_sec = tv_now.tv_sec - g_start_timeval.tv_sec;
	int d_usec = tv_now.tv_usec - g_start_timeval.tv_usec;
	return d_sec * 1000 + d_usec / 1000;
}

void poll()
{
	static int frequency = 200;
	static int n = 0;
	if ( ++n % frequency == 0 )
	{
		int e0 = get_elapsed();

		if ( e0 > g_time_limit )
			longjmp( g_jmp, 1 );

		if ( g_prev_elapsed )
		{
			int delta = e0 - g_prev_elapsed;
			if ( delta > 50 )
				frequency = max(frequency-100,500);
			else if ( delta < 10 )
				frequency = min(frequency+100,5000);
		}
		g_prev_elapsed = e0;
	}
}

int win( int depth ) 	{ return WIN  - depth; }
int lose( int depth ) 	{ return LOSE + depth; }
int draw( int depth ) 	{ return DRAW /*+ depth*/;  }

class visitedMap
{
private:
	void init( int width, int height )
	{
		m_width			= width;
		m_height	   	= height;
		mapData			= (int*)malloc(byteCount());
	}

public:
	visitedMap( const Map & map )
	{
		init( map.Width(), map.Height() );

		for ( int y = 0; y < m_height; y++ )
		{
			for ( int x = 0; x < m_width; x++ )
			{
				if ( map.IsWall(x,y) )
					visit(x,y);
				else
					unvisit(x,y);
			}
		}
	}

	visitedMap( visitedMap * map )
	{
		init( map->width(), map->height() );
		takeValues( map );
	}

	visitedMap( int width, int height )
	{
		init( width, height );
		clear();
	}

	~visitedMap()					{ free(mapData); }

	void clear()					{ memset( mapData, 0, byteCount() ); }

	size_t byteCount()				{ return m_height*m_width*sizeof(int); }
	int width()						{ return m_width; }
	int height()					{ return m_height; }
	size_t offset( int x, int y ) 	{ return y*m_width+x; }

	void visit( int x, int y ) 		{ mapData[offset(x,y)] = -1; }
	void unvisit( int x, int y )	{ mapData[offset(x,y)] = 0;	}

	int value( int x, int y ) 		{ return mapData[offset(x,y)]; }
	void set( int x, int y, int v )	{ mapData[offset(x,y)] = v; }

	void takeValues( visitedMap * other ) {	memcpy( mapData, other->mapData, byteCount() );	}

	int visited( int x, int y )
	{
		/*
		if ( x < 0 ) return 1;
		if ( x >= m_width ) return 1;
		if ( y < 0 ) return 1;
		if ( y >= m_height ) return 1;
		*/
		return mapData[offset(x,y)];
	}

	// count total reachable squares from x,y...
	// depth first search...
	// returns the number of squares, but messes up matrix...
	int countAvailable( int x, int y )
	{
		if ( visited( x, y ) )
		{
			return 0;
		}
		else
		{
			visit( x, y );
			int r = 1;
			r += countAvailable( x+1, y );
			r += countAvailable( x-1, y );
			r += countAvailable( x, y+1 );
			r += countAvailable( x, y-1 );
			return r;
		}
	}

	void print( FILE * f, int mx, int my, int ox, int oy )
	{
		for ( int y = 0; y < height(); y++ )
		{
			for ( int x = 0; x < width(); x++ )
			{
				if ( x == mx && y == my )
					fprintf(f,"M");
				else if ( x == ox && y == oy )
					fprintf(f,"O");
				else
					fprintf(f,"%c", visited(x,y) ? '#' : ' ' );

			}
			fprintf(f,"\n");
		}
	}

	void printValues( FILE * f )
	{
		for ( int y = 0; y < height(); y++ )
		{
			for ( int x = 0; x < width(); x++ )
			{
				fprintf(f,"%3d ", value(x,y));
			}
			fprintf(f,"\n");
		}
	}

	static int manhattan_distance( int x0, int y0, int x1, int y1 )
	{
		return abs(x1-x0)+abs(y1-y0);
	}

	int is_reachable( int sx, int sy, int ex, int ey )
	{
		if ( sx == ex && sy == ey )
			return 1;
		if ( visited( sx, sy ) )
			return 0;
		else
		{
			visit( sx, sy );
			return is_reachable( sx+1, sy, ex, ey ) ||
				is_reachable( sx-1, sy, ex, ey ) ||
				is_reachable( sx, sy-1, ex, ey ) ||
				is_reachable( sx, sy+1, ex, ey );
		}
	}

	int has_overlap( int mx, int my, int ox, int oy )
	{
		g_scratch_map->takeValues( this );
		return g_scratch_map->is_reachable( mx, my, ox, oy );
	}

	// fill each square with the minimize number of move to reach it...
	// breadth first search...
	void fillDistances( int x0, int y0, int d0 )
	{
		queue_clear();
		queue_add( x0,y0,d0 );
		do
		{
			int x,y,d;
			queue_take(&x,&y,&d);

			if ( 0 == value(x,y) )
			{
				set( x, y, d );
				queue_add( x+1,y,d+1 );
				queue_add( x-1,y,d+1 );
				queue_add( x,y-1,d+1 );
				queue_add( x,y+1,d+1 );
			}
		} while (!queue_emptyp());
	}

	int longestPathSearch( int x, int y, int d, longestPathStrategy strategy, int * best_move )
	{
		if ( g_poll ) poll();

		// dont need to bother checking offboard... just look for a wall...
		if ( visited(x,y) )
			return 0;

		if ( d <= 0 )
		{
			// just return 1...
			if ( strategy == LP_RETURN_ONE )
			{
				return 1;
			}
			// degrade to counting available squares when we out of depth...
			else if ( strategy == LP_RETURN_FLOODFILL )
			{
				g_scratch_map->takeValues( this );
				return g_scratch_map->countAvailable(x,y);
			}
			else
			{
				exit(-1);
			}
		}

		// find the longest subpath reachable from here and add one for this square...
		visit(x,y);
		int mx = 0;
		for ( int i=0; i<4; i++ )
		{
			int dx = g_moves[i].dx;
			int dy = g_moves[i].dy;
			int v0 = longestPathSearch(x+dx,y+dy,d-1,strategy,NULL);

			if ( v0 > mx )
			{
				mx = v0;
				if ( best_move )
					*best_move = i;
			}
		}
		unvisit(x,y);
		return mx+1;
	}

	int longestPath( int x, int y )
	{
		return longestPathSearch(x,y,width()*height(),LP_RETURN_ONE,NULL);
	}

	int non_overlap_evaluate( int mx, int my, int ox, int oy, int depth )
	{
		int m = longestPathSearch(mx,my,depth,LP_RETURN_FLOODFILL,NULL);
		int o = longestPathSearch(ox,oy,depth,LP_RETURN_FLOODFILL,NULL);

		// only return win() / lose() if we searched to an endpoint...
		if ( ( m > o ) && ( o <= depth ) )
			return win(depth);
		if ( ( o > m ) && ( m <= depth ) )
			return lose(depth);
		else
			return 10 * (m - o);
	}

	int evaluate( int mx, int my, int ox, int oy )
	{
	    if ( !has_overlap( mx, my, ox, oy ) )
		{
			return non_overlap_evaluate(mx,my,ox,oy,4);
		}
		else
 		{
			g_scratch_map_me->takeValues( this );
			g_scratch_map_op->takeValues( this );

			g_scratch_map_me->fillDistances( mx, my, 1 );
			g_scratch_map_op->fillDistances( ox, oy, 1 );

			int advantage = 0;

			for ( int y = 0; y < height(); y++ )
			{
				for ( int x = 0; x < width(); x++ )
				{
					// ignore walls
					if ( !visited(x,y) )
					{
						int me = g_scratch_map_me->value(x,y);
						int op = g_scratch_map_op->value(x,y);
						int dt = abs(me-op);

						if ( !me && !op )
							advantage += 0;
						else if ( !me )
							advantage -= 10;
						else if ( !op )
							advantage += 10;
						else if ( me < op )
							advantage += 10;
						else if ( op < me )
							advantage -= 10;
					}
				}
			}

			return advantage;
		}
	}

private:
	int m_width;
	int m_height;
	int * mapData;
};

class payOffMatrix : public visitedMap
{
public:
	payOffMatrix() : visitedMap(4,4)	{}

	int bestDirections( int * bestMin, int * bestTot )
	{
		int maxmin = 0;
		int maxtot = 0;
		int maxmin_index = 0;
		int maxtot_index = 0;

		for ( int i = 0; i < 4; i++ )
		{
			int min=0;
			int tot=0;

			for ( int j = 0; j < 4; j++ )
			{
				int n = value(i,j);
				tot += n;
				if ( j == 0 || n < min )
					min = n;
			}

			if ( i == 0 || min > maxmin )
			{
				maxmin = min;
				maxmin_index = i;
			}
			if ( i == 0 || tot > maxtot )
			{
				maxtot = tot;
				maxtot_index = i;
			}
		}

		if ( bestMin )
			*bestMin = maxmin_index;
		if ( bestTot )
			*bestTot = maxtot_index;
	}

	void print( FILE * f )
	{
		int maxmin = 0;
		int maxtot = 0;
		int maxmin_index = 0;
		int maxtot_index = 0;

		for ( int i = 0; i < 4; i++ )
		{
			fprintf( f, "%6s\t", g_moves[i].direction);
			int min=0;
			int tot=0;
			for ( int j = 0; j < 4; j++ )
			{
				int n = value(i,j);
				fprintf(f,"%7d\t",n);
				tot += n;
				if ( j == 0 || n < min )
					min = n;
			}

			if ( i == 0 || min > maxmin )
			{
				maxmin = min;
				maxmin_index = i;
			}
			if ( i == 0 || tot > maxtot )
			{
				maxtot = tot;
				maxtot_index = i;
			}

			fprintf(f,"\tmin: %7d\taverage: %7d",min,tot/4);
			fprintf(f,"\n");
		}

		fprintf(f,"maxmin: %7d - %s\n",maxmin,g_moves[maxmin_index].direction);
		fprintf(f,"maxtot: %7d - %s\n",maxtot,g_moves[maxtot_index].direction);
		if ( maxmin_index != maxtot_index )
			fprintf(f,"DIFFERENCE IN TOTAL / MAXMIN DIRECTIONS!\n");
	}
};


int search( visitedMap * map, int depth, int mx, int my, int ox, int oy, int best_min, payOffMatrix * payOff )
{
	if ( g_poll ) poll();

	if ( mx == ox && my == oy )
		return draw( depth );

	int me_crash = map->visited( mx, my );
	int op_crash = map->visited( ox, oy );

	if ( me_crash && op_crash )
		return draw(depth);
	else if ( me_crash )
		return lose(depth);
	else if ( op_crash )
		return win(depth);
	else if ( depth <= 0 )
		return map->evaluate(mx,my,ox,oy);
	else if ( !map->has_overlap(mx,my,ox,oy) )
		return map->non_overlap_evaluate(mx,my,ox,oy,depth+2);

	map->visit( mx, my );
	map->visit( ox, oy );

	// int best_move = LOSE * 2;
	int best_move = payOff ? LOSE * 2 : best_min;

	for ( int i_ = 0; i_ < 4; i_++ )
	{
		int i = i_;
		int this_move = WIN * 2;

		for ( int j_ = 0; j_ < 4; j_++ )
		{
			int j = j_;

			int v = search( map, depth - 1,
							mx + g_moves[i].dx,
							my + g_moves[i].dy,
							ox + g_moves[j].dx,
							oy + g_moves[j].dy,
							best_move,
							NULL
				);

			if ( payOff )
				payOff->set(i,j,v);

			this_move = min( this_move, v );

			// alpha-beta style pruning occurs here...
			if ( !payOff && ( v <= best_move ) )
				break;
		}

		best_move = max(best_move,this_move);
	}

	map->unvisit( mx, my );
	map->unvisit( ox, oy );

	return best_move;
}

std::string MakeMove(const Map& map)
{
	static int two_player_start_search_depth = 2;
	static int longest_path_start_search_depth = 4;
	bool print_eval_info = false;

	// get time asap for accuracy...
	gettimeofday( &g_start_timeval, NULL );

	visitedMap scratch0( map );
	visitedMap scratch1( map );
	visitedMap scratch2( map );

	g_scratch_map_me	= &scratch0;
	g_scratch_map_op	= &scratch1;
	g_scratch_map		= &scratch2;

	g_time_limit = (g_move_count == 0) ? 2500 : 950;

	int mx = map.MyX();
	int my = map.MyY();

	int ox = map.OpponentX();
	int oy = map.OpponentY();

	visitedMap v( map );

	// if the maps do not overlap, just try to find the optimal path...
	if ( !v.has_overlap( mx, my, ox, oy ) )
	{
		g_poll = 0;
		int n;

		if ( setjmp( g_jmp ) == 0 )
		{
			for ( n = longest_path_start_search_depth; ; n++ )
			{
				int best_move_index = 0;
				int lp = v.longestPathSearch(mx,my,n,LP_RETURN_FLOODFILL,&best_move_index);
				best_move_index_full_search = best_move_index;

				// we searched past the depth of available moves...
				if ( lp < n )
				{
					if ( print_eval_info )
						fprintf(stderr,"longest path found: %d\n", lp);
					break;
				}

				g_poll = 1;

				if ( n > longest_path_start_search_depth + 2 )
				{
					longest_path_start_search_depth = min(10,longest_path_start_search_depth);
				}
			}
		}
		else
		{
			if ( print_eval_info )
				fprintf(stderr,"ran out of time - depth: %d\n", n);
		}
	}
	else
	{
		best_move_index_full_search = 0;
		g_poll = 0;

		if ( setjmp( g_jmp ) == 0 )
		{
			for ( int search_depth = two_player_start_search_depth; ; search_depth+= 2 )
			{
				visitedMap vm( map );
				payOffMatrix payOff;

				g_prev_elapsed = 0;

				int best_move_value = search( &vm, search_depth, mx, my, ox, oy, LOSE * 2, &payOff );

				payOff.bestDirections( &best_move_index_full_search, NULL );

				if ( print_eval_info )
				{
					fprintf(stderr,"Pay Off Matrix [Depth %d]\n",search_depth);
					payOff.print(stderr);

					fprintf(stderr,"M%02d - [D%02d] - %7d - %s\n", g_move_count, search_depth, best_move_value,
							g_moves[best_move_index_full_search].direction);
				}

				g_poll = 1;

				if ( search_depth > two_player_start_search_depth + 4 )
				 	two_player_start_search_depth = min(7,two_player_start_search_depth+1);
			}
		}
	}

	int x2 = mx+g_moves[best_move_index_full_search].dx;
	int y2 = my+g_moves[best_move_index_full_search].dy;

	// try to find a valid move if the search failed...
	if ( map.IsWall(x2,y2) )
	{
		for ( best_move_index_full_search = 0; best_move_index_full_search < 4; best_move_index_full_search++ )
		{
			int i = best_move_index_full_search;
			x2 = mx+g_moves[i].dx;
			y2 = my+g_moves[i].dy;
			if ( !map.IsWall( x2, y2 ) )
				break;
		}
	}

	g_move_count++;

	return g_moves[best_move_index_full_search].direction;
}

// Ignore this function. It is just handling boring stuff for you, like
// communicating with the Tron tournament engine.
int main() {
	while (true) {
		Map map;
		Map::MakeMove(MakeMove(map));
	}
	return 0;
}

Tron Bot – Nuts & Bolts – Queue

As I mentioned in my previous post, I entered a Tron bot into the University of Waterloo Google AI Challenge. My bot is currently hovering the top ten in the preliminary rounds.

I plan to post all the code to my bot, but not the interesting pieces until after the final tournament has taken place.  Entries are allowed up until the end of the day this Friday.

But here is some code that I don’t really think gives me away any secrets so I will post it here.  Also, I doubt too many entrants will read this post.

As part of my bot, I needed to be able to figure out the minimum number of moves to squares on the board.  As you may recall from an algorithms course, minimum path = breadth first search = use a queue. My first pass at this used the std::queue provided by the STL, but I thought it might be too slow, mainly due to the potential to allocate memory dynamically. So I wrote my own queue functions using a statically allocated array. Allocating the array statically is not a problem because the tournament rules state that the largest tron field will be 50×50.

The structure of each queue item is 3 integers, the coordinates of the square and the depth reached in the search.

I think this is probably considerably faster than using the STL queue.

Without further ado, here’s the code:

struct queue_item { int x; int y; int d; } foo_queue[50*50*4];
int q_top = 0;
int q_bot = 0;

void queue_clear()
{
	q_top = 0;
	q_bot = 0;
}

void queue_add( int x, int y, int d )
{
	foo_queue[q_bot].x = x;
	foo_queue[q_bot].y = y;
	foo_queue[q_bot].d = d;
	q_bot++;
}

int queue_take( int * x, int * y, int * d )
{
	*x = foo_queue[q_top].x;
	*y = foo_queue[q_top].y;
	*d = foo_queue[q_top].d;
	q_top++;
}

int queue_emptyp()
{
	return ( q_top == q_bot );
}

PLT Scheme and LDAP authentication

I recently was trying to figure out how to do user authentication on our company’s Active Directory LDAP server. Using a combination of openldap and the linux and mac side and Microsoft’s LDAP library, I was able to create a small module that can authenticate users and works cross platform.

The code uses PLT Scheme’s foreign library interface.

To authenticate a user, just do (authenticate “ldap-server.host.com” “username@host.com” “user-password”) with an optional port number. It will return ‘SUCCESS for a successful connection, ‘INVALID_CREDENTIALS for a bad password / username combination, or the integer of the error code for other errors.

#lang scheme

(require
 scheme/foreign
 srfi/2
 )

(provide authenticate)

(unsafe!)

;;; library accessors...
(define libldap
  (cond [(eq? (system-type) 'windows) (ffi-lib "Wldap32")]
        [else (ffi-lib "libldap")]))

(define ldap-instance (_cpointer/null 'ldap-instance))
(define LDAP_OPT_PROTOCOL_VERSION #x0011)
(define LDAP_AUTH_SIMPLE #x80)
(define LDAP_SUCCESS #x00)
(define LDAP_INVALID_CREDENTIALS #x31)
(define ldap_init       (get-ffi-obj 'ldap_init libldap (_fun _string/utf-8 _int32 -> ldap-instance)))
(define ldap_bind_s     (get-ffi-obj 'ldap_bind_s libldap (_fun ldap-instance _string/utf-8 _string/utf-8 _int32 -> _int32)))
(define ldap_set_option (get-ffi-obj 'ldap_set_option libldap (_fun ldap-instance _int32 (foo : (_ptr i _int)) -> _int32)))
(define ldap_unbind_s   (get-ffi-obj 'ldap_unbind_s libldap (_fun ldap-instance -> _void)))

;;; high level interface...
(define (authenticate host user password #:port (port 389))
  (and-let* ([ldap-connection (ldap_init host port)])
    (ldap_set_option ldap-connection LDAP_OPT_PROTOCOL_VERSION 3)
    (let ([bind-result
           (ldap_bind_s ldap-connection user password LDAP_AUTH_SIMPLE)])
      (begin0
        (cond
          [(= bind-result LDAP_SUCCESS) 'SUCCESS]
          [(= bind-result LDAP_INVALID_CREDENTIALS) 'INVALID_CREDENTIALS]
          [else bind-result])
        (ldap_unbind_s ldap-connection)))))

From the Archives: TI-85 Password Protection

When I was in high school, the TI-85 was the shit. It was a great calculator, and someone had figured out how to install a loader that would allow you to run arbitrary native programs using some fancy hacking and installing programs into the “Custom” menu. The loader was called “ZShell.”

The TI-85 had an available serial port connector that let you transfer files to and from your computer and backup your calculator. ZShell was distributed as a backup file that once installed you could copy over individual programs in string variables.

Development was done using a Z80 cross assembler and some basic libraries and ROM calls that were distributed by the ZShell authors.

The first real program that I wrote and distributed was a password protection program called PWPROT. From the PWPROT readme file:

+-----------+
| Overview: |
+-----------+

    This program allows the user to implement a password protection scheme
for the Texas Instrument TI-85 graphing calculator.  The password can be
up to 8 characters long, uppercase alphabet only.

+--------+
| Usage: |
+--------+

    To run the program press the custom menu key.  This should show Zshell as
Custom Menu number 1.  Select it with the f1 key.  From the menu choose
pwprot.  The program will prompt the user for a menu of changing passwords,
shutting down, or quit.
    To change passwords select option 2, by hitting the 2 button.
The calculator will prompt you for the password, enter it bytyping it on
the calculator keys.  When you have completed the password hit enter.  The
calculator will then print the password you have entered and ask if it is
correct or not.  If it is correct select 1, and the calculatorwill display
password changed.  If you have have not entered the password correctly
choose option 2, and the calculator will display "Password Not Changed"
and return to the main menu.
    To shut down and use the password protection feature choose option
1 from the main menu.  The calculator will promptly turn itself off.  When
the on key is pressed pwprot will prompt you to enter a password if it is
correct the calculator will return to normal operating mode.  If it is
incorrect, the calculator will turn itself off again.

Below is the source code to PWPROT, my password protection program.

;+--------------------------------------------
;| Password Protection For TI-85, Via Zshell
;| Chris Bowron / Digital Dreamland 1996
;| ai150@detroit.freenet.org
;|
;|  "When Privacy Is Outlawed, Only Outlaws
;|             Will Have Privacy"
;|
;|  "Living is easy with eyes closed,
;|    Misunderstanding all you see"
;|                     John Lennon
;|
;|  "Anyone who says they can see through
;|        women is missing alot"
;|                     Groucho Marx
;+--------------------------------------------
;|Don't laugh, this is my first 'real' program
;+--------------------------------------------
;| Last Modification Jan 24, 1996 Circa 9:30 PM
;+----------------------------------------------
;| To Do List:
;|    Change Password From Program (e.g. Choose Shutdown or Change PW) - Done
;|    Distribute (String & Source) - In Progress
;|    Fathom How The Hell It Actually Works - Slowly, but Surely
;|    Self Encrypting????  - Pipe Dream 🙂
;|    Makes Wads Of Cash - Working on It
;|

#INCLUDE "TI-85.H"

#define RECALC     ld   hl,ZS_BITS  \ set  0,(hl)
#define WARPOUT    ld   hl,ZS_BITS  \ set  1,(hl)

TEXT_X =  $800D
TEXT_Y =  $800C

.org 0
.db "¿¿Â PW Protect v1.24",0

;+------
;| Start Here Buddy,
;+------

ProgStart:

    ROM_CALL(ClearLCD)
	ld  a,0
    ld  (text_x),a
    ld  (text_y),a
    ld  hl, (PROGRAM_ADDR)
    ld  de, MenuStr
    add hl, de
	ROM_CALL(D_ZT_STR)

    CALL_(WaitKey)

    cp K_EXIT
    jr z, OutOfHere

    cp K_3
    jr z, Menu3

    cp K_1
	jr z, Menu1

    cp K_2
	jr z, Menu2

    jr ProgStart

Menu1:
	CALL_(ShutDown)
    jr OutOfHere

Menu2:
    CALL_(ChangePassword)
	jr ProgStart

Menu3:

OutOfHere:

	CALL_(DispCorrect)
	CALL_(WaitKey)

	RECALC               ;Recalculate CheckSum
   	WARPOUT

	ret

;+-----
;| Change Password
;+-----
ChangePassword:

	ROM_CALL(ClearLCD)

    ld 	hl, (PROGRAM_ADDR)
    ld 	de, CurrentStr
    add hl, de
    ld  a,0
    ld  (text_x),a
    ld  a,4
    ld  (text_y),a
	ROM_CALL(D_ZT_STR)

    CALL_(GetPassword)
    CALL_(CheckPW)
    cp 0
    jr nz, WrongPW

    ROM_CALL(ClearLCD)

    ld 	hl, (PROGRAM_ADDR)
    ld 	de, NewStr
    add hl, de
    ld  a,0
    ld  (text_x),a
    ld  a,4
    ld  (text_y),a
    ROM_CALL(D_ZT_STR)

    CALL_(GetPassword)

    ROM_CALL(ClearLCD)

    ld a,0
    ld (text_x),a
    ld (text_y),a

    ld hl, (PROGRAM_ADDR)
    ld de, DummyStr
    add hl, de

    ROM_CALL(D_ZT_STR)

    ld a,0
    ld (text_x),a
    ld a,1
    ld (text_y),a

    ld hl, (PROGRAM_ADDR)
    ld de, YesNoStr
    add hl, de

    ROM_CALL(D_ZT_STR)

PWCKeyLoop:

	CALL_(WaitKey)

    cp K_1
    jr z, ChangeIt

    cp K_2
    jr z, DontDoIt

    cp K_EXIT
    jr z, DontDoIt

    jr PWCKeyLoop

ChangeIt:

	CALL_(DummyStr2PWStr)
    jr CPWExit

DontDoIt:

	ld 	hl, (PROGRAM_ADDR)
    ld 	de, NoChangeStr
    add hl, de
	ROM_CALL(D_ZT_STR)
	CALL_(WaitKey)
    jr  CPWExit

WrongPW:

	CALL_(DispWrong)
   	CALL_(WaitKey)

CPWExit:

    ret
;+----
;| Copy Password From DummyStr to PWStr
;+----
DummyStr2PWStr:
	ld  hl, (PROGRAM_ADDR)
    ld  de, DummyStr
    add hl, de				; HL Points To DummyStr
    ex  de, hl				; DE Points To DummyStr

    ld  hl, (PROGRAM_ADDR)
    ld  bc, PWStr
    add hl, bc				; HL Points To PWStr

    ld b, 8					; 8 Chars For PW
CILoop:

	ld a, (de)
    ld (hl), a				; PWStr[8-b] = DummyStr[8-b]

    inc hl
    inc de

	djnz CILoop

	ld 	hl, (PROGRAM_ADDR)
    ld 	de, ChangeStr
    add hl, de
	ROM_CALL(D_ZT_STR)
    CALL_(WaitKey)

	ret

;+-----
;| Shut Down Calculator, Prompting For PW When Turned ON
;+-----
ShutDown:

   ROM_CALL(ClearLCD)
   CALL_(DispInfo)

MajorLoop:

   CALL_(PowerDown)

   CALL_(GetPassWord)

   CALL_(CheckPW)

   cp 0
   jr z, CorrectPW

WrongStinkingPW:

   CALL_(DispWrong)
   CALL_(WaitKey)
   ROM_CALL(ClearLCD)
   CALL_(DispInfo)

   jr MajorLoop

CorrectPW:

ReturnCalc:

   ret

;+------
;| Display Program Info
;+------
DispInfo:

   ld  a, 0
   ld  (text_x),a
   ld  (text_y),a
   ld  de, InfoStr
   ld  hl, (PROGRAM_ADDR)
   add hl, de
   ROM_CALL(D_ZT_STR)
   ret

;+------
;| Display Incorrect PW Info
;+------
DispWrong:

   ROM_CALL(ClearLCD)

   ld  a, 0
   ld  (text_x),a
   ld  (text_y),a
   ld  de, WrongPWStr
   ld  hl, (PROGRAM_ADDR)
   add hl, de
   ROM_CALL(D_ZT_STR)
   ret

;+------
;| Display Correct PW Info
;+------
DispCorrect:

   ROM_CALL(ClearLCD)

   ld  a, 0
   ld  (text_x),a
   ld  (text_y),a
   ld  de, CorrectPWStr
   ld  hl, (PROGRAM_ADDR)
   add hl, de
   ROM_CALL(D_ZT_STR)

   ret

;+------
;| Wait Until Keypressed (A = Scan code)
;+------
WaitKey:

   call get_key
   cp K_NOKEY
   jr z, WaitKey
   ret

;+------
;| Wait Till Key Pressed Or LCD Is On / Currently Unused
;+------
;WaitOn:
;
;    in a, (3)
;
;    bit 1, a			; Is LCD On?
;    jr nz, ExitWO
;
;    bit 0, a
;    jr  nz, ExitWO		; Has On Been Pressed?
;
;    bit 3, a
;    jr  z, ExitWO	    ; Is On Pressed NOW...
;
;	call GET_KEY
;    cp 0
;    jr z, WaitOn
;
;ExitWO:
;
;	ret
;
;+------
;| Shut Down Calculator
;+------
PowerDown:

    DI

   	ld a, 00000001b
	OUT  ($03),A
		 ;76543210
    ld a, 00000000
    	 ;   ||||
         ;	 ||++---Low Resistance
		 ;	 ++---- 10 Byte Buffer
    out (4),a

	EX	AF,AF'		; 4B59
	EXX			; 4B5A

    ;ld a, $40		; Doesn't work correctly
    ;out (6),a

	EI
	HALT

TurnOn:

   	;DI

    ld 	a,  $16			; Normal Buffer Mode
    out (4),a

   	ld  a, $41
   	out (6),a

	ld a, 00001011b      ;LCD On
   	out (3),a

   	;EI

   	ret
;+------
;| Get Password From Calculator
;+------
GetPassWord:

   ld  a, 0
   ld  (text_x),a
   ld  a, 5
   ld  (text_y),a
   ld  hl, (PROGRAM_ADDR)
   ld  de, PromptStr
   add hl,de

   ROM_CALL(D_ZT_STR)           ; print prompt

   CALL_(GetString)             ; DummyStr = Entered Password

   ret

;+------
;| Get String Into DummyStr
;+------
GetString:
   ld   hl,(PROGRAM_ADDR)
   ld   de, DummyStr
   add  hl, de          ; HL Points To DummyStr

   push hl              ; start of DummyStr

   ld   b, 8            ; Zero Out DummyStr

FillLoop:
   inc hl
   ld  a,0
   ld  (hl), a
   djnz FillLoop

   pop hl

GSLoop:

   push hl

   ld a, 'Ú'
   ROM_CALL(TX_CHARPUT)
   ld hl, (TEXT_X)
   dec hl
   ld (TEXT_X), hl
   pop hl

   CALL_(GetLetter)
   cp 0
   jr z, HopOut

   cp K_EXIT
   jr z, HopOut

   ld (hl),a

   inc b
   inc hl

   ld a, b      ; Check If 8 Chars entered
   cp 8
   jr nz, GSLoop

HopOut:

   ld  a, 0
   ld  (hl),a   ;  Set to Null

   ret

;+----
;| Check if DummyStr=PWStr
;+----
CheckPW:
   ld   hl, (PROGRAM_ADDR)
   ld   de, DummyStr
   add hl, de
   ex  de, hl           ; DE Points To DummyStr

   ld  hl, (PROGRAM_ADDR)
   ld  bc, PWStr
   add hl, bc           ; HL Points To PWStr

CheckLoop:

   ld a, (hl)           ; HL to PWStr
   ld b, a              ; DE to DummyStr
   ex de, hl            ;  Switch

   ld a,(hl)            ; HL to DummyStr
   ex de, hl            ; DE Points to PWStr

   cp b
   jr nz, NotEqual

Equal:                  ; DE[n] = HL[n]

   inc de               ; increase pointers
   inc hl

   cp 0                 ; If Zero Return 0
   jr nz, CheckLoop     ; A = 0
                        ; EOS
   ld b,0
   jr BaBoom

NotEqual:

   ld b, 1              ; If Not = Then Return 1

BaBoom:

   ld a,b               ; ret b as acc
   ret

;+-------
;| Get One Letter & Translate
;| This routine and LetterTable from Magnus Hagander's Texanoid Game
;+-------
GetLetter:
   push hl
   push bc
   ld   hl,(PROGRAM_ADDR)
   ld   de,LetterTable-1 ;Point to one before, since we always inc once
   add  hl,de                   ;HL now points right

ConvertLoop:
   push hl
   CALL Get_Key                 ;ACC holds key
   pop  hl

   cp   K_EXIT                  ;temp
   jr   z, Shazam

   cp   0
   jr   z,ConvertLoop

   push hl                      ;save away...
   ld   b,a
   ld   c,0

ConversionLoop:
   inc  hl
   djnz ConversionLoop          ;After this, we have the correct offset...
   ld   a,(HL)
   pop  hl
   cp   255
   jr   z,ConvertLoop           ;Invalid key

   ROM_CALL(TX_CHARPUT)         ;Show the char

Shazam:

   pop  bc
   pop  hl

   ret                          ;Value is in acc

;+-------
;|  Stuff That's Useful
;+-------
InfoStr:
       ;123456789012345678901 ;
   .db "  Password  Protect  "
   .db "  Digital Dreamland  "
   .db " hristopher  owron "
   .db 0                       ; End String

MenuStr:
   	   ;123456789012345678901
   .db " ¿¿Â Password Protect"
   .db "      Ver 1.24       "
   .db "1. Shut Down         "
   .db "2. Change Password   "
   .db "3. Quit              "
   .db 0

NewStr:
   .db "New ",0
CurrentStr:
   .db "Current ",0
PromptStr:
   .db "Password:",0

PWStr: ;1234567
   .db 'B','O','O',0,0,0,0,0,0        ; Allow for 8 chars + Null Byte

DummyStr:
   .db 0,0,0,0,0,0,0,0,0        ; Temp Work Space

YesNoStr:
	   ;123456789012345678901
   .db "Is This Correct?    "
   .db "1. Yes               "
   .db "2. No                "
   .db 0

ChangeStr:
   .db "Password Changed",0

NoChangeStr:
   .db "Password Not Changed",0

WrongPWStr:
   .db "Incorrect PW!",0

CorrectPWStr:
       ;123456789012345678901
   .db " Thank You For Using " ;1
   .db "    DDL Products     " ;2
   .db "    ¿¿Â MCMXCVI      " ;3
   .db "                     " ;4
   .db "  [Share And Enjoy]  " ;5
   .db "ai150@               " ;6
   .db "detroit.freenet.org  " ;7
   .db 0

LetterTable:                                ; For converting keys into letters
   .db 255,255,255,255
   .db 255,255,255,255
   .db 000,'X','T','O'
   .db 'J','E',255,255
   .db ' ','W','S','N'
   .db 'I','D',255,255
   .db 'Z','V','R','M'
   .db 'H','C',255,255
   .db 'Y','U','Q','L'
   .db 'G','B',255,255
   .db 255,255,'P','K'
   .db 'F','A',255,255
   .db 255,255,255,255
   .db 255,255,255,255

.end