Skip to content

Commit

Permalink
feat: add puzzle mode, repository, and cache
Browse files Browse the repository at this point in the history
  • Loading branch information
thisissandipp committed Jul 19, 2024
1 parent 4392814 commit 00fb889
Show file tree
Hide file tree
Showing 10 changed files with 297 additions and 0 deletions.
23 changes: 23 additions & 0 deletions lib/cache/cache.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/// {@template cache_client}
/// An in-memory cache client.
/// {@endtemplate}
class CacheClient {
/// {@macro cache_client}
CacheClient() : _cache = <String, Object>{};

final Map<String, Object> _cache;

/// Writes the [key] and [value] pair into the cache.
void write<T extends Object>({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<T extends Object>({required String key}) {
final value = _cache[key];
if (value is T) return value;
return null;
}
}
2 changes: 2 additions & 0 deletions lib/puzzle/models/models.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export 'puzzle.dart';
export 'puzzle_status.dart';
69 changes: 69 additions & 0 deletions lib/puzzle/models/puzzle.dart
Original file line number Diff line number Diff line change
@@ -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<Object?> 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,
);
}
}
14 changes: 14 additions & 0 deletions lib/puzzle/models/puzzle_status.dart
Original file line number Diff line number Diff line change
@@ -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,
}
2 changes: 2 additions & 0 deletions lib/puzzle/puzzle.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export 'models/models.dart';
export 'repository/puzzle_repository.dart';
35 changes: 35 additions & 0 deletions lib/puzzle/repository/puzzle_repository.dart
Original file line number Diff line number Diff line change
@@ -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<Puzzle>(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<Puzzle>(key: kPuzzleKey, value: puzzle);
}
18 changes: 18 additions & 0 deletions test/cache/cache_test.dart
Original file line number Diff line number Diff line change
@@ -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<String>(key: key), isNull);
cache.write<String>(key: key, value: value);
expect(cache.read<String>(key: key), equals(value));
});
});
}
6 changes: 6 additions & 0 deletions test/helpers/mocks.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -26,3 +28,7 @@ class MockSudokuAPI extends Mock implements SudokuAPI {}
class MockHomeBloc extends MockBloc<HomeEvent, HomeState> implements HomeBloc {}

class MockDio extends Mock implements Dio {}

class MockCacheClient extends Mock implements CacheClient {}

class MockPuzzle extends Mock implements Puzzle {}
86 changes: 86 additions & 0 deletions test/puzzle/models/puzzle_test.dart
Original file line number Diff line number Diff line change
@@ -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(<Object?>[
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,
),
),
);
});
});
});
}
42 changes: 42 additions & 0 deletions test/puzzle/repository/puzzle_repository_test.dart
Original file line number Diff line number Diff line change
@@ -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<Puzzle>(key: PuzzleRepository.kPuzzleKey),
).called(1);
});
});

group('storePuzzle', () {
test('calls the write method from [CacheClient]', () {
puzzleRepository.storePuzzle(puzzle: puzzle);
verify(
() => cacheClient.write<Puzzle>(
key: PuzzleRepository.kPuzzleKey,
value: puzzle,
),
).called(1);
});
});
});
}

0 comments on commit 00fb889

Please sign in to comment.