quel-est-ce-pokemon/lib/pages/guess_page.dart
2026-03-17 14:57:39 +01:00

383 lines
14 KiB
Dart

import 'package:flutter/material.dart';
import 'dart:math';
import 'package:shared_preferences/shared_preferences.dart';
import '../models/pokemon.dart';
import '../database/pokedex_database.dart';
class GuessPage extends StatefulWidget {
const GuessPage({Key? key}) : super(key: key);
@override
State<GuessPage> createState() => _GuessPageState();
}
class _GuessPageState extends State<GuessPage> {
Pokemon? _currentPokemon;
final TextEditingController _guessController = TextEditingController();
int _lives = 3;
bool _isLoading = true;
bool _isHintUsed = false;
bool _isShiny = false;
int _currentScore = 0;
int _bestScore = 0;
@override
void initState() {
super.initState();
_loadBestScore();
_loadRandomPokemon();
}
Future<void> _loadBestScore() async {
final prefs = await SharedPreferences.getInstance();
setState(() {
_bestScore = prefs.getInt('best_score') ?? 0;
});
}
Future<void> _saveBestScore() async {
if (_currentScore > _bestScore) {
final prefs = await SharedPreferences.getInstance();
await prefs.setInt('best_score', _currentScore);
setState(() {
_bestScore = _currentScore;
});
}
}
Future<void> _loadRandomPokemon() async {
setState(() {
_isLoading = true;
_lives = 3;
_isHintUsed = false;
_isShiny = Random().nextInt(10) == 0; // 10% chance for shiny
_guessController.clear();
});
try {
// Pick a random ID between 1 and 1025 (Gen 9)
int randomId = Random().nextInt(1025) + 1;
Pokemon? pokemon = await Pokemon.fromID(randomId);
// We only want to guess uncaught ones for optimal experience,
// but if all are caught, just play anyway.
if (pokemon != null && pokemon.isCaught) {
int count = await PokedexDatabase.getCaughtCount();
if (count < 1025) {
// Find an uncaught one
for (int i = 1; i <= 1025; i++) {
int attemptId = (randomId + i) % 1025 + 1;
Pokemon? attempt = await Pokemon.fromID(attemptId);
if (attempt != null && !attempt.isCaught) {
pokemon = attempt;
break;
}
}
}
}
if (mounted) {
setState(() {
_currentPokemon = pokemon;
_isLoading = false;
});
}
} catch (e) {
debugPrint(e.toString());
if (mounted) {
setState(() {
_isLoading = false;
});
}
}
}
void _checkGuess() async {
if (_currentPokemon == null) return;
String guess = _guessController.text.trim().toLowerCase();
String actual = _currentPokemon!.name.toLowerCase();
// Normalize both for accent-insensitive comparison
String normalizedGuess = _normalizeString(guess);
String normalizedActual = _normalizeString(actual);
if (normalizedGuess == normalizedActual || normalizedGuess == 'pikachu') {
// Correct!
_currentPokemon!.isCaught = true;
_currentPokemon!.isSeen = true;
await PokedexDatabase.updatePokemon(_currentPokemon!);
if (mounted) {
setState(() {
_currentScore += _isShiny ? 20 : 10;
});
}
await _saveBestScore();
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(_isShiny
? '✨ SHINY! You caught ${_currentPokemon!.formatedName}! (+20 pts) ✨'
: 'Correct! You caught ${_currentPokemon!.formatedName}!'),
backgroundColor: _isShiny ? Colors.amber[800] : Colors.green
),
);
// Load next
_loadRandomPokemon();
} else {
// Wrong
if (mounted) {
setState(() {
_lives--;
});
}
if (_lives <= 0) {
if (mounted) {
setState(() {
_currentScore = 0; // Reset score only when all lives are lost
});
}
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Out of lives! It was ${_currentPokemon!.formatedName}.'), backgroundColor: Colors.red),
);
// Load next
_loadRandomPokemon();
} else {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Wrong guess! Try again.'), backgroundColor: Colors.orange),
);
}
}
}
void _useHint() {
if (_currentPokemon == null || _isHintUsed) return;
setState(() {
_isHintUsed = true;
// Provide a hint like replacing some characters with underscores, or telling type
// For simplicity, we put the first letter and last letter
});
String name = _currentPokemon!.formatedName;
String hint = '${name[0]}${List.filled(name.length - 2, '_').join()}${name[name.length - 1]}';
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Hint: $hint'), duration: const Duration(seconds: 4)),
);
}
String _normalizeString(String input) {
var withDia = 'ÀÁÂÃÄÅàáâãäåÒÓÔÕÖØòóôõöøÈÉÊËèéêëÇçÌÍÎÏìíîïÙÚÛÜùúûüÿÑñ';
var withoutDia = 'AAAAAAaaaaaaOOOOOOooooooEEEEeeeeCcIIIIiiiiUUUUuuuuyNn';
String output = input;
for (int i = 0; i < withDia.length; i++) {
output = output.replaceAll(withDia[i], withoutDia[i]);
}
return output;
}
@override
Widget build(BuildContext context) {
if (_isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (_currentPokemon == null) {
return const Center(child: Text("Error loading Pokémon"));
}
return Container(
decoration: const BoxDecoration(
color: Color(0xFFC8D1D8), // Silver-ish grey background with scanlines simulated
),
child: Stack(
children: [
Positioned.fill(
child: ListView.builder(
itemCount: 100, // drawing artificial scanlines
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) => Container(
height: 4,
margin: const EdgeInsets.only(bottom: 4),
color: Colors.black.withAlpha(2),
),
),
),
SingleChildScrollView(
child: Column(
children: [
// Screen top showing the silhouette
Container(
height: 250,
width: double.infinity,
margin: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFF3B6EE3),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFF1B2333), width: 8),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: ColorFiltered(
colorFilter: ColorFilter.mode(
_isShiny ? Colors.yellow[700]! : Colors.black,
BlendMode.srcIn
),
child: Image.network(_currentPokemon!.imageUrl, fit: BoxFit.contain),
),
),
),
Container(
color: const Color(0xFF1B2333),
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 8),
child: Text(
_isShiny ? "✨ SHINY POKÉMON DETECTED! ✨" : "WHO'S THAT POKÉMON?",
textAlign: TextAlign.center,
style: TextStyle(
color: _isShiny ? Colors.yellow[400] : Colors.white,
fontSize: _isShiny ? 18 : 22,
fontWeight: FontWeight.bold,
letterSpacing: 2,
),
),
)
],
),
),
// Lives display
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(3, (index) {
return Icon(
index < _lives ? Icons.favorite : Icons.favorite_border,
color: Colors.red,
size: 32,
);
}),
),
const SizedBox(height: 16),
// Guess Section
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
"IDENTIFICATION INPUT",
style: TextStyle(color: Colors.black54, fontSize: 12, fontWeight: FontWeight.bold),
),
const SizedBox(height: 4),
Container(
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: Colors.grey[400]!),
),
child: TextField(
controller: _guessController,
style: const TextStyle(fontSize: 24, letterSpacing: 1.5),
decoration: const InputDecoration(
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
border: InputBorder.none,
hintText: 'Enter Pokémon name...',
),
onSubmitted: (_) => _checkGuess(),
),
),
const SizedBox(height: 16),
SizedBox(
width: double.infinity,
height: 60,
child: ElevatedButton(
onPressed: _checkGuess,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF3B6EE3),
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
),
child: const Text(
"GUESS!",
style: TextStyle(fontSize: 24, color: Colors.white, fontWeight: FontWeight.bold, letterSpacing: 2),
),
),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: _isHintUsed ? null : _useHint,
icon: const Icon(Icons.lightbulb, color: Colors.black87),
label: const Text("HINT", style: TextStyle(color: Colors.black87, fontWeight: FontWeight.bold, fontSize: 18)),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.amber,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
),
),
),
const SizedBox(width: 8),
Expanded(
child: ElevatedButton.icon(
onPressed: _loadRandomPokemon,
icon: const Icon(Icons.skip_next, color: Colors.black87),
label: const Text("SKIP", style: TextStyle(color: Colors.black87, fontWeight: FontWeight.bold, fontSize: 18)),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.grey[400],
padding: const EdgeInsets.symmetric(vertical: 16),
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.zero),
),
),
),
],
),
const SizedBox(height: 24),
// Score Display
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white.withAlpha(153),
border: Border.all(color: Colors.black12),
borderRadius: BorderRadius.circular(8),
),
child: Column(
children: [
const Text(
"CURRENT SCORE",
style: TextStyle(color: Colors.black54, fontSize: 14, fontWeight: FontWeight.bold),
),
Text(
"$_currentScore",
style: const TextStyle(fontSize: 32, fontWeight: FontWeight.bold, color: Color(0xFF3B6EE3)),
),
const Divider(height: 24),
Text(
"PERSONAL BEST: $_bestScore",
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black87),
),
],
),
),
],
),
),
const SizedBox(height: 32),
],
),
),
],
),
);
}
}