master
ben 2024-01-10 14:36:24 +01:00
parent 7bfa961a9a
commit c93e98da00
7 changed files with 150 additions and 62 deletions

View File

@ -23,6 +23,5 @@ linter:
rules: rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule # avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at # Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options # https://dart.dev/guides/language/analysis-options

View File

@ -8,6 +8,7 @@ import 'package:flutter/scheduler.dart';
import 'package:pong/pong_menu.dart'; import 'package:pong/pong_menu.dart';
void main() { void main() {
// Run the PongGame app
runApp(const PongGame()); runApp(const PongGame());
} }
@ -17,9 +18,11 @@ class PongGame extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MaterialApp( return MaterialApp(
// Set the app title and theme
title: 'Pong Game', title: 'Pong Game',
theme: theme: ThemeData(
ThemeData(scaffoldBackgroundColor: Color.fromARGB(255, 41, 38, 38)), scaffoldBackgroundColor: const Color.fromARGB(255, 41, 38, 38)),
// Set the initial screen to the StartScreen
home: const StartScreen(), home: const StartScreen(),
); );
} }
@ -33,6 +36,7 @@ class StartScreen extends StatelessWidget {
return Scaffold( return Scaffold(
body: GestureDetector( body: GestureDetector(
onTap: () { onTap: () {
// Navigate to the GameScreen when tapped
Navigator.of(context).push(MaterialPageRoute( Navigator.of(context).push(MaterialPageRoute(
builder: (context) { builder: (context) {
return const GameScreen(); return const GameScreen();
@ -40,6 +44,7 @@ class StartScreen extends StatelessWidget {
)); ));
}, },
child: const Center( child: const Center(
// Display a message prompting the user to touch the screen to begin
child: Text( child: Text(
'Berühren um zu beginnen!', 'Berühren um zu beginnen!',
style: TextStyle( style: TextStyle(
@ -58,85 +63,131 @@ class GameScreen extends StatefulWidget {
const GameScreen({super.key}); const GameScreen({super.key});
@override @override
State<GameScreen> createState() => _GameScreenState(); State<GameScreen> createState() => GameScreenState();
} }
@visibleForTesting class GameScreenState extends State<GameScreen> {
class _GameScreenState extends State<GameScreen> { // Constants defining properties of the game
final ballSize = 20.0; final ballSize = 20.0;
final racketWidth = 240.0; final racketWidth = 100.0;
final racketHeight = 25.0; final racketHeight = 10.0;
final racketBottomOffset = 100.0; final racketBottomOffset = 100.0;
bool isPaused = false;
final initialBallSpeed = 2.0; // Initial speed of the ball
final initialBallSpeed = 3.0;
double ballX = 0; // Variables to track ball and racket positions, speed, score, and game state
double ballY = 0; double ballPositionX = 0;
double ballPositionY = 0;
double ballSpeedX = 0; double ballSpeedX = 0;
double ballSpeedY = 0; double ballSpeedY = 0;
double racketX = 20; double racketX = 20;
int score = 0; int score = 0;
// Ticker for updating the game state
late Ticker ticker; late Ticker ticker;
final double ballSpeedMultiplier = 2; double ballSpeedMultiplier = 1.1;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
// Initialize the game state when the widget is created
startGame(); startGame();
} }
@override // Initialize the game state
void dispose() {
super.dispose();
stopGame();
}
void startGame() { void startGame() {
final random = Random(); final random = Random();
ballX = 0; ballPositionX = 0;
ballY = 0; ballPositionY = 0;
ballSpeedX = initialBallSpeed; ballSpeedX = initialBallSpeed;
ballSpeedY = initialBallSpeed; ballSpeedY = initialBallSpeed;
ballSpeedMultiplier = 1.1;
racketX = 200; racketX = 200;
score = 0; score = 0;
// Randomize initial ball direction
if (random.nextBool()) ballSpeedX = -ballSpeedX; if (random.nextBool()) ballSpeedX = -ballSpeedX;
if (random.nextBool()) ballSpeedY = -ballSpeedY; if (random.nextBool()) ballSpeedY = -ballSpeedY;
Future.delayed(const Duration(seconds: 1), () { // Start the game loop after a delay
ticker = Ticker((elapsed) { continueGame();
setState(() {
moveBall(ballSpeedMultiplier);
});
});
ticker.start();
});
} }
// Stop the game loop
void stopGame() { void stopGame() {
ticker.dispose(); ticker.dispose();
} }
void moveBall(double ballSPeedMultiplier) { // Continue the game loop with a delay
ballX += ballSpeedX * ballSpeedMultiplier; void continueGame() {
ballY += ballSpeedY * ballSpeedMultiplier; if (!isPaused) {
final Size size = MediaQuery.of(context).size; Future.delayed(const Duration(seconds: 1), () {
ticker = Ticker((elapsed) {
setState(() {
moveBall(ballSpeedMultiplier);
});
});
ticker.start();
});
}
}
if (ballX < 0 || ballX > size.width - ballSize) { void moveBall(double ballSpeedMultiplier) {
// Update ball position based on speed and direction
ballPositionX += ballSpeedX * ballSpeedMultiplier;
ballPositionY += ballSpeedY * ballSpeedMultiplier;
// Get the screen size
final Size screenSize = MediaQuery.of(context).size;
// Check for collisions with horizontal walls and bounce the ball
if (ballPositionX < 0 || ballPositionX > screenSize.width - ballSize) {
ballSpeedX = -ballSpeedX; ballSpeedX = -ballSpeedX;
} }
if (ballY < 0) { // Check for collision with the top wall and bounce the ball
if (ballPositionY < 0) {
ballSpeedY = -ballSpeedY; ballSpeedY = -ballSpeedY;
} }
// Was prüft diese if-Anweisung? // Check if the ball has touched the floor
if (ballY > size.height - ballSize - racketHeight - racketBottomOffset && var ballTouchesFloor = ballPositionY > screenSize.height - ballSize;
ballX >= racketX &&
ballX <= racketX + racketWidth) { // Check if the ball collides with the racket
ballSpeedY = -ballSpeedY; var ballCollidesWithRacket =
} else if (ballY > size.height - ballSize) { ballPositionY + ballSize >= screenSize.height - racketBottomOffset &&
ballPositionY <= screenSize.height - racketBottomOffset &&
ballPositionX + ballSize >= racketX &&
ballPositionX <= racketX + racketWidth;
// Handle collision with the racket
if (ballCollidesWithRacket) {
// Check if the ball hits the top of the racket
if (ballPositionY <= screenSize.height - racketBottomOffset) {
ballSpeedY =
-ballSpeedY * ballSpeedMultiplier; // Bounce the ball upward
setState(() {
score += 1; // Increase the score
debugPrint('ballSpeedMultiplier: $ballSpeedMultiplier');
});
ballPositionY = screenSize.height - racketBottomOffset - ballSize;
// Adjust the Y position to stay above the racket
} else {
ballSpeedX =
-ballSpeedX * ballSpeedMultiplier; // Bounce the ball backward
// Adjust the Y position to prevent the ball from going through the racket
double adjustedY = screenSize.height - racketBottomOffset - ballSize;
if (ballPositionY < adjustedY) {
ballPositionY = adjustedY;
}
}
// Handle game over condition if the ball touches the floor
} else if (ballTouchesFloor) {
debugPrint('Game over'); debugPrint('Game over');
stopGame(); stopGame();
showDialog( showDialog(
@ -147,7 +198,7 @@ class _GameScreenState extends State<GameScreen> {
title: 'Game over!', title: 'Game over!',
subTitle: 'Deine Punkte: $score', subTitle: 'Deine Punkte: $score',
child: CupertinoButton( child: CupertinoButton(
child: const Text('Nochmal Spielen'), child: const Text('Nochmal spielen'),
onPressed: () { onPressed: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
startGame(); startGame();
@ -159,7 +210,8 @@ class _GameScreenState extends State<GameScreen> {
} }
} }
moveRacket(double x) { void moveRacket(double x) {
// Move the racket horizontally based on the user's touch position
setState(() { setState(() {
racketX = x - racketWidth / 2; racketX = x - racketWidth / 2;
}); });
@ -171,6 +223,7 @@ class _GameScreenState extends State<GameScreen> {
body: Stack( body: Stack(
children: [ children: [
GestureDetector( GestureDetector(
// Listen for horizontal drag updates to move the racket
onHorizontalDragUpdate: (details) { onHorizontalDragUpdate: (details) {
moveRacket(details.globalPosition.dx); moveRacket(details.globalPosition.dx);
}, },
@ -181,13 +234,14 @@ class _GameScreenState extends State<GameScreen> {
racketX: racketX, racketX: racketX,
racketBottomOffset: racketBottomOffset, racketBottomOffset: racketBottomOffset,
ballSize: ballSize, ballSize: ballSize,
ballX: ballX, ballX: ballPositionX,
ballY: ballY, ballY: ballPositionY,
), ),
size: Size.infinite, size: Size.infinite,
), ),
), ),
Center( Center(
// Display the player's score in the center of the screen
child: Text( child: Text(
'Punkte: $score', 'Punkte: $score',
style: const TextStyle( style: const TextStyle(
@ -197,24 +251,32 @@ class _GameScreenState extends State<GameScreen> {
), ),
), ),
Positioned( Positioned(
top: 20, top: 40,
right: 20, right: 20,
child: IconButton( child: IconButton(
icon: const Icon( icon: const Icon(
CupertinoIcons.pause, CupertinoIcons.pause,
size: 30, size: 30,
color: Colors.white,
), ),
onPressed: () { onPressed: () {
// Pause the game and show dialog
stopGame(); stopGame();
showDialog( showDialog(
context: context, context: context,
builder: (context) { barrierDismissible: false,
builder: (BuildContext context) {
return PongMenu( return PongMenu(
title: 'Pause', title: 'Pause',
subTitle: 'Deine Punkte: $score', subTitle: 'Deine Punkte: $score',
child: CupertinoButton( child: CupertinoButton(
onPressed: () {}, onPressed: () {
child: const Text('Weiter'), // Continues the game
Navigator.of(context).pop();
isPaused = false;
continueGame();
},
child: const Text('Fortfahren'),
), ),
); );
}); });
@ -237,6 +299,7 @@ class PongGamePainter extends CustomPainter {
final double racketHeight; final double racketHeight;
final double racketBottomOffset; final double racketBottomOffset;
// Constructor to initialize the painter with required properties
PongGamePainter({ PongGamePainter({
required this.ballSize, required this.ballSize,
required this.ballX, required this.ballX,
@ -249,14 +312,18 @@ class PongGamePainter extends CustomPainter {
@override @override
void paint(Canvas canvas, Size size) { void paint(Canvas canvas, Size size) {
// Paint object for drawing the racket
final racketPaint = Paint()..color = Colors.white; final racketPaint = Paint()..color = Colors.white;
// Paint object for drawing the ball
final ballPaint = Paint()..color = Colors.white; final ballPaint = Paint()..color = Colors.white;
// Draw the ball as an oval on the canvas
canvas.drawOval( canvas.drawOval(
Rect.fromLTWH(ballX, ballY, ballSize, ballSize), Rect.fromLTWH(ballX, ballY, ballSize, ballSize),
ballPaint, ballPaint,
); );
// Draw the racket as a rectangle on the canvas
canvas.drawRect( canvas.drawRect(
Rect.fromLTWH( Rect.fromLTWH(
racketX, racketX,
@ -270,6 +337,7 @@ class PongGamePainter extends CustomPainter {
@override @override
bool shouldRepaint(covariant PongGamePainter oldDelegate) { bool shouldRepaint(covariant PongGamePainter oldDelegate) {
// Repaint only if the ball or racket position changes
return ballX != oldDelegate.ballX || return ballX != oldDelegate.ballX ||
ballY != oldDelegate.ballY || ballY != oldDelegate.ballY ||
racketX != oldDelegate.racketX; racketX != oldDelegate.racketX;

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class PongMenu extends StatelessWidget { class PongMenu extends StatelessWidget {
// Constructor to initialize the PongMenu with required properties
const PongMenu({ const PongMenu({
super.key, super.key,
required this.title, required this.title,
@ -14,9 +15,13 @@ class PongMenu extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// Get the size of the screen
final mq = MediaQuery.of(context).size; final mq = MediaQuery.of(context).size;
// Calculate the height and width of the dialog
final dialogHeight = mq.height * 0.4; final dialogHeight = mq.height * 0.4;
final dialogWidth = mq.width * 0.8; final dialogWidth = mq.width * 0.8;
// Return a custom-styled Dialog with specified properties
return Dialog( return Dialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
elevation: 0, elevation: 0,
@ -25,27 +30,32 @@ class PongMenu extends StatelessWidget {
width: dialogWidth, width: dialogWidth,
height: dialogHeight, height: dialogHeight,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: const Color.fromARGB(255, 33, 33, 33),
borderRadius: BorderRadius.circular(25), borderRadius: BorderRadius.circular(25),
), ),
child: Column( child: Column(
mainAxisSize: MainAxisSize.max, mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [ children: [
// Display the title with custom styling
Text( Text(
title, title,
style: const TextStyle( style: const TextStyle(
fontSize: 30, fontSize: 30,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Colors.white,
), ),
), ),
// Display the subTitle with custom styling
Text( Text(
subTitle, subTitle,
style: const TextStyle( style: const TextStyle(
fontSize: 30, fontSize: 30,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Colors.white,
), ),
), ),
// Display the child widget
child, child,
], ],
), ),

View File

@ -66,10 +66,10 @@ packages:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: flutter_lints name: flutter_lints
sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04 sha256: e2a421b7e59244faef694ba7b30562e489c2b489866e505074eb005cd7060db7
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.3" version: "3.0.1"
flutter_test: flutter_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter
@ -79,10 +79,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: lints name: lints
sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.1" version: "3.0.0"
matcher: matcher:
dependency: transitive dependency: transitive
description: description:

View File

@ -44,7 +44,7 @@ dev_dependencies:
# activated in the `analysis_options.yaml` file located at the root of your # activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint # package. See that file for information about deactivating specific lint
# rules and activating additional ones. # rules and activating additional ones.
flutter_lints: ^2.0.0 flutter_lints: ^3.0.1
# For information on the generic Dart part of this file, see the # For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec # following page: https://dart.dev/tools/pub/pubspec

View File

@ -1,9 +1,19 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:pong/main.dart'; import 'package:pong/main.dart';
void main() { void main() {
test('moveRacket', () { test('moveRacket adjusts racketX correctly', () {
; // Arrange
final gameScreenState = GameScreenState();
const initialRacketX = 20.0;
const racketWidth = 100.0;
// Act
gameScreenState.moveRacket(initialRacketX +
50.0); // Simuliere einen horizontalen Drag an der Position initialRacketX + 50
// Assert
expect(gameScreenState.racketX,
equals(initialRacketX + 50.0 - racketWidth / 2));
}); });
} }

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:pong/main.dart'; import 'package:pong/main.dart';
@ -12,11 +13,11 @@ void main() {
expect(tapToStart, findsOneWidget); expect(tapToStart, findsOneWidget);
}); });
testWidgets('Tests ', (WidgetTester tester) async { testWidgets('Tests "Punkte:"', (WidgetTester tester) async {
final tapToStart = find.text("Berühren um zu beginnen!"); final points = find.text("Punkte: 0");
await tester.pumpWidget(const MaterialApp(home: StartScreen())); await tester.pumpWidget(const MaterialApp(home: GameScreen()));
await tester.tap(tapToStart);
await tester.pump(); expect(points, findsOneWidget);
}); });
} }