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

597 lines
24 KiB
Python

"""
사주팔자 계산 모듈
천간, 지지, 오행, 십성, 십이운성, 신살, 공망, 지장간, 지지 상호작용
"""
from datetime import date, datetime
from typing import Optional
from calculator.solar_terms import get_solar_term_month_branch
# ============================================================
# 기본 상수
# ============================================================
HEAVENLY_STEMS = ['', '', '', '', '', '', '', '', '', '']
HEAVENLY_STEMS_KR = ['', '', '', '', '', '', '', '', '', '']
EARTHLY_BRANCHES = ['', '', '', '', '', '', '', '', '', '', '', '']
EARTHLY_BRANCHES_KR = ['', '', '', '', '', '', '', '', '', '', '', '']
FIVE_ELEMENTS: dict[str, str] = {
'': '', '': '',
'': '', '': '',
'': '', '': '',
'': '', '': '',
'': '', '': '',
'': '', '': '',
'': '', '': '',
'': '', '': '', '': '', '': '',
'': '', '': '',
'': '', '': '',
}
FIVE_ELEMENTS_KR = {'': '', '': '', '': '', '': '', '': ''}
TWELVE_FORTUNES = ['장생', '목욕', '관대', '건록', '제왕', '', '', '', '', '', '', '']
# 기준년: 1900 = 庚子년
BASE_YEAR = 1900
BASE_YEAR_STEM = 6 # 庚
BASE_YEAR_BRANCH = 0 # 子
# 기준일: 1900-01-01 = 丙寅일
BASE_DAY_STEM = 2 # 丙
BASE_DAY_BRANCH = 2 # 寅
# ============================================================
# 간지 계산
# ============================================================
def get_year_ganzi(year: int) -> dict:
year_diff = year - BASE_YEAR
stem_idx = (BASE_YEAR_STEM + year_diff) % 10
branch_idx = (BASE_YEAR_BRANCH + year_diff) % 12
return {
'stem': HEAVENLY_STEMS[stem_idx],
'branch': EARTHLY_BRANCHES[branch_idx],
'stemKr': HEAVENLY_STEMS_KR[stem_idx],
'branchKr': EARTHLY_BRANCHES_KR[branch_idx],
}
def get_month_ganzi(year: int, month: int, day: int) -> dict:
branch_idx = get_solar_term_month_branch(year, month, day)
year_stem = get_year_ganzi(year)['stem']
year_stem_idx = HEAVENLY_STEMS.index(year_stem)
stem_idx = (year_stem_idx * 2 + branch_idx) % 10
return {
'stem': HEAVENLY_STEMS[stem_idx],
'branch': EARTHLY_BRANCHES[branch_idx],
'stemKr': HEAVENLY_STEMS_KR[stem_idx],
'branchKr': EARTHLY_BRANCHES_KR[branch_idx],
}
def get_day_ganzi(year: int, month: int, day: int) -> dict:
base = date(1900, 1, 1)
target = date(year, month, day)
days_diff = (target - base).days
stem_idx = (BASE_DAY_STEM + days_diff) % 10
branch_idx = (BASE_DAY_BRANCH + days_diff) % 12
# 음수 처리 (1900년 이전)
if stem_idx < 0:
stem_idx += 10
if branch_idx < 0:
branch_idx += 12
return {
'stem': HEAVENLY_STEMS[stem_idx],
'branch': EARTHLY_BRANCHES[branch_idx],
'stemKr': HEAVENLY_STEMS_KR[stem_idx],
'branchKr': EARTHLY_BRANCHES_KR[branch_idx],
}
def get_hour_ganzi(day_stem: str, hour: int) -> dict:
if hour >= 23 or hour < 1:
branch_idx = 0 # 子
elif hour < 3:
branch_idx = 1 # 丑
elif hour < 5:
branch_idx = 2 # 寅
elif hour < 7:
branch_idx = 3 # 卯
elif hour < 9:
branch_idx = 4 # 辰
elif hour < 11:
branch_idx = 5 # 巳
elif hour < 13:
branch_idx = 6 # 午
elif hour < 15:
branch_idx = 7 # 未
elif hour < 17:
branch_idx = 8 # 申
elif hour < 19:
branch_idx = 9 # 酉
elif hour < 21:
branch_idx = 10 # 戌
else:
branch_idx = 11 # 亥
day_stem_idx = HEAVENLY_STEMS.index(day_stem)
stem_idx = (day_stem_idx * 2 + branch_idx) % 10
return {
'stem': HEAVENLY_STEMS[stem_idx],
'branch': EARTHLY_BRANCHES[branch_idx],
'stemKr': HEAVENLY_STEMS_KR[stem_idx],
'branchKr': EARTHLY_BRANCHES_KR[branch_idx],
}
# ============================================================
# 십성 계산
# ============================================================
_PRODUCE_MAP = {'': '', '': '', '': '', '': '', '': ''}
_OVERCOME_MAP = {'': '', '': '', '': '', '': '', '': ''}
def get_ten_god(day_stem: str, target_stem: str, is_yang: bool) -> str:
day_elem = FIVE_ELEMENTS.get(day_stem, '')
target_elem = FIVE_ELEMENTS.get(target_stem, '')
if day_elem == target_elem:
return '비견' if is_yang else '겁재'
if _PRODUCE_MAP.get(day_elem) == target_elem:
return '식신' if is_yang else '상관'
if _OVERCOME_MAP.get(day_elem) == target_elem:
return '편재' if is_yang else '정재'
if _OVERCOME_MAP.get(target_elem) == day_elem:
return '편관' if is_yang else '정관'
if _PRODUCE_MAP.get(target_elem) == day_elem:
return '편인' if is_yang else '정인'
return '비견'
# ============================================================
# 십이운성 계산
# ============================================================
_FORTUNE_MAP: dict[str, dict[str, int]] = {
'': {'': 11, '': 0, '': 1, '': 2, '': 3, '': 4, '': 5, '': 6, '': 7, '': 8, '': 9, '': 10},
'': {'': 11, '': 0, '': 1, '': 2, '': 3, '': 4, '': 5, '': 6, '': 7, '': 8, '': 9, '': 10},
'': {'': 11, '': 0, '': 1, '': 2, '': 3, '': 4, '': 5, '': 6, '': 7, '': 8, '': 9, '': 10},
'': {'': 11, '': 0, '': 1, '': 2, '': 3, '': 4, '': 5, '': 6, '': 7, '': 8, '': 9, '': 10},
'': {'': 11, '': 0, '': 1, '': 2, '': 3, '': 4, '': 5, '': 6, '': 7, '': 8, '': 9, '': 10},
'': {'': 11, '': 0, '': 1, '': 2, '': 3, '': 4, '': 5, '': 6, '': 7, '': 8, '': 9, '': 10},
'': {'': 11, '': 0, '': 1, '': 2, '': 3, '': 4, '': 5, '': 6, '': 7, '': 8, '': 9, '': 10},
'': {'': 11, '': 0, '': 1, '': 2, '': 3, '': 4, '': 5, '': 6, '': 7, '': 8, '': 9, '': 10},
'': {'': 11, '': 0, '': 1, '': 2, '': 3, '': 4, '': 5, '': 6, '': 7, '': 8, '': 9, '': 10},
'': {'': 11, '': 0, '': 1, '': 2, '': 3, '': 4, '': 5, '': 6, '': 7, '': 8, '': 9, '': 10},
}
def get_twelve_fortune(day_stem: str, branch: str) -> str:
idx = _FORTUNE_MAP.get(day_stem, {}).get(branch, 0)
return TWELVE_FORTUNES[idx]
# ============================================================
# 지장간 (藏干)
# ============================================================
HIDDEN_STEMS: dict[str, list[str]] = {
'': [''],
'': ['', '', ''],
'': ['', '', ''],
'': [''],
'': ['', '', ''],
'': ['', '', ''],
'': ['', ''],
'': ['', '', ''],
'': ['', '', ''],
'': [''],
'': ['', '', ''],
'': ['', ''],
}
_ROLE_NAMES = ['정기(본기)', '중기', '여기']
def get_hidden_stems(branch: str) -> list[str]:
return HIDDEN_STEMS.get(branch, [])
def get_all_hidden_stems(saju: dict) -> list[dict]:
pillars = [
{'pillar': '년주', 'branch': saju['year']['branch'], 'branchKr': saju['year']['branchKr']},
{'pillar': '월주', 'branch': saju['month']['branch'], 'branchKr': saju['month']['branchKr']},
{'pillar': '일주', 'branch': saju['day']['branch'], 'branchKr': saju['day']['branchKr']},
]
if saju.get('hour'):
pillars.append({'pillar': '시주', 'branch': saju['hour']['branch'], 'branchKr': saju['hour']['branchKr']})
result = []
for p in pillars:
hidden = get_hidden_stems(p['branch'])
stems_info = []
for idx, stem in enumerate(hidden):
stem_idx = HEAVENLY_STEMS.index(stem)
stems_info.append({
'stem': stem,
'stemKr': HEAVENLY_STEMS_KR[stem_idx],
'element': FIVE_ELEMENTS.get(stem, ''),
'role': _ROLE_NAMES[idx] if idx < len(_ROLE_NAMES) else '여기',
})
result.append({
'pillar': p['pillar'],
'branch': p['branch'],
'branchKr': p['branchKr'],
'stems': stems_info,
})
return result
# ============================================================
# 지지 상호작용
# ============================================================
_YUKAP_PAIRS = [
('', '', ''), ('', '', ''), ('', '', ''),
('', '', ''), ('', '', ''), ('', '', ''),
]
_SAMHAP_GROUPS = [
('', '', '', ''), ('', '', '', ''),
('', '', '', ''), ('', '', '', ''),
]
_BANGHAP_GROUPS = [
('', '', '', ''), ('', '', '', ''),
('', '', '', ''), ('', '', '', ''),
]
_CHUNG_PAIRS = [
('', ''), ('', ''), ('', ''),
('', ''), ('', ''), ('', ''),
]
_HYUNG_GROUPS = [
{'branches': ['', '', ''], 'name': '무은지형(無恩之刑)'},
{'branches': ['', '', ''], 'name': '지세지형(恃勢之刑)'},
{'branches': ['', ''], 'name': '무례지형(無禮之刑)'},
]
_JAHYUNG_BRANCHES = ['', '', '', '']
_PA_PAIRS = [
('', ''), ('', ''), ('', ''),
('', ''), ('', ''), ('', ''),
]
_HAE_PAIRS = [
('', ''), ('', ''), ('', ''),
('', ''), ('', ''), ('', ''),
]
_ELEM_KR = {'': '', '': '', '': '', '': '', '': ''}
def analyze_branch_interactions(saju: dict) -> list[dict]:
pillar_branches = [
{'branch': saju['year']['branch'], 'branchKr': saju['year']['branchKr'], 'pillar': '년주'},
{'branch': saju['month']['branch'], 'branchKr': saju['month']['branchKr'], 'pillar': '월주'},
{'branch': saju['day']['branch'], 'branchKr': saju['day']['branchKr'], 'pillar': '일주'},
]
if saju.get('hour'):
pillar_branches.append({'branch': saju['hour']['branch'], 'branchKr': saju['hour']['branchKr'], 'pillar': '시주'})
branches = [p['branch'] for p in pillar_branches]
interactions = []
# 육합
for a, b, elem in _YUKAP_PAIRS:
if a in branches and b in branches:
idx_a = branches.index(a)
idx_b = branches.index(b)
interactions.append({
'type': '육합(六合)',
'branches': [a, b],
'branchesKr': [pillar_branches[idx_a]['branchKr'], pillar_branches[idx_b]['branchKr']],
'pillars': [pillar_branches[idx_a]['pillar'], pillar_branches[idx_b]['pillar']],
'description': f"{pillar_branches[idx_a]['branchKr']}{pillar_branches[idx_b]['branchKr']} 육합 → {_ELEM_KR.get(elem, '')}({elem}) 기운 생성. 조화와 화합의 관계.",
'resultElement': elem,
})
# 삼합
for a, b, c, elem in _SAMHAP_GROUPS:
found = [x for x in [a, b, c] if x in branches]
if len(found) >= 2:
found_pillars = [pillar_branches[branches.index(x)] for x in found]
is_complete = len(found) == 3
interactions.append({
'type': '삼합(三合)' if is_complete else '반삼합(半三合)',
'branches': found,
'branchesKr': [p['branchKr'] for p in found_pillars],
'pillars': [p['pillar'] for p in found_pillars],
'description': f"{''.join(p['branchKr'] for p in found_pillars)} {'삼합' if is_complete else '반삼합'}{_ELEM_KR.get(elem, '')}({elem})국.",
'resultElement': elem,
})
# 방합
for a, b, c, elem in _BANGHAP_GROUPS:
found = [x for x in [a, b, c] if x in branches]
if len(found) == 3:
found_pillars = [pillar_branches[branches.index(x)] for x in found]
interactions.append({
'type': '방합(方合)',
'branches': found,
'branchesKr': [p['branchKr'] for p in found_pillars],
'pillars': [p['pillar'] for p in found_pillars],
'description': f"{''.join(p['branchKr'] for p in found_pillars)} 방합 → {_ELEM_KR.get(elem, '')}({elem}) 방국. 매우 강한 오행 기운.",
'resultElement': elem,
})
# 충
for a, b in _CHUNG_PAIRS:
if a in branches and b in branches:
idx_a = branches.index(a)
idx_b = branches.index(b)
interactions.append({
'type': '충(沖)',
'branches': [a, b],
'branchesKr': [pillar_branches[idx_a]['branchKr'], pillar_branches[idx_b]['branchKr']],
'pillars': [pillar_branches[idx_a]['pillar'], pillar_branches[idx_b]['pillar']],
'description': f"{pillar_branches[idx_a]['branchKr']}{pillar_branches[idx_b]['branchKr']} 충 → 변동, 갈등, 변화의 에너지.",
})
# 형
for group in _HYUNG_GROUPS:
found = [x for x in group['branches'] if x in branches]
if len(found) >= 2:
found_pillars = [pillar_branches[branches.index(x)] for x in found]
interactions.append({
'type': '형(刑)',
'branches': found,
'branchesKr': [p['branchKr'] for p in found_pillars],
'pillars': [p['pillar'] for p in found_pillars],
'description': f"{''.join(p['branchKr'] for p in found_pillars)} {group['name']} → 시련과 갈등의 기운.",
})
# 자형
for jb in _JAHYUNG_BRANCHES:
count = branches.count(jb)
if count >= 2:
br_kr = EARTHLY_BRANCHES_KR[EARTHLY_BRANCHES.index(jb)]
interactions.append({
'type': '자형(自刑)',
'branches': [jb, jb],
'branchesKr': [br_kr, br_kr],
'pillars': [p['pillar'] for p in pillar_branches if p['branch'] == jb],
'description': f'{br_kr}{br_kr} 자형 → 자기 자신과의 갈등, 내면의 갈등 기운.',
})
# 파
for a, b in _PA_PAIRS:
if a in branches and b in branches:
idx_a = branches.index(a)
idx_b = branches.index(b)
interactions.append({
'type': '파(破)',
'branches': [a, b],
'branchesKr': [pillar_branches[idx_a]['branchKr'], pillar_branches[idx_b]['branchKr']],
'pillars': [pillar_branches[idx_a]['pillar'], pillar_branches[idx_b]['pillar']],
'description': f"{pillar_branches[idx_a]['branchKr']}{pillar_branches[idx_b]['branchKr']} 파 → 관계의 균열, 계획의 차질 가능성.",
})
# 해
for a, b in _HAE_PAIRS:
if a in branches and b in branches:
idx_a = branches.index(a)
idx_b = branches.index(b)
interactions.append({
'type': '해(害)',
'branches': [a, b],
'branchesKr': [pillar_branches[idx_a]['branchKr'], pillar_branches[idx_b]['branchKr']],
'pillars': [pillar_branches[idx_a]['pillar'], pillar_branches[idx_b]['pillar']],
'description': f"{pillar_branches[idx_a]['branchKr']}{pillar_branches[idx_b]['branchKr']} 해 → 은근한 방해, 원망의 기운.",
})
return interactions
# ============================================================
# 신살 (神煞)
# ============================================================
_SAMHAP_GROUP_MAP: dict[str, str] = {
'': '申子辰', '': '申子辰', '': '申子辰',
'': '寅午戌', '': '寅午戌', '': '寅午戌',
'': '巳酉丑', '': '巳酉丑', '': '巳酉丑',
'': '亥卯未', '': '亥卯未', '': '亥卯未',
}
_YEOKMA_MAP = {'申子辰': '', '寅午戌': '', '巳酉丑': '', '亥卯未': ''}
_DOHWA_MAP = {'申子辰': '', '寅午戌': '', '巳酉丑': '', '亥卯未': ''}
_HWAGAE_MAP = {'申子辰': '', '寅午戌': '', '巳酉丑': '', '亥卯未': ''}
_CHEONUL_MAP: dict[str, list[str]] = {
'': ['', ''], '': ['', ''], '': ['', ''], '': ['', ''],
'': ['', ''], '': ['', ''], '': ['', ''], '': ['', ''],
'': ['', ''], '': ['', ''],
}
_MUNCHANG_MAP: dict[str, str] = {
'': '', '': '', '': '', '': '',
'': '', '': '', '': '', '': '',
'': '', '': '',
}
_CHEONDUK_MAP: dict[str, str] = {
'': '', '': '', '': '', '': '',
'': '', '': '', '': '', '': '',
'': '', '': '', '': '', '': '',
}
def calculate_shinsal(saju: dict) -> list[dict]:
result = []
day_branch = saju['day']['branch']
day_stem = saju['dayStem']
month_branch = saju['month']['branch']
pillar_branches = [
{'branch': saju['year']['branch'], 'branchKr': saju['year']['branchKr'], 'pillar': '년주'},
{'branch': saju['month']['branch'], 'branchKr': saju['month']['branchKr'], 'pillar': '월주'},
{'branch': saju['day']['branch'], 'branchKr': saju['day']['branchKr'], 'pillar': '일주'},
]
if saju.get('hour'):
pillar_branches.append({'branch': saju['hour']['branch'], 'branchKr': saju['hour']['branchKr'], 'pillar': '시주'})
group = _SAMHAP_GROUP_MAP.get(day_branch)
if group:
# 역마살
yeokma = _YEOKMA_MAP[group]
for pb in pillar_branches:
if pb['branch'] == yeokma and pb['pillar'] != '일주':
result.append({
'name': '역마살', 'nameHanja': '驛馬殺',
'branch': yeokma, 'branchKr': pb['branchKr'], 'pillar': pb['pillar'],
'description': '이동, 변동, 해외, 출장이 많은 기운. 활동적이고 한 곳에 머물지 못하는 성향.',
})
# 도화살
dohwa = _DOHWA_MAP[group]
for pb in pillar_branches:
if pb['branch'] == dohwa and pb['pillar'] != '일주':
result.append({
'name': '도화살', 'nameHanja': '桃花殺',
'branch': dohwa, 'branchKr': pb['branchKr'], 'pillar': pb['pillar'],
'description': '매력, 인기, 예술적 감각. 이성에게 끌리는 기운이 강하며 대인관계가 화려함.',
})
# 화개살
hwagae = _HWAGAE_MAP[group]
for pb in pillar_branches:
if pb['branch'] == hwagae and pb['pillar'] != '일주':
result.append({
'name': '화개살', 'nameHanja': '華蓋殺',
'branch': hwagae, 'branchKr': pb['branchKr'], 'pillar': pb['pillar'],
'description': '학문, 종교, 예술에 심취하는 기운. 고독을 즐기며 정신적 세계에 몰두하는 성향.',
})
# 천을귀인
cheonul_branches = _CHEONUL_MAP.get(day_stem, [])
for pb in pillar_branches:
if pb['branch'] in cheonul_branches and pb['pillar'] != '일주':
result.append({
'name': '천을귀인', 'nameHanja': '天乙貴人',
'branch': pb['branch'], 'branchKr': pb['branchKr'], 'pillar': pb['pillar'],
'description': '위기에서 귀인의 도움을 받는 길한 기운. 어려울 때 도움을 주는 사람이 나타남.',
})
# 문창귀인
munchang_branch = _MUNCHANG_MAP.get(day_stem)
if munchang_branch:
for pb in pillar_branches:
if pb['branch'] == munchang_branch and pb['pillar'] != '일주':
result.append({
'name': '문창귀인', 'nameHanja': '文昌貴人',
'branch': pb['branch'], 'branchKr': pb['branchKr'], 'pillar': pb['pillar'],
'description': '학문, 시험, 문서에 유리한 기운. 공부를 잘하며 시험운이 좋음.',
})
# 천덕귀인 (월지 기준, 천간에서 확인)
cheonduk_stem = _CHEONDUK_MAP.get(month_branch)
if cheonduk_stem:
all_stems = [
{'stem': saju['year']['stem'], 'pillar': '년주'},
{'stem': saju['day']['stem'], 'pillar': '일주'},
]
if saju.get('hour'):
all_stems.append({'stem': saju['hour']['stem'], 'pillar': '시주'})
for ps in all_stems:
if ps['stem'] == cheonduk_stem:
result.append({
'name': '천덕귀인', 'nameHanja': '天德貴人',
'branch': month_branch,
'branchKr': EARTHLY_BRANCHES_KR[EARTHLY_BRANCHES.index(month_branch)],
'pillar': ps['pillar'],
'description': '하늘의 덕을 받는 기운. 재난을 피하고 복을 받는 길신 중의 길신.',
})
return result
# ============================================================
# 공망 (空亡)
# ============================================================
def calculate_gongmang(day_stem: str, day_branch: str) -> dict:
stem_idx = HEAVENLY_STEMS.index(day_stem)
branch_idx = EARTHLY_BRANCHES.index(day_branch)
start_branch_idx = (branch_idx - stem_idx + 120) % 12
gm1 = (start_branch_idx + 10) % 12
gm2 = (start_branch_idx + 11) % 12
branch1 = EARTHLY_BRANCHES[gm1]
branch2 = EARTHLY_BRANCHES[gm2]
br_kr1 = EARTHLY_BRANCHES_KR[gm1]
br_kr2 = EARTHLY_BRANCHES_KR[gm2]
return {
'branches': [branch1, branch2],
'branchesKr': [br_kr1, br_kr2],
'description': f'{br_kr1}({branch1}{br_kr2}({branch2}) 공망 → 해당 지지의 기운이 비어있어 허무하거나 집착이 없는 영역. 오히려 초월적 능력이 될 수 있음.',
}
# ============================================================
# 사주팔자 전체 계산
# ============================================================
def calculate_saju(
year: int,
month: int,
day: int,
hour: Optional[int],
gender: str,
) -> dict:
year_ganzi = get_year_ganzi(year)
month_ganzi = get_month_ganzi(year, month, day)
day_ganzi = get_day_ganzi(year, month, day)
hour_ganzi = get_hour_ganzi(day_ganzi['stem'], hour) if hour is not None else None
day_stem = day_ganzi['stem']
day_stem_idx = HEAVENLY_STEMS.index(day_stem)
is_day_yang = day_stem_idx % 2 == 0
def enrich(ganzi: dict, is_day_pillar: bool = False) -> dict:
stem = ganzi['stem']
branch = ganzi['branch']
stem_idx = HEAVENLY_STEMS.index(stem)
is_yang = (stem_idx % 2 == 0) == is_day_yang
return {
**ganzi,
'element': FIVE_ELEMENTS.get(stem, ''),
'tenGod': '일간' if is_day_pillar else get_ten_god(day_stem, stem, is_yang),
'fortune': get_twelve_fortune(day_stem, branch),
}
saju: dict = {
'year': enrich(year_ganzi),
'month': enrich(month_ganzi),
'day': enrich(day_ganzi, is_day_pillar=True),
'dayStem': day_stem,
'birthDate': {'year': year, 'month': month, 'day': day, 'hour': hour},
'gender': gender,
}
if hour_ganzi:
saju['hour'] = enrich(hour_ganzi)
return saju