diff --git a/docs/tictactoe.rst b/docs/tictactoe.rst index c4f182e4..9780ca6c 100644 --- a/docs/tictactoe.rst +++ b/docs/tictactoe.rst @@ -1,6 +1,44 @@ Tic Tac Toe =========== -A paper-and-pencil game for two players. +A single-player vs computer Tic Tac Toe game with the following features: + +- **Colored and styled pieces**: + X in blue, O in red, with thicker lines for clarity. +- **Move validation**: + Prevents placing a piece on an occupied square. +- **Win and draw detection**: + Automatically detects and displays a result message when someone + wins or when the board is full (draw). +- **Smart AI opponent**: + Uses the Minimax algorithm for optimal play, ensuring a + challenging game. + +Gameplay +-------- + +1. **Starting the game**: + + .. code-block:: python + + python tictactoe.py + +2. **Player moves**: + The human player uses X (blue) and always moves first. Click on + any empty square in the 3×3 grid to place your X. + +3. **Computer response**: + After your move, the computer (O in red) will calculate its optimal + move and place an O after a brief delay. + +4. **Winning**: + The first player to align three of their own pieces in a row + (horizontal, vertical, or diagonal) wins. + +5. **Draw**: + If all nine squares are filled without a winner, the game ends + in a draw. .. literalinclude:: ../src/freegames/tictactoe.py + :language: python + :linenos: \ No newline at end of file diff --git a/src/freegames/tictactoe.py b/src/freegames/tictactoe.py index 23a3142e..4ae76802 100644 --- a/src/freegames/tictactoe.py +++ b/src/freegames/tictactoe.py @@ -1,64 +1,157 @@ -"""Tic Tac Toe - -Exercises - -1. Give the X and O a different color and width. -2. What happens when someone taps a taken spot? -3. How would you detect when someone has won? -4. How could you create a computer player? -""" - from turtle import * from freegames import line +# ——— 全局状态 ——— +state = { + 'player': 0, # 0 = X(人),1 = O(电脑) + 'board': [[None]*3 for _ in range(3)], + 'game_over': False, +} +# 绘制格子 def grid(): """Draw tic-tac-toe grid.""" line(-67, 200, -67, -200) - line(67, 200, 67, -200) + line( 67, 200, 67, -200) line(-200, -67, 200, -67) - line(-200, 67, 200, 67) - + line(-200, 67, 200, 67) +# 画 X def drawx(x, y): - """Draw X player.""" + """Draw X player in blue, width=4.""" + color('blue') + width(4) line(x, y, x + 133, y + 133) line(x, y + 133, x + 133, y) - +# 画 O def drawo(x, y): - """Draw O player.""" + """Draw O player in red, width=4.""" up() goto(x + 67, y + 5) down() + color('red') + width(4) circle(62) - +# 将点击坐标归整到格子左下角 def floor(value): - """Round value down to grid with square size 133.""" return ((value + 200) // 133) * 133 - 200 +# 检测胜利 +def check_win(): + b = state['board'] + # 行、列检测 + for i in range(3): + if b[i][0] is not None and b[i][0] == b[i][1] == b[i][2]: + return b[i][0] + if b[0][i] is not None and b[0][i] == b[1][i] == b[2][i]: + return b[0][i] + # 对角线 + if b[0][0] is not None and b[0][0] == b[1][1] == b[2][2]: + return b[0][0] + if b[0][2] is not None and b[0][2] == b[1][1] == b[2][0]: + return b[0][2] + # 平局 + if all(all(cell is not None for cell in row) for row in b): + return -1 + return None + +def evaluate(): + """胜利时得分:电脑 O 赢 +1,人 X 赢 -1,平局或未结束 0""" + winner = check_win() + if winner == 1: # O (电脑) + return +1 + if winner == 0: # X (人) + return -1 + return 0 # 平局 or 未结束 + +def minimax(is_maximizing): + """Minimax 递归,返回 (最佳分值, (row, col) 最佳落子)""" + score = evaluate() + if score != 0 or all(all(cell is not None for cell in row) for row in state['board']): + # 终止:有人赢或满盘 + return score, None + + if is_maximizing: + best = (-2, None) # 初始化为比最小值还低 + for r in range(3): + for c in range(3): + if state['board'][r][c] is None: + state['board'][r][c] = 1 # 电脑落 O + val, _ = minimax(False) + state['board'][r][c] = None + if val > best[0]: + best = (val, (r, c)) + return best + else: + worst = (2, None) # 初始化为比最大值还高 + for r in range(3): + for c in range(3): + if state['board'][r][c] is None: + state['board'][r][c] = 0 # 玩家落 X + val, _ = minimax(True) + state['board'][r][c] = None + if val < worst[0]: + worst = (val, (r, c)) + return worst + +# 落子回调 +def tap(x, y): + if state['game_over']: + return -state = {'player': 0} -players = [drawx, drawo] + # 计算格子索引 + fx = floor(x) + fy = floor(y) + col = int((fx + 200) // 133) + row = int((fy + 200) // 133) + # 已有落子则忽略 + if state['board'][row][col] is not None: + return -def tap(x, y): - """Draw X or O in tapped square.""" - x = floor(x) - y = floor(y) + # 当前玩家落子 player = state['player'] - draw = players[player] - draw(x, y) + draw = drawx if player == 0 else drawo + draw(fx, fy) update() - state['player'] = not player - - + state['board'][row][col] = player + + # 检查胜利 + result = check_win() + if result is not None: + state['game_over'] = True + penup() + goto(0, 0) + color('green') + if result == 0: + write("You Win!", align='center', font=('Arial', 24, 'bold')) + elif result == 1: + write("Computer Wins!", align='center', font=('Arial', 24, 'bold')) + else: + write("Draw!", align='center', font=('Arial', 24, 'bold')) + return + + # 切换玩家 + state['player'] = 1 - player + + # 如果轮到电脑,就随机落子 + if state['player'] == 1: + _, move = minimax(True) # True 表示电脑层 (maximizing) + r, c = move + # 计算该格左下角坐标 + fx_ai = c*133 - 200 + fy_ai = r*133 - 200 + # 延迟一点,看起来更自然 + ontimer(lambda: tap(fx_ai + 1, fy_ai + 1), 200) + +# 主流程 setup(420, 420, 370, 0) hideturtle() tracer(False) grid() update() onscreenclick(tap) -done() +done() \ No newline at end of file