الآلي الخلوية في المتصفح

صورة

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

الأوتوماتيكية الخلوية لها أشكال وأنواع وأبعاد مختلفة. من المحتمل أن يكون الأوتوماتون الخلوي الأكثر شهرة هو لعبة كونواي للحياة (GOL). يتكون من شبكة ثنائية الأبعاد تحتوي كل خلية فيها على قيمة ثنائية (حية أو ميتة). تحدد القواعد المصاحبة ، بناءً على حالة الخلايا المجاورة ، ما إذا كانت الخلية يجب أن تكون ميتة أم حية. تقول القواعد أن الخلية الحية تموت من الوحدة إذا كان هناك أقل من خليتين حيتين حولها. إذا كانت أكثر من ثلاث خلايا مجاورة على قيد الحياة ، فإنها تموت من الاكتظاظ السكاني. بمعنى آخر ، الخلية "تنجو" إذا كانت هناك خلايا مجاورة حية أو 2 حولها. لكي تظهر الخلية الميتة ، يجب أن تحتوي على ثلاث خلايا مجاورة حية ، وإلا فإنها تظل ميتة. ويرد أدناه مثال على جهاز GoL يتكرر عبر عدة حالات.

لعبة الحياة

إصدار آخر مشهور من automaton الخلوية أحادي الأبعاد؛ يطلق عليه الابتدائية الخلوية التلقائية (ECA). هذا ما نطبقه في هذا المنشور.

يتم تخزين كل حالة من هذا الإكمال التلقائي كصفيف أحادي البعد لقيم منطقية ، وعلى الرغم من أن هناك حاجة إلى بعدين لتصور حالة GOL ، فإن الإضافة التلقائية للقيم تكفي لهذا الإكمال التلقائي. بفضل هذا ، يمكننا استخدام بعدين (بدلاً من الرسوم المتحركة) لتصور التاريخ الكامل لحالات هذا التشغيل التلقائي. كما هو الحال في GOL ، فإن حالة الخلية في هذا الجهاز هي 0 أو 1 ، ولكن على عكس خلية GOL ، والتي يتم تحديثها وفقًا لجيرانها الثمانية ، يتم تحديث خلية ECA استنادًا إلى حالة الجار الأيسر والجار الأيمن ونفسه!

فيما يلي أمثلة للقواعد: الخلايا الثلاثة الأولى هي مدخلات القاعدة ، والخلية السفلية هي الخرج ، حيث يكون الأسود 1 والأبيض يساوي 0. وأيضًا ، يمكننا أن نرى الأنماط التي تم إنشاؤها بواسطة كل منها ، عندما تكون الحالة الأولية جميعًا 0 باستثناء 1 في الخلية الوسطى.


قد تتساءل: لماذا القواعد المذكورة أعلاه مبينة بالأرقام؟ لأن كل رقم في النطاق من 0 إلى 255 يتوافق مباشرة مع قاعدة ECA ، وبالتالي يتم استخدام هذه الأرقام كأسماء للقواعد. هذه المراسلات مبينة أدناه:


من الرقم إلى القاعدة

يمكن تمثيل أي رقم في النطاق من 0 إلى 255 في شكل ثنائي مع 8 أرقام فقط (السهم الأول أعلاه). علاوة على ذلك ، يمكننا إعطاء كل من هذه الأرقام فهرسًا استنادًا إلى موقعه (السهم الثاني). من الطبيعي أن تكون هذه المؤشرات في حدود 0 إلى 7 ، أي أنه يمكن تمثيلها في شكل ثنائي باستخدام 3 أرقام فقط (السهم الثالث). عند تفسير هذه الأرقام الثلاثة كمدخلات ، والرقم المقابل من الرقم الأصلي كمخرجات ، نحصل على الوظيفة الثلاثية التي نحتاجها (السهم الرابع).

جيل القاعدة


دعنا ننفذ التفسير أعلاه كدالة get_rule ذات ترتيب get_rule تتلقى رقمًا من 0 إلى 255 كمدخلات وإرجاع قاعدة ECA المقابلة لهذا الرقم.

نحن بحاجة إلى إنشاء شيء مثل هذا:

 const rule30 = get_rule(30); const output110 = rule30(1, 1, 0); 

في المثال أعلاه ، rule30(1,1,0) جميع القيم الثنائية الثلاث في رقم واحد (110 = 6) وستعود قليلاً في هذا الموضع (6) في التمثيل الثنائي 30. الرقم 30 في التمثيل الثنائي هو 00011110 ، لذلك ، سوف ترجع الدالة 0 (نعول على اليمين ونبدأ العد من 0).

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

 const combine = (b1, b2, b3) => (b1 << 2) + (b2 << 1) + (b3 << 0); 

