لوحة المتصدرين
المحتوى الأكثر حصولًا على سمعة جيدة
المحتوى الأعلى تقييمًا في 03/09/22 في كل الموقع
-
هل هذه هي الطريقة الصحيحة لعمل الحدث في livewire ؟ <!--خطأ هذا الايفينت لا يعمل علي مستوي المشروع ككل --> <a href="#" style="margin-left:10px;" wire:click.prevent='deleteproduct({{$product->id}})'> </a> <!--عند االضغط علي الرابط يتم الذهاب الي href ولا يتم تنفيذ الحدث -->2 نقاط
-
1 نقطة
-
السلام عليكم : لدي قالب أضفت له تعريب لقالب ابن ، المشكلة لا تظهر التعريب بشكل تلقائي ، في ملف القالب الأب أنشأت ملف بإسم languages ووضعت بداخلة ملفين الترجمة ar.po , ar.mo وفي ملف functions.php أضفت الدالة : load_child_theme_textdomain( 'إسم القالب', get_stylesheet_directory() . '/languages' ); مع تغيير اسم القالب ، هل هذه الطريقة صحيحة؟1 نقطة
-
لو كان لدي جدول بيانات يحتوي على حقول مختلفة منها ارقام الهواتف على سبيل المثال قمت انا بأدراج بيانات الهواتف في حقل phone من خلال أوامر sql- Insert () واصبح لدي على سبيل المثال حقول بيانات تحتوي على تسلسل ID من رقم 1 الى 100 ثم اصبح لدي بيانات مختلفة وليكون أسماء وانا امتلك عمود اسمه Name ولكن ارغب بإضافة الأسماء حسب تسلسل ارقام الهواتف ولكن ليس من البداية بمعنى ارغب بأدراج من تسلسل ID مثلا 8 وحتى 34 فقط وسيكوون في نفس موقع row الذي سابقا وضعنا ارقام هواتف فيه رسم توضيحي : هذا اول مرحلة قمت بها : انا اقوم بتنفيذ ذلك من خلال صفحة MySQL مباشر ولا استعمل صفحة php لفعل ذلك صورة لتوضيح : حاولت القيام بشي مثل ذلك : INSERT INTO employees (Name) VALUES (('ahame'), ('sfgtg'), ('yosu') WHERE Id > 2594 AND id < 2592); ولكن الامر لم ينجح من +----+-------------------+------+--------+ | id | Name | phone| Date | +----+-------------------+------+--------+ | 1 | |563824| | | 2 | |525225| | | 3 | |546542| | | 4 | |464625| | | 5 | |654525| | | 6 | |849842| | | 7 | |654446| | +----+-------------------+------+--------+ إلى +----+-------------------+------+--------+ | id | Name | phone| Date | +----+-------------------+------+--------+ | 1 | |563824| | | 2 | |525225| | | 3 | Ali |546542| | | 4 | Ahmad |464625| | | 5 | Ayob |654525| | | 6 | |849842| | | 7 | |654446| | +----+-------------------+------+--------+1 نقطة
-
1 نقطة
-
mySQL لا تستعمل المصفوفات، لذلك كحل بديل عليك كتابة الأسماء جميها ضمن سلسلة نصية (بينهم فاصلة) و من ثم ضمن الحلقة نستخلص اسم لكل تكرار و نستعمله في الشيفرة، لتصبح كما يلي: set @id1 := 5; set @id2 := 10; set @i := @id1; SET @names = 'marwan,wael,adnan,sameh,eyad,'; while (LOCATE(',', @names) > 0) //@i <= @id2 do SET @name = ELT(1, @names); SET @names= SUBSTRING(@names, LOCATE(',',@names) + 1); UPDATE users SET userName = @name WHERE userId = @i; set @i = @i + 1; end while; الحل النهائي:: BEGIN set @id1 := 1; set @id2 := 104; set @i := @id1; SET @names = '1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,'; select @i,@names,Locate(',', @names); LABEL1: WHILE @names is not null and @i < @id2 do SET @Name = SUBSTRING_index(@names, ',',1); select @i,@names,@name; SET @names = SUBSTRING(@names, LOCATE(',', @names) + 1); UPDATE employees SET NAME = @Name WHERE id = @i; SET @i = @i + 1; END WHILE label1; SET @Name = SUBSTRING_index(@names, ',',1); select 'and finally' ,@i,@names,@name; end1 نقطة
-
لو كان لدي جدول بيانات يحتوي على حقول مختلفة منها ارقام الهواتف على سبيل المثال قمت انا بأدراج بيانات الهواتف في حقل phone من خلال أوامر sql- Insert () واصبح لدي على سبيل المثال حقول بيانات تحتوي على تسلسل ID من رقم 1 الى 100 ثم اصبح لدي بيانات مختلفة وليكون أسماء وانا امتلك عمود اسمه Name ولكن ارغب بإضافة الأسماء حسب تسلسل ارقام الهواتف ولكن ليس من البداية بمعنى ارغب بأدراج من تسلسل ID مثلا 8 وحتى 34 فقط وسيكوون في نفس موقع row الذي سابقا وضعنا ارقام هواتف فيه رسم توضيحي : هذا اول مرحلة قمت بها : // الارقام توضيحية فقط INSERT INTO table_name (phone) VALUES (654656), (546546), (456456), (645645), (546546), (456456); +----+-------------------+------+--------+ | id | Name | phone| Date | +----+-------------------+------+--------+ | 1 | |563824| | | 2 | |525225| | | 3 | |546542| | | 4 | |464625| | | 5 | |654525| | | 6 | |849842| | | 7 | |654446| | +----+-------------------+------+--------+ الان لدي حقول هواتف حتى تسلسل 7 من تسلسل id ارغب بعمل التالي: +----+-------------------+------+--------+ | id | Name | phone| Date | +----+-------------------+------+--------+ | 1 | |563824| | | 2 | |525225| | | 3 | Ali |546542| | | 4 | Ahmad |464625| | | 5 | Ayob |654525| | | 6 | |849842| | | 7 | |654446| | +----+-------------------+------+--------+ احتاج إضافة أسماء في تسلسل id من 3 الى 5 ليصبح شكل الجدول كما في المثال في أعلاه بحيث اني استطيع تعبئة بيانات مجموعة من الأشخاص دفعة واحده في كل مره احتاج ذلك واستثناء الاخرين هل يوجد طريقة لفعل ذلك؟ من خلال أوامر SQL1 نقطة
-
من الضروري في السؤال تحديد محرك قواعد البيانات ولغة البرمجة في BackEnd. سوف نقوم بعمل حلقة، تقوم بتحديث سطر لكل تكرار حسب شرط id و تقرأ الاسم من مصفوفة في PHP ثم تسند قيمة الاسم في الحقل مقابل id مناسب <?php $id1 = 5; $id2 = 10; $names = Array( 'name1', 'name2', 'name3' ); $serverName = "localhost"; $username = "username"; $password = "password"; $dbname = "databaseName"; try { $conn = new PDO("mysql:host=$serverName;dbname=$dbname", $username, $password); // set the PDO error mode to exception $conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); for($i=$id1 ; $i <= $id2 ; $i++){ $statement = $conn->prepare('UPDATE users SET userName = :userName WHERE userId = :userId'); $statement->bindValue(':userId', $i); $statement->bindValue(':userName', $names[$i]); $statement->execute(); } } catch(PDOException $e) { echo $sql . "<br>" . $e->getMessage(); } $conn = null;1 نقطة
-
السلام عليكم, كنت اتصفح بعض الاسئلة عن الجافاسكريبت ووجدت سؤالين بهم بعض الغموض نوعا ما بالنسبة لي, السؤال الاول: هل ترتيب العناصر في ال Arrays لا يهم؟ وكذلك بالنسبة لخصائص ال object...حسب معلوماتي ان الترتيب لا يهم في ال Array إلا اذا استخدمت .sort() سيتم الترتيب وقتها ابجديآ او من الارقام الصغيرة للكبيرة, فهل هذا صحيح؟ ومع الobject اعتقد ان الترتيب لا يهم, ونص السؤال كان كالتالي: In JavaScript, order of items in an array does not matter? true or false, ونص السؤال الثاني: In JavaScript, order of properties in Object does not matter? true or false وشكرا جزيلا1 نقطة
-
في المصفوفات Arrays يتم ترتيب العناصر كما تم إدخالها إلى المصفوفة، فعلى سبيل المثال يمكننا أن نقوم بعمل مصفوفة بالشكل التالي: const myArr = ['zoom', 'hello', 'hi', 'world', 'app'] console.log(myArr); // ['zoom', 'hello', 'hi', 'world', 'app'] وبالتأكيد فإن الترتيب مهم في المصفوفات، وذلك لأننا نصل إلى أحد القيم في المصفوفة من خلال الفهرس index، وبالتالي إذا كان الترتيب غير مهم فكيف سنصل إلى أحد العناصر في المصفوفة؟! console.log(myArr[0]) // zoom console.log(myArr[3]) // world أما بالنسبة للكائنات Objects فإننا نستخدم المفاتيح keys للوصول إلى القيم، لذلك لا يهم الترتيب على الإطلاق في هذه الحالة: const myObj = {zoom: true, hello: 'some value', app: 'downloaded'} console.log(myObj.zoom); // true console.log(myObj.app); // 'downloaded'1 نقطة
-
public class Employee { String name; int age; double salary; } public class Main { public static void main(String[] args) { name = "Zeina"; age = 21; salary = 1500000; }} قمت بإنشاء كلاس إسمه Employee و فكرته تخزين معلومات الموظفين name ,salary,age بعدها قمت باضافة معلومات الموظف في الكلاس Main لكن لم ينجح الأمر أرجو المساعدة..1 نقطة
-
فكرة البرمجة غرضية التوجه تكمن في عمل صنف Class نقوم بإنشاء أغراض برمجية منه، ثم إسناد قيم لخواص هذه الأغراض و استدعاء الطرائق methods / التوابع المعرفة في الصنف. لذلك علينا إنشاء غرض من الموظف، و إسناد القيم للخواص منه: public static void main(String[] args) { // Employee من الكلاس emp هنا قمنا بإنشاء الكائن Employee emp = new Employee(); // emp هنا قمنا بوضع قيم لخصائص الكائن emp.name = "Zeina"; emp.age = 21; emp.salary = 15000000000; // emp هنا قمنا بعرض قيم خصائص الكائن System.out.println("Name: " + emp.name); // Name: -> Zeina System.out.println("Age: " + emp.age); // Age: -> 21 System.out.println("Salary: " + emp.salary); // Salary: -> 15000000000 } لدينا emp كائن من نوع Employee فيه خواص العمر و الاسم و الراتب، نتعامل مع هذه الخاصيات منه. في حال رغبتنا بطباعة معلومات الموظف كاملة، يمكننا تعريف دالة تختصر الموضوع و تطبع القيم: public class Employee { String name; int age; double salary; public printEmployeeInfo () { // emp هنا قمنا بعرض قيم خصائص الكائن System.out.println("Name: " + emp.name); // Name: -> Zeina System.out.println("Age: " + emp.age); // Age: -> 21 System.out.println("Salary: " + emp.salary); // Salary: -> 15000000000 } } public static void main(String[] args) { // Employee من الكلاس emp هنا قمنا بإنشاء الكائن Employee emp = new Employee(); // emp هنا قمنا بوضع قيم لخصائص الكائن emp.name = "Zeina"; emp.age = 21; emp.salary = 15000000000; // emp هنا قمنا بعرض قيم خصائص الكائن emp.printEmployeeInfo(); }1 نقطة
-
قمت بتعريف كلاس بداخله قمت بتعريف enumيمثل أيام الأسبوع إسمه Days لكن اريد في الدالة main() عرض قيمة رقم الـ Index الخاص في كل يوم هل يمكن المساعدة في ذلك enum Days { MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY } public static void main(String[] args) { }1 نقطة
-
إن العناصر ضمن enum لها دوال مساعدة، مثل الداالة ordinal التي تعيد الترقيم (index) للعنصر المطبقة عليه.. لذلك يمكننا الطباعة بالطريفة التالية: System.out.println(Days.TUESDAY.ordinal()); // -> 1 System.out.println(Days.MONDAY.ordinal()); // -> 0 بدون استدعاء الدالة ستعيد اسم اليوم: System.out.println(Days.SATURDAY); // -> SATURDAY System.out.println(Days.FRIDAY); // -> FRIDAY1 نقطة
-
تأكد من تطابق اسم التابع deleteproduct ضمن صنف المكون مع ذكره ضمن ملف العرض، أيضًا تأكد من كون تابع الحدث public كالتالي: class MyComponent extends Component { public function deleteproduct($product_id) { ^^^^^^ ... } }1 نقطة
-
بشكل عام، الطرق الممكنة التي يمكن بها استدعاء أحداث في ليفواير هي ما كالتالي: من داخل ملف العرض: <button wire:click="$emit('yourEventName')"> من ملف المكون: $this->emit('yourEventName'); من داخل سكربت جافاسكربت: <script> Livewire.emit('yourEventName') </script> بمعنى أن استدعاءك للحدث يجب أن يكون كـ : <button wire:click="$emit('deleteproduct', {{$product->id}})"> كأشياء جانبية أخرى، تأكد من: أن الحدث deleteproduct موجود بالفعل أن الحدث deleteproduct موجود كتابع للمكون المستهدف1 نقطة
-
يمكنك تبديل العنصر a بالعنصر button كالتالي <button style="margin-left:10px;" wire:click="deleteproduct({{$product->id}})">..</button> جرب الأمر1 نقطة
-
خطأ لارافيل ؟ Creating default object from empty value <?php //live wire code namespace App\Http\Livewire\Admin; use App\Models\Category; use App\Models\Product; use Carbon\Carbon; use Illuminate\Contracts\Session\Session; use Livewire\Component; use Illuminate\Support\Str; use Livewire\WithFileUploads; class AdminEditProductComponent extends Component { use WithFileUploads; public $name; public $slug; public $short_description; public $description; public $regular_price; public $sale_price; public $SKU; public $stock_status; public $featured; public $quantity; public $image; public $category_id; public $newimage; public $product_id; public function mount($product_slug){ $product = Product::where('slug',$product_slug)->first(); $this->name = $product->name; $this->slug = $product->slug ; $this->short_description= $product->short_description; $this->description = $product->description; $this->regular_price = $product->regular_price; $this->sale_price = $product->sale_price; $this->SKU = $product->SKU; $this->stock_status = $product->stock_status; $this->featured = $product->featured; $this->quantity = $product->quantity; $this->image = $product->image; $this->category_id = $product->category_id; $this->newimage = $product->newimage ; $this->product_id = $product->product_id; } public function generateslug(){ $this->slug = Str::slug($this->name,'-'); } public function updateProduct(){ $product = Product::find($this->product_id); $product->name = $this->name; $product->slug = $this->slug; $product->short_description = $this->short_description; $product->description = $this->description; $product->regular_price = $this->regular_price; $product->sale_price = $this->sale_price; $product->SKU = $this->SKU; $product->stock_status = $this->stock_status; $product->featured = $this->featured; $product->quantity = $this->quantity; if($this->newimage){ $imageName = Carbon::now()->timestamp.'-' .$this->newimage->extension(); $this->newimage->storeAs('products',$imageName); $product->image = $imageName; } $product->category_id = $this->category_id; $product->save(); Session()->flash('message','Product Has Been Updated Successfully!'); } public function render() { $categories = Category::all(); return view('livewire.admin.admin-edit-product-component',[ 'categories' => $categories ])->layout('layouts.base'); } } <!--ملف العرض view --> <div> <div class="container" style="padding: 30px 0;"> <div class="row"> <div class="col-md-12"> <div class="panel panel default"> <div class="panel-heading"> <div class="row"> <div class="col-md-6"> Edit Product </div> <div class="col-md-6"> <a href="{{route('admin.products')}}" class="btn btn-success pull-right">All Products</a> </div> <div class="panel-body"> @if(Session::has('message')) <div class="alert alert-success" role="alert">{{ Session::get('message')}}</div> @endif <form class="form-horizontal" enctype="multipart/form-data" wire:submit.prevent='updateProduct'> @csrf <div class="form-group"> <label class="col-md-4 control-label">Product Name</label> <div class="col-md-4"> <input type="text" placeholder="Product Name" class="form-control input-md" wire:model='name' wire:keyup='generateslug'> </div> </div> <div class="form-group"> <label class="col-md-4 control-label">Product Slug</label> <div class="col-md-4"> <input type="text" placeholder="Product Slug" class="form-control input-md" wire:model='slug'> </div> </div> <div class="form-group"> <label class="col-md-4 control-label">Short Description</label> <div class="col-md-4"> <textarea type="text" placeholder="Short Description" class="form-control" wire:model='short_description'></textarea> </div> </div> <div class="form-group"> <label class="col-md-4 control-label">Description</label> <div class="col-md-4"> <textarea type="text" placeholder="Description" class="form-control" wire:model='description'></textarea> </div> </div> <div class="form-group"> <label class="col-md-4 control-label">Regular Price</label> <div class="col-md-4"> <input type="text" placeholder="Regular Price" class="form-control input-md" wire:model='regular_price'> </div> </div> <div class="form-group"> <label class="col-md-4 control-label">Sale Price</label> <div class="col-md-4"> <input type="text" placeholder="Sale Price" class="form-control input-md" wire:model='sale_price'> </div> </div> <div class="form-group"> <label class="col-md-4 control-label">SKU</label> <div class="col-md-4"> <input type="text" placeholder="SKU" class="form-control input-md" wire:model='SKU'> </div> </div> <div class="form-group"> <label class="col-md-4 control-label">Stock</label> <div class="col-md-4"> <select class="form-control" wire:model='stock_status'> <option value="instock">In Stock </option> <option value="outofstock">Out Of Stock </option> </select> </div> </div> <div class="form-group"> <label class="col-md-4 control-label">Featured</label> <div class="col-md-4"> <select class="form-control" wire:model='featured'> <option value="0">No</option> <option value="1">Yes</option> </select> </div> </div> <div class="form-group"> <label class="col-md-4 control-label">Quantity</label> <div class="col-md-4"> <input type="text" placeholder="Quantity" class="form-control input-md" wire:model='quantity'> </div> </div> <div class="form-group"> <label class="col-md-4 control-label">Product Image</label> <div class="col-md-4"> <input type="file" class="input-file" wire:model='newimage'> @if($newimage) <img src="{{$newimage->temporaryUrl()}}" width="120"> @else <img src="{{ asset('assets/images/products')}}/{{$image}}" width="120" > @endif </div> </div> <div class="form-group"> <label class="col-md-4 control-label">Category</label> <div class="col-md-4"> <select class="form-control" wire:model='category_id'> <option value="">Select Category</option> @foreach ($categories as $category) <option value="{{$category->id}}">{{$category->name}}</option> @endforeach </select> </div> </div> <div class="form-group"> <label class="col-md-4 control-label"></label> <div class="col-md-4"> <button type="submit" class="btn btn-primary">Update</button> </div> </div> </form> </div> </div> </div> </div> </div> </div> </div> </div>1 نقطة
-
نعم الحمد لله ؟ تم حل الخطأ .1 نقطة
-
1 نقطة
-
نعم الخطأ كان في السطر التالي : $this->product_id = $product->product_id; وتم تعديله الي التالي $this->product_id = $product->id;:1 نقطة
-
هذا الخطأ يشير إلى أنه تقوم بإضافة أو إنشاء كائن من قيمة فارغة لذلك يجب التأكد من أن المتغير التالي $this->product_id يحتوي على قيمة أو لا ، يبدو أنه لا يحتوي على قيمة لذلك لارافل ترجع هذا الخطأ , فلاحظ في الكود التالي $product = Product::find($this->product_id); أنه لا يوجد منتج لتقوم بالتعديل عليه ، يمكنك التأكد من ذلك و من ثم أخباري بالنتيجة لو سمحت ؟1 نقطة
-
أولاً: ما هي yield. yield عبارة عن كلمة مفتاحية في Python تُستخدم للعودة من الدالة بدون تدمير حالة المتغيرات المحلية "local variable" الخاص بها وعندما يتم استدعاء الدالة، يبدأ التنفيذ من حيث توقفت الدالة في المرة الأخيرة عندما صادفت الكلمة المفتاحية yield. أي دالة تحتوي على الكلمة المفتاحية yield تسمى مولد "generator". وبالتالي، فإن yield هو الذي يتم من خلاله صنع المولد. هذه الكلمة المفتاحية قليلة الشهرة في بايثون ومع ذلك لها فائدة أكثر مما قد تتوقعينه. ثانياً:أمثلة لتوضيح عمله. مثال 1: # generator to print even numbers # مولّد لطباعة الأعداد الفردية def printOdd(numbers) : for i in numbers: if i % 2 != 0: yield i # تحديد مجموعة من الأرقام numbers = [1, 2, 5, 4, 9] # طباعة الأعداد الفردية print ("The even numbers in list are : ", end = " ") for j in printOdd(numbers): print (j, end = " ") # الخرج: # The even numbers in list are : 1 5 9 كما ذكرت فإن أي دالة تحتوي على هذه الكلمة المفتاحية يكون اسمها مولّد، وهذا ما يؤكده الاستدعاء التالي: printOdd(numbers) # <generator object printOdd at 0x00000216842B4DC8> لاحظ أيضاً في المثال السابق أن j تقوم بالمرور على عناصر هذا المولد، وفي كل استدعاء له يقوم بالتنفيذ من حيثما توقف بدلاً من العودة للبداية. مثال 2: # برنامج لتوليد مربعات الأعداد من 1 ل 50 # سنعرّف الآن دالة توليد لانهائية تقوم بتوليد مربعات الأعداد بدءاً من 1 def nextSquare(): i = 1 # حلقة لانهائية لتوليد مربعات الأعداد while True: yield i*i i += 1 # الاستدعاء التالي للدالة سيتم فيها بدء التنفيذ من هنا # استدعاء الدالة لتحقيق المطلوب for num in nextSquare(): if num > 50: break print(num) لاحظ أن التنفيذ بعد كل استدعاء يبدأ من التعليمة التي تلي yield، ففي أول استدعاء يبدأ التنفيذ من بداية الدالة، لكن في الاستدعاءات التالي يتم التنفيذ بدءاً من السطر الذي أشرت إليه في الكود.1 نقطة
-
تَعرَّضنا بالقسم الفرعي 3.8.5 للمصفوفات ثنائية البعد (two-dimensional arrays)، ولكننا لم نَستخدِمها بالقدر الكافي منذ ذلك الحين. تَملُك المصفوفات ثنائية البعد نوعًا أيضًا مثل int[][] أو String[][] بزوجين من الأقواس المربعة. تُرتَّب عناصر المصفوفات ثنائية البعد ضِمْن مجموعة من الصفوف والأعمدة بحيث يُخصِّص العامل new كُلًا من عدد تلك الصفوف والأعمدة كالتالي: int[][] A; A = new int[3][4]; تُنشِئ تلك التَعليمَة مصفوفة ثنائية البعد مُكوَّنة من 12 عنصر مُرتَّبين ضِمْن 3 صفوف و 4 أعمدة. يَتَوفَّر أيضًا مُهيِئ (initializers) للمصفوفات ثنائية البعد. فمثلًا، تُنشِئ التَعْليمَة التالية مصفوفة 3x4: int[][] A = { { 1, 0, 12, -1 }, { 7, -3, 2, 5 }, { -5, -2, 2, -9 } }; يَتَكوَّن مُهيِئ المصفوفات ثنائية البعد من عدة صفوف مفصولة بفواصل (comma) ومُضمَّنة داخل أقواس. بالمثل، كل صف عبارة عن قائمة من القيم مفصولة بفواصل ومُضمَّنة داخل أقواس. إلى جانب ذلك، تَتَوفَّر قيم مُصنَّفة النوع (literals) لتَمثيِل المصفوفات ثنائية البعد لها نفس قواعد الصيغة (syntax)، ويُمكِن اِستخدَامها بأي مكان وليس فقط تَعْليمَات التّصرِيح (declarations). اُنظر المثال التالي: A = new int[][] { { 1, 0, 12, -1 }, { 7, -3, 2, 5 }, { -5, -2, 2, -9 } }; في الواقع، يُمكِنك أن تُطبِق نفس الأمر على المصفوفات ثلاثية البعد ورباعية البعد..إلخ، ولكنها غَيْر شائعة الاِستخدَام في العموم. حقيقة المصفوفات ثنائية البعد (two-dimensional arrays) قبل أن نتعمق أكثر من ذلك، هنالك مفاجأة صغيرة! لا تَملُك الجافا مصفوفات ثنائية البعد (two-dimensional arrays) بصورة فعلية. تَحتلّ العناصر بمصفوفة فعلية ثنائية البُعد مَواضِعًا مُتصِلة من الذاكرة، ولكن هذا ليس صحيحًا بالجافا. في الحقيقة، تَكشِف الصيغة (syntax) المُستخدَمة لتمثيل أنواع المصفوفة ذلك: بِفرض وجود النوع BaseType، ينبغي إذًا أن نَكُون قادرين على إنشاء النوع BaseType[]، وهو ما يَعنِي "مصفوفة من النوع BaseType". إذا اِستخدَمنا النوع int[] ذاته كنوع أساسي للمصفوفة، فإننا نَحصُل على النوع int[][] الذي يُمثِل "مصفوفة من النوع int[]" أو "مصفوفة من مصفوفة من النوع int ". بمصفوفة ثنائية البعد من النوع int[][]، تَكُون العناصر نفسها مُتْغيِّرات من النوع int[]. ولمّا كان أي مُتْغيِّر من النوع int[] بدوره قادرًا على حَمْل مؤشر (pointer) إلى مصفوفة من النوع int. يُمكِننا إذًا أن نقول أن أي مصفوفة ثنائية البعد هي في الواقع عبارة عن مصفوفة من المؤشرات (pointers) بحيث يُشيِر كل مؤشر منها بدوره إلى مصفوفة أحادية البعد (one-dimensional array). تُمثِل المصفوفات أحادية البعد إذًا صفوفًا (rows) ضِمْن المصفوفة ثنائية البعد (two-dimensional). تُبيِّن الصورة التالية مصفوفة مُكوَّنة من 3 صفوف و 4 أعمدة: غالبًا ما نتجاهل تلك الحقيقة، ونُفكِر بالمصفوفة ثنائية البعد كما لو كانت شبكة (grid). ولكننا سنحتاج أحيانًا لأن نتذكَّر أن كل صف ضِمْن تلك الشبكة هو مصفوفة بحد ذاته. يُمكننا في الواقع أن نُشيِر إلى تلك المصفوفات باِستخدَام A[0] و A[1] و A[2] أي أن كل صف هو قيمة من النوع int[] يُمكِننا أن نُمرِّرها حتى إلى برنامج فرعي (subroutine) يَستقبِل مُعاملًا (parameter) من النوع int[] على سبيل المثال. يترتَّب على التفكير بمصفوفة ثنائية البعد A على أنها مصفوفة من المصفوفات عدة نتائج: أولًا، يُساوِي A.length عدد الصفوف (rows) ضِمْن المصفوفة، وهو ما يَتضِح معناه عند التفكير بها بتلك الطريقة. ثانيًا، إذا كانت A تَملُك الشكل المعتاد للمصفوفات ثنائية البعد، فسيَكُون عدد الأعمدة بالمصفوفة A هو نفسه عدد العناصر بالصف الأول أي A[0].length. لاحِظ مع ذلك أنه لا توجد قاعدة تَنصّ على ضرورة أن تَكُون جميع الصفوف ضِمْن مصفوفة ثنائية البعد بنفس الطول حيث يُعدّ كل صف بمثابة مصفوفة مُنفصِلة أحادية البعد، ويُمكِن لها إذًا أن تَملٌك طولًا مختلفًا. في الواقع، قد يَكُون صف معين ضِمْن مصفوفة فارغًا (null). اُنظر التَعْليمَة التالية على سبيل المثال: A = new int[3][]; لا يَتَضمَّن التعريف السابق عددًا داخل الأقواس الثانية، ولهذا فإنه يُنشِئ مصفوفة مُكوَّنة من ثلاثة عناصر جميعها فارغة (null) أي أنه يُوفِّر مساحة لثلاثة صفوف (rows)، ولكنه لا يُنشِئ تلك الصفوف بشكل فعليّ. يُمكِنك أن تُنشِئ الصفوف A[0] و A[1] و A[2] بشكل منفصل. كمثال آخر، لنُفكِر بمصفوفة مُتماثِلة M -المصفوفة المتماثلة (symmetric) عبارة عن مصفوفة ثنائية البعد يَتساوَى عدد صفوفها مع عدد أعمدتها كما تَتَساوَى قيم M[j] و M[j] لجميع قيم i و j-، نحتاج لتَخْزِين قيم M[j] التي تُحقِّق الشَّرط i >= j أي يُمكِننا أن نُخزِّن البيانات بمصفوفة مُثلثة (triangular array) كالتالي: يُمكِننا أن نُنشِئ مصفوفة مثلثية (triangular array) إذا أنشأنا كل صف على حدى. تُنشِئ الشيفرة التالية مصفوفة مثلثية 7x7 من النوع double: double[][] matrix = new double[7][]; // لم ننشئ الصفوف بعد for (int i = 0; i < 7; i++) { // انشئ الصف i بحيث يحتوي على i+1 من العناصر matrix[i] = new double[i+1]; } إذا أردنا أن نَجلب قيمة matrix بالمَوضِع (i,j)، وكان i < j، فإنه ينبغي أن نَجلب قيمة matrix[j]، ويَنطبِق الأمر نفسه على ضَبْط قيم تلك المصفوفة. تُعرِّف الشيفرة التالية صَنْفًا لتمثيل المصفوفات المُتماثِلة (symmetric matrices): public class SymmetricMatrix { private double[][] matrix; // مصفوفة مثلثية لحمل البيانات public SymmetricMatrix(int n) { matrix = new double[n][]; for (int i = 0; i < n; i++) matrix[i] = new double[i+1]; } public double get( int row, int col ) { if (row >= col) return matrix[row][col]; else return matrix[col][row]; } public void set( int row, int col, double value ) { if (row >= col) matrix[row][col] = value; else matrix[col][row] = value; } public int size() { return matrix.length; // يمثل عدد الصفوف } } // end class SymmetricMatrix يُمكِنك أن تجد تعريف الصَنْف بالأعلى داخل الملف SymmetricMatrix.java كما يَحتوِي الملف TestSymmetricMatrix.java على برنامج صغير لاختبار الصَنْف. بالمناسبة، لا تستطيع الدالة (function) القياسية Arrays.copyOf() أن تُنشِئ نسخة كاملة من مصفوفة ثنائية البعد (2d array) ضِمْن خطوة واحدة، وإنما ينبغي أن تَفعَل ذلك لكل صف (row) ضِمْن المصفوفة على حدى. نستطيع إذًا كتابة الشيفرة التالية لنسخ مصفوفة ثنائية البعد من الأعداد الصحيحة: int[][] B = new int[A.length][]; // B و A لديهم نفس عدد الصفوف for (int i = 0; i < A.length; i++) { B[i] = Arrays.copyOf(A[i], A[i].length)); // انسخ الصف i } لعبة الحياة (Game of Life) كمثال آخر على معالجة المصفوفات ثنائية البعد، سنَفْحَص مثال مشهور آخر: لعبة الحياة (Game of Life) الذي اخترعها عالم الرياضيات جون هورتون كونواي (John Horton Conway) بعام 1970. لا تُعدّ لعبة الحياة (Game of Life) لعبة بحق، فهي تُمثِل آلة أوتوماتيكية ثنائية البعد. يَعنِي ذلك أنها مُكوَّنة من شبكة (grid) من الخلايا (cells) تَتَغيَّر محتوياتها بمرور الوقت وفقًا لقواعد محدَّدة. يُمكِن لأي خلية أن تَكُون بحالة من اثنين: "حية (alive)" أو "ميتة (dead)". سنَستخدِم مصفوفة ثنائية البعد لتَمثيِل تلك الشبكة بحيث يُمثِل كل عنصر بالمصفوفة حالة خلية واحدة ضِمْن الشبكة. ستُهيِئ اللعبة شبكة (grid) مبدئية بحيث تَكُون جميع خلاياها إما "حية" أو "ميتة". بَعْد ذلك، ستتطوَّر الشبكة وفقًا لسِلسِلة من الخطوات. ستُحدَّد حالة خلايا الشبكة بكل خطوة وفقًا لحالة خلاياها بالخطوة السابقة ووفقًا لعدد من القواعد البسيطة: ستَفحَص كل خلية بالشبكة الخلايا المجاورة لها (أفقيًا ورأسيًا وقطريًا)، وتَحسِب عدد الخلايا الحية ثم تُحدَّد حالة الخلية بالخطوة التالية وفقًا للقواعد التالية: في حالة كانت الخلية "حية" بالخطوة الحالية، وكانت أيضًا مُحاطَة بعدد 2 أو 3 خلايا حية، فإنها ستَبقَى حية بالخطوة التالية أما إذا لم تَكُن مُحاطَة بذلك العدد، فإنها ستموت. (أي تَموُت الخلية الحية نتيجة للوحدة إذا كانت مُحاطة بعدد أقل من خليتين حيتين أو نتيجة للازدحام إذا كانت مُحاطَة بأكثر من 3 خلايا حية). في حالة كانت الخلية "ميتة" بالخطوة الحالية، وكانت أيضًا مُحاطَة بثلاث خلايا حية، فإنها تتحوَّل لتُصبِح حية بالخطوة التالية أما إذا لم تَكُن مُحاطَة بذلك العدد، فإنها ستَبقَى ميتة. (أي يَنتُج عن وجود ثلاثة خلايا حية خلية حية جديدة). تُظهِر الصورة التالية شكل شبكة الخلايا قَبْل تطبيق قواعد اللعبة وبَعْدها. تُطبَق القواعد على كل خلية بالشبكة، وتُبيِّن الصورة كيفية تطبيقها على أربعة خلايا: تُعدّ لعبة الحياة (Game of Life) شيقة حيث تَعرِض الكثير من الأنماط المفاجئة (اُنظر بصفحة ويكيبيديا الخاصة باللعبة). نحن هنا مُهتمين فقط بكتابة برنامج لمحاكاة تلك اللعبة. يُمكِنك أن تَطلِع على شيفرة البرنامج بالكامل بالملف Life.java. تَظهَر رقعة الحياة كشبكة من المربعات بحيث تَكون المربعات الميتة سوداء اللون أما المربعات الحية فتَكُون بيضاء اللون. (سيَستخدِم البرنامج الصنف MosaicCanvas.java من القسم 4.7 لتَمثيِل تلك الشبكة، لذلك ستحتاج إلى إضافة ذلك الملف لتَصرِيف [compile] البرنامج وتَشْغِيله). يُمكِننا أن نملئ رقعة الحياة بخلايا حية وميتة عشوائيًا أو قد نَستخدِم الفأرة لضَبطها. يُوفِّر البرنامج الزر "Step" المسئول عن تحديد حالة الشبكة بَعْد خطوة واحدة فقط" بالإضافة إلى الزر "Start" المسئول عن تَشْغِيل تلك الخطوات بهيئة تحريكة (animation). سنَفْحَص العمليات المُتعلِقة بمعالجة المصفوفات (array processing) اللازمة لتّنفِيذ برنامج لعبة الحياة (Game of Life). أولًا، يُمكِن لأي خلية أن تَكُون حية أو ميتة، لذلك سنَستخدِم بطبيعة الحال مصفوفة ثنائية البعد من النوع boolean[][] لتمثيل حالة جميع الخلايا، وليَكُن اسم تلك المصفوفة هو alive. ستُساوِي قيمة أي عنصر alive[r][c] القيمة true إذا كانت الخلية برقم الصف r ورقم العمود c حية. سنَستخدِم أيضًا نفس قيمة الثابت GRID_SIZE لتمثيل عدد الصفوف والأعمدة بتلك المصفوفة. يُمكِننا إذًا أن نَستخدِم حَلْقة التَكْرار for المُتداخلة (nested) التالية لمَلْئ قيم تلك المصفوفة المُمثِلة لشبكة الحياة بقيم عشوائية: for (int r = 0; r < GRID_SIZE; r++) { for (int c = 0; c < GRID_SIZE; c++) { // اضبط الخلية لتكون حية بنسبة احتمال تساوي 25% alive[r][c] = (Math.random() < 0.25); } } يُعيد التعبير Math.random() < 0.25 القيمة true أو false، والتي يُمكِننا أن نُسنِدها (assign) إلى عنصر مصفوفة من النوع boolean. سنَستخدِم تلك المصفوفة لضَبْط لون كل خلية بالشاشة. نظرًا لأننا سنَرسِم شبكة الخلايا على شاشة من الصَنْف MosaicCanvas، فإننا سنَستخدِم واجهة برمجة التطبيقات (ِAPI) الخاصة بذلك الصنف لضَبْط ألوانها. ونظرًا لأن الصَنْف MosaicCanvas هو المسئول عن الرسم الفعليّ، سيَكُون برنامج لعبة الحياة مسئولًا فقط عن ضَبْط الألوان باِستخدَام واجهة برمجة التطبيقات الخاصة بالصَنْف. سنُعرِّف ذلك بتابع اسمه showBoard()، والذي ينبغي أن يُستدعَى بكل مرة تَتَغيَّر فيها رقعة الحياة. سنَستخدِم مجددًا حَلْقة تَكْرار for مُتداخِلة لضَبْط لون كل مربع بالشبكة كالتالي: for (int r = 0; r < GRID_SIZE; r++) { for (int c = 0; c < GRID_SIZE; c++) { if (alive[r][c]) display.setColor(r,c,Color.WHITE); else display.setColor(r,c,null); // اعرض لون الخلفية بالأسود } } بالطبع، يُعدّ حساب الحالة الجديدة لشبكة الخلايا بَعْد تطبيق قواعد اللعبة على حالتها الحالية هو الجزء الأكثر تشويقًا من البرنامج. لمّا كانت القواعد ستُطبَق على كل خلية على حدى، فإننا سنحتاج إلى اِستخدَام حَلْقة تَكْرار for مُتداخِلة للمرور عبر جميع خلايا الشبكة، ولكن ستَكُون المعالجة أكثر تعقيدًا هذه المرة. لاحظ أنه لا يُمكِننا أن نُجرِي أي تغييرات على قيم المصفوفة أثناء معالجتها؛ لأننا سنحتاج إلى معرفة الحالة القديمة لخلية معينة أثناء معالجة خلاياها المجاورة. سيَستخدِم البرنامج مصفوفة آخرى للاحتفاظ بالحالة الجديدة أثناء المعالجة، وعندما ننتهي من معالجة جميع الخلايا ضِمْن الشبكة، يُمكننا أن نَستخدِم المصفوفة الجديدة بدلًا من القديمة. يُمكِننا كتابة الخوارزمية (algorithm) بالشيفرة الوهمية (pseudocode) كالتالي: let newboard be a new boolean[][] array for each row r: for each column c: Let N be the number of neighbors of cell (r,c) in the alive array if ((N is 3) or (N is 2 and alive[r][c])) newboard[r][c] = true; else newboard[r][c] = false; alive = newboard سيُشيِر alive عند انتهاء المعالجة إلى مصفوفة جديدة، وهو أمر لا ينبغي أن نقلق بشأنه طالما كانت محتويات المصفوفة الجديدة مُمثِلة للحالة الجديدة لشبكة الخلايا أما المصفوفة القديمة فسيُحرِّرها كانس المُهملات (garabage collector). قد لا يَكون اختبار ما إذا كان newboard[r][c] مُتحقِقًا أم لا واضحًا بما فيه الكفاية، ولكنه يُنفِّذ القواعد بشكل سليم. سنحتاج أيضًا لحِسَاب عدد الخلايا المجاورة. إذا كان لدينا خلية بالصف r والعمود c، ولم تَكُن تلك الخلية واقعة على حافة الرقعة، فإن الخلايا المجاورة لتلك الخلية هي كالتالي: لاحِظ أن رقم الصف فوق الصف r يُساوِي r-1 أما الصف أسفله فهو r+1. يَنطبِق نفس الأمر على الأعمدة. ينبغي إذًا أن نَفْحَص القيم alive[r-1][c-1] و alive[r-1][c] و alive[r-1][c+1] و alive[r][c-1] و alive[r][c+1] و alive[r+1][c-1] و alive[r+1][c] و alive[r+1][c+1]، ونَعِد منها تلكم المُحتوية على القيمة true. اِحرِص على فهم كيفية عمل فهارس المصفوفة. في المقابل، ستواجهنا مشكلة إذا كانت الخلية وَاقِعة على إحدى حواف الشبكة. في تلك الحالة، لا تَكُون بعض العناصر ضِمْن القائمة السابقة موجودة، وستَتَسبَّب محاولة الاشارة إليها إلى حدوث اِعترَاض (exception). لكي نَتَجنَّب حُدوث ذلك، ينبغي أن نُعامِل الخلايا الواقعة على الحواف بطريقة مختلفة. يُمكِننا مثلًا أن نَفْحَص ما إذا كان عنصر المصفوفة موجودًا قبل محاولة الإشارة إليه. في تلك الحالة، يُمكِننا مثلًا أن نَكْتُب شيفرة حساب عدد الخلايا المجاورة الحية كالتالي: if (r-1 >= 0 && c-1 >= 0 && alive[r-1][c-1]) N++; // خلية بالموضع (r-1,c-1) موجودة وحية if (r-1 >= 0 && alive[r-1][c]) N++; // خلية بالموضع (r-1,c) موجودة وحية if (r-1 >= 0 && c+1 <= GRID_SIZE && alive[r-1][c+1]) N++; // خلية بالموضع (r-1,c+1) موجودة وحية // إلخ سنتجنَّب بذلك كل الاعتراضات (exceptions). اِستخدَمنا في الواقع حلًا آخر شائع الاِستخدَام بألعاب الحاسوب ثنائية البعد. سنَتَظاهَر كما لو أن الحافة اليسرى للرقعة مُرتبطِة بالحافة اليمنى وكما لو أن الحافة العلوية مُرتبطِة بالحافة السفلية. بالنسبة لخلية برقم الصف 0 على سبيل المثال، فإن الصف أعلاها هو الصف الأخير أي بالرقم GRID_SIZE-1. سنَستخدِم أيضًا مُتْغيِّرات لتمثيل المَواضِع above و below و left و right الخاصة بخلية معينة. اُنظر شيفرة التابع المسئولة عن حساب الحالة الجديدة للرقعة، والتي ستَجِد أنها أبسط كثيرًا: private void doFrame() { // احسب الحالة الجديدة لرقعة لعبة الحياة boolean[][] newboard = new boolean[GRID_SIZE][GRID_SIZE]; for ( int r = 0; r < GRID_SIZE; r++ ) { // تعد الصفوف أعلى وأسفل الصف r int above, below; // تعد الأعمدة على يمين ويسار العمود c int left, right; above = r > 0 ? r-1 : GRID_SIZE-1; // (for "?:" see Subsection 2.5.5) below = r < GRID_SIZE-1 ? r+1 : 0; for ( int c = 0; c < GRID_SIZE; c++ ) { left = c > 0 ? c-1 : GRID_SIZE-1; right = c < GRID_SIZE-1 ? c+1 : 0; int n = 0; // عدد الخلايا الحية المجاورة if (alive[above][left]) n++; if (alive[above][c]) n++; if (alive[above][right]) n++; if (alive[r][left]) n++; if (alive[r][right]) n++; if (alive[below][left]) n++; if (alive[below][c]) n++; if (alive[below][right]) n++; if (n == 3 || (alive[r][c] && n == 2)) newboard[r][c] = true; else newboard[r][c] = false; } } alive = newboard; } يُمكِنك الإطلاع على شيفرة البرنامج بالملف Life.java، وأن تُجرِبه. لا تنسى أنك ستحتاج إلى اِستخدَام الملف MosaicCanvas.java أيضًا. لعبة داما (Checkers) سنَفْحَص الآن مثالًا أكثر واقعية لاستخدام المصفوفات ثنائية البعد. يُعدّ هذا البرنامج هو الأطول حتى الآن حيث يَتَكوَّن من 745 سطر. يَسمَح ذلك البرنامج للمُستخدمين بلعب مباراة داما (checkers). تتكوَّن لعبة الداما من رقعة مُكوَّنة من 8 صفوف و8 أعمدة. سنعتمد على المثال الذي كتبناه بالقسم الفرعي 6.5.1. سنُسمِي اللاعبين بأسماء "أحمر" و "أسود" وفقًا للون قطع الداما الخاصة بهما. لن نشرح قواعد لعبة الداما هنا، ولربما تستطيع أن تتعلَّمها بينما تُجرِّب البرنامج. يستطيع اللاعب أن يَتحرَك بالنَقْر على القطعة التي يريد أن يُحرِكها ثُمَّ بالنَقْر على المربع الفارغ الذي يريد أن يَنقِل إليه تلك القطعة. سيَرسِم البرنامج بأي لحظة إطارًا حول المربعات التي يستطيع اللاعب أن يَنقُر عليها بلون أفتح قليلًا كنوع من المساعدة. على سبيل المثال، سيُحَاط المربع المُتضمِّن للقطعة التي اختارها المُستخدِم للحركة بإطار أصفر اللون بينما ستُحَاط القطع الآخرى التي بإمكانه تَحرِيكها بإطار أزرق سماوي. إذا كان المُستخدِم قد اختار قطعة بالفعل لتَحرِيكها، فستُحَاط جميع المربعات الفارغة التي يستطيع أن يُحرِك إليها تلك القطعة بإطار أخضر اللون. ستَتبِع اللعبة قاعدة تَنُص على أنه إذا كان اللاعب الحالي قادرًا على القفز (jump) على إحدى قطع الخصم، فإنه لابُدّ أن يَقفِز. عندما تُصبِح إحدى القطع "قطعة ملك" أي بَعْد وصولها إلى الحافة الأخرى من الرقعة، سيَرسِم البرنامج حرف "K" أبيض كبير على تلك القطعة. تَعرِض الصورة التالية لقطة شاشة مبكرة من اللعبة، والتي تُظهِر اللاعب الأسود وقد اختار القطعة الموجودة بالمربع ذو الإطار الأصفر للحركة. يستطيع اللاعب الآن أن يَنقُر على إحدى المربعات المُحاطَة بإطار أخضر لكي يُكمِل تلك الحركة أو قد يَنقُر على إحدى المربعات المُحاطَة بإطار أزرق سماوي ليختار قطعة آخرى لتَحرِيكها. سنَمُر عبر جزءًا من شيفرة ذلك المثال، ولكن يُمكِنك الاطلاع على الشيفرة بالكامل بالملف Checkers.java. قد يَكُون البرنامج طويلا ومعقدًا، ولكنه بقليل من الدراسة، ستَتَمكَّن من فهم جميع التقنيات المُستخدَمة به. يُعدّ البرنامج مثالًا جيدًا على البرمجة كائنية التوجه (object-oriented) المُعتمدِة على كُلًا من الأحداث (event-driven) والحالات (state-based). سنُخزِّن بيانات قطع الرقعة بمصفوفة ثنائية البعد (two-dimensional array). نظرًا لأن البرنامج مُعقَد نوعًا ما، سنُقسِّمه إلى عدة أصناف. إلى جانب الصَنْف الأساسي، سنُعرِّف بعض الأصناف المُتداخِلة (nested) الآخرى منها الصَنْف CheckersData المُستخدَم لمعالجة بيانات الرقعة. لا يُعدّ ذلك الصَنْف مسئولًا مباشرًا عن أي جزء من الرسوم (graphics) أو مُعالجة الأحداث (event-handling)، ولكنه يُوفِّر التوابع (methods) التي يُمكِننا أن نستدعيها بأصناف آخرى لأغراض معالجة الرسوم والأحداث (events). سنناقش ذلك الصَنْف قليلًا. يَحتوِي الصَنْف CheckersData على مُتْغيِّر نُسخة (instance variables) اسمه هو board من النوع int[][]. تُضبَط قيمة ذلك المُتْغيِّر إلى new int[8][8] لتُمثِل شبكة 8x8 من الأعداد الصحيحة (integers). تُستخدَم ثوابت (constants) لتعريف القيم المُحتمَل تَخْزِينها بمربع برقعة شطرنج (checkboard): static final int EMPTY = 0, // يمثل مربعًا فارغًا RED = 1, // قطعة حمراء عادية RED_KING = 2, // قطعة ملك حمراء BLACK = 3, // قطعة سوداء عادية BLACK_KING = 4; // قطعة ملك سوداء سنَستخدِم أيضًا الثابتين RED و BLACK بالبرنامج لتَمثيِل لاعبي المباراة. عندما تبدأ المباراة، ستُضبَط قيم المصفوفة لتمثيل الحالة المبدئية للرقعة، وستبدو كالتالي: يُمكِن لأي قطعة سوداء عادية أن تتحرك لأسفل الشبكة فقط أي لابُدّ أن يكون رقم الصف للمربع الذي ستنتقل إليه أكبر من رقم الصف للمربع القادمة منه. بالمقابل، تستطيع أي قطعة حمراء عادية أن تَتَحرك لأعلى الشبكة فقط. يُمكِن للملوك بأي من اللونين الحركة بكلا الاتجاهين. لابُدّ أن ينتبه الصَنْف CheckersData لأي تَغييرات ينبغي إجراؤها على هياكل البيانات (data structures) عندما يُحرِك أحد اللاعبين قطعة معينة بالرقعة. في الواقع، يُعرِّف الصَنْف تابع النسخة makeMove() المُخصَّص لذلك الغرض. عندما يُحرِك لاعب قطعة من مربع إلى آخر، سيُعدِّل التابع قيمة عنصرين ضِمْن المصفوفة. بالإضافة إلى ذلك، إذا كانت الحركة عبارة عن قفزة (jump)، ستُحذَف القطعة التي كانت موجودة بالمربع المقفوز إليه من الرقعة. يستطيع التابع أن يُحدِّد ما إذا كانت حركة معينة عبارة عن قفزة أم لا بِفْحَص ما إذا كان المربع الذي تَحرَكت إليه القطعة يَبعُد بمقدار صفين عن المربع الذي بدأت منه. إلى جانب ما سبق، تُصبِح القطعة الحمراء RED التي تتحرك إلى الصف 0 أو القطعة السوداء Black التي تتحرك إلى الصف 7 بمثابة قطعة ملك (king). سنَضَع جميع ما سبق ببرنامج فرعي (subroutine)، وبالتالي، لن يضطّر باقي البرنامج للقلق بشأن أي من تلك التفاصيل السابقة. يُمكِنه فقط أن يَستدعِي التابع makeMove(): void makeMove(int fromRow, int fromCol, int toRow, int toCol) { board[toRow][toCol] = board[fromRow][fromCol]; // حرك القطعة board[fromRow][fromCol] = EMPTY; // المربع الذي تحركت منه أصبح فارغًا if (fromRow - toRow == 2 || fromRow - toRow == -2) { // الحركة كانت قفزة لذلك احذف القطعة المقفوز إليها من الرقعة int jumpRow = (fromRow + toRow) / 2; // صف القطعة المقفوز إليها int jumpCol = (fromCol + toCol) / 2; // عمود القطعة المقفوز إليها board[jumpRow][jumpCol] = EMPTY; } if (toRow == 0 && board[toRow][toCol] == RED) board[toRow][toCol] = RED_KING; // تصبح القطعة الحمراء ملك if (toRow == 7 && board[toRow][toCol] == BLACK) board[toRow][toCol] = BLACK_KING; // تصبح القطعة السوداء ملك } // end makeMove() ينبغي أن يَسمَح الصَنْف CheckersData بالعثور على جميع الحركات الصالحة بالرقعة. سنستخدم كائنًا ينتمي للصَنْف التالي لتمثيل الحركة داخل لعبة داما: private static class CheckersMove { int fromRow, fromCol; // موضع القطعة المطلوب تحريكها int toRow, toCol; // المربع الذي انتقلت إليه القطعة CheckersMove(int r1, int c1, int r2, int c2) { // اضبط قيم متغيرات النسخة fromRow = r1; fromCol = c1; toRow = r2; toCol = c2; } boolean isJump() { // اختبر ما إذا كانت الحركة عبارة عن قفزة // بالقفزة، تتحرك القطعة مسافة صفين return (fromRow - toRow == 2 || fromRow - toRow == -2); } } // end class CheckersMove. يُعرِّف الصَنْف CheckersData تابع نسخة (instance method) للعثور على جميع الحركات المتاحة حاليًا لكل لاعب. يُعدّ ذلك التابع بمثابة دالة (function) تُعيِد مصفوفة من النوع CheckersMove[] تحتوي على جميع الحركات المتاحة مُمثَلة بهيئة كائنات من النوع CheckersMove. يُمكِننا إذًا كتابة توصيف التابع كالتالي: CheckersMove[] getLegalMoves(int player) يُمكِننا كتابة خوارزمية التابع بالشيفرة الوهمية (pseudocode) كالتالي: // ابدأ بقائمة فارغة من الحركات Start with an empty list of moves // ابحث عن أي قفزات صالحة و أضفها إلى القائمة Find any legal jumps and add them to the list // إذا لم يكن هناك أي قفزات صالحة if there are no jumps: // ابحث عن الحركات العادية الأخرى و أضفها إلى القائمة Find any other legal moves and add them to the list // إذا كانت القائمة فارغة if the list is empty: return null else: return the list الآن، ما هو المقصود بالقائمة (list)؟ في الواقع، ينبغي أن نُعيد الحركات المسموح بها ضِمْن مصفوفة، ولكن لأن المصفوفات ثابتة الحجم، لا يُمكِننا أن نُنشِئها حتى نَعرِف عدد الحركات، وهو في الواقع ما لا يُمكِننا معرفته حتى نَصِل إلى نهاية التابع أي بَعْد أن نَكُون قد أنشأنا القائمة بالفعل! أحد الحلول إذًا هو اِستخدَام مصفوفة ديناميكية من النوع ArrayList بدلًا من مصفوفة عادية لكي تَحمِل الحركات بينما نَجِدها. يَستخدِم البرنامج بالأسفل كائنًا من النوع ArrayList<CheckersMove> لكي يُمكِّن القائمة من حَمْل كائنات من الصَنْف CheckersMove فقط. بينما نُضيِف الحركات إلى تلك القائمة، سيزداد حجمها بما يتناسب مع عدد الحركات. يُمكِننا أن نُنشِئ المصفوفة المطلوبة ونَنسَخ البيانات إليها بنهاية التابع. اُنظر الشيفرة الوهمية التالية: // أنشئ قائمة moves فارغة للحركات Let "moves" be an empty ArrayList<CheckersMove> // ابحث عن القفزات المسموح بها و أضفها إلى القائمة Find any legal jumps and add them to moves // إذا كان عدد الحركات ما يزال يساوي 0 if moves.size() is 0: // There are no legal jumps! // ابحث عن الحركات العادية الصالحة و أضفها إلى القائمة Find any other legal moves and add them to moves // إذا لم يكن عدد الحركات يساوي 0 if moves.size() is 0: // There are no legal moves at all! return null else: // عرف مصفوفة moveArray من النوع CheckersMoves // طولها يساوي moves.size() Let moveArray be an array of CheckersMoves of length moves.size() // انسخ محتويات المصفوفة moves إلى moveArray Copy the contents of moves into moveArray return moveArray علينا الآن أن نُحدِّد الحركات والقفزات المسموح بها. تَتَوفَّر المعلومات التي نحتاج إليها بالمصفوفة board، ولكن يَلزَمنا بعض العمل لاِستخراجها. لابُدّ أن نَفْحَص جميع المواضع بالمصفوفة ونَعثُر على القطع التي تنتمي للاعب الحالي. بَعْد ذلك، ينبغي أن نَفْحَص المربعات التي يُمكِن لكل قطعة أن تنتقل إليها، ونُحدِّد ما إذا كانت تلك الحركة صالحة أم لا. إذا كنا نبحث عن القفزات المسموح بها، فينبغي أن نَفْحَص المربعات التي تَبعُد بمسافة صفين وعمودين من كل قطعة. يُمكِننا إذًا أن نُوسِّع سطر الخوارزمية "اِبحَث عن القفزات المسموح بها ثم أَضِفها إلى الحركات" كالتالي: // لكل صف بالرقعة For each row of the board: // لكل عمود بالرقعة For each column of the board: // إذا كانت إحدى قطع اللاعبين بذلك المكان if one of the player's pieces is at this location: // إذا كان من الممكن القفز إلى الموضع row+2,column+2 if it is legal to jump to row + 2, column + 2 // أضف تلك الحركة إلى moves add this move to moves // إذا كان من الممكن القفز إلى الموضع row-2,column+2 if it is legal to jump to row - 2, column + 2 add this move to moves // إذا كان من الممكن القفز إلى الموضع row+2,column-2 if it is legal to jump to row + 2, column - 2 add this move to moves // إذا كان من الممكن القفز إلى الموضع row-2,column-2 if it is legal to jump to row - 2, column - 2 add this move to moves ينبغي أن نُوسِّع السطر "ابحث عن الحركات الصالحة الآخرى وأضفها إلى قائمة الحركات" بنفس الطريقة باستثناء أن علينا الآن فَحْص المربعات الأربعة التي تَبعُد مسافة صف واحد وعمود واحد من كل قطعة. لاحِظ أن اختبار ما إذا كان لاعب معين يستطيع أن يُحرِك قطعة من مربع معين إلى مربع آخر ليس بالأمر السهل. لابُدّ أن يَكُون المربع الذي ينتقل إليه اللاعب موجودًا بالرقعة، كما ينبغي أن يَكُون فارغًا. علاوة على ذلك، تستطيع القطع الحمراء والسوداء العادية أن تَتَحرَك باتجاه واحد. سنَستخدِم التابع (method) التالي لفْحَص ما إذا كان لاعب معين قادر على القيام بتحريك مربع: private boolean canMove(int player, int r1, int c1, int r2, int c2) { if (r2 < 0 || r2 >= 8 || c2 < 0 || c2 >= 8) return false; // (r2,c2) خارج الرقعة if (board[r2][c2] != EMPTY) return false; // (r2,c2) يحتوي بالفعل على قطعة if (player == RED) { if (board[r1][c1] == RED && r2 > r1) return false; // القطع الحمراء العادية يمكنها أن تتحرك للأسفل فقط return true; // الحركة صالحة } else { if (board[r1][c1] == BLACK && r2 < r1) return false; // القطع السوداء العادية يمكنها أن تتحرك للأعلى فقط return true; // الحركة صالحة } } // end canMove() يَستدعِى التابع getLegalMoves() التابع canMove() المُعرَّف بالأعلى ليَفْحَص ما إذا كانت حركة مُحتمَلة لقطعة معينة صالحة فعليًا أم لا. سنُعرِّف تابعًا مشابهًا لفَحْص ما إذا كانت قفزة (jump) معينة صالحة أم لا. في تلك الحالة، سنُمرِّر للتابع كُلًا من مربع القطعة والمربع الذي يُحتمَل أن تَنتقِل إليه وكذلك المربع الواقع بين هذين المربعين أي ذلك الذي سيَقفِز اللاعب فوقه. لابُدّ أن يَحتوِي المربع المقفوز إليه على إحدى قطع الخصم. يُمكننا تَوصِيف ذلك التابع كالتالي: private boolean canJump(int player, int r1, int c1, int r2, int c2, int r3, int c3) { . . . ينبغي الآن أن تَكُون قادرًا على فِهم شيفرة التابع getLegalMoves() بالكامل. يَدمِج ذلك التابع عدة مواضيع معًا: المصفوفات أحادية البعد (one-dimensional) والمصفوفات الديناميكية ArrayLists والمصفوفات ثنائية البعد (two-dimensional): CheckersMove[] getLegalMoves(int player) { if (player != RED && player != BLACK) return null; // لن يحدث هذا ببرنامج سليم int playerKing; // The constant for a King belonging to the player. if (player == RED) playerKing = RED_KING; else playerKing = BLACK_KING; ArrayList<CheckersMove> moves = new ArrayList<CheckersMove>(); // ستخزن الحركة ضمن القائمة // تحقق من جميع القفزات الممكنة for (int row = 0; row < 8; row++) { for (int col = 0; col < 8; col++) { if (board[row][col] == player || board[row][col] == playerKing) { if (canJump(player, row, col, row+1, col+1, row+2, col+2)) moves.add(new CheckersMove(row, col, row+2, col+2)); if (canJump(player, row, col, row-1, col+1, row-2, col+2)) moves.add(new CheckersMove(row, col, row-2, col+2)); if (canJump(player, row, col, row+1, col-1, row+2, col-2)) moves.add(new CheckersMove(row, col, row+2, col-2)); if (canJump(player, row, col, row-1, col-1, row-2, col-2)) moves.add(new CheckersMove(row, col, row-2, col-2)); } } } // إذا وجدت أي قفزات ممكنة، فلابد أن يقفز اللاعب. لذلك لن نضيف أي // حركات عادية. أما إذا لم تجد أي قفزات، ابحث عن أي حركات عادية if (moves.size() == 0) { for (int row = 0; row < 8; row++) { for (int col = 0; col < 8; col++) { if (board[row][col] == player || board[row][col] == playerKing) { if (canMove(player,row,col,row+1,col+1)) moves.add(new CheckersMove(row,col,row+1,col+1)); if (canMove(player,row,col,row-1,col+1)) moves.add(new CheckersMove(row,col,row-1,col+1)); if (canMove(player,row,col,row+1,col-1)) moves.add(new CheckersMove(row,col,row+1,col-1)); if (canMove(player,row,col,row-1,col-1)) moves.add(new CheckersMove(row,col,row-1,col-1)); } } } } // إذا لم تجد أي حركات صالحة، أعد فراغا if (moves.size() == 0) return null; else { // انشئ مصفوفة وأضف إليها الحركات المسموح بها CheckersMove[] moveArray = new CheckersMove[moves.size()]; for (int i = 0; i < moves.size(); i++) moveArray[i] = moves.get(i); return moveArray; } } // end getLegalMoves يُعدّ برنامج الداما مُعقدًا نوعًا ما، ويحتاج إلى الكثير من التصميم الجيد لتَقْرِير الأصناف (classes) والكائنات (classes) المُستخدمَة، وكذلك لتَقرِير التوابع (methods) التي ينبغي كتابتها، وكذلك لتقرير الخوارزميات (algorithms) التي ينبغي لتلك التوابع اِستخدَامها. يُمكِنك الاطلاع على شيفرة البرنامج بالكامل بالملف Checkers.java. ترجمة -بتصرّف- للقسم Section 5: Two-dimensional Arrays من فصل Chapter 7: Arrays and ArrayLists من كتاب Introduction to Programming Using Java.1 نقطة
-
الإصدار 1.0.0
18375 تنزيل
لا يخفى على أحد شهرة لغة SQL أو لغة الاستعلامات البنيوية (Structured Query Language) سواءً للمبرمجين أو الداخلين الجدد إلى عالم البرمجة وعلوم الحاسوب، فهي لغة برمجة متُخصِّصة في مجال واحد وهو معالجة وإدارة قواعد البيانات، وتعد اللغة القياسية لأنظمة إدارة قواعد البيانات (RDBMS)؛ وتُستخدم تعليمات وأوامر SQL -لمن لا يعرفها- لإجراء عمليات مباشرة على البيانات، مثل تخزينها في قاعدة بيانات، وجلبها منها والتعديل عليها بالإضافة إلى إنجاز مهام إدارية على قواعد البيانات من تأمين ونسخ احتياطي وإدارة للمستخدمين. ونظرًا لأهمية SQL سواءً للمبرمجين، وحتى لغير المبرمجين من العاملين في القطاعات التقنية أو للمهتمين بقواعد البيانات عمومًا، نضع بين يديك هذا الكتاب المبني على أحد أفضل الكتب الإنجليزية المتقدمة عن SQL، وهو كتاب "SQL Notes For Professionals" من موقع GoalKicker المبني بدوره على توثيق موقع StackOverflow وقد ساهم في إعداده عدد كبير من المساهمين على شبكة StackOverflow الشهيرة (إن أردت الاطلاع على قائمة المساهمين الكاملة، ارجع إلى قسم "Credits" في نهاية الكتاب الأصلي، SQL Notes For Professionals). يغطِّي هذا الكتاب المفاهيم الأساسية للغة SQL، مثل العمليات الأولية، وإدراج البيانات وحذفها واستخلاصها وتحديثها، وأنواع البيانات، وتصميم الجداول وتنفيذ الاستعلامات، إضافة إلى مفاهيم متقدمة، مثل المعارض views والدوال، وإدارة المستخدمين، وكيفية تأمين الشيفرة وغيرها من المواضيع. كما أنّ الكتاب غني بالأمثلة التطبيقية التي تشرح كل هذه المواضيع لترسيخ فهمها. هذا الكتاب ليس مثل غيره من الكتب والشروحات التي تشرح لغة SQL من البداية شرحًا مُبسَّطًا ومتسلسلًا وإنَّما يعتمد على مبدأ خير الكلام ما قل ودل في الشرح وترك الشيفرة تشرح نفسها بنفسها، فيحوي على كم كبير من الشيفرات والأمثلة العملية بالموازنة مع الشرح النظري ووُجِّه لمن يريد اتقان لغة SQL وصقل مهاراته فيها إذ سيساهم هذا الكتاب في رفع مستواك في لغة SQL وسيُملِّكك مهارات متقدمة في استعمال لغة SQL بالإضافة إلى بعض الخدع والالتفافات المتقدمة أيضًا. هذا الكتاب مرخص بموجب رخصة المشاع الإبداعي Creative Commons «نسب المُصنَّف - غير تجاري - الترخيص بالمثل 4.0». أنشئ العمل الأصلي من هذا الكتاب لأغراض تعليمية ولا يتبع إلى أي شركة أو مجموعة رسمية متعلقة بلغة SQL ولا حتى شبكة Stack Overflow، كما أن جميع العلامات التجارية المذكورة في هذا الكتاب تتبع إلى الشركات المالكة لها. يمكنك قراءة فصول الكتاب على شكل مقالات من هذه الصفحة، «المرجع المتقدم إلى لغة SQL»، أو مباشرة مما يلي: المقال الأول: مدخل إلى SQL المقال الثاني: جلب الاستعلامات عبر SELECT في SQL المقال الثالث: التجميع والترتيب في SQL المقال الرابع: تنفيذ تعليمات شرطية عبر CASE في SQL المقال الخامس: البحث والتنقيب والترشيح في SQL المقال السادس: الدمج بين الجداول في SQL المقال السابع: تحديث الجداول في SQL المقال الثامن: معالجة الأخطاء والتعديل على قواعد البيانات في SQL المقال التاسع: حذف الجداول وقواعد البيانات في SQL المقال العاشر: مواضيع متقدمة في SQL المقال الحادي عشر: دوال التعامل مع البيانات في SQL المقال الثاني عشر: دوال التعامل مع النصوص في SQL المقال الثالث عشر: التعبيرات الجدولية الشائعة Common Table Expressions المقال الرابع عشر: مواضيع متفرقة في SQL المقال الخامس عشر: الاستعلامات الفرعية والإجراءات في SQL المقال السادس عشر: تصميم الجداول وترتيب تنفيذ الاستعلامات ومعلومات المخطط في SQL المقال السابع عشر: تنظيم وتأمين شيفرات SQL1 نقطة