مینی‌پروژهٔ ۱ · سطح مقدماتی · ۲ تا ۳ ساعت

بازی دار (Hangman) با پایتون

مینی‌پروژهٔ اول: ساخت گام‌به‌گام بازی حدس کلمه (Hangman) با پایتون؛ از خواندن کلمات از فایل تا موتور کامل بازی. نوت‌بوک Jupyter قابل دانلود.

دانلود فایل‌ها

پیش‌نیازها

این پروژه فقط از همین مباحث استفاده می‌کند. اگر جایی گیر کردید، درس مربوطش را مرور کنید:

در این مینی‌پروژه بازی معروف دار (Hangman) را می‌سازیم: کامپیوتر یک کلمه انتخاب می‌کند و شما باید حرف‌به‌حرف حدسش بزنید، قبل از اینکه حدس‌هایتان تمام شود. هیچ مفهوم تازه‌ای در کار نیست؛ همه‌چیز با ابزارهایی ساخته می‌شود که تا اینجا یاد گرفته‌اید: رشته، لیست، حلقه، شرط، تابع و خواندن فایل. هنر کار در کنار هم چیدن آن‌هاست.

پیش از شروع

  1. فایل zip بالای صفحه را دانلود و باز کنید. داخلش نوت‌بوک hangman_codealong_fa.ipynb و فایل words.txt است. این دو باید در یک پوشه کنار هم باشند.
  2. نوت‌بوک را با Jupyter یا VS Code باز کنید.
  3. همهٔ سلول‌های کد خالی‌اند؛ فقط اسکلت و کامنت راهنما دارند. هر گام را اینجا بخوانید و سلول متناظرش را در نوت‌بوک خودتان پر کنید.

💡 قرارداد نوت‌بوک: 🟩 یعنی «نوبت شماست»، 💡 یعنی راهنمایی (اول خودتان فکر کنید، بعد نگاه کنید)، و ✅ یعنی «تست کن ببین درست شد؟».

فایل words.txt این سایت حدود ۵۰۰ کلمهٔ انگلیسی دارد که با فاصله از هم جدا شده‌اند. اگر نسخهٔ کامل کلاس (با حدود ۵۵ هزار کلمه) را دارید، می‌توانید جایگزینش کنید؛ کد شما باید با هر دو کار کند.

نقشهٔ راه

گام موضوع تابعی که می‌نویسیم
۰ گرم‌کردن: رشته، لیست، حلقه —
۱ خواندن کلمات از فایل loadWords
۲ انتخاب کلمهٔ تصادفی chooseWord
۳ آیا کلمه کامل حدس زده شده؟ isWordGuessed
۴ نمایش حدس کاربر getGuessedWord
۵ حروف باقی‌مانده getAvailableLetters
۶ موتور بازی، در ۵ گام کوچک hangman
۷ اجرای نهایی و تمرین‌های بیشتر —

گام ۰: گرم‌کردن

پیش‌نیاز: حلقهٔ for · پیمایش رشته و عملگر in

کل پروژه فقط با چهار ابزار نوشته می‌شود:

  1. پیمایش رشته با حلقه: رشته هم مثل لیست قابل پیمایش است.
  2. عملگر in: آیا این حرف داخل رشته یا لیست هست؟ (True یا False)
  3. ساختن رشته با +: یک رشتهٔ خالی می‌سازیم و حرف‌به‌حرف به آن اضافه می‌کنیم.
  4. .lower(): تبدیل ورودی کاربر به حروف کوچک.

این سلول را همین‌جا هم می‌توانید پر و اجرا کنید (دکمهٔ «ویرایش»):

# --- گرم‌کردن ۱: پیمایش رشته ---
word = 'apple'
# TODO: با یک حلقه، هر حرف word را در یک خط جدا چاپ کنید


# --- گرم‌کردن ۲: عملگر in ---
guessed = ['e', 'i', 'k', 'p', 'r', 's']
# TODO: چاپ کنید آیا 'p' در لیست guessed هست؟ و آیا 'z' هست؟


# --- گرم‌کردن ۳: ساختن رشته حرف‌به‌حرف ---
# TODO: یک رشتهٔ خالی بسازید و در یک حلقه حروف word را به آن اضافه کنید، بعد چاپش کنید

💡 الگوی «انباشت رشته» که در این پروژه چند بار تکرار می‌شود: قبل از حلقه مقداردهی اولیه، داخل حلقه اضافه‌کردن، بعد از حلقه استفاده.

result = '' ← for ch in something: ← result += ch

⚠️ اشتباه کلاسیک: اگر result = '' را داخل حلقه بگذارید، هر دور پاک می‌شود و آخرش فقط یک حرف می‌ماند.

گام ۱: خواندن کلمات از فایل (loadWords)

پیش‌نیاز: خواندن و نوشتن فایل · split در متدهای لیست · تعریف تابع

کامپیوتر باید کلمه را از words.txt انتخاب کند. پس اول محتوای فایل را می‌خوانیم و به لیستی از کلمه‌ها تبدیل می‌کنیم.

تابع loadWords() باید:

  • ورودی نداشته باشد،
  • پیام Loading word list from file... را چاپ کند،
  • فایل words.txt را باز کند و بخواند،
  • تعداد کلمه‌ها را چاپ کند (مثلاً 525 words loaded.)،
  • و لیست کلمه‌ها را برگرداند.
def loadWords():
    '''
    returns: list, لیستی از کلمات موجود در فایل words.txt
    '''
    # TODO 1: پیام "Loading word list from file..." را چاپ کنید
    # TODO 2: فایل را باز کنید       -> open('words.txt', 'r')
    # TODO 3: کل محتوای فایل را در یک رشته بخوانید   -> .read()
    # TODO 4: فایل را ببندید         -> .close()
    # TODO 5: رشته را با split() به لیست کلمات تبدیل کنید
    # TODO 6: تعداد کلمات را چاپ کنید
    # TODO 7: لیست را return کنید
    pass

💡 line.split() بدون ورودی، رشته را از روی فاصله‌ها می‌شکند و لیست برمی‌گرداند. len(wordlist) هم تعداد عضوهای لیست است. برای چاپ عدد کنار متن، یا str() کنید یا از f-string استفاده کنید.

⚠️ اگر FileNotFoundError گرفتید، یعنی words.txt کنار نوت‌بوک نیست. با import os و print(os.listdir('.')) ببینید چه فایل‌هایی کنار دستتان است.

گام ۲: انتخاب کلمهٔ تصادفی (chooseWord)

پیش‌نیاز: import و ماژول‌ها · اندیس در لیست

تابع chooseWord(wordlist) یک لیست می‌گیرد و یک کلمهٔ تصادفی از آن برمی‌گرداند. برای تصادفی بودن از ماژول آمادهٔ random استفاده کنید.

💡 دو راه دارید: random.choice(wordlist) مستقیم یک عضو تصادفی می‌دهد. راه دوم wordlist[random.randint(0, len(wordlist) - 1)] است. چرا - 1؟ چون randint برخلاف range، خود عدد آخر را هم شامل می‌شود.

✅ تست: chooseWord را چند بار صدا بزنید؛ باید هر بار کلمهٔ متفاوتی ببینید.

گام ۳: آیا کلمه کامل حدس زده شده؟ (isWordGuessed)

پیش‌نیاز: return و خروج از تابع · عملگر in

  • ورودی ۱: secretWord، کلمه‌ای که باید حدس زده شود (رشته)
  • ورودی ۲: lettersGuessed، لیست حروفی که تا الان حدس زده شده
  • خروجی: True اگر تمام حروف secretWord در lettersGuessed باشند، وگرنه False

قبل از کدزدن این جمله را کامل کنید: «اگر حتی یک حرف از کلمه در لیست حدس‌ها نبود، یعنی …»

اسکلت را پر کنید و همین‌جا اجرا کنید. اگر درست نوشته باشید OK ✅ چاپ می‌شود؛ وگرنه AssertionError می‌گیرید:

def isWordGuessed(secretWord, lettersGuessed):
    # TODO 1: روی تک‌تک حروف secretWord حلقه بزنید
    #   TODO 2: اگر این حرف در lettersGuessed نبود -> همان‌جا False برگردانید
    # TODO 3: اگر حلقه کامل تمام شد یعنی همه بودند -> True برگردانید
    pass


# ✅ تست
assert isWordGuessed('apple', ['e', 'i', 'k', 'p', 'r', 's']) == False
assert isWordGuessed('banana', ['a', 'x', 'q', 'b', 'z', 'n']) == True
print('OK ✅')

💡 به محض اجرای return، تابع تمام می‌شود و بقیهٔ حلقه اجرا نمی‌شود. برای همین پیدا کردن یک نمونهٔ نقض کافی است.

⚠️ دام رایج: اگر return True را داخل حلقه بگذارید، فقط حرف اول بررسی می‌شود. جای درستش بیرون و بعد از حلقه است.

گام ۴: نمایش حدس کاربر (getGuessedWord)

پیش‌نیاز: شرط if/else · الگوی انباشت رشته (گام ۰)

خروجی این تابع یک رشته است: حروف حدس‌زده‌شده سر جایشان، و به‌جای هر حرف حدس‌نزده '_ ' (زیرخط و یک فاصله).

secretWord lettersGuessed خروجی
'apple' ['e', 'i', 'k', 'p', 'r', 's'] '_ pp_ e'
'grapefruit' ['g', 'r', 'c', 'n', 'd', 'j', 'm', 'p', 'z', 's'] 'gr_ p_ _ r_ _ _ '
def getGuessedWord(secretWord, lettersGuessed):
    # TODO 1: یک رشتهٔ خالی بسازید (قبل از حلقه!)
    # TODO 2: روی حروف secretWord حلقه بزنید
    #   TODO 3: اگر حرف حدس زده شده -> خود حرف را اضافه کنید
    #   TODO 4: وگرنه -> '_ ' را اضافه کنید
    # TODO 5: رشته را return کنید
    pass


# ✅ تست
assert getGuessedWord('apple', ['e', 'i', 'k', 'p', 'r', 's']) == '_ pp_ e'
assert getGuessedWord('grapefruit', ['g', 'r', 'c', 'n', 'd', 'j', 'm', 'p', 'z', 's']) == 'gr_ p_ _ r_ _ _ '
print('OK ✅')

💡 سؤال برای فکر کردن: چرا اینجا return را داخل حلقه نمی‌گذاریم، ولی در isWordGuessed گذاشتیم؟

گام ۵: حروف باقی‌مانده (getAvailableLetters)

پیش‌نیاز: import و ماژول‌ها · حلقهٔ for

ورودی فقط lettersGuessed است. خروجی: رشته‌ای از حروف کوچک انگلیسی که هنوز حدس زده نشده‌اند، به ترتیب الفبا.

لازم نیست a تا z را دستی تایپ کنید؛ ماژول string آماده‌اش را دارد: string.ascii_lowercase. دقت کنید این بار روی الفبا حلقه می‌زنیم، نه روی کلمه. چرا؟

import string

def getAvailableLetters(lettersGuessed):
    # TODO 1: یک رشتهٔ خالی برای نتیجه بسازید
    # TODO 2: روی string.ascii_lowercase حلقه بزنید
    #   TODO 3: اگر حرف در lettersGuessed نبود، به نتیجه اضافه‌اش کنید
    # TODO 4: نتیجه را return کنید
    pass


# ✅ تست
assert getAvailableLetters(['e', 'i', 'k', 'p', 'r', 's']) == 'abcdfghjlmnoqtuvwxyz'
assert getAvailableLetters([]) == 'abcdefghijklmnopqrstuvwxyz'
print('OK ✅')

💡 چون روی ascii_lowercase (که خودش مرتب است) حلقه می‌زنیم، خروجی خودبه‌خود به ترتیب الفباست و نیازی به مرتب‌سازی نیست.

