تمرین و پروژه‌های پایتون

ساخت بازی دوز با پایتون؛ پروژه کامل Tic-Tac-Toe دو نفره با تشخیص برد

ساخت بازی دوز با پایتون

🗓 آخرین به‌روزرسانی:

ساخت بازی دوز با پایتون یکی از پروژه‌های بسیار مناسب برای تمرین منطق برنامه‌نویسی، لیست‌ها، حلقه‌ها، شرط‌ها، توابع و مدیریت وضعیت یک بازی است. در این آموزش یک نسخه کامل Tic-Tac-Toe دو نفره در ترمینال می‌سازیم که صفحه ۳×۳ را نمایش می‌دهد، نوبت بازیکنان X و O را مدیریت می‌کند، ورودی نامعتبر یا خانه اشغال‌شده را رد می‌کند، برد را در سطر، ستون و قطر تشخیص می‌دهد و در صورت پر شدن صفحه بدون برنده، نتیجه مساوی اعلام می‌کند.

این مقاله یازدهمین پروژه فاز اول دسته تمرین و پروژه‌های پایتون است. اگر پروژه‌های قبلی مثل بازی حدس کلمه با پایتون یا سنگ کاغذ قیچی با پایتون را انجام داده باشید، ساختار حلقه اصلی بازی و مدیریت نوبت برایتان آشنا خواهد بود.

در این پروژه چه چیزی می‌سازیم؟

  • صفحه بازی ۳×۳ با ۹ خانه
  • نمایش شماره خانه‌های آزاد
  • دو بازیکن X و O
  • تعویض خودکار نوبت بازیکنان
  • اعتبارسنجی ورودی
  • جلوگیری از انتخاب خانه اشغال‌شده
  • تشخیص برد در سه سطر
  • تشخیص برد در سه ستون
  • تشخیص برد در دو قطر
  • تشخیص مساوی
  • خروج اختیاری از بازی
  • امکان اجرای دوباره بازی

پیش‌نیازهای پروژه

برای ساخت این پروژه نیازی به نصب کتابخانه خارجی نداریم. بهتر است با لیست‌ها در پایتون، شرط‌های if و elif، حلقه while و توابع و پارامترها در پایتون آشنا باشید.

فایل پروژه را با این نام بسازید:

tic_tac_toe.py

و برای اجرا از دستور زیر استفاده کنید:

python tic_tac_toe.py

منطق بازی دوز چگونه کار می‌کند؟

صفحه بازی ۹ خانه دارد. هر بازیکن در نوبت خود یکی از خانه‌های خالی را انتخاب می‌کند. بازیکن X همیشه دور را شروع می‌کند و بعد نوبت بین X و O جابه‌جا می‌شود. هر بازیکنی که سه علامت یکسان را در یک سطر، ستون یا قطر قرار دهد برنده است. اگر هر ۹ خانه پر شوند و هیچ ترکیب برنده‌ای وجود نداشته باشد، بازی مساوی می‌شود.

مرحله اول: ساخت صفحه بازی

ساده‌ترین ساختار مناسب برای صفحه، یک لیست ۹ عضوی است:

def create_board():
    return [" "] * 9

هر خانه خالی با یک رشته شامل فاصله نمایش داده می‌شود. اندیس‌های صفر تا هشت به خانه‌های یک تا نه مربوط هستند.

چرا از لیست یک‌بعدی استفاده می‌کنیم؟

می‌توانستیم صفحه را به شکل لیست تو در تو ۳×۳ بسازیم، اما برای این پروژه لیست یک‌بعدی ساده‌تر است. شماره خانه‌ای که کاربر وارد می‌کند با کم کردن یک، مستقیماً به اندیس لیست تبدیل می‌شود.

position = 5
index = position - 1

خانه شماره ۵ در اندیس ۴ قرار دارد.

مرحله دوم: نمایش صفحه

اگر خانه‌ای خالی باشد، شماره آن را نمایش می‌دهیم تا کاربر بداند برای انتخاب آن باید چه عددی وارد کند.

def display_board(board):
    cells = [
        value
        if value != " "
        else str(index + 1)
        for index, value in enumerate(board)
    ]

    print()
    print(
        f" {cells[0]} | {cells[1]} | {cells[2]} "
    )
    print("---+---+---")
    print(
        f" {cells[3]} | {cells[4]} | {cells[5]} "
    )
    print("---+---+---")
    print(
        f" {cells[6]} | {cells[7]} | {cells[8]} "
    )
    print()

صفحه خالی به شکل زیر دیده می‌شود:

 1 | 2 | 3
---+---+---
 4 | 5 | 6
---+---+---
 7 | 8 | 9

مرحله سوم: تعریف حالت‌های برنده

در دوز دقیقاً هشت ترکیب برنده داریم: سه سطر، سه ستون و دو قطر.

WINNING_COMBINATIONS = (
    (0, 1, 2),
    (3, 4, 5),
    (6, 7, 8),
    (0, 3, 6),
    (1, 4, 7),
    (2, 5, 8),
    (0, 4, 8),
    (2, 4, 6),
)

این روش بهتر از نوشتن هشت شرط جداگانه است، چون قوانین بازی به شکل داده نگهداری می‌شوند و تابع تشخیص برد می‌تواند روی آن‌ها پیمایش کند.

مرحله چهارم: تشخیص برنده

def check_winner(
    board,
    player,
):
    return any(
        all(
            board[index] == player
            for index in combination
        )
        for combination in WINNING_COMBINATIONS
    )

تابع all() بررسی می‌کند هر سه خانه یک ترکیب متعلق به همان بازیکن باشند و any() کافی بودن یکی از ترکیب‌های برنده را تشخیص می‌دهد.

یک نسخه ساده‌تر برای درک بهتر

اگر هنوز با any و all راحت نیستید، همین منطق را می‌توان واضح‌تر نوشت:

def check_winner_simple(
    board,
    player,
):
    for combination in WINNING_COMBINATIONS:
        a, b, c = combination

        if (
            board[a] == player
            and board[b] == player
            and board[c] == player
        ):
            return True

    return False

مرحله پنجم: دریافت حرکت معتبر

کاربر ممکن است متن، عدد خارج از بازه یا خانه‌ای را که قبلاً پر شده وارد کند. تابع دریافت حرکت باید همه این حالت‌ها را کنترل کند.

def get_move(
    board,
    player,
):
    while True:
        value = input(
            f"بازیکن {player}، "
            "خانه 1 تا 9 "
            "(یا 0 برای خروج): "
        ).strip()

        if value == "0":
            return None

        if not value.isdigit():
            print(
                "لطفاً فقط یک عدد وارد کنید."
            )
            continue

        position = int(value)

        if (
            position < 1
            or position > 9
        ):
            print(
                "شماره خانه باید بین 1 تا 9 باشد."
            )
            continue

        index = position - 1

        if board[index] != " ":
            print(
                "این خانه قبلاً انتخاب شده است."
            )
            continue

        return index

برای دریافت ورودی در برنامه‌های تعاملی، آموزش تابع input در پایتون نیز مکمل خوبی برای این پروژه است.

مرحله ششم: تشخیص پر شدن صفحه

def board_full(board):
    return all(
        cell != " "
        for cell in board
    )

اگر هیچ خانه خالی باقی نمانده باشد و قبل از آن برنده‌ای پیدا نشده باشد، بازی مساوی است.

مرحله هفتم: تغییر نوبت بازیکن

def switch_player(player):
    return (
        "O"
        if player == "X"
        else "X"
    )

این تابع عمداً کوچک است، اما باعث می‌شود منطق حلقه اصلی خواناتر باقی بماند.

مرحله هشتم: ساخت حلقه اصلی بازی

def play_game():
    board = create_board()
    player = "X"

    while True:
        display_board(board)

        move = get_move(
            board,
            player,
        )

        if move is None:
            print("بازی متوقف شد.")
            return None

        board[move] = player

        if check_winner(
            board,
            player,
        ):
            display_board(board)
            print(
                f"بازیکن {player} برنده شد!"
            )
            return player

        if board_full(board):
            display_board(board)
            print("بازی مساوی شد.")
            return "draw"

        player = switch_player(player)

ترتیب بررسی‌ها مهم است. بعد از ثبت حرکت ابتدا برد را بررسی می‌کنیم و سپس پر شدن صفحه را. اگر این ترتیب برعکس باشد، ممکن است حرکت نهم هم برنده باشد ولی برنامه زودتر مساوی اعلام کند.

مرحله نهم: اجرای دوباره بازی

def main():
    print(
        "=== بازی دوز با پایتون ==="
    )

    while True:
        result = play_game()

        if result is None:
            break

        again = input(
            "دوباره بازی کنیم؟ "
            "(بله/خیر): "
        ).strip().lower()

        if again not in {
            "بله",
            "yes",
            "y",
        }:
            print("بازی پایان یافت.")
            break

کد کامل بازی دوز با پایتون

WINNING_COMBINATIONS = (
    (0, 1, 2),
    (3, 4, 5),
    (6, 7, 8),
    (0, 3, 6),
    (1, 4, 7),
    (2, 5, 8),
    (0, 4, 8),
    (2, 4, 6),
)


def create_board():
    return [" "] * 9


def display_board(board):
    cells = [
        value
        if value != " "
        else str(index + 1)
        for index, value in enumerate(board)
    ]

    print()
    print(
        f" {cells[0]} | {cells[1]} | {cells[2]} "
    )
    print("---+---+---")
    print(
        f" {cells[3]} | {cells[4]} | {cells[5]} "
    )
    print("---+---+---")
    print(
        f" {cells[6]} | {cells[7]} | {cells[8]} "
    )
    print()


def get_move(
    board,
    player,
):
    while True:
        value = input(
            f"بازیکن {player}، "
            "خانه 1 تا 9 "
            "(یا 0 برای خروج): "
        ).strip()

        if value == "0":
            return None

        if not value.isdigit():
            print(
                "لطفاً فقط یک عدد وارد کنید."
            )
            continue

        position = int(value)

        if (
            position < 1
            or position > 9
        ):
            print(
                "شماره خانه باید بین 1 تا 9 باشد."
            )
            continue

        index = position - 1

        if board[index] != " ":
            print(
                "این خانه قبلاً انتخاب شده است."
            )
            continue

        return index


def check_winner(
    board,
    player,
):
    return any(
        all(
            board[index] == player
            for index in combination
        )
        for combination in WINNING_COMBINATIONS
    )


def board_full(board):
    return all(
        cell != " "
        for cell in board
    )


def switch_player(player):
    return (
        "O"
        if player == "X"
        else "X"
    )


def play_game():
    board = create_board()
    player = "X"

    while True:
        display_board(board)

        move = get_move(
            board,
            player,
        )

        if move is None:
            print("بازی متوقف شد.")
            return None

        board[move] = player

        if check_winner(
            board,
            player,
        ):
            display_board(board)
            print(
                f"بازیکن {player} برنده شد!"
            )
            return player

        if board_full(board):
            display_board(board)
            print("بازی مساوی شد.")
            return "draw"

        player = switch_player(player)


def main():
    print(
        "=== بازی دوز با پایتون ==="
    )

    while True:
        result = play_game()

        if result is None:
            break

        again = input(
            "دوباره بازی کنیم؟ "
            "(بله/خیر): "
        ).strip().lower()

        if again not in {
            "بله",
            "yes",
            "y",
        }:
            print("بازی پایان یافت.")
            break


if __name__ == "__main__":
    main()

نمونه اجرای بازی

=== بازی دوز با پایتون ===

 1 | 2 | 3
---+---+---
 4 | 5 | 6
---+---+---
 7 | 8 | 9

بازیکن X، خانه 1 تا 9: 5

پس از چند حرکت، صفحه می‌تواند به این شکل برسد:

 X | O | 3
---+---+---
 4 | X | O
---+---+---
 7 | 8 | X

بازیکن X برنده شد!

کد پروژه چگونه بررسی شد؟

قبل از انتشار، Syntax نسخه کامل بررسی شد و چند تست منطقی نیز انجام شد: برد افقی برای X، نبود برد برای O روی همان صفحه، تغییر صحیح نوبت بین X و O و تشخیص صفحه کاملاً پر. این تست‌ها کمک می‌کنند خطاهای پایه در منطق بازی قبل از توسعه نسخه‌های پیشرفته‌تر مشخص شوند.

اشتباه‌های رایج در پروژه Tic-Tac-Toe

فراموش کردن تبدیل شماره خانه به اندیس

کاربر خانه‌های ۱ تا ۹ را می‌بیند، اما لیست پایتون اندیس صفر تا هشت دارد. بنابراین باید یک واحد از مقدار ورودی کم شود.

اجازه انتخاب خانه اشغال‌شده

قبل از ثبت حرکت باید بررسی شود مقدار خانه برابر رشته خالی باشد. در غیر این صورت بازیکن می‌تواند علامت نفر قبلی را بازنویسی کند.

تعویض بازیکن قبل از بررسی برد

اگر بلافاصله پس از حرکت بازیکن را عوض کنید و سپس برنده را بررسی کنید، ممکن است نام بازیکن اشتباه اعلام شود. ابتدا نتیجه حرکت فعلی را بررسی کنید.

نوشتن هشت if جداگانه برای برد

این روش قابل اجراست، اما نگهداری آن سخت‌تر می‌شود. تعریف ترکیب‌های برد در یک Tuple و پیمایش روی آن ساختار تمیزتری ایجاد می‌کند.

بررسی مساوی قبل از برد

حرکت نهم ممکن است هم صفحه را پر کند و هم یک ترکیب سه‌تایی برنده بسازد. بنابراین برد باید قبل از مساوی بررسی شود.

