古法编程: Python OOP 井字棋游戏

79 阅读5分钟

井字棋游戏 (Tic-Tac-Toe)

本项目使用Python面向对象编程实现了一个完整的井字棋游戏,包含以下特性:

  • OOP面向对象的封装,继承
  • 多继承的MRO
  • 输入验证,确保玩家只能选择空位
  • 自动判断胜负和平局
  • 友好的用户界面,显示当前棋盘状态

【MRO super链分析】

image.png

场景描述

井字棋(Tic-Tac-Toe)是一款经典的两人对弈游戏,在3×3的棋盘上进行。玩家轮流放置自己的标记(X或O),率先在横、竖或斜方向连成一线者获胜。如果棋盘填满仍未分出胜负,则为平局。

流程图

┌─────────────────────────────────────────────────────────────┐
│                      游戏开始                                 │
└──────────────────────────┬──────────────────────────────────┘
                           │
                           ▼
              ┌────────────────────────┐
              │   显示欢迎信息           │
              │ "Welcome to tic-tac-toe"│
              └────────────┬───────────┘
                           │
                           ▼
              ┌────────────────────────┐
              │  初始化棋盘 (TTTBoard)   │
              │  current_player = X    │
              │  next_player = O       │
              └────────────┬───────────┘
                           │
                           ▼
              ┌────────────────────────┐
              │      显示棋盘            │
              │   (调用 __str__)        │
              └────────────┬───────────┘
                           │
              ┌────────────▼───────────┐
              │                        │
              │   ┌──────────────────┐ │
              │   │ 提示当前玩家输入   │ │◄───┐
              │   │ "X/O请选择移动"   │ │    │
              │   └────────┬─────────┘ │    │
              │            │           │    │
              │   ┌────────▼─────────┐ │    │
              │   │ 读取用户输入 (1-9)│ │    │
              │   └────────┬─────────┘ │    │
              │            │           │    │
              │   ┌────────▼─────────┐ │    │
              │   │ is_valid_space?  │─────┼──── 无效
              │   └────────┬─────────┘ │    │
              │            │           │    │
              │            │ 有效      │    │
              │   ┌────────▼─────────┐ │    │
              │   │ update_board()   │ │    │
              │   │ 放置标记 (X或O)   │ │    │
              │   └────────┬─────────┘ │    │
              └────────────┼───────────┘    │
                           │                │
                           ▼                │
              ┌────────────────────────┐    │
              │   is_winner(current)?  │    │
              └────────────┬───────────┘    │
                           │                │
              ┌────────────┼────────────┐   │
              │            │            │   │
            是│            │否          │   │
              ▼            │            ▼   │
   ┌──────────────┐   ┌─────▼─────┐  ┌──────────────┐
   │ 显示获胜信息  │   │is_board   │  │              │
   │ "X 获胜!"   │   │ _full()?  │  │              │
   └──────┬───────┘   └─────┬─────┘  │              │
          │                 │        │              │
          │         ┌───────┼───────┐│              │
          │        是│       │否     ││              │
          │         ▼       ▼       ││              │
          │   ┌──────────┐ ┌────────▼───────────┐  │
          │   │显示平局  │ │ 交换玩家           │  │
          │   │"平局"   │ │ current, next =    │  │
          │   └────┬────┘ │ next, current      │  │
          │        │      └────────┬───────────┘  │
          │        │               │              │
          │        │               └──────────────┘
          │        │                      │
          └────────┼──────────────────────┘
                   │
                   ▼
              ┌──────────┐
              │ 显示"游戏结束"│
              └─────┬────┘
                   │
                   ▼
              ┌──────────┐
              │  游戏结束  │
              └──────────┘

完整的游戏过程示例

Welcome to tic-tac-toe

             | |   1 2 3
            -+-+-
             | |   4 5 6
            -+-+-
             | |   7 8 9
            
X请选择移动: (1-9)
> 1

            X| |   1 2 3
            -+-+-
             | |   4 5 6
            -+-+-
             | |   7 8 9
            
O请选择移动: (1-9)
> 2

            X|O|   1 2 3
            -+-+-
             | |   4 5 6
            -+-+-
             | |   7 8 9
            
X请选择移动: (1-9)
> 3

            X|O|X  1 2 3
            -+-+-
             | |   4 5 6
            -+-+-
             | |   7 8 9
            
O请选择移动: (1-9)
> 6

            X|O|X  1 2 3
            -+-+-
             | |O  4 5 6
            -+-+-
             | |   7 8 9
            
X请选择移动: (1-9)
> 8

            X|O|X  1 2 3
            -+-+-
             | |O  4 5 6
            -+-+-
             |X|   7 8 9
            
O请选择移动: (1-9)
> 7

            X|O|X  1 2 3
            -+-+-
             | |O  4 5 6
            -+-+-
            O|X|   7 8 9
            
X请选择移动: (1-9)
> 9

            X|O|X  1 2 3
            -+-+-
             | |O  4 5 6
            -+-+-
            O|X|X  7 8 9
            
O请选择移动: (1-9)
> 4

            X|O|X  1 2 3
            -+-+-
            O| |O  4 5 6
            -+-+-
            O|X|X  7 8 9
            
X请选择移动: (1-9)
> 5

            X|O|X  1 2 3
            -+-+-
            O|X|O  4 5 6
            -+-+-
            O|X|X  7 8 9
            
X 获胜!
游戏结束

在这个例子中,玩家X通过占据第5个位置(中心位置),完成了从左上到右下的对角线连线,从而赢得了比赛。

核心代码实现

1. 游戏棋盘类 (tictactoe_oop.py)

# 井字棋棋盘字典key
ALL_SPACES = [str(i) for i in range(1, 10)]
# 字符串常量
X, O, BLANK = 'X', 'O', ' '


class TTTBoard:
    """井字棋游戏"""
    def __init__(self):
        """初始化棋盘"""
        self._spaces = {space: BLANK for space in ALL_SPACES}

    def is_valid_space(self, space) -> bool:
        """判断是否为有效的移动
        Args:
            space: 要检查的位置,应该是 '1' 到 '9' 的字符串
        Returns:
            bool: 如果该位置为空则返回 True,否则返回 False
        """
        return space in ALL_SPACES and self._spaces[space] == BLANK

    def is_winner(self, player):
        """判断玩家是否获胜"""
        s, p = self._spaces, player  # 简写

        # 检查3行,3列,2对角线上的标记
        return (s['1'] == s['2'] == s['3'] == p or
                s['4'] == s['5'] == s['6'] == p or
                s['7'] == s['8'] == s['9'] == p or
                s['1'] == s['4'] == s['7'] == p or
                s['2'] == s['5'] == s['8'] == p or
                s['3'] == s['6'] == s['9'] == p or
                s['1'] == s['5'] == s['9'] == p or
                s['3'] == s['5'] == s['7'] == p)

    def is_board_full(self) -> bool:
        """判断棋盘是否已满
         Returns:
            bool: 如果棋盘已满返回 True,否则返回 False
        """
        return not any(space == BLANK for space in self._spaces.values())

    def update_board(self, space, player):
        """更新棋盘
        Args:
            space: 要更新的位置,应该是 '1' 到 '9' 的字符串
            player: 要更新位置的玩家,应该是 'X' 或 'O'
        """
        self._spaces[space] = player

    def __str__(self):
        """返回棋盘字符串"""
        return f"""
            {self._spaces['1']}|{self._spaces['2']}|{self._spaces['3']}  1 2 3
            -+-+-
            {self._spaces['4']}|{self._spaces['5']}|{self._spaces['6']}  4 5 6
            -+-+-
            {self._spaces['7']}|{self._spaces['8']}|{self._spaces['9']}  7 8 9
            """

    __repr__ = __str__

2. 游戏主程序 (app.py)

