ساخت بازی مار با پایتون و Pygame؛ پروژه کامل Snake با سورس کد

🗓 آخرین بهروزرسانی:
ساخت بازی مار (Snake) با پایتون یکی از بهترین پروژهها برای قدم گذاشتن در دنیای بازیسازی است. این بازی ساده به نظر میرسد، اما تقریباً همه مفاهیم اصلی یک بازی واقعی را در خود دارد: حلقه بازی، دریافت ورودی از کیبورد، حرکت روی شبکه، تشخیص برخورد، امتیاز و شروع دوباره. در این آموزش یک بازی مار کامل با کتابخانه Pygame میسازیم؛ ولی برخلاف بیشتر آموزشها، منطق بازی را از بخش نمایش جدا میکنیم تا هر قانون بازی را بتوانیم جداگانه تست کنیم. همه کدها روی پایتون ۳.۱۳ و Pygame نسخه ۲.۶ اجرا شدهاند، خروجیها واقعی هستند و سورس کامل پروژه را هم میتوانید بهصورت فایل ZIP دانلود کنید.
در این پروژه چه چیزی میسازیم؟
نسخه نهایی بازی ما یک پنجره ۶۰۰ در ۴۰۰ پیکسلی است که به شبکهای از خانههای ۲۰ پیکسلی تقسیم شده است. مار با کلیدهای جهتدار یا W، A، S و D حرکت میکند، با خوردن هر غذا یک خانه بلندتر میشود و اگر به دیوار یا بدن خودش بخورد، بازی تمام میشود. امکانات بازی اینهاست:
- حرکت روان روی شبکه با سرعت قابل تنظیم
- جلوگیری از چرخش ۱۸۰ درجه که باعث مرگ ناگهانی میشود
- غذای تصادفی که هیچوقت روی بدن مار ظاهر نمیشود
- امتیاز فعلی و بهترین امتیاز در بالای صفحه
- پیام Game Over و شروع دوباره با کلید Space
- یک حالت نمایشی خودکار (Demo) که بازی را بدون دخالت کاربر اجرا و تست میکند
نصب Pygame
اگر Pygame را نصب ندارید، بهتر است ابتدا یک محیط مجازی بسازید و سپس کتابخانه را با pip نصب کنید. جزئیات ساخت محیط مجازی را در آموزش pip و venv و رفع خطاهای نصب را در نصب کتابخانه در پایتون ببینید:
python -m venv venv
# Windows: venv\Scripts\activate macOS/Linux: source venv/bin/activate
pip install pygame
python -c "import pygame; print(pygame.version.ver)"ایده اصلی: جدا کردن منطق بازی از نمایش
بیشتر آموزشهای بازی مار همه چیز را داخل یک حلقه بزرگ مینویسند: خواندن کلید، جابهجایی مار، بررسی برخورد و رسم صفحه. این روش برای بازی کوچک کار میکند، اما خیلی زود کد شلوغ میشود و پیدا کردن باگ سخت است. ما بازی را دو بخش میکنیم. بخش اول کلاس SnakeGame است که فقط قوانین بازی را میداند و هیچ ارتباطی با Pygame ندارد؛ مار کجاست، غذا کجاست و بعد از هر حرکت چه اتفاقی میافتد. بخش دوم تابع main است که با Pygame پنجره را میسازد، کلیدها را میخواند و وضعیت بازی را رسم میکند.
مزیت بزرگ این طراحی این است که قوانین بازی را بدون باز کردن پنجره و فقط با چند خط کد تست میکنیم؛ همان کاری که در ادامه برای هر قانون انجام میدهیم. همین الگو در بازیها و برنامههای بزرگتر هم استفاده میشود و به آن جداسازی مدل از نما میگویند.
مدلسازی مار با لیست مختصات
مار را بهصورت لیستی از تاپلهای مختصات (x, y) نگه میداریم. اولین عضو لیست سر مار و آخرین عضو دم آن است. حرکت مار ترفند سادهای دارد: یک سر جدید در جهت حرکت به ابتدای لیست اضافه میکنیم و دم را از انتهای لیست برمیداریم. به این ترتیب طول مار ثابت میماند و فقط جابهجا میشود:
snake = [(5, 5), (4, 5), (3, 5)] # head first
direction = (1, 0) # moving right
for step in range(3):
head_x, head_y = snake[0]
new_head = (head_x + direction[0], head_y + direction[1])
snake.insert(0, new_head) # add a new head
snake.pop() # remove the tail
print(f"step {step + 1}: {snake}")خروجی:
step 1: [(6, 5), (5, 5), (4, 5)]
step 2: [(7, 5), (6, 5), (5, 5)]
step 3: [(8, 5), (7, 5), (6, 5)]جهت حرکت را هم بهصورت یک بردار دو عددی نشان میدهیم: (1, 0) یعنی راست، (-1, 0) چپ، (0, -1) بالا و (0, 1) پایین. دقت کنید که در Pygame محور y رو به پایین است، پس بالا رفتن یعنی کم شدن y.
کلاس SnakeGame: قوانین بازی در یک جا
حالا همین ایده را داخل کلاس قرار میدهیم. متد reset مار سهخانهای را وسط صفحه میگذارد، place_food یک خانه خالی تصادفی برای غذا انتخاب میکند و step مار را یک خانه جلو میبرد. برای اینکه نتیجهها قابل تکرار باشند، به کلاس یک seed میدهیم تا اعداد تصادفی هر بار یکسان باشند. سورس کامل کلاس را در انتهای مقاله آوردهایم؛ اینجا رفتار آن را بررسی میکنیم:
from snake import SnakeGame
game = SnakeGame(cols=10, rows=8, seed=1)
print("snake:", game.snake)
print("food:", game.food, "score:", game.score)
for _ in range(2):
game.step()
print("after 2 steps:", game.snake)
print("alive:", game.alive)خروجی:
snake: [(5, 4), (4, 4), (3, 4)]
food: (2, 1) score: 0
after 2 steps: [(7, 4), (6, 4), (5, 4)]
alive: Trueتابع place_food اول فهرست همه خانههایی را میسازد که مار روی آنها نیست و سپس یکی را تصادفی انتخاب میکند. این کار تضمین میکند غذا هیچوقت زیر بدن مار ظاهر نشود؛ اشتباهی که در خیلی از نسخههای ساده بازی دیده میشود. برای آشنایی بیشتر با تولید عدد تصادفی، پروژه شبیهساز تاس را ببینید.
جلوگیری از برگشت مار روی خودش
اگر مار به سمت راست حرکت میکند و بازیکن کلید چپ را بزند، سر مار مستقیم به گردنش برخورد میکند و بازی بیدلیل تمام میشود. متد turn این حرکت را رد میکند: اگر جمع بردار جهت جدید و جهت فعلی صفر شود، یعنی دو جهت دقیقاً مخالف هم هستند. نکته دیگر این است که جهت جدید را در next_direction ذخیره میکنیم و فقط در حرکت بعدی اعمال میکنیم؛ پس اگر بازیکن دو کلید را خیلی سریع پشت سر هم بزند، مار باز هم روی خودش برنمیگردد:
from snake import SnakeGame, UP, LEFT
game = SnakeGame(seed=3)
print("moving:", game.direction)
game.turn(LEFT) # opposite direction: ignored
game.step()
print("after LEFT:", game.direction, "alive:", game.alive)
game.turn(UP) # a real turn
game.step()
print("after UP:", game.direction, "head:", game.snake[0])خروجی:
moving: (1, 0)
after LEFT: (1, 0) alive: True
after UP: (0, -1) head: (16, 9)خوردن غذا و بلند شدن مار
وقتی سر جدید مار روی خانه غذا قرار بگیرد، امتیاز یکی زیاد میشود و این بار دم مار را حذف نمیکنیم؛ پس مار یک خانه بلندتر میشود. سپس غذای جدیدی در یک خانه خالی دیگر ظاهر میشود. برای تست، غذا را دستی درست جلوی سر مار میگذاریم:
from snake import SnakeGame
game = SnakeGame(seed=5)
head_x, head_y = game.snake[0]
game.food = (head_x + 1, head_y) # put food right in front of the head
print("length before:", len(game.snake), "score:", game.score)
game.step()
print("length after:", len(game.snake), "score:", game.score)
print("new food is free:", game.food not in game.snake)خروجی:
length before: 3 score: 0
length after: 4 score: 1
new food is free: Trueتشخیص برخورد با دیوار و بدن
بازی در دو حالت تمام میشود: سر مار از شبکه بیرون برود یا به یکی از خانههای بدن خودش برسد. در بررسی برخورد با بدن از self.snake[:-1] استفاده کردهایم، نه کل لیست؛ چون در همین حرکت دم مار یک خانه جلو میرود و رسیدن سر به خانهای که دم همین حالا ترکش کرده، برخورد نیست. این جزئیات کوچک تفاوت یک بازی باگدار با یک بازی درست است:
from snake import SnakeGame, UP, LEFT, DOWN
# 1) wall: keep moving right until the snake leaves the grid
game = SnakeGame(cols=10, rows=8, seed=2)
game.food = (0, 0)
steps = 0
while game.alive:
game.step()
steps += 1
print("hit the wall after", steps, "steps, head:", game.snake[0])
# 2) self: a long snake that turns into its own body
game = SnakeGame(cols=10, rows=8, seed=2)
game.snake = [(4, 4), (4, 5), (5, 5), (6, 5), (6, 4), (6, 3)]
print("head:", game.snake[0], "moving:", game.direction)
game.turn(DOWN) # (4, 5) is part of the body
game.step()
print("after DOWN alive:", game.alive)خروجی:
hit the wall after 5 steps, head: (9, 4)
head: (4, 4) moving: (1, 0)
after DOWN alive: Falseدر تست اول، سر مار در آخرین خانه مجاز یعنی (9, 4) میماند و حرکت بعدی آن را از شبکه بیرون میبرد. در تست دوم، مار به شکل قلاب خم شده و با چرخیدن به پایین به خانه (4, 5) میرسد که بخشی از بدن خودش است؛ پس alive به False تغییر میکند. بعد از مرگ، متد step هیچ کاری نمیکند تا بازیکن با Space بازی را از نو شروع کند.
حلقه اصلی بازی با Pygame
حالا به بخش نمایش میرسیم. هر بازی Pygame یک حلقه اصلی دارد که در هر دور سه کار انجام میدهد: رویدادها (Events) مثل فشردن کلید یا بستن پنجره را میخواند، وضعیت بازی را یک قدم جلو میبرد و صفحه را دوباره رسم میکند. تابع clock.tick(FPS) سرعت حلقه را محدود میکند؛ با FPS برابر ۱۰، مار ده خانه در ثانیه حرکت میکند. ساختار حلقه در فایل نهایی اینطور است:
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.KEYDOWN:
if event.key in KEYS:
game.turn(KEYS[event.key])
elif event.key == pygame.K_SPACE and not game.alive:
game.reset()
game.step()
best = max(best, game.score)
draw(screen, font, game, best)
clock.tick(FPS)دیکشنری KEYS هر کلید را به یک جهت نگاشت میکند تا بهجای چند if پشت سر هم، با یک جستوجوی ساده در دیکشنری جهت را پیدا کنیم. تابع draw هم پسزمینه را با رنگ سرمهای پر میکند، هر خانه مار را با pygame.draw.rect سبز و غذا را قرمز رسم میکند و امتیاز را با font.render بالای صفحه مینویسد. در پایان، pygame.display.flip() تصویر آماده را یکجا نمایش میدهد تا صفحه چشمک نزند.
یک نکته مهم در رسم این است که منطق بازی با «خانه» کار میکند و Pygame با «پیکسل». برای همین هنگام رسم، مختصات هر خانه را در اندازه خانه یعنی CELL ضرب میکنیم؛ مثلاً خانه (3, 2) در پیکسل (60, 40) رسم میشود. یک پیکسل هم از عرض و ارتفاع هر مربع کم کردهایم تا بین خانههای بدن مار خط باریکی دیده شود و بدن مار قطعهقطعه و خواناتر به نظر برسد. اگر بخواهید اندازه صفحه را عوض کنید، کافی است ثابتهای COLS، ROWS و CELL را تغییر دهید؛ بقیه کد خودش را با آنها هماهنگ میکند.
اجرای بازی و حالت نمایشی خودکار
برای بازی کردن کافی است فایل را با python snake.py اجرا کنید. اما برای اینکه مطمئن شویم کل بازی، از حلقه Pygame تا رسم صفحه، بدون خطا کار میکند، یک حالت نمایشی هم ساختهایم. در این حالت تابع auto_direction نقش بازیکن را دارد: از بین حرکتهایی که باعث برخورد نمیشوند، آن را انتخاب میکند که مار را به غذا نزدیکتر کند. کد زیر بازی واقعی را ۳۰۰ فریم بدون پنجره اجرا میکند:
from snake import main
score, best = main(max_frames=300, autoplay=True)
print(f"Demo finished after 300 frames: score={score}, best={best}")خروجی:
Demo finished after 300 frames: score=15, best=15این ربات ساده فقط یک قدم جلوتر را میبیند و برای همین در مارهای خیلی بلند گیر میافتد؛ اما برای تست بازی کاملاً کافی است. اگر علاقه دارید، میتوانید آن را با الگوریتم جستوجوی مسیر مثل BFS هوشمندتر کنید.
سورس کامل بازی مار با پایتون
کد کامل بازی در ادامه آمده است. همین فایل را همراه با requirements.txt و راهنمای اجرا بهصورت ZIP هم آماده کردهایم:
⬇ دانلود سورس کامل بازی مار (ZIP)
"""Snake game with Pygame - Dev Source (dev-source.ir)"""
import random
import sys
import pygame
CELL = 20
COLS, ROWS = 30, 20
WIDTH, HEIGHT = CELL * COLS, CELL * ROWS
FPS = 10
UP, DOWN, LEFT, RIGHT = (0, -1), (0, 1), (-1, 0), (1, 0)
class SnakeGame:
"""Game rules only: no drawing, so it is easy to test."""
def __init__(self, cols=COLS, rows=ROWS, seed=None):
self.cols, self.rows = cols, rows
self.rng = random.Random(seed)
self.reset()
def reset(self):
cx, cy = self.cols // 2, self.rows // 2
self.snake = [(cx, cy), (cx - 1, cy), (cx - 2, cy)]
self.direction = RIGHT
self.next_direction = RIGHT
self.score = 0
self.alive = True
self.food = self.place_food()
def place_food(self):
free = [(x, y) for x in range(self.cols) for y in range(self.rows)
if (x, y) not in self.snake]
return self.rng.choice(free)
def turn(self, new_direction):
# ignore a turn that would reverse the snake into itself
if (new_direction[0] + self.direction[0],
new_direction[1] + self.direction[1]) != (0, 0):
self.next_direction = new_direction
def step(self):
if not self.alive:
return
self.direction = self.next_direction
head_x, head_y = self.snake[0]
new_head = (head_x + self.direction[0], head_y + self.direction[1])
hit_wall = not (0 <= new_head[0] < self.cols and 0 <= new_head[1] < self.rows)
hit_self = new_head in self.snake[:-1]
if hit_wall or hit_self:
self.alive = False
return
self.snake.insert(0, new_head)
if new_head == self.food:
self.score += 1
self.food = self.place_food()
else:
self.snake.pop()
KEYS = {pygame.K_UP: UP, pygame.K_DOWN: DOWN, pygame.K_LEFT: LEFT, pygame.K_RIGHT: RIGHT,
pygame.K_w: UP, pygame.K_s: DOWN, pygame.K_a: LEFT, pygame.K_d: RIGHT}
def auto_direction(game):
"""Simple bot for the demo: the safe move that gets closest to the food."""
hx, hy = game.snake[0]
fx, fy = game.food
best, best_dist = game.direction, None
for d in (UP, DOWN, LEFT, RIGHT):
if (d[0] + game.direction[0], d[1] + game.direction[1]) == (0, 0):
continue
nx, ny = hx + d[0], hy + d[1]
if not (0 <= nx < game.cols and 0 <= ny < game.rows) or (nx, ny) in game.snake[:-1]:
continue
dist = abs(fx - nx) + abs(fy - ny)
if best_dist is None or dist < best_dist:
best, best_dist = d, dist
return best
def draw(screen, font, game, best):
screen.fill((11, 29, 63))
for x, y in game.snake:
pygame.draw.rect(screen, (61, 220, 132), (x * CELL, y * CELL, CELL - 1, CELL - 1))
fx, fy = game.food
pygame.draw.rect(screen, (255, 92, 92), (fx * CELL, fy * CELL, CELL - 1, CELL - 1))
text = font.render(f"Score: {game.score} Best: {best}", True, (255, 255, 255))
screen.blit(text, (8, 6))
if not game.alive:
msg = font.render("Game Over - press SPACE to restart", True, (255, 201, 40))
screen.blit(msg, msg.get_rect(center=(WIDTH // 2, HEIGHT // 2)))
pygame.display.flip()
def main(max_frames=None, autoplay=False):
pygame.init()
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Snake - Dev Source")
font = pygame.font.Font(None, 28)
clock = pygame.time.Clock()
game = SnakeGame(seed=7 if autoplay else None)
best = 0
frames = 0
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.KEYDOWN:
if event.key in KEYS:
game.turn(KEYS[event.key])
elif event.key == pygame.K_SPACE and not game.alive:
game.reset()
elif event.key == pygame.K_ESCAPE:
running = False
if autoplay:
game.turn(auto_direction(game))
game.step()
best = max(best, game.score)
draw(screen, font, game, best)
clock.tick(FPS if not autoplay else 0)
frames += 1
if max_frames and frames >= max_frames:
running = False
pygame.quit()
return game.score, best
if __name__ == "__main__":
if "--demo" in sys.argv:
score, best = main(max_frames=300, autoplay=True)
print(f"Demo finished: score={score}, best={best}")
else:
main()
ایدههایی برای توسعه بازی
- افزایش سرعت: بعد از هر ۵ امتیاز مقدار FPS را یکی زیاد کنید تا بازی سختتر شود.
- ذخیره بهترین امتیاز: بهترین امتیاز را در فایل JSON نگه دارید تا با بستن بازی پاک نشود؛ روش آن را در کار با فایل JSON ببینید.
- عبور از دیوار: بهجای مردن، سر مار را با عملگر باقیمانده (%) به طرف دیگر صفحه منتقل کنید.
- صدا و تصویر: با
pygame.mixerصدای خوردن غذا اضافه کنید و بهجای مربع، تصویر سر مار را نمایش دهید. - مانع و مرحله: در هر مرحله چند دیوار ثابت داخل صفحه بگذارید.
خطاهای رایج و راه رفع
- ModuleNotFoundError: No module named pygame: Pygame در محیط فعلی نصب نیست؛ راهحل را در رفع خطای ModuleNotFoundError ببینید.
- پنجره «Not Responding» میشود: حلقه بازی باید در هر دور
pygame.event.get()را صدا بزند؛ در غیر این صورت سیستمعامل فکر میکند برنامه قفل شده است. - مار خیلی سریع حرکت میکند:
clock.tick(FPS)را فراموش کردهاید. - مار ناگهان میمیرد: احتمالاً چرخش ۱۸۰ درجه را کنترل نکردهاید یا برخورد را با کل لیست بدن، بهجای
snake[:-1]، بررسی میکنید. - IndexError یا غذا روی بدن: غذا را فقط از بین خانههای خالی انتخاب کنید. برای خطاهای فهرست، رفع خطای IndexError را ببینید.
سوالات متداول
برای ساخت بازی مار با پایتون چه کتابخانهای لازم است؟
رایجترین انتخاب Pygame است که با دستور pip install pygame نصب میشود. نسخههای سادهتری هم با turtle یا tkinter ساخته میشوند، اما Pygame برای بازیسازی امکانات بیشتری دارد.
بازی مار با پایتون برای مبتدیها مناسب است؟
بله. اگر با لیست، شرط، حلقه و تابع آشنا باشید، میتوانید آن را بسازید. این پروژه مفاهیم حلقه بازی، ورودی کیبورد و تشخیص برخورد را بهخوبی تمرین میدهد.
چطور سرعت مار را تغییر دهم؟
مقدار FPS در clock.tick تعداد حرکت مار در ثانیه را تعیین میکند. عدد بزرگتر یعنی مار سریعتر.
چرا مار با زدن کلید مخالف میمیرد و چطور جلویش را بگیرم؟
چون سر مار مستقیم به گردنش برخورد میکند. در متد turn اگر جمع جهت جدید و جهت فعلی صفر باشد، تغییر جهت را نادیده بگیرید.
آیا میتوانم این بازی را روی گوشی اجرا کنم؟
Pygame برای کامپیوتر طراحی شده است. برای اندروید میتوانید از ابزارهایی مثل Pydroid استفاده کنید؛ راهنمای آن را در آموزش اجرای پایتون روی گوشی ببینید.
چطور از بازی فایل اجرایی exe بسازم؟
با کتابخانه PyInstaller و دستور pyinstaller –onefile –noconsole snake.py یک فایل اجرایی ویندوزی ساخته میشود.
آموزشهای مرتبط
- ۳۰+ پروژه پایتون از مبتدی تا پیشرفته
- ساخت اولین بازی با Pygame
- ساخت بازی دوز با پایتون
- بازی حدس کلمه با پایتون
- اجرای پایتون روی گوشی
- کلاس و شیء در پایتون