view nono.py @ 74:0245bc07f16c simple

try to simplify Rows/Cols (do without?) by hiving off the run-counts copletely
author Henry Thompson <ht@markup.co.uk>
date Sat, 09 Aug 2025 15:59:19 -0400
parents 63e87db2bc31
children
line wrap: on
line source

#!/usr/bin/python3
# New version, input taken from Nonograms Outer HTML plus
#  > fgrep 'task = ' nono10.xhtml|tr ';' '\n' | fgrep task\ = | sed 's/^.*= //'| tr -d \'
#  E.g. 8/5.1/2.3/2.1/4.3/3.4/2/1.1/3/5.2/2.2/2.2/1.3.2/5.1/3.1.1/2/2.3.1/7.1/1.1.2/3.2

#######  New Plan #######
# All that matters is runs of incomplete cells.  We begin with n+n runs, the maximum number is
#  sum(len(rn) for rn in rows U cols)
# My own strategy seems to be FIFO for pass 0, then LIFO, for the queue of new/changed runs
# By cases, wrt types of change of a cell in its runs
# Change at the margin:
#  New fill: complete the run and discard it, kill the other end and queue it
#      dead: advance margin and append a new fill to any internal sequences of fills (always?), queue the new fill(s)
# Change in the middle
#  New dead: If deterministic, split the run in two and requeue the cell (now in two runs)
#      fill: If deterministic single gap at any end, kill, split, and requeue the cell (now in two runs)
# ----------
# Doesn't yet cover everything, e.g. killing gaps that are too small to be filled
# Do we need to actually represent run-internal segments, e.g. skips and bars?

# Hypothesis: we only need Columns and Rows for printing

import sys

Red = ""
eRed = ""
RedFmt = Red + "%s" + eRed


class Run:
  '''What we're looking for, converted eventually into what we've found.
  "left" and "right" make sense for Rows, for Columns read "above" and "below"'''

  def __init__(self, n, i, j):
    # A run of length n, starting within [i,j]
    self.n = n
    self.i = i
    self.j = j
    self.s = j - i
    self.done = False
    self.left = self.right = None

  def leftEdge(self):
    return self.i

  def rightEdge(self):
    if self.done:
      return self.i + self.n - 1
    return self.j + self.n - 1

  def __str__(self):
    return str(self.n)


class Space:
  def __init__(self, n, i, j):
    # A space of length 1..n, starting within [i,j]
    # Once done, length == n
    # i and j don't change, but s, which is used by check0
    #  to decide where to look, is optimistic and may need to
    #  be restricted if there is an overlap with the next (forward or backward)
    #  run (see self.right and self.left, if any, respectively)
    assert n > 0
    self.n = n
    self.i = i
    self.j = j
    self.s = j - i
    self.done = False

  def __str__(self):
    return ""


