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

برمجة لعبة أربع نقاط في صف واحد Four-in-a-Row باستخدام لغة بايثون


Naser Dakhel

لعبة أربع نقاط في صف واحد Four-in-a-Row هي لعبة للاعبين اثنين، إذ يضع كل منهما حجرًا، ويحاول كل لاعب إنشاء صف مكون من أربعة من حجراته، سواء أفقيًا أو رأسيًا أو قطريًا، وهي مشابهة للعبتَين Connect Four و Four Up. تستخدم اللعبة لوحة قياس 7×6، وتشغل المربعات أدنى مساحة شاغرة في العمود. في لعبتنا، سيلعب لاعبان بشريان، X و O، ضد بعضهما، وليس لاعب بشري واحد ضد الحاسوب.

خرج اللعبة

سيبدو الخرج كما يلي عند تنفيذ برنامج أربع نقاط في صف واحد:

Four-in-a-Row, by Al Sweigart al@inventwithpython.com

Two players take turns dropping tiles into one of seven columns, trying
to make four in a row horizontally, vertically, or diagonally.


     1234567
    +-------+
    |.......|
    |.......|
    |.......|
    |.......|
    |.......|
    |.......|
    +-------+
Player X, enter 1 to 7 or QUIT:
> 1

     1234567
    +-------+
    |.......|
    |.......|
    |.......|
    |.......|
    |.......|
    |X......|
    +-------+
Player O, enter 1 to 7 or QUIT:
--snip--
Player O, enter 1 to 7 or QUIT:
> 4

     1234567
    +-------+
    |.......|
    |.......|
    |...O...|
    |X.OO...|
    |X.XO...|
    |XOXO..X|
    +-------+
Player O has won!

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

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

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

"""Four-in-a-Row, by Al Sweigart al@inventwithpython.com
A tile-dropping game to get four-in-a-row, similar to Connect Four."""

import sys

# الثوابت المستخدمة لعرض اللوحة
EMPTY_SPACE = "."  # النقطة أسهل للعدّ والرؤية من المسافة
PLAYER_X = "X"
PLAYER_O = "O"

# ‎‫ملاحظة: عدّل قيمتي BOARD_TEMPLATE و COLUMN_LAVELS إذا تغيّر BOARD_WIDTH
BOARD_WIDTH = 7
BOARD_HEIGHT = 6
COLUMN_LABELS = ("1", "2", "3", "4", "5", "6", "7")
assert len(COLUMN_LABELS) == BOARD_WIDTH

# قالب السلسلة النصية الذي يُستخدم لطباعة اللوحة
BOARD_TEMPLATE = """
     1234567
    +-------+
    |{}{}{}{}{}{}{}|
    |{}{}{}{}{}{}{}|
    |{}{}{}{}{}{}{}|
    |{}{}{}{}{}{}{}|
    |{}{}{}{}{}{}{}|
    |{}{}{}{}{}{}{}|
    +-------+"""


def main()‎:
    """Runs a single game of Four-in-a-Row."""
    print(
        """Four-in-a-Row, by Al Sweigart al@inventwithpython.com

Two players take turns dropping tiles into one of seven columns, trying
to make Four-in-a-Row horizontally, vertically, or diagonally.
"""
    )

    # إعداد لعبة جديدة
    gameBoard = getNewBoard()‎
    playerTurn = PLAYER_X

    while True:  # بدء دور اللاعب
        # عرض اللوحة قبل الحصول على حركة اللاعب
        displayBoard(gameBoard)
        playerMove = getPlayerMove(playerTurn, gameBoard)
        gameBoard[playerMove]‎ = playerTurn

        # فحص حالة الفوز أو التعادل
        if isWinner(playerTurn, gameBoard):
            displayBoard(gameBoard)  # عرض اللوحة لمرة أخيرة
            print("Player {} has won!".format(playerTurn))
            sys.exit()‎
        elif isFull(gameBoard):
            displayBoard(gameBoard)  # عرض اللوحة لمرة أخيرة 
            print("There is a tie!")
            sys.exit()‎

        # تبديل الدور للاعب الآخر
        if playerTurn == PLAYER_X:
            playerTurn = PLAYER_O
        elif playerTurn == PLAYER_O:
            playerTurn = PLAYER_X


