مینیپروژهٔ ۱ · سطح مقدماتی · ۲ تا ۳ ساعت
بازی دار (Hangman) با پایتون
مینیپروژهٔ اول: ساخت گامبهگام بازی حدس کلمه (Hangman) با پایتون؛ از خواندن کلمات از فایل تا موتور کامل بازی. نوتبوک Jupyter قابل دانلود.
دانلود فایلها
- نوتبوک + فایل کلمات (zip)پیشنهادی؛ هر دو فایل لازم را با هم دارد
- فقط نوتبوک (ipynb)
- فقط فایل کلمات (words.txt)
پیشنیازها
این پروژه فقط از همین مباحث استفاده میکند. اگر جایی گیر کردید، درس مربوطش را مرور کنید:
- اجرای پایتون در Jupyter Notebook و JupyterLab مقدمات و راهاندازی
- رشته در پایتون: اندیسگذاری، برش (slicing) و تغییرناپذیری رشتهها
- پیمایش رشته و متدهای پرکاربرد: in، lower، strip و isdigit رشتهها
- شرط در پایتون: if، elif، else و شرطهای تودرتو کنترل جریان
- حلقهٔ for و range در پایتون؛ break و continue کنترل جریان
- حلقهٔ while در پایتون: تکرار تا وقتی شرط برقرار است کنترل جریان
- تعریف تابع در پایتون: def، پارامتر، return و docstring توابع
- تفاوت return و print در پایتون؛ None و برگرداندن چند مقدار توابع
- ماژول و پکیج در پایتون: import، from، as و ساخت ماژول شخصی ماژولها و پکیجها
- خواندن و نوشتن فایل در پایتون: open، حالتهای w، a، r و close فایلها و مدیریت خطا
- لیست در پایتون: ساخت، تغییرپذیری، append و extend ساختمان دادهها
- متدهای لیست در پایتون: pop، remove، sort و دام alias و clone ساختمان دادهها
در این مینیپروژه بازی معروف دار (Hangman) را میسازیم: کامپیوتر یک کلمه انتخاب میکند و شما باید حرفبهحرف حدسش بزنید، قبل از اینکه حدسهایتان تمام شود. هیچ مفهوم تازهای در کار نیست؛ همهچیز با ابزارهایی ساخته میشود که تا اینجا یاد گرفتهاید: رشته، لیست، حلقه، شرط، تابع و خواندن فایل. هنر کار در کنار هم چیدن آنهاست.
پیش از شروع
- فایل zip بالای صفحه را دانلود و باز کنید. داخلش نوتبوک
hangman_codealong_fa.ipynbو فایلwords.txtاست. این دو باید در یک پوشه کنار هم باشند. - نوتبوک را با Jupyter یا VS Code باز کنید.
- همهٔ سلولهای کد خالیاند؛ فقط اسکلت و کامنت راهنما دارند. هر گام را اینجا بخوانید و سلول متناظرش را در نوتبوک خودتان پر کنید.
💡 قرارداد نوتبوک: 🟩 یعنی «نوبت شماست»، 💡 یعنی راهنمایی (اول خودتان فکر کنید، بعد نگاه کنید)، و ✅ یعنی «تست کن ببین درست شد؟».
فایل words.txt این سایت حدود ۵۰۰ کلمهٔ انگلیسی دارد که با فاصله از هم جدا شدهاند. اگر نسخهٔ کامل کلاس (با حدود ۵۵ هزار کلمه) را دارید، میتوانید جایگزینش کنید؛ کد شما باید با هر دو کار کند.
نقشهٔ راه
| گام | موضوع | تابعی که مینویسیم |
|---|---|---|
| ۰ | گرمکردن: رشته، لیست، حلقه | — |
| ۱ | خواندن کلمات از فایل | loadWords |
| ۲ | انتخاب کلمهٔ تصادفی | chooseWord |
| ۳ | آیا کلمه کامل حدس زده شده؟ | isWordGuessed |
| ۴ | نمایش حدس کاربر | getGuessedWord |
| ۵ | حروف باقیمانده | getAvailableLetters |
| ۶ | موتور بازی، در ۵ گام کوچک | hangman |
| ۷ | اجرای نهایی و تمرینهای بیشتر | — |
گام ۰: گرمکردن
پیشنیاز: حلقهٔ for · پیمایش رشته و عملگر in
کل پروژه فقط با چهار ابزار نوشته میشود:
- پیمایش رشته با حلقه: رشته هم مثل لیست قابل پیمایش است.
- عملگر
in: آیا این حرف داخل رشته یا لیست هست؟ (TrueیاFalse) - ساختن رشته با
+: یک رشتهٔ خالی میسازیم و حرفبهحرف به آن اضافه میکنیم. .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فقط از حلقه بیرون میآیید و کدهای بعد از حلقه هم اجرا میشوند؛ آنوقت پیام باخت هم چاپ میشود، مگر اینکه برایش شرط بگذارید.
چکلیست قبل از اجرا
- آیا
guessesLeftفقط در حالت حدس غلط کم میشود؟ - آیا حرف تکراری دوباره به
lettersGuessedاضافه نمیشود؟ - آیا چک برد قبل از گرفتن ورودی انجام میشود؟ (وگرنه کاربر بعد از کامل کردن کلمه باز هم یک حرف میپرسد.)
- آیا پیام باخت فقط وقتی چاپ میشود که واقعاً باخته باشد؟
نمونهٔ اجرای درست
اگر 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در آن راحت نیست.
تمرینهای بیشتر
- اعتبارسنجی ورودی: اگر کاربر بهجای یک حرف، عدد یا چند حرف وارد کرد، پیام مناسب بدهید و حدسی کم نکنید. (راهنما:
len(guess)وguess.isalpha()) - نقاشی دار: تابعی بنویسید که بر اساس تعداد اشتباهها، شکل دار را با کاراکترها بکشد.
- امتیازدهی: در پایان بازی، امتیازی بر اساس حدسهای باقیمانده و طول کلمه حساب کنید.
- بازی دوباره: در پایان بپرسید «بازی دوباره؟ (y/n)» و در صورت
yبازی جدیدی شروع کنید. - حالت سخت: فقط ۴ حدس، و فقط کلمههای بلندتر از ۸ حرف.
- راهنما: اگر کاربر
?بزند، یک حرف تصادفی از کلمه لو برود ولی ۲ حدس کم شود.
چکلیست نهایی
-
loadWordsوchooseWordنوشته و تست شده -
isWordGuessed،getGuessedWordوgetAvailableLettersتستهایشان را رد میکنند -
hangmanکامل است و هر سه حالت (تکراری، درست، غلط) را رعایت میکند - حدسها فقط روی اشتباهها کم میشوند
- پیام برد و پیام باخت درست کار میکنند
- کد کامل در یک فایل
.pyهم اجرا میشود
بهروزرسانی: ۲۹ مهر ۱۴۰۵