98 lines
3.1 KiB
Dart
98 lines
3.1 KiB
Dart
import '../../core/config/app_constants.dart';
|
|
import '../entities/pokemon.dart';
|
|
import 'game_state.dart';
|
|
|
|
/// Règles du jeu, pures (aucune IO, aucun widget). Entièrement testable.
|
|
class GameEngine {
|
|
const GameEngine();
|
|
|
|
/// Nouvelle partie : réinitialise tout sauf le bestScore.
|
|
GameState newGame(GameState s) {
|
|
return s.copyWith(
|
|
lives: AppConstants.startingLives,
|
|
skips: AppConstants.startingSkips,
|
|
hints: AppConstants.startingHints,
|
|
sessionCorrectCount: 0,
|
|
currentScore: 0,
|
|
status: GameStatus.loading,
|
|
isHintUsed: false,
|
|
);
|
|
}
|
|
|
|
/// Démarre une manche avec un nouveau Pokémon.
|
|
GameState startRound(GameState s, Pokemon p, {required bool isShiny}) {
|
|
return s.copyWith(
|
|
currentPokemon: p,
|
|
isShiny: isShiny,
|
|
isHintUsed: false,
|
|
status: GameStatus.playing,
|
|
);
|
|
}
|
|
|
|
/// Soumet une réponse. Renvoie le nouvel état + la nature du résultat.
|
|
GuessOutcome submitGuess(GameState s, String guess) {
|
|
final pokemon = s.currentPokemon;
|
|
if (pokemon == null || s.status != GameStatus.playing) {
|
|
return GuessOutcome(s, GuessResult.invalid);
|
|
}
|
|
|
|
final normalizedGuess = _normalize(guess.trim().toLowerCase());
|
|
final normalizedActual = _normalize(pokemon.name.toLowerCase());
|
|
|
|
if (normalizedGuess == normalizedActual) {
|
|
final newSession = s.sessionCorrectCount + 1;
|
|
final gained = s.isShiny ? AppConstants.pointsShiny : AppConstants.pointsNormal;
|
|
final newScore = s.currentScore + gained;
|
|
final hintBonus =
|
|
newSession % AppConstants.hintBonusEvery == 0 ? 1 : 0;
|
|
final skipBonus =
|
|
newSession % AppConstants.skipBonusEvery == 0 ? 1 : 0;
|
|
|
|
final newState = s.copyWith(
|
|
currentPokemon: pokemon.copyWith(isCaught: true, isSeen: true),
|
|
currentScore: newScore,
|
|
bestScore: newScore > s.bestScore ? newScore : s.bestScore,
|
|
sessionCorrectCount: newSession,
|
|
hints: s.hints + hintBonus,
|
|
skips: s.skips + skipBonus,
|
|
status: GameStatus.roundWon,
|
|
);
|
|
return GuessOutcome(newState, GuessResult.correct);
|
|
}
|
|
|
|
final remainingLives = s.lives - 1;
|
|
if (remainingLives <= 0) {
|
|
return GuessOutcome(
|
|
s.copyWith(lives: 0, status: GameStatus.gameOver),
|
|
GuessResult.gameOver,
|
|
);
|
|
}
|
|
return GuessOutcome(
|
|
s.copyWith(lives: remainingLives),
|
|
GuessResult.wrong,
|
|
);
|
|
}
|
|
|
|
/// Consomme un indice si disponible et non déjà utilisé.
|
|
GameState useHint(GameState s) {
|
|
if (s.isHintUsed || s.hints <= 0) return s;
|
|
return s.copyWith(isHintUsed: true, hints: s.hints - 1);
|
|
}
|
|
|
|
/// Consomme un skip si disponible.
|
|
GameState useSkip(GameState s) {
|
|
if (s.skips <= 0) return s;
|
|
return s.copyWith(skips: s.skips - 1);
|
|
}
|
|
|
|
static String _normalize(String input) {
|
|
const withDia = 'ÀÁÂÃÄÅàáâãäåÒÓÔÕÖØòóôõöøÈÉÊËèéêëÇçÌÍÎÏìíîïÙÚÛÜùúûüÿÑñ';
|
|
const withoutDia = 'AAAAAAaaaaaaOOOOOOooooooEEEEeeeeCcIIIIiiiiUUUUuuuuyNn';
|
|
var output = input;
|
|
for (var i = 0; i < withDia.length; i++) {
|
|
output = output.replaceAll(withDia[i], withoutDia[i]);
|
|
}
|
|
return output;
|
|
}
|
|
}
|