From 00fb8898839b826157f7b0c436126758febd276e Mon Sep 17 00:00:00 2001 From: Sandip Date: Sat, 20 Jul 2024 00:47:25 +0530 Subject: [PATCH] feat: add puzzle mode, repository, and cache --- lib/cache/cache.dart | 23 +++++ lib/puzzle/models/models.dart | 2 + lib/puzzle/models/puzzle.dart | 69 +++++++++++++++ lib/puzzle/models/puzzle_status.dart | 14 +++ lib/puzzle/puzzle.dart | 2 + lib/puzzle/repository/puzzle_repository.dart | 35 ++++++++ test/cache/cache_test.dart | 18 ++++ test/helpers/mocks.dart | 6 ++ test/puzzle/models/puzzle_test.dart | 86 +++++++++++++++++++ .../repository/puzzle_repository_test.dart | 42 +++++++++ 10 files changed, 297 insertions(+) create mode 100644 lib/cache/cache.dart create mode 100644 lib/puzzle/models/models.dart create mode 100644 lib/puzzle/models/puzzle.dart create mode 100644 lib/puzzle/models/puzzle_status.dart create mode 100644 lib/puzzle/puzzle.dart create mode 100644 lib/puzzle/repository/puzzle_repository.dart create mode 100644 test/cache/cache_test.dart create mode 100644 test/puzzle/models/puzzle_test.dart create mode 100644 test/puzzle/repository/puzzle_repository_test.dart diff --git a/lib/cache/cache.dart b/lib/cache/cache.dart new file mode 100644 index 0000000..48a3478 --- /dev/null +++ b/lib/cache/cache.dart @@ -0,0 +1,23 @@ +/// {@template cache_client} +/// An in-memory cache client. +/// {@endtemplate} +class CacheClient { + /// {@macro cache_client} + CacheClient() : _cache = {}; + + final Map _cache; + + /// Writes the [key] and [value] pair into the cache. + void write({required String key, required T value}) { + _cache[key] = value; + } + + /// Find the value for the specified key. + /// + /// If the value is not found, it returns `null`. + T? read({required String key}) { + final value = _cache[key]; + if (value is T) return value; + return null; + } +} diff --git a/lib/puzzle/models/models.dart b/lib/puzzle/models/models.dart new file mode 100644 index 0000000..82ef986 --- /dev/null +++ b/lib/puzzle/models/models.dart @@ -0,0 +1,2 @@ +export 'puzzle.dart'; +export 'puzzle_status.dart'; diff --git a/lib/puzzle/models/puzzle.dart b/lib/puzzle/models/puzzle.dart new file mode 100644 index 0000000..bbd7246 --- /dev/null +++ b/lib/puzzle/models/puzzle.dart @@ -0,0 +1,69 @@ +// ignore_for_file: public_member_api_docs, sort_constructors_first +import 'package:equatable/equatable.dart'; +import 'package:flutter/widgets.dart'; +import 'package:sudoku/models/models.dart'; + +/// {@template puzzle} +/// Defines the model for a [Sudoku] puzzle. +/// {@endtemplate} +@immutable +class Puzzle extends Equatable { + /// {@macro puzzle} + const Puzzle({ + required this.sudoku, + required this.difficulty, + this.totalSecondsElapsed = 0, + this.remainingMistakes = 3, + this.remainingHints = 3, + }); + + /// Sudoku for this puzzle. + final Sudoku sudoku; + + /// The difficulty of the puzzle. + final Difficulty difficulty; + + /// Total seconds elapsed on this game. + /// + /// Default is set to 0. + final int totalSecondsElapsed; + + /// Defines the remaining mistakes available for this sudoku. + /// + /// Default is set to 3. + final int remainingMistakes; + + /// Defines the remaining hints available for this puzzle. + /// + /// Defaults to 3. + final int remainingHints; + + @override + List get props => [ + sudoku, + difficulty, + totalSecondsElapsed, + remainingMistakes, + remainingHints, + ]; + + /// Returns an updated copy of this [Puzzle] with the + /// updated parameters. + /// + /// {@macro puzzle} + Puzzle copyWith({ + Sudoku? sudoku, + Difficulty? difficulty, + int? totalSecondsElapsed, + int? remainingMistakes, + int? remainingHints, + }) { + return Puzzle( + sudoku: sudoku ?? this.sudoku, + difficulty: difficulty ?? this.difficulty, + totalSecondsElapsed: totalSecondsElapsed ?? this.totalSecondsElapsed, + remainingMistakes: remainingMistakes ?? this.remainingMistakes, + remainingHints: remainingHints ?? this.remainingHints, + ); + } +} diff --git a/lib/puzzle/models/puzzle_status.dart b/lib/puzzle/models/puzzle_status.dart new file mode 100644 index 0000000..75b99b0 --- /dev/null +++ b/lib/puzzle/models/puzzle_status.dart @@ -0,0 +1,14 @@ +/// Defines the status of the puzzle. +enum PuzzleStatus { + /// The puzzle is in progress. + incomplete, + + /// The puzzle is successfully solved. + complete, + + /// The puzzle is over. + /// + /// One reason could be the user has exhausted all + /// the remaining mistakes. + failed, +} diff --git a/lib/puzzle/puzzle.dart b/lib/puzzle/puzzle.dart new file mode 100644 index 0000000..4cb881a --- /dev/null +++ b/lib/puzzle/puzzle.dart @@ -0,0 +1,2 @@ +export 'models/models.dart'; +export 'repository/puzzle_repository.dart'; diff --git a/lib/puzzle/repository/puzzle_repository.dart b/lib/puzzle/repository/puzzle_repository.dart new file mode 100644 index 0000000..4af8080 --- /dev/null +++ b/lib/puzzle/repository/puzzle_repository.dart @@ -0,0 +1,35 @@ +import 'package:flutter/widgets.dart'; +import 'package:sudoku/cache/cache.dart'; +import 'package:sudoku/puzzle/puzzle.dart'; + +/// {@template puzzle_repository} +/// A repository that handles `puzzle` related data. +/// +/// Used to pass data from home page to puzzle page. +/// {@endtemplate} +class PuzzleRepository { + /// {@macro puzzle_repository} + const PuzzleRepository({ + required CacheClient cacheClient, + }) : _cacheClient = cacheClient; + + final CacheClient _cacheClient; + + /// The key used for storing the puzzle in-memory. + /// + /// This is only exposed for testing and shouldn't be used by consumers of + /// this library. + @visibleForTesting + static const kPuzzleKey = '__puzzle_key__'; + + /// Provides the puzzle stored in-memory. + /// + /// Returns null, if there is no puzzle. + Puzzle? getPuzzle() => _cacheClient.read(key: kPuzzleKey); + + /// Saves a puzzle in-memory. + /// + /// If there's already a puzzle there, it will be replaced. + void storePuzzle({required Puzzle puzzle}) => + _cacheClient.write(key: kPuzzleKey, value: puzzle); +} diff --git a/test/cache/cache_test.dart b/test/cache/cache_test.dart new file mode 100644 index 0000000..a474563 --- /dev/null +++ b/test/cache/cache_test.dart @@ -0,0 +1,18 @@ +// ignore_for_file: prefer_const_constructors + +import 'package:flutter_test/flutter_test.dart'; +import 'package:sudoku/cache/cache.dart'; + +void main() { + group('CacheClient', () { + test('can read and write value for a given key', () { + final cache = CacheClient(); + const key = '__key__'; + const value = '__value__'; + + expect(cache.read(key: key), isNull); + cache.write(key: key, value: value); + expect(cache.read(key: key), equals(value)); + }); + }); +} diff --git a/test/helpers/mocks.dart b/test/helpers/mocks.dart index ffa4cb0..71d0794 100644 --- a/test/helpers/mocks.dart +++ b/test/helpers/mocks.dart @@ -2,8 +2,10 @@ import 'package:bloc_test/bloc_test.dart'; import 'package:dio/dio.dart'; import 'package:mocktail/mocktail.dart'; import 'package:sudoku/api/api.dart'; +import 'package:sudoku/cache/cache.dart'; import 'package:sudoku/home/home.dart'; import 'package:sudoku/models/models.dart'; +import 'package:sudoku/puzzle/puzzle.dart'; import 'package:sudoku/sudoku/sudoku.dart'; import 'package:sudoku/timer/timer.dart'; @@ -26,3 +28,7 @@ class MockSudokuAPI extends Mock implements SudokuAPI {} class MockHomeBloc extends MockBloc implements HomeBloc {} class MockDio extends Mock implements Dio {} + +class MockCacheClient extends Mock implements CacheClient {} + +class MockPuzzle extends Mock implements Puzzle {} diff --git a/test/puzzle/models/puzzle_test.dart b/test/puzzle/models/puzzle_test.dart new file mode 100644 index 0000000..e717a1f --- /dev/null +++ b/test/puzzle/models/puzzle_test.dart @@ -0,0 +1,86 @@ +// ignore_for_file: prefer_const_constructors, avoid_redundant_argument_values + +import 'package:flutter_test/flutter_test.dart'; +import 'package:sudoku/models/models.dart'; +import 'package:sudoku/puzzle/puzzle.dart'; + +void main() { + group('Puzzle', () { + Puzzle createSubject({ + Sudoku? sudoku, + Difficulty? difficulty, + int? totalSecondsElapsed, + int? remainingMistakes, + int? remainingHints, + }) { + return Puzzle( + sudoku: sudoku ?? Sudoku(blocks: const []), + difficulty: difficulty ?? Difficulty.medium, + totalSecondsElapsed: totalSecondsElapsed ?? 0, + remainingMistakes: remainingMistakes ?? 2, + remainingHints: remainingHints ?? 1, + ); + } + + test('constructor works correctly', () { + expect(createSubject, returnsNormally); + }); + + test('supports value equality', () { + expect(createSubject(), equals(createSubject())); + }); + + test('props are correct', () { + expect( + createSubject().props, + equals([ + Sudoku(blocks: const []), + Difficulty.medium, + 0, + 2, + 1, + ]), + ); + }); + + group('copyWith', () { + test('returns same object if no argument is passed', () { + expect(createSubject().copyWith(), equals(createSubject())); + }); + + test('returns the old value for each parameter if null is provided', () { + expect( + createSubject().copyWith( + sudoku: null, + difficulty: null, + totalSecondsElapsed: null, + remainingMistakes: null, + remainingHints: null, + ), + equals(createSubject()), + ); + }); + + test('returns the updated copy of this for every non-null parameter', () { + expect( + createSubject().copyWith( + sudoku: Sudoku(blocks: const []), + difficulty: Difficulty.expert, + totalSecondsElapsed: 15, + remainingMistakes: 1, + remainingHints: 0, + ), + equals( + createSubject( + sudoku: Sudoku(blocks: const []), + difficulty: Difficulty.expert, + totalSecondsElapsed: 15, + remainingMistakes: 1, + remainingHints: 0, + ), + ), + ); + }); + }); + }); +} diff --git a/test/puzzle/repository/puzzle_repository_test.dart b/test/puzzle/repository/puzzle_repository_test.dart new file mode 100644 index 0000000..4ea091b --- /dev/null +++ b/test/puzzle/repository/puzzle_repository_test.dart @@ -0,0 +1,42 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:sudoku/cache/cache.dart'; +import 'package:sudoku/puzzle/puzzle.dart'; + +import '../../helpers/helpers.dart'; + +void main() { + group('PuzzleRepository', () { + late Puzzle puzzle; + late CacheClient cacheClient; + late PuzzleRepository puzzleRepository; + + setUp(() { + puzzle = MockPuzzle(); + cacheClient = MockCacheClient(); + puzzleRepository = PuzzleRepository(cacheClient: cacheClient); + when(() => cacheClient.read(key: any(named: 'key'))).thenReturn(puzzle); + }); + + group('getPuzzle', () { + test('calls the read method from [CacheClient]', () { + puzzleRepository.getPuzzle(); + verify( + () => cacheClient.read(key: PuzzleRepository.kPuzzleKey), + ).called(1); + }); + }); + + group('storePuzzle', () { + test('calls the write method from [CacheClient]', () { + puzzleRepository.storePuzzle(puzzle: puzzle); + verify( + () => cacheClient.write( + key: PuzzleRepository.kPuzzleKey, + value: puzzle, + ), + ).called(1); + }); + }); + }); +}