ساخت بازی دوز با سی‌پلاس‌پلاس

قسمت 12 از دوره C++

معرفی

آشنایی با آرایه‌های دو بعدی

ساخت صفحه بازی در حافظه

علامت گذاری صفحه

چاپ و نمایش صفحه بازی

جلوگیری از انتخاب‌های غیرمجاز

نتیجه گیری

معرفی
آشنایی با آرایه‌های دو بعدی
ساخت صفحه بازی در حافظه
علامت گذاری صفحه
چاپ و نمایش صفحه بازی
جلوگیری از انتخاب‌های غیرمجاز
نتیجه گیری

سلام، به دوره سی‌پلاس‌پلاس دانشجا خوش آمدید. در این پست می‌خواهیم با آرایه‌های ۲ بعدی و آشنا شویم و از آن‌ها برای ساخت یک بازی دوز استفاده کنیم.

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

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

برای تعریف یک آرایه دو بعدی ما تعداد سطرها و ستون ها را در براکت جلوی اسم متغیر می‌نویسیم:

int chart[3][5];

برای مقدار دهی اولیه از براکت‌های تو در تو استفاده می‌کنیم که هر مجموعه کوچکتر یک سطر را نمایش می‌دهد:

int chart[3][5] = {{1, 2, 3, 4, 5},
                   {1, 2, 3, 4, 5},
                   {1, 2, 3, 4, 5}};

و برای دسترسی به یک المان یا تغییر آن مجددا دو براکت برای تعیین سطر و ستون مدنظرمان می‌نویسیم.

cout << chart[1][2] << endl; // Prints the value in row 1, column 2 -> 3
chart[1][2] = 6; // Modifies the value
cout << chart[1][2] << endl; // Prints 6

حالا که با آرایه‌های دو بعدی آشنا شدید به پیاده‌سازی بازی دوز می‌رسیم.

از جایی که دوز یک صفحه ۳ در ۳ دارد ما هم آرایه دو بعدی خود را به شکل زیر از نوع کارکتر (برای نگه داری ‘x’ یا ‘o’) تعریف می‌کنیم:

char board[3][3] = {};

این خط آرایه را تعریف و تمام خانه‌های آن را معادل ۰ یا خالی قرار می‌دهد.

همچنین یک متغیر boolean تعریف می‌کنیم تا نوبت بازیکن‌ها را نگه دارد. چون فقط دو بازیکن داریم یک متغیر بولین که فقط می‌تواند true (بازیکن X) یا false (بازیکن O) دریافت کند برای تعیین بازیکن کافیست.

bool isXTurn = true;

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

while (true) {
    int cell;
    cout << (isXTurn ? 'X' : 'O') << "'s turn, please choose a cell: ";
    cin >> cell;
}

استفاده از اوپراتور Ternary یا اوپراتور شرطی برای تعیین بازیکن

 یک ابزار کاربردی در سی‌پلاس‌پلاس اوپراتور شرطی‌است که به نوعی شکل خلاصه شرط است و به ما کمک می‌کند بر مبنای یک شرط بین دو مقدار انتخاب کنیم:

condition ? value_if_true : value_if_false;

مثلا در این مثال از این اوپراتور برای تعیین اسم بازیکنی که نوبتش شده در دستور cout استفاده کردیم. اگر شرط ما یعنی متغیر isXTurn مقدار true داشته باشد کارکتر 'X' انتخاب و چاپ می‌شود در غیر این صورت 'O' چاپ می‌شود.

cout << (isXTurn ? 'X' : 'O') << "'s turn, please choose a cell: ";

وقتی که یک بازیکن نوبت خود را بازی می‌کند، ما باید بازیکن را جا به جا کنیم تا در دست بعدی نوبت به حریف این بازیکن برسد که برای انجام این کار از این دستور استفاده می‌کنیم:

isXTurn = !isXTurn;

این خط متغیر boolean ما را toggle می‌کند. یعنی مقدار آن را نقیض می‌کند. ابتدا آن را خوانده و با استفاده از اوپراتور ! (نات/نقیض منطقی) آن را عکس می‌کند (اگر true باشد false و بالعکس) و بعد این مقدار جدید داخل همان متغیر ذخیره می‌شود. این باعث می‌شود که پس از هر بازی، نوبت عوض شود. 

فرض کنید صفحه بازی ما به شکل زیر شماره گذاری شده:

1 2 3
4 5 6
7 8 9

زمانی که بازیکنی یک خانه انتخاب می‌کند، ما باید 'X' یا 'O' در آن خانه قرار دهیم. برای پیدا کردن ردیف و ستون خانه مدنظر کاربر باید عدد ورودی کاربر که یک تک عدد است و اصطلاحا یک مقدار تک بعدیست را به مقدار دو بعدی دارای دو اندیس سطر و ستون تبدیل کنیم.

چون صفحه بازی از ۳ ردیف و ۳ ستون تشکیل شده ما ورودی کاربر را بر ۳ (که تعداد خانه‌ها در هر سطر است) تقسیم می‌کنیم تا سطر را به دست آوریم و بعد باقی‌مانده تقسیم این عدد را بر ۳ نیز محاسبه می‌کنیم تا ستون مدنظر را پیدا کنیم. تنها مساله‌ای که باقی می‌ماند این است که کاربر ورودی‌ای از یک تا نه وارد می‌کند اما اندیس‌های ما از صفر شروع می‌شوند (درواقع خانه‌های ۰ تا ۸ را داریم). برای حل این مساله، ما یکی از ورودی کم می‌کنیم:

cell--;

حالا به شکل زیر محاسبه می‌کنیم:

  • cell / 3 →  اندیس ردیف.
  • cell % 3 → اندیس ستون.

مثلا اگر ورودی کاربر ۵ باشد:

  • 4 / 3 = 1 → ردیف یک (اعشار حذف می‌شود چون داریم یک مقدار int را تقسیم می‌کنیم)
  • 4 % 3 = 1 → ستون یک

این به درستی اندیس (1, 1) که وسط صفحه را نشان می‌دهد را انتخاب می‌کند.

برای پیدا کردن کارکتری که باید در آن خانه قرار بگیرد نیز مجددا از اوپراتور شرطی استفاده می‌کنیم:

board[cell / 3][cell % 3] = isXTurn ? 'X' : 'O';

تا اینجا ما یک صفحه بازی داریم که حرکات روی آن به درستی به روز می‌شوند و نگه‌داری می‌شوند. اما کاربر برنامه‌ما هیچوقت این صفحه بازی را نمیبیند چون هیچوقت آن را چاپ نکرده‌ایم.

پس یک تابع به اسم printBoard می‌نویسیم که صفحه بازی را چاپ کند. در این تابع از دو حلقه for تو در تو استفاده می‌کنیم که در تمام سطر و ستون‌ها حرکت کنیم. حلقه بیرونی روی سطرها و حلقه درونی روی ستون‌ها حرکت خواهند کرد.

void printBoard() {
    for (int i = 0; i < 3; i++) { // Loop through rows
        for (int j = 0; j < 3; j++) { // Loop through columns
            cout << '\t' << board[i][j]; // Print cell content with spacing
            if (j != 2) cout << "\t|"; // Add a separator between columns
        }
        cout << endl; // Move to the next row
    }
}

توضیحات این تابع:

  • حلقه بیرونی (i) روی سطر ها حرکت می‌کند.
  • حلقه درونی (j) روی ستون ها حرکت می‌کند.
  • یعنی به ازای هر سطر حلقه درونی کاری را به ازای هر ستون انجام می‌دهد که یعنی اگر اندیس (i, j) را برداریم هر بار یک خانه خاص را بررسی می‌کنیم.
  • یک \t (کارکتر تب) بعد از هر خانه چاپ می‌کنیم تا بین این خانه‌ها فاصله ایجاد کنیم. کارکتر تب که با \t نمایش داده می‌شود مثل \n یک کارکتر ویژه است که به جای رفتن به خط بعد فاصله بین کارکترها ایجاد می‌دهد. تب عموما معادل ۸ اسپیس تلقی می‌شود، اما ویژگی خاص آن این است که فاصله‌ای بین ۱ تا ۸ اسپیس با کارکتر بعدی حفظ می‌کند تا فاصله آن با کارکتر قبلی حداکثر ۸ کارکتر باشد. به لطف این تغییر تعداد تب می‌تواند برای چاپ کردن جدول استفاده شود:
