pangrid 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README +22 -0
- data/bin/pangrid +5 -0
- data/lib/deps/trollop.rb +838 -0
- data/lib/pangrid.rb +69 -0
- data/lib/pangrid/frontend/webrick.rb +74 -0
- data/lib/pangrid/plugin.rb +105 -0
- data/lib/pangrid/plugins/acrosslite.rb +466 -0
- data/lib/pangrid/plugins/csv.rb +83 -0
- data/lib/pangrid/plugins/excel.rb +66 -0
- data/lib/pangrid/plugins/reddit.rb +62 -0
- data/lib/pangrid/plugins/text.rb +34 -0
- data/lib/pangrid/utils.rb +9 -0
- data/lib/pangrid/version.rb +3 -0
- data/lib/pangrid/xw.rb +152 -0
- metadata +62 -0
@@ -0,0 +1,83 @@
|
|
1
|
+
# CSV representation
|
2
|
+
#
|
3
|
+
# Useful if you develop your grid in a spreadsheet, for example.
|
4
|
+
#
|
5
|
+
# Expected header format:
|
6
|
+
#
|
7
|
+
# Width: 15, Height: 15, Offset: 0, Black: ., Empty: -, Clues: 3
|
8
|
+
#
|
9
|
+
# detailing
|
10
|
+
# - the width, height and starting column of the
|
11
|
+
# grid
|
12
|
+
# - the characters representing black and empty squares
|
13
|
+
# - the column in which the clues are in after the
|
14
|
+
# grid.
|
15
|
+
#
|
16
|
+
#
|
17
|
+
# Blank rows are ignored, as are all rows before the
|
18
|
+
# header, and rows after the grid with nothing in
|
19
|
+
# the clue column.
|
20
|
+
#
|
21
|
+
# provides:
|
22
|
+
# CSV: read
|
23
|
+
|
24
|
+
require 'csv'
|
25
|
+
|
26
|
+
module Pangrid
|
27
|
+
|
28
|
+
class CSV < Plugin
|
29
|
+
def read(data)
|
30
|
+
s = ::CSV.parse(data)
|
31
|
+
s.reject! {|row| row.compact.empty?}
|
32
|
+
while s[0] && s[0][0] !~ /^Width:/i do
|
33
|
+
s.shift
|
34
|
+
end
|
35
|
+
check("No header row found. Header needs a 'Width: ' cell in the first column.") { !s.empty? }
|
36
|
+
xw = XWord.new
|
37
|
+
h = s.shift.map {|c| c.split(/:\s*/)}
|
38
|
+
header = OpenStruct.new
|
39
|
+
h.each do |k, v|
|
40
|
+
header[k.downcase] = v
|
41
|
+
end
|
42
|
+
|
43
|
+
header.clues = header.clues.to_i
|
44
|
+
xw.width = header.width.to_i
|
45
|
+
xw.height = header.height.to_i
|
46
|
+
xw.solution = []
|
47
|
+
xw.height.times do
|
48
|
+
row = s.shift
|
49
|
+
check("Row does not have #{xw.width} cells: \n" +
|
50
|
+
row.join(',')) { row.length == xw.width }
|
51
|
+
xw.solution << row.map do |c|
|
52
|
+
cell = Cell.new
|
53
|
+
if c == header.black
|
54
|
+
cell.solution = :black
|
55
|
+
elsif c == header.empty or c == nil
|
56
|
+
cell.solution = :null
|
57
|
+
else
|
58
|
+
cell.solution = c.gsub /[^[:alpha:]]/, ''
|
59
|
+
end
|
60
|
+
cell
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
xw.clues = []
|
65
|
+
s.each do |row|
|
66
|
+
if row[header.clues]
|
67
|
+
xw.clues << row[header.clues]
|
68
|
+
end
|
69
|
+
end
|
70
|
+
unpack_clues(xw)
|
71
|
+
xw
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
def unpack_clues(xw)
|
76
|
+
across, down = xw.number
|
77
|
+
n_across = across.length
|
78
|
+
xw.across_clues = xw.clues[0 ... n_across]
|
79
|
+
xw.down_clues = xw.clues[n_across .. -1]
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
end # module Pangrid
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# Write puzzles out in Excel's .xslx format
|
2
|
+
#
|
3
|
+
# Useful primarily for importing a grid into a google spreadsheet for online,
|
4
|
+
# collaborative solving
|
5
|
+
#
|
6
|
+
# provides:
|
7
|
+
# ExcelXSLX : write
|
8
|
+
|
9
|
+
module Pangrid
|
10
|
+
|
11
|
+
require_for_plugin 'excel', ['axlsx']
|
12
|
+
|
13
|
+
class ExcelXSLX < Plugin
|
14
|
+
# styles
|
15
|
+
STYLES = {
|
16
|
+
:black => {:bg_color => "00"},
|
17
|
+
:white => {
|
18
|
+
:bg_color => "FF", :fg_color => "00",
|
19
|
+
:alignment => { :horizontal=> :right },
|
20
|
+
:border => Axlsx::STYLE_THIN_BORDER
|
21
|
+
}
|
22
|
+
}
|
23
|
+
|
24
|
+
# cobble a cell number together out of unicode superscript chars
|
25
|
+
SUPERSCRIPTS = %w(⁰ ¹ ² ³ ⁴ ⁵ ⁶ ⁷ ⁸ ⁹)
|
26
|
+
|
27
|
+
def format_number(n)
|
28
|
+
n.to_s.split(//).map {|c| SUPERSCRIPTS[c.to_i]}.join("").rjust(3)
|
29
|
+
end
|
30
|
+
|
31
|
+
def write(xw)
|
32
|
+
xw.number
|
33
|
+
rows = xw.to_array(:black => " ", :null => " ") {|c|
|
34
|
+
format_number(c.number) + " " +
|
35
|
+
# We can insert the entire word for rebuses
|
36
|
+
c.solution
|
37
|
+
}
|
38
|
+
|
39
|
+
styles = xw.to_array(:black => :black, :null => :white) {
|
40
|
+
:white
|
41
|
+
}
|
42
|
+
|
43
|
+
p = Axlsx::Package.new
|
44
|
+
wb = p.workbook
|
45
|
+
|
46
|
+
# styles
|
47
|
+
wb.styles do |s|
|
48
|
+
xstyles = {}
|
49
|
+
STYLES.map {|k, v| xstyles[k] = s.add_style v}
|
50
|
+
wb.add_worksheet(:name => "Crossword") do |sheet|
|
51
|
+
rowstyles = styles.map {|r|
|
52
|
+
r.map {|c| xstyles[c]}
|
53
|
+
}
|
54
|
+
rows.zip(rowstyles).each {|r, s|
|
55
|
+
sheet.add_row r, :style => s
|
56
|
+
}
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
out = p.to_stream(true)
|
61
|
+
check("Spreadsheet did not validate") { out }
|
62
|
+
out.read
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
end # module Pangrid
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# Markup used by crosswords.reddit.com
|
2
|
+
|
3
|
+
module Pangrid
|
4
|
+
|
5
|
+
module RedditWriter
|
6
|
+
def write_line(row)
|
7
|
+
'|' + row.join('|') + '|'
|
8
|
+
end
|
9
|
+
|
10
|
+
def write_table(grid)
|
11
|
+
width = grid[0].length
|
12
|
+
out = grid.map {|row| write_line(row)}
|
13
|
+
sep = write_line(["--"] * width)
|
14
|
+
out = [out[0], sep] + out[1..-1]
|
15
|
+
out.join("\n") + "\n"
|
16
|
+
end
|
17
|
+
|
18
|
+
def format_clues(numbers, clues, indent)
|
19
|
+
numbers.zip(clues).map {|n, c| " "*indent + "#{n}\\. #{c}"}.join("\n\n")
|
20
|
+
end
|
21
|
+
|
22
|
+
def write_clues(xw, across, down)
|
23
|
+
ac = "**Across**\n\n" + format_clues(across, xw.across_clues, 2)
|
24
|
+
dn = "**Down**\n\n" + format_clues(down, xw.down_clues, 2)
|
25
|
+
ac + "\n\n" + dn
|
26
|
+
end
|
27
|
+
|
28
|
+
def write_xw(xw)
|
29
|
+
across, down = xw.number
|
30
|
+
write_table(grid(xw)) + "\n\n" + write_clues(xw, across, down) + "\n"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class RedditFilled < Plugin
|
35
|
+
include RedditWriter
|
36
|
+
|
37
|
+
def write(xw)
|
38
|
+
write_xw(xw)
|
39
|
+
end
|
40
|
+
|
41
|
+
def grid(xw)
|
42
|
+
xw.to_array({:black => '*.*', :null => ' '}) do |c|
|
43
|
+
c.to_char.upcase + (c.number ? "^#{c.number}" : '')
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
class RedditBlank < Plugin
|
49
|
+
include RedditWriter
|
50
|
+
|
51
|
+
def write(xw)
|
52
|
+
write_xw(xw)
|
53
|
+
end
|
54
|
+
|
55
|
+
def grid(xw)
|
56
|
+
xw.to_array({:black => '*.*', :null => ' '}) do |c|
|
57
|
+
c.number ? "^#{c.number}" : ''
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
end # module Pangrid
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# Plain text representation
|
2
|
+
#
|
3
|
+
# Mostly used for debugging and quick printing of a grid right now, but it
|
4
|
+
# would be useful to have a minimalist text representation of a grid and clues.
|
5
|
+
#
|
6
|
+
# provides:
|
7
|
+
# Text : write
|
8
|
+
|
9
|
+
module Pangrid
|
10
|
+
|
11
|
+
class Text < Plugin
|
12
|
+
def write(xw)
|
13
|
+
across, down = xw.number
|
14
|
+
rows = xw.to_array(:black => '#', :null => ' ')
|
15
|
+
grid = rows.map(&:join).join("\n") + "\n"
|
16
|
+
ac = "Across:\n\n" + format_clues(across, xw.across_clues, 2)
|
17
|
+
dn = "Down:\n\n" + format_clues(down, xw.down_clues, 2)
|
18
|
+
grid + "\n" + ac + "\n\n" + dn + "\n"
|
19
|
+
end
|
20
|
+
|
21
|
+
# rename to 'read' when this is complete
|
22
|
+
def read_grid(data)
|
23
|
+
s = data.each_line.map(&:strip)
|
24
|
+
m = s[0].match(/^Grid (\d+) (\d+)$/)
|
25
|
+
check("Grid line missing") { m }
|
26
|
+
xw.height, xw.width = m[1].to_i, m[2].to_i
|
27
|
+
end
|
28
|
+
|
29
|
+
def format_clues(numbers, clues, indent)
|
30
|
+
numbers.zip(clues).map {|n, c| " "*indent + "#{n}. #{c}"}.join("\n")
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
end # module Pangrid
|
data/lib/pangrid/xw.rb
ADDED
@@ -0,0 +1,152 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
|
3
|
+
module Pangrid
|
4
|
+
|
5
|
+
class PuzzleFormatError < StandardError
|
6
|
+
end
|
7
|
+
|
8
|
+
# symbol: the symbol representing the solution in the grid
|
9
|
+
# (populated by xword.encode_rebus!)
|
10
|
+
# solution: the word the symbol represents
|
11
|
+
# display_char: optional character representation of a rebus square
|
12
|
+
class Rebus
|
13
|
+
attr_accessor :symbol, :solution, :display_char
|
14
|
+
|
15
|
+
def initialize(str, char = nil)
|
16
|
+
@symbol = nil
|
17
|
+
@solution = str
|
18
|
+
@display_char = char || str[0]
|
19
|
+
end
|
20
|
+
|
21
|
+
def to_char
|
22
|
+
symbol || display_char
|
23
|
+
end
|
24
|
+
|
25
|
+
def inspect
|
26
|
+
"[#{symbol}|#{solution}]"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# solution = :black | :null | char | Rebus
|
31
|
+
# number = int
|
32
|
+
# borders = [:left, :right, :top, :bottom]
|
33
|
+
class Cell
|
34
|
+
attr_accessor :solution, :number, :borders
|
35
|
+
|
36
|
+
def initialize(**args)
|
37
|
+
args.each {|k,v| self.send :"#{k}=", v}
|
38
|
+
end
|
39
|
+
|
40
|
+
def black?
|
41
|
+
solution == :black
|
42
|
+
end
|
43
|
+
|
44
|
+
def has_bar?(s)
|
45
|
+
borders.include? s
|
46
|
+
end
|
47
|
+
|
48
|
+
def rebus?
|
49
|
+
solution.is_a?(Rebus)
|
50
|
+
end
|
51
|
+
|
52
|
+
def to_char
|
53
|
+
rebus? ? solution.to_char : solution
|
54
|
+
end
|
55
|
+
|
56
|
+
def inspect
|
57
|
+
case solution
|
58
|
+
when :black; '#'
|
59
|
+
when :null; '.'
|
60
|
+
when Rebus; solution.inspect
|
61
|
+
else; solution
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# solution = Cell[][]
|
67
|
+
# width, height = int
|
68
|
+
# across_clues = string[]
|
69
|
+
# down_clues = string[]
|
70
|
+
# rebus = { solution => [int, rebus_char] }
|
71
|
+
class XWord < OpenStruct
|
72
|
+
# Clue numbering
|
73
|
+
def black?(x, y)
|
74
|
+
solution[y][x].black?
|
75
|
+
end
|
76
|
+
|
77
|
+
def boundary?(x, y)
|
78
|
+
(x < 0) || (y < 0) || (x >= width) || (y >= height) || black?(x, y)
|
79
|
+
end
|
80
|
+
|
81
|
+
def across?(x, y)
|
82
|
+
boundary?(x - 1, y) && !boundary?(x, y) && !boundary?(x + 1, y)
|
83
|
+
end
|
84
|
+
|
85
|
+
def down?(x, y)
|
86
|
+
boundary?(x, y - 1) && !boundary?(x, y) && !boundary?(x, y + 1)
|
87
|
+
end
|
88
|
+
|
89
|
+
def number
|
90
|
+
n, across, down = 1, [], []
|
91
|
+
(0 ... height).each do |y|
|
92
|
+
(0 ... width).each do |x|
|
93
|
+
across << n if across? x, y
|
94
|
+
down << n if down? x, y
|
95
|
+
if across.last == n || down.last == n
|
96
|
+
solution[y][x].number = n
|
97
|
+
n += 1
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
[across, down]
|
102
|
+
end
|
103
|
+
|
104
|
+
def each_cell
|
105
|
+
(0 ... height).each do |y|
|
106
|
+
(0 ... width).each do |x|
|
107
|
+
yield solution[y][x]
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
# {:black => char, :null => char} -> Any[][]
|
113
|
+
def to_array(opts = {})
|
114
|
+
opts = {:black => '#', :null => ' '}.merge(opts)
|
115
|
+
solution.map {|row|
|
116
|
+
row.map {|c|
|
117
|
+
s = c.solution
|
118
|
+
case s
|
119
|
+
when :black, :null
|
120
|
+
opts[s]
|
121
|
+
when String
|
122
|
+
block_given? ? (yield c) : c.to_char
|
123
|
+
when Rebus
|
124
|
+
block_given? ? (yield c) : c.to_char
|
125
|
+
else
|
126
|
+
raise PuzzleFormatError, "Unrecognised cell #{c}"
|
127
|
+
end
|
128
|
+
}
|
129
|
+
}
|
130
|
+
end
|
131
|
+
|
132
|
+
# Collect a hash of rebus solutions, each mapped to an integer.
|
133
|
+
def encode_rebus!
|
134
|
+
k = 0
|
135
|
+
self.rebus = {}
|
136
|
+
each_cell do |c|
|
137
|
+
if c.rebus?
|
138
|
+
r = c.solution
|
139
|
+
if self.rebus[s]
|
140
|
+
sym, char = self.rebus[s]
|
141
|
+
r.symbol = sym.to_s
|
142
|
+
else
|
143
|
+
k += 1
|
144
|
+
self.rebus[r.solution] = [k, r.display_char]
|
145
|
+
r.symbol = k.to_s
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
end # module Pangrid
|
metadata
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: pangrid
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Martin DeMello
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-03-09 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description:
|
14
|
+
email: martindemello@gmail.com
|
15
|
+
executables:
|
16
|
+
- pangrid
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files:
|
19
|
+
- README
|
20
|
+
files:
|
21
|
+
- LICENSE
|
22
|
+
- README
|
23
|
+
- bin/pangrid
|
24
|
+
- lib/deps/trollop.rb
|
25
|
+
- lib/pangrid.rb
|
26
|
+
- lib/pangrid/frontend/webrick.rb
|
27
|
+
- lib/pangrid/plugin.rb
|
28
|
+
- lib/pangrid/plugins/acrosslite.rb
|
29
|
+
- lib/pangrid/plugins/csv.rb
|
30
|
+
- lib/pangrid/plugins/excel.rb
|
31
|
+
- lib/pangrid/plugins/reddit.rb
|
32
|
+
- lib/pangrid/plugins/text.rb
|
33
|
+
- lib/pangrid/utils.rb
|
34
|
+
- lib/pangrid/version.rb
|
35
|
+
- lib/pangrid/xw.rb
|
36
|
+
homepage: https://github.com/martindemello/pangrid
|
37
|
+
licenses:
|
38
|
+
- MIT
|
39
|
+
metadata: {}
|
40
|
+
post_install_message:
|
41
|
+
rdoc_options:
|
42
|
+
- "--main"
|
43
|
+
- README
|
44
|
+
require_paths:
|
45
|
+
- lib
|
46
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
47
|
+
requirements:
|
48
|
+
- - ">="
|
49
|
+
- !ruby/object:Gem::Version
|
50
|
+
version: '0'
|
51
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '0'
|
56
|
+
requirements: []
|
57
|
+
rubyforge_project:
|
58
|
+
rubygems_version: 2.5.1
|
59
|
+
signing_key:
|
60
|
+
specification_version: 4
|
61
|
+
summary: A crossword file format converter
|
62
|
+
test_files: []
|