pangrid 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []