File: android-tkinter/CODE/tictactoe_lists.py (original) (raw)
"""
[SA] Sep-2017: Standalone release of PyCalc, PyClock, PyPhoto, PyToe. Copyright 2017 M.Lutz, from book "Programming Python, 4th Edition". License: provided freely, but with no warranties of any kind.
#-----------------------------------------------------------------------
ANDROID VERSION, fall 2020 (see "# ANDROID" for changes)
Use time.perf_counter on Pythons that no longer have time.clock().
This includes the bundled Python 3.8 in the Pydroid 3 Android app.
#-----------------------------------------------------------------------
New in 2.0: configs scheme, Mac OS port, search tweaks, code typo fixes, demo mode that displays N-across boards via preset configs, customize quits for Tk (end session) and Toplevel (close window). Search on "[SA]" here and in tictactoe.py to find changes applied.
[PP4E] this file has been updated for Python 3.X at least enough to run--I'd probably change more given time and need
"""
import random, sys, time from tkinter import * from tkinter.messagebox import showinfo, askyesno from PP4E.Gui.Tools.guimaker import GuiMakerWindowMenu
ANDROID (and all Python 3.8+: after > 25 years, time.clock is no more...)
timecall = time.clock if hasattr(time, 'clock') else time.perf_counter
RunningOnMac = sys.platform.startswith('darwin') # [SA] Mac port
User, Machine = 'user', 'machine' # players X, O, Empty = 'X', 'O', ' ' # board cell states
[SA] defunct defaults - see tictactoe.py for new scheme
""" Fontsz = 50 # defaults if no constructor args Degree = 3 # default=3 rows/cols=tic-tac-toe Mode = 'Expert2' # default machine move strategy """
def helpdisplay(parent): """ help via dialog or scrolled text, used both here and in tictactoe.py; called for DemoMode label, '?' keypress, and menus on all platforms; """ from helpmessage import showhelp from windowicons import trySetWindowIcon # win/lin only
showhelp(parent, 'PyToe', HelpText, forcetext=False,
setwinicon=lambda win:
trySetWindowIcon(win, 'icons', 'pygadgets'))
#parent.focus_force() # now done in helpmessage
HelpText = """PyToe 2.0
A Python/tkinter tic-tac-toe game. For Mac OS, Windows, and Linux. From the book Programming Python. Author and © M. Lutz 1999-2017.
Click in the board's empty cells to move. The first player to claim all the cells in a row, column, or diagonal wins the game.
Command-line arguments, also available and documented as settings in the file PyGadgets_configs.py, can customize:
▶ Number rows/columns ▶ Machine skill level ▶ Board colors ▶ Font and initial size ▶ First-move player and mark
Set DemoMode=True for preset game boards, or configure one of your own.
Version history: ● 2.0: Sep 2017, standalone release ● 1.1: Apr 2010, Python 3.X port, PP4E ● 1.0: Jul 1999, developed for PP2E
For downloads and more apps, visit: http://learning-python.com/programs.html"""
Debug = True trace = print def traceif(*args): if Debug: trace(*args)
def pp(board): # display nicely if Debug: rows = (('\n\t' + str(row)) for row in board) # 3.x: was map/lambda in prior return ''.join(rows)
class Record: def init(self): self.win = self.loss = self.draw = 0
###############################################################################
Main class: manage GUI and gameplay, delegate move choice to subclasses.
###############################################################################
class TicTacToeBase(GuiMakerWindowMenu): # a kind of Frame appname = 'PyToe' # [SA] new guimaker
def __init__(self, parent=None, # with a menu bar
FgColor='black', BgColor='white', Font=None,
GoesFirst=User, UserMark=X,
Degree=3,
**ignoreothers):
self.nextMove = GoesFirst
self.userMark = UserMark
self.machineMark = (UserMark==X and O) or X # or if/else expr
self.degree = Degree
self.record = Record()
self.makeWidgets = lambda: self.drawBoard(FgColor, BgColor, Font)
GuiMakerWindowMenu.__init__(self, parent=parent)
self.master.title('PyToe 2.0')
if GoesFirst == Machine: self.machineMove() # else wait for click
def start(self):
# [SA] quit=master.destroy: Tk ends, Toplevel closes
self.helpButton = None
self.toolBar = None
self.menuBar = [ ('Board', 0,
[('Stats', 0, self.onStats, '*-s'),
('Quit', 0, self.master.destroy, '?-q')]) ]
# [SA] Mac OS help now automatic in guimaker
if not RunningOnMac:
self.menuBar.extend(
[ ('Help', 0,
[('About', 0, self.onAbout, '*-h')]) ])
def accBindWidget(self):
# [SA] required for new guimaker if accelerators
return self.master
def drawBoard(self, fg, bg, font):
self.coord = {}
self.label = {}
self.board = []
for i in range(self.degree):
self.board.append([0] * self.degree)
frm = Frame(self)
frm.pack(expand=YES, fill=BOTH)
for j in range(self.degree):
widget = Label(frm, fg=fg, bg=bg,
text=' ', font=font,
relief=SUNKEN, bd=4, padx=10, pady=10)
widget.pack(side=LEFT, expand=YES, fill=BOTH)
widget.bind('<Button-1>', self.onLeftClick)
self.coord[widget] = (i, j)
self.label[(i, j)] = widget
self.board[i][j] = Empty
def onLeftClick(self, event):
label = event.widget
row, col = self.coord[label]
if self.nextMove == User and self.board[row][col] == Empty:
label.config(text=self.userMark)
self.board[row][col] = self.userMark
self.nextMove = Machine
if not self.checkFinish(): # [SA] unless user quit
self.machineMove()
def machineMove(self):
row, col = self.pickMove()
self.board[row][col] = self.machineMark
label = self.label[(row, col)]
label.config(text=self.machineMark)
self.checkFinish()
self.nextMove = User # wait for next left click or quit
def clearBoard(self):
for row, col in self.label.keys():
self.label[(row, col)].config(text=' ')
self.board[row][col] = Empty
#
# end test
#
def checkDraw(self, board=None):
board = board or self.board
for row in board:
if Empty in row:
return 0 # 3.x: True/False better
return 1 # none empty = draw or win
def checkWin(self, mark, board=None):
board = board or self.board
for row in board:
if row.count(mark) == self.degree: # check across
return 1 # row=all mark?
for col in range(self.degree):
for row in board: # check down
if row[col] != mark: # break to next col
break
else:
return 1
for row in range(self.degree): # check diag1
col = row # row == col
if board[row][col] != mark: break
else:
return 1
for row in range(self.degree): # check diag2
col = (self.degree-1) - row # row+col = degree-1
if board[row][col] != mark: break
else:
return 1
def checkFinish(self):
# [SA] Mac OS requires update() to display final move
self.update()
outcome = None
if self.checkWin(self.userMark):
outcome = "You've won!"
self.record.win += 1 # 3.x: changed to use += globally
elif self.checkWin(self.machineMark): # for both style and performance
outcome = 'I win again :-)'
self.record.loss += 1
elif self.checkDraw():
outcome = 'Looks like a draw'
self.record.draw += 1
if outcome:
result = 'Game Over: ' + outcome
if not askyesno('PyToe', result + '\n\nPlay another game?'):
self.onStats()
self.master.destroy() # [SA] Tk (end) or Toplevel (close)
return True # [SA] end caller (was sys.exit())
else:
self.focus_force() # [SA] for Mac OS, Tk 8.5
self.clearBoard() # return and make move or wait for click
# player who moved last moves second next
return False # resume caller
#
# miscellaneous
#
def onAbout(self):
helpdisplay(self)
onHelp = onAbout # [SA] for Mac OS, new guimaker
def onStats(self):
showinfo('PyToe Stats',
'Your results:\n'
'wins: %(win)d, losses: %(loss)d, draws: %(draw)d'
% self.record.__dict__)
self.focus_force() # [SA] for Mac OS, Tk 8.5
###############################################################################
Subclass to customize move selection per multiple schemes.
###############################################################################
#------------------------------------------------------------------------------
Pick empty slot at random
#------------------------------------------------------------------------------
class TicTacToeRandom(TicTacToeBase): def pickMove(self): empties = [] for row in range(self.degree): # 3.x: could be a comprehension for col in range(self.degree): # [SA] added range() to fix if self.board[row][col] == Empty: empties.append((row, col)) return random.choice(empties)
#------------------------------------------------------------------------------
Pick imminent win or loss, else static score
#------------------------------------------------------------------------------
class TicTacToeSmart(TicTacToeBase): def pickMove(self): self.update(); time.sleep(1) # too fast! countMarks = self.countAcrossDown(), self.countDiagonal() for row in range(self.degree): for col in range(self.degree): move = (row, col) if self.board[row][col] == Empty: if self.isWin(move, countMarks): return move for row in range(self.degree): for col in range(self.degree): move = (row, col) if self.board[row][col] == Empty: if self.isBlock(move, countMarks): return move best = 0 for row in range(self.degree): for col in range(self.degree): move = (row, col) if self.board[row][col] == Empty: score = self.scoreMove(move, countMarks) if score >= best: pick = move best = score trace('Picked', pick, 'score', best) return pick
def countAcrossDown(self):
countRows = {} # sparse data structure
countCols = {} # zero counts aren't added
for row in range(self.degree):
for col in range(self.degree):
mark = self.board[row][col]
try:
countRows[(row, mark)] += 1
except KeyError:
countRows[(row, mark)] = 1
try:
countCols[(col, mark)] += 1
except KeyError:
countCols[(col, mark)] = 1
return countRows, countCols
def countDiagonal(self):
tally = {'X':0, 'O':0, ' ':0}
countDiag1 = tally.copy()
for row in range(self.degree):
col = row
mark = self.board[row][col]
countDiag1[mark] += 1 # 3.x: use += 1, globally
countDiag2 = tally.copy()
for row in range(self.degree):
col = (self.degree-1) - row
mark = self.board[row][col]
countDiag2[mark] += 1
return countDiag1, countDiag2
def isWin(self, T, countMarks): # 3.X drops tuple matching in arg lists
(row, col) = T
self.board[row][col] = self.machineMark
isWin = self.checkWin(self.machineMark)
self.board[row][col] = Empty
return isWin
def isBlock(self, T, countMarks):
(row, col) = T
self.board[row][col] = self.userMark
isLoss = self.checkWin(self.userMark)
self.board[row][col] = Empty
return isLoss
def scoreMove(self, T1, T2):
(row, col) = T1
((countRows, countCols), (countDiag1, countDiag2)) = T2 # 3.x: no arg tuples
return (
countCols.get((col, self.machineMark), 0) * 11 +
countRows.get((row, self.machineMark), 0) * 11 +
countDiag1[self.machineMark] * 11 +
countDiag2[self.machineMark] * 11 # [SA] not Diag1
+ # see PP4E errata
countCols.get((col, self.userMark), 0) * 10 +
countRows.get((row, self.userMark), 0) * 10 +
countDiag1[self.userMark] * 10 +
countDiag2[self.userMark] * 10 # [SA] ditto
+
countCols.get((col, Empty), 0) * 11 +
countRows.get((row, Empty), 0) * 11 +
countDiag1[Empty] * 11 +
countDiag2[Empty] * 11) # [SA] ditto
#------------------------------------------------------------------------------
Static score based on 1 or 2 move lookahead
#------------------------------------------------------------------------------
class TicTacToeExpert1(TicTacToeSmart): def pickMove(self): self.update(); time.sleep(1) countMarks = self.countAcrossDown(), self.countDiagonal() best = 0 for row in range(self.degree): for col in range(self.degree): move = (row, col) if self.board[row][col] == Empty: score = self.scoreMove(move, countMarks) if score > best: pick = move best = score trace('Picked', pick, 'score', best) return pick
def countAcrossDown(self):
tally = {'X':0, 'O':0, ' ':0} # uniform with diagonals
countRows = [] # no entries missing
countCols = [] # tally * degree fails
for row in range(self.degree):
countRows.append(tally.copy())
countCols.append(tally.copy())
for row in range(self.degree):
for col in range(self.degree):
mark = self.board[row][col]
countRows[row][mark] += 1 # 3.x: += 1
countCols[col][mark] += 1
return countRows, countCols
def scoreMove(self, T1, T2): # 3.x: no arg tuples
(row, col) = T1
((countRows, countCols), (countDiag1, countDiag2)) = T2
score = 0
mine = self.machineMark
user = self.userMark
# for empty slot (r,c):
partof = [countRows[row], countCols[col]] # check move row and col
if row == col: # plus diagonals, if any
partof.append(countDiag1)
if row+col == self.degree-1:
partof.append(countDiag2)
for line in partof:
if line[mine] == self.degree-1 and line[Empty] == 1:
score += 51 # 1 move to win
for line in partof:
if line[user] == self.degree-1 and line[Empty] == 1:
score += 25 # 1 move to loss
for line in partof:
if line[mine] == self.degree-2 and line[Empty] == 2:
score += 10 # 2 moves to win
for line in partof:
if line[user] == self.degree-2 and line[Empty] == 2:
score += 8 # 2 moves to loss
for line in partof:
if line[Empty] == self.degree: # prefer openness
score += 1
if score:
return score # detected pattern here?
else: # else use weighted scoring
for line in partof:
score += line[mine] * 3 + line[user] + line[Empty] * 2
return score / float(self.degree) # 3.x: float not really needed for /
#------------------------------------------------------------------------------
Static score based on win or loss N moves ahead
#------------------------------------------------------------------------------
class TicTacToeExpert2(TicTacToeExpert1):
def scoreMove(self, T1, T2): # 3.x: no arg tuples
(row, col) = T1
((countRows, countCols), (countDiag1, countDiag2)) = T2
score = 0
mine = self.machineMark
user = self.userMark
# for empty slot (r,c):
partof = [countRows[row], countCols[col]] # check move row and col
if row == col: # plus diagonals, if any
partof.append(countDiag1)
if row+col == self.degree-1:
partof.append(countDiag2)
weight = 3 ** (self.degree * 2) # 3.x: not 3L, int does long
for ahead in range(1, self.degree):
for line in partof:
if line[mine] == self.degree - ahead and line[Empty] == ahead:
score += weight
if line[user] == self.degree - ahead and line[Empty] == ahead:
score += weight // 3
weight = weight // 9 # 3.x: need // for int div
if score:
return score # detected pattern here?
else: # else use weighted scoring
for line in partof:
score += line[mine] * 3 + line[user] + line[Empty] * 2
return score / float(self.degree) # 3.x: float() not really needed
#------------------------------------------------------------------------------
Search ahead through moves and countermoves
#------------------------------------------------------------------------------
class TicTacToeMinimax(TicTacToeExpert2):
def pickMove(self):
self.update()
numMarks = self.degree ** 2
for row in self.board:
numMarks -= row.count(Empty)
if numMarks == 0:
return (self.degree // 2, self.degree // 2) # 3.x: // for int div
else:
#traceif('\n\nPick move...')
t1 = timecall() # ANDROID: was time.clock()
if self.degree <= 3:
maxdepth = numMarks + 4 # [SA] original 3x3 scheme
else:
maxdepth = 4 # [SA] else impossibly slow
#traceif(numMarks, maxdepth)
score, pick = self.findMax(self.board, maxdepth)
trace('Time to move:', timecall() - t1) # ANDROID: was time.clock()
if score == -1:
# lookahead can be too pessimistic
# if best is a loss, use static score
pick = TicTacToeExpert2.pickMove(self)
return pick
def checkLeaf(self, board):
if self.checkWin(self.machineMark, board): # score from machine's view
return +1 # a win is good; a loss bad
elif self.checkWin(self.userMark, board):
return -1
elif self.checkDraw(board):
return 0
else:
return None
def findMax(self, board, depth): # machine move level: find best case
#traceif('max start', depth, pp(board))
if depth == 0: # find start of best move sequence
return 0, None # could return static score here???
else:
term = self.checkLeaf(board)
if term != None: # depth cutoff
#traceif('max term', term, pp(board))
return term, None # or endgame detected
else: # or check countermoves
best = -2
for row in range(self.degree):
for col in range(self.degree):
if board[row][col] == Empty:
board[row][col] = self.machineMark
below, m = self.findMin(board, depth-1)
board[row][col] = Empty
if below >= best:
best = below
pick = (row, col)
#traceif('max best at', depth, best, pick)
return best, pick
def findMin(self, board, depth): # user move level: find worst case
#traceif('min start', depth, pp(board))
if depth == 0: # assume she will do her best
return 0, None
else:
term = self.checkLeaf(board)
if term != None: # depth cutoff
#traceif('min term', term, pp(board))
return term, None # or endgame detected
else: # or check countermoves
best = +2
for row in range(self.degree):
for col in range(self.degree):
if board[row][col] == Empty:
board[row][col] = self.userMark
below, m = self.findMax(board, depth-1)
board[row][col] = Empty
if below < best:
best = below
pick = (row, col)
#traceif('min best at', depth, best, pick)
return best, pick
###############################################################################
Moved to tictactoe.py: game object generator, command-line logic.
###############################################################################