اذهب إلى المحتوى

برمجة لغز أبراج هانوي Hanoi Towers باستخدام لغة بايثون


Naser Dakhel

يستخدم لغز أبراج هانوي Hanoi towers مكدسًا stack من الأقراص ذات أحجام مختلفة، وتحتوي هذه الأقراص على ثقوب في مراكزها، لذا يمكنك وضعها على أحد الأعمدة الثلاثة (الشكل 1). لحل اللغز، يجب على اللاعب نقل مجموعة الأقراص إلى أحد القطبين الآخرين. هناك ثلاثة قيود، هي:

  1. يمكن للاعب تحريك قرص واحد فقط في كل مرة.
  2. يمكن للاعب تحريك الأقراص من وإلى قمة البرج فقط.
  3. لا يمكن للاعب أبدًا وضع قرص أكبر فوق قرص أصغر.

لعبة أبراج هانوي في بايثون

[الشكل 1: لعبة أبراج هانوي]

حل هذا اللغز هو مشكلة شائعة في علوم الحاسوب ويُستخدم لتدريس الخوارزميات التعاودية recursive algorithms. لن يحل برنامجنا هذا اللغز، بل سيقدِّم اللغز للاعب بشري لحلها. يمكنك الاطلاع على معلومات إضافية حول خوارزمية أبراج هانوي والمتوفرة على موسوعة حسوب.

خرج برنامج أبراج هانوي

يعرض برنامج أبراج هانوي الأبراج على شكل محارف آسكي ASCII باستخدام أحرف نصية لتمثيل الأقراص. قد يبدو هذا الأمر بدائيًا مقارنةً بالتطبيقات الحديثة، لكنه يبقي التطبيق بسيطًا، لأننا نحتاج فقط إلى استدعاءات print()‎‎ و input()‎‎ للتفاعل مع المستخدم. عند تشغيل البرنامج، سيبدو الخرج مشابهًا لما يلي:

THE TOWER OF HANOI, by Al Sweigart al@inventwithpython.com

Move the tower of disks, one disk at a time, to another tower. Larger
disks cannot rest on top of a smaller disk.

More info at https://en.wikipedia.org/wiki/Tower_of_Hanoi

     ||          ||          ||
    @_1@         ||          ||
   @@_2@@        ||          ||
  @@@_3@@@       ||          ||
 @@@@_4@@@@      ||          ||
@@@@@_5@@@@@     ||          ||
      A           B           C

Enter the letters of "from" and "to" towers, or QUIT.
(e.g., AB to move a disk from tower A to tower B.)

> AC
     ||          ||          ||
     ||          ||          ||
   @@_2@@        ||          ||
  @@@_3@@@       ||          ||
 @@@@_4@@@@      ||          ||
@@@@@_5@@@@@     ||         @_1@
      A           B           C

Enter the letters of "from" and "to" towers, or QUIT.
(e.g., AB to move a disk from tower A to tower B.)

--snip--

     ||          ||          ||
     ||          ||         @_1@
     ||          ||        @@_2@@
     ||          ||       @@@_3@@@
     ||          ||      @@@@_4@@@@
     ||          ||     @@@@@_5@@@@@
      A           B           C

You have solved the puzzle! Well done!

إذا كان عدد الأقراص n، يستغرق الأمر ما لا يقل عن 2n - 1 حركة لحل أبراج هانوي. يتطلب هذا البرج المكون من خمسة أقراص 31 خطوة كما يلي:

 AC, AB, CB, AC, BA, BC, AC, AB, CB, CA, BA, CB, AC, AB, CB, AC, BA, BC, AC, BA, CB, CA, BA, BC, AC, AB, CB, AC, BA, BC, AC.

إذا كنت تريد حل تحدٍ أكبر بنفسك، فيمكنك زيادة متغير TOTAL_DISKS في البرنامج من 5 إلى 6.

الشيفرة المصدرية

افتح ملفًا جديدًا في المحرر أو البيئة التطويرية IDE، وأدخل الشيفرة التالية، ثم احفظ الملف باسم towerofhanoi.py:

"""THE TOWER OF HANOI, by Al Sweigart al@inventwithpython.com
A stack-moving puzzle game."""

import copy
import sys

TOTAL_DISKS = 5  # إضافة المزيد من الأقراص يجعل اللعبة أصعب

# البدء بجميع الأقراص على البرج‫ A
SOLVED_TOWER = list(range(TOTAL_DISKS, 0, -1))


def main()‎:
    """تشغيل لعبة أبراج هانوي واحدة"""
    print(
        """THE TOWER OF HANOI, by Al Sweigart al@inventwithpython.com

Move the tower of disks, one disk at a time, to another tower. Larger
disks cannot rest on top of a smaller disk.

More info at https://wiki.hsoub.com/Algorithms/Towers_of_Hanoi
"""
    )

"""
‫يحتوي قاموس الأبراج على المفاتيح A و B و C وقيم كل من المفتاح هي قائمة تمثّل الأقراص الموجودة على البرج.
تحتوي القائمة على أعداد صحيحة تمثل الأقراص التي تكون من أحجام مختلفة، وبداية القائمة هي أسفل
 البرج. في حال لعبة تحتوي على 5 أقراص، تمثّل القائمة [5,4,3,2,1] البرج المكتمل بينما تمثل القائمة [] برجًا
 لا يحتوي على أقراص. القائمة [1,3] تحتوي على قرص كبير في الأعلى وقرص صغير بالأسفل، وبالتالي فهي
 حالة غير صالحة. القائمة [3,1] هي حالة صالحة بما أن القرص الصغير هو أعلى القرص الكبير‫
"""

    towers = {"A": copy.copy(SOLVED_TOWER), "B": []‎, "C": []‎}

    while True:  # حلقة لدور واحد لكل تكرار من هذه الحلقة
        # اعرض الأقراص والأبراج
        displayTowers(towers)

        # اطلب من المستخدم أن يُدخل حركة
        fromTower, toTower = getPlayerMove(towers)

        # ‫حرّك القرص الكبير من البرج fromTower إلى البرج toTower
        disk = towers[fromTower]‎.pop()‎
        towers[toTower]‎.append(disk)

        # تحقّق إذا حلّ المستخدم اللعبة
        if SOLVED_TOWER in (towers["B"]‎, towers["C"]‎):
            displayTowers(towers)  # اعرض الأبراج مرةً أخيرة
            print("You have solved the puzzle! Well done!")
            sys.exit()‎


def getPlayerMove(towers):
    """‫اطلب من المستخدم أن يُدخل حركة، وأعد القيمتين fromTower وtoTower"""

    while True:  # استمرّ بطلب إدخال حركة من المستخدم إلى أن يُدخل حركة صالحة
        print('Enter the letters of "from" and "to" towers, or QUIT.')
        print("(e.g., AB to move a disk from tower A to tower B.)")
        print()‎
        response = input("> ").upper()‎.strip()‎

        if response == "QUIT":
            print("Thanks for playing!")
            sys.exit()‎

        # تأكّد أن المستخدم أدخل أحرف لأبراج موجودة
        if response not in ("AB", "AC", "BA", "BC", "CA", "CB"):
            print("Enter one of AB, AC, BA, BC, CA, or CB.")
            continue  # اطلب حركة المستخدم مجددًا

        # استخدم أسماء متغيرات معبّرة
        fromTower, toTower = response[0]‎, response[1]‎

        if len(towers[fromTower]‎) == 0:
            # ‫ لا يجب أن يكون برج "from" فارغًا
            print("You selected a tower with no disks.")
            continue  # اطلب حركة من المستخدم مجددًا
        elif len(towers[toTower]‎) == 0:
            # يمكن لأي قرص أن يُحرّك إلى برج فارغ
            return fromTower, toTower
        elif towers[toTower]‎[-1]‎ < towers[fromTower]‎[-1]‎:
            print("Can't put larger disks on top of smaller ones.")
            continue  # اطلب حركة المستخدم مجددًا
        else:
            # حالة حركة صالحة؛ أعِد الأبراج المختارة
            return fromTower, toTower


def displayTowers(towers):
    """اطبع الأبراج الثلاث مع أقراصها"""

    # اطبع الأبراج الثلاثة
    for level in range(TOTAL_DISKS, -1, -1):
        for tower in (towers["A"]‎, towers["B"]‎, towers["C"]‎):
            if level >= len(tower):
                displayDisk(0)  # اعرض العمود الفارغ دون أقراص
            else:
                displayDisk(tower[level]‎)  # اعرض القرص
        print()‎

    # اعرض تسميات الأبراج
    emptySpace = " " * (TOTAL_DISKS)
    print("{0} A{0}{0} B{0}{0} C\n".format(emptySpace))


def displayDisk(width):
    """أظهِر القرص مع العرض المحدّد، العرض بقيمة 0 يعني عدم وجود قرص"""

    emptySpace = " " * (TOTAL_DISKS - width)

    if width == 0:
        # اطبع قسم العمود الذي لا يحتوي على قرص
        print(f"{emptySpace}||{emptySpace}", end="")
    else:
        # اطبع القرص
        disk = "@" * width
        numLabel = str(width).rjust(2, "_")
        print(f"{emptySpace}{disk}{numLabel}{disk}{emptySpace}", end="")


# اذا نُفّذ البرنامج (عوضًا عن استيراده)، ابدأ اللعبة
if __name__ == "__main__":
    main()‎

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

كتابة الشيفرة

دعنا نلقي نظرةً على الشيفرة المصدرية لنرى كيف تتبع أفضل الممارسات والأنماط التي وضحناها سابقًا.

نبدأ بالجزء العلوي من البرنامج:

"""THE TOWER OF HANOI, by Al Sweigart al@inventwithpython.com
A stack-moving puzzle game."""

يبدأ البرنامج بتعليق متعدد الأسطر يعمل مثل سلسلة توثيق نصية docstring للوحدة module المسماة towerofhanoi. ستستخدم دالة help()‎‎ المضمنة هذه المعلومات لوصف الوحدة:

>>> import towerofhanoi
>>> help(towerofhanoi)
Help on module towerofhanoi:

NAME
    towerofhanoi

DESCRIPTION
    THE TOWER OF HANOI, by Al Sweigart al@inventwithpython.com
    A stack-moving puzzle game.

FUNCTIONS
    displayDisk(width)
        Display a single disk of the given width.
--snip--

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

تأتي تعليمات import بعد سلسلة توثيق الوحدة:

import copy
import sys

يعمل منسّق السطور Black على تنسيق هذه التعليمات مثل سطور منفصلة بدلًا من سطر واحد مثل import copy, sys، وهذا يجعل إضافة أو إزالة الوحدات النمطية المستوردة أسهل عند التعامل مع أنظمة التحكم في الإصدار، مثل غيت Git، التي تتعقب التغييرات التي يجريها المبرمجون.

بعد ذلك، نعرّف الثوابت constants التي سيحتاجها هذا البرنامج:

TOTAL_DISKS = 5  # More disks means a more difficult puzzle.

# Start with all disks on tower A:
SOLVED_TOWER = list(range(TOTAL_DISKS, 0, -1))

نعرّف هذه الثوابت بالقرب من أعلى الملف لتجميعها معًا وجعلها متغيرات عامة. كتبنا أسماء الثوابت بأحرف كبيرة وبتنسيق نمط الثعبان snake_case لتمييزهم على أنهم ثوابت.

يشير الثابت TOTAL_DISKS إلى عدد الأقراص التي يحتوي عليها اللغز، أما المتغير SOLVED_TOWER فهو مثال عن قائمة تحتوي على برج محلول؛ إذ تحتوي على كل قرص بحيث يكون أكبر قرص في الأسفل وأصغر قرص في الأعلى. نولّد هذه القيمة من قيمة TOTAL_DISKS، أما بالنسبة للأقراص الخمسة فهي [1, 2, 3, 4, 5]‎.

لاحظ عدم وجود تلميحات حول الكتابة في هذا الملف، والسبب هو أنه يمكننا استنتاج أنواع جميع المتغيرات والمعاملات والقيم المُعادة من الشيفرة. على سبيل المثال، عيَّنا قيمة العدد الصحيح 5 للثابت TOTAL_DISKS، وبناءً على ذلك سيستنتج المدقق الكتابي، مثل Mypy، أن TOTAL_DISKS يجب أن يحتوي على أعداد صحيحة فقط.