def getNewBoard()‎:
    """Returns a dictionary that represents a Four-in-a-Row board.

    The keys are (columnIndex, rowIndex) tuples of two integers, and the
    values are one of the "X", "O" or "." (empty space) strings."""
    board = {}
    for rowIndex in range(BOARD_HEIGHT):
        for columnIndex in range(BOARD_WIDTH):
            board[(columnIndex, rowIndex)]‎ = EMPTY_SPACE
    return board


def displayBoard(board):
    """Display the board and its tiles on the screen."""

    # ‫تحضير قائمة لتمريرها إلى تابع format()‎ لقالب اللوحة
    # تحتوي القائمة على خلايا اللوحة بما في ذلك المسافات الفارغة
    # من اليسار إلى اليمين ومن الأعلى للأسفل
    tileChars = []‎
    for rowIndex in range(BOARD_HEIGHT):
        for columnIndex in range(BOARD_WIDTH):
            tileChars.append(board[(columnIndex, rowIndex)]‎)

    # عرض اللوحة
    print(BOARD_TEMPLATE.format(*tileChars))


def getPlayerMove(playerTile, board):
    """Let a player select a column on the board to drop a tile into.

    Returns a tuple of the (column, row) that the tile falls into."""
    while True:  # استمر بسؤال اللاعب إلى أن يُدخل حركة صالحة
        print(f"Player {playerTile}, enter 1 to {BOARD_WIDTH} or QUIT:")
        response = input("> ").upper()‎.strip()‎

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

        if response not in COLUMN_LABELS:
            print(f"Enter a number from 1 to {BOARD_WIDTH}.")
            continue  # اطلب حركة من اللاعب مجددًا

        columnIndex = int(response) - 1  # نطرح واحد للحصول على فهرس يبدأ من الصفر

        # إذا كان العمود مليئًا، نطلب من اللاعب حركة مجددًا
        if board[(columnIndex, 0)]‎ != EMPTY_SPACE:
            print("That column is full, select another one.")
            continue  # اطلب حركة من اللاعب مجددًا

        # البدء من الأسفل واختيار أول خلية فارغة
        for rowIndex in range(BOARD_HEIGHT - 1, -1, -1):
            if board[(columnIndex, rowIndex)]‎ == EMPTY_SPACE:
                return (columnIndex, rowIndex)


def isFull(board):
    """Returns True if the `board` has no empty spaces, otherwise
    returns False."""
    for rowIndex in range(BOARD_HEIGHT):
        for columnIndex in range(BOARD_WIDTH):
            if board[(columnIndex, rowIndex)]‎ == EMPTY_SPACE:
                return False  # أعد‫ False إذا عُثر على مسافة فارغة
    return True  # في حال كانت جميع الخلايا ممتلئة


def isWinner(playerTile, board):
    """Returns True if `playerTile` has four tiles in a row on `board`,
    otherwise returns False."""

    # تفقّد اللوحة بكاملها بحثًا عن حالة فوز
    for columnIndex in range(BOARD_WIDTH - 3):
        for rowIndex in range(BOARD_HEIGHT):
            # التحقق من حالة الفوز بالذهاب لليمين
            tile1 = board[(columnIndex, rowIndex)]‎
            tile2 = board[(columnIndex + 1, rowIndex)]‎
            tile3 = board[(columnIndex + 2, rowIndex)]‎
            tile4 = board[(columnIndex + 3, rowIndex)]‎
            if tile1 == tile2 == tile3 == tile4 == playerTile:
                return True

    for columnIndex in range(BOARD_WIDTH):
        for rowIndex in range(BOARD_HEIGHT - 3):
            # التحقق من حالة فوز بالذهاب للأسفل
            tile1 = board[(columnIndex, rowIndex)]‎
            tile2 = board[(columnIndex, rowIndex + 1)]‎
            tile3 = board[(columnIndex, rowIndex + 2)]‎
            tile4 = board[(columnIndex, rowIndex + 3)]‎
            if tile1 == tile2 == tile3 == tile4 == playerTile:
                return True

    for columnIndex in range(BOARD_WIDTH - 3):
        for rowIndex in range(BOARD_HEIGHT - 3):
            # التحقق من حالة فوز بالذهاب قطريًا إلى اليمين والأسفل
            tile1 = board[(columnIndex, rowIndex)]‎
            tile2 = board[(columnIndex + 1, rowIndex + 1)]‎
            tile3 = board[(columnIndex + 2, rowIndex + 2)]‎
            tile4 = board[(columnIndex + 3, rowIndex + 3)]‎
            if tile1 == tile2 == tile3 == tile4 == playerTile:
                return True

            # التحقق من حالة فوز بالذهاب قطريًا إلى اليسار والأسفل
            tile1 = board[(columnIndex + 3, rowIndex)]‎
            tile2 = board[(columnIndex + 2, rowIndex + 1)]‎
            tile3 = board[(columnIndex + 1, rowIndex + 2)]‎
            tile4 = board[(columnIndex, rowIndex + 3)]‎
            if tile1 == tile2 == tile3 == tile4 == playerTile:
                return True
    return False


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

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

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

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

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

