برنامه‌نویسی پایتون فهرست مطالب
فصل ۲۰

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

پایان ترم اول. در این فصل مطلب تازه‌ای یاد نمی‌گیرید؛ همه چیزهایی که خوانده‌اید را در پنج برنامه کامل کنار هم می‌گذاریم. هر پنج پروژه فقط از ابزارهای فصل یک تا نوزده استفاده می‌کنند.

در این فصل می‌خوانید
  1. چطور یک پروژه را شروع کنیم
  2. پروژه یک: حدس عدد با مرحله و جان
  3. پروژه دو: سنگ کاغذ قیچی
  4. پروژه سه: وردل
  5. پروژه چهار: دوز
  6. پروژه پنج: بازی آزمون
  7. درس‌های مشترک این پنج پروژه

۱. چطور یک پروژه را شروع کنیم

بزرگ‌ترین مشکل تازه‌کارها این نیست که پایتون بلد نیستند؛ این است که نمی‌دانند از کجا شروع کنند. روش کار همیشه همین است:

  1. بازی را روی کاغذ توضیح دهید. اگر نمی‌توانید به فارسی بگویید چه اتفاقی می‌افتد، نمی‌توانید به پایتون هم بگویید.
  2. داده‌ها را مشخص کنید. چه چیزهایی را باید نگه دارید؟ امتیاز، صفحه بازی، لیست کلمات؟ نوع ظرفشان را انتخاب کنید.
  3. کارها را به تابع بشکنید. «صفحه را نشان بده»، «حرکت بگیر»، «برنده را پیدا کن».
  4. ساده‌ترین نسخه کارکننده را بنویسید. بدون امتیاز، بدون مرحله، بدون زیبایی.
  5. بعد کامل کنید. اعتبارسنجی ورودی، پیام‌های بهتر، ذخیره در فایل.

۲. پروژه یک: حدس عدد با مرحله و جان

نسخه پیشرفته بازی فصل هشتم: چند مرحله، تعدادی جان، و همه متن‌ها در یک ماژول تنظیمات.

'''All the texts and the levels of the game.'''

LEVELS = [
    {
        'level_number': 1,
        'number': 2,
        'hint': 'A number between 1 and 3: ',
    },
    {
        'level_number': 2,
        'number': -7,
        'hint': "A negative number greater than -10: ",
    },
    {
        'level_number': 3,
        'number': 42,
        'hint': 'A number between 1 and 100: ',
    },
]

START_HEARTS = 5

MESSAGE_WIN = 'You won the level!'
MESSAGE_GREATER = 'The secret number is greater!'
MESSAGE_LOWER = 'The secret number is lower!'
MESSAGE_GAMEOVER = 'Game over!'
MESSAGE_FINISHED = 'There are no more levels right now!'

فایل constants.py: پروژه 01_number_guess

def play_level(level: dict, hearts: int) -> int:
    '''Play one level.

    Returns the number of hearts left; 0 means the player lost.
    '''
    secret = level['number']

    while hearts > 0:
        guess = read_number(level['hint'])

        if guess == secret:
            print(MESSAGE_WIN)
            hearts += 1                 # a bonus life for winning the level
            show_hearts(hearts)
            return hearts

        if guess > secret:
            print(MESSAGE_LOWER)
        else:
            print(MESSAGE_GREATER)

        hearts -= 1
        show_hearts(hearts)

    return 0


def main() -> None:
    hearts = START_HEARTS
    show_hearts(hearts)

    for level in LEVELS:
        print(f"\n--- Level {level['level_number']} ---")
        hearts = play_level(level, hearts)

        if hearts == 0:
            print(MESSAGE_GAMEOVER)
            return          # leave the game at once

    print(MESSAGE_FINISHED)

منطق بازی: پروژه 01_number_guess

شرح پروژه

باگ‌هایی که در نسخه اصلی این بازی بود

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

۳. پروژه دو: سنگ کاغذ قیچی

'''Rock - paper - scissors, first player to three points wins.'''

import random

ROCK = 'rock'
PAPER = 'paper'
SCISSORS = 'scissors'

MOVES = [ROCK, PAPER, SCISSORS]

# BEATS[a] is the move that 'a' defeats. One dictionary replaces six elif lines.
BEATS = {
    ROCK: SCISSORS,
    PAPER: ROCK,
    SCISSORS: PAPER,
}

POINTS_TO_WIN = 3


def read_move(player: str) -> str:
    'Ask a player for a valid move (or "exit").'
    while True:
        move = input(f'{player}, enter your move (rock/paper/scissors): ').lower().strip()

        if move == 'exit' or move in MOVES:
            return move

        print('Unknown move, try again.')


def winner_of(move1: str, move2: str) -> int:
    '''Return 0 for a draw, 1 when move1 wins, 2 when move2 wins.'''
    if move1 == move2:
        return 0
    if BEATS[move1] == move2:
        return 1
    return 2

