コード例 #1
0
ファイル: sudoku.py プロジェクト: j4321/Sudoku-Tk
class Sudoku(Tk):
    def __init__(self, file=None):
        Tk.__init__(self, className="Sudoku-Tk")
        self.title("Sudoku-Tk")
        self.resizable(0, 0)
        self.protocol("WM_DELETE_WINDOW", self.quitter)
        cst.set_icon(self)
        self.columnconfigure(3, weight=1)

        # --- style
        bg = '#dddddd'
        activebg = '#efefef'
        pressedbg = '#c1c1c1'
        lightcolor = '#ededed'
        darkcolor = '#cfcdc8'
        bordercolor = '#888888'
        focusbordercolor = '#5E5E5E'
        disabledfg = '#999999'
        disabledbg = bg

        button_style_config = {'bordercolor': bordercolor,
                               'background': bg,
                               'lightcolor': lightcolor,
                               'darkcolor': darkcolor}

        button_style_map = {'background': [('active', activebg),
                                           ('disabled', disabledbg),
                                           ('pressed', pressedbg)],
                            'lightcolor': [('pressed', darkcolor)],
                            'darkcolor': [('pressed', lightcolor)],
                            'bordercolor': [('focus', focusbordercolor)],
                            'foreground': [('disabled', disabledfg)]}

        style = Style(self)
        style.theme_use(cst.STYLE)
        style.configure('TFrame', background=bg)
        style.configure('TLabel', background=bg)
        style.configure('TScrollbar', gripcount=0, troughcolor=pressedbg,
                        **button_style_config)
        style.map('TScrollbar', **button_style_map)
        style.configure('TButton', **button_style_config)
        style.map('TButton', **button_style_map)
        style.configure('TCheckutton', **button_style_config)
        style.map('TCheckutton', **button_style_map)
        self.option_add('*Toplevel.background', bg)
        self.option_add('*Menu.background', bg)
        self.option_add('*Menu.activeBackground', activebg)
        self.option_add('*Menu.activeForeground', "black")
        self.configure(bg=bg)

        style.configure("bg.TFrame", background="grey")
        style.configure("case.TFrame", background="white")
        style.configure("case.TLabel", background="white", foreground="black")
        style.configure("case_init.TFrame", background="lightgrey")
        style.configure("case_init.TLabel", background="lightgrey", foreground="black")
        style.configure("erreur.TFrame", background="white")
        style.configure("erreur.TLabel", background="white", foreground="red")
        style.configure("solution.TFrame", background="white")
        style.configure("solution.TLabel", background="white", foreground="blue")
        style.configure("pause.TLabel", foreground="grey", background='white')

        # --- images
        self.im_erreur = open_image(cst.ERREUR)
        self.im_pause = open_image(cst.PAUSE)
        self.im_restart = open_image(cst.RESTART)
        self.im_play = open_image(cst.PLAY)
        self.im_info = open_image(cst.INFO)
        self.im_undo = open_image(cst.UNDO)
        self.im_redo = open_image(cst.REDO)
        self.im_question = open_image(cst.QUESTION)

        # --- timer
        self.chrono = [0, 0]
        self.tps = Label(self, text=" %02i:%02i" % tuple(self.chrono),
                         font="Arial 16")
        self.debut = False  # la partie a-t-elle commencée ?
        self.chrono_on = False  # le chrono est-il en marche ?

        # --- buttons
        self.b_pause = Button(self, state="disabled", image=self.im_pause,
                              command=self.play_pause)
        self.b_restart = Button(self, state="disabled", image=self.im_restart,
                                command=self.recommence)
        self.b_undo = Button(self, image=self.im_undo, command=self.undo)
        self.b_redo = Button(self, image=self.im_redo, command=self.redo)

        # --- tooltips
        self.tooltip_wrapper = TooltipWrapper(self)
        self.tooltip_wrapper.add_tooltip(self.b_pause, _("Pause game"))
        self.tooltip_wrapper.add_tooltip(self.b_restart, _("Restart game"))
        self.tooltip_wrapper.add_tooltip(self.b_undo, _("Undo"))
        self.tooltip_wrapper.add_tooltip(self.b_redo, _("Redo"))

        # --- numbers
        frame_nb = Frame(self, style='bg.TFrame', width=36)
        self.progression = []
        for i in range(1, 10):
            self.progression.append(Progression(frame_nb, i))
            self.progression[-1].pack(padx=1, pady=1)

        # --- level indication
        frame = Frame(self)
        frame.grid(row=0, columnspan=5, padx=(30, 10), pady=10)
        Label(frame, text=_("Level") + ' - ', font="Arial 16").pack(side='left')
        self.label_level = Label(frame, font="Arial 16", text=_("Unknown"))
        self.label_level.pack(side='left')
        self.level = "unknown"  # puzzle level

        # --- frame contenant la grille de sudoku
        self.frame_puzzle = Frame(self, style="bg.TFrame")
        self.frame_pause = Frame(self, style="case.TFrame")
        self.frame_pause.grid_propagate(False)
        self.frame_pause.columnconfigure(0, weight=1)
        self.frame_pause.rowconfigure(0, weight=1)
        Label(self.frame_pause, text='PAUSE', style='pause.TLabel',
              font='Arial 30 bold').grid()

        # --- placement
        frame_nb.grid(row=1, column=6, sticky='en', pady=0, padx=(0, 30))
        self.frame_puzzle.grid(row=1, columnspan=5, padx=(30, 15))
        self.tps.grid(row=2, column=0, sticky="e", padx=(30, 10), pady=30)
        self.b_pause.grid(row=2, column=1, sticky="w", padx=2, pady=30)
        self.b_restart.grid(row=2, column=2, sticky="w", padx=2, pady=30)
        self.b_undo.grid(row=2, column=3, sticky="e", pady=30, padx=2)
        self.b_redo.grid(row=2, column=4, sticky="w", pady=30, padx=(2, 10))

        # --- menu
        menu = Menu(self, tearoff=0)

        menu_nouveau = Menu(menu, tearoff=0)

        menu_levels = Menu(menu_nouveau, tearoff=0)
        menu_levels.add_command(label=_("Easy"), command=self.new_easy)
        menu_levels.add_command(label=_("Medium"), command=self.new_medium)
        menu_levels.add_command(label=_("Difficult"), command=self.new_difficult)

        menu_nouveau.add_cascade(label=_("Level"), menu=menu_levels)
        menu_nouveau.add_command(label=_("Generate a puzzle"),
                                 command=self.genere_grille,
                                 accelerator="Ctrl+G")
        menu_nouveau.add_command(label=_("Empty grid"),
                                 command=self.grille_vide,
                                 accelerator="Ctrl+N")

        menu_ouvrir = Menu(menu, tearoff=0)
        menu_ouvrir.add_command(label=_("Game"), command=self.import_partie,
                                accelerator="Ctrl+O")
        menu_ouvrir.add_command(label=_("Puzzle"), command=self.import_grille,
                                accelerator="Ctrl+Shift+O")

        menu_game = Menu(menu, tearoff=0)
        menu_game.add_command(label=_("Restart"), command=self.recommence)
        menu_game.add_command(label=_("Solve"), command=self.resolution)
        menu_game.add_command(label=_("Save"), command=self.sauvegarde,
                              accelerator="Ctrl+S")
        menu_game.add_command(label=_("Export"), command=self.export_impression,
                              accelerator="Ctrl+E")
        menu_game.add_command(label=_("Evaluate level"),
                              command=self.evaluate_level)

        menu_language = Menu(menu, tearoff=0)
        self.langue = StringVar(self)
        self.langue.set(cst.LANGUE[:2])
        menu_language.add_radiobutton(label="Français",
                                      variable=self.langue,
                                      value="fr", command=self.translate)
        menu_language.add_radiobutton(label="English", variable=self.langue,
                                      value="en", command=self.translate)

        menu_help = Menu(menu, tearoff=0)
        menu_help.add_command(label=_("Help"), command=self.aide, accelerator='F1')
        menu_help.add_command(label=_("About"), command=self.about)

        menu.add_cascade(label=_("New"), menu=menu_nouveau)
        menu.add_cascade(label=_("Open"), menu=menu_ouvrir)
        menu.add_cascade(label=_("Game"), menu=menu_game)
        menu.add_cascade(label=_("Language"), menu=menu_language)
        menu.add_command(label=_("Statistics"), command=self.show_stat)
        menu.add_cascade(label=_("Help"), menu=menu_help)

        self.configure(menu=menu)

        # --- clavier popup
        self.clavier = None

        # --- cases
        self.nb_cases_remplies = 0
        self.blocs = np.zeros((9, 9), dtype=object)
        for i in range(9):
            for j in range(9):
                self.blocs[i, j] = Case(self.frame_puzzle, i, j, self.update_nbs, width=50, height=50)
                px, py = 1, 1
                if i % 3 == 2 and i != 8:
                    py = (1, 3)
                if j % 3 == 2 and j != 8:
                    px = (1, 3)
                self.blocs[i, j].grid(row=i, column=j, padx=px, pady=py)
                self.blocs[i, j].grid_propagate(0)

        # --- undo/redo stacks
        self._undo_stack = []
        self._redo_stack = []

        # --- raccourcis clavier et actions de la souris
        self.bind("<Button>", self.edit_case)
        self.bind("<Control-z>", lambda e: self.undo())
        self.bind("<Control-y>", lambda e: self.redo())
        self.bind("<Control-s>", lambda e: self.sauvegarde())
        self.bind("<Control-e>", lambda e: self.export_impression())
        self.bind("<Control-o>", lambda e: self.import_partie())
        self.bind("<Control-Shift-O>", lambda e: self.import_grille())
        self.bind("<Control-n>", lambda e: self.grille_vide())
        self.bind("<Control-g>", lambda e: self.genere_grille())
        self.bind("<FocusOut>", self.focus_out)
        self.bind("<F1>", self.aide)

        # --- open game
        if file:
            try:
                self.load_sudoku(file)
            except FileNotFoundError:
                one_button_box(self, _("Error"),
                               _("The file %(file)r does not exist.") % file,
                               image=self.im_erreur)
            except (KeyError, EOFError, UnpicklingError):
                try:
                    self.load_grille(file)
                except Exception:
                    one_button_box(self, _("Error"),
                                   _("This file is not a valid sudoku file."),
                                   image=self.im_erreur)
        elif exists(cst.PATH_SAVE):
            self.load_sudoku(cst.PATH_SAVE)
            remove(cst.PATH_SAVE)

    @property
    def level(self):
        return self._level

    @level.setter
    def level(self, level):
        self._level = level
        self.label_level.configure(text=_(level.capitalize()))

    def update_nbs(self, nb, delta):
        self.progression[nb - 1].nb += delta

    def reset_nbs(self):
        for p in self.progression:
            p.nb = 0

    def evaluate_level(self):
        grille = Grille()
        for i in range(9):
            for j in range(9):
                val = self.blocs[i, j].get_val()
                if val:
                    grille.ajoute_init(i, j, val)
        self.level = difficulte_grille(grille)

    def show_stat(self):
        """ show best times """
        def reset():
            """ reset best times """
            for level in ["easy", "medium", "difficult"]:
                CONFIG.set("Statistics", level, "")
            top.destroy()

        if self.chrono_on:
            self.play_pause()
        top = Toplevel(self)
        top.transient(self)
        top.columnconfigure(1, weight=1)
        top.resizable(0, 0)

        top.title(_("Statistics"))
        top.grab_set()

        Label(top, text=_("Best times"), font="Sans 12 bold").grid(row=0, columnspan=2,
                                                                   padx=30, pady=10)

        for i, level in enumerate(["easy", "medium", "difficult"]):
            Label(top, text=_(level.capitalize()),
                  font="Sans 10 bold").grid(row=i + 1, column=0, padx=(20, 4),
                                            pady=4, sticky="e")
            tps = CONFIG.get("Statistics", level)
            if tps:
                tps = int(tps)
                m = tps // 60
                s = tps % 60
                Label(top, text=" %i min %i s" % (m, s),
                      font="Sans 10").grid(row=i + 1, column=1,
                                           sticky="w", pady=4,
                                           padx=(4, 20))
        Button(top, text=_("Close"), command=top.destroy).grid(row=4, column=0, padx=(10, 4), pady=10)
        Button(top, text=_("Clear"), command=reset).grid(row=4, column=1, padx=(4, 10), pady=10)

    def new_easy(self):
        nb = np.random.randint(1, 101)
        fichier = join(cst.PUZZLES_LOCATION, "easy", "puzzle_easy_%i.txt" % nb)
        self.import_grille(fichier)
        self.level = "easy"

    def new_medium(self):
        nb = np.random.randint(1, 101)
        fichier = join(cst.PUZZLES_LOCATION, "medium", "puzzle_medium_%i.txt" % nb)
        self.import_grille(fichier)
        self.level = "medium"

    def new_difficult(self):
        nb = np.random.randint(1, 101)
        fichier = join(cst.PUZZLES_LOCATION, "difficult", "puzzle_difficult_%i.txt" % nb)
        self.import_grille(fichier)
        self.level = "difficult"

    def translate(self):
        """ changement de la langue de l'interface """
        one_button_box(self, _("Information"),
                       _("The language setting will take effect after restarting the application"),
                       image=self.im_info)
        CONFIG.set("General", "language", self.langue.get())

    def focus_out(self, event):
        """ met en pause si la fenêtre n'est plus au premier plan """
        try:
            if not self.focus_get() and self.chrono_on:
                self.play_pause()
        except KeyError:
            # erreur déclenchée par la présence d'une tkMessagebox
            if self.chrono_on:
                self.play_pause()

    def stacks_reinit(self):
        """efface l'historique des actions"""
        self._undo_stack.clear()
        self._redo_stack.clear()
        self.b_undo.configure(state="disabled")
        self.b_redo.configure(state="disabled")

    def stacks_modif(self, action):
        """Record action and clear redo stack."""
        self._undo_stack.append(action)
        self.b_undo.configure(state="normal")
        self.b_redo.configure(state="disabled")
        self._redo_stack.clear()

    def about(self):
        if self.chrono_on:
            self.play_pause()
        About(self)

    def aide(self, event=None):
        if self.chrono_on:
            self.play_pause()
        Aide(self)

    def quitter(self):
        rep = _("Yes")
        if self.debut:
            rep = two_button_box(self, _("Confirmation"),
                                 _("Do you want to interrupt the current puzzle?"),
                                 _("Yes"), _("No"), image=self.im_question)
        if rep == _("Yes"):
            if self.debut:
                self.save(cst.PATH_SAVE)
            self.destroy()

    def undo(self):
        if self._undo_stack and self.chrono_on:
            self.b_redo.configure(state="normal")
            i, j, val_prec, pos_prec, modifs, val, pos = self._undo_stack.pop(-1)
            self._redo_stack.append((i, j, val_prec, pos_prec, modifs, val, pos))
            if not self._undo_stack:
                self.b_undo.configure(state="disabled")

            if self.blocs[i, j].get_val():
                self.modifie_nb_cases_remplies(-1)
                self.update_nbs(self.blocs[i, j].get_val(), -1)
            self.blocs[i, j].efface_case()
            if val_prec:
                self.modifie_nb_cases_remplies(self.blocs[i, j].edit_chiffre(val_prec))
                if not self.test_case(i, j, val):
                    self.update_grille(i, j, val)
            else:
                for nb in pos_prec:
                    v = int(nb)
                    self.modifie_nb_cases_remplies(self.blocs[i, j].edit_possibilite(v))
                    self.test_possibilite(i, j, v)
            for k, l in modifs:
                self.blocs[k, l].edit_possibilite(val)

    def redo(self):
        if self._redo_stack and self.chrono_on:
            self.b_undo.configure(state="normal")
            i, j, val_prec, pos_prec, modifs, val, pos = self._redo_stack.pop(-1)
            self._undo_stack.append((i, j, val_prec, pos_prec, modifs, val, pos))
            if not self._redo_stack:
                self.b_redo.configure(state="disabled")
            val_prec = self.blocs[i, j].get_val()
            if val_prec:
                self.modifie_nb_cases_remplies(-1)
                self.update_nbs(val_prec, -1)
            self.blocs[i, j].efface_case()
            if val:
                self.modifie_nb_cases_remplies(self.blocs[i, j].edit_chiffre(val))
                if not self.test_case(i, j, val_prec):
                    self.update_grille(i, j, val_prec)
            else:
                for nb in pos:
                    v = int(nb)
                    self.modifie_nb_cases_remplies(self.blocs[i, j].edit_possibilite(v))
                    self.test_possibilite(i, j, v)

    def restart(self, m=0, s=0):
        """ réinitialise le chrono et les boutons """
        self.chrono = [m, s]
        self.chrono_on = False
        self.debut = False
        self.tps.configure(text=" %02i:%02i" % tuple(self.chrono))
        self.b_undo.configure(state="disabled")
        self.b_pause.configure(state="disabled", image=self.im_pause)
        self.b_redo.configure(state="disabled")
        self.b_restart.configure(state="disabled")
        self.stacks_reinit()
        self.frame_pause.place_forget()

    def play_pause(self):
        """ Démarre le chrono s'il était arrêté, le met en pause sinon """
        if self.debut:
            if self.chrono_on:
                self.chrono_on = False
                self.b_pause.configure(image=self.im_play)
                self.b_redo.configure(state="disabled")
                self.b_undo.configure(state="disabled")
                self.tooltip_wrapper.set_tooltip_text(self.b_pause, _("Resume game"))
                self.frame_pause.place(in_=self.frame_puzzle, x=0, y=0, anchor='nw',
                                       relwidth=1, relheight=1)
            elif self.nb_cases_remplies != 81:
                self.chrono_on = True
                self.b_pause.configure(image=self.im_pause)
                self.tps.after(1000, self.affiche_chrono)
                if self._undo_stack:
                    self.b_undo.configure(state="normal")
                if self._redo_stack:
                    self.b_redo.configure(state="normal")
                self.tooltip_wrapper.set_tooltip_text(self.b_pause, _("Pause game"))
                self.frame_pause.place_forget()

    def affiche_chrono(self):
        """ Met à jour l'affichage du temps """
        if self.chrono_on:
            self.chrono[1] += 1
            if self.chrono[1] == 60:
                self.chrono[0] += 1
                self.chrono[1] = 0
            self.tps.configure(text=" %02i:%02i" % tuple(self.chrono))
            self.tps.after(1000, self.affiche_chrono)

    def modifie_nb_cases_remplies(self, nb):
        self.nb_cases_remplies += nb

    def edit_case(self, event):
        if event.num in [1, 3]:
            if not self.debut and self.nb_cases_remplies != 81:
                self.debut = True
                self.b_pause.configure(state="normal")
                self.b_restart.configure(state="normal")
                self.play_pause()
            if str(event.widget) != "." and self.chrono_on:
                if self.clavier:
                    self.clavier.quitter()
                ref = self.blocs[0, 0].winfo_parent()
                case = event.widget.grid_info().get("in", None)
                if str(case) == ref:
                    case = event.widget
                try:
                    if case.is_modifiable():
                        if event.num == 1:
                            self.clavier = Clavier(self, case, "val")
                        elif event.num == 3:
                            self.clavier = Clavier(self, case, "possibilite")
                        self.clavier.display("+%i+%i" % (case.winfo_rootx() - 25, case.winfo_rooty() + 50))
                except AttributeError:
                    if self.clavier:
                        self.clavier.quitter()

            elif self.clavier:
                self.clavier.quitter()

    def test_case(self, i, j, val_prec=0):
        """ Teste si la valeur de la case est en contradiction avec celles des
            autres cases de la ligne / colonne / bloc et renvoie True s'il y a une erreur."""
        val = self.blocs[i, j].get_val()
        a, b = i // 3, j // 3
        error = False
        if val:
            if ((self.blocs[i, :] == val).sum() > 1 or (self.blocs[:, j] == val).sum() > 1 or
               (self.blocs[3 * a: 3 * (a + 1), 3 * b: 3 * (b + 1)] == val).sum() > 1):
                # erreur !
                self.blocs[i, j].affiche_erreur()
                error = True
        if val_prec:
            # a number was removed, remove obsolete errors
            line = self.blocs[i, :] == val_prec
            column = self.blocs[:, j] == val_prec
            bloc = self.blocs[3 * a: 3 * (a + 1), 3 * b: 3 * (b + 1)] == val_prec
            if line.sum() == 1:
                self.blocs[i, line.argmax()].no_error()
                self.test_case(i, line.argmax())
            if column.sum() == 1:
                self.blocs[column.argmax(), j].no_error()
                self.test_case(column.argmax(), j)
            if bloc.sum() == 1:
                x, y = divmod(bloc.argmax(), 3)
                self.blocs[3 * a + x, 3 * b + y].no_error()
                self.test_case(3 * a + x, 3 * b + y)
        return error

    def test_possibilite(self, i, j, val):
        """ Teste si la possibilité val de la case est en contradiction avec les valeurs des
            autres cases de la ligne / colonne / bloc """
        a, b = i // 3, j // 3
        if ((self.blocs[i, :] == val).sum() > 0 or (self.blocs[:, j] == val).sum() > 0 or
           (self.blocs[3 * a: 3 * (a + 1), 3 * b: 3 * (b + 1)] == val).sum() > 0):
            # erreur !
            self.blocs[i, j].affiche_erreur_possibilite(val)

    def test_remplie(self):
        """ Test si la grille est remplie """
        if self.nb_cases_remplies == 81:
            grille = Grille()
            for i in range(9):
                for j in range(9):
                    val = self.blocs[i, j].get_val()
                    if val:
                        grille.ajoute_init(i, j, val)
            sol = grille.solve()
            if type(sol) == np.ndarray:
                self.play_pause()
                self.frame_pause.place_forget()
                one_button_box(self, _("Information"),
                               _("You solved the puzzle in %(min)i minutes and %(sec)i secondes.") % {"min": self.chrono[0], "sec": self.chrono[1]},
                               image=self.im_info)
                if self.level != "unknown":
                    best = CONFIG.get("Statistics", self.level)
                    current = self.chrono[0] * 60 + self.chrono[1]
                    if best:
                        best = int(best)
                        if current < best:
                            CONFIG.set("Statistics", self.level, str(current))
                    else:
                        CONFIG.set("Statistics", self.level, str(current))
                self.b_pause.configure(state="disabled")
                self.debut = False

            else:
                i, j = sol[1]
                if self.blocs[i, j].get_val():
                    self.blocs[i, j].affiche_erreur()
                one_button_box(self, _("Information"), _("There is a mistake."),
                               image=self.im_info)

    def update_grille(self, i, j, val_prec=0):
        """ Enlève les possibilités devenues impossibles suite à l'ajout d'une
            valeur dans la case (i, j) """
        val = self.blocs[i, j].get_val()
        modif = []
        a, b = i // 3, j // 3
        if val_prec:
            x, y = divmod(val_prec - 1, 3)
        for k, (line, column, bloc) in enumerate(zip(self.blocs[i, :], self.blocs[:, j], self.blocs[3 * a: 3 * (a + 1), 3 * b: 3 * (b + 1)].flatten())):
            # works because if line is bloc then pos1 is pos3 and both are edited at once
            pos1 = line.get_possibilites()
            pos2 = column.get_possibilites()
            pos3 = bloc.get_possibilites()
            if val in pos1:
                self.blocs[i, k].edit_possibilite(val)
                modif.append((i, k))
            if val in pos2:
                self.blocs[k, j].edit_possibilite(val)
                modif.append((k, j))
            if val in pos3:
                m, n = divmod(k, 3)
                self.blocs[3 * a + m, 3 * b + n].edit_possibilite(val)
                modif.append((3 * a + m, 3 * b + n))
            if val_prec:
                if val_prec in pos1:
                    self.blocs[i, k].pas_erreur(x, y)
                    self.test_possibilite(i, k, val_prec)
                if val_prec in pos2:
                    self.blocs[k, j].pas_erreur(x, y)
                    self.test_possibilite(k, j, val_prec)
                if val_prec in pos3:
                    m, n = divmod(k, 3)
                    m, n = 3 * a + m, 3 * b + n
                    if m != i and n != j:
                        self.blocs[m, n].pas_erreur(x, y)
                        self.test_possibilite(m, n, val_prec)
        return modif

    def set_clavier(self, c):
        self.clavier = c

    def grille_vide(self):
        rep = _("Yes")
        if self.debut:
            rep = two_button_box(self, _("Confirmation"),
                                 _("Do you want to abandon the current puzzle?"),
                                 _("Yes"), _("No"), self.im_question)
        if rep == _("Yes"):
            self.nb_cases_remplies = 0
            self.restart()
            self.level = "unknown"
            self.reset_nbs()
            for i in range(9):
                for j in range(9):
                    self.blocs[i, j].set_modifiable(True)
                    self.blocs[i, j].efface_case()

    def genere_grille(self):
        """ Génère une nouvelle grille """
        if self.chrono_on:
            self.play_pause()
        rep = _("Yes")
        if self.debut:
            rep = two_button_box(self, _("Confirmation"),
                                 _("Do you want to abandon the current puzzle?"),
                                 _("Yes"), _("No"), self.im_question)

        if rep == _("Yes"):
            self.configure(cursor="watch")
            self.update()
            rep2 = _("Retry")
            while rep2 == _("Retry"):
                grille = genere_grille()
                diff = difficulte_grille(grille)
                nb = grille.nb_cases_remplies()
                self.configure(cursor="")
                rep2 = two_button_box(self, _("Information"),
                                      _("The generated puzzle contains %(nb)i numbers and its level is %(difficulty)s.") % ({"nb": nb, "difficulty": _(diff.capitalize())}),
                                      _("Play"), _("Retry"), image=self.im_info)
            if rep2 == _("Play"):
                self.level = diff
                self.affiche_grille(grille.get_sudoku())

    def recommence(self):
        if self.chrono_on:
            self.play_pause()
        rep = _("Yes")
        if self.debut:
            rep = two_button_box(self, _("Confirmation"),
                                 _("Do you really want to start again?"),
                                 _("Yes"), _("No"), self.im_question)
        if rep == _("Yes"):
            self.reset_nbs()
            for i in range(9):
                for j in range(9):
                    if self.blocs[i, j].is_modifiable():
                        if self.blocs[i, j].get_val():
                            self.nb_cases_remplies -= 1
                        self.blocs[i, j].efface_case()
                    else:
                        self.update_nbs(self.blocs[i, j].get_val(), 1)
            self.restart()
        elif self.debut:
            self.play_pause()

    def save(self, path):
        grille = np.zeros((9, 9), dtype=int)
        modif = np.zeros((9, 9), dtype=bool)
        possibilites = []
        for i in range(9):
            possibilites.append([])
            for j in range(9):
                grille[i, j] = self.blocs[i, j].get_val()
                modif[i, j] = self.blocs[i, j].is_modifiable()
                possibilites[i].append(self.blocs[i, j].get_possibilites())
        with open(path, "wb") as fich:
            p = Pickler(fich)
            p.dump(grille)
            p.dump(modif)
            p.dump(possibilites)
            p.dump(self.chrono)
            p.dump(self.level)

    def sauvegarde(self):
        if self.chrono_on:
            self.play_pause()
        fichier = asksaveasfilename(initialdir=cst.INITIALDIR,
                                    defaultextension='.sudoku',
                                    filetypes=[('Sudoku', '*.sudoku')])
        if fichier:
            self.save(fichier)

    def affiche_grille(self, grille):
        """ Affiche la grille """
        self.nb_cases_remplies = 0
        self.restart()
        self.reset_nbs()
        for i in range(9):
            for j in range(9):
                nb = grille[i, j]
                self.blocs[i, j].efface_case()
                if nb:
                    self.blocs[i, j].set_modifiable(False)
                    self.nb_cases_remplies += 1
                    self.blocs[i, j].edit_chiffre(nb)
                else:
                    self.blocs[i, j].set_modifiable(True)

    def load_sudoku(self, file):
        with open(file, "rb") as fich:
            dp = Unpickler(fich)
            grille = dp.load()
            modif = dp.load()
            possibilites = dp.load()
            chrono = dp.load()
            self.level = dp.load()
        self.nb_cases_remplies = 0
        self.reset_nbs()
        self.restart(*chrono)
        for i in range(9):
            for j in range(9):
                self.blocs[i, j].efface_case()
                if grille[i, j]:
                    self.nb_cases_remplies += 1
                    self.blocs[i, j].edit_chiffre(grille[i, j])
                else:
                    for pos in possibilites[i][j]:
                        self.blocs[i, j].edit_possibilite(pos)
                self.blocs[i, j].set_modifiable(modif[i, j])

    def import_partie(self):
        """ importe une partie stockée dans un fichier .sudoku """
        if self.chrono_on:
            self.play_pause()
        rep = _("Yes")
        if self.debut:
            rep = two_button_box(self, _("Confirmation"),
                                 _("Do you want to abandon the current puzzle?"),
                                 _("Yes"), _("No"), self.im_question)
        if rep == _("Yes"):
            fichier = askopenfilename(initialdir=cst.INITIALDIR,
                                      defaultextension='.sudoku',
                                      filetypes=[('Sudoku', '*.sudoku')])
            if fichier:
                try:
                    self.load_sudoku(fichier)
                except FileNotFoundError:
                    one_button_box(self, _("Error"),
                                   _("The file %(file)r does not exist.") % fichier,
                                   image=self.im_erreur)
                except (KeyError, EOFError, UnpicklingError):
                    one_button_box(self, _("Error"),
                                   _("This file is not a valid sudoku file."),
                                   image=self.im_erreur)
        elif self.debut:
            self.play_pause()

    def resolution_init(self):
        """ Résolution de la grille initiale (sans tenir compte des valeurs rentrées par l'utilisateur. """
        grille = Grille()
        for i in range(9):
            for j in range(9):
                if not self.blocs[i, j].is_modifiable():
                    val = self.blocs[i, j].get_val()
                    grille.ajoute_init(i, j, val)
        self.configure(cursor="watch")
        self.update()
        sol = grille.solve()
        self.configure(cursor="")
        if type(sol) == np.ndarray:
            for i in range(9):
                for j in range(9):
                    val = self.blocs[i, j].get_val()
                    if not val:
                        self.blocs[i, j].edit_chiffre(sol[i, j])
                        self.blocs[i, j].affiche_solution()
                    elif self.blocs[i, j].is_modifiable():
                        if val != sol[i, j]:
                            self.blocs[i, j].edit_chiffre(sol[i, j])
                            self.blocs[i, j].affiche_erreur()
            self.restart()
            self.nb_cases_remplies = 81

        elif sol[1]:
            i, j = sol[1]
            if self.blocs[i, j].get_val():
                self.blocs[i, j].affiche_erreur()
            one_button_box(self, _("Error"), _("The grid is wrong. It cannot be solved."),
                           image=self.im_erreur)
        else:
            one_button_box(self, _("Error"), _("Resolution failed."),
                           image=self.im_erreur)

    def resolution(self):
        if self.chrono_on:
            self.play_pause()
        rep = two_button_box(self, _("Confirmation"),
                             _("Do you really want to get the solution?"),
                             _("Yes"), _("No"), image=self.im_question)
        if rep == _("Yes"):
            self.frame_pause.place_forget()
            grille = Grille()
            for i in range(9):
                for j in range(9):
                    val = self.blocs[i, j].get_val()
                    if val:
                        grille.ajoute_init(i, j, val)
            self.configure(cursor="watch")
            self.update()
            sol = grille.solve()
            self.configure(cursor="")
            if type(sol) == np.ndarray:
                for i in range(9):
                    for j in range(9):
                        val = self.blocs[i, j].get_val()
                        if not val:
                            self.blocs[i, j].edit_chiffre(sol[i, j])
                            self.blocs[i, j].affiche_solution()
                self.restart()
                self.b_restart.configure(state="normal")
                self.nb_cases_remplies = 81
            elif sol[1]:
                i, j = sol[1]
                if self.blocs[i, j].get_val():
                    self.blocs[i, j].affiche_erreur()
                i, j = 0, 0
                while i < 9 and self.blocs[i, j].is_modifiable():
                    j += 1
                    if j == 9:
                        i += 1
                        j = 0
                if i < 9:
                    # il y a au moins une case de type "initial"
                    rep = two_button_box(self, _("Error"),
                                         _("The grid is wrong. It cannot be solved. Do you want the solution of the initial grid?"),
                                         _("Yes"), _("No"), image=self.im_erreur)
                    if rep == _("Yes"):
                        self.resolution_init()
                else:
                    one_button_box(self, _("Error"), _("The grid is wrong. It cannot be solved."),
                                   image=self.im_erreur)
            else:
                one_button_box(self, _("Error"), _("Resolution failed."),
                               image=self.im_erreur)

    def load_grille(self, file):
        gr = np.loadtxt(file, dtype=int)
        if gr.shape == (9, 9):
            self.affiche_grille(gr)
            self.level = "unknown"
        else:
            one_button_box(self, _("Error"), _("This is not a 9x9 sudoku grid."),
                           image=self.im_erreur)

    def import_grille(self, fichier=None):
        """ importe une grille stockée dans un fichier txt sous forme de
            chiffres séparés par des espaces (0 = case vide) """
        if self.chrono_on:
            self.play_pause()
        rep = _("Yes")
        if self.debut:
            rep = two_button_box(self, _("Confirmation"),
                                 _("Do you want to abandon the current puzzle?"),
                                 _("Yes"), _("No"), self.im_question)
        if rep == _("Yes"):
            if not fichier:
                fichier = askopenfilename(initialdir=cst.INITIALDIR,
                                          defaultextension='.txt',
                                          filetypes=[('Text', '*.txt'), ('Tous les fichiers', "*")])
            if fichier:
                try:
                    self.load_grille(fichier)
                except (ValueError, UnicodeDecodeError):
                    one_button_box(self, _("Error"),
                                   _("The file does not have the right format. It should be a .txt file with cell values separated by one space. 0 means empty cell."),
                                   image=self.im_erreur)
                except FileNotFoundError:
                    one_button_box(self, _("Error"),
                                   _("The file %(file)r does not exist.") % fichier,
                                   image=self.im_erreur)
        elif self.debut:
            self.play_pause()

    def export_impression(self):
        """ exporte la grille en image (pour pouvoir l'imprimer) """
        if self.chrono_on:
            self.play_pause()
        fichier = asksaveasfilename(title=_("Export"),
                                    initialdir=cst.INITIALDIR,
                                    defaultextension='.png',
                                    filetypes=[('PNG', '*.png'),
                                               ('JPEG', 'jpg')])
        if fichier:
            grille = np.zeros((9, 9), dtype=int)
            for i in range(9):
                for j in range(9):
                    grille[i, j] = self.blocs[i, j].get_val()
            font = ImageFont.truetype("arial.ttf", 64)
            im = Image.new("RGB", (748, 748), "white")
            draw = ImageDraw.Draw(im)
            i = 0
            l = 1
            while i < 10:
                if i % 3 == 0:
                    w = 4
                else:
                    w = 2
                draw.line((l, 1, l, 748), width=w, fill="black")
                draw.line((1, l, 748, l), width=w, fill="black")
                l += 80 + w
                i += 1

            for i in range(9):
                for j in range(9):
                    if grille[i, j]:
                        draw.text((26 + j * 82 + 2 * (j // 3),
                                   10 + i * 82 + 2 * (i // 3)),
                                  " %i" % grille[i, j], fill="black", font=font)

            del draw
            im.save(fichier)
コード例 #2
0
class Application(Frame):
    def __init__(self, master=None):
        """Initialization creates GUI, widgets, and variables."""
        super().__init__(master)
        self.master = master
        self.initUI()
        self.count = 0
        self.txt_speed = 0  # starts by not displaying until resumed
        self.saved_speed = 300  # default speed
        self.file = list()
        self.filename = None

    def initUI(self):
        """Initialize the User Interface"""
        global var
        columns = [0, 1, 2, 3, 4]  # columns grid padding
        rows = [0, 1, 2, 3, 4, 5]  # rows grid padding
        var = StringVar()
        self.master.title("Speed-Reader")

        Style().configure("Tbutton", padding=(0, 5, 0, 5), font="serif 10")
        self.columnconfigure(columns, pad=5, minsize=25)
        self.rowconfigure(rows, pad=5, minsize=25)

        self.textLabel = Label(self, textvariable=var)

        self.textLabel.grid(row=0, column=2)

        self.restart = Button(self,
                              text="Restart Text",
                              command=self.restart_txt)
        self.restart.grid(row=1, column=2)

        self.pause = Button(self, text="PLAY/PAUSE", command=self.pause_txt)
        self.pause.grid(row=2, column=2)

        init_slide_val = DoubleVar()  # create initial slider value variable
        self.speed_slider = Scale(
            self,
            cursor='sb_h_double_arrow',
            from_=100,
            to=300,
            length=200,
            tickinterval=50,
            resolution=50,
            orient='horizontal',
            variable=init_slide_val,
            command=lambda x: self.change_speed(self.speed_slider.get()))

        init_slide_val.set(200)  # initialize the slider to 200 WPM
        self.speed_slider.grid(row=3, column=2)

        self.quit = Button(self, text="QUIT", command=self.master.destroy)
        self.quit.grid(row=4, column=2)

        self.addFile = Button(self, text="OPEN", command=self.UploadAction)
        self.addFile.grid(row=5, column=2)

        self.textLabel.after(1000, self.display_text)

        self.pack()

    def display_text(self):
        """Temporary test case sentence output."""
        list = self.file
        if self.count < len(list):
            if self.txt_speed > 0:
                var.set(list[self.count])
                self.count += 1
                self.textLabel.after(self.txt_speed, self.display_text)

    def pause_txt(self):
        """Pause button stops the text from continously displaying."""
        # Set text speed to 0
        if self.txt_speed > 0:
            self.txt_speed = 0
        else:
            # set the text speed to the last used speed
            self.txt_speed = self.saved_speed
            # Resume the text display when resumed
            self.textLabel.after(self.txt_speed, self.display_text)

    def change_speed(self, speed):
        """Changes the text display speed in 60 WPM increments"""
        speed = int((60 / speed) * 1000)  # convert WPM to text_speed
        self.saved_speed = speed
        self.txt_speed = self.saved_speed

    def restart_txt(self):
        """Resets the displayed text to the beginning of the file."""
        # Pause the text display
        self.txt_speed = 0
        # Set to beginning of the file's text
        self.count = 0
        var.set("")
        self.textLabel.after(self.txt_speed, self.display_text)

    def UploadAction(self):
        """Gets file from user's computer."""
        self.filename = filedialog.askopenfilename()
        print('Selected:', self.filename)
        self.file = conv.readFile(self.filename, self.master)
コード例 #3
0
class Sudoku(Tk):
    def __init__(self, file=None):
        Tk.__init__(self, className="Sudoku-Tk")
        self.title("Sudoku-Tk")
        self.resizable(0, 0)
        self.protocol("WM_DELETE_WINDOW", self.quitter)
        cst.set_icon(self)
        self.columnconfigure(3, weight=1)

        # --- style
        bg = '#dddddd'
        activebg = '#efefef'
        pressedbg = '#c1c1c1'
        lightcolor = '#ededed'
        darkcolor = '#cfcdc8'
        bordercolor = '#888888'
        focusbordercolor = '#5E5E5E'
        disabledfg = '#999999'
        disabledbg = bg

        button_style_config = {'bordercolor': bordercolor,
                               'background': bg,
                               'lightcolor': lightcolor,
                               'darkcolor': darkcolor}

        button_style_map = {'background': [('active', activebg),
                                           ('disabled', disabledbg),
                                           ('pressed', pressedbg)],
                            'lightcolor': [('pressed', darkcolor)],
                            'darkcolor': [('pressed', lightcolor)],
                            'bordercolor': [('focus', focusbordercolor)],
                            'foreground': [('disabled', disabledfg)]}

        style = Style(self)
        style.theme_use(cst.STYLE)
        style.configure('TFrame', background=bg)
        style.configure('TLabel', background=bg)
        style.configure('TScrollbar', gripcount=0, troughcolor=pressedbg,
                        **button_style_config)
        style.map('TScrollbar', **button_style_map)
        style.configure('TButton', **button_style_config)
        style.map('TButton', **button_style_map)
        style.configure('TCheckutton', **button_style_config)
        style.map('TCheckutton', **button_style_map)
        self.option_add('*Toplevel.background', bg)
        self.option_add('*Menu.background', bg)
        self.option_add('*Menu.activeBackground', activebg)
        self.option_add('*Menu.activeForeground', "black")
        self.configure(bg=bg)

        style.configure("bg.TFrame", background="grey")
        style.configure("case.TFrame", background="white")
        style.configure("case.TLabel", background="white", foreground="black")
        style.configure("case_init.TFrame", background="lightgrey")
        style.configure("case_init.TLabel", background="lightgrey", foreground="black")
        style.configure("erreur.TFrame", background="white")
        style.configure("erreur.TLabel", background="white", foreground="red")
        style.configure("solution.TFrame", background="white")
        style.configure("solution.TLabel", background="white", foreground="blue")
        style.configure("pause.TLabel", foreground="grey", background='white')

        # --- images
        self.im_erreur = open_image(cst.ERREUR)
        self.im_pause = open_image(cst.PAUSE)
        self.im_restart = open_image(cst.RESTART)
        self.im_play = open_image(cst.PLAY)
        self.im_info = open_image(cst.INFO)
        self.im_undo = open_image(cst.UNDO)
        self.im_redo = open_image(cst.REDO)
        self.im_question = open_image(cst.QUESTION)

        # --- timer
        self.chrono = [0, 0]
        self.tps = Label(self, text=" %02i:%02i" % tuple(self.chrono),
                         font="Arial 16")
        self.debut = False  # la partie a-t-elle commencée ?
        self.chrono_on = False  # le chrono est-il en marche ?

        # --- buttons
        self.b_pause = Button(self, state="disabled", image=self.im_pause,
                              command=self.play_pause)
        self.b_restart = Button(self, state="disabled", image=self.im_restart,
                                command=self.recommence)
        self.b_undo = Button(self, image=self.im_undo, command=self.undo)
        self.b_redo = Button(self, image=self.im_redo, command=self.redo)

        # --- tooltips
        self.tooltip_wrapper = TooltipWrapper(self)
        self.tooltip_wrapper.add_tooltip(self.b_pause, _("Pause game"))
        self.tooltip_wrapper.add_tooltip(self.b_restart, _("Restart game"))
        self.tooltip_wrapper.add_tooltip(self.b_undo, _("Undo"))
        self.tooltip_wrapper.add_tooltip(self.b_redo, _("Redo"))

        # --- numbers
        frame_nb = Frame(self, style='bg.TFrame', width=36)
        self.progression = []
        for i in range(1, 10):
            self.progression.append(Progression(frame_nb, i))
            self.progression[-1].pack(padx=1, pady=1)

        # --- level indication
        frame = Frame(self)
        frame.grid(row=0, columnspan=5, padx=(30, 10), pady=10)
        Label(frame, text=_("Level") + ' - ', font="Arial 16").pack(side='left')
        self.label_level = Label(frame, font="Arial 16", text=_("Unknown"))
        self.label_level.pack(side='left')
        self.level = "unknown"  # puzzle level

        # --- frame contenant la grille de sudoku
        self.frame_puzzle = Frame(self, style="bg.TFrame")
        self.frame_pause = Frame(self, style="case.TFrame")
        self.frame_pause.grid_propagate(False)
        self.frame_pause.columnconfigure(0, weight=1)
        self.frame_pause.rowconfigure(0, weight=1)
        Label(self.frame_pause, text='PAUSE', style='pause.TLabel',
              font='Arial 30 bold').grid()

        # --- placement
        frame_nb.grid(row=1, column=6, sticky='en', pady=0, padx=(0, 30))
        self.frame_puzzle.grid(row=1, columnspan=5, padx=(30, 15))
        self.tps.grid(row=2, column=0, sticky="e", padx=(30, 10), pady=30)
        self.b_pause.grid(row=2, column=1, sticky="w", padx=2, pady=30)
        self.b_restart.grid(row=2, column=2, sticky="w", padx=2, pady=30)
        self.b_undo.grid(row=2, column=3, sticky="e", pady=30, padx=2)
        self.b_redo.grid(row=2, column=4, sticky="w", pady=30, padx=(2, 10))

        # --- menu
        menu = Menu(self, tearoff=0)

        menu_nouveau = Menu(menu, tearoff=0)

        menu_levels = Menu(menu_nouveau, tearoff=0)
        menu_levels.add_command(label=_("Easy"), command=self.new_easy)
        menu_levels.add_command(label=_("Medium"), command=self.new_medium)
        menu_levels.add_command(label=_("Difficult"), command=self.new_difficult)

        menu_nouveau.add_cascade(label=_("Level"), menu=menu_levels)
        menu_nouveau.add_command(label=_("Generate a puzzle"),
                                 command=self.genere_grille,
                                 accelerator="Ctrl+G")
        menu_nouveau.add_command(label=_("Empty grid"),
                                 command=self.grille_vide,
                                 accelerator="Ctrl+N")

        menu_ouvrir = Menu(menu, tearoff=0)
        menu_ouvrir.add_command(label=_("Game"), command=self.import_partie,
                                accelerator="Ctrl+O")
        menu_ouvrir.add_command(label=_("Puzzle"), command=self.import_grille,
                                accelerator="Ctrl+Shift+O")

        menu_game = Menu(menu, tearoff=0)
        menu_game.add_command(label=_("Restart"), command=self.recommence)
        menu_game.add_command(label=_("Solve"), command=self.resolution)
        menu_game.add_command(label=_("Save"), command=self.sauvegarde,
                              accelerator="Ctrl+S")
        menu_game.add_command(label=_("Export"), command=self.export_impression,
                              accelerator="Ctrl+E")
        menu_game.add_command(label=_("Evaluate level"),
                              command=self.evaluate_level)

        menu_language = Menu(menu, tearoff=0)
        self.langue = StringVar(self)
        self.langue.set(cst.LANGUE[:2])
        menu_language.add_radiobutton(label="Français",
                                      variable=self.langue,
                                      value="fr", command=self.translate)
        menu_language.add_radiobutton(label="English", variable=self.langue,
                                      value="en", command=self.translate)

        menu_help = Menu(menu, tearoff=0)
        menu_help.add_command(label=_("Help"), command=self.aide, accelerator='F1')
        menu_help.add_command(label=_("About"), command=self.about)

        menu.add_cascade(label=_("New"), menu=menu_nouveau)
        menu.add_cascade(label=_("Open"), menu=menu_ouvrir)
        menu.add_cascade(label=_("Game"), menu=menu_game)
        menu.add_cascade(label=_("Language"), menu=menu_language)
        menu.add_command(label=_("Statistics"), command=self.show_stat)
        menu.add_cascade(label=_("Help"), menu=menu_help)

        self.configure(menu=menu)

        # --- clavier popup
        self.clavier = None

        # --- cases
        self.nb_cases_remplies = 0
        self.blocs = np.zeros((9, 9), dtype=object)
        for i in range(9):
            for j in range(9):
                self.blocs[i, j] = Case(self.frame_puzzle, i, j, self.update_nbs, width=50, height=50)
                px, py = 1, 1
                if i % 3 == 2 and i != 8:
                    py = (1, 3)
                if j % 3 == 2 and j != 8:
                    px = (1, 3)
                self.blocs[i, j].grid(row=i, column=j, padx=px, pady=py)
                self.blocs[i, j].grid_propagate(0)

        # --- undo/redo stacks
        self._undo_stack = []
        self._redo_stack = []

        # --- raccourcis clavier et actions de la souris
        self.bind("<Button>", self.edit_case)
        self.bind("<Control-z>", lambda e: self.undo())
        self.bind("<Control-y>", lambda e: self.redo())
        self.bind("<Control-s>", lambda e: self.sauvegarde())
        self.bind("<Control-e>", lambda e: self.export_impression())
        self.bind("<Control-o>", lambda e: self.import_partie())
        self.bind("<Control-Shift-O>", lambda e: self.import_grille())
        self.bind("<Control-n>", lambda e: self.grille_vide())
        self.bind("<Control-g>", lambda e: self.genere_grille())
        self.bind("<FocusOut>", self.focus_out)
        self.bind("<F1>", self.aide)

        # --- open game
        if file:
            try:
                self.load_sudoku(file)
            except FileNotFoundError:
                one_button_box(self, _("Error"),
                               _("The file %(file)r does not exist.") % file,
                               image=self.im_erreur)
            except (KeyError, EOFError, UnpicklingError):
                try:
                    self.load_grille(file)
                except Exception:
                    one_button_box(self, _("Error"),
                                   _("This file is not a valid sudoku file."),
                                   image=self.im_erreur)
        elif exists(cst.PATH_SAVE):
            self.load_sudoku(cst.PATH_SAVE)
            remove(cst.PATH_SAVE)

    @property
    def level(self):
        return self._level

    @level.setter
    def level(self, level):
        self._level = level
        self.label_level.configure(text=_(level.capitalize()))

    def update_nbs(self, nb, delta):
        self.progression[nb - 1].nb += delta

    def reset_nbs(self):
        for p in self.progression:
            p.nb = 0

    def evaluate_level(self):
        grille = Grille()
        for i in range(9):
            for j in range(9):
                val = self.blocs[i, j].get_val()
                if val:
                    grille.ajoute_init(i, j, val)
        self.level = difficulte_grille(grille)

    def show_stat(self):
        """ show best times """
        def reset():
            """ reset best times """
            for level in ["easy", "medium", "difficult"]:
                CONFIG.set("Statistics", level, "")
            top.destroy()

        if self.chrono_on:
            self.play_pause()
        top = Toplevel(self)
        top.transient(self)
        top.columnconfigure(1, weight=1)
        top.resizable(0, 0)

        top.title(_("Statistics"))
        top.grab_set()

        Label(top, text=_("Best times"), font="Sans 12 bold").grid(row=0, columnspan=2,
                                                                   padx=30, pady=10)

        for i, level in enumerate(["easy", "medium", "difficult"]):
            Label(top, text=_(level.capitalize()),
                  font="Sans 10 bold").grid(row=i + 1, column=0, padx=(20, 4),
                                            pady=4, sticky="e")
            tps = CONFIG.get("Statistics", level)
            if tps:
                tps = int(tps)
                m = tps // 60
                s = tps % 60
                Label(top, text=" %i min %i s" % (m, s),
                      font="Sans 10").grid(row=i + 1, column=1,
                                           sticky="w", pady=4,
                                           padx=(4, 20))
        Button(top, text=_("Close"), command=top.destroy).grid(row=4, column=0, padx=(10, 4), pady=10)
        Button(top, text=_("Clear"), command=reset).grid(row=4, column=1, padx=(4, 10), pady=10)

    def new_easy(self):
        nb = np.random.randint(1, 101)
        fichier = join(cst.PUZZLES_LOCATION, "easy", "puzzle_easy_%i.txt" % nb)
        self.import_grille(fichier)
        self.level = "easy"

    def new_medium(self):
        nb = np.random.randint(1, 101)
        fichier = join(cst.PUZZLES_LOCATION, "medium", "puzzle_medium_%i.txt" % nb)
        self.import_grille(fichier)
        self.level = "medium"

    def new_difficult(self):
        nb = np.random.randint(1, 101)
        fichier = join(cst.PUZZLES_LOCATION, "difficult", "puzzle_difficult_%i.txt" % nb)
        self.import_grille(fichier)
        self.level = "difficult"

    def translate(self):
        """ changement de la langue de l'interface """
        one_button_box(self, _("Information"),
                       _("The language setting will take effect after restarting the application"),
                       image=self.im_info)
        CONFIG.set("General", "language", self.langue.get())

    def focus_out(self, event):
        """ met en pause si la fenêtre n'est plus au premier plan """
        try:
            if not self.focus_get() and self.chrono_on:
                self.play_pause()
        except KeyError:
            # erreur déclenchée par la présence d'une tkMessagebox
            if self.chrono_on:
                self.play_pause()

    def stacks_reinit(self):
        """efface l'historique des actions"""
        self._undo_stack.clear()
        self._redo_stack.clear()
        self.b_undo.configure(state="disabled")
        self.b_redo.configure(state="disabled")

    def stacks_modif(self, action):
        """Record action and clear redo stack."""
        self._undo_stack.append(action)
        self.b_undo.configure(state="normal")
        self.b_redo.configure(state="disabled")
        self._redo_stack.clear()

    def about(self):
        if self.chrono_on:
            self.play_pause()
        About(self)

    def aide(self, event=None):
        if self.chrono_on:
            self.play_pause()
        Aide(self)

    def quitter(self):
        rep = _("Yes")
        if self.debut:
            rep = two_button_box(self, _("Confirmation"),
                                 _("Do you want to interrupt the current puzzle?"),
                                 _("Yes"), _("No"), image=self.im_question)
        if rep == _("Yes"):
            if self.debut:
                self.save(cst.PATH_SAVE)
            self.destroy()

    def undo(self):
        if self._undo_stack and self.chrono_on:
            self.b_redo.configure(state="normal")
            i, j, val_prec, pos_prec, modifs, val, pos = self._undo_stack.pop(-1)
            self._redo_stack.append((i, j, val_prec, pos_prec, modifs, val, pos))
            if not self._undo_stack:
                self.b_undo.configure(state="disabled")

            if self.blocs[i, j].get_val():
                self.modifie_nb_cases_remplies(-1)
                self.update_nbs(self.blocs[i, j].get_val(), -1)
            self.blocs[i, j].efface_case()
            if val_prec:
                self.modifie_nb_cases_remplies(self.blocs[i, j].edit_chiffre(val_prec))
                if not self.test_case(i, j, val):
                    self.update_grille(i, j, val)
            else:
                for nb in pos_prec:
                    v = int(nb)
                    self.modifie_nb_cases_remplies(self.blocs[i, j].edit_possibilite(v))
                    self.test_possibilite(i, j, v)
            for k, l in modifs:
                self.blocs[k, l].edit_possibilite(val)

    def redo(self):
        if self._redo_stack and self.chrono_on:
            self.b_undo.configure(state="normal")
            i, j, val_prec, pos_prec, modifs, val, pos = self._redo_stack.pop(-1)
            self._undo_stack.append((i, j, val_prec, pos_prec, modifs, val, pos))
            if not self._redo_stack:
                self.b_redo.configure(state="disabled")
            val_prec = self.blocs[i, j].get_val()
            if val_prec:
                self.modifie_nb_cases_remplies(-1)
                self.update_nbs(val_prec, -1)
            self.blocs[i, j].efface_case()
            if val:
                self.modifie_nb_cases_remplies(self.blocs[i, j].edit_chiffre(val))
                if not self.test_case(i, j, val_prec):
                    self.update_grille(i, j, val_prec)
            else:
                for nb in pos:
                    v = int(nb)
                    self.modifie_nb_cases_remplies(self.blocs[i, j].edit_possibilite(v))
                    self.test_possibilite(i, j, v)

    def restart(self, m=0, s=0):
        """ réinitialise le chrono et les boutons """
        self.chrono = [m, s]
        self.chrono_on = False
        self.debut = False
        self.tps.configure(text=" %02i:%02i" % tuple(self.chrono))
        self.b_undo.configure(state="disabled")
        self.b_pause.configure(state="disabled", image=self.im_pause)
        self.b_redo.configure(state="disabled")
        self.b_restart.configure(state="disabled")
        self.stacks_reinit()
        self.frame_pause.place_forget()

    def play_pause(self):
        """ Démarre le chrono s'il était arrêté, le met en pause sinon """
        if self.debut:
            if self.chrono_on:
                self.chrono_on = False
                self.b_pause.configure(image=self.im_play)
                self.b_redo.configure(state="disabled")
                self.b_undo.configure(state="disabled")
                self.tooltip_wrapper.set_tooltip_text(self.b_pause, _("Resume game"))
                self.frame_pause.place(in_=self.frame_puzzle, x=0, y=0, anchor='nw',
                                       relwidth=1, relheight=1)
            elif self.nb_cases_remplies != 81:
                self.chrono_on = True
                self.b_pause.configure(image=self.im_pause)
                self.tps.after(1000, self.affiche_chrono)
                if self._undo_stack:
                    self.b_undo.configure(state="normal")
                if self._redo_stack:
                    self.b_redo.configure(state="normal")
                self.tooltip_wrapper.set_tooltip_text(self.b_pause, _("Pause game"))
                self.frame_pause.place_forget()

    def affiche_chrono(self):
        """ Met à jour l'affichage du temps """
        if self.chrono_on:
            self.chrono[1] += 1
            if self.chrono[1] == 60:
                self.chrono[0] += 1
                self.chrono[1] = 0
            self.tps.configure(text=" %02i:%02i" % tuple(self.chrono))
            self.tps.after(1000, self.affiche_chrono)

    def modifie_nb_cases_remplies(self, nb):
        self.nb_cases_remplies += nb

    def edit_case(self, event):
        if event.num in [1, 3]:
            if not self.debut and self.nb_cases_remplies != 81:
                self.debut = True
                self.b_pause.configure(state="normal")
                self.b_restart.configure(state="normal")
                self.play_pause()
            if str(event.widget) != "." and self.chrono_on:
                if self.clavier:
                    self.clavier.quitter()
                ref = self.blocs[0, 0].winfo_parent()
                case = event.widget.grid_info().get("in", None)
                if str(case) == ref:
                    case = event.widget
                try:
                    if case.is_modifiable():
                        if event.num == 1:
                            self.clavier = Clavier(self, case, "val")
                        elif event.num == 3:
                            self.clavier = Clavier(self, case, "possibilite")
                        self.clavier.display("+%i+%i" % (case.winfo_rootx() - 25, case.winfo_rooty() + 50))
                except AttributeError:
                    if self.clavier:
                        self.clavier.quitter()

            elif self.clavier:
                self.clavier.quitter()