Merge remote-tracking branch 'origin' into feat/flipper-component

pull/15/head
alestiago 4 years ago
commit 0ef29d6873

@ -1,6 +1,7 @@
{
"hosting": {
"public": "build/web",
"site": "ashehwkdkdjruejdnensjsjdne",
"ignore": [
"firebase.json",
"**/.*",

@ -9,6 +9,7 @@ class GameBloc extends Bloc<GameEvent, GameState> {
GameBloc() : super(const GameState.initial()) {
on<BallLost>(_onBallLost);
on<Scored>(_onScored);
on<BonusLetterActivated>(_onBonusLetterActivated);
}
void _onBallLost(BallLost event, Emitter emit) {
@ -22,4 +23,15 @@ class GameBloc extends Bloc<GameEvent, GameState> {
emit(state.copyWith(score: state.score + event.points));
}
}
void _onBonusLetterActivated(BonusLetterActivated event, Emitter emit) {
emit(
state.copyWith(
bonusLetters: [
...state.bonusLetters,
event.letter,
],
),
);
}
}

@ -24,3 +24,12 @@ class Scored extends GameEvent {
@override
List<Object?> get props => [points];
}
class BonusLetterActivated extends GameEvent {
const BonusLetterActivated(this.letter);
final String letter;
@override
List<Object?> get props => [letter];
}

@ -8,12 +8,14 @@ class GameState extends Equatable {
const GameState({
required this.score,
required this.balls,
required this.bonusLetters,
}) : 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;
balls = 3,
bonusLetters = const [];
/// The current score of the game.
final int score;
@ -23,6 +25,9 @@ class GameState extends Equatable {
/// When the number of balls is 0, the game is over.
final int balls;
/// Active bonus letters.
final List<String> bonusLetters;
/// Determines when the game is over.
bool get isGameOver => balls == 0;
@ -32,6 +37,7 @@ class GameState extends Equatable {
GameState copyWith({
int? score,
int? balls,
List<String>? bonusLetters,
}) {
assert(
score == null || score >= this.score,
@ -41,6 +47,7 @@ class GameState extends Equatable {
return GameState(
score: score ?? this.score,
balls: balls ?? this.balls,
bonusLetters: bonusLetters ?? this.bonusLetters,
);
}
@ -48,5 +55,6 @@ class GameState extends Equatable {
List<Object?> get props => [
score,
balls,
bonusLetters,
];
}

@ -0,0 +1,46 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:pinball/game/game.dart';
/// {@template game_hud}
/// Overlay of a [PinballGame] that displays the current [GameState.score] and
/// [GameState.balls].
/// {@endtemplate}
class GameHud extends StatelessWidget {
/// {@macro game_hud}
const GameHud({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final state = context.watch<GameBloc>().state;
return Container(
color: Colors.redAccent,
width: 200,
height: 100,
padding: const EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${state.score}',
style: Theme.of(context).textTheme.headline3,
),
Wrap(
direction: Axis.vertical,
children: [
for (var i = 0; i < state.balls; i++)
const Padding(
padding: EdgeInsets.only(top: 6, right: 6),
child: CircleAvatar(
radius: 8,
backgroundColor: Colors.black,
),
),
],
),
],
),
);
}
}

@ -56,7 +56,18 @@ class _PinballGameViewState extends State<PinballGameView> {
);
}
},
child: GameWidget<PinballGame>(game: _game),
child: Stack(
children: [
Positioned.fill(
child: GameWidget<PinballGame>(game: _game),
),
const Positioned(
top: 8,
left: 8,
child: GameHud(),
),
],
),
);
}
}

@ -1,2 +1,3 @@
export 'game_hud.dart';
export 'pinball_game_page.dart';
export 'widgets/widgets.dart';

@ -21,9 +21,9 @@ void main() {
}
},
expect: () => [
const GameState(score: 0, balls: 2),
const GameState(score: 0, balls: 1),
const GameState(score: 0, balls: 0),
const GameState(score: 0, balls: 2, bonusLetters: []),
const GameState(score: 0, balls: 1, bonusLetters: []),
const GameState(score: 0, balls: 0, bonusLetters: []),
],
);
});
@ -37,8 +37,8 @@ void main() {
..add(const Scored(points: 2))
..add(const Scored(points: 3)),
expect: () => [
const GameState(score: 2, balls: 3),
const GameState(score: 5, balls: 3),
const GameState(score: 2, balls: 3, bonusLetters: []),
const GameState(score: 5, balls: 3, bonusLetters: []),
],
);
@ -53,9 +53,55 @@ void main() {
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),
const GameState(score: 0, balls: 2, bonusLetters: []),
const GameState(score: 0, balls: 1, bonusLetters: []),
const GameState(score: 0, balls: 0, bonusLetters: []),
],
);
});
group('BonusLetterActivated', () {
blocTest<GameBloc, GameState>(
'adds the letter to the state',
build: GameBloc.new,
act: (bloc) => bloc
..add(const BonusLetterActivated('G'))
..add(const BonusLetterActivated('O'))
..add(const BonusLetterActivated('O'))
..add(const BonusLetterActivated('G'))
..add(const BonusLetterActivated('L'))
..add(const BonusLetterActivated('E')),
expect: () => [
const GameState(
score: 0,
balls: 3,
bonusLetters: ['G'],
),
const GameState(
score: 0,
balls: 3,
bonusLetters: ['G', 'O'],
),
const GameState(
score: 0,
balls: 3,
bonusLetters: ['G', 'O', 'O'],
),
const GameState(
score: 0,
balls: 3,
bonusLetters: ['G', 'O', 'O', 'G'],
),
const GameState(
score: 0,
balls: 3,
bonusLetters: ['G', 'O', 'O', 'G', 'L'],
),
const GameState(
score: 0,
balls: 3,
bonusLetters: ['G', 'O', 'O', 'G', 'L', 'E'],
),
],
);
});

@ -40,5 +40,22 @@ void main() {
expect(() => Scored(points: 0), throwsAssertionError);
});
});
group('BonusLetterActivated', () {
test('can be instantiated', () {
expect(const BonusLetterActivated('A'), isNotNull);
});
test('supports value equality', () {
expect(
BonusLetterActivated('A'),
equals(BonusLetterActivated('A')),
);
expect(
BonusLetterActivated('B'),
isNot(equals(BonusLetterActivated('A'))),
);
});
});
});
}

@ -7,14 +7,27 @@ void main() {
group('GameState', () {
test('supports value equality', () {
expect(
GameState(score: 0, balls: 0),
equals(const GameState(score: 0, balls: 0)),
GameState(
score: 0,
balls: 0,
bonusLetters: const [],
),
equals(
const GameState(
score: 0,
balls: 0,
bonusLetters: [],
),
),
);
});
group('constructor', () {
test('can be instantiated', () {
expect(const GameState(score: 0, balls: 0), isNotNull);
expect(
const GameState(score: 0, balls: 0, bonusLetters: []),
isNotNull,
);
});
});
@ -23,7 +36,7 @@ void main() {
'when balls are negative',
() {
expect(
() => GameState(balls: -1, score: 0),
() => GameState(balls: -1, score: 0, bonusLetters: const []),
throwsAssertionError,
);
},
@ -34,7 +47,7 @@ void main() {
'when score is negative',
() {
expect(
() => GameState(balls: 0, score: -1),
() => GameState(balls: 0, score: -1, bonusLetters: const []),
throwsAssertionError,
);
},
@ -47,6 +60,7 @@ void main() {
const gameState = GameState(
balls: 0,
score: 0,
bonusLetters: [],
);
expect(gameState.isGameOver, isTrue);
});
@ -57,6 +71,7 @@ void main() {
const gameState = GameState(
balls: 1,
score: 0,
bonusLetters: [],
);
expect(gameState.isGameOver, isFalse);
});
@ -70,6 +85,7 @@ void main() {
const gameState = GameState(
balls: 1,
score: 0,
bonusLetters: [],
);
expect(gameState.isLastBall, isTrue);
},
@ -82,6 +98,7 @@ void main() {
const gameState = GameState(
balls: 2,
score: 0,
bonusLetters: [],
);
expect(gameState.isLastBall, isFalse);
},
@ -96,6 +113,7 @@ void main() {
const gameState = GameState(
balls: 0,
score: 2,
bonusLetters: [],
);
expect(
() => gameState.copyWith(score: gameState.score - 1),
@ -111,6 +129,7 @@ void main() {
const gameState = GameState(
balls: 0,
score: 2,
bonusLetters: [],
);
expect(
gameState.copyWith(),
@ -126,10 +145,12 @@ void main() {
const gameState = GameState(
score: 2,
balls: 0,
bonusLetters: [],
);
final otherGameState = GameState(
score: gameState.score + 1,
balls: gameState.balls + 1,
bonusLetters: const ['A'],
);
expect(gameState, isNot(equals(otherGameState)));
@ -137,6 +158,7 @@ void main() {
gameState.copyWith(
score: otherGameState.score,
balls: otherGameState.balls,
bonusLetters: otherGameState.bonusLetters,
),
equals(otherGameState),
);

@ -135,7 +135,11 @@ void main() {
whenListen(
gameBloc,
const Stream<GameState>.empty(),
initialState: const GameState(score: 10, balls: 1),
initialState: const GameState(
score: 10,
balls: 1,
bonusLetters: [],
),
);
await game.ready();

@ -0,0 +1,79 @@
// ignore_for_file: prefer_const_constructors
import 'package:bloc_test/bloc_test.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball/game/game.dart';
import '../../helpers/helpers.dart';
void main() {
group('GameHud', () {
late GameBloc gameBloc;
const initialState = GameState(score: 10, balls: 2, bonusLetters: []);
void _mockState(GameState state) {
whenListen(
gameBloc,
Stream.value(state),
initialState: state,
);
}
Future<void> _pumpHud(WidgetTester tester) async {
await tester.pumpApp(
GameHud(),
gameBloc: gameBloc,
);
}
setUp(() {
gameBloc = MockGameBloc();
_mockState(initialState);
});
testWidgets(
'renders the current score',
(tester) async {
await _pumpHud(tester);
expect(find.text(initialState.score.toString()), findsOneWidget);
},
);
testWidgets(
'renders the current ball number',
(tester) async {
await _pumpHud(tester);
expect(
find.byType(CircleAvatar),
findsNWidgets(initialState.balls),
);
},
);
testWidgets('updates the score', (tester) async {
await _pumpHud(tester);
expect(find.text(initialState.score.toString()), findsOneWidget);
_mockState(initialState.copyWith(score: 20));
await tester.pump();
expect(find.text('20'), findsOneWidget);
});
testWidgets('updates the ball number', (tester) async {
await _pumpHud(tester);
expect(
find.byType(CircleAvatar),
findsNWidgets(initialState.balls),
);
_mockState(initialState.copyWith(balls: 1));
await tester.pump();
expect(
find.byType(CircleAvatar),
findsNWidgets(1),
);
});
});
}

@ -48,7 +48,7 @@ void main() {
});
group('PinballGameView', () {
testWidgets('renders game', (tester) async {
testWidgets('renders game and a hud', (tester) async {
final gameBloc = MockGameBloc();
whenListen(
gameBloc,
@ -61,13 +61,17 @@ void main() {
find.byWidgetPredicate((w) => w is GameWidget<PinballGame>),
findsOneWidget,
);
expect(
find.byType(GameHud),
findsOneWidget,
);
});
testWidgets(
'renders a game over dialog when the user has lost',
(tester) async {
final gameBloc = MockGameBloc();
const state = GameState(score: 0, balls: 0);
const state = GameState(score: 0, balls: 0, bonusLetters: []);
whenListen(
gameBloc,
Stream.value(state),

Loading…
Cancel
Save