From a15487846597c18816b2dfbb07bb6a50edbfa742 Mon Sep 17 00:00:00 2001 From: RuiAlonso Date: Mon, 2 May 2022 15:07:03 +0200 Subject: [PATCH] refactor: added behaviors to AndroidAcres --- .../{ => android_acres}/android_acres.dart | 16 ++ .../android_acres/behaviors/behaviors.dart | 2 + .../behaviors/ramp_bonus_behavior.dart | 84 +++++++ .../behaviors/ramp_shot_behavior.dart | 82 +++++++ lib/game/components/components.dart | 2 +- .../behaviors/ramp_bonus_behavior_test.dart | 208 ++++++++++++++++++ .../behaviors/ramp_shot_behavior_test.dart | 204 +++++++++++++++++ 7 files changed, 597 insertions(+), 1 deletion(-) rename lib/game/components/{ => android_acres}/android_acres.dart (68%) create mode 100644 lib/game/components/android_acres/behaviors/behaviors.dart create mode 100644 lib/game/components/android_acres/behaviors/ramp_bonus_behavior.dart create mode 100644 lib/game/components/android_acres/behaviors/ramp_shot_behavior.dart create mode 100644 test/game/components/android_acres/behaviors/ramp_bonus_behavior_test.dart create mode 100644 test/game/components/android_acres/behaviors/ramp_shot_behavior_test.dart diff --git a/lib/game/components/android_acres.dart b/lib/game/components/android_acres/android_acres.dart similarity index 68% rename from lib/game/components/android_acres.dart rename to lib/game/components/android_acres/android_acres.dart index 489dc2e5..3ca2c958 100644 --- a/lib/game/components/android_acres.dart +++ b/lib/game/components/android_acres/android_acres.dart @@ -1,6 +1,8 @@ // ignore_for_file: avoid_renaming_method_parameters import 'package:flame/components.dart'; +import 'package:flutter/material.dart'; +import 'package:pinball/game/components/android_acres/behaviors/behaviors.dart'; import 'package:pinball/game/game.dart'; import 'package:pinball_components/pinball_components.dart'; @@ -31,6 +33,20 @@ class AndroidAcres extends Component { ScoringBehavior(points: 20), ], )..initialPosition = Vector2(-20.5, -13.8), + RampShotBehavior( + points: 5000, + scorePosition: Vector2(0, -45), + ), + RampBonusBehavior( + points: 1000000, + scorePosition: Vector2(0, -60), + ), ], ); + + /// Creates a [AndroidAcres] without any children. + /// + /// This can be used for testing [AndroidAcres]'s behaviors in isolation. + @visibleForTesting + AndroidAcres.test(); } diff --git a/lib/game/components/android_acres/behaviors/behaviors.dart b/lib/game/components/android_acres/behaviors/behaviors.dart new file mode 100644 index 00000000..d10213fb --- /dev/null +++ b/lib/game/components/android_acres/behaviors/behaviors.dart @@ -0,0 +1,2 @@ +export 'ramp_bonus_behavior.dart'; +export 'ramp_shot_behavior.dart'; diff --git a/lib/game/components/android_acres/behaviors/ramp_bonus_behavior.dart b/lib/game/components/android_acres/behaviors/ramp_bonus_behavior.dart new file mode 100644 index 00000000..7ed596c7 --- /dev/null +++ b/lib/game/components/android_acres/behaviors/ramp_bonus_behavior.dart @@ -0,0 +1,84 @@ +import 'dart:collection'; +import 'dart:math'; + +import 'package:flame/components.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +/// {@template ramp_bonus_behavior} +/// When a [Ball] shot inside the [SpaceshipRamp] 10 times increases score. +/// {@endtemplate} +class RampBonusBehavior extends Component + with ParentIsA, HasGameRef { + /// {@macro ramp_bonus_behavior} + RampBonusBehavior({ + required int points, + required Vector2 scorePosition, + }) : _points = points, + _scorePosition = scorePosition, + super(); + + final int _oneMillionPointsTimes = 10; + final int _points; + final Vector2 _scorePosition; + + final Set _balls = HashSet(); + int _previousHits = 0; + + @override + void onMount() { + super.onMount(); + + final sensors = parent.children.whereType(); + for (final sensor in sensors) { + sensor.bloc.stream.listen((state) { + switch (state.type) { + case RampSensorType.door: + _handleOnDoor(state.ball!); + break; + case RampSensorType.inside: + _handleOnInside(state.ball!); + break; + } + }); + } + } + + void _handleOnDoor(Ball ball) { + if (!_balls.contains(ball)) { + _balls.add(ball); + } + } + + void _handleOnInside(Ball ball) { + if (_balls.contains(ball)) { + _balls.remove(ball); + _previousHits++; + _shot(_previousHits); + } + } + + /// When a [Ball] shot the [SpaceshipRamp] it achieve improvements for the + /// current game, like multipliers or score. + void _shot(int currentHits) { + if (currentHits % _oneMillionPointsTimes == 0) { + gameRef.read().add(Scored(points: _points)); + gameRef.add( + ScoreText( + text: _points.toString(), + position: _getRandomPosition, + ), + ); + } + } + + Vector2 get _getRandomPosition { + final randomX = Random().nextInt(2); + final randomY = Random().nextInt(2); + final sign = randomX + randomY % 2; + + return _scorePosition + + Vector2(sign * randomX.toDouble(), sign * randomY.toDouble()); + } +} diff --git a/lib/game/components/android_acres/behaviors/ramp_shot_behavior.dart b/lib/game/components/android_acres/behaviors/ramp_shot_behavior.dart new file mode 100644 index 00000000..b339b7b4 --- /dev/null +++ b/lib/game/components/android_acres/behaviors/ramp_shot_behavior.dart @@ -0,0 +1,82 @@ +import 'dart:collection'; +import 'dart:math'; + +import 'package:flame/components.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; +import 'package:pinball_flame/pinball_flame.dart'; + +/// {@template ramp_shot_behavior} +/// When a [Ball] shot inside the [SpaceshipRamp] it achieve improvements for +/// the current game, like multipliers or score. +/// {@endtemplate} +class RampShotBehavior extends Component + with ParentIsA, HasGameRef { + /// {@macro ramp_shot_behavior} + RampShotBehavior({ + required int points, + required Vector2 scorePosition, + }) : _points = points, + _scorePosition = scorePosition, + super(); + + final int _points; + final Vector2 _scorePosition; + + final Set _balls = HashSet(); + + @override + void onMount() { + super.onMount(); + + final sensors = parent.children.whereType(); + for (final sensor in sensors) { + sensor.bloc.stream.listen((state) { + switch (state.type) { + case RampSensorType.door: + _handleOnDoor(state.ball!); + break; + case RampSensorType.inside: + _handleOnInside(state.ball!); + break; + } + }); + } + } + + void _handleOnDoor(Ball ball) { + if (!_balls.contains(ball)) { + _balls.add(ball); + } + } + + void _handleOnInside(Ball ball) { + if (_balls.contains(ball)) { + _balls.remove(ball); + _shot(); + } + } + + void _shot() { + parent.spaceshipRamp.progress(); + + gameRef.read() + ..add(const MultiplierIncreased()) + ..add(Scored(points: _points)); + gameRef.add( + ScoreText( + text: _points.toString(), + position: _getRandomPosition, + ), + ); + } + + Vector2 get _getRandomPosition { + final randomX = Random().nextInt(3); + final randomY = Random().nextInt(3); + final sign = randomX + randomY % 2; + + return _scorePosition + + Vector2(sign * randomX.toDouble(), sign * randomY.toDouble()); + } +} diff --git a/lib/game/components/components.dart b/lib/game/components/components.dart index 5af4efc0..a8eabb13 100644 --- a/lib/game/components/components.dart +++ b/lib/game/components/components.dart @@ -1,4 +1,4 @@ -export 'android_acres.dart'; +export 'android_acres/android_acres.dart'; export 'bottom_group.dart'; export 'camera_controller.dart'; export 'controlled_ball.dart'; diff --git a/test/game/components/android_acres/behaviors/ramp_bonus_behavior_test.dart b/test/game/components/android_acres/behaviors/ramp_bonus_behavior_test.dart new file mode 100644 index 00000000..750aa354 --- /dev/null +++ b/test/game/components/android_acres/behaviors/ramp_bonus_behavior_test.dart @@ -0,0 +1,208 @@ +// ignore_for_file: cascade_invocations, prefer_const_constructors + +import 'dart:async'; + +import 'package:bloc_test/bloc_test.dart'; +import 'package:flame/components.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockingjay/mockingjay.dart'; +import 'package:pinball/game/components/android_acres/behaviors/behaviors.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; + +import '../../../../helpers/helpers.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final assets = [ + Assets.images.android.spaceship.saucer.keyName, + Assets.images.android.spaceship.animatronic.keyName, + Assets.images.android.spaceship.lightBeam.keyName, + Assets.images.android.ramp.boardOpening.keyName, + Assets.images.android.ramp.railingForeground.keyName, + Assets.images.android.ramp.railingBackground.keyName, + Assets.images.android.ramp.main.keyName, + Assets.images.android.ramp.arrow.inactive.keyName, + Assets.images.android.ramp.arrow.active1.keyName, + Assets.images.android.ramp.arrow.active2.keyName, + Assets.images.android.ramp.arrow.active3.keyName, + Assets.images.android.ramp.arrow.active4.keyName, + Assets.images.android.ramp.arrow.active5.keyName, + Assets.images.android.rail.main.keyName, + Assets.images.android.rail.exit.keyName, + Assets.images.android.bumper.a.lit.keyName, + Assets.images.android.bumper.a.dimmed.keyName, + Assets.images.android.bumper.b.lit.keyName, + Assets.images.android.bumper.b.dimmed.keyName, + Assets.images.android.bumper.cow.lit.keyName, + Assets.images.android.bumper.cow.dimmed.keyName, + ]; + + group('RampBonusBehavior', () { + const bonusPoints = 1000000; + + late GameBloc gameBloc; + + setUp(() { + gameBloc = MockGameBloc(); + whenListen( + gameBloc, + const Stream.empty(), + initialState: const GameState.initial(), + ); + }); + + final flameBlocTester = FlameBlocTester( + gameBuilder: EmptyPinballTestGame.new, + blocBuilder: () => gameBloc, + assets: assets, + ); + + flameBlocTester.testGameWidget( + "hit on door sensor doesn't add any score", + setUp: (game, tester) async { + final ball = Ball(baseColor: Colors.red); + final behavior = RampBonusBehavior( + points: bonusPoints, + scorePosition: Vector2.zero(), + ); + final parent = AndroidAcres.test(); + final sensors = [ + RampSensor.test( + type: RampSensorType.door, + bloc: RampSensorCubit(), + ), + ]; + + await parent.addAll(sensors); + await game.ensureAdd(parent); + await parent.ensureAdd(behavior); + + for (final sensor in sensors) { + sensor.bloc.onDoor(ball); + } + await tester.pump(); + + final scores = game.descendants().whereType(); + await game.ready(); + + verifyNever(() => gameBloc.add(Scored(points: bonusPoints))); + expect(scores.length, 0); + }, + ); + + flameBlocTester.testGameWidget( + 'hit on inside sensor without previous hit on door sensor ' + "doesn't add any score", + setUp: (game, tester) async { + final ball = Ball(baseColor: Colors.red); + + final behavior = RampBonusBehavior( + points: bonusPoints, + scorePosition: Vector2.zero(), + ); + final parent = AndroidAcres.test(); + final doorSensor = RampSensor.test( + type: RampSensorType.door, + bloc: RampSensorCubit(), + ); + final insideSensor = RampSensor.test( + type: RampSensorType.inside, + bloc: RampSensorCubit(), + ); + + await parent.addAll([doorSensor, insideSensor]); + await game.ensureAdd(parent); + await parent.ensureAdd(behavior); + + insideSensor.bloc.onInside(ball); + + await tester.pump(); + + final scores = game.descendants().whereType(); + await game.ready(); + + verifyNever(() => gameBloc.add(Scored(points: bonusPoints))); + expect(scores.length, 0); + }, + ); + + flameBlocTester.testGameWidget( + 'hit on inside sensor after hit on door sensor ' + "less than 10 times doesn't add any score neither shows score points", + setUp: (game, tester) async { + final ball = Ball(baseColor: Colors.red); + const bonusPoints = 1000000; + final behavior = RampBonusBehavior( + points: bonusPoints, + scorePosition: Vector2.zero(), + ); + final parent = AndroidAcres.test(); + final doorSensor = RampSensor.test( + type: RampSensorType.door, + bloc: RampSensorCubit(), + ); + final insideSensor = RampSensor.test( + type: RampSensorType.inside, + bloc: RampSensorCubit(), + ); + + await parent.addAll([doorSensor, insideSensor]); + await game.ensureAdd(parent); + await parent.ensureAdd(behavior); + + doorSensor.bloc.onDoor(ball); + insideSensor.bloc.onInside(ball); + + await tester.pump(); + + final scores = game.descendants().whereType(); + await game.ready(); + + verifyNever(() => gameBloc.add(Scored(points: bonusPoints))); + expect(scores.length, 0); + }, + ); + + flameBlocTester.testGameWidget( + 'hit on inside sensor after hit on door sensor ' + '10 times add score and show score point', + setUp: (game, tester) async { + final ball = Ball(baseColor: Colors.red); + const bonusPoints = 1000000; + final behavior = RampBonusBehavior( + points: bonusPoints, + scorePosition: Vector2.zero(), + ); + final parent = AndroidAcres.test(); + final doorSensor = RampSensor.test( + type: RampSensorType.door, + bloc: RampSensorCubit(), + ); + final insideSensor = RampSensor.test( + type: RampSensorType.inside, + bloc: RampSensorCubit(), + ); + + await parent.addAll([doorSensor, insideSensor]); + await game.ensureAdd(parent); + await parent.ensureAdd(behavior); + + for (var i = 0; i < 10; i++) { + doorSensor.bloc.onDoor(ball); + insideSensor.bloc.onInside(ball); + } + + await tester.pump(); + + final scores = game.descendants().whereType(); + await game.ready(); + + verify(() => gameBloc.add(Scored(points: bonusPoints))).called(1); + expect(scores.length, 1); + }, + ); + }); +} diff --git a/test/game/components/android_acres/behaviors/ramp_shot_behavior_test.dart b/test/game/components/android_acres/behaviors/ramp_shot_behavior_test.dart new file mode 100644 index 00000000..626fb3dd --- /dev/null +++ b/test/game/components/android_acres/behaviors/ramp_shot_behavior_test.dart @@ -0,0 +1,204 @@ +// ignore_for_file: cascade_invocations, prefer_const_constructors + +import 'dart:async'; + +import 'package:bloc_test/bloc_test.dart'; +import 'package:flame/components.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockingjay/mockingjay.dart'; +import 'package:pinball/game/components/android_acres/behaviors/behaviors.dart'; +import 'package:pinball/game/game.dart'; +import 'package:pinball_components/pinball_components.dart'; + +import '../../../../helpers/helpers.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + final assets = [ + Assets.images.android.spaceship.saucer.keyName, + Assets.images.android.spaceship.animatronic.keyName, + Assets.images.android.spaceship.lightBeam.keyName, + Assets.images.android.ramp.boardOpening.keyName, + Assets.images.android.ramp.railingForeground.keyName, + Assets.images.android.ramp.railingBackground.keyName, + Assets.images.android.ramp.main.keyName, + Assets.images.android.ramp.arrow.inactive.keyName, + Assets.images.android.ramp.arrow.active1.keyName, + Assets.images.android.ramp.arrow.active2.keyName, + Assets.images.android.ramp.arrow.active3.keyName, + Assets.images.android.ramp.arrow.active4.keyName, + Assets.images.android.ramp.arrow.active5.keyName, + Assets.images.android.rail.main.keyName, + Assets.images.android.rail.exit.keyName, + Assets.images.android.bumper.a.lit.keyName, + Assets.images.android.bumper.a.dimmed.keyName, + Assets.images.android.bumper.b.lit.keyName, + Assets.images.android.bumper.b.dimmed.keyName, + Assets.images.android.bumper.cow.lit.keyName, + Assets.images.android.bumper.cow.dimmed.keyName, + ]; + + group('RampShotBehavior', () { + late GameBloc gameBloc; + + setUp(() { + gameBloc = MockGameBloc(); + whenListen( + gameBloc, + const Stream.empty(), + initialState: const GameState.initial(), + ); + }); + + final flameBlocTester = FlameBlocTester( + gameBuilder: EmptyPinballTestGame.new, + blocBuilder: () => gameBloc, + assets: assets, + ); + + flameBlocTester.testGameWidget( + "hit on door sensor doesn't increase multiplier " + 'neither add any score or show any score points', + setUp: (game, tester) async { + final ball = Ball(baseColor: Colors.red); + const shotPoints = 5000; + final behavior = RampShotBehavior( + points: shotPoints, + scorePosition: Vector2.zero(), + ); + final parent = AndroidAcres.test(); + final sensors = [ + RampSensor.test( + type: RampSensorType.door, + bloc: RampSensorCubit(), + ), + ]; + + await parent.addAll(sensors); + await game.ensureAdd(parent); + await parent.ensureAdd(behavior); + + for (final sensor in sensors) { + sensor.bloc.onDoor(ball); + } + await tester.pump(); + + final scores = game.descendants().whereType(); + await game.ready(); + + verifyNever(() => gameBloc.add(MultiplierIncreased())); + verifyNever(() => gameBloc.add(Scored(points: shotPoints))); + expect(scores.length, 0); + }, + ); + + flameBlocTester.testGameWidget( + 'hit on inside sensor without previous hit on door sensor ' + "doesn't increase multiplier neither add any score or shows score points", + setUp: (game, tester) async { + final ball = Ball(baseColor: Colors.red); + const shotPoints = 5000; + final behavior = RampShotBehavior( + points: shotPoints, + scorePosition: Vector2.zero(), + ); + final parent = AndroidAcres.test(); + final doorSensor = RampSensor.test( + type: RampSensorType.door, + bloc: RampSensorCubit(), + ); + final insideSensor = RampSensor.test( + type: RampSensorType.inside, + bloc: RampSensorCubit(), + ); + + await parent.addAll([doorSensor, insideSensor]); + await game.ensureAdd(parent); + await parent.ensureAdd(behavior); + + insideSensor.bloc.onInside(ball); + + await tester.pump(); + + final scores = game.descendants().whereType(); + await game.ready(); + + verifyNever(() => gameBloc.add(MultiplierIncreased())); + verifyNever(() => gameBloc.add(Scored(points: shotPoints))); + expect(scores.length, 0); + }, + ); + + flameBlocTester.testGameWidget( + 'hit on inside sensor after hit on door sensor ' + 'increase multiplier', + setUp: (game, tester) async { + final ball = Ball(baseColor: Colors.red); + const shotPoints = 5000; + final behavior = RampShotBehavior( + points: shotPoints, + scorePosition: Vector2.zero(), + ); + final parent = AndroidAcres.test(); + final doorSensor = RampSensor.test( + type: RampSensorType.door, + bloc: RampSensorCubit(), + ); + final insideSensor = RampSensor.test( + type: RampSensorType.inside, + bloc: RampSensorCubit(), + ); + + await parent.addAll([doorSensor, insideSensor]); + await game.ensureAdd(parent); + await parent.ensureAdd(behavior); + + doorSensor.bloc.onDoor(ball); + insideSensor.bloc.onInside(ball); + + await tester.pump(); + + verify(() => gameBloc.add(MultiplierIncreased())).called(1); + }, + ); + + flameBlocTester.testGameWidget( + 'hit on inside sensor after hit on door sensor ' + 'add score and show score points', + setUp: (game, tester) async { + final ball = Ball(baseColor: Colors.red); + const shotPoints = 5000; + final behavior = RampShotBehavior( + points: shotPoints, + scorePosition: Vector2.zero(), + ); + final parent = AndroidAcres.test(); + final doorSensor = RampSensor.test( + type: RampSensorType.door, + bloc: RampSensorCubit(), + ); + final insideSensor = RampSensor.test( + type: RampSensorType.inside, + bloc: RampSensorCubit(), + ); + + await parent.addAll([doorSensor, insideSensor]); + await game.ensureAdd(parent); + await parent.ensureAdd(behavior); + + doorSensor.bloc.onDoor(ball); + insideSensor.bloc.onInside(ball); + + await tester.pump(); + + final scores = game.descendants().whereType(); + await game.ready(); + + verify(() => gameBloc.add(Scored(points: shotPoints))).called(1); + expect(scores.length, 1); + }, + ); + }); +}