Wordiff

Revision as of 01:57, 31 July 2021 by rosettacode>Gerard Schildberger (→‎{{header|REXX}}: added the computer programming language REXX.)

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

Wordiff is a draft programming task. It is not yet considered ready to be promoted as a complete task, for reasons that should be found in its talk page.

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 goal

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.

Nim

Translation of: Python

<lang Nim>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</lang>
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


Python

This is without timing, but ends by showing some wordiffs from the dictionary that could have worked on failure. <lang python># -*- 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</lang>
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 ...

REXX

<lang 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 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)); 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</lang>
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:  ─── sewn ───

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

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

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

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

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

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

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

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

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

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. <lang ecmascript>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 (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
   }

}</lang>

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!