Przeglądaj źródła

refactoring ok, tests ok for grids and geometries

olivier.massot 8 lat temu
rodzic
commit
f658b305b4
8 zmienionych plików z 549 dodań i 586 usunięć
  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
 *.pyc
 .project
 .project
 .pydevproject
 .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 **
     ** By Cro-Ki l@b, 2017 **
 '''
 '''
 from math import sqrt
 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):
     def __repr__(self):
         return "<{} object>".format(self.__class__.__name__)
         return "<{} object>".format(self.__class__.__name__)
 
 
+    @classmethod
+    def instance(cls):
+        return cls()
+
     @staticmethod
     @staticmethod
-    def _assertCoordinates(*args):
+    def assertCoordinates(*args):
         """ raise a ValueError if the args are not (x, y) iterables, where x and y are integers
         """ raise a ValueError if the args are not (x, y) iterables, where x and y are integers
         usage:
         usage:
-            self._assertCoordinates((x1, y1), (x2, y2), ...)
+            self.assertCoordinates((x1, y1), (x2, y2), ...)
         """
         """
         try:
         try:
             if all([isinstance(i, int) for x, y in args for i in (x, y)]):
             if all([isinstance(i, int) for x, y in args for i in (x, y)]):
                 return
                 return
         except (TypeError, ValueError):
         except (TypeError, ValueError):
             pass
             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
     @staticmethod
     def graphicsitem(x, y, scale=120):
     def graphicsitem(x, y, scale=120):
         """ returns the list of the points which compose the (x, y) cell """
         """ returns the list of the points which compose the (x, y) cell """
@@ -97,40 +113,33 @@ class BaseGrid(object):
 
 
     # geometrical algorithms
     # geometrical algorithms
     @classmethod
     @classmethod
-    def neighbors(cls, x, y):
+    def neighbors(cls, x, y, br=IBoundingRect()):
         """ returns a list of the neighbors of (x, y) """
         """ 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")
         raise NotImplementedError("this method is abstract and should be reimplemented in subclasses")
 
 
     @classmethod
     @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")
         raise NotImplementedError("this method is abstract and should be reimplemented in subclasses")
 
 
     @classmethod
     @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)
         """ returns a line from (x1 ,y1, z1) to (x2, y2, z2)
         as a list of (x, y, z) coordinates """
         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)
         hoLine = cls.line(x1, y1, x2, y2)
         if z1 == z2:
         if z1 == z2:
             return [(x, y, z1) for x, y in hoLine]
             return [(x, y, z1) for x, y in hoLine]
         else:
         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]
             return [(hoLine[d][0], hoLine[d][1], z) for d, z in ligneZ]
 
 
     @classmethod
     @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)
         """ 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)])
         buffer = frozenset([(x, y)])
 
 
         for _ in range(0, radius):
         for _ in range(0, radius):
@@ -140,13 +149,13 @@ class BaseGrid(object):
         return list(buffer)
         return list(buffer)
 
 
     @classmethod
     @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
         """ return the list of the (x, y) coordinates in a triangle
         with (xa, ya) apex and (xh, yh) middle of the base """
         with (xa, ya) apex and (xh, yh) middle of the base """
         raise NotImplementedError("this method is abstract and should be reimplemented in subclasses")
         raise NotImplementedError("this method is abstract and should be reimplemented in subclasses")
 
 
     @classmethod
     @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
         """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
         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")
         raise NotImplementedError("this method is abstract and should be reimplemented in subclasses")
 
 
     @classmethod
     @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)"""
         """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
     @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)"""
         """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))
         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
     @classmethod
-    def rotate(cls, center, coordinates, rotations):
+    def rotate(cls, center, coordinates, rotations, br=IBoundingRect()):
         """ return the 'coordinates' list of (x, y) coordinates
         """ return the 'coordinates' list of (x, y) coordinates
         after a rotation of 'rotations' times around the (x, y) center """
         after a rotation of 'rotations' times around the (x, y) center """
         raise NotImplementedError("this method is abstract and should be reimplemented in subclasses")
         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
     @staticmethod
     def graphicsitem(x, y, scale=120):
     def graphicsitem(x, y, scale=120):
