mnemo_cards/mnemo_cards_snake_game/lib/game/game_manager.dart
2025-11-11 02:55:41 +03:00

571 lines
15 KiB
Dart

import 'dart:math';
import 'dart:async';
import 'package:flutter/material.dart';
import '../main.dart';
import '../models/game_items.dart';
class GameManager {
final Random random = Random();
final List<Category> categories;
Category? currentCategory;
final int gameWidth = 10;
final int gameHeight = 14;
final VoidCallback onGameOver;
List<GameItem> gameItems = [];
List<PowerUp> activePowerUps = [];
double baseSpeed = 1.0;
int score = 0;
int combo = 0;
int healthPoints = 5; // Increased initial health points
int spawnCooldown = 0;
String targetWord = ''; // Current word to collect
List<String> collectedLetters = []; // Letters collected so far
// Add snake positions reference
List<Offset> snakePositions = [];
// Add snake direction reference
Direction snakeDirection = Direction.right;
// Independent magnet effect timer
Timer? _magnetTimer;
GameManager({
required this.categories,
required this.onGameOver,
}) {
// Start with the default category
currentCategory = categories.firstWhere((c) => c.name == 'Default');
_selectNewTargetWord();
}
void _selectNewTargetWord() {
if (currentCategory == null) {
currentCategory = categories.firstWhere((c) => c.name == 'Default');
}
targetWord = currentCategory!
.correctAnswers[random.nextInt(currentCategory!.correctAnswers.length)];
collectedLetters = [];
}
String get nextRequiredLetter {
if (targetWord.isEmpty) return '';
return targetWord[collectedLetters.length];
}
void spawnGameItem() {
// Remove expired items first
gameItems.removeWhere((item) => item.isExpired);
// Decrease remaining cycles for all items
for (var item in gameItems) {
item.decrementCycle();
}
if (random.nextDouble() <
gameItems.length.toDouble() / (gameWidth * gameHeight)) {
return;
}
// Spawn category challenge less frequently
if (currentCategory?.name == 'Default' && random.nextDouble() < 0.1) {
return _spawnCategoryChallenge();
}
// Spawn power-ups
if (random.nextDouble() < 0.25) {
return _spawnPowerUp();
}
// Always try to spawn a target
if (random.nextDouble() < 0.3) {
return _spawnTarget();
}
}
// Helper method to check if position is available and safe
bool _isPositionAvailable(Offset position) {
// Check if position is occupied by snake
if (snakePositions.contains(position)) {
return false;
}
// Check if position is occupied by other items
if (gameItems.any((item) => item.position == position)) {
return false;
}
// Don't spawn directly in front of the snake
if (snakePositions.isNotEmpty) {
Offset snakeHead = snakePositions.first;
// Check positions in front of the snake
switch (snakeDirection) {
case Direction.up:
if (position.dx == snakeHead.dx && position.dy < snakeHead.dy) {
return false;
}
case Direction.down:
if (position.dx == snakeHead.dx && position.dy > snakeHead.dy) {
return false;
}
case Direction.left:
if (position.dy == snakeHead.dy && position.dx < snakeHead.dx) {
return false;
}
case Direction.right:
if (position.dy == snakeHead.dy && position.dx > snakeHead.dx) {
return false;
}
}
// Don't spawn too close to snake head
double distance = (position - snakeHead).distance;
if (distance < 2) {
return false;
}
}
return true;
}
Offset _getRandomPosition() {
int attempts = 0;
Offset position;
List<Offset> availablePositions = [];
// Generate all possible positions
for (int x = 1; x <= gameWidth; x++) {
for (int y = 1; y <= gameHeight; y++) {
Offset pos = Offset(x.toDouble(), y.toDouble());
if (_isPositionAvailable(pos)) {
availablePositions.add(pos);
}
}
}
// If no positions available, return invalid position
if (availablePositions.isEmpty) {
return const Offset(-1, -1);
}
// Return random available position
return availablePositions[random.nextInt(availablePositions.length)];
}
void _spawnTarget() {
final position = _getRandomPosition();
if (position.dx < 0) return; // No free position found
final isCorrect = random.nextDouble() < 0.5;
String content;
if (isCorrect) {
content = nextRequiredLetter;
} else {
// Generate more diverse wrong letters
final availableLetters =
List.generate(26, (i) => String.fromCharCode(65 + i))
..remove(nextRequiredLetter); // Remove the correct letter
// Bias towards letters from the target word for more challenge
if (random.nextDouble() < 0.3 && targetWord.isNotEmpty) {
final targetLetters = targetWord.split('')
..remove(nextRequiredLetter); // Remove the next required letter
if (targetLetters.isNotEmpty) {
content = targetLetters[random.nextInt(targetLetters.length)];
} else {
content = availableLetters[random.nextInt(availableLetters.length)];
}
} else {
content = availableLetters[random.nextInt(availableLetters.length)];
}
}
_resetSpawnCooldown();
gameItems.add(GameItem(
position: position,
type: ItemType.letter,
content: content,
isCorrect: isCorrect,
value: currentCategory!.name == 'Default' ? 1.0 : 2.0,
maxCycles: 40,
));
}
void _spawnPowerUp() {
final position = _getRandomPosition();
if (position.dx < 0) return; // No free position found
final powerUpTypes = [
ItemType.slowTime,
ItemType.scoreMultiplier,
ItemType.shield,
ItemType.magnet,
ItemType.health
];
final type = powerUpTypes[random.nextInt(powerUpTypes.length)];
String content;
int maxCycles;
switch (type) {
case ItemType.slowTime:
content = '🐌';
maxCycles = 25;
case ItemType.health:
content = '❤️';
maxCycles = 25;
case ItemType.scoreMultiplier:
content = '2x';
maxCycles = 25;
case ItemType.shield:
content = '🛡️';
maxCycles = 30;
case ItemType.magnet:
content = '🧲';
maxCycles = 35;
default:
content = '?';
maxCycles = 25;
}
_resetSpawnCooldown();
gameItems.add(GameItem(
position: position,
type: type,
content: content,
maxCycles: maxCycles,
));
}
void _resetSpawnCooldown() {
spawnCooldown = 5;
}
void _spawnCategoryChallenge() {
final position = _getRandomPosition();
if (position.dx < 0) return; // No free position found
Category newCategory;
do {
newCategory = categories[random.nextInt(categories.length)];
} while (newCategory.name == currentCategory?.name);
_resetSpawnCooldown();
gameItems.add(GameItem(
position: position,
type: ItemType.categoryChallenge,
content: newCategory.name,
maxCycles: 60,
));
}
bool collectItem(GameItem item) {
gameItems.remove(item);
switch (item.type) {
case ItemType.letter:
case ItemType.word:
return _handleTargetCollection(item);
case ItemType.slowTime:
_activateSlowTime();
return true;
case ItemType.scoreMultiplier:
_activateScoreMultiplier();
return true;
case ItemType.categoryChallenge:
_startCategoryChallenge(item.content);
return true;
case ItemType.health:
_collectHealth();
return true;
case ItemType.shield:
_activateShield();
return true;
case ItemType.magnet:
_activateMagnet();
return true;
}
}
bool _handleTargetCollection(GameItem item) {
// Check if shield is active
bool isShieldActive = activePowerUps
.where((p) => p.type == PowerUpType.shield && p.isActive)
.isNotEmpty;
// Check if this is the next required letter
if (item.content == nextRequiredLetter) {
collectedLetters.add(item.content);
combo++;
score += ((combo * item.value) * _getCurrentMultiplier()).round();
// Check if word is completed
if (collectedLetters.length == targetWord.length) {
score +=
(50 * _getCurrentMultiplier()).round(); // Bonus for completing word
_selectNewTargetWord();
}
return true;
} else {
// Wrong letter collected
combo = 0;
if (!isShieldActive) {
healthPoints--;
}
return healthPoints > 0;
}
}
void _activateSlowTime() {
final slowPowerUp = PowerUp(
type: PowerUpType.slowTime,
duration: const Duration(seconds: 5),
value: 0.5,
);
slowPowerUp.activate();
activePowerUps.add(slowPowerUp);
}
void _activateScoreMultiplier() {
final multiplierPowerUp = PowerUp(
type: PowerUpType.scoreMultiplier,
duration: const Duration(seconds: 10),
value: 2.0,
);
multiplierPowerUp.activate();
activePowerUps.add(multiplierPowerUp);
}
void _startCategoryChallenge(String categoryName) {
currentCategory = categories.firstWhere((c) => c.name == categoryName);
_selectNewTargetWord();
Future.delayed(const Duration(seconds: 30), () {
currentCategory = categories.firstWhere((c) => c.name == 'Default');
_selectNewTargetWord();
});
}
double getCurrentSpeed() {
double speed = baseSpeed;
// Apply slow time power-up if active
final slowPowerUp = activePowerUps
.where((p) => p.type == PowerUpType.slowTime && p.isActive)
.firstOrNull;
if (slowPowerUp != null) {
speed *= slowPowerUp.value;
}
// Remove expired power-ups
activePowerUps.removeWhere((p) => !p.isActive);
return speed;
}
double _getCurrentMultiplier() {
double multiplier = 1.0;
final multiplierPowerUp = activePowerUps
.where((p) => p.type == PowerUpType.scoreMultiplier && p.isActive)
.firstOrNull;
if (multiplierPowerUp != null) {
multiplier *= multiplierPowerUp.value;
}
return multiplier;
}
void updateBaseSpeed() {
baseSpeed = 1.0 + (score / 1000); // Gradually increase speed with score
}
void _collectHealth() {
healthPoints =
(healthPoints + 1).clamp(0, 8); // Increased max health points
}
void _activateShield() {
final shieldPowerUp = PowerUp(
type: PowerUpType.shield,
duration: const Duration(seconds: 8),
value: 1.0,
);
shieldPowerUp.activate();
activePowerUps.add(shieldPowerUp);
}
void _activateMagnet() {
final magnetPowerUp = PowerUp(
type: PowerUpType.magnet,
duration: const Duration(seconds: 20),
value: 10.0,
);
magnetPowerUp.activate();
activePowerUps.add(magnetPowerUp);
// Cancel existing timer if any
_magnetTimer?.cancel();
// Start continuous attraction updates
_magnetTimer = Timer.periodic(const Duration(milliseconds: 16), (timer) {
if (!magnetPowerUp.isActive) {
timer.cancel();
_magnetTimer = null;
return;
}
_attractNearbyItems();
});
}
// Method to check if an item can be affected by magnet
bool _isAttractableItem(ItemType type) {
return type == ItemType.slowTime ||
type == ItemType.scoreMultiplier ||
type == ItemType.shield ||
type == ItemType.magnet ||
type == ItemType.health;
}
// Method to update snake positions and direction
void updateSnakePositions(List<Offset> positions, Direction direction) {
snakePositions = List.from(positions);
snakeDirection = direction;
}
void _attractNearbyItems() {
if (snakePositions.isEmpty) return;
final magnetPowerUp = activePowerUps
.where((p) => p.type == PowerUpType.magnet && p.isActive)
.firstOrNull;
if (magnetPowerUp == null) return;
final head = snakePositions.first;
final attractionRadius = magnetPowerUp.value;
List<GameItem> updatedItems = [];
List<GameItem> itemsToRemove = [];
for (var item in gameItems) {
if (!_isAttractableItem(item.type)) {
updatedItems.add(item);
continue;
}
// Calculate shortest path considering screen wrapping
double dx = item.position.dx - head.dx;
double dy = item.position.dy - head.dy;
// Handle screen wrapping for distance calculation
if (dx > gameWidth / 2) dx -= gameWidth;
if (dx < -gameWidth / 2) dx += gameWidth;
if (dy > gameHeight / 2) dy -= gameHeight;
if (dy < -gameHeight / 2) dy += gameHeight;
double distance = sqrt(dx * dx + dy * dy);
// Check for direct collision with snake head
if (distance < 0.5) {
itemsToRemove.add(item);
bool success = collectItem(item);
if (!success) {
onGameOver();
}
continue;
}
if (distance <= attractionRadius) {
// Enhanced smooth movement with easing
double attractionStrength = _easeInOutQuad(1.0 - (distance / attractionRadius));
// Smoother base movement speed
double moveSpeed = 0.15;
double moveX = dx / distance * moveSpeed * attractionStrength;
double moveY = dy / distance * moveSpeed * attractionStrength;
// Calculate new position
double newX = item.position.dx - moveX;
double newY = item.position.dy - moveY;
// Handle screen wrapping properly
if (newX < 0) {
newX = gameWidth - 0.01; // Slightly inside the right edge
} else if (newX >= gameWidth) {
newX = 0.01; // Slightly inside the left edge
}
if (newY < 0) {
newY = gameHeight - 0.01; // Slightly inside the bottom edge
} else if (newY >= gameHeight) {
newY = 0.01; // Slightly inside the top edge
}
updatedItems.add(
GameItem(
position: Offset(newX, newY),
type: item.type,
content: item.content,
isCorrect: item.isCorrect,
value: item.value,
maxCycles: item.maxCycles,
remainingCycles: item.remainingCycles,
),
);
} else {
updatedItems.add(item);
}
}
// Remove collected items and update positions
gameItems.removeWhere((item) => itemsToRemove.contains(item));
gameItems = updatedItems;
}
// Smooth easing function for magnet effect
double _easeInOutQuad(double t) {
return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
}
@override
void dispose() {
_magnetTimer?.cancel();
}
void resetGame() {
// Clear all game items
gameItems.clear();
// Reset power-ups and cancel magnet timer
activePowerUps.clear();
_magnetTimer?.cancel();
_magnetTimer = null;
// Reset score and combo
score = 0;
combo = 0;
// Reset speed
baseSpeed = 1.0;
// Reset health points to maximum
healthPoints = 8;
// Reset spawn cooldown
spawnCooldown = 0;
// Reset category to default and generate new target word
currentCategory = categories.firstWhere((c) => c.name == 'Default');
_selectNewTargetWord();
}
}