Browse Source

implement Cell, triangle and rectangle algorithms

olinox 9 years ago
parent
commit
7618c9161c
6 changed files with 384 additions and 15 deletions
  1. 55 0
      core/Cell.py
  2. 27 9
      core/Grid.py
  3. 5 6
      core/bresenham.py
  4. 48 0
      core/cube_coords.py
  5. 22 0
      core/rectangle.py
  6. 227 0
      core/triangle.py

+ 55 - 0
core/Cell.py

@@ -0,0 +1,55 @@
+'''
+Created on 8 nov. 2016
+    Cell of a board game
+@author: olinox
+'''
+from core import constants
+
+
+class Cell(object):
+    def __init__(self, geometry, x, y, z = 0):
+        if not all(isinstance(value, int) for value in [x, y, z]):
+            raise TypeError("x, y and z should be integers")
+        self._geometry = geometry
+        self._x = x
+        self._y = y
+        self._z = z
+        self.__update_neighbours()
+
+    @property
+    def x(self):
+        return self._x
+    
+    @property
+    def y(self):
+        return self._y
+    
+    @property
+    def z(self):
+        return self._z
+    
+    @property
+    def coord(self):
+        return (self._x, self._y)
+    
+    @property
+    def coord3d(self):
+        return (self._x, self._y, self._z)
+    
+    def __repr__(self):
+        return "Cell {}".format(self.coord)
+    
+    def __update_neighbours(self):
+        """update the tuple of neighbours cells"""
+        x, y = self._x, self._y
+        if self._geometry == constants.HEXGRID:
+            if 1 == (x % 2):
+                self._neighbours = ( (x, y-1), (x+1, y), (x+1, y+1), (x,  y+1), (x-1, y+1), (x-1, y) )
+            else:
+                self._neighbours = ( (x, y-1), (x+1, y-1), (x+1, y), (x,  y+1), (x-1, y), (x-1, y-1) )
+        elif self._geometry == constants.SQUAREGRID:
+            self._neighbours = ( (x-1, y-1), (x, y-1), (x+1, y-1), \
+                                (x-1, y)  , (x, y-1), (x+1, y)  , \
+                                (x-1, y+1), (x, y+1),(x+1, y+1) )
+    
+    

+ 27 - 9
core/Grid.py

@@ -3,8 +3,9 @@ Created on 7 nov. 2016
     Game Grid
 @author: olinox
 '''
-from core import bresenham
+from core import bresenham, triangle, rectangle
 from core.constants import GRID_GEOMETRIES
+from core.triangle import ANGLES
 
 
 class Grid(object):
@@ -48,22 +49,39 @@ class Grid(object):
     def cases_number(self):
         return self.height * self.width
     
-    def line(self, *args):
-        if len(args) == 4:
-            x1, y1, x2, y2 = args
-            return bresenham.line2d(self.geometry, x1, y1, x2, y2)
-        if len(args) == 6:
-            x1, y1, z1, x2, y2, z2 = args
-            return bresenham.line3d(self.geometry, x1, y1, z1, x2, y2, z2)
+    def in_grid(self, x, y):
+        """return True if the coordinates are in the grid"""
+        return (x > 0 and x <= self._width and y > 0 and y <= self._height)
     
+    def line(self, x1, y1, x2, y2):
+        return bresenham.line2d(self.geometry, x1, y1, x2, y2)
     
+    def line3d(self, x1, y1, z1, x2, y2, z2):
+        return bresenham.line3d(self.geometry, x1, y1, z1, x2, y2, z2)
     
+    def triangle(self, xa, ya, xh, yh, iAngle):
+        return triangle.triangle(self.geometry, xa, ya, xh, yh, iAngle)
+    
+    def triangle3d(self, xa, ya, za, xh, yh, zh, iAngle):
+        return triangle.triangle3d(self.geometry, xa, ya, za, xh, yh, zh, iAngle)
+
+    def rect(self, x1, y1, x2, y2):
+        return rectangle.rect(x1, y1, x2, y2)
+    
+    def hollow_rect(self, x1, y1, x2, y2):
+        return rectangle.hollow_rect(x1, y1, x2, y2)
+
+
     
 if __name__ == '__main__':
     gr = Grid(5, 100, 100)
     print(gr.cases_number())
+    
     print(gr.line(1,1,5,10))
-    print(gr.line(1,1,1,5,10,10))
+    print(gr.line3d(1,1,1,5,10,10))
+    
+    print(gr.triangle(1,1,5,10,ANGLES[1]))
+    print(gr.triangle3d(1,1,1,5,10,10, ANGLES[1]))
     
     
     

+ 5 - 6
core/bresenham.py

@@ -6,16 +6,18 @@ Created on 7 nov. 2016
 @author: olinox
 '''
 from math import sqrt
-from core.constants import SQUAREGRID, HEXGRID
+
+from core import constants
+
 
 def line2d(geometry, x1, y1, x2, y2):
     """returns a line from x1,y1 to x2,y2
     grid could be one of the GRIDTYPES values"""
     if not all(isinstance(c, int) for c in [x1, y1, x2, y2]):
         raise TypeError("x1, y1, x2, y2 have to be integers")
-    if geometry == SQUAREGRID:
+    if geometry == constants.SQUAREGRID:
         return _brH(x1, y1, x2, y2)
-    elif geometry == HEXGRID: 
+    elif geometry == constants.HEXGRID: 
         return _brC(x1, y1, x2, y2)
     else:
         raise ValueError("'geometry' has to be a value from GRID_GEOMETRIES")
@@ -66,7 +68,6 @@ def squ_2d_line(x1, y1, x2, y2):
         return [(x1, y1)]
     return _brC(x1, y1, x2, y2)
     
-
 def squ_3d_line(x1, y1, z1, x2, y2, z2):
     """returns a line from x1,y1,z1 to x2,y2,z2 on an square grid
     line is a list of coordinates"""
@@ -79,8 +80,6 @@ def squ_3d_line(x1, y1, z1, x2, y2, z2):
         zLine = _brC(0, z1, (len(hoLine)-1), z2)
         return [(hoLine[d][0], hoLine[d][1], z) for d, z in zLine]
 
-
-    
 def _brC(x1, y1, x2, y2):
     """Line Bresenham algorithm for square grid"""
     result = []

+ 48 - 0
core/cube_coords.py

@@ -0,0 +1,48 @@
+'''
+Created on 8 nov. 2016
+    Cubic coordinates are used in some algorythms about hexagonal grids
+@author: olinox
+'''
+
+
+def cv_cube_off(xu, yu, zu):
+    """convert cubic coordinates (xu, yu, zu) in standards coordinates (x, y) [offset]"""
+    if not all(isinstance(c, int) for c in [xu, yu, zu]):
+        raise TypeError("!err: xu, yu et zu doivent etre des entiers")
+    y = int( xu + ( zu - (zu & 1) ) / 2 )
+    x = zu
+    return (x, y)        
+
+def cv_off_cube(x, y):
+    """converts standards coordinates (x, y) [offset] in cubic coordinates (xu, yu, zu)"""
+    if not all(isinstance(c, int) for c in [x, y]):
+        raise TypeError("!err: x et y doivent etre des entiers")
+    zu = x
+    xu = int( y - ( x - (x & 1) ) / 2 )
+    yu = int( -xu -zu )
+    return (xu, yu, zu)    
+
+def cube_round(x, y, z):
+    """returns the nearest cell (in cubic coords)
+    x, y, z can be floating numbers, no problem."""
+    rx, ry, rz = round(x), round(y), round(z)
+    x_diff, y_diff, z_diff = abs(rx - x), abs(ry - y), abs(rz - z)
+    if x_diff > y_diff and x_diff > z_diff:
+        rx = -ry-rz
+    elif y_diff > z_diff:
+        ry = -rx-rz
+    else:
+        rz = -rx-ry
+    return (rx, ry, rz)
+
+def hex_distance_cube(xa, ya, za, xb, yb, zb):
+    """returns the manhattan distance between the two cells"""
+    return max(abs(xa - xb), abs(ya - yb), abs(za - zb))
+
+def distance_off(xa, ya, xb, yb):
+        """ distance between A and B (offset coordinates)"""
+        # 10 times quicker if no conversion...
+        xua, yua, zua = cv_off_cube(xa, ya)
+        xub, yub, zub = cv_off_cube(xb, yb)
+        return max(abs(xua - xub), abs(yua - yub), abs(zua - zub))
+    

+ 22 - 0
core/rectangle.py

@@ -0,0 +1,22 @@
+'''
+Created on 8 nov. 2016
+    Rectangle algorythms
+    rectangles are assumed to be composed of vertical/horizontal sides
+@author: olinox
+'''
+
+def rect(x1, y1, x2, y2):
+    """return a list of cells in a rectangle between (X1, Y1), (X2, Y2)"""
+    if not all(isinstance(val, int) for val in [x1, y1, x2, y2]):
+        raise TypeError("x1, y1, x2, y2 should be integers")
+    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)]
+
+def hollow_rect(x1, y1, x2, y2):
+    """return a list of cells composing the sides of the rectangle between (X1, Y1), (X2, Y2)"""
+    if not all(isinstance(val, int) for val in [x1, y1, x2, y2]):
+        raise TypeError("x1, y1, x2, y2 should be integers")
+    return [(x, y) for x, y in rect(x1, y1, x2, y2)
+            if (x == x1 or x == x2 or y == y1 or y == y2)]
+    
+    

+ 227 - 0
core/triangle.py

@@ -0,0 +1,227 @@
+'''
+Created on 8 nov. 2016
+    Triangle algorithms
+@author: olinox
+'''
+
+from math import sqrt
+
+from core import bresenham, constants
+from core.cube_coords import cv_off_cube, cube_round, cv_cube_off
+
+
+ANGLES = [1, 2, 3]
+
+def triangle(geometry, xa, ya, xh, yh, iAngle):
+    if geometry == constants.SQUAREGRID:
+        return triangle_sq(xa, ya, xh, yh, iAngle)
+    elif geometry == constants.HEXGRID: 
+        return triangle_hex(xa, ya, xh, yh, iAngle)
+    else:
+        raise ValueError("'geometry' has to be a value from GRID_GEOMETRIES")
+
+def triangle3d(geometry, xa, ya, za, xh, yh, zh, iAngle):
+    if geometry == constants.SQUAREGRID:
+        return triangle_sq_3d(xa, ya, za, xh, yh, zh, iAngle)
+    elif geometry == constants.HEXGRID: 
+        return triangle_hex_3d(xa, ya, za, xh, yh, zh, iAngle)
+    else:
+        raise ValueError("'geometry' has to be a value from GRID_GEOMETRIES")
+
+def triangle_sq(xa, ya, xh, yh, iAngle):   
+    """Returns a list of (x, y) coordinates in a triangle
+    A is the top of the triangle, H if the middle of the base
+    (square grid)
+    """
+    if not all(isinstance(c, int) for c in [xa, ya, xh, yh]):
+        raise TypeError("xa, ya, xh, yh should be integers")
+    if not iAngle in ANGLES:
+        raise ValueError("iAngle should be one of the ANGLES values")
+    if (xa, ya) == (xh, yh):
+        return [(xa, ya)]    
+    
+    result = []
+    
+    # direction vector
+    dx_dir, dy_dir = xh - xa, yh - ya
+    
+    # normal vector
+    dx_n, dy_n = - dy_dir, dx_dir
+
+    # B and C positions
+    k = 1 / ( iAngle * sqrt(3) )
+    xb, yb = xh + (k * dx_n), yh + (k * dy_n)
+    xc, yc = xh + (-k * dx_n), yh + (-k * dy_n)
+    
+    xb, yb = round(xb), round(yb)
+    xc, yc = round(xc), round(yc)
+
+    # sides:
+    lines = [(xa, ya, xb, yb), (xb, yb, xc, yc), (xc, yc, xa, ya)]
+    
+    # base (lower slope)
+    x1, y1, x2, y2 = min(lines, key=lambda x: (abs ( (x[3] - x[1]) / (x[2] - x[0]) ) if x[2] != x[0] else 10**10))
+    base = bresenham.squ_2d_line(x1, y1, x2, y2)
+    y_base = y1
+    lines.remove( (x1, y1, x2, y2) )
+    
+    # 'hat' (2 other sides)
+    hat = []
+    y_top = None
+    for x1, y1, x2, y2 in lines:
+        if y_top == None: 
+            y_top = y2
+        hat.extend( bresenham.squ_2d_line(x1, y1, x2, y2) )
+    
+    # sense (1 if top is under base, -1 if not)
+    sense = 1 if y_top > y_base else -1
+    
+    # rove over y values from base to hat
+    for x, y in base:
+        while not (x, y) in hat:
+            result.append( (x, y) )
+            y += sense
+    result.extend(hat)
+
+    return result
+
+def triangle_sq_3d(xa, ya, za, xh, yh, zh, iAngle):
+    """returns a dictionnary {coord: (-dh, +dh)}
+    coord keys are the cells in the triangle, (-dh, +dh) value is the vertical amplitude"""
+    if not all(isinstance(c, int) for c in [xa, ya, xh, yh]):
+        raise TypeError("xa, ya, za, xh, yh have to be integers")
+    if not iAngle in ANGLES:
+        raise ValueError("iAngle should be one of the ANGLES values")
+    if (xa, ya) == (xh, yh):
+        return [(xa, ya)]  
+
+    result = {} 
+    
+    flat_triangle = triangle_sq(xa, ya, xh, yh, iAngle)
+    k = 1 / ( iAngle * sqrt(3) )
+
+    length = max( abs(xh - xa), abs(yh - ya) )
+
+    vertical_line = bresenham.squ_2d_line(0, za, length, zh)
+    
+    #on cree un dictionnaire ou x est la cle, et ou la valeur est une liste de z
+    vertical_line_dict = {d:[] for d, z in vertical_line}
+    for d, z in vertical_line:
+        vertical_line_dict[d].append(z)
+        
+    #approximation: on met a jour la hauteur en fonction de la distance au centre
+    for x, y in flat_triangle:
+        distance = int( max( abs(x - xa), abs(y - ya) ) )
+        try:
+            z_list = vertical_line_dict[ distance ]
+        except KeyError:
+            distance = length
+            z_list = vertical_line_dict[ distance ]
+        dh = int( k * distance ) + 1 if distance > 0 else 0
+        result[ (x, y) ] = ( (min(z_list) - dh) , (max(z_list) + dh) ) 
+    return result
+
+
+def triangle_hex(xa, ya, xh, yh, iAngle):   
+    """Returns a list of (x, y) coordinates in a triangle
+    A is the top of the triangle, H if the middle of the base
+    (hexagonal grid)
+    """
+    if not all(isinstance(c, int) for c in [xa, ya, xh, yh]):
+        raise TypeError("xa, ya, xh, yh should be integers")
+    if not iAngle in [1, 2, 3]:
+        raise ValueError("iAngle should be one of the ANGLES values")
+    if (xa, ya) == (xh, yh):
+        return [(xa, ya)]    
+    
+    result = []
+    
+    # convert to cubic coodinates (see 'cube_coords' lib)
+    xua, yua, _ = cv_off_cube( xa, ya )
+    xuh, yuh, zuh = cv_off_cube( xh, yh )
+    
+    # direction vector
+    dx_dir, dy_dir = xuh - xua, yuh - yua
+    
+    # normal vector
+    dx_n, dy_n = - (2* dy_dir + dx_dir ), (2* dx_dir + dy_dir ) 
+    dz_n = (- dx_n - dy_n)        
+
+    # B and C positions
+    k = 1 / ( iAngle * sqrt(3) )
+    xub, yub, zub = xuh + (k * dx_n), yuh + (k * dy_n), zuh + (k * dz_n)
+    xuc, yuc, zuc = xuh + (-k * dx_n), yuh + (-k * dy_n), zuh + (-k * dz_n)
+    
+    xub, yub, zub = cube_round(xub, yub, zub)
+    xuc, yuc, zuc = cube_round(xuc, yuc, zuc)
+    
+    xb, yb = cv_cube_off(xub, yub, zub)
+    xc, yc = cv_cube_off(xuc, yuc, zuc)
+
+    # sides
+    segments = [(xa, ya, xb, yb), (xb, yb, xc, yc), (xc, yc, xa, ya)]
+    
+    # base (lower slope)
+    x1, y1, x2, y2 = min(segments, key=lambda x: (abs ( (x[3] - x[1]) / (x[2] - x[0]) ) if x[2] != x[0] else 10**10))
+    base = bresenham.hex_2d_line(x1, y1, x2, y2)
+    y_base = y1
+    segments.remove( (x1, y1, x2, y2) )
+    
+    # 'hat' (the 2 other sides)
+    chapeau = []
+    y_sommet = None
+    for x1, y1, x2, y2 in segments:
+        if y_sommet == None: 
+            y_sommet = y2
+        chapeau.extend( bresenham.hex_2d_line(x1, y1, x2, y2) )
+    
+    # sense (1 if top is under base, -1 if not)
+    sens = 1 if y_sommet > y_base else -1
+    
+    # rove over y values from base to hat
+    for x, y in base:
+        while not (x, y) in chapeau:
+            result.append( (x, y) )
+            y += sens
+    result.extend(chapeau)
+
+    return result
+
+def triangle_hex_3d(xa, ya, za, xh, yh, zh, iAngle):
+    """returns a dictionnary {coord: (-dh, +dh)}
+    coord (x,y) keys are the cells in the triangle, 
+    (-dh, +dh) value is the vertical amplitude"""
+    flat_trangle = triangle_hex(xa, ya, xh, yh, iAngle)
+    
+    if (xa, ya) == (xh, yh):
+        return [(xa, ya)]   
+    result = {} 
+    
+    k = 1 / ( iAngle * sqrt(3) )
+    
+    xua, yua, zua = cv_off_cube(xa, ya)
+    xuh, yuh, zuh = cv_off_cube(xh, yh)
+    
+    length = max( abs(xuh - xua), abs(yuh - yua), abs(zuh - zua) )
+
+    vertical_line = bresenham.squ_2d_line(0, za, length, zh)
+    
+    #on cree un dictionnaire ou x est la cle, et ou la valeur est une liste de z
+    vertical_line_dict = {d:[] for d, z in vertical_line}
+    for d, z in vertical_line:
+        vertical_line_dict[d].append(z)
+        
+    #approximation: on met a jour la hauteur en fonction de la distance au centre
+    for x, y in flat_trangle:
+        xu, yu, zu = cv_off_cube(x, y)
+        distance = int( max( abs(xu - xua), abs(yu - yua), abs(zu - zua) ) )
+        try:
+            z_list = vertical_line_dict[ distance ]
+        except KeyError:
+            distance = length
+            z_list = vertical_line_dict[ distance ]
+        dh = int( k * distance ) + 1 if distance > 0 else 0
+        result[ (x, y) ] = ( (min(z_list) - dh) , (max(z_list) + dh) ) 
+    return result
+
+