AI - هوش مصنوعی, Cute, توصیه شده, هوش مصنوعی

پیاده سازی هوش مصنوعی شطرنج – قسمت دوم – بازی tic-tac-toe

tic-tac-toe یا دوز رو احتمالا خیلی از دوستان بشناسند، یک بازی پرطرف دار قدیمی است که مکانیسم ساده ای دارد. این بازی ۲ نفره است و در آن بازیکن ها سعی می‌کنند سه مهره خود را به ردیف در جایگاه ها بنشانند و در این صورت پیروز خواهند بود. ردیف های افقی یا عمودی یا مورب فرقی نخواهد داشت و هر کدام باعث پیروزی در بازی خواهد شد. حریف سعی خواهد کرد با قرار دادن مهره های خود جلوی ردیف شدن ۳ مهره از شما را بگیرد. در قسمت اول از آموزش هوش مصنوعی شطرنج الگوریتم minmax را توضیح دادیم و به شکل فرضی نحوه عملکرد آن را توضیح دادیم. در این قسمت سعی خواهیم کرد به شکل عملی با پیاده سازی عملی بازی دوز (tic-tac-toe) نشان دهیم چطور می‌توان با استفاده از این الگوریتم یک رقیب خوب (در واقع رقیبی که حرکات حساب شده دارد) پیاده سازی کرد. با ما همراه باشید!

 

مختصری در باره بازی tic-tac-toe

بازی tic-tac-toe مدل‌های مختلفی دارد، مدل ۱۲ تایی یا ۶ تایی یا ۹ تایی سری ۱۲ تایی از همه سخت‌تر است و نیاز به تمرکز زیادی دارد، در این آموزش ما ساده‌ترین نوع دوز را پیاده سازی خواهیم کرد! چرا که هم تعداد حالت‌هایی که نیاز به پردازش دارند کم‌تر هستند و مدل برنامه ساده‌تر و قابل‌درک‌تر خواهد بود مخصوصاً برای هدف ما که آشنایی بیشتر با الگوریتم minmax است و هم این که مرسوم‌تر است و احتمالاً اغلب دوستان یک بار این بازی را تجربه کرده‌اند.

 

بازی دوز

همان‌طور که در تصویر مشخص است، در این بازی ۹ جای خالی وجود دارد که هر بازیکن مهره خود را به نوبت در هر کدام از موقعیت‌ها که دوست دارد قرار می‌دهد بازیکن‌ها باید سعی کنند که مهره‌های خود (سه مهره) را در یک راستای عمودی یا افقی یا مورب قرار دهند، در صورت بروز چنین حالتی بازیکن برنده بازی خواهد بود مثل عکس زیر:

تیک تاک تویی نام های دیگری مثل ایکس او یا Noughts and Crosses نیز دارد.

همین‌طور می‌توانید به‌صورت آنلاین آن را بازی کنید.

 

از کجا شروع کنیم؟ زمین!

قرار است با استفاده از الگوریتم minmax یک هوش مصنوعی برای بازی tic-tac-toe پیاده سازی کنیم که حالت‌های ممکن را در نظر گرفته و بهترین حرکت ممکن را انجام دهد، برای این کار اول لازم است که کامپیوتر یا میکروکنترلر از زمین بازی درک درستی داشته باشد، از آنجایی که ما قرار است ۹ مکان برای نگهداری مهره‌ها داشته باشیم برای تعریف زمین بازی می‌توانیم به سادگی از یک آرایه ۳ در ۳ استفاده کنیم، البته روش بهینه‌تری برای ذخیره زمین بازی وجود دارد ولی از آنجایی که مقاله بیشتر جنبه آموزشی دارد ما سعی خواهیم کرد ساده‌ترین حالت ممکن را در نظر بگیریم.

میدانیم که هر خانه از زمین بازی می‌تواند ۳ حالت داشته باشد، یا خالی است یا مهره بازیکن X در آن است یا مهره بازیکن O در خانه‌های آن قرار دارد. پس قبل از تعریف زمین لازم است متغیری را تعریف کنیم که وضعیت هر خانه از زمین را نمایش دهد. برای این کار از enum استفاده می‌کنیم. در نهایت تعریف زمین بازی به شکل زیر خواهد بود.

حالا که زمین بازی را تعریف کردیم، نیاز است که بتوانیم برد را چاپ کنیم (نمایش دهیم) برای این کار هم تابع زیر را به شکلی که می‌بینید می‌نویسم:

فکر می‌کنم تابع فوق عملکردش به اندازه کافی روشن است و نیاز به توضیح خاصی ندارد. این تابع متغیری تحت عنوان زمین بازی از ما دریافت می‌کنید و آن را به شکلی که قابل درک برای ما باشد نمایش می‌دهد. در نهایت تابع خروجی به شکل زیر خواهد داشت.

 

آیا بازی تمام شده است؟