from tictactoe_oop import (TTTBoard, X, O)


def run():
    print("Welcome to tic-tac-toe")
    board = TTTBoard()
    current_player, next_player = X, O  # X先行,O后行

    while True:
        print(board)
        move = None
        while not board.is_valid_space(move):
            print(f"{current_player}请选择移动: (1-9)")
            move = input("> ")

        board.update_board(move, current_player)  # 执行移动

        # 检查游戏是否结束
        if board.is_winner(current_player):  # 首先检查一方是否获胜
            print(board)
            print(f"{current_player} 获胜!")
            break
        elif board.is_board_full():  # 检查棋盘是否已满
            print(board)
            print("平局")
            break

        current_player, next_player = next_player, current_player  # 交换玩家
    print("游戏结束")


if __name__ == '__main__':
    run()

添加Mini棋盘MiniBoard

添加一种简约化的棋盘

image.png

X请选择移动: (1-9)
> 9

                    ..X  1 2 3
                    .OX  4 5 6
                    .OX  7 8 9
                    
X 获胜!
游戏结束

所有的功能逻辑都不变,只需要通过继承,重写展示的方法即可,扩展棋盘展示

class MiniBoard(TTTBoard):
    """小棋盘"""
    def __str__(self):
        s = {
            space: '.' if val == BLANK else val
            for space, val in self._spaces.items()
        }
        """返回棋盘字符串"""
        return f"""
                    {s['1']}{s['2']}{s['3']}  1 2 3
                    {s['4']}{s['5']}{s['6']}  4 5 6
                    {s['7']}{s['8']}{s['9']}  7 8 9
                    """

    __repr__ = __str__

添加提示Hint

打印棋盘的时候,如果判断下一步有人会获胜就给出提示

X请选择移动: (1-9)
> 5

            X|O|   1 2 3
            -+-+-
             |X|   4 5 6
            -+-+-
             | |   7 8 9
            
X的下一步能获胜
O请选择移动: (1-9)
class HintBoard(TTTBoard):
    """添加说明X和O是否只差一步胜利"""

    def __str__(self):
        msgs = [super().__str__()]
        if self._judge_next_win(X):
            msgs.append(f"{X}的下一步能获胜")
        if self._judge_next_win(O):
            msgs.append(f"{O}的下一步能获胜")
        return '\n'.join(msgs)

    def _judge_next_win(self, player):
        """判断玩家在下一步是否能获胜(通过模拟尝试每个空位)

        Args:
            player: 要检查的玩家,应该是 'X' 或 'O'

        Returns:
            bool: 如果玩家能在下一步获胜返回 True,否则返回 False
        """
        origin_space = self._spaces.copy()  # 保存原始棋盘状态用于恢复
        is_win = False
        for space in self._spaces.keys():
            if self._spaces[space] == BLANK:  # 只检查空位
                super().update_board(space, player)  # 模拟在该位置落子
                if self.is_winner(player):
                    is_win = True
                    break
                super().update_board(space, BLANK)  # 恢复该位置为空位,尝试下一个位置
        self._spaces = origin_space  # 恢复原始棋盘状态
        return is_win

给mini棋盘也加上提示(MRO技术)

效果如下:

X请选择移动: (1-9)
> 9

                    XO.  1 2 3
                    ...  4 5 6
                    ..X  7 8 9
                    
X的下一步能获胜
O请选择移动: (1-9)

我们创建一个新类,继承HintBoard,MiniBoard就可以了,只需要有这个类,里面的不需要写任何的新功能

class HybridBoard(HintBoard,MiniBoard):
    """mini棋盘拥有提示功能"""
    pass

【MRO super链分析】

image.png

app.py中将MiniBoard替换成HintBoard就好了,其底层是使用mro技术。

if input("是否使用小棋盘?(y/n): ").lower() == 'y':
    # board = MiniBoard()
    board = HybridBoard()
else:
    board = HintBoard()

源码

Github tictactoe