first working algorithm sample
This commit is contained in:
857341
data/de.json
Normal file
857341
data/de.json
Normal file
File diff suppressed because it is too large
Load Diff
322653
data/en.json
Normal file
322653
data/en.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -3,6 +3,10 @@
|
|||||||
from typing import List, Tuple, Dict, Set, Optional
|
from typing import List, Tuple, Dict, Set, Optional
|
||||||
from multiplayer_crosswords.crossword import Crossword, Orientation
|
from multiplayer_crosswords.crossword import Crossword, Orientation
|
||||||
from multiplayer_crosswords.dictionary import Dictionary, Word
|
from multiplayer_crosswords.dictionary import Dictionary, Word
|
||||||
|
from multiplayer_crosswords.utils import load_en_dictionary, load_de_dictionary
|
||||||
|
from copy import copy, deepcopy
|
||||||
|
import random
|
||||||
|
import os
|
||||||
|
|
||||||
class Slot:
|
class Slot:
|
||||||
def __init__(self, row: int, col: int, orientation: Orientation, length: int):
|
def __init__(self, row: int, col: int, orientation: Orientation, length: int):
|
||||||
@ -16,6 +20,17 @@ class Slot:
|
|||||||
return f"Slot(row={self.row}, col={self.col}, orientation={self.orientation}, length={self.length})"
|
return f"Slot(row={self.row}, col={self.col}, orientation={self.orientation}, length={self.length})"
|
||||||
|
|
||||||
def extract_slots(grid: List[List[str]], min_length: int = 2) -> List[Slot]:
|
def extract_slots(grid: List[List[str]], min_length: int = 2) -> List[Slot]:
|
||||||
|
"""Extracts slots from the grid where words can be placed.
|
||||||
|
|
||||||
|
A slot is a sequence of empty cells (not '#') either horizontally or vertically.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
grid (List[List[str]]): 2D grid representing the crossword layout.
|
||||||
|
min_length (int): Minimum length of a slot to be considered valid.
|
||||||
|
Returns:
|
||||||
|
List[Slot]: List of extracted slots.
|
||||||
|
"""
|
||||||
|
|
||||||
rows, cols = len(grid), len(grid[0])
|
rows, cols = len(grid), len(grid[0])
|
||||||
slots = []
|
slots = []
|
||||||
|
|
||||||
@ -46,6 +61,14 @@ def extract_slots(grid: List[List[str]], min_length: int = 2) -> List[Slot]:
|
|||||||
return slots
|
return slots
|
||||||
|
|
||||||
def slot_pattern(grid: List[List[str]], slot: Slot) -> str:
|
def slot_pattern(grid: List[List[str]], slot: Slot) -> str:
|
||||||
|
"""Generates a pattern string for a given slot in the grid.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
grid (List[List[str]]): 2D grid representing the crossword layout.
|
||||||
|
slot (Slot): The slot for which to generate the pattern.
|
||||||
|
Returns:
|
||||||
|
str: Pattern string with known letters and '*' for unknowns.
|
||||||
|
"""
|
||||||
dr, dc = (0, 1) if slot.orientation == Orientation.HORIZONTAL else (1, 0)
|
dr, dc = (0, 1) if slot.orientation == Orientation.HORIZONTAL else (1, 0)
|
||||||
pattern = []
|
pattern = []
|
||||||
for i in range(slot.length):
|
for i in range(slot.length):
|
||||||
@ -53,3 +76,413 @@ def slot_pattern(grid: List[List[str]], slot: Slot) -> str:
|
|||||||
cell = grid[r][c]
|
cell = grid[r][c]
|
||||||
pattern.append(cell if cell and cell != '#' else '*')
|
pattern.append(cell if cell and cell != '#' else '*')
|
||||||
return ''.join(pattern)
|
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]]:
|
||||||
|
"""Generates a grid template with blocks ('#') and empty cells (''). It will be rotationally symmetric.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
width (int): Width of the grid.
|
||||||
|
height (int): Height of the grid.
|
||||||
|
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.
|
||||||
|
Returns:
|
||||||
|
List[List[str]]: Generated grid template.
|
||||||
|
"""
|
||||||
|
|
||||||
|
rnd = random.Random()
|
||||||
|
if seed is not None:
|
||||||
|
rnd.seed(seed)
|
||||||
|
|
||||||
|
# Initialize empty grid
|
||||||
|
grid = [['' for _ in range(width)] for _ in range(height)]
|
||||||
|
|
||||||
|
def is_connected(test_grid: List[List[str]]) -> bool:
|
||||||
|
"""Check if all empty cells are connected using flood fill"""
|
||||||
|
rows, cols = len(test_grid), len(test_grid[0])
|
||||||
|
visited = set()
|
||||||
|
|
||||||
|
# Find first empty cell
|
||||||
|
start = None
|
||||||
|
for r in range(rows):
|
||||||
|
for c in range(cols):
|
||||||
|
if test_grid[r][c] == '':
|
||||||
|
start = (r, c)
|
||||||
|
break
|
||||||
|
if start:
|
||||||
|
break
|
||||||
|
|
||||||
|
if not start:
|
||||||
|
return True # No empty cells
|
||||||
|
|
||||||
|
# Flood fill from start position
|
||||||
|
stack = [start]
|
||||||
|
while stack:
|
||||||
|
r, c = stack.pop()
|
||||||
|
if (r, c) in visited or r < 0 or r >= rows or c < 0 or c >= cols or test_grid[r][c] == '#':
|
||||||
|
continue
|
||||||
|
visited.add((r, c))
|
||||||
|
stack.extend([(r+1, c), (r-1, c), (r, c+1), (r, c-1)])
|
||||||
|
|
||||||
|
# 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 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
|
||||||
|
|
||||||
|
# Can't place if positions are already blocked
|
||||||
|
if grid[r][c] == '#' or grid[sym_r][sym_c] == '#':
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Temporarily place blocks
|
||||||
|
grid[r][c] = '#'
|
||||||
|
grid[sym_r][sym_c] = '#'
|
||||||
|
|
||||||
|
# Check connectivity
|
||||||
|
connected = is_connected(grid)
|
||||||
|
|
||||||
|
# Restore original state
|
||||||
|
grid[r][c] = ''
|
||||||
|
grid[sym_r][sym_c] = ''
|
||||||
|
|
||||||
|
return connected
|
||||||
|
|
||||||
|
def place_block_permanently(r: int, c: int):
|
||||||
|
"""Place block at (r,c) and its symmetric position"""
|
||||||
|
grid[r][c] = '#'
|
||||||
|
grid[height - 1 - r][width - 1 - c] = '#'
|
||||||
|
|
||||||
|
# Calculate target number of blocks
|
||||||
|
total_cells = width * height
|
||||||
|
target_blocks = int(total_cells * block_ratio)
|
||||||
|
|
||||||
|
# Create candidate positions for the upper half of the grid
|
||||||
|
# We only work with the upper half due to rotational symmetry
|
||||||
|
candidates = []
|
||||||
|
for r in range((height + 1) // 2):
|
||||||
|
for c in range(width):
|
||||||
|
# For center row (if height is odd), only consider left half + center
|
||||||
|
if r == height // 2 and height % 2 == 1:
|
||||||
|
if c <= width // 2:
|
||||||
|
candidates.append((r, c))
|
||||||
|
else:
|
||||||
|
candidates.append((r, c))
|
||||||
|
|
||||||
|
rnd.shuffle(candidates)
|
||||||
|
|
||||||
|
# Place blocks
|
||||||
|
blocks_placed = 0
|
||||||
|
for r, c in candidates:
|
||||||
|
if blocks_placed >= target_blocks:
|
||||||
|
break
|
||||||
|
|
||||||
|
if can_place_block(r, c):
|
||||||
|
place_block_permanently(r, c)
|
||||||
|
# Count blocks added (1 if center position, 2 otherwise)
|
||||||
|
sym_r, sym_c = height - 1 - r, width - 1 - c
|
||||||
|
blocks_added = 1 if (r == sym_r and c == sym_c) else 2
|
||||||
|
blocks_placed += blocks_added
|
||||||
|
|
||||||
|
# Break long slots by strategically placing additional blocks
|
||||||
|
for _ in range(3): # Limited iterations to prevent infinite loops
|
||||||
|
slots = extract_slots(grid)
|
||||||
|
long_slots = [s for s in slots if s.length > max_slot_length]
|
||||||
|
|
||||||
|
if not long_slots:
|
||||||
|
break
|
||||||
|
|
||||||
|
# Sort by length (longest first) to prioritize breaking worst offenders
|
||||||
|
long_slots.sort(key=lambda x: x.length, reverse=True)
|
||||||
|
|
||||||
|
for slot in long_slots:
|
||||||
|
# Try to break the slot in the middle
|
||||||
|
break_pos = slot.length // 2
|
||||||
|
|
||||||
|
if slot.orientation == Orientation.HORIZONTAL:
|
||||||
|
r, c = slot.row, slot.col + break_pos
|
||||||
|
else: # VERTICAL
|
||||||
|
r, c = slot.row + break_pos, slot.col
|
||||||
|
|
||||||
|
# Only break if we can maintain connectivity and symmetry
|
||||||
|
if can_place_block(r, c):
|
||||||
|
place_block_permanently(r, c)
|
||||||
|
break # Break one slot at a time and re-evaluate
|
||||||
|
|
||||||
|
return grid
|
||||||
|
|
||||||
|
class CrosswordGeneratorStep(object):
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
dictionary: Dictionary,
|
||||||
|
grid: Optional[List[List[str]]] = None,
|
||||||
|
known_slots: Optional[List[Slot]] = None,
|
||||||
|
field_slotindex_map_hor: Optional[List[List[int]]] = None,
|
||||||
|
field_slotindex_map_ver: Optional[List[List[int]]] = None,
|
||||||
|
seed: float | int | None = None,
|
||||||
|
grid_width: int | None = None,
|
||||||
|
grid_height: int | None = None,
|
||||||
|
grid_block_ratio: float = 0.25,
|
||||||
|
max_slot_length: int = 15,
|
||||||
|
available_words_for_slotindex: Optional[Dict[int, Set[Word]]] = None,
|
||||||
|
unfilled_slots: Optional[Set[int]] = None,
|
||||||
|
rnd=None,
|
||||||
|
):
|
||||||
|
self._dictionary = dictionary
|
||||||
|
self._grid = grid
|
||||||
|
self._known_slots = known_slots
|
||||||
|
self._field_slotindex_map_hor = field_slotindex_map_hor
|
||||||
|
self._field_slotindex_map_ver = field_slotindex_map_ver
|
||||||
|
self._seed = seed
|
||||||
|
self._unfilled_slots = unfilled_slots
|
||||||
|
|
||||||
|
if self._seed is None:
|
||||||
|
self._seed = random.randint(0, 2**31 - 1)
|
||||||
|
|
||||||
|
if rnd is None:
|
||||||
|
self._rnd = random.Random()
|
||||||
|
self._rnd.seed(self._seed)
|
||||||
|
else:
|
||||||
|
self._rnd = rnd
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
self._grid_width = grid_width
|
||||||
|
self._grid_height = grid_height
|
||||||
|
self._grid_block_ratio = grid_block_ratio
|
||||||
|
self._max_slot_length = max_slot_length
|
||||||
|
self._available_words_for_slotindex = available_words_for_slotindex
|
||||||
|
|
||||||
|
|
||||||
|
if self._grid is None:
|
||||||
|
if grid_width is None or grid_height is None:
|
||||||
|
raise ValueError("If no grid is provided, grid_width and grid_height must be specified.")
|
||||||
|
# create a default grid if none is provided
|
||||||
|
self._grid = generate_grid_template(width=grid_width,
|
||||||
|
height=grid_height,
|
||||||
|
block_ratio=grid_block_ratio,
|
||||||
|
seed=self._seed,
|
||||||
|
max_slot_length=self._max_slot_length)
|
||||||
|
|
||||||
|
if self._known_slots is None:
|
||||||
|
self._known_slots = extract_slots(self._grid)
|
||||||
|
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):
|
||||||
|
if slot.orientation != Orientation.HORIZONTAL:
|
||||||
|
continue
|
||||||
|
dr, dc = (0, 1)
|
||||||
|
for i in range(slot.length):
|
||||||
|
r, c = slot.row + dr * i, slot.col + dc * i
|
||||||
|
self._field_slotindex_map_hor[r][c] = idx
|
||||||
|
if self._field_slotindex_map_ver is None:
|
||||||
|
self._field_slotindex_map_ver = [[-1 for _ in range(len(self._grid[0]))] for _ in range(len(self._grid))]
|
||||||
|
for idx, slot in enumerate(self._known_slots):
|
||||||
|
if slot.orientation != Orientation.VERTICAL:
|
||||||
|
continue
|
||||||
|
dr, dc = (1, 0)
|
||||||
|
for i in range(slot.length):
|
||||||
|
r, c = slot.row + dr * i, slot.col + dc * i
|
||||||
|
self._field_slotindex_map_ver[r][c] = idx
|
||||||
|
|
||||||
|
if self._available_words_for_slotindex is None:
|
||||||
|
self._available_words_for_slotindex = {}
|
||||||
|
for idx, slot in enumerate(self._known_slots):
|
||||||
|
pattern = slot_pattern(self._grid, slot)
|
||||||
|
matching_words = self._dictionary.find_by_pattern(pattern)
|
||||||
|
self._available_words_for_slotindex[idx] = matching_words
|
||||||
|
|
||||||
|
if self._unfilled_slots is None:
|
||||||
|
# initialize with all slots unfilled
|
||||||
|
self._unfilled_slots = set(range(len(self._known_slots)))
|
||||||
|
|
||||||
|
|
||||||
|
def copy(self) -> "CrosswordGeneratorStep":
|
||||||
|
return CrosswordGeneratorStep(
|
||||||
|
dictionary=self._dictionary,
|
||||||
|
grid=[row.copy() for row in self._grid],
|
||||||
|
known_slots=self._known_slots.copy(),
|
||||||
|
field_slotindex_map_hor=[row.copy() for row in self._field_slotindex_map_hor] if self._field_slotindex_map_hor else None,
|
||||||
|
field_slotindex_map_ver=[row.copy() for row in self._field_slotindex_map_ver] if self._field_slotindex_map_ver else None,
|
||||||
|
seed=self._seed,
|
||||||
|
grid_width=self._grid_width,
|
||||||
|
grid_height=self._grid_height,
|
||||||
|
grid_block_ratio=self._grid_block_ratio,
|
||||||
|
max_slot_length=self._max_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,
|
||||||
|
)
|
||||||
|
|
||||||
|
def fill_next_slot(self) -> bool:
|
||||||
|
if len(self._unfilled_slots) == 0:
|
||||||
|
return True # All slots filled, everything done
|
||||||
|
|
||||||
|
# choose slot randomly from all slots with minimal available words
|
||||||
|
min_num_words = min(len(self._available_words_for_slotindex.get(idx, [])) for idx in self._unfilled_slots)
|
||||||
|
|
||||||
|
|
||||||
|
candidates = [idx for idx in self._unfilled_slots if len(self._available_words_for_slotindex.get(idx, [])) == min_num_words ]
|
||||||
|
|
||||||
|
slot_to_fill = self._rnd.choice(candidates)
|
||||||
|
available_words = list(self._available_words_for_slotindex.get(slot_to_fill, []))
|
||||||
|
|
||||||
|
if len(available_words) == 0:
|
||||||
|
return False # No available words for this slot, dead end
|
||||||
|
|
||||||
|
# choose a random word from available words
|
||||||
|
chosen_word = self._rnd.choice(available_words)
|
||||||
|
slot = self._known_slots[slot_to_fill]
|
||||||
|
|
||||||
|
# update own slot
|
||||||
|
self._available_words_for_slotindex[slot_to_fill] = {chosen_word}
|
||||||
|
|
||||||
|
# Place the word in the grid
|
||||||
|
dr, dc = (0, 1) if slot.orientation == Orientation.HORIZONTAL else (1, 0)
|
||||||
|
for i in range(slot.length):
|
||||||
|
r, c = slot.row + dr * i, slot.col + dc * i
|
||||||
|
self._grid[r][c] = chosen_word.word[i]
|
||||||
|
self._unfilled_slots.remove(slot_to_fill)
|
||||||
|
|
||||||
|
# Update available words for intersecting slots
|
||||||
|
for i in range(slot.length):
|
||||||
|
r, c = slot.row + dr * i, slot.col + dc * i
|
||||||
|
# Check horizontal slot
|
||||||
|
hor_slot_idx = self._field_slotindex_map_hor[r][c]
|
||||||
|
if hor_slot_idx != -1 and hor_slot_idx in self._unfilled_slots:
|
||||||
|
pattern = slot_pattern(self._grid, self._known_slots[hor_slot_idx])
|
||||||
|
matching_words = self._dictionary.find_by_pattern(pattern)
|
||||||
|
# Dead end, no words fit anymore
|
||||||
|
self._available_words_for_slotindex[hor_slot_idx] = set(matching_words)
|
||||||
|
if len(matching_words) == 0:
|
||||||
|
return False
|
||||||
|
# Check vertical slot
|
||||||
|
ver_slot_idx = self._field_slotindex_map_ver[r][c]
|
||||||
|
if ver_slot_idx != -1 and ver_slot_idx in self._unfilled_slots:
|
||||||
|
pattern = slot_pattern(self._grid, self._known_slots[ver_slot_idx])
|
||||||
|
matching_words = self._dictionary.find_by_pattern(pattern)
|
||||||
|
|
||||||
|
self._available_words_for_slotindex[ver_slot_idx] = set(matching_words)
|
||||||
|
if len(matching_words) == 0:
|
||||||
|
return False # Dead end, no words fit anymore
|
||||||
|
return True
|
||||||
|
|
||||||
|
def generate(self,
|
||||||
|
max_tries_per_step: int = 10,
|
||||||
|
max_allowed_single_choice: int = 1,
|
||||||
|
max_allowed_threshold_choice: int = 3,
|
||||||
|
max_allowed_threshold: int = 3,
|
||||||
|
show_progress: bool = False
|
||||||
|
) -> Optional["CrosswordGeneratorStep"]:
|
||||||
|
# count how many slots have only one available word
|
||||||
|
single_choice_slots = sum(1 for idx in self._unfilled_slots if len(self._available_words_for_slotindex.get(idx, [])) == 1)
|
||||||
|
if single_choice_slots > max_allowed_single_choice:
|
||||||
|
return None # Too many single-choice slots, backtrack
|
||||||
|
# count how many slots have only less than threshold available words
|
||||||
|
threshold_choice_slots = sum(1 for idx in self._unfilled_slots if len(self._available_words_for_slotindex.get(idx, [])) <= max_allowed_threshold)
|
||||||
|
if threshold_choice_slots > max_allowed_threshold_choice:
|
||||||
|
return None # Too many threshold-choice slots, backtrack
|
||||||
|
for i in range(max_tries_per_step if single_choice_slots == 0 else 1):
|
||||||
|
step_copy = self.copy()
|
||||||
|
if step_copy.fill_next_slot():
|
||||||
|
if show_progress:
|
||||||
|
self.print_grid()
|
||||||
|
if len(step_copy._unfilled_slots) == 0:
|
||||||
|
return step_copy # Successfully filled all slots
|
||||||
|
else:
|
||||||
|
next_step = step_copy.generate(
|
||||||
|
max_tries_per_step,
|
||||||
|
max_allowed_single_choice=max_allowed_single_choice,
|
||||||
|
max_allowed_threshold_choice=max_allowed_threshold_choice,
|
||||||
|
max_allowed_threshold=max_allowed_threshold,
|
||||||
|
show_progress=show_progress
|
||||||
|
)
|
||||||
|
if next_step is not None:
|
||||||
|
return next_step
|
||||||
|
else:
|
||||||
|
if show_progress:
|
||||||
|
self.print_grid()
|
||||||
|
#pass
|
||||||
|
else:
|
||||||
|
#self.print_grid()
|
||||||
|
pass
|
||||||
|
return None # Failed to fill a slot after max tries
|
||||||
|
|
||||||
|
def print_grid(self):
|
||||||
|
# clear as many rows in terminal as we will print
|
||||||
|
|
||||||
|
os.system('cls' if os.name == 'nt' else 'clear')
|
||||||
|
|
||||||
|
result = ""
|
||||||
|
for row in self._grid:
|
||||||
|
# print on stdout dircectly and only flush in the end
|
||||||
|
row_str = ' '.join(cell if cell else ' ' for cell in row).replace("#", "█") + "\n"
|
||||||
|
row_str = row_str.replace("█ █", "███").replace("█ █", "███")
|
||||||
|
#row_str = row_str.replace(" █", "██")
|
||||||
|
result += row_str
|
||||||
|
|
||||||
|
print(result, flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
# Simple string representation for debugging
|
||||||
|
result = "Crossword Grid:\n"
|
||||||
|
for row in self._grid:
|
||||||
|
result += ' '.join(cell if cell else ' ' for cell in row).replace("#", ".") + "\n"
|
||||||
|
|
||||||
|
#return result
|
||||||
|
result += "\n"
|
||||||
|
result += f"Horizontal Slot Map:\n"
|
||||||
|
for r in range(len(self._field_slotindex_map_hor)):
|
||||||
|
result += ' '.join(f"{idx:2d}" if idx != -1 else " ." for idx in self._field_slotindex_map_hor[r]) + "\n"
|
||||||
|
|
||||||
|
result += f"Vertical Slot Map:\n"
|
||||||
|
for r in range(len(self._field_slotindex_map_ver)):
|
||||||
|
result += ' '.join(f"{idx:2d}" if idx != -1 else " ." for idx in self._field_slotindex_map_ver[r]) + "\n"
|
||||||
|
|
||||||
|
result += "Slots:\n"
|
||||||
|
for i, slot in enumerate(self._known_slots):
|
||||||
|
result += f"{i}: {slot} Pattern: {slot_pattern(self._grid, slot)} -- "
|
||||||
|
result += f"Available Words: {len(self._available_words_for_slotindex.get(i, []))}\n"
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
|
||||||
|
dummy_words = ["apple", "banana", "grape", "orange", "melon", "kiwi", "peach", "pear", "plum", "mango",
|
||||||
|
"cherry", "berry", "lemon", "lime", "apricot", "date", "fig", "papaya", "quince", "tangerine"]
|
||||||
|
|
||||||
|
dict_obj = load_de_dictionary()
|
||||||
|
|
||||||
|
dict_obj._build_pos_index_list()
|
||||||
|
|
||||||
|
test_seeds = [42]
|
||||||
|
for seed in test_seeds:
|
||||||
|
print(f"Testing with seed {seed}")
|
||||||
|
generator = CrosswordGeneratorStep(dictionary=dict_obj,
|
||||||
|
seed=seed,
|
||||||
|
grid_width=30,
|
||||||
|
grid_height=30,
|
||||||
|
grid_block_ratio=0.4,
|
||||||
|
max_slot_length=15)
|
||||||
|
final_step = generator.generate(
|
||||||
|
max_tries_per_step=2,
|
||||||
|
show_progress=True,
|
||||||
|
max_allowed_single_choice=1,
|
||||||
|
max_allowed_threshold_choice=3,
|
||||||
|
max_allowed_threshold=5
|
||||||
|
)
|
||||||
|
if final_step is None:
|
||||||
|
print("Failed to generate crossword. Last attempt:")
|
||||||
|
print(generator)
|
||||||
|
else:
|
||||||
|
|
||||||
|
final_step.print_grid()
|
||||||
|
print("Successfully generated crossword")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
26
multiplayer_crosswords/utils.py
Normal file
26
multiplayer_crosswords/utils.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
from multiplayer_crosswords.dictionary import Dictionary, Word
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
def load_dictionary(p: str | Path) -> Dictionary:
|
||||||
|
p = Path(p)
|
||||||
|
if not p.exists():
|
||||||
|
raise FileNotFoundError(f"Dictionary file not found: {p}")
|
||||||
|
with p.open("r", encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
dict_obj = Dictionary()
|
||||||
|
|
||||||
|
for key, obj in data.items():
|
||||||
|
word = obj.get("word", "").strip()
|
||||||
|
if not word.isalpha():
|
||||||
|
continue
|
||||||
|
word = word.lower()
|
||||||
|
dict_obj.add_word(Word(word=word, hints=[], difficulty=1))
|
||||||
|
return dict_obj
|
||||||
|
|
||||||
|
def load_en_dictionary() -> Dictionary:
|
||||||
|
return load_dictionary(Path(__file__).parent.parent / "data" / "en.json")
|
||||||
|
|
||||||
|
def load_de_dictionary() -> Dictionary:
|
||||||
|
return load_dictionary(Path(__file__).parent.parent / "data" / "de.json")
|
||||||
Reference in New Issue
Block a user