pangrid 0.2.1
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.
- 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: []
|