گام بعدی برای پیاده سازی هوش مصنوعی این است که چک کنیم ببینیم آیا بازی تمام شده است یا نه، همان‌طور که قبلاً توضیح داده‌ام در الگوریتم minmax ما باید تمام حالت‌های ممکن را چک کنیم و حالتی که بیشترین امتیاز را نسیب ما می‌کنید را انتخاب کنیم، برای این که بتوانیم تمام حالت‌های ممکن را چک کنیم نیاز است بدانیم آیا زمین بازی جای خالی برای حرکت جدید دارد یا نه! خوب پیاده سازی این تابع هم مثل قبلی‌ها ساده است تنها کافی است چک کنیم و ببینیم آیا موقعیت خالی در زمان بازی وجود دارد یا نه! تابع آن به شکل زیر خواهد بود.

عملکرد تابع بسیار ساده است، تک‌تک خانه‌های بازی را چک می‌کند، اگر خانه‌ای خالی مانده باشد پس هنوز می‌توان حرکت جدیدی انجام داد و به همین دلیل تابع مقدار true برمی‌گرداند.

 

آیا کسی برنده شده است؟

همان‌طور که در قسمت قبل گفتیم، در الگوریتم minmax ما دو بازیکن داریم یکی حداکثرکننده و دیگری حداقل‌کننده، در انجام حرکت‌های مختلفی که هرکدام از بازیکن‌ها انجام می‌دهند باید قادر باشند که بررسی کنند آیا حرکتی که انجام داده‌اند فارغ از این که باعث تمام شدن بازی شده است یا خیر باعث برد یا باخت رقیب شده است یا نه،‌ یعنی نیاز داریم که تابعی داشته باشیم که با بررسی زمین بازی بتواند بگوید کدام بازیکن برنده شده است و یا این که هیچ کدام برنده نشده است.

در واقع این تابع با توجه به وضعیت مهره‌ها ارزیابی می‌کند کدام بازیکن برنده شده است یا این که هنوز بازی هیچ برنده‌ای ندارد، این تابع بسته به نوع بازی به شکل‌های مختلفی نوشته می‌شود و اتفاقاً نوع نوشتن این تابع در هوش نمایش داده شده از برنامه تأثیر مستقیم دارد. ایده اولیه پشت این تابع آن است که برای حداکثرکننده مقدار مثبت خروجی دهد و برای بازیکن حداقل‌کننده مقدار منفی، و اگر هیچ کدام از بازیکن‌ها برنده نبودند خروجی صفر (بی طرف) برگرداند. در این سناریو ما فرض می‌کنیم که بازیکن X یک بازیکن حداکثرکننده است و بازیکن O بازیکن حداقل‌کننده است. خوب بیایید تابع رو بنویسیم:

 

۱. اگر بازیکن X (حداکثر کننده) برنده بازی باشد تابع ما مقدر مثبت ۱۰ را بر می‌گرداند.

۲. اگر بازیکن O (حداقل‌کننده) برنده بازی باشد تابع ما مقدار منفی ۱۰ را بر می‌گرداند.

 

۳. اگر هیچ کدام از بازیکن‌ها برنده نشده باشند، آنگاه تابع ارزیابی کننده مقدار صفر را بر می‌گرداند

ما می‌توانستم هر مقداری مثبت یا منفی دلخواهی استفاده کنیم تنها برای سادگی مقدار ۱۰ را انتخاب کردیم. تابع بورد ۳ در ۳ را ورودی می‌گیرد و تک تک ردیف‌ها و ستون را بررسی می‌کند و چک می‌کند که آیا بازیکنی موفق شده است سه مهره را پشت سر هم قرار دهد یا نه البته دو ردیف مورب هم وجود دارد که آنها را نیز چک می‌کند.

 

تابع حرکت ساز

تا اینجا فکر می‌کنم همه چیز روشن باشد حالا با توجه به ماهیت عملکردی تابع مینمکس نیاز است که بتوانیم حرکت‌های مختلفی را ایجاد کنیم بعد با استفاده از تابع ارزیابی امتیاز آن حرکت را ارزیابی کنیم. تابع حرکت ساز نقش مهمی در میزان هوش برنامه بازی می‌کند، البته در بازی tic-tac-toe پیاده سازی این تابع خیلی ساده است چرا که هیچ قاعده خاصی برای حرکت مهره‌ها وجود ندارد و تنها کافی است که جای خالی را پیدا کنیم و مهره مورد نظر را در آن قرار دهیم ولی در بازی‌های پیچیده‌تر مثل شطرنج یکی از مهم‌ترین توابعی که وجود خواهد داشت همین تابع است. در بازی شطرنج مهره‌ها باید طبق قاعده بازی حرکت کنند و هر مهره الگو (pattern) حرکتی منحصر به فرد خود را دارد و آنجاست که اهمیت این تابع مشخص می‌شود.

با توجه به این که عملکرد این تابع به عملکرد تابع minmax گره خورده است چرا که باید هر حرکت محتمل را وزن دهی کنیم، تابع را تنها به شکل شبه برنامه پیاده سازی کنیم و در ادامه کد دقیق آن را بنویسیم.

