Spaces:
Sleeping
Sleeping
Add enhanced game logic improvements
Browse files- Enhanced content filtering with additional formatting pattern detection
- Improved word matching algorithm with fallback mechanisms
- Better base word matching for inflected forms
- Fallback selection to ensure expected number of blanks
- More robust passage quality scoring for non-narrative content
- src/clozeGameEngine.js +50 -1
src/clozeGameEngine.js
CHANGED
|
@@ -168,6 +168,16 @@ class ClozeGame {
|
|
| 168 |
const dashSequences = (passage.match(/[-ββ]{3,}/g) || []).length;
|
| 169 |
const totalDashes = (passage.match(/[-ββ]/g) || []).length;
|
| 170 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 171 |
// Check for repetitive patterns (common in indexes/TOCs)
|
| 172 |
const repeatedPhrases = ['CONTENTS', 'CHAPTER', 'Volume', 'Vol.', 'Part', 'Book'];
|
| 173 |
const repetitionCount = repeatedPhrases.reduce((count, phrase) =>
|
|
@@ -187,6 +197,8 @@ class ClozeGame {
|
|
| 187 |
const repetitionRatio = repetitionCount / totalWords;
|
| 188 |
const titleLineRatio = titleLines / Math.max(1, lines.length);
|
| 189 |
const dashRatio = totalDashes / totalWords;
|
|
|
|
|
|
|
| 190 |
|
| 191 |
// Stricter thresholds for higher levels
|
| 192 |
const capsThreshold = this.currentLevel >= 3 ? 0.03 : 0.05;
|
|
@@ -205,6 +217,13 @@ class ClozeGame {
|
|
| 205 |
if (titleLineRatio > 0.2) { qualityScore += 5; issues.push(`title-lines: ${Math.round(titleLineRatio * 100)}%`); }
|
| 206 |
if (dashSequences > 0) { qualityScore += dashSequences * 3; issues.push(`dash-sequences: ${dashSequences}`); }
|
| 207 |
if (dashRatio > 0.02) { qualityScore += dashRatio * 25; issues.push(`dashes: ${Math.round(dashRatio * 100)}%`); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 208 |
|
| 209 |
// Reject if quality score indicates technical/non-narrative content
|
| 210 |
if (qualityScore > 3) {
|
|
@@ -268,7 +287,7 @@ class ClozeGame {
|
|
| 268 |
const words = this.originalText.split(/(\s+)/);
|
| 269 |
const wordsOnly = words.filter(w => w.trim() !== '');
|
| 270 |
|
| 271 |
-
// Find indices of selected words using
|
| 272 |
const selectedIndices = [];
|
| 273 |
selectedWords.forEach(word => {
|
| 274 |
// First try exact match (cleaned)
|
|
@@ -285,6 +304,18 @@ class ClozeGame {
|
|
| 285 |
);
|
| 286 |
}
|
| 287 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 288 |
if (index !== -1) {
|
| 289 |
selectedIndices.push(index);
|
| 290 |
} else {
|
|
@@ -292,6 +323,24 @@ class ClozeGame {
|
|
| 292 |
}
|
| 293 |
});
|
| 294 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 295 |
// Create blanks
|
| 296 |
this.blanks = [];
|
| 297 |
this.hints = [];
|
|
|
|
| 168 |
const dashSequences = (passage.match(/[-ββ]{3,}/g) || []).length;
|
| 169 |
const totalDashes = (passage.match(/[-ββ]/g) || []).length;
|
| 170 |
|
| 171 |
+
// Count additional formatting patterns
|
| 172 |
+
const asteriskSequences = (passage.match(/\*{3,}/g) || []).length;
|
| 173 |
+
const asteriskLines = (passage.match(/^\s*\*+\s*$/gm) || []).length;
|
| 174 |
+
const underscoreSequences = (passage.match(/_{3,}/g) || []).length;
|
| 175 |
+
const equalSequences = (passage.match(/={3,}/g) || []).length;
|
| 176 |
+
const pipeCount = (passage.match(/\|/g) || []).length;
|
| 177 |
+
const numberedLines = (passage.match(/^\s*\d+[\.\)]\s/gm) || []).length;
|
| 178 |
+
const parenthesesCount = (passage.match(/[()]/g) || []).length;
|
| 179 |
+
const squareBrackets = (passage.match(/[\[\]]/g) || []).length;
|
| 180 |
+
|
| 181 |
// Check for repetitive patterns (common in indexes/TOCs)
|
| 182 |
const repeatedPhrases = ['CONTENTS', 'CHAPTER', 'Volume', 'Vol.', 'Part', 'Book'];
|
| 183 |
const repetitionCount = repeatedPhrases.reduce((count, phrase) =>
|
|
|
|
| 197 |
const repetitionRatio = repetitionCount / totalWords;
|
| 198 |
const titleLineRatio = titleLines / Math.max(1, lines.length);
|
| 199 |
const dashRatio = totalDashes / totalWords;
|
| 200 |
+
const parenthesesRatio = parenthesesCount / totalWords;
|
| 201 |
+
const squareBracketRatio = squareBrackets / totalWords;
|
| 202 |
|
| 203 |
// Stricter thresholds for higher levels
|
| 204 |
const capsThreshold = this.currentLevel >= 3 ? 0.03 : 0.05;
|
|
|
|
| 217 |
if (titleLineRatio > 0.2) { qualityScore += 5; issues.push(`title-lines: ${Math.round(titleLineRatio * 100)}%`); }
|
| 218 |
if (dashSequences > 0) { qualityScore += dashSequences * 3; issues.push(`dash-sequences: ${dashSequences}`); }
|
| 219 |
if (dashRatio > 0.02) { qualityScore += dashRatio * 25; issues.push(`dashes: ${Math.round(dashRatio * 100)}%`); }
|
| 220 |
+
if (asteriskSequences > 0 || asteriskLines > 0) { qualityScore += (asteriskSequences + asteriskLines) * 2; issues.push(`asterisk-separators: ${asteriskSequences + asteriskLines}`); }
|
| 221 |
+
if (underscoreSequences > 0) { qualityScore += underscoreSequences * 2; issues.push(`underscore-lines: ${underscoreSequences}`); }
|
| 222 |
+
if (equalSequences > 0) { qualityScore += equalSequences * 2; issues.push(`equal-lines: ${equalSequences}`); }
|
| 223 |
+
if (pipeCount > 5) { qualityScore += 3; issues.push(`table-formatting: ${pipeCount} pipes`); }
|
| 224 |
+
if (numberedLines > 3) { qualityScore += 2; issues.push(`numbered-list: ${numberedLines} items`); }
|
| 225 |
+
if (parenthesesRatio > 0.05) { qualityScore += 2; issues.push(`excessive-parentheses: ${Math.round(parenthesesRatio * 100)}%`); }
|
| 226 |
+
if (squareBracketRatio > 0.02) { qualityScore += 2; issues.push(`excessive-brackets: ${Math.round(squareBracketRatio * 100)}%`); }
|
| 227 |
|
| 228 |
// Reject if quality score indicates technical/non-narrative content
|
| 229 |
if (qualityScore > 3) {
|
|
|
|
| 287 |
const words = this.originalText.split(/(\s+)/);
|
| 288 |
const wordsOnly = words.filter(w => w.trim() !== '');
|
| 289 |
|
| 290 |
+
// Find indices of selected words using flexible matching
|
| 291 |
const selectedIndices = [];
|
| 292 |
selectedWords.forEach(word => {
|
| 293 |
// First try exact match (cleaned)
|
|
|
|
| 304 |
);
|
| 305 |
}
|
| 306 |
|
| 307 |
+
// Enhanced fallback: try base word matching (remove common suffixes)
|
| 308 |
+
if (index === -1) {
|
| 309 |
+
const baseWord = word.replace(/[^\w]/g, '').toLowerCase().replace(/(ed|ing|s|es|er|est)$/, '');
|
| 310 |
+
if (baseWord.length > 2) {
|
| 311 |
+
index = wordsOnly.findIndex((w, idx) => {
|
| 312 |
+
const cleanW = w.replace(/[^\w]/g, '').toLowerCase();
|
| 313 |
+
const baseW = cleanW.replace(/(ed|ing|s|es|er|est)$/, '');
|
| 314 |
+
return baseW === baseWord && !selectedIndices.includes(idx);
|
| 315 |
+
});
|
| 316 |
+
}
|
| 317 |
+
}
|
| 318 |
+
|
| 319 |
if (index !== -1) {
|
| 320 |
selectedIndices.push(index);
|
| 321 |
} else {
|
|
|
|
| 323 |
}
|
| 324 |
});
|
| 325 |
|
| 326 |
+
// Ensure we have at least the expected number of blanks
|
| 327 |
+
if (selectedIndices.length < expectedBlanks) {
|
| 328 |
+
console.warn(`Only found ${selectedIndices.length} words, need ${expectedBlanks}. Using fallback selection.`);
|
| 329 |
+
const fallbackWords = this.selectWordsManually(wordsOnly, expectedBlanks - selectedIndices.length);
|
| 330 |
+
|
| 331 |
+
// Add fallback word indices
|
| 332 |
+
fallbackWords.forEach(fallbackWord => {
|
| 333 |
+
const cleanFallback = fallbackWord.toLowerCase().replace(/[^\w]/g, '');
|
| 334 |
+
const index = wordsOnly.findIndex((w, idx) => {
|
| 335 |
+
const cleanW = w.replace(/[^\w]/g, '').toLowerCase();
|
| 336 |
+
return cleanW === cleanFallback && !selectedIndices.includes(idx);
|
| 337 |
+
});
|
| 338 |
+
if (index !== -1) {
|
| 339 |
+
selectedIndices.push(index);
|
| 340 |
+
}
|
| 341 |
+
});
|
| 342 |
+
}
|
| 343 |
+
|
| 344 |
// Create blanks
|
| 345 |
this.blanks = [];
|
| 346 |
this.hints = [];
|