با فلاتر یک پازل کلمه بسازید

1. قبل از شروع

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

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

انیمیشن یک جدول کلمات متقاطع در حال تولید است.

با استفاده از این ابزار به عنوان پایه، سپس یک جدول کلمات متقاطع ایجاد می کنید که از سازنده جدول کلمات متقاطع برای ساختن پازل برای حل کردن کاربر استفاده می کند. این پازل در اندروید، iOS، ویندوز، macOS و لینوکس قابل استفاده است. اینجا در اندروید است:

عکس صفحه جدول کلمات متقاطع در حال حل شدن در شبیه ساز Pixel Fold.

پیش نیازها

آنچه یاد می گیرید

  • نحوه استفاده از ایزوله‌ها برای انجام کارهای محاسباتی پرهزینه بدون ایجاد مانع در حلقه رندر فلاتر با ترکیبی از تابع compute فلاتر و قابلیت‌های ذخیره ارزش فیلتر بازسازی select Riverpod.
  • چگونه می‌توان از ساختارهای داده تغییرناپذیر با built_value و built_collection استفاده کرد تا تکنیک‌های مبتنی بر جستجوی مبتنی بر هوش مصنوعی قدیمی (GOFAI) مانند جستجوی عمقی و بازگشت به عقب را آسان کنیم.
  • نحوه استفاده از قابلیت های بسته two_dimensional_scrollables برای نمایش داده های شبکه به روشی سریع و شهودی.

آنچه شما نیاز دارید

  • فلاتر SDK .
  • کد ویژوال استودیو (VS Code) با پلاگین های فلاتر و دارت .
  • نرم افزار کامپایلر برای هدف توسعه انتخابی شما. این کد لبه برای تمامی پلتفرم های دسکتاپ، اندروید و iOS کار می کند. برای هدف قرار دادن ویندوز، Xcode برای هدف قرار دادن macOS یا iOS و Android Studio برای هدف قرار دادن اندروید به کد VS نیاز دارید.

2. یک پروژه ایجاد کنید

اولین پروژه فلاتر خود را ایجاد کنید

  1. VS Code را اجرا کنید.
  2. در خط فرمان، flutter new را وارد کنید و سپس Flutter: New Project را در منو انتخاب کنید.

اسکرین شات VS Code با

  1. برنامه Empty را انتخاب کنید و سپس دایرکتوری را انتخاب کنید که در آن پروژه خود را ایجاد کنید. این باید هر دایرکتوری باشد که به امتیازات بالا نیاز نداشته باشد یا فضایی در مسیر خود نداشته باشد. به عنوان مثال می توان به فهرست اصلی یا C:\src\ اشاره کرد.

تصویری از کد VS با برنامه خالی به عنوان بخشی از جریان برنامه جدید انتخاب شده است

  1. نام پروژه خود را generate_crossword بگذارید. باقی‌مانده این کد لبه فرض می‌کند که نام برنامه خود را generate_crossword گذاشته‌اید.

اسکرین شات از VS Code با

Flutter اکنون پوشه پروژه شما را ایجاد می کند و VS Code آن را باز می کند. اکنون محتویات دو فایل را با یک داربست اولیه برنامه بازنویسی می کنید.

برنامه اولیه را کپی و پیست کنید

  1. در قسمت سمت چپ VS Code، روی Explorer کلیک کرده و فایل pubspec.yaml را باز کنید.

تصویری جزئی از VS Code با فلش هایی که محل فایل pubspec.yaml را برجسته می کند

  1. محتوای این فایل را با موارد زیر جایگزین کنید:

pubspec.yaml

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

environment:
  sdk: '>=3.3.3 <4.0.0'

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

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

flutter:
  uses-material-design: true

فایل pubspec.yaml اطلاعات اولیه برنامه شما، مانند نسخه فعلی و وابستگی های آن را مشخص می کند. مجموعه‌ای از وابستگی‌ها را می‌بینید که بخشی از یک برنامه معمولی خالی فلاتر نیستند. شما در مراحل بعدی از تمامی این بسته ها بهره مند خواهید شد.

  1. فایل main.dart را در دایرکتوری lib/ باز کنید.

تصویری جزئی از VS Code با پیکانی که محل فایل main.dart را نشان می‌دهد

  1. محتوای این فایل را با موارد زیر جایگزین کنید:

lib/main.dart

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