"""Four-in-a-Row, by Al Sweigart al@inventwithpython.com
A tile-dropping game to get four-in-a-row, similar to Connect Four."""

import sys

# Constants used for displaying the board:
EMPTY_SPACE = "."  # A period is easier to count than a space.
PLAYER_X = "X"
PLAYER_O = "O"

نبدأ البرنامج بسلسلة توثيق نصية docstring واستيراد للوحدات module، وتعيين للثوابت. كما فعلنا في برنامج برج هانوي. نعرّف الثابتَين PLAYER_X و PLAYER_O بحيث نبتعد عن استخدام سلاسل "X" و "O" ضمن البرنامج، مما يسهل اكتشاف الأخطاء. على سبيل المثال، سنحصل على استثناء NameError إذا أخطأنا بكتابة اسم الثابت، مثل كتابة PLAYER_XX مما يشير فورًا إلى المشكلة، ولكن إذا ارتكبنا خطأً كتابيًا باستخدام الحرف "X"، مثل "XX" أو "Z"، فقد لا يكون الخطأ الناتج واضحًا فورًا. كما هو موضح في قسم "الأرقام السحرية" من مقال اكتشاف دلالات الأخطاء في شيفرات لغة بايثون، فإن استخدام الثوابت بدلًا من قيمة السلسلة لا يمثل الوصف فحسب، بل يوفر أيضًا تحذير مبكر لأي أخطاء كتابية في الشيفرة المصدرية.

ينبغي ألا تتغير الثوابت أثناء تشغيل البرنامج، لكن يمكن للمبرمج تحديث قيمهم في الإصدارات المستقبلية من البرنامج. لهذا السبب، نقدم ملاحظةً تخبر المبرمجين بضرورة تحديث ثابتي BOARD_TEMPLATE و COLUMN_LABELS، إذا غيروا قيمة BOARD_WIDTH:

# Note: Update BOARD_TEMPLATE & COLUMN_LABELS if BOARD_WIDTH is changed.
BOARD_WIDTH = 7
BOARD_HEIGHT = 6

بعد ذلك، ننشئ ثابت COLUMN_LABELS:

COLUMN_LABELS = ("1", "2", "3", "4", "5", "6", "7")
assert len(COLUMN_LABELS) == BOARD_WIDTH

سنستخدم هذا الثابت لاحقًا للتأكد من أن اللاعب يختار عمودًا صالحًا. لاحظ أنه في حالة تعيين BOARD_WIDTH على أي قيمة أخرى بخلاف 7، فسنضطر إلى إضافة تسميات labels إلى مجموعة tuple تدعى COLUMN_LABELS أو إزالتها منها. كان بإمكاننا تجنب ذلك من خلال إنشاء قيمة COLUMN_LABELS بناءً على BOARD_WIDTH بشيفرة مثل هذه:

COLUMN_LABELS = tuple ([str (n) for n in range (1، BOARD_WIDTH + 1)]‎)

لكن من غير المرجح أن يتغير COLUMN_LABELS في المستقبل، لأن لعبة أربع نقاط في صف واحد تربح تُلعب على لوحة 7×6، لذلك قررنا كتابة قيمة صريحة للمجموعة.

بالتأكيد، تمثّل هذه الشيفرة شيفرة ذات رائحة smell code (وهي نمط شيفرة يشير إلى أخطاء محتملة)، ولكنها أكثر قابلية للقراءة من بديلها. تحذرنا تعليمة assert من تغيير BOARD_WIDTH بدون تحديث COLUMN_LABELS.

كما هو الحال مع برج هانوي، يستخدم برنامج أربع في صف واحد تربح محارف آسكي ASCII لرسم لوحة اللعبة. تمثّل الأسطر التالية تعليمة إسناد واحدة بسلسلة نصية متعددة الأسطر:

# The template string for displaying the board:
BOARD_TEMPLATE = """
     1234567
    +-------+
    |{}{}{}{}{}{}{}|
    |{}{}{}{}{}{}{}|
    |{}{}{}{}{}{}{}|
    |{}{}{}{}{}{}{}|
    |{}{}{}{}{}{}{}|
    |{}{}{}{}{}{}{}|
    +-------+"""

تحتوي هذه السلسلة على أقواس معقوصة braces {} يحل محلها سلسلة باستخدام التابع format()‎. (ستعمل دالة displayBoard()‎، التي ستُشرح لاحقًا، على تحقيق هذا.) نظرًا لأن اللوحة تتكون من سبعة أعمدة وستة صفوف، فإننا نستخدم سبعة أزواج من القوسين {} في كل من الصفوف الستة لتمثيل كل فتحة. لاحظ أنه تمامًا مثل COLUMN_LABELS، فإننا نشفّر من الناحية الفنية اللوحة لإنشاء عدد محدد من الأعمدة والصفوف. إذا غيرنا BOARD_WIDTH أو BOARD_HEIGHT إلى أعداد صحيحة جديدة، فسنضطر أيضًا إلى تحديث السلسلة متعددة الأسطر في BOARD_TEMPLATE.

كان بإمكاننا كتابة شيفرة لإنشاء BOARD_TEMPLATE استنادًا إلى الثابتين BOARD_WIDTH و BOARD_HEIGHT، مثل:

BOARD_EDGE = "    +" + ("-" * BOARD_WIDTH) + "+"
BOARD_ROW = "    |" + ("{}" * BOARD_WIDTH) + "|\n"
BOARD_TEMPLATE = "\n     " + "".join(COLUMN_LABELS) + "\n" + BOARD_EDGE + "\n" + (BOARD_ROW * BOARD_HEIGHT) + BOARD_EDGE

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

نبدأ بكتابة الدالة main()‎ التي ستستدعي جميع الدوال الأخرى التي أنشأناها لهذه اللعبة:

def main()‎:
    """Runs a single game of Four-in-a-Row."""
    print(
        """Four-in-a-Row, by Al Sweigart al@inventwithpython.com

Two players take turns dropping tiles into one of seven columns, trying
to make four-in-a-row horizontally, vertically, or diagonally.
"""
    )

    # Set up a new game:
    gameBoard = getNewBoard()‎
    playerTurn = PLAYER_X

نعطي الدالة main()‎ سلسلة توثيق نصية، قابلة للعرض viewable باستخدام دالة help()‎ المضمنة. تُعِد الدالة main()‎ أيضًا لوحة اللعبة للعبة جديدة وتختار اللاعب الأول.

تحتوي الدالة main()‎ حلقة لا نهائية:

    while True:  # Run a player's turn.
        # Display the board and get player's move:
        displayBoard(gameBoard)
        playerMove = getPlayerMove(playerTurn, gameBoard)
        gameBoard[playerMove]‎ = playerTurn

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

بعد ذلك، نقيم نتائج حركة اللاعب:

 # Check for a win or tie:
        if isWinner(playerTurn, gameBoard):
            displayBoard(gameBoard)  # Display the board one last time.
            print("Player {} has won!".format(playerTurn))
            sys.exit()‎
        elif isFull(gameBoard):
            displayBoard(gameBoard)  # Display the board one last time.
            print("There is a tie!")
            sys.exit()‎

