معرفی
بازی ۲۰۴۸ یک پازل عددی بسیار محبوب است. در این آموزش، قصد داریم این بازی را با استفاده از زبان برنامهنویسی پایتون و کتابخانه گرافیکی Pygame از صفر کدنویسی کنیم. هدف این پروژه، درک مفاهیم مدیریت رویدادها، پردازش ورودی کاربر، رندر گرافیکی و منطق ادغام کاشیها در یک بازی است.
نصب و پیشنیازها
برای شروع، نیاز دارید که پایتون (نسخه ۳ را توصیه میکنیم) بر روی سیستم شما نصب باشد. سپس باید کتابخانه مورد نیاز را از طریق خط فرمان نصب کنید:
pip install pygame
همچنین یک ویرایشگر کد مانند VS Code یا PyCharm برای راحتی کار مفید خواهد بود.
ساختار کلی کد
کد بازی از بخشهای اصلی زیر تشکیل شده است:
- مقداردهی اولیه Pygame و تعیین پارامترهای بازی (مانند ابعاد پنجره و نرخ فریم).
- تعریف کلاس
Tileبرای نمایش و مدیریت هر کاشی روی صفحه. - توابع کمکی برای رسم شبکه، تولید کاشیهای تصادفی و بهروزرسانی رابط کاربری.
- توابع اصلی برای پردازش حرکات (چپ، راست، بالا، پایین) و منطق ادغام کاشیها.
- تابع
mainکه حلقه اصلی بازی را اجرا میکند.
توضیح کلاس Tile
هر کاشی در بازی یک شیء از این کلاس است. ویژگیهای اصلی آن شامل مقدار عددی کاشی، موقعیت سطر و ستون آن و مختصات x و y برای رسم است. متد get_color بر اساس مقدار کاشی، رنگ مناسب را از یک لیست از پیش تعریف شده برمیگرداند. متد draw وظیفه رسم مستطیل کاشی و متن عدد داخل آن را برعهده دارد.
class Tile:
COLORS = [
(237, 229, 218),
(238, 225, 201),
(243, 178, 122),
# ... سایر رنگها
]
def __init__(self, value, row, col):
self.value = value
self.row = row
self.col = col
self.x = col * RECT_WIDTH
self.y = row * RECT_HEIGHT
def get_color(self):
color_index = int(math.log2(self.value)) - 1
color = self.COLORS[color_index]
return color
def draw(self, window):
color = self.get_color()
pygame.draw.rect(window, color, (self.x, self.y, RECT_WIDTH, RECT_HEIGHT))
text = FONT.render(str(self.value), 1, FONT_COLOR)
window.blit(text, (self.x + (RECT_WIDTH / 2 - text.get_width() / 2),
self.y + (RECT_HEIGHT / 2 - text.get_height() / 2)))
منطق حرکت و ادغام
عملکرد اصلی بازی در تابع move_tiles تعریف شده است. این تابع با توجه به جهت حرکت (چپ، راست، بالا، پایین)، کاشیها را مرتب کرده و آنها را به صورت پلهای حرکت میدهد. اگر دو کاشی با مقدار برابر در مسیر حرکت به هم برسند، با یکدیگر ادغام شده و مقدارشان دوبرابر میشود. تابع با استفاده از Lambda Functionها و Closureها، منطق خاص هر جهت را پیادهسازی میکند.
def move_tiles(window, tiles, clock, direction):
updated = True
blocks = set()
# ... تعریف پارامترهای ویژه هر جهت (sort_func, delta, boundary_check و ...)
while updated:
clock.tick(FPS)
updated = False
sorted_tiles = sorted(tiles.values(), key=sort_func, reverse=reverse)
for i, tile in enumerate(sorted_tiles):
# ... بررسی مرز، کاشی بعدی و شرایط ادغام یا حرکت
if not next_tile:
tile.move(delta)
elif (tile.value == next_tile.value and tile not in blocks and next_tile not in blocks):
if merge_check(tile, next_tile):
tile.move(delta)
else:
next_tile.value *= 2
sorted_tiles.pop(i)
blocks.add(next_tile)
elif move_check(tile, next_tile):
tile.move(delta)
else:
continue
tile.set_pos(ceil)
updated = True
update_tiles(window, tiles, sorted_tiles)
return end_move(tiles)
کد کامل بازی
در زیر کد کامل و یکپارچه پروژه قرار داده شده است. شما میتوانید این کد را در یک فایل با نام 2048.py ذخیره و اجرا کنید.
import pygame
import random
import math
pygame.init()
FPS = 60
WIDTH, HEIGHT = 800, 800
ROWS = 4
COLS = 4
RECT_HEIGHT = HEIGHT // ROWS
RECT_WIDTH = WIDTH // COLS
OUTLINE_COLOR = (187, 173, 160)
OUTLINE_THICKNESS = 10
BACKGROUND_COLOR = (205, 192, 180)
FONT_COLOR = (119, 110, 101)
FONT = pygame.font.SysFont("comicsans", 60, bold=True)
MOVE_VEL = 20
WINDOW = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("2048")
class Tile:
COLORS = [
(237, 229, 218),
(238, 225, 201),
(243, 178, 122),
(246, 150, 101),
(247, 124, 95),
(247, 95, 59),
(237, 208, 115),
(237, 204, 99),
(236, 202, 80),
]
def __init__(self, value, row, col):
self.value = value
self.row = row
self.col = col
self.x = col * RECT_WIDTH
self.y = row * RECT_HEIGHT
def get_color(self):
color_index = int(math.log2(self.value)) - 1
color = self.COLORS[color_index]
return color
def draw(self, window):
color = self.get_color()
pygame.draw.rect(window, color, (self.x, self.y, RECT_WIDTH, RECT_HEIGHT))
text = FONT.render(str(self.value), 1, FONT_COLOR)
window.blit(
text,
(
self.x + (RECT_WIDTH / 2 - text.get_width() / 2),
self.y + (RECT_HEIGHT / 2 - text.get_height() / 2),
),
)
def set_pos(self, ceil=False):
if ceil:
self.row = math.ceil(self.y / RECT_HEIGHT)
self.col = math.ceil(self.x / RECT_WIDTH)
else:
self.row = math.floor(self.y / RECT_HEIGHT)
self.col = math.floor(self.x / RECT_WIDTH)
def move(self, delta):
self.x += delta[0]
self.y += delta[1]
def draw_grid(window):
for row in range(1, ROWS):
y = row * RECT_HEIGHT
pygame.draw.line(window, OUTLINE_COLOR, (0, y), (WIDTH, y), OUTLINE_THICKNESS)
for col in range(1, COLS):
x = col * RECT_WIDTH
pygame.draw.line(window, OUTLINE_COLOR, (x, 0), (x, HEIGHT), OUTLINE_THICKNESS)
pygame.draw.rect(window, OUTLINE_COLOR, (0, 0, WIDTH, HEIGHT), OUTLINE_THICKNESS)
def draw(window, tiles):
window.fill(BACKGROUND_COLOR)
for tile in tiles.values():
tile.draw(window)
draw_grid(window)
pygame.display.update()
def get_random_pos(tiles):
row = None
col = None
while True:
row = random.randrange(0, ROWS)
col = random.randrange(0, COLS)
if f"{row}{col}" not in tiles:
break
return row, col
def move_tiles(window, tiles, clock, direction):
updated = True
blocks = set()
if direction == "left":
sort_func = lambda x: x.col
reverse = False
delta = (-MOVE_VEL, 0)
boundary_check = lambda tile: tile.col == 0
get_next_tile = lambda tile: tiles.get(f"{tile.row}{tile.col - 1}")
merge_check = lambda tile, next_tile: tile.x > next_tile.x + MOVE_VEL
move_check = (
lambda tile, next_tile: tile.x > next_tile.x + RECT_WIDTH + MOVE_VEL
)
ceil = True
elif direction == "right":
sort_func = lambda x: x.col
reverse = True
delta = (MOVE_VEL, 0)
boundary_check = lambda tile: tile.col == COLS - 1
get_next_tile = lambda tile: tiles.get(f"{tile.row}{tile.col + 1}")
merge_check = lambda tile, next_tile: tile.x < next_tile.x - MOVE_VEL
move_check = (
lambda tile, next_tile: tile.x + RECT_WIDTH + MOVE_VEL < next_tile.x
)
ceil = False
elif direction == "up":
sort_func = lambda x: x.row
reverse = False
delta = (0, -MOVE_VEL)
boundary_check = lambda tile: tile.row == 0
get_next_tile = lambda tile: tiles.get(f"{tile.row - 1}{tile.col}")
merge_check = lambda tile, next_tile: tile.y > next_tile.y + MOVE_VEL
move_check = (
lambda tile, next_tile: tile.y > next_tile.y + RECT_HEIGHT + MOVE_VEL
)
ceil = True
elif direction == "down":
sort_func = lambda x: x.row
reverse = True
delta = (0, MOVE_VEL)
boundary_check = lambda tile: tile.row == ROWS - 1
get_next_tile = lambda tile: tiles.get(f"{tile.row + 1}{tile.col}")
merge_check = lambda tile, next_tile: tile.y < next_tile.y - MOVE_VEL
move_check = (
lambda tile, next_tile: tile.y + RECT_HEIGHT + MOVE_VEL < next_tile.y
)
ceil = False
while updated:
clock.tick(FPS)
updated = False
sorted_tiles = sorted(tiles.values(), key=sort_func, reverse=reverse)
for i, tile in enumerate(sorted_tiles):
if boundary_check(tile):
continue
next_tile = get_next_tile(tile)
if not next_tile:
tile.move(delta)
elif (
tile.value == next_tile.value
and tile not in blocks
and next_tile not in blocks
):
if merge_check(tile, next_tile):
tile.move(delta)
else:
next_tile.value *= 2
sorted_tiles.pop(i)
blocks.add(next_tile)
elif move_check(tile, next_tile):
tile.move(delta)
else:
continue
tile.set_pos(ceil)
updated = True
update_tiles(window, tiles, sorted_tiles)
return end_move(tiles)
def end_move(tiles):
if len(tiles) == 16:
return "lost"
row, col = get_random_pos(tiles)
tiles[f"{row}{col}"] = Tile(random.choice([2, 4]), row, col)
return "continue"
def update_tiles(window, tiles, sorted_tiles):
tiles.clear()
for tile in sorted_tiles:
tiles[f"{tile.row}{tile.col}"] = tile
draw(window, tiles)
def generate_tiles():
tiles = {}
for _ in range(2):
row, col = get_random_pos(tiles)
tiles[f"{row}{col}"] = Tile(2, row, col)
return tiles
def main(window):
clock = pygame.time.Clock()
run = True
tiles = generate_tiles()
while run:
clock.tick(FPS)
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
break
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_LEFT:
move_tiles(window, tiles, clock, "left")
if event.key == pygame.K_RIGHT:
move_tiles(window, tiles, clock, "right")
if event.key == pygame.K_UP:
move_tiles(window, tiles, clock, "up")
if event.key == pygame.K_DOWN:
move_tiles(window, tiles, clock, "down")
draw(window, tiles)
pygame.quit()
if __name__ == "__main__":
main(WINDOW)
نحوه اجرا
پس از ذخیره کردن کد در یک فایل با پسوند .py، میتوانید آن را از طریق خط فرمان یا ترمینال اجرا کنید. کافیست در مسیر فایل، دستور زیر را وارد نمایید:
python 2048.py
پس از اجرا، پنجره بازی باز میشود. با استفاده از کلیدهای جهتدار صفحه کلید (بالا، پایین، چپ، راست) میتوانید کاشیها را حرکت دهید. هدف، رسیدن به کاشی ۲۰۴۸ است. در صورت پر شدن تمام خانهها و عدم امکان حرکت، بازی با پیام مناسبی پایان مییابد (که در این نسخه ساده، خروج از بازی است).
توسعه و بهبود پروژه
میتوانید قابلیتهای زیر را به این پروژه اضافه کنید تا بازی کاملتر و جذابتر شود:
- افزایش اندازه صفحه (مثلاً ۵ در ۵) و تغییر رنگها.
- اضافه کردن سیستم امتیازدهی (Score) برای هر ادغام.
- ذخیره بهترین امتیاز (High Score) در یک فایل.
- اضافه کردن منوی شروع بازی (Start Menu) و صفحه پایان بازی (Game Over).
- افزودن قابلیت بازی مجدد (Restart) بدون نیاز به بستن پنجره.
- بهینهسازی منطق حرکت و ادغام برای کارایی بهتر.
منابع و مستندات
- مستندات رسمی Pygame: pygame.org/docs