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 commas
pull/8/head
Alejandro Santiago 3 years ago committed by GitHub
parent 0d27b7f762
commit 18e1f950ad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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';

@ -113,6 +113,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.4.1" version: "0.4.1"
equatable:
dependency: "direct main"
description:
name: equatable
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.3"
fake_async: fake_async:
dependency: transitive dependency: transitive
description: description:

@ -8,6 +8,7 @@ environment:
dependencies: dependencies:
bloc: ^8.0.2 bloc: ^8.0.2
equatable: ^2.0.3
flame: ^1.1.0-releasecandidate.1 flame: ^1.1.0-releasecandidate.1
flame_bloc: ^1.2.0-releasecandidate.1 flame_bloc: ^1.2.0-releasecandidate.1
flame_forge2d: ^0.9.0-releasecandidate.1 flame_forge2d: ^0.9.0-releasecandidate.1

@ -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…
Cancel
Save