بعد أن قمت بتحويل الوسيطات إلى المواضع المقابلة ، ثم إضافة الأرقام الثلاثة المقلوبة ، نحصل على المجموعة المطلوبة.

الجزء المهم الثاني من دالة get_rule هو تحديد قيمة البت في موضع معين في رقم. لذلك ، دعونا ننشئ دالة get_bit(num, pos) ، والتي يمكنها إرجاع قيمة بت في موضع معين pos في رقم معين. على سبيل المثال ، الرقم 141 في شكل ثنائي هو 10001101 ، لذلك يجب أن ترجع get_bit(2, 141) 1 ، ويجب أن تعيد get_bit(5, 141) 0 .

يمكن تنفيذ وظيفة get_bit(num,pos) عن طريق إجراء أول تغيير في الرقم في pos إلى اليمين ، ثم تنفيذ عملية bitwise "AND" مع الرقم 1.

 const get_bit = (num, pos) => (num >> pos) & 1; 

الآن نحن بحاجة فقط إلى الجمع بين هاتين الوظيفتين:

 const get_rule = num => (b1, b2, b3) => get_bit(num, combine(b1, b2, b3)); 

! ممتاز لذلك ، لدينا وظيفة تتمثل في أن لكل رقم في الفواصل الزمنية لدينا يعطينا قاعدة فريدة من نوعها ECA التي يمكننا أن نفعل أي شيء. والخطوة التالية هي جعلها في المتصفح.

قواعد التصور


لتقديم automata في المستعرض ، سوف نستخدم عنصر canvas . يمكن إنشاء canvas وإضافته إلى نص html كما يلي:

 window.onload = function() { const canvas = document.createElement('canvas'); canvas.width = 800; canvas.height = 800; document.body.appendChild(canvas); }; 

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

 const context = canvas.getContext('2d'); 

تشير المعلمة '2d' إلى نوع السياق الذي سنستخدمه في هذا المثال.

بعد ذلك ، سننشئ وظيفة ، مع وجود السياق ، فإن قاعدة ECA ، وكذلك بعض المعلومات حول مقياس وعدد الخلايا ، ترسم القاعدة على canvas . الفكرة هي إنشاء ورسم خط شبكة بخط ؛ الجزء الرئيسي من الكود يبدو كالتالي:

 function draw_rule(ctx, rule, scale, width, height) { let row = initial_row(width); for (let i = 0; i < height; i++) { draw_row(ctx, row, scale); row = next_row(row, rule); } } 

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

بالنسبة إلى مقتطف الشفرة أعلاه ، نحتاج إلى تنفيذ ثلاث وظائف: initial_row و draw_row و next_row .

initial_row هي وظيفة بسيطة. يقوم بإنشاء صفيف من الأصفار وتغيير العنصر في وسط الصفيف واحدًا.

 function initial_row(length) { const initial_row = Array(length).fill(0); initial_row[Math.floor(length / 2)] = 1; return initial_row; } 

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

 const next_row = (row, rule) => row.map((_, i) => rule(row[i - 1], row[i], row[i + 1])); 

هل لاحظت أننا خدعنا على هذا الخط؟ تتطلب كل خلية في صف جديد إدخالًا من ثلاث خلايا أخرى ، لكن تتلقى خليتان عند حواف الصف بيانات من خليتين فقط. على سبيل المثال ، next_row[0] الحصول على قيمة next_row[0] من row[-1] . هذا لا يزال يعمل ، لأنه عند محاولة الوصول إلى القيم عن طريق الفهارس غير الموجودة في المصفوفة ، تُرجع javascript undefined ، ويحدث ذلك دائمًا (undefined >> [ ]) (من دالة combine ) دائمًا 0. هذا يعني أننا في الواقع نقوم بمعالجة كل قيمة خارج الصفيف كـ 0.

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

التالي يأتي وظيفة draw_row . هي التي تؤدي العرض!

 function draw_row(ctx, row, scale) { ctx.save(); row.forEach(cell => { ctx.fillStyle = cell === 1 ? '#000' : '#fff'; ctx.fillRect(0, 0, scale, scale); ctx.translate(scale, 0); }); ctx.restore(); ctx.translate(0, scale); } 

وهنا نعتمد اعتمادًا كبيرًا على كائن السياق ، باستخدام 5 طرق مختلفة على الأقل منه. فيما يلي قائمة مختصرة وكيفية استخدامها.

  • يشير fillStyle إلى الطريقة التي نريد بها ملء الأشكال. يمكن أن يكون لونًا ، على سبيل المثال ، "#f55" ، بالإضافة إلى تدرج أو نقش. نستخدم هذه الطريقة لفصل الخلايا بصريًا عن الخلايا 1.
  • fillRect(x, y, w, h) يرسم مستطيل من نقطة (س ، ص) مع عرض ث والارتفاع ح ، معبأ وفقا ل fillStyle . المستطيلات لدينا عبارة عن مربعات بسيطة ، ولكن قد تفاجأ بأن نقطة الانطلاق جميعها في الأصل. لقد حدث ذلك لأننا نستخدم هذه الطريقة مع translate .
  • translate(x, y) يسمح لك بنقل نظام الإحداثيات بأكمله. يتم حفظ الموضع ، وبالتالي فإن الطريقة هي بديل ممتاز لتتبع المواضع المختلفة للعناصر. على سبيل المثال ، بدلاً من حساب موضع كل خلية شبكة فردية ، يمكننا ببساطة رسم خلية ، والانتقال إلى اليمين ، ورسم خلية جديدة ، وما إلى ذلك.
  • save() restore() بالاقتران مع طرق تحويل الإحداثيات translate وغيرها. نستخدمها لحفظ نظام الإحداثيات الحالي عند نقطة معينة ، بحيث يمكننا لاحقًا الرجوع إليه (باستخدام الاستعادة ). في هذه الحالة ، نقوم بحفظ نظام الإحداثيات قبل تقديم الخط ونقله إلى اليمين. ثم ، عندما انتهينا من رسم الخط وذهبنا إلى اليمين ، تتم استعادة الإحداثيات ونعود إلى حالتها الأصلية. ثم نتحرك لأسفل للاستعداد لرسم السطر التالي.

الآن لدينا جميع الأجزاء اللازمة لوظيفة draw_rule . نحن نستخدم هذه الوظيفة في window.onload بعد إعداد canvas . سنحدد أيضًا المعلمات التي نحتاجها.

 window.onload = function() { const width = 1000; // Width of the canvas const height = 500; // Height of the canvas const cells_across = 200; // Number of cells horizontally in the grid const cell_scale = width / cells_across; // Size of each cell const cells_down = height / cell_scale; // Number of cells vertically in the grid const rule = get_rule(30); // The rule to display const canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height; document.body.appendChild(canvas); const context = canvas.getContext('2d'); draw_rule(context, rule, cell_scale, cells_across, cells_down); }; 

نحن استخراج أبعاد canvas كمتغيرات منفصلة جنبا إلى جنب مع عدد الخلايا أفقيا. ثم نقوم بحساب cell_scale و "cell_down" حتى تملأ الشبكة canvas بالكامل ، بينما تظل الخلايا مربعة. بفضل هذا ، يمكننا بسهولة تغيير "دقة" الشبكة ، وتبقى داخل canvas .


هذا كل شئ! المثال الكامل للرمز موجود على github وعلى codepen :

المضي قدما


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

يمكنك أيضًا جعل الحالة الأولية لخلايا الأوتوماتون عشوائية بدلاً من "الأصفار الصلبة ووحدة واحدة" ثابتة. لذلك نحصل على نتائج أكثر لا يمكن التنبؤ بها. يمكن كتابة هذا الإصدار من دالة initial_row كما يلي:

 function random_initial_row(width) { return Array.from(Array(width), _ => Math.floor(Math.random() * 2)); } 

فيما يلي ترى مدى تأثير تغيير خط الإخراج هذا بشكل كبير على الإخراج.


سلسلة مصدر عشوائي

وهذا مجرد جانب واحد يمكنك تغييره! لماذا تقتصر على شرطين فقط؟ (الانتقال من 2 إلى 3 حالات يزيد من عدد القواعد من 256 إلى 7 625 597 484 987!) لماذا تقتصر على المربعات؟ لماذا فقط 2 الأبعاد؟ لماذا قاعدة واحدة فقط في وقت واحد؟

فيما يلي أمثلة على المرئيات المرتكزة على ECA ، ولكن مع وظيفة draw_rule بديلة لا draw_rule الخطوط مع المربعات ، ولكن بنمط متساوي القياس ، ثم تملأ المساحات المحددة بواسطة هذه الخطوط بالألوان. ليس لديك حتى عرض خطوط فاصلة وعرض الألوان فقط.


إذا ذهبت إلى أبعد من ذلك ، فيمكنك إضافة تماثلات ، محورية (صف وسط) ومرآة (صف سفلي).


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

حظا سعيدا

Source: https://habr.com/ru/post/ar481672/


All Articles