إذا أدّت حركة اللاعب لفوزه، ستعيد الدالة isWinner()‎ القيمة True وتنتهي اللعبة؛ بينما إذا ملأ اللاعب اللوحة ولم يكن هناك فائز، ستعيد الدالة isFull()‎ القيمة True وتنتهي اللعبة. لاحظ أنه بدلًا من استدعاء sys.exit()‎، كان بإمكاننا استخدام تعليمة break بسيطة. كان من الممكن أن يتسبب هذا في انقطاع التنفيذ عن حلقة while، ولأنه لا يوجد شيفرة برمجية في الدالة main()‎ بعد هذه الحلقة، ستعود الدالة إلى استدعاء main()‎ في الجزء السفلي من البرنامج، مما يتسبب في إنهاء البرنامج، لكننا اخترنا استخدام sys.exit()‎ للتوضيح للمبرمجين الذين يقرؤون الشيفرة أن البرنامج سينتهي فورًا.

إذا لم تنته اللعبة، تُعِد الأسطر التالية playerTurn للاعب الآخر:

       # Switch turns to other player:
        if playerTurn == PLAYER_X:
            playerTurn = PLAYER_O
        elif playerTurn == PLAYER_O:
            playerTurn = PLAYER_X

لاحظ أنه كان بإمكاننا تحويل تعليمة elif إلى تعليمة else بسيطة دون شرط، لكن تذكر أن ممارسات بايثون الفُضلى تنص على "الصراحة أفضل من الضمنية explicit is better than implicit". تنص هذه الشيفرة صراحةً على أنه إذا جاء دور اللاعب O الآن، فسيكون دور اللاعب X هو التالي. ستنص الشيفرة البديلة على أنه إذا لم يكن دور اللاعب X الآن، فسيكون دور اللاعب X التالي.

على الرغم من أن دوال if و else تتناسب بصورةٍ طبيعية مع الشروط المنطقية، لا تتطابق قيمتا PLAYER_X و PLAYER_O مع True وقيمة False: not PLAYER_X ليست PLAYER_O. لذلك، من المفيد أن تكون مباشرًا عند التحقق من قيمة playerTurn.

بدلًا من ذلك، كان بإمكاننا تنفيذ جميع الإجراءات في سطر واحد:

playerTurn = {PLAYER_X: PLAYER_O, PLAYER_O: PLAYER_X}[ playerTurn]‎

يستخدم هذا السطر خدعة القاموس المذكورة في قسم "استخدام القواميس بدلا من العبارة Switch" في مقال الطرق البايثونية في استخدام قواميس بايثون ومتغيراتها وعاملها الثلاثي، ولكن مثل العديد من الأسطر الفردية، فهي غير سهلة القراءة مقارنةً بعبارة if و elif المباشرة.

بعد ذلك، نعرّف الدالة getNewBoard()‎:

def getNewBoard():
    """Returns a dictionary that represents a Four-in-a-Row board.

    The keys are (columnIndex, rowIndex) tuples of two integers, and the
    values are one of the "X", "O" or "." (empty space) strings."""
    board = {}
    for rowIndex in range(BOARD_HEIGHT):
        for columnIndex in range(BOARD_WIDTH):
            board[(columnIndex, rowIndex)] = EMPTY_SPACE
    return board

تُعيد هذه الدالة قاموسًا يمثل لوحة أربع نقاط في صف واحد تربح؛ إذ يحتوي هذا القاموس على مجموعات (indexIndex و rowIndex) للمفاتيح (يمثّل العمود indexIndex و rowIndex أعدادًا صحيحة) و "X" أو "O" أو "." حرف الحجر في كل مكان على اللوحة. تُخزَّن هذه السلاسل في PLAYER_X و PLAYER_O و EMPTY_SPACE على التوالي.

لعبة أربع نقاط في صف واحد تربح الخاصة بنا بسيطة نوعًا ما، لذا يُعد استخدام قاموس لتمثيل لوحة اللعبة أسلوبًا مناسبًا. ومع ذلك، كان بإمكاننا استخدام نهج كائني التوجه object-oriented بدلًا من ذلك. سنتعرف على البرمجة كائنية التوجه في الفصول القادمة.

