class Pangrid::AcrossLiteBinary

Binary format

Constants

DESCRIPTION
EXTENSIONS
EXT_HEADER_FORMAT
FILE_MAGIC
HEADER_CHECKSUM_FORMAT
HEADER_FORMAT

Attributes

cs[RW]

crossword, checksums

xw[RW]

crossword, checksums

Public Class Methods

new() click to toggle source
# File lib/pangrid/plugins/acrosslite.rb, line 86
def initialize
  @xw = XWord.new
  @cs = OpenStruct.new
  @xw.extensions = []
end

Public Instance Methods

read(data) click to toggle source
# File lib/pangrid/plugins/acrosslite.rb, line 92
def read(data)
  s = data.force_encoding("ISO-8859-1")

  i = s.index(FILE_MAGIC)
  check("Could not recognise AcrossLite binary file") { i }

  # read the header
  h_start, h_end = i - 2, i - 2 + 0x34
  header = s[h_start .. h_end]

  cs.global, _, cs.cib, cs.masked_low, cs.masked_high,
    xw.version, _, cs.scrambled, _,
    xw.width, xw.height, xw.n_clues, xw.puzzle_type, xw.scrambled_state =
    header.unpack(HEADER_FORMAT)

  # solution and fill = blocks of w*h bytes each
  size = xw.width * xw.height
  xw.solution = unpack_solution xw, s[h_end, size]
  xw.fill = s[h_end + size, size]
  s = s[h_end + 2 * size .. -1]

  # title, author, copyright, clues * n, notes = zero-terminated strings
  xw.title, xw.author, xw.copyright, *xw.clues, xw.notes, s =
    s.split("\0", xw.n_clues + 5)

  # extensions: 8-byte header + len bytes data + \0
  while (s.length > 8) do
    e = OpenStruct.new
    e.section, e.len, e.checksum = s.unpack(EXT_HEADER_FORMAT)
    check("Unrecognised extension #{e.section}") { EXTENSIONS.include? e.section }
    size = 8 + e.len + 1
    break if s.length < size
    e.data = s[8 ... size]
    self.send(:"read_#{e.section.downcase}", e)
    xw.extensions << e
    s = s[size .. -1]
  end

  # verify checksums
  check("Failed checksum") { checksums == cs }

  process_extensions
  unpack_clues

  xw
end
write(xw) click to toggle source
# File lib/pangrid/plugins/acrosslite.rb, line 139
def write(xw)
  @xw = xw

  # fill in some fields that might not be present (checksums needs this)
  pack_clues
  xw.clues = xw.clues.map(&:to_s)
  xw.n_clues = xw.clues.length
  xw.fill ||= empty_fill(xw)
  xw.puzzle_type ||= 1
  xw.scrambled_state ||= 0
  xw.version = "1.3"
  xw.notes ||= ""
  xw.extensions ||= []
  xw.title ||= ""
  xw.author ||= ""
  xw.copyright ||= ""

  # extensions
  xw.encode_rebus!
  if not xw.rebus.empty?
    # GRBS
    e = OpenStruct.new
    e.section = "GRBS"
    e.grid = xw.to_array({:black => 0, :null => 0}) {|s|
      s.rebus? ? s.solution.symbol.to_i : 0
    }.flatten
    xw.extensions << e
    # RTBL
    e = OpenStruct.new
    e.section = "RTBL"
    e.rebus = {}
    xw.rebus.each do |long, (k, short)|
      e.rebus[k] = [long, short]
    end
    xw.extensions << e
  end

  # calculate checksums
  @cs = checksums

  h = [cs.global, FILE_MAGIC, cs.cib, cs.masked_low, cs.masked_high,
       xw.version + "\0", 0, cs.scrambled, "\0" * 12,
       xw.width, xw.height, xw.n_clues, xw.puzzle_type, xw.scrambled_state]
  header = h.pack(HEADER_FORMAT)

  strings = [xw.title, xw.author, xw.copyright] + xw.clues + [xw.notes]
  strings = strings.map {|x| x + "\0"}.join

  [header, pack_solution(xw), xw.fill, strings, write_extensions].map {|x|
    x.force_encoding("ISO-8859-1")
  }.join
end

Private Instance Methods

checksums() click to toggle source
# File lib/pangrid/plugins/acrosslite.rb, line 348
def checksums
  c = OpenStruct.new
  c.masked_low, c.masked_high = magic_checksums
  c.cib = header_checksum
  c.global = global_checksum
  c.scrambled = 0
  c
end
get_extension(s) click to toggle source
# File lib/pangrid/plugins/acrosslite.rb, line 227
def get_extension(s)
  return nil unless xw.extensions
  xw.extensions.find {|e| e.section == s}
end
global_checksum() click to toggle source
# File lib/pangrid/plugins/acrosslite.rb, line 324
def global_checksum
  c = Checksum.new header_checksum
  c.add_string pack_solution(xw)
  c.add_string xw.fill
  text_checksum c.sum
end
header_checksum() click to toggle source
# File lib/pangrid/plugins/acrosslite.rb, line 319
def header_checksum
  h = [xw.width, xw.height, xw.n_clues, xw.puzzle_type, xw.scrambled_state]
  Checksum.of_string h.pack(HEADER_CHECKSUM_FORMAT)
