James Gray
1/7/2007 4:54:00 PM
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 Gray II