class Bounce():
    ''' The Bounce class is used to define the ball and the user
    interaction. '''
    def __init__(self, canvas, path, parent=None):
        ''' Initialize the canvas and set up the callbacks. '''
        self.activity = parent

        if parent is None:  # Starting from command line
            self.sugar = False
            self.canvas = canvas
        else:  # Starting from Sugar
            self.sugar = True
            self.canvas = canvas
            parent.show_all()

        self.canvas.grab_focus()

        if os.path.exists(ACCELEROMETER_DEVICE):
            self.accelerometer = True
        else:
            self.accelerometer = False

        self.canvas.set_flags(gtk.CAN_FOCUS)
        self.canvas.add_events(gtk.gdk.BUTTON_PRESS_MASK)
        self.canvas.add_events(gtk.gdk.BUTTON_RELEASE_MASK)
        self.canvas.add_events(gtk.gdk.POINTER_MOTION_MASK)
        self.canvas.add_events(gtk.gdk.KEY_PRESS_MASK)
        self.canvas.add_events(gtk.gdk.KEY_RELEASE_MASK)
        self.canvas.connect('expose-event', self._expose_cb)
        self.canvas.connect('button-press-event', self._button_press_cb)
        self.canvas.connect('button-release-event', self._button_release_cb)
        self.canvas.connect('key_press_event', self._keypress_cb)
        self.canvas.connect('key_release_event', self._keyrelease_cb)
        self.width = gtk.gdk.screen_width()
        self.height = gtk.gdk.screen_height() - GRID_CELL_SIZE
        self.sprites = Sprites(self.canvas)
        self.scale = gtk.gdk.screen_height() / 900.0
        self.timeout = None

        self.buddies = []  # used for sharing
        self.my_turn = False
        self.select_a_fraction = False

        self.easter_egg = int(uniform(1, 100))

        # Find paths to sound files
        self.path_to_success = os.path.join(path, LAUGH)
        self.path_to_failure = os.path.join(path, CRASH)
        self.path_to_bubbles = os.path.join(path, BUBBLES)

        self._create_sprites(path)

        self.challenge = 0
        self.expert = False
        self.challenges = []
        for challenge in CHALLENGES[self.challenge]:
            self.challenges.append(challenge)
        self.fraction = 0.5  # the target of the current challenge
        self.label = '1/2'  # the label
        self.count = 0  # number of bounces played
        self.correct = 0  # number of correct answers
        self.press = None  # sprite under mouse click
        self.mode = 'fractions'
        self.new_bounce = False
        self.n = 0

        self.dx = 0.  # ball horizontal trajectory
        # acceleration (with dampening)
        self.ddy = (6.67 * self.height) / (STEPS * STEPS)
        self.dy = self.ddy * (1 - STEPS) / 2.  # initial step size

    def _create_sprites(self, path):
        ''' Create all of the sprites we'll need '''
        self.smiley_graphic = svg_str_to_pixbuf(
            svg_from_file(os.path.join(path, 'smiley.svg')))

        self.frown_graphic = svg_str_to_pixbuf(
            svg_from_file(os.path.join(path, 'frown.svg')))

        self.egg_graphic = svg_str_to_pixbuf(
            svg_from_file(os.path.join(path, 'Easter_egg.svg')))

        self.blank_graphic = svg_str_to_pixbuf(
            svg_header(REWARD_HEIGHT, REWARD_HEIGHT, 1.0) + \
            svg_rect(REWARD_HEIGHT, REWARD_HEIGHT, 5, 5, 0, 0,
                     '#C0C0C0', '#282828') + \
            svg_footer())

        self.ball = Ball(self.sprites, os.path.join(path, 'basketball.svg'))
        self.current_frame = 0

        self.bar = Bar(self.sprites, self.width, self.height, self.scale,
                       self.ball.width())
        self.current_bar = self.bar.get_bar(2)

        self.ball_y_max = self.bar.bar_y() - self.ball.height()
        self.ball.move_ball((int(
            (self.width - self.ball.width()) / 2), self.ball_y_max))

    def pause(self):
        ''' Pause play when visibility changes '''
        if self.timeout is not None:
            gobject.source_remove(self.timeout)
            self.timeout = None

    def we_are_sharing(self):
        ''' If there is more than one buddy, we are sharing. '''
        if len(self.buddies) > 1:
            return True

    def its_my_turn(self):
        ''' When sharing, it is your turn... '''
        gobject.timeout_add(1000, self._take_a_turn)

    def _take_a_turn(self):
        ''' On your turn, choose a fraction. '''
        self.my_turn = True
        self.select_a_fraction = True
        self.activity.set_player_on_toolbar(self.activity.nick)
        self.activity.challenge.set_label(
            _("Click on the bar to choose a fraction."))

    def its_their_turn(self, nick):
        ''' When sharing, it is nick's turn... '''
        gobject.timeout_add(1000, self._wait_your_turn, nick)

    def _wait_your_turn(self, nick):
        ''' Wait for nick to choose a fraction. '''
        self.my_turn = False
        self.activity.set_player_on_toolbar(nick)
        self.activity.challenge.set_label(
            _('Waiting for %(buddy)s') % {'buddy': nick})

    def play_a_fraction(self, fraction):
        ''' Play this fraction '''
        fraction_is_new = True
        for i, c in enumerate(self.challenges):
            if c[0] == fraction:
                fraction_is_new = False
                self.n = i
                break
        if fraction_is_new:
            self.add_fraction(fraction)
            self.n = len(self.challenges)
        self._choose_a_fraction()
        self._move_ball()

    def _button_press_cb(self, win, event):
        ''' Callback to handle the button presses '''
        win.grab_focus()
        x, y = map(int, event.get_coords())
        self.press = self.sprites.find_sprite((x, y))
        return True

    def _button_release_cb(self, win, event):
        ''' Callback to handle the button releases '''
        win.grab_focus()
        x, y = map(int, event.get_coords())
        if self.press is not None:
            if self.we_are_sharing():
                if self.select_a_fraction and self.press == self.current_bar:
                    # Find the fraction closest to the click
                    fraction = self._search_challenges(
                        (x - self.bar.bar_x()) / float(self.bar.width()))
                    self.select_a_fraction = False
                    self.activity.send_a_fraction(fraction)
                    self.play_a_fraction(fraction)
            else:
                if self.timeout is None and self.press == self.ball.ball:
                    self._choose_a_fraction()
                    self._move_ball()
        return True

    def _search_challenges(self, f):
        ''' Find the fraction which is closest to f in the list. '''
        dist = 1.
        closest = '1/2'
        for c in self.challenges:
            numden = c[0].split('/')
            delta = abs((float(numden[0]) / float(numden[1])) - f)
            if delta <= dist:
                dist = delta
                closest = c[0]
        return closest

    def _move_ball(self):
        ''' Move the ball and test boundary conditions '''
        if self.new_bounce:
            self.bar.mark.move((0, self.height))  # hide the mark
            if not self.we_are_sharing():
                self._choose_a_fraction()
            self.new_bounce = False
            self.dy = self.ddy * (1 - STEPS) / 2  # initial step size

        if self.accelerometer:
            fh = open(ACCELEROMETER_DEVICE)
            string = fh.read()
            xyz = string[1:-2].split(',')
            self.dx = float(xyz[0]) / 18.
            fh.close()

        if self.ball.ball_x() + self.dx > 0 and \
           self.ball.ball_x() + self.dx < self.width - self.ball.width():
            self.ball.move_ball_relative((int(self.dx), int(self.dy)))
        else:
            self.ball.move_ball_relative((0, int(self.dy)))

        # speed up ball in x while key is pressed
        self.dx *= DDX

        # accelerate in y
        self.dy += self.ddy

        if self.ball.ball_y() >= self.ball_y_max:
            # hit the bottom
            self.ball.move_ball((self.ball.ball_x(), self.ball_y_max))
            self._test()
            self.new_bounce = True

            if self.we_are_sharing():
                if self.my_turn:
                    # Let the next player know it is their turn.
                    i = (self.buddies.index(self.activity.nick) + 1) % \
                        len(self.buddies)
                    self.its_their_turn(self.buddies[i])
                    self.activity.send_event('t|%s' % (self.buddies[i]))
            else:
                if self._easter_egg_test():
                    self._animate()
                else:
                    self.timeout = gobject.timeout_add(
                        max(STEP_PAUSE,
                            BOUNCE_PAUSE - self.count * STEP_PAUSE),
                        self._move_ball)
        else:
            self.timeout = gobject.timeout_add(STEP_PAUSE, self._move_ball)

    def _animate(self):
        ''' A little Easter Egg just for fun. '''
        if self.new_bounce:
            self.dy = self.ddy * (1 - STEPS) / 2  # initial step size
            self.new_bounce = False
            self.current_frame = 0
            self.frame_counter = 0
            self.ball.move_frame(self.current_frame,
                                 (self.ball.ball_x(), self.ball.ball_y()))
            self.ball.move_ball((self.ball.ball_x(), self.height))
            gobject.idle_add(play_audio_from_file, self, self.path_to_bubbles)

        if self.accelerometer:
            fh = open(ACCELEROMETER_DEVICE)
            string = fh.read()
            xyz = string[1:-2].split(',')
            self.dx = float(xyz[0]) / 18.
            fh.close()
        else:
            self.dx = uniform(-int(DX * self.scale), int(DX * self.scale))
        self.ball.move_frame_relative(self.current_frame,
                                      (int(self.dx), int(self.dy)))
        self.dy += self.ddy

        self.frame_counter += 1
        self.current_frame = self.ball.next_frame(self.frame_counter)

        if self.ball.frame_y(self.current_frame) >= self.ball_y_max:
            # hit the bottom
            self.ball.move_ball((self.ball.ball_x(), self.ball_y_max))
            self.ball.hide_frames()
            self._test(easter_egg=True)
            self.new_bounce = True
            self.timeout = gobject.timeout_add(BOUNCE_PAUSE, self._move_ball)
        else:
            gobject.timeout_add(STEP_PAUSE, self._animate)

    def add_fraction(self, string):
        ''' Add a new challenge; set bar to 2x demominator '''
        numden = string.split('/', 2)
        self.challenges.append([string, int(numden[1]), 0])

    def _choose_a_fraction(self):
        ''' Select a new fraction challenge from the table '''
        if not self.we_are_sharing():
            self.n = int(uniform(0, len(self.challenges)))
        fstr = self.challenges[self.n][0]
        saw_a_fraction = False
        if '/' in fstr:  # fraction
            numden = fstr.split('/', 2)
            self.fraction = float(numden[0].strip()) / float(numden[1].strip())
            saw_a_fraction = True
        elif '%' in fstr:  # percentage
            self.fraction = float(fstr.strip().strip('%').strip()) / 100.
        else:  # To do: add support for decimals (using locale)
            _logger.debug('Could not parse challenge (%s)', fstr)
            fstr = '1/2'
            self.fraction = 0.5
            saw_a_fraction = True

        if self.mode == 'fractions':
            if saw_a_fraction:
                self.label = fstr
            else:
                self.label = fstr.strip().strip('%').strip() + '/100'
        else:  # percentage
            if not saw_a_fraction:
                self.label = fstr
            else:
                self.label = str(int(self.fraction * 100 + 0.5)) + '%'
        self.activity.reset_label(self.label)
        self.ball.ball.set_label(self.label)

        self.bar.hide_bars()
        if self.expert:  # Show two-segment bar in expert mode
            self.current_bar = self.bar.get_bar(2)
        else:
            if self.mode == 'fractions':
                nseg = self.challenges[self.n][1]
            else:
                nseg = 10  # percentages
            # generate new bar on demand
            self.current_bar = self.bar.get_bar(nseg)
            self.current_bar.move((self.bar.bar_x(), self.bar.bar_y()))
        self.current_bar.set_layer(0)

    def _easter_egg_test(self):
        ''' Test to see if we show the Easter Egg '''
        delta = self.ball.width() / 8
        x = self.ball.ball_x() + self.ball.width() / 2
        f = self.bar.width() * self.easter_egg / 100.
        if x > f - delta and x < f + delta:
            return True
        else:
            return False

    def _test(self, easter_egg=False):
        ''' Test to see if we estimated correctly '''
        self.timeout = None
        delta = self.ball.width() / 4
        x = self.ball.ball_x() + self.ball.width() / 2
        f = self.ball.width() / 2 + int(self.fraction * self.bar.width())
        self.bar.mark.move(
            (int(f - self.bar.mark_width() / 2), self.bar.bar_y() - 2))
        if self.challenges[self.n][2] == 0:  # label the column
            spr = Sprite(self.sprites, 0, 0, self.blank_graphic)
            spr.set_label(self.label)
            spr.move((int(self.n * 25), 0))
            spr.set_layer(-1)
        self.challenges[self.n][2] += 1
        if x > f - delta and x < f + delta:
            if not easter_egg:
                spr = Sprite(self.sprites, 0, 0, self.smiley_graphic)
            self.correct += 1
            gobject.idle_add(play_audio_from_file, self, self.path_to_success)
        else:
            if not easter_egg:
                spr = Sprite(self.sprites, 0, 0, self.frown_graphic)
            gobject.idle_add(play_audio_from_file, self, self.path_to_failure)

        if easter_egg:
            spr = Sprite(self.sprites, 0, 0, self.egg_graphic)

        spr.move((int(self.n * 25), int(self.challenges[self.n][2] * 25)))
        spr.set_layer(-1)

        # after enough correct answers, up the difficulty
        if self.correct == len(self.challenges) * 2:
            self.challenge += 1
            if self.challenge < len(CHALLENGES):
                for challenge in CHALLENGES[self.challenge]:
                    self.challenges.append(challenge)
            else:
                self.expert = True

        self.count += 1
        self.dx = 0.  # stop horizontal movement between bounces

    def _keypress_cb(self, area, event):
        ''' Keypress: moving the slides with the arrow keys '''
        k = gtk.gdk.keyval_name(event.keyval)
        if k in ['h', 'Left', 'KP_Left']:
            self.dx = -DX * self.scale
        elif k in ['l', 'Right', 'KP_Right']:
            self.dx = DX * self.scale
        elif k in ['KP_Page_Up', 'Return']:
            self._choose_a_fraction()
            self._move_ball()
        else:
            self.dx = 0.
        return True

    def _keyrelease_cb(self, area, event):
        ''' Keyrelease: stop horizontal movement '''
        self.dx = 0.
        return True

    def _expose_cb(self, win, event):
        ''' Callback to handle window expose events '''
        self.sprites.redraw_sprites(event.area)
        return True

    def _destroy_cb(self, win, event):
        ''' Callback to handle quit '''
        gtk.main_quit()