end
magic_checksums() click to toggle source
# File lib/pangrid/plugins/acrosslite.rb, line 331
def magic_checksums
  mask = "ICHEATED".bytes
  sums = [
    text_checksum(0),
    Checksum.of_string(xw.fill),
    Checksum.of_string(pack_solution(xw)),
    header_checksum
  ]

  l, h = 0, 0
  sums.each_with_index do |sum, i|
    l = (l << 8) | (mask[3 - i] ^ (sum & 0xff))
    h = (h << 8) | (mask[7 - i] ^ (sum >> 8))
  end
  [l, h]
end
pack_clues() click to toggle source

combine across and down clues -> xw.clues

# File lib/pangrid/plugins/acrosslite.rb, line 210
def pack_clues
  across, down = xw.number
  clues = across.map {|x| [x, :a]} + down.map {|x| [x, :d]}
  clues.sort!
  ac, dn = xw.across_clues.dup, xw.down_clues.dup
  xw.clues = []
  clues.each do |n, dir|
    if dir == :a
      xw.clues << ac.shift
    else
      xw.clues << dn.shift
    end
  end
  check("Extra across clue") { ac.empty? }
  check("Extra down clue") { dn.empty? }
end
process_extensions() click to toggle source
# File lib/pangrid/plugins/acrosslite.rb, line 232
def process_extensions
  # record these for file inspection, though they're unlikely to be useful
  if (ltim = get_extension("LTIM"))
    xw.time_elapsed = ltim.elapsed
    xw.paused
  end

  # we need both grbs and rtbl
  grbs, rtbl = get_extension("GRBS"), get_extension("RTBL")
  if grbs and rtbl
    grbs.grid.each_with_index do |n, i|
      if n > 0 and (v = rtbl.rebus[n])
        x, y = i % xw.width, i / xw.width
        cell = xw.solution[y][x]
        cell.solution = Rebus.new(v[0])
      end
    end
  end
end
read_gext(e) click to toggle source
# File lib/pangrid/plugins/acrosslite.rb, line 279
def read_gext(e)
  e.grid = e.data.bytes
end
read_grbs(e) click to toggle source
# File lib/pangrid/plugins/acrosslite.rb, line 287
def read_grbs(e)
  e.grid = e.data.bytes.map {|b| b == 0 ? 0 : b - 1 }
end
read_ltim(e) click to toggle source
# File lib/pangrid/plugins/acrosslite.rb, line 252
def read_ltim(e)
  m = e.data.match /^(\d+),(\d+)\0$/
  check("Could not read extension LTIM") { m }
  e.elapsed = m[1].to_i
  e.stopped = m[2] == "1"
end
read_rtbl(e) click to toggle source
# File lib/pangrid/plugins/acrosslite.rb, line 263
def read_rtbl(e)
  rx = /(([\d ]\d):(\w+);)/
  m = e.data.match /^#{rx}*\0$/
  check("Could not read extension RTBL") { m }
  e.rebus = {}
  e.data.scan(rx).each {|_, k, v|
    e.rebus[k.to_i] = [v, '-']
  }
end
text_checksum(seed) click to toggle source

checksums

# File lib/pangrid/plugins/acrosslite.rb, line 307
def text_checksum(seed)
  c = Checksum.new(seed)
  c.add_string_0 xw.title
  c.add_string_0 xw.author
  c.add_string_0 xw.copyright
  xw.clues.each {|cl| c.add_string cl}
  if (xw.version == '1.3')
    c.add_string_0 xw.notes
  end
  c.sum
end
unpack_clues() click to toggle source

sort incoming clues in xw.clues -> across and down

# File lib/pangrid/plugins/acrosslite.rb, line 194
def unpack_clues
  across, down = xw.number
  clues = across.map {|x| [x, :a]} + down.map {|x| [x, :d]}
  clues.sort!
  xw.across_clues = []
  xw.down_clues = []
  clues.zip(xw.clues).each do |(n, dir), clue|
    if dir == :a
      xw.across_clues << clue
    else
      xw.down_clues << clue
    end
  end
end
write_extensions() click to toggle source
# File lib/pangrid/plugins/acrosslite.rb, line 295
def write_extensions
  xw.extensions.map {|e|
    e.data = self.send(:"write_#{e.section.downcase}", e)
    e.len = e.data.length
    e.data += "\0"
    e.checksum = Checksum.of_string(e.data)
    [e.section, e.len, e.checksum].pack(EXT_HEADER_FORMAT) +
      e.data
  }.join
end
write_gext(e) click to toggle source
# File lib/pangrid/plugins/acrosslite.rb, line 283
def write_gext(e)
  e.grid.map(&:chr).join
end
write_grbs(e) click to toggle source
# File lib/pangrid/plugins/acrosslite.rb, line 291
def write_grbs(e)
  e.grid.map {|x| x == 0 ? 0 : x + 1}.map(&:chr).join
end
write_ltim(e) click to toggle source
# File lib/pangrid/plugins/acrosslite.rb, line 259
def write_ltim(e)
  e.elapsed.to_s + "," + (e.stopped ? "1" : "0") + "\0"
end
write_rtbl(e) click to toggle source
# File lib/pangrid/plugins/acrosslite.rb, line 273
def write_rtbl(e)
  e.rebus.keys.sort.map {|x|
    x.to_s.rjust(2) + ":" + e.rebus[x][0] + ";"
  }.join
end