import enum import json import logging import numpy as np from numpy.lib.function_base import diff from . import crossword_generator from enum import Enum import traceback class HintOrientation(Enum): VERTICAL = 0 HORIZONTAL = 1 BOTH = 2 class FieldType(Enum): EMPTY = 0 HINT = 1 LETTER = 2 class Field(object): def __init__(self, field_type: FieldType = FieldType.EMPTY): self._field_type = field_type def get_type(self) -> FieldType: return self._field_type def get_content(self) -> str: return None def serialize(self): type_names = { FieldType.EMPTY: "empty", FieldType.HINT: "hint", FieldType.LETTER: "letter" } return { 'cell_type': type_names[self._field_type] } class HintField(Field): def __init__(self, horizontal_hint: str = None, vertical_hint: str = None): super().__init__(field_type=FieldType.HINT) self._horizontal_hint = horizontal_hint self._vertical_hint = vertical_hint def get_horizontal_hint(self) -> str: return self._horizontal_hint def get_vertical_hint(self) -> str: return self._vertical_hint def set_horizintal_hint(self, hint: str): self._horizontal_hint = hint def set_vertical_hint(self, hint: str): self._vertical_hint = hint def serialize(self): result = super().serialize() result['vertical_hint'] = self._vertical_hint result['horizontal_hint'] = self._horizontal_hint return result class LetterField(Field): def __init__(self, letter: str): assert len(letter) <= 1 super().__init__(field_type=FieldType.LETTER) self._letter = letter.lower() self._revealed = False self._user_letter = "" def get_content(self) -> str: return self._letter.upper() def get_user_content(self) -> str: return self._user_letter.upper() def reveal(self): self._revealed = True def user_input(self, input_letter): assert len(input_letter) <= 1 self._user_letter = input_letter.lower() def is_revealed(self) -> bool: return self._revealed def serialize(self): result = super().serialize() result['letter'] = self._letter return result class Grid(object): def __init__(self, width: int, height: int, lang_code: str, density=0.81, difficulty: int = 0): self._width = width self._height = height self._lang_code = lang_code self._difficulty = difficulty self._density = density self._grid = [] self._solution_locations = None try: self._build_grid() except Exception as e: logging.error("error in generation", str(e)) traceback.print_exc() def serialize(self): return [ [cell.serialize() for cell in row] for row in self._grid ] def get_status(self): status = [] for y, row in enumerate(self._grid): for x, cell in enumerate(row): if cell.get_type() == FieldType.LETTER: user_content = cell.get_user_content() if cell.is_revealed(): status.append({ 'x': x, 'y': y, 'revealed': cell.get_content() }) elif len(user_content) > 0: status.append({ 'x': x, 'y': y, 'user_input': user_content }) return status def check_and_reveal_horizontal(self, x: int, y: int) -> list: # TODO: this would be much more perfomant and elegant, if every cell would hold a reference # to it's own word^^ status_update = [] cells_to_reveal = [] x_start = x while (self._grid[y][x_start-1].get_type() == FieldType.LETTER): x_start -= 1 x_i = x_start - 1 while(x_i + 1 < self._width and self._grid[y][x_i+1].get_type() == FieldType.LETTER): x_i += 1 cell = self._grid[y][x_i] if cell.get_user_content() != cell.get_content(): return [] cells_to_reveal.append(cell) if x_start - x_i == 0: # we have a single letter, not a word return [] for i, cell in enumerate(cells_to_reveal): status_update.append({ 'x': x_start + i, 'y': y, 'revealed': cell.get_content() }) cell.reveal() return status_update def check_and_reveal_vertical(self, x: int, y: int) -> list: # TODO: this would be much more perfomant and elegant, if every cell would hold a reference # to it's own word^^ status_update = [] cells_to_reveal = [] y_start = y while (self._grid[y_start - 1][x].get_type() == FieldType.LETTER): y_start -= 1 y_i = y_start - 1 while(y_i + 1 < self._width and self._grid[y_i+1][x].get_type() == FieldType.LETTER): y_i += 1 cell = self._grid[y_i][x] if cell.get_user_content() != cell.get_content(): return [] cells_to_reveal.append(cell) if y_start - y_i == 0: # we have a single letter, not a word return [] for i, cell in enumerate(cells_to_reveal): status_update.append({ 'x': x, 'y': y_start + i, 'revealed': cell.get_content() }) cell.reveal() return status_update def check_and_reveal_word(self, x: int, y: int): grid_update = self.check_and_reveal_horizontal( x, y) + self.check_and_reveal_vertical(x, y) # check also the solution locations is_solution_cell = False for location in self._solution_locations: ly = location[0] lx = location[1] if lx == x and ly == y: is_solution_cell = True cell = self._grid[ly][lx] if cell.get_user_content() != cell.get_content(): # not solved, nothing to do, return only grid update return grid_update if not is_solution_cell: return grid_update solution_updates = [] for location in self._solution_locations: ly = location[0] lx = location[1] solution_updates.append({ 'x': lx, 'y': ly, 'revealed': self._grid[ly][lx].get_content() }) self._grid[ly][lx].reveal() return grid_update + solution_updates def user_input(self, x: int, y: int, letter: str) -> list: assert len(letter) <= 1 cell = self._grid[y][x] if cell.get_type() != FieldType.LETTER: # should not happen if the client does everything right logging.warning("try to modify wrong cell") return [] if cell.is_revealed(): # user tries to modify already revealed change, telling him it's already revealed ;) return [{ 'x': x, 'y': y, 'revealed': cell.get_content() }] letter = letter.lower() cell.user_input(letter.lower()) revealed_changes = self.check_and_reveal_word(x, y) if len(revealed_changes) == 0: return [{ 'x': x, 'y': y, 'user_input': cell.get_user_content() }] return revealed_changes def get_solution_locations(self): return self._solution_locations def _build_grid(self): raw_grid, word_infos, solution_locations = crossword_generator.create_word_grid(self._width - 1, self._height - 1, lang_code=self._lang_code, target_density=self._density, difficulty=self._difficulty) self._solution_locations = solution_locations # fix solution locations offsets for i in range(len(self._solution_locations)): self._solution_locations[i][0] += 1 self._solution_locations[i][1] += 1 # note: we will append an additional row and column, to have enough space to place hint fields self._grid = [[Field()] * self._width] # initialize with empty row for y in range(self._height - 1): row = [Field()] # initialize row with empty column for x in range(self._width - 1): raw_cell = raw_grid[y, x] if raw_cell == " ": row.append(Field()) else: row.append(LetterField(raw_cell)) self._grid.append(row) # place hint fields: for word, info in word_infos.items(): y, x = info.get_hint_location() # correct offset y += 1 x += 1 cell = self._grid[y][x] # check if we already have a hint here: if cell.get_type() == FieldType.HINT: if info.is_vertical(): cell.set_vertical_hint(info.get_hint()) else: cell.set_horizintal_hint(info.get_hint()) elif cell.get_type() == FieldType.LETTER: # edge case: a word has "eaten up" another one, skipping that case pass else: if info.is_vertical(): self._grid[y][x] = HintField(vertical_hint=info.get_hint()) else: self._grid[y][x] = HintField( horizontal_hint=info.get_hint()) class Crossword(object): def __init__(self, width: int, height: int, lang_code: str = "en", difficulty: int = 0): self._width = width self._height = height self._difficulty = difficulty self._grid = Grid(width, height, lang_code, difficulty=difficulty) def serialize(self): return { 'w': self._width, 'h': self._height, 'grid': self._grid.serialize(), 'solution': self._grid.get_solution_locations() } def user_input(self, x: int, y: int, letter: str) -> list: return self._grid.user_input(x=x, y=y, letter=letter) def get_status(self) -> list: return self._grid.get_status() if __name__ == "__main__": logging.basicConfig(level=logging.INFO) cw = Crossword(30, 30, "de") print(cw.serialize())