void main() {
  runApp(
    ProviderScope(
      child: MaterialApp(
        title: 'Crossword Builder',
        debugShowCheckedModeBanner: false,
        theme: ThemeData(
          useMaterial3: true,
          colorSchemeSeed: Colors.blueGrey,
          brightness: Brightness.light,
        ),
        home: Scaffold(
          body: Center(
            child: Text(
              'Hello, World!',
              style: TextStyle(fontSize: 24),
            ),
          ),
        ),
      ),
    ),
  );
}
  1. این کد را اجرا کنید تا بررسی کنید همه چیز کار می کند. باید یک پنجره جدید با عبارت شروع اجباری هر پروژه جدید در همه جا نمایش دهد. یک ProviderScope وجود دارد که نشان می دهد این برنامه riverpod برای مدیریت حالت استفاده می کند.

یک پنجره برنامه با عبارت "Hello, World!" در مرکز

3. کلمات را اضافه کنید

بلوک های سازنده برای جدول کلمات متقاطع

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

یک منبع خوب برای این کلمات صفحه داده های Corpus زبان طبیعی پیتر نورویگ است. فهرست SOWPODS به عنوان یک نقطه شروع مفید، با 267750 کلمه.

در این مرحله، فهرستی از کلمات را دانلود می‌کنید، آن را به عنوان دارایی به برنامه Flutter خود اضافه می‌کنید و یک ارائه‌دهنده Riverpod ترتیب می‌دهید تا فهرست را در هنگام راه‌اندازی در برنامه بارگیری کند.

برای شروع، این مراحل را دنبال کنید:

  1. فایل pubspec.yaml پروژه خود را تغییر دهید تا اظهارنامه دارایی زیر را برای لیست کلمات انتخابی خود اضافه کنید. این فهرست فقط بند فلاتر پیکربندی برنامه شما را نشان می دهد، زیرا بقیه موارد ثابت مانده است.

pubspec.yaml

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

ویرایشگر شما احتمالاً این خط آخر را با یک هشدار برجسته می کند زیرا هنوز این فایل را ایجاد نکرده اید.

  1. با استفاده از مرورگر و ویرایشگر خود، یک دایرکتوری assets در سطح بالای پروژه خود ایجاد کنید و یک فایل words.txt در آن با یکی از لیست های کلمه لینک شده در بالا ایجاد کنید.

این کد با لیست SOWPODS ذکر شده در بالا طراحی شده است، اما باید با هر لیست کلمه ای که فقط از کاراکترهای AZ تشکیل شده باشد کار کند. گسترش این پایگاه کد برای کار با مجموعه کاراکترهای مختلف به عنوان یک تمرین به خواننده واگذار می شود.

کلمات را بارگذاری کنید

برای نوشتن کد مسئول بارگیری لیست کلمات در راه اندازی برنامه، این مراحل را دنبال کنید:

  1. یک فایل providers.dart در دایرکتوری lib ایجاد کنید.
  2. موارد زیر را به فایل اضافه کنید:

lib/providers.dart

import 'dart:convert';

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

part 'providers.g.dart';

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

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

این اولین ارائه دهنده Riverpod شما برای این پایگاه کد است. متوجه خواهید شد که چندین حوزه وجود دارد که ویرایشگر شما به عنوان یک کلاس تعریف نشده یا یک هدف تولید نشده از آنها شکایت می کند. این پروژه از تولید کد برای وابستگی‌های متعدد، از جمله Riverpod استفاده می‌کند، بنابراین خطاهای کلاس تعریف‌نشده انتظار می‌رود.

  1. برای شروع تولید کد، دستور زیر را اجرا کنید:
$ dart run build_runner watch -d
[INFO] Generating build script completed, took 174ms
[INFO] Setting up file watchers completed, took 5ms
[INFO] Waiting for all file watchers to be ready completed, took 202ms
[INFO] Reading cached asset graph completed, took 65ms
[INFO] Checking for updates since last build completed, took 680ms
[INFO] Running build completed, took 2.3s
[INFO] Caching finalized dependency graph completed, took 42ms
[INFO] Succeeded after 2.3s with 122 outputs (243 actions)

همچنان در پس‌زمینه اجرا می‌شود و با ایجاد تغییرات در پروژه، فایل‌های تولید شده را به‌روزرسانی می‌کند. هنگامی که این دستور کد را در providers.g.dart ایجاد کرد، ویرایشگر شما باید از کدی که در بالا به providers.dart اضافه کردید راضی باشد.

در Riverpod، ارائه دهندگانی مانند تابع wordList که در بالا تعریف کردید، عموماً با تنبلی نمونه‌سازی می‌شوند. با این حال، برای اهداف این برنامه، به فهرست کلمات مشتاقانه نیاز دارید. مستندات Riverpod روش زیر را برای برخورد با ارائه دهندگانی که مشتاقانه به آنها نیاز دارید پیشنهاد می کند. اکنون آن را اجرا خواهید کرد.

  1. یک فایل crossword_generator_app.dart در دایرکتوری lib/widgets ایجاد کنید.
  2. موارد زیر را به فایل اضافه کنید:

