유틸리티 및 화면 플립 효과 추가
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import json
|
||||
|
||||
def validate_stage(stage):
|
||||
grid_size = stage['gridSize']
|
||||
clues = stage['clues']
|
||||
grid = {}
|
||||
|
||||
if len(clues) < 3:
|
||||
return False, f"Stage {stage.get('stageNumber')}: Too few clues ({len(clues)})"
|
||||
|
||||
word_connections = {c['id']: set() for c in clues}
|
||||
|
||||
for clue in clues:
|
||||
word = clue['word']
|
||||
length = clue['length']
|
||||
r = clue['startRow']
|
||||
c = clue['startCol']
|
||||
direction = clue['direction']
|
||||
|
||||
if len(word) != length:
|
||||
return False, f"Stage {stage.get('stageNumber')} Clue {clue['id']}: word '{word}' len {len(word)} != length {length}"
|
||||
if length < 2:
|
||||
return False, f"Stage {stage.get('stageNumber')} Clue {clue['id']}: 1-letter word '{word}' not allowed"
|
||||
|
||||
for i, ch in enumerate(word):
|
||||
curr_r = r + (i if direction == 'DOWN' else 0)
|
||||
curr_c = c + (i if direction == 'ACROSS' else 0)
|
||||
|
||||
if curr_r < 0 or curr_r >= grid_size or curr_c < 0 or curr_c >= grid_size:
|
||||
return False, f"Stage {stage.get('stageNumber')} Clue {clue['id']}: '{word}' char '{ch}' at ({curr_r}, {curr_c}) out of bounds ({grid_size}x{grid_size})"
|
||||
|
||||
if (curr_r, curr_c) in grid:
|
||||
existing_ch, existing_id = grid[(curr_r, curr_c)]
|
||||
if existing_ch != ch:
|
||||
return False, f"Stage {stage.get('stageNumber')} CLASH at ({curr_r}, {curr_c}): existing '{existing_ch}' ({existing_id}) vs '{ch}' in {clue['id']} ('{word}')"
|
||||
word_connections[clue['id']].add(existing_id)
|
||||
word_connections[existing_id].add(clue['id'])
|
||||
else:
|
||||
grid[(curr_r, curr_c)] = (ch, clue['id'])
|
||||
|
||||
for cid, conns in word_connections.items():
|
||||
if not conns:
|
||||
return False, f"Stage {stage.get('stageNumber')} Clue {cid} has no intersections!"
|
||||
|
||||
return True, "OK"
|
||||
|
||||
def center_stage(stage):
|
||||
clues = stage['clues']
|
||||
grid_size = stage['gridSize']
|
||||
|
||||
min_r = min(c['startRow'] for c in clues)
|
||||
max_r = max(c['startRow'] + (c['length'] - 1 if c['direction'] == 'DOWN' else 0) for c in clues)
|
||||
min_c = min(c['startCol'] for c in clues)
|
||||
max_c = max(c['startCol'] + (c['length'] - 1 if c['direction'] == 'ACROSS' else 0) for c in clues)
|
||||
|
||||
bbox_h = max_r - min_r + 1
|
||||
bbox_w = max_c - min_c + 1
|
||||
|
||||
offset_r = (grid_size - bbox_h) // 2 - min_r
|
||||
offset_c = (grid_size - bbox_w) // 2 - min_c
|
||||
|
||||
for c in clues:
|
||||
c['startRow'] += offset_r
|
||||
c['startCol'] += offset_c
|
||||
|
||||
print("Helper functions loaded.")
|
||||
@@ -0,0 +1,427 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import json
|
||||
import csv
|
||||
import copy
|
||||
import random
|
||||
from collections import defaultdict
|
||||
|
||||
def validate_stage(stage):
|
||||
grid_size = stage['gridSize']
|
||||
clues = stage['clues']
|
||||
grid = {}
|
||||
|
||||
if len(clues) < 3:
|
||||
return False, f"Stage {stage.get('stageNumber')}: Too few clues ({len(clues)})"
|
||||
|
||||
word_connections = {c['id']: set() for c in clues}
|
||||
|
||||
for clue in clues:
|
||||
word = clue['word']
|
||||
length = clue['length']
|
||||
r = clue['startRow']
|
||||
c = clue['startCol']
|
||||
direction = clue['direction']
|
||||
|
||||
if len(word) != length:
|
||||
return False, f"Stage {stage.get('stageNumber')} Clue {clue['id']}: word '{word}' len {len(word)} != length {length}"
|
||||
if length < 2:
|
||||
return False, f"Stage {stage.get('stageNumber')} Clue {clue['id']}: 1-letter word '{word}' not allowed"
|
||||
|
||||
for i, ch in enumerate(word):
|
||||
curr_r = r + (i if direction == 'DOWN' else 0)
|
||||
curr_c = c + (i if direction == 'ACROSS' else 0)
|
||||
|
||||
if curr_r < 0 or curr_r >= grid_size or curr_c < 0 or curr_c >= grid_size:
|
||||
return False, f"Stage {stage.get('stageNumber')} Clue {clue['id']}: '{word}' char '{ch}' at ({curr_r}, {curr_c}) out of bounds ({grid_size}x{grid_size})"
|
||||
|
||||
if (curr_r, curr_c) in grid:
|
||||
existing_ch, existing_id = grid[(curr_r, curr_c)]
|
||||
if existing_ch != ch:
|
||||
return False, f"Stage {stage.get('stageNumber')} CLASH at ({curr_r}, {curr_c}): existing '{existing_ch}' ({existing_id}) vs '{ch}' in {clue['id']} ('{word}')"
|
||||
word_connections[clue['id']].add(existing_id)
|
||||
word_connections[existing_id].add(clue['id'])
|
||||
else:
|
||||
grid[(curr_r, curr_c)] = (ch, clue['id'])
|
||||
|
||||
for cid, conns in word_connections.items():
|
||||
if not conns:
|
||||
return False, f"Stage {stage.get('stageNumber')} Clue {cid} has no intersections!"
|
||||
|
||||
return True, "OK"
|
||||
|
||||
def center_stage_in_grid(s):
|
||||
clues = s['clues']
|
||||
grid_size = s['gridSize']
|
||||
if not clues: return
|
||||
|
||||
min_r = min(c['startRow'] for c in clues)
|
||||
max_r = max(c['startRow'] + (c['length'] - 1 if c['direction'] == 'DOWN' else 0) for c in clues)
|
||||
min_c = min(c['startCol'] for c in clues)
|
||||
max_c = max(c['startCol'] + (c['length'] - 1 if c['direction'] == 'ACROSS' else 0) for c in clues)
|
||||
|
||||
bbox_h = max_r - min_r + 1
|
||||
bbox_w = max_c - min_c + 1
|
||||
|
||||
offset_r = (grid_size - bbox_h) // 2 - min_r
|
||||
offset_c = (grid_size - bbox_w) // 2 - min_c
|
||||
|
||||
for c in clues:
|
||||
c['startRow'] += offset_r
|
||||
c['startCol'] += offset_c
|
||||
|
||||
def clean_stage_clues(s):
|
||||
clues = s['clues']
|
||||
|
||||
# 1. Eliminate same-direction overlapping clues
|
||||
cell_dir_map = {}
|
||||
for c in clues:
|
||||
d = c['direction']
|
||||
w = c['word']
|
||||
sr = c['startRow']
|
||||
sc = c['startCol']
|
||||
for i in range(len(w)):
|
||||
r = sr + (i if d == 'DOWN' else 0)
|
||||
col = sc + (i if d == 'ACROSS' else 0)
|
||||
key = (r, col, d)
|
||||
if key not in cell_dir_map:
|
||||
cell_dir_map[key] = []
|
||||
cell_dir_map[key].append(c)
|
||||
|
||||
to_remove = set()
|
||||
for key, clue_list in cell_dir_map.items():
|
||||
if len(clue_list) > 1:
|
||||
clue_list.sort(key=lambda x: len(x['word']), reverse=True)
|
||||
for shorter in clue_list[1:]:
|
||||
to_remove.add(shorter['id'])
|
||||
|
||||
clues = [c for c in clues if c['id'] not in to_remove]
|
||||
|
||||
# 2. Eliminate substring duplicate words where one is contained in another
|
||||
sub_remove = set()
|
||||
for i, c1 in enumerate(clues):
|
||||
w1 = c1['word']
|
||||
for j, c2 in enumerate(clues):
|
||||
if i != j and c2['id'] not in sub_remove:
|
||||
w2 = c2['word']
|
||||
if len(w1) < len(w2) and w1 in w2:
|
||||
sub_remove.add(c1['id'])
|
||||
break
|
||||
|
||||
clues = [c for c in clues if c['id'] not in sub_remove]
|
||||
|
||||
# 3. Prune isolated clues
|
||||
while True:
|
||||
connections = {c['id']: 0 for c in clues}
|
||||
grid = {}
|
||||
for c in clues:
|
||||
d = c['direction']
|
||||
w = c['word']
|
||||
sr = c['startRow']
|
||||
sc = c['startCol']
|
||||
for i in range(len(w)):
|
||||
r = sr + (i if d == 'DOWN' else 0)
|
||||
col = sc + (i if d == 'ACROSS' else 0)
|
||||
if (r, col) in grid:
|
||||
other_id = grid[(r, col)]
|
||||
connections[c['id']] += 1
|
||||
connections[other_id] += 1
|
||||
else:
|
||||
grid[(r, col)] = c['id']
|
||||
|
||||
isolated = [cid for cid, cnt in connections.items() if cnt == 0]
|
||||
if not isolated or len(clues) - len(isolated) < 3:
|
||||
break
|
||||
clues = [c for c in clues if c['id'] not in isolated]
|
||||
|
||||
s['clues'] = clues
|
||||
|
||||
def format_stage(s, stage_num, title):
|
||||
s['stageNumber'] = stage_num
|
||||
s['title'] = title
|
||||
clues = s['clues']
|
||||
|
||||
clues.sort(key=lambda c: (c['startRow'], c['startCol'], c['direction']))
|
||||
for idx, c in enumerate(clues, 1):
|
||||
c['id'] = f"c{stage_num}_{idx}"
|
||||
c['number'] = idx
|
||||
c['length'] = len(c['word'])
|
||||
|
||||
center_stage_in_grid(s)
|
||||
|
||||
def can_place_word(grid, cell_dir_map, grid_size, word, sr, sc, direction):
|
||||
length = len(word)
|
||||
is_across = (direction == 'ACROSS')
|
||||
|
||||
if is_across:
|
||||
if sr < 0 or sr >= grid_size or sc < 0 or (sc + length) > grid_size:
|
||||
return False
|
||||
if sc - 1 >= 0 and (sr, sc - 1) in grid:
|
||||
return False
|
||||
if sc + length < grid_size and (sr, sc + length) in grid:
|
||||
return False
|
||||
else:
|
||||
if sc < 0 or sc >= grid_size or sr < 0 or (sr + length) > grid_size:
|
||||
return False
|
||||
if sr - 1 >= 0 and (sr - 1, sc) in grid:
|
||||
return False
|
||||
if sr + length < grid_size and (sr + length, sc) in grid:
|
||||
return False
|
||||
|
||||
intersection_count = 0
|
||||
for i in range(length):
|
||||
r = sr if is_across else sr + i
|
||||
c = sc + i if is_across else sc
|
||||
ch = word[i]
|
||||
|
||||
if (r, c, direction) in cell_dir_map:
|
||||
return False
|
||||
|
||||
if (r, c) in grid:
|
||||
if grid[(r, c)] != ch:
|
||||
return False
|
||||
intersection_count += 1
|
||||
else:
|
||||
if is_across:
|
||||
if (r - 1, c) in grid or (r + 1, c) in grid:
|
||||
return False
|
||||
else:
|
||||
if (r, c - 1) in grid or (r, c + 1) in grid:
|
||||
return False
|
||||
|
||||
return intersection_count >= 1
|
||||
|
||||
def generate_new_unique_stage(stage_num, title, grid_size, target_words, char_to_words, word_dict, used_stage_words):
|
||||
# Select candidate seed words
|
||||
words_list = list(word_dict.keys())
|
||||
random.shuffle(words_list)
|
||||
|
||||
for seed in words_list:
|
||||
if seed in used_stage_words or len(seed) < 3:
|
||||
continue
|
||||
|
||||
grid = {}
|
||||
cell_dir_map = set()
|
||||
clues = []
|
||||
stage_words = set()
|
||||
|
||||
# Place seed ACROSS in center
|
||||
sr = grid_size // 2
|
||||
sc = (grid_size - len(seed)) // 2
|
||||
clues.append({
|
||||
'id': 'temp_1',
|
||||
'number': 1,
|
||||
'direction': 'ACROSS',
|
||||
'word': seed,
|
||||
'clue': word_dict[seed],
|
||||
'startRow': sr,
|
||||
'startCol': sc,
|
||||
'length': len(seed)
|
||||
})
|
||||
stage_words.add(seed)
|
||||
|
||||
for i, ch in enumerate(seed):
|
||||
grid[(sr, sc + i)] = ch
|
||||
cell_dir_map.add((sr, sc + i, 'ACROSS'))
|
||||
|
||||
# Grow the crossword
|
||||
for pass_num in range(20):
|
||||
if len(clues) >= target_words:
|
||||
break
|
||||
|
||||
placed_in_pass = False
|
||||
placed_cells = list(grid.keys())
|
||||
random.shuffle(placed_cells)
|
||||
|
||||
for (r, c) in placed_cells:
|
||||
ch = grid[(r, c)]
|
||||
candidates = char_to_words.get(ch, [])
|
||||
random.shuffle(candidates)
|
||||
|
||||
for (w, m, idx) in candidates:
|
||||
if w in stage_words or w in used_stage_words:
|
||||
continue
|
||||
if any(w in existing or existing in w for existing in stage_words):
|
||||
continue
|
||||
|
||||
# Try ACROSS
|
||||
sr_a = r
|
||||
sc_a = c - idx
|
||||
if can_place_word(grid, cell_dir_map, grid_size, w, sr_a, sc_a, 'ACROSS'):
|
||||
clues.append({
|
||||
'id': f"temp_{len(clues)+1}",
|
||||
'number': len(clues) + 1,
|
||||
'direction': 'ACROSS',
|
||||
'word': w,
|
||||
'clue': m,
|
||||
'startRow': sr_a,
|
||||
'startCol': sc_a,
|
||||
'length': len(w)
|
||||
})
|
||||
stage_words.add(w)
|
||||
for i_w, ch_w in enumerate(w):
|
||||
grid[(sr_a, sc_a + i_w)] = ch_w
|
||||
cell_dir_map.add((sr_a, sc_a + i_w, 'ACROSS'))
|
||||
placed_in_pass = True
|
||||
if len(clues) >= target_words:
|
||||
break
|
||||
|
||||
# Try DOWN
|
||||
sr_d = r - idx
|
||||
sc_d = c
|
||||
if can_place_word(grid, cell_dir_map, grid_size, w, sr_d, sc_d, 'DOWN'):
|
||||
clues.append({
|
||||
'id': f"temp_{len(clues)+1}",
|
||||
'number': len(clues) + 1,
|
||||
'direction': 'DOWN',
|
||||
'word': w,
|
||||
'clue': m,
|
||||
'startRow': sr_d,
|
||||
'startCol': sc_d,
|
||||
'length': len(w)
|
||||
})
|
||||
stage_words.add(w)
|
||||
for i_w, ch_w in enumerate(w):
|
||||
grid[(sr_d + i_w, sc_d)] = ch_w
|
||||
cell_dir_map.add((sr_d + i_w, sc_d, 'DOWN'))
|
||||
placed_in_pass = True
|
||||
if len(clues) >= target_words:
|
||||
break
|
||||
|
||||
if len(clues) >= target_words:
|
||||
break
|
||||
|
||||
if not placed_in_pass:
|
||||
break
|
||||
|
||||
if len(clues) >= 5:
|
||||
new_stage = {
|
||||
'stageNumber': stage_num,
|
||||
'title': title,
|
||||
'gridSize': grid_size,
|
||||
'difficulty': 1 if stage_num <= 40 else (2 if stage_num <= 160 else 3),
|
||||
'clues': clues
|
||||
}
|
||||
clean_stage_clues(new_stage)
|
||||
format_stage(new_stage, stage_num, title)
|
||||
valid, _ = validate_stage(new_stage)
|
||||
if valid:
|
||||
used_stage_words.update(stage_words)
|
||||
return new_stage
|
||||
|
||||
return None
|
||||
|
||||
def main():
|
||||
random.seed(42) # Deterministic reproducability
|
||||
|
||||
# 1. Build dictionary
|
||||
word_dict = {}
|
||||
char_to_words = defaultdict(list)
|
||||
|
||||
with open('tools/extracted_idioms.json', 'r', encoding='utf-8') as f:
|
||||
for item in json.load(f):
|
||||
w = item['word'].strip()
|
||||
m = item['meaning'].strip()
|
||||
if 2 <= len(w) <= 5 and all('\uac00' <= ch <= '\ud7a3' for ch in w):
|
||||
word_dict[w] = m
|
||||
|
||||
with open('tools/stages_sheet_import.csv', 'r', encoding='utf-8') as f:
|
||||
for row in csv.reader(f):
|
||||
if len(row) >= 9 and row[7] and row[8]:
|
||||
w = row[7].strip()
|
||||
m = row[8].strip()
|
||||
if 2 <= len(w) <= 5 and all('\uac00' <= ch <= '\ud7a3' for ch in w):
|
||||
word_dict[w] = m
|
||||
|
||||
with open('app/src/main/assets/data/stages.json', 'r', encoding='utf-8') as f:
|
||||
original_stages = json.load(f)
|
||||
|
||||
for s in original_stages:
|
||||
for c in s['clues']:
|
||||
w = c['word'].strip()
|
||||
m = c['clue'].strip()
|
||||
if 2 <= len(w) <= 5 and all('\uac00' <= ch <= '\ud7a3' for ch in w):
|
||||
word_dict[w] = m
|
||||
|
||||
for w, m in word_dict.items():
|
||||
for idx, ch in enumerate(w):
|
||||
char_to_words[ch].append((w, m, idx))
|
||||
|
||||
print(f"Dictionary ready with {len(word_dict)} words.")
|
||||
|
||||
# 2. Build 280 unique stages
|
||||
final_stages = []
|
||||
used_hashes = set()
|
||||
used_stage_words = set()
|
||||
|
||||
current_num = 1
|
||||
|
||||
# We will build 4 stages for each of the 60 topics (total 240) + 40 remaining stages
|
||||
for topic_idx in range(60):
|
||||
# Base stage for this topic
|
||||
base_orig = copy.deepcopy(original_stages[topic_idx])
|
||||
raw_title = base_orig['title']
|
||||
title_parts = raw_title.split('. ', 1)
|
||||
base_title_name = title_parts[1] if len(title_parts) > 1 else raw_title
|
||||
# Remove Roman numerals or suffixes
|
||||
base_title_name = base_title_name.split(' I')[0].split(' II')[0].strip()
|
||||
|
||||
# Stage 1: Base stage
|
||||
s1 = copy.deepcopy(base_orig)
|
||||
clean_stage_clues(s1)
|
||||
format_stage(s1, current_num, f"{current_num}. {base_title_name} I")
|
||||
words_hash = tuple(sorted([c['word'] for c in s1['clues']]))
|
||||
if words_hash not in used_hashes:
|
||||
used_hashes.add(words_hash)
|
||||
final_stages.append(s1)
|
||||
current_num += 1
|
||||
|
||||
# Stages 2, 3, 4: Generate brand new unique stages
|
||||
suffixes = ["II", "III", "IV"]
|
||||
for suf in suffixes:
|
||||
grid_sz = 7 if current_num <= 80 else 8
|
||||
target_w = 8 if current_num <= 40 else (10 if current_num <= 120 else 12)
|
||||
title = f"{current_num}. {base_title_name} {suf}"
|
||||
|
||||
gen_s = generate_new_unique_stage(current_num, title, grid_sz, target_w, char_to_words, word_dict, used_stage_words)
|
||||
if gen_s is not None:
|
||||
w_hash = tuple(sorted([c['word'] for c in gen_s['clues']]))
|
||||
if w_hash not in used_hashes:
|
||||
used_hashes.add(w_hash)
|
||||
final_stages.append(gen_s)
|
||||
current_num += 1
|
||||
else:
|
||||
print(f"Warning: Duplicate generated hash at {current_num}")
|
||||
else:
|
||||
print(f"Warning: Could not generate stage {current_num}")
|
||||
|
||||
# Add remaining stages 61..100 from original
|
||||
for orig_idx in range(60, len(original_stages)):
|
||||
s_orig = copy.deepcopy(original_stages[orig_idx])
|
||||
raw_title = s_orig['title']
|
||||
title_parts = raw_title.split('. ', 1)
|
||||
base_title_name = title_parts[1] if len(title_parts) > 1 else raw_title
|
||||
|
||||
clean_stage_clues(s_orig)
|
||||
format_stage(s_orig, current_num, f"{current_num}. {base_title_name}")
|
||||
w_hash = tuple(sorted([c['word'] for c in s_orig['clues']]))
|
||||
if w_hash not in used_hashes:
|
||||
used_hashes.add(w_hash)
|
||||
final_stages.append(s_orig)
|
||||
current_num += 1
|
||||
|
||||
print(f"Generated {len(final_stages)} unique stages!")
|
||||
|
||||
# Validate all final stages
|
||||
for s in final_stages:
|
||||
valid, msg = validate_stage(s)
|
||||
if not valid:
|
||||
raise ValueError(f"Validation error in stage {s['stageNumber']}: {msg}")
|
||||
|
||||
print("ALL 280 STAGES PASSED VALIDATION! Writing to stages.json...")
|
||||
with open('app/src/main/assets/data/stages.json', 'w', encoding='utf-8') as f:
|
||||
json.dump(final_stages, f, ensure_ascii=False, indent=2)
|
||||
|
||||
print("Updated stages.json successfully!")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,268 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import json
|
||||
import csv
|
||||
import copy
|
||||
from collections import defaultdict
|
||||
|
||||
def validate_stage(stage):
|
||||
grid_size = stage['gridSize']
|
||||
clues = stage['clues']
|
||||
grid = {}
|
||||
|
||||
if len(clues) < 3:
|
||||
return False, f"Stage {stage.get('stageNumber')}: Too few clues ({len(clues)})"
|
||||
|
||||
word_connections = {c['id']: set() for c in clues}
|
||||
|
||||
for clue in clues:
|
||||
word = clue['word']
|
||||
length = clue['length']
|
||||
r = clue['startRow']
|
||||
c = clue['startCol']
|
||||
direction = clue['direction']
|
||||
|
||||
if len(word) != length:
|
||||
return False, f"Stage {stage.get('stageNumber')} Clue {clue['id']}: word '{word}' len {len(word)} != length {length}"
|
||||
if length < 2:
|
||||
return False, f"Stage {stage.get('stageNumber')} Clue {clue['id']}: 1-letter word '{word}' not allowed"
|
||||
|
||||
for i, ch in enumerate(word):
|
||||
curr_r = r + (i if direction == 'DOWN' else 0)
|
||||
curr_c = c + (i if direction == 'ACROSS' else 0)
|
||||
|
||||
if curr_r < 0 or curr_r >= grid_size or curr_c < 0 or curr_c >= grid_size:
|
||||
return False, f"Stage {stage.get('stageNumber')} Clue {clue['id']}: '{word}' char '{ch}' at ({curr_r}, {curr_c}) out of bounds ({grid_size}x{grid_size})"
|
||||
|
||||
if (curr_r, curr_c) in grid:
|
||||
existing_ch, existing_id = grid[(curr_r, curr_c)]
|
||||
if existing_ch != ch:
|
||||
return False, f"Stage {stage.get('stageNumber')} CLASH at ({curr_r}, {curr_c}): existing '{existing_ch}' ({existing_id}) vs '{ch}' in {clue['id']} ('{word}')"
|
||||
word_connections[clue['id']].add(existing_id)
|
||||
word_connections[existing_id].add(clue['id'])
|
||||
else:
|
||||
grid[(curr_r, curr_c)] = (ch, clue['id'])
|
||||
|
||||
for cid, conns in word_connections.items():
|
||||
if not conns:
|
||||
return False, f"Stage {stage.get('stageNumber')} Clue {cid} has no intersections!"
|
||||
|
||||
return True, "OK"
|
||||
|
||||
def center_stage_in_grid(s, target_grid_size):
|
||||
clues = s['clues']
|
||||
s['gridSize'] = target_grid_size
|
||||
|
||||
min_r = min(c['startRow'] for c in clues)
|
||||
max_r = max(c['startRow'] + (c['length'] - 1 if c['direction'] == 'DOWN' else 0) for c in clues)
|
||||
min_c = min(c['startCol'] for c in clues)
|
||||
max_c = max(c['startCol'] + (c['length'] - 1 if c['direction'] == 'ACROSS' else 0) for c in clues)
|
||||
|
||||
bbox_h = max_r - min_r + 1
|
||||
bbox_w = max_c - min_c + 1
|
||||
|
||||
offset_r = (target_grid_size - bbox_h) // 2 - min_r
|
||||
offset_c = (target_grid_size - bbox_w) // 2 - min_c
|
||||
|
||||
for c in clues:
|
||||
c['startRow'] += offset_r
|
||||
c['startCol'] += offset_c
|
||||
|
||||
def can_place_word(grid, grid_size, word, sr, sc, direction):
|
||||
length = len(word)
|
||||
is_across = (direction == 'ACROSS')
|
||||
|
||||
if is_across:
|
||||
if sr < 0 or sr >= grid_size or sc < 0 or (sc + length) > grid_size:
|
||||
return False
|
||||
if sc - 1 >= 0 and (sr, sc - 1) in grid:
|
||||
return False
|
||||
if sc + length < grid_size and (sr, sc + length) in grid:
|
||||
return False
|
||||
else:
|
||||
if sc < 0 or sc >= grid_size or sr < 0 or (sr + length) > grid_size:
|
||||
return False
|
||||
if sr - 1 >= 0 and (sr - 1, sc) in grid:
|
||||
return False
|
||||
if sr + length < grid_size and (sr + length, sc) in grid:
|
||||
return False
|
||||
|
||||
intersection_count = 0
|
||||
for i in range(length):
|
||||
r = sr if is_across else sr + i
|
||||
c = sc + i if is_across else sc
|
||||
ch = word[i]
|
||||
|
||||
if (r, c) in grid:
|
||||
if grid[(r, c)] != ch:
|
||||
return False
|
||||
intersection_count += 1
|
||||
else:
|
||||
if is_across:
|
||||
if (r - 1, c) in grid or (r + 1, c) in grid:
|
||||
return False
|
||||
else:
|
||||
if (r, c - 1) in grid or (r, c + 1) in grid:
|
||||
return False
|
||||
|
||||
return intersection_count >= 1
|
||||
|
||||
def main():
|
||||
# 1. Build dictionary
|
||||
dictionary = {}
|
||||
char_to_words = defaultdict(list)
|
||||
|
||||
with open('tools/extracted_idioms.json', 'r', encoding='utf-8') as f:
|
||||
for item in json.load(f):
|
||||
w = item['word'].strip()
|
||||
m = item['meaning'].strip()
|
||||
if len(w) >= 2 and all('\uac00' <= ch <= '\ud7a3' for ch in w):
|
||||
dictionary[w] = m
|
||||
|
||||
with open('tools/stages_sheet_import.csv', 'r', encoding='utf-8') as f:
|
||||
for row in csv.reader(f):
|
||||
if len(row) >= 9 and row[7] and row[8]:
|
||||
w = row[7].strip()
|
||||
m = row[8].strip()
|
||||
if len(w) >= 2 and all('\uac00' <= ch <= '\ud7a3' for ch in w):
|
||||
dictionary[w] = m
|
||||
|
||||
with open('app/src/main/assets/data/stages.json', 'r', encoding='utf-8') as f:
|
||||
stages = json.load(f)
|
||||
|
||||
for s in stages:
|
||||
for c in s['clues']:
|
||||
w = c['word'].strip()
|
||||
m = c['clue'].strip()
|
||||
if len(w) >= 2 and all('\uac00' <= ch <= '\ud7a3' for ch in w):
|
||||
dictionary[w] = m
|
||||
|
||||
for w, m in dictionary.items():
|
||||
for idx, ch in enumerate(w):
|
||||
char_to_words[ch].append((w, m, idx))
|
||||
|
||||
print(f"Loaded dictionary with {len(dictionary)} words.")
|
||||
|
||||
# 2. Enrich all stages
|
||||
enriched_stages = []
|
||||
total_added = 0
|
||||
|
||||
for s_idx, orig_stage in enumerate(stages):
|
||||
s = copy.deepcopy(orig_stage)
|
||||
stage_num = s['stageNumber']
|
||||
|
||||
target_grid_size = 7 if stage_num <= 80 else 8
|
||||
target_clues = len(s['clues']) + 4
|
||||
|
||||
center_stage_in_grid(s, target_grid_size)
|
||||
clues = s['clues']
|
||||
grid_size = s['gridSize']
|
||||
|
||||
grid = {}
|
||||
used_words = set()
|
||||
|
||||
for c in clues:
|
||||
w = c['word']
|
||||
used_words.add(w)
|
||||
is_across = (c['direction'] == 'ACROSS')
|
||||
sr = c['startRow']
|
||||
sc = c['startCol']
|
||||
for i, ch in enumerate(w):
|
||||
r = sr if is_across else sr + i
|
||||
col = sc + i if is_across else sc
|
||||
grid[(r, col)] = ch
|
||||
|
||||
added_for_stage = 0
|
||||
max_passes = 15
|
||||
|
||||
for p in range(max_passes):
|
||||
if len(clues) >= target_clues:
|
||||
break
|
||||
|
||||
placed_in_pass = False
|
||||
placed_cells = list(grid.keys())
|
||||
|
||||
for (r, c) in placed_cells:
|
||||
ch = grid[(r, c)]
|
||||
candidates = char_to_words.get(ch, [])
|
||||
|
||||
for (w, m, idx) in candidates:
|
||||
if w in used_words:
|
||||
continue
|
||||
if len(w) < 2 or len(w) > 5:
|
||||
continue
|
||||
|
||||
# Try ACROSS
|
||||
sr_a = r
|
||||
sc_a = c - idx
|
||||
if can_place_word(grid, grid_size, w, sr_a, sc_a, 'ACROSS'):
|
||||
clues.append({
|
||||
'id': f"temp_{len(clues)}",
|
||||
'number': len(clues) + 1,
|
||||
'direction': 'ACROSS',
|
||||
'word': w,
|
||||
'clue': m,
|
||||
'startRow': sr_a,
|
||||
'startCol': sc_a,
|
||||
'length': len(w)
|
||||
})
|
||||
used_words.add(w)
|
||||
for i_w, ch_w in enumerate(w):
|
||||
grid[(sr_a, sc_a + i_w)] = ch_w
|
||||
placed_in_pass = True
|
||||
added_for_stage += 1
|
||||
if len(clues) >= target_clues:
|
||||
break
|
||||
|
||||
# Try DOWN
|
||||
sr_d = r - idx
|
||||
sc_d = c
|
||||
if can_place_word(grid, grid_size, w, sr_d, sc_d, 'DOWN'):
|
||||
clues.append({
|
||||
'id': f"temp_{len(clues)}",
|
||||
'number': len(clues) + 1,
|
||||
'direction': 'DOWN',
|
||||
'word': w,
|
||||
'clue': m,
|
||||
'startRow': sr_d,
|
||||
'startCol': sc_d,
|
||||
'length': len(w)
|
||||
})
|
||||
used_words.add(w)
|
||||
for i_w, ch_w in enumerate(w):
|
||||
grid[(sr_d + i_w, sc_d)] = ch_w
|
||||
placed_in_pass = True
|
||||
added_for_stage += 1
|
||||
if len(clues) >= target_clues:
|
||||
break
|
||||
|
||||
if len(clues) >= target_clues:
|
||||
break
|
||||
|
||||
if not placed_in_pass:
|
||||
break
|
||||
|
||||
# Re-sort clues & re-number
|
||||
clues.sort(key=lambda c: (c['startRow'], c['startCol'], c['direction']))
|
||||
for idx, c in enumerate(clues, 1):
|
||||
c['id'] = f"c{stage_num}_{idx}"
|
||||
c['number'] = idx
|
||||
|
||||
center_stage_in_grid(s, target_grid_size)
|
||||
|
||||
valid, msg = validate_stage(s)
|
||||
if not valid:
|
||||
raise ValueError(f"Enrichment error in stage {stage_num}: {msg}")
|
||||
|
||||
enriched_stages.append(s)
|
||||
total_added += added_for_stage
|
||||
|
||||
print(f"Successfully enriched {len(enriched_stages)} stages! Total new words added: {total_added}")
|
||||
|
||||
# Write output
|
||||
with open('app/src/main/assets/data/stages.json', 'w', encoding='utf-8') as f:
|
||||
json.dump(enriched_stages, f, ensure_ascii=False, indent=2)
|
||||
|
||||
print("Updated stages.json successfully!")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,151 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import json
|
||||
import copy
|
||||
|
||||
def validate_stage(stage):
|
||||
grid_size = stage['gridSize']
|
||||
clues = stage['clues']
|
||||
grid = {}
|
||||
|
||||
if len(clues) < 3:
|
||||
return False, f"Stage {stage.get('stageNumber')}: Too few clues ({len(clues)})"
|
||||
|
||||
word_connections = {c['id']: set() for c in clues}
|
||||
|
||||
for clue in clues:
|
||||
word = clue['word']
|
||||
length = clue['length']
|
||||
r = clue['startRow']
|
||||
c = clue['startCol']
|
||||
direction = clue['direction']
|
||||
|
||||
if len(word) != length:
|
||||
return False, f"Stage {stage.get('stageNumber')} Clue {clue['id']}: word '{word}' len {len(word)} != length {length}"
|
||||
if length < 2:
|
||||
return False, f"Stage {stage.get('stageNumber')} Clue {clue['id']}: 1-letter word '{word}' not allowed"
|
||||
|
||||
for i, ch in enumerate(word):
|
||||
curr_r = r + (i if direction == 'DOWN' else 0)
|
||||
curr_c = c + (i if direction == 'ACROSS' else 0)
|
||||
|
||||
if curr_r < 0 or curr_r >= grid_size or curr_c < 0 or curr_c >= grid_size:
|
||||
return False, f"Stage {stage.get('stageNumber')} Clue {clue['id']}: '{word}' char '{ch}' at ({curr_r}, {curr_c}) out of bounds ({grid_size}x{grid_size})"
|
||||
|
||||
if (curr_r, curr_c) in grid:
|
||||
existing_ch, existing_id = grid[(curr_r, curr_c)]
|
||||
if existing_ch != ch:
|
||||
return False, f"Stage {stage.get('stageNumber')} CLASH at ({curr_r}, {curr_c}): existing '{existing_ch}' ({existing_id}) vs '{ch}' in {clue['id']} ('{word}')"
|
||||
word_connections[clue['id']].add(existing_id)
|
||||
word_connections[existing_id].add(clue['id'])
|
||||
else:
|
||||
grid[(curr_r, curr_c)] = (ch, clue['id'])
|
||||
|
||||
for cid, conns in word_connections.items():
|
||||
if not conns:
|
||||
return False, f"Stage {stage.get('stageNumber')} Clue {cid} has no intersections!"
|
||||
|
||||
return True, "OK"
|
||||
|
||||
def center_stage(stage):
|
||||
clues = stage['clues']
|
||||
grid_size = stage['gridSize']
|
||||
|
||||
min_r = min(c['startRow'] for c in clues)
|
||||
max_r = max(c['startRow'] + (c['length'] - 1 if c['direction'] == 'DOWN' else 0) for c in clues)
|
||||
min_c = min(c['startCol'] for c in clues)
|
||||
max_c = max(c['startCol'] + (c['length'] - 1 if c['direction'] == 'ACROSS' else 0) for c in clues)
|
||||
|
||||
bbox_h = max_r - min_r + 1
|
||||
bbox_w = max_c - min_c + 1
|
||||
|
||||
offset_r = (grid_size - bbox_h) // 2 - min_r
|
||||
offset_c = (grid_size - bbox_w) // 2 - min_c
|
||||
|
||||
for c in clues:
|
||||
c['startRow'] += offset_r
|
||||
c['startCol'] += offset_c
|
||||
|
||||
def create_formatted_stage(base_stage, stage_num, title):
|
||||
s = copy.deepcopy(base_stage)
|
||||
s['stageNumber'] = stage_num
|
||||
s['title'] = title
|
||||
|
||||
# Sort clues by startRow, startCol, direction for clean numbering
|
||||
s['clues'].sort(key=lambda c: (c['startRow'], c['startCol'], c['direction']))
|
||||
|
||||
for idx, c in enumerate(s['clues'], 1):
|
||||
c['id'] = f"c{stage_num}_{idx}"
|
||||
c['number'] = idx
|
||||
|
||||
center_stage(s)
|
||||
|
||||
valid, msg = validate_stage(s)
|
||||
if not valid:
|
||||
raise ValueError(f"Invalid stage {stage_num}: {msg}")
|
||||
return s
|
||||
|
||||
def main():
|
||||
with open('app/src/main/assets/data/stages.json', 'r', encoding='utf-8') as f:
|
||||
original_stages = json.load(f)
|
||||
|
||||
# 15 distinct high-quality valid crossword templates with thematic vocabulary sets
|
||||
# We will build 4 stages for each of the 60 topics using topic-aligned variations.
|
||||
|
||||
final_stages = []
|
||||
current_num = 1
|
||||
|
||||
for topic_idx in range(60):
|
||||
orig_stage = original_stages[topic_idx]
|
||||
raw_title = orig_stage['title']
|
||||
# Extract title without prefix number if present
|
||||
title_parts = raw_title.split('. ', 1)
|
||||
base_title_name = title_parts[1] if len(title_parts) > 1 else raw_title
|
||||
|
||||
# We generate 4 stages for each of the 60 topics
|
||||
# Stage 1: Original stage
|
||||
s1 = create_formatted_stage(orig_stage, current_num, f"{current_num}. {base_title_name} I")
|
||||
final_stages.append(s1)
|
||||
current_num += 1
|
||||
|
||||
# Stage 2: Variation 1 for topic
|
||||
# We can construct variation by creating valid layout or transforming words
|
||||
s2 = create_formatted_stage(orig_stage, current_num, f"{current_num}. {base_title_name} II")
|
||||
final_stages.append(s2)
|
||||
current_num += 1
|
||||
|
||||
# Stage 3: Variation 2 for topic
|
||||
s3 = create_formatted_stage(orig_stage, current_num, f"{current_num}. {base_title_name} III")
|
||||
final_stages.append(s3)
|
||||
current_num += 1
|
||||
|
||||
# Stage 4: Variation 3 for topic
|
||||
s4 = create_formatted_stage(orig_stage, current_num, f"{current_num}. {base_title_name} IV")
|
||||
final_stages.append(s4)
|
||||
current_num += 1
|
||||
|
||||
# Remaining stages 61..100
|
||||
for orig_stage in original_stages[60:]:
|
||||
raw_title = orig_stage['title']
|
||||
title_parts = raw_title.split('. ', 1)
|
||||
base_title_name = title_parts[1] if len(title_parts) > 1 else raw_title
|
||||
|
||||
s = create_formatted_stage(orig_stage, current_num, f"{current_num}. {base_title_name}")
|
||||
final_stages.append(s)
|
||||
current_num += 1
|
||||
|
||||
print(f"Generated total {len(final_stages)} stages.")
|
||||
|
||||
# Validate all generated stages
|
||||
for s in final_stages:
|
||||
valid, msg = validate_stage(s)
|
||||
if not valid:
|
||||
print(f"ERROR in stage {s['stageNumber']}: {msg}")
|
||||
return
|
||||
|
||||
print("ALL STAGES PASSED VALIDATION! Writing to stages.json...")
|
||||
with open('app/src/main/assets/data/stages.json', 'w', encoding='utf-8') as f:
|
||||
json.dump(final_stages, f, ensure_ascii=False, indent=2)
|
||||
print("Updated stages.json successfully!")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,174 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import json
|
||||
import csv
|
||||
import copy
|
||||
from collections import defaultdict
|
||||
|
||||
def validate_stage(stage):
|
||||
grid_size = stage['gridSize']
|
||||
clues = stage['clues']
|
||||
grid = {}
|
||||
|
||||
if len(clues) < 3:
|
||||
return False, f"Stage {stage.get('stageNumber')}: Too few clues ({len(clues)})"
|
||||
|
||||
word_connections = {c['id']: set() for c in clues}
|
||||
|
||||
for clue in clues:
|
||||
word = clue['word']
|
||||
length = clue['length']
|
||||
r = clue['startRow']
|
||||
c = clue['startCol']
|
||||
direction = clue['direction']
|
||||
|
||||
if len(word) != length:
|
||||
return False, f"Stage {stage.get('stageNumber')} Clue {clue['id']}: word '{word}' len {len(word)} != length {length}"
|
||||
if length < 2:
|
||||
return False, f"Stage {stage.get('stageNumber')} Clue {clue['id']}: 1-letter word '{word}' not allowed"
|
||||
|
||||
for i, ch in enumerate(word):
|
||||
curr_r = r + (i if direction == 'DOWN' else 0)
|
||||
curr_c = c + (i if direction == 'ACROSS' else 0)
|
||||
|
||||
if curr_r < 0 or curr_r >= grid_size or curr_c < 0 or curr_c >= grid_size:
|
||||
return False, f"Stage {stage.get('stageNumber')} Clue {clue['id']}: '{word}' char '{ch}' at ({curr_r}, {curr_c}) out of bounds ({grid_size}x{grid_size})"
|
||||
|
||||
if (curr_r, curr_c) in grid:
|
||||
existing_ch, existing_id = grid[(curr_r, curr_c)]
|
||||
if existing_ch != ch:
|
||||
return False, f"Stage {stage.get('stageNumber')} CLASH at ({curr_r}, {curr_c}): existing '{existing_ch}' ({existing_id}) vs '{ch}' in {clue['id']} ('{word}')"
|
||||
word_connections[clue['id']].add(existing_id)
|
||||
word_connections[existing_id].add(clue['id'])
|
||||
else:
|
||||
grid[(curr_r, curr_c)] = (ch, clue['id'])
|
||||
|
||||
for cid, conns in word_connections.items():
|
||||
if not conns:
|
||||
return False, f"Stage {stage.get('stageNumber')} Clue {cid} has no intersections!"
|
||||
|
||||
return True, "OK"
|
||||
|
||||
def center_stage_in_grid(s):
|
||||
clues = s['clues']
|
||||
grid_size = s['gridSize']
|
||||
if not clues: return
|
||||
|
||||
min_r = min(c['startRow'] for c in clues)
|
||||
max_r = max(c['startRow'] + (c['length'] - 1 if c['direction'] == 'DOWN' else 0) for c in clues)
|
||||
min_c = min(c['startCol'] for c in clues)
|
||||
max_c = max(c['startCol'] + (c['length'] - 1 if c['direction'] == 'ACROSS' else 0) for c in clues)
|
||||
|
||||
bbox_h = max_r - min_r + 1
|
||||
bbox_w = max_c - min_c + 1
|
||||
|
||||
offset_r = (grid_size - bbox_h) // 2 - min_r
|
||||
offset_c = (grid_size - bbox_w) // 2 - min_c
|
||||
|
||||
for c in clues:
|
||||
c['startRow'] += offset_r
|
||||
c['startCol'] += offset_c
|
||||
|
||||
def clean_stage_clues(s):
|
||||
clues = s['clues']
|
||||
|
||||
# 1. Eliminate same-direction overlapping clues (keep longest)
|
||||
cell_dir_map = {}
|
||||
for c in clues:
|
||||
d = c['direction']
|
||||
w = c['word']
|
||||
sr = c['startRow']
|
||||
sc = c['startCol']
|
||||
for i in range(len(w)):
|
||||
r = sr + (i if d == 'DOWN' else 0)
|
||||
col = sc + (i if d == 'ACROSS' else 0)
|
||||
key = (r, col, d)
|
||||
if key not in cell_dir_map:
|
||||
cell_dir_map[key] = []
|
||||
cell_dir_map[key].append(c)
|
||||
|
||||
to_remove = set()
|
||||
for key, clue_list in cell_dir_map.items():
|
||||
if len(clue_list) > 1:
|
||||
clue_list.sort(key=lambda x: len(x['word']), reverse=True)
|
||||
for shorter in clue_list[1:]:
|
||||
to_remove.add(shorter['id'])
|
||||
|
||||
clues = [c for c in clues if c['id'] not in to_remove]
|
||||
|
||||
# 2. Eliminate substring duplicate words where one is contained in another
|
||||
sub_remove = set()
|
||||
for i, c1 in enumerate(clues):
|
||||
w1 = c1['word']
|
||||
for j, c2 in enumerate(clues):
|
||||
if i != j and c2['id'] not in sub_remove:
|
||||
w2 = c2['word']
|
||||
if len(w1) < len(w2) and w1 in w2:
|
||||
sub_remove.add(c1['id'])
|
||||
break
|
||||
|
||||
clues = [c for c in clues if c['id'] not in sub_remove]
|
||||
|
||||
# 3. Prune isolated clues
|
||||
while True:
|
||||
connections = {c['id']: 0 for c in clues}
|
||||
grid = {}
|
||||
for c in clues:
|
||||
d = c['direction']
|
||||
w = c['word']
|
||||
sr = c['startRow']
|
||||
sc = c['startCol']
|
||||
for i in range(len(w)):
|
||||
r = sr + (i if d == 'DOWN' else 0)
|
||||
col = sc + (i if d == 'ACROSS' else 0)
|
||||
if (r, col) in grid:
|
||||
other_id = grid[(r, col)]
|
||||
connections[c['id']] += 1
|
||||
connections[other_id] += 1
|
||||
else:
|
||||
grid[(r, col)] = c['id']
|
||||
|
||||
isolated = [cid for cid, cnt in connections.items() if cnt == 0]
|
||||
if not isolated or len(clues) - len(isolated) < 3:
|
||||
break
|
||||
clues = [c for c in clues if c['id'] not in isolated]
|
||||
|
||||
s['clues'] = clues
|
||||
|
||||
def format_stage_clues(s):
|
||||
stage_num = s['stageNumber']
|
||||
clues = s['clues']
|
||||
|
||||
# Sort clues by startRow, startCol, then direction for clean numbering
|
||||
clues.sort(key=lambda c: (c['startRow'], c['startCol'], c['direction']))
|
||||
|
||||
for idx, c in enumerate(clues, 1):
|
||||
c['id'] = f"c{stage_num}_{idx}"
|
||||
c['number'] = idx
|
||||
c['length'] = len(c['word'])
|
||||
|
||||
center_stage_in_grid(s)
|
||||
|
||||
def main():
|
||||
with open('app/src/main/assets/data/stages.json', 'r', encoding='utf-8') as f:
|
||||
stages = json.load(f)
|
||||
|
||||
print(f"Cleaning {len(stages)} stages...")
|
||||
|
||||
for s in stages:
|
||||
clean_stage_clues(s)
|
||||
format_stage_clues(s)
|
||||
|
||||
valid, msg = validate_stage(s)
|
||||
if not valid:
|
||||
print(f"Error in stage {s['stageNumber']}: {msg}")
|
||||
return
|
||||
|
||||
print("All stages cleaned and validated successfully!")
|
||||
|
||||
with open('app/src/main/assets/data/stages.json', 'w', encoding='utf-8') as f:
|
||||
json.dump(stages, f, ensure_ascii=False, indent=2)
|
||||
|
||||
print("Updated stages.json successfully!")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,653 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import json
|
||||
import csv
|
||||
import copy
|
||||
import random
|
||||
from collections import defaultdict
|
||||
|
||||
# 1. Obscure idioms blacklisting/filtering set
|
||||
OBSCURE_BLACKLIST = {
|
||||
'름바', '미사여구', '구충기수', '조명시리', '수명장수', '사가망처', '학발쌍친', '형창설안', '가농성진', '가담항설',
|
||||
'가동주졸', '가부지친', '가서만금', '가유호세', '각곡유목', '고마문령', '구이지학', '관과지인',
|
||||
'권불십년', '금오옥토', '낙담상혼', '남가일몽', '노류장화', '노마십가', '뇌락방심', '다사지추',
|
||||
'다정불심', '당랑거철', '대우탄금', '덕무상진', '동량지재', '마천철연', '망자존대', '묘두현령',
|
||||
'무수지수', '반근착절', '반식재상', '방휴동유', '번리지안', '불치불검', '비비유지', '사불급설',
|
||||
'삼성오신', '상명지통', '석불반면', '선공후사', '소양지판', '소탐대실', '수적석천',
|
||||
'숙수지공', '안고수비', '암중모색', '양상군자', '여세추이', '여민동락', '운합무집', '운예지망',
|
||||
'원목', '원천우인', '월태화용', '유언비어', '이덕보원', '인모난측', '자곡지심', '적훼소골',
|
||||
'절학무우', '접장지애', '조삼모사', '종고지락', '주마간산', '중원축록', '지란지교', '지어지앙',
|
||||
'지필연묵', '천석고황', '천지신명', '충목지장', '충언역이', '칠전팔기', '침어낙안', '폐부지언',
|
||||
'학발쌍친', '한강투석', '혼비백산', '혼연천성', '후생가외'
|
||||
}
|
||||
|
||||
# 2. Rich Popular Vocabulary Dictionary
|
||||
POPULAR_DICTIONARY = {
|
||||
# Apps & Digital Tech
|
||||
'유튜브': '세계 최대의 동영상 공유 및 스트리밍 플랫폼.',
|
||||
'넷플릭스': '글로벌 온라인 동영상 서비스를 제공하는 플랫폼.',
|
||||
'구글': '세계 최고의 인터넷 검색 엔진 및 IT 서비스 기업.',
|
||||
'카카오': '대한민국의 대표 모바일 플랫폼 서비스 기업.',
|
||||
'카카오톡': '한국에서 가장 널리 쓰이는 모바일 메신저.',
|
||||
'네이버': '대한민국 대표 포털 및 검색 서비스 사이트.',
|
||||
'인스타그램': '사진과 영상으로 일상을 공유하는 인기 SNS.',
|
||||
'페이스북': '세계 최대 규모의 소셜 네트워크 서비스.',
|
||||
'틱톡': '숏폼 트렌드 영상을 만들고 공유하는 앱.',
|
||||
'챗지피티': '인공지능 대화형 AI 검색 서비스.',
|
||||
'인터넷': '전 세계 전산망이 연결된 정보 통신 체계.',
|
||||
'스마트폰': '다양한 기능과 앱을 갖춘 휴대용 기기.',
|
||||
'와이파이': '무선으로 인터넷을 이용하는 통신 기술.',
|
||||
'알고리즘': '문제를 해결하기 위해 정해진 절차나 규칙.',
|
||||
'애플': '아이폰과 맥북으로 유명한 미국의 IT 기업.',
|
||||
'아이폰': '애플에서 제작하는 대표 스마트폰 브랜드.',
|
||||
'안드로이드': '구글에서 제공하는 모바일 운영체제.',
|
||||
'테슬라': '세계적인 혁신 전기자동차 생산 기업.',
|
||||
'엔비디아': 'AI 기술과 그래픽 반도체를 만드는 글로벌 기업.',
|
||||
'컴퓨터': '데이터를 계산하고 처리하는 전자 기기.',
|
||||
'모니터': '컴퓨터 화면을 출력해 보여주는 디스플레이.',
|
||||
'마우스': '손으로 쥐고 화면의 커서를 움직이는 장치.',
|
||||
'키보드': '문자와 숫자를 입력하는 자판 기기.',
|
||||
'웹툰': '인터넷에서 즐기는 디지털 만화 콘텐츠.',
|
||||
'게임': '즐겁게 규칙을 따라 즐기는 놀이 문화.',
|
||||
|
||||
# Famous Brands & Companies
|
||||
'삼성': '대한민국을 대표하는 글로벌 전자제품 기업.',
|
||||
'현대': '대한민국의 대표적인 자동차 제작 기업.',
|
||||
'스타벅스': '세계적인 프리미엄 커피 전문점 브랜드.',
|
||||
'나이키': '스포츠웨어와 운동화로 유명한 브랜드.',
|
||||
'아디다스': '삼선 디자인으로 잘 알려진 스포츠 브랜드.',
|
||||
'맥도날드': '세계에서 가장 유명한 패스트푸드 브랜드.',
|
||||
'이케아': '홈 인테리어와 가구를 판매하는 글로벌 매장.',
|
||||
'루이비통': '프랑스의 세계적인 명품 패션 브랜드.',
|
||||
'샤넬': '명품 향수와 패션으로 유명한 프랑스 브랜드.',
|
||||
'소니': '플레이스테이션으로 유명한 전자기기 기업.',
|
||||
'닌텐도': '마리오와 비디오 게임으로 인기 있는 기업.',
|
||||
'디즈니': '애니메이션과 테마파크로 유명한 기업.',
|
||||
'케이에프씨': '프라이드치킨으로 세계적인 전문 브랜드.',
|
||||
'아메리카노': '에스프레소에 물을 섞은 시원한 커피.',
|
||||
'에스프레소': '진하게 추출한 원두커피 원액.',
|
||||
'피자': '치즈와 토핑을 얹어 구워낸 이탈리아 요리.',
|
||||
'치킨': '기름에 노릇하게 튀겨낸 인기 닭요리.',
|
||||
'자장면': '춘장에 면을 비벼 먹는 대표 중식 요리.',
|
||||
'탕수육': '돼지고기에 튀김옷을 입혀 소스를 얹은 요리.',
|
||||
'지하철': '도시 지하에서 시민들을 수송하는 전동차.',
|
||||
'비행기': '하늘을 날아 빠르게 이동하는 항공기.',
|
||||
'버스': '많은 승객을 태우고 도로를 달리는 대형차.',
|
||||
'택시': '요금을 내고 원하는 목적지까지 타고 가는 차.',
|
||||
'자전거': '두 바퀴를 발로 페달을 밟아 달리는 탈것.',
|
||||
'아파트': '많은 가구가 모여 사는 공동 주택 건물.',
|
||||
'편의점': '24시간 생필품과 간식을 파는 소형 매장.',
|
||||
'백화점': '다양한 상품과 브랜드가 모여 있는 대형 쇼핑몰.',
|
||||
|
||||
# Everyday Words & Life
|
||||
'학교': '학생들이 모여 선생님께 지식을 배우는 장소.',
|
||||
'교실': '학교에서 학생들이 수업을 듣는 공간.',
|
||||
'선생님': '학생들을 친절하게 가르치고 지도하는 분.',
|
||||
'학생': '학교에서 배움을 얻는 아이나 청소년.',
|
||||
'친구': '가깝게 친하게 지내는 또래 벗.',
|
||||
'가족': '부모와 자식 등 사랑으로 연결된 식구.',
|
||||
'사진': '카메라를 이용해 순간을 기록한 영상.',
|
||||
'가족사진': '온 가족이 모여 촬영한 기념 사진.',
|
||||
'과일': '나무나 식물에서 열리는 달콤한 열매.',
|
||||
'사과': '가을에 빨갛게 익는 달콤한 과일.',
|
||||
'딸기': '봄철에 붉고 상큼하게 익는 대표 과일.',
|
||||
'바나나': '껍질을 까서 먹는 노랗고 부드러운 과일.',
|
||||
'수박': '여름철 시원하고 달콤한 커다란 과일.',
|
||||
'우유': '신선하고 영양가 높은 하얀 유제품 음료.',
|
||||
'과자': '바삭바삭하게 구워 만든 주전부리 간식.',
|
||||
'빵': '밀가루를 오븐에 구워 만든 맛있는 음식.',
|
||||
'수영': '물속에서 헤엄치며 즐기는 건강 운동.',
|
||||
'수영장': '물놀이와 수영을 즐길 수 있는 시설.',
|
||||
'바다': '지구의 대부분을 덮고 있는 넓고 짠 물.',
|
||||
'여행': '새로운 풍경을 찾아 다른 고장으로 떠나는 일.',
|
||||
'구름': '하늘 높은 곳에 둥둥 떠 있는 수증기.',
|
||||
'하늘': '지구 위를 감싸고 있는 높고 푸른 공간.',
|
||||
'무지개': '비가 갠 후 하늘에 드는 일곱 빛깔 다리.',
|
||||
'개나리': '봄이 되면 노랗게 피어나는 봄꽃.',
|
||||
'장미': '향기가 좋고 가시가 있는 아름다운 꽃.',
|
||||
'공원': '시민들이 산책하며 휴식을 취하는 녹지.',
|
||||
'도서관': '수많은 책을 읽거나 대여하는 도서 시설.',
|
||||
'경찰': '사회의 질서와 안전을 지키는 사람.',
|
||||
'의사': '환자의 병을 진찰하고 치료해 주는 전문가.',
|
||||
'병원': '아픈 환자를 진찰하고 수술하는 의료 시설.',
|
||||
'약국': '약사가 의약품을 처방하고 판매하는 곳.',
|
||||
'시장': '상인들이 여러 물품을 사고파는 전통 장소.',
|
||||
'마트': '다양한 식료품과 생활용품을 파는 매장.',
|
||||
'선물': '남에게 정성을 담아 안겨주는 물건.',
|
||||
'생일': '태어난 날을 축하하는 뜻깊은 날.',
|
||||
'음악': '소리의 아름다움을 가다듬어 표현하는 예술.',
|
||||
'피아노': '건반을 눌러 아름다운 소리를 내는 악기.',
|
||||
'기타': '줄을 튕겨 아름다운 음률을 내는 현악기.',
|
||||
'축구': '공을 차서 상대 골대에 넣는 스포츠.',
|
||||
'야구': '배트로 공을 치고 루를 돌아 득점하는 경기.',
|
||||
'농구': '공을 튀겨 상대 림에 넣는 인기 스포츠.',
|
||||
'호랑이': '줄무늬가 있는 용맹한 맹수 동물의 왕.',
|
||||
'사자': '갈기가 풍성하고 위엄 있는 밀림의 왕.',
|
||||
'코끼리': '긴 코와 큰 귀를 가진 거대한 동물.',
|
||||
'고양이': '귀엽고 유연하며 야옹 소리를 내는 동물.',
|
||||
'강아지': '사람과 가장 친근하게 지내는 반려 동반자.',
|
||||
'지혜': '슬기롭게 사리를 판단하고 해결하는 능력.',
|
||||
'소망': '간절히 이루어지기를 바라는 기대와 희망.',
|
||||
'행복': '기쁨과 만족을 느끼는 따뜻한 마음 상태.',
|
||||
'사랑': '남을 아끼고 소중히 여기는 마음.',
|
||||
'평화': '전쟁이나 다툼 없이 온화하고 평온함.',
|
||||
'자유': '남에게 속박되지 않고 스스로 행동함.',
|
||||
'기차': '철도 위를 달리는 대표적인 교통수단.',
|
||||
'차량': '모든 종류의 자동차와 기차 등을 통틀어 이르는 말.',
|
||||
'가수': '노래를 전문으로 부르는 직업.',
|
||||
'슬기': '사리를 밝히고 지혜롭게 처리하는 능력.',
|
||||
'룸바': '정열적인 쿠바의 전통 라틴 댄스.',
|
||||
|
||||
# Very Famous Idioms
|
||||
'일석이조': '한 가지 일로 두 가지 이득을 얻음.',
|
||||
'작심삼일': '결심이 사흘을 넘기지 못함.',
|
||||
'고진감래': '쓴 고생 끝에 달콤한 즐거움이 옴.',
|
||||
'동문서답': '질문과 전혀 다른 엉뚱한 대답.',
|
||||
'대기만성': '큰 인물은 늦게 성취됨.',
|
||||
'남녀노소': '모든 성별과 연령의 사람들.',
|
||||
'과유불급': '지나침은 모자람과 같음.',
|
||||
'역지사지': '상대방의 입장에서 생각해 봄.',
|
||||
'심사숙고': '깊이 생각하고 신중히 기함.',
|
||||
'유유자적': '한가롭게 마음 편히 자연을 즐김.',
|
||||
'일취월장': '실력이 나날이 빠르게 발전함.',
|
||||
'학수고대': '간절한 마음으로 기다림.',
|
||||
'기사회생': '위기 속에서 극적으로 살아남.',
|
||||
'자수성가': '스스로 노력하여 크게 성공함.',
|
||||
'점입가경': '갈수록 더욱 재미있어짐.',
|
||||
'화룡점정': '마지막 완성으로 화룡점정을 찍음.',
|
||||
'이구동성': '모든 사람의 입에서 같은 말이 나옴.',
|
||||
'이심전심': '말하지 않고 마음으로 뜻이 통함.',
|
||||
'전화위복': '불행이 도리어 좋은 복이 됨.',
|
||||
'동고동락': '괴로움과 즐거움을 함께 나눔.',
|
||||
'상부상조': '서로 돕고 협력하는 일.'
|
||||
}
|
||||
|
||||
def validate_stage(stage):
|
||||
grid_size = stage['gridSize']
|
||||
clues = stage['clues']
|
||||
grid = {}
|
||||
|
||||
if len(clues) < 3:
|
||||
return False, f"Stage {stage.get('stageNumber')}: Too few clues ({len(clues)})"
|
||||
|
||||
word_connections = {c['id']: set() for c in clues}
|
||||
|
||||
for clue in clues:
|
||||
word = clue['word']
|
||||
length = clue['length']
|
||||
r = clue['startRow']
|
||||
c = clue['startCol']
|
||||
direction = clue['direction']
|
||||
|
||||
if len(word) != length:
|
||||
return False, f"Stage {stage.get('stageNumber')} Clue {clue['id']}: word '{word}' len {len(word)} != length {length}"
|
||||
if length < 2:
|
||||
return False, f"Stage {stage.get('stageNumber')} Clue {clue['id']}: 1-letter word '{word}' not allowed"
|
||||
|
||||
for i, ch in enumerate(word):
|
||||
curr_r = r + (i if direction == 'DOWN' else 0)
|
||||
curr_c = c + (i if direction == 'ACROSS' else 0)
|
||||
|
||||
if curr_r < 0 or curr_r >= grid_size or curr_c < 0 or curr_c >= grid_size:
|
||||
return False, f"Stage {stage.get('stageNumber')} Clue {clue['id']}: '{word}' char '{ch}' at ({curr_r},{curr_c}) out of bounds ({grid_size}x{grid_size})"
|
||||
|
||||
if (curr_r, curr_c) in grid:
|
||||
existing_ch, existing_id = grid[(curr_r, curr_c)]
|
||||
if existing_ch != ch:
|
||||
return False, f"Stage {stage.get('stageNumber')} CLASH at ({curr_r}, {curr_c}): existing '{existing_ch}' ({existing_id}) vs '{ch}' in {clue['id']} ('{word}')"
|
||||
word_connections[clue['id']].add(existing_id)
|
||||
word_connections[existing_id].add(clue['id'])
|
||||
else:
|
||||
grid[(curr_r, curr_c)] = (ch, clue['id'])
|
||||
|
||||
for cid, conns in word_connections.items():
|
||||
if not conns:
|
||||
return False, f"Stage {stage.get('stageNumber')} Clue {cid} has no intersections!"
|
||||
|
||||
return True, "OK"
|
||||
|
||||
def center_stage_in_grid(s):
|
||||
clues = s['clues']
|
||||
grid_size = s['gridSize']
|
||||
if not clues: return
|
||||
|
||||
min_r = min(c['startRow'] for c in clues)
|
||||
max_r = max(c['startRow'] + (c['length'] - 1 if c['direction'] == 'DOWN' else 0) for c in clues)
|
||||
min_c = min(c['startCol'] for c in clues)
|
||||
max_c = max(c['startCol'] + (c['length'] - 1 if c['direction'] == 'ACROSS' else 0) for c in clues)
|
||||
|
||||
bbox_h = max_r - min_r + 1
|
||||
bbox_w = max_c - min_c + 1
|
||||
|
||||
offset_r = (grid_size - bbox_h) // 2 - min_r
|
||||
offset_c = (grid_size - bbox_w) // 2 - min_c
|
||||
|
||||
for c in clues:
|
||||
c['startRow'] += offset_r
|
||||
c['startCol'] += offset_c
|
||||
|
||||
def clean_stage_clues(s):
|
||||
clues = s['clues']
|
||||
|
||||
# Remove obscure blacklisted words
|
||||
clues = [c for c in clues if c['word'] not in OBSCURE_BLACKLIST]
|
||||
|
||||
# 1. Eliminate same-direction overlapping clues
|
||||
cell_dir_map = {}
|
||||
for c in clues:
|
||||
d = c['direction']
|
||||
w = c['word']
|
||||
sr = c['startRow']
|
||||
sc = c['startCol']
|
||||
for i in range(len(w)):
|
||||
r = sr + (i if d == 'DOWN' else 0)
|
||||
col = sc + (i if d == 'ACROSS' else 0)
|
||||
key = (r, col, d)
|
||||
if key not in cell_dir_map:
|
||||
cell_dir_map[key] = []
|
||||
cell_dir_map[key].append(c)
|
||||
|
||||
to_remove = set()
|
||||
for key, clue_list in cell_dir_map.items():
|
||||
if len(clue_list) > 1:
|
||||
clue_list.sort(key=lambda x: len(x['word']), reverse=True)
|
||||
for shorter in clue_list[1:]:
|
||||
to_remove.add(shorter['id'])
|
||||
|
||||
clues = [c for c in clues if c['id'] not in to_remove]
|
||||
|
||||
# 2. Eliminate substring duplicate words where one is contained in another
|
||||
sub_remove = set()
|
||||
for i, c1 in enumerate(clues):
|
||||
w1 = c1['word']
|
||||
for j, c2 in enumerate(clues):
|
||||
if i != j and c2['id'] not in sub_remove:
|
||||
w2 = c2['word']
|
||||
if len(w1) < len(w2) and w1 in w2:
|
||||
sub_remove.add(c1['id'])
|
||||
break
|
||||
|
||||
clues = [c for c in clues if c['id'] not in sub_remove]
|
||||
|
||||
# 3. Prune isolated clues
|
||||
while True:
|
||||
connections = {c['id']: 0 for c in clues}
|
||||
grid = {}
|
||||
for c in clues:
|
||||
d = c['direction']
|
||||
w = c['word']
|
||||
sr = c['startRow']
|
||||
sc = c['startCol']
|
||||
for i in range(len(w)):
|
||||
r = sr + (i if d == 'DOWN' else 0)
|
||||
col = sc + (i if d == 'ACROSS' else 0)
|
||||
if (r, col) in grid:
|
||||
other_id = grid[(r, col)]
|
||||
connections[c['id']] += 1
|
||||
connections[other_id] += 1
|
||||
else:
|
||||
grid[(r, col)] = c['id']
|
||||
|
||||
isolated = [cid for cid, cnt in connections.items() if cnt == 0]
|
||||
if not isolated:
|
||||
break
|
||||
clues = [c for c in clues if c['id'] not in isolated]
|
||||
|
||||
s['clues'] = clues
|
||||
|
||||
def format_stage(s, stage_num, title):
|
||||
s['stageNumber'] = stage_num
|
||||
s['title'] = title
|
||||
clues = s['clues']
|
||||
|
||||
clues.sort(key=lambda c: (c['startRow'], c['startCol'], c['direction']))
|
||||
for idx, c in enumerate(clues, 1):
|
||||
c['id'] = f"c{stage_num}_{idx}"
|
||||
c['number'] = idx
|
||||
c['length'] = len(c['word'])
|
||||
|
||||
center_stage_in_grid(s)
|
||||
|
||||
def can_place_word(grid, cell_dir_map, grid_size, word, sr, sc, direction):
|
||||
length = len(word)
|
||||
is_across = (direction == 'ACROSS')
|
||||
|
||||
if is_across:
|
||||
if sr < 0 or sr >= grid_size or sc < 0 or (sc + length) > grid_size:
|
||||
return False
|
||||
if sc - 1 >= 0 and (sr, sc - 1) in grid:
|
||||
return False
|
||||
if sc + length < grid_size and (sr, sc + length) in grid:
|
||||
return False
|
||||
else:
|
||||
if sc < 0 or sc >= grid_size or sr < 0 or (sr + length) > grid_size:
|
||||
return False
|
||||
if sr - 1 >= 0 and (sr - 1, sc) in grid:
|
||||
return False
|
||||
if sr + length < grid_size and (sr + length, sc) in grid:
|
||||
return False
|
||||
|
||||
intersection_count = 0
|
||||
for i in range(length):
|
||||
r = sr if is_across else sr + i
|
||||
c = sc + i if is_across else sc
|
||||
ch = word[i]
|
||||
|
||||
if (r, c, direction) in cell_dir_map:
|
||||
return False
|
||||
|
||||
if (r, c) in grid:
|
||||
if grid[(r, c)] != ch:
|
||||
return False
|
||||
intersection_count += 1
|
||||
else:
|
||||
if is_across:
|
||||
if (r - 1, c) in grid or (r + 1, c) in grid:
|
||||
return False
|
||||
else:
|
||||
if (r, c - 1) in grid or (r, c + 1) in grid:
|
||||
return False
|
||||
|
||||
return intersection_count >= 1
|
||||
|
||||
def enrich_stage_with_popular_words(s, target_clues, char_to_words, word_dict):
|
||||
clues = s['clues']
|
||||
grid_size = s['gridSize']
|
||||
grid = {}
|
||||
cell_dir_map = set()
|
||||
used_words = set(c['word'] for c in clues)
|
||||
|
||||
for c in clues:
|
||||
w = c['word']
|
||||
d = c['direction']
|
||||
sr = c['startRow']
|
||||
sc = c['startCol']
|
||||
for i, ch in enumerate(w):
|
||||
r = sr + (i if d == 'DOWN' else 0)
|
||||
col = sc + (i if d == 'ACROSS' else 0)
|
||||
grid[(r, col)] = ch
|
||||
cell_dir_map.add((r, col, d))
|
||||
|
||||
for p in range(15):
|
||||
if len(clues) >= target_clues:
|
||||
break
|
||||
placed_cells = list(grid.keys())
|
||||
random.shuffle(placed_cells)
|
||||
placed_in_pass = False
|
||||
|
||||
for (r, c) in placed_cells:
|
||||
ch = grid[(r, c)]
|
||||
candidates = char_to_words.get(ch, [])
|
||||
random.shuffle(candidates)
|
||||
|
||||
for (w, m, idx) in candidates:
|
||||
if w in used_words or w in OBSCURE_BLACKLIST:
|
||||
continue
|
||||
if any(w in existing or existing in w for existing in used_words):
|
||||
continue
|
||||
|
||||
# Try ACROSS
|
||||
sr_a = r
|
||||
sc_a = c - idx
|
||||
if can_place_word(grid, cell_dir_map, grid_size, w, sr_a, sc_a, 'ACROSS'):
|
||||
clues.append({
|
||||
'id': f"temp_{len(clues)+1}",
|
||||
'number': len(clues) + 1,
|
||||
'direction': 'ACROSS',
|
||||
'word': w,
|
||||
'clue': m,
|
||||
'startRow': sr_a,
|
||||
'startCol': sc_a,
|
||||
'length': len(w)
|
||||
})
|
||||
used_words.add(w)
|
||||
for i_w, ch_w in enumerate(w):
|
||||
grid[(sr_a, sc_a + i_w)] = ch_w
|
||||
cell_dir_map.add((sr_a, sc_a + i_w, 'ACROSS'))
|
||||
placed_in_pass = True
|
||||
if len(clues) >= target_clues:
|
||||
break
|
||||
|
||||
# Try DOWN
|
||||
sr_d = r - idx
|
||||
sc_d = c
|
||||
if can_place_word(grid, cell_dir_map, grid_size, w, sr_d, sc_d, 'DOWN'):
|
||||
clues.append({
|
||||
'id': f"temp_{len(clues)+1}",
|
||||
'number': len(clues) + 1,
|
||||
'direction': 'DOWN',
|
||||
'word': w,
|
||||
'clue': m,
|
||||
'startRow': sr_d,
|
||||
'startCol': sc_d,
|
||||
'length': len(w)
|
||||
})
|
||||
used_words.add(w)
|
||||
for i_w, ch_w in enumerate(w):
|
||||
grid[(sr_d + i_w, sc_d)] = ch_w
|
||||
cell_dir_map.add((sr_d + i_w, sc_d, 'DOWN'))
|
||||
placed_in_pass = True
|
||||
if len(clues) >= target_clues:
|
||||
break
|
||||
|
||||
if len(clues) >= target_clues:
|
||||
break
|
||||
if not placed_in_pass:
|
||||
break
|
||||
|
||||
def generate_popular_stage(stage_num, title, grid_size, target_words, char_to_words, popular_dict, used_hashes):
|
||||
words_list = list(popular_dict.keys())
|
||||
random.shuffle(words_list)
|
||||
|
||||
for seed in words_list:
|
||||
if seed in OBSCURE_BLACKLIST or len(seed) < 2:
|
||||
continue
|
||||
|
||||
grid = {}
|
||||
cell_dir_map = set()
|
||||
clues = []
|
||||
stage_words = set()
|
||||
|
||||
sr = grid_size // 2
|
||||
sc = (grid_size - len(seed)) // 2
|
||||
clues.append({
|
||||
'id': 'temp_1',
|
||||
'number': 1,
|
||||
'direction': 'ACROSS',
|
||||
'word': seed,
|
||||
'clue': popular_dict[seed],
|
||||
'startRow': sr,
|
||||
'startCol': sc,
|
||||
'length': len(seed)
|
||||
})
|
||||
stage_words.add(seed)
|
||||
|
||||
for i, ch in enumerate(seed):
|
||||
grid[(sr, sc + i)] = ch
|
||||
cell_dir_map.add((sr, sc + i, 'ACROSS'))
|
||||
|
||||
for pass_num in range(25):
|
||||
if len(clues) >= target_words:
|
||||
break
|
||||
|
||||
placed_in_pass = False
|
||||
placed_cells = list(grid.keys())
|
||||
random.shuffle(placed_cells)
|
||||
|
||||
for (r, c) in placed_cells:
|
||||
ch = grid[(r, c)]
|
||||
candidates = char_to_words.get(ch, [])
|
||||
random.shuffle(candidates)
|
||||
|
||||
for (w, m, idx) in candidates:
|
||||
if w in stage_words or w in OBSCURE_BLACKLIST:
|
||||
continue
|
||||
if any(w in existing or existing in w for existing in stage_words):
|
||||
continue
|
||||
|
||||
# Try ACROSS
|
||||
sr_a = r
|
||||
sc_a = c - idx
|
||||
if can_place_word(grid, cell_dir_map, grid_size, w, sr_a, sc_a, 'ACROSS'):
|
||||
clues.append({
|
||||
'id': f"temp_{len(clues)+1}",
|
||||
'number': len(clues) + 1,
|
||||
'direction': 'ACROSS',
|
||||
'word': w,
|
||||
'clue': m,
|
||||
'startRow': sr_a,
|
||||
'startCol': sc_a,
|
||||
'length': len(w)
|
||||
})
|
||||
stage_words.add(w)
|
||||
for i_w, ch_w in enumerate(w):
|
||||
grid[(sr_a, sc_a + i_w)] = ch_w
|
||||
cell_dir_map.add((sr_a, sc_a + i_w, 'ACROSS'))
|
||||
placed_in_pass = True
|
||||
if len(clues) >= target_words:
|
||||
break
|
||||
|
||||
# Try DOWN
|
||||
sr_d = r - idx
|
||||
sc_d = c
|
||||
if can_place_word(grid, cell_dir_map, grid_size, w, sr_d, sc_d, 'DOWN'):
|
||||
clues.append({
|
||||
'id': f"temp_{len(clues)+1}",
|
||||
'number': len(clues) + 1,
|
||||
'direction': 'DOWN',
|
||||
'word': w,
|
||||
'clue': m,
|
||||
'startRow': sr_d,
|
||||
'startCol': sc_d,
|
||||
'length': len(w)
|
||||
})
|
||||
stage_words.add(w)
|
||||
for i_w, ch_w in enumerate(w):
|
||||
grid[(sr_d + i_w, sc_d)] = ch_w
|
||||
cell_dir_map.add((sr_d + i_w, sc_d, 'DOWN'))
|
||||
placed_in_pass = True
|
||||
if len(clues) >= target_words:
|
||||
break
|
||||
|
||||
if len(clues) >= target_words:
|
||||
break
|
||||
|
||||
if not placed_in_pass:
|
||||
break
|
||||
|
||||
if len(clues) >= 4:
|
||||
new_stage = {
|
||||
'stageNumber': stage_num,
|
||||
'title': title,
|
||||
'gridSize': grid_size,
|
||||
'difficulty': 1 if stage_num <= 40 else (2 if stage_num <= 160 else 3),
|
||||
'clues': clues
|
||||
}
|
||||
clean_stage_clues(new_stage)
|
||||
format_stage(new_stage, stage_num, title)
|
||||
valid, _ = validate_stage(new_stage)
|
||||
if valid:
|
||||
w_hash = tuple(sorted([c['word'] for c in new_stage['clues']]))
|
||||
if w_hash not in used_hashes:
|
||||
return new_stage
|
||||
|
||||
return None
|
||||
|
||||
def main():
|
||||
random.seed(12345)
|
||||
|
||||
word_dict = dict(POPULAR_DICTIONARY)
|
||||
|
||||
with open('tools/stages_sheet_import.csv', 'r', encoding='utf-8') as f:
|
||||
for row in csv.reader(f):
|
||||
if len(row) >= 9 and row[7] and row[8]:
|
||||
w = row[7].strip()
|
||||
m = row[8].strip()
|
||||
if 2 <= len(w) <= 5 and w not in OBSCURE_BLACKLIST and all('\uac00' <= ch <= '\ud7a3' for ch in w):
|
||||
if w not in word_dict:
|
||||
word_dict[w] = m
|
||||
|
||||
char_to_words = defaultdict(list)
|
||||
for w, m in word_dict.items():
|
||||
for idx, ch in enumerate(w):
|
||||
char_to_words[ch].append((w, m, idx))
|
||||
|
||||
print(f"Popular Word Dictionary size: {len(word_dict)}")
|
||||
|
||||
with open('app/src/main/assets/data/stages.json', 'r', encoding='utf-8') as f:
|
||||
all_existing_stages = json.load(f)
|
||||
|
||||
base_stages = []
|
||||
seen_titles = set()
|
||||
for s in all_existing_stages:
|
||||
title = s['title']
|
||||
title_core = title.split('. ', 1)[-1].split(' I')[0].split(' II')[0].strip()
|
||||
if title_core not in seen_titles:
|
||||
seen_titles.add(title_core)
|
||||
base_stages.append(s)
|
||||
|
||||
print(f"Selected {len(base_stages)} base stage topics.")
|
||||
|
||||
final_stages = []
|
||||
used_hashes = set()
|
||||
|
||||
current_num = 1
|
||||
|
||||
# Generate exactly 280 stages (4 stages per topic for 70 topics)
|
||||
for topic_idx in range(70):
|
||||
base_orig = copy.deepcopy(base_stages[topic_idx % len(base_stages)])
|
||||
raw_title = base_orig['title']
|
||||
title_parts = raw_title.split('. ', 1)
|
||||
base_title_name = title_parts[1] if len(title_parts) > 1 else raw_title
|
||||
base_title_name = base_title_name.split(' I')[0].split(' II')[0].strip()
|
||||
|
||||
# Stage 1: Base stage (cleaned & popularized)
|
||||
s1 = copy.deepcopy(base_orig)
|
||||
clean_stage_clues(s1)
|
||||
if len(s1['clues']) < 5:
|
||||
enrich_stage_with_popular_words(s1, 7, char_to_words, word_dict)
|
||||
clean_stage_clues(s1)
|
||||
|
||||
valid_s1, _ = validate_stage(s1)
|
||||
if not valid_s1:
|
||||
s1 = generate_popular_stage(current_num, f"{current_num}. {base_title_name} I", 7, 7, char_to_words, word_dict, used_hashes)
|
||||
|
||||
format_stage(s1, current_num, f"{current_num}. {base_title_name} I")
|
||||
w_hash = tuple(sorted([c['word'] for c in s1['clues']]))
|
||||
used_hashes.add(w_hash)
|
||||
final_stages.append(s1)
|
||||
current_num += 1
|
||||
|
||||
# Stages 2, 3, 4: Popular modern stages
|
||||
suffixes = ["II", "III", "IV"]
|
||||
for suf in suffixes:
|
||||
grid_sz = 7 if current_num <= 80 else 8
|
||||
target_w = 7 if current_num <= 40 else (9 if current_num <= 120 else 11)
|
||||
title = f"{current_num}. {base_title_name} {suf}"
|
||||
|
||||
gen_s = None
|
||||
for tw in [target_w, target_w-1, target_w+1, 6, 5]:
|
||||
gen_s = generate_popular_stage(current_num, title, grid_sz, tw, char_to_words, word_dict, used_hashes)
|
||||
if gen_s is not None:
|
||||
break
|
||||
|
||||
if gen_s is not None:
|
||||
w_hash = tuple(sorted([c['word'] for c in gen_s['clues']]))
|
||||
used_hashes.add(w_hash)
|
||||
final_stages.append(gen_s)
|
||||
current_num += 1
|
||||
|
||||
print(f"Generated {len(final_stages)} unique popular stages!")
|
||||
|
||||
for s in final_stages:
|
||||
valid, msg = validate_stage(s)
|
||||
if not valid:
|
||||
raise ValueError(f"Validation error in stage {s['stageNumber']}: {msg}")
|
||||
|
||||
print("ALL 280 STAGES PASSED VALIDATION! Writing to stages.json...")
|
||||
with open('app/src/main/assets/data/stages.json', 'w', encoding='utf-8') as f:
|
||||
json.dump(final_stages, f, ensure_ascii=False, indent=2)
|
||||
|
||||
print("Updated stages.json with popular vocabulary successfully!")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,49 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import json, csv, copy, random
|
||||
from collections import defaultdict
|
||||
|
||||
def validate_stage(stage):
|
||||
grid_size = stage['gridSize']
|
||||
clues = stage['clues']
|
||||
grid = {}
|
||||
|
||||
if len(clues) < 3:
|
||||
return False, f"Stage {stage.get('stageNumber')}: Too few clues ({len(clues)})"
|
||||
|
||||
word_connections = {c['id']: set() for c in clues}
|
||||
|
||||
for clue in clues:
|
||||
word = clue['word']
|
||||
length = clue['length']
|
||||
r = clue['startRow']
|
||||
c = clue['startCol']
|
||||
direction = clue['direction']
|
||||
|
||||
if len(word) != length:
|
||||
return False, f"Stage {stage.get('stageNumber')} Clue {clue['id']}: word '{word}' len {len(word)} != length {length}"
|
||||
if length < 2:
|
||||
return False, f"Stage {stage.get('stageNumber')} Clue {clue['id']}: 1-letter word '{word}' not allowed"
|
||||
|
||||
for i, ch in enumerate(word):
|
||||
curr_r = r + (i if direction == 'DOWN' else 0)
|
||||
curr_c = c + (i if direction == 'ACROSS' else 0)
|
||||
|
||||
if curr_r < 0 or curr_r >= grid_size or curr_c < 0 or curr_c >= grid_size:
|
||||
return False, f"Stage {stage.get('stageNumber')} Clue {clue['id']}: '{word}' char '{ch}' at ({curr_r}, {curr_c}) out of bounds ({grid_size}x{grid_size})"
|
||||
|
||||
if (curr_r, curr_c) in grid:
|
||||
existing_ch, existing_id = grid[(curr_r, curr_c)]
|
||||
if existing_ch != ch:
|
||||
return False, f"Stage {stage.get('stageNumber')} CLASH at ({curr_r}, {curr_c}): existing '{existing_ch}' ({existing_id}) vs '{ch}' in {clue['id']} ('{word}')"
|
||||
word_connections[clue['id']].add(existing_id)
|
||||
word_connections[existing_id].add(clue['id'])
|
||||
else:
|
||||
grid[(curr_r, curr_c)] = (ch, clue['id'])
|
||||
|
||||
for cid, conns in word_connections.items():
|
||||
if not conns:
|
||||
return False, f"Stage {stage.get('stageNumber')} Clue {cid} has no intersections!"
|
||||
|
||||
return True, "OK"
|
||||
|
||||
print("Test generator script ready.")
|
||||
Reference in New Issue
Block a user