feat: aggiunge il vincolo DIFFXY e il feedback live durante l'elaborazione

This commit is contained in:
2026-04-13 18:10:13 +02:00
parent 899dba0007
commit 0b42c2ecd4

View File

@@ -35,6 +35,7 @@ WORDS = [
HORIZONTAL = "H"
VERTICAL = "V"
DIFFXY = 7
@dataclass(frozen=True)
@@ -78,9 +79,28 @@ class CrosswordState:
x_min, y_min, x_max, y_max = self.bounds()
return (x_max - x_min + 1) * (y_max - y_min + 1)
def score(self) -> Tuple[int, int, int]:
# Maximize placed words first, then intersections, then prefer compact grids.
return (self.placed_words, self.intersections, -self.area())
def width(self) -> int:
x_min, _, x_max, _ = self.bounds()
return x_max - x_min + 1
def height(self) -> int:
_, y_min, _, y_max = self.bounds()
return y_max - y_min + 1
def shape_difference(self) -> int:
return abs(self.width() - self.height())
def score(self, diffxy: int) -> Tuple[int, int, int, int, int]:
# Prefer valid shapes first; within those, maximize placed words and intersections,
# then prefer squarer and more compact grids.
is_shape_valid = 1 if self.shape_difference() <= diffxy else 0
return (
is_shape_valid,
self.placed_words,
self.intersections,
-self.shape_difference(),
-self.area(),
)
class CrosswordGenerator:
@@ -90,6 +110,7 @@ class CrosswordGenerator:
*,
max_candidates_per_word: int = 12,
time_limit_seconds: float = 8.0,
diffxy: int = DIFFXY,
) -> None:
normalized = [self._normalize(word) for word in words]
unique_words = list(dict.fromkeys(word for word in normalized if len(word) >= 2))
@@ -97,6 +118,7 @@ class CrosswordGenerator:
self.best_state = CrosswordState(grid={}, placements=[], intersections=0)
self.max_candidates_per_word = max_candidates_per_word
self.time_limit_seconds = time_limit_seconds
self.diffxy = diffxy
self.started_at = 0.0
self.visited: Dict[Tuple[frozenset, Tuple[str, ...]], Tuple[int, int, int]] = {}
self.nodes_visited = 0
@@ -151,7 +173,7 @@ class CrosswordGenerator:
if time.perf_counter() - self.started_at > self.time_limit_seconds:
return
if state.score() > self.best_state.score():
if state.score(self.diffxy) > self.best_state.score(self.diffxy):
self.best_state = state.copy()
if not remaining_words:
@@ -167,9 +189,10 @@ class CrosswordGenerator:
signature = (frozenset(state.grid.items()), tuple(sorted(remaining_words)))
best_seen = self.visited.get(signature)
if best_seen is not None and best_seen >= state.score():
current_score = state.score(self.diffxy)
if best_seen is not None and best_seen >= current_score:
return
self.visited[signature] = state.score()
self.visited[signature] = current_score
next_word = self._select_next_word(state, remaining_words)
candidates = self._generate_candidates(state, next_word)
@@ -206,8 +229,8 @@ class CrosswordGenerator:
message = (
f"\r{frame} elaborazione... "
f"nodi={self.nodes_visited} "
f"migliore={self.best_state.placed_words} parole, {self.best_state.intersections} incroci "
f"attuale={state.placed_words} parole "
f"migliore={self.best_state.placed_words} parole, {self.best_state.intersections} incroci, diff={self.best_state.shape_difference()} "
f"attuale={state.placed_words} parole, diff={state.shape_difference()} "
f"t={elapsed:0.1f}s"
)
print(message, end="", file=sys.stderr, flush=True)
@@ -371,6 +394,8 @@ def main() -> None:
print("Miglior soluzione trovata")
print(f"Parole inserite: {solution.placed_words}/{len(generator.words)}")
print(f"Intersezioni totali: {solution.intersections}")
print(f"Dimensioni: {solution.width()} colonne x {solution.height()} righe")
print(f"Differenza righe/colonne: {solution.shape_difference()} (vincolo DIFFXY={generator.diffxy})")
print(f"Area occupata: {solution.area()}")
print()
print(render_grid(solution.grid, solution.placements))