توليد لوحة اللعبة ديناميكياً
حتى الآن كانت اللوحة تُعطى لك جاهزة كقائمة متداخلة. لكن في البرنامج الكامل يجب أن تنشئها بنفسك.
هذا كود يبدو قريبًا من الحل:
def make_board(size):
row = []
for _ in range(size):
row.append(' ')
board = []
for _ in range(size):
board.append(row)
return board
def test():
board = make_board(3)
check_result(board, [[' ', ' ', ' '], [' ', ' ', ' '], [' ', ' ', ' ']])
board[0][0] = 'X'
check_result(board, [['X', ' ', ' '], [' ', ' ', ' '], [' ', ' ', ' ']])
test()
الكود قريب جدًا، لكن توجد مشكلة خفية. تأكد أنك تفهم ما يفعله، وحاول اكتشاف الخطأ إن استطعت. إذا لم تره سريعًا فلا تضيع وقتًا كبيرًا؛ المشكلة دقيقة.
هل رأيت ما حدث؟
كل صف حصل على '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 أيضًا باستخدام row[:] أو list(row). لكن من المهم أن تعرف أن هذه الطرق كلها تصنع نسخة سطحية(shallow copy) من القائمة. أي إنها تنسخ القائمة في المستوى الأعلى، ولا تنشئ نسخًا مستقلة من كل عنصر بداخلها. هذا مناسب هنا لأن row تحتوي نصوصًا لا تُعدّل في مكانها، ولذلك لا تحتاج عناصرها إلى نسخ.
الكود الموجود شبه صحيح.
توجد عدة طرق للحل.
بعض الحلول تحتاج إلى إضافة شيء صغير.
ويمكنك أيضًا إعادة ترتيب الكود من دون إضافة أو حذف شيء، باستثناء المسافات.
المشكلة أن القائمة نفسها
rowتُستخدم عدة مرات.أحد الحلول هو إنشاء نسخ مستقلة من
row.وحل آخر هو إنشاء
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، فقد تظهر المشكلة نفسها على مستوى أعمق. انظر إلى هذا المثال:
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بإنشاء نسخ عبر مستويات الكائنات المتداخلة بدل نسخ المستوى الأعلى فقط.
إذا بدت هذه الفكرة مربكة الآن فلا مشكلة. الهدف أن تتعرف مبكرًا على سبب محتمل لسلوك غريب في البرامج التي تحتوي كائنات متداخلة قابلة للتعديل. لست مطالبًا بإتقان النسخ العميق الآن، وما تعلمته في هذا الدرس سيظل مفيدًا.
أصبحنا الآن جاهزين لتجميع اللعبة كاملة.