Skip to content

Instantly share code, notes, and snippets.

@bradmartin333
Last active June 17, 2025 18:03
Show Gist options
  • Save bradmartin333/a8e4e21115acfbdd0eae28b32584fc71 to your computer and use it in GitHub Desktop.
Save bradmartin333/a8e4e21115acfbdd0eae28b32584fc71 to your computer and use it in GitHub Desktop.
card game demo
import 'package:flutter/material.dart';
import 'dart:math';
void main() {
runApp(const CardGameApp());
}
class CardGameApp extends StatelessWidget {
const CardGameApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(title: 'Card Game', home: const CardGamePage());
}
}
class CardGamePage extends StatefulWidget {
const CardGamePage({super.key});
@override
State<CardGamePage> createState() => _CardGamePageState();
}
class _CardGamePageState extends State<CardGamePage>
with SingleTickerProviderStateMixin {
final List<String> _allCardAssets = [
'https://raw.githubusercontent.com/hayeah/playing-cards-assets/refs/heads/master/png/10_of_clubs.png',
'https://raw.githubusercontent.com/hayeah/playing-cards-assets/refs/heads/master/png/10_of_diamonds.png',
'https://raw.githubusercontent.com/hayeah/playing-cards-assets/refs/heads/master/png/10_of_hearts.png',
'https://raw.githubusercontent.com/hayeah/playing-cards-assets/refs/heads/master/png/10_of_spades.png',
'https://raw.githubusercontent.com/hayeah/playing-cards-assets/refs/heads/master/png/2_of_clubs.png',
'https://raw.githubusercontent.com/hayeah/playing-cards-assets/refs/heads/master/png/2_of_diamonds.png',
'https://raw.githubusercontent.com/hayeah/playing-cards-assets/refs/heads/master/png/2_of_hearts.png',
'https://raw.githubusercontent.com/hayeah/playing-cards-assets/refs/heads/master/png/2_of_spades.png',
'https://raw.githubusercontent.com/hayeah/playing-cards-assets/refs/heads/master/png/3_of_clubs.png',
'https://raw.githubusercontent.com/hayeah/playing-cards-assets/refs/heads/master/png/3_of_diamonds.png',
'https://raw.githubusercontent.com/hayeah/playing-cards-assets/refs/heads/master/png/3_of_hearts.png',
'https://raw.githubusercontent.com/hayeah/playing-cards-assets/refs/heads/master/png/3_of_spades.png',
'https://raw.githubusercontent.com/hayeah/playing-cards-assets/refs/heads/master/png/4_of_clubs.png',
'https://raw.githubusercontent.com/hayeah/playing-cards-assets/refs/heads/master/png/4_of_diamonds.png',
'https://raw.githubusercontent.com/hayeah/playing-cards-assets/refs/heads/master/png/4_of_hearts.png',
'https://raw.githubusercontent.com/hayeah/playing-cards-assets/refs/heads/master/png/4_of_spades.png',
'https://raw.githubusercontent.com/hayeah/playing-cards-assets/refs/heads/master/png/5_of_clubs.png',
'https://raw.githubusercontent.com/hayeah/playing-cards-assets/refs/heads/master/png/5_of_diamonds.png',
'https://raw.githubusercontent.com/hayeah/playing-cards-assets/refs/heads/master/png/5_of_hearts.png',
'https://raw.githubusercontent.com/hayeah/playing-cards-assets/refs/heads/master/png/5_of_spades.png',
'https://raw.githubusercontent.com/hayeah/playing-cards-assets/refs/heads/master/png/6_of_clubs.png',
'https://raw.githubusercontent.com/hayeah/playing-cards-assets/refs/heads/master/png/6_of_diamonds.png',
'https://raw.githubusercontent.com/hayeah/playing-cards-assets/refs/heads/master/png/6_of_hearts.png',
'https://raw.githubusercontent.com/hayeah/playing-cards-assets/refs/heads/master/png/6_of_spades.png',
'https://raw.githubusercontent.com/hayeah/playing-cards-assets/refs/heads/master/png/7_of_clubs.png',
'https://raw.githubusercontent.com/hayeah/playing-cards-assets/refs/heads/master/png/7_of_diamonds.png',
'https://raw.githubusercontent.com/hayeah/playing-cards-assets/refs/heads/master/png/7_of_hearts.png',
'https://raw.githubusercontent.com/hayeah/playing-cards-assets/refs/heads/master/png/7_of_spades.png',
'https://raw.githubusercontent.com/hayeah/playing-cards-assets/refs/heads/master/png/8_of_clubs.png',
'https://raw.githubusercontent.com/hayeah/playing-cards-assets/refs/heads/master/png/8_of_diamonds.png',
'https://raw.githubusercontent.com/hayeah/playing-cards-assets/refs/heads/master/png/8_of_hearts.png',
'https://raw.githubusercontent.com/hayeah/playing-cards-assets/refs/heads/master/png/8_of_spades.png',
'https://raw.githubusercontent.com/hayeah/playing-cards-assets/refs/heads/master/png/9_of_clubs.png',
'https://raw.githubusercontent.com/hayeah/playing-cards-assets/refs/heads/master/png/9_of_diamonds.png',
'https://raw.githubusercontent.com/hayeah/playing-cards-assets/refs/heads/master/png/9_of_hearts.png',
'https://raw.githubusercontent.com/hayeah/playing-cards-assets/refs/heads/master/png/9_of_spades.png',
'https://raw.githubusercontent.com/hayeah/playing-cards-assets/refs/heads/master/png/ace_of_clubs.png',
'https://raw.githubusercontent.com/hayeah/playing-cards-assets/refs/heads/master/png/ace_of_diamonds.png',
'https://raw.githubusercontent.com/hayeah/playing-cards-assets/refs/heads/master/png/ace_of_hearts.png',
'https://raw.githubusercontent.com/hayeah/playing-cards-assets/refs/heads/master/png/ace_of_spades.png',
'https://raw.githubusercontent.com/hayeah/playing-cards-assets/refs/heads/master/png/black_joker.png',
'https://raw.githubusercontent.com/hayeah/playing-cards-assets/refs/heads/master/png/jack_of_clubs.png',
'https://raw.githubusercontent.com/hayeah/playing-cards-assets/refs/heads/master/png/jack_of_diamonds.png',
'https://raw.githubusercontent.com/hayeah/playing-cards-assets/refs/heads/master/png/jack_of_hearts.png',
'https://raw.githubusercontent.com/hayeah/playing-cards-assets/refs/heads/master/png/jack_of_spades.png',
'https://raw.githubusercontent.com/hayeah/playing-cards-assets/refs/heads/master/png/king_of_clubs.png',
'https://raw.githubusercontent.com/hayeah/playing-cards-assets/refs/heads/master/png/king_of_diamonds.png',
'https://raw.githubusercontent.com/hayeah/playing-cards-assets/refs/heads/master/png/king_of_hearts.png',
'https://raw.githubusercontent.com/hayeah/playing-cards-assets/refs/heads/master/png/king_of_spades.png',
'https://raw.githubusercontent.com/hayeah/playing-cards-assets/refs/heads/master/png/queen_of_clubs.png',
'https://raw.githubusercontent.com/hayeah/playing-cards-assets/refs/heads/master/png/queen_of_diamonds.png',
'https://raw.githubusercontent.com/hayeah/playing-cards-assets/refs/heads/master/png/queen_of_hearts.png',
'https://raw.githubusercontent.com/hayeah/playing-cards-assets/refs/heads/master/png/queen_of_spades.png',
'https://raw.githubusercontent.com/hayeah/playing-cards-assets/refs/heads/master/png/red_joker.png',
];
final String _cardBack =
'https://raw.githubusercontent.com/hayeah/playing-cards-assets/refs/heads/master/png/back.png';
List<String> _topPlayerCards = [];
List<String> _bottomPlayerCards = [];
List<String> _centerPiles = [];
// GlobalKeys for each card to get their RenderBox positions
List<GlobalKey> _topPlayerCardKeys = [];
List<GlobalKey> _bottomPlayerCardKeys = [];
List<GlobalKey> _centerPileKeys = [];
int? _selectedUserCardIndex; // Nullable to indicate no selection
bool _isTopPlayerSelected = false; // True if top player's card is selected
int? _selectedCenterPileIndex; // Nullable to indicate no selection
late AnimationController _animationController;
Animation<Offset>? _animation;
String? _animatingCardAsset;
Offset _startOffset = Offset.zero;
Offset _endOffset = Offset.zero;
@override
void initState() {
super.initState();
_dealCards();
_animationController = AnimationController(
duration: const Duration(milliseconds: 500),
vsync: this,
);
_animationController.addStatusListener((status) {
if (status == AnimationStatus.completed) {
setState(() {
// Update the target pile with the moved card after animation
if (_selectedCenterPileIndex != null && _animatingCardAsset != null) {
_centerPiles[_selectedCenterPileIndex!] = _animatingCardAsset!;
}
if (_isTopPlayerSelected) {
_topPlayerCards[_selectedUserCardIndex!] = _cardBack;
} else {
_bottomPlayerCards[_selectedUserCardIndex!] = _cardBack;
}
// Reset selection after animation
_selectedUserCardIndex = null;
_isTopPlayerSelected = false;
_selectedCenterPileIndex = null;
_animatingCardAsset = null; // Clear animating card
});
}
});
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
void _dealCards() {
final random = Random();
_allCardAssets.shuffle(random); // Shuffle all available cards
setState(() {
_topPlayerCards = _allCardAssets.sublist(0, 5); // 5 cards for top player
_bottomPlayerCards = _allCardAssets.sublist(
5,
10,
); // 5 cards for bottom player
_centerPiles = _allCardAssets.sublist(
10,
13,
); // 3 cards for center piles (or a mix of face up/down)
// Initialize unique keys for each card
_topPlayerCardKeys = List.generate(
_topPlayerCards.length,
(index) => GlobalKey(),
);
_bottomPlayerCardKeys = List.generate(
_bottomPlayerCards.length,
(index) => GlobalKey(),
);
_centerPileKeys = List.generate(
_centerPiles.length,
(index) => GlobalKey(),
);
});
}
void _onUserCardTap(int index, bool isTopPlayer) {
setState(() {
if (_selectedUserCardIndex == index &&
_isTopPlayerSelected == isTopPlayer) {
// Deselect if the same card is tapped again
_selectedUserCardIndex = null;
_isTopPlayerSelected = false;
} else {
_selectedUserCardIndex = index;
_isTopPlayerSelected = isTopPlayer;
}
// Reset center pile selection when a new user card is selected
_selectedCenterPileIndex = null;
});
_attemptMoveCard();
}
void _onCenterPileTap(int index) {
setState(() {
if (_selectedCenterPileIndex == index) {
// Deselect if the same pile is tapped again
_selectedCenterPileIndex = null;
} else {
_selectedCenterPileIndex = index;
}
});
_attemptMoveCard();
}
void _attemptMoveCard() {
if (_selectedUserCardIndex != null && _selectedCenterPileIndex != null) {
// Both a user card and a center pile are selected, initiate animation
_startAnimation();
}
}
void _startAnimation() {
// Determine the key of the selected user card
GlobalKey selectedCardKey = _isTopPlayerSelected
? _topPlayerCardKeys[_selectedUserCardIndex!]
: _bottomPlayerCardKeys[_selectedUserCardIndex!];
// Determine the key of the selected center pile
GlobalKey targetPileKey = _centerPileKeys[_selectedCenterPileIndex!];
// Get the RenderBox of the selected user card
final RenderBox userCardBox =
selectedCardKey.currentContext?.findRenderObject() as RenderBox;
_startOffset = userCardBox.localToGlobal(Offset.zero);
// Get the RenderBox of the target center pile
final RenderBox targetPileBox =
targetPileKey.currentContext?.findRenderObject() as RenderBox;
_endOffset = targetPileBox.localToGlobal(Offset.zero);
setState(() {
// Store the asset of the card that will be animated
_animatingCardAsset = _isTopPlayerSelected
? _topPlayerCards[_selectedUserCardIndex!]
: _bottomPlayerCards[_selectedUserCardIndex!];
});
_animation = Tween<Offset>(begin: _startOffset, end: _endOffset).animate(
CurvedAnimation(parent: _animationController, curve: Curves.easeInOut),
);
_animationController.forward(from: 0.0);
}
Widget _buildCard(
String assetPath,
bool isSelected,
VoidCallback onTap, {
Key? key,
bool isOriginalCardBeingAnimated = false,
}) {
return GestureDetector(
onTap: onTap,
child: Opacity(
// Hide the original card during animation
opacity: isOriginalCardBeingAnimated ? 0.0 : 1.0,
child: Card(
color: Colors.white,
elevation: isSelected ? 8.0 : 2.0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.0),
side: isSelected
? const BorderSide(color: Colors.blue, width: 3.0)
: BorderSide.none,
),
child: ClipRRect(
borderRadius: BorderRadius.circular(8.0),
child: Image.network(
assetPath,
key: key, // Attach unique key for position lookup
width: 70, // Adjust card width as needed
height: 100, // Adjust card height as needed
fit: BoxFit.contain,
),
),
),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: Stack(
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
// Top Player's Cards
_buildCardRow(
_topPlayerCards,
_topPlayerCardKeys,
(index) => _onUserCardTap(index, true),
true, // isTopPlayer
),
const Spacer(),
// Center Piles
_buildCardRow(
_centerPiles,
_centerPileKeys,
(index) => _onCenterPileTap(index),
false, // Not a player's card
isCenterPile: true,
),
const Spacer(),
// Bottom Player's Cards
_buildCardRow(
_bottomPlayerCards,
_bottomPlayerCardKeys,
(index) => _onUserCardTap(index, false),
false, // isTopPlayer
),
],
),
),
// This is the card that animates
if (_animatingCardAsset != null && _animation != null)
AnimatedBuilder(
animation: _animation!,
builder: (context, child) {
return Positioned(
left: _animation!.value.dx,
top: _animation!.value.dy,
child: _buildCard(_animatingCardAsset!, false, () {}),
);
},
),
],
),
);
}
Widget _buildCardRow(
List<String> cards,
List<GlobalKey> keys,
Function(int) onTap,
bool isTopPlayerRow, {
bool isCenterPile = false,
}) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(cards.length, (index) {
bool isSelected = false;
if (!isCenterPile) {
isSelected =
_selectedUserCardIndex == index &&
_isTopPlayerSelected == isTopPlayerRow;
} else {
isSelected = _selectedCenterPileIndex == index;
}
// Determine if this is the original card that is currently animating
bool isOriginalCardBeingAnimated = false;
if (_animatingCardAsset != null && !isCenterPile) {
if (_isTopPlayerSelected == isTopPlayerRow &&
_selectedUserCardIndex == index) {
isOriginalCardBeingAnimated = true;
}
}
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 4.0),
child: _buildCard(
cards[index],
isSelected,
() => onTap(index),
key: keys[index], // Assign the unique key for this card
isOriginalCardBeingAnimated: isOriginalCardBeingAnimated,
),
);
}),
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment