فهرست منبع

refactoring ok, tests ok for grids and geometries

olivier.massot 8 سال پیش
والد
کامیت
f658b305b4
8فایلهای تغییر یافته به همراه549 افزوده شده و 586 حذف شده
  1. 2 1
      .gitignore
  2. 7 0
      nose2.cfg
  3. 0 144
      pypog/Piece.py
  4. 169 147
      pypog/geometry_objects.py
  5. 122 0
      pypog/grid_objects.py
  6. 2 2
      pypog/painting.py
  7. 152 208
      tests/test_geometry.py
  8. 95 84
      tests/test_grid.py

+ 2 - 1
.gitignore

@@ -1,4 +1,5 @@
 *.pyc
 .project
 .pydevproject
-bac_a_sable.py
+htmlcov/
+.coverage

+ 7 - 0
nose2.cfg

@@ -0,0 +1,7 @@
+[unittest]
+with-coverage = True
+
+[coverage]
+always-on = True
+coverage = pypog
+coverage-report = html

+ 0 - 144
pypog/Piece.py

@@ -1,144 +0,0 @@
-'''
-Created on 6 mars 2017
-
-@author: olinox
-'''
-
-class Piece(object):
-    def __init__(self):
-        super(Piece, self).__init__()
-        self._position = ()
-        self._rotation = 0
-        self._height = 1
-        self._zR = 0
-        self._cell_shape = None
-        self._shape = [ (0, 0) ]
-
-    # properties
-    @property
-    def position(self):
-        """return the (x, y) position of the Piece"""
-        return self._position
-
-    @position.setter
-    def position(self, position):
-        """update the (x, y) position of the Piece
-        x, y have to be integers"""
-        try:
-            _, _ = position
-            if not all([isinstance(var, int) for var in position]):
-                raise ValueError("'position' have to be an (x, y) tuple, with x and y integers")
-        except ValueError:
-            raise ValueError("'position' have to be an (x, y) tuple, with x and y integers")
-        self._position = position
-
-    @property
-    def x(self):
-        """x coordinate of the Piece"""
-        return self._position[0]
-
-    @property
-    def y(self):
-        """y coordinate of the Piece"""
-        return self._position[1]
-
-    @property
-    def height(self):
-        """height (in cells) of the Piece"""
-        return self._height
-
-    @height.setter
-    def height(self, height):
-        """set the height (in cells) of the Piece
-        'height' has to be a positive integer"""
-        if not isinstance(height, int):
-            raise TypeError("'height' has to be an integer")
-        if not height >= 0:
-            raise TypeError("'height' has to be positive")
-        self._height = height
-
-    @property
-    def zR(self):
-        """return the relative altitude of the Piece
-        the relative altitude is the difference of altitude between the bottom of the Piece
-        and the altitude of its position"""
-        return self._zR
-
-    @zR.setter
-    def zR(self, zR):
-        """set the relative altitude of the Piece"""
-        if not isinstance(zR, int):
-            raise TypeError("'zR' has to be an integer")
-        self._zR = zR
-
-    @property
-    def shape(self):
-        """return the shape of the Piece
-        shape is a list of relative coordinates, assuming (0, 0) is the position of the piece
-        eg: [(-1, -1), (-1, 0), (0, 0)]"""
-        return self._shape
-
-    @shape.setter
-    def shape(self, shape):
-        """set the shape of the piece (see shape property for more informations)"""
-        if not isinstance(shape, list):
-            raise TypeError("'shape' has to be a list")
-        for item in shape:
-            try:
-                _, _ = item
-                if not all([isinstance(var, int) for var in item]):
-                    raise ValueError("'shape' have to be a list of (x, y) tuples, with x and y integers")
-            except ValueError:
-                raise ValueError("'shape' have to be a list of (x, y) tuples, with x and y integers")
-        self._shape = shape
-
-    @property
-    def cell_shape(self):
-        """return the 'cell_shape' from GRID_GEOMETRIES for which the Piece is made for.
-        Cell's shape is needed for pivot algorithm, and for graphical purposes"""
-        return self._cell_shape
-
-    @cell_shape.setter
-    def cell_shape(self, cell_shape):
-        """set the 'cell_shape' of the piece (see cell_shape property for more informations)"""
-        self._cell_shape = cell_shape
-
-    @property
-    def rotation(self):
-        """return the current rotation of the piece.
-        rotation is an integer representing the number of pivot applied to the piece"""
-        return self._rotation
-
-    @rotation.setter
-    def rotation(self, rotation):
-        """set the 'rotation' of the piece (see rotation property for more informations)"""
-        if not isinstance(rotation, int):
-            raise TypeError("'rotation' has to be an integer")
-        self._rotation = rotation
-
-    # methods
-    def occupation(self):
-        """return the list of the (x, y) coordinates currently occupied by the Piece"""
-        result = [ (xr + self.x, yr + self.y) for xr, yr in self._shape ]
-        if self._rotation != 0:
-            result = geometry.pivot(self._cell_shape, self.position, result, self.rotation)
-        return result
-
-    def occupation3d(self, dz=0):
-        """return the list of the (x, y, z) coordinates currently occupied by the Piece
-
-        Because Pieces altitude zR is relative, the z coordinate returned is not an absolute altitude
-        If you want an absolute altitude, use the 'dz' modifier to correct the result."""
-        occupation = self.occupation()
-        return [(x, y, z) for x, y in occupation for z in range(dz + self.zR, dz + self.zR + self.height)]
-
-    def move_to(self, x, y, zR=None):
-        """move the piece to (x, y) position and is requested to zR relative altitude"""
-        self.position = x, y
-        if zR != None:
-            self.zR = zR
-
-    def rotate(self, i):
-        """pivot the Piece i times (counterclockwise rotation, i can be negative)"""
-        new_rotation = self.rotation + i
-        self.rotation = new_rotation % self.cell_shape

+ 169 - 147
pypog/gridlib.py → pypog/geometry_objects.py

@@ -1,95 +1,111 @@
 '''
-    Grid objects
+    Geometry objects
 
     ** By Cro-Ki l@b, 2017 **
 '''
 from math import sqrt
+import math
 
 
-class BaseGrid(object):
-    """ Base class for grids
-    This class should be overriden """
-    def __init__(self, width, height):
-        """ instanciate a new BaseGrid object """
-        self._width = 0
-        self.width = width
-        self._height = 0
-        self.height = height
+class BoundingRect(tuple):
+    """ Bounding rectangle defined by a top-left (xmin, ymin) point
+     and a bottom-right (xmax, ymax) point """
+    def __new__(self, xmin, ymin, xmax, ymax):
+        return tuple.__new__(self, (xmin, ymin, xmax, ymax))
+
+    @classmethod
+    def from_(cls, *args):
+        BaseGeometry.assertCoordinates(*args)
+        xs, ys = zip(*list(args))
+        xs, ys = sorted(list(xs)), sorted(list(ys))
+        return cls(xs[0], ys[0], xs[-1], ys[-1])
+
+    @property
+    def xmin(self):
+        return self[0]
+
+    @property
+    def ymin(self):
+        return self[1]
+
+    @property
+    def xmax(self):
+        return self[2]
+
+    @property
+    def ymax(self):
+        return self[3]
+
+    def __contains__(self, key):
+        BaseGeometry.assertCoordinates(key)
+        return self.xmin <= key[0] <= self.xmax and self.ymin <= key[1] <= self.ymax
+
+    @property
+    def topleft(self):
+        return (self.xmin, self.ymin)
+
+    @property
+    def bottomright(self):
+        return (self.xmax, self.ymax)
+
+    @property
+    def width(self):
+        return self.xmax - self.xmin + 1
+
+    @property
+    def height(self):
+        return self.ymax - self.ymin + 1
+
+class IBoundingRect(BoundingRect):
+    """ Infinite bounding rectangle
+    >> '(x, y) in IBoundingRect()' is always True"""
+    def __new__(self):
+        return BoundingRect.__new__(self, -math.inf, -math.inf, math.inf, math.inf)
+
+
+class BaseGeometry:
+    """ Base class for geometry classes
+    ! Should be overriden """
+    ANGLES = (1, 2, 3)
 
     def __repr__(self):
         return "<{} object>".format(self.__class__.__name__)
 
+    @classmethod
+    def instance(cls):
+        return cls()
+
     @staticmethod
-    def _assertCoordinates(*args):
+    def assertCoordinates(*args):
         """ raise a ValueError if the args are not (x, y) iterables, where x and y are integers
         usage:
-            self._assertCoordinates((x1, y1), (x2, y2), ...)
+            self.assertCoordinates((x1, y1), (x2, y2), ...)
         """
         try:
             if all([isinstance(i, int) for x, y in args for i in (x, y)]):
                 return
         except (TypeError, ValueError):
             pass
