Merge branch 'main' into feat/plunger

pull/10/head
Allison Ryan 4 years ago
commit 6cf2ccb5e0

@ -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,
];
}

@ -0,0 +1,31 @@
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/body_component.dart';
import 'package:flutter/material.dart';
import 'package:forge2d/forge2d.dart';
import 'package:pinball/game/game.dart';
class Ball extends BodyComponent<PinballGame>
with BlocComponent<GameBloc, GameState> {
Ball({
required Vector2 position,
}) : _position = position {
// TODO(alestiago): Use asset instead of color when provided.
paint = Paint()..color = const Color(0xFFFFFFFF);
}
final Vector2 _position;
@override
Body createBody() {
final shape = CircleShape()..radius = 2;
final fixtureDef = FixtureDef(shape)..density = 1;
final bodyDef = BodyDef()
..userData = this
..position = _position
..type = BodyType.dynamic;
return world.createBody(bodyDef)..createFixture(fixtureDef);
}
}

@ -1,2 +1,4 @@
export 'ball.dart';
export 'boundaries.dart'; export 'boundaries.dart';
export 'plunger.dart'; export 'plunger.dart';
export 'score_points.dart';

@ -0,0 +1,30 @@
// ignore_for_file: avoid_renaming_method_parameters
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:pinball/game/game.dart';
/// {@template score_points}
/// Specifies the amount of points received on [Ball] collision.
/// {@endtemplate}
mixin ScorePoints on BodyComponent {
/// {@macro score_points}
int get points;
}
/// Adds points to the score when a [Ball] collides with a [BodyComponent] that
/// implements [ScorePoints].
class BallScorePointsCallback extends ContactCallback<Ball, ScorePoints> {
@override
void begin(
Ball ball,
ScorePoints hasPoints,
Contact _,
) {
ball.gameRef.read<GameBloc>().add(Scored(points: hasPoints.points));
}
// TODO(alestiago): remove once this issue is closed.
// https://github.com/flame-engine/flame/issues/1414
@override
void end(Ball _, ScorePoints __, Contact ___) {}
}

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

@ -1,16 +1,17 @@
import 'package:flame/input.dart'; import 'package:flame/input.dart';
import 'package:flame_bloc/flame_bloc.dart';
import 'package:flame_forge2d/flame_forge2d.dart'; import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_forge2d/forge2d_game.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:pinball/game/game.dart'; import 'package:pinball/game/game.dart';
class PinballGame extends Forge2DGame with KeyboardEvents { class PinballGame extends Forge2DGame with FlameBloc, KeyboardEvents {
late Plunger plunger; late Plunger plunger;
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
await super.onLoad(); await super.onLoad();
addContactCallback(BallScorePointsCallback());
final boundaries = createBoundaries(this)..forEach(add); final boundaries = createBoundaries(this)..forEach(add);
final bottomWall = boundaries[2]; final bottomWall = boundaries[2];

@ -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:
@ -133,14 +140,28 @@ packages:
name: flame name: flame
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.0" version: "1.1.0-releasecandidate.1"
flame_bloc:
dependency: "direct main"
description:
name: flame_bloc
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.0-releasecandidate.1"
flame_forge2d: flame_forge2d:
dependency: "direct main" dependency: "direct main"
description: description:
name: flame_forge2d name: flame_forge2d
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.8.3" version: "0.9.0-releasecandidate.1"
flame_test:
dependency: "direct dev"
description:
name: flame_test
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
flutter: flutter:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter

@ -8,8 +8,10 @@ environment:
dependencies: dependencies:
bloc: ^8.0.2 bloc: ^8.0.2
flame: ^1.0.0 equatable: ^2.0.3
flame_forge2d: ^0.8.3 flame: ^1.1.0-releasecandidate.1
flame_bloc: ^1.2.0-releasecandidate.1
flame_forge2d: ^0.9.0-releasecandidate.1
flutter: flutter:
sdk: flutter sdk: flutter
flutter_bloc: ^8.0.1 flutter_bloc: ^8.0.1
@ -19,6 +21,7 @@ dependencies:
dev_dependencies: dev_dependencies:
bloc_test: ^9.0.2 bloc_test: ^9.0.2
flame_test: ^1.1.0
flutter_test: flutter_test:
sdk: flutter sdk: flutter
mockingjay: ^0.2.0 mockingjay: ^0.2.0

@ -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),
);
},
);
});
});
}

