Flutter দিয়ে একটি শব্দ ধাঁধা তৈরি করুন

1. আপনি শুরু করার আগে

কল্পনা করুন যে বিশ্বের বৃহত্তম ক্রসওয়ার্ড পাজল তৈরি করা সম্ভব কিনা জিজ্ঞাসা করা হচ্ছে। আপনি স্কুলে অধ্যয়ন করা কিছু AI কৌশলের কথা মনে করেন এবং আপনি কম্পিউটেশনালভাবে নিবিড় সমস্যার সমাধান তৈরি করতে অ্যালগরিদমিক বিকল্পগুলি অন্বেষণ করতে ফ্লটার ব্যবহার করতে পারেন কিনা তা ভাবছেন।

এই কোডল্যাবে, আপনি ঠিক যে করবেন. শেষ পর্যন্ত, আপনি শব্দ গ্রিড পাজল তৈরির জন্য অ্যালগরিদমের জায়গায় খেলার জন্য একটি টুল তৈরি করেন। একটি বৈধ ক্রসওয়ার্ড ধাঁধা কী তার অনেকগুলি সংজ্ঞা রয়েছে এবং এই কৌশলগুলি আপনাকে আপনার সংজ্ঞার সাথে মানানসই ধাঁধা তৈরি করতে সহায়তা করে।

একটি ক্রসওয়ার্ড ধাঁধার অ্যানিমেশন তৈরি করা হচ্ছে।

এই টুলটিকে একটি বেস হিসাবে ব্যবহার করে, আপনি তারপরে একটি ক্রসওয়ার্ড ধাঁধা তৈরি করেন যা ক্রসওয়ার্ড জেনারেটর ব্যবহার করে ধাঁধাটি তৈরি করতে ব্যবহারকারীর সমাধান করতে পারে। এই ধাঁধাটি Android, iOS, Windows, macOS এবং Linux-এ ব্যবহারযোগ্য। এখানে এটি অ্যান্ড্রয়েডে রয়েছে:

একটি পিক্সেল ফোল্ড এমুলেটরে সমাধান করার প্রক্রিয়ায় একটি ক্রসওয়ার্ড পাজলের স্ক্রিন শট।

পূর্বশর্ত

আপনি কি শিখুন

  • ফ্লটারের compute ফাংশন এবং রিভারপডের select পুনর্নির্মাণ ফিল্টারের মান-ক্যাশিং ক্ষমতার সংমিশ্রণে ফ্লটারের রেন্ডার লুপকে বাধা না দিয়ে গণনাগতভাবে ব্যয়বহুল কাজ করার জন্য কীভাবে আইসোলেট ব্যবহার করবেন।
  • গভীরতা-প্রথম অনুসন্ধান এবং ব্যাকট্র্যাকিংয়ের মতো অনুসন্ধান-ভিত্তিক গুড ওল্ড ফ্যাশনড এআই (জিওএফএআই) কৌশলগুলিকে কার্যকর করার জন্য built_value এবং built_collection সহ অপরিবর্তনীয় ডেটা স্ট্রাকচারের সুবিধা কীভাবে নেওয়া যায়।
  • একটি দ্রুত এবং স্বজ্ঞাত উপায়ে গ্রিড ডেটা প্রদর্শন করতে two_dimensional_scrollables প্যাকেজের ক্ষমতাগুলি কীভাবে ব্যবহার করবেন।

আপনার যা প্রয়োজন

  • ফ্লাটার SDK
  • ফ্লাটার এবং ডার্ট প্লাগইন সহ ভিজ্যুয়াল স্টুডিও কোড (ভিএস কোড)।
  • আপনার নির্বাচিত উন্নয়ন লক্ষ্যের জন্য কম্পাইলার সফ্টওয়্যার। এই কোডল্যাব সমস্ত ডেস্কটপ প্ল্যাটফর্ম, অ্যান্ড্রয়েড এবং আইওএসের জন্য কাজ করে। উইন্ডোজ টার্গেট করার জন্য আপনার ভিএস কোড, ম্যাকওএস বা আইওএস টার্গেট করার জন্য এক্সকোড এবং অ্যান্ড্রয়েড টার্গেট করার জন্য অ্যান্ড্রয়েড স্টুডিওর প্রয়োজন।

2. একটি প্রকল্প তৈরি করুন

আপনার প্রথম ফ্লাটার প্রকল্প তৈরি করুন

  1. VS কোড চালু করুন।
  2. কমান্ড লাইনে, flutter new লিখুন এবং তারপর মেনুতে Flutter: New Project নির্বাচন করুন।

সাথে ভিএস কোডের একটি স্ক্রিনশট

  1. খালি অ্যাপ্লিকেশন নির্বাচন করুন এবং তারপর আপনার প্রকল্প তৈরি করতে একটি ডিরেক্টরি নির্বাচন করুন। এটি এমন কোনও ডিরেক্টরি হওয়া উচিত যার জন্য উন্নত সুবিধার প্রয়োজন নেই বা এর পথে কোনও স্থান নেই। উদাহরণগুলির মধ্যে আপনার হোম ডিরেক্টরি বা C:\src\ অন্তর্ভুক্ত রয়েছে।

খালি অ্যাপ্লিকেশন সহ VS কোডের একটি স্ক্রিনশট নতুন অ্যাপ্লিকেশন প্রবাহের অংশ হিসাবে নির্বাচিত হিসাবে দেখানো হয়েছে৷

  1. আপনার প্রকল্পের নাম দিন generate_crossword । এই কোডল্যাবের অবশিষ্টাংশ অনুমান করে আপনি আপনার অ্যাপের নাম দিয়েছেন generate_crossword

সাথে ভিএস কোডের একটি স্ক্রিন শট

ফ্লটার এখন আপনার প্রোজেক্ট ফোল্ডার তৈরি করে এবং VS কোড এটি খোলে। আপনি এখন অ্যাপের একটি বেসিক স্ক্যাফোল্ড সহ দুটি ফাইলের বিষয়বস্তু ওভাররাইট করবেন।

প্রাথমিক অ্যাপটি কপি এবং পেস্ট করুন

  1. VS কোডের বাম প্যানে, এক্সপ্লোরার ক্লিক করুন এবং pubspec.yaml ফাইলটি খুলুন।

Pubspec.yaml ফাইলের অবস্থান হাইলাইট করে তীর সহ VS কোডের একটি আংশিক স্ক্রিন শট

  1. এই ফাইলের বিষয়বস্তু নিম্নলিখিত দিয়ে প্রতিস্থাপন করুন:

pubspec.yaml

name: generate_crossword
description: "A new Flutter project."
publish_to: 'none'
version: 0.1.0

environment:
  sdk: '>=3.3.3 <4.0.0'

dependencies:
  built_collection: ^5.1.1
  built_value: ^8.9.2
  characters: ^1.3.0
  flutter:
    sdk: flutter
  flutter_riverpod: ^2.5.1
  intl: ^0.19.0
  riverpod: ^2.5.1
  riverpod_annotation: ^2.3.5
  two_dimensional_scrollables: ^0.2.0

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^3.0.0
  build_runner: ^2.4.9
  built_value_generator: ^8.9.2
  custom_lint: ^0.6.4
  riverpod_generator: ^2.4.0
  riverpod_lint: ^2.3.10

flutter:
  uses-material-design: true

pubspec.yaml ফাইলটি আপনার অ্যাপ সম্পর্কে প্রাথমিক তথ্য যেমন এর বর্তমান সংস্করণ এবং এর নির্ভরতা নির্দিষ্ট করে। আপনি নির্ভরতার একটি সংগ্রহ দেখতে পাচ্ছেন যা সাধারণ খালি ফ্লাটার অ্যাপের অংশ নয়। আগামী ধাপে আপনি এই সমস্ত প্যাকেজ থেকে উপকৃত হবেন।

  1. lib/ ডিরেক্টরিতে main.dart ফাইলটি খুলুন।

একটি তীর সহ VS কোডের একটি আংশিক স্ক্রিন শট যা main.dart ফাইলের অবস্থান দেখাচ্ছে৷

  1. এই ফাইলের বিষয়বস্তু নিম্নলিখিত দিয়ে প্রতিস্থাপন করুন:

lib/main.dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

void main() {
  runApp(
    ProviderScope(
      child: MaterialApp(
        title: 'Crossword Builder',
        debugShowCheckedModeBanner: false,
        theme: ThemeData(
          useMaterial3: true,
          colorSchemeSeed: Colors.blueGrey,
          brightness: Brightness.light,
        ),
        home: Scaffold(
          body: Center(
            child: Text(
              'Hello, World!',
              style: TextStyle(fontSize: 24),
            ),
          ),
        ),
      ),
    ),
  );
}
  1. সবকিছু কাজ করে চেক করতে এই কোড চালান. এটি সর্বত্র প্রতিটি নতুন প্রকল্পের বাধ্যতামূলক শুরুর বাক্যাংশ সহ একটি নতুন উইন্ডো প্রদর্শন করা উচিত। একটি ProviderScope আছে, যা নির্দেশ করে যে এই অ্যাপটি রাজ্য পরিচালনার জন্য riverpod ব্যবহার করবে।

একটি অ্যাপ উইন্ডো যেখানে 'হ্যালো, ওয়ার্ল্ড!' কেন্দ্রে

3. শব্দ যোগ করুন

একটি ক্রসওয়ার্ড ধাঁধা জন্য ব্লক বিল্ডিং

একটি ক্রসওয়ার্ড, তার হৃদয়ে, শব্দের একটি তালিকা। শব্দগুলি একটি গ্রিডে সাজানো হয়, কিছু জুড়ে, কিছু নীচে, এমনভাবে শব্দগুলি ইন্টারলক করে। একটি শব্দ সমাধান করলে সেই প্রথম শব্দটি অতিক্রমকারী শব্দের সূত্র পাওয়া যায়। সুতরাং, প্রথম বিল্ডিং ব্লক শব্দের একটি তালিকা হতে হবে.

এই শব্দগুলির জন্য একটি ভাল উত্স হল পিটার নরভিগের প্রাকৃতিক ভাষা কর্পাস ডেটা পৃষ্ঠা। SOWPODS তালিকাটি একটি দরকারী সূচনা পয়েন্ট হিসাবে, এর 267,750 শব্দ সহ।

এই ধাপে, আপনি শব্দের একটি তালিকা ডাউনলোড করুন, এটিকে আপনার ফ্লাটার অ্যাপে একটি সম্পদ হিসেবে যোগ করুন এবং একটি রিভারপড প্রদানকারীকে স্টার্টআপে অ্যাপে তালিকা লোড করার ব্যবস্থা করুন।

শুরু করতে, এই পদক্ষেপগুলি অনুসরণ করুন:

  1. আপনার নির্বাচিত শব্দ তালিকার জন্য নিম্নলিখিত সম্পদ ঘোষণা যোগ করতে আপনার প্রকল্পের pubspec.yaml ফাইল পরিবর্তন করুন। এই তালিকাটি আপনার অ্যাপের কনফিগারেশনের শুধুমাত্র ফ্লটার স্তবক দেখায়, কারণ বাকিগুলি একই রয়ে গেছে।

pubspec.yaml

flutter:
  uses-material-design: true
  assets:                                       // Add this line
    - assets/words.txt                          // And this one.

আপনার সম্পাদক সম্ভবত একটি সতর্কতা সহ এই শেষ লাইনটি হাইলাইট করবেন কারণ আপনি এখনও এই ফাইলটি তৈরি করেননি।

  1. আপনার ব্রাউজার এবং আপনার সম্পাদক ব্যবহার করে, আপনার প্রকল্পের শীর্ষ স্তরে একটি assets ডিরেক্টরি তৈরি করুন এবং উপরে লিঙ্ক করা শব্দ তালিকাগুলির মধ্যে একটি দিয়ে এটিতে একটি words.txt ফাইল তৈরি করুন৷

এই কোডটি উপরে উল্লিখিত SOWPODS তালিকার সাথে ডিজাইন করা হয়েছে, তবে শুধুমাত্র AZ অক্ষর সমন্বিত যেকোনো শব্দ তালিকার সাথে কাজ করা উচিত। বিভিন্ন অক্ষর সেটের সাথে কাজ করার জন্য এই কোডবেসটি প্রসারিত করা পাঠকের জন্য একটি অনুশীলন হিসাবে ছেড়ে দেওয়া হয়েছে।

শব্দ লোড

অ্যাপ স্টার্টআপে শব্দ তালিকা লোড করার জন্য দায়ী কোডটি লিখতে, এই পদক্ষেপগুলি অনুসরণ করুন:

  1. lib ডিরেক্টরিতে একটি providers.dart ফাইল তৈরি করুন।
  2. ফাইলে নিম্নলিখিত যোগ করুন:

lib/providers.dart

import 'dart:convert';

import 'package:built_collection/built_collection.dart';
import 'package:flutter/services.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'providers.g.dart';

/// A provider for the wordlist to use when generating the crossword.
@riverpod
Future<BuiltSet<String>> wordList(WordListRef ref) async {
  // This codebase requires that all words consist of lowercase characters
  // in the range 'a'-'z'. Words containing uppercase letters will be
  // lowercased, and words containing runes outside this range will
  // be removed.

  final re = RegExp(r'^[a-z]+$');
  final words = await rootBundle.loadString('assets/words.txt');
  return const LineSplitter().convert(words).toBuiltSet().rebuild((b) => b
    ..map((word) => word.toLowerCase().trim())
    ..where((word) => word.length > 2)
    ..where((word) => re.hasMatch(word)));
}

এই কোডবেসের জন্য এটি আপনার প্রথম রিভারপড প্রদানকারী। আপনি লক্ষ্য করবেন যে এমন বেশ কয়েকটি ক্ষেত্র রয়েছে যা আপনার সম্পাদক একটি অনির্ধারিত শ্রেণী বা একটি অনির্ধারিত লক্ষ্য হিসাবে অভিযোগ করবেন। এই প্রকল্পটি রিভারপড সহ একাধিক নির্ভরতার জন্য কোড জেনারেশন ব্যবহার করে, তাই অনির্ধারিত শ্রেণী ত্রুটি প্রত্যাশিত।

  1. কোড তৈরি করা শুরু করতে, নিম্নলিখিত কমান্ডটি চালান:
$ dart run build_runner watch -d
[INFO] Generating build script completed, took 174ms
[INFO] Setting up file watchers completed, took 5ms
[INFO] Waiting for all file watchers to be ready completed, took 202ms
[INFO] Reading cached asset graph completed, took 65ms
[INFO] Checking for updates since last build completed, took 680ms
[INFO] Running build completed, took 2.3s
[INFO] Caching finalized dependency graph completed, took 42ms
[INFO] Succeeded after 2.3s with 122 outputs (243 actions)

আপনি প্রজেক্টে পরিবর্তন করার সাথে সাথে জেনারেট করা ফাইলগুলি আপডেট করে এটি পটভূমিতে চলতে থাকবে। একবার এই কমান্ডটি providers.g.dart এ কোড তৈরি করলে, আপনার সম্পাদক উপরের providers.dart এ যে কোডটি যোগ করেছেন তাতে খুশি হওয়া উচিত।

রিভারপডে, আপনার উপরে সংজ্ঞায়িত wordList ফাংশনের মতো প্রদানকারীরা সাধারণত অলসভাবে তাত্ক্ষণিক হয়। যাইহোক, এই অ্যাপের উদ্দেশ্যে, আপনার শব্দ তালিকাটি সাগ্রহে লোড করা দরকার। রিভারপড ডকুমেন্টেশন আপনাকে সাগ্রহে লোড করতে হবে এমন প্রদানকারীদের সাথে ডিল করার জন্য নিম্নলিখিত পদ্ধতির পরামর্শ দেয়। আপনি এখন এটি বাস্তবায়ন করবেন।

  1. একটি lib/widgets ডিরেক্টরিতে একটি crossword_generator_app.dart ফাইল তৈরি করুন।
  2. ফাইলে নিম্নলিখিত যোগ করুন:

lib/widgets/crossword_generator_app.dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import '../providers.dart';

class CrosswordGeneratorApp extends StatelessWidget {
  const CrosswordGeneratorApp({super.key});

  @override
  Widget build(BuildContext context) {
    return _EagerInitialization(
      child: Scaffold(
        appBar: AppBar(
          titleTextStyle: TextStyle(
            color: Theme.of(context).colorScheme.primary,
            fontSize: 16,
            fontWeight: FontWeight.bold,
          ),
          title: Text('Crossword Generator'),
        ),
        body: SafeArea(
          child: Consumer(
            builder: (context, ref, _) {
              final wordListAsync = ref.watch(wordListProvider);
              return wordListAsync.when(
                data: (wordList) => ListView.builder(
                  itemCount: wordList.length,
                  itemBuilder: (context, index) {
                    return ListTile(
                      title: Text(wordList.elementAt(index)),
                    );
                  },
                ),
                error: (error, stackTrace) => Center(
                  child: Text('$error'),
                ),
                loading: () => Center(
                  child: CircularProgressIndicator(),
                ),
              );
            },
          ),
        ),
      ),
    );
  }
}

class _EagerInitialization extends ConsumerWidget {
  const _EagerInitialization({required this.child});
  final Widget child;

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    ref.watch(wordListProvider);
    return child;
  }
}

এই ফাইল দুটি পৃথক দিক থেকে আকর্ষণীয়. প্রথমটি হল _EagerInitialization উইজেট, যার একমাত্র উদ্দেশ্য রয়েছে শব্দ তালিকা লোড করার জন্য আপনার উপরে তৈরি করা wordList প্রদানকারীর প্রয়োজন। এই উইজেটটি ref.watch() কল ব্যবহার করে প্রদানকারীর কথা শুনে এই উদ্দেশ্য অর্জন করে। আপনি এই কৌশলটি সম্পর্কে আরও পড়তে পারেন রিভারপড ডকুমেন্টেশনে প্রোভাইডারদের সূচনা করার জন্য

এই ফাইলে লক্ষ্য করার দ্বিতীয় আকর্ষণীয় বিষয় হল রিভারপড কীভাবে অ্যাসিঙ্ক্রোনাস সামগ্রী পরিচালনা করে। আপনি মনে করতে পারেন, wordList প্রদানকারীকে একটি অ্যাসিঙ্ক্রোনাস ফাংশন হিসাবে সংজ্ঞায়িত করা হয়েছে, কারণ ডিস্ক থেকে সামগ্রী লোড করা ধীর। এই কোডে শব্দ তালিকা প্রদানকারী দেখার সময়, আপনি একটি AsyncValue<BuiltSet<String>> পাবেন। এই ধরনের AsyncValue অংশটি প্রদানকারীর অ্যাসিঙ্ক্রোনাস ওয়ার্ল্ড এবং উইজেটের build পদ্ধতির সিঙ্ক্রোনাস ওয়ার্ল্ডের মধ্যে একটি অ্যাডাপ্টার।

AsyncValue 's when পদ্ধতি তিনটি সম্ভাব্য অবস্থা পরিচালনা করে যে ভবিষ্যত মান হতে পারে। ভবিষ্যত সফলভাবে সমাধান করা হয়েছে, যে ক্ষেত্রে data কলব্যাক আহ্বান করা হয়েছে, এটি একটি ত্রুটির অবস্থায় থাকতে পারে, যে ক্ষেত্রে error কলব্যাক আমন্ত্রণ করা হয়েছে, বা অবশেষে এটি এখনও লোড হতে পারে। তিনটি কলব্যাকের রিটার্ন প্রকারের অবশ্যই সামঞ্জস্যপূর্ণ রিটার্নের ধরন থাকতে হবে, কারণ কলব্যাকের রিটার্ন when পদ্ধতি দ্বারা ফেরত দেওয়া হয়। এই উদাহরণে, কখন পদ্ধতির ফলাফল Scaffold উইজেটের body হিসাবে প্রদর্শিত হয়।

একটি প্রায় অসীম তালিকা অ্যাপ্লিকেশন তৈরি করুন

আপনার অ্যাপে CrosswordGeneratorApp উইজেট সংহত করতে, এই পদক্ষেপগুলি অনুসরণ করুন:

  1. নিম্নলিখিত কোড যোগ করে lib/main.dart ফাইল আপডেট করুন:

lib/main.dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import 'widgets/crossword_generator_app.dart';             // Add this import

void main() {
  runApp(
    ProviderScope(
      child: MaterialApp(
        title: 'Crossword Builder',
        debugShowCheckedModeBanner: false,
        theme: ThemeData(
          useMaterial3: true,
          colorSchemeSeed: Colors.blueGrey,
          brightness: Brightness.light,
        ),
        home: CrosswordGeneratorApp(),                     // Remove what was here and replace
      ),
    ),
  );
}
  1. অ্যাপটি রিস্টার্ট করুন। আপনার একটি স্ক্রলিং তালিকা দেখতে হবে যা প্রায় চিরকাল চলবে।

'ক্রসওয়ার্ড জেনারেটর' শিরোনাম এবং শব্দের তালিকা সহ একটি অ্যাপ উইন্ডো

4. একটি গ্রিডে শব্দ প্রদর্শন করুন

এই ধাপে, আপনি built_value এবং built_collection প্যাকেজ ব্যবহার করে একটি ক্রসওয়ার্ড পাজল তৈরি করার জন্য একটি ডেটা স্ট্রাকচার তৈরি করবেন। এই দুটি প্যাকেজ অপরিবর্তনীয় মান হিসাবে ডেটা স্ট্রাকচার তৈরি করতে সক্ষম করে, যা আইসোলেটের মধ্যে সহজে ডেটা পাস করার জন্য এবং গভীরতার প্রথম অনুসন্ধান এবং ব্যাকট্র্যাকিংকে অনেক সহজ করে তোলার জন্য কার্যকর হবে।

শুরু করতে, এই পদক্ষেপগুলি অনুসরণ করুন:

  1. lib ডিরেক্টরিতে একটি model.dart ফাইল তৈরি করুন এবং তারপর ফাইলটিতে নিম্নলিখিত বিষয়বস্তু যোগ করুন:

lib/model.dart

import 'package:built_collection/built_collection.dart';
import 'package:built_value/built_value.dart';
import 'package:built_value/serializer.dart';
import 'package:characters/characters.dart';

part 'model.g.dart';

/// A location in a crossword.
abstract class Location implements Built<Location, LocationBuilder> {
  static Serializer<Location> get serializer => _$locationSerializer;

  /// The horizontal part of the location. The location is 0 based.
  int get x;

  /// The vertical part of the location. The location is 0 based.
  int get y;

  /// Returns a new location that is one step to the left of this location.
  Location get left => rebuild((b) => b.x = x - 1);

  /// Returns a new location that is one step to the right of this location.
  Location get right => rebuild((b) => b.x = x + 1);

  /// Returns a new location that is one step up from this location.
  Location get up => rebuild((b) => b.y = y - 1);

  /// Returns a new location that is one step down from this location.
  Location get down => rebuild((b) => b.y = y + 1);

  /// Returns a new location that is [offset] steps to the left of this location.
  Location leftOffset(int offset) => rebuild((b) => b.x = x - offset);

  /// Returns a new location that is [offset] steps to the right of this location.
  Location rightOffset(int offset) => rebuild((b) => b.x = x + offset);

  /// Returns a new location that is [offset] steps up from this location.
  Location upOffset(int offset) => rebuild((b) => b.y = y - offset);

  /// Returns a new location that is [offset] steps down from this location.
  Location downOffset(int offset) => rebuild((b) => b.y = y + offset);

  /// Pretty print a location as a (x,y) coordinate.
  String prettyPrint() => '($x,$y)';

  /// Returns a new location built from [updates]. Both [x] and [y] are
  /// required to be non-null.
  factory Location([void Function(LocationBuilder)? updates]) = _$Location;
  Location._();

  /// Returns a location at the given coordinates.
  factory Location.at(int x, int y) {
    return Location((b) {
      b
        ..x = x
        ..y = y;
    });
  }
}

/// The direction of a word in a crossword.
enum Direction {
  across,
  down;

  @override
  String toString() => name;
}

/// A word in a crossword. This is a word at a location in a crossword, in either
/// the across or down direction.
abstract class CrosswordWord
    implements Built<CrosswordWord, CrosswordWordBuilder> {
  static Serializer<CrosswordWord> get serializer => _$crosswordWordSerializer;

  /// The word itself.
  String get word;

  /// The location of this word in the crossword.
  Location get location;

  /// The direction of this word in the crossword.
  Direction get direction;

  /// Compare two CrosswordWord by coordinates, x then y.
  static int locationComparator(CrosswordWord a, CrosswordWord b) {
    final compareRows = a.location.y.compareTo(b.location.y);
    final compareColumns = a.location.x.compareTo(b.location.x);
    return switch (compareColumns) { 0 => compareRows, _ => compareColumns };
  }

  /// Constructor for [CrosswordWord].
  factory CrosswordWord.word({
    required String word,
    required Location location,
    required Direction direction,
  }) {
    return CrosswordWord((b) => b
      ..word = word
      ..direction = direction
      ..location.replace(location));
  }

  /// Constructor for [CrosswordWord].
  /// Use [CrosswordWord.word] instead.
  factory CrosswordWord([void Function(CrosswordWordBuilder)? updates]) =
      _$CrosswordWord;
  CrosswordWord._();
}

/// A character in a crossword. This is a single character at a location in a
/// crossword. It may be part of an across word, a down word, both, but not
/// neither. The neither constraint is enforced elsewhere.
abstract class CrosswordCharacter
    implements Built<CrosswordCharacter, CrosswordCharacterBuilder> {
  static Serializer<CrosswordCharacter> get serializer =>
      _$crosswordCharacterSerializer;

  /// The character at this location.
  String get character;

  /// The across word that this character is a part of.
  CrosswordWord? get acrossWord;

  /// The down word that this character is a part of.
  CrosswordWord? get downWord;

  /// Constructor for [CrosswordCharacter].
  /// [acrossWord] and [downWord] are optional.
  factory CrosswordCharacter.character({
    required String character,
    CrosswordWord? acrossWord,
    CrosswordWord? downWord,
  }) {
    return CrosswordCharacter((b) {
      b.character = character;
      if (acrossWord != null) {
        b.acrossWord.replace(acrossWord);
      }
      if (downWord != null) {
        b.downWord.replace(downWord);
      }
    });
  }

  /// Constructor for [CrosswordCharacter].
  /// Use [CrosswordCharacter.character] instead.
  factory CrosswordCharacter(
          [void Function(CrosswordCharacterBuilder)? updates]) =
      _$CrosswordCharacter;
  CrosswordCharacter._();
}

/// A crossword puzzle. This is a grid of characters with words placed in it.
/// The puzzle constraint is in the English crossword puzzle tradition.
abstract class Crossword implements Built<Crossword, CrosswordBuilder> {
  /// Serializes and deserializes the [Crossword] class.
  static Serializer<Crossword> get serializer => _$crosswordSerializer;

  /// Width across the [Crossword] puzzle.
  int get width;

  /// Height down the [Crossword] puzzle.
  int get height;

  /// The words in the crossword.
  BuiltList<CrosswordWord> get words;

  /// The characters by location. Useful for displaying the crossword.
  BuiltMap<Location, CrosswordCharacter> get characters;

  /// Add a word to the crossword at the given location and direction.
  Crossword addWord({
    required Location location,
    required String word,
    required Direction direction,
  }) {
    return rebuild(
      (b) => b
        ..words.add(
          CrosswordWord.word(
            word: word,
            direction: direction,
            location: location,
          ),
        ),
    );
  }

  /// As a finalize step, fill in the characters map.
  @BuiltValueHook(finalizeBuilder: true)
  static void _fillCharacters(CrosswordBuilder b) {
    b.characters.clear();

    for (final word in b.words.build()) {
      for (final (idx, character) in word.word.characters.indexed) {
        switch (word.direction) {
          case Direction.across:
            b.characters.updateValue(
              word.location.rightOffset(idx),
              (b) => b.rebuild((bInner) => bInner.acrossWord.replace(word)),
              ifAbsent: () => CrosswordCharacter.character(
                acrossWord: word,
                character: character,
              ),
            );
          case Direction.down:
            b.characters.updateValue(
              word.location.downOffset(idx),
              (b) => b.rebuild((bInner) => bInner.downWord.replace(word)),
              ifAbsent: () => CrosswordCharacter.character(
                downWord: word,
                character: character,
              ),
            );
        }
      }
    }
  }

  /// Pretty print a crossword. Generates the character grid, and lists
  /// the down words and across words sorted by location.
  String prettyPrintCrossword() {
    final buffer = StringBuffer();
    final grid = List.generate(
      height,
      (_) => List.generate(
        width, (_) => '░', // https://www.compart.com/en/unicode/U+2591
      ),
    );

    for (final MapEntry(key: Location(:x, :y), value: character)
        in characters.entries) {
      grid[y][x] = character.character;
    }

    for (final row in grid) {
      buffer.writeln(row.join());
    }

    buffer.writeln();
    buffer.writeln('Across:');
    for (final word
        in words.where((word) => word.direction == Direction.across).toList()
          ..sort(CrosswordWord.locationComparator)) {
      buffer.writeln('${word.location.prettyPrint()}: ${word.word}');
    }

    buffer.writeln();
    buffer.writeln('Down:');
    for (final word
        in words.where((word) => word.direction == Direction.down).toList()
          ..sort(CrosswordWord.locationComparator)) {
      buffer.writeln('${word.location.prettyPrint()}: ${word.word}');
    }

    return buffer.toString();
  }

  /// Constructor for [Crossword].
  factory Crossword.crossword({
    required int width,
    required int height,
    Iterable<CrosswordWord>? words,
  }) {
    return Crossword((b) {
      b
        ..width = width
        ..height = height;
      if (words != null) {
        b.words.addAll(words);
      }
    });
  }

  /// Constructor for [Crossword].
  /// Use [Crossword.crossword] instead.
  factory Crossword([void Function(CrosswordBuilder)? updates]) = _$Crossword;
  Crossword._();
}

/// Construct the serialization/deserialization code for the data model.
@SerializersFor([
  Location,
  Crossword,
  CrosswordWord,
  CrosswordCharacter,
])
final Serializers serializers = _$serializers;

এই ফাইলটি ডেটা কাঠামোর শুরুর বর্ণনা দেয় যা আপনি ক্রসওয়ার্ড তৈরি করতে ব্যবহার করবেন। এর হৃদয়ে, একটি ক্রসওয়ার্ড পাজল হল একটি গ্রিডে আবদ্ধ অনুভূমিক এবং উল্লম্ব শব্দগুলির একটি তালিকা। এই ডেটা স্ট্রাকচার ব্যবহার করার জন্য, আপনি Crossword.crossword নামের কন্সট্রাক্টর দিয়ে উপযুক্ত আকারের একটি Crossword তৈরি করেন, তারপর আপনি addWord পদ্ধতি ব্যবহার করে শব্দ যোগ করেন। চূড়ান্ত মান নির্মাণের অংশ হিসেবে, _fillCharacters পদ্ধতি দ্বারা CrosswordCharacter s-এর একটি গ্রিড তৈরি করা হয়।

এই তথ্য কাঠামো ব্যবহার করতে, এই পদক্ষেপগুলি অনুসরণ করুন:

  1. lib ডিরেক্টরিতে একটি utils ফাইল তৈরি করুন এবং তারপর ফাইলটিতে নিম্নলিখিত বিষয়বস্তু যোগ করুন:

lib/utils.dart

import 'dart:math';

import 'package:built_collection/built_collection.dart';

/// A [Random] instance for generating random numbers.
final _random = Random();

/// An extension on [BuiltSet] that adds a method to get a random element.
extension RandomElements<E> on BuiltSet<E> {
  E randomElement() {
    return elementAt(_random.nextInt(length));
  }
}

এটি BuiltSet একটি এক্সটেনশন যা সেটের একটি এলোমেলো উপাদান পুনরুদ্ধার করা সহজ করে তোলে। এক্সটেনশন পদ্ধতি অতিরিক্ত কার্যকারিতা সহ ক্লাস প্রসারিত করা সহজ করে তোলে। utils.dart ফাইলের বাইরে এক্সটেনশন উপলব্ধ করতে এক্সটেনশনের নামকরণ প্রয়োজন।

  1. আপনার lib/providers.dart ফাইলে, নিম্নলিখিত আমদানি যোগ করুন:

lib/providers.dart

import 'dart:convert';
import 'dart:math';                                        // Add this import

import 'package:built_collection/built_collection.dart';
import 'package:flutter/foundation.dart';                  // Add this import
import 'package:flutter/services.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

import 'model.dart' as model;                              // And this import
import 'utils.dart';                                       // And this one

part 'providers.g.dart';

/// A provider for the wordlist to use when generating the crossword.
@riverpod
Future<BuiltSet<String>> wordList(WordListRef ref) async {

এই আমদানিগুলি আপনি তৈরি করতে চলেছেন এমন সরবরাহকারীদের কাছে উপরে সংজ্ঞায়িত মডেলটি প্রকাশ করে৷ Random এর জন্য dart:math ইম্পোর্ট অন্তর্ভুক্ত করা হয়েছে, flutter/foundation.dart আমদানি debugPrint জন্য, মডেলের জন্য model.dart এবং BuiltSet এক্সটেনশনের জন্য utils.dart অন্তর্ভুক্ত করা হয়েছে।

  1. একই ফাইলের শেষে, নিম্নলিখিত প্রদানকারী যোগ করুন:

lib/providers.dart

/// An enumeration for different sizes of [model.Crossword]s.
enum CrosswordSize {
  small(width: 20, height: 11),
  medium(width: 40, height: 22),
  large(width: 80, height: 44),
  xlarge(width: 160, height: 88),
  xxlarge(width: 500, height: 500);

  const CrosswordSize({
    required this.width,
    required this.height,
  });

  final int width;
  final int height;
  String get label => '$width x $height';
}

/// A provider that holds the current size of the crossword to generate.
@Riverpod(keepAlive: true)
class Size extends _$Size {
  var _size = CrosswordSize.medium;

  @override
  CrosswordSize build() => _size;

  void setSize(CrosswordSize size) {
    _size = size;
    ref.invalidateSelf();
  }
}

final _random = Random();

@riverpod
Stream<model.Crossword> crossword(CrosswordRef ref) async* {
  final size = ref.watch(sizeProvider);
  final wordListAsync = ref.watch(wordListProvider);

  var crossword =
      model.Crossword.crossword(width: size.width, height: size.height);

  yield* wordListAsync.when(
    data: (wordList) async* {
      while (crossword.characters.length < size.width * size.height * 0.8) {
        final word = wordList.randomElement();
        final direction =
            _random.nextBool() ? model.Direction.across : model.Direction.down;
        final location = model.Location.at(
            _random.nextInt(size.width), _random.nextInt(size.height));

        crossword = crossword.addWord(
            word: word, direction: direction, location: location);
        yield crossword;
        await Future.delayed(Duration(milliseconds: 100));
      }

      yield crossword;
    },
    error: (error, stackTrace) async* {
      debugPrint('Error loading word list: $error');
      yield crossword;
    },
    loading: () async* {
      yield crossword;
    },
  );
}

এই পরিবর্তনগুলি আপনার অ্যাপে দুটি প্রদানকারী যোগ করে। প্রথমটি হল Size , যা কার্যকরীভাবে একটি বিশ্বব্যাপী পরিবর্তনশীল যা বর্তমানে CrosswordSize গণনার নির্বাচিত মান ধারণ করে। এটি নির্মাণাধীন ক্রসওয়ার্ডের আকার প্রদর্শন এবং সেট উভয়ই UI-কে অনুমতি দেবে। দ্বিতীয় প্রদানকারী, crossword , একটি আরো আকর্ষণীয় সৃষ্টি. এটি এমন একটি ফাংশন যা Crossword একটি সিরিজ প্রদান করে। এটি জেনারেটরের জন্য ডার্টের সমর্থন ব্যবহার করে তৈরি করা হয়েছে, যেমন ফাংশনে async* দ্বারা চিহ্নিত করা হয়েছে। এর মানে হল যে রিটার্নে শেষ হওয়ার পরিবর্তে, এটি Crossword s-এর একটি সিরিজ দেয়, একটি গণনা লেখার অনেক সহজ উপায় যা মধ্যবর্তী ফলাফল প্রদান করে।

crossword প্রদানকারী ফাংশনের শুরুতে একজোড়া ref.watch কলের উপস্থিতির কারণে, প্রতিবার ক্রসওয়ার্ডের নির্বাচিত আকার পরিবর্তন হলে এবং শব্দ তালিকা লোড হওয়া শেষ হলে Crossword s-এর স্ট্রিম রিভারপড সিস্টেম দ্বারা পুনরায় চালু হবে।

এখন আপনার কাছে ক্রসওয়ার্ড তৈরি করার কোড আছে, যদিও এলোমেলো শব্দে পূর্ণ, টুলটির ব্যবহারকারীকে সেগুলি দেখালে ভালো হবে।

  1. নিম্নলিখিত বিষয়বস্তু সহ lib/widgets ডিরেক্টরিতে একটি crossword_widget.dart ফাইল তৈরি করুন:

lib/widgets/crossword_widget.dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:two_dimensional_scrollables/two_dimensional_scrollables.dart';

import '../model.dart';
import '../providers.dart';

class CrosswordWidget extends ConsumerWidget {
  const CrosswordWidget({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final size = ref.watch(sizeProvider);
    return TableView.builder(
      diagonalDragBehavior: DiagonalDragBehavior.free,
      cellBuilder: _buildCell,
      columnCount: size.width,
      columnBuilder: (index) => _buildSpan(context, index),
      rowCount: size.height,
      rowBuilder: (index) => _buildSpan(context, index),
    );
  }

  TableViewCell _buildCell(BuildContext context, TableVicinity vicinity) {
    final location = Location.at(vicinity.column, vicinity.row);

    return TableViewCell(
      child: Consumer(
        builder: (context, ref, _) {
          final character = ref.watch(
            crosswordProvider.select(
              (crosswordAsync) => crosswordAsync.when(
                data: (crossword) => crossword.characters[location],
                error: (error, stackTrace) => null,
                loading: () => null,
              ),
            ),
          );

          if (character != null) {
            return Container(
              color: Theme.of(context).colorScheme.onPrimary,
              child: Center(
                child: Text(
                  character.character,
                  style: TextStyle(
                    fontSize: 24,
                    color: Theme.of(context).colorScheme.primary,
                  ),
                ),
              ),
            );
          }

          return ColoredBox(
            color: Theme.of(context).colorScheme.primaryContainer,
          );
        },
      ),
    );
  }

  TableSpan _buildSpan(BuildContext context, int index) {
    return TableSpan(
      extent: FixedTableSpanExtent(32),
      foregroundDecoration: TableSpanDecoration(
        border: TableSpanBorder(
          leading: BorderSide(
              color: Theme.of(context).colorScheme.onPrimaryContainer),
          trailing: BorderSide(
              color: Theme.of(context).colorScheme.onPrimaryContainer),
        ),
      ),
    );
  }
}

এই উইজেটটি, একটি ConsumerWidget হওয়ায়, Crossword অক্ষরগুলি প্রদর্শন করার জন্য গ্রিডের আকার নির্ধারণ করতে সরাসরি Size প্রদানকারীর উপর নির্ভর করতে পারে। এই গ্রিডের প্রদর্শনটি two_dimensional_scrollables প্যাকেজ থেকে TableView উইজেট দিয়ে সম্পন্ন করা হয়।

এটা লক্ষণীয় যে _buildCell হেল্পার ফাংশন দ্বারা রেন্ডার করা পৃথক কোষ প্রতিটি তাদের ফেরত Widget ট্রিতে একটি Consumer উইজেট ধারণ করে। এটি একটি রিফ্রেশ সীমানা হিসাবে কাজ করে। যখন ref.watch এর প্রত্যাবর্তিত মান পরিবর্তিত হয় তখন Consumer উইজেটের ভিতরের সবকিছু পুনরায় তৈরি করা হয়। প্রতিবার Crossword পরিবর্তিত হলে পুরো গাছটিকে পুনরায় তৈরি করতে প্রলুব্ধ হয়, তবে এর ফলে অনেক গণনা হয় যা এই সেটআপটি ব্যবহার করে এড়িয়ে যেতে পারে।

আপনি যদি ref.watch এর পরামিতিটি দেখেন, আপনি দেখতে পাবেন যে crosswordProvider.select ব্যবহার করে পুনরায় কম্পিউটিং লেআউট এড়ানোর আরেকটি স্তর রয়েছে। এর মানে হল যে ref.watch শুধুমাত্র TableViewCell এর বিষয়বস্তুগুলির পুনর্নির্মাণকে ট্রিগার করবে যখন সেলটি রেন্ডার করার জন্য দায়ী চরিত্রটি পরিবর্তন করবে। রি-রেন্ডারিংয়ের এই হ্রাস UI কে প্রতিক্রিয়াশীল রাখার একটি অপরিহার্য অংশ।

CrosswordWidget এবং Size প্রদানকারীকে ব্যবহারকারীর কাছে প্রকাশ করতে, crossword_generator_app.dart ফাইলটি নিম্নরূপ পরিবর্তন করুন:

lib/widgets/crossword_generator_app.dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import '../providers.dart';
import 'crossword_widget.dart';                               // Add this import

class CrosswordGeneratorApp extends StatelessWidget {
  const CrosswordGeneratorApp({super.key});

  @override
  Widget build(BuildContext context) {
    return _EagerInitialization(
      child: Scaffold(
        appBar: AppBar(
          actions: [_CrosswordGeneratorMenu()],               // Add this line
          titleTextStyle: TextStyle(
            color: Theme.of(context).colorScheme.primary,
            fontSize: 16,
            fontWeight: FontWeight.bold,
          ),
          title: Text('Crossword Generator'),
        ),
        body: SafeArea(
          child: CrosswordWidget(),                           // Replaces everything that was here before
        ),
      ),
    );
  }
}

class _EagerInitialization extends ConsumerWidget {
  const _EagerInitialization({required this.child});
  final Widget child;

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    ref.watch(wordListProvider);
    return child;
  }
}

class _CrosswordGeneratorMenu extends ConsumerWidget {        // Add from here
  @override
  Widget build(BuildContext context, WidgetRef ref) => MenuAnchor(
        menuChildren: [
          for (final entry in CrosswordSize.values)
            MenuItemButton(
              onPressed: () => ref.read(sizeProvider.notifier).setSize(entry),
              leadingIcon: entry == ref.watch(sizeProvider)
                  ? Icon(Icons.radio_button_checked_outlined)
                  : Icon(Icons.radio_button_unchecked_outlined),
              child: Text(entry.label),
            ),
        ],
        builder: (context, controller, child) => IconButton(
          onPressed: () => controller.open(),
          icon: Icon(Icons.settings),
        ),
      );                                                      // To here.
}

এখানে কিছু জিনিস পরিবর্তন হয়েছে। প্রথমত, wordList ListView হিসাবে রেন্ডার করার জন্য দায়ী কোডটি CrosswordWidget একটি কল দিয়ে প্রতিস্থাপিত হয়েছে যা পূর্ববর্তী ফাইলে সংজ্ঞায়িত করা হয়েছিল। অন্যান্য প্রধান পরিবর্তন হল অ্যাপের আচরণ পরিবর্তনের জন্য একটি মেনু শুরু করা, ক্রসওয়ার্ডের আকার পরিবর্তন করা থেকে শুরু করে। ভবিষ্যতের ধাপে আরও MenuItemButton যোগ করা হবে। আপনার অ্যাপটি চালান, আপনি এরকম কিছু দেখতে পাবেন:

ক্রসওয়ার্ড জেনারেটর শিরোনাম সহ একটি অ্যাপ উইন্ডো এবং অক্ষরগুলির একটি গ্রিড কোন ছড়া বা কারণ ছাড়াই ওভারল্যাপিং শব্দ হিসাবে বিন্যস্ত

একটি গ্রিডে প্রদর্শিত অক্ষর এবং একটি মেনু রয়েছে যা ব্যবহারকারীকে গ্রিডের আকার পরিবর্তন করতে সক্ষম করে। কিন্তু শব্দগুলো ক্রসওয়ার্ড ধাঁধার মতো বিছানো হয় না। শব্দগুলি কীভাবে ক্রসওয়ার্ডে যুক্ত করা হয় তার উপর কোনও সীমাবদ্ধতা প্রয়োগ না করার ফল। সংক্ষেপে, এটি একটি জগাখিচুড়ি. পরবর্তী ধাপে আপনি কিছু নিয়ন্ত্রণে আনতে শুরু করবেন!

5. সীমাবদ্ধতা প্রয়োগ করুন

ক্রসওয়ার্ড সীমাবদ্ধতা প্রয়োগ করতে মডেলটিতে কোড যোগ করা এই পদক্ষেপের লক্ষ্য। বিভিন্ন ধরনের ক্রসওয়ার্ড পাজল রয়েছে এবং এই কোডল্যাবটি যে স্টাইলটি প্রয়োগ করবে তা ইংরেজি ক্রসওয়ার্ড পাজলের ঐতিহ্য অনুসরণ করে। ক্রসওয়ার্ড পাজলগুলির অন্যান্য শৈলী তৈরি করতে এই কোডটি পরিবর্তন করা, বরাবরের মতো, পাঠকের জন্য একটি অনুশীলন হিসাবে রেখে দেওয়া হয়েছে।

শুরু করতে, এই পদক্ষেপগুলি অনুসরণ করুন:

  1. model.dart ফাইলটি খুলুন এবং Crossword মডেলটি নিম্নলিখিত দিয়ে প্রতিস্থাপন করুন:

lib/model.dart

/// A crossword puzzle. This is a grid of characters with words placed in it.
/// The puzzle constraint is in the English crossword puzzle tradition.
abstract class Crossword implements Built<Crossword, CrosswordBuilder> {
  /// Serializes and deserializes the [Crossword] class.
  static Serializer<Crossword> get serializer => _$crosswordSerializer;

  /// Width across the [Crossword] puzzle.
  int get width;

  /// Height down the [Crossword] puzzle.
  int get height;

  /// The words in the crossword.
  BuiltList<CrosswordWord> get words;

  /// The characters by location. Useful for displaying the crossword,
  /// or checking the proposed solution.
  BuiltMap<Location, CrosswordCharacter> get characters;

  /// Checks if this crossword is valid.
  bool get valid {
    // Check that there are no duplicate words.
    final wordSet = words.map((word) => word.word).toBuiltSet();
    if (wordSet.length != words.length) {
      return false;
    }

    for (final MapEntry(key: location, value: character)
        in characters.entries) {
      // All characters must be a part of an across or down word.
      if (character.acrossWord == null && character.downWord == null) {
        return false;
      }

      // All characters must be within the crossword puzzle.
      // No drawing outside the lines.
      if (location.x < 0 ||
          location.y < 0 ||
          location.x >= width ||
          location.y >= height) {
        return false;
      }

      // Characters above and below this character must be related
      // by a vertical word
      if (characters[location.up] case final up?) {
        if (character.downWord == null) {
          return false;
        }
        if (up.downWord != character.downWord) {
          return false;
        }
      }

      if (characters[location.down] case final down?) {
        if (character.downWord == null) {
          return false;
        }
        if (down.downWord != character.downWord) {
          return false;
        }
      }

      // Characters to the left and right of this character must be
      // related by a horizontal word
      final left = characters[location.left];
      if (left != null) {
        if (character.acrossWord == null) {
          return false;
        }
        if (left.acrossWord != character.acrossWord) {
          return false;
        }
      }

      final right = characters[location.right];
      if (right != null) {
        if (character.acrossWord == null) {
          return false;
        }
        if (right.acrossWord != character.acrossWord) {
          return false;
        }
      }
    }

    return true;
  }

  /// Add a word to the crossword at the given location and direction.
  Crossword? addWord({
    required Location location,
    required String word,
    required Direction direction,
  }) {
    // Require that the word is not already in the crossword.
    if (words.map((crosswordWord) => crosswordWord.word).contains(word)) {
      return null;
    }

    final wordCharacters = word.characters;
    bool overlap = false;

    // Check that the word fits in the crossword.
    for (final (index, character) in wordCharacters.indexed) {
      final characterLocation = switch (direction) {
        Direction.across => location.rightOffset(index),
        Direction.down => location.downOffset(index),
      };

      final target = characters[characterLocation];
      if (target != null) {
        overlap = true;
        if (target.character != character) {
          return null;
        }
        if (direction == Direction.across && target.acrossWord != null ||
            direction == Direction.down && target.downWord != null) {
          return null;
        }
      }
    }
    if (words.isNotEmpty && !overlap) {
      return null;
    }

    final candidate = rebuild(
      (b) => b
        ..words.add(
          CrosswordWord.word(
            word: word,
            direction: direction,
            location: location,
          ),
        ),
    );

    if (candidate.valid) {
      return candidate;
    } else {
      return null;
    }
  }

  /// As a finalize step, fill in the characters map.
  @BuiltValueHook(finalizeBuilder: true)
  static void _fillCharacters(CrosswordBuilder b) {
    b.characters.clear();

    for (final word in b.words.build()) {
      for (final (idx, character) in word.word.characters.indexed) {
        switch (word.direction) {
          case Direction.across:
            b.characters.updateValue(
              word.location.rightOffset(idx),
              (b) => b.rebuild((bInner) => bInner.acrossWord.replace(word)),
              ifAbsent: () => CrosswordCharacter.character(
                acrossWord: word,
                character: character,
              ),
            );
          case Direction.down:
            b.characters.updateValue(
              word.location.downOffset(idx),
              (b) => b.rebuild((bInner) => bInner.downWord.replace(word)),
              ifAbsent: () => CrosswordCharacter.character(
                downWord: word,
                character: character,
              ),
            );
        }
      }
    }
  }

  /// Pretty print a crossword. Generates the character grid, and lists
  /// the down words and across words sorted by location.
  String prettyPrintCrossword() {
    final buffer = StringBuffer();
    final grid = List.generate(
      height,
      (_) => List.generate(
        width, (_) => '░', // https://www.compart.com/en/unicode/U+2591
      ),
    );

    for (final MapEntry(key: Location(:x, :y), value: character)
        in characters.entries) {
      grid[y][x] = character.character;
    }

    for (final row in grid) {
      buffer.writeln(row.join());
    }

    buffer.writeln();
    buffer.writeln('Across:');
    for (final word
        in words.where((word) => word.direction == Direction.across).toList()
          ..sort(CrosswordWord.locationComparator)) {
      buffer.writeln('${word.location.prettyPrint()}: ${word.word}');
    }

    buffer.writeln();
    buffer.writeln('Down:');
    for (final word
        in words.where((word) => word.direction == Direction.down).toList()
          ..sort(CrosswordWord.locationComparator)) {
      buffer.writeln('${word.location.prettyPrint()}: ${word.word}');
    }

    return buffer.toString();
  }

  /// Constructor for [Crossword].
  factory Crossword.crossword({
    required int width,
    required int height,
    Iterable<CrosswordWord>? words,
  }) {
    return Crossword((b) {
      b
        ..width = width
        ..height = height;
      if (words != null) {
        b.words.addAll(words);
      }
    });
  }

  /// Constructor for [Crossword].
  /// Use [Crossword.crossword] instead.
  factory Crossword([void Function(CrosswordBuilder)? updates]) = _$Crossword;
  Crossword._();
}

একটি দ্রুত অনুস্মারক হিসাবে, আপনি model.dart এবং providers.dart ফাইলগুলিতে যে পরিবর্তনগুলি করছেন তার জন্য সংশ্লিষ্ট model.g.dart এবং providers.g.dart ফাইলগুলিকে আপডেট করার জন্য build_runner চালু হওয়া প্রয়োজন৷ যদি এই ফাইলগুলি স্বয়ংক্রিয়ভাবে আপডেট না হয়ে থাকে, তাহলে এখনই dart run build_runner watch -d দিয়ে build_runner আবার শুরু করার উপযুক্ত সময়।

মডেল স্তরে এই নতুন ক্ষমতার সুবিধা নিতে, আপনাকে মেলে প্রদানকারী স্তর আপডেট করতে হবে।

  1. আপনার providers.dart ফাইলটি নিম্নরূপ সম্পাদনা করুন:

lib/providers.dart

import 'dart:convert';
import 'dart:math';

import 'package:built_collection/built_collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

import 'model.dart' as model;
import 'utils.dart';

part 'providers.g.dart';

/// A provider for the wordlist to use when generating the crossword.
@riverpod
Future<BuiltSet<String>> wordList(WordListRef ref) async {
  // This codebase requires that all words consist of lowercase characters
  // in the range 'a'-'z'. Words containing uppercase letters will be
  // lowercased, and words containing runes outside this range will
  // be removed.

  final re = RegExp(r'^[a-z]+$');
  final words = await rootBundle.loadString('assets/words.txt');
  return const LineSplitter().convert(words).toBuiltSet().rebuild((b) => b
    ..map((word) => word.toLowerCase().trim())
    ..where((word) => word.length > 2)
    ..where((word) => re.hasMatch(word)));
}

/// An enumeration for different sizes of [model.Crossword]s.
enum CrosswordSize {
  small(width: 20, height: 11),
  medium(width: 40, height: 22),
  large(width: 80, height: 44),
  xlarge(width: 160, height: 88),
  xxlarge(width: 500, height: 500);

  const CrosswordSize({
    required this.width,
    required this.height,
  });

  final int width;
  final int height;
  String get label => '$width x $height';
}

/// A provider that holds the current size of the crossword to generate.
@Riverpod(keepAlive: true)
class Size extends _$Size {
  var _size = CrosswordSize.medium;

  @override
  CrosswordSize build() => _size;

  void setSize(CrosswordSize size) {
    _size = size;
    ref.invalidateSelf();
  }
}

final _random = Random();

@riverpod
Stream<model.Crossword> crossword(CrosswordRef ref) async* {
  final size = ref.watch(sizeProvider);
  final wordListAsync = ref.watch(wordListProvider);

  var crossword =
      model.Crossword.crossword(width: size.width, height: size.height);

  yield* wordListAsync.when(
    data: (wordList) async* {
      while (crossword.characters.length < size.width * size.height * 0.8) {
        final word = wordList.randomElement();
        final direction =
            _random.nextBool() ? model.Direction.across : model.Direction.down;
        final location = model.Location.at(
            _random.nextInt(size.width), _random.nextInt(size.height));

        var candidate = crossword.addWord(                 // Edit from here
            word: word, direction: direction, location: location);
        await Future.delayed(Duration(milliseconds: 10));
        if (candidate != null) {
          debugPrint('Added word: $word');
          crossword = candidate;
          yield crossword;
        } else {
          debugPrint('Failed to add word: $word');
        }                                                  // To here.
      }

      yield crossword;
    },
    error: (error, stackTrace) async* {
      debugPrint('Error loading word list: $error');
      yield crossword;
    },
    loading: () async* {
      yield crossword;
    },
  );
}
  1. আপনার অ্যাপ চালান। UI তে অনেক কিছুই ঘটছে না, তবে লগগুলি দেখলে অনেক কিছু ঘটছে।

ক্রসওয়ার্ড জেনারেটর অ্যাপ উইন্ডোতে শব্দগুলি জুড়ে এবং নীচে বিন্যস্ত, এলোমেলো পয়েন্টগুলিতে ছেদ করা

আপনি যদি এখানে কি ঘটছে তা নিয়ে চিন্তা করেন, আমরা র্যান্ডম সুযোগে একটি ক্রসওয়ার্ড দেখতে পাচ্ছি। Crossword মডেলের addWord পদ্ধতিটি বর্তমান ক্রসওয়ার্ডের সাথে খাপ খায় না এমন কোনো প্রস্তাবিত শব্দকে প্রত্যাখ্যান করছে, তাই এটি আশ্চর্যজনক যে আমরা যেকোন কিছুকেই দেখা যাচ্ছে।

কোন শব্দগুলি কোথায় ব্যবহার করতে হবে তা বেছে নেওয়ার বিষয়ে আরও পদ্ধতিগত হওয়ার প্রস্তুতির জন্য, এই গণনাটিকে UI থ্রেডের বাইরে এবং একটি ব্যাকগ্রাউন্ড আইসোলেটে সরানো খুব সহায়ক হবে৷ ফ্লটারের একটি খুব দরকারী র‍্যাপার রয়েছে যা কিছু কাজ নেওয়ার জন্য এবং এটিকে ব্যাকগ্রাউন্ড আইসোলেটে চালানোর জন্য - compute ফাংশন।

  1. providers.dart ফাইলে, ক্রসওয়ার্ড প্রদানকারীকে নিম্নরূপ পরিবর্তন করুন:

lib/providers.dart

@riverpod
Stream<model.Crossword> crossword(CrosswordRef ref) async* {
  final size = ref.watch(sizeProvider);
  final wordListAsync = ref.watch(wordListProvider);

  var crossword =
      model.Crossword.crossword(width: size.width, height: size.height);

  yield* wordListAsync.when(
    data: (wordList) async* {
      while (crossword.characters.length < size.width * size.height * 0.8) {
        final word = wordList.randomElement();
        final direction =
            _random.nextBool() ? model.Direction.across : model.Direction.down;
        final location = model.Location.at(
            _random.nextInt(size.width), _random.nextInt(size.height));
        try {
          var candidate = await compute(                   // Edit from here.
              ((String, model.Direction, model.Location) wordToAdd) {
            final (word, direction, location) = wordToAdd;
            return crossword.addWord(
                word: word, direction: direction, location: location);
          }, (word, direction, location));

          if (candidate != null) {
            crossword = candidate;
            yield crossword;
          }
        } catch (e) {
          debugPrint('Error running isolate: $e');
        }                                                  // To here.
      }

      yield crossword;
    },
    error: (error, stackTrace) async* {
      debugPrint('Error loading word list: $error');
      yield crossword;
    },
    loading: () async* {
      yield crossword;
    },
  );
}

এই কোড কাজ করে. তবে এতে একটি ফাঁদ রয়েছে। আপনি যদি এই পথটি চালিয়ে যান তবে আপনি শেষ পর্যন্ত নিম্নলিখিতগুলির মতো একটি লগ করা ত্রুটির সাথে শেষ হয়ে যাবেন:

flutter: Error running isolate: Invalid argument(s): Illegal argument in isolate message: object is unsendable - Library:'dart:async' Class: _Future@4048458 (see restrictions listed at `SendPort.send()` documentation for more information)
flutter:  <- Instance of 'AutoDisposeStreamProviderElement<Crossword>' (from package:riverpod/src/stream_provider.dart)
flutter:  <- Context num_variables: 2 <- Context num_variables: 1 parent:{ Context num_variables: 2 }
flutter:  <- Context num_variables: 1 parent:{ Context num_variables: 1 parent:{ Context num_variables: 2 } }
flutter:  <- Closure: () => Crossword? (from package:generate_crossword/providers.dart)

এটি বন্ধের ফলাফল যে compute একটি প্রদানকারীর উপর ব্যাকগ্রাউন্ড আইসোলেট ক্লোজিংকে হস্তান্তর করছে, যা SendPort.send() এর মাধ্যমে পাঠানো যাবে না। এটির জন্য একটি সমাধান হল এটি নিশ্চিত করা যে বন্ধ করার জন্য এমন কিছু নেই যা প্রেরণযোগ্য নয়।

একটি প্রথম ধাপ হল আইসোলেট কোড থেকে প্রদানকারীদের আলাদা করা।

  1. আপনার lib ডিরেক্টরিতে একটি isolates.dart ফাইল তৈরি করুন এবং তারপরে নিম্নলিখিত বিষয়বস্তু যুক্ত করুন:

lib/isolates.dart

import 'dart:math';

import 'package:built_collection/built_collection.dart';
import 'package:flutter/foundation.dart';

import 'model.dart';
import 'utils.dart';

final _random = Random();

Stream<Crossword> exploreCrosswordSolutions({
  required Crossword crossword,
  required BuiltSet<String> wordList,
}) async* {
  while (
      crossword.characters.length < crossword.width * crossword.height * 0.8) {
    final word = wordList.randomElement();
    final direction = _random.nextBool() ? Direction.across : Direction.down;
    final location = Location.at(
        _random.nextInt(crossword.width), _random.nextInt(crossword.height));
    try {
      var candidate = await compute(((String, Direction, Location) wordToAdd) {
        final (word, direction, location) = wordToAdd;
        return crossword.addWord(
            word: word, direction: direction, location: location);
      }, (word, direction, location));

      if (candidate != null) {
        crossword = candidate;
        yield crossword;
      }
    } catch (e) {
      debugPrint('Error running isolate: $e');
    }
  }
}

এই কোড যুক্তিসঙ্গতভাবে পরিচিত দেখা উচিত. এটি crossword প্রদানকারীর মধ্যে যা ছিল তার মূল, কিন্তু এখন একটি স্বতন্ত্র জেনারেটর ফাংশন হিসাবে। এখন আপনি আপনার providers.dart ফাইল আপডেট করতে পারেন এই নতুন ফাংশন ব্যবহার করে ব্যাকগ্রাউন্ড আইসোলেট ইনস্ট্যান্টিয়েট করতে।

lib/providers.dart

// Drop the dart:math import, the _random instance moved to isolates.dart
import 'dart:convert';

import 'package:built_collection/built_collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

import 'isolates.dart';                                    // Add this import
import 'model.dart' as model;
                                                           // Drop the utils.dart import

part 'providers.g.dart';

/// A provider for the wordlist to use when generating the crossword.
@riverpod
Future<BuiltSet<String>> wordList(WordListRef ref) async {
  // This codebase requires that all words consist of lowercase characters
  // in the range 'a'-'z'. Words containing uppercase letters will be
  // lowercased, and words containing runes outside this range will
  // be removed.

  final re = RegExp(r'^[a-z]+$');
  final words = await rootBundle.loadString('assets/words.txt');
  return const LineSplitter().convert(words).toBuiltSet().rebuild((b) => b
    ..map((word) => word.toLowerCase().trim())
    ..where((word) => word.length > 2)
    ..where((word) => re.hasMatch(word)));
}

/// An enumeration for different sizes of [model.Crossword]s.
enum CrosswordSize {
  small(width: 20, height: 11),
  medium(width: 40, height: 22),
  large(width: 80, height: 44),
  xlarge(width: 160, height: 88),
  xxlarge(width: 500, height: 500);

  const CrosswordSize({
    required this.width,
    required this.height,
  });

  final int width;
  final int height;
  String get label => '$width x $height';
}

/// A provider that holds the current size of the crossword to generate.
@Riverpod(keepAlive: true)
class Size extends _$Size {
  var _size = CrosswordSize.medium;

  @override
  CrosswordSize build() => _size;

  void setSize(CrosswordSize size) {
    _size = size;
    ref.invalidateSelf();
  }
}
                                                           // Drop the _random instance
@riverpod
Stream<model.Crossword> crossword(CrosswordRef ref) async* {
  final size = ref.watch(sizeProvider);
  final wordListAsync = ref.watch(wordListProvider);

  final emptyCrossword =                                   // Edit from here
      model.Crossword.crossword(width: size.width, height: size.height);

  yield* wordListAsync.when(
    data: (wordList) => exploreCrosswordSolutions(
      crossword: emptyCrossword,
      wordList: wordList,
    ),
    error: (error, stackTrace) async* {
      debugPrint('Error loading word list: $error');
      yield emptyCrossword;
    },
    loading: () async* {
      yield emptyCrossword;                                // To here.
    },
  );
}

এটির সাহায্যে আপনার কাছে এখন একটি টুল রয়েছে যা বিভিন্ন আকারের ক্রসওয়ার্ড পাজল তৈরি করছে, যার compute একটি ব্যাকগ্রাউন্ড আইসোলেটে ঘটতে থাকা ধাঁধাটি বের করা যাবে। এখন, ক্রসওয়ার্ড ধাঁধায় কোন শব্দ যোগ করার চেষ্টা করতে হবে তা নির্ধারণ করার সময় যদি শুধুমাত্র কোডটি আরও দক্ষ হতে পারে।

6. কাজের সারি পরিচালনা করুন

কোডের সাথে সমস্যাটির একটি অংশ যেমন এটি দাঁড়িয়েছে যে সমস্যার সমাধান হচ্ছে কার্যকরভাবে একটি অনুসন্ধান সমস্যা, এবং বর্তমান সমাধান হল অন্ধ অনুসন্ধান করা। যদি কোডটি বর্তমান শব্দের সাথে সংযুক্ত শব্দগুলি খুঁজে বের করার দিকে মনোনিবেশ করে, গ্রিডের যে কোনও জায়গায় এলোমেলোভাবে শব্দ রাখার চেষ্টা না করে, তাহলে সিস্টেমটি দ্রুত সমাধান খুঁজে পাবে। এটির কাছে যাওয়ার একটি উপায় হল শব্দ খুঁজে বের করার চেষ্টা করার জন্য অবস্থানগুলির একটি কাজের সারি প্রবর্তন করা।

কোডটি বর্তমানে প্রার্থীর সমাধান তৈরি করে, প্রার্থীর সমাধান বৈধ কিনা তা পরীক্ষা করে এবং বৈধতার উপর নির্ভর করে প্রার্থীকে অন্তর্ভুক্ত করে বা ফেলে দেয়। এটি অ্যালগরিদমের ব্যাকট্র্যাকিং পরিবার থেকে একটি উদাহরণ বাস্তবায়ন। এই বাস্তবায়নটি built_value এবং built_collection দ্বারা অনেক সহজ করা হয়েছে, যা নতুন অপরিবর্তনীয় মান তৈরি করতে সক্ষম করে যা থেকে উদ্ভূত হয় এবং এইভাবে, তারা যে অপরিবর্তনীয় মান থেকে উদ্ভূত হয়েছিল তার সাথে সাধারণ অবস্থা ভাগ করে। এটি গভীর অনুলিপি করার জন্য প্রয়োজনীয় মেমরি খরচ ছাড়াই সম্ভাব্য প্রার্থীদের সস্তা শোষণ করতে সক্ষম করে।

শুরু করতে, এই পদক্ষেপগুলি অনুসরণ করুন:

  1. model.dart ফাইলটি খুলুন এবং এতে নিম্নলিখিত WorkQueue সংজ্ঞা যোগ করুন:

lib/model.dart

  /// Constructor for [Crossword].
  /// Use [Crossword.crossword] instead.
  factory Crossword([void Function(CrosswordBuilder)? updates]) = _$Crossword;
  Crossword._();
}
                                                           // Add from here
/// A work queue for a worker to process. The work queue contains a crossword
/// and a list of locations to try, along with candidate words to add to the
/// crossword.
abstract class WorkQueue implements Built<WorkQueue, WorkQueueBuilder> {
  static Serializer<WorkQueue> get serializer => _$workQueueSerializer;

  /// The crossword the worker is working on.
  Crossword get crossword;

  /// The outstanding queue of locations to try.
  BuiltMap<Location, Direction> get locationsToTry;

  /// Known bad locations.
  BuiltSet<Location> get badLocations;

  /// The list of unused candidate words that can be added to this crossword.
  BuiltSet<String> get candidateWords;

  /// Returns true if the work queue is complete.
  bool get isCompleted => locationsToTry.isEmpty || candidateWords.isEmpty;

  /// Create a work queue from a crossword.
  static WorkQueue from({
    required Crossword crossword,
    required Iterable<String> candidateWords,
    required Location startLocation,
  }) =>
      WorkQueue((b) {
        if (crossword.words.isEmpty) {
          // Strip candidate words too long to fit in the crossword
          b.candidateWords.addAll(candidateWords
              .where((word) => word.characters.length <= crossword.width));

          b.crossword.replace(crossword);

          b.locationsToTry.addAll({startLocation: Direction.across});
        } else {
          // Assuming words have already been stripped to length
          b.candidateWords.addAll(
            candidateWords.toBuiltSet().rebuild(
                (b) => b.removeAll(crossword.words.map((word) => word.word))),
          );
          b.crossword.replace(crossword);
          crossword.characters
              .rebuild((b) => b.removeWhere((location, character) {
                    if (character.acrossWord != null &&
                        character.downWord != null) {
                      return true;
                    }
                    final left = crossword.characters[location.left];
                    if (left != null && left.downWord != null) return true;
                    final right = crossword.characters[location.right];
                    if (right != null && right.downWord != null) return true;
                    final up = crossword.characters[location.up];
                    if (up != null && up.acrossWord != null) return true;
                    final down = crossword.characters[location.down];
                    if (down != null && down.acrossWord != null) return true;
                    return false;
                  }))
              .forEach((location, character) {
            b.locationsToTry.addAll({
              location: switch ((character.acrossWord, character.downWord)) {
                (null, null) =>
                  throw StateError('Character is not part of a word'),
                (null, _) => Direction.across,
                (_, null) => Direction.down,
                (_, _) => throw StateError('Character is part of two words'),
              }
            });
          });
        }
      });

  WorkQueue remove(Location location) => rebuild((b) => b
    ..locationsToTry.remove(location)
    ..badLocations.add(location));

  /// Update the work queue from a crossword derived from the current crossword
  /// that this work queue is built from.
  WorkQueue updateFrom(final Crossword crossword) => WorkQueue.from(
        crossword: crossword,
        candidateWords: candidateWords,
        startLocation: locationsToTry.isNotEmpty
            ? locationsToTry.keys.first
            : Location.at(0, 0),
      ).rebuild((b) => b
        ..badLocations.addAll(badLocations)
        ..locationsToTry
            .removeWhere((location, _) => badLocations.contains(location)));

  /// Factory constructor for [WorkQueue]
  factory WorkQueue([void Function(WorkQueueBuilder)? updates]) = _$WorkQueue;

  WorkQueue._();
}                                                          // To here.

/// Construct the serialization/deserialization code for the data model.
@SerializersFor([
  Location,
  Crossword,
  CrosswordWord,
  CrosswordCharacter,
  WorkQueue,                                               // Add this line
])
final Serializers serializers = _$serializers;
  1. কয়েক সেকেন্ডের বেশি সময় ধরে এই নতুন বিষয়বস্তু যোগ করার পরে যদি আপনার এই ফাইলে লাল স্কুইগল অবশিষ্ট থাকে, তাহলে নিশ্চিত করুন যে আপনার build_runner এখনও চলছে। যদি না হয়, dart run build_runner watch -d কমান্ড চালান।

কোডটিতে আপনি বিভিন্ন আকারে ক্রসওয়ার্ড তৈরি করতে কতক্ষণ সময় লাগে তা দেখানোর জন্য লগিং চালু করতে চলেছেন। এটি ঝরঝরে হবে যদি Durations-এ সুন্দরভাবে ফরম্যাট করা ডিসপ্লে থাকে। সৌভাগ্যক্রমে, এক্সটেনশন পদ্ধতির সাহায্যে আমরা আমাদের প্রয়োজনীয় সঠিক পদ্ধতি যোগ করতে পারি।

  1. utils.dart ফাইলটি নিম্নরূপ সম্পাদনা করুন:

lib/utils.dart

import 'dart:math';

import 'package:built_collection/built_collection.dart';

/// A [Random] instance for generating random numbers.
final _random = Random();

/// An extension on [BuiltSet] that adds a method to get a random element.
extension RandomElements<E> on BuiltSet<E> {
  E randomElement() {
    return elementAt(_random.nextInt(length));
  }
}
                                                              // Add from here
/// An extension on [Duration] that adds a method to format the duration.
extension DurationFormat on Duration {
  /// A human-readable string representation of the duration.
  /// This format is tuned for durations in the seconds to days range.
  String get formatted {
    final hours = inHours.remainder(24).toString().padLeft(2, '0');
    final minutes = inMinutes.remainder(60).toString().padLeft(2, '0');
    final seconds = inSeconds.remainder(60).toString().padLeft(2, '0');
    return switch ((inDays, inHours, inMinutes, inSeconds)) {
      (0, 0, 0, _) => '${inSeconds}s',
      (0, 0, _, _) => '$inMinutes:$seconds',
      (0, _, _, _) => '$inHours:$minutes:$seconds',
      _ => '$inDays days, $hours:$minutes:$seconds',
    };
  }
}                                                             // To here.

এই এক্সটেনশন পদ্ধতিটি সেকেন্ড থেকে দিন পর্যন্ত বিভিন্ন সময়কাল প্রদর্শনের উপযুক্ত উপায় নির্বাচন করতে রেকর্ডের উপর সুইচ এক্সপ্রেশন এবং প্যাটার্ন ম্যাচিং এর সুবিধা নেয়। কোডের এই শৈলী সম্পর্কে আরও তথ্যের জন্য, ডার্টের প্যাটার্ন এবং রেকর্ড কোডল্যাবের মধ্যে ডুব দেখুন।

  1. এই নতুন কার্যকারিতা একত্রিত করতে, কিভাবে exploreCrosswordSolutions ফাংশনটি নিম্নরূপ সংজ্ঞায়িত করা হয়েছে তা পুনরায় সংজ্ঞায়িত করতে isolates.dart ফাইলটি প্রতিস্থাপন করুন:

lib/isolates.dart

import 'package:built_collection/built_collection.dart';
import 'package:characters/characters.dart';
import 'package:flutter/foundation.dart';

import 'model.dart';
import 'utils.dart';

Stream<Crossword> exploreCrosswordSolutions({
  required Crossword crossword,
  required BuiltSet<String> wordList,
}) async* {
  final start = DateTime.now();
  var workQueue = WorkQueue.from(
    crossword: crossword,
    candidateWords: wordList,
    startLocation: Location.at(0, 0),
  );
  while (!workQueue.isCompleted) {
    final location = workQueue.locationsToTry.keys.toBuiltSet().randomElement();
    try {
      final crossword = await compute(((WorkQueue, Location) workMessage) {
        final (workQueue, location) = workMessage;
        final direction = workQueue.locationsToTry[location]!;
        final target = workQueue.crossword.characters[location];
        if (target == null) {
          return workQueue.crossword.addWord(
            direction: direction,
            location: location,
            word: workQueue.candidateWords.randomElement(),
          );
        }
        var words = workQueue.candidateWords.toBuiltList().rebuild((b) => b
          ..where((b) => b.characters.contains(target.character))
          ..shuffle());
        int tryCount = 0;
        for (final word in words) {
          tryCount++;
          for (final (index, character) in word.characters.indexed) {
            if (character != target.character) continue;

            final candidate = workQueue.crossword.addWord(
              location: switch (direction) {
                Direction.across => location.leftOffset(index),
                Direction.down => location.upOffset(index),
              },
              word: word,
              direction: direction,
            );
            if (candidate != null) {
              return candidate;
            }
          }
          if (tryCount > 1000) {
            break;
          }
        }
      }, (workQueue, location));
      if (crossword != null) {
        workQueue = workQueue.updateFrom(crossword);
        yield crossword;
      } else {
        workQueue = workQueue.remove(location);
      }
    } catch (e) {
      debugPrint('Error running isolate: $e');
    }
  }
  debugPrint('${crossword.width} x ${crossword.height} Crossword generated in '
      '${DateTime.now().difference(start).formatted}');
}

এই কোডটি চালানোর ফলে একটি অ্যাপ তৈরি হবে যা পৃষ্ঠে অভিন্ন দেখায়, তবে পার্থক্য হল একটি সমাপ্ত ক্রসওয়ার্ড ধাঁধা খুঁজে পেতে কতক্ষণ লাগে। এখানে একটি 80 x 44 ক্রসওয়ার্ড ধাঁধা 1 মিনিট এবং 29 সেকেন্ডে তৈরি হয়।

ক্রসওয়ার্ড জেনারেটর, প্রচুর শব্দ ছেদ করে। জুম আউট, শব্দ পড়ার জন্য খুব ছোট.

