Last active
June 17, 2025 18:03
-
-
Save bradmartin333/a8e4e21115acfbdd0eae28b32584fc71 to your computer and use it in GitHub Desktop.
card game demo
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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