diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -5,264 +5,127 @@ import time import math # --- 페이지 설정 (스크립트 최상단) --- -st.set_page_config(layout="wide", page_title="격동의 현대사: 선택의 순간들") +st.set_page_config(layout="wide", page_title="진실을 찾아서: 현대사 취재기록") # --- 게임 설정 --- -QUARTERLY_ACTIONS_LIMIT = 1 # 각 턴(국면) 당 주요 결정 횟수 +ACTIONS_PER_TURN_LIMIT = 2 # 턴 당 취재 활동 횟수 제한 # --- 시나리오 설정 --- SCENARIOS = { "4.19_revolution": { - "display_name": "4.19 혁명 (1960)", + "display_name": "4.19 혁명 취재 (1960)", "start_year": 1960, - "initial_capital": 10, - "initial_approval": 30, - "initial_stability": 60, - "initial_resistance": 20, + "player_role": "신입 기자 (자유일보)", # 가상 언론사 + "initial_press_freedom": 40, # 언론 통제 상황 + "initial_scoop_points": 0, # 특종 점수 + "initial_reporter_safety": 70, # 기자 안전도 + "initial_public_trust": 50, # 언론사/기자 신뢰도 "vocab_level": "보통", - "max_turns": 8, # 3.15 ~ 4.26 - "player_role": "이승만 정부" + "max_turns": 10, # 취재 기간 }, "5.18_gwangju": { - "display_name": "5.18 광주 민주화운동 (1980)", + "display_name": "5.18 광주 현장 취재 (1980)", "start_year": 1980, - "initial_capital": 15, - "initial_approval": 25, - "initial_stability": 40, - "initial_resistance": 30, + "player_role": "지방 주재 기자 (민주신문)", + "initial_press_freedom": 20, # 극심한 통제 + "initial_scoop_points": 0, + "initial_reporter_safety": 50, + "initial_public_trust": 40, "vocab_level": "보통", - "max_turns": 10, # 5.15 ~ 5.27 - "player_role": "신군부 계엄사령부" + "max_turns": 12, }, "june_struggle": { - "display_name": "6월 민주 항쟁 (1987)", + "display_name": "6월 항쟁 동행 취재 (1987)", "start_year": 1987, - "initial_capital": 12, - "initial_approval": 35, - "initial_stability": 50, - "initial_resistance": 40, + "player_role": "사회부 기자 (시민일보)", + "initial_press_freedom": 30, + "initial_scoop_points": 0, + "initial_reporter_safety": 60, + "initial_public_trust": 45, "vocab_level": "보통", - "max_turns": 10, # 1월 ~ 6월 말 - "player_role": "전두환 정부" + "max_turns": 12, } } -# --- 난이도별 텍스트 저장소 (역사 시나리오에 맞게 대폭 수정) --- +# --- 텍스트 저장소 (기자 컨셉에 맞게 대폭 수정) --- ALL_TEXTS = { # --- 공통 UI --- - "game_title": {"보통": "📜 격동의 현대사: 선택의 순간들"}, - "scenario_select_title": {"보통": "📜 시나리오 선택"}, - "scenario_select_button": {"보통": "이 시대로 가기"}, - "dashboard_title": {"보통": "📊 정부 상황판"}, - "dashboard_term": {"보통": "{year}년 {turn}번째 국면"}, - "term_approval": {"보통": "정권 지지도"}, - "term_political_capital": {"보통": "정치적 자산"}, - "term_stability": {"보통": "국가 안정도"}, - "term_resistance": {"보통": "국민 저항도"}, - "historical_briefing_title": {"보통": "📜 역사 기록 브리핑"}, - "player_options_title": {"보통": "📝 대응 방안 선택"}, - "action_button_label": {"보통": "{action} ({cost} 정치자산)"}, - "action_cost_warning": {"보통": "정치적 자산이 부족합니다!"}, - "action_success_message": {"보통": "'{action}' 지시 완료."}, - "button_next_turn": {"보통": "➡️ 다음 국면으로"}, - "button_restart_scenario": {"보통": "시나리오 다시 시작"}, - "button_back_to_scenario_select": {"보통": "다른 시나리오 선택"}, - "game_over_title": {"보통": "역사의 한 페이지가 넘어갑니다"}, - "game_over_subtitle": {"보통": "주요 결정과 그 결과"}, - "term_event_log": {"보통": "주요 사건 기록"}, - "historical_source_title": {"보통": "사료 보기"}, - "status_loading_report": {"보통": "{year}년 {turn}번째 국면 준비 중..."}, - "status_action_limit_reached": {"보통": "이번 국면에 주요 결정을 모두 내렸습니다. 다음 국면으로 진행하세요."}, - "status_final_event_notice": {"보통": "이 국면은 역사적 결과 보고로 마무리됩니다. 다음 국면으로 진행하세요."}, + "game_title": {"보통": "🎙️ 진실을 찾아서: 현대사 취재기록"}, + "scenario_select_title": {"보통": "📰 취재할 시대를 선택하세요"}, + "scenario_select_button": {"보통": "취재 시작"}, + "dashboard_title": {"보통": "📊 기자 상황판"}, + "dashboard_term": {"보통": "{year}년 {turn}번째 취재일"}, + "term_press_freedom": {"보통": "취재 자유도"}, + "term_scoop_points": {"보��": "특종 점수"}, + "term_reporter_safety": {"보통": "기자 안전도"}, + "term_public_trust": {"보통": "대중 신뢰도"}, + "current_assignment_title": {"보통": "📋 오늘의 취재 지시"}, + "reporter_actions_title": {"보통": "🎤 취재 활동 선택"}, + "action_button_label": {"보통": "{action} (자유도 {cost} 소모 가능성)"}, # 비용 개념 변경 + "action_success_message": {"보통": "'{action}' 취재 완료."}, + "button_next_day": {"보통": "➡️ 다음 날로"}, + "button_restart_assignment": {"보통": "이번 취재 다시 시작"}, + "button_back_to_scenario_select": {"보통": "다른 시대 취재"}, + "assignment_over_title": {"보통": "취재 기간 종료"}, + "assignment_over_subtitle": {"보통": "최종 취재 결과 및 평가"}, + "term_event_log": {"보통": "취재 일지"}, # 이전 게임의 이벤트 로그 + "historical_source_title": {"보통": "참고 자료"}, # 이전 게임의 사료 + "reporter_notebook_title": {"보통": "📝 나의 취재 노트"}, + "article_writing_title": {"보통": "🖋️ 기사 작성"}, + "desk_feedback_title": {"보통": "📢 데스크 피드백"}, + "status_loading_assignment": {"보통": "{year}년 {turn}번째 취재일 준비 중..."}, + "status_actions_taken": {"보통": "오늘의 주요 취재 활동을 마쳤습니다. 기사를 정리하거나 다음 날로 진행하세요."}, "sidebar_title": {"보통": "메뉴"}, - "sidebar_glossary_title": {"보통": "📜 주요 용어 해설"}, - "sidebar_current_source_title": {"보통": "📜 현재 국면 사료"}, - "sidebar_no_source": {"보통": "현재 국면에 제공된 사료가 없습니다."}, - "error_invalid_action": {"보통": "잘못된 행동 선택입니다."}, - "error_event_generation_failed": {"보통": "다음 국면 정보를 가져오는데 실패했습니다."}, - - # --- 4.19 혁명 시나리오 관련 텍스트 --- - "scenario_419_revolution_name": {"보통": "4.19 혁명 (1960)"}, - # 4.19 Turn 1: 3.15 부정선거 - "event_419_t1_title": {"보통": "3.15 부정선거와 민심 이반 (1960.3.15.)"}, - "event_419_t1_desc": {"보통": "제4대 대통령 선거 및 제5대 부통령 선거에서 자유당 정권은 광범위한 부정행위를 자행했습니다. 3인조 또는 5인조 공개투표, 투표함 바꿔치기, 유령 유권자 동원 등 불법이 만연하여 국민들의 분노가 극에 달하고 있습니다. 마산 등 일부 지역에서는 이미 항의 시위가 시작되었습니다."}, - "event_419_t1_source": {"보통": "[사료] 당시 야당 측 주장: '이번 선거는 선거가 아니라 완전한 선거 강도 행위였다. 정부는 국민의 준엄한 심판을 면치 못할 것이다.' (동아일보 1960.3.16. 기사 취지 인용)"}, - "action_419_t1_opt1_text": {"보통": "부정선거 의혹 전면 부인, 불순세력 책동으로 규정"}, - "action_419_t1_opt2_text": {"보통": "일부 지역 소요에 유감 표명, 선거 결과는 합법적임을 강조"}, - "action_419_t1_opt3_text": {"보통": "내무장관 문책 및 일부 지역 재조사 검토 시사 (민심 수습 시도)"}, - # 4.19 Turn 2: 김주열 열사 발견 - "event_419_t2_title": {"보통": "김주열 열사 시신 발견과 분노 확산 (1960.4.11.)"}, - "event_419_t2_desc": {"보통": "3.15 마산 시위 중 실종되었던 마산상고 학생 김주열 군이 눈에 최루탄이 박힌 처참한 모습으로 마산 중앙부두 앞바다에서 발견되었습니다. 이 소식은 전국으로 빠르게 퍼져나가며 국민들의 분노에 기름을 부었습니다. 학생들을 중심으로 시위가 격화될 조짐입니다."}, - "event_419_t2_source": {"보통": "[사료] 부산일보 호외 (1960.4.12.): '마산서 제2시체 떠올라. 눈에 최루탄 박힌 학생 시체. 시위 중 행방불명된 김주열 군으로 판명.'"}, - "action_419_t2_opt1_text": {"보통": "김주열 군 사망에 깊은 유감 표명, 철저한 진상 규명 및 책임자 처벌 약속"}, - "action_419_t2_opt2_text": {"보통": "경찰력 증강, 시위 강경 진압 방침 천명"}, - "action_419_t2_opt3_text": {"보통": "사건 축소 보도 지시, 배후 불순세력 개입 가능성 언급"}, - # 4.19 Turn 3: 고려대생 피습 - "event_419_t3_title": {"보통": "고려대생 피습 사건 (1960.4.18.)"}, - "event_419_t3_desc": {"보통": "김주열 열사 사건 이후 시위가 확산되는 가운데, 국회의사당 앞에서 연좌시위를 마치고 귀교하던 고려대학교 학생들이 정치깡패들에게 무차별 폭행당하는 사건이 발생했습니다. 경찰은 이를 방관했다는 비판에 직면했으며, 학생과 시민들의 분노는 더욱 커지고 있습니다."}, - "event_419_t3_source": {"보통": "[사료] 당시 시위 학생 증언: '우리가 평화적으로 시위를 마치고 돌아가는데 갑자기 괴한들이 쇠파이프와 각목을 휘두르며 덮쳤다. 경찰은 멀리서 보고만 있었다.'"}, - "action_419_t3_opt1_text": {"보통": "정치테러 강력 규탄, 배후세력 발본색원 지시"}, - "action_419_t3_opt2_text": {"보통": "치안 책임자 문책, 학생 대표 면담 추진"}, - "action_419_t3_opt3_text": {"보통": "우발적 충돌로 규정, 과격 시위 자제 촉구"}, - # 4.19 Turn 4: 4.19 혁명 당일 - 경무대 앞 발포 - "event_419_t4_title": {"보통": "피의 화요일, 경무대 앞 발포 (1960.4.19.)"}, - "event_419_t4_desc": {"보통": "서울 시내 대학생과 중고생, 시민들이 대규모 시위를 벌이며 경무대(현 청와대)로 향했습니다. 경찰은 시위대를 향해 무차별 발포하여 수많은 사상자가 발생했습니다. 시가지는 전쟁터를 방불케 하며, 사태는 걷잡을 수 없이 악화되고 있습니다."}, - "event_419_t4_source": {"보통": "[사료] 당시 외신 보도: '서울은 혁명의 도시로 변했다. 학생들은 경찰의 총탄에 쓰러지면서도 '독재 타도'를 외치고 있다.' (AP통신 1960.4.19. 보도 취지)"}, - "action_419_t4_opt1_text": {"보통": "서울 지역 비상계엄 선포, 군 동원하여 질서 회복 시도"}, - "action_419_t4_opt2_text": {"보통": "대국민 담화 발표, 유혈사태 사과 및 정치개혁 약속"}, - "action_419_t4_opt3_text": {"보통": "이승만 대통령 하야 고려 (최후의 수단)"}, - # 4.19 Turn 5: 계엄령 선포와 확산 - "event_419_t5_title": {"보통": "비상계엄 전국 확대와 민심 동요 (1960.4.20~)"}, - "event_419_t5_desc": {"보통": "정부는 서울, 부산, 대구, 광주, 대전 등 주요 도시에 비상계엄을 선포했습니다. 군대가 시내에 진주했지만, 시위는 수그러들지 않고 오히려 전국적으로 확산되는 양상입니다. 일부 군부대에서 동요의 움직임도 감지됩니다."}, - "event_419_t5_source": {"보통": "[사료] 계엄사령부 포고령 제1호: '일체의 옥내외 집회 및 시위를 금한다. 언론, 출판, 보도는 사전 검열을 받아야 한다. 영장 없이 체포, 구금, 수색할 수 있다.'"}, - "action_419_t5_opt1_text": {"보통": "계엄군 동원, 시위 강력 진압 지속"}, - "action_419_t5_opt2_text": {"보통": "정치 원로 및 학생 대표와 대화 시도"}, - "action_419_t5_opt3_text": {"보통": "자유당 주요 간부 사퇴 등 책임지는 모습 연출"}, - # 4.19 Turn 6: 교수단 시위 - "event_419_t6_title": {"보통": "대학교수단 시국선언 및 시위 (1960.4.25.)"}, - "event_419_t6_desc": {"보통": "전국 27개 대학 교수 258명이 '학생들의 피에 보답하라'며 시국선언문을 발표하고 평화적인 시위를 벌였습니다. 지식인 사회의 전면적인 저항 표시는 정권에 큰 타격을 주었으며, 이승만 대통령의 결단을 압박하고 있습니다."}, - "event_419_t6_source": {"보통": "[사료] 교수단 시국선언문 중: '백만 학도의 정의에 불타는 순수한 혈관 속에 용솟음치는 민주주의의 이념을 우리들 지성의 명예를 걸고 수호하자... 이 대통령은 즉시 하야하라.'"}, - "action_419_t6_opt1_text": {"보통": "교수단 대표 면담, 요구사항 청취 및 일부 수용 검토"}, - "action_419_t6_opt2_text": {"보통": "교수단 시위도 불법으로 규정, 해산 시도"}, - "action_419_t6_opt3_text": {"보통": "이기붕 부통령 등 핵심 측근 사퇴 발표"}, - # 4.19 Turn 7: 미국의 압력과 하야 결심 - "event_419_t7_title": {"보통": "미국의 압력과 이승만 대통령의 고뇌 (1960.4.25~)"}, - "event_419_t7_desc": {"보통": "미국 정부는 주한미국대사를 통해 이승만 대통령에게 현 사태에 대한 우려와 함께 평화적 해결을 촉구하는 메시지를 전달했습니다. 사실상의 하야 권고로 해석되며, 이 대통령은 깊은 고뇌에 빠졌습니다. 정권의 운명이 경각에 달렸습니다."}, - "event_419_t7_source": {"보통": "[사료] 매카나기 주한미국대사 이승만 대통령 면담 후 기자회견: '미국 정부는 한국의 현 사태를 매우 우려하고 있으며, 한국 국민의 정당한 불만이 해결되기를 바란다.' (1960.4.25)"}, - "action_419_t7_opt1_text": {"보통": "내각 총사퇴 및 거국중립내각 구성 제안"}, - "action_419_t7_opt2_text": {"보통": "미국의 내정간섭 비판, 자주적 해결 의지 표명"}, - "action_419_t7_opt3_text": {"보통": "하야 결심, 발표 시기 조율"}, - # 4.19 Turn 8: 이승만 대통령 하야 (최종) - "event_419_t8_title": {"보통": "이승만 대통령 하야 발표 (1960.4.26.)"}, - "event_419_t8_desc": {"보통": "국민들의 거센 저항과 국제사회의 압력 속에 이승만 대통령은 결국 하야를 발표했습니다. '국민이 원한다면 대통령직을 사임하겠다'는 성명은 라디오를 통해 전국에 알려졌고, 시민들은 환호했습니다. 이로써 12년간 이어진 자유��� 정권은 막을 내렸습니다."}, - "event_419_t8_source": {"보통": "[사료] 이승만 대통령 하야 성명 (1960.4.26.): '나는 국민이 원한다면 대통령직을 사임할 것이며, 지난번 정부통령 선거에 많은 부정이 있었다 하니 선거를 다시 하도록 지시하였고... 국민이 원한다면 내각책임제 개헌도 할 것이다.'"}, - - # --- 5.18 광주 민주화운동 시나리오 관련 텍스트 --- - "scenario_518_gwangju_name": {"보통": "5.18 광주 민주화운동 (1980)"}, - # 5.18 Turn 1: 5.17 비상계엄 전국 확대 - "event_518_t1_title": {"보통": "5.17 비상계엄 전국 확대 조치 (1980.5.17.)"}, - "event_518_t1_desc": {"보통": "신군부는 국가보위비상대책위원회를 설치하고, 5월 17일 24시를 기해 비상계엄을 제주도를 포함한 전국으로 확대했습니다. 정치활동 금지, 대학 휴교령, 언론 검열 강화 등의 조치가 내려졌으며, 김대중, 김영삼, 김종필 등 주요 정치인들이 체포되거나 가택 연금되었습니다. 광주 지역에서도 긴장감이 고조되고 있습니다."}, - "event_518_t1_source": {"보통": "[사료] 계엄포고 제10호 (1980.5.17.): '1. 모든 정치활동을 중지한다. 옥내외 집회 및 시위를 일체 불허한다. 2. 각 대학(전문대 포함)은 당분간 휴교 조치한다. 3. 언론, 출판, 보도 및 방송은 사전 검열을 받아야 한다.'"}, - "action_518_t1_opt1_text": {"보통": "계엄 조치 불가피성 홍보, 북한 위협 및 사회 혼란 방지 강조"}, - "action_518_t1_opt2_text": {"보통": "광주 지역 주요 인사 예비 검속 및 감시 강화"}, - "action_518_t1_opt3_text": {"보통": "계엄군 추가 투입 준비, 만일의 사태 대비"}, - # 5.18 Turn 2: 5.18 첫날, 전남대 앞 충돌 - "event_518_t2_title": {"보통": "광주, 학생 시위와 계엄군 최초 충돌 (1980.5.18.)"}, - "event_518_t2_desc": {"보통": "휴교령에도 불구하고 전남대학교 학생들이 교문 앞에서 시위를 벌이자, 공수부대원들이 강경 진압에 나섰습니다. 학생들을 무차별 구타하고 연행하는 과정에서 부상자가 속출했습니다. 이 소식이 전해지면서 시민들의 분노가 커지고 있습니다."}, - "event_518_t2_source": {"보통": "[사료] 당시 목격자 증언: '공수부대원들이 교문으로 들어가려는 학생들을 곤봉으로 마구 때렸다. 도망가는 학생들까지 쫓아가서 발로 밟고 군홧발로 찼다. 여학생도 예외는 없었다.'"}, - "action_518_t2_opt1_text": {"보통": "학생 시위 불법 규정, 계엄군 진압 정당성 주장"}, - "action_518_t2_opt2_text": {"보통": "광주 지역 통신 일부 차단, 외부 유언비어 확산 방지"}, - "action_518_t2_opt3_text": {"보통": "공수부대 추가 투입, 시위 초기 강력 제압 지시"}, - # 5.18 Turn 3: 시위 확산과 계엄군 과잉 진압 - "event_518_t3_title": {"보통": "금남로 시위 확산과 계엄군 과잉 진압 (1980.5.19~20.)"}, - "event_518_t3_desc": {"보통": "학생들의 시위에 시민들이 합세하면서 시위 규모가 급격히 커졌습니다. 금남로 일대에서 대규모 시위가 벌어졌고, 계엄군은 장갑차를 동원하고 대검을 착용한 채 시위대를 무자비하게 진압했습니다. 사망자와 부상자가 급증하고 있으며, 시내는 공포 분위기에 휩싸였습니다."}, - "event_518_t3_source": {"보통": "[사료] 당시 외신기자 기록: '군인들은 남녀노소를 가리지 않고 곤봉을 휘둘렀고, 심지어 총검으로 찌르기도 했다. 거리는 피로 물들었고, 시민들의 비명과 울음소리가 끊이지 않았다.' (헨드릭슨 목사의 증언 내용 취지)"}, - "action_518_t3_opt1_text": {"보통": "시위 주동자 색출 및 검거 강화, '폭도' 규정"}, - "action_518_t3_opt2_text": {"보통": "광주 외부와의 교통 및 통신 완전 차단 검토"}, - "action_518_t3_opt3_text": {"보통": "일부 부대 전술적 후퇴 후 재진입 작전 구상"}, - # 5.18 Turn 4: 시민군 등장과 무장 - "event_518_t4_title": {"보통": "시민군 등장과 무장, 계엄군 일시 후퇴 (1980.5.21.)"}, - "event_518_t4_desc": {"보통": "계엄군의 잔혹한 진압에 분노한 시민들이 예비군 무기고와 경찰서를 습격하여 스스로 무장하기 시작했습니다. '시민군'이 조직되어 계엄군과 총격전을 벌였고, 오후에는 계엄군이 광주 외곽으로 일시 철수했습니다. 광주는 시민들의 자치 상태에 놓였습니다."}, - "event_518_t4_source": {"보통": "[사료] 시민군 참여자 수기: '우리는 더 이상 맨손으로 죽을 수 없었다. 가족과 이웃을 지키기 위해 총을 들었다. 이것은 폭도가 아니라 항쟁이다.'"}, - "action_518_t4_opt1_text": {"보통": "광주 완전 봉쇄, 외부 지원 차단"}, - "action_518_t4_opt2_text": {"보통": "시민 대표와 협��� 시도 (무기 반납 조건)"}, - "action_518_t4_opt3_text": {"보통": "최종 진압 작전(상무충정작전) 준비 착수"}, - # 5.18 Turn 5: 해방 광주와 고립 - "event_518_t5_title": {"보통": "해방 광주, 그러나 깊어지는 고립 (1980.5.22~26.)"}, - "event_518_t5_desc": {"보통": "계엄군이 물러난 광주에서는 시민들이 자치적으로 질서를 유지하며 '해방구'를 이루었습니다. 시민들은 매일 도청 앞 분수대에서 궐기대회를 열고 민주주의를 외쳤습니다. 그러나 외부와의 연결은 끊기고 식량과 의약품이 부족해지는 등 고립은 심화되고 있습니다."}, - "event_518_t5_source": {"보통": "[사료] 5월 25일 시민 궐기대회 결의문 중: '우리는 최후의 일인까지 광주를 사수할 것이다. 민주주의를 위해 죽음을 각오한다. 정부는 살인만행을 즉각 중지하고 평화적으로 사태를 해결하라.'"}, - "action_518_t5_opt1_text": {"보통": "선무방송 및 전단 살포 (투항 권고, 이간책)"}, - "action_518_t5_opt2_text": {"보통": "국제 여론 악화 방지, 국내 언론 통제 강화"}, - "action_518_t5_opt3_text": {"보통": "진압 작전 D-day 설정, 군 병력 재배치"}, - # 5.18 Turn 6: 도청 앞 마지막 항전 준비 - "event_518_t6_title": {"보통": "도청 앞 마지막 항전 준비 (1980.5.26.)"}, - "event_518_t6_desc": {"보통": "계엄군의 최종 진압이 임박했다는 소문이 퍼지면서, 도청을 중심으로 한 시민군과 학생들은 결사항전의 의지를 다지고 있습니다. '우리를 넘어서 가라'며 도청에 남은 이들은 비장한 각오로 마지막 밤을 맞이하고 있습니다. 협상의 여지는 거의 사라진 상태입니다."}, - "event_518_t6_source": {"보통": "[사료] 윤상원 열사 (당시 시민군 대변인)의 마지막 방송 (추정): '시민 여러분, 지금 계엄군이 쳐들어오고 있습니다. 우리는 끝까지 싸울 것입니다. 우리를 잊지 말아주십시오.'"}, - "action_518_t6_opt1_text": {"보통": "최후통첩 전달 (새벽까지 무기 반납 시 선처)"}, - "action_518_t6_opt2_text": {"보통": "진압 작전 시간 확정, 최종 점검"}, - "action_518_t6_opt3_text": {"보통": "작전 후 사후 처리 및 언론 발표 내용 준비"}, - # 5.18 Turn 7: 계엄군 최종 진압 작전 (상무충정작전) - "event_518_t7_title": {"보통": "계엄군 최종 진압 작전 개시 (1980.5.27. 새벽)"}, - "event_518_t7_desc": {"보통": "5월 27일 새벽, 계엄군은 탱크를 앞세우고 전남도청을 비롯한 광주 시내로 진입하여 최종 진압 작전을 개시했습니다. 도청을 사수하던 시민군과 학생들은 격렬히 저항했으나, 압도적인 화력 앞에 희생되었습니다. 이로써 열흘간의 광주민주화운동은 비극적으로 막을 내렸습니다."}, - "event_518_t7_source": {"보통": "[사료] 계엄사 발표 (1980.5.27.): '금일 새벽 광주 시내에 잔존한 불순분자 및 폭도들을 완전 소탕하고 질서를 회복하였다. 이 과정에서 일부 폭도들이 사망하고 군경에도 경미한 피해가 있었다.' (당시 정부 발표는 사건을 축소/왜곡함)"}, - # (이후 턴은 사후 처리, 진상 규명 등으로 이어질 수 있으나, 여기서는 진압으로 마무리) - # 5.18 시나리오는 7턴으로 마무리 (max_turns는 10이지만, 주요 사건 흐름상) - "event_518_t8_title": {"보통": "광주, 그 이후 (1980.5.27~)"}, - "event_518_t8_desc": {"보통": "계엄군의 진압으로 광주는 다시 침묵에 잠겼습니다. 수많은 희생자와 부상자, 구속자가 발생했으며, 정부는 사건을 '불순분자와 폭도들의 난동'으로 규정하고 철저한 언론 통제를 통해 진실을 은폐하려 했습니다. 그러나 광주의 진실은 이후 민주화 운동 과정에서 끊임없이 제기되며 한국 사회에 큰 영향을 미치게 됩니다."}, - "event_518_t8_source": {"보통": "[기록] 5.18 민주화운동은 이후 한국 민주주의 발전에 중요한 밑거름이 되었으며, 1987년 6월 항쟁의 동력 중 하나로 작용했습니다. 진상 규명과 책임자 처벌, 명예 회복을 위한 노력은 현재까지도 이어지고 있습니다."}, - - - # --- 6월 민주 항쟁 시나리오 관련 텍스트 --- - "scenario_june_struggle_name": {"보통": "6월 민주 항쟁 (1987)"}, - # 6월 항쟁 Turn 1: 박종철 고문치사 사건 - "event_june_t1_title": {"보통": "박종철 고문치사 사건과 국민적 공분 (1987.1.)"}, - "event_june_t1_desc": {"보통": "서울대생 박종철 군이 경찰 조사 중 고문으로 사망하는 사건이 발생했습니다. 경찰은 '책상을 탁 치니 억 하고 죽었다'며 사인을 단순 쇼크사로 발표했으나, 고문 의혹이 끊이지 않고 있습니다. 이 사건은 전두환 정권의 도덕성에 큰 타격을 주며 국민적 공분을 사고 있습��다."}, - "event_june_t1_source": {"보통": "[사료] 당시 천주교정의구현전국사제단 성명 (1987.5.18.): '박종철 군은 물고문 등 경찰의 야만적 고문으로 사망했다. 우리는 이 사실을 은폐하려는 모든 시도에 맞서 진실을 밝힐 것이다.'"}, - "action_june_t1_opt1_text": {"보통": "경찰 발표 신뢰, 야당과 재야의 정치 공세로 규정"}, - "action_june_t1_opt2_text": {"보통": "유감 표명, 철저한 수사 지시 (형식적)"}, - "action_june_t1_opt3_text": {"보통": "내무장관 및 치안본부장 경질, 진상 규명 의지 표명"}, - # 6월 항쟁 Turn 2: 4.13 호헌 조치 - "event_june_t2_title": {"보통": "4.13 호헌 조치 발표와 민주화 열망 좌절 (1987.4.13.)"}, - "event_june_t2_desc": {"보통": "전두환 대통령이 '현행 헌법에 의한 정부 이양'을 골자로 하는 4.13 호헌 조치를 발표했습니다. 이는 대통령 직선제 개헌을 요구하는 국민적 열망을 정면으로 거부한 것으로, 야당과 재야세력, 학생들은 강력히 반발하고 있습니다. 민주화 시위가 격화될 것으로 예상됩니다."}, - "event_june_t2_source": {"보통": "[사료] 전두환 대통령 특별담화 (4.13 호헌 조치): '본인은 얼마 남지 않은 임기 동안 현재의 헌법에 따라 평화적인 정부 이양과 서울올림픽이라는 양대 국가 대사를 성공적으로 완수하는 데 모든 노력을 경주하고자 한다.'"}, - "action_june_t2_opt1_text": {"보통": "호헌 조치 불가피성 강조, 국론 분열 행위 엄단 경고"}, - "action_june_t2_opt2_text": {"보통": "야당과의 대화 채널 유지, 일부 민주화 조치 검토 시사"}, - "action_june_t2_opt3_text": {"보통": "경찰력 증강, 예상되는 시위 강력 대처 준비"}, - # 6월 항쟁 Turn 3: 이한열 열사 피격 - "event_june_t3_title": {"보통": "이한열 열사, 최루탄 피격 중태 (1987.6.9.)"}, - "event_june_t3_desc": {"보통": "연세대생 이한열 군이 '6.10 국민대회 출정을 위한 연세인 결의대회' 중 경찰이 쏜 최루탄에 머리를 맞아 중태에 빠졌습니다. 이 사건은 다음날 예정된 대규모 시위를 앞두고 국민들의 분노를 극대화시키고 있으며, 정국의 최대 변수로 떠올랐습니다."}, - "event_june_t3_source": {"보통": "[사료] 당시 신문 사진: 최루탄에 맞아 피를 흘리며 다른 학생에게 안겨 있는 이한열 열사의 모습은 6월 항쟁의 상징적인 장면이 되었다."}, - "action_june_t3_opt1_text": {"보통": "불행한 사고 유감 표명, 과격 시위 자제 촉구"}, - "action_june_t3_opt2_text": {"보통": "6.10 국민대회 원천 봉쇄, 강경 진압 방침 재확인"}, - "action_june_t3_opt3_text": {"보통": "야당 대표 긴급 회동, 사태 수습 방안 논의"}, - # 6월 항쟁 Turn 4: 6.10 국민대회와 시위 전국 확산 - "event_june_t4_title": {"보통": "6.10 국민대회, 민주화 시위 전국 확산 (1987.6.10.)"}, - "event_june_t4_desc": {"보통": "'고문살인 은폐 규탄 및 민주헌법 쟁취 범국민대회'가 경찰의 원천봉쇄에도 불구하고 전국 22개 도시에서 24만여 명이 참여한 가운데 열렸습니다. 서울 명동성당에서는 학생들이 농성에 들어갔으며, 시위는 이후 전국적으로 걷잡을 수 없이 확산되고 있습니다."}, - "event_june_t4_source": {"보통": "[사료] 6.10 국민대회 구호: '호헌 철폐, 독재 타도!', '직선제를 쟁취하자!', '고문 없는 세상에서 살고 싶다!'"}, - "action_june_t4_opt1_text": {"보통": "불법 폭력 시위로 규정, 주동자 전원 검거 지시"}, - "action_june_t4_opt2_text": {"보통": "명동성당 농성 강제 해산 검토"}, - "action_june_t4_opt3_text": {"보통": "여당 내 온건파 통해 야당과 물밑 협상 시도"}, - # 6월 항쟁 Turn 5: 넥타이 부대 등장과 민심 이반 심화 - "event_june_t5_title": {"보통": "넥타이 부대 등장, 중산층 시위 참여 (1987.6.15~)"}, - "event_june_t5_desc": {"보통": "학생 중심의 시위에 일반 사무직 회사원들, 일명 '넥타이 부대'가 합류하기 시작했습니다. 중산층의 시위 참여는 민심 이반이 심각한 수준임을 보여주며, 정권에 큰 부담으로 작용하고 있습니다. 시위 양상도 더욱 격렬해지고 있습니다."}, - "event_june_t5_source": {"보통": "[사료] 당시 시위 참가 회사원 인터뷰: '더 이상 방관할 수 없었다. 내 아이들이 민주주의 사회에서 살게 하고 싶어 거리로 나왔다.'"}, - "action_june_t5_opt1_text": {"보통": "계엄령 선포 검토 등 비상조치 논의"}, - "action_june_t5_opt2_text": {"보통": "민정당 총재(노태우) 통해 대국민 메시지 발표, 민심 수습 시도"}, - "action_june_t5_opt3_text": {"보통": "경제 안정 최우선, 시위로 인한 경제 피해 부각"}, - # 6월 항쟁 Turn 6: 6.26 국민��화대행진 - "event_june_t6_title": {"보통": "6.26 국민평화대행진, 최대 규모 시위 (1987.6.26.)"}, - "event_june_t6_desc": {"보통": "민주헌법쟁취국민운동본부가 주최한 '국민평화대행진'이 전국 33개 도시와 4개 군에서 100만여 명(주최측 추산)이 참여한 가운데 열렸습니다. 이는 6월 항쟁 기간 중 최대 규모의 시위로, 정권의 입지를 극도로 위축시키고 있습니다. 정권의 선택지는 많지 않아 보입니다."}, - "event_june_t6_source": {"보통": "[사료] 국민평화대행진 현장 스케치: '거리는 '독재 타도'와 '직선제 쟁취'를 외치는 시민들로 가득 찼다. 경찰 저지선은 곳곳에서 무너졌고, 시민들은 자동차 경적을 울리며 호응했다.'"}, - "action_june_t6_opt1_text": {"보통": "군 동원 포함한 최후의 수단 고려 (위수령 등)"}, - "action_june_t6_opt2_text": {"보통": "노태우 민정당 대표에게 전권 위임, 사태 수습 방안 모색 지시"}, - "action_june_t6_opt3_text": {"보통": "국민투표를 통한 개헌 방식 제안 (시간 벌기)"}, - # 6월 항쟁 Turn 7: 6.29 선언 (최종) - "event_june_t7_title": {"보통": "6.29 선언 발표, 직선제 개헌 수용 (1987.6.29.)"}, - "event_june_t7_desc": {"보통": "민정당 노태우 대표위원이 대통령 직선제 개헌, 김대중 사면복권, 언론자유 보장 등을 골자로 하는 '시국수습을 위한 특별선언'(6.29 선언)을 발표했습니다. 이는 국민들의 민주화 요구를 대폭 수용한 것으로, 전두환 대통령도 이를 수용할 뜻을 밝혔습니다. 이로써 6월 항쟁은 국민의 승리로 막을 내렸습니다."}, - "event_june_t7_source": {"보통": "[사료] 6.29 선언 주요 내용: '1. 여야 합의하에 조속히 대통령 직선제로 개헌하고... 2. 자유로운 출마와 공정한 경쟁이 보장되는 대통령 선거를 실시한다... 4. 김대중 씨를 사면 복권한다... 8. 언론자유의 창달을 위해 최대한 노력한다.'"}, - - # --- 용어 해설 --- - # 4.19 용어 - "glossary_419_free_party": {"보통": "**자유당 (自由黨):** 1951년 이승만 대통령을 중심으로 창당된 정당. 1960년 4.19 혁명으로 이승만 대통령이 하야하면서 사실상 해체되었다."}, - "glossary_419_315_election": {"보통": "**3.15 부정선거:** 1960년 3월 15일 실시된 제4대 대통령 및 제5대 부통령 선거에서 자유당 정권이 이승만-이기붕 후보를 당선시키기 위해 자행한 대규모 부정선거. 4.19 혁명의 직접적인 도화선이 되었다."}, - "glossary_419_kimjuyeol": {"보통": "**김주열 (金朱烈):** 1943~1960. 마산상업고등학교 학생. 3.15 마산 시위 중 실종되었다가 4월 11일 눈에 최루탄이 박힌 시신으로 발견되어 4.19 혁명을 격화시키는 계기가 되었다."}, - "glossary_419_gyeongmudae": {"보통": "**경무대 (景武臺):** 현재 청와대 자리에 있던 조선시대 건물로, 대한민국 정부 수립 후 대통령 관저 및 집무실로 사용되었다. 4.19 혁명 당시 시위대가 경무대로 향하자 경찰이 발포하여 많은 사상자가 발생했다."}, - "glossary_419_martial_law": {"보통": "**계엄령 (戒嚴令):** 전시, 사변 또는 이에 준하는 국가비상사태 시 법률이 정하는 바에 따라 군사권을 발동하여 치안을 유지할 수 있는 국가긴급권의 하나. 4.19 혁명 당시 정부는 주요 도시에 계엄령을 선포했다."}, - # 5.18 용어 - "glossary_518_new_military": {"보통": "**신군부 (新軍部):** 1979년 10.26 사건 이후 12.12 군사반란을 통해 권력을 장악한 전두환, 노태우 등 하나회 중심의 군부 세력."}, - "glossary_518_517_measure": {"보통": "**5.17 비상계엄 전국 확대 조치:** 1980년 5월 17일 신군부가 비상계엄을 전국으로 확대하고 정치활동 금지, 대학 휴교, 언론 검열 강화 등을 단행한 조치. 5.18 광주 민주화운동의 직접적인 배경이 되었다."}, - "glossary_518_paratroopers": {"보통": "**공수부대 (공수특전여단):** 5.18 광주 민주화운동 당시 광주에 투입되어 학생과 시민들을 과잉 진압한 부대. 잔혹한 진압 방식으로 많은 사상자를 발생시켰다."}, - "glossary_518_geumnamno": {"보통": "**금남로 (錦南路):** 광주광역시의 중심 도로. 5.18 광주 민주화운동 기간 동안 대규모 시위와 계엄군과의 충돌이 벌어진 핵심 장소였다."}, - "glossary_518_citizens_army": {"보통": "**시민군 (市民軍):** 5.18 광주 민주화운동 당시 계엄군의 폭력적인 진압에 맞서 스스로 무장한 광주 시민들. 도청을 중심으로 항쟁을 벌였다."}, - "glossary_518_sangmuchungjeong": {"보통": "**상무충정작전 (尙武忠正作戰):** 1980년 5월 27일 새벽 계엄군이 광주 시내로 재진입하여 전남도청 등을 장악한 군사작전명. 이 작전으��� 5.18 광주 민주화운동은 비극적으로 진압되었다."}, - # 6월 항쟁 용어 - "glossary_june_parkjongchul": {"보통": "**박종철 (朴鍾哲):** 1965~1987. 서울대학교 언어학과 학생. 1987년 1월 경찰의 고문으로 사망하여 6월 민주 항쟁의 도화선이 되었다."}, - "glossary_june_413_measure": {"보통": "**4.13 호헌 조치 (四一三護憲措置):** 1987년 4월 13일 전두환 대통령이 대통령 직선제 개헌 요구를 거부하고 현행 헌법(간선제)을 고수하겠다고 발표한 조치. 국민적 저항을 불러일으켰다."}, - "glossary_june_leehanyeol": {"보통": "**이한열 (李韓烈):** 1966~1987. 연세대학교 경영학과 학생. 1987년 6월 9일 시위 중 경찰이 쏜 최루탄에 맞아 중태에 빠졌다가 7월 5일 사망했다. 그의 죽음은 6월 항쟁을 더욱 확산시키는 계기가 되었다."}, - "glossary_june_610_rally": {"보통": "**6.10 국민대회:** 1987년 6월 10일 '박종철군 고문살인 은폐 규탄 및 민주헌법 쟁취 범국민대회'라는 이름으로 전국적으로 열린 대규모 시위. 6월 항쟁의 본격적인 시작을 알렸다."}, - "glossary_june_myeongdong_cathedral": {"보통": "**명동성당 (明洞聖堂):** 서울 명동에 위치한 천주교 서울대교구 주교좌 성당. 6월 항쟁 당시 시위대의 주요 농성 장소이자 민주화 운동의 상징적 공간이 되었다."}, - "glossary_june_necktie_brigade": {"보통": "**넥타이 부대:** 6월 항쟁 당시 시위에 참여한 일반 사무직 회사원들을 일컫는 말. 중산층의 시위 참여를 상징하며 항쟁의 대중적 확산을 보여주었다."}, - "glossary_june_629_declaration": {"보통": "**6.29 선언:** 1987년 6월 29일 당시 민정당 대표위원 노태우가 발표한 시국 수습 특별 선언. 대통령 직선제 개헌, 김대중 사면복권 등 8개항의 민주화 조치를 내용으로 하며, 6월 항쟁의 실질적인 승리를 의미했다."}, + "sidebar_glossary_title": {"보통": "📰 관련 용어/인물"}, + "sidebar_current_source_title": {"보통": "📎 현재 참고 자료"}, + "sidebar_no_source": {"보통": "현재 참고할 만한 특별 자료가 없습니다."}, + "input_placeholder_keyword": {"보통": "핵심 키워드 입력..."}, + "input_placeholder_sentence": {"보통": "간단한 문장으로 요약..."}, + "button_add_to_notebook": {"보통": "노트에 추가"}, + "button_submit_article": {"보통": "기사 송고"}, + + # --- 4.19 혁명 취재 시나리오 --- + "scenario_419_revolution_name": {"보통": "4.19 혁명 취재 (1960)"}, + # 4.19 Turn 1: 3.15 부정선거 당일 + "event_419_t1_assignment": {"보통": "3.15 정부통령 선거일. 전국 투표소 상황 및 개표 과정에서 부정행위 정황을 포착하고 관련자 증언을 확보하라."}, + "event_419_t1_source": {"보통": "[배경] 자유당 정권의 장기집권을 위한 조직적인 선거 개입이 예상됨. 야당 및 시민사회는 감시 활동을 벌이고 있음."}, + "action_419_t1_opt1_text": {"보통": "주요 투표소 잠입 취재 (위험도 높음)"}, + "action_419_t1_opt2_text": {"보통": "야당 선거 감시단 동행 취재"}, + "action_419_t1_opt3_text": {"보통": "경찰 및 선관위 관계자 익명 인터뷰 시도"}, + "info_419_t1_opt1_got": {"보통": "정보: 투표함 바꿔치기 현장 목격. (키워드: 투표함, 바꿔치기, 자유당)"}, + "info_419_t1_opt2_got": {"보통": "정보: 야당 감시단, '3인조 공개투표' 등 다수 부정 사례 제보. (키워드: 공개투표, 협박, 감시단)"}, + "info_419_t1_opt3_got": {"보통": "정보: 익명의 경찰, '윗선 지시로 어쩔 수 없었다' 토로. (키워드: 경찰, 윗선지시, 양심선언)"}, + + "desk_feedback_419_t1_positive": {"보통": "데스크: 훌륭해! 이 정도면 특종감이야. 사실 확인 철저히 해서 바로 기사 올리게."}, + "desk_feedback_419_t1_negative": {"보통": "데스크: 아직 확실한 물증이 부족하군. 좀 더 파고들어야겠어. 괜히 긁어 부스럼 만들지 말고 신중하게 접근하게."}, + "desk_feedback_419_t1_pressure": {"보통": "데스크: (한숨) 이런 민감한 시기에는... 알지? 너무 정권에 불리한 기사는 우리도 곤란해. 적당히 수위 조절하게."}, + + # (이하 5.18, 6월 항쟁 관련 텍스트도 유사하게 기자 컨셉으로 추가) + # 예시: 5.18 + "scenario_518_gwangju_name": {"보통": "5.18 광주 현장 취재 (1980)"}, + "event_518_t1_assignment": {"보통": "5월 17일, 비상계엄 전국 확대. 광주 지역 분위기 및 주요 인사 동향을 파악하고, 계엄군의 움직임을 주시하라."}, + "event_518_t1_source": {"보통": "[배경] 신군부의 권력 장악 가속화. 김대중 등 정치인 연행. 광주 지역 대학가 중심 불온 움직임 감지."}, + "action_518_t1_opt1_text": {"보통": "전남대 등 주요 대학가 ��문 취재"}, + "action_518_t1_opt2_text": {"보통": "광주 시내 주요 기관(도청, 경찰서) 주변 상황 취재"}, + "action_518_t1_opt3_text": {"보통": "지역 유지 및 재야인사 접촉 시도 (보안 유의)"}, + "info_518_t1_opt1_got": {"보통": "정보: 전남대 학생들, '휴교령 반대, 계엄 철폐' 등 구호 준비. (키워드: 전남대, 학생시위, 계엄반대)"}, + + # 예시: 6월 항쟁 + "scenario_june_struggle_name": {"보통": "6월 항쟁 동행 취재 (1987)"}, + "event_june_t1_assignment": {"보통": "박종철 고문치사 사건 발생. 경찰 발표의 허점을 찾아내고, 사건의 진실을 규명할 단서를 확보하라."}, + "event_june_t1_source": {"보통": "[배경] 경찰, '단순 쇼크사' 발표. 그러나 고문 의혹 확산. 천주교정의구현사제단 등 진실 규명 움직임."}, + "action_june_t1_opt1_text": {"보통": "사건 담당 경찰서 주변 탐문 및 관계자 접촉"}, + "action_june_t1_opt2_text": {"보통": "박종철 군 유족 및 학교 친구 인터뷰"}, + "action_june_t1_opt3_text": {"보통": "의료계 전문가 익명 자문 (사인 관련)"}, + "info_june_t1_opt3_got": {"보통": "정보: 익명의 의사, '경찰 발표 사인은 의학적으로 이해하기 어렵다. 외부 충격 가능성 배제 못해.' (키워드: 의료자문, 타살의혹, 고문)"}, + + # 기사 작성 관련 텍스트 + "article_template_headline_placeholder": {"보통": "기사 제목을 입력하거나 선택하세요..."}, + "article_template_body_placeholder": {"보통": "취재 노트 내용을 바탕으로 기사 본문 핵심을 작성하세요..."}, + "article_tone_fact_based": {"보통": "객관적 사실 전달 위주"}, + "article_tone_critical": {"보통": "정부/기관 비판적 논조"}, + "article_tone_sympathetic": {"보통": "피해자/시위대 동정적 논조"}, + "article_tone_cautious": {"보통": "신중하고 유보적인 논조"}, } # --- 어휘 조정 함수 (기존과 동일) --- @@ -273,11 +136,10 @@ def get_text(key, level="보통"): return level_texts[level] elif "보통" in level_texts: return level_texts["보통"] - # st.warning(f"텍스트 키 '{key}'에 대한 정의를 찾을 수 없습니다. 키를 그대로 반환합니다.") # 디버깅용 return key -# --- 게임 상태 초기화 (시나리오 기반으로 변경) --- -def initialize_scenario_state(scenario_key): +# --- 게임 상태 초기화 (기자 컨셉) --- +def initialize_reporter_scenario_state(scenario_key): scenario_settings = SCENARIOS[scenario_key] vocab_level = scenario_settings["vocab_level"] get_text_func = lambda k: get_text(k, vocab_level) @@ -289,297 +151,269 @@ def initialize_scenario_state(scenario_key): 'max_turns': scenario_settings['max_turns'], 'game_year': scenario_settings['start_year'], 'status': { - 'approval_rating': scenario_settings['initial_approval'], - 'political_capital': scenario_settings['initial_capital'], - 'stability': scenario_settings['initial_stability'], - 'resistance': scenario_settings['initial_resistance'], + 'press_freedom': scenario_settings['initial_press_freedom'], + 'scoop_points': scenario_settings['initial_scoop_points'], + 'reporter_safety': scenario_settings['initial_reporter_safety'], + 'public_trust': scenario_settings['initial_public_trust'], }, - 'event_log': [f"{get_text_func(f'scenario_{scenario_key}_name')} ({scenario_settings['player_role']}) 시작"], - 'historical_events_completed': [] + 'reporter_notebook': [], # 취재한 정보 조각(텍스트) 저장 + 'submitted_articles': [], # 제출한 기사 정보 저장 + 'event_log': [f"{get_text_func(f'scenario_{scenario_key}_name')} ({scenario_settings['player_role']}) 취재 시작"], } -# --- 역사 이벤트 데이터 --- -HISTORICAL_EVENTS = { +# --- 역사 이벤트 데이터 (기자 컨셉) --- +# 각 턴은 "취재 지시"와 "취재 활동 선택지"로 구성 +# 선택 결과로 "정보 조각" 획득 +HISTORICAL_ASSIGNMENTS = { "4.19_revolution": [ - {"turn": 1, "title_key": "event_419_t1_title", "description_key": "event_419_t1_desc", "source_key": "event_419_t1_source", - "options": [ - {"action_key": "action_419_t1_opt1_text", "cost": 1, "impact": {"approval": -10, "stability": -5, "resistance": +15, "political_capital": +1}}, - {"action_key": "action_419_t1_opt2_text", "cost": 2, "impact": {"approval": -5, "stability": 0, "resistance": +10}}, - {"action_key": "action_419_t1_opt3_text", "cost": 3, "impact": {"approval": +5, "stability": +5, "resistance": -5, "political_capital": -2}}, - ]}, - {"turn": 2, "title_key": "event_419_t2_title", "description_key": "event_419_t2_desc", "source_key": "event_419_t2_source", - "options": [ - {"action_key": "action_419_t2_opt1_text", "cost": 2, "impact": {"approval": +5, "stability": -5, "resistance": +5}}, - {"action_key": "action_419_t2_opt2_text", "cost": 1, "impact": {"approval": -15, "stability": -10, "resistance": +20, "political_capital": 0}}, - {"action_key": "action_419_t2_opt3_text", "cost": 0, "impact": {"approval": -10, "stability": -5, "resistance": +15}}, - ]}, - {"turn": 3, "title_key": "event_419_t3_title", "description_key": "event_419_t3_desc", "source_key": "event_419_t3_source", - "options": [ - {"action_key": "action_419_t3_opt1_text", "cost": 2, "impact": {"approval": +3, "stability": -3, "resistance": +8}}, - {"action_key": "action_419_t3_opt2_text", "cost": 3, "impact": {"approval": +7, "stability": +2, "resistance": -3, "political_capital": -1}}, - {"action_key": "action_419_t3_opt3_text", "cost": 1, "impact": {"approval": -8, "stability": -7, "resistance": +12}}, - ]}, - {"turn": 4, "title_key": "event_419_t4_title", "description_key": "event_419_t4_desc", "source_key": "event_419_t4_source", - "options": [ - {"action_key": "action_419_t4_opt1_text", "cost": 2, "impact": {"approval": -20, "stability": -25, "resistance": +30, "political_capital": +2}}, # 계엄선포 - {"action_key": "action_419_t4_opt2_text", "cost": 3, "impact": {"approval": +10, "stability": +5, "resistance": -15, "political_capital": -2}}, # 정치개혁 약속 - {"action_key": "action_419_t4_opt3_text", "cost": 5, "impact": {"approval": +20, "stability": +20, "resistance": -30, "political_capital": -4}, "ends_scenario": True, "custom_end_message_key": "event_419_t8_title"}, # 하야 - ]}, - {"turn": 5, "title_key": "event_419_t5_title", "description_key": "event_419_t5_desc", "source_key": "event_419_t5_source", - "options": [ - {"action_key": "action_419_t5_opt1_text", "cost": 1, "impact": {"approval": -10, "stability": -15, "resistance": +20}}, - {"action_key": "action_419_t5_opt2_text", "cost": 2, "impact": {"approval": +5, "stability": 0, "resistance": -5, "political_capital": -1}}, - {"action_key": "action_419_t5_opt3_text", "cost": 3, "impact": {"approval": +8, "stability": +3, "resistance": -8, "political_capital": -2}}, - ]}, - {"turn": 6, "title_key": "event_419_t6_title", "description_key": "event_419_t6_desc", "source_key": "event_419_t6_source", - "options": [ - {"action_key": "action_419_t6_opt1_text", "cost": 2, "impact": {"approval": +5, "stability": 0, "resistance": -10, "political_capital": -1}}, - {"action_key": "action_419_t6_opt2_text", "cost": 1, "impact": {"approval": -15, "stability": -10, "resistance": +15}}, - {"action_key": "action_419_t6_opt3_text", "cost": 3, "impact": {"approval": +10, "stability": +5, "resistance": -12, "political_capital": -2}}, - ]}, - {"turn": 7, "title_key": "event_419_t7_title", "description_key": "event_419_t7_desc", "source_key": "event_419_t7_source", - "options": [ - {"action_key": "action_419_t7_opt1_text", "cost": 4, "impact": {"approval": +15, "stability": +10, "resistance": -20, "political_capital": -3}}, - {"action_key": "action_419_t7_opt2_text", "cost": 1, "impact": {"approval": -5, "stability": -5, "resistance": +5}}, - {"action_key": "action_419_t7_opt3_text", "cost": 2, "impact": {"approval": 0, "stability": 0, "resistance": 0}, "next_event_immediate_key": "event_419_t8_title"}, # 하야 결심, 바로 다음 턴으로 - ]}, - {"turn": 8, "is_final_event": True, "title_key": "event_419_t8_title", "description_key": "event_419_t8_desc", "source_key": "event_419_t8_source", "options": []} + { # Turn 1 + "turn": 1, + "assignment_key": "event_419_t1_assignment", + "source_key": "event_419_t1_source", + "options": [ + {"action_key": "action_419_t1_opt1_text", "cost_freedom_risk": 10, "safety_risk": 20, "scoop_potential": 15, "info_key": "info_419_t1_opt1_got"}, + {"action_key": "action_419_t1_opt2_text", "cost_freedom_risk": 5, "safety_risk": 5, "scoop_potential": 10, "info_key": "info_419_t1_opt2_got"}, + {"action_key": "action_419_t1_opt3_text", "cost_freedom_risk": 5, "safety_risk": 10, "scoop_potential": 8, "info_key": "info_419_t1_opt3_got"}, + ], + "article_writing_phase": True, # 이 턴 종료 후 기사 작성 + "desk_feedback_keys": { # 기사 톤에 따른 피드백 키 + "fact_based": "desk_feedback_419_t1_positive", + "critical": "desk_feedback_419_t1_pressure", + "cautious": "desk_feedback_419_t1_negative" + } + }, + # ... (이후 턴들도 유사하게 구성) ... ], "5.18_gwangju": [ - {"turn": 1, "title_key": "event_518_t1_title", "description_key": "event_518_t1_desc", "source_key": "event_518_t1_source", - "options": [ - {"action_key": "action_518_t1_opt1_text", "cost": 1, "impact": {"approval": +5, "stability": +10, "resistance": +5, "political_capital": +1}}, # 신군부 내부 지지 - {"action_key": "action_518_t1_opt2_text", "cost": 2, "impact": {"approval": -5, "stability": +5, "resistance": +10}}, - {"action_key": "action_518_t1_opt3_text", "cost": 0, "impact": {"approval": 0, "stability": +5, "resistance": +5}}, - ]}, - {"turn": 2, "title_key": "event_518_t2_title", "description_key": "event_518_t2_desc", "source_key": "event_518_t2_source", - "options": [ - {"action_key": "action_518_t2_opt1_text", "cost": 0, "impact": {"approval": -10, "stability": -5, "resistance": +15}}, - {"action_key": "action_518_t2_opt2_text", "cost": 1, "impact": {"approval": -5, "stability": 0, "resistance": +10}}, - {"action_key": "action_518_t2_opt3_text", "cost": 2, "impact": {"approval": -15, "stability": -10, "resistance": +25, "political_capital": +1}}, - ]}, - {"turn": 3, "title_key": "event_518_t3_title", "description_key": "event_518_t3_desc", "source_key": "event_518_t3_source", - "options": [ - {"action_key": "action_518_t3_opt1_text", "cost": 1, "impact": {"approval": -20, "stability": -15, "resistance": +30}}, - {"action_key": "action_518_t3_opt2_text", "cost": 2, "impact": {"approval": -10, "stability": -10, "resistance": +20, "political_capital": -1}}, - {"action_key": "action_518_t3_opt3_text", "cost": 3, "impact": {"approval": -5, "stability": +5, "resistance": +10, "political_capital": 0}}, # 전술적 후퇴 - ]}, - {"turn": 4, "title_key": "event_518_t4_title", "description_key": "event_518_t4_desc", "source_key": "event_518_t4_source", - "options": [ - {"action_key": "action_518_t4_opt1_text", "cost": 1, "impact": {"approval": -5, "stability": -10, "resistance": +15}}, - {"action_key": "action_518_t4_opt2_text", "cost": 3, "impact": {"approval": +5, "stability": +5, "resistance": -10, "political_capital": -2}}, # 협상 시도 - {"action_key": "action_518_t4_opt3_text", "cost": 2, "impact": {"approval": 0, "stability": 0, "resistance": +5, "political_capital": +1}}, # 진압 준비 - ]}, - {"turn": 5, "title_key": "event_518_t5_title", "description_key": "event_518_t5_desc", "source_key": "event_518_t5_source", - "options": [ - {"action_key": "action_518_t5_opt1_text", "cost": 1, "impact": {"approval": -3, "stability": 0, "resistance": +5}}, - {"action_key": "action_518_t5_opt2_text", "cost": 2, "impact": {"approval": +2, "stability": +5, "resistance": -3, "political_capital": 0}}, - {"action_key": "action_518_t5_opt3_text", "cost": 0, "impact": {"approval": 0, "stability": 0, "resistance": 0}}, - ]}, - {"turn": 6, "title_key": "event_518_t6_title", "description_key": "event_518_t6_desc", "source_key": "event_518_t6_source", - "options": [ - {"action_key": "action_518_t6_opt1_text", "cost": 1, "impact": {"approval": -5, "stability": -5, "resistance": +5}}, # 최후통첩 - {"action_key": "action_518_t6_opt2_text", "cost": 0, "impact": {"approval": 0, "stability": 0, "resistance": 0}, "next_event_immediate_key": "event_518_t7_title"}, # 진압 확정 - {"action_key": "action_518_t6_opt3_text", "cost": 0, "impact": {"approval": 0, "stability": 0, "resistance": 0}}, - ]}, - {"turn": 7, "title_key": "event_518_t7_title", "description_key": "event_518_t7_desc", "source_key": "event_518_t7_source", "options": [], "next_event_immediate_key": "event_518_t8_title"}, # 진압 후 바로 다음 턴(결과) - {"turn": 8, "is_final_event": True, "title_key": "event_518_t8_title", "description_key": "event_518_t8_desc", "source_key": "event_518_t8_source", "options": []} - # 5.18은 8턴으로 마무리 (max_turns는 10이지만 주요 사건 흐름상) + { "turn": 1, "assignment_key": "event_518_t1_assignment", "source_key": "event_518_t1_source", + "options": [ + {"action_key": "action_518_t1_opt1_text", "cost_freedom_risk": 15, "safety_risk": 25, "scoop_potential": 10, "info_key": "info_518_t1_opt1_got"}, + # ... + ], + "article_writing_phase": False # 초기에는 정보 수집만 + }, ], "june_struggle": [ - {"turn": 1, "title_key": "event_june_t1_title", "description_key": "event_june_t1_desc", "source_key": "event_june_t1_source", - "options": [ - {"action_key": "action_june_t1_opt1_text", "cost": 0, "impact": {"approval": -10, "stability": -5, "resistance": +15}}, - {"action_key": "action_june_t1_opt2_text", "cost": 1, "impact": {"approval": -5, "stability": 0, "resistance": +10}}, - {"action_key": "action_june_t1_opt3_text", "cost": 3, "impact": {"approval": +5, "stability": +5, "resistance": -10, "political_capital": -2}}, - ]}, - {"turn": 2, "title_key": "event_june_t2_title", "description_key": "event_june_t2_desc", "source_key": "event_june_t2_source", - "options": [ - {"action_key": "action_june_t2_opt1_text", "cost": 1, "impact": {"approval": -15, "stability": -10, "resistance": +20, "political_capital": +1}}, - {"action_key": "action_june_t2_opt2_text", "cost": 2, "impact": {"approval": -5, "stability": 0, "resistance": +5, "political_capital": -1}}, - {"action_key": "action_june_t2_opt3_text", "cost": 0, "impact": {"approval": -5, "stability": -5, "resistance": +10}}, - ]}, - {"turn": 3, "title_key": "event_june_t3_title", "description_key": "event_june_t3_desc", "source_key": "event_june_t3_source", - "options": [ - {"action_key": "action_june_t3_opt1_text", "cost": 1, "impact": {"approval": -10, "stability": -5, "resistance": +15}}, - {"action_key": "action_june_t3_opt2_text", "cost": 0, "impact": {"approval": -20, "stability": -15, "resistance": +25, "political_capital": +1}}, - {"action_key": "action_june_t3_opt3_text", "cost": 3, "impact": {"approval": +5, "stability": +5, "resistance": -5, "political_capital": -2}}, - ]}, - {"turn": 4, "title_key": "event_june_t4_title", "description_key": "event_june_t4_desc", "source_key": "event_june_t4_source", - "options": [ - {"action_key": "action_june_t4_opt1_text", "cost": 1, "impact": {"approval": -15, "stability": -10, "resistance": +20}}, - {"action_key": "action_june_t4_opt2_text", "cost": 2, "impact": {"approval": -10, "stability": -15, "resistance": +25, "political_capital": -1}}, # 명동성당 해산 - {"action_key": "action_june_t4_opt3_text", "cost": 2, "impact": {"approval": 0, "stability": 0, "resistance": -5, "political_capital": -1}}, - ]}, - {"turn": 5, "title_key": "event_june_t5_title", "description_key": "event_june_t5_desc", "source_key": "event_june_t5_source", - "options": [ - {"action_key": "action_june_t5_opt1_text", "cost": 3, "impact": {"approval": -25, "stability": -30, "resistance": +35, "political_capital": +2}}, # 계엄령 - {"action_key": "action_june_t5_opt2_text", "cost": 2, "impact": {"approval": +5, "stability": +5, "resistance": -10, "political_capital": -1}}, - {"action_key": "action_june_t5_opt3_text", "cost": 1, "impact": {"approval": -5, "stability": 0, "resistance": +5}}, - ]}, - {"turn": 6, "title_key": "event_june_t6_title", "description_key": "event_june_t6_desc", "source_key": "event_june_t6_source", - "options": [ - {"action_key": "action_june_t6_opt1_text", "cost": 4, "impact": {"approval": -30, "stability": -35, "resistance": +40, "political_capital": +3}}, # 군 동원 - {"action_key": "action_june_t6_opt2_text", "cost": 1, "impact": {"approval": 0, "stability": 0, "resistance": 0}, "next_event_immediate_key": "event_june_t7_title"}, # 노태우에게 위임 -> 6.29로 - {"action_key": "action_june_t6_opt3_text", "cost": 2, "impact": {"approval": -5, "stability": -5, "resistance": +10, "political_capital": -1}}, - ]}, - {"turn": 7, "is_final_event": True, "title_key": "event_june_t7_title", "description_key": "event_june_t7_desc", "source_key": "event_june_t7_source", "options": []} - # 6월 항쟁은 7턴으로 마무리 (max_turns는 10이지만 주요 사건 흐름상) + { "turn": 1, "assignment_key": "event_june_t1_assignment", "source_key": "event_june_t1_source", + "options": [ + # ... + {"action_key": "action_june_t1_opt3_text", "cost_freedom_risk": 5, "safety_risk": 5, "scoop_potential": 12, "info_key": "info_june_t1_opt3_got"}, + ], + "article_writing_phase": True + }, ] } -# --- 다음 역사 이벤트 가져오기 --- -def get_next_historical_event(scenario_key, current_turn, game_state): - scenario_events = HISTORICAL_EVENTS.get(scenario_key, []) - for event_data in scenario_events: - if event_data["turn"] == current_turn: +# --- 다음 취재 지시 가져오기 --- +def get_next_assignment(scenario_key, current_turn, game_state): + scenario_assignments = HISTORICAL_ASSIGNMENTS.get(scenario_key, []) + for assignment_data in scenario_assignments: + if assignment_data["turn"] == current_turn: vocab_level = SCENARIOS[scenario_key]["vocab_level"] - get_text_for_event = lambda k: get_text(k, vocab_level) + get_text_for_assignment = lambda k: get_text(k, vocab_level) - final_event = { - "title": get_text_for_event(event_data["title_key"]), - "description": get_text_for_event(event_data["description_key"]), - "source_text": get_text_for_event(event_data.get("source_key", "")), - "is_final_event": event_data.get("is_final_event", False), + final_assignment = { + "assignment_text": get_text_for_assignment(assignment_data["assignment_key"]), + "source_text": get_text_for_assignment(assignment_data.get("source_key", "")), "options": [], - "ends_scenario": event_data.get("ends_scenario", False), # 조기 종료 플래그 - "custom_end_message_key": event_data.get("custom_end_message_key", ""), - "next_event_immediate_key": event_data.get("next_event_immediate_key", None) # 특정 선택 시 즉시 다음 이벤트로 + "article_writing_phase": assignment_data.get("article_writing_phase", False), + "desk_feedback_keys": assignment_data.get("desk_feedback_keys", {}) } - for opt_data in event_data.get("options", []): - final_event["options"].append({ - "action": get_text_for_event(opt_data["action_key"]), - "cost": opt_data["cost"], - "impact": opt_data.get("impact", {}), - "ends_scenario": opt_data.get("ends_scenario", False), - "custom_end_message_key": opt_data.get("custom_end_message_key", ""), - "next_event_immediate_key": opt_data.get("next_event_immediate_key", None) + for opt_data in assignment_data.get("options", []): + final_assignment["options"].append({ + "action_text": get_text_for_assignment(opt_data["action_key"]), + "cost_freedom_risk": opt_data.get("cost_freedom_risk", 0), + "safety_risk": opt_data.get("safety_risk", 0), + "scoop_potential": opt_data.get("scoop_potential", 0), + "info_key": opt_data.get("info_key", "") }) - log_message = f"{game_state['game_year']}년 {current_turn}번째 국면: {final_event['title']}" - if game_state.get('event_log') is not None: # 안전장치 + log_message = f"{game_state['game_year']}년 {current_turn}번째 취재일: {final_assignment['assignment_text'][:30]}..." # 로그 간략화 + if game_state.get('event_log') is not None: game_state['event_log'].append(log_message) - return final_event + return final_assignment return None -# --- 이벤트 브리핑 제공 함수 --- -def provide_historical_event_briefing(event, game_state): +# --- 취재 활동 결과 처리 --- +def process_reporter_action(selected_option, game_state): scenario_key = game_state['scenario_key'] vocab_level = SCENARIOS[scenario_key]["vocab_level"] - get_text_for_briefing = lambda k: get_text(k, vocab_level) - - briefing = f"**{get_text_for_briefing('dashboard_term').format(year=game_state['game_year'], turn=game_state['current_turn'])} 보고: {event['title']}**\n\n" - briefing += f"{event['description']}\n\n" - if event.get('source_text'): - briefing += f"**{get_text_for_briefing('historical_source_title')}**\n" - briefing += f"> {event['source_text']}\n\n" - - briefing += f"**{get_text_for_briefing('player_options_title')}**\n" - options_data = [] - for i, opt in enumerate(event.get("options", [])): - cost = opt['cost'] - action_text = opt['action'] - - cost_display_text = get_text_for_briefing('action_button_label').split('(')[-1].split(')')[0].replace('{cost}', str(cost)) + get_text_for_info = lambda k: get_text(k, vocab_level) + status = game_state['status'] - briefing += f"{i+1}. **{action_text}** ({cost_display_text})\n" + # 취재 자유도 및 안전도 감소 (확률적 또는 고정적) + if random.random() < (selected_option.get("cost_freedom_risk", 0) / 100): + freedom_loss = random.randint(1, 5) + status['press_freedom'] -= freedom_loss + game_state['event_log'].append(f" - 취재 중 제약 발생, 취재 자유도 {freedom_loss} 감소.") + if random.random() < (selected_option.get("safety_risk", 0) / 100): + safety_loss = random.randint(1, 10) + status['reporter_safety'] -= safety_loss + game_state['event_log'].append(f" - 취재 중 신변 위협 감지, 기자 안전도 {safety_loss} 감소.") + + # 특종 점수 획득 + scoop_gain = selected_option.get("scoop_potential", 0) + if scoop_gain > 0 and random.random() < 0.7: # 70% 확률로 특종 점수 획득 + actual_scoop = random.randint(scoop_gain // 2, scoop_gain) + status['scoop_points'] += actual_scoop + game_state['event_log'].append(f" - 결정적 단서 포착! 특종 점수 {actual_scoop} 획득.") + + # 정보 조각 획득 및 노트에 추가 + info_text = "" + if selected_option.get("info_key"): + info_text = get_text_for_info(selected_option["info_key"]) + game_state['reporter_notebook'].append(info_text) + st.success(f"정보 획득: {info_text}") + game_state['event_log'].append(f" - 정보 획득: {info_text[:50]}...") + else: + st.info("특별한 정보는 얻지 못했습니다.") + game_state['event_log'].append(f" - 특별한 정보는 얻지 못함.") + + # 지표 범위 유지 + status['press_freedom'] = max(0, min(100, status['press_freedom'])) + status['reporter_safety'] = max(0, min(100, status['reporter_safety'])) + + st.session_state['actions_taken_this_turn'] += 1 + return info_text - effects = [] - impact = opt.get("impact", {}) - if 'approval' in impact: effects.append(f"{get_text_for_briefing('term_approval')} {impact['approval']:+}%p") - if 'stability' in impact: effects.append(f"{get_text_for_briefing('term_stability')} {impact['stability']:+}%p") - if 'resistance' in impact: effects.append(f"{get_text_for_briefing('term_resistance')} {impact['resistance']:+}%p") - if 'political_capital' in impact and impact['political_capital'] != 0 : effects.append(f"{get_text_for_briefing('term_political_capital')} {impact['political_capital']:+}") +# --- 기사 작성 인터페이스 생성 --- +def generate_article_interface(game_state, vocab_level): + get_text_func = lambda k: get_text(k, vocab_level) + st.subheader(get_text_func("article_writing_title")) + + # 취재 노트 보여주기 + st.markdown("**취재 노트 내용:**") + if game_state['reporter_notebook']: + for idx, note in enumerate(game_state['reporter_notebook']): + st.markdown(f"- {note}") + else: + st.caption("아직 취재한 내용이 없습니다.") + + st.markdown("---") - if effects: briefing += f" - *예상 결과:* {', '.join(effects)}\n" - options_data.append(opt) - return {"text": briefing, "options": options_data} + # 기사 제목 입력/선택 (예시) + # 실제로는 취재 내용 기반 추천 제목 등을 제공할 수 있음 + article_headline = st.text_input(get_text_func("article_template_headline_placeholder"), key="article_headline_input") -# --- 플레이어 행동 실행 함수 --- -def execute_player_action(action_index, briefing_data, game_state): - scenario_key = game_state['scenario_key'] - vocab_level = SCENARIOS[scenario_key]["vocab_level"] - get_text_for_exec = lambda k: get_text(k, vocab_level) - - options = briefing_data.get("options", []) - if not (0 <= action_index < len(options)): - st.error(get_text_for_exec("error_invalid_action")) - return False, None, None # 성공여부, 조기종료여부, 다음 이벤트 키 - - selected_action = options[action_index] - action_name = selected_action['action'] - cost = selected_action['cost'] - impact = selected_action.get('impact', {}) - current_status = game_state['status'] - - if current_status['political_capital'] < cost: - st.warning(get_text_for_exec('action_cost_warning') + f" (필요: {cost}, 보유: {current_status['political_capital']})") - return False, None, None - - current_status['political_capital'] -= cost - log_message = f" - 정부 결정({game_state['current_turn']}국면): '{action_name}' ({get_text_for_exec('term_political_capital')} {cost} 소모)" - if game_state.get('event_log') is not None: - game_state['event_log'].append(log_message) - st.success(get_text_for_exec('action_success_message').format(action=action_name)) - - log_parts = [] - if 'approval' in impact: - current_status['approval_rating'] += impact['approval'] - log_parts.append(f"{get_text_for_exec('term_approval')} {impact['approval']:+}%p") - if 'stability' in impact: - current_status['stability'] += impact['stability'] - log_parts.append(f"{get_text_for_exec('term_stability')} {impact['stability']:+}%p") - if 'resistance' in impact: - current_status['resistance'] += impact['resistance'] - log_parts.append(f"{get_text_for_exec('term_resistance')} {impact['resistance']:+}%p") - if 'political_capital' in impact and impact['political_capital'] !=0: - current_status['political_capital'] += impact['political_capital'] - log_parts.append(f"{get_text_for_exec('term_political_capital')} {impact['political_capital']:+}") - - if log_parts and game_state.get('event_log') is not None: - game_state['event_log'].append(f" - 결과: {', '.join(log_parts)} 변동.") - - current_status['approval_rating'] = max(0, min(100, current_status['approval_rating'])) - current_status['stability'] = max(0, min(100, current_status['stability'])) - current_status['resistance'] = max(0, min(100, current_status['resistance'])) - current_status['political_capital'] = max(0, current_status['political_capital']) + # 기사 본문 핵심 내용 입력 (예시) + article_body_summary = st.text_area(get_text_func("article_template_body_placeholder"), height=150, key="article_body_input") - st.session_state['actions_taken_this_turn'] += 1 - - ends_scenario = selected_action.get("ends_scenario", False) - custom_end_message_key = selected_action.get("custom_end_message_key", "") - next_event_key = selected_action.get("next_event_immediate_key", None) + # 기사 논조 선택 + article_tones = { + "fact_based": get_text_func("article_tone_fact_based"), + "critical": get_text_func("article_tone_critical"), + "sympathetic": get_text_func("article_tone_sympathetic"), + "cautious": get_text_func("article_tone_cautious"), + } + selected_tone_key = st.selectbox( + "기사 논조를 선택하세요:", + options=list(article_tones.keys()), + format_func=lambda key: article_tones[key], + key="article_tone_select" + ) + + if st.button(get_text_func("button_submit_article"), key="submit_article_button"): + if not article_headline or not article_body_summary: + st.warning("기사 제목과 핵심 내용을 모두 작성해주세요.") + return None + + submitted_article = { + "turn": game_state['current_turn'], + "headline": article_headline, + "body_summary": article_body_summary, + "tone": selected_tone_key, + "raw_notes": list(game_state['reporter_notebook']) # 당시 노트 스냅샷 + } + # 입력 필드 초기화를 위해 st.session_state 직접 수정 (더 나은 방법은 form 사용) + # st.session_state.article_headline_input = "" + # st.session_state.article_body_input = "" + return submitted_article + return None - if ends_scenario: - st.session_state.game_mode = 'scenario_over' - if custom_end_message_key and game_state.get('event_log') is not None: - game_state['event_log'].append(f"--- {get_text(custom_end_message_key, vocab_level)} ---") - elif game_state.get('event_log') is not None: - game_state['event_log'].append(f"--- {SCENARIOS[scenario_key]['display_name']} 종료 (플레이어 선택) ---") - return True, True, next_event_key # 성공, 시나리오 종료, 다음 이벤트 키 +# --- 기사 평가 및 데스크 피드백 --- +def evaluate_article_and_get_feedback(article, game_state, assignment_data): + status = game_state['status'] + vocab_level = SCENARIOS[game_state['scenario_key']]["vocab_level"] + get_text_func = lambda k: get_text(k, vocab_level) - return True, False, next_event_key # 성공, 시나리오 계속, 다음 이벤트 키 + feedback_text = "데스크: 기사 잘 봤네. " + trust_change = 0 + freedom_change = 0 + safety_change = 0 + + # 논조에 따른 기본 피드백 및 영향 + desk_feedback_keys = assignment_data.get("desk_feedback_keys", {}) + specific_feedback_key = desk_feedback_keys.get(article['tone']) + if specific_feedback_key: + feedback_text += get_text_func(specific_feedback_key).replace("데스크: ", "") # 중복 제거 + + if article['tone'] == "critical": + trust_change += random.randint(5, 10) # 비판적 기사에 대한 대중 신뢰 상승 + freedom_change -= random.randint(5, 15) # 정부 압박으로 취재 자유도 하락 + safety_change -= random.randint(3, 10) # 안전도 하락 + feedback_text += " 파장이 클 것 같으니 신변 조심하게." + elif article['tone'] == "fact_based": + trust_change += random.randint(2, 7) + feedback_text += " 균형감이 좋군." + elif article['tone'] == "sympathetic": + trust_change += random.randint(3, 8) + safety_change -= random.randint(1, 5) + feedback_text += " 감성적인 접근이 독자들에게 와닿을 수 있겠어." + elif article['tone'] == "cautious": + trust_change -= random.randint(1, 5) # 너무 신중하면 신뢰도 하락 가능성 + freedom_change += random.randint(1, 3) # 정부와의 마찰 회피 + feedback_text += " 좀 더 과감해도 좋았을 텐데." + + # 특종 점수 반영 (예시) + if status['scoop_points'] > (game_state['current_turn'] * 5): # 턴 대비 높은 특종 점수 + trust_change += status['scoop_points'] // 5 + feedback_text += f" 이번 특종(점수: {status['scoop_points']}) 덕분에 우리 신문 위상이 올라가겠어!" + status['scoop_points'] = 0 # 특종 점수 사용 + + status['public_trust'] = max(0, min(100, status['public_trust'] + trust_change)) + status['press_freedom'] = max(0, min(100, status['press_freedom'] + freedom_change)) + status['reporter_safety'] = max(0, min(100, status['reporter_safety'] + safety_change)) + + game_state['event_log'].append(f"기사 송고: '{article['headline']}' (논조: {get_text_func(f'article_tone_{article['tone']}')})") + game_state['event_log'].append(f" - 데스크 피드백: {feedback_text}") + if trust_change != 0: game_state['event_log'].append(f" - 대중 신뢰도 {trust_change:+} 변동.") + if freedom_change != 0: game_state['event_log'].append(f" - 취재 자유도 {freedom_change:+} 변동.") + if safety_change != 0: game_state['event_log'].append(f" - 기자 안전도 {safety_change:+} 변동.") + + game_state['submitted_articles'].append(article) + game_state['reporter_notebook'] = [] # 기사 작성 후 노트 비우기 (선택적) + + return feedback_text # --- UI 표시 함수들 --- -def display_dashboard(game_state): +def display_reporter_dashboard(game_state): scenario_key = game_state['scenario_key'] vocab_level = SCENARIOS[scenario_key]["vocab_level"] get_text_for_ui = lambda k: get_text(k, vocab_level) status = game_state['status'] col1, col2, col3, col4 = st.columns(4) - with col1: st.metric(get_text_for_ui("term_approval"), f"{status['approval_rating']}%") - with col2: st.metric(get_text_for_ui("term_political_capital"), f"{status['political_capital']}") - with col3: st.metric(get_text_for_ui("term_stability"), f"{status['stability']} / 100") - with col4: st.metric(get_text_for_ui("term_resistance"), f"{status['resistance']} / 100") + with col1: st.metric(get_text_for_ui("term_public_trust"), f"{status['public_trust']}%") + with col2: st.metric(get_text_for_ui("term_press_freedom"), f"{status['press_freedom']}%") + with col3: st.metric(get_text_for_ui("term_reporter_safety"), f"{status['reporter_safety']}%") + with col4: st.metric(get_text_for_ui("term_scoop_points"), f"{status['scoop_points']}점") -def display_historical_glossary(scenario_key, vocab_level): +def display_reporter_notebook(game_state, vocab_level): + get_text_func = lambda k: get_text(k, vocab_level) + st.subheader(get_text_func("reporter_notebook_title")) + if game_state['reporter_notebook']: + for note in game_state['reporter_notebook']: + st.markdown(f"- {note}") + else: + st.caption("아직 취재 노트에 기록된 내용이 없습니다.") + +def display_historical_glossary_for_reporter(scenario_key, vocab_level): get_text_for_ui = lambda k: get_text(k, vocab_level) st.sidebar.subheader(get_text_for_ui("sidebar_glossary_title")) glossary_keys_for_scenario = [k for k in ALL_TEXTS if k.startswith(f'glossary_{scenario_key}')] if not glossary_keys_for_scenario: - st.sidebar.caption(f"{SCENARIOS[scenario_key]['display_name']} 관련 용어 해설이 아직 없습니다.") + st.sidebar.caption(f"{SCENARIOS[scenario_key]['display_name']} 관련 용어/인물 정보가 아직 없습니다.") for key in glossary_keys_for_scenario: term_definition = get_text_for_ui(key) @@ -591,21 +425,21 @@ def display_historical_glossary(scenario_key, vocab_level): # --- 세션 상태 초기화 --- if 'game_mode' not in st.session_state: - st.session_state.game_mode = 'scenario_select' + st.session_state.game_mode = 'scenario_select' # 'scenario_select', 'assignment_briefing', 'reporter_action', 'article_writing', 'assignment_over' if 'current_scenario_key' not in st.session_state: st.session_state.current_scenario_key = None if 'game_state' not in st.session_state: st.session_state.game_state = None -if 'current_historical_event' not in st.session_state: - st.session_state.current_historical_event = None -if 'event_briefing_data' not in st.session_state: - st.session_state.event_briefing_data = None +if 'current_assignment_data' not in st.session_state: # 현재 턴의 취재 지시/선택지 데이터 + st.session_state.current_assignment_data = None if 'actions_taken_this_turn' not in st.session_state: st.session_state.actions_taken_this_turn = 0 +if 'desk_feedback_message' not in st.session_state: + st.session_state.desk_feedback_message = None -# --- 메인 게임 로직 --- -def historical_simulation_main(): +# --- 메인 게임 로직 (기자 컨셉) --- +def reporter_simulation_main(): vocab_level = "보통" if st.session_state.current_scenario_key: vocab_level = SCENARIOS[st.session_state.current_scenario_key]["vocab_level"] @@ -613,223 +447,230 @@ def historical_simulation_main(): st.title(get_text_main("game_title")) - # --- 시나리오 선택 화면 --- + # --- 시나리오 선택 --- if st.session_state.game_mode == 'scenario_select': st.header(get_text_main("scenario_select_title")) - - scenario_descriptions = { - "4.19_revolution": "1960년, 자유당 정권의 3.15 부정선거에 맞서 일어난 학생과 시민들의 혁명. 당신은 이승만 정부의 입장에서 격동의 시기를 헤쳐나가야 합니다.", - "5.18_gwangju": "1980년 5월, 신군부의 등장과 비상계엄 확대에 저항하여 광주에서 일어난 민주화운동. 당신은 신군부 계엄사령부의 입장에서 역사의 소용돌이와 마주합니다. (주의: 민감한 내용을 다룹니다. 교육적 목적으로 제작되었습니다.)", - "june_struggle": "1987년, 대통령 직선제 개헌을 요구하며 전국적으로 확산된 민주 항쟁. 당신은 전두환 정부의 일원으로서 중대한 결정을 내려야 합니다." + scenario_descriptions = { # 각 시나리오에 대한 기자로서의 설명 + "4.19_revolution": "1960년, 격동의 현장으로! 자유당 정권의 3.15 부정선거 의혹부터 4.19 혁명의 순간까지, 신입 기자로서 진실을 추적하고 역���의 기록자가 되십시오.", + "5.18_gwangju": "1980년 5월, 통제된 도시 광주로... 신군부의 계엄 하에 고립된 광주의 참상을 목격하고, 검열을 뚫고 진실을 알려야 합니다. (주의: 민감한 내용을 다룹니다. 신중한 취재가 요구됩니다.)", + "june_struggle": "1987년, 민주화의 열기가 뜨거운 거리로! 박종철 고문치사부터 6월 항쟁의 함성까지, 독재에 맞선 시민들의 목소리를 담아내십시오." } - for key, scenario_info in SCENARIOS.items(): with st.container(border=True): st.subheader(scenario_info["display_name"]) - st.caption(f"시대: {scenario_info['start_year']}년, 플레이어 역할: {scenario_info['player_role']}") - st.markdown(scenario_descriptions.get(key, "시나리오 설명이 준비되지 않았습니다.")) + st.caption(f"시대: {scenario_info['start_year']}년, 당신의 역할: {scenario_info['player_role']}") + st.markdown(scenario_descriptions.get(key, "이 시대의 취재 임무 설명이 준비되지 않았습니다.")) if st.button(get_text_main("scenario_select_button"), key=f"select_{key}"): st.session_state.current_scenario_key = key - st.session_state.game_state = initialize_scenario_state(key) - st.session_state.current_historical_event = None - st.session_state.event_briefing_data = None + st.session_state.game_state = initialize_reporter_scenario_state(key) + st.session_state.current_assignment_data = None st.session_state.actions_taken_this_turn = 0 - st.session_state.game_mode = 'game_running' + st.session_state.desk_feedback_message = None + st.session_state.game_mode = 'assignment_briefing' # 다음 모드 st.rerun() st.stop() - # --- 게임 진행 중 또는 시나리오 종료 --- - if st.session_state.game_mode in ['game_running', 'scenario_over'] and st.session_state.game_state: + # --- 게임 진행 (취재 또는 기사작성 등) --- + if st.session_state.game_mode != 'scenario_select' and st.session_state.game_state: game_state = st.session_state.game_state scenario_key = st.session_state.current_scenario_key vocab_level = SCENARIOS[scenario_key]["vocab_level"] - get_text_main = lambda key: get_text(key, vocab_level) # get_text_main 업데이트 + get_text_main = lambda key: get_text(key, vocab_level) - st.caption(f"시나리오: {SCENARIOS[scenario_key]['display_name']} | 역할: {game_state['player_role']}") + st.caption(f"취재 중: {SCENARIOS[scenario_key]['display_name']} | 기자: {game_state['player_role']}") - if st.session_state.game_mode == 'scenario_over': + # --- 취재 기간 종료 처리 --- + if st.session_state.game_mode == 'assignment_over': st.balloons() - st.header(get_text_main("game_over_title")) - st.subheader(get_text_main("game_over_subtitle")) - final_status = game_state['status'] - display_dashboard(game_state) # 최종 상태 대시보드 표시 + st.header(get_text_main("assignment_over_title")) + st.subheader(get_text_main("assignment_over_subtitle")) + display_reporter_dashboard(game_state) + + st.subheader("송고한 주요 기사 목록") + if game_state['submitted_articles']: + for article in game_state['submitted_articles']: + st.markdown(f"- **{article['headline']}** (논조: {get_text_main(f'article_tone_{article['tone']}')}) - {article['turn']}일차 송고") + else: + st.caption("이번 취재 기간 동안 송고한 기사가 없습니다.") st.subheader(get_text_main("term_event_log")) log_text = "\n".join(game_state.get('event_log', [])[::-1]) - st.text_area(get_text_main("term_event_log"), log_text, height=300, disabled=True, key="final_log") + st.text_area(get_text_main("term_event_log"), log_text, height=250, disabled=True, key="final_log_reporter") col_btn1, col_btn2 = st.columns(2) with col_btn1: - if st.button(get_text_main("button_restart_scenario"), use_container_width=True): - st.session_state.game_state = initialize_scenario_state(scenario_key) - st.session_state.current_historical_event = None - st.session_state.event_briefing_data = None + if st.button(get_text_main("button_restart_assignment"), use_container_width=True): + st.session_state.game_state = initialize_reporter_scenario_state(scenario_key) + st.session_state.current_assignment_data = None st.session_state.actions_taken_this_turn = 0 - st.session_state.game_mode = 'game_running' + st.session_state.desk_feedback_message = None + st.session_state.game_mode = 'assignment_briefing' st.rerun() with col_btn2: if st.button(get_text_main("button_back_to_scenario_select"), use_container_width=True): st.session_state.game_mode = 'scenario_select' - keys_to_clear = ['current_scenario_key', 'game_state', 'current_historical_event', 'event_briefing_data', 'actions_taken_this_turn'] + # 필요한 세션 상태 정리 + keys_to_clear = ['current_scenario_key', 'game_state', 'current_assignment_data', 'actions_taken_this_turn', 'desk_feedback_message'] for k_to_clear in keys_to_clear: if k_to_clear in st.session_state: del st.session_state[k_to_clear] st.rerun() st.stop() - col_dashboard, col_agenda, col_actions = st.columns([1.2, 1.8, 1.5]) + # --- 메인 레이아웃 (대시보드, 취재내용, 활동/기사작성) --- + col_dashboard, col_main_content, col_actions_notebook = st.columns([1.2, 1.8, 1.5]) with col_dashboard: st.header(get_text_main("dashboard_title")) st.markdown(f"**{get_text_main('dashboard_term').format(year=game_state['game_year'], turn=game_state['current_turn'])}**") - display_dashboard(game_state) - - with col_agenda: - st.header(get_text_main("historical_briefing_title")) - - if st.session_state.current_historical_event is None: - spinner_text = get_text_main('status_loading_report').format(year=game_state['game_year'], turn=game_state['current_turn']) - with st.spinner(spinner_text): - time.sleep(0.5) - event = get_next_historical_event(scenario_key, game_state['current_turn'], game_state) - if event: - st.session_state.current_historical_event = event - st.session_state.event_briefing_data = provide_historical_event_briefing(event, game_state) - st.session_state.actions_taken_this_turn = 0 - st.rerun() - else: - st.session_state.game_mode = 'scenario_over' - if game_state.get('event_log') is not None: - game_state['event_log'].append(f"--- {SCENARIOS[scenario_key]['display_name']}의 모든 국면이 종료되었습니다. ---") - st.rerun() - - if st.session_state.current_historical_event: - event_data = st.session_state.current_historical_event - st.subheader(f"{event_data.get('title', '알 수 없는 현안')}") - st.markdown(event_data.get('description', '')) - - # 사료는 사이드바로 옮김 (본문이 길어지는 것을 방지) - # if event_data.get('source_text'): - # st.info(f"**{get_text_main('historical_source_title')}:** {event_data.get('source_text')}") + display_reporter_dashboard(game_state) + st.divider() + display_reporter_notebook(game_state, vocab_level) # 취재 노트 표시 + + with col_main_content: + # --- 취재 지시 브리핑 --- + if st.session_state.game_mode == 'assignment_briefing': + st.header(get_text_main("current_assignment_title")) + if st.session_state.current_assignment_data is None: # 새 턴 시작 시 + spinner_text = get_text_main('status_loading_assignment').format(year=game_state['game_year'], turn=game_state['current_turn']) + with st.spinner(spinner_text): + time.sleep(0.5) + assignment = get_next_assignment(scenario_key, game_state['current_turn'], game_state) + if assignment: + st.session_state.current_assignment_data = assignment + st.session_state.actions_taken_this_turn = 0 + st.session_state.desk_feedback_message = None # 이전 피드백 초기화 + st.session_state.game_mode = 'reporter_action' # 다음 모드로 변경 + st.rerun() + else: # 더 이상 취재 지시가 없으면 + st.session_state.game_mode = 'assignment_over' + if game_state.get('event_log') is not None: + game_state['event_log'].append(f"--- {SCENARIOS[scenario_key]['display_name']} 취재 기간 종료 ---") + st.rerun() + # (assignment_briefing 모드에서는 특별히 더 표시할 내용이 없을 수 있음. 바로 reporter_action으로 넘어감) + + # --- 취재 활동 --- + if st.session_state.game_mode == 'reporter_action' and st.session_state.current_assignment_data: + assignment_data = st.session_state.current_assignment_data + st.header(get_text_main("current_assignment_title")) + st.subheader(assignment_data['assignment_text']) + if assignment_data.get('source_text'): + st.info(f"**{get_text_main('historical_source_title')}:** {assignment_data['source_text']}") st.divider() - if st.session_state.event_briefing_data: - st.markdown(st.session_state.event_briefing_data.get('text', ''), unsafe_allow_html=True) - else: - st.warning(get_text_main("error_event_generation_failed")) # 브리핑 생성 실패 메시지 활용 - - with col_actions: - st.header(SCENARIOS[scenario_key]['player_role'] + " 집무실") - st.markdown(f"가용 {get_text_main('term_political_capital')}: **{game_state['status']['political_capital']}**") - remaining_actions = QUARTERLY_ACTIONS_LIMIT - st.session_state.actions_taken_this_turn - st.warning(f"이번 국면 주요 결정 잔여 횟수: {remaining_actions}회") - - action_possible = (st.session_state.current_historical_event and - st.session_state.event_briefing_data and - remaining_actions > 0 and - not st.session_state.current_historical_event.get("is_final_event", False) and - len(st.session_state.current_historical_event.get("options", [])) > 0) - - if action_possible: - st.subheader(get_text_main("player_options_title")) - options = st.session_state.event_briefing_data.get("options", []) - for i, opt in enumerate(options): - button_label = get_text_main('action_button_label').format(action=opt.get('action', '알 수 없는 행동'), cost=opt.get('cost', 0)) - button_key = f"action_{game_state['current_turn']}_{i}_{scenario_key}" # 시나리오 키 추가하여 고유성 확보 - is_disabled = (game_state['status']['political_capital'] < opt.get('cost', 0)) - if st.button(button_label, key=button_key, use_container_width=True, disabled=is_disabled): - success, ends_scenario_now, next_event_key_from_action = execute_player_action(i, st.session_state.event_briefing_data, game_state) - if success: - if ends_scenario_now: # 플레이어 선택으로 즉시 종료 - st.rerun() # scenario_over 모드로 전환됨 - elif next_event_key_from_action: # 특정 선택이 다음 이벤트로 즉시 연결 - # 다음 턴으로 강제 이동 및 해당 이벤트 로드 - # 이 로직은 get_next_historical_event에서 next_event_immediate_key를 처리하도록 수정하는 것이 더 깔끔할 수 있음 - # 여기서는 간단히 다음 턴으로 넘기고, 다음 턴 로드 시 해당 이벤트가 나오도록 유도 - # (HISTORICAL_EVENTS 구조에서 해당 이벤트의 turn 번호를 현재 턴+1로 설정하는 방식 등) - # 지금은 일단 일반적인 턴 종료로 처리하고, 다음 턴에 해당 이벤트가 나오도록 설계 - st.session_state.current_historical_event = None # 다음 턴 이벤트 로드하도록 - st.session_state.event_briefing_data = None - # game_state['current_turn'] += 1 # 이 부분은 "다음 국면으로" 버튼에서 처리 - st.rerun() - else: - st.rerun() # 일반적인 액션 후 상태 업데이트 - - elif st.session_state.actions_taken_this_turn >= QUARTERLY_ACTIONS_LIMIT and st.session_state.current_historical_event and not st.session_state.current_historical_event.get("is_final_event", False): - st.info(get_text_main("status_action_limit_reached")) - elif st.session_state.current_historical_event and st.session_state.current_historical_event.get("is_final_event", False): - st.info(get_text_main("status_final_event_notice")) - - st.divider() - if st.button(get_text_main("button_next_turn"), use_container_width=True, key="next_turn_button"): - current_turn = game_state['current_turn'] - max_turns_for_scenario = SCENARIOS[scenario_key]['max_turns'] + st.subheader(get_text_main("reporter_actions_title")) - # 현재 이벤트가 즉시 다음 이��트를 지정했는지 확인 - current_event_data = st.session_state.current_historical_event - next_event_key_from_current_event = None - if current_event_data: - next_event_key_from_current_event = current_event_data.get("next_event_immediate_key") - - if next_event_key_from_current_event: - # 이 경우, HISTORICAL_EVENTS에서 해당 키를 가진 이벤트의 turn을 찾아 current_turn을 그 값으로 설정해야 함. - # 또는, get_next_historical_event가 next_event_key_from_current_event를 우선적으로 찾도록 수정. - # 여기서는 일단 다음 턴으로 넘기고, get_next_historical_event가 처리하도록 가정. - # (실제로는 get_next_historical_event에서 이 로직을 처리하는 것이 더 적합) - # 지금은 단순히 다음 턴으로 진행. - game_state['current_turn'] += 1 - if game_state.get('event_log') is not None: - game_state['event_log'].append(f"--- {game_state['game_year']}년 {game_state['current_turn']}번째 국면(연결) 시작 ---") - - elif current_turn >= max_turns_for_scenario or \ - (st.session_state.current_historical_event and st.session_state.current_historical_event.get("is_final_event", False)): - st.session_state.game_mode = 'scenario_over' - if game_state.get('event_log') is not None: - game_state['event_log'].append(f"--- {SCENARIOS[scenario_key]['display_name']}의 모든 국면이 종료되었습니다. ---") + if st.session_state.actions_taken_this_turn < ACTIONS_PER_TURN_LIMIT: + for i, opt in enumerate(assignment_data['options']): + # 비용 표기 수정: "자유도 10, 안전도 20 감소 가능성" 등으로 표시 가능 + risk_info = [] + if opt.get('cost_freedom_risk',0) > 0: risk_info.append(f"자유도↓{opt['cost_freedom_risk']}%") + if opt.get('safety_risk',0) > 0: risk_info.append(f"안전도↓{opt['safety_risk']}%") + risk_str = ", ".join(risk_info) if risk_info else "위험 낮음" + + button_label = f"{opt['action_text']} (위험도: {risk_str})" + + if st.button(button_label, key=f"rep_action_{game_state['current_turn']}_{i}", use_container_width=True): + process_reporter_action(opt, game_state) + st.rerun() # 상태 변경 후 UI 즉시 업데이트 else: - game_state['current_turn'] += 1 - if game_state.get('event_log') is not None: - game_state['event_log'].append(f"--- {game_state['game_year']}년 {game_state['current_turn']}번째 국면 시작 ---") - - st.session_state.current_historical_event = None - st.session_state.event_briefing_data = None - st.session_state.actions_taken_this_turn = 0 - st.rerun() + st.info(get_text_main("status_actions_taken")) + if assignment_data.get("article_writing_phase"): + if st.button("기사 작성하기", key="go_to_article_writing"): + st.session_state.game_mode = 'article_writing' + st.rerun() + # (기사 작성 안하는 턴이면 바로 다음날로 버튼만 활성화) + + # --- 기사 작성 --- + if st.session_state.game_mode == 'article_writing' and st.session_state.current_assignment_data: + assignment_data = st.session_state.current_assignment_data + submitted_article = generate_article_interface(game_state, vocab_level) + if submitted_article: + feedback = evaluate_article_and_get_feedback(submitted_article, game_state, assignment_data) + st.session_state.desk_feedback_message = feedback + # 기사 작성 후에는 바로 다음 턴 브리핑으로 (또는 reporter_action으로 가서 다음날 버튼 활성화) + st.session_state.game_mode = 'reporter_action' # 데스크 피드백 보여주고 다음날로 넘어갈 수 있도록 + st.rerun() + + # --- 데스크 피드백 표시 --- + if st.session_state.desk_feedback_message and st.session_state.game_mode == 'reporter_action': + st.divider() + st.subheader(get_text_main("desk_feedback_title")) + st.warning(st.session_state.desk_feedback_message) + # 피드백 확인 후에는 메시지 초기화 안 함 (다음 턴 시작 시 초기화) + with col_actions_notebook: # 오른��� 컬럼 (기존 col_actions 위치) + st.header("활동 기록 및 다음 단계") # 임시 제목 + # (이 컬럼은 현재 특별한 인터랙션 요소는 없음. 주로 정보 표시) + + # "다음 날로" 버튼 로직 + # 취재 활동을 다 했거나, 기사 작성을 마쳤거나, 기사 작성 안 하는 턴일 때 활성화 + can_proceed_to_next_day = False + if st.session_state.game_mode == 'reporter_action' and st.session_state.current_assignment_data: + assignment_data = st.session_state.current_assignment_data + if st.session_state.actions_taken_this_turn >= ACTIONS_PER_TURN_LIMIT: + if not assignment_data.get("article_writing_phase"): # 기사작성 없는 턴 + can_proceed_to_next_day = True + # 기사작성 있는 턴이면, 기사 작성 후 (desk_feedback_message가 있을 때) 다음날로 가능 + elif st.session_state.desk_feedback_message: + can_proceed_to_next_day = True + + + if can_proceed_to_next_day: + if st.button(get_text_main("button_next_day"), use_container_width=True, key="next_day_button"): + current_turn = game_state['current_turn'] + max_turns_for_scenario = SCENARIOS[scenario_key]['max_turns'] + + if current_turn >= max_turns_for_scenario: + st.session_state.game_mode = 'assignment_over' + if game_state.get('event_log') is not None: + game_state['event_log'].append(f"--- {SCENARIOS[scenario_key]['display_name']} 취재 기간 종료 ---") + else: + game_state['current_turn'] += 1 + # 연도 변경 로직은 현재 시나리오에서는 불필요 + if game_state.get('event_log') is not None: + game_state['event_log'].append(f"--- {game_state['game_year']}년 {game_state['current_turn']}번째 취재일 시작 ---") + st.session_state.game_mode = 'assignment_briefing' # 다음 턴 지시 받기 + + st.session_state.current_assignment_data = None # 다음 턴 데이터 새로 로드 + st.session_state.actions_taken_this_turn = 0 + # st.session_state.desk_feedback_message = None # 다음 턴 시작 시 초기화됨 + st.rerun() + st.divider() st.subheader(get_text_main("term_event_log")) log_text = "\n".join(game_state.get('event_log', [])[::-1]) - st.text_area(get_text_main("term_event_log"), log_text, height=200, disabled=True, key="event_log_area_main") + st.text_area(get_text_main("term_event_log"), log_text, height=300, disabled=True, key="event_log_area_reporter") + with st.sidebar: st.header(get_text_main("sidebar_title")) - display_historical_glossary(scenario_key, vocab_level) + display_historical_glossary_for_reporter(scenario_key, vocab_level) st.subheader(get_text_main("sidebar_current_source_title")) - if st.session_state.current_historical_event and st.session_state.current_historical_event.get('source_text'): - st.sidebar.info(st.session_state.current_historical_event.get('source_text')) + if st.session_state.current_assignment_data and st.session_state.current_assignment_data.get('source_text'): + st.sidebar.info(st.session_state.current_assignment_data.get('source_text')) else: st.sidebar.caption(get_text_main("sidebar_no_source")) st.divider() - if st.button(get_text_main("button_back_to_scenario_select") + " (다른 시대)", use_container_width=True): + if st.button(get_text_main("button_back_to_scenario_select") + " (다른 시대 취재)", use_container_width=True): st.session_state.game_mode = 'scenario_select' - keys_to_clear = ['current_scenario_key', 'game_state', 'current_historical_event', 'event_briefing_data', 'actions_taken_this_turn'] + keys_to_clear = ['current_scenario_key', 'game_state', 'current_assignment_data', 'actions_taken_this_turn', 'desk_feedback_message'] for k_to_clear in keys_to_clear: if k_to_clear in st.session_state: del st.session_state[k_to_clear] st.rerun() - elif st.session_state.game_mode in ['game_running', 'scenario_over'] and not st.session_state.game_state: - # game_state가 None인 비정상적인 상황 (예: 새로고침 후 세션 일부 유실) + + elif st.session_state.game_mode != 'scenario_select' and not st.session_state.game_state: st.warning("게임 상태 정보가 유실되었습니다. 시나리오 선택 화면으�� 돌아갑니다.") st.session_state.game_mode = 'scenario_select' - keys_to_clear = ['current_scenario_key', 'game_state', 'current_historical_event', 'event_briefing_data', 'actions_taken_this_turn'] + keys_to_clear = ['current_scenario_key', 'game_state', 'current_assignment_data', 'actions_taken_this_turn', 'desk_feedback_message'] for k_to_clear in keys_to_clear: if k_to_clear in st.session_state: del st.session_state[k_to_clear] - time.sleep(2) # 메시지 확인 시간 + time.sleep(1) st.rerun() - if __name__ == "__main__": - historical_simulation_main() \ No newline at end of file + reporter_simulation_main() \ No newline at end of file