স্পষ্ট প্রশ্ন অবশ্যই, আমরা কি দ্রুত যেতে পারি? ওহ হ্যাঁ, হ্যাঁ আমরা পারি.

7. সারফেস পরিসংখ্যান

দ্রুত কিছু তৈরি করতে, এটি কী ঘটছে তা দেখতে সহায়তা করে। একটি জিনিস যা এটিতে সহায়তা করে তা হল প্রক্রিয়াটি চলাকালীন সে সম্পর্কে তথ্য প্রকাশ করা। সুতরাং, এখন সময় এসেছে ইন্সট্রুমেন্টেশন যোগ করার এবং সেই তথ্যটিকে একটি হোভারিং ইনফরমেশন প্যানেল হিসাবে প্রদর্শন করার।

আপনি যে তথ্য প্রদর্শন করবেন তা WorkQueue থেকে বের করে UI-তে প্রদর্শন করতে হবে।

একটি দরকারী প্রথম ধাপ হল একটি নতুন মডেল ক্লাস সংজ্ঞায়িত করা যাতে আপনি যে তথ্য প্রদর্শন করতে চান তা রয়েছে।

শুরু করতে, এই পদক্ষেপগুলি অনুসরণ করুন:

  1. DisplayInfo ক্লাস যোগ করতে নিম্নরূপ model.dart ফাইলটি সম্পাদনা করুন:

lib/model.dart

import 'package:built_collection/built_collection.dart';
import 'package:built_value/built_value.dart';
import 'package:built_value/serializer.dart';
import 'package:characters/characters.dart';
import 'package:intl/intl.dart';                           // Add this import

part 'model.g.dart';

/// A location in a crossword.
abstract class Location implements Built<Location, LocationBuilder> {
  1. ফাইলের শেষে, DisplayInfo ক্লাস যোগ করতে নিম্নলিখিত পরিবর্তনগুলি করুন:

lib/model.dart

  /// Factory constructor for [WorkQueue]
  factory WorkQueue([void Function(WorkQueueBuilder)? updates]) = _$WorkQueue;

  WorkQueue._();
}
                                                           // Add from here.
/// Display information for the current state of the crossword solve.
abstract class DisplayInfo implements Built<DisplayInfo, DisplayInfoBuilder> {
  static Serializer<DisplayInfo> get serializer => _$displayInfoSerializer;

  /// The number of words in the grid.
  String get wordsInGridCount;

  /// The number of candidate words.
  String get candidateWordsCount;

  /// The number of locations to explore.
  String get locationsToExploreCount;

  /// The number of known bad locations.
  String get knownBadLocationsCount;

  /// The percentage of the grid filled.
  String get gridFilledPercentage;

  /// Construct a [DisplayInfo] instance from a [WorkQueue].
  factory DisplayInfo.from({required WorkQueue workQueue}) {
    final gridFilled = (workQueue.crossword.characters.length /
        (workQueue.crossword.width * workQueue.crossword.height));
    final fmt = NumberFormat.decimalPattern();

    return DisplayInfo((b) => b
      ..wordsInGridCount = fmt.format(workQueue.crossword.words.length)
      ..candidateWordsCount = fmt.format(workQueue.candidateWords.length)
      ..locationsToExploreCount = fmt.format(workQueue.locationsToTry.length)
      ..knownBadLocationsCount = fmt.format(workQueue.badLocations.length)
      ..gridFilledPercentage = '${(gridFilled * 100).toStringAsFixed(2)}%');
  }

  /// An empty [DisplayInfo] instance.
  static DisplayInfo get empty => DisplayInfo((b) => b
    ..wordsInGridCount = '0'
    ..candidateWordsCount = '0'
    ..locationsToExploreCount = '0'
    ..knownBadLocationsCount = '0'
    ..gridFilledPercentage = '0%');

  factory DisplayInfo([void Function(DisplayInfoBuilder)? updates]) =
      _$DisplayInfo;
  DisplayInfo._();
}                                                          // To here.

/// Construct the serialization/deserialization code for the data model.
@SerializersFor([
  Location,
  Crossword,
  CrosswordWord,
  CrosswordCharacter,
  WorkQueue,
  DisplayInfo,                                             // Add this line.
])
final Serializers serializers = _$serializers;
  1. নিম্নরূপ WorkQueue মডেলটি প্রকাশ করতে isolates.dart ফাইলটি পরিবর্তন করুন:

lib/isolates.dart

import 'package:built_collection/built_collection.dart';
import 'package:characters/characters.dart';
import 'package:flutter/foundation.dart';

import 'model.dart';
import 'utils.dart';

Stream<WorkQueue> exploreCrosswordSolutions({              // Modify this line
  required Crossword crossword,
  required BuiltSet<String> wordList,
}) async* {
  final start = DateTime.now();
  var workQueue = WorkQueue.from(
    crossword: crossword,
    candidateWords: wordList,
    startLocation: Location.at(0, 0),
  );
  while (!workQueue.isCompleted) {
    final location = workQueue.locationsToTry.keys.toBuiltSet().randomElement();
    try {
      final crossword = await compute(((WorkQueue, Location) workMessage) {
        final (workQueue, location) = workMessage;
        final direction = workQueue.locationsToTry[location]!;
        final target = workQueue.crossword.characters[location];
        if (target == null) {
          return workQueue.crossword.addWord(
            direction: direction,
            location: location,
            word: workQueue.candidateWords.randomElement(),
          );
        }
        var words = workQueue.candidateWords.toBuiltList().rebuild((b) => b
          ..where((b) => b.characters.contains(target.character))
          ..shuffle());
        int tryCount = 0;
        for (final word in words) {
          tryCount++;
          for (final (index, character) in word.characters.indexed) {
            if (character != target.character) continue;

            final candidate = workQueue.crossword.addWord(
              location: switch (direction) {
                Direction.across => location.leftOffset(index),
                Direction.down => location.upOffset(index),
              },
              word: word,
              direction: direction,
            );
            if (candidate != null) {
              return candidate;
            }
          }
          if (tryCount > 1000) {
            break;
          }
        }
      }, (workQueue, location));
      if (crossword != null) {
        workQueue = workQueue.updateFrom(crossword);       // Drop the yield crossword;
      } else {
        workQueue = workQueue.remove(location);
      }
      yield workQueue;                                     // Add this line.
    } catch (e) {
      debugPrint('Error running isolate: $e');
    }
  }
  debugPrint('${crossword.width} x ${crossword.height} Crossword generated in '
      '${DateTime.now().difference(start).formatted}');
}

এখন যেহেতু ব্যাকগ্রাউন্ড আইসোলেট কাজের সারিতে উন্মোচিত হচ্ছে, এখন এই ডেটা উৎস থেকে পরিসংখ্যান কিভাবে এবং কোথা থেকে সংগ্রহ করা যায় তা এখন একটি প্রশ্ন।

  1. একটি কাজের সারি প্রদানকারীর সাথে পুরানো ক্রসওয়ার্ড প্রদানকারীকে প্রতিস্থাপন করুন এবং তারপরে আরও প্রদানকারী যোগ করুন যেগুলি কাজের সারি প্রদানকারীর স্ট্রীম থেকে তথ্য সংগ্রহ করে:

lib/providers.dart

import 'dart:convert';
import 'dart:math';                                        // Add this import

import 'package:built_collection/built_collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

import 'isolates.dart';
import 'model.dart' as model;

part 'providers.g.dart';

/// A provider for the wordlist to use when generating the crossword.
@riverpod
Future<BuiltSet<String>> wordList(WordListRef ref) async {
  // This codebase requires that all words consist of lowercase characters
  // in the range 'a'-'z'. Words containing uppercase letters will be
  // lowercased, and words containing runes outside this range will
  // be removed.

  final re = RegExp(r'^[a-z]+$');
  final words = await rootBundle.loadString('assets/words.txt');
  return const LineSplitter().convert(words).toBuiltSet().rebuild((b) => b
    ..map((word) => word.toLowerCase().trim())
    ..where((word) => word.length > 2)
    ..where((word) => re.hasMatch(word)));
}

/// An enumeration for different sizes of [model.Crossword]s.
enum CrosswordSize {
  small(width: 20, height: 11),
  medium(width: 40, height: 22),
  large(width: 80, height: 44),
  xlarge(width: 160, height: 88),
  xxlarge(width: 500, height: 500);

  const CrosswordSize({
    required this.width,
    required this.height,
  });

  final int width;
  final int height;
  String get label => '$width x $height';
}

/// A provider that holds the current size of the crossword to generate.
@Riverpod(keepAlive: true)
class Size extends _$Size {
  var _size = CrosswordSize.medium;

  @override
  CrosswordSize build() => _size;

  void setSize(CrosswordSize size) {
    _size = size;
    ref.invalidateSelf();
  }
}

@riverpod
Stream<model.WorkQueue> workQueue(WorkQueueRef ref) async* { // Modify this provider
  final size = ref.watch(sizeProvider);
  final wordListAsync = ref.watch(wordListProvider);
  final emptyCrossword =
      model.Crossword.crossword(width: size.width, height: size.height);
  final emptyWorkQueue = model.WorkQueue.from(
    crossword: emptyCrossword,
    candidateWords: BuiltSet<String>(),
    startLocation: model.Location.at(0, 0),
  );

  ref.read(startTimeProvider.notifier).start();
  ref.read(endTimeProvider.notifier).clear();

  yield* wordListAsync.when(
    data: (wordList) => exploreCrosswordSolutions(
      crossword: emptyCrossword,
      wordList: wordList,
    ),
    error: (error, stackTrace) async* {
      debugPrint('Error loading word list: $error');
      yield emptyWorkQueue;
    },
    loading: () async* {
      yield emptyWorkQueue;
    },
  );

  ref.read(endTimeProvider.notifier).end();
}                                                          // To here.

@Riverpod(keepAlive: true)                                 // Add from here to end of file
class StartTime extends _$StartTime {
  @override
  DateTime? build() => _start;

  DateTime? _start;

  void start() {
    _start = DateTime.now();
    ref.invalidateSelf();
  }
}

@Riverpod(keepAlive: true)
class EndTime extends _$EndTime {
  @override
  DateTime? build() => _end;

  DateTime? _end;

  void clear() {
    _end = null;
    ref.invalidateSelf();
  }

  void end() {
    _end = DateTime.now();
    ref.invalidateSelf();
  }
}

const _estimatedTotalCoverage = 0.54;

@riverpod
Duration expectedRemainingTime(ExpectedRemainingTimeRef ref) {
  final startTime = ref.watch(startTimeProvider);
  final endTime = ref.watch(endTimeProvider);
  final workQueueAsync = ref.watch(workQueueProvider);

  return workQueueAsync.when(
    data: (workQueue) {
      if (startTime == null || endTime != null || workQueue.isCompleted) {
        return Duration.zero;
      }
      try {
        final soFar = DateTime.now().difference(startTime);
        final completedPercentage = min(
            0.99,
            (workQueue.crossword.characters.length /
                (workQueue.crossword.width * workQueue.crossword.height) /
                _estimatedTotalCoverage));
        final expectedTotal = soFar.inSeconds / completedPercentage;
        final expectedRemaining = expectedTotal - soFar.inSeconds;
        return Duration(seconds: expectedRemaining.toInt());
      } catch (e) {
        return Duration.zero;
      }
    },
    error: (error, stackTrace) => Duration.zero,
    loading: () => Duration.zero,
  );
}

/// A provider that holds whether to display info.
@Riverpod(keepAlive: true)
class ShowDisplayInfo extends _$ShowDisplayInfo {
  var _display = true;

  @override
  bool build() => _display;

  void toggle() {
    _display = !_display;
    ref.invalidateSelf();
  }
}

/// A provider that summarise the DisplayInfo from a [model.WorkQueue].
@riverpod
class DisplayInfo extends _$DisplayInfo {
  @override
  model.DisplayInfo build() => ref.watch(workQueueProvider).when(
        data: (workQueue) => model.DisplayInfo.from(workQueue: workQueue),
        error: (error, stackTrace) => model.DisplayInfo.empty,
        loading: () => model.DisplayInfo.empty,
      );
}

নতুন প্রদানকারীরা বৈশ্বিক অবস্থার মিশ্রণ, তথ্য প্রদর্শনটি ক্রসওয়ার্ড গ্রিডের উপরে ওভারলেড করা উচিত কিনা এবং ক্রসওয়ার্ড প্রজন্মের চলমান সময়ের মতো ডেটা প্রাপ্ত করা। এই সমস্ত কিছু এই রাজ্যের শ্রোতারা ক্ষণস্থায়ী হওয়ার কারণে জটিল। তথ্য প্রদর্শন লুকানো থাকলে ক্রসওয়ার্ড কম্পিউটেশনের শুরু এবং শেষের সময় কিছুই শুনছে না, তবে তথ্য প্রদর্শন দেখানোর সময় গণনাটি সঠিক হতে হলে তাদের মেমরিতে থাকতে হবে। Riverpod অ্যাট্রিবিউটের keepAlive প্যারামিটার এই ক্ষেত্রে খুব দরকারী।

তথ্য প্রদর্শন দেখানোর মধ্যে, একটি সামান্য বলি আছে. আমরা বর্তমানে অতিবাহিত রান টাইম দেখানোর ক্ষমতা চাই, কিন্তু বর্তমানে অতিবাহিত সময়ের ধ্রুবক আপডেট সহজে জোর করার জন্য এখানে কিছুই নেই। Flutter codelab-এ বিল্ডিং পরবর্তী প্রজন্মের UI- তে ফিরে যাওয়া, এখানে এই প্রয়োজনীয়তার জন্য একটি দরকারী উইজেট রয়েছে।

  1. lib/widgets ডিরেক্টরিতে একটি ticker_builder.dart ফাইল তৈরি করুন এবং তারপরে নিম্নলিখিত বিষয়বস্তু যোগ করুন:

lib/widgets/ticker_builder.dart

import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';

/// A Builder widget that invokes its [builder] function on every animation frame.
class TickerBuilder extends StatefulWidget {
  const TickerBuilder({super.key, required this.builder});
  final Widget Function(BuildContext context) builder;
  @override
  State<TickerBuilder> createState() => _TickerBuilderState();
}

class _TickerBuilderState extends State<TickerBuilder>
    with SingleTickerProviderStateMixin {
  late final Ticker _ticker;

  @override
  void initState() {
    super.initState();
    _ticker = createTicker(_handleTick)..start();
  }

  @override
  void dispose() {
    _ticker.dispose();
    super.dispose();
  }

  void _handleTick(Duration elapsed) {
    setState(() {
      // Force a rebuild without changing the widget tree.
    });
  }

  @override
  Widget build(BuildContext context) => widget.builder.call(context);
}

এই উইজেটটি একটি স্লেজহ্যামার। এটি প্রতিটি ফ্রেমে এর বিষয়বস্তু পুনর্নির্মাণ করে। এটি সাধারণত ভ্রুকুটি করা হয়, তবে ক্রসওয়ার্ড পাজলগুলি অনুসন্ধানের গণনামূলক লোডের তুলনায়, প্রতিটি ফ্রেমের অতিবাহিত সময় পুনরায় রঙ করার গণনামূলক লোড সম্ভবত গোলমালের মধ্যে অদৃশ্য হয়ে যাবে। এই নতুন প্রাপ্ত তথ্যের সুবিধা নিতে, এটি একটি নতুন উইজেট তৈরি করার সময়।

  1. আপনার lib/widgets ডিরেক্টরিতে একটি crossword_info_widget.dart ফাইল তৈরি করুন এবং তারপরে নিম্নলিখিত বিষয়বস্তু যুক্ত করুন:

lib/widgets/crossword_info_widget.dart

class CrosswordInfoWidget extends ConsumerWidget {
  const CrosswordInfoWidget({
    super.key,
  });

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final size = ref.watch(sizeProvider);
    final displayInfo = ref.watch(displayInfoProvider);
    final startTime = ref.watch(startTimeProvider);
    final endTime = ref.watch(endTimeProvider);
    final remaining = ref.watch(expectedRemainingTimeProvider);

    return Align(
      alignment: Alignment.bottomRight,
      child: Padding(
        padding: const EdgeInsets.only(
          right: 32.0,
          bottom: 32.0,
        ),
        child: ClipRRect(
          borderRadius: BorderRadius.circular(8),
          child: ColoredBox(
            color: Theme.of(context).colorScheme.onPrimary.withAlpha(230),
            child: Padding(
              padding: const EdgeInsets.symmetric(
                horizontal: 12,
                vertical: 8,
              ),
              child: DefaultTextStyle(
                style: TextStyle(
                    fontSize: 16, color: Theme.of(context).colorScheme.primary),
                child: Column(
                  mainAxisSize: MainAxisSize.min,
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    _CrosswordInfoRichText(
                        label: 'Grid Size',
                        value: '${size.width} x ${size.height}'),
                    _CrosswordInfoRichText(
                        label: 'Words in grid',
                        value: displayInfo.wordsInGridCount),
                    _CrosswordInfoRichText(
                        label: 'Candidate words',
                        value: displayInfo.candidateWordsCount),
                    _CrosswordInfoRichText(
                        label: 'Locations to explore',
                        value: displayInfo.locationsToExploreCount),
                    _CrosswordInfoRichText(
                        label: 'Known bad locations',
                        value: displayInfo.knownBadLocationsCount),
                    _CrosswordInfoRichText(
                        label: 'Grid filled',
                        value: displayInfo.gridFilledPercentage),
                    switch ((startTime, endTime)) {
                      (null, _) => _CrosswordInfoRichText(
                          label: 'Time elapsed',
                          value: 'Not started yet',
                        ),
                      (DateTime start, null) => TickerBuilder(
                          builder: (context) => _CrosswordInfoRichText(
                            label: 'Time elapsed',
                            value: DateTime.now().difference(start).formatted,
                          ),
                        ),
                      (DateTime start, DateTime end) => _CrosswordInfoRichText(
                          label: 'Completed in',
                          value: end.difference(start).formatted),
                    },
                    if (startTime != null && endTime == null)
                      _CrosswordInfoRichText(
                          label: 'Est. remaining', value: remaining.formatted),
                  ],
                ),
              ),
            ),
          ),
        ),
      ),
    );
  }
}

class _CrosswordInfoRichText extends StatelessWidget {
  final String label;
  final String value;

  const _CrosswordInfoRichText({required this.label, required this.value});

  @override
  Widget build(BuildContext context) => RichText(
        text: TextSpan(
          children: [
            TextSpan(
              text: '$label ',
              style: DefaultTextStyle.of(context).style,
            ),
            TextSpan(
              text: value,
              style: DefaultTextStyle.of(context)
                  .style
                  .copyWith(fontWeight: FontWeight.bold),
            ),
          ],
        ),
      );
}

এই উইজেটটি রিভারপড প্রদানকারীদের ক্ষমতার একটি প্রধান উদাহরণ। এই উইজেটটি পুনর্নির্মাণের জন্য চিহ্নিত করা হবে যখন পাঁচটি প্রদানকারীর যেকোনো একটি আপডেট হবে। এই ধাপে সর্বশেষ প্রয়োজনীয় পরিবর্তনটি হল এই নতুন উইজেটটিকে UI-তে সংহত করা।

  1. আপনার crossword_generator_app.dart ফাইলটি নিম্নরূপ সম্পাদনা করুন:

