vimalk78 commited on
Commit
245c727
Β·
1 Parent(s): 1c72dce

works well

Browse files

Signed-off-by: Vimal Kumar <vimal78@gmail.com>

crossword-app/backend/src/app.js CHANGED
@@ -27,7 +27,7 @@ app.use(limiter);
27
 
28
  const generateLimiter = rateLimit({
29
  windowMs: 5 * 60 * 1000,
30
- max: 10,
31
  message: 'Too many puzzle generation requests, please wait before trying again.',
32
  });
33
 
 
27
 
28
  const generateLimiter = rateLimit({
29
  windowMs: 5 * 60 * 1000,
30
+ max: 50,
31
  message: 'Too many puzzle generation requests, please wait before trying again.',
32
  });
33
 
crossword-app/backend/src/services/crosswordGenerator.js CHANGED
@@ -8,19 +8,30 @@ class CrosswordGenerator {
8
  }
9
 
10
  async generatePuzzle(topics, difficulty = 'medium') {
 
 
11
  try {
12
  const words = await this.selectWords(topics, difficulty);
 
 
13
  if (words.length < this.minWords) {
 
14
  throw new Error('Not enough words available for selected topics');
15
  }
16
 
 
17
  const gridResult = this.createGrid(words);
 
18
  if (!gridResult) {
 
19
  throw new Error('Could not place words in grid');
20
  }
21
 
 
 
22
  const clues = this.generateClues(words, gridResult.placedWords);
23
 
 
24
  return {
25
  grid: gridResult.grid,
26
  clues: clues,
@@ -32,7 +43,7 @@ class CrosswordGenerator {
32
  }
33
  };
34
  } catch (error) {
35
- console.error('Error generating puzzle:', error);
36
  return null;
37
  }
38
  }
