#!/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()