finalize crossword class
This commit is contained in:
@ -1,158 +1,337 @@
|
|||||||
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
|
from multiplayer_crosswords.crossword_algorithm import CrosswordGeneratorStep, Slot, Orientation, extract_slots
|
||||||
import random
|
import random
|
||||||
|
|
||||||
class PlacedWord:
|
from multiplayer_crosswords.utils import load_de_dictionary
|
||||||
def __init__(self, word: Word, row: int, col: int, orientation: Orientation):
|
|
||||||
|
class CrosswordWord:
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
word: str,
|
||||||
|
start_x: int,
|
||||||
|
start_y: int,
|
||||||
|
orientation: Orientation,
|
||||||
|
hist: str,
|
||||||
|
index: Optional[int],
|
||||||
|
solved: bool = False,
|
||||||
|
|
||||||
|
):
|
||||||
self.word = word
|
self.word = word
|
||||||
self.row = row
|
self.start_x = start_x
|
||||||
self.col = col
|
self.start_y = start_y
|
||||||
self.orientation = orientation
|
self.orientation = orientation
|
||||||
|
self.hist = hist
|
||||||
|
self.length = len(word)
|
||||||
|
self.index: Optional[int] = index
|
||||||
|
self.solved = solved
|
||||||
|
|
||||||
|
|
||||||
class Crossword:
|
class Crossword:
|
||||||
|
|
||||||
@staticmethod
|
@property
|
||||||
def generate(
|
def solved_grid(self) -> List[List[Optional[str]]]:
|
||||||
rows: int,
|
return self._solved_grid
|
||||||
cols: int,
|
|
||||||
dictionary: Dictionary,
|
@property
|
||||||
seed: Optional[int] = None,
|
def dictionary(self) -> Dictionary:
|
||||||
block_ratio: float = 0.4,
|
return self._dictionary
|
||||||
) -> "Crossword":
|
|
||||||
generator = CrosswordGeneratorStep(
|
@property
|
||||||
|
def words(self) -> List[CrosswordWord]:
|
||||||
|
return self._words
|
||||||
|
|
||||||
|
|
||||||
|
def get_words_by_y_x_position(self, x, y) -> List[CrosswordWord]:
|
||||||
|
"""Get the list of CrosswordWord objects that start at position (x, y)."""
|
||||||
|
result = []
|
||||||
|
horizontal_cw = self._horizontal_words_by_y_x_position.get((y, x))
|
||||||
|
if horizontal_cw is not None:
|
||||||
|
result.append(horizontal_cw)
|
||||||
|
vertical_cw = self._vertical_words_by_y_x_position.get((y, x))
|
||||||
|
if vertical_cw is not None:
|
||||||
|
result.append(vertical_cw)
|
||||||
|
return result
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_serialized(cls, data: dict, dictionary: Dictionary) -> "Crossword":
|
||||||
|
"""Create a Crossword object from serialized data."""
|
||||||
|
grid = data.get("solved_grid", [])
|
||||||
|
|
||||||
|
words: List[CrosswordWord] = []
|
||||||
|
for wdata in data.get("words", []):
|
||||||
|
cw = CrosswordWord(
|
||||||
|
word=wdata.get("word"),
|
||||||
|
start_x=wdata.get("start_x"),
|
||||||
|
start_y=wdata.get("start_y"),
|
||||||
|
orientation=Orientation[wdata.get("orientation")],
|
||||||
|
hist=wdata.get("hint", ""),
|
||||||
|
index=wdata.get("index", None),
|
||||||
|
solved=wdata.get("solved", False)
|
||||||
|
)
|
||||||
|
words.append(cw)
|
||||||
|
|
||||||
|
crossword = cls(
|
||||||
dictionary=dictionary,
|
dictionary=dictionary,
|
||||||
seed=seed,
|
grid=grid,
|
||||||
grid_width=cols,
|
current_grid=data.get("current_grid", None),
|
||||||
grid_height=rows,
|
words=words
|
||||||
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
|
return crossword
|
||||||
|
|
||||||
def __init__(self, rows: int, cols: int, dictionary: Dictionary):
|
@staticmethod
|
||||||
self.rows = rows
|
def generate(
|
||||||
self.cols = cols
|
dictionary: Dictionary,
|
||||||
self.grid = [['' for _ in range(cols)] for _ in range(rows)]
|
grid_width: int = 15,
|
||||||
self.dictionary = dictionary
|
grid_height: int = 15,
|
||||||
self.placed_words: List[PlacedWord] = []
|
grid_block_ratio: float = 0.4,
|
||||||
|
seed: Optional[int] = None,
|
||||||
|
|
||||||
|
) -> "Crossword":
|
||||||
|
|
||||||
|
if seed is None:
|
||||||
|
# initialize seed based on current system time
|
||||||
|
seed = random.randint(0, 2**32 - 1)
|
||||||
|
generator = CrosswordGeneratorStep(
|
||||||
|
grid_width=grid_width,
|
||||||
|
grid_height=grid_height,
|
||||||
|
dictionary=dictionary,
|
||||||
|
grid_block_ratio=grid_block_ratio,
|
||||||
|
seed=seed
|
||||||
|
)
|
||||||
|
final_step = generator.generate(
|
||||||
|
max_tries_per_step=2,
|
||||||
|
max_allowed_single_choice=1,
|
||||||
|
max_allowed_threshold_choice=3,
|
||||||
|
max_allowed_threshold=5,
|
||||||
|
show_progress=False
|
||||||
|
)
|
||||||
|
|
||||||
|
if final_step is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return Crossword(
|
||||||
|
dictionary=dictionary,
|
||||||
|
grid=final_step.grid,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _in_bounds(self, row: int, col: int) -> bool:
|
def __init__(
|
||||||
return 0 <= row < self.rows and 0 <= col < self.cols
|
self,
|
||||||
|
dictionary: Dictionary,
|
||||||
|
grid: List[List[Optional[str]]],
|
||||||
|
current_grid: Optional[List[List[Optional[str]]]] = None,
|
||||||
|
words: Optional[List[CrosswordWord]] = None,
|
||||||
|
):
|
||||||
|
self._dictionary = dictionary
|
||||||
|
self._solved_grid = grid
|
||||||
|
self._words: List[CrosswordWord] = []
|
||||||
|
|
||||||
def can_place_word(self, word: Word, row: int, col: int, orientation: Orientation) -> bool:
|
self._horizontal_words_by_y_x_position = {}
|
||||||
length = len(word.word)
|
self._vertical_words_by_y_x_position = {}
|
||||||
dr, dc = (0, 1) if orientation == Orientation.HORIZONTAL else (1, 0)
|
|
||||||
|
|
||||||
# Check start block
|
if current_grid is not None:
|
||||||
start_r, start_c = row - dr, col - dc
|
self._current_grid = current_grid
|
||||||
if self._in_bounds(start_r, start_c):
|
|
||||||
if self.grid[start_r][start_c] not in ('', '#'):
|
|
||||||
return False
|
|
||||||
if self.grid[start_r][start_c] == '#':
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Check end block
|
else:
|
||||||
end_r, end_c = row + dr * length, col + dc * length
|
# create an empty grid (empty = all letter cells are '')
|
||||||
if self._in_bounds(end_r, end_c):
|
self._current_grid = [['' for _ in range(len(grid[0]))] for _ in range(len(grid))]
|
||||||
if self.grid[end_r][end_c] not in ('', '#'):
|
|
||||||
return False
|
|
||||||
if self.grid[end_r][end_c] == '#':
|
|
||||||
return False
|
|
||||||
|
|
||||||
for i in range(length):
|
# place walls in the current grid
|
||||||
r, c = row + dr * i, col + dc * i
|
for r in range(len(grid)):
|
||||||
if not self._in_bounds(r, c):
|
for c in range(len(grid[0])):
|
||||||
|
if grid[r][c] == '#':
|
||||||
|
self._current_grid[r][c] = '#'
|
||||||
|
|
||||||
|
if words is not None:
|
||||||
|
self._words = words
|
||||||
|
else:
|
||||||
|
self.extract_words()
|
||||||
|
|
||||||
|
if current_grid is not None:
|
||||||
|
# current grid was passed, check which words are already solved
|
||||||
|
for cw in self._words:
|
||||||
|
cw.solved = self.check_word_solved(cw)
|
||||||
|
|
||||||
|
for cw in self._words:
|
||||||
|
if cw.orientation == Orientation.HORIZONTAL:
|
||||||
|
for i in range(cw.length):
|
||||||
|
self._horizontal_words_by_y_x_position[(cw.start_y, cw.start_x + i)] = cw
|
||||||
|
else:
|
||||||
|
for i in range(cw.length):
|
||||||
|
self._vertical_words_by_y_x_position[(cw.start_y + i, cw.start_x)] = cw
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def extract_words(self):
|
||||||
|
"""Extract all fully placed words from the grid and store them in self._words.
|
||||||
|
|
||||||
|
A "placed" word is a slot (horizontal or vertical, length >= 2) where every
|
||||||
|
cell contains a letter (not '' and not '#'). For each found word we try to
|
||||||
|
retrieve a random hint from the dictionary (using Dictionary.Word.copy_with_random_hint)
|
||||||
|
and store it on the CrosswordWord.hist field (empty string if no hint found).
|
||||||
|
"""
|
||||||
|
self._words = []
|
||||||
|
|
||||||
|
# Use extract_slots to find candidate slots (min_length=2)
|
||||||
|
slots = extract_slots(self._solved_grid, min_length=2)
|
||||||
|
|
||||||
|
word_index = 1
|
||||||
|
|
||||||
|
for slot in slots:
|
||||||
|
# Build the word and check all cells are filled with letters
|
||||||
|
dr, dc = (0, 1) if slot.orientation == Orientation.HORIZONTAL else (1, 0)
|
||||||
|
chars = []
|
||||||
|
fully_filled = True
|
||||||
|
for i in range(slot.length):
|
||||||
|
r, c = slot.row + dr * i, slot.col + dc * i
|
||||||
|
cell = self._solved_grid[r][c]
|
||||||
|
# treat '#' or empty string as not filled
|
||||||
|
if not cell or cell == '#':
|
||||||
|
fully_filled = False
|
||||||
|
break
|
||||||
|
chars.append(cell)
|
||||||
|
|
||||||
|
if not fully_filled:
|
||||||
|
continue
|
||||||
|
|
||||||
|
word_str = ''.join(chars)
|
||||||
|
|
||||||
|
# Find the Word object in the dictionary (if available) to extract a random hint
|
||||||
|
hint_str = ""
|
||||||
|
try:
|
||||||
|
match = next((w for w in self._dictionary.words if w.word == word_str), None)
|
||||||
|
if match is not None:
|
||||||
|
hint_word = match.copy_with_random_hint()
|
||||||
|
hint_str = hint_word.hints[0] if hint_word.hints else ""
|
||||||
|
except Exception:
|
||||||
|
# Fallback: ignore dictionary problems and leave hint empty
|
||||||
|
|
||||||
|
hint_str = "ERROR while generating hint for word " + word_str
|
||||||
|
|
||||||
|
if len(hint_str) == 0:
|
||||||
|
hint_str = "No hint available for this word: " + word_str
|
||||||
|
|
||||||
|
# start_x/start_y: choose x=col, y=row (cartesian-like)
|
||||||
|
cw = CrosswordWord(
|
||||||
|
word=word_str,
|
||||||
|
start_x=slot.col,
|
||||||
|
start_y=slot.row,
|
||||||
|
orientation=slot.orientation,
|
||||||
|
hist=hint_str,
|
||||||
|
index=word_index,
|
||||||
|
)
|
||||||
|
word_index += 1
|
||||||
|
self._words.append(cw)
|
||||||
|
|
||||||
|
def serialize(self) -> dict:
|
||||||
|
"""Serialize the crossword to a dictionary format."""
|
||||||
|
return {
|
||||||
|
"grid_width": len(self._solved_grid[0]),
|
||||||
|
"grid_height": len(self._solved_grid),
|
||||||
|
"solved_grid": self._solved_grid,
|
||||||
|
"current_grid": self._current_grid,
|
||||||
|
"words": [
|
||||||
|
{
|
||||||
|
"word": w.word,
|
||||||
|
"start_x": w.start_x,
|
||||||
|
"start_y": w.start_y,
|
||||||
|
"orientation": w.orientation.name,
|
||||||
|
"hint": w.hist,
|
||||||
|
"length": w.length,
|
||||||
|
"solved": w.solved,
|
||||||
|
"index": w.index,
|
||||||
|
}
|
||||||
|
for w in self._words
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
def place_letter(self, y: int, x: int, letter: str) -> bool:
|
||||||
|
"""Place a letter at position (x, y) in the current grid.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
x (int): The x-coordinate (column index).
|
||||||
|
y (int): The y-coordinate (row index).
|
||||||
|
letter (str): The letter to place.
|
||||||
|
Returns:
|
||||||
|
bool: True if the letter was placed successfully, False otherwise.
|
||||||
|
"""
|
||||||
|
if y < 0 or y >= len(self._current_grid):
|
||||||
return False
|
return False
|
||||||
cell = self.grid[r][c]
|
if x < 0 or x >= len(self._current_grid[0]):
|
||||||
if cell == '#':
|
|
||||||
return False
|
|
||||||
if cell != '' and cell != word.word[i]:
|
|
||||||
return False
|
return False
|
||||||
|
if self._solved_grid[y][x] == '#':
|
||||||
|
return False # Cannot place letter on a wall
|
||||||
|
self._current_grid[y][x] = letter
|
||||||
|
|
||||||
|
horizontal_cw = self._horizontal_words_by_y_x_position.get((y, x))
|
||||||
|
if horizontal_cw is not None:
|
||||||
|
horizontal_cw.solved = self.check_word_solved(horizontal_cw)
|
||||||
|
|
||||||
|
vertical_cw = self._vertical_words_by_y_x_position.get((y, x))
|
||||||
|
if vertical_cw is not None:
|
||||||
|
vertical_cw.solved = self.check_word_solved(vertical_cw)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def add_word(self, word: Word, row: int, col: int, orientation: Orientation) -> bool:
|
def check_word_solved(self, crossword_word: CrosswordWord) -> bool:
|
||||||
if not self.can_place_word(word, row, col, orientation):
|
"""Check if the given CrosswordWord is correctly solved in the current grid.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
crossword_word (CrosswordWord): The word to check.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if the word is correctly solved, False otherwise.
|
||||||
|
"""
|
||||||
|
dr, dc = (0, 1) if crossword_word.orientation == Orientation.HORIZONTAL else (1, 0)
|
||||||
|
for i in range(crossword_word.length):
|
||||||
|
r = crossword_word.start_y + dr * i
|
||||||
|
c = crossword_word.start_x + dc * i
|
||||||
|
if self._current_grid[r][c] != crossword_word.word[i]:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
length = len(word.word)
|
|
||||||
dr, dc = (0, 1) if orientation == Orientation.HORIZONTAL else (1, 0)
|
|
||||||
|
|
||||||
# Place start block
|
|
||||||
start_r, start_c = row - dr, col - dc
|
|
||||||
if self._in_bounds(start_r, start_c):
|
|
||||||
self.grid[start_r][start_c] = '#'
|
|
||||||
|
|
||||||
# Place word
|
|
||||||
for i in range(length):
|
|
||||||
r, c = row + dr * i, col + dc * i
|
|
||||||
self.grid[r][c] = word.word[i]
|
|
||||||
|
|
||||||
# Place end block
|
|
||||||
end_r, end_c = row + dr * length, col + dc * length
|
|
||||||
if self._in_bounds(end_r, end_c):
|
|
||||||
self.grid[end_r][end_c] = '#'
|
|
||||||
|
|
||||||
self.placed_words.append(PlacedWord(word, row, col, orientation))
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return '\n'.join(
|
# Simple string representation for debugging
|
||||||
''.join(cell if cell else '.' for cell in row)
|
result = "\nCurrent Grid:\n"
|
||||||
for row in self.grid
|
for row in self._current_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
|
||||||
|
|
||||||
|
result += "\n\nSolved Grid:\n"
|
||||||
|
for row in self._solved_grid:
|
||||||
|
row_str = ' '.join(cell if cell else ' ' for cell in row).replace("#", "█") + "\n"
|
||||||
|
row_str = row_str.replace("█ █", "███").replace("█ █", "███")
|
||||||
|
result += row_str
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
# Example usage
|
# Example usage
|
||||||
dictionary = Dictionary()
|
dictionary = load_de_dictionary()
|
||||||
dictionary.add_word(Word("hello", ["greeting"], "easy"))
|
|
||||||
dictionary.add_word(Word("world", ["earth"], "easy"))
|
dictionary._build_pos_index_list()
|
||||||
|
|
||||||
|
|
||||||
|
crossword = Crossword.generate(
|
||||||
|
dictionary=dictionary,
|
||||||
|
seed=None,
|
||||||
|
grid_width=20,
|
||||||
|
grid_height=20,
|
||||||
|
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.hist}")
|
||||||
|
|
||||||
crossword = Crossword(5, 5, dictionary)
|
|
||||||
crossword.add_word(dictionary.words[0], 1, 0, Orientation.HORIZONTAL)
|
|
||||||
crossword.add_word(dictionary.words[1], 0, 4, Orientation.VERTICAL)
|
|
||||||
|
|
||||||
print(crossword)
|
print(crossword)
|
||||||
@ -218,6 +218,10 @@ def generate_grid_template(width: int, height: int, block_ratio: float = 0.25, s
|
|||||||
|
|
||||||
class CrosswordGeneratorStep(object):
|
class CrosswordGeneratorStep(object):
|
||||||
|
|
||||||
|
@property
|
||||||
|
def grid(self) -> List[List[str]]:
|
||||||
|
return self._grid
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
dictionary: Dictionary,
|
dictionary: Dictionary,
|
||||||
|
|||||||
0
multiplayer_crosswords/websocket_server.py
Normal file
0
multiplayer_crosswords/websocket_server.py
Normal file
@ -1,115 +1,91 @@
|
|||||||
import pytest
|
import pytest
|
||||||
from multiplayer_crosswords.crossword import Crossword, Orientation, PlacedWord
|
|
||||||
from unittest.mock import MagicMock
|
|
||||||
from multiplayer_crosswords.dictionary import Dictionary
|
|
||||||
from multiplayer_crosswords.utils import load_en_dictionary, load_de_dictionary
|
|
||||||
|
|
||||||
class DummyWord:
|
from multiplayer_crosswords.crossword import Crossword, CrosswordWord
|
||||||
def __init__(self, word):
|
from multiplayer_crosswords.dictionary import Dictionary, Word
|
||||||
self.word = word
|
|
||||||
|
|
||||||
class DummyDictionary:
|
|
||||||
def __init__(self, words):
|
|
||||||
self.words = words
|
|
||||||
|
|
||||||
def make_crossword(rows=5, cols=5):
|
|
||||||
dictionary = DummyDictionary([DummyWord("HELLO"), DummyWord("WORLD")])
|
|
||||||
return Crossword(rows, cols, dictionary)
|
|
||||||
|
|
||||||
def test_can_place_word_empty_grid_horizontal():
|
|
||||||
cw = make_crossword()
|
|
||||||
word = DummyWord("HELLO")
|
|
||||||
assert cw.can_place_word(word, 0, 0, Orientation.HORIZONTAL)
|
|
||||||
assert not cw.can_place_word(word, 0, 1, Orientation.HORIZONTAL) # would go out of bounds
|
|
||||||
|
|
||||||
def test_can_place_word_empty_grid_vertical():
|
|
||||||
cw = make_crossword()
|
|
||||||
word = DummyWord("HELLO")
|
|
||||||
assert cw.can_place_word(word, 0, 0, Orientation.VERTICAL)
|
|
||||||
assert not cw.can_place_word(word, 1, 0, Orientation.VERTICAL) # would go out of bounds
|
|
||||||
|
|
||||||
def test_add_word_and_grid_update():
|
|
||||||
cw = make_crossword()
|
|
||||||
word = DummyWord("HELLO")
|
|
||||||
assert cw.add_word(word, 0, 0, Orientation.HORIZONTAL)
|
|
||||||
# Check grid contents
|
|
||||||
assert cw.grid[0][0] == "H"
|
|
||||||
assert cw.grid[0][1] == "E"
|
|
||||||
assert cw.grid[0][2] == "L"
|
|
||||||
assert cw.grid[0][3] == "L"
|
|
||||||
assert cw.grid[0][4] == "O"
|
|
||||||
# Start and end blocks
|
|
||||||
assert cw.grid[0][0-0][0-1] == "#" if cw._in_bounds(0, -1) else True
|
|
||||||
assert cw.grid[0][5] == "#" if cw._in_bounds(0, 5) else True
|
|
||||||
|
|
||||||
def test_cannot_place_overlapping_different_letter():
|
|
||||||
cw = make_crossword()
|
|
||||||
word1 = DummyWord("HELLO")
|
|
||||||
word2 = DummyWord("WORLD")
|
|
||||||
assert cw.add_word(word1, 0, 0, Orientation.HORIZONTAL)
|
|
||||||
# Try to place "WORLD" vertically overlapping 'E' with 'O'
|
|
||||||
assert not cw.can_place_word(word2, 0, 1, Orientation.VERTICAL)
|
|
||||||
|
|
||||||
def test_can_place_overlapping_same_letter():
|
|
||||||
cw = make_crossword()
|
|
||||||
word1 = DummyWord("HELLO")
|
|
||||||
word2 = DummyWord("EVE")
|
|
||||||
assert cw.add_word(word1, 0, 0, Orientation.HORIZONTAL)
|
|
||||||
# "EVE" vertical, E overlaps at (0,1)
|
|
||||||
word2 = DummyWord("EVE")
|
|
||||||
assert cw.can_place_word(word2, 0, 1, Orientation.VERTICAL)
|
|
||||||
assert cw.add_word(word2, 0, 1, Orientation.VERTICAL)
|
|
||||||
|
|
||||||
def test_blocks_are_placed_correctly():
|
|
||||||
cw = make_crossword()
|
|
||||||
word = DummyWord("HI")
|
|
||||||
assert cw.add_word(word, 2, 2, Orientation.HORIZONTAL)
|
|
||||||
# Start block
|
|
||||||
assert cw.grid[2][1] == "#"
|
|
||||||
# End block
|
|
||||||
assert cw.grid[2][4] == "#"
|
|
||||||
|
|
||||||
def test_str_representation():
|
|
||||||
cw = make_crossword()
|
|
||||||
word = DummyWord("HI")
|
|
||||||
cw.add_word(word, 0, 0, Orientation.HORIZONTAL)
|
|
||||||
s = str(cw)
|
|
||||||
assert "HI" in s
|
|
||||||
assert "." in s
|
|
||||||
|
|
||||||
def test_placed_words_tracking():
|
|
||||||
cw = make_crossword()
|
|
||||||
word = DummyWord("HI")
|
|
||||||
cw.add_word(word, 1, 1, Orientation.VERTICAL)
|
|
||||||
assert len(cw.placed_words) == 1
|
|
||||||
placed = cw.placed_words[0]
|
|
||||||
assert placed.word == word
|
|
||||||
assert placed.row == 1
|
|
||||||
assert placed.col == 1
|
|
||||||
assert placed.orientation == Orientation.VERTICAL
|
|
||||||
|
|
||||||
def test_valid_hello_world_crossword():
|
|
||||||
cw = make_crossword()
|
|
||||||
assert cw.add_word(DummyWord("HELLO"), 1, 0, Orientation.HORIZONTAL)
|
|
||||||
assert cw.add_word(DummyWord("WORLD"), 0, 4, Orientation.VERTICAL)
|
|
||||||
assert str(cw).count('H') == 1
|
|
||||||
assert str(cw).count('E') == 1
|
|
||||||
assert str(cw).count('L') == 3
|
|
||||||
assert str(cw).count('O') == 1
|
|
||||||
assert str(cw).count('W') == 1
|
|
||||||
assert str(cw).count('R') == 1
|
|
||||||
assert str(cw).count('D') == 1
|
|
||||||
|
|
||||||
|
|
||||||
def test_crossword_generation():
|
def make_test_dictionary():
|
||||||
dictionary = load_de_dictionary()
|
d = Dictionary()
|
||||||
crossword = Crossword.generate(15, 15, dictionary)
|
d.add_word(Word("dog", hints=["bark"], difficulty=1.0))
|
||||||
assert crossword is not None
|
d.add_word(Word("cat", hints=["meow"], difficulty=1.0))
|
||||||
|
return d
|
||||||
|
|
||||||
# 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
|
def test_extract_words_and_positions():
|
||||||
print(crossword)
|
# small 5x5 grid with two placed words: "dog" horizontal at (0,0)
|
||||||
|
# and "cat" vertical starting at (4,1)
|
||||||
|
grid = [
|
||||||
|
["d", "o", "g", "#", "#"],
|
||||||
|
["#", "#", "#", "#", "c"],
|
||||||
|
["#", "#", "#", "#", "a"],
|
||||||
|
["#", "#", "#", "#", "t"],
|
||||||
|
["#", "#", "#", "#", "#"],
|
||||||
|
]
|
||||||
|
|
||||||
|
d = make_test_dictionary()
|
||||||
|
|
||||||
|
cw = Crossword(dictionary=d, grid=grid)
|
||||||
|
|
||||||
|
# Two words should be extracted
|
||||||
|
words = cw.words
|
||||||
|
assert len(words) == 2
|
||||||
|
|
||||||
|
words_by_text = {w.word: w for w in words}
|
||||||
|
assert "dog" in words_by_text and "cat" in words_by_text
|
||||||
|
|
||||||
|
dog = words_by_text["dog"]
|
||||||
|
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
|
||||||
|
|
||||||
|
cat = words_by_text["cat"]
|
||||||
|
assert cat.start_x == 4 and cat.start_y == 1
|
||||||
|
assert cat.orientation.name == "VERTICAL"
|
||||||
|
|
||||||
|
# get_words_by_y_x_position uses x,y ordering
|
||||||
|
by_pos = cw.get_words_by_y_x_position(0, 0)
|
||||||
|
assert any(w.word == "dog" for w in by_pos)
|
||||||
|
|
||||||
|
by_pos_cat = cw.get_words_by_y_x_position(4, 1)
|
||||||
|
assert any(w.word == "cat" for w in by_pos_cat)
|
||||||
|
|
||||||
|
|
||||||
|
def test_place_letter_and_check_solved():
|
||||||
|
grid = [
|
||||||
|
["d", "o", "g", "#"],
|
||||||
|
["#", "#", "#", "#"],
|
||||||
|
]
|
||||||
|
d = make_test_dictionary()
|
||||||
|
cw = Crossword(dictionary=d, grid=grid)
|
||||||
|
|
||||||
|
# Initially not solved
|
||||||
|
dog = next(w for w in cw.words if w.word == "dog")
|
||||||
|
assert dog.solved is False
|
||||||
|
|
||||||
|
# Place letters one by one
|
||||||
|
assert cw.place_letter(0, 0, "d") is True
|
||||||
|
assert cw.place_letter(0, 1, "o") is True
|
||||||
|
assert cw.place_letter(0, 2, "g") is True
|
||||||
|
|
||||||
|
# Now the word should be marked solved
|
||||||
|
assert dog.solved is True
|
||||||
|
|
||||||
|
# Cannot place on a wall
|
||||||
|
assert cw.place_letter(3, 0, "x") is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_serialize_and_from_serialized_roundtrip():
|
||||||
|
grid = [
|
||||||
|
["d", "o", "g", "#"],
|
||||||
|
["#", "#", "#", "#"],
|
||||||
|
]
|
||||||
|
d = make_test_dictionary()
|
||||||
|
cw = Crossword(dictionary=d, grid=grid)
|
||||||
|
|
||||||
|
data = cw.serialize()
|
||||||
|
reconstructed = Crossword.from_serialized(data, dictionary=d)
|
||||||
|
|
||||||
|
assert reconstructed is not None
|
||||||
|
assert len(reconstructed.words) == len(cw.words)
|
||||||
|
assert reconstructed.serialize()["solved_grid"] == cw.serialize()["solved_grid"]
|
||||||
|
|||||||
Reference in New Issue
Block a user