lib/widgets/crossword_generator_app.dart

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

import '../providers.dart';

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

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

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

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

این فایل از دو جهت مجزا جالب است. اولین مورد ویجت _EagerInitialization است که تنها وظیفه آن این است که از ارائه‌دهنده wordList که در بالا ایجاد کرده‌اید برای بارگیری فهرست کلمات بخواهد. این ویجت با گوش دادن به ارائه دهنده با استفاده از فراخوانی ref.watch() به این هدف دست می یابد. می توانید اطلاعات بیشتری در مورد این تکنیک در مستندات Riverpod در مورد مقداردهی اولیه Eager ارائه دهندگان بخوانید.

دومین نکته جالب توجه در این فایل این است که Riverpod چگونه محتوای ناهمزمان را مدیریت می کند. همانطور که ممکن است به یاد داشته باشید، ارائه دهنده wordList به عنوان یک تابع ناهمزمان تعریف می شود، زیرا بارگیری محتوا از دیسک کند است. در تماشای ارائه دهنده لیست کلمه در این کد، یک AsyncValue<BuiltSet<String>> دریافت می کنید. بخش AsyncValue از آن نوع، آداپتوری بین دنیای ناهمزمان ارائه دهندگان و دنیای همزمان روش build ویجت است.

متد AsyncValue 's when سه حالت بالقوه را مدیریت می کند که مقدار آینده ممکن است در آن باشد. آینده ممکن است با موفقیت حل شده باشد، در این صورت فراخوانی data فراخوانی می شود، ممکن است در حالت خطا باشد، در این صورت error فراخوانی مجدد است. فراخوانی می شود، یا در نهایت ممکن است هنوز در حال بارگیری باشد. انواع برگشتی از سه فراخوان باید دارای انواع برگشتی سازگار باشند، زیرا برگشت تماس فراخوانی شده توسط متد when برگردانده می شود. در این مثال، نتیجه روش وقتی به عنوان body ویجت Scaffold نمایش داده می شود.

یک برنامه لیست تقریبا بی نهایت ایجاد کنید

برای ادغام ویجت CrosswordGeneratorApp در برنامه خود، این مراحل را دنبال کنید:

  1. فایل lib/main.dart را با افزودن کد زیر به روز کنید:

lib/main.dart

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

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

void main() {
  runApp(
    ProviderScope(
      child: MaterialApp(
        title: 'Crossword Builder',
        debugShowCheckedModeBanner: false,
        theme: ThemeData(
          useMaterial3: true,
          colorSchemeSeed: Colors.blueGrey,
          brightness: Brightness.light,
        ),
        home: CrosswordGeneratorApp(),                     // Remove what was here and replace
      ),
    ),
  );
}
  1. برنامه را مجددا راه اندازی کنید. شما باید یک لیست پیمایشی را ببینید که تقریباً برای همیشه ادامه خواهد داشت.

یک پنجره برنامه با عنوان "Crossword Generator" و لیستی از کلمات

4. کلمات را در یک شبکه نمایش دهید

در این مرحله با استفاده از بسته های built_value و built_collection یک ساختار داده برای ایجاد جدول کلمات متقاطع ایجاد می کنید. این دو بسته ساخت ساختارهای داده را به عنوان مقادیر تغییرناپذیر امکان پذیر می کنند، که هم برای انتقال آسان داده ها بین ایزوله ها مفید خواهد بود و هم اجرای جستجوی عمقی و بازگشت به عقب را بسیار آسان تر می کند.

برای شروع، این مراحل را دنبال کنید:

  1. یک فایل model.dart در پوشه lib ایجاد کنید و سپس محتوای زیر را به فایل اضافه کنید:

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 اضافه می کنید. به عنوان بخشی از ساخت مقدار نهایی شده، یک شبکه از CrosswordCharacter s توسط روش _fillCharacters ایجاد می شود.

برای استفاده از این ساختار داده، مراحل زیر را دنبال کنید:

  1. یک فایل utils در دایرکتوری lib ایجاد کنید و سپس محتوای زیر را به فایل اضافه کنید:

lib/utils.dart

import 'dart:math';

import 'package:built_collection/built_collection.dart';

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

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

این یک افزونه در BuiltSet است که بازیابی یک عنصر تصادفی از مجموعه را بدون دردسر می کند. روش های افزونه گسترش کلاس ها را با قابلیت های اضافی آسان می کند. نامگذاری پسوند برای در دسترس قرار دادن پسوند در خارج از فایل utils.dart ضروری است.

  1. در فایل lib/providers.dart خود، واردات زیر را اضافه کنید:

lib/providers.dart

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

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

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

part 'providers.g.dart';

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

