tzot

Combinatorial puzzle solver

May 12th, 2020
218
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 10.95 KB | None | 0 0
  1. #!/usr/local/bin/python
  2.  
  3. import re, itertools as it, sys
  4.  
  5. __all__= "LogicPuzzle",
  6.  
  7. class NotYet(Exception):
  8.     "Raised when a rule cannot be tested because some of its items are not yet in the matrix"
  9.     pass
  10.    
  11. class PyzzleSyntaxError(SyntaxError):
  12.     "Raised when there is a syntax error in the description file"
  13.     pass
  14.    
  15. class _Item(object):
  16.     __slots__= 'name', 'column', 'category', 'row', 'value', 'rules'
  17.     reIsNumeric= re.compile('^_\d+$')
  18.    
  19.     def __init__(self, category, name, value=None):
  20.         self.category= category
  21.         self.name= name
  22.         ## self.row= self.column= None
  23.         if value is None and self.reIsNumeric.match(name):
  24.             self.value= int(name[1:])
  25.             ## print name, self.value
  26.         elif value is not None:
  27.             self.value= value
  28.             # deliberately we do not set the value if not provided with one
  29.            
  30.         self.rules= []
  31.        
  32.     def __eq__(self, other):
  33.         "True when operands are in the same column"
  34.         return self.row == other.row
  35.        
  36.     def __ne__(self, other):
  37.         "True when operands are on different columns"
  38.         return self.row != other.row
  39.        
  40.     def __str__(self):
  41.         return self.name
  42.        
  43.     def _get_unknown(self):
  44.         return not hasattr(self, 'row')
  45.     unknown= property(_get_unknown)
  46.    
  47. class _Rule(object):
  48.     __slots__= 'source', 'code', 'namespace',
  49.    
  50.     # here let's make the program a little version independent
  51.     # what is reported when we don't index a list with an integer?
  52.     _alist= [1]
  53.     try:
  54.         _alist['hello']
  55.     except TypeError, exc:
  56.         LIST_INDEX_TYPE_ERROR_ARGS= exc.args
  57.     del _alist
  58.     TYPE_ERROR_ARGS= LIST_INDEX_TYPE_ERROR_ARGS,
  59.    
  60.     def __init__(self, source, namespace):
  61.         self.source= source
  62.         self.code= compile(source, "", "eval")
  63.         self.namespace= namespace
  64.        
  65.     def __nonzero__(self):
  66.         try:
  67.             return eval(self.code, self.namespace)
  68.         except AttributeError, exc:
  69.             if exc.args[0] in ('row','value'):
  70.                 raise NotYet
  71.             else:
  72.                 raise AttributeError, (self.source,) + exc.args
  73.         except NameError, exc:
  74.             raise PyzzleSyntaxError, "rule '%s': %s" % (self.source, exc.args)
  75.            
  76. class _Category(object):
  77.     "A category of items in the puzzle"
  78.     __slots__= 'name', 'items', 'max_width', 'null_item'
  79.     # name: the name of the category
  80.     # items: list of _Item
  81.     # max_width: used to pretty print the result
  82.     reItem= re.compile("(?P<item>\w+)\s*(?:=\s*(?P<value>\d+))?")
  83.    
  84.     def __init__(self, name, items_as_string, custom_item_class):
  85.         self.name= name
  86.         self.max_width= len(name)
  87.         self.items= []
  88.         items= self.reItem.findall(items_as_string)
  89.         self.null_item= custom_item_class(name, '%s' % name)
  90.         ## print repr(items)
  91.         for item_name, item_value in items:
  92.             self.max_width= max(self.max_width, len(item_name))
  93.             if item_value=='':
  94.                 item_value= None
  95.             else:
  96.                 item_value= int(item_value)
  97.             self.items.append(custom_item_class(self.name, item_name, item_value))
  98.            
  99.     def __iter__(self):
  100.         for item in self.items:
  101.             if item.unknown:
  102.                 yield item
  103.                
  104. class LogicPuzzle(object):
  105.     "A logic puzzle as described in a list of strings (file.xreadlines comes to mind :)"
  106.     __slots__= 'categories', 'matrix', 'items', 'category_column', 'rules', 'empty_item'
  107.    
  108.     def __init__(self, text_lines=None):
  109.         if text_lines is not None:
  110.             self.from_lines(text_lines)
  111.            
  112.     def _reset(self):
  113.         "Initialise the data"
  114.         self.categories= []
  115.         self.matrix= []
  116.         self.rules= []
  117.         self.items= {'__builtins__': {}} # security precaution
  118.        
  119.     def from_lines(self, text_lines):
  120.         "Read the puzzle description from a list of lines (or a text file object)"
  121.         reCategory= re.compile("^(?P<category>\w+)\s*:\s*(?P<items>(?:\w+\s*(?:=\s*\d+\s*)?,?\s*)+)$")
  122.         self._reset()
  123.         reading_stage= "categories"
  124.         line_count= 0
  125.         categories= []
  126.         for text in text_lines:
  127.             text= text.strip()
  128.             line_count+= 1
  129.             if not text or text.startswith("#"): continue # skip over empty lines and comments
  130.             if reading_stage=="categories":
  131.                 if text.startswith("--"): # we reached the end of categories
  132.                     reading_stage= "rules"
  133.                     item_class, self.category_column= self._create_custom_item_class(categories, self.matrix)
  134.                     self._setup_categories(categories, item_class)
  135.                     continue
  136.                 match= reCategory.match(text) # it MUST be a category
  137.                 if match is None: # oops!
  138.                     raise PyzzleSyntaxError, "line %d, '%s' is not a category line" % (line_count, text)
  139.                 categories.append( (match.group("category"), match.group("items")) )
  140.             elif reading_stage=="rules":
  141.                 new_rule= _Rule(text, self.items)
  142.                 self.rules.append(new_rule)
  143.                 # FIXME rules hung on specific items here
  144.                 for name in new_rule.code.co_names:
  145.                     if name in self.items:
  146.                         self.items[name].rules.append(new_rule)
  147.             else: # normally we never come here
  148.                 raise RuntimeError, "line %d: invalid reading_stage '%s'" % (line_count, reading_stage)
  149.                
  150.     def _create_custom_item_class(self, categories, matrix):
  151.         "internal: return a custom Item class for this puzzle and a category to column index map"
  152.         category_columns= {}
  153.         def _make_item_lookup_through_category(category_index, matrix):
  154.             def _how_can_i_name_this(self):
  155.                 return matrix[self.row][category_index].value
  156.             return _how_can_i_name_this
  157.         for column, category_stuff in enumerate(categories):
  158.             category_name, category_items= category_stuff
  159.             category_columns[category_name]= column
  160.         class CustomItem(_Item):
  161.             pass
  162.         for category_column, category_stuff in enumerate(categories):
  163.             category_name, category_items= category_stuff
  164.             setattr(CustomItem, category_name,
  165.             property(_make_item_lookup_through_category(category_column, matrix)))
  166.         return CustomItem, category_columns
  167.        
  168.     def _setup_categories(self, categories, item_class):
  169.         count_of_items= None
  170.         for category_name, category_items in categories:
  171.             new_category= _Category(category_name, category_items, item_class)
  172.            
  173.             # assure all categories have as many items as the first
  174.             if count_of_items is None: # this is the first category
  175.                 count_of_items= len(new_category.items)
  176.             else:
  177.                 if len(new_category.items)!=count_of_items:
  178.                     raise PyzzleSyntaxError, "category %s has %d items instead of %d" % \
  179.                     (category_name, len(new_category.items), count_of_items)
  180.                    
  181.             self.categories.append(new_category)
  182.             for item in new_category.items:
  183.                 self.items[item.name]= item
  184.                
  185.     def printit(self):
  186.         format= ' '.join(["%2d"] + ["%%-%ds" % category.max_width for category in self.categories])
  187.         print format % tuple([0] + [category.name for category in self.categories])
  188.         for row in xrange(len(self.categories[0].items)):
  189.             print format % tuple([row+1] + self.matrix[row])
  190.         print
  191.        
  192.     def solve(self):
  193.         matrix= self.matrix
  194.         del matrix[:] # clean the data
  195.         row_count= len(self.categories[0].items)
  196.         column_count= len(self.categories)
  197.         stack= []
  198.         for row in xrange(row_count):
  199.             self.matrix.append([])
  200.             for column, category in enumerate(self.categories):
  201.                 if column==0:
  202.                     item= category.items[row]
  203.                     self.matrix[row].append(item) # first column does not change
  204.                     item.row= row
  205.                     item.column= 0
  206.                 else:
  207.                     self.matrix[row].append(category.null_item)
  208.         def recurse(row, column):
  209.         ## try:
  210.         ## if self.rules[0]:
  211.         ## self.printit()
  212.         ## print "here we are!"
  213.         ## print '\n'.join([rule.source for rule in self.rules])
  214.         ## raw_input()
  215.         ## except NotYet:
  216.         ## pass
  217.             if column>=column_count:
  218.                 recurse(row+1, 1)
  219.                 return
  220.             if row>=row_count:
  221.             ## _= self.items['LeadPiping']
  222.             ## print dir(_)
  223.             ## print "name", _.name
  224.             ## print "value", _.value
  225.             ## print "row", _.row
  226.             ## print "column", _.column
  227.             ## print _.DiscoveryDate
  228.             ## self.printit()
  229.             ## print self.items['LeadPiping'].DiscoveryDate
  230.             ## print self.items['RobertTrench'].DiscoveryDate
  231.             ## sys.exit(0)
  232.             # check the rules
  233.                 all_rules_ok= True
  234.                 for rule in self.rules:
  235.                     try:
  236.                         if not rule:
  237.                         ## print "failed:", rule.source, " "
  238.                         ## raw_input()
  239.                             all_rules_ok= False
  240.                             break
  241.                     except NotYet: # here ???
  242.                         print rule.source, self.items.keys()
  243.                         for name in rule.code.co_names:
  244.                             if name in self.items:
  245.                                 print name, "=", self.items[name].row, self.items[name].column
  246.                         sys.exit(1)
  247.                 if all_rules_ok:
  248.                     self.printit()
  249.                     print "******** solution! ************"
  250.                     print '\n'.join(['%s: %s' % (rule.source, bool(rule)) for rule in self.rules])
  251.                 return
  252.             for item in self.categories[column]:
  253.                 matrix[row][column]= item
  254.                 item.row, item.column= row, column
  255.                 for rule in item.rules: # item.rules
  256.                     try:
  257.                         if not rule:
  258.                         ## self.printit()
  259.                         ## print rule.source, " "
  260.                         ## raw_input()
  261.                             break
  262.                     except NotYet:
  263.                         pass
  264.                 else:
  265.                     recurse(row, column+1)
  266.                 del item.row, item.column
  267.                 matrix[row][column]= self.categories[column].null_item
  268.         recurse(0, 1)
  269.        
  270. if __name__=='__main__':
  271.     p = LogicPuzzle(file(sys.argv[1]))
  272.     p.solve()
Add Comment
Please, Sign In to add comment