Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9c8a6a13f5 | |||
| 9348b5ecd5 |
@ -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}")
|
||||
|
||||
@ -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.
|
||||
"""
|
||||
@ -132,6 +133,72 @@ def generate_grid_template(width: int, height: int, block_ratio: float = 0.25, s
|
||||
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"""
|
||||
sym_r, sym_c = height - 1 - r, width - 1 - c
|
||||
@ -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(" ", "."))
|
||||
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "multiplayer-crosswords"
|
||||
version = "0.2.0"
|
||||
version = "0.2.1"
|
||||
description = ""
|
||||
authors = [
|
||||
{name="Jonas Weinz"}
|
||||
@ -17,7 +17,7 @@ dependencies = [
|
||||
]
|
||||
[tool.poetry]
|
||||
name = "multiplayer-crosswords"
|
||||
version = "0.2.0"
|
||||
version = "0.2.1"
|
||||
description = ""
|
||||
authors = [
|
||||
"Jonas Weinz"
|
||||
|
||||
@ -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
|
||||
|
||||
52
tests/test_grid_generator_min_length_relaxed.py
Normal file
52
tests/test_grid_generator_min_length_relaxed.py
Normal file
@ -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}"
|
||||
66
tests/test_grid_min4.py
Normal file
66
tests/test_grid_min4.py
Normal file
@ -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}"
|
||||
Reference in New Issue
Block a user