#!/usr/bin/env python # -*- coding: utf-8 -*- # $Id: __init__.py 660 2019-10-03 18:14:50Z goodger $ # Author: David Goodger <goodger@python.org> # Copyright: (C) 1998-2015 by David J. Goodger # License: # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 # as published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, refer to # http://puzzler.sourceforge.net/GPL2.txt or write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA, USA 02111-1307 import sys import copy import datetime import re from pprint import pprint, pformat from puzzler import coordsys from puzzler import colors class DataError(RuntimeError): pass class Puzzle(object): """ Abstract base class for puzzles. """ height = 0 width = 0 depth = 0 check_for_duplicates = False duplicate_conditions = () """A list of dictionaries of default-value keyword arguments to `format_solution`, to generate all solution permutations.""" secondary_columns = 0 empty_cell = ' ' margin = 1 piece_data = {} """Mapping of piece names to 2-tuples (a copy.deepcopy is made first): * list of unit coordinates (usually one unit, the origin, is implicit) * dictionary of aspect restrictions, keyword arguments to `make_aspects`; customized in `customize_piece_data` """ implied_0 = True piece_colors = None """Mapping of piece names to colors. The '0' name is reserved for formatting solution coordinates.""" svg_header = '''\ <?xml version="1.0" standalone="no"?> <!-- Created by Polyform Puzzler (http://puzzler.sourceforge.net/) --> <svg width="%(width).3f" height="%(height).3f" viewBox="0 0 %(width).3f %(height).3f" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> ''' svg_footer = '</svg>\n' svg_g_start = '<g>\n' svg_g_start_with_transform = ( '<g transform="%(extra)stranslate(%(dx).3f,%(dy).3f)' ' rotate(%(angle)s)">\n') svg_flip_matrix = 'matrix(1,0,0,-1,0,%(dy).3f)' svg_g_end = '</g>\n' svg_polygon = '''\ <polygon fill="%(color)s" stroke="%(stroke)s" stroke-width="%(stroke_width)s" points="%(points)s"> <desc>%(name)s</desc> </polygon> ''' svg_path = '''\ <path fill="%(color)s" stroke="%(stroke)s" stroke-width="%(stroke_width)s" d="%(path_data)s"> <desc>%(name)s</desc> </path> ''' svg_stroke = 'white' """Polygon outline color.""" svg_stroke_width = '1' """Width of polygon outline.""" svg_unit_length = 10 """Unit side length in pixels.""" svg_unit_width = svg_unit_length """Unit width in pixels.""" svg_unit_height = svg_unit_length """Unit height in pixels.""" svg_rotation = 0 """Default figure rotation, in degrees clockwise.""" svg_flip = False """Default figure flip (about X axis).""" x3d_header = '''\ <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE X3D PUBLIC "ISO//Web3D//DTD X3D 3.0//EN" "http://www.web3d.org/specifications/x3d-3.0.dtd"> <X3D profile="Immersive" version="2.0"> <Scene> ''' x3d_footer = '</Scene>\n</X3D>\n' @classmethod def components(cls): """Return a tuple of puzzle component classes (sub-puzzles).""" return (cls,) def __init__(self, init_puzzle=True): """ Use `init_puzzle` to speed up initialization when not actually solving the puzzle. """ self.solutions = set() """Set of all permutations of solutions, for duplicate checking.""" self.solution_coords = set(self.coordinates()) """A set of all coordinates that make up the solution area/space.""" self.aspects = {} """Mapping of piece name to a set of aspects (pieces in all orientations).""" self.pieces = {} """Mapping of piece name to a sorted list of 2-tuples: sorted coordinates & aspect objects. Ensures reproducible results.""" self.x_width = len(str(self.width - 1)) """Maximum width of string representation of X coordinate.""" self.y_width = len(str(self.height - 1)) """Maximum width of string representation of Y coordinate.""" self.z_width = len(str(self.depth - 1)) """Maximum width of string representation of Z coordinate.""" self.matrix = [] """A list of lists; see the ExactCover.load_matrix() method of puzzler.exact_cover_dlx or puzzler.exact_cover_x2.""" self.matrix_columns = {} """Mapping of `self.matrix` column names to indices.""" # Make an object-local deep copy of the dict: self.piece_data = copy.deepcopy(self.piece_data) # Now we can modify it as we like: self.customize_piece_data() if init_puzzle: self.init_puzzle() def init_puzzle(self): """Initialize the puzzle pieces and matrix.""" self.build_aspects() self.build_matrix_header() self.build_matrix() def coordinates(self): """ A generator that yields all the coordinates of the solution space. Implement in subclasses. """ raise NotImplementedError def customize_piece_data(self): """ Make instance-specific customizations to copies of `self.piece_data` and `self.piece_colors`. Override in subclasses. """ pass def build_aspects(self): """Populate `self.aspects` and `self.pieces`.""" self.build_regular_aspects(sorted(self.piece_data.keys())) def build_regular_aspects(self, names): """ Populate `self.aspects` from the puzzle pieces listed in `names`, and populate `self.pieces` from aspects in `self.aspects`. """ for name in names: data, kwargs = self.piece_data[name] self.aspects[name] = self.make_aspects(data, **kwargs) for name, aspects in self.aspects.items(): self.pieces[name] = tuple( sorted((tuple(sorted(aspect)), aspect) for aspect in aspects)) def make_aspects(self, data, **kwargs): """ Return a set of aspects (rotations & flips) of a puzzle piece described by `data`. Puzzle-specific parameters control the range of aspect orientations. Implement in subclasses. """ raise NotImplementedError def matrix_header_pieces(self): """Return an ordered list of piece names for build_matrix_header.""" return sorted(self.pieces.keys()) def matrix_header_coords(self): """ Return an ordered list of coordinates for build_matrix_header. Override e.g. to ensure that secondary columns are at the end. """ return sorted(self.solution_coords) def build_matrix_header(self): """ Create and populate the first row of `self.matrix`, a list of column names. Implement in subclasses. """ raise NotImplementedError def build_matrix(self): """ Create and populate the data rows of `self.matrix`, lists of 0's and 1's (or other true values). """ self.build_regular_matrix(sorted(self.pieces.keys())) def build_regular_matrix(self, keys, solution_coords=None): """ Build `self.matrix` rows from puzzle pieces listed in `keys`. Implement in subclasses. """ raise NotImplementedError def build_restricted_matrix(self): """ Builds `self.matrix` based first on a set of restrictions. Once the restrictions are exhausted, builds the rest of the matrix the normal way. Requires a `self.restrictions` attribute, mapping piece names to ``[(aspect index, offset), ...]``. """ keys = sorted(self.pieces.keys()) for key, details in sorted(self.restrictions.items()): for aspect_index, offset in details: coords, aspect = self.pieces[key][aspect_index] translated = aspect.translate(offset) self.build_matrix_row(key, translated) keys.remove(key) self.build_regular_matrix(keys) def record_solution(self, solution, solver, stream=sys.stdout, dated=False): """ Output a formatted solution to `stream`. Return True for valid solution. """ formatted = self.format_solution(solution, normalized=True) if self.check_for_duplicates: if self.store_solutions(solution, formatted): return False if dated: print >>stream, 'at %s,' % datetime.datetime.now(), print >>stream, solver.format_solution() print >>stream print >>stream, self.format_solution(solution, normalized=False) print >>stream stream.flush() return True def record_dated_solution(self, solution, solver, stream=sys.stdout): """A dated variant of `self.record_solution`.""" self.record_solution(solution, solver, stream=stream, dated=True) def format_solution(self, solution, normalized=True): """ Return a puzzle-specific formatting of a solution. Implement in subclasses. * `solution` is a list of pieces. Each piece is a list of column names from the exact cover matrix: cooridinates and the piece name. * `normalized` is a flag; if set, the solution is formatted in a way that allows for easy duplicate detection (such as renaming identical pieces). """ raise NotImplementedError def format_svg(self, solution=None, s_matrix=None, thin=False): """ Return a puzzle-specific SVG formatting of a solution. Implement in subclasses. """ raise NotImplementedError def write_svg(self, output_path, solution=None, s_matrix=None, thin=False): try: if thin: svg = self.format_thin_svg(solution, s_matrix) else: svg = self.format_svg(solution, s_matrix) except NotImplementedError: print >>sys.stderr, ( 'Warning: SVG output not supported by this puzzle.\n') else: svg_file = None try: if output_path == '-': svg_file = sys.stdout elif hasattr(output_path, 'write'): svg_file = output_path else: svg_file = open(output_path, 'w') svg_file.write(svg) finally: if ( svg_file is not None and not ((output_path == '-') or hasattr(output_path, 'write'))): svg_file.close() def format_x3d(self, solution=None, s_matrix=None): """ Return a puzzle-specific X3D formatting of a solution. Implement in subclasses. """ raise NotImplementedError def write_x3d(self, output_path, solution=None, s_matrix=None): try: x3d = self.format_x3d(solution, s_matrix) except NotImplementedError: print >>sys.stderr, ( 'Warning: X3D output not supported by this puzzle.\n') else: x3d_file = None try: if output_path == '-': x3d_file = sys.stdout elif hasattr(output_path, 'write'): x3d_file = output_path else: x3d_file = open(output_path, 'w') x3d_file.write(x3d) finally: if ( x3d_file is not None and not ((output_path == '-') or hasattr(output_path, 'write'))): x3d_file.close() solution_header = re.compile(r'^solution (\d+)( .+)?:$', re.IGNORECASE) def read_solution(self, input_path, solution_number): if input_path == '-': input_file = sys.stdin elif hasattr(input_path, 'readline'): input_file = input_path else: input_file = open(input_path, 'rU') try: for line in input_file: match = self.solution_header.match(line) if match: if solution_number: number = int(match.group(1)) if number == solution_number: break else: break else: raise DataError('Input does not contain a solution record.') record = [] for line in input_file: line = line.strip() if not line: break record.append(line) finally: input_file.close() s_matrix = self.convert_record_to_solution_matrix(record) return s_matrix def convert_record_to_solution_matrix(self, record): """ `record` is a list of strings, the solution record. Implement in subclasses """ raise NotImplementedError def store_solutions(self, solution, formatted): """ Return True if the solution is a duplicate, False if unique. Store the formatted solution along with puzzle-specific variants (reflections, rotations) in `self.solutions`, to check for duplicates. """ if formatted in self.solutions: return True self.solutions.add(formatted) # keep track of formatted variants of *this* solution, to # differentiate from previous solutions: solutions = set([formatted]) for conditions in self.duplicate_conditions: formatted = self.format_solution(solution, **conditions) if formatted in self.solutions: # applying duplicate conditions may result in variant # solutions identical to `formatted` if formatted not in solutions: return True else: self.solutions.add(formatted) solutions.add(formatted) return False class Puzzle2D(Puzzle): duplicate_conditions = ({'x_reversed': True}, {'y_reversed': True}, {'x_reversed': True, 'y_reversed': True}) def coordinates(self): return self.coordinates_rectangle(self.width, self.height) @classmethod def coordinate_offset(cls, x, y, offset): if offset: return coordsys.Cartesian2D((x, y)) + offset else: return coordsys.Cartesian2D((x, y)) @classmethod def coordinates_rectangle(cls, width, height, offset=None): for y in range(height): for x in range(width): yield cls.coordinate_offset(x, y, offset) @classmethod def coordinates_triangle(cls, side_length, offset=None): for coord in cls.coordinates_rectangle(side_length, side_length): x, y = coord xy = x + y if xy < side_length: yield cls.coordinate_offset(x, y, offset) @classmethod def coordinates_double_triangle(cls, side_length, offset=None): width = side_length * 2 - 1 for coord in cls.coordinates_rectangle(width, side_length): x, y = coord if (y <= x) and (y < (width - x)): yield cls.coordinate_offset(x, y, offset) @classmethod def coordinates_diamond(cls, side_length, offset=None): bound = 2 * side_length - 1 min_xy = side_length - 1 max_xy = 3 * (side_length - 1) max_x_y = side_length - 1 min_x_y = - max_x_y for coord in cls.coordinates_rectangle(bound, bound): x, y = coord xy = x + y x_y = x - y if (min_xy <= xy <= max_xy) and (min_x_y <= x_y <= max_x_y): yield cls.coordinate_offset(x, y, offset) @classmethod def coordinates_aztec_diamond(cls, side_length, offset=None): bound = 2 * side_length min_xy = side_length - 1 max_xy = 3 * side_length - 1 max_x_y = side_length min_x_y = - side_length for coord in cls.coordinates_rectangle(bound, bound): x, y = coord xy = x + y x_y = x - y if (min_xy <= xy <= max_xy) and (min_x_y <= x_y <= max_x_y): yield cls.coordinate_offset(x, y, offset) def make_aspects(self, units, flips=(False, True), rotations=(0, 1, 2, 3)): aspects = set() if self.implied_0: coord_list = ((0, 0),) + units else: coord_list = units for flip in flips or (0,): for rotation in rotations or (0,): aspect = coordsys.Cartesian2DView(coord_list, rotation, flip) aspects.add(aspect) return aspects def build_matrix_header(self): headers = [] for i, key in enumerate(self.matrix_header_pieces()): self.matrix_columns[key] = i headers.append(key) for (x, y) in self.matrix_header_coords(): header = '%0*i,%0*i' % (self.x_width, x, self.y_width, y) self.matrix_columns[header] = len(headers) headers.append(header) self.matrix.append(tuple(headers)) def build_regular_matrix(self, keys, solution_coords=None): if solution_coords is None: solution_coords = self.solution_coords for key in keys: for coords, aspect in self.pieces[key]: for y in range(self.height - aspect.bounds[1]): for x in range(self.width - aspect.bounds[0]): translated = aspect.translate((x, y)) if translated.issubset(solution_coords): self.build_matrix_row(key, translated) def build_matrix_row(self, name, coords): row = [0] * len(self.matrix[0]) row[self.matrix_columns[name]] = name for coord in coords: label = '%0*i,%0*i' % (self.x_width, coord[0], self.y_width, coord[1]) row[self.matrix_columns[label]] = label self.matrix.append(tuple(row)) def format_solution(self, solution, normalized=True, x_reversed=False, y_reversed=False): s_matrix = self.build_solution_matrix(solution) formatted = self.format_solution_matrix( s_matrix, x_reversed, y_reversed) omitted = '\n'.join( '(%s omitted)' % row[-1] for row in solution if row[0] == '!') if omitted: return '\n'.join([omitted, formatted]) else: return formatted def format_solution_matrix(self, s_matrix, x_reversed=False, y_reversed=False): order_functions = (lambda x: x, reversed) x_reversed_fn = order_functions[x_reversed] y_reversed_fn = order_functions[1 - y_reversed] # reversed by default formatted = '\n'.join( ''.join('%-*s' % (self.piece_width, name) for name in x_reversed_fn(s_matrix[y])).rstrip() for y in y_reversed_fn(range(self.height))) return formatted def empty_solution_matrix(self, margin=0): s_matrix = [[self.empty_cell] * (self.width + 2 * margin) for y in range(self.height + 2 * margin)] return s_matrix def build_solution_matrix(self, solution, margin=0): s_matrix = self.empty_solution_matrix(margin) for row in solution: name = row[-1] for cell_name in row[:-1]: if cell_name.endswith('i') or cell_name == '!': continue # skip intersections & omitted pieces x, y = [int(d.strip()) for d in cell_name.split(',')] s_matrix[y + margin][x + margin] = name return s_matrix def format_svg(self, solution=None, s_matrix=None): if s_matrix: assert solution is None, ( 'Provide only one of solution & s_matrix arguments, not both.') else: s_matrix = self.build_solution_matrix(solution, margin=1) shapes = self.format_svg_shapes(s_matrix) height, width = self.calculate_svg_dimensions() if self.svg_flip: g_flip_matrix = self.svg_flip_matrix % {'dy': height} + ' ' else: g_flip_matrix = '' if self.svg_rotation: max_dim = max(height, width) g_start = self.svg_g_start_with_transform % { 'extra': g_flip_matrix, 'dx': max_dim, 'dy': max_dim * (0.5 - self.svg_flip), 'angle': self.svg_rotation} height = width = max_dim * 2 else: if g_flip_matrix: g_start = self.svg_g_start_with_transform % { 'extra': g_flip_matrix, 'dx': 0.0, 'dy': 0.0, 'angle': 0} else: g_start = self.svg_g_start header = self.svg_header % {'height': height, 'width': width} return '%s%s%s%s%s' % ( header, g_start, ''.join(shapes), self.svg_g_end, self.svg_footer) def format_svg_shapes(self, s_matrix): shapes = [] for y in range(1, self.height + 1): for x in range(1, self.width + 1): if s_matrix[y][x] == self.empty_cell: continue shapes.append(self.build_svg_shape(s_matrix, x, y)) return shapes def calculate_svg_dimensions(self): height = (self.height + 2) * self.svg_unit_length width = (self.width + 2) * self.svg_unit_length return height, width def build_svg_shape(self, s_matrix, x, y): """ Return an SVG shape definition for the shape at (x,y), and erase the shape from s_matrix. """ points = self.get_polygon_points(s_matrix, x, y) name = s_matrix[y][x] color = self.piece_colors[name] # Erase cells of this piece: for x, y in self.get_piece_cells(s_matrix, x, y): s_matrix[y][x] = self.empty_cell points_str = ' '.join('%.3f,%.3f' % (x, y) for (x, y) in points) return self.svg_polygon % {'color': color, 'stroke': self.svg_stroke, 'stroke_width': self.svg_stroke_width, 'points': points_str, 'name': name} edge_trace = {(+1, 0): ((( 0, -1), ( 0, -1)), # right (( 0, 0), (+1, 0)), (None, ( 0, +1))), (-1, 0): (((-1, 0), ( 0, +1)), # left ((-1, -1), (-1, 0)), (None, ( 0, -1))), ( 0, +1): ((( 0, 0), (+1, 0)), # up ((-1, 0), ( 0, +1)), (None, (-1, 0))), ( 0, -1): (((-1, -1), (-1, 0)), # down (( 0, -1), ( 0, -1)), (None, (+1, 0))),} """Mapping of counterclockwise (x,y)-direction vector to list (ordered by test) of 2-tuples: examination cell coordinate delta & new direction vector.""" # !!! This will not work for shapes with holes. # See polyhexes.py for another approach. def get_polygon_points(self, s_matrix, x, y): """ Return a list of coordinate tuples, the corner points of the polygon for the piece at (x,y). """ cell_content = s_matrix[y][x] unit = self.svg_unit_length height = (self.height + 2) * unit points = [(x * unit, height - y * unit)] direction = (+1, 0) # to the right start = (x, y) x += 1 while (x, y) != start: for delta, new_direction in self.edge_trace[direction]: if ( delta is None or cell_content != '0' and s_matrix[y + delta[1]][x + delta[0]] == cell_content): break if new_direction != direction: direction = new_direction points.append((x * unit, height - y * unit)) x += direction[0] y += direction[1] return points def get_piece_cells(self, s_matrix, x, y): cell_content = s_matrix[y][x] coord = self.coord_class((x, y)) cells = set([coord]) if cell_content != '0': self._get_piece_cells(cells, coord, s_matrix, cell_content) return cells def _get_piece_cells(self, cells, coord, s_matrix, cell_content): for neighbor in coord.neighbors(): x, y = neighbor if neighbor not in cells and s_matrix[y][x] == cell_content: cells.add(neighbor) self._get_piece_cells(cells, neighbor, s_matrix, cell_content) def format_coords_svg(self, piece_name='0'): s_matrix = self.empty_solution_matrix(margin=self.margin) for x, y in self.solution_coords: s_matrix[y + self.margin][x + self.margin] = piece_name return self.format_svg(s_matrix=s_matrix) def convert_record_to_solution_matrix(self, record): s_matrix = self.empty_solution_matrix(self.margin) for row in record: parts = row.split() name = parts[-1] for coords in parts[:-1]: if coords.endswith('i') or coords == '!': continue # skip intersections & omitted pieces x, y = coords.split(',') s_matrix[int(y) + self.margin][int(x) + self.margin] = name return s_matrix class Puzzle3D(Puzzle): duplicate_conditions = () margin = 0 piece_width = 2 # for format_solution svg_x_width = 9 svg_x_height = -2 svg_y_height = 10 svg_z_height = -3 svg_z_width = -6 svg_stroke_width = '0.5' svg_defs_start = '<defs>\n' svg_cube_def = '''\ <symbol id="cube%(name)s"> <polygon fill="%(color)s" stroke="%(stroke)s" stroke-width="%(stroke_width)s" stroke-linejoin="round" points="0,13 9,15 15,12 15,2 6,0 0,3" /> <polygon fill="black" fill-opacity="0.25" stroke="%(stroke)s" stroke-width="%(stroke_width)s" stroke-linejoin="round" points="9,15 15,12 15,2 9,5" /> <polygon fill="white" fill-opacity="0.30" stroke="%(stroke)s" stroke-width="%(stroke_width)s" stroke-linejoin="round" points="0,3 9,5 15,2 6,0" /> </symbol> ''' svg_defs_end = '</defs>\n' svg_cube = '<use xlink:href="#cube%(name)s" x="%(x).3f" y="%(y).3f" />\n' x3d_box = '''\ <Transform translation="%(x)s %(y)s %(z)s"> <Shape> <Appearance> <Material diffuseColor="%(color)s"/> </Appearance> <Box size="1 1 1"/> </Shape> </Transform> ''' def coordinates(self): return self.coordinates_cuboid(self.width, self.height, self.depth) @classmethod def coordinate_offset(cls, x, y, z, offset): if offset: return coordsys.Cartesian3D((x, y, z)) + offset else: return coordsys.Cartesian3D((x, y, z)) @classmethod def coordinates_cuboid(cls, width, height, depth, offset=None): for z in range(depth): for y in range(height): for x in range(width): yield cls.coordinate_offset(x, y, z, offset) @classmethod def coordinates_aztec_pyramid(cls, side_length, offset=None): coords = set() for z in range(side_length): layer = Puzzle2D.coordinates_aztec_diamond( (side_length - z), offset=(z, z)) for x, y in layer: coords.add(cls.coordinate_offset(x, y, z, offset)) return sorted(coords) @classmethod def coordinates_open_box(cls, width, height, depth, offset=None): if offset: offset1 = tuple(dim + 1 for dim in offset) else: offset1 = (1,1,1) coords = ( set(cls.coordinates_cuboid(width, height, depth, offset=offset)) - set(cls.coordinates_cuboid(width-2, height-2, depth-1, offset=offset1))) return sorted(coords) @classmethod def coordinates_ring_wall(cls, width, height, depth, offset=None): offset1 = (1,1,0) if offset: offset1 = tuple(dim + offset1[i] for (i, dim) in enumerate(offset)) coords = ( set(cls.coordinates_cuboid(width, height, depth, offset=offset)) - set(cls.coordinates_cuboid(width-2, height-2, depth, offset=offset1))) return sorted(coords) @classmethod def coordinates_triangular_prism(cls, side_length, depth, offset=None): for coord in cls.coordinates_cuboid(side_length, side_length, depth): x, y, z = coord xy = x + y if xy < side_length: yield cls.coordinate_offset(x, y, z, offset) def make_aspects(self, units, flips=(0, 1), axes=(0, 1, 2), rotations=(0, 1, 2, 3)): aspects = set() if self.implied_0: coord_list = ((0, 0, 0),) + units else: coord_list = units for axis in axes or (2,): coord_set = coordsys.Cartesian3DView(coord_list) if axis != 2: coord_set = coord_set.rotate0(1, (1 - axis) % 3) coords = tuple(coord_set) for flip in flips or (0,): for rotation in rotations or (0,): aspect = coordsys.Cartesian3DView( coords, rotation, axis, flip) aspects.add(aspect) return aspects def build_matrix_header(self): headers = [] for i, key in enumerate(self.matrix_header_pieces()): self.matrix_columns[key] = i headers.append(key) for (x, y, z) in self.matrix_header_coords(): header = '%0*i,%0*i,%0*i' % ( self.x_width, x, self.y_width, y, self.z_width, z) self.matrix_columns[header] = len(headers) headers.append(header) self.matrix.append(tuple(headers)) def build_regular_matrix(self, keys, solution_coords=None): if solution_coords is None: solution_coords = self.solution_coords for key in keys: for coords, aspect in self.pieces[key]: for z in range(self.depth - aspect.bounds[2]): for y in range(self.height - aspect.bounds[1]): for x in range(self.width - aspect.bounds[0]): translated = aspect.translate((x, y, z)) if translated.issubset(solution_coords): self.build_matrix_row(key, translated) def build_matrix_row(self, name, coords): row = [0] * len(self.matrix[0]) row[self.matrix_columns[name]] = name for coord in coords: label = '%0*i,%0*i,%0*i' % (self.x_width, coord[0], self.y_width, coord[1], self.z_width, coord[2]) row[self.matrix_columns[label]] = label self.matrix.append(tuple(row)) def format_solution(self, solution, normalized=True, x_reversed=False, y_reversed=False, z_reversed=False, xy_swapped=False, xz_swapped=False, yz_swapped=False): order_functions = (lambda x: x, reversed) x_reversed_fn = order_functions[x_reversed] y_reversed_fn = order_functions[1 - y_reversed] # reversed by default z_reversed_fn = order_functions[z_reversed] #s_matrix = self.build_solution_matrix(solution) s_matrix = self.empty_solution_matrix() import pdb for row in solution: name = row[-1] for cell_name in row[:-1]: try: x, y, z = (int(d.strip()) for d in cell_name.split(',')) except: pdb.set_trace() if xy_swapped: x, y = y, x if xz_swapped: x, z = z, x if yz_swapped: y, z = z, y s_matrix[z][y][x] = name return '\n'.join( ' '.join(''.join('%-*s' % (self.piece_width, name) for name in x_reversed_fn(s_matrix[z][y])) for z in z_reversed_fn(range(self.depth))).rstrip() for y in y_reversed_fn(range(self.height))) def empty_solution_matrix(self, margin=0): s_matrix = [[[self.empty_cell] * (self.width + 2 * margin) for y in range(self.height + 2 * margin)] for z in range(self.depth + 2 * margin)] return s_matrix def build_solution_matrix(self, solution, margin=0): s_matrix = self.empty_solution_matrix(margin) for row in solution: name = row[-1] for cell_name in row[:-1]: x, y, z = [int(d.strip()) for d in cell_name.split(',')] s_matrix[z + margin][y + margin][x + margin] = name return s_matrix def format_svg(self, solution=None, s_matrix=None): if s_matrix: assert solution is None, ('Provide only one of solution ' '& s_matrix arguments, not both.') else: s_matrix = self.build_solution_matrix(solution) s_matrix = self.transform_solution_matrix(s_matrix) s_depth = len(s_matrix) s_height = len(s_matrix[0]) s_width = len(s_matrix[0][0]) height = (s_height * abs(self.svg_y_height) + s_depth * abs(self.svg_z_height) + s_width * abs(self.svg_x_height) + 2 * self.svg_unit_length) width = (s_width * abs(self.svg_x_width) + s_depth * abs(self.svg_z_width) + 2 * self.svg_unit_length) cube_defs = [] for name in sorted(self.piece_colors.keys()): color = self.piece_colors[name] cube_defs.append( self.svg_cube_def % {'color': color, 'stroke': self.svg_stroke, 'stroke_width': self.svg_stroke_width, 'name': name}) cubes = [] for z in range(s_depth): for y in range(s_height): for x in range(s_width): name = s_matrix[z][y][x] if name == self.empty_cell: continue cubes.append( self.svg_cube % {'name': name, 'x': (x * self.svg_x_width + (z + 1 - s_depth) * self.svg_z_width + self.svg_unit_length), 'y': (height - (y * self.svg_y_height + (z - s_depth) * self.svg_z_height + (x - s_width) * self.svg_x_height + 2 * self.svg_unit_length))}) header = self.svg_header % {'height': height, 'width': width} defs = '%s%s%s' % (self.svg_defs_start, ''.join(cube_defs), self.svg_defs_end) return '%s%s%s%s%s%s' % (header, defs, self.svg_g_start, ''.join(cubes), self.svg_g_end, self.svg_footer) def format_coords_svg(self, piece_name='0'): s_matrix = self.empty_solution_matrix(margin=self.margin) for x, y, z in self.solution_coords: s_matrix[z + self.margin][y + self.margin][ x + self.margin] = piece_name return self.format_svg(s_matrix=s_matrix) def format_x3d(self, solution=None, s_matrix=None): if s_matrix: assert solution is None, ('Provide only one of solution ' '& s_matrix arguments, not both.') else: s_matrix = self.build_solution_matrix(solution) s_matrix = self.transform_solution_matrix(s_matrix) s_depth = len(s_matrix) s_height = len(s_matrix[0]) s_width = len(s_matrix[0][0]) cubes = [] for z in range(s_depth): for y in range(s_height): for x in range(s_width): name = s_matrix[z][y][x] if name == self.empty_cell: continue cubes.append( self.x3d_box % {'name': name, 'x': x, 'y': y, 'z': z, 'color': colors.x3d[self.piece_colors[name]]}) return '%s%s%s' % (self.x3d_header, ''.join(cubes), self.x3d_footer) def transform_solution_matrix(self, s_matrix): """Transform for rendering `s_matrix`. Override in subclasses.""" return s_matrix def swap_yz_transform(self, s_matrix): """Common solution matrix transform: X, Z, reversed(Y).""" return [[[s_matrix[z][y][x] for x in range(self.width)] for z in range(self.depth)] for y in reversed(range(self.height))] def cycle_xyz_transform(self, s_matrix): """Common solution matrix transform: cycle X Y & Z dimensions.""" return [[[s_matrix[z][y][x] for y in range(self.height)] for z in range(self.depth)] for x in range(self.width)] def convert_record_to_solution_matrix(self, record): s_matrix = self.empty_solution_matrix(self.margin) for row in record: parts = row.split() name = parts[-1] for coords in parts[:-1]: if coords.endswith('i') or coords == '!': continue # skip intersections & omitted pieces x, y, z = (int(coord) + self.margin for coord in coords.split(',')) s_matrix[z][y][x] = name return s_matrix class PuzzlePseudo3D(Puzzle3D, Puzzle2D): """The Z dimension is used for direction/orientation.""" def build_regular_matrix(self, keys, solution_coords=None): if solution_coords is None: solution_coords = self.solution_coords for key in keys: for coords, aspect in self.pieces[key]: for x in range(self.width - aspect.bounds[0]): for y in range(self.height - aspect.bounds[1]): translated = aspect.translate((x, y, 0)) if translated.issubset(solution_coords): self.build_matrix_row(key, translated) def empty_solution_matrix(self, margin=0): s_matrix = [[[self.empty_cell] * (self.width + 2 * margin) for y in range(self.height + 2 * margin)] for z in range(self.depth)] return s_matrix def build_solution_matrix(self, solution, margin=0): s_matrix = self.empty_solution_matrix(margin) for row in solution: name = row[-1] for cell_name in row[:-1]: x, y, z = [int(d.strip()) for d in cell_name.split(',')] s_matrix[z][y + margin][x + margin] = name return s_matrix def format_coords_svg(self, piece_name='0'): s_matrix = self.empty_solution_matrix(margin=self.margin) for x, y, z in self.solution_coords: s_matrix[z][y + self.margin][x + self.margin] = piece_name return self.format_svg(s_matrix=s_matrix) format_svg = Puzzle2D.format_svg def format_x3d(self, solution=None, s_matrix=None): raise NotImplementedError class OneSidedLowercaseMixin(object): """ Must be the first base class listed in client subclass definitions for MRO (method resolution order) to work. """ def customize_piece_data(self): """ Disable flips on all pieces, and add flipped versions of asymmetric pieces with lowercase names. """ self.piece_colors = copy.deepcopy(self.piece_colors) for key in self.piece_data.keys(): self.piece_data[key][-1]['flips'] = None for key in self.asymmetric_pieces: new_key = key.lower() self.piece_data[new_key] = copy.deepcopy(self.piece_data[key]) self.piece_data[new_key][-1]['flips'] = (1,) self.piece_colors[new_key] = self.piece_colors[key] if __name__ == '__main__': from puzzler.puzzles.pentominoes import Pentominoes6x10 p = Pentominoes6x10() print 'matrix length =', len(p.matrix) print 'first 20 rows:' pprint(p.matrix[:20], width=720)