import datetime import random import dabo.db import dabo.biz import dabo.ui import dabo.lib.StopWatch as StopWatch dabo.ui.loadUI("wx") from dabo.dLocalize import _ import dabo.dEvents as dEvents import dabo.lib.datanav as datanav from dabo.common import specParser # Dabo MineSweeper # This is a demo of Dabo's UI - no bizobj or database layer. # # Begun 10/22/2004 while I was waiting for a program to compile. # I wanted to see how useful dEditor was, so I developed minesweeper # completely with dEditor. It isn't finished, but is playable. The # code needs to be cleaned/refactored in places. But it does use the # Dabo UI exclusively. # # My idea is to make a network-playable "battle minesweeper" where 2 or # more users can play the same puzzle and see who can finish first. Each # user's form would show how many mines their opponent has cleared. # # Another idea is to make a network "team minesweeper" where 2 or more # players can collaborate on clearing the same minefield. Each user's # minefield would get updated as other players flag mines and clear # squares. # # Perhaps the high-scores can be recorded using a database/bizobj just # to stay relevant with the Dabo project. # # Anyway, this is also a good test for Dabo's performance, as each square # is a dButton: make a 20x20 grid and all of a sudden you are # instantiating 400 dButtons, each responding to mouse clicks. # # (if someone really wants to speed it up, I'd suggest using a DC to draw # directly on the panel, and divining the current square based on the # position of the mouse click). # # pkm # Good games: 20x15, 36 _defaultBoardSize = (12,8) _defaultMineCount = 15 _mineMarkChar = "@" _squareBorder = 0 _timerInterval = 500 _mineMarkerColor = "orange" _questionColor = "yellow" _hintColor = "black" class StateChanged(dabo.dEvents.Event): pass class Square(dabo.ui.dPanel): def initProperties(self): self.BackColor = "grey" def initEvents(self): self.bindEvent(dEvents.Paint, self.onPaint) def cycleState(self): if self.State == "UnMarked": self.State = "MarkedMine" elif self.State == "MarkedMine": self.State = "QuestionMarked" elif self.State == "QuestionMarked": self.State = "UnMarked" else: #self.State = "Cleared" pass def onPaint(self, evt): import wx ## (need to abstract DC drawing) dc = wx.PaintDC(self) rect = self.GetClientRect() try: self.FontSize = self.Parent._squareFontSize except: # not saved yet: no problem, this is just an optimization pass while True: fm = dabo.ui.fontMetric("9", self) xdiff = self.Width - fm[0] ydiff = self.Height - fm[1] tolerance = 7 if xdiff < 0 or ydiff < 0: self.FontSize -= 1 elif xdiff < tolerance or ydiff < tolerance: break else: self.FontSize += 1 self.Parent._squareFontSize = self.FontSize if self.Caption == _mineMarkChar: self.ForeColor = _mineMarkerColor elif self.Caption == "?": self.ForeColor = _questionColor else: self.ForeColor = _hintColor font = self.Font dc.SetTextForeground(self.ForeColor) dc.SetFont(font) dc.DrawLabel(self.Caption, (rect[0]+2, rect[1], rect[2]-4, rect[3]), wx.ALIGN_CENTER) def _getState(self): """Returns one of: "UnMarked", "MarkedMine", "QuestionMarked", "Cleared" """ try: state = self._state except AttributeError: state = self._state = "UnMarked" return state def _setState(self, val): """Sets State to one of: + "UnMarked" + "MarkedMine" + "QuestionMarked" + "Cleared" Side Effects: + Caption set to one of "", , "?" + Raises StateChanged event """ self._state = val self.Caption = {"UnMarked": "", "MarkedMine": _mineMarkChar, "QuestionMarked": "?", "Cleared": ""}[val] self.raiseEvent(StateChanged) def _getCaption(self): try: v = self._caption except AttributeError: v = self._caption = "" return v def _setCaption(self, val): self._caption = val self.refresh() Caption = property(_getCaption, _setCaption) State = property(_getState, _setState, None, "") class Board(dabo.ui.dPanel): def initProperties(self): self.BackColor = "green" def newGame(self): sw = StopWatch.StopWatch() sw.start() self._makeBoardDict() self._fillBoard() self.layout() sw.stop() print "Board created in %f second(s)." % (sw.Value,) self._GameInProgress = True self._MinesRemaining = self.MineCount def onHit(self, evt): o = evt.EventObject if o.Caption not in ("@", "?"): # if the user has marked the square, don't detonate: it could have # been a mistake. self.showSquare(o.square) self.checkWin() def checkWin(self): """See if the player has won the game. """ if self._GameInProgress: self.StopWatch.start() check = True if self.allCleared(): for sq in self._boardDict.keys(): square = self._boardDict[sq] if square["mine"]: if square["obj"].Caption == "@": pass else: check = False break if check: self._GameInProgress = False self.Form.setStatusText("You win!!! Picture fireworks bursting in air, in your honor!") # dabo.ui.info("You win!") def allCleared(self): """Return True if all non-mine squares have been cleared. """ bd = self._boardDict for sq in bd.keys(): square = bd[sq] if not square["mine"]: if ((square["adjacent"] == 0 and square["obj"].Visible == False) or square["adjacent"] > 0 and square["obj"].Enabled == False): pass else: return False return True def onContextMenu(self, evt): # stop the event from propagating, so that the click isn't processed (Mac): evt.stop() o = evt.EventObject if o.Enabled: o.cycleState() def showSquare(self, square): i = self._boardDict[square] o = i["obj"] if i["mine"]: o.FontBold=True o.FontItalic=True self.showAllSquares() self._GameInProgress = False # dabo.ui.stop("You lose!") self.Form.setStatusText("KaBoom!") else: a = i["adjacent"] if a == 0: # recursively clear all adjacent 0 squares self.clearZeros(o.square) else: o.Caption = str(a) o.Enabled = False o.unBindEvent(dabo.dEvents.Hit) def showAllSquares(self): bd = self._boardDict for sq in bd.keys(): o = bd[sq]["obj"] if bd[sq]["mine"]: o.Caption = "M" o.Enabled = False elif bd[sq]["adjacent"] == 0: o.Visible = False else: o.Caption = str(bd[sq]["adjacent"]) o.Enabled = False def clearZeros(self, square): bd = self._boardDict if bd[square]["adjacent"] == 0: bd[square]["obj"].Visible = False for sq in self.getAdjacentSquares(square): if bd[sq]["adjacent"] == 0 \ and bd[sq]["mine"] == False \ and bd[sq]["obj"].Visible: bd[sq]["obj"].Visible = False self.clearZeros(sq) else: if bd[sq]["obj"].Visible and bd[sq]["adjacent"] > 0: bd[sq]["obj"].Caption = str(bd[sq]["adjacent"]) bd[sq]["obj"].Enabled = False def _makeBoardDict(self): self._boardDict = {} width = self.BoardSize[0] height = self.BoardSize[1] for h in range(height): for w in range(width): self._boardDict[(w,h)] = {"mine": False, "flag": False, "adjacent": 0} self._fillMines() self._fillAdjacentCounts() def _fillMines(self): r = random.Random() r.seed() bc = self.MineCount squares = self._boardDict.keys() if bc > len(squares): bc = self.MineCount = len(squares) mines = random.sample(squares, bc) for mine in mines: self._boardDict[mine]["mine"] = True def _fillAdjacentCounts(self): for key in self._boardDict.keys(): as = self.getAdjacentSquares(key) c = 0 for s in as: if self._boardDict[s]["mine"]: c += 1 self._boardDict[key]["adjacent"] = c def _fillBoard(self): cols = self.BoardSize[0] rows = self.BoardSize[1] sizer = dabo.ui.dGridSizer() sw = StopWatch.StopWatch() sw.start() dabo.fastNameSet = True for row in range(rows): for col in range(cols): o = self.addObject(Square, "square_%s_%s" % (col, row)) o.square = (col, row) self._boardDict[(col, row)]["obj"] = o o.bindEvent(dabo.dEvents.MouseLeftClick, self.onHit) o.bindEvent(dabo.dEvents.ContextMenu, self.onContextMenu) o.bindEvent(StateChanged, self.onStateChanged) sizer.append(o, "expand", row=row, col=col, border=_squareBorder) dabo.fastNameSet = False sw.stop() print "\n\nTime creating squares:", sw.Value sw.reset() sw.start() sizer.setRowExpand(True, "all") sizer.setColExpand(True, "all") self.Sizer = sizer self.layout() sw.stop() print "Time sizing:", sw.Value def onStateChanged(self, evt): o = evt.EventObject if o.State == "MarkedMine": self._MinesRemaining -= 1 elif o.State == "QuestionMarked": # this is the next state after MarkedMine self._MinesRemaining += 1 self.checkWin() def getAdjacentSquares(self, square): row, col = square[1], square[0] adj = [] for i in (-1,0,1): for j in (-1,0,1): r = j+row c = i+col if r >= 0 and r < self.BoardSize[1] \ and c >= 0 and c < self.BoardSize[0] \ and (c,r) != square: adj.append((c,r)) return tuple(adj) def onTimer(self, evt): self.Form.pausebutton.Caption = "%s sec." % int(round(self.StopWatch.Value)) def _getBoardSize(self): try: bs = self._boardSize except AttributeError: bs = (self.Application.getUserSetting("minesweeper_boardwidth"), self.Application.getUserSetting("minesweeper_boardheight")) if bs[0] is None or bs[1] is None: bs = _defaultBoardSize self.BoardSize = tuple(bs) return bs def _setBoardSize(self, size): assert type(size) in (list, tuple) assert len(size) == 2 assert (type(size[0]), type(size[1])) == (int, int) self._boardSize = size self.Application.setUserSetting("minesweeper_boardwidth", size[0]) self.Application.setUserSetting("minesweeper_boardheight", size[1]) def _getMineCount(self): try: bc = self._mineCount except AttributeError: bc = self.Application.getUserSetting("minesweeper_minecount") if bc is None: bc = _defaultMineCount self.MineCount = int(bc) return bc def _setMineCount(self, count): assert type(count) == int self._mineCount = count self.Application.setUserSetting("minesweeper_minecount", count) def _getMinesRemaining(self): try: v = self._minesRemaining except AttributeError: v = self._minesRemaining = None return v def _setMinesRemaining(self, val): self._minesRemaining = val self.Form.tbMines.Value = val def _getGameInProgress(self): try: v = self._gameInProgress except AttributeError: v = self._gameInProgress = False return v def _setGameInProgress(self, val): if val: self.StopWatch.reset() #- self.StopWatch.start() ## no, do this on the first square clicked self.Timer.start() else: self.StopWatch.stop() self.Timer.stop() print "Game time: %f seconds." % self.StopWatch.Value self._gameInProgress = val self.Form.pausebutton.Enabled = val def _getStopWatch(self): try: v = self._stopWatch except AttributeError: v = self._stopWatch = StopWatch.StopWatch() return v def _getTimer(self): try: v = self._timer except AttributeError: v = self._timer = dabo.ui.dTimer(self) v.Interval = _timerInterval v.bindEvent(dEvents.Hit, self.onTimer) return v BoardSize = property(_getBoardSize, _setBoardSize, None, "Sets the dimension of the board. (w,h)") MineCount = property(_getMineCount, _setMineCount, None, "Sets the number of mines on the board.") StopWatch = property(_getStopWatch) Timer = property(_getTimer) _MinesRemaining = property(_getMinesRemaining, _setMinesRemaining) _GameInProgress = property(_getGameInProgress, _setGameInProgress) class Form(dabo.ui.dForm): def afterInit(self): self.addObject(Board, Name="board") self.Sizer.append(self.board, "expand", 1) self.fillMenu() dabo.ui.callAfter(self.newGame) def initProperties(self): self.Caption = "Dabo MineSweeper" self.Sizer = dabo.ui.dSizer("vertical") self._autopause = False def initEvents(self): self.bindEvent(dEvents.Deactivate, self.onDeactivate) self.bindEvent(dEvents.Activate, self.onActivate) def onDeactivate(self, evt): ## pause the game automatically when form loses focus if not self.pausebutton.Value and self.board._GameInProgress: self._autopause = True self.pausebutton.Value = True self.pausebutton.raiseEvent(dEvents.Hit) def onActivate(self, evt): ## if game was automatically paused upon deactivate, unpause it now if self._autopause and self.board._GameInProgress: self.pausebutton.Value = False self.pausebutton.raiseEvent(dEvents.Hit) self._autopause = False def onEditPreferences(self, evt): dlg = PreferenceDialog(self) dlg.show() if dlg.accepted: ng = dabo.ui.areYouSure("Do you want to start a new game with your new settings?") if ng is not None: self.board.BoardSize = (dlg.boardWidth, dlg.boardHeight) self.board.MineCount = dlg.boardMines self.preset = dlg.preset if ng: self.newGame() dlg.release() def onPause(self, evt): if self.pausebutton.Value == True: self.board.Timer.stop() self.board.StopWatch.stop() props = {"Caption": "Resume", "FontBold": True, "ForeColor": "red"} else: self.board.StopWatch.start() self.board.Timer.start() props = {"Caption": "Pause", "FontBold": False, "ForeColor": "black"} self.pausebutton.setProperties(props) self.board.Visible = not self.pausebutton.Value self.tbMines.Visible = not self.pausebutton.Value def onNewGame(self, evt): self.newGame() def newGame(self): bs = self.board.BoardSize mc = self.board.MineCount self.board.release() self.addObject(Board, "board") self.Sizer.append(self.board, "expand", 1) self.board.BoardSize = bs self.board.MineCount = mc self.board.newGame() self.layout() def fillMenu(self): mb = self.MenuBar fileMenu = mb.getMenu(_("File")) fileMenu.prependSeparator() fileMenu.prepend(_("&New Game\tCtrl+N"), help=_("Start a new game"), bindfunc=self.onNewGame, bmp="new") tb = self.ToolBar = dabo.ui.dToolBar(self) tb.appendButton("New", pic="new", toggle=False, bindfunc=self.onNewGame, tip="New Game", help="Start a new game") tb.appendButton("Preferences", pic="configure", toggle=False, bindfunc=self.onEditPreferences, tip="Preferences", help="Edit preferences") tb.appendSeparator() tb.appendControl(dabo.ui.dLabel(tb, Caption="Mines:", FontSize=9)) self.tbMines = tb.appendControl(dabo.ui.dTextBox(tb, Width=30, ReadOnly=True)) tb.appendSeparator() self.pausebutton = tb.appendControl(dabo.ui.dToggleButton(tb, Caption="Pause", ToolTipText="Pause/Resume", StatusText="Pause/Resume the game"), bindfunc=self.onPause) class PreferenceDialog(dabo.ui.dOkCancelDialog): def initProperties(self): self.AutoSize = False self.Caption = "Minesweeper Preferences" self.SaveUserGeometry = True def afterInit(self): b = self.Parent.board self.boardWidth = b.BoardSize[0] self.boardHeight = b.BoardSize[1] self.boardMines = b.MineCount self.accepted = False self.Modal = True def onPickPreset(self, evt): """Called when the user pushes the butPickPreset button on the preset page.""" class Browse(datanav.Grid): pass class PickPreset(dabo.ui.dOkCancelDialog): def initProperties(self): self.AutoSize = False self.Caption = "Pick Game Preset" self.SaveUserGeometry = True self.Modal = True self.FormType = "PickList" self.accepted = False def pickRecord(self): self.accepted = True self.hide() def addControls(self): conn = dabo.db.dConnection(MinesweeperCI()) biz = self.biz = MinesweeperBO_gamedefs(conn) biz.requery() g = self.grid = self.addObject(Browse) g.bizobj = biz g.FieldSpecs = biz.getGameDefFieldSpecs() p = self.Parent p.Sizer.append1x(self) p.layout() self.Sizer.append1x(g) f = PickPreset(self) f.grid.populate() f.show() if f.accepted: biz = f.biz self.preset["Id"] = biz.id self.preset["Name"] = biz.name self.preset["Width"] = biz.width self.preset["Height"] = biz.height self.preset["Mines"] = biz.mines self.refreshPresets() def refreshPresets(self): p = self.PageFrame.Pages[0] for c in ("Name", "Width", "Height", "Mines"): o = p.__dict__["o%s" % c] o.Value = self.preset[c] def addControls(self): pgf = self.PageFrame = dabo.ui.dPageFrame(self) p1 = pgf.appendPage(caption="Choose From Presets") p2 = pgf.appendPage(caption="Set By Hand") class lbl(dabo.ui.dLabel): def initProperties(self): self.Alignment = "Right" self.AutoResize = False self.Width = 100 # p1: b = 5 vs = dabo.ui.dSizer("vertical") app = self.Application preset = self.preset = {} preset["Id"] = app.getUserSetting("preset_id") preset["Name"] = app.getUserSetting("preset_name", "") preset["Width"] = app.getUserSetting("preset_width", 0) preset["Height"] = app.getUserSetting("preset_height", 0) preset["Mines"] = app.getUserSetting("preset_mines", 0) if preset["Id"] is None: preset["Name"] = "< None >" hs = dabo.ui.dSizer("horizontal") cb = p1.addObject(dabo.ui.dButton, Name="butPickPreset", Caption="Preset:", ToolTipText="""Press this button to choose a preset from the public game definitions. Note that this will require an internet connection. """) t = p1.addObject(dabo.ui.dTextBox, "oName", Value=preset["Name"], ReadOnly=True) hs.append(cb, "fixed", alignment="right", border=b) hs.append(t, 1, border=b) vs.append(hs, "expand") cb.bindEvent(dEvents.Hit, self.onPickPreset) for name in ("Width", "Height", "Mines"): hs = dabo.ui.dSizer("horizontal") l = p1.addObject(lbl, Name="lbl%s" % name, Caption="%s:" % name) s = p1.addObject(dabo.ui.dSpinner, "o%s" % name, Value=preset[name], Enabled=False) hs.append(l, "fixed", alignment="right", border=b) hs.append(s, border=b) vs.append(hs) p1.Sizer = vs # p2: b = 5 vs = dabo.ui.dSizer("vertical") for name in ("Width", "Height", "Mines"): hs = dabo.ui.dSizer("horizontal") l = p2.addObject(lbl, Name="lbl%s" % name, Caption="%s:" % name) s = p2.addObject(dabo.ui.dSpinner, "o%s" % name, Value=eval("self.board%s" % name)) hs.append(l, "fixed", alignment="right", border=b) hs.append(s, border=b) vs.append(hs) p2.Sizer = vs self.Sizer.append1x(pgf) if preset["Id"] is None: pgf.SelectedPageNum = 1 def onOK(self, evt): self.accepted = True p = self.PageFrame.SelectedPage self.boardWidth = p.oWidth.Value self.boardHeight = p.oHeight.Value self.boardMines = p.oMines.Value self.Application.setUserSetting("preset_id", self.preset["Id"]) self.Application.setUserSetting("preset_name", self.preset["Name"]) self.Application.setUserSetting("preset_width", self.preset["Width"]) self.Application.setUserSetting("preset_height", self.preset["Height"]) self.Application.setUserSetting("preset_mines", self.preset["Mines"]) ##pkm: The following CI and BO's are for the future high-score and dynamic game # type editor. People will be able to define their favorite game settings, # and then save those settings to the public database so other players can # select that game from a list. It is these public games that will be able # to have high scores recorded. class MinesweeperCI(dabo.db.dConnectInfo): def initProperties(self): self.DbType = "MySQL" self.Host = "paulmcnett.com" self.Database = "dabotest" self.User = "dabo" self.Port = 3306 self.Password = "Y38Z11XA2Z5F" class MinesweeperBO_gamedefs(dabo.biz.dBizobj): def initProperties(self): self.Caption = "Minesweeper Game Definitions" self.DataSource = "minesweeper_gamedefs" self.KeyField = "id" self.defaultValues = {} def afterInit(self): self.setBaseSQL() def setBaseSQL(self): self.addFrom("minesweeper_gamedefs") self.setLimitClause("500") self.addField("minesweeper_gamedefs.id as id") self.addField("minesweeper_gamedefs.name as name") self.addField("minesweeper_gamedefs.width as width") self.addField("minesweeper_gamedefs.height as height") self.addField("minesweeper_gamedefs.mines as mines") self.addField("minesweeper_gamedefs.comments as comments") self.addField("minesweeper_gamedefs.submittedby as submittedby") def getGameDefFieldSpecs(self): """For simplicity, I put this into the bizobj.""" xml = """
""" return specParser.importFieldSpecs(xml, "minesweeper_gamedefs") class MinesweeperBO_scores(dabo.biz.dBizobj): def initProperties(self): self.Caption = "Minesweeper High Scores" self.DataSource = "minesweeper_scores" self.KeyField = "id" self.defaultValues = {"timestamp": datetime.datetime.utcnow} self.setBaseSQL() def setBaseSQL(self): self.addFrom("minesweeper_scores") self.setLimitClause("500") self.addField("minesweeper_scores.id as id") self.addField("minesweeper_scores.gamedefid as gamedefid") self.addField("minesweeper_scores.timestamp as timestamp") self.addField("minesweeper_scores.playername as playername") self.addField("minesweeper_scores.playercomments as playercomments") app = dabo.dApp() app.setAppInfo("appName", "Dabo Minesweeper") app.MainFormClass = Form app.setup() app.start()