class Vector(list):
  # reads top-to-bottom or left-to-right
  def __init__(self, m, left, right):
    self.n = n = (left.x - right.x) + 1 # size
    list.__init__(self, list(range(n)))
    self.m = m  # parent NoNo
    self.margin0 = 0  # a run can start here, so if >0 then self[m0-1].val must be False
    self.marginN = n - 1  # a run can end here, so if <n-1 then self[mN+1].val ditto

  def initRuns(self, runs):
    # Set runs to alternating list of Run and Skip
    self.rn = len(runs)
    self.runs = []
    if self.rn == 0:
      # No runs means a vector of x
      for c in self:
        c.setVal(False)
    else:
      need = sum(1 + runs[k] for k in range(self.rn)) - 1
      space = self.n - need
      pos = 0
      while runs:
        self.runs.append(Run(r := runs.pop(0), pos, pos + space))
        pos += r
        if runs:
          self.runs.append(Space(space, pos, pos + space))
          pos += 1
      # Adjacent pairs mutually restrict possible starting points
      for i in range(0, self.rn - 1):
        left = self.runs[2 * i]
        right = self.runs[(2 * i) + 2]
        left.right = right
        right.left = left
    self.initDisp()

  def __str__(self):
    return "%s|" % ("|".join(str(c) for c in self))

  def myPrintSize(self):
    return sum((len(str(run)) if isinstance(run, Run) else 1) for run in self.runs)

  def pass0(self):
    # simple first pass down/across a row
    for i in range(0, len(self.runs), 2):
      run = self.runs[i]
      for p in range(run.i + run.s, run.i + run.n):
        self[p].setVal(True)

  def check0(self):
    # Check 1st and last not-done runs for completeness
    # Have to restrict lookahead/behind if there's overlap between runs...
    # Forwards
    # ****Assumes margin0 is 0***** and marginN is n
    for i in range(0, len(self.runs), 2):
      run = self.runs[i]
      if not run.done:
        # look for first True cell
        for p in range(
          run.i,
          min(
            run.i + run.s + 1,
            run.right.leftEdge() - run.n if (run.right and run.i > run.n) else 100,
          ),
        ):
          if self[p].val:
            if p > 0 and self[p - 1].val == False:
              # We're pinned, force start here from now on
              self.i = p
              self.s = p + 1
              self.right = None  # Maybe?
            l = self.trueRun(p, run.n)
            if l == run.n:
              if p != 0:
                self[p - 1].setVal(False)
              e = p + l
              if e != self.n:
                self[e].setVal(False)
              break
        break
      # and backwards
    m = len(self.runs) - 1
    #    if isinstance(self,Column) and self.x==3:
    #      breakpoint()
    for i in range(0, m + 1, 2):
      run = self.runs[m - i]
      if not run.done:
        # look for first True cell
        for p in range(
          run.i + run.s + (run.n - 1),
          max(
            run.i - 1,
            run.left.rightEdge() + run.n
            if (run.left and run.i < self.n - run.n)
            else 0,
          ),
          -1,
        ):
          if self[p].val:
            if p < self.n - 1 and self[p + 1] == False:
              self.i = p
              self.s = p + 1
              self.left = None  # Maybe?
            l = self.trueRun(p, run.n, -1)
            if l == run.n:
              e = p - l
              if e != 0:
                self[e].setVal(False)
              if p + 1 != self.n:
                self[p + 1].setVal(False)
              break
        break

  def trueRun(self, p, n, delta=1):
    res = 0
    for i in range(p, p + (delta * n), delta):
      if self[i].val:
        res += 1
      else:
        break
    return res


class Row(Vector):
  cLet = "R"

  def __init__(self, m, left, right):
    Vector.__init__(self, left, right, m)
    self.y = left.y

  def __repr__(self):
    return "R@%s%s:%s" % (self.y, self.runs, list.__repr__(self))

  def initDisp(self):
    self.width = self.myPrintSize()

  def __str__(self):
    return (
      self.fmt % (" ".join(str(r) for r in self.runs if isinstance(r, Run)))
    ) + Vector.__str__(self)

  def updateHeader(self, *, maxWidth=None, r=None, pre=None):
    if maxWidth is None:
      # update
      spacer = " " if self.runs else ""
      if pre:
        self.infix += (RedFmt % r) + spacer
      else:
        # post
        self.suffix = spacer + RedFmt % r + self.suffix
      self.fmt = "%s%s%%s%s|" % (self.prespace, self.infix, self.suffix)
    else:
      # init
      self.maxWidth = maxWidth
      self.prespace = " " * (maxWidth - self.width)
      self.fmt = "%s%%s|" % self.prespace
      self.infix = ""
      self.suffix = ""


class Column(Vector):
  cLet = "C"

  def __init__(self, m, top, bottom):
    Vector.__init__(self, top, bottom, m)
    self.x = top.x

  def __repr__(self):
    return "C@%s%s:%s" % (self.x, self.runs, list.__repr__(self))

  def initDisp(self):
    self.height = self.myPrintSize()

  def updateHeader(self, *, maxHeight=None, r=None, pre=None):
    dprint("CuH", r, pre)
    if maxHeight is None:
      # update
      if pre:
        for rc in str(r):
          self.infix.append(RedFmt % rc)
        if self.runs:
          self.infix.append("-")
      else:
        # post
        ins = ["-"] if self.runs else []
        for rc in str(r):
          ins.append(RedFmt % r)
        self.suffix = ins + self.suffix
      dprint(
        "CuH1: |%s|,%s,%s,%s" % (self.prespace, self.infix, self.suffix, self.runs)
      )
      self.header = (
        ([" "] * self.prespace)
        + self.infix
        + (["-".join(str(c) for c in self.runs)] if self.runs else [])
        + self.suffix
      )
    else:
      # init
      self.maxHeight = maxHeight
      self.infix = []
      self.suffix = []
      self.prespace = maxHeight - self.height  # pad to same 'height'
      self.fmt = "%s%%s" % (" " * self.prespace)
      header = "-".join(str(c) for c in self.runs if isinstance(c, Run))
      self.header = self.fmt % header
    dprint(self.header)


