cb
codebamin
الفصل 11 · مشروع لعبة إكس-أو (Tic-Tac-Toe)

توليد لوحة اللعبة ديناميكياً

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

هذا كود يبدو قريبًا من الحل:

__copyable__ __program_indented__

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

هل رأيت ما حدث؟

كل صف حصل على 'X' في أول موضع، وكأن البرنامج نفذ:

board[0][0] = 'X' board[1][0] = 'X' board[2][0] = 'X'

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

المشكلة أن القائمة row أُنشئت مرة واحدة فقط ثم أُعيد استخدام الكائن نفسه عدة مرات. لذلك تحتوي board على القائمة نفسها ثلاث مرات، وكأننا كتبنا:

board = [row, row, row]

وعند تنفيذ:

board[0][0] = 'X'

فهذا يعادل تعديل:

row[0] = 'X'

ولأن board[0] وboard[1] وboard[2] كلها تشير إلى row نفسها، يظهر التغيير في الصفوف كلها. هذه هي فكرة الفرق بين الكائن نفسه ونسخ منفصلة منه.

مهمتك الآن إصلاح make_board. يجب أن تعيد قائمة طولها size، وكل عنصر فيها قائمة منفصلة طولها size، وكل عنصر داخلي هو النص ' '. يجب أن تكون القوائم الفرعية كائنات مختلفة لا نفس القائمة مكررة.

يمكنك حل ذلك بإنشاء نسخة مستقلة من row لكل صف، أو ببناء قائمة جديدة من الصفر في كل دورة.

أحسنت!

يمكن حل المشكلة بنقل الحلقة الأولى إلى داخل الحلقة الثانية حتى ننشئ row جديدة في كل مرة:

def make_board(size): board = [] for _ in range(size): row = [] for _ in range(size): row.append(' ') board.append(row) return board

وهناك طريقة أخرى: أن تنشئ نسخة من row في كل مرة. يمكنك إبقاء الكود الأصلي وتغيير سطر واحد فقط إلى:

board.append(row.copy())

ويمكن نسخ row أيضًا باستخدام row[:] أو list(row). لكن من المهم أن تعرف أن هذه الطرق كلها تصنع نسخة سطحية (shallow copy) من القائمة. أي إنها تنسخ القائمة في المستوى الأعلى، ولا تنشئ نسخًا مستقلة من كل عنصر بداخلها. هذا مناسب هنا لأن row تحتوي نصوصًا لا تُعدّل في مكانها، ولذلك لا تحتاج عناصرها إلى نسخ.

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

__copyable__ def make_board(size): row = [] for _ in range(size): row.append(' ') board = [] for _ in range(size): board.append(row.copy()) return board

def make_cube(size): cube = [] board = make_board(size) for _ in range(size): cube.append(board.copy()) return cube

def test(): cube = make_cube(2) print(cube) cube[0][0][0] = 'X' print(cube) print(cube[0] is cube[1]) print(cube[0][0] is cube[0][1]) print(cube[0][0] is cube[1][0])

test()

كل عنصر في cube هنا قائمة مستقلة لأنها نسخة من board. وداخل كل نسخة من board توجد قوائم صفوف. لكن لأننا استخدمنا نسخة سطحية من board، فإن نسخ board المختلفة ما زالت تشترك في كائنات الصفوف الداخلية نفسها: العنصر الأول داخل كل نسخة يشير إلى نسخة row الأولى نفسها، والعنصر الثاني إلى الكائن نفسه في الموضع الثاني، وهكذا.

تعديل make_board وحده لا يحل مشكلة النسخ في cube. لديك حلان عامان:

  • استدعِ make_board من جديد في كل مرة لتنشئ board مستقلة تمامًا.
  • أو استخدم deepcopy بدل board.copy() عندما تحتاج إلى نسخ المستويات الداخلية أيضًا. تقوم deepcopy بإنشاء نسخ عبر مستويات الكائنات المتداخلة بدل نسخ المستوى الأعلى فقط.

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

أصبحنا الآن جاهزين لتجميع اللعبة كاملة.

solution.py
1
2
3
4
5
6
7
8
9
10
الطرفية (shell)
>>>