diff --git a/packages/pinball_components/lib/src/components/spaceship_ramp/cubit/ramp_sensor_cubit.dart b/packages/pinball_components/lib/src/components/spaceship_ramp/cubit/ramp_sensor_cubit.dart deleted file mode 100644 index bcbd7737..00000000 --- a/packages/pinball_components/lib/src/components/spaceship_ramp/cubit/ramp_sensor_cubit.dart +++ /dev/null @@ -1,28 +0,0 @@ -// ignore_for_file: public_member_api_docs - -import 'package:bloc/bloc.dart'; -import 'package:pinball_components/pinball_components.dart'; - -part 'ramp_sensor_state.dart'; - -class RampSensorCubit extends Cubit { - RampSensorCubit() : super(const RampSensorState.initial()); - - void onDoor(Ball ball) { - emit( - state.copyWith( - type: RampSensorType.door, - ball: ball, - ), - ); - } - - void onInside(Ball ball) { - emit( - state.copyWith( - type: RampSensorType.inside, - ball: ball, - ), - ); - } -} diff --git a/packages/pinball_components/lib/src/components/spaceship_ramp/cubit/ramp_sensor_state.dart b/packages/pinball_components/lib/src/components/spaceship_ramp/cubit/ramp_sensor_state.dart deleted file mode 100644 index 8583c067..00000000 --- a/packages/pinball_components/lib/src/components/spaceship_ramp/cubit/ramp_sensor_state.dart +++ /dev/null @@ -1,35 +0,0 @@ -// ignore_for_file: public_member_api_docs - -part of 'ramp_sensor_cubit.dart'; - -/// Used to know when a [Ball] gets into the [SpaceshipRamp] against every ball -/// that crosses the opening. -enum RampSensorType { - /// Sensor at the entrance of the opening. - door, - - /// Sensor inside the [SpaceshipRamp]. - inside, -} - -class RampSensorState { - const RampSensorState({ - required this.type, - this.ball, - }); - - const RampSensorState.initial() : this(type: RampSensorType.door); - - final RampSensorType type; - final Ball? ball; - - RampSensorState copyWith({ - RampSensorType? type, - Ball? ball, - }) { - return RampSensorState( - type: type ?? this.type, - ball: ball ?? this.ball, - ); - } -} diff --git a/packages/pinball_components/lib/src/components/spaceship_ramp/cubit/spaceship_ramp_cubit.dart b/packages/pinball_components/lib/src/components/spaceship_ramp/cubit/spaceship_ramp_cubit.dart new file mode 100644 index 00000000..37cf8d08 --- /dev/null +++ b/packages/pinball_components/lib/src/components/spaceship_ramp/cubit/spaceship_ramp_cubit.dart @@ -0,0 +1,43 @@ +// ignore_for_file: public_member_api_docs + +import 'dart:collection'; + +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:pinball_components/pinball_components.dart'; + +part 'spaceship_ramp_state.dart'; + +class SpaceshipRampCubit extends Cubit { + SpaceshipRampCubit() : super(SpaceshipRampState.initial()); + + void onDoor(Ball ball) { + if (!state.balls.contains(ball)) { + emit( + state.copyWith( + balls: state.balls..add(ball), + status: SpaceshipRampStatus.withoutBonus, + ), + ); + } + } + + void onInside(Ball ball) { + if (state.balls.contains(ball)) { + final hits = state.hits + 1; + final bonus = (hits % 10 == 0) + ? SpaceshipRampStatus.withBonus + : SpaceshipRampStatus.withoutBonus; + final shot = hits % 10 != 0; + + emit( + state.copyWith( + hits: hits, + balls: state.balls..remove(ball), + shot: shot, + status: bonus, + ), + ); + } + } +} diff --git a/packages/pinball_components/lib/src/components/spaceship_ramp/cubit/spaceship_ramp_state.dart b/packages/pinball_components/lib/src/components/spaceship_ramp/cubit/spaceship_ramp_state.dart new file mode 100644 index 00000000..d440eba8 --- /dev/null +++ b/packages/pinball_components/lib/src/components/spaceship_ramp/cubit/spaceship_ramp_state.dart @@ -0,0 +1,57 @@ +// ignore_for_file: public_member_api_docs + +part of 'spaceship_ramp_cubit.dart'; + +/// Used to know when a [Ball] gets into the [SpaceshipRamp] against every ball +/// that crosses the opening. +enum RampSensorType { + /// Sensor at the entrance of the opening. + door, + + /// Sensor inside the [SpaceshipRamp]. + inside, +} + +enum SpaceshipRampStatus { + withoutBonus, + withBonus, +} + +class SpaceshipRampState extends Equatable { + const SpaceshipRampState({ + required this.hits, + required this.balls, + required this.shot, + required this.status, + }) : assert(hits >= 0, "Hits can't be negative"); + + SpaceshipRampState.initial() + : this( + hits: 0, + balls: HashSet(), + shot: false, + status: SpaceshipRampStatus.withoutBonus, + ); + + final int hits; + final Set balls; + final bool shot; + final SpaceshipRampStatus status; + + SpaceshipRampState copyWith({ + int? hits, + Set? balls, + bool? shot, + SpaceshipRampStatus? status, + }) { + return SpaceshipRampState( + hits: hits ?? this.hits, + balls: balls ?? this.balls, + shot: shot ?? this.shot, + status: status ?? this.status, + ); + } + + @override + List get props => [hits, balls, shot, status]; +} diff --git a/packages/pinball_components/lib/src/components/spaceship_ramp/spaceship_ramp.dart b/packages/pinball_components/lib/src/components/spaceship_ramp/spaceship_ramp.dart index 6a505263..cb54357e 100644 --- a/packages/pinball_components/lib/src/components/spaceship_ramp/spaceship_ramp.dart +++ b/packages/pinball_components/lib/src/components/spaceship_ramp/spaceship_ramp.dart @@ -8,7 +8,7 @@ import 'package:pinball_components/pinball_components.dart' hide Assets; import 'package:pinball_components/src/components/spaceship_ramp/behavior/behavior.dart'; import 'package:pinball_flame/pinball_flame.dart'; -export 'cubit/ramp_sensor_cubit.dart'; +export 'cubit/spaceship_ramp_cubit.dart'; /// {@template spaceship_ramp} /// Ramp leading into the [AndroidSpaceship]. @@ -17,14 +17,23 @@ class SpaceshipRamp extends Component { /// {@macro spaceship_ramp} SpaceshipRamp({ Iterable? children, - }) : super( + }) : bloc = SpaceshipRampCubit(), + super( children: [ // TODO(ruimiguel): refactor RampSensor and RampOpening to be in // only one sensor. - RampSensor(type: RampSensorType.door) - ..initialPosition = Vector2(1.7, -20.4), - RampSensor(type: RampSensorType.inside) - ..initialPosition = Vector2(1.7, -22), + RampSensor( + type: RampSensorType.door, + children: [ + RampContactBehavior(), + ], + )..initialPosition = Vector2(1.7, -20.4), + RampSensor( + type: RampSensorType.inside, + children: [ + RampContactBehavior(), + ], + )..initialPosition = Vector2(1.7, -22), _SpaceshipRampOpening( outsidePriority: ZIndexes.ballOnBoard, rotation: math.pi, @@ -53,7 +62,21 @@ class SpaceshipRamp extends Component { /// /// This can be used for testing [SpaceshipRamp]'s behaviors in isolation. @visibleForTesting - SpaceshipRamp.test(); + SpaceshipRamp.test({ + required this.bloc, + Iterable? children, + }) : super(children: children); + +// TODO(alestiago): Consider refactoring once the following is merged: + // https://github.com/flame-engine/flame/pull/1538 + // ignore: public_member_api_docs + final SpaceshipRampCubit bloc; + + @override + void onRemove() { + bloc.close(); + super.onRemove(); + } /// Forwards the sprite to the next [SpaceshipRampArrowSpriteState]. /// @@ -398,13 +421,12 @@ class _SpaceshipRampOpening extends LayerSensor { class RampSensor extends BodyComponent with ParentIsA, InitialPosition, Layered { /// {@macro ramp_sensor} - RampSensor({required this.type}) - : bloc = RampSensorCubit(), - super( - children: [ - RampContactBehavior(), - ], - renderBody: true, + RampSensor({ + required this.type, + Iterable? children, + }) : super( + children: children, + renderBody: false, ) { layer = Layer.spaceshipEntranceRamp; } @@ -414,23 +436,11 @@ class RampSensor extends BodyComponent @visibleForTesting RampSensor.test({ required this.type, - required this.bloc, }); /// Type for the sensor, to know if it's the one at the door or inside ramp. final RampSensorType type; - // TODO(alestiago): Consider refactoring once the following is merged: - // https://github.com/flame-engine/flame/pull/1538 - // ignore: public_member_api_docs - final RampSensorCubit bloc; - - @override - void onRemove() { - bloc.close(); - super.onRemove(); - } - @override Body createBody() { final shape = PolygonShape() diff --git a/packages/pinball_components/test/src/components/spaceship_ramp/cubit/ramp_sensor_cubit_test.dart b/packages/pinball_components/test/src/components/spaceship_ramp/cubit/ramp_sensor_cubit_test.dart deleted file mode 100644 index 81f02f60..00000000 --- a/packages/pinball_components/test/src/components/spaceship_ramp/cubit/ramp_sensor_cubit_test.dart +++ /dev/null @@ -1,66 +0,0 @@ -// 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_components/pinball_components.dart'; - -void main() { - group('RampSensorCubit', () { - final ball = Ball(baseColor: Colors.red); - - blocTest( - 'onDoor emits [door]', - build: RampSensorCubit.new, - act: (bloc) => bloc.onDoor(ball), - expect: () => [ - isA() - ..having((state) => state.type, 'type', RampSensorType.door) - ..having((state) => state.ball, 'ball', ball), - ], - ); - - blocTest( - 'onDoor twice emits [door, door]', - build: RampSensorCubit.new, - act: (bloc) => bloc - ..onDoor(ball) - ..onDoor(ball), - expect: () => [ - isA() - ..having((state) => state.type, 'type', RampSensorType.door) - ..having((state) => state.ball, 'ball', ball), - isA() - ..having((state) => state.type, 'type', RampSensorType.door) - ..having((state) => state.ball, 'ball', ball), - ], - ); - - blocTest( - 'onInside emits [inside]', - build: RampSensorCubit.new, - act: (bloc) => bloc.onInside(ball), - expect: () => [ - isA() - ..having((state) => state.type, 'type', RampSensorType.inside) - ..having((state) => state.ball, 'ball', ball), - ], - ); - - blocTest( - 'onInside twice emits [inside,inside]', - build: RampSensorCubit.new, - act: (bloc) => bloc - ..onInside(ball) - ..onInside(ball), - expect: () => [ - isA() - ..having((state) => state.type, 'type', RampSensorType.inside) - ..having((state) => state.ball, 'ball', ball), - isA() - ..having((state) => state.type, 'type', RampSensorType.inside) - ..having((state) => state.ball, 'ball', ball), - ], - ); - }); -} diff --git a/packages/pinball_components/test/src/components/spaceship_ramp/cubit/ramp_sensor_state_test.dart b/packages/pinball_components/test/src/components/spaceship_ramp/cubit/ramp_sensor_state_test.dart deleted file mode 100644 index 7ac4f3dd..00000000 --- a/packages/pinball_components/test/src/components/spaceship_ramp/cubit/ramp_sensor_state_test.dart +++ /dev/null @@ -1,110 +0,0 @@ -// ignore_for_file: prefer_const_constructors - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:pinball_components/pinball_components.dart'; - -void main() { - group('RampSensorState', () { - test('same states are different even though they have same content', () { - final ball = Ball(baseColor: Colors.red); - - final rampSensorState = RampSensorState( - type: RampSensorType.door, - ball: ball, - ); - final otherRampSensorState = RampSensorState( - type: RampSensorType.door, - ball: ball, - ); - - expect( - rampSensorState, - isNot(equals(otherRampSensorState)), - ); - - expect( - rampSensorState.type, - equals(otherRampSensorState.type), - ); - - expect( - rampSensorState.ball, - equals(otherRampSensorState.ball), - ); - }); - - group('constructor', () { - test('can be instantiated', () { - expect( - RampSensorState( - type: RampSensorType.door, - ball: Ball(baseColor: Colors.red), - ), - isNotNull, - ); - }); - }); - - group('copyWith', () { - test( - 'copies correctly ' - 'when no argument specified', - () { - final rampSensorState = RampSensorState( - type: RampSensorType.door, - ball: Ball(baseColor: Colors.red), - ); - - final copiedRampSensorState = rampSensorState.copyWith(); - - expect( - copiedRampSensorState.type, - equals(rampSensorState.type), - ); - - expect( - copiedRampSensorState.ball, - equals(rampSensorState.ball), - ); - }, - ); - - test( - 'copies correctly ' - 'when all arguments specified', - () { - final ball = Ball(baseColor: Colors.blue); - final rampSensorState = RampSensorState( - type: RampSensorType.door, - ball: Ball(baseColor: Colors.red), - ); - final otherRampSensorState = RampSensorState( - type: RampSensorType.inside, - ball: ball, - ); - - final copiedRampSensorState = rampSensorState.copyWith( - type: RampSensorType.inside, - ball: ball, - ); - - expect( - rampSensorState, - isNot(equals(otherRampSensorState)), - ); - - expect( - copiedRampSensorState.type, - equals(otherRampSensorState.type), - ); - - expect( - copiedRampSensorState.ball, - equals(otherRampSensorState.ball), - ); - }, - ); - }); - }); -} diff --git a/packages/pinball_components/test/src/components/spaceship_ramp/cubit/spaceship_ramp_cubit_test.dart b/packages/pinball_components/test/src/components/spaceship_ramp/cubit/spaceship_ramp_cubit_test.dart new file mode 100644 index 00000000..c970a26c --- /dev/null +++ b/packages/pinball_components/test/src/components/spaceship_ramp/cubit/spaceship_ramp_cubit_test.dart @@ -0,0 +1,98 @@ +// 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_components/pinball_components.dart'; + +void main() { + group('SpaceshipRampCubit', () { + final ball = Ball(baseColor: Colors.red); + + group('onDoor', () { + blocTest( + 'emits nothing if contains ball', + build: SpaceshipRampCubit.new, + seed: () => SpaceshipRampState.initial().copyWith( + balls: {ball}, + ), + act: (bloc) => bloc.onDoor(ball), + expect: () => [], + ); + + blocTest( + 'emits [{ball}, withoutBonus] if not contains ball', + build: SpaceshipRampCubit.new, + act: (bloc) => bloc.onDoor(ball), + expect: () => [ + isA() + ..having( + (state) => state.balls, + 'balls', + contains(ball), + ) + ..having( + (state) => state.status, + 'status', + SpaceshipRampStatus.withoutBonus, + ), + ], + ); + }); + + group('onInside', () { + blocTest( + 'emits nothing if not contains ball', + build: SpaceshipRampCubit.new, + seed: () => SpaceshipRampState.initial().copyWith( + balls: {}, + ), + act: (bloc) => bloc.onInside(ball), + expect: () => [], + ); + + blocTest( + 'emits withoutBonus if contains ball ' + 'and hits less than 10 times', + build: SpaceshipRampCubit.new, + seed: () => SpaceshipRampState.initial().copyWith( + hits: 5, + balls: {ball}, + ), + act: (bloc) => bloc.onInside(ball), + expect: () => [ + isA() + ..having((state) => state.hits, 'hits', 6) + ..having((state) => state.balls, 'balls', isNot(contains(ball))) + ..having((state) => state.shot, 'shot', true) + ..having( + (state) => state.status, + 'status', + SpaceshipRampStatus.withoutBonus, + ), + ], + ); + + blocTest( + 'emits withBonus if contains ball and hits 10 times', + build: SpaceshipRampCubit.new, + seed: () => SpaceshipRampState.initial().copyWith( + hits: 9, + balls: {ball}, + ), + act: (bloc) => bloc.onInside(ball), + expect: () => [ + isA() + ..having((state) => state.hits, 'hits', 10) + ..having((state) => state.balls, 'balls', contains(ball)) + ..having((state) => state.shot, 'shot', false) + ..having( + (state) => state.status, + 'status', + SpaceshipRampStatus.withBonus, + ), + ], + ); + }); + }); +} diff --git a/packages/pinball_components/test/src/components/spaceship_ramp/cubit/spaceship_ramp_state_test.dart b/packages/pinball_components/test/src/components/spaceship_ramp/cubit/spaceship_ramp_state_test.dart new file mode 100644 index 00000000..cd57e95d --- /dev/null +++ b/packages/pinball_components/test/src/components/spaceship_ramp/cubit/spaceship_ramp_state_test.dart @@ -0,0 +1,126 @@ +// ignore_for_file: prefer_const_constructors + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pinball_components/src/components/components.dart'; + +void main() { + group('SpaceshipRampState', () { + test('supports value equality', () { + expect( + SpaceshipRampState( + hits: 0, + balls: const {}, + shot: false, + status: SpaceshipRampStatus.withoutBonus, + ), + equals( + SpaceshipRampState( + hits: 0, + balls: const {}, + shot: false, + status: SpaceshipRampStatus.withoutBonus, + ), + ), + ); + }); + + group('constructor', () { + test('can be instantiated', () { + expect( + SpaceshipRampState( + hits: 0, + balls: const {}, + shot: false, + status: SpaceshipRampStatus.withoutBonus, + ), + isNotNull, + ); + }); + }); + + test( + 'throws AssertionError ' + 'when hits is negative', + () { + expect( + () => SpaceshipRampState( + hits: -1, + balls: const {}, + shot: false, + status: SpaceshipRampStatus.withoutBonus, + ), + throwsAssertionError, + ); + }, + ); + + group('copyWith', () { + test( + 'throws AssertionError ' + 'when hits is decreased', + () { + const rampState = SpaceshipRampState( + hits: 0, + balls: {}, + shot: false, + status: SpaceshipRampStatus.withoutBonus, + ); + expect( + () => rampState.copyWith(hits: rampState.hits - 1), + throwsAssertionError, + ); + }, + ); + + test( + 'copies correctly ' + 'when no argument specified', + () { + const rampState = SpaceshipRampState( + hits: 0, + balls: {}, + shot: false, + status: SpaceshipRampStatus.withoutBonus, + ); + expect( + rampState.copyWith(), + equals(rampState), + ); + }, + ); + + test( + 'copies correctly ' + 'when all arguments specified', + () { + final ball = Ball(baseColor: Colors.black); + + const rampState = SpaceshipRampState( + hits: 0, + balls: {}, + shot: false, + status: SpaceshipRampStatus.withoutBonus, + ); + final otherRampState = SpaceshipRampState( + hits: rampState.hits + 1, + balls: {ball}, + shot: true, + status: SpaceshipRampStatus.withBonus, + ); + expect(rampState, isNot(equals(otherRampState))); + + expect( + rampState.copyWith( + hits: rampState.hits + 1, + balls: {ball}, + shot: true, + status: SpaceshipRampStatus.withBonus, + ), + equals(otherRampState), + ); + }, + ); + }); + }); +} diff --git a/packages/pinball_components/test/src/components/spaceship_ramp/spaceship_ramp_test.dart b/packages/pinball_components/test/src/components/spaceship_ramp/spaceship_ramp_test.dart index e4803411..1b8fcbb5 100644 --- a/packages/pinball_components/test/src/components/spaceship_ramp/spaceship_ramp_test.dart +++ b/packages/pinball_components/test/src/components/spaceship_ramp/spaceship_ramp_test.dart @@ -8,7 +8,7 @@ import 'package:pinball_components/pinball_components.dart'; import '../../../helpers/helpers.dart'; -class MockRampSensorCubit extends Mock implements RampSensorCubit {} +class _MockSpaceshipRampCubit extends Mock implements SpaceshipRampCubit {} void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -22,27 +22,23 @@ void main() { ]; final flameTester = FlameTester(() => TestGame(assets)); - group('RampSensor', () { - flameTester.test('closes bloc when removed', (game) async { - final bloc = MockRampSensorCubit(); - whenListen( - bloc, - const Stream.empty(), - initialState: const RampSensorState.initial(), - ); - when(bloc.close).thenAnswer((_) async {}); - final rampSensor = RampSensor.test( - type: RampSensorType.door, - bloc: bloc, - ); - final parent = SpaceshipRamp.test(); - - await game.ensureAdd(parent); - await parent.ensureAdd(rampSensor); - parent.remove(rampSensor); - await game.ready(); - - verify(bloc.close).called(1); - }); + flameTester.test('closes bloc when removed', (game) async { + final bloc = _MockSpaceshipRampCubit(); + whenListen( + bloc, + const Stream.empty(), + initialState: SpaceshipRampState.initial(), + ); + when(bloc.close).thenAnswer((_) async {}); + + final ramp = SpaceshipRamp.test( + bloc: bloc, + ); + + await game.ensureAdd(ramp); + game.remove(ramp); + await game.ready(); + + verify(bloc.close).called(1); }); }