این واردات، مدل تعریف شده در بالا را در اختیار ارائه دهندگانی قرار می دهد که می خواهید ایجاد کنید. وارد کردن dart:math برای Random ، واردات flutter/foundation.dart برای debugPrint ، model.dart برای مدل، و utils.dart برای پسوند BuiltSet گنجانده شده است.

  1. در پایان همان فایل، ارائه دهندگان زیر را اضافه کنید:

lib/providers.dart

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

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

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

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

  @override
  CrosswordSize build() => _size;

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

final _random = Random();

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

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

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

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

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

این تغییرات دو ارائه دهنده را به برنامه شما اضافه می کند. اولین مورد Size است، که در واقع یک متغیر سراسری است که حاوی مقدار انتخاب شده کنونی شمارش CrosswordSize است. این به رابط کاربری اجازه می‌دهد هم اندازه جدول کلمات متقاطع در دست ساخت را نمایش دهد و هم اندازه آن را تنظیم کند. ارائه دهنده دوم، crossword ، خلاقیت جالب تری است. این تابعی است که یک سری Crossword را برمی گرداند. این با استفاده از پشتیبانی Dart برای ژنراتورها ساخته شده است، همانطور که با async* روی تابع مشخص شده است. این بدان معنی است که به جای پایان دادن به یک بازده، یک سری از Crossword به دست می‌آید، راهی بسیار ساده‌تر برای نوشتن محاسباتی که نتایج میانی را برمی‌گرداند.

به دلیل وجود یک جفت تماس ref.watch در شروع عملکرد ارائه دهنده crossword ، جریان Crossword s توسط سیستم Riverpod هر بار که اندازه انتخاب شده جدول متقاطع تغییر می کند و زمانی که فهرست کلمات بارگذاری تمام می شود، مجدداً راه اندازی می شود.

اکنون که کدی برای تولید کلمات متقاطع دارید، البته پر از کلمات تصادفی، بهتر است آنها را به کاربر ابزار نشان دهید.

  1. یک فایل crossword_widget.dart در پوشه lib/widgets با محتوای زیر ایجاد کنید:

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 تکیه کند. نمایش این شبکه با ویجت TableView از بسته two_dimensional_scrollables انجام می شود.

شایان ذکر است که سلول های تکی ارائه شده توسط توابع کمکی _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(),                           // Replaces everything that was here before
        ),
      ),
    );
  }
}

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

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

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

چند چیز در اینجا تغییر کرده است. ابتدا کدی که مسئول رندر wordList به عنوان ListView است با فراخوانی به CrosswordWidget که در فایل قبلی تعریف شده بود جایگزین شد. تغییر عمده دیگر شروع منوی تغییر رفتار برنامه است که با تغییر اندازه جدول کلمات متقاطع شروع می شود. MenuItemButton های بیشتری در مراحل بعدی اضافه خواهند شد. برنامه خود را اجرا کنید، چیزی شبیه به این خواهید دید:

یک پنجره برنامه با عنوان ایجاد کننده کلمات متقاطع و شبکه ای از کاراکترها که به صورت کلمات همپوشانی بدون قافیه یا دلیل قرار گرفته اند.

کاراکترهایی در یک شبکه نمایش داده می شوند و یک منو به کاربر امکان می دهد اندازه شبکه را تغییر دهد. اما کلمات مانند جدول کلمات متقاطع چیده نشده اند. این نتیجه عدم اعمال محدودیت در نحوه افزودن کلمات به جدول کلمات متقاطع است. خلاصه کلافه است. چیزی که در مرحله بعد شروع به تحت کنترل درآوردن آن خواهید کرد!

5. اعمال محدودیت ها

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

برای شروع، این مراحل را دنبال کنید:

  1. فایل model.dart را باز کنید و مدل Crossword را با عبارت زیر جایگزین کنید:

lib/model.dart

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

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

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

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

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

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

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

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

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

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

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

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

    return true;
  }

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

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

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

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

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

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

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

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

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

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

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

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

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

    return buffer.toString();
  }

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

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

به عنوان یک یادآوری سریع، تغییراتی که در فایل‌های model.dart و providers.dart ایجاد می‌کنید، نیازمند اجرای build_runner برای به‌روزرسانی فایل‌های model.g.dart و providers.g.dart مربوطه هستند. اگر این فایل‌ها به‌طور خودکار به‌روزرسانی نشده‌اند، اکنون زمان خوبی برای شروع دوباره build_runner با dart run build_runner watch -d است.

برای استفاده از این قابلیت جدید در لایه مدل، باید لایه ارائه دهنده را برای مطابقت به روز رسانی کنید.

  1. فایل providers.dart خود را به صورت زیر ویرایش کنید:

lib/providers.dart

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

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

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

part 'providers.g.dart';

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

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

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

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

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

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

  @override
  CrosswordSize build() => _size;

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

