From 65884d06c39bde9e109c6f1235623f35f97f7950 Mon Sep 17 00:00:00 2001 From: Andrés Ambrois Date: Fri, 15 Aug 2008 06:29:34 +0000 Subject: Added new v2 code. New Features: - Cleaner code - Save and resume games from Journal - Added a toolbar with options to set board size & restart the game. - Better collaboration (turn enforcement, notifications) - Pass and Undo in hotseat (not shared) mode. - Full Spanish translation --- diff --git a/MANIFEST b/MANIFEST new file mode 100644 index 0000000..68f8d82 --- /dev/null +++ b/MANIFEST @@ -0,0 +1,17 @@ +activity.py +gametoolbar.py +collaboration.py +boardwidget.py +gogame.py +infopanel.py +activity/activity-go.svg +activity/activity.info +images/b.gif +images/black.gif +images/board.gif +images/BsTurn.gif +images/bw.gif +images/w.gif +images/white.gif +images/WsTurn.gif +images/gtk-refresh.svg diff --git a/NEWS b/NEWS new file mode 100644 index 0000000..2d48ff4 --- /dev/null +++ b/NEWS @@ -0,0 +1,13 @@ +2 +* Almost complete rewrite by aa. New features: + - Cleaner code + - Save and resume games from Journal + - Different Board sizes + - Restart game button + - Better collaboration (turn enforcement, notifications) + - Pass and Undo in hotseat (not shared) mode. + - Full Spanish translation + +1 +* New activity PlayGo written by Gerard J. Cerchio (www.circlesoft.com) + diff --git a/README b/README new file mode 100644 index 0000000..9efab78 --- /dev/null +++ b/README @@ -0,0 +1,9 @@ +PlayGo activity for the OLPC + +Authors: Gerard J. Cerchio, + www.circlesoft.com + Andrés Ambrois (aa) + andresambrois@gmail.com + +Copyright under the GNU GPL + diff --git a/TODO b/TODO new file mode 100644 index 0000000..97fe2ec --- /dev/null +++ b/TODO @@ -0,0 +1,8 @@ + TODO: + - Support spectators + - Add a pretty panel to display who's turn it is, and who is spectating + - Stop the game after two passes, and detect a winner. + - Add Atari Go mode. + - Add a 'Help' button that will take you to the PlayGo wiki page. + - Integrate with GnuGO + - Add different types of Ko and different rulesets. diff --git a/activity.py b/activity.py new file mode 100644 index 0000000..f95ae67 --- /dev/null +++ b/activity.py @@ -0,0 +1,288 @@ +# -*- coding: UTF-8 -*- +# Copyright 2007-2008 One Laptop Per Child +# Copyright 2007 Gerard J. Cerchio +# Copyright 2008 Andrés Ambrois +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import logging +import sugar.logger + +from gettext import gettext as _ + +import cPickle +import gtk +from sugar.activity.activity import Activity, ActivityToolbox + +from gametoolbar import GameToolbar +from gogame import GoGame +import boardwidget +import infopanel +from collaboration import CollaborationWrapper + + +logger = logging.getLogger('PlayGo') + +DEFAULT_SIZE = 19 + +class PlayGo(Activity): + def __init__(self, handle): + # Initialize the parent + Activity.__init__(self, handle) + logger.debug('Initiating PlayGo') + + self.size = DEFAULT_SIZE + + # Set the activity toolbox + toolbox = ActivityToolbox(self) + self.set_toolbox(toolbox) + self.gameToolbar = GameToolbar(self) + toolbox.add_toolbar(_('Game'), self.gameToolbar) + self.gameToolbar.connect('game-restart', self.restart_game) + self.gameToolbar.connect('game-board-size', self.board_size_change) + self.gameToolbar.show() + + # Initialize the game + self.game = GoGame(self.size) + self.CurrentColor = 'B' + self.PlayerColor = 'B' + self.set_up_ui() + + if not handle.object_id: + self.infopanel.show(_('Welcome to PlayGo!')) + else: + self.show_score() + self.lastX = -1 + self.lastY = -1 + + #Set up collaboration + self.collaboration = CollaborationWrapper(self, + self.buddy_joined, + self.buddy_left, + self.Play, + self.game.undostack, + self.bootstrap) + + self.connect('shared', self.collaboration._shared_cb) + if self._shared_activity: + # We are joining the activity + self.connect('joined', self.collaboration._joined_cb) + if self.get_shared(): + # We've already joined + self.collaboration._joined_cb() + + def set_up_ui(self): + self.board = boardwidget.GoBoardWidget(self.game.get_status(), self.size) + self.board.connect('motion-notify-event', self.board_motion_cb) + self.board.connect('insert-requested', self.insert_cb) + + self.main_view = gtk.VBox() + + self.board_aspect = gtk.AspectFrame(None, .5, .5, 1, False) + self.board_aspect.add(self.board) + self.main_view.pack_start(self.board_aspect) + + self.buttons_box = gtk.HBox() + self.buttons_alignment = gtk.Alignment(0.5, 1, 0.5, 1) + #Pass button + self.pass_button = gtk.Button(_('Pass')) + self.pass_button.connect("clicked", self.pass_cb) + self.buttons_box.pack_start(self.pass_button, True, True, 10) + + #Undo button + self.undo_button = gtk.Button(_('Undo')) + self.undo_button.connect("clicked", self.undo_cb) + self.buttons_box.pack_start(self.undo_button, True, True, 10) + + self.buttons_alignment.add(self.buttons_box) + self.main_view.pack_start(self.buttons_alignment, False, padding=10) + + self.infopanel = infopanel.InfoPanel() + self.main_view.pack_start(self.infopanel, False) + + self.set_canvas(self.main_view) + self.show_all() + + def insert_cb(self, widget, x, y, announce=True): + if announce and self.get_currentcolor() != self.get_playercolor(): + logger.debug('Play at %s x %s was out-of-turn!', x, y) + self.infopanel.show('It\'s not your turn!') + return False + # Make the play only if it wasn't a pass move. + if x != -1: + error = self.game.illegal(x, y, self.get_currentcolor()) + if error: + self.infopanel.show(error) + return False + # Make the play + captures = self.game.play((x, y), self.get_currentcolor()) + self.gameToolbar.grey_out_size_change() + if captures: self.redraw_captures(captures) + self.show_score() + self.board.draw_stone(x, y, self.get_currentcolor(), widget) + # Announce the local play + if self.get_shared() and announce: + self.collaboration.Play(x, y) + self.change_turn() + if not self.get_shared(): self.change_player_color() + + def undo_cb(self, widget, data=None): + if self.game.undo(): + self.board.queue_draw() + self.change_turn() + if not self.get_shared(): self.change_player_color() + + def pass_cb(self, widget, data=None): + if self.get_shared(): + if self.get_currentcolor() == self.get_playercolor(): + self.collaboration.Play(-1, -1) + else: + self.infopanel.show('It\'s not your turn!') + return + else: + self.change_player_color() + self.change_turn() + + def write_file(self, file_path): + logger.debug('Writing file: %s', file_path) + # Strip the undostack + undostack = self.game.undostack[:] + strippedstack = [] + for pos, color, captures in undostack: + strippedstack.append(pos) + f = open(file_path, 'w') + try: + cPickle.dump(strippedstack, f, cPickle.HIGHEST_PROTOCOL) + finally: + f.close() + self.metadata['our-color'] = self.get_playercolor() + self.metadata['shared'] = str(self.get_shared()) + self.metadata['size'] = str(self.size) + + def read_file(self, file_path): + logger.debug('Reading file: %s', file_path) + f = open(file_path, 'r') + try: + newstack = cPickle.load(f) + finally: + f.close() + if self.get_shared(): + logger.debug('The game we are loading is shared!') + self.PlayerColor = self.metadata.get('our-color', 'B') + if self.size != self.metadata.get('size', DEFAULT_SIZE): + self.board_size_change(None, int(self.metadata.get('size', DEFAULT_SIZE))) + self.bootstrap(newstack) + + def board_motion_cb(self, widget, event): + x, y = self.board.get_mouse_event_xy(event) + if x == self.lastX and y == self.lastY: + return + self.lastX = x + self.lastY = y + if not self.game.is_occupied(x, y) and self.game.legal((x, y), self.get_playercolor()): + self.board.draw_ghost_stone(x, y, self.get_playercolor()) + + def invert_color(self, color): + if color == 'B': return 'W' + return 'B' + + def get_currentcolor(self): + return self.CurrentColor + + def change_turn(self): + # It's the other guy's turn now + if self.CurrentColor == 'B': + self.infopanel.show('White\'s turn') + else: + self.infopanel.show('Black\'s turn') + self.CurrentColor = self.invert_color(self.get_currentcolor()) + + def get_playercolor(self): + return self.PlayerColor + + def change_player_color(self): + self.PlayerColor = self.invert_color(self.get_playercolor()) + + def set_player_color(self, color): + self.PlayerColor = color + + def redraw_captures(self, captures): + for x in captures: + self.board.redraw_area(x[0], x[1]) + + def bootstrap(self, plays): + ''' Take our game to the state it would have if @plays were manually played''' + logger.debug('Bootstraping...') + self.board.do_expose_event() # HACK: Looks like read_file is called before the board is exposed + for pos in plays: + logger.debug('Playing at %s with color %s', pos, self.get_currentcolor()) + captures = self.game.play((pos[0], pos[1]), self.get_currentcolor()) + if captures: self.redraw_captures(captures) + self.change_turn() + self.change_player_color() + logger.debug('Color after bootstraping is %s', self.get_currentcolor()) + self.show_score() + self.board.do_expose_event() + + def restart_game(self, widget=None): + logger.debug('Received restart signal!') + self.game.clear() + self.board.status = self.game.status + self.board.do_expose_event() + self.show_score() + + def board_size_change(self, widget, size): + if size == self.size: + return + self.size = size + del self.game + self.game = GoGame(size) + self.board_aspect.remove(self.board) + del self.board + self.board = boardwidget.GoBoardWidget(self.game.get_status(), int(size)) + self.board_aspect.add(self.board) + self.board.connect('motion-notify-event', self.board_motion_cb) + self.board.connect('insert-requested', self.insert_cb) + self.board.show() + + def show_score(self): + self.infopanel.show_score(_("Score is: Whites %(W)d - Blacks %(B)d" % self.game.get_score())) + + def _alert(self, title, text=None): + from sugar.graphics.alert import NotifyAlert + alert = NotifyAlert(timeout=5) + alert.props.title = title + alert.props.msg = text + self.add_alert(alert) + alert.connect('response', self._alert_cancel_cb) + alert.show() + + def _alert_cancel_cb(self, alert, response_id): + self.remove_alert(alert) + + # ------- Callbacks for Collaboration -------- # + def buddy_joined(self, buddy): + self._alert(_('Buddy joined'), _('%s joined' % buddy.props.nick)) + + def buddy_left(self, buddy): + self._alert(_('Buddy left'), _('%s left' % buddy.props.nick)) + + def Play(self, x, y, sender=None): + ''' Called when a stone was placed at x,y by sender''' + # Discard a pass move received in our turn. Do it here for extra security + if x == -1 and self.get_currentcolor() == self.get_playercolor(): + return + self.insert_cb(None, x, y, False) + diff --git a/activity/activity-go.svg b/activity/activity-go.svg new file mode 100644 index 0000000..4348e85 --- /dev/null +++ b/activity/activity-go.svg @@ -0,0 +1,118 @@ + + + + + + + ]> +image/svg+xml + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/activity/activity-helloworld.svg b/activity/activity-helloworld.svg new file mode 100644 index 0000000..b9278b0 --- /dev/null +++ b/activity/activity-helloworld.svg @@ -0,0 +1,8 @@ + + + +]> + + + diff --git a/activity/activity.info b/activity/activity.info new file mode 100644 index 0000000..1468a41 --- /dev/null +++ b/activity/activity.info @@ -0,0 +1,8 @@ +[Activity] +name = PlayGo +service_name = org.laptop.PlayGo +class = activity.PlayGo +icon = activity-go +activity_version = 2 +host_version = 1 +show_launcher = yes diff --git a/boardwidget.py b/boardwidget.py new file mode 100644 index 0000000..845520e --- /dev/null +++ b/boardwidget.py @@ -0,0 +1,244 @@ +# -*- coding: UTF-8 -*- +# Copyright 2007-2008 One Laptop Per Child +# Copyright 2007 Gerard J. Cerchio +# Copyright 2008 Andrés Ambrois +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import gobject +import gtk + +import logging + +logger = logging.getLogger('PlayGo.GoBoardWidget') + +class GoBoardWidget(gtk.Widget): + ''' A Go Board Widget ''' + + __gsignals__ = { + 'insert-requested': (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, (gobject.TYPE_INT, gobject.TYPE_INT)), + } + + def __init__(self, status, size=19): + gtk.Widget.__init__(self) + + self.status = status + self.size = size + + self.lastX = -1 + self.lastY = -1 + + + def do_realize(self): + """Called when the widget should create all of its + windowing resources. Create our gtk.gdk.Window + and load our pixmaps.""" + + # First set an internal flag telling that we're realized + self.set_flags(self.flags() | gtk.REALIZED) + + # Create our window and set the event masks we need + self.window = gtk.gdk.Window( + self.get_parent_window(), + width=self.allocation.width, + height=self.allocation.height, + window_type=gtk.gdk.WINDOW_CHILD, + wclass=gtk.gdk.INPUT_OUTPUT, + event_mask=self.get_events() | gtk.gdk.EXPOSURE_MASK + | gtk.gdk.BUTTON1_MOTION_MASK | gtk.gdk.BUTTON_PRESS_MASK + | gtk.gdk.POINTER_MOTION_MASK | gtk.gdk.POINTER_MOTION_HINT_MASK + | gtk.gdk.BUTTON_RELEASE_MASK) + + # Asociate ourselves with this window + self.window.set_user_data(self) + + # Set this window's style + self.style.attach(self.window) + + # The default color of the background should be what + # the style (theme engine) tells us. + self.style.set_background(self.window, gtk.STATE_NORMAL) + self.window.move_resize(*self.allocation) + + # Load the board pixmap + pixbuf = gtk.gdk.pixbuf_new_from_file("./images/board.gif") + self.BoardPixmap, mask = pixbuf.render_pixmap_and_mask() + del pixbuf + + # Load the white stone pixmap + self.WhitePixbuf = gtk.gdk.pixbuf_new_from_file("./images/white.gif") + + # Load the black stone pixmap + self.BlackPixbuf = gtk.gdk.pixbuf_new_from_file("./images/black.gif") + + self.gc = self.style.fg_gc[gtk.STATE_NORMAL] + + self.connect('button-release-event', self.button_release_cb) + + def draw_lines(self): + ctx = self.window.cairo_create() + + # Single width black lines + ctx.set_line_width(1) + ctx.set_source_rgba(0, 0, 0, 1) + + # Horizontal lines + for i in xrange(1, self.size + 1): + ctx.move_to( self.unit, i * self.unit) + ctx.line_to(self.size * self.unit, i * self.unit ) + + # Vertical lines + for i in xrange(1, self.size + 1): + ctx.move_to(i * self.unit, self.unit ) + ctx.line_to(i * self.unit, self.size * self.unit) + + ctx.stroke() + + # star point coords per board size + if self.size == 19 : + seq = [ 4, 10, 16 ] + elif self.size == 13 : + seq = [ 4, 7, 10 ] + elif self.size == 9 : + seq = [ 3, 7 ] + # set the middle singleton + ctx.arc( self.unit * 5, self.unit * 5, 3, 0, -1e-10) + ctx.fill_preserve() + ctx.stroke() + else : + seq = [] + + # stroke in the star points + #TODO: adjust size for teeny boards + for x in seq : + for y in seq : + ctx.arc( self.unit * x, self.unit * y, 3, 0, -1e-10) + ctx.fill_preserve() + ctx.stroke() + + + def do_unrealize(self): + # The do_unrealized method is responsible for freeing the GDK resources + # De-associate the window we created in do_realize with ourselves + self.window.destroy() + + def do_size_request(self, requisition): + """From Widget.py: The do_size_request method Gtk+ is calling + on a widget to ask it the widget how large it wishes to be. + It's not guaranteed that gtk+ will actually give this size + to the widget. So we will send gtk+ an appropiate minimum size""" + + requisition.height = 500 + requisition.width = 500 + + def do_size_allocate(self, allocation): + """The do_size_allocate is called by when the actual + size is known and the widget is told how much space + could actually be allocated Save the allocated space + self.allocation = allocation.""" + + logger.debug('Allocating %s x %s for widget', allocation.height, allocation.width) + self.allocation = allocation + if self.flags() & gtk.REALIZED: + self.window.move_resize(*allocation) + + def do_expose_event(self, event=None): + """This is where the widget must draw itself.""" + + #Scale everything + self.unit = (min(self.allocation.height, self.allocation.width)+10)/(self.size + 1) + if self.unit == 0: + return + self.BlackPixbuf = self.BlackPixbuf.scale_simple( int(self.unit), int(self.unit), gtk.gdk.INTERP_BILINEAR ) + self.WhitePixbuf = self.WhitePixbuf.scale_simple( int(self.unit), int(self.unit), gtk.gdk.INTERP_BILINEAR ) + #Draw the board + self.window.draw_drawable(self.gc, self.BoardPixmap, 0, 0, 0, 0, self.allocation.width, self.allocation.height) + #Draw the lines + self.draw_lines() + #Draw the stones + self.draw_stones(self.status) + + def get_mouse_event_xy(self, event): + """ + calculate the x and y position on the board given pixel address + """ + + x0 = 0 #self.get_allocation().x + y0 = 0 #self.get_allocation().y + x = int(( ( event.x - x0 ) / self.unit ) - 0.5) + y = int(( ( event.y - y0 ) / self.unit ) - 0.5) + if x > self.size - 1: x = self.size - 1 + if y > self.size - 1: y = self.size - 1 + return x, y + + def draw_ghost_stone(self, x, y, color): + x, y = self.get_pixel_from_coordinates(x, y) + if x == self.lastX and y == self.lastY: + return + + if self.lastX is not -1 : + self.window.invalidate_rect(gtk.gdk.Rectangle(int(self.lastX - self.unit/2), int(self.lastY - self.unit/2), int(self.unit), int(self.unit)), False) + + self.lastX = x + self.lastY = y + + ctx = self.window.cairo_create() + if color is 'B': + ctx.set_source_rgba(0, 0, 0, .5 ) + else: + ctx.set_source_rgba(0xff, 0xff, 0xff, .5 ) + + ctx.arc( self.lastX, self.lastY, self.unit/2 -4, 0, -1e-10) + ctx.fill_preserve() + ctx.stroke() + del ctx + + def button_release_cb(self, widget, event): + x, y = self.get_mouse_event_xy(event) + self.emit('insert-requested', x, y) + + def draw_stone(self, x, y, color, widget): + """ + paint a single stone on a point + """ + x = x + 1 + y = y + 1 + ctx = self.window.cairo_create() + ct = gtk.gdk.CairoContext(ctx) + if color == 'B': + ct.set_source_pixbuf(self.BlackPixbuf, self.unit*x - self.unit/2, self.unit*y - self.unit/2 ) + else: + ct.set_source_pixbuf(self.WhitePixbuf, self.unit*x - self.unit/2, self.unit*y - self.unit/2 ) + ctx.paint() + + def draw_stones(self, status): + for x in status.keys(): + self.draw_stone(x[0], x[1], status[x], self) + + def redraw_area(self, x, y): + x, y = self.get_pixel_from_coordinates(x, y) + self.window.invalidate_rect(gtk.gdk.Rectangle(int(x - self.unit/2), int(y - self.unit/2), int(self.unit), int(self.unit)), False) + + def get_pixel_from_coordinates(self, x, y): + if x > self.size - 1: x = self.size - 1 + if y > self.size - 1: y = self.size - 1 + x = (x+1) * self.unit + y = (y+1) * self.unit + return x, y + + def clear(self): + self.lastX = -1 + self.lastY = -1 + self.do_expose_event() diff --git a/collaboration.py b/collaboration.py new file mode 100644 index 0000000..38f926e --- /dev/null +++ b/collaboration.py @@ -0,0 +1,225 @@ +# -*- coding: UTF-8 -*- +# Copyright 2007-2008 One Laptop Per Child +# Copyright 2007 Gerard J. Cerchio +# Copyright 2008 Andrés Ambrois +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import logging +import sugar.logger + +from sugar.presence import presenceservice +import telepathy +from dbus.service import method, signal +from sugar.presence.sugartubeconn import SugarTubeConnection +from dbus.gobject_service import ExportedGObject + +SERVICE = "org.freedesktop.Telepathy.Tube.PlayGo" +IFACE = SERVICE +PATH = "/org/freedesktop/Telepathy/Tube/PlayGo" + +logger = logging.getLogger('PlayGo') + +class CollaborationWrapper(ExportedGObject): + ''' A wrapper for the collaboration bureaucracy''' + def __init__(self, activity, buddy_joined_cb, buddy_left_cb, play_cb, undostack, bootstrap): + self.activity = activity + self.buddy_joined = buddy_joined_cb + self.buddy_left = buddy_left_cb + self.Play_cb = play_cb + self.undostack = undostack + self.bootstrap = bootstrap + self.world = False + self.entered = False + self.presence_service = presenceservice.get_instance() + self.owner = self.presence_service.get_owner() + + def _shared_cb(self, activity): + self.activity.gameToolbar.grey_out_size_change() + self.activity.gameToolbar.grey_out_restart() + self._sharing_setup() + self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES].OfferDBusTube( + SERVICE, {}) + self.is_initiator = True + self.activity.undo_button.hide() + + def _joined_cb(self, activity): + self.activity.gameToolbar.grey_out_size_change() + self.activity.gameToolbar.grey_out_restart() + self._sharing_setup() + self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES].ListTubes( + reply_handler=self._list_tubes_reply_cb, + error_handler=self._list_tubes_error_cb) + self.is_initiator = False + self.activity.undo_button.hide() + + def _sharing_setup(self): + if self.activity._shared_activity is None: + logger.error('Failed to share or join activity') + return + + self.conn = self.activity._shared_activity.telepathy_conn + self.tubes_chan = self.activity._shared_activity.telepathy_tubes_chan + self.text_chan = self.activity._shared_activity.telepathy_text_chan + + self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES].connect_to_signal( + 'NewTube', self._new_tube_cb) + + self.activity._shared_activity.connect('buddy-joined', self._buddy_joined_cb) + self.activity._shared_activity.connect('buddy-left', self._buddy_left_cb) + + # Optional - included for example: + # Find out who's already in the shared activity: + for buddy in self.activity._shared_activity.get_joined_buddies(): + logger.debug('Buddy %s is already in the activity', + buddy.props.nick) + + def participant_change_cb(self, added, removed): + logger.debug('Tube: Added participants: %r', added) + logger.debug('Tube: Removed participants: %r', removed) + for handle, bus_name in added: + buddy = self._get_buddy(handle) + if buddy is not None: + logger.debug('Tube: Handle %u (Buddy %s) was added', + handle, buddy.props.nick) + for handle in removed: + buddy = self._get_buddy(handle) + if buddy is not None: + logger.debug('Buddy %s was removed' % buddy.props.nick) + if not self.entered: + if self.is_initiator: + logger.debug("I'm initiating the tube, will " + "watch for hellos.") + self.add_hello_handler() + else: + logger.debug('Hello, everyone! What did I miss?') + self.Hello() + self.entered = True + + + # This is sent to all participants whenever we join an activity + @signal(dbus_interface=IFACE, signature='') + def Hello(self): + """Say Hello to whoever else is in the tube.""" + logger.debug('I said Hello.') + + # This is called by whoever receives our Hello signal + # This method receives the current game state and puts us in sync + # with the rest of the participants. + # The current game state is represented by the game object + @method(dbus_interface=IFACE, in_signature='a(ii)si', out_signature='') + def World(self, undostack, taken_color, size): + """To be called on the incoming XO after they Hello.""" + if not self.world: + logger.debug('Somebody called World and sent me undostack: %s', + undostack) + self.activity.board_size_change(None, size) + self.bootstrap(list(undostack)) + self.activity.set_player_color(self.activity.invert_color(taken_color)) + #self.players = players + # now I can World others + self.add_hello_handler() + else: + self.world = True + logger.debug("I've already been welcomed, doing nothing") + + @signal(dbus_interface=IFACE, signature='ii') + def Play(self, x, y): + """Say Hello to whoever else is in the tube.""" + logger.debug('Signaling players of stone placement at:%s x %s.', x, y) + + def add_hello_handler(self): + logger.debug('Adding hello handler.') + self.tube.add_signal_receiver(self.hello_signal_cb, 'Hello', IFACE, + path=PATH, sender_keyword='sender') + self.tube.add_signal_receiver(self.play_signal_cb, 'Play', IFACE, + path=PATH, sender_keyword='sender') + + def hello_signal_cb(self, sender=None): + """Somebody Helloed me. World them.""" + if sender == self.tube.get_unique_name(): + # sender is my bus name, so ignore my own signal + return + logger.debug('Newcomer %s has joined', sender) + logger.debug('Welcoming newcomer and sending them the game state') + # Strip the undostack to reduce net traffic =) + strippedstack = [] + for pos, color, captures in self.undostack: + strippedstack.append(pos) + # FIXME: A spectator needs to send the color that was taken, not its own + self.tube.get_object(sender, PATH).World(strippedstack, + self.activity.get_playercolor(), + self.activity.size, + dbus_interface=IFACE) + + def play_signal_cb(self, x, y, sender=None): + """Somebody placed a stone. """ + if sender == self.tube.get_unique_name(): + # sender is my bus name, so ignore my own signal + return + logger.debug('Buddy %s placed a stone at %s x %s', sender, x, y) + # Call our Play callback + self.Play_cb(x, y, sender) + + def _list_tubes_error_cb(self, e): + logger.error('ListTubes() failed: %s', e) + + def _list_tubes_reply_cb(self, tubes): + for tube_info in tubes: + self._new_tube_cb(*tube_info) + + def _new_tube_cb(self, id, initiator, type, service, params, state): + logger.debug('New tube: ID=%d initator=%d type=%d service=%s ' + 'params=%r state=%d', id, initiator, type, service, + params, state) + if (type == telepathy.TUBE_TYPE_DBUS and + service == SERVICE): + if state == telepathy.TUBE_STATE_LOCAL_PENDING: + self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES].AcceptDBusTube(id) + self.tube = SugarTubeConnection(self.conn, + self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES], + id, group_iface=self.text_chan[telepathy.CHANNEL_INTERFACE_GROUP]) + super(CollaborationWrapper, self).__init__(self.tube, PATH) + self.tube.watch_participants(self.participant_change_cb) + + def _buddy_joined_cb (self, activity, buddy): + """Called when a buddy joins the shared activity. """ + logger.debug('Buddy %s joined', buddy.props.nick) + self.buddy_joined(buddy) + + def _buddy_left_cb (self, activity, buddy): + """Called when a buddy leaves the shared activity. """ + self.buddy_left(buddy) + + def _get_buddy(self, cs_handle): + """Get a Buddy from a channel specific handle.""" + logger.debug('Trying to find owner of handle %u...', cs_handle) + group = self.text_chan[telepathy.CHANNEL_INTERFACE_GROUP] + my_csh = group.GetSelfHandle() + logger.debug('My handle in that group is %u', my_csh) + if my_csh == cs_handle: + handle = self.conn.GetSelfHandle() + logger.debug('CS handle %u belongs to me, %u', cs_handle, handle) + elif group.GetGroupFlags() & telepathy.CHANNEL_GROUP_FLAG_CHANNEL_SPECIFIC_HANDLES: + handle = group.GetHandleOwners([cs_handle])[0] + logger.debug('CS handle %u belongs to %u', cs_handle, handle) + else: + handle = cs_handle + logger.debug('non-CS handle %u belongs to itself', handle) + # XXX: deal with failure to get the handle owner + assert handle != 0 + return self.presence_service.get_buddy_by_telepathy_handle( + self.conn.service_name, self.conn.object_path, handle) + diff --git a/gametoolbar.py b/gametoolbar.py new file mode 100644 index 0000000..834b1e0 --- /dev/null +++ b/gametoolbar.py @@ -0,0 +1,97 @@ +# -*- coding: UTF-8 -*- +# Copyright 2007-2008 One Laptop Per Child +# Copyright 2007 Gerard J. Cerchio +# Copyright 2008 Andrés Ambrois +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import gtk +from os.path import join, dirname + +from gettext import gettext as _ +from sugar.graphics.toolbutton import ToolButton +from sugar.graphics.toolcombobox import ToolComboBox +from sugar.graphics.objectchooser import ObjectChooser +import logging +from gobject import SIGNAL_RUN_FIRST, TYPE_PYOBJECT, TYPE_NONE, TYPE_INT + +logger = logging.getLogger('PlayGo') + +class GameToolbar(gtk.Toolbar): + __gtype_name__ = 'GameToolbar' + + __gsignals__ = { + 'game-restart': (SIGNAL_RUN_FIRST, TYPE_NONE, []), + 'game-board-size': (SIGNAL_RUN_FIRST, TYPE_NONE, [TYPE_INT]), + } + + def __init__(self, activity): + gtk.Toolbar.__init__(self) + self.activity = activity + + # Reset Button + restart_icon = join(dirname(__file__), 'images', 'gtk-refresh.svg') + restart_image = gtk.Image() + restart_image.set_from_file(restart_icon) + self._restart_button = ToolButton() + self._restart_button.set_icon_widget(restart_image) + self._restart_button.connect('clicked', self._game_restart_cb) + self._restart_button.set_tooltip(_('Restart Game')) + self.insert(self._restart_button, -1) + self._restart_button.show() + + # Separator + separator = gtk.SeparatorToolItem() + separator.set_draw(True) + self.insert(separator, -1) + + self._add_widget(gtk.Label(_('Board size') + ': ')) + # Change size combobox + self._size_combo = ToolComboBox() + self._sizes = ['19 X 19', '13 X 13', '9 X 9'] + for i, f in enumerate(self._sizes): + self._size_combo.combo.append_item(i, f) + self._size_combo.combo.connect('changed', self._game_size_cb) + self._add_widget(self._size_combo) + self._size_combo.combo.set_active(0) + + def _add_widget(self, widget, expand=False): + tool_item = gtk.ToolItem() + tool_item.set_expand(expand) + tool_item.add(widget) + widget.show() + self.insert(tool_item, -1) + tool_item.show() + + def _game_restart_cb(self, widget): + self._size_combo.set_sensitive(True) + self.emit('game-restart') + + def grey_out_restart(self): + self._restart_button.set_sensitive(False) + + def _game_size_cb(self, widget): + game_size = int(self._sizes[self._size_combo.combo.get_active()][:2]) + self.emit('game-board-size', game_size) + + def grey_out_size_change(self): + self._size_combo.set_sensitive(False) + + def update_toolbar(self, widget, data, grid): + size = data.get('size') + self._size_combo.combo.handler_block(self.size_handle_id) + size_index = self._sizes.index(size+' X '+size) + self._size_combo.combo.set_active(int(size_index)) + self._size_combo.combo.handler_unblock(self.size_handle_id) diff --git a/gogame.py b/gogame.py new file mode 100644 index 0000000..01390ba --- /dev/null +++ b/gogame.py @@ -0,0 +1,205 @@ +# -*- coding: UTF-8 -*- +# Copyright 2007-2008 One Laptop Per Child +# Copyright 2007 Gerard J. Cerchio +# Copyright 2008 Andrés Ambrois +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import logging +_logger = logging.getLogger('PlayGo') + +from gettext import gettext as _ + +class GoGame: + """ This class administrates a go board. + It keeps track of the stones currently on the board in the dictionary self.status, + and of the moves played so far in self.undostack + + It has methods to clear the board, play a stone, undo a move. """ + + def __init__(self, boardSize = 19): + self.size = boardSize + self.status = {} + self.undostack = [] + self.score = {'B' : 0, 'W' : 0} + _logger.setLevel( logging.DEBUG ) + + def get_score(self): + return self.score + + def increase_score(self, color): + self.score[color] = self.score[color] + 1 + + def neighbors(self,x): + """ Returns the coordinates of the 4 (resp. 3 resp. 2 at the side 1 in the corner) intersections + adjacent to the given one. """ + if x[0]== 0 : l0 = [1] + elif x[0]== self.size-1 : l0 = [self.size-2] + else: l0 = [x[0]-1, x[0]+1] + + if x[1]== 0 : l1 = [1] + elif x[1]== self.size-1 : l1 = [self.size-2] + else: l1 = [x[1]-1, x[1]+1] + + l = [] + for i in l0: l.append((i,x[1])) + for j in l1: l.append((x[0],j)) + + return l + + def is_occupied(self, x, y): + return self.status.has_key((x, y)) + + def clear(self): + """ Clear the board """ + self.status = {} + self.undostack=[] + self.score = {'B' : 0, 'W' : 0} + + def play(self,pos,color): + """ This plays a color=black/white stone at pos, if that is a legal move + and deletes stones captured by that move. + It returns 1 if the move has been played, 0 if not. """ + if self.status.has_key(pos): # check if empty + return 0 + + if self.legal(pos,color): # legal move? + self.status[pos] = color + captures = self.get_captures(pos, color) + if captures: + for x in captures: + del self.status[x] # remove captured stones, if any + self.increase_score(color) + self.undostack.append((pos,color,captures)) # remember move + captured stones for easy undo + return captures + else: + return 0 + + def get_captures(self, pos, color): + """Returns a list of captured stones resulting from placing a color stone at pos """ + c = [] # captured stones + + for x in self.neighbors(pos): + if self.status.has_key(x) and self.status[x]==self.invert(color): + c = c + self.hasNoLibExcP(x, self.invert(color), pos) + + if c: + captures = [] + for x in c: + if not x in captures: captures.append(x) + return captures + + return 0 + + def checkKo(self, pos, color): + ''' Check if a move by color at pos would be a basic Ko infraction ''' + # Basically what we need to check, is if the current play would undo + # all that was done by the last entry in undostack (capture what was placed + # and place what was captured). + if self.undostack: + lastpos, lastcolor, lastcaptures = self.undostack[-1] + currentcaptures = self.get_captures(pos, color) + if lastcaptures != 0 and currentcaptures != 0: + if lastcolor != color and lastcaptures[0] == pos and lastpos == currentcaptures[0]: + return 1 + return 0 + + def legal(self, pos, color): + """ Check if a play by color at pos would be a legal move. """ + if self.status.has_key(pos): + return 0 + + # If the play at pos would leave that stone without liberties, we have two possibilities: + # 1- It's a capturing move + # 2- It's an illegal move + if self.hasNoLibExcP(pos, color): + # Check if it would capture any stones + if self.get_captures(pos, color): + return 1 + # It didnt, so I guess it's illegal + return 0 + else: return not self.checkKo(pos, color) + + def illegal(self, x, y, color): + """ Check if a play by color at pos would be an illigal move, and return pretty errors""" + if self.status.has_key((x, y)): + return _('There already is a stone there!') + if self.checkKo((x, y), color): + return _('Ko violation!') + + # If the play at pos would leave that stone without liberties, we have two possibilities: + # 1- It's a capturing move + # 2- It's an illegal move + if self.hasNoLibExcP((x, y), color): + # Check if it would capture any stones + if self.get_captures((x, y), color): + return False + # It didnt, so I guess it's illegal + return _('Illegal move.') + else: return False + + def hasNoLibExcP(self, pos, color, exc = None): + """ This function checks if the string (=solidly connected) of stones containing + the stone at pos has a liberty (resp. has a liberty besides that at exc). + If no liberties are found, a list of all stones in the string is returned. + + The algorithm is a non-recursive implementation of a simple flood-filling: + starting from the stone at pos, the main while-loop looks at the intersections + directly adjacent to the stones found so far, for liberties or other stones that belong + to the string. Then it looks at the neighbors of those newly found stones, and so + on, until it finds a liberty, or until it doesn't find any new stones belonging + to the string, which means that there are no liberties. + Once a liberty is found, the function returns immediately. """ + + st = [] # in the end, this list will contain all stones solidly connected to the + # one at pos, if this string has no liberties + newlyFound = [pos] # in the while loop, we will look at the neighbors of stones in newlyFound + foundNew = 1 + + while foundNew: + foundNew = 0 + n = [] # this will contain the stones found in this iteration of the loop + for x in newlyFound: + for y in self.neighbors(x): + if not self.status.has_key(y) and y != exc and y != pos: # found a liberty + return [] + elif self.status.has_key(y) and self.status[y]==color \ + and not y in newlyFound and not y in st: # found another stone of same color + n.append(y) + foundNew = 1 + + st[:0] = newlyFound + newlyFound = n + + return st # no liberties found, return list of all stones connected to the original one + + def undo(self, no=1): + """ Undo the last no moves. """ + for i in range(no): + if self.undostack: + pos, color, captures = self.undostack.pop() + del self.status[pos] + if captures: + for p in captures: self.status[p] = self.invert(color) + return True + else: + return False + + def invert(self,color): + if color == 'B': return 'W' + else: return 'B' + + def get_status(self): + return self.status diff --git a/images/BsTurn.gif b/images/BsTurn.gif new file mode 100644 index 0000000..52edae7 --- /dev/null +++ b/images/BsTurn.gif Binary files differ diff --git a/images/README b/images/README new file mode 100644 index 0000000..cca19e0 --- /dev/null +++ b/images/README @@ -0,0 +1,7 @@ +OLPC PlayGo Activity image library + +These images are from Ulrich Goertz's (u@g0ertz.de) uliGo 0.3 + +They are published under the GNU GENERAL PUBLIC LICENSE Version 2, June 1991. + +(C) Ulrich Goertz (u@g0ertz.de), 2001-2003. \ No newline at end of file diff --git a/images/WsTurn.gif b/images/WsTurn.gif new file mode 100644 index 0000000..ab00f50 --- /dev/null +++ b/images/WsTurn.gif Binary files differ diff --git a/images/b.gif b/images/b.gif new file mode 100644 index 0000000..e56bc98 --- /dev/null +++ b/images/b.gif Binary files differ diff --git a/images/black.gif b/images/black.gif new file mode 100644 index 0000000..1719614 --- /dev/null +++ b/images/black.gif Binary files differ diff --git a/images/board.gif b/images/board.gif new file mode 100644 index 0000000..c253991 --- /dev/null +++ b/images/board.gif Binary files differ diff --git a/images/bw.gif b/images/bw.gif new file mode 100644 index 0000000..eb069a0 --- /dev/null +++ b/images/bw.gif Binary files differ diff --git a/images/gtk-refresh.svg b/images/gtk-refresh.svg new file mode 100644 index 0000000..23610c9 --- /dev/null +++ b/images/gtk-refresh.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/images/w.gif b/images/w.gif new file mode 100644 index 0000000..1374cba --- /dev/null +++ b/images/w.gif Binary files differ diff --git a/images/white.gif b/images/white.gif new file mode 100644 index 0000000..03e3bf7 --- /dev/null +++ b/images/white.gif Binary files differ diff --git a/infopanel.py b/infopanel.py new file mode 100755 index 0000000..b8501b4 --- /dev/null +++ b/infopanel.py @@ -0,0 +1,40 @@ +# -*- coding: UTF-8 -*- +# Copyright 2007-2008 One Laptop Per Child +# Copyright 2007 Gerard J. Cerchio +# Copyright 2008 Andrés Ambrois +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import gtk +from sugar.graphics import style + +class InfoPanel(gtk.EventBox): + def __init__(self): + gtk.EventBox.__init__(self) + self.Box = gtk.VBox() + self.status_label = gtk.Label() + self.Box.pack_start(self.status_label, True, True, 10) + self.score_label = gtk.Label() + self.Box.pack_start(self.score_label, True, True, 10) + self.modify_bg(gtk.STATE_NORMAL, gtk.gdk.color_parse("dark grey")) + self.add(self.Box) + self.show_all() + + def show(self, text): + self.status_label.set_text(text) + + def show_score(self, text): + self.score_label.set_text(text) + diff --git a/locale/es/LC_MESSAGES/org.laptop.PlayGo.mo b/locale/es/LC_MESSAGES/org.laptop.PlayGo.mo new file mode 100644 index 0000000..a671276 --- /dev/null +++ b/locale/es/LC_MESSAGES/org.laptop.PlayGo.mo Binary files differ diff --git a/po/POTFILES.in b/po/POTFILES.in new file mode 100644 index 0000000..83fab9a --- /dev/null +++ b/po/POTFILES.in @@ -0,0 +1,8 @@ +encoding: UTF-8 +activity.py +gametoolbar.py +collaboration.py +boardwidget.py +gogame.py +infopanel.py + diff --git a/po/PlayGo.pot b/po/PlayGo.pot new file mode 100644 index 0000000..85e8650 --- /dev/null +++ b/po/PlayGo.pot @@ -0,0 +1,80 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2008-08-14 10:24-0300\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: activity/activity.info:2 +msgid "PlayGo" +msgstr "" + +#: gogame.py:137 +msgid "There already is a stone there!" +msgstr "" + +#: gogame.py:139 +msgid "Ko violation!" +msgstr "" + +#: gogame.py:149 +msgid "Illegal move." +msgstr "" + +#: activity.py:51 +msgid "Game" +msgstr "" + +#: activity.py:65 +msgid "Welcome to PlayGo!" +msgstr "" + +#: activity.py:101 +msgid "Pass" +msgstr "" + +#: activity.py:106 +msgid "Undo" +msgstr "" + +#: activity.py:259 +#, python-format +msgid "Score is: Whites %(W)d - Blacks %(B)d" +msgstr "" + +#: activity.py:275 +msgid "Buddy joined" +msgstr "" + +#: activity.py:275 +#, python-format +msgid "%s joined" +msgstr "" + +#: activity.py:278 +msgid "Buddy left" +msgstr "" + +#: activity.py:278 +#, python-format +msgid "%s left" +msgstr "" + +#: gametoolbar.py:50 +msgid "Restart Game" +msgstr "" + +#: gametoolbar.py:59 +msgid "Board size" +msgstr "" diff --git a/po/es.po b/po/es.po new file mode 100644 index 0000000..8bd4044 --- /dev/null +++ b/po/es.po @@ -0,0 +1,80 @@ +# Spanish translations for PlayGo package. +# Copyright (C) 2008 THE PlayGo'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PlayGo package. +# Andrés Ambrois , 2008. +# +msgid "" +msgstr "" +"Project-Id-Version: PlayGo 2\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2008-08-14 10:24-0300\n" +"PO-Revision-Date: 2008-08-14 10:25-0300\n" +"Last-Translator: Andrs Ambrois \n" +"Language-Team: Spanish\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: activity/activity.info:2 +msgid "PlayGo" +msgstr "PlayGo" + +#: gogame.py:137 +msgid "There already is a stone there!" +msgstr "Ya hay una piedra allí!" + +#: gogame.py:139 +msgid "Ko violation!" +msgstr "Violación de Ko!" + +#: gogame.py:149 +msgid "Illegal move." +msgstr "Movida ilegal." + +#: activity.py:51 +msgid "Game" +msgstr "Juego" + +#: activity.py:65 +msgid "Welcome to PlayGo!" +msgstr "Bienvenido/a a PlayGo!" + +#: activity.py:101 +msgid "Pass" +msgstr "Pasar" + +#: activity.py:106 +msgid "Undo" +msgstr "Deshacer" + +#: activity.py:259 +#, python-format +msgid "Score is: Whites %(W)d - Blacks %(B)d" +msgstr "El puntaje es: Blancas %(W)d - Negras %(B)d" + +#: activity.py:275 +msgid "Buddy joined" +msgstr "Un amigo se ha unido" + +#: activity.py:275 +#, python-format +msgid "%s joined" +msgstr "%s se ha unido" + +#: activity.py:278 +msgid "Buddy left" +msgstr "Un amigo se ha ido" + +#: activity.py:278 +#, python-format +msgid "%s left" +msgstr "%s se ha ido" + +#: gametoolbar.py:50 +msgid "Restart Game" +msgstr "Reiniciar el juego" + +#: gametoolbar.py:59 +msgid "Board size" +msgstr "Tamaño del tablero" diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..d07fb3d --- /dev/null +++ b/setup.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python + +# Copyright (C) 2006, Red Hat, Inc. +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +from sugar.activity import bundlebuilder + +if __name__ == "__main__": + bundlebuilder.start('PlayGo') -- cgit v0.9.1