From 9348b5ecd51925abcbddbeee1dd6d8f51e1f519f Mon Sep 17 00:00:00 2001 From: Jonas Weinz Date: Sun, 1 Feb 2026 20:53:12 +0100 Subject: [PATCH] enfoce min word length 3 --- multiplayer_crosswords/crossword.py | 2 +- multiplayer_crosswords/crossword_algorithm.py | 99 +++++++++++++++++-- .../server/server_config.py | 2 +- tests/test_crossword.py | 2 +- .../test_grid_generator_min_length_relaxed.py | 52 ++++++++++ tests/test_grid_min4.py | 66 +++++++++++++ 6 files changed, 214 insertions(+), 9 deletions(-) create mode 100644 tests/test_grid_generator_min_length_relaxed.py create mode 100644 tests/test_grid_min4.py diff --git a/multiplayer_crosswords/crossword.py b/multiplayer_crosswords/crossword.py index 470851d..40485a9 100644 --- a/multiplayer_crosswords/crossword.py +++ b/multiplayer_crosswords/crossword.py @@ -456,7 +456,7 @@ if __name__ == "__main__": grid_block_ratio=0.38 ) - crossword.extract_words() + for word in crossword.words: print(f"Word: {word.word}, Start: ({word.start_x}, {word.start_y}), Orientation: {word.orientation}, Hint: {word.hint}") diff --git a/multiplayer_crosswords/crossword_algorithm.py b/multiplayer_crosswords/crossword_algorithm.py index f4ffd8e..cc13579 100644 --- a/multiplayer_crosswords/crossword_algorithm.py +++ b/multiplayer_crosswords/crossword_algorithm.py @@ -81,7 +81,7 @@ def slot_pattern(grid: List[List[str]], slot: Slot) -> str: pattern.append(cell if cell and cell != '#' else '*') return ''.join(pattern) -def generate_grid_template(width: int, height: int, block_ratio: float = 0.25, seed: Optional[int] = None, max_slot_length: int = 15) -> List[List[str]]: +def generate_grid_template(width: int, height: int, block_ratio: float = 0.25, seed: Optional[int] = None, max_slot_length: int = 15, min_slot_length: int = 3) -> List[List[str]]: """Generates a grid template with blocks ('#') and empty cells (''). It will be rotationally symmetric. Args: @@ -90,6 +90,7 @@ def generate_grid_template(width: int, height: int, block_ratio: float = 0.25, s block_ratio (float): Approximate ratio of blocks in the grid. seed (Optional[int]): Random seed for reproducibility. max_slot_length (int): Maximum length of any slot to avoid overly long slots. + min_slot_length (int): Minimum length of any slot. Returns: List[List[str]]: Generated grid template. """ @@ -131,6 +132,72 @@ def generate_grid_template(width: int, height: int, block_ratio: float = 0.25, s # Count total empty cells empty_count = sum(1 for r in range(rows) for c in range(cols) if test_grid[r][c] == '') return len(visited) == empty_count + + def get_horizontal_length(r: int, c: int) -> int: + if grid[r][c] == '#': return 0 + # look left + lc = c + while lc >= 0 and grid[r][lc] != '#': + lc -= 1 + # look right + rc = c + while rc < width and grid[r][rc] != '#': + rc += 1 + return (rc - 1) - (lc + 1) + 1 + + def get_vertical_length(r: int, c: int) -> int: + if grid[r][c] == '#': return 0 + # look up + ur = r + while ur >= 0 and grid[ur][c] != '#': + ur -= 1 + # look down + dr = r + while dr < height and grid[dr][c] != '#': + dr += 1 + return (dr - 1) - (ur + 1) + 1 + + def check_valid_lengths_around(r: int, c: int) -> bool: + """Check if placing a block at (r, c) creates any slots of invalid length (1 < length < min_slot_length)""" + # grid[r][c] is assumed to be '#' + + # Horizontal Left + if c > 0 and grid[r][c-1] != '#': + start_c = c - 1 + while start_c >= 0 and grid[r][start_c] != '#': + start_c -= 1 + length = (c - 1) - start_c + if 1 < length < min_slot_length: + return False + + # Horizontal Right + if c < width - 1 and grid[r][c+1] != '#': + end_c = c + 1 + while end_c < width and grid[r][end_c] != '#': + end_c += 1 + length = end_c - (c + 1) + if 1 < length < min_slot_length: + return False + + # Vertical Up + if r > 0 and grid[r-1][c] != '#': + start_r = r - 1 + while start_r >= 0 and grid[start_r][c] != '#': + start_r -= 1 + length = (r - 1) - start_r + if 1 < length < min_slot_length: + return False + + # Vertical Down + if r < height - 1 and grid[r+1][c] != '#': + end_r = r + 1 + while end_r < height and grid[end_r][c] != '#': + end_r += 1 + length = end_r - (r + 1) + if 1 < length < min_slot_length: + return False + + return True def can_place_block(r: int, c: int) -> bool: """Check if we can place a block at (r,c) while maintaining symmetry and connectivity""" @@ -144,14 +211,24 @@ def generate_grid_template(width: int, height: int, block_ratio: float = 0.25, s grid[r][c] = '#' grid[sym_r][sym_c] = '#' + possible = True # Check connectivity - connected = is_connected(grid) + if not is_connected(grid): + possible = False + + # Check slot lengths + if possible and not check_valid_lengths_around(r, c): + possible = False + + if possible and (r != sym_r or c != sym_c): + if not check_valid_lengths_around(sym_r, sym_c): + possible = False # Restore original state grid[r][c] = '' grid[sym_r][sym_c] = '' - return connected + return possible def place_block_permanently(r: int, c: int): """Place block at (r,c) and its symmetric position""" @@ -234,6 +311,7 @@ class CrosswordGeneratorStep(object): grid_height: int | None = None, grid_block_ratio: float = 0.25, max_slot_length: int = None, + min_slot_length: int = 3, available_words_for_slotindex: Optional[Dict[int, Set[Word]]] = None, unfilled_slots: Optional[Set[int]] = None, rnd=None, @@ -266,6 +344,7 @@ class CrosswordGeneratorStep(object): if max_slot_length is None: max_slot_length = min(max(grid_width, grid_height) // 2 + max(grid_width, grid_height) + 4, 20) self._max_slot_length = max_slot_length + self._min_slot_length = min_slot_length self._available_words_for_slotindex = available_words_for_slotindex @@ -277,10 +356,11 @@ class CrosswordGeneratorStep(object): height=grid_height, block_ratio=grid_block_ratio, seed=self._seed, - max_slot_length=self._max_slot_length) + max_slot_length=self._max_slot_length, + min_slot_length=self._min_slot_length) if self._known_slots is None: - self._known_slots = extract_slots(self._grid) + self._known_slots = extract_slots(self._grid, min_length=self._min_slot_length) if self._field_slotindex_map_hor is None: self._field_slotindex_map_hor = [[-1 for _ in range(len(self._grid[0]))] for _ in range(len(self._grid))] for idx, slot in enumerate(self._known_slots): @@ -324,6 +404,7 @@ class CrosswordGeneratorStep(object): grid_height=self._grid_height, grid_block_ratio=self._grid_block_ratio, max_slot_length=self._max_slot_length, + min_slot_length=self._min_slot_length, available_words_for_slotindex={k: v.copy() for k, v in self._available_words_for_slotindex.items()} if self._available_words_for_slotindex else None, unfilled_slots=self._unfilled_slots.copy(), rnd=self._rnd, @@ -486,7 +567,8 @@ if __name__ == "__main__": seed=seed, grid_width=40, grid_height=20, - grid_block_ratio=0.4) + grid_block_ratio=0.41, + min_slot_length=3) final_step = generator.generate( max_tries_per_step=2, show_progress=True, @@ -502,4 +584,9 @@ if __name__ == "__main__": final_step.print_grid() print("Successfully generated crossword") + #grid = generate_grid_template(width=15, height=15, block_ratio=0.25, seed=42, min_slot_length=4) + # print grid + #for row in grid: + # print(' '.join(cell if cell else ' ' for cell in row).replace("#", "█").replace(" ", ".")) + diff --git a/multiplayer_crosswords/server/server_config.py b/multiplayer_crosswords/server/server_config.py index e3e5dec..e682fdd 100644 --- a/multiplayer_crosswords/server/server_config.py +++ b/multiplayer_crosswords/server/server_config.py @@ -6,7 +6,7 @@ DEFAULT_WEBSOCKET_PORT = 8765 DEFAULT_MIN_GRID_SIZE = 12 DEFAULT_MAX_GRID_SIZE = 25 -DEFAULT_GRID_BLOCK_RATIO = 0.39 +DEFAULT_GRID_BLOCK_RATIO = 0.41 DEFAULT_MAX_SESSION_IDLE_TIME_SECONDS = 3600 * 48 # 2 days diff --git a/tests/test_crossword.py b/tests/test_crossword.py index 068cc44..b41a0e8 100644 --- a/tests/test_crossword.py +++ b/tests/test_crossword.py @@ -37,7 +37,7 @@ def test_extract_words_and_positions(): assert dog.start_x == 0 and dog.start_y == 0 assert dog.orientation.name == "HORIZONTAL" # Hint should come from dictionary - assert dog.hist in ("bark",) or dog.hist.startswith("No hint available") is False + assert dog.hint in ("bark",) or dog.hint.startswith("No hint available") is False cat = words_by_text["cat"] assert cat.start_x == 4 and cat.start_y == 1 diff --git a/tests/test_grid_generator_min_length_relaxed.py b/tests/test_grid_generator_min_length_relaxed.py new file mode 100644 index 0000000..e58ae5d --- /dev/null +++ b/tests/test_grid_generator_min_length_relaxed.py @@ -0,0 +1,52 @@ +from multiplayer_crosswords.crossword_algorithm import generate_grid_template, extract_slots +import pytest + +def test_generate_grid_template_min_length_relaxed(): + width = 15 + height = 15 + min_len = 3 + + def get_len_at(grid, r, c, dr, dc): + # Scan backward + curr_r, curr_c = r, c + while curr_r >= 0 and curr_c >= 0 and grid[curr_r][curr_c] != '#': + curr_r -= dr + curr_c -= dc + start_r, start_c = curr_r + dr, curr_c + dc + + # Scan forward + curr_r, curr_c = r, c + while curr_r < height and curr_c < width and grid[curr_r][curr_c] != '#': + curr_r += dr + curr_c += dc + end_r, end_c = curr_r - dr, curr_c - dc + + if dr == 0: # Horizontal + return end_c - start_c + 1 + else: # Vertical + return end_r - start_r + 1 + + # Run multiple times to catch random failures + for seed in range(20): + grid = generate_grid_template(width=width, height=height, block_ratio=0.25, seed=seed, min_slot_length=min_len) + + failed_cells = [] + for r in range(height): + for c in range(width): + if grid[r][c] == '': + h_len = get_len_at(grid, r, c, 0, 1) + v_len = get_len_at(grid, r, c, 1, 0) + + if h_len < min_len and v_len < min_len: + failed_cells.append((r, c, h_len, v_len)) + + if failed_cells: + print(f"Seed {seed} failed. Invalid cells found:") + for cell in failed_cells: + print(f"Cell ({cell[0]}, {cell[1]}) has h_len={cell[2]} and v_len={cell[3]}") + # Print grid + print("Grid layout:") + for row in grid: + print("".join(['#' if ch == '#' else '.' for ch in row])) + + assert len(failed_cells) == 0, f"Found {len(failed_cells)} invalid cells for seed {seed}" diff --git a/tests/test_grid_min4.py b/tests/test_grid_min4.py new file mode 100644 index 0000000..5cc4796 --- /dev/null +++ b/tests/test_grid_min4.py @@ -0,0 +1,66 @@ +from multiplayer_crosswords.crossword_algorithm import generate_grid_template +import pytest + +def test_generate_grid_template_min_length_4_strict(): + width = 15 + height = 15 + min_len = 4 # STRICTER + + def get_len_at(grid, r, c, dr, dc): + # Scan backward + curr_r, curr_c = r, c + while curr_r >= 0 and curr_c >= 0 and grid[curr_r][curr_c] != '#': + curr_r -= dr + curr_c -= dc + start_r, start_c = curr_r + dr, curr_c + dc + + # Scan forward + curr_r, curr_c = r, c + while curr_r < height and curr_c < width and grid[curr_r][curr_c] != '#': + curr_r += dr + curr_c += dc + end_r, end_c = curr_r - dr, curr_c - dc + + if dr == 0: # Horizontal + return end_c - start_c + 1 + else: # Vertical + return end_r - start_r + 1 + + # Run multiple times + for seed in range(50): + grid = generate_grid_template(width=width, height=height, block_ratio=0.25, seed=seed, min_slot_length=min_len) + + failed_cells = [] + for r in range(height): + for c in range(width): + if grid[r][c] == '': + h_len = get_len_at(grid, r, c, 0, 1) + v_len = get_len_at(grid, r, c, 1, 0) + + if h_len < min_len and v_len < min_len: + failed_cells.append((r, c, h_len, v_len)) + + if failed_cells: + print(f"Seed {seed} failed. Invalid cells found:") + # Only print first few + for cell in failed_cells[:5]: + print(f"Cell ({cell[0]}, {cell[1]}) has h_len={cell[2]} and v_len={cell[3]}") + + # Print grid heavily + print("\nFAILED GRID:") + for r in range(height): + row_str = "" + for c in range(width): + if grid[r][c] == '#': + row_str += "# " + else: + # check if this cell is failed + is_failed = False + for fc in failed_cells: + if fc[0] == r and fc[1] == c: + is_failed = True + break + row_str += "X " if is_failed else ". " + print(row_str) + + assert len(failed_cells) == 0, f"Found {len(failed_cells)} invalid cells for seed {seed}"