برمجة لعبة (X/O) بلغة البايثون

السبت 24 محرم 1442ھ السبت 12 سبتمبر 2020م

2٬327 كلمة

30 دقيقة

برمجة لعبة إكس أوُ بلغة البايثون
Tic Tac Toe AI

في هذا الدرس ستتعلم برمجة الآلة لاستخدام المنطق في لعبة إكس أوُ. سنقوم ببناء البيئة، والإجراءات الممكن اتخاذها خلال اللعبة، وبرمجة المنطق المتبع للفوز في اللعبة. في نهاية الدرس ستكون بنيت بيئة للعب إكس أوُ، وآلة تستعمل المنطق في اللعب ضدك، وربما في الفوز عليك.​

يمكنك تجربة كود لعبة إكس أوُ « X / O » بشكل مباشر عن طريق كولاب نوتبوك
Laptop 226
3D Fihm Logo
3D Colab Logo
3D Fihm Logo

بناء البيئة

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

يمكننا الاستفادة من هذه الدالة في طباعة البيئة في كل مرة يختار فيها أحد اللاعبين موقع الحركة التالية.

def drawBoard(board):

# تستقبل الدالة تحركات اللاعبين كقائمة وتقوم بطباعتها على شكل لوح لعبة إكس أوُ
# يتم تخزين التحركات في القائمة بدءًا من الخانة 1 وليس 0 لسهولة استرجاع البيانات
# في كل خانة في القائمة المستلمة يمكن أن نجد قيمة إكس أو قيمة أوُ أو خانة خالية

    print('   |   |')
    print(' ' + board[7] + ' | ' + board[8] + ' | ' + board[9])
    print('   |   |')
    print('-----------')
    print('   |   |')
    print(' ' + board[4] + ' | ' + board[5] + ' | ' + board[6])
    print('   |   |')
    print('-----------')
    print('   |   |')
    print(' ' + board[1] + ' | ' + board[2] + ' | ' + board[3])
    print('   |   |')

برمجة أساسيات مسار اللعبة

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

في الدالة التالية يقوم اللاعب بإدخال الحرف لبدء اللعب ثم يتم استرجاع قائمة بالحرفين مع ملاحظة ترتيب حرف اللاعب أولاً متبعاً بحرف الآلة.

def inputPlayerLetter():

# X or O تمكّن اللاعب من اختيار الحرف المرغوب
# تقوم الدالة بتمرير قيمتين بحيث يتغير الترتيب بناءً على اختيار اللاعب. إذا اختار اللاعب حرف إكس يكون الأول في القائمة وهكذا
    letter = ''
    while not (letter == 'X' or letter == 'O'):
        print('Do you want to be X or O?')
        letter = input().upper()

    # القيمة الآولى حرف اللاعب والقيمة الثانية حرف الآلة
    if letter == 'X':
        return ['X', 'O']
    else:
        return ['O', 'X']

من يلعب أولاً؟

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

import random

def whoGoesFirst():

# Random يتم اختيار صاحب الدور الأول عشوائياً عن طريق تطويع الدال الرياضية

    if random.randint(0, 1) == 0:
        return 'computer'
    else:
        return 'player'

الاستمرار في اللعب

كخاصية إضافية يمكن سؤال اللاعب عن رغبته في بدء لوحة جديدة بعد انتهاء اللعبة. يمكننا كتابة الدالة كالتالي واستدعائها بعد تحديد الفائز في اللعبة السابقة.

def playAgain():

# تقوم هذه الدالة بتمرير قيمة منطقية (صائب أو خطأ) بناءً على اختيار اللاعب في بدء جولة جديدة

    print('Do you want to play again? (yes or no)')
    return input().lower().startswith('y')

اتخاذ التحركات

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

def makeMove(board, letter, move):

board[move] = letter

من الفائز؟

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

def isWinner(bo, le):

# يتم تمرير محتويات اللوح الحالية والحرف المنتمي للاعب إلى الدالة
# كل سطر يتحقق من أحد حالات الفوز الثمانية والشرط هو تحقيق أحد تلك الحالات لتمرير قيمة (صائب

    return ((bo[7] == le and bo[8] == le and bo[9] == le) or # across the top
            (bo[4] == le and bo[5] == le and bo[6] == le) or # across the middle
            (bo[1] == le and bo[2] == le and bo[3] == le) or # across the bottom
            (bo[7] == le and bo[4] == le and bo[1] == le) or # down the left side
            (bo[8] == le and bo[5] == le and bo[2] == le) or # down the middle
            (bo[9] == le and bo[6] == le and bo[3] == le) or # down the right side
            (bo[7] == le and bo[5] == le and bo[3] == le) or # diagonal
            (bo[9] == le and bo[5] == le and bo[1] == le)) # diagonal

التحقق من صلاحية الحركة

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

def isSpaceFree(board, move):

# تقوم الدالة بتمرير القيمة المنطقية (صائب) في حال كانت الخانة المطلوبة شاغرة

    return board[move] == ' '

سؤال اللاعب عن حركته التالية على اللوح

نحتاج بناء دالة لسؤال اللاعب عن تحركاته والخانة التي يريد اللعب اختيارها. يمكننا ذلك عن طريق دالة المدخلات كما هو ظاهر في الدالة التالي. بالإضافة للحصول على رقم الخانة فإننا نتحقق من صلاحية المدخل عن طريق مقارنته بالمدخلات المقبولة مع الأخذ في عين الاعتبار كون الخانة خالية. إن لم تكن القيمة المدخلة من قبل اللاعب ضمن الأرقام من 1 إلى 9 أو كانت الخانة قد تم اختيارها مسبقاً فإننا نسأل اللاعب مرة أخرى عن حركته التالية.

def getPlayerMove(board):

move = ' '
# لطالما أن المستخدم لم يقم بادخال قيمة تستوفي الشروط، يستمر البرنامج في السؤال عن الحركة التالية
# الشروط هي:
# 1. يجب أن تكون القيمة المدخلة أحد قيم خانات اللوح من 1 إلى 9
# 2. يجب أن تكون الخانة شاغرة
while move not in '1 2 3 4 5 6 7 8 9'.split() or not isSpaceFree(board, int(move)):
    print('What is your next move? (1-9)')
    move = input()
# في حال قام المستخدم بادخال قيمة صالحة تقوم الدالة بتمرير القيمة
return int(move)

ذكاء اصطناعي بدائي

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

تحتوي الدالة التالية على منطق الآلة في اختيار الحركة التالية مع التركيز على الهدف المبدئي، وهو منع اللاعب الآخر من الفوز والهدف النهائي والأهم، وهو تحقيق الفوز على اللاعب المنافس. يتكون المنطق من الخطوات التالية:

تتحقق الآلة من جميع الخانات وما إذا كان اختيار أي خانة يؤدي إلى الفوز المباشر.

في حال عدم وجود خانة تؤدي إلى فوز الآلة، تقوم الآلة بالبحث عن خانة تؤدي إلى فوز الخصم مباشرة.

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

في حال كانت جميع الأركان محتلة فإن الآلة تحاول احتلال الخانة في المنتصف.

في حال لم تكن خانة المنتصف شاغرة فإن الآلة تختار عشوائيا إحدى الخانات المتبقية في الأطراف.

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

def getComputerMove(board, computerLetter):

# بناءً على حرف الآلة الذي تستلمه الدالة فإنها تقوم بتعريف متغير لحرف اللاعب كالتالي
if computerLetter == 'X':
    playerLetter = 'O'
else:
    playerLetter = 'X'

# Tic Tac Toe AI خوارزمية الذكاء الاصطناعي :
# أولاً تتحقق الآلة من إمكانية الفوز في حركة واحدة وتحتل الخانة اللازمة للفوز
#----------------------(1)-----------------------------
for i in range(1, 10):
    copy = getBoardCopy(board)
    if isSpaceFree(copy, i):
        makeMove(copy, computerLetter, i)
        if isWinner(copy, computerLetter):
            return i
#----------------------(1)-----------------------------

# تتحقق الآلة من إمكانية اللاعب من الفوز في حركة واحدة ومنعه عن طريق احتلال الخانة
#----------------------(2)-----------------------------
for i in range(1, 10):
    copy = getBoardCopy(board)
    if isSpaceFree(copy, i):
        makeMove(copy, playerLetter, i)
        if isWinner(copy, playerLetter):
            return i
#----------------------(2)-----------------------------


# تحاول الآلة احتلال أحد الأركان في حال كان شاغر
#----------------------(3)-----------------------------
move = chooseRandomMoveFromList(board, [1, 3, 7, 9])
if move != None:
    return move
#----------------------(3)-----------------------------


# تحاول الآلة احتلات خانة المنتصف في حال لازالت شاغرة
#----------------------(4)-----------------------------
if isSpaceFree(board, 5):
    return 5
#----------------------(4)-----------------------------


# في محاولة أخيرة تقوم الآلة باختيار خانة عشوائية من الخانات المتبقية
#----------------------(5)-----------------------------
return chooseRandomMoveFromList(board, [2, 4, 6, 8])
#----------------------(5)-----------------------------

اختيار خانة عشوائية من قائمة تم تمريرها

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

def chooseRandomMoveFromList(board, movesList):

possibleMoves = []
for i in movesList:
    if isSpaceFree(board, i):
        possibleMoves.append(i)

if len(possibleMoves) != 0:
    return random.choice(possibleMoves)
else:
    return None

دوال إضافية مساعدة

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

def getBoardCopy(board):

dupeBoard = []
for i in board:
    dupeBoard.append(i)
return dupeBoard
def isBoardFull(board):

for i in range(1, 10):
    if isSpaceFree(board, i):
        return False
return True

مسار اللعب

بعد بناء البيئة وتحديد أساسيات اللعبة وتوضيح منطق اللعبة وإجراءاتها للآلة يأتي وقت بدء اللعب، البشر ضد الآلة.

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

# Tic Tac Toe
#الترحيب باللاعب قبل بدأ الجولة
print('Welcome to Tic Tac Toe!')

# While Loop تبدأ الجولة هنا وتنتهي متى ما اختار اللاعب إنهاء الجولات عن طريق الخروج المباشر من الـ
while True:
    # نقوم في بداية كل جولة بمسح التحركات من على اللوح وتعيين قيمة مسافة خالية لكل خانة
    # نعين عشر خانات للقائمة ونتجاهل الخانة ذات المؤشر 0 بحيث تمثل كل خانة من التسع الباقية خانات اللوح

    #    7 | 8 | 9
    #   ---+---+---
    #    4 | 5 | 6
    #   ---+---+---
    #    1 | 2 | 3

    theBoard = [' '] * 10

    # عند استدعاء دالة ادخال اللاعب للحرف المرغوب فإن الدالة تمرر قيمتين: حرف اللاعب وحرف الآلة
    playerLetter, computerLetter = inputPlayerLetter()
    # يتم اختيار صاحب الدور الأول عشوائياً
    turn = whoGoesFirst()
    print('The ' + turn + ' will go first.')
    gameIsPlaying = True

    # لطالما أن الجولة قائمة ولم تنتهي بالتعادل أو فوز أحد اللاعبين
    while gameIsPlaying:
        if turn == 'player':
        # في حال بدء دور اللاعب
            # يتم رسم اللوح
            drawBoard(theBoard)
            # يُسأل اللاعب عن الخانة المراد احتلالها
            move = getPlayerMove(theBoard)
            # يتم تخزين حركة اللاعب على اللوح
            makeMove(theBoard, playerLetter, move)

            # نتحقق مما إذا فاز اللاعب
            if isWinner(theBoard, playerLetter):
                # يتم رسم اللوح وإعلام اللاعب بفوزه وإنهاء الجولة الحالية
                drawBoard(theBoard)
                print('Hooray! You have won the game!')
                gameIsPlaying = False
            else:
                # إذا تأكدنا من عدم فوز اللاعب يأتي دور التحقق من التعادل في حال كان اكتمال اللوح
                if isBoardFull(theBoard):
                    drawBoard(theBoard)
                    print('The game is a tie!')
                    break
                else:
                    # ما لم يفز اللاعب ولم يتحقق التعادل بعد، ينتقل الدور إلى الآلة
                    turn = 'computer'

        else:
        # في حال بدء دور الآلة
            # نستدعي منطق الآلة مع تمرير اللوح الحالي ونستلم الحركة التي اختارتها الآلة بناءً على المنطق المذكور سابقاً
            move = getComputerMove(theBoard, computerLetter)
            # نقوم بتخزين حركة الآلة على اللوح
            makeMove(theBoard, computerLetter, move)

            # نتحقق مما إذا فاز اللاعب
            if isWinner(theBoard, computerLetter):
                # يتم رسم اللوح وإعلام اللاعب بخسارته وإنهاء الجولة الحالية
                drawBoard(theBoard)
                print('The computer has beaten you! You lose.')
                gameIsPlaying = False
            else:
                # إذا تأكدنا من عدم فوز الآلة يأتي دور التحقق من التعادل في حال اكتمال اللوح
                if isBoardFull(theBoard):
                    drawBoard(theBoard)
                    print('The game is a tie!')
                    break
                else:
                    # ما لم يخسر اللاعب ولم يتحقق التعادل، ينتقل الدور إلى اللاعب
                    turn = 'player'

    # في حالة إنتهاء الجولة يتم سؤال اللاعب عما إذا كان يريد بدء جولة جديدة
    # في حالة الرفض يتم إنهاء اللعبة
    if not playAgain():
        break
Welcome to Tic Tac Toe!
Do you want to be X or O?
X
The computer will go first.
     |      |
     |      |  o
     |      |
 ——————
     |      |
     |      |
     |      |
 ——————
     |      |
     |      |
     |      |
What is your next move? (1-9)
1
     |      |
     |      |  o
     |      |
 ——————
     |      |
     |      |
     |      |
 ——————
     |      |
 x  |      |  o
     |      |
What is your next move? (1-9)
6
     |      |
 o  |      |  o
     |      |
 ——————
     |      |
     |      |  x
     |      |
 ——————
     |      |
 x  |      |  o
     |      |
What is your next move? (1-9)
8
     |      |
 o  |  x  |  o
     |      |
 ——————
     |      |
     |  o  |  x
     |      |
 ——————
     |      |
 x  |      |  o
     |      |
The computer has beaten you! You lose.
Do you want to play again? (yes or no)
InventWithPython CH10
المصدر Invent with Python
Chapter 10 - TIC TAC TOE
InventWithPython ScreenShoot
نشرة فهم البريدية
لتبقى على اطلاع دائم على كل ما هو جديد مما تقدمه منصة فهم، انضم لنشرتنا البريدية.
مرشحة لدرجة الدكتوراه من جامعة كاليفورنيا وعضو هيئة تدريس بجامعة الملك عبدالعزيز.
  1. Arafa Arafa

    على افتراض ان هناك لعبه مكونه من 5 صفوف و5 اعمده وكل صف به 5 خانات يوجد خانه وحيده فى كل صف خاسره .. وفى كل مره يتم تغيرها بواسطه الذكاء الاصطناعى الخاص باللعبه … كيف استنتج او اعرف مكان الخانه الخاسره باستخدام بايثون والذكاء الاصطناعى على اقل تقدير بنسبه 90% مثلا … وشكرا لكى على مجهودك الجميل الرائع

  2. Arafa Arafa

    مع العلم ان اللعبه تستخدم خوازميه التوزيع العشوائى ومع الوقت تستنج اختيارات المستخدم

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *