Github: @EhsanShahbazii - Telegram: @ehsandevv
پیاده سازی بازی زندگی (Game of Life) با جاوااسکریپت
میخوایم تو این مقاله یه دنیای کوچولو با قوانین خیلی ساده رو مدلسازی کنیم. خب اول میریم با بازی زندگی (Game of Life) آشنا میشیم. بعد قوانین هاش رو یاد میگیرم. میرسیم به مرحله جذاب یعنی پیاده سازیش و در نهایت میشینیم و حرکات این موجودات رو نگاه میکنیم.
توی پرانتز بگم من کلا به نیم فاصله عادت ندارم و به طور مثال میگم رو به شکل میگم مینویسم. امیدوارم که اذیت نشین و کامنت هارو هم فارسی نوشتم راحت تر بشه فهمید :)
بازی زندگی یعنی چی؟
به زبون ساده بازی زندگی یه بازی بدون بازیکنه! مثل بقیه بازی ها نیست که یه حریف باشه و یه نفر دیگه. اینجا سلول هامون رشد و آیندشون فقط به وضعیت و شرایطی که به دنیا اومدن داره و اصلا نیازی نداره از اون بالا بهشون بگیم تو بیا اینور تو برو اونور! انگار چند تا سلول انداختی تو ظرف و داری نگاه میکنی و دخالتی توشون نداری.
این ایده از کجا اومده؟
حدودا توی سال 1940 جناب جان فون نیومن زندگی رو به یه صورت مهندسی تعریف کرد:
زندگی به صورت مخلوقی (مثل یک ارگانیزم یا یک موجود) است که میتواند تولید مثل کرده و یک ماشین تورینگ را شبیه سازی کند.
خیلی نمیخوام وارد بحث تکنیکیش بشم اگه علاقه داشتین تو لینک میتونید بخونید. خلاصه بعدش توی سال 1968 آقای جان کانوی ریاضیدان معروف خواست یه اتوماتای سلول (cellular automaton) بسازه که پیش بینی ناپذیر باشه.
به زبون عامیانه میخوایم یه مدلی بسازیم که المان های توش به صورت تصادفی کارایی انجام بدن بدون اینکه از بیرون بهشون وضعیت هایی رو تشریح کنیم. به قول معروف شلم شوربا به پا کنیم :)
بعد ها مدل ایشون پیشرفت کرد و قوانین های پیچیده تری نوشته شدن. حتی مدل از دوبعدی به سمت سه بعدی هم رفت و پیاده سازی شد. الگو های زیادی بعد از ارائه کشف شده که تعدادشون زیاده و از معروف ترین هاش میشه به گلایدر (Glider) اشاره کرد. لیستش رو اینجا میتونید ببینید.
قوانین بازی زندگی
قانون های خیلی ساده ای داره. قطعا میشه قانون هارو دستکاری کرد و چیزای پیچیده تری براشون نوشت ولی هدف ما اینجا اینه که یه مدلسازی خیلی ساده ای داشته باشیم.
- هر سلول زنده اگه کمتر از 2 تا همسایه داشته باشه دق میکنه و میمیره!
- هر سلول زنده اگه بیشتر از 3 تا همسایه داشته باشه جامعه گریز میشه و میمیره!
- هر سلول زنده اگه 2 یا 3 تا همسایه داشته باشه به زندگی سلولیش ادامه میده :)
- هر سلول فوت کرده اگه دقیقا 3 تا همسایه داشته باشه به طور موجزه آسایی یهوی زنده میشه!
پیاده سازی مدل بازی زندگی
خب برای اینکه کد ها درکشون آسون تر باشه و راحت تر بشه خروجی گرفت برای همین با زبان جاوا اسکریپت پیاده میکنیم. خب مرحله اول یه سند html برای نمایش لازم داریم و یه استایل ریزی هم میدیم:
<head>
<title>Game of Life :D</title>
<style>
body {
min-height: 100vh;
}
canvas {
max-width: 100%;
height: 100vh;
image-rendering: pixelated;
image-rendering: crisp-edges;
}
</style>
</head>
<body>
<canvas id="game"></canvas>
</body>
دقت کنید توی خروجی مدلسازی بعدا میبینیم اگه بخوایم تصویر شفاف و واضح باشه نیازه که این دو تا استایل image-rendering: pixelated و image-rendering: crisp-edges به canva اضافه کنیم و تصویر واضح تر و گوشه های پیکسل ها تیز تر رندر بشه. عکس پایینو ببینید:
پیاده سازی قوانین و ساختار مدل
خیلی سریع و سامورایی بریم سر اصل مطلب. کار هایی که نیازه انجام بدیم:
- دنیایی با ابعاد دلخواه بسازیم: به صورت یه ماتریس دو بعدی میشه.
- جمعیت اولیه رو بسازیم: به صورت رندوم 0 (سلول مرده) و 1 (سلول زنده) میسازیم.
- حالت بعدی سلول رو پیدا میکنیم: اون بالا هم فهمیدیم حالت های هر سلول به تعداد همسایه هاش مربوطه و طبق قوانین محکوم به مرگ میشه یا نسلش ادامه پیدا میکنه.
- نسل جدید رو بدست میاریم: حالا که مشخص شد کی زنده میمونه و کی دار فانی رو ... بله الان نسل جدید به دست میاد. که احتمال خیلی زیاد کمتر از حالت اولیه بشه.
- الان سلول هارو نشون میدیم: طبق 0 و 1 ای که به دست اومده تصویرش رو ایجاد میکنیم.
نوشتن کد قوانین و مدل
- مرحله اول - ایجاد دنیا: یه طول و عرضی برا دنیا فرض میکنیم. میتونیم نسبت به تعداد سلول هامون بگیم مثلا 100 تا سلول بتونه تو عرض دنیامون قرار بگیره و این حرفا یا اینکه ثابت بگیم طول دنیا 200 پیکسل هست حالا چند تا سلول میتونه جا بشه به ما مربوط نیست.
و دو تا هم آرایه میخوایم تا جمعیت الان و نسل بعدی رو ذخیره کنیم:
const canvas = document.getElementById("game");
const ctx = canvas.getContext("2d");
const width = 300; // طول دنیا
const height = 150; // عرض دنیا
const cell_size = 1; // اندازه سلول ها
let current_population = [ ]; // جمعیت کنونی
let next_population = [ ]; // نسل جدید
- مرحله دوم - ایجاد جمعیت اولیه: میایم و اول آرایه دو بعدی مون رو درست میکنیم بعدش جمعیت اولیه رو با یه تابع رندوم مقدار دهی میکنیم:
const initialize = ( ) => {
for (let y = 0; y < height; y++) { // برا هر خونه آرایه، یه آرایه دیگه ایجاد میکنیم
current_population[y] = [];
next_population[y] = [];
for (let x = 0; x < width; x++) // مقدار 0 یا 1 به جمعیت ها میدیم
current_population[y][x] = Math.floor(Math.random() * 2);
}
};
- مرحله سوم - شمارش همسایه های سلول: خب برای اینکه تعیین و تکلیف کنیم که سلول ما الان میمیره یا زنده میمونه نیازه که تعداد همسایه هاش رو بشماریم:
const countCellNeighbors = (x, y) => {
let count = 0;
for (let yf = -1; yf <= 1; yf++) // ستونی شمارش میکنیم
for (let xf= -1; xf <= 1; xf++) // سطری شمارش میکنیم
// اگه همسایه سلول ما زنده بود یعنی 1 هست و میشماریم
count += current_population[(y + yf+ height) % height][(x + xf+ width) % width];
return count - current_population[y][x]; // خود سلول رو نمیخوایم بشماریم
}
- مرحله چهارم - ایجاد نسل جدید: الان اون چهار تا قانونی که داشتیم رو پیاده سازی میکنیم. شرط هارو ساده نوشتم که واضح تر بشه میشه خلاصه تر از اینا هم نوشت:
const updateGeneration = () => {
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const neighbors = countCellNeighbors(x, y); // تعداد همسایه های سلول
let next_state = 0; // پیشفرض حالت بعدی اینه سلول مرده
if (current_population[y][x] == 0 && neighbors == 3)
next_state = 1; // سلول مرده وقتی سه تا همسایه داره زنده میشه
else if (current_population[y][x] == 1 && (neighbors > 3 || neighbors < 2))
next_state = 0; // سلول زنده وقتی همسایه کمتر از 2 تا یا بیشتر از 3 تا داره میمیره
else
next_state = current_population[y][x]; // سلول دست نخورده باقی میمونه
next_population[y][x] = next_state;
}
}
}
- مرحله پنجم - نمایش مدل: الان اطلاعات کافی رو بدست آوردیم کافیه که اون رو تو canva که بالاتر توی سند html ایجاد کردیم نمایش بدیم:
const draw = () => {
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) { // اگه سلول مرده سیاه بشه وگرنه سفید
ctx.fillStyle = next_population[y][x] == 1 ? "white" : "black" // رنگ سلول ها سفید
ctx.fillRect(x * cell_size, y * cell_size, cell_size, cell_size); // ساخت پیکسل سلول
}
}
}
خب الان فقط تابع اصلیمون موند که بنویسیم. هر بار که میخوایم تابع فراخوانی بشه، جمعیت ما آپدیت میشه بعد نسل جدید که به دست اومد جاش با نسل قبلی تغییر میکنه و بعد دیتا رو نمایش میدیم و دوباره همین کار هارو با دوره تناوب 100 میلی ثانیه انجام میدیم:
const main = () => {
updateGeneration();
// جای جمعیت الان رو با نسل جدید عوض میکنیم
[current_population, next_population] = [next_population, current_population];
draw();
setTimeout(main, 100); // سرعت هر ایترشین رو اعمال میکنیم
}
initialize();
main();
خروجی کد به شکل زیره: شکل اول بازی زندگی شروع شده و سلول ها دارن میمیرن و متولد میشن. شکل دوم بعد مدتی سلول ها به حالت نسبتا پایداری در اومدن:
اگه به شکل های سلول ها دقت کنیم میبینیم به شکل های خاصی در اومدن. اگه به لیست شکل های معروف نگاه کنید اینارو میبینید:
کد پروژه رو میزارم توی گیتهاب. میشه بهتر و خوشگل ترش کرد ولی از اونجایی که بنده یه ذره کمالگرایی داشتم (الان خیلی بهتر شدم) کدش رو زدم بعدا وقت کردم بهترش میکنم :)
ممنونم که وقت گذاشتی و خوندی. امیدوارم لذت برده باشی.
مطلبی دیگر از این انتشارات
تفاوت بین == و === در کاتلین
مطلبی دیگر از این انتشارات
برنامه نویسی داینامیک؛ راه حل الگوریتم های پیچیده
مطلبی دیگر از این انتشارات
چگونه برای پروژه های توسعه یافته تست بنویسیم؟