تأخذ دالة displayBoard()‎ بنية بيانات لوحة اللعبة من أجل الوسيط board وتعرض اللوحة على الشاشة باستخدام ثابت BOARD_TEMPLATE:

def displayBoard(board):
    """Display the board and its tiles on the screen."""

    # Prepare a list to pass to the format() string method for the board
    # template. The list holds all of the board's tiles (and empty
    # spaces) going left to right, top to bottom:
    tileChars = []

تذكر أن BOARD_TEMPLATE هي سلسلة متعددة الأسطر بها عدة أزواج من الأقواس. عند استدعاء دالة format()‎ على BOARD_TEMPLATE، ستُستبدل هذه الأقواس بقيم الوسطاء الممرّرة إلى format()‎.

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

    for rowIndex in range(BOARD_HEIGHT):
        for columnIndex in range(BOARD_WIDTH):
            tileChars.append(board[(columnIndex, rowIndex)]‎)

    # Display the board:
    print(BOARD_TEMPLATE.format(*tileChars))

تتكرر حلقات for المتداخلة هذه على كل صف وعمود محتملين على اللوحة، لتلحقهم بالقائمة في tileChars. بمجرد الانتهاء من هذه الحلقات، نمرر القيم الموجودة في قائمة tileChars بصورةٍ مفردة إلى التابع format()‎ باستخدام محرف النجمة * في البادئة.

يشرح قسم "استخدام * لإنشاء دوال مرنة" من مقال كتابة دوال فعالة في بايثون كيفية استخدام رمز النجمة للتعامل مع القيم الموجودة في قائمة مثل وسطاء دالة منفصلة، إذ تعادل الشيفرة print(*['cat', 'dog', 'rat']‎)‎ الشيفرة print('cat', 'dog', 'rat')‎.

نحتاج إلى النجمة لأن التابع format()‎ يتوقع وسيطًا واحدًا لكل زوج من الأقواس، وليس وسيطًا واحدًا للقائمة الواحدة. بعد ذلك، نكتب دالة getPlayerMove()‎:

def getPlayerMove(playerTile, board):
    """Let a player select a column on the board to drop a tile into.

    Returns a tuple of the (column, row) that the tile falls into."""
    while True:  # Keep asking player until they enter a valid move.
        print(f"Player {playerTile}, enter 1 to {BOARD_WIDTH} or QUIT:")
        response = input("> ").upper().strip()

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

تبدأ الدالة بحلقة لا نهائية تنتظر أن يدخل اللاعب نقلة move صحيحة. تشبه هذه الشيفرة دالة getPlayerMove()‎ في برنامج برج هانوي سابقًا. لاحظ أن استدعاء print()‎ في بداية حلقة while loop يستخدم سلسلة نصية من النوع f، لذا لا يتعين علينا تغيير الرسالة إذا حدثنا BOARD_WIDTH.

نتحقق من أن رد اللاعب هو عمود صالح؛ إذا لم يكن كذلك، تنقل دالة continue التنفيذ مرةً أخرى إلى بداية الحلقة لتطلب من اللاعب نقلة صحيحة:

       if response not in COLUMN_LABELS:
            print(f"Enter a number from 1 to {BOARD_WIDTH}.")
            continue  # Ask player again for their move.

كان من الممكن كتابة شرط التحقق من صحة الإدخال هذا على شكل:

not response.isdecimal()‎ or spam < 1 or spam > BOARD_WIDTH

ولكن من الأسهل استخدام response not in COLUMN_LABELS.

بعد ذلك، نحتاج إلى معرفة الصف الذي ستصل إليه الحجرة التي سقطت في العمود المحدد للاعب:

columnIndex = int(response) - 1  # -1 for 0-based column indexes.

        # If the column is full, ask for a move again:
        if board[(columnIndex, 0)]‎ != EMPTY_SPACE:
            print("That column is full, select another one.")
            continue  # Ask player again for their move.