🎉 ایست بازرسی: هر پنج تابع کمکی آماده است. اگر تست‌های گام ۳ تا ۵ رد شدند، سراغ خود بازی بروید.

گام ۶: موتور بازی (hangman)

پیش‌نیاز: حلقهٔ while · شرط‌های تودرتو · input · append در لیست

قوانین بازی

  • کاربر ۸ حدس دارد.
  • فقط حدس اشتباه یکی از حدس‌ها را کم می‌کند.
  • حرف تکراری حدسی کم نمی‌کند، ولی به کاربر تذکر داده می‌شود.
  • در شروع بازی، تعداد حروف کلمه گفته می‌شود.
  • در هر دور، تعداد حدس‌های باقی‌مانده و حروف قابل استفاده نمایش داده می‌شود.
  • بازی وقتی تمام می‌شود که کاربر ببرد، یا حدس‌هایش تمام شود (آن‌وقت کلمه لو می‌رود).

این تابع بزرگ است، پس در نوت‌بوک آن را در پنج گام کوچک می‌سازیم و هر گام را جدا تست می‌کنیم.

۶.۱: خوش‌آمد و متغیرهای اولیه

guessesLeft = 8 و lettersGuessed = []، بعد پیام‌های شروع:

Welcome to the game Hangman!
I am thinking of a word that is 4 letters long
-----------

۶.۲: حلقهٔ اصلی

بازی تا کی ادامه دارد؟ تا وقتی حدسی باقی مانده باشد: while guessesLeft > 0:. داخل حلقه، وضعیت را چاپ کنید (You have 8 guesses left. و Available Letters: ...). برای اینکه فعلاً حلقه بی‌نهایت نشود، در این گام موقتاً هر دور یکی از guessesLeft کم کنید.

⚠️ اگر یادتان برود guessesLeft را کم کنید، حلقه بی‌نهایت می‌شود. در Jupyter دکمهٔ ⏹ (Interrupt) بالای نوت‌بوک متوقفش می‌کند.

۶.۳: گرفتن حدس

guess = input('Please guess a letter: ').lower(). چرا lower()؟ چون اگر کاربر A بزند و کلمه apple باشد، حدسش باید درست حساب شود.

۶.۴: سه حالت ممکن (قلب بازی)

حالت پیام حدس کم می‌شود؟
حرف تکراری است Oops! You've already guessed that letter: ... نه
حرف در کلمه هست Good guess: ... نه
حرف در کلمه نیست Oops! That letter is not in my word: ... بله

جای ... خروجی getGuessedWord می‌نشیند.

💡 ترتیب شرط‌ها مهم است: اول «تکراری بودن» را چک کنید. اگر اول «در کلمه هست؟» را بپرسید، حرف تکراریِ درست هم Good guess می‌گیرد. ساختار پیشنهادی: if تکراری … else: اضافه‌کردن به لیست و بعد یک if/else تودرتو برای درست یا غلط بودن.

در نوت‌بوک این منطق را اول در تابع جداگانهٔ checkGuess می‌نویسید تا بدون input تستش کنید. این روش تست‌کردن تکه‌تکه، عادت خیلی خوبی است.

۶.۵: برد و باخت

  • برد: در ابتدای هر دور با isWordGuessed چک کنید. اگر کلمه کامل شده، Congratulations, you won! و خروج از تابع.
  • باخت: اگر حلقه به‌طور طبیعی تمام شد (یعنی guessesLeft صفر شد): Sorry, you ran out of guesses. The word was ...

💡 برای خروج در حالت برد، return و break هر دو کار می‌کنند. تفاوتشان چیست؟ با break فقط از حلقه بیرون می‌آیید و کدهای بعد از حلقه هم اجرا می‌شوند؛ آن‌وقت پیام باخت هم چاپ می‌شود، مگر اینکه برایش شرط بگذارید.

چک‌لیست قبل از اجرا

  1. آیا guessesLeft فقط در حالت حدس غلط کم می‌شود؟
  2. آیا حرف تکراری دوباره به lettersGuessed اضافه نمی‌شود؟
  3. آیا چک برد قبل از گرفتن ورودی انجام می‌شود؟ (وگرنه کاربر بعد از کامل کردن کلمه باز هم یک حرف می‌پرسد.)
  4. آیا پیام باخت فقط وقتی چاپ می‌شود که واقعاً باخته باشد؟