Beispiel #2
0
class Bounce():
    ''' The Bounce class is used to define the ball and the user
    interaction. '''
    def __init__(self, canvas, path, parent=None):
        ''' Initialize the canvas and set up the callbacks. '''
        self._activity = parent
        self._fraction = None
        self._path = path

        if parent is None:  # Starting from command line
            self._sugar = False
        else:  # Starting from Sugar
            self._sugar = True

        self._canvas = canvas
        self._canvas.grab_focus()

        self._canvas.add_events(Gdk.EventMask.BUTTON_PRESS_MASK)
        self._canvas.add_events(Gdk.EventMask.BUTTON_RELEASE_MASK)
        self._canvas.add_events(Gdk.EventMask.POINTER_MOTION_MASK)
        self._canvas.add_events(Gdk.EventMask.KEY_PRESS_MASK)
        self._canvas.add_events(Gdk.EventMask.KEY_RELEASE_MASK)
        self._canvas.connect('draw', self.__draw_cb)
        self._canvas.connect('button-press-event', self._button_press_cb)
        self._canvas.connect('button-release-event', self._button_release_cb)
        self._canvas.connect('key-press-event', self._keypress_cb)
        self._canvas.connect('key-release-event', self._keyrelease_cb)
        self._canvas.set_can_focus(True)
        self._canvas.grab_focus()

        self._sprites = Sprites(self._canvas)

        self._width = Gdk.Screen.width()
        self._height = Gdk.Screen.height() - GRID_CELL_SIZE
        self._scale = Gdk.Screen.height() / 900.0

        self._step_sid = None  # repeating timeout between steps of ball move
        self._bounce_sid = None  # one-off timeout between bounces
        self.buddies = []  # used for sharing
        self._my_turn = False
        self.select_a_fraction = False

        self._easter_egg = int(uniform(1, 100))

        # Find paths to sound files
        self._path_to_success = os.path.join(path, LAUGH)
        self._path_to_failure = os.path.join(path, CRASH)
        self._path_to_bubbles = os.path.join(path, BUBBLES)

        self._create_sprites(path)

        self.mode = 'fractions'

        self._challenge = 0
        self._expert = False
        self._challenges = []
        for challenge in CHALLENGES[self._challenge]:
            self._challenges.append(challenge)
        self._fraction = 0.5  # the target of the current challenge
        self._label = '1/2'  # the label
        self.count = 0  # number of bounces played
        self._correct = 0  # number of correct answers
        self._press = None  # sprite under mouse click
        self._new_bounce = False
        self._n = 0
        self._accelerometer = self._check_accelerometer()
        self._accel_index = 0
        self._accel_flip = False
        self._accel_xy = [0, 0]
        self._guess_orientation()

        self._dx = 0.  # ball horizontal trajectory
        # acceleration (with dampening)
        self._ddy = (6.67 * self._height) / (STEPS * STEPS)
        self._dy = self._ddy * (1 - STEPS) / 2.  # initial step size

        if self._sugar:
            if _is_tablet_mode():
                self._activity.reset_label(
                    _('Click the ball to start. Rock the computer left '
                      'and right to move the ball.'))
            else:
                self._activity.reset_label(
                    _('Click the ball to start. Then use the arrow keys to '
                      'move the ball.'))

        self._keyrelease_id = None

    def _check_accelerometer(self):
        return os.path.exists(ACCELEROMETER_DEVICE) and _is_tablet_mode()

    def configure_cb(self, event):
        self._width = Gdk.Screen.width()
        self._height = Gdk.Screen.height() - GRID_CELL_SIZE
        self._scale = Gdk.Screen.height() / 900.0

        # We need to resize the backgrounds
        width, height = self._calc_background_size()
        for bg in list(self._backgrounds.keys()):
            if bg == 'custom':
                path = self._custom_dsobject.file_path
                pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(
                    path, width, height)
            else:
                pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(
                    os.path.join(self._path, 'images', bg), width, height)
            if Gdk.Screen.height() > Gdk.Screen.width():
                pixbuf = self._crop_to_portrait(pixbuf)

            self._backgrounds[bg] = pixbuf

        self._background = Sprite(self._sprites, 0, 0,
                                  self._backgrounds[self._current_bg])
        self._background.set_layer(-100)
        self._background.type = 'background'

        # and resize and reposition the bars
        self.bar.resize_all()
        self.bar.show_bar(2)
        self._current_bar = self.bar.get_bar(2)

        # Calculate a new accerlation based on screen height.
        self._ddy = (6.67 * self._height) / (STEPS * STEPS)

        self._guess_orientation()

    def _create_sprites(self, path):
        ''' Create all of the sprites we'll need '''
        self.smiley_graphic = svg_str_to_pixbuf(
            svg_from_file(os.path.join(path, 'images', 'smiley.svg')))

        self.frown_graphic = svg_str_to_pixbuf(
            svg_from_file(os.path.join(path, 'images', 'frown.svg')))

        self.blank_graphic = svg_str_to_pixbuf(
            svg_header(REWARD_HEIGHT, REWARD_HEIGHT, 1.0) + svg_rect(
                REWARD_HEIGHT, REWARD_HEIGHT, 5, 5, 0, 0, 'none', 'none') +
            svg_footer())

        self.ball = Ball(self._sprites,
                         os.path.join(path, 'images', 'soccerball.svg'))
        self._current_frame = 0

        self.bar = Bar(self._sprites, self.ball.width(), COLORS)
        self._current_bar = self.bar.get_bar(2)

        self.ball_y_max = \
            self.bar.bar_y() - self.ball.height() + int(BAR_HEIGHT / 2.)
        self.ball.move_ball((int(
            (self._width - self.ball.width()) // 2), self.ball_y_max))

        self._backgrounds = {}
        width, height = self._calc_background_size()
        pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(
            os.path.join(path, 'images', 'grass_background.png'), width,
            height)
        if Gdk.Screen.height() > Gdk.Screen.width():
            pixbuf = self._crop_to_portrait(pixbuf)

        self._backgrounds['grass_background.png'] = pixbuf

        self._background = Sprite(self._sprites, 0, 0, pixbuf)
        self._background.set_layer(-100)
        self._background.type = 'background'
        self._current_bg = 'grass_background.png'

    def _crop_to_portrait(self, pixbuf):
        tmp = GdkPixbuf.Pixbuf.new(0, True, 8, Gdk.Screen.width(),
                                   Gdk.Screen.height())
        x = int(Gdk.Screen.height() // 3)
        pixbuf.copy_area(x, 0, Gdk.Screen.width(), Gdk.Screen.height(), tmp, 0,
                         0)
        return tmp

    def _calc_background_size(self):
        if Gdk.Screen.height() > Gdk.Screen.width():
            height = Gdk.Screen.height()
            return int(4 * height // 3), height
        else:
            width = Gdk.Screen.width()
            return width, int(3 * width // 4)

    def new_background_from_image(self, path, dsobject=None):
        if path is None:
            path = dsobject.file_path
        width, height = self._calc_background_size()
        pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(path, width, height)

        if Gdk.Screen.height() > Gdk.Screen.width():
            pixbuf = self._crop_to_portrait(pixbuf)

        self._backgrounds['custom'] = pixbuf
        self.set_background('custom')
        self._custom_dsobject = dsobject
        self._current_bg = 'custom'

    def set_background(self, name):
        if name not in self._backgrounds:
            width, height = self._calc_background_size()
            pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(
                os.path.join(self._path, 'images', name), width, height)
            if Gdk.Screen.height() > Gdk.Screen.width():
                pixbuf = self._crop_to_portrait(pixbuf)
            self._backgrounds[name] = pixbuf
        self._background.set_image(self._backgrounds[name])
        self.bar.mark.hide()
        self._current_bar.hide()
        self.ball.ball.hide()
        self.ball.ball.set_layer(3)
        self._current_bar.set_layer(2)
        self.bar.mark.set_layer(1)
        self._current_bg = name
        self._canvas.queue_draw()

    def pause(self):
        ''' Pause play when visibility changes '''
        if self._step_sid is not None:
            GLib.source_remove(self._step_sid)
            self._step_sid = None

        if self._bounce_sid is not None:
            GLib.source_remove(self._bounce_sid)
            self._bounce_sid = None

    def we_are_sharing(self):
        ''' If there is more than one buddy, we are sharing. '''
        if len(self.buddies) > 1:
            return True

    def its_my_turn(self):
        ''' When sharing, it is your turn... '''
        GLib.timeout_add(1000, self._take_a_turn)

    def _take_a_turn(self):
        ''' On your turn, choose a fraction. '''
        self._my_turn = True
        self.select_a_fraction = True
        self._activity.set_player_on_toolbar(self._activity.nick,
                                             self._activity.key)
        self._activity.reset_label(_("Click on the bar to choose a fraction."))

    def its_their_turn(self, nick, key):
        ''' When sharing, it is nick's turn... '''
        GLib.timeout_add(1000, self._wait_your_turn, nick, key)

    def _wait_your_turn(self, nick, key):
        ''' Wait for nick to choose a fraction. '''
        self._my_turn = False
        self._activity.set_player_on_toolbar(nick, key)
        self._activity.reset_label(
            _('Waiting for %(buddy)s') % {'buddy': nick})

    def play_a_fraction(self, fraction):
        ''' Play this fraction '''
        fraction_is_new = True
        for i, c in enumerate(self._challenges):
            if c[0] == fraction:
                fraction_is_new = False
                self._n = i
                break
        if fraction_is_new:
            self.add_fraction(fraction)
            self._n = len(self._challenges)
        self._choose_a_fraction()
        self._start_step()

    def _button_press_cb(self, win, event):
        ''' Callback to handle the button presses '''
        win.grab_focus()
        x, y = list(map(int, event.get_coords()))
        self._press = self._sprites.find_sprite((x, y))
        return True

    def _button_release_cb(self, win, event):
        ''' Callback to handle the button releases '''
        win.grab_focus()
        x, y = list(map(int, event.get_coords()))
        if self._press is not None:
            if self.we_are_sharing():
                if self.select_a_fraction and self._press == self._current_bar:
                    # Find the fraction closest to the click
                    fraction = self._search_challenges(
                        (x - self.bar.bar_x()) / float(self.bar.width()))
                    self.select_a_fraction = False
                    self._activity.send_a_fraction(fraction)
                    self.play_a_fraction(fraction)
            else:
                if self._step_sid is None and \
                   self._bounce_sid is None and \
                   self._press == self.ball.ball:
                    self._choose_a_fraction()
                    self._start_step()
        return True

    def _search_challenges(self, f):
        ''' Find the fraction which is closest to f in the list. '''
        dist = 1.
        closest = '1/2'
        for c in self._challenges:
            numden = c[0].split('/')
            delta = abs((float(numden[0]) / float(numden[1])) - f)
            if delta <= dist:
                dist = delta
                closest = c[0]
        return closest

    def _guess_orientation(self):
        if self._accelerometer:
            fh = open(ACCELEROMETER_DEVICE)
            string = fh.read()
            fh.close()
            xyz = string[1:-2].split(',')
            x = int(xyz[0])
            y = int(xyz[1])
            self._accel_xy = [x, y]
            if abs(x) > abs(y):
                self._accel_index = 1  # Portrait mode
                self._accel_flip = x > 0
            else:
                self._accel_index = 0  # Landscape mode
                self._accel_flip = y < 0

    def _defer_bounce(self, ms):
        ''' Pause and then start the ball again '''
        self._bounce_sid = GLib.timeout_add(ms, self._bounce)

    def _bounce(self):
        ''' Start the ball again '''
        self._accelerometer = self._check_accelerometer()
        self._start_step()
        self._bounce_sid = None
        return False

    def _start_step(self):
        ''' Start the ball and keep moving until boundary conditions '''
        if self._step():
            self._step_sid = GLib.timeout_add(STEP_PAUSE, self._step)

    def _step(self):
        ''' Move the ball once and test boundary conditions '''
        if self._new_bounce:
            self.bar.mark.move((0, self._height))  # hide the mark
            if not self.we_are_sharing():
                self._choose_a_fraction()
            self._new_bounce = False
            self._dy = self._ddy * (1 - STEPS) / 2  # initial step size

        if self._accelerometer:
            self._guess_orientation()
            self._dx = float(self._accel_xy[self._accel_index]) / 18.
            if self._accel_flip:
                self._dx *= -1

        if self.ball.ball_x() + self._dx > 0 and \
           self.ball.ball_x() + self._dx < self._width - self.ball.width():
            self.ball.move_ball_relative((int(self._dx), int(self._dy)))
        else:
            self.ball.move_ball_relative((0, int(self._dy)))

        # speed up ball in x while key is pressed
        self._dx *= DDX

        # accelerate in y
        self._dy += self._ddy

        # Calculate a new ball_y_max depending on the x position
        self.ball_y_max = \
            self.bar.bar_y() - self.ball.height() + self._wedge_offset()

        if self.ball.ball_y() >= self.ball_y_max:
            # hit the bottom
            self.ball.move_ball((self.ball.ball_x(), self.ball_y_max))
            self._test()
            self._new_bounce = True

            if self.we_are_sharing():
                if self._my_turn:
                    # Let the next player know it is their turn.
                    i = (self.buddies.index(
                        [self._activity.nick, self._activity.key]) + 1) % \
                        len(self.buddies)
                    [nick, key] = self.buddies[i]
                    self.its_their_turn(nick, key)
                    self._activity.send_event('t', self.buddies[i])
            else:
                if not self.we_are_sharing() and self._easter_egg_test():
                    self._animate()
                else:
                    ms = max(STEP_PAUSE,
                             BOUNCE_PAUSE - self.count * STEP_PAUSE)
                    self._defer_bounce(ms)
            self._step_sid = None
            return False
        else:
            return True

    def _wedge_offset(self):
        return int(BAR_HEIGHT *
                   (1 - (self.ball.ball_x() / float(self.bar.width()))))

    def _mark_offset(self, x):
        return int(BAR_HEIGHT * (1 - (x / float(self.bar.width())))) - 12

    def _animate(self):
        ''' A little Easter Egg just for fun. '''
        if self._new_bounce:
            self._dy = self._ddy * (1 - STEPS) / 2  # initial step size
            self._new_bounce = False
            self._current_frame = 0
            self._frame_counter = 0
            self.ball.move_frame(self._current_frame,
                                 (self.ball.ball_x(), self.ball.ball_y()))
            self.ball.move_ball((self.ball.ball_x(), self._height))
            aplay.play(self._path_to_bubbles)

        if self._accelerometer:
            fh = open(ACCELEROMETER_DEVICE)
            string = fh.read()
            xyz = string[1:-2].split(',')
            self._dx = float(xyz[0]) / 18.
            fh.close()
        else:
            self._dx = uniform(-int(DX * self._scale), int(DX * self._scale))
        self.ball.move_frame_relative(self._current_frame,
                                      (int(self._dx), int(self._dy)))
        self._dy += self._ddy

        self._frame_counter += 1
        self._current_frame = self.ball.next_frame(self._frame_counter)

        if self.ball.frame_y(self._current_frame) >= self.ball_y_max:
            # hit the bottom
            self.ball.move_ball((self.ball.ball_x(), self.ball_y_max))
            self.ball.hide_frames()
            self._test(easter_egg=True)
            self._new_bounce = True
            self._defer_bounce(BOUNCE_PAUSE)
        else:
            GLib.timeout_add(STEP_PAUSE, self._animate)

    def add_fraction(self, string):
        ''' Add a new challenge; set bar to 2x demominator '''
        numden = string.split('/', 2)
        self._challenges.append([string, int(numden[1]), 0])

    def _get_new_fraction(self):
        ''' Select a new fraction challenge from the table '''
        if not self.we_are_sharing():
            n = int(uniform(0, len(self._challenges)))
        else:
            n = self._n
        fstr = self._challenges[n][0]
        if '/' in fstr:  # fraction
            numden = fstr.split('/', 2)
            fraction = float(numden[0].strip()) / float(numden[1].strip())
        elif '%' in fstr:  # percentage
            fraction = float(fstr.strip().strip('%').strip()) / 100.
        else:  # To do: add support for decimals (using locale)
            _logger.debug('Could not parse challenge (%s)', fstr)
            fstr = '1/2'
            fraction = 0.5
        return fraction, fstr, n

    def _choose_a_fraction(self):
        ''' choose a new fraction and set the corresponding bar '''
        # Don't repeat the same fraction twice in a row
        fraction, fstr, n = self._get_new_fraction()
        if not self.we_are_sharing():
            while fraction == self._fraction:
                fraction, fstr, n = self._get_new_fraction()

        self._fraction = fraction
        self._n = n
        if self.mode == 'percents':
            self._label = str(int(self._fraction * 100 + 0.5)) + '%'
        else:  # percentage
            self._label = fstr
        if self.mode == 'sectors':
            self.ball.new_ball_from_fraction(self._fraction)

        if not Gdk.Screen.width() < 1024:
            self._activity.reset_label(
                _('Bounce the ball to a position '
                  '%(fraction)s of the way from the left side of the bar.') %
                {'fraction': self._label})
        else:
            self._activity.reset_label(
                _('Bounce the ball to %(fraction)s') %
                {'fraction': self._label})

        self.ball.ball.set_label(self._label)

        self.bar.hide_bars()
        if self._expert:  # Show two-segment bar in expert mode
            nseg = 2
        else:
            if self.mode == 'percents':
                nseg = 10
            else:
                nseg = self._challenges[self._n][1]
        # generate new bar on demand
        self._current_bar = self.bar.get_bar(nseg)
        self.bar.show_bar(nseg)

    def _easter_egg_test(self):
        ''' Test to see if we show the Easter Egg '''
        delta = self.ball.width() / 8
        x = self.ball.ball_x() + self.ball.width() / 2
        f = self.bar.width() * self._easter_egg / 100.
        if x > f - delta and x < f + delta:
            return True
        else:
            return False

    def _test(self, easter_egg=False):
        ''' Test to see if we estimated correctly '''
        if self._expert:
            delta = self.ball.width() / 6
        else:
            delta = self.ball.width() / 3

        x = self.ball.ball_x() + self.ball.width() / 2
        f = int(self._fraction * self.bar.width())
        self.bar.mark.move((int(f - self.bar.mark_width() / 2),
                            int(self.bar.bar_y() + self._mark_offset(f))))
        if self._challenges[self._n][2] == 0:  # label the column
            spr = Sprite(self._sprites, 0, 0, self.blank_graphic)
            spr.set_label(self._label)
            spr.move((int(self._n * 27), 0))
            spr.set_layer(-1)
        self._challenges[self._n][2] += 1
        if x > f - delta and x < f + delta:
            spr = Sprite(self._sprites, 0, 0, self.smiley_graphic)
            self._correct += 1
            aplay.play(self._path_to_success)
        else:
            spr = Sprite(self._sprites, 0, 0, self.frown_graphic)
            aplay.play(self._path_to_failure)

        spr.move((int(self._n * 27), int(self._challenges[self._n][2] * 27)))
        spr.set_layer(-1)

        # after enough correct answers, up the difficulty
        if self._correct == len(self._challenges) * 2:
            self._challenge += 1
            if self._challenge < len(CHALLENGES):
                for challenge in CHALLENGES[self._challenge]:
                    self._challenges.append(challenge)
            else:
                self._expert = True

        self.count += 1
        self._dx = 0.  # stop horizontal movement between bounces

    def _keypress_cb(self, area, event):
        ''' Keypress: moving the slides with the arrow keys '''
        k = Gdk.keyval_name(event.keyval)
        if k in ['KP_Page_Down', 'KP_Home', 'h', 'Left', 'KP_Left']:
            self._dx = -DX * self._scale
        elif k in ['KP_Page_Up', 'KP_End', 'l', 'Right', 'KP_Right']:
            self._dx = DX * self._scale
        elif k in ['Return']:
            if self._step_sid:
                self._dy = -self._ddy * (1 - STEPS) * 2.
        else:
            self._dx = 0.
        if self._accel_flip:
            self._dx = -self._dx
        return True

    def _keyrelease_cb(self, area, event):
        ''' Keyrelease: stop horizontal movement '''
        def timer_cb():
            self._dx = 0.
            self._keyrelease_id = None
            return False

        if self._keyrelease_id is not None:
            GLib.source_remove(self._keyrelease_id)
        self._keyrelease_id = GLib.timeout_add(100, timer_cb)

        return True

    def __draw_cb(self, canvas, cr):
        self._sprites.redraw_sprites(cr=cr)

    def _destroy_cb(self, win, event):
        ''' Callback to handle quit '''
        Gtk.main_quit()
Beispiel #3
0
class Bounce():
    ''' The Bounce class is used to define the ball and the user
    interaction. '''

    def __init__(self, canvas, path, parent=None):
        ''' Initialize the canvas and set up the callbacks. '''
        self._activity = parent
        self._fraction = None
        self._path = path

        if parent is None:        # Starting from command line
            self._sugar = False
        else:                     # Starting from Sugar
            self._sugar = True

        self._canvas = canvas
        self._canvas.grab_focus()

        self._canvas.add_events(Gdk.EventMask.BUTTON_PRESS_MASK)
        self._canvas.add_events(Gdk.EventMask.BUTTON_RELEASE_MASK)
        self._canvas.add_events(Gdk.EventMask.POINTER_MOTION_MASK)
        self._canvas.add_events(Gdk.EventMask.KEY_PRESS_MASK)
        self._canvas.add_events(Gdk.EventMask.KEY_RELEASE_MASK)
        self._canvas.connect('draw', self.__draw_cb)
        self._canvas.connect('button-press-event', self._button_press_cb)
        self._canvas.connect('button-release-event', self._button_release_cb)
        self._canvas.connect('key-press-event', self._keypress_cb)
        self._canvas.connect('key-release-event', self._keyrelease_cb)
        self._canvas.set_can_focus(True)
        self._canvas.grab_focus()

        self._sprites = Sprites(self._canvas)

        self._width = Gdk.Screen.width()
        self._height = Gdk.Screen.height() - GRID_CELL_SIZE
        self._scale = Gdk.Screen.height() / 900.0

        self._timeout = None
        self.buddies = []  # used for sharing
        self._my_turn = False
        self.select_a_fraction = False

        self._easter_egg = int(uniform(1, 100))

        # Find paths to sound files
        self._path_to_success = os.path.join(path, LAUGH)
        self._path_to_failure = os.path.join(path, CRASH)
        self._path_to_bubbles = os.path.join(path, BUBBLES)

        self._create_sprites(path)

        self.mode = 'fractions'

        self._challenge = 0
        self._expert = False
        self._challenges = []
        for challenge in CHALLENGES[self._challenge]:
            self._challenges.append(challenge)
        self._fraction = 0.5  # the target of the current challenge
        self._label = '1/2'  # the label
        self.count = 0  # number of bounces played
        self._correct = 0  # number of correct answers
        self._press = None  # sprite under mouse click
        self._new_bounce = False
        self._n = 0
        self._accel_index = 0
        self._accel_flip = False
        self._accel_xy = [0, 0]
        self._guess_orientation()

        self._dx = 0.  # ball horizontal trajectory
        # acceleration (with dampening)
        self._ddy = (6.67 * self._height) / (STEPS * STEPS)
        self._dy = self._ddy * (1 - STEPS) / 2.  # initial step size

        if self._sugar:
            if _is_tablet_mode():
                self._activity.reset_label(
                    _('Click the ball to start. Rock the computer left '
                      'and right to move the ball.'))
            else:
                self._activity.reset_label(
                    _('Click the ball to start. Then use the arrow keys to '
                      'move the ball.'))

    def _accelerometer(self):
        return os.path.exists(ACCELEROMETER_DEVICE) and _is_tablet_mode()

    def configure_cb(self, event):
        self._width = Gdk.Screen.width()
        self._height = Gdk.Screen.height() - GRID_CELL_SIZE
        self._scale = Gdk.Screen.height() / 900.0

        # We need to resize the backgrounds
        width, height = self._calc_background_size()
        for bg in self._backgrounds.keys():
            if bg == 'custom':
                path = self._custom_dsobject.file_path
                pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(
                    path, width, height)
            else:
                pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(
                    os.path.join(self._path, 'images', bg),
                    width, height)
            if Gdk.Screen.height() > Gdk.Screen.width():
                pixbuf = self._crop_to_portrait(pixbuf)

            self._backgrounds[bg] = pixbuf

        self._background = Sprite(self._sprites, 0, 0,
                                  self._backgrounds[self._current_bg])
        self._background.set_layer(-100)
        self._background.type = 'background'

        # and resize and reposition the bars
        self.bar.resize_all()
        self.bar.show_bar(2)
        self._current_bar = self.bar.get_bar(2)

        # Calculate a new accerlation based on screen height.
        self._ddy = (6.67 * self._height) / (STEPS * STEPS)

        self._guess_orientation()

    def _create_sprites(self, path):
        ''' Create all of the sprites we'll need '''
        self.smiley_graphic = svg_str_to_pixbuf(svg_from_file(
            os.path.join(path, 'images', 'smiley.svg')))

        self.frown_graphic = svg_str_to_pixbuf(svg_from_file(
            os.path.join(path, 'images', 'frown.svg')))

        self.blank_graphic = svg_str_to_pixbuf(
            svg_header(REWARD_HEIGHT, REWARD_HEIGHT, 1.0) +
            svg_rect(REWARD_HEIGHT, REWARD_HEIGHT, 5, 5, 0, 0,
                     'none', 'none') +
            svg_footer())

        self.ball = Ball(self._sprites,
                         os.path.join(path, 'images', 'soccerball.svg'))
        self._current_frame = 0

        self.bar = Bar(self._sprites, self.ball.width(), COLORS)
        self._current_bar = self.bar.get_bar(2)

        self.ball_y_max = self.bar.bar_y() - self.ball.height() + \
                          int(BAR_HEIGHT / 2.)
        self.ball.move_ball((int((self._width - self.ball.width()) / 2),
                             self.ball_y_max))

        self._backgrounds = {}
        width, height = self._calc_background_size()
        pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(
            os.path.join(path, 'images', 'grass_background.png'),
            width, height)
        if Gdk.Screen.height() > Gdk.Screen.width():
            pixbuf = self._crop_to_portrait(pixbuf)

        self._backgrounds['grass_background.png'] = pixbuf

        self._background = Sprite(self._sprites, 0, 0, pixbuf)
        self._background.set_layer(-100)
        self._background.type = 'background'
        self._current_bg = 'grass_background.png'

    def _crop_to_portrait(self, pixbuf):
        tmp = GdkPixbuf.Pixbuf.new(0, True, 8, Gdk.Screen.width(),
                                   Gdk.Screen.height())
        x = int(Gdk.Screen.height() / 3)
        pixbuf.copy_area(x, 0, Gdk.Screen.width(), Gdk.Screen.height(),
                         tmp, 0, 0)
        return tmp

    def _calc_background_size(self):
        if Gdk.Screen.height() > Gdk.Screen.width():
            height = Gdk.Screen.height()
            return int(4 * height / 3), height
        else:
            width = Gdk.Screen.width()
            return width, int(3 * width / 4)

    def new_background_from_image(self, path, dsobject=None):
        if path is None:
            path = dsobject.file_path
        width, height = self._calc_background_size()
        pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(
            path, width, height)

        if Gdk.Screen.height() > Gdk.Screen.width():
            pixbuf = self._crop_to_portrait(pixbuf)

        self._backgrounds['custom'] = pixbuf
        self.set_background('custom')
        self._custom_dsobject = dsobject
        self._current_bg = 'custom'

    def set_background(self, name):
        if not name in self._backgrounds:
            width, height = self._calc_background_size()
            pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(
                os.path.join(self._path, 'images', name), width, height)
            if Gdk.Screen.height() > Gdk.Screen.width():
                pixbuf = self._crop_to_portrait(pixbuf)
            self._backgrounds[name] = pixbuf
        self._background.set_image(self._backgrounds[name])
        self.bar.mark.hide()
        self._current_bar.hide()
        self.ball.ball.hide()
        self.do_expose_event()
        self.ball.ball.set_layer(3)
        self._current_bar.set_layer(2)
        self.bar.mark.set_layer(1)
        self._current_bg = name

    def pause(self):
        ''' Pause play when visibility changes '''
        if self._timeout is not None:
            GObject.source_remove(self._timeout)
            self._timeout = None

    def we_are_sharing(self):
        ''' If there is more than one buddy, we are sharing. '''
        if len(self.buddies) > 1:
            return True

    def its_my_turn(self):
        ''' When sharing, it is your turn... '''
        GObject.timeout_add(1000, self._take_a_turn)

    def _take_a_turn(self):
        ''' On your turn, choose a fraction. '''
        self._my_turn = True
        self.select_a_fraction = True
        self._activity.set_player_on_toolbar(self._activity.nick)
        self._activity.reset_label(
            _("Click on the bar to choose a fraction."))

    def its_their_turn(self, nick):
        ''' When sharing, it is nick's turn... '''
        GObject.timeout_add(1000, self._wait_your_turn, nick)

    def _wait_your_turn(self, nick):
        ''' Wait for nick to choose a fraction. '''
        self._my_turn = False
        self._activity.set_player_on_toolbar(nick)
        self._activity.reset_label(
            _('Waiting for %(buddy)s') % {'buddy': nick})

    def play_a_fraction(self, fraction):
        ''' Play this fraction '''
        fraction_is_new = True
        for i, c in enumerate(self._challenges):
            if c[0] == fraction:
                fraction_is_new = False
                self._n = i
                break
        if fraction_is_new:
            self.add_fraction(fraction)
            self._n = len(self._challenges)
        self._choose_a_fraction()
        self._move_ball()

    def _button_press_cb(self, win, event):
        ''' Callback to handle the button presses '''
        win.grab_focus()
        x, y = map(int, event.get_coords())
        self._press = self._sprites.find_sprite((x, y))
        return True

    def _button_release_cb(self, win, event):
        ''' Callback to handle the button releases '''
        win.grab_focus()
        x, y = map(int, event.get_coords())
        if self._press is not None:
            if self.we_are_sharing():
                if self.select_a_fraction and self._press == self._current_bar:
                    # Find the fraction closest to the click
                    fraction = self._search_challenges(
                        (x - self.bar.bar_x()) / float(self.bar.width()))
                    self.select_a_fraction = False
                    self._activity.send_a_fraction(fraction)
                    self.play_a_fraction(fraction)
            else:
                if self._timeout is None and self._press == self.ball.ball:
                    self._choose_a_fraction()
                    self._move_ball()
        return True

    def _search_challenges(self, f):
        ''' Find the fraction which is closest to f in the list. '''
        dist = 1.
        closest = '1/2'
        for c in self._challenges:
            numden = c[0].split('/')
            delta = abs((float(numden[0]) / float(numden[1])) - f)
            if delta <= dist:
                dist = delta
                closest = c[0]
        return closest

    def _guess_orientation(self):
        if self._accelerometer():
            fh = open(ACCELEROMETER_DEVICE)
            string = fh.read()
            fh.close()
            xyz = string[1:-2].split(',')
            x = int(xyz[0])
            y = int(xyz[1])
            self._accel_xy = [x, y]
            if abs(x) > abs(y):
                self._accel_index = 1  # Portrait mode
                self._accel_flip = x > 0
            else:
                self._accel_index = 0  # Landscape mode
                self._accel_flip = y < 0

    def _move_ball(self):
        ''' Move the ball and test boundary conditions '''
        if self._new_bounce:
            self.bar.mark.move((0, self._height))  # hide the mark
            if not self.we_are_sharing():
                self._choose_a_fraction()
            self._new_bounce = False
            self._dy = self._ddy * (1 - STEPS) / 2  # initial step size

        if self._accelerometer():
            self._guess_orientation()
            self._dx = float(self._accel_xy[self._accel_index]) / 18.
            if self._accel_flip:
                self._dx *= -1

        if self.ball.ball_x() + self._dx > 0 and \
           self.ball.ball_x() + self._dx < self._width - self.ball.width():
            self.ball.move_ball_relative((int(self._dx), int(self._dy)))
        else:
            self.ball.move_ball_relative((0, int(self._dy)))

        # speed up ball in x while key is pressed
        self._dx *= DDX

        # accelerate in y
        self._dy += self._ddy

        # Calculate a new ball_y_max depending on the x position
        self.ball_y_max = self.bar.bar_y() - self.ball.height() + \
                          self._wedge_offset()

        if self.ball.ball_y() >= self.ball_y_max:
            # hit the bottom
            self.ball.move_ball((self.ball.ball_x(), self.ball_y_max))
            self._test()
            self._new_bounce = True

            if self.we_are_sharing():
                if self._my_turn:
                    # Let the next player know it is their turn.
                    i = (self.buddies.index(self._activity.nick) + 1) % \
                        len(self.buddies)
                    self.its_their_turn(self.buddies[i])
                    self._activity.send_event('', {"data": (self.buddies[i])})
            else:
                if not self.we_are_sharing() and self._easter_egg_test():
                    self._animate()
                else:
                    self._timeout = GObject.timeout_add(
                        max(STEP_PAUSE,
                            BOUNCE_PAUSE - self.count * STEP_PAUSE),
                        self._move_ball)
        else:
            self._timeout = GObject.timeout_add(STEP_PAUSE, self._move_ball)

    def _wedge_offset(self):
        return int(BAR_HEIGHT * (1 - (self.ball.ball_x() /
                                      float(self.bar.width()))))

    def _mark_offset(self, x):
        return int(BAR_HEIGHT * (1 - (x / float(self.bar.width())))) - 12

    def _animate(self):
        ''' A little Easter Egg just for fun. '''
        if self._new_bounce:
            self._dy = self._ddy * (1 - STEPS) / 2  # initial step size
            self._new_bounce = False
            self._current_frame = 0
            self._frame_counter = 0
            self.ball.move_frame(self._current_frame,
                                (self.ball.ball_x(), self.ball.ball_y()))
            self.ball.move_ball((self.ball.ball_x(), self._height))
            GObject.idle_add(play_audio_from_file, self, self._path_to_bubbles)

        if self._accelerometer():
            fh = open(ACCELEROMETER_DEVICE)
            string = fh.read()
            xyz = string[1:-2].split(',')
            self._dx = float(xyz[0]) / 18.
            fh.close()
        else:
            self._dx = uniform(-int(DX * self._scale), int(DX * self._scale))
        self.ball.move_frame_relative(
            self._current_frame, (int(self._dx), int(self._dy)))
        self._dy += self._ddy

        self._frame_counter += 1
        self._current_frame = self.ball.next_frame(self._frame_counter)

        if self.ball.frame_y(self._current_frame) >= self.ball_y_max:
            # hit the bottom
            self.ball.move_ball((self.ball.ball_x(), self.ball_y_max))
            self.ball.hide_frames()
            self._test(easter_egg=True)
            self._new_bounce = True
            self._timeout = GObject.timeout_add(BOUNCE_PAUSE, self._move_ball)
        else:
            GObject.timeout_add(STEP_PAUSE, self._animate)

    def add_fraction(self, string):
        ''' Add a new challenge; set bar to 2x demominator '''
        numden = string.split('/', 2)
        self._challenges.append([string, int(numden[1]), 0])

    def _get_new_fraction(self):
        ''' Select a new fraction challenge from the table '''
        if not self.we_are_sharing():
            n = int(uniform(0, len(self._challenges)))
        else:
            n = self._n
        fstr = self._challenges[n][0]
        if '/' in fstr:  # fraction
            numden = fstr.split('/', 2)
            fraction = float(numden[0].strip()) / float(numden[1].strip())
        elif '%' in fstr:  # percentage
            fraction = float(fstr.strip().strip('%').strip()) / 100.
        else:  # To do: add support for decimals (using locale)
            _logger.debug('Could not parse challenge (%s)', fstr)
            fstr = '1/2'
            fraction = 0.5
        return fraction, fstr, n

    def _choose_a_fraction(self):
        ''' choose a new fraction and set the corresponding bar '''
        # Don't repeat the same fraction twice in a row
        fraction, fstr, n = self._get_new_fraction()
        if not self.we_are_sharing():
            while fraction == self._fraction:
                fraction, fstr, n = self._get_new_fraction()

        self._fraction = fraction
        self._n = n
        if self.mode == 'percents':
            self._label = str(int(self._fraction * 100 + 0.5)) + '%'
        else:  # percentage
            self._label = fstr
        if self.mode == 'sectors':
            self.ball.new_ball_from_fraction(self._fraction)

        if not Gdk.Screen.width() < 1024:
            self._activity.reset_label(
                _('Bounce the ball to a position '
                  '%(fraction)s of the way from the left side of the bar.')
                % {'fraction': self._label})
        else:
            self._activity.reset_label(_('Bounce the ball to %(fraction)s')
                                       % {'fraction': self._label})

        self.ball.ball.set_label(self._label)

        self.bar.hide_bars()
        if self._expert:  # Show two-segment bar in expert mode
            nseg = 2
        else:
            if self.mode == 'percents':
                nseg = 10
            else:
                nseg = self._challenges[self._n][1]
        # generate new bar on demand
        self._current_bar = self.bar.get_bar(nseg)
        self.bar.show_bar(nseg)

    def _easter_egg_test(self):
        ''' Test to see if we show the Easter Egg '''
        delta = self.ball.width() / 8
        x = self.ball.ball_x() + self.ball.width() / 2
        f = self.bar.width() * self._easter_egg / 100.
        if x > f - delta and x < f + delta:
            return True
        else:
            return False

    def _test(self, easter_egg=False):
        ''' Test to see if we estimated correctly '''
        self._timeout = None

        if self._expert:
            delta = self.ball.width() / 6
        else:
            delta = self.ball.width() / 3

        x = self.ball.ball_x() + self.ball.width() / 2
        f = int(self._fraction * self.bar.width())
        self.bar.mark.move((int(f - self.bar.mark_width() / 2),
                            int(self.bar.bar_y() + self._mark_offset(f))))
        if self._challenges[self._n][2] == 0:  # label the column
            spr = Sprite(self._sprites, 0, 0, self.blank_graphic)
            spr.set_label(self._label)
            spr.move((int(self._n * 27), 0))
            spr.set_layer(-1)
        self._challenges[self._n][2] += 1
        if x > f - delta and x < f + delta:
            spr = Sprite(self._sprites, 0, 0, self.smiley_graphic)
            self._correct += 1
            GObject.idle_add(play_audio_from_file, self, self._path_to_success)
        else:
            spr = Sprite(self._sprites, 0, 0, self.frown_graphic)
            GObject.idle_add(play_audio_from_file, self, self._path_to_failure)

        spr.move((int(self._n * 27), int(self._challenges[self._n][2] * 27)))
        spr.set_layer(-1)

        # after enough correct answers, up the difficulty
        if self._correct == len(self._challenges) * 2:
            self._challenge += 1
            if self._challenge < len(CHALLENGES):
                for challenge in CHALLENGES[self._challenge]:
                    self._challenges.append(challenge)
            else:
                self._expert = True

        self.count += 1
        self._dx = 0.  # stop horizontal movement between bounces

    def _keypress_cb(self, area, event):
        ''' Keypress: moving the slides with the arrow keys '''
        k = Gdk.keyval_name(event.keyval)
        if k in ['KP_Page_Down', 'KP_Home', 'h', 'Left', 'KP_Left']:
            self._dx = -DX * self._scale
        elif k in ['KP_Page_Up', 'KP_End', 'l', 'Right', 'KP_Right']:
            self._dx = DX * self._scale
        elif k in ['Return']:
            self._choose_a_fraction()
            self._move_ball()
        else:
            self._dx = 0.
        if self._accel_flip:
            self._dx = -self._dx
        return True

    def _keyrelease_cb(self, area, event):
        ''' Keyrelease: stop horizontal movement '''
        self._dx = 0.
        return True

    def __draw_cb(self, canvas, cr):
        self._sprites.redraw_sprites(cr=cr)

    def do_expose_event(self, event=None):
        ''' Handle the expose-event by drawing '''
        # Restrict Cairo to the exposed area
        cr = self._activity.get_window().cairo_create()
        if event is None:
            cr.rectangle(0, 0, Gdk.Screen.width(), Gdk.Screen.height())
        else:
            cr.rectangle(event.area.x, event.area.y,
                         event.area.width, event.area.height)
        cr.clip()
        self._sprites.redraw_sprites(cr=cr)

    def _destroy_cb(self, win, event):
        ''' Callback to handle quit '''
        Gtk.main_quit()