نعرّف الدالة main()‎‎ التي يستدعيها البرنامج بالقرب من أسفل الملف:

def main():
    """Runs a single game of The Tower of Hanoi."""
    print(
        """THE TOWER OF HANOI, by Al Sweigart al@inventwithpython.com

Move the tower of disks, one disk at a time, to another tower. Larger
disks cannot rest on top of a smaller disk.

More info at https://wiki.hsoub.com/Algorithms/Towers_of_Hanoi
"""
    )

يمكن أن تحتوي الدوال على سلاسل توثيق نصية أيضًا. لاحظ سلاسل التوثيق للدالة main()‎‎ أسفل تعليمة def. يمكنك عرض هذا السلاسل عن طريق تنفيذ importofhanoi و help (towerofhanoi.main)‎ من الصدفة التفاعلية.

بعد ذلك، نكتب تعليقًا يصف مفصّلًا هيكل البيانات الذي نستخدمه لتمثيل البرج، لأنه يشكّل جوهر عمل هذا البرنامج:

    """The towers dictionary has keys "A", "B", and "C" and values
    that are lists representing a tower of disks. The list contains
    integers representing disks of different sizes, and the start of
    the list is the bottom of the tower. For a game with 5 disks,
    the list [5, 4, 3, 2, 1]‎ represents a completed tower. The blank
    list []‎ represents a tower of no disks. The list [1, 3]‎ has a
    larger disk on top of a smaller disk and is an invalid
    configuration. The list [3, 1]‎ is allowed since smaller disks
    can go on top of larger ones."""
    towers = {"A": copy.copy(SOLVED_TOWER), "B": []‎, "C": []‎}

نستخدم قائمة SOLVED_TOWER مثل مكدس stack وهو أحد أبسط هياكل البيانات في تطوير البرمجيات؛ فالمكدس هو قائمة مرتبة من القيم التي تتغير فقط من خلال إضافة (تسمى أيضًا الدفع Pushing) أو إزالة (تسمى أيضًا السحب Popping) القيم من أعلى المكدس. يمثل هيكل البيانات البرج في برنامجنا. يمكننا تحويل قائمة بايثون إلى مكدس إذا استخدمنا تابع append()‎‎ للدفع وتابع pop()‎‎ للسحب، وتجنبنا تغيير القائمة بأي طريقة أخرى. سنتعامل مع نهاية القائمة على أنها أعلى المكدس.

يمثل كل عدد صحيح في قائمة الأبراج قرصًا واحدًا بحجم معين. على سبيل المثال، في لعبة تحتوي على خمسة أقراص، تمثل القائمة [1, 2, 3, 4, 5]‎ مجموعةً كاملةً من الأقراص من الأكبر ( رقم 5) في الأسفل وصولًا إلى الأصغر (رقم 1) في الأعلى.

لاحظ أن تعليقنا يقدم أيضًا أمثلةً على كومة برج صالحة وغير صالحة.

نكتب داخل الدالة main()‎‎ حلقةً لا نهائية تُنفّذ لكل دور من جولة لعبة الألغاز الخاصة بنا:

 while True:  # Run a single turn on each iteration of this loop.
        # Display the towers and disks:
        displayTowers(towers)

        # Ask the user for a move:
        fromTower, toTower = getPlayerMove(towers)

        # Move the top disk from fromTower to toTower:
        disk = towers[fromTower]‎.pop()‎
        towers[toTower]‎.append(disk)

يرى اللاعب في كل دور حالة الأبراج ومن ثمّ يحدد الحركة التالية، ثم يحدّث البرنامج بعد ذلك بنية بيانات الأبراج. أخفينا تفاصيل هذه المهام في دالتي displayTowers()‎‎ و getPlayerMove()‎‎. يسمح اسما الدالتين السابقتين الوصفيّين للدالة main()‎‎ بتقديم نظرة عامة على ما يفعله البرنامج.

تتحقق الأسطر التالية مما إذا كان اللاعب قد حل اللغز من خلال مقارنة البرج الكامل في SOLVED_TOWER بالقيمتين towers["B"‎]‎‎ و towers["C"]‎‎:

     # Check if the user has solved the puzzle:
        if SOLVED_TOWER in (towers["B"]‎, towers["C"]‎):
            displayTowers(towers)  # Display the towers one last time.
            print("You have solved the puzzle! Well done!")
            sys.exit()‎

لا نقارن القيمة مع towers["A"]‎، لأن هذا العمود يبدأ ببرج مكتمل فعلًا؛ ويحتاج اللاعب إلى تشكيل البرج على العمودين B أو C لحل اللغز. لاحظ أننا نعيد استخدام SOLVED_TOWER لإنشاء أبراج البداية والتحقق فيما إذا كان اللاعب قد حل اللغز. نظرًا لأن SOLVED_TOWER ثابت، يمكننا الوثوق في أنه سيحظى دائمًا بالقيمة التي خصصناها له في بداية الشيفرة المصدرية.

الشرط الذي نستخدمه يعادل الصياغة التالية ولكنه أقصر منها:

SOLVED_TOWER == towers["B"]‎ or SOLVED_TOWER == towers["C"]‎

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

تطلب دالة getPlayerMove()‎ من اللاعب نقل القرص والتحقق من صحة هذه الخطوة بحسب قواعد اللعبة:

def getPlayerMove(towers):
    """Asks the player for a move. Returns (fromTower, toTower)."""
    while True:  # Keep asking player until they enter a valid move.
        print('Enter the letters of "from" and "to" towers, or QUIT.')
        print("(e.g., AB to move a disk from tower A to tower B.)")
        print()
        response = input("> ").upper().strip()‎

نبدأ حلقة لا نهائية تستمر في التكرار حتى تتسبب تعليمةreturn في أن يترك التنفيذ الحلقة والدالة، أو ينهي استدعاء sys.exit()‎ البرنامج. يطلب الجزء الأول من الحلقة من اللاعب إدخال حركة جديدة من خلال تحديد البرج الذي سينتقل منه القرص "from" إلى البرج الذي سينتقل القرص إليه "to".

لاحظ تعليمة ()input("> ").upper().strip التي تتلقى مدخلات لوحة المفاتيح من اللاعب، إذ تقبل (" <")input إدخال النص من اللاعب من خلال الرمز <، الذي يشير إلى أنه يجب على اللاعب إدخال شيء ما، وقد يعتقد اللاعب أن البرنامج قد توقف إذا لم يوجد هذا الرمز.

نستخدم تابع upper()‎ على السلسلة المُعادة من input()‎ لكي تُعيد صيغة الأحرف الكبيرة للسلسلة، ويسمح هذا للاعب بإدخال تسميات الأبراج بأحرف كبيرة أو صغيرة، مثل 'a' أو 'A' للبرج A، ثم يُستدعى تابع strip()‎ على السلسلة الكبيرة، وإعادة السلسلة دون أي مسافات فارغة على أي من الجانبين في حال أضاف المستخدم مسافة عن طريق الخطأ عند إدخال حركته. تجعل سهولة الاستخدام هذه برنامجنا أسهل قليلًا على اللاعبين لاستخدامه.

استمرارًا للدالة getPlayerMove()‎، نتحقق من الدخل الذي يدخله المستخدم:

    if response == "QUIT":
            print("Thanks for playing!")
            sys.exit()‎

        # Make sure the user entered valid tower letters:
        if response not in ("AB", "AC", "BA", "BC", "CA", "CB"):
            print("Enter one of AB, AC, BA, BC, CA, or CB.")
            continue  # Ask player again for their move.

إذا أدخل المستخدم QUIT (في أي حالة، وحتى مع وجود مسافات في بداية السلسلة النصية أو نهايتها، بسبب استدعاءات upper()‎ و strip()‎)، ينتهي البرنامج. كان من الممكن أن نجعل getPlayerMove()‎ تُعيد 'QUIT' للإشارة إلى أنه على االلاعب استدعاء sys.exit()‎ بدلًا من أن تستدعي الدالة getPlayerMove()‎ التابع sys.exit()‎، لكن هذا من شأنه أن يعقّد القيمة المُعادة للدالة getPlayerMove()‎: إذ سيعيد ذلك إما مجموعةً من سلسلتين (لتحرُّك اللاعب) أو سلسلة واحدة 'QUIT'.

الدالة التي تُعيد قيمًا من نوع بيانات واحد أسهل في الفهم من الدالة التي يمكنها إعادة قيم من العديد من الأنواع الممكنة. ناقشنا ذلك سابقًا في القسم "ينبغي على القيم المعادة أن تتضمن دوما نمط البيانات نفسه" من المقال البرمجة الوظيفية Functional Programming وتطبيقها في بايثون.

من الممكن فقط تكوين ست مجموعات من الأبراج بين الأبراج الثلاثة، وعلى الرغم من أننا وفرنا القيمة في الشيفرة لجميع القيم الست بصورةٍ ثابتة في الحالة التي تتحقق من الحركة، ستكون قراءة الشيفرة أسهل بكثير من قراءة شيء مثل:

len(response) != 2 or response[0]‎ not in 'ABC' or response[1]‎ not in 'ABC'or response[0]‎ == response[1]‎

في ظل هذه الظروف، يعدّ التوفير الثابت للقيم في الشيفرة هو النهج الأكثر وضوحًا.

يُعد توفير القيم في الشيفرة، مثل "AB" و"AC" وقيم أخرى على أنها قيم سحرية صالحة فقط طالما أن البرنامج يحتوي على ثلاثة أعمدة ممارسةً سيئةً عمومًا، ولكن على الرغم من أننا قد نرغب في تعديل عدد الأقراص عن طريق تغيير ثابت TOTAL_DISKS، فمن المستبعد جدًا أن نضيف المزيد من الأعمدة إلى اللعبة، فلا بأس بكتابة كل خطوة ممكنة على هذا النحو.

نُنشئ متغيرين جديدين fromTower و toTower مثل أسماء وصفية للبيانات، فهي لا تخدم غرضًا وظيفيًا، لكنها تجعل قراءة الشيفرة أسهل من قراءة response[0]‎‎ و response[1]‎‎:

  # Use more descriptive variable names:
        fromTower, toTower = response[0]‎, response[1]‎

بعد ذلك، نتحقق مما إذا كانت الأبراج المحددة تشكل حركةً صالحة أم لا:

 if len(towers[fromTower]‎) == 0:
            # The "from" tower cannot be an empty tower:
            print("You selected a tower with no disks.")
            continue  # Ask player again for their move.
        elif len(towers[toTower]‎) == 0:
            # Any disk can be moved onto an empty "to" tower:
            return fromTower, toTower
        elif towers[toTower]‎[-1]‎ < towers[fromTower]‎[-1]‎:
            print("Can't put larger disks on top of smaller ones.")
            continue  # Ask player again for their move.

إذا لم تكن الحركة صالحة، ستعيد عبارة continue التنفيذ إلى بداية الحلقة، التي تطلب من اللاعب إدخال حركته مرةً أخرى. لاحظ أننا نتحقق فيما إذا كان toTower فارغًا؛ فإذا كان الأمر كذلك، فإننا نعيد fromTower, toTower للتأكيد إلى أن عملية النقل كانت صالحة، لأنه يمكنك دائمًا وضع قرص على عمود فارغ. يضمن هذان الشرطان الأولان أنه بحلول الوقت الذي يجري فيه فحص الشرط الثالث، لن تكون towers[toTower]‎ و towers[fromTower]‎ فارغةً أو تتسبب بحدوث خطأ IndexError. لقد طلبنا هذه الشروط بطريقة تمنع IndexError أو أي فحص إضافي.

من المهم أن يتعامل برنامجك مع أي إدخال غير صالح من المستخدم أو حالات الخطأ المحتملة؛ فقد لا يعرف المستخدمون ماذا يدخلون، أو قد يخطئون في الكتابة، وبالمثل، قد تختفي الملفات بصورةٍ غير متوقعة، أو قد تتعطل قواعد البيانات. يجب أن تكون برامجك مرنة في مواجهة الحالات الاستثنائية، وإلا ستتعطل بصورةٍ غير متوقعة أو تتسبب في حدوث أخطاء دقيقة في وقت لاحق.