class Cell:
  def __init__(self, x, y):
    self.x = x
    self.y = y
    self.val = None  # three valued: None(unknown), True(filled), False(empty)

  def __repr__(self):
    return "c@(%s,%s):%s" % (self.x, self.y, self.val)

  def __str__(self):
    return " " if self.val is None else ("\u25a0" if self.val else "x")

  def setVal(self, v):
    # Returns true iff old value was None
    assert v in (True, False)
    if v == self.val:
      return False
    if self.val is not None:
      eprint(
        "Reset not allowed: %s -> %s at %s,%s" % (self.val, v, self.x, self.y),
        err=103,
      )
    self.val = v
    return True


class Nono(list):
  # 0,0 is upper left, so increasing y goes _downwards_, to match the standard layout
  def __getitem__(self, xy):
    return list.__getitem__(self, xy[1])[xy[0]]

  def __setitem__(self, xy, v):
    list.__getitem__(self, xy[1])[xy[0]] = v

  def __init__(
    self, runsPerRow: list[int], runsPerCol: list[int], debug: bool
  ) -> list[list[Cell]]:
    global SOLVER
    self.loop = 0
    self.dp = ""  # old depth hack
    self.debug = debug
    self.dstate = []
    self.pending = [] # newly filled/dead cells whose runs need to be checkedo
    SOLVER = self
    n = self.n = len(runsPerCol)
    if n != len(runsPerRow):
      print("losing r:%s x c:%s" % (len(runsPerRow), n), sys.stderr)
      exit(1)
    list.__init__(self,list(list(list(range(n)) for _ in range(n))))
    self.rc = runsPerRow
    self.cc = runsPerCol
    # print col nums>9 vertically :-(
    self.rows = [[(self[x,y] := Cell(x,y)) for y in range(n)] for x in range(n)]
    # Need cells in place for the following
    maxCRheight = 0
    for i in range(n):
      self.initRuns([Cell[i,y] for y in range(n)], runsPerRow[i])
      self.initRuns([Cell[x,i] for x in range(n)], runsPerCol[i])
      maxCRheight = max(maxCRheight = max(col.height for col in cc)
    self.maxCRheight = maxCRheight
    for c in cc:
      c.updateHeader(maxHeight=maxCRheight)
    maxRRwidth = max(row.width for row in rr)
    for r in rr:
      r.updateHeader(maxWidth=maxRRwidth)
    self.rowfmt = "%s|%%s|" % (" " * maxRRwidth)

  def __str__(self):
    lines = [
      self.rowfmt
      % ("|".join([(self.columns[i]).header[j] for i in range(self.n)]))  # 'rotate'
      for j in range(self.maxCRheight)
    ]
    lines += [str(r) for r in self.rows]
    return "\n".join(lines)

  def pass0(self):  # do columns of empty layout
    for c in self.columns:
      c.pass0()
    dprint("After Pass 0 for columns", self, sep="\n")
    for r in self.rows:
      r.pass0()
    dprint("After Pass 0 for rows", self, sep="\n")
    for c in self.columns:
      c.check0()
    dprint("After Check 0 for columns", self, sep="\n")
    dprint(self)
    for r in self.rows:
      r.check0()
    dprint("After Check 0 for rows", self, sep="\n")

  def solve(self):
    self.pass0()


def dprint(*args, **kwargs):
  """Debug print"""
  if SOLVER.debug:
    print(*args, **kwargs)
    sys.stdout.flush()


def eprint(*args, **kw):
  """error print"""
  print(*args, file=sys.stderr)
  sys.stderr.flush()
  print(SOLVER)
  breakpoint()
  exit(kw["err"])


if __name__ == "__main__":
  if sys.argv[1] == "-d":
    sys.argv.pop(1)
    debug = True
  else:
    debug = False
  if len(sys.argv) > 1:
    f = open(sys.argv[1])
  else:
    f = sys.stdin

  l = f.readline().rstrip()
  vv = l.split("/")
  n = int(len(vv) / 2)
  print("%sx%s puzzle" % (n, n), file=sys.stderr)
  cols = [[int(i) for i in v.split(".")] for v in vv[:n]]
  rows = [[int(i) for i in v.split(".")] for v in vv[n:]]

  solver = Nono(rows, cols, debug)
  print(solver)
  print()
  solver.solve()
  print("Done", solver, sep="\n")