268 lines
9.6 KiB
Plaintext
268 lines
9.6 KiB
Plaintext
output file "Wordiff" ' 27 november 2022 '
|
|
|
|
#plist NSAppTransportSecurity @{NSAllowsArbitraryLoads:YES}
|
|
|
|
begin enum 1
|
|
_playerLabel : _playerInput : _wordLabel : _wordInPlay : _playsLabel
|
|
_scrlView : _textView
|
|
_resignBtn : _againBtn : _quitBtn
|
|
end enum
|
|
|
|
begin globals
|
|
CFMutableArrayRef gWords, gNames, gUsed
|
|
gWords = fn MutableArrayWithCapacity( 0 )
|
|
gNames = fn MutableArrayWithCapacity( 0 )
|
|
gUsed = fn MutableArrayWithCapacity( 0 )
|
|
CFMutableStringRef gTxt
|
|
gTxt = fn MutableStringWithCapacity( 0 )
|
|
end globals
|
|
|
|
void local fn BuildInterface
|
|
window 1, @"Wordiff", ( 0, 0, 400, 450 ), NSWindowStyleMaskTitled + NSWindowStyleMaskClosable
|
|
// Fields for labels and input
|
|
textlabel _playerLabel, @"The players:", ( 0, 370, 148, 24 )
|
|
textlabel _wordLabel, @"Word in play:", ( 68, 409, 100, 24 )
|
|
textlabel _playsLabel, , ( 113, 370, 150, 24 )
|
|
textfield _playerInput, Yes, , ( 160, 372, 150, 24 )
|
|
textfield _wordInPlay, No, @". . .", ( 160, 412, 148, 24 )
|
|
ControlSetAlignment( _playerLabel, NSTextAlignmentRight )
|
|
ControlSetAlignment( _playerInput, NSTextAlignmentCenter )
|
|
ControlSetAlignment( _wordInPlay, NSTextAlignmentCenter )
|
|
ControlSetFormat( _playerInput, @"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", YES, 8, _formatCapitalize )
|
|
TextFieldSetTextColor( _wordLabel, fn ColorLightGray )
|
|
TextFieldSetTextColor( _wordInPlay, fn ColorLightGray )
|
|
TextFieldSetSelectable( _wordInPlay, No )
|
|
// Fields for computer feedback
|
|
scrollview _scrlView, ( 20, 60, 356, 300 ), NSBezelBorder
|
|
textview _textView, , _scrlView, , 1
|
|
ScrollViewSetHasVerticalScroller( _scrlView, YES )
|
|
TextViewSetTextContainerInset( _textView, fn CGSizeMake( 3, 3 ) )
|
|
TextSetFontWithName( _textView, @"Menlo", 12 )
|
|
TextSetColor( _textView, fn colorLightGray )
|
|
TextSetString( _textView, @"First, enter the name of each player and press Return to confirm. ¬
|
|
When done, press Return to start the game." )
|
|
// Buttons and menus
|
|
button _resignBtn, No, , @"Resign", ( 251, 15, 130, 32 )
|
|
button _againBtn, No, , @"New game", ( 114, 15, 130, 32 )
|
|
button _quitBtn, Yes, , @"Quit", ( 15, 15, 92, 32 )
|
|
filemenu 1 : menu 1, , No ' Nothing to file
|
|
editmenu 2 : menu 2, , No ' Nothing to edit
|
|
WindowMakeFirstResponder( 1, _playerInput ) ' Activate player input field
|
|
end fn
|
|
|
|
void local fn LoadWords
|
|
CFURLRef url
|
|
CFStringRef words, string
|
|
CFArrayRef tmp
|
|
CFRange range
|
|
// Fill the gWords list with just the lowercase words in unixdict
|
|
url = fn URLWithString( @"http://wiki.puzzlers.org/pub/wordlists/unixdict.txt" )
|
|
words = fn StringWithContentsOfURL( url, NSUTF8StringEncoding, NULL )
|
|
tmp = fn StringComponentsSeparatedByCharactersInSet( ( words ), fn CharacterSetNewlineSet )
|
|
for string in tmp
|
|
range = fn StringRangeOfStringWithOptions( string, @"^[a-z]+$", NSRegularExpressionSearch )
|
|
if range.location != NSNotFound then MutableArrayAddObject( gWords, string )
|
|
next
|
|
end fn
|
|
|
|
void local fn Say(str1 as CFStringRef, str2 as CFStringRef )
|
|
// Add strings to the computer feedback
|
|
fn MutableStringAppendString( gTxt, str1 )
|
|
fn MutableStringAppendString( gTxt, str2 )
|
|
TextSetString( _textView, gTxt )
|
|
TextScrollRangeToVisible( _textView, fn CFRangeMake( len( fn TextString( _textView ) ), 0) )
|
|
end fn
|
|
|
|
local fn CompareEqual( wrd1 as CFStringRef, wrd2 as CFStringRef ) as short
|
|
NSInteger i, k
|
|
CFStringRef a, b
|
|
//Find the number of differences in two strings
|
|
k = 0
|
|
for i = 0 to len( wrd1 ) - 1
|
|
a = mid( wrd1, i, 1 ) : b = mid( wrd2, i, 1 )
|
|
if fn StringIsEqual( a, b ) == No then k++
|
|
next
|
|
end fn = k
|
|
|
|
|
|
local fn ChopAndStitch( sShort as CFStringRef, sLong as CFStringRef ) as CFStringRef
|
|
NSInteger i, k
|
|
CFStringRef a, b
|
|
// Find the extra letter in the long string and remove it
|
|
k = 0
|
|
for i = 0 to len( sLong ) - 1
|
|
a = mid( sShort, i, 1 ) : b = mid( sLong, i, 1 )
|
|
if fn StringIsEqual( a, b ) == No then k = i : break ' Found it
|
|
next
|
|
a = left( sLong, k ) : b = mid( sLong, k + 1 ) 'Removed it
|
|
end fn = fn StringByAppendingString( a, b )
|
|
|
|
|
|
local fn WordiffWords( wrd1 as CFStringRef, wrd2 as CFStringRef ) as short
|
|
Short err = 0
|
|
// If a letter was added or removed, the strings should be identical after
|
|
// we remove the extra letter from the longest string.
|
|
// If they are the same length, the strings may differ at just one place.
|
|
select case
|
|
case len( wrd2 ) > len( wrd1 )
|
|
wrd2 = fn ChopAndStitch( wrd1, wrd2 )
|
|
if fn CompareEqual( wrd1, wrd2 ) != 0 then err = 1 ' Words identical?
|
|
case len( wrd1 ) > len( wrd2 )
|
|
wrd1 = fn ChopAndStitch( wrd2, wrd1 )
|
|
if fn CompareEqual( wrd1, wrd2 ) != 0 then err = 2
|
|
case len( wrd2 ) = len( wrd1 )
|
|
if fn CompareEqual( wrd1, wrd2 ) != 1 then err = 3 ' Only one change?
|
|
end select
|
|
end fn = err
|
|
|
|
local fn CheckWord( wrd1 as CFStringRef, wrd2 as CFStringRef ) as short
|
|
Short err = 0
|
|
// Preliminary tests to generate error codes
|
|
select case
|
|
case fn StringIsEqual( wrd1, wrd2 ) : err = 1
|
|
case len( wrd2 ) < 3 : err = 2
|
|
case len( wrd2 ) - len( wrd1 ) > 1 : err = 3
|
|
case len( wrd1 ) - len( wrd2 ) > 1 : err = 4
|
|
case fn ArrayContainsObject( gUsed, wrd2 ) == Yes : err = 5
|
|
case fn ArrayContainsObject( gWords, wrd2 ) == No : err = 6
|
|
end select
|
|
// Report error. If no error, check against Wordiff rules
|
|
select err
|
|
case 1 : fn Say( @"Don't be silly.", @"\n" )
|
|
case 2 : fn Say( @"New word must be three or more letters.", @"\n" )
|
|
case 3 : fn Say( @"Add just one letter, please.", @"\n" )
|
|
case 4 : fn Say( @"Delete just one letter, please.", @"\n" )
|
|
case 5 : fn Say( fn StringCapitalizedString( wrd2 ), @" was already used.\n" )
|
|
case 6 : fn Say( fn StringCapitalizedString( wrd2 ), @" is not in the dictionary.\n")
|
|
case 0
|
|
err = fn WordiffWords ( wrd1, wrd2 )
|
|
select err
|
|
case 1 : fn Say( @"Either change or add a letter.", @"\n" )
|
|
case 2 : fn Say( @"Either change or delete a letter.", @"\n" )
|
|
case 3 : fn Say( @"Don't change more than one letter.", @"\n")
|
|
end select
|
|
end select
|
|
end fn = err
|
|
|
|
void local fn ShowAllPossible
|
|
CFMutableArrayRef poss
|
|
CFStringRef wrd1, wrd2
|
|
NSUInteger i
|
|
// Check all words in dictionary, ignore error messages
|
|
poss = fn MutableArrayWithCapacity( 0 )
|
|
wrd1 = fn ControlStringValue( _wordInPlay )
|
|
for i = 0 to fn ArrayCount( gWords ) - 1
|
|
wrd2 = fn ArrayObjectAtIndex( gWords, i )
|
|
if fn fabs( len( wrd1 ) - len( wrd2 ) ) < 2 ' Not too long or short?
|
|
if len( wrd2 ) > 2 ' Has more than 2 chars?
|
|
if fn ArrayContainsObject( gUsed, wrd2 ) == No ' Not used before?
|
|
if ( fn WordiffWords( wrd1, wrd2 ) == 0 ) ' According to rules?
|
|
MutableArrayAddObject( poss, wrd2 ) ' Legal, so add to the pot
|
|
end if
|
|
end if
|
|
end if
|
|
end if
|
|
next
|
|
// Display legal words
|
|
fn Say( @"\n", fn ControlStringValue( _playerLabel ) )
|
|
if fn ArrayCount( poss ) > 0 ' Any words left?
|
|
fn Say( @" resigns, but could have chosen:", @"\n" )
|
|
fn MutableStringAppendString( gTxt, fn ArrayComponentsJoinedByString( poss, @", or " ) )
|
|
TextSetString( _textView, gTxt )
|
|
else
|
|
fn Say(@" resigns, there were no words left to play. ", @"New game?\n" )
|
|
end if
|
|
textfield _playerInput, No ' Just to be safe
|
|
end fn
|
|
|
|
void local fn Play
|
|
CFStringRef old, new, name
|
|
NSUInteger n
|
|
// Gather the info
|
|
name = fn ControlStringValue( _playerLabel )
|
|
new = fn ControlStringValue( _playerInput )
|
|
old = fn ArrayLastObject( gUsed )
|
|
if len( new ) == 0 then exit fn ' Just to be safe
|
|
fn Say(new, @"\n" )
|
|
if fn CheckWord( old, new ) == 0
|
|
// Input OK, so get ready next player
|
|
n = ( ( fn ArrayIndexOfObject( gNames, name ) + 1 ) mod fn ArrayCount( gNames ) )
|
|
name = fn ArrayObjectAtIndex( gNames, n )
|
|
textlabel _playerLabel, name
|
|
textfield _wordInPlay, , new
|
|
MutableArrayAddObject( gUsed, new )
|
|
end if
|
|
fn Say( name, @" plays: " )
|
|
textfield _playerInput, , @""
|
|
end fn
|
|
|
|
void local fn StartNewGame
|
|
CFStringRef name, wrd
|
|
NSUInteger n
|
|
// Pick a first player
|
|
n = rnd( fn ArrayCount( gNames ) )
|
|
name = fn ArrayObjectAtIndex( gNames, n - 1 )
|
|
// Pick a first word
|
|
MutableArrayRemoveAllObjects( gUsed )
|
|
do
|
|
n = rnd( fn ArrayCount( gWords ) ) - 1
|
|
wrd = fn ArrayObjectAtIndex( gWords, n )
|
|
until ( len( wrd ) = 3 ) or ( len( wrd ) = 4 )
|
|
MutableArrayAddObject( gUsed, wrd )
|
|
// Update window
|
|
ControlSetFormat( _playerInput, @"abcdefghijklmnopqrstuvwxyz", YES, 0, _formatLowercase )
|
|
fn Say( @"\n", @"Word in play: " ) : fn Say( wrd, @"\n" )
|
|
fn Say( name, @" plays: " )
|
|
textfield _wordInPlay, Yes, wrd
|
|
textlabel _playerLabel, name, (0, 370, 110, 24 )
|
|
textlabel _playsLabel, @"plays:"
|
|
textfield _playerInput, Yes
|
|
button _againBtn, Yes
|
|
button _resignBtn, Yes
|
|
WindowMakeFirstResponder( 1, _playerInput )
|
|
end fn
|
|
|
|
void local fn AskNames
|
|
CFStringRef name
|
|
name = fn ControlStringValue( _playerInput )
|
|
if len( name ) > 0 ' Another player?
|
|
MutableArrayAddObject( gNames, name )
|
|
fn Say( @"Welcome, ", name )
|
|
fn Say( @"!", @"\n" )
|
|
textfield _playerInput, YES, @""
|
|
else
|
|
if fn ArrayFirstObject( gNames ) != Null ' Just to be safe
|
|
fn StartNewGame
|
|
end if
|
|
end if
|
|
end fn
|
|
|
|
void local fn DoDialog( evt as Long, tag as Long )
|
|
select evt
|
|
case _btnClick
|
|
select tag
|
|
case _againBtn
|
|
fn MutableStringSetString( gTxt, @"" )
|
|
fn StartNewGame
|
|
case _resignBtn
|
|
button _resignBtn, No
|
|
fn ShowAllPossible
|
|
case _quitBtn : end
|
|
end select
|
|
case _textFieldDidEndEditing
|
|
if fn ArrayCount( gUsed ) == 0
|
|
fn AskNames
|
|
else
|
|
fn Play
|
|
end if
|
|
case _windowShouldClose : end
|
|
end select
|
|
end fn
|
|
|
|
on dialog fn DoDialog
|
|
|
|
fn BuildInterface
|
|
fn LoadWords
|
|
|
|
handleevents
|