using System; using System.Collections.Generic; using System.Linq; using UnityEngine; using WordsToolkit.Scripts.Gameplay; using Random = System.Random; namespace WordsToolkit.Scripts.Utilities { // Enum moved outside the scriptable object so it can be used by both public enum GenerationStrategy { Auto = -1, // Choose automatically based on seed Horizontal = 0, // Strategy 0: Strict horizontal-first approach MixedLength = 1, // Strategy 1: Mix medium and other length words Interleaved = 2, // Strategy 2: Interleave long and short words MaxIntersections = 3 // Strategy 3: Focus on maximizing intersections } // Simple struct for configuration, can be created from the scriptable object [Serializable] public struct CrosswordGenerationConfig { public int columns; public int rows; public int seed; public int maxAttempts; public int minHorizontalRatio; public int maxRows; public int verticalWordMaxLength; public int smallWordMaxLength; public bool forceUniqueLayout; public GenerationStrategy preferredStrategy; public int maxOverlapRetries; // New: maximum additional attempts when overlaps are detected // Constructor with default values public static CrosswordGenerationConfig Default => new CrosswordGenerationConfig { columns = 15, rows = 15, seed = 0, maxAttempts = 20, minHorizontalRatio = 40, // Even more balanced - allows more vertical words maxRows = 5, verticalWordMaxLength = 8, // Allow longer vertical words smallWordMaxLength = 6, // Allow more words to be placed vertically forceUniqueLayout = true, preferredStrategy = GenerationStrategy.Auto, // Doesn't matter anymore since we removed strategies maxOverlapRetries = 5 }; } [Serializable] public class WordPlacement { public string word; public Vector2Int startPosition; public bool isHorizontal; public int wordNumber; public List tiles = new List(); // Store tiles directly in word placement } public static class CrosswordGenerator { private static bool firstWordHorizontal; // Add a new overload that uses default config instead of dependency on ConfigManager /// /// Generate a crossword using the default configuration. /// /// Words to place in the crossword /// Output grid containing the crossword /// Output list of word placements /// True if the crossword was generated successfully public static bool RegenerateCrossword(string[] words, out char[,] grid, out List placements) { return RegenerateCrossword(words, CrosswordGenerationConfig.Default, out grid, out placements); } // Overload that accepts a scriptable object configuration public static bool RegenerateCrossword(string[] words, CrosswordGenerationConfigSO configSO, out char[,] grid, out List placements) { if (configSO == null) { return RegenerateCrossword(words, CrosswordGenerationConfig.Default, out grid, out placements); } return RegenerateCrossword(words, configSO.ToConfig(), out grid, out placements); } // Overload for RegenerateCrossword that accepts a configuration struct public static bool RegenerateCrossword(string[] words, CrosswordGenerationConfig config, out char[,] grid, out List placements) { // Maximum number of attempts before giving up int maxAttempts = config.maxAttempts; int currentAttempt = 0; int baseSeed = config.seed; int overlapRetryCount = 0; // Track how many times we've retried due to overlaps // Use exactly what the user specified int columns = config.columns; int rows = config.rows; // Always initialize with exact dimensions grid = new char[columns, rows]; placements = new List(); // Track previously used seeds to ensure variation if requested HashSet usedSeeds = new HashSet(); while (currentAttempt < maxAttempts) { try { // Use the base seed plus the attempt number to create variation between attempts int attemptSeed = baseSeed + currentAttempt * 1000; Random random = new Random(attemptSeed); // Re-initialize grid and placements each attempt grid = new char[columns, rows]; placements = new List(); // Create a shuffled copy of the words array - completely random order List sortedWords = words.OrderBy(w => random.Next()).ToList(); // Place first word in the middle - vary direction based on strategy and seed if (sortedWords.Count > 0) { string firstWord = sortedWords[0]; // Check which directions are possible bool canPlaceHorizontally = firstWord.Length <= columns; bool canPlaceVertically = firstWord.Length <= rows; if (!canPlaceHorizontally && !canPlaceVertically) { currentAttempt++; continue; } // Decide direction based on strategy and randomness to create more variety bool placeHorizontally; if (!canPlaceHorizontally) { placeHorizontally = false; } else if (!canPlaceVertically) { placeHorizontally = true; } else { // Both directions possible - choose randomly (50/50) placeHorizontally = random.Next(2) == 0; } if (placeHorizontally) { int startX = columns / 2 - firstWord.Length / 2; int startY = rows / 2; startX = Mathf.Clamp(startX, 0, columns - firstWord.Length); startY = Mathf.Clamp(startY, 0, rows - 1); PlaceWord(firstWord, new Vector2Int(startX, startY), true, 1, grid, placements); } else { int startX = columns / 2; int startY = rows / 2 - firstWord.Length / 2; startX = Mathf.Clamp(startX, 0, columns - 1); startY = Mathf.Clamp(startY, 0, rows - firstWord.Length); PlaceWord(firstWord, new Vector2Int(startX, startY), false, 1, grid, placements); } // Track the first word placement for statistics firstWordHorizontal = placeHorizontally; sortedWords.RemoveAt(0); } else { currentAttempt++; continue; } // Try to place remaining words int wordNumber = 2; bool allWordsPlaced = true; // Track word placement statistics int horizontalPlaced = firstWordHorizontal ? 1 : 0; // Track actual first word direction int totalPlaced = 1; // Try to place each remaining word foreach (var word in sortedWords) { // Calculate current horizontal ratio for basic balance int currentHorizontalRatio = horizontalPlaced * 100 / totalPlaced; bool forceHorizontal = currentHorizontalRatio < config.minHorizontalRatio || word.Length > config.verticalWordMaxLength; bool placed = false; // NEW APPROACH: Find all possible placements and randomly choose one var allPossiblePlacements = FindAllPossiblePlacements(word, grid, placements, columns, rows, forceHorizontal); if (allPossiblePlacements.Count > 0) { // Randomly select one of the possible placements var randomPlacement = allPossiblePlacements[random.Next(allPossiblePlacements.Count)]; PlaceWord(word, randomPlacement.position, randomPlacement.isHorizontal, wordNumber, grid, placements); placed = true; } else { // Fallback: try forced placement if no intersections found bool forceDirection = random.Next(2) == 0; // Random direction placed = TryForcedPlacement(word, wordNumber, forceDirection, grid, placements, columns, rows); // If that failed, try the other direction if (!placed) { placed = TryForcedPlacement(word, wordNumber, !forceDirection, grid, placements, columns, rows); } } if (placed) { wordNumber++; totalPlaced++; // Check if last placed word was horizontal if (placements[placements.Count - 1].isHorizontal) { horizontalPlaced++; } } else { allWordsPlaced = false; break; } } // If all words were placed successfully, return true - ignore constraints if (allWordsPlaced && placements.Count == words.Length) { int finalHorizontalRatio = horizontalPlaced * 100 / totalPlaced; // Check for overlapping words and warn about potential issues bool hasProblematicOverlaps = CheckForOverlappingWords(placements); // If we found problematic overlaps and still have retry attempts left, try again if (hasProblematicOverlaps && overlapRetryCount < config.maxOverlapRetries && currentAttempt < maxAttempts - 1) { overlapRetryCount++; currentAttempt++; continue; // Try again with next attempt } // Don't check any constraints - always accept (either no overlaps or out of attempts) CalculateGridBounds(grid, out Vector2Int min, out Vector2Int max); int width = max.x - min.x + 1; int height = max.y - min.y + 1; return true; } // Try again with a different seed currentAttempt++; } catch (Exception ex) { // Log any errors and continue to next attempt Debug.LogError($"Error during crossword generation attempt {currentAttempt}: {ex.Message}"); currentAttempt++; continue; } } return GenerateFallbackLayout(words, columns, rows, out grid, out placements); } // Wrapper for backward compatibility public static bool RegenerateCrossword(string[] words, int seed, out char[,] grid, out List placements) { var config = Resources.Load("Settings/CrosswordConfig"); if (config == null) { // Use default config if none is found var defaultConfig = CrosswordGenerationConfig.Default; defaultConfig.seed = seed; return RegenerateCrossword(words, defaultConfig, out grid, out placements); } // Use the loaded config but override the seed var configStruct = config.ToConfig(); configStruct.seed = seed; return RegenerateCrossword(words, configStruct, out grid, out placements); } // Replace GenerateCrosswordWide with GenerateFallbackLayout that respects dimensions private static bool GenerateFallbackLayout(string[] words, int columns, int rows, out char[,] grid, out List placements) { // Initialize grid with exact dimensions grid = new char[columns, rows]; placements = new List(); // Sort words by length (longest first for better placement) var sortedWords = words.OrderByDescending(w => w.Length).ToList(); // Place first word in the center horizontally if (sortedWords.Count > 0) { string firstWord = sortedWords[0]; int startX = columns / 2 - firstWord.Length / 2; int startY = rows / 2; // Make sure first word fits in the grid if (startX < 0 || startX + firstWord.Length > columns) { // If it doesn't fit horizontally, try vertically startX = columns / 2; startY = rows / 2 - firstWord.Length / 2; if (startY < 0 || startY + firstWord.Length > rows) { // If it still doesn't fit, place it at origin startX = 0; startY = 0; } PlaceWord(firstWord, new Vector2Int(startX, startY), false, 1, grid, placements); } else { PlaceWord(firstWord, new Vector2Int(startX, startY), true, 1, grid, placements); } sortedWords.RemoveAt(0); } else { return false; } // Try to place remaining words int wordNumber = 2; // Track words we couldn't place on first attempt List unplacedWords = new List(); // First pass - try standard placement for each word foreach (var word in sortedWords) { bool placed = false; // First try horizontal placement with intersections placed = TryPlaceHorizontally(word, wordNumber, grid, placements, columns, rows); // If horizontal failed, try vertical if (!placed) { placed = TryPlaceVertically(word, wordNumber, grid, placements, columns, rows); } // If both failed, try forced placement if (!placed) { placed = TryForcedPlacement(word, wordNumber, true, grid, placements, columns, rows); } if (placed) { wordNumber++; } else { // Track this word to try again later unplacedWords.Add(word); } } // Second pass - try aggressive placement for any words we couldn't place foreach (var word in unplacedWords) { bool placed = false; // Try placing horizontally anywhere, even if not intersecting for (int y = 0; y < rows && !placed; y++) { for (int x = 0; x < columns - word.Length + 1 && !placed; x++) { Vector2Int start = new Vector2Int(x, y); // Only check if we don't overlap with other words, don't require intersection if (IsValidPlacementIgnoringIntersection(word, start, true, grid, columns, rows)) { PlaceWord(word, start, true, wordNumber, grid, placements); placed = true; wordNumber++; } } } // If horizontal placement failed everywhere, try vertical if (!placed) { for (int x = 0; x < columns && !placed; x++) { for (int y = 0; y < rows - word.Length + 1 && !placed; y++) { Vector2Int start = new Vector2Int(x, y); // Only check if we don't overlap with other words, don't require intersection if (IsValidPlacementIgnoringIntersection(word, start, false, grid, columns, rows)) { PlaceWord(word, start, false, wordNumber, grid, placements); placed = true; wordNumber++; } } } } // If we still couldn't place the word, it's a serious problem if (!placed) { // ABSOLUTE LAST RESORT - find any legal cell where this can be placed // This should always succeed unless the grid is COMPLETELY full for (int attempt = 0; attempt < 4 && !placed; attempt++) { bool tryHorizontal = (attempt % 2 == 0); bool relaxConstraints = (attempt >= 2); for (int y = 0; y < rows && !placed; y++) { for (int x = 0; x < columns && !placed; x++) { if (tryHorizontal && x + word.Length <= columns) { Vector2Int start = new Vector2Int(x, y); if (relaxConstraints ? CanPlaceWordVeryAggressively(word, start, true, grid, columns, rows) : IsValidPlacementIgnoringIntersection(word, start, true, grid, columns, rows)) { PlaceWord(word, start, true, wordNumber, grid, placements); placed = true; wordNumber++; } } else if (!tryHorizontal && y + word.Length <= rows) { Vector2Int start = new Vector2Int(x, y); if (relaxConstraints ? CanPlaceWordVeryAggressively(word, start, false, grid, columns, rows) : IsValidPlacementIgnoringIntersection(word, start, false, grid, columns, rows)) { PlaceWord(word, start, false, wordNumber, grid, placements); placed = true; wordNumber++; } } } } } if (!placed) { // This should never happen unless the grid is completely full or the word is longer than grid Debug.LogError($"CRITICAL: Failed to place word {word} even with emergency placement! This is likely a bug."); } } } // Check for overlapping words and warn about potential issues bool hasProblematicOverlaps = CheckForOverlappingWords(placements); return placements.Count > 0; } // Helper method to remove a word from the grid (for enforcing constraints) private static void RemoveWord(WordPlacement placement, char[,] grid) { for (int i = 0; i < placement.word.Length; i++) { int x = placement.isHorizontal ? placement.startPosition.x + i : placement.startPosition.x; int y = placement.isHorizontal ? placement.startPosition.y : placement.startPosition.y + i; grid[x, y] = (char)0; } } // Keep for backward compatibility public static bool GenerateCrossword(string[] words, int gridSize, out char[,] grid, out List placements) { // Call the new version with gridSize for both dimensions return GenerateCrossword(words, gridSize, gridSize, out grid, out placements); } // New version that uses columns and rows public static bool GenerateCrossword(string[] words, int columns, int rows, out char[,] grid, out List placements) { // Initialize grid and placements grid = new char[columns, rows]; placements = new List(); // Sort words by length (longest first for better placement) var sortedWords = words.OrderByDescending(w => w.Length).ToList(); // Place first word in the center horizontally if (sortedWords.Count > 0) { string firstWord = sortedWords[0]; int startX = columns / 2 - firstWord.Length / 2; int startY = rows / 2; PlaceWord(firstWord, new Vector2Int(startX, startY), true, 1, grid, placements); sortedWords.RemoveAt(0); } else { return false; } // Try to place remaining words int wordNumber = 2; // First place longer words horizontally as much as possible List longWords = sortedWords.Where(w => w.Length >= 5).ToList(); List shortWords = sortedWords.Where(w => w.Length < 5).ToList(); // Try to place longer words first with horizontal preference foreach (var word in longWords) { if (!TryPlaceWord(word, wordNumber, true, grid, placements, columns, rows)) // true means strong horizontal preference { } else { wordNumber++; } } // Then place shorter words with normal placement rules foreach (var word in shortWords) { if (!TryPlaceWord(word, wordNumber, false, grid, placements, columns, rows)) // false means normal placement rules { } else { wordNumber++; } } return placements.Count > 0; } // Fixed TryPlaceWord to correctly use columns and rows public static bool TryPlaceWord(string word, int wordNumber, bool forceHorizontalPreference, char[,] grid, List placements, int columns, int rows) { // Track horizontal and vertical word counts int horizontalWords = placements.Count(w => w.isHorizontal); int verticalWords = placements.Count - horizontalWords; // Try to intersect with already placed words foreach (var placedWord in placements) { string placed = placedWord.word; Vector2Int startPos = placedWord.startPosition; bool isHorizontal = placedWord.isHorizontal; // Try to place the word intersecting with each letter of the placed word for (int i = 0; i < placed.Length; i++) { char intersectChar = placed[i]; // Check if this character exists in the new word int indexInNewWord = word.IndexOf(intersectChar); while (indexInNewWord >= 0) { Vector2Int intersectionPoint; if (isHorizontal) { intersectionPoint = new Vector2Int(startPos.x + i, startPos.y); } else { intersectionPoint = new Vector2Int(startPos.x, startPos.y + i); } // Try horizontal placement first (regardless of the placed word orientation) Vector2Int horizontalStart = new Vector2Int( intersectionPoint.x - indexInNewWord, intersectionPoint.y ); if (CanPlaceWord(word, horizontalStart, true, grid, columns, rows, placements)) { // Place the word horizontally PlaceWord(word, horizontalStart, true, wordNumber, grid, placements); return true; } // Only try vertical placement if we're maintaining the desired ratio // and not forcing horizontal preference for longer words if (!forceHorizontalPreference && CanAddVerticalWord(horizontalWords, verticalWords, word.Length)) { Vector2Int verticalStart = new Vector2Int( intersectionPoint.x, intersectionPoint.y - indexInNewWord ); if (CanPlaceWord(word, verticalStart, false, grid, columns, rows, placements)) { PlaceWord(word, verticalStart, false, wordNumber, grid, placements); return true; } } // Look for next occurrence of this character indexInNewWord = word.IndexOf(intersectChar, indexInNewWord + 1); } } } // If we couldn't place the word with intersections but we're not forcing horizontal, // try placing it horizontally anywhere valid as a last resort if (!forceHorizontalPreference) { for (int y = 0; y < rows; y++) { for (int x = 0; x < columns - word.Length + 1; x++) { Vector2Int start = new Vector2Int(x, y); if (CanPlaceWord(word, start, true, grid, columns, rows)) { PlaceWord(word, start, true, wordNumber, grid, placements); return true; } } } } return false; } // New helper methods to support horizontal-focused placement private static bool TryPlaceHorizontally(string word, int wordNumber, char[,] grid, List placements, int columns, int rows) { // Simple horizontal placement - just find first valid placement foreach (var placedWord in placements) { string placed = placedWord.word; Vector2Int startPos = placedWord.startPosition; bool isHorizontal = placedWord.isHorizontal; for (int i = 0; i < placed.Length; i++) { char intersectChar = placed[i]; int indexInNewWord = word.IndexOf(intersectChar); while (indexInNewWord >= 0) { Vector2Int intersectionPoint; if (isHorizontal) { intersectionPoint = new Vector2Int(startPos.x + i, startPos.y); } else { intersectionPoint = new Vector2Int(startPos.x, startPos.y + i); } // Try horizontal placement Vector2Int horizontalStart = new Vector2Int( intersectionPoint.x - indexInNewWord, intersectionPoint.y ); if (CanPlaceWord(word, horizontalStart, true, grid, columns, rows)) { PlaceWord(word, horizontalStart, true, wordNumber, grid, placements); return true; } // Look for next occurrence of this character indexInNewWord = word.IndexOf(intersectChar, indexInNewWord + 1); } } } return false; } private static bool TryPlaceVertically(string word, int wordNumber, char[,] grid, List placements, int columns, int rows) { // Simple vertical placement - just find first valid placement foreach (var placedWord in placements) { string placed = placedWord.word; Vector2Int startPos = placedWord.startPosition; bool isHorizontal = placedWord.isHorizontal; for (int i = 0; i < placed.Length; i++) { char intersectChar = placed[i]; int indexInNewWord = word.IndexOf(intersectChar); while (indexInNewWord >= 0) { Vector2Int intersectionPoint; if (isHorizontal) { intersectionPoint = new Vector2Int(startPos.x + i, startPos.y); } else { intersectionPoint = new Vector2Int(startPos.x, startPos.y + i); } // Try vertical placement Vector2Int verticalStart = new Vector2Int( intersectionPoint.x, intersectionPoint.y - indexInNewWord ); if (CanPlaceWord(word, verticalStart, false, grid, columns, rows)) { PlaceWord(word, verticalStart, false, wordNumber, grid, placements); return true; } // Look for next occurrence of this character indexInNewWord = word.IndexOf(intersectChar, indexInNewWord + 1); } } } return false; } private static bool TryForcedPlacement(string word, int wordNumber, bool preferHorizontal, char[,] grid, List placements, int columns, int rows) { // Try placing the word anywhere valid as a last resort // First try horizontal (if preferred) if (preferHorizontal) { for (int y = 0; y < rows; y++) { for (int x = 0; x < columns - word.Length + 1; x++) { Vector2Int start = new Vector2Int(x, y); if (CanPlaceWord(word, start, true, grid, columns, rows)) { PlaceWord(word, start, true, wordNumber, grid, placements); return true; } } } } // If horizontal failed (or not preferred), try vertical for (int x = 0; x < columns; x++) { for (int y = 0; y < rows - word.Length + 1; y++) { Vector2Int start = new Vector2Int(x, y); if (CanPlaceWord(word, start, false, grid, columns, rows)) { PlaceWord(word, start, false, wordNumber, grid, placements); return true; } } } // If we still failed and tried horizontal first, now try vertical places if (preferHorizontal) { // Already tried vertical above return false; } else { // Try horizontal as last resort for (int y = 0; y < rows; y++) { for (int x = 0; x < columns - word.Length + 1; x++) { Vector2Int start = new Vector2Int(x, y); if (CanPlaceWord(word, start, true, grid, columns, rows)) { PlaceWord(word, start, true, wordNumber, grid, placements); return true; } } } } return false; } // Helper method to check if we can add another vertical word while maintaining the desired ratio private static bool CanAddVerticalWord(int horizontalWords, int verticalWords, int wordLength) { // Allow vertical placement if horizontal words are at least 1-2 more than vertical words // For longer words, require an even greater horizontal-to-vertical ratio if (wordLength >= 7) { // Very long words require even more horizontal words return (horizontalWords >= verticalWords + 3); } else if (wordLength >= 5) { // Medium-long words require slightly more horizontal words return (horizontalWords >= verticalWords + 2); } else { // Shorter words use the standard ratio return (horizontalWords >= verticalWords + 1); } } public static bool CanPlaceWord(string word, Vector2Int start, bool isHorizontal, char[,] grid, int columns, int rows, List existingPlacements = null) { // Check if the word would fit within the grid boundaries if (isHorizontal) { if (start.x < 0 || start.x + word.Length > columns || start.y < 0 || start.y >= rows) return false; } else { if (start.x < 0 || start.x >= columns || start.y < 0 || start.y + word.Length > rows) return false; } // CRITICAL FIX: Check for problematic overlaps with existing words if (existingPlacements != null && WouldCreateProblematicOverlap(word, start, isHorizontal, existingPlacements)) { return false; } // Check if the word can be placed (no conflicts with existing words) bool hasIntersection = false; int intersectionCount = 0; for (int i = 0; i < word.Length; i++) { int x = isHorizontal ? start.x + i : start.x; int y = isHorizontal ? start.y : start.y + i; char existing = grid[x, y]; // If this cell already has a character, it must match if (existing != 0) { if (existing != word[i]) return false; hasIntersection = true; intersectionCount++; } else { // Check adjacent cells perpendicular to word direction if (isHorizontal) { // Check above and below for horizontal words if ((y > 0 && grid[x, y - 1] != 0) || (y < rows - 1 && grid[x, y + 1] != 0)) return false; } else { // Check left and right for vertical words if ((x > 0 && grid[x - 1, y] != 0) || (x < columns - 1 && grid[x + 1, y] != 0)) return false; } } // Check if the cell before or after the word is empty (to avoid words running into each other) if (i == 0) { int prevX = isHorizontal ? x - 1 : x; int prevY = isHorizontal ? y : y - 1; if (prevX >= 0 && prevY >= 0 && grid[prevX, prevY] != 0) return false; } if (i == word.Length - 1) { int nextX = isHorizontal ? x + 1 : x; int nextY = isHorizontal ? y : y + 1; if (nextX < columns && nextY < rows && grid[nextX, nextY] != 0) return false; } } // CRITICAL FIX: Prevent words that overlap too much with existing content // If more than 50% of the word already exists in the grid, this is likely // a problematic overlap (like "swipe" vs "wipe") if (hasIntersection && intersectionCount > word.Length / 2) { return false; } // At least one intersection is required (except for the first word) return grid.GetLength(0) == 0 || hasIntersection; } public static void PlaceWord(string word, Vector2Int start, bool isHorizontal, int wordNumber, char[,] grid, List placements) { // Place the word on the grid for (int i = 0; i < word.Length; i++) { int x = isHorizontal ? start.x + i : start.x; int y = isHorizontal ? start.y : start.y + i; grid[x, y] = word[i]; } // Add to placed words list placements.Add(new WordPlacement { word = word, startPosition = start, isHorizontal = isHorizontal, wordNumber = wordNumber }); } // Also update CalculateGridBounds to better handle larger dimensions public static void CalculateGridBounds(char[,] grid, out Vector2Int min, out Vector2Int max) { int columns = grid.GetLength(0); int rows = grid.GetLength(1); // Find the minimum and maximum coordinates used in the grid min = new Vector2Int(columns, rows); max = new Vector2Int(0, 0); bool foundAnyCell = false; for (int y = 0; y < rows; y++) { for (int x = 0; x < columns; x++) { if (grid[x, y] != 0) { min.x = Mathf.Min(min.x, x); min.y = Mathf.Min(min.y, y); max.x = Mathf.Max(max.x, x); max.y = Mathf.Max(max.y, y); foundAnyCell = true; } } } // If no cells are used, default to center if (!foundAnyCell) { min = new Vector2Int(columns/2, rows/2); max = new Vector2Int(columns/2, rows/2); } } // Helper method to check if a word can be placed without requiring intersections private static bool IsValidPlacementIgnoringIntersection(string word, Vector2Int start, bool isHorizontal, char[,] grid, int columns, int rows) { // Check if the word would fit within the grid boundaries if (isHorizontal) { if (start.x < 0 || start.x + word.Length > columns || start.y < 0 || start.y >= rows) return false; } else { if (start.x < 0 || start.x >= columns || start.y < 0 || start.y + word.Length > rows) return false; } // Check if the placement doesn't conflict with existing words for (int i = 0; i < word.Length; i++) { int x = isHorizontal ? start.x + i : start.x; int y = isHorizontal ? start.y : start.y + i; char existing = grid[x, y]; // If this cell already has a character, it must match if (existing != 0 && existing != word[i]) return false; // Check adjacent cells perpendicular to word direction if (isHorizontal) { // Check above and below for horizontal words if ((y > 0 && grid[x, y - 1] != 0) || (y < rows - 1 && grid[x, y + 1] != 0)) return false; } else { // Check left and right for vertical words if ((x > 0 && grid[x - 1, y] != 0) || (x < columns - 1 && grid[x + 1, y] != 0)) return false; } } // Additional check for words not running into each other if (isHorizontal) { // Check if there's a character before the word if (start.x > 0 && grid[start.x - 1, start.y] != 0) return false; // Check if there's a character after the word if (start.x + word.Length < columns && grid[start.x + word.Length, start.y] != 0) return false; } else { // Check if there's a character before the word if (start.y > 0 && grid[start.x, start.y - 1] != 0) return false; // Check if there's a character after the word if (start.y + word.Length < rows && grid[start.x, start.y + word.Length] != 0) return false; } return true; } // Emergency placement - only checks that we don't directly clash with an existing letter private static bool CanPlaceWordVeryAggressively(string word, Vector2Int start, bool isHorizontal, char[,] grid, int columns, int rows) { // Check if the word would fit within the grid boundaries if (isHorizontal) { if (start.x < 0 || start.x + word.Length > columns || start.y < 0 || start.y >= rows) return false; } else { if (start.x < 0 || start.x >= columns || start.y < 0 || start.y + word.Length > rows) return false; } // Only check for direct conflicts, ignore adjacency rules for (int i = 0; i < word.Length; i++) { int x = isHorizontal ? start.x + i : start.x; int y = isHorizontal ? start.y : start.y + i; char existing = grid[x, y]; // If this cell already has a different character, we can't place the word if (existing != 0 && existing != word[i]) return false; } return true; } /// /// Check for overlapping words that start at the same position with different orientations. /// This can cause issues when grid is manually edited and one of the overlapping words gets broken. /// /// List of word placements to check /// True if problematic overlaps were found that should trigger regeneration private static bool CheckForOverlappingWords(List placements) { // Check for words starting at the same position var sameStartGroups = placements .GroupBy(p => p.startPosition) .Where(g => g.Count() > 1) .ToList(); return sameStartGroups.Count > 0; } // Add method to check for problematic same-orientation overlaps public static bool WouldCreateProblematicOverlap(string word, Vector2Int start, bool isHorizontal, List existingPlacements) { foreach (var existing in existingPlacements) { // Only check words with the same orientation if (existing.isHorizontal == isHorizontal) { // Check if the words would overlap in a problematic way if (isHorizontal) { // Both horizontal - check if they're on the same row if (existing.startPosition.y == start.y) { int existingEnd = existing.startPosition.x + existing.word.Length - 1; int newEnd = start.x + word.Length - 1; // Check for overlap bool overlaps = !(existingEnd < start.x || newEnd < existing.startPosition.x); if (overlaps) { // Calculate overlap amount int overlapStart = Math.Max(existing.startPosition.x, start.x); int overlapEnd = Math.Min(existingEnd, newEnd); int overlapLength = overlapEnd - overlapStart + 1; // If overlap is more than 1 character, it's problematic if (overlapLength > 1) { return true; } } } } else { // Both vertical - check if they're on the same column if (existing.startPosition.x == start.x) { int existingEnd = existing.startPosition.y + existing.word.Length - 1; int newEnd = start.y + word.Length - 1; // Check for overlap bool overlaps = !(existingEnd < start.y || newEnd < existing.startPosition.y); if (overlaps) { // Calculate overlap amount int overlapStart = Math.Max(existing.startPosition.y, start.y); int overlapEnd = Math.Min(existingEnd, newEnd); int overlapLength = overlapEnd - overlapStart + 1; // If overlap is more than 1 character, it's problematic if (overlapLength > 1) { return true; } } } } } } return false; } /// /// Find all possible placements for a word (both horizontal and vertical intersections) /// private static List<(Vector2Int position, bool isHorizontal)> FindAllPossiblePlacements(string word, char[,] grid, List placements, int columns, int rows, bool forceHorizontal) { var possiblePlacements = new List<(Vector2Int position, bool isHorizontal)>(); // Find all horizontal placements foreach (var placedWord in placements) { string placed = placedWord.word; Vector2Int startPos = placedWord.startPosition; bool isHorizontal = placedWord.isHorizontal; for (int i = 0; i < placed.Length; i++) { char intersectChar = placed[i]; int indexInNewWord = word.IndexOf(intersectChar); while (indexInNewWord >= 0) { Vector2Int intersectionPoint; if (isHorizontal) { intersectionPoint = new Vector2Int(startPos.x + i, startPos.y); } else { intersectionPoint = new Vector2Int(startPos.x, startPos.y + i); } // Try horizontal placement Vector2Int horizontalStart = new Vector2Int( intersectionPoint.x - indexInNewWord, intersectionPoint.y ); if (CanPlaceWord(word, horizontalStart, true, grid, columns, rows)) { possiblePlacements.Add((horizontalStart, true)); } // Try vertical placement (if not forcing horizontal) if (!forceHorizontal) { Vector2Int verticalStart = new Vector2Int( intersectionPoint.x, intersectionPoint.y - indexInNewWord ); if (CanPlaceWord(word, verticalStart, false, grid, columns, rows)) { possiblePlacements.Add((verticalStart, false)); } } // Look for next occurrence of this character indexInNewWord = word.IndexOf(intersectChar, indexInNewWord + 1); } } } return possiblePlacements; } /// /// Count the actual number of intersections a word would create if placed at a specific position /// private static int CountActualIntersections(string word, Vector2Int startPos, bool isHorizontal, List placements) { int intersectionCount = 0; // Check each character position of the word for (int i = 0; i < word.Length; i++) { Vector2Int charPos; if (isHorizontal) { charPos = new Vector2Int(startPos.x + i, startPos.y); } else { charPos = new Vector2Int(startPos.x, startPos.y + i); } // Check if this position intersects with any existing word foreach (var placement in placements) { // Only count intersections with perpendicular words if (placement.isHorizontal == isHorizontal) continue; Vector2Int placementStart = placement.startPosition; // Check if this character position intersects with the existing word bool intersects = false; if (placement.isHorizontal) { // Existing word is horizontal, check if our vertical word crosses it if (charPos.y == placementStart.y && charPos.x >= placementStart.x && charPos.x < placementStart.x + placement.word.Length) { intersects = true; } } else { // Existing word is vertical, check if our horizontal word crosses it if (charPos.x == placementStart.x && charPos.y >= placementStart.y && charPos.y < placementStart.y + placement.word.Length) { intersects = true; } } if (intersects) { intersectionCount++; break; // Only count one intersection per character position } } } return intersectionCount; } } }