final _random = Random();

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

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

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

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

      yield crossword;
    },
    error: (error, stackTrace) async* {
      debugPrint('Error loading word list: $error');
      yield crossword;
    },
    loading: () async* {
      yield crossword;
    },
  );
}
  1. برنامه خود را اجرا کنید چیز زیادی در رابط کاربری اتفاق نمی افتد، اما اگر به گزارش ها نگاه کنید، اتفاقات زیادی رخ می دهد.

پنجره برنامه مولد جدول کلمات متقاطع با کلماتی که در دو طرف و پایین قرار گرفته اند و در نقاط تصادفی متقاطع می شوند

اگر به آنچه در اینجا اتفاق می‌افتد فکر کنید، می‌بینیم که جدول کلمات متقاطع به طور تصادفی ظاهر می‌شود. روش addWord در مدل Crossword هر کلمه پیشنهادی را که در جدول کلمات متقاطع فعلی جا نمی‌گیرد را رد می‌کند، بنابراین شگفت‌انگیز است که ما شاهد ظاهر شدن هر چیزی هستیم.

در آماده سازی برای روشمندتر بودن در مورد انتخاب کلماتی که باید در کجا امتحان کنید، انتقال این محاسبات از رشته UI و به یک ایزوله پس زمینه بسیار مفید خواهد بود. Flutter دارای یک پوشش بسیار مفید برای برداشتن یک تکه از کار و اجرای آن در ایزوله پس‌زمینه - تابع compute است.

  1. در فایل providers.dart ، ارائه دهنده جدول کلمات متقاطع را به صورت زیر تغییر دهید:

lib/providers.dart

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

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

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

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

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

این کد کار می کند. با این حال، حاوی یک تله است. اگر این مسیر را ادامه دهید، در نهایت با یک خطای ثبت شده مانند زیر مواجه خواهید شد:

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

این نتیجه بسته شدنی است که compute به ایزوله پس‌زمینه می‌دهد و روی یک ارائه‌دهنده بسته می‌شود، که نمی‌تواند از طریق SendPort.send() ارسال شود. یک راه حل برای این این است که مطمئن شوید چیزی برای بسته شدن روی بسته وجود ندارد که قابل ارسال نباشد.

اولین قدم این است که ارائه دهندگان را از کد Isolate جدا کنید.

  1. یک فایل isolates.dart در دایرکتوری lib خود ایجاد کنید و سپس محتوای زیر را به آن اضافه کنید:

lib/Isolates.dart

import 'dart:math';

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

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

final _random = Random();

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

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

این کد باید کاملا آشنا به نظر برسد. این هسته اصلی آن چیزی است که در ارائه دهنده crossword وجود داشت، اما اکنون به عنوان یک تابع تولید کننده مستقل. اکنون می توانید فایل providers.dart خود را به روز کنید تا از این تابع جدید برای نمونه سازی جداسازی پس زمینه استفاده کنید.

lib/providers.dart

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

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

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

part 'providers.g.dart';

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

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

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

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

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

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

  @override
  CrosswordSize build() => _size;

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

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

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

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

6. صف کار را مدیریت کنید

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

کد در حال حاضر راه‌حل‌های نامزد را می‌سازد، بررسی می‌کند که آیا راه‌حل کاندید معتبر است یا خیر، و بسته به اعتبار، یا نامزد را ترکیب می‌کند یا آن را دور می‌اندازد. این یک نمونه پیاده‌سازی از خانواده الگوریتم‌های Backtracking است. این پیاده‌سازی به‌وسیله‌ی built_value و built_collection بسیار آسان‌تر می‌شود، که امکان ایجاد مقادیر تغییرناپذیر جدید را فراهم می‌کند که از حالت مشترک با مقدار تغییرناپذیری که از آن مشتق شده‌اند، به اشتراک می‌گذارند. این امکان بهره برداری ارزان از نامزدهای بالقوه را بدون هزینه حافظه مورد نیاز برای کپی عمیق فراهم می کند.

برای شروع، این مراحل را دنبال کنید:

  1. فایل model.dart را باز کنید و تعریف WorkQueue زیر را به آن اضافه کنید:

lib/model.dart

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

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

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

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

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

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

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

          b.crossword.replace(crossword);

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

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

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

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

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

/// Construct the serialization/deserialization code for the data model.
@SerializersFor([
  Location,
  Crossword,
  CrosswordWord,
  CrosswordCharacter,
  WorkQueue,                                               // Add this line
])
final Serializers serializers = _$serializers;
  1. اگر پس از افزودن این محتوای جدید برای بیش از چند ثانیه، squiggles قرمز در این فایل باقی مانده است، تأیید کنید که build_runner شما همچنان در حال اجرا است. اگر نه، دستور dart run build_runner watch -d اجرا کنید.

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

  1. فایل utils.dart را به صورت زیر ویرایش کنید:

lib/utils.dart

import 'dart:math';

import 'package:built_collection/built_collection.dart';

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

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

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

  1. برای ادغام این قابلیت جدید، فایل 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 در 44 است که در 1 دقیقه و 29 ثانیه ایجاد شده است.

سازنده کلمات متقاطع، با تعداد زیادی کلمات متقاطع. بزرگ‌نمایی شد، کلمات برای خواندن خیلی کوچک هستند.

البته سوال واضح این است که آیا می‌توانیم سریع‌تر برویم؟ اوه بله، بله ما می توانیم.

7. آمار سطح

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

اطلاعاتی که نمایش می دهید باید از WorkQueue استخراج شده و در UI نمایش داده شوند.

اولین قدم مفید، تعریف یک کلاس مدل جدید است که حاوی اطلاعاتی است که می خواهید نمایش دهید.

برای شروع، این مراحل را دنبال کنید:

  1. فایل model.dart را به صورت زیر ویرایش کنید تا کلاس DisplayInfo را اضافه کنید:

lib/model.dart

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

part 'model.g.dart';

/// A location in a crossword.
abstract class Location implements Built<Location, LocationBuilder> {
  1. در انتهای فایل، تغییرات زیر را برای اضافه کردن کلاس DisplayInfo اعمال کنید:

lib/model.dart

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

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

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

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

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

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

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

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

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

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

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

/// Construct the serialization/deserialization code for the data model.
@SerializersFor([
  Location,
  Crossword,
  CrosswordWord,
  CrosswordCharacter,
  WorkQueue,
  DisplayInfo,                                             // Add this line.
])
final Serializers serializers = _$serializers;
  1. فایل 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}');
}

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

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

lib/providers.dart

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

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

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

part 'providers.g.dart';

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

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

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

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

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

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

  @override
  CrosswordSize build() => _size;

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

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

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

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

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

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

  DateTime? _start;

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

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

  DateTime? _end;

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

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

const _estimatedTotalCoverage = 0.54;

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

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

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

  @override
  bool build() => _display;

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

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

ارائه دهندگان جدید ترکیبی از وضعیت جهانی هستند، به این شکل که آیا نمایش اطلاعات باید در بالای جدول جدول کلمات متقاطع قرار گیرد یا نه، و داده های مشتق شده مانند زمان اجرای تولید جدول کلمات متقاطع. همه اینها با این واقعیت پیچیده می شود که شنوندگان برخی از این حالت ها گذرا هستند. اگر نمایش اطلاعات پنهان باشد، هیچ چیز به زمان شروع و پایان محاسبه جدول کلمات متقاطع گوش نمی دهد، اما اگر قرار است محاسبه هنگام نمایش اطلاعات نمایش داده شود، باید در حافظه بماند. پارامتر keepAlive ویژگی Riverpod در این مورد بسیار مفید است.

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

  1. یک فایل ticker_builder.dart در دایرکتوری lib/widgets ایجاد کنید و سپس محتوای زیر را به آن اضافه کنید:

lib/widgets/ticker_builder.dart

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

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

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

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

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

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

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

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

  1. یک فایل crossword_info_widget.dart در دایرکتوری lib/widgets خود ایجاد کنید و سپس محتوای زیر را به آن اضافه کنید:

lib/widgets/crossword_info_widget.dart

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

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

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

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

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

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

این ویجت نمونه بارز قدرت ارائه دهندگان Riverpod است. هنگامی که هر یک از پنج ارائه دهنده به روز می شوند، این ویجت برای بازسازی علامت گذاری می شود. آخرین تغییر مورد نیاز در این مرحله، ادغام این ویجت جدید در رابط کاربری است.

  1. فایل crossword_generator_app.dart خود را به صورت زیر ویرایش کنید:

lib/widgets/crossword_generator_app.dart

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

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

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

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

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

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

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

دو تغییر در اینجا رویکردهای متفاوتی را برای یکپارچه سازی ارائه دهندگان نشان می دهد. در روش build CrosswordGeneratorApp ، شما یک Consumer builder جدید معرفی کردید که در صورت نمایش یا پنهان شدن نمایشگر اطلاعات، ناحیه‌ای را که مجبور به بازسازی می‌شود را در بر می‌گیرد. از سوی دیگر، کل منوی کشویی یک ConsumerWidget است که خواه تغییر اندازه جدول کلمات متقاطع یا نمایش یا پنهان کردن نمایشگر اطلاعات باشد، بازسازی می شود. هر رویکردی که باید اتخاذ شود همیشه یک موازنه مهندسی سادگی در مقابل هزینه محاسبه مجدد طرح‌بندی درختان ویجت بازسازی‌شده است.

