import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter/foundation.dart'; import 'dart:io'; class WordlePage extends StatefulWidget { @override _WordlePageState createState() => _WordlePageState(); } class _WordlePageState extends State with TickerProviderStateMixin { static const String TARGET_WORD = "HELLO"; static const int MAX_ATTEMPTS = 6; static const int WORD_LENGTH = 5; List> grid = List.generate( MAX_ATTEMPTS, (_) => List.filled(WORD_LENGTH, ''), ); List> gridStates = List.generate( MAX_ATTEMPTS, (_) => List.filled(WORD_LENGTH, LetterState.empty), ); Map keyboardStates = {}; int currentRow = 0; int currentCol = 0; int animatingRow = -1; bool gameWon = false; bool gameLost = false; late AnimationController _revealController; late List> _tileAnimations; late FocusNode _focusNode; Set _pressedKeys = {}; @override void initState() { super.initState(); _initializeAnimations(); _initializeKeyboardStates(); _focusNode = FocusNode(); // Request focus for keyboard input on web/desktop if (kIsWeb || (!kIsWeb && !Platform.isIOS && !Platform.isAndroid)) { WidgetsBinding.instance.addPostFrameCallback((_) { _focusNode.requestFocus(); }); } } void _initializeAnimations() { _revealController = AnimationController( duration: Duration(milliseconds: 1500), vsync: this, ); _tileAnimations = List.generate( WORD_LENGTH, (index) => Tween(begin: 0.0, end: 1.0).animate( CurvedAnimation( parent: _revealController, curve: Interval( index * 0.1, (index * 0.1) + 0.5, curve: Curves.elasticOut, ), ), ), ); } void _initializeKeyboardStates() { for (int i = 65; i <= 90; i++) { keyboardStates[String.fromCharCode(i)] = LetterState.empty; } } @override void dispose() { _revealController.dispose(); _focusNode.dispose(); super.dispose(); } void _addLetter(String letter) { if (currentCol < WORD_LENGTH && currentRow < MAX_ATTEMPTS && !gameWon && !gameLost) { setState(() { grid[currentRow][currentCol] = letter; currentCol++; }); HapticFeedback.lightImpact(); } } void _deleteLetter() { if (currentCol > 0 && !gameWon && !gameLost) { setState(() { currentCol--; grid[currentRow][currentCol] = ''; }); HapticFeedback.lightImpact(); } } Future _submitWord() async { if (currentCol == WORD_LENGTH && !gameWon && !gameLost) { String guess = grid[currentRow].join(''); List newStates = _evaluateGuess(guess); // Set the states and start animating the current row setState(() { gridStates[currentRow] = newStates; _updateKeyboardStates(guess, newStates); animatingRow = currentRow; }); // Start the reveal animation _revealController.reset(); await _revealController.forward(); // Clear the animating row and move to next row setState(() { animatingRow = -1; }); if (guess == TARGET_WORD) { setState(() { gameWon = true; }); HapticFeedback.heavyImpact(); _showGameEndDialog(true); } else if (currentRow == MAX_ATTEMPTS - 1) { setState(() { gameLost = true; }); HapticFeedback.heavyImpact(); _showGameEndDialog(false); } else { setState(() { currentRow++; currentCol = 0; }); } } } List _evaluateGuess(String guess) { List states = List.filled(WORD_LENGTH, LetterState.absent); List targetLetters = TARGET_WORD.split(''); List used = List.filled(WORD_LENGTH, false); // First pass: check for correct positions for (int i = 0; i < WORD_LENGTH; i++) { if (guess[i] == targetLetters[i]) { states[i] = LetterState.correct; used[i] = true; } } // Second pass: check for present letters for (int i = 0; i < WORD_LENGTH; i++) { if (states[i] != LetterState.correct) { for (int j = 0; j < WORD_LENGTH; j++) { if (!used[j] && guess[i] == targetLetters[j]) { states[i] = LetterState.present; used[j] = true; break; } } } } return states; } void _updateKeyboardStates(String guess, List states) { for (int i = 0; i < guess.length; i++) { String letter = guess[i]; LetterState currentState = keyboardStates[letter] ?? LetterState.empty; LetterState newState = states[i]; if (currentState == LetterState.correct) continue; if (currentState == LetterState.present && newState == LetterState.absent) continue; keyboardStates[letter] = newState; } } void _showGameEndDialog(bool won) { showDialog( context: context, barrierDismissible: false, builder: (BuildContext context) { return AlertDialog( title: Text(won ? 'Congratulations!' : 'Game Over'), content: Text( won ? 'You solved today\'s puzzle!' : 'The word was: $TARGET_WORD', ), actions: [ TextButton( onPressed: () { Navigator.of(context).pop(); _resetGame(); }, child: Text( 'Play Again', style: TextStyle(color: Color(0xFF6A4C93)), ), ), ], ); }, ); } void _resetGame() { setState(() { grid = List.generate(MAX_ATTEMPTS, (_) => List.filled(WORD_LENGTH, '')); gridStates = List.generate( MAX_ATTEMPTS, (_) => List.filled(WORD_LENGTH, LetterState.empty), ); currentRow = 0; currentCol = 0; animatingRow = -1; gameWon = false; gameLost = false; _initializeKeyboardStates(); }); } @override Widget build(BuildContext context) { return PopScope( canPop: false, child: GestureDetector( onTap: () { // Prevent mobile keyboard from showing up FocusScope.of(context).unfocus(); // Only request focus on web/desktop platforms if (kIsWeb || (!kIsWeb && !Platform.isIOS && !Platform.isAndroid)) { _focusNode.requestFocus(); } }, child: RawKeyboardListener( focusNode: _focusNode, onKey: _handleRawKeyEvent, child: Scaffold( backgroundColor: Colors.white, appBar: AppBar( backgroundColor: Colors.white, elevation: 0, automaticallyImplyLeading: false, centerTitle: true, title: Row( mainAxisSize: MainAxisSize.min, children: [ Text( 'وصال', style: TextStyle( fontSize: 28, fontWeight: FontWeight.w200, fontFamily: 'Blaka', color: Color(0xFF6A4C93), ), ), SizedBox(width: 8), Text( 'Daily Challenge', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w500, color: Colors.grey[600], ), ), ], ), bottom: PreferredSize( preferredSize: Size.fromHeight(1), child: Container(height: 1, color: Colors.grey[200]), ), ), body: Column( children: [ Expanded( child: Center( child: Container( constraints: BoxConstraints(maxWidth: 350), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ // Game Grid Container( padding: EdgeInsets.all(16), child: Column( children: List.generate(MAX_ATTEMPTS, (row) { return Padding( padding: EdgeInsets.symmetric(vertical: 2), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: List.generate(WORD_LENGTH, (col) { bool shouldAnimate = row == animatingRow && gridStates[row][col] != LetterState.empty; return Container( margin: EdgeInsets.all(2), width: 62, height: 62, child: shouldAnimate ? AnimatedBuilder( animation: _tileAnimations[col], builder: (context, child) { double scale = 0.8 + (0.2 * _tileAnimations[col] .value); return Transform.scale( scale: scale, child: Container( width: 62, height: 62, decoration: BoxDecoration( border: Border.all( color: _getBorderColor( row, col, ), width: 2, ), color: _getAnimatedTileColor( row, col, _tileAnimations[col] .value, ), borderRadius: BorderRadius.circular( 4, ), ), child: Center( child: Text( grid[row][col], style: TextStyle( fontSize: 32, fontWeight: FontWeight.bold, color: _getAnimatedTextColor( row, col, _tileAnimations[col] .value, ), ), ), ), ), ); }, ) : Container( width: 62, height: 62, decoration: BoxDecoration( border: Border.all( color: _getBorderColor( row, col, ), width: 2, ), color: _getTileColor( row, col, ), borderRadius: BorderRadius.circular(4), ), child: Center( child: Text( grid[row][col], style: TextStyle( fontSize: 32, fontWeight: FontWeight.bold, color: _getTextColor( row, col, ), ), ), ), ), ); }), ), ); }), ), ), ], ), ), ), ), // Keyboard Container(padding: EdgeInsets.all(8), child: _buildKeyboard()), ], ), ), ), ), ); } Widget _buildKeyboard() { List> keyboardLayout = [ ['Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P'], ['A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L'], ['ENTER', 'Z', 'X', 'C', 'V', 'B', 'N', 'M', '⌫'], ]; return Column( children: keyboardLayout.map((row) { return Padding( padding: EdgeInsets.symmetric(vertical: 2), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: row.map((key) { bool isSpecial = key == 'ENTER' || key == '⌫'; double width = isSpecial ? 65 : 35; return Container( margin: EdgeInsets.symmetric(horizontal: 1.5), width: width, height: 58, child: Material( color: _getKeyColor(key), borderRadius: BorderRadius.circular(4), child: InkWell( borderRadius: BorderRadius.circular(4), onTap: () => _handleKeyTap(key), child: Container( alignment: Alignment.center, child: Text( key, style: TextStyle( fontSize: isSpecial ? 12 : 14, fontWeight: FontWeight.bold, color: _getKeyTextColor(key), ), ), ), ), ), ); }).toList(), ), ); }).toList(), ); } void _handleKeyTap(String key) { if (key == 'ENTER') { _submitWord(); } else if (key == '⌫') { _deleteLetter(); } else { _addLetter(key); } } void _handleRawKeyEvent(RawKeyEvent event) { if (event is RawKeyDownEvent) { if (event.logicalKey == LogicalKeyboardKey.enter) { _submitWord(); } else if (event.logicalKey == LogicalKeyboardKey.backspace) { _deleteLetter(); } else if (event.character != null && event.character!.isNotEmpty) { String key = event.character!.toUpperCase(); if (key.codeUnitAt(0) >= 65 && key.codeUnitAt(0) <= 90) { _addLetter(key); } } } } Color _getBorderColor(int row, int col) { if (row >= MAX_ATTEMPTS || col >= WORD_LENGTH) return Colors.grey[300]!; if (row < currentRow || (row == currentRow && gameWon)) { return Colors.transparent; } else if (row == currentRow && col < currentCol) { return Colors.grey[600]!; } else { return Colors.grey[300]!; } } Color _getTileColor(int row, int col) { if (row >= MAX_ATTEMPTS || col >= WORD_LENGTH) return Colors.white; if (row >= currentRow && !gameWon) return Colors.white; switch (gridStates[row][col]) { case LetterState.correct: return Color(0xFF6AAE7C); case LetterState.present: return Color(0xFFC9B037); case LetterState.absent: return Color(0xFF787C7E); default: return Colors.white; } } Color _getTextColor(int row, int col) { if (row >= MAX_ATTEMPTS || col >= WORD_LENGTH) return Colors.black; if (row >= currentRow && !gameWon) return Colors.black; return gridStates[row][col] == LetterState.empty ? Colors.black : Colors.white; } Color _getAnimatedTileColor(int row, int col, double animationValue) { if (row >= MAX_ATTEMPTS || col >= WORD_LENGTH) return Colors.white; if (row != animatingRow || gridStates[row][col] == LetterState.empty) { return _getTileColor(row, col); } // Show color based on animation progress Color targetColor; switch (gridStates[row][col]) { case LetterState.correct: targetColor = Color(0xFF6AAE7C); break; case LetterState.present: targetColor = Color(0xFFC9B037); break; case LetterState.absent: targetColor = Color(0xFF787C7E); break; default: targetColor = Colors.white; } return Color.lerp(Colors.white, targetColor, animationValue) ?? Colors.white; } Color _getAnimatedTextColor(int row, int col, double animationValue) { if (row >= MAX_ATTEMPTS || col >= WORD_LENGTH) return Colors.black; if (row != animatingRow || gridStates[row][col] == LetterState.empty) { return _getTextColor(row, col); } // Text color changes with animation return Color.lerp(Colors.black, Colors.white, animationValue) ?? Colors.black; } Color _getKeyColor(String key) { if (key == 'ENTER' || key == '⌫') { return Colors.grey[300]!; } LetterState state = keyboardStates[key] ?? LetterState.empty; switch (state) { case LetterState.correct: return Color(0xFF6AAE7C); case LetterState.present: return Color(0xFFC9B037); case LetterState.absent: return Color(0xFF787C7E); default: return Colors.grey[200]!; } } Color _getKeyTextColor(String key) { if (key == 'ENTER' || key == '⌫') { return Colors.black; } LetterState state = keyboardStates[key] ?? LetterState.empty; return state == LetterState.empty ? Colors.black : Colors.white; } } enum LetterState { empty, absent, present, correct }