From 51d061044532d4db09e8c6e8dab2be3428291b2a Mon Sep 17 00:00:00 2001 From: Jonas Weinz Date: Tue, 31 Aug 2021 13:56:29 +0200 Subject: [PATCH] introducing new grid generation --- data/better_grid_building.ipynb | 1658 +++++++++++++++++++++++++++++ data/better_grid_building2.ipynb | 1423 +++++++++++++++++++++++++ server/crossword.py | 2 +- server/crossword_generator.py | 909 ++++++++++++---- server/crossword_generator_old.py | 374 +++++++ 5 files changed, 4151 insertions(+), 215 deletions(-) create mode 100644 data/better_grid_building.ipynb create mode 100644 data/better_grid_building2.ipynb create mode 100644 server/crossword_generator_old.py diff --git a/data/better_grid_building.ipynb b/data/better_grid_building.ipynb new file mode 100644 index 0000000..8bf4555 --- /dev/null +++ b/data/better_grid_building.ipynb @@ -0,0 +1,1658 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "source": [ + "# trying to create a better grid builging algorithm" + ], + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 1, + "source": [ + "# load stuff\n", + "import json\n", + "import random\n", + "import numpy as np\n", + "from string import digits, ascii_lowercase\n", + "import pathlib\n", + "import logging\n", + "import copy" + ], + "outputs": [], + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 2, + "source": [ + "def get_difficulty_threshold(lang: str, difficulty: int):\n", + " return get_difficulty_threshold.thresholds[lang][difficulty]\n", + "\n", + "\n", + "get_difficulty_threshold.thresholds = {\n", + " 'de': {\n", + " 0: 12,\n", + " 1: 6,\n", + " 2: 0\n", + " },\n", + " 'en': {\n", + " 0: 200,\n", + " 1: 100,\n", + " 2: 10\n", + " }\n", + "}\n", + "\n", + "\n", + "def get_database(lang: str = \"en\") -> dict:\n", + " if lang not in get_database._dbs:\n", + " try:\n", + " file = __file__\n", + " except:\n", + " file = \"./.tmp\"\n", + " current_folder = pathlib.Path(file).parents[0]\n", + " db_file = str(current_folder / f\"{lang}.json\")\n", + "\n", + " logging.info(\"loading database: %s\", lang)\n", + "\n", + " with open(db_file, \"r\") as f:\n", + " db = json.load(f)\n", + " get_database._dbs[lang] = db\n", + "\n", + " logging.info(\"database loaded\")\n", + "\n", + " return get_database._dbs[lang]\n", + "\n", + "\n", + "get_database._dbs = {}\n", + "\n", + "def build_inverted_index(db):\n", + "\n", + " inverted_db = {}\n", + "\n", + " inverted_db['#'] = {}\n", + " number_db = inverted_db['#']\n", + "\n", + " for letter in ascii_lowercase:\n", + " inverted_db[letter] = {}\n", + "\n", + " for key, item in db.items():\n", + " try:\n", + " word = item['word']\n", + " norm_word = normalize_word(word)\n", + "\n", + " n = len(norm_word)\n", + "\n", + " if norm_word.isalnum():\n", + "\n", + " for i, letter in enumerate(norm_word):\n", + " letter_db = inverted_db[letter]\n", + " if i not in letter_db:\n", + " letter_db[i] = {}\n", + " letter_db_i = letter_db[i]\n", + " if n not in letter_db_i:\n", + " letter_db_i[n] = []\n", + " if n not in number_db:\n", + " number_db[n] = []\n", + " \n", + " letter_db_i[n].append(key)\n", + " number_db[n].append(key)\n", + " except:\n", + " pass\n", + " #print(\"error processing \" + word)\n", + " \n", + " return inverted_db\n", + "\n", + "def get_inverted_database(lang: str) -> dict:\n", + " if lang not in get_inverted_database._dbs:\n", + " get_inverted_database._dbs[lang] = build_inverted_index(get_database(lang))\n", + " return get_inverted_database._dbs[lang]\n", + "\n", + "get_inverted_database._dbs = {}\n", + " \n", + "\n", + "remove_digits = str.maketrans('', '', digits)\n", + "\n", + "def normalize_word(word: str):\n", + " word = word.translate(remove_digits)\n", + " return word.lower()\n", + "\n", + "def find_suitable_words(constraints: list, db: dict, inverted_db: dict):\n", + " sets = []\n", + "\n", + " n = len(constraints)\n", + " for i,letter in enumerate(constraints):\n", + " if letter == ' ':\n", + " continue\n", + " \n", + " letter_db = inverted_db[letter]\n", + " if i in letter_db:\n", + " i_list = letter_db[i]\n", + " \n", + " if not n in i_list:\n", + " return set()\n", + " \n", + " sets.append(set(i_list[n]))\n", + " \n", + " else:\n", + " return set()\n", + " \n", + " # at least one constraint must be set\n", + " if len(sets) == 0:\n", + " \n", + " # set first letter random and try again\n", + " if n in inverted_db['#']:\n", + " return inverted_db['#'][n]\n", + " return set()\n", + " \n", + " return set.intersection(*sets)\n", + " \n", + "\n" + ], + "outputs": [], + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 3, + "source": [ + "find_suitable_words(list(\" \"), get_database(\"de\"), get_inverted_database(\"de\"))" + ], + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "['du',\n", + " 'du',\n", + " 'er',\n", + " 'er',\n", + " 'hi',\n", + " 'hi',\n", + " 'bp',\n", + " 'bp',\n", + " 'ol',\n", + " 'ol',\n", + " 'ei',\n", + " 'ei',\n", + " 'iq',\n", + " 'iq',\n", + " 'we',\n", + " 'we',\n", + " 'cd',\n", + " 'cd',\n", + " 'am',\n", + " 'am',\n", + " 'ha',\n", + " 'ha',\n", + " 'ei0',\n", + " 'ei0',\n", + " 'hi0',\n", + " 'hi0',\n", + " 'hi1',\n", + " 'hi1',\n", + " 'es',\n", + " 'es',\n", + " 'ai',\n", + " 'ai',\n", + " 'aa',\n", + " 'aa',\n", + " 'ag',\n", + " 'ag',\n", + " 'ag0',\n", + " 'ag0',\n", + " 'db',\n", + " 'db',\n", + " 'fc',\n", + " 'fc',\n", + " 'ab',\n", + " 'ab',\n", + " 'ab0',\n", + " 'ab0',\n", + " 'hp',\n", + " 'hp',\n", + " 'ms',\n", + " 'ms',\n", + " 'pa',\n", + " 'pa',\n", + " 'eh',\n", + " 'eh',\n", + " 'eh1',\n", + " 'eh1',\n", + " 'fs',\n", + " 'fs',\n", + " 'it',\n", + " 'it',\n", + " 'ki',\n", + " 'ki',\n", + " 'pc',\n", + " 'pc',\n", + " 'pc0',\n", + " 'pc0',\n", + " 'um',\n", + " 'um',\n", + " 'um0',\n", + " 'um0',\n", + " 'hr',\n", + " 'hr',\n", + " 'in',\n", + " 'in',\n", + " 'im',\n", + " 'im',\n", + " 'ir',\n", + " 'ir',\n", + " 'hf',\n", + " 'hf',\n", + " 'cu',\n", + " 'cu',\n", + " 'ja',\n", + " 'ja',\n", + " 'ja0',\n", + " 'ja0',\n", + " 'ja1',\n", + " 'ja1',\n", + " 'aa0',\n", + " 'aa0',\n", + " 'aa2',\n", + " 'aa2',\n", + " 'dm',\n", + " 'dm',\n", + " 'zu',\n", + " 'zu',\n", + " 'zu1',\n", + " 'zu1',\n", + " 'zu2',\n", + " 'zu2',\n", + " 'zu3',\n", + " 'zu3',\n", + " 'zu4',\n", + " 'zu4',\n", + " 'an',\n", + " 'an',\n", + " 'an0',\n", + " 'an0',\n", + " 'so',\n", + " 'so',\n", + " 'so0',\n", + " 'so0',\n", + " 'so1',\n", + " 'so1',\n", + " 'so3',\n", + " 'so3',\n", + " 'af',\n", + " 'af',\n", + " 'lg',\n", + " 'lg',\n", + " 'nf',\n", + " 'nf',\n", + " 'nl',\n", + " 'nl',\n", + " 'da1',\n", + " 'da1',\n", + " 'wo',\n", + " 'wo',\n", + " 'wo0',\n", + " 'wo0',\n", + " 'wo1',\n", + " 'wo1',\n", + " 'ab1',\n", + " 'ab1',\n", + " 'ai0',\n", + " 'ai0',\n", + " 'dj',\n", + " 'dj',\n", + " 'on0',\n", + " 'on0',\n", + " 'ob0',\n", + " 'ob0',\n", + " 'am1',\n", + " 'am1',\n", + " 'ta',\n", + " 'ta',\n", + " 'to',\n", + " 'to',\n", + " 'kw',\n", + " 'kw',\n", + " 'dv',\n", + " 'dv',\n", + " 'dv0',\n", + " 'dv0',\n", + " 'mr',\n", + " 'mr',\n", + " 'zv',\n", + " 'zv',\n", + " 'kg',\n", + " 'kg',\n", + " 'ap',\n", + " 'ap',\n", + " 'ao',\n", + " 'ao',\n", + " 'av',\n", + " 'av',\n", + " 'ba',\n", + " 'ba',\n", + " 'bg',\n", + " 'bg',\n", + " 'bk',\n", + " 'bk',\n", + " 'br',\n", + " 'br',\n", + " 'bt',\n", + " 'bt',\n", + " 'eg0',\n", + " 'eg0',\n", + " 'eg1',\n", + " 'eg1',\n", + " 'fg0',\n", + " 'fg0',\n", + " 'fm0',\n", + " 'fm0',\n", + " 'pd',\n", + " 'pd',\n", + " 'fw',\n", + " 'fw',\n", + " 'ra',\n", + " 'ra',\n", + " 'qm0',\n", + " 'qm0',\n", + " 'rg',\n", + " 'rg',\n", + " 'wm',\n", + " 'wm',\n", + " 'sg',\n", + " 'sg',\n", + " 'va',\n", + " 'va',\n", + " 'vg',\n", + " 'vg',\n", + " 'vz',\n", + " 'vz',\n", + " 'wd',\n", + " 'wd',\n", + " 'wp',\n", + " 'wp',\n", + " 'zh',\n", + " 'zh',\n", + " 'ct',\n", + " 'ct',\n", + " 'dg',\n", + " 'dg',\n", + " 'dh',\n", + " 'dh',\n", + " 'dt',\n", + " 'dt',\n", + " 'ep',\n", + " 'ep',\n", + " 'hk',\n", + " 'hk',\n", + " 'km',\n", + " 'km',\n", + " 'kp',\n", + " 'kp',\n", + " 'mk',\n", + " 'mk',\n", + " 'mm',\n", + " 'mm',\n", + " 'og',\n", + " 'og',\n", + " 'ot',\n", + " 'ot',\n", + " 'rh',\n", + " 'rh',\n", + " 'as0',\n", + " 'as0',\n", + " 'as1',\n", + " 'as1',\n", + " 'be',\n", + " 'be',\n", + " 'es2',\n", + " 'es2',\n", + " 'fr',\n", + " 'fr',\n", + " 'mo',\n", + " 'mo',\n", + " 'os',\n", + " 'os',\n", + " 'os0',\n", + " 'os0',\n", + " 'po',\n", + " 'po',\n", + " 'po0',\n", + " 'po0',\n", + " 'ra0',\n", + " 'ra0',\n", + " 're0',\n", + " 're0',\n", + " 'tb',\n", + " 'tb',\n", + " 'np',\n", + " 'np',\n", + " 'ps',\n", + " 'ps',\n", + " 'sv',\n", + " 'sv',\n", + " 'xl',\n", + " 'xl',\n", + " 'bw',\n", + " 'bw',\n", + " 'bh',\n", + " 'bh',\n", + " 'ss',\n", + " 'ss',\n", + " 'sw',\n", + " 'sw',\n", + " 'ac',\n", + " 'ac',\n", + " 'ak',\n", + " 'ak',\n", + " 'aw',\n", + " 'aw',\n", + " 'az',\n", + " 'az',\n", + " 'bb',\n", + " 'bb',\n", + " 'bl',\n", + " 'bl',\n", + " 'bm',\n", + " 'bm',\n", + " 'bs',\n", + " 'bs',\n", + " 'bz',\n", + " 'bz',\n", + " 'cb',\n", + " 'cb',\n", + " 'ww',\n", + " 'ww',\n", + " 'di',\n", + " 'di',\n", + " 'do0',\n", + " 'do0',\n", + " 'rg0',\n", + " 'rg0',\n", + " 'ho0',\n", + " 'ho0',\n", + " 'tv',\n", + " 'tv',\n", + " 'oh',\n", + " 'oh',\n", + " 'nt',\n", + " 'nt',\n", + " 'wc',\n", + " 'wc',\n", + " 'mp',\n", + " 'mp',\n", + " 'na',\n", + " 'na',\n", + " 'pk',\n", + " 'pk',\n", + " 'op',\n", + " 'op',\n", + " 'sp',\n", + " 'sp',\n", + " 'ka',\n", + " 'ka',\n", + " 'kp0',\n", + " 'kp0',\n", + " 'rm',\n", + " 'rm',\n", + " 'lw',\n", + " 'lw',\n", + " 'mw',\n", + " 'mw',\n", + " 'pp',\n", + " 'pp',\n", + " 'sb',\n", + " 'sb',\n", + " 'nu1',\n", + " 'nu1',\n", + " 'gz',\n", + " 'gz',\n", + " 'ae0',\n", + " 'ae0',\n", + " 'at',\n", + " 'at',\n", + " 'be0',\n", + " 'be0',\n", + " 'pi',\n", + " 'pi',\n", + " 'vn',\n", + " 'vn',\n", + " 'ss0',\n", + " 'ss0',\n", + " 'ss1',\n", + " 'ss1',\n", + " 'kp1',\n", + " 'kp1',\n", + " 'sz',\n", + " 'sz',\n", + " 'hm',\n", + " 'hm',\n", + " 'ld',\n", + " 'ld',\n", + " 'wu',\n", + " 'wu',\n", + " 'kv',\n", + " 'kv',\n", + " 'vu',\n", + " 'vu',\n", + " 'za',\n", + " 'za',\n", + " 'wg',\n", + " 'wg',\n", + " 'tg',\n", + " 'tg',\n", + " 'ja2',\n", + " 'ja2',\n", + " 'mc',\n", + " 'mc',\n", + " 'ur',\n", + " 'ur',\n", + " 'id',\n", + " 'id',\n", + " 'my',\n", + " 'my',\n", + " 'kl',\n", + " 'kl',\n", + " 'sd',\n", + " 'sd',\n", + " 'md',\n", + " 'md',\n", + " 'im0',\n", + " 'im0',\n", + " 'dw',\n", + " 'dw',\n", + " 'ny',\n", + " 'ny',\n", + " 'nv',\n", + " 'nv',\n", + " 'dz0',\n", + " 'dz0',\n", + " 'et',\n", + " 'et',\n", + " 'nn',\n", + " 'nn',\n", + " 'ff',\n", + " 'ff',\n", + " 'ff0',\n", + " 'ff0',\n", + " 'vv',\n", + " 'vv',\n", + " 'gr',\n", + " 'gr',\n", + " 'ig',\n", + " 'ig',\n", + " 'vp',\n", + " 'vp',\n", + " 'do1',\n", + " 'do1',\n", + " 'da2',\n", + " 'da2',\n", + " 'dk',\n", + " 'dk',\n", + " 'dp',\n", + " 'dp',\n", + " 'de',\n", + " 'de',\n", + " 'ec',\n", + " 'ec',\n", + " 'er2',\n", + " 'er2',\n", + " 'ga',\n", + " 'ga',\n", + " 'gd',\n", + " 'gd',\n", + " 'ug',\n", + " 'ug',\n", + " 'mg0',\n", + " 'mg0',\n", + " 'ic',\n", + " 'ic',\n", + " 'rb',\n", + " 'rb',\n", + " 'gb',\n", + " 'gb',\n", + " 'zr',\n", + " 'zr',\n", + " 'ns',\n", + " 'ns',\n", + " 'kr',\n", + " 'kr',\n", + " 'hz',\n", + " 'hz',\n", + " 'nf0',\n", + " 'nf0',\n", + " 'sm0',\n", + " 'sm0',\n", + " 'en',\n", + " 'en',\n", + " 'qi',\n", + " 'qi',\n", + " 'gv',\n", + " 'gv',\n", + " 'uz0',\n", + " 'uz0',\n", + " 'kh',\n", + " 'kh',\n", + " 'gf',\n", + " 'gf',\n", + " 'tk',\n", + " 'tk',\n", + " 'hc',\n", + " 'hc',\n", + " 'zk',\n", + " 'zk',\n", + " 'lb',\n", + " 'lb',\n", + " 'wr',\n", + " 'wr',\n", + " 'st',\n", + " 'st',\n", + " 'te',\n", + " 'te',\n", + " 'hb',\n", + " 'hb',\n", + " 'vj',\n", + " 'vj',\n", + " 'us',\n", + " 'us',\n", + " 'on1',\n", + " 'on1',\n", + " 'on2',\n", + " 'on2',\n", + " 'vr',\n", + " 'vr']" + ] + }, + "metadata": {}, + "execution_count": 3 + } + ], + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 4, + "source": [ + "class NoDataException(Exception):\n", + " pass\n", + "\n", + "\n", + "class WordInfo(object):\n", + " def __init__(self, word: str, y: int, x: int, is_vertical: bool, database: dict, opposite_prefix: str = \"opposite of\", synonym_prefix: str = \"other word for\"):\n", + " self._dictionary_database = database\n", + " self._y = y\n", + " self._x = x\n", + " self._word = word\n", + " self._hint = None\n", + " self._is_vertical = is_vertical\n", + "\n", + " self.opposite_prefix = opposite_prefix\n", + " self.synonym_prefix = synonym_prefix\n", + "\n", + " self.choose_info()\n", + "\n", + " def get_attribute(self, attr: str):\n", + " attr = self._dictionary_database[self._word][attr]\n", + " if attr is None or len(attr) == 0:\n", + " raise NoDataException\n", + " return attr\n", + "\n", + " def get_best_antonym(self) -> str:\n", + " antonyms = self.get_attribute(\"antonyms\")\n", + " return random.choice(antonyms)\n", + "\n", + " def get_best_synonym(self) -> str:\n", + " synonyms = self.get_attribute(\"synonyms\")\n", + " return random.choice(synonyms)\n", + "\n", + " def get_best_sense(self) -> str:\n", + " senses = self.get_attribute(\"senses\")\n", + " return random.choice(senses)\n", + "\n", + " def choose_info(self, n: int = 1):\n", + " assert n <= 4\n", + " # first choose antonyms, then synonyms, then senses\n", + "\n", + " hints = []\n", + "\n", + " try:\n", + " antonyms = self.get_attribute(\"antonyms\")\n", + " antonyms = [f\"{self.opposite_prefix} {w}\" for w in antonyms]\n", + " hints = hints + antonyms\n", + " except NoDataException:\n", + " pass\n", + "\n", + " try:\n", + " synonyms = self.get_attribute(\"synonyms\")\n", + " synonyms = [f\"{self.synonym_prefix} {w}\" for w in synonyms]\n", + "\n", + " hints = hints + synonyms\n", + " except NoDataException:\n", + " pass\n", + "\n", + " try:\n", + " senses = self.get_attribute(\"senses\")\n", + " hints = hints + senses\n", + " except NoDataException:\n", + " pass\n", + "\n", + " final_hints = []\n", + " for i in range(n):\n", + " choice = random.choice(hints)\n", + " hints.remove(choice)\n", + " final_hints.append(choice)\n", + "\n", + " if n == 1:\n", + " self._hint = final_hints[0]\n", + " return\n", + "\n", + " hint_symbols = ['a)', 'b)', 'c)', 'd)']\n", + "\n", + " self._hint = \"\"\n", + " for i in range(n):\n", + " self._hint += hint_symbols[i] + \" \" + final_hints[i] + \". \"\n", + "\n", + " def get_hint(self) -> str:\n", + " return self._hint\n", + "\n", + " def get_hint_location(self):\n", + " x = self._x if self._is_vertical else self._x - 1\n", + " y = self._y - 1 if self._is_vertical else self._y\n", + " return (y, x)\n", + "\n", + " def is_vertical(self):\n", + " return self._is_vertical\n" + ], + "outputs": [], + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 64, + "source": [ + "def create_base_grid(n:int, min_word_length: int = 4, max_word_length: int = 15, filter_length: int = 2):\n", + " w = n\n", + " h = n\n", + " horizontal_grid = np.full(shape=(h,w), dtype=np.bool, fill_value=False)\n", + " vertical_grid = np.full(shape=(h,w), dtype=np.bool, fill_value=False)\n", + "\n", + "\n", + " for y in range(h):\n", + " x = 0\n", + " while x < w:\n", + " rand_max = random.randint(min_word_length, max_word_length)\n", + " rand_length = random.randint(min_word_length, rand_max)#\n", + "\n", + " x_next = min(x + rand_length, w)\n", + " horizontal_grid[y,x:x_next] = True\n", + " x = x_next + 1\n", + " \n", + " for x in range(w):\n", + " y = 0\n", + " while y < h:\n", + " rand_max = random.randint(min_word_length, max_word_length)\n", + " rand_length = random.randint(min_word_length, rand_max)\n", + " y_next = min(y + rand_length, h)\n", + " vertical_grid[y:y_next, x] = True\n", + " y = y_next + 1\n", + " \n", + "\n", + " \n", + " for y in range(0,h,2):\n", + " horizontal_grid[y,0] = False\n", + " horizontal_grid[y,-1] = False\n", + "\n", + " \n", + " \n", + " for x in range(2,w,2):\n", + " vertical_grid[0,x] = False\n", + " vertical_grid[-1,x] = False\n", + "\n", + " for y in range(2,h,2):\n", + " for x in range(2,w,2):\n", + " if random.randint(0,3) == 0:\n", + " horizontal_grid[y,x] = False\n", + "\n", + "\n", + "\n", + " combined = np.logical_and(horizontal_grid, vertical_grid)\n", + "\n", + " #combined = np.logical_and(horizontal_grid, horizontal_grid.transpose())\n", + " \n", + "\n", + "\n", + " # clean up areas that are to small\n", + " old_combined = combined.copy()\n", + " first_try = True\n", + "\n", + " while np.sum(old_combined.astype(int)) != np.sum(combined.astype(int)) or first_try:\n", + "\n", + " first_try = False\n", + "\n", + " count_x = np.full(shape=(h,w), dtype=np.int, fill_value=0)\n", + " count_y = np.full(shape=(h,w), dtype=np.int, fill_value=0)\n", + "\n", + " for x in range(w):\n", + " last_y = 0\n", + " for y in range(h):\n", + " if not combined[y,x] and last_y != y: \n", + " count_y[last_y:y,x] = y - last_y\n", + " last_y = y + 1\n", + " \n", + " if last_y < h:\n", + " count_y[last_y:h,x] = h - last_y\n", + " \n", + " for y in range(h):\n", + " last_x = 0\n", + " for x in range(w):\n", + " if not combined[y,x] and last_x != x: \n", + " count_x[y,last_x:x] = x - last_x\n", + " last_x = x + 1\n", + " \n", + " if last_x < w:\n", + " count_x[y, last_x:w] = w - last_x\n", + "\n", + " count_mask = np.logical_and(count_x < filter_length, count_y < filter_length)\n", + " old_combined = combined.copy()\n", + " combined[count_mask] = False\n", + "\n", + "\n", + " # clean up areas that are too small\n", + "\n", + " reachable = np.full(shape=(h,w), dtype=np.bool, fill_value=False)\n", + "\n", + " old_reachable = reachable.copy()\n", + "\n", + " reachable[1,0] = True\n", + " reachable[0,1] = True\n", + "\n", + " \n", + " i = 0\n", + " while np.sum(old_reachable.astype(int)) != np.sum(reachable.astype(int)):\n", + " old_reachable = reachable.copy()\n", + " i += 1\n", + " for y in range(h):\n", + " for x in range(w):\n", + " if x > 0 and (reachable[y,x-1] and combined[y,x-1]) or x < w-1 and (reachable[y,x+1] and combined[y,x+1]):\n", + " reachable[y,x] = True\n", + " continue\n", + " if y > 0 and (reachable[y-1,x] and combined[y-1,x]) or y < h-1 and (reachable[y+1,x] and combined[y+1,x]):\n", + " reachable[y,x] = True\n", + " \n", + " #print(\"flooded after \" + str(i) + \" iterations\")\n", + " \n", + " combined = np.logical_and(combined, reachable)\n", + "\n", + " return combined\n" + ], + "outputs": [], + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 6, + "source": [ + "def create_base_grid(n: int):\n", + " grid = np.full(shape=(n,n), dtype=np.bool, fill_value=True)\n", + " for y in range(n):\n", + " for x in range(n):\n", + " if y % 2 == 1 and x % 2 == 1 and not random.randint(0,8) == 0:\n", + " grid[y,x] = False\n", + " \n", + " else:\n", + " if y % 2 != x % 2:\n", + " if random.randint(0,3) == 0:\n", + " grid[y,x] = False\n", + " else:\n", + " if random.randint(0,8) == 0:\n", + " pass\n", + " #grid[y,x] = False\n", + "\n", + " return grid\n", + " #return np.logical_and(grid, grid.transpose())\n", + "\n", + " " + ], + "outputs": [], + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 65, + "source": [ + "%%prun\n", + "import matplotlib.pyplot as plt\n", + "\n", + "base_grid = create_base_grid(20)" + ], + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + " " + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + " 2317 function calls in 0.008 seconds\n", + "\n", + " Ordered by: internal time\n", + "\n", + " ncalls tottime percall cumtime percall filename:lineno(function)\n", + " 1 0.006 0.006 0.008 0.008 :1(create_base_grid)\n", + " 18 0.001 0.000 0.001 0.000 {method 'copy' of 'numpy.ndarray' objects}\n", + " 311 0.000 0.000 0.001 0.000 random.py:291(randrange)\n", + " 311 0.000 0.000 0.000 0.000 random.py:238(_randbelow_with_getrandbits)\n", + " 36 0.000 0.000 0.000 0.000 {method 'astype' of 'numpy.ndarray' objects}\n", + " 36 0.000 0.000 0.000 0.000 {method 'reduce' of 'numpy.ufunc' objects}\n", + " 311 0.000 0.000 0.001 0.000 random.py:335(randint)\n", + " 36 0.000 0.000 0.000 0.000 fromnumeric.py:70(_wrapreduction)\n", + " 1 0.000 0.000 0.008 0.008 {built-in method builtins.exec}\n", + " 36 0.000 0.000 0.000 0.000 fromnumeric.py:2105(sum)\n", + " 43 0.000 0.000 0.001 0.000 {built-in method numpy.core._multiarray_umath.implement_array_function}\n", + " 541 0.000 0.000 0.000 0.000 {method 'getrandbits' of '_random.Random' objects}\n", + " 36 0.000 0.000 0.001 0.000 <__array_function__ internals>:2(sum)\n", + " 115 0.000 0.000 0.000 0.000 {built-in method builtins.min}\n", + " 311 0.000 0.000 0.000 0.000 {method 'bit_length' of 'int' objects}\n", + " 36 0.000 0.000 0.000 0.000 fromnumeric.py:71()\n", + " 7 0.000 0.000 0.000 0.000 numeric.py:268(full)\n", + " 7 0.000 0.000 0.000 0.000 {built-in method numpy.empty}\n", + " 7 0.000 0.000 0.000 0.000 <__array_function__ internals>:2(copyto)\n", + " 1 0.000 0.000 0.008 0.008 :1()\n", + " 36 0.000 0.000 0.000 0.000 {built-in method builtins.isinstance}\n", + " 36 0.000 0.000 0.000 0.000 fromnumeric.py:2100(_sum_dispatcher)\n", + " 36 0.000 0.000 0.000 0.000 {method 'items' of 'dict' objects}\n", + " 7 0.000 0.000 0.000 0.000 multiarray.py:1043(copyto)\n", + " 1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}" + ] + } + ], + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 66, + "source": [ + "\n", + "\n", + "\n", + "plt.imshow(base_grid)\n", + "plt.colorbar()\n", + "plt.show()\n" + ], + "outputs": [ + { + "output_type": "display_data", + "data": { + "text/plain": [ + "
" + ], + "image/svg+xml": "\n\n\n\n \n \n \n \n 2021-08-23T14:04:57.641931\n image/svg+xml\n \n \n Matplotlib v3.3.4, https://matplotlib.org/\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n", + "image/png": "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" + }, + "metadata": { + "needs_background": "light" + } + } + ], + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 112, + "source": [ + "class GridWord(object):\n", + "\n", + " @classmethod\n", + " def from_base_grid(cls, base_grid: np.ndarray, min_word_length = 2) -> list:\n", + "\n", + " grid_words = []\n", + " # a word starts after each non-letter field, and at the beginning if there is\n", + " # a letter field at the beginning of a row/column\n", + " h, w = base_grid.shape\n", + "\n", + " # first get all horizontal words\n", + " for y in range(h):\n", + " word_start = 0\n", + " for x in range(w):\n", + " if not base_grid[y, x]:\n", + " if word_start < x:\n", + " word = GridWord(\n", + " y=y,\n", + " x=word_start,\n", + " is_vertical=False,\n", + " length=x - word_start\n", + " )\n", + " if word.length >= min_word_length:\n", + " grid_words.append(word)\n", + " \n", + " word_start = x + 1\n", + "\n", + " if word_start < w:\n", + " word = GridWord(\n", + " y=y,\n", + " x=word_start,\n", + " is_vertical=False,\n", + " length=w - word_start\n", + " )\n", + " if word.length >= min_word_length:\n", + " grid_words.append(word)\n", + " \n", + " # then get all vertical word slots\n", + " for x in range(w):\n", + " word_start = 0\n", + " for y in range(h):\n", + " if not base_grid[y, x]:\n", + " if word_start < y:\n", + " word = GridWord(\n", + " y=word_start,\n", + " x=x,\n", + " is_vertical=True,\n", + " length=y - word_start\n", + " )\n", + "\n", + " if word.length >= min_word_length:\n", + " grid_words.append(word)\n", + " \n", + " word_start = y + 1\n", + "\n", + " if word_start < h:\n", + " word = GridWord(\n", + " y=word_start,\n", + " x=x,\n", + " is_vertical=True,\n", + " length=h - word_start\n", + " )\n", + " if word.length >= min_word_length:\n", + " grid_words.append(word)\n", + " \n", + " for i in range(len(grid_words)):\n", + " grid_words[i].id = i\n", + " for j in range(i):\n", + " word_a = grid_words[i]\n", + " word_b = grid_words[j]\n", + "\n", + " if word_a.check_connected(word_b):\n", + " word_a.connect_word(word_b)\n", + " word_b.connect_word(word_a)\n", + "\n", + " return grid_words\n", + "\n", + " def __init__(self, y: int, x: int, is_vertical: bool, length: int, id: int = None) -> None:\n", + " self.x = x\n", + " self.y = y\n", + " self.is_vertical = is_vertical\n", + " self.length = length\n", + " self.word_key = None\n", + " self.connected_words = []\n", + " self.id = id\n", + "\n", + " self.candidate_cache = []\n", + " self.candidate_cache_key = \"\"\n", + " \n", + " \n", + " def get_letters(self, letter_grid: np.ndarray) -> list:\n", + " if self.is_vertical:\n", + " return letter_grid[self.y:self.y+self.length, self.x].flatten()\n", + " return letter_grid[self.y, self.x: self.x + self.length].flatten()\n", + " \n", + " def set_letters(self, letters, letter_grid: np.ndarray):\n", + " if self.is_vertical:\n", + " letter_grid[self.y:self.y + self.length, self.x] = letters\n", + " else:\n", + " letter_grid[self.y, self.x: self.x + self.length] = letters\n", + " \n", + " def set_word_key(self, word_key:str):\n", + " self.word_key = word_key\n", + " \n", + " def connect_word(self, grid_word):\n", + " self.connected_words.append(grid_word)\n", + " \n", + " \n", + " \n", + " def check_connected(self, grid_word):\n", + " if self.is_vertical == grid_word.is_vertical:\n", + " return False\n", + " \n", + " if self.is_vertical:\n", + " if self.y > grid_word.y:\n", + " return False\n", + " if self.y + self.length <= grid_word.y:\n", + " return False\n", + " \n", + " if self.x >= grid_word.x + grid_word.length:\n", + " return False\n", + " \n", + " if self.x < grid_word.x:\n", + " return False\n", + " \n", + " else:\n", + " if self.x > grid_word.x:\n", + " return False\n", + " if self.x + self.length <= grid_word.x:\n", + " return False\n", + " if self.y >= grid_word.y + grid_word.length:\n", + " return False\n", + " if self.y < grid_word.y:\n", + " return False\n", + " \n", + " return True\n", + " \n", + " def get_candidates(self, letter_grid, db, inverted_db):\n", + " letters = self.get_letters(letter_grid)\n", + " word = \"\".join(letters) \n", + " if word == self.candidate_cache_key:\n", + " return self.candidate_cache\n", + " self.candidate_cache = find_suitable_words(letters, db, inverted_db)\n", + " self.candidate_cache_key = word\n", + " return self.candidate_cache\n", + " \n", + " def get_connected_words(self):\n", + " return self.connected_words\n", + "\n", + " \n", + "\n", + "\n", + "class GridCreationState(object):\n", + " def __init__(self, base_grid, db, inverted_db, letter_grid = None, grid_words = None, final_grid_words = None, used_ids = None) -> None:\n", + " self.base_grid = base_grid\n", + " self.shape = base_grid.shape\n", + " self.db = db\n", + " self.inverted_db = inverted_db\n", + " \n", + "\n", + " if letter_grid is None:\n", + " self.letter_grid = np.full(shape = self.shape, fill_value=' ', dtype=np.unicode)\n", + " else:\n", + " self.letter_grid = letter_grid\n", + " \n", + " if grid_words is None:\n", + " self.grid_words = GridWord.from_base_grid(base_grid=base_grid)\n", + " else:\n", + " self.grid_words = grid_words\n", + "\n", + " \n", + " if final_grid_words is None:\n", + " self.final_grid_words = []\n", + " else:\n", + " self.final_grid_words = final_grid_words\n", + " \n", + " if used_ids is None:\n", + " self.used_ids = [[False]] * len(self.grid_words)\n", + " else:\n", + " self.used_ids = used_ids\n", + " \n", + "\n", + "\n", + " def copy(self):\n", + " return GridCreationState(self.base_grid, self.db, self.inverted_db, self.letter_grid.copy(), self.grid_words.copy(), self.final_grid_words.copy(), self.used_ids.copy())\n", + " \n", + "\n", + " def set_random_word(self, last_word = None, n_retries = 1, max_preselected = 10):\n", + " # choose random word\n", + "\n", + " preselected_id = None\n", + " preselected_candidates = []\n", + "\n", + " for word in self.final_grid_words:\n", + " connected_words = word.get_connected_words()\n", + " #random.shuffle(connected_words)\n", + " for connected_word in connected_words:\n", + " if not self.used_ids[connected_word.id]:\n", + " preselected_id = connected_word.id\n", + " for i in range(preselected_id):\n", + " if self.used_ids[i]:\n", + " preselected_id -= 1\n", + " preselected_candidates.append(preselected_id)\n", + " \n", + " \n", + " if len(preselected_candidates) > 0:\n", + " random.shuffle(preselected_candidates)\n", + " break\n", + " \n", + " \n", + "\n", + "\n", + " n_tries = 1\n", + " if preselected_id is not None:\n", + " n_tries = min(len(preselected_candidates), max_preselected)\n", + " else:\n", + " random_index = random.randint(0, len(self.grid_words) - 1)\n", + " \n", + " for i in range(n_tries):\n", + " if preselected_id is not None:\n", + " random_index = preselected_candidates[i]\n", + " grid_word = self.grid_words[random_index]\n", + " \n", + " candidates_raw = list(grid_word.get_candidates(self.letter_grid, self.db, self.inverted_db))\n", + " \n", + " \n", + " if len(candidates_raw) > 0:\n", + " for j in range(min(n_retries, len(candidates_raw))):\n", + " word_key = candidates_raw[j]\n", + " word = normalize_word(self.db[word_key]['word'])\n", + "\n", + " new_grid = self.letter_grid.copy()\n", + "\n", + " grid_word.set_letters(list(word), new_grid)\n", + "\n", + " for word in grid_word.get_connected_words():\n", + " if word.id not in self.used_ids:\n", + " n_cands = len(word.get_candidates(new_grid, self.db, self.inverted_db))\n", + " if n_cands == 0:\n", + " return None\n", + "\n", + "\n", + " self.letter_grid = new_grid\n", + " grid_word.set_word_key(word_key)\n", + " self.used_ids[grid_word.id] = True\n", + " self.final_grid_words.append(grid_word)\n", + " del(self.grid_words[random_index])\n", + "\n", + " return grid_word\n", + " \n", + " return None\n", + " \n", + " \n", + " def fill(self, depth = 0, last_word = None):\n", + " if depth % 5 == 0:\n", + " print(\"depth\", depth)\n", + "\n", + " n_retries = 100 if depth == 0 else 2\n", + " if len(self.grid_words) == 0:\n", + " return self\n", + "\n", + "\n", + " \n", + " state_copy = self.copy()\n", + " grid_word = None\n", + " for _ in range(n_retries):\n", + " \n", + " \n", + " grid_word = state_copy.set_random_word(last_word = last_word, n_retries=20, max_preselected = 2)\n", + " if grid_word is not None:\n", + " \n", + " final_state = state_copy.fill(\n", + " depth = depth + 1,\n", + " last_word = grid_word)\n", + " if final_state is not None:\n", + " return final_state\n", + " return None\n", + "\n", + " \n", + " \n", + "\n", + "\n", + "\n", + " \n" + ], + "outputs": [], + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 114, + "source": [ + "%%prun\n", + "base_grid = create_base_grid(14, min_word_length=3, max_word_length=12)\n", + "\n", + "db = get_database(\"de\")\n", + "inverted_db = get_inverted_database(\"de\")\n", + "\n", + "grid_state = GridCreationState(base_grid, db, inverted_db)\n", + "\n", + "final_state = grid_state.fill()\n", + "\n", + "print(final_state is not None)" + ], + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "depth 0\n", + "True\n", + " " + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + " 1490 function calls (1488 primitive calls) in 0.004 seconds\n", + "\n", + " Ordered by: internal time\n", + "\n", + " ncalls tottime percall cumtime percall filename:lineno(function)\n", + " 1 0.002 0.002 0.003 0.003 :1(create_base_grid)\n", + " 14 0.000 0.000 0.000 0.000 {method 'astype' of 'numpy.ndarray' objects}\n", + " 14 0.000 0.000 0.000 0.000 {method 'reduce' of 'numpy.ufunc' objects}\n", + " 1 0.000 0.000 0.000 0.000 :3(from_base_grid)\n", + " 184 0.000 0.000 0.000 0.000 random.py:238(_randbelow_with_getrandbits)\n", + " 184 0.000 0.000 0.000 0.000 random.py:291(randrange)\n", + " 2 0.000 0.000 0.000 0.000 :188(set_random_word)\n", + " 2 0.000 0.000 0.000 0.000 :92(find_suitable_words)\n", + " 1 0.000 0.000 0.004 0.004 {built-in method builtins.exec}\n", + " 7 0.000 0.000 0.000 0.000 socket.py:438(send)\n", + " 11 0.000 0.000 0.000 0.000 {method 'copy' of 'numpy.ndarray' objects}\n", + " 3/1 0.000 0.000 0.001 0.001 :254(fill)\n", + " 184 0.000 0.000 0.000 0.000 random.py:335(randint)\n", + " 22 0.000 0.000 0.000 0.000 {built-in method numpy.core._multiarray_umath.implement_array_function}\n", + " 14 0.000 0.000 0.000 0.000 fromnumeric.py:70(_wrapreduction)\n", + " 14 0.000 0.000 0.000 0.000 fromnumeric.py:2105(sum)\n", + " 3 0.000 0.000 0.000 0.000 {method 'join' of 'str' objects}\n", + " 315 0.000 0.000 0.000 0.000 {method 'getrandbits' of '_random.Random' objects}\n", + " 7 0.000 0.000 0.000 0.000 iostream.py:195(schedule)\n", + " 14 0.000 0.000 0.000 0.000 <__array_function__ internals>:2(sum)\n", + " 1 0.000 0.000 0.004 0.004 :1()\n", + " 6 0.000 0.000 0.000 0.000 iostream.py:384(write)\n", + " 75 0.000 0.000 0.000 0.000 {built-in method builtins.min}\n", + " 3 0.000 0.000 0.000 0.000 :154(__init__)\n", + " 3 0.000 0.000 0.000 0.000 :138(get_candidates)\n", + " 8 0.000 0.000 0.000 0.000 numeric.py:268(full)\n", + " 8 0.000 0.000 0.000 0.000 {built-in method numpy.empty}\n", + " 1 0.000 0.000 0.000 0.000 {method 'intersection' of 'set' objects}\n", + " 184 0.000 0.000 0.000 0.000 {method 'bit_length' of 'int' objects}\n", + " 8 0.000 0.000 0.000 0.000 <__array_function__ internals>:2(copyto)\n", + " 3 0.000 0.000 0.000 0.000 {method 'flatten' of 'numpy.ndarray' objects}\n", + " 2 0.000 0.000 0.000 0.000 :96(set_letters)\n", + " 2 0.000 0.000 0.000 0.000 {built-in method builtins.print}\n", + " 14 0.000 0.000 0.000 0.000 fromnumeric.py:71()\n", + " 3 0.000 0.000 0.000 0.000 :91(get_letters)\n", + " 7 0.000 0.000 0.000 0.000 :78(__init__)\n", + " 7 0.000 0.000 0.000 0.000 threading.py:1093(is_alive)\n", + " 2 0.000 0.000 0.000 0.000 :184(copy)\n", + " 6 0.000 0.000 0.000 0.000 iostream.py:308(_is_master_process)\n", + " 20 0.000 0.000 0.000 0.000 {built-in method builtins.isinstance}\n", + " 7 0.000 0.000 0.000 0.000 iostream.py:91(_event_pipe)\n", + " 7 0.000 0.000 0.000 0.000 threading.py:1039(_wait_for_tstate_lock)\n", + " 2 0.000 0.000 0.000 0.000 :88(normalize_word)\n", + " 7 0.000 0.000 0.000 0.000 {method 'acquire' of '_thread.lock' objects}\n", + " 14 0.000 0.000 0.000 0.000 fromnumeric.py:2100(_sum_dispatcher)\n", + " 1 0.000 0.000 0.000 0.000 :19(get_database)\n", + " 2 0.000 0.000 0.000 0.000 {method 'translate' of 'str' objects}\n", + " 6 0.000 0.000 0.000 0.000 {built-in method posix.getpid}\n", + " 17 0.000 0.000 0.000 0.000 {built-in method builtins.len}\n", + " 14 0.000 0.000 0.000 0.000 {method 'items' of 'dict' objects}\n", + " 6 0.000 0.000 0.000 0.000 iostream.py:321(_schedule_flush)\n", + " 8 0.000 0.000 0.000 0.000 multiarray.py:1043(copyto)\n", + " 7 0.000 0.000 0.000 0.000 {method 'append' of 'list' objects}\n", + " 6 0.000 0.000 0.000 0.000 {method 'copy' of 'list' objects}\n", + " 7 0.000 0.000 0.000 0.000 threading.py:529(is_set)\n", + " 1 0.000 0.000 0.000 0.000 :78(get_inverted_database)\n", + " 2 0.000 0.000 0.000 0.000 :105(connect_word)\n", + " 3 0.000 0.000 0.000 0.000 :147(get_connected_words)\n", + " 1 0.000 0.000 0.000 0.000 :110(check_connected)\n", + " 2 0.000 0.000 0.000 0.000 {method 'lower' of 'str' objects}\n", + " 7 0.000 0.000 0.000 0.000 {method 'append' of 'collections.deque' objects}\n", + " 2 0.000 0.000 0.000 0.000 :102(set_word_key)\n", + " 1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}" + ] + } + ], + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 115, + "source": [ + "\n", + "plt.imshow(base_grid)\n", + "plt.show()\n", + "print(final_state.letter_grid)\n", + "for grid_word in final_state.final_grid_words:\n", + " connected = []\n", + " for word in grid_word.connected_words:\n", + " connected.append(word.word_key)\n", + " print(grid_word.word_key, connected)" + ], + "outputs": [ + { + "output_type": "display_data", + "data": { + "text/plain": [ + "
" + ], + "image/svg+xml": "\n\n\n\n \n \n \n \n 2021-08-23T14:20:12.832599\n image/svg+xml\n \n \n Matplotlib v3.3.4, https://matplotlib.org/\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n", + "image/png": "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" + }, + "metadata": { + "needs_background": "light" + } + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "[[' ' 'm' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ']\n", + " ['a' 'a' 'c' 'h' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ']\n", + " [' ' 'i' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ']\n", + " [' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ']\n", + " [' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ']\n", + " [' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ']\n", + " [' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ']\n", + " [' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ']\n", + " [' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ']\n", + " [' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ']\n", + " [' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ']\n", + " [' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ']\n", + " [' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ']\n", + " [' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ']]\n", + "mai ['aach0']\n", + "aach0 ['mai']\n" + ] + } + ], + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": null, + "source": [ + "grid_word" + ], + "outputs": [], + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 292, + "source": [ + "def create_word_grid(w: int, h: int, lang_code: str = \"en\", target_density: float = 0.5, difficulty: int = 0):\n", + " np.full()\n", + "\n" + ], + "outputs": [], + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 85, + "source": [ + "w = 5\n", + "h = 5\n", + "\n", + "hh_index = np.zeros(shape=(h,w), dtype = int)\n", + "vv_index = np.zeros(shape=(w,h), dtype = int)\n", + "\n", + "\n", + "h_index = w - np.arange(w)\n", + "v_index = h - np.arange(h)\n", + "\n", + "hh_index[:] = h_index\n", + "vv_index[:] = v_index\n", + "vv_index = vv_index.transpose()\n", + "\n", + "\n", + "horizontal_starting_points = np.full(shape=(h, w), dtype=int, fill_value=-1)\n", + "horizontal_starting_points[::2,::2] = 0\n", + "horizontal_starting_points[horizontal_starting_points == 0] = hh_index[horizontal_starting_points == 0]\n", + "\n", + "vertical_starting_points = np.full(shape=(h, w), dtype=int, fill_value=-1)\n", + "vertical_starting_points[::2,::2] = 0\n", + "vertical_starting_points[vertical_starting_points == 0] = vv_index[vertical_starting_points == 0]\n", + "\n", + "vertical_starting_points" + ], + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "array([[ 5, -1, 5, -1, 5],\n", + " [-1, -1, -1, -1, -1],\n", + " [ 3, -1, 3, -1, 3],\n", + " [-1, -1, -1, -1, -1],\n", + " [ 1, -1, 1, -1, 1]])" + ] + }, + "metadata": {}, + "execution_count": 85 + } + ], + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": null, + "source": [], + "outputs": [], + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 82, + "source": [ + "w = 10\n", + "h = 5\n", + "\n", + "hh_index = np.zeros(shape=(h,w), dtype = int)\n", + "vv_index = np.zeros(shape=(w,h), dtype = int)\n", + "\n", + "\n", + "h_index = w - 1 - np.arange(w)\n", + "v_index = h - 1 - np.arange(h)\n", + "\n", + "hh_index[:] = h_index\n", + "vv_index[:] = v_index\n", + "vv_index = vv_index.transpose()\n", + "\n", + "hh_index\n" + ], + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "array([[9, 8, 7, 6, 5, 4, 3, 2, 1, 0],\n", + " [9, 8, 7, 6, 5, 4, 3, 2, 1, 0],\n", + " [9, 8, 7, 6, 5, 4, 3, 2, 1, 0],\n", + " [9, 8, 7, 6, 5, 4, 3, 2, 1, 0],\n", + " [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]])" + ] + }, + "metadata": {}, + "execution_count": 82 + } + ], + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 87, + "source": [ + "l = [[1,2],[2,3]]\n", + "\n", + "[1,3] in l" + ], + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "False" + ] + }, + "metadata": {}, + "execution_count": 87 + } + ], + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 53, + "source": [ + "import pprint\n", + "\n", + "grid, hints, solution = create_word_grid(10,20)\n", + "\n", + "pprint.pprint(grid)" + ], + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "100\n", + "200\n", + "300\n", + "400\n", + "500\n", + "600\n", + "700\n", + "800\n", + "900\n", + "1000\n", + "1100\n", + "1200\n" + ] + }, + { + "output_type": "error", + "ename": "RecursionError", + "evalue": "maximum recursion depth exceeded while calling a Python object", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mRecursionError\u001b[0m Traceback (most recent call last)", + "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[0;32mimport\u001b[0m \u001b[0mpprint\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 2\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 3\u001b[0;31m \u001b[0mgrid\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mhints\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0msolution\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mcreate_word_grid\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m10\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;36m20\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 4\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 5\u001b[0m \u001b[0mpprint\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mpprint\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mgrid\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m\u001b[0m in \u001b[0;36mcreate_word_grid\u001b[0;34m(w, h, lang_code, target_density, difficulty)\u001b[0m\n\u001b[1;32m 279\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 280\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 281\u001b[0;31m \u001b[0msolution_word_locations\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mget_solution_word\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 282\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 283\u001b[0m \u001b[0mlogging\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0minfo\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\"crossword generation done after %s iterations\"\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mstr\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mi\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;32m\u001b[0m in \u001b[0;36mget_solution_word\u001b[0;34m(min_length, max_length)\u001b[0m\n\u001b[1;32m 173\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mchar\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mlocations_cpy\u001b[0m \u001b[0;32mor\u001b[0m \u001b[0mlen\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mlocations_cpy\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mchar\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0;36m0\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 174\u001b[0m \u001b[0;31m# next try:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 175\u001b[0;31m \u001b[0;32mreturn\u001b[0m \u001b[0mget_solution_word\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mmin_length\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mmin_length\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mmax_length\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mmax_length\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 176\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 177\u001b[0m \u001b[0mlocation_candidates\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mlocations_cpy\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mchar\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "... last 1 frames repeated, from the frame below ...\n", + "\u001b[0;32m\u001b[0m in \u001b[0;36mget_solution_word\u001b[0;34m(min_length, max_length)\u001b[0m\n\u001b[1;32m 173\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mchar\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mlocations_cpy\u001b[0m \u001b[0;32mor\u001b[0m \u001b[0mlen\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mlocations_cpy\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mchar\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0;36m0\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 174\u001b[0m \u001b[0;31m# next try:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 175\u001b[0;31m \u001b[0;32mreturn\u001b[0m \u001b[0mget_solution_word\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mmin_length\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mmin_length\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mmax_length\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mmax_length\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 176\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 177\u001b[0m \u001b[0mlocation_candidates\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mlocations_cpy\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0mchar\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", + "\u001b[0;31mRecursionError\u001b[0m: maximum recursion depth exceeded while calling a Python object" + ] + } + ], + "metadata": {} + } + ], + "metadata": { + "orig_nbformat": 4, + "language_info": { + "name": "python", + "version": "3.9.5", + "mimetype": "text/x-python", + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "pygments_lexer": "ipython3", + "nbconvert_exporter": "python", + "file_extension": ".py" + }, + "kernelspec": { + "name": "python3", + "display_name": "Python 3.9.5 64-bit" + }, + "interpreter": { + "hash": "916dbcbb3f70747c44a77c7bcd40155683ae19c65e1c03b4aa3499c5328201f1" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} \ No newline at end of file diff --git a/data/better_grid_building2.ipynb b/data/better_grid_building2.ipynb new file mode 100644 index 0000000..fc33236 --- /dev/null +++ b/data/better_grid_building2.ipynb @@ -0,0 +1,1423 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "source": [ + "# load stuff\n", + "import json\n", + "import random\n", + "import numpy as np\n", + "from string import digits, ascii_lowercase\n", + "import pathlib\n", + "import logging\n", + "\n", + "import matplotlib.pyplot as plt\n", + "from IPython.display import display" + ], + "outputs": [], + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 2, + "source": [ + "def get_difficulty_threshold(lang: str, difficulty: int):\n", + " return get_difficulty_threshold.thresholds[lang][difficulty]\n", + "\n", + "\n", + "get_difficulty_threshold.thresholds = {\n", + " 'de': {\n", + " 0: 10,\n", + " 1: 6,\n", + " 2: 0\n", + " },\n", + " 'en': {\n", + " 0: 150,\n", + " 1: 100,\n", + " 2: 10\n", + " }\n", + "}\n", + "\n", + "\n", + "def get_database(lang: str = \"en\", difficulty: int = -1) -> dict:\n", + " if lang not in get_database._dbs:\n", + " try:\n", + " file = __file__\n", + " except:\n", + " file = \"./.tmp\"\n", + " current_folder = pathlib.Path(file).parents[0]\n", + " db_file = str(current_folder / f\"{lang}.json\")\n", + "\n", + " logging.info(\"loading database: %s\", lang)\n", + "\n", + " with open(db_file, \"r\") as f:\n", + " db = json.load(f)\n", + " get_database._dbs[lang] = {}\n", + " get_database._dbs[lang][-1] = db\n", + "\n", + " logging.info(\"database loaded\")\n", + " \n", + " if difficulty not in get_database._dbs[lang]:\n", + " t = get_difficulty_threshold(lang, difficulty)\n", + " logging.info(\"generate sub database for lang %s with difficulty %s\", lang, str(difficulty))\n", + " db = get_database._dbs[lang][-1]\n", + " new_db = {}\n", + " for word_key, item in db.items():\n", + " num_translations = item['num_translations']\n", + " if num_translations >= t:\n", + " new_db[word_key] = item\n", + " \n", + " get_database._dbs[lang][difficulty] = new_db\n", + "\n", + " return get_database._dbs[lang][difficulty]\n", + "\n", + "\n", + "get_database._dbs = {}\n", + "\n", + "def build_inverted_index(db):\n", + "\n", + " inverted_db = {}\n", + "\n", + " inverted_db['#'] = {}\n", + " number_db = inverted_db['#']\n", + "\n", + " for letter in ascii_lowercase:\n", + " inverted_db[letter] = {}\n", + "\n", + " for key, item in db.items():\n", + " try:\n", + " word = item['word']\n", + " norm_word = normalize_word(word)\n", + "\n", + " n = len(norm_word)\n", + "\n", + " if norm_word.isalnum():\n", + "\n", + " for i, letter in enumerate(norm_word):\n", + " letter_db = inverted_db[letter]\n", + " if i not in letter_db:\n", + " letter_db[i] = {}\n", + " letter_db_i = letter_db[i]\n", + " if n not in letter_db_i:\n", + " letter_db_i[n] = []\n", + " if n not in number_db:\n", + " number_db[n] = []\n", + " \n", + " letter_db_i[n].append(key)\n", + " number_db[n].append(key)\n", + " except:\n", + " pass\n", + " #print(\"error processing \" + word)\n", + " \n", + " return inverted_db\n", + "\n", + "def get_inverted_database(lang: str, difficulty: int = -1) -> dict:\n", + " if lang not in get_inverted_database._dbs:\n", + " get_inverted_database._dbs[lang] = {}\n", + " if difficulty not in get_inverted_database._dbs[lang]:\n", + " get_inverted_database._dbs[lang][difficulty] = build_inverted_index(get_database(lang, difficulty))\n", + " return get_inverted_database._dbs[lang][difficulty]\n", + "\n", + "get_inverted_database._dbs = {}\n", + " \n", + "\n", + "remove_digits = str.maketrans('', '', digits)\n", + "\n", + "def normalize_word(word: str):\n", + " word = word.translate(remove_digits)\n", + " return word.lower()\n", + "\n", + "def find_suitable_words(constraints: list, db: dict, inverted_db: dict):\n", + " sets = []\n", + "\n", + " n = len(constraints)\n", + " for i,letter in enumerate(constraints):\n", + " if letter == ' ':\n", + " continue\n", + " \n", + " letter_db = inverted_db[letter]\n", + " if i in letter_db:\n", + " i_list = letter_db[i]\n", + " \n", + " if not n in i_list:\n", + " return set()\n", + " \n", + " sets.append(set(i_list[n]))\n", + " \n", + " else:\n", + " return set()\n", + " \n", + " # at least one constraint must be set\n", + " if len(sets) == 0:\n", + " \n", + " # set first letter random and try again\n", + " if n in inverted_db['#']:\n", + " return inverted_db['#'][n]\n", + " return set()\n", + " \n", + " return set.intersection(*sets)\n", + " \n", + "\n" + ], + "outputs": [], + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 3, + "source": [ + "%%prun\n", + "\n", + "print(len(find_suitable_words(list(\" \"), get_database(\n", + " \"de\", difficulty=0), get_inverted_database(\"de\", difficulty=0))))\n" + ], + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "40\n", + " " + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + " 325711 function calls (325710 primitive calls) in 0.776 seconds\n", + "\n", + " Ordered by: internal time\n", + "\n", + " ncalls tottime percall cumtime percall filename:lineno(function)\n", + " 1 0.533 0.533 0.533 0.533 decoder.py:343(raw_decode)\n", + " 1 0.118 0.118 0.183 0.183 :54(build_inverted_index)\n", + " 1 0.029 0.029 0.044 0.044 {method 'read' of '_io.TextIOWrapper' objects}\n", + " 250884 0.028 0.000 0.028 0.000 {method 'append' of 'list' objects}\n", + " 14931 0.019 0.000 0.019 0.000 {method 'translate' of 'str' objects}\n", + " 1 0.015 0.015 0.015 0.015 {built-in method _codecs.utf_8_decode}\n", + " 2 0.014 0.007 0.593 0.296 :19(get_database)\n", + " 14931 0.009 0.000 0.031 0.000 :103(normalize_word)\n", + " 14931 0.003 0.000 0.003 0.000 {method 'isalnum' of 'str' objects}\n", + " 14931 0.003 0.000 0.003 0.000 {method 'lower' of 'str' objects}\n", + "14941/14940 0.002 0.000 0.002 0.000 {built-in method builtins.len}\n", + " 1 0.001 0.001 0.578 0.578 __init__.py:274(load)\n", + " 1 0.000 0.000 0.000 0.000 {built-in method io.open}\n", + " 1 0.000 0.000 0.776 0.776 {built-in method builtins.exec}\n", + " 3 0.000 0.000 0.000 0.000 socket.py:438(send)\n", + " 1 0.000 0.000 0.775 0.775 :1()\n", + " 1 0.000 0.000 0.000 0.000 __init__.py:1908(basicConfig)\n", + " 2 0.000 0.000 0.000 0.000 pathlib.py:64(parse_parts)\n", + " 2 0.000 0.000 0.000 0.000 pathlib.py:672(_parse_args)\n", + " 1 0.000 0.000 0.533 0.533 __init__.py:299(loads)\n", + " 1 0.000 0.000 0.533 0.533 decoder.py:332(decode)\n", + " 3 0.000 0.000 0.000 0.000 iostream.py:195(schedule)\n", + " 1 0.000 0.000 0.016 0.016 codecs.py:319(decode)\n", + " 3 0.000 0.000 0.000 0.000 __init__.py:2089(info)\n", + " 1 0.000 0.000 0.000 0.000 {method '__exit__' of '_io._IOBase' objects}\n", + " 1 0.000 0.000 0.000 0.000 pathlib.py:692(_from_parts)\n", + " 2 0.000 0.000 0.000 0.000 iostream.py:384(write)\n", + " 2 0.000 0.000 0.000 0.000 {method 'match' of 're.Pattern' objects}\n", + " 3 0.000 0.000 0.000 0.000 __init__.py:1689(isEnabledFor)\n", + " 1 0.000 0.000 0.000 0.000 {built-in method builtins.print}\n", + " 5 0.000 0.000 0.000 0.000 __init__.py:218(_acquireLock)\n", + " 1 0.000 0.000 0.000 0.000 __init__.py:857(__init__)\n", + " 1 0.000 0.000 0.000 0.000 pathlib.py:633(__getitem__)\n", + " 1 0.000 0.000 0.000 0.000 pathlib.py:1069(__new__)\n", + " 1 0.000 0.000 0.000 0.000 _bootlocale.py:33(getpreferredencoding)\n", + " 1 0.000 0.000 0.183 0.183 :91(get_inverted_database)\n", + " 3 0.000 0.000 0.000 0.000 threading.py:1093(is_alive)\n", + " 1 0.000 0.000 0.000 0.000 __init__.py:553(__init__)\n", + " 2 0.000 0.000 0.000 0.000 iostream.py:308(_is_master_process)\n", + " 1 0.000 0.000 0.000 0.000 pathlib.py:732(__str__)\n", + " 3 0.000 0.000 0.000 0.000 threading.py:1039(_wait_for_tstate_lock)\n", + " 5 0.000 0.000 0.000 0.000 __init__.py:227(_releaseLock)\n", + " 1 0.000 0.000 0.000 0.000 {method 'search' of 're.Pattern' objects}\n", + " 1 0.000 0.000 0.000 0.000 _weakrefset.py:82(add)\n", + " 3 0.000 0.000 0.000 0.000 __init__.py:1436(info)\n", + " 1 0.000 0.000 0.000 0.000 pathlib.py:726(_make_child)\n", + " 3 0.000 0.000 0.000 0.000 {method 'acquire' of '_thread.lock' objects}\n", + " 1 0.000 0.000 0.000 0.000 {built-in method _locale.nl_langinfo}\n", + " 1 0.000 0.000 0.000 0.000 __init__.py:838(_addHandlerRef)\n", + " 2 0.000 0.000 0.000 0.000 pathlib.py:293(splitroot)\n", + " 2 0.000 0.000 0.000 0.000 pathlib.py:705(_from_parsed_parts)\n", + " 1 0.000 0.000 0.000 0.000 __init__.py:886(createLock)\n", + " 1 0.000 0.000 0.000 0.000 __init__.py:424(validate)\n", + " 8 0.000 0.000 0.000 0.000 {built-in method builtins.isinstance}\n", + " 1 0.000 0.000 0.000 0.000 __init__.py:1049(__init__)\n", + " 5 0.000 0.000 0.000 0.000 {method 'acquire' of '_thread.RLock' objects}\n", + " 1 0.000 0.000 0.000 0.000 :107(find_suitable_words)\n", + " 3 0.000 0.000 0.000 0.000 iostream.py:91(_event_pipe)\n", + " 1 0.000 0.000 0.000 0.000 __init__.py:246(_register_at_fork_reinit_lock)\n", + " 1 0.000 0.000 0.000 0.000 {method 'startswith' of 'str' objects}\n", + " 1 0.000 0.000 0.000 0.000 pathlib.py:715(_format_parsed_parts)\n", + " 1 0.000 0.000 0.000 0.000 __init__.py:1601(addHandler)\n", + " 2 0.000 0.000 0.000 0.000 iostream.py:321(_schedule_flush)\n", + " 1 0.000 0.000 0.000 0.000 pathlib.py:620(__init__)\n", + " 1 0.000 0.000 0.000 0.000 codecs.py:309(__init__)\n", + " 1 0.000 0.000 0.000 0.000 threading.py:82(RLock)\n", + " 11 0.000 0.000 0.000 0.000 {method 'pop' of 'dict' objects}\n", + " 1 0.000 0.000 0.000 0.000 pathlib.py:986(parents)\n", + " 3 0.000 0.000 0.000 0.000 {built-in method __new__ of type object at 0x90efa0}\n", + " 3 0.000 0.000 0.000 0.000 pathlib.py:1079(_init)\n", + " 2 0.000 0.000 0.000 0.000 {built-in method sys.intern}\n", + " 5 0.000 0.000 0.000 0.000 {method 'release' of '_thread.RLock' objects}\n", + " 2 0.000 0.000 0.000 0.000 {method 'end' of 're.Match' objects}\n", + " 1 0.000 0.000 0.000 0.000 pathlib.py:964(__truediv__)\n", + " 2 0.000 0.000 0.000 0.000 {built-in method posix.getpid}\n", + " 1 0.000 0.000 0.000 0.000 __init__.py:771(__init__)\n", + " 1 0.000 0.000 0.000 0.000 :1(get_difficulty_threshold)\n", + " 1 0.000 0.000 0.000 0.000 codecs.py:260(__init__)\n", + " 1 0.000 0.000 0.000 0.000 __init__.py:418(__init__)\n", + " 1 0.000 0.000 0.000 0.000 __init__.py:193(_checkLevel)\n", + " 1 0.000 0.000 0.000 0.000 __init__.py:1675(getEffectiveLevel)\n", + " 1 0.000 0.000 0.000 0.000 pathlib.py:627(__len__)\n", + " 3 0.000 0.000 0.000 0.000 {method 'append' of 'collections.deque' objects}\n", + " 1 0.000 0.000 0.000 0.000 pathlib.py:102(join_parsed_parts)\n", + " 3 0.000 0.000 0.000 0.000 threading.py:529(is_set)\n", + " 1 0.000 0.000 0.000 0.000 __init__.py:1276(disable)\n", + " 2 0.000 0.000 0.000 0.000 {method 'items' of 'dict' objects}\n", + " 1 0.000 0.000 0.000 0.000 {method 'split' of 'str' objects}\n", + " 1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}\n", + " 1 0.000 0.000 0.000 0.000 {method 'add' of 'set' objects}\n", + " 2 0.000 0.000 0.000 0.000 {built-in method posix.fspath}\n", + " 1 0.000 0.000 0.000 0.000 __init__.py:957(setFormatter)\n", + " 2 0.000 0.000 0.000 0.000 {method 'reverse' of 'list' objects}\n", + " 1 0.000 0.000 0.000 0.000 {method 'join' of 'str' objects}" + ] + } + ], + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 4, + "source": [ + "class NoDataException(Exception):\n", + " pass\n", + "\n", + "\n", + "class WordInfo(object):\n", + " def __init__(self, word: str, y: int, x: int, is_vertical: bool, database: dict, opposite_prefix: str = \"opposite of\", synonym_prefix: str = \"other word for\"):\n", + " self._dictionary_database = database\n", + " self._y = y\n", + " self._x = x\n", + " self._word = word\n", + " self._hint = None\n", + " self._is_vertical = is_vertical\n", + "\n", + " self.opposite_prefix = opposite_prefix\n", + " self.synonym_prefix = synonym_prefix\n", + "\n", + " self.choose_info()\n", + "\n", + " def get_attribute(self, attr: str):\n", + " attr = self._dictionary_database[self._word][attr]\n", + " if attr is None or len(attr) == 0:\n", + " raise NoDataException\n", + " return attr\n", + "\n", + " def get_best_antonym(self) -> str:\n", + " antonyms = self.get_attribute(\"antonyms\")\n", + " return random.choice(antonyms)\n", + "\n", + " def get_best_synonym(self) -> str:\n", + " synonyms = self.get_attribute(\"synonyms\")\n", + " return random.choice(synonyms)\n", + "\n", + " def get_best_sense(self) -> str:\n", + " senses = self.get_attribute(\"senses\")\n", + " return random.choice(senses)\n", + "\n", + " def choose_info(self, n: int = 1):\n", + " assert n <= 4\n", + " # first choose antonyms, then synonyms, then senses\n", + "\n", + " hints = []\n", + "\n", + " try:\n", + " antonyms = self.get_attribute(\"antonyms\")\n", + " antonyms = [f\"{self.opposite_prefix} {w}\" for w in antonyms]\n", + " hints = hints + antonyms\n", + " except NoDataException:\n", + " pass\n", + "\n", + " try:\n", + " synonyms = self.get_attribute(\"synonyms\")\n", + " synonyms = [f\"{self.synonym_prefix} {w}\" for w in synonyms]\n", + "\n", + " hints = hints + synonyms\n", + " except NoDataException:\n", + " pass\n", + "\n", + " try:\n", + " senses = self.get_attribute(\"senses\")\n", + " hints = hints + senses\n", + " except NoDataException:\n", + " pass\n", + "\n", + " final_hints = []\n", + " for i in range(n):\n", + " choice = random.choice(hints)\n", + " hints.remove(choice)\n", + " final_hints.append(choice)\n", + "\n", + " if n == 1:\n", + " self._hint = final_hints[0]\n", + " return\n", + "\n", + " hint_symbols = ['a)', 'b)', 'c)', 'd)']\n", + "\n", + " self._hint = \"\"\n", + " for i in range(n):\n", + " self._hint += hint_symbols[i] + \" \" + final_hints[i] + \". \"\n", + "\n", + " def get_hint(self) -> str:\n", + " return self._hint\n", + "\n", + " def get_hint_location(self):\n", + " x = self._x if self._is_vertical else self._x - 1\n", + " y = self._y - 1 if self._is_vertical else self._y\n", + " return (y, x)\n", + "\n", + " def is_vertical(self):\n", + " return self._is_vertical\n" + ], + "outputs": [], + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 5, + "source": [ + "TYPE_EMPTY = -1\n", + "TYPE_NEIGHBOR = -2\n", + "TYPE_BLOCKED = -3\n", + "\n", + "\n", + "class GridCreationWord(object):\n", + " def __init__(self, y: int, x: int, length: int, is_vertical: bool, id: int) -> None:\n", + " self.y = y\n", + " self.x = x\n", + " self.length = length\n", + " self.is_vertical = is_vertical\n", + " self.id = id\n", + "\n", + " self.word_key = None\n", + " self.connected_words = []\n", + "\n", + " def get_letters(self, letter_grid: np.ndarray) -> list:\n", + " if self.is_vertical:\n", + " return letter_grid[self.y:self.y+self.length, self.x].flatten()\n", + " return letter_grid[self.y, self.x: self.x + self.length].flatten()\n", + "\n", + " def write(self, word: str, letter_grid: np.ndarray, x_grid: np.ndarray, y_grid: np.ndarray):\n", + " letters = list(word)\n", + " if self.is_vertical:\n", + "\n", + " xmin = max(self.x - 1, 0)\n", + " xmax = min(self.x + 2, letter_grid.shape[1])\n", + " ymin = self.y\n", + " ymax = self.y + self.length\n", + "\n", + " letter_grid[ymin:ymax, self.x] = letters\n", + "\n", + " conflicts = np.argwhere(\n", + " x_grid[ymin:ymax, self.x] == TYPE_NEIGHBOR\n", + " )\n", + " if len(conflicts) > 0:\n", + " corrected_conflicts = np.zeros(\n", + " shape=(len(conflicts), 2), dtype=np.int)\n", + " corrected_conflicts[:, 0] = ymin + conflicts.flatten()\n", + " corrected_conflicts[:, 1] = self.x\n", + " conflicts = corrected_conflicts\n", + "\n", + " x_neighbors = x_grid[ymin:ymax, xmin:xmax]\n", + " x_neighbors[x_neighbors == TYPE_EMPTY] = TYPE_NEIGHBOR\n", + " x_grid[ymin:ymax, xmin:xmax] = x_neighbors\n", + "\n", + " x_grid[ymin:ymax, self.x] = self.id\n", + "\n", + " fields_to_block = y_grid[ymin:ymax, self.x]\n", + " fields_to_block[fields_to_block < 0] = TYPE_BLOCKED\n", + " y_grid[ymin:ymax, self.x] = fields_to_block\n", + "\n", + " if ymin > 0:\n", + " x_grid[ymin - 1, self.x] = TYPE_BLOCKED\n", + " y_grid[ymin - 1, self.x] = TYPE_BLOCKED\n", + "\n", + " if ymax < letter_grid.shape[0]:\n", + "\n", + " x_grid[ymax, self.x] = TYPE_BLOCKED\n", + " y_grid[ymax, self.x] = TYPE_BLOCKED\n", + "\n", + " else:\n", + "\n", + " xmin = self.x\n", + " xmax = self.x + self.length\n", + " ymin = max(self.y - 1, 0)\n", + " ymax = min(self.y + 2, letter_grid.shape[0])\n", + "\n", + " letter_grid[self.y, xmin:xmax] = letters\n", + "\n", + " conflicts = np.argwhere(\n", + " y_grid[self.y, xmin:xmax] == TYPE_NEIGHBOR,\n", + " )\n", + " if len(conflicts) > 0:\n", + " corrected_conflicts = np.zeros(\n", + " shape=(len(conflicts), 2), dtype=np.int)\n", + " corrected_conflicts[:, 1] = xmin + conflicts.flatten()\n", + " corrected_conflicts[:, 0] = self.y\n", + " conflicts = corrected_conflicts\n", + "\n", + " y_neighbors = y_grid[ymin:ymax, xmin:xmax]\n", + " y_neighbors[y_neighbors == TYPE_EMPTY] = TYPE_NEIGHBOR\n", + " y_grid[ymin:ymax, xmin:xmax] = y_neighbors\n", + "\n", + " fields_to_block = x_grid[self.y, xmin:xmax]\n", + " fields_to_block[fields_to_block < 0] = TYPE_BLOCKED\n", + " x_grid[self.y, xmin:xmax] = fields_to_block\n", + "\n", + " y_grid[self.y, xmin:xmax] = self.id\n", + "\n", + " if xmin > 0:\n", + " x_grid[self.y, xmin - 1] = TYPE_BLOCKED\n", + " y_grid[self.y, xmin - 1] = TYPE_BLOCKED\n", + "\n", + " if xmax < letter_grid.shape[1]:\n", + "\n", + " x_grid[self.y, xmax] = TYPE_BLOCKED\n", + " y_grid[self.y, xmax] = TYPE_BLOCKED\n", + "\n", + " return conflicts\n", + "\n", + " def set_word_key(self, word_key: str):\n", + " self.word_key = word_key\n", + "\n", + " def connect_word(self, grid_word):\n", + " self.connected_words.append(grid_word)\n", + "\n", + " def get_connected_words(self):\n", + " return self.connected_words\n", + "\n", + " def check_connected(self, grid_word):\n", + " if self.is_vertical == grid_word.is_vertical:\n", + " return False\n", + "\n", + " if self.is_vertical:\n", + " if self.y > grid_word.y:\n", + " return False\n", + " if self.y + self.length <= grid_word.y:\n", + " return False\n", + "\n", + " if self.x >= grid_word.x + grid_word.length:\n", + " return False\n", + "\n", + " if self.x < grid_word.x:\n", + " return False\n", + "\n", + " else:\n", + " if self.x > grid_word.x:\n", + " return False\n", + " if self.x + self.length <= grid_word.x:\n", + " return False\n", + " if self.y >= grid_word.y + grid_word.length:\n", + " return False\n", + " if self.y < grid_word.y:\n", + " return False\n", + "\n", + " return True\n", + "\n", + "\n", + "class GridCreationState(object):\n", + " def __init__(self, h: int, w: int, db, inverted_db, old_state=None) -> None:\n", + " if old_state is not None:\n", + " self.h = h\n", + " self.w = w\n", + " self.db = db\n", + " self.inverted_db = inverted_db\n", + " self.x_grid = old_state.x_grid.copy()\n", + " self.y_grid = old_state.y_grid.copy()\n", + " self.letter_grid = old_state.letter_grid.copy()\n", + " self.placed_words = old_state.placed_words.copy()\n", + " self.used_word_keys = old_state.used_word_keys.copy()\n", + "\n", + " return\n", + "\n", + " self.h = h\n", + " self.w = w\n", + " self.x_grid = np.full(shape=(h, w), dtype=np.int,\n", + " fill_value=TYPE_EMPTY)\n", + " self.y_grid = np.full(shape=(h, w), dtype=np.int,\n", + " fill_value=TYPE_EMPTY)\n", + "\n", + " self.letter_grid = np.full(\n", + " shape=(h, w), dtype=np.unicode, fill_value=' ')\n", + "\n", + " self.placed_words = []\n", + " self.used_word_keys = set()\n", + "\n", + " self.db = db\n", + " self.inverted_db = inverted_db\n", + "\n", + " def write_word(self, word_key: str, y: int, x: int, is_vertical: bool):\n", + " id = len(self.placed_words)\n", + "\n", + " word_raw = self.db[word_key]['word']\n", + " word_normalized = normalize_word(word_raw)\n", + "\n", + " grid_word = GridCreationWord(y=y,\n", + " x=x,\n", + " length=len(word_normalized),\n", + " is_vertical=is_vertical, id=id)\n", + "\n", + " grid_word.set_word_key(word_key=word_key)\n", + "\n", + " conflicts = grid_word.write(word=word_normalized,\n", + " letter_grid=self.letter_grid,\n", + " x_grid=self.x_grid,\n", + " y_grid=self.y_grid)\n", + "\n", + " self.placed_words.append(grid_word)\n", + " self.used_word_keys.add(word_key)\n", + "\n", + " return conflicts\n", + "\n", + " def copy(self):\n", + " return GridCreationState(self.h, self.w, self.db, self.inverted_db, self)\n", + "\n", + " def get_density(self):\n", + "\n", + " blocked_fields_x = np.logical_or(\n", + " self.x_grid >= 0, self.x_grid == TYPE_BLOCKED)\n", + " blocked_fields_y = np.logical_or(\n", + " self.y_grid >= 0, self.y_grid == TYPE_BLOCKED)\n", + "\n", + " blocked_fields = np.logical_or(blocked_fields_x, blocked_fields_y)\n", + "\n", + " return np.sum(blocked_fields) / (self.w * self.h)\n", + "\n", + " def get_letters(self, y: int, x: int, length: int, is_vertical: bool):\n", + " if is_vertical:\n", + " return self.letter_grid[y:y+length, x].flatten()\n", + " return self.letter_grid[y, x:x+length].flatten()\n", + "\n", + " def get_max_extents(self, y: int, x: int, is_vertical: bool):\n", + " # check min max offsets\n", + " if is_vertical:\n", + " min_coord = y - 1\n", + " if min_coord < 0 or self.y_grid[min_coord, x] == TYPE_BLOCKED:\n", + " min_coord = y\n", + " else:\n", + " while min_coord > 0 and self.y_grid[min_coord - 1, x] != TYPE_BLOCKED:\n", + " min_coord -= 1\n", + " max_coord = y + 1\n", + " while max_coord < self.h and self.y_grid[max_coord, x] != TYPE_BLOCKED:\n", + " max_coord += 1\n", + "\n", + " return min_coord, max_coord\n", + " else:\n", + " min_coord = x - 1\n", + " if min_coord < 0 or self.x_grid[y, min_coord] == TYPE_BLOCKED:\n", + " min_coord = x\n", + " else:\n", + " while min_coord > 0 and self.x_grid[y, min_coord - 1] != TYPE_BLOCKED:\n", + " min_coord -= 1\n", + " max_coord = x + 1\n", + " while max_coord < self.w and self.x_grid[y, max_coord] != TYPE_BLOCKED:\n", + " max_coord += 1\n", + " return min_coord, max_coord\n", + "\n", + " def expand_coordinates(self, y: int, x: int, length: int, is_vertical: bool):\n", + " if is_vertical:\n", + " min_coord = y\n", + " max_coord = y + length\n", + " while min_coord > 0 and self.y_grid[min_coord - 1, x] >= 0:\n", + " min_coord -= 1\n", + " while max_coord < self.h and self.y_grid[max_coord, x] >= 0:\n", + " max_coord += 1\n", + "\n", + " return min_coord, max_coord\n", + " else:\n", + " min_coord = x\n", + " max_coord = x + length\n", + " while min_coord > 0 and self.x_grid[y, min_coord - 1] >= 0:\n", + " min_coord -= 1\n", + " while max_coord < self.w and self.x_grid[y, max_coord] >= 0:\n", + " max_coord += 1\n", + "\n", + " return min_coord, max_coord\n", + "\n", + " def place_random_word(self, min_length: int = 4, max_length: int = 15):\n", + " # first, find a random intersection\n", + " letter_locations = np.argwhere(self.letter_grid != ' ')\n", + " if len(letter_locations) == 0:\n", + " # if nothing is placed so far, just choose a random place\n", + " length = np.random.randint(min_length, max_length)\n", + " length = min(length, max_length)\n", + " y = np.random.randint(0, self.h - 1)\n", + " x = np.random.randint(0, self.w - length)\n", + " is_vertical = False\n", + " word_template = \" \" * length\n", + " else:\n", + " # possible candidates are fields where words are placed\n", + " # only horizontally or only vertically\n", + " candidates = np.argwhere(\n", + " np.logical_xor(self.x_grid >= 0, self.y_grid >= 0)\n", + " )\n", + "\n", + " if len(candidates) == 0:\n", + " #print(\"field is full\")\n", + " return None\n", + "\n", + " candidate_index = random.randint(0, len(candidates) - 1)\n", + " y, x = candidates[candidate_index]\n", + "\n", + " is_vertical = self.x_grid[y, x] == TYPE_BLOCKED\n", + "\n", + " min_coord, max_coord = self.get_max_extents(y, x, is_vertical)\n", + "\n", + " extent = max_coord - min_coord\n", + "\n", + " if extent < min_length:\n", + " #print(\"not enough space to place a word\")\n", + " return None\n", + "\n", + " min_length = min(extent, min_length)\n", + " max_length = min(extent, max_length)\n", + "\n", + " length = random.randint(min_length, max_length)\n", + " offset = random.randint(0, extent - length)\n", + "\n", + " min_coord += offset\n", + "\n", + " if is_vertical:\n", + " if min_coord + length <= y:\n", + " min_coord = y - length + 1\n", + " max_coord = min_coord + length\n", + " if min_coord > y:\n", + " min_coord = y\n", + " max_coord = min_coord + length\n", + "\n", + " min_coord, max_coord = self.expand_coordinates(y=min_coord,\n", + " x=x,\n", + " length=length,\n", + " is_vertical=is_vertical)\n", + "\n", + " length = max_coord - min_coord\n", + "\n", + " letters = self.get_letters(min_coord, x, length, is_vertical)\n", + "\n", + " y = min_coord\n", + "\n", + " else:\n", + "\n", + " if min_coord + length <= x:\n", + " min_coord = x - length + 1\n", + " max_coord = min_coord + length\n", + " if min_coord > x:\n", + " min_coord = x\n", + " max_coord = min_coord + length\n", + "\n", + " min_coord, max_coord = self.expand_coordinates(y=y,\n", + " x=min_coord,\n", + " length=length,\n", + " is_vertical=is_vertical)\n", + "\n", + " length = max_coord - min_coord\n", + "\n", + " letters = self.get_letters(y, min_coord, length, is_vertical)\n", + "\n", + " x = min_coord\n", + "\n", + " word_template = \"\".join(letters)\n", + "\n", + " word_candidates = list(find_suitable_words(\n", + " word_template, self.db, self.inverted_db))\n", + "\n", + " if len(word_candidates) == 0:\n", + " #print(\"no word available for given combination\")\n", + " return None\n", + "\n", + " word_candidate_index = random.randint(0, len(word_candidates) - 1)\n", + " word_key = word_candidates[word_candidate_index]\n", + "\n", + " if word_key in self.used_word_keys:\n", + " return None\n", + "\n", + " return self.write_word(word_key, y, x, is_vertical)\n", + "\n", + " def solve_conflicts(self, conflicts, n_retries=3, max_depth=5, depth=0):\n", + " if len(conflicts) == 0:\n", + " return self\n", + " # else:\n", + " # return None\n", + "\n", + " if depth > max_depth:\n", + " return None\n", + "\n", + " new_conflictes = []\n", + "\n", + " for conflict in conflicts:\n", + "\n", + " y, x = conflict\n", + "\n", + " if self.x_grid[y, x] >= 0 and self.y_grid[y, x] >= 0:\n", + " # conflict already solved\n", + " continue\n", + "\n", + " # find out whether the conflict is vertical or horizontal\n", + " is_vertical = self.y_grid[y, x] == TYPE_NEIGHBOR\n", + "\n", + " # calculate the minimum and maximum extend to fix the conflict\n", + " if is_vertical:\n", + " max_ymin = y\n", + " while max_ymin > 0 and self.y_grid[max_ymin-1, x] >= 0:\n", + " max_ymin -= 1\n", + " min_ymax = y + 1\n", + " while min_ymax < self.h and self.y_grid[min_ymax, x] >= 0:\n", + " min_ymax += 1\n", + "\n", + " min_ymin = max_ymin\n", + " while min_ymin > 0 and self.y_grid[min_ymin - 1, x] != TYPE_BLOCKED:\n", + " min_ymin -= 1\n", + " max_ymax = min_ymax\n", + " while max_ymax < self.h and self.y_grid[max_ymax, x] != TYPE_BLOCKED:\n", + " max_ymax += 1\n", + "\n", + " min_coord_min = min_ymin\n", + " max_coord_min = max_ymin\n", + " min_coord_max = min_ymax\n", + " max_coord_max = max_ymax\n", + "\n", + " else:\n", + " max_xmin = x\n", + " while max_xmin > 0 and self.x_grid[y, max_xmin - 1] >= 0:\n", + " max_xmin -= 1\n", + " min_xmax = x + 1\n", + " while min_xmax < self.w and self.x_grid[y, min_xmax] >= 0:\n", + " min_xmax += 1\n", + "\n", + " min_xmin = max_xmin\n", + " while min_xmin > 0 and self.x_grid[y, min_xmin - 1] != TYPE_BLOCKED:\n", + " min_xmin -= 1\n", + " max_xmax = min_xmax\n", + " while max_xmax < self.w and self.x_grid[y, max_xmax] != TYPE_BLOCKED:\n", + " max_xmax += 1\n", + "\n", + " min_coord_min = min_xmin\n", + " max_coord_min = max_xmin\n", + " min_coord_max = min_xmax\n", + " max_coord_max = max_xmax\n", + "\n", + " n_options = max_coord_max - min_coord_max + max_coord_min - min_coord_min\n", + "\n", + " solved = False\n", + "\n", + " for _ in range(min(n_options, n_retries)):\n", + " coord_min = random.randint(min_coord_min, max_coord_min)\n", + " coord_max = random.randint(min_coord_max, max_coord_max)\n", + " length = coord_max - coord_min\n", + " if length < 2:\n", + " continue\n", + "\n", + " if is_vertical:\n", + "\n", + " coord_min, coord_max = self.expand_coordinates(y=coord_min,\n", + " x=x,\n", + " length=length,\n", + " is_vertical=is_vertical)\n", + "\n", + " length = coord_max - coord_min\n", + "\n", + " y = coord_min\n", + "\n", + " else:\n", + "\n", + " coord_min, coord_max = self.expand_coordinates(y=y,\n", + " x=coord_min,\n", + " length=length,\n", + " is_vertical=is_vertical)\n", + "\n", + " length = coord_max - coord_min\n", + "\n", + " x = coord_min\n", + "\n", + " letters = self.get_letters(y, x, length, is_vertical)\n", + "\n", + " word_template = \"\".join(letters)\n", + "\n", + " candidates = list(find_suitable_words(\n", + " word_template, self.db, self.inverted_db))\n", + "\n", + " if len(candidates) == 0:\n", + " continue\n", + "\n", + " candidate_index = random.randint(0, len(candidates) - 1)\n", + " word_key = candidates[candidate_index]\n", + "\n", + " if word_key in self.used_word_keys:\n", + " continue\n", + "\n", + " word_conflicts = self.write_word(word_key, y, x, is_vertical)\n", + " if len(word_conflicts) > 0:\n", + " new_conflictes.append(word_conflicts)\n", + "\n", + " solved = True\n", + " break\n", + "\n", + " if not solved:\n", + " return None\n", + "\n", + " if len(new_conflictes) == 0:\n", + " return self\n", + "\n", + " new_conflictes = np.concatenate(new_conflictes)\n", + " for _ in range(n_retries):\n", + " next_state = self.copy()\n", + " solved_state = next_state.solve_conflicts(\n", + " new_conflictes, n_retries, max_depth, depth + 1)\n", + " if solved_state is not None:\n", + " return solved_state\n", + " return None\n", + "\n", + " def fill_grid(self, target_density: float = 0.6, inner_retries: int = 5, conflict_retries: int = 10, conflict_solver_depth=5, min_length: int = 4, max_length: int = 10, max_iterations: int = 1000):\n", + " i = 0\n", + " state = self.copy()\n", + " while i < max_iterations and state.get_density() < target_density:\n", + " i += 1\n", + " new_state = state.copy()\n", + " conflicts = new_state.place_random_word(min_length, max_length)\n", + " if conflicts is None:\n", + " continue\n", + " if len(conflicts) == 0:\n", + " state = new_state\n", + "\n", + " if len(conflicts) > 0:\n", + " \n", + " solved_state = new_state.solve_conflicts(\n", + " conflicts, inner_retries, conflict_solver_depth)\n", + " if solved_state is not None:\n", + " state = solved_state\n", + " \n", + "\n", + " print(\"finished after\", i,\n", + " \"iterations, with a density of\", state.get_density())\n", + " return state\n" + ], + "outputs": [], + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 6, + "source": [ + "\" \" * 4" + ], + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "' '" + ] + }, + "metadata": {}, + "execution_count": 6 + } + ], + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 7, + "source": [ + "%%prun\n", + "\n", + "difficulty = 0\n", + "size = 20\n", + "\n", + "base_grid = GridCreationState(size,size, db = get_database(\"de\", difficulty=difficulty), inverted_db= get_inverted_database(\"de\", difficulty=difficulty))\n", + "\n", + "#word_key = \"hallo0\"\n", + "#\n", + "#base_grid.write_word(word_key=word_key, y=3, x=3, is_vertical=True)\n", + "#\n", + "#word_key = \"hai\"\n", + "#\n", + "#base_grid.write_word(word_key=word_key, y=3, x=3, is_vertical=False)\n", + "\n", + "final_state = base_grid\n", + "\n", + "#for _ in range(3):\n", + "#while base_grid.get_density() < 0.8:\n", + "# final_state = final_state.copy()\n", + "# conflict = final_state.place_random_word(min_length=3, max_length=4)\n", + "\n", + "final_state = base_grid.fill_grid(target_density=0.8, inner_retries=5, conflict_solver_depth=20, min_length=3, max_iterations=max(size * 75, 1000))\n", + "\n", + "#print(final_state.letter_grid)\n", + "#for word in final_state.placed_words:\n", + "# print(word.word_key)\n" + ], + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "finished after 1500 iterations, with a density of 0.765\n", + " " + ] + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + " 209587 function calls (198419 primitive calls) in 0.692 seconds\n", + "\n", + " Ordered by: internal time\n", + "\n", + " ncalls tottime percall cumtime percall filename:lineno(function)\n", + " 1500 0.094 0.000 0.442 0.000 :259(place_random_word)\n", + " 2085 0.093 0.000 0.104 0.000 :107(find_suitable_words)\n", + " 682 0.048 0.000 0.082 0.000 :22(write)\n", + " 659/534 0.038 0.000 0.128 0.000 :358(solve_conflicts)\n", + " 3681 0.032 0.000 0.032 0.000 {built-in method numpy.array}\n", + " 1501 0.029 0.000 0.073 0.000 :197(get_density)\n", + " 3681 0.028 0.000 0.028 0.000 {method 'nonzero' of 'numpy.ndarray' objects}\n", + " 1499 0.024 0.000 0.024 0.000 :213(get_max_extents)\n", + " 1501 0.021 0.000 0.021 0.000 {method 'reduce' of 'numpy.ufunc' objects}\n", + " 1 0.020 0.020 0.692 0.692 :492(fill_grid)\n", + " 2084 0.019 0.000 0.019 0.000 {method 'join' of 'str' objects}\n", + "16253/5210 0.016 0.000 0.190 0.000 {built-in method numpy.core._multiarray_umath.implement_array_function}\n", + " 3681 0.016 0.000 0.055 0.000 fromnumeric.py:39(_wrapit)\n", + " 3681 0.014 0.000 0.146 0.000 numeric.py:537(argwhere)\n", + " 6361 0.013 0.000 0.028 0.000 random.py:291(randrange)\n", + " 4878 0.013 0.000 0.013 0.000 {method 'copy' of 'numpy.ndarray' objects}\n", + " 7362 0.013 0.000 0.098 0.000 fromnumeric.py:52(_wrapfunc)\n", + " 6361 0.010 0.000 0.015 0.000 random.py:238(_randbelow_with_getrandbits)\n", + " 682 0.009 0.000 0.097 0.000 :171(write_word)\n", + " 2084 0.009 0.000 0.009 0.000 :239(expand_coordinates)\n", + " 1627 0.009 0.000 0.027 0.000 :141(__init__)\n", + " 6361 0.008 0.000 0.036 0.000 random.py:335(randint)\n", + " 2010 0.008 0.000 0.008 0.000 {method 'intersection' of 'set' objects}\n", + " 2694 0.008 0.000 0.008 0.000 {method 'flatten' of 'numpy.ndarray' objects}\n", + " 1501 0.007 0.000 0.030 0.000 fromnumeric.py:70(_wrapreduction)\n", + " 2084 0.006 0.000 0.011 0.000 :208(get_letters)\n", + " 1501 0.006 0.000 0.037 0.000 fromnumeric.py:2105(sum)\n", + " 3681 0.006 0.000 0.157 0.000 <__array_function__ internals>:2(argwhere)\n", + " 3681 0.004 0.000 0.010 0.000 <__array_function__ internals>:2(ndim)\n", + " 3870 0.004 0.000 0.004 0.000 {built-in method builtins.min}\n", + " 3681 0.004 0.000 0.076 0.000 <__array_function__ internals>:2(transpose)\n", + " 11043 0.004 0.000 0.004 0.000 {built-in method builtins.getattr}\n", + " 15988 0.004 0.000 0.004 0.000 {built-in method builtins.len}\n", + " 3681 0.004 0.000 0.045 0.000 <__array_function__ internals>:2(nonzero)\n", + " 1626 0.004 0.000 0.030 0.000 :194(copy)\n", + " 1626 0.004 0.000 0.004 0.000 {method 'copy' of 'set' objects}\n", + " 3681 0.003 0.000 0.038 0.000 fromnumeric.py:1816(nonzero)\n", + " 1501 0.003 0.000 0.044 0.000 <__array_function__ internals>:2(sum)\n", + " 3681 0.003 0.000 0.067 0.000 fromnumeric.py:601(transpose)\n", + " 3681 0.003 0.000 0.003 0.000 {method 'transpose' of 'numpy.ndarray' objects}\n", + " 3681 0.003 0.000 0.035 0.000 _asarray.py:14(asarray)\n", + " 11364 0.003 0.000 0.003 0.000 {method 'getrandbits' of '_random.Random' objects}\n", + " 3681 0.002 0.000 0.002 0.000 fromnumeric.py:3075(ndim)\n", + " 7162 0.002 0.000 0.002 0.000 {method 'append' of 'list' objects}\n", + " 610 0.002 0.000 0.002 0.000 {built-in method numpy.zeros}\n", + " 1501 0.002 0.000 0.002 0.000 fromnumeric.py:71()\n", + " 6361 0.002 0.000 0.002 0.000 {method 'bit_length' of 'int' objects}\n", + " 682 0.002 0.000 0.002 0.000 {method 'translate' of 'str' objects}\n", + " 3681 0.001 0.000 0.001 0.000 numeric.py:533(_argwhere_dispatcher)\n", + " 1626 0.001 0.000 0.001 0.000 {method 'copy' of 'list' objects}\n", + " 682 0.001 0.000 0.001 0.000 :7(__init__)\n", + " 683 0.001 0.000 0.001 0.000 {built-in method builtins.max}\n", + " 682 0.001 0.000 0.003 0.000 :103(normalize_word)\n", + " 3681 0.001 0.000 0.001 0.000 fromnumeric.py:3071(_ndim_dispatcher)\n", + " 1509 0.001 0.000 0.001 0.000 {built-in method builtins.isinstance}\n", + " 3681 0.001 0.000 0.001 0.000 fromnumeric.py:597(_transpose_dispatcher)\n", + " 3681 0.001 0.000 0.001 0.000 fromnumeric.py:1812(_nonzero_dispatcher)\n", + " 1501 0.001 0.000 0.001 0.000 fromnumeric.py:2100(_sum_dispatcher)\n", + " 1501 0.001 0.000 0.001 0.000 {method 'items' of 'dict' objects}\n", + " 682 0.000 0.000 0.000 0.000 :102(set_word_key)\n", + " 682 0.000 0.000 0.000 0.000 {method 'lower' of 'str' objects}\n", + " 682 0.000 0.000 0.000 0.000 {method 'add' of 'set' objects}\n", + " 1 0.000 0.000 0.692 0.692 {built-in method builtins.exec}\n", + " 25 0.000 0.000 0.000 0.000 <__array_function__ internals>:2(concatenate)\n", + " 9 0.000 0.000 0.000 0.000 socket.py:438(send)\n", + " 3 0.000 0.000 0.000 0.000 {method 'randint' of 'numpy.random.mtrand.RandomState' objects}\n", + " 9 0.000 0.000 0.000 0.000 iostream.py:195(schedule)\n", + " 8 0.000 0.000 0.000 0.000 iostream.py:384(write)\n", + " 3 0.000 0.000 0.000 0.000 {built-in method numpy.empty}\n", + " 1 0.000 0.000 0.692 0.692 :1()\n", + " 1 0.000 0.000 0.000 0.000 {built-in method builtins.print}\n", + " 9 0.000 0.000 0.000 0.000 threading.py:1093(is_alive)\n", + " 3 0.000 0.000 0.000 0.000 numeric.py:268(full)\n", + " 25 0.000 0.000 0.000 0.000 multiarray.py:143(concatenate)\n", + " 8 0.000 0.000 0.000 0.000 iostream.py:308(_is_master_process)\n", + " 9 0.000 0.000 0.000 0.000 threading.py:1039(_wait_for_tstate_lock)\n", + " 3 0.000 0.000 0.000 0.000 <__array_function__ internals>:2(copyto)\n", + " 9 0.000 0.000 0.000 0.000 {method 'acquire' of '_thread.lock' objects}\n", + " 9 0.000 0.000 0.000 0.000 iostream.py:91(_event_pipe)\n", + " 8 0.000 0.000 0.000 0.000 {built-in method posix.getpid}\n", + " 8 0.000 0.000 0.000 0.000 iostream.py:321(_schedule_flush)\n", + " 1 0.000 0.000 0.000 0.000 :19(get_database)\n", + " 9 0.000 0.000 0.000 0.000 threading.py:529(is_set)\n", + " 1 0.000 0.000 0.000 0.000 :91(get_inverted_database)\n", + " 9 0.000 0.000 0.000 0.000 {method 'append' of 'collections.deque' objects}\n", + " 3 0.000 0.000 0.000 0.000 multiarray.py:1043(copyto)\n", + " 1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}" + ] + } + ], + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 8, + "source": [ + "#for line in base_grid.letter_grid:\n", + "# print(\"\".join(line))\n", + "\n", + "print(final_state.letter_grid)\n", + "\n", + "plt.imshow(final_state.x_grid, vmax= 0)\n", + "plt.colorbar()\n", + "plt.show()\n", + "plt.imshow(final_state.y_grid, vmax= 0)\n", + "plt.colorbar()\n", + "plt.show()\n", + "plt.imshow(final_state.letter_grid != ' ')\n", + "plt.show()\n", + "\n", + "print(sorted([word.word_key for word in final_state.placed_words]))" + ], + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "[[' ' ' ' 's' 't' 'r' 'i' 'c' 'h' ' ' 'l' ' ' ' ' ' ' ' ' ' ' 'v' ' ' 'i'\n", + " ' ' ' ']\n", + " [' ' ' ' ' ' 'a' ' ' ' ' ' ' 'e' ' ' 'o' 'l' ' ' 'l' 'o' 'g' 'o' ' ' 'd'\n", + " 'i' 'e']\n", + " [' ' ' ' ' ' 'b' 'a' 's' 'i' 'l' 'i' 'k' 'a' ' ' ' ' ' ' 'e' 'r' 'g' 'o'\n", + " ' ' ' ']\n", + " [' ' ' ' ' ' 'u' ' ' ' ' ' ' 'l' ' ' ' ' 'm' 'a' 'k' 'e' 'l' ' ' 'o' ' '\n", + " ' ' 's']\n", + " [' ' 'd' 'u' 'r' ' ' 'l' 'u' 's' 'a' 'k' 'a' ' ' 'u' ' ' ' ' 'u' 'n' 'd'\n", + " ' ' 'k']\n", + " [' ' ' ' ' ' 'e' ' ' 'o' ' ' 'e' ' ' ' ' ' ' ' ' 'e' ' ' ' ' 'r' ' ' 'u'\n", + " 's' 'a']\n", + " ['n' 'a' 'u' 't' 'i' 's' 'c' 'h' ' ' ' ' 'e' 'i' 'n' 'k' 'l' 'a' 'n' 'g'\n", + " ' ' 'l']\n", + " ['a' ' ' ' ' 't' ' ' ' ' ' ' 'e' ' ' ' ' 'r' ' ' 'd' ' ' ' ' 'n' ' ' 'o'\n", + " ' ' 'd']\n", + " ['i' ' ' ' ' ' ' 'b' ' ' ' ' 'r' ' ' 'f' 'r' 'e' 'i' ' ' ' ' ' ' ' ' 'n'\n", + " 'i' 'e']\n", + " ['v' 'i' 'd' 'e' 'o' ' ' 'v' ' ' ' ' ' ' 'a' ' ' 'g' 'e' 'h' 'w' 'e' 'g'\n", + " ' ' ' ']\n", + " [' ' ' ' ' ' 't' 'e' 'r' 'e' 'b' 'i' 'n' 't' 'h' 'e' ' ' ' ' 'e' ' ' ' '\n", + " ' ' ' ']\n", + " [' ' 'z' 'e' 'h' ' ' ' ' 'r' ' ' 'n' ' ' 'e' ' ' 'n' 'a' 'd' 'i' 'r' ' '\n", + " ' ' ' ']\n", + " [' ' ' ' ' ' 'o' 's' 'e' 'l' ' ' 'd' 'o' 'n' ' ' ' ' 'g' ' ' 's' ' ' 'p'\n", + " ' ' 'b']\n", + " ['e' 'g' 'a' 'l' ' ' ' ' 'i' ' ' 'e' ' ' ' ' ' ' ' ' 'a' ' ' 's' 'e' 'k'\n", + " 't' 'e']\n", + " [' ' 'm' ' ' 'o' 'a' 's' 'e' ' ' 'x' ' ' 'c' 'h' 'e' 'r' 'u' 'b' ' ' 'w'\n", + " ' ' 'n']\n", + " [' ' 'b' ' ' 'g' ' ' ' ' 'r' ' ' ' ' 'a' ' ' 'u' ' ' ' ' ' ' 'r' ' ' ' '\n", + " ' ' 'e']\n", + " [' ' 'h' 'a' 'i' ' ' ' ' 'e' ' ' 'i' 'r' 'g' 'e' 'n' 'd' 'w' 'o' ' ' 'm'\n", + " 'a' 'i']\n", + " [' ' ' ' 'h' 'e' 'u' 'e' 'r' ' ' ' ' 'i' ' ' 'f' ' ' 'a' ' ' 't' ' ' 'e'\n", + " ' ' 'd']\n", + " ['g' 'i' 'n' ' ' 'n' ' ' ' ' ' ' ' ' 'e' ' ' 't' ' ' 'n' ' ' ' ' ' ' 'h'\n", + " ' ' 'e']\n", + " [' ' ' ' ' ' ' ' 'o' 'p' 'a' ' ' ' ' ' ' 'v' 'e' 'r' 'k' 'n' 'a' 'l' 'l'\n", + " 'e' 'n']]\n" + ] + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "
" + ], + "image/svg+xml": "\n\n\n\n \n \n \n \n 2021-08-31T12:13:54.263271\n image/svg+xml\n \n \n Matplotlib v3.3.4, https://matplotlib.org/\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n", + "image/png": "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" + }, + "metadata": { + "needs_background": "light" + } + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "
" + ], + "image/svg+xml": "\n\n\n\n \n \n \n \n 2021-08-31T12:13:54.790527\n image/svg+xml\n \n \n Matplotlib v3.3.4, https://matplotlib.org/\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n", + "image/png": "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" + }, + "metadata": { + "needs_background": "light" + } + }, + { + "output_type": "display_data", + "data": { + "text/plain": [ + "
" + ], + "image/svg+xml": "\n\n\n\n \n \n \n \n 2021-08-31T12:13:55.251665\n image/svg+xml\n \n \n Matplotlib v3.3.4, https://matplotlib.org/\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n", + "image/png": "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" + }, + "metadata": { + "needs_background": "light" + } + }, + { + "output_type": "stream", + "name": "stdout", + "text": [ + "['agar', 'ahn', 'arie', 'basilika', 'beneiden', 'boe', 'cherub', 'dank1', 'die', 'don', 'dugong', 'dur', 'egal', 'einklang', 'ergo', 'erraten', 'ethologie', 'frei', 'gehweg', 'gel', 'gin', 'gmbh', 'gon', 'hai', 'hellseher', 'heuer', 'huefte', 'ido', 'index', 'irgendwo', 'kuendigen', 'lama0', 'logo', 'lok', 'los0', 'lusaka', 'mai', 'makel', 'mehl', 'nadir', 'naiv', 'nautisch', 'nie', 'oase', 'ol', 'opa', 'osel', 'pkw', 'sekte', 'skalde', 'strich', 'taburett', 'terebinthe', 'und', 'uno', 'uran', 'usa', 'verknallen', 'verlierer', 'video', 'vor', 'weissbrot', 'zeh']\n" + ] + } + ], + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 9, + "source": [ + "get_database(\"de\")['sol']" + ], + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "{'word': 'Sol',\n", + " 'senses': ['Chemie: eine kolloidale Suspension von festen Partikeln oder Makromolekülen in einer flüssigen Phase'],\n", + " 'synonyms': [],\n", + " 'antonyms': ['Gel'],\n", + " 'num_translations': 6}" + ] + }, + "metadata": {}, + "execution_count": 9 + } + ], + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 21, + "source": [ + "def create_word_grid(w: int,\n", + " h: int,\n", + " lang_code: str = \"en\",\n", + " target_density: float = 0.8,\n", + " difficulty: int = 0):\n", + "\n", + " logging.info(\"generate new crossword with params: w:%s h:%s lang:%s density:%s difficulty:%s\",\n", + " str(w),\n", + " str(h),\n", + " lang_code,\n", + " str(target_density),\n", + " str(difficulty))\n", + "\n", + " db = get_database(lang_code, difficulty=difficulty)\n", + " inverted_db = get_inverted_database(lang_code, difficulty=difficulty)\n", + "\n", + " base_grid = GridCreationState(h=h, w=w, db=db, inverted_db=inverted_db)\n", + "\n", + " final_state = base_grid.fill_grid(target_density=target_density,\n", + " inner_retries=5,\n", + " conflict_solver_depth=20,\n", + " min_length=3,\n", + " max_iterations=max(size * 75, 1000))\n", + "\n", + " # generate word hints\n", + "\n", + " word_hints = {}\n", + "\n", + " opposite_prefix = \"opposite of:\" if lang_code == \"en\" else \"Gegenteil von:\"\n", + " synonym_prefix = \"other word for:\" if lang_code == \"en\" else \"anderes Wort für:\"\n", + "\n", + " for placed_word in final_state.placed_words:\n", + " word_key = placed_word.word_key\n", + " word = normalize_word(db[word_key]['word'])\n", + " y = placed_word.y\n", + " x = placed_word.x\n", + " is_vertical = placed_word.is_vertical\n", + "\n", + " word_info = WordInfo(word_key, y, x, is_vertical,\n", + " db, opposite_prefix, synonym_prefix)\n", + " word_hints[word] = word_info\n", + "\n", + " # create a solution word\n", + "\n", + " char_locations = {}\n", + " for char in list(\"abcdefghijklmnopqrstuvwxyz\"):\n", + " char_locations[char] = np.argwhere(\n", + " final_state.letter_grid == char).tolist()\n", + "\n", + " words = list(db.keys())\n", + " n_words = len(words)\n", + "\n", + " min_solution_length = 10\n", + " max_solution_length = 20\n", + "\n", + " solution_word_locations = None\n", + "\n", + " while solution_word_locations is None:\n", + "\n", + " random_index = random.randint(0, n_words - 1)\n", + " random_word_key = words[random_index]\n", + " random_word = db[random_word_key]['word']\n", + " normalized_random_word = normalize_word(random_word)\n", + " if len(normalized_random_word) < min_solution_length or len(normalized_random_word) > max_solution_length:\n", + " continue\n", + "\n", + " char_locations_copy = {}\n", + " for char in char_locations:\n", + " char_locations_copy[char] = char_locations[char].copy()\n", + " \n", + " solution = []\n", + " \n", + " aborted = False\n", + " for char in list(normalized_random_word):\n", + " if char not in char_locations_copy:\n", + " aborted = True\n", + " break\n", + " locations = char_locations_copy[char]\n", + " if len(locations) == 0:\n", + " aborted = True\n", + " break\n", + " \n", + " i = random.randint(0, len(locations) - 1)\n", + " location = locations[i]\n", + " del(locations[i])\n", + " solution.append(location)\n", + " \n", + " \n", + " if aborted:\n", + " continue\n", + "\n", + " solution_word_locations = solution\n", + " \n", + "\n", + " return final_state.letter_grid, word_hints, solution_word_locations\n" + ], + "outputs": [], + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 22, + "source": [ + "state, hints, solution = create_word_grid(10,10)" + ], + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "finished after 1500 iterations, with a density of 0.69\n" + ] + } + ], + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 29, + "source": [ + "solution" + ], + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "[[6, 4],\n", + " [6, 5],\n", + " [6, 9],\n", + " [3, 4],\n", + " [5, 9],\n", + " [4, 2],\n", + " [8, 1],\n", + " [8, 0],\n", + " [1, 8],\n", + " [8, 8]]" + ] + }, + "metadata": {}, + "execution_count": 29 + } + ], + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 83, + "source": [ + "grid = final_state.letter_grid" + ], + "outputs": [], + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": 95, + "source": [ + "[0,8] in np.argwhere(grid == \"a\").tolist()" + ], + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "False" + ] + }, + "metadata": {}, + "execution_count": 95 + } + ], + "metadata": {} + }, + { + "cell_type": "code", + "execution_count": null, + "source": [], + "outputs": [], + "metadata": {} + } + ], + "metadata": { + "orig_nbformat": 4, + "language_info": { + "name": "python", + "version": "3.9.5", + "mimetype": "text/x-python", + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "pygments_lexer": "ipython3", + "nbconvert_exporter": "python", + "file_extension": ".py" + }, + "kernelspec": { + "name": "python3", + "display_name": "Python 3.9.5 64-bit" + }, + "interpreter": { + "hash": "916dbcbb3f70747c44a77c7bcd40155683ae19c65e1c03b4aa3499c5328201f1" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} \ No newline at end of file diff --git a/server/crossword.py b/server/crossword.py index d4f2d46..299c14b 100644 --- a/server/crossword.py +++ b/server/crossword.py @@ -106,7 +106,7 @@ class LetterField(Field): class Grid(object): - def __init__(self, width: int, height: int, lang_code: str, density=0.55, difficulty: int = 0): + def __init__(self, width: int, height: int, lang_code: str, density=0.8, difficulty: int = 0): self._width = width self._height = height self._lang_code = lang_code diff --git a/server/crossword_generator.py b/server/crossword_generator.py index 198afe3..47226f0 100644 --- a/server/crossword_generator.py +++ b/server/crossword_generator.py @@ -1,7 +1,8 @@ +# load stuff import json import random import numpy as np -from string import digits +from string import digits, ascii_lowercase import pathlib import logging @@ -12,37 +13,144 @@ def get_difficulty_threshold(lang: str, difficulty: int): get_difficulty_threshold.thresholds = { 'de': { - 0: 12, + 0: 10, 1: 6, 2: 0 }, 'en': { - 0: 200, + 0: 150, 1: 100, 2: 10 } } -def get_database(lang: str = "en") -> dict: +def get_database(lang: str = "en", difficulty: int = -1) -> dict: if lang not in get_database._dbs: - current_folder = pathlib.Path(__file__).parents[0] + try: + file = __file__ + except: + file = "./.tmp" + current_folder = pathlib.Path(file).parents[0] db_file = str(current_folder / f"{lang}.json") logging.info("loading database: %s", lang) with open(db_file, "r") as f: db = json.load(f) - get_database._dbs[lang] = db + get_database._dbs[lang] = {} + get_database._dbs[lang][-1] = db logging.info("database loaded") - return get_database._dbs[lang] + if difficulty not in get_database._dbs[lang]: + t = get_difficulty_threshold(lang, difficulty) + logging.info( + "generate sub database for lang %s with difficulty %s", lang, str(difficulty)) + db = get_database._dbs[lang][-1] + new_db = {} + for word_key, item in db.items(): + num_translations = item['num_translations'] + if num_translations >= t: + new_db[word_key] = item + + get_database._dbs[lang][difficulty] = new_db + + return get_database._dbs[lang][difficulty] get_database._dbs = {} +def build_inverted_index(db): + + inverted_db = {} + + inverted_db['#'] = {} + number_db = inverted_db['#'] + + for letter in ascii_lowercase: + inverted_db[letter] = {} + + for key, item in db.items(): + try: + word = item['word'] + norm_word = normalize_word(word) + + n = len(norm_word) + + if norm_word.isalnum(): + + for i, letter in enumerate(norm_word): + letter_db = inverted_db[letter] + if i not in letter_db: + letter_db[i] = {} + letter_db_i = letter_db[i] + if n not in letter_db_i: + letter_db_i[n] = [] + if n not in number_db: + number_db[n] = [] + + letter_db_i[n].append(key) + number_db[n].append(key) + except: + pass + #print("error processing " + word) + + return inverted_db + + +def get_inverted_database(lang: str, difficulty: int = -1) -> dict: + if lang not in get_inverted_database._dbs: + get_inverted_database._dbs[lang] = {} + if difficulty not in get_inverted_database._dbs[lang]: + get_inverted_database._dbs[lang][difficulty] = build_inverted_index( + get_database(lang, difficulty)) + return get_inverted_database._dbs[lang][difficulty] + + +get_inverted_database._dbs = {} + + +remove_digits = str.maketrans('', '', digits) + + +def normalize_word(word: str): + word = word.translate(remove_digits) + return word.lower() + + +def find_suitable_words(constraints: list, db: dict, inverted_db: dict): + sets = [] + + n = len(constraints) + for i, letter in enumerate(constraints): + if letter == ' ': + continue + + letter_db = inverted_db[letter] + if i in letter_db: + i_list = letter_db[i] + + if not n in i_list: + return set() + + sets.append(set(i_list[n])) + + else: + return set() + + # at least one constraint must be set + if len(sets) == 0: + + # set first letter random and try again + if n in inverted_db['#']: + return inverted_db['#'][n] + return set() + + return set.intersection(*sets) + + class NoDataException(Exception): pass @@ -134,7 +242,527 @@ class WordInfo(object): return self._is_vertical -def create_word_grid(w: int, h: int, lang_code: str = "en", target_density: float = 0.5, difficulty: int = 0): +TYPE_EMPTY = -1 +TYPE_NEIGHBOR = -2 +TYPE_BLOCKED = -3 + + +class GridCreationWord(object): + def __init__(self, y: int, x: int, length: int, is_vertical: bool, id: int) -> None: + self.y = y + self.x = x + self.length = length + self.is_vertical = is_vertical + self.id = id + + self.word_key = None + self.connected_words = [] + + def get_letters(self, letter_grid: np.ndarray) -> list: + if self.is_vertical: + return letter_grid[self.y:self.y+self.length, self.x].flatten() + return letter_grid[self.y, self.x: self.x + self.length].flatten() + + def write(self, word: str, letter_grid: np.ndarray, x_grid: np.ndarray, y_grid: np.ndarray): + letters = list(word) + if self.is_vertical: + + xmin = max(self.x - 1, 0) + xmax = min(self.x + 2, letter_grid.shape[1]) + ymin = self.y + ymax = self.y + self.length + + letter_grid[ymin:ymax, self.x] = letters + + conflicts = np.argwhere( + x_grid[ymin:ymax, self.x] == TYPE_NEIGHBOR + ) + if len(conflicts) > 0: + corrected_conflicts = np.zeros( + shape=(len(conflicts), 2), dtype=np.int) + corrected_conflicts[:, 0] = ymin + conflicts.flatten() + corrected_conflicts[:, 1] = self.x + conflicts = corrected_conflicts + + x_neighbors = x_grid[ymin:ymax, xmin:xmax] + x_neighbors[x_neighbors == TYPE_EMPTY] = TYPE_NEIGHBOR + x_grid[ymin:ymax, xmin:xmax] = x_neighbors + + x_grid[ymin:ymax, self.x] = self.id + + fields_to_block = y_grid[ymin:ymax, self.x] + fields_to_block[fields_to_block < 0] = TYPE_BLOCKED + y_grid[ymin:ymax, self.x] = fields_to_block + + if ymin > 0: + x_grid[ymin - 1, self.x] = TYPE_BLOCKED + y_grid[ymin - 1, self.x] = TYPE_BLOCKED + + if ymax < letter_grid.shape[0]: + + x_grid[ymax, self.x] = TYPE_BLOCKED + y_grid[ymax, self.x] = TYPE_BLOCKED + + else: + + xmin = self.x + xmax = self.x + self.length + ymin = max(self.y - 1, 0) + ymax = min(self.y + 2, letter_grid.shape[0]) + + letter_grid[self.y, xmin:xmax] = letters + + conflicts = np.argwhere( + y_grid[self.y, xmin:xmax] == TYPE_NEIGHBOR, + ) + if len(conflicts) > 0: + corrected_conflicts = np.zeros( + shape=(len(conflicts), 2), dtype=np.int) + corrected_conflicts[:, 1] = xmin + conflicts.flatten() + corrected_conflicts[:, 0] = self.y + conflicts = corrected_conflicts + + y_neighbors = y_grid[ymin:ymax, xmin:xmax] + y_neighbors[y_neighbors == TYPE_EMPTY] = TYPE_NEIGHBOR + y_grid[ymin:ymax, xmin:xmax] = y_neighbors + + fields_to_block = x_grid[self.y, xmin:xmax] + fields_to_block[fields_to_block < 0] = TYPE_BLOCKED + x_grid[self.y, xmin:xmax] = fields_to_block + + y_grid[self.y, xmin:xmax] = self.id + + if xmin > 0: + x_grid[self.y, xmin - 1] = TYPE_BLOCKED + y_grid[self.y, xmin - 1] = TYPE_BLOCKED + + if xmax < letter_grid.shape[1]: + + x_grid[self.y, xmax] = TYPE_BLOCKED + y_grid[self.y, xmax] = TYPE_BLOCKED + + return conflicts + + def set_word_key(self, word_key: str): + self.word_key = word_key + + def connect_word(self, grid_word): + self.connected_words.append(grid_word) + + def get_connected_words(self): + return self.connected_words + + def check_connected(self, grid_word): + if self.is_vertical == grid_word.is_vertical: + return False + + if self.is_vertical: + if self.y > grid_word.y: + return False + if self.y + self.length <= grid_word.y: + return False + + if self.x >= grid_word.x + grid_word.length: + return False + + if self.x < grid_word.x: + return False + + else: + if self.x > grid_word.x: + return False + if self.x + self.length <= grid_word.x: + return False + if self.y >= grid_word.y + grid_word.length: + return False + if self.y < grid_word.y: + return False + + return True + + +class GridCreationState(object): + def __init__(self, h: int, w: int, db, inverted_db, old_state=None) -> None: + if old_state is not None: + self.h = h + self.w = w + self.db = db + self.inverted_db = inverted_db + self.x_grid = old_state.x_grid.copy() + self.y_grid = old_state.y_grid.copy() + self.letter_grid = old_state.letter_grid.copy() + self.placed_words = old_state.placed_words.copy() + self.used_word_keys = old_state.used_word_keys.copy() + + return + + self.h = h + self.w = w + self.x_grid = np.full(shape=(h, w), dtype=np.int, + fill_value=TYPE_EMPTY) + self.y_grid = np.full(shape=(h, w), dtype=np.int, + fill_value=TYPE_EMPTY) + + self.letter_grid = np.full( + shape=(h, w), dtype=np.unicode, fill_value=' ') + + self.placed_words = [] + self.used_word_keys = set() + + self.db = db + self.inverted_db = inverted_db + + def write_word(self, word_key: str, y: int, x: int, is_vertical: bool): + id = len(self.placed_words) + + word_raw = self.db[word_key]['word'] + word_normalized = normalize_word(word_raw) + + grid_word = GridCreationWord(y=y, + x=x, + length=len(word_normalized), + is_vertical=is_vertical, id=id) + + grid_word.set_word_key(word_key=word_key) + + conflicts = grid_word.write(word=word_normalized, + letter_grid=self.letter_grid, + x_grid=self.x_grid, + y_grid=self.y_grid) + + self.placed_words.append(grid_word) + self.used_word_keys.add(word_key) + + return conflicts + + def copy(self): + return GridCreationState(self.h, self.w, self.db, self.inverted_db, self) + + def get_density(self): + + blocked_fields_x = np.logical_or( + self.x_grid >= 0, self.x_grid == TYPE_BLOCKED) + blocked_fields_y = np.logical_or( + self.y_grid >= 0, self.y_grid == TYPE_BLOCKED) + + blocked_fields = np.logical_or(blocked_fields_x, blocked_fields_y) + + return np.sum(blocked_fields) / (self.w * self.h) + + def get_letters(self, y: int, x: int, length: int, is_vertical: bool): + if is_vertical: + return self.letter_grid[y:y+length, x].flatten() + return self.letter_grid[y, x:x+length].flatten() + + def get_max_extents(self, y: int, x: int, is_vertical: bool): + # check min max offsets + if is_vertical: + min_coord = y - 1 + if min_coord < 0 or self.y_grid[min_coord, x] == TYPE_BLOCKED: + min_coord = y + else: + while min_coord > 0 and self.y_grid[min_coord - 1, x] != TYPE_BLOCKED: + min_coord -= 1 + max_coord = y + 1 + while max_coord < self.h and self.y_grid[max_coord, x] != TYPE_BLOCKED: + max_coord += 1 + + return min_coord, max_coord + else: + min_coord = x - 1 + if min_coord < 0 or self.x_grid[y, min_coord] == TYPE_BLOCKED: + min_coord = x + else: + while min_coord > 0 and self.x_grid[y, min_coord - 1] != TYPE_BLOCKED: + min_coord -= 1 + max_coord = x + 1 + while max_coord < self.w and self.x_grid[y, max_coord] != TYPE_BLOCKED: + max_coord += 1 + return min_coord, max_coord + + def expand_coordinates(self, y: int, x: int, length: int, is_vertical: bool): + if is_vertical: + min_coord = y + max_coord = y + length + while min_coord > 0 and self.y_grid[min_coord - 1, x] >= 0: + min_coord -= 1 + while max_coord < self.h and self.y_grid[max_coord, x] >= 0: + max_coord += 1 + + return min_coord, max_coord + else: + min_coord = x + max_coord = x + length + while min_coord > 0 and self.x_grid[y, min_coord - 1] >= 0: + min_coord -= 1 + while max_coord < self.w and self.x_grid[y, max_coord] >= 0: + max_coord += 1 + + return min_coord, max_coord + + def place_random_word(self, min_length: int = 4, max_length: int = 15): + # first, find a random intersection + letter_locations = np.argwhere(self.letter_grid != ' ') + if len(letter_locations) == 0: + # if nothing is placed so far, just choose a random place + length = np.random.randint(min_length, max_length) + length = min(length, max_length) + y = np.random.randint(0, self.h - 1) + x = np.random.randint(0, self.w - length) + is_vertical = False + word_template = " " * length + else: + # possible candidates are fields where words are placed + # only horizontally or only vertically + candidates = np.argwhere( + np.logical_xor(self.x_grid >= 0, self.y_grid >= 0) + ) + + if len(candidates) == 0: + #print("field is full") + return None + + candidate_index = random.randint(0, len(candidates) - 1) + y, x = candidates[candidate_index] + + is_vertical = self.x_grid[y, x] == TYPE_BLOCKED + + min_coord, max_coord = self.get_max_extents(y, x, is_vertical) + + extent = max_coord - min_coord + + if extent < min_length: + #print("not enough space to place a word") + return None + + min_length = min(extent, min_length) + max_length = min(extent, max_length) + + length = random.randint(min_length, max_length) + offset = random.randint(0, extent - length) + + min_coord += offset + + if is_vertical: + if min_coord + length <= y: + min_coord = y - length + 1 + max_coord = min_coord + length + if min_coord > y: + min_coord = y + max_coord = min_coord + length + + min_coord, max_coord = self.expand_coordinates(y=min_coord, + x=x, + length=length, + is_vertical=is_vertical) + + length = max_coord - min_coord + + letters = self.get_letters(min_coord, x, length, is_vertical) + + y = min_coord + + else: + + if min_coord + length <= x: + min_coord = x - length + 1 + max_coord = min_coord + length + if min_coord > x: + min_coord = x + max_coord = min_coord + length + + min_coord, max_coord = self.expand_coordinates(y=y, + x=min_coord, + length=length, + is_vertical=is_vertical) + + length = max_coord - min_coord + + letters = self.get_letters(y, min_coord, length, is_vertical) + + x = min_coord + + word_template = "".join(letters) + + word_candidates = list(find_suitable_words( + word_template, self.db, self.inverted_db)) + + if len(word_candidates) == 0: + #print("no word available for given combination") + return None + + word_candidate_index = random.randint(0, len(word_candidates) - 1) + word_key = word_candidates[word_candidate_index] + + if word_key in self.used_word_keys: + return None + + return self.write_word(word_key, y, x, is_vertical) + + def solve_conflicts(self, conflicts, n_retries=3, max_depth=5, depth=0): + if len(conflicts) == 0: + return self + # else: + # return None + + if depth > max_depth: + return None + + new_conflictes = [] + + for conflict in conflicts: + + y, x = conflict + + if self.x_grid[y, x] >= 0 and self.y_grid[y, x] >= 0: + # conflict already solved + continue + + # find out whether the conflict is vertical or horizontal + is_vertical = self.y_grid[y, x] == TYPE_NEIGHBOR + + # calculate the minimum and maximum extend to fix the conflict + if is_vertical: + max_ymin = y + while max_ymin > 0 and self.y_grid[max_ymin-1, x] >= 0: + max_ymin -= 1 + min_ymax = y + 1 + while min_ymax < self.h and self.y_grid[min_ymax, x] >= 0: + min_ymax += 1 + + min_ymin = max_ymin + while min_ymin > 0 and self.y_grid[min_ymin - 1, x] != TYPE_BLOCKED: + min_ymin -= 1 + max_ymax = min_ymax + while max_ymax < self.h and self.y_grid[max_ymax, x] != TYPE_BLOCKED: + max_ymax += 1 + + min_coord_min = min_ymin + max_coord_min = max_ymin + min_coord_max = min_ymax + max_coord_max = max_ymax + + else: + max_xmin = x + while max_xmin > 0 and self.x_grid[y, max_xmin - 1] >= 0: + max_xmin -= 1 + min_xmax = x + 1 + while min_xmax < self.w and self.x_grid[y, min_xmax] >= 0: + min_xmax += 1 + + min_xmin = max_xmin + while min_xmin > 0 and self.x_grid[y, min_xmin - 1] != TYPE_BLOCKED: + min_xmin -= 1 + max_xmax = min_xmax + while max_xmax < self.w and self.x_grid[y, max_xmax] != TYPE_BLOCKED: + max_xmax += 1 + + min_coord_min = min_xmin + max_coord_min = max_xmin + min_coord_max = min_xmax + max_coord_max = max_xmax + + n_options = max_coord_max - min_coord_max + max_coord_min - min_coord_min + + solved = False + + for _ in range(min(n_options, n_retries)): + coord_min = random.randint(min_coord_min, max_coord_min) + coord_max = random.randint(min_coord_max, max_coord_max) + length = coord_max - coord_min + if length < 2: + continue + + if is_vertical: + + coord_min, coord_max = self.expand_coordinates(y=coord_min, + x=x, + length=length, + is_vertical=is_vertical) + + length = coord_max - coord_min + + y = coord_min + + else: + + coord_min, coord_max = self.expand_coordinates(y=y, + x=coord_min, + length=length, + is_vertical=is_vertical) + + length = coord_max - coord_min + + x = coord_min + + letters = self.get_letters(y, x, length, is_vertical) + + word_template = "".join(letters) + + candidates = list(find_suitable_words( + word_template, self.db, self.inverted_db)) + + if len(candidates) == 0: + continue + + candidate_index = random.randint(0, len(candidates) - 1) + word_key = candidates[candidate_index] + + if word_key in self.used_word_keys: + continue + + word_conflicts = self.write_word(word_key, y, x, is_vertical) + if len(word_conflicts) > 0: + new_conflictes.append(word_conflicts) + + solved = True + break + + if not solved: + return None + + if len(new_conflictes) == 0: + return self + + new_conflictes = np.concatenate(new_conflictes) + for _ in range(n_retries): + next_state = self.copy() + solved_state = next_state.solve_conflicts( + new_conflictes, n_retries, max_depth, depth + 1) + if solved_state is not None: + return solved_state + return None + + def fill_grid(self, target_density: float = 0.6, inner_retries: int = 5, conflict_retries: int = 10, conflict_solver_depth=5, min_length: int = 4, max_length: int = 10, max_iterations: int = 1000): + i = 0 + state = self.copy() + while i < max_iterations and state.get_density() < target_density: + i += 1 + new_state = state.copy() + conflicts = new_state.place_random_word(min_length, max_length) + if conflicts is None: + continue + if len(conflicts) == 0: + state = new_state + + if len(conflicts) > 0: + + solved_state = new_state.solve_conflicts( + conflicts, inner_retries, conflict_solver_depth) + if solved_state is not None: + state = solved_state + + logging.info("finished after %s iterations, with a density of %s", str( + i), str(state.get_density())) + return state + + +def create_word_grid(w: int, + h: int, + lang_code: str = "en", + target_density: float = 0.8, + difficulty: int = 0): + logging.info("generate new crossword with params: w:%s h:%s lang:%s density:%s difficulty:%s", str(w), str(h), @@ -142,233 +770,86 @@ def create_word_grid(w: int, h: int, lang_code: str = "en", target_density: floa str(target_density), str(difficulty)) - t_num_translations = get_difficulty_threshold(lang = lang_code, difficulty = difficulty) + db = get_database(lang_code, difficulty=difficulty) + inverted_db = get_inverted_database(lang_code, difficulty=difficulty) - database = get_database(lang=lang_code) - list_words = list(database.keys()) + base_grid = GridCreationState(h=h, w=w, db=db, inverted_db=inverted_db) - grid = np.full(shape=(h, w), dtype=np.unicode, fill_value=' ') + final_state = base_grid.fill_grid(target_density=target_density, + inner_retries=7, + conflict_solver_depth=20, + min_length=2, + max_iterations=max(75 * (w+h)/2, 1000)) - locations = {} + # generate word hints word_hints = {} - def store_location(char: str, y: int, x: int): - assert len(char) == 1 + opposite_prefix = "opposite of:" if lang_code == "en" else "Gegenteil von:" + synonym_prefix = "other word for:" if lang_code == "en" else "anderes Wort für:" - if char not in locations: - locations[char] = [] + for placed_word in final_state.placed_words: + word_key = placed_word.word_key + word = normalize_word(db[word_key]['word']) + y = placed_word.y + x = placed_word.x + is_vertical = placed_word.is_vertical - if [y,x] not in locations[char]: - locations[char].append([y, x]) + word_info = WordInfo(word_key, y, x, is_vertical, + db, opposite_prefix, synonym_prefix) + #logging.info("word: %s, (%s,%s,%s): %s", word, str(y), str(x), str(is_vertical), word_info.get_hint()) + word_hints[word_key] = word_info - remove_digits = str.maketrans('', '', digits) - n_words = len(list_words) + # create a solution word - def get_word(max_length: int, min_length=0): - assert max_length > 1 + char_locations = {} + for char in list("abcdefghijklmnopqrstuvwxyz"): + char_locations[char] = np.argwhere( + final_state.letter_grid == char).tolist() - index = random.randint(0, n_words-1) - word = list_words[index][:] + words = list(db.keys()) + n_words = len(words) - num_translations = database[word]['num_translations'] - t = t_num_translations + min_solution_length = 10 + max_solution_length = 20 - while len(word) >= max_length or not word.isalnum() or len(word) <= min_length or num_translations < t: - index = random.randint(0, n_words-1) - word = list_words[index][:] - num_translations = database[word]['num_translations'] + solution_word_locations = None - return word + while solution_word_locations is None: - def normalize_word(word: str): - word = word.translate(remove_digits) - return word.lower() - - opposite_prefix = "opposite of" if lang_code == "en" else "Gegenteil von" - synonym_prefix = "other word for" if lang_code == "en" else "anderes Wort für" - - def place_word(word: str, y: int, x: int, vertical: bool = False): - normalized_word = normalize_word(word) - n = len(normalized_word) - if vertical: - assert grid.shape[0] - n >= y - for i, char in enumerate(normalized_word): - grid[y + i, x] = char - store_location(char, y+i, x) - else: - assert grid.shape[1] - n >= x - for i, char in enumerate(normalized_word): - grid[y, x + i] = char - store_location(char, y, x+i) - - word_hints[normalized_word] = WordInfo( - word, y, x, vertical, database, opposite_prefix, synonym_prefix) - - def density(): - return 1 - (grid == " ").sum() / (w * h) - - def check_if_fits(word: str, y: int, x: int, vertical: bool): - n = len(word) - if vertical: - - # check if there is space before and after - if y - 1 >= 0 and grid[y - 1, x] != " ": - return False - if y + n < grid.shape[0] and grid[y+n, x] != " ": - return False - - if grid.shape[0] - n < y or y < 0: - # print("over board") - return False - - for i, char in enumerate(word): - char_x = x - char_y = y + i - - if not (grid[char_y, char_x] == " " or grid[char_y, char_x] == char): - # print("not matching") - return False - - if grid[char_y, char_x] == " ": - # check for horizonatal neighbors: - if char_x - 1 >= 0 and grid[char_y, char_x - 1] != " ": - # print("3") - return False - if char_x + 1 < grid.shape[1] and grid[char_y, char_x + 1] != " ": - # print("4") - return False - - else: - - # check if there is space before and after - if x - 1 >= 0 and grid[y, x - 1] != " ": - return False - if x + n < grid.shape[1] and grid[y, x + n] != " ": - return False - - if grid.shape[1] - n < x or x < 0: - # print("over board") - return False - - for i, char in enumerate(word): - char_x = x + i - char_y = y - - if not (grid[char_y, char_x] == " " or grid[char_y, char_x] == char): - # print("not matching") - return False - - if grid[char_y, char_x] == " ": - # check for vertical neighbors: - if char_y - 1 >= 0 and grid[char_y - 1, char_x] != " ": - # print("1") - return False - if char_y + 1 < grid.shape[0] and grid[char_y + 1, char_x] != " ": - # print("2") - return False - - return True - - def get_crossover(word: str): - # returns Tuple of: (y,x, is_vertical?) or None - - shuffled_order = list(range(len(word))) - random.shuffle(shuffled_order) - - for index in shuffled_order: - # check for existing locations - char = word[index] - if char in locations: - char_locations = locations[char] - - for char_loc in char_locations: - # test vertical - y = char_loc[0] - index - x = char_loc[1] - - if check_if_fits(word, y, x, vertical=True): - return (y, x, True) - - # test horizontal - y = char_loc[0] - x = char_loc[1] - index - - if check_if_fits(word, y, x, vertical=False): - return (y, x, False) - - return None - - def get_solution_word(min_length=15, max_length=100): - word = get_word(min_length=min_length, max_length=max_length) - - # search for matching characters in locations - locations_cpy = dict(locations) - solution_locations = [] - - for char in word: - if char not in locations_cpy or len(locations_cpy[char]) == 0: - # next try: - return get_solution_word(min_length=min_length, max_length=max_length) - - location_candidates = locations_cpy[char] - - n = len(location_candidates) - - i = random.randint(0, n-1) - - solution_locations.append(location_candidates[i]) - del(location_candidates[i]) - - return solution_locations - - min_shape = min(w, h, 30) - - # place first word: - first_word = get_word(max_length=min_shape, - min_length=min(10, grid.shape[1] - 2)) - - # find random place: - x = random.randint(0, grid.shape[1] - len(first_word) - 1) - y = random.randint(0, grid.shape[0] - 1) - - place_word(first_word, y, x, vertical=False) - - i = 0 - - current_density = density() - - while current_density < target_density: - word = get_word(max_length=(1 - current_density ** 0.4) * min_shape, - min_length=max(min(10, 0.5 * (1 - current_density ** 0.3) * min_shape), 2)) - - normalized_word = normalize_word(word) - - if normalized_word in word_hints: + random_index = random.randint(0, n_words - 1) + random_word_key = words[random_index] + random_word = db[random_word_key]['word'] + normalized_random_word = normalize_word(random_word) + if len(normalized_random_word) < min_solution_length or len(normalized_random_word) > max_solution_length: continue - # check if matching characters exist: - crossover = get_crossover(normalized_word) + char_locations_copy = {} + for char in char_locations: + char_locations_copy[char] = char_locations[char].copy() - i += 1 - if i % 1000 == 0: - print(i) - if i > 1200: - break + solution = [] - if crossover == None: - current_density = density() + aborted = False + for char in list(normalized_random_word): + if char not in char_locations_copy: + aborted = True + break + locations = char_locations_copy[char] + if len(locations) == 0: + aborted = True + break + + i = random.randint(0, len(locations) - 1) + location = locations[i] + del(locations[i]) + solution.append(location) + + if aborted: continue - y, x, is_vertical = crossover + solution_word_locations = solution - place_word(word, y, x, is_vertical) - - current_density = density() - - solution_word_locations = get_solution_word() - - logging.info("crossword generation done after %s iterations", str(i)) - return grid, word_hints, solution_word_locations + return final_state.letter_grid, word_hints, solution_word_locations diff --git a/server/crossword_generator_old.py b/server/crossword_generator_old.py new file mode 100644 index 0000000..198afe3 --- /dev/null +++ b/server/crossword_generator_old.py @@ -0,0 +1,374 @@ +import json +import random +import numpy as np +from string import digits +import pathlib +import logging + + +def get_difficulty_threshold(lang: str, difficulty: int): + return get_difficulty_threshold.thresholds[lang][difficulty] + + +get_difficulty_threshold.thresholds = { + 'de': { + 0: 12, + 1: 6, + 2: 0 + }, + 'en': { + 0: 200, + 1: 100, + 2: 10 + } +} + + +def get_database(lang: str = "en") -> dict: + if lang not in get_database._dbs: + current_folder = pathlib.Path(__file__).parents[0] + db_file = str(current_folder / f"{lang}.json") + + logging.info("loading database: %s", lang) + + with open(db_file, "r") as f: + db = json.load(f) + get_database._dbs[lang] = db + + logging.info("database loaded") + + return get_database._dbs[lang] + + +get_database._dbs = {} + + +class NoDataException(Exception): + pass + + +class WordInfo(object): + def __init__(self, word: str, y: int, x: int, is_vertical: bool, database: dict, opposite_prefix: str = "opposite of", synonym_prefix: str = "other word for"): + self._dictionary_database = database + self._y = y + self._x = x + self._word = word + self._hint = None + self._is_vertical = is_vertical + + self.opposite_prefix = opposite_prefix + self.synonym_prefix = synonym_prefix + + self.choose_info() + + def get_attribute(self, attr: str): + attr = self._dictionary_database[self._word][attr] + if attr is None or len(attr) == 0: + raise NoDataException + return attr + + def get_best_antonym(self) -> str: + antonyms = self.get_attribute("antonyms") + return random.choice(antonyms) + + def get_best_synonym(self) -> str: + synonyms = self.get_attribute("synonyms") + return random.choice(synonyms) + + def get_best_sense(self) -> str: + senses = self.get_attribute("senses") + return random.choice(senses) + + def choose_info(self, n: int = 1): + assert n <= 4 + # first choose antonyms, then synonyms, then senses + + hints = [] + + try: + antonyms = self.get_attribute("antonyms") + antonyms = [f"{self.opposite_prefix} {w}" for w in antonyms] + hints = hints + antonyms + except NoDataException: + pass + + try: + synonyms = self.get_attribute("synonyms") + synonyms = [f"{self.synonym_prefix} {w}" for w in synonyms] + + hints = hints + synonyms + except NoDataException: + pass + + try: + senses = self.get_attribute("senses") + hints = hints + senses + except NoDataException: + pass + + final_hints = [] + for i in range(n): + choice = random.choice(hints) + hints.remove(choice) + final_hints.append(choice) + + if n == 1: + self._hint = final_hints[0] + return + + hint_symbols = ['a)', 'b)', 'c)', 'd)'] + + self._hint = "" + for i in range(n): + self._hint += hint_symbols[i] + " " + final_hints[i] + ". " + + def get_hint(self) -> str: + return self._hint + + def get_hint_location(self): + x = self._x if self._is_vertical else self._x - 1 + y = self._y - 1 if self._is_vertical else self._y + return (y, x) + + def is_vertical(self): + return self._is_vertical + + +def create_word_grid(w: int, h: int, lang_code: str = "en", target_density: float = 0.5, difficulty: int = 0): + logging.info("generate new crossword with params: w:%s h:%s lang:%s density:%s difficulty:%s", + str(w), + str(h), + lang_code, + str(target_density), + str(difficulty)) + + t_num_translations = get_difficulty_threshold(lang = lang_code, difficulty = difficulty) + + database = get_database(lang=lang_code) + list_words = list(database.keys()) + + grid = np.full(shape=(h, w), dtype=np.unicode, fill_value=' ') + + locations = {} + + word_hints = {} + + def store_location(char: str, y: int, x: int): + assert len(char) == 1 + + if char not in locations: + locations[char] = [] + + if [y,x] not in locations[char]: + locations[char].append([y, x]) + + + + remove_digits = str.maketrans('', '', digits) + n_words = len(list_words) + + def get_word(max_length: int, min_length=0): + assert max_length > 1 + + index = random.randint(0, n_words-1) + word = list_words[index][:] + + num_translations = database[word]['num_translations'] + t = t_num_translations + + while len(word) >= max_length or not word.isalnum() or len(word) <= min_length or num_translations < t: + index = random.randint(0, n_words-1) + word = list_words[index][:] + num_translations = database[word]['num_translations'] + + return word + + def normalize_word(word: str): + word = word.translate(remove_digits) + return word.lower() + + opposite_prefix = "opposite of" if lang_code == "en" else "Gegenteil von" + synonym_prefix = "other word for" if lang_code == "en" else "anderes Wort für" + + def place_word(word: str, y: int, x: int, vertical: bool = False): + normalized_word = normalize_word(word) + n = len(normalized_word) + if vertical: + assert grid.shape[0] - n >= y + for i, char in enumerate(normalized_word): + grid[y + i, x] = char + store_location(char, y+i, x) + else: + assert grid.shape[1] - n >= x + for i, char in enumerate(normalized_word): + grid[y, x + i] = char + store_location(char, y, x+i) + + word_hints[normalized_word] = WordInfo( + word, y, x, vertical, database, opposite_prefix, synonym_prefix) + + def density(): + return 1 - (grid == " ").sum() / (w * h) + + def check_if_fits(word: str, y: int, x: int, vertical: bool): + n = len(word) + if vertical: + + # check if there is space before and after + if y - 1 >= 0 and grid[y - 1, x] != " ": + return False + if y + n < grid.shape[0] and grid[y+n, x] != " ": + return False + + if grid.shape[0] - n < y or y < 0: + # print("over board") + return False + + for i, char in enumerate(word): + char_x = x + char_y = y + i + + if not (grid[char_y, char_x] == " " or grid[char_y, char_x] == char): + # print("not matching") + return False + + if grid[char_y, char_x] == " ": + # check for horizonatal neighbors: + if char_x - 1 >= 0 and grid[char_y, char_x - 1] != " ": + # print("3") + return False + if char_x + 1 < grid.shape[1] and grid[char_y, char_x + 1] != " ": + # print("4") + return False + + else: + + # check if there is space before and after + if x - 1 >= 0 and grid[y, x - 1] != " ": + return False + if x + n < grid.shape[1] and grid[y, x + n] != " ": + return False + + if grid.shape[1] - n < x or x < 0: + # print("over board") + return False + + for i, char in enumerate(word): + char_x = x + i + char_y = y + + if not (grid[char_y, char_x] == " " or grid[char_y, char_x] == char): + # print("not matching") + return False + + if grid[char_y, char_x] == " ": + # check for vertical neighbors: + if char_y - 1 >= 0 and grid[char_y - 1, char_x] != " ": + # print("1") + return False + if char_y + 1 < grid.shape[0] and grid[char_y + 1, char_x] != " ": + # print("2") + return False + + return True + + def get_crossover(word: str): + # returns Tuple of: (y,x, is_vertical?) or None + + shuffled_order = list(range(len(word))) + random.shuffle(shuffled_order) + + for index in shuffled_order: + # check for existing locations + char = word[index] + if char in locations: + char_locations = locations[char] + + for char_loc in char_locations: + # test vertical + y = char_loc[0] - index + x = char_loc[1] + + if check_if_fits(word, y, x, vertical=True): + return (y, x, True) + + # test horizontal + y = char_loc[0] + x = char_loc[1] - index + + if check_if_fits(word, y, x, vertical=False): + return (y, x, False) + + return None + + def get_solution_word(min_length=15, max_length=100): + word = get_word(min_length=min_length, max_length=max_length) + + # search for matching characters in locations + locations_cpy = dict(locations) + solution_locations = [] + + for char in word: + if char not in locations_cpy or len(locations_cpy[char]) == 0: + # next try: + return get_solution_word(min_length=min_length, max_length=max_length) + + location_candidates = locations_cpy[char] + + n = len(location_candidates) + + i = random.randint(0, n-1) + + solution_locations.append(location_candidates[i]) + del(location_candidates[i]) + + return solution_locations + + min_shape = min(w, h, 30) + + # place first word: + first_word = get_word(max_length=min_shape, + min_length=min(10, grid.shape[1] - 2)) + + # find random place: + x = random.randint(0, grid.shape[1] - len(first_word) - 1) + y = random.randint(0, grid.shape[0] - 1) + + place_word(first_word, y, x, vertical=False) + + i = 0 + + current_density = density() + + while current_density < target_density: + word = get_word(max_length=(1 - current_density ** 0.4) * min_shape, + min_length=max(min(10, 0.5 * (1 - current_density ** 0.3) * min_shape), 2)) + + normalized_word = normalize_word(word) + + if normalized_word in word_hints: + continue + + # check if matching characters exist: + crossover = get_crossover(normalized_word) + + i += 1 + if i % 1000 == 0: + print(i) + if i > 1200: + break + + if crossover == None: + current_density = density() + continue + + y, x, is_vertical = crossover + + place_word(word, y, x, is_vertical) + + current_density = density() + + solution_word_locations = get_solution_word() + + logging.info("crossword generation done after %s iterations", str(i)) + return grid, word_hints, solution_word_locations