lib/widgets/crossword_generator_app.dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import '../providers.dart';
import 'crossword_info_widget.dart';                       // Add this import
import 'crossword_widget.dart';

class CrosswordGeneratorApp extends StatelessWidget {
  const CrosswordGeneratorApp({super.key});

  @override
  Widget build(BuildContext context) {
    return _EagerInitialization(
      child: Scaffold(
        appBar: AppBar(
          actions: [_CrosswordGeneratorMenu()],
          titleTextStyle: TextStyle(
            color: Theme.of(context).colorScheme.primary,
            fontSize: 16,
            fontWeight: FontWeight.bold,
          ),
          title: Text('Crossword Generator'),
        ),
        body: SafeArea(
          child: Consumer(                                 // Modify from here
            builder: (context, ref, child) {
              return Stack(
                children: [
                  Positioned.fill(
                    child: CrosswordWidget(),
                  ),
                  if (ref.watch(showDisplayInfoProvider)) CrosswordInfoWidget(),
                ],
              );
            },
          ),                                               // To here.
        ),
      ),
    );
  }
}

class _EagerInitialization extends ConsumerWidget {
  const _EagerInitialization({required this.child});
  final Widget child;

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    ref.watch(wordListProvider);
    return child;
  }
}

class _CrosswordGeneratorMenu extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) => MenuAnchor(
        menu Children: [
          for (final entry in CrosswordSize.values)
            MenuItemButton(
              onPressed: () => ref.read(sizeProvider.notifier).setSize(entry),
              leadingIcon: entry == ref.watch(sizeProvider)
                  ? Icon(Icons.radio_button_checked_outlined)
                  : Icon(Icons.radio_button_unchecked_outlined),
              child: Text(entry.label),
            ),
          MenuItemButton(                                  // Add from here
            leadingIcon: ref.watch(showDisplayInfoProvider)
                ? Icon(Icons.check_box_outlined)
                : Icon(Icons.check_box_outline_blank_outlined),
            onPressed: () =>
                ref.read(showDisplayInfoProvider.notifier).toggle(),
            child: Text('Display Info'),
          ),                                               // To here.
        ],
        builder: (context, controller, child) => IconButton(
          onPressed: () => controller.open(),
          icon: Icon(Icons.settings),
        ),
      );
}

এখানে দুটি পরিবর্তন প্রদানকারীদের একীভূত করার জন্য বিভিন্ন পদ্ধতির প্রদর্শন করে। CrosswordGeneratorApp এর build পদ্ধতিতে, তথ্য প্রদর্শন দেখানো বা লুকানো অবস্থায় পুনর্নির্মাণ করতে বাধ্য করা এলাকা ধারণ করার জন্য আপনি একটি নতুন Consumer বিল্ডার প্রবর্তন করেছেন। অন্যদিকে, পুরো ড্রপ-ডাউন মেনুটি হল একটি ConsumerWidget , যা ক্রসওয়ার্ডের আকার পরিবর্তন করা হোক বা তথ্য প্রদর্শন দেখানো বা লুকানো হোক না কেন তা পুনর্নির্মাণ করা হবে। কোন পন্থা অবলম্বন করা সর্বদা সরলতার একটি ইঞ্জিনিয়ারিং ট্রেডঅফ বনাম পুনর্নির্মিত উইজেট গাছের বিন্যাস পুনঃগণনা করার খরচ।

এখন অ্যাপটি চালানোর ফলে ব্যবহারকারীকে ক্রসওয়ার্ড জেনারেশন কীভাবে এগিয়ে চলেছে সে সম্পর্কে আরও অন্তর্দৃষ্টি দেয়। যাইহোক, ক্রসওয়ার্ড প্রজন্মের শেষের কাছাকাছি আমরা দেখতে পাই এমন একটি সময় আছে যেখানে সংখ্যা পরিবর্তন হচ্ছে, কিন্তু অক্ষরের গ্রিডে খুব সামান্য পরিবর্তন আছে।

ক্রসওয়ার্ড জেনারেটর অ্যাপ উইন্ডো, এবার ছোট, শনাক্তযোগ্য শব্দ, এবং বর্তমান প্রজন্মের রানের পরিসংখ্যান সহ নীচের ডানদিকে একটি ভাসমান ওভারলে

কী ঘটছে এবং কেন হচ্ছে সে সম্পর্কে অতিরিক্ত অন্তর্দৃষ্টি পেতে এটি কার্যকর হবে।

8. থ্রেড সঙ্গে সমান্তরাল

কেন জিনিসগুলি শেষের দিকে ধীর হয়ে যায় তা বোঝার জন্য, অ্যালগরিদম কী করছে তা কল্পনা করতে সক্ষম হওয়া কার্যকর। একটি মূল অংশ হল অসামান্য locationsToTry WorkQueue চেষ্টা করুন৷ টেবিলভিউ আমাদের এটি তদন্ত করার একটি দরকারী উপায় দেয়। আমরা ঘরের রঙ পরিবর্তন করতে পারি এটি locationsToTry আছে কিনা তার উপর ভিত্তি করেToTry.

শুরু করতে, এই পদক্ষেপগুলি অনুসরণ করুন:

  1. crossword_widget.dart ফাইলটি নিম্নরূপ পরিবর্তন করুন:

lib/widgets/crossword_widget.dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:two_dimensional_scrollables/two_dimensional_scrollables.dart';

import '../model.dart';
import '../providers.dart';

class CrosswordWidget extends ConsumerWidget {
  const CrosswordWidget({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final size = ref.watch(sizeProvider);
    return TableView.builder(
      diagonalDragBehavior: DiagonalDragBehavior.free,
      cellBuilder: _buildCell,
      columnCount: size.width,
      columnBuilder: (index) => _buildSpan(context, index),
      rowCount: size.height,
      rowBuilder: (index) => _buildSpan(context, index),
    );
  }

  TableViewCell _buildCell(BuildContext context, TableVicinity vicinity) {
    final location = Location.at(vicinity.column, vicinity.row);

    return TableViewCell(
      child: Consumer(
        builder: (context, ref, _) {
          final character = ref.watch(
            workQueueProvider.select(
              (workQueueAsync) => workQueueAsync.when(
                data: (workQueue) => workQueue.crossword.characters[location],
                error: (error, stackTrace) => null,
                loading: () => null,
              ),
            ),
          );

          final explorationCell = ref.watch(               // Add from here
            workQueueProvider.select(
              (workQueueAsync) => workQueueAsync.when(
                data: (workQueue) =>
                    workQueue.locationsToTry.keys.contains(location),
                error: (error, stackTrace) => false,
                loading: () => false,
              ),
            ),
          );                                               // To here.

          if (character != null) {                         // Modify from here
            return AnimatedContainer(
              duration: Durations.extralong1,
              curve: Curves.easeInOut,
              color: explorationCell
                  ? Theme.of(context).colorScheme.primary
                  : Theme.of(context).colorScheme.onPrimary,
              child: Center(
                child: AnimatedDefaultTextStyle(
                  duration: Durations.extralong1,
                  curve: Curves.easeInOut,
                  style: TextStyle(
                    fontSize: 24,
                    color: explorationCell
                        ? Theme.of(context).colorScheme.onPrimary
                        : Theme.of(context).colorScheme.primary,
                  ),
                  child: Text(character.character),
                ),                                          // To here.
              ),
            );
          }

          return ColoredBox(
            color: Theme.of(context).colorScheme.primaryContainer,
          );
        },
      ),
    );
  }

  TableSpan _buildSpan(BuildContext context, int index) {
    return TableSpan(
      extent: FixedTableSpanExtent(32),
      foregroundDecoration: TableSpanDecoration(
        border: TableSpanBorder(
          leading: BorderSide(
              color: Theme.of(context).colorScheme.onPrimaryContainer),
          trailing: BorderSide(
              color: Theme.of(context).colorScheme.onPrimaryContainer),
        ),
      ),
    );
  }
}

আপনি যখন এই কোডটি চালাবেন, তখন আপনি অসামান্য অবস্থানগুলির একটি ভিজ্যুয়ালাইজেশন দেখতে পাবেন যেগুলি অ্যালগরিদম এখনও তদন্ত করতে পারেনি৷

ক্রসওয়ার্ড জেনারেটর প্রজন্মের অংশ পথ দেখাচ্ছে. কিছু অক্ষরের গাঢ় নীল পটভূমিতে সাদা টেক্সট থাকে, অন্যগুলো সাদা পটভূমিতে নীল টেক্সট থাকে।

ক্রসওয়ার্ডটি সমাপ্তির দিকে অগ্রসর হওয়ার সাথে সাথে এটি দেখার মধ্যে আকর্ষণীয় জিনিসটি হ'ল তদন্ত করা বাকি রয়েছে যা কিছু কার্যকর হবে না। এখানে বিকল্প একটি দম্পতি আছে; একটি হল ক্রসওয়ার্ড সেলগুলির একটি নির্দিষ্ট শতাংশ পূরণ হয়ে গেলে তদন্তকে ক্যাপ করা এবং দ্বিতীয়টি হল একবারে একাধিক আগ্রহের বিষয়গুলি তদন্ত করা। দ্বিতীয় পথটি আরও মজার মনে হচ্ছে, তাই আসুন এটি করি।

  1. isolates.dart ফাইলটি সম্পাদনা করুন। এক ব্যাকগ্রাউন্ড আইসোলেটে যা গণনা করা হয়েছিল তা N ব্যাকগ্রাউন্ড আইসোলেটের পুলে বিভক্ত করার জন্য এটি কোডটির প্রায় সম্পূর্ণ পুনর্লিখন।

lib/isolates.dart

import 'package:built_collection/built_collection.dart';
import 'package:characters/characters.dart';
import 'package:flutter/foundation.dart';

import 'model.dart';
import 'utils.dart';

Stream<WorkQueue> exploreCrosswordSolutions({
  required Crossword crossword,
  required BuiltSet<String> wordList,
  required int maxWorkerCount,
}) async* {
  final start = DateTime.now();
  var workQueue = WorkQueue.from(
    crossword: crossword,
    candidateWords: wordList,
    startLocation: Location.at(0, 0),
  );
  while (!workQueue.isCompleted) {
    try {
      workQueue = await compute(_generate, (workQueue, maxWorkerCount));
      yield workQueue;
    } catch (e) {
      debugPrint('Error running isolate: $e');
    }
  }

  debugPrint('Generated ${workQueue.crossword.width} x '
      '${workQueue.crossword.height} crossword in '
      '${DateTime.now().difference(start).formatted} '
      'with $maxWorkerCount workers.');
}

Future<WorkQueue> _generate((WorkQueue, int) workMessage) async {
  var (workQueue, maxWorkerCount) = workMessage;
  final candidateGeneratorFutures = <Future<(Location, Direction, String?)>>[];
  final locations = workQueue.locationsToTry.keys.toBuiltList().rebuild((b) => b
    ..shuffle()
    ..take(maxWorkerCount));

  for (final location in locations) {
    final direction = workQueue.locationsToTry[location]!;

    candidateGeneratorFutures.add(compute(_generateCandidate,
        (workQueue.crossword, workQueue.candidateWords, location, direction)));
  }

  try {
    final results = await candidateGeneratorFutures.wait;
    var crossword = workQueue.crossword;
    for (final (location, direction, word) in results) {
      if (word != null) {
        final candidate = crossword.addWord(
            location: location, word: word, direction: direction);
        if (candidate != null) {
          crossword = candidate;
        }
      } else {
        workQueue = workQueue.remove(location);
      }
    }

    workQueue = workQueue.updateFrom(crossword);
  } catch (e) {
    debugPrint('$e');
  }

  return workQueue;
}

(Location, Direction, String?) _generateCandidate(
    (Crossword, BuiltSet<String>, Location, Direction) searchDetailMessage) {
  final (crossword, candidateWords, location, direction) = searchDetailMessage;

  final target = crossword.characters[location];
  if (target == null) {
    return (location, direction, candidateWords.randomElement());
  }

  // Filter down the candidate word list to those that contain the letter
  // at the current location
  final words = candidateWords.toBuiltList().rebuild((b) => b
    ..where((b) => b.characters.contains(target.character))
    ..shuffle());
  int tryCount = 0;
  final start = DateTime.now();
  for (final word in words) {
    tryCount++;
    for (final (index, character) in word.characters.indexed) {
      if (character != target.character) continue;

      final candidate = crossword.addWord(
        location: switch (direction) {
          Direction.across => location.leftOffset(index),
          Direction.down => location.upOffset(index),
        },
        word: word,
        direction: direction,
      );
      if (candidate != null) {
        return switch (direction) {
          Direction.across => (location.leftOffset(index), direction, word),
          Direction.down => (location.upOffset(index), direction, word),
        };
      }
      final deltaTime = DateTime.now().difference(start);
      if (tryCount >= 1000 || deltaTime > Duration(seconds: 10)) {
        return (location, direction, null);
      }
    }
  }

  return (location, direction, null);
}

এই কোডের বেশিরভাগই পরিচিত হওয়া উচিত কারণ মূল ব্যবসার যুক্তি পরিবর্তিত হয়নি। কী পরিবর্তন হয়েছে তা হল এখন compute কলের দুটি স্তর রয়েছে৷ প্রথম স্তরটি N কর্মী আইসোলেটগুলি অনুসন্ধান করার জন্য পৃথক অবস্থান তৈরি করার জন্য দায়ী, এবং তারপর সমস্ত N কর্মী বিচ্ছিন্ন হয়ে গেলে ফলাফলগুলি পুনরায় একত্রিত করার জন্য। দ্বিতীয় স্তরটি এন ওয়ার্কার আইসোলেট নিয়ে গঠিত। সেরা পারফরম্যান্স পেতে N টিউন করা আপনার কম্পিউটার এবং প্রশ্নে থাকা ডেটা উভয়ের উপর নির্ভরশীল। গ্রিড যত বড় হবে, তত বেশি শ্রমিক একে অপরের পথে না গিয়ে একসঙ্গে কাজ করতে পারবে।

একটি আকর্ষণীয় বলি হল যে এই কোডটি এখন যে জিনিসগুলি ক্যাপচার করা উচিত নয় তা ক্যাপচার করার ক্লোজারের সমস্যাটি কীভাবে পরিচালনা করে। এখন কোন বন্ধ আছে. _generate এবং _generateWorker ফাংশনগুলিকে শীর্ষ-স্তরের ফাংশন হিসাবে সংজ্ঞায়িত করা হয়েছে, যেগুলির থেকে ক্যাপচার করার জন্য কোনও পার্শ্ববর্তী পরিবেশ নেই। এই উভয় ফাংশনের মধ্যে আর্গুমেন্ট এবং ফলাফল ডার্ট রেকর্ডের আকারে। compute কলের ওয়ান ভ্যালু ইন, ওয়ান ভ্যালু আউট সিমেন্টিকসকে ঘিরে কাজ করার এটি একটি সহজ উপায়।

এখন আপনার কাছে পটভূমি কর্মীদের একটি পুল তৈরি করার ক্ষমতা রয়েছে যা একটি ক্রসওয়ার্ড ধাঁধা তৈরি করার জন্য একটি গ্রিডে ইন্টারলক করে এমন শব্দগুলি অনুসন্ধান করার জন্য, এখন সেই ক্ষমতাটি ক্রসওয়ার্ড জেনারেটর টুলের বাকি অংশে প্রকাশ করার সময়।

  1. নিম্নরূপ workQueue প্রদানকারী সম্পাদনা করে providers.dart ফাইল সম্পাদনা করুন:

lib/providers.dart

@riverpod
Stream<model.WorkQueue> workQueue(WorkQueueRef ref) async* {
  final workers = ref.watch(workerCountProvider);          // Add this line
  final size = ref.watch(sizeProvider);
  final wordListAsync = ref.watch(wordListProvider);
  final emptyCrossword =
      model.Crossword.crossword(width: size.width, height: size.height);
  final emptyWorkQueue = model.WorkQueue.from(
    crossword: emptyCrossword,
    candidateWords: BuiltSet<String>(),
    startLocation: model.Location.at(0, 0),
  );

  ref.read(startTimeProvider.notifier).start();
  ref.read(endTimeProvider.notifier).clear();

  yield* wordListAsync.when(
    data: (wordList) => exploreCrosswordSolutions(
      crossword: emptyCrossword,
      wordList: wordList,
      maxWorkerCount: workers.count,                       // Add this line
    ),
    error: (error, stackTrace) async* {
      debugPrint('Error loading word list: $error');
      yield emptyWorkQueue;
    },
    loading: () async* {
      yield emptyWorkQueue;
    },
  );

  ref.read(endTimeProvider.notifier).end();
}
  1. নিম্নরূপ ফাইলের শেষে WorkerCount প্রদানকারী যোগ করুন:

lib/providers.dart

/// A provider that summarise the DisplayInfo from a [model.WorkQueue].
@riverpod
class DisplayInfo extends _$DisplayInfo {
  @override
  model.DisplayInfo build() => ref.watch(workQueueProvider).when(
        data: (workQueue) => model.DisplayInfo.from(workQueue: workQueue),
        error: (error, stackTrace) => model.DisplayInfo.empty,
        loading: () => model.DisplayInfo.empty,
      );
}

enum BackgroundWorkers {                                   // Add from here 
  one(1),
  two(2),
  four(4),
  eight(8),
  sixteen(16),
  thirtyTwo(32),
  sixtyFour(64),
  oneTwentyEight(128);

  const BackgroundWorkers(this.count);

  final int count;
  String get label => count.toString();
}

/// A provider that holds the current number of background workers to use.
@Riverpod(keepAlive: true)
class WorkerCount extends _$WorkerCount {
  var _count = BackgroundWorkers.four;

  @override
  BackgroundWorkers build() => _count;

  void setCount(BackgroundWorkers count) {
    _count = count;
    ref.invalidateSelf();
  }
}                                                          // To here.

এই দুটি পরিবর্তনের সাথে, প্রদানকারী স্তরটি এখন ব্যাকগ্রাউন্ড আইসোলেট পুলের জন্য সর্বাধিক কর্মী সংখ্যা সেট করার একটি উপায় প্রকাশ করে যাতে আইসোলেট ফাংশনগুলি সঠিকভাবে কনফিগার করা হয়।

  1. crossword_info_widget.dart ফাইলটি নিম্নরূপ CrosswordInfoWidget পরিবর্তন করে আপডেট করুন:

lib/widgets/crossword_info_widget.dart

class CrosswordInfoWidget extends ConsumerWidget {
  const CrosswordInfoWidget({
    super.key,
  });

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final size = ref.watch(sizeProvider);
    final displayInfo = ref.watch(displayInfoProvider);
    final workerCount = ref.watch(workerCountProvider).label;  // Add this line
    final startTime = ref.watch(startTimeProvider);
    final endTime = ref.watch(endTimeProvider);
    final remaining = ref.watch(expectedRemainingTimeProvider);

