الروبوت والصناديق
أصعب ما في تنفيذ لعبة بأسلوب سوكوبان هو تحرير الروبوت بحيث يستطيع دفع الصناديق بالاتجاه المطلوب. وينبغي للعبة أن تستطيع معرفة متى يستطيع الروبوت التحرّك في اتجاه معيّن، وأن تستطيع التعامل مع أي موقف يتطلّب حركة صندوق أيضًا. لنواجه هذا التحدّي الآن.
معالجة أحداث المفاتيح
يوجّه اللاعب الروبوت بمفاتيح الأسهم الأربعة، لذا ينبغي لمُعالج الأحداث لدينا أن يكون قادرًا على الاستجابة لأحداث المفاتيح المناسبة:
def check_events(self):
for event in pygame.event.get():
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_LEFT:
self.move(0, -1)
if event.key == pygame.K_RIGHT:
self.move(0, 1)
if event.key == pygame.K_UP:
self.move(-1, 0)
if event.key == pygame.K_DOWN:
self.move(1, 0)
if event.type == pygame.QUIT:
exit()
الآن فكلّما ضغط اللاعب مفتاح أسهم، تُستدعى الطريقة move بزوج وسطاء مناسب. يحوي الوسيط الأول الحركة في الاتجاه الرأسي، بينما يحوي الثاني الحركة في الاتجاه الأفقي.
البحث عن الروبوت
تحتاج اللعبة إلى معرفة موقع الروبوت لتحرّكه بشكل صحيح. لنُضِف الطريقة find_robot التي تُحدّد موقع الروبوت:
def find_robot(self ):
for y in range(self.height):
for x in range(self.width):
if self.map[y][x] in [4, 6]:
return (y, x)
تمرّ الطريقة على جميع مربّعات شبكة اللعبة وتُعيد إحداثيات المربّع الذي يحمل إمّا العدد 4 (الروبوت وحده) أو العدد 6 (الروبوت داخل مربّع هدف).
الفكرة هي أنّ كلّما ضغط اللاعب مفتاح أسهم، يُحدَّد أوّلًا موقع الروبوت بالمرور على مربّعات الشبكة. وقد يبدو هذا بطيئًا إلى حدٍّ وبائدًا، إذ كان يمكننا بسهولة الاحتفاظ بمتغيّر أو متغيّرين منفصلين لتخزين موقع الروبوت. وميزة هذا النهج البحثي أنّنا لا نخزّن موقع الروبوت في موضعين مختلفين (في شبكة اللعبة *ومتغيّرات منفصلة)، بل نهتمّ بالموضع الواحد فقط (شبكة اللعبة)، وهذا يعني أنّ حالة اللعبة في ذاكرة الحاسوب أسهل في التعامل معها.
تعديلات على شبكة اللعبة
استدعينا الطريقة move سابقًا، لكننا لم نُعرِّفها بعد. لنفعل ذلك الآن.
تأخذ الطريقة move الاتجاه الذي يريد اللاعب التحرّك إليه كوسطاء. ثم تُحدِّث شبكة اللعبة وفقًا لذلك، أو تُقرّر أنّ الحركة غير مسموحة وتترك الشبكة دون تغيير.
def move(self, move_y, move_x):
robot_old_y, robot_old_x = self.find_robot()
robot_new_y = robot_old_y + move_y
robot_new_x = robot_old_x + move_x
if self.map[robot_new_y][robot_new_x] == 1:
return
if self.map[robot_new_y][robot_new_x] in [3, 5]:
box_new_y = robot_new_y + move_y
box_new_x = robot_new_x + move_x
if self.map[box_new_y][box_new_x] in [1, 3, 5]:
return
self.map[robot_new_y][robot_new_x] -= 3
self.map[box_new_y][box_new_x] += 3
self.map[robot_old_y][robot_old_x] -= 4
self.map[robot_new_y][robot_new_x] += 4
تحتوي الطريقة على مراحل مختلفة كثيرة، لننظر في كل واحدة منها بدورها:
الموقع القديم والجديد للروبوت
robot_old_y, robot_old_x = self.find_robot()
robot_new_y = robot_old_y + move_y
robot_new_x = robot_old_x + move_x
أوّلًا، تستدعي الطريقة find_robot لتحديد الموقع الحالي للروبوت قبل الحركة، ويُخزَّن ذلك في المتغيّرين robot_old_y وrobot_old_x.
ثم يُخزَّن الموقع الجديد للروبوت بعد الحركة المتوقّعة في المتغيّرين robot_new_y وrobot_new_x. ويمكن حساب الإحداثيات الجديدة بسهولة بجمع القيم المُمرَّرة كوسطاء على الموقع القديم للروبوت، إذ يحوي كلٌّ منهما قيمًا رأسية وأفقية.
هل اصطدم الروبوت بجدار؟
if self.map[robot_new_y][robot_new_x] == 1:
return
تتولّى جملة if أعلاه الموقف الذي يصطدم فيه الروبوت بجدار نتيجةً للحركة. تذكّر أنّ 1 كان موضع مربّع الجدار في قائمة الصور. وهذا غير مسموح، فتُعيد الطريقة ببساطة دون أيّ شيء آخر.
تحريك صندوق
if self.map[robot_new_y][robot_new_x] in [3, 5]:
box_new_y = robot_new_y + move_y
box_new_x = robot_new_x + move_x
if self.map[box_new_y][box_new_x] in [1, 3, 5]:
return
self.map[robot_new_y][robot_new_x] -= 3
self.map[box_new_y][box_new_x] += 3
إذا كان الموقع الجديد المحتمل للروبوت يحوي العدد 3 (صندوق وحده) أو العدد 5 (صندوق داخل مربّع هدف)، يحاول الروبوت تحريك الصندوق إلى المربّع التالي على التوالي. ولهذا الغرض نحتاج إلى متغيّرين جديدين: box_new_y وbox_new_x، اللذين يحويان موقع الصندوق بعد الحركة.
وكما هو الحال مع الروبوت، لا يمكن تحريك الصندوق إلى مربّع جدار يحمل المعرّف 1. كما لا يمكن للصندوق أن ينتقل فوق صندوق آخر، أو إلى مربّع هدف يحوي صندوقًا. لو حدث ذلك نتيجةً للحالة، تُعيد الطريقة ببساطة دون إحداث أيّ تغيير في الشبكة.
وفي أيّ حالة أخرى يمكن للصندوق أن يتحرّك. يُخصم 3 من القيمة في موضع الصندوق الحالي بالشبكة، ويُضاف 3 إلى قيمته في موضعه الجديد. وبسبب الترتيب الذكي لبنود قائمة images، يظهر هذا صحيحًا سواء تضمّنت الحركة مربّعات أرضية أم مربّعات هدف.
تحريك الروبوت
self.map[robot_old_y][robot_old_x] -= 4
self.map[robot_new_y][robot_new_x] += 4
إذا وصل تنفيذ الطريقة إلى هذه النقطة دون إعادة، حان وقت تحريك الروبوت أيضًا. والإجراء مشابه لتحريك الصندوق، لكن القيمة التي تُخصم وتُضاف إلى الموضعين المناسبين في الشبكة هي 4 هذه المرة. وهذا يضمن — من جديد بفضل الترتيب الذكي لبنود قائمة images — أنّ النتيجة النهائية على الشبكة صحيحة سواء تضمّنت الحركة مربّعات أرضية أم مربّعات هدف.
إعادة هيكلة؟
استخدام الشبكة وحدها لتخزين حالة اللعبة في كل وقت مفيد للغاية، من حيث إنّ متغيّرًا واحدًا فقط هو المعنيّ بهذا العملية كاملة، ومن السهل تحديث حالة الشبكة عبر جمع وطرح بسيط.
والعيب في ذلك أنّه قد يصبح من الصعب قليلًا فهم شيفرة اللعبة. ولو أنّ شخصًا غير معتاد على المنطق المستخدم رأى السطر التالي من الشيفرة، لربما تحوّل قليلًا:
if self.map[box_new_y][box_new_x] in [1, 3, 5]:
يستخدم هذا المقتطف من الشيفرة أرقامًا سحرية (magic numbers) للدلالة على مربّعات الشبكة. وكلّ من يقرأ الشيفرة عليه أن يعرف أنّ 1 تعني الجدار، و3 تعني صندوقًا، و5 تعني صندوقًا داخل مربّع هدف.
وأسطر الجمع والطرح الذكية تبدو غامضة أكثر:
self.map[robot_new_y][robot_new_x] -= 3
كان العدد 3 يدلّ على صندوق قبل قليل، والآن يُخصم من قيمة مربّع في الشبكة. ويعمل هذا في سياق نظام الترقيم لدينا، إذ يحوّل صندوقًا (3) إلى مربّع أرضي عادي (0)، أو مربّع هدف يحوي صندوقًا (5) إلى مربّع هدف فارغ (2) — لكن فهم هذا يستلزم مقدّمة في نظام الترقيم المستخدم.
يمكننا تسهيل الفهم على كلّ من يقرأ الشيفرة عبر إعادة الهيكلة (refactoring) لتنفيذنا. ويعني ذلك تحسين بنية الشيفرة ووضوحها. وإحدى طرق تحقيق ذلك استعمال أسماء المربّعات بدلًا من الأرقام 0 إلى 6، ولو أن هذا لا يشرح أيضًا كيف ولماذا يمكن جمع الأرقام وطرحها مع الحفاظ على سلامة الشبكة.
ولجعل الشيفرة مفهومة حقًا، يلزم على الأرجح إجراء إعادة هيكلة أكثر جذرية. فعلى سبيل المثال يمكننا الاحتفاظ ببنية خريطة اللعبة في موضع واحد، وتخزين مواقع الروبوت والصناديق في بنية بيانات منفصلة. والعيب في هذا هو أنّه سيؤدّي على الأرجح إلى شيفرة أكثر بكثير، وأنّ البنية الداخلية للعبة ستصبح أعقد بكثير.
وإعادة الهيكلة وجودة الشيفرة موضوع لدورات لاحقة، مثل منهجيات تطوير البرمجيات وهندسة البرمجيات.