From a3bac3b0fbaa4240adb9c90a06ea0b86a15ef715 Mon Sep 17 00:00:00 2001 From: adrienmalin <41926238+adrienmalin@users.noreply.github.com> Date: Mon, 30 Jul 2018 23:09:25 +0200 Subject: [PATCH] Add files via upload --- PySudoku.ico | Bin 0 -> 55140 bytes PySudoku.pyw | 1198 ++++++++++++++++++++++++++++++++++++++++++ ai_escargot.pysudoku | Bin 0 -> 1198 bytes demo.pysudoku | Bin 0 -> 1405 bytes 4 files changed, 1198 insertions(+) create mode 100644 PySudoku.ico create mode 100644 PySudoku.pyw create mode 100644 ai_escargot.pysudoku create mode 100644 demo.pysudoku diff --git a/PySudoku.ico b/PySudoku.ico new file mode 100644 index 0000000000000000000000000000000000000000..cf9791c3dada700e80bd66c43630a3945e0da3e4 GIT binary patch literal 55140 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 literal 0 HcmV?d00001 diff --git a/PySudoku.pyw b/PySudoku.pyw new file mode 100644 index 0000000..fd2c2bc --- /dev/null +++ b/PySudoku.pyw @@ -0,0 +1,1198 @@ +#! /usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +PySudoku v0.2 by Adrien MALINGREY +Sudoku game assistant +Tested on Windows 10 with Python 3.6.3 and Linux Mint 18 with Python 3.5.1 +""" +try: + from tkinter import * + from tkinter import ttk + from tkinter.filedialog import askopenfilename, asksaveasfilename + from tkinter.messagebox import showinfo, showerror, askokcancel, askyesnocancel + from pickle import Pickler, Unpickler, UnpicklingError + from itertools import combinations + from os.path import basename, dirname, exists + from random import sample, shuffle + from webbrowser import open as open_web_browser + from sys import argv, exit + from string import digits +except ImportError as e: + exit(e.msg) + + +# 16x16, 32x32, 48x48 gif images encoded in base64 +ICON16 = """ + R0lGODlhEAAQAPcAAAAAAGlpaW1tbe0cJKCgoMDAgODggOPj4/Dw8P///wAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAAEAAQAAAIfAADCBxI + sOCBAAgSIiCgcKGBhwYOIhAgYOHEigQgRkRI0WLHjBAlKmQ4UuNBAihTqlR5MIHLBARewixAs0DL + lzEBAJhJ0WYAmTl3EijQ86bLmDhr+lzJNOVBpUNrgnz4tOdQqwYGDNhIVECBq14zauUqFarJA2jT + ql17ICAAOw==""" +ICON32 = """ + R0lGODlhIAAgAPcAAAAAAGlpaX9/f+0cJL23a6CgoPDmjOPj4/Dw8P///wAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAAIAAgAAAI/wALCBxI + sKDBgwMTFAjAsGHDAw4jQoz4UGEABBgzYoSoUSNEAyBDgjxgsWNGjiYRfBQZkuTCjAIEnLyIMeZM + li1L1pS5kSYCmz1xjtT5k6dKn0CPCjXgEqlRlEVvCm2a8mjVlTippoTaEStLlwfCih1LtqzZsBYp + Mpyoli3Fpgniyo0Lce5ciATy6s0L1y7dAH7/7t3b12/duAAAyMWbN6bewnYPJ0i8OIBex3wtBk4g + mbLgxgIeaw7cWfFnApgJQL4LmLTlwZkXbpYc+TVssGdz60a7UG0AtxJ9/7YIm6/twV5FNi2u+jjh + AEuXg35sOzXWAQNyLrwc2jh36iGxaymv3r35d+Phsw/dPt17++bpxzNnfBv6VOLF6SO3n1Xh7v9m + bSbggH4FBAA7""" +ICON48 = """ + R0lGODlhMAAwAPcAAAIAAAAADgkKCgAAEgkXHh0LAAAAOQQVJQcMcAMVeAQdfiI/am0GA20/IV9F + K19ebX97RWNjbWRqbGhhaGtjbm9lbW9pZ2hoa2hob29qbW1sbGdwbGp1bHFqbHppbnptbX1obHZp + cXJpenJza3V+a31ybHt6bHFycXR0czl8uxJ4xV1yi2VzgWp7jHJ5kWx8p22Aan2NanyQa16Eg3WF + l26ArXONunSev3WhyKFNDodobId3bJ1vcJR4dP4BAPwLB/8AHvoUBfkjCP4gLf4vJf4mNcd1I/VR + G/ZEJfRxJf1BRftvVJR6kIuJV4qJW4yKWIWTa4uaapaCbJyJbJSXapWfap6aa5+oaqSdXaSJbKyG + eqmKeaeVbKKUdLqOcrCSbLebbLeQfKGtaq6oa6Kwaa60abKiarGqa7usarS5abmzZ7qyarq0aLy2 + ari5ab25ab+5bMGBU9afVsCqa8+gYM2kdsCyasG5asC6bcG8asK8bvKdQveOb/iOcO7VXe/Yc//G + eoCAg4GBhIaGhoWHiIeOjY2Oh4iIiIqKjJqQhpeXl5efn5+fl5iYmJ6enpmZq52jop2sq7CcmqKj + naCgoKSkpKqqqq2trauyuLiypbGxsbW1tbm5uZ680Yy55abB1qjL36jM2KTW6bzc7L/c9sOqisuw + kt2zh9W9mMq0pd/PktPBtfLLi+PPq+XVs+7fs+/lie/mje7rjeXlne/jlu/tku71lO73mO74lvDl + i/DljPLoivLpjfbsjvDzkObmoOvrpfHwoPX0sfv7sPz6tvz8sP78t///vsnJycPM09PMxNLPz9LS + 1drW1cHf7cjX5Mb//9Hp89fs9dfv+d3q8d7u8tX28d/w/+XZyvTdxO7hw+Hh2ejn3eno3vTlxfLj + 0PT33+Xl5O7u7u7v8OD9+Ony8uv18u7w8O/w9PDu5fDv7vDw5vDw7vL07Pj37f/z7P/+5fDw8PHx + 9fL08PXy8PT09PH0+PX7+Pf///j08f369vr6+vr+////+f7+/gAAACwAAAAAMAAwAAAI/wAVHRpI + sKDBQQYTEkSoMGGgcIMaVZpIsSJFSpcsapzoyB2WjyBDhhQm71CleChTqkwpjlK9lTDjievGS5fN + mzhvEit5MubKli99sqSZs6jNnSZTspuX6tCjeONYumSpzVAhZ1FRzqxpNCfSnvHQmWrBqcsLc1KD + xss2AVMmCtGGcu2qk2fKdpKmnbvnQVralKdw5LNXx5Ncuji/rlTXLMwNen9ReguRTJkIa4cRH7Wr + Mh0hFDTKndM6VWuPEydcZJVJVLMuxSpHx9PSKTI7L6FQorIhe6vr15zjgWNC7py8LZ9s8wBFT16r + Gr1ba4YdT14pFpQSraDWu3Q8VxgWMf+yMCrz9OBLVzmKdE5dZKjYJkF65l6rdMTUhcr0LtS36/z9 + 8eeTf+cdQok4CCaooIKOyLTgg+JwswssFFZoYYU7DcLJJRx26GGHliDz4YgcWpKOEyimqKKKv5S0 + iT8wxiijjJfMaGOM2+iBx4489shjMC7eeGONQs64DRxtJKnkkkoCeciLRdI44zvLMMOPkUgy2cYY + bCzpJJRRwkgkjPoUIIAABFyJY5ZLfhFBGV4GGaaYMuaQAox0qCDjkUyiwUEGcDYp55xj9sOAKDC+ + koCa/vCp5BodpNFBoEl+OSedMRqxAD79NIAAo462YUcJZbwxaZxPXurPmP704wAAA8z/oACobE5B + ghVWdECFG4Kmeimr31wDoxx6rpnkG1OYoKwGI6TRK5hhsgqIAdWoEgA0e7KppKmUtmHprzPGAcAB + pGCp5RsVdPstoaqGuuQbTK4bbbvaatmrJqpa0i4bavTr77/+AilIN9sUbPDBB6+D8MIGwwPMwxBH + HPEw8iACARwYZ6xxxm00ocfGIMPBBjC98GLyySifvJPF9mrJxhN4tLykGsDMhV/FEDCZx855LPly + zEnyzLOSNNvMiy22yJJLYjgviUYEUD9A6c9K6gB1BBJc0GUbRd8ECxI++EAEK0yzrKQZIwytJNVB + 73xHCWIQXfNNewxByy1HKCFLXWYn58mFDGdw6TPMTCIbw8xz65JLLXvn4kcRe2/W9xtSMNuBBLwm + ybaSc2COuM02wSJEH7DwnfOjW+9QxdqELykFFEx2jZMsQSwRueSnB61kFrBr3vqxH5ARe+I2/eED + H7Esbfq2IESRBxodXME60EnasQG8n9uUyx8/sIIL0soD13cbZ3yAdRR3TL8kGDCkn71NSYQdNhC+ + LP9uHu6rv23+cnO1ONIADN/KciezNmxOZrI7z/hkdsCWJfBmgphFMCZIwQpWUIIWzOAEi2EMYnjw + gyD84DHkEQ55mPCEKEyhClfIwhbKYx8BAQA7""" + + +class CustomStyle(ttk.Style): + """ + Manage widgets' style + """ + def __init__(self, app): + ttk.Style.__init__(self, app) + self.app = app + self.theme = StringVar() + self.theme.trace_variable("w", self.change_theme) + self.theme.set(self.theme_use()) + + def change_theme(self, *args): + """ + Called on "View > Theme > theme_name" menu pressed + Change and customize theme of tkinter.ttk widgets + """ + new_theme = self.theme.get() + self.theme_use(new_theme) + # Redefine Entry layout to permit fieldbackground color change + if new_theme in ("vista", "xpnative"): + try: + self.element_create("clam.field", "from", "clam") + except TclError: + pass + self.layout("Box.TEntry", + [('Entry.clam.field', {'sticky': 'nswe', 'border': '1', 'children': + [('Entry.padding', {'sticky': 'nswe', 'children': + [('Entry.textarea', {'sticky': 'nswe'})]})]})]) + self.configure("Box.TEntry", + bordercolor = "grey", + background = "grey", + foreground = "black", + fieldbackground = "white") + if new_theme == "vista": + self.map ("Box.TEntry", + bordercolor = [("hover", "!focus", "!disabled", "black"), ("focus", "dodger blue")], + background = [("hover", "!focus", "!disabled", "black"), ("focus", "dodger blue")]) + # Define style for boxes with highlighted digits or digits' conflicts + self.configure("Box.TEntry", fieldbackground = "white", ) + self.map ("Box.TEntry", fieldbackground = [("disabled", "light grey")]) + self.configure("HighlightArea.Box.TEntry", fieldbackground = "light cyan", ) + self.map ("HighlightArea.Box.TEntry", fieldbackground = [("disabled", "light steel blue")]) + self.configure("HighlightBox.HighlightArea.Box.TEntry", foreground = "midnight blue") + self.map ("HighlightBox.HighlightArea.Box.TEntry", foreground = [("disabled", "blue")]) + self.configure("ErrorArea.Box.TEntry", fieldbackground = "khaki") + self.map ("ErrorArea.Box.TEntry", fieldbackground = [("disabled", "dark khaki")]) + self.configure("ErrorBox.ErrorArea.Box.TEntry", foreground = "red") + self.map ("ErrorBox.ErrorArea.Box.TEntry", foreground = [("disabled", "red")]) + + def redraw(self): + """ + Called when a box's digit is changed + Colorize boxes where highlighted digit can't be written + and boxes with same digits in the same area + """ + if self.app.gr1d.progress_box\ + and self.app.gr1d.progress_box.title() in ("Validation de la grille", "Génération d'une grille"): + # Hide digits behind "?" when grid is validating + for box in self.app.gr1d: + if box.instate(("!disabled",)): + box.configure(style="Box.TEntry", show="?") + else: # Reset style + for box in self.app.gr1d: + box.configure(style="Box.TEntry", show="") + # Disable highlight_button if all it's digit are solved + for digit, highlight_button in enumerate(self.app.highlight_buttons.buttons, start=1): + if sum(box.digit.get() == highlight_button.digit for box in self.app.gr1d) == 9: + highlight_button.disable() + else: + highlight_button.enable() + if HighlightButton.digit: # Highlight selected digitures' area + for box in self.app.gr1d: + if HighlightButton.digit not in box.possible_digits: + box.configure(style="HighlightArea.Box.TEntry") + if HighlightButton.digit == box.digit.get(): + box.configure(style="HighlightBox.HighlightArea.Box.TEntry") + + +class MenuBar(Menu): + """ + Window menu bar + """ + def __init__(self, app): + Menu.__init__(self, app) + self.app = app + # Grid menu + self.grid = IndexedMenu(app) + self.grid.add_command( label="Générer...", underline=0, command=self.app.gr1d.open_nb_clues_message_box) + self.grid.add_separator() + self.grid.add_command( label="Créer", underline=0, command=self.app.gr1d.create) + self.grid.add_command( label="Éditer", underline=4, command=self.app.gr1d.edit, state=DISABLED) + self.grid.add_command( label="Valider", underline=0, command=self.app.gr1d.validate, state=DISABLED) + self.grid.add_separator() + self.grid.add_command( label="Résoudre", underline=0, command=self.app.gr1d.solve, state=DISABLED) + self.add_cascade( label="Grille", underline=0, menu=self.grid) + # Game menu + self.game = IndexedMenu(app) + self.game.add_command( label="Charger...", underline=0, command=self.app.game.open) + self.game.add_command( label="Sauvegarder...", underline=0, command=self.app.game.save, state=DISABLED) + self.game.add_separator() + self.game.add_command( label="Redémarrer...", underline=0, command=self.app.game.restart, state=DISABLED) + self.add_cascade( label="Partie", underline=0, menu=self.game) + # View menu + self.view = IndexedMenu(app) + self.view.theme_menu = Menu(self.view, tearoff=0) + self.view.add_cascade( label="Thème", underline=0, menu=self.view.theme_menu) + for label in self.app.style.theme_names(): + self.view.theme_menu.add_radiobutton(label=label, variable=self.app.style.theme) + self.view.add_separator() + self.view.add_checkbutton(label="Afficher les info-bulles", underline=13, variable=self.app.gr1d.tips_shown) + self.view.add_checkbutton(label="Afficher les conflits", underline=13, variable=self.app.gr1d.conflicts_shown) + self.add_cascade( label="Affichage", underline=0, menu=self.view) + # Help menu + self.help = IndexedMenu(app) + self.help.add_command( label="Wikipédia", underline=0, command=self.app.open_wiki) + self.help.add_separator() + self.help.add_command( label="À propos...", underline=2, command=self.app.show_about) + self.add_cascade( label="?", underline=0, menu=self.help) + + +class IndexedMenu(Menu): + """ + tkinter.Menu redefined to get menus indices + """ + def __init__(self, parent): + Menu.__init__(self, parent, tearoff=0) + self.labels = [] + + def add(self, itemType, cnf, **kw): + super().add(itemType, cnf, **kw) + if "label" in cnf: + self.labels.append(cnf["label"]) + elif "label" in kw: + self.labels.append(kw["label"]) + else: + self.labels.append(None) + + def entryconfigure(self, label_or_index, label=None, **options): + """ + Redefined Menu.entryconfigure + label_or_index can be the entry index as usual or its label + """ + if isinstance(label_or_index, int): + index = label_or_index + elif isinstance(label_or_index, str): + index = self.labels.index(label_or_index) + else: + raise TypeError("label_or_index must be the entry index as int or its label as str") + if label: + super().entryconfigure(index, label=label, **options) + self.labels[index] = label + else: + super().entryconfigure(index, **options) + + +class Gr1d(Frame): + """ + The 9x9 boxes sudoku grid + Leet speak for Grid not to confuse with tkinter.Grid class + """ + def __init__(self, app): + Frame.__init__(self, app) + self.app = app + self.correct = True + self.solutions = self.solution_generator() + self.progress_box = None + self.tips_shown = IntVar(value=True) + self.conflicts_shown = IntVar(value=True) + self.conflicts_shown.trace_variable("w", self.check) + self.regions = [[Region(self, reg_row, reg_col) for reg_col in range(3)] for reg_row in range(3)] + # Structures of boxes used by Gr1d.__iter__ and Box.neighbourhood + self.rows = [[Box(app, self, self.regions[row // 3][col // 3], row, col, row // 3 * 3 + col // 3) + for col in range(9)] for row in range(9)] + self.boxes_of = {"row": self.rows, + "col": [[self.rows[row][col] for row in range(9)] for col in range(9)], + "reg": [[self.rows[row][col] for row in range(reg_row, reg_row + 3) + for col in range(reg_col, reg_col + 3)] + for reg_row in range(0, 9, 3) for reg_col in range(0, 9, 3)]} + self.pack() + + def __getitem__(self, key): + """ Returns a box """ + if isinstance(key, int): + # grid[row][box] returns box of coordinates [row][col] with row, col its integer indices + return self.rows[key] + elif isinstance(key, str): + # grid[area][m][n] returns the nth box of the mth area + # with area in "row" (row), "col" (column) or "reg" (region) + return self.boxes_of[key] + else: + raise TypeError("key must be the row index as int or a area as str") + + def __iter__(self): + """ Browses every box of the grid """ + for boxes_of_row in self.rows: + for box in boxes_of_row: + yield box + + def open_nb_clues_message_box(self): + """ + Opens a message box to get the minimum number of clues, + then generate a grid with a near number of clues + """ + if self.app.game.confirm_erase(): + self.create() + NbCluesMessageBox(self.app) + + def auto_create(self, min_nb_clues): + """ Automatic grid creation """ + self.progress_box = ProgressBox(self.app, + "Génération d'une grille", + "Génération d'une nouvelle grille...", + maximum=81) + # Build a valid solution + try: + self.solve() + except CancelInterrupt: + # self.create(force=True) + self.progress_box.destroy() + self.progress_box = None + else: + remaining_boxes = [box for box in self] + shuffle(remaining_boxes) + nb_clues = len(remaining_boxes) + self.progress_box.text.set("Suppression d'indices...") + self.progress_box.progress_bar.configure(maximum=81) + self.progress_box.cancel_button.configure(text="Arrêter", command=self.progress_box.on_stop) + self.progress_box.cancel_button.bind("", self.progress_box.on_stop) + self.progress_box.bind("", self.progress_box.on_stop) + # Remove clues while the grid is valid + # until the number of clues is near the number requested by user + while remaining_boxes and nb_clues > min_nb_clues and not self.progress_box.cancel_pressed: + box = remaining_boxes.pop(0) + tmp_digit = box.digit.get() + box.state(("!disabled",)) + self.progress_box.variable.set(81 - len(remaining_boxes)) + box.digit.set("") + if self.validate(): + nb_clues -= 1 + else: + box.digit.set(tmp_digit) + box.state(("disabled",)) + self.app.update() + self.progress_box.update() + self.validate() + self.app.game.restart(force=True) + self.app.game.erase_enabled = False + self.progress_box.destroy() + self.progress_box = None + self.check() + + def create(self, force=False): + """ Make a blank grid to edit """ + if self.app.game.confirm_erase(force): + app.title("PySudoku") + for box in self: + box.digit.set("") + self.edit(force=True) + self[0][0].focus_set() + + def edit(self, force=False): + """ Allow user to write the grid """ + if self.app.game.confirm_erase(force): + self.app.game.restart(force=True) + for box in self: + box.state(("!disabled",)) + self.app.menu.grid.entryconfigure("Éditer", state=DISABLED) + self.app.menu.grid.entryconfigure("Valider", state=NORMAL) + self.app.menu.grid.entryconfigure("Résoudre", state=DISABLED) + self.app.menu.game.entryconfigure("Charger...", state=NORMAL) + self.app.menu.game.entryconfigure("Sauvegarder...", state=NORMAL) + self.app.menu.game.entryconfigure("Redémarrer...", state=DISABLED) + + def validate(self): + """ Check if grid is valid: if it has a unique solution """ + if self.correct: + if not self.progress_box: + self.progress_box = ProgressBox( + self.app, "Validation de la grille", + "Vérification que la grille a une solution...", + maximum=sum(len(box.possible_digits) for box in self if not box.digit.get())) + self.progress_box.variable.set(0) + hidden_solutions = self.solution_generator() + try: + next(hidden_solutions) + except StopIteration: # No solution + self.app.game.restart(force=True) + if self.progress_box.title() == "Validation de la grille": + self.progress_box.destroy() + self.progress_box = None + showerror("Grille insoluble", + "La grille comporte des cases sans valeur possible. " + "Corrigez-les pour pouvoir Valider la grille.", + icon="error") + self.edit() + else: + return False + except CancelInterrupt: + self.app.game.restart(force=True) + if self.progress_box.title() == "Validation de la grille": + self.progress_box.destroy() + self.progress_box = None + self.edit(force=True) + else: + return False + else: + if self.progress_box.title() == "Validation de la grille": + self.progress_box.text.set("Vérification qu'il n'y a pas d'autres solution...") + try: + next(hidden_solutions) + except StopIteration: # Unique solution + self.app.game.restart(force=True) + if self.progress_box.title() == "Validation de la grille": + self.progress_box.destroy() + self.progress_box = None + else: + return True + except CancelInterrupt: + self.app.game.restart(force=True) + if self.progress_box.title() == "Validation de la grille": + self.progress_box.destroy() + self.progress_box = None + self.edit(force=True) + else: + return False + else: # More than 1 solution + self.app.game.restart(force=True) + if self.progress_box.title() == "Validation de la grille": + self.progress_box.destroy() + showerror("Grille incorrecte", + "La grille a plusieurs solutions possibles.", + icon="error") + self.edit(force=True) + else: + return False + else: # Incorrect grid + if not self.progress_box: + showerror("Grille incorrecte", + "La grille comporte des cases d'une même colonne, " + "ligne ou région avec des valeurs identiques. " + "Corrigez-les pour pouvoir Valider la grille.", + icon="error") + return False + + def solve(self): + """ Automatic grid solving: the first generated solution """ + if self.correct: + if not self.progress_box: + self.progress_box = ProgressBox( + self.app, "Résolution de la grille", + "Cacul des valeurs sûres...", + maximum = sum(len(box.possible_digits) + for box in self if not box.digit.get())) + self.progress_box.variable.set(0) + try: + next(self.solutions) + except StopIteration as e: + self.app.menu.grid.entryconfigure("Résoudre", state=DISABLED) + if self.progress_box.title() == "Résolution de la grille": + self.progress_box.destroy() + self.progress_box = None + showerror("Grille insoluble", e.args[0] if e.args else "Pas de solution trouvée.", icon="error") + except CancelInterrupt: + if self.progress_box.title() == "Résolution de la grille": + self.progress_box.destroy() + self.progress_box = None + else: + raise CancelInterrupt + else: + self.app.menu.grid.entryconfigure("Résoudre", state=NORMAL) + if self.progress_box.title() == "Résolution de la grille": + self.progress_box.destroy() + self.progress_box = None + else: # Incorrect grid + if not self.progress_box: + showerror("Grille incorrecte", + "La grille comporte des cases d'une même colonne, " + "ligne ou région avec des valeurs identiques. " + "Corrigez-les pour pouvoir résoudre la grille.", + icon="error") + return False + + def check(self, *args): + """ + Called when a box's digit is changed + Check if there is no boxes with same digits in conflict in the same area + """ + self.app.game.erase_enabled = False + self.app.style.redraw() # Redraw GUI + # Check and show digits' conflicts + self.correct = True + for area in "row", "col", "reg": + for index in range(9): + for box1, box2 in combinations(self.app.gr1d[area][index], 2): + if box1.digit.get() == box2.digit.get() != "": + self.correct = False + if self.conflicts_shown.get(): + for box in self.app.gr1d[area][index]: + box.config(style="ErrorArea.Box.TEntry") + for box in box1, box2: + box.config(style="ErrorBox.ErrorArea.Box.TEntry") + for box in self.app.gr1d[area][index]: + if self.conflicts_shown and not box.digit.get() and not box.possible_digits: + box.config(style="ErrorBox.ErrorArea.Box.TEntry") + if not self.progress_box: + # Check if grid is solved + if self.correct: + if sum(box.digit.get() == "" for box in self) == 0: + self.app.menu.grid.entryconfigure("Résoudre", state=DISABLED) + self.app.game.erase_enabled = True + showinfo("Bravo !", "La grille est résolue.") + else: # Focus to the easiest box to solve + try: + next(box for box in self + if not box.digit.get()\ + and len(box.possible_digits) == 1\ + and HighlightButton.digit in {""}|box.possible_digits).focus_set() + except StopIteration: + pass + self.solutions = self.solution_generator() + self.app.menu.grid.entryconfigure("Valider", state=NORMAL) + self.app.menu.grid.entryconfigure("Éditer", state=NORMAL) + self.app.menu.grid.entryconfigure("Résoudre", state=NORMAL) + self.app.menu.game.entryconfigure("Charger...", state=NORMAL) + self.app.menu.game.entryconfigure("Sauvegarder...", state=NORMAL) + self.app.menu.game.entryconfigure("Redémarrer...", state=NORMAL) + + def solution_generator(self, nb_recursion=1): + """ + Yields each solution of the grid + """ + empty_boxes = [box for box in self if box.digit.get() == ""] + found_boxes = [] + shuffle(empty_boxes) + another_digit_found = True + # Find sure digits: when there is only 1 possible digit in the box + while self.correct and empty_boxes and another_digit_found and not self.progress_box.cancel_pressed: + another_digit_found = False + for box in empty_boxes: + if box.possible_digits: + if len(box.possible_digits) == 1: + box.digit.set(box.possible_digits.pop()) + if self.progress_box.title() == "Résolution de la grille" \ + or self.progress_box.text.get() == "Génération d'une nouvelle grille...": + if self.progress_box.text.get() == "Génération d'une nouvelle grille...": + box.state(("disabled",)) + box.update() + self.progress_box.variable.set(self.progress_box.variable.get() + 1 / nb_recursion) + found_boxes.append(box) + empty_boxes.remove(box) + another_digit_found = True + if self.progress_box.cancel_pressed: + self.solutions = self.solution_generator() + raise CancelInterrupt("Annulation") + # Try every possible digits + elif self.correct: + if empty_boxes: + empty_boxes.sort(key=lambda box: len(box.possible_digits)) + tested_box = empty_boxes[0] + digits_to_try = sample(tested_box.possible_digits, len(tested_box.possible_digits)) + for tested_digit in digits_to_try: + tested_box.digit.set(tested_digit) + if self.progress_box.title() == "Résolution de la grille": + self.progress_box.text.set("Essai : " + tested_digit + " en " + str(tested_box)) + elif self.progress_box.text.get() == "Génération d'une nouvelle grille...": + tested_box.state(("disabled",)) + tested_box.update() + self.progress_box.variable.set(self.progress_box.variable.get() + 1 / nb_recursion) + yield from self.solution_generator(1 if self.progress_box.text.get() == "Génération d'une nouvelle grille..." else nb_recursion + 1) + for box in empty_boxes: + if box.digit.get(): + if self.progress_box.title() == "Génération d'une grille": + box.state(("!disabled",)) + box.digit.set("") + else: + yield None + else: + # Incorrect grid, maybe a wrong hypothesis + self.app.menu.grid.entryconfigure("Résoudre", state=DISABLED) + raise StopIteration("La grille comporte des erreurs. Corrigez-les pour pouvoir résoudre la grille.") + for box in found_boxes: + if self.progress_box.title() == "Génération d'une grille": + box.state(("!disabled",)) + box.digit.set("") + self.app.menu.grid.entryconfigure("Résoudre", state=DISABLED) + + def set_box_focus(self, box, row_delta, col_delta): + """ Select an enabled adjacent box """ + row, col = box.row, box.col + keep_browse = True + while keep_browse: + col += col_delta; row += row_delta + if col > 8: + col = 0; row = (row + 1) % 9 + elif col < 0: + col = 8; row = (row - 1) % 9 + if row < 0: + row = 8; col = (col - 1) % 9 + elif row > 8: + row = 0; col = (col + 1) % 9 + keep_browse = self.rows[row][col].instate(("disabled",)) + self.rows[row][col].focus_set() + + +class Region(Frame): + """ + 3x3 boxes subgrid/region + """ + def __init__(self, grid, reg_row, reg_col): + Frame.__init__(self, grid, relief=SUNKEN, borderwidth=2) + self.grid(row=reg_row, column=reg_col, sticky="nswe") + + +class Box(ttk.Entry): + """ + Entry box of the grid + """ + + def __init__(self, app, gr1d, region, row, col, reg, *options): + self.app = app + self.digit = StringVar() + self.digit.trace_variable("w", self.on_digit_change) + ttk.Entry.__init__(self, + region, + textvariable = self.digit, + style = "Box.TEntry", + font = ("Arial", 16), + width = 2, + justify = CENTER, + validate = "key", + validatecommand = (app.register(self.check), "%P"), + *options) + self.state(("disabled",)) + self.row, self.col, self.reg = row, col, reg + self.possible_digits = set(digits) + self.grid(row=row % 3, column=col % 3) + self.bind("", self.on_focus) + self.bind("", lambda event: gr1d.set_box_focus(self, -1, 0)) + self.bind("", lambda event: gr1d.set_box_focus(self, +1, 0)) + self.bind("", lambda event: gr1d.set_box_focus(self, 0, -1)) + self.bind("", lambda event: gr1d.set_box_focus(self, 0, +1)) + self.tip = Tooltip(self) + gr1d.tips_shown.trace_variable("w", self.show_tips) + + def check(self, changed_digit): + """ + Called on user input + Input is allowed only if it's one digit + """ + return len(changed_digit) <= 1 and changed_digit in digits + + def on_focus(self, event): + """ Selects text """ + self.select_range(0, END) + + def neighbourhood(self, area): + """ + neighbourhood(area) browses every boxes of the area of the box + with area in "row" (row), "col" (column) or "reg" (region) + """ + for box in self.app.gr1d[area][getattr(self, area)]: + yield box + + def on_digit_change(self, *args): + """ + Called when a box's digit is changed + Find the digits wich aren't in the row, column or region of the box + """ + for area in "row", "col", "reg": + for box in self.neighbourhood(area): + box.possible_digits = set(digits) + for box_area in "row", "col", "reg": + box.possible_digits -= {box.digit.get() for box in box.neighbourhood(box_area)} + if self.app.gr1d.tips_shown.get() and box.digit.get() == "": + if box.possible_digits: + box.tip.text = " ".join(sorted(box.possible_digits)) + " ?" + else: + box.tip.text = "???" + box.tip.shown = True + else: + box.tip.shown = False + if not self.app.gr1d.progress_box and not HighlightButton.digit and self.digit.get(): + self.app.gr1d.set_box_focus(self, 0, +1) + self.app.gr1d.check() + if not self.app.gr1d.correct: + self.focus_set() + + + def show_tips(self, *args): + """ + Called when "View > Show tips" menu is changed + Show or hide a tooltip on the box with its possible digits + """ + self.tip.shown = self.tips_shown.get() + + def __str__(self): + return "case " + str((self.row + 1, self.col + 1)) + + def __repr__(self): + return str((self.row, self.col)) + str([self.digit.get()]) + str(self.possible_digits) + + +class Tooltip: + ''' + It creates a tooltip for a given widget as the mouse goes on it. + + see: + + http://stackoverflow.com/questions/3221956/ + what-is-the-simplest-way-to-make-tooltips- + in-tkinter/36221216#36221216 + + http://www.daniweb.com/programming/software-development/ + code/484591/a-tooltip-class-for-tkinter + + - Originally written by vegaseat on 2014.09.09. + + - Modified to include a delay time by Victor Zaccardo on 2016.03.25. + + - Modified + - to correct extreme right and extreme bottom behavior, + - to stay inside the screen whenever the tooltip might go out on + the top but still the screen is higher than the tooltip, + - to use the more flexible mouse positioning, + - to add customizable background color, padding, waittime and + wraplength on creation + by Alberto Vassena on 2016.11.05. + + Tested on Ubuntu 16.04/16.10, running Python 3.5.2 + + TODO: themes styles support + ''' + + TIP_DELAY = 2000 + + def __init__(self, widget, + *, + bg='white', + fg='dim gray', + pad=(3, 2, 3, 2), + text='widget info', + delay=TIP_DELAY, + wraplength=250, + shown=True): + + + self.shown = shown # Added by AM + self.delay = delay # in milliseconds, originally 500 + self.wraplength = wraplength # in pixels, originally 180 + self.widget = widget + self.text = text + self.bg = bg + self.fg = fg + self.pad = pad + self.id = None + self.tw = None + self.widget.bind("", self.onEnter) + self.widget.bind("", self.onLeave) + self.widget.bind("", self.onLeave) + + def onEnter(self, event=None): + if self.shown: # Added by AM + self.schedule() + + def onLeave(self, event=None): + self.unschedule() + self.hide() + + def schedule(self): + self.unschedule() + self.id = self.widget.after(self.delay, self.show) + + def unschedule(self): + id_ = self.id + self.id = None + if id_: + self.widget.after_cancel(id_) + + def show(self): + def tip_pos_calculator(widget, label, + *, + tip_delta=(10, 5), pad=(5, 3, 5, 3)): + + w = widget + + s_width, s_height = w.winfo_screenwidth(), w.winfo_screenheight() + + width, height = (pad[0] + label.winfo_reqwidth() + pad[2], + pad[1] + label.winfo_reqheight() + pad[3]) + + mouse_x, mouse_y = w.winfo_pointerxy() + + x1, y1 = mouse_x + tip_delta[0], mouse_y + tip_delta[1] + x2, y2 = x1 + width, y1 + height + + x_delta = x2 - s_width + if x_delta < 0: + x_delta = 0 + y_delta = y2 - s_height + if y_delta < 0: + y_delta = 0 + + offscreen = (x_delta, y_delta) != (0, 0) + + if offscreen: + + if x_delta: + x1 = mouse_x - tip_delta[0] - width + + if y_delta: + y1 = mouse_y - tip_delta[1] - height + + offscreen_again = y1 < 0 # out on the top + + if offscreen_again: + # No further checks will be done. + + # TIP: + # A further mod might automagically augment the + # wraplength when the tooltip is too high to be + # kept inside the screen. + y1 = 0 + + return x1, y1 + + bg = self.bg + fg = self.fg + pad = self.pad + widget = self.widget + + # creates a toplevel window + self.tw = Toplevel(widget) + + # Leaves only the label and removes the app window + self.tw.wm_overrideredirect(True) + + border = Frame(self.tw, background=fg) + win = Frame(border, + background = bg, + borderwidth = 1) + label = Label(win, + text = self.text, + justify = LEFT, + background = bg, + foreground = fg, + relief = SOLID, + borderwidth = 0, + wraplength = self.wraplength) + + label.grid(padx=(pad[0], pad[2]), + pady=(pad[1], pad[3]), + sticky=NSEW) + win.grid(padx=1, pady=1) + border.grid() + + x, y = tip_pos_calculator(widget, label) + + self.tw.wm_geometry("+%d+%d" % (x, y)) + + def hide(self): + tw = self.tw + if tw: + tw.destroy() + self.tw = None + + +class NbCluesMessageBox(Toplevel): + """ + A message box allowing user to choose a number of clues boxes + """ + + DEFAULT_NB_CLUES = 25 + MIN_NB_CLUES = 17 + MAX_NB_CLUES = 80 + + def __init__(self, app): + Toplevel.__init__(self, app) + self.app = app + self.title("Générer une nouvelle grille") + self.resizable(width=False, height=False) + self.grab_set() + self.nb_clues = IntVar() + self.nb_clues.set(NbCluesMessageBox.DEFAULT_NB_CLUES) + self.nb_clues.trace_variable("w", self.round_nb_clues) + message_box_frame = ttk.Frame(self) + enter_nb_frame = ttk.Frame(message_box_frame) + enter_nb_label = ttk.Label(enter_nb_frame, text="Entrez le nombre minimum d'indices :") + enter_nb_label.pack(side=LEFT) + spinbox = Spinbox(enter_nb_frame, + width = 2, + from_ = NbCluesMessageBox.MIN_NB_CLUES, + to = NbCluesMessageBox.MAX_NB_CLUES, + increment = 1, + textvariable = self.nb_clues) + spinbox.pack(side=RIGHT) + enter_nb_frame.pack(padx=5, pady=5, fill=X) + scale_frame = ttk.Frame(message_box_frame) + simpler_msg = ttk.Label(scale_frame, text="← plus\ndifficile", justify=RIGHT) + simpler_msg.pack(side=LEFT) + self.scale = ttk.Scale(scale_frame, + command = self.round_nb_clues, + length = 162, + from_ = NbCluesMessageBox.MIN_NB_CLUES, + to = NbCluesMessageBox.MAX_NB_CLUES, + orient = HORIZONTAL, + variable = self.nb_clues) + self.scale.pack(side=LEFT, padx=5, pady=5) + more_difficult_msg = ttk.Label(scale_frame, text="plus →\nfacile", justify=LEFT) + more_difficult_msg.pack(side=LEFT, padx=5, pady=5) + scale_frame.pack(padx=5, pady=5) + buttons_frame = ttk.Frame(message_box_frame) + cancel_button = ttk.Button(buttons_frame, text="Annuler", command=self.destroy) + cancel_button.pack(side=RIGHT, padx=5, pady=5) + cancel_button.bind("", lambda event: self.destroy()) + ok_button = ttk.Button(buttons_frame, text="OK", command=self.on_ok) + ok_button.pack(side=RIGHT, padx=5, pady=5) + ok_button.bind("", self.on_ok) + ok_button.focus_set() + buttons_frame.pack() + self.bind("", self.on_ok) + self.bind("", self.destroy) + message_box_frame.pack() + + def round_nb_clues(self, *args): + try: + nb = int(self.nb_clues.get()) + except TclError: + nb = NbCluesMessageBox.MIN_NB_CLUES + finally: + if nb < NbCluesMessageBox.MIN_NB_CLUES: + nb = NbCluesMessageBox.MIN_NB_CLUES + elif nb > NbCluesMessageBox.MAX_NB_CLUES: + nb = NbCluesMessageBox.MAX_NB_CLUES + self.nb_clues.set(nb) + self.update() + + def on_ok(self, event=None): + """ Generate a grid on [OK] button press """ + self.destroy() + self.app.gr1d.auto_create(self.nb_clues.get()) + + +class CancelInterrupt(KeyboardInterrupt): + pass + + +class ProgressBox(Toplevel): + """ + Message box showing progress used by Gr1d.validate and Gr1d.generate + """ + def __init__(self, app, title="", text="", **options): + Toplevel.__init__(self, app) + self.protocol("WM_DELETE_WINDOW", self.on_cancel) + self.title(title) + self.resizable(width=False, height=False) + self.grab_set() + frame = ttk.Frame(self) + self.text = StringVar() + self.text.set(text) + label = ttk.Label(frame, textvariable=self.text) + label.pack(anchor=W, padx=10, pady=5) + self.variable = DoubleVar(0) + self.variable.trace_variable("w", lambda *args: self.update()) + self.progress_bar = ttk.Progressbar(frame, + length = 250, + orient = "horizontal", + mode = "determinate", + variable = self.variable, + **options) + self.progress_bar.pack(padx=10, pady=5) + self.cancel_button = ttk.Button(frame, text="Annuler", command=self.on_cancel) + self.cancel_button.bind("", self.on_cancel) + self.cancel_button.pack(side=RIGHT, padx=10, pady=5) + self.cancel_pressed = False + frame.pack(ipady=5) + self.bind("", self.on_cancel) + self.update() + + def on_cancel(self, event=None): + """ + Called on Cancel button or quit button press + """ + self.cancel_button.state(("disabled",)) + self.text.set("Annulation...") + self.cancel_pressed = True + self.update() + + def on_stop(self, event=None): + """ + Called on Cancel button or quit button press + """ + self.cancel_button.state(("disabled",)) + self.text.set("Arrêt...") + self.cancel_pressed = True + self.update() + + + +class HighlightButtonsFrame(Frame): + """ Frame of HighlightButtons looking like a status bar """ + def __init__(self, app): + Frame.__init__(self, app, border=1, relief=SUNKEN) + self.buttons = [HighlightButton(app, self, digit) for digit in digits] + self.pack(fill=X) + + +class HighlightButton(ttk.Button): + """ + Buttons showing every digits + Allowing user to see where not to write the selected digit + """ + digit = "" + + def __init__(self, app, parent, digit): + self.digit = digit + self.pressed = False + self.app = app + ttk.Button.__init__(self, + parent, + text = digit, + width = 2, + padding = "0 0", + command = self.on_click) + self.app = app + self.state(("disabled",)) + self.pack(side="left", expand=True, fill="x") + + def on_click(self, *args): + """ + Select or unselect a digit and show where not to write the selected digit + """ + if self.pressed: + self.state(("!pressed",)) + self.pressed = False + HighlightButton.digit = "" + else: + for highlight_button in self.app.highlight_buttons.buttons: + highlight_button.state(("!pressed",)) + highlight_button.pressed = False + self.state(("pressed",)) + self.pressed = True + HighlightButton.digit = self.digit + self.app.gr1d.check() + + def enable(self): + """ Enable button when all the same digits aren't found """ + self.state(("!disabled",)) + + def disable(self): + """ Disable button when all the same digits are found """ + self.state(("disabled",)) + if HighlightButton.digit == self.digit: + HighlightButton.digit = "" + + +class Game: + """ + Game functions + """ + def __init__(self, app): + self.app = app + self.erase_enabled = True + self.file_path = "" + + def confirm_erase(self, force=False): + """ If a game is started, pop a message box to confirm game erasement """ + self.erase_enabled = self.erase_enabled or force\ + or askokcancel("Effacer la partie en cours ?", + "Une partie est en cours. Voulez-vous l'effacer ?", + default="cancel", icon="warning") + return self.erase_enabled + + def open(self, file_path=""): + """ Open a game saved in a file """ + if self.confirm_erase(): + self.file_path = file_path or askopenfilename(parent = self.app, + defaultextension = ".pysudoku", + initialdir = ".", + title = "Ouvrir une partie", + filetypes = [("Partie PySudoku", "*.pysudoku")]) + if exists(self.file_path): + self.app.gr1d.create() + with open(self.file_path, "rb") as file: + loader = Unpickler(file) + try: + HighlightButton.digit = loader.load() + for box in self.app.gr1d: + box.state(("!disabled",)) + box.state(loader.load()) + box.digit.set(loader.load()) + except UnpicklingError: + showerror("Erreur de fichier", "Le fichier n'a pas pu être lu.", icon="error") + else: + print(basename(self.file_path), basename(self.file_path).rstrip("pysudoku")) + self.app.title(basename(self.file_path).rstrip(".pysudoku")+" - PySudoku") + self.erase_enabled = True + self.app.menu.grid.entryconfigure("Valider", state=NORMAL) + self.app.self.app.menu.grid.entryconfigure("Éditer", state=NORMAL) + self.app.menu.grid.entryconfigure("Résoudre", state=NORMAL) + self.app.menu.game.entryconfigure("Charger...", state=NORMAL) + self.app.menu.game.entryconfigure("Sauvegarder...", state=DISABLED) + self.app.menu.game.entryconfigure("Redémarrer...", state=NORMAL) + else: + showerror("Erreur de fichier", "Le fichier "+basename(file_path)+" n'a pas été trouvé.", icon="error") + + def save(self): + """ Save a game in a file """ + save_path = asksaveasfilename(parent = self.app, + defaultextension = ".pysudoku", + initialdir = dirname(self.file_path), + initialfile = basename(self.file_path), + title = "Enregistrer la partie", + filetypes = [("Partie PySudoku", "*.pysudoku")]) + if save_path: + self.file_path = save_path + with open(save_path, "wb") as file: + saver = Pickler(file) + saver.dump(HighlightButton.digit) + for box in self.app.gr1d: + saver.dump(box.state()) + saver.dump(box.digit.get()) + self.erase_enabled = True + self.app.title(basename(self.file_path).rstrip(".pysudoku")+" - PySudoku") + return save_path + + def restart(self, force=False): + """ Restart game: blank enabled boxes and keep clues boxes """ + if force or self.confirm_erase(): + for box in self.app.gr1d: + if box.instate(("!disabled",)): + box.digit.set("") + self.erase_enabled = True + self.app.menu.grid.entryconfigure("Éditer", state=NORMAL) + self.app.menu.grid.entryconfigure("Valider", state=DISABLED) + self.app.menu.grid.entryconfigure("Résoudre", state=NORMAL) + self.app.menu.game.entryconfigure("Charger...", state=NORMAL) + self.app.menu.game.entryconfigure("Sauvegarder...", state=NORMAL) + self.app.menu.game.entryconfigure("Redémarrer...", state=NORMAL) + + + + +class App(Tk): + def __init__(self, argv): + Tk.__init__(self) + self.title("PySudoku") + self.iconphoto(True, + PhotoImage(name="icon16", data=ICON16), + PhotoImage(name="icon32", data=ICON32), + PhotoImage(name="icon48", data=ICON48)) # Titlebar + try: # Windows taskbar icon + from ctypes import windll + windll.shell32.SetCurrentProcessExplicitAppUserModelID("MALINGREY.Adrien.PySudoku.0.2") + except ImportError: # Linux + pass + self.resizable(width=False, height=False) + self.style = CustomStyle(self) + self.gr1d = Gr1d(self) # leet speak for grid not to confuse with tkinter.Grid.grid method + self.highlight_buttons = HighlightButtonsFrame(self) + self.game = Game(self) + self["menu"] = self.menu = MenuBar(self) # Window menu bar + self.protocol("WM_DELETE_WINDOW", self.on_close) # Action on close button press + if len(argv) > 1: + self.game.open(argv[1]) + + def open_wiki(self): + """ Open Sudoku article on wikipedia to learn sudoku rules (and more) """ + open_web_browser("https://fr.wikipedia.org/wiki/Sudoku") + + def show_about(self): + """ About message box """ + showinfo("À propos de PySudoku", + "Auteur : Adrien Malingrey\n" + "Licence : CC-BY-SA") + + + def on_close(self): + """ + Called on close button press + Allow user to save started game and quit + """ + save_before_close = not self.game.erase_enabled \ + and askyesnocancel("Enregistrer la partie ?", + "Une partie est en cours. Voulez-vous l'enregistrer ?", + icon="warning", default="cancel") + + if save_before_close != None: # [Yes] or [No] button pressed (not [Cancel]) + if save_before_close == True: # [Yes] button pressed + if self.game.save() == "": # Save dialog box cancelled + self.on_close() # Ask again + self.quit() + else : #[Cancel] button pressed : do nothing + pass + + + +if __name__ == "__main__": + app = App(argv) + exit(app.mainloop()) \ No newline at end of file diff --git a/ai_escargot.pysudoku b/ai_escargot.pysudoku new file mode 100644 index 0000000000000000000000000000000000000000..b40477c2cf08e23c2d501dcbb33adba909850f8d GIT binary patch literal 1198 zcma)+Sx*8%5QITQ@xJf-77vsg{t2%tnl&*o72-QD^}n5ZmBRbtJfJ*-Djd%O)|)5#Luik)l`+uo~#og7}f z#GWVG*pCw7;GwXo2gIQR6VY~ZL>z09XN4!kskFMQ5R@)#z!`BaDE=3++6&@R+W3k? zl7+5_Ye9OXc5*}9Y7*aaunUEytHhlw^#-(X@5&%d7Z!aW9u*v4YMM@WtY3|Il3$)3 zJ`;6m|2c3vGIVTqgJ{anyuKOQ7SYbuWJv5u~E%)ty#1i0pI zq9?J}P;RrYM3hQV_>NxUY3iurS({i)ui^Rq<2qink6zD&t?(22i2gKT83QWa&V$5I z{_#q)J@S$m79!Uk86jQ?N%1_eQIs1c#?)l7q%;c|Kj1a-M%+Mq9(4^^~=?X6(JUfnoZ7gSBW)o zW2)vpX1Go?LX+41Mtm2=-kixr*FT62aihmgVoON;ZEO=ig+#Sq#EuZVMz@0B@;9;j z@Z>#WUr026KpYCO#B|e^?jx6VL>#Lr8aW|Og_w%z%9_`W%bXGC>W=U5g7_n(RJjBF ChhilF literal 0 HcmV?d00001