前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >python tictactoe游戏

python tictactoe游戏

作者头像
用户5760343
发布2022-05-13 10:41:58
1.5K0
发布2022-05-13 10:41:58
举报
文章被收录于专栏:sktj

image.png

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 guimaker import GuiMakerWindowMenu

User, Machine = 'user', 'machine' # players X, O, Empty = 'X', 'O', ' ' # board cell states Fontsz = 50 # defaults if no constructor args Degree = 3 # default=3 rows/cols=tic-tac-toe Mode = 'Expert2' # default machine move strategy

Debug = True trace = print

def traceif(args): if Debug: trace(args)

def pp(board): if Debug: rows = (('\n\t' + str(row)) for row in board) # 3.x: was map/lambda in prior return ''.join(rows)

helptext = """PyToe 1.1 Programming Python 4E A Tic-tac-toe board game written in Python with tkinter\n Version 1.1: April 2010, Python 3.X port Version 1.0: July 1999, developed for 2E\n Click in cells to move. Command-line arguments:\n -degree N sets board size N=number rows/columns\n -mode M sets machine skill M=Minimax, Expert1|2,...\n -fg F, -bg B F,B=color name\n -fontsz N N=marks size\n -goesFirst user|machine -userMark X|O"""

class Record: def init(self): self.win = self.loss = self.draw = 0

class TicTacToeBase(GuiMakerWindowMenu): # a kind of Frame def init(self, parent=None, # with a menu bar fg='black', bg='white', fontsz=Fontsz, goesFirst=User, userMark=X, degree=Degree): 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(fg, bg, fontsz) # no defaults GuiMakerWindowMenu.init(self, parent=parent) self.master.title('PyToe 1.1') if goesFirst == Machine: self.machineMove() # else wait for click

代码语言:javascript
复制
def start(self):
    self.helpButton = None
    self.toolBar = None
    self.menuBar = [('File', 0, [('Stats', 0, self.onStats),
                                 ('Quit', 0, self.quit)]),
                    ('Help', 0, [('About', 0, self.onAbout)])]

def drawBoard(self, fg, bg, fontsz):
    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=('courier', fontsz, 'bold'),
                           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
        self.checkFinish()
        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):
    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.quit()
            sys.exit()  # don't return to caller
        else:
            self.clearBoard()  # return and make move or wait for click
            # player who moved last moves second next

#
# miscellaneous
#

def onAbout(self):
    showinfo('PyToe 1.0', helptext)

def onStats(self):
    showinfo('PyToe Stats',
             'Your results:\n'
             'wins: %(win)d,  losses: %(loss)d,  draws: %(draw)d'
             % self.record.__dict__)

######################################

subclass to customize move selection

######################################

pick empty slot at random

class TicTacToeRandom(TicTacToeBase): def pickMove(self): empties = [] for row in self.degree: # 3.x: could be a comprehension for col in self.degree: 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

代码语言:javascript
复制
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 +
            countDiag1[self.machineMark] * 11
            +
            countCols.get((col, self.userMark), 0) * 10 +
            countRows.get((row, self.userMark), 0) * 10 +
            countDiag1[self.userMark] * 10 +
            countDiag1[self.userMark] * 10
            +
            countCols.get((col, Empty), 0) * 11 +
            countRows.get((row, Empty), 0) * 11 +
            countDiag1[Empty] * 11 +
            countDiag1[Empty] * 11)

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

代码语言:javascript
复制
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)

代码语言:javascript
复制
    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: need // for int div else: # traceif('\n\nPick move...') t1 = time.clock() maxdepth = numMarks + 4 # traceif(maxdepth) score, pick = self.findMax(self.board, maxdepth) trace('Time to move:', time.clock() - t1) if score == -1: # lookahead can be too pessimistic # if best is a loss, use static score pick = TicTacToeExpert2.pickMove(self) return pick

代码语言:javascript
复制
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
    # 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 - external interface

command-line logic


this file has been updated for Python 3.X

from tictactoe_lists import *

game object generator - external interface

def TicTacToe(mode=Mode, args): try: classname = 'TicTacToe' + mode # e.g., -mode Minimax classobj = eval(classname) # get class by string name except: print('Bad -mode flag value:', mode) raise # reraise return eval(classname)(args) # run class constructor (3.x: was apply())

command-line logic

if name == 'main': if len(sys.argv) == 1: TicTacToe().mainloop() # default=3-across, expert2 else: # ex: TicTacToe.py -degree 5 -mode Smart -bg blue -fg white -fontsz 30 needEval = ['-degree'] args = sys.argv[1:] opts = {} for i in range(0, len(args), +2): if args[i] in needEval: opts[args[i][1:]] = eval(args[i+1]) else: opts[args[i][1:]] = args[i+1] # any constructor arg trace(opts) # on cmd line: '-name value' TicTacToe(**opts).mainloop() # 3.x: was apply

