1. शुरू करने से पहले
मान लें कि आपसे पूछा गया है कि क्या दुनिया का सबसे बड़ा क्रॉसवर्ड पज़ल बनाया जा सकता है. आपको स्कूल में पढ़ी गई कुछ एआई तकनीकों के बारे में याद है. आपको यह जानना है कि क्या Flutter का इस्तेमाल करके, कंप्यूटेशनल इंटेंसिव समस्याओं को हल करने के लिए एल्गोरिथम के विकल्पों को एक्सप्लोर किया जा सकता है.
इस कोडलैब में, आपको यही करना है. कोर्स के आखिर तक, आपके पास शब्दों के ग्रिड वाले पज़ल बनाने के लिए एल्गोरिदम का इस्तेमाल करने वाला एक टूल होगा. क्रॉसवर्ड पज़ल के मान्य होने की कई अलग-अलग परिभाषाएं हैं. इन तकनीकों की मदद से, ऐसी पज़ल बनाई जा सकती हैं जो आपकी परिभाषा के मुताबिक हों.
इस टूल को आधार बनाकर, क्रॉसवर्ड पज़ल जनरेट करने वाले टूल की मदद से एक क्रॉसवर्ड पज़ल बनाएं. इससे उपयोगकर्ता को पज़ल हल करने में मदद मिलेगी. इस पज़ल को Android, iOS, Windows, macOS, और Linux पर इस्तेमाल किया जा सकता है. Android पर यह सुविधा ऐसे ऐक्सेस करें:
ज़रूरी शर्तें
- आपका पहला Flutter ऐप्लिकेशन कोडलैब पूरा होना
आपको ये सब सीखने को मिलेगा
- Flutter के
compute
फ़ंक्शन और Riverpod केselect
रीबिल्ड फ़िल्टर की वैल्यू-कैशिंग की सुविधाओं का इस्तेमाल करके, कंप्यूटेशनल तौर पर मुश्किल काम करने के लिए आइसोलेट का इस्तेमाल कैसे करें. इससे Flutter के रेंडर लूप में कोई रुकावट नहीं आएगी. - सर्च पर आधारित गुड ओल्ड फ़ैशन एआई (जीओएफ़एआई) की तकनीकों को लागू करने के लिए,
built_value
औरbuilt_collection
के साथ, न बदलने वाले डेटा स्ट्रक्चर का फ़ायदा कैसे पाएं. जैसे, डेप्थ-फ़र्स्ट सर्च और बैकट्रैकिंग. two_dimensional_scrollables
पैकेज की सुविधाओं का इस्तेमाल करके, ग्रिड डेटा को तेज़ी से और आसानी से दिखाने का तरीका.
आपको इन चीज़ों की ज़रूरत पड़ेगी
- Flutter SDK.
- Visual Studio Code (VS Code) में Flutter और Dart प्लगिन इंस्टॉल होने चाहिए.
- चुने गए डेवलपमेंट टारगेट के लिए कंपाइलर सॉफ़्टवेयर. यह कोडलैब, डेस्कटॉप के सभी प्लैटफ़ॉर्म, Android, और iOS के लिए काम करता है. Windows को टारगेट करने के लिए VS Code, macOS या iOS को टारगेट करने के लिए Xcode, और Android को टारगेट करने के लिए Android Studio की ज़रूरत होती है.
2. प्रोजेक्ट बनाना
अपना पहला Flutter प्रोजेक्ट बनाना
- VS Code लॉन्च करें.
- कमांड पैलेट खोलें. इसके लिए, Windows/Linux पर Ctrl+Shift+P और macOS पर Cmd+Shift+P दबाएं. इसके बाद, "flutter new" टाइप करें. फिर, मेन्यू में Flutter: New Project चुनें.
- Empty application को चुनें. इसके बाद, वह डायरेक्ट्री चुनें जिसमें आपको अपना प्रोजेक्ट बनाना है. यह ऐसी डायरेक्ट्री होनी चाहिए जिसके पाथ में कोई स्पेस न हो और जिसके लिए ज़्यादा अनुमतियों की ज़रूरत न हो. उदाहरण के लिए, आपकी होम डायरेक्ट्री या
C:\src\
.
- अपने प्रोजेक्ट को
generate_crossword
नाम दें. इस कोडलैब के बाकी हिस्से में, यह मान लिया गया है कि आपने अपने ऐप्लिकेशन का नाम रखा है.generate_crossword
अब Flutter, आपका प्रोजेक्ट फ़ोल्डर बनाता है और VS Code उसे खोलता है. अब आपको ऐप्लिकेशन के बेसिक स्ट्रक्चर के साथ, दो फ़ाइलों के कॉन्टेंट को बदलना होगा.
शुरुआती ऐप्लिकेशन को कॉपी करके चिपकाएं
- VS Code के बाएं पैनल में, Explorer पर क्लिक करें और
pubspec.yaml
फ़ाइल खोलें.
- इस फ़ाइल के कॉन्टेंट को, क्रॉस वर्ड बनाने के लिए ज़रूरी इन डिपेंडेंसी से बदलें:
pubspec.yaml
name: generate_crossword
description: "A new Flutter project."
publish_to: 'none'
version: 0.1.0
environment:
sdk: ^3.9.0
dependencies:
flutter:
sdk: flutter
built_collection: ^5.1.1
built_value: ^8.10.1
characters: ^1.4.0
flutter_riverpod: ^2.6.1
intl: ^0.20.2
riverpod: ^2.6.1
riverpod_annotation: ^2.6.1
two_dimensional_scrollables: ^0.3.7
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^5.0.0
build_runner: ^2.5.4
built_value_generator: ^8.10.1
custom_lint: ^0.7.6
riverpod_generator: ^2.6.5
riverpod_lint: ^2.6.5
flutter:
uses-material-design: true
pubspec.yaml
फ़ाइल में आपके ऐप्लिकेशन के बारे में बुनियादी जानकारी होती है. जैसे, उसका मौजूदा वर्शन और उसकी डिपेंडेंसी. आपको डिपेंडेंसी का एक ऐसा कलेक्शन दिखता है जो सामान्य तौर पर, खाली फ़्लटर ऐप्लिकेशन का हिस्सा नहीं होता. आने वाले चरणों में, आपको इन सभी पैकेज का फ़ायदा मिलेगा.
डिपेंडेंसी के बारे में जानकारी
कोड के बारे में जानने से पहले, आइए समझते हैं कि इन पैकेज को क्यों चुना गया है:
- built_value: यह ऐसे ऑब्जेक्ट बनाता है जिन्हें बदला नहीं जा सकता. ये ऑब्जेक्ट, मेमोरी को असरदार तरीके से शेयर करते हैं. यह हमारे बैकट्रेकिंग एल्गोरिदम के लिए ज़रूरी है
- Riverpod: यह
select()
के साथ स्टेट मैनेजमेंट की सुविधा देता है, ताकि कम से कम रीबिल्ड हो - two_dimensional_scrollables: परफ़ॉर्मेंस पर असर डाले बिना बड़ी ग्रिड को मैनेज करता है
lib/
डायरेक्ट्री में मौजूदmain.dart
फ़ाइल खोलें.
- इस फ़ाइल के कॉन्टेंट की जगह यह कॉन्टेंट डालें:
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(
colorSchemeSeed: Colors.blueGrey,
brightness: Brightness.light,
),
home: Scaffold(
body: Center(
child: Text('Hello, World!', style: TextStyle(fontSize: 24)),
),
),
),
),
);
}
- यह कोड चलाएं, ताकि यह पक्का किया जा सके कि सब कुछ काम कर रहा है. इसमें एक नई विंडो दिखनी चाहिए. साथ ही, हर नए प्रोजेक्ट के लिए ज़रूरी शुरुआती वाक्यांश भी दिखना चाहिए.
ProviderScope
से पता चलता है कि यह ऐप्लिकेशन, स्टेट मैनेजमेंट के लिएriverpod
का इस्तेमाल करेगा.
चेकपॉइंट: ऐप्लिकेशन ठीक से चल रहा है
इस समय, आपको "Hello, World!" विंडो दिखनी चाहिए. अगर ऐसा नहीं है, तो:
- देखें कि Flutter सही तरीके से इंस्टॉल किया गया हो
- पुष्टि करें कि ऐप्लिकेशन,
flutter run
के साथ काम करता है - पक्का करें कि टर्मिनल में कंपाइल करने से जुड़ी कोई गड़बड़ी न हो
3. शब्द जोड़ना
क्रॉसवर्ड पहेली के लिए बिल्डिंग ब्लॉक
क्रॉसवर्ड में मुख्य तौर पर शब्दों की सूची होती है. शब्दों को ग्रिड में व्यवस्थित किया जाता है. कुछ शब्द आड़े होते हैं और कुछ खड़े होते हैं. इस तरह, शब्द एक-दूसरे से जुड़े होते हैं. एक शब्द को हल करने से, उस शब्द को क्रॉस करने वाले शब्दों के बारे में सुराग मिलते हैं. इसलिए, शब्दों की सूची को पहला बिल्डिंग ब्लॉक माना जाता है.
इन शब्दों के लिए, Peter Norvig का नैचुरल लैंग्वेज कॉर्पस डेटा पेज एक अच्छा सोर्स है. SOWPODS की सूची, 2,67,750 शब्दों के साथ एक उपयोगी शुरुआती पॉइंट है.
इस चरण में, शब्दों की सूची डाउनलोड की जाती है. इसके बाद, इसे अपने Flutter ऐप्लिकेशन में ऐसेट के तौर पर जोड़ा जाता है. साथ ही, Riverpod प्रोवाइडर को इस तरह से व्यवस्थित किया जाता है कि ऐप्लिकेशन के शुरू होने पर, सूची को ऐप्लिकेशन में लोड किया जा सके.
शुरू करने के लिए, इन चरणों का पालन करें:
- अपने प्रोजेक्ट की
pubspec.yaml
फ़ाइल में बदलाव करके, चुनी गई शब्दों की सूची के लिए ऐसेट का यह एलान जोड़ें. इस लिस्टिंग में, आपके ऐप्लिकेशन के कॉन्फ़िगरेशन का सिर्फ़ फ़्लटर स्टैंज़ा दिखता है, क्योंकि बाकी कॉन्फ़िगरेशन में कोई बदलाव नहीं हुआ है.
pubspec.yaml
flutter:
uses-material-design: true
assets: # Add this line
- assets/words.txt # And this one.
आपका एडिटर शायद इस आखिरी लाइन को चेतावनी के साथ हाइलाइट करेगा, क्योंकि आपने अब तक यह फ़ाइल नहीं बनाई है.
- अपने ब्राउज़र और एडिटर का इस्तेमाल करके, अपने प्रोजेक्ट के टॉप लेवल पर एक
assets
डायरेक्ट्री बनाएं. इसके बाद, इसमें एकwords.txt
फ़ाइल बनाएं. इस फ़ाइल में, पहले से लिंक की गई शब्दों की किसी एक सूची को शामिल करें.
इस कोड को पहले बताई गई SOWPODS सूची के हिसाब से डिज़ाइन किया गया है. हालांकि, यह A-Z वर्णों वाली किसी भी शब्द सूची के साथ काम करेगा. इस कोडबेस को अलग-अलग वर्ण सेट के साथ काम करने के लिए, रीडर को एक टास्क दिया गया है.
शब्द लोड करना
ऐप्लिकेशन के शुरू होने पर शब्दों की सूची लोड करने के लिए ज़िम्मेदार कोड लिखने के लिए, यह तरीका अपनाएं:
lib
डायरेक्ट्री मेंproviders.dart
फ़ाइल बनाएं.- फ़ाइल में यह जानकारी जोड़ें:
lib/providers.dart
import 'dart:convert';
import 'package:built_collection/built_collection.dart';
import 'package:flutter/services.dart';
import 'package:riverpod/riverpod.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(Ref 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)),
);
}
यह इस कोडबेस के लिए आपका पहला Riverpod provider है.
यह सेवा देने वाली कंपनी कैसे काम करती है:
- यह फ़ंक्शन, ऐसेट से शब्दों की सूची को एसिंक्रोनस तरीके से लोड करता है
- यह फ़िल्टर, शब्दों को इस तरह से फ़िल्टर करता है कि उनमें सिर्फ़ a से z तक के वर्ण शामिल हों और वे दो से ज़्यादा वर्णों के हों
- यह फ़ंक्शन, रैंडम ऐक्सेस के लिए एक इम्यूटेबल
BuiltSet
दिखाता है
यह प्रोजेक्ट, Riverpod जैसी कई डिपेंडेंसी के लिए कोड जनरेट करने की सुविधा का इस्तेमाल करता है.
- कोड जनरेट करने के लिए, यह कमांड चलाएं:
$ 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
में जोड़े गए कोड से कोई समस्या नहीं होनी चाहिए.
Riverpod में, wordList
जैसे प्रोवाइडर को आम तौर पर लेज़ी इंस्टैंटिएट किया जाता है. हालांकि, इस ऐप्लिकेशन के लिए, आपको शब्द सूची को तुरंत लोड करने की ज़रूरत है. Riverpod के दस्तावेज़ में, उन प्रोवाइडर को मैनेज करने का यह तरीका सुझाया गया है जिन्हें आपको तुरंत लोड करना है. अब आपको इसे लागू करना होगा.
lib/widgets
डायरेक्ट्री में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';
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()
कॉल का इस्तेमाल करके, प्रोवाइडर से जानकारी लेता है. इससे यह मकसद पूरा होता है. इस तकनीक के बारे में ज़्यादा जानने के लिए, Riverpod के दस्तावेज़ में Eager initialization of providers पढ़ें.
इस फ़ाइल में ध्यान देने वाली दूसरी दिलचस्प बात यह है कि Riverpod, एसिंक्रोनस कॉन्टेंट को कैसे मैनेज करता है. आपको याद होगा कि wordList
फ़ंक्शन को एसिंक्रोनस फ़ंक्शन के तौर पर तय किया गया है, क्योंकि डिस्क से कॉन्टेंट लोड होने में समय लगता है. इस कोड में, शब्दों की सूची देने वाली कंपनी को देखने पर आपको AsyncValue<BuiltSet<String>>
मिलता है. उस टाइप का AsyncValue
हिस्सा, एसिंक्रोनस (एसिंक्रोनस का मतलब है कि कोई टास्क पूरा होने में समय लगता है) दुनिया के प्रोवाइडर और विजेट के build
तरीके की सिंक्रोनस (सिंक्रोनस का मतलब है कि कोई टास्क तुरंत पूरा हो जाता है) दुनिया के बीच अडैप्टर का काम करता है.
AsyncValue
के when
तरीके से, उन तीन संभावित स्थितियों को मैनेज किया जाता है जिनमें आने वाले समय में वैल्यू हो सकती है. ऐसा हो सकता है कि आने वाले समय में समस्या हल हो जाए. ऐसे में, data
कॉलबैक शुरू हो जाता है. ऐसा भी हो सकता है कि समस्या बनी रहे. ऐसे में, error
कॉलबैक शुरू हो जाता है. इसके अलावा, ऐसा भी हो सकता है कि अब भी डेटा लोड हो रहा हो. तीनों कॉलबैक के रिटर्न टाइप एक जैसे होने चाहिए, क्योंकि कॉल किए गए कॉलबैक का रिटर्न, when
तरीके से मिलता है. इस उदाहरण में, when तरीके का नतीजा, body
के Scaffold
विजेट के तौर पर दिखाया गया है.
कभी न खत्म होने वाली सूची वाला ऐप्लिकेशन बनाना
CrosswordGeneratorApp
विजेट को अपने ऐप्लिकेशन में इंटिग्रेट करने के लिए, यह तरीका अपनाएं:
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(
colorSchemeSeed: Colors.blueGrey,
brightness: Brightness.light,
),
home: CrosswordGeneratorApp(), // Remove what was here and replace
),
),
);
}
- ऐप्लिकेशन को फिर से चालू करें. आपको स्क्रोल करने लायक एक सूची दिखेगी. इसमें शब्दकोश के सभी 2,67,750 से ज़्यादा शब्द शामिल होंगे.
अगले चरण में क्या बनाया जाएगा
अब आपको इम्यूटेबल ऑब्जेक्ट का इस्तेमाल करके, अपने क्रॉसवर्ड पज़ल के लिए मुख्य डेटा स्ट्रक्चर बनाने होंगे. इस फ़ाउंडेशन की मदद से, बेहतर एल्गोरिदम और यूज़र इंटरफ़ेस (यूआई) के अपडेट आसानी से लागू किए जा सकेंगे.
4. शब्दों को ग्रिड में दिखाएं
इस चरण में, built_value
और built_collection
पैकेज का इस्तेमाल करके, क्रॉस वर्ड पज़ल बनाने के लिए डेटा स्ट्रक्चर बनाया जाएगा. इन दोनों पैकेज की मदद से, डेटा स्ट्रक्चर को ऐसी वैल्यू के तौर पर बनाया जा सकता है जिनमें बदलाव नहीं किया जा सकता. यह सुविधा, आइसोलेट के बीच डेटा ट्रांसफ़र करने के साथ-साथ, डेप्थ फ़र्स्ट सर्च और बैकट्रैकिंग को लागू करने के लिए भी फ़ायदेमंद होगी.
शुरू करने के लिए, इन चरणों का पालन करें:
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
का ग्रिड बनाया जाता है.
इस डेटा स्ट्रक्चर का इस्तेमाल करने के लिए, यह तरीका अपनाएं:
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
फ़ाइल के बाहर भी इस्तेमाल किया जा सके.
- अपनी
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
इंपोर्ट, debugPrint
के लिए flutter/foundation.dart
इंपोर्ट, मॉडल के लिए model.dart
, और BuiltSet
एक्सटेंशन के लिए utils.dart
शामिल है.
- उसी फ़ाइल के आखिर में, इन कंपनियों के नाम जोड़ें:
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(Ref 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
इन्यूमरेशन की चुनी गई वैल्यू शामिल होती है. इससे यूज़र इंटरफ़ेस (यूआई) को, तैयार हो रहे क्रॉसवर्ड का साइज़ दिखाने और सेट करने की अनुमति मिलेगी. दूसरी कंपनी, crossword
ने ज़्यादा दिलचस्प तरीके से वीडियो बनाया है. यह एक ऐसा फ़ंक्शन है जो Crossword
की सीरीज़ दिखाता है. इसे जनरेटर के लिए Dart के सपोर्ट का इस्तेमाल करके बनाया गया है. इसे फ़ंक्शन पर async*
के तौर पर मार्क किया गया है. इसका मतलब है कि यह फ़ंक्शन, किसी वैल्यू को वापस नहीं भेजता, बल्कि Crossword
की एक सीरीज़ जनरेट करता है. यह एक ऐसा तरीका है जिससे इंटरमीडिएट नतीजे देने वाले कंप्यूटेशन को आसानी से लिखा जा सकता है.
crossword
प्रोवाइडर फ़ंक्शन की शुरुआत में ref.watch
कॉल का एक पेयर मौजूद होने की वजह से, Riverpod सिस्टम, Crossword
s की स्ट्रीम को फिर से शुरू करेगा. ऐसा तब होगा, जब क्रॉस वर्ड का चुना गया साइज़ बदलेगा और जब शब्दों की सूची लोड हो जाएगी.
अब आपके पास क्रॉस वर्ड पज़ल जनरेट करने के लिए कोड है. हालांकि, इसमें कई रैंडम शब्द हैं. इसलिए, टूल का इस्तेमाल करने वाले व्यक्ति को ये शब्द दिखाना अच्छा होगा.
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
होने की वजह से, Size
प्रोवाइडर पर सीधे तौर पर भरोसा कर सकता है. इससे Crossword
के वर्णों को दिखाने के लिए, ग्रिड का साइज़ तय किया जा सकता है. इस ग्रिड को two_dimensional_scrollables
पैकेज के TableView
विजेट की मदद से दिखाया गया है.
ध्यान दें कि _buildCell
हेल्पर फ़ंक्शन से रेंडर किए गए हर सेल में, Widget
ट्री में Consumer
विजेट होता है. यह रीफ़्रेश बाउंड्री के तौर पर काम करता है. ref.watch
से मिली वैल्यू में बदलाव होने पर, Consumer
विजेट में मौजूद सभी चीज़ें फिर से बनाई जाती हैं. Crossword
में बदलाव होने पर, पूरे ट्री को फिर से बनाने का विकल्प होता है. हालांकि, इससे बहुत ज़्यादा कंप्यूटेशन होता है. इस सेटअप का इस्तेमाल करके, इस कंप्यूटेशन को स्किप किया जा सकता है.
अगर ref.watch
के पैरामीटर पर नज़र डालें, तो आपको दिखेगा कि crosswordProvider.select
का इस्तेमाल करके, लेआउट को फिर से कंप्यूट करने से बचने की एक और लेयर है. इसका मतलब है कि ref.watch
सिर्फ़ तब TableViewCell
के कॉन्टेंट को फिर से बनाएगा, जब सेल में मौजूद वर्ण बदल जाएगा. फिर से रेंडर करने की प्रोसेस को कम करना, यूज़र इंटरफ़ेस (यूआई) को रिस्पॉन्सिव बनाए रखने के लिए ज़रूरी है.
उपयोगकर्ता को 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()), // Replace what 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
के कॉल से बदल दिया गया है. यह कॉल, lib/widgets/crossword_widget.dart
फ़ाइल में तय किया गया था. एक और बड़ा बदलाव यह है कि ऐप्लिकेशन के व्यवहार को बदलने के लिए एक मेन्यू जोड़ा गया है. इसकी शुरुआत, क्रॉस वर्ड के साइज़ को बदलने से होती है. आने वाले समय में, इसमें और MenuItemButton
जोड़े जाएंगे. अपना ऐप्लिकेशन चलाएं. आपको कुछ ऐसा दिखेगा:
इसमें एक ग्रिड में वर्ण दिखाए गए हैं. साथ ही, एक मेन्यू दिया गया है, जिसकी मदद से उपयोगकर्ता ग्रिड का साइज़ बदल सकता है. हालांकि, शब्दों को क्रॉस वर्ड पज़ल की तरह नहीं रखा गया है. ऐसा इसलिए होता है, क्योंकि क्रॉसवर्ड में शब्दों को जोड़ने के तरीके पर कोई पाबंदी नहीं लगाई गई है. कम शब्दों में कहें, तो यह एक गड़बड़ी है. अगले चरण में, आपको इस समस्या को ठीक करने का तरीका बताया जाएगा!
5. पाबंदियां लागू करना
हम क्या बदल रहे हैं और क्यों
फ़िलहाल, आपके क्रॉस वर्ड में शब्दों को एक-दूसरे पर ओवरलैप करने की अनुमति है. साथ ही, इसकी पुष्टि नहीं की जाती है. आपको शब्दों के बीच में सही कनेक्शन बनाने के लिए, कॉन्स्ट्रेंट की जांच करने की सुविधा जोड़नी होगी. इससे यह पक्का किया जा सकेगा कि शब्द, असली क्रॉसवर्ड पज़ल की तरह सही तरीके से जुड़े हों.
इस चरण का मकसद, मॉडल में कोड जोड़ना है, ताकि क्रॉसवर्ड की शर्तों को लागू किया जा सके. क्रॉसवर्ड पज़ल कई तरह की होती हैं. इस कोडलैब में, अंग्रेज़ी क्रॉसवर्ड पज़ल के स्टाइल का इस्तेमाल किया जाएगा. इस कोड में बदलाव करके, क्रॉसवर्ड पज़ल की अन्य स्टाइल जनरेट करने का काम, हमेशा की तरह पढ़ने वाले व्यक्ति के लिए छोड़ दिया गया है.
शुरू करने के लिए, इन चरणों का पालन करें:
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
फ़ाइलों में किए जा रहे बदलावों के लिए, build_runner
का चालू होना ज़रूरी है. इससे model.g.dart
और providers.g.dart
फ़ाइलों को अपडेट किया जा सकेगा. अगर ये फ़ाइलें अपने-आप अपडेट नहीं हुई हैं, तो अब dart run build_runner watch -d
के साथ build_runner
को फिर से शुरू करने का सही समय है.
मॉडल लेयर में इस नई सुविधा का फ़ायदा पाने के लिए, आपको प्रोवाइडर लेयर को अपडेट करना होगा, ताकि वह मॉडल लेयर से मैच कर सके.
- अपनी
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;
},
);
}
- अपना ऐप्लिकेशन चलाएं. यूज़र इंटरफ़ेस (यूआई) में ज़्यादा कुछ नहीं हो रहा है, लेकिन अगर लॉग देखे जाएं, तो बहुत कुछ हो रहा है.
अगर हम इस बारे में सोचें कि यहां क्या हो रहा है, तो हमें पता चलता है कि क्रॉस वर्ड अचानक से दिख रहा है. Crossword
मॉडल में मौजूद addWord
मेथड, ऐसे किसी भी शब्द को अस्वीकार कर रही है जो मौजूदा क्रॉसवर्ड में फ़िट नहीं होता. इसलिए, यह बहुत अच्छी बात है कि हमें कुछ भी दिख रहा है.
बैकग्राउंड प्रोसेसिंग का इस्तेमाल क्यों करना चाहिए?
क्रॉसवर्ड जनरेट करने के दौरान, आपको यूज़र इंटरफ़ेस (यूआई) में कुछ समय के लिए कोई बदलाव नहीं दिखेगा. ऐसा इसलिए होता है, क्योंकि क्रॉस वर्ड जनरेट करने के लिए, पुष्टि करने की हज़ारों जांचें की जाती हैं. इन कैलकुलेशन से, Flutter के 60fps रेंडरिंग लूप में रुकावट आती है. इसलिए, ज़्यादा कैलकुलेशन वाले टास्क को बैकग्राउंड आइसोलेट में ले जाएं. इससे यह फ़ायदा होता है कि बैकग्राउंड में पज़ल जनरेट होने के दौरान, यूज़र इंटरफ़ेस (यूआई) स्मूद बना रहता है
यह तय करने के लिए कि किस शब्द को कहां इस्तेमाल करना है, इस कंप्यूटेशन को यूज़र इंटरफ़ेस (यूआई) थ्रेड से हटाकर बैकग्राउंड आइसोलेट में ले जाना बहुत मददगार होगा. Flutter में, काम के एक हिस्से को बैकग्राउंड आइसोलेट में चलाने के लिए, बहुत काम का रैपर होता है. इसे compute
फ़ंक्शन कहते हैं.
providers.dart
फ़ाइल में, क्रॉस वर्ड पज़ल की सुविधा देने वाली कंपनी के नाम में इस तरह बदलाव करें:
lib/providers.dart
@riverpod
Stream<model.Crossword> crossword(Ref 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 { // Edit from here
var candidate = await compute((
(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()
पर नहीं भेजा जा सकता. इस समस्या को ठीक करने के लिए, यह पक्का करें कि क्लोज़र के पास ऐसी कोई वैल्यू न हो जिसे भेजा नहीं जा सकता.
पहला चरण, कोड को अलग करने के लिए, प्रोवाइडर को अलग करना है.
- अपनी
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/riverpod.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(Ref 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(Ref ref) async* {
final size = ref.watch(sizeProvider);
final wordListAsync = ref.watch(wordListProvider);
final emptyCrossword = model.Crossword.crossword( // Edit from here
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
की मदद से, इस प्रोसेस को काफ़ी आसान बनाया जा सकता है. इनकी मदद से, नई अपरिवर्तनीय वैल्यू बनाई जा सकती हैं. ये वैल्यू, उस अपरिवर्तनीय वैल्यू के साथ सामान्य स्थिति शेयर करती हैं जिससे इन्हें बनाया गया है. इससे, डीप कॉपी के लिए ज़रूरी मेमोरी की लागत के बिना, संभावित उम्मीदवारों का कम लागत में इस्तेमाल किया जा सकता है.
शुरू करने के लिए, इन चरणों का पालन करें:
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;
- अगर इस फ़ाइल में नया कॉन्टेंट जोड़ने के कुछ सेकंड बाद भी लाल रंग की लाइनें दिख रही हैं, तो पुष्टि करें कि
build_runner
अब भी चल रहा हो. अगर ऐसा नहीं है, तोdart run build_runner watch -d
निर्देश चलाएं.
इस कोड में लॉगिंग की सुविधा जोड़ी जा रही है. इससे यह पता चलेगा कि अलग-अलग साइज़ के क्रॉस वर्ड बनाने में कितना समय लगता है. अगर अवधि को बेहतर तरीके से फ़ॉर्मैट करके दिखाया जाए, तो यह बहुत अच्छा होगा. शुक्र है कि एक्सटेंशन मेथड की मदद से, हम अपनी ज़रूरत के हिसाब से सटीक मेथड जोड़ सकते हैं.
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.
एक्सटेंशन के इस तरीके में, स्विच एक्सप्रेशन और पैटर्न मैचिंग का इस्तेमाल किया जाता है. इससे, सेकंड से लेकर दिनों तक की अलग-अलग अवधि को दिखाने का सही तरीका चुना जा सकता है. इस तरह के कोड के बारे में ज़्यादा जानने के लिए, डार्ट के पैटर्न और रिकॉर्ड के बारे में ज़्यादा जानें कोडलैब देखें.
- इस नई सुविधा को इंटिग्रेट करने के लिए,
isolates.dart
फ़ाइल को बदलें. इससेexploreCrosswordSolutions
फ़ंक्शन को फिर से इस तरह से तय किया जा सकेगा:
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 से निकाला जाना चाहिए और यूज़र इंटरफ़ेस (यूआई) में दिखाया जाना चाहिए. सबसे पहले, एक नई मॉडल क्लास तय करें. इसमें वह जानकारी शामिल करें जिसे आपको दिखाना है.
शुरू करने के लिए, इन चरणों का पालन करें:
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> {
- फ़ाइल के आखिर में,
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;
isolates.dart
फ़ाइल में बदलाव करके,WorkQueue
मॉडल को इस तरह से दिखाएं:
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}',
);
}
अब बैकग्राउंड आइसोलेट, वर्क क्यू को दिखा रहा है. इसलिए, अब यह सवाल है कि इस डेटा सोर्स से आंकड़े कैसे और कहां निकाले जाएं.
- क्रॉसवर्ड की सुविधा देने वाली पुरानी कंपनी की जगह, वर्क क्यू की सुविधा देने वाली कंपनी को चुनें. इसके बाद, वर्क क्यू की सुविधा देने वाली कंपनी की स्ट्रीम से जानकारी पाने वाली अन्य कंपनियों को जोड़ें:
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/riverpod.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(Ref 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(Ref 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(Ref 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 में अगली पीढ़ी के यूज़र इंटरफ़ेस (यूआई) बनाना कोडलैब में वापस जाकर देखें. यहां इस ज़रूरत के लिए एक काम का विजेट दिया गया है.
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);
}
यह विजेट एक स्लेजहैमर है. यह हर फ़्रेम पर अपने कॉन्टेंट को फिर से बनाता है. आम तौर पर, इसे अच्छा नहीं माना जाता. हालांकि, क्रॉसवर्ड पहेलियों को खोजने के कंप्यूटेशनल लोड की तुलना में, हर फ़्रेम में बीता हुआ समय फिर से पेंट करने का कंप्यूटेशनल लोड शायद न के बराबर हो. इस नई जानकारी का फ़ायदा पाने के लिए, अब एक नया विजेट बनाने का समय है.
- अपनी
lib/widgets
डायरेक्ट्री मेंcrossword_info_widget.dart
फ़ाइल बनाएं. इसके बाद, उसमें यह कॉन्टेंट जोड़ें:
lib/widgets/crossword_info_widget.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../providers.dart';
import '../utils.dart';
import 'ticker_builder.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),
),
],
),
);
}
यह विजेट, Riverpod के प्रोवाइडर की क्षमता का एक बेहतरीन उदाहरण है. जब पांच में से कोई भी प्रोवाइडर अपडेट करेगा, तब इस विजेट को फिर से बनाने के लिए मार्क कर दिया जाएगा. इस चरण में, आखिरी ज़रूरी बदलाव यह है कि इस नए विजेट को यूज़र इंटरफ़ेस (यूआई) में इंटिग्रेट किया जाए.
- अपनी
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(
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( // 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
का होना एक अहम हिस्सा है. TableView की मदद से, इस समस्या की जांच आसानी से की जा सकती है. हम सेल के रंग को इस आधार पर बदल सकते हैं कि वह locationsToTry
में है या नहीं.
शुरू करने के लिए, इन चरणों का पालन करें:
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,
),
),
),
);
}
}
इस कोड को चलाने पर, आपको उन जगहों का विज़ुअलाइज़ेशन दिखेगा जिनकी जांच एल्गोरिदम को अभी करनी है.
क्रॉसवर्ड को पूरा करने के दौरान, यह देखना दिलचस्प होता है कि कई ऐसे पॉइंट हैं जिनकी जांच की जानी बाकी है, लेकिन उनसे कोई फ़ायदा नहीं होगा. यहां दो विकल्प दिए गए हैं. पहला विकल्प यह है कि जब क्रॉस वर्ड के कुछ सेल भर जाएं, तब जांच को सीमित कर दिया जाए. दूसरा विकल्प यह है कि एक ही समय में कई लोकप्रिय जगहों की जांच की जाए. दूसरा रास्ता ज़्यादा मज़ेदार लग रहा है. इसलिए, अब हमें वही करना चाहिए.
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({
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 वर्कर आइसोलेट होते हैं. सबसे अच्छी परफ़ॉर्मेंस पाने के लिए, N को ट्यून करना आपके कंप्यूटर और उस डेटा, दोनों पर निर्भर करता है. ग्रिड जितना बड़ा होगा, उतने ही ज़्यादा वर्कर एक साथ काम कर पाएंगे. इससे उन्हें एक-दूसरे के काम में रुकावट नहीं आएगी.
इस कोड में एक दिलचस्प बदलाव यह है कि अब यह क्लोज़र की उस समस्या को हल करता है जिसमें वे ऐसी चीज़ों को कैप्चर करते हैं जिन्हें उन्हें कैप्चर नहीं करना चाहिए. अब कोई भी लेन बंद नहीं है. _generate
और _generateWorker
फ़ंक्शन को टॉप-लेवल फ़ंक्शन के तौर पर तय किया जाता है. इनके आस-पास कोई ऐसा एनवायरमेंट नहीं होता जिससे ये कैप्चर कर सकें. इन दोनों फ़ंक्शन में आर्ग्युमेंट और नतीजे, Dart रिकॉर्ड के तौर पर होते हैं. यह compute
कॉल के लिए, एक वैल्यू इनपुट करने और एक वैल्यू आउटपुट करने के सिमैंटिक के साथ काम करने का एक तरीका है.
अब आपके पास बैकग्राउंड वर्कर का एक पूल बनाने की सुविधा है. इसकी मदद से, ऐसे शब्दों को खोजा जा सकता है जो ग्रिड में एक-दूसरे से जुड़े होते हैं और एक क्रॉसवर्ड पज़ल बनाते हैं. अब इस सुविधा को क्रॉसवर्ड जनरेटर टूल के बाकी हिस्सों के लिए उपलब्ध कराने का समय आ गया है.
- workQueue provider में इस तरह बदलाव करके,
providers.dart
फ़ाइल में बदलाव करें:
lib/providers.dart
@riverpod
Stream<model.WorkQueue> workQueue(Ref 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();
}
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.
इन दो बदलावों के बाद, प्रोवाइडर लेयर अब बैकग्राउंड आइसोलेट पूल के लिए वर्कर की ज़्यादा से ज़्यादा संख्या सेट करने का तरीका दिखाती है. इससे यह पक्का किया जा सकता है कि आइसोलेट फ़ंक्शन सही तरीके से कॉन्फ़िगर किए गए हों.
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 from here
label: 'Max worker count',
value: workerCount,
), // To here.
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,
),
],
),
),
),
),
),
),
);
}
}
crossword_generator_app.dart
फ़ाइल में बदलाव करें. इसके लिए,_CrosswordGeneratorMenu
विजेट में यह सेक्शन जोड़ें:
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),
),
);
}
अब ऐप्लिकेशन चलाने पर, आपको बैकग्राउंड में इंस्टैंटिएट किए जा रहे आइसोलेट की संख्या में बदलाव करने का विकल्प मिलेगा. इससे, क्रॉस वर्ड में शब्दों को खोजने में मदद मिलेगी.
- क्रॉसवर्ड के साइज़, जनरेट किए गए क्रॉसवर्ड पर आंकड़े दिखाने हैं या नहीं, और अब इस्तेमाल किए जाने वाले आइसोलेट की संख्या के बारे में जानकारी देने वाले कॉन्टेक्स्ट मेन्यू को खोलने के लिए, गियर आइकॉन पर क्लिक करें.
चेकपॉइंट: मल्टी-थ्रेड परफ़ॉर्मेंस
क्रॉसवर्ड जनरेटर को चलाने से, 80x44 क्रॉसवर्ड के लिए कंप्यूटिंग में लगने वाला समय काफ़ी कम हो गया है. ऐसा एक साथ कई कोर का इस्तेमाल करके किया गया है. आपको इन बातों का ध्यान रखना चाहिए:
- ज़्यादा वर्कर की मदद से, तेज़ी से क्रॉसवर्ड जनरेट करना
- जवाब जनरेट होने के दौरान, यूज़र इंटरफ़ेस (यूआई) का आसानी से काम करना
- रीयल-टाइम में दिखने वाले आंकड़े, जनरेशन की प्रोग्रेस दिखाते हैं
- एल्गोरिदम एक्सप्लोरेशन के क्षेत्रों का विज़ुअल फ़ीडबैक
9. इसे गेम में बदलना
हम क्या बना रहे हैं: Playable सुविधा के साथ उपलब्ध एक क्रॉसवर्ड गेम
यह आखिरी सेक्शन, बोनस राउंड है. क्रॉसवर्ड जनरेटर बनाते समय आपने जो भी तकनीकें सीखी हैं उनका इस्तेमाल करके, आपको एक गेम बनाना होगा. आपको:
- पहेलियां जनरेट करना: हल की जा सकने वाली पहेलियां बनाने के लिए, क्रॉस वर्ड जनरेटर का इस्तेमाल करें
- शब्दों के विकल्प बनाना: हर जगह के लिए शब्दों के कई विकल्प दें
- इंटरैक्शन की सुविधा चालू करें: उपयोगकर्ताओं को शब्दों को चुनने और उन्हें जगह पर रखने की अनुमति दें
- जवाबों की पुष्टि करना: देखें कि क्या पूरा किया गया क्रॉसवर्ड सही है
क्रॉसवर्ड पज़ल बनाने के लिए, क्रॉसवर्ड जनरेटर का इस्तेमाल करें. आपको कॉन्टेक्स्ट मेन्यू के मुहावरों का फिर से इस्तेमाल करना होगा, ताकि उपयोगकर्ता ग्रिड में मौजूद अलग-अलग आकार के छेदों में शब्दों को चुन और अनचुने कर सके. इन सभी का मकसद क्रॉसवर्ड पहेली को पूरा करना है.
हम यह नहीं कहेंगे कि यह गेम पूरी तरह से तैयार है. इसमें शब्दों के चुनाव से जुड़ी समस्याएं हैं. इन्हें शब्दों के बेहतर विकल्प चुनकर ठीक किया जा सकता है. उपयोगकर्ताओं को पज़ल में ले जाने के लिए कोई ट्यूटोरियल नहीं है. मैं "आपने जीत लिया!" स्क्रीन के बारे में भी नहीं बताऊँगा.
हालांकि, इस प्रोटोटाइप को पूरी तरह से गेम में बदलने के लिए, ज़्यादा कोड की ज़रूरत होगी. एक कोडलैब में जितना कोड होना चाहिए उससे ज़्यादा कोड. इसलिए, यह एक स्पीड रन वाला चरण है. इसका मकसद, इस कोडलैब में अब तक सीखी गई तकनीकों को मज़बूत करना है. इसके लिए, यह बदला जाता है कि उनका इस्तेमाल कहां और कैसे किया जाता है. हमें उम्मीद है कि इससे आपको इस कोडलैब में पहले सीखे गए सबक को और बेहतर तरीके से समझने में मदद मिलेगी. इसके अलावा, इस कोड के आधार पर अपने अनुभव बनाए जा सकते हैं. हमें यह देखने में खुशी होगी कि आपने क्या बनाया है!
शुरू करने के लिए, इन चरणों का पालन करें:
lib/widgets
डायरेक्ट्री में मौजूद सभी फ़ाइलें मिटा दें. आपको अपने गेम के लिए नए विजेट बनाने होंगे. यह पुराने विजेट से काफ़ी मिलता-जुलता है.
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
को अब भी बेस मॉडल के तौर पर इस्तेमाल किया जा सकता है. यह सिर्फ़ कुछ शब्दों की सूची होती है, जो किसी खास जगह पर और किसी खास दिशा में रखी जाती है.
- अपनी
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/riverpod.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(Ref 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(Ref 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
बनाने में आने वाले खर्च को कम करने के लिए अपनाई गई रणनीतियां और किसी शब्द को चुनने में आने वाला खर्च. इन दोनों कार्रवाइयों को बैकग्राउंड आइसोलेट की मदद के बिना करने पर, यूज़र इंटरफ़ेस (यूआई) के साथ इंटरैक्ट करने में समस्या आती है. बैकग्राउंड में फ़ाइनल नतीजे का हिसाब लगाते समय, बीच के नतीजे को दिखाने के लिए कुछ तरकीबों का इस्तेमाल किया जाता है. इससे आपको रिस्पॉन्सिव यूज़र इंटरफ़ेस मिलता है. वहीं, बैकग्राउंड में ज़रूरी हिसाब-किताब किया जाता है.
- अब खाली हो चुकी
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),
),
);
}
इस फ़ाइल का ज़्यादातर हिस्सा, अब तक आपको समझ आ गया होगा. हां, ऐसे अनडिफ़ाइंड विजेट होंगे जिन्हें अब ठीक किया जाएगा.
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,
),
),
),
);
}
}
यह भी काफ़ी हद तक जाना-पहचाना होना चाहिए. इनमें मुख्य अंतर यह है कि जनरेट किए जा रहे शब्दों के वर्णों को दिखाने के बजाय, अब आपको एक यूनिकोड वर्ण दिखाना होगा. इससे यह पता चलेगा कि कोई वर्ण मौजूद है, लेकिन उसके बारे में जानकारी नहीं है. इसे और बेहतर बनाने के लिए, कुछ और काम करने की ज़रूरत है.
crossword_puzzle_widget.dart
फ़ाइल बनाएं और उसमें यह कॉन्टेंट जोड़ें:
lib/widgets/crossword_puzzle_widget.dart
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),
);
}
}
यह विजेट पिछले विजेट की तुलना में ज़्यादा बेहतर है. हालांकि, इसे उन हिस्सों से बनाया गया है जिनका इस्तेमाल आपने पहले भी किया है. अब, भरी गई हर सेल पर क्लिक करने से एक कॉन्टेक्स्ट मेन्यू दिखता है. इसमें ऐसे शब्दों की सूची होती है जिन्हें उपयोगकर्ता चुन सकता है. अगर शब्दों को चुना गया है, तो मेल न खाने वाले शब्दों को नहीं चुना जा सकता. किसी शब्द से सही का निशान हटाने के लिए, उपयोगकर्ता उस शब्द के मेन्यू आइटम पर टैप करता है.
मान लें कि प्लेयर के पास पूरे क्रॉसवर्ड को भरने के लिए शब्द चुनने का विकल्प है. ऐसे में, आपको "आप जीत गए!" स्क्रीन की ज़रूरत होगी.
- एक
puzzle_completed_widget.dart
फ़ाइल बनाएं. इसके बाद, उसमें यह कॉन्टेंट जोड़ें:
lib/widgets/puzzle_completed_widget.dart
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),
),
);
}
}
हमें भरोसा है कि आप इसे और भी दिलचस्प बना सकते हैं. ऐनिमेशन टूल के बारे में ज़्यादा जानने के लिए, Flutter में नई जनरेशन वाले यूज़र इंटरफ़ेस (यूआई) बनाना कोडलैब देखें.
- अपनी
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(
colorSchemeSeed: Colors.blueGrey,
brightness: Brightness.light,
),
home: CrosswordPuzzleApp(), // Update this line
),
),
);
}
इस ऐप्लिकेशन को चलाने पर, आपको ऐनिमेशन दिखेगा. यह ऐनिमेशन तब दिखेगा, जब क्रॉस वर्ड जनरेटर आपके लिए पज़ल जनरेट करेगा. इसके बाद, आपको एक खाली पज़ल दी जाएगी, जिसे आपको हल करना होगा. मान लें कि आपने समस्या हल कर ली है. इसके बाद, आपको यह स्क्रीन दिखेगी:
10. बधाई हो
बधाई हो! आपने Flutter का इस्तेमाल करके, एक पज़ल गेम बना लिया है!
आपने एक क्रॉस वर्ड जनरेटर बनाया, जो एक पज़ल गेम बन गया. आपने आइसोलेट के पूल में बैकग्राउंड कंप्यूटेशन चलाने में महारत हासिल कर ली है. आपने बैकट्रैकिंग एल्गोरिदम को आसानी से लागू करने के लिए, इम्यूटेबल डेटा स्ट्रक्चर का इस्तेमाल किया. साथ ही, आपने TableView
के साथ अच्छा समय बिताया. यह अगली बार टेबल वाला डेटा दिखाने के लिए आपके काम आएगा.