Posts Tagged ‘geometry’

Generating grids to use as go boards

Posted in Programming on December 29th, 2009 by Mat – Be the first to comment

So I decided to go back to my python go game I started many months ago. I have several other things I want to work on, most of which also involve go, but for some reason this interests me the most at the moment, and I haven’t done any python programming in a while so I’m going to put them on hold.

One of the main things I wanted to do with the game was to allow for playing on many different types of board, rather than limit it to the boring old 19×19 square kind, so I’ve written a module with several different classes which can be used to create board objects.

The rules of go are very simple, and are pretty much independent of the board shape. To work out what happens to the rest of the board when you place a stone, all you really need to know is how the points are connected to each other and where all the stones are. This means that an arbitrary graph can be used as the go board and what the board actually looks like is irrelevant. You can easily play go on 3D structures such as diamond structures or invent some strange irregular shape if you want. This makes it pretty easy to seperate the board structure from the game logic.

I’ve decided to limit myself to regular structures for the time being, and have stuck to 2 dimensions as I’m too lazy to mess around with 3D visualizations. I’ve implemented a regular rectangular board a board which allows the edges to be wrapped pacman style, which can be used to simulate playing on a the surface of a cylinder or torus (donut shape). To test it works ok I’ve whipped up a simple view in pygame which shows the neighbours of each point when you click on it.

The next step is to rewrite my go game around this module and create a usable gui for it.

'''Classes for modelling the goboard itself, i.e. an arrangement of points with lines connecting them. No game logic is included.
Grid/lattice coordinates should all be integers'''
from itertools import izip
import sys

#Move this somewhere else later
class Observable:
    '''Implement observer pattern'''

    def __init__(self):
        self.listeners = []

    def register_listener(self, listener):
        self.listeners.append(listener) #this object has synasthaesia

    def remove_listener(self, listener):
        self.listeners.remove(listener)

    def notify(self, *args):
        for i in self.listeners:
            if args:
                i(args)
            else:
                i()

class Grid(Observable):
    '''A representation of a 2D grid with each point holding some value.
    Stores the relative location of each point and the lines connecting them.
    Lattice is a list of points representing the lattice structure the grid is based around.
    Basis is a tuple of coordinates of the grid points relative to the lattice points.
    Connections is a list of 4-tuples of the form (x1,y1,x2,y2) indicating how points in the basis are connected'''

    def __init__(self, lattice, basis, connections, default_value=0):
        '''Store points and connections'''
        Observable.__init__(self)
        self.points = {}
        self.connections = {}

        for lx,ly in lattice.items():

            #Add the points by superimposing the basis onto each lattice point
            for b in basis:
                bx, by = b
                self.points[(lx+bx, ly+by)] = default_value
                self.connections[(lx+bx, ly+by)] = []

        #Now connect the points
        for lx,ly in lattice.items():

            try:
                for c in connections:
                    cx1, cy1, cx2, cy2 = c
                    a = (lx+cx1, ly+cy1)
                    b = (lx+cx2, ly+cy2)
                    if a in self.points and b in self.points:
                        self.connections[a].append(b)
                        self.connections[b].append(a)
                    else:
                        print 'Missing points for connection '+str(a)+'-'+str(b)
                        if a not in self.points: print a
                        if b not in self.points: print b
            except KeyError:
                raise Exception('Invalid connections')

    def neighbours(self, x, y):
        try:
            return self.connections[(x,y)]
        except KeyError:
            raise Exception('Bad grid coordinates')

    def set_point(self, x, y, val):
        try:
            self.points[(x,y)] = val
            self.notify() #notify observers on changes
        except KeyError:
            raise Exception('Bad grid coordinates')

class RectangularGrid(Grid):
    '''A rectangular grid. Aspect ratio should be a natural number (horizontal spacing >= vertical spacing)'''

    def __init__(self, width, height, aspect_ratio=1):
        aspect_ratio = int(aspect_ratio) #no floats please!
        lattice = RectangularLattice(width, height, aspect_ratio)
        basis = ((0, 0),)
        connections = ((0, 0, 1, 0), (0, 0, 0, 1))
        Grid.__init__(self, lattice, basis, connections)

class FoldedGrid(RectangularGrid):
    '''A rectangular grid where sides connect with each other.
    Joins is a list of tuples indicating which sides are joined, where each of the two values can be N, E, S or W
    Reverse_joins is the same except there is also a twist, so one side can map to another side reversed (or the same side)'''

    def __init__(self, width, height, aspect_ratio, joins = [], reverse_joins = []):
        RectangularGrid.__init__(self, width, height, aspect_ratio)

        #Lists of coordinates making up each side
        east = []
        west = []
        north = []
        south = []

        #East/West coordinates
        x1 = 0
        x2 = (width - 1) * aspect_ratio
        for i in range(height):
            east.append((x1, i))
            west.append((x2, i))

        #North/South coordinates
        y1 = 0
        y2 = height - 1
        for i in range(width):
            i *= aspect_ratio
            north.append((i, y1))
            south.append((i, y2))

        sides = {'E':east, 'W':west, 'N':north, 'S':south}

        #Make the extra connections
        try:
            for sideA, sideB in joins:
                #Zip the two sides together to get a tuple of tuples (coordinates) for each connected pair
                for c1, c2 in izip(sides[sideA], sides[sideB]):
                    self.connections[c1].append(c2)
                    self.connections[c2].append(c1)

            for sideA, sideB in reverse_joins:
                #This time the second side is reversed!
                for c1, c2 in izip(sides[sideA], reversed(sides[sideB])):
                    print str(c1) + '-' + str(c2)
                    self.connections[c1].append(c2)
                    self.connections[c2].append(c1)

        except KeyError:
            raise Exception('Invalid value for join or reverse_join')

class RectangularLattice:
    '''List of coordinates of points arranged in a grid'''

    def __init__(self, width, height, aspect_ratio, scale=1):
        aspect_ratio = int(aspect_ratio) #no floats please
        self.points = []
        for i in range(width):
            for j in range(height):
                j *= aspect_ratio
                self.points.append((i,j))

    def items(self):
        for i in self.points:
            yield i

class GridViewPygame:
    '''Draw grids using pygame'''

    def __init__(self, grid, surface, scale=None, rotation=0, zoom_to_fit=False, join_connected=False, highlight_connected=True):
        grid_width = max([x for x,y in grid.points])
        grid_height = max([y for x,y in grid.points])
        width = surface.get_width()
        height = surface.get_height()

        if zoom_to_fit:
            scale = min(width/float(grid_width), height/float(grid_height))
        elif scale is None:
            scale = 1

        self.scale = scale
        self.grid = grid
        self.surface = surface
        self.grid.register_listener(self.draw)
        self.highlighted = None

        self.highlight_connected = highlight_connected
        self.join_connected = join_connected

    def draw(self, *args):
        '''Draw the grid'''
        print 'redrawing'
        scale = self.scale
        self.surface.fill((255,255,255))

        for p in self.grid.points:
            x,y = p
            x *= scale
            y *= scale
            if (p == self.highlighted):
                pygame.draw.circle(self.surface, (0,0,255), (x,y), 3)
            else:
                pygame.draw.circle(self.surface, (0,0,0), (x,y), 2)

        #This actually draws the line for each connection twice but never mind
        if self.join_connected:
            for a,bs in self.grid.connections.items():
                for b in bs:
                    ax,ay = a
                    bx,by = b
                    ax *= scale
                    ay *= scale
                    bx *= scale
                    by *= scale
                    if (a == self.highlighted or b == self.highlighted):
                        pygame.draw.line(self.surface, (0,0,255), (ax,ay), (bx,by))
                    else:
                        pygame.draw.line(self.surface, (0,0,0), (ax,ay), (bx,by))

        if self.highlight_connected and self.highlighted:
            for x,y in self.grid.connections[self.highlighted]:
                x *= scale
                y *= scale
                pygame.draw.circle(self.surface, (0,255,0), (x,y), 2)

        pygame.display.flip()

    def board_to_grid(self, pos):
        '''Convert view coordinates to grid ones'''
        x,y = pos
        x = int(round(x/self.scale))
        y = int(round(y/self.scale))
        return (x,y)

    def onClick(self, pos):
        '''Highlight the nearest point'''
        pos = self.board_to_grid(pos)
        print pos
        self.highlighted = pos
        self.draw(False,True)

if __name__ == '__main__':

    #Initialize pygame
    import pygame
    pygame.init()
    from pygame.locals import *
    screen = pygame.display.set_mode((800, 600))

    #Create grid
#    grid = RectangularGrid(19,19)
    grid = FoldedGrid(19,19,1,[('N','S'),('E','W')])
#    grid = FoldedGrid(19,19,1,[],[('N','S'),('E','W')])
    view = GridViewPygame(grid, screen, zoom_to_fit=True)
    view.draw()

    #Handle events
    while True:
        for event in pygame.event.get():
            if event.type == QUIT:
                pygame.quit()
                sys.exit()
            elif event.type == MOUSEBUTTONDOWN:
                view.onClick(pygame.mouse.get_pos())