-        raise ValueError("{} is not a valid (x, y) coordinates iterable".format(args))
-
-    # properties
-    @property
-    def width(self):
-        """ the width of the grid """
-        return self._width
+        raise ValueError("'{}' is not a valid (x, y) coordinates iterable".format(args))
 
-    @width.setter
-    def width(self, width):
-        """ set a new width for the grid.
-        the new width has to be a strictly positive integer"""
-        if not isinstance(width, int) or not width > 0:
-            raise ValueError("'width' has to be a strictly positive integer")
-        self._width = width
-
-    @property
-    def height(self):
-        """ the height of the grid """
-        return self._height
-
-    @height.setter
-    def height(self, height):
-        """ set a new height for the grid.
-        the new height has to be a strictly positive integer"""
-        if not isinstance(height, int) or not height > 0:
-            raise ValueError("'width' has to be a strictly positive integer")
-        self._height = height
-
-    # geometric methods
-    def __len__(self):
-        """ return the number of cells in the grid """
-        return self.height * self.width
+    @staticmethod
+    def _assertPositiveInt(value, strict=False):
+        """ raise a ValueError if the 'value' is not a dimension,
+        i.e. a (strictly) positive integer """
+        if not isinstance(value, int) or not ((value > 0) or (not strict and value >= 0)):
+            raise ValueError("Expected: strictly positive integer(given: '{}')".format(value))
 
-    def __contains__(self, key):
-        """return True if the (x, y) coordinates are in the grid"""
-        try:
-            self._assertCoordinates(key)
-        except ValueError:
-            pass
-        else:
-            return 0 <= key[0] < self._width and 0 <= key[1] < self._height
-        return False
+    @staticmethod
+    def _assertValidAngle(value):
+        """ raise a ValueError if the 'value' is not a valid angle """
+        if not value in BaseGeometry.ANGLES:
+            raise ValueError("angle has to be a value from BaseGeometry.ANGLES (given: {})".format(value))
 
-    def __iter__(self):
-        """ iterate over the coordinates of the grid """
-        for item in ((x, y) for x in range(self.width) for y in range(self.height)):
-            yield item
-        raise StopIteration()
+    @staticmethod
+    def _bounding_rect(*args):
+        """ return the bounding rectangle of the from (x, y) coordinates """
+        return BoundingRect.from_(*args)
 
-    @classmethod
-    def _bounding_rect(cls, *args):
-        """ return (xmin, ymin, xmax, ymax) from (x, y) coordinates """
-        cls._assertCoordinates(*args)
-        xs, ys = zip(*args)
-        xs.sort()
-        ys.sort()
-        return xs[0], xs[-1], ys[0], ys[-1]
-
-    # graphical methods
     @staticmethod
     def graphicsitem(x, y, scale=120):
         """ returns the list of the points which compose the (x, y) cell """
@@ -97,40 +113,33 @@ class BaseGrid(object):
 
     # geometrical algorithms
     @classmethod
-    def neighbors(cls, x, y):
+    def neighbors(cls, x, y, br=IBoundingRect()):
         """ returns a list of the neighbors of (x, y) """
-        return [key for key in cls._neighbors(x, y) if cls._is_in(key)]
-
-    @classmethod
-    def _neighbors(cls, x, y):
         raise NotImplementedError("this method is abstract and should be reimplemented in subclasses")
 
     @classmethod
-    def line(cls, x1, y1, x2, y2):
+    def line(cls, x1, y1, x2, y2, br=IBoundingRect()):
         raise NotImplementedError("this method is abstract and should be reimplemented in subclasses")
 
     @classmethod
-    def line3d(cls, x1, y1, z1, x2, y2, z2):
+    def line3d(cls, x1, y1, z1, x2, y2, z2, br=IBoundingRect()):
         """ returns a line from (x1 ,y1, z1) to (x2, y2, z2)
         as a list of (x, y, z) coordinates """
-        if not all(isinstance(c, int) for c in [z1, z2]):
-            raise TypeError("x1, y1, z1, x2, y2, z2 have to be integers")
+        cls.assertCoordinates((z1, z2))
         hoLine = cls.line(x1, y1, x2, y2)
         if z1 == z2:
             return [(x, y, z1) for x, y in hoLine]
         else:
-            ligneZ = SquareGrid.line(0, z1, (len(hoLine) - 1), z2)
+            ligneZ = SquareGeometry.line(0, z1, (len(hoLine) - 1), z2)
             return [(hoLine[d][0], hoLine[d][1], z) for d, z in ligneZ]
 
     @classmethod
-    def zone(cls, x, y, radius):
+    def zone(cls, x, y, radius, br=IBoundingRect()):
         """ returns the list of the coordinates of the cells in a zone around (x, y)
         """
-        cls._assertCoordinates((x, y))
-        if not isinstance(radius, int):
-            raise TypeError("radius has to be an integer (given: {})".format(radius))
-        if not radius >= 0:
-            raise ValueError("radius has to be positive")
+        cls.assertCoordinates((x, y))
+        cls._assertPositiveInt(radius)
+
         buffer = frozenset([(x, y)])
 
         for _ in range(0, radius):
@@ -140,13 +149,13 @@ class BaseGrid(object):
         return list(buffer)
 
     @classmethod
-    def triangle(cls, xa, ya, xh, yh, iAngle):
+    def triangle(cls, xa, ya, xh, yh, iAngle, br=IBoundingRect()):
         """ return the list of the (x, y) coordinates in a triangle
         with (xa, ya) apex and (xh, yh) middle of the base """
         raise NotImplementedError("this method is abstract and should be reimplemented in subclasses")
 
     @classmethod
-    def triangle3d(self, xa, ya, za, xh, yh, zh, iAngle):
+    def triangle3d(self, xa, ya, za, xh, yh, zh, iAngle, br=IBoundingRect()):
         """Returns a list of (x, y, z) coordinates in a 3d-cone
         A is the top of the cone, H if the center of the base
 
@@ -160,42 +169,35 @@ class BaseGrid(object):
         raise NotImplementedError("this method is abstract and should be reimplemented in subclasses")
 
     @classmethod
-    def rectangle(cls, x1, y1, x2, y2):
+    def rectangle(cls, x1, y1, x2, y2, br=IBoundingRect()):
         """return a list of cells in a rectangle between (X1, Y1), (X2, Y2)"""
-        cls._assertCoordinates((x1, y1), (x2, y2))
-        xa, ya, xb, yb = min([x1, x2]), min([y1, y2]), max([x1, x2]), max([y1, y2])
-        return [(x, y) for x in range(xa, xb + 1) for y in range(ya, yb + 1)]
+        xmin, ymin, xmax, ymax = cls._bounding_rect((x1, y1), (x2, y2))
+        return [(x, y) for x in range(xmin, xmax + 1) for y in range(ymin, ymax + 1)]
 
     @classmethod
-    def hollow_rectangle(cls, x1, y1, x2, y2):
+    def hollow_rectangle(cls, x1, y1, x2, y2, br=IBoundingRect()):
         """return a list of cells composing the sides of the rectangle between (X1, Y1), (X2, Y2)"""
-        cls._assertCoordinates((x1, y1), (x2, y2))
         xmin, ymin, xmax, ymax = cls._bounding_rect((x1, y1), (x2, y2))
-        return [(x, ymin) for x in range(xmin, xmax + 1)] + \
-               [(x, ymax) for x in range(xmin, xmax + 1)] + \
-               [(xmin, y) for y in range(ymin, ymax + 1)] + \
-               [(xmax, y) for y in range(ymin, ymax + 1)]
+        if (xmin, ymin) == (xmax, ymax):
+            return [(xmin, ymin)]
+        return [(x, ymin) for x in range(xmin, xmax)] + \
+               [(xmax, y) for y in range(ymin, ymax)] + \
+               [(x, ymax) for x in range(xmax, xmin, -1)] + \
+               [(xmin, y) for y in range(ymax, ymin, -1)]
 
     @classmethod
-    def rotate(cls, center, coordinates, rotations):
+    def rotate(cls, center, coordinates, rotations, br=IBoundingRect()):
         """ return the 'coordinates' list of (x, y) coordinates
         after a rotation of 'rotations' times around the (x, y) center """
         raise NotImplementedError("this method is abstract and should be reimplemented in subclasses")
 
-#     def moving_cost(self, *args):
-#         return 1
-#
-#     def path(self, x1, y1, x2, y2):
-#         raise NotImplementedError("this method is abstract and should be reimplemented in subclasses")
-
-class SquareGrid(BaseGrid):
-    """ Square grid object """
-    def __init__(self, *args, **kwargs):
-        BaseGrid.__init__(self, *args, **kwargs)
+class SquareGeometry(BaseGeometry):
+    """ Geometry on square grids """
+    _nodiags = False
 
     @staticmethod
     def graphicsitem(x, y, scale=120):
-        """ reimplemented from BaseGrid.graphicsitem """
+        """ reimplemented from BaseGeometry.graphicsitem """
         return  [
                     (x * scale, y * scale), \
                     ((x + 1) * scale, y * scale), \
@@ -204,17 +206,32 @@ class SquareGrid(BaseGrid):
                 ]
 
     @classmethod
-    def _neighbors(cls, x, y):
-        """ reimplemented from BaseGrid._neighbors """
-        return [(x - 1, y - 1), (x, y - 1), (x + 1, y - 1), \
-                (x - 1, y), (x + 1, y)  , \
-                (x - 1, y + 1), (x, y + 1), (x + 1, y + 1)]
+    def set_no_diagonals(cls, active):
+        """ if nodiags is set to True, the neighbors method
+        won't return the diagonals cells """
+        cls._nodiags = active
+
+    @classmethod
+    def neighbors(cls, x, y, br=IBoundingRect()):
+        """ reimplemented from BaseGeometry._neighbors """
+        cls.assertCoordinates((x, y))
+
+        if cls._nodiags:
+            return [(x, y - 1), \
+                    (x - 1, y), (x + 1, y)  , \
+                    (x, y + 1)]
+        else:
+            return [(x - 1, y - 1), (x, y - 1), (x + 1, y - 1), \
+                    (x - 1, y), (x + 1, y)  , \
+                    (x - 1, y + 1), (x, y + 1), (x + 1, y + 1)]
 
     @classmethod
