Files
WordConnect/Assets/WordConnectGameToolkit/Scripts/Utilities/CrosswordGenerator.cs

1285 lines
56 KiB
C#

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<Tile> tiles = new List<Tile>(); // 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
/// <summary>
/// Generate a crossword using the default configuration.
/// </summary>
/// <param name="words">Words to place in the crossword</param>
/// <param name="grid">Output grid containing the crossword</param>
/// <param name="placements">Output list of word placements</param>
/// <returns>True if the crossword was generated successfully</returns>
public static bool RegenerateCrossword(string[] words, out char[,] grid, out List<WordPlacement> 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<WordPlacement> 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<WordPlacement> 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<WordPlacement>();
// Track previously used seeds to ensure variation if requested
HashSet<int> usedSeeds = new HashSet<int>();
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<WordPlacement>();
// Create a shuffled copy of the words array - completely random order
List<string> 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<WordPlacement> placements)
{
var config = Resources.Load<CrosswordGenerationConfigSO>("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<WordPlacement> placements)
{
// Initialize grid with exact dimensions
grid = new char[columns, rows];
placements = new List<WordPlacement>();
// 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<string> unplacedWords = new List<string>();
// 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<WordPlacement> 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<WordPlacement> placements)
{
// Initialize grid and placements
grid = new char[columns, rows];
placements = new List<WordPlacement>();
// 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<string> longWords = sortedWords.Where(w => w.Length >= 5).ToList();
List<string> 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<WordPlacement> 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<WordPlacement> 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<WordPlacement> 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<WordPlacement> 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<WordPlacement> 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<WordPlacement> 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;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="placements">List of word placements to check</param>
/// <returns>True if problematic overlaps were found that should trigger regeneration</returns>
private static bool CheckForOverlappingWords(List<WordPlacement> 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<WordPlacement> 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;
}
/// <summary>
/// Find all possible placements for a word (both horizontal and vertical intersections)
/// </summary>
private static List<(Vector2Int position, bool isHorizontal)> FindAllPossiblePlacements(string word, char[,] grid, List<WordPlacement> 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;
}
/// <summary>
/// Count the actual number of intersections a word would create if placed at a specific position
/// </summary>
private static int CountActualIntersections(string word, Vector2Int startPos, bool isHorizontal, List<WordPlacement> 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;
}
}
}