إذا لم يكن أي من الشروط السابقة صحيحًا، فإن الدالة getPlayerMove()‎ تُعيد fromTower, toTower:

     else:
            # This is a valid move, so return the selected towers:
            return fromTower, toTower

تُعيد عبارات return دائمًا قيمة واحدة في بلغة بايثون. على الرغم من أن عبارة return هذه تبدو وكأنها تُعيد قيمتين، إلا أن بايثون تُعيد فعليًا مجموعةً واحدةً من قيمتين، وهو ما يعادل return (fromTower, toTower)‎. يتجاهل مبرمجو بايثون الأقواس غالبًا في هذا السياق، إذ لا تعرّف الأقواس صفوفًا tuples كما تفعل الفواصل.

لاحظ أن البرنامج يستدعي دالة getPlayerMove()‎ مرةً واحدةً فقط من الدالة main()‎. لا تنقذنا الدالة من تكرار الشيفرة، وهو الغرض الأكثر شيوعًا لاستخدامها. لا يوجد سبب يمنعنا من وضع جميع الشيفرات في getPlayerMove()‎ في الدالة main()‎، ولكن يمكننا أيضًا استخدام الدوال مثل طريقة لتنظيم الشيفرة في وحدات منفصلة، وهذه هي الطريقة التي نستخدم بها getPlayerMove()‎، إذ يؤدي ذلك إلى منع دالة main()‎ من أن تصبح طويلة جدًا وغير عملية.

تعرض دالة displayTowers()‎ الأقراص الموجودة على الأبراج A و B و C في الوسيط towers:

def displayTowers(towers):
    """Display the three towers with their disks."""

    # Display the three towers:
    for level in range(TOTAL_DISKS, -1, -1):
        for tower in (towers["A"], towers["B"], towers["C"]):
            if level >= len(tower):
                displayDisk(0)  # Display the bare pole with no disk.
            else:
                displayDisk(tower[level])  # Display the disk.
        print()‎

تعتمد الدالة السابقة على دالة displayDisk()‎، التي سنغطيها تاليًا، لعرض كل قرص في البرج. تتحقق حلقة for level من كل قرص محتمل للبرج، وتتحقق حلقة for tower من الأبراج A و B و C.

تستدعي دالة displayTowers()‎ دالة displayDisk()‎ لعرض كل قرص باتساع width معين، أو العمود الذي لا يحتوي على قرص في حال تمرير 0:

    # Display the tower labels A, B, and C:
    emptySpace = ' ' * (TOTAL_DISKS)
    print('{0} A{0}{0} B{0}{0} C\n'.format(emptySpace))

تعرض الشيفرة السابقة الأبراج A و B و C على الشاشة. يحتاج اللاعب إلى هذه المعلومات للتمييز بين الأبراج ولتعزيز أن الأبراج تحمل علامات A و B و C بدلًا من 1 و2 و3 أو يسار ومتوسط ويمين. اخترنا عدم استخدام 1 و2 و3 لاسم البرج لمنع اللاعبين من الخلط بين هذه الأرقام والأرقام المستخدمة لأحجام الأقراص.

ضبطنا متغير emptySpace على عدد المسافات التي يجب وضعها بين كل تسمية، والتي بدورها تعتمد على TOTAL_DISKS، لأنه كلما زاد عدد الأقراص في اللعبة، كلما اتسعت المسافة بين العمودين. يمكننا استخدام تابع format()‎ بدلًا من سلسلة f النصية، فيما يلي:

print(f'{emptySpace} A{emptySpace}{emptySpace} B{emptySpace}{emptySpace} C\n')

يتيح لنا ذلك استخدام الوسيط emptySpace نفسه في أي مكان يظهر فيه {0} في السلسلة المعنية، مما ينتج عنه شيفرة أقصر وأكثر قابلية للقراءة من إصدار سلسلة f.

تعرض دالة displayDisk()‎ قرصًا واحدًا مع اتساعه، وفي حالة عدم وجود قرص، فإنه يعرض العمود فقط:

