사주 기능 이식 & 로그인, 유저 페이지 Supabase 연동 & 토스 페이먼츠 결제 연동 & 사주 심층 분석을 위한 기능 분리
This commit is contained in:
267
saju-engine/calculator/lotto_generator.py
Normal file
267
saju-engine/calculator/lotto_generator.py
Normal file
@@ -0,0 +1,267 @@
|
||||
"""
|
||||
사주 기반 로또 번호 생성 모듈
|
||||
오행 균형, 천간/지지 고유 숫자, 신살 등을 반영
|
||||
"""
|
||||
|
||||
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
|
||||
Reference in New Issue
Block a user