#!/usr/bin/env python
# $Id: polyhexes.py 600 2015-02-24 20:21:02Z goodger $

# Author: David Goodger <goodger@python.org>
# Copyright: (C) 1998-2015 by David J. Goodger
# License: GPL 2 (see __init__.py)

"""
Polyhex puzzle base classes.
"""

import copy
import math
import collections

from puzzler import coordsys
from puzzler.puzzles import Puzzle2D, OneSidedLowercaseMixin


class Polyhexes(Puzzle2D):

    """
    The shape of the matrix is defined by the `coordinates` generator method.
    The `width` and `height` attributes define the maximum bounds only.
    """

    svg_unit_height = Puzzle2D.svg_unit_length * math.sqrt(3) / 2

    coord_class = coordsys.Hexagonal2D

    def coordinates(self):
        return self.coordinates_parallelogram(self.width, self.height)

    @classmethod
    def coordinates_parallelogram(cls, width, height, offset=None):
        for y in range(height):
            for x in range(width):
                yield cls.coordinate_offset(x, y, offset)

    @classmethod
    def coordinate_offset(cls, x, y, offset):
        if offset:
            return coordsys.Hexagonal2D((x, y)) + offset
        else:
            return coordsys.Hexagonal2D((x, y))

    @classmethod
    def coordinates_staggered_rectangle(cls, width, height, offset=None):
        for x in range(width):
            y_offset = int((width - x - 1) / 2)
            for y in range(height):
                yield cls.coordinate_offset(x, y + y_offset, offset)

    @classmethod
    def coordinates_hexagon(cls, side_length, offset=None):
        bound = side_length * 2 - 1
        min_xy = side_length - 1
        max_xy = 3 * side_length - 3
        for coord in cls.coordinates_parallelogram(bound, bound):
            x, y = coord
            if min_xy <= (x + y) <= max_xy:
                yield cls.coordinate_offset(x, y, offset)

    @classmethod
    def coordinates_elongated_hexagon(cls, base_length, side_length,
                                      offset=None):
        x_bound = side_length + base_length - 1
        y_bound = 2 * side_length - 1
        min_xy = side_length - 1
        max_xy = base_length + 2 * side_length - 3
        for coord in cls.coordinates_parallelogram(x_bound, y_bound):
            x, y = coord
            if min_xy <= (x + y) <= max_xy:
                yield cls.coordinate_offset(x, y, offset)

    @classmethod
    def coordinates_semiregular_hexagon(cls, base_length, side_length,
                                        offset=None):
        bound =  base_length + side_length - 1
        min_xy = side_length - 1
        max_xy = base_length + 2 * side_length - 3
        for coord in cls.coordinates_parallelogram(bound, bound):
            x, y = coord
            if min_xy <= (x + y) <= max_xy:
                yield cls.coordinate_offset(x, y, offset)

    # old name:
    coordinates_semi_regular_hexagon = coordinates_semiregular_hexagon

    @classmethod
    def coordinates_hexagram(cls, side_length, offset=None):
        bound = (side_length - 1) * 4 + 1
        min_x = min_y = side_length - 1
        max_x = max_y = (side_length - 1) * 3
        min_xy = (side_length - 1) * 3
        max_xy = (side_length - 1) * 5
        for coord in cls.coordinates_parallelogram(bound, bound):
            x, y = coord
            xy = x + y
            if (  (min_xy <= xy and y <= max_y and x <= max_x)
                  or (xy <= max_xy and y >= min_y and x >= min_x)):
                yield cls.coordinate_offset(x, y, offset)

    @classmethod
    def coordinates_trapezoid(cls, base_length, side_length, offset=None):
        max_xy = base_length - 1
        for coord in cls.coordinates_parallelogram(base_length, side_length):
            x, y = coord
            if (x + y) <= max_xy:
                yield cls.coordinate_offset(x, y, offset)

    @classmethod
    def coordinates_triangle(cls, side_length, offset=None):
        return cls.coordinates_trapezoid(side_length, side_length, offset)

    @classmethod
    def coordinates_inverted_triangle(cls, side_length, offset=None):
        coords = set(cls.coordinates_parallelogram(
            side_length, side_length, offset=offset))
        coords -= set(cls.coordinates_triangle(side_length - 1, offset=offset))
        return sorted(coords)

    @classmethod
    def coordinates_butterfly(cls, base_length, side_length, offset=None):
        """
        The base_length is actually the figure height (vertical length), and
        the side_length is the length of the four angled sides.
        """
        x_bound = side_length * 2 - 1
        y_bound = base_length + side_length - 1
        min_y = side_length - 1
        max_y = base_length - 1
        min_xy = x_bound - 1
        max_xy = y_bound - 1
        for coord in cls.coordinates_parallelogram(x_bound, y_bound):
            x, y = coord
            xy = x + y
            if (xy >= min_xy or y >= min_y) and (xy <= max_xy or y <= max_y):
                yield cls.coordinate_offset(x, y, offset)

    def make_aspects(self, units, flips=(False, True),
                     rotations=(0, 1, 2, 3, 4, 5)):
        aspects = set()
        if self.implied_0:
            coord_list = ((0, 0),) + units
        else:
            coord_list = tuple(units)
        for flip in flips or (0,):
            for rotation in rotations or (0,):
                aspect = coordsys.Hexagonal2DView(coord_list, rotation, flip)
                aspects.add(aspect)
        return aspects

    def format_solution(self, solution, normalized=True,
                        rotate_180=False, row_reversed=False):
        s_matrix = self.build_solution_matrix(solution)
        if rotate_180:
            s_matrix = [list(reversed(s_matrix[y]))
                        for y in reversed(range(self.height))]
        if row_reversed:
            out = []
            trim = (self.height - 1) // 2
            for y in range(self.height):
                index = self.height - 1 - y
                out.append(([self.empty_cell] * index
                            + s_matrix[index]
                            + [self.empty_cell] * y)[trim:-trim])
            s_matrix = out
        return self.format_hex_grid(s_matrix)

    empty_cell = '  '

    def empty_content(self, cell, x, y):
        return self.empty_cell

    def cell_content(self, cell, x, y):
        return cell

    def format_hex_grid(self, s_matrix, content=None):
        if content is None:
            content = self.empty_content
        width = len(s_matrix[0])
        height = len(s_matrix)
        output = []
        for x in range(width - 1, -1, -1):
            # padding for slanted top row:
            output.append([' ' * (x * 3 + 1)])
        for y in range(height - 1, -1, -1):
            output.append([])
            if s_matrix[y][0] != self.empty_cell:
                # leftmost edge:
                output.append(['\\'])
            else:
                output.append([' '])
            for x in range(width):
                cell = s_matrix[y][x]
                left_wall = right_wall = ' '
                ceiling = self.empty_cell
                if x > 0 and y < (height - 1):
                    if s_matrix[y + 1][x - 1] != cell:
                        left_wall = '/'
                elif cell != self.empty_cell:
                    left_wall = '/'
                if x < (width - 1):
                    if s_matrix[y][x + 1] != cell:
                        right_wall = '\\'
                elif cell != self.empty_cell:
                    right_wall = '\\'
                output[-2 - x].append(
                    left_wall + content(cell, x, y) + right_wall)
                if y < (height - 1):
                    if s_matrix[y + 1][x] != cell:
                        ceiling = '__'
                elif cell != self.empty_cell:
                    ceiling = '__'
                output[-3 - x].append(ceiling)
        for y in range(height - 1, 0, -1):
            if s_matrix[y][-1] != self.empty_cell:
                # rightmost bottom right edges:
                output[-width - 2 * y].append('/')
        for x in range(width):
            if s_matrix[0][x] != self.empty_cell:
                output[-x - 1].append('__/')
        for i in range(len(output)):
            output[i] = ''.join(output[i]).rstrip()
        while not output[-1].strip():
            output.pop()
        while not output[0].strip():
            output.pop(0)
        return '\n'.join(output) + '\n'

    def format_coords(self):
        s_matrix = self.empty_solution_matrix()
        for x, y in self.solution_coords:
            s_matrix[y][x] = '* '
        return self.format_hex_grid(s_matrix)

    def calculate_svg_dimensions(self):
        height = (self.height + 2) * self.svg_unit_height
        width = (self.width + self.height / 2.0 + 2) * self.svg_unit_width
        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.
        """
        name = s_matrix[y][x]
        color = self.piece_colors[name]
        cells = self.get_piece_cells(s_matrix, x, y)
        path_points = self.get_path_points(cells)
        # Erase cells of this piece:
        for x, y in cells:
            s_matrix[y][x] = self.empty_cell
        path_strings = [
            ('M %.3f,%.3f %s Z'
             % (points[0][0], points[0][1],
                ' '.join(('L %.3f,%.3f' % coord) for coord in points[1:])))
            for points in path_points]
        return self.svg_path % {
            'color': color,
            'stroke': self.svg_stroke,
            'stroke_width': self.svg_stroke_width,
            'path_data': ' '.join(path_strings),
            'name': name}

    _sqrt3 = math.sqrt(3)
    corner_offsets = {0: (0.0, 1 / _sqrt3),
                      1: (0.0, 0.0),
                      2: (0.5, -_sqrt3 / 6),
                      3: (1.0, 0.0),
                      4: (1.0, 1 / _sqrt3),
                      5: (0.5, _sqrt3 / 2)}
    """Offset of corners from the lower left-hand corner of hexagon."""

    def get_path_points(self, cells):
        """
        Return a list of paths, each a list of closed path points.

        The first path is the main shape outline, and subsequent subpaths (if
        any) are holes.
        """
        # This version allows for shapes with holes.  It works for polyhexes,
        # but doesn't take into account multiple path choices that would occur
        # for polyiamonds and polyominoes (e.g.  heptomino with a hole).
        segments = set()
        segment_starts = collections.defaultdict(set)
        for cell in cells:
            for segment in self.get_path_segments(cell):
                start, end = segment
                reverse = (end, start)
                if reverse in segments:
                    # two cells are adjacent; segments cancel each other out
                    segments.remove(reverse)
                    segment_starts[end].remove(start)
                else:
                    segments.add(segment)
                    segment_starts[start].add(end)
        # Join the remaining segments into (potentially multiple) paths.
        paths = []
        while segments:
            # arbitrary starting point, should be outermost path:
            segment = min(segments)
            segments.remove(segment)
            start, end = segment
            path = [start, end]
            # keep track of start point, to know when to stop:
            first = start
            while True:
                start = end
                # not true for polyominoes & polyiamonds with holes at edge:
                assert len(segment_starts[start]) == 1
                end = segment_starts[start].pop()
                segment = (start, end)
                segments.remove(segment)
                if end == first:
                    break
                else:
                    path.append(end)
            paths.append(path)
        return paths

    def get_path_segments(self, cell):
        """
        Return a list of (start,end) pairs of coordinate tuples for edge
        segments of the cell at (x,y), counterclockwise.
        """
        x, y = cell
        unit = self.svg_unit_length
        yunit = self.svg_unit_height
        height = (self.height + 2) * yunit
        base_x = (x + (y - 1) / 2.0) * unit
        base_y = height - y * yunit
        corners = len(self.corner_offsets)
        segments = []
        start = None
        for i in range(corners + 1):
            end = (
                round(base_x + self.corner_offsets[i % corners][0] * unit, 6),
                round(base_y - self.corner_offsets[i % corners][1] * unit, 6))
            if start:
                segments.append((start, end))
            start = end
        return segments


class Monohex(Polyhexes):

    piece_data = {'H1': ((), {})}
    """(0,0) is implied."""

    symmetric_pieces = piece_data.keys() # all of them

    asymmetric_pieces = []

    piece_colors = {'H1': 'gray'}


class Dihex(Polyhexes):

    piece_data = {'I2': ((( 1, 0),), {})}
    """(0,0) is implied."""

    symmetric_pieces = piece_data.keys() # all of them

    asymmetric_pieces = []

    piece_colors = {'I2': 'steelblue'}


class Trihexes(Polyhexes):

    piece_data = {
        'I3': ((( 1, 0), ( 2, 0)), {}),
        'V3': ((( 1, 0), ( 1, 1)), {}),
        'A3': ((( 1, 0), ( 0, 1)), {}),}
    """(0,0) is implied."""

    symmetric_pieces = piece_data.keys() # all of them

    asymmetric_pieces = []

    piece_colors = {
        'I3': 'teal',
        'V3': 'plum',
        'A3': 'olive',
        '0': 'gray',
        '1': 'black'}


class Tetrahexes(Polyhexes):

    piece_data = {
        'I4': ((( 1, 0), ( 2, 0), ( 3, 0)), {}),
        'J4': ((( 1, 0), ( 2, 0), ( 2, 1)), {}),
        'P4': ((( 1, 0), ( 2, 0), ( 1, 1)), {}),
        'S4': ((( 1, 0), ( 1, 1), ( 2, 1)), {}),
        'U4': (((-1, 1), ( 1, 0), ( 1, 1)), {}),
        'Y4': ((( 1, 0), ( 2,-1), ( 1, 1)), {}),
        'O4': ((( 1, 0), ( 0, 1), ( 1, 1)), {}),}
    """(0,0) is implied."""

    symmetric_pieces = 'I4 O4 U4 Y4'.split()
    """Pieces with reflexive symmetry, identical to their mirror images."""

    asymmetric_pieces = 'J4 P4 S4'.split()
    """Pieces without reflexive symmetry, different from their mirror images."""

    piece_colors = {
        'I4': 'blue',
        'O4': 'red',
        'Y4': 'green',
        'U4': 'lime',
        'J4': 'blueviolet',
        'P4': 'gold',
        'S4': 'navy',
        '0': 'gray',
        '1': 'black'}


class Polyhexes34(Tetrahexes):

    piece_data = copy.deepcopy(Tetrahexes.piece_data)
    piece_data.update(copy.deepcopy(Trihexes.piece_data))

    symmetric_pieces = (
        Trihexes.symmetric_pieces + Tetrahexes.symmetric_pieces)

    asymmetric_pieces = (
        Trihexes.asymmetric_pieces + Tetrahexes.asymmetric_pieces)

    piece_colors = copy.deepcopy(Tetrahexes.piece_colors)
    piece_colors.update(Trihexes.piece_colors)


class OneSidedPolyhexes34(OneSidedLowercaseMixin, Polyhexes34):

    pass


class Polyhexes1234(Polyhexes34):

    piece_data = copy.deepcopy(Polyhexes34.piece_data)
    piece_data.update(copy.deepcopy(Monohex.piece_data))
    piece_data.update(copy.deepcopy(Dihex.piece_data))

    symmetric_pieces = (
        Monohex.symmetric_pieces + Dihex.symmetric_pieces
        + Polyhexes34.symmetric_pieces)

    asymmetric_pieces = (
        Monohex.asymmetric_pieces + Dihex.asymmetric_pieces
        + Polyhexes34.asymmetric_pieces)

    piece_colors = copy.deepcopy(Polyhexes34.piece_colors)
    piece_colors.update(Monohex.piece_colors)
    piece_colors.update(Dihex.piece_colors)


class OneSidedPolyhexes1234(OneSidedLowercaseMixin, Polyhexes1234):

    pass


class Pentahexes(Polyhexes):

    piece_data = {
        'A5': ((( 1, 0), ( 2, 0), ( 1, 1), ( 2,-1)), {}),
        'B5': ((( 1, 0), ( 2, 0), ( 1, 1), ( 2, 1)), {}),
        'C5': ((( 1, 0), ( 2, 0), ( 2, 1), (-1, 1)), {}),
        'D5': ((( 1, 0), ( 2, 0), ( 0, 1), ( 1, 1)), {}),
        'E5': ((( 1, 0), ( 1, 1), ( 2, 0), ( 3, 0)), {}),
        'F5': ((( 1, 0), ( 2, 0), ( 0, 1), ( 2, 1)), {}),
        'G5': ((( 1, 0), ( 1, 1), ( 2, 1), ( 3, 0)), {}),
        'H5': ((( 1, 0), ( 1, 1), ( 0, 2), ( 2, 1)), {}),
        'I5': ((( 1, 0), ( 2, 0), ( 3, 0), ( 4, 0)), {}),
        'J5': ((( 1, 0), ( 2, 0), ( 3, 0), ( 3, 1)), {}),
        'L5': ((( 1, 0), ( 2, 0), ( 2, 1), ( 2, 2)), {}),
        'N5': ((( 1, 0), ( 2, 0), ( 2, 1), ( 3, 1)), {}),
        'P5': ((( 1, 0), ( 2, 0), ( 2, 1), ( 3, 0)), {}),
        'Q5': ((( 1, 0), ( 2, 0), ( 2, 1), ( 1,-1)), {}),
        'R5': ((( 1, 0), ( 2, 0), ( 2, 1), ( 1, 2)), {}),
        'S5': ((( 1, 0), ( 2, 0), ( 2, 1), ( 0,-1)), {}),
        'T5': ((( 1, 0), ( 2, 0), ( 2, 1), ( 2,-1)), {}),
        'U5': ((( 1, 0), ( 1, 1), (-1, 1), (-1, 2)), {}),
        'V5': ((( 1, 0), ( 2, 0), ( 0, 1), ( 0, 2)), {}),
        'W5': ((( 1, 0), ( 1, 1), ( 2, 1), ( 2, 2)), {}),
        'X5': ((( 1, 0), ( 2, 0), ( 0, 1), ( 2,-1)), {}),
        'Y5': ((( 1, 0), ( 2, 0), ( 2, 1), ( 3,-1)), {}),}
    """(0,0) is implied."""

    symmetric_pieces = 'I5 E5 L5 C5 Y5 D5 X5 A5 V5 U5 W5'.split()
    """Pieces with reflexive symmetry, identical to their mirror images."""

    asymmetric_pieces = 'J5 P5 N5 R5 B5 F5 S5 Q5 T5 H5 G5'.split()
    """Pieces without reflexive symmetry, different from their mirror images."""

    piece_colors = {
        'A5': 'maroon',
        'B5': 'steelblue',
        'C5': 'lime',
        'D5': 'green',
        'E5': 'magenta',
        'F5': 'lightcoral',
        'G5': 'indigo',
        'H5': 'tan',
        'I5': 'blue',
        'J5': 'darkseagreen',
        'L5': 'darkorange',
        'N5': 'plum',
        'P5': 'peru',
        'Q5': 'olive',
        'R5': 'yellow',
        'S5': 'gray',
        'T5': 'teal',
        'U5': 'navy',
        'V5': 'blueviolet',
        'W5': 'gold',
        'X5': 'red',
        'Y5': 'turquoise',
        '0': 'gray',
        '1': 'black'}


class OneSidedPentahexes(OneSidedLowercaseMixin, Pentahexes):

    pass


class Polyhexes12345(Polyhexes1234, Pentahexes):

    piece_data = copy.deepcopy(Pentahexes.piece_data)
    piece_data.update(copy.deepcopy(Polyhexes1234.piece_data))

    symmetric_pieces = (
        Polyhexes1234.symmetric_pieces + Pentahexes.symmetric_pieces)

    asymmetric_pieces = (
        Polyhexes1234.asymmetric_pieces + Pentahexes.asymmetric_pieces)

    piece_colors = copy.deepcopy(Pentahexes.piece_colors)
    piece_colors.update(Polyhexes1234.piece_colors)


class OneSidedPolyhexes12345(OneSidedLowercaseMixin, Polyhexes12345):

    pass


class Hexahexes(Polyhexes):

    implied_0 = False

    piece_data = {
        'A06': (((0, 0), (0, 1), (0, 2), (1, 0), (1, 1), (2, 0)), {}),
        'A16': (((0, 0), (0, 1), (0, 2), (1, 1), (1, 2), (2, 0)), {}),
        'A26': (((0, 0), (0, 1), (0, 2), (1, 1), (2, 0), (2, 1)), {}),
        'C06': (((0, 1), (0, 2), (1, 0), (1, 1), (1, 2), (2, 0)), {}),
        'C16': (((0, 0), (0, 1), (1, 1), (2, 1), (3, 1), (4, 0)), {}),
        'C26': (((0, 1), (0, 2), (0, 3), (1, 0), (1, 3), (2, 0)), {}),
        'C36': (((0, 0), (0, 1), (0, 2), (1, 0), (1, 2), (2, 1)), {}),
        'C46': (((0, 0), (0, 1), (0, 2), (1, 0), (1, 2), (2, 0)), {}),
        'C56': (((0, 0), (0, 1), (0, 2), (0, 3), (1, 0), (1, 3)), {}),
        'C66': (((0, 0), (0, 1), (0, 2), (1, 2), (2, 2), (3, 1)), {}),
        'C76': (((0, 1), (0, 2), (1, 0), (1, 2), (1, 3), (2, 0)), {}),
        'E06': (((0, 0), (0, 1), (0, 2), (1, 1), (1, 2), (2, 2)), {}),
        'F06': (((0, 0), (0, 1), (0, 2), (0, 3), (1, 1), (1, 3)), {}),
        'F16': (((0, 0), (0, 1), (1, 1), (1, 2), (2, 1), (3, 1)), {}),
        'H06': (((0, 0), (0, 1), (0, 2), (0, 3), (1, 2), (2, 2)), {}),
        'H16': (((0, 0), (0, 1), (1, 1), (1, 2), (1, 3), (2, 0)), {}),
        'H26': (((0, 0), (0, 1), (1, 1), (1, 2), (1, 3), (2, 1)), {}),
        'I06': (((0, 0), (0, 1), (0, 2), (0, 3), (0, 4), (0, 5)), {}),
        'J06': (((0, 0), (0, 1), (0, 2), (0, 3), (1, 3), (2, 2)), {}),
        'J16': (((0, 0), (0, 1), (0, 2), (1, 2), (2, 0), (2, 1)), {}),
        'J26': (((0, 0), (0, 1), (0, 2), (1, 0), (1, 2), (2, 2)), {}),
        'J36': (((0, 0), (0, 1), (1, 1), (1, 3), (2, 1), (2, 2)), {}),
        'J46': (((0, 1), (0, 2), (0, 3), (1, 0), (1, 2), (2, 2)), {}),
        'K06': (((0, 1), (0, 2), (1, 1), (2, 1), (2, 2), (3, 0)), {}),
        'L06': (((0, 0), (0, 1), (0, 2), (0, 3), (1, 3), (2, 3)), {}),
        'L16': (((0, 0), (0, 1), (0, 2), (0, 3), (0, 4), (1, 4)), {}),
        'L26': (((0, 0), (0, 1), (0, 2), (1, 2), (2, 1), (3, 1)), {}),
        'L36': (((0, 0), (0, 1), (0, 2), (1, 1), (2, 1), (3, 0)), {}),
        'M06': (((0, 0), (0, 1), (1, 1), (1, 2), (1, 3), (2, 2)), {}),
        'M16': (((0, 0), (0, 1), (0, 3), (1, 1), (1, 2), (2, 2)), {}),
        'M26': (((0, 0), (0, 1), (0, 2), (1, 0), (1, 1), (2, 1)), {}),
        'M36': (((0, 0), (0, 1), (0, 2), (1, 0), (1, 1), (1, 2)), {}),
        'M46': (((0, 0), (0, 1), (1, 1), (1, 2), (2, 2), (2, 3)), {}),
        'N06': (((0, 0), (0, 1), (0, 2), (1, 2), (1, 3), (1, 4)), {}),
        'N16': (((0, 0), (0, 1), (0, 2), (0, 3), (1, 3), (1, 4)), {}),
        'O06': (((0, 1), (0, 2), (1, 0), (1, 2), (2, 0), (2, 1)), {}),
        'P06': (((0, 0), (0, 1), (0, 2), (0, 3), (1, 0), (1, 1)), {}),
        'P16': (((0, 0), (0, 1), (0, 2), (0, 4), (1, 2), (1, 3)), {}),
        'P26': (((0, 0), (0, 1), (0, 2), (0, 3), (0, 4), (1, 0)), {}),
        'P36': (((0, 0), (0, 1), (0, 4), (1, 1), (1, 2), (1, 3)), {}),
        'P46': (((0, 0), (0, 1), (0, 2), (1, 1), (2, 0), (3, 0)), {}),
        'P56': (((0, 0), (0, 1), (0, 2), (0, 3), (1, 2), (1, 3)), {}),
        'P66': (((0, 0), (0, 1), (0, 2), (0, 3), (0, 4), (1, 1)), {}),
        'P76': (((0, 0), (0, 1), (0, 2), (1, 0), (1, 2), (1, 3)), {}),
        'Q06': (((0, 0), (0, 1), (1, 1), (2, 1), (2, 2), (3, 1)), {}),
        'Q16': (((0, 0), (0, 1), (0, 2), (1, 2), (1, 3), (2, 2)), {}),
        'Q26': (((0, 1), (0, 2), (1, 0), (1, 2), (1, 3), (2, 2)), {}),
        'Q36': (((0, 0), (0, 1), (1, 0), (1, 1), (1, 2), (2, 2)), {}),
        'R06': (((0, 0), (0, 1), (0, 3), (1, 1), (1, 2), (2, 0)), {}),
        'R16': (((0, 1), (0, 2), (1, 0), (1, 2), (1, 3), (2, 1)), {}),
        'S06': (((0, 0), (0, 1), (1, 1), (2, 0), (3, 0), (3, 1)), {}),
        'S16': (((0, 0), (0, 1), (1, 1), (2, 1), (3, 1), (3, 2)), {}),
        'S26': (((0, 0), (0, 1), (0, 2), (1, 1), (1, 2), (1, 3)), {}),
        'S36': (((0, 0), (0, 1), (0, 2), (1, 2), (2, 2), (2, 3)), {}),
        'T06': (((0, 0), (0, 1), (1, 1), (1, 2), (2, 0), (2, 2)), {}),
        'T16': (((0, 1), (0, 2), (1, 0), (1, 1), (2, 1), (3, 1)), {}),
        'T26': (((0, 1), (0, 2), (1, 2), (1, 3), (2, 1), (3, 0)), {}),
        'T36': (((0, 0), (0, 1), (0, 3), (1, 0), (1, 1), (1, 2)), {}),
        'T46': (((0, 0), (0, 1), (0, 2), (1, 1), (2, 1), (3, 1)), {}),
        'T56': (((0, 0), (0, 1), (1, 0), (1, 1), (1, 2), (2, 1)), {}),
        'T66': (((0, 0), (0, 1), (1, 1), (1, 2), (2, 0), (2, 1)), {}),
        'T76': (((0, 1), (0, 2), (1, 1), (1, 2), (1, 3), (2, 0)), {}),
        'U06': (((0, 0), (0, 1), (0, 3), (0, 4), (1, 1), (1, 2)), {}),
        'U16': (((0, 0), (0, 1), (0, 2), (0, 3), (1, 0), (1, 2)), {}),
        'U26': (((0, 0), (0, 1), (0, 2), (1, 1), (2, 1), (2, 2)), {}),
        'V06': (((0, 0), (0, 1), (0, 2), (1, 2), (2, 1), (3, 0)), {}),
        'V16': (((0, 0), (0, 1), (0, 2), (0, 3), (1, 0), (2, 0)), {}),
        'W06': (((0, 0), (0, 1), (1, 1), (1, 2), (2, 2), (3, 1)), {}),
        'W16': (((0, 0), (0, 1), (0, 2), (1, 2), (1, 3), (2, 3)), {}),
        'W26': (((0, 0), (0, 1), (0, 2), (1, 1), (1, 2), (2, 1)), {}),
        'W36': (((0, 0), (0, 1), (1, 1), (1, 2), (1, 3), (2, 3)), {}),
        'X06': (((0, 1), (0, 3), (1, 1), (1, 2), (2, 0), (2, 2)), {}),
        'X16': (((0, 1), (1, 1), (1, 2), (2, 0), (2, 1), (3, 1)), {}),
        'X26': (((0, 1), (0, 2), (1, 1), (2, 0), (2, 1), (3, 1)), {}),
        'Y06': (((0, 1), (0, 2), (1, 2), (1, 3), (2, 0), (2, 1)), {}),
        'Y16': (((0, 1), (1, 1), (2, 1), (2, 2), (2, 3), (3, 0)), {}),
        'Y26': (((0, 1), (1, 1), (1, 2), (1, 3), (1, 4), (2, 0)), {}),
        'Y36': (((0, 0), (0, 1), (0, 2), (0, 3), (1, 1), (2, 0)), {}),
        'Y46': (((0, 0), (0, 1), (1, 1), (2, 1), (2, 2), (3, 0)), {}),
        'Y56': (((0, 0), (0, 1), (0, 2), (1, 2), (1, 3), (2, 1)), {}),
        'Y66': (((0, 1), (1, 1), (1, 2), (1, 3), (2, 1), (3, 0)), {}),
        'Z06': (((0, 1), (0, 2), (1, 1), (2, 1), (3, 0), (3, 1)), {}),
        }
    """(0,0) is NOT implied."""

    symmetric_pieces = (
        'A06 C06 C16 E06 I06 O06 T06 T16 T56 U06 U16 V06 X06 X16 Y06 Y16 Y26'
        .split())
    """Pieces with reflexive symmetry, identical to their mirror images."""

    asymmetric_pieces = (
        'A16 A26 C26 C36 C46 C56 C66 C76 F06 F16 H06 H16 H26 J06 J16 J26 '
        'J36 J46 K06 L06 L16 L26 L36 M06 M16 M26 M36 M46 N06 N16 P06 P16 '
        'P26 P36 P46 P56 P66 P76 Q06 Q16 Q26 Q36 R06 R16 S06 S16 S26 S36 '
        'T26 T36 T46 T66 T76 U26 V16 W06 W16 W26 W36 X26 Y36 Y46 Y56 Y66 Z06'
        .split())
    """Pieces without reflexive symmetry, different from their mirror images."""

    piece_colors = {
        # a selection of colors, repeating:
        'A06': 'maroon',
        'A16': 'steelblue',
        'A26': 'lime',
        'C06': 'green',
        'C16': 'magenta',
        'C26': 'indigo',
        'C36': 'blue',
        'C46': 'darkseagreen',
        'C56': 'darkorange',
        'C66': 'plum',
        'C76': 'peru',
        'E06': 'olive',
        'F06': 'teal',
        'F16': 'navy',
        'H06': 'blueviolet',
        'H16': 'red',
        'H26': 'turquoise',
        'I06': 'maroon',
        'J06': 'steelblue',
        'J16': 'lime',
        'J26': 'green',
        'J36': 'magenta',
        'J46': 'indigo',
        'K06': 'blue',
        'L06': 'darkseagreen',
        'L16': 'darkorange',
        'L26': 'plum',
        'L36': 'peru',
        'M06': 'olive',
        'M16': 'teal',
        'M26': 'navy',
        'M36': 'blueviolet',
        'M46': 'red',
        'N06': 'turquoise',
        'N16': 'maroon',
        'O06': 'steelblue',
        'P06': 'lime',
        'P16': 'green',
        'P26': 'magenta',
        'P36': 'indigo',
        'P46': 'blue',
        'P56': 'darkseagreen',
        'P66': 'darkorange',
        'P76': 'plum',
        'Q06': 'peru',
        'Q16': 'olive',
        'Q26': 'teal',
        'Q36': 'navy',
        'R06': 'blueviolet',
        'R16': 'red',
        'S06': 'turquoise',
        'S16': 'maroon',
        'S26': 'steelblue',
        'S36': 'lime',
        'T06': 'green',
        'T16': 'magenta',
        'T26': 'indigo',
        'T36': 'blue',
        'T46': 'darkseagreen',
        'T56': 'darkorange',
        'T66': 'plum',
        'T76': 'peru',
        'U06': 'olive',
        'U16': 'teal',
        'U26': 'navy',
        'V06': 'blueviolet',
        'V16': 'red',
        'W06': 'turquoise',
        'W16': 'maroon',
        'W26': 'steelblue',
        'W36': 'lime',
        'X06': 'green',
        'X16': 'magenta',
        'X26': 'indigo',
        'Y06': 'blue',
        'Y16': 'darkseagreen',
        'Y26': 'darkorange',
        'Y36': 'plum',
        'Y46': 'peru',
        'Y56': 'olive',
        'Y66': 'teal',
        'Z06': 'navy',}


class OneSidedHexahexes(OneSidedLowercaseMixin, Hexahexes):

    pass


class Polyhexes123456(Polyhexes12345, Hexahexes):

    piece_data = copy.deepcopy(Hexahexes.piece_data)
    piece_data.update(copy.deepcopy(Polyhexes12345.piece_data))

    symmetric_pieces = (
        Polyhexes12345.symmetric_pieces + Hexahexes.symmetric_pieces)

    asymmetric_pieces = (
        Polyhexes12345.asymmetric_pieces + Hexahexes.asymmetric_pieces)

    piece_colors = copy.deepcopy(Hexahexes.piece_colors)
    piece_colors.update(Polyhexes12345.piece_colors)


class OneSidedPolyhexes123456(OneSidedLowercaseMixin, Polyhexes123456):

    pass