------------------------------------------------guimaker--------------------- """ ############################################################################### An extended Frame that makes window menus and toolbars automatically. Use GuiMakerFrameMenu for embedded components (makes frame-based menus). Use GuiMakerWindowMenu for top-level windows (makes Tk8.0 window menus). See the self-test code (and PyEdit) for an example layout tree format. ############################################################################### """

import sys from tkinter import * # widget classes from tkinter.messagebox import showinfo

class GuiMaker(Frame): menuBar = [] # class defaults toolBar = [] # change per instance in subclasses helpButton = True # set these in start() if need self

代码语言:javascript
复制
def __init__(self, parent=None):
    Frame.__init__(self, parent)
    self.pack(expand=YES, fill=BOTH)        # make frame stretchable
    self.start()                            # for subclass: set menu/toolBar
    self.makeMenuBar()                      # done here: build menu bar
    self.makeToolBar()                      # done here: build toolbar
    self.makeWidgets()                      # for subclass: add middle part

def makeMenuBar(self):
    """
    make menu bar at the top (Tk8.0 menus below)
    expand=no, fill=x so same width on resize
    """
    menubar = Frame(self, relief=RAISED, bd=2)
    menubar.pack(side=TOP, fill=X)

    for (name, key, items) in self.menuBar:
        mbutton  = Menubutton(menubar, text=name, underline=key)
        mbutton.pack(side=LEFT)
        pulldown = Menu(mbutton)
        self.addMenuItems(pulldown, items)
        mbutton.config(menu=pulldown)

    if self.helpButton:
        Button(menubar, text    = 'Help',
                        cursor  = 'gumby',
                        relief  = FLAT,
                        command = self.help).pack(side=RIGHT)

def addMenuItems(self, menu, items):
    for item in items:                     # scan nested items list
        if item == 'separator':            # string: add separator
            menu.add_separator({})
        elif type(item) == list:           # list: disabled item list
            for num in item:
                menu.entryconfig(num, state=DISABLED)
        elif type(item[2]) != list:
            menu.add_command(label     = item[0],         # command:
                             underline = item[1],         # add command
                             command   = item[2])         # cmd=callable
        else:
            pullover = Menu(menu)
            self.addMenuItems(pullover, item[2])          # sublist:
            menu.add_cascade(label     = item[0],         # make submenu
                             underline = item[1],         # add cascade
                             menu      = pullover)

def makeToolBar(self):
    """
    make button bar at bottom, if any
    expand=no, fill=x so same width on resize
    this could support images too: see Chapter 9,
    would need prebuilt gifs or PIL for thumbnails
    """
    if self.toolBar:
        toolbar = Frame(self, cursor='hand2', relief=SUNKEN, bd=2)
        toolbar.pack(side=BOTTOM, fill=X)
        for (name, action, where) in self.toolBar:
            Button(toolbar, text=name, command=action).pack(where)

def makeWidgets(self):
    """
    make 'middle' part last, so menu/toolbar
    is always on top/bottom and clipped last;
    override this default, pack middle any side;
    for grid: grid middle part in a packed frame
    """
    name = Label(self,
                 width=40, height=10,
                 relief=SUNKEN, bg='white',
                 text   = self.__class__.__name__,
                 cursor = 'crosshair')
    name.pack(expand=YES, fill=BOTH, side=TOP)

def help(self):
    "override me in subclass"
    showinfo('Help', 'Sorry, no help for ' + self.__class__.__name__)

def start(self):
    "override me in subclass: set menu/toolbar with self"
    pass

###############################################################################

Customize for Tk 8.0 main window menu bar, instead of a frame

###############################################################################

GuiMakerFrameMenu = GuiMaker # use this for embedded component menus

class GuiMakerWindowMenu(GuiMaker): # use this for top-level window menus def makeMenuBar(self): menubar = Menu(self.master) self.master.config(menu=menubar)

代码语言:javascript
复制
    for (name, key, items) in self.menuBar:
        pulldown = Menu(menubar)
        self.addMenuItems(pulldown, items)
        menubar.add_cascade(label=name, underline=key, menu=pulldown)

    if self.helpButton:
        if sys.platform[:3] == 'win':
            menubar.add_command(label='Help', command=self.help)
        else:
            pulldown = Menu(menubar)  # Linux needs real pull down
            pulldown.add_command(label='About', command=self.help)
            menubar.add_cascade(label='Help', menu=pulldown)
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022-05-13,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • this file has been updated for Python 3.X
  • at least enough to run--I'd probably change more given time and need
  • subclass to customize move selection
  • pick empty slot at random
  • pick imminent win or loss, else static score
  • static score based on 1 or 2 move lookahead
  • static score based on win or loss N moves ahead
  • search ahead through moves and countermoves
  • moved to tictactoe.py:
  • game object generator - external interface
  • command-line logic
  • this file has been updated for Python 3.X
  • game object generator - external interface
  • command-line logic
  • Customize for Tk 8.0 main window menu bar, instead of a frame
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档