# sudoku_ui.py # by: David Blume # August 2005 # # This code is provided as is. You are free to use and modify it. # # This is a wxWidget application that requires my sudoku.py module. # It's my first program, so there must be plenty wrong with it. # # Instructions: # * Try some of the games included in the File menu. # * Keyboard or mouse navigate around the board. # * Right-clicking brings up a context menu of choices. # * Selecting Edit->Runtime_Analysis limits the choices to available choices. # * Selecting Edit->Solve attempts to solve the game. # # Features: # Save, Open, Copy, Paste, Multiple-Undo and Redo all work. # # See http://www.wxpython.org/onlinedocs.php import os import wx import wx.html import string import sudoku colors = [wx.Colour(255, 255, 255), wx.Colour(112, 112, 245), wx.Colour(84, 98, 211), wx.Colour(42, 84, 190), wx.Colour(70, 70, 169), wx.Colour(56, 56, 148), wx.Colour(42, 42, 127), wx.Colour(28, 28, 106), wx.Colour(20, 20, 96), wx.Colour(0, 0, 64), wx.Colour(225, 225, 0), # Selected item wx.Colour(128, 128, 0)] # Disabled selected clipboard_translation = string.maketrans("ABCDEFGHI0", "__________") wx.RegisterId(5500) ID_ANALYSE = wx.NewId() ID_SOLVE = wx.NewId() ID_TEST_FILES = [wx.NewId() for i in range(len(sudoku.sample_boards))] class MainWindow(wx.Frame): """ We simply derive a new class of Frame. """ menuItems = {100: '1', 101: '2', 102: '3', 103: '4', 104: '5', 105: '6', 106: '7', 107: '8', 108: '9'} def __init__(self, parent, id, title): wx.Frame.__init__(self, parent, wx.ID_ANY, title, size=(302, 302), style=wx.DEFAULT_FRAME_STYLE | wx.NO_FULL_REPAINT_ON_RESIZE) self.CreateStatusBar() # A statusbar in the bottom of the window self.rects = [] for i in range(sudoku.nelements * sudoku.nelements): self.rects.append(wx.Rect()) self.board = sudoku.Board() self.selected_cell = 0 self.analyse = False self.InitBuffer() # Setting up the menu filemenu = wx.Menu() filemenu.Append(wx.ID_NEW, "&New\tCtrl+N", "Create a new game") filemenu.Append(wx.ID_OPEN, "&Open...\tCtrl+O", "Open a game") filemenu.Append(wx.ID_SAVE, "&Save\tCtrl+S", "Save the game") filemenu.Append(wx.ID_SAVEAS, "Save &As...", "Save the game in a new file") filemenu.AppendSeparator() for i in ID_TEST_FILES: filemenu.Append(i, sudoku.sample_boards[i - ID_TEST_FILES[0]]["name"], "Open a test file") filemenu.AppendSeparator() filemenu.Append(wx.ID_EXIT, "E&xit\tCtrl+W", "Quit the program") editmenu = wx.Menu() editmenu.Append(wx.ID_UNDO, "&Undo\tCtrl+Z", "Undo the last action") editmenu.Append(wx.ID_REDO, "&Redo\tCtrl+Y", "Redo the action undone") editmenu.AppendSeparator() editmenu.Append(wx.ID_COPY, "&Copy\tCtrl+C", "Copy a board in text format") editmenu.Append(wx.ID_PASTE, "&Paste\tCtrl+V", "Paste a text representation of a board") editmenu.AppendSeparator() editmenu.Append(ID_ANALYSE, "Runtime &Analysis\tCtrl+R", "Perform runtime cell reduction", kind=wx.ITEM_CHECK) editmenu.Append(ID_SOLVE, "&Solve\tCtrl+L", "Attempt to solve") helpmenu = wx.Menu() helpmenu.Append(wx.ID_ABOUT, "&About...\tCtrl+I", "Information about Sudoku") # Creating the menubar menu_bar = wx.MenuBar() menu_bar.Append(filemenu, "&File",) menu_bar.Append(editmenu, "&Edit",) menu_bar.Append(helpmenu, "&Help",) self.SetMenuBar(menu_bar) wx.EVT_MENU(self, wx.ID_NEW, self.OnNew) wx.EVT_MENU(self, wx.ID_OPEN, self.OnOpen) wx.EVT_MENU(self, wx.ID_SAVE, self.OnSave) wx.EVT_MENU(self, wx.ID_SAVEAS, self.OnSaveAs) wx.EVT_MENU_RANGE(self, ID_TEST_FILES[0], ID_TEST_FILES[-1], self.OnSampleFile) wx.EVT_MENU(self, wx.ID_EXIT, self.OnExit) wx.EVT_MENU(self, wx.ID_UNDO, self.OnUndo) wx.EVT_MENU(self, wx.ID_REDO, self.OnRedo) wx.EVT_MENU(self, wx.ID_COPY, self.OnCopy) wx.EVT_MENU(self, wx.ID_PASTE, self.OnPaste) wx.EVT_MENU(self, ID_ANALYSE, self.OnAnalyse) wx.EVT_MENU(self, ID_SOLVE, self.OnSolve) wx.EVT_MENU(self, wx.ID_ABOUT, self.OnAbout) wx.EVT_LEFT_DOWN(self, self.OnLeftDown) wx.EVT_LEFT_UP(self, self.OnLeftUp) wx.EVT_RIGHT_DOWN(self, self.OnLeftDown) wx.EVT_RIGHT_UP(self, self.OnRightUp) wx.EVT_MOTION(self, self.OnMotion) wx.EVT_SIZE(self, self.OnSize) wx.EVT_IDLE(self, self.OnIdle) wx.EVT_PAINT(self, self.OnPaint) wx.EVT_WINDOW_DESTROY(self, self.Cleanup) wx.EVT_UPDATE_UI(self, wx.ID_UNDO, self.OnUpdateMenu) wx.EVT_UPDATE_UI(self, wx.ID_REDO, self.OnUpdateMenu) wx.EVT_UPDATE_UI(self, wx.ID_PASTE, self.OnUpdateMenu) wx.EVT_UPDATE_UI(self, ID_ANALYSE, self.OnUpdateMenu) wx.EVT_KEY_DOWN(self, self.OnKeyDown) first, last = self.MakeMenu() wx.EVT_MENU_RANGE(self, first, last, self.OnMenuSetValue) self.dirname = os.getcwd() self.filename = "" self.undo_stack = [] self.undo_stack.append(tuple([self.board.get("", ""), self.selected_cell])) self.undo_pos = 0 self.Show(True) title = "Sudoku" extension = ".txt" wildcard = "Sudoku files (*" + extension + ")|*" + extension + "|All files (*.*)|*.*" rows = sudoku.nelements def OnSampleFile(self, e): self.SetBoard(sudoku.sample_boards[e.GetId() - ID_TEST_FILES[0]]["board"]) self.DrawNow() pass def OnSolve(self, e): waitCursor = wx.BusyCursor() if self.analyse is False: self.OnAnalyse(None) while self.board.solve(): self.DrawNow() self.DrawNow() self.undo_pos += 1 if len(self.undo_stack) > self.undo_pos + 1: self.undo_stack = self.undo_stack[:self.undo_pos + 1] self.undo_stack.append(tuple([self.board.get("", ""), self.selected_cell])) try: self.board.check() except Exception, detail: d = wx.MessageDialog(self, str(detail), "Bad Board", wx.OK) d.ShowModal() d.Destroy() def OnAnalyse(self, e): if self.analyse: self.analyse = False else: self.analyse = True self.board.analyse(self.analyse) self.board.reset() self.board.set_board(self.undo_stack[self.undo_pos][0]) self.DrawNow() try: self.board.check() except Exception, detail: d = wx.MessageDialog(self, str(detail), "Bad Board", wx.OK) d.ShowModal() d.Destroy() def OnPaste(self, e): if wx.TheClipboard.Open(): if wx.TheClipboard.IsSupported(wx.DataFormat(wx.DF_TEXT)): data = wx.TextDataObject() wx.TheClipboard.GetData(data) s = data.GetText() s = s.replace("+", "").replace("\n", "").replace(chr(13), "") if len(s) > self.rows * self.rows: s = s.replace(" ", "") if len(s) > self.rows * self.rows: s = s.replace("|", "").replace("-", "") if len(s) == self.rows * self.rows: self.SetBoard(s) self.DrawNow() else: d = wx.MessageDialog(self, "The contents of the clipboard was not a text string of " + str(self.rows * self.rows) + " characters.", "Paste Error", wx.OK) d.ShowModal() d.Destroy() wx.TheClipboard.Close() def OnCopy(self, e): """ Use the string representation from http://www.sudokusolver.co.uk/ """ if wx.TheClipboard.Open(): s = self.undo_stack[self.undo_pos][0] s = string.translate(s, clipboard_translation) s = "+".join([s[t:t+9] for t in range(0, self.rows * self.rows, self.rows)]) wx.TheClipboard.SetData(wx.TextDataObject(s)) wx.TheClipboard.Close() def OnRightUp(self, event): pt = event.GetPosition() self.MakeMenu(self.board._cells[self.selected_cell]) self.PopupMenu(self.popup_menu, pt) def SetValue(self, value): if self.analyse is False or value in self.board._cells[self.selected_cell]._values: self.undo_pos += 1 if len(self.undo_stack) > self.undo_pos + 1: self.undo_stack = self.undo_stack[:self.undo_pos + 1] if value is 0: self.board._cells[self.selected_cell].Reset() self.selected_cell -= 1 else: self.board.set_cell(self.selected_cell, value) self.selected_cell += 1 self.undo_stack.append(tuple([self.board.get("", ""), self.selected_cell])) if self.selected_cell >= self.rows * self.rows: self.selected_cell = 0 if self.selected_cell < 0: self.selected_cell = self.rows * self.rows - 1 self.DrawNow() try: self.board.check() except Exception, detail: d = wx.MessageDialog(self, str(detail), "Bad Board", wx.OK) d.ShowModal() d.Destroy() def DrawNow(self): dc = wx.BufferedDC(wx.ClientDC(self), self.buffer) self.Draw(dc) def SetBoard(self, values): self.board.reset() self.board.set_board(values) if len(self.undo_stack) > self.undo_pos + 1: self.undo_stack = self.undo_stack[:self.undo_pos + 1] self.undo_stack.append(tuple([self.board.get("", ""), self.selected_cell])) self.undo_pos += 1 def OnMenuSetValue(self, event): self.SetValue(event.GetId() - 99) def OnKeyDown(self, e): keycode = e.GetKeyCode() if keycode == wx.WXK_TAB or keycode == wx.WXK_RIGHT or keycode == wx.WXK_RETURN or keycode == wx.WXK_SPACE: self.selected_cell += 1 if self.selected_cell == self.rows * self.rows: self.selected_cell = 0 self.DrawNow() elif keycode == wx.WXK_UP: self.selected_cell -= self.rows if self.selected_cell < 0: self.selected_cell += self.rows * self.rows self.DrawNow() elif keycode == wx.WXK_DOWN: self.selected_cell += self.rows if self.selected_cell >= self.rows * self.rows: self.selected_cell -= self.rows * self.rows self.DrawNow() elif keycode == wx.WXK_LEFT: self.selected_cell -= 1 if self.selected_cell < 0: self.selected_cell = self.rows * self.rows - 1 self.DrawNow() elif keycode == wx.WXK_DELETE: if self.analyse is False: self.SetValue(0) elif keycode > ord("0") and keycode <= ord("9"): if self.analyse is False or self.board._cells[self.selected_cell].count() > 1: self.SetValue(keycode - ord("0")) def OnIdle(self, e): """ If the size was changed then resize the bitmap used for double buffering to match the window size. We do it in Idle time so there is only one refresh after resizing is done, not lots while it is happening. """ if self.reInitBuffer: self.InitBuffer() self.Refresh(False) def OnPaint(self, event): """ Called when the window is exposed. """ # Create a buffered paint DC. It will create the real # wxPaintDC and then blit the bitmap to it when dc is # deleted. Since we don't need to draw anything else # here that's all there is to it. dc = wx.BufferedPaintDC(self, self.buffer) def Cleanup(self, evt): if hasattr(self, "popup_menu"): self.popup_menu.Destroy() del self.popup_menu def OnSize(self, event): """ Called when the window is resized. We set a flag so the idle handler will resize the buffer. """ self.reInitBuffer = True def InitBuffer(self): """Initialize the bitmap used for buffering the display.""" size = self.GetClientSize() self.buffer = wx.EmptyBitmap(size.width, size.height) dc = wx.BufferedDC(None, self.buffer) # dc.SetBackground(wx.Brush(self.GetBackgroundColour())) dc.SetBackground(wx.Brush(wx.Colour(96, 96, 255))) dc.SetTextForeground(wx.NamedColour("White")) dc.Clear() self.RectWidth = (size.width - (self.rows * 2) - 6) / self.rows self.RectHeight = (size.height - (self.rows * 2) - 6) / self.rows rect_y_pos = 0; for i in range(self.rows): if i and i % 3 == 0: rect_y_pos += 3 rect_x_pos = 0 for j in range(self.rows): if j and j % 3 == 0: rect_x_pos += 3 self.rects[i * self.rows + j].Set(rect_x_pos, rect_y_pos, self.RectWidth, self.RectHeight) rect_x_pos += (self.RectWidth + 2) rect_y_pos += (self.RectHeight + 2) self.TextWidth, self.TextHeight = dc.GetTextExtent("X") self.Draw(dc) self.reInitBuffer = False def Draw(self, dc): dc.BeginDrawing() i = 0 dc.SetTextForeground(wx.NamedColour("White")) text_x_offset = (self.rects[0].GetWidth() - self.TextWidth) / 2 text_y_offset = (self.rects[0].GetHeight() - self.TextHeight) / 2 for r in self.rects: cell_index = self.board._cells[i].count() pen = wx.Pen(colors[cell_index], 1, wx.SOLID) brush = wx.Brush(colors[cell_index], wx.SOLID) if self.selected_cell == i: if cell_index == 1 and self.analyse is True: brush = wx.Brush(colors[11], wx.SOLID) else: brush = wx.Brush(colors[10], wx.SOLID) dc.SetPen(pen) dc.SetBrush(brush) dc.DrawRectangle(r.GetLeft(), r.GetTop(), self.RectWidth, self.RectHeight) if cell_index == 1: dc.DrawText(self.board.get_cell(i), r.GetLeft() + text_x_offset, r.GetTop() + text_y_offset) i += 1 dc.EndDrawing() def OnLeftDown(self, e): """called when the left mouse button is pressed""" # self.CaptureMouse() self.mouse_x, self.mouse_y = e.GetPositionTuple() foundRect = False i = 0 p = wx.Point(self.mouse_x, self.mouse_y) while foundRect is False and i < self.rows * self.rows: if self.rects[i].Inside(p): foundRect = True self.selected_cell = i i += 1 if foundRect: self.DrawNow() def OnLeftUp(self, e): """called when the left mouse button is released""" if self.HasCapture(): mouse_x, mouse_y = e.GetPositionTuple() self.ReleaseMouse() def OnMotion(self, e): """ Called when the mouse is in motion. """ if e.Dragging() and e.LeftIsDown(): pass ## dc = wx.BufferedDC(wxClientDC(self), self.buffer) ## dc.BeginDrawing() ## dc.SetPen(self.pen) ## pos = e.GetPositionTuple() ## coords = (self.x, self.y) + pos ## self.curLine.append(coords) ## dc.DrawLine(self.x, self.y, pos[0], pos[1]) ## self.x, self.y = pos ## dc.EndDrawing() # From wx-2.6-msw-ansi\wx\py\frame.py def OnUpdateMenu(self, e): """Update menu items based on current status and context.""" id = e.GetId() e.Enable(True) if id == wx.ID_UNDO: e.Enable(self.CanUndo()) elif id == wx.ID_REDO: e.Enable(self.CanRedo()) elif id == wx.ID_PASTE: e.Enable(wx.TheClipboard.IsSupported(wx.DataFormat(wx.DF_TEXT))) elif id == ID_ANALYSE: e.Check(self.analyse) def CanUndo(self): return self.undo_pos > 0 def CanRedo(self): return len(self.undo_stack) > 1 and self.undo_pos < len(self.undo_stack) - 1 def OnAbout(self, e): d=wx.MessageDialog(self, "Sudoku\n\nBy David Blume\nhttp://www.dlma.com\n\n(c) 2005", "About Sudoku", wx.OK) # d = SudokuAbout(self) d.ShowModal() d.Destroy() def OnNew(self, e): self.board.reset() self.undo_stack = [] self.undo_stack.append(tuple([self.board.get("", ""), self.selected_cell])) self.undo_pos = 0 self.SetTitle(self.title) self.filename = "" self.DrawNow() def OnSave(self, e): if not self.filename: self.OnSaveAs(e) else: self.SaveFile() def OnSaveAs(self, e): """ Save a file""" dlg = wx.FileDialog(self, "Save a sudoku file...", self.dirname, self.filename, self.wildcard, style=wx.SAVE | wx.OVERWRITE_PROMPT) if dlg.ShowModal() == wx.ID_OK: self.filename = dlg.GetFilename() self.dirname = dlg.GetDirectory() if not os.path.splitext(self.filename)[1]: self.filename = self.filename + extension self.SaveFile() self.SetTitle(self.title + ' -- ' + self.filename) dlg.Destroy() def SaveFile(self): f = open(os.path.join(self.dirname, self.filename), 'w') f.write(str(self.board)) f.close() def OnUndo(self, e): self.undo_pos -= 1 l, self.selected_cell = self.undo_stack[self.undo_pos] self.board.reset() self.board.set_board(l) self.DrawNow() def OnRedo(self, e): self.undo_pos += 1 l, self.selected_cell = self.undo_stack[self.undo_pos] self.board.reset() self.board.set_board(l) self.DrawNow() def OnExit(self, e): self.Close(True) def OnOpen(self, e): """ Open a file""" dlg = wx.FileDialog(self, "Open a sudoku file...", self.dirname, "", self.wildcard, wx.OPEN) if dlg.ShowModal() == wx.ID_OK: self.filename = dlg.GetFilename() # dlg.GetPath() self.dirname = dlg.GetDirectory() f = open(os.path.join(self.dirname, self.filename), 'r') values = f.read().replace(" ", "").replace("\n", "") self.undo_stack = [] self.undo_pos = -1 self.set_board(values) f.close() self.SetTitle(self.title + ' -- ' + self.filename) self.DrawNow() try: self.board.check() except Exception, detail: d = wx.MessageDialog(self, detail, "Bad Board", wx.OK) d.ShowModal() d.Destroy() dlg.Destroy() def MakeMenu(self, cell=None): """Make a menu that can be popped up later""" self.popup_menu = wx.Menu() keys = self.menuItems.keys() keys.sort() if cell: for n in cell._values: k = n + keys[0] - 1 self.popup_menu.Append(k, self.menuItems[k], kind=wx.ITEM_NORMAL) return keys[0], keys[-1] class SudokuAbout(wx.Dialog): """ An about box that uses an HTML window """ text = '''

Sudoku

Sudoku is brought to you by David Blume

''' def __init__(self, parent): wx.Dialog.__init__(self, parent, -1, 'About Sudoku', size=(420, 380)) html = wx.html.HtmlWindow(self, -1) html.SetPage(self.text) button = wx.Button(self, wx.ID_OK, "Okay") # constraints for the html window lc = wx.LayoutConstraints() lc.top.SameAs(self, wx.Top, 5) lc.left.SameAs(self, wx.Left, 5) lc.bottom.SameAs(button, wx.Top, 5) lc.right.SameAs(self, wx.Right, 5) html.SetConstraints(lc) # constraints for the button lc = wx.LayoutConstraints() lc.bottom.SameAs(self, wx.Bottom, 5) lc.centreX.SameAs(self, wx.CentreX) lc.width.AsIs() lc.height.AsIs() button.SetConstraints(lc) self.SetAutoLayout(True) self.Layout() self.CentreOnParent(wx.BOTH) #BEGIN_EVENT_TABLE(MainWindow, wx.Frame) if __name__ == '__main__': app = wx.PySimpleApp() frame = MainWindow(None, -1, "Sudoku") app.MainLoop() app = None # This is required for PythonWin 2.4 (Otherwise, only one launch will work.)