لعبة XO

ببساطة شديدة , في هذا المقال نشرعُ في بناء لعبة XO باستخدام فلَتر مع تطبيق بعضِ مفاهيم ادارة الحالات والريفربود سويةً جنباً إلى جنب.


ونبدأ أولاً بتثبيت الريفربود وذلك من خلال استدعاء أمر موجّه الأوامر:


$ flutter pub add flutter_riverpod

ثم بعد ذلك نقوم باضافة ProviderScope إلى دالة main في الصفحة الأولى: 

void main() { runApp(ProviderScope(child: const MyApp())); } class MyApp extends StatelessWidget { const MyApp({super.key}); // This widget is the root of your application. @override Widget build(BuildContext context) { return const MaterialApp( home: Home(), ); } }


بدايةً حتّى نبدأ في بناء الواجهة اخترنا لكم واجهةً بسيطةً خاليةً من التعقيد , فيها صفحة تحوي عاموداً من القطع , وفيه قطعة نصية وتحتها قطعة من نوع القطعة المجدولة GridView , ثم في داخلها حاوية والحاوية بداخل حساس للضغط حتى اذا ما ضغط المستخدم استدعى دالة الضغط.


الصفحة هي من نوع الصفحة المستهلكة المتفاعلة ConsumerStatefulWidget.


وقبل أن نعرض الكود البرمجي علينا أن نعرج قليلاً على المنطق البرمجي للعبة , هناك قاعدة واحدة , اذا تتالت ثلاث اختيارات وتشابهت فهي الفائزة سواءً كانت X أو O.


لنطبق هذه القاعدة علينا حصر الاحتمالات الفائزة وهي كالتالي:

اذاً نحتاجُ دالةً تتحقق من تحقق هذا الشرط , وهو تتالي المتغيرات داخل هذه المصفوفة الثنائية أو القائمة , ولنوضح الأمر أكثر قمنا بتعريف لوحة اللعبة على هذا الشكل :

List<int> board = [0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0];

ورمزنا هنا بصفر للخيار الفارغ , فواحد هو X واثنين هو O.


ثم حددنا اللاعب الأصل عن طريق متغيرٍ منطقي , وحددنا اللاعب الفائز عن طريق متغيرٍ نصي , ثم قمنا ببناء الحالة عن طريق هذه الأسس فصار شكلها كالتالي:

enum Status { init, finish, draw } class XOState { List<int> board; bool isComputePlayer; String winner; Status status; XOState({ required this.board, required this.status, required this.winner, required this.isComputePlayer, }); factory XOState.Initial() { return XOState( board: [0, 0, 0, 0, 0, 0, 0, 0, 0], status: Status.init, winner: "none", isComputePlayer: false, ); } XOState copyWith({ List<int>? play, Status? status, String? winner, bool? isComputerPlayer, }) { return XOState( board: play ?? this.board, status: status ?? this.status, winner: winner ?? this.winner, isComputePlayer: isComputePlayer ?? this.isComputePlayer, ); } }

قمنا ايضاً بصنع حالةٍ للعب وهي مقسمة الى أساسية , فائزة , ومتعادلة.


بعد ذلك قمنا ببناء كلاس المتحكّم على طريقة الريفربود من خلال كلاسٍ يدعى StateNotifier , فصار لدينا بالشكل التالي :

import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:xo_game/state/state.dart'; final XOProvider = StateNotifierProvider<XONotifier, XOState>((ref) { return XONotifier(); }); class XONotifier extends StateNotifier<XOState> { XONotifier() : super(XOState.Initial()); void setIndex(int ind) { if (state.status != Status.init) { state = state.copyWith( play: [0, 0, 0, 0, 0, 0, 0, 0, 0], isComputerPlayer: false, status: Status.init, winner: "none"); } if (state.board[ind] == 0) { state.board[ind] = getPlayer(); } else { return; } ; checkWinner(); if (state.board.contains(0) == false) { state = state.copyWith(status: Status.draw); } if (state.status == Status.init) { state.isComputePlayer = !state.isComputePlayer; state = state.copyWith(isComputerPlayer: state.isComputePlayer); } } void checkWinner() { if (state.board[0] == state.board[1] && state.board[0] == state.board[2] && state.board[0] != 0) { setWinner(); } else if (state.board[3] == state.board[4] && state.board[3] == state.board[5] && state.board[3] != 0) { setWinner(); } else if (state.board[6] == state.board[7] && state.board[6] == state.board[8] && state.board[6] != 0) { setWinner(); } else if (state.board[0] == state.board[3] && state.board[0] == state.board[6] && state.board[0] != 0) { setWinner(); } else if (state.board[1] == state.board[4] && state.board[1] == state.board[7] && state.board[1] != 0) { setWinner(); } else if (state.board[2] == state.board[5] && state.board[2] == state.board[8] && state.board[2] != 0) { setWinner(); } else if (state.board[0] == state.board[4] && state.board[0] == state.board[8] && state.board[0] != 0) { setWinner(); } else if (state.board[2] == state.board[4] && state.board[2] == state.board[6] && state.board[2] != 0) { setWinner(); } } void setWinner() { List<String> win = ['none', 'X', 'O']; state = state.copyWith(status: Status.finish, winner: win[getPlayer()]); } int getPlayer() { return state.isComputePlayer ? 1 : 2; } }

تقوم دالة setIndex بالتأكد أولاً من حالة اللعب , فإذا لم تكن حالة اللعبة أساسية فذلك يعني أن اللعبة انتهت فتعيد اللعبة إلى وضعها الأساسي.


ثم بعد ذلك تتحقق من خلو مكان اللاعب فحيث انه ان صار مساوياً لصفر فذلك يعني إمكانية تثبيت اللعب.


ثم بعد ذلك تقوم الدالة بالتأكد من أن ما اختاره اللاعب الأول أم اللاعب الثاني من خلال متغير isComputer وذلك عبر استدعاء دالة getPlayer والتي ترجع اللاعب المختار.


ثم بعد ذلك تأتي دالة checkWinner والتي تتحقق من وجود أي حالةٍ فائزة من الحالات المذكورة , فإن كانت هناك حالةٌ فائزة حددت اللاعب الفائز , ثم حولت الحالة إلى وجود فائز.


تأتي بعد ذلك دالةٌ أخرى وهي التي تتحقق من وجود التعادل وذلك حينما تخلو المصفوفة من أي ارقام 0 فتعني أن الحالة تعادل.


ثم بعد ان تتأكد تأتي الدالة التي تحول اللعب من اللاعب الأول الى الثاني في حالةِ أنه لم يفز أحد.


ثم بعد ذلك قمنا بصناعة مزوّد Provider يزوّد الحالة والمتحكّم للشاشة , وقمنا من خلاله باستدعاء الدوال وعرض الحالة على الشاشة.


أما الشاشة فقمنا بكتابة برنامجها على النحو التالي:

import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:xo_game/notifire/notifire.dart'; import 'package:xo_game/state/state.dart'; class Home extends ConsumerStatefulWidget { const Home({super.key}); @override ConsumerState createState() => _HomeState(); } class _HomeState extends ConsumerState<Home> { @override Widget build(BuildContext context) { final state = ref.watch(XOProvider); final controller = ref.read(XOProvider.notifier); List<String> options = ['', 'X', 'O']; return Scaffold( body: AnimatedContainer( duration: const Duration(milliseconds: 200), alignment: Alignment.center, padding: const EdgeInsets.all(12), child: Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ Text( state.status == Status.init ? "الآن دور : ${state.isComputePlayer ? "X" : "O"}" : state.status == Status.draw ? "تعادل" : "الفائز هو : ${state.winner}", textDirection: TextDirection.rtl, style: TextStyle( fontWeight: FontWeight.bold, fontSize: 50, color: state.status == Status.init ? Colors.black : state.status == Status.draw ? Colors.red : Colors.greenAccent, ), ), GridView.builder( gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 3, ), itemCount: 9, shrinkWrap: true, itemBuilder: (context, index) => Padding( padding: const EdgeInsets.all(12.0), child: GestureDetector( onTap: () async { controller.setIndex(index); }, child: Container( decoration: BoxDecoration( color: Colors.black12, borderRadius: BorderRadius.circular(12.0)), alignment: Alignment.center, child: Text( options[state.board[index]], style: const TextStyle( fontSize: 50, ), ), ), ), ), ) ], ), ), ); } }

تقوم الواجهة بالتأكد من الحالة وعرض النص المناسب , فإما تختار بين اللاعبين أو تعرض اللاعب الفائز أو أنها تعرض تعادل , كما أنها تغير الألوان تبعاً لذلك.


بحكم أن برنامجنا يركّز على المنطق وكيفية صنعِ لعبةٍ بسيطة من خلال الريفربود فلم نوسع مجالاً لعرض كود الواجهة الرئيسية.


وللإستزادة وتجربة المشروع على غت هب:

اضغط هنا
Join