همان‌طور که مشخص است عملکرد این تابع زیاد هم پیچیده نیست، تمام حرکت‌های ممکن را می‌سنجیم و می‌بینیم که کدام حرکت امتیاز بیشتری در بر خواهد داشت که نهایتاً همان حرکت را انجام خواهیم داد.

 

محاسبه امتیاز هر حرکت (minmax)

خوب حالا می‌رسیم به قسمت چالشی هوش مصنوعی، چطور امتیاز یک حرکت را باید حساب کنیم؟، برای این کار نیاز است برنامه به نوعی با خودش بازی را ادامه دهد یعنی به سعی کند عکس‌العمل شما را حدس بزند و ببینید آیا این حرکتی که انجام داده است آیا باعث پیروزی‌اش در بازی می‌شود یا خیر! این کار به شکل تابع برگشتی انجام می‌دهیم مکانیسمی مثل پیاده سازی تابع فاکتوریل!

همان‌طور که میدانید که در الگوریتم مینمکس دو بازیکن وجود دارد یکی در تلاش برای زیاد کردن امتیاز و دیگری برای تلاش برای کم کردن امتیاز است! برای این که بتوانیم به شکل بازگشتی (ساده‌تر) از این تابع استفاده کنیم نیاز نیست دوتابع مختلف بنویسیم تنها کافی است که نوع بازیکن را به شکل پارامتر در ورودی دریافت کنیم. به شبه کد زیر دقت کنید:

همان‌طور که در شبه کد می‌بینید مثلاً برای کاربر حداکثر کننده ما از منفی‌ترین مقدار ممکن شروع به سنجش حرکت‌های ممکن (تابع ایجاد حرکت) می‌کنیم و همیشه به دنبال بیشترین امتیاز ممکن هستیم. در قیمت حداقل کننده از مثبت‌ترین امتیاز ممکن شروع می‌کنیم و به دنبال کم کردن امتیاز هستیم. بعد از انجام یک حرکت مجدداً خود تابع را صدا می‌زنیم برای تخمین حرکت بازیکن مقابل.

بگذارید برای روشن شدن بیشتر عملکرد تابع با تصویر عملکرد شبه کد را نمایش دهیم.

در بالاترین سطر بازیکن حداقل کننده (O) بازی کرده و حالا نوبت بازیکن حداکثر کننده است (X) سه موقعیت خالی وجود دارد که می‌تواند در آنها بازی کند (پایین‌ترین ردیف بازی) پس بازیکن حداکثرکننده (X) مهره خود را در هر موقعیت قرار داده و بررسی می‌کنید در هر کدام از موقعیت‌ها احتمال برد بیشتر است.

حداکثرکننده پس بعد از هر انتخاب تابع minmax را برای حداقل کننده (O) فراخوانی می‌کند و تا او بازی را با قواعد خودش انجام دهد و بعد مجدداً تابع minmax را برای بازیکن حداکثرکننده صدا میزند و این فرایند تا تمام شدن تمام حالت‌های ممکن ادامه پیدا می‌کند و امتیازها محاسبه می‌شود و از بین امتیازها حرکتی انتخاب می‌شود که بالاترین امتیاز را داشته باشد.

 

کمی هوشمندتر

به مسائل بالا دقت کنید، اگر بازیکن حداکثرکننده (X) مهره خودش رو وسط یا سمت راست بذار توی هر کدام از حالت‌ها امکان برنده شدن هست، یعنی امتیازش میشه +۱۰ ولی اگر وسط رو انتخاب کنه به احتمال ۵۰ درصد ممکنه بازی رو نبره! و بازی مساوی بشه که اون ۵۰ درست بستگی به نحوه بازی حداقل‌کننده (O) داره، یا این که بازی طوری پیش بره که توی هر دو احتمال امکان برنده شدن وجود داشته باشه و هیچ مساوی‌ای هم در کار نباشد ولی برای برنده شدن در یک حالت اول نیاز به ۲ حرکت باشه در حالت دوم نیاز به ۴ حرکت باشد! منطقی‌تر هست که با ۲ حرکت بازی را برنده شویم به جای ۴ حرکت، اما چطور می‌شود چنین چیزی را پیاده سازی کرد؟ ساده است اینجا ما می‌توانیم تعداد حرکات رو در الگوریتم محاسبه امتیاز در نظر بگیریم هر حرکت اضافه (عمق بیشتر) امتیاز منفی در بر خواهد داشت، که به عبارتی می‌شود برای حداکثرکننده (X) و امتیاز مثبت خواهد داشت برای حداقل کننده (O) با این شرایط برنامه سعی خواهد کرد در سریع‌ترین حالت ممکن بازی را برنده شود.

در انتها برنامه به شکل زیر خواهد بود 

سورس کد برنامه را برای STM32f030f4 می‌توانید از گیت هاب سیسوگ دانلود کنید.

انتشار مطالب با ذکر نام و آدرس وب سایت سیسوگ، بلامانع است.

شما نیز میتوانید یکی از نویسندگان سیسوگ باشید.   همکاری با سیسوگ

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *