2026-02-27 23:35:54 +01:00
|
|
|
// ===========================================================================
|
|
|
|
|
// 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';
|
2026-03-01 20:59:06 +01:00
|
|
|
|
2026-02-27 23:35:54 +01:00
|
|
|
import '../models/game_board.dart';
|
2026-03-01 20:59:06 +01:00
|
|
|
export '../models/game_board.dart';
|
|
|
|
|
|
2026-02-27 23:35:54 +01:00
|
|
|
import 'ai_engine.dart';
|
|
|
|
|
import '../services/audio_service.dart';
|
|
|
|
|
import '../services/storage_service.dart';
|
|
|
|
|
import '../services/multiplayer_service.dart';
|
|
|
|
|
import '../core/app_colors.dart';
|
|
|
|
|
|
2026-03-13 22:00:00 +01:00
|
|
|
class CpuMatchSetup {
|
|
|
|
|
final int radius;
|
|
|
|
|
final ArenaShape shape;
|
|
|
|
|
CpuMatchSetup(this.radius, this.shape);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-27 23:35:54 +01:00
|
|
|
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;
|
2026-03-20 22:00:01 +01:00
|
|
|
int timeLeft = 10;
|
|
|
|
|
int maxTime = 10;
|
|
|
|
|
String timeModeSetting = 'fixed'; // 'fixed', 'relax', 'dynamic'
|
|
|
|
|
bool get isTimeMode => timeModeSetting != 'relax';
|
|
|
|
|
int consecutiveRematches = 0; // Contatore per la modalità Dinamica
|
2026-02-27 23:35:54 +01:00
|
|
|
|
|
|
|
|
String effectText = '';
|
|
|
|
|
Color effectColor = Colors.transparent;
|
|
|
|
|
Timer? _effectTimer;
|
|
|
|
|
|
|
|
|
|
String? myReaction;
|
|
|
|
|
String? opponentReaction;
|
|
|
|
|
Timer? _myReactionTimer;
|
|
|
|
|
Timer? _oppReactionTimer;
|
2026-03-01 20:59:06 +01:00
|
|
|
|
|
|
|
|
Timestamp? _lastOpponentReactionTime;
|
|
|
|
|
|
2026-02-27 23:35:54 +01:00
|
|
|
bool rematchRequested = false;
|
|
|
|
|
bool opponentWantsRematch = false;
|
2026-03-01 20:59:06 +01:00
|
|
|
int lastMatchXP = 0;
|
|
|
|
|
|
2026-03-20 14:00:00 +01:00
|
|
|
static const Map<int, List<Map<String, dynamic>>> rewardsRoadmap = {
|
|
|
|
|
2: [{'title': 'Bomba & Oro', 'desc': 'Appaiono le caselle speciali: Oro (+2) e Bomba (-1)!', 'icon': Icons.stars, 'color': Colors.amber}],
|
|
|
|
|
3: [
|
|
|
|
|
{'title': 'Tema Cyberpunk', 'desc': 'Sbloccato un nuovo tema visivo nelle impostazioni.', 'icon': Icons.palette, 'color': Colors.tealAccent},
|
|
|
|
|
{'title': 'Arena a Croce', 'desc': 'Sbloccata una nuova forma arena più complessa.', 'icon': Icons.add_box, 'color': Colors.blueAccent}
|
|
|
|
|
],
|
|
|
|
|
5: [{'title': 'Scambio', 'desc': 'Nuova casella! Inverte istantaneamente i punteggi.', 'icon': Icons.swap_horiz, 'color': Colors.purpleAccent}],
|
|
|
|
|
7: [
|
|
|
|
|
{'title': 'Tema 8-Bit', 'desc': 'Sbloccato il nostalgico tema sala giochi.', 'icon': Icons.videogame_asset, 'color': Colors.greenAccent},
|
|
|
|
|
{'title': 'Arene Caos', 'desc': 'Generazione procedurale sbloccata. Nessuna partita sarà uguale!', 'icon': Icons.all_inclusive, 'color': Colors.redAccent}
|
|
|
|
|
],
|
|
|
|
|
10: [
|
|
|
|
|
{'title': 'Tema Grimorio', 'desc': 'Sbloccato il tema della magia antica.', 'icon': Icons.auto_stories, 'color': Colors.deepPurpleAccent},
|
|
|
|
|
{'title': 'Blocco di Ghiaccio', 'desc': 'Nuova meccanica! Il ghiaccio richiede due colpi per rompersi.', 'icon': Icons.ac_unit, 'color': Colors.cyanAccent}
|
|
|
|
|
],
|
|
|
|
|
15: [
|
|
|
|
|
{'title': 'Tema Musica', 'desc': 'Sbloccato il tema a tempo di beat.', 'icon': Icons.headphones, 'color': Colors.pinkAccent},
|
|
|
|
|
{'title': 'Moltiplicatore x2', 'desc': 'Nuova casella! Raddoppia i punti della tua prossima conquista.', 'icon': Icons.bolt, 'color': Colors.yellowAccent}
|
|
|
|
|
],
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-12 21:00:08 +01:00
|
|
|
bool hasLeveledUp = false;
|
|
|
|
|
int newlyReachedLevel = 1;
|
2026-03-20 22:00:01 +01:00
|
|
|
List<Map<String, dynamic>> unlockedRewards = [];
|
2026-03-12 21:00:08 +01:00
|
|
|
|
2026-03-01 20:59:06 +01:00
|
|
|
bool isSetupPhase = true;
|
|
|
|
|
bool myJokerPlaced = false;
|
|
|
|
|
bool oppJokerPlaced = false;
|
|
|
|
|
Player jokerTurn = Player.red;
|
2026-02-27 23:35:54 +01:00
|
|
|
|
2026-03-01 20:59:06 +01:00
|
|
|
Player get myPlayer => isOnline ? (isHost ? Player.red : Player.blue) : Player.red;
|
2026-02-27 23:35:54 +01:00
|
|
|
bool get isGameOver => board.isGameOver;
|
|
|
|
|
|
|
|
|
|
int cpuLevel = 1;
|
|
|
|
|
int currentMatchLevel = 1;
|
|
|
|
|
int? currentSeed;
|
2026-03-13 22:00:00 +01:00
|
|
|
AppThemeType _activeTheme = AppThemeType.doodle;
|
2026-02-27 23:35:54 +01:00
|
|
|
|
|
|
|
|
String onlineHostName = "ROSSO";
|
|
|
|
|
String onlineGuestName = "BLU";
|
|
|
|
|
ArenaShape onlineShape = ArenaShape.classic;
|
|
|
|
|
|
|
|
|
|
GameController({int radius = 3}) {
|
|
|
|
|
cpuLevel = StorageService.instance.cpuLevel;
|
|
|
|
|
startNewGame(radius);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-13 22:00:00 +01:00
|
|
|
CpuMatchSetup _getSetupForCpuLevel(int level) {
|
|
|
|
|
final rand = Random();
|
|
|
|
|
if (level == 1) return CpuMatchSetup(3, ArenaShape.classic);
|
|
|
|
|
if (level == 2) return CpuMatchSetup(4, ArenaShape.classic);
|
|
|
|
|
if (level == 3) return CpuMatchSetup(4, ArenaShape.cross);
|
|
|
|
|
if (level == 4) return CpuMatchSetup(4, ArenaShape.donut);
|
|
|
|
|
if (level == 5) return CpuMatchSetup(5, ArenaShape.classic);
|
|
|
|
|
if (level == 6) return CpuMatchSetup(4, ArenaShape.hourglass);
|
|
|
|
|
if (level == 7) return CpuMatchSetup(5, ArenaShape.cross);
|
|
|
|
|
if (level == 8) return CpuMatchSetup(5, ArenaShape.donut);
|
|
|
|
|
if (level == 9) return CpuMatchSetup(5, ArenaShape.hourglass);
|
|
|
|
|
|
|
|
|
|
List<ArenaShape> hardShapes = [ArenaShape.classic, ArenaShape.cross, ArenaShape.donut, ArenaShape.hourglass, ArenaShape.chaos];
|
|
|
|
|
ArenaShape chosenShape = hardShapes[rand.nextInt(hardShapes.length)];
|
|
|
|
|
|
|
|
|
|
int chosenRadius = (chosenShape == ArenaShape.chaos) ? (rand.nextInt(2) + 4) : (rand.nextInt(2) + 5);
|
|
|
|
|
return CpuMatchSetup(chosenRadius, chosenShape);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-20 22:00:01 +01:00
|
|
|
void startNewGame(int radius, {bool vsCPU = false, bool isOnline = false, String? roomCode, bool isHost = false, ArenaShape shape = ArenaShape.classic, String timeMode = 'fixed', bool isRematch = false}) {
|
2026-02-27 23:35:54 +01:00
|
|
|
_onlineSubscription?.cancel();
|
|
|
|
|
_onlineSubscription = null;
|
|
|
|
|
_blitzTimer?.cancel();
|
|
|
|
|
_effectTimer?.cancel();
|
|
|
|
|
effectText = '';
|
|
|
|
|
_hasSavedResult = false;
|
2026-03-01 20:59:06 +01:00
|
|
|
lastMatchXP = 0;
|
2026-02-27 23:35:54 +01:00
|
|
|
|
2026-03-12 21:00:08 +01:00
|
|
|
hasLeveledUp = false;
|
2026-03-20 14:00:00 +01:00
|
|
|
unlockedRewards.clear();
|
2026-03-12 21:00:08 +01:00
|
|
|
|
2026-02-27 23:35:54 +01:00
|
|
|
myReaction = null;
|
|
|
|
|
opponentReaction = null;
|
2026-03-01 20:59:06 +01:00
|
|
|
_lastOpponentReactionTime = null;
|
2026-02-27 23:35:54 +01:00
|
|
|
rematchRequested = false;
|
|
|
|
|
opponentWantsRematch = false;
|
|
|
|
|
|
2026-03-01 20:59:06 +01:00
|
|
|
isSetupPhase = true;
|
|
|
|
|
myJokerPlaced = false;
|
|
|
|
|
oppJokerPlaced = false;
|
|
|
|
|
jokerTurn = Player.red;
|
|
|
|
|
|
2026-02-27 23:35:54 +01:00
|
|
|
this.isVsCPU = vsCPU;
|
|
|
|
|
this.isOnline = isOnline;
|
|
|
|
|
this.roomCode = roomCode;
|
|
|
|
|
this.isHost = isHost;
|
2026-03-20 22:00:01 +01:00
|
|
|
|
|
|
|
|
if (!isRematch) consecutiveRematches = 0;
|
|
|
|
|
this.timeModeSetting = timeMode;
|
|
|
|
|
|
|
|
|
|
// --- LOGICA TIMER ---
|
|
|
|
|
if (this.isVsCPU) {
|
|
|
|
|
int pLevel = StorageService.instance.playerLevel;
|
|
|
|
|
int calculatedTime = 15 - ((pLevel - 1) * 12 / 14).round();
|
|
|
|
|
maxTime = calculatedTime.clamp(3, 15);
|
|
|
|
|
} else {
|
|
|
|
|
if (timeModeSetting == 'dynamic') {
|
|
|
|
|
maxTime = max(2, 10 - (consecutiveRematches * 2));
|
|
|
|
|
} else if (timeModeSetting == 'relax') {
|
2026-03-23 00:00:04 +01:00
|
|
|
maxTime = 0;
|
2026-03-20 22:00:01 +01:00
|
|
|
} else {
|
2026-03-23 00:00:04 +01:00
|
|
|
maxTime = 10;
|
2026-03-20 22:00:01 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
timeLeft = maxTime;
|
2026-02-27 23:35:54 +01:00
|
|
|
|
2026-03-13 22:00:00 +01:00
|
|
|
int finalRadius = radius;
|
|
|
|
|
ArenaShape finalShape = shape;
|
|
|
|
|
|
|
|
|
|
if (this.isVsCPU) {
|
|
|
|
|
CpuMatchSetup setup = _getSetupForCpuLevel(cpuLevel);
|
|
|
|
|
finalRadius = setup.radius;
|
|
|
|
|
finalShape = setup.shape;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onlineShape = finalShape;
|
2026-02-27 23:35:54 +01:00
|
|
|
int levelToUse = isOnline ? (currentMatchLevel == 1 ? 2 : currentMatchLevel) : cpuLevel;
|
|
|
|
|
|
2026-03-13 22:00:00 +01:00
|
|
|
board = GameBoard(radius: finalRadius, level: levelToUse, seed: currentSeed, shape: finalShape);
|
2026-02-27 23:35:54 +01:00
|
|
|
board.currentPlayer = Player.red;
|
|
|
|
|
|
|
|
|
|
isCPUThinking = false;
|
|
|
|
|
opponentLeft = false;
|
|
|
|
|
|
|
|
|
|
if (this.isOnline && this.roomCode != null) {
|
|
|
|
|
_listenToOnlineGame(this.roomCode!);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
notifyListeners();
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-01 20:59:06 +01:00
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-27 23:35:54 +01:00
|
|
|
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();
|
2026-03-01 20:59:06 +01:00
|
|
|
_myReactionTimer = Timer(const Duration(seconds: 4), () { myReaction = null; notifyListeners(); });
|
2026-02-27 23:35:54 +01:00
|
|
|
} else {
|
|
|
|
|
opponentReaction = reaction;
|
|
|
|
|
_oppReactionTimer?.cancel();
|
2026-03-01 20:59:06 +01:00
|
|
|
_oppReactionTimer = Timer(const Duration(seconds: 4), () { opponentReaction = null; notifyListeners(); });
|
2026-02-27 23:35:54 +01:00
|
|
|
}
|
|
|
|
|
notifyListeners();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void triggerSpecialEffect(String text, Color color) {
|
|
|
|
|
effectText = text;
|
|
|
|
|
effectColor = color;
|
|
|
|
|
notifyListeners();
|
|
|
|
|
_effectTimer?.cancel();
|
|
|
|
|
_effectTimer = Timer(const Duration(milliseconds: 1200), () { effectText = ''; notifyListeners(); });
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-01 20:59:06 +01:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-27 23:35:54 +01:00
|
|
|
if (newClosed.isEmpty) {
|
|
|
|
|
AudioService.instance.playLineSfx(_activeTheme);
|
|
|
|
|
if (!isOpponent) HapticFeedback.lightImpact();
|
|
|
|
|
} else {
|
2026-03-01 20:59:06 +01:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-27 23:35:54 +01:00
|
|
|
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);
|
2026-03-01 20:59:06 +01:00
|
|
|
bool isMultiplier = newClosed.any((b) => b.type == BoxType.multiplier);
|
2026-02-27 23:35:54 +01:00
|
|
|
|
|
|
|
|
if (isSwap) {
|
|
|
|
|
AudioService.instance.playBonusSfx();
|
|
|
|
|
triggerSpecialEffect("SCAMBIO!", Colors.purpleAccent);
|
|
|
|
|
HapticFeedback.heavyImpact();
|
2026-03-01 20:59:06 +01:00
|
|
|
} else if (isMultiplier) {
|
|
|
|
|
AudioService.instance.playBonusSfx();
|
|
|
|
|
triggerSpecialEffect("MOLTIPLICATORE x2!", Colors.yellowAccent);
|
|
|
|
|
HapticFeedback.heavyImpact();
|
2026-02-27 23:35:54 +01:00
|
|
|
} 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();
|
2026-03-20 22:00:01 +01:00
|
|
|
if (isSetupPhase || !isTimeMode) return;
|
2026-02-27 23:35:54 +01:00
|
|
|
|
2026-03-01 20:59:06 +01:00
|
|
|
timeLeft = maxTime;
|
2026-02-27 23:35:54 +01:00
|
|
|
|
|
|
|
|
_blitzTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
|
|
|
|
if (isGameOver || isCPUThinking) { timer.cancel(); return; }
|
|
|
|
|
if (timeLeft > 0) {
|
2026-03-01 20:59:06 +01:00
|
|
|
timeLeft--; notifyListeners();
|
2026-02-27 23:35:54 +01:00
|
|
|
} else {
|
|
|
|
|
timer.cancel();
|
2026-03-01 20:59:06 +01:00
|
|
|
if (!isOnline || board.currentPlayer == myPlayer) { _handleTimeOut(); }
|
2026-02-27 23:35:54 +01:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void _handleTimeOut() {
|
2026-03-01 20:59:06 +01:00
|
|
|
if (!isTimeMode || isSetupPhase) return;
|
2026-02-27 23:35:54 +01:00
|
|
|
|
2026-03-14 19:00:00 +01:00
|
|
|
if (isOnline && board.currentPlayer != myPlayer) return;
|
|
|
|
|
|
|
|
|
|
List<Line> availableLines = board.lines.where((l) => l.owner == Player.none && l.isPlayable).toList();
|
|
|
|
|
if (availableLines.isEmpty) return;
|
|
|
|
|
|
|
|
|
|
final random = Random();
|
|
|
|
|
Line randomMove = availableLines[random.nextInt(availableLines.length)];
|
|
|
|
|
|
|
|
|
|
handleLineTap(randomMove, _activeTheme, forced: true);
|
2026-02-27 23:35:54 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void disconnectOnlineGame() {
|
|
|
|
|
_onlineSubscription?.cancel();
|
|
|
|
|
_onlineSubscription = null;
|
|
|
|
|
_blitzTimer?.cancel();
|
|
|
|
|
_effectTimer?.cancel();
|
|
|
|
|
_myReactionTimer?.cancel();
|
|
|
|
|
_oppReactionTimer?.cancel();
|
2026-03-01 20:59:06 +01:00
|
|
|
_lastOpponentReactionTime = null;
|
2026-02-27 23:35:54 +01:00
|
|
|
|
|
|
|
|
if (isOnline && roomCode != null) {
|
|
|
|
|
FirebaseFirestore.instance.collection('games').doc(roomCode).update({'status': 'abandoned'}).catchError((e) => null);
|
|
|
|
|
}
|
2026-03-01 20:59:06 +01:00
|
|
|
isOnline = false; roomCode = null; currentMatchLevel = 1; currentSeed = null;
|
2026-02-27 23:35:54 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@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";
|
|
|
|
|
|
2026-03-23 00:00:04 +01:00
|
|
|
// 1. GESTIONE ABBANDONO
|
2026-02-27 23:35:54 +01:00
|
|
|
if (data['status'] == 'abandoned' && !board.isGameOver && !opponentLeft) {
|
|
|
|
|
opponentLeft = true; notifyListeners(); return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-23 00:00:04 +01:00
|
|
|
// 2. GESTIONE REAZIONI
|
2026-02-27 23:35:54 +01:00
|
|
|
String? p1React = data['p1_reaction'];
|
2026-03-01 20:59:06 +01:00
|
|
|
Timestamp? p1Time = data['p1_reaction_time'] as Timestamp?;
|
2026-02-27 23:35:54 +01:00
|
|
|
String? p2React = data['p2_reaction'];
|
2026-03-01 20:59:06 +01:00
|
|
|
Timestamp? p2Time = data['p2_reaction_time'] as Timestamp?;
|
2026-02-27 23:35:54 +01:00
|
|
|
|
2026-03-01 20:59:06 +01:00
|
|
|
if (isHost && p2React != null && p2Time != null && p2Time != _lastOpponentReactionTime) {
|
|
|
|
|
_lastOpponentReactionTime = p2Time;
|
2026-02-27 23:35:54 +01:00
|
|
|
_showReaction(false, p2React);
|
2026-03-01 20:59:06 +01:00
|
|
|
} else if (!isHost && p1React != null && p1Time != null && p1Time != _lastOpponentReactionTime) {
|
|
|
|
|
_lastOpponentReactionTime = p1Time;
|
2026-02-27 23:35:54 +01:00
|
|
|
_showReaction(false, p1React);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-23 00:00:04 +01:00
|
|
|
// 3. LOGICA RIVINCITA MIGLIORATA
|
2026-02-27 23:35:54 +01:00
|
|
|
bool p1Rematch = data['p1_rematch'] ?? false;
|
|
|
|
|
bool p2Rematch = data['p2_rematch'] ?? false;
|
|
|
|
|
opponentWantsRematch = isHost ? p2Rematch : p1Rematch;
|
|
|
|
|
|
2026-03-23 00:00:04 +01:00
|
|
|
// SOLO L'HOST si occupa di chiamare resetMatch sul server
|
|
|
|
|
if (isHost && p1Rematch && p2Rematch && data['status'] != 'playing') {
|
2026-02-27 23:35:54 +01:00
|
|
|
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)];
|
2026-03-23 00:00:04 +01:00
|
|
|
|
|
|
|
|
// Questo cambierà lo status in 'playing' e svuoterà l'array moves.
|
2026-02-27 23:35:54 +01:00
|
|
|
MultiplayerService().resetMatch(roomCode!, newRadius, newShape.name, newSeed);
|
2026-03-23 00:00:04 +01:00
|
|
|
return; // L'host aspetterà il prossimo trigger dal server con il nuovo seed.
|
2026-02-27 23:35:54 +01:00
|
|
|
}
|
2026-03-01 20:59:06 +01:00
|
|
|
|
2026-03-23 00:00:04 +01:00
|
|
|
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);
|
|
|
|
|
String hostTimeMode = data['timeMode'] is String ? data['timeMode'] : (data['timeMode'] == true ? 'fixed' : 'relax');
|
|
|
|
|
|
|
|
|
|
// TUTTI (Host e Guest) ripartono SOLO quando vedono il reset effettivo (nuovo seed e status 'playing')
|
|
|
|
|
if (rematchRequested && data['status'] == 'playing' && hostSeed != null && hostSeed != currentSeed) {
|
|
|
|
|
currentSeed = hostSeed;
|
|
|
|
|
consecutiveRematches++;
|
|
|
|
|
startNewGame(hostRadius, isOnline: true, roomCode: roomCode, isHost: isHost, shape: hostShape, timeMode: hostTimeMode, isRematch: true);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 4. GESTIONE FASE INIZIALE (JOLLY)
|
2026-03-01 20:59:06 +01:00
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-27 23:35:54 +01:00
|
|
|
|
2026-03-23 00:00:04 +01:00
|
|
|
// 5. AGGIORNAMENTO LIVELLO / SEED (se non in rivincita)
|
2026-02-27 23:35:54 +01:00
|
|
|
int hostLevel = data['matchLevel'] ?? 1;
|
|
|
|
|
onlineShape = hostShape;
|
2026-03-20 22:00:01 +01:00
|
|
|
timeModeSetting = hostTimeMode;
|
2026-02-27 23:35:54 +01:00
|
|
|
|
|
|
|
|
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);
|
2026-03-01 20:59:06 +01:00
|
|
|
board.currentPlayer = Player.red;
|
2026-02-27 23:35:54 +01:00
|
|
|
isCPUThinking = false; notifyListeners(); return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-23 00:00:04 +01:00
|
|
|
// 6. GESTIONE MOSSE
|
|
|
|
|
List<dynamic> moves = data['moves'] ?? [];
|
2026-02-27 23:35:54 +01:00
|
|
|
int firebaseMovesCount = moves.length;
|
|
|
|
|
int localMovesCount = board.lines.where((l) => l.owner != Player.none).length;
|
|
|
|
|
|
2026-03-23 00:00:04 +01:00
|
|
|
// Resilienza: se il locale ha mosse e il server no (e non stiamo aspettando una rivincita), pulisci.
|
2026-02-27 23:35:54 +01:00
|
|
|
if (firebaseMovesCount == 0 && localMovesCount > 0 && !rematchRequested) {
|
|
|
|
|
int levelToUse = (currentMatchLevel == 1) ? 2 : currentMatchLevel;
|
|
|
|
|
board = GameBoard(radius: hostRadius, level: levelToUse, seed: currentSeed, shape: onlineShape);
|
2026-03-01 20:59:06 +01:00
|
|
|
board.currentPlayer = Player.red;
|
2026-02-27 23:35:54 +01:00
|
|
|
notifyListeners(); return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-23 00:00:04 +01:00
|
|
|
// Applica mosse remote
|
2026-02-27 23:35:54 +01:00
|
|
|
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();
|
2026-03-01 20:59:06 +01:00
|
|
|
List<Box> ghostsBefore = board.boxes.where((b) => b.type == BoxType.invisible && b.isRevealed).toList();
|
2026-02-27 23:35:54 +01:00
|
|
|
|
|
|
|
|
board.playMove(lineToPlay, forcedPlayer: playerFromFirebase);
|
|
|
|
|
newMovesApplied = true;
|
|
|
|
|
|
|
|
|
|
List<Box> newClosed = board.boxes.where((b) => b.owner != Player.none && !closedBefore.contains(b)).toList();
|
2026-03-01 20:59:06 +01:00
|
|
|
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);
|
2026-02-27 23:35:54 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (newMovesApplied) {
|
|
|
|
|
String expectedTurnStr = data['turn'] ?? 'red';
|
|
|
|
|
Player expectedTurn = expectedTurnStr == 'red' ? Player.red : Player.blue;
|
2026-03-01 20:59:06 +01:00
|
|
|
if (!board.isGameOver && board.currentPlayer != expectedTurn) { board.currentPlayer = expectedTurn; }
|
2026-02-27 23:35:54 +01:00
|
|
|
_startTimer();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (board.isGameOver) _saveMatchResult();
|
|
|
|
|
notifyListeners();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void handleLineTap(Line line, AppThemeType theme, {bool forced = false}) {
|
2026-03-01 20:59:06 +01:00
|
|
|
if ((isSetupPhase || isCPUThinking || board.isGameOver || opponentLeft) && !forced) return;
|
2026-02-27 23:35:54 +01:00
|
|
|
if (isOnline && board.currentPlayer != myPlayer && !forced) return;
|
|
|
|
|
|
|
|
|
|
_activeTheme = theme;
|
|
|
|
|
List<Box> closedBefore = board.boxes.where((b) => b.owner != Player.none).toList();
|
2026-03-01 20:59:06 +01:00
|
|
|
List<Box> ghostsBefore = board.boxes.where((b) => b.type == BoxType.invisible && b.isRevealed).toList();
|
2026-02-27 23:35:54 +01:00
|
|
|
|
|
|
|
|
if (board.playMove(line)) {
|
|
|
|
|
List<Box> newClosed = board.boxes.where((b) => b.owner != Player.none && !closedBefore.contains(b)).toList();
|
2026-03-01 20:59:06 +01:00
|
|
|
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);
|
2026-02-27 23:35:54 +01:00
|
|
|
|
|
|
|
|
_startTimer(); notifyListeners();
|
|
|
|
|
|
|
|
|
|
if (isOnline && roomCode != null) {
|
|
|
|
|
Map<String, dynamic> moveData = {
|
2026-03-23 01:00:01 +01:00
|
|
|
'id': DateTime.now().millisecondsSinceEpoch,
|
2026-02-27 23:35:54 +01:00
|
|
|
'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();
|
2026-03-01 20:59:06 +01:00
|
|
|
List<Box> ghostsBefore = board.boxes.where((b) => b.type == BoxType.invisible && b.isRevealed).toList();
|
|
|
|
|
|
2026-02-27 23:35:54 +01:00
|
|
|
Line bestMove = AIEngine.getBestMove(board, cpuLevel);
|
|
|
|
|
board.playMove(bestMove);
|
|
|
|
|
|
|
|
|
|
List<Box> newClosed = board.boxes.where((b) => b.owner != Player.none && !closedBefore.contains(b)).toList();
|
2026-03-01 20:59:06 +01:00
|
|
|
List<Box> newGhosts = board.boxes.where((b) => b.type == BoxType.invisible && b.isRevealed && !ghostsBefore.contains(b)).toList();
|
|
|
|
|
|
|
|
|
|
_playEffects(newClosed, newGhosts: newGhosts, isOpponent: true);
|
2026-02-27 23:35:54 +01:00
|
|
|
|
|
|
|
|
isCPUThinking = false; _startTimer(); notifyListeners();
|
|
|
|
|
|
|
|
|
|
if (board.isGameOver) _saveMatchResult();
|
|
|
|
|
else _checkCPUTurn();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-20 14:00:00 +01:00
|
|
|
List<Map<String, dynamic>> _getUnlocks(int oldLevel, int newLevel) {
|
|
|
|
|
List<Map<String, dynamic>> unlocks = [];
|
2026-03-12 21:00:08 +01:00
|
|
|
for(int i = oldLevel + 1; i <= newLevel; i++) {
|
2026-03-20 14:00:00 +01:00
|
|
|
if (rewardsRoadmap.containsKey(i)) {
|
|
|
|
|
unlocks.addAll(rewardsRoadmap[i]!);
|
2026-03-12 21:00:08 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return unlocks;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-23 01:00:01 +01:00
|
|
|
Future<void> _saveMatchResult() async {
|
2026-02-27 23:35:54 +01:00
|
|
|
if (_hasSavedResult) return;
|
|
|
|
|
_hasSavedResult = true;
|
|
|
|
|
|
2026-03-01 20:59:06 +01:00
|
|
|
int calculatedXP = 0;
|
|
|
|
|
bool isDraw = board.scoreRed == board.scoreBlue;
|
2026-02-27 23:35:54 +01:00
|
|
|
String myRealName = StorageService.instance.playerName;
|
|
|
|
|
if (myRealName.isEmpty) myRealName = "IO";
|
|
|
|
|
|
2026-03-13 22:00:00 +01:00
|
|
|
int oldLevel = StorageService.instance.playerLevel;
|
2026-03-12 21:00:08 +01:00
|
|
|
|
2026-02-27 23:35:54 +01:00
|
|
|
if (isOnline) {
|
2026-03-01 20:59:06 +01:00
|
|
|
bool isWin = isHost ? board.scoreRed > board.scoreBlue : board.scoreBlue > board.scoreRed;
|
|
|
|
|
calculatedXP = isWin ? 20 : (isDraw ? 5 : 2);
|
2026-02-27 23:35:54 +01:00
|
|
|
String oppName = isHost ? onlineGuestName : onlineHostName;
|
|
|
|
|
int myScore = isHost ? board.scoreRed : board.scoreBlue;
|
|
|
|
|
int oppScore = isHost ? board.scoreBlue : board.scoreRed;
|
2026-03-23 01:00:01 +01:00
|
|
|
await StorageService.instance.saveMatchToHistory(myName: myRealName, opponent: oppName, myScore: myScore, oppScore: oppScore, isOnline: true);
|
2026-03-01 20:59:06 +01:00
|
|
|
|
2026-03-23 01:00:01 +01:00
|
|
|
if (isWin) await StorageService.instance.updateQuestProgress(0, 1);
|
2026-03-01 20:59:06 +01:00
|
|
|
|
2026-02-27 23:35:54 +01:00
|
|
|
} else if (isVsCPU) {
|
|
|
|
|
int myScore = board.scoreRed; int cpuScore = board.scoreBlue;
|
2026-03-01 20:59:06 +01:00
|
|
|
bool isWin = myScore > cpuScore;
|
|
|
|
|
calculatedXP = isWin ? (10 + (cpuLevel * 2)) : (isDraw ? 5 : 2);
|
|
|
|
|
|
|
|
|
|
if (isWin) {
|
2026-03-23 01:00:01 +01:00
|
|
|
await StorageService.instance.addWin();
|
|
|
|
|
await StorageService.instance.updateQuestProgress(1, 1);
|
2026-03-01 20:59:06 +01:00
|
|
|
} else if (cpuScore > myScore) {
|
2026-03-23 01:00:01 +01:00
|
|
|
await StorageService.instance.addLoss();
|
2026-03-01 20:59:06 +01:00
|
|
|
}
|
2026-03-23 01:00:01 +01:00
|
|
|
await StorageService.instance.saveMatchToHistory(myName: myRealName, opponent: "CPU (Liv. $cpuLevel)", myScore: myScore, oppScore: cpuScore, isOnline: false);
|
2026-02-27 23:35:54 +01:00
|
|
|
} else {
|
2026-03-01 20:59:06 +01:00
|
|
|
calculatedXP = 2;
|
2026-03-23 01:00:01 +01:00
|
|
|
await StorageService.instance.saveMatchToHistory(myName: myRealName, opponent: "Ospite (Locale)", myScore: board.scoreRed, oppScore: board.scoreBlue, isOnline: false);
|
2026-02-27 23:35:54 +01:00
|
|
|
}
|
2026-03-01 20:59:06 +01:00
|
|
|
|
|
|
|
|
if (board.shape != ArenaShape.classic) {
|
2026-03-23 01:00:01 +01:00
|
|
|
await StorageService.instance.updateQuestProgress(2, 1);
|
2026-03-01 20:59:06 +01:00
|
|
|
}
|
|
|
|
|
|
2026-03-12 21:00:08 +01:00
|
|
|
lastMatchXP = calculatedXP;
|
2026-03-23 01:00:01 +01:00
|
|
|
await StorageService.instance.addXP(calculatedXP);
|
2026-03-12 21:00:08 +01:00
|
|
|
|
|
|
|
|
int newLevel = StorageService.instance.playerLevel;
|
|
|
|
|
if (newLevel > oldLevel) {
|
|
|
|
|
hasLeveledUp = true;
|
|
|
|
|
newlyReachedLevel = newLevel;
|
2026-03-20 14:00:00 +01:00
|
|
|
unlockedRewards = _getUnlocks(oldLevel, newLevel);
|
2026-03-12 21:00:08 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
notifyListeners();
|
2026-02-27 23:35:54 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void increaseLevelAndRestart() {
|
|
|
|
|
cpuLevel++; StorageService.instance.saveCpuLevel(cpuLevel);
|
2026-03-20 22:00:01 +01:00
|
|
|
startNewGame(board.radius, vsCPU: true, shape: board.shape, timeMode: timeModeSetting);
|
2026-02-27 23:35:54 +01:00
|
|
|
}
|
|
|
|
|
}
|