Skip to content

Commit d3e172a

Browse files
committed
feat: add sudoku bloc, and few helper methods
1 parent c42e8aa commit d3e172a

File tree

9 files changed

+615
-0
lines changed

9 files changed

+615
-0
lines changed

lib/models/sudoku.dart

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,26 @@ class Sudoku extends Equatable {
157157
return blocks.where((e) => e.position.y == block.position.y).toList();
158158
}
159159

160+
/// Updates the [blockToUpdate] with the [value], and returns a new instance
161+
/// of the [Sudoku].
162+
Sudoku updateBlock(Block blockToUpdate, int value) {
163+
final mutableBlocks = [...blocks];
164+
final indexToReplace = mutableBlocks.indexWhere(
165+
(block) => block.position == blockToUpdate.position,
166+
);
167+
168+
final removedBlock = mutableBlocks.removeAt(indexToReplace);
169+
final updatedBlock = removedBlock.copyWith(currentValue: value);
170+
171+
mutableBlocks.insert(indexToReplace, updatedBlock);
172+
return Sudoku(blocks: mutableBlocks);
173+
}
174+
175+
/// Determines if the sudoku is complete
176+
bool isComplete() {
177+
return blocks.every((block) => block.correctValue == block.currentValue);
178+
}
179+
160180
@override
161181
List<Object?> get props => [blocks];
162182
}

lib/sudoku/bloc/sudoku_bloc.dart

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import 'package:bloc/bloc.dart';
2+
import 'package:equatable/equatable.dart';
3+
import 'package:sudoku/models/models.dart';
4+
5+
part 'sudoku_event.dart';
6+
part 'sudoku_state.dart';
7+
8+
class SudokuBloc extends Bloc<SudokuEvent, SudokuState> {
9+
SudokuBloc({required Sudoku sudoku}) : super(SudokuState(sudoku: sudoku)) {
10+
on<SudokuBlockSelected>(_onBlockSelected);
11+
on<SudokuInputTapped>(_onInputTapped);
12+
}
13+
14+
void _onBlockSelected(SudokuBlockSelected event, Emitter<SudokuState> emit) {
15+
if (event.block.isGenerated) {
16+
emit(
17+
state.copyWith(
18+
blockSelectionStatus: () => BlockSelectionStatus.cannotBeSelected,
19+
currentSelectedBlock: () => null,
20+
highlightedBlocks: () => {},
21+
),
22+
);
23+
} else {
24+
final subGridBlocks = state.sudoku.getSubGridBlocks(event.block);
25+
final rowBlocks = state.sudoku.getRowBlocks(event.block);
26+
final columnBlocks = state.sudoku.getColumnBlocks(event.block);
27+
28+
final highlightedBlocks = [
29+
...subGridBlocks,
30+
...rowBlocks,
31+
...columnBlocks,
32+
];
33+
34+
emit(
35+
state.copyWith(
36+
blockSelectionStatus: () => BlockSelectionStatus.selected,
37+
currentSelectedBlock: () => event.block,
38+
highlightedBlocks: highlightedBlocks.toSet,
39+
),
40+
);
41+
}
42+
}
43+
44+
void _onInputTapped(SudokuInputTapped event, Emitter<SudokuState> emit) {
45+
if (state.currentSelectedBlock == null) return;
46+
47+
final mutableSudoku = Sudoku(blocks: [...state.sudoku.blocks]);
48+
final blockToUpdate = state.currentSelectedBlock!;
49+
50+
final sudoku = mutableSudoku.updateBlock(blockToUpdate, event.input);
51+
52+
if (sudoku.isComplete()) {
53+
emit(
54+
state.copyWith(
55+
puzzleStatus: () => SudokuPuzzleStatus.complete,
56+
sudoku: () => sudoku,
57+
),
58+
);
59+
} else {
60+
emit(
61+
state.copyWith(sudoku: () => sudoku),
62+
);
63+
}
64+
}
65+
}

lib/sudoku/bloc/sudoku_event.dart

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
part of 'sudoku_bloc.dart';
2+
3+
sealed class SudokuEvent extends Equatable {
4+
const SudokuEvent();
5+
}
6+
7+
final class SudokuBlockSelected extends SudokuEvent {
8+
const SudokuBlockSelected(this.block);
9+
final Block block;
10+
11+
@override
12+
List<Object> get props => [block];
13+
}
14+
15+
final class SudokuInputTapped extends SudokuEvent {
16+
const SudokuInputTapped(this.input);
17+
final int input;
18+
19+
@override
20+
List<Object> get props => [input];
21+
}

lib/sudoku/bloc/sudoku_state.dart

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// ignore_for_file: public_member_api_docs, sort_constructors_first
2+
part of 'sudoku_bloc.dart';
3+
4+
enum SudokuPuzzleStatus { incomplete, complete }
5+
6+
enum BlockSelectionStatus { nothingSelected, cannotBeSelected, selected }
7+
8+
class SudokuState extends Equatable {
9+
const SudokuState({
10+
required this.sudoku,
11+
this.puzzleStatus = SudokuPuzzleStatus.incomplete,
12+
this.blockSelectionStatus = BlockSelectionStatus.nothingSelected,
13+
this.highlightedBlocks = const <Block>{},
14+
this.currentSelectedBlock,
15+
});
16+
17+
final Sudoku sudoku;
18+
final SudokuPuzzleStatus puzzleStatus;
19+
final BlockSelectionStatus blockSelectionStatus;
20+
final Set<Block> highlightedBlocks;
21+
final Block? currentSelectedBlock;
22+
23+
@override
24+
List<Object?> get props => [
25+
sudoku,
26+
puzzleStatus,
27+
blockSelectionStatus,
28+
highlightedBlocks,
29+
currentSelectedBlock,
30+
];
31+
32+
SudokuState copyWith({
33+
Sudoku Function()? sudoku,
34+
SudokuPuzzleStatus Function()? puzzleStatus,
35+
BlockSelectionStatus Function()? blockSelectionStatus,
36+
Set<Block> Function()? highlightedBlocks,
37+
Block? Function()? currentSelectedBlock,
38+
}) {
39+
return SudokuState(
40+
sudoku: sudoku != null ? sudoku() : this.sudoku,
41+
puzzleStatus: puzzleStatus != null ? puzzleStatus() : this.puzzleStatus,
42+
blockSelectionStatus: blockSelectionStatus != null
43+
? blockSelectionStatus()
44+
: this.blockSelectionStatus,
45+
highlightedBlocks: highlightedBlocks != null
46+
? highlightedBlocks()
47+
: this.highlightedBlocks,
48+
currentSelectedBlock: currentSelectedBlock != null
49+
? currentSelectedBlock()
50+
: this.currentSelectedBlock,
51+
);
52+
}
53+
}

lib/sudoku/sudoku.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export 'bloc/sudoku_bloc.dart';

test/models/sudoku_test.dart

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,5 +329,34 @@ void main() {
329329
);
330330
});
331331
});
332+
333+
group('updateBlock', () {
334+
test('updates the block and returns a new sudoku instance', () {
335+
final newSudoku = sudoku.updateBlock(sudoku2x2Block8, 1);
336+
expect(newSudoku.blocks.contains(sudoku2x2Block8), isFalse);
337+
338+
final indexOfBlock8 = sudoku.blocks.indexOf(sudoku2x2Block8);
339+
final newBlock8 = newSudoku.blocks[indexOfBlock8];
340+
341+
expect(
342+
newBlock8,
343+
equals(sudoku2x2Block8.copyWith(currentValue: 1)),
344+
);
345+
346+
expect(newBlock8.position, equals(sudoku2x2Block8.position));
347+
expect(newBlock8.currentValue, equals(1));
348+
});
349+
});
350+
351+
group('isComplete', () {
352+
test('returns false when the sudoku is not yet completed', () {
353+
expect(sudoku.isComplete(), isFalse);
354+
});
355+
356+
test('returns true if the sudoku is completed', () {
357+
final solvedSudoku = Sudoku.fromRawData(answerRawData, answerRawData);
358+
expect(solvedSudoku.isComplete(), isTrue);
359+
});
360+
});
332361
});
333362
}

0 commit comments

Comments
 (0)