-        """ reimplemented from BaseGrid.graphicsitem """
+        """ reimplemented from BaseGeometry.graphicsitem """
         return  [
         return  [
                     (x * scale, y * scale), \
                     (x * scale, y * scale), \
                     ((x + 1) * scale, y * scale), \
                     ((x + 1) * scale, y * scale), \
@@ -204,17 +206,32 @@ class SquareGrid(BaseGrid):
                 ]
                 ]
 
 
     @classmethod
     @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
     @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
         Implementation of bresenham's algorithm
         """
         """
+        cls.assertCoordinates((x1, y1), (x2, y2))
+
         result = []
         result = []
 
 
         if (x1, y1) == (x2, y2):
         if (x1, y1) == (x2, y2):
@@ -249,8 +266,11 @@ class SquareGrid(BaseGrid):
         return result
         return result
 
 
     @classmethod
     @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):
         if (xa, ya) == (xh, yh):
             return [(xa, ya)]
             return [(xa, ya)]
 
 
@@ -300,11 +320,12 @@ class SquareGrid(BaseGrid):
         return result
         return result
 
 
     @classmethod
     @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)
         flat_triangle = cls.triangle(xa, ya, xh, yh, iAngle)
+
+        result = {}
         k = 1 / (iAngle * sqrt(3))
         k = 1 / (iAngle * sqrt(3))
 
 
         length = max(abs(xh - xa), abs(yh - ya))
         length = max(abs(xh - xa), abs(yh - ya))
@@ -330,8 +351,10 @@ class SquareGrid(BaseGrid):
 
 
 
 
     @classmethod
     @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:
         if coordinates == [center] or rotations % 4 == 0:
             return coordinates
             return coordinates
         x0, y0 = center
         x0, y0 = center
@@ -344,11 +367,9 @@ class SquareGrid(BaseGrid):
             result.append((xr, yr))
             result.append((xr, yr))
         return result
         return result
 
 
-class _HexGrid(BaseGrid):
+class HexGeometry(BaseGeometry):
     """ Base class for hexagonal grids classes
     """ Base class for hexagonal grids classes
     This class should be overridden """
     This class should be overridden """
-    def __init__(self, *args, **kwargs):
-        BaseGrid.__init__(self, *args, **kwargs)
 
 
     @staticmethod
     @staticmethod
     def cv_cube_off(xu, yu, zu):
     def cv_cube_off(xu, yu, zu):
@@ -391,20 +412,17 @@ class _HexGrid(BaseGrid):
     def distance_off(xa, ya, xb, yb):
     def distance_off(xa, ya, xb, yb):
         """ distance between A and B (offset coordinates)"""
         """ distance between A and B (offset coordinates)"""
         # 10 times quicker if no conversion...
         # 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))
         return max(abs(xua - xub), abs(yua - yub), abs(zua - zub))
 
 
 
 
-class FHexGrid(_HexGrid):
+class FHexGeometry(HexGeometry):
     """ Flat-hexagonal grid object """
     """ Flat-hexagonal grid object """
 
 
-    def __init__(self, *args, **kwargs):
-        _HexGrid.__init__(self, *args, **kwargs)
-
     @staticmethod
     @staticmethod
     def graphicsitem(x, y, scale=120):
     def graphicsitem(x, y, scale=120):
-        """ reimplemented from BaseGrid.graphicsitem """
+        """ reimplemented from BaseGeometry.graphicsitem """
         if x % 2 != 0:
         if x % 2 != 0:
             y += 0.5
             y += 0.5
         return [
         return [
@@ -417,16 +435,18 @@ class FHexGrid(_HexGrid):
                 ]
                 ]
 
 
     @classmethod
     @classmethod
