273 lines
10 KiB
Python
273 lines
10 KiB
Python
import asyncio
|
|
import asyncpg
|
|
import json
|
|
import random
|
|
import csv
|
|
from config import DB_CONFIG
|
|
|
|
DIRECTIONS = [(-1,0),(0,-1),(0,1),(1,0)]
|
|
ALPHABET = "AĄBCĆDEĘFGHIJKLŁMNŃOÓPRSŚTUWYZŹŻ"
|
|
|
|
def load_words_from_csv(filename):
|
|
"""Загружает слова из CSV файла без тем. Возвращает список слов и словарь переводов."""
|
|
words = []
|
|
translations = {}
|
|
with open(filename, 'r', encoding='utf-8') as f:
|
|
reader = csv.DictReader(f)
|
|
for row in reader:
|
|
word = row['polish'].strip().lower()
|
|
if word and word not in words:
|
|
words.append(word)
|
|
translations[word] = {
|
|
'en': row.get('english', ''),
|
|
'ru': row.get('russian', ''),
|
|
'uk': row.get('ukrainian', '')
|
|
}
|
|
return words, translations
|
|
|
|
def get_words_for_grid(all_words, num_words, grid_size):
|
|
"""Выбирает случайные слова подходящей длины."""
|
|
max_len = grid_size
|
|
candidates = [w for w in all_words if len(w) <= max_len]
|
|
if len(candidates) < num_words:
|
|
candidates = all_words
|
|
random.shuffle(candidates)
|
|
return candidates[:num_words]
|
|
|
|
def try_place_word(word, grid_size, occupied, max_attempts=500):
|
|
word = word.upper()
|
|
for _ in range(max_attempts):
|
|
start = (random.randrange(grid_size), random.randrange(grid_size))
|
|
if start in occupied:
|
|
continue
|
|
path = [start]
|
|
visited = {start}
|
|
ok = True
|
|
for _ in range(len(word) - 1):
|
|
r, c = path[-1]
|
|
candidates = []
|
|
for dr, dc in DIRECTIONS:
|
|
nr, nc = r + dr, c + dc
|
|
if 0 <= nr < grid_size and 0 <= nc < grid_size:
|
|
if (nr, nc) not in occupied and (nr, nc) not in visited:
|
|
candidates.append((nr, nc))
|
|
if not candidates:
|
|
ok = False
|
|
break
|
|
nxt = random.choice(candidates)
|
|
path.append(nxt)
|
|
visited.add(nxt)
|
|
if ok and len(path) == len(word):
|
|
return path
|
|
return None
|
|
|
|
def fill_empty_cells(grid, grid_size, occupied, all_words, used_words):
|
|
"""Заполняет пустые клетки короткими словами из того же уровня."""
|
|
empty_cells = set()
|
|
for r in range(grid_size):
|
|
for c in range(grid_size):
|
|
if (r, c) not in occupied:
|
|
empty_cells.add((r, c))
|
|
|
|
if not empty_cells:
|
|
return [], {}
|
|
|
|
filler_words = [w for w in all_words if 2 <= len(w) <= 4 and w not in used_words]
|
|
filler_words = sorted(list(set(filler_words)), key=len, reverse=True)
|
|
random.shuffle(filler_words)
|
|
|
|
filled_paths = {}
|
|
filled_words = []
|
|
remaining_empty = empty_cells.copy()
|
|
|
|
for word in filler_words:
|
|
if len(word) > len(remaining_empty):
|
|
continue
|
|
|
|
for _ in range(500):
|
|
if not remaining_empty:
|
|
break
|
|
start = random.choice(list(remaining_empty))
|
|
|
|
path = [start]
|
|
visited = {start}
|
|
ok = True
|
|
|
|
for _ in range(len(word) - 1):
|
|
r, c = path[-1]
|
|
candidates = []
|
|
for dr, dc in DIRECTIONS:
|
|
nr, nc = r + dr, c + dc
|
|
if 0 <= nr < grid_size and 0 <= nc < grid_size:
|
|
if (nr, nc) in remaining_empty and (nr, nc) not in visited:
|
|
candidates.append((nr, nc))
|
|
if not candidates:
|
|
ok = False
|
|
break
|
|
nxt = random.choice(candidates)
|
|
path.append(nxt)
|
|
visited.add(nxt)
|
|
|
|
if ok and len(path) == len(word):
|
|
for (r, c), letter in zip(path, word.upper()):
|
|
grid[r][c] = letter
|
|
if (r, c) in remaining_empty:
|
|
remaining_empty.remove((r, c))
|
|
|
|
filled_paths[word] = [list(p) for p in path]
|
|
filled_words.append(word)
|
|
break
|
|
|
|
for r, c in remaining_empty:
|
|
grid[r][c] = random.choice(ALPHABET)
|
|
|
|
return filled_words, filled_paths
|
|
|
|
def generate_grid(words, grid_size, all_words, used_words, max_restarts=200):
|
|
words_sorted = sorted(words, key=len, reverse=True)
|
|
for _ in range(max_restarts):
|
|
grid = [['' for _ in range(grid_size)] for _ in range(grid_size)]
|
|
occupied = set()
|
|
word_paths = {}
|
|
success = True
|
|
|
|
for word in words_sorted:
|
|
path = try_place_word(word, grid_size, occupied)
|
|
if path is None:
|
|
success = False
|
|
break
|
|
for (r, c), letter in zip(path, word.upper()):
|
|
grid[r][c] = letter
|
|
occupied.update(path)
|
|
word_paths[word] = [list(p) for p in path]
|
|
|
|
if not success:
|
|
continue
|
|
|
|
all_used = set(words_sorted) | used_words
|
|
filled_words, filled_paths = fill_empty_cells(
|
|
grid, grid_size, occupied, all_words, all_used
|
|
)
|
|
|
|
word_paths.update(filled_paths)
|
|
all_placed_words = words_sorted + filled_words
|
|
|
|
return grid, word_paths, all_placed_words
|
|
|
|
return None, None, None
|
|
|
|
async def save_level(conn, level_num, grid_size, level_name, grid, word_paths, words, translations):
|
|
level = await conn.fetchrow(
|
|
"""INSERT INTO levels (level_number, grid_size, level, grid_letters, word_paths)
|
|
VALUES ($1, $2, $3, $4, $5) RETURNING id""",
|
|
level_num, grid_size, level_name, json.dumps(grid), json.dumps(word_paths)
|
|
)
|
|
level_id = level['id']
|
|
for word in words:
|
|
trans = translations.get(word, {})
|
|
await conn.execute(
|
|
"""INSERT INTO level_words (level_id, word_pl, translation_ru, translation_uk, translation_en, level)
|
|
VALUES ($1, $2, $3, $4, $5, $6)""",
|
|
level_id, word,
|
|
trans.get('ru', ''),
|
|
trans.get('uk', ''),
|
|
trans.get('en', ''),
|
|
level_name
|
|
)
|
|
|
|
async def main():
|
|
# Конфигурация уровней: 550 уровней на каждый уровень польского
|
|
level_configs = [
|
|
{
|
|
"name": "A1",
|
|
"file": "words_a1.csv",
|
|
"levels": [
|
|
(50, 4, 3), # 50 уровней: 4x4, 3 слова
|
|
(100, 5, 4), # 100 уровней: 5x5, 4 слова
|
|
(200, 6, 5), # 200 уровней: 6x6, 5 слов
|
|
(200, 6, 6), # 200 уровней: 6x6, 6 слов
|
|
],
|
|
},
|
|
{
|
|
"name": "A2",
|
|
"file": "words_a2.csv",
|
|
"levels": [
|
|
(100, 6, 5), # 100 уровней: 6x6, 5 слов
|
|
(150, 7, 6), # 150 уровней: 7x7, 6 слов
|
|
(150, 8, 7), # 150 уровней: 8x8, 7 слов
|
|
(150, 8, 8), # 150 уровней: 8x8, 8 слов
|
|
],
|
|
},
|
|
{
|
|
"name": "B1",
|
|
"file": "words_b1.csv",
|
|
"levels": [
|
|
(100, 8, 7), # 100 уровней: 8x8, 7 слов
|
|
(150, 9, 8), # 150 уровней: 9x9, 8 слов
|
|
(150, 10, 9), # 150 уровней: 10x10, 9 слов
|
|
(150, 10, 10), # 150 уровней: 10x10, 10 слов
|
|
],
|
|
},
|
|
]
|
|
|
|
conn = await asyncpg.connect(**DB_CONFIG)
|
|
|
|
# Очищаем старые данные
|
|
await conn.execute("DELETE FROM level_words")
|
|
await conn.execute("DELETE FROM user_progress")
|
|
await conn.execute("DELETE FROM sentence_progress")
|
|
await conn.execute("DELETE FROM word_quiz_progress")
|
|
await conn.execute("DELETE FROM bubble_words_progress")
|
|
await conn.execute("DELETE FROM levels")
|
|
await conn.execute("ALTER SEQUENCE levels_id_seq RESTART WITH 1")
|
|
await conn.execute("UPDATE users SET last_completed_level = 0, current_level = 'A1'")
|
|
|
|
total_generated = 0
|
|
|
|
for level_config in level_configs:
|
|
level_name = level_config["name"]
|
|
filename = level_config["file"]
|
|
|
|
print(f"\n{'='*50}")
|
|
print(f"Генерирую уровни для {level_name} из {filename}")
|
|
print(f"{'='*50}")
|
|
|
|
all_words, translations = load_words_from_csv(filename)
|
|
print(f"Загружено слов: {len(all_words)}")
|
|
|
|
if len(all_words) < 10:
|
|
print(f"❌ Слишком мало слов в {filename}")
|
|
continue
|
|
|
|
used_words = set()
|
|
level_num = 0
|
|
|
|
for count, grid_size, num_words in level_config["levels"]:
|
|
for i in range(count):
|
|
level_num += 1
|
|
|
|
for attempt in range(20):
|
|
words = get_words_for_grid(all_words, num_words, grid_size)
|
|
if len(words) < num_words:
|
|
continue
|
|
|
|
grid, word_paths, all_placed_words = generate_grid(
|
|
words, grid_size, all_words, used_words
|
|
)
|
|
if grid is not None:
|
|
await save_level(
|
|
conn, level_num, grid_size, level_name,
|
|
grid, word_paths, all_placed_words, translations
|
|
)
|
|
used_words.update(all_placed_words)
|
|
total_generated += 1
|
|
print(f"✅ {level_name} Level {level_num}: {grid_size}x{grid_size}, {len(all_placed_words)} words")
|
|
break
|
|
else:
|
|
print(f"❌ {level_name} Level {level_num}: FAILED after 20 attempts")
|
|
|
|
await conn.close()
|
|
print(f"\n🎉 Готово! Сгенерировано уровней: {total_generated}")
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(main()) |