قواعد بازی: پروژه 02_rock_paper_scissors

شرح پروژه

۴. پروژه سه: وردل

def mask_word(word: str, found_letters: set[str]) -> str:
    'Show the letters already found and hide the others behind "_".'
    masked = ''
    for letter in word:
        if letter in found_letters:
            masked += letter
        else:
            masked += '_'
        masked += ' '
    return masked.strip()


def read_letter(already_tried: set[str]) -> str:
    'Ask for one single letter that was not tried before.'
    while True:
        letter = input('Enter a letter: ').lower().strip()

        if len(letter) != 1:
            print('You can only enter one letter!')
            continue

        if not letter.isalpha():
            print('Letters only, please.')
            continue

        if letter in already_tried:
            print('You already tried that one.')
            continue

        return letter

نمایش و ورودی: پروژه 03_wordle

def play(word: str) -> bool:
    'Play one word. Return True when the player found it.'
    found_letters: set[str] = set()
    tried_letters: set[str] = set()
    wrong_guesses = 0

    print(f'\nThe word has {len(word)} letters!')
    print(mask_word(word, found_letters))

    while wrong_guesses < MAX_WRONG_GUESSES:
        letter = read_letter(tried_letters)
        tried_letters.add(letter)

        if letter in word:
            found_letters.add(letter)
            print('Yes!')
        else:
            wrong_guesses += 1

        print(mask_word(word, found_letters))

        # The word is complete when every one of its letters has been found.
        if set(word) == set(word) & found_letters:
            print(f'You win! The word was {word.capitalize()}.')
            return True

    print(f'You lost. The word was {word.capitalize()}.')
    return False

حلقه بازی: پروژه 03_wordle

The word has 6 letters!
_ _ _ _ _ _
Enter a letter: a
Yes!
_ a _ _ _ _
Enter a letter: o
Yes!
_ a _ o o _

شرح پروژه

باگ‌های نسخه اصلی

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

۵. پروژه چهار: دوز

PLAYER_SIGN = 'X'
AI_SIGN = 'O'

WINNING_LINES = [
    (0, 1, 2), (3, 4, 5), (6, 7, 8),      # rows
    (0, 3, 6), (1, 4, 7), (2, 5, 8),      # columns
    (0, 4, 8), (2, 4, 6),                 # diagonals
]


def new_board() -> list[str]:
    'Return a fresh board where every cell shows its own number.'
    return ['1', '2', '3', '4', '5', '6', '7', '8', '9']


def show_board(board: list[str]) -> None:
    'Draw the board.'
    print()
    print(f' {board[0]} | {board[1]} | {board[2]} ')
    print('---+---+---')
    print(f' {board[3]} | {board[4]} | {board[5]} ')
    print('---+---+---')
    print(f' {board[6]} | {board[7]} | {board[8]} ')
    print()


def is_legal(board: list[str], move: str) -> bool:
    'A move is legal when it is 1-9 and the cell is still free.'
    if not move.isdigit():
        return False

    number = int(move)
    if number < 1 or number > 9:
        return False

    return board[number - 1] not in (PLAYER_SIGN, AI_SIGN)

صفحه و قواعد: پروژه 04_tic_tac_toe

def convert_persian_digits(text: str) -> str:
    'Turn Persian digits into English ones so both keyboards work.'
    persian_digits = '۰۱۲۳۴۵۶۷۸۹'
    english_digits = '0123456789'
    table = str.maketrans(persian_digits, english_digits)
    return text.translate(table)

پشتیبانی از کیبورد فارسی: پروژه 04_tic_tac_toe

شرح پروژه

باگ‌های نسخه اصلی

ورودی غیرعددی باعث ValueError و خراب شدن برنامه می‌شد، امتیازها در متغیرهای سراسری نگه داشته می‌شدند، و برای خروج از sys.exit() استفاده شده بود که برنامه را ناگهانی قطع می‌کند. نسخه اصلاح‌شده با isdigit() بررسی می‌کند و با return تمیز بیرون می‌آید.

۶. پروژه پنج: بازی آزمون

این پروژه تازه‌ای است که در نسخه اصلی دوره وجود نداشت و برای تمرین فصل هجدهم اضافه شده است: سؤال‌ها در فایل متنی هستند، نه داخل کد.

Which keyword defines a function in Python?|def|class|func|define
What does len('Kadoos') return?|6|5|7|error
Which type is the result of input()?|str|int|float|bool

قالب فایل questions.txt: سؤال، پاسخ درست، سپس گزینه‌های غلط

def load_questions(path: str) -> list[dict]:
    '''Read the file and return a list of questions.

    Every question is a dictionary: {'text': ..., 'correct': ..., 'options': [...]}
    '''
    if not os.path.exists(path):
        raise FileNotFoundError(f'{path} was not found')

    questions: list[dict] = []

    with open(path, encoding='utf-8') as f:
        for line_number, line in enumerate(f, start=1):
            line = line.strip()
            if line == '':
                continue

            parts = line.split('|')
            if len(parts) < 3:
                print(f'[warning] line {line_number} is ignored (wrong format)')
                continue

            text = parts[0]
            correct = parts[1]
            options = parts[1:]
            random.shuffle(options)

            questions.append({
                'text': text,
                'correct': correct,
                'options': options,
            })

    return questions

خواندن سؤال‌ها از فایل: پروژه 05_quiz

شرح پروژه

۷. درس‌های مشترک این پنج پروژه

اگر هر پنج پروژه را بخوانید، الگوهای یکسانی می‌بینید. این موارد همان چیزی است که از ترم اول باید با خودتان ببرید:

الگوتوضیح
ثابت‌ها در بالای فایلبا حروف بزرگ. برای تغییر تنظیمات، کد را نمی‌گردید
داده به جای کددیکشنری BEATS، لیست WINNING_LINES، لیست LEVELS
هر تابع یک کارخواندن، محاسبه، نمایش، جدا از هم
توابع بدون چاپتابعی که فقط مقدار برمی‌گرداند، همه‌جا قابل استفاده است
اعتبارسنجی هر ورودیحلقه تا وقتی جواب معتبر شود
راه خروجهر بازی باید گزینه exit داشته باشد
main() و نگهباندر هر پنج پروژه
بدون globalمقدار را برگردانید، متغیر سراسری نسازید
پیشنهاد جدی

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

فایل‌های تمرینی این فصل

پوشه S20-term1-projects شامل پنج پوشه پروژه: 01_number_guess، 02_rock_paper_scissors، 03_wordle، 04_tic_tac_toe و 05_quiz. هر کدام را از داخل پوشه خودش با python app.py اجرا کنید.

پایان ترم اول

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

ترم دوم از فصل بیست و یکم شروع می‌شود و کاملاً متفاوت است: به جای نوشتن دستورها، یاد می‌گیرید مدل بسازید. شیءگرایی، پایگاه داده، رابط گرافیکی و وب. اگر مطالب ترم اول را خوب بلد نیستید، پیش از رفتن به فصل بعد یک بار دیگر تمرین‌های فصل‌های یازده تا نوزده را انجام دهید.

تمرین‌های فصل بیست

حدس عدد

  1. بهترین رکورد (کمترین تعداد حدس) را در فایل ذخیره کنید و در شروع هر بازی نشانش دهید.
  2. بازی را برعکس کنید: شما عددی در ذهن بگیرید و کامپیوتر حدس بزند و بپرسد «بیشتر یا کمتر». برای بازه ۱ تا ۱۰۰ چند حدس لازم دارد و چرا؟
  3. مرحله‌ای اضافه کنید که بازه ۱ تا ۱۰۰۰ باشد ولی فقط ده حدس داشته باشید.

سنگ کاغذ قیچی

  1. به جای یک دور، سه برد از پنج بازی کنید.
  2. بشمارید بازیکن بیشتر چه چیزی می‌زند و کامپیوتر از آن استفاده کند. آیا شروع به بردن می‌کند؟
  3. حالت‌های «مارمولک» و «اسپاک» را اضافه کنید. قواعد را در دیکشنری بگذارید، نه در زنجیره if.

وردل

  1. فهرست کلمات را به جای لیست پایتونی، از فایل بخوانید.
  2. حروف امتحان‌شده را نشان دهید و از تکرارشان جلوگیری کنید.
  3. یک وردل واقعی بسازید: پنج حرف، شش تلاش، و برای هر حرف یک علامت (درست و در جای درست، درست ولی جای غلط، اصلاً نیست).

دوز

  1. حالت دو نفره روی یک کیبورد اضافه کنید.
  2. کامپیوتر را باهوش کنید: اگر حرکت برنده دارد بزند، وگرنه جلوی برد حریف را بگیرد.
  3. امتیاز چند بازی را نگه دارید و در پایان نشان دهید.

بازی آزمون

  1. زمان‌سنج اضافه کنید: ده ثانیه برای هر سؤال.
  2. به سؤال‌ها دسته‌بندی بدهید و بگذارید بازیکن انتخاب کند.
  3. نتیجه هر بازی را در فایل بنویسید و صفحه «پنج نفر برتر» بسازید.

مشترک بین پروژه‌ها

  1. چیزهای مشترک هر پنج برنامه (پاک کردن صفحه، خواندن امن عدد، انتظار برای اینتر) را در ماژول helpers.py بگذارید.
  2. یک menu.py بنویسید که پنج بازی را نشان دهد و بازی انتخاب‌شده را اجرا کند.
  3. کد خودتان را مرور کنید و مطمئن شوید هر برنامه در برابر جواب غلط، خط خالی و Ctrl + C مقاوم است.
فصل قبل: خطاها و استثناها فصل بعد: شیءگرایی