-    def line(cls, x1, y1, x2, y2):
-        """ reimplemented from BaseGrid.line
+    def line(cls, x1, y1, x2, y2, br=IBoundingRect()):
+        """ reimplemented from BaseGeometry.line
         Implementation of bresenham's algorithm
         """
+        cls.assertCoordinates((x1, y1), (x2, y2))
+
         result = []
 
         if (x1, y1) == (x2, y2):
@@ -249,8 +266,11 @@ class SquareGrid(BaseGrid):
         return result
 
     @classmethod
-    def triangle(cls, xa, ya, xh, yh, iAngle):
-        """ reimplemented from BaseGrid.triangle """
+    def triangle(cls, xa, ya, xh, yh, iAngle, br=IBoundingRect()):
+        """ reimplemented from BaseGeometry.triangle """
+        cls.assertCoordinates((xa, ya), (xh, yh))
+        cls._assertValidAngle(iAngle)
+
         if (xa, ya) == (xh, yh):
             return [(xa, ya)]
 
@@ -300,11 +320,12 @@ class SquareGrid(BaseGrid):
         return result
 
     @classmethod
-    def triangle3d(cls, xa, ya, za, xh, yh, zh, iAngle):
-        """ reimplemented from BaseGrid.triangle3d """
-        result = []
-
+    def triangle3d(cls, xa, ya, za, xh, yh, zh, iAngle, br=IBoundingRect()):
+        """ reimplemented from BaseGeometry.triangle3d """
+        cls.assertCoordinates((za, zh))
         flat_triangle = cls.triangle(xa, ya, xh, yh, iAngle)
+
+        result = {}
         k = 1 / (iAngle * sqrt(3))
 
         length = max(abs(xh - xa), abs(yh - ya))
@@ -330,8 +351,10 @@ class SquareGrid(BaseGrid):
 
 
     @classmethod
-    def rotate(cls, center, coordinates, rotations):
-        """ reimplemented from BaseGrid.rotate """
+    def rotate(cls, center, coordinates, rotations, br=IBoundingRect()):
+        """ reimplemented from BaseGeometry.rotate """
+        cls.assertCoordinates(center, *coordinates)
+
         if coordinates == [center] or rotations % 4 == 0:
             return coordinates
         x0, y0 = center
@@ -344,11 +367,9 @@ class SquareGrid(BaseGrid):
             result.append((xr, yr))
         return result
 
-class _HexGrid(BaseGrid):
+class HexGeometry(BaseGeometry):
     """ Base class for hexagonal grids classes
     This class should be overridden """
-    def __init__(self, *args, **kwargs):
-        BaseGrid.__init__(self, *args, **kwargs)
 
     @staticmethod
     def cv_cube_off(xu, yu, zu):
@@ -391,20 +412,17 @@ class _HexGrid(BaseGrid):
     def distance_off(xa, ya, xb, yb):
         """ distance between A and B (offset coordinates)"""
         # 10 times quicker if no conversion...
-        xua, yua, zua = FHexGrid.cv_off_cube(xa, ya)
-        xub, yub, zub = FHexGrid.cv_off_cube(xb, yb)
+        xua, yua, zua = HexGeometry.cv_off_cube(xa, ya)
+        xub, yub, zub = HexGeometry.cv_off_cube(xb, yb)
         return max(abs(xua - xub), abs(yua - yub), abs(zua - zub))
 
 
-class FHexGrid(_HexGrid):
+class FHexGeometry(HexGeometry):
     """ Flat-hexagonal grid object """
 
-    def __init__(self, *args, **kwargs):
-        _HexGrid.__init__(self, *args, **kwargs)
-
     @staticmethod
     def graphicsitem(x, y, scale=120):
-        """ reimplemented from BaseGrid.graphicsitem """
+        """ reimplemented from BaseGeometry.graphicsitem """
         if x % 2 != 0:
             y += 0.5
         return [
@@ -417,16 +435,18 @@ class FHexGrid(_HexGrid):
                 ]
 
     @classmethod
-    def _neighbors(cls, x, y):
+    def neighbors(cls, x, y, br=IBoundingRect()):
         if x % 2 == 0:
             return [(x, y - 1), (x + 1, y - 1), (x + 1, y), (x, y + 1), (x - 1, y), (x - 1, y - 1)]
         else:
             return [(x, y - 1), (x + 1, y), (x + 1, y + 1), (x, y + 1), (x - 1, y + 1), (x - 1, y)]
 
     @classmethod
-    def line(cls, x1, y1, x2, y2):
-        """ reimplemented from BaseGrid.line
+    def line(cls, x1, y1, x2, y2, br=IBoundingRect()):
+        """ reimplemented from BaseGeometry.line
         Implementation of bresenham's algorithm """
+        cls.assertCoordinates((x1, y1), (x2, y2))
+
         if (x1, y1) == (x2, y2):
             return [(x1, y1)]
 
@@ -522,8 +542,11 @@ class FHexGrid(_HexGrid):
         return result
 
     @classmethod
-    def triangle(cls, xa, ya, xh, yh, iAngle):
-        """ reimplemented from BaseGrid.triangle """
+    def triangle(cls, xa, ya, xh, yh, iAngle, br=IBoundingRect()):
+        """ reimplemented from BaseGeometry.triangle """
+        cls.assertCoordinates((xa, ya), (xh, yh))
+        cls._assertValidAngle(iAngle)
+
         if (xa, ya) == (xh, yh):
             return [(xa, ya)]
 
@@ -581,9 +604,9 @@ class FHexGrid(_HexGrid):
         return result
 
     @classmethod
-    def triangle3d(cls, xa, ya, za, xh, yh, zh, iAngle):
-        """ reimplemented from BaseGrid.triangle3d """
-
+    def triangle3d(cls, xa, ya, za, xh, yh, zh, iAngle, br=IBoundingRect()):
+        """ reimplemented from BaseGeometry.triangle3d """
+        cls.assertCoordinates((za, zh))
         flat_triangle = cls.triangle(xa, ya, xh, yh, iAngle)
 
         result = {}
@@ -596,7 +619,7 @@ class FHexGrid(_HexGrid):
 
         length = max(abs(xuh - xua), abs(yuh - yua), abs(zuh - zua))
 
-        vertical_line = SquareGrid.line(0, za, length, zh)
+        vertical_line = SquareGeometry.line(0, za, length, zh)
 
         # build a dict with X key and value is a list of Z values
         vertical_line_dict = {d:[] for d, z in vertical_line}
@@ -617,8 +640,10 @@ class FHexGrid(_HexGrid):
         return result
 
     @classmethod
-    def rotate(cls, center, coordinates, rotations):
-        """ reimplemented from BaseGrid.rotate """
+    def rotate(cls, center, coordinates, rotations, br=IBoundingRect()):
+        """ reimplemented from BaseGeometry.rotate """
+        cls.assertCoordinates(center, *coordinates)
+
         if coordinates == [center] or rotations % 6 == 0:
             return coordinates
         x0, y0 = center
@@ -634,6 +659,3 @@ class FHexGrid(_HexGrid):
             xr, yr = cls.cv_cube_off(xru, yru, zru)
             result.append((xr, yr))
         return result
-
-
-

+ 122 - 0
pypog/grid_objects.py

