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.
@@ -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
@@ -0,0 +1,9 @@
1
+ module Pangrid
2
+
3
+ module PluginUtils
4
+ def check(msg = "")
5
+ raise PuzzleFormatError, msg unless yield
6
+ end
7
+ end
8
+
9
+ end # module Pangrid
@@ -0,0 +1,3 @@
1
+ module Pangrid
2
+ VERSION='0.2.1'
3
+ end
@@ -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: []