@ -0,0 +1,83 @@
// ignore_for_file: cascade_invocations
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:pinball/game/game.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('Ball', () {
final flameTester = FlameTester(PinballGame.new);
flameTester.test(
'loads correctly',
(game) async {
final ball = Ball(position: Vector2.zero());
await game.ensureAdd(ball);
expect(game.contains(ball), isTrue);
},
);
group('body', () {
flameTester.test(
'positions correctly',
(game) async {
final position = Vector2.all(10);
final ball = Ball(position: position);
await game.ensureAdd(ball);
game.contains(ball);
expect(ball.body.position, position);
},
);
flameTester.test(
'is dynamic',
(game) async {
final ball = Ball(position: Vector2.zero());
await game.ensureAdd(ball);
expect(ball.body.bodyType, equals(BodyType.dynamic));
},
);
});
group('first fixture', () {
flameTester.test(
'exists',
(game) async {
final ball = Ball(position: Vector2.zero());
await game.ensureAdd(ball);
expect(ball.body.fixtures[0], isA<Fixture>());
},
);
flameTester.test(
'is dense',
(game) async {
final ball = Ball(position: Vector2.zero());
await game.ensureAdd(ball);
final fixture = ball.body.fixtures[0];
expect(fixture.density, greaterThan(0));
},
);
flameTester.test(
'shape is circular',
(game) async {
final ball = Ball(position: Vector2.zero());
await game.ensureAdd(ball);
final fixture = ball.body.fixtures[0];
expect(fixture.shape.shapeType, equals(ShapeType.circle));
expect(fixture.shape.radius, equals(2));
},
);
});
});
}

@ -0,0 +1,80 @@
import 'package:flame_forge2d/flame_forge2d.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:pinball/game/game.dart';
class MockBall extends Mock implements Ball {}
class MockGameBloc extends Mock implements GameBloc {}
class MockPinballGame extends Mock implements PinballGame {}
class FakeContact extends Fake implements Contact {}
class FakeGameEvent extends Fake implements GameEvent {}
class FakeScorePoints extends BodyComponent with ScorePoints {
@override
Body createBody() {
throw UnimplementedError();
}
@override
int get points => 2;
}
void main() {
group('BallScorePointsCallback', () {
late PinballGame game;
late GameBloc bloc;
late Ball ball;
late FakeScorePoints fakeScorePoints;
setUp(() {
game = MockPinballGame();
bloc = MockGameBloc();
ball = MockBall();
fakeScorePoints = FakeScorePoints();
});
setUpAll(() {
registerFallbackValue(FakeGameEvent());
});
group('begin', () {
test(
'emits Scored event with points',
() {
when<PinballGame>(() => ball.gameRef).thenReturn(game);
when<GameBloc>(game.read).thenReturn(bloc);
BallScorePointsCallback().begin(
ball,
fakeScorePoints,
FakeContact(),
);
verify(
() => bloc.add(
Scored(points: fakeScorePoints.points),
),
).called(1);
},
);
});
group('end', () {
test("doesn't add events to GameBloc", () {
BallScorePointsCallback().end(
ball,
fakeScorePoints,
FakeContact(),
);
verifyNever(
() => bloc.add(any()),
);
});
});
});
}

@ -0,0 +1,9 @@
import 'package:flutter_test/flutter_test.dart';
void main() {
group('PinballGame', () {
// TODO(alestiago): test if [PinballGame] registers
// [BallScorePointsCallback] once the following issue is resolved:
// https://github.com/flame-engine/flame/issues/1416
});
}
Loading…
Cancel
Save