feat: simple wordle UI without animation
This commit is contained in:
parent
121395481e
commit
8d730c7c15
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'pages/feed_page.dart';
|
||||
import 'pages/invitations_page.dart';
|
||||
import 'pages/profile_page.dart';
|
||||
import 'pages/wordle_page.dart';
|
||||
import '../services/invitations_service.dart';
|
||||
|
||||
class HomeScreen extends StatefulWidget {
|
||||
@ -13,7 +14,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
int _currentIndex = 0;
|
||||
int _availableInvitationsCount = 0;
|
||||
|
||||
final List<Widget> _pages = [FeedPage(), InvitationsPage(), ProfilePage()];
|
||||
final List<Widget> _pages = [FeedPage(), InvitationsPage(), WordlePage(), ProfilePage()];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -113,6 +114,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
icon: _buildInvitationsBadge(),
|
||||
label: 'Invitations',
|
||||
),
|
||||
BottomNavigationBarItem(icon: Icon(Icons.games), label: 'Puzzles'),
|
||||
BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Profile'),
|
||||
],
|
||||
),
|
||||
|
||||
537
frontend/lib/screens/pages/wordle_page.dart
Normal file
537
frontend/lib/screens/pages/wordle_page.dart
Normal file
@ -0,0 +1,537 @@
|
||||
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<WordlePage> with TickerProviderStateMixin {
|
||||
static const String TARGET_WORD = "HELLO";
|
||||
static const int MAX_ATTEMPTS = 6;
|
||||
static const int WORD_LENGTH = 5;
|
||||
|
||||
List<List<String>> grid = List.generate(MAX_ATTEMPTS, (_) => List.filled(WORD_LENGTH, ''));
|
||||
List<List<LetterState>> gridStates = List.generate(MAX_ATTEMPTS, (_) => List.filled(WORD_LENGTH, LetterState.empty));
|
||||
Map<String, LetterState> keyboardStates = {};
|
||||
|
||||
int currentRow = 0;
|
||||
int currentCol = 0;
|
||||
bool gameWon = false;
|
||||
bool gameLost = false;
|
||||
|
||||
late List<AnimationController> _flipControllers;
|
||||
late List<Animation<double>> _flipAnimations;
|
||||
late FocusNode _focusNode;
|
||||
|
||||
@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() {
|
||||
_flipControllers = List.generate(
|
||||
WORD_LENGTH,
|
||||
(index) => AnimationController(
|
||||
duration: Duration(milliseconds: 600),
|
||||
vsync: this,
|
||||
),
|
||||
);
|
||||
_flipAnimations = _flipControllers.map((controller) =>
|
||||
Tween<double>(begin: 0.0, end: 1.0).animate(
|
||||
CurvedAnimation(parent: controller, curve: Curves.easeInOut)
|
||||
)
|
||||
).toList();
|
||||
}
|
||||
|
||||
void _initializeKeyboardStates() {
|
||||
for (int i = 65; i <= 90; i++) {
|
||||
keyboardStates[String.fromCharCode(i)] = LetterState.empty;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
for (var controller in _flipControllers) {
|
||||
controller.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<void> _submitWord() async {
|
||||
if (currentCol == WORD_LENGTH && !gameWon && !gameLost) {
|
||||
String guess = grid[currentRow].join('');
|
||||
|
||||
// Animate the flip
|
||||
for (int i = 0; i < WORD_LENGTH; i++) {
|
||||
await Future.delayed(Duration(milliseconds: 100));
|
||||
_flipControllers[i].forward();
|
||||
}
|
||||
|
||||
await Future.delayed(Duration(milliseconds: 300));
|
||||
|
||||
List<LetterState> newStates = _evaluateGuess(guess);
|
||||
|
||||
setState(() {
|
||||
gridStates[currentRow] = newStates;
|
||||
_updateKeyboardStates(guess, newStates);
|
||||
});
|
||||
|
||||
// Reset animations
|
||||
for (var controller in _flipControllers) {
|
||||
controller.reset();
|
||||
}
|
||||
|
||||
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<LetterState> _evaluateGuess(String guess) {
|
||||
List<LetterState> states = List.filled(WORD_LENGTH, LetterState.absent);
|
||||
List<String> targetLetters = TARGET_WORD.split('');
|
||||
List<bool> 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<LetterState> 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;
|
||||
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: KeyboardListener(
|
||||
focusNode: _focusNode,
|
||||
onKeyEvent: _handleKeyEvent,
|
||||
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) {
|
||||
return Container(
|
||||
margin: EdgeInsets.all(2),
|
||||
width: 62,
|
||||
height: 62,
|
||||
child: AnimatedBuilder(
|
||||
animation: _flipAnimations[col],
|
||||
builder: (context, child) {
|
||||
// Check if this tile should be animating
|
||||
bool isCurrentRowAndAnimating = row == currentRow && _flipAnimations[col].value > 0;
|
||||
|
||||
// If not animating, show the final state
|
||||
if (!isCurrentRowAndAnimating) {
|
||||
return Container(
|
||||
width: 62,
|
||||
height: 62,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: _getBorderColor(row, col),
|
||||
width: 2,
|
||||
),
|
||||
color: _getTileColor(row, col),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
grid[row][col],
|
||||
style: TextStyle(
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: _getTextColor(row, col),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Animation is running - create flip effect
|
||||
double progress = _flipAnimations[col].value;
|
||||
double rotationX = progress * 3.14159;
|
||||
|
||||
// Determine colors to show during animation
|
||||
bool showFinalColors = progress > 0.5;
|
||||
Color bgColor = showFinalColors ? _getTileColor(row, col) : Colors.white;
|
||||
Color textColor = showFinalColors ? _getTextColor(row, col) : Colors.black;
|
||||
|
||||
return Container(
|
||||
width: 62,
|
||||
height: 62,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: _getBorderColor(row, col),
|
||||
width: 2,
|
||||
),
|
||||
color: Colors.white,
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
// Animated background that flips
|
||||
Transform(
|
||||
alignment: Alignment.center,
|
||||
transform: Matrix4.identity()
|
||||
..setEntry(3, 2, 0.001)
|
||||
..rotateX(rotationX),
|
||||
child: Container(
|
||||
width: 62,
|
||||
height: 62,
|
||||
color: bgColor,
|
||||
),
|
||||
),
|
||||
// Text that always stays upright and visible
|
||||
Center(
|
||||
child: Text(
|
||||
grid[row][col],
|
||||
style: TextStyle(
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: textColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Keyboard
|
||||
Container(
|
||||
padding: EdgeInsets.all(8),
|
||||
child: _buildKeyboard(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildKeyboard() {
|
||||
List<List<String>> 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);
|
||||
}
|
||||
}
|
||||
|
||||
bool _handleKeyEvent(KeyEvent event) {
|
||||
if (event is KeyDownEvent) {
|
||||
if (event.logicalKey == LogicalKeyboardKey.enter) {
|
||||
_submitWord();
|
||||
return true;
|
||||
} else if (event.logicalKey == LogicalKeyboardKey.backspace) {
|
||||
_deleteLetter();
|
||||
return true;
|
||||
} else if (event.logicalKey.keyLabel.length == 1) {
|
||||
String key = event.logicalKey.keyLabel.toUpperCase();
|
||||
if (key.codeUnitAt(0) >= 65 && key.codeUnitAt(0) <= 90) {
|
||||
_addLetter(key);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
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 _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,
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user