@@ -0,0 +1,122 @@
+'''
+    Grid objects
+
+    ** By Cro-Ki l@b, 2017 **
+'''
+from pypog.geometry_objects import BaseGeometry, FHexGeometry, SquareGeometry, \
+    BoundingRect
+
+class BaseGrid(object):
+    """ Base class for grids
+    This class should be overriden """
+    geometry = BaseGeometry
+
+    def __init__(self, width, height):
+        """ instanciate a new BaseGrid object """
+        self._width = 0
+        self.width = width
+        self._height = 0
+        self.height = height
+
+    def __repr__(self):
+        return "<{} object>".format(self.__class__.__name__)
+
+    @staticmethod
+    def from_geometry(geometry, *args):
+        if geometry == SquareGeometry:
+            return SquareGrid(*args)
+        elif geometry == FHexGeometry:
+            return FHexGrid(*args)
+        else:
+            raise TypeError("geometry has to be a non-abstract subclass of BaseGeometry")
+
+    # properties
+    @property
+    def width(self):
+        """ the width of the grid """
+        return self._width
+
+    @width.setter
+    def width(self, width):
+        """ set a new width for the grid.
+        the new width has to be a strictly positive integer"""
+        if not isinstance(width, int) or not width > 0:
+            raise ValueError("'width' has to be a strictly positive integer")
+        self._width = width
+
+    @property
+    def height(self):
+        """ the height of the grid """
+        return self._height
+
+    @height.setter
+    def height(self, height):
+        """ set a new height for the grid.
+        the new height has to be a strictly positive integer"""
+        if not isinstance(height, int) or not height > 0:
+            raise ValueError("'width' has to be a strictly positive integer")
+        self._height = height
+
+    @property
+    def br(self):
+        """ return the bounding rectangle of the current Grid """
+        return BoundingRect(0, 0, self.width - 1, self.height - 1)
+
+    # geometric methods
+    def __len__(self):
+        """ return the number of cells in the grid """
+        return self.height * self.width
+
+    def __contains__(self, key):
+        """return True if the (x, y) coordinates are in the grid"""
+        try:
+            self.geometry.assertCoordinates(key)
+        except ValueError:
+            return False
+        else:
+            return 0 <= key[0] < self._width and 0 <= key[1] < self._height
+
+    def __iter__(self):
+        """ iterate over the coordinates of the grid """
+        for item in ((x, y) for x in range(self.width) for y in range(self.height)):
+            yield item
+
+    # geometrical algorithms
+    def neighbors(self, *args):
+        return self.geometry.neighbors(*args, br=self.br)
+
+    def line(self, *args):
+        return self.geometry.line(*args, br=self.br)
+
+    def line3d(self, *args):
+        return self.geometry.line3d(*args, br=self.br)
+
+    def zone(self, *args):
+        return self.geometry.zone(*args, br=self.br)
+
+    def triangle(self, *args):
+        return self.geometry.triangle(*args, br=self.br)
+
+    def triangle3d(self, *args):
+        return self.geometry.triangle3d(*args, br=self.br)
+
+    def rectangle(self, *args):
+        return self.geometry.rectangle(*args, br=self.br)
+
+    def hollow_rectangle(self, *args):
+        return self.geometry.hollow_rectangle(*args, br=self.br)
+
+    def rotate(self, *args):
+        return self.geometry.rotate(*args, br=self.br)
+
+class SquareGrid(BaseGrid):
+    """ Square grid object """
+    geometry = SquareGeometry
+    def __init__(self, *args, **kwargs):
+        BaseGrid.__init__(self, *args, **kwargs)
+
+class FHexGrid(BaseGrid):
+    """ Flat-hexagonal grid object """
+    geometry = FHexGeometry
+    def __init__(self, *args, **kwargs):
+        BaseGrid.__init__(self, *args, **kwargs)

+ 2 - 2
pypog/painting.py

@@ -13,7 +13,7 @@
 
     ** By Cro-Ki l@b, 2017 **
 '''
-from pypog import gridlib
+from pypog import geometry_objects
 
 
 class NotStartedException(Exception):
@@ -30,7 +30,7 @@ class BasePencil(object):
     def __init__(self, grid):
 
         # do we really need the grid ref? cell_shape could be enough?
-        if not isinstance(grid, gridlib.BaseGrid):
+        if not isinstance(grid, geometry_objects.BaseGeometry):
             raise TypeError("'grid' should be a Grid object (given: {})".format(grid))
         self._grid = grid
 

+ 152 - 208
tests/test_geometry.py

@@ -4,63 +4,100 @@
 
     ** By Cro-Ki l@b, 2017 **
 '''
+from math import inf
 import unittest
 
-from pypog import geometry
+from pypog.geometry_objects import FHexGeometry, SquareGeometry, BaseGeometry, \
+    BoundingRect, IBoundingRect
 
-class Test(unittest.TestCase):
-
-    # # neighbours
 
-    def test_neighbours(self):
-        """ test for geometry.neighbours """
-        for coord in ((0, 0), (-10, -10), (10, 10)):
-            x, y = coord
-            self.assertEqual(geometry.neighbours(geometry.FLAT_HEX, x, y), geometry.fhex2_neighbours(x, y))
-            self.assertEqual(geometry.neighbours(geometry.SQUARE, x, y), geometry.squ2_neighbours(x, y))
-
-    def test_fhex2_neighbours(self):
-        """ test for geometry.fhex2_neighbours """
-        self.assertCountEqual(geometry.fhex2_neighbours(3, 3), [(3, 2), (4, 3), (4, 4), (3, 4), (2, 4), (2, 3)])
-        self.assertCountEqual(geometry.fhex2_neighbours(4, 4), [(4, 3), (5, 3), (5, 4), (4, 5), (3, 4), (3, 3)])
+class Test(unittest.TestCase):
 
-    def test_squ2_neighbours(self):
-        """ test for geometry.squ2_neighbours """
-        self.assertCountEqual(geometry.squ2_neighbours(3, 3), [(2, 3), (2, 2), (3, 2), (4, 2), (4, 3), (4, 4), (3, 4), (2, 4)])
+    def setUp(self):
+        SquareGeometry.set_no_diagonals(False)
+
+    def test_bounding_rect(self):
+        br = BoundingRect(0, 1, 10, 11)
+        self.assertEqual(br.xmin, 0)
+        self.assertEqual(br.ymin, 1)
+        self.assertEqual(br.xmax, 10)
+        self.assertEqual(br.ymax, 11)
+        self.assertEqual(br.topleft, (0, 1))
+        self.assertEqual(br.bottomright, (10, 11))
+        self.assertEqual(br.width, 11)
+        self.assertEqual(br.height, 11)
+        self.assertTrue((5, 5) in br)
+        self.assertFalse((15, 15) in br)
+
+        ibr = IBoundingRect()
+        self.assertTrue((5, 5) in ibr)
+        self.assertTrue((15, 15) in ibr)
+        self.assertTrue((10000, 10000) in ibr)
+        self.assertEqual(ibr.xmin, -inf)
+        self.assertEqual(ibr.ymin, -inf)
+        self.assertEqual(ibr.xmax, inf)
+        self.assertEqual(ibr.ymax, inf)
+        self.assertEqual(ibr.topleft, (-inf, -inf))
+        self.assertEqual(ibr.bottomright, (inf, inf))
+        self.assertEqual(ibr.width, inf)
+        self.assertEqual(ibr.height, inf)
+
+    def test_various(self):
+        self.assertEqual(str(BaseGeometry()), "<BaseGeometry object>")
+        self.assertEqual(str(SquareGeometry()), "<SquareGeometry object>")
+        self.assertEqual(str(FHexGeometry()), "<FHexGeometry object>")
+
+        self.assertTrue(isinstance(BaseGeometry.instance(), BaseGeometry))
+
+        self.assertRaises(NotImplementedError, BaseGeometry.graphicsitem, 0, 0, 120)
+        self.assertRaises(NotImplementedError, BaseGeometry.neighbors, 0, 0)
+        self.assertRaises(NotImplementedError, BaseGeometry.line, 0, 0, 0, 0)
+        self.assertRaises(NotImplementedError, BaseGeometry.triangle, 0, 0, 0, 0, 1)
+        self.assertRaises(NotImplementedError, BaseGeometry.triangle3d, 0, 0, 0, 0, 0, 0, 1)
+        self.assertRaises(NotImplementedError, BaseGeometry.rotate, (0, 0), [(0, 0)], 1)
+
+
+    # # neighbors
+    def test_neighbors(self):
+        """ test for geometry.neighbors """
+        self.assertCountEqual(FHexGeometry.neighbors(3, 3), [(3, 2), (4, 3), (4, 4), (3, 4), (2, 4), (2, 3)])
+        self.assertCountEqual(FHexGeometry.neighbors(4, 4), [(4, 3), (5, 3), (5, 4), (4, 5), (3, 4), (3, 3)])
+
+        self.assertCountEqual(SquareGeometry.neighbors(3, 3), [(2, 3), (2, 2), (3, 2), (4, 2), (4, 3), (4, 4), (3, 4), (2, 4)])
+        SquareGeometry.set_no_diagonals(True)
+        self.assertCountEqual(SquareGeometry.neighbors(3, 3), [(2, 3), (3, 2), (4, 3), (3, 4)])
 
     def test_zone(self):
         """ test for geometry.zone """
-        self.assertRaises(TypeError, geometry.zone, 5, 0, 0, "a")
-        self.assertRaises(TypeError, geometry.zone, 5, "a", 0, 1)
-        self.assertRaises(TypeError, geometry.zone, 5, 0, "a", 1)
-        self.assertRaises(ValueError, geometry.zone, 5, 0, 0, -1)
-        self.assertRaises(ValueError, geometry.zone, -1, 0, 0, 1)
-        self.assertRaises(ValueError, geometry.zone, "a", 0, 0, 1)
-
-        self.assertCountEqual(geometry.zone(geometry.FLAT_HEX, 3, 3, 0), [(3, 3)])
-        self.assertCountEqual(geometry.zone(geometry.FLAT_HEX, 3, 3, 1), [(3, 2), (2, 3), (3, 3), (4, 3), (4, 4), (3, 4), (2, 4)])
-        self.assertCountEqual(geometry.zone(geometry.FLAT_HEX, 3, 3, 2), [(3, 2), (1, 3), (5, 4), (4, 5), (1, 4), (2, 3), (4, 2), \
+        self.assertRaises(ValueError, BaseGeometry.zone, "a", 0, 1)
+        self.assertRaises(ValueError, BaseGeometry.zone, 0, "a", 1)
+        self.assertRaises(ValueError, BaseGeometry.zone, 0, 0, "a")
+        self.assertRaises(ValueError, BaseGeometry.zone, 0, 0, -1)
+
+        self.assertCountEqual(FHexGeometry.zone(3, 3, 0), [(3, 3)])
+        self.assertCountEqual(FHexGeometry.zone(3, 3, 1), [(3, 2), (2, 3), (3, 3), (4, 3), (4, 4), (3, 4), (2, 4)])
+        self.assertCountEqual(FHexGeometry.zone(3, 3, 2), [(3, 2), (1, 3), (5, 4), (4, 5), (1, 4), (2, 3), (4, 2), \
                                                             (2, 5), (5, 3), (1, 2), (3, 5), (3, 3), (4, 4), (3, 1), \
                                                             (4, 3), (2, 2), (3, 4), (2, 4), (5, 2)])
 
-        self.assertCountEqual(geometry.zone(geometry.SQUARE, 3, 3, 0), [(3, 3)])
-        self.assertCountEqual(geometry.zone(geometry.SQUARE, 3, 3, 1), [(3, 2), (3, 3), (4, 4), (2, 3), (4, 3), (2, 2), (4, 2), (3, 4), (2, 4)])
-        self.assertCountEqual(geometry.zone(geometry.SQUARE, 3, 3, 2), [(2, 4), (3, 2), (5, 4), (1, 3), (4, 5), (2, 1), (1, 4), (2, 3), (4, 2), \
-                                                                    (5, 1), (2, 5), (3, 5), (5, 3), (1, 2), (3, 3), (5, 5), (4, 4), (3, 1), \
-                                                                    (1, 5), (4, 3), (2, 2), (4, 1), (5, 2), (3, 4), (1, 1)])
+        self.assertCountEqual(SquareGeometry.zone(3, 3, 0), [(3, 3)])
+        self.assertCountEqual(SquareGeometry.zone(3, 3, 1), [(3, 2), (3, 3), (4, 4), (2, 3), (4, 3), (2, 2), (4, 2), (3, 4), (2, 4)])
+        self.assertCountEqual(SquareGeometry.zone(3, 3, 2), [(2, 4), (3, 2), (5, 4), (1, 3), (4, 5), (2, 1), (1, 4), (2, 3), (4, 2), \
+                                                                (5, 1), (2, 5), (3, 5), (5, 3), (1, 2), (3, 3), (5, 5), (4, 4), (3, 1), \
+                                                                (1, 5), (4, 3), (2, 2), (4, 1), (5, 2), (3, 4), (1, 1)])
 
     # # lines
 
     def test_line2d(self):
         """ test for geometry.line """
-        self.assertRaises(TypeError, geometry.line, geometry.FLAT_HEX, "a", 1, 1, 1)
-        self.assertRaises(TypeError, geometry.line, geometry.FLAT_HEX, 1, "a", 1, 1)
-        self.assertRaises(TypeError, geometry.line, geometry.FLAT_HEX, 1, 1, "a", 1)
-        self.assertRaises(TypeError, geometry.line, geometry.FLAT_HEX, 1, 1, 1, "a")
-        self.assertRaises(ValueError, geometry.line, 0, 1, 1, 1, 1)
+        for geometry in (SquareGeometry, FHexGeometry):
+            self.assertRaises(ValueError, geometry.line, "a", 1, 1, 1)
+            self.assertRaises(ValueError, geometry.line, 1, "a", 1, 1)
+            self.assertRaises(ValueError, geometry.line, 1, 1, "a", 1)
+            self.assertRaises(ValueError, geometry.line, 1, 1, 1, "a")
 
         attended = {
-                    geometry.FLAT_HEX:    {
+                    FHexGeometry:    {
                                       (1, 1, 1, 1): [(1, 1)],
                                       (0, 0, 1, 1): [(0, 0), (0, 1), (1, 1)],
                                       (1, 1, 0, 0): [(1, 1), (0, 1), (0, 0)],
@@ -72,7 +109,7 @@ class Test(unittest.TestCase):
                                       (3, 3, 3, 0): [(3, 3), (3, 2), (3, 1), (3, 0)]
                                      },
 
-                    geometry.SQUARE: {
+                    SquareGeometry: {
                                       (1, 1, 1, 1): [(1, 1)],
                                       (0, 0, 0, 1): [(0, 0), (0, 1)],
                                       (0, 1, 0, 0): [(0, 1), (0, 0)],
@@ -87,18 +124,19 @@ class Test(unittest.TestCase):
                                      }
                    }
 
-        for cell_shape, tests in attended.items():
+        for geometry, tests in attended.items():
             for args, attended in tests.items():
-                result = geometry.line(cell_shape, *args)
-                self.assertEqual(result, attended)
+                result = geometry.line(*args)
+                self.assertCountEqual(result, attended)
 
     def test_line3d(self):
         """ test for geometry.line3d """
-        self.assertRaises(TypeError, geometry.line3d, geometry.FLAT_HEX, 1, 1, "a", 1, 1, 1)
-        self.assertRaises(TypeError, geometry.line3d, geometry.FLAT_HEX, 1, 1, 1, 1, 1, "a")
+        for geometry in (SquareGeometry, FHexGeometry):
+            self.assertRaises(ValueError, geometry.line3d, 1, 1, "a", 1, 1, 1)
+            self.assertRaises(ValueError, geometry.line3d, 1, 1, 1, 1, 1, "a")
 
         attended = {
-                    geometry.FLAT_HEX:    {
+                    FHexGeometry:    {
                                       (1, 1, 1, 1, 1, 1) : [(1, 1, 1)],
                                       (1, 1, 0, 1, 1, 1) : [(1, 1, 0), (1, 1, 1)],
                                       (0, 0, 0, 1, 1, 1) : [(0, 0, 0), (0, 1, 0), (1, 1, 1)],
@@ -107,7 +145,7 @@ class Test(unittest.TestCase):
                                       (3, 0, 0, 3, 3, 0) : [(3, 0, 0), (3, 1, 0), (3, 2, 0), (3, 3, 0)]
                                      },
 
-                    geometry.SQUARE: {
+                    SquareGeometry: {
                                       (1, 1, 1, 1, 1, 1) : [(1, 1, 1)],
                                       (1, 1, 0, 1, 1, 1) : [(1, 1, 0), (1, 1, 1)],
                                       (0, 0, 0, 1, 1, 1) : [(0, 0, 0), (1, 1, 1)],
@@ -117,173 +155,92 @@ class Test(unittest.TestCase):
                                      }
                    }
 
-        for cell_shape, tests in attended.items():
+        for geometry, tests in attended.items():
             for args, result in tests.items():
-                line = geometry.line3d(cell_shape, *args)
+                line = geometry.line3d(*args)
                 self.assertEqual(line, result)
 
-    def test_squ2_line(self):
-        """ test for geometry.squ2_line """
-        pass
-
-    def test_fhex2_line(self):
-        """ test for geometry.fhex2_line """
-        pass
-
-
     # # Rectangles
     def test_rectangle(self):
         """ test for geometry.rectangle """
-        self.assertRaises(TypeError, geometry.rectangle, "a", 1, 1, 1)
-        self.assertRaises(TypeError, geometry.rectangle, 1, "a", 1, 1)
-        self.assertRaises(TypeError, geometry.rectangle, 1, 1, "a", 1)
-        self.assertRaises(TypeError, geometry.rectangle, 1, 1, 1, "a")
-
-        self.assertEquals(geometry.rectangle(0, 0, 0, 0), [(0, 0)])
-        self.assertCountEqual(geometry.rectangle(0, 0, 1, 1), [(0, 0), (0, 1), (1, 1), (1, 0)])
-        self.assertCountEqual(geometry.rectangle(1, 1, 0, 0), [(0, 0), (0, 1), (1, 1), (1, 0)])
-        self.assertCountEqual(geometry.rectangle(4, 3, 7, 5), [(4, 3), (4, 4), (4, 5), (5, 5), (6, 5), (7, 5), (7, 4), (7, 3), (6, 3), (5, 3), (6, 4), (5, 4)])
-        self.assertCountEqual(geometry.rectangle(3, 3, 9, 9), [(3, 3), (9, 9), (9, 8), (9, 7), (9, 5), (9, 6), (9, 4), (9, 3), (8, 4), (7, 3), (6, 4), (4, 4),
-                                                       (3, 4), (3, 5), (3, 6), (3, 7), (3, 8), (3, 9), (4, 9), (5, 9), (6, 9), (7, 9), (8, 9), (4, 5),
-                                                       (5, 4), (6, 5), (7, 4), (8, 5), (4, 6), (5, 5), (6, 6), (7, 5), (8, 6), (4, 7), (5, 6), (6, 7),
-                                                       (7, 6), (8, 7), (4, 8), (5, 7), (6, 8), (7, 7), (8, 8), (7, 8), (5, 8), (8, 3), (6, 3), (4, 3),
-                                                       (5, 3)])
+        self.assertRaises(ValueError, BaseGeometry.rectangle, "a", 1, 1, 1)
+        self.assertRaises(ValueError, BaseGeometry.rectangle, 1, "a", 1, 1)
+        self.assertRaises(ValueError, BaseGeometry.rectangle, 1, 1, "a", 1)
+        self.assertRaises(ValueError, BaseGeometry.rectangle, 1, 1, 1, "a")
+
+        self.assertEqual(BaseGeometry.rectangle(0, 0, 0, 0), [(0, 0)])
+        self.assertCountEqual(BaseGeometry.rectangle(0, 0, 1, 1), [(0, 0), (0, 1), (1, 1), (1, 0)])
+        self.assertCountEqual(BaseGeometry.rectangle(1, 1, 0, 0), [(0, 0), (0, 1), (1, 1), (1, 0)])
+        self.assertCountEqual(BaseGeometry.rectangle(4, 3, 7, 5), [(4, 3), (4, 4), (4, 5), (5, 5), (6, 5), (7, 5), (7, 4), (7, 3), (6, 3), (5, 3), (6, 4), (5, 4)])
+        self.assertCountEqual(BaseGeometry.rectangle(3, 3, 9, 9), [(3, 3), (9, 9), (9, 8), (9, 7), (9, 5), (9, 6), (9, 4), (9, 3), (8, 4), (7, 3), (6, 4), (4, 4),
+                                                                   (3, 4), (3, 5), (3, 6), (3, 7), (3, 8), (3, 9), (4, 9), (5, 9), (6, 9), (7, 9), (8, 9), (4, 5),
+                                                                   (5, 4), (6, 5), (7, 4), (8, 5), (4, 6), (5, 5), (6, 6), (7, 5), (8, 6), (4, 7), (5, 6), (6, 7),
+                                                                   (7, 6), (8, 7), (4, 8), (5, 7), (6, 8), (7, 7), (8, 8), (7, 8), (5, 8), (8, 3), (6, 3), (4, 3),
+                                                                   (5, 3)])
 
     def test_hollow_rectangle(self):
         """ test for geometry.hollow_rectangle """
-        self.assertRaises(TypeError, geometry.hollow_rectangle, "a", 1, 1, 1)
-        self.assertRaises(TypeError, geometry.hollow_rectangle, 1, "a", 1, 1)
-        self.assertRaises(TypeError, geometry.hollow_rectangle, 1, 1, "a", 1)
-        self.assertRaises(TypeError, geometry.hollow_rectangle, 1, 1, 1, "a")
-
-        self.assertEquals(geometry.hollow_rectangle(0, 0, 0, 0), [(0, 0)])
-        self.assertCountEqual(geometry.hollow_rectangle(0, 0, 1, 1), [(0, 0), (0, 1), (1, 1), (1, 0)])
-        self.assertCountEqual(geometry.hollow_rectangle(1, 1, 0, 0), [(0, 0), (0, 1), (1, 1), (1, 0)])
-        self.assertCountEqual(geometry.hollow_rectangle(4, 3, 7, 5), [(4, 3), (4, 4), (4, 5), (5, 5), (6, 5), (7, 5), (7, 4), (7, 3), (6, 3), (5, 3)])
-        self.assertCountEqual(geometry.hollow_rectangle(3, 3, 9, 9), [(3, 3), (9, 9), (9, 8), (9, 7), (9, 5), (9, 6), (9, 4), (9, 3), (7, 3), (3, 4),
+        self.assertRaises(ValueError, BaseGeometry.hollow_rectangle, "a", 1, 1, 1)
+        self.assertRaises(ValueError, BaseGeometry.hollow_rectangle, 1, "a", 1, 1)
+        self.assertRaises(ValueError, BaseGeometry.hollow_rectangle, 1, 1, "a", 1)
+        self.assertRaises(ValueError, BaseGeometry.hollow_rectangle, 1, 1, 1, "a")
+
+        self.assertEqual(BaseGeometry.hollow_rectangle(0, 0, 0, 0), [(0, 0)])
+        self.assertCountEqual(BaseGeometry.hollow_rectangle(0, 0, 1, 1), [(0, 0), (0, 1), (1, 1), (1, 0)])
+        self.assertCountEqual(BaseGeometry.hollow_rectangle(1, 1, 0, 0), [(0, 0), (0, 1), (1, 1), (1, 0)])
+        self.assertCountEqual(BaseGeometry.hollow_rectangle(4, 3, 7, 5), [(4, 3), (4, 4), (4, 5), (5, 5), (6, 5), (7, 5), (7, 4), (7, 3), (6, 3), (5, 3)])
+        self.assertCountEqual(BaseGeometry.hollow_rectangle(3, 3, 9, 9), [(3, 3), (9, 9), (9, 8), (9, 7), (9, 5), (9, 6), (9, 4), (9, 3), (7, 3), (3, 4),
                                                               (3, 5), (3, 6), (3, 7), (3, 8), (3, 9), (4, 9), (5, 9), (6, 9), (7, 9), (8, 9),
                                                               (8, 3), (6, 3), (4, 3), (5, 3)])
 
     # # Triangles
-
-
     def test_triangle(self):
         """ test for geometry.triangle """
-        for cell_shape in (geometry.FLAT_HEX, geometry.SQUARE):
-            self.assertRaises(TypeError, geometry.triangle, cell_shape, "a", 1, 1, 1, 1)
-            self.assertRaises(TypeError, geometry.triangle, cell_shape, 1, "a", 1, 1, 1)
-            self.assertRaises(TypeError, geometry.triangle, cell_shape, 1, 1, "a", 1, 1)
-            self.assertRaises(TypeError, geometry.triangle, cell_shape, 1, 1, 1, "a", 1)
-            self.assertRaises(ValueError, geometry.triangle, cell_shape, 1, 1, 1, 1, -1)
-        self.assertRaises(ValueError, geometry.triangle, 0, 1, 1, 1, 1, 1)
+        for geometry in (SquareGeometry, FHexGeometry):
+            self.assertRaises(ValueError, geometry.triangle, "a", 1, 1, 1, 1)
+            self.assertRaises(ValueError, geometry.triangle, 1, "a", 1, 1, 1)
+            self.assertRaises(ValueError, geometry.triangle, 1, 1, "a", 1, 1)
+            self.assertRaises(ValueError, geometry.triangle, 1, 1, 1, "a", 1)
+            self.assertRaises(ValueError, geometry.triangle, 1, 1, 1, 1, -1)
 
-        for i in geometry.ANGLES:
-            self.assertCountEqual(geometry.triangle(cell_shape, 0, 0, 0, 0, i), [(0, 0)])
+            for i in geometry.ANGLES:
+                self.assertCountEqual(geometry.triangle(0, 0, 0, 0, i), [(0, 0)])
 
     def test_triangle3d(self):
         """ test for geometry.triangle3d """
-        for cell_shape in (geometry.FLAT_HEX, geometry.SQUARE):
-            self.assertRaises(TypeError, geometry.triangle3d, cell_shape, "a", 1, 1, 1, 1, 1, 1)
-            self.assertRaises(TypeError, geometry.triangle3d, cell_shape, 1, "a", 1, 1, 1, 1, 1)
-            self.assertRaises(TypeError, geometry.triangle3d, cell_shape, 1, 1, "a", 1, 1, 1, 1)
-            self.assertRaises(TypeError, geometry.triangle3d, cell_shape, 1, 1, 1, "a", 1, 1, 1)
-            self.assertRaises(TypeError, geometry.triangle3d, cell_shape, 1, 1, 1, 1, "a", 1, 1)
-            self.assertRaises(TypeError, geometry.triangle3d, cell_shape, 1, 1, 1, 1, 1, "a", 1)
-            self.assertRaises(ValueError, geometry.triangle3d, cell_shape, 1, 1, 1, 1, 1, 1, -1)
-        self.assertRaises(ValueError, geometry.triangle3d, 0, 1, 1, 1, 1, 1, 1, 1)
-
-    def test_squ2_triangle(self):
-        """ test for geometry.squ2_triangle """
-        cell_shape = geometry.SQUARE
-        # TODO: check and validate
-
-#         # left to right
-#         # TODO: complete
-#
-#         # top to bottom
-#         # TODO: complete
-#
-#         # right to left
-#         # TODO: complete
-#
-#         # bottom to top
-#         # TODO: complete
-#
-#         # top left to bottom right
-#         # TODO: complete
-#
-#         # bottom right to top left
-#         # TODO: complete
-#
-#         # top right to bottom left
-#         # TODO: complete
-#
-#         # bottom right to top left
-#         # TODO: complete
-
-
-    def test_fhex2_triangle(self):
-        """ test for geometry.fhex2_triangle """
-        cell_shape = geometry.FLAT_HEX
-
+        for geometry in (SquareGeometry, FHexGeometry):
+            self.assertRaises(ValueError, geometry.triangle3d, "a", 1, 1, 1, 1, 1, 1)
+            self.assertRaises(ValueError, geometry.triangle3d, 1, "a", 1, 1, 1, 1, 1)
+            self.assertRaises(ValueError, geometry.triangle3d, 1, 1, "a", 1, 1, 1, 1)
+            self.assertRaises(ValueError, geometry.triangle3d, 1, 1, 1, "a", 1, 1, 1)
+            self.assertRaises(ValueError, geometry.triangle3d, 1, 1, 1, 1, "a", 1, 1)
+            self.assertRaises(ValueError, geometry.triangle3d, 1, 1, 1, 1, 1, "a", 1)
+            self.assertRaises(ValueError, geometry.triangle3d, 1, 1, 1, 1, 1, 1, -1)
+
+        # ## flat hex
         # left to right
-        self.assertCountEqual(geometry.triangle(cell_shape, 2, 3, 4, 3, 1), [(3, 3), (3, 4), (3, 3), (4, 5), (4, 4), (4, 3), (4, 2), (4, 1), (4, 1), (3, 1), (3, 2), (2, 3)])
-        self.assertCountEqual(geometry.triangle(cell_shape, 2, 3, 4, 3, 2), [(3, 3), (4, 4), (4, 3), (4, 2), (4, 2), (3, 2), (2, 3)])
-        self.assertCountEqual(geometry.triangle(cell_shape, 2, 3, 4, 3, 3), [(3, 3), (4, 4), (4, 3), (4, 2), (4, 2), (3, 2), (2, 3)])
-
-        # TODO: check and validate
-
-#         # top to bottom
-#         # TODO: complete
+        self.assertCountEqual(FHexGeometry.triangle(2, 3, 4, 3, 1), [(3, 3), (3, 4), (3, 3), (4, 5), (4, 4), (4, 3), (4, 2), (4, 1), (4, 1), (3, 1), (3, 2), (2, 3)])
+        self.assertCountEqual(FHexGeometry.triangle(2, 3, 4, 3, 2), [(3, 3), (4, 4), (4, 3), (4, 2), (4, 2), (3, 2), (2, 3)])
+        self.assertCountEqual(FHexGeometry.triangle(2, 3, 4, 3, 3), [(3, 3), (4, 4), (4, 3), (4, 2), (4, 2), (3, 2), (2, 3)])
 
         # right to left
-        self.assertCountEqual(geometry.triangle(cell_shape, 4, 3, 2, 3, 1), [(3, 2), (3, 1), (3, 2), (2, 1), (2, 2), (2, 3), (2, 4), (2, 5), (2, 5), (3, 4), (3, 3), (4, 3)])
-        self.assertCountEqual(geometry.triangle(cell_shape, 4, 3, 2, 3, 2), [(3, 2), (2, 2), (2, 3), (2, 4), (2, 4), (3, 3), (4, 3)])
-        self.assertCountEqual(geometry.triangle(cell_shape, 4, 3, 2, 3, 3), [(3, 2), (2, 2), (2, 3), (2, 4), (2, 4), (3, 3), (4, 3)])
-
-#         # bottom to top
-#         # TODO: complete
-
-#         # top left to bottom right
-#         # TODO: complete
-
-#         # bottom right to top left
-#         # TODO: complete
-
-#         # top right to bottom left
-#         # TODO: complete
-
-#         # bottom right to top left
-#         # TODO: complete
-
-
-    def test_squ3_triangle(self):
-        """ test for geometry.squ3_triangle """
-        cell_shape = geometry.SQUARE
-        # TODO: complete
-
-    def test_fhex3_triangle(self):
-        """ test for geometry.fhex3_triangle """
-        cell_shape = geometry.FLAT_HEX
-        # TODO: complete
-
+        self.assertCountEqual(FHexGeometry.triangle(4, 3, 2, 3, 1), [(3, 2), (3, 1), (3, 2), (2, 1), (2, 2), (2, 3), (2, 4), (2, 5), (2, 5), (3, 4), (3, 3), (4, 3)])
+        self.assertCountEqual(FHexGeometry.triangle(4, 3, 2, 3, 2), [(3, 2), (2, 2), (2, 3), (2, 4), (2, 4), (3, 3), (4, 3)])
+        self.assertCountEqual(FHexGeometry.triangle(4, 3, 2, 3, 3), [(3, 2), (2, 2), (2, 3), (2, 4), (2, 4), (3, 3), (4, 3)])
 
     # # Translations, rotations
-    def test_pivot(self):
-        """ test for geometry.pivot """
-        self.assertRaises(ValueError, geometry.pivot, 0, (0, 0), [(0, 0)], 1)
-
-        self.assertRaises(TypeError, geometry.pivot, 0, "a"    , [(0, 0)], 1)
-        self.assertRaises(ValueError, geometry.pivot, 0, ("a", 0), [(0, 0)], 1)
-
-        self.assertRaises(TypeError, geometry.pivot, 0, (0, 0), 0, 1)
-        self.assertRaises(ValueError, geometry.pivot, 0, (0, 0), ["a", (0, 0)], 1)
-        self.assertRaises(ValueError, geometry.pivot, 0, (0, 0), [("a", 0), (0, 0)], 1)
-
-        self.assertRaises(TypeError, geometry.pivot, 0, (0, 0), 1, "a")
-
-    def test_fhex2_pivot(self):
-        """ test for geometry.fhex2_pivot """
+    def test_rotate(self):
+        """ test for geometry.rotate """
+        for geometry in (SquareGeometry, FHexGeometry):
+            self.assertRaises(ValueError, geometry.rotate, 0, (0, 0), [(0, 0)], 1)
+            self.assertRaises(ValueError, geometry.rotate, 0, "a", [(0, 0)], 1)
+            self.assertRaises(ValueError, geometry.rotate, 0, ("a", 0), [(0, 0)], 1)
+            self.assertRaises(ValueError, geometry.rotate, 0, (0, 0), 0, 1)
+            self.assertRaises(ValueError, geometry.rotate, 0, (0, 0), ["a", (0, 0)], 1)
+            self.assertRaises(ValueError, geometry.rotate, 0, (0, 0), [("a", 0), (0, 0)], 1)
+            self.assertRaises(ValueError, geometry.rotate, 0, (0, 0), 1, "a")
+
+        # ## Flat hex
         attended = [
                     [(5, 5), (4, 5), (6, 6)],
                     [(5, 6), (4, 7), (6, 6)],
@@ -293,15 +250,12 @@ class Test(unittest.TestCase):
                     [(6, 5), (6, 4), (6, 6)],
                     [(5, 5), (4, 5), (6, 6)]
                    ]
-
-
         for i in range(len(attended)):
-            self.assertCountEqual(geometry.pivot(geometry.FLAT_HEX, (6, 6), [(6, 6)], i), [(6, 6)])
-            result = geometry.pivot(geometry.FLAT_HEX, (6, 6), [(5, 5), (4, 5), (6, 6)], i)
+            self.assertCountEqual(FHexGeometry.rotate((6, 6), [(6, 6)], i), [(6, 6)])
+            result = FHexGeometry.rotate((6, 6), [(5, 5), (4, 5), (6, 6)], i)
             self.assertCountEqual(result, attended[i])
 
-    def test_squ2_pivot(self):
-        """ test for geometry.squ2_pivot """
+        # ## Square
         attended = [
                     [(6, 6), (6, 5), (5, 5), (5, 6)],
                     [(6, 6), (5, 6), (5, 7), (6, 7)],
@@ -311,19 +265,9 @@ class Test(unittest.TestCase):
                    ]
 
         for i in range(len(attended)):
-            self.assertCountEqual(geometry.pivot(geometry.SQUARE, (6, 6), [(6, 6)], i), [(6, 6)])
-            result = geometry.pivot(geometry.SQUARE, (6, 6), [(6, 6), (6, 5), (5, 5), (5, 6)], i)
+            self.assertCountEqual(SquareGeometry.rotate((6, 6), [(6, 6)], i), [(6, 6)])
+            result = SquareGeometry.rotate((6, 6), [(6, 6), (6, 5), (5, 5), (5, 6)], i)
             self.assertCountEqual(result, attended[i])
 
-
-    # # cubic coordinates
-    def test_cv_cube_off(self):
-        """ test for geometry.cv_cube_off """
-        pass
-
-    def test_cv_off_cube(self):
-        """ test for geometry.cv_off_cube """
-        pass
-
 if __name__ == "__main__":
     unittest.main()

+ 95 - 84
tests/test_grid.py

@@ -4,97 +4,108 @@
 '''
 import unittest
 
-from pypog import geometry
-from pypog.grid_objects import Grid, SquareGrid, HexGrid
+from pypog.geometry_objects import SquareGeometry, FHexGeometry, BoundingRect
+from pypog.grid_objects import BaseGrid, SquareGrid, FHexGrid
 
 
 class Test(unittest.TestCase):
 
+    def assertObjEquals(self, obj1, obj2):
+        self.assertEqual(obj1.__dict__, obj2.__dict__)
+
     def test_init(self):
         # square grid
-        _ = Grid(geometry.SQUARE, 1, 1)
+        _ = BaseGrid(1, 1)
         _ = SquareGrid(1, 1)
-
-        # hex grid
-        _ = Grid(geometry.FLAT_HEX, 1, 1)
-        _ = HexGrid(1, 1)
-
-    def test_cell_shape(self):
-        grid = Grid(geometry.SQUARE, 1, 1)
-        self.assertEqual(grid.cell_shape, geometry.SQUARE)
-
-        grid.cell_shape = geometry.FLAT_HEX
-        self.assertEqual(grid.cell_shape, geometry.FLAT_HEX)
-
-        def _set_invalid_cell_shape():
-            grid.cell_shape = -1
-        self.assertRaises(ValueError, _set_invalid_cell_shape)
-
-    def test_dimensions(self):
-
-        for cls in (SquareGrid, HexGrid):
-            grid = cls(1, 1)
-            self.assertEqual(grid.height, 1)
-            self.assertEqual(grid.width, 1)
-
-            grid.height = 1000
-            self.assertEqual(grid.height, 1000)
-            grid.width = 1000
-            self.assertEqual(grid.width, 1000)
-
-            def _set_invalid_height():
-                grid.height = -1
-            self.assertRaises(ValueError, _set_invalid_height)
-
-            def _set_invalid_width():
-                grid.height = -1
-            self.assertRaises(ValueError, _set_invalid_width)
-
-    def test_cases_number(self):
-        for cls in (SquareGrid, HexGrid):
-            grid = cls(1, 1)
-            self.assertEqual(grid.cases_number(), 1)
-
-            grid.width = 100
-            grid.height = 100
-
-            self.assertEqual(grid.cases_number(), 10000)
-
-    def test_in_grid(self):
-
-        for cls in (SquareGrid, HexGrid):
-            grid = cls(10, 10)
-            self.assertTrue(grid.in_grid(5, 5))
-            self.assertFalse(grid.in_grid(11, 5))
-            self.assertFalse(grid.in_grid(5, 11))
-
-    def test_line(self):
-
-        # line algorithm is tested in tests.geometry.test_line
-
-        grid = SquareGrid(10, 10)
-
-        line = grid.line(0, 0, 0, 1)
-        self.assertEqual(line, [(0, 0), (0, 1)])
-
-    def test_line_3d(self):
-
-        # line algorithm is tested in tests.geometry.test_line
-
-        grid = HexGrid(10, 10)
-
-        line = grid.line3d(1, 1, 1, 1, 1, 1)
-        self.assertEqual(line, [(1, 1, 1)])
-
-
-    def test_hex_zone(self):
-
-        # zone algorithm is tested in tests.geometry.test_zone
-
-        grid = HexGrid(10, 10)
-        zone = grid.zone(0, 0, 0)
-        self.assertCountEqual(zone, [(0, 0)])
-
+        _ = FHexGrid(1, 1)
+        self.assertObjEquals(SquareGrid(1, 1), BaseGrid.from_geometry(SquareGeometry, 1, 1))
+        self.assertObjEquals(FHexGrid(1, 1), BaseGrid.from_geometry(FHexGeometry, 1, 1))
+        self.assertRaises(TypeError, BaseGrid.from_geometry, int, 1, 1)
+
+    def test_various(self):
+        self.assertEqual(str(BaseGrid(1, 1)), "<BaseGrid object>")
+        self.assertEqual(str(SquareGrid(1, 1)), "<SquareGrid object>")
+        self.assertEqual(str(FHexGrid(1, 1)), "<FHexGrid object>")
+
+    def test_properties(self):
+        grid = BaseGrid(1, 1)
+        self.assertEqual(grid.height, 1)
+        self.assertEqual(grid.width, 1)
+
+        grid.height = 1000
+        self.assertEqual(grid.height, 1000)
+        grid.width = 1000
+        self.assertEqual(grid.width, 1000)
+
+        def _set_invalid_height():
+            grid.height = -1
+        self.assertRaises(ValueError, _set_invalid_height)
+
+        def _set_invalid_width():
+            grid.width = -1
+        self.assertRaises(ValueError, _set_invalid_width)
+
+        self.assertEqual(grid.br, BoundingRect(0, 0, 999, 999))
+
+    def test_length(self):
+        grid = BaseGrid(1, 1)
+        self.assertEqual(len(grid), 1)
+
+        grid.width = 100
+        grid.height = 100
+        self.assertEqual(len(grid), 10000)
+
+    def test_contains(self):
+        grid = BaseGrid(10, 10)
+        self.assertTrue((5, 5) in grid)
+        self.assertFalse((11, 5) in grid)
+        self.assertFalse((5, 11) in grid)
+        self.assertFalse("a" in grid)
+
+    def test_iter(self):
+        grid = BaseGrid(2, 2)
+        self.assertCountEqual([(x, y) for x, y in grid], [(0, 0), (0, 1), (1, 0), (1, 1)])
+
+    def test_geometry(self):
+        # geometrics algorithms are properly tested in tests.test_geometry
+        square_grid = SquareGrid(10, 10)
+        fhex_grid = FHexGrid(10, 10)
+
+        args = (0, 0)
+        self.assertEqual(square_grid.neighbors(*args), SquareGeometry.neighbors(*args))
+        self.assertEqual(fhex_grid.neighbors(*args), FHexGeometry.neighbors(*args))
+
+        args = (0, 0, 3, 3)
+        self.assertEqual(square_grid.line(*args), SquareGeometry.line(*args))
+        self.assertEqual(fhex_grid.line(*args), FHexGeometry.line(*args))
+
+        args = (0, 0, 0, 3, 3, 3)
+        self.assertEqual(square_grid.line3d(*args), SquareGeometry.line3d(*args))
+        self.assertEqual(fhex_grid.line3d(*args), FHexGeometry.line3d(*args))
+
+        args = (0, 0, 1)
+        self.assertEqual(square_grid.zone(*args), SquareGeometry.zone(*args))
+        self.assertEqual(fhex_grid.zone(*args), FHexGeometry.zone(*args))
+
+        args = (0, 0, 2, 2 , 1)
+        self.assertEqual(square_grid.triangle(*args), SquareGeometry.triangle(*args))
+        self.assertEqual(fhex_grid.triangle(*args), FHexGeometry.triangle(*args))
+
+        args = (0, 0, 0, 2, 2, 2, 1)
+        self.assertEqual(square_grid.triangle3d(*args), SquareGeometry.triangle3d(*args))
+        self.assertEqual(fhex_grid.triangle3d(*args), FHexGeometry.triangle3d(*args))
+
+        args = (0, 0, 3, 3)
+        self.assertEqual(square_grid.rectangle(*args), SquareGeometry.rectangle(*args))
+        self.assertEqual(fhex_grid.rectangle(*args), FHexGeometry.rectangle(*args))
+
+        args = (0, 0, 3, 3)
+        self.assertEqual(square_grid.hollow_rectangle(*args), SquareGeometry.hollow_rectangle(*args))
+        self.assertEqual(fhex_grid.hollow_rectangle(*args), FHexGeometry.hollow_rectangle(*args))
+
+        args = ((5, 5), [(6, 6)], 1)
+        self.assertEqual(square_grid.rotate(*args), SquareGeometry.rotate(*args))
+        self.assertEqual(fhex_grid.rotate(*args), FHexGeometry.rotate(*args))
 
 if __name__ == "__main__":
     unittest.main()