feat: consolida lessico semantico, temi controllati e filler a quota tematica

This commit is contained in:
2026-04-15 15:37:52 +02:00
parent b172b9c04b
commit a1f8cb8577
8 changed files with 14030 additions and 46434 deletions

View File

@@ -62,7 +62,7 @@ class FillCandidate:
slot: FillSlot
new_letters: int
reused_letters: int
local_score: Tuple[int, int, int]
local_score: Tuple[int, ...]
class CrosswordFiller:
@@ -73,6 +73,9 @@ class CrosswordFiller:
*,
target_empty_ratio: float = TARGET_EMPTY_RATIO,
vocabulary_metadata: Optional[Dict[str, Dict[str, object]]] = None,
semantic_metadata: Optional[Dict[str, Dict[str, object]]] = None,
selected_topic: str = "general",
max_themed_fill_words: int = 10,
seed: Optional[int] = None,
) -> None:
self.state = state.copy()
@@ -83,6 +86,9 @@ class CrosswordFiller:
self.vocabulary = self._normalize_vocabulary(vocabulary)
self.words_by_length = self._index_vocabulary(self.vocabulary)
self.vocabulary_metadata = vocabulary_metadata or {}
self.semantic_metadata = semantic_metadata or {}
self.selected_topic = selected_topic.strip().lower()
self.max_themed_fill_words = max(0, max_themed_fill_words)
self.seed = seed
self.rng = random.Random(seed)
self.bounds = self._compute_bounds(self.state.grid)
@@ -281,9 +287,11 @@ class CrosswordFiller:
new_letters = sum(1 for cell in slot.cells if cell not in self.state.grid)
reused_letters = slot.fixed_letters
local_score = (
self._semantic_topic_score(word),
reused_letters,
new_letters,
self._word_quality(word),
self._semantic_quality(word),
len(set(word)),
)
candidates.append(
@@ -311,6 +319,56 @@ class CrosswordFiller:
except (TypeError, ValueError):
return 0
def _semantic_entry(self, word: str) -> Dict[str, object]:
return self.semantic_metadata.get(word, {})
def _semantic_quality(self, word: str) -> int:
entry = self._semantic_entry(word)
semantic = entry.get("semantic", {})
score = 0
if semantic.get("matched"):
score += 2
score += min(3, len(semantic.get("glosses", [])))
score += min(2, len(semantic.get("synonyms", [])))
return score
def _semantic_topic_score(self, word: str) -> int:
if not self.selected_topic or self.selected_topic == "general":
return 0
entry = self._semantic_entry(word)
try:
relevance = int(entry.get("_topic_relevance", 0))
except (TypeError, ValueError):
relevance = 0
if relevance:
if self._themed_added_count() < self.max_themed_fill_words:
return relevance
return min(relevance, 10)
topics = {str(item).lower() for item in entry.get("topics", [])}
semantic = entry.get("semantic", {})
semantic_topics = {str(item).lower() for item in semantic.get("semantic_topics", [])}
score = 0
if self.selected_topic in topics:
score += 4
if self.selected_topic in semantic_topics:
score += 6
if "general" in topics:
score += 1
return score
def _themed_added_count(self) -> int:
total = 0
for placement in self.added_words:
entry = self._semantic_entry(placement.word)
try:
if int(entry.get("_strong_topic_relevance", 0)) > 0:
total += 1
except (TypeError, ValueError):
continue
return total
def _placement_is_valid(self, slot: FillSlot, word: str) -> bool:
dx, dy = (1, 0) if slot.direction == HORIZONTAL else (0, 1)
before = (slot.x - dx, slot.y - dy)
@@ -380,6 +438,7 @@ class CrosswordFiller:
f"vuote={self.empty_cells_count()}/{self.total_cells} "
f"target={self.target_empty_cells} "
f"aggiunte={len(self.added_words)} "
f"tema={self._themed_added_count()}/{self.max_themed_fill_words} "
f"ultima={self.last_word} "
f"t={elapsed:0.1f}s"
)