Wrote this program out of curiosity after initially solving the Word Chains puzzle. Explanation of puzzle in link below.
http://rubyquiz.com/quiz44.html
NWordChains#find_word_chains will take in a number as an arg and return that amount of random word chains from a given dictionary. Initialize NWordChains.new and then run find_word_chains with the number of pairs that you would like returned.
My question is are there any quick glaring spots where this code could be DRYed up? Looking for any quick feedback, trying to learn how to think DRYily.
NOTE: You will need a dictionary.txt file of your own to run the code. I found one here if you'd like to use it: https://github.com/dwyl/english-words/blob/master/words.txt
require_relative "word_chains"
require 'set'
class NWordChains < WordChains
def initialize
words = File.readlines("dictionary.txt").map(&:chomp)
@dictionary = Array.new(words)
@all_paths = Hash.new { |h, k| h[k] = [] }
@invalid_comps = Set.new
end
def find_word_chains(num)
count = 0
until count == num
word_pair = two_rand_eql_length_words
redo if invalid_pair?(word_pair)
word1 = word_pair[0]
word2 = word_pair[1]
path_search = WordChains.new(word1, word2)
if path_search.find_path == false
@invalid_comps << [ word1, word2 ]
else
all_paths_key = "#{word1.upcase + ' ' + "to" + ' ' + word2.upcase}"
@all_paths[all_paths_key] << path_search.path
count += 1
end
end
@all_paths
end
attr_reader :all_paths
def two_rand_eql_length_words
word1 = @dictionary.sample
narrowed_dict = self.narrow_dict_params(word1)
word2 = narrowed_dict.sample
[ word1, word2 ]
end
def invalid_pair?(word_pair)
@invalid_comps.include?(word_pair) ||
@invalid_comps.include?(word_pair.reverse)
end
end
#----------------------------------------------------
class WordChains
def initialize(beginning_word, target_word)
@origin_word = beginning_word
@current_word = beginning_word
@target_word = target_word
words = File.readlines("dictionary.txt").map(&:chomp)
@dictionary = Array.new(words)
@narrowed_dict = self.narrow_dict_params(@origin_word)
@path = [ @origin_word ]
end
attr_accessor :current_word, :target_word, :dictionary, :path, :dead_ends, :invalid_words
def find_path
return false if @origin_word == @target_word
until @path.include?(@origin_word) && @path.include?(@target_word)
valid_words = find_valid_words(@current_word)
if !valid_words.empty?
find_and_move_to_next_word(@current_word, valid_words)
else
# In my version of the puzzle solution, if a dead end has been reached
# than the program assumes no path could be found.
return false
end
end
return false if @path.length == 2
@path
end
def narrow_dict_params(origin_word)
narrowed_dict = @dictionary.select { |word| word.length == origin_word.length }
narrowed_dict
end
def find_and_move_to_next_word(current_word, valid_words)
next_word = word_moving_towards_target(valid_words)
@path << next_word
@current_word = next_word
end
def word_moving_towards_target(valid_words)
count_hash = Hash.new { |h, k| h[k] = 0 }
valid_words.each do |word|
count = 0
(0...word.length).each do |i|
count += 1 if word[i] == @target_word[i]
end
count_hash[word] += count
end
sorted_hash = count_hash.sort_by { |k, v| v }
sorted_hash[-1][0]
end
def find_valid_words(current_word)
valid_words = @narrowed_dict.select { |word| word if valid_word?(word) }
valid_words
end
def valid_word?(word)
word != @current_word &&
last_word_compatible?(word) &&
target_compatible?(word) &&
!path.include?(word)
end
def last_word_compatible?(word)
count = 0
(0...word.length).each do |i|
count += 1 if word[i] != @current_word[i]
end
return false if count > 1
true
end
def target_compatible?(word)
count = 0
(0...word.length).each do |i|
count += 1 if word[i] == @target_word[i]
end
return true if count >= 1
false
end
end