Files
jaengseung-made/saju-engine/calculator/lotto_generator.py

268 lines
7.9 KiB
Python

"""
사주 기반 로또 번호 생성 모듈
오행 균형, 천간/지지 고유 숫자, 신살 등을 반영
"""
import hashlib
import random
from typing import Optional
from calculator.saju_calculator import FIVE_ELEMENTS
# 오행별 로또 번호 후보
_ELEMENT_NUMBERS: dict[str, list[int]] = {
'': [1, 2, 11, 12, 21, 22, 31, 32, 41, 42],
'': [3, 4, 13, 14, 23, 24, 33, 34, 43, 44],
'': [5, 6, 15, 16, 25, 26, 35, 36, 45],
'': [7, 8, 17, 18, 27, 28, 37, 38],
'': [9, 10, 19, 20, 29, 30, 39, 40],
}
# 천간 고유 숫자 (각 천간에 대응하는 행운 숫자)
_STEM_NUMBERS: dict[str, list[int]] = {
'': [1, 11, 21, 31, 41],
'': [2, 12, 22, 32, 42],
'': [3, 13, 23, 33, 43],
'': [4, 14, 24, 34, 44],
'': [5, 15, 25, 35, 45],
'': [6, 16, 26, 36],
'': [7, 17, 27, 37],
'': [8, 18, 28, 38],
'': [9, 19, 29, 39],
'': [10, 20, 30, 40],
}
# 지지 고유 숫자
_BRANCH_NUMBERS: dict[str, list[int]] = {
'': [9, 19, 29, 39],
'': [6, 15, 25, 36],
'': [1, 11, 31, 41],
'': [2, 12, 22, 42],
'': [5, 16, 26, 35],
'': [3, 14, 24, 43],
'': [4, 13, 23, 44],
'': [6, 16, 26, 45],
'': [7, 18, 27, 37],
'': [8, 17, 28, 38],
'': [5, 15, 25, 35],
'': [10, 20, 30, 40],
}
# 신살별 보너스 숫자
_SHINSAL_BONUS: dict[str, list[int]] = {
'역마살': [7, 17, 27, 37],
'도화살': [3, 13, 23, 33, 43],
'화개살': [11, 22, 33, 44],
'천을귀인': [1, 7, 14, 21, 28, 35, 42],
'문창귀인': [4, 16, 25, 36],
'천덕귀인': [6, 12, 24, 36],
}
def _seed_from_saju(saju: dict) -> str:
"""사주 데이터에서 결정론적 시드 생성"""
bd = saju.get('birthDate', {})
key = (
f"{bd.get('year')}-{bd.get('month')}-{bd.get('day')}-"
f"{bd.get('hour', 'X')}-{saju.get('gender', 'X')}"
)
return hashlib.sha256(key.encode()).hexdigest()
def _get_dominant_elements(saju: dict) -> list[str]:
"""사주에서 강한 오행 추출 (빈도 기준 정렬)"""
count: dict[str, int] = {'': 0, '': 0, '': 0, '': 0, '': 0}
pillars = ['year', 'month', 'day', 'hour']
for p in pillars:
pillar = saju.get(p)
if not pillar:
continue
for key in ['stem', 'branch']:
char = pillar.get(key, '')
elem = FIVE_ELEMENTS.get(char)
if elem:
count[elem] = count.get(elem, 0) + 1
return sorted(count, key=lambda e: count[e], reverse=True)
def generate_lotto_numbers(
saju: dict,
shinsal: Optional[list[dict]] = None,
count: int = 6,
) -> dict:
"""
사주 기반 로또 번호 생성
Returns:
{
'numbers': [int, ...], # 추천 번호 (오름차순)
'basis': str, # 생성 근거 설명
'elementBalance': dict, # 오행별 번호 분포
}
"""
seed_hex = _seed_from_saju(saju)
rng = random.Random(int(seed_hex, 16) % (2**32))
# 1. 후보 풀 구성 (우선순위 점수)
scores: dict[int, float] = {n: 0.0 for n in range(1, 46)}
# 오행 비중 (강한 오행 우선)
dominant_elements = _get_dominant_elements(saju)
for rank, elem in enumerate(dominant_elements):
weight = 5.0 - rank # 1위=5점, 2위=4점, ...
for n in _ELEMENT_NUMBERS.get(elem, []):
if n in scores:
scores[n] += weight
# 일간 비중
day_stem = saju.get('dayStem', '')
for n in _STEM_NUMBERS.get(day_stem, []):
if n in scores:
scores[n] += 4.0
# 일지 비중
day_branch = saju.get('day', {}).get('branch', '')
for n in _BRANCH_NUMBERS.get(day_branch, []):
if n in scores:
scores[n] += 3.0
# 월지 비중
month_branch = saju.get('month', {}).get('branch', '')
for n in _BRANCH_NUMBERS.get(month_branch, []):
if n in scores:
scores[n] += 2.0
# 신살 보너스
shinsal_names = []
if shinsal:
for s in shinsal:
name = s.get('name', '')
shinsal_names.append(name)
for n in _SHINSAL_BONUS.get(name, []):
if n in scores:
scores[n] += 2.5
# 2. 점수 기반 확률 가중 샘플링
numbers_pool = list(scores.keys())
weights = [scores[n] + 1.0 for n in numbers_pool] # 최소 1.0 보장
selected: list[int] = []
remaining_pool = list(zip(numbers_pool, weights))
while len(selected) < count and remaining_pool:
total = sum(w for _, w in remaining_pool)
pick = rng.uniform(0, total)
cumulative = 0
picked_n = None
for n, w in remaining_pool:
cumulative += w
if pick <= cumulative:
picked_n = n
break
if picked_n is None:
picked_n = remaining_pool[-1][0]
selected.append(picked_n)
remaining_pool = [(n, w) for n, w in remaining_pool if n != picked_n]
selected.sort()
# 3. 오행 분포 계산
def _number_to_element(n: int) -> str:
for elem, nums in _ELEMENT_NUMBERS.items():
if n in nums:
return elem
return ''
element_balance = {}
for n in selected:
elem = _number_to_element(n)
element_balance[elem] = element_balance.get(elem, [])
element_balance[elem].append(n)
# 4. 근거 설명 생성
basis_parts = [
f"일간 {saju.get('dayStem', '')}({day_stem}) 기반",
f"강한 오행: {', '.join(dominant_elements[:2])}",
]
if shinsal_names:
basis_parts.append(f"신살 반영: {', '.join(set(shinsal_names))}")
basis = ' / '.join(basis_parts)
return {
'numbers': selected,
'basis': basis,
'elementBalance': element_balance,
}
def generate_multiple_sets(
saju: dict,
shinsal: Optional[list[dict]] = None,
sets: int = 5,
) -> list[dict]:
"""여러 세트의 로또 번호 생성 (시드 변형)"""
results = []
seed_hex = _seed_from_saju(saju)
base_seed = int(seed_hex, 16) % (2**32)
for i in range(sets):
# 세트별 시드 변형
modified_saju = dict(saju)
modified_saju['_set_index'] = i # 내부 변형용
rng = random.Random(base_seed + i * 997)
dominant_elements = _get_dominant_elements(saju)
# 각 세트는 조금씩 다른 오행 강조
scores: dict[int, float] = {n: rng.random() * 2 for n in range(1, 46)}
elem_to_emphasize = dominant_elements[i % len(dominant_elements)]
for n in _ELEMENT_NUMBERS.get(elem_to_emphasize, []):
if n in scores:
scores[n] += 5.0
day_stem = saju.get('dayStem', '')
for n in _STEM_NUMBERS.get(day_stem, []):
if n in scores:
scores[n] += 3.0
if shinsal:
for s in shinsal:
for n in _SHINSAL_BONUS.get(s.get('name', ''), []):
if n in scores:
scores[n] += 2.0
pool = list(scores.keys())
weights = [scores[n] for n in pool]
selected: list[int] = []
remaining = list(zip(pool, weights))
while len(selected) < 6 and remaining:
total = sum(w for _, w in remaining)
pick = rng.uniform(0, total)
cumulative = 0.0
picked_n = None
for n, w in remaining:
cumulative += w
if pick <= cumulative:
picked_n = n
break
if picked_n is None:
picked_n = remaining[-1][0]
selected.append(picked_n)
remaining = [(n, w) for n, w in remaining if n != picked_n]
selected.sort()
results.append({
'set': i + 1,
'numbers': selected,
'emphasis': elem_to_emphasize,
})
return results