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()
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()
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()