flutter_wordle/lib/game_screen.dart
2025-06-19 13:38:17 +02:00

272 lines
7.3 KiB
Dart

import 'package:diacritic/diacritic.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:wordle/db/board.dart';
import 'package:wordle/db/classic_game.dart';
import 'package:wordle/db/classic_game_provider.dart';
import 'package:wordle/word_loader.dart';
import 'guess_row.dart';
import 'letter_keyboard.dart';
class GameData {
final String correctWord;
final int maxGuesses;
final List<String> guesses;
final bool hasWon;
const GameData({
required this.correctWord,
required this.maxGuesses,
required this.guesses,
required this.hasWon,
});
}
class GameScreen extends StatefulWidget {
final int maxGuesses;
final List<String>? guesses;
final String correctWord;
final bool readOnly;
final void Function(GameData data)? onGameEnd;
const GameScreen({
super.key,
required this.maxGuesses,
required this.correctWord,
this.guesses,
this.readOnly = false,
this.onGameEnd,
});
@override
_GameScreenState createState() => _GameScreenState();
}
class _GameScreenState extends State<GameScreen> {
late List<String> guesses;
String currentGuess = "";
int currentRowIndex = 0;
bool gameEnded = false;
String? _errorMessage;
late ScrollController _scrollController;
@override
void initState() {
super.initState();
_scrollController = ScrollController();
if (widget.readOnly) {
guesses = widget.guesses ?? [];
currentRowIndex = guesses.length;
gameEnded = true;
} else {
guesses = List.generate(widget.maxGuesses, (_) => "");
}
}
void _resetGame() {
setState(() {
guesses = List.generate(widget.maxGuesses, (_) => "");
currentRowIndex = 0;
currentGuess = "";
gameEnded = false;
_errorMessage = null;
});
}
void onLetterPressed(String letter) {
if (currentGuess.length < widget.correctWord.length && !gameEnded) {
setState(() {
currentGuess += letter;
guesses[currentRowIndex] = currentGuess;
_errorMessage = null;
WidgetsBinding.instance.addPostFrameCallback((_) {
_scrollToCurrentGuess();
});
});
}
}
void onBackspacePressed() {
if (currentGuess.isNotEmpty && !gameEnded) {
setState(() {
currentGuess = currentGuess.substring(0, currentGuess.length - 1);
guesses[currentRowIndex] = currentGuess;
_errorMessage = null;
});
}
}
void onSubmitGuess() async {
if (currentGuess.length == widget.correctWord.length && !gameEnded) {
bool isValidWord = await WordLoader.isValidWord(currentGuess);
if (!isValidWord) {
setState(() {
_errorMessage = 'Invalid word. Please enter a valid word.';
});
return;
}
setState(() {
guesses[currentRowIndex] = currentGuess;
if (currentGuess.toUpperCase() ==
removeDiacritics(widget.correctWord.toUpperCase())) {
gameEnded = true;
_handleGameEnd(GameData(
correctWord: widget.correctWord,
maxGuesses: widget.maxGuesses,
guesses: guesses,
hasWon: true));
} else if (currentRowIndex == widget.maxGuesses - 1) {
gameEnded = true;
_handleGameEnd(GameData(
correctWord: widget.correctWord,
maxGuesses: widget.maxGuesses,
guesses: guesses,
hasWon: false));
} else {
currentRowIndex++;
currentGuess = "";
_errorMessage = null;
WidgetsBinding.instance.addPostFrameCallback((_) {
_scrollToCurrentGuess();
});
}
});
}
}
Future<void> saveGame(GameData data) async {
final game = DbClassicGame(
date: DateTime.now(),
board: DbBoard(
correctWord: data.correctWord,
maxGuesses: data.maxGuesses,
guesses: data.guesses,
hasWon: data.hasWon),
);
final gameProvider =
Provider.of<DbClassicGameProvider>(context, listen: false);
await gameProvider.insertClassicGame(game);
}
void _handleGameEnd(GameData data) {
if (widget.onGameEnd != null) {
widget.onGameEnd!(data);
} else {
saveGame(data);
showDefaultEndGameDialog(
data.hasWon ? "You Win!" : "Game Over",
data.hasWon
? "Congratulations, you guessed the correct word!"
: "You've used all your guesses. The correct word was \"${widget.correctWord}\".",
"Return to the main screen",
);
}
_resetGame();
}
Future<void> _scrollToCurrentGuess() async {
double rowHeight = 70.0;
double offset = (currentRowIndex * rowHeight) -
(MediaQuery.of(context).size.height / 2) +
(rowHeight / 2);
offset = offset < 0 ? 0 : offset;
_scrollController.animateTo(
offset,
duration: const Duration(milliseconds: 500),
curve: Curves.easeInOut,
);
}
void showDefaultEndGameDialog(String title, String message, String btnText) {
showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return AlertDialog(
title: Text(title),
content: Text(message),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
Navigator.of(context).pop();
},
child: Text(btnText),
),
],
);
},
);
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: ListView.builder(
controller: _scrollController,
itemCount: guesses.length,
itemBuilder: (context, index) {
return GuessRow(
guess: guesses[index],
correctWord: widget.correctWord,
evaluateColors: index < currentRowIndex || widget.readOnly,
);
},
),
),
if (_errorMessage != null) ...[
Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Text(
_errorMessage!,
style: const TextStyle(color: Colors.red, fontSize: 16),
),
),
],
if (!widget.readOnly) ...[
ElevatedButton(
onPressed:
currentGuess.length == widget.correctWord.length && !gameEnded
? onSubmitGuess
: null,
style: ElevatedButton.styleFrom(
foregroundColor: Colors.white,
backgroundColor:
currentGuess.length == widget.correctWord.length && !gameEnded
? const Color(0xFF4F545C)
: const Color(0xFF2C2F33),
),
child: const Text("Submit Guess"),
),
Padding(
padding:
const EdgeInsets.symmetric(horizontal: 16.0, vertical: 16.0),
child: LetterKeyboard(
onLetterPressed: onLetterPressed,
onBackspacePressed: onBackspacePressed,
),
),
]
],
);
}
}