def displayDisk(width):
    """Display a disk of the given width. A width of 0 means no disk."""
    emptySpace = ' ' * (TOTAL_DISKS - width)
    if width == 0:
        # Display a pole segment without a disk:
        print(f'{emptySpace}||{emptySpace}', end='')
    else:
        # Display the disk:
        disk = '@' * width
        numLabel = str(width).rjust(2, '_')
        print(f"{emptySpace}{disk}{numLabel}{disk}{emptySpace}", end='')

نمثل هنا قرصًا يستخدم مساحةً فارغةً أولية، وعددًا من محارف "@" يساوي اتساع القرص، ومحرفين للاتساع (بما في ذلك شرطة سفلية إذا كان الاتساع رقمًا واحدًا)، وسلسلةً أخرى من محارف "@"، ثم مسافة فارغة لاحقة. لعرض العمود الفارغ فقط، كل ما نحتاجه هو المسافة الفارغة الأولية، وحرفي أنبوب pipe |، ومسافة فارغة لاحقة. نتيجةً لذلك، سنحتاج إلى ستة استدعاءات لعرض ()‎ displayDisk مع ستة وسطاء مختلفة للاتساع width للبرج التالي:

     ||
    @_1@
   @@_2@@
  @@@_3@@@
 @@@@_4@@@@
@@@@@_5@@@@@

لاحظ كيف تتقاسم دالتي displayTowers()‎ و displayDisk()‎ مسؤولية عرض الأبراج. على الرغم من أن displayTowers()‎ تقرر كيفية تفسير هياكل البيانات التي تمثل كل برج، إلا أنها تعتمد على displayDisk()‎ لعرض كل قرص في البرج فعليًا.

يؤدي تقسيم البرنامج إلى دوال أصغر مثل هذه إلى تسهيل اختبار كل جزء. إذا كان البرنامج يعرض الأقراص بشكلٍ غير صحيح، فمن المحتمل أن تكون المشكلة في displayDisk()‎؛ أما إذا ظهرت الأقراص بترتيب خاطئ، فمن المحتمل أن تكون المشكلة في displayTowers()‎، وفي كلتا الحالتين، سيكون قسم الشيفرة الذي يتعين عليك تصحيحه أصغر بكثير.

لاستدعاء الدالة main()‎، نستخدم دالة بايثون الشائعة:

# If this program was run (instead of imported), run the game:
if __name__ == '__main__':
    main()‎

تعيّن بايثون تلقائيًا المتغير __name__ إلى '__main__' إذا شغل اللاعب برنامج towerofhanoi.py مباشرةً، ولكن إذا استورد شخص ما البرنامج مثل وحدة باستخدام import towerofhanoi، سيُعيَّن __name__على 'towerofhanoi'.

سيستدعي السطر ‎if __name__ == '__main__' :‎ الدالة main()‎، إذا شغّل شخص ما برنامجنا، وبدأ لعبة برج هانوي، ولكن إذا أردنا ببساطة استيراد البرنامج مثل وحدة حتى نتمكن -على سبيل المثال- من استدعاء الدوال الفردية فيه لاختبار الوحدة، فسيكون هذا الشرط False ولن تُستدعى main()‎.

الخلاصة

نمثّل الأبراج الثلاثة في أبراج هانوي، مثل قاموس بمفاتيح 'A' و 'B' و 'C' وقيمها هي قوائم من الأعداد الصحيحة. ينجح هذا الأمر في برنامجنا ولكن إذا كان برنامجنا أكبر أو أكثر تعقيدًا، فسيكون من الجيد تمثيل هذه البيانات باستخدام الأصناف classes. لم نستخدم الأصناف وتقنيات البرمجة كائنية التوجه لأننا لم نناقش هذه المواضيع بعد، لكن ضع في الحسبان أنه من الجيد تمامًا استخدام صنف لهيكل البيانات هذا. تظهر الأبراج على أنها محارف آسكي ASCII على الشاشة، باستخدام أحرف نصية لإظهار كل قرص من الأبراج.

ترجمة -وبتصرف- لقسم من الفصل Practice Projects من كتاب Beyond the Basic Stuff with Python.

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...