Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
0795dff811
|
|||
|
eb2c2771c4
|
|||
|
9036816e97
|
|||
|
ab7fb24b01
|
|||
|
a2f96e247a
|
|||
|
cf98f5ccbd
|
|||
|
fcd7f6aaf9
|
|||
|
042e8b8581
|
|||
|
9069f751c3
|
|||
|
472956c76d
|
|||
|
27c275bccb
|
|||
|
f89a031997
|
|||
| 8033b498f9 |
@@ -1,4 +1,5 @@
|
|||||||
### Python template
|
### Python template
|
||||||
|
scripts/
|
||||||
|
|
||||||
deploy/
|
deploy/
|
||||||
.idea/
|
.idea/
|
||||||
|
|||||||
@@ -1,2 +1,4 @@
|
|||||||
**/*.csv filter=lfs diff=lfs merge=lfs -text
|
**/*.csv filter=lfs diff=lfs merge=lfs -text
|
||||||
**/*.png filter=lfs diff=lfs merge=lfs -text
|
**/*.png filter=lfs diff=lfs merge=lfs -text
|
||||||
|
scripts/dicts/data/*.txt filter=lfs diff=lfs merge=lfs -text
|
||||||
|
**/*.gz filter=lfs diff=lfs merge=lfs -text
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ jobs:
|
|||||||
lint:
|
lint:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v7
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v6
|
uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
@@ -20,15 +20,7 @@ jobs:
|
|||||||
pytest:
|
pytest:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v7
|
||||||
- name: Set up Python
|
|
||||||
uses: actions/setup-python@v6
|
|
||||||
with:
|
|
||||||
python-version: '3.12'
|
|
||||||
- name: Update docker-compose
|
|
||||||
uses: KengoTODA/actions-setup-docker-compose@v1
|
|
||||||
with:
|
|
||||||
version: "2.28.0"
|
|
||||||
- name: run tests
|
- name: run tests
|
||||||
run: docker compose run --build --rm api pytest -vv
|
run: docker compose run --build --rm api pytest -vv
|
||||||
- name: show logs
|
- name: show logs
|
||||||
|
|||||||
+6
-5
@@ -25,8 +25,8 @@ services:
|
|||||||
ZANGRAMRU_DB_USER: zangramru
|
ZANGRAMRU_DB_USER: zangramru
|
||||||
ZANGRAMRU_DB_PASS: zangramru
|
ZANGRAMRU_DB_PASS: zangramru
|
||||||
ZANGRAMRU_DB_BASE: zangramru
|
ZANGRAMRU_DB_BASE: zangramru
|
||||||
volumes:
|
# volumes:
|
||||||
- .:/app/src/
|
# - .:/app/src/
|
||||||
|
|
||||||
db:
|
db:
|
||||||
image: postgres:18.3-trixie
|
image: postgres:18.3-trixie
|
||||||
@@ -49,13 +49,14 @@ services:
|
|||||||
migrator:
|
migrator:
|
||||||
<<: *main_app
|
<<: *main_app
|
||||||
restart: "no"
|
restart: "no"
|
||||||
command: piccolo migrations forwards all
|
command:
|
||||||
|
- bash
|
||||||
|
- -c
|
||||||
|
- "ls && piccolo migrations forwards all"
|
||||||
depends_on:
|
depends_on:
|
||||||
db:
|
db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
zangramru-db-data:
|
zangramru-db-data:
|
||||||
name: zangramru-db-data
|
name: zangramru-db-data
|
||||||
|
|||||||
-115
@@ -1,115 +0,0 @@
|
|||||||
digraph model_graph {
|
|
||||||
fontname = "Roboto"
|
|
||||||
fontsize = 8
|
|
||||||
splines = true
|
|
||||||
rankdir = "LR";
|
|
||||||
|
|
||||||
node [
|
|
||||||
fontname = "Roboto"
|
|
||||||
fontsize = 8
|
|
||||||
shape = "plaintext"
|
|
||||||
]
|
|
||||||
|
|
||||||
edge [
|
|
||||||
fontname = "Roboto"
|
|
||||||
fontsize = 8
|
|
||||||
]
|
|
||||||
|
|
||||||
// Tables
|
|
||||||
|
|
||||||
TABLE_Puzzle [label=<
|
|
||||||
<TABLE BGCOLOR="white" BORDER="1" CELLBORDER="0" CELLSPACING="0">
|
|
||||||
<TR>
|
|
||||||
<TD COLSPAN="2" CELLPADDING="5" ALIGN="CENTER" BGCOLOR="#4C89C8">
|
|
||||||
<FONT FACE="Roboto" COLOR="white" POINT-SIZE="10">
|
|
||||||
<B>Puzzle</B>
|
|
||||||
</FONT>
|
|
||||||
</TD>
|
|
||||||
</TR>
|
|
||||||
|
|
||||||
|
|
||||||
<TR>
|
|
||||||
<TD ALIGN="LEFT" BORDER="0">
|
|
||||||
<FONT FACE="Roboto">
|
|
||||||
<B>id</B>
|
|
||||||
</FONT>
|
|
||||||
</TD>
|
|
||||||
<TD ALIGN="LEFT">
|
|
||||||
<FONT FACE="Roboto">
|
|
||||||
<B>UUID</B>
|
|
||||||
</FONT>
|
|
||||||
</TD>
|
|
||||||
</TR>
|
|
||||||
|
|
||||||
<TR>
|
|
||||||
<TD ALIGN="LEFT" BORDER="0">
|
|
||||||
<FONT FACE="Roboto">
|
|
||||||
<B>board</B>
|
|
||||||
</FONT>
|
|
||||||
</TD>
|
|
||||||
<TD ALIGN="LEFT">
|
|
||||||
<FONT FACE="Roboto">
|
|
||||||
<B>Varchar</B>
|
|
||||||
</FONT>
|
|
||||||
</TD>
|
|
||||||
</TR>
|
|
||||||
|
|
||||||
<TR>
|
|
||||||
<TD ALIGN="LEFT" BORDER="0">
|
|
||||||
<FONT FACE="Roboto">
|
|
||||||
<B>solutions</B>
|
|
||||||
</FONT>
|
|
||||||
</TD>
|
|
||||||
<TD ALIGN="LEFT">
|
|
||||||
<FONT FACE="Roboto">
|
|
||||||
<B>JSON</B>
|
|
||||||
</FONT>
|
|
||||||
</TD>
|
|
||||||
</TR>
|
|
||||||
|
|
||||||
<TR>
|
|
||||||
<TD ALIGN="LEFT" BORDER="0">
|
|
||||||
<FONT FACE="Roboto">
|
|
||||||
<B>extra</B>
|
|
||||||
</FONT>
|
|
||||||
</TD>
|
|
||||||
<TD ALIGN="LEFT">
|
|
||||||
<FONT FACE="Roboto">
|
|
||||||
<B>JSON</B>
|
|
||||||
</FONT>
|
|
||||||
</TD>
|
|
||||||
</TR>
|
|
||||||
|
|
||||||
<TR>
|
|
||||||
<TD ALIGN="LEFT" BORDER="0">
|
|
||||||
<FONT FACE="Roboto">
|
|
||||||
<B>definitions</B>
|
|
||||||
</FONT>
|
|
||||||
</TD>
|
|
||||||
<TD ALIGN="LEFT">
|
|
||||||
<FONT FACE="Roboto">
|
|
||||||
<B>JSON</B>
|
|
||||||
</FONT>
|
|
||||||
</TD>
|
|
||||||
</TR>
|
|
||||||
|
|
||||||
<TR>
|
|
||||||
<TD ALIGN="LEFT" BORDER="0">
|
|
||||||
<FONT FACE="Roboto">
|
|
||||||
<B>date</B>
|
|
||||||
</FONT>
|
|
||||||
</TD>
|
|
||||||
<TD ALIGN="LEFT">
|
|
||||||
<FONT FACE="Roboto">
|
|
||||||
<B>Date</B>
|
|
||||||
</FONT>
|
|
||||||
</TD>
|
|
||||||
</TR>
|
|
||||||
|
|
||||||
</TABLE>
|
|
||||||
>]
|
|
||||||
|
|
||||||
|
|
||||||
// Relations
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import csv
|
||||||
|
import gzip
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from collections import defaultdict
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from zangramru.utils.trie import TrieNode
|
||||||
|
|
||||||
|
CWD = Path(__file__).parent
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
|
||||||
|
|
||||||
|
def build_trie(dict_path: Path) -> TrieNode:
|
||||||
|
"""Build trie for wikidict definitions."""
|
||||||
|
trie = TrieNode()
|
||||||
|
total_words = 0
|
||||||
|
with gzip.open(dict_path) as f_in:
|
||||||
|
for line in f_in:
|
||||||
|
word_info = json.loads(line)
|
||||||
|
# If it starts wit capital.
|
||||||
|
if word_info.get("lang_code") != "ru":
|
||||||
|
continue
|
||||||
|
if word_info.get("pos") != "noun":
|
||||||
|
continue
|
||||||
|
if word_info["word"][0].isupper():
|
||||||
|
continue
|
||||||
|
senses = word_info.get("senses", [])
|
||||||
|
multi_def = []
|
||||||
|
for sense in senses:
|
||||||
|
glosses = sense.get("glosses")
|
||||||
|
if glosses:
|
||||||
|
multi_def.extend(glosses)
|
||||||
|
|
||||||
|
total_words += 1
|
||||||
|
trie.add(word_info["word"], definitions=multi_def)
|
||||||
|
|
||||||
|
logger.info(f"Trie is ready. Total words: {total_words}")
|
||||||
|
return trie
|
||||||
|
|
||||||
|
|
||||||
|
def get_defs(
|
||||||
|
dict_file: Path,
|
||||||
|
defs: TrieNode,
|
||||||
|
stats: dict[str, int],
|
||||||
|
) -> dict[str, list[str]]:
|
||||||
|
"""Find all word definitions for all our dicts."""
|
||||||
|
word_def = {}
|
||||||
|
with dict_file.open("r") as f_in:
|
||||||
|
for line in f_in:
|
||||||
|
if len(line.strip()) < 4:
|
||||||
|
stats["too_short"] += 1
|
||||||
|
continue
|
||||||
|
node = defs.find_node(line)
|
||||||
|
if node is None:
|
||||||
|
stats["not_found"] += 1
|
||||||
|
continue
|
||||||
|
if not node.valid_word:
|
||||||
|
stats["invalid"] += 1
|
||||||
|
continue
|
||||||
|
word_def[node.value] = node.definitions
|
||||||
|
return word_def
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
"""Update dictionaries."""
|
||||||
|
trie = build_trie(Path(CWD / "wikidict.jsonl.gz"))
|
||||||
|
with Path(CWD.parent.parent / "zangramru/services/dict.csv").open("w") as f_out:
|
||||||
|
writer = csv.writer(f_out, delimiter="|")
|
||||||
|
writer.writerow(["word", "def"])
|
||||||
|
stats = defaultdict(int)
|
||||||
|
words: dict[str, list[str]] = {}
|
||||||
|
|
||||||
|
for file in Path(CWD / "data").iterdir():
|
||||||
|
logger.info(f"Processing {file}")
|
||||||
|
words.update(get_defs(file, trie, stats))
|
||||||
|
|
||||||
|
logger.info("Done updating")
|
||||||
|
logger.info(stats)
|
||||||
|
|
||||||
|
for word, defs in words.items():
|
||||||
|
all_defs = ""
|
||||||
|
if len(defs) == 1:
|
||||||
|
all_defs = defs[0]
|
||||||
|
else:
|
||||||
|
all_defs = "\n".join(f"{i + 1}: {defn}" for i, defn in enumerate(defs))
|
||||||
|
writer.writerow([word, all_defs])
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
и
|
||||||
|
в
|
||||||
|
не
|
||||||
|
на
|
||||||
|
я
|
||||||
|
быть
|
||||||
|
он
|
||||||
|
с
|
||||||
|
что
|
||||||
|
а
|
||||||
|
по
|
||||||
|
это
|
||||||
|
она
|
||||||
|
этот
|
||||||
|
к
|
||||||
|
но
|
||||||
|
они
|
||||||
|
мы
|
||||||
|
как
|
||||||
|
из
|
||||||
|
у
|
||||||
|
который
|
||||||
|
то
|
||||||
|
за
|
||||||
|
свой
|
||||||
|
что
|
||||||
|
весь
|
||||||
|
год
|
||||||
|
от
|
||||||
|
так
|
||||||
|
о
|
||||||
|
для
|
||||||
|
ты
|
||||||
|
же
|
||||||
|
все
|
||||||
|
тот
|
||||||
|
мочь
|
||||||
|
вы
|
||||||
|
человек
|
||||||
|
такой
|
||||||
|
его
|
||||||
|
сказать
|
||||||
|
только
|
||||||
|
или
|
||||||
|
ещё
|
||||||
|
бы
|
||||||
|
себя
|
||||||
|
один
|
||||||
|
как
|
||||||
|
уже
|
||||||
|
до
|
||||||
|
время
|
||||||
|
если
|
||||||
|
сам
|
||||||
|
когда
|
||||||
|
другой
|
||||||
|
вот
|
||||||
|
говорить
|
||||||
|
наш
|
||||||
|
мой
|
||||||
|
знать
|
||||||
|
стать
|
||||||
|
при
|
||||||
|
чтобы
|
||||||
|
дело
|
||||||
|
жизнь
|
||||||
|
кто
|
||||||
|
первый
|
||||||
|
очень
|
||||||
|
два
|
||||||
|
день
|
||||||
|
её
|
||||||
|
новый
|
||||||
|
рука
|
||||||
|
даже
|
||||||
|
во
|
||||||
|
со
|
||||||
|
раз
|
||||||
|
где
|
||||||
|
там
|
||||||
|
под
|
||||||
|
можно
|
||||||
|
ну
|
||||||
|
какой
|
||||||
|
после
|
||||||
|
их
|
||||||
|
работа
|
||||||
|
без
|
||||||
|
самый
|
||||||
|
потом
|
||||||
|
надо
|
||||||
|
хотеть
|
||||||
|
ли
|
||||||
|
слово
|
||||||
|
идти
|
||||||
|
большой
|
||||||
|
должен
|
||||||
|
место
|
||||||
|
иметь
|
||||||
|
ничто
|
||||||
@@ -0,0 +1,900 @@
|
|||||||
|
то
|
||||||
|
сейчас
|
||||||
|
тут
|
||||||
|
лицо
|
||||||
|
каждый
|
||||||
|
друг
|
||||||
|
нет
|
||||||
|
теперь
|
||||||
|
ни
|
||||||
|
глаз
|
||||||
|
тоже
|
||||||
|
тогда
|
||||||
|
видеть
|
||||||
|
вопрос
|
||||||
|
через
|
||||||
|
да
|
||||||
|
здесь
|
||||||
|
дом
|
||||||
|
да
|
||||||
|
потому
|
||||||
|
сторона
|
||||||
|
какой-то
|
||||||
|
думать
|
||||||
|
сделать
|
||||||
|
страна
|
||||||
|
жить
|
||||||
|
чем
|
||||||
|
мир
|
||||||
|
об
|
||||||
|
последний
|
||||||
|
случай
|
||||||
|
голова
|
||||||
|
более
|
||||||
|
делать
|
||||||
|
что-то
|
||||||
|
смотреть
|
||||||
|
ребенок
|
||||||
|
просто
|
||||||
|
конечно
|
||||||
|
сила
|
||||||
|
российский
|
||||||
|
конец
|
||||||
|
перед
|
||||||
|
несколько
|
||||||
|
вид
|
||||||
|
система
|
||||||
|
всегда
|
||||||
|
работать
|
||||||
|
между
|
||||||
|
три
|
||||||
|
нет
|
||||||
|
понять
|
||||||
|
пойти
|
||||||
|
часть
|
||||||
|
спросить
|
||||||
|
город
|
||||||
|
дать
|
||||||
|
также
|
||||||
|
никто
|
||||||
|
понимать
|
||||||
|
получить
|
||||||
|
отношение
|
||||||
|
лишь
|
||||||
|
второй
|
||||||
|
именно
|
||||||
|
ваш
|
||||||
|
хотя
|
||||||
|
ни
|
||||||
|
сидеть
|
||||||
|
над
|
||||||
|
женщина
|
||||||
|
оказаться
|
||||||
|
русский
|
||||||
|
один
|
||||||
|
взять
|
||||||
|
прийти
|
||||||
|
являться
|
||||||
|
деньги
|
||||||
|
почему
|
||||||
|
вдруг
|
||||||
|
любить
|
||||||
|
стоить
|
||||||
|
почти
|
||||||
|
земля
|
||||||
|
общий
|
||||||
|
ведь
|
||||||
|
машина
|
||||||
|
однако
|
||||||
|
сразу
|
||||||
|
хорошо
|
||||||
|
вода
|
||||||
|
отец
|
||||||
|
высокий
|
||||||
|
остаться
|
||||||
|
выйти
|
||||||
|
много
|
||||||
|
проблема
|
||||||
|
начать
|
||||||
|
хороший
|
||||||
|
час
|
||||||
|
это
|
||||||
|
сегодня
|
||||||
|
право
|
||||||
|
совсем
|
||||||
|
нога
|
||||||
|
считать
|
||||||
|
главный
|
||||||
|
решение
|
||||||
|
увидеть
|
||||||
|
дверь
|
||||||
|
казаться
|
||||||
|
образ
|
||||||
|
писать
|
||||||
|
история
|
||||||
|
лучший
|
||||||
|
власть
|
||||||
|
закон
|
||||||
|
все
|
||||||
|
война
|
||||||
|
бог
|
||||||
|
голос
|
||||||
|
найти
|
||||||
|
поэтому
|
||||||
|
стоять
|
||||||
|
вообще
|
||||||
|
тысяча
|
||||||
|
больше
|
||||||
|
вместе
|
||||||
|
маленький
|
||||||
|
книга
|
||||||
|
некоторый
|
||||||
|
решить
|
||||||
|
любой
|
||||||
|
возможность
|
||||||
|
результат
|
||||||
|
ночь
|
||||||
|
стол
|
||||||
|
никогда
|
||||||
|
имя
|
||||||
|
область
|
||||||
|
молодой
|
||||||
|
пройти
|
||||||
|
например
|
||||||
|
статья
|
||||||
|
оно
|
||||||
|
число
|
||||||
|
компания
|
||||||
|
про
|
||||||
|
государственный
|
||||||
|
полный
|
||||||
|
принять
|
||||||
|
народ
|
||||||
|
никакой
|
||||||
|
советский
|
||||||
|
жена
|
||||||
|
настоящий
|
||||||
|
всякий
|
||||||
|
группа
|
||||||
|
развитие
|
||||||
|
процесс
|
||||||
|
суд
|
||||||
|
давать
|
||||||
|
ответить
|
||||||
|
старый
|
||||||
|
условие
|
||||||
|
твой
|
||||||
|
пока
|
||||||
|
средство
|
||||||
|
помнить
|
||||||
|
начало
|
||||||
|
ждать
|
||||||
|
свет
|
||||||
|
пора
|
||||||
|
путь
|
||||||
|
душа
|
||||||
|
куда
|
||||||
|
нужно
|
||||||
|
разный
|
||||||
|
нужный
|
||||||
|
уровень
|
||||||
|
иной
|
||||||
|
форма
|
||||||
|
связь
|
||||||
|
уж
|
||||||
|
минута
|
||||||
|
кроме
|
||||||
|
находиться
|
||||||
|
опять
|
||||||
|
многий
|
||||||
|
белый
|
||||||
|
собственный
|
||||||
|
улица
|
||||||
|
черный
|
||||||
|
написать
|
||||||
|
вечер
|
||||||
|
снова
|
||||||
|
основной
|
||||||
|
качество
|
||||||
|
мысль
|
||||||
|
дорога
|
||||||
|
мать
|
||||||
|
действие
|
||||||
|
месяц
|
||||||
|
оставаться
|
||||||
|
государство
|
||||||
|
язык
|
||||||
|
любовь
|
||||||
|
взгляд
|
||||||
|
мама
|
||||||
|
играть
|
||||||
|
далекий
|
||||||
|
лежать
|
||||||
|
нельзя
|
||||||
|
век
|
||||||
|
школа
|
||||||
|
подумать
|
||||||
|
уйти
|
||||||
|
цель
|
||||||
|
среди
|
||||||
|
общество
|
||||||
|
посмотреть
|
||||||
|
деятельность
|
||||||
|
организация
|
||||||
|
кто-то
|
||||||
|
вернуться
|
||||||
|
президент
|
||||||
|
комната
|
||||||
|
порядок
|
||||||
|
момент
|
||||||
|
театр
|
||||||
|
следовать
|
||||||
|
читать
|
||||||
|
письмо
|
||||||
|
подобный
|
||||||
|
следующий
|
||||||
|
утро
|
||||||
|
особенно
|
||||||
|
помощь
|
||||||
|
ситуация
|
||||||
|
роль
|
||||||
|
бывать
|
||||||
|
ходить
|
||||||
|
рубль
|
||||||
|
начинать
|
||||||
|
появиться
|
||||||
|
смысл
|
||||||
|
состояние
|
||||||
|
называть
|
||||||
|
рядом
|
||||||
|
квартира
|
||||||
|
назад
|
||||||
|
равный
|
||||||
|
из-за
|
||||||
|
орган
|
||||||
|
внимание
|
||||||
|
тело
|
||||||
|
труд
|
||||||
|
прийтись
|
||||||
|
хотеться
|
||||||
|
сын
|
||||||
|
мера
|
||||||
|
пять
|
||||||
|
смерть
|
||||||
|
живой
|
||||||
|
рынок
|
||||||
|
программа
|
||||||
|
задача
|
||||||
|
предприятие
|
||||||
|
известный
|
||||||
|
окно
|
||||||
|
вести
|
||||||
|
совершенно
|
||||||
|
военный
|
||||||
|
разговор
|
||||||
|
показать
|
||||||
|
правительство
|
||||||
|
важный
|
||||||
|
семья
|
||||||
|
великий
|
||||||
|
производство
|
||||||
|
простой
|
||||||
|
значит
|
||||||
|
третий
|
||||||
|
сколько
|
||||||
|
огромный
|
||||||
|
давно
|
||||||
|
политический
|
||||||
|
информация
|
||||||
|
действительно
|
||||||
|
положение
|
||||||
|
поставить
|
||||||
|
бояться
|
||||||
|
наконец
|
||||||
|
центр
|
||||||
|
происходить
|
||||||
|
ответ
|
||||||
|
муж
|
||||||
|
автор
|
||||||
|
все-таки
|
||||||
|
стена
|
||||||
|
существовать
|
||||||
|
даже
|
||||||
|
интерес
|
||||||
|
становиться
|
||||||
|
федерация
|
||||||
|
правило
|
||||||
|
оба
|
||||||
|
часто
|
||||||
|
московский
|
||||||
|
управление
|
||||||
|
слышать
|
||||||
|
быстро
|
||||||
|
смочь
|
||||||
|
заметить
|
||||||
|
как-то
|
||||||
|
мужчина
|
||||||
|
долго
|
||||||
|
правда
|
||||||
|
идея
|
||||||
|
партия
|
||||||
|
иногда
|
||||||
|
использовать
|
||||||
|
пытаться
|
||||||
|
готовый
|
||||||
|
чуть
|
||||||
|
зачем
|
||||||
|
представить
|
||||||
|
чувствовать
|
||||||
|
создать
|
||||||
|
совет
|
||||||
|
счет
|
||||||
|
сердце
|
||||||
|
движение
|
||||||
|
вещь
|
||||||
|
материал
|
||||||
|
неделя
|
||||||
|
чувство
|
||||||
|
затем
|
||||||
|
данный
|
||||||
|
заниматься
|
||||||
|
продолжать
|
||||||
|
красный
|
||||||
|
глава
|
||||||
|
ко
|
||||||
|
слушать
|
||||||
|
наука
|
||||||
|
узнать
|
||||||
|
ряд
|
||||||
|
газета
|
||||||
|
причина
|
||||||
|
против
|
||||||
|
плечо
|
||||||
|
современный
|
||||||
|
цена
|
||||||
|
план
|
||||||
|
приехать
|
||||||
|
речь
|
||||||
|
четыре
|
||||||
|
отвечать
|
||||||
|
точка
|
||||||
|
основа
|
||||||
|
товарищ
|
||||||
|
культура
|
||||||
|
слишком
|
||||||
|
рассказывать
|
||||||
|
вполне
|
||||||
|
далее
|
||||||
|
рассказать
|
||||||
|
данные
|
||||||
|
представлять
|
||||||
|
мнение
|
||||||
|
социальный
|
||||||
|
около
|
||||||
|
документ
|
||||||
|
институт
|
||||||
|
ход
|
||||||
|
брать
|
||||||
|
забыть
|
||||||
|
проект
|
||||||
|
ранний
|
||||||
|
встреча
|
||||||
|
особый
|
||||||
|
целый
|
||||||
|
директор
|
||||||
|
провести
|
||||||
|
спать
|
||||||
|
плохой
|
||||||
|
может
|
||||||
|
впрочем
|
||||||
|
сильный
|
||||||
|
наверное
|
||||||
|
скорый
|
||||||
|
ведь
|
||||||
|
срок
|
||||||
|
палец
|
||||||
|
опыт
|
||||||
|
помочь
|
||||||
|
больше
|
||||||
|
приходить
|
||||||
|
служба
|
||||||
|
крупный
|
||||||
|
внутренний
|
||||||
|
просить
|
||||||
|
вспомнить
|
||||||
|
открыть
|
||||||
|
привести
|
||||||
|
судьба
|
||||||
|
пока
|
||||||
|
девушка
|
||||||
|
поскольку
|
||||||
|
очередь
|
||||||
|
лес
|
||||||
|
пусть
|
||||||
|
экономический
|
||||||
|
оставить
|
||||||
|
правый
|
||||||
|
состав
|
||||||
|
словно
|
||||||
|
федеральный
|
||||||
|
спрашивать
|
||||||
|
принимать
|
||||||
|
член
|
||||||
|
искать
|
||||||
|
близкий
|
||||||
|
количество
|
||||||
|
похожий
|
||||||
|
событие
|
||||||
|
объект
|
||||||
|
зал
|
||||||
|
создание
|
||||||
|
войти
|
||||||
|
различный
|
||||||
|
значение
|
||||||
|
назвать
|
||||||
|
достаточно
|
||||||
|
период
|
||||||
|
хоть
|
||||||
|
шаг
|
||||||
|
необходимый
|
||||||
|
успеть
|
||||||
|
произойти
|
||||||
|
брат
|
||||||
|
искусство
|
||||||
|
единственный
|
||||||
|
легкий
|
||||||
|
структура
|
||||||
|
выходить
|
||||||
|
номер
|
||||||
|
предложить
|
||||||
|
пример
|
||||||
|
пить
|
||||||
|
исследование
|
||||||
|
гражданин
|
||||||
|
глядеть
|
||||||
|
человеческий
|
||||||
|
игра
|
||||||
|
начальник
|
||||||
|
сей
|
||||||
|
рост
|
||||||
|
ехать
|
||||||
|
международный
|
||||||
|
тема
|
||||||
|
принцип
|
||||||
|
дорогой
|
||||||
|
попасть
|
||||||
|
десять
|
||||||
|
начаться
|
||||||
|
верить
|
||||||
|
метод
|
||||||
|
тип
|
||||||
|
фильм
|
||||||
|
небольшой
|
||||||
|
держать
|
||||||
|
либо
|
||||||
|
позволять
|
||||||
|
край
|
||||||
|
местный
|
||||||
|
менее
|
||||||
|
гость
|
||||||
|
купить
|
||||||
|
уходить
|
||||||
|
собираться
|
||||||
|
воздух
|
||||||
|
туда
|
||||||
|
относиться
|
||||||
|
бывший
|
||||||
|
требовать
|
||||||
|
характер
|
||||||
|
борьба
|
||||||
|
использование
|
||||||
|
кстати
|
||||||
|
подойти
|
||||||
|
размер
|
||||||
|
удаться
|
||||||
|
образование
|
||||||
|
получать
|
||||||
|
мальчик
|
||||||
|
кровь
|
||||||
|
район
|
||||||
|
небо
|
||||||
|
американский
|
||||||
|
армия
|
||||||
|
класс
|
||||||
|
представитель
|
||||||
|
участие
|
||||||
|
девочка
|
||||||
|
политика
|
||||||
|
сначала
|
||||||
|
герой
|
||||||
|
картина
|
||||||
|
широкий
|
||||||
|
доллар
|
||||||
|
спина
|
||||||
|
территория
|
||||||
|
мировой
|
||||||
|
пол
|
||||||
|
тяжелый
|
||||||
|
довольно
|
||||||
|
поле
|
||||||
|
ж
|
||||||
|
изменение
|
||||||
|
умереть
|
||||||
|
более
|
||||||
|
направление
|
||||||
|
рисунок
|
||||||
|
течение
|
||||||
|
возможный
|
||||||
|
церковь
|
||||||
|
банк
|
||||||
|
отдельный
|
||||||
|
средний
|
||||||
|
красивый
|
||||||
|
сцена
|
||||||
|
население
|
||||||
|
большинство
|
||||||
|
сесть
|
||||||
|
двадцать
|
||||||
|
случиться
|
||||||
|
музыка
|
||||||
|
короткий
|
||||||
|
правда
|
||||||
|
проходить
|
||||||
|
составлять
|
||||||
|
свобода
|
||||||
|
память
|
||||||
|
приходиться
|
||||||
|
причем
|
||||||
|
команда
|
||||||
|
установить
|
||||||
|
союз
|
||||||
|
будто
|
||||||
|
поднять
|
||||||
|
врач
|
||||||
|
серьезный
|
||||||
|
договор
|
||||||
|
стараться
|
||||||
|
уметь
|
||||||
|
встать
|
||||||
|
дерево
|
||||||
|
интересный
|
||||||
|
факт
|
||||||
|
добрый
|
||||||
|
всего
|
||||||
|
хозяин
|
||||||
|
национальный
|
||||||
|
однажды
|
||||||
|
длинный
|
||||||
|
природа
|
||||||
|
домой
|
||||||
|
страшный
|
||||||
|
прошлый
|
||||||
|
будто
|
||||||
|
общественный
|
||||||
|
угол
|
||||||
|
чтоб
|
||||||
|
телефон
|
||||||
|
позиция
|
||||||
|
проводить
|
||||||
|
скоро
|
||||||
|
наиболее
|
||||||
|
двор
|
||||||
|
обычно
|
||||||
|
бросить
|
||||||
|
разве
|
||||||
|
писатель
|
||||||
|
самолет
|
||||||
|
объем
|
||||||
|
далеко
|
||||||
|
род
|
||||||
|
солнце
|
||||||
|
вера
|
||||||
|
берег
|
||||||
|
спектакль
|
||||||
|
фирма
|
||||||
|
способ
|
||||||
|
завод
|
||||||
|
цвет
|
||||||
|
трудно
|
||||||
|
журнал
|
||||||
|
руководитель
|
||||||
|
специалист
|
||||||
|
возможно
|
||||||
|
детский
|
||||||
|
точно
|
||||||
|
объяснить
|
||||||
|
оценка
|
||||||
|
единый
|
||||||
|
снять
|
||||||
|
определенный
|
||||||
|
низкий
|
||||||
|
нравиться
|
||||||
|
услышать
|
||||||
|
регион
|
||||||
|
связать
|
||||||
|
песня
|
||||||
|
процент
|
||||||
|
родитель
|
||||||
|
позволить
|
||||||
|
чужой
|
||||||
|
море
|
||||||
|
странный
|
||||||
|
требование
|
||||||
|
чистый
|
||||||
|
весьма
|
||||||
|
какой-нибудь
|
||||||
|
основание
|
||||||
|
половина
|
||||||
|
поехать
|
||||||
|
положить
|
||||||
|
входить
|
||||||
|
легко
|
||||||
|
поздний
|
||||||
|
роман
|
||||||
|
круг
|
||||||
|
анализ
|
||||||
|
стихи
|
||||||
|
автомобиль
|
||||||
|
специальный
|
||||||
|
экономика
|
||||||
|
литература
|
||||||
|
бумага
|
||||||
|
вместо
|
||||||
|
впервые
|
||||||
|
видно
|
||||||
|
научный
|
||||||
|
оказываться
|
||||||
|
поэт
|
||||||
|
показывать
|
||||||
|
степень
|
||||||
|
вызвать
|
||||||
|
касаться
|
||||||
|
господин
|
||||||
|
надежда
|
||||||
|
сложный
|
||||||
|
вокруг
|
||||||
|
предмет
|
||||||
|
отметить
|
||||||
|
заявить
|
||||||
|
вариант
|
||||||
|
министр
|
||||||
|
откуда
|
||||||
|
реальный
|
||||||
|
граница
|
||||||
|
действовать
|
||||||
|
дух
|
||||||
|
модель
|
||||||
|
операция
|
||||||
|
пара
|
||||||
|
сон
|
||||||
|
немного
|
||||||
|
название
|
||||||
|
ум
|
||||||
|
повод
|
||||||
|
старик
|
||||||
|
способный
|
||||||
|
мало
|
||||||
|
миллион
|
||||||
|
малый
|
||||||
|
старший
|
||||||
|
успех
|
||||||
|
практически
|
||||||
|
получиться
|
||||||
|
личный
|
||||||
|
счастье
|
||||||
|
необходимо
|
||||||
|
свободный
|
||||||
|
ребята
|
||||||
|
обычный
|
||||||
|
кабинет
|
||||||
|
прекрасный
|
||||||
|
высший
|
||||||
|
кричать
|
||||||
|
прежде
|
||||||
|
магазин
|
||||||
|
пространство
|
||||||
|
выход
|
||||||
|
остановиться
|
||||||
|
удар
|
||||||
|
база
|
||||||
|
знание
|
||||||
|
текст
|
||||||
|
сюда
|
||||||
|
темный
|
||||||
|
защита
|
||||||
|
предлагать
|
||||||
|
руководство
|
||||||
|
вовсе
|
||||||
|
площадь
|
||||||
|
сознание
|
||||||
|
гражданский
|
||||||
|
убить
|
||||||
|
возраст
|
||||||
|
молчать
|
||||||
|
согласиться
|
||||||
|
участник
|
||||||
|
участок
|
||||||
|
рано
|
||||||
|
пункт
|
||||||
|
несмотря
|
||||||
|
сильно
|
||||||
|
столь
|
||||||
|
сообщить
|
||||||
|
линия
|
||||||
|
бежать
|
||||||
|
желание
|
||||||
|
папа
|
||||||
|
кажется
|
||||||
|
петь
|
||||||
|
доктор
|
||||||
|
губа
|
||||||
|
известно
|
||||||
|
дома
|
||||||
|
вызывать
|
||||||
|
дочь
|
||||||
|
показаться
|
||||||
|
среда
|
||||||
|
председатель
|
||||||
|
представление
|
||||||
|
солдат
|
||||||
|
художник
|
||||||
|
принести
|
||||||
|
волос
|
||||||
|
оружие
|
||||||
|
выглядеть
|
||||||
|
соответствие
|
||||||
|
никак
|
||||||
|
ветер
|
||||||
|
внешний
|
||||||
|
парень
|
||||||
|
служить
|
||||||
|
зрение
|
||||||
|
попросить
|
||||||
|
генерал
|
||||||
|
состоять
|
||||||
|
огонь
|
||||||
|
отдать
|
||||||
|
боевой
|
||||||
|
понятие
|
||||||
|
строительство
|
||||||
|
ухо
|
||||||
|
выступать
|
||||||
|
грудь
|
||||||
|
нос
|
||||||
|
ставить
|
||||||
|
завтра
|
||||||
|
возникать
|
||||||
|
когда
|
||||||
|
страх
|
||||||
|
услуга
|
||||||
|
рабочий
|
||||||
|
что-нибудь
|
||||||
|
глубокий
|
||||||
|
содержание
|
||||||
|
радость
|
||||||
|
безопасность
|
||||||
|
надеяться
|
||||||
|
продукт
|
||||||
|
видимо
|
||||||
|
комплекс
|
||||||
|
бизнес
|
||||||
|
подняться
|
||||||
|
вспоминать
|
||||||
|
мало
|
||||||
|
сад
|
||||||
|
долгий
|
||||||
|
одновременно
|
||||||
|
называться
|
||||||
|
сотрудник
|
||||||
|
лето
|
||||||
|
тихо
|
||||||
|
зато
|
||||||
|
прямой
|
||||||
|
курс
|
||||||
|
помогать
|
||||||
|
предложение
|
||||||
|
финансовый
|
||||||
|
открытый
|
||||||
|
почему-то
|
||||||
|
значить
|
||||||
|
возникнуть
|
||||||
|
рот
|
||||||
|
где-то
|
||||||
|
технология
|
||||||
|
знакомый
|
||||||
|
недавно
|
||||||
|
реформа
|
||||||
|
отсутствие
|
||||||
|
нынешний
|
||||||
|
собака
|
||||||
|
камень
|
||||||
|
будущее
|
||||||
|
звать
|
||||||
|
рассказ
|
||||||
|
контроль
|
||||||
|
позвонить
|
||||||
|
река
|
||||||
|
хватать
|
||||||
|
продукция
|
||||||
|
сумма
|
||||||
|
техника
|
||||||
|
исторический
|
||||||
|
вновь
|
||||||
|
народный
|
||||||
|
прямо
|
||||||
|
ибо
|
||||||
|
выпить
|
||||||
|
здание
|
||||||
|
сфера
|
||||||
|
знаменитый
|
||||||
|
иначе
|
||||||
|
потерять
|
||||||
|
необходимость
|
||||||
|
больший
|
||||||
|
фонд
|
||||||
|
иметься
|
||||||
|
вперед
|
||||||
|
подготовка
|
||||||
|
вчера
|
||||||
|
лист
|
||||||
|
пустой
|
||||||
|
очередной
|
||||||
|
республика
|
||||||
|
хозяйство
|
||||||
|
полностью
|
||||||
|
получаться
|
||||||
|
учиться
|
||||||
|
плохо
|
||||||
|
воля
|
||||||
|
судебный
|
||||||
|
бюджет
|
||||||
|
возвращаться
|
||||||
|
расти
|
||||||
|
снег
|
||||||
|
деревня
|
||||||
|
обнаружить
|
||||||
|
мужик
|
||||||
|
постоянно
|
||||||
|
зеленый
|
||||||
|
элемент
|
||||||
|
обстоятельство
|
||||||
|
почувствовать
|
||||||
|
немец
|
||||||
|
многое
|
||||||
|
победа
|
||||||
|
источник
|
||||||
|
немецкий
|
||||||
|
золотой
|
||||||
|
передать
|
||||||
|
технический
|
||||||
|
нормальный
|
||||||
|
едва
|
||||||
|
желать
|
||||||
|
ожидать
|
||||||
|
некий
|
||||||
|
звезда
|
||||||
|
городской
|
||||||
|
выбор
|
||||||
|
соответствующий
|
||||||
|
масса
|
||||||
|
составить
|
||||||
|
итог
|
||||||
|
сестра
|
||||||
|
что
|
||||||
|
шесть
|
||||||
|
ясно
|
||||||
|
практика
|
||||||
|
сто
|
||||||
|
нести
|
||||||
|
определить
|
||||||
|
проведение
|
||||||
|
карман
|
||||||
|
любимый
|
||||||
|
родной
|
||||||
|
западный
|
||||||
|
обязательно
|
||||||
|
слава
|
||||||
|
кухня
|
||||||
|
определение
|
||||||
|
пользоваться
|
||||||
|
быстрый
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,32 @@
|
|||||||
|
фрак
|
||||||
|
тина
|
||||||
|
зонт
|
||||||
|
некроз
|
||||||
|
норка
|
||||||
|
порка
|
||||||
|
фигня
|
||||||
|
понт
|
||||||
|
пони
|
||||||
|
рокот
|
||||||
|
топот
|
||||||
|
потоп
|
||||||
|
жакет
|
||||||
|
гопак
|
||||||
|
колпак
|
||||||
|
сено
|
||||||
|
навес
|
||||||
|
воск
|
||||||
|
хакер
|
||||||
|
рокер
|
||||||
|
срам
|
||||||
|
меркурий
|
||||||
|
венера
|
||||||
|
марс
|
||||||
|
юпитер
|
||||||
|
сатурн
|
||||||
|
уран
|
||||||
|
нептун
|
||||||
|
плутон
|
||||||
|
резка
|
||||||
|
резак
|
||||||
|
пилон
|
||||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -0,0 +1,26 @@
|
|||||||
|
from piccolo.apps.migrations.auto.migration_manager import MigrationManager
|
||||||
|
from piccolo.columns.column_types import Integer
|
||||||
|
|
||||||
|
ID = "2026-07-02T12:57:47:552419"
|
||||||
|
VERSION = "1.34.0"
|
||||||
|
DESCRIPTION = ""
|
||||||
|
|
||||||
|
|
||||||
|
async def forwards():
|
||||||
|
manager = MigrationManager(
|
||||||
|
migration_id=ID, app_name="zangramru_db", description=DESCRIPTION
|
||||||
|
)
|
||||||
|
|
||||||
|
manager.alter_column(
|
||||||
|
table_class_name="Puzzle",
|
||||||
|
tablename="puzzle",
|
||||||
|
column_name="number",
|
||||||
|
db_column_name="number",
|
||||||
|
params={"unique": True},
|
||||||
|
old_params={"unique": False},
|
||||||
|
column_class=Integer,
|
||||||
|
old_column_class=Integer,
|
||||||
|
schema=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
return manager
|
||||||
@@ -12,4 +12,4 @@ class Puzzle(Table):
|
|||||||
solutions = JSON()
|
solutions = JSON()
|
||||||
extra = JSON()
|
extra = JSON()
|
||||||
definitions = JSON()
|
definitions = JSON()
|
||||||
number = Integer()
|
number = Integer(unique=True)
|
||||||
|
|||||||
+22850
-3652
File diff suppressed because it is too large
Load Diff
@@ -10,6 +10,8 @@ from typing import Annotated
|
|||||||
|
|
||||||
from fastapi import Depends, FastAPI, Request
|
from fastapi import Depends, FastAPI, Request
|
||||||
|
|
||||||
|
from zangramru.utils.trie import TrieNode
|
||||||
|
|
||||||
CWD = Path(__file__).parent
|
CWD = Path(__file__).parent
|
||||||
|
|
||||||
# Weighted toward common Russian letters
|
# Weighted toward common Russian letters
|
||||||
@@ -42,61 +44,6 @@ class Puzzle:
|
|||||||
print(data) # noqa: T201
|
print(data) # noqa: T201
|
||||||
|
|
||||||
|
|
||||||
class TrieNode:
|
|
||||||
"""Prefix tree for optimized solution search."""
|
|
||||||
|
|
||||||
def __init__(self, value: str = "") -> None:
|
|
||||||
self.value = value
|
|
||||||
self.children: dict[str, TrieNode] = {}
|
|
||||||
self.valid_word = False
|
|
||||||
|
|
||||||
def get_next(self, symbol: str) -> "TrieNode | None":
|
|
||||||
"""
|
|
||||||
Get next possible node in from a given symobl.
|
|
||||||
|
|
||||||
Simple lookup in children of this prefix.
|
|
||||||
"""
|
|
||||||
return self.children.get(symbol)
|
|
||||||
|
|
||||||
def add(self, word: str) -> None:
|
|
||||||
"""Add a word to the trie."""
|
|
||||||
word = word.lower()
|
|
||||||
node = self
|
|
||||||
for i in range(len(word)):
|
|
||||||
if word[i] not in node.children:
|
|
||||||
node.children[word[i]] = TrieNode(word[: i + 1])
|
|
||||||
node = node.children[word[i]]
|
|
||||||
node.valid_word = True
|
|
||||||
|
|
||||||
def find_node(self, word: str) -> "TrieNode | None":
|
|
||||||
"""
|
|
||||||
Find a node in the trie.
|
|
||||||
|
|
||||||
The node will only be returned if
|
|
||||||
the word is in the trie, or
|
|
||||||
is a prefix of a longer word.
|
|
||||||
"""
|
|
||||||
word = word.lower()
|
|
||||||
node = self
|
|
||||||
for i in range(len(word)):
|
|
||||||
if word[i] not in node.children:
|
|
||||||
return None
|
|
||||||
node = node.children[word[i]]
|
|
||||||
return node
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
ret = "Node"
|
|
||||||
if self.value:
|
|
||||||
ret += f"(val={self.value})"
|
|
||||||
ret += "\n"
|
|
||||||
for prefix in self.children:
|
|
||||||
ret += f" {prefix}:\n"
|
|
||||||
child = str(self.children[prefix])
|
|
||||||
for line in child.splitlines():
|
|
||||||
ret += f" {line}\n"
|
|
||||||
return ret
|
|
||||||
|
|
||||||
|
|
||||||
class PuzzleGenerator:
|
class PuzzleGenerator:
|
||||||
"""Helper for generating puzzles."""
|
"""Helper for generating puzzles."""
|
||||||
|
|
||||||
@@ -113,7 +60,7 @@ class PuzzleGenerator:
|
|||||||
self.words[word] = row["def"]
|
self.words[word] = row["def"]
|
||||||
self.trie.add(word)
|
self.trie.add(word)
|
||||||
|
|
||||||
def find_solutions( # noqa: C901
|
def _find_solutions( # noqa: C901
|
||||||
self, board: list[list[str]]
|
self, board: list[list[str]]
|
||||||
) -> dict[str, list[tuple[int, int]]]:
|
) -> dict[str, list[tuple[int, int]]]:
|
||||||
"""
|
"""
|
||||||
@@ -258,7 +205,7 @@ class PuzzleGenerator:
|
|||||||
|
|
||||||
return cover, extras
|
return cover, extras
|
||||||
|
|
||||||
def get_valid_points(
|
def _get_valid_points(
|
||||||
self,
|
self,
|
||||||
solutions: dict[str, list[tuple[int, int]]],
|
solutions: dict[str, list[tuple[int, int]]],
|
||||||
) -> set[tuple[int, int]]:
|
) -> set[tuple[int, int]]:
|
||||||
@@ -269,6 +216,32 @@ class PuzzleGenerator:
|
|||||||
|
|
||||||
return used_cells
|
return used_cells
|
||||||
|
|
||||||
|
def _verify_solution(
|
||||||
|
self, board: list[list[str]], target: int
|
||||||
|
) -> tuple[set[tuple[int, int]], Puzzle | None]:
|
||||||
|
solutions = self._find_solutions(board)
|
||||||
|
cover, extra = self._find_minimal_cover(solutions, target)
|
||||||
|
valid_points = self._get_valid_points(cover)
|
||||||
|
|
||||||
|
# We found a valid solution | board combination
|
||||||
|
if len(valid_points) != 16 or len(cover) < target:
|
||||||
|
return valid_points, None
|
||||||
|
|
||||||
|
word_defs = {word: self.words[word] for word in cover | extra}
|
||||||
|
|
||||||
|
return valid_points, Puzzle(
|
||||||
|
board=board,
|
||||||
|
solutions=cover,
|
||||||
|
extra=extra,
|
||||||
|
word_defs=word_defs,
|
||||||
|
)
|
||||||
|
|
||||||
|
def check_board(self, compressed_board: str, target: int = 10) -> Puzzle | None:
|
||||||
|
"""Check if given board is valid."""
|
||||||
|
board = [list(compressed_board[i * 4 : i * 4 + 4]) for i in range(4)]
|
||||||
|
_, puzzle = self._verify_solution(board, target)
|
||||||
|
return puzzle
|
||||||
|
|
||||||
def generate(self, target: int, timeout: int = 10) -> Puzzle:
|
def generate(self, target: int, timeout: int = 10) -> Puzzle:
|
||||||
"""
|
"""
|
||||||
Generate a valid puzzle that has solutions.
|
Generate a valid puzzle that has solutions.
|
||||||
@@ -289,27 +262,19 @@ class PuzzleGenerator:
|
|||||||
)
|
)
|
||||||
if single_board_attempt > 50:
|
if single_board_attempt > 50:
|
||||||
valid_points = set()
|
valid_points = set()
|
||||||
|
single_board_attempt = 0
|
||||||
|
|
||||||
for x in range(4):
|
for x in range(4):
|
||||||
for y in range(4):
|
for y in range(4):
|
||||||
if (x, y) not in valid_points:
|
if (x, y) not in valid_points:
|
||||||
board[x][y] = secrets.choice(WEIGHTED_ALPHABET)
|
board[x][y] = secrets.choice(WEIGHTED_ALPHABET)
|
||||||
|
|
||||||
solutions = self.find_solutions(board)
|
valid_points, puzzle = self._verify_solution(board, target)
|
||||||
cover, extra = self._find_minimal_cover(solutions, target)
|
if not puzzle:
|
||||||
valid_points = self.get_valid_points(cover)
|
|
||||||
word_defs = {word: self.words[word] for word in cover | extra}
|
|
||||||
# We found a valid solution | board combination
|
|
||||||
if len(valid_points) != 16 or len(cover) < target:
|
|
||||||
single_board_attempt += 1
|
single_board_attempt += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
return Puzzle(
|
return puzzle
|
||||||
board=board,
|
|
||||||
solutions=cover,
|
|
||||||
extra=extra,
|
|
||||||
word_defs=word_defs,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def setup_generator(app: FastAPI) -> None:
|
def setup_generator(app: FastAPI) -> None:
|
||||||
|
|||||||
+35
-12
@@ -403,7 +403,7 @@
|
|||||||
const cctx = confettiCanvas.getContext('2d');
|
const cctx = confettiCanvas.getContext('2d');
|
||||||
|
|
||||||
// ===================== Constants =====================
|
// ===================== Constants =====================
|
||||||
const CELL = 70, GAP = 20, GRID = 4, PITCH = CELL + GAP, SH = 5;
|
const CELL = 70, GAP = 30, GRID = 4, PITCH = CELL + GAP, SH = 5;
|
||||||
const ENTRANCE_DUR = 360, ENTRANCE_STAGGER = 42, PULSE_DUR = 520;
|
const ENTRANCE_DUR = 360, ENTRANCE_STAGGER = 42, PULSE_DUR = 520;
|
||||||
const PALETTE = [
|
const PALETTE = [
|
||||||
'#ff3d81','#ffd400','#00e0c6','#7c5cff','#54a0ff',
|
'#ff3d81','#ffd400','#00e0c6','#7c5cff','#54a0ff',
|
||||||
@@ -501,8 +501,10 @@
|
|||||||
// ===================== Persistence =====================
|
// ===================== Persistence =====================
|
||||||
function save() {
|
function save() {
|
||||||
if (puzzleNumber === null) return;
|
if (puzzleNumber === null) return;
|
||||||
|
if (!allWordsFound()) {
|
||||||
elapsed += Date.now() - lastSaveTime;
|
elapsed += Date.now() - lastSaveTime;
|
||||||
lastSaveTime = Date.now();
|
lastSaveTime = Date.now();
|
||||||
|
}
|
||||||
const state = { foundWords: Array.from(foundWords), elapsed, board, allSolutions, extraWords, wordDefs };
|
const state = { foundWords: Array.from(foundWords), elapsed, board, allSolutions, extraWords, wordDefs };
|
||||||
localStorage.setItem('puzzle_' + puzzleNumber, JSON.stringify(state));
|
localStorage.setItem('puzzle_' + puzzleNumber, JSON.stringify(state));
|
||||||
}
|
}
|
||||||
@@ -560,7 +562,7 @@
|
|||||||
// ===================== Rendering =====================
|
// ===================== Rendering =====================
|
||||||
function drawConnections() {
|
function drawConnections() {
|
||||||
const p = PAL();
|
const p = PAL();
|
||||||
ctx.strokeStyle = p.line; ctx.lineWidth = GAP - 4; ctx.lineCap = 'round';
|
ctx.strokeStyle = p.line; ctx.lineWidth = 8; ctx.lineCap = 'round';
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
for (const key of getActiveConnections()) {
|
for (const key of getActiveConnections()) {
|
||||||
const [ka, kb] = key.split('-');
|
const [ka, kb] = key.split('-');
|
||||||
@@ -576,7 +578,7 @@
|
|||||||
function drawSelectionLine(color) {
|
function drawSelectionLine(color) {
|
||||||
if (selected.length < 2) return;
|
if (selected.length < 2) return;
|
||||||
const t1 = CELL / (2 * PITCH), t2 = 1 - t1;
|
const t1 = CELL / (2 * PITCH), t2 = 1 - t1;
|
||||||
ctx.strokeStyle = color; ctx.lineWidth = GAP - 2; ctx.lineCap = 'round'; ctx.lineJoin = 'round';
|
ctx.strokeStyle = color; ctx.lineWidth = 10; ctx.lineCap = 'round'; ctx.lineJoin = 'round';
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
for (let i = 0; i < selected.length - 1; i++) {
|
for (let i = 0; i < selected.length - 1; i++) {
|
||||||
const c1 = cellCenter(selected[i][0], selected[i][1]);
|
const c1 = cellCenter(selected[i][0], selected[i][1]);
|
||||||
@@ -706,7 +708,7 @@
|
|||||||
'<span class="word-text">' + word + '</span>' +
|
'<span class="word-text">' + word + '</span>' +
|
||||||
(isExtra ? '<span class="word-tag">бонус</span>' : '') +
|
(isExtra ? '<span class="word-tag">бонус</span>' : '') +
|
||||||
'</div>' +
|
'</div>' +
|
||||||
(def ? '<span class="word-def">— ' + def + '</span>' : '');
|
(def ? '<span class="word-def">' + def.replace(/\n/g, '<br>') + '</span>' : '');
|
||||||
grid.appendChild(item);
|
grid.appendChild(item);
|
||||||
}
|
}
|
||||||
foundDiv.appendChild(grid);
|
foundDiv.appendChild(grid);
|
||||||
@@ -951,7 +953,9 @@
|
|||||||
}
|
}
|
||||||
function finishGame(withConfetti) {
|
function finishGame(withConfetti) {
|
||||||
if (timerInterval) { clearInterval(timerInterval); timerInterval = null; }
|
if (timerInterval) { clearInterval(timerInterval); timerInterval = null; }
|
||||||
gameTimeEl.textContent = formatTime(displayElapsed());
|
elapsed = displayElapsed();
|
||||||
|
if (puzzleNumber !== null) save();
|
||||||
|
gameTimeEl.textContent = formatTime(elapsed);
|
||||||
gameCompleteEl.classList.add('show');
|
gameCompleteEl.classList.add('show');
|
||||||
if (withConfetti) celebrate();
|
if (withConfetti) celebrate();
|
||||||
}
|
}
|
||||||
@@ -1005,14 +1009,20 @@
|
|||||||
startTimer();
|
startTimer();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadRandomPuzzle() {
|
async function loadRandomPuzzle(compressed) {
|
||||||
gameCompleteEl.classList.remove('show');
|
gameCompleteEl.classList.remove('show');
|
||||||
statLeft.textContent = '…';
|
statLeft.textContent = '…';
|
||||||
resetTimer();
|
resetTimer();
|
||||||
puzzleNumber = null;
|
puzzleNumber = null;
|
||||||
statNumber.textContent = '?';
|
statNumber.textContent = '?';
|
||||||
|
let data;
|
||||||
|
if (compressed) {
|
||||||
|
const res = await fetch('/api/puzzles/random?board=' + encodeURIComponent(compressed));
|
||||||
|
data = await res.json();
|
||||||
|
} else {
|
||||||
const res = await fetch('/api/puzzles/random');
|
const res = await fetch('/api/puzzles/random');
|
||||||
const data = await res.json();
|
data = await res.json();
|
||||||
|
}
|
||||||
if (data.error) { statNumber.textContent = 'ERR'; return; }
|
if (data.error) { statNumber.textContent = 'ERR'; return; }
|
||||||
foundWords = new Set();
|
foundWords = new Set();
|
||||||
board = data.board;
|
board = data.board;
|
||||||
@@ -1047,9 +1057,16 @@
|
|||||||
|
|
||||||
// ===================== Copy result =====================
|
// ===================== Copy result =====================
|
||||||
function copyResult() {
|
function copyResult() {
|
||||||
const text = 'Занграммы #' + puzzleNumber +
|
let text;
|
||||||
'\n🎉 Мне потребовалось всего лишь ' + formatTime(displayElapsed()) +
|
if (puzzleNumber === null) {
|
||||||
|
const compressedBoard = board.flat().join('');
|
||||||
|
const url = window.location.origin + window.location.pathname + '#' + compressedBoard;
|
||||||
|
text = 'Занграммы!\n🎉 Мне потребовалось всего лишь ' + formatTime(elapsed) + '\n\n' + url;
|
||||||
|
} else {
|
||||||
|
text = 'Занграммы #' + puzzleNumber +
|
||||||
|
'\n🎉 Мне потребовалось всего лишь ' + formatTime(elapsed) +
|
||||||
'\n\n' + window.location.href;
|
'\n\n' + window.location.href;
|
||||||
|
}
|
||||||
navigator.clipboard.writeText(text).then(
|
navigator.clipboard.writeText(text).then(
|
||||||
() => showMessage('Результат скопирован!', PAL().found),
|
() => showMessage('Результат скопирован!', PAL().found),
|
||||||
() => showMessage('Не удалось скопировать', PAL().selTile),
|
() => showMessage('Не удалось скопировать', PAL().selTile),
|
||||||
@@ -1069,19 +1086,25 @@
|
|||||||
wordDisplay.addEventListener('click', submitSelection);
|
wordDisplay.addEventListener('click', submitSelection);
|
||||||
$('copyResultBtn').addEventListener('click', copyResult);
|
$('copyResultBtn').addEventListener('click', copyResult);
|
||||||
$('prevPuzzlesBtn').addEventListener('click', showNumberSelector);
|
$('prevPuzzlesBtn').addEventListener('click', showNumberSelector);
|
||||||
$('randomPuzzleBtn').addEventListener('click', loadRandomPuzzle);
|
$('randomPuzzleBtn').addEventListener('click', () => loadRandomPuzzle());
|
||||||
$('closeNumberBtn').addEventListener('click', () => numberSelectorEl.classList.remove('show'));
|
$('closeNumberBtn').addEventListener('click', () => numberSelectorEl.classList.remove('show'));
|
||||||
themeBtn.addEventListener('click', () => applyTheme(theme === 'dark' ? 'light' : 'dark'));
|
themeBtn.addEventListener('click', () => applyTheme(theme === 'dark' ? 'light' : 'dark'));
|
||||||
document.addEventListener('keydown', onKeyDown);
|
document.addEventListener('keydown', onKeyDown);
|
||||||
|
|
||||||
window.addEventListener('resize', sizeConfetti);
|
window.addEventListener('resize', sizeConfetti);
|
||||||
window.addEventListener('beforeunload', save);
|
|
||||||
document.addEventListener('visibilitychange', () => { if (document.hidden) save(); });
|
|
||||||
|
|
||||||
// ===================== Init =====================
|
// ===================== Init =====================
|
||||||
applyTheme(theme);
|
applyTheme(theme);
|
||||||
sizeConfetti();
|
sizeConfetti();
|
||||||
|
const hash = decodeURIComponent(window.location.hash.slice(1) || '');
|
||||||
|
if (hash && hash.length === 16) {
|
||||||
|
history.replaceState(null, '', window.location.pathname);
|
||||||
|
loadRandomPuzzle(hash);
|
||||||
|
} else {
|
||||||
loadPuzzle();
|
loadPuzzle();
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 26 KiB |
@@ -0,0 +1,56 @@
|
|||||||
|
class TrieNode:
|
||||||
|
"""Prefix tree for optimized solution search."""
|
||||||
|
|
||||||
|
def __init__(self, value: str = "") -> None:
|
||||||
|
self.value = value
|
||||||
|
self.children: dict[str, TrieNode] = {}
|
||||||
|
self.valid_word = False
|
||||||
|
self.definitions: list[str] = []
|
||||||
|
|
||||||
|
def get_next(self, symbol: str) -> "TrieNode | None":
|
||||||
|
"""
|
||||||
|
Get next possible node in from a given symobl.
|
||||||
|
|
||||||
|
Simple lookup in children of this prefix.
|
||||||
|
"""
|
||||||
|
return self.children.get(symbol)
|
||||||
|
|
||||||
|
def add(self, word: str, definitions: list[str] | None = None) -> None:
|
||||||
|
"""Add a word to the trie."""
|
||||||
|
word = word.lower().strip().replace("ё", "е") # noqa: RUF001
|
||||||
|
node = self
|
||||||
|
for i in range(len(word)):
|
||||||
|
if word[i] not in node.children:
|
||||||
|
node.children[word[i]] = TrieNode(word[: i + 1])
|
||||||
|
node = node.children[word[i]]
|
||||||
|
node.valid_word = True
|
||||||
|
if definitions:
|
||||||
|
node.definitions.extend(definitions)
|
||||||
|
|
||||||
|
def find_node(self, word: str) -> "TrieNode | None":
|
||||||
|
"""
|
||||||
|
Find a node in the trie.
|
||||||
|
|
||||||
|
The node will only be returned if
|
||||||
|
the word is in the trie, or
|
||||||
|
is a prefix of a longer word.
|
||||||
|
"""
|
||||||
|
word = word.lower().strip().replace("ё", "е") # noqa: RUF001
|
||||||
|
node = self
|
||||||
|
for i in range(len(word)):
|
||||||
|
if word[i] not in node.children:
|
||||||
|
return None
|
||||||
|
node = node.children[word[i]]
|
||||||
|
return node
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
ret = "Node"
|
||||||
|
if self.value:
|
||||||
|
ret += f"(val={self.value})"
|
||||||
|
ret += "\n"
|
||||||
|
for prefix in self.children:
|
||||||
|
ret += f" {prefix}:\n"
|
||||||
|
child = str(self.children[prefix])
|
||||||
|
for line in child.splitlines():
|
||||||
|
ret += f" {line}\n"
|
||||||
|
return ret
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import asyncio
|
||||||
import json
|
import json
|
||||||
from datetime import date, timedelta
|
from datetime import date, timedelta
|
||||||
|
|
||||||
@@ -15,9 +16,20 @@ router = APIRouter()
|
|||||||
|
|
||||||
|
|
||||||
@router.get("/random")
|
@router.get("/random")
|
||||||
def random_puzzle(generator: PuzzleGeneratorDep) -> PuzzleOutputDTO:
|
def random_puzzle(
|
||||||
|
generator: PuzzleGeneratorDep,
|
||||||
|
board: str | None = None,
|
||||||
|
) -> PuzzleOutputDTO:
|
||||||
"""Generate a random puzzle."""
|
"""Generate a random puzzle."""
|
||||||
puzzle = generator.generate(target=10)
|
if board:
|
||||||
|
puzzle = generator.check_board(board, target=10)
|
||||||
|
if puzzle is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_421_MISDIRECTED_REQUEST,
|
||||||
|
detail="Given board has no solution",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
puzzle = generator.generate(10)
|
||||||
return PuzzleOutputDTO.model_validate(puzzle, from_attributes=True)
|
return PuzzleOutputDTO.model_validate(puzzle, from_attributes=True)
|
||||||
|
|
||||||
|
|
||||||
@@ -30,7 +42,7 @@ async def daily_puzzle(
|
|||||||
# Because they start at one, so we need to subtract one.
|
# Because they start at one, so we need to subtract one.
|
||||||
target_date = date.today()
|
target_date = date.today()
|
||||||
if number is not None:
|
if number is not None:
|
||||||
target_date = settings.start_date + timedelta(days=number - 1)
|
target_date = settings.start_date + timedelta(days=number)
|
||||||
|
|
||||||
if target_date > date.today():
|
if target_date > date.today():
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@@ -45,13 +57,15 @@ async def daily_puzzle(
|
|||||||
if daily_puzzle is not None:
|
if daily_puzzle is not None:
|
||||||
board = [list(daily_puzzle.board[i * 4 : i * 4 + 4]) for i in range(4)]
|
board = [list(daily_puzzle.board[i * 4 : i * 4 + 4]) for i in range(4)]
|
||||||
return DailyPuzzleDTO(
|
return DailyPuzzleDTO(
|
||||||
number=(date.today() - settings.start_date).days,
|
number=daily_puzzle.number,
|
||||||
board=board,
|
board=board,
|
||||||
solutions=json.loads(daily_puzzle.solutions),
|
solutions=json.loads(daily_puzzle.solutions),
|
||||||
extra=json.loads(daily_puzzle.extra),
|
extra=json.loads(daily_puzzle.extra),
|
||||||
word_defs=json.loads(daily_puzzle.definitions),
|
word_defs=json.loads(daily_puzzle.definitions),
|
||||||
)
|
)
|
||||||
puzzle = generator.generate(target=10)
|
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
puzzle = await loop.run_in_executor(None, generator.generate, 10)
|
||||||
compressedboard = "".join("".join(row) for row in puzzle.board)
|
compressedboard = "".join("".join(row) for row in puzzle.board)
|
||||||
await Puzzle(
|
await Puzzle(
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user