Fix server binding and reduce verbose logging for production deployment
Browse files- Bind server to 0.0.0.0 in production for external connections (fixes HF Spaces connectivity)
- Add debug logging for working directory and app location in production
- Remove verbose crossword generation logs, keep only error messages
- Streamline backtracking placement logging for better performance
Signed-off-by: Vimal Kumar <vimal78@gmail.com>
crossword-app/backend/src/app.js
CHANGED
|
@@ -115,9 +115,12 @@ app.use((error, req, res, next) => {
|
|
| 115 |
});
|
| 116 |
|
| 117 |
if (require.main === module) {
|
| 118 |
-
|
| 119 |
-
|
|
|
|
| 120 |
console.log(`Environment: ${process.env.NODE_ENV || 'development'}`);
|
|
|
|
|
|
|
| 121 |
if (process.env.NODE_ENV === 'production') {
|
| 122 |
console.log(`Serving React app from /public directory`);
|
| 123 |
console.log(`CORS enabled for same origin`);
|
|
|
|
| 115 |
});
|
| 116 |
|
| 117 |
if (require.main === module) {
|
| 118 |
+
const HOST = process.env.NODE_ENV === 'production' ? '0.0.0.0' : 'localhost';
|
| 119 |
+
app.listen(PORT, HOST, () => {
|
| 120 |
+
console.log(`Server running on ${HOST}:${PORT}`);
|
| 121 |
console.log(`Environment: ${process.env.NODE_ENV || 'development'}`);
|
| 122 |
+
console.log(`Working directory: ${process.cwd()}`);
|
| 123 |
+
console.log(`App file location: ${__dirname}`);
|
| 124 |
if (process.env.NODE_ENV === 'production') {
|
| 125 |
console.log(`Serving React app from /public directory`);
|
| 126 |
console.log(`CORS enabled for same origin`);
|
crossword-app/backend/src/services/crosswordGenerator.js
CHANGED
|
@@ -8,30 +8,23 @@ class CrosswordGenerator {
|
|
| 8 |
}
|
| 9 |
|
| 10 |
async generatePuzzle(topics, difficulty = 'medium') {
|
| 11 |
-
console.log(`🎯 Starting puzzle generation - Topics: ${JSON.stringify(topics)}, Difficulty: ${difficulty}`);
|
| 12 |
-
|
| 13 |
try {
|
| 14 |
const words = await this.selectWords(topics, difficulty);
|
| 15 |
-
console.log(`📚 Selected ${words.length} words for puzzle:`, words.map(w => w.word).join(', '));
|
| 16 |
|
| 17 |
if (words.length < this.minWords) {
|
| 18 |
-
console.
|
| 19 |
throw new Error('Not enough words available for selected topics');
|
| 20 |
}
|
| 21 |
|
| 22 |
-
console.log(`🔧 Starting grid creation with ${words.length} words`);
|
| 23 |
const gridResult = this.createGrid(words);
|
| 24 |
|
| 25 |
if (!gridResult) {
|
| 26 |
-
console.
|
| 27 |
throw new Error('Could not place words in grid');
|
| 28 |
}
|
| 29 |
-
|
| 30 |
-
console.log(`✅ Grid created successfully - Size: ${gridResult.size}x${gridResult.size}, Words placed: ${gridResult.placedWords.length}`);
|
| 31 |
|
| 32 |
const clues = this.generateClues(words, gridResult.placedWords);
|
| 33 |
|
| 34 |
-
console.log(`🎉 Puzzle generation completed successfully!`);
|
| 35 |
return {
|
| 36 |
grid: gridResult.grid,
|
| 37 |
clues: clues,
|
|
@@ -122,48 +115,40 @@ class CrosswordGenerator {
|
|
| 122 |
|
| 123 |
createGrid(words) {
|
| 124 |
if (!words || words.length === 0) {
|
| 125 |
-
console.
|
| 126 |
return null;
|
| 127 |
}
|
| 128 |
|
| 129 |
const wordList = words.map(w => w.word.toUpperCase()).sort((a, b) => b.length - a.length);
|
| 130 |
const size = this.calculateGridSize(wordList);
|
| 131 |
-
console.log(`📐 Calculated grid size: ${size}x${size} for words:`, wordList.join(', '));
|
| 132 |
|
| 133 |
// Try with different grid sizes and word counts
|
| 134 |
for (let attempt = 0; attempt < 3; attempt++) {
|
| 135 |
const currentSize = size + attempt;
|
| 136 |
-
console.log(`🔍 Attempt ${attempt + 1}: Trying grid size ${currentSize}x${currentSize} with ${wordList.length} words`);
|
| 137 |
|
| 138 |
// First try with all words
|
| 139 |
let result = this.placeWordsInGrid(wordList, currentSize);
|
| 140 |
if (result) {
|
| 141 |
-
console.log(`✅ Success with all ${wordList.length} words on size ${currentSize}x${currentSize}`);
|
| 142 |
return { grid: result.grid, size: currentSize, placedWords: result.placedWords };
|
| 143 |
}
|
| 144 |
|
| 145 |
// If that fails, try with 1-2 fewer words (but not too aggressive)
|
| 146 |
if (wordList.length > 7) {
|
| 147 |
const reducedWords = wordList.slice(0, wordList.length - 1);
|
| 148 |
-
console.log(`🔄 Retrying with ${reducedWords.length} words:`, reducedWords.join(', '));
|
| 149 |
result = this.placeWordsInGrid(reducedWords, currentSize);
|
| 150 |
if (result) {
|
| 151 |
-
console.log(`✅ Success with ${reducedWords.length} words on size ${currentSize}x${currentSize}`);
|
| 152 |
return { grid: result.grid, size: currentSize, placedWords: result.placedWords };
|
| 153 |
}
|
| 154 |
}
|
| 155 |
}
|
| 156 |
|
| 157 |
// Only as last resort, try with minimum words
|
| 158 |
-
console.log(`🚨 Fallback: Trying with minimum 6 words`);
|
| 159 |
for (let attempt = 0; attempt < 2; attempt++) {
|
| 160 |
const currentSize = size + attempt;
|
| 161 |
if (wordList.length >= 6) {
|
| 162 |
const minWords = wordList.slice(0, 6);
|
| 163 |
-
console.log(`🔄 Last resort attempt ${attempt + 1}: ${minWords.length} words on ${currentSize}x${currentSize}:`, minWords.join(', '));
|
| 164 |
const result = this.placeWordsInGrid(minWords, currentSize);
|
| 165 |
if (result) {
|
| 166 |
-
console.log(`✅ Success with minimum ${minWords.length} words on size ${currentSize}x${currentSize}`);
|
| 167 |
return { grid: result.grid, size: currentSize, placedWords: result.placedWords };
|
| 168 |
}
|
| 169 |
}
|
|
@@ -171,14 +156,13 @@ class CrosswordGenerator {
|
|
| 171 |
|
| 172 |
// Absolute last resort: simple cross pattern with just 2 words
|
| 173 |
if (wordList.length >= 2) {
|
| 174 |
-
console.log(`🆘 Emergency fallback: Simple 2-word cross pattern`);
|
| 175 |
const result = this.createSimpleCross(wordList.slice(0, 2));
|
| 176 |
if (result) {
|
| 177 |
return result;
|
| 178 |
}
|
| 179 |
}
|
| 180 |
|
| 181 |
-
console.
|
| 182 |
return null;
|
| 183 |
}
|
| 184 |
|
|
@@ -242,53 +226,40 @@ class CrosswordGenerator {
|
|
| 242 |
}
|
| 243 |
|
| 244 |
placeWordsInGrid(words, size) {
|
| 245 |
-
console.log(`🎲 Creating ${size}x${size} grid for words:`, words.join(', '));
|
| 246 |
const grid = Array(size).fill().map(() => Array(size).fill('.'));
|
| 247 |
const placedWords = [];
|
| 248 |
|
| 249 |
-
console.log(`🔄 Starting backtrack placement...`);
|
| 250 |
const startTime = Date.now();
|
| 251 |
const timeout = 5000; // Reduced to 5 second timeout
|
| 252 |
|
| 253 |
if (this.backtrackPlacement(grid, words, 0, placedWords, startTime, timeout)) {
|
| 254 |
-
console.log(`✅ Backtracking successful! Placed ${placedWords.length} words`);
|
| 255 |
const trimmed = this.trimGrid(grid, placedWords);
|
| 256 |
-
console.log(`📏 Grid trimmed to ${trimmed.grid.length}x${trimmed.grid[0].length}`);
|
| 257 |
return { grid: trimmed.grid, placedWords: trimmed.placedWords };
|
| 258 |
}
|
| 259 |
|
| 260 |
-
console.log(`❌ Backtracking failed for ${size}x${size} grid (timeout or no solution)`);
|
| 261 |
return null;
|
| 262 |
}
|
| 263 |
|
| 264 |
backtrackPlacement(grid, words, wordIndex, placedWords, startTime = Date.now(), timeout = 5000, callCount = 0) {
|
| 265 |
// Check timeout more frequently
|
| 266 |
if (callCount % 50 === 0 && Date.now() - startTime > timeout) {
|
| 267 |
-
console.
|
| 268 |
return false;
|
| 269 |
}
|
| 270 |
|
| 271 |
if (wordIndex >= words.length) {
|
| 272 |
-
console.log(`🎉 All ${words.length} words placed successfully!`);
|
| 273 |
return true;
|
| 274 |
}
|
| 275 |
|
| 276 |
const word = words[wordIndex];
|
| 277 |
const size = grid.length;
|
| 278 |
-
|
| 279 |
-
// Limit excessive logging for performance
|
| 280 |
-
if (callCount % 50 === 0 || wordIndex <= 2) {
|
| 281 |
-
console.log(`🔤 Placing word ${wordIndex + 1}/${words.length}: "${word}" (${word.length} chars) [call ${callCount}]`);
|
| 282 |
-
}
|
| 283 |
|
| 284 |
// For the first word, place the longest word in the center horizontally
|
| 285 |
if (wordIndex === 0) {
|
| 286 |
const centerRow = Math.floor(size / 2);
|
| 287 |
const centerCol = Math.floor((size - word.length) / 2);
|
| 288 |
-
console.log(`📍 First word "${word}": trying center position (${centerRow}, ${centerCol}) horizontal`);
|
| 289 |
|
| 290 |
if (this.canPlaceWord(grid, word, centerRow, centerCol, 'horizontal')) {
|
| 291 |
-
console.log(`✅ First word "${word}" placed at center`);
|
| 292 |
const originalState = this.placeWord(grid, word, centerRow, centerCol, 'horizontal');
|
| 293 |
placedWords.push({ word, row: centerRow, col: centerCol, direction: 'horizontal', number: 1 });
|
| 294 |
|
|
@@ -296,11 +267,8 @@ class CrosswordGenerator {
|
|
| 296 |
return true;
|
| 297 |
}
|
| 298 |
|
| 299 |
-
console.log(`🔙 Backtracking from first word "${word}"`);
|
| 300 |
this.removeWord(grid, originalState);
|
| 301 |
placedWords.pop();
|
| 302 |
-
} else {
|
| 303 |
-
console.log(`❌ Cannot place first word "${word}" at center`);
|
| 304 |
}
|
| 305 |
return false;
|
| 306 |
}
|
|
@@ -308,7 +276,6 @@ class CrosswordGenerator {
|
|
| 308 |
// For the second word, try to create a central cross with smart selection
|
| 309 |
if (wordIndex === 1) {
|
| 310 |
const firstWord = placedWords[0];
|
| 311 |
-
console.log(`🔍 Smart second word selection - trying to find word that intersects with "${firstWord.word}"`);
|
| 312 |
|
| 313 |
// Try all remaining words to find one that can intersect
|
| 314 |
const remainingWords = words.slice(1); // All words except the first
|
|
@@ -328,41 +295,31 @@ class CrosswordGenerator {
|
|
| 328 |
|
| 329 |
// Sort by intersection count (more intersections = better)
|
| 330 |
wordsWithIntersections.sort((a, b) => b.intersectionCount - a.intersectionCount);
|
| 331 |
-
console.log(`📊 Found ${wordsWithIntersections.length} words with intersections:`,
|
| 332 |
-
wordsWithIntersections.map(w => `${w.word}(${w.intersectionCount})`).join(', '));
|
| 333 |
|
| 334 |
// Try each word with intersections
|
| 335 |
for (const candidateInfo of wordsWithIntersections) {
|
| 336 |
const candidateWord = candidateInfo.word;
|
| 337 |
-
console.log(`🔍 Trying second word "${candidateWord}" with ${candidateInfo.intersectionCount} intersections`);
|
| 338 |
|
| 339 |
for (const intersection of candidateInfo.intersections) {
|
| 340 |
const placement = this.calculateIntersectionPlacement(candidateWord, intersection.wordPos, firstWord, intersection.placedPos);
|
| 341 |
-
console.log(`🎯 Trying intersection at word[${intersection.wordPos}] = placed[${intersection.placedPos}], placement:`, placement);
|
| 342 |
|
| 343 |
if (placement && this.canPlaceWord(grid, candidateWord, placement.row, placement.col, placement.direction)) {
|
| 344 |
-
console.log(`✅ Valid placement found for "${candidateWord}"`);
|
| 345 |
const originalState = this.placeWord(grid, candidateWord, placement.row, placement.col, placement.direction);
|
| 346 |
placedWords.push({ word: candidateWord, ...placement, number: 2 });
|
| 347 |
|
| 348 |
// Create new word order with the selected second word
|
| 349 |
const newWordOrder = [words[0], candidateWord, ...words.slice(1).filter(w => w !== candidateWord)];
|
| 350 |
-
console.log(`🔄 New word order:`, newWordOrder.join(', '));
|
| 351 |
|
| 352 |
if (this.backtrackPlacement(grid, newWordOrder, wordIndex + 1, placedWords, startTime, timeout, callCount + 1)) {
|
| 353 |
return true;
|
| 354 |
}
|
| 355 |
|
| 356 |
-
console.log(`🔙 Backtracking from second word "${candidateWord}"`);
|
| 357 |
this.removeWord(grid, originalState);
|
| 358 |
placedWords.pop();
|
| 359 |
-
} else {
|
| 360 |
-
console.log(`❌ Invalid placement for "${candidateWord}" at`, placement);
|
| 361 |
}
|
| 362 |
}
|
| 363 |
}
|
| 364 |
|
| 365 |
-
console.log(`❌ No valid second word found with intersections`);
|
| 366 |
return false;
|
| 367 |
}
|
| 368 |
|
|
@@ -901,15 +858,13 @@ class CrosswordGenerator {
|
|
| 901 |
}
|
| 902 |
|
| 903 |
createSimpleCross(words) {
|
| 904 |
-
console.log(`🛠️ Creating simple cross with: ${words.join(', ')}`);
|
| 905 |
-
|
| 906 |
// Find the best intersection between the two words
|
| 907 |
const word1 = words[0];
|
| 908 |
const word2 = words[1];
|
| 909 |
const intersections = this.findWordIntersections(word1, word2);
|
| 910 |
|
| 911 |
if (intersections.length === 0) {
|
| 912 |
-
console.
|
| 913 |
return null;
|
| 914 |
}
|
| 915 |
|
|
@@ -940,7 +895,6 @@ class CrosswordGenerator {
|
|
| 940 |
];
|
| 941 |
|
| 942 |
const trimmed = this.trimGrid(grid, placedWords);
|
| 943 |
-
console.log(`✅ Simple cross created successfully`);
|
| 944 |
return { grid: trimmed.grid, size: trimmed.grid.length, placedWords: trimmed.placedWords };
|
| 945 |
}
|
| 946 |
|
|
|
|
| 8 |
}
|
| 9 |
|
| 10 |
async generatePuzzle(topics, difficulty = 'medium') {
|
|
|
|
|
|
|
| 11 |
try {
|
| 12 |
const words = await this.selectWords(topics, difficulty);
|
|
|
|
| 13 |
|
| 14 |
if (words.length < this.minWords) {
|
| 15 |
+
console.error(`❌ Not enough words: ${words.length} < ${this.minWords}`);
|
| 16 |
throw new Error('Not enough words available for selected topics');
|
| 17 |
}
|
| 18 |
|
|
|
|
| 19 |
const gridResult = this.createGrid(words);
|
| 20 |
|
| 21 |
if (!gridResult) {
|
| 22 |
+
console.error(`❌ Grid creation failed - could not place words`);
|
| 23 |
throw new Error('Could not place words in grid');
|
| 24 |
}
|
|
|
|
|
|
|
| 25 |
|
| 26 |
const clues = this.generateClues(words, gridResult.placedWords);
|
| 27 |
|
|
|
|
| 28 |
return {
|
| 29 |
grid: gridResult.grid,
|
| 30 |
clues: clues,
|
|
|
|
| 115 |
|
| 116 |
createGrid(words) {
|
| 117 |
if (!words || words.length === 0) {
|
| 118 |
+
console.error(`❌ No words provided to createGrid`);
|
| 119 |
return null;
|
| 120 |
}
|
| 121 |
|
| 122 |
const wordList = words.map(w => w.word.toUpperCase()).sort((a, b) => b.length - a.length);
|
| 123 |
const size = this.calculateGridSize(wordList);
|
|
|
|
| 124 |
|
| 125 |
// Try with different grid sizes and word counts
|
| 126 |
for (let attempt = 0; attempt < 3; attempt++) {
|
| 127 |
const currentSize = size + attempt;
|
|
|
|
| 128 |
|
| 129 |
// First try with all words
|
| 130 |
let result = this.placeWordsInGrid(wordList, currentSize);
|
| 131 |
if (result) {
|
|
|
|
| 132 |
return { grid: result.grid, size: currentSize, placedWords: result.placedWords };
|
| 133 |
}
|
| 134 |
|
| 135 |
// If that fails, try with 1-2 fewer words (but not too aggressive)
|
| 136 |
if (wordList.length > 7) {
|
| 137 |
const reducedWords = wordList.slice(0, wordList.length - 1);
|
|
|
|
| 138 |
result = this.placeWordsInGrid(reducedWords, currentSize);
|
| 139 |
if (result) {
|
|
|
|
| 140 |
return { grid: result.grid, size: currentSize, placedWords: result.placedWords };
|
| 141 |
}
|
| 142 |
}
|
| 143 |
}
|
| 144 |
|
| 145 |
// Only as last resort, try with minimum words
|
|
|
|
| 146 |
for (let attempt = 0; attempt < 2; attempt++) {
|
| 147 |
const currentSize = size + attempt;
|
| 148 |
if (wordList.length >= 6) {
|
| 149 |
const minWords = wordList.slice(0, 6);
|
|
|
|
| 150 |
const result = this.placeWordsInGrid(minWords, currentSize);
|
| 151 |
if (result) {
|
|
|
|
| 152 |
return { grid: result.grid, size: currentSize, placedWords: result.placedWords };
|
| 153 |
}
|
| 154 |
}
|
|
|
|
| 156 |
|
| 157 |
// Absolute last resort: simple cross pattern with just 2 words
|
| 158 |
if (wordList.length >= 2) {
|
|
|
|
| 159 |
const result = this.createSimpleCross(wordList.slice(0, 2));
|
| 160 |
if (result) {
|
| 161 |
return result;
|
| 162 |
}
|
| 163 |
}
|
| 164 |
|
| 165 |
+
console.error(`❌ All grid creation attempts failed`);
|
| 166 |
return null;
|
| 167 |
}
|
| 168 |
|
|
|
|
| 226 |
}
|
| 227 |
|
| 228 |
placeWordsInGrid(words, size) {
|
|
|
|
| 229 |
const grid = Array(size).fill().map(() => Array(size).fill('.'));
|
| 230 |
const placedWords = [];
|
| 231 |
|
|
|
|
| 232 |
const startTime = Date.now();
|
| 233 |
const timeout = 5000; // Reduced to 5 second timeout
|
| 234 |
|
| 235 |
if (this.backtrackPlacement(grid, words, 0, placedWords, startTime, timeout)) {
|
|
|
|
| 236 |
const trimmed = this.trimGrid(grid, placedWords);
|
|
|
|
| 237 |
return { grid: trimmed.grid, placedWords: trimmed.placedWords };
|
| 238 |
}
|
| 239 |
|
|
|
|
| 240 |
return null;
|
| 241 |
}
|
| 242 |
|
| 243 |
backtrackPlacement(grid, words, wordIndex, placedWords, startTime = Date.now(), timeout = 5000, callCount = 0) {
|
| 244 |
// Check timeout more frequently
|
| 245 |
if (callCount % 50 === 0 && Date.now() - startTime > timeout) {
|
| 246 |
+
console.error(`⏰ Placement timeout reached after ${Date.now() - startTime}ms`);
|
| 247 |
return false;
|
| 248 |
}
|
| 249 |
|
| 250 |
if (wordIndex >= words.length) {
|
|
|
|
| 251 |
return true;
|
| 252 |
}
|
| 253 |
|
| 254 |
const word = words[wordIndex];
|
| 255 |
const size = grid.length;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 256 |
|
| 257 |
// For the first word, place the longest word in the center horizontally
|
| 258 |
if (wordIndex === 0) {
|
| 259 |
const centerRow = Math.floor(size / 2);
|
| 260 |
const centerCol = Math.floor((size - word.length) / 2);
|
|
|
|
| 261 |
|
| 262 |
if (this.canPlaceWord(grid, word, centerRow, centerCol, 'horizontal')) {
|
|
|
|
| 263 |
const originalState = this.placeWord(grid, word, centerRow, centerCol, 'horizontal');
|
| 264 |
placedWords.push({ word, row: centerRow, col: centerCol, direction: 'horizontal', number: 1 });
|
| 265 |
|
|
|
|
| 267 |
return true;
|
| 268 |
}
|
| 269 |
|
|
|
|
| 270 |
this.removeWord(grid, originalState);
|
| 271 |
placedWords.pop();
|
|
|
|
|
|
|
| 272 |
}
|
| 273 |
return false;
|
| 274 |
}
|
|
|
|
| 276 |
// For the second word, try to create a central cross with smart selection
|
| 277 |
if (wordIndex === 1) {
|
| 278 |
const firstWord = placedWords[0];
|
|
|
|
| 279 |
|
| 280 |
// Try all remaining words to find one that can intersect
|
| 281 |
const remainingWords = words.slice(1); // All words except the first
|
|
|
|
| 295 |
|
| 296 |
// Sort by intersection count (more intersections = better)
|
| 297 |
wordsWithIntersections.sort((a, b) => b.intersectionCount - a.intersectionCount);
|
|
|
|
|
|
|
| 298 |
|
| 299 |
// Try each word with intersections
|
| 300 |
for (const candidateInfo of wordsWithIntersections) {
|
| 301 |
const candidateWord = candidateInfo.word;
|
|
|
|
| 302 |
|
| 303 |
for (const intersection of candidateInfo.intersections) {
|
| 304 |
const placement = this.calculateIntersectionPlacement(candidateWord, intersection.wordPos, firstWord, intersection.placedPos);
|
|
|
|
| 305 |
|
| 306 |
if (placement && this.canPlaceWord(grid, candidateWord, placement.row, placement.col, placement.direction)) {
|
|
|
|
| 307 |
const originalState = this.placeWord(grid, candidateWord, placement.row, placement.col, placement.direction);
|
| 308 |
placedWords.push({ word: candidateWord, ...placement, number: 2 });
|
| 309 |
|
| 310 |
// Create new word order with the selected second word
|
| 311 |
const newWordOrder = [words[0], candidateWord, ...words.slice(1).filter(w => w !== candidateWord)];
|
|
|
|
| 312 |
|
| 313 |
if (this.backtrackPlacement(grid, newWordOrder, wordIndex + 1, placedWords, startTime, timeout, callCount + 1)) {
|
| 314 |
return true;
|
| 315 |
}
|
| 316 |
|
|
|
|
| 317 |
this.removeWord(grid, originalState);
|
| 318 |
placedWords.pop();
|
|
|
|
|
|
|
| 319 |
}
|
| 320 |
}
|
| 321 |
}
|
| 322 |
|
|
|
|
| 323 |
return false;
|
| 324 |
}
|
| 325 |
|
|
|
|
| 858 |
}
|
| 859 |
|
| 860 |
createSimpleCross(words) {
|
|
|
|
|
|
|
| 861 |
// Find the best intersection between the two words
|
| 862 |
const word1 = words[0];
|
| 863 |
const word2 = words[1];
|
| 864 |
const intersections = this.findWordIntersections(word1, word2);
|
| 865 |
|
| 866 |
if (intersections.length === 0) {
|
| 867 |
+
console.error(`❌ No intersections found between ${word1} and ${word2}`);
|
| 868 |
return null;
|
| 869 |
}
|
| 870 |
|
|
|
|
| 895 |
];
|
| 896 |
|
| 897 |
const trimmed = this.trimGrid(grid, placedWords);
|
|
|
|
| 898 |
return { grid: trimmed.grid, size: trimmed.grid.length, placedWords: trimmed.placedWords };
|
| 899 |
}
|
| 900 |
|