Wordiff
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.
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 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
<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: ─── 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
Wren
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!