Skip to content

Instantly share code, notes, and snippets.

@bizz84
Created December 11, 2023 14:18
Show Gist options
  • Save bizz84/7a2240cbcffad312e09e9fbcab576542 to your computer and use it in GitHub Desktop.
Save bizz84/7a2240cbcffad312e09e9fbcab576542 to your computer and use it in GitHub Desktop.
A game where the player needs to find out under which card the Flutter logo is hiding.
import 'dart:math';
import 'package:flutter/material.dart';
// To use this package, run:
// dart pub add page_flip_builder
import 'package:page_flip_builder/page_flip_builder.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.indigo,
),
home: const GamePage(),
);
}
}
enum GameStatus {
playing,
notPlaying,
restarting,
}
class GamePage extends StatefulWidget {
const GamePage({super.key});
@override
State<GamePage> createState() => _GamePageState();
}
class _GamePageState extends State<GamePage> {
static final _rng = Random();
static const cardFlipDuration = Duration(milliseconds: 350);
int _targetIndex = _rng.nextInt(2);
// whether the game is currently playing (both cards are not turned)
GameStatus _gameStatus = GameStatus.playing;
// whether the player won the game (found the Flutter logo)
bool _didWin = false;
// flip key for first card
final _flipKey0 = GlobalKey<PageFlipBuilderState>();
// flip key for second card
final _flipKey1 = GlobalKey<PageFlipBuilderState>();
void _completeGame(bool didWin) {
setState(() {
_didWin = didWin;
_gameStatus = GameStatus.notPlaying;
});
}
Future<void> _tryAgain() async {
setState(() {
_gameStatus = GameStatus.restarting;
});
// hide again the flipped card
// which key to use depends on these variables:
if (_didWin && _targetIndex == 0 || !_didWin && _targetIndex == 1) {
_flipKey0.currentState?.flip();
} else {
_flipKey1.currentState?.flip();
}
// await until the card is flipped before changing the state (and revealing the new position)
await Future.delayed(cardFlipDuration);
setState(() {
_targetIndex = _rng.nextInt(2);
_gameStatus = GameStatus.playing;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Find the Flutter Logo'),
),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
// first card
GameCard(
flipKey: _flipKey0,
canFlip: _gameStatus == GameStatus.playing,
hasFlutterLogo: _targetIndex == 0,
flipDuration: cardFlipDuration,
onFlip: (frontSide) {
if (!frontSide) {
_completeGame(_targetIndex == 0);
}
},
),
// second card
GameCard(
flipKey: _flipKey1,
canFlip: _gameStatus == GameStatus.playing,
hasFlutterLogo: _targetIndex == 1,
flipDuration: cardFlipDuration,
onFlip: (frontSide) {
if (!frontSide) {
_completeGame(_targetIndex == 1);
}
},
),
// retry/end game UI
RetryGameWidget(
gameStatus: _gameStatus,
didWin: _didWin,
onRetry: _tryAgain,
),
],
),
),
),
),
);
}
}
class RetryGameWidget extends StatelessWidget {
const RetryGameWidget({
super.key,
required this.gameStatus,
required this.didWin,
this.onRetry,
});
final GameStatus gameStatus;
final bool didWin;
final VoidCallback? onRetry;
@override
Widget build(BuildContext context) {
return IgnorePointer(
// ignore any clicks if game is playing or restarting
ignoring: gameStatus != GameStatus.notPlaying,
child: AnimatedOpacity(
opacity: gameStatus == GameStatus.playing ? 0.0 : 1.0,
duration: const Duration(milliseconds: 200),
child: Column(
children: [
Text(
didWin ? 'You won!' : 'You lost!',
style: Theme.of(context).textTheme.headlineSmall!.copyWith(
color: didWin ? Colors.green : Colors.red,
fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
OutlinedButton(
style: OutlinedButton.styleFrom(
shape: const StadiumBorder(),
side: const BorderSide(width: 2, color: Colors.black54),
),
onPressed: onRetry,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text('Try Again',
style: Theme.of(context).textTheme.headlineSmall),
),
),
],
),
),
);
}
}
/// A game card that can be flipped
class GameCard extends StatelessWidget {
const GameCard({
super.key,
required this.flipKey,
required this.canFlip,
required this.hasFlutterLogo,
this.flipDuration = const Duration(milliseconds: 250),
this.onFlip,
});
final GlobalKey<PageFlipBuilderState> flipKey;
final bool canFlip;
final bool hasFlutterLogo;
final Duration flipDuration;
final ValueChanged<bool>? onFlip;
@override
Widget build(BuildContext context) {
return AspectRatio(
aspectRatio: 1.5,
child: PageFlipBuilder(
key: flipKey,
nonInteractiveAnimationDuration: flipDuration,
// only enable flip by tapping on the card
interactiveFlipEnabled: false,
// Show a card with a '?' text inside it
frontBuilder: (_) => GameCardSide(
color: Colors.yellow,
// only flip if canFlip is true
onPressed: canFlip ? () => flipKey.currentState?.flip() : null,
child: Text(
'?',
style: Theme.of(context).textTheme.headlineMedium,
),
),
// Show a card revealing the FlutterLogo or an empty SizedBox
backBuilder: (_) => GameCardSide(
color: Colors.white70,
child:
hasFlutterLogo ? const FlutterLogo(size: 160) : const SizedBox(),
),
onFlipComplete: onFlip,
),
);
}
}
class GameCardSide extends StatelessWidget {
const GameCardSide(
{super.key, required this.child, this.color, this.onPressed});
final Widget child;
final Color? color;
final VoidCallback? onPressed;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onPressed,
child: Card(
elevation: 5,
color: color,
child: Center(
child: child,
),
),
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment