initial commit (backend)
This commit is contained in:
+258
@@ -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())
|
||||
Reference in New Issue
Block a user