tetraq/lib/logic/game_controller.dart
2026-03-12 21:00:08 +01:00

592 lines
No EOL
21 KiB
Dart

// ===========================================================================
// FILE: lib/logic/game_controller.dart
// ===========================================================================
import 'dart:async';
import 'dart:math';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../models/game_board.dart';
export '../models/game_board.dart';
import 'ai_engine.dart';
import '../services/audio_service.dart';
import '../services/storage_service.dart';
import '../services/multiplayer_service.dart';
import '../core/app_colors.dart';
class GameController extends ChangeNotifier {
late GameBoard board;
bool isVsCPU = false;
bool isCPUThinking = false;
bool isOnline = false;
String? roomCode;
bool isHost = false;
StreamSubscription<DocumentSnapshot>? _onlineSubscription;
bool opponentLeft = false;
bool _hasSavedResult = false;
Timer? _blitzTimer;
int timeLeft = 15;
final int maxTime = 15;
bool isTimeMode = true;
String effectText = '';
Color effectColor = Colors.transparent;
Timer? _effectTimer;
String? myReaction;
String? opponentReaction;
Timer? _myReactionTimer;
Timer? _oppReactionTimer;
Timestamp? _lastOpponentReactionTime;
bool rematchRequested = false;
bool opponentWantsRematch = false;
int lastMatchXP = 0;
// --- VARIABILI PER IL LEVEL UP ---
bool hasLeveledUp = false;
int newlyReachedLevel = 1;
List<String> unlockedFeatures = [];
// ---------------------------------
bool isSetupPhase = true;
bool myJokerPlaced = false;
bool oppJokerPlaced = false;
Player jokerTurn = Player.red;
Player get myPlayer => isOnline ? (isHost ? Player.red : Player.blue) : Player.red;
bool get isGameOver => board.isGameOver;
int cpuLevel = 1;
int currentMatchLevel = 1;
int? currentSeed;
AppThemeType _activeTheme = AppThemeType.cyberpunk;
String onlineHostName = "ROSSO";
String onlineGuestName = "BLU";
ArenaShape onlineShape = ArenaShape.classic;
GameController({int radius = 3}) {
cpuLevel = StorageService.instance.cpuLevel;
startNewGame(radius);
}
void startNewGame(int radius, {bool vsCPU = false, bool isOnline = false, String? roomCode, bool isHost = false, ArenaShape shape = ArenaShape.classic, bool timeMode = true}) {
_onlineSubscription?.cancel();
_onlineSubscription = null;
_blitzTimer?.cancel();
_effectTimer?.cancel();
effectText = '';
_hasSavedResult = false;
lastMatchXP = 0;
// Reset Level Up vars
hasLeveledUp = false;
unlockedFeatures.clear();
myReaction = null;
opponentReaction = null;
_lastOpponentReactionTime = null;
rematchRequested = false;
opponentWantsRematch = false;
isSetupPhase = true;
myJokerPlaced = false;
oppJokerPlaced = false;
jokerTurn = Player.red;
this.isVsCPU = vsCPU;
this.isOnline = isOnline;
this.roomCode = roomCode;
this.isHost = isHost;
this.isTimeMode = timeMode;
onlineShape = shape;
int levelToUse = isOnline ? (currentMatchLevel == 1 ? 2 : currentMatchLevel) : cpuLevel;
board = GameBoard(radius: radius, level: levelToUse, seed: currentSeed, shape: onlineShape);
board.currentPlayer = Player.red;
isCPUThinking = false;
opponentLeft = false;
if (this.isOnline && this.roomCode != null) {
_listenToOnlineGame(this.roomCode!);
}
notifyListeners();
}
void placeJoker(int bx, int by) {
if (!isSetupPhase) return;
Box? target;
try { target = board.boxes.firstWhere((b) => b.x == bx && b.y == by); } catch(e) {}
if (target == null || target.type == BoxType.invisible || target.hiddenJokerOwner != null) return;
AudioService.instance.playLineSfx(_activeTheme);
if (isOnline) {
if (myJokerPlaced) return;
target.hiddenJokerOwner = myPlayer;
myJokerPlaced = true;
String prefix = isHost ? 'p1' : 'p2';
FirebaseFirestore.instance.collection('games').doc(roomCode).update({
'${prefix}_joker': {'x': bx, 'y': by}
});
} else {
target.hiddenJokerOwner = jokerTurn;
if (jokerTurn == Player.red) {
jokerTurn = Player.blue;
if (isVsCPU) {
_placeCpuJoker();
}
} else {
jokerTurn = Player.red;
}
}
notifyListeners();
_checkSetupComplete();
}
void _placeCpuJoker() {
var validBoxes = board.boxes.where((b) => b.type != BoxType.invisible && b.hiddenJokerOwner == null).toList();
if (validBoxes.isNotEmpty) {
var b = validBoxes[Random().nextInt(validBoxes.length)];
b.hiddenJokerOwner = Player.blue;
}
jokerTurn = Player.red;
_checkSetupComplete();
}
void _checkSetupComplete() {
if (isOnline) {
if (myJokerPlaced && oppJokerPlaced) {
isSetupPhase = false;
_startTimer();
}
} else {
if (jokerTurn == Player.red) {
isSetupPhase = false;
_startTimer();
}
}
notifyListeners();
}
void sendReaction(String reaction) {
if (!isOnline || roomCode == null) return;
MultiplayerService().sendReaction(roomCode!, isHost, reaction);
_showReaction(true, reaction);
}
void requestRematch() {
if (!isOnline || roomCode == null) return;
rematchRequested = true;
notifyListeners();
MultiplayerService().requestRematch(roomCode!, isHost);
}
void _showReaction(bool isMe, String reaction) {
if (isMe) {
myReaction = reaction;
_myReactionTimer?.cancel();
_myReactionTimer = Timer(const Duration(seconds: 4), () { myReaction = null; notifyListeners(); });
} else {
opponentReaction = reaction;
_oppReactionTimer?.cancel();
_oppReactionTimer = Timer(const Duration(seconds: 4), () { opponentReaction = null; notifyListeners(); });
}
notifyListeners();
}
void triggerSpecialEffect(String text, Color color) {
effectText = text;
effectColor = color;
notifyListeners();
_effectTimer?.cancel();
_effectTimer = Timer(const Duration(milliseconds: 1200), () { effectText = ''; notifyListeners(); });
}
void _playEffects(List<Box> newClosed, {List<Box> newGhosts = const [], required bool isOpponent}) {
if (newGhosts.isNotEmpty) {
AudioService.instance.playBombSfx();
triggerSpecialEffect("TRAPPOLA!", Colors.grey.shade400);
HapticFeedback.heavyImpact();
return;
}
bool isIceCracked = board.lastMove?.isIceCracked ?? false;
if (isIceCracked) {
AudioService.instance.playLineSfx(_activeTheme);
triggerSpecialEffect("GHIACCIO INCRINATO!", Colors.cyanAccent);
HapticFeedback.mediumImpact();
return;
}
if (newClosed.isEmpty) {
AudioService.instance.playLineSfx(_activeTheme);
if (!isOpponent) HapticFeedback.lightImpact();
} else {
for (var b in newClosed) {
if (b.isJokerRevealed) {
if (b.owner == b.hiddenJokerOwner) {
AudioService.instance.playBonusSfx();
triggerSpecialEffect("JOLLY! +2", Colors.greenAccent);
} else {
AudioService.instance.playBombSfx();
triggerSpecialEffect("JOLLY! -1", Colors.redAccent);
}
HapticFeedback.heavyImpact();
return;
}
}
bool isGold = newClosed.any((b) => b.type == BoxType.gold);
bool isBomb = newClosed.any((b) => b.type == BoxType.bomb);
bool isSwap = newClosed.any((b) => b.type == BoxType.swap);
bool isMultiplier = newClosed.any((b) => b.type == BoxType.multiplier);
if (isSwap) {
AudioService.instance.playBonusSfx();
triggerSpecialEffect("SCAMBIO!", Colors.purpleAccent);
HapticFeedback.heavyImpact();
} else if (isMultiplier) {
AudioService.instance.playBonusSfx();
triggerSpecialEffect("MOLTIPLICATORE x2!", Colors.yellowAccent);
HapticFeedback.heavyImpact();
} else if (isGold) {
AudioService.instance.playBonusSfx(); triggerSpecialEffect("+2", Colors.amber); HapticFeedback.heavyImpact();
} else if (isBomb) {
AudioService.instance.playBombSfx(); triggerSpecialEffect("-1", Colors.redAccent); HapticFeedback.heavyImpact();
} else {
AudioService.instance.playBoxSfx(_activeTheme); HapticFeedback.heavyImpact();
}
}
}
void _startTimer() {
_blitzTimer?.cancel();
if (isSetupPhase) return;
timeLeft = maxTime;
if (!isTimeMode) { notifyListeners(); return; }
_blitzTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (isGameOver || isCPUThinking) { timer.cancel(); return; }
if (timeLeft > 0) {
timeLeft--; notifyListeners();
} else {
timer.cancel();
if (!isOnline || board.currentPlayer == myPlayer) { _handleTimeOut(); }
}
});
}
void _handleTimeOut() {
if (!isTimeMode || isSetupPhase) return;
if (isOnline) {
Line randomMove = AIEngine.getBestMove(board, 5);
handleLineTap(randomMove, _activeTheme, forced: true);
} else if (isVsCPU && board.currentPlayer == Player.red) {
Line randomMove = AIEngine.getBestMove(board, cpuLevel);
handleLineTap(randomMove, _activeTheme, forced: true);
} else if (!isVsCPU) {
Line randomMove = AIEngine.getBestMove(board, 5);
handleLineTap(randomMove, _activeTheme, forced: true);
}
}
void disconnectOnlineGame() {
_onlineSubscription?.cancel();
_onlineSubscription = null;
_blitzTimer?.cancel();
_effectTimer?.cancel();
_myReactionTimer?.cancel();
_oppReactionTimer?.cancel();
_lastOpponentReactionTime = null;
if (isOnline && roomCode != null) {
FirebaseFirestore.instance.collection('games').doc(roomCode).update({'status': 'abandoned'}).catchError((e) => null);
}
isOnline = false; roomCode = null; currentMatchLevel = 1; currentSeed = null;
}
@override
void dispose() { disconnectOnlineGame(); super.dispose(); }
void _listenToOnlineGame(String code) {
_onlineSubscription = FirebaseFirestore.instance.collection('games').doc(code).snapshots().listen((doc) {
if (!doc.exists) return;
var data = doc.data() as Map<String, dynamic>;
onlineHostName = data['hostName'] ?? "ROSSO";
onlineGuestName = (data['guestName'] != null && data['guestName'] != '') ? data['guestName'] : "BLU";
if (data['status'] == 'abandoned' && !board.isGameOver && !opponentLeft) {
opponentLeft = true; notifyListeners(); return;
}
String? p1React = data['p1_reaction'];
Timestamp? p1Time = data['p1_reaction_time'] as Timestamp?;
String? p2React = data['p2_reaction'];
Timestamp? p2Time = data['p2_reaction_time'] as Timestamp?;
if (isHost && p2React != null && p2Time != null && p2Time != _lastOpponentReactionTime) {
_lastOpponentReactionTime = p2Time;
_showReaction(false, p2React);
} else if (!isHost && p1React != null && p1Time != null && p1Time != _lastOpponentReactionTime) {
_lastOpponentReactionTime = p1Time;
_showReaction(false, p1React);
}
bool p1Rematch = data['p1_rematch'] ?? false;
bool p2Rematch = data['p2_rematch'] ?? false;
opponentWantsRematch = isHost ? p2Rematch : p1Rematch;
if (data['status'] == 'playing' && (data['moves'] as List).isEmpty && rematchRequested) {
currentSeed = data['seed'];
startNewGame(data['radius'], isOnline: true, roomCode: roomCode, isHost: isHost, shape: ArenaShape.values.firstWhere((e) => e.name == data['shape']), timeMode: data['timeMode']);
return;
}
if (p1Rematch && p2Rematch && isHost && data['status'] != 'playing') {
currentMatchLevel++;
int newSeed = DateTime.now().millisecondsSinceEpoch % 1000000;
final rand = Random();
int newRadius = rand.nextInt(4) + 3;
ArenaShape newShape = ArenaShape.values[rand.nextInt(ArenaShape.values.length)];
MultiplayerService().resetMatch(roomCode!, newRadius, newShape.name, newSeed);
}
if (isSetupPhase) {
if (!isHost && data['p1_joker'] != null && !oppJokerPlaced) {
int jx = data['p1_joker']['x']; int jy = data['p1_joker']['y'];
board.boxes.firstWhere((b) => b.x == jx && b.y == jy).hiddenJokerOwner = Player.red;
oppJokerPlaced = true; _checkSetupComplete();
}
if (isHost && data['p2_joker'] != null && !oppJokerPlaced) {
int jx = data['p2_joker']['x']; int jy = data['p2_joker']['y'];
board.boxes.firstWhere((b) => b.x == jx && b.y == jy).hiddenJokerOwner = Player.blue;
oppJokerPlaced = true; _checkSetupComplete();
}
}
List<dynamic> moves = data['moves'] ?? [];
int hostLevel = data['matchLevel'] ?? 1;
int? hostSeed = data['seed'];
int hostRadius = data['radius'] ?? board.radius;
String shapeStr = data['shape'] ?? 'classic';
ArenaShape hostShape = ArenaShape.values.firstWhere((e) => e.name == shapeStr, orElse: () => ArenaShape.classic);
onlineShape = hostShape;
isTimeMode = data['timeMode'] ?? true;
if (!rematchRequested && (hostLevel > currentMatchLevel || (isOnline && currentSeed == null && hostSeed != null) || (hostSeed != null && hostSeed != currentSeed))) {
currentMatchLevel = hostLevel; currentSeed = hostSeed;
int levelToUse = (currentMatchLevel == 1) ? 2 : currentMatchLevel;
board = GameBoard(radius: hostRadius, level: levelToUse, seed: currentSeed, shape: onlineShape);
board.currentPlayer = Player.red;
isCPUThinking = false; notifyListeners(); return;
}
int firebaseMovesCount = moves.length;
int localMovesCount = board.lines.where((l) => l.owner != Player.none).length;
if (firebaseMovesCount == 0 && localMovesCount > 0 && !rematchRequested) {
int levelToUse = (currentMatchLevel == 1) ? 2 : currentMatchLevel;
board = GameBoard(radius: hostRadius, level: levelToUse, seed: currentSeed, shape: onlineShape);
board.currentPlayer = Player.red;
notifyListeners(); return;
}
if (firebaseMovesCount > localMovesCount) {
bool newMovesApplied = false;
for (int i = localMovesCount; i < firebaseMovesCount; i++) {
var m = moves[i]; Line? lineToPlay;
for (var line in board.lines) {
if ((line.p1.x == m['x1'] && line.p1.y == m['y1'] && line.p2.x == m['x2'] && line.p2.y == m['y2']) ||
(line.p1.x == m['x2'] && line.p1.y == m['y2'] && line.p2.x == m['x1'] && line.p2.y == m['y1'])) {
lineToPlay = line; break;
}
}
if (lineToPlay != null && lineToPlay.owner == Player.none) {
Player playerFromFirebase = (m['player'] == 'red') ? Player.red : Player.blue;
bool isOpponentMove = (playerFromFirebase != myPlayer);
List<Box> closedBefore = board.boxes.where((b) => b.owner != Player.none).toList();
List<Box> ghostsBefore = board.boxes.where((b) => b.type == BoxType.invisible && b.isRevealed).toList();
board.playMove(lineToPlay, forcedPlayer: playerFromFirebase);
newMovesApplied = true;
List<Box> newClosed = board.boxes.where((b) => b.owner != Player.none && !closedBefore.contains(b)).toList();
List<Box> newGhosts = board.boxes.where((b) => b.type == BoxType.invisible && b.isRevealed && !ghostsBefore.contains(b)).toList();
if (isOpponentMove) _playEffects(newClosed, newGhosts: newGhosts, isOpponent: true);
}
}
if (newMovesApplied) {
String expectedTurnStr = data['turn'] ?? 'red';
Player expectedTurn = expectedTurnStr == 'red' ? Player.red : Player.blue;
if (!board.isGameOver && board.currentPlayer != expectedTurn) { board.currentPlayer = expectedTurn; }
_startTimer();
}
if (board.isGameOver) _saveMatchResult();
notifyListeners();
}
});
}
void handleLineTap(Line line, AppThemeType theme, {bool forced = false}) {
if ((isSetupPhase || isCPUThinking || board.isGameOver || opponentLeft) && !forced) return;
if (isOnline && board.currentPlayer != myPlayer && !forced) return;
_activeTheme = theme;
List<Box> closedBefore = board.boxes.where((b) => b.owner != Player.none).toList();
List<Box> ghostsBefore = board.boxes.where((b) => b.type == BoxType.invisible && b.isRevealed).toList();
if (board.playMove(line)) {
List<Box> newClosed = board.boxes.where((b) => b.owner != Player.none && !closedBefore.contains(b)).toList();
List<Box> newGhosts = board.boxes.where((b) => b.type == BoxType.invisible && b.isRevealed && !ghostsBefore.contains(b)).toList();
if (!forced) _playEffects(newClosed, newGhosts: newGhosts, isOpponent: false);
_startTimer(); notifyListeners();
if (isOnline && roomCode != null) {
Map<String, dynamic> moveData = {
'x1': line.p1.x, 'y1': line.p1.y, 'x2': line.p2.x, 'y2': line.p2.y,
'player': myPlayer == Player.red ? 'red' : 'blue'
};
String nextTurnStr = board.currentPlayer == Player.red ? 'red' : 'blue';
FirebaseFirestore.instance.collection('games').doc(roomCode).update({
'moves': FieldValue.arrayUnion([moveData]),
'turn': nextTurnStr
}).catchError((e) => debugPrint("Errore: $e"));
if (board.isGameOver) {
_saveMatchResult();
if (isHost) FirebaseFirestore.instance.collection('games').doc(roomCode).update({'status': 'finished'});
}
} else {
if (board.isGameOver) _saveMatchResult();
else if (isVsCPU && board.currentPlayer == Player.blue) _checkCPUTurn();
}
}
}
void _checkCPUTurn() async {
if (isVsCPU && board.currentPlayer == Player.blue && !board.isGameOver) {
isCPUThinking = true; _blitzTimer?.cancel(); notifyListeners();
await Future.delayed(const Duration(milliseconds: 600));
if (!board.isGameOver) {
List<Box> closedBefore = board.boxes.where((b) => b.owner != Player.none).toList();
List<Box> ghostsBefore = board.boxes.where((b) => b.type == BoxType.invisible && b.isRevealed).toList();
Line bestMove = AIEngine.getBestMove(board, cpuLevel);
board.playMove(bestMove);
List<Box> newClosed = board.boxes.where((b) => b.owner != Player.none && !closedBefore.contains(b)).toList();
List<Box> newGhosts = board.boxes.where((b) => b.type == BoxType.invisible && b.isRevealed && !ghostsBefore.contains(b)).toList();
_playEffects(newClosed, newGhosts: newGhosts, isOpponent: true);
isCPUThinking = false; _startTimer(); notifyListeners();
if (board.isGameOver) _saveMatchResult();
else _checkCPUTurn();
}
}
}
// --- LOGICA LEVEL UP E SBLOCCHI ---
List<String> _getUnlocks(int oldLevel, int newLevel) {
List<String> unlocks = [];
for(int i = oldLevel + 1; i <= newLevel; i++) {
if (i == 3) unlocks.add("Tema: Legno & Fiammiferi");
if (i == 5) unlocks.add("Tema: Quaderno (Doodle)");
if (i == 7) unlocks.add("Tema: Cyberpunk");
if (i == 10) {
unlocks.add("Tema: 8-Bit Arcade");
unlocks.add("Forma Arena: Caos");
}
if (i == 15) unlocks.add("Tema: Grimorio");
}
return unlocks;
}
void _saveMatchResult() {
if (_hasSavedResult) return;
_hasSavedResult = true;
int calculatedXP = 0;
bool isDraw = board.scoreRed == board.scoreBlue;
String myRealName = StorageService.instance.playerName;
if (myRealName.isEmpty) myRealName = "IO";
int oldLevel = StorageService.instance.playerLevel; // Salviamo il vecchio livello
if (isOnline) {
bool isWin = isHost ? board.scoreRed > board.scoreBlue : board.scoreBlue > board.scoreRed;
calculatedXP = isWin ? 20 : (isDraw ? 5 : 2);
String oppName = isHost ? onlineGuestName : onlineHostName;
int myScore = isHost ? board.scoreRed : board.scoreBlue;
int oppScore = isHost ? board.scoreBlue : board.scoreRed;
StorageService.instance.saveMatchToHistory(myName: myRealName, opponent: oppName, myScore: myScore, oppScore: oppScore, isOnline: true);
if (isWin) StorageService.instance.updateQuestProgress(0, 1);
} else if (isVsCPU) {
int myScore = board.scoreRed; int cpuScore = board.scoreBlue;
bool isWin = myScore > cpuScore;
calculatedXP = isWin ? (10 + (cpuLevel * 2)) : (isDraw ? 5 : 2);
if (isWin) {
StorageService.instance.addWin();
StorageService.instance.updateQuestProgress(1, 1);
} else if (cpuScore > myScore) {
StorageService.instance.addLoss();
}
StorageService.instance.saveMatchToHistory(myName: myRealName, opponent: "CPU (Liv. $cpuLevel)", myScore: myScore, oppScore: cpuScore, isOnline: false);
} else {
calculatedXP = 2;
StorageService.instance.saveMatchToHistory(myName: myRealName, opponent: "Ospite (Locale)", myScore: board.scoreRed, oppScore: board.scoreBlue, isOnline: false);
}
if (board.shape != ArenaShape.classic) {
StorageService.instance.updateQuestProgress(2, 1);
}
lastMatchXP = calculatedXP;
StorageService.instance.addXP(calculatedXP);
// --- CONTROLLO LEVEL UP DOPO AVER DATO GLI XP ---
int newLevel = StorageService.instance.playerLevel;
if (newLevel > oldLevel) {
hasLeveledUp = true;
newlyReachedLevel = newLevel;
unlockedFeatures = _getUnlocks(oldLevel, newLevel);
}
notifyListeners();
}
void increaseLevelAndRestart() {
cpuLevel++; StorageService.instance.saveCpuLevel(cpuLevel);
startNewGame(board.radius, vsCPU: true, shape: board.shape, timeMode: isTimeMode);
}
}