On 4/11/08, Matthew Moss <matthew.moss / gmail.com> wrote: > The three rules of Ruby Quiz 2: > Your task for this week's quiz is to write a Ruby program that > generates word search puzzles, given a list of words and the desired > width and height for the puzzle. Here's my solution - it's mostly brute force, but it does attempt to place the first letter of a new word on an existing letter in the grid. If it gets stuck it will remove some placed words and try again. It usually fits the list of programming words into a 20x20 grid, but sometimes it takes a long time to get there. -Adam --- Directions = [[1,0],[1,1],[0,1],[-1,1],[-1,0],[-1,-1],[0,-1],[1,-1]] Mark = ['|', '\\', '-', '/', '|', '\\', '-', '/'] #$ShowProgress = true class CrossWord attr_accessor :word, :place, :dir def initialize w @word=w @dir=0 @place=nil end def unplaced? @place==nil end end class CharGrid def initialize cols,rows @w=cols @h=rows @g="."*(@w*@h) @ref_count = Array.new(@w*@h){0} @iterations = 0 end #fills the grid with words def fill words iterations = 0 @words = words.map{|w| CrossWord.new(w)} words_todo = @words.select{|w|w.unplaced?} until words_todo.empty? words_todo.each{|cw| place cw } words_todo = @words.select{|w|w.unplaced?}.sort_by{rand} if (iterations+=1) %(@w+@h)==0 #if we are getting stuck, try removing some words puts "#{togo = words_todo.size} to go..." words_done = (@words-words_todo).sort_by{rand} (togo*2).times{|i| words_todo<< remove(words_done[i]) if words_done[i]} end end end #returns the obfuscated grid def to_s puz = @g.dup puz.size.times{|i|puz[i]=('a'..'z').to_a[rand(26)] if puz[i]==?.} sln=[] newwidth=(2*@w-1) @h.times{|r| sln<<puz[r*@w,@w].split('')*' ' sln<<' '*newwidth } sln end #returns the packed grid def pack s='' @h.times{|r| s<<@g[r*@w,@w]<<"\n" } s end #returns the exploded grid def solution sln=[] newwidth=(2*@w-1) @h.times{|r| sln<<@g[r*@w,@w].split('')*' ' sln<<' '*newwidth } @words.each_with_index{|cw,i| next unless cw.place pt = [cw.place/@w*2, cw.place%@w*2] (cw.word.size-1).times { pt[0]+=Directions[cw.dir][0] pt[1]+=Directions[cw.dir][1] sln[pt[0]][pt[1]] = case sln[pt[0]][pt[1]] when 32 then Mark[cw.dir] when ?-,?| then ?+ else ?X end pt[0]+=Directions[cw.dir][0] pt[1]+=Directions[cw.dir][1] } } sln end private #tries to put word in grid #starts search at overlap with previously placed words def place cw @words.sort_by{rand}.each{|otherword| startpt = find_overlap(cw, otherword) return if test_place(cw, startpt) } end #tests if cw overlaps with placed testword. #returns point of overlap if so. def find_overlap cw, testword return nil if testword.unplaced? if (offset = testword.word.index cw.word[0]) startpt = testword.place offset.times{startpt=nextp(startpt,testword.dir)} else startpt = nil end startpt end # takes suggested starting point, or picks a random one, and # tries to fit cw in grid - tests all 8 directions. def test_place cw, suggestion=nil dir=randDir start= suggestion || randStart(cw.word[0]) 8.times do pt = start good = true cw.word.each_byte{|chr| good = (@g[pt]==?. || @g[pt]==chr) && (pt=nextp(pt,dir)) break unless good } return add(cw, start, dir) if good dir=(dir+1)%8 end nil end #find next grid index in given direction def nextp pos, dir #don't allow to step off the edge if (pos<@w && (3..5).include?(dir)) or (pos+@w >= @g.size && [0,1,7].include?(dir)) or (pos%@w == 0 && (5..7).include?(dir)) or (pos%@w+1 == @w && (1..4).include?(dir)) return nil end pos+Directions[dir][0]*@w+Directions[dir][1] end #adds word at index in direction def add cw, idx, dir puts "+#{cw.word}" if $ShowProgress cw.dir = dir pt = cw.place = idx cw.word.each_byte{|chr| @g[pt]=chr @ref_count[pt]+=1 pt=nextp(pt,dir) } return [idx,dir] end #removes word from grid def remove cw p "-#{cw.word}" if $ShowProgress pt = cw.place cw.word.each_byte{|chr| @g[pt]=?. if 0== (@ref_count[pt]-=1) pt=nextp(pt,cw.dir) } cw.place=nil cw end #finds random start for word begining with matchchar def randStart matchChar start = rand(@w*@h) start= (start+1)%(@w*@h) until (@g[start]==?. || @g[start]==matchChar) start end def randDir rand(8) end end if __FILE__ == $0 gridsize = ARGV[1].split('x').map{|v|v.to_i} g = CharGrid.new *gridsize words=File.open(ARGV[0],"r").read.split.sort_by{|s|s.length}.reverse puts "unsolvable!" or exit if (words[0].size>gridsize.max) g.fill words puts g.pack File.open("search.txt","w"){|f|f.puts g.to_s} File.open("solution.txt","w"){|f|f.puts g.solution} end