scrabbler 0.1.2
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 +13 -0
- data/bin/scrabblit +41 -0
- data/lib/array_extensions.rb +29 -0
- data/lib/data/en-US.dict +62114 -0
- data/lib/data/nederlands.dict +158819 -0
- data/lib/data/scores.yml +54 -0
- data/lib/score.rb +22 -0
- data/lib/scrabbler.rb +113 -0
- data/lib/vocabulary.rb +51 -0
- data/test/test_array_extensions.rb +31 -0
- data/test/test_helper.rb +6 -0
- data/test/test_scrabbler.rb +44 -0
- data/test/test_vocabulary.rb +19 -0
- metadata +57 -0
data/lib/data/scores.yml
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
---
|
|
2
|
+
:en:
|
|
3
|
+
:l: 1
|
|
4
|
+
:a: 1
|
|
5
|
+
:x: 8
|
|
6
|
+
:v: 4
|
|
7
|
+
:g: 2
|
|
8
|
+
:n: 1
|
|
9
|
+
:y: 4
|
|
10
|
+
:h: 4
|
|
11
|
+
:u: 1
|
|
12
|
+
:i: 1
|
|
13
|
+
:t: 1
|
|
14
|
+
:w: 4
|
|
15
|
+
:p: 3
|
|
16
|
+
:b: 3
|
|
17
|
+
:c: 3
|
|
18
|
+
:s: 1
|
|
19
|
+
:e: 1
|
|
20
|
+
:k: 5
|
|
21
|
+
:f: 4
|
|
22
|
+
:j: 8
|
|
23
|
+
:m: 3
|
|
24
|
+
:d: 2
|
|
25
|
+
:q: 10
|
|
26
|
+
:r: 1
|
|
27
|
+
:o: 1
|
|
28
|
+
:nl:
|
|
29
|
+
:l: 3
|
|
30
|
+
:a: 1
|
|
31
|
+
:x: 8
|
|
32
|
+
:v: 4
|
|
33
|
+
:g: 3
|
|
34
|
+
:n: 1
|
|
35
|
+
:y: 8
|
|
36
|
+
:h: 4
|
|
37
|
+
:u: 4
|
|
38
|
+
:t: 2
|
|
39
|
+
:i: 1
|
|
40
|
+
:w: 5
|
|
41
|
+
:p: 3
|
|
42
|
+
:b: 3
|
|
43
|
+
:c: 5
|
|
44
|
+
:s: 2
|
|
45
|
+
:e: 1
|
|
46
|
+
:f: 4
|
|
47
|
+
:k: 3
|
|
48
|
+
:j: 4
|
|
49
|
+
:m: 3
|
|
50
|
+
:d: 2
|
|
51
|
+
:q: 10
|
|
52
|
+
:r: 1
|
|
53
|
+
:z: 4
|
|
54
|
+
:o: 1
|
data/lib/score.rb
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Author:: Peter Maas (pfmmaas [at] gmail [dot] com)
|
|
2
|
+
# Copyright:: Copyright (c) 2008
|
|
3
|
+
# License:: Distributes under the same terms as Ruby
|
|
4
|
+
#
|
|
5
|
+
|
|
6
|
+
class Score
|
|
7
|
+
attr_accessor(:word, :score, :explanation)
|
|
8
|
+
|
|
9
|
+
def initialize(word, score, explanation)
|
|
10
|
+
@word = word
|
|
11
|
+
@score = score
|
|
12
|
+
@explanation = explanation
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def inspect
|
|
16
|
+
"Scrabble score [word=#{@word}, score=#{@score}]"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def <=> other
|
|
20
|
+
other.score <=> @score
|
|
21
|
+
end
|
|
22
|
+
end
|
data/lib/scrabbler.rb
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Author:: Peter Maas (pfmmaas [at] gmail [dot] com)
|
|
2
|
+
# Copyright:: Copyright (c) 2008
|
|
3
|
+
# License:: Distributes under the same terms as Ruby
|
|
4
|
+
|
|
5
|
+
require 'rubygems'
|
|
6
|
+
require 'ostruct'
|
|
7
|
+
require 'yaml'
|
|
8
|
+
|
|
9
|
+
require File.dirname(__FILE__) +'/array_extensions'
|
|
10
|
+
require File.dirname(__FILE__) +'/score'
|
|
11
|
+
require File.dirname(__FILE__) +'/vocabulary'
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# Class for solving scrabble puzzles
|
|
15
|
+
class Scrabbler
|
|
16
|
+
MIN_WORD_SIZE = 3
|
|
17
|
+
|
|
18
|
+
SCRABBLER_DICTIONARIES = {
|
|
19
|
+
:nl => File.dirname(__FILE__) +"/data/nederlands.dict",
|
|
20
|
+
:en => File.dirname(__FILE__) +"/data/en-US.dict"
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
SCORES = YAML::load(File.new(File.dirname(__FILE__) + "/data/scores.yml").read)
|
|
24
|
+
MAX_THREADS = 10;
|
|
25
|
+
|
|
26
|
+
# TODO: rates in constant
|
|
27
|
+
|
|
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
|
|
40
|
+
end
|
|
41
|
+
|
|
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
|
+
|
|
46
|
+
letters = letters.scan(/[a-z#{wildcard}]/)
|
|
47
|
+
num_wildcards = letters.select{|c| c == wildcard}.length # count the number of wildcards
|
|
48
|
+
|
|
49
|
+
# create a subset based on the word letters
|
|
50
|
+
dictonary_subset = @dictionary.calculate_subset(letters, wildcard, MIN_WORD_SIZE)
|
|
51
|
+
|
|
52
|
+
scores, threads = [], []
|
|
53
|
+
chunks = dictonary_subset.chunk(@dictionary.words.length / (MAX_THREADS - 1) + 1)
|
|
54
|
+
|
|
55
|
+
chunks.each do |chunk|
|
|
56
|
+
threads << Thread.new(chunk) do |c|
|
|
57
|
+
chunk_scores = []
|
|
58
|
+
chunk.each do |word|
|
|
59
|
+
score = calculate_score(letters, word, num_wildcards, rating)
|
|
60
|
+
chunk_scores << score if score && score.score > min_score
|
|
61
|
+
end
|
|
62
|
+
chunk_scores
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
threads.each {|t| scores = scores + t.value} # gather values from all threads
|
|
67
|
+
|
|
68
|
+
scores.sort
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# calculate the score of the test_word. Doesn't return anything
|
|
72
|
+
# if it is impossible to create the word from the given letters
|
|
73
|
+
def calculate_score(letters, test_word, num_wild = 0, rating = :nl)
|
|
74
|
+
return if letters.length < test_word.length
|
|
75
|
+
|
|
76
|
+
letters = letters.dup # make a copy, we'll use the array desctructively
|
|
77
|
+
score_sum, score_explanation = 0, []
|
|
78
|
+
|
|
79
|
+
test_word.scan(/[a-z]/).each do |c|
|
|
80
|
+
if letters.include? c
|
|
81
|
+
letters.remove_first{|v| v == c} # we've used this letter, remove it
|
|
82
|
+
curr_rate = SCORES[rating][c.to_sym];
|
|
83
|
+
score_sum = score_sum + curr_rate
|
|
84
|
+
score_explanation << "#{c} (#{curr_rate})"
|
|
85
|
+
elsif num_wild > 0 # see if we've got any wildcards left
|
|
86
|
+
num_wild = num_wild - 1
|
|
87
|
+
score_explanation << "#{c} (wildcard)"
|
|
88
|
+
else
|
|
89
|
+
return
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# return the result
|
|
94
|
+
Score.new(test_word, score_sum , score_explanation)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def inspect
|
|
98
|
+
"scrabbler, dictionary: #{@dictionary.info}"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def self.def_singletons(*args)
|
|
102
|
+
args.each do |arg|
|
|
103
|
+
module_eval <<-EOS
|
|
104
|
+
def self.get_#{arg}
|
|
105
|
+
@@instance_#{arg} ||= Scrabbler.new(:#{arg})
|
|
106
|
+
end
|
|
107
|
+
EOS
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def_singletons *SCRABBLER_DICTIONARIES.keys
|
|
112
|
+
|
|
113
|
+
end
|
data/lib/vocabulary.rb
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Author:: Peter Maas (pfmmaas [at] gmail [dot] com)
|
|
2
|
+
# Copyright:: Copyright (c) 2008
|
|
3
|
+
# License:: Distributes under the same terms as Ruby
|
|
4
|
+
|
|
5
|
+
# Vocabulary is a simple dictionary with options for creating subsets
|
|
6
|
+
class Vocabulary
|
|
7
|
+
attr_accessor(:words, :info, :index)
|
|
8
|
+
|
|
9
|
+
# removes non-word characters
|
|
10
|
+
SANITIZE_FILTER = proc{|v| v.chomp.downcase.gsub(/\W/,'')}
|
|
11
|
+
|
|
12
|
+
def initialize(words)
|
|
13
|
+
@words = words.map(&SANITIZE_FILTER)
|
|
14
|
+
build_index
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def build_index
|
|
18
|
+
@index = Hash.new { |hash, key| hash[key] = [] }
|
|
19
|
+
@words.each do |w|
|
|
20
|
+
w.scan(/[a-z]/).uniq.each do |c|
|
|
21
|
+
@index[c.to_sym] << w
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def calculate_subset(letters, wildcard, min_word_size = 3)
|
|
27
|
+
# create a subset based on the word letters
|
|
28
|
+
lf = length_filter(min_word_size..letters.length)
|
|
29
|
+
|
|
30
|
+
if letters.include? wildcard
|
|
31
|
+
subset = @words.select(&lf)
|
|
32
|
+
else
|
|
33
|
+
subset = []
|
|
34
|
+
letters.uniq.each do |c|
|
|
35
|
+
subset = subset + index[c.to_sym]
|
|
36
|
+
end
|
|
37
|
+
subset = subset.select(&lf)
|
|
38
|
+
subset = subset.uniq.grep(create_pattern(letters, min_word_size))
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
subset
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def length_filter(range)
|
|
45
|
+
proc{|s| range.include? s.length}
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def create_pattern(letters, min_word_size = 3)
|
|
49
|
+
/^[#{letters.join}]{#{min_word_size},#{letters.length}}$/
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
require File.dirname(__FILE__) + '/test_helper.rb'
|
|
2
|
+
|
|
3
|
+
class TestScrabbler < Test::Unit::TestCase
|
|
4
|
+
|
|
5
|
+
def test_remove_first
|
|
6
|
+
r = %w(the quick brown fox jumps over the lazy dog)
|
|
7
|
+
r.remove_first{|i| i == 'the'}
|
|
8
|
+
assert_equal(%w(quick brown fox jumps over the lazy dog), r)
|
|
9
|
+
|
|
10
|
+
r.remove_first{|i| i == 'the'}
|
|
11
|
+
assert_equal(%w(quick brown fox jumps over lazy dog), r)
|
|
12
|
+
|
|
13
|
+
r.remove_first{|i| i == 'the'}
|
|
14
|
+
assert_equal(%w(quick brown fox jumps over lazy dog), r)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def test_uses_input_block
|
|
18
|
+
assert_raise BlockCalled do
|
|
19
|
+
r = %w(the quick brown fox jumps over the lazy dog)
|
|
20
|
+
r.remove_first{|i| raise BlockCalled}
|
|
21
|
+
end
|
|
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
|
|
31
|
+
end
|
data/test/test_helper.rb
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
require File.dirname(__FILE__) + '/test_helper.rb'
|
|
2
|
+
|
|
3
|
+
class TestScrabbler < Test::Unit::TestCase
|
|
4
|
+
|
|
5
|
+
def setup
|
|
6
|
+
dictionary = %w(the quick brown fox jumps over the lazy dog but not over the quicker bunny)
|
|
7
|
+
@scrabbler = Scrabbler.new(dictionary)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def test_run_scrabble
|
|
11
|
+
results = @scrabbler.scrabble("q,u,i,c,k", 19, :en)
|
|
12
|
+
assert_equal(1, results.length)
|
|
13
|
+
assert_equal(20, results[0].score)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def test_illegal_wildcard
|
|
17
|
+
assert_raise RuntimeError do
|
|
18
|
+
results = @scrabbler.scrabble("q,u,i,c,k", 19, :en,'a') # wildcard can not be a word character
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def test_run_scrabble_with_one_wildcard
|
|
23
|
+
results = @scrabbler.scrabble("q,u,i,$,k", 15, :en)
|
|
24
|
+
assert_equal(1, results.length)
|
|
25
|
+
assert_equal(17, results[0].score) # score should be lower, wildcard has no score
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def test_run_scrabble_with_two_wildcards
|
|
29
|
+
results = @scrabbler.scrabble("q,u,i,c,k,$,$", 19, :en)
|
|
30
|
+
assert_equal(2, results.length)
|
|
31
|
+
assert_equal(results[0].score, results[1].score)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def test_results_should_be_ordered_by_score
|
|
35
|
+
results = @scrabbler.scrabble("q,u,i,c,k,e,r", 19, :en)
|
|
36
|
+
assert_equal(2, results.length)
|
|
37
|
+
assert(results[0].score > results[1].score, "scores are not ordered correctly")
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
#def test_singleton_methods
|
|
41
|
+
# assert Scrabbler.get_nl.equal?(Scrabbler.get_nl)
|
|
42
|
+
# assert Scrabbler.get_en.equal?(Scrabbler.get_en)
|
|
43
|
+
#end
|
|
44
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
require File.dirname(__FILE__) + '/test_helper.rb'
|
|
2
|
+
|
|
3
|
+
class TestVocabulary < Test::Unit::TestCase
|
|
4
|
+
|
|
5
|
+
def setup
|
|
6
|
+
dictionary = %w(the quick brown fox jumps over the lazy dog but not over the quicker bunny)
|
|
7
|
+
@voc = Vocabulary.new(dictionary)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def test_create_pattern
|
|
11
|
+
ta = ('a'..'j').to_a
|
|
12
|
+
assert_equal(/^[#{ta.join()}]{#{Scrabbler::MIN_WORD_SIZE},#{ta.length}}$/, @voc.create_pattern(ta))
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def test_calculate_subset
|
|
16
|
+
tl = %w(q u i c k)
|
|
17
|
+
assert_equal(['quick'], @voc.calculate_subset(tl, '$'))
|
|
18
|
+
end
|
|
19
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
rubygems_version: 0.9.4
|
|
3
|
+
specification_version: 1
|
|
4
|
+
name: scrabbler
|
|
5
|
+
version: !ruby/object:Gem::Version
|
|
6
|
+
version: 0.1.2
|
|
7
|
+
date: 2008-01-30 00:00:00 +01:00
|
|
8
|
+
summary: A bruteforce word finder for scrabble.
|
|
9
|
+
require_paths:
|
|
10
|
+
- lib
|
|
11
|
+
email: pfmmaas @nospam@ gmail -dot- com
|
|
12
|
+
homepage:
|
|
13
|
+
rubyforge_project:
|
|
14
|
+
description:
|
|
15
|
+
autorequire: scrabbler
|
|
16
|
+
default_executable:
|
|
17
|
+
bindir: bin
|
|
18
|
+
has_rdoc: true
|
|
19
|
+
required_ruby_version: !ruby/object:Gem::Version::Requirement
|
|
20
|
+
requirements:
|
|
21
|
+
- - ">"
|
|
22
|
+
- !ruby/object:Gem::Version
|
|
23
|
+
version: 0.0.0
|
|
24
|
+
version:
|
|
25
|
+
platform: ruby
|
|
26
|
+
signing_key:
|
|
27
|
+
cert_chain:
|
|
28
|
+
post_install_message:
|
|
29
|
+
authors:
|
|
30
|
+
- Peter Maas
|
|
31
|
+
files:
|
|
32
|
+
- lib/array_extensions.rb
|
|
33
|
+
- lib/score.rb
|
|
34
|
+
- lib/scrabbler.rb
|
|
35
|
+
- lib/vocabulary.rb
|
|
36
|
+
- lib/data/en-US.dict
|
|
37
|
+
- lib/data/nederlands.dict
|
|
38
|
+
- lib/data/scores.yml
|
|
39
|
+
- bin/scrabblit
|
|
40
|
+
- README
|
|
41
|
+
test_files:
|
|
42
|
+
- test/test_array_extensions.rb
|
|
43
|
+
- test/test_helper.rb
|
|
44
|
+
- test/test_scrabbler.rb
|
|
45
|
+
- test/test_vocabulary.rb
|
|
46
|
+
rdoc_options: []
|
|
47
|
+
|
|
48
|
+
extra_rdoc_files:
|
|
49
|
+
- README
|
|
50
|
+
executables:
|
|
51
|
+
- scrabblit
|
|
52
|
+
extensions: []
|
|
53
|
+
|
|
54
|
+
requirements: []
|
|
55
|
+
|
|
56
|
+
dependencies: []
|
|
57
|
+
|