اکنون اجرای برنامه به کاربر بینش بیشتری در مورد چگونگی پیشرفت تولید جدول کلمات متقاطع می دهد. با این حال، نزدیک به پایان تولید جدول کلمات متقاطع، دوره ای وجود دارد که اعداد در حال تغییر هستند، اما تغییر بسیار کمی در شبکه کاراکترها وجود دارد.

پنجره برنامه مولد جدول کلمات متقاطع، این بار کلمات کوچکتر و قابل تشخیص، و یک پوشش شناور در گوشه پایین سمت راست با آمار مربوط به اجرای نسل فعلی

دریافت بینش بیشتر در مورد اینکه چه اتفاقی می افتد و چرا می افتد مفید خواهد بود.

8. با نخ ها موازی کنید

برای درک اینکه چرا کارها در انتها کند می شوند، مفید است که بتوانید کاری که الگوریتم انجام می دهد را تجسم کنید. یکی از بخش‌های کلیدی locationsToTry بی‌نظیر برای امتحان در WorkQueue است. TableView راه مفیدی برای بررسی این موضوع به ما می دهد. ما می توانیم رنگ سلول را بر اساس اینکه آیا در locationsToTry است تغییر دهیم.

برای شروع، این مراحل را دنبال کنید:

  1. فایل crossword_widget.dart را به صورت زیر تغییر دهید:

lib/widgets/crossword_widget.dart

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

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

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

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

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

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

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

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

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

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

هنگامی که این کد را اجرا می کنید، تصویری از مکان های برجسته را مشاهده خواهید کرد که الگوریتم هنوز باید آنها را بررسی کند.

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

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

  1. فایل isolates.dart را ویرایش کنید. این تقریباً یک بازنویسی کامل از کد است تا آنچه را که در یک ایزوله پس‌زمینه محاسبه می‌شد به مجموعه‌ای از N ایزوله پس‌زمینه تقسیم کند.

lib/Isolates.dart

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

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

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

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

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

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

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

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

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

  return workQueue;
}

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

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

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

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

  return (location, direction, null);
}

بیشتر این کد باید آشنا باشد زیرا منطق اصلی تجارت تغییر نکرده است. چیزی که تغییر کرده این است که اکنون دو لایه از تماس های compute وجود دارد. لایه اول مسئول ایجاد موقعیت‌های فردی برای جستجوی N ایزوله کارگر است، و سپس با پایان یافتن تمام N worker، نتایج را دوباره ترکیب می‌کند. لایه دوم از جدایه های N کارگر تشکیل شده است. تنظیم N برای دریافت بهترین عملکرد هم به رایانه و هم به داده های مورد نظر بستگی دارد. هرچه شبکه بزرگ‌تر باشد، کارگران بیشتری می‌توانند بدون اینکه در مسیر یکدیگر قرار بگیرند، با هم کار کنند.

یکی از نکات جالب توجه این است که چگونه این کد اکنون با موضوع بسته شدن مواردی که نباید ضبط کنند، برخورد می کند. اکنون هیچ تعطیلی وجود ندارد. توابع _generate و _generateWorker به عنوان توابع سطح بالا تعریف می‌شوند که هیچ محیط اطراف برای گرفتن عکس ندارند. آرگومان ها و نتایج حاصل از هر دوی این توابع به شکل رکوردهای دارت هستند. این یک راه آسان برای کار در اطراف معنایی یک مقدار در، یک ارزش خارج از فراخوانی compute است.

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

  1. فایل providers.dart را با ویرایش ارائه دهنده workQueue به صورت زیر ویرایش کنید:

lib/providers.dart

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

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

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

  ref.read(endTimeProvider.notifier).end();
}
  1. ارائه دهنده WorkerCount را به صورت زیر به انتهای فایل اضافه کنید:

lib/providers.dart

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

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

  const BackgroundWorkers(this.count);

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

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

  @override
  BackgroundWorkers build() => _count;

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

با این دو تغییر، لایه ارائه‌دهنده اکنون راهی را برای تنظیم حداکثر تعداد کارگران برای استخر جداسازی پس‌زمینه به گونه‌ای که توابع جداسازی به درستی پیکربندی شده‌اند، نشان می‌دهد.

  1. فایل crossword_info_widget.dart را با تغییر CrosswordInfoWidget به صورت زیر به روز کنید:

lib/widgets/crossword_info_widget.dart

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

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

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

اگر اکنون برنامه را اجرا کنید، می‌توانید تعداد جداشده‌های پس‌زمینه را برای جستجوی کلماتی که در جدول کلمات متقاطع قرار می‌گیرند، تغییر دهید.

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

