# -*- 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()