Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/ColorDeductoActivity.py
diff options
context:
space:
mode:
Diffstat (limited to 'ColorDeductoActivity.py')
-rw-r--r--ColorDeductoActivity.py492
1 files changed, 492 insertions, 0 deletions
diff --git a/ColorDeductoActivity.py b/ColorDeductoActivity.py
new file mode 100644
index 0000000..a831c70
--- /dev/null
+++ b/ColorDeductoActivity.py
@@ -0,0 +1,492 @@
+# -*- coding: utf-8 -*-
+#Copyright (c) 2012 Walter Bender
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# You should have received a copy of the GNU General Public License
+# along with this library; if not, write to the Free Software
+# Foundation, 51 Franklin Street, Suite 500 Boston, MA 02110-1335 USA
+
+
+import gtk
+
+from sugar.activity import activity
+from sugar import profile
+try:
+ from sugar.graphics.toolbarbox import ToolbarBox
+ _have_toolbox = True
+except ImportError:
+ _have_toolbox = False
+
+if _have_toolbox:
+ from sugar.activity.widgets import ActivityToolbarButton
+ from sugar.activity.widgets import StopButton
+from sugar.graphics.objectchooser import ObjectChooser
+
+from toolbar_utils import button_factory, label_factory, separator_factory
+from utils import json_load, json_dump
+
+import telepathy
+import dbus
+from dbus.service import signal
+from dbus.gobject_service import ExportedGObject
+from sugar.presence import presenceservice
+from sugar.presence.tubeconn import TubeConnection
+
+from gettext import gettext as _
+
+from game import Game, LEVELS_TRUE, LEVELS_FALSE
+
+import logging
+_logger = logging.getLogger('color-deducto-activity')
+
+
+SERVICE = 'in.seeta.ColorDeducto'
+IFACE = SERVICE
+PATH = '/in/seeta/ColorDeductoActivity'
+
+
+class ColorDeductoActivity(activity.Activity):
+ """ Logic puzzle game """
+
+ def __init__(self, handle):
+ """ Initialize the toolbars and the game board """
+ try:
+ super(ColorDeductoActivity, self).__init__(handle)
+ except dbus.exceptions.DBusException, e:
+ _logger.error(str(e))
+
+ self.nick = profile.get_nick_name()
+ if profile.get_color() is not None:
+ self.colors = profile.get_color().to_string().split(',')
+ else:
+ self.colors = ['#A0FFA0', '#FF8080']
+
+ self.level = 0
+ self._correct = 0
+ self._playing = True
+ self._game_over = False
+
+ self._python_code = None
+
+ self._setup_toolbars(_have_toolbox)
+ self._setup_dispatch_table()
+
+ # Create a canvas
+ canvas = gtk.DrawingArea()
+ canvas.set_size_request(gtk.gdk.screen_width(), \
+ gtk.gdk.screen_height())
+ self.set_canvas(canvas)
+ canvas.show()
+ self.show_all()
+
+ self._game = Game(canvas, parent=self, colors=self.colors)
+
+ self._sharing = False
+ self._initiating = False
+ self._setup_presence_service()
+
+ if 'level' in self.metadata:
+ self.level = int(self.metadata['level'])
+ self.status.set_label(_('Resuming level %d') % (self.level + 1))
+ self._game.show_random()
+ else:
+ self._game.new_game()
+
+ def _setup_toolbars(self, have_toolbox):
+ """ Setup the toolbars. """
+
+ self.max_participants = 4
+
+ if have_toolbox:
+ toolbox = ToolbarBox()
+
+ # Activity toolbar
+ activity_button = ActivityToolbarButton(self)
+
+ toolbox.toolbar.insert(activity_button, 0)
+ activity_button.show()
+
+ self.set_toolbar_box(toolbox)
+ toolbox.show()
+ self.toolbar = toolbox.toolbar
+
+ else:
+ # Use pre-0.86 toolbar design
+ games_toolbar = gtk.Toolbar()
+ toolbox = activity.ActivityToolbox(self)
+ self.set_toolbox(toolbox)
+ toolbox.add_toolbar(_('Game'), games_toolbar)
+ toolbox.show()
+ toolbox.set_current_toolbar(1)
+ self.toolbar = games_toolbar
+
+ self._new_game_button = button_factory(
+ 'new-game', self.toolbar, self._new_game_cb,
+ tooltip=_('Start a new game.'))
+
+ if _have_toolbox:
+ separator_factory(toolbox.toolbar, False, True)
+
+ self._true_button = button_factory(
+ 'true', self.toolbar, self._true_cb,
+ tooltip=_('The pattern matches the rule.'))
+
+ self._false_button = button_factory(
+ 'false', self.toolbar, self._false_cb,
+ tooltip=_('The pattern does not match the rule.'))
+
+ if _have_toolbox:
+ separator_factory(toolbox.toolbar, False, True)
+
+ self._example_button = button_factory(
+ 'example', self.toolbar, self._example_cb,
+ tooltip=_('Explore some examples.'))
+
+ self.status = label_factory(self.toolbar, '')
+
+ if _have_toolbox:
+ separator_factory(toolbox.toolbar, True, False)
+
+ self._gear_button = button_factory(
+ 'view-source', self.toolbar,
+ self._gear_cb,
+ tooltip=_('Load a custom level.'))
+
+ if _have_toolbox:
+ stop_button = StopButton(self)
+ stop_button.props.accelerator = '<Ctrl>q'
+ toolbox.toolbar.insert(stop_button, -1)
+ stop_button.show()
+
+ def _new_game_cb(self, button=None):
+ ''' Start a new game. '''
+ if (not self._sharing) or self._initiating:
+ self._game_over = False
+ self._correct = 0
+ self.level = 0
+ if not self._playing:
+ self._example_cb()
+ self._game.new_game()
+ if self._initiating:
+ _logger.debug('sending new game and new grid')
+ self._send_new_game()
+ self._send_new_grid()
+ self.status.set_label(_('Playing level %d') % (self.level + 1))
+ else:
+ self.status.set_label(_('Only sharer can start a new game.'))
+
+ def _test_for_game_over(self):
+ ''' If we are at maximum levels, the game is over '''
+ if self.level == self._game.max_levels:
+ self.level = 0
+ self._game_over = True
+ self.status.set_label(_('Game over.'))
+ else:
+ self.status.set_label(_('Playing level %d') % (self.level + 1))
+ self._correct = 0
+ if (not self._sharing) or self._initiating:
+ self._game.show_random()
+ if self._initiating:
+ self._send_new_grid()
+
+ def _true_cb(self, button=None):
+ ''' Declare pattern true or show an example of a true pattern. '''
+ if self._game_over:
+ if (not self._sharing) or self._initiating:
+ self.status.set_label(_('Click on new game button to begin.'))
+ else:
+ self.status.set_label(_('Wait for sharer to start a new game.'))
+ return
+ if self._playing:
+ if self._game.this_pattern:
+ self._correct += 1
+ if self._correct == 5:
+ self.level += 1
+ self._test_for_game_over()
+ self.metadata['level'] = str(self.level)
+ else:
+ self.status.set_label(
+ _('%d correct answers.') % (self._correct))
+ if (not self._sharing) or self._initiating:
+ self._game.show_random()
+ if self._initiating:
+ self._send_new_grid()
+ else:
+ self.status.set_label(_('Pattern was false.'))
+ self._correct = 0
+ if (button is not None) and self._sharing:
+ self._send_true_button_click()
+ else:
+ self._game.show_true()
+
+ def _false_cb(self, button=None):
+ ''' Declare pattern false or show an example of a false pattern. '''
+ if self._game_over:
+ if (not self._sharing) or self._initiating:
+ self.status.set_label(_('Click on new game button to begin.'))
+ else:
+ self.status.set_label(_('Wait for sharer to start a new game.'))
+ return
+ if self._playing:
+ if not self._game.this_pattern:
+ self._correct += 1
+ if self._correct == 5:
+ self.level += 1
+ self._test_for_game_over()
+ else:
+ self.status.set_label(
+ _('%d correct answers.') % (self._correct))
+ if (not self._sharing) or self._initiating:
+ self._game.show_random()
+ if self._initiating:
+ self._send_new_grid()
+ else:
+ self.status.set_label(_('Pattern was true.'))
+ self._correct = 0
+ if (button is not None) and self._sharing:
+ self._send_false_button_click()
+ else:
+ self._game.show_false()
+
+ def _example_cb(self, button=None):
+ ''' Show examples or resume play of current level. '''
+ if self._playing:
+ self._example_button.set_icon('resume-play')
+ self._example_button.set_tooltip(_('Resume play'))
+ self._true_button.set_tooltip(
+ _('Show a pattern that matches the rule.'))
+ self._false_button.set_tooltip(
+ _('Show a pattern that does not match the rule.'))
+ self.status.set_label(
+ _('Explore patterns with the %s and %s buttons.') % ('☑', '☒'))
+ self._playing = False
+ else:
+ self._example_button.set_icon('example')
+ self._example_button.set_tooltip(_('Explore some examples.'))
+ self._true_button.set_tooltip(
+ _('The pattern matches the rule.'))
+ self._false_button.set_tooltip(
+ _('The pattern does not match the rule.'))
+ self.status.set_label(_('Playing level %d') % (self.level + 1))
+ self._playing = True
+ self._correct = 0
+
+ def _gear_cb(self, button=None):
+ ''' Load a custom level. '''
+ self.status.set_text(
+ _('Load a "True" pattern generator from the journal'))
+ self._chooser('org.laptop.Pippy',
+ self._load_python_code_from_journal)
+ if self._python_code is None:
+ return
+ LEVELS_TRUE.append(self._python_code)
+ self.status.set_text(
+ _('Load a "False" pattern generator from the journal'))
+ self._chooser('org.laptop.Pippy',
+ self._load_python_code_from_journal)
+ LEVELS_FALSE.append(self._python_code)
+ if self._python_code is None:
+ return
+ self.status.set_text(_('New level added'))
+ self._game.max_levels += 1
+
+ def _load_python_code_from_journal(self, dsobject):
+ ''' Read the Python code from the Journal object '''
+ self._python_code = None
+ try:
+ _logger.debug("opening %s " % dsobject.file_path)
+ file_handle = open(dsobject.file_path, "r")
+ self._python_code = file_handle.read()
+ file_handle.close()
+ except IOError:
+ _logger.debug("couldn't open %s" % dsobject.file_path)
+
+ def _chooser(self, filter, action):
+ ''' Choose an object from the datastore and take some action '''
+ chooser = None
+ try:
+ chooser = ObjectChooser(parent=self, what_filter=filter)
+ except TypeError:
+ chooser = ObjectChooser(
+ None, self,
+ gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT)
+ if chooser is not None:
+ try:
+ result = chooser.run()
+ if result == gtk.RESPONSE_ACCEPT:
+ dsobject = chooser.get_selected_object()
+ action(dsobject)
+ dsobject.destroy()
+ finally:
+ chooser.destroy()
+ del chooser
+
+ # Collaboration-related methods
+
+ # The sharer sends patterns and everyone shares whatever vote is
+ # cast first among all the sharer and joiners.
+
+ def _setup_presence_service(self):
+ ''' Setup the Presence Service. '''
+ self.pservice = presenceservice.get_instance()
+ self._initiating = None # sharing (True) or joining (False)
+
+ owner = self.pservice.get_owner()
+ self.owner = owner
+ self._share = ""
+ self.connect('shared', self._shared_cb)
+ self.connect('joined', self._joined_cb)
+
+ def _shared_cb(self, activity):
+ ''' Either set up initial share...'''
+ self._new_tube_common(True)
+
+ def _joined_cb(self, activity):
+ ''' ...or join an exisiting share. '''
+ self._new_tube_common(False)
+
+ def _new_tube_common(self, sharer):
+ ''' Joining and sharing are mostly the same... '''
+ if self._shared_activity is None:
+ _logger.debug("Error: Failed to share or join activity ... \
+ _shared_activity is null in _shared_cb()")
+ return
+
+ self._initiating = sharer
+ self._sharing = True
+
+ self.conn = self._shared_activity.telepathy_conn
+ self.tubes_chan = self._shared_activity.telepathy_tubes_chan
+ self.text_chan = self._shared_activity.telepathy_text_chan
+
+ self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES].connect_to_signal(
+ 'NewTube', self._new_tube_cb)
+
+ if sharer:
+ _logger.debug('This is my activity: making a tube...')
+ id = self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES].OfferDBusTube(
+ SERVICE, {})
+ else:
+ _logger.debug('I am joining an activity: waiting for a tube...')
+ self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES].ListTubes(
+ reply_handler=self._list_tubes_reply_cb,
+ error_handler=self._list_tubes_error_cb)
+
+ def _list_tubes_reply_cb(self, tubes):
+ ''' Reply to a list request. '''
+ for tube_info in tubes:
+ self._new_tube_cb(*tube_info)
+
+ def _list_tubes_error_cb(self, e):
+ ''' Log errors. '''
+ _logger.debug('Error: ListTubes() failed: %s' % (e))
+
+ def _new_tube_cb(self, id, initiator, type, service, params, state):
+ ''' Create a new tube. '''
+ _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)
+
+ tube_conn = TubeConnection(self.conn,
+ self.tubes_chan[telepathy.CHANNEL_TYPE_TUBES], id, \
+ group_iface=self.text_chan[telepathy.CHANNEL_INTERFACE_GROUP])
+
+ self.chattube = ChatTube(tube_conn, self._initiating, \
+ self._event_received_cb)
+
+ def _setup_dispatch_table(self):
+ ''' Associate tokens with commands. '''
+ self._processing_methods = {
+ 'n': [self._receive_new_game, 'new game'],
+ 'g': [self._receive_new_grid, 'get a new grid'],
+ 't': [self._receive_true_button_click, 'get a true button press'],
+ 'f': [self._receive_false_button_click, 'get a false button press'],
+ }
+
+ def _event_received_cb(self, event_message):
+ ''' Data from a tube has arrived. '''
+ if len(event_message) == 0:
+ return
+ try:
+ command, payload = event_message.split('|', 2)
+ except ValueError:
+ _logger.debug('Could not split event message %s' % (event_message))
+ return
+ self._processing_methods[command][0](payload)
+
+ def _send_new_game(self):
+ ''' Send a new game message to all players (only sharer sends grids) '''
+ self._send_event('n| ')
+
+ def _receive_new_game(self, payload):
+ ''' Receive a new game notification from the sharer. '''
+ self._game_over = False
+ self._correct = 0
+ self.level = 0
+ if not self._playing:
+ self._example_cb()
+ self.status.set_label(_('Playing level %d') % (self.level + 1))
+
+ def _send_new_grid(self):
+ ''' Send a new grid to all players (only sharer sends grids) '''
+ self._send_event('g|%s' % (json_dump(self._game.save_grid())))
+
+ def _receive_new_grid(self, payload):
+ ''' Receive a grid from the sharer. '''
+ (dot_list, boolean) = json_load(payload)
+ self._game.restore_grid(dot_list, boolean)
+
+ def _send_true_button_click(self):
+ ''' Send a true click to all the players '''
+ self._send_event('t|t')
+
+ def _receive_true_button_click(self, payload):
+ ''' When a button is clicked, everyone should react. '''
+ self._playing = True
+ self._true_cb()
+
+ def _send_false_button_click(self):
+ ''' Send a false click to all the players '''
+ self._send_event('f|f')
+
+ def _receive_false_button_click(self, payload):
+ ''' When a button is clicked, everyone should react. '''
+ self._playing = True
+ self._false_cb()
+
+ def _send_event(self, entry):
+ ''' Send event through the tube. '''
+ if hasattr(self, 'chattube') and self.chattube is not None:
+ self.chattube.SendText(entry)
+
+
+class ChatTube(ExportedGObject):
+ ''' Class for setting up tube for sharing '''
+
+ def __init__(self, tube, is_initiator, stack_received_cb):
+ super(ChatTube, self).__init__(tube, PATH)
+ self.tube = tube
+ self.is_initiator = is_initiator # Are we sharing or joining activity?
+ self.stack_received_cb = stack_received_cb
+ self.stack = ''
+
+ self.tube.add_signal_receiver(self._send_stack_cb, 'SendText', IFACE,
+ path=PATH, sender_keyword='sender')
+
+ def _send_stack_cb(self, text, sender=None):
+ if sender == self.tube.get_unique_name():
+ return
+ self.stack = text
+ self.stack_received_cb(text)
+
+ @signal(dbus_interface=IFACE, signature='s')
+ def SendText(self, text):
+ self.stack = text