Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--MANIFEST17
-rw-r--r--NEWS13
-rw-r--r--README9
-rw-r--r--TODO8
-rw-r--r--activity.py288
-rw-r--r--activity/activity-go.svg118
-rw-r--r--activity/activity-helloworld.svg8
-rw-r--r--activity/activity.info8
-rw-r--r--boardwidget.py244
-rw-r--r--collaboration.py225
-rw-r--r--gametoolbar.py97
-rw-r--r--gogame.py205
-rw-r--r--images/BsTurn.gifbin0 -> 691 bytes
-rw-r--r--images/README7
-rw-r--r--images/WsTurn.gifbin0 -> 728 bytes
-rw-r--r--images/b.gifbin0 -> 107 bytes
-rw-r--r--images/black.gifbin0 -> 1343 bytes
-rw-r--r--images/board.gifbin0 -> 197707 bytes
-rw-r--r--images/bw.gifbin0 -> 141 bytes
-rw-r--r--images/gtk-refresh.svg7
-rw-r--r--images/w.gifbin0 -> 105 bytes
-rw-r--r--images/white.gifbin0 -> 1250 bytes
-rwxr-xr-xinfopanel.py40
-rw-r--r--locale/es/LC_MESSAGES/org.laptop.PlayGo.mobin0 -> 1189 bytes
-rw-r--r--po/POTFILES.in8
-rw-r--r--po/PlayGo.pot80
-rw-r--r--po/es.po80
-rwxr-xr-xsetup.py22
28 files changed, 1484 insertions, 0 deletions
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 <www.circlesoft.com>
+# Copyright 2008 Andrés Ambrois <andresambrois@gmail.com>
+#
+# 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 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+
+ <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
+ <!ENTITY ns_svg "http://www.w3.org/2000/svg">
+ <!ENTITY ns_xlink " http://www.w3.org/1999/xlink">
+ <!ENTITY stroke_color "#000000">
+ <!ENTITY fill_color "#FFFFFF">
+ ]>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://web.resource.org/cc/"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ enable-background="new 0 0 55 55"
+ height="55px"
+ version="1.1"
+ viewBox="0 0 55 55"
+ width="55px"
+ x="0px"
+ xml:space="preserve"
+ y="0px"
+ id="svg2"
+ sodipodi:version="0.32"
+ inkscape:version="0.45.1"
+ sodipodi:docname="activity-go.svg"
+ inkscape:output_extension="org.inkscape.output.svg.inkscape"
+ sodipodi:docbase="/home/gjpc/PlayGo/PlayGo/src/activity"><metadata
+ id="metadata26"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
+ id="defs24" /><sodipodi:namedview
+ inkscape:window-height="619"
+ inkscape:window-width="872"
+ inkscape:pageshadow="2"
+ inkscape:pageopacity="0.0"
+ guidetolerance="10.0"
+ gridtolerance="10.0"
+ objecttolerance="10.0"
+ borderopacity="1.0"
+ bordercolor="#666666"
+ pagecolor="#ffffff"
+ id="base"
+ inkscape:zoom="7.3818182"
+ inkscape:cx="27.5"
+ inkscape:cy="27.5"
+ inkscape:window-x="26"
+ inkscape:window-y="59"
+ inkscape:current-layer="svg2" /><g
+ display="block"
+ id="activity-connect"
+ transform="translate(-0.5418719,-1.2192118)"
+ style="display:block">
+ <g
+ display="inline"
+ id="g5"
+ style="display:inline">
+ <path
+ d="M 14.118,9.535 C 14.119,11.967 12.148,13.94 9.716,13.941 C 7.283,13.942 5.311,11.971 5.31,9.539 C 5.31,9.538 5.31,9.536 5.31,9.535 C 5.308,7.103 7.279,5.13 9.711,5.128 C 12.143,5.127 14.116,7.098 14.117,9.53 C 14.118,9.532 14.118,9.534 14.118,9.535 z "
+ id="path2160"
+ style="fill:&fill_color;stroke:&stroke_color;stroke-width:2.25;stroke-linejoin:bevel" />
+ <path
+ d="M 51.188,9.535 C 51.19,11.967 49.219,13.94 46.786,13.941 C 44.354,13.942 42.382,11.971 42.38,9.539 C 42.38,9.538 42.38,9.536 42.38,9.535 C 42.38,7.102 44.351,5.13 46.782,5.128 C 49.216,5.127 51.188,7.098 51.188,9.53 C 51.188,9.532 51.188,9.534 51.188,9.535 z "
+ id="path2162"
+ style="fill:#010101;stroke:&stroke_color;stroke-width:2.25;stroke-linejoin:bevel" />
+ <path
+ d="M 26.475,9.535 C 26.475,11.967 24.504,13.94 22.072,13.941 C 19.639,13.942 17.667,11.971 17.665,9.539 C 17.665,9.538 17.665,9.536 17.665,9.535 C 17.664,7.102 19.635,5.13 22.067,5.128 C 24.5,5.127 26.472,7.098 26.474,9.53 C 26.475,9.532 26.475,9.534 26.475,9.535 z "
+ id="path3134"
+ style="fill:&fill_color;stroke:&fill_color;stroke-width:2.25;stroke-linejoin:bevel" />
+ <path
+ d="M 38.832,9.535 C 38.832,11.967 36.861,13.94 34.43,13.941 C 31.996,13.942 30.024,11.971 30.022,9.539 C 30.022,9.538 30.022,9.536 30.022,9.535 C 30.022,7.102 31.993,5.13 34.424,5.128 C 36.858,5.127 38.83,7.098 38.832,9.53 C 38.832,9.532 38.832,9.534 38.832,9.535 z "
+ id="path3136"
+ style="fill:&fill_color;stroke:&stroke_color;stroke-width:2.25;stroke-linejoin:bevel" />
+ <path
+ d="M 26.475,21.892 C 26.475,24.325 24.504,26.297 22.072,26.298 C 19.639,26.299 17.667,24.328 17.665,21.896 C 17.665,21.895 17.665,21.893 17.665,21.892 C 17.664,19.459 19.635,17.487 22.067,17.485 C 24.5,17.484 26.472,19.455 26.474,21.887 C 26.475,21.889 26.475,21.891 26.475,21.892 z "
+ id="path3138"
+ style="fill:&fill_color;stroke:&stroke_color;stroke-width:2.25;stroke-linejoin:bevel" />
+ <path
+ d="M 38.832,21.892 C 38.832,24.325 36.861,26.297 34.43,26.298 C 31.996,26.299 30.024,24.328 30.022,21.896 C 30.022,21.895 30.022,21.893 30.022,21.892 C 30.022,19.459 31.993,17.487 34.424,17.485 C 36.858,17.484 38.83,19.455 38.832,21.887 C 38.832,21.889 38.832,21.891 38.832,21.892 z "
+ id="path3140"
+ style="fill:#010101;stroke:&stroke_color;stroke-width:2.25;stroke-linejoin:bevel" />
+ <path
+ d="M 14.118,21.892 C 14.119,24.325 12.148,26.297 9.716,26.298 C 7.284,26.299 5.311,24.328 5.31,21.896 C 5.31,21.895 5.31,21.893 5.31,21.892 C 5.309,19.459 7.28,17.487 9.712,17.485 C 12.144,17.484 14.117,19.455 14.118,21.887 C 14.118,21.889 14.118,21.891 14.118,21.892 z "
+ id="path3142"
+ style="fill:&fill_color;stroke:&stroke_color;stroke-width:2.25;stroke-linejoin:bevel" />
+ <path
+ d="M 51.188,21.892 C 51.19,24.325 49.219,26.297 46.786,26.298 C 44.354,26.299 42.382,24.328 42.38,21.896 C 42.38,21.895 42.38,21.893 42.38,21.892 C 42.38,19.459 44.351,17.487 46.782,17.485 C 49.216,17.484 51.188,19.455 51.188,21.887 C 51.188,21.889 51.188,21.891 51.188,21.892 z "
+ id="path3144"
+ style="fill:&fill_color;stroke:&stroke_color;stroke-width:2.25;stroke-linejoin:bevel" />
+ <path
+ d="M 14.118,34.248 C 14.119,36.68 12.148,38.652 9.716,38.654 C 7.283,38.655 5.311,36.684 5.31,34.252 C 5.31,34.25 5.31,34.249 5.31,34.248 C 5.309,31.815 7.28,29.843 9.712,29.842 C 12.144,29.84 14.117,31.811 14.118,34.244 C 14.118,34.245 14.118,34.246 14.118,34.248 z "
+ id="path3146"
+ style="fill:&fill_color;stroke:&stroke_color;stroke-width:2.25;stroke-linejoin:bevel" />
+
+ <path
+ d="M 38.832,34.248 C 38.832,36.68 36.861,38.652 34.43,38.654 C 31.996,38.655 30.024,36.684 30.022,34.252 C 30.022,34.25 30.022,34.249 30.022,34.248 C 30.022,31.815 31.993,29.843 34.424,29.842 C 36.858,29.84 38.83,31.811 38.832,34.244 C 38.832,34.245 38.832,34.246 38.832,34.248 z "
+ id="path3152"
+ style="fill:&fill_color;stroke:&fill_color;stroke-width:2.25;stroke-linejoin:bevel" />
+ <path
+ d="M 38.832,46.604 C 38.832,49.038 36.861,51.01 34.43,51.011 C 31.996,51.012 30.024,49.041 30.022,46.609 C 30.022,46.608 30.022,46.606 30.022,46.604 C 30.022,44.172 31.993,42.2 34.424,42.198 C 36.858,42.198 38.83,44.169 38.832,46.6 C 38.832,46.602 38.832,46.603 38.832,46.604 z "
+ id="path3154"
+ style="fill:&fill_color;stroke:&stroke_color;stroke-width:2.25;stroke-linejoin:bevel" />
+ <path
+ d="M 14.118,46.604 C 14.119,49.038 12.148,51.01 9.716,51.011 C 7.284,51.012 5.311,49.041 5.31,46.609 C 5.31,46.608 5.31,46.606 5.31,46.604 C 5.309,44.172 7.28,42.2 9.712,42.198 C 12.144,42.198 14.117,44.169 14.118,46.6 C 14.118,46.602 14.118,46.603 14.118,46.604 z "
+ id="path3156"
+ style="fill:#010101;stroke:&stroke_color;stroke-width:2.25;stroke-linejoin:bevel" />
+ <path
+ d="M 26.475,46.604 C 26.475,49.038 24.504,51.01 22.072,51.011 C 19.639,51.012 17.667,49.041 17.665,46.609 C 17.665,46.608 17.665,46.606 17.665,46.604 C 17.664,44.172 19.635,42.2 22.067,42.198 C 24.5,42.198 26.472,44.169 26.474,46.6 C 26.475,46.602 26.475,46.603 26.475,46.604 z "
+ id="path3158"
+ style="fill:&fill_color;stroke:&stroke_color;stroke-width:2.25;stroke-linejoin:bevel" />
+ <path
+ d="M 51.188,46.604 C 51.19,49.038 49.219,51.01 46.786,51.011 C 44.354,51.012 42.382,49.041 42.38,46.609 C 42.38,46.608 42.38,46.606 42.38,46.604 C 42.38,44.172 44.351,42.2 46.782,42.198 C 49.216,42.198 51.188,44.169 51.188,46.6 C 51.188,46.602 51.188,46.603 51.188,46.604 z "
+ id="path3160"
+ style="fill:&fill_color;stroke:&stroke_color;stroke-width:2.25;stroke-linejoin:bevel" />
+ </g>
+</g></svg> \ 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 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
+ <!ENTITY fill_color "#FFFFFF">
+ <!ENTITY stroke_color "#000000">
+]>
+<svg xmlns="http://www.w3.org/2000/svg" width="50" height="50">
+<rect x="1" y="1" width="48" height="48" style="fill:&fill_color;;stroke:&stroke_color;;stroke-width:2"/>
+</svg>
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 <www.circlesoft.com>
+# Copyright 2008 Andrés Ambrois <andresambrois@gmail.com>
+#
+# 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 <www.circlesoft.com>
+# Copyright 2008 Andrés Ambrois <andresambrois@gmail.com>
+#
+# 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 <www.circlesoft.com>
+# Copyright 2008 Andrés Ambrois <andresambrois@gmail.com>
+#
+# 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 <www.circlesoft.com>
+# Copyright 2008 Andrés Ambrois <andresambrois@gmail.com>
+#
+# 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 @@
+<?xml version="1.0" ?><!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'><svg enable-background="new 0 0 55 55" height="55px" version="1.1" viewBox="0 0 55 55" width="55px" x="0px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" y="0px"><g display="block" id="view-refresh">
+ <g>
+ <path d="M27.6,4.943c12.429,0,22.5,10.076,22.5,22.498c0,12.426-10.07,22.502-22.5,22.502 c-12.428,0-22.5-10.078-22.5-22.502C5.101,15.019,15.173,4.943,27.6,4.943z" fill="#FFFFFF"/>
+ <path d="M25.5,37.994c-4.879-0.975-8.555-5.281-8.555-10.445 c0-5.885,4.77-10.656,10.652-10.656c5.886,0,10.656,4.771,10.656,10.656c0,5.074-3.551,9.322-8.305,10.395" fill="none" stroke="#4C4D4F" stroke-width="3.5"/>
+ <polyline fill="none" points=" 22.936,31.578 25.5,37.994 18.746,39.439 " stroke="#4C4D4F" stroke-linecap="round" stroke-linejoin="round" stroke-width="3.5"/>
+ </g>
+</g></svg> \ 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 <www.circlesoft.com>
+# Copyright 2008 Andrés Ambrois <andresambrois@gmail.com>
+#
+# 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 <EMAIL@ADDRESS>, 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 <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\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 <andresambrois@gmail.com>, 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 <andresambrois@gmail.com>\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')