generate crosswords using crossword algorithm

This commit is contained in:
2025-10-05 20:38:25 +02:00
parent d9cbc85678
commit bd54ce3f37
5 changed files with 102 additions and 7 deletions

View File

@ -1,10 +1,8 @@
from enum import Enum from enum import Enum
from typing import List, Tuple, Optional from typing import List, Tuple, Optional
from multiplayer_crosswords.dictionary import Dictionary, Word from multiplayer_crosswords.dictionary import Dictionary, Word
from multiplayer_crosswords.crossword_algorithm import CrosswordGeneratorStep, Slot, Orientation
class Orientation(Enum): import random
HORIZONTAL = 1
VERTICAL = 2
class PlacedWord: class PlacedWord:
def __init__(self, word: Word, row: int, col: int, orientation: Orientation): def __init__(self, word: Word, row: int, col: int, orientation: Orientation):
@ -14,12 +12,72 @@ class PlacedWord:
self.orientation = orientation self.orientation = orientation
class Crossword: class Crossword:
@staticmethod
def generate(
rows: int,
cols: int,
dictionary: Dictionary,
seed: Optional[int] = None,
block_ratio: float = 0.4,
) -> "Crossword":
generator = CrosswordGeneratorStep(
dictionary=dictionary,
seed=seed,
grid_width=cols,
grid_height=rows,
grid_block_ratio=block_ratio
)
final_step = generator.generate(
max_tries_per_step=2,
show_progress=False,
max_allowed_single_choice=1,
max_allowed_threshold_choice=3,
max_allowed_threshold=5
)
crossword = Crossword(rows, cols, dictionary)
horizontal_slots = final_step.get_horizontal_slots()
vertical_slots = final_step.get_vertical_slots()
grid = final_step.get_grid()
horizontal_words = []
vertical_words = []
for slot in horizontal_slots:
word_str = ''.join(grid[slot.row][slot.col + i] for i in range(slot.length))
horizontal_words.append((word_str, slot.row, slot.col, Orientation.HORIZONTAL))
word_candidates = list(dictionary.find_by_pattern(word_str))
if len(word_candidates) <= 0:
raise ValueError(f"No word found for pattern '{word_str}' at ({slot.row}, {slot.col})")
# choose a candidate at random
chosen_word = random.choice(word_candidates).copy_with_random_hint()
crossword.add_word(chosen_word, slot.row, slot.col, Orientation.HORIZONTAL)
for slot in vertical_slots:
word_str = ''.join(grid[slot.row + i][slot.col] for i in range(slot.length))
vertical_words.append((word_str, slot.row, slot.col, Orientation.VERTICAL))
word_candidates = list(dictionary.find_by_pattern(word_str))
if len(word_candidates) <= 0:
raise ValueError(f"No word found for pattern '{word_str}' at ({slot.row}, {slot.col})")
chosen_word = random.choice(word_candidates).copy_with_random_hint()
crossword.add_word(chosen_word, slot.row, slot.col, Orientation.VERTICAL)
return crossword
def __init__(self, rows: int, cols: int, dictionary: Dictionary): def __init__(self, rows: int, cols: int, dictionary: Dictionary):
self.rows = rows self.rows = rows
self.cols = cols self.cols = cols
self.grid = [['' for _ in range(cols)] for _ in range(rows)] self.grid = [['' for _ in range(cols)] for _ in range(rows)]
self.dictionary = dictionary self.dictionary = dictionary
self.placed_words: List[PlacedWord] = [] self.placed_words: List[PlacedWord] = []
def _in_bounds(self, row: int, col: int) -> bool: def _in_bounds(self, row: int, col: int) -> bool:
return 0 <= row < self.rows and 0 <= col < self.cols return 0 <= row < self.rows and 0 <= col < self.cols

View File

@ -1,12 +1,16 @@
# algorithms will be implemnented here # algorithms will be implemnented here
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.dictionary import Dictionary, Word from multiplayer_crosswords.dictionary import Dictionary, Word
from multiplayer_crosswords.utils import load_en_dictionary, load_de_dictionary from multiplayer_crosswords.utils import load_en_dictionary, load_de_dictionary
from copy import copy, deepcopy from copy import copy, deepcopy
import random import random
import os import os
from enum import Enum
class Orientation(Enum):
HORIZONTAL = 1
VERTICAL = 2
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):
@ -429,6 +433,14 @@ class CrosswordGeneratorStep(object):
result += row_str result += row_str
print(result, flush=True) print(result, flush=True)
def get_horizontal_slots(self) -> List[Slot]:
return [slot for slot in self._known_slots if slot.orientation == Orientation.HORIZONTAL]
def get_vertical_slots(self) -> List[Slot]:
return [slot for slot in self._known_slots if slot.orientation == Orientation.VERTICAL]
def get_grid(self) -> List[List[str]]:
return self._grid
def __str__(self): def __str__(self):

View File

@ -8,6 +8,8 @@ import numpy as np
import cProfile import cProfile
import pstats import pstats
import random
class Word: class Word:
@ -15,6 +17,13 @@ class Word:
self.word = word self.word = word
self.hints = hints self.hints = hints
self.difficulty = difficulty self.difficulty = difficulty
def copy_with_random_hint(self) -> "Word":
if not self.hints:
return Word(self.word, [], self.difficulty)
hint = random.choice(self.hints)
return Word(self.word, [hint], self.difficulty)
def to_dict(self) -> dict[str, any]: def to_dict(self) -> dict[str, any]:
return { return {

View File

@ -1,6 +1,8 @@
import pytest import pytest
from multiplayer_crosswords.crossword import Crossword, Orientation, PlacedWord from multiplayer_crosswords.crossword import Crossword, Orientation, PlacedWord
from unittest.mock import MagicMock from unittest.mock import MagicMock
from multiplayer_crosswords.dictionary import Dictionary
from multiplayer_crosswords.utils import load_en_dictionary, load_de_dictionary
class DummyWord: class DummyWord:
def __init__(self, word): def __init__(self, word):
@ -96,4 +98,18 @@ def test_valid_hello_world_crossword():
assert str(cw).count('O') == 1 assert str(cw).count('O') == 1
assert str(cw).count('W') == 1 assert str(cw).count('W') == 1
assert str(cw).count('R') == 1 assert str(cw).count('R') == 1
assert str(cw).count('D') == 1 assert str(cw).count('D') == 1
def test_crossword_generation():
dictionary = load_de_dictionary()
crossword = Crossword.generate(15, 15, dictionary)
assert crossword is not None
# Basic checks on the generated crossword
assert len(crossword.grid) == 15
assert all(len(row) == 15 for row in crossword.grid)
assert len(crossword.placed_words) > 0
# print the crossword for visual inspection
print(crossword)

View File

@ -1,5 +1,5 @@
import pytest import pytest
from multiplayer_crosswords.crossword_generators import extract_slots, slot_pattern, Slot from multiplayer_crosswords.crossword_algorithm import extract_slots, slot_pattern, Slot
from multiplayer_crosswords.crossword import Orientation from multiplayer_crosswords.crossword import Orientation
def test_extract_slots_simple(): def test_extract_slots_simple():