mirror of https://github.com/flutter/pinball.git
feat: implement GameBloc (#3)
* chore: generated bloc using VS extension * chore: included equatable dependency * feat: implemented BallLost and Scored events * refactor: renamed ballsLeft to balls * chore: exported bloc in barrel file * feat: tested and improved GameState * feat: tested and improved GameState * feat: tested and improved GameEvent * feat: tested GameBloc * refactor: modified redundant test message * refactor: included initial factory constructor * refactor: corrected class doc comment * feat: made GameEvent support value equality * refactor: adapted equality test to pick coverage * refactor: moved linter ignore comment * refactor: removed const from equality comparison * refactor: changed factory constructor for named constructor * refactor: added trailing commaspull/8/head
parent
0d27b7f762
commit
18e1f950ad
@ -0,0 +1,25 @@
|
|||||||
|
import 'package:bloc/bloc.dart';
|
||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:meta/meta.dart';
|
||||||
|
|
||||||
|
part 'game_event.dart';
|
||||||
|
part 'game_state.dart';
|
||||||
|
|
||||||
|
class GameBloc extends Bloc<GameEvent, GameState> {
|
||||||
|
GameBloc() : super(const GameState.initial()) {
|
||||||
|
on<BallLost>(_onBallLost);
|
||||||
|
on<Scored>(_onScored);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onBallLost(BallLost event, Emitter emit) {
|
||||||
|
if (state.balls > 0) {
|
||||||
|
emit(state.copyWith(balls: state.balls - 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onScored(Scored event, Emitter emit) {
|
||||||
|
if (!state.isGameOver) {
|
||||||
|
emit(state.copyWith(score: state.score + event.points));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
part of 'game_bloc.dart';
|
||||||
|
|
||||||
|
@immutable
|
||||||
|
abstract class GameEvent extends Equatable {
|
||||||
|
const GameEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Event added when a user drops a ball off the screen.
|
||||||
|
class BallLost extends GameEvent {
|
||||||
|
const BallLost();
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Event added when a user increases their score.
|
||||||
|
class Scored extends GameEvent {
|
||||||
|
const Scored({
|
||||||
|
required this.points,
|
||||||
|
}) : assert(points > 0, 'Points must be greater than 0');
|
||||||
|
|
||||||
|
final int points;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [points];
|
||||||
|
}
|
@ -0,0 +1,49 @@
|
|||||||
|
part of 'game_bloc.dart';
|
||||||
|
|
||||||
|
/// {@template game_state}
|
||||||
|
/// Represents the state of the pinball game.
|
||||||
|
/// {@endtemplate}
|
||||||
|
class GameState extends Equatable {
|
||||||
|
/// {@macro game_state}
|
||||||
|
const GameState({
|
||||||
|
required this.score,
|
||||||
|
required this.balls,
|
||||||
|
}) : assert(score >= 0, "Score can't be negative"),
|
||||||
|
assert(balls >= 0, "Number of balls can't be negative");
|
||||||
|
|
||||||
|
const GameState.initial()
|
||||||
|
: score = 0,
|
||||||
|
balls = 3;
|
||||||
|
|
||||||
|
/// The current score of the game.
|
||||||
|
final int score;
|
||||||
|
|
||||||
|
/// The number of balls left in the game.
|
||||||
|
///
|
||||||
|
/// When the number of balls is 0, the game is over.
|
||||||
|
final int balls;
|
||||||
|
|
||||||
|
/// Determines when the game is over.
|
||||||
|
bool get isGameOver => balls == 0;
|
||||||
|
|
||||||
|
GameState copyWith({
|
||||||
|
int? score,
|
||||||
|
int? balls,
|
||||||
|
}) {
|
||||||
|
assert(
|
||||||
|
score == null || score >= this.score,
|
||||||
|
"Score can't be decreased",
|
||||||
|
);
|
||||||
|
|
||||||
|
return GameState(
|
||||||
|
score: score ?? this.score,
|
||||||
|
balls: balls ?? this.balls,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [
|
||||||
|
score,
|
||||||
|
balls,
|
||||||
|
];
|
||||||
|
}
|
@ -1,3 +1,4 @@
|
|||||||
|
export 'bloc/game_bloc.dart';
|
||||||
export 'components/components.dart';
|
export 'components/components.dart';
|
||||||
export 'pinball_game.dart';
|
export 'pinball_game.dart';
|
||||||
export 'view/pinball_game_page.dart';
|
export 'view/pinball_game_page.dart';
|
||||||
|
@ -0,0 +1,63 @@
|
|||||||
|
import 'package:bloc_test/bloc_test.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:pinball/game/game.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('GameBloc', () {
|
||||||
|
test('initial state has 3 balls and empty score', () {
|
||||||
|
final gameBloc = GameBloc();
|
||||||
|
expect(gameBloc.state.score, equals(0));
|
||||||
|
expect(gameBloc.state.balls, equals(3));
|
||||||
|
});
|
||||||
|
|
||||||
|
group('LostBall', () {
|
||||||
|
blocTest<GameBloc, GameState>(
|
||||||
|
"doesn't decrease ball "
|
||||||
|
'when no balls left',
|
||||||
|
build: GameBloc.new,
|
||||||
|
act: (bloc) {
|
||||||
|
for (var i = 0; i <= bloc.state.balls; i++) {
|
||||||
|
bloc.add(const BallLost());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
expect: () => [
|
||||||
|
const GameState(score: 0, balls: 2),
|
||||||
|
const GameState(score: 0, balls: 1),
|
||||||
|
const GameState(score: 0, balls: 0),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
group('Scored', () {
|
||||||
|
blocTest<GameBloc, GameState>(
|
||||||
|
'increases score '
|
||||||
|
'when game is not over',
|
||||||
|
build: GameBloc.new,
|
||||||
|
act: (bloc) => bloc
|
||||||
|
..add(const Scored(points: 2))
|
||||||
|
..add(const Scored(points: 3)),
|
||||||
|
expect: () => [
|
||||||
|
const GameState(score: 2, balls: 3),
|
||||||
|
const GameState(score: 5, balls: 3),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
blocTest<GameBloc, GameState>(
|
||||||
|
"doesn't increase score "
|
||||||
|
'when game is over',
|
||||||
|
build: GameBloc.new,
|
||||||
|
act: (bloc) {
|
||||||
|
for (var i = 0; i < bloc.state.balls; i++) {
|
||||||
|
bloc.add(const BallLost());
|
||||||
|
}
|
||||||
|
bloc.add(const Scored(points: 2));
|
||||||
|
},
|
||||||
|
expect: () => [
|
||||||
|
const GameState(score: 0, balls: 2),
|
||||||
|
const GameState(score: 0, balls: 1),
|
||||||
|
const GameState(score: 0, balls: 0),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,44 @@
|
|||||||
|
// ignore_for_file: prefer_const_constructors
|
||||||
|
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:pinball/game/game.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('GameEvent', () {
|
||||||
|
group('BallLost', () {
|
||||||
|
test('can be instantiated', () {
|
||||||
|
expect(const BallLost(), isNotNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('supports value equality', () {
|
||||||
|
expect(
|
||||||
|
BallLost(),
|
||||||
|
equals(const BallLost()),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('Scored', () {
|
||||||
|
test('can be instantiated', () {
|
||||||
|
expect(const Scored(points: 1), isNotNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('supports value equality', () {
|
||||||
|
expect(
|
||||||
|
Scored(points: 1),
|
||||||
|
equals(const Scored(points: 1)),
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
const Scored(points: 1),
|
||||||
|
isNot(equals(const Scored(points: 2))),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'throws AssertionError '
|
||||||
|
'when score is smaller than 1', () {
|
||||||
|
expect(() => Scored(points: 0), throwsAssertionError);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,121 @@
|
|||||||
|
// ignore_for_file: prefer_const_constructors
|
||||||
|
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:pinball/game/game.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('GameState', () {
|
||||||
|
test('supports value equality', () {
|
||||||
|
expect(
|
||||||
|
GameState(score: 0, balls: 0),
|
||||||
|
equals(const GameState(score: 0, balls: 0)),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
group('constructor', () {
|
||||||
|
test('can be instantiated', () {
|
||||||
|
expect(const GameState(score: 0, balls: 0), isNotNull);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'throws AssertionError '
|
||||||
|
'when balls are negative',
|
||||||
|
() {
|
||||||
|
expect(
|
||||||
|
() => GameState(balls: -1, score: 0),
|
||||||
|
throwsAssertionError,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'throws AssertionError '
|
||||||
|
'when score is negative',
|
||||||
|
() {
|
||||||
|
expect(
|
||||||
|
() => GameState(balls: 0, score: -1),
|
||||||
|
throwsAssertionError,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
group('isGameOver', () {
|
||||||
|
test(
|
||||||
|
'is true '
|
||||||
|
'when no balls are left', () {
|
||||||
|
const gameState = GameState(
|
||||||
|
balls: 0,
|
||||||
|
score: 0,
|
||||||
|
);
|
||||||
|
expect(gameState.isGameOver, isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'is false '
|
||||||
|
'when one 1 ball left', () {
|
||||||
|
const gameState = GameState(
|
||||||
|
balls: 1,
|
||||||
|
score: 0,
|
||||||
|
);
|
||||||
|
expect(gameState.isGameOver, isFalse);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('copyWith', () {
|
||||||
|
test(
|
||||||
|
'throws AssertionError '
|
||||||
|
'when scored is decreased',
|
||||||
|
() {
|
||||||
|
const gameState = GameState(
|
||||||
|
balls: 0,
|
||||||
|
score: 2,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
() => gameState.copyWith(score: gameState.score - 1),
|
||||||
|
throwsAssertionError,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'copies correctly '
|
||||||
|
'when no arguement specified',
|
||||||
|
() {
|
||||||
|
const gameState = GameState(
|
||||||
|
balls: 0,
|
||||||
|
score: 2,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
gameState.copyWith(),
|
||||||
|
equals(gameState),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'copies correctly '
|
||||||
|
'when all arguements specified',
|
||||||
|
() {
|
||||||
|
const gameState = GameState(
|
||||||
|
score: 2,
|
||||||
|
balls: 0,
|
||||||
|
);
|
||||||
|
final otherGameState = GameState(
|
||||||
|
score: gameState.score + 1,
|
||||||
|
balls: gameState.balls + 1,
|
||||||
|
);
|
||||||
|
expect(gameState, isNot(equals(otherGameState)));
|
||||||
|
|
||||||
|
expect(
|
||||||
|
gameState.copyWith(
|
||||||
|
score: otherGameState.score,
|
||||||
|
balls: otherGameState.balls,
|
||||||
|
),
|
||||||
|
equals(otherGameState),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
Loading…
Reference in new issue