تعرض اللوحة تسميات الأعمدة من 1 إلى 7 على الشاشة، بينما تستخدم فهارس (indexIndex، rowIndex) على اللوحة الفهرسة المستندة إلى 0، لذا فهي تتراوح من 0 إلى 6. لحل هذا التناقض، نحوّل قيم السلسلة '1' إلى '7' إلى القيم الصحيحة من 0 إلى 6.

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

     # Starting from the bottom, find the first empty space.
        for rowIndex in range(BOARD_HEIGHT - 1, -1, -1):
            if board[(columnIndex, rowIndex)]‎ == EMPTY_SPACE:
                return (columnIndex, rowIndex)

تبدأ حلقة for من فهرس الصف السفلي، BOARD_HEIGHT - 1 أو 6، وتتحرك لأعلى حتى تعثر على أول مساحة فارغة. تُعيد الدالة بعد ذلك فهارس أدنى مساحة فارغة.

في أي وقت تكون اللوحة ممتلئة، تنتهي اللعبة بالتعادل:

def isFull(board):
    """Returns True if the `board` has no empty spaces, otherwise
    returns False."""
    for rowIndex in range(BOARD_HEIGHT):
        for columnIndex in range(BOARD_WIDTH):
            if board[(columnIndex, rowIndex)] == EMPTY_SPACE:
                return False  # Found an empty space, so return False.
    return True  # All spaces are full.

تستخدم الدالة isFull()‎ زوجًا من حلقات for المتداخلة للمرور على كل مكان على اللوحة، وإذا عثرت على مساحة فارغة واحدة، فإن اللوحة ليست ممتلئة، وبالتالي تُعيد الدالة False. إذا نجح التنفيذ في المرور عبر كلتا الحلقتين، فإن الدالة isFull()‎ لم تعثر على مساحة فارغة، لذا فإنها تعيد True.

تتحقق دالة isWinner()‎ ما إذا كان اللاعب قد فاز باللعبة أم لا:

def isWinner(playerTile, board):
    """Returns True if `playerTile` has four tiles in a row on `board`,
    otherwise returns False."""

    # Go through the entire board, checking for four-in-a-row:
    for columnIndex in range(BOARD_WIDTH - 3):
        for rowIndex in range(BOARD_HEIGHT):
            # Check for four-in-a-row going across to the right:
            tile1 = board[(columnIndex, rowIndex)]
            tile2 = board[(columnIndex + 1, rowIndex)]
            tile3 = board[(columnIndex + 2, rowIndex)]
            tile4 = board[(columnIndex + 3, rowIndex)]
            if tile1 == tile2 == tile3 == tile4 == playerTile:
                return True

تُعيد هذه الدالة True إذا ظهر playerTile أربع مرات على التوالي أفقيًا أو رأسيًا أو قطريًا. لمعرفة استيفاء الشرط، يتعين علينا التحقق من كل مجموعة من أربع مسافات متجاورة على اللوحة، وسنستخدم سلسلةً من حلقات for المتداخلة لذلك.

تمثل المجموعة (columnIndex, rowIndex) نقطة البداية، إذ نتحقق من نقطة البداية والمسافات الثلاثة على يمينها لسلسلة playerTile. إذا كانت مساحة البداية هي (columnIndex, rowIndex)، ستكون المسافة الموجودة على يمينها (columnIndex + 1, rowIndex)، وهكذا. سنحفظ المربعات الموجودة في هذه المساحات الأربعة في المتغيرات tile1 و tile2 و tile3 و tile4. إذا كانت كل هذه المتغيرات لها نفس قيمة playerTile، فقد وجدنا أربع نقاط في صف واحد، وتعيد الدالة isWinner()‎ القيمة True.

ذكرنا سابقًا في قسم "المتغيرات ذات اللواحق الرقمية" من مقال اكتشاف دلالات الأخطاء في شيفرات لغة بايثون أن الأسماء المتغيرات ذات اللواحق الرقمية المتسلسلة (مثل tile1 إلى tile4 في هذه اللعبة) تشير غالبًا إلى شيفرة ذات رائحة code smell تشير إلى أنه يجب عليك استخدام قائمة واحدة بدلًا من ذلك، لكن في هذا السياق، لا بأس بأسماء المتغيرات هذه؛ إذ لا نحتاج إلى استبدالها بقائمة، لأن برنامج الأربع نقاط في صف واحد سيتطلب دائمًا أربعة متغيرات تحديدًا.

تذكر أن رائحة الشيفرة البرمجية لا تشير بالضرورة إلى وجود مشكلة؛ وهذا يعني فقط أننا يجب أن نلقي نظرة ثانية ونتأكد أننا كتبنا الشيفرة الخاصة بنا بطريقة أكثر قابلية للقراءة. قد يؤدي استخدام القائمة إلى جعل الشيفرة أكثر تعقيدًا في هذه الحالة، ولن تضيف أي فائدة، لذلك سنلتزم باستخدام tile1 و tile2 و tile3 و tile4.

نستخدم عملية مماثلة للتحقق من وجود أربع أحجار متتالية رأسيًا:

    for columnIndex in range(BOARD_WIDTH):
        for rowIndex in range(BOARD_HEIGHT - 3):
            # Check for four-in-a-row going down:
            tile1 = board[(columnIndex, rowIndex)]‎
            tile2 = board[(columnIndex, rowIndex + 1)]‎
            tile3 = board[(columnIndex, rowIndex + 2)]‎
            tile4 = board[(columnIndex, rowIndex + 3)]‎
            if tile1 == tile2 == tile3 == tile4 == playerTile:
                return True

بعد ذلك، نتحقق من وجود أربعة أحجار متتالية قطريًا للأسفل وإلى اليمين؛ ثم نتحقق من وجود أربعة أحجار متتالية قطريًا للأسفل وإلى اليسار:

   for columnIndex in range(BOARD_WIDTH - 3):
        for rowIndex in range(BOARD_HEIGHT - 3):
            # Check for four-in-a-row going right-down diagonal:
            tile1 = board[(columnIndex, rowIndex)]
            tile2 = board[(columnIndex + 1, rowIndex + 1)]
            tile3 = board[(columnIndex + 2, rowIndex + 2)]
            tile4 = board[(columnIndex + 3, rowIndex + 3)]
            if tile1 == tile2 == tile3 == tile4 == playerTile:
                return True

            # Check for four-in-a-row going left-down diagonal:
            tile1 = board[(columnIndex + 3, rowIndex)]
            tile2 = board[(columnIndex + 2, rowIndex + 1)]
            tile3 = board[(columnIndex + 1, rowIndex + 2)]
            tile4 = board[(columnIndex, rowIndex + 3)]
            if tile1 == tile2 == tile3 == tile4 == playerTile:
                return True

هذه الشيفرة مشابهة لعمليات التحقق الأفقية، لذلك لن نكرر الشرح هنا. إذا فشلت جميع عمليات التحقق الخاصة بالعثور على تتاليات رباعية، تعيد الدالة False للإشارة إلى أن playerTile ليس فائزًا في هذه الحالة:

    return False

الدالة الوحيدة المتبقية هي استدعاء دالة main()‎:

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

نستخدم لغة بايثون الشائعة التي ستستدعي main()‎ في حال تشغيل fourinarow.py مباشرةً، ولكن ليس في حال استيراد fourinarow.py مثل وحدة module.

الخلاصة

تستخدم لعبة أربع نقاط في صف واحد تربح محارف آسكي ASCII لعرض تمثيل للوحة اللعبة. نعرض هذا باستخدام سلسلة متعددة الأسطر مخزنة في ثابت BOARD_TEMPLATE. تحتوي هذه السلسلة على 42 زوجًا من الأقواس {} لعرض كل مسافة على لوحة بقياس 7×6. نستخدم الأقواس بحيث يمكن لتابع السلسلة format()‎ استبدالها بالحجر الموجود في تلك المساحة. بهذه الطريقة، يصبح الأمر أكثر وضوحًا كيف تنتج سلسلة BOARD_TEMPLATE لوحة اللعبة كما تظهر على الشاشة.

ترجمة -وبتصرف- لقسم من الفصل 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.


×
×
  • أضف...