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:

 a deletion of one letter; 
 addition of one letter; 
 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
 Array length
 String length
 Copy a string
 Empty string  (assignment)
Counting
 Word frequency
 Letter frequency
 Jewels and stones
 I before E except after C
 Bioinformatics/base count
 Count occurrences of a substring
 Count how many vowels and consonants occur in a string
Remove/replace
 XXXX redacted
 Conjugate a Latin verb
 Remove vowels from a string
 String interpolation (included)
 Strip block comments
 Strip comments from a string
 Strip a set of characters from a string
 Strip whitespace from a string -- top and tail
 Strip control codes and extended characters from a string
Anagrams/Derangements/shuffling
 Word wheel
 ABC problem
 Sattolo cycle
 Knuth shuffle
 Ordered words
 Superpermutation minimisation
 Textonyms (using a phone text pad)
 Anagrams
 Anagrams/Deranged anagrams
 Permutations/Derangements
Find/Search/Determine
 ABC words
 Odd words
 Word ladder
 Semordnilap
 Word search
 Wordiff  (game)
 String matching
 Tea cup rim text
 Alternade words
 Changeable words
 State name puzzle
 String comparison
 Unique characters
 Unique characters in each string
 Extract file extension
 Levenshtein distance
 Palindrome detection
 Common list elements
 Longest common suffix
 Longest common prefix
 Compare a list of strings 
 Longest common substring
 Find common directory path
 Words from neighbour ones
 Change e letters to i in words
 Non-continuous subsequences
 Longest common subsequence
 Longest palindromic substrings
 Longest increasing subsequence
 Words containing "the" substring
 Sum of the digits of n is substring of n
 Determine if a string is numeric
 Determine if a string is collapsible
 Determine if a string is squeezable
 Determine if a string has all unique characters
 Determine if a string has all the same characters
 Longest substrings without repeating characters
 Find words which contains all the vowels
 Find words which contains most consonants
 Find words which contains more than 3 vowels
 Find words which first and last three letters are equals
 Find words which odd letters are consonants and even letters are vowels or vice_versa
Formatting
 Substring
 Rep-string
 Word wrap
 String case
 Align columns
 Literals/String
 Repeat a string
 Brace expansion
 Brace expansion using ranges
 Reverse a string
 Phrase reversals
 Comma quibbling
 Special characters
 String concatenation
 Substring/Top and tail
 Commatizing numbers
 Reverse words in a string
 Suffixation of decimal numbers
 Long literals, with continuations 
 Numerical and alphabetical suffixes
 Abbreviations, easy
 Abbreviations, simple
 Abbreviations, automatic
Song lyrics/poems/Mad Libs/phrases
 Mad Libs
 Magic 8-ball
 99 Bottles of Beer
 The Name Game (a song)
 The Old lady swallowed a fly
 The Twelve Days of Christmas
Tokenize
 Text between
 Tokenize a string
 Word break problem
 Tokenize a string with escaping
 Split a character string based on change of character
Sequences
 Show ASCII table
 De Bruijn sequences
 Self-referential sequences
 Generate lower case ASCII alphabet




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