import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter/foundation.dart'; import 'dart:io'; import '../../services/puzzle_service.dart'; import '../../models/puzzle_models.dart'; class WordlePage extends StatefulWidget { final String? targetWord; final DailyChallenge? dailyChallenge; const WordlePage({Key? key, this.targetWord, this.dailyChallenge}) : super(key: key); @override _WordlePageState createState() => _WordlePageState(); } class _WordlePageState extends State with TickerProviderStateMixin { late String TARGET_WORD; static const int MAX_ATTEMPTS = 6; static const int WORD_LENGTH = 5; DateTime? gameStartTime; 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(); TARGET_WORD = widget.targetWord ?? "HELLO"; gameStartTime = DateTime.now(); _initializeAnimations(); _initializeKeyboardStates(); _focusNode = FocusNode(); _loadGameState(); // Request focus for keyboard input on web/desktop if (kIsWeb || (!kIsWeb && !Platform.isIOS && !Platform.isAndroid)) { WidgetsBinding.instance.addPostFrameCallback((_) { _focusNode.requestFocus(); }); } } Future _loadGameState() async { final gameState = await PuzzleService.loadGameState(); if (gameState != null && gameState.targetWord == TARGET_WORD) { setState(() { // Restore game state for (int i = 0; i < gameState.guesses.length; i++) { if (i < MAX_ATTEMPTS && gameState.guesses[i].isNotEmpty) { for (int j = 0; j < WORD_LENGTH; j++) { if (j < gameState.guesses[i].length) { grid[i][j] = gameState.guesses[i][j]; } } // Evaluate the guess to set proper states List states = _evaluateGuess(gameState.guesses[i]); gridStates[i] = states; _updateKeyboardStates(gameState.guesses[i], states); } } currentRow = gameState.currentAttempt; currentCol = 0; gameWon = gameState.isWon; gameLost = gameState.isGameComplete && !gameState.isWon; if (gameState.solveTime != null) { gameStartTime = gameState.solveTime; } }); // If game is already completed, show the completed state if (gameState.isGameComplete) { WidgetsBinding.instance.addPostFrameCallback((_) { _showCompletedGameUI(); }); } } } Future _saveGameState() async { final guesses = []; for (int i = 0; i <= currentRow && i < MAX_ATTEMPTS; i++) { if (grid[i].any((cell) => cell.isNotEmpty)) { guesses.add(grid[i].join('')); } } final gameState = GameState( targetWord: TARGET_WORD, guesses: guesses, currentAttempt: currentRow, isGameComplete: gameWon || gameLost, isWon: gameWon, solveTime: gameWon ? DateTime.now() : null, gameDate: PuzzleService.getTodayDateString(), ); await PuzzleService.saveGameState(gameState); } 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++; }); _saveGameState(); HapticFeedback.lightImpact(); } } void _deleteLetter() { if (currentCol > 0 && !gameWon && !gameLost) { setState(() { currentCol--; grid[currentRow][currentCol] = ''; }); _saveGameState(); 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; }); await _saveGameState(); await _submitAttempt(currentRow + 1, true); HapticFeedback.heavyImpact(); _showCompletedGameUI(); } else if (currentRow == MAX_ATTEMPTS - 1) { setState(() { gameLost = true; }); await _saveGameState(); await _submitAttempt(MAX_ATTEMPTS, false); HapticFeedback.heavyImpact(); _showCompletedGameUI(); } else { setState(() { currentRow++; currentCol = 0; }); await _saveGameState(); } } } 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; } } Future _submitAttempt(int attempts, bool solved) async { try { await PuzzleService.submitAttempt(attempts: attempts, solved: solved); } catch (e) { print('Failed to submit attempt: $e'); } } void _showCompletedGameUI() { // Remove the old dialog and show a new completed game state // This will be handled in the UI by showing a different keyboard/message area } void _goToLeaderboard() { Navigator.pop( context, ); // Go back to puzzles page which will show leaderboard option } 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, leading: IconButton( icon: Icon(Icons.arrow_back, color: Color(0xFF6A4C93)), onPressed: () => Navigator.pop(context), ), 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 or completion message Container( padding: EdgeInsets.all(8), child: (gameWon || gameLost) ? _buildCompletionMessage() : _buildKeyboard(), ), ], ), ), ), ), ); } Widget _buildCompletionMessage() { String message; String buttonText = 'Go to Leaderboard'; Color messageColor; IconData messageIcon; if (gameWon) { if (widget.dailyChallenge?.solved == true && widget.dailyChallenge?.solveTime != null) { // Already completed today message = 'You solved this at ${PuzzleService.formatSolveTime(widget.dailyChallenge!.solveTime!)} in ${widget.dailyChallenge!.attempts} attempts!'; } else { // Just completed now message = 'You got it!\nSolved in ${currentRow + 1} attempts!'; } messageColor = Colors.green; messageIcon = Icons.celebration; } else { if (widget.dailyChallenge?.attempted == true) { // Already attempted today message = 'You attempted this challenge but didn\'t solve it in ${widget.dailyChallenge!.attempts} attempts.\nThe word was: $TARGET_WORD'; } else { // Just failed now message = 'Better luck next time!\nThe word was: $TARGET_WORD'; } messageColor = Colors.orange; messageIcon = Icons.sentiment_neutral; } return Column( children: [ Container( padding: EdgeInsets.all(24), margin: EdgeInsets.symmetric(horizontal: 16), decoration: BoxDecoration( color: messageColor.withOpacity(0.1), borderRadius: BorderRadius.circular(20), border: Border.all(color: messageColor.withOpacity(0.3), width: 2), ), child: Column( children: [ Icon(messageIcon, color: messageColor, size: 48), SizedBox(height: 16), Text( message, style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: Colors.black87, ), textAlign: TextAlign.center, ), ], ), ), SizedBox(height: 24), ElevatedButton( onPressed: _goToLeaderboard, style: ElevatedButton.styleFrom( backgroundColor: Color(0xFF6A4C93), foregroundColor: Colors.white, padding: EdgeInsets.symmetric(horizontal: 32, vertical: 16), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(25), ), elevation: 4, ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.leaderboard, size: 20), SizedBox(width: 8), Text( buttonText, style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600), ), ], ), ), ], ); } 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 }