Example #1
0
class Spirolaterals:
    def __init__(self,
                 canvas,
                 colors,
                 parent,
                 score=0,
                 delay=500,
                 pattern=1,
                 last=None):
        self._canvas = canvas
        self._colors = colors
        self._parent = parent
        self.delay = delay
        self.score = score
        self.pattern = pattern
        self.last_pattern = last
        self._running = False

        self._turtle_canvas = None
        self._user_numbers = [1, 1, 1, 3, 2]
        self._active_index = 0

        self._sprites = Sprites(self._canvas)
        self._sprites.set_delay(True)

        size = max(Gdk.Screen.width(), Gdk.Screen.height())

        cr = self._canvas.get_property('window').cairo_create()
        self._turtle_canvas = cr.get_target().create_similar(
            cairo.CONTENT_COLOR, size, size)
        self._canvas.connect('draw', self.__draw_cb)

        self._cr = cairo.Context(self._turtle_canvas)
        self._cr.set_line_cap(1)  # Set the line cap to be round
        self._sprites.set_cairo_context(self._cr)

        self._canvas.set_can_focus(True)
        self._canvas.grab_focus()

        self._canvas.add_events(Gdk.EventMask.BUTTON_PRESS_MASK)

        self._canvas.connect('button-press-event', self._button_press_cb)
        self._canvas.connect('key_press_event', self._keypress_cb)

        self._width = Gdk.Screen.width()
        self._height = Gdk.Screen.height() - style.GRID_CELL_SIZE

        if self._width < self._height:
            self.i = 1
        else:
            self.i = 0

        self._calculate_scale_and_offset()

        self._numbers = []
        self._glownumbers = []
        self._create_number_sprites()
        self._create_turtle_sprites()
        self._create_results_sprites()

        self._set_color(colors[0])
        self._set_pen_size(4)

        self.reset_level()

    def _calculate_scale_and_offset(self):
        self.offset = 0
        if self.i == 0:
            self.scale = self._height / (900. - style.GRID_CELL_SIZE) * 1.25
            self.offset = (
                self._width -
                (self.sx(X1[self.i] + X2[self.i]) + self.ss(BS[self.i]))) / 2.
        else:
            self.scale = self._width / 900.
            self.offset = (self._width -
                           (self.sx(X1[self.i]) + self.ss(BS[self.i]))) / 2.

    def reset_level(self):
        self._width = Gdk.Screen.width()
        self._height = Gdk.Screen.height() - style.GRID_CELL_SIZE
        if self._width < self._height:
            self.i = 1
        else:
            self.i = 0

        self._calculate_scale_and_offset()

        self._show_background_graphics()
        self._show_user_numbers()

        self._get_goal()
        self._draw_goal()

        self._reset_sprites()

        if self.score > 0:
            self._parent.update_score(int(self.score))

    def _reset_sprites(self):
        x = self.sx(TX[self.i] - TS[self.i] / 2)
        y = self.sy(TY[self.i])
        self._target_turtle.move((x, y))

        x = self.sx(UX[self.i] - US[self.i] / 2)
        y = self.sy(UY[self.i])
        self._user_turtles[0].move((x, y))

        for i in range(5):
            for j in range(5):
                if self.i == 0:
                    x = self.sx(
                        NX[self.i]) + i * (self.ss(NS[self.i] + NO[self.i]))
                    y = self.sy(NY[self.i])
                else:
                    x = self.sx(NX[self.i])
                    y = self.sy(
                        NY[self.i]) + i * (self.ss(NS[self.i] + NO[self.i]))
                self._numbers[i][j].move((x, y))
                self._glownumbers[i][j].move((x, y))

        x = 0
        y = self.sy(GY[self.i])
        self._success.move((x, y))
        self._success.hide()
        self._failure.move((x, y))
        self._failure.hide()
        self._splot.hide()

        if self.last_pattern == self.pattern:
            self._parent.cyan.set_sensitive(True)

    def _keypress_cb(self, area, event):
        ''' Keypress: moving the slides with the arrow keys '''

        k = Gdk.keyval_name(event.keyval)
        if k in ['1', '2', '3', '4', '5']:
            self.do_stop()
            i = self._active_index
            j = int(k) - 1
            self._numbers[i][self._user_numbers[i] - 1].set_layer(HIDDEN_LAYER)
            self._numbers[i][j].set_layer(NUMBER_LAYER)
            self._user_numbers[i] = j + 1
            self.inval(self._numbers[i][j].rect)
        elif k in ['KP_Up', 'j', 'Up']:
            self.do_stop()
            i = self._active_index
            j = self._user_numbers[i]
            if j < 5:
                j += 1
            self._numbers[i][self._user_numbers[i] - 1].set_layer(HIDDEN_LAYER)
            self._numbers[i][j - 1].set_layer(NUMBER_LAYER)
            self._user_numbers[i] = j
            self.inval(self._numbers[i][j].rect)
        elif k in ['KP_Down', 'k', 'Down']:
            self.do_stop()
            i = self._active_index
            j = self._user_numbers[i]
            if j > 0:
                j -= 1
            self._numbers[i][self._user_numbers[i] - 1].set_layer(HIDDEN_LAYER)
            self._numbers[i][j - 1].set_layer(NUMBER_LAYER)
            self._user_numbers[i] = j
            self.inval(self._numbers[i][j].rect)
        elif k in ['KP_Left', 'h', 'Left']:
            self.do_stop()
            self._active_index -= 1
            self._active_index %= 5
        elif k in ['KP_Right', 'l', 'Right']:
            self.do_stop()
            self._active_index += 1
            self._active_index %= 5
        elif k in ['Return', 'KP_Page_Up', 'KP_End']:
            self.do_run()
        elif k in ['space', 'Esc', 'KP_Page_Down', 'KP_Home']:
            self.do_stop()
        else:
            logging.debug(k)

        self._canvas.grab_focus()

    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))
        if self.press is not None and self.press.type == 'number':
            self.do_stop()
            i = int(self.press.name.split(',')[0])
            self._active_index = i
            j = int(self.press.name.split(',')[1])
            j1 = (j + 1) % 5
            self._numbers[i][j1].set_layer(NUMBER_LAYER)
            self._numbers[i][j].set_layer(HIDDEN_LAYER)
            self._user_numbers[i] = j1 + 1
            self.inval(self._numbers[i][j].rect)

    def _create_results_sprites(self):
        x = 0
        y = self.sy(GY[self.i])
        self._success = Sprite(self._sprites, x, y,
                               self._parent.good_job_pixbuf())
        self._success.hide()
        self._failure = Sprite(self._sprites, x, y,
                               self._parent.try_again_pixbuf())
        self._failure.hide()

    def _create_turtle_sprites(self):
        x = self.sx(TX[self.i] - TS[self.i] / 2)
        y = self.sy(TY[self.i])
        pixbuf = self._parent.turtle_pixbuf()
        self._target_turtle = Sprite(self._sprites, x, y, pixbuf)
        self._user_turtles = []
        x = self.sx(UX[self.i] - US[self.i] / 2)
        y = self.sy(UY[self.i])
        self._user_turtles.append(Sprite(self._sprites, x, y, pixbuf))
        pixbuf = pixbuf.rotate_simple(270)
        self._user_turtles.append(Sprite(self._sprites, x, y, pixbuf))
        pixbuf = pixbuf.rotate_simple(270)
        self._user_turtles.append(Sprite(self._sprites, x, y, pixbuf))
        pixbuf = pixbuf.rotate_simple(270)
        self._user_turtles.append(Sprite(self._sprites, x, y, pixbuf))
        self._show_turtle(0)
        self._splot = Sprite(self._sprites, 0, 0, self._parent.splot_pixbuf())
        self._splot.hide()

    def _show_splot(self, x, y, dd, h):
        for i in range(4):
            self._user_turtles[i].hide()
        if h == 0:
            self._splot.move((x - int(dd / 2), y))
        elif h == 1:
            self._splot.move((x - dd, y - int(dd / 2)))
        elif h == 2:
            self._splot.move((x - int(dd / 2), y - dd))
        elif h == 3:
            self._splot.move((x, y - int(dd / 2)))
        self._splot.set_layer(SUCCESS_LAYER)
        self._failure.set_layer(SUCCESS_LAYER)

    def _show_turtle(self, t):
        for i in range(4):
            if i == t:
                self._user_turtles[i].set_layer(TURTLE_LAYER)
            else:
                self._user_turtles[i].hide()

    def _reset_user_turtle(self):
        x = self.sx(UX[self.i] - US[self.i] / 2)
        y = self.sy(UY[self.i])
        self._user_turtles[0].move((x, y))
        self._show_turtle(0)

    def _create_number_sprites(self):
        for i in range(5):
            self._numbers.append([])
            self._glownumbers.append([])
            for j in range(5):
                if self.i == 0:
                    x = self.sx(
                        NX[self.i]) + i * (self.ss(NS[self.i] + NO[self.i]))
                    y = self.sy(NY[self.i])
                else:
                    x = self.sx(NX[self.i])
                    y = self.sy(
                        NY[self.i]) + i * (self.ss(NS[self.i] + NO[self.i]))
                number = Sprite(
                    self._sprites, x, y,
                    self._parent.number_pixbuf(self.ss(NS[self.i]), j + 1,
                                               self._parent.sugarcolors[1]))
                number.type = 'number'
                number.name = '%d,%d' % (i, j)
                self._numbers[i].append(number)

                number = Sprite(
                    self._sprites, x, y,
                    self._parent.number_pixbuf(self.ss(NS[self.i]), j + 1,
                                               '#FFFFFF'))
                number.type = 'number'
                number.name = '%d,%d' % (i, j)
                self._glownumbers[i].append(number)

    def _show_user_numbers(self):
        # Hide the numbers
        for i in range(5):
            for j in range(5):
                self._numbers[i][j].set_layer(HIDDEN_LAYER)
                self._glownumbers[i][j].set_layer(HIDDEN_LAYER)
        # Show user numbers
        self._numbers[0][self._user_numbers[0] - 1].set_layer(NUMBER_LAYER)
        self._numbers[1][self._user_numbers[1] - 1].set_layer(NUMBER_LAYER)
        self._numbers[2][self._user_numbers[2] - 1].set_layer(NUMBER_LAYER)
        self._numbers[3][self._user_numbers[3] - 1].set_layer(NUMBER_LAYER)
        self._numbers[4][self._user_numbers[4] - 1].set_layer(NUMBER_LAYER)

    def _show_background_graphics(self):
        self._draw_pixbuf(self._parent.background_pixbuf(), 0, 0, self._width,
                          self._height)
        self._draw_pixbuf(self._parent.box_pixbuf(self.ss(BS[self.i])),
                          self.sx(X1[self.i]), self.sy(Y1[self.i]),
                          self.ss(BS[self.i]), self.ss(BS[self.i]))
        self._draw_pixbuf(self._parent.box_pixbuf(self.ss(BS[self.i])),
                          self.sx(X2[self.i]), self.sy(Y2[self.i]),
                          self.ss(BS[self.i]), self.ss(BS[self.i]))
        self._draw_text(self.pattern, self.sx(X1[self.i]), self.sy(Y1[self.i]),
                        self.ss(LS[self.i]))

    def _set_pen_size(self, ps):
        self._cr.set_line_width(ps)

    def _set_color(self, color):
        r = color[0] / 255.
        g = color[1] / 255.
        b = color[2] / 255.
        self._cr.set_source_rgb(r, g, b)

    def _draw_line(self, x1, y1, x2, y2):
        self._cr.move_to(x1, y1)
        self._cr.line_to(x2, y2)
        self._cr.stroke()

    def ss(self, f):  # scale size function
        return int(f * self.scale)

    def sx(self, f):  # scale x function
        return int(f * self.scale + self.offset)

    def sy(self, f):  # scale y function
        return int(f * self.scale)

    def _draw_pixbuf(self, pixbuf, x, y, w, h):
        self._cr.save()
        self._cr.translate(x + w / 2., y + h / 2.)
        self._cr.translate(-x - w / 2., -y - h / 2.)
        Gdk.cairo_set_source_pixbuf(self._cr, pixbuf, x, y)
        self._cr.rectangle(x, y, w, h)
        self._cr.fill()
        self._cr.restore()

    def _draw_text(self, label, x, y, size):
        pl = PangoCairo.create_layout(self._cr)
        fd = Pango.FontDescription('Sans')
        fd.set_size(int(size) * Pango.SCALE)
        pl.set_font_description(fd)
        if type(label) == str or type(label) == unicode:
            pl.set_text(label.replace('\0', ' '), -1)
        elif type(label) == float or type(label) == int:
            pl.set_text(str(label), -1)
        else:
            pl.set_text(str(label), -1)
        self._cr.save()
        self._cr.translate(x, y)
        self._cr.set_source_rgb(1, 1, 1)
        PangoCairo.update_layout(self._cr, pl)
        PangoCairo.show_layout(self._cr, pl)
        self._cr.restore()

    def inval(self, r):
        self._canvas.queue_draw_area(r[0], r[1], r[2], r[3])

    def inval_all(self):
        self._canvas.queue_draw_area(0, 0, self._width, self._height)

    def __draw_cb(self, canvas, cr):
        cr.set_source_surface(self._turtle_canvas)
        cr.paint()

        self._sprites.redraw_sprites(cr=cr)

    def do_stop(self):
        self._parent.green.set_sensitive(True)
        self._running = False

    def do_run(self):
        self._show_background_graphics()
        # TODO: Add turtle graphics
        self._success.hide()
        self._failure.hide()
        self._splot.hide()
        self._get_goal()
        self._draw_goal()
        self.inval_all()
        self._running = True
        self.loop = 0
        self._active_index = 0
        self.step = 0
        self._set_pen_size(4)
        self._set_color(self._colors[0])
        x1 = self.sx(UX[self.i])
        y1 = self.sy(UY[self.i])
        dd = self.ss(US[self.i])
        self._numbers[0][self._user_numbers[0] - 1].set_layer(HIDDEN_LAYER)
        self._glownumbers[0][self._user_numbers[0] - 1].set_layer(NUMBER_LAYER)
        self._user_turtles[0].move((int(x1 - dd / 2), y1))
        self._show_turtle(0)

        if self._running:
            GObject.timeout_add(self.delay, self._do_step, x1, y1, dd, 0)

    def _do_step(self, x1, y1, dd, h):
        if not self._running:
            return
        if self.loop > 3:
            return
        if h == 0:  # up
            x2 = x1
            y2 = y1 - dd
            self._user_turtles[h].move((int(x2 - dd / 2), int(y2 - dd)))
        elif h == 1:  # right
            x2 = x1 + dd
            y2 = y1
            self._user_turtles[h].move((int(x2), int(y2 - dd / 2)))
        elif h == 2:  # down
            x2 = x1
            y2 = y1 + dd
            self._user_turtles[h].move((int(x2 - dd / 2), int(y2)))
        elif h == 3:  # left
            x2 = x1 - dd
            y2 = y1
            self._user_turtles[h].move((int(x2 - dd), int(y2 - dd / 2)))
        self._show_turtle(h)

        if x2 < self.sx(X2[self.i]) or \
           x2 > self.sx(X2[self.i] + BS[self.i]) or \
           y2 < self.sy(Y2[self.i]) or \
           y2 > self.sy(Y2[self.i] + BS[self.i]):
            self.do_stop()
            self._show_splot(x2, y2, dd, h)

        self._draw_line(x1, y1, x2, y2)
        self.inval_all()
        self.step += 1
        i = self._active_index
        if self.step == self._user_numbers[i]:
            number = self._user_numbers[i] - 1
            self._numbers[i][number].set_layer(NUMBER_LAYER)
            self._glownumbers[i][number].set_layer(HIDDEN_LAYER)
            h += 1
            h %= 4
            self.step = 0
            self._active_index += 1
            if self._active_index == 5:
                self.loop += 1
                self._active_index = 0
            else:
                i = self._active_index
                number = self._user_numbers[i] - 1
                self._numbers[i][number].set_layer(HIDDEN_LAYER)
                self._glownumbers[i][number].set_layer(NUMBER_LAYER)

        if self.loop < 4 and self._running:
            GObject.timeout_add(self.delay, self._do_step, x2, y2, dd, h)
        elif self.loop == 4:  # Test to see if we win
            self._running = False
            self._parent.green.set_sensitive(True)
            self._reset_user_turtle()
            self._show_user_numbers()
            self._test_level()

    def _test_level(self):
        success = True
        for i in range(5):
            if self._user_numbers[i] != self._goal[i]:
                success = False
                break
        if success:
            self._do_success()
        else:
            self._do_fail()

    def _do_success(self):
        self._success.set_layer(SUCCESS_LAYER)
        self._parent.cyan.set_sensitive(True)
        if self.last_pattern != self.pattern:
            self.score += 6
            self.last_pattern = self.pattern
        self._parent.update_score(int(self.score))

    def _do_fail(self):
        self._failure.set_layer(SUCCESS_LAYER)
        self._parent.cyan.set_sensitive(False)

    def do_slider(self, value):
        self.delay = int(value)

    def do_button(self, bu):
        self._success.hide()
        self._failure.hide()
        if bu == 'cyan':  # Next level
            self.do_stop()
            self._splot.hide()
            self.pattern += 1
            if self.pattern == 123:
                self.pattern = 1
            self._get_goal()
            self._show_background_graphics()
            self._draw_goal()
            self._reset_user_turtle()
            self.inval_all()
            self._parent.cyan.set_sensitive(False)
        elif bu == 'green':  # Run level
            self._parent.green.set_sensitive(False)
            self.do_run()
        elif bu == 'red':  # Stop level
            self.do_stop()

    def _draw_goal(self):  # draws the left hand pattern
        x1 = self.sx(TX[self.i])
        y1 = self.sy(TY[self.i])
        dd = self.ss(TS[self.i])
        dx = 0
        dy = -dd
        for i in range(4):
            for j in self._goal:
                for k in range(j):
                    x2 = x1 + dx
                    y2 = y1 + dy
                    self._set_pen_size(4)
                    self._set_color(self._colors[0])
                    self._draw_line(x1, y1, x2, y2)
                    x1 = x2
                    y1 = y2
                if dy == -dd:
                    dx = dd
                    dy = 0
                elif dx == dd:
                    dx = 0
                    dy = dd
                elif dy == dd:
                    dx = -dd
                    dy = 0
                else:
                    dx = 0
                    dy = -dd

    def _get_goal(self):
        fname = os.path.join('data', 'patterns.dat')
        try:
            f = open(fname, 'r')
            for n in range(0, self.pattern):
                s = f.readline()
            s = s[0:5]
        except:
            s = 11132
            self.pattern = 1
        f.close
        l = [int(c) for c in str(s)]
        self._goal = l
Example #2
0
class Game():
    ''' OLPC XO man color changer designed in memory of Nat Jacobson '''

    def __init__(self, canvas, parent=None, mycolors=['#A0FFA0', '#FF8080']):
        self._activity = parent
        self.colors = [mycolors[0]]
        self.colors.append(mycolors[1])

        self._canvas = canvas
        if parent is not None:
            parent.show_all()
            self._parent = parent

        self._canvas.connect("draw", self.__draw_cb)
        self._canvas.add_events(Gdk.EventMask.BUTTON_PRESS_MASK)
        self._canvas.connect("button-press-event", self._button_press_cb)
        self._canvas.add_events(Gdk.EventMask.BUTTON_RELEASE_MASK)
        self._canvas.connect('button-release-event', self._button_release_cb)
        self._canvas.add_events(Gdk.EventMask.POINTER_MOTION_MASK)
        self._canvas.connect("motion-notify-event", self._mouse_move_cb)

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

        self.press = None
        self.dragpos = [0, 0]
        self.startpos = [0, 0]

        self._dot_cache = {}
        self._xo_cache = {}

        self._radius = 22.5
        self._stroke_width = 9.5

        # Generate the sprites we'll need...
        self._sprites = Sprites(self._canvas)
        self._sprites.set_delay(True)
        self._dots = []
        self._xo_man = None
        self._generate_bg('#FFF')

        # First dot, starting angle
        self._cxy = [self._width / 2, self._height / 2]
        self._xy = [self._width / 2 + 120 * self._scale,
                    self._height / 2 - self._radius * self._scale]
        self._angle = 0
        self._dot_size_plus = self._radius * 3 * self._scale
        self._min = -self._dot_size_plus / 3
        self._max = self._height - (self._dot_size_plus / 2.2)

        self._zones = []
        self._calc_zones()
        self._generate_spiral()
        self._sprites.draw_all()

    def _calc_zones(self):
        for color in colors:
            rgb1 = _from_hex(color[0])
            rgb2 = _from_hex(color[1])
            dv = _contrast(rgb1, rgb2)
            dh = _delta_hue(rgb1, rgb2)
            self._zones.append(_zone(dv, dh))

    def _calc_next_dot_position(self):
        ''' calculate spiral coordinates '''
        dx = self._xy[0] - self._cxy[0]
        dy = self._xy[1] - self._cxy[1]
        r = sqrt(dx * dx + dy * dy)
        c = 2 * r * pi
        a = atan2(dy, dx)
        da = (self._dot_size_plus / c) * 2 * pi
        a += da
        r += self._dot_size_plus / (c / self._dot_size_plus)
        self._xy[0] = r * cos(a) + self._cxy[0]
        self._xy[1] = r * sin(a) + self._cxy[1]
        if self._xy[1] < self._min or self._xy[1] > self._max:
            self._calc_next_dot_position()

    def _generate_spiral(self):
        ''' Make a new set of dots for a sprial '''
        for z in range(4):
            for i in range(len(colors)):
                if self._zones[i] == z:
                    self._dots.append(
                        Sprite(self._sprites, self._xy[0], self._xy[1],
                               self._new_dot(colors[i])))
                    self._dots[-1].type = i
                    self._calc_next_dot_position()
        if self._xo_man is None:
            x = 510 * self._scale
            y = 280 * self._scale
            self._xo_man = Sprite(self._sprites, x, y,
                                 self._new_xo_man(self.colors))
            self._xo_man.type = None

    def move_dot(self, i, x, y):
        self._dots[i].move((x, y))

    def get_dot_xy(self, i):
        return self._dots[i].get_xy()

    def move_xo_man(self, x, y):
        self._xo_man.move((x, y))

    def get_xo_man_xy(self):
        return self._xo_man.get_xy()

    def rotate(self):
        x, y = self._dots[0].get_xy()
        for i in range(len(colors) - 1):
            self._dots[i].move(self._dots[i + 1].get_xy())
        self._dots[-1].move((x, y))

    def _generate_bg(self, color):
        ''' a background color '''
        self._bg = Sprite(self._sprites, 0, 0, self._new_background(color))
        self._bg.set_layer(0)
        self._bg.type = None

    def adj_background(self, color):
        ''' Change background '''
        self._bg.set_image(self._new_background(color))
        self._bg.set_layer(0)

    def _button_press_cb(self, win, event):
        win.grab_focus()
        x, y = map(int, event.get_coords())
        self.dragpos = [x, y]

        spr = self._sprites.find_sprite((x, y))
        if spr == None or spr == self._bg:
            return
        self.startpos = spr.get_xy()
        self.press = spr

    def _mouse_move_cb(self, win, event):
        """ Drag a rule with the mouse. """
        if self.press is None:
            self.dragpos = [0, 0]
            return True
        win.grab_focus()
        x, y = map(int, event.get_coords())
        dx = x - self.dragpos[0]
        dy = y - self.dragpos[1]
        self.press.move_relative((dx, dy))
        self.dragpos = [x, y]

    def _button_release_cb(self, win, event):
        if self.press == None:
            return True
        if _distance(self.press.get_xy(), self.startpos) < 20:
            if type(self.press.type) == int:
                self.i = self.press.type
                self._new_surface()
            self.press.move(self.startpos)
        self.press = None

    def _new_surface(self):
        self.colors[0] = colors[self.i][0]
        self.colors[1] = colors[self.i][1]
        self._xo_man.set_image(self._new_xo_man(colors[self.i]))
        self._xo_man.set_layer(100)

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

    def do_expose_event(self, event):
        ''' Handle the expose-event by drawing '''
        # Restrict Cairo to the exposed area
        cr = self._canvas.window.cairo_create()
        cr.rectangle(event.area.x, event.area.y,
                event.area.width, event.area.height)
        cr.clip()
        # Refresh sprite list
        self._sprites.redraw_sprites(cr=cr)

    def _destroy_cb(self, win, event):
        Gtk.main_quit()

    def _new_dot(self, color):
        ''' generate a dot of a color color '''
        if True: # not color in self._dot_cache:
            self._stroke = color[0]
            self._fill = color[1]
            self._svg_width = int(60 * self._scale)
            self._svg_height = int(60 * self._scale)
            pixbuf = svg_str_to_pixbuf(
                self._header() + \
                '<circle cx="%f" cy="%f" r="%f" stroke="%s" fill="%s" \
stroke-width="%f" visibility="visible" />' % (
                        30 * self._scale, 30 * self._scale,
                        self._radius * self._scale, self._stroke,
                        self._fill, self._stroke_width * self._scale) + \
                self._footer())

            surface = cairo.ImageSurface(cairo.FORMAT_ARGB32,
                                         self._svg_width, self._svg_height)
            context = cairo.Context(surface)
            Gdk.cairo_set_source_pixbuf(context, pixbuf, 0, 0)
            context.rectangle(0, 0, self._svg_width, self._svg_height)
            context.fill()
            # self._dot_cache[color] = surface

        return surface  # self._dot_cache[color]

    def _new_background(self, color):
        ''' Background color '''
        self._svg_width = int(self._width)
        self._svg_height = int(self._height)
        string = \
            self._header() + \
            '<rect width="%f" height="%f" x="%f" \
y="%f" fill="%s" stroke="none" visibility="visible" />' % (
                    self._width, self._height, 0, 0, color) + \
            self._footer()
        pixbuf = svg_str_to_pixbuf(string)
        surface = cairo.ImageSurface(cairo.FORMAT_ARGB32,
                                     self._svg_width, self._svg_height)
        context = cairo.Context(surface)
        Gdk.cairo_set_source_pixbuf(context, pixbuf, 0, 0)
        context.rectangle(0, 0, self._svg_width, self._svg_height)
        context.fill()
        return surface

    def _new_xo_man(self, color):
        ''' generate a xo-man of a color color '''
        if True: # not color in self._xo_cache:
            self._stroke = color[0]
            self._fill = color[1]
            self._svg_width = int(240. * self._scale)
            self._svg_height = int(260. * self._scale)
            string = \
                self._header() + \
                '<g>' + \
                '<g id="XO">' + \
                '<path id="Line1" d="M%f,%f C%f,%f %f,%f %f,%f" stroke="%s" \
stroke-width="%f" stroke-linecap="round" fill="none" visibility="visible" />' \
% (
                        165.5 * self._scale, 97 * self._scale,
                        120 * self._scale, 140.5 * self._scale,
                        120 * self._scale, 140.5 * self._scale,
                        74.5 * self._scale, 188 * self._scale,
                        self._stroke, 37 * self._scale) + \
                '<path id="Line2" d="M%f,%f C%f,%f %f,%f %f,%f" stroke="%s" \
stroke-width="%f" stroke-linecap="round" fill="none" visibility="visible" />' \
% (
                        165.5 * self._scale, 188 * self._scale,
                        120 * self._scale, 140.5 * self._scale,
                        120 * self._scale, 140.5 * self._scale,
                        74.5 * self._scale, 97 * self._scale,
                        self._stroke, 37 * self._scale) + \
                '<path id="Fill1" d="M%f,%f C%f,%f %f,%f %f,%f" stroke="%s" \
stroke-width="%f" stroke-linecap="round" fill="none" visibility="visible" />' \
% (
                        165.5 * self._scale, 97 * self._scale,
                        120 * self._scale, 140.5 * self._scale,
                        120 * self._scale, 140.5 * self._scale,
                        74.5 * self._scale, 188 * self._scale,
                        self._fill, 17 * self._scale) + \
                '<path id="Fill2" d="M%f,%f C%f,%f %f,%f %f,%f" stroke="%s" \
stroke-width="%f" stroke-linecap="round" fill="none" visibility="visible" />' \
% (
                        165.5 * self._scale, 188 * self._scale,
                        120 * self._scale, 140.5 * self._scale,
                        120 * self._scale, 140.5 * self._scale,
                        74.5 * self._scale, 97 * self._scale,
                        self._fill, 17 * self._scale) + \
                '<circle id="Circle" cx="%f" cy="%f" r="%f" \
fill="%s" stroke="%s" stroke-width="%f" visibility="visible" />' % (
                        120 * self._scale, 61.5 * self._scale,
                        27.5 * self._scale,
                        self._fill, self._stroke, 11 * self._scale) + \
                '</g></g>' + \
                self._footer()
            pixbuf = svg_str_to_pixbuf(string)

            surface = cairo.ImageSurface(cairo.FORMAT_ARGB32,
                                         self._svg_width, self._svg_height)
            context = cairo.Context(surface)
            Gdk.cairo_set_source_pixbuf(context, pixbuf, 0, 0)
            context.rectangle(0, 0, self._svg_width, self._svg_height)
            context.fill()
            # self._xo_cache[color] = surface
        return surface # self._xo_cache[color]

    def _header(self):
        return '<svg\n' + 'xmlns:svg="http:#www.w3.org/2000/svg"\n' + \
            'xmlns="http://www.w3.org/2000/svg"\n' + \
            'xmlns:xlink="http://www.w3.org/1999/xlink"\n' + \
            'version="1.1"\n' + 'width="' + str(self._svg_width) + '"\n' + \
            'height="' + str(self._svg_height) + '">\n'

    def _footer(self):
        return '</svg>\n'
