acrosslite 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ Copyright (c) 2010 Samuel Mullen
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
data/README.rdoc ADDED
@@ -0,0 +1,88 @@
1
+ = Acrosslite
2
+
3
+ http://github.com/samullen/acrosslite
4
+
5
+ A Ruby library for parsing Across Lite crossword puzzle (.puz) files.
6
+
7
+ The Across Lite format is probably the most popular format for encoding
8
+ crossword puzzles. This library in its current incarnation provides a means for
9
+ retrieving the encoded crossword information from that format. In the future I
10
+ may take a go at building an encoder, but there are some legal issues around
11
+ doing such.
12
+
13
+ For more information about acrosslite, go to http://litsoft.com.
14
+
15
+ == Installation
16
+
17
+ The acrosslite gem is hosted on RubyGems (http://rubygems.org).
18
+
19
+ == Getting Started
20
+
21
+ Instantiation of the library can be done in one of two ways: 1) passing in the full path to a file; 2) by passing in the puzzle blob.
22
+
23
+ require 'acrosslite'
24
+
25
+ ac = Acrosslite.new(:filepath => "/path/to/the/puzzle/file.puz")
26
+
27
+ -- Or --
28
+
29
+ blob = open("/path/to/the/puzzle/file.puz", "r").read
30
+ ac = Acrosslite.new(:content => blob)
31
+
32
+ Useful information about the puzzle can be retrieved with a handful of method
33
+ calls.
34
+
35
+ === Puzzle Meta
36
+
37
+ ac.title # -> Title of the puzzle
38
+ ac.author # -> Author of the puzzle
39
+ ac.copyright # -> Puzzle Copyright
40
+
41
+ === Puzzle Content
42
+
43
+ ac.diagram # -> two-dimensional matrix of the diagram
44
+ ac.solution # -> two-dimensional matrix of the solution
45
+
46
+ ac.rows # -> Array of Acrosslite::Entry objects
47
+ ac.columns # -> Array of Acrosslite::Entry objects
48
+
49
+ Acrosslite::Entry objects are broken down thusly:
50
+
51
+ ac.direction - The direction the answer goes (across, down)
52
+ ac.clue - The clue to provide an answer for
53
+ ac.clue_number - Clue number represented by the little number in a crossword cell
54
+ ac.row - What row the answer begins on (zero-based).
55
+ ac.column - What column the answer begins on (zero-based).
56
+ ac.length - The length of the answer
57
+ ac.cell_number - The "physical" cell the answer begins on
58
+ ac.answer - The answer
59
+
60
+ == Other
61
+
62
+ US Crossword Rules:
63
+ http://www.maa.org/editorial/mathgames/mathgames_05_10_04.html
64
+
65
+ The rules for American crosswords are as follows:
66
+
67
+ 1. The pattern of black-and-white squares must be symmetrical. Generally this rule means that if you turn the grid upside-down, the pattern will look the same as it does right-side-up.
68
+ 2. Do not use too many black squares. In the old days of puzzles, black squares were not allowed to occupy more than 16% of a grid. Nowadays there is no strict limit, in order to allow maximum flexibility for the placement of theme entries. Still, "cheater" black squares (ones that do not affect the number of words in the puzzle, but are added to make constructing easier) should be kept to a minimum, and large clumps of black squares anywhere in a grid are strongly discouraged.
69
+ 3. Do not use unkeyed letters (letters that appear in only one word across or down). In fairness to solvers, every letter has to be appear in both an Across and a Down word.
70
+ 4. Do not use two-letter words. The minimum word length is three letters.
71
+ 5. The grid must have all-over interlock. In other words, the black squares may not cut the grid up into separate pieces. A solver, theoretically, should be able to able to proceed from any section of the grid to any other without having to stop and start over.
72
+ 6. Long theme entries must be symmetrically placed. If there is a major theme entry three rows down from the top of the grid, for instance, then there must be another theme entry in the same position three rows up from the bottom. Also, as a general rule, no nontheme entry should be longer than any theme entry.
73
+ 7. Do not repeat words in the grid.
74
+ 8. Do not make up words and phrases. Every answer must have a reference or else be in common use in everyday speech or writing.
75
+ 9. (Modern rule) The vocabulary in a crossword must be lively and have very little obscurity.
76
+
77
+ == Acknowledgements
78
+
79
+ * Doug Sparling who created the perl Convert-AcrossLite library (http://cpansearch.perl.org/src/DSPARLING/Convert-AcrossLite-0.10/README). I also credit him with getting me into Ruby and getting me excited about programming again.
80
+ * Bob Newell (http://www.gtoal.com/wordgames/gene/AcrossLite) who originally decyphered the Acrosslite format to begin with.
81
+
82
+ == Author
83
+
84
+ Samuel Mullen <samullen@gmail.com>
85
+
86
+ == Copyright
87
+
88
+ Copyright(c) 2010 Samuel Mullen (samullen). See LICENSE for details
data/Rakefile ADDED
@@ -0,0 +1,56 @@
1
+ $:.unshift File.expand_path("../lib", __FILE__)
2
+
3
+ require 'rubygems'
4
+ require 'rake/gempackagetask'
5
+ require 'acrosslite'
6
+
7
+ lib_dir = File.expand_path('lib')
8
+ spec_dir = File.expand_path('spec')
9
+
10
+ gem_spec = Gem::Specification.new do |s|
11
+ s.name = "acrosslite"
12
+ s.version = Acrosslite::VERSION
13
+ s.authors = ["Samuel Mullen"]
14
+ s.email = "samullen@gmail.com"
15
+ s.homepage = "http://github.com/samullen/acrosslite"
16
+ s.summary = "A Ruby library for parsing Across Lite puzzle (.puz) files"
17
+ s.description = false
18
+ s.test_files = Dir['spec/**/*']
19
+ s.add_development_dependency "rspec"
20
+ s.files = [
21
+ "LICENSE",
22
+ "README.rdoc",
23
+ "Rakefile",
24
+ "lib/acrosslite.rb",
25
+ "lib/entry.rb"
26
+ ] + s.test_files
27
+ end
28
+
29
+ begin
30
+ require 'spec/rake/spectask'
31
+ rescue LoadError
32
+ task :spec do
33
+ $stderr.puts '`gem install rspec` to run specs'
34
+ end
35
+ else
36
+ desc "Run specs"
37
+ Spec::Rake::SpecTask.new do |t|
38
+ t.spec_files = Dir['spec/**/*.rb']
39
+ t.spec_opts = %w(-fs --color)
40
+ end
41
+ end
42
+
43
+ Rake::GemPackageTask.new(gem_spec) do |pkg|
44
+ pkg.need_zip = false
45
+ pkg.need_tar = false
46
+ end
47
+
48
+ desc "Install the gem locally"
49
+ task :install => [:spec, :gem] do
50
+ sh %{gem install pkg/#{gem_spec.name}-#{gem_spec.version}}
51
+ end
52
+
53
+ desc "Remove the pkg directory and all of its contents."
54
+ task :clean => :clobber_package
55
+
56
+ task :default => [:spec, :gem]
data/lib/acrosslite.rb ADDED
@@ -0,0 +1,263 @@
1
+ require 'stringio'
2
+
3
+ require File.join(File.dirname(__FILE__), 'entry')
4
+
5
+ class Acrosslite
6
+ attr_reader :across, :down, :solution, :diagram, :copyright, :title, :author,
7
+ :filepath
8
+
9
+ VERSION = '0.2.0'
10
+
11
+ ACROSSLITE = 2
12
+ ROWS = 44
13
+ COLUMNS = 45
14
+ SOLUTION = 52
15
+
16
+ DEFAULT_OPTIONS = {
17
+ :filepath => nil,
18
+ :content => nil,
19
+ }
20
+
21
+ def initialize(*args)
22
+ opts = {}
23
+
24
+ case
25
+ when args.length == 0 then
26
+ when args.length == 1 && args[0].class == Hash then
27
+ arg = args.shift
28
+
29
+ if arg.class == Hash
30
+ opts = arg
31
+ end
32
+ else
33
+ raise ArgumentError, "new() expects hash or hashref as argument"
34
+ end
35
+
36
+ opts = DEFAULT_OPTIONS.merge opts
37
+
38
+ @filepath = opts[:filepath]
39
+ @content = opts[:content] || content
40
+
41
+ @content_io = StringIO.new @content
42
+
43
+ @across = Array.new
44
+ @down = Array.new
45
+ @layout = Array.new
46
+ @solution = Array.new
47
+ @diagram = Array.new
48
+ end
49
+
50
+ def content
51
+ @content ||= read_puzzle
52
+ end
53
+
54
+ def rows
55
+ unless @rows
56
+ @content_io.seek(ROWS)
57
+ @rows = @content_io.read(1).unpack('C').first
58
+ end
59
+ @rows
60
+ end
61
+
62
+ def columns
63
+ unless @columns
64
+ @content_io.seek(COLUMNS)
65
+ @columns = @content_io.read(1).unpack('C').first
66
+ end
67
+ @columns
68
+ end
69
+
70
+ def solution
71
+ width = columns
72
+
73
+ # if @solution.empty?
74
+ unless @solution.size == rows
75
+ @content_io.seek(SOLUTION)
76
+
77
+ rows.times do |r|
78
+ @solution << @content_io.read(width).unpack("C#{width}").map {|c| c.chr}
79
+ end
80
+ end
81
+ @solution
82
+ end
83
+
84
+ def diagram
85
+ width = columns
86
+
87
+ if @diagram.empty?
88
+ @content_io.seek(SOLUTION + rows * width)
89
+
90
+ rows.times do |r|
91
+ @diagram << @content_io.read(width).unpack("C#{width}").map {|c| c.chr}
92
+ end
93
+ end
94
+ @diagram
95
+ end
96
+
97
+ def area
98
+ rows * columns
99
+ end
100
+
101
+ def across
102
+ if @across.empty?
103
+ parse
104
+ end
105
+
106
+ @across
107
+ end
108
+
109
+ def down
110
+ if @down.empty?
111
+ parse
112
+ end
113
+
114
+ @down
115
+ end
116
+
117
+ def title
118
+ unless @title
119
+ parse
120
+ end
121
+
122
+ @title
123
+ end
124
+
125
+ def author
126
+ unless @author
127
+ parse
128
+ end
129
+
130
+ @author
131
+ end
132
+
133
+ def copyright
134
+ unless @copyright
135
+ parse
136
+ end
137
+
138
+ @copyright
139
+ end
140
+
141
+ =begin rdoc
142
+ If a filehandle or filepath were provided, open reads in the file's contents
143
+ into the content attribute which is then used for parsing.
144
+
145
+ open must be called prior to parsing if content has not already been provided.
146
+
147
+ =end
148
+ def read_puzzle(filepath=nil)
149
+ filepath ||= @filepath
150
+ raise unless filepath
151
+ @content = open(@filepath).read
152
+ end
153
+
154
+ private
155
+
156
+ =begin rdoc
157
+ =end
158
+ def next_field
159
+ string = String.new
160
+
161
+ while (c = @content_io.getc.chr) != "\0"
162
+ string += c
163
+ end
164
+
165
+ return string
166
+ end
167
+
168
+ def content_io
169
+ @content_io ||= StringIO.new @content
170
+ end
171
+
172
+ =begin rdoc
173
+ The parse method takes the puzzle loaded into content and breaks it out into the
174
+ following attributes: rows, columns, solution, diagram, title, author, copyright, across, and down.
175
+ =end
176
+
177
+ def parse
178
+ clues = Array.new
179
+
180
+ @content_io.seek(SOLUTION + area + area)
181
+
182
+ @title = next_field
183
+ @author = next_field
184
+ @copyright = next_field
185
+
186
+ #----- build clues array -----#
187
+ until @content_io.eof? do
188
+ clues << next_field
189
+ end
190
+
191
+ #----- determine answers -----#
192
+ across_clue = down_clue = 1 # clue_number: incremented only in "down" area
193
+
194
+ 0.upto(rows - 1) do |r|
195
+ 0.upto(columns - 1) do |c|
196
+ next if solution[r][c] =~ /[.:]/
197
+
198
+ if c - 1 < 0 || solution[r][c - 1] == "."
199
+ entry = Acrosslite::Entry.new
200
+ answer = ''
201
+
202
+ c.upto(columns - 1) do |cc|
203
+ char = solution[r][cc]
204
+
205
+ if char != '.'
206
+ answer += char
207
+ end
208
+
209
+ if char == "." || cc + 1 >= columns
210
+ entry.direction = "across"
211
+ entry.clue = clues.shift
212
+ entry.answer = answer
213
+ entry.clue_number = across_clue
214
+ entry.row = r
215
+ entry.column = c
216
+ entry.length = answer.size
217
+ entry.cell_number = r * columns + c + 1
218
+
219
+ @across << entry
220
+ across_clue += 1
221
+ break
222
+ end
223
+ end
224
+ end
225
+
226
+ if r - 1 < 0 || solution[r - 1][c] == "."
227
+ entry = Acrosslite::Entry.new
228
+ answer = ''
229
+
230
+ r.upto(rows - 1) do |rr|
231
+ char = solution[rr][c]
232
+
233
+ if char != '.'
234
+ answer += char
235
+ end
236
+
237
+ if char == "." || rr + 1 >= rows
238
+ entry.direction = "down"
239
+ entry.clue = clues.shift
240
+ entry.answer = answer
241
+ entry.clue_number = down_clue
242
+ entry.row = r
243
+ entry.column = c
244
+ entry.length = answer.size
245
+ entry.cell_number = r * columns + c + 1
246
+
247
+ @down << entry
248
+ down_clue += 1
249
+ break
250
+ end
251
+ end
252
+ end
253
+
254
+ if across_clue > down_clue
255
+ down_clue = across_clue
256
+ else
257
+ across_clue = down_clue
258
+ end
259
+ end
260
+ end
261
+ end
262
+
263
+ end
data/lib/entry.rb ADDED
@@ -0,0 +1,9 @@
1
+ class Acrosslite
2
+ class Entry
3
+ attr_accessor :direction, :clue, :clue_number, :row, :column, :length,
4
+ :cell_number, :answer
5
+
6
+ def initialize
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,102 @@
1
+ require File.join(File.dirname(__FILE__), '..', 'lib','acrosslite')
2
+
3
+ describe Acrosslite do
4
+ before(:all) do
5
+ basedir = File.dirname(__FILE__)
6
+ @example_files = Hash.new
7
+
8
+ @example_files[:halloween] = File.join(basedir, "files/halloween2009.puz")
9
+ @example_files[:crnet] = File.join(basedir, "files/crnet100306.puz")
10
+ @example_files[:tmcal] = File.join(basedir, "files/tmcal100306.puz")
11
+ @example_files[:xp] = File.join(basedir, "files/xp100306.puz")
12
+ @example_files[:ydx] = File.join(basedir, "files/ydx100515.puz")
13
+ end
14
+
15
+ # before(:each) do
16
+ # end
17
+ #
18
+ # after(:each) do
19
+ # end
20
+
21
+ #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=#
22
+ # Builder Tests
23
+ #-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=#
24
+
25
+ it "should instantiate the puzzle with passing of file" do
26
+ ac = Acrosslite.new(:filepath => @example_files[:halloween])
27
+ ac.should be_an_instance_of Acrosslite
28
+ ac.filepath.should == @example_files[:halloween]
29
+ ac.content.should == File.open(@example_files[:halloween]).read
30
+ end
31
+
32
+ it "should instantiate the puzzle with passing of an acrosslite blob" do
33
+ ac = Acrosslite.new(:content => File.open(@example_files[:halloween]).read)
34
+ ac.should be_an_instance_of Acrosslite
35
+ ac.content.should == File.open(@example_files[:halloween]).read
36
+ end
37
+
38
+ it "should parse dimensions" do
39
+ ac = Acrosslite.new(:filepath => @example_files[:tmcal])
40
+ ac.rows.should == 15
41
+ ac.columns.should == 15
42
+ ac.area.should == 15 * 15
43
+ end
44
+
45
+ it "should parse solution and diagram" do
46
+ ac = Acrosslite.new(:filepath => @example_files[:crnet])
47
+ ac.solution.join.should == 'PASSED.MAMACASSLEANER.AMICABLYURBANA.FIGUREONCARRYWEIGHT.TVAKTEL.BLOAT.STEPYES.GLASS.COINS...KYOTO.TOWNIEMCENROE.RAREGASALLIED.RAKED...NESTS.HADES.PEWDAIS.REPOS.MUNIARN.FORINSTANCELOOKATME.IONIANAUREVOIR.CROSSESTEWARTS.KOSHER'
48
+ ac.diagram.join.should == '------.--------------.--------------.-------------------.-------.-----.-------.-----.-----...-----.-------------.-------------.-----...-----.-----.-------.-----.-------.-------------------.--------------.--------------.------'
49
+ end
50
+
51
+ it "should retrieve meta data about the puzzle" do
52
+ ac = Acrosslite.new(:filepath => @example_files[:tmcal])
53
+ ac.title.should == 'LA Times, Sat, Mar 6, 2010'
54
+ ac.author.should == 'Barry C. Silk / Ed. Rich Norris'
55
+ # ac.copyright.should == "© 2010 Tribune Media Services, Inc."
56
+
57
+ ac = Acrosslite.new(:filepath => @example_files[:crnet])
58
+ ac.title.should == '03/06/10 SATURDAY STUMPER'
59
+ ac.author.should == 'Merle Baker , edited by Stanley Newman'
60
+ # ac.copyright.should == "© Copyright 2010 Stanley Newman, Distributed by Creators Syndicate, Inc."
61
+
62
+ ac = Acrosslite.new(:filepath => @example_files[:xp])
63
+ ac.title.should == ''
64
+ ac.author.should == ''
65
+ ac.copyright.should == ''
66
+ end
67
+
68
+ it "should retrieve the data for each of the entries" do
69
+ ac = Acrosslite.new(:filepath => @example_files[:halloween])
70
+ ac.across.first.clue.should == "Item sought by kids in costumes"
71
+ ac.across.first.answer.should == "CANDY"
72
+ ac.across.first.clue_number.should == 1
73
+ ac.across.first.row.should == 0
74
+ ac.across.first.column.should == 0
75
+ ac.across.first.length.should == 5
76
+ ac.across.first.cell_number.should == 1
77
+
78
+ ac.across.last.clue.should == "Has to have"
79
+ ac.across.last.answer.should == "NEEDS"
80
+ ac.across.last.clue_number.should == 73
81
+ ac.across.last.row.should == 14
82
+ ac.across.last.column.should == 10
83
+ ac.across.last.length.should == 5
84
+ ac.across.last.cell_number.should == 221
85
+
86
+ ac.down.first.clue.should == "Slept under the stars"
87
+ ac.down.first.answer.should == "CAMPED"
88
+ ac.down.first.clue_number.should == 1
89
+ ac.down.first.row.should == 0
90
+ ac.down.first.column.should == 0
91
+ ac.down.first.length.should == 6
92
+ ac.down.first.cell_number.should == 1
93
+
94
+ ac.down.last.clue.should == "Take in slowly"
95
+ ac.down.last.answer.should == "SIP"
96
+ ac.down.last.clue_number.should == 66
97
+ ac.down.last.row.should == 12
98
+ ac.down.last.column.should == 6
99
+ ac.down.last.length.should == 3
100
+ ac.down.last.cell_number.should == 187
101
+ end
102
+ end
Binary file
Binary file
Binary file
Binary file
Binary file
metadata ADDED
@@ -0,0 +1,88 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: acrosslite
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 2
8
+ - 0
9
+ version: 0.2.0
10
+ platform: ruby
11
+ authors:
12
+ - Samuel Mullen
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-03-25 00:00:00 -05:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: rspec
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ segments:
28
+ - 0
29
+ version: "0"
30
+ type: :development
31
+ version_requirements: *id001
32
+ description: "false"
33
+ email: samullen@gmail.com
34
+ executables: []
35
+
36
+ extensions: []
37
+
38
+ extra_rdoc_files: []
39
+
40
+ files:
41
+ - LICENSE
42
+ - README.rdoc
43
+ - Rakefile
44
+ - lib/acrosslite.rb
45
+ - lib/entry.rb
46
+ - spec/acrosslite_spec.rb
47
+ - spec/files/tmcal100306.puz
48
+ - spec/files/crnet100306.puz
49
+ - spec/files/halloween2009.puz
50
+ - spec/files/ydx100515.puz
51
+ - spec/files/xp100306.puz
52
+ has_rdoc: true
53
+ homepage: http://github.com/samullen/acrosslite
54
+ licenses: []
55
+
56
+ post_install_message:
57
+ rdoc_options: []
58
+
59
+ require_paths:
60
+ - lib
61
+ required_ruby_version: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ segments:
66
+ - 0
67
+ version: "0"
68
+ required_rubygems_version: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ segments:
73
+ - 0
74
+ version: "0"
75
+ requirements: []
76
+
77
+ rubyforge_project:
78
+ rubygems_version: 1.3.6
79
+ signing_key:
80
+ specification_version: 3
81
+ summary: A Ruby library for parsing Across Lite puzzle (.puz) files
82
+ test_files:
83
+ - spec/acrosslite_spec.rb
84
+ - spec/files/tmcal100306.puz
85
+ - spec/files/crnet100306.puz
86
+ - spec/files/halloween2009.puz
87
+ - spec/files/ydx100515.puz
88
+ - spec/files/xp100306.puz