Word Blender (#108)


#1

The three rules of Ruby Q.:

  1. Please do not post any solutions or spoiler discussion for this quiz
    until
    48 hours have passed from the time on this message.

  2. Support Ruby Q. by submitting ideas as often as you can:

http://www.rubyquiz.com/

  1. Enjoy!

Suggestion: A [QUIZ] in the subject of emails about the problem helps
everyone
on Ruby T. follow the discussion. Please reply to the original quiz
message,
if you can.

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

by Ben B.

This is a riff on the Jumble puzzle found in many (US) newspapers. More
specifically, it’s based on the game TextTwist1, made by GameHouse2
and
published in various places around the web.

The mechanic of TextTwist is simple. The player is given six letters and
is
tasked with unscrambling those letters into as many words as possible.
If the
player can use all six letters in a word, they proceed to the next
round.

Your task is to build the back-end engine to run a TextTwist clone.
Effectively,
this means that you must generate a list of three- to six-letter words
that can
all be constructed from the same six letters. This list must contain at
least
one six-letter word.

Bonus points for building a completely functional game!


#2

It just so happened that I was learning ruby last summer and wrote a
program
to cheat for me at TextTwist. Needless to say the game got boring really
fast, but it was neat writing the program. I’ve modified it a bit to
instead
play the game, but I’m a fairly new user and would appreciate any
feedback,
both ruby related and general programming.

To run this you’ll need a dictionary file of words, each on a new line,
called wordlist.txt and you need to require ‘dict’ and run
Dict.reduce(3,6)
before you run the program. This will create a reduced dictionary file
of
only words of lengths between 3 and 6. I didn’t make this part of my
program’s general execution as I figured doing this once was enough and
I
could do it manually.

It’s got a simple text interface that looks sort of like the TextTwist
gui
layout on Yahoo. Sort of. The dictionary I use, which I did not attach
because its size (3 megs, I believe it was the full Scrabble dictionary)
makes for a much harder game than the Yahoo TextTwist as some of the
words
are really obscure.

Also, I have the problem that when running this on Windows it doesn’t
allow
me to manipulate files with Ruby, giving me a Permission Denied error on
File.open. Any idea why this might be? This is if I try to run
Dict.reduce(3,6)
for example.


#3

This is my first ruby program that actually does all it’s supposed to
do. It
doesn’t have a lot of style, but hopefully my next programs will look
better.
The wordlist is from wordlist.sourceforge.net and the first part of the
program reads the word.lst file and puts all the words in an array.

Hope it does what it’s supposed to do, as i couldn’t see the texttwist
on
yahoo.
I’m also attaching the Unnoficial Jargon File Wordlist word.lst file.

-Ionuţ Arţărişi


#4

On 1/7/07, Fedor L. removed_email_address@domain.invalid wrote:

only words of lengths between 3 and 6. I didn’t make this part of my
allow me to manipulate files with Ruby, giving me a Permission Denied error
on File.open. Any idea why this might be? This is if I try to run
Dict.reduce(3,6) for example.

E:\ruby\programs\rubyquiz\quiz108\other submissions>ruby
texttwistGame.rb
./dict.rb:20:in initialize': No such file or directory - reducedwordlist3.txt ( Errno::ENOENT) from ./dict.rb:20:inopen’
from ./dict.rb:20:in `get_dict’
from texttwistGame.rb:37

fixed that, but then:

E:\ruby\programs\rubyquiz\quiz108\other submissions>ruby
texttwistGame.rb
texttwistGame.rb:17:in pick_word': undefined methoddelete_if’ for
nil:NilClas
s (NoMethodError)
from texttwistGame.rb:38

I’m too tired to go bug hunting. Check out my submission crop.rb to see
how
I did the windows file writing. Now that I’ve submitted my program I’m
going back to sleep. :slight_smile:


#5

Here it is. Scoring works like this: 10 points per (tile used)^2. 100
bonus points for using all 6 tiles. 5 wrong answers in a row ends the
game.
I made a dictionary file of 3-6 letter words using crop.rb. Any
feedback is
appreciated, both from a stylistic and general programming perspective.
If
there’s a way to rubify (not a word!) anything, also let me know.


#6

Here’s the solution I wrote while considering this quiz (it requires
Unix):

#!/usr/bin/env ruby -w

require “io/wait”

game date cache

CACHE_FILE = “.game_words”

if File.exist? CACHE_FILE # load from cache
word_list = File.open(CACHE_FILE) { |file| Marshal.load(file) }
else # build word list

prepare data structure

words_by_signature = Hash.new { |words, sig| words[sig] = Array.new }

read dictionary

File.foreach(ARGV.shift || “/usr/share/dict/words”) do |word|
word.downcase!
word.delete!("^a-z")

 next unless word.length.between? 3, 6

 (words_by_signature[word.split("").sort.join] << word).uniq!

end

prepare recursive signature search

def choices(sig, seen = Hash.new { |all, cur| all[cur] = true;
false }, &blk)
sig.length.times do |i|
shorter = sig[0…i] + sig[(i+1)…sig.length]
unless seen[shorter]
blk[shorter]
choices(shorter, seen, &blk) unless shorter.length == 3
end
end
end

prepare game data structure

word_list = Hash.new

build game choices

words_by_signature.keys.grep(/\A.{6}\Z/) do |possible|
word_list[possible] = words_by_signature[possible]

 choices(possible) do |shorter_signature|
   if words_by_signature.include? shorter_signature
     word_list[possible].push(*words_by_signature

[shorter_signature])
end
end
end

cache for faster loads

File.open(CACHE_FILE, “w”) { |file| Marshal.dump(word_list, file) }
end

game interface (requires Unix)

TERMINAL_STATE = stty -g
system “stty raw -echo cbreak”
at_exit { system “stty #{TERMINAL_STATE}” }
clear = clear

a raw mode savvy puts

def out(args) print((args + ["\r\n"])) end

for easy selection

words = word_list.keys

rounds = 0
loop do

select letters

letters = current = words[rand(words.size)]
while word_list.include? letters
letters = letters.split("").sort_by { rand }.join
end
letters.gsub!(/.(?=.)/, '\0 ')

round data

advance = false
matches = Array.new
current_match = String.new
start = Time.now
message = nil
last_update = start - 1

round event loop

until Time.now >= start + 2 * 60
# game display
if last_update <= Time.now - 1
print clear

   out "Your letters:  #{letters}"
   out "   Time left:  #{120 - (Time.now - start).round} seconds"
   out "  Your words:  #{matches.join(', ')}"
   out
   unless message.nil?
     out message
     out
   end
   print current_match
   $stdout.flush

   last_update = Time.now
 end

 # input handler
 if $stdin.ready?
   char = $stdin.getc
   case char
  when ?a..?z, ?A..?Z  # read input
    current_match << char.chr.downcase
    message       =  nil
     last_update   =  start - 1
   when ?\b, 127        # backspace/delete
     current_match = current_match[0..-2]
    message       =  nil
     last_update   =  start - 1
   when ?\r, ?\n        # test entered word
     if word_list[current].include? current_match
       matches << current_match
       matches = matches.sort_by { |word| [word.size, word] }
       if not advance and current_match.length == 6
         advance = true
         message = "You will advance to the next round!"
       else
         message = nil
       end
     else
       message = "Unknown word."
     end
     current_match = String.new
     last_update   = start - 1
   end
 end

end

round results

print clear
missed = word_list[current] - matches
unless missed.empty?
out “Other words using “#{letters}:””
out missed.sort_by { |word| [word.size, word] }.join(", ")
out
end
if advance
rounds += 1

 out "You made #{matches.size} word#{'s' if matches.size != 1}, ",
     "including at least one six letter word.  Nice work."
 out "Press any key to begin the next round."

 $stdin.getc

else
out "You made #{matches.size} word#{‘s’ if matches.size != 1}, ",
“but failed to find a six letter word.”

 break  # end game

end
end

game results

out “You completed #{rounds} round#{‘s’ if rounds != 1}. Thanks for
playing.”

END

James Edward G. II


#7

E:\ruby\programs\rubyquiz\quiz108\other submissions>ruby texttwistGame.rb
texttwistGame.rb:17:in pick_word': undefined methoddelete_if’ for
nil:NilClas
s (NoMethodError)
from texttwistGame.rb:38

I’m too tired to go bug hunting. Check out my submission crop.rb to see
how
I did the windows file writing. Now that I’ve submitted my program I’m
going back to sleep. :slight_smile:

Thanks Jason. It seems that the problem is that as long as a file by
that
name exists, Windows doesn’t let me overwrite it. I’m using the
FILE::CREAT
flag but I don’t think that should be causing this. It seems to be
saying I
don’t have the permission to modify the file with ruby, though I haven’t
done much research into why that is so and how to fix it.


#8

Here is my solution.

-Chunyun

#== Synopsis
#This is the solution to Ruby Q. #108 described on
http://www.rubyquiz.com/quiz108.html.

#== Usage

text_twist.rb dictionary_file

#== Author

Chunyun Z.(removed_email_address@domain.invalid)

class Dictionary
MIN_LEN, MAX_LEN = 3, 6
attr_reader :max_letters
def initialize(dict_file, min=MIN_LEN, max=MAX_LEN)
@min_len = min
@max_len = max
@words = Hash.new {|hash,key|hash[key]=[]}
File.foreach(dict_file) {|word|add_word(word.strip)}
@max_letters = @words.keys.select {|key| key.size==@max_len}
end
def word_list(letters)
words=[]
permutate(letters).select {|letters|
letters.size.between? @min_len, @max_len
}.uniq.each {|key|
words += @words[key]
}
words.sort_by {|word| word.size}
end
private
def add_word(word)
if (@min_len@max_len)===word.size && word=~/^[a-z]+$/i
word.downcase!
@words[word.split(//).sort] << word
end
end
def permutate(letters)
_letters = letters.dup
result = []
while letter = _letters.shift
permutate(_letters).each do |perm|
result << [letter] + perm
end
result << [letter]
end
result
end
end

Task = Struct.new(:letters, :words)

class GameUi
def initialize(dict)
@dictionary = dict
@history_tasks = []
@rounds = 1
@score = 0
end

def next_task
letters =
@dictionary.max_letters[rand(@dictionary.max_letters.size)]
retry if @history_tasks.include?(letters)
task = Task.new(letters, @dictionary.word_list(letters))
@history_tasks << task
task
end

def run_task
@task = next_task
@found = []
@cleared = false
puts “\nRound #{@rounds}. Letters: #{@task.letters*’, '}. Hint:
number
of matching words: #{@task.words.size}”
while !(word=ask(“Enter your word:”)).empty?
if @found.include? word
puts “Word already found!”
elsif @task.words.include? word
@found << word
@score += word.size
puts “Good job! You scored #{word.size} points!”
if word.size == @task.letters.size
@cleared = true
puts "\nBingo! Round #@rounds cleared. You found
#{@found.size}
word#{‘s’ if @found.size > 1}. "
break
end
else
puts “Wrong word!”
end
end
puts “Missed words: #{(@task.words-@found)*’, '}.”
@cleared
end

def run
while run_task
answer = ask("\nProceed to next round?")
break if answer !~ /^y/i
@rounds += 1
end
puts “\nYou’ve cleared #{cleared=@cleared?@rounds:@rounds-1}
round#{‘s’
if cleared > 1}, and your total score is #{@score}.”
end

def ask question
print question, " (Hit enter to exit)=> "
gets.strip
end
end
if FILE == $0
if ARGV.size != 1
puts “Usage: #{File.basename(FILE)} dictionary_file”
exit
end
GameUi.new(Dictionary.new(ARGV.shift)).run
end
END


#9

There is a bug in my previous submission, here it is again:

#== Synopsis
#This is the solution to Ruby Q. #108 described on
http://www.rubyquiz.com/quiz108.html.

#== Usage

text_twist.rb dictionary_file

#== Author

Chunyun Z.(removed_email_address@domain.invalid)

class Dictionary
MIN_LEN, MAX_LEN = 3, 6
attr_reader :max_letters
def initialize(dict_file, min=MIN_LEN, max=MAX_LEN)
@min_len = min
@max_len = max
@words = Hash.new {|hash,key|hash[key]=[]}
File.foreach(dict_file) {|word|add_word(word.strip)}
@max_letters = @words.keys.select {|key| key.size==@max_len}
end
def word_list(letters)
words=[]
permutate(letters).select {|letters|
letters.size.between? @min_len, @max_len
}.uniq.each {|key|
words += @words[key]
}
words.sort_by {|word| word.size}
end
private
def add_word(word)
if (@min_len@max_len)===word.size && word=~/^[a-z]+$/i
word.downcase!
@words[word.split(//).sort] << word
end
end
def permutate(letters)
_letters = letters.dup
result = []
while letter = _letters.shift
permutate(_letters).each do |perm|
result << [letter] + perm
end
result << [letter]
end
result
end
end

Task = Struct.new(:letters, :words)

class GameUi
def initialize(dict)
@dictionary = dict
@history_tasks = []
@rounds = 1
@score = 0
end

def run
while run_task
answer = ask("\nProceed to next round?")
break if answer !~ /^y/i
@rounds += 1
end
puts “\nYou’ve cleared #{cleared=@cleared?@rounds:@rounds-1}
round#{‘s’
if cleared > 1}, and your total score is #{@score}.”
end

private
def next_task
letters =
@dictionary.max_letters[rand(@dictionary.max_letters.size)]
retry if @history_tasks.include?(letters)
task = Task.new(letters, @dictionary.word_list(letters))
@history_tasks << task
task
end

def run_task
@task = next_task
@found = []
@cleared = false
puts “\nRound #{@rounds}. Letters: #{@task.letters*’, '}. Hint:
number
of matching words: #{@task.words.size}”
while !(word=ask(“Enter your word:”)).empty?
if @found.include? word
puts “Word already found!”
elsif @task.words.include? word
@found << word
@score += word.size
puts “Good job! You scored #{word.size} points!”
if word.size == @task.letters.size
@cleared = true
puts "\nBingo! Round #@rounds cleared. You found
#{@found.size}
word#{‘s’ if @found.size > 1}. "
break
end
else
puts “Wrong word!”
end
end
puts “Missed words: #{(@task.words-@found)*’, '}.”
@cleared
end

def ask question
print question, " (Hit enter to exit)=> "
gets.strip.downcase
end
end
if FILE == $0
if ARGV.size != 1
puts “Usage: #{File.basename(FILE)} dictionary_file”
exit
end
GameUi.new(Dictionary.new(ARGV.shift)).run
end


#10

Here is my solution. Takes the filename of the dictionary to use as the
first argument and optionally a word to solve for as the second
argument. If no second argument is provided, you play the game.

class String
def lettercount
split(//).uniq.map{|c| [c, count©]}
end
def fisher_yates_shuffle
a = self.dup
(length-1).downto(0){|i|
j = rand(i+1)
a[i], a[j] = a[j], a[i] if i != j
}
a
end
end

class Array
def random_element
self[rand(length)]
end
end

class Dictionary
def initialize(filename)
@words = []
IO.foreach(filename) do |line|
word = line.chomp
@words << word.downcase if word.length.between?(3, 6)
end
end
def blend(word)
@words.select{|x|
x.count(word.downcase) == x.length &&
x.lettercount.all?{|c, n|
n <= word.downcase.lettercount.assoc©.last }
}
end
def randomword
@words.select{|x| x.length == 6}.random_element
end
end

class WordBlender
def initialize(dictionary)
@dictionary = dictionary
end
def blend_to_s(word)
word_blend = @dictionary.blend(word)
puts “WordBlender: ‘#{word}’ has #{word_blend.length} answers.”
puts
max = -1
word_blend.sort_by{|x| [x.length, x]}.each do |x|
if x.length > max
max = x.length
puts “Words of length #{max}:”
end
puts " #{x}"
end
end
def play
puts “Welcome to WordBlender! (enter a blank line to quit)”
puts
round = 0
points = 0
continue = true
while continue do
points = points + 10 * round
round = round + 1
word = @dictionary.randomword
word_blend = @dictionary.blend(word)
word_shuffled = word.fisher_yates_shuffle
puts “Round: #{round} - Blend: ‘#{word_shuffled}’ - Total Score:
#{points}”
current_word = “”
current_words = []
current_continue = true
while continue && current_continue do
current_word = STDIN.gets.chomp.downcase
if current_word == “”
puts
puts “Final Word: ‘#{word}’ - Final Score: #{points}”
continue = false
elsif current_words.include?(current_word)
puts “’#{current_word}’ already used.”
elsif word_blend.include?(current_word)
current_words << current_word
points = points + current_word.length * current_word.length
current_continue = (current_word.length < word.length)
elsif current_word.count(word) == current_word.length
puts “’#{current_word}’ not in dictionary.”
else
puts “’#{current_word}’ not found in ‘#{word_shuffled}’.”
end
end
end
end
end

if ARGV.size == 0
puts “Usage: wordblender.rb - play WordBlender with the
specified dictionary”
puts “Usage: wordblender.rb - show all blends for
the word using the dictionary”
elsif ARGV.size == 1
WordBlender.new(Dictionary.new(ARGV[0])).play
elsif ARGV.size >= 2
WordBlender.new(Dictionary.new(ARGV[0])).blend_to_s(ARGV[1])
end


#11

On 1/7/07, Daniel F. removed_email_address@domain.invalid wrote:

Find words that use the same letters

selectedWords = dict.scan(/^[#{baseWord}]{3,6}$/)

I was really impressed when I first saw this. It doesn’t quite work if
you
want to exclude reusing the same letter more than once
(“hhh”.scan(/^[hello]{3,6}$/) => [“hhh”]) but it comes so close to
something
I’ve only ever thought about implementing as a recursive method.
Unfortunately I don’t know much about this but now I wonder if it’s
possible
to find all partial permutations of a word with a regexp.


#12

I made two this time. One plays a game and one just picks the word.

Here’s the one that just picks a word. It runs very quickly, about .155
or .2 seconds.
#! /usr/bin/ruby -w

class Array
def rand_elem
self[rand(size)]
end
end

Open and read the dictionary.

dict = IO.read("/usr/share/dict/words")

Pick a random word with 6 letters.

baseWord = dict.scan(/^[a-z]{6}$/).rand_elem

Find words that use the same letters

selectedWords = dict.scan(/^[#{baseWord}]{3,6}$/)

Display the words.

puts baseWord + “:\n\t” + selectedWords.join("\n\t")

And here’s the one that plays a game. Basically, it gives you the
letters and you enter the 5 best words you can think of. The score is
the length of the word ** 3 added to a running total, but subtracted if
the word isn’t valid (already said or not in the list).

#! /usr/bin/ruby -w

class Array
def rand_elem
self[rand(size)]
end

def english_join
self[0…-1].join(’, ') + ', and ’ + self[-1]
end
end

class String
def letters
unless $DEBUG
split(//).uniq.sort_by{rand}
else
split(//)
end
end
end

class Game
@@dict = nil

def initialize
# Open and read the dictionary.
@@dict ||= IO.read("/usr/share/dict/words")

@points = 0
@round = 1

end

def play
# Pick a random word with 6 letters.
baseWord = @@dict.scan(/^[a-z]{6}$/).rand_elem

# Find words that use the same letters
selectedWords = @@dict.scan(/^[#{baseWord}]{3,6}$/)

# Initialize word list & continue var.
guessed = []
continue = false

# Display banner
puts "",
  "Round #{@round}:",
  "Enter the 5 longest words you can make from the letters

#{baseWord.letters.english_join}.",
“Invalid and repeated words count towards the 5 words but subtract
points.”,
“”

# Gather all the points, calculate the score, and see if the player

should go to the next round.
5.times do
print “#{@points}\t”
word = gets.chomp.downcase
if !guessed.include?(word) && selectedWords.include?(word)
@points += word.length ** 3
guessed << word
continue = true if word.length == 6
else
@points -= word.length ** 3
end
end

# Go on to the next round or lose.
if continue
  @round += 1
  play
else
  puts "Sorry, you didn't get a 6 letter word.  You got #{@points}

points, however."
end
end
end

Game.new.play


#13

Here is a version of the regex that works around that:
regex =
/^([a-z])(?!\1)([a-z])(?!\1|\2)([a-z])(?!\1|\2|\3)([a-z])(?!\1|\2|\3|\4)([a-z])(?!\1|\2|\3|\4|\5)([a-z])$/

Some examples:

regex.match ‘abcdef’
=> #MatchData:0xb7b46a18

regex.match ‘abcdefg’
=> nil

regex.match ‘abcddf’
=> nil

regex.match ‘abbdtf’
=> nil

regex.match ‘Abbdtf’
=> nil

regex.match ‘awesom’
=> #MatchData:0xb7b3a5b0

regex.match ‘hollow’
=> nil


#14

This solution simply chooses a six-letter word (or allows the user to
choose one) and then displays a list of words that can be composed with
a subset of the letters (size >= 3). Here is output from a sample run:

ape
lap
pal
pea
sap
sea
see
else
leap
pale
peas
peel
sale
seal
slap
sleep
asleep
please

Eric

Interested in Ruby training with a well-reviewed instructor and
training materials? www.LearnRuby.com

Given an array of letters, a whole/partial word built up so far, and

a hash, adds to the hash all permutations of subsets built from the

partial word and the array of letters. If a block is given it acts

as a filter since the words must produce a true result when submitted

to the block in order to be added to the hash.

def permute(letters, word, possible_words, &filter_block)
possible_words[word] = true if filter_block.nil? ||
filter_block.call(word)
return if letters.empty?

letters.each_with_index do |letter, i|
(new_letters = letters.dup).delete_at(i)
permute(new_letters, word + letter, possible_words, &filter_block)
end
end

Verify that a filename was provided as the first argument and that

it is a readable file

if ARGV[0].nil?
$stderr.puts(“Usage: #{$0} dictionary-file [word]”)
exit 1
elsif ! File.file?(ARGV[0]) || ! File.readable?(ARGV[0])
$stderr.puts(“Error: “#{ARGV[0]}” is not a readable file.”)
exit 2
end

Build list of all six-letter words from dictionary file

words6 = Array.new
open(ARGV[0], “r”) do |f|
f.each_line { |w| words6 << w if w.chomp! =~ /^[a-z]{6}$/ }
end

Determine whether a random six-letter word is chosen or the user

specifies one.

if ARGV[1]

user attempted to specify a word; check its validity

if words6.include?(ARGV[1])
word = ARGV[1]
else
$stderr.puts(“Error: “#{ARGV[1]}” is not a known six-letter
word.”)
exit 3
end
else
word = words6[rand(words6.size)] # choose a random word
end

Generate a hash of all three- to six-letter permutations using the

letters of the chosen six-letter word. Note: most will not be valid

words.

possible_words = Hash.new
permute(word.split(""), “”, possible_words) { |w| w.length >= 3 }

Generate a list of all valid words that are also permutations of

subsets of the chosen six-letter word. This is done by

re-reading the word file and testing each word against the

possible permutations.

actual_words = Array.new
open(ARGV[0], “r”) do |f|
f.each_line { |w| actual_words << w if possible_words[w.chomp!] }
end

Display the resulting actual words sorted first by length and then

alphabetically.

puts actual_words.sort_by { |w| [w.length, w] }


#15

Oops, that doesn’t match {3,6} just {6}.

regex =
/^([a-z])(?!\1)([a-z])(?!\1|\2)([a-z])(?:(?!\1|\2|\3)([a-z]))?(?:(?!\1|\2|\3|\4)([a-z]))?(?:(?!\1|\2|\3|\4|\5)([a-z]))?$/
=> [a-z]1[a-z]12[a-z]:123[a-z]:1234[a-z]:12345[a-z]

regex.match ‘hhh’
=> nil

regex.match ‘hal’
=> #MatchData:0xb7cfb7d0

regex.match ‘sos’
=> nil

regex.match ‘sauce’
=> #MatchData:0xb7c12bac

regex.match ‘hatoff’
=> nil

That’s a better one.


#16

Here is my solution. I didn’t post the whole thing. This is the main
engine minus the Tk UI class. You can get the full solution
(dictionary, graphics and ruby source at
http://www.dalemartenson.com/files/rubytexttwist.tar.gz

–Dale M.

class TextTwist

Algorithm derived from online book by Robert Sedgewick and Kevin

Wayne.

It was origomally written in Java.

References:

http://www.cs.princeton.edu/introcs/31datatype/

http://www.cs.princeton.edu/introcs/31datatype/TextTwist.java.html

class Profile < Hash
def initialize( word )
super
word.downcase.each_byte do |b|
self[b.chr] = 0 unless self.has_key?(b.chr)
self[b.chr] += 1
end
end

def contains( p )
  p.each_pair do |k,v|
    return false unless self.has_key?(k)
    return false if self[k] < v
  end
  true
end

end

attr_reader :word, :words

def initialize( dictionary, word=nil )
@dictionary = dictionary
@dictionary.collect! {|x| x.downcase}
@dictionary.sort!
word.nil? ? restart : start( word )
end

def start( word )
@word = word.downcase
@profile = Profile.new( @word )
@words = process
end

def process
result = []

@dictionary.each do |dw|
  next if dw.length < 3 || dw.length > @word.length
  result << dw if @profile.contains( Profile.new( dw ) )
end

result

end

def mix
a = @word.split(//)
1.upto(a.length) do
i1 = rand(a.length)
i2 = rand(a.length)
t = a[i1]
a[i1] = a[i2]
a[i2] = t
end
a
end

def check( word )
@words.include?( word.downcase )
end

def contains( word )
@profile.contains( Profile.new( word ) )
end

def restart
six_letter_words = []
@dictionary.each do |w|
six_letter_words << w if w.length == 6
end

start( six_letter_words[ rand(six_letter_words.size) ] )

end
end

class TextTwistUI

… SNIP …

end

MAIN PROGRAM – where the magic begins

Using the crossword dictionary from “Moby Word Lists” by Grady Ward

(part of

Project Gutenberg). I reduced the dictionary to only words that are 3

to 6

characters in length.

Note: This may not be the best dictionary for this game, but it

works. It

does contain numerous obscure words which is both good and bad.

irb(main):001:0> f = File.open(“crosswd.txt”)

=> #<File:crosswd.txt>

irb(main):002:0> of = File.open(“3-6.txt”,“w+”)

=> #<File:3-6.txt>

irb(main):003:0> f.each_line do |line|

irb(main):004:1* l = line.strip

irb(main):005:1> if l.length >= 3 && l.length <= 6 then

irb(main):006:2* of.puts( l )

irb(main):007:2> end

irb(main):008:1> end

=> #<File:crosswd.txt>

irb(main):009:0> of.close

=> nil

References:

http://www.gutenberg.org/etext/3201

dictionary = []
f = File.open(“3-6.txt”)
dictionary = f.read.split

tt = TextTwist.new( dictionary )

TextTwistUI.new( tt )
Tk.mainloop


#17

My game will play almost like the one you can play at yahoo without
score. Has
cheating and giving-up commands and draws possible solutions on the
screen.
Run it with wordblender.rb .

martin

wordblender.rb

Usage: wordblender.rb [dictfile]

class String

Checks if string can be build out of these characters.

“hello”.build_outof?(“llohe”) => true

“world”.build_outof?(“dlrowl”) => true

def build_outof?(other)
return false if self.length > other.length
o = other.clone
self.each_byte do |c|
return false unless o.include?(c.chr)
o[o.index(c.chr)] = 0
end
true
end

Shuffle a word.

“hello”.shuffle => “oellh”

def shuffle
return self.scan(/./).sort_by{rand}.to_s
end
end

class WordBlenderGame

attr_reader :words

limits for words for the game

MINCHARACTERS = 3
MAXCHARACTERS = 6

time limit per game

TIME_PER_GAME = 90

how to display the board

DISPLAY_COLUMNS = 5

read the dictionary from a file.

we also keep words with length of MAXCHARACTERS to find

good initial letters quickly.

def initialize(dictionary)
@words, @maxwords = [], []
File.open(dictionary).each do |line|
l = line.strip.downcase
@words << l if (l.length >= MINCHARACTERS && l.length <=
MAXCHARACTERS)
@maxwords << l if l.length == MAXCHARACTERS
end
end

this generates a bunch of letters to play with and looks up words

that can be build by them from the dictionary (“candidates”).

def prepare_game()
@letters = @maxwords[rand(@maxwords.size-1)].shuffle
@candidates = []
@words.each { |w| @candidates << w if w.build_outof?(@letters) }
@candidates = @candidates.uniq # this fixed duplicated entries
@candidates = @candidates.sort {|x,y| x.length <=> y.length }
@found_candidates = @candidates.collect { false }
end

This is to display the candidates to the screen. Draws it into

columns

and returns a string.

def get_board(solution=false, title=“Words to find”)
result = “” ; i = 0
sempty = ’ '(DISPLAY_COLUMNS(MAXCHARACTERS+2))
s = String.new(sempty)
result << title << “:\n”

 @found_candidates.each_index do |idx|
     f = @found_candidates[idx] || solution
     s[i.modulo(DISPLAY_COLUMNS)*(MAXCHARACTERS+2)] =

f ? “[#{@candidates[idx]}]” : “[”+(’ ‘*@candidates[idx].length)+"]"
if i.modulo(DISPLAY_COLUMNS) == DISPLAY_COLUMNS-1 then
result << (s + “\n”)
s = String.new(sempty)
end
i+=1
end
result << s if s.include?(’[’)
result << “\n”
end

This plays one round of the game, returns true if won

def play
self.prepare_game
message = “Press RETURN to shuffle the letters, ‘!’ to give up, ‘?’
to
cheat.”

# start the time.
@time = TIME_PER_GAME
timer = Thread.new { while true do @time-=1; sleep 1 end }

# game loop
while @found_candidates.include?(false) do

   # print board and other stuff
   puts get_board
   puts
   puts  "Time: " + @time.to_s
   puts  "Msg:  " + message if message != ''
   puts  "Use:  " + @letters
   print "Try:  "

   # get user's guess and handle it
   $stdout.flush
   s = STDIN.gets.downcase.strip

   if @time <= 0 then
     puts "Time's up!"
     break
   end

   if s == "" then
     @letters = @letters.shuffle
     message = "Letters shuffled!"
     next
   end

   break if s == '!'

   if s == '?' then
     puts get_board(true)
     message = "Cheater!"
     next
   end

   if !s.build_outof?(@letters) then
     message = "Invalid word!"
     next
   end

   if @candidates.include?(s) then
     @found_candidates[@candidates.index(s)] = true
     message = "#{s} Found!"
   else
     message =  "#{s} not listed!"
   end
end

Thread.kill(timer)

# print solution
puts get_board(true, "Solution is")

# Check if player found a word with all characters
@found_candidates.each_index do |idx|
  return true if @found_candidates[idx] && @candidates[idx].length 

==
MAXCHARACTERS
end
false
end
end

print “Loading game…”;$stdout.flush
game = WordBlenderGame.new(ARGV[1] || ‘/usr/share/dict/words’)
puts “#{game.words.size} words found.”

while game.play do
puts “You won, press any key to play next round.”
gets
end

puts “Game over!”


#18

On Sunday 07 January 2007 15:04, Ionut Artarisi wrote:

This is my first ruby program that actually does all it’s supposed to do.
It doesn’t have a lot of style, but hopefully my next programs will look
better. The wordlist is from wordlist.sourceforge.net and the first part of
the program reads the word.lst file and puts all the words in an array.

Hope it does what it’s supposed to do, as i couldn’t see the texttwist on
yahoo.
I’m also attaching the Unnoficial Jargon File Wordlist word.lst file.

-Ionuţ Arţărişi

I remembered a mistake that i had done. Well not actually a mistake. I
just
wrote some 10 lines of code that did some extra work that i never
actually
used later in the program. I deleted those lines now, which resulted in
a
unidimensional array (it was bidimensional before). This version should
be
more readable and should make more sense though there’s no extra
functionallity. So the version on rubyquiz.com should be replaced with
this
one, for the sake of future readers if it’s not too much of a trouble.

Thank you. And thank you for rubyquiz.com. It’s fantastic!


#19

Here is my solution. I’m not too good with regular expressions, so the
word selection script is a little bit slow.

#!/usr/bin/env ruby

Author: Dan M. - http://www.dcmanges.com

Ruby Q. #108 - http://rubyquiz.com/quiz108.html

class WordList
include Enumerable

attr_accessor :file
attr_reader :filters

def initialize(file = nil)
@file, @filters = file, []
end

def each
File.open(@file, “r”) do |file|
while line = file.gets
yield apply_filters(line.chomp)
end
end
end

protected

def apply_filters(word)
@filters.inject(word) do |word, filter|
filter.call(word)
end
end

end

Module to select words based on length and letter composition.

module WordFinder

Finds words of length +size+ which can be composed with the letters

in +base_word+
def find_words(size, base_word)
letter_counts = base_word.split(//).inject(Hash.new(0)) {
|hash,letter| hash[letter] += 1; hash }
regexp = Regexp.new("^" + letter_counts.map { |letter,count|
“#{letter}{0,#{count}}”}.sort.join + “$”)
select { |word| word.to_s.size == size && word.split(//).sort.join
=~ regexp }
end

Finds a random word of the given size

def random_word_of_size(size)
words = find_words(size, ((‘a’…‘z’).to_a * 3).join)
words[rand(words.size)]
end
end

WordList.send(:include, WordFinder)

Dictionary file from: http://wordlist.sourceforge.net/

http://prdownloads.sourceforge.net/wordlist/alt12dicts-4.tar.gz

@wordlist = WordList.new("/Users/dan/Desktop/alt12dicts/2of12full.txt")

This particular wordlist has an offset

@wordlist.filters << lambda { |word| word[17…-1] }

Skip proper names, contractions, etc.

@wordlist.filters << lambda { |word| word =~ /^[a-z]+$/ ? word : “” }

module WordBlender
class Round
def initialize(wordlist, word_size = (3…6))
@wordlist = wordlist
@min_size, @max_size = word_size.first, word_size.last
@qualified = false
@hits = Hash.new { |h,k| h[k] = [] }
load_words
end

def qualified?
  @qualified
end

def guess?(word)
  word = word.to_s.strip
  dup?(word) || qualify?(word) || hit?(word) || :miss
end

def letters
  @base.split(//).sort
end

def status
  result = []
  @min_size.upto(@max_size) do |size|
    result << [size, @hits[size].size, @words[size].size]
  end
  result.map { |data| "#{data[0]} letters: #{data[1]} of 

#{data[2]}"}.join(", ")
end

protected

def dup?(word)
  :dup if @hits[word.size].include?(word)
end

def qualify?(word)
  if @words[word.size].include?(word) and word.size == @max_size
    @hits[word.size] << word
    @qualified = true
    :qualify
  end
end

def hit?(word)
  if @words[word.size].include?(word)
    @hits[word.size] << word
    :hit
  end
end

def load_base_word
  @base = @wordlist.random_word_of_size(@max_size)
end

def load_words
  @words = Hash.new([])
  load_base_word
  @min_size.upto(@max_size) do |size|
    @words[size] = @wordlist.find_words(size, @base)
  end
end

end

class Game
def initialize(wordlist)
@wordlist = wordlist
reset
end

def start!
  help
  start_round
  print 'guess> '
  while input = gets
    input = input.strip
    break if input == ".quit"
    if input[0,1] == "." && respond_to?(input[1..-1])
      send(input[1..-1])
      print 'guess> '
      next
    end
    result = @round.guess?(input)
    puts case result
      when :miss
        "Wrong!"
      when :dup
        "Already guessed that!"
      when :hit
        "You got it!"
      when :qualify
        "You got it! And you qualify for the next round!"
    end + " " + input
    status unless result == :miss
    print 'guess> '
  end
  puts "Goodbye!"
end
alias :play! :start!

protected

def letters
  puts "Available Letters: " + @round.letters.sort_by {rand}.join(', 

')
end

def next
  if @round.qualified?
    start_round
  else
    puts "You have not yet qualified for the next round!"
  end
end

def help
  puts <<-END_HELP
  When prompted, either enter a word or a command.
  The following commands are available:
    .quit => quits the game
    .help => display this help
    .next => goes to the next round (if qualified)
    .letters => display available letters
    .status  => show the current status of this round
  END_HELP
end

def reset
  @round_number = 0
end

def start_round
  @round_number += 1
  @round = Round.new(@wordlist)
  puts "Beginning Round #{@round_number}!"
  letters
end

def status
  puts @round.status
end

end
end

@blender = WordBlender::Game.new(@wordlist)
@blender.play!


#20

On 1/7/07, Fedor L. removed_email_address@domain.invalid wrote:

Unfortunately I don’t know much about this but now I wonder if it’s possible
to find all partial permutations of a word with a regexp.

Here’s a revision to Daniel’s approach that seems to work well:

Open and read the dictionary.

dict = IO.read("/usr/share/dict/words").scan(/^[a-z]{3,6}$/)

Pick a random word with 6 letters.

baseWord = dict.grep(/^[a-z]{6}$/).rand_elem

Find words that use the same letters

sortWord = baseWord.scan(/./).sort.to_s
selectedWords = dict.select {|w|
Regexp.new(w.scan(/./).sort.join(’.*’)).match(sortWord) }

I started by just extracting only the 3-6 letter words.

Then I sort the base word so the letters are in order. Let’s say the
baseWord is “parlor”. Then sortWord would be “aloprr”.

Now for each word in the dictionary, sort it in letter order and
create a regex. Suppose the word is “roar”. The sorted version is
“aorr” and the regex is “a.*o.*r.*r”. If that regex matches the
sortWord, we found a valid subword.

This could be sped up by precompiling the regexes I would guess.

Bob