Example #3
0
class Game():
    ''' The game play -- called from within Sugar or GNOME '''

    def __init__(self, canvas, parent=None):
        ''' Initialize the playing surface '''
        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.set_can_focus(True)

        self._canvas.add_events(Gdk.EventMask.TOUCH_MASK)
        self._canvas.add_events(Gdk.EventMask.BUTTON_PRESS_MASK)
        self._canvas.add_events(Gdk.EventMask.BUTTON_RELEASE_MASK)
        self._canvas.add_events(Gdk.EventMask.BUTTON_MOTION_MASK)

        self._canvas.connect('event', self.__event_cb)
        self._canvas.connect('draw', self.__draw_cb)

        self._width = Gdk.Screen.width()
        self._height = Gdk.Screen.height() - GRID_CELL_SIZE * 2
        if self._width < self._height:
            self.portrait = True
            self._scale = 0.8 * self._width / (CARD_HEIGHT * 5.5)
        else:
            self.portrait = False
            self._scale = 0.8 * self._height / (CARD_HEIGHT * 5.5)

        self._card_width = CARD_WIDTH * self._scale
        self._card_height = CARD_HEIGHT * self._scale
        self.custom_paths = [None, None, None, None, None, None, None, None,
                             None]
        self._sprites = Sprites(self._canvas)
        self._sprites.set_delay(True)
        self._press = None
        self.matches = 0
        self.robot_matches = 0
        self._match_area = []
        self._matches_on_display = False
        self._smiley = []
        self._frowny = []
        self._help = []
        self._stop_help = True
        self._failure = None
        self.clicked = []
        self.last_click = None
        self._drag_pos = [0, 0]
        self._start_pos = [0, 0]
        self.low_score = [-1, -1, -1]
        self.all_scores = []
        self.robot = False 
        self.robot_time = 0
        self.total_time = 0
        self.numberC = 0
        self.numberO = 0
        self.word_lists = None
        self.editing_word_list = False
        self.editing_custom_cards = False
        self._edit_card = None
        self._dead_key = None
        self._found_a_match = False
        self.level = 0
        self.card_type = 'pattern'
        self.buddies = []
        self._dealing = False
        self._the_game_is_over = False

        self.grid = Grid(self._width, self._height, self._card_width,
                         self._card_height)

        self._cards = []
        for i in range(DECKSIZE):
            self._cards.append(Card(scale=self._scale))

        self.deck = Deck(self._cards, scale=self._scale)

        for i in range(CARDS_IN_A_MATCH):
            self.clicked.append(Click())
            self._match_area.append(Card(scale=self._scale))
            self._match_area[-1].create(
                generate_match_card(self._scale), sprites=self._sprites)
            self._match_area[-1].spr.move(self.grid.match_to_xy(i))

        for i in range((ROW - 1) * COL):
            self._smiley.append(Card(scale=self._scale))
            self._smiley[-1].create(
                generate_smiley(self._scale), sprites=self._sprites)
            self._smiley[-1].spr.move(self.grid.grid_to_xy(i))
        self._smiley.append(Card(scale=self._scale))
        self._smiley[-1].create(
            generate_smiley(self._scale), sprites=self._sprites)
        self._smiley[-1].spr.move(self.grid.match_to_xy(3))
        self._smiley[-1].spr.hide()

        # A different frowny face for each type of error
        self._frowny.append(Card(self._scale))
        self._frowny[-1].create(
            generate_frowny_shape(self._scale), sprites=self._sprites)
        self._frowny[-1].spr.move(self.grid.match_to_xy(3))
        self._frowny.append(Card(self._scale))
        self._frowny[-1].create(
            generate_frowny_color(self._scale), sprites=self._sprites)
        self._frowny[-1].spr.move(self.grid.match_to_xy(3))
        self._frowny.append(Card(self._scale))
        self._frowny[-1].create(
            generate_frowny_texture(self._scale), sprites=self._sprites)
        self._frowny[-1].spr.move(self.grid.match_to_xy(3))
        self._frowny.append(Card(self._scale))
        self._frowny[-1].create(
            generate_frowny_number(self._scale), sprites=self._sprites)
        self._frowny[-1].spr.move(self.grid.match_to_xy(3))

        self._label = Card()
        self._label.create(generate_label(min(self._width, self._height),
                                          LABELH),
                           sprites=self._sprites)
        self._label.spr.move((0, 0))
        self._label.spr.set_label_attributes(24, horiz_align="left")

        self._labels = {'deck': '', 'match': '', 'clock': '', 'status': ''}

        Gdk.Screen.get_default().connect('size-changed', self._configure_cb)

    def _configure_cb(self, event):
        self.grid.stop_animation = True

        self._width = Gdk.Screen.width()
        self._height = Gdk.Screen.height() - GRID_CELL_SIZE * 2

        if self._width < self._height:
            self.portrait = True
        else:
            self.portrait = False

        self.grid.rotate(self._width, self._height)

        for i in range(CARDS_IN_A_MATCH):
            self._match_area[i].spr.move(self.grid.match_to_xy(i))
        self._smiley[-1].spr.move(self.grid.match_to_xy(3))
        for c in self._frowny:
            c.spr.move(self.grid.match_to_xy(3))

        for i, c in enumerate(self.clicked):
            if c.spr is not None:
                c.spr.move(self.grid.match_to_xy(i))

    def new_game(self, saved_state=None, deck_index=0):
        ''' Start a new game '''
        # If we were editing the word list, time to stop
        self.grid.stop_animation = True
        self.editing_word_list = False
        self.editing_custom_cards = False
        self._edit_card = None
        self._saved_state = saved_state
        self._deck_index = deck_index
        # Wait for any animations to stop before starting new game
        timeout = GObject.timeout_add(200, self._prepare_new_game)

    def _prepare_new_game(self):
        # If there is already a deck, hide it.
        if hasattr(self, 'deck'):
            self.deck.hide()

        self._dealing = False

        self._hide_clicked()

        self._matches_on_display = False
        self._failure = None

        self._hide_frowny()

        self._smiley[-1].spr.hide()

        if self._saved_state is not None:
            _logger.debug('Restoring state: %s' % (str(self._saved_state)))
            if self.card_type == 'custom':
                self.deck.create(self._sprites, self.card_type,
                                 [self.numberO, self.numberC],
                                 self.custom_paths,
                                 DIFFICULTY_LEVEL[self.level])
            else:
                self.deck.create(self._sprites, self.card_type,
                                 [self.numberO, self.numberC], self.word_lists,
                                 DIFFICULTY_LEVEL[self.level])
            self.deck.hide()
            self.deck.index = self._deck_index
            deck_start = ROW * COL + 3
            deck_stop = deck_start + self.deck.count()
            self._restore_word_list(self._saved_state[deck_stop +
                                                      3 * self.matches:])
            if self._saved_state[deck_start] is not None:
                self.deck.restore(self._saved_state[deck_start: deck_stop])
                self.grid.restore(self.deck, self._saved_state[0: ROW * COL])
                self._restore_matches(
                    self._saved_state[deck_stop: deck_stop + 3 * self.matches])
                self._restore_clicked(
                    self._saved_state[ROW * COL: ROW * COL + 3])
            else:
                self.deck.hide()
                self.deck.shuffle()
                self.grid.deal(self.deck)
                if not self._find_a_match():
                    self.grid.deal_extra_cards(self.deck)
                self.matches = 0
                self.robot_matches = 0
                self.match_list = []
                self.total_time = 0

        elif not self.joiner():
            _logger.debug('Starting new game.')
            if self.card_type == 'custom':
                self.deck.create(self._sprites, self.card_type,
                                 [self.numberO, self.numberC],
                                 self.custom_paths,
                                 DIFFICULTY_LEVEL[self.level])
            else:
                self.deck.create(self._sprites, self.card_type,
                                 [self.numberO, self.numberC], self.word_lists,
                                 DIFFICULTY_LEVEL[self.level])
            self.deck.hide()
            self.deck.shuffle()
            self.grid.deal(self.deck)
            if not self._find_a_match():
                self.grid.deal_extra_cards(self.deck)
            self.matches = 0
            self.robot_matches = 0
            self.match_list = []
            self.total_time = 0

        # When sharer starts a new game, joiners should be notified.
        if self.sharer():
            self.activity._send_event('J')

        self._update_labels()

        self._the_game_is_over = False

        if self._game_over():
            if hasattr(self, 'timeout_id') and self.timeout_id is not None:
                GObject.source_remove(self.timeout_id)
        else:
            if hasattr(self, 'match_timeout_id') and \
                    self.match_timeout_id is not None:
                GObject.source_remove(self.match_timeout_id)
            self._timer_reset()

        for i in range((ROW - 1) * COL):
            self._smiley[i].hide_card()

        self._sprites.draw_all()

    def _sharing(self):
        ''' Are we sharing? '''
        if self._sugar and hasattr(self.activity, 'chattube') and \
                self.activity.chattube is not None:
            return True
        return False

    def joiner(self):
        ''' Are you the one joining? '''
        if self._sharing() and not self.activity.initiating:
            return True
        return False

    def sharer(self):
        ''' Are you the one sharing? '''
        if self._sharing() and self.activity.initiating:
            return True
        return False

    def edit_custom_card(self):
        ''' Update the custom cards from the Journal '''
        if not self.editing_custom_cards:
            return

        # Set the card type to custom, and generate a new deck.
        self._hide_clicked()

        self.deck.hide()
        self.card_type = 'custom'
        if len(self.custom_paths) < 3:
            for i in range(len(self.custom_paths), 81):
                self.custom_paths.append(None)
        self.deck.create(self._sprites, self.card_type,
                         [self.numberO, self.numberC], self.custom_paths,
                         DIFFICULTY_LEVEL.index(HIGH))
        self.deck.hide()
        self.matches = 0
        self.robot_matches = 0
        self.match_list = []
        self.total_time = 0
        self._edit_card = None
        self._dead_key = None
        if hasattr(self, 'timeout_id') and self.timeout_id is not None:
            GObject.source_remove(self.timeout_id)

        # Fill the grid with custom cards.
        self.grid.restore(self.deck, CUSTOM_CARD_INDICIES)
        self.set_label('deck', '')
        self.set_label('match', '')
        self.set_label('clock', '')
        self.set_label('status', _('Edit the custom cards.'))

    def edit_word_list(self):
        ''' Update the word cards '''
        if not self.editing_word_list:
            if hasattr(self, 'text_entry'):
                self.text_entry.hide()
                self.text_entry.disconnect(self.text_event_id)
            return

        # Set the card type to words, and generate a new deck.
        self._hide_clicked()
        self.deck.hide()
        self.card_type = 'word'
        self.deck.create(self._sprites, self.card_type,
                         [self.numberO, self.numberC], self.word_lists,
                         DIFFICULTY_LEVEL.index(HIGH))
        self.deck.hide()
        self.matches = 0
        self.robot_matches = 0
        self.match_list = []
        self.total_time = 0
        self._edit_card = None
        self._dead_key = None
        if hasattr(self, 'timeout_id') and self.timeout_id is not None:
            GObject.source_remove(self.timeout_id)
        # Fill the grid with word cards.
        self.grid.restore(self.deck, WORD_CARD_INDICIES)
        self.set_label('deck', '')
        self.set_label('match', '')
        self.set_label('clock', '')
        self.set_label('status', _('Edit the word cards.'))

        if not hasattr(self, 'text_entry'):
            self.text_entry = Gtk.TextView()
            self.text_entry.set_wrap_mode(Gtk.WrapMode.WORD)
            self.text_entry.set_pixels_above_lines(0)
            self.text_entry.set_size_request(self._card_width,
                                             self._card_height)
            '''
            rgba = Gdk.RGBA()
            rgba.red, rgba.green, rgba.blue = rgb(self._colors[1])
            rgba.alpha = 1.
            self.text_entry.override_background_color(
            Gtk.StateFlags.NORMAL, rgba)
            '''
            font_text = Pango.font_description_from_string('24')
            self.text_entry.modify_font(font_text)
            self.activity.fixed.put(self.text_entry, 0, 0)

    def _text_focus_out_cb(self, widget=None, event=None):
        if self._edit_card is None:
            self.text_entry.hide()
            self.text_entry.disconnect(self.text_event_id)
        self._update_word_card()
        self.text_entry.hide()

    def _update_word_card(self):
        bounds = self.text_buffer.get_bounds()
        text = self.text_buffer.get_text(bounds[0], bounds[1], True)
        self._edit_card.spr.set_label(text)
        (i, j) = WORD_CARD_MAP[self._edit_card.index]
        self.word_lists[i][j] = text
        self._edit_card = None

    def __event_cb(self, widget, event):
        ''' Handle touch events '''
        if event.type in (Gdk.EventType.TOUCH_BEGIN,
                          Gdk.EventType.TOUCH_END,
                          Gdk.EventType.TOUCH_UPDATE,
                          Gdk.EventType.BUTTON_PRESS,
                          Gdk.EventType.BUTTON_RELEASE,
                          Gdk.EventType.MOTION_NOTIFY):
            x = event.get_coords()[1]
            y = event.get_coords()[2]
            if event.type == Gdk.EventType.TOUCH_BEGIN or \
                    event.type == Gdk.EventType.BUTTON_PRESS:
                self._button_press(x, y)
            elif event.type == Gdk.EventType.TOUCH_UPDATE or \
                    event.type == Gdk.EventType.MOTION_NOTIFY:
                self._drag_event(x, y)
            elif event.type == Gdk.EventType.TOUCH_END or \
                    event.type == Gdk.EventType.BUTTON_RELEASE:
                self._button_release(x, y)

    def _button_press_cb(self, win, event):
        ''' Look for a card under the button press and save its position. '''
        win.grab_focus()

        x, y = map(int, event.get_coords())
        self._button_press(x, y)

    def _button_press(self, x, y):
        # Turn off help animation
        if not self._stop_help:
            self._stop_help = True
            return True

        # Don't do anything if the game is over
        if self._the_game_is_over:
            return True

        # Don't do anything during a deal
        if self._dealing:
            return True

        # Find the sprite under the mouse.
        spr = self._sprites.find_sprite((x, y))

        # If there is a match showing, hide it.
        if self._matches_on_display:
            self.clean_up_match(share=True)

        # Nothing else to do.
        if spr is None:
            return True

        # Don't grab cards in the match pile.
        if spr in self.match_list:
            return True

        # Don't grab a card being animated.
        if True in self.grid.animation_lock:
            return True

        # Don't do anything if a card is already in motion
        if self._in_motion(spr):
            return True

        # Keep track of starting drag position.
        self._drag_pos = [x, y]
        self._start_pos = [x, y]

        # If the match area is full, we need to move a card back to the grid
        if self._failure is not None:
            if not self.grid.xy_in_match(spr.get_xy()):
                return True

        # We are only interested in cards in the deck.
        if self.deck.spr_to_card(spr) is not None:
            self._press = spr
            # Save its starting position so we can restore it if necessary
            if self._where_in_clicked(spr) is None:
                i = self._none_in_clicked()
                if i is None:
                    self._press = None
                else:
                    self.clicked[i].spr = spr
                    self.clicked[i].pos = spr.get_xy()
                    self.last_click = i
        else:
            self._press = None
        return True

    def clean_up_match(self, share=False):
        ''' Unselect clicked cards that are now in the match pile '''
        self._matches_on_display = False
        self._hide_clicked()
        self._smiley[-1].spr.hide()
        if share and self._sharing():
            self.activity._send_event('r:')

    def clean_up_no_match(self, spr, share=False):
        ''' Return last card played to grid '''
        if self.clicked[2].spr is not None and self.clicked[2].spr != spr:
            self.return_card_to_grid(2)
            self.last_click = 2
            if share and self._sharing():
                self.activity._send_event('R:2')
        self._hide_frowny()
        self._failure = None

    def _mouse_move_cb(self, win, event):
        ''' Drag the card with the mouse. '''
        win.grab_focus()
        x, y = map(int, event.get_coords())
        self._drag_event(x, y)

    def _drag_event(self, x, y):
        if self._press is None or self.editing_word_list or \
                self.editing_custom_cards:
            self._drag_pos = [0, 0]
            return True
        dx = x - self._drag_pos[0]
        dy = y - self._drag_pos[1]
        self._press.set_layer(5000)
        self._press.move_relative((dx, dy))
        self._drag_pos = [x, y]

    def _button_release_cb(self, win, event):
        ''' Lots of possibilities here between clicks and drags '''
        win.grab_focus()
        x, y = map(int, event.get_coords())
        self._button_release(x, y)

    def _button_release(self, x, y):
        # Maybe there is nothing to do.
        if self._press is None:
            if self.editing_word_list:
                self._text_focus_out_cb()
            self._drag_pos = [0, 0]
            return True

        self._press.set_layer(2000)

        # Determine if it was a click, a drag, or an aborted drag
        d = _distance((x, y), (self._start_pos[0], self._start_pos[1]))
        if d < self._card_width / 10:  # click
            move = 'click'
        elif d < self._card_width / 2:  # aborted drag
            move = 'abort'
        else:
            move = 'drag'

        # Determine status of card
        status = self.grid.spr_to_grid(self._press)

        if move == 'click':
            if self.editing_word_list:
                # Only edit one card at a time, so unselect other cards
                for i, c in enumerate(self.clicked):
                    if c.spr is not None and c.spr != self._press:
                        c.spr.set_label(
                            c.spr.labels[0].replace(CURSOR, ''))
                        c.spr = None  # Unselect
            elif self.editing_custom_cards:
                pass
            else:
                self.process_click(self._press)
        elif move == 'abort':
            i = self._where_in_clicked(self._press)
            self._press.move(self.clicked[i].pos)
        else:  # move == 'drag'
            move = self._process_drag(self._press, x, y)

        if move == 'abort':
            self._press = None
            return

        if self._sharing():
            if self.deck.spr_to_card(self._press) is not None:
                # Tell everyone about the card we just clicked
                self.activity._send_event(
                    'B:%d' % (self.deck.spr_to_card(self._press).index))
            i = self._where_in_clicked(self._press)
            if i is not None:
                self.activity._send_event('S:%d' % (i))
            elif self.last_click is not None:
                self.activity._send_event('S:%d' % (self.last_click))
            else:
                _logger.error('WARNING: Cannot find last click')
            self.last_click = None

        self.process_selection(self._press)
        self._press = None
        return

    def process_click(self, spr):
        ''' Either move the card to the match area or back to the grid.'''
        if self.grid.spr_to_grid(spr) is None:  # Return card to grid
            i = self._where_in_clicked(spr)
            if i is not None:
                self.return_card_to_grid(i)
                self.last_click = i
            self._hide_frowny()
            self._failure = None
        else:
            i = self._where_in_clicked(spr)
            if i is None:
                spr.move((self._start_pos))
            else:
                spr.set_layer(5000)
                self.grid.grid[self.grid.spr_to_grid(spr)] = None
                self.grid.display_match(spr, i)

    def _process_drag(self, spr, x, y):
        ''' Either drag to the match area, back to the grid, or to a
        new slot. '''
        move = 'drag'
        if self.grid.spr_to_grid(spr) is None:
            # Returning a card to the grid
            if (self.portrait and y < self.grid.bottom) or \
                    (not self.portrait and x > self.grid.left):
                i = self.grid.xy_to_grid((x, y))
                if self.grid.grid[i] is not None:
                    i = self.grid.find_an_empty_slot()
                spr.move(self.grid.grid_to_xy(i))
                self.grid.grid[i] = self.deck.spr_to_card(spr)
                i = self._where_in_clicked(spr)
                self.last_click = i
                self.clicked[i].reset()
                self._hide_frowny()
                self._failure = None
            # Move a click to a different match slot
            else:
                i = self._where_in_clicked(spr)
                j = self.grid.xy_to_match((x, y))
                if i == j:
                    spr.move(self.clicked[i].pos)
                else:
                    temp_spr = self.clicked[i].spr
                    self.clicked[i].spr = self.clicked[j].spr
                    self.clicked[j].spr = temp_spr
                    if self.clicked[i].spr is not None:
                        self.clicked[i].spr.move(self.grid.match_to_xy(i))
                    if self.clicked[j].spr is not None:
                        self.clicked[j].spr.move(self.grid.match_to_xy(j))
                move = 'abort'
        else:
            i = self._where_in_clicked(spr)
            if i is None:
                move = 'abort'
            # Moving a card to the match area
            elif (self.portrait and y > self.grid.bottom) or \
                    (not self.portrait and x < self.grid.left):
                self.grid.grid[self.grid.spr_to_grid(spr)] = None
                spr.move(self._match_area[i].spr.get_xy())
            else:  # Shuffle positions in match area
                j = self.grid.xy_to_grid((x, y))
                k = self.grid.xy_to_grid(self.clicked[i].pos)
                if j < 0 or k < 0 or j > 15 or k > 15 or j == k:
                    spr.move(self.clicked[i].pos)
                else:
                    tmp_card = self.grid.grid[k]
                    if self.grid.grid[j] is not None:
                        self.grid.grid[j].spr.move(self.grid.grid_to_xy(k))
                        spr.move(self.grid.grid_to_xy(j))
                        self.grid.grid[k] = self.grid.grid[j]
                        self.grid.grid[j] = tmp_card
                    else:
                        spr.move(self.grid.grid_to_xy(j))
                        self.grid.grid[j] = self.grid.grid[k]
                        self.grid.grid[k] = None
                move = 'abort'
                self.clicked[i].reset()

        self._consistency_check()
        return move

    def _consistency_check(self):
        ''' Make sure that the cards in the grid are really in the grid '''
        # Root cause: a race condition?
        for i in range(3):
            spr = self.clicked[i].spr
            if spr is not None:
                if not self.grid.xy_in_match(spr.get_xy()):
                    _logger.debug('card in both the grid and '
                                  'match area (%d)' % (i))
                    spr.move(self.grid.match_to_xy(i))

    def process_selection(self, spr):
        ''' After a card has been selected... '''
        if self.editing_word_list:  # Edit label of selected card
            x, y = spr.get_xy()
            if self._edit_card is not None:
                self._update_word_card()
            self._edit_card = self.deck.spr_to_card(spr)
            self.text_buffer = self.text_entry.get_buffer()
            self.text_entry.show()
            self.text_buffer.set_text(self._edit_card.spr.labels[0])
            self.activity.fixed.move(self.text_entry, x, y)
            self.text_event_id = self.text_entry.connect(
                'focus-out-event', self._text_focus_out_cb)
            self.text_entry.grab_focus()
        elif self.editing_custom_cards:
            # Only edit one card at a time, so unselect other cards
            for i, c in enumerate(self.clicked):
                if c.spr is not None and c.spr != spr:
                    c.spr = None
            # Choose an image from the Journal for a card
            self._edit_card = self.deck.spr_to_card(spr)
            self._choose_custom_card()
            # Regenerate the deck with the new card definitions
            self.deck.create(self._sprites, self.card_type,
                             [self.numberO, self.numberC],
                             self.custom_paths, DIFFICULTY_LEVEL[1])
            self.deck.hide()
            self.grid.restore(self.deck, CUSTOM_CARD_INDICIES)
        elif self._none_in_clicked() == None:
            # If we have three cards selected, test for a match.
            self._test_for_a_match()
            if self._matches_on_display:
                self._smiley[-1].spr.set_layer(100)
            elif not self._the_game_is_over and self._failure is not None:
                self._frowny[self._failure].spr.set_layer(100)
        return

    def _none_in_clicked(self):
        ''' Look for room on the click list '''
        for i, c in enumerate(self.clicked):
            if c.spr is None:
                return i
        return None

    def _where_in_clicked(self, spr):
        ''' Is the card already selected? '''
        for i, c in enumerate(self.clicked):
            if c.spr == spr:
                return i
        return None

    def add_to_clicked(self, spr, pos=[0, 0]):
        ''' Add a card to the selected list '''
        i = self._where_in_clicked(spr)
        if i is not None:
            self.last_click = i
        else:
            i = self._none_in_clicked()
            if i is None:
                _logger.error('WARNING: No room in clicked')
                self.last_click = None
                return
            self.clicked[i].spr = spr
            self.clicked[i].pos = pos
            self.last_click = i

    def _hide_clicked(self):
        ''' Hide the clicked cards '''
        for c in self.clicked:
            if c is not None:
                c.hide()

    def _hide_frowny(self):
        ''' Hide the frowny cards '''
        for c in self._frowny:
            c.spr.hide()

    def return_card_to_grid(self, i):
        ''' "Unclick" '''
        j = self.grid.find_an_empty_slot()
        if j is not None:
            self.grid.return_to_grid(self.clicked[i].spr, j, i)
            self.grid.grid[j] = self.deck.spr_to_card(self.clicked[i].spr)
            self.clicked[i].reset()

    def _game_over(self):
        ''' Game is over when the deck is empty and no more matches. '''
        if self.deck.empty() and not self._find_a_match():
            self._hide_frowny()
            self.set_label('deck', '')
            self.set_label('clock', '')
            self.set_label('status', '%s (%d:%02d)' %
                (_('Game over'), int(self.total_time / 60),
                 int(self.total_time % 60)))
            for i in range((ROW - 1) * COL):
                if self.grid.grid[i] == None:
                    self._smiley[i].show_card()
            self.match_timeout_id = GObject.timeout_add(
                2000, self._show_matches, 0)
            self._the_game_is_over = True
        elif self.grid.cards_in_grid() == DEAL + 3 \
                and not self._find_a_match():
            self._hide_frowny()
            self.set_label('deck', '')
            self.set_label('clock', '')
            self.set_label('status', _('unsolvable'))
            self._the_game_is_over = True
        return self._the_game_is_over

    def _test_for_a_match(self):
        ''' If we have a match, then we have work to do. '''
        if self._match_check([self.deck.spr_to_card(self.clicked[0].spr),
                              self.deck.spr_to_card(self.clicked[1].spr),
                              self.deck.spr_to_card(self.clicked[2].spr)],
                             self.card_type):
            # Stop the timer.
            if hasattr(self, 'timeout_id'):
                if self.timeout_id is not None:
                    GObject.source_remove(self.timeout_id)
                self.total_time += GObject.get_current_time() - self.start_time

            # Increment the match counter and add the match to the match list.
            self.matches += 1
            for c in self.clicked:
                self.match_list.append(c.spr)
            self._matches_on_display = True

            # Test to see if the game is over.
            if self._game_over():
                if hasattr(self, 'timeout_id'):
                    GObject.source_remove(self.timeout_id)
                if self.low_score[self.level] == -1:
                    self.low_score[self.level] = self.total_time
                elif self.total_time < self.low_score[self.level]:
                    self.low_score[self.level] = self.total_time
                    self.set_label('status', '%s (%d:%02d)' %
                        (_('New record'), int(self.total_time / 60),
                         int(self.total_time % 60)))
                # Round to nearest second
                self.all_scores.append(int(self.total_time + 0.5))
                if not self._sugar:
                    self.activity.save_score()
                else:
                    self._auto_increase_difficulty()
                return True
            else:
                # Wait a few seconds before dealing new cards.
                self._dealing = True
                GObject.timeout_add(2000, self._deal_new_cards)

            # Keep playing.
            self._update_labels()
            self._timer_reset()

        else:
            self._matches_on_display = False

    def _auto_increase_difficulty(self):
        ''' Auto advance levels '''
        if self.level == 2 and len(self.all_scores) > 3:
            sum = 0
            for i in range(3):
                sum += self.all_scores[-i - 1]
            if sum < 120:
                self.level = 0
                self.activity.intermediate_button.set_active(True)
        elif self.level == 0 and len(self.all_scores) > 8:
            sum = 0
            for i in range(3):
                sum += self.all_scores[-i - 1]
            if sum < 240:
                self.level = 1
                self.activity.expert_button.set_active(True)

    def _deal_new_cards(self):
        ''' Deal three new cards. '''
        self.grid.replace(self.deck)
        self.set_label('deck', '%d %s' %
                       (self.deck.cards_remaining(), _('cards')))
        # Consolidate the grid.
        self.grid.consolidate()
        # Test to see if we need to deal extra cards.
        if not self._find_a_match():
            self.grid.deal_extra_cards(self.deck)
            self._failure = None
        self._dealing = False

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

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

    # Handle the expose-event by drawing
    def do_expose_event(self, event):

        # Create the cairo context
        cr = self._canvas.window.cairo_create()

        # Restrict Cairo to the exposed area; avoid extra work
        cr.rectangle(event.area.x, event.area.y,
                event.area.width, event.area.height)
        cr.clip()

        # Refresh sprite list
        if cr is not None:
            self._sprites.redraw_sprites(cr=cr)

    def _destroy_cb(self, win, event):
        ''' This is the end '''
        Gtk.main_quit()

    def _update_labels(self):
        ''' Write strings to a label in the toolbar. '''
        self.set_label('deck', '%d %s' %
            (self.deck.cards_remaining(), _('cards')))
        self.set_label('status', '')
        if self.matches == 1:
            if self.robot_matches > 0:
                self.set_label('match', '%d (%d) %s' % (
                    self.matches - self.robot_matches, self.robot_matches,
                    _('match')))
            else:
                self.set_label('match', '%d %s' % (self.matches, _('match')))
        else:
            if self.robot_matches > 0:
                self.set_label('match', '%d (%d) %s' % (
                    self.matches - self.robot_matches, self.robot_matches,
                    _('matches')))
            else:
                self.set_label('match', '%d %s' % (self.matches, _('matches')))

    def set_label(self, label, s):
        ''' Update the toolbar labels '''
        if label in self._labels:
            self._labels[label] = s

        msg = "%s - %s - %s - %s" % (self._labels['deck'],
                                     self._labels['match'],
                                     self._labels['clock'],
                                     self._labels['status'])

        self._label.spr.set_label(msg)

    def _restore_clicked(self, saved_selected_indices):
        ''' Restore the selected cards upon resume or share. '''
        j = 0
        for i in saved_selected_indices:
            if i is None:
                self.clicked[j].reset()
            else:
                self.clicked[j].spr = self.deck.index_to_card(i).spr
                k = self.grid.spr_to_grid(self.clicked[j].spr)
                self.clicked[j].spr.move(self.grid.match_to_xy(j))
                self.clicked[j].pos = self.grid.match_to_xy(j)
                self.clicked[j].spr.set_layer(2000)
            j += 1
        self.process_selection(None)

    def _restore_matches(self, saved_match_list_indices):
        ''' Restore the match list upon resume or share. '''
        j = 0
        self.match_list = []
        for i in saved_match_list_indices:
            if i is not None:
                try:
                    self.match_list.append(self.deck.index_to_card(i).spr)
                except AttributeError:
                    _logger.debug('index %s was not found in deck' % (str(i)))

    def _restore_word_list(self, saved_word_list):
        ''' Restore the word list upon resume or share. '''
        if len(saved_word_list) == 9:
            for i in range(3):
                for j in range(3):
                    self.word_lists[i][j] = saved_word_list[i * 3 + j]

    def _counter(self):
        ''' Display of seconds since start_time. '''
        seconds = int(GObject.get_current_time() - self.start_time)
        self.set_label('clock', str(seconds))
        if self.robot and self.robot_time < seconds:
            self._find_a_match(robot_match=True)
        else:
            self.timeout_id = GObject.timeout_add(1000, self._counter)

    def _timer_reset(self):
        ''' Reset the timer for the robot '''
        self.start_time = GObject.get_current_time()
        self.timeout_id = None
        self._counter()

    def _show_matches(self, i):
        ''' Show all the matches as a simple animation. '''
        if i < self.matches and \
                i * CARDS_IN_A_MATCH < len(self.match_list):
            for j in range(CARDS_IN_A_MATCH):
                self.grid.display_match(
                    self.match_list[i * CARDS_IN_A_MATCH + j], j)
            self.match_timeout_id = GObject.timeout_add(
                2000, self._show_matches, i + 1)

    def _find_a_match(self, robot_match=False):
        ''' Check to see whether there are any matches on the board. '''
        # Before finding a match, return any cards from the match area
        if self._matches_on_display:
            if not self.deck.empty():
                self._matches_on_display = False
                GObject.timeout_add(1000, self.clean_up_match)
        else:
            for c in self.clicked:
                if c.spr is not None:
                    i = self.grid.find_an_empty_slot()
                    if i is not None:
                        c.spr.move(self.grid.grid_to_xy(i))
                        self.grid.grid[i] = self.deck.spr_to_card(c.spr)
                        c.reset()

        a = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
        for i in Permutation(a):  # TODO: really should be combination
            cardarray = [self.grid.grid[i[0]],
                         self.grid.grid[i[1]],
                         self.grid.grid[i[2]]]
            if self._match_check(cardarray, self.card_type):
                if robot_match:
                    # Stop animations before moving robot match
                    self.grid.stop_animation = True
                    self._robot_match(i)
                return True
        return False

    def _robot_match(self, i):
        ''' Robot finds a match '''
        for j in range(CARDS_IN_A_MATCH):
            # WARNING: Sometimes there is a race condition between the
            # robot delay and shoing a robot match.
            if self.grid.grid[i[j]] is not None:
                self.clicked[j].spr = self.grid.grid[i[j]].spr
                self.grid.grid[i[j]].spr.move(self.grid.match_to_xy(j))
            else:
                _logger.error('in robot match, grid[%d] is None' % (i[j]))
            self.grid.grid[i[j]] = None
        self.robot_matches += 1
        self._test_for_a_match()
        self._smiley[-1].spr.set_layer(100)
        self._matches_on_display = True

    def _match_check(self, cardarray, card_type):
        ''' For each attribute, either it is the same or different. '''
        for a in cardarray:
            if a is None:
                return False

        if (cardarray[0].shape + cardarray[1].shape +
            cardarray[2].shape) % 3 != 0:
            self._failure = 0
            return False
        if (cardarray[0].color + cardarray[1].color +
            cardarray[2].color) % 3 != 0:
            self._failure = 1
            return False
        if (cardarray[0].fill + cardarray[1].fill +
            cardarray[2].fill) % 3 != 0:
            self._failure = 2
            return False
        # Special case: only check number when shapes are the same
        if card_type == 'word':
            if cardarray[0].shape == cardarray[1].shape and \
                  cardarray[0].shape == cardarray[2].shape and \
                  (cardarray[0].num + cardarray[1].num + cardarray[2].num) % 3\
                  != 0:
                return False
        else:
            if (cardarray[0].num + cardarray[1].num +
                cardarray[2].num) % 3 != 0:
                self._failure = 3
                return False
        self._failure = None
        return True

    def _choose_custom_card(self):
        ''' Select a custom card from the Journal '''
        chooser = None
        name = None
        if hasattr(mime, 'GENERIC_TYPE_IMAGE'):
            # See #2398
            if 'image/svg+xml' not in \
                    mime.get_generic_type(mime.GENERIC_TYPE_IMAGE).mime_types:
                mime.get_generic_type(
                    mime.GENERIC_TYPE_IMAGE).mime_types.append('image/svg+xml')
            chooser = ObjectChooser(parent=self.activity,
                                    what_filter=mime.GENERIC_TYPE_IMAGE)
        else:
            try:
                chooser = ObjectChooser(parent=self, what_filter=None)
            except TypeError:
                chooser = ObjectChooser(
                    None, self.activity,
                    Gtk.DialogFlags.MODAL |
                    Gtk.DialogFlags.DESTROY_WITH_PARENT)

        if chooser is not None:
            try:
                result = chooser.run()
                if result == Gtk.ResponseType.ACCEPT:
                    jobject = chooser.get_selected_object()
                    if jobject and jobject.file_path:
                        name = jobject.metadata['title']
                        mime_type = jobject.metadata['mime_type']
                        _logger.debug('result of choose: %s (%s)' %
                                      (name, str(mime_type)))
            finally:
                chooser.destroy()
                del chooser

            if name is not None:
                self._find_custom_paths(jobject)

    def _find_custom_paths(self, jobject):
        ''' Associate a Journal object with a card '''
        found_a_sequence = False
        if self.custom_paths[0] is None:
            basename, suffix, i = _find_the_number_in_the_name(
                jobject.metadata['title'])
            ''' If this is the first card, try to find paths for other custom
            cards based on the name; else just load the card. '''
            if i >= 0:
                dsobjects, nobjects = datastore.find(
                    {'mime_type': [str(jobject.metadata['mime_type'])]})
                self.custom_paths = []
                if nobjects > 0:
                    for j in range(DECKSIZE):
                        for i in range(nobjects):
                            if dsobjects[i].metadata['title'] == \
                                    _construct_a_name(basename, j + 1, suffix):
                                self.custom_paths.append(dsobjects[i])
                                break

                if len(self.custom_paths) < 9:
                    for i in range(3, 81):
                        self.custom_paths.append(
                            self.custom_paths[int(i / 27)])
                elif len(self.custom_paths) < 27:
                    for i in range(9, 81):
                        self.custom_paths.append(
                            self.custom_paths[int(i / 9)])
                elif len(self.custom_paths) < 81:
                    for i in range(9, 81):
                        self.custom_paths.append(
                            self.custom_paths[int(i / 3)])
                found_a_sequence = True
                self.activity.metadata['custom_object'] = jobject.object_id
                self.activity.metadata['custom_mime_type'] = \
                    jobject.metadata['mime_type']

        if not found_a_sequence:
            grid_index = self.grid.spr_to_grid(self._edit_card.spr)
            self.custom_paths[grid_index] = jobject
            self.activity.metadata['custom_' + str(grid_index)] = \
                jobject.object_id

        self.card_type = 'custom'
        self.activity.button_custom.set_icon('new-custom-game')
        self.activity.button_custom.set_tooltip(_('New custom game'))
        return

    def _in_motion(self, spr):
        ''' Is the sprite in a grid or match position or in motion? '''
        x, y = spr.get_xy()
        if self.grid.xy_in_match((x, y)):
            return False
        if self.grid.xy_in_grid((x, y)):
            return False
        return True

    def help_animation(self):
        ''' Simple explanatory animation at start of play '''
        for i in range(22):
            path = os.path.join(activity.get_bundle_path(),
                                'images', 'help-%d.svg' % i)
            svg_str = svg_from_file(path)
            pixbuf = svg_str_to_pixbuf(svg_str, int(self._width / 2),
                                       int(self._height / 2))
            self._help.append(Sprite(self._sprites, int(self._width / 4),
                                     int(self._height / 4), pixbuf))
            self._help[-1].hide()

        self._help_index = 0
        self._stop_help = False
        self._help[self._help_index].set_layer(5000)
        self._help_timeout_id = GObject.timeout_add(2000, self._help_next)

    def _help_next(self):
        ''' Load the next frame in the animation '''
        self._help[self._help_index].hide()
        if self._stop_help:
            self._help = []
            return
        self._help_index += 1
        self._help_index %= len(self._help)
        self._help[self._help_index].set_layer(5000)
        if self._help_index in [0, 9, 10, 20, 21]:
            self._help_timeout_id = GObject.timeout_add(2000, self._help_next)
        else:
            self._help_timeout_id = GObject.timeout_add(1000, self._help_next)