initial commit (backend)

This commit is contained in:
2026-09-20 11:45:58 +00:00
commit 033adf023e
65 changed files with 28292 additions and 0 deletions
+258
View File
@@ -0,0 +1,258 @@
"""
Безопасное добавление уровней без удаления старых.
Использует только words.csv.
"""
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(filenames):
words_by_theme = {}
translations = {}
for filename in filenames:
with open(filename, 'r', encoding='utf-8') as f:
reader = csv.DictReader(f)
for row in reader:
theme = row['theme']
word = row['polish'].lower()
if theme not in words_by_theme:
words_by_theme[theme] = []
if word not in words_by_theme[theme]:
words_by_theme[theme].append(word)
translations[word] = {
'en': row['english'],
'ru': row['russian'],
'uk': row['ukrainian']
}
return words_by_theme, translations
def get_words_for_grid(words_by_theme, theme, num_words, grid_size):
max_len = grid_size
candidates = [w for w in words_by_theme[theme] if len(w) <= max_len]
if len(candidates) < num_words:
candidates = words_by_theme[theme]
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, words_by_theme, all_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 = []
for theme in words_by_theme:
for w in words_by_theme[theme]:
if 2 <= len(w) <= 4 and w not in all_words:
filler_words.append(w)
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, words_by_theme, 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_words = set(words_sorted)
filled_words, filled_paths = fill_empty_cells(
grid, grid_size, occupied, words_by_theme, all_words
)
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, theme, grid, word_paths, words, translations):
level = await conn.fetchrow(
"""INSERT INTO levels (level_number, grid_size, theme, grid_letters, word_paths)
VALUES ($1, $2, $3, $4, $5) RETURNING id""",
level_num, grid_size, theme, 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)
VALUES ($1, $2, $3, $4, $5)""",
level_id, word,
trans.get('ru', ''),
trans.get('uk', ''),
trans.get('en', '')
)
async def main():
# ============ НАСТРОЙКИ ============
words_by_theme, translations = load_words_from_csv([
'words.csv'
])
NEW_LEVEL_CONFIG = [
(25, 10, 9), # 25 уровней: 10x10, 9 слов
(25, 10, 10), # 25 уровней: 10x10, 10 слов
(25, 10, 11), # 25 уровней: 10x10, 11 слов
(25, 10, 12), # 25 уровней: 10x10, 12 слов
]
# ==================================
conn = await asyncpg.connect(**DB_CONFIG)
current_max = await conn.fetchval("SELECT COALESCE(MAX(level_number), 0) FROM levels")
print(f"Сейчас уровней в базе: {current_max}")
print(f"Добавляем уровни {current_max + 1}{current_max + 100}")
themes = list(words_by_theme.keys())
random.shuffle(themes)
level_num = current_max
theme_idx = 0
total_generated = 0
for count, grid_size, num_words in NEW_LEVEL_CONFIG:
for i in range(count):
level_num += 1
for attempt in range(15):
theme = themes[theme_idx % len(themes)]
theme_idx += 1
words = get_words_for_grid(words_by_theme, theme, num_words, grid_size)
if len(words) < num_words:
continue
grid, word_paths, all_words = generate_grid(
words, grid_size, words_by_theme
)
if grid is not None:
await save_level(
conn, level_num, grid_size, theme,
grid, word_paths, all_words, translations
)
total_cells = grid_size * grid_size
filled_cells = sum(len(w) for w in all_words)
print(f"✅ Level {level_num}: {grid_size}x{grid_size}, "
f"theme={theme}, words={len(all_words)} ({num_words} main), "
f"fill={filled_cells}/{total_cells}")
total_generated += 1
break
else:
print(f"❌ FAILED: Level {level_num} after 15 attempts")
level_num -= 1
await conn.close()
print(f"\n🎉 Готово! Добавлено {total_generated} новых уровней. Теперь всего уровней: {level_num}")
if __name__ == "__main__":
asyncio.run(main())