Wordiff

From Rosetta Code
Task
Wordiff
You are encouraged to solve this task according to the task description, using any language you may know.

Wordiff is an original game in which contestants take turns spelling new dictionary words of three or more characters that only differ from the last by a change in one letter.


The change can be either:

  1. a deletion of one letter;
  2. addition of one letter;
  3. or change in one letter.


Note:

  • All words must be in the dictionary.
  • No word in a game can be repeated.
  • The first word must be three or four letters long.


Task

Create a program to aid in the playing of the game by:

  • Asking for contestants names.
  • Choosing an initial random three or four letter word from the dictionary.
  • Asking each contestant in their turn for a wordiff word.
  • Checking the wordiff word is:
  • in the dictionary,
  • not a repetition of past words,
  • and differs from the last appropriately.


Optional stretch goals
  • Add timing.
  • Allow players to set a maximum playing time for the game.
  • An internal timer accumulates how long each user takes to respond in their turns.
  • Play is halted if the maximum playing time is exceeded on a players input.
  • That last player must have entered a wordiff or loses.
  • If the game is timed-out, the loser is the person who took the longest `average` time to answer in their rounds.


Other tasks related to string operations:
Metrics
Counting
Remove/replace
Anagrams/Derangements/shuffling
Find/Search/Determine
Formatting
Song lyrics/poems/Mad Libs/phrases
Tokenize
Sequences



11l

Translation of: Python
V dict_fname = ‘unixdict.txt’

F load_dictionary(String fname = dict_fname)
   ‘Return appropriate words from a dictionary file’
   R Set(File(fname).read().split("\n").filter(word -> re:‘[a-z]{3,}’.match(word)))

