برمجة بايثون 2026 — جامعة هلسنكي › الجزء 14: مشروع اللعبة 14 / 15 السابق التالي

إنهاء اللعبة

لعبةنا باتت وظيفية إلى حد كافٍ، حان الوقت لإضافة بعض اللمسات الأخيرة. سنضيف عدّادًا يعرض عدد الحركات التي نفّذها اللاعب، وخيارًا لبدء لعبة جديدة وإنهاء اللعبة بلوحة المفاتيح، وإشعارًا عند نجاح اللاعب في حلّ اللعبة.

عدّاد الحركات

عدّاد الحركات القريب من الحافة السفلية لنافذة اللعبة يعرض عدد الحركات التي نفّذها اللاعب حتى الآن. ويمكن استخدامه للوصول إلى الحل بأقل عدد ممكن من الحركات.

يتطلّب العدّاد بعض التعديلات على الشيفرة. أوّلًا، لنُعدِّل المُنشِئ (constructor) بحيث تكون هناك مساحة كافية للعدّاد، ولدينا خطّ مناسب متاح لرسم النصّ:

    def __init__(self):
        ...
        self.window = pygame.display.set_mode((window_width, window_height + self.scale))

        self.game_font = pygame.font.SysFont("Arial", 24)
        ...

يُهيَّأ عدّاد الحركات بالصفر في بداية اللعبة. وكلّ حركة تزيده بمقدار واحد:

    def new_game(self):
        ...
        self.moves = 0
    def move(self, move_y, move_x):
        ...
        self.moves += 1

في كلّ مرّة تُحدَّث فيها محتويات النافذة، ينبغي أن يُحدَّث أيضًا عدد الحركات المعروض على الشاشة:

    def draw_window(self):
        ...
        game_text = self.game_font.render("Moves: " + str(self.moves), True, (255, 0, 0))
        self.window.blit(game_text, (25, self.height * self.scale + 10))
        ...

لعبة جديدة وإنهاء اللعبة

الخطوة التالية، لنُضِف أوامر لوحة المفاتيح لبدء لعبة جديدة بمفتاح F2 ولإنهاء اللعبة بمفتاح Esc. كلاهما سهل التنفيذ إلى حدّ كبير:

    def check_events(self):
        ...
                if event.key == pygame.K_F2:
                    self.new_game()
                if event.key == pygame.K_ESCAPE:
                    exit()
        ...

نضيف أيضًا معلومة عن هذه الوظيفة ليراها اللاعب:

    def draw_window(self):
        ...
        game_text = self.game_font.render("F2 = new game", True, (255, 0, 0))
        self.window.blit(game_text, (200, self.height * self.scale + 10))

        game_text = self.game_font.render("Esc = exit game", True, (255, 0, 0))
        self.window.blit(game_text, (400, self.height * self.scale + 10))
        ...

حلّ اللعبة

يكون اللاعب قد حلّ اللعبة عندما تكون كلّ صندوق داخل إحدى المربّعات الهدف. والطريقة (method) التالية تتولّى التحقّق من ذلك:

    def game_solved(self):
        for y in range(self.height):
            for x in range(self.width):
                if self.map[y][x] in [2, 6]:
                    return False
        return True

تمرّ الطريقة على جميع مربّعات شبكة اللعبة. فإذا كان أيّ من المربّعات يحمل القيمة 2 (مربّع هدف فارغ) أو 6 (روبوت داخل مربّع هدف)، فلم تُحَل اللعبة بعد، ولذلك تُعيد الطريقة False. وإذا لم يكن هناك مربّع كهذا في الشبكة، فلا بدّ أن جميع المربّعات الهدف مشغولة بالصناديق، وقد حُلّت اللعبة، وتُعيد الطريقة True.

إذا حلّ اللاعب اللعبة، ينبغي أن نعرض رسالة مناسبة بوسيلة الطريقة draw_window:

    def draw_window(self):
        ...
        if self.game_solved():
            game_text = self.game_font.render("Congratulations, you solved the game!", True, (255, 0, 0))
            game_text_x = self.scale * self.width / 2 - game_text.get_width() / 2
            game_text_y = self.scale * self.height / 2 - game_text.get_height() / 2
            pygame.draw.rect(self.window, (0, 0, 0), (game_text_x, game_text_y, game_text.get_width(), game_text.get_height()))
            self.window.blit(game_text, (game_text_x, game_text_y))
        ...

ولإكمال الصورة، لنُعدِّل الطريقة move أيضًا بحيث لا يستطيع اللاعب التحرّك بعد حلّ اللعبة:

    def move(self, move_y, move_x):
        if self.game_solved():
            return
        ...

ولا يزال اللاعب قادرًا على رؤية شبكة اللعبة وحالتها النهائية.

تلميح للاختبار

عند تطوير الألعاب يحدث غالبًا أنّك ترغب في التحقّق عمّا سيحدث في مراحل لاحقة من اللعبة. وفي هذه اللعبة على سبيل المثال، تكون لحظة حلّ اللعبة من هذا النوع من المواقف.

قد يكون اختبار عمل موقق كهذا صعبًا، لأنك في العادة ستحتاج إلى حلّ اللعبة للوصول إلى تلك النقطة منها. وبصفتنا مبرمجين، يمكننا إدخال بعض التعديلات المؤقّتة في ألعابنا ليسهل اختبارها. فعلى سبيل المثال يمكننا إضافة ما يلي ليسهل حلّ اللعبة مؤقّتًا:

    def game_solved(self):
        return True

الآن تُعيد الطريقة دائمًا True، أي إنّ اللعبة «محلولة» منذ بدايتها. وهذا يُسهّل التحقّق من أنّ الإشعار في النهاية يبدو جيدًا، ومن أنّ اللاعب لم يعد قادرًا على التحرّك في الشبكة بعد الحلّ. وعندما يُختبر هذا الجزء اختبارًا كاملًا، يمكننا التراجع عن التعديلات.

لعبتك على GitHub؟

انتهت اللعبة الآن. وإن أردت طريقة سهلة للعب بالشيفرة والصور، يمكنك استراد الشيفرة المصدرية من GitHub:

وهو مكان شائع لأنواع عديدة من مشاريع البرمجة. ويمكنك استخدامه لتخزين الشيفرة المصدرية ومواد مشاريعك البرمجية الخاصة أيضًا، وعندها سيبقى برنامجك محفوظًا عبر نظام التحكّم في الإصدارات git، ويمكن مشاركته مع الآخرين بسهولة. وستصبح ملمًّا بـ git وGitHub إن واصلت الدراسة في دورات mooc.fi البرمجية الأخرى.

كم عدد الحركات المطلوبة؟

شبكة هذه اللعبة صغيرة، لكن اللعبة ليست سهلة إلى هذه الدرجة. التحدّي الأول هو ببساطة إنهاء اللعبة، أمّا المرحلة التالية فهي السعي إلى ذلك بأقلّ قدر ممكن من الحركات. فما أقصر طريق إلى الحلّ؟

البحث عن أقصر حلّ ممكن ليس مهمة سهلة على الإطلاق، لكنه يبقى ذا حلول حسابية أيضًا. وهي إحدى موضوعات مقرّر بنى البيانات والخوارزميات.