scrabbler 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README +10 -1
- data/bin/scrabblit +18 -15
- data/lib/array_extensions.rb +2 -9
- data/lib/score.rb +16 -15
- data/lib/scrabbler.rb +80 -80
- data/lib/vocabulary.rb +36 -34
- data/test/test_array_extensions.rb +1 -8
- data/test/test_helper.rb +2 -0
- metadata +1 -1
data/README
CHANGED
|
@@ -6,7 +6,16 @@ Solve scrabble puzzles by the means of dictionary lookups.
|
|
|
6
6
|
:en - English/US dictonary based as provided in Firefox
|
|
7
7
|
|
|
8
8
|
== CLI
|
|
9
|
-
A commandline tool is provided
|
|
9
|
+
A commandline tool is provided, use like this:
|
|
10
|
+
|
|
11
|
+
scrabble [options] 'letters'
|
|
12
|
+
-l en Set language [en, nl]
|
|
13
|
+
-m max Limit the number of results
|
|
14
|
+
-t thresh Set the score threshold for the results
|
|
15
|
+
-e Show the score explanation
|
|
16
|
+
-h Show help
|
|
17
|
+
|
|
18
|
+
it is possible to give multiple sets of letters.
|
|
10
19
|
|
|
11
20
|
== Author
|
|
12
21
|
Peter Maas (pfmmaas [at] gmail [dot] com)
|
data/bin/scrabblit
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
require 'optparse'
|
|
2
|
+
|
|
2
3
|
require File.dirname(__FILE__) + '/../lib/scrabbler.rb'
|
|
4
|
+
include SCRABBLR
|
|
3
5
|
|
|
4
6
|
options = {:language => :en, :max => 10, :thresh => 10, :explanation => false}
|
|
5
7
|
|
|
6
8
|
optionParser = OptionParser.new do |opts|
|
|
7
|
-
opts.banner = "Usage: scrabblit [options
|
|
9
|
+
opts.banner = "Usage: scrabblit [options] 'letters'"
|
|
8
10
|
|
|
9
11
|
opts.on("-l en", "Set language [en, nl]") do |l|
|
|
10
12
|
options[:language] = l.to_sym
|
|
@@ -23,19 +25,20 @@ optionParser = OptionParser.new do |opts|
|
|
|
23
25
|
puts opts
|
|
24
26
|
exit
|
|
25
27
|
end
|
|
26
|
-
end.parse!
|
|
27
28
|
|
|
28
|
-
if ARGV
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
r = s.scrabble(set, options[:thresh], options[:language])
|
|
32
|
-
puts '==> ' + set
|
|
33
|
-
r[0...options[:max]].each do |score|
|
|
34
|
-
puts "#{score.word} (#{score.score})"
|
|
35
|
-
puts "-- #{score.explanation.join(", ")}" if options[:explanation]
|
|
36
|
-
end
|
|
37
|
-
puts ''
|
|
29
|
+
if !ARGV || ARGV.length <= 0
|
|
30
|
+
puts opts
|
|
31
|
+
exit
|
|
38
32
|
end
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
33
|
+
end.parse!
|
|
34
|
+
|
|
35
|
+
s = Scrabbler.new(options[:language])
|
|
36
|
+
ARGV.each do |set|
|
|
37
|
+
r = s.scrabble(set, options[:thresh], options[:language])
|
|
38
|
+
puts '==> ' + set
|
|
39
|
+
r[0...options[:max]].each do |score|
|
|
40
|
+
puts "#{score.word} (#{score.score})"
|
|
41
|
+
puts "-- #{score.explanation.join(", ")}" if options[:explanation]
|
|
42
|
+
end
|
|
43
|
+
puts ''
|
|
44
|
+
end
|
data/lib/array_extensions.rb
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
# This module contains some extensions for the build-in array
|
|
6
6
|
# type. Will not change existing methods
|
|
7
|
+
require 'enumerator'
|
|
8
|
+
|
|
7
9
|
module ArrayExtensions
|
|
8
10
|
# removes the first entry in the array for which the test returns true
|
|
9
11
|
def remove_first(&test)
|
|
@@ -13,15 +15,6 @@ module ArrayExtensions
|
|
|
13
15
|
|
|
14
16
|
nil # nothing removed, nothing to return
|
|
15
17
|
end
|
|
16
|
-
|
|
17
|
-
def chunk(chunk_size = 100)
|
|
18
|
-
chunked = []
|
|
19
|
-
(0...self.length).step(chunk_size) do |i|
|
|
20
|
-
chunked[i/chunk_size] = self[i...(i+chunk_size)]
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
chunked
|
|
24
|
-
end
|
|
25
18
|
end
|
|
26
19
|
|
|
27
20
|
class Array
|
data/lib/score.rb
CHANGED
|
@@ -1,22 +1,23 @@
|
|
|
1
1
|
# Author:: Peter Maas (pfmmaas [at] gmail [dot] com)
|
|
2
2
|
# Copyright:: Copyright (c) 2008
|
|
3
3
|
# License:: Distributes under the same terms as Ruby
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
class Score
|
|
7
|
-
|
|
4
|
+
module SCRABBLR
|
|
5
|
+
# simple container for scrabble scores
|
|
6
|
+
class Score
|
|
7
|
+
attr_accessor(:word, :score, :explanation)
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
def initialize(word, score, explanation)
|
|
10
|
+
@word = word
|
|
11
|
+
@score = score
|
|
12
|
+
@explanation = explanation
|
|
13
|
+
end
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
def inspect
|
|
16
|
+
"Scrabble score [word=#{@word}, score=#{@score}]"
|
|
17
|
+
end
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
def <=> other
|
|
20
|
+
other.score <=> @score
|
|
21
|
+
end
|
|
22
|
+
end
|
|
22
23
|
end
|
data/lib/scrabbler.rb
CHANGED
|
@@ -10,104 +10,104 @@ require File.dirname(__FILE__) +'/array_extensions'
|
|
|
10
10
|
require File.dirname(__FILE__) +'/score'
|
|
11
11
|
require File.dirname(__FILE__) +'/vocabulary'
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
# Class for solving scrabble puzzles
|
|
15
|
-
class Scrabbler
|
|
16
|
-
|
|
13
|
+
module SCRABBLR
|
|
14
|
+
# Class for solving scrabble puzzles
|
|
15
|
+
class Scrabbler
|
|
16
|
+
MIN_WORD_SIZE = 3
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
SCRABBLER_DICTIONARIES = {
|
|
19
|
+
:nl => File.dirname(__FILE__) +"/data/nederlands.dict",
|
|
20
|
+
:en => File.dirname(__FILE__) +"/data/en-US.dict"
|
|
21
|
+
}
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
SCORES = YAML::load(File.new(File.dirname(__FILE__) + "/data/scores.yml").read)
|
|
24
|
+
MAX_THREADS = 10;
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
# TODO: rates in constant
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
28
|
+
# initializes the Scrabbler with:
|
|
29
|
+
# - the array if the given object is of type array
|
|
30
|
+
# - the dictonary file specified by the given symbos (:nl, :en)
|
|
31
|
+
def initialize(_dictionary)
|
|
32
|
+
case _dictionary
|
|
33
|
+
when Symbol
|
|
34
|
+
@dictionary = Vocabulary.new(File.read(SCRABBLER_DICTIONARIES[_dictionary]))
|
|
35
|
+
@dictionary.info = _dictionary
|
|
36
|
+
when Array
|
|
37
|
+
@dictionary = Vocabulary.new(_dictionary)
|
|
38
|
+
@dictionary.info = "based on supplied array"
|
|
39
|
+
end
|
|
39
40
|
end
|
|
40
|
-
end
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
# find possible solutions of the given array of letters
|
|
43
|
+
def scrabble(letters, min_score = 10, rating = :nl, wildcard = '$')
|
|
44
|
+
raise "Wildcard can not be a word character" if wildcard =~ /[\w]/
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
letters = letters.scan(/[a-z#{wildcard}]/)
|
|
47
|
+
num_wildcards = letters.select{|c| c == wildcard}.length # count the number of wildcards
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
# create a subset based on the word letters
|
|
50
|
+
dictonary_subset = @dictionary.calculate_subset(letters, wildcard, MIN_WORD_SIZE)
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
end
|
|
64
|
-
end
|
|
52
|
+
scores, threads = [], []
|
|
53
|
+
|
|
54
|
+
dictonary_subset.each_slice(@dictionary.words.length / (MAX_THREADS - 1) + 1) do |slice|
|
|
55
|
+
threads << Thread.new(slice) do |s|
|
|
56
|
+
slice_scores = []
|
|
57
|
+
s.each do |word|
|
|
58
|
+
score = calculate_score(letters, word, num_wildcards, rating)
|
|
59
|
+
slice_scores << score if score && score.score > min_score
|
|
60
|
+
end
|
|
61
|
+
slice_scores
|
|
62
|
+
end
|
|
63
|
+
end
|
|
65
64
|
|
|
66
|
-
|
|
65
|
+
threads.each {|t| scores = scores + t.value} # gather values from all threads
|
|
67
66
|
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
scores.sort
|
|
68
|
+
end
|
|
70
69
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
70
|
+
# calculate the score of the test_word. Doesn't return anything
|
|
71
|
+
# if it is impossible to create the word from the given letters
|
|
72
|
+
def calculate_score(letters, test_word, num_wild = 0, rating = :nl)
|
|
73
|
+
return if letters.length < test_word.length
|
|
75
74
|
|
|
76
|
-
|
|
77
|
-
|
|
75
|
+
letters = letters.dup # make a copy, we'll use the array desctructively
|
|
76
|
+
score_sum, score_explanation = 0, []
|
|
78
77
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
78
|
+
test_word.scan(/[a-z]/).each do |c|
|
|
79
|
+
if letters.include? c
|
|
80
|
+
letters.remove_first{|v| v == c} # we've used this letter, remove it
|
|
81
|
+
curr_rate = SCORES[rating][c.to_sym];
|
|
82
|
+
score_sum = score_sum + curr_rate
|
|
83
|
+
score_explanation << "#{c} (#{curr_rate})"
|
|
84
|
+
elsif num_wild > 0 # see if we've got any wildcards left
|
|
85
|
+
num_wild = num_wild - 1
|
|
86
|
+
score_explanation << "#{c} (wildcard)"
|
|
87
|
+
else
|
|
88
|
+
return
|
|
89
|
+
end
|
|
90
90
|
end
|
|
91
|
-
end
|
|
92
91
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
92
|
+
# return the result
|
|
93
|
+
Score.new(test_word, score_sum , score_explanation)
|
|
94
|
+
end
|
|
96
95
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
96
|
+
def inspect
|
|
97
|
+
"scrabbler, dictionary: #{@dictionary.info}"
|
|
98
|
+
end
|
|
100
99
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
100
|
+
def self.def_singletons(*args)
|
|
101
|
+
args.each do |arg|
|
|
102
|
+
module_eval <<-EOS
|
|
103
|
+
def self.get_#{arg}
|
|
104
|
+
@@instance_#{arg} ||= Scrabbler.new(:#{arg})
|
|
105
|
+
end
|
|
106
|
+
EOS
|
|
107
|
+
end
|
|
108
108
|
end
|
|
109
|
-
end
|
|
110
109
|
|
|
111
|
-
|
|
110
|
+
def_singletons *SCRABBLER_DICTIONARIES.keys
|
|
112
111
|
|
|
113
|
-
end
|
|
112
|
+
end
|
|
113
|
+
end
|
data/lib/vocabulary.rb
CHANGED
|
@@ -2,50 +2,52 @@
|
|
|
2
2
|
# Copyright:: Copyright (c) 2008
|
|
3
3
|
# License:: Distributes under the same terms as Ruby
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
module SCRABBLR
|
|
6
|
+
# Vocabulary is a simple dictionary with options for creating subsets
|
|
7
|
+
class Vocabulary
|
|
8
|
+
attr_accessor(:words, :info, :index)
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
# removes non-word characters
|
|
11
|
+
SANITIZE_FILTER = proc{|v| v.chomp.downcase.gsub(/\W/,'')}
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
def initialize(words)
|
|
14
|
+
@words = words.map(&SANITIZE_FILTER)
|
|
15
|
+
build_index
|
|
16
|
+
end
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
def build_index
|
|
19
|
+
@index = Hash.new { |hash, key| hash[key] = [] }
|
|
20
|
+
@words.each do |w|
|
|
21
|
+
w.scan(/[a-z]/).uniq.each do |c|
|
|
22
|
+
@index[c.to_sym] << w
|
|
23
|
+
end
|
|
22
24
|
end
|
|
23
25
|
end
|
|
24
|
-
end
|
|
25
26
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
def calculate_subset(letters, wildcard, min_word_size = 3)
|
|
28
|
+
# create a subset based on the word letters
|
|
29
|
+
lf = length_filter(min_word_size..letters.length)
|
|
29
30
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
31
|
+
if letters.include? wildcard
|
|
32
|
+
subset = @words.select(&lf)
|
|
33
|
+
else
|
|
34
|
+
subset = []
|
|
35
|
+
letters.uniq.each do |c|
|
|
36
|
+
subset = subset + index[c.to_sym]
|
|
37
|
+
end
|
|
38
|
+
subset = subset.select(&lf)
|
|
39
|
+
subset = subset.uniq.grep(create_pattern(letters, min_word_size))
|
|
36
40
|
end
|
|
37
|
-
subset = subset.select(&lf)
|
|
38
|
-
subset = subset.uniq.grep(create_pattern(letters, min_word_size))
|
|
39
|
-
end
|
|
40
41
|
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
subset
|
|
43
|
+
end
|
|
43
44
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
def length_filter(range)
|
|
46
|
+
proc{|s| range.include? s.length}
|
|
47
|
+
end
|
|
47
48
|
|
|
48
|
-
|
|
49
|
-
|
|
49
|
+
def create_pattern(letters, min_word_size = 3)
|
|
50
|
+
/^[#{letters.join}]{#{min_word_size},#{letters.length}}$/
|
|
51
|
+
end
|
|
50
52
|
end
|
|
51
53
|
end
|
|
@@ -20,12 +20,5 @@ class TestScrabbler < Test::Unit::TestCase
|
|
|
20
20
|
r.remove_first{|i| raise BlockCalled}
|
|
21
21
|
end
|
|
22
22
|
end
|
|
23
|
-
|
|
24
|
-
def test_chunk
|
|
25
|
-
r = %w(the quick brown fox jumps over the lazy dog)
|
|
26
|
-
assert_equal([%w(the quick brown),%w(fox jumps over),%w(the lazy dog)], r.chunk(3))
|
|
27
|
-
|
|
28
|
-
r = %w(the quick brown fox jumps over the lazy dog but didn't jump over the fast bunny)
|
|
29
|
-
assert_equal([%w(the quick brown),%w(fox jumps over),%w(the lazy dog), %w(but didn't jump), %w(over the fast), %w(bunny)], r.chunk(3))
|
|
30
|
-
end
|
|
23
|
+
|
|
31
24
|
end
|
data/test/test_helper.rb
CHANGED