F get_players()
   V names = input(‘Space separated list of contestants: ’)
   R names.trim(‘ ’).split(‘ ’, group_delimiters' 1B).map(n -> n.capitalize())

F is_wordiff_removal(word, String prev; comment = 1B)
   ‘Is word derived from prev by removing one letter?’
   V ans = word C Set((0 .< prev.len).map(i -> @prev[0 .< i]‘’@prev[i + 1 ..]))
   I !ans
      I comment
         print(‘Word is not derived from previous by removal of one letter.’)
   R ans

F counter(s)
   DefaultDict[Char, Int] d
   L(c) s
      d[c]++
   R d

F is_wordiff_insertion(String word, prev; comment = 1B) -> Bool
   ‘Is word derived from prev by adding one letter?’
   V diff = counter(word)
   L(c) prev
      I --diff[c] <= 0
         diff.pop(c)
   V diffcount = sum(diff.values())
   I diffcount != 1
      I comment
         print(‘More than one character insertion difference.’)
      R 0B

   V insert = Array(diff.keys())[0]
   V ans = word C Set((0 .. prev.len).map(i -> @prev[0 .< i]‘’@insert‘’@prev[i ..]))
   I !ans
      I comment
         print(‘Word is not derived from previous by insertion of one letter.’)
   R ans

F is_wordiff_change(String word, String prev; comment = 1B) -> Bool
   ‘Is word derived from prev by changing exactly one letter?’
   V diffcount = sum(zip(word, prev).map((w, p) -> Int(w != p)))
   I diffcount != 1
      I comment
         print(‘More or less than exactly one character changed.’)
      R 0B
   R 1B

F is_wordiff(wordiffs, word, dic, comment = 1B)
   ‘Is word a valid wordiff from wordiffs[-1] ?’
   I word !C dic
      I comment
         print(‘That word is not in my dictionary’)
      R 0B
   I word C wordiffs
      I comment
         print(‘That word was already used.’)
      R 0B
   I word.len < wordiffs.last.len
      R is_wordiff_removal(word, wordiffs.last, comment)
   E I word.len > wordiffs.last.len
      R is_wordiff_insertion(word, wordiffs.last, comment)

   R is_wordiff_change(word, wordiffs.last, comment)

F could_have_got(wordiffs, dic)
   R (dic - Set(wordiffs)).filter(word -> is_wordiff(@wordiffs, word, @dic, comment' 0B))

V dic = load_dictionary()
V dic_3_4 = dic.filter(word -> word.len C (3, 4))
V start = random:choice(dic_3_4)
V wordiffs = [start]
V players = get_players()
V cur_player = 0
L
   V name = players[cur_player]
   cur_player = (cur_player + 1) % players.len

   V word = input(name‘: Input a wordiff from '’wordiffs.last‘': ’).trim(‘ ’)
   I is_wordiff(wordiffs, word, dic)
      wordiffs.append(word)
   E
      print(‘YOU HAVE LOST ’name‘!’)
      print(‘Could have used: ’(could_have_got(wordiffs, dic)[0.<10]).join(‘, ’)‘ ...’)
      L.break
Output:
Space separated list of contestants: Paddy Maggie
Paddy: Input a wordiff from 'ease': case
Maggie: Input a wordiff from 'case': casey
Paddy: Input a wordiff from 'casey': carey
Maggie: Input a wordiff from 'carey': care
Paddy: Input a wordiff from 'care': make
More or less than exactly one character changed.
YOU HAVE LOST Paddy!
Could have used: are, bare, cadre, cafe, cage, cake, came, cane, cape, car ...

Arturo

wordset: map read.lines relative "unixdict.txt" => strip

validAnswer?: function [answer][
    if not? contains? wordset answer [
        prints "\tNot a valid dictionary word."
        return false
    ]
    if contains? pastWords answer [
        prints "\tWord already used."
        return false
    ]
    if 1 <> levenshtein answer last pastWords [
        prints "\tNot a correct wordiff."
        return false
    ]
    return true
]

playerA: input "player A: what is your name? "
playerB: input "player B: what is your name? "

pastWords: new @[sample select wordset 'word [ contains? [3 4] size word ]]

print ["\nThe initial word is:" last pastWords "\n"]

current: playerA
while ø [
    neww: strip input ~"|current|, what is the next word? "
    while [not? validAnswer? neww][
        neww: strip input " Try again: "
    ]
    'pastWords ++ neww
    current: (current=playerA)? -> playerB -> playerA
    print ""
]
Output:
player A: what is your name? John
player B: what is your name? Bill

The initial word is: rare 
 
John, what is the next word? bare

Bill, what is the next word? care

John, what is the next word? mares
	Not a valid dictionary word. Try again: mare

Bill, what is the next word? bare
	Word already used. Try again: dare

John, what is the next word? dares
	Not a valid dictionary word. Try again: dire

Bill, what is the next word? dime

John, what is the next word? mime
	Not a valid dictionary word. Try again: dame

Bill, what is the next word? famous
	Not a correct wordiff. Try again: fame

C++

#include <algorithm>
#include <cstdint>
#include <fstream>
#include <iostream>
#include <random>
#include <string>
#include <vector>

std::vector<std::string> request_player_names() {
	std::vector<std::string> player_names;
	std::string player_name;
	for ( uint32_t i = 0; i < 2; ++i ) {
		std::cout << "Please enter the player's name: ";
		std::getline(std::cin, player_name);
		player_names.emplace_back(player_name);
	}
	return player_names;
}

bool is_letter_removed(const std::string& previous_word, const std::string& current_word) {
	for ( uint64_t i = 0; i < previous_word.length(); ++i ) {
		if ( current_word == previous_word.substr(0, i) + previous_word.substr(i + 1) ) {
			return true;
		}
	}
	return false;
}

bool is_letter_added(const std::string& previous_word, const std::string& current_word) {
	return is_letter_removed(current_word, previous_word);
}

bool is_letter_changed(const std::string& previous_word, const std::string& current_word) {
	if ( previous_word.length() != current_word.length() ) {
		return false;
	}

	uint32_t difference_count = 0;
	for ( uint64_t i = 0; i < current_word.length(); ++i ) {
		difference_count += ( current_word[i] == previous_word[i] ) ? 0 : 1;
	}
	return difference_count == 1;
}

bool is_wordiff(const std::string& current_word,
		        const std::vector<std::string>& words_used,
				const std::vector<std::string>& dictionary) {
	if ( std::find(dictionary.begin(), dictionary.end(), current_word) == dictionary.end()
		|| std::find(words_used.begin(), words_used.end(), current_word) != words_used.end() ) {
		return false;
	}

	std::string previous_word = words_used.back();
	return is_letter_changed(previous_word, current_word)
		|| is_letter_removed(previous_word, current_word) || is_letter_added(previous_word, current_word);
}

std::vector<std::string> could_have_entered(const std::vector<std::string>& words_used,
										    const std::vector<std::string>& dictionary) {
	std::vector<std::string> result;
	for ( const std::string& word : dictionary ) {
		if ( std::find(words_used.begin(), words_used.end(), word) == words_used.end()
			&& is_wordiff(word, words_used, dictionary) ) {
			result.emplace_back(word);
		}
	}
	return result;
}

int main() {
	std::vector<std::string> dictionary;
	std::vector<std::string> starters;
	std::fstream file_stream;
	file_stream.open("../unixdict.txt");
	std::string word;
	while ( file_stream >> word ) {
		dictionary.emplace_back(word);
		if ( word.length() == 3 || word.length() == 4 ) {
			starters.emplace_back(word);
		}
	}

	std::random_device rand;
	std::mt19937 mersenne_twister(rand());
	std::shuffle(starters.begin(), starters.end(), mersenne_twister);
	std::vector<std::string> words_used;
	words_used.emplace_back(starters[0]);

	std::vector<std::string> player_names = request_player_names();
	bool playing = true;
	uint32_t playerIndex = 0;
	std::string current_word;
	std::cout << "The first word is: " << words_used.back() << std::endl;

	while ( playing ) {
		std::cout << player_names[playerIndex] << " enter your word: ";
		std::getline(std::cin, current_word);
		if ( is_wordiff(current_word, words_used, dictionary) ) {
			words_used.emplace_back(current_word);
			playerIndex = ( playerIndex == 0 ) ? 1 : 0;
		} else {
			std::cout << "You have lost the game, " << player_names[playerIndex] << std::endl;
			std::vector<std::string> missed_words = could_have_entered(words_used, dictionary);
			std::cout << "You could have entered: [";
			for ( uint64_t i = 0; i < missed_words.size() - 1; ++i ) {
				std::cout << missed_words[i] << ", ";
			}
			std::cout << missed_words.back() << "]" << std::endl;
			playing = false;
		}
	}
}
Output:
Please enter the player's name: Alice
Please enter the player's name: Bob
The first word is: her
Alice enter your word: hem
Bob enter your word: hemp
Alice enter your word: temp
You have lost the game, Alice
You could have entered: [heap, help, hump, kemp]

FreeBASIC

REM  Wordiff  ' 10 march 2024 '

'--- Declaración de variables globales ---
Dim Shared As String words()
Dim Shared As String used()
Dim Shared As String player1
Dim Shared As String player2
Dim Shared As String player
Dim Shared As String prevWord
Dim Shared As Integer prevLen

'--- SUBrutinas y FUNCiones ---
Sub loadWords
    Dim As Integer i, j, numLines
    Dim As String linea
    
    Open "unixdict.txt" For Input As #1
    While Not Eof(1)
        Line Input #1, linea
        If Len(linea) = 3 Or Len(linea) = 4 Then
            Redim Preserve words(numLines)
            words(numLines) = linea
            numLines += 1
        End If
    Wend
    Close #1
End Sub

Sub Intro
    Cls
    Color 10, 0 '10, black
    Locate 10, 30 : Print "---WORDIFF---"
    Locate 12, 5 : Print "Por turnos, teclear nuevas palabras del "
    Locate 13, 5 : Print "diccionario de tres o mas caracteres que "
    Locate 14, 5 : Print "se diferencien de la anterior en una letra."
    
    Color 14
    Locate 16, 5 : Input "Player 1, please enter your name: ", player1
    Locate 17, 5 : Input "Player 2, please enter your name: ", player2
    If player2 = player1 Then player2 &= "2"
    Color 7
End Sub


Function wordExists(word As String) As Boolean
    For i As Integer = 0 To Ubound(words)
        If words(i) = word Then Return True
    Next i
    Return False
End Function

Function wordUsed(word As String) As Boolean
    For i As Integer = 0 To Ubound(used)
        If used(i) = word Then Return True
    Next i
    Return False
End Function

Sub addUsedWord(word As String)
    Redim Preserve used(Ubound(used) + 1)
    used(Ubound(used)) = word
End Sub

Sub MenuPrincipal
    Dim As String word
    Dim As Integer i, changes, longi
    Dim As Boolean ok
    
    Cls
    prevWord = words(Int(Rnd * Ubound(words)))
    prevLen = Len(prevWord)
    player = player1
    Print "The first word is "; 
    Color 15 : Print prevWord : Color 7
    Do
        Color 7 : Print player; ", plays:";
        Color 15 : Input " ", word
        word = Lcase(word)
        longi = Len(word)
        ok = False
        Color 12
        If longi < 3 Then
            Print "Words must be at least 3 letters long."
        Elseif wordExists(word) = 0 Then
            Print "Not in dictionary."
        Elseif wordUsed(word) <> 0 Then
            Print "Word has been used before."
        Elseif word = prevWord Then
            Print "You must change the previous word."
        Elseif longi = prevLen Then
            changes = 0
            For i = 1 To longi
                If Mid(word, i, 1) <> Mid(prevWord, i, 1) Then changes += 1
            Next i
            If changes > 1 Then
                Print "Only one letter can be changed."
            Else
                ok = True
            End If
        Else
            Print "Invalid change."
        End If
        If ok Then
            prevLen = longi
            prevWord = word
            addUsedWord(word)
            player = Iif(player = player1, player2, player1)
        Else
            Print "So, sorry "; player; ", you've lost!"
            Dim As String KBD
            Do: KBD = Inkey: Loop While KBD = ""
            Exit Do
        End If
    Loop
End Sub

'--- Programa Principal ---
Randomize Timer
Intro
loadWords
MenuPrincipal
End
'--------------------------

FutureBasic

  
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
Output:

J

require'general/misc/prompt'
wordiff=: {{
  words=: cutLF tolower fread'unixdict.txt'
  c1=: prompt 'Name of contestant 1: '
  c2=: prompt 'Name of contestant 2: '
  hist=. ,word=. ({~ ?@#) (#~ 3 4 e.~ #@>) words
  echo 'First word is ',toupper;word
  echo 'each contestant must pick a new word'
  echo 'the new word must either change 1 letter or remove or add 1 letter'
  echo 'the new word cannot be an old word'
  while. do.
    next=. <tolower prompt 'Pick a new word ',c1,': '
    if. next e. hist do.
      echo next,&;' has already been picked'
      break.
    end.
    if. -. next e. words do.
      echo next,&;' is not in the dictionary'
      break.
    end.
    if. next =&#&> word do.
      if. 1~:+/d=.next~:&;word do.
        echo next,&;' differs from ',word,&;' by ',(":d),' characters'
        break.
      end.
    else.
      if. -. */1=(-&#&>/,[:+/=/&>/)(\:#@>) next,word do.
        echo next,&;' differs too much from ',;word
        break.
      end.
    end.
    hist=. hist,word=. next
    'c2 c1'=. c1;c2
  end.
  echo c2,' wins'
}}

Example game:

   wordiff''
Name of contestant 1: Jack
Name of contestant 2: Jill
First word is FLAX
each contestant must pick a new word
the new word must either change 1 letter or remove or add 1 letter
the new word cannot be an old word
Pick a new word Jack: flak
Pick a new word Jill: flap
Pick a new word Jack: clap
Pick a new word Jill: slap
Pick a new word Jack: slam
Pick a new word Jill: slap
slap has already been picked
Jack wins

Java

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Scanner;
import java.util.stream.Collectors;

public final class Wordiff {

	public static void main(String[] args) throws IOException {
		List<String> dictionary = Files.lines(Path.of("unixdict.txt")).toList();
		List<String> starters = dictionary.stream()
			.filter( word -> word.length() == 3 || word.length() == 4 ).collect(Collectors.toList());
		Collections.shuffle(starters);
		List<String> wordsUsed = new ArrayList<String>();
		wordsUsed.add(starters.get(0));		
		
		Scanner scanner = new Scanner(System.in);
		List<String> playerNames = requestPlayerNames(scanner);		
		boolean playing = true;
		int playerIndex = 0;
		System.out.println("The first word is: " + wordsUsed.get(wordsUsed.size() -  1));		
		
		while ( playing ) {			
			System.out.println(playerNames.get(playerIndex) + " enter your word: ");
			String currentWord = scanner.nextLine();
			if ( isWordiff(currentWord, wordsUsed, dictionary) ) {
				wordsUsed.add(currentWord);
				playerIndex = ( playerIndex == 0 ) ? 1 : 0;
			} else {
				System.out.println("You have lost the game, " + playerNames.get(playerIndex));
				System.out.println("You could have entered: " + couldHaveEntered(wordsUsed, dictionary));
				playing = false;
			}			
		}
		scanner.close();
	}	
	
	private static boolean isWordiff(String currentWord, List<String> wordsUsed, List<String> dictionary) {
		if ( ! dictionary.contains(currentWord) || wordsUsed.contains(currentWord) ) {
			return false;
		}
	
		String previousWord = wordsUsed.get(wordsUsed.size() - 1);
		return isLetterChanged(previousWord, currentWord) 
			|| isLetterRemoved(previousWord, currentWord) || isLetterAdded(previousWord, currentWord);
	}
	
	private static boolean isLetterRemoved(String previousWord, String currentWord) {
		for ( int i = 0; i < previousWord.length(); i++ ) {
		    if ( currentWord.equals(previousWord.substring(0, i) + previousWord.substring(i + 1)) ) {
		    	return true;
		    }
		}
		return false;
	} 
	
	private static boolean isLetterAdded(String previousWord, String currentWord) {
		return isLetterRemoved(currentWord, previousWord);
	}

	private static boolean isLetterChanged(String previousWord, String currentWord) {
		if ( previousWord.length() != currentWord.length() ) {
			return false;
		}
		
		int differenceCount = 0;
		for ( int i = 0; i < currentWord.length(); i++ ) {
		    differenceCount += ( currentWord.charAt(i) == previousWord.charAt(i) ) ? 0 : 1;
		}
		return differenceCount == 1;
	}	
	
	private static List<String> couldHaveEntered(List<String> wordsUsed, List<String> dictionary) {
		List<String> result = new ArrayList<String>();		
	    for ( String word : dictionary ) {
		    if ( ! wordsUsed.contains(word) && isWordiff(word, wordsUsed, dictionary) ) {
		        result.add(word);
		    }
	    }
	    return result;
	}
	
	private static List<String> requestPlayerNames(Scanner scanner) {
		List<String> playerNames = new ArrayList<String>();
		for ( int i = 0; i < 2; i++ ) {
			System.out.print("Please enter the player's name: ");	   
			String playerName = scanner.nextLine().trim();
			playerNames.add(playerName);
		}
	    return playerNames;
	}

}
Output:
Please enter the player's name: Alice
Please enter the player's name: Bob
The first word is: tnt
Alice enter your word: 
tent
Bob enter your word: 
teat
Alice enter your word: 
team
Bob enter your word: 
steam
Alice enter your word: 
steamy
Bob enter your word: 
steams
You have lost the game, Bob
You could have entered: [seamy, steady]

jq

Adapted from Wren

Works with jq, the C implementation of jq

Works with gojq, the Go implementation of jq

In case the word list is large, binary search is used when looking up a proposed word. If the word list is already sorted, the sorting step is skipped.

As of this writing, the C and Go implementations of jq do not include a PRN generator, and so in the following the MRG32k3a module available at Category:jq/MRG32k3a.jq is used.

include "MRG32k3a" {search: "."};  # see comment above

### Generic functions

# Determine if stream is non-decreasing
def is_sorted(stream):
  first(foreach stream as $s ( null; 
    if . == null or $s >= .[0] then [$s]
    else 0
    end;
    select(. == 0) ) )
  // 1
  | . == 1;

# Write to stderr
def inform(msg):
  . as $in
  | ("\(msg)\n" | stderr)
  | $in;

# q means quit
def read($prompt; $regex):
  def r:
    ($prompt | stderr | empty),
    (try ((input
          | if . == "q" then halt 
            else select(test($regex))
            end) // r)
     catch if . == "break" then halt else r end );
  r;

# Returns levenshteinDistance(s1; $s2) <= $max
# A recursive algorithm is good enough if $max is small
def levenshteinDistance($s1; $s2; $max):
  def lev:
    . as [$s1, $s2, $max]
    | if   ($s1|length) == 0 then ($s2|length) <= $max
      elif ($s2|length) == 0 then ($s1|length) <= $max
      elif $s1[:1] == $s2[:1]
      then [$s1[1:], $s2[1:], $max] | lev
      else ($s1|length) <= $max
        or ($s2|length) <= $max
        or ([$s1[1:], $s2, $max-1] | lev)
        or ([$s1, $s2[1:], $max-1] | lev)
        or ([$s1[1:], $s2[1:], $max-1] | lev)
    end ;
  [$s1, $s2, $max] | lev;

##### The Wordiff Game

# Output: the sorted list of words
# The sort is skipped if we can readily determine the list is already sorted
def words:
  [$dict | splits(" *\n *")]
  | if is_sorted(.[]) then .
    else sort
    end;

def player1: read("Player 1, please enter your name : "; ".");
def player2: read("Player 2, please enter your name : "; ".");

def round:
  .words as $words
  | read("\(.player), enter your word: "; ".") as $word
  | ($word|length) as $len
  | .ok = false
  | if $len < 3
    then inform("Words must be at least three letters long.")
    elif $word == .prevWord
    then  inform("You must change the previous word.")
    elif .used[$word]
    then inform("The word \"\($word)\" has been used before.")
    elif # not in dictionary
         # if $words is not sorted:  ($word | IN($words[]) | not)
         # binary search:
         ($words | bsearch($word) < 0)
    then inform("Not in dictionary.")

    elif levenshteinDistance($word; .prevWord; 1)
    #### Good to go
    then .ok = true
    | .prevLen = ($word|length)
    | .prevWord = $word
    | .used[$word] = true
    | .player = (if .player == .player1 then .player2 else .player1 end)
    else inform("Sorry. Only one addition, deletion or alteration is allowed.")
    end
  | if (.ok|not) then inform("Please retry. The current word is: \(.prevWord)") end
  | round ;

def play:
  player1 as $player1
  | { player1: $player1,
      player2: ( player2 | if . == $player1 then . + "2" else . end)
    }
  | inform("Reading and perhaps sorting the list of words takes a few seconds... ")
  | inform("Meanwhile note that you can quit by entering q at a prompt.")
  | .words = words
  # Avoid storing the subset of words needed to make a random selection:
  | (.words | map(select(length | IN(3,4))) | .[length | prn(1)[]]) as $firstWord
  | .prevLen = ($firstWord|length)
  | .prevWord = $firstWord
  | .used = {($firstWord): true}  # a JSON object for efficiency
  | .player = .player1
  | "\nThe first word is: \($firstWord)",
     round;

play

Invocation': jq -nrRf --rawfile dict unixdict.txt wordiff.jq

Output:
Player 1, please enter your name : Rosetta
Player 2, please enter your name : Code
Reading and perhaps sorting the list of words takes a minute... 
Meanwhile note that you can quit by entering q at a prompt.

The first word is: show
Rosetta, enter your word: stow
Code, enter your word: stowaway
Sorry. Only one addition, deletion or alteration is allowed.
Please retry. The current word is: stow
Code, enter your word: tow
Rosetta, enter your word: town
Code, enter your word: 
Code, enter your word: yown
Not in dictionary.
Please retry. The current word is: town
Code, enter your word: q

Julia

isoneless(nw, ow) = any(i -> nw == ow[begin:i-1] * ow[i+1:end], eachindex(ow))
isonemore(nw, ow) = isoneless(ow, nw)
isonechanged(x, y) = length(x) == length(y) && count(i -> x[i] != y[i], eachindex(y)) == 1
onefrom(nw, ow) = isoneless(nw, ow) || isonemore(nw, ow) || isonechanged(nw, ow)

function askprompt(prompt)
    ans = ""
    while isempty(ans)
        print(prompt)
        ans = strip(readline())
    end
    return ans
end

function wordiff(dictfile = "unixdict.txt")
    wordlist = [w for w in split(read(dictfile, String), r"\s+") if !occursin(r"\W", w) && length(w) > 2]
    starters = [w for w in wordlist if 3 <= length(w) <= 4]

    timelimit = something(tryparse(Float64, askprompt("Time limit (min) or 0 for none: ")), 0.0)

    players = split(askprompt("Enter players' names. Separate by commas: "), r"\s*,\s*")
    times, word = Dict(player => Float32[] for player in players), rand(starters)
    used, totalsecs, timestart = [word], timelimit * 60, time()
    while length(players) > 1
        player = popfirst!(players)
        playertimestart = time()
        newword = askprompt("$player, your move. The current word is $word.  Your worddiff? ")
        if timestart + totalsecs > time()
            if onefrom(newword, word) && !(newword in used) && lowercase(newword) in wordlist
                println("Correct.")
                push!(players, player)
                word = newword
                push!(used, newword)
                push!(times[player], time() - playertimestart)
            else
                println("Wordiff choice incorrect. Player $player exits game.")
            end
        else  # out of time
            println("Sorry, time was up. Timing ranks for remaining players:")
            avtimes = Dict(p => isempty(times[p]) ? NaN : sum(times[p]) / length(times[p])
                for p in players)
            sort!(players, lt = (x, y) -> avtimes[x] < avtimes[y])
            foreach(p -> println("    $p:", lpad(avtimes[p], 10), " seconds average"), players)
            break
        end
        sleep(rand() * 3)
    end
    length(players) < 2 && println("Player $(first(players)) is the only one left, and wins the game.")
end

wordiff()
Output:
Time limit (min) or 0 for none: 0.7
Enter players' names. Separate by commas: Ann, Betty, Sam, Ron, Kim
Ann, your move. The current word is city.  Your worddiff? cite
Correct.
Betty, your move. The current word is cite.  Your worddiff? kite
Correct.
Sam, your move. The current word is kite.  Your worddiff? kit
Correct.
Ron, your move. The current word is kit.  Your worddiff? sit
Correct.
Kim, your move. The current word is sit.  Your worddiff? it
Wordiff choice incorrect. Player Kim exits game.
Ann, your move. The current word is sit.  Your worddiff? site
Correct.
Betty, your move. The current word is site.  Your worddiff? mite
Sorry, time was up. Timing ranks for remaining players:
    Ron:     4.134 seconds average
    Ann:     6.265 seconds average
    Sam: 6.9189997 seconds average

Nim

Translation of: Python
import httpclient, sequtils, sets, strutils, sugar
from unicode import capitalize

const
  DictFname = "unixdict.txt"
  DictUrl1 = "http://wiki.puzzlers.org/pub/wordlists/unixdict.txt"    # ~25K words
  DictUrl2 = "https://raw.githubusercontent.com/dwyl/english-words/master/words.txt"  # ~470K words


type Dictionary = HashSet[string]


proc loadDictionary(fname = DictFname): Dictionary =
  ## Return appropriate words from a dictionary file.
  for word in fname.lines():
    if word.len >= 3 and word.allCharsInSet(Letters): result.incl word.toLowerAscii


proc loadWebDictionary(url: string): Dictionary =
  ## Return appropriate words from a dictionary web page.
  let client = newHttpClient()
  for word in client.getContent(url).splitLines():
    if word.len >= 3 and word.allCharsInSet(Letters): result.incl word.toLowerAscii


proc getPlayers(): seq[string] =
  ## Return inputted ordered list of contestant names.
  try:
    stdout.write "Space separated list of contestants: "
    stdout.flushFile()
    result = stdin.readLine().splitWhitespace().map(capitalize)
    if result.len == 0:
      quit "Empty list of names. Quitting.", QuitFailure
  except EOFError:
    echo()
    quit "Encountered end of file. Quitting.", QuitFailure


proc isWordiffRemoval(word, prev: string; comment = true): bool =
  ## Is "word" derived from "prev" by removing one letter?
  for i in 0..prev.high:
    if word == prev[0..<i] & prev[i+1..^1]: return true
  if comment: echo "Word is not derived from previous by removal of one letter."
  result = false


proc isWordiffInsertion(word, prev: string; comment = true): bool =
  ## Is "word" derived from "prev" by adding one letter?
  for i in 0..word.high:
    if prev == word[0..<i] & word[i+1..^1]: return true
  if comment: echo "Word is not derived from previous by insertion of one letter."
  return false


proc isWordiffChange(word, prev: string; comment = true): bool =
  ## Is "word" derived from "prev" by changing exactly one letter?
  var diffcount = 0
  for i in 0..word.high:
    diffcount += ord(word[i] != prev[i])
  if diffcount != 1:
    if comment:
      echo "More or less than exactly one character changed."
    return false
  result = true


proc isWordiff(word: string; wordiffs: seq[string]; dict: Dictionary; comment = true): bool =
  ## Is "word" a valid wordiff from "wordiffs[^1]"?
  if word notin dict:
    if comment:
      echo "That word is not in my dictionary."
      return false
  if word in wordiffs:
    if comment:
      echo "That word was already used."
      return false
  result = if word.len < wordiffs[^1].len: word.isWordiffRemoval(wordiffs[^1], comment)
           elif word.len > wordiffs[^1].len: word.isWordiffInsertion(wordiffs[^1], comment)
           else: word.isWordiffChange(wordiffs[^1], comment)


proc couldHaveGot(wordiffs: seq[string]; dict: Dictionary): seq[string] =
  for word in dict - wordiffs.toHashSet:
    if word.isWordiff(wordiffs, dict, comment = false):
      result.add word


when isMainModule:
  import random

  randomize()
  let dict = loadDictionary(DictFname)
  let dict34 = collect(newSeq):
                 for word in dict:
                   if word.len in [3, 4]: word
  let start = sample(dict34)
  var wordiffs = @[start]
  let players = getPlayers()
  var iplayer = 0
  var word: string
  while true:
    let name = players[iplayer]
    while true:
      stdout.write "$1, input a wordiff from “$2”: ".format(name, wordiffs[^1])
      stdout.flushFile()
      try:
        word = stdin.readLine().strip()
        if word.len > 0: break
      except EOFError:
        quit "Encountered end of file. Quitting.", QuitFailure
    if word.isWordiff(wordiffs, dict):
      wordiffs.add word
    else:
      echo "You have lost, $#.".format(name)
      let possibleWords = couldHaveGot(wordiffs, dict)
      if possibleWords.len > 0:
        echo "You could have used: ", possibleWords[0..min(possibleWords.high, 20)].join(" ")
      break
    iplayer = (iplayer + 1) mod players.len
Output:
Space separated list of contestants: Paddy Maggie
Paddy, input a wordiff from “beta”: bet
Maggie, input a wordiff from “bet”: bee
Paddy, input a wordiff from “bee”: tee
Maggie, input a wordiff from “tee”: teen
Paddy, input a wordiff from “teen”: teeny
That word is not in my dictionary.
You have lost, Paddy.
You could have used: then steen tern keen teem teet been seen ten


Phix

Library: Phix/pGUI

Doubtless this could be improved in umpteen ways. Not quite yet working under pwa/p2js, but not too far off.

-- demo\rosetta\Wordiff.exw
include pGUI.e
Ihandle dlg, playerset, playtime, current, remain, turn, input, 
        help, quit, hframe, history, timer
atom t0, t1, t=0

constant title = "Wordiff game",
     help_text = """
Allows single or multi-player modes.
Enter eg "Pete" to play every round yourself,
"Computer" for the computer to play itself,
"Pete,Computer" (or vice versa) to play against the computer,
"Pete,Sue" for a standard two-payer game, or 
"Pete,Computer,Sue,Computer" for auto-plays between each human.
Words must be 3 letters or more, and present in the dictionary,
and not already used. You must key return (not tab) to finish
entering your move. The winner is the fastest average time, if
the timer is running (/non-zero), otherwise play continues 
until player elimination leaves one (or less) remaining.
NB: Pressing tab or clicking on help will restart or otherwise
mess up the gameplay.
"""
function help_cb(Ihandln /*ih*/)
    IupMessage(title,help_text)
    IupSetFocus(dlg)
    return IUP_DEFAULT
end function

function over2(string word) return length(word)>2 end function
function less5(string word) return length(word)<5 end function

sequence words = filter(unix_dict(),over2),
         valid = {},
         used = {}

string word
integer lw

sequence players, eliminated, times, averages
integer player

function levenshtein1(string w)
    bool res = false
    integer l = length(w)
    if not find(w,used) and abs(l-lw)<=1 then
        sequence costs = tagset(l+1,0)
        for i=1 to lw do
            costs[1] = i
            integer newcost = i-1, pj = i
            for j=1 to l do
                integer cj = costs[j+1],
                        ne = word[i]!=w[j],
                        nc = newcost+ne
                pj = min({pj+1, cj+1, nc})
                costs[j+1] = pj
                newcost = cj
            end for
        end for
        res = costs[$-1]==1
    end if
    return res
end function

procedure game_over()
    IupSetAttribute(history,"APPENDITEM","GAME OVER:")
    if length(valid) then
        string valids = "You could have had "&join(valid,", ")
        IupSetAttribute(history,"APPENDITEM",valids)
    end if
    string winner = "nobody"
    atom best = -1
    for i=1 to length(players) do
        string player = players[i], msg
        if eliminated[i] then
            msg = ": eliminated"
        else
            atom average = averages[i]
            if average=-1 then
                msg = ": no times"
            else
                msg = sprintf(": %.3f",average)
                if best=-1 or average<best then
                    winner = player
                    best = average
                end if
            end if
        end if
        IupSetAttribute(history,"APPENDITEM",player&msg)
    end for
    IupSetAttribute(history,"APPENDITEM","And the winner is: "&winner)
    IupSetInt(history,"TOPITEM",IupGetInt(history,"COUNT"))
    IupSetInt(timer,"RUN",false)
end procedure

procedure score(string move)
    times[player] = append(times[player],time()-t1)
    averages[player] = sum(times[player])/length(times[player])
    used = append(used,move)
    word = move
    lw = length(word)
    valid = filter(words,levenshtein1)
    IupSetStrAttribute(current,"TITLE","Current word: "&word)
end procedure

procedure advance_player()
    while true do
        player = mod(player,length(players))+1
        if not eliminated[player] then exit end if
    end while
    IupSetStrAttribute(turn,"TITLE",players[player]&"'s turn:")
    IupRefreshChildren(turn)
    IupSetStrAttribute(input,"VALUE",word)
    t1 = time()
end procedure

procedure autoplay()
    while true do
        if length(valid)=0 then
            IupSetAttribute(history,"APPENDITEM","no more moves possible")
            game_over()
            exit
        end if
        if proper(players[player])!="Computer" then exit end if
        string move = valid[rand(length(valid))]
        IupSetStrAttribute(history,"APPENDITEM","%s's move: %s\n",
                           {players[player],move})  
        IupSetInt(history,"TOPITEM",IupGetInt(history,"COUNT"))
        score(move)
        advance_player()
    end while
end procedure

procedure new_game(bool bStart=true)
    bool bActive = length(players)!=0
    IupSetInt(turn,"ACTIVE",bActive)
    IupSetInt(input,"ACTIVE",bActive)
    if bActive and bStart then
        sequence w34 = filter(words,less5)
        while true do
            integer r = rand(length(w34))
            word = w34[r]
            lw = length(word)
            used = {word}
            valid = filter(words,levenshtein1)
            if length(valid)!=0 then exit end if
            w34[r..r] = {}
        end while
        IupSetStrAttribute(current,"TITLE","Current word: "&word)
        IupSetStrAttribute(turn,"TITLE",players[player]&"'s turn:")
        IupRefreshChildren(turn)
        IupSetStrAttribute(input,"VALUE",word)
        IupSetAttribute(history,"REMOVEITEM","ALL")
        IupSetAttribute(history,"APPENDITEM","Initial word: "&word)
        IupSetInt(history,"TOPITEM",IupGetInt(history,"COUNT"))
        integer l = length(players)
        eliminated = repeat(false,l)
        times = repeat({},l)
        averages = repeat(-1,l)
        t0 = time()
        t1 = time()
        IupSetInt(timer,"RUN",t!=0)
        autoplay()
    end if
end procedure

function players_cb(Ihandln /*playerset*/)
    players = split(IupGetAttribute(playerset,"VALUE"),",")
    player = 1
    new_game(false)
    return IUP_DEFAULT
end function

function playtime_cb(Ihandle /*playtime*/)
    t = IupGetInt(playtime, "VALUE")
    if t then
        IupSetInt(remain,"VISIBLE",true)
        IupSetStrAttribute(remain,"TITLE","Remaining: %.1fs",{t})
    else
        IupSetInt(remain,"VISIBLE",false)
    end if
    IupRefreshChildren(remain)
    return IUP_DEFAULT
end function

function focus_cb(Ihandle /*input*/)
    new_game(true)
    return IUP_DEFAULT
end function

procedure verify_move()
    string move = IupGetAttribute(input,"VALUE"),
           okstr = "ok" 
    bool ok = not find(move,used)
    if not ok then
        okstr = "already used"
    else
        ok = find(move,words)
        if not ok then
            okstr = "not in dictionary"
        else
            ok = find(move,valid)
            if not ok then
                okstr = "more than one change"
            else
                used = append(used,move)
            end if
        end if
    end if
    if not ok then
        okstr &= ", player eliminated"
    end if
    IupSetStrAttribute(history,"APPENDITEM","%s's move: %s %s\n",
                       {players[player],move,okstr})    
    IupSetInt(history,"TOPITEM",IupGetInt(history,"COUNT"))
    if not ok then
        eliminated[player] = true
        if length(players)-sum(eliminated)<=1 then
            game_over()
            return
        end if
    else
        score(move)
    end if  
    advance_player()
    autoplay()
end procedure

function timer_cb(Ihandle /*timer*/)
    atom e = time()-t0
    if e>=t then
        IupSetStrAttribute(remain,"TITLE","Remaining: 0s")
        IupSetInt(turn,"ACTIVE",false)
        IupSetInt(input,"ACTIVE",false)
        game_over()
    else
        IupSetStrAttribute(remain,"TITLE","Remaining: %.1fs",{t-e})
    end if
    return IUP_DEFAULT
end function

function quit_cb(Ihandle /*ih*/)
    return IUP_CLOSE
end function

function key_cb(Ihandle /*dlg*/, atom c)
    if c=K_ESC then return IUP_CLOSE
    elsif c=K_CR then
        Ihandln focus = IupGetFocus()
        if focus=playerset then
            IupSetFocus(playtime)
        elsif focus=playtime then
            IupSetFocus(input)
            new_game()
        elsif focus=input then
            verify_move()
        end if
    elsif c=K_F1 then return help_cb(NULL)
    elsif c=K_cC then
        integer n = IupGetInt(history,"COUNT")
        sequence hist = repeat(0,n)
        for i=1 to n do
            hist[i] = IupGetAttributeId(history,"",i)
        end for
        hist = join(hist,"\n")
        Ihandln clip = IupClipboard()
        IupSetAttribute(clip,"TEXT",hist)
        clip = IupDestroy(clip)
    end if
    return IUP_CONTINUE
end function

IupOpen()
playerset = IupText(`EXPAND=HORIZONTAL`)
playtime = IupText(`SPIN=Yes, SPINMIN=0, RASTERSIZE=48x`)
IupSetCallback({playerset,playtime},"KILLFOCUS_CB",Icallback("players_cb"))
IupSetCallback(playtime,"VALUECHANGED_CB",Icallback("playtime_cb"))
turn = IupLabel("turn","ACTIVE=NO")
input = IupText("EXPAND=HORIZONTAL, ACTIVE=NO")
IupSetCallback(input,"GETFOCUS_CB",Icallback("focus_cb"))
current = IupLabel("Current word:","EXPAND=HORIZONTAL")
remain = IupLabel("Remaining time:0s","VISIBLE=NO")
history = IupList("VISIBLELINES=10, EXPAND=YES, CANFOCUS=NO")
hframe = IupFrame(history,"TITLE=History, PADDING=5x4")
help = IupButton("Help (F1)",Icallback("help_cb"),"PADDING=5x4")
quit = IupButton("Close", Icallback("quit_cb"))
timer = IupTimer(Icallback("timer_cb"),100,false)
sequence buttons = {IupFill(),help,IupFill(),quit,IupFill()}
constant acp = "ALIGNMENT=ACENTER, PADDING=5"
Ihandle settings = IupHbox({IupLabel("Contestant name(s)"),
                            playerset,
                            IupLabel("Timer (seconds)"),
                            playtime},acp),
        currbox = IupHbox({current,remain},acp),
        numbox = IupHbox({turn,input},acp),
        btnbox = IupHbox(buttons,"PADDING=40, NORMALIZESIZE=BOTH"),
        vbox = IupVbox({settings,
                        currbox,
                        numbox, hframe, btnbox}, "GAP=5,MARGIN=5x5")
dlg = IupDialog(vbox, `TITLE="%s", SIZE=500x220`, {title})
IupSetCallback(dlg, "K_ANY", Icallback("key_cb"))
IupShow(dlg)
if platform()!=JS then
    IupMainLoop()
    IupClose()
end if
Output:

A quick timed-out game:

Initial word: farm
Pete's move: harm ok
Sue's move: warm ok
Pete's move: war ok
Sue's move: ward ok
Pete's move: hard ok
Sue's move: lard ok
Pete's move: card ok
GAME OVER:
You could have had bard, car, care, carl, carp, carr, cart, chard, cord, curd, yard
Pete: 2.519
Sue: 3.078
And the winner is: Pete

Computer playing against itself:

Initial word: sage
Computer's move: sake
Computer's move: take
Computer's move: wake
Computer's move: wave
Computer's move: ware
Computer's move: aware
Computer's move: awake
Computer's move: awaken
Computer's move: waken
Computer's move: taken
Computer's move: oaken
no more moves possible
GAME OVER:
Computer: 0.000
And the winner is: Computer

Perl

Borrowed code from Levenshtein_distance.

use strict;
use warnings;
use feature 'say';
use List::Util 'min';

my %cache;
sub leven {
    my ($s, $t) = @_;
    return length($t) if $s eq '';
    return length($s) if $t eq '';
    $cache{$s}{$t} //=
      do {
        my ($s1, $t1) = (substr($s, 1), substr($t, 1));
        (substr($s, 0, 1) eq substr($t, 0, 1))
          ? leven($s1, $t1)
          : 1 + min(
                    leven($s1, $t1),
                    leven($s,  $t1),
                    leven($s1, $t ),
            );
      };
}

print "What is your name?"; my $name = <STDIN>;
$name = 'Number 6';
say "What is your quest? Never mind that, I will call you '$name'";
say 'Hey! I am not a number, I am a free man!';

my @starters = grep { length() < 6 } my @words = grep { /.{2,}/ } split "\n", `cat unixdict.txt`;

my(%used,@possibles,$guess);
my $rounds = 0;
my $word = say $starters[ rand $#starters ];

while () {
    say "Word in play: $word";
    $used{$word} = 1;
    @possibles = ();
    for my $w (@words) {
        next if abs(length($word) - length($w)) > 1;
        push @possibles, $w if leven($word, $w) == 1 and ! defined $used{$w};
    }
    print "Your word? "; $guess = <STDIN>;  chomp $guess;
    last unless grep { $guess eq $_ } @possibles;
    $rounds++;
    $word = $guess;
}

my $already = defined $used{$guess} ? " '$guess' was already played but" : '';

if (@possibles) { say "\nSorry $name,${already} one of <@possibles> would have continued the game." }
else            { say "\nGood job $name,${already} there were no possible words to play."                }
say "You made it through $rounds rounds.";
Output:
What is your name? Sir Lancelot of Camelot
What is your quest?  Never mind that, I'm going to call you 'Number 6'
Hey! I am not a number, I am a free man!
Word in play: ago
Your word? age
Word in play: age
Your word? rage
Word in play: rage
Your word? range
Word in play: range
Your word? orange
Word in play: orange
Your word?

Good job Number 6, there were no possible words to play.
You made it through 4 rounds.

Python

This is without timing, but ends by showing some wordiffs from the dictionary that could have worked on failure.

# -*- coding: utf-8 -*-

from typing import List, Tuple, Dict, Set
from itertools import cycle, islice
from collections import Counter
import re
import random
import urllib

dict_fname = 'unixdict.txt'
dict_url1 = 'http://wiki.puzzlers.org/pub/wordlists/unixdict.txt'    # ~25K words
dict_url2 = 'https://raw.githubusercontent.com/dwyl/english-words/master/words.txt'  # ~470K words

word_regexp = re.compile(r'^[a-z]{3,}$')    # reduce dict words to those of three or more a-z characters.


def load_dictionary(fname: str=dict_fname) -> Set[str]:
    "Return appropriate words from a dictionary file"
    with open(fname) as f:
        return {lcase for lcase in (word.strip().lower() for word in f)
                if word_regexp.match(lcase)}

def load_web_dictionary(url: str) -> Set[str]:
    "Return appropriate words from a dictionary web page"
    words = urllib.request.urlopen(url).read().decode().strip().lower().split()
    return {word for word in words if word_regexp.match(word)}


def get_players() -> List[str]:
    "Return inputted ordered list of contestant names."
    names = input('Space separated list of contestants: ')
    return [n.capitalize() for n in names.strip().split()]

def is_wordiff(wordiffs: List[str], word: str, dic: Set[str], comment=True) -> bool:
    "Is word a valid wordiff from wordiffs[-1] ?"
    if word not in dic:
        if comment: 
            print('That word is not in my dictionary')
        return False
    if word in wordiffs:
        if comment: 
            print('That word was already used.')
        return False
    if len(word) < len(wordiffs[-1]):
        return is_wordiff_removal(word, wordiffs[-1], comment)
    elif len(word) > len(wordiffs[-1]):
        return is_wordiff_insertion(word, wordiffs[-1], comment)
    
    return is_wordiff_change(word, wordiffs[-1], comment)

        
def is_wordiff_removal(word: str, prev: str, comment=True) -> bool:
    "Is word derived from prev by removing one letter?"
    ...
    ans = word in {prev[:i] + prev[i+1:] for i in range(len(prev))}
    if not ans:
        if comment: 
            print('Word is not derived from previous by removal of one letter.')
    return ans

        
def is_wordiff_insertion(word: str, prev: str, comment=True) -> bool:
    "Is word derived from prev by adding one letter?"
    diff = Counter(word) - Counter(prev)
    diffcount = sum(diff.values())
    if diffcount != 1:
        if comment: 
            print('More than one character insertion difference.')
        return False
    
    insert = list(diff.keys())[0] 
    ans =  word in {prev[:i] + insert + prev[i:] for i in range(len(prev) + 1)}
    if not ans:
        if comment: 
            print('Word is not derived from previous by insertion of one letter.')
    return ans

        
def is_wordiff_change(word: str, prev: str, comment=True) -> bool:
    "Is word derived from prev by changing exactly one letter?"
    ...
    diffcount = sum(w != p for w, p in zip(word, prev))
    if diffcount != 1:
        if comment: 
            print('More or less than exactly one character changed.')
        return False
    return True

def could_have_got(wordiffs: List[str], dic: Set[str]):
    return (word for word in (dic - set(wordiffs)) 
            if is_wordiff(wordiffs, word, dic, comment=False))

if __name__ == '__main__':
    dic = load_web_dictionary(dict_url2)
    dic_3_4 = [word for word in dic if len(word) in {3, 4}]
    start = random.choice(dic_3_4)
    wordiffs = [start]
    players = get_players()
    for name in cycle(players):
        word = input(f"{name}: Input a wordiff from {wordiffs[-1]!r}: ").strip()
        if is_wordiff(wordiffs, word, dic):
            wordiffs.append(word)
        else:
            print(f'YOU HAVE LOST {name}!')
            print("Could have used:", 
                  ', '.join(islice(could_have_got(wordiffs, dic), 10)), '...')
            break
Output:
Space separated list of contestants: Paddy Maggie

Paddy: Input a wordiff from 'sett': sets

Maggie: Input a wordiff from 'sets': bets

Paddy: Input a wordiff from 'bets': buts

Maggie: Input a wordiff from 'buts': bits

Paddy: Input a wordiff from 'bits': bit

Maggie: Input a wordiff from 'bit': bite

Paddy: Input a wordiff from 'bite': biter

Maggie: Input a wordiff from 'biter': bitter

Paddy: Input a wordiff from 'bitter': sitter

Maggie: Input a wordiff from 'sitter': titter

Paddy: Input a wordiff from 'titter': tutter
That word is not in my dictionary
YOU HAVE LOST Paddy!
Could have used: titfer, witter, tittery, totter, titler, kitter, twitter, tilter, gitter, jitter ...

Raku

my @words = 'unixdict.txt'.IO.slurp.lc.words.grep(*.chars > 2);

my @small = @words.grep(*.chars < 6);

use Text::Levenshtein;

my ($rounds, $word, $guess, @used, @possibles) = 0;

loop {
    my $lev;
    $word = @small.pick;
    hyper for @words  -> $this {
        next if ($word.chars - $this.chars).abs > 1;
        last if ($lev = distance($word, $this)[0]) == 1;
    }
    last if $lev;
}

my $name = ',';

#[[### Entirely unnecessary and unuseful "chatty repartee" but is required by the task

run 'clear';
$name = prompt "Hello player one, what is your name? ";
say "Cool. I'm going to call you Gomer.";
$name = ' Gomer,';
sleep 1;
say "\nPlayer two, what is your name?\nOh wait, this isn't a \"specified number of players\" game...";
sleep 1;
say "Nevermind.\n";

################################################################################]]

loop {
    say "Word in play: $word";
    push @used, $word;
    @possibles = @words.hyper.map: -> $this {
        next if ($word.chars - $this.chars).abs > 1;
        $this if distance($word, $this)[0] == 1 and $this@used;
    }
    $guess = prompt "your word? ";
    last unless $guess@possibles;
    ++$rounds;
    say qww<Ok! Woot! 'Way to go!' Nice! 👍 😀>.pick ~ "\n";
    $word = $guess;
}

my $already = ($guess@used) ?? " $guess was already played but" !! '';

if @possibles {
    say "\nOops. Sorry{$name}{$already} one of [{@possibles}] would have continued the game."
} else {
    say "\nGood job{$name}{$already} there were no possible words to play."
}
say "You made it through $rounds rounds.";
Sample output:
Hello player one, what is your name? Burtram Redneck
Cool. I'm going to call you Gomer.

Player two, what is your name?
Oh wait, this isn't a "specified number of players" game...
Nevermind.

Word in play: howe
your word? how
Woot!

Word in play: how
your word? show
👍

Word in play: show
your word? shot
Nice!

Word in play: shot
your word? hot
😀

Word in play: hot
your word? hit
Way to go!

Word in play: hit
your word? mit
Nice!

Word in play: mit
your word? kit
😀

Word in play: kit
your word? nit
Woot!

Word in play: nit
your word? nip
😀

Word in play: nip
your word? snip
Ok!

Word in play: snip
your word? slip
Ok!

Word in play: slip
your word? slap
Way to go!

Word in play: slap
your word? lap
Woot!

Word in play: lap
your word? nap
Woot!

Word in play: nap
your word? nan
Nice!

Word in play: nan
your word? man
Nice!

Word in play: man
your word? men
Woot!

Word in play: men
your word? ben
Nice!

Word in play: ben
your word? ban
👍

Word in play: ban
your word? man

Oops. Sorry Gomer, man was already played but one of [bad bag bah bam band bane bang bank bar barn bat bay bean bin bon bran bun can dan fan han ian jan pan ran san tan van wan zan] would have continued the game.
You made it through 19 rounds.

REXX

/*REXX program acts as a host and allows two or more people to play the   WORDIFF  game.*/
signal on halt                                   /*allow the user(s) to halt the game.  */
parse arg iFID seed .                            /*obtain optional arguments from the CL*/
if iFID=='' | iFID=="," then iFID='unixdict.txt' /*Not specified?  Then use the default.*/
if datatype(seed, 'W')  then call random ,,seed  /*If      "         "   "   "    seed. */
call read
call IDs
first= random(1, min(100000, starters) )         /*get a random start word for the game.*/
list= $$$.first
say;                say eye  "OK, let's play the  WORDIFF  game.";   say;   say
         do round=1
                    do player=1  for players
                    call show;   ou= o;   upper ou
                    call CBLF  word(names, player)
                    end   /*players*/
         end              /*round*/

halt: say;  say;  say eye 'The  WORDIFF  game has been halted.'
done: exit 0                                     /*stick a fork in it,  we're all done. */
quit: say;  say;  say eye 'The  WORDDIF  game is quitting.';   signal done
/*──────────────────────────────────────────────────────────────────────────────────────*/
isMix: return datatype(arg(1), 'M')              /*return unity if arg has mixed letters*/
ser:   say;   say eye '***error*** ' arg(1).;   say;    return  /*issue error message.  */
last:  parse arg y;      return word(y, words(y) )              /*get last word in list.*/
over:  call ser 'word ' _ x _ arg(1);  say eye 'game over,' you; signal done /*game over*/
show:  o= last(list);   say;  call what;  say;   L= length(o);    return
verE:  m= 0;  do v=1  for L; m= m + (substr(ou,v,1)==substr(xu,v,1)); end;   return m==L-1
verL:  do v=1  for L;  if space(overlay(' ', ou, v), 0)==xu  then return 1; end;  return 0
verG:  do v=1  for w;  if space(overlay(' ', xu, v), 0)==ou  then return 1; end;  return 0
what:  say eye 'The current word in play is: '   _   o   _;       return
/*──────────────────────────────────────────────────────────────────────────────────────*/
CBLF:  parse arg you                             /*ask carbon-based life form for a word*/
          do getword=0  by 0  until x\==''
          say eye "What's your word to be played, " you'?'
          parse pull x;  x= space(x);   #= words(x);  if #==0  then iterate;  w= length(x)
          if #>1  then do;  call ser 'too many words given: '   x
                            x=;  iterate getword
                       end
          if \isMix(x)  then do;  call ser 'the name'   _  x  _  " isn't alphabetic"
                                  x=;   iterate getword
                             end
          end   /*getword*/

       if wordpos(x, list)>0  then call over " has already been used"
       xu= x;  upper xu                          /*obtain an uppercase version of word. */
       if \@.xu  then call over  " doesn't exist in the dictionary: " iFID
       if length(x) <3            then call over  " must be at least three letters long."
       if w <L  then  if \verL()  then call over  " isn't a legal letter deletion."
       if w==L  then  if \verE()  then call over  " isn't a legal letter substitution."
       if w >L  then  if \verG()  then call over  " isn't a legal letter addition."
       list= list  x                             /*add word to the list of words used.  */
       return
/*──────────────────────────────────────────────────────────────────────────────────────*/
IDs:   ?= "Enter the names of the people that'll be playing the WORDIFF game   (or Quit):"
       names=                                    /*start with a clean slate (of names). */
          do getIDs=0  by 0  until words(names)>1
          say;  say eye ?
          parse pull ids;  ids= space( translate(ids, , ',') )      /*elide any commas. */
          if ids==''  then iterate;  q= ids;  upper q               /*use uppercase QUIT*/
          if abbrev('QUIT', q, 1)  then signal quit
            do j=1  for words(ids);    x= word(ids, j)
            if \isMix(x)  then do;  call ser 'the name'    _ x _  " isn't alphabetic"
                                    names=;   iterate getIDs
                               end
            if wordpos(x, names)>0  then do; call ser 'the name' _ x _ " is already taken"
                                             names=;   iterate getIDs
                                         end
            names= space(names x)
            end   /*j*/
          end     /*getIDs*/
       say
       players= words(names)
          do until ans\==''
          say eye 'The '    players     " player's names are: "    names
          say eye 'Is this correct?';   pull ans;  ans= space(ans)
          end   /*until*/
       yeahs= 'yah yeah yes ja oui si da';   upper yeahs
          do ya=1  for words(yeahs)
          if abbrev( word(yeahs, ya), ans, 2) | ans=='Y'  then return
          end   /*ya*/
       call IDS;                                               return
/*──────────────────────────────────────────────────────────────────────────────────────*/
read: _= '───';       eye= copies('─', 8)        /*define a couple of eye catchers.     */
      say;   say eye eye eye  'Welcome to the  WORDIFF  word game.'  eye eye eye;    say
      @.= 0;           starters= 0
            do r=1  while lines(iFID)\==0        /*read each word in the file  (word=X).*/
            x= strip(linein(iFID))               /*pick off a word from the input line. */
            if \isMix(x)  then iterate           /*Not a suitable word for WORDIFF? Skip*/
            y= x;    upper x                     /*pick off a word from the input line. */
            @.x= 1;  L= length(x)                /*set a semaphore for uppercased word. */
            if L<3 | L>4  then iterate           /*only use short words for the start.  */
            starters= starters + 1               /*bump the count of starter words.     */
            $$$.starters= y                      /*save short words for the starter word*/
            end   /*#*/
      if r>100  &  starters> 10  then return     /*is the dictionary satisfactory ?     */
      call ser 'Dictionary file ' _ iFID _ "wasn't found or isn't satisfactory.";  exit 13
output   when using the default inputs:
──────── ──────── ──────── Welcome to the  WORDIFF  word game. ──────── ──────── ────────


──────── Enter the names of the people that'll be playing the WORDIFF game   (or Quit):
Teddy Jasmine Cy                                            ◄■■■■■■■ user input

──────── The  3  player's names are:  Teddy Jasmine Cy
──────── Is this correct?
yes                                                         ◄■■■■■■■ user input

──────── OK, let's play the  WORDIFF  game.



──────── The current word in play is:  ─── tame ───

──────── What's your word to be played,  Teddy?
fame                                                        ◄■■■■■■■ user input

──────── The current word in play is:  ─── fame ───

──────── What's your word to be played,  Jasmine?
lame                                                        ◄■■■■■■■ user input

──────── The current word in play is:  ─── lame ───

──────── What's your word to be played,  Cy?
lam                                                         ◄■■■■■■■ user input


──────── The current word in play is:  ─── lam ───


──────── What's your word to be played,  Teddy?
lamb                                                        ◄■■■■■■■ user input


──────── The current word in play is:  ─── lamb ───

──────── What's your word to be played,  Jasmine?
limb                                                        ◄■■■■■■■ user input


──────── The current word in play is:  ─── limb ───

──────── What's your word to be played,  Cy?
climb                                                       ◄■■■■■■■ user input


──────── The current word in play is:  ─── climb ───

──────── What's your word to be played,  Teddy?
climbs                                                      ◄■■■■■■■ user input


──────── ***error***  word  ─── climbs ───  doesn't exist in the dictionary:  unixdict.txt.

──────── game over, Teddy

Rust

Translation of: C++
use rand::seq::SliceRandom;
use std::collections::HashSet;
use std::fs;
use std::io::Write;

fn request_player_names(wanted: i32) -> Vec<String> {
    let mut player_names = vec![];
    for i in 0..wanted {
        let mut buf = String::new();
        print!("\nPlease enter the player's name for player {}: ", i + 1);
        let _ = std::io::stdout().flush();
        let _ = std::io::stdin().read_line(&mut buf);
        player_names.push(buf.trim_end().to_owned());
    }
    return player_names;
}

fn is_letter_removed(previous_word: &str, current_word: &str) -> bool {
    for i in 0..previous_word.len() {
        if current_word == previous_word[..i].to_owned() + &previous_word[i + 1..] {
            return true;
        }
    }
    return false;
}

fn is_letter_added(previous_word: &str, current_word: &str) -> bool {
    return is_letter_removed(current_word, previous_word);
}

fn is_letter_changed(previous_word: &str, current_word: &str) -> bool {
    if previous_word.len() != current_word.len() {
        return false;
    }

    let mut difference_count = 0;
    for i in 0..current_word.len() {
        difference_count += (current_word[i..=i] != previous_word[i..=i]) as usize;
    }
    return difference_count == 1;
}

fn is_wordiff(current_word: &str, words_used: &Vec<String>, dictionary: &HashSet<&str>) -> bool {
    if !dictionary.contains(current_word) || words_used.contains(&current_word.to_string()) {
        return false;
    }
    let previous_word = words_used.last().unwrap();
    return is_letter_changed(previous_word, current_word)
        || is_letter_removed(previous_word, current_word)
        || is_letter_added(previous_word, current_word);
}

fn could_have_entered(words_used: &Vec<String>, dictionary: &HashSet<&str>) -> Vec<String> {
    let mut result: Vec<String> = vec![];
    for word in dictionary {
        if is_wordiff(word, words_used, dictionary) {
            result.push(word.to_string());
        }
    }
    return result;
}

fn main() {
    let mut rng = rand::thread_rng();
    let wordsfile = fs::read_to_string("unixdict.txt").unwrap().to_lowercase();
    let words = wordsfile.split_whitespace().collect::<Vec<&str>>();
    let dictionary: &HashSet<&str> = &words.iter().cloned().collect();
    let mut starters = words
        .clone()
        .into_iter()
        .filter(|w| w.len() == 3 || w.len() == 4)
        .collect::<Vec<&str>>();
    starters.shuffle(&mut rng);
    let mut words_used = vec![starters[0].to_string(); 1];

    let player_names = request_player_names(2);
    let mut playing = true;
    let mut player_index = 0_usize;
    let mut current_word = words_used.last().unwrap().to_string();
    println!("\nThe first word is: {}", current_word);
    let _ = std::io::stdout().flush();

    while playing {
        print!("{}, enter your word: ", player_names[player_index]);
        let _ = std::io::stdout().flush();
        current_word.clear();
        let _ = std::io::stdin().read_line(&mut current_word);
        current_word = current_word.trim_end().to_owned();
        println!("You entered {}.", current_word);
        if is_wordiff(&current_word, &words_used, dictionary) {
            words_used.push(current_word.clone());
            player_index = (player_index + 1) % player_names.len();
        } else {
            println!("You have lost the game, {}.", player_names[player_index]);
            let missed_words = could_have_entered(&words_used, dictionary);
            println!("You could have entered: {:?}", missed_words);
            playing = false;
        }
    }
}
Output:
Please enter the player's name for player 1: Dick

Please enter the player's name for player 2: Jane

The first word is: win
Dick, enter your word: wine
You entered wine.
Jane, enter your word: pine
You entered pine.
Dick, enter your word: pint
You entered pint.
Jane, enter your word: lint
You entered lint.
Dick, enter your word: line
You entered line.
Jane, enter your word: tine
You entered tine.
Dick, enter your word: tint
You entered tint.
Jane, enter your word: stint
You entered stint.
Dick, enter your word: stunt
You entered stunt.
Jane, enter your word: stump
You entered stump.
You have lost the game, Jane.
You could have entered: ["stung", "shunt", "stunk", "stun"]

V (Vlang)

import os
import rand
import time
import arrays

fn is_wordiff(guesses []string, word string, dict []string) bool {
    if word !in dict {
        println("That word is not in the dictionary")
        return false
    }
    if word in guesses {
        println("That word has already been used")
        return false
    }
    if word.len < guesses[guesses.len-1].len {
        return is_wordiff_removal(word, guesses[guesses.len-1])
    } else if word.len > guesses[guesses.len-1].len {
        return is_wordiff_insertion(word, guesses[guesses.len-1])
    }
    return is_wordiff_change(word,guesses[guesses.len-1])
}
fn is_wordiff_removal(new_word string, last_word string) bool {
    for i in 0..last_word.len {
        if new_word == last_word[..i] + last_word[i+1..] {
            return true
        }
    }
    println("Word is not derived from previous by removal of one letter")
    return false
}
fn is_wordiff_insertion(new_word string, last_word string) bool {
    if new_word.len > last_word.len+1 {
        println("More than one character insertion difference")
        return false
    }
    mut a := new_word.split("")
    b := last_word.split("")
    for c in b {
        idx := a.index(c)
        if idx >=0 {
            a.delete(idx)
        }
    }
    if a.len >1 {
        println("Word is not derived from previous by insertion of one letter")
        return false
    }
    return true
}
fn is_wordiff_change(new_word string, last_word string) bool  {
    mut diff:=0
    for i,c in new_word {
        if c != last_word[i] {
            diff++
        }
    }
    if diff != 1 {
        println("More or less than exactly one character changed")
        return false
    }
    return true
}

fn main() {
    words := os.read_lines("unixdict.txt")?
    time_limit := os.input("Time limit (sec) or 0 for none: ").int()
    players := os.input("Please enter player names, separated by commas: ").split(",")

    dic_3_4 := words.filter(it.len in [3,4])
    mut wordiffs := rand.choose<string>(dic_3_4,1)?
    mut timing := [][]f64{len: players.len}
    start := time.now()
    mut turn_count := 0
    for {
        turn_start := time.now()
        word := os.input("${players[turn_count%players.len]}: Input a wordiff from ${wordiffs[wordiffs.len-1]}: ")
        if time_limit != 0.0 && time.since(start).seconds()>time_limit{
            println("TIMES UP ${players[turn_count%players.len]}")
            break
        } else {
            if is_wordiff(wordiffs, word, words) {
                wordiffs<<word
            }else{
                timing[turn_count%players.len] << time.since(turn_start).seconds()
                println("YOU HAVE LOST ${players[turn_count%players.len]}")
                break
            }
        }
        timing[turn_count%players.len] << time.since(turn_start).seconds()
        turn_count++
    }
    println("Timing ranks:")
    for i,p in timing {
        sum := arrays.sum<f64>(p) or {0}
        println("  ${players[i]}: ${sum/p.len:10.3 f} seconds average")
    }
}
Output:
Time limit (sec) or 0 for none: 40
Please enter player names, separated by commas: Steven,Amy,Jo
Steven: Input a wordiff from weld: weed
Amy: Input a wordiff from weed: seed
Jo: Input a wordiff from seed: steed
Steven: Input a wordiff from steed: stead
Amy: Input a wordiff from stead: seedy 
More or less than exactly one character changed
YOU HAVE LOST Amy
Timing ranks:
  Steven:      6.320 seconds average
  Amy:      8.956 seconds average
  Jo:      2.784 seconds average

Wren

Library: Wren-ioutil
Library: Wren-str
Library: Wren-sort

Due to a bug in the System.clock method (basically it gets suspended whilst waiting for user input), it is not currently possible to add timings.

import "random" for Random
import "./ioutil" for File, Input
import "./str" for Str
import "./sort" for Find

var rand = Random.new()
var words = File.read("unixdict.txt").trim().split("\n")

var player1 = Input.text("Player 1, please enter your name : ", 1)
var player2 = Input.text("Player 2, please enter your name : ", 1)
if (player2 == player1) player2 = player2 + "2"

var words3or4 = words.where { |w| w.count == 3 || w.count == 4 }.toList
var n = words3or4.count
var firstWord = words3or4[rand.int(n)]
var prevLen = firstWord.count
var prevWord = firstWord
var used = []
var player = player1
System.print("\nThe first word is %(firstWord)\n")
while (true) {
    var word = Str.lower(Input.text("%(player), enter your word : ", 1))
    var len = word.count
    var ok = false
    if (len < 3) {
        System.print("Words must be at least 3 letters long.")
    } else if (Find.first(words, word) == -1) {
        System.print("Not in dictionary.")
    } else if (used.contains(word)) {
        System.print("Word has been used before.")
    } else if (word == prevWord) {
        System.print("You must change the previous word.")
    } else if (len == prevLen) {
        var changes = 0
        for (i in 0...len) {
            if (word[i] != prevWord[i]) {
                changes = changes + 1
            }
        }
        if (changes > 1) {
            System.print("Only one letter can be changed.")
        } else ok = true
    } else if (len == prevLen + 1) {
        var addition = false
        var temp = word
        for (i in 0...prevLen) {
            if (word[i] != prevWord[i]) {
                addition = true
                temp = Str.delete(temp, i)
                if (temp == prevWord) {
                    ok = true
                }
                break
            }
        }
        if (!addition) ok = true
        if (!ok) System.print("Invalid addition.")
    } else if (len == prevLen - 1) {
        var deletion = false
        var temp = prevWord
        for (i in 0...len) {
            if (word[i] != prevWord[i]) {
                deletion = true
                temp = Str.delete(temp, i)
                if (temp == word) {
                    ok = true
                }
                break
            }
        }
        if (!deletion) ok = true
        if (!ok) System.print("Invalid deletion.")
    } else {
        System.print("Invalid change.")
    }
    if (ok) {
        prevLen = word.count
        prevWord = word
        used.add(word)
        player = (player == player1) ? player2 : player1
    } else {
        System.print("So, sorry %(player), you've lost!")
        return
    }
}
Output:

Sample game:

Player 1, please enter your name : Paddy
Player 2, please enter your name : Maggie

The first word is pan

Paddy, enter your word : pen
Maggie, enter your word : pin
Paddy, enter your word : pint
Maggie, enter your word : pant
Paddy, enter your word : pane
Maggie, enter your word : pang
Paddy, enter your word : bang
Maggie, enter your word : rang
Paddy, enter your word : sang
Maggie, enter your word : sing
Paddy, enter your word : ling
Not in dictionary.
So, sorry Paddy, you've lost!