#!/usr/bin/python
"""Wires
This is a simple game where a set of tiles must be rotated so as to
form a simply connected graph. The game is also known as Net.
Written by Ask Hjorth Larsen, asklarsen@gmail.com
Copyright (C) 2007.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
"""
def makesections(strings):
"""
Concatenate strings with sections between each
"""
return ''.join([string+'\n\n' for string in strings]).strip()
# A quick hack so we don't have to rewrite all the docstring text
NAME, QUICK_DESCR, WRITTEN_BY, COPYRIGHT, GPL1, GPL2, GPL3 = \
__doc__.split('\n\n')
VERSION = 'beta 0.2'
AUTHOR = 'Ask Hjorth Larsen'
EMAIL = 'asklarsen@gmail.com'
INSTRUCTIONS = """
The objective of Wires is to make sure that all tiles are connected by
rotating each tile perspicaciously.
Use the mouse scroll wheel to rotate a tile.
Use the left mouse button to lock a tile, thus labelling it so as to
remember that it is not supposed to be turned around anymore.
Right-click on a tile to flood-fill all tiles connected to it.
Clear all flood-filled tiles by pressing the Clean button in the Game
menu.
If all tiles can be flood-filled with one right-click, then the game
is completed.
"""
import pygtk
pygtk.require('2.0')
import gtk
import random
import sys
class Wires:
def __init__(self, width, height, seed, shuffle, wrap):
self.model = Model(width, height, seed, wrap)
self.board = Board(self.model.tilemap, 55)
vbox = gtk.VBox()
vbox.show()
if wrap:
arrows = [gtk.Arrow(gtk.ARROW_UP, gtk.SHADOW_OUT),
gtk.Arrow(gtk.ARROW_DOWN, gtk.SHADOW_OUT),
gtk.Arrow(gtk.ARROW_LEFT, gtk.SHADOW_OUT),
gtk.Arrow(gtk.ARROW_RIGHT, gtk.SHADOW_OUT)]
directions = [(0,1), (0,-1), (1,0), (-1,0)]
buttons = [self.make_translator_button(dir, arrow)
for dir,arrow in zip(directions, arrows)]
arrowbox = gtk.Toolbar()
for button, arrow in zip(buttons,arrows):
arrowbox.append_widget(button, None, None)
arrow.show()
button.show()
arrowbox.show()
vbox.pack_start(arrowbox, False, False, 0)
vbox.pack_end(self.board.table, True, True, 0)
#self.frame.add(vbox)
self.component = vbox
if shuffle:
self.model.generator.randomize(None)
def make_translator_button(self, direction, arrow):
button = gtk.Button()
button.add(arrow)
tilemap = self.model.tilemap
translator = lambda a,b=None : tilemap.translate(*direction)
button.connect('clicked', translator)
return button
def clearmarks(self, widget, data=None):
for tile in self.model.tilemap.tiles:
tile.haschanged()
class GUI:
def __init__(self, wires):
w = gtk.Window(gtk.WINDOW_TOPLEVEL)
w.set_title(NAME+' '+VERSION)
w.connect('destroy', lambda a,b=None : gtk.main_quit())
w.connect('delete_event', lambda a,b=None : False)
self.frame = w
self.wires = wires
bar = self.make_menu_bar(w)
vbox = gtk.VBox()
vbox.show()
vbox.pack_start(bar, False, False, 0)
vbox.pack_end(wires.component, False, False, 0)
w.add(vbox)
def set_wires(self, wires):
self.wires = wires
def make_menu_bar(self, window):
bar = gtk.MenuBar()
bar.show()
gameitem, gamemenu = self.makemenu('Game', bar)
newitem = self.makemenuitem('New', gamemenu)
cleanitem = self.makemenuitem('Clean', gamemenu, self.wires.clearmarks)
quititem = self.makemenuitem('Quit', gamemenu,
lambda a,b=None : gtk.main_quit())
settingsitem, settingsmenu = self.makemenu('Settings', bar)
prefitem = self.makemenuitem('Preferences', settingsmenu)
helpitem, helpmenu = self.makemenu('Help', bar)
instructionsitem = self.makemenuitem('Instructions', helpmenu,
self.show_instructions_dialog)
aboutitem = self.makemenuitem('About', helpmenu,
self.show_about_dialog)
licenseitem = self.makemenuitem('License', helpmenu,
self.show_license_dialog)
return bar
def makemenuitem(self, name, menu=None, action=None):
item = gtk.MenuItem(name)
if action is not None:
item.connect('activate', action, None)
item.show()
if menu is not None:
menu.append(item)
return item
def makemenu(self, name, bar=None):
menu = gtk.Menu()
item = gtk.MenuItem(name)
item.set_submenu(menu)
item.show()
if bar is not None:
bar.append(item)
return item,menu
def showdialog(self, title, text):
dialog = gtk.Dialog(title, self.frame)
label = gtk.Label(text)
label.show()
button = gtk.Button('All right then')
button.show()
button.connect('clicked', lambda a,b: dialog.destroy(), None)
dialog.vbox.pack_start(label, True, True, 0)
dialog.action_area.pack_start(button, True, True, 0)
dialog.show()
def show_instructions_dialog(self, widget=None, data=None):
title = 'Instructions'
text = INSTRUCTIONS
self.showdialog(title, text)
def show_about_dialog(self, widget=None, data=None):
title = 'About '+NAME
text = NAME + ' ' + VERSION + ' by ' +AUTHOR + '.\n\n' + \
EMAIL + '\n\n' + QUICK_DESCR
self.showdialog(title, text)
def show_license_dialog(self, widget=None, data=None):
title = 'License'
text = makesections([COPYRIGHT+' '+AUTHOR+'.', GPL1, GPL2, GPL3])
self.showdialog(title, text)
class Tile:
def __init__(self, index=0, x=0, y=0):
self.index = index
self.group = index # Used to track connected tiles
self.x = x
self.y = y
self.orientation = 0
self.connections = [False]*DIRCOUNT
self.connectioncount = 0
self.neighbours = [None]*DIRCOUNT
self.mark = None # Used to fetch connected tiles with flood fill
self.locked = False
self.listener = None
def rotate(self, amount):
self.orientation = (self.orientation + amount) % DIRCOUNT
self.group = self.index
self.haschanged()
def isconnected(self, direction):
return self.connections[(direction - self.orientation) % DIRCOUNT]
def ismutuallyconnected(self, direction):
return (self.isconnected(direction)
and self.neighbours[direction].isconnected(direction+2))
def descriptor(self):
facing = ''.join([str(i) for i in map(int, self.connections)])
return ''.join(['Dir:',str(self.orientation),'\n',facing])
def connect(self, direction):
if self.isconnected(direction):
raise Exception('Already connected!')
self.connectioncount += 1
self.connections[direction] = True
nb = self.neighbours[direction]
nb.connectioncount += 1
nb.connections[-DIRCOUNT/2+direction] = True
self.haschanged()
nb.haschanged()
if self.group > nb.group:
self.set_new_group(nb.group)
elif nb.group > self.group:
nb.set_new_group(self.group)
else:
print '--------------------------------------------'
print 'WARNING: Tiles from same group connected!'
print ' Tiles', (self.x,self.y),'and',(nb.x,nb.y)
print '--------------------------------------------'
return nb
def markconnected(self, index):
if self.mark == index:
return #Already marked
else:
self.mark = index
for i in range(DIRCOUNT):
if self.ismutuallyconnected(i):
self.neighbours[i].markconnected(index)
self.haschanged()
self.mark = None
def toggle_lock(self):
self.locked = not self.locked
self.haschanged()
def set_new_group(self, group):
if group > self.group:
raise Exception('Requested group '+group+
', but is group '+self.group)
self.group = group
for isconnected, neighbour in zip(self.connections, self.neighbours):
if isconnected:
if neighbour.group > group:
neighbour.set_new_group(group)
def haschanged(self):
if self.listener != None:
self.listener.tilechanged(self)
class TileMap:
def __init__(self, width, height, wrap):
self.width = width
self.height = height
self.offsetx = 0
self.offsety = 0
self.tiles = [Tile(i, i%width, i/width) for i in range(width*height)]
if wrap:
self.get = self.get_wrap
else:
self.get = self.get_nowrap
# Fake tile used to distinguish board edge
# This is an unbelievably ugly hack which, perhaps, should be replaced
# before it causes trouble
self.edge_of_the_world = Tile(-1, -1, -1)
self.edge_of_the_world.ismutuallyconnected = lambda direction : False
for tile in self.tiles:
tile.neighbours[0] = self.get(tile.x + 1, tile.y)
tile.neighbours[1] = self.get(tile.x, tile.y - 1)
tile.neighbours[2] = self.get(tile.x - 1, tile.y)
tile.neighbours[3] = self.get(tile.x, tile.y + 1)
def get_wrap(self, x, y):
x, y = self.transform(x,y)
return self.tiles[ (x % self.width)
+(y % self.height) * self.width]
def get_nowrap(self, x, y):
if x < 0 or x >= self.width or y < 0 or y >= self.height:
return self.edge_of_the_world
return self.tiles[x + self.width*y]
def transform(self, x, y):
return (x + self.offsetx, y + self.offsety)
def translate(self, offsetx, offsety):
self.offsetx = (self.offsetx + offsetx) % self.width
self.offsety = (self.offsety + offsety) % self.height
for tile in self.tiles:
tile.haschanged()
def set(self, x, y, tile):
x, y = self.transform(x,y)
self.tiles[(x % self.width) + self.width*(y % self.height)] = tile
class Generator:
def __init__(self, seed=None, wrap=False):
self.seed = seed
if seed is None:
seed = random.randint(0,1<<64)
print 'Seed randomly selected:',seed
self.wrap = wrap
self.rand = random.Random(seed)
self.generate = self.generate_contiguous
self.edgedata = None
self.tilemap = None
def isvalidconnection(self, edge):
return not edge.vertex1.group == edge.vertex2.group
def randomize(self, widget, data=None):
for tile in self.tilemap.tiles:
tile.orientation = self.rand.randrange(0,4)
def generate_contiguous(self, width, height):
tilemap = TileMap(width, height, self.wrap)
self.tilemap = tilemap
rand = self.rand
vertices = list(tilemap.tiles)
candidatecount = 2*width*height
maxedges = width*height-1
edgecandidates = []
# Add all possible edges to candidate list
for i in range(width):
for j in range(height):
tile1 = tilemap.get(i,j)
edge1 = Edge(tile1, 0) # connection right
edge2 = Edge(tile1, 3) # connection down
edgecandidates.append(edge1)
edgecandidates.append(edge2)
if not self.wrap:
edgecandidates = [edge for edge in edgecandidates
if not edge.wraps()]
rand.shuffle(edgecandidates)
for edge in edgecandidates:
if self.isvalidconnection(edge):
edge.vertex1.connect(edge.direction)
return tilemap
class Edge:
def __init__(self, vertex1, direction):
self.direction = direction
self.vertex1 = vertex1
self.vertex2 = vertex1.neighbours[direction]
def wraps(self):
v1 = self.vertex1
v2 = self.vertex2
return abs(v2.x - v1.x) > 1 or abs(v2.y - v1.y) > 1
class EdgeGenerationData:
"""
A wrapper class used to hold different data during
map generation.
"""
def __init__(self, tilemap, vertices, maxedges, edges):
self.edges = edges
self.maxedges = maxedges
self.tilemap = tilemap
self.vertices = vertices
class Model:
"""
A wrapper object around a Generator and a TileMap
"""
def __init__(self, width, height, seed=0, wrap=False):
self.generator = Generator(seed, wrap)
self.tilemap = self.generator.generate(width,height)
class Board:
def __init__(self, tilemap, tileSize):
"""
Creates and returns a gtk.Table, the elements of which are
appropriately arranged TilePanels, thus representing the
entire board.
"""
self.tilemap = tilemap
self.panels = [None]*tilemap.width*tilemap.height
table = gtk.Table(rows=tilemap.height, columns=tilemap.width,
homogeneous=True)
self.table = table
self.mark = -1
for i in range(tilemap.width):
for j in range(tilemap.height):
tile = tilemap.get(i,j)
panel = TilePanel(self, i, j, tileSize)
self.panels[i + tilemap.width*j] = panel
panel.show()
table.attach(panel, i, i+1, j, j+1)
buttonlistener = ButtonListener(self, i, j, panel)
tile.listener = self
panel.set_events(gtk.gdk.SCROLL_MASK)
panel.connect('button_press_event',
buttonlistener.buttonPressed)
panel.connect('scroll_event', buttonlistener.rotate)
table.show()
self.table = table
def tilechanged(self, tile):
i = (tile.x - self.tilemap.offsetx) % self.tilemap.width
j = (tile.y - self.tilemap.offsety) % self.tilemap.width
self.get_panel(i,j).repaint()
def get_panel(self, x, y):
return self.panels[x + self.tilemap.width * y]
DIRCOUNT = 4
class TilePanel(gtk.DrawingArea):
"""
A panel which represents a single tile graphically
"""
def __init__(self, board, x, y, tileSize):
gtk.DrawingArea.__init__(self)
self.board = board
self.tilemap = board.tilemap
self.x = x
self.y = y
self.set_size_request(tileSize, tileSize)
self.width = 1
self.height = 1
self.pixmap = None
self.connect('configure_event', self.resize)
self.connect('expose_event', self.render)
#self.markedGroup = None
def resize(self, widget, event):
"""
Make new backbuffer and repaint it
"""
x, y, width, height = widget.get_allocation()
self.width = width
self.height = height
self.pixmap = gtk.gdk.Pixmap(widget.window, width, height)
self.repaint()
def repaint(self):
"""
Redraw widget backbuffer according to tile rotation or similar
"""
(widget, width, height) = (self, self.width, self.height)
x = width/5
y = height/5
style = widget.get_style()
selectedBG = style.bg_gc[gtk.STATE_SELECTED]
selectedFG = style.fg_gc[gtk.STATE_SELECTED]
BG = style.bg_gc[gtk.STATE_NORMAL]
FG = style.fg_gc[gtk.STATE_NORMAL]
insensitive = style.bg_gc[gtk.STATE_INSENSITIVE]
black = style.black_gc
white = style.white_gc
tile = self.tilemap.get(self.x, self.y)
locked = tile.locked
if locked:
wire = black
background = insensitive
component = selectedBG
marked = selectedBG
else:
wire = black
background = white
component = selectedBG
marked = selectedBG
self.pixmap.draw_rectangle(background, True, 0,0, width, height)
#If this is a dead end, draw a neat-looking blob
if tile.connectioncount == 1:
self.pixmap.draw_rectangle(component, True, x, y, 3*x, 3*y)
rec = self.pixmap.draw_rectangle
#Draw wires
if tile.isconnected(0):
rec(wire, True, 2*x, 2*y, width-2*x, y)
if tile.isconnected(1):
rec(wire, True, 2*x, 0, x, 3*y)
if tile.isconnected(2):
rec(wire, True, 0, 2*y, 3*x, y)
if tile.isconnected(3):
rec(wire, True, 2*x, 2*y, x, height-2*y)
#If the tile group is marked, draw smaller, nice-looking lines
draw_group_connection = True
if draw_group_connection and self.board.mark == tile.mark:
#if drawGroupConnection and tile.group == 0:
dx = x/3
dy = y/3
if tile.isconnected(0):
rec(marked, True, 2*x+dx, 2*y+dy, width-2*x, y-2*dy)
if tile.isconnected(1):
rec(marked, True, 2*x+dx, 0, x-2*dx, 3*y-dy)
if tile.isconnected(2):
rec(marked, True, 0, 2*y+dy, 3*x-dx, y-2*dy)
if tile.isconnected(3):
rec(marked, True, 2*x+dx, 2*y+dy, x-2*dx, 3*y-dy)
widget.queue_draw_area(0,0,width, height)
def render(self, widget, event):
"""
Render widget backbuffer to screen
"""
x, y, width, height = event.area
widget.window.draw_drawable(widget.get_style().fg_gc[gtk.STATE_NORMAL],
self.pixmap, x, y, x, y, width, height)
return False
class ButtonListener:
"""
Represents listeners which handle clicks/scroll events on some tile
"""
def __init__(self, board, x, y, panel):
self.panel = panel
self.board = board
self.tilemap = board.tilemap
self.x = x
self.y = y
def buttonPressed(self, widget, event, data=None):
if event.button == 1:
self.toggleLock(widget, event, data=None)
elif event.button == 3:
tile = self.get_tile()
self.board.mark = tile.index
tile.markconnected(tile.index)
def toggleLock(self, widget, event, data=None):
self.get_tile().toggle_lock()
def get_tile(self):
return self.tilemap.get(self.x, self.y)
def rotate(self, widget, event, data=None):
tile = self.get_tile()
if tile.locked:
return
if event.direction == gtk.gdk.SCROLL_DOWN:
amount = -1
else:
amount = +1
tile.rotate(amount)
self.panel.repaint()
def printInfo(self, widget, event, data=None):
tile = self.get_tile()
print tile.x, tile.y, tile.orientation, tile.connections
def tilechanged(self, tile):
self.panel.repaint()
def main():
"""
Runs wires after parsing command line arguments
"""
argc = len(sys.argv)-1
if argc >= 1 and sys.argv[1] == '-h':
print 'Wires',version+'.'
print
print 'USAGE: wires.py [] [] [noshuffle] []'
print
print 'Starts wires on a width*height board.'
print
print 'With the noshuffle argument, the graph will be generated but'
print 'left continuous.'
print
print 'If the seed parameter is specified, this will be the random'
print 'seed used to generate the graph.'
sys.exit(0)
width = 6
height = 6
seed = None
shuffle = True
wrap = False
if argc > 0:
width = int(sys.argv[1])
height = width
if argc > 1:
height = int(sys.argv[2])
if argc > 2:
args = sys.argv[3:]
if args.count('noshuffle') > 0:
shuffle = False
if args.count('seed') > 0:
seed = int(args[args.index('seed') + 1])
if args.count('wrap') > 0:
wrap = True
wires = Wires(width, height, seed, shuffle, wrap)
gui = GUI(wires)
gui.frame.show()
gtk.main()
if __name__ == '__main__':
main()