-    def _neighbors(cls, x, y):
+    def neighbors(cls, x, y, br=IBoundingRect()):
         if x % 2 == 0:
         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)]
             return [(x, y - 1), (x + 1, y - 1), (x + 1, y), (x, y + 1), (x - 1, y), (x - 1, y - 1)]
         else:
         else:
             return [(x, y - 1), (x + 1, y), (x + 1, y + 1), (x, y + 1), (x - 1, y + 1), (x - 1, y)]
             return [(x, y - 1), (x + 1, y), (x + 1, y + 1), (x, y + 1), (x - 1, y + 1), (x - 1, y)]
 
 
     @classmethod
     @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 """
         Implementation of bresenham's algorithm """
+        cls.assertCoordinates((x1, y1), (x2, y2))
+
         if (x1, y1) == (x2, y2):
         if (x1, y1) == (x2, y2):
             return [(x1, y1)]
             return [(x1, y1)]
 
 
@@ -522,8 +542,11 @@ class FHexGrid(_HexGrid):
         return result
         return result
 
 
     @classmethod
     @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):
         if (xa, ya) == (xh, yh):
             return [(xa, ya)]
             return [(xa, ya)]
 
 
@@ -581,9 +604,9 @@ class FHexGrid(_HexGrid):
         return result
         return result
 
 
     @classmethod
     @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)
         flat_triangle = cls.triangle(xa, ya, xh, yh, iAngle)
 
 
         result = {}
         result = {}
@@ -596,7 +619,7 @@ class FHexGrid(_HexGrid):
 
 
         length = max(abs(xuh - xua), abs(yuh - yua), abs(zuh - zua))
         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
         # build a dict with X key and value is a list of Z values
         vertical_line_dict = {d:[] for d, z in vertical_line}
         vertical_line_dict = {d:[] for d, z in vertical_line}
@@ -617,8 +640,10 @@ class FHexGrid(_HexGrid):
         return result
         return result
 
 
     @classmethod
     @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:
         if coordinates == [center] or rotations % 6 == 0:
             return coordinates
             return coordinates
         x0, y0 = center
         x0, y0 = center
@@ -634,6 +659,3 @@ class FHexGrid(_HexGrid):
             xr, yr = cls.cv_cube_off(xru, yru, zru)
             xr, yr = cls.cv_cube_off(xru, yru, zru)
             result.append((xr, yr))
             result.append((xr, yr))
         return result
         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 **
     ** By Cro-Ki l@b, 2017 **
 '''
 '''
-from pypog import gridlib
+from pypog import geometry_objects
 
 
 
 
 class NotStartedException(Exception):
 class NotStartedException(Exception):
@@ -30,7 +30,7 @@ class BasePencil(object):
     def __init__(self, grid):
     def __init__(self, grid):
 
 
         # do we really need the grid ref? cell_shape could be enough?
         # 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))
             raise TypeError("'grid' should be a Grid object (given: {})".format(grid))
         self._grid = grid
         self._grid = grid
 
 

+ 152 - 208
tests/test_geometry.py

@@ -4,63 +4,100 @@
 
 
     ** By Cro-Ki l@b, 2017 **
     ** By Cro-Ki l@b, 2017 **
 '''
 '''
+from math import inf
 import unittest
 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):
     def test_zone(self):
         """ test for geometry.zone """
         """ 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), \
                                                             (2, 5), (5, 3), (1, 2), (3, 5), (3, 3), (4, 4), (3, 1), \
                                                             (4, 3), (2, 2), (3, 4), (2, 4), (5, 2)])
                                                             (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
     # # lines
 
 
     def test_line2d(self):
     def test_line2d(self):
         """ test for geometry.line """
         """ 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 = {
         attended = {
-                    geometry.FLAT_HEX:    {
+                    FHexGeometry:    {
                                       (1, 1, 1, 1): [(1, 1)],
                                       (1, 1, 1, 1): [(1, 1)],
                                       (0, 0, 1, 1): [(0, 0), (0, 1), (1, 1)],
                                       (0, 0, 1, 1): [(0, 0), (0, 1), (1, 1)],
                                       (1, 1, 0, 0): [(1, 1), (0, 1), (0, 0)],
                                       (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)]
                                       (3, 3, 3, 0): [(3, 3), (3, 2), (3, 1), (3, 0)]
                                      },
                                      },
 
 
-                    geometry.SQUARE: {
+                    SquareGeometry: {
                                       (1, 1, 1, 1): [(1, 1)],
                                       (1, 1, 1, 1): [(1, 1)],
                                       (0, 0, 0, 1): [(0, 0), (0, 1)],
                                       (0, 0, 0, 1): [(0, 0), (0, 1)],
                                       (0, 1, 0, 0): [(0, 1), (0, 0)],
                                       (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():
             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):
     def test_line3d(self):
         """ test for geometry.line3d """
         """ 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 = {
         attended = {
-                    geometry.FLAT_HEX:    {
+                    FHexGeometry:    {
                                       (1, 1, 1, 1, 1, 1) : [(1, 1, 1)],
                                       (1, 1, 1, 1, 1, 1) : [(1, 1, 1)],
                                       (1, 1, 0, 1, 1, 1) : [(1, 1, 0), (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)],
                                       (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)]
                                       (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, 1, 1, 1, 1) : [(1, 1, 1)],
                                       (1, 1, 0, 1, 1, 1) : [(1, 1, 0), (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)],
                                       (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():
             for args, result in tests.items():
-                line = geometry.line3d(cell_shape, *args)
+                line = geometry.line3d(*args)
                 self.assertEqual(line, result)
                 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
     # # Rectangles
     def test_rectangle(self):
     def test_rectangle(self):
         """ test for geometry.rectangle """
         """ 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):
     def test_hollow_rectangle(self):
         """ test for geometry.hollow_rectangle """
         """ 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),
                                                               (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)])
                                                               (8, 3), (6, 3), (4, 3), (5, 3)])
 
 
     # # Triangles
     # # Triangles
-
-
     def test_triangle(self):
     def test_triangle(self):
         """ test for geometry.triangle """
         """ 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):
     def test_triangle3d(self):
         """ test for geometry.triangle3d """
         """ 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
         # 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
         # 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
     # # 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 = [
         attended = [
                     [(5, 5), (4, 5), (6, 6)],
                     [(5, 5), (4, 5), (6, 6)],
                     [(5, 6), (4, 7), (6, 6)],
                     [(5, 6), (4, 7), (6, 6)],
@@ -293,15 +250,12 @@ class Test(unittest.TestCase):
                     [(6, 5), (6, 4), (6, 6)],
                     [(6, 5), (6, 4), (6, 6)],
                     [(5, 5), (4, 5), (6, 6)]
                     [(5, 5), (4, 5), (6, 6)]
                    ]
                    ]
-
-
         for i in range(len(attended)):
         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])
             self.assertCountEqual(result, attended[i])
 
 
-    def test_squ2_pivot(self):
-        """ test for geometry.squ2_pivot """
+        # ## Square
         attended = [
         attended = [
                     [(6, 6), (6, 5), (5, 5), (5, 6)],
                     [(6, 6), (6, 5), (5, 5), (5, 6)],
                     [(6, 6), (5, 6), (5, 7), (6, 7)],
                     [(6, 6), (5, 6), (5, 7), (6, 7)],
@@ -311,19 +265,9 @@ class Test(unittest.TestCase):
                    ]
                    ]
 
 
         for i in range(len(attended)):
         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])
             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__":
 if __name__ == "__main__":
     unittest.main()
     unittest.main()

+ 95 - 84
tests/test_grid.py

@@ -4,97 +4,108 @@
 '''
 '''
 import unittest
 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):
 class Test(unittest.TestCase):
 
 
+    def assertObjEquals(self, obj1, obj2):
+        self.assertEqual(obj1.__dict__, obj2.__dict__)
+
     def test_init(self):
     def test_init(self):
         # square grid
         # square grid
-        _ = Grid(geometry.SQUARE, 1, 1)
+        _ = BaseGrid(1, 1)
         _ = SquareGrid(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__":
 if __name__ == "__main__":
     unittest.main()
     unittest.main()