mirror of https://github.com/flutter/pinball.git
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';
|
||||||
|
@ -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…
Reference in new issue