=== FLUTTER PROJECT BACKUP ===
=== PROJECT STRUCTURE (LIB & ASSETS) ===
assets/.DS_Store
assets/audio/.DS_Store
assets/audio/bgm/8-bit_Prowler.mp3
assets/audio/bgm/Cyber_Dystopia.mp3
assets/audio/bgm/Grimorio_Astral.mp3
assets/audio/bgm/Legno_Canopy.mp3
assets/audio/bgm/Music_Loop.mp3
assets/audio/bgm/Quad_Dreams.mp3
assets/audio/sfx/cyber_box.wav
assets/audio/sfx/cyber_line.wav
assets/audio/sfx/doodle_box.wav
assets/audio/sfx/doodle_line.wav
assets/audio/sfx/minimal_box.wav
assets/audio/sfx/minimal_line.wav
assets/icon/icona_master.png
assets/images/.DS_Store
assets/images/arcade.jpg
assets/images/cyber_bg.jpg
assets/images/doodle_bg.jpg
assets/images/doodle_bg_riserva.jpg
assets/images/egizi_bg.jpg
assets/images/grimorio copia.jpg
assets/images/grimorio.jpg
assets/images/icona_big.jpeg
assets/images/music_bg.jpg
assets/images/sfondo_temi.jpg
lib/.DS_Store
lib/core/app_colors.dart
lib/core/constants.dart
lib/core/theme_manager.dart
lib/firebase_options.dart
lib/l10n/app_de.arb
lib/l10n/app_en.arb
lib/l10n/app_es.arb
lib/l10n/app_fr.arb
lib/l10n/app_it.arb
lib/l10n/app_localizations.dart
lib/l10n/app_localizations_de.dart
lib/l10n/app_localizations_en.dart
lib/l10n/app_localizations_es.dart
lib/l10n/app_localizations_fr.dart
lib/l10n/app_localizations_it.dart
lib/l10n/app_localizations_pt.dart
lib/l10n/app_localizations_ru.dart
lib/l10n/app_localizations_zh.dart
lib/l10n/app_pt.arb
lib/l10n/app_ru.arb
lib/l10n/app_zh.arb
lib/logic/ai_engine.dart
lib/logic/game_controller.dart
lib/main.dart
lib/models/game_board.dart
lib/models/player_info.dart
lib/services/audio_service.dart
lib/services/firebase_service.dart
lib/services/multiplayer_service.dart
lib/services/storage_service.dart
lib/ui/.DS_Store
lib/ui/admin/admin_screen.dart
lib/ui/game/board_painter.dart
lib/ui/game/game_screen.dart
lib/ui/game/score_board.dart
lib/ui/home/dialog.dart
lib/ui/home/history_screen.dart
lib/ui/home/home_modals.dart
lib/ui/home/home_screen.dart
lib/ui/multiplayer/lobby_screen.dart
lib/ui/multiplayer/lobby_widgets.dart
lib/ui/settings/settings_screen.dart
lib/widgets/custom_button.dart
lib/widgets/custom_settings_button.dart
lib/widgets/cyber_border.dart
lib/widgets/game_over_dialog.dart
lib/widgets/home_buttons.dart
lib/widgets/music_theme_widgets.dart
lib/widgets/painters.dart
=== pubspec.yaml ===
name: tetraq
description: A new Flutter project.
publish_to: 'none'
version: 1.1.5+7
environment:
sdk: ^3.10.7
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.8
# I nostri "Superpoteri"
provider: ^6.1.2 # Per gestire lo stato (Temi, Punteggi)
shared_preferences: ^2.5.4 # Per salvare opzioni e record sul telefono
audioplayers: ^5.2.1 # Per la musica e gli effetti sonori
intl: ^0.20.2 # Necessario per le traduzioni
flutter_localizations: # Il sistema multilingua ufficiale
sdk: flutter
firebase_core: ^4.4.0
firebase_auth: ^6.1.4 # <--- NUOVO: LA CORAZZA DI SICUREZZA!
cloud_firestore: ^6.1.2
share_plus: ^12.0.1
app_links: ^7.0.0
google_fonts: ^8.0.2
font_awesome_flutter: ^10.12.0
firebase_app_check: ^0.4.1+5
package_info_plus: ^9.0.0
device_info_plus: ^12.3.0
# --- NUOVI PACCHETTI PER GLI AGGIORNAMENTI ---
upgrader: ^12.5.0
in_app_update: ^4.2.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^6.0.0
flutter_launcher_icons: ^0.13.1
change_app_package_name: ^1.5.0
flutter:
uses-material-design: true
# Abilita la generazione automatica delle traduzioni
generate: true
# Dichiariamo le cartelle dove metteremo immagini e suoni
assets:
- assets/images/
- assets/audio/bgm/
- assets/audio/sfx/
- assets/audio/
flutter_icons:
android: "ic_launcher"
ios: true
macos:
generate: true
image_path: "assets/icon/icona_master.png"
min_sdk_android: 21 # Serve per compatibilità con Android recenti
=== MAC OS CONFIG ===
--- Info.plist ---
LSApplicationCategoryType
public.app-category.puzzle-games
CFBundleDevelopmentRegion
$(DEVELOPMENT_LANGUAGE)
CFBundleExecutable
$(EXECUTABLE_NAME)
CFBundleIconFile
CFBundleIdentifier
$(PRODUCT_BUNDLE_IDENTIFIER)
CFBundleInfoDictionaryVersion
6.0
CFBundleName
$(PRODUCT_NAME)
CFBundlePackageType
APPL
CFBundleShortVersionString
$(FLUTTER_BUILD_NAME)
CFBundleVersion
$(FLUTTER_BUILD_NUMBER)
LSMinimumSystemVersion
$(MACOSX_DEPLOYMENT_TARGET)
NSHumanReadableCopyright
$(PRODUCT_COPYRIGHT)
NSMainNibFile
MainMenu
NSPrincipalClass
NSApplication
--- Entitlements ---
com.apple.security.app-sandbox
com.apple.security.cs.allow-jit
com.apple.security.network.client
com.apple.security.network.server
keychain-access-groups
com.apple.security.app-sandbox
com.apple.security.cs.allow-jit
com.apple.security.network.client
keychain-access-groups
--- Podfile ---
platform :osx, '10.15'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
project 'Runner', {
'Debug' => :debug,
'Profile' => :release,
'Release' => :release,
}
def flutter_root
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__)
unless File.exist?(generated_xcode_build_settings_path)
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first"
end
File.foreach(generated_xcode_build_settings_path) do |line|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
return matches[1].strip if matches
end
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\""
end
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
flutter_macos_podfile_setup
target 'Runner' do
use_frameworks!
flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__))
target 'RunnerTests' do
inherit! :search_paths
end
end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_macos_build_settings(target)
end
end
=== IOS CONFIG ===
--- Info.plist ---
CADisableMinimumFrameDurationOnPhone
CFBundleDevelopmentRegion
$(DEVELOPMENT_LANGUAGE)
CFBundleDisplayName
Tetraq
CFBundleExecutable
$(EXECUTABLE_NAME)
CFBundleIdentifier
$(PRODUCT_BUNDLE_IDENTIFIER)
CFBundleInfoDictionaryVersion
6.0
CFBundleName
tetraq
CFBundlePackageType
APPL
CFBundleShortVersionString
$(FLUTTER_BUILD_NAME)
CFBundleSignature
????
CFBundleURLTypes
CFBundleTypeRole
Editor
CFBundleURLName
com.sanza.tetraq
CFBundleURLSchemes
tetraq
CFBundleVersion
$(FLUTTER_BUILD_NUMBER)
LSRequiresIPhoneOS
UIApplicationSupportsIndirectInputEvents
UILaunchStoryboardName
LaunchScreen
UIMainStoryboardFile
Main
UISupportedInterfaceOrientations
UIInterfaceOrientationPortrait
UIInterfaceOrientationLandscapeLeft
UIInterfaceOrientationLandscapeRight
UISupportedInterfaceOrientations~ipad
UIInterfaceOrientationPortrait
UIInterfaceOrientationPortraitUpsideDown
UIInterfaceOrientationLandscapeLeft
UIInterfaceOrientationLandscapeRight
--- Podfile ---
# Uncomment this line to define a global platform for your project
platform :ios, '15.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
project 'Runner', {
'Debug' => :debug,
'Profile' => :release,
'Release' => :release,
}
def flutter_root
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
unless File.exist?(generated_xcode_build_settings_path)
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
end
File.foreach(generated_xcode_build_settings_path) do |line|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
return matches[1].strip if matches
end
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
end
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
flutter_ios_podfile_setup
target 'Runner' do
use_frameworks!
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
target 'RunnerTests' do
inherit! :search_paths
end
end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
end
end
=== ANDROID CONFIG ===
--- AndroidManifest.xml ---
--- build.gradle / build.gradle.kts ---
plugins {
id("com.android.application")
// START: FlutterFire Configuration
id("com.google.gms.google-services")
// END: FlutterFire Configuration
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
// Aggiungiamo esplicitamente gli import richiesti da Kotlin
import java.io.FileInputStream
import java.util.Properties
// Carichiamo il file con le password
val keystoreProperties = Properties()
val keystorePropertiesFile = rootProject.file("key.properties")
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
}
android {
namespace = "com.amastra.tetraq"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
// Sintassi aggiornata come richiesto dal compilatore Kotlin
kotlin {
compilerOptions {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
}
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.amastra.tetraq"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
// Aggiunto il blocco per la firma in formato Kotlin DSL
signingConfigs {
create("release") {
keyAlias = keystoreProperties.getProperty("keyAlias")
keyPassword = keystoreProperties.getProperty("keyPassword")
val storeFileString = keystoreProperties.getProperty("storeFile")
if (storeFileString != null) {
storeFile = file(storeFileString)
}
storePassword = keystoreProperties.getProperty("storePassword")
}
}
buildTypes {
getByName("release") {
// TODO: Add your own signing config for the release build.
// Ora usiamo la chiave di release appena creata
signingConfig = signingConfigs.getByName("release")
}
}
}
flutter {
source = "../.."
}
=== SOURCE CODE (lib/) ===
// ===========================================================================
// FILE: lib/core/app_colors.dart
// ===========================================================================
// ===========================================================================
// FILE: lib/core/app_colors.dart
// ===========================================================================
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
enum AppThemeType { doodle, cyberpunk, arcade, grimorio, music }
class ThemeColors {
final Color background;
final Color gridLine;
final Color playerRed;
final Color playerBlue;
final Color text;
const ThemeColors({
required this.background,
required this.gridLine,
required this.playerRed,
required this.playerBlue,
required this.text,
});
}
class AppColors {
static const ThemeColors doodle = ThemeColors(
background: Color(0xFFFFF9E6), gridLine: Color(0xFFB0BEC5),
playerRed: Color(0xFFD32F2F), playerBlue: Color(0xFF1565C0), text: Color(0xFF37474F),
);
static const ThemeColors cyberpunk = ThemeColors(
background: Color(0xFF0A001A), gridLine: Color(0xFF6200EA),
playerRed: Color(0xFFFF007F), playerBlue: Color(0xFF69F0AE), text: Color(0xFFFFFFFF),
);
static const ThemeColors arcade = ThemeColors(
background: Color(0xFF111111), gridLine: Color(0xFF00FF00),
playerRed: Color(0xFFFF004D), playerBlue: Color(0xFF00E5FF), text: Color(0xFFFFFFFF),
);
static const ThemeColors grimorio = ThemeColors(
background: Color(0xFF1E112A), gridLine: Colors.black,
playerRed: Color(0xFFE91E63), playerBlue: Color(0xFF4FC3F7), text: Color(0xFFFFF3E0),
);
static const ThemeColors music = ThemeColors(
background: Color(0xFF120B29),
gridLine: Color(0xFF6A1B9A),
playerRed: Color(0xFFFF2A6D),
playerBlue: Color(0xFF05D5FF),
text: Color(0xFFE0E0E0),
);
static ThemeColors getTheme(AppThemeType type) {
switch (type) {
case AppThemeType.doodle: return doodle;
case AppThemeType.cyberpunk: return cyberpunk;
case AppThemeType.arcade: return arcade;
case AppThemeType.grimorio: return grimorio;
case AppThemeType.music: return music;
}
}
}
class ThemeIcons {
static IconData gold(AppThemeType type) {
switch (type) {
case AppThemeType.doodle: return FontAwesomeIcons.star;
case AppThemeType.cyberpunk: return FontAwesomeIcons.microchip;
case AppThemeType.arcade: return FontAwesomeIcons.coins;
case AppThemeType.grimorio: return FontAwesomeIcons.crown;
case AppThemeType.music: return FontAwesomeIcons.compactDisc;
}
}
static IconData bomb(AppThemeType type) {
switch (type) {
case AppThemeType.doodle: return FontAwesomeIcons.virus;
case AppThemeType.cyberpunk: return FontAwesomeIcons.bug;
case AppThemeType.arcade: return FontAwesomeIcons.ghost;
case AppThemeType.grimorio: return FontAwesomeIcons.hatWizard;
case AppThemeType.music: return FontAwesomeIcons.volumeXmark;
}
}
static IconData swap(AppThemeType type) {
switch (type) {
case AppThemeType.doodle: return FontAwesomeIcons.arrowsRotate;
case AppThemeType.cyberpunk: return FontAwesomeIcons.networkWired;
case AppThemeType.arcade: return FontAwesomeIcons.shuffle;
case AppThemeType.grimorio: return FontAwesomeIcons.hurricane;
case AppThemeType.music: return FontAwesomeIcons.sliders;
}
}
static IconData joker(AppThemeType type) {
switch (type) {
case AppThemeType.doodle: return FontAwesomeIcons.faceSmileBeam;
case AppThemeType.cyberpunk: return FontAwesomeIcons.robot;
case AppThemeType.arcade: return FontAwesomeIcons.gamepad;
case AppThemeType.grimorio: return FontAwesomeIcons.masksTheater;
case AppThemeType.music: return FontAwesomeIcons.headphones;
}
}
static IconData block(AppThemeType type) {
switch (type) {
case AppThemeType.doodle: return FontAwesomeIcons.squareXmark;
case AppThemeType.cyberpunk: return FontAwesomeIcons.shieldHalved;
case AppThemeType.arcade: return FontAwesomeIcons.powerOff;
case AppThemeType.grimorio: return FontAwesomeIcons.meteor;
case AppThemeType.music: return FontAwesomeIcons.pause;
}
}
static IconData ice(AppThemeType type) {
if (type == AppThemeType.music) return FontAwesomeIcons.music;
return FontAwesomeIcons.snowflake;
}
static IconData multiplier(AppThemeType type) {
if (type == AppThemeType.music) return FontAwesomeIcons.forwardFast;
return FontAwesomeIcons.bolt;
}
}
// ===========================================================================
// FILE: lib/core/constants.dart
// ===========================================================================
class Constants {
// Chiavi per salvare i dati sul telefono
static const String prefThemeKey = 'selected_theme';
static const String prefLanguageKey = 'selected_language';
static const String prefBoardSizeKey = 'board_size';
// Impostazioni della scacchiera a rombo - RAGGI INCREMENTATI
static const int minBoardRadius = 2; // Ex Normale, ora è Piccola
static const int maxBoardRadius = 5; // Formato MAX, enorme
static const int defaultBoardRadius = 3; // Ora il default è più grande
}
// ===========================================================================
// FILE: lib/core/theme_manager.dart
// ===========================================================================
// ===========================================================================
// FILE: lib/core/theme_manager.dart
// ===========================================================================
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'app_colors.dart';
import '../services/storage_service.dart';
// --- ENUM DEI TEMI AGGIORNATO ---
const Map themeIcons = {
AppThemeType.cyberpunk: Icons.electric_bolt,
AppThemeType.doodle: Icons.brush,
AppThemeType.music: Icons.headset_mic,
AppThemeType.arcade: Icons.videogame_asset,
AppThemeType.grimorio: Icons.auto_stories,
};
const Map themeNames = {
AppThemeType.cyberpunk: "Cyberpunk",
AppThemeType.doodle: "Doodle",
AppThemeType.music: "Music",
AppThemeType.arcade: "Arcade",
AppThemeType.grimorio: "Grimorio",
};
class ThemeManager with ChangeNotifier {
AppThemeType _currentThemeType = AppThemeType.doodle;
ThemeColors _currentColors = AppColors.getTheme(AppThemeType.doodle);
AppThemeType get currentThemeType => _currentThemeType;
ThemeColors get currentColors => _currentColors;
ThemeManager() {
_loadTheme();
}
void _loadTheme() async {
String themeStr = StorageService.instance.getTheme();
AppThemeType loadedType = AppThemeType.values.firstWhere(
(e) => e.toString() == themeStr,
orElse: () => AppThemeType.doodle
);
_currentThemeType = loadedType;
_currentColors = AppColors.getTheme(loadedType);
_updateSystemUI();
notifyListeners();
}
void setTheme(AppThemeType type) {
_currentThemeType = type;
_currentColors = AppColors.getTheme(type);
StorageService.instance.saveTheme(type.toString());
_updateSystemUI();
notifyListeners();
}
void _updateSystemUI() {
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: _currentThemeType == AppThemeType.doodle ? Brightness.dark : Brightness.light,
systemNavigationBarColor: _currentColors.background,
systemNavigationBarIconBrightness: _currentThemeType == AppThemeType.doodle ? Brightness.dark : Brightness.light,
));
}
}
// ===========================================================================
// FILE: lib/firebase_options.dart
// ===========================================================================
// File generated by FlutterFire CLI.
// ignore_for_file: type=lint
import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;
import 'package:flutter/foundation.dart'
show defaultTargetPlatform, kIsWeb, TargetPlatform;
/// Default [FirebaseOptions] for use with your Firebase apps.
///
/// Example:
/// ```dart
/// import 'firebase_options.dart';
/// // ...
/// await Firebase.initializeApp(
/// options: DefaultFirebaseOptions.currentPlatform,
/// );
/// ```
class DefaultFirebaseOptions {
static FirebaseOptions get currentPlatform {
if (kIsWeb) {
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for web - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
}
switch (defaultTargetPlatform) {
case TargetPlatform.android:
return android;
case TargetPlatform.iOS:
return ios;
case TargetPlatform.macOS:
return macos;
case TargetPlatform.windows:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for windows - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
case TargetPlatform.linux:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for linux - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
default:
throw UnsupportedError(
'DefaultFirebaseOptions are not supported for this platform.',
);
}
}
static const FirebaseOptions android = FirebaseOptions(
apiKey: 'AIzaSyBsXO595xVITDPrRnXrW8HPQLOe7Rz4Gg4',
appId: '1:705460445314:android:ceac21bb06b7a9f07b949b',
messagingSenderId: '705460445314',
projectId: 'tetraq-32a4a',
storageBucket: 'tetraq-32a4a.firebasestorage.app',
);
static const FirebaseOptions ios = FirebaseOptions(
apiKey: 'AIzaSyB77j18Jgeb9gBAEwp-uyOQvr4m-RJ_rAE',
appId: '1:705460445314:ios:54d64cb7592954327b949b',
messagingSenderId: '705460445314',
projectId: 'tetraq-32a4a',
storageBucket: 'tetraq-32a4a.firebasestorage.app',
iosBundleId: 'com.amastra.tetraq',
);
static const FirebaseOptions macos = FirebaseOptions(
apiKey: 'AIzaSyB77j18Jgeb9gBAEwp-uyOQvr4m-RJ_rAE',
appId: '1:705460445314:ios:da11cbca5d1f6bc27b949b',
messagingSenderId: '705460445314',
projectId: 'tetraq-32a4a',
storageBucket: 'tetraq-32a4a.firebasestorage.app',
iosBundleId: 'com.sanza.tetraq',
);
}
// ===========================================================================
// FILE: lib/l10n/app_localizations.dart
// ===========================================================================
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:intl/intl.dart' as intl;
import 'app_localizations_de.dart';
import 'app_localizations_en.dart';
import 'app_localizations_es.dart';
import 'app_localizations_fr.dart';
import 'app_localizations_it.dart';
import 'app_localizations_pt.dart';
import 'app_localizations_ru.dart';
import 'app_localizations_zh.dart';
// ignore_for_file: type=lint
/// Callers can lookup localized strings with an instance of AppLocalizations
/// returned by `AppLocalizations.of(context)`.
///
/// Applications need to include `AppLocalizations.delegate()` in their app's
/// `localizationDelegates` list, and the locales they support in the app's
/// `supportedLocales` list. For example:
///
/// ```dart
/// import 'l10n/app_localizations.dart';
///
/// return MaterialApp(
/// localizationsDelegates: AppLocalizations.localizationsDelegates,
/// supportedLocales: AppLocalizations.supportedLocales,
/// home: MyApplicationHome(),
/// );
/// ```
///
/// ## Update pubspec.yaml
///
/// Please make sure to update your pubspec.yaml to include the following
/// packages:
///
/// ```yaml
/// dependencies:
/// # Internationalization support.
/// flutter_localizations:
/// sdk: flutter
/// intl: any # Use the pinned version from flutter_localizations
///
/// # Rest of dependencies
/// ```
///
/// ## iOS Applications
///
/// iOS applications define key application metadata, including supported
/// locales, in an Info.plist file that is built into the application bundle.
/// To configure the locales supported by your app, you’ll need to edit this
/// file.
///
/// First, open your project’s ios/Runner.xcworkspace Xcode workspace file.
/// Then, in the Project Navigator, open the Info.plist file under the Runner
/// project’s Runner folder.
///
/// Next, select the Information Property List item, select Add Item from the
/// Editor menu, then select Localizations from the pop-up menu.
///
/// Select and expand the newly-created Localizations item then, for each
/// locale your application supports, add a new item and select the locale
/// you wish to add from the pop-up menu in the Value field. This list should
/// be consistent with the languages listed in the AppLocalizations.supportedLocales
/// property.
abstract class AppLocalizations {
AppLocalizations(String locale)
: localeName = intl.Intl.canonicalizedLocale(locale.toString());
final String localeName;
static AppLocalizations? of(BuildContext context) {
return Localizations.of(context, AppLocalizations);
}
static const LocalizationsDelegate delegate =
_AppLocalizationsDelegate();
/// A list of this localizations delegate along with the default localizations
/// delegates.
///
/// Returns a list of localizations delegates containing this delegate along with
/// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate,
/// and GlobalWidgetsLocalizations.delegate.
///
/// Additional delegates can be added by appending to this list in
/// MaterialApp. This list does not have to be used at all if a custom list
/// of delegates is preferred or required.
static const List> localizationsDelegates =
>[
delegate,
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
];
/// A list of this localizations delegate's supported locales.
static const List supportedLocales = [
Locale('de'),
Locale('en'),
Locale('es'),
Locale('fr'),
Locale('it'),
Locale('pt'),
Locale('ru'),
Locale('zh'),
];
/// No description provided for @appTitle.
///
/// In it, this message translates to:
/// **'TetraQ'**
String get appTitle;
/// No description provided for @welcomeTitle.
///
/// In it, this message translates to:
/// **'BENVENUTO IN TETRAQ!'**
String get welcomeTitle;
/// No description provided for @nameHint.
///
/// In it, this message translates to:
/// **'NOME'**
String get nameHint;
/// No description provided for @saveAndPlay.
///
/// In it, this message translates to:
/// **'SALVA E GIOCA'**
String get saveAndPlay;
/// No description provided for @onlineTitle.
///
/// In it, this message translates to:
/// **'ONLINE'**
String get onlineTitle;
/// No description provided for @onlineSub.
///
/// In it, this message translates to:
/// **'Sfida il mondo'**
String get onlineSub;
/// No description provided for @cpuTitle.
///
/// In it, this message translates to:
/// **'VS CPU'**
String get cpuTitle;
/// No description provided for @cpuSub.
///
/// In it, this message translates to:
/// **'Allenati con l\'IA'**
String get cpuSub;
/// No description provided for @localTitle.
///
/// In it, this message translates to:
/// **'LOCALE'**
String get localTitle;
/// No description provided for @localSub.
///
/// In it, this message translates to:
/// **'Stesso schermo'**
String get localSub;
/// No description provided for @leaderboardTitle.
///
/// In it, this message translates to:
/// **'CLASSIFICA'**
String get leaderboardTitle;
/// No description provided for @questsTitle.
///
/// In it, this message translates to:
/// **'SFIDE'**
String get questsTitle;
/// No description provided for @themesTitle.
///
/// In it, this message translates to:
/// **'TEMI'**
String get themesTitle;
/// No description provided for @tutorialTitle.
///
/// In it, this message translates to:
/// **'TUTORIAL'**
String get tutorialTitle;
/// No description provided for @startGame.
///
/// In it, this message translates to:
/// **'AVVIA PARTITA'**
String get startGame;
/// No description provided for @createMatch.
///
/// In it, this message translates to:
/// **'CREA PARTITA'**
String get createMatch;
/// No description provided for @joinMatch.
///
/// In it, this message translates to:
/// **'UNISCITI'**
String get joinMatch;
/// No description provided for @gameOver.
///
/// In it, this message translates to:
/// **'FINE PARTITA'**
String get gameOver;
/// No description provided for @mainMenu.
///
/// In it, this message translates to:
/// **'TORNA AL MENU'**
String get mainMenu;
/// No description provided for @exit.
///
/// In it, this message translates to:
/// **'ESCI'**
String get exit;
}
class _AppLocalizationsDelegate
extends LocalizationsDelegate {
const _AppLocalizationsDelegate();
@override
Future load(Locale locale) {
return SynchronousFuture(lookupAppLocalizations(locale));
}
@override
bool isSupported(Locale locale) => [
'de',
'en',
'es',
'fr',
'it',
'pt',
'ru',
'zh',
].contains(locale.languageCode);
@override
bool shouldReload(_AppLocalizationsDelegate old) => false;
}
AppLocalizations lookupAppLocalizations(Locale locale) {
// Lookup logic when only language code is specified.
switch (locale.languageCode) {
case 'de':
return AppLocalizationsDe();
case 'en':
return AppLocalizationsEn();
case 'es':
return AppLocalizationsEs();
case 'fr':
return AppLocalizationsFr();
case 'it':
return AppLocalizationsIt();
case 'pt':
return AppLocalizationsPt();
case 'ru':
return AppLocalizationsRu();
case 'zh':
return AppLocalizationsZh();
}
throw FlutterError(
'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely '
'an issue with the localizations generation tool. Please file an issue '
'on GitHub with a reproducible sample app and the gen-l10n configuration '
'that was used.',
);
}
// ===========================================================================
// FILE: lib/l10n/app_localizations_de.dart
// ===========================================================================
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
/// The translations for German (`de`).
class AppLocalizationsDe extends AppLocalizations {
AppLocalizationsDe([String locale = 'de']) : super(locale);
@override
String get appTitle => 'TetraQ';
@override
String get welcomeTitle => 'WILLKOMMEN BEI TETRAQ!';
@override
String get nameHint => 'NAME';
@override
String get saveAndPlay => 'SPEICHERN & SPIELEN';
@override
String get onlineTitle => 'ONLINE';
@override
String get onlineSub => 'Fordere die Welt heraus';
@override
String get cpuTitle => 'VS CPU';
@override
String get cpuSub => 'Trainiere mit KI';
@override
String get localTitle => 'LOKAL';
@override
String get localSub => 'Gleicher Bildschirm';
@override
String get leaderboardTitle => 'RANGLISTE';
@override
String get questsTitle => 'MISSIONEN';
@override
String get themesTitle => 'THEMEN';
@override
String get tutorialTitle => 'TUTORIAL';
@override
String get startGame => 'SPIEL STARTEN';
@override
String get createMatch => 'SPIEL ERSTELLEN';
@override
String get joinMatch => 'BEITRETEN';
@override
String get gameOver => 'SPIELENDE';
@override
String get mainMenu => 'ZURÜCK ZUM MENÜ';
@override
String get exit => 'BEENDEN';
}
// ===========================================================================
// FILE: lib/l10n/app_localizations_en.dart
// ===========================================================================
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
/// The translations for English (`en`).
class AppLocalizationsEn extends AppLocalizations {
AppLocalizationsEn([String locale = 'en']) : super(locale);
@override
String get appTitle => 'TetraQ';
@override
String get welcomeTitle => 'WELCOME TO TETRAQ!';
@override
String get nameHint => 'NAME';
@override
String get saveAndPlay => 'SAVE & PLAY';
@override
String get onlineTitle => 'ONLINE';
@override
String get onlineSub => 'Challenge the world';
@override
String get cpuTitle => 'VS CPU';
@override
String get cpuSub => 'Train with AI';
@override
String get localTitle => 'LOCAL';
@override
String get localSub => 'Same screen';
@override
String get leaderboardTitle => 'LEADERBOARD';
@override
String get questsTitle => 'QUESTS';
@override
String get themesTitle => 'THEMES';
@override
String get tutorialTitle => 'TUTORIAL';
@override
String get startGame => 'START GAME';
@override
String get createMatch => 'CREATE MATCH';
@override
String get joinMatch => 'JOIN';
@override
String get gameOver => 'GAME OVER';
@override
String get mainMenu => 'BACK TO MENU';
@override
String get exit => 'EXIT';
}
// ===========================================================================
// FILE: lib/l10n/app_localizations_es.dart
// ===========================================================================
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
/// The translations for Spanish Castilian (`es`).
class AppLocalizationsEs extends AppLocalizations {
AppLocalizationsEs([String locale = 'es']) : super(locale);
@override
String get appTitle => 'TetraQ';
@override
String get welcomeTitle => '¡BIENVENIDO A TETRAQ!';
@override
String get nameHint => 'NOMBRE';
@override
String get saveAndPlay => 'GUARDAR Y JUGAR';
@override
String get onlineTitle => 'ONLINE';
@override
String get onlineSub => 'Desafía al mundo';
@override
String get cpuTitle => 'VS CPU';
@override
String get cpuSub => 'Entrena con IA';
@override
String get localTitle => 'LOCAL';
@override
String get localSub => 'Misma pantalla';
@override
String get leaderboardTitle => 'RANKING';
@override
String get questsTitle => 'MISIONES';
@override
String get themesTitle => 'TEMAS';
@override
String get tutorialTitle => 'TUTORIAL';
@override
String get startGame => 'INICIAR JUEGO';
@override
String get createMatch => 'CREAR PARTIDA';
@override
String get joinMatch => 'UNIRSE';
@override
String get gameOver => 'FIN DEL JUEGO';
@override
String get mainMenu => 'VOLVER AL MENÚ';
@override
String get exit => 'SALIR';
}
// ===========================================================================
// FILE: lib/l10n/app_localizations_fr.dart
// ===========================================================================
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
/// The translations for French (`fr`).
class AppLocalizationsFr extends AppLocalizations {
AppLocalizationsFr([String locale = 'fr']) : super(locale);
@override
String get appTitle => 'TetraQ';
@override
String get welcomeTitle => 'BIENVENUE DANS TETRAQ !';
@override
String get nameHint => 'NOM';
@override
String get saveAndPlay => 'SAUVEGARDER ET JOUER';
@override
String get onlineTitle => 'EN LIGNE';
@override
String get onlineSub => 'Défiez le monde';
@override
String get cpuTitle => 'VS CPU';
@override
String get cpuSub => 'Entraînez avec l\'IA';
@override
String get localTitle => 'LOCAL';
@override
String get localSub => 'Même écran';
@override
String get leaderboardTitle => 'CLASSEMENT';
@override
String get questsTitle => 'QUÊTES';
@override
String get themesTitle => 'THÈMES';
@override
String get tutorialTitle => 'TUTORIEL';
@override
String get startGame => 'JOUER';
@override
String get createMatch => 'CRÉER UN MATCH';
@override
String get joinMatch => 'REJOINDRE';
@override
String get gameOver => 'FIN DE PARTIE';
@override
String get mainMenu => 'RETOUR AU MENU';
@override
String get exit => 'QUITTER';
}
// ===========================================================================
// FILE: lib/l10n/app_localizations_it.dart
// ===========================================================================
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
/// The translations for Italian (`it`).
class AppLocalizationsIt extends AppLocalizations {
AppLocalizationsIt([String locale = 'it']) : super(locale);
@override
String get appTitle => 'TetraQ';
@override
String get welcomeTitle => 'BENVENUTO IN TETRAQ!';
@override
String get nameHint => 'NOME';
@override
String get saveAndPlay => 'SALVA E GIOCA';
@override
String get onlineTitle => 'ONLINE';
@override
String get onlineSub => 'Sfida il mondo';
@override
String get cpuTitle => 'VS CPU';
@override
String get cpuSub => 'Allenati con l\'IA';
@override
String get localTitle => 'LOCALE';
@override
String get localSub => 'Stesso schermo';
@override
String get leaderboardTitle => 'CLASSIFICA';
@override
String get questsTitle => 'SFIDE';
@override
String get themesTitle => 'TEMI';
@override
String get tutorialTitle => 'TUTORIAL';
@override
String get startGame => 'AVVIA PARTITA';
@override
String get createMatch => 'CREA PARTITA';
@override
String get joinMatch => 'UNISCITI';
@override
String get gameOver => 'FINE PARTITA';
@override
String get mainMenu => 'TORNA AL MENU';
@override
String get exit => 'ESCI';
}
// ===========================================================================
// FILE: lib/l10n/app_localizations_pt.dart
// ===========================================================================
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
/// The translations for Portuguese (`pt`).
class AppLocalizationsPt extends AppLocalizations {
AppLocalizationsPt([String locale = 'pt']) : super(locale);
@override
String get appTitle => 'TetraQ';
@override
String get welcomeTitle => 'BEM-VINDO AO TETRAQ!';
@override
String get nameHint => 'NOME';
@override
String get saveAndPlay => 'SALVAR E JOGAR';
@override
String get onlineTitle => 'ONLINE';
@override
String get onlineSub => 'Desafie o mundo';
@override
String get cpuTitle => 'VS CPU';
@override
String get cpuSub => 'Treine com a IA';
@override
String get localTitle => 'LOCAL';
@override
String get localSub => 'Mesma tela';
@override
String get leaderboardTitle => 'CLASSIFICAÇÃO';
@override
String get questsTitle => 'DESAFIOS';
@override
String get themesTitle => 'TEMAS';
@override
String get tutorialTitle => 'TUTORIAL';
@override
String get startGame => 'INICIAR JOGO';
@override
String get createMatch => 'CRIAR PARTIDA';
@override
String get joinMatch => 'ENTRAR';
@override
String get gameOver => 'FIM DE JOGO';
@override
String get mainMenu => 'VOLTAR AO MENU';
@override
String get exit => 'SAIR';
}
// ===========================================================================
// FILE: lib/l10n/app_localizations_ru.dart
// ===========================================================================
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
/// The translations for Russian (`ru`).
class AppLocalizationsRu extends AppLocalizations {
AppLocalizationsRu([String locale = 'ru']) : super(locale);
@override
String get appTitle => 'TetraQ';
@override
String get welcomeTitle => 'ДОБРО ПОЖАЛОВАТЬ В TETRAQ!';
@override
String get nameHint => 'ИМЯ';
@override
String get saveAndPlay => 'СОХРАНИТЬ И ИГРАТЬ';
@override
String get onlineTitle => 'ОНЛАЙН';
@override
String get onlineSub => 'Брось вызов миру';
@override
String get cpuTitle => 'VS ИИ';
@override
String get cpuSub => 'Тренировка с ИИ';
@override
String get localTitle => 'ЛОКАЛЬНО';
@override
String get localSub => 'Один экран';
@override
String get leaderboardTitle => 'РЕЙТИНГ';
@override
String get questsTitle => 'ЗАДАНИЯ';
@override
String get themesTitle => 'ТЕМЫ';
@override
String get tutorialTitle => 'ОБУЧЕНИЕ';
@override
String get startGame => 'НАЧАТЬ ИГРУ';
@override
String get createMatch => 'СОЗДАТЬ ИГРУ';
@override
String get joinMatch => 'ПРИСОЕДИНИТЬСЯ';
@override
String get gameOver => 'ИГРА ОКОНЧЕНА';
@override
String get mainMenu => 'В ГЛАВНОЕ МЕНЮ';
@override
String get exit => 'ВЫХОД';
}
// ===========================================================================
// FILE: lib/l10n/app_localizations_zh.dart
// ===========================================================================
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
/// The translations for Chinese (`zh`).
class AppLocalizationsZh extends AppLocalizations {
AppLocalizationsZh([String locale = 'zh']) : super(locale);
@override
String get appTitle => 'TetraQ';
@override
String get welcomeTitle => '欢迎来到 TETRAQ!';
@override
String get nameHint => '名字';
@override
String get saveAndPlay => '保存并开始';
@override
String get onlineTitle => '在线匹配';
@override
String get onlineSub => '挑战世界';
@override
String get cpuTitle => '人机对战';
@override
String get cpuSub => '与AI训练';
@override
String get localTitle => '本地游戏';
@override
String get localSub => '同屏对战';
@override
String get leaderboardTitle => '排行榜';
@override
String get questsTitle => '任务';
@override
String get themesTitle => '主题';
@override
String get tutorialTitle => '教程';
@override
String get startGame => '开始游戏';
@override
String get createMatch => '创建比赛';
@override
String get joinMatch => '加入';
@override
String get gameOver => '游戏结束';
@override
String get mainMenu => '返回主菜单';
@override
String get exit => '退出';
}
// ===========================================================================
// FILE: lib/logic/ai_engine.dart
// ===========================================================================
// ===========================================================================
// FILE: lib/logic/ai_engine.dart
// ===========================================================================
import 'dart:math';
import '../models/game_board.dart';
class _ClosureResult {
final bool closesSomething;
final int netValue;
final bool causesSwap;
_ClosureResult(this.closesSomething, this.netValue, this.causesSwap);
}
class AIEngine {
static Line getBestMove(GameBoard board, int level) {
List availableLines = board.lines.where((l) => l.owner == Player.none && l.isPlayable).toList();
final random = Random();
if (availableLines.isEmpty) return board.lines.first;
double smartChance = 0.50 + ((level - 1) * 0.10);
if (smartChance > 1.0) smartChance = 1.0;
bool beSmart = random.nextDouble() < smartChance;
int myScore = board.currentPlayer == Player.red ? board.scoreRed : board.scoreBlue;
int oppScore = board.currentPlayer == Player.red ? board.scoreBlue : board.scoreRed;
List goodClosingMoves = [];
List badClosingMoves = [];
for (var line in availableLines) {
var result = _checkClosure(board, line);
if (result.closesSomething) {
if (result.causesSwap) {
if (myScore < oppScore) {
goodClosingMoves.add(line);
} else {
badClosingMoves.add(line);
}
} else {
if (result.netValue >= 0) {
goodClosingMoves.add(line);
} else {
badClosingMoves.add(line);
}
}
}
}
// --- REGOLA 1: Chiudere i quadrati vantaggiosi ---
if (goodClosingMoves.isNotEmpty) {
if (beSmart || random.nextDouble() < 0.70) {
return goodClosingMoves[random.nextInt(goodClosingMoves.length)];
}
}
// --- REGOLA 2: Mosse Sicure ---
List safeMoves = [];
for (var line in availableLines) {
if (!badClosingMoves.contains(line) && !goodClosingMoves.contains(line) && _isSafeMove(board, line, myScore, oppScore)) {
safeMoves.add(line);
}
}
if (safeMoves.isNotEmpty) {
if (beSmart) {
return safeMoves[random.nextInt(safeMoves.length)];
} else {
if (random.nextDouble() < 0.5) {
return safeMoves[random.nextInt(safeMoves.length)];
}
}
}
// --- REGOLA 3: Scegliere il male minore ---
if (beSmart) {
List riskyButNotTerrible = availableLines.where((l) => !badClosingMoves.contains(l) && !goodClosingMoves.contains(l)).toList();
if (riskyButNotTerrible.isNotEmpty) {
return riskyButNotTerrible[random.nextInt(riskyButNotTerrible.length)];
}
}
List nonTerribleMoves = availableLines.where((l) => !badClosingMoves.contains(l)).toList();
if (nonTerribleMoves.isNotEmpty) {
return nonTerribleMoves[random.nextInt(nonTerribleMoves.length)];
}
return availableLines[random.nextInt(availableLines.length)];
}
static _ClosureResult _checkClosure(GameBoard board, Line line) {
int netValue = 0;
bool closesSomething = false;
bool causesSwap = false;
for (var box in board.boxes) {
if (box.type == BoxType.invisible) continue;
if (box.top == line || box.bottom == line || box.left == line || box.right == line) {
int linesCount = 0;
if (box.top.owner != Player.none || box.top == line) linesCount++;
if (box.bottom.owner != Player.none || box.bottom == line) linesCount++;
if (box.left.owner != Player.none || box.left == line) linesCount++;
if (box.right.owner != Player.none || box.right == line) linesCount++;
if (linesCount == 4) {
closesSomething = true;
// FIX: Togliamo la "vista a raggi X" all'Intelligenza Artificiale!
if (box.hiddenJokerOwner == board.currentPlayer) {
// L'IA conosce il suo Jolly, sa che vale +2 e cercherà di chiuderlo
netValue += 2;
} else {
// Se c'è il Jolly del giocatore, l'IA NON DEVE SAPERLO e valuta la casella normalmente!
if (box.type == BoxType.gold) netValue += 2;
else if (box.type == BoxType.bomb) netValue -= 1;
else if (box.type == BoxType.swap) netValue += 0;
else netValue += 1;
}
if (box.type == BoxType.swap) causesSwap = true;
}
}
}
return _ClosureResult(closesSomething, netValue, causesSwap);
}
static bool _isSafeMove(GameBoard board, Line line, int myScore, int oppScore) {
for (var box in board.boxes) {
if (box.type == BoxType.invisible) continue;
if (box.top == line || box.bottom == line || box.left == line || box.right == line) {
int currentLinesCount = 0;
if (box.top.owner != Player.none) currentLinesCount++;
if (box.bottom.owner != Player.none) currentLinesCount++;
if (box.left.owner != Player.none) currentLinesCount++;
if (box.right.owner != Player.none) currentLinesCount++;
if (currentLinesCount == 2) {
// Nuova logica di sicurezza: cosa succede se l'IA lascia questa scatola all'avversario?
int valueForOpponent = 0;
if (box.hiddenJokerOwner == board.currentPlayer) {
// Se l'avversario la chiude, becca la trappola dell'IA (-1).
// Quindi PER L'IA È SICURISSIMO LASCIARE QUESTA CASELLA APERTA!
valueForOpponent = -1;
} else {
if (box.type == BoxType.gold) valueForOpponent = 2;
else if (box.type == BoxType.bomb) valueForOpponent = -1;
else if (box.type == BoxType.swap) valueForOpponent = 0;
else valueForOpponent = 1;
}
// Se per l'avversario vale -1 (bomba normale o trappola dell'IA), lasciamogliela!
if (valueForOpponent < 0) {
continue;
}
if (box.type == BoxType.swap) {
if (myScore < oppScore) {
continue;
} else {
return false;
}
}
return false;
}
}
}
return true;
}
}
// ===========================================================================
// FILE: lib/logic/game_controller.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 CpuMatchSetup {
final int radius;
final ArenaShape shape;
CpuMatchSetup(this.radius, this.shape);
}
class GameController extends ChangeNotifier {
late GameBoard board;
bool isVsCPU = false;
bool isCPUThinking = false;
bool isOnline = false;
String? roomCode;
bool isHost = false;
StreamSubscription? _onlineSubscription;
bool opponentLeft = false;
bool _hasSavedResult = false;
Timer? _blitzTimer;
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
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;
static const Map>> 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}
],
};
bool hasLeveledUp = false;
int newlyReachedLevel = 1;
List