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())