پنجره ژنراتور جدول کلمات متقاطع با کلمات و آمار

اجرای مولد جدول کلمات متقاطع به طور قابل توجهی زمان محاسبه جدول کلمات متقاطع 80x44 را با استفاده از چندین هسته به طور همزمان کاهش داده است.

9. آن را به یک بازی تبدیل کنید

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

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

معامله در اینجا این است که برای صیقل دادن صحیح این بازی اولیه به یک بازی کامل، به میزان قابل توجهی کد بیشتری نیاز دارد. کد بیشتر از آنچه که باید در یک Codelab واحد باشد. بنابراین، در عوض، این یک مرحله اجرا با سرعت است که برای تقویت تکنیک‌های آموخته‌شده تاکنون در این آزمایشگاه کد با تغییر مکان و نحوه استفاده از آنها طراحی شده است. امیدواریم این درس‌هایی را که قبلاً در این نرم‌افزار آموخته‌ایم، تقویت کند. از طرف دیگر، می توانید ادامه دهید و تجربیات خود را بر اساس این کد بسازید. ما دوست داریم ببینیم چه چیزی می سازید!

برای شروع، این مراحل را دنبال کنید:

  1. همه موارد موجود در فهرست lib/widgets را حذف کنید. شما ابزارک های جدید و درخشانی را برای بازی خود ایجاد خواهید کرد. اتفاقاً چیزهای زیادی از ویجت های قدیمی قرض می گیرد.
  2. فایل model.dart خود را برای به روز رسانی متد addWord Crossword به صورت زیر ویرایش کنید:

lib/model.dart

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

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

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

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

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

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

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

  1. کلاس مدل CrosswordPuzzleGame را به انتهای فایل model.dart خود اضافه کنید.

lib/model.dart

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

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

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

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

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

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

    var puzzle = this;

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

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

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

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

    var puzzle = this;

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

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

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

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

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

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

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

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

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

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

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

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

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

به‌روزرسانی‌های فایل providers.dart یک بسته جالب از تغییرات است. اکثر ارائه دهندگانی که برای پشتیبانی از جمع آوری آمار حضور داشتند حذف شده اند. قابلیت تغییر تعداد ایزوله های پس زمینه حذف شده و با یک ثابت جایگزین شده است. همچنین ارائه‌دهنده جدیدی وجود دارد که به مدل جدید CrosswordPuzzleGame که در بالا اضافه کرده‌اید، دسترسی می‌دهد.

lib/providers.dart

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

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

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

part 'providers.g.dart';

const backgroundWorkerCount = 4;                           // Add this line

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

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

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

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

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

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

  @override
  CrosswordSize build() => _size;

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

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

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

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

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

    return _puzzle;
  }

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

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

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

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

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

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

جالب‌ترین بخش‌های ارائه‌دهنده Puzzle ، استراتژی‌هایی است که برای پنهان کردن هزینه ایجاد CrosswordPuzzleGame از Crossword و wordList و هزینه انتخاب یک کلمه انجام می‌شود. هر دوی این اقدامات زمانی که بدون کمک پس‌زمینه ایزوله انجام می‌شوند، باعث ایجاد تعامل کند با رابط کاربری می‌شوند. با استفاده از کمی دقت برای بیرون راندن یک نتیجه میانی در حین محاسبه نتیجه نهایی در پس‌زمینه، در حالی که محاسبات مورد نیاز در پس‌زمینه انجام می‌شوند، با یک رابط کاربری پاسخگو مواجه می‌شوید.

  1. در پوشه lib/widgets که اکنون خالی است، یک فایل crossword_puzzle_app.dart با محتوای زیر ایجاد کنید:

lib/widgets/crossword_puzzle_app.dart

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

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

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

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

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

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

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

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

بیشتر این فایل باید تا به حال نسبتاً آشنا باشد. بله، ویجت های تعریف نشده ای وجود خواهد داشت که اکنون شروع به تعمیر آنها خواهید کرد.

  1. یک فایل crossword_generator_widget.dart ایجاد کنید و محتوای زیر را به آن اضافه کنید:

lib/widgets/crossword_generator_widget.dart

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

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

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

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

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

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

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

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

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

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

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

  1. فایل crossword_puzzle_widget.dart ایجاد کنید و محتوای زیر را به آن اضافه کنید:

lib/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),
    );
  }
}

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

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

  1. یک فایل 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 codelab مراجعه کنید.

  1. فایل lib/main.dart خود را به صورت زیر ویرایش کنید:

lib/main.dart

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

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

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

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

پنجره برنامه جدول کلمات متقاطع که متن "Puzzle تکمیل شد!"

10. تبریک می گویم

تبریک می گویم! شما موفق به ساخت یک بازی پازل با فلاتر شدید!

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

بیشتر بدانید