نمونهٔ اجرای درست

اگر hangman('tact') را اجرا کنید و به ترتیب حروف a، a، s، t، r، m و c را بزنید، خروجی شما باید دقیقاً این باشد:

Welcome to the game Hangman!
I am thinking of a word that is 4 letters long
-----------
You have 8 guesses left.
Available Letters: abcdefghijklmnopqrstuvwxyz
Please guess a letter: a
Good guess: _ a_ _ 
-----------
You have 8 guesses left.
Available Letters: bcdefghijklmnopqrstuvwxyz
Please guess a letter: a
Oops! You've already guessed that letter: _ a_ _ 
-----------
You have 8 guesses left.
Available Letters: bcdefghijklmnopqrstuvwxyz
Please guess a letter: s
Oops! That letter is not in my word: _ a_ _ 
-----------
You have 7 guesses left.
Available Letters: bcdefghijklmnopqrtuvwxyz
Please guess a letter: t
Good guess: ta_ t
-----------
You have 7 guesses left.
Available Letters: bcdefghijklmnopqruvwxyz
Please guess a letter: r
Oops! That letter is not in my word: ta_ t
-----------
You have 6 guesses left.
Available Letters: bcdefghijklmnopquvwxyz
Please guess a letter: m
Oops! That letter is not in my word: ta_ t
-----------
You have 5 guesses left.
Available Letters: bcdefghijklnopquvwxyz
Please guess a letter: c
Good guess: tact
-----------
Congratulations, you won!

دقت کنید حرف تکراری (a دوم) حدسی کم نکرد، ولی s و r و m هر کدام یکی کم کردند.

برای تست باخت: hangman('else') را اجرا کنید و حروف a تا i را به ترتیب بزنید. بعد از i باید پیام Sorry, you ran out of guesses. The word was else. را ببینید.

گام ۷: اجرای بازی واقعی

سه خط که کل پروژه را زنده می‌کند:

wordlist = loadWords()
secretWord = chooseWord(wordlist).lower()
hangman(secretWord)

💡 بازی را در Jupyter یا ترمینال اجرا کنید، نه در ویرایشگر این سایت. اجراگر سایت برای کدهای کوتاه است و input تکرارشونده و خواندن words.txt در آن راحت نیست.

تمرین‌های بیشتر

  1. اعتبارسنجی ورودی: اگر کاربر به‌جای یک حرف، عدد یا چند حرف وارد کرد، پیام مناسب بدهید و حدسی کم نکنید. (راهنما: len(guess) و guess.isalpha())
  2. نقاشی دار: تابعی بنویسید که بر اساس تعداد اشتباه‌ها، شکل دار را با کاراکترها بکشد.
  3. امتیازدهی: در پایان بازی، امتیازی بر اساس حدس‌های باقی‌مانده و طول کلمه حساب کنید.
  4. بازی دوباره: در پایان بپرسید «بازی دوباره؟ (y/n)» و در صورت y بازی جدیدی شروع کنید.
  5. حالت سخت: فقط ۴ حدس، و فقط کلمه‌های بلندتر از ۸ حرف.
  6. راهنما: اگر کاربر ? بزند، یک حرف تصادفی از کلمه لو برود ولی ۲ حدس کم شود.

چک‌لیست نهایی

  • loadWords و chooseWord نوشته و تست شده
  • isWordGuessed، getGuessedWord و getAvailableLetters تست‌هایشان را رد می‌کنند
  • hangman کامل است و هر سه حالت (تکراری، درست، غلط) را رعایت می‌کند
  • حدس‌ها فقط روی اشتباه‌ها کم می‌شوند
  • پیام برد و پیام باخت درست کار می‌کنند
  • کد کامل در یک فایل .py هم اجرا می‌شود

به‌روزرسانی: ۲۹ مهر ۱۴۰۵