چطور پروژه را توسعه دهیم؟

  1. ثبت امتیاز چند دور: تعداد بردهای X، بردهای O و مساوی‌ها را نگه دارید.
  2. انتخاب علامت: اجازه دهید بازیکن اول X یا O را انتخاب کند.
  3. شروع تصادفی: در هر دور بازیکن شروع‌کننده را تصادفی انتخاب کنید.
  4. بازی مقابل کامپیوتر: ابتدا یک ربات ساده با انتخاب خانه آزاد تصادفی بسازید.
  5. هوش مصنوعی بهتر: الگوریتم Minimax را برای ساخت رقیب شکست‌ناپذیر پیاده‌سازی کنید.
  6. نسخه گرافیکی: با Tkinter برای هر خانه یک Button بسازید.
  7. ذخیره نتیجه: تاریخچه برد و باخت را در JSON ذخیره کنید.
  8. رنگ خروجی: با ANSI Escape Code یا کتابخانه مناسب، X و O را با رنگ متفاوت نمایش دهید.

تمرین پیشنهادی: ساخت ربات ساده

نسخه دو نفره را به نسخه انسان در برابر کامپیوتر تبدیل کنید. در ساده‌ترین حالت، ابتدا خانه‌های خالی را پیدا کنید:

def available_moves(board):
    return [
        index
        for index, cell in enumerate(board)
        if cell == " "
    ]

سپس کامپیوتر می‌تواند با random.choice() یکی از خانه‌های آزاد را انتخاب کند. این ربات هوشمند نیست، اما پایه مناسبی برای مرحله بعدی و پیاده‌سازی استراتژی یا Minimax است.

تمرین پیشرفته: تشخیص حرکت برد یا دفاع

قبل از انتخاب تصادفی، برنامه را طوری توسعه دهید که بررسی کند آیا کامپیوتر در یک حرکت می‌تواند برنده شود. اگر نه، بررسی کند آیا حریف در حرکت بعدی امکان برد دارد و آن خانه را مسدود کند. همین دو قانون ساده کیفیت بازی کامپیوتر را به شکل محسوسی بهتر می‌کنند.

اگر بعد از این بازی متنی دوست دارید سراغ یک بازی گرافیکی بروید، ساخت بازی مار با پایتون گزینه مناسبی است. برای انتخاب پروژه بعدی هم فهرست ۳۲ پروژه پایتون از مبتدی تا پیشرفته را ببینید.

این پروژه چه مفاهیمی را تمرین می‌دهد؟

  • List و اندیس‌ها
  • Tuple برای قوانین ثابت
  • List Comprehension
  • Generator Expression
  • توابع any و all
  • حلقه while
  • شرط‌ها
  • توابع و مقدار بازگشتی
  • اعتبارسنجی ورودی
  • مدیریت نوبت بازیکنان
  • مدیریت State بازی
  • مدل‌سازی قوانین با داده

جمع‌بندی

در این آموزش یک بازی دوز کامل با پایتون ساختیم که دو بازیکن می‌توانند در ترمینال اجرا کنند. صفحه با یک لیست ۹ عضوی نگهداری می‌شود، ورودی‌ها قبل از ثبت اعتبارسنجی می‌شوند، هشت ترکیب برنده در یک ساختار ثابت تعریف شده‌اند و بعد از هر حرکت برد یا مساوی بررسی می‌شود.

ارزش اصلی این پروژه در یادگیری مدل‌سازی قوانین است. به‌جای اینکه برای هر وضعیت کد جداگانه بنویسیم، قواعد برد را به داده تبدیل کردیم و یک تابع عمومی آن‌ها را بررسی می‌کند. پس از اجرای نسخه پایه، ساخت ربات ساده یا افزودن Minimax بهترین مسیر برای تبدیل این پروژه به یک تمرین پیشرفته‌تر است.

📌 همچنین بخوانید: تمرین پایتون با جواب، ساخت بازی با پایتون و Pygame و ساخت ربات با پایتون

می‌خواهی پیشرفتت ذخیره شود؟با ورود یا ثبت‌نام رایگان، درس‌های تمام‌شده را علامت بزن و درصد پیشرفتت را در حساب کاربری ببین.ورود / ثبت‌نام برای ذخیره پیشرفت
درس بعدی · پروژه‌های پایتون (12 از 19)ساخت برنامه مدیریت نمرات دانش‌آموزان با پایتون؛ پروژه کامل با JSON، میانگین و رتبه‌بندی ←
آموزش‌های تازه، کد آماده و رفع خطاهر هفته در کانال Dev Source؛ رایگان.

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *