=== 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_bg1.jpg
assets/images/egizi_bg.jpg
assets/images/grimorio.jpg
assets/images/icona_big.jpeg
assets/images/music_bg.jpg
assets/images/wood_bg.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_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.4+6
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
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, wood, 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 wood = ThemeColors(
background: Color(0xFF905D3B), gridLine: Color(0xFF4A301E),
playerRed: Color(0xFFE53935), playerBlue: Color(0xFF29B6F6), text: Color(0xFFFBE9E7),
);
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, // <--- Modificato in nero!
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.wood: return wood;
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.wood: return FontAwesomeIcons.gem;
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.wood: return FontAwesomeIcons.fire;
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.wood: return FontAwesomeIcons.rightLeft;
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.wood: return FontAwesomeIcons.key;
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.wood: return FontAwesomeIcons.ban;
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 'app_colors.dart';
import '../services/storage_service.dart';
import '../services/audio_service.dart'; // <-- NUOVO IMPORT PER LA MUSICA
class ThemeManager extends ChangeNotifier {
late AppThemeType _currentThemeType;
ThemeManager() {
// Quando l'app parte, legge il tema dalla memoria!
_currentThemeType = AppThemeType.values[StorageService.instance.savedThemeIndex];
// Fai partire subito la colonna sonora del tema salvato!
AudioService.instance.playBgm(_currentThemeType);
}
AppThemeType get currentThemeType => _currentThemeType;
ThemeColors get currentColors => AppColors.getTheme(_currentThemeType);
void setTheme(AppThemeType type) {
_currentThemeType = type;
StorageService.instance.saveTheme(type); // Salva la scelta nel "disco fisso"
// Cambia magicamente la canzone in sottofondo!
AudioService.instance.playBgm(type);
notifyListeners();
}
}
// ===========================================================================
// 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 = 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;
bool hasLeveledUp = false;
int newlyReachedLevel = 1;
List 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.doodle;
String onlineHostName = "ROSSO";
String onlineGuestName = "BLU";
ArenaShape onlineShape = ArenaShape.classic;
GameController({int radius = 3}) {
cpuLevel = StorageService.instance.cpuLevel;
startNewGame(radius);
}
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 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);
}
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;
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;
int finalRadius = radius;
ArenaShape finalShape = shape;
if (this.isVsCPU) {
CpuMatchSetup setup = _getSetupForCpuLevel(cpuLevel);
finalRadius = setup.radius;
finalShape = setup.shape;
}
onlineShape = finalShape;
int levelToUse = isOnline ? (currentMatchLevel == 1 ? 2 : currentMatchLevel) : cpuLevel;
board = GameBoard(radius: finalRadius, level: levelToUse, seed: currentSeed, shape: finalShape);
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 newClosed, {List 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;
// Solo chi deve giocare può subire il timeout (se è online)
if (isOnline && board.currentPlayer != myPlayer) return;
// 1. Raccogliamo TUTTE le linee ancora libere e giocabili
List availableLines = board.lines.where((l) => l.owner == Player.none && l.isPlayable).toList();
// Sicurezza: se non ci sono mosse, non facciamo nulla
if (availableLines.isEmpty) return;
// 2. Scegliamo una linea in modo PURAMENTE CASUALE (nessuna intelligenza artificiale)
final random = Random();
Line randomMove = availableLines[random.nextInt(availableLines.length)];
// 3. Eseguiamo la mossa forzata
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;
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 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 closedBefore = board.boxes.where((b) => b.owner != Player.none).toList();
List ghostsBefore = board.boxes.where((b) => b.type == BoxType.invisible && b.isRevealed).toList();
board.playMove(lineToPlay, forcedPlayer: playerFromFirebase);
newMovesApplied = true;
List newClosed = board.boxes.where((b) => b.owner != Player.none && !closedBefore.contains(b)).toList();
List 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 closedBefore = board.boxes.where((b) => b.owner != Player.none).toList();
List ghostsBefore = board.boxes.where((b) => b.type == BoxType.invisible && b.isRevealed).toList();
if (board.playMove(line)) {
List newClosed = board.boxes.where((b) => b.owner != Player.none && !closedBefore.contains(b)).toList();
List 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 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 closedBefore = board.boxes.where((b) => b.owner != Player.none).toList();
List ghostsBefore = board.boxes.where((b) => b.type == BoxType.invisible && b.isRevealed).toList();
Line bestMove = AIEngine.getBestMove(board, cpuLevel);
board.playMove(bestMove);
List newClosed = board.boxes.where((b) => b.owner != Player.none && !closedBefore.contains(b)).toList();
List 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();
}
}
}
List _getUnlocks(int oldLevel, int newLevel) {
List unlocks = [];
for(int i = oldLevel + 1; i <= newLevel; i++) {
if (i == 3) unlocks.add("Tema: Legno & Fiammiferi");
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;
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);
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);
}
}
// ===========================================================================
// FILE: lib/main.dart
// ===========================================================================
// ===========================================================================
// FILE: lib/main.dart
// ===========================================================================
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:provider/provider.dart';
import 'core/theme_manager.dart';
import 'logic/game_controller.dart';
import 'ui/home/home_screen.dart';
import 'services/storage_service.dart';
import 'services/audio_service.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'firebase_options.dart';
import 'package:firebase_app_check/firebase_app_check.dart';
// --- IMPORT PER IL SUPPORTO MULTILINGUA ---
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:tetraq/l10n/app_localizations.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
await FirebaseAppCheck.instance.activate(
androidProvider: kDebugMode ? AndroidProvider.debug : AndroidProvider.playIntegrity,
appleProvider: kDebugMode ? AppleProvider.debug : AppleProvider.deviceCheck,
);
try {
await FirebaseAuth.instance.signInAnonymously();
} catch (e) {
debugPrint("Errore Auth: $e");
}
await StorageService.instance.init();
await AudioService.instance.init();
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => ThemeManager()),
ChangeNotifierProvider(create: (_) => GameController()),
],
child: const TetraQApp(),
),
);
}
class TetraQApp extends StatelessWidget {
const TetraQApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'TetraQ',
debugShowCheckedModeBanner: false,
theme: ThemeData(
fontFamily: 'Roboto',
useMaterial3: true,
),
// --- BIVIO DELLE LINGUE ATTIVATO! ---
// Flutter si occuperà di caricare automaticamente tutte le lingue
// che hai generato tramite lo script.
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
// ------------------------------------
home: const HomeScreen(),
);
}
}
// ===========================================================================
// FILE: lib/models/game_board.dart
// ===========================================================================
// ===========================================================================
// FILE: lib/models/game_board.dart
// ===========================================================================
import 'dart:math';
enum Player { red, blue, none }
enum BoxType { normal, gold, bomb, invisible, swap, ice, multiplier } // Aggiunti ice e multiplier
enum ArenaShape { classic, cross, donut, hourglass, chaos }
class Dot {
final int x;
final int y;
Dot(this.x, this.y);
@override
bool operator ==(Object other) => identical(this, other) || other is Dot && runtimeType == other.runtimeType && x == other.x && y == other.y;
@override
int get hashCode => x.hashCode ^ y.hashCode;
}
class Line {
final Dot p1;
final Dot p2;
Player owner = Player.none;
bool isPlayable = false;
bool isIceCracked = false; // NUOVO: Stato per il blocco di ghiaccio
Line(this.p1, this.p2);
bool connects(Dot a, Dot b) { return (p1 == a && p2 == b) || (p1 == b && p2 == a); }
}
class Box {
final int x;
final int y;
Player owner = Player.none;
late Line top, bottom, left, right;
BoxType type = BoxType.normal;
bool isRevealed = false;
Player? hiddenJokerOwner;
bool isJokerRevealed = false;
Box(this.x, this.y);
bool isClosed() {
if (type == BoxType.invisible) return false;
return top.owner != Player.none && bottom.owner != Player.none && left.owner != Player.none && right.owner != Player.none;
}
int getCalculatedValue(Player closer) {
if (hiddenJokerOwner != null) {
return (closer == hiddenJokerOwner) ? 2 : -1;
}
if (type == BoxType.gold) return 2;
if (type == BoxType.bomb) return -1;
if (type == BoxType.swap || type == BoxType.ice || type == BoxType.multiplier) return 0; // Il moltiplicatore e il ghiaccio non danno punti base
return 1;
}
}
class GameBoard {
final int radius;
final int level;
final int? seed;
final ArenaShape shape;
late int columns;
late int rows;
List dots = [];
List lines = [];
List boxes = [];
Player currentPlayer = Player.red;
int scoreRed = 0;
int scoreBlue = 0;
bool isGameOver = false;
Line? lastMove;
// Variabili per il Moltiplicatore
bool redHasMultiplier = false;
bool blueHasMultiplier = false;
GameBoard({required this.radius, this.level = 1, this.seed, this.shape = ArenaShape.classic}) {
_generateBoard();
}
void _generateBoard() {
final random = seed != null ? Random(seed) : Random();
int chaosAlgorithm = random.nextInt(5);
if (shape == ArenaShape.chaos) {
columns = radius * 2 + 1;
rows = (radius * 3) + 2;
} else {
columns = radius * 2 + 1;
rows = radius * 2 + 1;
}
dots.clear();
lines.clear();
boxes.clear();
lastMove = null;
for (int y = 0; y < rows; y++) {
for (int x = 0; x < columns; x++) {
var box = Box(x, y);
bool isVisible = true;
if (shape != ArenaShape.chaos) {
int dx = (x - radius).abs();
int dy = (y - radius).abs();
isVisible = (dx + dy) <= radius;
if (isVisible) {
switch (shape) {
case ArenaShape.cross:
int spessoreBraccio = radius > 3 ? 1 : 0;
if (dx > spessoreBraccio && dy > spessoreBraccio) isVisible = false; break;
case ArenaShape.donut:
int dimensioneBuco = radius > 3 ? 2 : 1;
if ((dx + dy) <= dimensioneBuco) isVisible = false; break;
case ArenaShape.hourglass:
if (dx > dy) isVisible = false;
if (x == radius && y == radius) isVisible = true; break;
default: break;
}
}
} else {
double percentY = y / rows;
if (chaosAlgorithm == 0) {
isVisible = (x % 2 == 0) && (random.nextDouble() > 0.15);
} else if (chaosAlgorithm == 1) {
double chance = 0.2 + (percentY * 0.7);
isVisible = random.nextDouble() < chance;
} else if (chaosAlgorithm == 2) {
int midY = rows ~/ 2;
int distFromCenterY = (y - midY).abs();
int allowedWidth = (distFromCenterY / midY * radius).ceil() + 1;
int dx = (x - radius).abs();
isVisible = dx <= allowedWidth && random.nextDouble() > 0.1;
} else if (chaosAlgorithm == 3) {
isVisible = (y % 2 == 0) ? (x < columns - 1) : (x > 0);
if (random.nextDouble() > 0.8) isVisible = false;
} else if (chaosAlgorithm == 4) {
isVisible = random.nextDouble() > 0.45;
}
if (x == radius && y == rows ~/ 2) isVisible = true;
}
if (!isVisible) {
box.type = BoxType.invisible;
} else if (level > 1) {
double chance = random.nextDouble();
if (chance < 0.08) box.type = BoxType.gold;
else if (chance > 0.92) box.type = BoxType.bomb;
else if (level >= 5 && chance > 0.88 && chance <= 0.92) box.type = BoxType.swap;
else if (level >= 10 && chance > 0.83 && chance <= 0.88) box.type = BoxType.ice; // Nuova Scatola Ghiaccio
else if (level >= 15 && chance > 0.78 && chance <= 0.83) box.type = BoxType.multiplier; // Nuova Scatola x2
}
boxes.add(box);
}
}
for (var box in boxes) {
Dot tl = _getOrAddDot(box.x, box.y);
Dot tr = _getOrAddDot(box.x + 1, box.y);
Dot bl = _getOrAddDot(box.x, box.y + 1);
Dot br = _getOrAddDot(box.x + 1, box.y + 1);
box.top = _getOrAddLine(tl, tr);
box.bottom = _getOrAddLine(bl, br);
box.left = _getOrAddLine(tl, bl);
box.right = _getOrAddLine(tr, br);
if (box.type != BoxType.invisible) {
box.top.isPlayable = true; box.bottom.isPlayable = true;
box.left.isPlayable = true; box.right.isPlayable = true;
}
}
}
Dot _getOrAddDot(int x, int y) {
for (var dot in dots) { if (dot.x == x && dot.y == y) return dot; }
var newDot = Dot(x, y);
dots.add(newDot); return newDot;
}
Line _getOrAddLine(Dot a, Dot b) {
for (var line in lines) { if (line.connects(a, b)) return line; }
var newLine = Line(a, b);
lines.add(newLine); return newLine;
}
bool playMove(Line lineToPlay, {Player? forcedPlayer}) {
if (isGameOver) return false;
Player playerMakingMove = forcedPlayer ?? currentPlayer;
Line? actualLine;
for (var l in lines) {
if (l.connects(lineToPlay.p1, lineToPlay.p2)) { actualLine = l; break; }
}
if (actualLine == null || actualLine.owner != Player.none || !actualLine.isPlayable) return false;
// --- LOGICA BLOCCO DI GHIACCIO ---
bool closesIce = false;
for (var box in boxes) {
if (box.type == BoxType.ice && box.owner == Player.none) {
int linesCount = 0;
if (box.top.owner != Player.none || box.top == actualLine) linesCount++;
if (box.bottom.owner != Player.none || box.bottom == actualLine) linesCount++;
if (box.left.owner != Player.none || box.left == actualLine) linesCount++;
if (box.right.owner != Player.none || box.right == actualLine) linesCount++;
if (linesCount == 4) closesIce = true;
}
}
if (closesIce && !actualLine.isIceCracked) {
actualLine.isIceCracked = true; // Si incrina ma non si chiude!
lastMove = actualLine;
if (forcedPlayer == null) currentPlayer = (currentPlayer == Player.red) ? Player.blue : Player.red;
else currentPlayer = (forcedPlayer == Player.red) ? Player.blue : Player.red;
return true; // Mossa valida, ma turno finito.
}
// Mossa normale o secondo colpo al ghiaccio
actualLine.isIceCracked = false;
actualLine.owner = playerMakingMove;
lastMove = actualLine;
bool scoredPoint = false;
bool triggeredSwap = false;
for (var box in boxes) {
if (box.owner == Player.none && box.isClosed()) {
box.owner = playerMakingMove;
scoredPoint = true;
if (box.hiddenJokerOwner != null) box.isJokerRevealed = true;
int points = box.getCalculatedValue(playerMakingMove);
// --- LOGICA MOLTIPLICATORE x2 ---
if (box.type == BoxType.multiplier) {
if (playerMakingMove == Player.red) redHasMultiplier = true;
else blueHasMultiplier = true;
} else if (points != 0) {
// Se la scatola chiusa dà punti e il giocatore ha un x2 attivo...
if (playerMakingMove == Player.red && redHasMultiplier) {
points *= 2;
redHasMultiplier = false; // Si consuma
} else if (playerMakingMove == Player.blue && blueHasMultiplier) {
points *= 2;
blueHasMultiplier = false; // Si consuma
}
}
if (playerMakingMove == Player.red) { scoreRed += points; }
else { scoreBlue += points; }
if (box.type == BoxType.swap && box.hiddenJokerOwner == null) {
triggeredSwap = true;
}
}
if (box.type == BoxType.invisible && !box.isRevealed) {
if (box.top.owner != Player.none && box.bottom.owner != Player.none &&
box.left.owner != Player.none && box.right.owner != Player.none) {
box.isRevealed = true;
}
}
}
if (triggeredSwap) {
int temp = scoreRed; scoreRed = scoreBlue; scoreBlue = temp;
}
if (lines.where((l) => l.isPlayable).every((l) => l.owner != Player.none)) { isGameOver = true; }
if (forcedPlayer == null) {
if (!scoredPoint && !isGameOver) { currentPlayer = (currentPlayer == Player.red) ? Player.blue : Player.red; }
else if (scoredPoint && !isGameOver) { currentPlayer = playerMakingMove; }
} else {
if (!scoredPoint && !isGameOver) { currentPlayer = (forcedPlayer == Player.red) ? Player.blue : Player.red; }
else { currentPlayer = forcedPlayer; }
}
return true;
}
}
// ===========================================================================
// FILE: lib/models/player_info.dart
// ===========================================================================
// ===========================================================================
// FILE: lib/services/audio_service.dart
// ===========================================================================
// ===========================================================================
// FILE: lib/services/audio_service.dart
// ===========================================================================
import 'package:flutter/material.dart';
import 'package:audioplayers/audioplayers.dart';
import '../core/app_colors.dart';
import 'package:shared_preferences/shared_preferences.dart';
class AudioService extends ChangeNotifier {
static final AudioService instance = AudioService._internal();
AudioService._internal();
bool isMuted = false;
final AudioPlayer _sfxPlayer = AudioPlayer();
final AudioPlayer _bgmPlayer = AudioPlayer();
AppThemeType _currentTheme = AppThemeType.doodle;
Future init() async {
final prefs = await SharedPreferences.getInstance();
isMuted = prefs.getBool('isMuted') ?? false;
await _bgmPlayer.setReleaseMode(ReleaseMode.loop);
}
void toggleMute() async {
isMuted = !isMuted;
final prefs = await SharedPreferences.getInstance();
await prefs.setBool('isMuted', isMuted);
if (isMuted) {
// Se abbiamo appena silenziato, FERMA TUTTO immediatamente.
await _bgmPlayer.pause();
await _sfxPlayer.stop();
} else {
// Se riaccendiamo, fai ripartire la canzone
playBgm(_currentTheme);
}
notifyListeners();
}
Future playBgm(AppThemeType theme) async {
_currentTheme = theme;
await _bgmPlayer.stop();
if (isMuted) return;
String audioPath = '';
switch (theme) {
case AppThemeType.cyberpunk:
audioPath = 'audio/bgm/Cyber_Dystopia.mp3';
break;
case AppThemeType.doodle:
audioPath = 'audio/bgm/Quad_Dreams.mp3';
break;
case AppThemeType.wood:
audioPath = 'audio/bgm/Legno_Canopy.mp3';
break;
case AppThemeType.arcade:
audioPath = 'audio/bgm/8-bit_Prowler.mp3';
break;
case AppThemeType.grimorio:
audioPath = 'audio/bgm/Grimorio_Astral.mp3';
break;
case AppThemeType.music:
audioPath = 'audio/bgm/Music_Loop.mp3'; // <-- DEVI INSERIRE QUESTO FILE IN ASSETS
break;
}
if (audioPath.isNotEmpty) {
try {
await _bgmPlayer.play(AssetSource(audioPath), volume: 0.15);
} catch (e) {
debugPrint("Errore riproduzione BGM: $e");
}
}
}
Future stopBgm() async {
await _bgmPlayer.stop();
}
void playLineSfx(AppThemeType theme) async {
if (isMuted) return;
String file = '';
switch (theme) {
case AppThemeType.arcade:
case AppThemeType.music: // Usiamo l'effetto arcade o cyber per la musica
file = 'minimal_line.wav'; break;
case AppThemeType.doodle:
case AppThemeType.wood:
file = 'doodle_line.wav'; break;
case AppThemeType.cyberpunk:
case AppThemeType.grimorio:
file = 'cyber_line.wav'; break;
}
if (file.isNotEmpty) {
try {
await _sfxPlayer.play(AssetSource('audio/sfx/$file'), volume: 1.0);
} catch (e) {
debugPrint("Errore SFX Linea: $file");
}
}
}
void playBoxSfx(AppThemeType theme) async {
if (isMuted) return;
String file = '';
switch (theme) {
case AppThemeType.arcade:
case AppThemeType.music:
file = 'minimal_box.wav'; break;
case AppThemeType.doodle:
case AppThemeType.wood:
file = 'doodle_box.wav'; break;
case AppThemeType.cyberpunk:
case AppThemeType.grimorio:
file = 'cyber_box.wav'; break;
}
if (file.isNotEmpty) {
try {
await _sfxPlayer.play(AssetSource('audio/sfx/$file'), volume: 1.0);
} catch (e) {
debugPrint("Errore SFX Box: $file");
}
}
}
void playBonusSfx() async {
if (isMuted) return;
try {
await _sfxPlayer.play(AssetSource('audio/sfx/bonus.wav'), volume: 1.0);
} catch(e) {}
}
void playBombSfx() async {
if (isMuted) return;
try {
await _sfxPlayer.play(AssetSource('audio/sfx/bomb.wav'), volume: 1.0);
} catch(e) {}
}
}
// ===========================================================================
// FILE: lib/services/firebase_service.dart
// ===========================================================================
// ===========================================================================
// FILE: lib/services/multiplayer_service.dart
// ===========================================================================
// ===========================================================================
// FILE: lib/services/multiplayer_service.dart
// ===========================================================================
import 'dart:math';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:share_plus/share_plus.dart';
class MultiplayerService {
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
final FirebaseAuth _auth = FirebaseAuth.instance;
CollectionReference get _gamesCollection => _firestore.collection('games');
CollectionReference get _invitesCollection => _firestore.collection('invites');
Future createGameRoom(int boardRadius, String hostName, String shapeName, bool isTimeMode, {bool isPublic = true}) async {
String roomCode = _generateRoomCode();
int randomSeed = Random().nextInt(1000000);
await _gamesCollection.doc(roomCode).set({
'status': 'waiting',
'radius': boardRadius,
'createdAt': FieldValue.serverTimestamp(),
'players': ['host'],
'turn': 'host',
'moves': [],
'seed': randomSeed,
'hostName': hostName,
'hostUid': _auth.currentUser?.uid,
'guestName': '',
'shape': shapeName,
'timeMode': isTimeMode,
'isPublic': isPublic,
'p1_reaction': null,
'p2_reaction': null,
'p1_rematch': false,
'p2_rematch': false,
});
return roomCode;
}
Future