@@ -59,8 +70,14 @@ class CrosswordGenerator {
59
  const word = wordObj.word;
60
  let score = 0;
61
 
62
- // Longer words get higher scores (they make good anchors)
63
- score += word.length * 2;
 
 
 
 
 
 
64
 
65
  // Words with common letters get bonus points
66
  const commonLetters = ['E', 'A', 'R', 'I', 'O', 'T', 'N', 'S'];
@@ -80,7 +97,7 @@ class CrosswordGenerator {
80
 
81
  // Sort by score (descending) then shuffle within score groups
82
  scoredWords.sort((a, b) => {
83
- if (Math.abs(a.crosswordScore - b.crosswordScore) <= 2) {
84
  return Math.random() - 0.5; // Shuffle similar scores
85
  }
86
  return b.crosswordScore - a.crosswordScore;
@@ -91,7 +108,7 @@ class CrosswordGenerator {
91
 
92
  filterWordsByDifficulty(words, difficulty) {
93
  const difficultyMap = {
94
- easy: { minLen: 3, maxLen: 7 },
95
  medium: { minLen: 4, maxLen: 10 },
96
  hard: { minLen: 5, maxLen: 15 }
97
  };
@@ -104,19 +121,64 @@ class CrosswordGenerator {
104
  }
105
 
106
  createGrid(words) {
107
- if (!words || words.length === 0) return null;
 
 
 
108
 
109
  const wordList = words.map(w => w.word.toUpperCase()).sort((a, b) => b.length - a.length);
110
  const size = this.calculateGridSize(wordList);
 
111
 
 
112
  for (let attempt = 0; attempt < 3; attempt++) {
113
  const currentSize = size + attempt;
114
- const result = this.placeWordsInGrid(wordList, currentSize);
 
 
 
115
  if (result) {
 
116
  return { grid: result.grid, size: currentSize, placedWords: result.placedWords };
117
  }
 
 
 
 
 
 
 
 
 
 
 
118
  }
119
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120
  return null;
121
  }
122
 
@@ -180,64 +242,127 @@ class CrosswordGenerator {
180
  }
181
 
182
  placeWordsInGrid(words, size) {
 
183
  const grid = Array(size).fill().map(() => Array(size).fill('.'));
184
  const placedWords = [];
185
 
186
- if (this.backtrackPlacement(grid, words, 0, placedWords)) {
 
 
 
 
 
187
  const trimmed = this.trimGrid(grid, placedWords);
 
188
  return { grid: trimmed.grid, placedWords: trimmed.placedWords };
189
  }
190
 
 
191
  return null;
192
  }
193
 
194
- backtrackPlacement(grid, words, wordIndex, placedWords) {
 
 
 
 
 
 
195
  if (wordIndex >= words.length) {
 
196
  return true;
197
  }
198
 
199
  const word = words[wordIndex];
200
  const size = grid.length;
 
 
 
 
 
201
 
202
  // For the first word, place the longest word in the center horizontally
203
  if (wordIndex === 0) {
204
  const centerRow = Math.floor(size / 2);
205
  const centerCol = Math.floor((size - word.length) / 2);
 
206
 
207
  if (this.canPlaceWord(grid, word, centerRow, centerCol, 'horizontal')) {
 
208
  const originalState = this.placeWord(grid, word, centerRow, centerCol, 'horizontal');
209
  placedWords.push({ word, row: centerRow, col: centerCol, direction: 'horizontal', number: 1 });
210
 
211
- if (this.backtrackPlacement(grid, words, wordIndex + 1, placedWords)) {
212
  return true;
213
  }
214
 
 
215
  this.removeWord(grid, originalState);
216
  placedWords.pop();
 
 
217
  }
218
  return false;
219
  }
220
 
221
- // For the second word, try to create a central cross
222
  if (wordIndex === 1) {
223
  const firstWord = placedWords[0];
224
- const intersections = this.findWordIntersections(word, firstWord.word);
225
 
226
- for (const intersection of intersections) {
227
- const placement = this.calculateIntersectionPlacement(word, intersection.wordPos, firstWord, intersection.placedPos);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
228
 
229
- if (placement && this.canPlaceWord(grid, word, placement.row, placement.col, placement.direction)) {
230
- const originalState = this.placeWord(grid, word, placement.row, placement.col, placement.direction);
231
- placedWords.push({ word, ...placement, number: 2 });
232
 
233
- if (this.backtrackPlacement(grid, words, wordIndex + 1, placedWords)) {
234
- return true;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
235
  }
236
-
237
- this.removeWord(grid, originalState);
238
- placedWords.pop();
239
  }
240
  }
 
 
241
  return false;
242
  }
243
 
@@ -255,7 +380,7 @@ class CrosswordGenerator {
255
  const originalState = this.placeWord(grid, word, row, col, direction);
256
  placedWords.push({ word, row, col, direction, number: wordIndex + 1 });
257
 
258
- if (this.backtrackPlacement(grid, words, wordIndex + 1, placedWords)) {
259
  return true;
260
  }
261
 
@@ -775,6 +900,50 @@ class CrosswordGenerator {
775
  return grid[row] && grid[row][col] === letter;
776
  }
777
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
778
  shuffleArray(array) {
779
  const shuffled = [...array];
780
  for (let i = shuffled.length - 1; i > 0; i--) {
 
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.log(`❌ Not enough words: ${words.length} < ${this.minWords}`);
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.log(`❌ Grid creation failed - could not place words`);
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,
 
43
  }
44
  };
45
  } catch (error) {
46
+ console.error('❌ Error generating puzzle:', error);
47
  return null;
48
  }
49
  }
 
70
  const word = wordObj.word;
71
  let score = 0;
72
 
73
+ // Moderate preference for medium-length words (5-7 chars optimal)
74
+ if (word.length >= 5 && word.length <= 7) {
75
+ score += 10; // Good base score for medium words
76
+ } else if (word.length >= 3 && word.length <= 4) {
77
+ score += 5; // Short words get some points
78
+ } else if (word.length >= 8) {
79
+ score += 3; // Long words get fewer points
80
+ }
81
 
82
  // Words with common letters get bonus points
83
  const commonLetters = ['E', 'A', 'R', 'I', 'O', 'T', 'N', 'S'];
 
97
 
98
  // Sort by score (descending) then shuffle within score groups
99
  scoredWords.sort((a, b) => {
100
+ if (Math.abs(a.crosswordScore - b.crosswordScore) <= 3) {
101
  return Math.random() - 0.5; // Shuffle similar scores
102
  }
103
  return b.crosswordScore - a.crosswordScore;
 
108
 
109
  filterWordsByDifficulty(words, difficulty) {
110
  const difficultyMap = {
111
+ easy: { minLen: 3, maxLen: 8 },
112
  medium: { minLen: 4, maxLen: 10 },
113
  hard: { minLen: 5, maxLen: 15 }
114
  };
 
121
  }
122
 
123
  createGrid(words) {
124
+ if (!words || words.length === 0) {
125
+ console.log(`❌ No words provided to createGrid`);
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
+ }
170
+ }
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.log(`❌ All grid creation attempts failed`);
182
  return null;
183
  }
184
 
 
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.log(`⏰ Timeout reached after ${Date.now() - startTime}ms (${callCount} calls)`);
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
 
295
+ if (this.backtrackPlacement(grid, words, wordIndex + 1, placedWords, startTime, timeout, callCount + 1)) {
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
  }
307
 
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
315
+ const wordsWithIntersections = [];
316
+
317
+ // Score each remaining word by intersection potential
318
+ for (const candidateWord of remainingWords) {
319
+ const intersections = this.findWordIntersections(candidateWord, firstWord.word);
320
+ if (intersections.length > 0) {
321
+ wordsWithIntersections.push({
322
+ word: candidateWord,
323
+ intersectionCount: intersections.length,
324
+ intersections: intersections
325
+ });
326
+ }
327
+ }
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
 
 
380
  const originalState = this.placeWord(grid, word, row, col, direction);
381
  placedWords.push({ word, row, col, direction, number: wordIndex + 1 });
382
 
383
+ if (this.backtrackPlacement(grid, words, wordIndex + 1, placedWords, startTime, timeout, callCount + 1)) {
384
  return true;
385
  }
386
 
 
900
  return grid[row] && grid[row][col] === letter;
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.log(`❌ No intersections found between ${word1} and ${word2}`);
913
+ return null;
914
+ }
915
+
916
+ // Use the first intersection
917
+ const intersection = intersections[0];
918
+ const size = Math.max(word1.length, word2.length) + 4;
919
+ const grid = Array(size).fill().map(() => Array(size).fill('.'));
920
+
921
+ // Place first word horizontally in center
922
+ const centerRow = Math.floor(size / 2);
923
+ const centerCol = Math.floor((size - word1.length) / 2);
924
+
925
+ for (let i = 0; i < word1.length; i++) {
926
+ grid[centerRow][centerCol + i] = word1[i];
927
+ }
928
+
929
+ // Place second word vertically at intersection
930
+ const intersectionCol = centerCol + intersection.wordPos;
931
+ const word2StartRow = centerRow - intersection.placedPos;
932
+
933
+ for (let i = 0; i < word2.length; i++) {
934
+ grid[word2StartRow + i][intersectionCol] = word2[i];
935
+ }
936
+
937
+ const placedWords = [
938
+ { word: word1, row: centerRow, col: centerCol, direction: 'horizontal', number: 1 },
939
+ { word: word2, row: word2StartRow, col: intersectionCol, direction: 'vertical', number: 2 }
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
+
947
  shuffleArray(array) {
948
  const shuffled = [...array];
949
  for (let i = shuffled.length - 1; i > 0; i--) {
crossword-app/frontend/src/App.jsx CHANGED
@@ -58,6 +58,7 @@ function App() {
58
  <TopicSelector
59
  onTopicsChange={handleTopicsChange}
60
  availableTopics={topics}
 
61
  />
62
 
63
  <div className="puzzle-controls">
 
58
  <TopicSelector
59
  onTopicsChange={handleTopicsChange}
60
  availableTopics={topics}
61
+ selectedTopics={selectedTopics}
62
  />
63
 
64
  <div className="puzzle-controls">
crossword-app/frontend/src/components/TopicSelector.jsx CHANGED
@@ -1,14 +1,11 @@
1
- import React, { useState } from 'react';
2
-
3
- const TopicSelector = ({ onTopicsChange, availableTopics = [] }) => {
4
- const [selectedTopics, setSelectedTopics] = useState([]);
5
 
 
6
  const handleTopicToggle = (topic) => {
7
  const newSelectedTopics = selectedTopics.includes(topic)
8
  ? selectedTopics.filter(t => t !== topic)
9
  : [...selectedTopics, topic];
10
 
11
- setSelectedTopics(newSelectedTopics);
12
  onTopicsChange(newSelectedTopics);
13
  };
14
 
 
1
+ import React from 'react';
 
 
 
2
 
3
+ const TopicSelector = ({ onTopicsChange, availableTopics = [], selectedTopics = [] }) => {
4
  const handleTopicToggle = (topic) => {
5
  const newSelectedTopics = selectedTopics.includes(topic)
6
  ? selectedTopics.filter(t => t !== topic)
7
  : [...selectedTopics, topic];
8
 
 
9
  onTopicsChange(newSelectedTopics);
10
  };
11
 
crossword-app/frontend/src/hooks/useCrossword.js CHANGED
@@ -38,7 +38,10 @@ const useCrossword = () => {
38
  })
39
  });
40
 
41
- if (!response.ok) throw new Error('Failed to generate puzzle');
 
 
 
42
 
43
  const puzzleData = await response.json();
44
  setPuzzle(puzzleData);
 
38
  })
39
  });
40
 
41
+ if (!response.ok) {
42
+ const errorData = await response.json().catch(() => ({}));
43
+ throw new Error(errorData.message || 'Failed to generate puzzle');
44
+ }
45
 
46
  const puzzleData = await response.json();
47
  setPuzzle(puzzleData);