    return Align(
      alignment: Alignment.bottomRight,
      child: Padding(
        padding: const EdgeInsets.only(
          right: 32.0,
          bottom: 32.0,
        ),
        child: ClipRRect(
          borderRadius: BorderRadius.circular(8),
          child: ColoredBox(
            color: Theme.of(context).colorScheme.onPrimary.withAlpha(230),
            child: Padding(
              padding: const EdgeInsets.symmetric(
                horizontal: 12,
                vertical: 8,
              ),
              child: DefaultTextStyle(
                style: TextStyle(
                    fontSize: 16, color: Theme.of(context).colorScheme.primary),
                child: Column(
                  mainAxisSize: MainAxisSize.min,
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    _CrosswordInfoRichText(
                        label: 'Grid Size',
                        value: '${size.width} x ${size.height}'),
                    _CrosswordInfoRichText(
                        label: 'Words in grid',
                        value: displayInfo.wordsInGridCount),
                    _CrosswordInfoRichText(
                        label: 'Candidate words',
                        value: displayInfo.candidateWordsCount),
                    _CrosswordInfoRichText(
                        label: 'Locations to explore',
                        value: displayInfo.locationsToExploreCount),
                    _CrosswordInfoRichText(
                        label: 'Known bad locations',
                        value: displayInfo.knownBadLocationsCount),
                    _CrosswordInfoRichText(
                        label: 'Grid filled',
                        value: displayInfo.gridFilledPercentage),
                    _CrosswordInfoRichText(               // Add these two lines
                        label: 'Max worker count', value: workerCount),
                    switch ((startTime, endTime)) {
                      (null, _) => _CrosswordInfoRichText(
                          label: 'Time elapsed',
                          value: 'Not started yet',
                        ),
                      (DateTime start, null) => TickerBuilder(
                          builder: (context) => _CrosswordInfoRichText(
                            label: 'Time elapsed',
                            value: DateTime.now().difference(start).formatted,
                          ),
                        ),
                      (DateTime start, DateTime end) => _CrosswordInfoRichText(
                          label: 'Completed in',
                          value: end.difference(start).formatted),
                    },
                    if (startTime != null && endTime == null)
                      _CrosswordInfoRichText(
                          label: 'Est. remaining', value: remaining.formatted),
                  ],
                ),
              ),
            ),
          ),
        ),
      ),
    );
  }
}
  1. _CrosswordGeneratorMenu উইজেটে নিম্নলিখিত বিভাগটি যোগ করে crossword_generator_app.dart ফাইলটি পরিবর্তন করুন:

lib/widgets/crossword_generator_app.dart

class _CrosswordGeneratorMenu extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) => MenuAnchor(
        menuChildren: [
          for (final entry in CrosswordSize.values)
            MenuItemButton(
              onPressed: () => ref.read(sizeProvider.notifier).setSize(entry),
              leadingIcon: entry == ref.watch(sizeProvider)
                  ? Icon(Icons.radio_button_checked_outlined)
                  : Icon(Icons.radio_button_unchecked_outlined),
              child: Text(entry.label),
            ),
          MenuItemButton(
            leadingIcon: ref.watch(showDisplayInfoProvider)
                ? Icon(Icons.check_box_outlined)
                : Icon(Icons.check_box_outline_blank_outlined),
            onPressed: () =>
                ref.read(showDisplayInfoProvider.notifier).toggle(),
            child: Text('Display Info'),
          ),
          for (final count in BackgroundWorkers.values)    // Add from here
            MenuItemButton(
              leadingIcon: count == ref.watch(workerCountProvider)
                  ? Icon(Icons.radio_button_checked_outlined)
                  : Icon(Icons.radio_button_unchecked_outlined),
              onPressed: () =>
                  ref.read(workerCountProvider.notifier).setCount(count),
              child: Text(count.label),                    // To here.
            ),
        ],
        builder: (context, controller, child) => IconButton(
          onPressed: () => controller.open(),
          icon: Icon(Icons.settings),
        ),
      );
}

আপনি যদি এখন অ্যাপটি চালান, তাহলে আপনি ক্রসওয়ার্ডে স্লট করার জন্য শব্দগুলি অনুসন্ধান করার জন্য পটভূমি আইসোলেটের সংখ্যা পরিবর্তন করতে সক্ষম হবেন।

  1. ক্রসওয়ার্ডের সাইজিং সম্বলিত প্রাসঙ্গিক মেনু খুলতে, বর্তমানে তৈরি করা ক্রসওয়ার্ডে পরিসংখ্যান প্রদর্শন করতে হবে কিনা এবং এখন, ব্যবহার করার জন্য বিচ্ছিন্নতার সংখ্যা দেখতে গিয়ার আইকনে ক্লিক করুন।

শব্দ এবং পরিসংখ্যান সহ ক্রসওয়ার্ড জেনারেটর উইন্ডো

ক্রসওয়ার্ড জেনারেটর চালানোর ফলে একযোগে একাধিক কোর ব্যবহার করে একটি 80x44 ক্রসওয়ার্ডের গণনার সময় উল্লেখযোগ্যভাবে হ্রাস পেয়েছে।

9. এটিকে একটি খেলায় পরিণত করুন

এই শেষ বিভাগ সত্যিই একটি বোনাস রাউন্ড. ক্রসওয়ার্ড জেনারেটর তৈরি করার সময় আপনি যে সমস্ত কৌশল শিখেছেন তা গ্রহণ করবেন এবং একটি গেম তৈরি করতে এই কৌশলগুলি ব্যবহার করবেন। আপনি একটি ক্রসওয়ার্ড পাজল তৈরি করতে ক্রসওয়ার্ড জেনারেটর ব্যবহার করবেন। ব্যবহারকারীকে গ্রিডে বিভিন্ন শব্দ-আকৃতির গর্তে রাখার জন্য শব্দ নির্বাচন এবং অনির্বাচন করতে সক্ষম করতে আপনি প্রাসঙ্গিক মেনু বাগধারাগুলি পুনরায় ব্যবহার করবেন। ক্রসওয়ার্ড সম্পূর্ণ করার লক্ষ্যে সব.

আমি এই গেমটি পালিশ বা সমাপ্ত বলতে যাচ্ছি না, এটি আসলে অনেক দূরে। ভারসাম্য এবং অসুবিধার সমস্যা রয়েছে যা বিকল্প শব্দের পছন্দের উন্নতির মাধ্যমে সমাধান করা যেতে পারে। ব্যবহারকারীদের নেতৃত্ব দেওয়ার জন্য কোনও টিউটোরিয়াল নেই এবং চিন্তাভাবনা অ্যানিমেশনটি পছন্দের অনেক কিছু ছেড়ে দেয়। আমি এমনকি খালি হাড় উল্লেখ করতে যাচ্ছি না "আপনি জিতেছেন!" পর্দা

এখানে ট্রেডঅফ হল যে এই প্রোটো-গেমটিকে একটি সম্পূর্ণ গেমে সঠিকভাবে পোলিশ করতে উল্লেখযোগ্যভাবে আরও বেশি কোড লাগবে। একটি একক কোডল্যাবে থাকা উচিত তার চেয়ে বেশি কোড। সুতরাং, পরিবর্তে, এই কোডল্যাবে এখন পর্যন্ত শেখা কৌশলগুলিকে কোথায় এবং কীভাবে ব্যবহার করা হয় তা পরিবর্তন করে আরও শক্তিশালী করার জন্য এটি একটি গতি চালানোর পদক্ষেপ। আশা করি এটি এই কোডল্যাবে আগে শেখা পাঠকে শক্তিশালী করবে। বিকল্পভাবে, আপনি এগিয়ে যেতে পারেন এবং এই কোডের উপর ভিত্তি করে আপনার নিজস্ব অভিজ্ঞতা তৈরি করতে পারেন। আমরা আপনি কি নির্মাণ দেখতে চাই!

শুরু করতে, এই পদক্ষেপগুলি অনুসরণ করুন:

  1. lib/widgets ডিরেক্টরির সবকিছু মুছুন। আপনি আপনার গেমের জন্য চকচকে নতুন উইজেট তৈরি করবেন। পুরানো উইজেটগুলি থেকে অনেক ধার নেওয়ার জন্য এটি ঘটে।
  2. Crossword addWord পদ্ধতিটি নিম্নরূপ আপডেট করতে আপনার model.dart ফাইলটি সম্পাদনা করুন:

lib/model.dart

  /// Add a word to the crossword at the given location and direction.
  Crossword? addWord({
    required Location location,
    required String word,
    required Direction direction,
    bool requireOverlap = true,                            // Add this parameter
  }) {
    // Require that the word is not already in the crossword.
    if (words.map((crosswordWord) => crosswordWord.word).contains(word)) {
      return null;
    }

    final wordCharacters = word.characters;
    bool overlap = false;

    // Check that the word fits in the crossword.
    for (final (index, character) in wordCharacters.indexed) {
      final characterLocation = switch (direction) {
        Direction.across => location.rightOffset(index),
        Direction.down => location.downOffset(index),
      };

      final target = characters[characterLocation];
      if (target != null) {
        overlap = true;
        if (target.character != character) {
          return null;
        }
        if (direction == Direction.across && target.acrossWord != null ||
            direction == Direction.down && target.downWord != null) {
          return null;
        }
      }
    }
                                                           // Edit from here
    // If overlap is required, make sure that the word overlaps with an existing
    // word. Skip this test if the crossword is empty.
    if (words.isNotEmpty && !overlap && requireOverlap) {  // To here.
      return null;
    }

    final candidate = rebuild(
      (b) => b
        ..words.add(
          CrosswordWord.word(
            word: word,
            direction: direction,
            location: location,
          ),
        ),
    );

    if (candidate.valid) {
      return candidate;
    } else {
      return null;
    }
  }

আপনার ক্রসওয়ার্ড মডেলের এই ক্ষুদ্র পরিবর্তনটি এমন শব্দ যোগ করতে সক্ষম করে যা ওভারল্যাপ হয় না। খেলোয়াড়দের একটি বোর্ডে যেকোনো জায়গায় খেলার অনুমতি দেওয়া এবং এখনও প্লেয়ারের চালগুলি সংরক্ষণ করার জন্য একটি বেস মডেল হিসাবে Crossword ব্যবহার করতে সক্ষম হওয়া দরকারী। এটি শুধুমাত্র একটি নির্দিষ্ট অবস্থানে একটি নির্দিষ্ট দিকে রাখা শব্দের একটি তালিকা।

  1. আপনার model.dart ফাইলের শেষে CrosswordPuzzleGame মডেল ক্লাস যোগ করুন।

lib/model.dart

/// Creates a puzzle from a crossword and a set of candidate words.
abstract class CrosswordPuzzleGame
    implements Built<CrosswordPuzzleGame, CrosswordPuzzleGameBuilder> {
  static Serializer<CrosswordPuzzleGame> get serializer =>
      _$crosswordPuzzleGameSerializer;

  /// The [Crossword] that this puzzle is based on.
  Crossword get crossword;

  /// The alternate words for each [CrosswordWord] in the crossword.
  BuiltMap<Location, BuiltMap<Direction, BuiltList<String>>> get alternateWords;

  /// The player's selected words.
  BuiltList<CrosswordWord> get selectedWords;

  bool canSelectWord({
    required Location location,
    required String word,
    required Direction direction,
  }) {
    final crosswordWord = CrosswordWord.word(
      word: word,
      location: location,
      direction: direction,
    );

    if (selectedWords.contains(crosswordWord)) {
      return true;
    }

    var puzzle = this;

    if (puzzle.selectedWords
        .where((b) => b.direction == direction && b.location == location)
        .isNotEmpty) {
      puzzle = puzzle.rebuild((b) => b
        ..selectedWords.removeWhere(
          (selectedWord) =>
              selectedWord.location == location &&
              selectedWord.direction == direction,
        ));
    }

    return null !=
        puzzle.crosswordFromSelectedWords.addWord(
            location: location,
            word: word,
            direction: direction,
            requireOverlap: false);
  }

  CrosswordPuzzleGame? selectWord({
    required Location location,
    required String word,
    required Direction direction,
  }) {
    final crosswordWord = CrosswordWord.word(
      word: word,
      location: location,
      direction: direction,
    );

    if (selectedWords.contains(crosswordWord)) {
      return rebuild((b) => b.selectedWords.remove(crosswordWord));
    }

    var puzzle = this;

    if (puzzle.selectedWords
        .where((b) => b.direction == direction && b.location == location)
        .isNotEmpty) {
      puzzle = puzzle.rebuild((b) => b
        ..selectedWords.removeWhere(
          (selectedWord) =>
              selectedWord.location == location &&
              selectedWord.direction == direction,
        ));
    }

    // Check if the selected word meshes with the already selected words.
    // Note this version of the crossword does not enforce overlap to
    // allow the player to select words anywhere on the grid. Enforcing words
    // to be solved in order is a possible alternative.
    final updatedSelectedWordsCrossword =
        puzzle.crosswordFromSelectedWords.addWord(
      location: location,
      word: word,
      direction: direction,
      requireOverlap: false,
    );

    // Make sure the selected word is in the crossword or is an alternate word.
    if (updatedSelectedWordsCrossword != null) {
      if (puzzle.crossword.words.contains(crosswordWord) ||
          puzzle.alternateWords[location]?[direction]?.contains(word) == true) {
        return puzzle.rebuild((b) => b
          ..selectedWords.add(CrosswordWord.word(
              word: word, location: location, direction: direction)));
      }
    }
    return null;
  }

  /// The crossword from the selected words.
  Crossword get crosswordFromSelectedWords => Crossword.crossword(
      width: crossword.width, height: crossword.height, words: selectedWords);

  /// Test if the puzzle is solved. Note, this allows for the possibility of
  /// multiple solutions.
  bool get solved =>
      crosswordFromSelectedWords.valid &&
      crosswordFromSelectedWords.words.length == crossword.words.length &&
      crossword.words.isNotEmpty;

  /// Create a crossword puzzle game from a crossword and a set of candidate
  /// words.
  factory CrosswordPuzzleGame.from({
    required Crossword crossword,
    required BuiltSet<String> candidateWords,
  }) {
    // Remove all of the currently used words from the list of candidates
    candidateWords = candidateWords
        .rebuild((p0) => p0.removeAll(crossword.words.map((p1) => p1.word)));

    // This is the list of alternate words for each word in the crossword
    var alternates =
        BuiltMap<Location, BuiltMap<Direction, BuiltList<String>>>();

    // Build the alternate words for each word in the crossword
    for (final crosswordWord in crossword.words) {
      final alternateWords = candidateWords.toBuiltList().rebuild((b) => b
        ..where((b) => b.length == crosswordWord.word.length)
        ..shuffle()
        ..take(4)
        ..sort());

      candidateWords =
          candidateWords.rebuild((b) => b.removeAll(alternateWords));

      alternates = alternates.rebuild(
        (b) => b.updateValue(
          crosswordWord.location,
          (b) => b.rebuild(
            (b) => b.updateValue(
              crosswordWord.direction,
              (b) => b.rebuild((b) => b.replace(alternateWords)),
              ifAbsent: () => alternateWords,
            ),
          ),
          ifAbsent: () => {crosswordWord.direction: alternateWords}.build(),
        ),
      );
    }

    return CrosswordPuzzleGame((b) {
      b
        ..crossword.replace(crossword)
        ..alternateWords.replace(alternates);
    });
  }

  factory CrosswordPuzzleGame(
          [void Function(CrosswordPuzzleGameBuilder)? updates]) =
      _$CrosswordPuzzleGame;
  CrosswordPuzzleGame._();
}

/// Construct the serialization/deserialization code for the data model.
@SerializersFor([
  Location,
  Crossword,
  CrosswordWord,
  CrosswordCharacter,
  WorkQueue,
  DisplayInfo,
  CrosswordPuzzleGame,                                     // Add this line
])
final Serializers serializers = _$serializers;

providers.dart ফাইলের আপডেটগুলি পরিবর্তনের একটি আকর্ষণীয় গ্র্যাব ব্যাগ। পরিসংখ্যান সংগ্রহের সমর্থনে উপস্থিত থাকা বেশিরভাগ প্রদানকারীকে সরিয়ে দেওয়া হয়েছে। ব্যাকগ্রাউন্ড আইসোলেটের সংখ্যা পরিবর্তন করার ক্ষমতা সরানো হয়েছে এবং একটি ধ্রুবক দিয়ে প্রতিস্থাপিত করা হয়েছে। এছাড়াও একটি নতুন প্রদানকারী রয়েছে যা আপনি এইমাত্র উপরে যোগ করা নতুন CrosswordPuzzleGame মডেলটিতে অ্যাক্সেস দেয়৷

lib/providers.dart

import 'dart:convert';
                                                           // Drop the dart:math import

import 'package:built_collection/built_collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

import 'isolates.dart';
import 'model.dart' as model;

part 'providers.g.dart';

const backgroundWorkerCount = 4;                           // Add this line

/// A provider for the wordlist to use when generating the crossword.
@riverpod
Future<BuiltSet<String>> wordList(WordListRef ref) async {
  // This codebase requires that all words consist of lowercase characters
  // in the range 'a'-'z'. Words containing uppercase letters will be
  // lowercased, and words containing runes outside this range will
  // be removed.

  final re = RegExp(r'^[a-z]+$');
  final words = await rootBundle.loadString('assets/words.txt');
  return const LineSplitter().convert(words).toBuiltSet().rebuild((b) => b
    ..map((word) => word.toLowerCase().trim())
    ..where((word) => word.length > 2)
    ..where((word) => re.hasMatch(word)));
}

/// An enumeration for different sizes of [model.Crossword]s.
enum CrosswordSize {
  small(width: 20, height: 11),
  medium(width: 40, height: 22),
  large(width: 80, height: 44),
  xlarge(width: 160, height: 88),
  xxlarge(width: 500, height: 500);

  const CrosswordSize({
    required this.width,
    required this.height,
  });

  final int width;
  final int height;
  String get label => '$width x $height';
}

/// A provider that holds the current size of the crossword to generate.
@Riverpod(keepAlive: true)
class Size extends _$Size {
  var _size = CrosswordSize.medium;

  @override
  CrosswordSize build() => _size;

  void setSize(CrosswordSize size) {
    _size = size;
    ref.invalidateSelf();
  }
}

@riverpod
Stream<model.WorkQueue> workQueue(WorkQueueRef ref) async* {
  final size = ref.watch(sizeProvider);                   // Drop the ref.watch(workerCountProvider)
  final wordListAsync = ref.watch(wordListProvider);
  final emptyCrossword =
      model.Crossword.crossword(width: size.width, height: size.height);
  final emptyWorkQueue = model.WorkQueue.from(
    crossword: emptyCrossword,
    candidateWords: BuiltSet<String>(),
    startLocation: model.Location.at(0, 0),
  );
                                                          // Drop the startTimeProvider and endTimeProvider refs
  yield* wordListAsync.when(
    data: (wordList) => exploreCrosswordSolutions(
      crossword: emptyCrossword,
      wordList: wordList,
      maxWorkerCount: backgroundWorkerCount,              // Edit this line
    ),
    error: (error, stackTrace) async* {
      debugPrint('Error loading word list: $error');
      yield emptyWorkQueue;
    },
    loading: () async* {
      yield emptyWorkQueue;
    },
  );
}                                                         // Drop the endTimeProvider ref

