268 lines
7.9 KiB
Python
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
|