diff --git a/crossword_generator.py b/crossword_generator.py index fd71f85..e14d368 100644 --- a/crossword_generator.py +++ b/crossword_generator.py @@ -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))