@riverpod                                                 // Add from here to end of file
class Puzzle extends _$Puzzle {
  model.CrosswordPuzzleGame _puzzle = model.CrosswordPuzzleGame.from(
    crossword: model.Crossword.crossword(width: 0, height: 0),
    candidateWords: BuiltSet<String>(),
  );

  @override
  model.CrosswordPuzzleGame build() {
    final size = ref.watch(sizeProvider);
    final wordList = ref.watch(wordListProvider).value;
    final workQueue = ref.watch(workQueueProvider).value;

    if (wordList != null &&
        workQueue != null &&
        workQueue.isCompleted &&
        (_puzzle.crossword.height != size.height ||
            _puzzle.crossword.width != size.width ||
            _puzzle.crossword != workQueue.crossword)) {
      compute(_puzzleFromCrosswordTrampoline, (workQueue.crossword, wordList))
          .then((puzzle) {
        _puzzle = puzzle;
        ref.invalidateSelf();
      });
    }

    return _puzzle;
  }

  Future<void> selectWord({
    required model.Location location,
    required String word,
    required model.Direction direction,
  }) async {
    final candidate = await compute(
        _puzzleSelectWordTrampoline, (_puzzle, location, word, direction));

    if (candidate != null) {
      _puzzle = candidate;
      ref.invalidateSelf();
    } else {
      debugPrint('Invalid word selection: $word');
    }
  }

  bool canSelectWord({
    required model.Location location,
    required String word,
    required model.Direction direction,
  }) {
    return _puzzle.canSelectWord(
      location: location,
      word: word,
      direction: direction,
    );
  }
}

// Trampoline functions to disentangle these Isolate target calls from the
// unsendable reference to the [Puzzle] provider.

Future<model.CrosswordPuzzleGame> _puzzleFromCrosswordTrampoline(
        (model.Crossword, BuiltSet<String>) args) async =>
    model.CrosswordPuzzleGame.from(crossword: args.$1, candidateWords: args.$2);

model.CrosswordPuzzleGame? _puzzleSelectWordTrampoline(
        (
          model.CrosswordPuzzleGame,
          model.Location,
          String,
          model.Direction
        ) args) =>
    args.$1.selectWord(location: args.$2, word: args.$3, direction: args.$4);

Puzzle প্রদানকারীর সবচেয়ে আকর্ষণীয় অংশগুলি হল একটি Crossword এবং একটি wordList থেকে CrosswordPuzzleGame তৈরির খরচ এবং একটি শব্দ নির্বাচন করার খরচের উপর গ্লস করার জন্য গৃহীত কৌশলগুলি। এই দুটি কাজই যখন ব্যাকগ্রাউন্ডের সাহায্য ছাড়াই করা হয় তখন আইসোলেট UI মিথস্ক্রিয়াকে স্থবির করে দেয়। ব্যাকগ্রাউন্ডে চূড়ান্ত ফলাফল গণনা করার সময় একটি মধ্যবর্তী ফলাফল পুশ করার জন্য সামান্য হাত ব্যবহার করে, ব্যাকগ্রাউন্ডে প্রয়োজনীয় কম্পিউটেশন চলাকালীন আপনি একটি প্রতিক্রিয়াশীল UI দিয়ে শেষ করেন।

  1. এখন-খালি lib/widgets ডিরেক্টরিতে, নিম্নলিখিত বিষয়বস্তু সহ একটি crossword_puzzle_app.dart ফাইল তৈরি করুন:

lib/widgets/crossword_puzzle_app.dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import '../providers.dart';
import 'crossword_generator_widget.dart';
import 'crossword_puzzle_widget.dart';
import 'puzzle_completed_widget.dart';

class CrosswordPuzzleApp extends StatelessWidget {
  const CrosswordPuzzleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return _EagerInitialization(
      child: Scaffold(
        appBar: AppBar(
          actions: [_CrosswordPuzzleAppMenu()],
          titleTextStyle: TextStyle(
            color: Theme.of(context).colorScheme.primary,
            fontSize: 16,
            fontWeight: FontWeight.bold,
          ),
          title: Text('Crossword Puzzle'),
        ),
        body: SafeArea(
          child: Consumer(builder: (context, ref, _) {
            final workQueueAsync = ref.watch(workQueueProvider);
            final puzzleSolved =
                ref.watch(puzzleProvider.select((puzzle) => puzzle.solved));

            return workQueueAsync.when(
              data: (workQueue) {
                if (puzzleSolved) {
                  return PuzzleCompletedWidget();
                }
                if (workQueue.isCompleted &&
                    workQueue.crossword.characters.isNotEmpty) {
                  return CrosswordPuzzleWidget();
                }
                return CrosswordGeneratorWidget();
              },
              loading: () => Center(child: CircularProgressIndicator()),
              error: (error, stackTrace) => Center(child: Text('$error')),
            );
          }),
        ),
      ),
    );
  }
}

class _EagerInitialization extends ConsumerWidget {
  const _EagerInitialization({required this.child});
  final Widget child;

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    ref.watch(wordListProvider);
    return child;
  }
}

class _CrosswordPuzzleAppMenu extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) => MenuAnchor(
        menuChildren: [
          for (final entry in CrosswordSize.values)
            MenuItemButton(
              onPressed: () => ref.read(sizeProvider.notifier).setSize(entry),
              leadingIcon: entry == ref.watch(sizeProvider)
                  ? Icon(Icons.radio_button_checked_outlined)
                  : Icon(Icons.radio_button_unchecked_outlined),
              child: Text(entry.label),
            ),
        ],
        builder: (context, controller, child) => IconButton(
          onPressed: () => controller.open(),
          icon: Icon(Icons.settings),
        ),
      );
}

এই ফাইলের বেশিরভাগই এখন পর্যন্ত মোটামুটি পরিচিত হওয়া উচিত। হ্যাঁ, সেখানে অনির্ধারিত উইজেট থাকবে, যা আপনি এখন ঠিক করা শুরু করবেন।

  1. একটি crossword_generator_widget.dart ফাইল তৈরি করুন এবং এতে নিম্নলিখিত বিষয়বস্তু যোগ করুন:

lib/widgets/crossword_generator_widget.dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:two_dimensional_scrollables/two_dimensional_scrollables.dart';

import '../model.dart';
import '../providers.dart';

class CrosswordGeneratorWidget extends ConsumerWidget {
  const CrosswordGeneratorWidget({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final size = ref.watch(sizeProvider);
    return TableView.builder(
      diagonalDragBehavior: DiagonalDragBehavior.free,
      cellBuilder: _buildCell,
      columnCount: size.width,
      columnBuilder: (index) => _buildSpan(context, index),
      rowCount: size.height,
      rowBuilder: (index) => _buildSpan(context, index),
    );
  }

  TableViewCell _buildCell(BuildContext context, TableVicinity vicinity) {
    final location = Location.at(vicinity.column, vicinity.row);

    return TableViewCell(
      child: Consumer(
        builder: (context, ref, _) {
          final character = ref.watch(
            workQueueProvider.select(
              (workQueueAsync) => workQueueAsync.when(
                data: (workQueue) => workQueue.crossword.characters[location],
                error: (error, stackTrace) => null,
                loading: () => null,
              ),
            ),
          );

          final explorationCell = ref.watch(
            workQueueProvider.select(
              (workQueueAsync) => workQueueAsync.when(
                data: (workQueue) =>
                    workQueue.locationsToTry.keys.contains(location),
                error: (error, stackTrace) => false,
                loading: () => false,
              ),
            ),
          );

          if (character != null) {
            return AnimatedContainer(
              duration: Durations.extralong1,
              curve: Curves.easeInOut,
              color: explorationCell
                  ? Theme.of(context).colorScheme.primary
                  : Theme.of(context).colorScheme.onPrimary,
              child: Center(
                child: AnimatedDefaultTextStyle(
                  duration: Durations.extralong1,
                  curve: Curves.easeInOut,
                  style: TextStyle(
                    fontSize: 24,
                    color: explorationCell
                        ? Theme.of(context).colorScheme.onPrimary
                        : Theme.of(context).colorScheme.primary,
                  ),
                  child: Text('•'), // https://www.compart.com/en/unicode/U+2022
                ),
              ),
            );
          }

          return ColoredBox(
            color: Theme.of(context).colorScheme.primaryContainer,
          );
        },
      ),
    );
  }

  TableSpan _buildSpan(BuildContext context, int index) {
    return TableSpan(
      extent: FixedTableSpanExtent(32),
      foregroundDecoration: TableSpanDecoration(
        border: TableSpanBorder(
          leading: BorderSide(
              color: Theme.of(context).colorScheme.onPrimaryContainer),
          trailing: BorderSide(
              color: Theme.of(context).colorScheme.onPrimaryContainer),
        ),
      ),
    );
  }
}

এটিও যুক্তিসঙ্গতভাবে পরিচিত হওয়া উচিত। প্রাথমিক পার্থক্য হল যে শব্দগুলির অক্ষরগুলি তৈরি করা হচ্ছে তা প্রদর্শন করার পরিবর্তে, আপনি এখন একটি অজানা অক্ষরের উপস্থিতি বোঝাতে একটি ইউনিকোড অক্ষর প্রদর্শন করছেন। এটি সত্যই নান্দনিকতার উন্নতি করতে কিছু কাজ ব্যবহার করতে পারে।

  1. crossword_puzzle_widget.dart ফাইল তৈরি করুন এবং এতে নিম্নলিখিত সামগ্রী যুক্ত করুন:

lib/উইজেটস/ক্রসওয়ার্ড_পজল_উইডেট.ডার্ট

import 'package:built_collection/built_collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:two_dimensional_scrollables/two_dimensional_scrollables.dart';

import '../model.dart';
import '../providers.dart';

class CrosswordPuzzleWidget extends ConsumerWidget {
  const CrosswordPuzzleWidget({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final size = ref.watch(sizeProvider);
    return TableView.builder(
      diagonalDragBehavior: DiagonalDragBehavior.free,
      cellBuilder: _buildCell,
      columnCount: size.width,
      columnBuilder: (index) => _buildSpan(context, index),
      rowCount: size.height,
      rowBuilder: (index) => _buildSpan(context, index),
    );
  }

  TableViewCell _buildCell(BuildContext context, TableVicinity vicinity) {
    final location = Location.at(vicinity.column, vicinity.row);

    return TableViewCell(
      child: Consumer(
        builder: (context, ref, _) {
          final character = ref.watch(puzzleProvider
              .select((puzzle) => puzzle.crossword.characters[location]));
          final selectedCharacter = ref.watch(puzzleProvider.select((puzzle) =>
              puzzle.crosswordFromSelectedWords.characters[location]));
          final alternateWords = ref
              .watch(puzzleProvider.select((puzzle) => puzzle.alternateWords));

          if (character != null) {
            final acrossWord = character.acrossWord;
            var acrossWords = BuiltList<String>();
            if (acrossWord != null) {
              acrossWords = acrossWords.rebuild((b) => b
                ..add(acrossWord.word)
                ..addAll(alternateWords[acrossWord.location]
                        ?[acrossWord.direction] ??
                    [])
                ..sort());
            }

            final downWord = character.downWord;
            var downWords = BuiltList<String>();
            if (downWord != null) {
              downWords = downWords.rebuild((b) => b
                ..add(downWord.word)
                ..addAll(alternateWords[downWord.location]
                        ?[downWord.direction] ??
                    [])
                ..sort());
            }

            return MenuAnchor(
              builder: (context, controller, _) {
                return GestureDetector(
                  onTapDown: (details) =>
                      controller.open(position: details.localPosition),
                  child: AnimatedContainer(
                    duration: Durations.extralong1,
                    curve: Curves.easeInOut,
                    color: Theme.of(context).colorScheme.onPrimary,
                    child: Center(
                      child: AnimatedDefaultTextStyle(
                        duration: Durations.extralong1,
                        curve: Curves.easeInOut,
                        style: TextStyle(
                          fontSize: 24,
                          color: Theme.of(context).colorScheme.primary,
                        ),
                        child: Text(selectedCharacter?.character ?? ''),
                      ),
                    ),
                  ),
                );
              },
              menuChildren: [
                if (acrossWords.isNotEmpty && downWords.isNotEmpty)
                  Padding(
                    padding: const EdgeInsets.all(4),
                    child: Text('Across'),
                  ),
                for (final word in acrossWords)
                  _WordSelectMenuItem(
                    location: acrossWord!.location,
                    word: word,
                    selectedCharacter: selectedCharacter,
                    direction: Direction.across,
                  ),
                if (acrossWords.isNotEmpty && downWords.isNotEmpty)
                  Padding(
                    padding: const EdgeInsets.all(4),
                    child: Text('Down'),
                  ),
                for (final word in downWords)
                  _WordSelectMenuItem(
                    location: downWord!.location,
                    word: word,
                    selectedCharacter: selectedCharacter,
                    direction: Direction.down,
                  ),
              ],
            );
          }

          return ColoredBox(
            color: Theme.of(context).colorScheme.primaryContainer,
          );
        },
      ),
    );
  }

  TableSpan _buildSpan(BuildContext context, int index) {
    return TableSpan(
      extent: FixedTableSpanExtent(32),
      foregroundDecoration: TableSpanDecoration(
        border: TableSpanBorder(
          leading: BorderSide(
              color: Theme.of(context).colorScheme.onPrimaryContainer),
          trailing: BorderSide(
              color: Theme.of(context).colorScheme.onPrimaryContainer),
        ),
      ),
    );
  }
}

class _WordSelectMenuItem extends ConsumerWidget {
  const _WordSelectMenuItem({
    required this.location,
    required this.word,
    required this.selectedCharacter,
    required this.direction,
  });

  final Location location;
  final String word;
  final CrosswordCharacter? selectedCharacter;
  final Direction direction;

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final notifier = ref.read(puzzleProvider.notifier);
    return MenuItemButton(
      onPressed: ref.watch(puzzleProvider.select((puzzle) =>
              puzzle.canSelectWord(
                  location: location, word: word, direction: direction)))
          ? () => notifier.selectWord(
              location: location, word: word, direction: direction)
          : null,
      leadingIcon: switch (direction) {
        Direction.across => selectedCharacter?.acrossWord?.word == word,
        Direction.down => selectedCharacter?.downWord?.word == word,
      }
          ? Icon(Icons.radio_button_checked_outlined)
          : Icon(Icons.radio_button_unchecked_outlined),
      child: Text(word),
    );
  }
}

এই উইজেটটি শেষের চেয়ে কিছুটা তীব্র, যদিও এটি আপনি অতীতে অন্যান্য জায়গায় ব্যবহৃত টুকরোগুলি থেকে তৈরি করা হয়েছে। এখন, প্রতিটি জনবহুল সেল ক্লিক করার সময় একটি প্রসঙ্গ মেনু উত্পাদন করে, যা ব্যবহারকারী নির্বাচন করতে পারে এমন শব্দগুলির তালিকা করে। যদি শব্দগুলি নির্বাচন করা হয়, তবে যে শব্দগুলি দ্বন্দ্ব নির্বাচনযোগ্য নয়। কোনও শব্দকে ব্যাখ্যা করতে, ব্যবহারকারী সেই শব্দের জন্য মেনু আইটেমটিতে ট্যাপ করে।

ধরে নিই যে প্লেয়ার পুরো ক্রসওয়ার্ডটি পূরণ করতে শব্দগুলি নির্বাচন করতে পারে, আপনার একটি "আপনি জিতেছেন!" পর্দা

  1. একটি puzzle_completed_widget.dart ফাইল তৈরি করুন এবং তারপরে এটিতে নিম্নলিখিত সামগ্রী যুক্ত করুন:

lib/উইজেটস/ধাঁধা_কমপ্লিট_উইডেট.ডার্ট

import 'package:flutter/material.dart';

class PuzzleCompletedWidget extends StatelessWidget {
  const PuzzleCompletedWidget({super.key});

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Text(
        'Puzzle Completed!',
        style: TextStyle(
          fontSize: 36,
          fontWeight: FontWeight.bold,
        ),
      ),
    );
  }
}

আমি নিশ্চিত যে আপনি এটি নিতে পারেন এবং এটি আরও আকর্ষণীয় করে তুলতে পারেন। অ্যানিমেশন সরঞ্জামগুলি সম্পর্কে আরও জানতে, ফ্লুটার কোডল্যাব -এ বিল্ডিং নেক্সট জেনারেশন ইউআইএস দেখুন।

  1. নিম্নলিখিত হিসাবে আপনার lib/main.dart ফাইল সম্পাদনা করুন:

lib/main.dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import 'widgets/crossword_puzzle_app.dart';                 // Update this line

void main() {
  runApp(
    ProviderScope(
      child: MaterialApp(
        title: 'Crossword Puzzle',                          // Update this line
        debugShowCheckedModeBanner: false,
        theme: ThemeData(
          useMaterial3: true,
          colorSchemeSeed: Colors.blueGrey,
          brightness: Brightness.light,
        ),
        home: CrosswordPuzzleApp(),                         // Update this line
      ),
    ),
  );
}

আপনি যখন এই অ্যাপ্লিকেশনটি চালাবেন, আপনি ক্রসওয়ার্ড জেনারেটরটি আপনার ধাঁধা তৈরি করার সাথে সাথে অ্যানিমেশনটি দেখতে পাবেন। তারপরে আপনাকে সমাধানের জন্য একটি ফাঁকা ধাঁধা উপস্থাপন করা হবে। আপনি এটি সমাধান করুন ধরে নিচ্ছেন, আপনাকে এমন একটি স্ক্রিন উপস্থাপন করা উচিত যা দেখতে এটির মতো দেখাচ্ছে:

ক্রসওয়ার্ড ধাঁধা অ্যাপ উইন্ডো পাঠ্যটি দেখায় 'ধাঁধাটি সম্পূর্ণ হয়েছে!'

10. অভিনন্দন

অভিনন্দন! আপনি ঝাঁকুনির সাথে একটি ধাঁধা গেম তৈরি করতে সফল হয়েছেন!

আপনি একটি ক্রসওয়ার্ড জেনারেটর তৈরি করেছিলেন যা একটি ধাঁধা গেম হয়ে যায়। আপনি বিচ্ছিন্নতার একটি পুলে ব্যাকগ্রাউন্ড গণনা চালানো দক্ষতা অর্জন করেছেন। আপনি ব্যাকট্র্যাকিং অ্যালগরিদম বাস্তবায়ন সহজ করতে অপরিবর্তনীয় ডেটা স্ট্রাকচার ব্যবহার করেছেন। এবং আপনি TableView সাথে মানসম্পন্ন সময় ব্যয় করেছেন, যা পরের বার আপনাকে সারণী ডেটা প্রদর্শন করতে হবে তা কার্যকর হবে।

আরও জানুন