"""Implementation of a hexagonal grid"""
import math
import serge.actor
import serge.common
import serge.input
import serge.events
import serge.blocks.layout
import serge.blocks.visualblocks
[docs]class OutOfRange(Exception): """The cell was out of range of the grid"""
# Events
E_CELL_LEFT_CLICK = 'cell-left-click'
E_CELL_RIGHT_CLICK = 'cell-right-click'
[docs]class HexagonalGrid(serge.common.Loggable):
"""A hexagonal grid
The grid is referenced by an x, y pair with 0, 0 being the top left.
"""
_odd_x_neighbour_offsets = [
(-1, 0), (0, -1), (1, 0), (-1, 1), (0, 1), (1, 1),
]
_even_x_neighbour_offsets = [
(-1, -1), (0, -1), (1, -1), (-1, 0), (0, 1), (1, 0),
]
_offset_ordinals = [
'nw', 'n', 'ne', 'sw', 's', 'se',
]
_offset_reversed = {
'nw': 'se', 'n': 's', 'ne': 'sw', 'sw': 'ne', 's': 'n', 'se': 'nw',
}
_points_offsets = [
(-0.5, -1.0), (0.5, -1.0),
(1.0, 0.0), (0.5, 1.0),
(-0.5, 1.0), (-1.0, 0.0),
]
def __init__(self, width, height, hexagon_size=1):
"""Initialise the grid"""
self.width = width
self.height = height
self.hexagon_size = hexagon_size
self.half_size = math.sqrt(3.0 / 4.0) * self.hexagon_size
#
self.grid = []
self.cell_centers = []
self.clearGrid()
[docs] def clearGrid(self):
"""Clear the grid"""
#
# Initialise the grid
for y in range(self.height + 1):
self.grid.append([])
for x in range(self.width + 1):
self.grid[-1].append(None)
px, py = self.getCellLocation(x, y)
self.cell_centers.append(((px, py), (x, y)))
[docs] def setCell(self, x, y, cell):
"""Set the cell contents"""
if not self.cellIsInGrid(x, y):
raise OutOfRange('Cell %s, %s is out of range of the grid' % (x, y))
#
self.grid[y][x] = cell
[docs] def getCell(self, x, y):
"""Return the cell contents"""
if not self.cellIsInGrid(x, y):
raise OutOfRange('Cell %s, %s is out of range of the grid' % (x, y))
#
return self.grid[y][x]
[docs] def cellIsInGrid(self, x, y):
"""Return True if the cell is in the grid"""
return 0 <= x < self.width and 0 <= y < self.height
def _getOffsetsForCell(self, x, y):
"""Return the neighbour offsets appropriate for a cell"""
return self._even_x_neighbour_offsets if x % 2 == 0 else self._odd_x_neighbour_offsets
def _getOffsetsDictionary(self, x, y):
"""Return a dictionary of offsets for a cell"""
return dict(zip(self._offset_ordinals, self._getOffsetsForCell(x, y)))
[docs] def getNeighbourLocations(self, x, y):
"""Return the neighbouring cells of a cell"""
if not self.cellIsInGrid(x, y):
raise OutOfRange('Cell %s, %s is out of range of the grid' % (x, y))
#
# Choose the right offsets
offsets = self._getOffsetsForCell(x, y)
#
neighbours = [(x + dx, y + dy) for dx, dy in offsets if self.cellIsInGrid(x + dx, y + dy)]
return neighbours
[docs] def getNeighbours(self, x, y):
"""Return the neighbour cells of the specified location"""
return [self.getCell(cx, cy) for cx, cy in self.getNeighbourLocations(x, y)]
[docs] def getNeighbourLocationsWithin(self, x, y, distance):
"""Return all the neighbour cell locations within a certain distance of the point"""
if not self.cellIsInGrid(x, y):
raise OutOfRange('Cell %s, %s is out of range of the grid' % (x, y))
#
# Go through all locations and find those within the distance range
matching_locations = []
for cx, cy in self.iterLocations():
cell_distance = self.getCellDistance((x, y), (cx, cy))
if 1 <= cell_distance <= distance:
matching_locations.append((cx, cy))
#
return matching_locations
[docs] def getNeighbourCellsWithin(self, x, y, distance):
"""Return all the neighbour cells within a certain distance of the point"""
return [self.getCell(cx, cy) for cx, cy in self.getNeighbourLocationsWithin(x, y, distance)]
[docs] def getRelativeCellCoords(self, x, y, direction):
"""Return the coordinates of the cell at a certain direction from this"""
dx, dy = self._getOffsetsDictionary(x, y)[direction]
cx, cy = x + dx, y + dy
#
if not self.cellIsInGrid(cx, cy):
raise OutOfRange('Cell %s, %s would be out of range of the grid' % (x, y))
else:
return cx, cy
[docs] def getRelativeCell(self, x, y, direction):
"""Return the cell at a relative offset to this one"""
return self.getCell(*self.getRelativeCellCoords(x, y, direction))
[docs] def getCellDirection(self, (x, y), (cx, cy)):
"""Get the direction of one cell from another"""
offsets = self._getOffsetsDictionary(x, y)
reverse_offsets = {}
for direction, (dx, dy) in offsets.iteritems():
reverse_offsets[x + dx, y + dy] = self._offset_reversed[direction]
try:
return reverse_offsets[cx, cy]
except KeyError:
raise OutOfRange('Cannot find direction of cell %s, %s from %s, %s' % (
x, y, cx, cy
))
[docs] def getCellLocation(self, x, y):
"""Return the location of cell center based on the top left being 0, 0"""
vertical_offset = self.half_size if x % 2 == 0 else 2 * self.half_size
#
return (
self.hexagon_size + self.hexagon_size * x * 1.5,
vertical_offset + self.half_size * y * 2.0
)
[docs] def getCellDistance(self, (x1, y1), (x2, y2)):
"""Return the distance between two cells"""
if x1 > x2:
return self.getCellDistance((x2, y2), (x1, y1))
#
dx = x2 - x1
dy = y2 - y1
x_odd = 1 if x1 % 2 == 1 else 0
x_even = 1 if not x_odd else 0
#
if dx == 0:
return abs(dy)
elif dy == 0:
return abs(dx)
elif dy >= 0:
return abs(dx) + abs(dy) - min(abs(dy), (abs(dx) + x_odd) // 2)
else:
return abs(dx) + abs(dy) - min(abs(dy), (abs(dx) + x_even) // 2)
[docs] def getCellCoordsContaining(self, px, py):
"""Return the coordinates of a cell containing the point"""
dist, (x, y) = self._getClosestCenter(px, py)
if dist <= self.hexagon_size:
return x, y
else:
raise OutOfRange('%s, %s is not in closest cell (%s, %s: dist=%s)' % (
px, py, x, y, dist
))
[docs] def getCellContaining(self, x, y):
"""Return the cell containing the point"""
return self.getCell(*self.getCellCoordsContaining(x, y))
def _getClosestCenter(self, px, py):
"""Return the closest cell center"""
distances = []
for (tx, ty), (x, y) in self.cell_centers:
distance = math.sqrt((px - tx) ** 2 + (py - ty) ** 2)
distances.append((distance, (x, y)))
distances.sort()
return distances[0]
[docs] def getCellPoints(self, x, y):
"""Return the points for the given cell"""
px, py = self.getCellLocation(x, y)
points = []
for dx, dy in self._points_offsets:
points.append((
px + dx * self.hexagon_size,
py + dy * self.half_size
))
#
return points
[docs] def iterLocations(self):
"""Iterate through the locations"""
for x in range(self.width):
for y in range(self.height):
yield (x, y)
[docs] def iterCells(self):
"""Iterate through the cells"""
for x, y in self.iterLocations():
yield self.getCell(x, y)
[docs]class HexGridCell(serge.actor.Actor):
"""A grid cell on the screen"""
def __init__(self, tag, name, location, grid, colour, stroke_colour=None, stroke_width=0):
"""Initialise the grid cell"""
super(HexGridCell, self).__init__(tag, name)
#
self.grid = grid
self.location = location
self.colour = colour
self.stroke_colour = stroke_colour
self.stroke_width = stroke_width
#
self.visual = serge.blocks.visualblocks.Polygon(
points=self.grid.getCellPoints(0, 0),
colour=self.colour,
stroke_colour=self.stroke_colour,
stroke_width=self.stroke_width
)
[docs]class HexGridDisplay(serge.blocks.layout.BaseGrid):
"""Displays and manages a hexagonal grid on the screen"""
def __init__(self, tag, name='', size=(1, 1), width=None,
height=None, background_colour=None, background_layer=None,
hexagon_size=1, cell_colour=(255, 255, 255), stroke_colour=(255, 255, 255),
stroke_width=1, cell_cls=HexGridCell):
"""Initialise the grid"""
#
self.hexagon_size = hexagon_size
self.cell_colour = cell_colour
self.stroke_width = stroke_width
self.stroke_colour = stroke_colour
self.cell_cls = cell_cls
self._mouse_over_cell = None
self._mouse_down = False
self._right_mouse_down = False
#
super(HexGridDisplay, self).__init__(
tag, name, size, width,
height, background_colour, background_layer)
[docs] def setGrid(self, size):
"""Set the new grid"""
self._overlay_grid = HexagonalGrid(size[0], size[1], self.hexagon_size)
#
for x, y in self._overlay_grid.iterLocations():
cell = self.cell_cls(
'grid-cell', 'cell-%s-%s' % (x, y),
(x, y),
self._overlay_grid,
self.cell_colour,
self.stroke_colour,
self.stroke_width
)
cell.moveTo(*self._overlay_grid.getCellLocation(x, y))
self.addChild(cell)
self._overlay_grid.setCell(x, y, cell)
def _redoLocations(self):
"""Reloate items in case we have moved"""
for x, y in self._overlay_grid.iterLocations():
cell = self._overlay_grid.getCell(x, y)
px, py = self._overlay_grid.getCellLocation(x, y)
cell.moveTo(self.x + px, self.y + py)
[docs] def addedToWorld(self, world):
"""Added to the world"""
super(HexGridDisplay, self).addedToWorld(world)
#
self.mouse = world.getEngine().getMouse()
[docs] def updateActor(self, interval, world):
"""Update the display"""
super(HexGridDisplay, self).updateActor(interval, world)
#
# Check if the mouse is over a cell
px, py = self.mouse.getScreenPos()
try:
cell = self._overlay_grid.getCellContaining(px - self.x, py - self.y)
except OutOfRange:
cell = None
#
if self._mouse_over_cell != cell:
if self._mouse_over_cell is not None:
self.processEvent((serge.events.E_MOUSE_LEAVE, self._mouse_over_cell))
self._mouse_over_cell = None
if cell is not None:
self._mouse_over_cell = cell
self.processEvent((serge.events.E_MOUSE_ENTER, cell))
elif self._mouse_over_cell is not None:
self.processEvent((serge.events.E_MOUSE_OVER, self._mouse_over_cell))
#
if self.mouse.isDown(serge.input.M_LEFT):
self._mouse_down = True
if self.mouse.isUp(serge.input.M_LEFT):
if self._mouse_down and self._mouse_over_cell:
self.processEvent((E_CELL_LEFT_CLICK, self._mouse_over_cell))
self._mouse_down = False
if self.mouse.isDown(serge.input.M_RIGHT) and self._mouse_over_cell:
self._right_mouse_down = True
if self.mouse.isUp(serge.input.M_RIGHT):
if self._right_mouse_down and self._mouse_over_cell:
self.processEvent((E_CELL_RIGHT_CLICK, self._mouse_over_cell))
self._right_mouse_down = False
[docs] def getNeighbourCells(self, cell):
"""Get the neighbours of a cell"""
return self._overlay_grid.getNeighbours(*cell.location)
[docs] def getNeighbourCellsWithin(self, x, y, distance):
"""Return the neighbour cells in a certain distance"""
return self._overlay_grid.getNeighbourCellsWithin(x, y, distance)
[docs] def getCell(self, x, y):
"""Return the cell"""
return self._overlay_grid.getCell(x, y)
[docs] def iterLocations(self):
"""Iterate through the cell locations"""
for cx, cy in self._overlay_grid.iterLocations():
yield cx, cy
[docs] def getGrid(self):
"""Return the underlying grid"""
return self._overlay_grid
[docs] def getCellDistance(self, loc1, loc2):
"""Return the distance between two cells"""
return self._overlay_grid.getCellDistance(loc1, loc2)