A        D
ABC D

با استفاده از تب

A        D
ABC D

با استفاده از ۸ اسپیس ثابت

  • یک بار عمودی یا پایپ (|) نیز بین خانه‌ها چاپ می‌شود که آن‌ها را به صورت بصری از هم جدا کند. نکته مهم این که یک کارکتر پایپ آخر هر خط اضافه خواهد آمد که با استفاده از یک شرط آن را حذف می‌کنیم.
  • بعد از چاپ همه ستون‌های هر ردیف با cout << endl; کرسر را به خط بعد برای چاپ ردیف بعدی می‌بریم.

پاک کردن کنسول برای نمایش صحیح جداول

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

cout << "\033[2J\033[H";

این رشته ویژه صفحه را پاک می‌کند و کرسر را به بالای صفحه منتقل می‌کند، که تضمین می‌کند همیشه آخرین ورژن برد روی صفحه نمایش داده می‌شود.

اجرای تابع printBoard در حلقه بازی

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

while (true) {
    int cell;
    cout << (isXTurn? 'X' : 'O') << "\'s turn, please choose a cell: ";
    cin >> cell;
    cell--;
    board[cell / 3][cell % 3] = isXTurn? 'X' : 'O';
    isXTurn = !isXTurn;
    printBoard();
}

با اجرا شدن این تابع صفحه بازی نیز به صورت بصری به روز شده و به کاربر نمایش داده می‌شود.

چند مشکل ریز هنوز در بازی ما وجود دارد که مربوط به validate شدن مقادیر ورودی کاربر باز می‌گردد:

اول این که بازیکنان هنوز می‌توانند حرکت‌های موجود را overwrite کنند یعنی خانه‌های دیگر بازیکن دیگر را تغییر دهند. که برای حل این مشکل یک شرط اضافه می‌کنیم تا چک کند آن خانه خالی باشد و تغییر نکرده باشد.

while (true) {
    int cell;
    cout << (isXTurn? 'X' : 'O') << "\'s turn, please choose a cell: ";
    cin >> cell;
    cell--;
    if (board[cell / 3][cell % 3] == 0) {
        board[cell / 3][cell % 3] = isXTurn? 'X' : 'O';
    } else {
        cout << "Please choose a cell that hasn\'t been chosen before" << endl;
        continue;
    }
    isXTurn = !isXTurn;
    printBoard();
}

اگر خانه انتخابی کاربر پر شده باشد، ما پیامی چاپ می‌کنیم تا به کاربر بگوییم یک خانه دیگر انتخاب کند و از دستور continue نیز استفاده می‌کنیم که تغییر نوبت را رد کند و همان بازیکن نوبت بازی دیگری بگیرد.

ما همچنین مقدار input را validate می‌کنیم تا مطمین باشیم این مقدار بین ۱ تا ۹ باشد.

if (cell > 9 || cell < 1) {
    cout << "Please choose a number from 1 to 9" << endl;
    continue;
}

با اضافه کردن این چک‌ها ما تضمین می‌کنیم که حرکات ناصحیح اجازه داده نمی‌شوند و بازی به روانی اجرا می‌شود.

در این مرحله ما یک بازی دوز با عملکرد صحیح داریم که

✅ بازیکنان سر نوبت خود بازی می‌کنند.
✅ صفحه بازی بعد از هر حرکت بازیکنان بروز می‌شود.
✅ حرکات ناصحیح به درستی هندل می‌شوند.

اما یک قابلیت مهم که همچنان پیاده نکرده‌ایم انتخاب برنده بازی است.

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

این مساله را در پست بعدی حل خواهیم کرد. منتظرتان هستیم.

وارد شوید تا پیشرفت خود را ثبت کنید

وارد شوید تا پروژه‌هایی که تکمیل می‌کنید را علامت گذاری کنید و فرایند یادگیری خود را ثبت کنید

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

برای نوشتن نظر٬ اول باید وارد شوید

مرحله بعد

معرفی

اطلاعات شما با موفقیت ثبت شد. کارشناسان ما در اسرع وقت با شما تماس خواهند گرفت.

از شکیبایی شما متشکریم.